@deksden-com/dd-flow-cli 0.9.0-beta.11 → 0.9.0-beta.111

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