{"record":{"id":"75053735af3496d8","repo":"coleam00/Archon","slug":"wait-node-node-id-lost-ownership-of-its-persi","errorCode":null,"errorMessage":"Wait node '${node.id}' lost ownership of its persisted wait cursor","messagePattern":"Wait node '(.+?)' lost ownership of its persisted wait cursor","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/workflows/src/dag-executor.ts","lineNumber":7206,"sourceCode":"  const result = {\n    status,\n    waited_ms: Math.max(0, now.getTime() - Date.parse(context.waitingSince)),\n    ...(context.kind === 'event'\n      ? {\n          event: context.event,\n          ...(context.payload !== undefined ? { payload: context.payload } : {}),\n        }\n      : {}),\n  } as const;\n  const output = JSON.stringify(result);\n  const stepName = stepNamePrefix + node.id;\n  if (persisted !== undefined) {\n    const { cleared } = await deps.store.clearWorkflowWaitContext(workflowRun.id, context, {\n      stepName,\n      result,\n    });\n    if (!cleared) {\n      throw new Error(`Wait node '${node.id}' lost ownership of its persisted wait cursor`);\n    }\n  } else {\n    await deps.store.createWorkflowEvent({\n      workflow_run_id: workflowRun.id,\n      event_type: status === 'expired' ? 'wait_expired' : 'wait_completed',\n      step_name: stepName,\n      data: result,\n    });\n    await deps.store.createWorkflowEvent({\n      workflow_run_id: workflowRun.id,\n      event_type: 'node_completed',\n      step_name: stepName,\n      data: {\n        type: 'wait',\n        duration_ms: result.waited_ms,\n        node_output: output,\n        structured_output: result,\n      },","sourceCodeStart":7188,"sourceCodeEnd":7224,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/workflows/src/dag-executor.ts#L7188-L7224","documentation":"The DAG executor persists a 'wait cursor' when a wait node suspends so a later resume can be attributed to the same owner. When completing or expiring the wait, clearWorkflowWaitContext must report the cursor was cleared; if it returns cleared:false, the persisted cursor no longer matches this run/node, so the executor refuses to proceed rather than double-resume a wait someone else already consumed.","triggerScenarios":"A wait node resumes while another process or an earlier resume already cleared the persisted wait context for the same workflow run and step; concurrent resume attempts or a duplicated wait-expiration callback make the second clearWorkflowWaitContext return cleared:false.","commonSituations":"Running two Archon instances against the same SQLite/Postgres database; a retry after a partially-completed resume (event written, cursor already cleared); operator manually resuming an expired wait that was already processed.","solutions":["Verify only one Archon process/worker is running against this database before resuming waits","Check workflow_events for a prior wait_completed/wait_expired event for this step to confirm it was already resumed; treat the run as done instead of retrying","Re-run resume from a fresh workflow run rather than re-driving the suspended run","Inspect clearWorkflowWaitContext in the store adapter to confirm cursor ownership (run id + step) matches before resuming"],"exampleFix":"// before: blindly retrying the resume\nawait engine.resume(runId, stepName, result);\n// after: check prior completion first\nconst events = await store.getWorkflowEvents(runId);\nif (!events.some(e => e.step_name === stepName && ['wait_completed','wait_expired'].includes(e.event_type))) {\n  await engine.resume(runId, stepName, result);\n}","handlingStrategy":"try-catch","validationCode":"const events = await store.getWorkflowEvents(runId);\nif (events.some(e => e.step_name === stepName && ['wait_completed','wait_expired'].includes(e.event_type))) {\n  throw new Error('wait already resolved; do not resume again');\n}","typeGuard":"function canResumeWait(events, stepName: string): boolean {\n  return !events.some(e => e.step_name === stepName && ['wait_completed','wait_expired'].includes(e.event_type));\n}","tryCatchPattern":"try {\n  await engine.resume(runId, stepName, result);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('lost ownership of its persisted wait cursor')) {\n    // treat as already-resumed; inspect events instead of retrying\n  } else throw err;\n}","preventionTips":["Ensure a single Archon process owns the database (no concurrent installs on one DSN)","Check for prior wait_completed/wait_expired events before issuing a resume","Avoid manual retries of resume calls after a timeout; verify run state first"],"tags":["concurrency","wait-node","state-management"],"backgroundTag":"lost-lock-ownership","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}