@deksden-com/dd-flow-cli 0.9.0-beta.123 → 0.9.0-beta.127

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +33 -1
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/argument-values.js +38 -0
  5. package/dist/cli/command-inputs.js +4 -3
  6. package/dist/cli/help.js +1 -0
  7. package/dist/cli/hook-ingress.js +1 -1
  8. package/dist/cli/run-cli.js +126 -193
  9. package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
  10. package/dist/cli/runtime-maintenance-ingress.js +51 -0
  11. package/dist/cli/runtime-process.js +128 -0
  12. package/dist/cli.js +8 -5
  13. package/dist/domain/stage-catalog.js +1 -1
  14. package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  17. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  18. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  19. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  20. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  21. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  22. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  23. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  24. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  25. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  26. package/dist/harness-runtime/lib/daemon-operations.mjs +159 -15
  27. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +215 -86
  28. package/dist/harness-runtime/lib/dd-agy.mjs +11 -13
  29. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +47 -23
  30. package/dist/harness-runtime/lib/dd-codex.mjs +257 -99
  31. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +32 -30
  32. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  33. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +96 -38
  34. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  35. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +54 -20
  36. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  37. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +81 -76
  38. package/dist/harness-runtime/lib/dd-zcode.mjs +204 -116
  39. package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
  40. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  41. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  42. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  43. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  44. package/dist/harness-runtime/lib/managed-daemon.d.mts +11 -0
  45. package/dist/harness-runtime/lib/managed-daemon.mjs +321 -65
  46. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  47. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  48. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  49. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  50. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  51. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  52. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  53. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  54. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  55. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  56. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  57. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  58. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  59. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  60. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  61. package/dist/runtime/context.js +8 -4
  62. package/dist/schemas/flow-run.schema.json +1 -0
  63. package/dist/schemas/plan-review-decision.schema.json +3 -3
  64. package/dist/schemas/run-control-receipt.schema.json +19 -0
  65. package/dist/services/code-checks.js +93 -27
  66. package/dist/services/codex-capacity.js +251 -74
  67. package/dist/services/codex-hook-delivery.js +2 -2
  68. package/dist/services/compatibility-preflight.js +4 -1
  69. package/dist/services/controller-fanout.js +9 -75
  70. package/dist/services/engines.js +70 -20
  71. package/dist/services/eval-snapshots.js +179 -40
  72. package/dist/services/external-work-launch.js +21 -9
  73. package/dist/services/harness-adapter.js +195 -25
  74. package/dist/services/hooks.js +65 -21
  75. package/dist/services/lifecycle-invocations.js +404 -32
  76. package/dist/services/managed-lease-monitor.js +128 -0
  77. package/dist/services/managed-processes.js +117 -53
  78. package/dist/services/merge-server.js +66 -23
  79. package/dist/services/recovery-observation-budget.js +67 -7
  80. package/dist/services/recovery-snapshot-database.js +7 -4
  81. package/dist/services/review-deferral.js +23 -0
  82. package/dist/services/run-control-receipt.js +1 -1
  83. package/dist/services/run-control-worker.js +177 -118
  84. package/dist/services/run-control.js +99 -8
  85. package/dist/services/run-controller-adapter.js +177 -51
  86. package/dist/services/run-controller-capture.js +14 -2
  87. package/dist/services/run-controller-process.js +11 -4
  88. package/dist/services/run-controller-recovery.js +17 -0
  89. package/dist/services/run-controller-state.js +68 -7
  90. package/dist/services/run-controller.js +207 -107
  91. package/dist/services/run-fork.js +8 -3
  92. package/dist/services/run-recovery-guard.js +75 -0
  93. package/dist/services/run-recovery-runtime.js +21 -30
  94. package/dist/services/run-recovery.js +17 -13
  95. package/dist/services/runs.js +40 -7
  96. package/dist/services/runtime-budget.js +32 -15
  97. package/dist/services/runtime-scope-control.js +8 -0
  98. package/dist/services/runtime-scope-resume.js +30 -8
  99. package/dist/services/runtime-scope-worker.js +118 -73
  100. package/dist/services/runtime-service.js +6 -7
  101. package/dist/services/scope-writer-cleanup.js +3 -2
  102. package/dist/services/stage-context.js +135 -0
  103. package/dist/services/stage-lifecycle.js +3 -15
  104. package/dist/services/stage-pause.js +3 -2
  105. package/dist/services/usage.js +14 -3
  106. package/dist/services/verification-matrix.js +30 -1
  107. package/dist/services/vnext-code-review.js +26 -13
  108. package/dist/services/vnext-code.js +10 -8
  109. package/dist/services/vnext-contracts.js +10 -1
  110. package/dist/services/vnext-execution-profile.js +75 -2
  111. package/dist/services/vnext-fanout.js +5 -0
  112. package/dist/services/vnext-merge.js +30 -11
  113. package/dist/services/vnext-plan-review.js +102 -35
  114. package/dist/services/vnext-plan.js +9 -7
  115. package/dist/services/vnext-protocolize.js +15 -9
  116. package/dist/services/vnext-specify.js +6 -6
  117. package/dist/services/work-registry.js +43 -6
  118. package/dist/services/workspace-bootstrap.js +14 -3
  119. package/dist/shared/errors.js +11 -6
  120. package/dist/storage/agent-output.js +26 -0
  121. package/dist/storage/database.js +80 -21
  122. package/package.json +7 -4
@@ -1,14 +1,17 @@
1
- import { observedTimeout } from "./observation-clock.mjs";
1
+ import { ObservationClock, observedTimeout, validateObservationDuration } from "./observation-clock.mjs";
2
2
  import { ToolObservations, acpToolObservation, zcodePhysicalSession } from "./tool-observations.mjs";
3
- import { invokeNativeHook } from "./native-hook-command.mjs";
4
- import { parseLifecycleCommand, continuationCommand } from "./lifecycle-command.mjs";
3
+ import { hookBudget, invokeNativeHook } from "./native-hook-command.mjs";
4
+ import { parseLifecycleCommand, continuationCommand, commandOption } from "./lifecycle-command.mjs";
5
5
  import { spawn } from "node:child_process";
6
6
  import { createHash } from "node:crypto";
7
7
  import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
8
8
  import path from "node:path";
9
- import readline from "node:readline";
10
- import { stopProcessGroup } from "./managed-daemon.mjs";
9
+ import { closeBridgeProcess } from "./managed-daemon.mjs";
10
+ import { errorRecord, isProvenShellCorrection } from "./operation-errors.mjs";
11
11
  import { checkObservedProfile, observeModel, observedFields } from "./model-observations.mjs";
12
+ import { commandText } from "./process-json.mjs";
13
+ import { readNativeLines, streamNativeDiagnostics, nativeObservationError } from "./native-streams.mjs";
14
+ import { operationProgressReporter } from "./operation-progress.mjs";
12
15
 
13
16
  import { readZcodeUsage } from "./zcode-usage.mjs";
14
17
  const ZCODE_HARNESS_CONTRACT = "dd-zcode-harness@1";
@@ -17,11 +20,11 @@ export const supportedZcodeHarnessContract = contract => [ZCODE_HARNESS_CONTRACT
17
20
  // request. Keep the original correlation slot briefly so a slow native
18
21
  // response (especially cold-start/session creation) can still settle it.
19
22
  const LATE_RESPONSE_GRACE_MS = 5_000;
23
+ // Only the owned lifecycle transport can attest a persisted CLI barrier.
24
+ // Model text and arbitrary callback error.details cannot grant correction.
25
+ const correctableLifecycleErrors = new WeakSet();
20
26
 
21
27
  function fail(message, code) { const error = new Error(message); if (code) error.code = code; throw error; }
22
- function fatalLifecycleError(error) {
23
- 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", "native_hook_timeout", "native_hook_failed", "native_hook_response_invalid", "hook_storage_unprepared", "storage_write_failed"].includes(error?.code);
24
- }
25
28
  function absolute(value, label) { if (!value || !path.isAbsolute(value)) fail(`${label} must be an absolute path`); return path.resolve(value); }
26
29
  function text(value, label) { if (typeof value !== "string" || !value.trim()) fail(`${label} is required`); return value.trim(); }
27
30
  function now() { return new Date().toISOString(); }
@@ -38,27 +41,12 @@ function executable(command, args = []) {
38
41
 
39
42
  async function commandOutput(command, args) {
40
43
  const target = executable(command, args);
41
- return await new Promise((resolve, reject) => {
42
- const child = spawn(target.command, target.args, { stdio: ["ignore", "pipe", "pipe"] });
43
- let stdout = ""; let stderr = "";
44
- child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk));
45
- child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
46
- child.on("error", reject);
47
- child.on("close", (code) => code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `${target.command} exited ${code}`)));
48
- });
44
+ return await commandText(target.command, target.args, { phase: "native-version" });
49
45
  }
50
46
 
51
47
  async function runWithStdin(command, args, stdin, env = {}) {
52
48
  const target = executable(command, args);
53
- return await new Promise((resolve, reject) => {
54
- const child = spawn(target.command, target.args, { env: { ...process.env, ...env }, stdio: ["pipe", "pipe", "pipe"] });
55
- let stdout = ""; let stderr = "";
56
- child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk));
57
- child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
58
- child.on("error", reject);
59
- child.on("close", (code) => code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `${target.command} exited ${code}`)));
60
- child.stdin.end(stdin);
61
- });
49
+ return await commandText(target.command, target.args, { input: stdin, env, phase: "usage-ingest" });
62
50
  }
63
51
 
64
52
  class Journal {
@@ -72,6 +60,7 @@ class Journal {
72
60
  if (!this.file) return;
73
61
  const line = JSON.stringify({ order: ++this.order, observed_at: now(), kind, payload });
74
62
  this.pending = this.pending.then(() => appendFile(this.file, `${line}\n`));
63
+ void this.pending.catch(() => {});
75
64
  return this.pending;
76
65
  }
77
66
  writeInbound(payload, method) {
@@ -109,6 +98,7 @@ class Journal {
109
98
  }
110
99
  await appendFile(this.file, `${line}\n`);
111
100
  });
101
+ void this.pending.catch(() => {});
112
102
  return this.pending;
113
103
  }
114
104
  async flush() { await this.pending; }
@@ -157,8 +147,11 @@ export class AcpBridge {
157
147
  }
158
148
 
159
149
  configure(options) { this.options = { ...this.options, ...options }; }
160
- registerModelSession(adapterId, sessionId, requested, parentSessionId = null, armed = true) {
161
- this.modelSessions.set(adapterId, { sessionId, requested, parentSessionId, armed });
150
+ registerModelSession(adapterId, sessionId, requested, parentSessionId = null, armed = true, bindCurrentRequest = false) {
151
+ const parentAdapter = parentSessionId ? [...this.modelSessions].find(([key, value]) => key === parentSessionId || value.sessionId === parentSessionId)?.[0] : null;
152
+ const parentRequest = this.activityOwner(parentAdapter);
153
+ const prior = this.modelSessions.get(adapterId);
154
+ this.modelSessions.set(adapterId, { sessionId, requested, parentSessionId, armed, parentRequestId: prior && !bindCurrentRequest ? prior.parentRequestId ?? null : parentRequest?.id ?? null });
162
155
  }
163
156
  async recordModel(adapterId, observed, source) {
164
157
  const identity = this.modelSessions.get(adapterId); if (!identity) return;
@@ -182,15 +175,15 @@ export class AcpBridge {
182
175
  const target = executable(this.options.bin, this.options.commandArgs ?? ["server"]);
183
176
  const cwd = this.options.cwd ? absolute(this.options.cwd, "--cwd") : process.cwd();
184
177
  this.child = spawn(target.command, target.args, { cwd, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...(this.options.env ?? {}) } });
185
- this.child.stderr.setEncoding("utf8").on("data", (chunk) => { this.bridgeStderr += String(chunk); this.journal.write("bridge_stderr", { text: String(chunk) }); });
178
+ this.diagnostics = streamNativeDiagnostics(this.child.stderr, async chunk => { this.bridgeStderr = (this.bridgeStderr + chunk).slice(-65_536); await this.journal.write("bridge_stderr", { text: chunk }); }, error => this.rejectAll(error));
186
179
  this.child.stdin?.on("error", (error) => {
187
180
  error.details = { system_code: error.code ?? null };
188
181
  error.code = "bridge_pipe_broken";
189
182
  this.rejectAll(error);
190
183
  });
191
184
  this.child.on("error", (error) => { error.code ??= "bridge_exited"; this.rejectAll(error); });
192
- 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); });
193
- readline.createInterface({ input: this.child.stdout }).on("line", (line) => this.receive(line));
185
+ this.child.on("close", code => { void Promise.all([this.nativeDrain, this.diagnostics]).then(() => { const error = new Error(`zcode-acp exited ${code}${this.bridgeStderr.trim() ? `: ${this.bridgeStderr.trim()}` : ""}`); error.code = "bridge_exited"; this.rejectAll(error); }); });
186
+ this.nativeDrain = readNativeLines(this.child.stdout, async line => { this.receive(line); await this.journal.flush(); }, error => this.rejectAll(error));
194
187
  if (this.options.onSpawn) await this.options.onSpawn(this.child);
195
188
  return await this.request("initialize", {
196
189
  protocolVersion: 1,
@@ -205,15 +198,23 @@ export class AcpBridge {
205
198
  const request = message.id !== undefined ? this.pending.get(message.id) : null;
206
199
  const expired = message.id !== undefined ? this.expiredRequests.get(message.id) : null;
207
200
  const isResponse = message.id !== undefined && !message.method;
201
+ const rejected = isResponse && this.rejectedHookRequests?.get(message.id);
202
+ if (rejected) {
203
+ this.rejectedHookRequests.delete(message.id);
204
+ const terminal = { request_id: message.id, method: rejected.method, session_id: rejected.sessionId, terminal_response: message };
205
+ rejected.error.details = { ...rejected.error.details, native_terminal_after_hook_failure: terminal };
206
+ try { void Promise.resolve(this.journal.write("native_terminal_after_hook_failure", terminal)).catch(storage => { rejected.error.cleanup_error ??= errorRecord(storage); }); }
207
+ catch (storage) { rejected.error.cleanup_error ??= errorRecord(storage); }
208
+ }
208
209
  if (!isResponse || request) this.journal.writeInbound(message, request?.method);
209
- if (typeof message.params?.sessionId === "string" && isProductiveSessionNotification(message)) this.sessionActivity.set(message.params.sessionId, Date.now());
210
+ if (typeof message.params?.sessionId === "string" && isProductiveSessionNotification(message)) this.observeActivity(message.params.sessionId, message.params.update);
210
211
  const update = message.params?.update;
211
212
  // ZCode emits this as `session/update`; Grok Build wraps the same ACP
212
213
  // payload in `_x.ai/session_notification`. Preserve either provider's
213
214
  // native error before its generic JSON-RPC terminal response arrives.
214
215
  if (typeof message.params?.sessionId === "string" && update?.sessionUpdate === "retry_state" && update.type === "failed") {
215
216
  for (const pending of this.pending.values()) {
216
- if (pending.method === "session/prompt" && pending.sessionId === message.params.sessionId) pending.providerError = update;
217
+ if (pending.method === "session/prompt" && pending.sessionId === message.params.sessionId && !pending.providerError) pending.providerError = { ...update, observed_at: now(), provider_session_id: pending.sessionId, source: message.method };
217
218
  }
218
219
  }
219
220
  if (message.method === "session/update" && message.params?.update?.sessionUpdate === "agent_message_chunk" && message.params.update.content?.type === "text") {
@@ -235,17 +236,21 @@ export class AcpBridge {
235
236
  const pending = Promise.resolve().then(() => observer(message)).catch(async error => {
236
237
  // A validated no-effect rejection is returned through the invocation
237
238
  // ledger. It is evidence, not a poisoned ACP Session.
238
- const fatal = fatalLifecycleError(error);
239
239
  const current = this.options.onLifecycleNotification === observer;
240
- if (current && (!this.notificationError || fatal && !fatalLifecycleError(this.notificationError) && this.notificationError.code !== "profile_integrity_violation")) this.notificationError = error;
240
+ const correctable = current && correctableLifecycleErrors.has(error);
241
+ if (current && !correctable) {
242
+ this.lifecycleFailure ??= this.notificationError?.code === "profile_integrity_violation" ? this.notificationError : error;
243
+ this.notificationError = this.lifecycleFailure;
244
+ }
241
245
  // A missing/foreign authority is not a recoverable CLI validation
242
246
  // result. Stop admitting productive requests immediately; the managed
243
247
  // controller still owns physical stop/drain and evidence capture.
244
- if (fatal && current) this.rejectAll(this.notificationError ?? error);
248
+ if (current && !correctable) this.rejectAll(this.notificationError);
245
249
  try {
246
- await this.journal.write("notification_failure", { code: error.code ?? "invocation_observation_failed", message: error.message, ...(error.details ? { details: error.details } : {}),
247
- session_id: message.params?.sessionId ?? null, tool_call_id: update?.toolCallId ?? null, event_time: now(), delivery_stage: "lifecycle_receipt", completeness: "incomplete" });
250
+ await hookBudget(1_000, null).run(() => this.journal.write("notification_failure", { ...errorRecord(error),
251
+ session_id: message.params?.sessionId ?? null, tool_call_id: update?.toolCallId ?? null, event_time: now(), delivery_stage: "lifecycle_receipt", completeness: "incomplete" }), "failure_persistence");
248
252
  } catch (persistenceError) {
253
+ if (current) this.notificationError ??= persistenceError;
249
254
  process.stderr.write(`${JSON.stringify({ code: error.code, message: error.message, persistence_error: persistenceError.message })}\n`);
250
255
  }
251
256
  });
@@ -275,6 +280,7 @@ export class AcpBridge {
275
280
  error.code = unknown ? nativeCode : /payment required|quota|usage balance exhausted|insufficient.?credit|billing/i.test(messageText) ? "provider_quota_exhausted" : /rate.?limit|too many requests/i.test(messageText) ? "provider_rate_limited" : /\b429\b/.test(messageText) ? "provider_limit_unknown" : "acp_request_failed";
276
281
  error.retryable = error.code === "provider_rate_limited";
277
282
  error.details = { acp_code: message.error.code ?? null,
283
+ observed_at: provider?.observed_at ?? now(), provider_session_id: pending.sessionId ?? null, source: "acp.error_response", request_id: message.id,
278
284
  terminal_response: { method: pending.method, session_id: pending.sessionId, request_id: message.id },
279
285
  ...(data ? { native: data } : {}), ...(details ? { provider_details: details } : {}), ...(providerDetails ? { provider_update: provider } : {}) };
280
286
  pending.reject(error);
@@ -299,6 +305,60 @@ export class AcpBridge {
299
305
  this.toolObservations.add(acpToolObservation(message, { harness: this.options.observationHarness ?? "zcode-acp", rootSessionId }));
300
306
  }
301
307
 
308
+ observeActivity(sessionId, update) {
309
+ const observedAt = Date.now(), seen = new Set();
310
+ let adapterId = sessionId;
311
+ // ACP has no Turn ID: scope direct notifications to the active request on
312
+ // this connection, and descendants to their parent's dispatch generation.
313
+ const owner = this.activityOwner(adapterId);
314
+ if (!owner) return;
315
+ if (["tool_call", "tool_call_update"].includes(update?.sessionUpdate)) {
316
+ const toolId = update.toolCallId;
317
+ if (!toolId) return;
318
+ owner.toolProgress ??= new Map();
319
+ const signature = JSON.stringify({ status: update.status, kind: update.kind, title: update.title, content: update.content, rawInput: update.rawInput, rawOutput: update.rawOutput });
320
+ const key = `${sessionId}:${toolId}`, previous = owner.toolProgress.get(key);
321
+ if (previous === signature) return;
322
+ // An empty update is only a replay/metadata snapshot, not tool work.
323
+ if (update.sessionUpdate === "tool_call_update" && !update.status && !update.content?.length && update.rawInput === undefined && update.rawOutput === undefined) return;
324
+ if (owner.toolProgress.size >= 1_024 && !owner.toolProgress.has(key)) return;
325
+ owner.toolProgress.set(key, signature);
326
+ }
327
+ while (adapterId && !seen.has(adapterId)) {
328
+ seen.add(adapterId);
329
+ const pending = [...this.pending.values()].find(value => value.method === "session/prompt" && value.sessionId === adapterId);
330
+ if (pending) {
331
+ pending.progress = { cursor: (pending.progress?.cursor ?? 0) + 1, observed_at: observedAt };
332
+ const rejectObservation = cause => {
333
+ if (this.pending.get(pending.id) !== pending) return;
334
+ clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer);
335
+ this.pending.delete(pending.id); pending.reject(nativeObservationError(cause));
336
+ };
337
+ void pending.publish("native_progress", { native_request_id: String(pending.id), session_id: adapterId, observed_at: new Date(observedAt).toISOString() }).catch(rejectObservation);
338
+ this.sessionActivity.set(adapterId, observedAt);
339
+ void Promise.resolve(this.options.onActivity?.({ sessionId: adapterId, requestId: pending.id, progress: pending.progress })).catch(rejectObservation);
340
+ }
341
+ const child = this.modelSessions.get(adapterId), parent = child?.parentSessionId;
342
+ const next = parent ? [...this.modelSessions].find(([key, value]) => key === parent || value.sessionId === parent)?.[0] : null;
343
+ const parentPending = this.activityOwner(next);
344
+ adapterId = parentPending?.id === child?.parentRequestId ? next : null;
345
+ }
346
+ }
347
+
348
+ activityOwner(sessionId) {
349
+ const path = [], seen = new Set();
350
+ while (sessionId && !seen.has(sessionId)) {
351
+ seen.add(sessionId);
352
+ const pending = [...this.pending.values()].find(value => value.method === "session/prompt" && value.sessionId === sessionId);
353
+ if (pending) return path.every(child => child.parentRequestId === pending.id) ? pending : null;
354
+ const identity = this.modelSessions.get(sessionId);
355
+ if (!identity?.parentSessionId) return null;
356
+ path.push(identity);
357
+ sessionId = [...this.modelSessions].find(([key, value]) => key === identity.parentSessionId || value.sessionId === identity.parentSessionId)?.[0];
358
+ }
359
+ return null;
360
+ }
361
+
302
362
  toolSummary(sessionId) {
303
363
  return this.toolObservations.summary(this.modelSessions.get(sessionId)?.sessionId ?? sessionId);
304
364
  }
@@ -329,7 +389,9 @@ export class AcpBridge {
329
389
  }
330
390
 
331
391
  request(method, params = {}, timeoutMs = 30_000, { idleSessionId = null, idleTimeoutMs = null, inactivityMode = "reject" } = {}) {
332
- if ((this.notificationError?.code === "profile_integrity_violation" || fatalLifecycleError(this.notificationError)) && ["session/new", "session/prompt", "session/fork"].includes(method)) return Promise.reject(this.notificationError);
392
+ if (timeoutMs !== null) validateObservationDuration(timeoutMs);
393
+ if (idleTimeoutMs !== null) validateObservationDuration(idleTimeoutMs);
394
+ if ((this.lifecycleFailure || this.notificationError?.code === "profile_integrity_violation") && ["session/new", "session/prompt", "session/fork"].includes(method)) return Promise.reject(this.lifecycleFailure ?? this.notificationError);
333
395
  if (this.inactivityError && ["session/new", "session/prompt", "session/fork"].includes(method)) return Promise.reject(this.inactivityError);
334
396
  const id = this.nextId++;
335
397
  return new Promise((resolve, reject) => {
@@ -353,39 +415,32 @@ export class AcpBridge {
353
415
  this.expiredRequests.set(id, { method, sessionId: params.sessionId ?? null });
354
416
  while (this.expiredRequests.size > 128) this.expiredRequests.delete(this.expiredRequests.keys().next().value);
355
417
  finish(timeoutError);
356
- }, LATE_RESPONSE_GRACE_MS);
357
- }, timeoutMs)
418
+ }, LATE_RESPONSE_GRACE_MS, { progress: () => pending.progress });
419
+ }, timeoutMs, { progress: () => this.pending.get(id)?.progress })
358
420
  : null;
359
421
  const validIdleTimeout = Number.isFinite(idleTimeoutMs) && idleTimeoutMs > 0 ? idleTimeoutMs : null;
360
422
  if (idleSessionId) this.sessionActivity.set(idleSessionId, Date.now());
361
- let lastTimerAt = Date.now();
362
- let resumedAt = null;
363
- let inactivityRecorded = false;
423
+ const clock = validIdleTimeout ? new ObservationClock({ timeoutMs: validIdleTimeout,
424
+ onGap: gap => this.journal.write("observation_gap", { ...gap, method, request_id: id, session_id: idleSessionId }) }) : null;
425
+ let checking = false, quietCursor = null;
364
426
  const checkIdle = async () => {
365
- if (!this.pending.has(id)) return;
366
- const observedAt = Date.now();
367
- const observationGapMs = observedAt - lastTimerAt;
368
- lastTimerAt = observedAt;
369
- // A host pause makes wall time jump while this process cannot observe
370
- // native progress. Keep the SAME native request alive so its late
371
- // result can still be persisted. Grant one fresh observation window;
372
- // do not rewrite provider activity or resend the request.
373
- if (observationGapMs > 60_000) {
374
- resumedAt = observedAt;
375
- this.journal.write("observation_gap", { method, request_id: id, session_id: idleSessionId, observation_gap_ms: observationGapMs, observed_at: new Date(observedAt).toISOString() });
376
- return;
377
- }
378
- const lastActivity = this.sessionActivity.get(idleSessionId) ?? Date.now();
379
- if (observedAt - Math.max(lastActivity, resumedAt ?? lastActivity) < validIdleTimeout || inactivityRecorded) return;
380
- // ACP silence is diagnostic only. A native child can be computing while
381
- // its Session is non-resident and therefore unsubscribable.
382
- inactivityRecorded = true;
383
- 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() });
384
- if (inactivityMode !== "diagnostic") {
385
- 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 } }));
386
- return;
387
- }
388
- await this.options.onInactivity?.({ method, requestId: id, sessionId: idleSessionId, lastActivityAt: lastActivity, idleTimeoutMs: validIdleTimeout });
427
+ const pending = this.pending.get(id);
428
+ if (!pending || checking || !clock.sample(pending.progress)) return;
429
+ const cursor = pending.progress?.cursor ?? 0;
430
+ if (quietCursor === cursor) return;
431
+ quietCursor = cursor; checking = true;
432
+ const lastActivity = this.sessionActivity.get(idleSessionId) ?? pending.dispatchedAt;
433
+ try {
434
+ 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: now() });
435
+ if (inactivityMode === "diagnostic") await this.options.onInactivity?.({ method, requestId: id, sessionId: idleSessionId, lastActivityAt: lastActivity, idleTimeoutMs: validIdleTimeout, cursor });
436
+ const current = this.pending.get(id);
437
+ if (current !== pending || (current.progress?.cursor ?? 0) !== cursor || !clock.sample(current.progress)) return;
438
+ this.expiredRequests.set(id, { method, sessionId: pending.sessionId });
439
+ finish(Object.assign(new Error(`${method} native outcome cannot be confirmed after ${validIdleTimeout}ms of silence`), { code: clock.observationLost ? "operation_observation_lost" : "subject_liveness_timeout", details: { request_id: id, session_id: idleSessionId, last_activity_at: new Date(lastActivity).toISOString(), idle_timeout_ms: validIdleTimeout, native_outcome: "unknown" } }));
440
+ } catch (error) {
441
+ const current = this.pending.get(id);
442
+ if (current === pending && (current.progress?.cursor ?? 0) === cursor) finish(Object.assign(new Error("ACP inactivity reconciliation could not confirm the native outcome"), { code: "operation_observation_lost", details: { request_id: id, session_id: idleSessionId, native_outcome: "unknown", observation_error: errorRecord(error) }, cause: error }));
443
+ } finally { checking = false; }
389
444
  };
390
445
  const idleTimer = validIdleTimeout ? setInterval(() => {
391
446
  void checkIdle().catch((error) => {
@@ -396,8 +451,9 @@ export class AcpBridge {
396
451
  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(() => {});
397
452
  });
398
453
  }, Math.max(10, Math.min(1_000, Math.floor(validIdleTimeout / 4)))) : null;
399
- this.pending.set(id, { resolve, reject, timer, idleTimer, method, sessionId: params.sessionId ?? null });
454
+ this.pending.set(id, { id, resolve, reject, timer, idleTimer, method, sessionId: params.sessionId ?? null, dispatchedAt: Date.now(), publish: operationProgressReporter() });
400
455
  this.send({ jsonrpc: "2.0", id, method, params });
456
+ if (method === "session/prompt") this.journal.write("native_dispatch", { operation_id: this.dispatchOperationId ?? process.env.DD_EVAL_OPERATION_ID ?? null, session_id: params.sessionId, native_request_id: id });
401
457
  });
402
458
  }
403
459
 
@@ -413,8 +469,18 @@ export class AcpBridge {
413
469
  try { stdin.write(`${JSON.stringify(message)}\n`); }
414
470
  catch (error) { error.details = { system_code: error.code ?? null }; error.code = "bridge_pipe_broken"; this.rejectAll(error); throw error; }
415
471
  }
416
- rejectAll(error) { for (const pending of this.pending.values()) { clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); pending.reject(error); } this.pending.clear(); }
472
+ rejectAll(error) {
473
+ for (const [id, pending] of this.pending) {
474
+ if (this.notificationError === error && pending.method === "session/prompt") {
475
+ this.rejectedHookRequests ??= new Map(); this.rejectedHookRequests.set(id, { method: pending.method, sessionId: pending.sessionId, error });
476
+ if (this.rejectedHookRequests.size > 32) this.rejectedHookRequests.delete(this.rejectedHookRequests.keys().next().value);
477
+ }
478
+ clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); pending.reject(error);
479
+ }
480
+ this.pending.clear();
481
+ }
417
482
  async flush() {
483
+ if (this.notificationError) throw this.notificationError;
418
484
  try { await Promise.all([...this.lifecycleNotifications]); await this.notifications; await this.journal.flush(); }
419
485
  catch (error) { throw this.notificationError ?? error; }
420
486
  if (this.notificationError) throw this.notificationError;
@@ -423,13 +489,12 @@ export class AcpBridge {
423
489
  async close() {
424
490
  // Productive notification failures belong to flush()/the original Turn.
425
491
  // Draining them must not relabel a successful physical shutdown as failed.
426
- await Promise.allSettled([...this.lifecycleNotifications, this.notifications]);
427
- if (this.child) {
428
- this.child.stdin.end();
429
- await Promise.race([new Promise((resolve) => this.child.once("exit", resolve)), new Promise((resolve) => setTimeout(resolve, 2000))]);
430
- await stopProcessGroup(this.child, 2_000);
431
- }
432
- await this.journal.flush();
492
+ return await closeBridgeProcess(this, async () => {
493
+ await Promise.all([this.nativeDrain, this.diagnostics]);
494
+ await Promise.allSettled([...this.lifecycleNotifications, this.notifications]);
495
+ if (this.notificationError) await hookBudget(1_000, null).run(() => this.journal.flush(), "failure_drain");
496
+ else await this.journal.flush();
497
+ });
433
498
  }
434
499
  }
435
500
 
@@ -441,8 +506,8 @@ function isProductiveSessionNotification(message) {
441
506
  // synthetic flag through the ACP wire contract.
442
507
  if (update?.sessionUpdate === "heartbeat" || update?.type === "heartbeat") return false;
443
508
  if (update?.sessionUpdate === "agent_thought_chunk" && typeof update?.messageId === "string" && update.messageId.startsWith("thinking_")) return false;
444
- if ((update?.sessionUpdate === "status" || update?.type === "status") && update?.status === "running") return false;
445
- return true;
509
+ if (["agent_message_chunk", "agent_thought_chunk"].includes(update?.sessionUpdate)) return update.content?.type === "text" && typeof update.content.text === "string" && update.content.text.length > 0;
510
+ return ["agent_message_chunk", "agent_thought_chunk", "tool_call", "tool_call_update"].includes(update?.sessionUpdate);
446
511
  }
447
512
 
448
513
  // The qualified bridge encodes exactly providerId\\modelId; no other suffix or
@@ -474,16 +539,14 @@ async function resume(bridge, sessionId, cwd) {
474
539
  /** Ended children are paginated by the native API (20 by default). A partial
475
540
  * directory is never settlement evidence, including during teardown. */
476
541
  export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000, retained = false) {
477
- const deadline = performance.now() + timeoutMs;
542
+ const clock = new ObservationClock({ timeoutMs });
478
543
  const cursors = new Set(), ended = new Map();
479
544
  let first, cursor;
480
545
  for (;;) {
481
- const remaining = deadline - performance.now();
482
- if (remaining <= 0) fail("ZCode child observation deadline expired", "native_topology_incomplete");
483
- const response = await bridge.request(retained ? "zcode/session/retainedSubagents" : "zcode/session/subagents", { sessionId, ...(cursor ? { endedCursor: cursor } : {}) }, remaining);
546
+ if (clock.sample()) fail("ZCode child observation is unavailable", "operation_observation_lost");
547
+ const response = await bridge.request(retained ? "zcode/session/retainedSubagents" : "zcode/session/subagents", { sessionId, ...(cursor ? { endedCursor: cursor } : {}) }, timeoutMs);
484
548
  if (retained && response?.sessionId !== sessionId) fail("Retained ZCode child directory has a foreign Session identity", "native_topology_incomplete");
485
549
  const page = retained ? response.topology : response;
486
- if (performance.now() >= deadline) fail("ZCode child observation deadline expired", "native_topology_incomplete");
487
550
  if (!page || !Array.isArray(page.running)) fail("ZCode child directory is unavailable", "native_topology_incomplete");
488
551
  if (!first) first = page;
489
552
  if (!page.ended) {
@@ -505,6 +568,8 @@ export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000,
505
568
  }
506
569
  if (typeof cursor !== "string" || cursors.has(cursor) || !Number.isSafeInteger(first.revision)) fail("ZCode child pagination has no advancing, versioned cursor", "native_topology_incomplete");
507
570
  cursors.add(cursor);
571
+ if (cursors.size > 1_024) fail("ZCode child pagination exceeds its bounded directory limit", "native_topology_incomplete");
572
+ clock.sample(cursor);
508
573
  }
509
574
  }
510
575
 
@@ -566,12 +631,11 @@ export function zcodeChildSessionIds(snapshot) {
566
631
  // No resolve/read/subagents extension here: those may implicitly resume.
567
632
  export async function observeClosedZcodeTree(bridge, root, retainedChildren = [], neverPromptedRootProof = null) {
568
633
  const pending = new Set([root, ...retainedChildren]), residents = [], snapshots = [];
569
- const deadline = performance.now() + 30_000;
634
+ const clock = new ObservationClock({ timeoutMs: 30_000 });
570
635
  try {
571
636
  for (const sessionId of pending) {
572
- if (pending.size > 1024 || performance.now() >= deadline) throw new Error("Closed tree observation limit exceeded");
573
- const timeout = () => Math.max(1, Math.min(5000, deadline - performance.now()));
574
- const resident = await bridge.request("zcode/session/resident", { sessionId }, timeout());
637
+ if (pending.size > 1024 || clock.sample()) throw new Error("Closed tree observation limit exceeded");
638
+ const resident = await bridge.request("zcode/session/resident", { sessionId }, 5_000);
575
639
  if (resident?.sessionId !== sessionId || typeof resident.resident !== "boolean") throw new Error("Invalid residency identity or state");
576
640
  residents.push(resident);
577
641
  // A newly allocated root is not durable in ZCode until its first prompt.
@@ -583,7 +647,7 @@ export async function observeClosedZcodeTree(bridge, root, retainedChildren = []
583
647
  snapshots.push({ sessionId, childSessionIds: [], running: [], ended: {total:0,items:[]}, provenance: neverPromptedRootProof });
584
648
  continue;
585
649
  }
586
- const tree = await readZcodeSubagents(bridge, sessionId, deadline - performance.now(), true);
650
+ const tree = await readZcodeSubagents(bridge, sessionId, 30_000, true);
587
651
  if (!Array.isArray(tree?.childSessionIds)
588
652
  || tree.childSessionIds.some(id => typeof id !== "string" || !id || id === sessionId)
589
653
  || !Array.isArray(tree?.running) || !Array.isArray(tree?.ended?.items)
@@ -591,11 +655,12 @@ export async function observeClosedZcodeTree(bridge, root, retainedChildren = []
591
655
  || tree.ended.total > tree.childSessionIds.length
592
656
  || [...tree.running, ...tree.ended.items].some(child => !tree.childSessionIds.includes(child?.childSessionId))) throw new Error("Incomplete retained native topology");
593
657
  snapshots.push({ sessionId, ...tree });
658
+ clock.sample(sessionId);
594
659
  for (const child of tree.childSessionIds) pending.add(child);
595
660
  }
596
661
  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) };
597
662
  } catch (error) {
598
- 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 } };
663
+ return { topology: { root_session_id: root, child_session_ids: [...pending].filter(id => id !== root), complete: false }, residents, snapshots, settled: false, observation_error: errorRecord(error) };
599
664
  }
600
665
  }
601
666
 
@@ -610,7 +675,7 @@ async function cancelTree(bridge, sessionId, before, providerSessionId, retained
610
675
  const taskId = child.taskId ?? child.agentId;
611
676
  if (taskId) {
612
677
  try { cancellations.push(await bridge.request("session/cancelBackgroundTask", { sessionId, taskId })); }
613
- catch (error) { cancellations.push({ taskId, cancelled: false, error: { code: error.code ?? "cancel_failed", message: error.message } }); }
678
+ catch (error) { cancellations.push({ taskId, cancelled: false, error: errorRecord(error) }); }
614
679
  }
615
680
  }
616
681
  // ZCode's stop is deliberately idempotent. A stop acknowledgement is not a
@@ -623,8 +688,8 @@ async function cancelTree(bridge, sessionId, before, providerSessionId, retained
623
688
  await new Promise((resolve) => setTimeout(resolve, 1000));
624
689
  const timeout = Math.max(1, deadline - Date.now());
625
690
  [after, read] = await Promise.all([
626
- readZcodeSubagents(bridge, sessionId, timeout).catch(error => ({ observation_error: { code: error.code, message: error.message } })),
627
- bridge.request("zcode/session/read", { sessionId }, timeout).catch(error => ({ observation_error: { code: error.code, message: error.message } })),
691
+ readZcodeSubagents(bridge, sessionId, timeout).catch(error => ({ observation_error: errorRecord(error) })),
692
+ bridge.request("zcode/session/read", { sessionId }, timeout).catch(error => ({ observation_error: errorRecord(error) })),
628
693
  ]);
629
694
  root_status = read?.projection?.status ?? read?.session?.status ?? null;
630
695
  remember(after);
@@ -637,7 +702,7 @@ async function cancelTree(bridge, sessionId, before, providerSessionId, retained
637
702
  }
638
703
  let close = null;
639
704
  try { close = await bridge.request("zcode/session/close", { sessionId }); }
640
- catch (error) { close = { closed: false, error: { code: error.code ?? "close_failed", message: error.message } }; }
705
+ catch (error) { close = { closed: false, error: errorRecord(error) }; }
641
706
  const observation = close?.closed === true ? await observeClosedZcodeTree(bridge, providerSessionId, [...knownChildren], neverPromptedRootProof ? {...neverPromptedRootProof,close_confirmed:true} : null)
642
707
  : { topology: { root_session_id: providerSessionId, child_session_ids: [...knownChildren], complete: false }, settled: false };
643
708
  return { cancellations, before, after, root_status, cancellation_requested: true, close_requested: true, close, ...observation };
@@ -721,9 +786,10 @@ function requireJournal(options) { absolute(options.journal, "--journal"); }
721
786
  function receipt(options, value) { return options.daemonId ? { daemon_id: options.daemonId, ...value } : value; }
722
787
 
723
788
  async function withBridge(options, operation) {
724
- const bridge = new AcpBridge(options);
725
- try { const initialized = await bridge.start(); const result = await operation(bridge, initialized); await bridge.flush(); return result; }
726
- finally { await bridge.close(); }
789
+ const bridge = new AcpBridge(options); let primary, result;
790
+ try { const initialized = await bridge.start(); result = await operation(bridge, initialized); await bridge.flush(); return result; }
791
+ catch (error) { primary = result === undefined ? error : nativeObservationError(error, { result }); throw primary; }
792
+ finally { try { await bridge.close(); } catch (error) { if (!primary) throw nativeObservationError(error, result === undefined ? {} : { result }); primary.cleanup_error = errorRecord(error); primary.details = { ...primary.details, cleanup_error: primary.cleanup_error }; } }
727
793
  }
728
794
 
729
795
  function flowForwarder(options, rootProviderSessionId, bridge) {
@@ -745,16 +811,22 @@ function flowForwarder(options, rootProviderSessionId, bridge) {
745
811
  export async function zcodeInvocationObserver(options, rootProviderSessionId, send = invokeNativeHook) {
746
812
  if (!options.ddFlowBin) return async () => {};
747
813
  const owners = new Map();
748
- const remember = message => {
814
+ const remember = (message, mandatory = false) => {
749
815
  const update = message.params?.update;
750
816
  if (message.method !== "session/update" || update?.sessionUpdate !== "tool_call") return null;
751
817
  if (options.adapterSessionId && message.params?.sessionId !== options.adapterSessionId) return null;
752
818
  const native = update._meta?.zcodeRuntime ?? {};
753
819
  const session = zcodePhysicalSession(update, rootProviderSessionId);
754
- if (!session) throw Object.assign(new Error("Native subagent event has no physical child identity"), { code: "zcode_child_identity_missing" });
820
+ if (!session) {
821
+ if (mandatory) throw Object.assign(new Error("Native subagent event has no physical child identity"), { code: "zcode_child_identity_missing" });
822
+ return null;
823
+ }
755
824
  const key = update.toolCallId;
756
825
  if (!key || typeof session !== "string") return null;
757
- 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" });
826
+ if (owners.has(key) && owners.get(key) !== session) {
827
+ if (mandatory) throw Object.assign(new Error("Native tool id has conflicting Session ownership"), { code: "zcode_tool_identity_conflict" });
828
+ return null;
829
+ }
758
830
  owners.set(key, session);
759
831
  return { update, native, session };
760
832
  };
@@ -767,21 +839,35 @@ export async function zcodeInvocationObserver(options, rootProviderSessionId, se
767
839
  }
768
840
  } catch (error) { if (error.code !== "ENOENT") throw error; }
769
841
  }
842
+ // The same native Session can host several Turns. Retained tool IDs belong
843
+ // to earlier Turns; a delayed duplicate cannot reject a new prompt waiter.
844
+ const priorTools = new Set(owners.keys());
770
845
  return async message => {
771
- const facts = remember(message);
846
+ if (priorTools.has(message.params?.update?.toolCallId)) return;
847
+ const candidate = message.params?.update, command = candidate?.rawInput?.command ?? candidate?.rawInput?.cmd;
848
+ const shell = ["Bash", "bash", "execute"].includes(candidate?._meta?.claudeCode?.toolName) && typeof command === "string";
849
+ const analysis = shell ? parseLifecycleCommand(command) : null;
850
+ const mandatory = shell && (analysis.kind !== "none" || ["lifecycle", "auxiliary"].includes(analysis.participation) || continuationCommand(command));
851
+ const facts = remember(message, mandatory);
772
852
  if (!facts || !options.ddFlowBin) return;
773
- const { update, native, session } = facts;
774
- const command = update.rawInput?.command ?? update.rawInput?.cmd;
775
- const toolName = update._meta?.claudeCode?.toolName;
776
- if (!["Bash", "bash", "execute"].includes(toolName) || typeof command !== "string") return;
777
- const analysis = parseLifecycleCommand(command);
778
- if (analysis.kind === "none" && !analysis.unresolved && !continuationCommand(command)) return;
853
+ const { native, session } = facts;
854
+ if (!mandatory) return;
779
855
  const parent = session === rootProviderSessionId ? null : owners.get(native.parentToolCallId);
780
856
  if (session !== rootProviderSessionId && !parent) throw Object.assign(new Error("Native child has no confirmed immediate parent tool owner"), { code: "zcode_parent_identity_missing" });
781
- await send({ bin: options.ddFlowBin, home: options.ddFlowHome,
782
- args: ["zcode", "event", "handle", "--project-root", options.projectRoot ?? options.cwd, "--json"],
783
- payload: { ...message, _meta: { ddZcode: { daemonId: options.daemonId, rootProviderSessionId, parentProviderSessionId: parent } } },
784
- });
857
+ try {
858
+ await send({ bin: options.ddFlowBin, home: options.ddFlowHome,
859
+ args: ["zcode", "event", "handle", "--project-root", options.projectRoot ?? options.cwd, "--json"],
860
+ payload: { ...message, _meta: { ddZcode: { daemonId: options.daemonId, rootProviderSessionId, parentProviderSessionId: parent } } },
861
+ });
862
+ } catch (error) {
863
+ const details = error.details;
864
+ // Issued-ID commands cannot cross the CLI CAS without the receipt.
865
+ // This proof concerns lifecycle effects only, never shell-prefix replay.
866
+ if (analysis.kind !== "none" && commandOption(analysis.invocation, "invocation-id")
867
+ && isProvenShellCorrection(error)
868
+ && details.issued_invocation_id === commandOption(analysis.invocation, "invocation-id")) correctableLifecycleErrors.add(error);
869
+ throw error;
870
+ }
785
871
  };
786
872
  }
787
873
 
@@ -919,19 +1005,20 @@ export async function promptSessionWithBridge(bridge, options) {
919
1005
  catch (error) { initial_usage_error = { code: error.code ?? "usage_observation_failed", message: error.message ?? String(error) }; }
920
1006
  // This is the request budget. ACP silence is observed separately and must
921
1007
  // never turn a long native child request into a fabricated provider failure.
922
- const timeoutMs = options.timeoutMs === undefined ? 1_800_000 : options.timeoutMs;
1008
+ const timeoutMs = null;
923
1009
  const textCursor = bridge.assistantTextCursor(identity.adapterSessionId);
924
1010
  options.assertDispatch?.();
1011
+ bridge.dispatchOperationId = options.operationId;
925
1012
  const turn = await bridge.request("session/prompt", { sessionId: identity.adapterSessionId, prompt: [{ type: "text", text: prompt }] }, timeoutMs, {
926
1013
  idleSessionId: identity.adapterSessionId,
927
- idleTimeoutMs: options.livenessTimeoutMs ?? null,
1014
+ idleTimeoutMs: options.livenessTimeoutMs ?? 600_000,
928
1015
  inactivityMode: "diagnostic",
929
1016
  });
930
1017
  const stopReason = typeof turn?.stopReason === "string" ? turn.stopReason : null;
931
1018
  const completionEvidence = turn?._meta?.zcodeCompletionEvidence;
932
1019
  if (completionEvidence === "inferred" || completionEvidence === "failed") {
933
1020
  let observationError = null;
934
- try { await bridge.flush(); } catch (error) { observationError = { code: error.code ?? "zcode_post_turn_observation_failed", message: error.message ?? String(error) }; }
1021
+ try { await bridge.flush(); } catch (error) { observationError = errorRecord(error); }
935
1022
  const error = new Error(`ZCode completion is ${completionEvidence}, not a confirmed successful native turn`);
936
1023
  error.code = completionEvidence === "inferred" ? "native_outcome_unknown" : "zcode_turn_not_completed";
937
1024
  error.retryable = false;
@@ -943,7 +1030,7 @@ export async function promptSessionWithBridge(bridge, options) {
943
1030
  // collect live evidence first: ordinary inspection is allowed to recover
944
1031
  // an evicted Session, which would undo the just-confirmed close.
945
1032
  let observationError = null;
946
- try { await bridge.flush(); } catch (error) { observationError = { code: error.code ?? "zcode_post_turn_observation_failed", message: error.message ?? String(error) }; }
1033
+ try { await bridge.flush(); } catch (error) { observationError = errorRecord(error); }
947
1034
  const error = new Error(`ZCode ended the provider request with ${stopReason}, not end_turn`);
948
1035
  error.code = stopReason === "cancelled" ? "zcode_turn_cancelled" : "zcode_turn_not_completed";
949
1036
  error.details = { stop_reason: stopReason, provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, ...(observationError ? { observation_error: observationError } : {}) };
@@ -955,7 +1042,8 @@ export async function promptSessionWithBridge(bridge, options) {
955
1042
  const error = new Error("ZCode completed the native turn, but post-turn observation failed");
956
1043
  error.code = "native_outcome_observation_failed";
957
1044
  error.retryable = false;
958
- 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) } };
1045
+ error.cause = cause;
1046
+ error.details = { native_outcome: { status: "completed", stop_reason: stopReason ?? "end_turn", completion_evidence: completionEvidence ?? "native" }, provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, observation_error: errorRecord(cause) };
959
1047
  throw error;
960
1048
  }
961
1049
  evidence.tool_calls = bridge.toolSummary(identity.adapterSessionId);
@@ -988,7 +1076,7 @@ export async function cancelSession(options) {
988
1076
  export async function cancelSessionWithBridge(bridge, options) {
989
1077
  requireJournal(options);
990
1078
  const identity = await controlledIdentity(bridge, options);
991
- const before = await readZcodeSubagents(bridge, identity.adapterSessionId).catch(error => ({ observation_error: { code: error.code, message: error.message } }));
1079
+ const before = await readZcodeSubagents(bridge, identity.adapterSessionId).catch(error => ({ observation_error: errorRecord(error) }));
992
1080
  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, options.neverPromptedRootProof) });
993
1081
  }
994
1082