@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
@@ -0,0 +1,51 @@
1
+ import { isRuntimeMaintenance, runtimeMaintenanceDiagnostics, withMaintenanceDiagnosticScope } from "./runtime-maintenance-diagnostics.js";
2
+ import { errorRecord, isAppError } from "../shared/errors.js";
3
+ import { writeJson } from "../shared/json.js";
4
+ /** Only the already-selected engine's internal JSON transport takes this path.
5
+ * Operator calls and richer output formats retain the ordinary engine router. */
6
+ export function isRuntimeMaintenanceIngress(args, env) {
7
+ return env.DD_FLOW_ENGINE_MODE === "1" && Number.isFinite(Number(env.DD_FLOW_HOOK_DEADLINE_AT))
8
+ && Number(env.DD_FLOW_HOOK_DEADLINE_AT) > 0 && args.includes("--json")
9
+ && isRuntimeMaintenance(args)
10
+ && !args.some(arg => ["--help", "-h", "--progress-jsonl", "--response-file"].includes(arg) || arg.startsWith("--response-file="));
11
+ }
12
+ export async function runRuntimeMaintenanceIngress(args, env = process.env, io = { stdout: process.stdout, stderr: process.stderr }) {
13
+ return withMaintenanceDiagnosticScope(async () => {
14
+ const deadline = Number(env.DD_FLOW_HOOK_DEADLINE_AT);
15
+ // Publish evidence before loading even the maintenance service graph.
16
+ const diagnostics = runtimeMaintenanceDiagnostics(args, deadline, line => io.stderr.write(line));
17
+ let context;
18
+ try {
19
+ const [{ createContext }, { prepareCommandRoute }, { resolveCommandProjectRoot, resolveCommandPaths }, processCommands] = await Promise.all([
20
+ import("../runtime/context.js"), import("./command-inputs.js"), import("../services/command-context.js"), import("./runtime-process.js")
21
+ ]);
22
+ const invocation = args.filter(arg => arg !== "--json");
23
+ const options = { databaseDeadlineAt: deadline, resourceDatabaseMode: args[2] === "register" ? "initialize" : args[2] === "status" ? "read_existing" : "prepared_existing" };
24
+ context = createContext({ ...env, DD_FLOW_INVOCATION_READONLY: args[2] === "status" ? "1" : undefined }, "read_existing", undefined, options);
25
+ const root = resolveCommandProjectRoot(context, invocation, null);
26
+ const routedArgs = resolveCommandPaths(context, invocation, root);
27
+ const route = prepareCommandRoute(routedArgs);
28
+ const prepared = processCommands.prepareRuntimeProcess(context, route.key, route.parsed);
29
+ // Preserve the common compatibility gate; this is not a second dispatch
30
+ // implementation or permission granted by the ingress predicate.
31
+ const { preflightCliCompatibility } = await import("../services/compatibility-preflight.js");
32
+ preflightCliCompatibility(context, routedArgs, env, root);
33
+ const result = await processCommands.dispatchRuntimeProcess(context, args[2], route.parsed, prepared);
34
+ const receipt = result;
35
+ const expiry = receipt?.lease_expires_at ?? receipt?.process?.lease_expires_at;
36
+ diagnostics.phase("ack", { ...(expiry && Number.isFinite(Date.parse(expiry)) ? { lease_expires_at: expiry } : {}) });
37
+ writeJson(io.stdout, result);
38
+ return 0;
39
+ }
40
+ catch (error) {
41
+ diagnostics.failure(error);
42
+ const record = errorRecord(error);
43
+ writeJson(io.stderr, { ok: false, error: { ...record, details: { ...(!isAppError(error) ? { effect: "unknown" } : {}), ...record.details } } });
44
+ return record.exitCode;
45
+ }
46
+ finally {
47
+ context?.db.close?.();
48
+ diagnostics.close();
49
+ }
50
+ });
51
+ }
@@ -0,0 +1,128 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { requiredOption, optionalOption, optionalNumber, optionalPositiveNumber } from "./argument-values.js";
3
+ import { prepareError } from "./input-preparation.js";
4
+ import { recoveryGuard, assertRunMutationAllowed } from "../services/run-recovery-guard.js";
5
+ import { recoveryRuntimeAdmission } from "../services/run-recovery-guard.js";
6
+ import { prepareManagedProcessRegistration, prepareManagedProcessConfirmation, prepareManagedProcessHeartbeat, prepareManagedProcessFinish, prepareManagedProcessStopOperation, prepareManagedProcessReconcile, stopManagedProcess, confirmManagedProcess, finishManagedProcess, renewManagedProcess, assertManagedProcessAdmission, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, reconcileExpiredManagedProcesses, registerManagedProcess } from "../services/managed-processes.js";
7
+ import { admitRuntimeCheck, parseRuntimeBudget, reserveProviderTurn, releaseProviderTurns, prepareProviderTurnRelease } from "../services/runtime-budget.js";
8
+ function runtimeProcessStopInput(parsed) {
9
+ const graceMs = optionalNumber(parsed, "grace-ms");
10
+ return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(graceMs !== undefined ? { graceMs } : {}) };
11
+ }
12
+ function runtimeProcessRegistrationInput(context, parsed) {
13
+ const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
14
+ const budget = parseRuntimeBudget(optionalOption(parsed, "budget-json"));
15
+ const kind = requiredOption(parsed, "kind");
16
+ const projectId = optionalOption(parsed, "project-id") ?? null, runId = optionalOption(parsed, "run") ?? null;
17
+ const guard = projectId && runId ? recoveryGuard(context, projectId, runId) : null;
18
+ const run = projectId && runId ? context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]) : null;
19
+ return {
20
+ ...(optionalOption(parsed, "id") ? { id: optionalOption(parsed, "id") } : {}), kind, ownerId: requiredOption(parsed, "owner"), uniqueActiveOperation: kind === "eval-observer",
21
+ ...(kind === "eval-observer" ? { pid: optionalPositiveNumber(parsed, "pid", true) ?? 0, ...(optionalPositiveNumber(parsed, "process-group-id", true) ? { processGroupId: optionalPositiveNumber(parsed, "process-group-id", true) } : {}) } : {}),
22
+ ownerPid: optionalPositiveNumber(parsed, "owner-pid", true), projectId, runId,
23
+ workId: optionalOption(parsed, "work") ?? null, checkId: optionalOption(parsed, "check") ?? null,
24
+ operationId: optionalOption(parsed, "operation") ?? null, stdoutPath: optionalOption(parsed, "stdout") ?? null,
25
+ stderrPath: optionalOption(parsed, "stderr") ?? null, ...(leaseMs !== undefined ? { leaseMs } : {}),
26
+ 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") } : {}) }
27
+ };
28
+ }
29
+ function runtimeProcessConfirmationInput(parsed) {
30
+ const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true), processGroupId = optionalPositiveNumber(parsed, "process-group-id", true);
31
+ 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 } : {}) };
32
+ }
33
+ function runtimeProcessHeartbeatInput(parsed) {
34
+ const leaseMs = optionalPositiveNumber(parsed, "lease-ms", true);
35
+ return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), ...(leaseMs !== undefined ? { leaseMs } : {}) };
36
+ }
37
+ function runtimeProcessFinishInput(parsed) {
38
+ const state = requiredOption(parsed, "state");
39
+ if (state !== "stopped" && state !== "failed")
40
+ throw new AppError("validation", "--state must be stopped or failed", 2);
41
+ return { id: requiredOption(parsed, "id"), leaseToken: requiredOption(parsed, "lease-token"), state, reason: optionalOption(parsed, "reason") ?? null };
42
+ }
43
+ export function prepareRuntimeProcess(context, key, parsed) {
44
+ try {
45
+ return prepareRuntimeProcessInput(context, key, parsed);
46
+ }
47
+ catch (error) {
48
+ return prepareError(error);
49
+ }
50
+ }
51
+ function prepareRuntimeProcessInput(context, key, parsed) {
52
+ const prepared = {};
53
+ if (key === "runtime process register") {
54
+ prepareManagedProcessRegistration(context, runtimeProcessRegistrationInput(context, parsed));
55
+ }
56
+ if (key === "runtime process heartbeat")
57
+ prepareManagedProcessHeartbeat(context, runtimeProcessHeartbeatInput(parsed));
58
+ if (key === "runtime process finish")
59
+ prepareManagedProcessFinish(context, runtimeProcessFinishInput(parsed));
60
+ if (key === "runtime process confirm")
61
+ prepareManagedProcessConfirmation(context, runtimeProcessConfirmationInput(parsed));
62
+ if (key === "runtime process stop")
63
+ prepareManagedProcessStopOperation(context, runtimeProcessStopInput(parsed));
64
+ if (key === "runtime process reconcile")
65
+ prepareManagedProcessReconcile(requiredOption(parsed, "owner"), optionalNumber(parsed, "grace-ms"));
66
+ if (key === "runtime process release-turn") {
67
+ prepared.providerTurnRelease = prepareProviderTurnRelease({ daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") });
68
+ }
69
+ return prepared;
70
+ }
71
+ export async function dispatchRuntimeProcess(context, action, parsed, prepared = {}) {
72
+ if (action === "check-admission")
73
+ return { ok: true, ...admitRuntimeCheck(context, requiredOption(parsed, "id"), requiredOption(parsed, "lease-token")) };
74
+ if (action === "stop")
75
+ return { ok: true, process: await stopManagedProcess(context, runtimeProcessStopInput(parsed)) };
76
+ if (action === "register") {
77
+ const process = registerManagedProcess(context, runtimeProcessRegistrationInput(context, parsed));
78
+ return { ok: true, process };
79
+ }
80
+ if (action === "confirm")
81
+ return { ok: true, process: confirmManagedProcess(context, runtimeProcessConfirmationInput(parsed)) };
82
+ if (action === "heartbeat") {
83
+ const receipt = renewManagedProcess(context, runtimeProcessHeartbeatInput(parsed));
84
+ return { ...receipt, ok: receipt.owned };
85
+ }
86
+ if (action === "admission") {
87
+ const [{ controllerHitlAdmission }, { assertCapacityOwnerAdmission }] = await Promise.all([import("../services/run-controller-adapter.js"), import("../services/codex-capacity.js")]);
88
+ const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
89
+ 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))
90
+ throw new AppError("process_ownership_unknown", "Productive dispatch requires its current confirmed runtime owner", 1);
91
+ assertManagedProcessAdmission(context, record);
92
+ const metadata = JSON.parse(record.metadata_json);
93
+ if (metadata.state_dir !== requiredOption(parsed, "state-dir"))
94
+ throw new AppError("process_ownership_unknown", "Runtime admission belongs to another state directory", 1);
95
+ if (record.project_id && record.run_id) {
96
+ if (managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
97
+ throw new AppError("process_runtime_scope_mismatch", "Runtime admission belongs to another or unproven flow home", 1);
98
+ 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") });
99
+ if (!recovering) {
100
+ if (metadata.run_admitted !== true || !Number.isSafeInteger(metadata.run_generation))
101
+ throw new AppError("run_recovery_guarded", "Runtime was not admitted before the RUN fence; reconcile its launch", 1);
102
+ assertRunMutationAllowed(context, record.project_id, record.run_id, metadata.run_generation);
103
+ }
104
+ const run = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [record.project_id, record.run_id]);
105
+ 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")]);
106
+ 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") });
107
+ if (!run || run.status !== "running" && !retainedHitlPrompt && !acceptedHitlPrompt && !["judge", "probe"].includes(metadata.role ?? ""))
108
+ throw new AppError("invalid_run_state", "Subject dispatch requires a running RUN or its retained recovery/HITL prompt", 1);
109
+ if (!recovering)
110
+ assertCapacityOwnerAdmission(context, record, optionalOption(parsed, "operation-id"), { nativeSessionId: optionalOption(parsed, "native-session-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
111
+ }
112
+ 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"), promptSha256: optionalOption(parsed, "prompt-sha256") });
113
+ return { ok: true, ...budget, process_id: record.id, generation: metadata.run_generation ?? null };
114
+ }
115
+ if (action === "release-turn") {
116
+ const record = managedProcessStatus(context).find(process => process.id === requiredOption(parsed, "id"));
117
+ if (!record || record.lease_token !== requiredOption(parsed, "lease-token") || record.owner_id !== requiredOption(parsed, "owner") || managedProcessRuntimeHome(record, context) !== context.ddFlowHome)
118
+ throw new AppError("process_ownership_unknown", "Budget release requires its retained physical owner", 1);
119
+ return releaseProviderTurns(context, record, { daemonId: requiredOption(parsed, "daemon-id"), operationId: optionalOption(parsed, "operation-id"), observationFile: optionalOption(parsed, "observation-file") }, prepared.providerTurnRelease);
120
+ }
121
+ if (action === "finish")
122
+ return { ok: finishManagedProcess(context, runtimeProcessFinishInput(parsed)) };
123
+ if (action === "status")
124
+ return { ok: true, processes: managedProcessStatus(context) };
125
+ if (action === "reconcile")
126
+ return { ok: true, processes: await reconcileExpiredManagedProcesses(context, requiredOption(parsed, "owner"), optionalNumber(parsed, "grace-ms")) };
127
+ throw new AppError("usage", "Usage: dd-flow runtime process <start|stop|register|confirm|heartbeat|admission|finish|status|reconcile>", 2);
128
+ }
package/dist/cli.js CHANGED
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { isNativeHookIngress } from "./cli/native-hook-ingress.js";
3
+ import { isRuntimeMaintenanceIngress, runRuntimeMaintenanceIngress } from "./cli/runtime-maintenance-ingress.js";
3
4
  const args = process.argv.slice(2);
4
5
  const managedHook = Boolean(process.env.DD_FLOW_CODEX_STATE_DIR) && args.slice(0, 3).join(" ") === "codex hook handle" && !args.includes("--help") && !args.includes("-h");
5
- const exitCode = managedHook
6
- ? await (await import("./cli/hook-ingress.js")).runHookIngress(args)
7
- : isNativeHookIngress(args) && !args.includes("--help") && !args.includes("-h")
8
- ? await (await import("./cli/native-hook-ingress.js")).runNativeHookIngress(args)
9
- : await (await import("./cli/run-cli.js")).runCli(args);
6
+ const exitCode = isRuntimeMaintenanceIngress(args, process.env)
7
+ ? await runRuntimeMaintenanceIngress(args)
8
+ : managedHook
9
+ ? await (await import("./cli/hook-ingress.js")).runHookIngress(args)
10
+ : isNativeHookIngress(args) && !args.includes("--help") && !args.includes("-h")
11
+ ? await (await import("./cli/native-hook-ingress.js")).runNativeHookIngress(args)
12
+ : await (await import("./cli/run-cli.js")).runCli(args);
10
13
  process.exitCode = exitCode;
@@ -3,7 +3,7 @@ export const vnextStages = [
3
3
  { id: "specify", directory: "01-specify", result: "specify.json", schema: "dd-flow/vnext-specify-result@4", next: ["protocolize"] },
4
4
  { id: "protocolize", directory: "02-protocolize", result: "protocolize-result.json", schema: "dd-flow/vnext-protocolize-result@3", next: ["plan"] },
5
5
  { id: "plan", directory: "03-plan", result: "stage-report.json", schema: "dd-flow/stage-report@2", next: ["plan-review", "code"] },
6
- { id: "plan-review", directory: "04-plan-review", result: "decision.json", schema: "dd-flow/plan-review-decision@3", next: ["code"] },
6
+ { id: "plan-review", directory: "04-plan-review", result: "decision.json", schema: "dd-flow/plan-review-decision@4", next: ["code"] },
7
7
  { id: "code", directory: "05-code", result: "code-verification.json", schema: "dd-flow/code-verification@2", next: ["code-review", "merge"] },
8
8
  { id: "code-review", directory: "06-code-review", result: "decision.json", schema: "dd-flow/code-review-decision@3", next: ["merge"] },
9
9
  { id: "merge", directory: "07-merge", result: "stage-report.json", schema: "dd-flow/stage-report@2", next: [] }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { adapterTimeoutOptions } from "../lib/adapter-timeouts.mjs";
2
3
  import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
4
  import { readFile } from "node:fs/promises";
4
5
  import { randomUUID } from "node:crypto";
@@ -38,7 +39,7 @@ async function hook(options) {
38
39
  }
39
40
 
40
41
  try {
41
- const { positional, options } = parse(process.argv.slice(2)), [family, command, modifier] = positional; const common = { journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, bin: options["agy-bin"], provider: options.provider, model: options.model, reasoning: options.reasoning, mode: options.mode, projectRoot: options["project-root"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], noFlow: options["no-flow"] === true, timeoutMs: options.timeout ? Number(options.timeout) * 1000 : undefined }; let result;
42
+ const { positional, options } = parse(process.argv.slice(2)), [family, command, modifier] = positional; const common = { journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, bin: options["agy-bin"], provider: options.provider, model: options.model, reasoning: options.reasoning, mode: options.mode, projectRoot: options["project-root"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], noFlow: options["no-flow"] === true, ...adapterTimeoutOptions(options, family === "session" ? command : null) }; let result;
42
43
  if (family === "doctor") result = await doctor(common);
43
44
  else if (family === "hooks" && ["install", "check"].includes(command)) result = await qualifyAgyHooks(common.projectRoot, { stateDir: common.stateDir, install: command === "install" });
44
45
  else if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...common, sessionId: options["session-id"], entryPath: process.argv[1] }, startDaemon);
@@ -49,7 +50,7 @@ try {
49
50
  else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(common.stateDir, options["operation-id"]);
50
51
  else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: common.stateDir, cancelTree: options["cancel-tree"], timeoutMs: common.timeoutMs });
51
52
  else if (family === "hook" && command === "handle") { process.stdout.write(`${JSON.stringify(await hook(options))}\n`); process.exit(0); }
52
- else if (family === "session") { if (command === "fork") throw Object.assign(new Error("Antigravity native headless fork is unsupported; use deterministic_replay"), { code: "agy_headless_fork_unsupported" }); const operation = command === "status" ? "inspect" : command; result = await callDaemon(common.stateDir, `session.${operation}`, { sessionId: options["session-id"], ...(command === "create" || command === "prompt" ? { prompt: await prompt(options) } : {}), tree: options.tree === true }, common.timeoutMs ?? 1_800_000); }
53
+ else if (family === "session") { if (command === "fork") throw Object.assign(new Error("Antigravity native headless fork is unsupported; use deterministic_replay"), { code: "agy_headless_fork_unsupported" }); const operation = command === "status" ? "inspect" : command; result = await callDaemon(common.stateDir, `session.${operation}`, { sessionId: options["session-id"], livenessTimeoutMs: common.livenessTimeoutMs, ...(command === "create" || command === "prompt" ? { prompt: await prompt(options) } : {}), tree: options.tree === true }, common.timeoutMs ?? 1_800_000); }
53
54
  else throw new Error("usage: dd-agy doctor | hooks install|check | daemon start|status|stop | session create|prompt|inspect|resume|cancel|fork [options] --json");
54
55
  process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
55
56
  } catch (error) { if (process.argv.includes("hook") && process.argv.includes("handle")) { process.stdout.write(`${JSON.stringify({ decision: "deny", reason: `${error.code ?? "agy_hook_rejected"}: ${error.message ?? "Native hook refused this tool call"}` })}\n`); process.exit(0); } process.stderr.write(`${JSON.stringify({ ok: false, error: error.message, ...(error.code ? { code: error.code, retryable: error.retryable === true, details: error.details } : {}) })}\n`); process.exit(1); }
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ import { adapterTimeoutOptions } from "../lib/adapter-timeouts.mjs";
2
3
  import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
4
  import { cancelSession, createSession, doctor, inspectSession, promptFromFile, promptSession, startSession } from "../lib/dd-codex.mjs";
4
5
  import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-codex-daemon.mjs";
5
6
  import { errorRecord } from "../lib/operation-errors.mjs";
6
7
 
7
8
  function parse(argv) { const positional = []; const options = {}; for (let i = 0; i < argv.length; i += 1) { const token = argv[i]; if (!token.startsWith("--")) { positional.push(token); continue; } const key = token.slice(2); if (["json", "cancel-tree", "no-flow"].includes(key)) { options[key] = true; continue; } const value = argv[++i]; if (value === undefined) throw new Error(`--${key} requires a value`); options[key] = value; } return { positional, options }; }
8
- function common(options) { return { bin: options["codex-bin"], journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, sessionId: options["session-id"], turnId: options["turn-id"], model: options.model, reasoning: options.reasoning, ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], noFlow: options["no-flow"] === true, timeoutMs: options.timeout ? Number(options.timeout) * 1000 : undefined }; }
9
+ function common(options, operation) { return { bin: options["codex-bin"], journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, sessionId: options["session-id"], turnId: options["turn-id"], model: options.model, reasoning: options.reasoning, ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], noFlow: options["no-flow"] === true, ...adapterTimeoutOptions(options, operation) }; }
9
10
  try {
10
- const { positional, options } = parse(process.argv.slice(2)); const [family, command, modifier] = positional; const shared = common(options); let result;
11
+ const { positional, options } = parse(process.argv.slice(2)); const [family, command, modifier] = positional; const shared = common(options, family === "session" ? command : null); let result;
11
12
  if (family === "doctor") result = await doctor(shared);
12
13
  else if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...shared, entryPath: process.argv[1] }, startDaemon);
13
14
  else if (family === "daemon" && command === "serve") { await serveDaemon(shared.stateDir); process.exit(0); }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { adapterTimeoutOptions } from "../lib/adapter-timeouts.mjs";
2
3
  import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
4
  import { mkdtemp, readFile, rm } from "node:fs/promises";
4
5
  import { randomUUID } from "node:crypto";
@@ -12,7 +13,7 @@ import { hookBudget, readNativeHookInput } from "../lib/native-hook-command.mjs"
12
13
  function parse(argv) { const positional = [], options = {}; for (let i = 0; i < argv.length; i++) { const word = argv[i]; if (!word.startsWith("--")) { positional.push(word); continue; } const key = word.slice(2); if (["json", "cancel-tree", "tree", "no-flow"].includes(key)) options[key] = true; else { if (argv[i + 1] === undefined) throw new Error(`${word} requires a value`); options[key] = argv[++i]; } } return { positional, options }; }
13
14
  try {
14
15
  const { positional: [family, command, modifier], options } = parse(process.argv.slice(2));
15
- const config = { stateDir: options["state-dir"], sessionId: options["session-id"], cwd: options.cwd, projectRoot: options["project-root"], bin: options["droid-bin"] ?? "droid", entryPath: process.argv[1], journal: options.journal, authHome: options["auth-home"], model: options.model, provider: options.provider, reasoning: options.reasoning, mode: options.mode, ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], noFlow: options["no-flow"] === true, timeoutMs: options.timeout ? Number(options.timeout) * 1_000 : undefined };
16
+ const config = { stateDir: options["state-dir"], sessionId: options["session-id"], cwd: options.cwd, projectRoot: options["project-root"], bin: options["droid-bin"] ?? "droid", entryPath: process.argv[1], journal: options.journal, authHome: options["auth-home"], model: options.model, provider: options.provider, reasoning: options.reasoning, mode: options.mode, ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], noFlow: options["no-flow"] === true, ...adapterTimeoutOptions(options, family === "session" ? command : null, "droid-cli") };
16
17
  let result;
17
18
  if (family === "doctor") {
18
19
  const dir = await mkdtemp(path.join(os.tmpdir(), "dd-droid-doctor-"));
@@ -27,7 +28,7 @@ try {
27
28
  else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(config.stateDir, options["operation-id"]);
28
29
  else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: config.stateDir, cancelTree: options["cancel-tree"] });
29
30
  else if (family === "hook" && command === "handle") { const budget = hookBudget(); const payload = JSON.parse(await readNativeHookInput(process.stdin, budget)); const eventId = randomUUID(); result = await callDaemon(config.stateDir, "hook.observe", { payload, eventId, deadlineAt: budget.deadlineAt }, budget.remaining("observation"), eventId); }
30
- else if (family === "session") { if (command === "fork") throw new Error("Droid fork is not qualified for this adapter"); const prompt = options["prompt-file"] ? await readFile(options["prompt-file"], "utf8") : options.prompt; result = await callDaemon(config.stateDir, `session.${command === "status" ? "inspect" : command}`, { sessionId: options["session-id"], ...(config.noFlow ? { noFlow: true } : {}), ...(prompt ? { prompt } : {}) }); }
31
+ else if (family === "session") { if (command === "fork") throw new Error("Droid fork is not qualified for this adapter"); const prompt = options["prompt-file"] ? await readFile(options["prompt-file"], "utf8") : options.prompt; result = await callDaemon(config.stateDir, `session.${command === "status" ? "inspect" : command}`, { sessionId: options["session-id"], livenessTimeoutMs: config.livenessTimeoutMs, ...(config.noFlow ? { noFlow: true } : {}), ...(prompt ? { prompt } : {}) }); }
31
32
  else throw new Error("usage: dd-droid doctor | daemon start|status|operation|stop | session create|resume|prompt|inspect|cancel");
32
33
  process.stdout.write(`${JSON.stringify(family === "hook" ? { ...(result.hookSpecificOutput ? { hookSpecificOutput: result.hookSpecificOutput } : {}), ...(typeof result.continue === "boolean" ? { continue: result.continue } : {}), ...(result.stopReason ? { stopReason: result.stopReason } : {}) } : { ok: true, ...result })}\n`);
33
34
  } catch (error) { process.stderr.write(`${JSON.stringify({ ok: false, error: errorRecord(error) })}\n`); process.exitCode = process.argv[2] === "hook" ? 2 : 1; }
@@ -1,20 +1,30 @@
1
1
  #!/usr/bin/env node
2
+ import { adapterTimeoutOptions } from "../lib/adapter-timeouts.mjs";
2
3
  import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
- import { hookBudget, invokeNativeHook, readNativeHookInput } from "../lib/native-hook-command.mjs";
4
+ import { hookBudget, invokeNativeHook, readNativeHookInput, writeNativeHookFailure } from "../lib/native-hook-command.mjs";
4
5
  import { cancelSession, createSession, doctor, forkSession, inspectSession, promptFromFile, promptSession } from "../lib/dd-grok.mjs";
5
6
  import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-grok-daemon.mjs";
7
+ import { errorRecord } from "../lib/operation-errors.mjs";
6
8
 
7
9
  function parse(argv) { const positional = []; const options = {}; for (let index = 0; index < argv.length; index += 1) { const token = argv[index]; if (!token.startsWith("--")) { positional.push(token); continue; } const key = token.slice(2); if (["json", "cancel-tree", "no-flow"].includes(key)) { options[key] = true; continue; } const value = argv[++index]; if (value === undefined) throw new Error(`--${key} requires a value`); options[key] = value; } return { positional, options }; }
8
- function common(options) { return { bin: options["grok-bin"], journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, sessionId: options["session-id"], model: options.model, reasoning: options.reasoning, mode: options.mode ?? "bypassPermissions", authPath: options["auth-path"], authMethodId: options["auth-method-id"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], sessionArchive: options["session-archive"], noFlow: options["no-flow"] === true, timeoutMs: options.timeout ? Number(options.timeout) * 1000 : undefined }; }
10
+ function common(options, operation) { return { bin: options["grok-bin"], journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, sessionId: options["session-id"], model: options.model, reasoning: options.reasoning, mode: options.mode ?? "bypassPermissions", authPath: options["auth-path"], authMethodId: options["auth-method-id"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], sessionArchive: options["session-archive"], noFlow: options["no-flow"] === true, ...adapterTimeoutOptions(options, operation) }; }
9
11
  async function hook(options) {
10
12
  const budget = hookBudget();
11
- const stdin = await readNativeHookInput(process.stdin, budget); const payload = JSON.parse(stdin || "{}"); const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.sessionId ?? payload.session_id }, budget.remaining("identity"));
13
+ const stdin = await readNativeHookInput(process.stdin, budget); const payload = JSON.parse(stdin || "{}"); const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.sessionId ?? payload.session_id, payload }, budget.remaining("identity"));
12
14
  const enriched = { ...payload, _meta: { ...(payload._meta ?? {}), ddGrok: identity } }; const args = ["grok", "event", "handle", "--project-root", options["project-root"], "--json"]; const command = options["dd-flow-bin"];
13
- return invokeNativeHook({ bin: command, args, home: options["dd-flow-home"], payload: enriched, timeoutMs: budget.remaining("admission", 15_000) });
15
+ try { return await invokeNativeHook({ bin: command, args, home: options["dd-flow-home"], payload: enriched, timeoutMs: budget.remaining("admission", 15_000) }); }
16
+ catch (error) {
17
+ if (identity.nativeHookBinding) error.details = { ...error.details, native_hook_binding: identity.nativeHookBinding };
18
+ try {
19
+ await writeNativeHookFailure(options["state-dir"], identity.nativeHookBinding, error, hookBudget(1_000, null));
20
+ if (identity.nativeHookBinding) await callDaemon(options["state-dir"], "hook.failed", { requestId: identity.nativeHookBinding.request_id }, 1_000);
21
+ } catch (storage) { process.stderr.write(`${JSON.stringify({ code: error.code, receipt_error: storage.message })}\n`); }
22
+ throw error;
23
+ }
14
24
  }
15
25
 
16
26
  try {
17
- const { positional, options } = parse(process.argv.slice(2)); const [family, command, modifier] = positional; const shared = common(options); let result;
27
+ const { positional, options } = parse(process.argv.slice(2)); const [family, command, modifier] = positional; const shared = common(options, family === "session" ? command : null); let result;
18
28
  if (family === "doctor") result = await doctor(shared);
19
29
  else if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...shared, entryPath: process.argv[1] }, startDaemon);
20
30
  else if (family === "daemon" && command === "serve") { await serveDaemon(shared.stateDir); process.exit(0); }
@@ -27,4 +37,4 @@ try {
27
37
  else if (family === "session") { const params = { ...shared, ...(command === "create" || command === "prompt" ? { prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt } : {}), ...(command === "fork" ? { target: JSON.parse(options["target-json"] ?? "null") } : {}), ...(command === "archive" ? { archiveDir: options["archive-dir"] } : {}) }; if (shared.stateDir) result = await callDaemon(shared.stateDir, `session.${command === "status" ? "inspect" : command}`, params, shared.timeoutMs ?? 1_800_000); else if (command === "create") result = await createSession(params); else if (command === "prompt") result = await promptSession(params); else if (["inspect", "status", "resume"].includes(command)) result = await inspectSession(params); else if (command === "cancel") result = await cancelSession(params); else if (command === "fork") result = await forkSession(params); else throw new Error(`unknown session command: ${command}`); }
28
38
  else throw new Error("usage: dd-grok doctor | daemon start|status|stop | session create|prompt|inspect|status|resume|cancel|fork|archive [options] --json");
29
39
  process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
30
- } catch (error) { if (process.argv[2] === "hook") { process.stdout.write(`${JSON.stringify({ decision: "deny", reason: error.message })}\n`); process.exit(2); } process.stderr.write(`${JSON.stringify({ ok: false, error: error.message, ...(error.code ? { code: error.code, retryable: error.retryable === true, details: error.details } : {}) })}\n`); process.exit(1); }
40
+ } catch (error) { if (process.argv[2] === "hook") { process.stderr.write(`${JSON.stringify({ native_hook_failure: errorRecord(error), bound: Boolean(error.details?.native_hook_binding) })}\n`); process.stdout.write(`${JSON.stringify({ decision: "deny", reason: error.message })}\n`); process.exit(2); } const failure = errorRecord(error); process.stderr.write(`${JSON.stringify({ ok: false, error: failure.message, ...failure })}\n`); process.exit(1); }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { adapterTimeoutOptions } from "../lib/adapter-timeouts.mjs";
2
3
  import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
4
  import { readFile } from "node:fs/promises";
4
5
  import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-opencode-daemon.mjs";
@@ -6,7 +7,7 @@ import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-open
6
7
  function parse(argv) { const positional = [], options = {}; for (let i = 0; i < argv.length; i += 1) { const token = argv[i]; if (!token.startsWith("--")) { positional.push(token); continue; } const key = token.slice(2); if (["json","cancel-tree","no-flow"].includes(key)) options[key] = true; else { if (argv[i + 1] === undefined) throw new Error(`--${key} requires a value`); options[key] = argv[++i]; } } return { positional, options }; }
7
8
  const prompt = async options => options["prompt-file"] ? await readFile(options["prompt-file"], "utf8") : options.prompt;
8
9
  try {
9
- const { positional, options } = parse(process.argv.slice(2)), [family, command, modifier] = positional; const common = { journal: options.journal, stateDir: options["state-dir"], sessionId: options["session-id"], cwd: options.cwd, bin: options["opencode-bin"], provider: options.provider, model: options.model, variant: options.variant, agent: options.agent, authPath: options["auth-path"], projectRoot: options["project-root"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], noFlow: options["no-flow"] === true, sessionArchive: options["session-archive"], timeoutMs: options.timeout ? Number(options.timeout) * 1000 : undefined }; let result;
10
+ const { positional, options } = parse(process.argv.slice(2)), [family, command, modifier] = positional; const common = { journal: options.journal, stateDir: options["state-dir"], sessionId: options["session-id"], cwd: options.cwd, bin: options["opencode-bin"], provider: options.provider, model: options.model, variant: options.variant, agent: options.agent, authPath: options["auth-path"], projectRoot: options["project-root"], ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], noFlow: options["no-flow"] === true, sessionArchive: options["session-archive"], ...adapterTimeoutOptions(options, family === "session" ? command : null) }; let result;
10
11
  if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...common, entryPath: process.argv[1] }, startDaemon);
11
12
  else if (family === "daemon" && command === "serve") { await serveDaemon(common.stateDir); process.exit(0); }
12
13
  else if (family === "daemon" && command === "status") result = await callDaemon(common.stateDir, "daemon.status");
@@ -15,7 +16,7 @@ try {
15
16
  else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(common.stateDir, options["operation-id"]);
16
17
  else if (family === "doctor") result = await callDaemon(common.stateDir, "daemon.doctor");
17
18
  else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: common.stateDir, cancelTree: options["cancel-tree"], timeoutMs: common.timeoutMs });
18
- else if (family === "session") { const operation = command === "status" ? "inspect" : command; const params = { sessionId: options["session-id"], title: options.title, messageId: options["message-id"], archiveFile: options["archive-file"], ...(command === "prompt" ? { prompt: await prompt(options) } : {}) }; result = await callDaemon(common.stateDir, `session.${operation}`, params, common.timeoutMs ?? 1_800_000); }
19
+ else if (family === "session") { const operation = command === "status" ? "inspect" : command; const params = { livenessTimeoutMs: common.livenessTimeoutMs, sessionId: options["session-id"], title: options.title, messageId: options["message-id"], archiveFile: options["archive-file"], ...(command === "prompt" ? { prompt: await prompt(options) } : {}) }; result = await callDaemon(common.stateDir, `session.${operation}`, params, common.timeoutMs ?? 1_800_000); }
19
20
  else throw new Error("usage: dd-opencode daemon start|status|stop | session create|prompt|inspect|fork|cancel|archive [options] --json");
20
21
  process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
21
22
  } catch (error) { process.stderr.write(`${JSON.stringify({ ok: false, error: error.message, ...(error.code ? { code: error.code, retryable: error.retryable === true, details: error.details } : {}) })}\n`); process.exit(1); }
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import { adapterTimeoutOptions } from "../lib/adapter-timeouts.mjs";
2
3
  import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
- import { hookBudget, invokeNativeHook, readNativeHookInput } from "../lib/native-hook-command.mjs";
4
+ import { hookBudget, invokeNativeHook, readNativeHookInput, writeNativeHookFailure } from "../lib/native-hook-command.mjs";
4
5
  import { cancelChild, cancelSession, createSession, doctor, forkSession, inspectSession, promptFromFile, promptSession } from "../lib/dd-zcode.mjs";
5
6
  import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-zcode-daemon.mjs";
7
+ import { errorRecord } from "../lib/operation-errors.mjs";
6
8
 
7
9
  function parse(argv) {
8
10
  const positional = []; const options = {};
@@ -15,13 +17,7 @@ function parse(argv) {
15
17
  return { positional, options };
16
18
  }
17
19
 
18
- function common(options) {
19
- const seconds = (name) => {
20
- if (options[name] === undefined) return undefined;
21
- const value = Number(options[name]);
22
- if (!Number.isFinite(value) || value <= 0) throw new Error(`--${name} must be a positive finite number of seconds`);
23
- return value * 1000;
24
- };
20
+ function common(options, operation) {
25
21
  return {
26
22
  bin: options["zcode-acp-bin"], zcodePath: options["zcode-path"], journal: options.journal,
27
23
  stateDir: options["state-dir"],
@@ -31,25 +27,33 @@ function common(options) {
31
27
  permission: options.permission ?? "deny", ddFlowBin: options["dd-flow-bin"],
32
28
  ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], noFlow: options["no-flow"] === true,
33
29
  env: options["dd-flow-home"] ? { DD_FLOW_HOME: options["dd-flow-home"] } : undefined,
34
- timeoutMs: seconds("timeout"),
35
- livenessTimeoutMs: seconds("liveness-timeout"),
36
- requestTimeoutMs: seconds("request-timeout"),
30
+ ...adapterTimeoutOptions(options, operation),
37
31
  };
38
32
  }
39
33
 
40
34
  async function hook(options) {
41
35
  const budget = hookBudget();
42
36
  const payload = JSON.parse((await readNativeHookInput(process.stdin, budget)) || "{}");
43
- const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.session_id ?? payload.sessionId }, budget.remaining("identity"));
37
+ const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.session_id ?? payload.sessionId, payload }, budget.remaining("identity"));
44
38
  const command = options["dd-flow-bin"]; const args = ["zcode", "hook", "handle", "--project-root", options["project-root"], "--json"];
45
39
 
46
- return invokeNativeHook({ bin: command, args, home: options["dd-flow-home"], timeoutMs: budget.remaining("admission", 15_000), payload: { ...payload, _meta: { ...(payload._meta ?? {}), ddZcode: identity } } });
40
+ try { return await invokeNativeHook({ bin: command, args, home: options["dd-flow-home"], timeoutMs: budget.remaining("admission", 15_000), payload: { ...payload, _meta: { ...(payload._meta ?? {}), ddZcode: identity } } }); }
41
+ catch (error) {
42
+ if (identity.nativeHookBinding) error.details = { ...error.details, native_hook_binding: identity.nativeHookBinding };
43
+ try {
44
+ // The 25s work budget leaves this bounded 2s evidence tail inside the
45
+ // qualified 30s native hook limit, even after the work deadline expired.
46
+ await writeNativeHookFailure(options["state-dir"], identity.nativeHookBinding, error, hookBudget(1_000, null));
47
+ if (identity.nativeHookBinding) await callDaemon(options["state-dir"], "hook.failed", { requestId: identity.nativeHookBinding.request_id }, 1_000);
48
+ } catch (storage) { process.stderr.write(`${JSON.stringify({ code: error.code, receipt_error: storage.message })}\n`); }
49
+ throw error;
50
+ }
47
51
  }
48
52
 
49
53
  try {
50
54
  const { positional, options } = parse(process.argv.slice(2)); const [family, command, modifier] = positional; let result;
51
55
  const answers = options["answers-file"] ? JSON.parse(await promptFromFile(options["answers-file"])) : undefined;
52
- const shared = common(options);
56
+ const shared = common(options, family === "session" ? command : null);
53
57
  if (family === "doctor") result = await doctor(shared);
54
58
  else if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...shared, entryPath: process.argv[1] }, startDaemon);
55
59
  else if (family === "daemon" && command === "serve") { await serveDaemon(shared.stateDir); process.exit(0); }
@@ -78,6 +82,6 @@ try {
78
82
  } else throw new Error("usage: dd-zcode doctor | daemon start|status|stop | hook handle | session create|prompt|inspect|status|resume|cancel|cancel-child|fork [options] --json");
79
83
  process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
80
84
  } catch (error) {
81
- if (process.argv[2] === "hook") { process.stdout.write(`${JSON.stringify({ decision: "deny", reason: error.message })}\n`); process.exit(2); }
82
- process.stderr.write(`${JSON.stringify({ ok: false, error: error.message, ...(error.code ? { code: error.code, retryable: error.retryable === true, details: error.details } : {}) })}\n`); process.exit(1);
85
+ if (process.argv[2] === "hook") { process.stderr.write(`${JSON.stringify({ native_hook_failure: errorRecord(error), bound: Boolean(error.details?.native_hook_binding) })}\n`); process.stdout.write(`${JSON.stringify({ decision: "deny", reason: error.message })}\n`); process.exit(2); }
86
+ const failure = errorRecord(error); process.stderr.write(`${JSON.stringify({ ok: false, error: failure.message, ...failure })}\n`); process.exit(1);
83
87
  }
@@ -0,0 +1,5 @@
1
+ export const NATIVE_OPERATION_WAIT_CONTRACT: "native-operation-wait@1";
2
+ export function canonicalNativeOperation(harness: string, operation: string): string;
3
+ export function nativeOperationWait(harness: string, operation: string): "native-work" | "control";
4
+ export function validateDaemonWait(harness: string, operation: string, duration: number | null): void;
5
+ export function adapterTimeoutOptions(options: Record<string, unknown>, operation?: string | null, harness?: string): { timeoutMs?: number; livenessTimeoutMs?: number; requestTimeoutMs?: number };
@@ -0,0 +1,34 @@
1
+ // Public work flags express silence, never the lifetime of a native Turn.
2
+ import { validateObservationDuration } from "./observation-clock.mjs";
3
+ export const NATIVE_OPERATION_WAIT_CONTRACT = "native-operation-wait@1";
4
+ // Grok's resume only inspects its retained native Session. Other adapters
5
+ // materialize that Session, so their original operation remains productive.
6
+ export function canonicalNativeOperation(harness, operation) {
7
+ return ["grok", "grok-acp"].includes(harness) && operation === "session.resume" ? "session.inspect" : operation;
8
+ }
9
+ // Only Droid's start waits for the full Turn. Codex start returns acceptance;
10
+ // the other bundled adapters do not expose start as a productive operation.
11
+ export function nativeOperationWait(harness, operation) {
12
+ operation = canonicalNativeOperation(harness, operation);
13
+ return operation === "session.prompt" || ["droid", "droid-cli"].includes(harness) && operation === "session.start" ? "native-work" : "control";
14
+ }
15
+ export function validateDaemonWait(harness, operation, duration) {
16
+ if (duration === null && nativeOperationWait(harness, operation) === "native-work") return;
17
+ validateObservationDuration(duration, "daemon control duration");
18
+ }
19
+
20
+ export function adapterTimeoutOptions(options, operation, harness) {
21
+ const duration = name => {
22
+ if (options[name] === undefined) return undefined;
23
+ const ms = Number(options[name]) * 1_000;
24
+ if (!Number.isInteger(ms) || ms < 1 || ms > 2_147_483_647) throw Object.assign(new Error(`--${name} requires positive finite seconds within the timer range`), { code: "invalid_timeout" });
25
+ return ms;
26
+ };
27
+ const timeoutMs = duration("timeout"), livenessTimeoutMs = duration("liveness-timeout"), requestTimeoutMs = duration("request-timeout");
28
+ if (operation === "prompt" || nativeOperationWait(harness, `session.${operation}`) === "native-work") {
29
+ if (timeoutMs !== undefined && livenessTimeoutMs !== undefined && timeoutMs !== livenessTimeoutMs) throw Object.assign(new Error("--timeout and --liveness-timeout must agree for session.prompt"), { code: "invalid_timeout" });
30
+ if (requestTimeoutMs !== undefined) throw Object.assign(new Error("--request-timeout cannot limit a productive prompt"), { code: "invalid_timeout" });
31
+ return { timeoutMs: undefined, livenessTimeoutMs: livenessTimeoutMs ?? timeoutMs };
32
+ }
33
+ return { timeoutMs, livenessTimeoutMs, requestTimeoutMs };
34
+ }
@@ -0,0 +1,10 @@
1
+ export const CAPACITY_POLICY: "codex-overload-burst@1";
2
+ export const OVERLOAD_WINDOW_MS: number;
3
+ export type CapacityRefusal = { policy: string; ordinal: number; session_id: string; turn_id: string; first_observed_at: string; native_terminal_at?: string; native_terminal_basis?: string };
4
+ export function parseCapacityOperation(id: string): { root: string; ordinal: number };
5
+ export function normalizeCodexFailure(error: unknown, sessionId?: string, turnId?: string): null | { code: string; session_id: string; turn_id: string; native_error: Record<string, unknown>; observed_at: unknown; terminal_at: unknown; native_turn_items: unknown; primary_error: { details?: Record<string, unknown> } };
6
+ export function terminalCodexOverload(error: unknown, sessionId?: string, turnId?: string): boolean;
7
+ export function nativeItemsSettled(items: unknown): boolean;
8
+ export function capacityBackoff(error: unknown, ordinal: number, now?: number): number;
9
+ export function refusalObservation(error: unknown, ordinal: number, firstObservedAt: string): CapacityRefusal;
10
+ export function overloadBurst(refusals: CapacityRefusal[]): null | { policy: string; basis: string; elapsed_ms: number; refusals: CapacityRefusal[] };
@@ -0,0 +1,89 @@
1
+ /** Pure policy shared by the controller and pinned-engine EVAL consumers. */
2
+ export const CAPACITY_POLICY = "codex-overload-burst@1";
3
+ export const OVERLOAD_WINDOW_MS = 120_000;
4
+
5
+ function policyError(code, message, details = {}) { return Object.assign(new Error(message), { code, details }); }
6
+
7
+ export function parseCapacityOperation(id) {
8
+ if (typeof id !== "string" || !id) throw policyError("capacity_continuation_conflict", "Missing capacity operation identity");
9
+ const match = /^(.*):capacity:(\d+):session\.prompt$/.exec(id);
10
+ if (!match) return { root: id, ordinal: 0 };
11
+ const ordinal = Number(match[2]);
12
+ if (!Number.isSafeInteger(ordinal) || ordinal < 1 || String(ordinal) !== match[2]) throw policyError("capacity_continuation_conflict", "Capacity ordinal is not a canonical positive safe integer", { operation_id: id });
13
+ return { root: match[1], ordinal };
14
+ }
15
+
16
+ export function normalizeCodexFailure(error, expectedSessionId, expectedTurnId) {
17
+ let current = error;
18
+ for (let depth = 0; depth < 4 && current && typeof current === "object"; depth++) {
19
+ if (current.code === "turn_interrupted") {
20
+ const details = current.details ?? {}, native = details.provider_error;
21
+ const code = typeof native?.codexErrorInfo === "string" ? native.codexErrorInfo : native?.codexErrorInfo?.code;
22
+ if (details.terminal_status !== "failed" || typeof details.provider_session_id !== "string" || !details.provider_session_id
23
+ || expectedSessionId !== undefined && details.provider_session_id !== expectedSessionId
24
+ || typeof details.turn_id !== "string" || !details.turn_id || details.native_turn_id !== details.turn_id
25
+ || expectedTurnId !== undefined && details.turn_id !== expectedTurnId || typeof code !== "string") return null;
26
+ return { code, session_id: details.provider_session_id, turn_id: details.turn_id, native_error: native,
27
+ observed_at: details.observed_at ?? null, terminal_at: details.terminal_at ?? null, terminal_basis: details.terminal_basis ?? null,
28
+ native_turn_items: details.native_turn_items ?? null, primary_error: current };
29
+ }
30
+ // No arbitrary recursive cause search: an ownership/hook/lifecycle error wins.
31
+ if (current.code !== "harness_adapter_failed") return null;
32
+ current = current.details?.cause ?? current.cause;
33
+ }
34
+ return null;
35
+ }
36
+
37
+ export function terminalCodexOverload(error, expectedSessionId, expectedTurnId) {
38
+ return normalizeCodexFailure(error, expectedSessionId, expectedTurnId)?.code === "serverOverloaded";
39
+ }
40
+
41
+ export function nativeItemsSettled(items) {
42
+ return items?.observed === true && typeof items.possible_effects === "boolean" && items.pending !== true
43
+ && (items.possible_effects === false || items.pending === false);
44
+ }
45
+
46
+ export function capacityBackoff(error, ordinal, now = Date.now()) {
47
+ if (!Number.isSafeInteger(ordinal) || ordinal < 1 || !Number.isFinite(now)) throw policyError("capacity_continuation_conflict", "Invalid capacity timing identity");
48
+ const native = normalizeCodexFailure(error)?.native_error ?? error?.details?.provider_error ?? error;
49
+ const header = Object.entries(native?.headers ?? {}).find(([key]) => key.toLowerCase() === "retry-after")?.[1];
50
+ const value = header ?? native?.retryAfter ?? native?.retry_after;
51
+ let retry = 0;
52
+ if (value !== undefined && value !== null) {
53
+ retry = typeof value === "number" || typeof value === "string" && /^\d+(\.\d+)?$/.test(value.trim()) ? Number(value) * 1000 : typeof value === "string" ? Math.max(0, Date.parse(value) - now) : NaN;
54
+ if (!Number.isFinite(retry) || retry < 0 || !Number.isFinite(now + retry) || now + retry > 8.64e15)
55
+ throw policyError("capacity_retry_metadata_invalid", "Retry-After cannot be represented safely", { retry_after: value });
56
+ }
57
+ return Math.max(ordinal === 1 ? 5_000 : 15_000, retry);
58
+ }
59
+
60
+ export function refusalObservation(error, ordinal, firstObservedAt) {
61
+ const failure = normalizeCodexFailure(error);
62
+ const observed = Date.parse(firstObservedAt);
63
+ if (!failure || !Number.isSafeInteger(ordinal) || ordinal < 0 || !Number.isFinite(observed))
64
+ throw policyError("capacity_continuation_conflict", "Refusal lacks an immutable native identity/time");
65
+ if (failure.observed_at !== null && (typeof failure.observed_at !== "string" || !Number.isFinite(Date.parse(failure.observed_at)) || Date.parse(failure.observed_at) !== observed))
66
+ throw policyError("capacity_continuation_conflict", "Native refusal observation time is invalid or changed");
67
+ const native = failure.terminal_at && Date.parse(failure.terminal_at);
68
+ return { policy: CAPACITY_POLICY, ordinal, session_id: failure.session_id, turn_id: failure.turn_id,
69
+ first_observed_at: firstObservedAt, ...(Number.isFinite(native) && typeof failure.terminal_basis === "string" && failure.terminal_basis ? { native_terminal_at: failure.terminal_at, native_terminal_basis: failure.terminal_basis } : {}) };
70
+ }
71
+
72
+ export function overloadBurst(refusals) {
73
+ for (const [index, refusal] of refusals.entries()) {
74
+ const previous = refusals[index - 1];
75
+ if (typeof refusal.first_observed_at !== "string" || !Number.isFinite(Date.parse(refusal.first_observed_at))
76
+ || previous && Date.parse(refusal.first_observed_at) < Date.parse(previous.first_observed_at))
77
+ throw policyError("capacity_continuation_conflict", "Refusal observation clock reversed or is invalid");
78
+ }
79
+ const continuations = refusals.filter(item => item.ordinal > 0);
80
+ const pair = continuations.slice(-2);
81
+ if (pair.length !== 2) return null;
82
+ const [previous, current] = pair;
83
+ if (previous.session_id !== current.session_id || previous.turn_id === current.turn_id || current.ordinal !== previous.ordinal + 1)
84
+ throw policyError("capacity_continuation_conflict", "Refusal chain identity is inconsistent");
85
+ const basis = previous.native_terminal_at && current.native_terminal_at && previous.native_terminal_basis && previous.native_terminal_basis === current.native_terminal_basis ? "native_terminal_at" : "first_observed_at";
86
+ const elapsed = Date.parse(current[basis]) - Date.parse(previous[basis]);
87
+ if (!Number.isFinite(elapsed) || elapsed < 0) throw policyError("capacity_continuation_conflict", "Refusal clock reversed or is invalid");
88
+ return elapsed <= OVERLOAD_WINDOW_MS ? { policy: CAPACITY_POLICY, basis, elapsed_ms: elapsed, refusals: pair } : null;
89
+ }