@deksden-com/dd-flow-cli 0.9.0-beta.123 → 0.9.0-beta.127

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 (122) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +33 -1
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +4 -3
  6. package/dist/cli/help.js +1 -0
  7. package/dist/cli/hook-ingress.js +1 -1
  8. package/dist/cli/run-cli.js +126 -193
  9. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  10. package/dist/cli/runtime-maintenance-ingress.js +51 -0
  11. package/dist/cli/runtime-process.js +128 -0
  12. package/dist/cli.js +8 -5
  13. package/dist/domain/stage-catalog.js +1 -1
  14. package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  17. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  18. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  19. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  20. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  21. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  22. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  23. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  24. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  25. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  26. package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
  27. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
  28. package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
  29. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
  30. package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
  31. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
  32. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  33. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
  34. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  35. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
  36. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  37. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
  38. package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
  39. package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
  40. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  41. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  42. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  43. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  44. package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
  45. package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
  46. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  47. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  48. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  49. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  50. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  51. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  52. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  53. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  54. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  55. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  56. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  57. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  58. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  59. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  60. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  61. package/dist/runtime/context.js +8 -4
  62. package/dist/schemas/flow-run.schema.json +1 -0
  63. package/dist/schemas/plan-review-decision.schema.json +3 -3
  64. package/dist/schemas/run-control-receipt.schema.json +19 -0
  65. package/dist/services/code-checks.js +93 -27
  66. package/dist/services/codex-capacity.js +251 -74
  67. package/dist/services/codex-hook-delivery.js +2 -2
  68. package/dist/services/compatibility-preflight.js +4 -1
  69. package/dist/services/controller-fanout.js +9 -75
  70. package/dist/services/engines.js +70 -20
  71. package/dist/services/eval-snapshots.js +179 -40
  72. package/dist/services/external-work-launch.js +21 -9
  73. package/dist/services/harness-adapter.js +195 -25
  74. package/dist/services/hooks.js +65 -21
  75. package/dist/services/lifecycle-invocations.js +404 -32
  76. package/dist/services/managed-lease-monitor.js +128 -0
  77. package/dist/services/managed-processes.js +117 -53
  78. package/dist/services/merge-server.js +66 -23
  79. package/dist/services/recovery-observation-budget.js +67 -7
  80. package/dist/services/recovery-snapshot-database.js +7 -4
  81. package/dist/services/review-deferral.js +23 -0
  82. package/dist/services/run-control-receipt.js +1 -1
  83. package/dist/services/run-control-worker.js +177 -118
  84. package/dist/services/run-control.js +99 -8
  85. package/dist/services/run-controller-adapter.js +177 -51
  86. package/dist/services/run-controller-capture.js +14 -2
  87. package/dist/services/run-controller-process.js +11 -4
  88. package/dist/services/run-controller-recovery.js +17 -0
  89. package/dist/services/run-controller-state.js +68 -7
  90. package/dist/services/run-controller.js +207 -107
  91. package/dist/services/run-fork.js +8 -3
  92. package/dist/services/run-recovery-guard.js +75 -0
  93. package/dist/services/run-recovery-runtime.js +21 -30
  94. package/dist/services/run-recovery.js +17 -13
  95. package/dist/services/runs.js +40 -7
  96. package/dist/services/runtime-budget.js +32 -15
  97. package/dist/services/runtime-scope-control.js +8 -0
  98. package/dist/services/runtime-scope-resume.js +30 -8
  99. package/dist/services/runtime-scope-worker.js +118 -73
  100. package/dist/services/runtime-service.js +6 -7
  101. package/dist/services/scope-writer-cleanup.js +3 -2
  102. package/dist/services/stage-context.js +135 -0
  103. package/dist/services/stage-lifecycle.js +3 -15
  104. package/dist/services/stage-pause.js +3 -2
  105. package/dist/services/usage.js +14 -3
  106. package/dist/services/verification-matrix.js +30 -1
  107. package/dist/services/vnext-code-review.js +26 -13
  108. package/dist/services/vnext-code.js +10 -8
  109. package/dist/services/vnext-contracts.js +10 -1
  110. package/dist/services/vnext-execution-profile.js +75 -2
  111. package/dist/services/vnext-fanout.js +5 -0
  112. package/dist/services/vnext-merge.js +30 -11
  113. package/dist/services/vnext-plan-review.js +102 -35
  114. package/dist/services/vnext-plan.js +9 -7
  115. package/dist/services/vnext-protocolize.js +15 -9
  116. package/dist/services/vnext-specify.js +6 -6
  117. package/dist/services/work-registry.js +43 -6
  118. package/dist/services/workspace-bootstrap.js +14 -3
  119. package/dist/shared/errors.js +11 -6
  120. package/dist/storage/agent-output.js +26 -0
  121. package/dist/storage/database.js +80 -21
  122. package/package.json +7 -4
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
+ import { requiredOption, optionalOption, optionalNumber, optionalPositiveNumber } from "./argument-values.js";
2
3
  import { resolveCommandPaths, resolveCommandProjectRoot } from "../services/command-context.js";
3
- import { assertAuxiliaryCommandScope } from "../services/lifecycle-invocations.js";
4
+ import { assertAuxiliaryCommandScope, cliManagedInvocationScope, manualRecoveryInvocationScope, assertManualRecoveryInvocationTarget } from "../services/lifecycle-invocations.js";
4
5
  import path from "node:path";
5
6
  import { randomUUID } from "node:crypto";
6
7
  import { quote } from "shell-quote";
@@ -9,11 +10,12 @@ import { verifyCodexHookDelivery } from "../services/codex-hook-delivery.js";
9
10
  import { assertLifecycleInvocationCurrent, awaitLifecycleInvocation, bindLifecyclePublishedPacket, expandLifecycleInvocationArgs, issueLifecycleInvocation, prepareLifecycleIssuance, lifecycleInvocationScope, observedLifecycleDiagnostic, observedLifecycleInvocation, resolveObservedLifecycleArgs, retainObservedLifecycleSemantics, settleLifecycleInvocation, settleLifecyclePreparationRejection, settleLifecycleRejection, validateLifecyclePathAliases } from "../services/lifecycle-invocations.js";
10
11
  import { createContext, createRouterContext } from "../runtime/context.js";
11
12
  import { dispatchNativeHook, isNativeHookIngress } from "./native-hook-ingress.js";
13
+ import { isRuntimeMaintenance, runtimeMaintenanceDiagnostics, withMaintenanceDiagnosticScope } from "./runtime-maintenance-diagnostics.js";
12
14
  import { migrateStoreWriter } from "../storage/writer-migration.js";
13
15
  import { helpForArgs } from "./help.js";
14
- import { AppError, isAppError } from "../shared/errors.js";
16
+ import { AppError, errorRecord, isAppError } from "../shared/errors.js";
15
17
  import { entityReferenceVariants } from "../shared/entity-references.js";
16
- import { writeJson } from "../shared/json.js";
18
+ import { writeJson, parseJsonObject } from "../shared/json.js";
17
19
  import { archiveProject, prepareProjectArchive, getProjectStatus, migrateProjectIds, registerProject, resolveProject } from "../services/projects.js";
18
20
  import { cancelProtocol, getProtocolBlockers, getProtocolStatus, getReadyProtocols, implementProtocol, readyForMerge, registerProtocol, prepareProtocolRegistration, requireProtocol, syncProtocolFromRun, prepareProtocolTransition, transitionProtocol } from "../services/protocols.js";
19
21
  import { getPlanStatus, preparePlanItemMutation, updatePlanItem } from "../services/plans.js";
@@ -41,7 +43,7 @@ import { ingestDroidUsage, ingestAgyUsage, ingestGrokUsage, ingestOpenCodeUsage,
41
43
  import { applyMigration, assessMigrationImpact, assertUpgradeRunStorageReady, planMigration, prepareMigrationApply, verifyMigrationReport } from "../services/migrations.js";
42
44
  import { publishProjectSummary } from "../services/project-summary.js";
43
45
  import { requireProjectByRoot } from "../services/projects.js";
44
- import { resolveProjectRoot } from "../storage/paths.js";
46
+ import { resolveProjectRoot, resolveDdFlowHome } from "../storage/paths.js";
45
47
  import { doctorEngines, bindRunEngine, prepareRunEngineBind, engineInfo, installCurrentEngine, listEngines, resolveEngine, resolveExplicitProjectRoot, resolveOperationProjectRoot, routeArgsThroughEngine } from "../services/engines.js";
46
48
  import { preflightCliCompatibility } from "../services/compatibility-preflight.js";
47
49
  import { finishVnextSpecifyStage, getVnextWorkStatus, isVnextSpecifyFlow, isVnextSpecifyRun, launchVnextSpecify, prepareVnextSpecifyStart } from "../services/vnext-specify.js";
@@ -62,20 +64,17 @@ import { addWorkBatch, prepareWorkBatch, prepareWorkDeps, prepareWorkDelete, pre
62
64
  import { launchExternalWork } from "../services/external-work-launch.js";
63
65
  import { createEvalBootstrapSnapshot, prepareEvalBootstrapSnapshot, createEvalRunSnapshot, prepareEvalRunSnapshotCreation, prepareVnextSpecifyRun, prepareEvalRunRestore, prepareEvalBootstrapRestore, restoreEvalBootstrapSnapshot, restoreEvalRunSnapshot } from "../services/eval-snapshots.js";
64
66
  import { forkRunFromSnapshot, prepareRunFork } from "../services/run-fork.js";
65
- import { acceptRunRecovery, assertRunMutationAllowed, beginRunRecovery, inspectRunRecovery, prepareRecoveryCapture, prepareRecoveryPayload, prepareRunRecoveryResume, recordRecoveryCapture, recoveryGuard, resumeRunRecovery, sealRunRecovery } from "../services/run-recovery.js";
66
- import { recoveryRuntimeAdmission } from "../services/run-recovery-runtime.js";
67
+ import { acceptRunRecovery, beginRunRecovery, inspectRunRecovery, prepareRecoveryCapture, prepareRecoveryPayload, prepareRunRecoveryResume, recordRecoveryCapture, resumeRunRecovery, sealRunRecovery } from "../services/run-recovery.js";
67
68
  import { loadExternalStageContext, prepareExternalStageContext } from "../services/stage-context.js";
68
69
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
69
70
  import { prepareCommandRoute } from "./command-inputs.js";
70
71
  import { parseLifecycleCommand } from "../services/lifecycle-command.js";
71
- import { prepareManagedProcessRegistration, prepareManagedProcessConfirmation, prepareManagedProcessHeartbeat, prepareManagedProcessFinish, prepareManagedProcessStopOperation, prepareManagedProcessReconcile, stopManagedProcess, confirmManagedProcess, finishManagedProcess, heartbeatManagedProcess, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
72
- import { fenceRuntimeBudget, runtimeBudgetStatus, admitRuntimeCheck, parseRuntimeBudget, prepareRuntimeScopeControl, prepareRuntimeScopeFence, reserveProviderTurn, releaseProviderTurns, prepareProviderTurnRelease } from "../services/runtime-budget.js";
72
+ import { dispatchRuntimeProcess, prepareRuntimeProcess } from "./runtime-process.js";
73
+ import { fenceRuntimeBudget, runtimeBudgetStatus, prepareRuntimeScopeControl, prepareRuntimeScopeFence } from "../services/runtime-budget.js";
73
74
  import { stopRuntimeScope } from "../services/runtime-scope-stop.js";
74
75
  import { launchRuntimeScopeControl, launchRuntimeScopeResume, prepareRuntimeScopeReconciliation, prepareRuntimeScopeServing, reconcileRuntimeScopeControl, serveRuntimeScopeControl } from "../services/runtime-scope-worker.js";
75
76
  import { prepareRuntimeScopeResumeRequest } from "../services/runtime-scope-resume.js";
76
77
  import { launchRunController, prepareRunControllerLaunch, resumeRunController, serveRunController, runControllerStatus, answerRunController, prepareRunControllerAnswer, supplyRunControllerContext, prepareRunControllerContext } from "../services/run-controller.js";
77
- import { controllerHitlAdmission } from "../services/run-controller-adapter.js";
78
- import { assertCapacityOwnerAdmission } from "../services/codex-capacity.js";
79
78
  import { controlLifecycleReply, runControlStatus } from "../services/run-control.js";
80
79
  import { reconcileRunControl, requestRunControl, serveRunControl } from "../services/run-control-worker.js";
81
80
  import { startRuntimeService, prepareRuntimeService } from "../services/runtime-service.js";
@@ -86,6 +85,9 @@ const defaultIo = {
86
85
  };
87
86
  const flagValue = "__dd_flow_flag__";
88
87
  export async function runCli(args, io = defaultIo, env = process.env) {
88
+ return withMaintenanceDiagnosticScope(() => runCliRequest(args, io, env));
89
+ }
90
+ async function runCliRequest(args, io, env) {
89
91
  let output;
90
92
  try {
91
93
  output = parseOutputOptions(args);
@@ -94,7 +96,10 @@ export async function runCli(args, io = defaultIo, env = process.env) {
94
96
  const rejection = isAppError(error) ? error : new AppError("usage", error instanceof Error ? error.message : String(error), 2);
95
97
  Object.assign(rejection.details, { phase: "prepare", effect: "no_effect", recoverable: true, ...rejection.details });
96
98
  try {
97
- const command = quote(["dd-flow", ...args]), event = observedLifecycleNativeEvent(createRouterContext(env), command, env.DD_FLOW_DAEMON_ID);
99
+ const command = quote(["dd-flow", ...args]);
100
+ const analysis = parseLifecycleCommand(command);
101
+ const event = analysis.kind !== "none" || analysis.auxiliary
102
+ ? observedLifecycleNativeEvent(createRouterContext(env), command, env.DD_FLOW_DAEMON_ID) : null;
98
103
  if (event) {
99
104
  const context = createContext({ ...env }, "hook");
100
105
  try {
@@ -108,7 +113,7 @@ export async function runCli(args, io = defaultIo, env = process.env) {
108
113
  catch (persistence) {
109
114
  rejection.details.lifecycle_diagnostic_error = String(persistence);
110
115
  }
111
- writeJson(io.stderr, { ok: false, error: { code: rejection.code, message: rejection.message, details: rejection.details } });
116
+ writeJson(io.stderr, { ok: false, error: errorRecord(rejection) });
112
117
  return rejection.exitCode;
113
118
  }
114
119
  const progress = progressForCommand(output.args);
@@ -116,6 +121,7 @@ export async function runCli(args, io = defaultIo, env = process.env) {
116
121
  let invocationSettlement;
117
122
  let settleHook;
118
123
  let diagnosticHome;
124
+ let maintenanceDiagnostics;
119
125
  try {
120
126
  if (output.args.length === 1 && output.args[0] === "--version") {
121
127
  const payload = getCliVersionReport();
@@ -135,6 +141,36 @@ export async function runCli(args, io = defaultIo, env = process.env) {
135
141
  writeJson(io.stdout, await dispatchNativeHook(output.args, await readStdin(io.stdin), env));
136
142
  return 0;
137
143
  }
144
+ if (output.args.slice(0, 3).join(" ") === "codex hook handle") {
145
+ const parsed = prepareCommandRoute(output.args).parsed;
146
+ const stdin = await readStdin(io.stdin);
147
+ const payload = parseJsonObject(stdin || "{}", "codex hook stdin");
148
+ const event = requiredOption(parsed, "event");
149
+ const isolatedHome = event === "PreToolUse" ? isolatedDdFlowHomeFromHookStdin(stdin) : null;
150
+ const hookEnv = isolatedHome ? { ...env, DD_FLOW_HOME: isolatedHome } : env;
151
+ const proof = await verifyCodexHookDelivery(hookEnv, payload);
152
+ let opened;
153
+ const mode = databaseModeForCommand(output.args, classifyCliOperation(output.args, hookEnv));
154
+ const context = {
155
+ env: hookEnv, ddFlowHome: resolveDdFlowHome(hookEnv), now: () => new Date().toISOString(),
156
+ get db() { return (opened ??= createContext(hookEnv, mode)).db; }
157
+ };
158
+ try {
159
+ // Retired events retain the explicit startup/migration path. PreToolUse
160
+ // only opens its selected prepared store when the handler needs it.
161
+ if (mode !== "hook")
162
+ opened = createContext(hookEnv, mode);
163
+ if (proof)
164
+ context.codexHookProof = proof;
165
+ const result = handleCodexHook(context, { event, projectRoot: optionalOption(parsed, "project-root"), stdin });
166
+ writeJson(io.stdout, result && typeof result === "object" && !Array.isArray(result) && "hookSpecificOutput" in result
167
+ ? { hookSpecificOutput: result.hookSpecificOutput } : {});
168
+ return 0;
169
+ }
170
+ finally {
171
+ opened?.db.close?.();
172
+ }
173
+ }
138
174
  if (output.args[0] === "run" && ["drive", "control"].includes(output.args[1] ?? "")) {
139
175
  const controlArgs = parseArgs(output.args.slice(2));
140
176
  requiredOption(controlArgs, "run");
@@ -151,22 +187,45 @@ export async function runCli(args, io = defaultIo, env = process.env) {
151
187
  required_marker: "DD_FLOW_COMPATIBILITY_MODE=mb-upgrade"
152
188
  });
153
189
  }
154
- const routerContext = createRouterContext(env);
190
+ // Registration may bootstrap the resource registry, but never the unrelated
191
+ // RUN store. Its caller's absolute deadline also bounds that first open.
192
+ const maintenanceOptions = isRuntimeMaintenance(output.args) ? { resourceDatabaseMode: output.args[2] === "register" ? "initialize" : output.args[2] === "status" ? "read_existing" : "prepared_existing", ...(env.DD_FLOW_HOOK_DEADLINE_AT ? { databaseDeadlineAt: Number(env.DD_FLOW_HOOK_DEADLINE_AT) } : {}) } : {};
193
+ // Keep ordinary CLI JSON/stderr compatibility; bounded maintenance callers
194
+ // provide this request's deadline and consume its structured phase evidence.
195
+ if (maintenanceOptions.databaseDeadlineAt !== undefined)
196
+ maintenanceDiagnostics = runtimeMaintenanceDiagnostics(output.args, maintenanceOptions.databaseDeadlineAt, line => io.stderr.write(line));
197
+ const routerContext = createRouterContext(env, maintenanceOptions.databaseDeadlineAt);
155
198
  const publicCommand = quote(["dd-flow", ...output.args]);
156
199
  const nativeCommand = quote(["dd-flow", ...args]);
157
200
  const lifecycleCommand = parseLifecycleCommand(publicCommand);
158
201
  if (lifecycleCommand.kind === "standalone")
159
202
  validateLifecyclePathAliases(output.args, lifecycleCommand.invocation.operation);
160
- const observedInvocation = observedLifecycleInvocation(routerContext, publicCommand, env.DD_FLOW_DAEMON_ID);
203
+ const managedScope = lifecycleCommand.kind === "standalone" && env.DD_FLOW_DAEMON_ID ? cliManagedInvocationScope({ ...routerContext, env }) : null;
161
204
  const nativeEvent = env.DD_FLOW_DAEMON_ID && lifecycleCommand.kind === "standalone"
162
205
  ? observedLifecycleNativeEvent(routerContext, nativeCommand, env.DD_FLOW_DAEMON_ID) : null;
206
+ const nativeEventKey = nativeEvent && routerContext.db.get("SELECT event_key FROM hook_events WHERE project_id = ? AND id = ?", [nativeEvent.project_id, nativeEvent.id])?.event_key;
207
+ const observedInvocation = managedScope ? observedLifecycleInvocation(routerContext, publicCommand, managedScope, nativeEventKey ? { eventKey: nativeEventKey } : undefined) : null;
208
+ if (managedScope && suppliedInvocationId
209
+ && routerContext.db.get("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'lifecycle_invocations'")
210
+ && routerContext.db.get("SELECT id FROM lifecycle_invocations WHERE id = ?", [suppliedInvocationId])) {
211
+ const suppliedScope = lifecycleInvocationScope({ ...routerContext, env }, suppliedInvocationId);
212
+ if (["projectRoot", "daemonId", "rootSessionId", "runId", "generation"].some(key => suppliedScope[key] !== managedScope[key])) {
213
+ throw new AppError("invocation_scope_mismatch", "Explicit lifecycle assignment differs from the current physical native owner", 1, { effect: "no_effect", recoverable: false });
214
+ }
215
+ }
163
216
  const diagnostic = env.DD_FLOW_DAEMON_ID && lifecycleCommand.kind === "standalone"
164
217
  ? observedLifecycleDiagnostic(routerContext, publicCommand, env.DD_FLOW_DAEMON_ID, observedInvocation?.id) : null;
165
218
  if (diagnostic && (!nativeEvent || Number(diagnostic.hook_event_id) > nativeEvent.id)) {
166
219
  throw new AppError("lifecycle_shell_syntax_invalid", `Native observer rejected the shell form: ${String(diagnostic.reason ?? "unsupported shell composition")}. Write structured input to a file and run the returned dd-flow command separately.`, 2, { ...diagnostic, phase: "prepare", effect: "no_effect", recoverable: true });
167
220
  }
221
+ let manualRecoveryHookEventKey;
168
222
  if (!observedInvocation && !suppliedInvocationId && env.DD_FLOW_DAEMON_ID && lifecycleCommand.kind === "standalone") {
169
- throw new AppError(nativeEvent ? "invocation_assignment_missing" : "invocation_receipt_missing", nativeEvent ? "Native call was observed, but its arguments do not match an issued lifecycle assignment" : "Managed lifecycle command has no committed native receipt", 1, { effect: "no_effect", recoverable: false });
223
+ const receipt = nativeEvent && routerContext.db.get("SELECT event_key,harness,provider_session_id,parent_session_id,daemon_id,status FROM hook_events WHERE id = ? AND project_id = ?", [nativeEvent.id, nativeEvent.project_id]);
224
+ const manual = managedScope && receipt?.status === "observed" ? manualRecoveryInvocationScope({ ...routerContext, env }, managedScope.projectRoot, { daemonId: receipt.daemon_id, rootSessionId: receipt.provider_session_id, sessionId: receipt.provider_session_id, parentSessionId: receipt.parent_session_id, toolCallId: String(nativeEvent.id) }, receipt.harness) : null;
225
+ if (!manual || JSON.stringify(manual) !== JSON.stringify(managedScope))
226
+ throw new AppError(nativeEvent ? "invocation_assignment_missing" : "invocation_receipt_missing", nativeEvent ? "Native call was observed, but its arguments do not match an issued lifecycle assignment" : "Managed lifecycle command has no committed native receipt", 1, { effect: "no_effect", recoverable: false });
227
+ assertManualRecoveryInvocationTarget({ ...routerContext, env }, manual, publicCommand);
228
+ manualRecoveryHookEventKey = receipt.event_key;
170
229
  }
171
230
  const invocationId = observedInvocation?.id ?? suppliedInvocationId;
172
231
  let boundInvocationArgs;
@@ -207,7 +266,7 @@ export async function runCli(args, io = defaultIo, env = process.env) {
207
266
  }
208
267
  // Route by retained authority, so a mistyped project path reaches the CLI
209
268
  // validator instead of selecting a different engine or failing preflight.
210
- const scopeProjectRoot = invocationScope?.projectRoot ?? resolveCommandProjectRoot(routerContext, output.args, resolveProjectRootBeforeRouting(output.args, env));
269
+ const scopeProjectRoot = invocationScope?.projectRoot ?? resolveCommandProjectRoot(routerContext, output.args, resolveProjectRootBeforeRouting(output.args, env, maintenanceOptions.databaseDeadlineAt));
211
270
  assertAuxiliaryCommandScope({ ...routerContext, env }, publicCommand, scopeProjectRoot);
212
271
  invocationArgs = resolveCommandPaths(routerContext, invocationArgs, scopeProjectRoot);
213
272
  const routedArgs = output.responseFile ? [...invocationArgs, "--response-file", output.responseFile] : invocationArgs;
@@ -223,7 +282,7 @@ export async function runCli(args, io = defaultIo, env = process.env) {
223
282
  let preparedInput;
224
283
  let preparedRoute;
225
284
  if (!invocationId) {
226
- const preparationContext = createContext({ ...env }, "read_existing");
285
+ const preparationContext = createContext({ ...env }, "read_existing", undefined, maintenanceOptions);
227
286
  try {
228
287
  preparedInput = await prepareCliInput(invocationArgs, preparationContext, io, scopeProjectRoot, output.responseFile);
229
288
  }
@@ -232,8 +291,8 @@ export async function runCli(args, io = defaultIo, env = process.env) {
232
291
  }
233
292
  }
234
293
  const dispatchContext = routed?.inProcess
235
- ? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version }, databaseModeForCommand(invocationArgs, classification))
236
- : createContext({ ...env }, databaseModeForCommand(invocationArgs, classification));
294
+ ? createContext({ ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: "in-process", DD_FLOW_ROUTED_FROM: getCliVersionReport().cli.version }, databaseModeForCommand(invocationArgs, classification), undefined, maintenanceOptions)
295
+ : createContext({ ...env }, databaseModeForCommand(invocationArgs, classification), undefined, maintenanceOptions);
237
296
  diagnosticHome = dispatchContext.ddFlowHome;
238
297
  dispatchContext.env.DD_FLOW_INVOCATION_READONLY = classification.mode === "read_only_diagnostics" ? "1" : undefined;
239
298
  let dispatchArgs = invocationArgs;
@@ -271,6 +330,13 @@ export async function runCli(args, io = defaultIo, env = process.env) {
271
330
  dispatchContext.env.DD_FLOW_CURRENT_INVOCATION = invocationId;
272
331
  }
273
332
  }
333
+ if (!invocationId && manualRecoveryHookEventKey) {
334
+ // Keep the physically scoped receipt selected by admission; a service must
335
+ // not rediscover a different daemon's matching command event.
336
+ if (!invocationArgs.includes("--hook-event-id"))
337
+ dispatchArgs = [...invocationArgs, "--hook-event-id", manualRecoveryHookEventKey];
338
+ preparedInput?.route?.parsed.options.set("hook-event-id", [manualRecoveryHookEventKey]);
339
+ }
274
340
  if (retainedOutcome?.error) {
275
341
  const error = retainedOutcome.error;
276
342
  throw new AppError(error.code, error.message, error.exitCode, error.details);
@@ -313,6 +379,9 @@ export async function runCli(args, io = defaultIo, env = process.env) {
313
379
  emitHumanProgress(io, output, progress, "done");
314
380
  const payload = attachProgress(resultWithWarnings, progress, output);
315
381
  const visible = writeResponseFile(payload, output);
382
+ const maintenanceReceipt = payload;
383
+ const maintenanceExpiry = maintenanceReceipt?.lease_expires_at ?? maintenanceReceipt?.process?.lease_expires_at;
384
+ maintenanceDiagnostics?.phase("ack", { ...(typeof maintenanceExpiry === "string" && Number.isFinite(Date.parse(maintenanceExpiry)) ? { lease_expires_at: maintenanceExpiry } : {}) });
316
385
  if (output.json)
317
386
  writeJson(io.stdout, visible);
318
387
  else
@@ -320,19 +389,24 @@ export async function runCli(args, io = defaultIo, env = process.env) {
320
389
  return completedCommandExitCode(payload);
321
390
  }
322
391
  catch (caught) {
323
- const error = isAppError(caught) ? caught : new AppError("unexpected", caught instanceof Error ? caught.message : String(caught), 1, { effect: "unknown", cause: { code: caught?.code ?? null, sqlite_extended_code: caught?.errcode ?? null } });
392
+ const record = errorRecord(caught);
393
+ maintenanceDiagnostics?.failure(caught);
394
+ const error = isAppError(caught) ? caught : Object.assign(new AppError(record.code, record.message, record.exitCode, { effect: "unknown", ...record.details, ...(caught?.errcode !== undefined ? { sqlite_extended_code: caught.errcode } : {}) }), { ...(record.cause ? { cause: record.cause } : {}), ...(record.cleanup_error ? { cleanup_error: record.cleanup_error } : {}), ...(record.retryable !== undefined ? { retryable: record.retryable } : {}) });
324
395
  if (!settleHook && !invocationSettlement) {
325
396
  let earlyContext;
326
397
  try {
327
398
  const command = quote(["dd-flow", ...args]);
328
- const router = createRouterContext(env);
329
- const event = observedLifecycleNativeEvent(router, command, env.DD_FLOW_DAEMON_ID);
399
+ const analysis = parseLifecycleCommand(command);
400
+ // Maintenance failures must not start an unrelated, unbounded hook
401
+ // lookup after their own request deadline has already expired.
402
+ const router = analysis.kind !== "none" || analysis.auxiliary ? createRouterContext(env) : null;
403
+ const event = router ? observedLifecycleNativeEvent(router, command, env.DD_FLOW_DAEMON_ID) : null;
330
404
  // A correctable prepare rejection precedes the terminal command's
331
405
  // single-use claim. Correlate its unique hook by the same lifecycle
332
406
  // match key used at dispatch, including compound provider tool shells;
333
407
  // otherwise a repaired retry sees two fresh, unclaimed events.
334
- if (event || (error.details.phase === "prepare" && error.details.effect === "no_effect"
335
- && fs.existsSync(path.join(router.ddFlowHome, "db.sqlite")))) {
408
+ if (router && (event || (error.details.phase === "prepare" && error.details.effect === "no_effect"
409
+ && fs.existsSync(path.join(router.ddFlowHome, "db.sqlite"))))) {
336
410
  earlyContext = createContext({ ...env }, "hook");
337
411
  observeLifecycleCommand(earlyContext, command, event ?? undefined)({ error });
338
412
  }
@@ -360,8 +434,13 @@ export async function runCli(args, io = defaultIo, env = process.env) {
360
434
  clearInterval(heartbeat);
361
435
  if (invocationSettlement && error.details.effect !== "committed") {
362
436
  try {
363
- if (isAppError(error))
364
- settleLifecycleRejection(invocationSettlement.context, invocationSettlement.id, { code: error.code, message: error.message, exitCode: error.exitCode, details: error.details });
437
+ if (isAppError(error)) {
438
+ const settled = errorRecord(error);
439
+ settleLifecycleRejection(invocationSettlement.context, invocationSettlement.id, settled);
440
+ // Serialization owns a copy. Publish the committed settlement, not
441
+ // the original rejection which has no successor authority.
442
+ Object.assign(error.details, settled.details);
443
+ }
365
444
  else
366
445
  settleLifecycleInvocation(invocationSettlement.context, invocationSettlement.id, { error: { code: "unexpected", message: String(error), exitCode: 1, details: {} } });
367
446
  }
@@ -377,9 +456,7 @@ export async function runCli(args, io = defaultIo, env = process.env) {
377
456
  io.stderr.write(`${JSON.stringify({ primary: String(error), lifecycle_persistence_error: String(persistence) })}\n`);
378
457
  }
379
458
  }
380
- const payload = isAppError(error)
381
- ? { ok: false, error: { code: error.code, message: error.message, details: error.details } }
382
- : { ok: false, error: { code: "unexpected", message: String(error) } };
459
+ const payload = { ok: false, error: errorRecord(error) };
383
460
  progress.splice(0, progress.length, ...progress.filter(event => event.phase !== "done"));
384
461
  if (progress.length)
385
462
  progress.push({ phase: "failed", message: payload.error.message, at: new Date().toISOString() });
@@ -403,14 +480,23 @@ export async function runCli(args, io = defaultIo, env = process.env) {
403
480
  io.stderr.write(renderHumanError(payload));
404
481
  return isAppError(error) ? error.exitCode : 1;
405
482
  }
483
+ finally {
484
+ maintenanceDiagnostics?.close();
485
+ }
406
486
  }
407
- function resolveProjectRootBeforeRouting(args, env) {
487
+ function resolveProjectRootBeforeRouting(args, env, deadlineAt) {
408
488
  const explicit = resolveExplicitProjectRoot(args);
409
489
  if (explicit)
410
490
  return explicit;
411
491
  try {
412
492
  // Router identity lookup is read-only; it must not initialize or migrate runtime storage.
413
- return resolveOperationProjectRoot(createContext(env, "read_existing"), args);
493
+ const context = createContext(env, "read_existing", undefined, deadlineAt === undefined ? {} : { databaseDeadlineAt: deadlineAt });
494
+ try {
495
+ return resolveOperationProjectRoot(context, args);
496
+ }
497
+ finally {
498
+ context.db.close?.();
499
+ }
414
500
  }
415
501
  catch {
416
502
  return null;
@@ -419,6 +505,8 @@ function resolveProjectRootBeforeRouting(args, env) {
419
505
  function databaseModeForCommand(args, classification) {
420
506
  if (isNativeHookIngress(args))
421
507
  return "hook";
508
+ if (isRuntimeMaintenance(args))
509
+ return "read_existing";
422
510
  const [family, command] = args;
423
511
  // Managed attempts are issued only from a prepared store. Opening that
424
512
  // existing writer must not run migrations before their input is admitted.
@@ -1005,22 +1093,8 @@ async function prepareCliInputValue(args, context, io, scopeProjectRoot, respons
1005
1093
  return prepareError(error, { parameter: "scope-id/request-id/manifest-json" });
1006
1094
  }
1007
1095
  }
1008
- if (route.key === "runtime process register") {
1009
- prepareManagedProcessRegistration(context, runtimeProcessRegistrationInput(context, parsed));
1010
- }
1011
- if (route.key === "runtime process heartbeat")
1012
- prepareManagedProcessHeartbeat(context, runtimeProcessHeartbeatInput(parsed));
1013
- if (route.key === "runtime process finish")
1014
- prepareManagedProcessFinish(context, runtimeProcessFinishInput(parsed));
1015
- if (route.key === "runtime process confirm")
1016
- prepareManagedProcessConfirmation(context, runtimeProcessConfirmationInput(parsed));
1017
- if (route.key === "runtime process stop")
1018
- prepareManagedProcessStopOperation(context, runtimeProcessStopInput(parsed));
1019
- if (route.key === "runtime process reconcile")
1020
- prepareManagedProcessReconcile(requiredOption(parsed, "owner"), optionalNumber(parsed, "grace-ms"));
1021
- if (route.key === "runtime process release-turn") {
1022
- prepared.providerTurnRelease = prepareProviderTurnRelease({ daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") });
1023
- }
1096
+ if (family === "runtime" && command === "process")
1097
+ Object.assign(prepared, prepareRuntimeProcess(context, route.key, parsed));
1024
1098
  if (route.key === "runtime process start") {
1025
1099
  try {
1026
1100
  prepareRuntimeService(context, runtimeServiceInput(parsed));
@@ -1990,7 +2064,8 @@ async function dispatch(args, context, io, scopeProjectRoot = null, classificati
1990
2064
  return dispatchCodexHome(context, parsed, prepared);
1991
2065
  }
1992
2066
  if (family === "codex" && command === "hook") {
1993
- return dispatchCodexHook(context, parsed, await readStdin(io.stdin));
2067
+ // `handle` is owned by the pre-context ingress above, never generic dispatch.
2068
+ throw new AppError("usage", "Use: dd-flow codex hook handle --event EVENT", 2);
1994
2069
  }
1995
2070
  if (family === "zcode" && command === "hook") {
1996
2071
  return dispatchZcodeHook(context, parsed, await readStdin(io.stdin));
@@ -2383,14 +2458,14 @@ function snapshotCreateInput(parsed) {
2383
2458
  return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 1, "run-id"), output: requiredOption(parsed, "output"), candidate: parsed.options.has("candidate"), incomplete: parsed.options.has("incomplete"), ...(recoveryId !== undefined ? { recoveryId } : {}), ...(stageEntry !== undefined ? { stageEntry } : {}) };
2384
2459
  }
2385
2460
  function snapshotRestoreInput(parsed) {
2386
- return { snapshot: requiredOption(parsed, "snapshot"), projectRoot: requiredOption(parsed, "project-root"), executionRoutingFile: optionalOption(parsed, "execution-routing-file"), ...(optionalOption(parsed, "recovery-id") ? { recoveryId: requiredOption(parsed, "recovery-id") } : {}) };
2461
+ return { snapshot: requiredOption(parsed, "snapshot"), projectRoot: requiredOption(parsed, "project-root"), executionRoutingFile: optionalOption(parsed, "execution-routing-file"), operationalDecisionFile: optionalOption(parsed, "operational-decision-file"), ...(optionalOption(parsed, "recovery-id") ? { recoveryId: requiredOption(parsed, "recovery-id") } : {}) };
2387
2462
  }
2388
2463
  function runStartInput(parsed) {
2389
2464
  const protocolOverrides = readOptionalJsonFile(optionalOption(parsed, "protocol-override-file"));
2390
2465
  return { projectRoot: requiredOption(parsed, "project-root"), workspaceRoot: optionalOption(parsed, "workspace-root"), flowKind: requiredOption(parsed, "flow-kind"), subjectType: requiredOption(parsed, "subject-type"), subjectId: requiredOption(parsed, "subject-id"), slug: requiredOption(parsed, "slug"), nextAction: optionalOption(parsed, "next-action"), preset: optionalOption(parsed, "preset"), taskProfile: readOptionalJsonFile(optionalOption(parsed, "task-profile-file")), executionRoutingFile: optionalOption(parsed, "execution-routing-file"), ...(protocolOverrides !== undefined ? { protocolOverrides } : {}), runOverrides: parseFlowFlagOverrides(allOptions(parsed, "flag")) };
2391
2466
  }
2392
2467
  function runForkInput(parsed) {
2393
- return { snapshot: requiredOption(parsed, "from"), output: requiredOption(parsed, "output"), engineVersion: requiredOption(parsed, "engine-version"), requestId: requiredOption(parsed, "request-id"), ...(optionalOption(parsed, "integrity-checksum") ? { integrityChecksum: optionalOption(parsed, "integrity-checksum") } : {}) };
2468
+ return { snapshot: requiredOption(parsed, "from"), output: requiredOption(parsed, "output"), engineVersion: requiredOption(parsed, "engine-version"), requestId: requiredOption(parsed, "request-id"), ...(optionalOption(parsed, "integrity-checksum") ? { integrityChecksum: optionalOption(parsed, "integrity-checksum") } : {}), ...(optionalOption(parsed, "operational-decision-file") ? { operationalDecisionFile: optionalOption(parsed, "operational-decision-file") } : {}) };
2394
2469
  }
2395
2470
  function recoveryCaptureInput(parsed) {
2396
2471
  return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredPosition(parsed, 1, "run-id"), recoveryId: requiredOption(parsed, "recovery-id"), capturePath: requiredOption(parsed, "capture-path") };
@@ -2542,44 +2617,9 @@ function stageCompletionInput(parsed) {
2542
2617
  function runtimeServiceInput(parsed) {
2543
2618
  return { projectRoot: requiredOption(parsed, "project-root"), runId: requiredOption(parsed, "run"), command: requiredOption(parsed, "command"), ports: requiredOption(parsed, "ports").split(","), readyPort: requiredOption(parsed, "ready-port"), readyPath: optionalOption(parsed, "ready-path") ?? "/", timeoutMs: optionalPositiveNumber(parsed, "ready-timeout-ms", true) ?? 30_000 };
2544
2619
  }
2545
- function runtimeProcessStopInput(parsed) {
2546
- const graceMs = optionalNumber(parsed, "grace-ms");
2547
- return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(graceMs !== undefined ? { graceMs } : {}) };
2548
- }
2549
2620
  function runtimeScopeResumeInput(parsed) {
2550
2621
  return { scopeId: requiredOption(parsed, "scope-id"), requestId: requiredOption(parsed, "request-id"), generation: Number(requiredOption(parsed, "generation")), captureKey: requiredOption(parsed, "capture-key") };
2551
2622
  }
2552
- function runtimeProcessRegistrationInput(context, parsed) {
2553
- const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
2554
- const budget = parseRuntimeBudget(optionalOption(parsed, "budget-json"));
2555
- const kind = requiredOption(parsed, "kind");
2556
- const projectId = optionalOption(parsed, "project-id") ?? null, runId = optionalOption(parsed, "run") ?? null;
2557
- const guard = projectId && runId ? recoveryGuard(context, projectId, runId) : null;
2558
- const run = projectId && runId ? context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]) : null;
2559
- return {
2560
- kind, ownerId: requiredOption(parsed, "owner"), uniqueActiveOperation: kind === "eval-observer",
2561
- ...(kind === "eval-observer" ? { pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(optionalPositiveNumber(parsed, "process-group-id", true) ? { processGroupId: optionalPositiveNumber(parsed, "process-group-id", true) } : {}) } : {}),
2562
- ownerPid: optionalPositiveNumber(parsed, "owner-pid", true), projectId, runId,
2563
- workId: optionalOption(parsed, "work") ?? null, checkId: optionalOption(parsed, "check") ?? null,
2564
- operationId: optionalOption(parsed, "operation") ?? null, stdoutPath: optionalOption(parsed, "stdout") ?? null,
2565
- stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs !== undefined ? { leaseMs } : {}),
2566
- metadata: { ...(budget ? { budget, harness: optionalOption(parsed, "harness") ?? null } : {}), ...(projectId && runId ? { run_generation: guard?.generation ?? 0, run_admitted: Boolean(run) && !["draining", "sealed", "resuming"].includes(guard?.status ?? "") } : {}), ...(optionalOption(parsed, "dispatch-barrier") ? { dispatch_barrier: optionalOption(parsed, "dispatch-barrier") } : {}), ...(optionalOption(parsed, "role") ? { role: optionalOption(parsed, "role") } : {}), ...(optionalOption(parsed, "state-dir") ? { state_dir: optionalOption(parsed, "state-dir") } : {}), ...(optionalOption(parsed, "adapter-executable") ? { adapter_executable: optionalOption(parsed, "adapter-executable") } : {}) }
2567
- };
2568
- }
2569
- function runtimeProcessConfirmationInput(parsed) {
2570
- const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true), processGroupId = optionalPositiveNumber(parsed, "process-group-id", true);
2571
- return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ownerPid: optionalPositiveNumber(parsed, "owner-pid", true), pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(processGroupId !== undefined ? { processGroupId } : {}), ...(leaseMs !== undefined ? { leaseMs } : {}) };
2572
- }
2573
- function runtimeProcessHeartbeatInput(parsed) {
2574
- const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
2575
- return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(leaseMs !== undefined ? { leaseMs } : {}) };
2576
- }
2577
- function runtimeProcessFinishInput(parsed) {
2578
- const state = requiredOption(parsed, "state");
2579
- if (state !== "stopped" && state !== "failed")
2580
- throw new AppError("validation", "--state must be stopped or failed", 2);
2581
- return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), state, reason: optionalOption(parsed, "reason") ?? null };
2582
- }
2583
2623
  async function dispatchRuntime(context, command, parsed, progress, prepared = {}) {
2584
2624
  if (command === "scope") {
2585
2625
  if (parsed.positional.length === 1 && parsed.positional[0] === "reconcile") {
@@ -2622,60 +2662,9 @@ async function dispatchRuntime(context, command, parsed, progress, prepared = {}
2622
2662
  if (command !== "process")
2623
2663
  throw new AppError("usage", "Usage: dd-flow runtime <scope|process>; see dd-flow runtime scope --help", 2);
2624
2664
  const action = requiredPosition(parsed, 0, "runtime process action");
2625
- if (action === "check-admission")
2626
- return { ok: true, ...admitRuntimeCheck(context, requiredOption(parsed, "id"), requiredOption(parsed, "lease-token")) };
2627
2665
  if (action === "start")
2628
2666
  return await startRuntimeService(context, { ...runtimeServiceInput(parsed), progress });
2629
- if (action === "stop")
2630
- return { ok: true, process: await stopManagedProcess(context, runtimeProcessStopInput(parsed)) };
2631
- if (action === "register") {
2632
- const process = registerManagedProcess(context, runtimeProcessRegistrationInput(context, parsed));
2633
- return { ok: true, process };
2634
- }
2635
- if (action === "confirm")
2636
- return { ok: true, process: confirmManagedProcess(context, runtimeProcessConfirmationInput(parsed)) };
2637
- if (action === "heartbeat")
2638
- return { ok: heartbeatManagedProcess(context, runtimeProcessHeartbeatInput(parsed)) };
2639
- if (action === "admission") {
2640
- const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
2641
- if (!record || record.lease_token !== requiredOption(parsed, "lease-token") || record.owner_id !== requiredOption(parsed, "owner") || record.state !== "running" || !record.pid || !record.pid_started_at || !processIsAlive(record))
2642
- throw new AppError("process_ownership_unknown", "Productive dispatch requires its current confirmed runtime owner", 1);
2643
- const metadata = JSON.parse(record.metadata_json);
2644
- if (metadata.state_dir !== requiredOption(parsed, "state-dir"))
2645
- throw new AppError("process_ownership_unknown", "Runtime admission belongs to another state directory", 1);
2646
- if (record.project_id && record.run_id) {
2647
- if (managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
2648
- throw new AppError("process_runtime_scope_mismatch", "Runtime admission belongs to another or unproven flow home", 1);
2649
- const recovering = recoveryRuntimeAdmission(context, record, { stateDir: requiredOption(parsed, "state-dir"), daemonId: optionalOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), operation: optionalOption(parsed, "operation"), nativeSessionId: optionalOption(parsed, "native-session-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
2650
- if (!recovering) {
2651
- if (metadata.run_admitted !== true || !Number.isSafeInteger(metadata.run_generation))
2652
- throw new AppError("run_recovery_guarded", "Runtime was not admitted before the RUN fence; reconcile its launch", 1);
2653
- assertRunMutationAllowed(context, record.project_id, record.run_id, metadata.run_generation);
2654
- }
2655
- const run = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [record.project_id, record.run_id]);
2656
- const retainedHitlPrompt = recovering && ["paused", "waiting_for_user"].includes(run?.status ?? "") && ["session.prompt", "session.resume"].includes(optionalOption(parsed, "operation") ?? "") && context.db.get("SELECT 1 FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND g.run_id = ? AND g.generation = ? AND b.daemon_id = ? AND b.provider_session_id = ?", [record.project_id, record.run_id, metadata.run_generation, optionalOption(parsed, "daemon-id"), optionalOption(parsed, "native-session-id")]);
2657
- const acceptedHitlPrompt = ["paused", "waiting_for_user"].includes(run?.status ?? "") && controllerHitlAdmission(context, record, { stateDir: requiredOption(parsed, "state-dir"), operationId: optionalOption(parsed, "operation-id"), operation: optionalOption(parsed, "operation"), nativeSessionId: optionalOption(parsed, "native-session-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
2658
- if (!run || run.status !== "running" && !retainedHitlPrompt && !acceptedHitlPrompt && !["judge", "probe"].includes(metadata.role ?? ""))
2659
- throw new AppError("invalid_run_state", "Subject dispatch requires a running RUN or its retained recovery/HITL prompt", 1);
2660
- if (!recovering)
2661
- assertCapacityOwnerAdmission(context, record, optionalOption(parsed, "operation-id"));
2662
- }
2663
- const budget = reserveProviderTurn(context, record, { operationId: optionalOption(parsed, "operation-id") ?? "", operation: optionalOption(parsed, "operation") ?? "", sessionId: optionalOption(parsed, "native-session-id"), daemonId: optionalOption(parsed, "daemon-id") });
2664
- return { ok: true, ...budget, process_id: record.id, generation: metadata.run_generation ?? null };
2665
- }
2666
- if (action === "release-turn") {
2667
- const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
2668
- if (!record || record.lease_token !== requiredOption(parsed, "lease-token") || record.owner_id !== requiredOption(parsed, "owner") || managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
2669
- throw new AppError("process_ownership_unknown", "Budget release requires its retained physical owner", 1);
2670
- return releaseProviderTurns(context, record, { daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") }, prepared.providerTurnRelease);
2671
- }
2672
- if (action === "finish")
2673
- return { ok: finishManagedProcess(context, runtimeProcessFinishInput(parsed)) };
2674
- if (action === "status")
2675
- return { ok: true, processes: managedProcessStatus(context) };
2676
- if (action === "reconcile")
2677
- return { ok: true, processes: await reconcileExpiredManagedProcesses(context, requiredOption(parsed, "owner"), optionalNumber(parsed, "grace-ms")) };
2678
- throw new AppError("usage", "Usage: dd-flow runtime process <start|stop|register|confirm|heartbeat|admission|finish|status|reconcile>", 2);
2667
+ return await dispatchRuntimeProcess(context, action, parsed, prepared);
2679
2668
  }
2680
2669
  function dispatchStat(context, command, parsed) {
2681
2670
  if (command === "usage") {
@@ -3048,26 +3037,6 @@ function dispatchCodexHome(context, parsed, prepared) {
3048
3037
  }
3049
3038
  throw new AppError("usage", `Unknown codex home action: ${action}`, 2);
3050
3039
  }
3051
- async function dispatchCodexHook(context, parsed, stdin) {
3052
- const action = requiredPosition(parsed, 0, "codex hook action");
3053
- if (action === "handle") {
3054
- const isolatedHome = isolatedDdFlowHomeFromHookStdin(stdin);
3055
- const hookContext = isolatedHome ? createContext({ ...context.env, DD_FLOW_HOME: isolatedHome }, "hook") : context;
3056
- const proof = await verifyCodexHookDelivery(hookContext.env, JSON.parse(stdin || "{}"));
3057
- if (proof)
3058
- hookContext.codexHookProof = proof;
3059
- const result = handleCodexHook(hookContext, {
3060
- event: requiredOption(parsed, "event"),
3061
- projectRoot: optionalOption(parsed, "project-root"),
3062
- stdin
3063
- });
3064
- if (result && typeof result === "object" && !Array.isArray(result) && "hookSpecificOutput" in result) {
3065
- return { hookSpecificOutput: result.hookSpecificOutput };
3066
- }
3067
- return {};
3068
- }
3069
- throw new AppError("usage", `Unknown codex hook action: ${action}`, 2);
3070
- }
3071
3040
  function dispatchZcodeEvent(context, parsed, stdin) {
3072
3041
  const action = requiredPosition(parsed, 0, "zcode event action");
3073
3042
  if (action === "handle") {
@@ -3214,25 +3183,9 @@ function requiredPosition(parsed, index, label) {
3214
3183
  }
3215
3184
  return value;
3216
3185
  }
3217
- function optionalOption(parsed, key) {
3218
- const values = parsed.options.get(key);
3219
- const value = values?.[values.length - 1];
3220
- if (value === flagValue)
3221
- throw new AppError("usage", `--${key} requires a value`, 2);
3222
- if (value !== undefined && !value.trim())
3223
- throw new AppError("usage", `--${key} requires a non-empty value`, 2);
3224
- return value;
3225
- }
3226
3186
  function allOptions(parsed, key) {
3227
3187
  return (parsed.options.get(key) ?? []).filter((value) => value !== flagValue);
3228
3188
  }
3229
- function requiredOption(parsed, key) {
3230
- const value = optionalOption(parsed, key);
3231
- if (!value) {
3232
- throw new AppError("usage", `Missing required option: --${key}`, 2);
3233
- }
3234
- return value;
3235
- }
3236
3189
  function requiredScopeProjectRoot(root, operation) {
3237
3190
  if (root)
3238
3191
  return root;
@@ -3272,26 +3225,6 @@ function parseBoolean(value, key) {
3272
3225
  function parseOptionalBoolean(value, key) {
3273
3226
  return typeof value === "string" ? parseBoolean(value, key) : undefined;
3274
3227
  }
3275
- function optionalNumber(parsed, key) {
3276
- const value = optionalOption(parsed, key);
3277
- if (typeof value === "undefined") {
3278
- return undefined;
3279
- }
3280
- const parsedNumber = Number(value);
3281
- if (!Number.isFinite(parsedNumber)) {
3282
- throw new AppError("validation", `--${key} must be a number`, 2);
3283
- }
3284
- return parsedNumber;
3285
- }
3286
- function optionalPositiveNumber(parsed, key, integer = false) {
3287
- const value = optionalNumber(parsed, key);
3288
- if (typeof value === "undefined")
3289
- return undefined;
3290
- if (value <= 0 || (integer && !Number.isInteger(value))) {
3291
- throw new AppError("validation", `--${key} must be a positive${integer ? " integer" : ""} number`, 2);
3292
- }
3293
- return value;
3294
- }
3295
3228
  function requiredWorkerId(parsed) {
3296
3229
  return requiredOption(parsed, "worker-id");
3297
3230
  }
@@ -0,0 +1,46 @@
1
+ import { channel } from "node:diagnostics_channel";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ const requests = new AsyncLocalStorage();
4
+ export function isRuntimeMaintenance(args) {
5
+ return args[0] === "runtime" && args[1] === "process" && ["register", "heartbeat", "confirm", "admission", "finish", "check-admission", "release-turn", "status"].includes(args[2] ?? "");
6
+ }
7
+ export const withMaintenanceDiagnosticScope = (action) => requests.run(Symbol(), action);
8
+ /** Request-local, bounded evidence; never command bytes, paths or lease tokens. */
9
+ export function runtimeMaintenanceDiagnostics(args, deadline, write) {
10
+ const started = performance.now();
11
+ const request = requests.getStore();
12
+ const option = (name) => { const at = args.indexOf(`--${name}`); return at < 0 ? undefined : args[at + 1]; };
13
+ const safe = (value) => typeof value === "string" && /^[A-Za-z0-9:._-]{1,128}$/.test(value) ? value : undefined;
14
+ const identity = { action: safe(args[2]), process_id: safe(option("id")), operation_id: safe(option("operation-id")), native_dispatch_started: false };
15
+ let previous = "", count = 0, committed = false;
16
+ const phase = (name, details = {}) => {
17
+ const key = `${details.store_kind ?? ""}:${name}`;
18
+ if (key === previous || count >= 64)
19
+ return;
20
+ previous = key;
21
+ count++;
22
+ write(`${JSON.stringify({ kind: "process_maintenance_phase", ...identity, phase: name, elapsed_ms: Math.round(performance.now() - started), ...(deadline !== undefined ? { remaining_ms: Math.max(0, deadline - Date.now()) } : {}), ...details, ...(["ack", "failed"].includes(name) ? { maintenance_effect: committed ? "committed" : name === "ack" ? "no_mutation_observed" : "unknown" } : {}) })}\n`);
23
+ };
24
+ const timing = channel("dd-flow.sqlite.ingress");
25
+ const log = (message) => {
26
+ if (requests.getStore() !== request)
27
+ return;
28
+ const record = message;
29
+ if (record.phase === "committed" && record.store_kind === "resource")
30
+ committed = true;
31
+ phase(record.phase, { ...(record.store_kind ? { store_kind: record.store_kind } : {}) });
32
+ };
33
+ timing.subscribe(log);
34
+ phase("request", { maintenance_effect: "unknown" });
35
+ return { phase, failure: (error) => {
36
+ let current = error;
37
+ let sqlite;
38
+ for (let depth = 0; current && depth < 8; depth++) {
39
+ sqlite = current.errcode ?? current.sqlite_extended_code ?? current.details?.sqlite_extended_code;
40
+ if (Number.isSafeInteger(sqlite))
41
+ break;
42
+ current = (current.cause ?? current.details?.cause);
43
+ }
44
+ phase("failed", { ...(Number.isSafeInteger(sqlite) ? { sqlite_extended_code: sqlite } : {}) });
45
+ }, close: () => timing.unsubscribe(log) };
46
+ }