@deksden-com/dd-flow-cli 0.9.0-beta.13 → 0.9.0-beta.134

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/CHANGELOG.md +687 -0
  2. package/README.md +101 -1
  3. package/dist/build-info.json +10 -10
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +306 -0
  6. package/dist/cli/help.js +88 -12
  7. package/dist/cli/hook-ingress.js +78 -0
  8. package/dist/cli/input-preparation.js +116 -0
  9. package/dist/cli/native-hook-ingress.js +80 -0
  10. package/dist/cli/run-cli.js +1465 -450
  11. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  12. package/dist/cli/runtime-maintenance-ingress.js +52 -0
  13. package/dist/cli/runtime-process.js +153 -0
  14. package/dist/cli.js +17 -2
  15. package/dist/domain/stage-catalog.js +1 -1
  16. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  17. package/dist/harness-runtime/bin/dd-agy.mjs +56 -0
  18. package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
  19. package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
  20. package/dist/harness-runtime/bin/dd-grok.mjs +40 -0
  21. package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
  22. package/dist/harness-runtime/bin/dd-zcode.mjs +87 -0
  23. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  24. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  25. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  26. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  27. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  28. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  29. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  30. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  31. package/dist/harness-runtime/lib/daemon-operations.mjs +370 -0
  32. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
  33. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +693 -0
  34. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  35. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  36. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +190 -0
  37. package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-codex.mjs +805 -0
  39. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +128 -0
  40. package/dist/harness-runtime/lib/dd-droid.mjs +561 -0
  41. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +490 -0
  42. package/dist/harness-runtime/lib/dd-grok.mjs +177 -0
  43. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +255 -0
  44. package/dist/harness-runtime/lib/dd-opencode.mjs +128 -0
  45. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +703 -0
  46. package/dist/harness-runtime/lib/dd-zcode.mjs +1128 -0
  47. package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
  48. package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
  49. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  50. package/dist/harness-runtime/lib/driver-recovery.mjs +139 -0
  51. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  52. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  53. package/dist/harness-runtime/lib/lease-renewal.mjs +36 -0
  54. package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
  55. package/dist/harness-runtime/lib/lifecycle-command.mjs +272 -0
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
  58. package/dist/harness-runtime/lib/managed-daemon.d.mts +12 -0
  59. package/dist/harness-runtime/lib/managed-daemon.mjs +619 -0
  60. package/dist/harness-runtime/lib/model-observations.mjs +101 -0
  61. package/dist/harness-runtime/lib/native-children.d.mts +14 -0
  62. package/dist/harness-runtime/lib/native-children.mjs +94 -0
  63. package/dist/harness-runtime/lib/native-hook-command.d.mts +7 -0
  64. package/dist/harness-runtime/lib/native-hook-command.mjs +251 -0
  65. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  66. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  67. package/dist/harness-runtime/lib/observation-clock.mjs +92 -0
  68. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  69. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  70. package/dist/harness-runtime/lib/operation-errors.mjs +116 -0
  71. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  72. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  73. package/dist/harness-runtime/lib/process-json.d.mts +5 -0
  74. package/dist/harness-runtime/lib/process-json.mjs +201 -0
  75. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  76. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  77. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  78. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  79. package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
  80. package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
  81. package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
  82. package/dist/harness-runtime/lib/tool-observations.mjs +228 -0
  83. package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
  84. package/dist/protocol/local-files.js +18 -9
  85. package/dist/runtime/context.js +8 -4
  86. package/dist/schemas/agent-profile.schema.json +1 -1
  87. package/dist/schemas/code-review-result.schema.json +1 -1
  88. package/dist/schemas/code-verification.schema.json +7 -5
  89. package/dist/schemas/code-work-batch.schema.json +2 -2
  90. package/dist/schemas/flow-run.schema.json +2 -0
  91. package/dist/schemas/plan-review-decision.schema.json +3 -3
  92. package/dist/schemas/plan-review-result.schema.json +2 -2
  93. package/dist/schemas/run-control-receipt.schema.json +118 -0
  94. package/dist/schemas/run-control-request.schema.json +36 -0
  95. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  96. package/dist/services/branch-context.js +3 -3
  97. package/dist/services/canon.js +11 -8
  98. package/dist/services/check-runner-evidence.js +121 -0
  99. package/dist/services/cleanup.js +133 -40
  100. package/dist/services/cli-operation-classifier.js +22 -24
  101. package/dist/services/code-checks.js +851 -109
  102. package/dist/services/codex-capacity.js +367 -0
  103. package/dist/services/codex-hook-delivery.js +32 -0
  104. package/dist/services/command-context.js +80 -0
  105. package/dist/services/compatibility-preflight.js +13 -3
  106. package/dist/services/config.js +11 -8
  107. package/dist/services/continuation-outcome.js +20 -0
  108. package/dist/services/controller-fanout.js +136 -0
  109. package/dist/services/dashboard.js +135 -56
  110. package/dist/services/engines.js +245 -119
  111. package/dist/services/eval-snapshots.js +1146 -105
  112. package/dist/services/execution-policy.js +181 -0
  113. package/dist/services/external-work-launch.js +230 -0
  114. package/dist/services/harness-adapter.js +329 -36
  115. package/dist/services/harness-config.js +2 -0
  116. package/dist/services/hooks.js +753 -325
  117. package/dist/services/lanes.js +78 -65
  118. package/dist/services/lifecycle-command.js +1 -288
  119. package/dist/services/lifecycle-contract.js +1 -0
  120. package/dist/services/lifecycle-invocations.js +1828 -0
  121. package/dist/services/managed-daemon-binding.js +38 -0
  122. package/dist/services/managed-lease-monitor.js +133 -0
  123. package/dist/services/managed-processes.js +409 -51
  124. package/dist/services/merge-queue.js +228 -149
  125. package/dist/services/merge-server.js +163 -35
  126. package/dist/services/migrations.js +37 -16
  127. package/dist/services/native-daemon-history.js +49 -0
  128. package/dist/services/native-session-control.js +39 -0
  129. package/dist/services/plan-runtime.js +26 -11
  130. package/dist/services/plans.js +28 -25
  131. package/dist/services/projects.js +20 -15
  132. package/dist/services/prompts.js +28 -17
  133. package/dist/services/protocols.js +163 -87
  134. package/dist/services/recovery-observation-budget.js +121 -0
  135. package/dist/services/recovery-snapshot-database.js +145 -0
  136. package/dist/services/repair-intents.js +42 -0
  137. package/dist/services/review-copy.js +157 -0
  138. package/dist/services/review-deferral.js +23 -0
  139. package/dist/services/run-control-receipt.js +56 -0
  140. package/dist/services/run-control-worker.js +411 -0
  141. package/dist/services/run-control.js +985 -0
  142. package/dist/services/run-controller-adapter.js +399 -0
  143. package/dist/services/run-controller-capture.js +179 -0
  144. package/dist/services/run-controller-process.js +187 -0
  145. package/dist/services/run-controller-recovery.js +289 -0
  146. package/dist/services/run-controller-state.js +97 -0
  147. package/dist/services/run-controller.js +1035 -0
  148. package/dist/services/run-engine-bindings.js +1 -1
  149. package/dist/services/run-fork.js +147 -0
  150. package/dist/services/run-observations.js +152 -0
  151. package/dist/services/run-projection.js +22 -64
  152. package/dist/services/run-recovery-guard.js +75 -0
  153. package/dist/services/run-recovery-runtime.js +112 -0
  154. package/dist/services/run-recovery.js +349 -0
  155. package/dist/services/runs.js +558 -217
  156. package/dist/services/runtime-budget.js +430 -0
  157. package/dist/services/runtime-command.js +43 -0
  158. package/dist/services/runtime-scope-capture.js +52 -0
  159. package/dist/services/runtime-scope-control.js +468 -0
  160. package/dist/services/runtime-scope-resume.js +599 -0
  161. package/dist/services/runtime-scope-stop.js +180 -0
  162. package/dist/services/runtime-scope-worker.js +287 -0
  163. package/dist/services/runtime-service.js +96 -0
  164. package/dist/services/schema-validation.js +10 -8
  165. package/dist/services/scope-writer-cleanup.js +27 -0
  166. package/dist/services/sessions.js +35 -69
  167. package/dist/services/stage-blocker.js +17 -7
  168. package/dist/services/stage-context.js +185 -19
  169. package/dist/services/stage-lifecycle.js +98 -107
  170. package/dist/services/stage-pause.js +89 -75
  171. package/dist/services/stage-report-renderer.js +158 -6
  172. package/dist/services/stage-work-graph.js +35 -0
  173. package/dist/services/tooling.js +32 -23
  174. package/dist/services/usage.js +49 -101
  175. package/dist/services/verification-matrix.js +374 -0
  176. package/dist/services/vnext-code-review.js +222 -87
  177. package/dist/services/vnext-code.js +324 -251
  178. package/dist/services/vnext-contracts.js +10 -1
  179. package/dist/services/vnext-execution-profile.js +79 -4
  180. package/dist/services/vnext-fanout.js +182 -17
  181. package/dist/services/vnext-merge.js +335 -113
  182. package/dist/services/vnext-plan-review.js +276 -126
  183. package/dist/services/vnext-plan.js +227 -102
  184. package/dist/services/vnext-protocolize.js +335 -82
  185. package/dist/services/vnext-specify.js +119 -81
  186. package/dist/services/work-registry.js +1083 -194
  187. package/dist/services/workspace-bootstrap.js +87 -0
  188. package/dist/services/workspace-files.js +85 -0
  189. package/dist/services/worktrees.js +98 -44
  190. package/dist/shared/entity-references.js +8 -0
  191. package/dist/shared/errors.js +17 -0
  192. package/dist/storage/agent-output.js +26 -0
  193. package/dist/storage/database.js +785 -37
  194. package/dist/storage/foreign-key-transaction.js +39 -0
  195. package/dist/storage/immutable-input.js +45 -0
  196. package/dist/storage/import-guard.js +102 -0
  197. package/dist/storage/native-cleanup.js +52 -0
  198. package/dist/storage/paths.js +17 -4
  199. package/dist/storage/projection-lock.js +73 -0
  200. package/dist/storage/work-references.js +23 -0
  201. package/dist/storage/writer-contract.js +77 -0
  202. package/dist/storage/writer-migration.js +85 -0
  203. package/package.json +30 -13
  204. package/tools/repair-paused-run-status.mjs +84 -0
@@ -0,0 +1,1035 @@
1
+ import { assertLifecycleOutcomes, currentLifecycleFailure, managedInvocationContext, managedLifecycleCommand } from "./lifecycle-invocations.js";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { fileURLToPath } from "node:url";
7
+ import { quote } from "shell-quote";
8
+ import { ensureRunControllerStorage, getDatabase } from "../storage/database.js";
9
+ import { canonicalPath, resolveProjectRoot } from "../storage/paths.js";
10
+ import { publishImmutableInput } from "../storage/immutable-input.js";
11
+ import { AppError } from "../shared/errors.js";
12
+ import { prepareFileBytes, prepareJsonText } from "../cli/input-preparation.js";
13
+ import { parseRuntimeBudget } from "./runtime-budget.js";
14
+ import { requireProjectByRoot } from "./projects.js";
15
+ import { assertRunMutationAllowed, recoveryGuard } from "./run-recovery.js";
16
+ import { getFlowRunStatus, setFlowRunRecoveryPaused } from "./runs.js";
17
+ import { loadExternalStageContext } from "./stage-context.js";
18
+ import { flowCommand } from "./stage-pause.js";
19
+ import { resolveStageTransition, sameSessionProfile, stageExecutionCwd, runtimeHarness } from "./execution-policy.js";
20
+ import { resolveHarnessCommand, harnessRuntimeArguments } from "./harness-config.js";
21
+ import { adapterSessionId, prepareHarnessFlowExecutable, runHarnessAdapter } from "./harness-adapter.js";
22
+ import { assertManagedProcessLease, confirmManagedProcess, renewManagedProcess, finishManagedProcess, processStartedAt, processTreeIsAlive, resourceHome } from "./managed-processes.js";
23
+ import { managedLeaseMonitor } from "./managed-lease-monitor.js";
24
+ import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
25
+ import { initCodexHome } from "./hooks.js";
26
+ import { launchExternalWork } from "./external-work-launch.js";
27
+ import { controllerNativeChildren, controllerStageEntryPrompt, controllerStageResumePrompt, nextControllerFanout } from "./controller-fanout.js";
28
+ import { claimQueuedMergeRequest } from "./merge-server.js";
29
+ import { prepareVnextCodeWorkspace } from "./vnext-code.js";
30
+ import { requireRun, requireController, controllerReceipt, appendEvent, saveState, retainControllerContinuation, retainControllerCapacityIntent, digest, digestBytes, errorRecord } from "./run-controller-state.js";
31
+ import { assertNoCorrelatedRejection } from "./continuation-outcome.js";
32
+ import { spawnController, recordControllerLauncher, claimControllerProcess, markControllerDispatchReady, assertControllerRecoveryAllowed } from "./run-controller-process.js";
33
+ import { controllerAdapter, controllerEnvironment, sessionArguments } from "./run-controller-adapter.js";
34
+ import { capacityDelay, continueCodexCapacity, eligibleCodexChildOverload } from "./codex-capacity.js";
35
+ import { resumeControllerSession, failControllerRecovery } from "./run-controller-recovery.js";
36
+ import { captureControllerBoundary } from "./run-controller-capture.js";
37
+ import { requestRunControl } from "./run-control-worker.js";
38
+ import { storageSessionId } from "./session-identity.js";
39
+ import { CAPACITY_POLICY, nativeItemsSettled, overloadBurst, refusalObservation, terminalCodexOverload } from "../harness-runtime/lib/codex-capacity-policy.mjs";
40
+ import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
41
+ export { resumeRunController } from "./run-controller-recovery.js";
42
+ export function prepareRunControllerLaunch(context, input) {
43
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !/^[a-f0-9]{64}$/.test(input.contextSha256))
44
+ throw new AppError("usage", "Managed launch needs a request id and context SHA-256", 2);
45
+ if (input.stopAfter !== undefined && !/^[a-z][a-z-]*$/.test(input.stopAfter))
46
+ throw new AppError("usage", "Invalid stop-after stage", 2);
47
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
48
+ const run = requireRun(context, project.id, input.runId);
49
+ const captureRoot = input.captureRoot === undefined ? undefined : canonicalPath(input.captureRoot, false);
50
+ if (captureRoot && [context.ddFlowHome, project.root, run.workspace_root].some(root => { const relative = path.relative(canonicalPath(root, false), captureRoot); return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative); }))
51
+ throw new AppError("snapshot_output_inside_runtime", "Controller captures require a root outside the runtime, project and workspace", 2);
52
+ if (captureRoot && fs.existsSync(captureRoot) && !fs.statSync(captureRoot).isDirectory())
53
+ throw new AppError("usage", "Controller capture root must be a directory", 2);
54
+ const budget = parseRuntimeBudget(context.env.DD_FLOW_RUNTIME_BUDGET);
55
+ const request = { project_root: project.root, run_id: run.id, context_sha256: input.contextSha256, stop_after: input.stopAfter ?? null, ...(captureRoot ? { capture_root: captureRoot } : {}), ...(budget ? { budget_sha256: digest(budget) } : {}) };
56
+ const requestHash = digest(request);
57
+ const existing = context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_controllers'")
58
+ ? context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? AND request_id = ?", [project.id, run.id, input.requestId]) : undefined;
59
+ if (existing) {
60
+ if (existing.request_hash !== requestHash)
61
+ throw new AppError("runtime_request_conflict", "Managed launch request id has different inputs", 1);
62
+ return { kind: "replay", existing };
63
+ }
64
+ const generation = assertRunMutationAllowed(context, project.id, run.id);
65
+ if (run.status !== "running")
66
+ throw new AppError("invalid_run_state", "Managed launch requires a running logical RUN", 1, { status: run.status });
67
+ const index = JSON.parse(run.index_json);
68
+ const execution = index.execution_profile;
69
+ if (!execution?.settings.execution || !execution.agent_profiles)
70
+ throw new AppError("execution_profile_not_frozen", "Managed launch requires the RUN's frozen execution profiles", 1);
71
+ const contexts = readContexts(input.contextFile, input.contextSha256);
72
+ const next = getFlowRunStatus(context, { projectRoot: project.root, runId: run.id }).continuation;
73
+ if (next.kind !== "start_stage" || !next.stage)
74
+ throw new AppError("controller_attach_required", "Managed launch cannot replace an attached or unfinished coordinator; reconcile its existing owner", 1, { continuation: next });
75
+ if (!contexts[next.stage])
76
+ throw new AppError("controller_context_missing", "Managed context omits the first stage", 2, { stage: next.stage });
77
+ const adapters = {};
78
+ for (const profile of Object.values(execution.agent_profiles)) {
79
+ const harness = runtimeHarness(profile);
80
+ adapters[harness] = { executable: resolveHarnessCommand(context.ddFlowHome, harness, "adapter"), runtime_args: harnessRuntimeArguments(context.ddFlowHome, harness) };
81
+ }
82
+ const entry = fileURLToPath(new URL("../cli.js", import.meta.url));
83
+ if (!fs.existsSync(entry))
84
+ throw new AppError("harness_runtime_unbuilt", "Build the CLI before launching its detached controller", 1);
85
+ return { kind: "create", project, run, request, requestHash, generation, budget, execution, contexts, adapters };
86
+ }
87
+ export async function launchRunController(context, input, prepared = prepareRunControllerLaunch(context, input)) {
88
+ if (prepared.kind === "replay")
89
+ return { ...await spawnController(context, prepared.existing), reused: true };
90
+ const { project, run, request, requestHash, generation, budget, execution, contexts, adapters } = prepared;
91
+ assertRunMutationAllowed(context, project.id, run.id, generation);
92
+ ensureRunControllerStorage(context.db);
93
+ const controllerId = `DRV-${crypto.randomUUID()}`;
94
+ const root = path.join(run.run_root, "controllers", controllerId);
95
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
96
+ const flowExecutable = prepareHarnessFlowExecutable(context, root);
97
+ const frozenContexts = {};
98
+ for (const [stage, value] of Object.entries(contexts)) {
99
+ const file = path.join(root, `${stage}.context.json`);
100
+ fs.writeFileSync(file, value.bytes, { flag: "wx", mode: 0o600 });
101
+ frozenContexts[stage] = { file, sha256: value.sha256 };
102
+ }
103
+ const manifest = { schema_id: "dd-flow/run-controller-manifest@1", request, execution_profile: execution, contexts: frozenContexts, adapters, flow_executable: flowExecutable, resource_home: resourceHome(context), budget };
104
+ const token = crypto.randomUUID();
105
+ const now = context.now();
106
+ const state = { stage: null, sessions: [], current_session: null, turns: 0, last_continuation: null };
107
+ const launcher = { pid: process.pid, pid_started_at: processStartedAt(process.pid) };
108
+ const duplicate = context.db.writeTransaction(() => {
109
+ const duplicate = context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? AND request_id = ?", [project.id, run.id, input.requestId]);
110
+ if (duplicate) {
111
+ if (duplicate.request_hash !== requestHash)
112
+ throw new AppError("runtime_request_conflict", "Managed launch request id has different inputs", 1);
113
+ return { ...controllerReceipt(duplicate), reused: true };
114
+ }
115
+ assertRunMutationAllowed(context, project.id, run.id, generation);
116
+ if (requireRun(context, project.id, run.id).status !== "running")
117
+ throw new AppError("invalid_run_state", "Managed launch requires a running logical RUN", 1);
118
+ const owner = context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','cancelled','superseded')", [project.id, run.id]);
119
+ if (owner)
120
+ throw new AppError("controller_already_owned", "RUN already has an unresolved managed owner; observe or reconcile it", 1, { controller_id: owner.controller_id });
121
+ if (context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.status = 'running' LIMIT 1", [project.id, run.id]))
122
+ throw new AppError("controller_attach_required", "A WorkSession acquired the RUN before managed launch", 1);
123
+ context.db.run("INSERT INTO run_controllers (controller_id, project_id, run_id, request_id, request_hash, generation, status, owner_token, manifest_json, state_json, state_dir, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, 'starting', ?, ?, ?, ?, ?, ?)", [controllerId, project.id, run.id, input.requestId, requestHash, generation, token, JSON.stringify(manifest), JSON.stringify(state), root, now, now]);
124
+ recordControllerLauncher(context, requireController(context, controllerId), undefined, launcher);
125
+ appendEvent(context, controllerId, "launch_requested", { request_id: input.requestId, generation, request_hash: requestHash });
126
+ return null;
127
+ });
128
+ if (duplicate)
129
+ return duplicate;
130
+ return await spawnController(context, requireController(context, controllerId));
131
+ }
132
+ /**
133
+ * A registry write can be temporarily unavailable while another runtime client
134
+ * owns SQLite's writer lock. That is not evidence that this controller lost
135
+ * its lease. Productive dispatch still requires a successful recheck before
136
+ * proceeding, while an explicit negative renewal remains a hard ownership
137
+ * loss.
138
+ */
139
+ export const controllerLeaseMonitor = managedLeaseMonitor;
140
+ /** Unconfirmed ownership is not evidence authorizing a native cancellation. */
141
+ export function retainControllerLeaseUncertainty(context, row, error) {
142
+ if (!(error instanceof AppError) || error.code !== "controller_lease_unconfirmed")
143
+ return false;
144
+ try {
145
+ const db = context.db.withDeadline(Date.now() + 5_000);
146
+ db.writeTransaction(() => {
147
+ const changed = db.run("UPDATE run_controllers SET status = CASE WHEN status IN ('capturing','capture_failed') THEN 'capture_failed' ELSE 'recovery_required' END, last_error_json = ?, updated_at = ? WHERE controller_id = ? AND owner_token = ? AND generation = ? AND status NOT IN ('completed','cancelled','superseded','recovery_required','capture_failed')", [JSON.stringify(errorRecord(error)), context.now(), row.controller_id, row.owner_token, row.generation]);
148
+ if (changed.changes)
149
+ appendEvent({ ...context, db }, row.controller_id, "owner_confirmation_required", { ...errorRecord(error), native_cancelled: false });
150
+ });
151
+ }
152
+ catch (persistence) {
153
+ process.stderr.write(`${JSON.stringify({ controller_id: row.controller_id, primary: errorRecord(error), persistence: errorRecord(persistence), native_cancelled: false })}\n`);
154
+ }
155
+ return true;
156
+ }
157
+ /** No initialization, provider calls or lease takeover on the observation path. */
158
+ export function runControllerStatus(context, input) {
159
+ if (input.after !== undefined && (!Number.isSafeInteger(input.after) || input.after < 0))
160
+ throw new AppError("usage", "--after must be a non-negative cursor", 2);
161
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
162
+ const run = requireRun(context, project.id, input.runId);
163
+ if (!context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_controllers'"))
164
+ return { ok: true, run_id: run.id, controller: null, events: [], next_cursor: input.after ?? 0 };
165
+ const row = context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? ORDER BY created_at DESC, controller_id DESC LIMIT 1", [project.id, run.id]);
166
+ if (!row)
167
+ return { ok: true, run_id: run.id, controller: null, events: [], next_cursor: input.after ?? 0 };
168
+ const events = context.db.all("SELECT sequence, event_type, data_json, created_at FROM run_controller_events WHERE controller_id = ? AND sequence > ? ORDER BY sequence LIMIT 100", [row.controller_id, input.after ?? 0]);
169
+ return { ...controllerReceipt(row), events: events.map(event => ({ sequence: event.sequence, type: event.event_type, data: JSON.parse(event.data_json), at: event.created_at })), next_cursor: events.at(-1)?.sequence ?? input.after ?? 0 };
170
+ }
171
+ export async function serveRunController(context, input) {
172
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
173
+ const row = requireController(context, input.controllerId);
174
+ if (row.project_id !== project.id || row.run_id !== input.runId || row.owner_token !== context.env.DD_FLOW_CONTROLLER_TOKEN)
175
+ throw new AppError("controller_owner_mismatch", "Managed serve does not own this launch", 1);
176
+ claimControllerProcess(context, row);
177
+ if (!row.process_id || !row.process_lease_token)
178
+ throw new AppError("controller_owner_mismatch", "Managed owner has no resource lease", 1);
179
+ const confirmed = confirmManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
180
+ const lease = controllerLeaseMonitor(attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: row.process_id, leaseToken: row.process_lease_token }), confirmed.lease_expires_at);
181
+ // Resource and RUN commits are distinct effects. A RUN keepalive failure must
182
+ // neither erase a resource receipt nor grant RUN dispatch authority.
183
+ const mergeLease = controllerLeaseMonitor(attemptMs => {
184
+ const expiry = new Date(Date.now() + 120_000).toISOString();
185
+ const db = context.db.withDeadline(Date.now() + attemptMs);
186
+ if (db.get("SELECT 1 FROM merge_requests WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status = 'dispatching' LIMIT 1", [row.project_id, row.run_id, row.controller_id]))
187
+ db.writeTransaction(() => {
188
+ const guard = recoveryGuard({ ...context, db }, row.project_id, row.run_id);
189
+ if ((guard?.generation ?? 0) !== row.generation || ["draining", "sealed", "resuming"].includes(guard?.status ?? ""))
190
+ throw new AppError("controller_lease_lost", "RUN generation changed before MERGE keepalive", 1);
191
+ db.run("UPDATE merge_requests SET dispatch_lease_expires_at = ?, updated_at = ? WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status = 'dispatching'", [expiry, context.now(), row.project_id, row.run_id, row.controller_id]);
192
+ });
193
+ return { owned: true, lease_expires_at: expiry };
194
+ });
195
+ const assertLeaseFence = () => {
196
+ const current = requireController(context, row.controller_id);
197
+ if (current.owner_token !== row.owner_token || current.generation !== row.generation)
198
+ throw new AppError("controller_owner_mismatch", "Lease renewal no longer owns this controller generation", 1);
199
+ assertControllerRecoveryAllowed(context, current);
200
+ };
201
+ const heartbeat = setInterval(() => {
202
+ lease.background(assertLeaseFence, () => { });
203
+ mergeLease.background(assertLeaseFence, () => { });
204
+ }, 30_000);
205
+ heartbeat.unref();
206
+ const manifest = JSON.parse(row.manifest_json);
207
+ const state = JSON.parse(row.state_json);
208
+ const assertPhysicalOwner = () => {
209
+ lease.assertNoRenewal();
210
+ mergeLease.assertNoRenewal();
211
+ assertManagedProcessLease({ ...context, databaseDeadlineAt: Date.now() + 5_000 }, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid });
212
+ const current = requireController(context, row.controller_id);
213
+ if (current.owner_pid !== process.pid || current.owner_token !== row.owner_token || !["running", "capturing", "waiting_for_user", "waiting_for_context", "waiting_for_children", "waiting_for_capacity", "waiting_for_merge"].includes(current.status))
214
+ throw new AppError("controller_owner_mismatch", "Controller no longer owns productive dispatch", 1);
215
+ };
216
+ const assertOwner = () => { assertPhysicalOwner(); assertRunMutationAllowed(context, row.project_id, row.run_id, row.generation); };
217
+ let primaryFailure;
218
+ const retainSecondary = (phase, error) => {
219
+ process.stderr.write(`${JSON.stringify({ controller_id: row.controller_id, phase, primary: errorRecord(primaryFailure), secondary: errorRecord(error) })}\n`);
220
+ };
221
+ try {
222
+ await lease.ensureConfirmed(assertLeaseFence);
223
+ await mergeLease.ensureConfirmed(assertLeaseFence);
224
+ context.db.writeTransaction(() => {
225
+ markControllerDispatchReady(context, row);
226
+ if (!state.recovery && (recoveryGuard(context, row.project_id, row.run_id)?.generation ?? 0) === row.generation && requireRun(context, row.project_id, row.run_id).status === "paused")
227
+ setFlowRunRecoveryPaused(context, row.project_id, row.run_id, false);
228
+ });
229
+ appendEvent(context, row.controller_id, "owner_started", { pid: process.pid, generation: row.generation });
230
+ if (state.recovery && !["done", "failed", "cancelled"].includes(requireRun(context, row.project_id, row.run_id).status))
231
+ await resumeControllerSession(context, row, manifest, state, assertPhysicalOwner);
232
+ await executeController(context, row, manifest, state, assertOwner, assertPhysicalOwner, async () => { await lease.ensureConfirmed(assertLeaseFence); await mergeLease.ensureConfirmed(assertLeaseFence); });
233
+ return controllerReceipt(requireController(context, row.controller_id));
234
+ }
235
+ catch (error) {
236
+ primaryFailure = error;
237
+ try {
238
+ try {
239
+ failControllerRecovery(context, row, state, error);
240
+ }
241
+ catch (secondary) {
242
+ retainSecondary("fail_controller_recovery", secondary);
243
+ }
244
+ if ((error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale"].includes(error.code) || (recoveryGuard(context, row.project_id, row.run_id)?.generation ?? 0) !== row.generation)
245
+ && context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_controls'")) {
246
+ const guard = recoveryGuard(context, row.project_id, row.run_id);
247
+ const control = guard && ["draining", "sealed"].includes(guard.status) && context.db.get("SELECT control_id, action FROM run_controls WHERE project_id = ? AND run_id = ? AND recovery_id = ? AND status IN ('requested','settled')", [row.project_id, row.run_id, guard.recovery_id]);
248
+ if (control) {
249
+ // Control is a later state transition, not evidence that the causal
250
+ // failure which fenced admission disappeared.
251
+ context.db.run("UPDATE run_controllers SET status = CASE WHEN last_error_json IS NULL THEN 'control_requested' ELSE status END, updated_at = ? WHERE controller_id = ? AND owner_token = ?", [context.now(), row.controller_id, row.owner_token]);
252
+ appendEvent(context, row.controller_id, "control_boundary_reached", { control_id: control.control_id, action: control.action, generation: guard.generation, settled: false });
253
+ return controllerReceipt(requireController(context, row.controller_id));
254
+ }
255
+ }
256
+ try {
257
+ context.db.writeTransaction(() => {
258
+ // A failed capture must not reopen admission when this process exits.
259
+ const changed = context.db.run("UPDATE run_controllers SET status = CASE WHEN status IN ('capturing','capture_failed') THEN 'capture_failed' ELSE 'recovery_required' END, last_error_json = ?, updated_at = ? WHERE controller_id = ? AND owner_token = ? AND status NOT IN ('completed','cancelled','superseded')", [JSON.stringify(errorRecord(error)), context.now(), row.controller_id, row.owner_token]);
260
+ context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status IN ('dispatching','active')", [JSON.stringify(errorRecord(error)), context.now(), row.project_id, row.run_id, row.controller_id]);
261
+ if (changed.changes && (recoveryGuard(context, row.project_id, row.run_id)?.generation ?? 0) === row.generation) {
262
+ setFlowRunRecoveryPaused(context, row.project_id, row.run_id, true);
263
+ }
264
+ appendEvent(context, row.controller_id, "recovery_required", errorRecord(error));
265
+ });
266
+ }
267
+ catch (failure) {
268
+ retainSecondary("persist_controller_failure", failure);
269
+ }
270
+ }
271
+ catch (secondary) {
272
+ retainSecondary("controller_failure_boundary", secondary);
273
+ }
274
+ throw error;
275
+ }
276
+ finally {
277
+ lease.close();
278
+ mergeLease.close();
279
+ clearInterval(heartbeat);
280
+ finishOwner();
281
+ }
282
+ function finishOwner() {
283
+ try {
284
+ finishManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token, state: "stopped", reason: "controller_owner_exited" });
285
+ }
286
+ catch (cleanup) {
287
+ if (!primaryFailure)
288
+ throw cleanup;
289
+ retainSecondary("finish_controller_process", cleanup);
290
+ }
291
+ }
292
+ }
293
+ export function prepareRunControllerAnswer(context, input) {
294
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !input.pauseId.trim())
295
+ throw new AppError("usage", "Answer requires request-id and pause-id", 2);
296
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
297
+ const prior = context.db.get(`SELECT o.operation_id, o.status, o.input_json
298
+ FROM run_controller_operations o JOIN run_controllers c ON c.controller_id = o.controller_id
299
+ WHERE c.project_id = ? AND c.run_id = ? AND o.operation_id = c.controller_id || ':answer:' || ?`, [project.id, input.runId, input.requestId]);
300
+ if (prior) {
301
+ const retained = JSON.parse(prior.input_json);
302
+ if (retained.pause_id !== input.pauseId)
303
+ throw new AppError("runtime_request_conflict", "HITL answer request id belongs to another pause", 1, { operation_id: prior.operation_id });
304
+ return { kind: "replay", operationId: prior.operation_id, status: prior.status };
305
+ }
306
+ const { bytes } = prepareFileBytes(input.answerFile, "HITL answer");
307
+ if (!bytes.toString("utf8").trim())
308
+ throw new AppError("validation", "HITL answer must not be empty", 2);
309
+ const row = context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','cancelled','superseded')", [project.id, input.runId]);
310
+ if (!row)
311
+ throw new AppError("not_found", "RUN has no managed controller for this answer", 1);
312
+ const operationId = `${row.controller_id}:answer:${input.requestId}`;
313
+ const answerHash = digestBytes(bytes);
314
+ const hash = digest({ pause_id: input.pauseId, answer_sha256: answerHash });
315
+ const previous = prepareControllerAnswerAcceptance(context, input, project.id, row, operationId, hash);
316
+ if (previous.prior)
317
+ return { kind: "replay", operationId, status: previous.prior.status };
318
+ return { kind: "create", project, row, bytes, operationId, answerHash, hash };
319
+ }
320
+ function prepareControllerAnswerAcceptance(context, input, projectId, row, operationId, hash) {
321
+ const prior = context.db.get("SELECT input_hash, status FROM run_controller_operations WHERE operation_id = ?", [operationId]);
322
+ if (prior) {
323
+ if (prior.input_hash !== hash)
324
+ throw new AppError("runtime_request_conflict", "HITL answer request id has different raw input", 1);
325
+ return { prior };
326
+ }
327
+ const currentOwner = requireController(context, row.controller_id);
328
+ if (["completed", "cancelled", "superseded"].includes(currentOwner.status))
329
+ throw new AppError("invalid_run_state", "The answer's controller is no longer current", 1);
330
+ const competing = context.db.get("SELECT operation_id FROM run_controller_operations WHERE controller_id = ? AND json_extract(input_json, '$.action') = 'user_answer' AND json_extract(input_json, '$.pause_id') = ? LIMIT 1", [row.controller_id, input.pauseId]);
331
+ if (competing)
332
+ throw new AppError("runtime_request_conflict", "This HITL pause already has an accepted answer", 1, { operation_id: competing.operation_id });
333
+ const run = requireRun(context, projectId, input.runId);
334
+ if (["done", "failed", "cancelled"].includes(run.status))
335
+ throw new AppError("invalid_run_state", "A terminal RUN cannot accept a new answer", 1);
336
+ const index = JSON.parse(run.index_json);
337
+ const stage = index.stage_runs.find(stage => stage.pause?.id === input.pauseId && ["paused", "waiting_for_user"].includes(stage.status));
338
+ if (!stage?.pause?.work_id)
339
+ throw new AppError("hitl_pause_mismatch", "Answer does not match the RUN's current HITL pause", 1);
340
+ return { run, stage, workId: stage.pause.work_id };
341
+ }
342
+ export function answerRunController(context, input, prepared = prepareRunControllerAnswer(context, input)) {
343
+ if (prepared.kind === "replay")
344
+ return { ok: true, operation_id: prepared.operationId, status: prepared.status, reused: true };
345
+ const { project, row, bytes, operationId, answerHash, hash } = prepared;
346
+ const previous = prepareControllerAnswerAcceptance(context, input, project.id, row, operationId, hash);
347
+ if (previous.prior)
348
+ return { ok: true, operation_id: operationId, status: previous.prior.status, reused: true };
349
+ const file = path.join(row.state_dir, `answer-${digest(operationId)}-${answerHash}.md`);
350
+ publishImmutableInput(file, bytes, "runtime_request_conflict");
351
+ return context.db.writeTransaction(() => {
352
+ // Recheck only mutable acceptance state under the write transaction; bytes are already prepared.
353
+ const acceptance = prepareControllerAnswerAcceptance(context, input, project.id, row, operationId, hash);
354
+ if (acceptance.prior) {
355
+ return { ok: true, operation_id: operationId, status: acceptance.prior.status, reused: true };
356
+ }
357
+ const { run, stage, workId } = acceptance;
358
+ if (requireController(context, row.controller_id).state_dir !== row.state_dir)
359
+ throw new AppError("controller_owner_mismatch", "Controller input directory changed before acceptance", 1);
360
+ const now = context.now();
361
+ const generation = recoveryGuard(context, project.id, run.id)?.generation ?? row.generation;
362
+ context.db.run("INSERT INTO run_controller_operations (operation_id, controller_id, generation, input_hash, input_json, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'accepted', ?, ?)", [operationId, row.controller_id, generation, hash, JSON.stringify({ action: "user_answer", pause_id: input.pauseId, stage: stage.stage, work_id: workId, file, sha256: answerHash }), now, now]);
363
+ appendEvent(context, row.controller_id, "answer_accepted", { operation_id: operationId, pause_id: input.pauseId, answer_sha256: answerHash });
364
+ return { ok: true, operation_id: operationId, status: "accepted" };
365
+ });
366
+ }
367
+ export function prepareRunControllerContext(context, input) {
368
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !/^[a-z][a-z-]*$/.test(input.stage) || !/^[a-f0-9]{64}$/.test(input.contextSha256))
369
+ throw new AppError("usage", "Context submission requires request id, stage and SHA-256", 2);
370
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
371
+ const operationId = `${input.controllerId}:context:${input.requestId}`;
372
+ const hash = digest({ stage: input.stage, sha256: input.contextSha256 });
373
+ const row = requireController(context, input.controllerId);
374
+ if (row.project_id !== project.id || row.run_id !== input.runId)
375
+ throw new AppError("controller_owner_mismatch", "Context submission belongs to another RUN", 1);
376
+ const prior = context.db.get("SELECT input_hash, status FROM run_controller_operations WHERE operation_id = ?", [operationId]);
377
+ if (prior) {
378
+ if (prior.input_hash !== hash)
379
+ throw new AppError("runtime_request_conflict", "Context request id has different inputs", 1);
380
+ return { kind: "replay", operationId, status: prior.status };
381
+ }
382
+ availableControllerContext(context, input, row);
383
+ const { bytes } = loadExternalStageContext({ file: input.contextFile, expectedSha256: input.contextSha256, stage: input.stage });
384
+ return { kind: "create", project, operationId, hash, bytes };
385
+ }
386
+ export function supplyRunControllerContext(context, input, prepared = prepareRunControllerContext(context, input)) {
387
+ if (prepared.kind === "replay")
388
+ return { ok: true, operation_id: prepared.operationId, status: prepared.status, reused: true };
389
+ const { project, operationId, hash, bytes } = prepared;
390
+ const preparedOwner = requireController(context, input.controllerId);
391
+ if (preparedOwner.project_id !== project.id || preparedOwner.run_id !== input.runId)
392
+ throw new AppError("controller_owner_mismatch", "Context submission belongs to another RUN", 1);
393
+ const existing = context.db.get("SELECT input_hash, status FROM run_controller_operations WHERE operation_id = ?", [operationId]);
394
+ if (existing) {
395
+ if (existing.input_hash !== hash)
396
+ throw new AppError("runtime_request_conflict", "Context request id has different inputs", 1);
397
+ return { ok: true, operation_id: operationId, status: existing.status, reused: true };
398
+ }
399
+ availableControllerContext(context, input, preparedOwner);
400
+ const file = path.join(preparedOwner.state_dir, `${input.stage}-${input.contextSha256}.context.json`);
401
+ publishImmutableInput(file, bytes, "stage_context_replacement");
402
+ return context.db.writeTransaction(() => {
403
+ const row = requireController(context, input.controllerId);
404
+ if (row.project_id !== project.id || row.run_id !== input.runId)
405
+ throw new AppError("controller_owner_mismatch", "Context submission belongs to another RUN", 1);
406
+ const prior = context.db.get("SELECT input_hash, status FROM run_controller_operations WHERE operation_id = ?", [operationId]);
407
+ if (prior) {
408
+ if (prior.input_hash !== hash)
409
+ throw new AppError("runtime_request_conflict", "Context request id has different inputs", 1);
410
+ return { ok: true, operation_id: operationId, status: prior.status, reused: true };
411
+ }
412
+ const manifest = availableControllerContext(context, input, row);
413
+ if (row.state_dir !== preparedOwner.state_dir)
414
+ throw new AppError("controller_owner_mismatch", "Controller input directory changed before acceptance", 1);
415
+ manifest.contexts[input.stage] = { file, sha256: input.contextSha256 };
416
+ const now = context.now();
417
+ context.db.run("UPDATE run_controllers SET manifest_json = ?, updated_at = ? WHERE controller_id = ?", [JSON.stringify(manifest), now, row.controller_id]);
418
+ context.db.run("INSERT INTO run_controller_operations (operation_id, controller_id, generation, input_hash, input_json, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'accepted', ?, ?)", [operationId, row.controller_id, row.generation, hash, JSON.stringify({ action: "stage_context", stage: input.stage, file, sha256: input.contextSha256 }), now, now]);
419
+ appendEvent(context, row.controller_id, "context_accepted", { operation_id: operationId, stage: input.stage, context_sha256: input.contextSha256 });
420
+ return { ok: true, operation_id: operationId, status: "accepted" };
421
+ });
422
+ }
423
+ function availableControllerContext(context, input, row) {
424
+ assertRunMutationAllowed(context, row.project_id, input.runId, row.generation);
425
+ if (!["starting", "running", "waiting_for_context", "waiting_for_user", "waiting_for_children", "waiting_for_capacity", "waiting_for_merge"].includes(row.status))
426
+ throw new AppError("invalid_run_state", "Controller cannot accept a new context in its current state", 1);
427
+ const manifest = JSON.parse(row.manifest_json);
428
+ if (manifest.contexts[input.stage])
429
+ throw new AppError("stage_context_replacement", "Controller already has an immutable context for this stage", 1);
430
+ return manifest;
431
+ }
432
+ async function executeController(context, row, manifest, state, assertOwnership, assertPhysicalOwner, renewLease) {
433
+ assertOwnership();
434
+ const observer = { ...context, db: getDatabase(context.ddFlowHome, "read_existing") };
435
+ const checkOutcomes = () => assertLifecycleOutcomes(observer, { projectRoot: manifest.request.project_root, runId: row.run_id, generation: row.generation });
436
+ const assertOwner = () => { if (fatal)
437
+ throw fatal; checkOutcomes(); assertOwnership(); };
438
+ const prepareMutation = async (action) => {
439
+ for (;;) {
440
+ await renewLease();
441
+ try {
442
+ context.db.writeTransaction(() => { assertOwner(); action(); });
443
+ return;
444
+ }
445
+ catch (error) {
446
+ if (!(error instanceof AppError) || error.code !== "controller_lease_unconfirmed")
447
+ throw error;
448
+ }
449
+ }
450
+ };
451
+ const policy = manifest.execution_profile.settings.execution;
452
+ const profiles = manifest.execution_profile.agent_profiles;
453
+ let executionContext = { ...context, env: { ...context.env, DD_FLOW_BIN: manifest.flow_executable, DD_FLOW_RESOURCE_HOME: manifest.resource_home, DD_FLOW_RUNTIME_BUDGET: manifest.budget ? JSON.stringify(manifest.budget) : undefined, DD_FLOW_CODE_READINESS_OWNER: row.controller_id } };
454
+ const bindInvocationScope = (session) => {
455
+ executionContext = managedInvocationContext(executionContext, { harness: runtimeHarness(session.profile), stateDir: session.state_dir, sessionId: session.id,
456
+ projectRoot: manifest.request.project_root, runId: row.run_id, generation: row.generation });
457
+ };
458
+ let lastCapture = null;
459
+ let terminalFinalization = false;
460
+ let fatal;
461
+ let stopping;
462
+ const cancellation = new AbortController();
463
+ const capacityUncertain = (error) => error instanceof AppError && ["capacity_continuation_blocked", "operation_observation_lost"].includes(error.code) && error.details.capacity_uncertainty === true || Boolean(state.pending_capacity) && (String(error?.code) === "capacity_continuation_blocked" || isObservationLoss(error));
464
+ const stopAfterFailure = (error) => {
465
+ // A retained successor may already be running. Fencing/recovery records
466
+ // uncertainty; cancellation is not evidence of its missing outcome.
467
+ if (capacityUncertain(error))
468
+ return Promise.resolve();
469
+ if (retainControllerLeaseUncertainty(context, row, error))
470
+ return Promise.resolve();
471
+ if (stopping)
472
+ return stopping;
473
+ fatal = error;
474
+ // Persist the cause before fencing/cancellation; later control is not an
475
+ // operator pause and cannot replace this execution failure.
476
+ stopping = (async () => {
477
+ try {
478
+ context.db.run("UPDATE run_controllers SET status = CASE WHEN status IN ('capturing','capture_failed') THEN 'capture_failed' ELSE 'recovery_required' END, last_error_json = ?, updated_at = ? WHERE controller_id = ? AND owner_token = ?", [JSON.stringify(errorRecord(error)), context.now(), row.controller_id, row.owner_token]);
479
+ }
480
+ catch (persistence) {
481
+ process.stderr.write(`${JSON.stringify({ primary: errorRecord(error), persistence: errorRecord(persistence) })}\n`);
482
+ }
483
+ await requestRunControl(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id, requestId: `fatal:${row.controller_id}:${row.generation}`, action: "stop", waitMs: 0 });
484
+ })().catch(cleanup => {
485
+ // The caller still receives the primary failure if control cannot start.
486
+ try {
487
+ appendEvent(context, row.controller_id, "failure_control_error", { primary: errorRecord(error), cleanup: errorRecord(cleanup) });
488
+ }
489
+ catch {
490
+ process.stderr.write(`${JSON.stringify({ primary: errorRecord(error), cleanup: errorRecord(cleanup) })}\n`);
491
+ }
492
+ }).finally(() => cancellation.abort());
493
+ return stopping;
494
+ };
495
+ const setStatus = (status) => {
496
+ const changed = context.db.run("UPDATE run_controllers SET status = ?, updated_at = ? WHERE controller_id = ? AND owner_token = ? AND status <> ?", [status, context.now(), row.controller_id, row.owner_token, status]);
497
+ if (changed.changes)
498
+ appendEvent(context, row.controller_id, "status_changed", { status, stage: state.stage });
499
+ };
500
+ const call = (session, args, productive = true) => controllerAdapter(executionContext, row, manifest, session, args, productive ? assertOwner : undefined, undefined, productive ? cancellation.signal : undefined, productive ? renewLease : undefined);
501
+ const confirmStopped = async (stateDir, receipt) => {
502
+ const fail = () => new AppError("controller_native_shutdown_unproven", "The managed daemon is not proven clean and closed", 1, { state_dir: stateDir, receipt });
503
+ if (receipt.stopped !== true || receipt.clean !== true)
504
+ throw fail();
505
+ const binding = requireManagedDaemonBinding(executionContext, { stateDir, projectId: row.project_id, runId: row.run_id });
506
+ const deadline = performance.now() + 30_000;
507
+ while (processTreeIsAlive(binding.process) && performance.now() < deadline)
508
+ await delay(100);
509
+ const current = readNativeDaemonState(stateDir);
510
+ if (processTreeIsAlive(binding.process) || current.shutdown_state !== "clean"
511
+ || current.pid !== binding.process.pid || (current.daemon_id ?? current.config?.daemonId) !== (binding.state.daemon_id ?? binding.state.config?.daemonId)
512
+ || current.resource_process?.id !== binding.process.id || current.resource_process?.lease_token !== binding.process.lease_token)
513
+ throw fail();
514
+ };
515
+ const stopSession = async (session) => {
516
+ if (session.stopped)
517
+ return;
518
+ const receipt = await call(session, ["daemon", "stop", "--state-dir", session.state_dir, "--json"], false);
519
+ await confirmStopped(session.state_dir, receipt);
520
+ session.stopped = true;
521
+ saveState(context, row, state);
522
+ appendEvent(context, row.controller_id, "session_stopped", { session_id: session.id || null, state_dir: session.state_dir, receipt });
523
+ };
524
+ const settle = async () => {
525
+ const results = await Promise.allSettled(state.sessions.map(stopSession));
526
+ const failure = results.find((result) => result.status === "rejected");
527
+ if (failure)
528
+ throw failure.reason;
529
+ };
530
+ const reconcilePromptPredecessor = async (session) => {
531
+ const prior = state.last_receipt?.settlement;
532
+ if (!prior || typeof prior !== "object" || Array.isArray(prior) || !["pending", "blocked"].includes(String(prior.state)))
533
+ return;
534
+ const receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
535
+ state.last_receipt = receipt;
536
+ saveState(context, row, state);
537
+ const current = receipt.settlement;
538
+ if (current && typeof current === "object" && !Array.isArray(current) && ["pending", "blocked"].includes(String(current.state))) {
539
+ throw new AppError("settlement_reconciliation_required", "The managed Session still has an unsettled productive predecessor", 1, { session_id: session.id, settlement: current });
540
+ }
541
+ };
542
+ const prompt = async (session, text, operationId, kind) => {
543
+ const hitl = kind === "hitl";
544
+ const pending = state.pending_capacity;
545
+ if (pending && (pending.session_id !== session.id || pending.generation !== row.generation || pending.kind !== (kind ?? "stage") || operationId && operationId !== pending.root_operation_id))
546
+ throw new AppError("capacity_continuation_conflict", "Retained capacity owner reference differs from its Session/task", 1);
547
+ if (!pending)
548
+ await reconcilePromptPredecessor(session);
549
+ const firstOperationId = pending?.root_operation_id ?? operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
550
+ const send = (value, id) => {
551
+ const retained = context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [id, row.controller_id, row.generation]);
552
+ if (retained) {
553
+ const dispatch = JSON.parse(retained.input_json);
554
+ if (![value, `${value}\n`].some(text => dispatch.prompt_sha256 === crypto.createHash("sha256").update(text).digest("hex")))
555
+ throw new AppError("capacity_continuation_conflict", "Retained prompt differs from continuation", 1);
556
+ return controllerAdapter(executionContext, row, manifest, session, dispatch.args, assertOwner, id, cancellation.signal, renewLease);
557
+ }
558
+ const file = path.join(row.state_dir, `turn-${String(state.turns + 1).padStart(5, "0")}.md`);
559
+ fs.writeFileSync(file, value, { flag: "wx", mode: 0o600 });
560
+ state.turns += 1;
561
+ saveState(context, row, state);
562
+ return controllerAdapter(executionContext, row, manifest, session, ["session", "prompt", "--session-id", session.id, "--prompt-file", file, ...sessionArguments(context, manifest, session)], assertOwner, id, cancellation.signal, renewLease);
563
+ };
564
+ let receipt;
565
+ try {
566
+ try {
567
+ if (pending) {
568
+ const predecessor = context.db.get("SELECT error_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'failed'", [pending.predecessor, row.controller_id, row.generation]);
569
+ if (!predecessor?.error_json)
570
+ throw new AppError("capacity_continuation_conflict", "Retained capacity predecessor has no failure receipt", 1);
571
+ // The root identifies the whole chain; inspect its frozen failure,
572
+ // then reconcile all retained successors before any dispatch.
573
+ const root = context.db.get("SELECT error_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [firstOperationId, row.controller_id, row.generation]);
574
+ if (!root?.error_json)
575
+ throw new AppError("capacity_continuation_conflict", "Retained capacity root has no failure receipt", 1);
576
+ throw JSON.parse(root.error_json);
577
+ }
578
+ receipt = await send(text, firstOperationId);
579
+ }
580
+ catch (error) {
581
+ if (runtimeHarness(session.profile) !== "codex-desktop")
582
+ throw error;
583
+ const resumeCommand = hitl ? text.split("\n\n")[1] : undefined;
584
+ if (hitl && !resumeCommand?.includes(" stage resume "))
585
+ throw error;
586
+ const inspect = (args, id) => runHarnessAdapter({ executable: manifest.adapters["codex-desktop"].executable,
587
+ args, env: controllerEnvironment(context, manifest, session), runtimeOwner: session.owner, operationId: id, timeoutMs: 30_000, signal: cancellation.signal });
588
+ const resumed = await continueCodexCapacity({ error, sessionId: session.id, operationId: firstOperationId, signal: cancellation.signal, allowSettledDescendants: true, atomicIntent: true,
589
+ retainedIntents: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ?", [row.controller_id, row.generation])
590
+ .flatMap(item => { const retained = JSON.parse(item.receipt_json || "null"); return retained?.capacity_intent?.root_operation_id === firstOperationId ? [retained.capacity_intent] : []; }).sort((a, b) => a.ordinal - b.ordinal),
591
+ retainedFailures: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ?", [row.controller_id, row.generation])
592
+ .flatMap(item => { const retained = JSON.parse(item.receipt_json || "null"); return retained?.capacity_root === firstOperationId && retained.capacity_failure ? [retained.capacity_failure] : []; }),
593
+ retainFailure: (id, refusal) => {
594
+ const changed = context.db.run("UPDATE run_controller_operations SET receipt_json = ?, updated_at = ? WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'outcome_unknown'", [JSON.stringify({ capacity_root: firstOperationId, capacity_failure: refusal }), context.now(), id, row.controller_id, row.generation]);
595
+ if (changed.changes !== 1)
596
+ throw new AppError("capacity_continuation_blocked", "Failed Turn owner changed before its refusal was retained", 1);
597
+ },
598
+ inspectOperation: id => inspect(["daemon", "operation", "--state-dir", session.state_dir, "--operation-id", id, "--json"], `${id}:capacity-observe`),
599
+ inspectSession: turnId => inspect(["session", "inspect", "--session-id", session.id, ...(turnId ? ["--turn-id", turnId] : []), ...sessionArguments(context, manifest, session)], `${firstOperationId}:capacity-inspect`),
600
+ assertCurrent: () => {
601
+ assertOwner();
602
+ },
603
+ decision: async () => {
604
+ if (kind === "child") {
605
+ const target = Object.entries(state.child_capacity ?? {}).find(([, retained]) => retained.operation_id === firstOperationId);
606
+ if (!target)
607
+ return "reconciliation_blocked";
608
+ const seen = await inspect(["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], `${firstOperationId}:child-followup-observe`);
609
+ const child = controllerNativeChildren(seen, session.id).find(item => item.session_id === target[0]);
610
+ if (child?.parent_session_id === session.id && (child.status === "running" || child.terminal_turn_id && child.terminal_turn_id !== target[1].turn_id))
611
+ return "completed";
612
+ }
613
+ const view = getFlowRunStatus(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id });
614
+ const next = view.continuation;
615
+ if (next.kind === "blocked")
616
+ return "reconciliation_blocked";
617
+ if (next.kind === "paused" && next.stage !== state.stage)
618
+ return "paused";
619
+ if (next.kind === "terminal" || next.stage !== state.stage)
620
+ return "completed";
621
+ if (hitl) {
622
+ const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
623
+ const answer = context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND status = 'accepted'", [answerId, row.controller_id]);
624
+ const accepted = answer && JSON.parse(answer.input_json);
625
+ const pause = view.index.stage_runs.find(item => item.stage === accepted?.stage)?.pause;
626
+ if (next.kind !== "paused")
627
+ return "continue";
628
+ if (!accepted || pause?.id !== accepted.pause_id || pause.work_id !== accepted.work_id || pause.answer_path)
629
+ return "paused";
630
+ }
631
+ else if (next.kind === "paused")
632
+ return "paused";
633
+ return next.kind === "wait" && !["native_children_unsettled", "work_receipts_pending", "qualified_capacity_required"].includes(next.reason) ? "reconciliation_blocked" : "continue";
634
+ },
635
+ retainIntent: intent => retainControllerCapacityIntent(context, row, state, intent, session.id, kind ?? "stage", assertOwner),
636
+ prompt: (id, value) => send(value, id),
637
+ ...(resumeCommand ? { continuationText: `Continue the same accepted HITL task in this Session. First inspect current durable RUN and pause state. Only if the exact pause still awaits the accepted answer, run this previously issued command once:\n\n${resumeCommand}\n\nNever replay an already committed answer. Otherwise use only the current issued successor.` } : {}) });
638
+ if (!resumed)
639
+ throw error;
640
+ receipt = resumed;
641
+ }
642
+ checkOutcomes();
643
+ if (fatal)
644
+ throw fatal;
645
+ }
646
+ catch (error) {
647
+ if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
648
+ throw error;
649
+ await stopAfterFailure(error);
650
+ if (!capacityUncertain(error) && !(error instanceof AppError && error.code === "controller_lease_unconfirmed"))
651
+ cancellation.abort();
652
+ throw fatal ?? error;
653
+ }
654
+ state.last_receipt = receipt;
655
+ delete state.pending_capacity;
656
+ saveState(context, row, state);
657
+ return receipt;
658
+ };
659
+ const monitor = setInterval(() => {
660
+ if (fatal)
661
+ return;
662
+ try {
663
+ checkOutcomes();
664
+ }
665
+ catch (error) {
666
+ void stopAfterFailure(error);
667
+ }
668
+ }, 1000);
669
+ monitor.unref();
670
+ try {
671
+ for (;;) {
672
+ // Renewal touches the resource registry; never do it from a callback that
673
+ // may already own the RUN writer.
674
+ await renewLease();
675
+ assertOwner();
676
+ if (state.current_session !== null && !state.sessions[state.current_session].stopped)
677
+ bindInvocationScope(state.sessions[state.current_session]);
678
+ const run = requireRun(context, row.project_id, row.run_id);
679
+ if (state.pending_capacity && !["done", "failed", "cancelled"].includes(run.status)) {
680
+ const pending = state.pending_capacity;
681
+ const session = state.sessions.find(item => item.id === pending.session_id && !item.stopped);
682
+ const operation = context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [pending.root_operation_id, row.controller_id, row.generation]);
683
+ const dispatch = operation && JSON.parse(operation.input_json);
684
+ const args = dispatch?.args;
685
+ const file = args?.includes("--prompt-file") ? args[args.indexOf("--prompt-file") + 1] : undefined;
686
+ if (!session || pending.generation !== row.generation || !file || digestBytes(fs.readFileSync(file)) !== dispatch?.prompt_sha256)
687
+ throw new AppError("capacity_continuation_conflict", "Retained capacity resume reference has no exact owned prompt", 1);
688
+ await prompt(session, fs.readFileSync(file, "utf8"), pending.root_operation_id, pending.kind === "stage" ? undefined : pending.kind);
689
+ continue;
690
+ }
691
+ const view = getFlowRunStatus(executionContext, { projectRoot: run.project_root, runId: run.id });
692
+ const next = view.continuation;
693
+ terminalFinalization = next.kind === "terminal";
694
+ const completed = state.stage && view.index.stage_runs.find(stage => stage.stage === state.stage && ["done", "skipped"].includes(stage.status));
695
+ if (completed && manifest.request.stop_after === state.stage) {
696
+ // A bounded run is final evidence, not an entry fixture for its successor.
697
+ // Stop owned observers before capture; keep the controller lease/fence.
698
+ await settle();
699
+ if (manifest.request.capture_root)
700
+ await captureControllerBoundary(executionContext, row, manifest, completed, null, assertPhysicalOwner);
701
+ setStatus("stop_target_reached");
702
+ appendEvent(context, row.controller_id, "stop_target_reached", { stage: state.stage, attempt: completed.attempt ?? null });
703
+ return;
704
+ }
705
+ if (completed && manifest.request.capture_root && ["start_stage", "terminal"].includes(next.kind)) {
706
+ const key = JSON.stringify([completed.stage, completed.attempt, completed.started_at, completed.completed_at]);
707
+ if (key !== lastCapture) {
708
+ await captureControllerBoundary(executionContext, row, manifest, completed, next.kind === "start_stage" ? next.stage : null, assertPhysicalOwner);
709
+ lastCapture = key;
710
+ }
711
+ }
712
+ if (next.kind === "terminal") {
713
+ await settle();
714
+ setStatus(run.status === "cancelled" ? "cancelled" : "completed");
715
+ appendEvent(context, row.controller_id, "terminal", { reason: next.reason });
716
+ return;
717
+ }
718
+ if (next.kind === "paused") {
719
+ setStatus("waiting_for_user");
720
+ const pause = view.index.stage_runs.find(stage => stage.stage === next.stage)?.pause;
721
+ if (!pause || state.current_session === null)
722
+ throw new AppError("controller_hitl_missing", "Paused RUN has no managed Session or HITL receipt", 1);
723
+ const answer = context.db.get("SELECT operation_id, input_json, status FROM run_controller_operations WHERE controller_id = ? AND status IN ('accepted','completed') AND json_extract(input_json, '$.action') = 'user_answer' AND json_extract(input_json, '$.pause_id') = ? ORDER BY created_at LIMIT 1", [row.controller_id, pause.id]);
724
+ if (!answer) {
725
+ await delay(1000);
726
+ continue;
727
+ }
728
+ if (answer.status === "completed")
729
+ throw new AppError("controller_answer_not_applied", "Completed answer operation still has its original HITL pause", 1, { pause_id: pause.id, operation_id: answer.operation_id, stage: next.stage });
730
+ const input = JSON.parse(answer.input_json);
731
+ if (digestBytes(fs.readFileSync(input.file)) !== input.sha256)
732
+ throw new AppError("context_checksum_mismatch", "Accepted HITL answer bytes changed", 1);
733
+ await renewLease();
734
+ assertOwner();
735
+ // The accepted answer is logical intent; only its generation-bound
736
+ // adapter operation represents a physical dispatch that stop must drain.
737
+ const command = managedLifecycleCommand(executionContext, `${flowCommand(executionContext)} stage resume ${run.id} --stage ${next.stage} --work ${input.work_id} --project-root ${quote([run.project_root])} --answer-file ${quote([input.file])} --json`, { replaceUnobservedAnswer: true });
738
+ const receipt = await prompt(state.sessions[state.current_session], controllerStageResumePrompt(pause.id, command), `${answer.operation_id}:prompt:${row.generation}`, "hitl");
739
+ const afterAnswer = getFlowRunStatus(executionContext, { projectRoot: run.project_root, runId: run.id });
740
+ if (afterAnswer.index.stage_runs.some(stage => stage.status === "paused" && stage.pause?.id === pause.id)) {
741
+ throw new AppError("controller_answer_not_applied", "The answer Turn completed without resuming the assigned HITL pause", 1, { pause_id: pause.id, operation_id: answer.operation_id, stage: next.stage, effect: "no_progress" });
742
+ }
743
+ context.db.run("UPDATE run_controller_operations SET status = 'completed', receipt_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify({ delivered: true, session_id: adapterSessionId(receipt) }), context.now(), answer.operation_id]);
744
+ state.last_continuation = null;
745
+ saveState(context, row, state);
746
+ setStatus("running");
747
+ continue;
748
+ }
749
+ if (next.kind === "blocked" || next.kind === "wait" || !next.stage)
750
+ throw new AppError("controller_continuation_blocked", "RUN has no legal managed continuation", 1, { continuation: next });
751
+ if (next.kind === "start_stage") {
752
+ // Only context slices can be added; profiles and launch inputs stay frozen.
753
+ manifest.contexts = JSON.parse(requireController(context, row.controller_id).manifest_json).contexts;
754
+ const stageContext = manifest.contexts[next.stage];
755
+ if (!stageContext) {
756
+ if (requireController(context, row.controller_id).status !== "waiting_for_context")
757
+ appendEvent(context, row.controller_id, "context_required", { stage: next.stage, attempt: next.attempt ?? null });
758
+ setStatus("waiting_for_context");
759
+ await delay(1000);
760
+ continue;
761
+ }
762
+ loadExternalStageContext({ file: stageContext.file, expectedSha256: stageContext.sha256, stage: next.stage });
763
+ const continuation = JSON.stringify(["start_stage", next.stage, next.attempt ?? null]);
764
+ retainControllerContinuation(state, continuation, { code: "stage_entry_nonprogressing", message: "The coordinator returned without starting the assigned Stage", details: { stage: next.stage, attempt: next.attempt ?? null, last_receipt: state.last_receipt ?? null } });
765
+ const transition = resolveStageTransition({ ddFlowHome: context.ddFlowHome, policy, profiles, ...(state.stage ? { fromStage: state.stage } : {}), stage: next.stage, projectRoot: run.project_root, workspaceRoot: run.workspace_root, stageSessionMode: manifest.execution_profile.settings.stage_session_mode, mergeMode: manifest.execution_profile.settings.merge_mode });
766
+ if (next.stage === "merge" && manifest.execution_profile.settings.merge_mode === "server" && !state.merge_claim) {
767
+ const request = context.db.get("SELECT merge_request_id, status FROM merge_requests WHERE project_id = ? AND run_id = ? ORDER BY created_at DESC, merge_request_id DESC LIMIT 1", [row.project_id, row.run_id]);
768
+ if (!request || request.status !== "queued")
769
+ throw new AppError("controller_attach_required", "Managed MERGE needs an unclaimed queued request; reconcile an existing owner", 1, { request });
770
+ const claim = claimQueuedMergeRequest(context, { requestId: request.merge_request_id, ownerId: row.controller_id, expectedGeneration: row.generation });
771
+ if (!claim) {
772
+ setStatus("waiting_for_merge");
773
+ await delay(1000);
774
+ continue;
775
+ }
776
+ state.merge_claim = { request_id: claim.request.merge_request_id, token: claim.token, target_workspace: claim.request.target_workspace, work_id: claim.request.executor_work_id };
777
+ saveState(context, row, state);
778
+ appendEvent(context, row.controller_id, "merge_claimed", { request_id: claim.request.merge_request_id });
779
+ }
780
+ const stageCwd = stageExecutionCwd({ stage: next.stage, projectRoot: run.project_root, workspaceRoot: run.workspace_root, ...(state.merge_claim ? { mergeTargetWorkspace: state.merge_claim.target_workspace } : {}) });
781
+ let session = state.current_session === null ? null : state.sessions[state.current_session];
782
+ if (session && next.stage === "merge" && manifest.execution_profile.settings.merge_mode === "same_session" && path.resolve(session.cwd) !== stageCwd) {
783
+ throw new AppError("merge_session_workspace_conflict", "Same-session MERGE cannot change the native Session working directory", 1, { session_cwd: session.cwd, target_cwd: stageCwd });
784
+ }
785
+ if (!session || transition.session_mode === "new_session" || !sameSessionProfile(session.profile, transition.coordinator) || path.resolve(session.cwd) !== stageCwd) {
786
+ if (session)
787
+ await stopSession(session);
788
+ await renewLease();
789
+ assertOwner();
790
+ const directory = path.join(row.state_dir, `session-${state.sessions.length + 1}`);
791
+ fs.mkdirSync(directory, { mode: 0o700 });
792
+ const owner = { schema_id: "dd-flow/runtime-owner@1", owner_id: row.controller_id, role: "subject", project_id: row.project_id, run_id: row.run_id, operation_id: `${row.controller_id}:session:${state.sessions.length + 1}`, state_dir: directory, dd_flow_home: context.ddFlowHome, dd_flow_bin: manifest.flow_executable, resource_home: manifest.resource_home, budget: manifest.budget };
793
+ if (next.stage === "merge" && state.merge_claim) {
794
+ owner.role = "merge";
795
+ owner.work_id = state.merge_claim.work_id;
796
+ }
797
+ session = { id: "", profile: transition.coordinator, state_dir: directory, cwd: stageCwd, journal: path.join(directory, "adapter.events.jsonl"), owner };
798
+ if (runtimeHarness(session.profile) === "codex-desktop") {
799
+ session.codex_home = path.join(directory, "codex-home");
800
+ initCodexHome(context, { projectRoot: run.project_root, profile: `controller-${digest(directory).slice(0, 16)}`, targetHome: session.codex_home, ...(context.env.CODEX_HOME ? { sourceHome: context.env.CODEX_HOME } : {}) });
801
+ }
802
+ state.sessions.push(session);
803
+ state.current_session = state.sessions.length - 1;
804
+ saveState(context, row, state);
805
+ await call(session, ["daemon", "start", ...sessionArguments(context, manifest, session)]);
806
+ const created = await call(session, ["session", "create", ...sessionArguments(context, manifest, session)]);
807
+ const sessionId = adapterSessionId(created);
808
+ if (!sessionId)
809
+ throw new AppError("harness_adapter_invalid", "Managed adapter did not return its Session id", 1);
810
+ session.id = sessionId;
811
+ saveState(context, row, state);
812
+ bindInvocationScope(session);
813
+ if (next.stage === "merge" && state.merge_claim)
814
+ context.db.run("UPDATE merge_requests SET adapter_receipt_json = ?, updated_at = ? WHERE merge_request_id = ? AND dispatch_owner = ? AND dispatch_lease_token = ?", [JSON.stringify({ controller_id: row.controller_id, profile: session.profile, session_id: session.id, state_dir: session.state_dir, create: created }), context.now(), state.merge_claim.request_id, row.controller_id, state.merge_claim.token]);
815
+ appendEvent(context, row.controller_id, "session_created", { stage: next.stage, session_id: sessionId, harness: runtimeHarness(session.profile), profile: session.profile });
816
+ }
817
+ state.native_baseline = controllerNativeChildren(state.last_receipt ?? {}, session.id).map(child => child.session_id);
818
+ state.stage = next.stage;
819
+ state.child_capacity = {};
820
+ saveState(context, row, state);
821
+ setStatus("running");
822
+ if (next.stage === "code" || next.stage === "code-review") {
823
+ // A CODE_REVIEW fork has source files but intentionally no installed
824
+ // dependencies. Its repair Work executes checks just like CODE Work.
825
+ const readiness = await prepareVnextCodeWorkspace(executionContext, { projectRoot: run.project_root, runId: run.id, ownerId: row.controller_id, stage: next.stage });
826
+ await renewLease();
827
+ assertOwner();
828
+ appendEvent(context, row.controller_id, "code_workspace_prepared", { stage: next.stage, readiness_status: readiness.status ?? null });
829
+ }
830
+ const responseFile = path.join(row.state_dir, `${next.stage}-${next.attempt ?? 1}.stage-start-response.json`);
831
+ const command = managedLifecycleCommand(executionContext, `${flowCommand(executionContext)} stage start ${run.id} --stage ${next.stage} --project-root ${JSON.stringify(run.project_root)} --context-file ${JSON.stringify(stageContext.file)} --context-sha256 ${stageContext.sha256} --require-session-binding --response-file ${JSON.stringify(responseFile)} --json --progress-jsonl`);
832
+ appendEvent(context, row.controller_id, "stage_entered", { stage: next.stage, session_id: session.id, context_sha256: stageContext.sha256 });
833
+ await prompt(session, controllerStageEntryPrompt(next.stage, command));
834
+ continue;
835
+ }
836
+ if (state.current_session === null)
837
+ throw new AppError("controller_attach_required", "An unfinished Stage has no managed coordinator", 1);
838
+ const session = state.sessions[state.current_session];
839
+ const baseline = new Set(state.native_baseline ?? []);
840
+ let children = controllerNativeChildren(state.last_receipt ?? {}, session.id).filter(child => !baseline.has(child.session_id));
841
+ if (runtimeHarness(session.profile) === "codex-desktop" && children.some(child => child.status === "failed")) {
842
+ const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
843
+ state.last_receipt = inspected;
844
+ saveState(context, row, state);
845
+ children = controllerNativeChildren(inspected, session.id).filter(child => !baseline.has(child.session_id));
846
+ const failed = children.filter(child => child.status === "failed");
847
+ // Freeze each distinct child refusal on its first exact observation,
848
+ // even while a healthy sibling still owns an active native Turn.
849
+ for (const failedChild of failed.filter(item => eligibleCodexChildOverload(item, session.id))) {
850
+ const linked = context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: "codex-desktop", session_id: failedChild.session_id })]);
851
+ if (linked.length > 1 || linked.some(link => link.status !== "running"))
852
+ continue;
853
+ const retained = state.child_capacity?.[failedChild.session_id];
854
+ const refusals = [...(retained?.refusals ?? [])];
855
+ if (refusals.some(item => item.turn_id === failedChild.terminal_turn_id))
856
+ continue;
857
+ const nativeFailure = { code: "turn_interrupted", details: { provider_session_id: failedChild.session_id, turn_id: failedChild.terminal_turn_id, native_turn_id: failedChild.terminal_turn_id, terminal_status: "failed", provider_error: failedChild.provider_error, observed_at: failedChild.terminal_observed_at } };
858
+ refusals.push(refusalObservation(nativeFailure, retained?.attempts ?? 0, failedChild.terminal_observed_at ?? context.now()));
859
+ state.child_capacity ??= {};
860
+ state.child_capacity[failedChild.session_id] = { ...retained, turn_id: retained?.turn_id ?? failedChild.terminal_turn_id, attempts: retained?.attempts ?? 0, policy: CAPACITY_POLICY, refusals };
861
+ await prepareMutation(() => { saveState(context, row, state); appendEvent(context, row.controller_id, "capacity_child_refusal", { child_session_id: failedChild.session_id, refusal: refusals.at(-1) }); });
862
+ const burst = overloadBurst(refusals);
863
+ if (burst)
864
+ throw new AppError("provider_overload_burst", "Native child had two continuation refusals within two minutes", 1, { ...burst, child_session_id: failedChild.session_id, primary_error: nativeFailure });
865
+ }
866
+ // Continue one existing child at a time; another failed child does not
867
+ // create a second fanout wave or share this child's refusal counter.
868
+ const child = children.every(item => item.status === "failed" || item.status === "completed") ? failed.find(item => item.parent_session_id === session.id && item.terminal_turn_id && nativeItemsSettled(item.native_turn_items) && terminalCodexOverload({ code: "turn_interrupted", details: { provider_session_id: item.session_id, turn_id: item.terminal_turn_id, native_turn_id: item.terminal_turn_id, terminal_status: "failed", provider_error: item.provider_error } }, item.session_id)) : undefined;
869
+ const turnId = child?.terminal_turn_id;
870
+ const native = child && turnId ? { code: "turn_interrupted", details: { provider_session_id: child.session_id, turn_id: turnId, native_turn_id: turnId,
871
+ terminal_status: "failed", provider_error: child.provider_error, observed_at: child.terminal_observed_at } } : null;
872
+ const prior = child && state.child_capacity?.[child.session_id];
873
+ const links = child ? context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: "codex-desktop", session_id: child.session_id })]) : [];
874
+ if (child && turnId && native && terminalCodexOverload(native, child.session_id, turnId)
875
+ && nativeItemsSettled(child.native_turn_items)
876
+ && inspected.settled === true && child.parent_session_id === session.id && links.length <= 1
877
+ && (!links.length || links[0].status === "running")) {
878
+ const prepared = prior?.turn_id === turnId && prior.dispatched === false && prior.operation_id && prior.not_before ? prior : null;
879
+ if (prior?.turn_id === turnId && prior.dispatched === true && !prepared)
880
+ throw new AppError("capacity_child_followup_unconfirmed", "Parent continuation did not start a new native child Turn", 1, { child_session_id: child.session_id, turn_id: turnId });
881
+ const attempt = prepared ? prepared.attempts : (prior?.attempts ?? 0) + 1;
882
+ if (!Number.isSafeInteger(attempt) || prior && prior.policy !== CAPACITY_POLICY)
883
+ throw new AppError("capacity_continuation_conflict", "Native child continuation policy or ordinal is incompatible", 1);
884
+ const refusals = [...(prior?.refusals ?? [])];
885
+ if (!refusals.some(item => item.turn_id === turnId)) {
886
+ const refusal = refusalObservation(native, attempt - 1, child.terminal_observed_at ?? context.now());
887
+ refusals.push(refusal);
888
+ state.child_capacity ??= {};
889
+ state.child_capacity[child.session_id] = { ...prior, turn_id: prior?.turn_id ?? turnId, attempts: prior?.attempts ?? 0, policy: CAPACITY_POLICY, refusals };
890
+ saveState(context, row, state);
891
+ }
892
+ const burst = overloadBurst(refusals);
893
+ if (burst)
894
+ throw new AppError("provider_overload_burst", "Native child had two continuation refusals within two minutes", 1, { ...burst, child_session_id: child.session_id, primary_error: native });
895
+ const wait = prepared ? Math.max(0, Date.parse(prepared.not_before) - Date.now()) : capacityDelay(native, attempt);
896
+ if (!Number.isFinite(wait) || Date.now() + wait > 8.64e15)
897
+ throw new AppError("capacity_retry_metadata_invalid", "Native child retry time is invalid", 1, { child_session_id: child.session_id, delay_ms: wait });
898
+ const rootOperationId = prior?.root_operation_id ?? `${row.controller_id}:child:${digest([child.session_id, turnId]).slice(0, 16)}`;
899
+ const operationId = prepared?.operation_id ?? `${rootOperationId}:followup:${attempt}`;
900
+ state.child_capacity ??= {};
901
+ state.child_capacity[child.session_id] = { turn_id: turnId, attempts: attempt, operation_id: operationId, root_operation_id: rootOperationId, policy: CAPACITY_POLICY, refusals, not_before: prepared?.not_before ?? new Date(Date.now() + wait).toISOString(), dispatched: false };
902
+ await prepareMutation(() => { saveState(context, row, state); appendEvent(context, row.controller_id, "capacity_child_followup", { child_session_id: child.session_id, turn_id: turnId, attempt, operation_id: operationId }); });
903
+ const until = performance.now() + wait;
904
+ while (performance.now() < until) {
905
+ await delay(Math.min(250, Math.max(0, until - performance.now())), undefined, { signal: cancellation.signal });
906
+ await renewLease();
907
+ assertOwner();
908
+ }
909
+ await renewLease();
910
+ assertOwner();
911
+ const current = getFlowRunStatus(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id });
912
+ if (current.continuation.stage !== state.stage || ["terminal", "paused"].includes(current.continuation.kind))
913
+ continue;
914
+ if (["blocked", "wait"].includes(current.continuation.kind))
915
+ throw new AppError("capacity_child_followup_blocked", "RUN changed during native child capacity wait", 1, { child_session_id: child.session_id });
916
+ const fresh = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
917
+ const still = controllerNativeChildren(fresh, session.id).find(item => item.session_id === child.session_id);
918
+ const currentLinks = context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: "codex-desktop", session_id: child.session_id })]);
919
+ if (currentLinks.length === 1 && ["completed", "cancelled", "paused"].includes(currentLinks[0].status)) {
920
+ state.last_receipt = fresh;
921
+ saveState(context, row, state);
922
+ continue;
923
+ }
924
+ if (fresh.settled !== true || !still || still.parent_session_id !== session.id || still.status !== "failed" || still.terminal_turn_id !== turnId
925
+ || !nativeItemsSettled(still.native_turn_items) || currentLinks.length > 1
926
+ || currentLinks.some(link => link.status !== "running"))
927
+ throw new AppError("capacity_child_followup_blocked", "Native child or Work changed during capacity wait", 1, { child_session_id: child.session_id, turn_id: turnId });
928
+ await renewLease();
929
+ assertOwner();
930
+ const text = `The existing native child ${child.session_id} has a confirmed terminal overloaded Turn. Use the native followup_task tool exactly once for that same child ID with this short message: "Continue your assigned Work from its current durable state. Do not repeat an observed lifecycle command. Finish only the original Work." Do not spawn another child, claim another Work, or perform the child's Work in this parent Turn. End this Turn after the followup_task response.`;
931
+ await prompt(session, text, operationId, "child");
932
+ state.child_capacity[child.session_id].dispatched = true;
933
+ saveState(context, row, state);
934
+ const after = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
935
+ state.last_receipt = after;
936
+ saveState(context, row, state);
937
+ const observed = controllerNativeChildren(after, session.id).find(item => item.session_id === child.session_id);
938
+ if (!observed || observed.status === "failed" && observed.terminal_turn_id === turnId)
939
+ throw new AppError("capacity_child_followup_unconfirmed", "Native child did not advance to a new Turn after parent followup", 1, { child_session_id: child.session_id, turn_id: turnId });
940
+ continue;
941
+ }
942
+ }
943
+ const fanout = await nextControllerFanout(executionContext, { projectRoot: run.project_root, runId: run.id, stage: next.stage, harness: runtimeHarness(session.profile), sessionId: session.id, children, policy, profiles });
944
+ assertOwner();
945
+ if (fanout.kind === "none") {
946
+ assertNoCorrelatedRejection(context, { projectId: row.project_id, runId: row.run_id, generation: row.generation, stage: next.stage, turn: state.turns });
947
+ const cause = currentLifecycleFailure(executionContext, { runId: run.id, generation: row.generation, stage: next.stage, turn: state.turns, projectId: row.project_id, projectRoot: run.project_root });
948
+ if (cause)
949
+ throw new AppError(cause.code, cause.message, 1, { ...cause.details, ...(cause.invocation_id ? { invocation_id: cause.invocation_id } : {}), ...(cause.hook_event_id ? { hook_event_id: cause.hook_event_id } : {}), controller_wrapper: "incomplete_subject_turn", stage: next.stage });
950
+ throw new AppError("incomplete_subject_turn", "Managed Turn ended without a completed Stage or declared continuation", 1, { stage: next.stage });
951
+ }
952
+ if (fanout.kind === "wait") {
953
+ setStatus(fanout.reason === "qualified_capacity_required" ? "waiting_for_capacity" : "waiting_for_children");
954
+ await delay(1000);
955
+ state.last_receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
956
+ saveState(context, row, state);
957
+ continue;
958
+ }
959
+ setStatus("running");
960
+ if (fanout.kind === "external") {
961
+ const results = await Promise.allSettled(fanout.works.map(async (work) => {
962
+ try {
963
+ await renewLease();
964
+ assertOwner();
965
+ const receipt = await launchExternalWork(executionContext, { workId: work.work_id, stage: next.stage, signal: cancellation.signal });
966
+ if (fatal)
967
+ throw fatal;
968
+ appendEvent(context, row.controller_id, "external_work_observed", { work_id: work.work_id, receipt });
969
+ const launch = context.db.get("SELECT state_dir, profile_json FROM work_launches WHERE work_id = ?", [work.work_id]);
970
+ const status = context.db.get("SELECT status FROM works WHERE work_id = ?", [work.work_id])?.status;
971
+ if (!launch || !["completed", "failed", "cancelled"].includes(status ?? ""))
972
+ throw new AppError("external_work_unsettled", "External launch returned without its terminal Work receipt; observe it rather than relaunching", 1, { work_id: work.work_id, status });
973
+ const resolved = JSON.parse(launch.profile_json);
974
+ const executable = manifest.adapters[runtimeHarness(resolved.worker)].executable;
975
+ const stopped = await runHarnessAdapter({ executable, args: ["daemon", "stop", "--state-dir", launch.state_dir, "--json"], env: controllerEnvironment(context, manifest), operationId: `${row.controller_id}:${work.work_id}:settle` });
976
+ await confirmStopped(launch.state_dir, stopped);
977
+ appendEvent(context, row.controller_id, "external_work_settled", { work_id: work.work_id, receipt: stopped });
978
+ }
979
+ catch (error) {
980
+ await stopAfterFailure(error);
981
+ throw error;
982
+ }
983
+ }));
984
+ const rejected = results.find((result) => result.status === "rejected");
985
+ if (rejected)
986
+ throw fatal ?? rejected.reason;
987
+ state.last_continuation = null;
988
+ saveState(context, row, state);
989
+ continue;
990
+ }
991
+ if (state.last_continuation === fanout.fingerprint)
992
+ assertNoCorrelatedRejection(context, { projectId: row.project_id, runId: row.run_id, generation: row.generation, stage: next.stage, turn: state.turns });
993
+ retainControllerContinuation(state, fanout.fingerprint, { code: "fanout_stage_nonprogressing", message: "The coordinator repeated an unchanged Work-graph continuation", details: { stage: next.stage, action: fanout.action, before: state.last_continuation, after: fanout.fingerprint, controller_turn: state.turns } });
994
+ saveState(context, row, state);
995
+ await prompt(session, fanout.prompt);
996
+ }
997
+ }
998
+ catch (error) {
999
+ if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
1000
+ throw error;
1001
+ let cause = fatal ?? error;
1002
+ if (!fatal && terminalFinalization) {
1003
+ const record = errorRecord(error);
1004
+ const provenance = { phase: "terminal_finalization", project_root: manifest.request.project_root, run_id: row.run_id, controller_id: row.controller_id };
1005
+ if (error instanceof AppError)
1006
+ Object.assign(error.details, provenance);
1007
+ else
1008
+ cause = new AppError(String(record.code), String(record.message), 1, { ...record.details, ...provenance, cause: record });
1009
+ }
1010
+ await stopAfterFailure(cause);
1011
+ throw fatal ?? cause;
1012
+ }
1013
+ finally {
1014
+ clearInterval(monitor);
1015
+ if (stopping)
1016
+ await stopping;
1017
+ observer.db.close?.();
1018
+ }
1019
+ }
1020
+ function readContexts(file, expected) {
1021
+ const { bytes } = prepareFileBytes(file, "Managed context package");
1022
+ if (digestBytes(bytes) !== expected)
1023
+ throw new AppError("context_checksum_mismatch", "Managed context package SHA-256 differs", 2);
1024
+ const value = prepareJsonText(bytes.toString("utf8"), { file, label: "Managed context package" }).value;
1025
+ if (!value || value.schema_id !== "dd-flow/managed-context@1" || !value.stages || typeof value.stages !== "object" || Array.isArray(value.stages) || Object.keys(value).some(key => !["schema_id", "stages"].includes(key)))
1026
+ throw new AppError("validation", "Expected dd-flow/managed-context@1 with stage file/hash references", 2);
1027
+ const result = {};
1028
+ for (const [stage, source] of Object.entries(value.stages)) {
1029
+ if (!/^[a-z][a-z-]*$/.test(stage) || !source || typeof source !== "object" || Object.keys(source).some(key => !["file", "sha256"].includes(key)) || typeof source.file !== "string" || !path.isAbsolute(source.file) || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
1030
+ throw new AppError("validation", "Invalid managed stage context reference", 2);
1031
+ const { bytes: content } = loadExternalStageContext({ file: source.file, expectedSha256: source.sha256, stage });
1032
+ result[stage] = { bytes: content, sha256: source.sha256 };
1033
+ }
1034
+ return result;
1035
+ }