{"record":{"id":"04d6b1b6161d8b00","repo":"coleam00/Archon","slug":"workflow-run-not-found-or-already-terminal-id","errorCode":null,"errorMessage":"Workflow run not found or already terminal (id: ${id})","messagePattern":"Workflow run not found or already terminal \\(id: (.+?)\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/core/src/db/workflows.ts","lineNumber":1302,"sourceCode":"        });\n      }\n      if ((update.rowCount ?? 0) > 0) {\n        await insertWorkflowEvent(query, {\n          workflow_run_id: id,\n          event_type: 'workflow_failed',\n          data: { error },\n        });\n      }\n      return update;\n    });\n  } catch (dbError) {\n    const err = dbError as Error;\n    getLog().error({ err }, 'db.workflow_run_mark_failed_error');\n    throw new Error(`Failed to fail workflow run: ${err.message}`);\n  }\n  if (result.rowCount === 0) {\n    getLog().warn({ workflowRunId: id }, 'db.workflow_run_fail_no_match');\n    throw new Error(`Workflow run not found or already terminal (id: ${id})`);\n  }\n}\n\nexport async function cancelWorkflowRun(\n  id: string,\n  event?: WorkflowCancellationEventDetails\n): Promise<{ cancelled: boolean }> {\n  const dialect = getDialect();\n  let result: Awaited<ReturnType<IDatabase['query']>>;\n  try {\n    // Guard against re-stamping an already-finished run. Cancelling a run that\n    // is 'completed' or 'cancelled' must be a no-op, not a re-write of\n    // completed_at / a resurrection of terminal state. 'failed' is intentionally\n    // still cancellable (it remains a resumable state, so the user must be able\n    // to discard it), and a 'running' run stays cancellable — that is\n    // cooperative cancellation, which the executor honors via its between-layer\n    // status check (dag-executor).\n    result = await getDatabase().withTransaction(async query => {","sourceCodeStart":1284,"sourceCodeEnd":1320,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/core/src/db/workflows.ts#L1284-L1320","documentation":"failWorkflowRun() updates only rows whose status is IN ('running','pending'); terminal rows (completed/failed/cancelled) never transition. If rowCount is 0 — id unknown or run already terminal — this error is thrown so callers never silently re-fail or re-stamp a finished run.","triggerScenarios":"Calling failWorkflowRun(id, ...) for: a nonexistent id; a run already 'failed' (double-failure, e.g. step failure then outer catch fails again); a run that was cancelled or completed concurrently; or a run id from a different/older database.","commonSituations":"Executor error handler and a finally/cleanup path both call failWorkflowRun; operator cancelled the run just before it errored; replaying failure handling after a crash against an already-terminal row; stale id cached from a previous run.","solutions":["Check the run's current status first; treat 'already terminal' as expected and skip re-failing","Serialize failure handling so only one code path can fail a run (guard with a local flag or re-read after the error)","If the run id may be stale, re-resolve the id from the run record before failing","If you need to record additional error info on a terminal run, write an event/log instead of mutating status","Retry only if you confirmed the run is in 'running' or 'pending' state"],"exampleFix":"// before\ntry { await failWorkflowRun(id, msg); } catch { /* retried blindly */ }\n// after\ntry {\n  await failWorkflowRun(id, msg);\n} catch (err) {\n  if (err.message.includes('already terminal') || err.message.includes('not found')) {\n    getLog().info({ runId: id }, 'run already terminal; skipping fail');\n  } else { throw err; }\n}","handlingStrategy":"validation","validationCode":"const run = await getWorkflowRun(id);\nif (!run) throw new Error(`run ${id} does not exist`);\nif (['completed', 'failed', 'cancelled'].includes(run.status)) {\n  return; // already terminal: nothing to do\n}","typeGuard":"function isNonTerminal(run: { status: string } | null | undefined): boolean {\n  return run?.status === 'running' || run?.status === 'pending';\n}","tryCatchPattern":"try {\n  await failWorkflowRun(id, msg);\n} catch (err) {\n  if (err.message.includes('not found or already terminal')) {\n    getLog().info({ runId: id }, 'run already terminal; failure not re-recorded');\n  } else { throw err; }\n}","preventionTips":["Guard failure handling with a one-shot flag so only one path fails a run","Treat 'already terminal' as success in cleanup/finally blocks","Re-read run status after any concurrent-cancel possibility","Do not cache run ids across database resets","Use workflow events to append extra error detail on terminal runs instead of mutating status"],"tags":["database","workflow","state-transition","idempotency"],"backgroundTag":"workflow-run-already-terminal","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}