Skip to content

feat: retry transient evaluation errors within and across runs before recording them (lfx-cdp-insights-ops#215) - #5008

Merged
ulemons merged 1 commit into
mainfrom
feat/issue-215-transient-evaluation-retries
Oct 16, 2026
Merged

ulemons merged 1 commit into
mainfrom
feat/issue-215-transient-evaluation-retries

Conversation

@ulemons

@ulemons ulemons commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Transient evaluator failures (GitHub or Bedrock not answering, throttling, network, the daily cap) no longer finalize a catalog row on the first attempt. They are retried within the run and, for scheduled rows, across runs with a bound; only then does the row become error with the existing alert. Verdict-level errors (missing config, bad GitHub token, retired model, malformed LLM answer) stay terminal as today.

  • The evaluation API answers 503 instead of a 200 verdict on a transient error. evaluateProject classifies the failure: GithubPublicClientError of kind TRANSIENT or RATE_LIMIT, Bedrock ThrottlingException / ServiceUnavailableException / ModelNotReadyException / ModelTimeoutException / InternalServerException, AWS $fault: 'server' or $retryable, HTTP 429/5xx metadata, and ECONNRESET-style network codes throw the new ServiceUnavailableError (503, @crowd/common). Everything else keeps the evaluationResult: 'error' response.
  • The worker retries in the run. The evaluator client throws a typed TransientEvaluationFailure on a rejected fetch, HTTP 429 or 5xx (with the reason from the API body); the evaluate activity now retries 3 times with a 30s/60s backoff. The daily cap (Daily evaluation limit reached) throws it as non-retryable so no attempt is wasted in the same run.
  • Scheduled rows retry across runs, bounded. When the attempts are exhausted the workflow calls markEvaluationFailed(id, reason, transient): a discovery or API row that has not reached MAX_EVALUATION_RUNS (3) stays in evaluate with evaluationAttempts + 1 and the last reason, and the next run picks it up; the run log counts it as deferred, no alert. After the third failed run the row becomes error with the alert, as today. Bot rows are never deferred: they get error and the Try again button at once, as today. Try again and the manual API reset the counter.
  • Reconciliation, reduced. A "not mainly run on GitHub" verdict on a repository with 100+ closed pull requests is logged as a contradiction and never overridden. The "already onboarded" skip the issue mentions came from the previous external agent; the in-house evaluator has no such verdict, so there is nothing to reconcile there.

Migration V1792160000__project-catalog-evaluation-attempts.sql adds evaluationAttempts SMALLINT NOT NULL DEFAULT 0 to projectCatalog (no index).

Closes linuxfoundation/lfx-cdp-insights-ops#215

Test plan

  • pnpm tsc-check, pnpm lint --deny-warnings
  • Unit tests: backend evaluateProject (503 on transient GitHub/Bedrock errors, terminal errors unchanged, contradiction log), worker evaluator client (typed failure per status, 400 stays a verdict error), canDeferEvaluation, DAL deferProjectCatalogEvaluation (CI)
  • Run the migration, then deploy api and projects-evaluation-worker with no evaluateProjects run in flight
  • Prod: a scheduled run with Bedrock throttling leaves the rows in evaluate with evaluationAttempts = 1 and deferred=N in the run log, no alert; a bot request under the same conditions still gets ⚠️ and Try again
  • Prod: the 23 rows currently in error/unsure with evaluationResult = 'error' can be requeued with the SQL from the issue thread and are evaluated by the next run

🤖 Generated with Claude Code

… recording them

Refs linuxfoundation/lfx-cdp-insights-ops#215

Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
Copilot AI balanced review requested due to automatic review settings October 16, 2026 14:42
@ulemons ulemons self-assigned this Oct 16, 2026
@cursor

cursor Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes failure semantics (503 vs 200 errors) and catalog row lifecycle for the evaluation pipeline; requires DB migration before deploy.

Overview
Transient GitHub/Bedrock/network/throttle failures no longer land as a 200 evaluationResult: 'error' verdict. The public evaluateProject path classifies errors via isTransientEvaluationError and throws ServiceUnavailableError (503); permanent issues (bad token, retired model, malformed LLM output) still return the existing error verdict.

The evaluation worker treats API 429/5xx and fetch failures as TransientEvaluationFailure (Temporal retries: 3 attempts, 30s backoff). markEvaluationFailed can defer scheduled rows: new evaluationAttempts column and deferProjectCatalogEvaluation keep rows in evaluate until MAX_EVALUATION_RUNS (3); Slack-bot rows are never deferred and still get Try again immediately. The workflow logs deferred separately and skips alerts on deferral.

Also adds a warn-only log when the LLM says “not mainly run on GitHub” but the repo has 100+ closed PRs (verdict unchanged). Requeue/manual re-eval resets evaluationAttempts to 0.

Reviewed by Cursor Bugbot for commit 9a54a6c. Bugbot is set up for automated code reviews on this repo. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Retry bookkeeping, timeout handling, execution budgets, and premature API alerts undermine the intended bounded-retry behavior.

4 open findings
What changed in this PR

Adds bounded retries for transient project-evaluation failures, reducing premature catalog errors while preserving terminal verdicts.

Changes:

  • Returns HTTP 503 for transient failures and retries evaluations within runs.
  • Tracks bounded cross-run retries, with manual resets and bot-specific handling.
  • Adds retry tests and logs contradictory GitHub-activity verdicts without overriding them.
File Description
services/​libs/​data-access-layer/​src/​project-catalog/​types.ts Adds evaluation attempt count.
services/​libs/​data-access-layer/​src/​project-catalog/​projectCatalog.ts Adds deferral updates and counter resets.
services/​libs/​data-access-layer/​src/​project-catalog/​projectCatalog.test.ts Tests deferral limits and resets.
services/​libs/​common/​src/​index.ts Exports service-unavailable error.
services/​libs/​common/​src/​errors/​http.ts Defines HTTP 503 error.
services/​apps/​projects_evaluation_worker/​src/​workflows/​evaluateProjects.ts Adds retries and deferred-result handling.
services/​apps/​projects_evaluation_worker/​src/​evaluator/​retry.ts Defines retry constants and status classification.
services/​apps/​projects_evaluation_worker/​src/​evaluator/​evaluator.ts Throws typed transient failures.
services/​apps/​projects_evaluation_worker/​src/​evaluator/​evaluator.test.ts Tests transient and terminal responses.
services/​apps/​projects_evaluation_worker/​src/​activities/​immediateOnboarding.test.ts Updates catalog fixture.
services/​apps/​projects_evaluation_worker/​src/​activities/​evaluationRetry.ts Determines cross-run retry eligibility.
services/​apps/​projects_evaluation_worker/​src/​activities/​evaluationRetry.test.ts Tests retry limits and bot exclusion.
services/​apps/​projects_evaluation_worker/​src/​activities/​classifyAndRoute.test.ts Updates catalog fixture.
services/​apps/​projects_evaluation_worker/​src/​activities/​activities.ts Defers eligible evaluation failures.
backend/​src/​database/​migrations/​V1792160000__project-catalog-evaluation-attempts.sql Adds persisted attempt counter.
backend/​src/​api/​public/​v1/​projectEvaluation/​evaluateProject.ts Classifies transient errors and logs contradictions.
backend/​src/​api/​public/​v1/​projectEvaluation/​evaluateProject.test.ts Tests classification and contradiction logging.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +125 to +126
if (isTransientEvaluationError(err)) {
throw new ServiceUnavailableError(getErrorMessage(err))
const evaluateActivities = proxyActivities<typeof activities>({
startToCloseTimeout: '3 minutes',
retry: { maximumAttempts: 2 },
retry: { maximumAttempts: 3, initialInterval: '30 seconds', backoffCoefficient: 2 },
Comment on lines +111 to +113
if (current instanceof ApplicationFailure && current.type === TRANSIENT_EVALUATION_FAILURE) {
return true
}
Comment on lines +1054 to +1057
SET "evaluationAttempts" = "evaluationAttempts" + 1, "evaluationReason" = $(reason),
"updatedAt" = NOW()
WHERE id = $(id) AND "action" = 'evaluate' AND "evaluatedAt" IS NULL
AND "evaluationAttempts" + 1 < $(maxRuns)

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9a54a6c. Configure here.

const qx = pgpQx(svc.postgres.writer.connection())
if (transient && (await deferEvaluation(qx, projectId, reason))) {
return 'deferred'
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defer increment is not idempotent

Medium Severity

deferProjectCatalogEvaluation always does evaluationAttempts + 1, and markEvaluationFailed still uses the mark activity retry policy (maximumAttempts: 3). If Temporal retries after the increment commits (worker crash, lost completion, overlapping runs), one transient failure can count as two. On the second failure that is especially bad: a retry sees evaluationAttempts = 2, skips defer, and finalizes the row as error with an alert, so the third run never happens. The old mark-as-error write was idempotent; this counter is not.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9a54a6c. Configure here.

@ulemons
ulemons merged commit c8cf73c into main Oct 16, 2026
14 checks passed
@ulemons
ulemons deleted the feat/issue-215-transient-evaluation-retries branch October 16, 2026 15:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants