{"record":{"id":"aaa06e3c872af2fd","repo":"coleam00/Archon","slug":"failed-to-cancel-fan-out-run-err-message","errorCode":null,"errorMessage":"Failed to cancel fan-out run: ${err.message}","messagePattern":"Failed to cancel fan-out run: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/core/src/db/workflows.ts","lineNumber":1380,"sourceCode":"         SET status = 'cancelled',\n             completed_at = ${dialect.now()},\n             metadata = ${dialect.jsonMerge('metadata', 2)}\n         WHERE id = $1 AND status NOT IN ('completed', 'cancelled')`,\n        [id, JSON.stringify({ cancelled_reason: reason })]\n      );\n      if ((update.rowCount ?? 0) > 0) {\n        await insertWorkflowEvent(query, {\n          workflow_run_id: id,\n          event_type: 'workflow_cancelled',\n          data: { reason },\n        });\n      }\n      return update;\n    });\n  } catch (error) {\n    const err = error as Error;\n    getLog().error({ err, workflowRunId: id, reason }, 'db.workflow_run_fan_out_cancel_failed');\n    throw new Error(`Failed to cancel fan-out run: ${err.message}`);\n  }\n  const cancelled = (result.rowCount ?? 0) > 0;\n  if (!cancelled) {\n    getLog().info({ workflowRunId: id, reason }, 'db.workflow_run_fan_out_cancel_noop');\n  }\n  return { cancelled };\n}\n\n/**\n * Pause a running workflow run for human approval.\n * Sets status to 'paused' and stores approval context in metadata.\n * Does NOT set completed_at — the run is not finished.\n *\n * The stored `metadata.approval` is REPLACED with `approvalContext` wholesale\n * (writeApprovalMetadata), never merged into. So a fresh pause stores exactly\n * the keys the caller set — at every depth — and nothing a prior gate of the\n * same run left behind can survive, on either dialect (#2673). Readers treat an\n * absent key exactly like a JSON null (`!= null`, `=== true`, `?? ''`), and","sourceCodeStart":1362,"sourceCodeEnd":1398,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/core/src/db/workflows.ts#L1362-L1398","documentation":"This error is thrown by the fan-out run cancel function in the database layer after the underlying cancellation transaction fails. The original database error is logged (db.workflow_run_fan_out_cancel_failed) and re-thrown wrapped in a message that includes the inner error message, so the caller sees one combined failure explaining that cancelling a fan-out workflow run could not be persisted.","triggerScenarios":"Calling cancelWorkflowRunFanOut (the fan-out cancel path, packages/core/src/db/workflows.ts:1380) when the underlying SQL UPDATE/transaction throws: database connection failure, constraint violation, deadlock, or the transaction aborted mid-cancel.","commonSituations":"Postgres or SQLite temporarily unreachable during a cancel request; concurrent cancellation or completion racing the UPDATE inside the transaction; malformed workflowRunId type; the DB pool exhausted under load.","solutions":["Read the inner err.message in the thrown message and the db.workflow_run_fan_out_cancel_failed log entry to identify the underlying database error","Verify the database is reachable and the connection pool is healthy, then retry the cancel","Check for concurrent operations on the same workflow_run row (deadlock/serialization failure) and retry with backoff","Confirm the workflowRunId exists and is a fan-out run in a state that permits cancellation","If the run is already terminal (completed/failed/cancelled), treat the cancel as a no-op instead of retrying"],"exampleFix":"// before\nawait cancelWorkflowRunFanOut(id, reason); // unhandled -> crashes caller\n// after\ntry {\n  await cancelWorkflowRunFanOut(id, reason);\n} catch (err) {\n  logger.error({ id, err }, 'cancel failed; checking run state before retry');\n  const run = await getWorkflowRun(id);\n  if (run?.status === 'running') await cancelWorkflowRunFanOut(id, reason);\n}","handlingStrategy":"try-catch","validationCode":"const run = await getWorkflowRun(id);\nif (!run) throw new Error(`run ${id} not found before cancel`);\nif (run.status !== 'running') return { cancelled: false };","typeGuard":"function isErrorWithMessage(e: unknown): e is Error {\n  return e instanceof Error && typeof e.message === 'string';\n}","tryCatchPattern":"try {\n  await cancelWorkflowRunFanOut(id, reason);\n} catch (e) {\n  const msg = e instanceof Error ? e.message : String(e);\n  logger.error({ id, msg }, 'fan-out cancel failed');\n  if (/connect|ECONNREFUSED|timeout/i.test(msg)) scheduleRetry(id, reason);\n}","preventionTips":["Check the run's status is 'running' before issuing a cancel","Keep the DB connection pool sized for cancel bursts and monitor connectivity","Treat cancel as idempotent: tolerate no-op (already cancelled) results","Always log the wrapped inner message, which this function already embeds in the thrown text"],"tags":["database","workflow","cancellation","error-wrapping"],"backgroundTag":"database-operation-failed","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}