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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/CHANGELOG.md +687 -0
  2. package/README.md +101 -1
  3. package/dist/build-info.json +10 -10
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +306 -0
  6. package/dist/cli/help.js +88 -12
  7. package/dist/cli/hook-ingress.js +78 -0
  8. package/dist/cli/input-preparation.js +116 -0
  9. package/dist/cli/native-hook-ingress.js +80 -0
  10. package/dist/cli/run-cli.js +1465 -450
  11. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  12. package/dist/cli/runtime-maintenance-ingress.js +52 -0
  13. package/dist/cli/runtime-process.js +153 -0
  14. package/dist/cli.js +17 -2
  15. package/dist/domain/stage-catalog.js +1 -1
  16. package/dist/harness-runtime/DD-ZCODE.md +99 -0
  17. package/dist/harness-runtime/bin/dd-agy.mjs +56 -0
  18. package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
  19. package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
  20. package/dist/harness-runtime/bin/dd-grok.mjs +40 -0
  21. package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
  22. package/dist/harness-runtime/bin/dd-zcode.mjs +87 -0
  23. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  24. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  25. package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
  26. package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
  27. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  28. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  29. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  30. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  31. package/dist/harness-runtime/lib/daemon-operations.mjs +370 -0
  32. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
  33. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +693 -0
  34. package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
  35. package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
  36. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +190 -0
  37. package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-codex.mjs +805 -0
  39. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +128 -0
  40. package/dist/harness-runtime/lib/dd-droid.mjs +561 -0
  41. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +490 -0
  42. package/dist/harness-runtime/lib/dd-grok.mjs +177 -0
  43. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +255 -0
  44. package/dist/harness-runtime/lib/dd-opencode.mjs +128 -0
  45. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +703 -0
  46. package/dist/harness-runtime/lib/dd-zcode.mjs +1128 -0
  47. package/dist/harness-runtime/lib/delegation-instructions.d.mts +13 -0
  48. package/dist/harness-runtime/lib/delegation-instructions.mjs +144 -0
  49. package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
  50. package/dist/harness-runtime/lib/driver-recovery.mjs +139 -0
  51. package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
  52. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  53. package/dist/harness-runtime/lib/lease-renewal.mjs +36 -0
  54. package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
  55. package/dist/harness-runtime/lib/lifecycle-command.mjs +272 -0
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
  58. package/dist/harness-runtime/lib/managed-daemon.d.mts +12 -0
  59. package/dist/harness-runtime/lib/managed-daemon.mjs +619 -0
  60. package/dist/harness-runtime/lib/model-observations.mjs +101 -0
  61. package/dist/harness-runtime/lib/native-children.d.mts +14 -0
  62. package/dist/harness-runtime/lib/native-children.mjs +94 -0
  63. package/dist/harness-runtime/lib/native-hook-command.d.mts +7 -0
  64. package/dist/harness-runtime/lib/native-hook-command.mjs +251 -0
  65. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  66. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  67. package/dist/harness-runtime/lib/observation-clock.mjs +92 -0
  68. package/dist/harness-runtime/lib/operation-context.mjs +5 -0
  69. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  70. package/dist/harness-runtime/lib/operation-errors.mjs +116 -0
  71. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  72. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  73. package/dist/harness-runtime/lib/process-json.d.mts +5 -0
  74. package/dist/harness-runtime/lib/process-json.mjs +201 -0
  75. package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
  76. package/dist/harness-runtime/lib/runner-events.mjs +200 -0
  77. package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
  78. package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
  79. package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
  80. package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
  81. package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
  82. package/dist/harness-runtime/lib/tool-observations.mjs +228 -0
  83. package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
  84. package/dist/protocol/local-files.js +18 -9
  85. package/dist/runtime/context.js +8 -4
  86. package/dist/schemas/agent-profile.schema.json +1 -1
  87. package/dist/schemas/code-review-result.schema.json +1 -1
  88. package/dist/schemas/code-verification.schema.json +7 -5
  89. package/dist/schemas/code-work-batch.schema.json +2 -2
  90. package/dist/schemas/flow-run.schema.json +2 -0
  91. package/dist/schemas/plan-review-decision.schema.json +3 -3
  92. package/dist/schemas/plan-review-result.schema.json +2 -2
  93. package/dist/schemas/run-control-receipt.schema.json +118 -0
  94. package/dist/schemas/run-control-request.schema.json +36 -0
  95. package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
  96. package/dist/services/branch-context.js +3 -3
  97. package/dist/services/canon.js +11 -8
  98. package/dist/services/check-runner-evidence.js +121 -0
  99. package/dist/services/cleanup.js +133 -40
  100. package/dist/services/cli-operation-classifier.js +22 -24
  101. package/dist/services/code-checks.js +851 -109
  102. package/dist/services/codex-capacity.js +367 -0
  103. package/dist/services/codex-hook-delivery.js +32 -0
  104. package/dist/services/command-context.js +80 -0
  105. package/dist/services/compatibility-preflight.js +13 -3
  106. package/dist/services/config.js +11 -8
  107. package/dist/services/continuation-outcome.js +20 -0
  108. package/dist/services/controller-fanout.js +136 -0
  109. package/dist/services/dashboard.js +135 -56
  110. package/dist/services/engines.js +245 -119
  111. package/dist/services/eval-snapshots.js +1146 -105
  112. package/dist/services/execution-policy.js +181 -0
  113. package/dist/services/external-work-launch.js +230 -0
  114. package/dist/services/harness-adapter.js +329 -36
  115. package/dist/services/harness-config.js +2 -0
  116. package/dist/services/hooks.js +753 -325
  117. package/dist/services/lanes.js +78 -65
  118. package/dist/services/lifecycle-command.js +1 -288
  119. package/dist/services/lifecycle-contract.js +1 -0
  120. package/dist/services/lifecycle-invocations.js +1828 -0
  121. package/dist/services/managed-daemon-binding.js +38 -0
  122. package/dist/services/managed-lease-monitor.js +133 -0
  123. package/dist/services/managed-processes.js +409 -51
  124. package/dist/services/merge-queue.js +228 -149
  125. package/dist/services/merge-server.js +163 -35
  126. package/dist/services/migrations.js +37 -16
  127. package/dist/services/native-daemon-history.js +49 -0
  128. package/dist/services/native-session-control.js +39 -0
  129. package/dist/services/plan-runtime.js +26 -11
  130. package/dist/services/plans.js +28 -25
  131. package/dist/services/projects.js +20 -15
  132. package/dist/services/prompts.js +28 -17
  133. package/dist/services/protocols.js +163 -87
  134. package/dist/services/recovery-observation-budget.js +121 -0
  135. package/dist/services/recovery-snapshot-database.js +145 -0
  136. package/dist/services/repair-intents.js +42 -0
  137. package/dist/services/review-copy.js +157 -0
  138. package/dist/services/review-deferral.js +23 -0
  139. package/dist/services/run-control-receipt.js +56 -0
  140. package/dist/services/run-control-worker.js +411 -0
  141. package/dist/services/run-control.js +985 -0
  142. package/dist/services/run-controller-adapter.js +399 -0
  143. package/dist/services/run-controller-capture.js +179 -0
  144. package/dist/services/run-controller-process.js +187 -0
  145. package/dist/services/run-controller-recovery.js +289 -0
  146. package/dist/services/run-controller-state.js +97 -0
  147. package/dist/services/run-controller.js +1035 -0
  148. package/dist/services/run-engine-bindings.js +1 -1
  149. package/dist/services/run-fork.js +147 -0
  150. package/dist/services/run-observations.js +152 -0
  151. package/dist/services/run-projection.js +22 -64
  152. package/dist/services/run-recovery-guard.js +75 -0
  153. package/dist/services/run-recovery-runtime.js +112 -0
  154. package/dist/services/run-recovery.js +349 -0
  155. package/dist/services/runs.js +558 -217
  156. package/dist/services/runtime-budget.js +430 -0
  157. package/dist/services/runtime-command.js +43 -0
  158. package/dist/services/runtime-scope-capture.js +52 -0
  159. package/dist/services/runtime-scope-control.js +468 -0
  160. package/dist/services/runtime-scope-resume.js +599 -0
  161. package/dist/services/runtime-scope-stop.js +180 -0
  162. package/dist/services/runtime-scope-worker.js +287 -0
  163. package/dist/services/runtime-service.js +96 -0
  164. package/dist/services/schema-validation.js +10 -8
  165. package/dist/services/scope-writer-cleanup.js +27 -0
  166. package/dist/services/sessions.js +35 -69
  167. package/dist/services/stage-blocker.js +17 -7
  168. package/dist/services/stage-context.js +185 -19
  169. package/dist/services/stage-lifecycle.js +98 -107
  170. package/dist/services/stage-pause.js +89 -75
  171. package/dist/services/stage-report-renderer.js +158 -6
  172. package/dist/services/stage-work-graph.js +35 -0
  173. package/dist/services/tooling.js +32 -23
  174. package/dist/services/usage.js +49 -101
  175. package/dist/services/verification-matrix.js +374 -0
  176. package/dist/services/vnext-code-review.js +222 -87
  177. package/dist/services/vnext-code.js +324 -251
  178. package/dist/services/vnext-contracts.js +10 -1
  179. package/dist/services/vnext-execution-profile.js +79 -4
  180. package/dist/services/vnext-fanout.js +182 -17
  181. package/dist/services/vnext-merge.js +335 -113
  182. package/dist/services/vnext-plan-review.js +276 -126
  183. package/dist/services/vnext-plan.js +227 -102
  184. package/dist/services/vnext-protocolize.js +335 -82
  185. package/dist/services/vnext-specify.js +119 -81
  186. package/dist/services/work-registry.js +1083 -194
  187. package/dist/services/workspace-bootstrap.js +87 -0
  188. package/dist/services/workspace-files.js +85 -0
  189. package/dist/services/worktrees.js +98 -44
  190. package/dist/shared/entity-references.js +8 -0
  191. package/dist/shared/errors.js +17 -0
  192. package/dist/storage/agent-output.js +26 -0
  193. package/dist/storage/database.js +785 -37
  194. package/dist/storage/foreign-key-transaction.js +39 -0
  195. package/dist/storage/immutable-input.js +45 -0
  196. package/dist/storage/import-guard.js +102 -0
  197. package/dist/storage/native-cleanup.js +52 -0
  198. package/dist/storage/paths.js +17 -4
  199. package/dist/storage/projection-lock.js +73 -0
  200. package/dist/storage/work-references.js +23 -0
  201. package/dist/storage/writer-contract.js +77 -0
  202. package/dist/storage/writer-migration.js +85 -0
  203. package/package.json +30 -13
  204. package/tools/repair-paused-run-status.mjs +84 -0
@@ -0,0 +1,703 @@
1
+ import { SETUP_STARTUP_MS, startupObserver, observationDetails } from './setup-progress.mjs';
2
+ import { callDaemonObserved } from './daemon-observation.mjs';
3
+ import { admitNativeRequestBuffer } from "./native-streams.mjs";
4
+ import { ObservationClock } from "./observation-clock.mjs";
5
+ import { spawn } from "node:child_process";
6
+ import { createHash, randomUUID } from "node:crypto";
7
+ import { chmod, lstat, mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
8
+ import net from "node:net";
9
+ import { durableDaemonDispatch, completeDaemonShutdown, commitDaemonShutdown, assertDaemonDispatch, assertDaemonRestartable, trackDaemonConnections, retireDaemonEndpoint, retireFailedDaemon } from "./daemon-operations.mjs";
10
+ import path from "node:path";
11
+ import { prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
12
+ import { acceptNativeHookRequest, hookBudget, readNativeHookFailures } from "./native-hook-command.mjs";
13
+ import { parseLifecycleCommand, continuationCommand } from "./lifecycle-command.mjs";
14
+ import { errorRecord, isProvenShellCorrection } from "./operation-errors.mjs";
15
+
16
+ import {
17
+ AcpBridge,
18
+ cancelSessionWithBridge,
19
+ cancelChildWithBridge,
20
+ createSessionWithBridge,
21
+ doctor,
22
+ forkSessionWithBridge,
23
+ inspectSessionWithBridge,
24
+ promptSessionWithBridge,
25
+ zcodeChildSessionIds,
26
+ observeClosedZcodeTree,
27
+ } from "./dd-zcode.mjs";
28
+
29
+ const REQUEST_SCHEMA = "dd-zcode/daemon-request@1";
30
+ const RESPONSE_SCHEMA = "dd-zcode/daemon-response@1";
31
+ const STATE_SCHEMA = "dd-zcode/daemon-state@1";
32
+ // ACP silence starts diagnostics; it is not a provider-failure deadline.
33
+ export const DEFAULT_LIVENESS_TIMEOUT_MS = 600_000;
34
+ export const DEFAULT_REQUEST_TIMEOUT_MS = null;
35
+
36
+ function positiveDuration(value, name, fallback) {
37
+ const duration = value ?? fallback;
38
+ if (!Number.isFinite(duration) || duration <= 0) throw new DaemonError("invalid_duration", `${name} must be a positive finite duration`, false, { name, value: duration });
39
+ return duration;
40
+ }
41
+
42
+ function optionalDuration(value, name, fallback) {
43
+ if (value === null) return null;
44
+ if (value === undefined && fallback === null) return null;
45
+ return positiveDuration(value, name, fallback);
46
+ }
47
+
48
+ class DaemonError extends Error {
49
+ constructor(code, message, retryable = false, details) {
50
+ super(message); this.code = code; this.retryable = retryable; this.details = details;
51
+ }
52
+ }
53
+
54
+ function absolute(value, label) {
55
+ if (!value || !path.isAbsolute(value)) throw new DaemonError("invalid_path", `${label} must be an absolute path`);
56
+ return path.resolve(value);
57
+ }
58
+
59
+ function locations(stateDir) {
60
+ const dir = absolute(stateDir, "--state-dir");
61
+ const localSocket = path.join(dir, "daemon.sock");
62
+ const socket = Buffer.byteLength(localSocket) < 100
63
+ ? localSocket
64
+ : `/tmp/dd-zcode-${createHash("sha256").update(dir).digest("hex").slice(0, 24)}.sock`;
65
+ return { dir, socket, state: path.join(dir, "daemon.json"), log: path.join(dir, "daemon.log") };
66
+ }
67
+
68
+ async function readState(stateDir) {
69
+ const file = locations(stateDir).state;
70
+ try { return JSON.parse(await readFile(file, "utf8")); }
71
+ catch (error) { if (error.code === "ENOENT") return null; throw error; }
72
+ }
73
+
74
+ async function writeState(file, value) {
75
+ const temporary = `${file}.${process.pid}.tmp`;
76
+ await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
77
+ await rename(temporary, file);
78
+ }
79
+
80
+ async function removeSocket(socket, dir) {
81
+ if (socket !== locations(dir).socket) throw new DaemonError("unsafe_socket", "daemon socket does not match the exact state directory");
82
+ try {
83
+ const info = await lstat(socket);
84
+ if (!info.isSocket()) throw new DaemonError("unsafe_socket", "refusing to remove a non-socket daemon path");
85
+ await unlink(socket);
86
+ } catch (error) { if (error.code !== "ENOENT") throw error; }
87
+ }
88
+
89
+ const errorPayload = errorRecord;
90
+
91
+ export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 30_000) {
92
+ const request = { schema_id: REQUEST_SCHEMA, id: process.env.DD_EVAL_OPERATION_ID ?? randomUUID(), operation, params };
93
+ return await callDaemonObserved({ stateDir, socket: locations(stateDir).socket, request, responseSchema: RESPONSE_SCHEMA, harness: 'zcode', timeoutMs });
94
+ }
95
+
96
+ const ZCODE_ROUTING_ENV = ["ZCODE_HOME", "ZCODE_STORAGE_DIR", "ZCODE_SESSION_DB_PATH"];
97
+
98
+ function nativeRoutingEnv() {
99
+ return Object.fromEntries(ZCODE_ROUTING_ENV.filter(key => process.env[key] !== undefined)
100
+ .map(key => [key, absolute(process.env[key], key)]));
101
+ }
102
+
103
+ function sameNativeRouting(previous, config) {
104
+ return ZCODE_ROUTING_ENV.every(key => (previous?.env?.[key] ?? null) === (config.env?.[key] ?? null));
105
+ }
106
+
107
+ function sameConfig(state, config) {
108
+ return sameNativeRouting(state.config, config) && ["cwd", "journal", "bin", "zcodePath", "ddFlowBin", "ddFlowHome", "projectRoot", "resourceHome", "livenessTimeoutMs", "requestTimeoutMs"].every((key) => (state.config?.[key] ?? null) === (config[key] ?? null));
109
+ }
110
+
111
+ export async function startDaemon(options) {
112
+ const paths = locations(options.stateDir);
113
+ const hasFlowContext = Boolean(options.ddFlowHome || options.projectRoot || options.ddFlowBin);
114
+ if (hasFlowContext && (!options.ddFlowHome || !options.projectRoot)) {
115
+ throw new DaemonError("flow_context_incomplete", "--dd-flow-home and --project-root must be provided together for ZCode lifecycle forwarding");
116
+ }
117
+ const config = {
118
+ cwd: absolute(options.cwd, "--cwd"),
119
+ journal: absolute(options.journal, "--journal"),
120
+ bin: options.bin ?? null,
121
+ zcodePath: options.zcodePath ?? null,
122
+ // The worker itself invokes `dd-flow` from PATH. Use that same executable
123
+ // for the trusted event bridge unless a pinned executable was supplied.
124
+ ddFlowBin: hasFlowContext ? (options.ddFlowBin ? absolute(options.ddFlowBin, "--dd-flow-bin") : "dd-flow") : null,
125
+ ddFlowHome: options.ddFlowHome ? absolute(options.ddFlowHome, "--dd-flow-home") : null,
126
+ projectRoot: options.projectRoot ? absolute(options.projectRoot, "--project-root") : null,
127
+ resourceHome: process.env.DD_FLOW_RESOURCE_HOME ? absolute(process.env.DD_FLOW_RESOURCE_HOME, "DD_FLOW_RESOURCE_HOME") : null,
128
+ env: { ...nativeRoutingEnv(), ...(options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home"), ...(options.ddFlowBin ? { DD_FLOW_BIN: absolute(options.ddFlowBin, "--dd-flow-bin") } : {}) } : {}) },
129
+ livenessTimeoutMs: positiveDuration(options.livenessTimeoutMs, "livenessTimeoutMs", DEFAULT_LIVENESS_TIMEOUT_MS),
130
+ requestTimeoutMs: optionalDuration(options.requestTimeoutMs, "requestTimeoutMs", DEFAULT_REQUEST_TIMEOUT_MS),
131
+ };
132
+ await prepareRuntimeOwner(config, paths.dir);
133
+ await mkdir(paths.dir, { recursive: true, mode: 0o700 });
134
+ await chmod(paths.dir, 0o700);
135
+ let previous = await readState(paths.dir); assertDaemonRestartable(previous);
136
+ try {
137
+ const status = await callDaemon(paths.dir, "daemon.status", {}, 1000);
138
+ if (!sameConfig(status, config)) throw new DaemonError("daemon_config_mismatch", "a daemon is already running with different configuration");
139
+ return { ...status, already_running: true };
140
+ } catch (error) { if (error.code !== "daemon_not_running" && error.code !== "daemon_timeout") throw error; }
141
+
142
+ previous = await readState(paths.dir); assertDaemonRestartable(previous);
143
+ if (previous?.shutdown_state === "clean") {
144
+ if (!sameNativeRouting(previous.config, config)) throw new DaemonError("daemon_config_mismatch", "retained ZCode native storage routing changed");
145
+ await authorizeRetainedDaemonResume(paths.dir, previous, options.sessionId, config);
146
+ }
147
+ if (previous?.shutdown_state === "running" && previous.active_tree) {
148
+ throw new DaemonError("invalid_harness_crash", "the previous daemon died with an active or unproven Session tree", false, { daemon_id: previous.daemon_id });
149
+ }
150
+ await removeSocket(paths.socket, paths.dir);
151
+ const versions = await doctor({ bin: config.bin, zcodePath: config.zcodePath });
152
+ if (hasFlowContext && versions.compatible !== true) {
153
+ throw new DaemonError("zcode_lifecycle_unqualified", "ZCode native event identity is not qualified for managed CLI admission; inspect lifecycle_qualification before starting E2E.", false, { startup_resources: "not_created", observed_runtime: versions.observed_runtime, lifecycle_qualification: versions.lifecycle_qualification });
154
+ }
155
+ const resourceProcess = await registerDaemonProcess(config, {
156
+ kind: "zcode-daemon", owner: `zcode:${path.basename(paths.dir)}`, operation: `zcode-daemon:${path.basename(paths.dir)}`,
157
+ stdout: paths.log, stderr: paths.log,
158
+ });
159
+ const state = {
160
+ schema_id: STATE_SCHEMA,
161
+ daemon_id: randomUUID(),
162
+ pid: null,
163
+ socket: paths.socket,
164
+ cwd: config.cwd,
165
+ journal: config.journal,
166
+ started_at: new Date().toISOString(),
167
+ shutdown_state: "starting",
168
+ active_tree: false,
169
+ recovery_status: previous?.shutdown_state === "running" ? "recovered_idle" : "clean_start",
170
+ versions: versions.versions,
171
+ config,
172
+ sessions: previous?.sessions ?? [],
173
+ resource_process: resourceProcess,
174
+ };
175
+ await writeState(paths.state, state);
176
+ const log = await open(paths.log, "a", 0o600);
177
+ const child = spawn(process.execPath, [absolute(options.entryPath, "dd-zcode entry"), "daemon", "serve", "--state-dir", paths.dir], {
178
+ cwd: config.cwd, detached: true, stdio: ["ignore", log.fd, log.fd],
179
+ });
180
+ child.unref();
181
+ await log.close();
182
+ try {
183
+ await confirmDaemonProcess(config, resourceProcess, child);
184
+ const startupClock = new ObservationClock({ timeoutMs: SETUP_STARTUP_MS }); const startupProgress = startupObserver(paths.dir, state.daemon_id);
185
+ while (!startupClock.sample(await startupProgress())) {
186
+ await new Promise((resolve) => setTimeout(resolve, 100));
187
+ try { return await callDaemon(paths.dir, "daemon.status", {}, 1000); }
188
+ catch (error) { if (error.code !== "daemon_not_running" && error.code !== "daemon_timeout") throw error; }
189
+ }
190
+ throw Object.assign(new DaemonError("daemon_start_failed", "daemon did not become ready", true), { details: observationDetails(startupClock, 'daemon-start', child.pid) });
191
+ } catch (error) {
192
+ await cleanupFailedDaemonStart(config, resourceProcess, child, error, () => readState(paths.dir), state.daemon_id);
193
+ throw error;
194
+ }
195
+ }
196
+
197
+ export async function stopDaemon(options) {
198
+ const timeoutMs = options.timeoutMs ?? 30_000;
199
+ const { socket } = locations(options.stateDir);
200
+ return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), socket, timeoutMs, { readState: () => readState(options.stateDir) });
201
+ }
202
+
203
+ export class DaemonRuntime {
204
+ constructor(paths, state, bridge, initialized) {
205
+ this.paths = paths; this.state = state; this.bridge = bridge; this.initialized = initialized;
206
+ this.sessions = new Map((state.sessions ?? []).map((item) => [item.provider_session_id, item]));
207
+ this.liveAdapters = new Set();
208
+ this.activeProductive = null; this.server = null; this.persisting = Promise.resolve();
209
+ }
210
+
211
+ persist(patch = {}) {
212
+ Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...this.sessions.values()] });
213
+ const snapshot = structuredClone(this.state);
214
+ this.persisting = this.persisting.catch(() => {}).then(() => writeState(this.paths.state, snapshot));
215
+ return this.persisting;
216
+ }
217
+
218
+ options(params) {
219
+ const known = this.sessions.get(params.sessionId);
220
+ const adapter = params.adapterSessionId ?? known?.adapter_session_id ?? params.sessionId;
221
+ const generation = this.activeGeneration ?? this.controlGeneration ?? 0;
222
+ return {
223
+ ...this.state.config, ...params, operationId: this.state.active_request?.request_id, knownChildSessionIds: known?.known_child_session_ids ?? [], adapterSessionId: adapter,
224
+ neverPromptedRootProof: this.neverPromptedRootProof(params.sessionId),
225
+ daemonId: this.state.daemon_id, allowBackground: true, liveSession: this.liveAdapters.has(adapter),
226
+ assertDispatch: () => { if (generation !== (this.controlGeneration ?? 0)) throw new DaemonError("operation_cancelled", "ZCode prompt was cancelled before native dispatch"); },
227
+ onActivity: async ({ sessionId, requestId, progress }) => {
228
+ const request = this.state.active_request;
229
+ if (!request || request.adapter_session_id !== sessionId || this.activeGeneration !== request.generation) return;
230
+ await this.persist({ active_request: { ...request, observation: { state: "productive", request_id: requestId, progress } } });
231
+ },
232
+ // The bridge owns the quiet timer; the daemon owns the durable status.
233
+ // Do not infer provider state or cancel here.
234
+ onInactivity: async ({ requestId, sessionId, lastActivityAt, idleTimeoutMs, cursor }) => {
235
+ const request = this.state.active_request;
236
+ // `sessionId` is ACP identity; the durable request's `session_id` is
237
+ // native/provider identity. These strings can coincide but are not
238
+ // interchangeable.
239
+ if (!request || request.adapter_session_id !== sessionId) return;
240
+ const adapterSessionId = request.adapter_session_id;
241
+ let diagnostics;
242
+ try {
243
+ // Both are non-mutating native observations. In particular, do not
244
+ // inspect/resume a running child: some ZCode children are known to
245
+ // be non-resident and unsubscribable while their request is alive.
246
+ const [resident, topology] = await Promise.all([
247
+ this.bridge.request("zcode/session/resident", { sessionId: adapterSessionId }, 5_000),
248
+ // childSessionIds is complete on every page. This diagnostic reads
249
+ // identities only, not ended outcomes or a settlement verdict.
250
+ this.bridge.request("zcode/session/retainedSubagents", { sessionId: adapterSessionId }, 5_000),
251
+ ]);
252
+ diagnostics = {
253
+ observed_at: new Date().toISOString(), adapter_session_id: adapterSessionId,
254
+ resident: resident?.resident === true,
255
+ child_session_ids: zcodeChildSessionIds(topology?.topology ?? topology),
256
+ };
257
+ } catch (error) {
258
+ diagnostics = { observed_at: new Date().toISOString(), adapter_session_id: adapterSessionId, observation_error: errorPayload(error) };
259
+ }
260
+ // The probe is asynchronous. Do not recreate a completed or
261
+ // reconnected request with a stale callback snapshot.
262
+ const current = this.state.active_request;
263
+ if (!current || current.request_id !== request.request_id || current.adapter_session_id !== adapterSessionId || this.activeGeneration !== request.generation
264
+ || cursor !== undefined && this.bridge.pending?.get(requestId)?.progress?.cursor !== cursor && cursor !== 0) return;
265
+ await this.persist({ active_request: {
266
+ ...current,
267
+ observation: { state: "quiet", request_id: requestId, detected_at: new Date().toISOString(), last_activity_at: new Date(lastActivityAt).toISOString(), quiet_threshold_ms: idleTimeoutMs, diagnostics },
268
+ } });
269
+ },
270
+ // Native create publishes its identities before post-create work. Keep
271
+ // that allocation in the same durable request record so recovery can
272
+ // distinguish "no Session allocated" from "allocated, later step lost".
273
+ onSessionAliasAllocated: async (identity) => {
274
+ const current = this.state.active_request;
275
+ await this.persist({
276
+ active_request: {
277
+ ...(current ?? {
278
+ request_id: randomUUID(),
279
+ session_id: params.sessionId ?? null,
280
+ adapter_session_id: identity.adapter_session_id,
281
+ generation: this.controlGeneration ?? 0,
282
+ dispatched_at: new Date().toISOString(),
283
+ }),
284
+ allocation: {
285
+ adapter_session_id: identity.adapter_session_id,
286
+ provider_session_id: null,
287
+ cwd: identity.cwd,
288
+ allocated_at: new Date().toISOString(),
289
+ },
290
+ },
291
+ });
292
+ },
293
+ onSessionAllocated: async (identity) => {
294
+ const current = this.state.active_request;
295
+ // A late identity is evidence, not permission to resurrect a completed
296
+ // request or overwrite another operation's ownership.
297
+ this.track(identity);
298
+ const allocated = this.sessions.get(identity.provider_session_id);
299
+ if (allocated && this.activeProductive === "session.create" && current?.allocation?.adapter_session_id === identity.adapter_session_id
300
+ && allocated.prompt_dispatched === undefined) {
301
+ allocated.prompt_dispatched = false;
302
+ allocated.created_by_daemon_id = this.state.daemon_id;
303
+ }
304
+ if (!current || current.allocation?.adapter_session_id !== identity.adapter_session_id) {
305
+ await this.persist({});
306
+ return;
307
+ }
308
+ await this.persist({
309
+ active_request: {
310
+ ...(current ?? {
311
+ request_id: randomUUID(),
312
+ session_id: params.sessionId ?? null,
313
+ adapter_session_id: params.adapterSessionId ?? params.sessionId ?? null,
314
+ generation: this.controlGeneration ?? 0,
315
+ dispatched_at: new Date().toISOString(),
316
+ }),
317
+ allocation: {
318
+ adapter_session_id: identity.adapter_session_id,
319
+ provider_session_id: identity.provider_session_id,
320
+ cwd: identity.cwd,
321
+ allocated_at: new Date().toISOString(),
322
+ },
323
+ },
324
+ });
325
+ },
326
+ };
327
+ }
328
+
329
+ track(result, topology = result?.evidence?.subagents) {
330
+ if (result?.provider_session_id) { const existing = this.sessions.get(result.provider_session_id); const suppliedParent = result.parent_provider_session_id;
331
+ if (existing && suppliedParent !== undefined && suppliedParent !== existing.parent_provider_session_id) {
332
+ throw new DaemonError("session_ancestry_conflict", "ZCode Session parent changed after it was recorded", false, { session_id: result.provider_session_id, expected_parent: existing.parent_provider_session_id, actual_parent: suppliedParent });
333
+ }
334
+ const parent = suppliedParent ?? existing?.parent_provider_session_id ?? null;
335
+ const parentRoot = parent ? this.sessions.get(parent)?.root_provider_session_id ?? null : null;
336
+ this.sessions.set(result.provider_session_id, {
337
+ provider_session_id: result.provider_session_id,
338
+ adapter_session_id: result.adapter_session_id ?? existing?.adapter_session_id ?? result.provider_session_id,
339
+ parent_provider_session_id: parent,
340
+ root_provider_session_id: existing?.root_provider_session_id ?? (parent ? parentRoot : result.provider_session_id),
341
+ topology: topology ?? existing?.topology ?? null,
342
+ known_child_session_ids: [...new Set([...(existing?.known_child_session_ids ?? zcodeChildSessionIds(existing?.topology)), ...zcodeChildSessionIds(topology)])],
343
+ completed_turns: existing?.completed_turns ?? 0,
344
+ prompt_dispatched: existing?.prompt_dispatched,
345
+ created_by_daemon_id: existing?.created_by_daemon_id,
346
+ close_receipt: result.close?.closed === true ? result : existing?.close_receipt ?? null,
347
+ }); }
348
+ if (result?.adapter_session_id) this.liveAdapters.add(result.adapter_session_id);
349
+ for (const child of topology?.running ?? []) {
350
+ const adapter = child.childSessionId ?? child.adapterSessionId ?? child.sessionId;
351
+ if (typeof adapter === "string" && adapter) this.liveAdapters.add(adapter);
352
+ }
353
+ }
354
+
355
+ treeRunning(result) {
356
+ const closed = this.sessions.get(result?.provider_session_id)?.close_receipt;
357
+ if (closed) return closed.settled !== true;
358
+ const evidence = result?.evidence ?? result;
359
+ const children = evidence?.subagents?.running;
360
+ return evidence?.read?.projection?.status !== "idle" || !Array.isArray(children) || children.length > 0 || Boolean(evidence?.subagents?.observation_error);
361
+ }
362
+
363
+ async productive(name, task, request = null) {
364
+ if (this.activeProductive) throw new DaemonError("operation_busy", `${this.activeProductive} is already running`, true);
365
+ this.activeProductive = name;
366
+ this.activeGeneration = this.controlGeneration ?? 0;
367
+ const current = this.nativeHookOperation = { operationId: request?.operation_id, generation: this.activeGeneration, requests: new Map() };
368
+ let rejectHook;
369
+ const hookFailed = new Promise((_, reject) => { rejectHook = reject; });
370
+ void hookFailed.catch(() => {}); // A hook can fail while the dispatch receipt is still persisting.
371
+ current.reject = error => { if (this.nativeHookOperation !== current || current.error) return; current.error = error; this.bridge.lifecycleFailure ??= error; this.bridge.notificationError = this.bridge.lifecycleFailure; this.bridge.rejectAll(this.bridge.lifecycleFailure); rejectHook(this.bridge.lifecycleFailure); };
372
+ const timer = setInterval(() => { void this.observeHookFailures(current).catch(error => current.reject(error)); }, 100);
373
+ try {
374
+ await this.persist({ active_tree: true, active_operation: name, ...(request ? { active_request: request } : {}) });
375
+ const result = await Promise.race([task(), hookFailed]);
376
+ this.track(result);
377
+ const running = this.treeRunning(result);
378
+ await this.persist({ active_tree: running, active_operation: null, active_request: null });
379
+ return { ...result, settled: !running };
380
+ } catch (error) {
381
+ // Keep an allocated identity visible until the controller seals the
382
+ // outcome. Clearing it here was the source of `provider_identity_unknown`
383
+ // after a late/partial create: the native Session existed, but the
384
+ // daemon state no longer carried the only correlation to it.
385
+ const active = this.state?.active_request;
386
+ try {
387
+ await hookBudget(1_000, null).run(() => this.persist({
388
+ active_operation: null,
389
+ active_request: active?.allocation
390
+ ? { ...active, outcome: "unknown", error: errorPayload(error) }
391
+ : null,
392
+ }), "failure_persistence");
393
+ } catch (cleanupError) {
394
+ // Preserve the operation's primary failure if durable cleanup itself
395
+ // is unavailable. The caller will retain the outcome as unknown and
396
+ // the next recovery pass can retry the state write.
397
+ if (error && typeof error === "object") error.cleanup_error = errorPayload(cleanupError);
398
+ }
399
+ throw error;
400
+ } finally { clearInterval(timer); current.requests.clear(); if (this.nativeHookOperation === current) this.nativeHookOperation = null; this.activeProductive = null; this.activeGeneration = null; }
401
+ }
402
+
403
+ async observeHookFailures(current = this.nativeHookOperation) {
404
+ if (!current || this.nativeHookOperation !== current || current.reading) return;
405
+ current.reading = true;
406
+ try { await readNativeHookFailures(this.paths.dir, current.requests, current, error => {
407
+ if (!isProvenShellCorrection(error)) current.reject(error);
408
+ }); } finally { current.reading = false; }
409
+ }
410
+
411
+ async requireSettled(params) {
412
+ if (this.sessions.get(params.sessionId)?.close_receipt) throw new DaemonError("session_closed", "Closed Session requires explicit recovery");
413
+ // A freshly created ZCode Session has no prior Turn and is not yet exposed
414
+ // by the provider's subagent index. Its first prompt is therefore safe.
415
+ if (this.neverPromptedRootProof(params.sessionId)) return null;
416
+ const observed = await inspectSessionWithBridge(this.bridge, this.options(params));
417
+ const runningChildren = observed.subagents?.running ?? [];
418
+ if (this.treeRunning(observed)) {
419
+ throw new DaemonError("tree_not_settled", "the prior ZCode agent tree is not fully settled", false, { session_id: params.sessionId, running_children: runningChildren.map((child) => child.sessionId ?? child.childSessionId ?? null) });
420
+ }
421
+ this.track(observed, observed.subagents);
422
+ return observed;
423
+ }
424
+
425
+ neverPromptedRootProof(sessionId) {
426
+ const known = this.sessions.get(sessionId);
427
+ return known?.prompt_dispatched === false && known.created_by_daemon_id === this.state.daemon_id
428
+ && known.parent_provider_session_id === null && known.root_provider_session_id === sessionId
429
+ && Array.isArray(known.known_child_session_ids) && known.known_child_session_ids.length === 0
430
+ ? { source: "owned_daemon_create_without_prompt", daemon_id: this.state.daemon_id, provider_session_id: sessionId }
431
+ : null;
432
+ }
433
+
434
+ async markPromptDispatched(sessionId) {
435
+ const known = this.sessions.get(sessionId);
436
+ if (known) known.prompt_dispatched = true;
437
+ await this.persist();
438
+ }
439
+
440
+ async dispatch(operation, params, operationId = null) {
441
+ if (["session.create", "session.prompt", "session.start", "session.fork", "session.resume"].includes(operation)) assertDaemonDispatch(this.paths.dir);
442
+ if (operation === "daemon.status") return {
443
+ daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket,
444
+ cwd: this.state.cwd, journal: this.state.journal, versions: this.state.versions,
445
+ started_at: this.state.started_at, updated_at: this.state.updated_at,
446
+ shutdown_state: this.state.shutdown_state, recovery_status: this.state.recovery_status,
447
+ active_tree: this.state.active_tree, active_operation: this.activeProductive, active_request: this.state.active_request ?? null,
448
+ sessions: [...this.sessions.values()], config: this.state.config,
449
+ };
450
+ if (operation === "hook.resolve") {
451
+ const sessionId = String(params.sessionId ?? "");
452
+ const session = this.sessions.get(sessionId);
453
+ if (!session?.root_provider_session_id) throw new DaemonError("hook_identity_unknown", "ZCode hook Session is not a tracked controlled Session", false, { session_id: sessionId });
454
+ const identity = { daemonId: this.state.daemon_id, rootProviderSessionId: session.root_provider_session_id, parentProviderSessionId: session.parent_provider_session_id ?? null, observedProfile: this.bridge.modelProfiles?.get(sessionId) ?? {} };
455
+ const payload = params.payload, command = payload?.tool_input?.command ?? payload?.tool_input?.cmd;
456
+ if (payload?.hook_event_name === "PreToolUse" && typeof command === "string" && (["lifecycle", "auxiliary"].includes(parseLifecycleCommand(command).participation) || continuationCommand(command))) {
457
+ const current = this.nativeHookOperation, active = this.state.active_request;
458
+ const tool = payload.tool_use_id ?? payload.tool_call_id;
459
+ const pending = [...(this.bridge.pending?.values() ?? [])].some(request => request.method === "session/prompt" && request.sessionId === active?.adapter_session_id);
460
+ const root = active?.session_id ?? active?.allocation?.provider_session_id;
461
+ if (!current || !active || !pending || current.generation !== (this.controlGeneration ?? 0) || session.root_provider_session_id !== root || !tool) throw new DaemonError("native_hook_unproven", "Native hook has no exact active Session operation");
462
+ const binding = await hookBudget(1_000, null).run(() => acceptNativeHookRequest(this.paths.dir, { daemon_id: this.state.daemon_id, operation_id: current.operationId, turn_generation: current.generation, provider_session_id: sessionId, root_provider_session_id: session.root_provider_session_id, tool_call_id: tool, event: "PreToolUse", command_sha256: createHash("sha256").update(command).digest("hex"), native_cwd: payload.cwd ?? null }, current), "hook_binding");
463
+ if (this.nativeHookOperation !== current) throw new DaemonError("native_hook_unproven", "Native hook operation changed during acceptance");
464
+ current.requests.set(binding.request_id, { current, binding }); identity.nativeHookBinding = binding;
465
+ }
466
+ return identity;
467
+ }
468
+ if (operation === "hook.failed") { await this.observeHookFailures(); return { observed: true }; }
469
+ if (operation === "session.create") {
470
+ const request = {
471
+ operation_id: operationId,
472
+ request_id: randomUUID(),
473
+ session_id: params.sessionId ?? null,
474
+ adapter_session_id: params.adapterSessionId ?? params.sessionId ?? null,
475
+ generation: this.controlGeneration ?? 0,
476
+ dispatched_at: new Date().toISOString(),
477
+ request_timeout_ms: this.state.config.requestTimeoutMs,
478
+ };
479
+ return await this.productive(operation, async () => await createSessionWithBridge(this.bridge, this.options(params), this.initialized), request);
480
+ }
481
+ if (operation === "session.prompt") {
482
+ if (this.sessions.get(params.sessionId)?.close_receipt) throw new DaemonError("session_closed", "Closed Session requires explicit recovery before another prompt");
483
+ const known = this.sessions.get(params.sessionId);
484
+ const request = {
485
+ operation_id: operationId,
486
+ request_id: randomUUID(),
487
+ session_id: params.sessionId,
488
+ adapter_session_id: params.adapterSessionId ?? known?.adapter_session_id ?? params.sessionId,
489
+ generation: this.controlGeneration ?? 0,
490
+ dispatched_at: new Date().toISOString(),
491
+ request_timeout_ms: this.state.config.requestTimeoutMs,
492
+ };
493
+ const result = await this.productive(operation, async () => { await this.requireSettled(params); await this.markPromptDispatched(params.sessionId); return await promptSessionWithBridge(this.bridge, { ...this.options(params), timeoutMs: request.request_timeout_ms }); }, request);
494
+ const session = this.sessions.get(result.provider_session_id); if (session) session.completed_turns = (session.completed_turns ?? 0) + 1;
495
+ await this.persist(); return result;
496
+ }
497
+ if (operation === "session.fork") return await this.productive(operation, async () => { await this.requireSettled(params); await this.markPromptDispatched(params.sessionId); return await forkSessionWithBridge(this.bridge, this.options(params)); });
498
+ if (operation === "session.inspect" || operation === "session.resume") {
499
+ const closed = this.sessions.get(params.sessionId)?.close_receipt;
500
+ if (closed) {
501
+ if (operation === "session.resume") throw new DaemonError("session_closed", "Closed Session requires explicit recovery");
502
+ const topology = closed.topology;
503
+ if (topology?.root_session_id !== params.sessionId || !Array.isArray(topology.child_session_ids) || topology.child_session_ids.some(id => typeof id !== "string" || !id)) return { ...closed, settled: false, observation_error: { code: "topology_incomplete", message: "closed Session has no native topology identity" } };
504
+ const previous = this.state.settlement_observations?.[params.sessionId];
505
+ // A confirmed closed tree cannot resume through this daemon. Retain
506
+ // that terminal evidence even after its bridge has been retired.
507
+ for (const receipt of [previous, closed]) {
508
+ if (receipt?.close?.closed === true && receipt.settled === true
509
+ && receipt.topology?.complete === true && receipt.topology.root_session_id === params.sessionId
510
+ && Array.isArray(receipt.topology.child_session_ids)) return receipt;
511
+ }
512
+ const known = [...new Set([...topology.child_session_ids, ...(previous?.topology?.child_session_ids ?? [])])];
513
+ const emptyRoot = this.neverPromptedRootProof(params.sessionId);
514
+ const observation = { ...closed, ...await observeClosedZcodeTree(this.bridge, params.sessionId, known, emptyRoot && closed.close?.closed === true ? {...emptyRoot,close_confirmed:true} : null), observed_at: new Date().toISOString() };
515
+ // Observations are not ownership inventory: their timestamps must not
516
+ // invalidate a drain fence on every read.
517
+ this.state.settlement_observations ??= {};
518
+ this.state.settlement_observations[params.sessionId] = observation;
519
+ await this.persist();
520
+ return observation;
521
+ }
522
+ const adapter = params.adapterSessionId ?? params.sessionId;
523
+ if (!this.liveAdapters.has(adapter) && this.activeProductive) {
524
+ throw new DaemonError(
525
+ "child_inspection_unavailable",
526
+ "cannot inspect an unregistered child while a daemon-owned turn is running; inspection would resume and mutate it",
527
+ true,
528
+ { session_id: params.sessionId },
529
+ );
530
+ }
531
+ const result = await inspectSessionWithBridge(this.bridge, this.options(params));
532
+ // A child can be inspected through its parent's live bridge, but is not
533
+ // an independently daemon-owned root Session. Do not promote it into
534
+ // `sessions`: daemon shutdown would then treat the child as a second
535
+ // root and could reject an otherwise settled parent tree.
536
+ if (this.sessions.has(result.provider_session_id)) this.track(result, result.subagents);
537
+ else if (result.adapter_session_id) this.liveAdapters.add(result.adapter_session_id);
538
+ await this.persist({ active_tree: this.treeRunning(result) || Boolean(this.activeProductive) });
539
+ return { ...result, settled: !this.treeRunning(result) && !this.activeProductive };
540
+ }
541
+ if (operation === "session.cancel") {
542
+ if (!this.sessions.has(params.sessionId)) throw new DaemonError("session_identity_mismatch", "cancellation Session is not owned by this daemon");
543
+ const closed = this.sessions.get(params.sessionId)?.close_receipt;
544
+ if (closed) return closed;
545
+ this.controlGeneration = (this.controlGeneration ?? 0) + 1;
546
+ const result = await cancelSessionWithBridge(this.bridge, this.options(params));
547
+ this.track(result, result.after);
548
+ await this.persist({ active_tree: result.settled !== true && (this.treeRunning(result) || Boolean(this.activeProductive)) });
549
+ return result;
550
+ }
551
+ if (operation === "session.cancel-child") {
552
+ const result = await cancelChildWithBridge(this.bridge, this.options(params));
553
+ this.track(result, result.after);
554
+ await this.persist({ active_tree: this.treeRunning(result) || Boolean(this.activeProductive) });
555
+ return result;
556
+ }
557
+ if (operation === "daemon.stop") return await completeDaemonShutdown(this.paths.dir, this.state, patch => this.persist(patch), () => this.prepareStop(params.cancelTree === true), [
558
+ ["provider_resource", () => finishDaemonProcess(this.state.config, this.state.provider_process)],
559
+ ["daemon_resource", () => finishDaemonProcess(this.state.config, this.state.resource_process)]
560
+ ]);
561
+ throw new DaemonError("unknown_operation", `unknown daemon operation: ${operation}`);
562
+ }
563
+
564
+ async prepareStop(cancel) {
565
+ if (this.activeProductive && !cancel) throw new DaemonError("tree_not_settled", "productive operation is running");
566
+ if (cancel) this.controlGeneration = (this.controlGeneration ?? 0) + 1;
567
+ if (!this.state.native_stop_proof || this.state.native_stop_proof.daemon_id !== this.state.daemon_id) {
568
+ const unsettled = [];
569
+ for (const session of this.sessions.values()) {
570
+ if (session.close_receipt) {
571
+ const observed = await this.dispatch("session.inspect", { sessionId: session.provider_session_id });
572
+ if (observed.settled !== true) unsettled.push(session.provider_session_id);
573
+ continue;
574
+ }
575
+ const options = this.options({ sessionId: session.provider_session_id, adapterSessionId: session.adapter_session_id });
576
+ let observed;
577
+ // A daemon-owned allocation with no productive dispatch cannot have
578
+ // children, and native ZCode has no durable read index for it yet. Close
579
+ // that resident directly; do not send cancellation for default stop.
580
+ const emptyRoot = !cancel && !this.activeProductive ? this.neverPromptedRootProof(session.provider_session_id) : null;
581
+ if (emptyRoot) {
582
+ const close = await this.bridge.request("zcode/session/close", { sessionId: session.adapter_session_id });
583
+ const observation = close?.closed === true
584
+ ? await observeClosedZcodeTree(this.bridge, session.provider_session_id, [], { ...emptyRoot, close_confirmed: true })
585
+ : { settled: false, topology: { root_session_id: session.provider_session_id, child_session_ids: [], complete: false } };
586
+ this.track({ provider_session_id: session.provider_session_id, adapter_session_id: session.adapter_session_id, cancellation_requested: false, close_requested: true, close, ...observation });
587
+ await this.persist({ active_tree: observation.settled !== true });
588
+ if (observation.settled !== true) unsettled.push(session.provider_session_id);
589
+ continue;
590
+ }
591
+ if (!cancel) observed = await inspectSessionWithBridge(this.bridge, options);
592
+ if (cancel || !observed || this.treeRunning(observed)) {
593
+ if (!cancel) unsettled.push(session.provider_session_id);
594
+ else {
595
+ let cancelled;
596
+ try { cancelled = await cancelSessionWithBridge(this.bridge, options); }
597
+ catch (error) { unsettled.push(session.provider_session_id); continue; }
598
+ this.track(cancelled, cancelled.after);
599
+ await this.persist({ active_tree: cancelled.settled !== true });
600
+ if (cancelled.close?.closed === true) {
601
+ if (!cancelled.settled) unsettled.push(session.provider_session_id);
602
+ continue;
603
+ }
604
+ // A cooperative cancel needs a final read. A close receipt is
605
+ // terminal itself: native session/read correctly rejects it as no
606
+ // longer resident, so treating that rejection as unsettled would
607
+ // turn a successful close into an immortal daemon.
608
+ if (!cancelled.settled) {
609
+ try { observed = await inspectSessionWithBridge(this.bridge, options); }
610
+ catch (error) { unsettled.push(session.provider_session_id); continue; }
611
+ if (this.treeRunning(observed)) unsettled.push(session.provider_session_id);
612
+ }
613
+ }
614
+ }
615
+ }
616
+ if (unsettled.length) throw new DaemonError("tree_not_settled", "daemon still owns running Sessions", false, { sessions: unsettled });
617
+ commitDaemonShutdown(this.paths.dir);
618
+ await this.persist({ native_stop_proof: { daemon_id: this.state.daemon_id, sessions: [...this.sessions.keys()], observed_at: new Date().toISOString() } });
619
+ }
620
+ commitDaemonShutdown(this.paths.dir);
621
+ // A cooperative close can make the native Session non-resident while its
622
+ // original ACP prompt is still pending. For an explicitly whole-RUN
623
+ // cancellation this daemon owns the bridge process group, so retire that
624
+ // group now rather than reporting a clean-looking but still live tree.
625
+ // A per-child cancellation never takes this path.
626
+ let bridgeClosed = false;
627
+ if (this.activeProductive && cancel) {
628
+ try {
629
+ if (typeof this.bridge.close === "function") await this.bridge.close();
630
+ bridgeClosed = true;
631
+ } catch (error) {
632
+ throw new DaemonError(error.code ?? "provider_process_stop_incomplete", error.message ?? String(error), false, error.details);
633
+ }
634
+ }
635
+ const deadline = Date.now() + 5000;
636
+ while (this.activeProductive && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 50));
637
+ if (this.activeProductive) throw new DaemonError("tree_not_settled", "productive operation did not settle after cancellation");
638
+ // The owned bridge process group is the physical stop barrier. A native
639
+ // closed/resident receipt only proves API state and cannot seal recovery.
640
+ // A bridge without a close method occurs only in in-memory diagnostics: it
641
+ // owns no OS process boundary. Production AcpBridge always has one.
642
+ try { if (!bridgeClosed && typeof this.bridge.close === "function") await this.bridge.close(); }
643
+ catch (error) { throw new DaemonError(error.code ?? "provider_process_stop_incomplete", error.message ?? String(error), false, error.details); }
644
+ await this.persist({ active_tree: false, shutdown_state: "stopping", active_operation: null, active_request: null, physical_settlement: { confirmed_at: new Date().toISOString(), bridge_pid: this.bridge.child?.pid ?? null } });
645
+ return { daemon_id: this.state.daemon_id, stopped: true, clean: true, physical_settlement: this.state.physical_settlement, _shutdown: true };
646
+ }
647
+
648
+ }
649
+
650
+ export function zcodeBridgeEnv(config, daemonId) {
651
+ return { ...config.env, DD_FLOW_DAEMON_ID: daemonId,
652
+ DD_FLOW_RUNTIME_OWNER: config.runtime_owner ? JSON.stringify(config.runtime_owner) : undefined };
653
+ }
654
+
655
+ export async function serveDaemon(stateDir) {
656
+ const paths = locations(stateDir);
657
+ const state = await readState(paths.dir);
658
+ if (!state || state.schema_id !== STATE_SCHEMA) throw new DaemonError("daemon_state_missing", "daemon state is missing or incompatible");
659
+ const bridge = new AcpBridge({ ...state.config, env: zcodeBridgeEnv(state.config, state.daemon_id) });
660
+ const initialized = await startManagedBridge({ config: state.config, state, bridge, kind: "zcode-provider", publish: snapshot => writeState(paths.state, snapshot) });
661
+ const runtime = new DaemonRuntime(paths, state, bridge, initialized);
662
+ scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
663
+ await removeSocket(paths.socket, paths.dir);
664
+ const server = net.createServer((connection) => {
665
+ connection.setEncoding("utf8"); let buffer = "", consumed = false;
666
+ connection.on("data", (chunk) => {
667
+ if (consumed) return;
668
+ buffer += chunk; if (!admitNativeRequestBuffer(buffer, connection, RESPONSE_SCHEMA)) { consumed = true; return; }
669
+ const newline = buffer.indexOf("\n");
670
+ if (newline < 0) return;
671
+ consumed = true;
672
+ const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1);
673
+ void (async () => {
674
+ let request;
675
+ try {
676
+ request = JSON.parse(line);
677
+ if (request.schema_id !== REQUEST_SCHEMA || !request.id || !request.operation) throw new DaemonError("daemon_protocol_mismatch", "invalid daemon request");
678
+ const result = await durableDaemonDispatch(paths.dir, request, () => runtime.dispatch(request.operation, request.params ?? {}, request.id), () => runtime.dispatch("session.inspect", request.params ?? {}));
679
+ if (result?._shutdown) retireDaemonEndpoint(server);
680
+ connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`);
681
+ } catch (error) {
682
+ await retireFailedDaemon(server, state, error, patch => runtime.persist(patch));
683
+ connection.end(`${JSON.stringify({ schema_id: RESPONSE_SCHEMA, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`);
684
+ }
685
+ })();
686
+ });
687
+ });
688
+ runtime.server = server;
689
+ trackDaemonConnections(server);
690
+ await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); });
691
+ await chmod(paths.socket, 0o600);
692
+ await runtime.persist({ shutdown_state: "running", active_tree: false });
693
+ let shuttingDown = false;
694
+ const onSignal = () => {
695
+ if (shuttingDown) return;
696
+ shuttingDown = true;
697
+ void runtime.shutdown().catch(async () => { await runtime.persist({ shutdown_state: "running" }); }).finally(() => process.exit(1));
698
+ };
699
+ process.on("SIGTERM", onSignal);
700
+ process.on("SIGINT", onSignal);
701
+ return await new Promise(() => {});
702
+ }
703
+ import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";