@deksden-com/dd-flow-cli 0.9.0-beta.8 → 0.9.0-beta.81

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 (138) hide show
  1. package/CHANGELOG.md +402 -0
  2. package/README.md +65 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +300 -0
  5. package/dist/cli/help.js +81 -9
  6. package/dist/cli/hook-ingress.js +82 -0
  7. package/dist/cli/input-preparation.js +115 -0
  8. package/dist/cli/run-cli.js +1344 -376
  9. package/dist/cli.js +6 -2
  10. package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
  11. package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
  12. package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
  13. package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
  14. package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
  15. package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
  16. package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
  17. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
  18. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  19. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  20. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +146 -0
  21. package/dist/harness-runtime/lib/dd-codex.mjs +562 -0
  22. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
  23. package/dist/harness-runtime/lib/dd-droid.mjs +471 -0
  24. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
  25. package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
  26. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
  27. package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
  28. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
  29. package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
  30. package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
  31. package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
  32. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  33. package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
  34. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  35. package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
  36. package/dist/harness-runtime/lib/model-observations.mjs +77 -0
  37. package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
  38. package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
  39. package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
  40. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  41. package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
  42. package/dist/harness-runtime/lib/process-json.mjs +70 -0
  43. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  44. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  45. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  46. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  47. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  48. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  49. package/dist/schemas/agent-profile.schema.json +1 -1
  50. package/dist/schemas/code-review-result.schema.json +2 -2
  51. package/dist/schemas/code-work-batch.schema.json +4 -3
  52. package/dist/schemas/code-work-result.schema.json +4 -4
  53. package/dist/schemas/plan-review-result.schema.json +2 -2
  54. package/dist/schemas/run-control-receipt.schema.json +99 -0
  55. package/dist/schemas/run-control-request.schema.json +36 -0
  56. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  57. package/dist/services/canon.js +11 -8
  58. package/dist/services/cleanup.js +57 -13
  59. package/dist/services/cli-operation-classifier.js +22 -24
  60. package/dist/services/code-checks.js +543 -96
  61. package/dist/services/codex-hook-delivery.js +28 -0
  62. package/dist/services/config.js +11 -8
  63. package/dist/services/controller-fanout.js +116 -0
  64. package/dist/services/dashboard.js +103 -31
  65. package/dist/services/engines.js +121 -76
  66. package/dist/services/eval-snapshots.js +750 -73
  67. package/dist/services/execution-policy.js +170 -0
  68. package/dist/services/external-work-launch.js +110 -0
  69. package/dist/services/harness-adapter.js +147 -24
  70. package/dist/services/harness-config.js +2 -0
  71. package/dist/services/hooks.js +575 -293
  72. package/dist/services/lanes.js +61 -53
  73. package/dist/services/lifecycle-command.js +87 -7
  74. package/dist/services/lifecycle-invocations.js +835 -0
  75. package/dist/services/managed-daemon-binding.js +38 -0
  76. package/dist/services/managed-processes.js +283 -27
  77. package/dist/services/merge-queue.js +179 -104
  78. package/dist/services/merge-server.js +73 -28
  79. package/dist/services/migrations.js +14 -9
  80. package/dist/services/native-daemon-history.js +49 -0
  81. package/dist/services/native-session-control.js +39 -0
  82. package/dist/services/plan-runtime.js +19 -8
  83. package/dist/services/plans.js +28 -25
  84. package/dist/services/projects.js +18 -5
  85. package/dist/services/prompts.js +23 -16
  86. package/dist/services/protocols.js +64 -20
  87. package/dist/services/recovery-observation-budget.js +61 -0
  88. package/dist/services/recovery-snapshot-database.js +107 -0
  89. package/dist/services/run-control-receipt.js +56 -0
  90. package/dist/services/run-control-worker.js +363 -0
  91. package/dist/services/run-control.js +876 -0
  92. package/dist/services/run-controller-adapter.js +222 -0
  93. package/dist/services/run-controller-capture.js +134 -0
  94. package/dist/services/run-controller-process.js +189 -0
  95. package/dist/services/run-controller-recovery.js +222 -0
  96. package/dist/services/run-controller-state.js +36 -0
  97. package/dist/services/run-controller.js +779 -0
  98. package/dist/services/run-engine-bindings.js +1 -1
  99. package/dist/services/run-fork.js +138 -0
  100. package/dist/services/run-observations.js +112 -0
  101. package/dist/services/run-recovery-runtime.js +69 -0
  102. package/dist/services/run-recovery.js +343 -0
  103. package/dist/services/runs.js +368 -74
  104. package/dist/services/runtime-budget.js +434 -0
  105. package/dist/services/runtime-scope-capture.js +50 -0
  106. package/dist/services/runtime-scope-control.js +450 -0
  107. package/dist/services/runtime-scope-resume.js +565 -0
  108. package/dist/services/runtime-scope-stop.js +99 -0
  109. package/dist/services/runtime-scope-worker.js +243 -0
  110. package/dist/services/runtime-service.js +97 -0
  111. package/dist/services/schema-validation.js +10 -8
  112. package/dist/services/sessions.js +35 -69
  113. package/dist/services/stage-blocker.js +17 -7
  114. package/dist/services/stage-context.js +44 -18
  115. package/dist/services/stage-lifecycle.js +99 -102
  116. package/dist/services/stage-pause.js +69 -28
  117. package/dist/services/usage.js +37 -93
  118. package/dist/services/vnext-code-review.js +189 -85
  119. package/dist/services/vnext-code.js +257 -243
  120. package/dist/services/vnext-execution-profile.js +5 -3
  121. package/dist/services/vnext-fanout.js +134 -19
  122. package/dist/services/vnext-merge.js +285 -102
  123. package/dist/services/vnext-plan-review.js +148 -94
  124. package/dist/services/vnext-plan.js +141 -72
  125. package/dist/services/vnext-protocolize.js +129 -56
  126. package/dist/services/vnext-specify.js +87 -64
  127. package/dist/services/work-registry.js +701 -164
  128. package/dist/services/workspace-bootstrap.js +76 -0
  129. package/dist/services/worktrees.js +85 -35
  130. package/dist/shared/entity-references.js +8 -0
  131. package/dist/shared/errors.js +12 -0
  132. package/dist/storage/database.js +444 -32
  133. package/dist/storage/paths.js +17 -4
  134. package/dist/storage/work-references.js +23 -0
  135. package/dist/storage/writer-contract.js +77 -0
  136. package/dist/storage/writer-migration.js +102 -0
  137. package/package.json +11 -4
  138. package/tools/repair-paused-run-status.mjs +59 -0
package/dist/cli.js CHANGED
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from "./cli/run-cli.js";
3
- const exitCode = await runCli(process.argv.slice(2));
2
+ const args = process.argv.slice(2);
3
+ const managedHook = Boolean(process.env.DD_FLOW_CODEX_STATE_DIR) && args.slice(0, 3).join(" ") === "codex hook handle" && !args.includes("--help") && !args.includes("-h");
4
+ const exitCode = managedHook
5
+ ? await (await import("./cli/hook-ingress.js")).runHookIngress(args)
6
+ : await (await import("./cli/run-cli.js")).runCli(args);
4
7
  process.exitCode = exitCode;
8
+ export {};
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { spawn } from "node:child_process";
5
+ import { doctor } from "../lib/dd-agy.mjs";
6
+ import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-agy-daemon.mjs";
7
+
8
+ 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","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 }; }
9
+ const prompt = async options => options["prompt-file"] ? await readFile(options["prompt-file"], "utf8") : options.prompt;
10
+ async function stdin() { let value = ""; process.stdin.setEncoding("utf8"); for await (const chunk of process.stdin) value += chunk; return value; }
11
+ async function hook(options) {
12
+ const payload = JSON.parse(await stdin() || "{}"), event = options.event;
13
+ const identity = await callDaemon(options["state-dir"], "hook.observe", { event, payload });
14
+ if (event === "PreToolUse" || event === "PostToolUse") {
15
+ const phase = event === "PreToolUse" ? "before" : "after", args = payload.toolCall?.args ?? {};
16
+ const normalized = { schema_id: "dd-flow/agy-tool-event@1", event_id: identity.event_id, phase, daemon_id: identity.daemon_id, conversation_id: payload.conversationId, parent_conversation_id: identity.parent_conversation_id, step_index: payload.stepIdx, tool: payload.toolCall?.name, input: args, workspace_paths: payload.workspacePaths, transcript_path: payload.transcriptPath, model: payload.modelName, profile: identity.profile, ...(phase === "after" ? { error: payload.error ?? "" } : {}) };
17
+ const command = options["dd-flow-bin"] ?? identity.dd_flow_bin, target = /\.[cm]?js$/.test(command) ? { command: process.execPath, args: [command] } : { command, args: [] };
18
+ await new Promise((resolve, reject) => { const child = spawn(target.command, [...target.args, "agy", "event", "handle", "--project-root", options["project-root"] ?? identity.project_root, "--json"], { env: { ...process.env, DD_FLOW_HOME: options["dd-flow-home"] ?? identity.dd_flow_home }, stdio: ["pipe", "ignore", "pipe"] }); let error = ""; child.stderr.setEncoding("utf8").on("data", chunk => error += chunk); child.on("error", reject); child.on("close", code => code === 0 ? resolve() : reject(new Error(error || "dd-flow rejected Antigravity hook"))); child.stdin.end(`${JSON.stringify(normalized)}\n`); });
19
+ }
20
+ return event === "PreToolUse" ? { decision: "allow" } : {};
21
+ }
22
+
23
+ try {
24
+ 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;
25
+ if (family === "doctor") result = await doctor(common);
26
+ else if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...common, sessionId: options["session-id"], entryPath: process.argv[1] }, startDaemon);
27
+ else if (family === "daemon" && command === "serve") { await serveDaemon(common.stateDir); process.exit(0); }
28
+ else if (family === "daemon" && command === "status") result = await callDaemon(common.stateDir, "daemon.status");
29
+ else if (family === "daemon" && command === "operation" && modifier === "interrupt") result = await interruptDaemonOperation(common.stateDir, options["operation-id"], { control_id: options["control-id"], stop_operation_id: options["stop-operation-id"], daemon_id: options["daemon-id"], session_id: options["session-id"], evidence: options.evidence ? JSON.parse(options.evidence) : null });
30
+ else if (family === "daemon" && command === "operation" && modifier === "settle") result = await settleDaemonOperation(common.stateDir, options["operation-id"]);
31
+ else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(common.stateDir, options["operation-id"]);
32
+ else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: common.stateDir, cancelTree: options["cancel-tree"], timeoutMs: common.timeoutMs });
33
+ else if (family === "hook" && command === "handle") { process.stdout.write(`${JSON.stringify(await hook(options))}\n`); process.exit(0); }
34
+ 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); }
35
+ else throw new Error("usage: dd-agy doctor | daemon start|status|stop | session create|prompt|inspect|resume|cancel|fork [options] --json");
36
+ process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
37
+ } catch (error) { if (process.argv.includes("hook") && process.argv.includes("handle")) { process.stdout.write(`${JSON.stringify({ decision: "deny", reason: error.code ?? "agy_hook_rejected" })}\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); }
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
+ import { cancelSession, createSession, doctor, inspectSession, promptFromFile, promptSession, startSession } from "../lib/dd-codex.mjs";
4
+ import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-codex-daemon.mjs";
5
+
6
+ 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"].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 }; }
7
+ 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"], timeoutMs: options.timeout ? Number(options.timeout) * 1000 : undefined }; }
8
+ try {
9
+ const { positional, options } = parse(process.argv.slice(2)); const [family, command, modifier] = positional; const shared = common(options); let result;
10
+ if (family === "doctor") result = await doctor(shared);
11
+ else if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...shared, entryPath: process.argv[1] }, startDaemon);
12
+ else if (family === "daemon" && command === "serve") { await serveDaemon(shared.stateDir); process.exit(0); }
13
+ else if (family === "daemon" && command === "status") result = await callDaemon(shared.stateDir, "daemon.status");
14
+ else if (family === "daemon" && command === "operation" && modifier === "interrupt") result = await interruptDaemonOperation(shared.stateDir, options["operation-id"], { control_id: options["control-id"], stop_operation_id: options["stop-operation-id"], daemon_id: options["daemon-id"], session_id: options["session-id"], evidence: options.evidence ? JSON.parse(options.evidence) : null });
15
+ else if (family === "daemon" && command === "operation" && modifier === "settle") result = await settleDaemonOperation(shared.stateDir, options["operation-id"]);
16
+ else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(shared.stateDir, options["operation-id"]);
17
+ else if (family === "daemon" && command === "stop") result = await stopDaemon({ ...shared, cancelTree: options["cancel-tree"] });
18
+ else if (family === "session" && command === "create") result = shared.stateDir ? await callDaemon(shared.stateDir, "session.create", shared) : await createSession(shared);
19
+ else if (family === "session" && command === "prompt") result = shared.stateDir ? await callDaemon(shared.stateDir, "session.prompt", { ...shared, prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt }, shared.timeoutMs ?? 1_800_000) : await promptSession({ ...shared, prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt });
20
+ else if (family === "session" && command === "start") result = shared.stateDir ? await callDaemon(shared.stateDir, "session.start", { ...shared, prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt }, shared.timeoutMs ?? 30_000) : await startSession({ ...shared, prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt });
21
+ else if (family === "session" && ["inspect", "status", "resume"].includes(command)) result = shared.stateDir ? await callDaemon(shared.stateDir, command === "resume" ? "session.resume" : "session.inspect", shared) : await inspectSession(shared);
22
+ else if (family === "session" && command === "cancel") result = shared.stateDir ? await callDaemon(shared.stateDir, "session.cancel", shared) : await cancelSession(shared);
23
+ else throw new Error("usage: dd-codex doctor | daemon start|status|stop | session create|start|prompt|inspect|status|resume|cancel [--json]");
24
+ process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
25
+ } catch (error) { process.stderr.write(`${JSON.stringify({ ok: false, error: error.message, ...(error.code ? { code: error.code } : {}) })}\n`); process.exit(1); }
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
4
+ import { randomUUID } from "node:crypto";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { DROID_CONTRACT, DROID_PROTOCOL, DroidRuntime, droidVersion } from "../lib/dd-droid.mjs";
8
+ import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-droid-daemon.mjs";
9
+ import { errorRecord } from "../lib/operation-errors.mjs";
10
+
11
+ 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 }; }
12
+ async function input() { let text = ""; for await (const chunk of process.stdin) text += chunk; return text; }
13
+ try {
14
+ 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
+ let result;
17
+ if (family === "doctor") {
18
+ const dir = await mkdtemp(path.join(os.tmpdir(), "dd-droid-doctor-"));
19
+ const runtime = new DroidRuntime({ ...config, stateDir: dir, home: path.join(dir, "home"), cwd: config.cwd ?? process.cwd(), journal: path.join(dir, "events.jsonl"), daemonId: randomUUID(), ddFlowHome: null, ddFlowBin: null }, {}, async () => {});
20
+ try { await runtime.start(); result = { harness: "droid-cli", observed_runtime: { droid: await droidVersion(config.bin), factory_protocol: runtime.protocol, dd_harness_contract: DROID_CONTRACT }, models: runtime.models.map(model => model.id ?? model.modelId), capabilities: { native_children: true, tree_cancel: true, selective_child_cancel: false, native_fork: false }, protocol_expected: DROID_PROTOCOL }; }
21
+ finally { await runtime.closeTree(true); await rm(dir, { recursive: true, force: true }); }
22
+ } else if (family === "daemon" && command === "start") result = await durableDaemonStart(config, startDaemon);
23
+ else if (family === "daemon" && command === "serve") { await serveDaemon(config.stateDir); process.exit(0); }
24
+ else if (family === "daemon" && command === "status") result = await callDaemon(config.stateDir, "daemon.status");
25
+ else if (family === "daemon" && command === "operation" && modifier === "interrupt") result = await interruptDaemonOperation(config.stateDir, options["operation-id"], { control_id: options["control-id"], stop_operation_id: options["stop-operation-id"], daemon_id: options["daemon-id"], session_id: options["session-id"], evidence: options.evidence ? JSON.parse(options.evidence) : null });
26
+ else if (family === "daemon" && command === "operation" && modifier === "settle") result = await settleDaemonOperation(config.stateDir, options["operation-id"]);
27
+ else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(config.stateDir, options["operation-id"]);
28
+ else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: config.stateDir, cancelTree: options["cancel-tree"] });
29
+ else if (family === "hook" && command === "handle") { const payload = JSON.parse(await input()); const eventId = randomUUID(); result = await callDaemon(config.stateDir, "hook.observe", { payload, eventId }, 25_000, 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"], ...(prompt ? { prompt } : {}) }); }
31
+ else throw new Error("usage: dd-droid doctor | daemon start|status|operation|stop | session create|resume|prompt|inspect|cancel");
32
+ 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
+ } catch (error) { process.stderr.write(`${JSON.stringify({ ok: false, error: errorRecord(error) })}\n`); process.exitCode = process.argv[2] === "hook" ? 2 : 1; }
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
+ import { invokeNativeHook } from "../lib/native-hook-command.mjs";
4
+ import { cancelSession, createSession, doctor, forkSession, inspectSession, promptFromFile, promptSession } from "../lib/dd-grok.mjs";
5
+ import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-grok-daemon.mjs";
6
+
7
+ 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 }; }
9
+ async function readStdin() { let value = ""; process.stdin.setEncoding("utf8"); for await (const chunk of process.stdin) value += chunk; return value; }
10
+ async function hook(options) {
11
+ const stdin = await readStdin(); const payload = JSON.parse(stdin || "{}"); const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.sessionId ?? payload.session_id });
12
+ 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 });
14
+ }
15
+
16
+ try {
17
+ const { positional, options } = parse(process.argv.slice(2)); const [family, command, modifier] = positional; const shared = common(options); let result;
18
+ if (family === "doctor") result = await doctor(shared);
19
+ else if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...shared, entryPath: process.argv[1] }, startDaemon);
20
+ else if (family === "daemon" && command === "serve") { await serveDaemon(shared.stateDir); process.exit(0); }
21
+ else if (family === "daemon" && command === "status") result = await callDaemon(shared.stateDir, "daemon.status");
22
+ else if (family === "daemon" && command === "operation" && modifier === "interrupt") result = await interruptDaemonOperation(shared.stateDir, options["operation-id"], { control_id: options["control-id"], stop_operation_id: options["stop-operation-id"], daemon_id: options["daemon-id"], session_id: options["session-id"], evidence: options.evidence ? JSON.parse(options.evidence) : null });
23
+ else if (family === "daemon" && command === "operation" && modifier === "settle") result = await settleDaemonOperation(shared.stateDir, options["operation-id"]);
24
+ else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(shared.stateDir, options["operation-id"]);
25
+ else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: shared.stateDir, cancelTree: options["cancel-tree"], timeoutMs: shared.timeoutMs });
26
+ else if (family === "hook" && command === "handle") { process.stdout.write(`${JSON.stringify(await hook(options))}\n`); process.exit(0); }
27
+ 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
+ else throw new Error("usage: dd-grok doctor | daemon start|status|stop | session create|prompt|inspect|status|resume|cancel|fork|archive [options] --json");
29
+ 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); }
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-opencode-daemon.mjs";
5
+
6
+ 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
+ const prompt = async options => options["prompt-file"] ? await readFile(options["prompt-file"], "utf8") : options.prompt;
8
+ 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
+ if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...common, entryPath: process.argv[1] }, startDaemon);
11
+ else if (family === "daemon" && command === "serve") { await serveDaemon(common.stateDir); process.exit(0); }
12
+ else if (family === "daemon" && command === "status") result = await callDaemon(common.stateDir, "daemon.status");
13
+ else if (family === "daemon" && command === "operation" && modifier === "interrupt") result = await interruptDaemonOperation(common.stateDir, options["operation-id"], { control_id: options["control-id"], stop_operation_id: options["stop-operation-id"], daemon_id: options["daemon-id"], session_id: options["session-id"], evidence: options.evidence ? JSON.parse(options.evidence) : null });
14
+ else if (family === "daemon" && command === "operation" && modifier === "settle") result = await settleDaemonOperation(common.stateDir, options["operation-id"]);
15
+ else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(common.stateDir, options["operation-id"]);
16
+ else if (family === "doctor") result = await callDaemon(common.stateDir, "daemon.doctor");
17
+ 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 throw new Error("usage: dd-opencode daemon start|status|stop | session create|prompt|inspect|fork|cancel|archive [options] --json");
20
+ process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
21
+ } 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); }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, settleDaemonOperation } from "../lib/daemon-operations.mjs";
3
+ import { invokeNativeHook } from "../lib/native-hook-command.mjs";
4
+ import { cancelChild, cancelSession, createSession, doctor, forkSession, inspectSession, promptFromFile, promptSession } from "../lib/dd-zcode.mjs";
5
+ import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-zcode-daemon.mjs";
6
+
7
+ function parse(argv) {
8
+ const positional = []; const options = {};
9
+ for (let index = 0; index < argv.length; index += 1) {
10
+ const token = argv[index];
11
+ if (!token.startsWith("--")) { positional.push(token); continue; }
12
+ const key = token.slice(2); if (["json", "cancel-tree"].includes(key)) { options[key] = true; continue; }
13
+ const value = argv[++index]; if (value === undefined) throw new Error(`--${key} requires a value`); options[key] = value;
14
+ }
15
+ return { positional, options };
16
+ }
17
+
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
+ };
25
+ return {
26
+ bin: options["zcode-acp-bin"], zcodePath: options["zcode-path"], journal: options.journal,
27
+ stateDir: options["state-dir"],
28
+ cwd: options.cwd, sessionId: options["session-id"], adapterSessionId: options["adapter-session-id"], provider: options.provider,
29
+ childSessionId: options["child-session-id"],
30
+ model: options.model, reasoning: options.reasoning, mode: options.mode,
31
+ permission: options.permission ?? "deny", ddFlowBin: options["dd-flow-bin"],
32
+ ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"],
33
+ 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"),
37
+ };
38
+ }
39
+
40
+ async function readStdin() { let value = ""; process.stdin.setEncoding("utf8"); for await (const chunk of process.stdin) value += chunk; return value; }
41
+ async function hook(options) {
42
+ const payload = JSON.parse((await readStdin()) || "{}");
43
+ const identity = await callDaemon(options["state-dir"], "hook.resolve", { sessionId: payload.session_id ?? payload.sessionId });
44
+ const command = options["dd-flow-bin"]; const args = ["zcode", "hook", "handle", "--project-root", options["project-root"], "--json"];
45
+
46
+ return invokeNativeHook({ bin: command, args, home: options["dd-flow-home"], payload: { ...payload, _meta: { ...(payload._meta ?? {}), ddZcode: identity } } });
47
+ }
48
+
49
+ try {
50
+ const { positional, options } = parse(process.argv.slice(2)); const [family, command, modifier] = positional; let result;
51
+ const answers = options["answers-file"] ? JSON.parse(await promptFromFile(options["answers-file"])) : undefined;
52
+ const shared = common(options);
53
+ if (family === "doctor") result = await doctor(shared);
54
+ else if (family === "daemon" && command === "start") result = await durableDaemonStart({ ...shared, entryPath: process.argv[1] }, startDaemon);
55
+ else if (family === "daemon" && command === "serve") { await serveDaemon(shared.stateDir); process.exit(0); }
56
+ else if (family === "daemon" && command === "status") result = await callDaemon(shared.stateDir, "daemon.status");
57
+ else if (family === "daemon" && command === "operation" && modifier === "interrupt") result = await interruptDaemonOperation(shared.stateDir, options["operation-id"], { control_id: options["control-id"], stop_operation_id: options["stop-operation-id"], daemon_id: options["daemon-id"], session_id: options["session-id"], evidence: options.evidence ? JSON.parse(options.evidence) : null });
58
+ else if (family === "daemon" && command === "operation" && modifier === "settle") result = await settleDaemonOperation(shared.stateDir, options["operation-id"]);
59
+ else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(shared.stateDir, options["operation-id"]);
60
+ else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: shared.stateDir, cancelTree: options["cancel-tree"], timeoutMs: shared.timeoutMs });
61
+ else if (family === "hook" && command === "handle") { process.stdout.write(`${JSON.stringify(await hook(options))}\n`); process.exit(0); }
62
+ else if (family === "session") {
63
+ const params = { ...shared, answers, ...(command === "create" || command === "prompt" ? { prompt: options["prompt-file"] ? await promptFromFile(options["prompt-file"]) : options.prompt } : {}), ...(command === "fork" ? { target: JSON.parse(options["target-json"] ?? "null") } : {}) };
64
+ // The daemon owns the durable native-request deadline. A disposable RPC
65
+ // wait must not manufacture a second provider timeout.
66
+ const operation = `session.${command === "status" ? "inspect" : command}`;
67
+ const rpcTimeout = command === "prompt" && shared.timeoutMs === undefined
68
+ ? null
69
+ : (shared.timeoutMs ?? 1_800_000);
70
+ if (shared.stateDir) result = await callDaemon(shared.stateDir, operation, params, rpcTimeout);
71
+ else if (command === "create") result = await createSession(params);
72
+ else if (command === "prompt") result = await promptSession(params);
73
+ else if (["inspect", "status", "resume"].includes(command)) result = await inspectSession(params);
74
+ else if (command === "cancel") result = await cancelSession(params);
75
+ else if (command === "cancel-child") result = await cancelChild(params);
76
+ else if (command === "fork") result = await forkSession(params);
77
+ else throw new Error(`unknown session command: ${command}`);
78
+ } 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
+ process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`);
80
+ } 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);
83
+ }
@@ -0,0 +1,215 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, link, unlink, open } from "node:fs/promises";
3
+ import { randomUUID } from "node:crypto";
4
+ import path from "node:path";
5
+ import { hashJson, writeJsonAtomic } from "./runner-events.mjs";
6
+ import { errorRecord, isObservationLoss, reportedError } from "./operation-errors.mjs";
7
+ import { assertDaemonOwnership, budgetObservationScope, releaseBudgetObservation, completeBudgetOperation } from "./managed-daemon.mjs";
8
+ import { readModelObservations, modelAttribution } from "./model-observations.mjs";
9
+
10
+ const productive = new Set(["session.create", "session.prompt", "session.start", "session.fork", "session.resume"]);
11
+ const controls = new Map();
12
+ const dispatchGenerations = new Map();
13
+
14
+ async function controlDispatch(root, request, action) {
15
+ const key = request.operation === "daemon.stop" ? `stop:${request.params?.cancelTree === true}` : `cancel:${request.params?.sessionId ?? ""}`;
16
+ let pending = controls.get(root);
17
+ if (!pending) { pending = new Map(); controls.set(root, pending); }
18
+ if (pending.has(key)) return await pending.get(key);
19
+ // Preserve the adapter's identity check for every distinct cancel target.
20
+ // A stop arriving after cancel waits for its attempts before verification.
21
+ const predecessors = key.startsWith("stop:") ? [...pending.values()] : [];
22
+ const promise = (async () => { await Promise.allSettled(predecessors); return await action(); })();
23
+ pending.set(key, promise);
24
+ try { return await promise; }
25
+ finally { pending.delete(key); if (!pending.size) controls.delete(root); }
26
+ }
27
+ function location(root, id) {
28
+ if (typeof id !== "string" || !id) throw Object.assign(new Error("operation id is required"), { code: "operation_id_required" });
29
+ return path.join(root, "operations", createHash("sha256").update(id).digest("hex"));
30
+ }
31
+ async function read(file) { try { return JSON.parse(await readFile(file, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
32
+
33
+ export async function inspectDaemonOperation(root, id) {
34
+ if (typeof root !== "string" || !path.isAbsolute(root)) throw Object.assign(new Error("operation observation requires an absolute state directory"), { code: "invalid_path" });
35
+ const directory = location(root, id);
36
+ const requested = await read(path.join(directory, "requested.json"));
37
+ if (!requested) throw Object.assign(new Error("daemon operation is not recorded"), { code: "operation_not_found", details: { operation_id: id } });
38
+ const settlement = await read(path.join(directory, "settlement.json"));
39
+ return { ...requested, ...(await read(path.join(directory, "result.json")) ?? await read(path.join(directory, "interrupted.json")) ?? await read(path.join(directory, "observation-lost.json")) ?? { state: "running" }), ...(settlement ? { settlement } : {}) };
40
+ }
41
+
42
+ /** Retry only the bookkeeping that follows a durable productive receipt.
43
+ * This is intentionally separate from dispatch: it never invokes a native
44
+ * prompt/create/fork operation, so recovery cannot duplicate agent work. */
45
+ export async function settleDaemonOperation(root, id) {
46
+ const current = await inspectDaemonOperation(root, id);
47
+ if (current.state !== "completed") return current;
48
+ if (current.settlement?.state === "settled") return current;
49
+ let settlement;
50
+ try {
51
+ settlement = await completeBudgetOperation(root, {
52
+ id: current.operation_id,
53
+ operation: current.operation,
54
+ params: { sessionId: current.session_id ?? undefined }
55
+ }, current.result);
56
+ } catch (error) {
57
+ settlement = { state: "pending", reason: "settlement_error", observed_at: new Date().toISOString(), error: errorRecord(error) };
58
+ }
59
+ try { await writeSettlement(root, id, settlement); }
60
+ catch (error) {
61
+ settlement = { state: "blocked", reason: "settlement_storage_error", observed_at: new Date().toISOString(), error: errorRecord(error) };
62
+ }
63
+ return { ...current, settlement };
64
+ }
65
+
66
+ /** A sealed whole-tree stop is the only authority that may turn an unknown
67
+ * productive request into a terminal interruption. This is deliberately a
68
+ * journal operation: a closed daemon cannot be asked to manufacture a reply. */
69
+ export async function interruptDaemonOperation(root, id, barrier) {
70
+ if (!barrier || typeof barrier !== "object" || typeof barrier.control_id !== "string" || !barrier.control_id) throw Object.assign(new Error("interruption requires a sealed control identity"), { code: "stop_barrier_invalid" });
71
+ const current = await inspectDaemonOperation(root, id);
72
+ if (["completed", "failed", "interrupted"].includes(current.state)) return current;
73
+ if (!(["running", "awaiting_provider"].includes(current.state)) || !productive.has(current.operation)) throw Object.assign(new Error("only an observed productive operation may be interrupted"), { code: "operation_not_interruptible" });
74
+ // A lazy session/create is allowed to have no ACP/native session id yet.
75
+ // The owned daemon identity plus the sealed stop operation is the authority
76
+ // in that case; never manufacture a session id merely to satisfy the check.
77
+ const barrierSession = barrier.session_id ?? null;
78
+ const currentSession = current.session_id ?? null;
79
+ if (barrier.daemon_id !== current.daemon_id || barrierSession !== currentSession) throw Object.assign(new Error("interruption barrier belongs to another daemon operation"), { code: "stop_barrier_mismatch" });
80
+ const value = { state: "interrupted", outcome: "interrupted", observed_at: new Date().toISOString(), interrupted_by: { control_id: barrier.control_id, stop_operation_id: barrier.stop_operation_id, evidence: barrier.evidence ?? null }, error: { code: "operation_interrupted", message: "Operation was interrupted after the owned native tree was stopped", cause: current.error ?? null } };
81
+ const saved = await writeTerminal(root, id, value);
82
+ if (saved.state !== "interrupted" || saved.interrupted_by?.control_id !== barrier.control_id) throw Object.assign(new Error("operation already has a conflicting terminal outcome"), { code: "operation_terminal_conflict" });
83
+ return await inspectDaemonOperation(root, id);
84
+ }
85
+
86
+ /** At-most-once dispatch and a durable terminal reply, including when the socket
87
+ * observer disconnects. Recovery reads this receipt; it never resends a prompt. */
88
+ export async function durableDaemonDispatch(root, request, action, observe) {
89
+ if (request.operation === "operation.inspect") return await inspectDaemonOperation(root, request.params?.operationId);
90
+ if (request.operation === "daemon.status") {
91
+ const result = await action();
92
+ if (!result?.config?.journal) return result;
93
+ try { return { ...result, model_attribution: modelAttribution(await readModelObservations(result.config.journal)) }; }
94
+ catch (error) { return { ...result, model_observation_error: errorRecord(error) }; }
95
+ }
96
+ if (request.operation === "session.inspect") {
97
+ const scope = await budgetObservationScope(root, request.params?.sessionId);
98
+ const result = await action();
99
+ let settlement;
100
+ if (!scope?.operationIds.length) settlement = { state: "not_required" };
101
+ else if (result?.settled !== true) settlement = { state: "pending", reason: "tree_unsettled" };
102
+ else try {
103
+ await releaseBudgetObservation(scope, result);
104
+ settlement = { state: "settled", operation_ids: scope.operationIds, source: "inspection" };
105
+ } catch (error) {
106
+ settlement = { state: "pending", reason: "settlement_error", observed_at: new Date().toISOString(), error: errorRecord(error) };
107
+ }
108
+ return settlement.state === "not_required" || !result || typeof result !== "object" || Array.isArray(result) ? result : { ...result, settlement };
109
+ }
110
+ const control = ["daemon.stop", "session.cancel"].includes(request.operation);
111
+ if (!productive.has(request.operation) && !control) return await action();
112
+ const generation = dispatchGenerations.get(root) ?? 0;
113
+ if (control) dispatchGenerations.set(root, generation + 1);
114
+ const assertCurrent = () => { if (generation !== (dispatchGenerations.get(root) ?? 0)) throw Object.assign(new Error("control cancelled a queued productive dispatch"), { code: "operation_cancelled" }); };
115
+ const result = await journalDispatch(root, request, async () => {
116
+ // Persist before admission so a racing fence can observe the writer.
117
+ if (!control) await assertDaemonOwnership(root, { "operation-id": request.id, operation: request.operation, "native-session-id": request.params?.sessionId, "prompt-sha256": typeof request.params?.prompt === "string" ? createHash("sha256").update(request.params.prompt).digest("hex") : undefined }, assertCurrent);
118
+ if (!control) assertCurrent();
119
+ return control ? await controlDispatch(root, request, action) : await action();
120
+ });
121
+ if (control) return result;
122
+ let settlement;
123
+ try {
124
+ settlement = await completeBudgetOperation(root, request, result, observe);
125
+ } catch (error) {
126
+ settlement = { state: "pending", reason: "settlement_error", observed_at: new Date().toISOString(), error: errorRecord(error) };
127
+ }
128
+ try { await writeSettlement(root, request.id, settlement); }
129
+ catch (error) {
130
+ settlement = { state: "blocked", reason: "settlement_storage_error", observed_at: new Date().toISOString(), error: errorRecord(error) };
131
+ }
132
+ return settlement?.state === "not_required" || !result || typeof result !== "object" || Array.isArray(result)
133
+ ? result
134
+ : { ...result, settlement };
135
+ }
136
+
137
+ async function writeSettlement(root, id, value) {
138
+ const directory = location(root, id);
139
+ await writeJsonAtomic(path.join(directory, "settlement.json"), value);
140
+ }
141
+
142
+ /** Startup has no registered daemon yet. Its existing native implementation
143
+ * owns registration/admission; only the CLI entrypoint uses this journal path. */
144
+ export async function durableDaemonStart(options, action) {
145
+ const root = options.stateDir;
146
+ if (typeof root !== "string" || !path.isAbsolute(root)) throw Object.assign(new Error("daemon startup requires an absolute state directory"), { code: "invalid_path" });
147
+ // Keep only configuration locations, never inherited credentials. The owned
148
+ // scope can reuse this exact adapter launch after retiring the old daemon.
149
+ const launch = { schema_id: "dd-flow/native-launch@1", executable: options.entryPath ?? null, args: process.argv.slice(2), cwd: process.cwd(), runtime_owner: process.env.DD_FLOW_RUNTIME_OWNER ?? null,
150
+ env: Object.fromEntries(["CODEX_HOME", "DD_FLOW_CONFIG_HOME", "DD_FLOW_HOME", "DD_FLOW_BIN", "DD_FLOW_RESOURCE_HOME"].map(key => [key, process.env[key] ?? null])) };
151
+ const request = { id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation: "daemon.start", params: { ...options, runtimeOwner: process.env.DD_FLOW_RUNTIME_OWNER ?? null, resourceHome: process.env.DD_FLOW_RESOURCE_HOME ?? null, launch } };
152
+ return await journalDispatch(root, request, () => action(options));
153
+ }
154
+
155
+ async function journalDispatch(root, request, action) {
156
+ const directory = location(root, request.id), fingerprint = hashJson({ operation: request.operation, params: request.params ?? {} });
157
+ await mkdir(directory, { recursive: true, mode: 0o700 });
158
+ // Publish a complete request atomically and exclusively. Writing directly with
159
+ // wx exposes a partially written JSON document to a concurrent observer.
160
+ const temporary = path.join(directory, `requested-${randomUUID()}.json`);
161
+ const daemon = await read(path.join(root, "daemon.json"));
162
+ const startup = request.operation === "daemon.start";
163
+ const daemonId = daemon?.daemon_id ?? daemon?.config?.daemonId ?? null;
164
+ await writeJsonAtomic(temporary, { schema_id: "dd-flow/daemon-operation@1", operation_id: request.id, operation: request.operation, session_id: request.params?.sessionId ?? null, daemon_id: startup ? null : daemonId, ...(startup ? { predecessor_daemon_id: daemonId, launch: request.params?.launch } : {}), params_sha256: fingerprint, requested_at: new Date().toISOString() });
165
+ try {
166
+ await link(temporary, path.join(directory, "requested.json"));
167
+ } catch (error) {
168
+ if (error.code !== "EEXIST") throw error;
169
+ const saved = await inspectDaemonOperation(root, request.id);
170
+ if (saved.params_sha256 !== fingerprint) throw Object.assign(new Error("operation id was reused with different parameters"), { code: "operation_conflict", details: { operation_id: request.id } });
171
+ if (saved.state === "completed") return saved.result;
172
+ if (["failed", "interrupted"].includes(saved.state)) throw reportedError(saved.error, "provider operation failed");
173
+ throw Object.assign(new Error("provider operation has no confirmed terminal result; inspect the existing operation instead of resending it"), { code: "operation_observation_lost", details: { operation_id: request.id, receipt_directory: directory } });
174
+ } finally { await unlink(temporary); }
175
+ let result;
176
+ try {
177
+ result = await action();
178
+ }
179
+ catch (error) {
180
+ const unknown = isObservationLoss(error);
181
+ if (unknown) await writeJsonAtomic(path.join(directory, "observation-lost.json"), { state: "awaiting_provider", observed_at: new Date().toISOString(), error: errorRecord(error) });
182
+ else {
183
+ const terminal = await writeTerminal(root, request.id, { state: "failed", observed_at: new Date().toISOString(), error: errorRecord(error) });
184
+ if (terminal.state !== "failed") throw reportedError(terminal.error, "provider operation did not retain this failure");
185
+ }
186
+ throw error;
187
+ }
188
+ try {
189
+ const terminal = await writeTerminal(root, request.id, { state: "completed", ...(startup ? { started_daemon_id: result?.daemon_id ?? null } : {}), finished_at: new Date().toISOString(), result });
190
+ if (terminal.state !== "completed") throw reportedError(terminal.error, "provider operation was terminalized before its late reply arrived");
191
+ } catch (cause) {
192
+ if (cause?.code === "operation_interrupted" || cause?.code === "operation_terminal_conflict") throw cause;
193
+ throw Object.assign(new Error("provider returned but the durable reply could not be saved; do not resend the operation"), { code: "operation_observation_lost", cause, details: { operation_id: request.id, receipt_directory: directory } });
194
+ }
195
+ return result;
196
+ }
197
+
198
+ /** `result.json` is the one exclusive terminal slot. Old interrupted.json
199
+ * files remain readable for compatibility, but new writes never race them. */
200
+ async function writeTerminal(root, id, value) {
201
+ const file = path.join(location(root, id), "result.json");
202
+ const temporary = `${file}.${randomUUID()}.tmp`;
203
+ const handle = await open(temporary, "wx", 0o600);
204
+ try { await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`); await handle.sync(); }
205
+ finally { await handle.close(); }
206
+ try {
207
+ await link(temporary, file);
208
+ return value;
209
+ } catch (error) {
210
+ if (error.code !== "EEXIST") throw error;
211
+ const saved = await read(file);
212
+ if (!saved || !["completed", "failed", "interrupted"].includes(saved.state)) throw Object.assign(new Error("operation terminal record is invalid"), { code: "operation_terminal_conflict" });
213
+ return saved;
214
+ } finally { await unlink(temporary); }
215
+ }