@deksden-com/dd-flow-cli 0.9.0-beta.9 → 0.9.0-beta.90

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 (143) hide show
  1. package/CHANGELOG.md +460 -0
  2. package/README.md +65 -0
  3. package/dist/build-info.json +8 -8
  4. package/dist/cli/command-inputs.js +304 -0
  5. package/dist/cli/help.js +89 -13
  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 +1409 -382
  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/command-context.js +80 -0
  63. package/dist/services/config.js +11 -8
  64. package/dist/services/continuation-outcome.js +20 -0
  65. package/dist/services/controller-fanout.js +132 -0
  66. package/dist/services/dashboard.js +103 -31
  67. package/dist/services/engines.js +121 -76
  68. package/dist/services/eval-snapshots.js +750 -73
  69. package/dist/services/execution-policy.js +170 -0
  70. package/dist/services/external-work-launch.js +110 -0
  71. package/dist/services/harness-adapter.js +147 -24
  72. package/dist/services/harness-config.js +2 -0
  73. package/dist/services/hooks.js +587 -299
  74. package/dist/services/lanes.js +61 -53
  75. package/dist/services/lifecycle-command.js +118 -7
  76. package/dist/services/lifecycle-invocations.js +1055 -0
  77. package/dist/services/managed-daemon-binding.js +38 -0
  78. package/dist/services/managed-processes.js +283 -27
  79. package/dist/services/merge-queue.js +179 -104
  80. package/dist/services/merge-server.js +73 -28
  81. package/dist/services/migrations.js +14 -9
  82. package/dist/services/native-daemon-history.js +49 -0
  83. package/dist/services/native-session-control.js +39 -0
  84. package/dist/services/plan-runtime.js +19 -8
  85. package/dist/services/plans.js +28 -25
  86. package/dist/services/projects.js +18 -5
  87. package/dist/services/prompts.js +23 -16
  88. package/dist/services/protocols.js +64 -20
  89. package/dist/services/recovery-observation-budget.js +61 -0
  90. package/dist/services/recovery-snapshot-database.js +107 -0
  91. package/dist/services/repair-intents.js +40 -0
  92. package/dist/services/run-control-receipt.js +56 -0
  93. package/dist/services/run-control-worker.js +363 -0
  94. package/dist/services/run-control.js +876 -0
  95. package/dist/services/run-controller-adapter.js +222 -0
  96. package/dist/services/run-controller-capture.js +134 -0
  97. package/dist/services/run-controller-process.js +189 -0
  98. package/dist/services/run-controller-recovery.js +222 -0
  99. package/dist/services/run-controller-state.js +36 -0
  100. package/dist/services/run-controller.js +789 -0
  101. package/dist/services/run-engine-bindings.js +1 -1
  102. package/dist/services/run-fork.js +138 -0
  103. package/dist/services/run-observations.js +112 -0
  104. package/dist/services/run-recovery-runtime.js +69 -0
  105. package/dist/services/run-recovery.js +343 -0
  106. package/dist/services/runs.js +368 -74
  107. package/dist/services/runtime-budget.js +434 -0
  108. package/dist/services/runtime-command.js +43 -0
  109. package/dist/services/runtime-scope-capture.js +50 -0
  110. package/dist/services/runtime-scope-control.js +450 -0
  111. package/dist/services/runtime-scope-resume.js +565 -0
  112. package/dist/services/runtime-scope-stop.js +99 -0
  113. package/dist/services/runtime-scope-worker.js +243 -0
  114. package/dist/services/runtime-service.js +97 -0
  115. package/dist/services/schema-validation.js +10 -8
  116. package/dist/services/sessions.js +35 -69
  117. package/dist/services/stage-blocker.js +17 -7
  118. package/dist/services/stage-context.js +44 -18
  119. package/dist/services/stage-lifecycle.js +99 -102
  120. package/dist/services/stage-pause.js +84 -55
  121. package/dist/services/stage-work-graph.js +35 -0
  122. package/dist/services/usage.js +37 -93
  123. package/dist/services/vnext-code-review.js +195 -88
  124. package/dist/services/vnext-code.js +285 -252
  125. package/dist/services/vnext-execution-profile.js +5 -3
  126. package/dist/services/vnext-fanout.js +160 -17
  127. package/dist/services/vnext-merge.js +291 -104
  128. package/dist/services/vnext-plan-review.js +139 -81
  129. package/dist/services/vnext-plan.js +138 -69
  130. package/dist/services/vnext-protocolize.js +129 -56
  131. package/dist/services/vnext-specify.js +88 -65
  132. package/dist/services/work-registry.js +716 -168
  133. package/dist/services/workspace-bootstrap.js +76 -0
  134. package/dist/services/worktrees.js +85 -35
  135. package/dist/shared/entity-references.js +8 -0
  136. package/dist/shared/errors.js +12 -0
  137. package/dist/storage/database.js +459 -32
  138. package/dist/storage/paths.js +17 -4
  139. package/dist/storage/work-references.js +23 -0
  140. package/dist/storage/writer-contract.js +77 -0
  141. package/dist/storage/writer-migration.js +102 -0
  142. package/package.json +24 -13
  143. package/tools/repair-paused-run-status.mjs +59 -0
@@ -0,0 +1,910 @@
1
+ import { observedTimeout } from "./observation-clock.mjs";
2
+ import { ToolObservations, acpToolObservation, zcodePhysicalSession } from "./tool-observations.mjs";
3
+ import { invokeNativeHook } from "./native-hook-command.mjs";
4
+ import { spawn } from "node:child_process";
5
+ import { createHash } from "node:crypto";
6
+ import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
7
+ import path from "node:path";
8
+ import readline from "node:readline";
9
+ import { stopProcessGroup } from "./managed-daemon.mjs";
10
+ import { checkObservedProfile, observeModel, observedFields } from "./model-observations.mjs";
11
+
12
+ const ZCODE_HARNESS_CONTRACT = "dd-zcode-harness@1";
13
+ // A deadline is an observation boundary, not permission to replay an ACP
14
+ // request. Keep the original correlation slot briefly so a slow native
15
+ // response (especially cold-start/session creation) can still settle it.
16
+ const LATE_RESPONSE_GRACE_MS = 5_000;
17
+
18
+ function fail(message, code) { const error = new Error(message); if (code) error.code = code; throw error; }
19
+ function fatalLifecycleError(error) {
20
+ return ["invocation_unknown", "invocation_identity_mismatch", "invocation_identity_conflict", "invocation_receipt_missing", "invocation_command_mismatch", "invocation_scope_mismatch", "invocation_generation_stale", "invocation_expired", "zcode_parent_identity_missing", "zcode_tool_identity_conflict"].includes(error?.code);
21
+ }
22
+ function absolute(value, label) { if (!value || !path.isAbsolute(value)) fail(`${label} must be an absolute path`); return path.resolve(value); }
23
+ function text(value, label) { if (typeof value !== "string" || !value.trim()) fail(`${label} is required`); return value.trim(); }
24
+ function now() { return new Date().toISOString(); }
25
+ function responseSummary(message, method = null) {
26
+ const result = message?.result;
27
+ return { request_id: message?.id ?? null, method, has_result: result !== undefined,
28
+ result_keys: result && typeof result === "object" && !Array.isArray(result) ? Object.keys(result).slice(0, 32) : [], error: message?.error ?? null };
29
+ }
30
+
31
+ function executable(command, args = []) {
32
+ const resolved = command ?? process.env.DD_ZCODE_ACP_BIN ?? "zcode-acp";
33
+ return /\.[cm]?js$/.test(resolved) ? { command: process.execPath, args: [resolved, ...args] } : { command: resolved, args };
34
+ }
35
+
36
+ async function commandOutput(command, args) {
37
+ const target = executable(command, args);
38
+ return await new Promise((resolve, reject) => {
39
+ const child = spawn(target.command, target.args, { stdio: ["ignore", "pipe", "pipe"] });
40
+ let stdout = ""; let stderr = "";
41
+ child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk));
42
+ child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
43
+ child.on("error", reject);
44
+ child.on("close", (code) => code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `${target.command} exited ${code}`)));
45
+ });
46
+ }
47
+
48
+ async function runWithStdin(command, args, stdin, env = {}) {
49
+ const target = executable(command, args);
50
+ return await new Promise((resolve, reject) => {
51
+ const child = spawn(target.command, target.args, { env: { ...process.env, ...env }, stdio: ["pipe", "pipe", "pipe"] });
52
+ let stdout = ""; let stderr = "";
53
+ child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk));
54
+ child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
55
+ child.on("error", reject);
56
+ child.on("close", (code) => code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `${target.command} exited ${code}`)));
57
+ child.stdin.end(stdin);
58
+ });
59
+ }
60
+
61
+ class Journal {
62
+ constructor(file) {
63
+ this.file = file ? absolute(file, "--journal") : null;
64
+ this.pending = this.file ? mkdir(path.dirname(this.file), { recursive: true }) : Promise.resolve();
65
+ this.order = 0;
66
+ this.artifacts = new Set();
67
+ }
68
+ write(kind, payload) {
69
+ if (!this.file) return;
70
+ const line = JSON.stringify({ order: ++this.order, observed_at: now(), kind, payload });
71
+ this.pending = this.pending.then(() => appendFile(this.file, `${line}\n`));
72
+ return this.pending;
73
+ }
74
+ writeInbound(payload, method) {
75
+ if (!this.file || method !== "zcode/session/read" || !payload?.result || typeof payload.result !== "object") return this.write("inbound", payload);
76
+ const raw = JSON.stringify(payload.result);
77
+ const sha256 = createHash("sha256").update(raw).digest("hex");
78
+ const artifactDir = `${this.file}.artifacts`, artifact = path.join(artifactDir, `${sha256}.json`);
79
+ const blobs = new Map();
80
+ const messages = Array.isArray(payload.result.messages) ? payload.result.messages.map(message => {
81
+ const bytes = Buffer.from(JSON.stringify(message));
82
+ const chunks = [];
83
+ for (let offset = 0; offset < bytes.length; offset += 65536) {
84
+ const chunk = bytes.subarray(offset, offset + 65536);
85
+ const hash = createHash("sha256").update(chunk).digest("hex");
86
+ const file = path.join(artifactDir, `${hash}.chunk`);
87
+ blobs.set(file, chunk); chunks.push({ path: path.basename(file), sha256: hash, bytes: chunk.length });
88
+ }
89
+ return { chunks };
90
+ }) : null;
91
+ const manifest = JSON.stringify({ encoding: "dd-zcode/session-read@1", result: { ...payload.result, ...(messages ? { messages } : {}) } });
92
+ const result = { ...payload.result };
93
+ if (Array.isArray(result.messages)) result.messages = { omitted_count: result.messages.length };
94
+ const compact = { ...payload, result, raw_result: { sha256, bytes: Buffer.byteLength(raw), path: artifact, encoding: "dd-zcode/session-read@1" } };
95
+ const line = JSON.stringify({ order: ++this.order, observed_at: now(), kind: "inbound", payload: compact });
96
+ this.pending = this.pending.then(async () => {
97
+ await mkdir(artifactDir, { recursive: true });
98
+ for (const [file, bytes] of [...blobs, [artifact, manifest]]) {
99
+ if (this.artifacts.has(file)) continue;
100
+ try { await writeFile(file, bytes, { flag: "wx" }); }
101
+ catch (error) {
102
+ if (error?.code !== "EEXIST") throw error;
103
+ if (!Buffer.from(await readFile(file)).equals(Buffer.from(bytes))) throw new Error(`Journal artifact conflict: ${file}`);
104
+ }
105
+ this.artifacts.add(file);
106
+ }
107
+ await appendFile(this.file, `${line}\n`);
108
+ });
109
+ return this.pending;
110
+ }
111
+ async flush() { await this.pending; }
112
+ }
113
+
114
+ /** Reconstruct a retained read, verifying every chunk and the original result. */
115
+ export async function readSessionJournalArtifact(reference) {
116
+ const stored = JSON.parse(await readFile(reference.path, "utf8"));
117
+ const result = reference.encoding === "dd-zcode/session-read@1" ? stored.result : stored;
118
+ if (reference.encoding === "dd-zcode/session-read@1" && Array.isArray(result.messages)) {
119
+ const messages = [];
120
+ for (const message of result.messages) {
121
+ const parts = [];
122
+ for (const chunk of message.chunks) {
123
+ if (chunk.path !== `${chunk.sha256}.chunk` || !/^[a-f0-9]{64}$/.test(chunk.sha256)) throw new Error("Invalid journal chunk reference");
124
+ const bytes = await readFile(path.join(path.dirname(reference.path), chunk.path));
125
+ if (bytes.length !== chunk.bytes || createHash("sha256").update(bytes).digest("hex") !== chunk.sha256) throw new Error("Journal chunk integrity mismatch");
126
+ parts.push(bytes);
127
+ }
128
+ messages.push(JSON.parse(Buffer.concat(parts).toString("utf8")));
129
+ }
130
+ result.messages = messages;
131
+ }
132
+ const raw = JSON.stringify(result);
133
+ if (Buffer.byteLength(raw) !== reference.bytes || createHash("sha256").update(raw).digest("hex") !== reference.sha256) throw new Error("Journal result integrity mismatch");
134
+ return result;
135
+ }
136
+
137
+ export class AcpBridge {
138
+ constructor(options) {
139
+ this.options = options;
140
+ this.nextId = 1;
141
+ this.pending = new Map();
142
+ this.allocationObservers = new Map();
143
+ this.lateResponses = new Map();
144
+ this.expiredRequests = new Map();
145
+ this.sessionActivity = new Map();
146
+ this.notifications = Promise.resolve();
147
+ this.lifecycleNotifications = new Set();
148
+ this.notificationError = null;
149
+ this.bridgeStderr = "";
150
+ this.toolObservations = new ToolObservations();
151
+ this.assistantText = new Map();
152
+ this.journal = new Journal(options.journal);
153
+ this.modelSessions = new Map(); this.modelProfiles = new Map();
154
+ }
155
+
156
+ configure(options) { this.options = { ...this.options, ...options }; }
157
+ registerModelSession(adapterId, sessionId, requested, parentSessionId = null, armed = true) {
158
+ this.modelSessions.set(adapterId, { sessionId, requested, parentSessionId, armed });
159
+ }
160
+ async recordModel(adapterId, observed, source) {
161
+ const identity = this.modelSessions.get(adapterId); if (!identity) return;
162
+ this.modelProfiles.set(identity.sessionId, observed);
163
+ await observeModel({ journal: this.options.journal, harness: this.options.clientInfo?.name === "dd-grok" ? "grok-acp" : "zcode-acp",
164
+ sessionId: identity.sessionId, parentSessionId: identity.parentSessionId, requested: identity.requested, observed, source });
165
+ }
166
+ async observeModelUpdate(message) {
167
+ const update = message.params?.update;
168
+ if (update?.sessionUpdate !== "config_option_update" || !Array.isArray(update.configOptions)) return;
169
+ const value = category => update.configOptions.find(option => option.type === "select" && (option.category === category || option.id === category))?.currentValue ?? null;
170
+ const observed = { model: value("model"), reasoning: value("thought_level"), mode: value("mode") };
171
+ const identity = this.modelSessions.get(message.params.sessionId); if (!identity?.armed) return;
172
+ checkObservedProfile(identity.requested, observed);
173
+ await this.recordModel(message.params.sessionId, observed, "acp.config_option_update");
174
+ }
175
+
176
+ async start() {
177
+ const target = executable(this.options.bin, this.options.commandArgs ?? ["server"]);
178
+ const cwd = this.options.cwd ? absolute(this.options.cwd, "--cwd") : process.cwd();
179
+ this.child = spawn(target.command, target.args, { cwd, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...(this.options.env ?? {}) } });
180
+ this.child.stderr.setEncoding("utf8").on("data", (chunk) => { this.bridgeStderr += String(chunk); this.journal.write("bridge_stderr", { text: String(chunk) }); });
181
+ this.child.stdin?.on("error", (error) => {
182
+ error.details = { system_code: error.code ?? null };
183
+ error.code = "bridge_pipe_broken";
184
+ this.rejectAll(error);
185
+ });
186
+ this.child.on("error", (error) => { error.code ??= "bridge_exited"; this.rejectAll(error); });
187
+ this.child.on("exit", (code) => { const error = new Error(`zcode-acp exited ${code}${this.bridgeStderr.trim() ? `: ${this.bridgeStderr.trim()}` : ""}`); error.code = "bridge_exited"; this.rejectAll(error); });
188
+ readline.createInterface({ input: this.child.stdout }).on("line", (line) => this.receive(line));
189
+ return await this.request("initialize", {
190
+ protocolVersion: 1,
191
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, elicitation: { form: {} } },
192
+ clientInfo: this.options.clientInfo ?? { name: "dd-zcode", version: "0.1.0" },
193
+ });
194
+ }
195
+
196
+ receive(line) {
197
+ let message;
198
+ try { message = JSON.parse(line); } catch { this.journal.write("malformed", { line }); return; }
199
+ const request = message.id !== undefined ? this.pending.get(message.id) : null;
200
+ const expired = message.id !== undefined ? this.expiredRequests.get(message.id) : null;
201
+ const isResponse = message.id !== undefined && !message.method;
202
+ if (!isResponse || request) this.journal.writeInbound(message, request?.method);
203
+ if (typeof message.params?.sessionId === "string" && isProductiveSessionNotification(message)) this.sessionActivity.set(message.params.sessionId, Date.now());
204
+ const update = message.params?.update;
205
+ // ZCode emits this as `session/update`; Grok Build wraps the same ACP
206
+ // payload in `_x.ai/session_notification`. Preserve either provider's
207
+ // native error before its generic JSON-RPC terminal response arrives.
208
+ if (typeof message.params?.sessionId === "string" && update?.sessionUpdate === "retry_state" && update.type === "failed") {
209
+ for (const pending of this.pending.values()) {
210
+ if (pending.method === "session/prompt" && pending.sessionId === message.params.sessionId) pending.providerError = update;
211
+ }
212
+ }
213
+ if (message.method === "session/update" && message.params?.update?.sessionUpdate === "agent_message_chunk" && message.params.update.content?.type === "text") {
214
+ const sessionId = message.params.sessionId;
215
+ this.assistantText.set(sessionId, `${this.assistantText.get(sessionId) ?? ""}${message.params.update.content.text ?? ""}`);
216
+ }
217
+ if (message.method === "zcode/session/allocated") {
218
+ const identity = message.params;
219
+ const observer = this.allocationObservers.get(identity?.adapter_session_id);
220
+ if (observer && typeof identity?.provider_session_id === "string" && identity.provider_session_id.length > 0) {
221
+ this.allocationObservers.delete(identity.adapter_session_id);
222
+ this.notifications = this.notifications.then(() => observer(identity));
223
+ void this.notifications.catch(error => { this.notificationError ??= error; });
224
+ }
225
+ }
226
+ this.observeToolCall(message);
227
+ if (message.method === "session/update" && this.options.onLifecycleNotification) {
228
+ const observer = this.options.onLifecycleNotification;
229
+ const pending = Promise.resolve().then(() => observer(message)).catch(async error => {
230
+ // A validated no-effect rejection is returned through the invocation
231
+ // ledger. It is evidence, not a poisoned ACP Session.
232
+ const fatal = fatalLifecycleError(error);
233
+ if (!this.notificationError || fatal) this.notificationError = error;
234
+ // A missing/foreign authority is not a recoverable CLI validation
235
+ // result. Stop admitting productive requests immediately; the managed
236
+ // controller still owns physical stop/drain and evidence capture.
237
+ if (fatal) this.rejectAll(error);
238
+ try {
239
+ await this.journal.write("notification_failure", { code: error.code ?? "invocation_observation_failed", message: error.message,
240
+ session_id: message.params?.sessionId ?? null, tool_call_id: update?.toolCallId ?? null, event_time: now(), delivery_stage: "lifecycle_receipt", completeness: "incomplete" });
241
+ } catch (persistenceError) {
242
+ process.stderr.write(`${JSON.stringify({ code: error.code, message: error.message, persistence_error: persistenceError.message })}\n`);
243
+ }
244
+ });
245
+ this.lifecycleNotifications.add(pending);
246
+ void pending.then(() => this.lifecycleNotifications.delete(pending), () => this.lifecycleNotifications.delete(pending));
247
+ }
248
+ if (message.id !== undefined && message.method) { void this.answer(message).catch((error) => this.rejectAll(error)); return; }
249
+ if (message.id !== undefined) {
250
+ const pending = request ?? this.pending.get(message.id);
251
+ if (!pending) {
252
+ this.lateResponses.set(message.id, message);
253
+ while (this.lateResponses.size > 128) this.lateResponses.delete(this.lateResponses.keys().next().value);
254
+ this.journal.write("late_response", responseSummary(message, expired?.method ?? null));
255
+ return;
256
+ }
257
+ clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); this.pending.delete(message.id);
258
+ if (pending.timedOut) this.journal.write("late_response", { ...responseSummary(message, pending.method), grace_ms: LATE_RESPONSE_GRACE_MS });
259
+ if (message.error) {
260
+ const data = message.error.data && typeof message.error.data === "object" && !Array.isArray(message.error.data) ? message.error.data : null;
261
+ const details = data?.details ?? data?.message ?? data ?? null;
262
+ const provider = pending.providerError;
263
+ const providerDetails = provider?.message ?? null;
264
+ const messageText = [message.error.message, details, providerDetails].filter(Boolean).map(value => typeof value === "string" ? value : JSON.stringify(value)).join(": ") || JSON.stringify(message.error);
265
+ const error = new Error(messageText);
266
+ const nativeCode = typeof data?.code === "string" ? data.code : null;
267
+ const unknown = ["native_timeout", "native_backend_dead", "native_backend_pipe_broken", "native_outcome_unknown"].includes(nativeCode);
268
+ error.code = unknown ? nativeCode : /rate limit/i.test(messageText) ? "provider_rate_limited" : /payment required|usage balance exhausted/i.test(messageText) ? "provider_quota_exhausted" : "acp_request_failed";
269
+ error.retryable = !unknown && /rate limit/i.test(messageText);
270
+ error.details = { acp_code: message.error.code ?? null, ...(data ? { native: data } : {}), ...(details ? { provider_details: details } : {}), ...(providerDetails ? { provider_update: provider } : {}) };
271
+ pending.reject(error);
272
+ }
273
+ else { pending.resolve(message.result ?? {}); }
274
+ return;
275
+ }
276
+ if (message.method) this.notifications = this.notifications
277
+ .then(async () => { await this.observeModelUpdate(message); await this.options.onNotification?.(message); })
278
+ .catch(async (error) => {
279
+ const failure = { code: error.code ?? "notification_observation_failed", message: error.message, session_id: message.params?.sessionId ?? null, tool_call_id: message.params?.update?.toolCallId ?? null, event_time: now(), delivery_stage: "acp_evidence", completeness: "incomplete" };
280
+ try { await this.journal.write("notification_failure", failure); }
281
+ catch (persistenceError) { process.stderr.write(`${JSON.stringify({ ...failure, persistence_error: persistenceError.message })}\n`); }
282
+ // Evidence loss does not retroactively revoke a committed native hook.
283
+ // Permission/profile integrity is a separate, still-fatal contract.
284
+ if (error.code === "profile_integrity_violation") { this.notificationError ??= error; this.rejectAll(error); }
285
+ });
286
+ }
287
+
288
+ observeToolCall(message) {
289
+ const rootSessionId = this.modelSessions.get(message.params?.sessionId)?.sessionId;
290
+ this.toolObservations.add(acpToolObservation(message, { harness: this.options.observationHarness ?? "zcode-acp", rootSessionId }));
291
+ }
292
+
293
+ toolSummary(sessionId) {
294
+ return this.toolObservations.summary(this.modelSessions.get(sessionId)?.sessionId ?? sessionId);
295
+ }
296
+ toolCursor() { return this.toolObservations.cursor(); }
297
+ assistantTextCursor(sessionId) { return (this.assistantText.get(sessionId) ?? "").length; }
298
+ assistantTextSince(sessionId, cursor) { return (this.assistantText.get(sessionId) ?? "").slice(cursor); }
299
+ toolSummarySince(cursor, sessionId) {
300
+ return this.toolObservations.summary(this.modelSessions.get(sessionId)?.sessionId ?? sessionId, cursor);
301
+ }
302
+
303
+ async answer(message) {
304
+ await this.notifications;
305
+ let result;
306
+ if (message.method === "session/request_permission") {
307
+ const options = message.params?.options ?? [];
308
+ const selected = this.options.permission === "allow" && !this.notificationError
309
+ ? options.find((item) => item.optionId === "allow_once") ?? options[0]
310
+ : options.find((item) => item.optionId === "deny");
311
+ result = selected ? { outcome: { outcome: "selected", optionId: selected.optionId } } : { outcome: { outcome: "cancelled" } };
312
+ } else if (message.method === "elicitation/create") {
313
+ result = this.options.answers ? { action: "accept", content: this.options.answers } : { action: "decline", reason: "dd-zcode has no declared answers" };
314
+ } else {
315
+ this.send({ jsonrpc: "2.0", id: message.id, error: { code: -32601, message: `unsupported client request: ${message.method}` } });
316
+ return;
317
+ }
318
+ this.journal.write("interaction", { method: message.method, params: message.params, result });
319
+ this.send({ jsonrpc: "2.0", id: message.id, result });
320
+ }
321
+
322
+ request(method, params = {}, timeoutMs = 30_000, { idleSessionId = null, idleTimeoutMs = null, inactivityMode = "reject" } = {}) {
323
+ if ((this.notificationError?.code === "profile_integrity_violation" || fatalLifecycleError(this.notificationError)) && ["session/new", "session/prompt", "session/fork"].includes(method)) return Promise.reject(this.notificationError);
324
+ if (this.inactivityError && ["session/new", "session/prompt", "session/fork"].includes(method)) return Promise.reject(this.inactivityError);
325
+ const id = this.nextId++;
326
+ return new Promise((resolve, reject) => {
327
+ const finish = (error) => {
328
+ const pending = this.pending.get(id); if (!pending) return;
329
+ clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); this.pending.delete(id); reject(error);
330
+ };
331
+ // Productive Turns use the sliding session-activity timeout below. A
332
+ // second wall-clock timeout would kill an active, long-running Turn.
333
+ const timer = Number.isFinite(timeoutMs) && timeoutMs > 0
334
+ ? observedTimeout(() => {
335
+ const pending = this.pending.get(id);
336
+ if (!pending || pending.timedOut) return;
337
+ pending.timedOut = true;
338
+ const timeoutError = Object.assign(new Error(`${method} timed out`), {
339
+ code: "rpc_timeout",
340
+ details: { method, request_id: id, session_id: params.sessionId ?? null, timeout_ms: timeoutMs, grace_ms: LATE_RESPONSE_GRACE_MS },
341
+ });
342
+ this.journal.write("request_deadline", timeoutError.details);
343
+ pending.timer = observedTimeout(() => {
344
+ this.expiredRequests.set(id, { method, sessionId: params.sessionId ?? null });
345
+ while (this.expiredRequests.size > 128) this.expiredRequests.delete(this.expiredRequests.keys().next().value);
346
+ finish(timeoutError);
347
+ }, LATE_RESPONSE_GRACE_MS);
348
+ }, timeoutMs)
349
+ : null;
350
+ const validIdleTimeout = Number.isFinite(idleTimeoutMs) && idleTimeoutMs > 0 ? idleTimeoutMs : null;
351
+ if (idleSessionId) this.sessionActivity.set(idleSessionId, Date.now());
352
+ let lastTimerAt = Date.now();
353
+ let resumedAt = null;
354
+ let inactivityRecorded = false;
355
+ const checkIdle = async () => {
356
+ if (!this.pending.has(id)) return;
357
+ const observedAt = Date.now();
358
+ const observationGapMs = observedAt - lastTimerAt;
359
+ lastTimerAt = observedAt;
360
+ // A host pause makes wall time jump while this process cannot observe
361
+ // native progress. Keep the SAME native request alive so its late
362
+ // result can still be persisted. Grant one fresh observation window;
363
+ // do not rewrite provider activity or resend the request.
364
+ if (observationGapMs > 60_000) {
365
+ resumedAt = observedAt;
366
+ this.journal.write("observation_gap", { method, request_id: id, session_id: idleSessionId, observation_gap_ms: observationGapMs, observed_at: new Date(observedAt).toISOString() });
367
+ return;
368
+ }
369
+ const lastActivity = this.sessionActivity.get(idleSessionId) ?? Date.now();
370
+ if (observedAt - Math.max(lastActivity, resumedAt ?? lastActivity) < validIdleTimeout || inactivityRecorded) return;
371
+ // ACP silence is diagnostic only. A native child can be computing while
372
+ // its Session is non-resident and therefore unsubscribable.
373
+ inactivityRecorded = true;
374
+ this.journal.write("inactivity_detected", { method, request_id: id, session_id: idleSessionId, last_activity_at: new Date(lastActivity).toISOString(), idle_timeout_ms: validIdleTimeout, observed_at: new Date(observedAt).toISOString() });
375
+ if (inactivityMode !== "diagnostic") {
376
+ finish(Object.assign(new Error(`${method} received no session update for ${validIdleTimeout}ms`), { code: "subject_liveness_timeout", details: { session_id: idleSessionId, last_activity_at: new Date(lastActivity).toISOString(), idle_timeout_ms: validIdleTimeout } }));
377
+ return;
378
+ }
379
+ await this.options.onInactivity?.({ method, requestId: id, sessionId: idleSessionId, lastActivityAt: lastActivity, idleTimeoutMs: validIdleTimeout });
380
+ };
381
+ const idleTimer = validIdleTimeout ? setInterval(() => {
382
+ void checkIdle().catch((error) => {
383
+ // A timer callback is outside the request promise. Preserve the
384
+ // failure for the owning daemon/flush path instead of creating an
385
+ // unhandled rejection that loses the primary request evidence.
386
+ this.inactivityError ??= error;
387
+ void this.journal.write("inactivity_observation_failure", { method, request_id: id, session_id: idleSessionId, error: { code: error?.code ?? "inactivity_observation_failed", message: error?.message ?? String(error) } }).catch(() => {});
388
+ });
389
+ }, Math.max(10, Math.min(1_000, Math.floor(validIdleTimeout / 4)))) : null;
390
+ this.pending.set(id, { resolve, reject, timer, idleTimer, method, sessionId: params.sessionId ?? null });
391
+ this.send({ jsonrpc: "2.0", id, method, params });
392
+ });
393
+ }
394
+
395
+ notify(method, params = {}) { this.send({ jsonrpc: "2.0", method, params }); }
396
+ send(message) {
397
+ this.journal.write("outbound", message);
398
+ const stdin = this.child?.stdin;
399
+ if (!stdin || stdin.destroyed) {
400
+ const error = Object.assign(new Error("zcode-acp stdin is closed"), { code: "bridge_pipe_broken" });
401
+ this.rejectAll(error);
402
+ throw error;
403
+ }
404
+ try { stdin.write(`${JSON.stringify(message)}\n`); }
405
+ catch (error) { error.details = { system_code: error.code ?? null }; error.code = "bridge_pipe_broken"; this.rejectAll(error); throw error; }
406
+ }
407
+ rejectAll(error) { for (const pending of this.pending.values()) { clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); pending.reject(error); } this.pending.clear(); }
408
+ async flush() {
409
+ try { await Promise.all([...this.lifecycleNotifications]); await this.notifications; await this.journal.flush(); }
410
+ catch (error) { throw this.notificationError ?? error; }
411
+ if (this.notificationError) throw this.notificationError;
412
+ if (this.inactivityError) throw this.inactivityError;
413
+ }
414
+ async close() {
415
+ // Productive notification failures belong to flush()/the original Turn.
416
+ // Draining them must not relabel a successful physical shutdown as failed.
417
+ await Promise.allSettled([...this.lifecycleNotifications, this.notifications]);
418
+ if (this.child) {
419
+ this.child.stdin.end();
420
+ await Promise.race([new Promise((resolve) => this.child.once("exit", resolve)), new Promise((resolve) => setTimeout(resolve, 2000))]);
421
+ await stopProcessGroup(this.child, 2_000);
422
+ }
423
+ await this.journal.flush();
424
+ }
425
+ }
426
+
427
+ function isProductiveSessionNotification(message) {
428
+ if (!["session/update", "_x.ai/session_notification"].includes(message.method)) return false;
429
+ const update = message.params?.update;
430
+ // Transport state and the bridge's cosmetic thinking hint are not native
431
+ // progress. The latter is identifiable without leaking a ZCode-specific
432
+ // synthetic flag through the ACP wire contract.
433
+ if (update?.sessionUpdate === "heartbeat" || update?.type === "heartbeat") return false;
434
+ if (update?.sessionUpdate === "agent_thought_chunk" && typeof update?.messageId === "string" && update.messageId.startsWith("thinking_")) return false;
435
+ if ((update?.sessionUpdate === "status" || update?.type === "status") && update?.status === "running") return false;
436
+ return true;
437
+ }
438
+
439
+ export function observedProfile(read) {
440
+ const model = read?.settings?.model?.current ?? {};
441
+ return {
442
+ provider: typeof model.providerId === "string" ? model.providerId : null,
443
+ model: typeof model.modelId === "string" ? model.modelId : null,
444
+ reasoning: read?.settings?.thoughtLevel?.current ?? null,
445
+ mode: read?.settings?.mode?.current ?? read?.projection?.mode ?? null,
446
+ };
447
+ }
448
+
449
+ export function assertProfile(requested, observed) { return checkObservedProfile(requested, observed); }
450
+
451
+ async function resume(bridge, sessionId, cwd) {
452
+ await bridge.request("session/resume", { sessionId: text(sessionId, "--session-id"), cwd: absolute(cwd, "--cwd"), mcpServers: [] });
453
+ return sessionId;
454
+ }
455
+
456
+ async function inspect(bridge, sessionId) {
457
+ const [read, subagents] = await Promise.all([
458
+ bridge.request("zcode/session/read", { sessionId }),
459
+ bridge.request("zcode/session/subagents", { sessionId }),
460
+ ]);
461
+ let usage = null, usage_observation_error = null;
462
+ try { usage = await bridge.request("zcode/session/usage", { sessionId }); }
463
+ catch (error) { usage_observation_error = { code: error.code ?? "usage_observation_failed", message: error.message ?? String(error) }; }
464
+ const observed = observedProfile(read);
465
+ await bridge.recordModel?.(sessionId, observed, "zcode/session/read");
466
+ return { read, subagents, usage, ...(usage_observation_error ? { usage_observation_error } : {}), observed_profile: observed };
467
+ }
468
+
469
+ // The journal is the immutable transcript. CLI receipts only need the
470
+ // operational projection; returning every message makes a routine status
471
+ // command enormous and obscures the useful facts.
472
+ function publicEvidence(evidence) {
473
+ return {
474
+ ...evidence,
475
+ read: {
476
+ projection: evidence.read?.projection ?? null,
477
+ session: evidence.read?.session ?? null,
478
+ settings: evidence.read?.settings ?? null,
479
+ },
480
+ };
481
+ }
482
+
483
+ // ACP returns the terminal turn separately from its visible transcript. The
484
+ // probe contract needs the literal terminal answer, so extract only text parts
485
+ // from the last assistant message rather than asking a model to interpret it.
486
+ export function latestAssistantText(read) {
487
+ const messages = Array.isArray(read?.messages) ? read.messages : [];
488
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
489
+ const message = messages[index];
490
+ if (message?.info?.role !== "assistant") continue;
491
+ const text = (Array.isArray(message.parts) ? message.parts : [])
492
+ .filter((part) => part?.type === "text" && typeof part.text === "string")
493
+ .map((part) => part.text)
494
+ .join("")
495
+ .trim();
496
+ if (text) return text;
497
+ }
498
+ return null;
499
+ }
500
+
501
+ export function zcodeChildSessionIds(snapshot) {
502
+ return [...new Set([
503
+ ...(Array.isArray(snapshot?.childSessionIds) ? snapshot.childSessionIds : []),
504
+ ...(Array.isArray(snapshot?.running) ? snapshot.running : []).map(child => child?.childSessionId),
505
+ ...(Array.isArray(snapshot?.ended?.items) ? snapshot.ended.items : []).map(child => child?.childSessionId),
506
+ ].filter(id => typeof id === "string" && id))];
507
+ }
508
+
509
+ // Read residency BEFORE each durable topology snapshot. An inactive node cannot
510
+ // add descendants; a resident node never proves settlement, even when idle.
511
+ // No resolve/read/subagents extension here: those may implicitly resume.
512
+ export async function observeClosedZcodeTree(bridge, root, retainedChildren = []) {
513
+ const pending = new Set([root, ...retainedChildren]), residents = [], snapshots = [];
514
+ const deadline = performance.now() + 30_000;
515
+ try {
516
+ for (const sessionId of pending) {
517
+ if (pending.size > 1024 || performance.now() >= deadline) throw new Error("Closed tree observation limit exceeded");
518
+ const timeout = () => Math.max(1, Math.min(5000, deadline - performance.now()));
519
+ const resident = await bridge.request("zcode/session/resident", { sessionId }, timeout());
520
+ if (resident?.sessionId !== sessionId || typeof resident.resident !== "boolean") throw new Error("Invalid residency identity or state");
521
+ residents.push(resident);
522
+ const retained = await bridge.request("zcode/session/retainedSubagents", { sessionId }, timeout());
523
+ const tree = retained?.topology;
524
+ if (retained?.sessionId !== sessionId || !Array.isArray(tree?.childSessionIds)
525
+ || tree.childSessionIds.some(id => typeof id !== "string" || !id || id === sessionId)
526
+ || !Array.isArray(tree?.running) || !Array.isArray(tree?.ended?.items)
527
+ || !Number.isSafeInteger(tree.ended.total) || tree.ended.total < 0
528
+ || tree.ended.total > tree.childSessionIds.length
529
+ || [...tree.running, ...tree.ended.items].some(child => !tree.childSessionIds.includes(child?.childSessionId))) throw new Error("Incomplete retained native topology");
530
+ snapshots.push({ sessionId, ...tree });
531
+ for (const child of tree.childSessionIds) pending.add(child);
532
+ }
533
+ return { topology: { root_session_id: root, child_session_ids: [...pending].filter(id => id !== root), complete: true }, residents, snapshots, settled: residents.every(item => item.resident === false) };
534
+ } catch (error) {
535
+ return { topology: { root_session_id: root, child_session_ids: [...pending].filter(id => id !== root), complete: false }, residents, snapshots, settled: false, observation_error: { code: error.code ?? "topology_observation_failed", message: error.message } };
536
+ }
537
+ }
538
+
539
+ async function cancelTree(bridge, sessionId, before, providerSessionId, retainedChildren = []) {
540
+ const knownChildren = new Set(retainedChildren);
541
+ const remember = snapshot => {
542
+ for (const id of zcodeChildSessionIds(snapshot)) knownChildren.add(id);
543
+ };
544
+ remember(before);
545
+ const cancellations = [];
546
+ for (const child of before.running ?? []) {
547
+ const taskId = child.taskId ?? child.agentId;
548
+ if (taskId) {
549
+ try { cancellations.push(await bridge.request("session/cancelBackgroundTask", { sessionId, taskId })); }
550
+ catch (error) { cancellations.push({ taskId, cancelled: false, error: { code: error.code ?? "cancel_failed", message: error.message } }); }
551
+ }
552
+ }
553
+ // ZCode's stop is deliberately idempotent. A stop acknowledgement is not a
554
+ // terminal receipt: native turns (and a root without children) can remain
555
+ // active after it. Observe a bounded drain, then close this owned Session.
556
+ bridge.notify("session/cancel", { sessionId });
557
+ let after = before, read = null, root_status = null;
558
+ const deadline = Date.now() + 6000;
559
+ for (let attempt = 0; attempt < 6 && Date.now() < deadline; attempt += 1) {
560
+ await new Promise((resolve) => setTimeout(resolve, 1000));
561
+ const timeout = Math.max(1, deadline - Date.now());
562
+ [after, read] = await Promise.all([
563
+ bridge.request("zcode/session/subagents", { sessionId }, timeout).catch(error => ({ observation_error: { code: error.code, message: error.message } })),
564
+ bridge.request("zcode/session/read", { sessionId }, timeout).catch(error => ({ observation_error: { code: error.code, message: error.message } })),
565
+ ]);
566
+ root_status = read?.projection?.status ?? read?.session?.status ?? null;
567
+ remember(after);
568
+ if (after.observation_error || read?.observation_error) break;
569
+ if (Array.isArray(after.running) && !after.running.length && !(read?.projection?.activeToolCalls ?? []).length && ["idle", "completed", "cancelled", "failed", "stopped"].includes(root_status)) {
570
+ // Idle is only a turn-level observation. Close the owned resident before
571
+ // proving the transitive tree cannot create another child.
572
+ break;
573
+ }
574
+ }
575
+ let close = null;
576
+ try { close = await bridge.request("zcode/session/close", { sessionId }); }
577
+ catch (error) { close = { closed: false, error: { code: error.code ?? "close_failed", message: error.message } }; }
578
+ const observation = close?.closed === true ? await observeClosedZcodeTree(bridge, providerSessionId, [...knownChildren])
579
+ : { topology: { root_session_id: providerSessionId, child_session_ids: [...knownChildren], complete: false }, settled: false };
580
+ return { cancellations, before, after, root_status, cancellation_requested: true, close_requested: true, close, ...observation };
581
+ }
582
+
583
+ // Cancel one known child without cancelling its parent turn. The parent keeps
584
+ // its ACP listener, so later child lifecycle events still reach dd-flow.
585
+ export async function cancelChildWithBridge(bridge, options) {
586
+ requireJournal(options);
587
+ const identity = await controlledIdentity(bridge, options);
588
+ const childSessionId = text(options.childSessionId, "--child-session-id");
589
+ const before = await bridge.request("zcode/session/subagents", { sessionId: identity.adapterSessionId });
590
+ const child = (before.running ?? []).find((item) => item.childSessionId === childSessionId);
591
+ if (!child) fail(`ZCode has no running child ${childSessionId}`, "child_not_running");
592
+ const taskId = child.taskId ?? child.agentId;
593
+ if (!taskId) fail(`ZCode child ${childSessionId} has no cancellation identifier`, "child_cancel_unsupported");
594
+ const cancellation = await bridge.request("session/cancelBackgroundTask", { sessionId: identity.adapterSessionId, taskId });
595
+ let after = before;
596
+ for (let attempt = 0; attempt < 6; attempt += 1) {
597
+ await new Promise((resolve) => setTimeout(resolve, 1000));
598
+ after = await bridge.request("zcode/session/subagents", { sessionId: identity.adapterSessionId });
599
+ if (!(after.running ?? []).some((item) => item.childSessionId === childSessionId)) break;
600
+ }
601
+ if ((after.running ?? []).some((item) => item.childSessionId === childSessionId)) {
602
+ fail(`ZCode child ${childSessionId} did not stop`, "partial_cancellation");
603
+ }
604
+ return receipt(options, {
605
+ harness: "zcode-acp",
606
+ provider_session_id: identity.providerSessionId,
607
+ adapter_session_id: identity.adapterSessionId,
608
+ child_session_id: childSessionId,
609
+ cancellation,
610
+ before,
611
+ after,
612
+ });
613
+ }
614
+
615
+ export async function cancelChild(options) {
616
+ return await withBridge(options, async (bridge) => await cancelChildWithBridge(bridge, options));
617
+ }
618
+
619
+ async function controlledIdentity(bridge, options) {
620
+ const providerSessionId = text(options.sessionId, "--session-id");
621
+ const adapterSessionId = options.adapterSessionId ?? providerSessionId;
622
+ if (!options.liveSession) await resume(bridge, adapterSessionId, options.cwd);
623
+ const resolved = await bridge.request("zcode/session/resolve", { sessionId: adapterSessionId });
624
+ if (resolved.providerSessionId !== providerSessionId) fail(`ZCode Session identity mismatch: expected ${providerSessionId}, observed ${resolved.providerSessionId}`, "session_identity_mismatch");
625
+ bridge.registerModelSession?.(adapterSessionId, providerSessionId, profile(options));
626
+ return { providerSessionId, adapterSessionId };
627
+ }
628
+
629
+ async function applyProfile(bridge, sessionId, requested, assertDispatch = () => {}) {
630
+ assertDispatch();
631
+ if (requested.mode) await bridge.request("session/set_mode", { sessionId, modeId: requested.mode });
632
+ assertDispatch();
633
+ if (requested.reasoning) await bridge.request("session/setThoughtLevel", { sessionId, thoughtLevel: requested.reasoning });
634
+ assertDispatch();
635
+ if (requested.model) await bridge.request("session/setModel", { sessionId, modelId: requested.provider ? `${requested.provider}\\${requested.model}` : requested.model });
636
+ assertDispatch();
637
+ const read = await bridge.request("zcode/session/read", { sessionId });
638
+ const observed = observedProfile(read);
639
+ await bridge.recordModel?.(sessionId, observed, "zcode/session/read.applied");
640
+ const receipt = assertProfile(requested, observed);
641
+ const identity = bridge.modelSessions?.get(sessionId); if (identity) identity.armed = true;
642
+ return receipt;
643
+ }
644
+
645
+ function profile(options) { return { provider: options.provider ?? null, model: options.model ?? null, reasoning: options.reasoning ?? null, mode: options.mode ?? null }; }
646
+
647
+ function requiredProfile(options) {
648
+ const requested = profile(options);
649
+ for (const key of ["provider", "model", "reasoning", "mode"]) text(requested[key], `--${key}`);
650
+ return requested;
651
+ }
652
+
653
+ function requireJournal(options) { absolute(options.journal, "--journal"); }
654
+
655
+ function receipt(options, value) { return options.daemonId ? { daemon_id: options.daemonId, ...value } : value; }
656
+
657
+ async function withBridge(options, operation) {
658
+ const bridge = new AcpBridge(options);
659
+ try { const initialized = await bridge.start(); const result = await operation(bridge, initialized); await bridge.flush(); return result; }
660
+ finally { await bridge.close(); }
661
+ }
662
+
663
+ function flowForwarder(options, rootProviderSessionId, bridge) {
664
+ return async (notification) => {
665
+ if (notification.method !== "session/update") return;
666
+ const update = notification.params?.update;
667
+ const command = update?.sessionUpdate === "tool_call" ? update.rawInput?.command ?? update.rawInput?.cmd : null;
668
+ if (typeof command !== "string" || !command.includes("dd-flow")) return;
669
+ const childId = notification.params?.update?._meta?.zcodeRuntime?.childSessionId;
670
+ const observed = bridge?.modelProfiles?.get(childId ?? rootProviderSessionId) ?? {};
671
+ if (childId && !observed.model) await observeModel({ journal: options.journal, harness: "zcode-acp", sessionId: childId, parentSessionId: rootProviderSessionId, requested: profile(options), observed, source: "zcode.child_tool_event", evidence: "unavailable" });
672
+ // Native PreToolUse has already admitted (or denied) the command. ACP is
673
+ // intentionally only journal/model evidence: its serialized delivery can
674
+ // lag behind execution and must never mint a second lifecycle receipt.
675
+ };
676
+ }
677
+
678
+ /** Preserve native tool ownership independently of slow secondary evidence.
679
+ * Rehydration uses this same session's retained inbound journal, not model text. */
680
+ export async function zcodeInvocationObserver(options, rootProviderSessionId, send = invokeNativeHook) {
681
+ if (!options.ddFlowBin) return async () => {};
682
+ const owners = new Map();
683
+ const remember = message => {
684
+ const update = message.params?.update;
685
+ if (message.method !== "session/update" || update?.sessionUpdate !== "tool_call") return null;
686
+ if (options.adapterSessionId && message.params?.sessionId !== options.adapterSessionId) return null;
687
+ const native = update._meta?.zcodeRuntime ?? {};
688
+ const session = zcodePhysicalSession(update, rootProviderSessionId);
689
+ if (!session) throw Object.assign(new Error("Native subagent event has no physical child identity"), { code: "zcode_child_identity_missing" });
690
+ const key = update.toolCallId;
691
+ if (!key || typeof session !== "string") return null;
692
+ if (owners.has(key) && owners.get(key) !== session) throw Object.assign(new Error("Native tool id has conflicting Session ownership"), { code: "zcode_tool_identity_conflict" });
693
+ owners.set(key, session);
694
+ return { update, native, session };
695
+ };
696
+ if (options.journal) {
697
+ try {
698
+ const contents = await readFile(options.journal, "utf8");
699
+ for (const line of contents.split("\n").filter(Boolean)) {
700
+ const event = JSON.parse(line);
701
+ if (event.kind === "inbound") remember(event.payload);
702
+ }
703
+ } catch (error) { if (error.code !== "ENOENT") throw error; }
704
+ }
705
+ return async message => {
706
+ const facts = remember(message);
707
+ if (!facts || !options.ddFlowBin) return;
708
+ const { update, native, session } = facts;
709
+ const command = update.rawInput?.command ?? update.rawInput?.cmd;
710
+ const toolName = update._meta?.claudeCode?.toolName;
711
+ if (!["Bash", "bash", "execute"].includes(toolName) || typeof command !== "string" || !/(?:^|\s)--invocation-id\s/.test(command)) return;
712
+ const parent = session === rootProviderSessionId ? null : owners.get(native.parentToolCallId);
713
+ if (session !== rootProviderSessionId && !parent) throw Object.assign(new Error("Native child has no confirmed immediate parent tool owner"), { code: "zcode_parent_identity_missing" });
714
+ await send({ bin: options.ddFlowBin, home: options.ddFlowHome,
715
+ args: ["zcode", "event", "handle", "--project-root", options.projectRoot ?? options.cwd, "--json"],
716
+ payload: { ...message, _meta: { ddZcode: { daemonId: options.daemonId, rootProviderSessionId, parentProviderSessionId: parent } } },
717
+ });
718
+ };
719
+ }
720
+
721
+ export function zcodeLifecycleEnvelope(notification, options, rootProviderSessionId) {
722
+ return { ...notification, _meta: { ddZcode: {
723
+ rootProviderSessionId,
724
+ requestedProfile: profile(options),
725
+ observedProfile: observedFields(options.observedProfile),
726
+ ...(options.daemonId ? { daemonId: options.daemonId } : {}),
727
+ } } };
728
+ }
729
+
730
+ async function forwardUsage(options, providerSessionId, usage, toolCalls) {
731
+ if (!options.ddFlowBin || !usage) return;
732
+ const projectRoot = absolute(options.projectRoot, "--project-root");
733
+ const payload = {
734
+ provider_session_id: providerSessionId,
735
+ daemon_id: options.daemonId ?? null,
736
+ observed_at: new Date().toISOString(),
737
+ usage,
738
+ // ZCode exposes counters for the addressed physical session only. State
739
+ // this explicitly so dd-flow never has to infer an aggregation model.
740
+ usage_scope: "physical_session",
741
+ completeness: usage?.usageIsIncomplete ? "partial" : "complete",
742
+ ...(toolCalls ? { tool_calls: toolCalls } : {}),
743
+ };
744
+ await runWithStdin(options.ddFlowBin, ["zcode", "usage", "ingest", "--project-root", projectRoot, "--json"], `${JSON.stringify(payload)}\n`, options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home") } : {});
745
+ }
746
+
747
+ async function forwardUsageBestEffort(options, providerSessionId, usage, toolCalls) {
748
+ try { await forwardUsage(options, providerSessionId, usage, toolCalls); return null; }
749
+ catch (error) { return { code: error.code ?? "usage_ingest_failed", message: error.message ?? String(error) }; }
750
+ }
751
+
752
+ export function zcodeLifecycleQualification(sha256, bridgeCommit, harnessContract) {
753
+ // Behavioural evidence: dd-eval production-invocations probe, 2026-09-13.
754
+ // Pin both providers of native event identity, not a string in the bundle.
755
+ // 60af0d3 adds retainedSubagents without changing native event forwarding.
756
+ const qualified = sha256 === "e9f1868c0fdb863537ed910ee3828b9be96b8c2fd805473f63b439e1113266b8"
757
+ && ["e0600fe3c46e215695f257e65aa6f674ced998d8", "60af0d31e13076a313d9770f10aa70f7c94742cf"].includes(bridgeCommit) && harnessContract === ZCODE_HARNESS_CONTRACT;
758
+ return { status: qualified ? "qualified" : "unqualified", mechanism: "cli-invocation@1", sha256,
759
+ capabilities: qualified ? ["root", "concurrent_children", "child_continuation"] : [],
760
+ native_nested_children: false, reason: qualified ? null : "native_event_contract_unqualified" };
761
+ }
762
+
763
+ export async function doctor(options = {}) {
764
+ const zcodeAcp = await commandOutput(options.bin, ["--version"]);
765
+ const zcodeAcpCommit = await commandOutput(options.bin, ["--dd-harness-commit"]);
766
+ const ddHarness = await commandOutput(options.bin, ["--dd-harness-version"]);
767
+ const zcodePath = options.zcodePath ?? process.env.ZCODE_PATH ?? "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs";
768
+ const zcode = await commandOutput(zcodePath, ["--version"]);
769
+ const native = await readFile(zcodePath);
770
+ const sha256 = createHash("sha256").update(native).digest("hex");
771
+ const qualification = zcodeLifecycleQualification(sha256, zcodeAcpCommit, ddHarness);
772
+ return { compatible: qualification.status === "qualified", lifecycle_qualification: qualification, observed_runtime: { zcode, zcode_acp: zcodeAcp, zcode_acp_commit: zcodeAcpCommit, dd_harness_contract: ddHarness } };
773
+ }
774
+
775
+ export async function createSession(options) {
776
+ requireJournal(options);
777
+ return await withBridge(options, async (bridge, initialized) => await createSessionWithBridge(bridge, options, initialized));
778
+ }
779
+
780
+ export async function createSessionWithBridge(bridge, options, initialized) {
781
+ requireJournal(options);
782
+ const cwd = absolute(options.cwd, "--cwd"); const requested = requiredProfile(options);
783
+ let forward;
784
+ bridge.configure({ ...options, onNotification: async (event) => { await forward?.(event); await options.onNotification?.(event); } });
785
+ const created = await bridge.request("session/new", { cwd, mcpServers: [] });
786
+ // The ACP alias is the first durable identity available. Persist it before
787
+ // resolving the native Session so a later resolve timeout cannot erase the
788
+ // fact that allocation was already admitted.
789
+ await options.onSessionAliasAllocated?.({ adapter_session_id: created.sessionId, cwd });
790
+ bridge.allocationObservers?.set(created.sessionId, async identity => {
791
+ if (identity.cwd !== cwd) fail("ZCode allocation workspace mismatch", "session_identity_mismatch");
792
+ await options.onSessionAllocated?.(identity);
793
+ });
794
+ const resolved = await bridge.request("zcode/session/resolve", { sessionId: created.sessionId });
795
+ const providerSessionId = resolved.providerSessionId;
796
+ // Expose both identities before profile/usage bookkeeping. The owning
797
+ // daemon can persist this allocation even when a later step fails.
798
+ await options.onSessionAllocated?.({
799
+ adapter_session_id: created.sessionId,
800
+ provider_session_id: providerSessionId,
801
+ cwd,
802
+ });
803
+ bridge.registerModelSession?.(created.sessionId, providerSessionId, requested, null, false);
804
+ const profile_receipt = await applyProfile(bridge, created.sessionId, requested);
805
+ forward = flowForwarder(options, providerSessionId, bridge);
806
+ let usage = null, usage_observation_error = null;
807
+ try { usage = await bridge.request("zcode/session/usage", { sessionId: created.sessionId }); }
808
+ catch (error) { usage_observation_error = { code: error.code ?? "usage_observation_failed", message: error.message ?? String(error) }; }
809
+ const usage_ingest_error = await forwardUsageBestEffort(options, providerSessionId, usage, bridge.toolSummary(created.sessionId));
810
+ await bridge.flush();
811
+ // ZCode materializes `subagents` only after its first prompt. Session
812
+ // creation is intentionally nonproductive, so querying that topology here
813
+ // would both fail on real ZCode and turn the common create contract into an
814
+ // implicit first turn. The first prompt/inspect records full evidence.
815
+ const evidence = { read: null, subagents: { running: [], completed: [] }, usage, ...(usage_observation_error ? { usage_observation_error } : {}), ...(usage_ingest_error ? { usage_ingest_error } : {}), observed_profile: profile_receipt.observed, tool_calls: bridge.toolSummary(created.sessionId) };
816
+ return receipt(options, { harness: "zcode-acp", runtime_family: "zcode", adapter_session_id: created.sessionId, provider_session_id: providerSessionId, cwd, initialized, profile: profile_receipt, evidence: publicEvidence(evidence) });
817
+ }
818
+
819
+ export async function promptSession(options) {
820
+ requireJournal(options);
821
+ return await withBridge(options, async (bridge) => await promptSessionWithBridge(bridge, options));
822
+ }
823
+
824
+ export async function promptSessionWithBridge(bridge, options) {
825
+ requireJournal(options);
826
+ const cwd = absolute(options.cwd, "--cwd"); const prompt = text(options.prompt, "prompt"); const requested = requiredProfile(options);
827
+ let forward;
828
+ bridge.configure({ ...options, onNotification: async (event) => { await forward?.(event); await options.onNotification?.(event); } });
829
+ const identity = await controlledIdentity(bridge, { ...options, cwd }); forward = flowForwarder(options, identity.providerSessionId, bridge);
830
+ await bridge.journal.flush();
831
+ bridge.configure({ onLifecycleNotification: await zcodeInvocationObserver({ ...options, adapterSessionId: identity.adapterSessionId }, identity.providerSessionId) });
832
+ const profile_receipt = await applyProfile(bridge, identity.adapterSessionId, requested, options.assertDispatch);
833
+ options.assertDispatch?.();
834
+ let initial_usage_error = null;
835
+ try { initial_usage_error = await forwardUsageBestEffort(options, identity.providerSessionId, await bridge.request("zcode/session/usage", { sessionId: identity.adapterSessionId }), bridge.toolSummary(identity.adapterSessionId)); }
836
+ catch (error) { initial_usage_error = { code: error.code ?? "usage_observation_failed", message: error.message ?? String(error) }; }
837
+ // This is the request budget. ACP silence is observed separately and must
838
+ // never turn a long native child request into a fabricated provider failure.
839
+ const timeoutMs = options.timeoutMs === undefined ? 1_800_000 : options.timeoutMs;
840
+ const textCursor = bridge.assistantTextCursor(identity.adapterSessionId);
841
+ options.assertDispatch?.();
842
+ const turn = await bridge.request("session/prompt", { sessionId: identity.adapterSessionId, prompt: [{ type: "text", text: prompt }] }, timeoutMs, {
843
+ idleSessionId: identity.adapterSessionId,
844
+ idleTimeoutMs: options.livenessTimeoutMs ?? null,
845
+ inactivityMode: "diagnostic",
846
+ });
847
+ const stopReason = typeof turn?.stopReason === "string" ? turn.stopReason : null;
848
+ if (stopReason && stopReason !== "end_turn") {
849
+ // A close can settle the pending prompt before its terminal event. Do not
850
+ // collect live evidence first: ordinary inspection is allowed to recover
851
+ // an evicted Session, which would undo the just-confirmed close.
852
+ await bridge.flush();
853
+ const error = new Error(`ZCode ended the provider request with ${stopReason}, not end_turn`);
854
+ error.code = stopReason === "cancelled" ? "zcode_turn_cancelled" : "zcode_turn_not_completed";
855
+ error.details = { stop_reason: stopReason, provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId };
856
+ throw error;
857
+ }
858
+ await bridge.flush();
859
+ const evidence = await inspect(bridge, identity.adapterSessionId);
860
+ evidence.tool_calls = bridge.toolSummary(identity.adapterSessionId);
861
+ const usage_ingest_error = await forwardUsageBestEffort(options, identity.providerSessionId, evidence.usage, evidence.tool_calls);
862
+ if (initial_usage_error) evidence.initial_usage_error = initial_usage_error;
863
+ if (usage_ingest_error) evidence.usage_ingest_error = usage_ingest_error;
864
+ if (!options.allowBackground && (evidence.subagents.running ?? []).length) {
865
+ await cancelTree(bridge, identity.adapterSessionId, evidence.subagents, identity.providerSessionId);
866
+ fail("background ZCode subagents cannot outlive the one-shot dd-zcode connection; the live child tree was cancelled");
867
+ }
868
+ return receipt(options, { harness: "zcode-acp", provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, assistant_text: bridge.assistantTextSince(identity.adapterSessionId, textCursor), turn, profile: profile_receipt, evidence: publicEvidence(evidence) });
869
+ }
870
+
871
+ export async function inspectSession(options) {
872
+ requireJournal(options);
873
+ return await withBridge(options, async (bridge) => await inspectSessionWithBridge(bridge, options));
874
+ }
875
+
876
+ export async function inspectSessionWithBridge(bridge, options) {
877
+ requireJournal(options);
878
+ const identity = await controlledIdentity(bridge, options);
879
+ return receipt(options, { harness: "zcode-acp", provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, ...publicEvidence(await inspect(bridge, identity.adapterSessionId)) });
880
+ }
881
+
882
+ export async function cancelSession(options) {
883
+ requireJournal(options);
884
+ return await withBridge(options, async (bridge) => await cancelSessionWithBridge(bridge, options));
885
+ }
886
+
887
+ export async function cancelSessionWithBridge(bridge, options) {
888
+ requireJournal(options);
889
+ const identity = await controlledIdentity(bridge, options);
890
+ const before = await bridge.request("zcode/session/subagents", { sessionId: identity.adapterSessionId }).catch(error => ({ observation_error: { code: error.code, message: error.message } }));
891
+ return receipt(options, { harness: "zcode-acp", provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, ...await cancelTree(bridge, identity.adapterSessionId, before, identity.providerSessionId, options.knownChildSessionIds) });
892
+ }
893
+
894
+ export async function forkSession(options) {
895
+ requireJournal(options);
896
+ return await withBridge(options, async (bridge) => await forkSessionWithBridge(bridge, options));
897
+ }
898
+
899
+ export async function forkSessionWithBridge(bridge, options) {
900
+ requireJournal(options);
901
+ if (!options.target || typeof options.target !== "object") fail("explicit --target-json is required");
902
+ const identity = await controlledIdentity(bridge, options);
903
+ const topology = await bridge.request("zcode/session/subagents", { sessionId: identity.adapterSessionId });
904
+ if ((topology.running ?? []).length) fail("cannot fork while background children are running");
905
+ const result = await bridge.request("session/fork", { sessionId: identity.adapterSessionId, target: options.target });
906
+ if (!result.forkedSessionId) fail("ZCode fork returned no provider Session ID");
907
+ return receipt(options, { harness: "zcode-acp", parent_provider_session_id: identity.providerSessionId, parent_adapter_session_id: identity.adapterSessionId, provider_session_id: result.forkedSessionId, adapter_session_id: result.forkedSessionId, target: options.target });
908
+ }
909
+
910
+ export async function promptFromFile(file) { return await readFile(absolute(file, "--prompt-file"), "utf8"); }