@deksden-com/dd-flow-cli 0.9.0-beta.124 → 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 (113) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +10 -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/harness-runtime/bin/dd-agy.mjs +3 -2
  14. package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
  15. package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
  16. package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
  17. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
  18. package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
  19. package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
  20. package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
  21. package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
  22. package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
  23. package/dist/harness-runtime/lib/daemon-operations.mjs +60 -13
  24. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +151 -72
  25. package/dist/harness-runtime/lib/dd-agy.mjs +11 -23
  26. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +9 -14
  27. package/dist/harness-runtime/lib/dd-codex.mjs +252 -80
  28. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +14 -19
  29. package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
  30. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +56 -30
  31. package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
  32. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +18 -27
  33. package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
  34. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +56 -53
  35. package/dist/harness-runtime/lib/dd-zcode.mjs +200 -111
  36. package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
  37. package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
  38. package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
  39. package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
  40. package/dist/harness-runtime/lib/managed-daemon.d.mts +10 -1
  41. package/dist/harness-runtime/lib/managed-daemon.mjs +228 -40
  42. package/dist/harness-runtime/lib/native-children.d.mts +13 -0
  43. package/dist/harness-runtime/lib/native-children.mjs +78 -0
  44. package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
  45. package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
  46. package/dist/harness-runtime/lib/native-streams.mjs +45 -0
  47. package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
  48. package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
  49. package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
  50. package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
  51. package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
  52. package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
  53. package/dist/harness-runtime/lib/process-json.mjs +136 -38
  54. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  55. package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
  56. package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
  57. package/dist/runtime/context.js +8 -4
  58. package/dist/schemas/run-control-receipt.schema.json +19 -0
  59. package/dist/services/code-checks.js +79 -9
  60. package/dist/services/codex-capacity.js +117 -41
  61. package/dist/services/codex-hook-delivery.js +2 -2
  62. package/dist/services/compatibility-preflight.js +4 -1
  63. package/dist/services/controller-fanout.js +9 -76
  64. package/dist/services/engines.js +70 -20
  65. package/dist/services/eval-snapshots.js +179 -40
  66. package/dist/services/external-work-launch.js +13 -8
  67. package/dist/services/harness-adapter.js +175 -11
  68. package/dist/services/hooks.js +65 -21
  69. package/dist/services/lifecycle-invocations.js +404 -32
  70. package/dist/services/managed-lease-monitor.js +128 -0
  71. package/dist/services/managed-processes.js +103 -47
  72. package/dist/services/merge-server.js +58 -21
  73. package/dist/services/recovery-observation-budget.js +67 -7
  74. package/dist/services/recovery-snapshot-database.js +7 -4
  75. package/dist/services/run-control-receipt.js +1 -1
  76. package/dist/services/run-control-worker.js +177 -118
  77. package/dist/services/run-control.js +99 -8
  78. package/dist/services/run-controller-adapter.js +169 -46
  79. package/dist/services/run-controller-capture.js +14 -2
  80. package/dist/services/run-controller-process.js +11 -4
  81. package/dist/services/run-controller-state.js +68 -7
  82. package/dist/services/run-controller.js +166 -94
  83. package/dist/services/run-fork.js +8 -3
  84. package/dist/services/run-recovery-guard.js +75 -0
  85. package/dist/services/run-recovery-runtime.js +1 -56
  86. package/dist/services/run-recovery.js +17 -13
  87. package/dist/services/runs.js +39 -6
  88. package/dist/services/runtime-budget.js +32 -15
  89. package/dist/services/runtime-scope-control.js +8 -0
  90. package/dist/services/runtime-scope-resume.js +30 -8
  91. package/dist/services/runtime-scope-worker.js +118 -73
  92. package/dist/services/runtime-service.js +6 -7
  93. package/dist/services/scope-writer-cleanup.js +3 -2
  94. package/dist/services/stage-context.js +135 -0
  95. package/dist/services/stage-lifecycle.js +3 -15
  96. package/dist/services/stage-pause.js +3 -2
  97. package/dist/services/usage.js +14 -3
  98. package/dist/services/vnext-code-review.js +10 -9
  99. package/dist/services/vnext-code.js +10 -8
  100. package/dist/services/vnext-contracts.js +10 -1
  101. package/dist/services/vnext-execution-profile.js +75 -2
  102. package/dist/services/vnext-fanout.js +5 -0
  103. package/dist/services/vnext-merge.js +10 -9
  104. package/dist/services/vnext-plan-review.js +21 -17
  105. package/dist/services/vnext-plan.js +9 -7
  106. package/dist/services/vnext-protocolize.js +15 -9
  107. package/dist/services/vnext-specify.js +6 -6
  108. package/dist/services/work-registry.js +34 -4
  109. package/dist/services/workspace-bootstrap.js +14 -3
  110. package/dist/shared/errors.js +11 -6
  111. package/dist/storage/agent-output.js +26 -0
  112. package/dist/storage/database.js +80 -21
  113. package/package.json +7 -4
@@ -1,15 +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
9
  import { closeBridgeProcess } from "./managed-daemon.mjs";
11
- import { errorRecord } from "./operation-errors.mjs";
10
+ import { errorRecord, isProvenShellCorrection } from "./operation-errors.mjs";
12
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";
13
15
 
14
16
  import { readZcodeUsage } from "./zcode-usage.mjs";
15
17
  const ZCODE_HARNESS_CONTRACT = "dd-zcode-harness@1";
@@ -18,11 +20,11 @@ export const supportedZcodeHarnessContract = contract => [ZCODE_HARNESS_CONTRACT
18
20
  // request. Keep the original correlation slot briefly so a slow native
19
21
  // response (especially cold-start/session creation) can still settle it.
20
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();
21
26
 
22
27
  function fail(message, code) { const error = new Error(message); if (code) error.code = code; throw error; }
23
- function fatalLifecycleError(error) {
24
- 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);
25
- }
26
28
  function absolute(value, label) { if (!value || !path.isAbsolute(value)) fail(`${label} must be an absolute path`); return path.resolve(value); }
27
29
  function text(value, label) { if (typeof value !== "string" || !value.trim()) fail(`${label} is required`); return value.trim(); }
28
30
  function now() { return new Date().toISOString(); }
@@ -39,27 +41,12 @@ function executable(command, args = []) {
39
41
 
40
42
  async function commandOutput(command, args) {
41
43
  const target = executable(command, args);
42
- return await new Promise((resolve, reject) => {
43
- const child = spawn(target.command, target.args, { stdio: ["ignore", "pipe", "pipe"] });
44
- let stdout = ""; let stderr = "";
45
- child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk));
46
- child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
47
- child.on("error", reject);
48
- child.on("close", (code) => code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `${target.command} exited ${code}`)));
49
- });
44
+ return await commandText(target.command, target.args, { phase: "native-version" });
50
45
  }
51
46
 
52
47
  async function runWithStdin(command, args, stdin, env = {}) {
53
48
  const target = executable(command, args);
54
- return await new Promise((resolve, reject) => {
55
- const child = spawn(target.command, target.args, { env: { ...process.env, ...env }, stdio: ["pipe", "pipe", "pipe"] });
56
- let stdout = ""; let stderr = "";
57
- child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk));
58
- child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk));
59
- child.on("error", reject);
60
- child.on("close", (code) => code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `${target.command} exited ${code}`)));
61
- child.stdin.end(stdin);
62
- });
49
+ return await commandText(target.command, target.args, { input: stdin, env, phase: "usage-ingest" });
63
50
  }
64
51
 
65
52
  class Journal {
@@ -73,6 +60,7 @@ class Journal {
73
60
  if (!this.file) return;
74
61
  const line = JSON.stringify({ order: ++this.order, observed_at: now(), kind, payload });
75
62
  this.pending = this.pending.then(() => appendFile(this.file, `${line}\n`));
63
+ void this.pending.catch(() => {});
76
64
  return this.pending;
77
65
  }
78
66
  writeInbound(payload, method) {
@@ -110,6 +98,7 @@ class Journal {
110
98
  }
111
99
  await appendFile(this.file, `${line}\n`);
112
100
  });
101
+ void this.pending.catch(() => {});
113
102
  return this.pending;
114
103
  }
115
104
  async flush() { await this.pending; }
@@ -158,8 +147,11 @@ export class AcpBridge {
158
147
  }
159
148
 
160
149
  configure(options) { this.options = { ...this.options, ...options }; }
161
- registerModelSession(adapterId, sessionId, requested, parentSessionId = null, armed = true) {
162
- 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 });
163
155
  }
164
156
  async recordModel(adapterId, observed, source) {
165
157
  const identity = this.modelSessions.get(adapterId); if (!identity) return;
@@ -183,15 +175,15 @@ export class AcpBridge {
183
175
  const target = executable(this.options.bin, this.options.commandArgs ?? ["server"]);
184
176
  const cwd = this.options.cwd ? absolute(this.options.cwd, "--cwd") : process.cwd();
185
177
  this.child = spawn(target.command, target.args, { cwd, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...(this.options.env ?? {}) } });
186
- 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));
187
179
  this.child.stdin?.on("error", (error) => {
188
180
  error.details = { system_code: error.code ?? null };
189
181
  error.code = "bridge_pipe_broken";
190
182
  this.rejectAll(error);
191
183
  });
192
184
  this.child.on("error", (error) => { error.code ??= "bridge_exited"; this.rejectAll(error); });
193
- 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); });
194
- 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));
195
187
  if (this.options.onSpawn) await this.options.onSpawn(this.child);
196
188
  return await this.request("initialize", {
197
189
  protocolVersion: 1,
@@ -206,15 +198,23 @@ export class AcpBridge {
206
198
  const request = message.id !== undefined ? this.pending.get(message.id) : null;
207
199
  const expired = message.id !== undefined ? this.expiredRequests.get(message.id) : null;
208
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
+ }
209
209
  if (!isResponse || request) this.journal.writeInbound(message, request?.method);
210
- 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);
211
211
  const update = message.params?.update;
212
212
  // ZCode emits this as `session/update`; Grok Build wraps the same ACP
213
213
  // payload in `_x.ai/session_notification`. Preserve either provider's
214
214
  // native error before its generic JSON-RPC terminal response arrives.
215
215
  if (typeof message.params?.sessionId === "string" && update?.sessionUpdate === "retry_state" && update.type === "failed") {
216
216
  for (const pending of this.pending.values()) {
217
- 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 };
218
218
  }
219
219
  }
220
220
  if (message.method === "session/update" && message.params?.update?.sessionUpdate === "agent_message_chunk" && message.params.update.content?.type === "text") {
@@ -236,17 +236,21 @@ export class AcpBridge {
236
236
  const pending = Promise.resolve().then(() => observer(message)).catch(async error => {
237
237
  // A validated no-effect rejection is returned through the invocation
238
238
  // ledger. It is evidence, not a poisoned ACP Session.
239
- const fatal = fatalLifecycleError(error);
240
239
  const current = this.options.onLifecycleNotification === observer;
241
- 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
+ }
242
245
  // A missing/foreign authority is not a recoverable CLI validation
243
246
  // result. Stop admitting productive requests immediately; the managed
244
247
  // controller still owns physical stop/drain and evidence capture.
245
- if (fatal && current) this.rejectAll(this.notificationError ?? error);
248
+ if (current && !correctable) this.rejectAll(this.notificationError);
246
249
  try {
247
- await this.journal.write("notification_failure", { code: error.code ?? "invocation_observation_failed", message: error.message, ...(error.details ? { details: error.details } : {}),
248
- 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");
249
252
  } catch (persistenceError) {
253
+ if (current) this.notificationError ??= persistenceError;
250
254
  process.stderr.write(`${JSON.stringify({ code: error.code, message: error.message, persistence_error: persistenceError.message })}\n`);
251
255
  }
252
256
  });
@@ -276,6 +280,7 @@ export class AcpBridge {
276
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";
277
281
  error.retryable = error.code === "provider_rate_limited";
278
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,
279
284
  terminal_response: { method: pending.method, session_id: pending.sessionId, request_id: message.id },
280
285
  ...(data ? { native: data } : {}), ...(details ? { provider_details: details } : {}), ...(providerDetails ? { provider_update: provider } : {}) };
281
286
  pending.reject(error);
@@ -300,6 +305,60 @@ export class AcpBridge {
300
305
  this.toolObservations.add(acpToolObservation(message, { harness: this.options.observationHarness ?? "zcode-acp", rootSessionId }));
301
306
  }
302
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
+
303
362
  toolSummary(sessionId) {
304
363
  return this.toolObservations.summary(this.modelSessions.get(sessionId)?.sessionId ?? sessionId);
305
364
  }
@@ -330,7 +389,9 @@ export class AcpBridge {
330
389
  }
331
390
 
332
391
  request(method, params = {}, timeoutMs = 30_000, { idleSessionId = null, idleTimeoutMs = null, inactivityMode = "reject" } = {}) {
333
- 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);
334
395
  if (this.inactivityError && ["session/new", "session/prompt", "session/fork"].includes(method)) return Promise.reject(this.inactivityError);
335
396
  const id = this.nextId++;
336
397
  return new Promise((resolve, reject) => {
@@ -354,39 +415,32 @@ export class AcpBridge {
354
415
  this.expiredRequests.set(id, { method, sessionId: params.sessionId ?? null });
355
416
  while (this.expiredRequests.size > 128) this.expiredRequests.delete(this.expiredRequests.keys().next().value);
356
417
  finish(timeoutError);
357
- }, LATE_RESPONSE_GRACE_MS);
358
- }, timeoutMs)
418
+ }, LATE_RESPONSE_GRACE_MS, { progress: () => pending.progress });
419
+ }, timeoutMs, { progress: () => this.pending.get(id)?.progress })
359
420
  : null;
360
421
  const validIdleTimeout = Number.isFinite(idleTimeoutMs) && idleTimeoutMs > 0 ? idleTimeoutMs : null;
361
422
  if (idleSessionId) this.sessionActivity.set(idleSessionId, Date.now());
362
- let lastTimerAt = Date.now();
363
- let resumedAt = null;
364
- 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;
365
426
  const checkIdle = async () => {
366
- if (!this.pending.has(id)) return;
367
- const observedAt = Date.now();
368
- const observationGapMs = observedAt - lastTimerAt;
369
- lastTimerAt = observedAt;
370
- // A host pause makes wall time jump while this process cannot observe
371
- // native progress. Keep the SAME native request alive so its late
372
- // result can still be persisted. Grant one fresh observation window;
373
- // do not rewrite provider activity or resend the request.
374
- if (observationGapMs > 60_000) {
375
- resumedAt = observedAt;
376
- this.journal.write("observation_gap", { method, request_id: id, session_id: idleSessionId, observation_gap_ms: observationGapMs, observed_at: new Date(observedAt).toISOString() });
377
- return;
378
- }
379
- const lastActivity = this.sessionActivity.get(idleSessionId) ?? Date.now();
380
- if (observedAt - Math.max(lastActivity, resumedAt ?? lastActivity) < validIdleTimeout || inactivityRecorded) return;
381
- // ACP silence is diagnostic only. A native child can be computing while
382
- // its Session is non-resident and therefore unsubscribable.
383
- inactivityRecorded = true;
384
- 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() });
385
- if (inactivityMode !== "diagnostic") {
386
- 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 } }));
387
- return;
388
- }
389
- 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; }
390
444
  };
391
445
  const idleTimer = validIdleTimeout ? setInterval(() => {
392
446
  void checkIdle().catch((error) => {
@@ -397,8 +451,9 @@ export class AcpBridge {
397
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(() => {});
398
452
  });
399
453
  }, Math.max(10, Math.min(1_000, Math.floor(validIdleTimeout / 4)))) : null;
400
- 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() });
401
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 });
402
457
  });
403
458
  }
404
459
 
@@ -414,8 +469,18 @@ export class AcpBridge {
414
469
  try { stdin.write(`${JSON.stringify(message)}\n`); }
415
470
  catch (error) { error.details = { system_code: error.code ?? null }; error.code = "bridge_pipe_broken"; this.rejectAll(error); throw error; }
416
471
  }
417
- 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
+ }
418
482
  async flush() {
483
+ if (this.notificationError) throw this.notificationError;
419
484
  try { await Promise.all([...this.lifecycleNotifications]); await this.notifications; await this.journal.flush(); }
420
485
  catch (error) { throw this.notificationError ?? error; }
421
486
  if (this.notificationError) throw this.notificationError;
@@ -425,8 +490,10 @@ export class AcpBridge {
425
490
  // Productive notification failures belong to flush()/the original Turn.
426
491
  // Draining them must not relabel a successful physical shutdown as failed.
427
492
  return await closeBridgeProcess(this, async () => {
493
+ await Promise.all([this.nativeDrain, this.diagnostics]);
428
494
  await Promise.allSettled([...this.lifecycleNotifications, this.notifications]);
429
- await this.journal.flush();
495
+ if (this.notificationError) await hookBudget(1_000, null).run(() => this.journal.flush(), "failure_drain");
496
+ else await this.journal.flush();
430
497
  });
431
498
  }
432
499
  }
@@ -439,8 +506,8 @@ function isProductiveSessionNotification(message) {
439
506
  // synthetic flag through the ACP wire contract.
440
507
  if (update?.sessionUpdate === "heartbeat" || update?.type === "heartbeat") return false;
441
508
  if (update?.sessionUpdate === "agent_thought_chunk" && typeof update?.messageId === "string" && update.messageId.startsWith("thinking_")) return false;
442
- if ((update?.sessionUpdate === "status" || update?.type === "status") && update?.status === "running") return false;
443
- 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);
444
511
  }
445
512
 
446
513
  // The qualified bridge encodes exactly providerId\\modelId; no other suffix or
@@ -472,16 +539,14 @@ async function resume(bridge, sessionId, cwd) {
472
539
  /** Ended children are paginated by the native API (20 by default). A partial
473
540
  * directory is never settlement evidence, including during teardown. */
474
541
  export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000, retained = false) {
475
- const deadline = performance.now() + timeoutMs;
542
+ const clock = new ObservationClock({ timeoutMs });
476
543
  const cursors = new Set(), ended = new Map();
477
544
  let first, cursor;
478
545
  for (;;) {
479
- const remaining = deadline - performance.now();
480
- if (remaining <= 0) fail("ZCode child observation deadline expired", "native_topology_incomplete");
481
- 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);
482
548
  if (retained && response?.sessionId !== sessionId) fail("Retained ZCode child directory has a foreign Session identity", "native_topology_incomplete");
483
549
  const page = retained ? response.topology : response;
484
- if (performance.now() >= deadline) fail("ZCode child observation deadline expired", "native_topology_incomplete");
485
550
  if (!page || !Array.isArray(page.running)) fail("ZCode child directory is unavailable", "native_topology_incomplete");
486
551
  if (!first) first = page;
487
552
  if (!page.ended) {
@@ -503,6 +568,8 @@ export async function readZcodeSubagents(bridge, sessionId, timeoutMs = 30_000,
503
568
  }
504
569
  if (typeof cursor !== "string" || cursors.has(cursor) || !Number.isSafeInteger(first.revision)) fail("ZCode child pagination has no advancing, versioned cursor", "native_topology_incomplete");
505
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);
506
573
  }
507
574
  }
508
575
 
@@ -564,12 +631,11 @@ export function zcodeChildSessionIds(snapshot) {
564
631
  // No resolve/read/subagents extension here: those may implicitly resume.
565
632
  export async function observeClosedZcodeTree(bridge, root, retainedChildren = [], neverPromptedRootProof = null) {
566
633
  const pending = new Set([root, ...retainedChildren]), residents = [], snapshots = [];
567
- const deadline = performance.now() + 30_000;
634
+ const clock = new ObservationClock({ timeoutMs: 30_000 });
568
635
  try {
569
636
  for (const sessionId of pending) {
570
- if (pending.size > 1024 || performance.now() >= deadline) throw new Error("Closed tree observation limit exceeded");
571
- const timeout = () => Math.max(1, Math.min(5000, deadline - performance.now()));
572
- 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);
573
639
  if (resident?.sessionId !== sessionId || typeof resident.resident !== "boolean") throw new Error("Invalid residency identity or state");
574
640
  residents.push(resident);
575
641
  // A newly allocated root is not durable in ZCode until its first prompt.
@@ -581,7 +647,7 @@ export async function observeClosedZcodeTree(bridge, root, retainedChildren = []
581
647
  snapshots.push({ sessionId, childSessionIds: [], running: [], ended: {total:0,items:[]}, provenance: neverPromptedRootProof });
582
648
  continue;
583
649
  }
584
- const tree = await readZcodeSubagents(bridge, sessionId, deadline - performance.now(), true);
650
+ const tree = await readZcodeSubagents(bridge, sessionId, 30_000, true);
585
651
  if (!Array.isArray(tree?.childSessionIds)
586
652
  || tree.childSessionIds.some(id => typeof id !== "string" || !id || id === sessionId)
587
653
  || !Array.isArray(tree?.running) || !Array.isArray(tree?.ended?.items)
@@ -589,11 +655,12 @@ export async function observeClosedZcodeTree(bridge, root, retainedChildren = []
589
655
  || tree.ended.total > tree.childSessionIds.length
590
656
  || [...tree.running, ...tree.ended.items].some(child => !tree.childSessionIds.includes(child?.childSessionId))) throw new Error("Incomplete retained native topology");
591
657
  snapshots.push({ sessionId, ...tree });
658
+ clock.sample(sessionId);
592
659
  for (const child of tree.childSessionIds) pending.add(child);
593
660
  }
594
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) };
595
662
  } catch (error) {
596
- 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) };
597
664
  }
598
665
  }
599
666
 
@@ -608,7 +675,7 @@ async function cancelTree(bridge, sessionId, before, providerSessionId, retained
608
675
  const taskId = child.taskId ?? child.agentId;
609
676
  if (taskId) {
610
677
  try { cancellations.push(await bridge.request("session/cancelBackgroundTask", { sessionId, taskId })); }
611
- 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) }); }
612
679
  }
613
680
  }
614
681
  // ZCode's stop is deliberately idempotent. A stop acknowledgement is not a
@@ -621,8 +688,8 @@ async function cancelTree(bridge, sessionId, before, providerSessionId, retained
621
688
  await new Promise((resolve) => setTimeout(resolve, 1000));
622
689
  const timeout = Math.max(1, deadline - Date.now());
623
690
  [after, read] = await Promise.all([
624
- readZcodeSubagents(bridge, sessionId, timeout).catch(error => ({ observation_error: { code: error.code, message: error.message } })),
625
- 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) })),
626
693
  ]);
627
694
  root_status = read?.projection?.status ?? read?.session?.status ?? null;
628
695
  remember(after);
@@ -635,7 +702,7 @@ async function cancelTree(bridge, sessionId, before, providerSessionId, retained
635
702
  }
636
703
  let close = null;
637
704
  try { close = await bridge.request("zcode/session/close", { sessionId }); }
638
- catch (error) { close = { closed: false, error: { code: error.code ?? "close_failed", message: error.message } }; }
705
+ catch (error) { close = { closed: false, error: errorRecord(error) }; }
639
706
  const observation = close?.closed === true ? await observeClosedZcodeTree(bridge, providerSessionId, [...knownChildren], neverPromptedRootProof ? {...neverPromptedRootProof,close_confirmed:true} : null)
640
707
  : { topology: { root_session_id: providerSessionId, child_session_ids: [...knownChildren], complete: false }, settled: false };
641
708
  return { cancellations, before, after, root_status, cancellation_requested: true, close_requested: true, close, ...observation };
@@ -719,10 +786,10 @@ function requireJournal(options) { absolute(options.journal, "--journal"); }
719
786
  function receipt(options, value) { return options.daemonId ? { daemon_id: options.daemonId, ...value } : value; }
720
787
 
721
788
  async function withBridge(options, operation) {
722
- const bridge = new AcpBridge(options); let primary;
723
- try { const initialized = await bridge.start(); const result = await operation(bridge, initialized); await bridge.flush(); return result; }
724
- catch (error) { primary = error; throw error; }
725
- finally { try { await bridge.close(); } catch (error) { if (!primary) throw error; primary.cleanup_error = errorRecord(error); primary.details = { ...primary.details, cleanup_error: primary.cleanup_error }; } }
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 }; } }
726
793
  }
727
794
 
728
795
  function flowForwarder(options, rootProviderSessionId, bridge) {
@@ -744,16 +811,22 @@ function flowForwarder(options, rootProviderSessionId, bridge) {
744
811
  export async function zcodeInvocationObserver(options, rootProviderSessionId, send = invokeNativeHook) {
745
812
  if (!options.ddFlowBin) return async () => {};
746
813
  const owners = new Map();
747
- const remember = message => {
814
+ const remember = (message, mandatory = false) => {
748
815
  const update = message.params?.update;
749
816
  if (message.method !== "session/update" || update?.sessionUpdate !== "tool_call") return null;
750
817
  if (options.adapterSessionId && message.params?.sessionId !== options.adapterSessionId) return null;
751
818
  const native = update._meta?.zcodeRuntime ?? {};
752
819
  const session = zcodePhysicalSession(update, rootProviderSessionId);
753
- 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
+ }
754
824
  const key = update.toolCallId;
755
825
  if (!key || typeof session !== "string") return null;
756
- 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
+ }
757
830
  owners.set(key, session);
758
831
  return { update, native, session };
759
832
  };
@@ -766,21 +839,35 @@ export async function zcodeInvocationObserver(options, rootProviderSessionId, se
766
839
  }
767
840
  } catch (error) { if (error.code !== "ENOENT") throw error; }
768
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());
769
845
  return async message => {
770
- 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);
771
852
  if (!facts || !options.ddFlowBin) return;
772
- const { update, native, session } = facts;
773
- const command = update.rawInput?.command ?? update.rawInput?.cmd;
774
- const toolName = update._meta?.claudeCode?.toolName;
775
- if (!["Bash", "bash", "execute"].includes(toolName) || typeof command !== "string") return;
776
- const analysis = parseLifecycleCommand(command);
777
- if (analysis.kind === "none" && !analysis.unresolved && !continuationCommand(command)) return;
853
+ const { native, session } = facts;
854
+ if (!mandatory) return;
778
855
  const parent = session === rootProviderSessionId ? null : owners.get(native.parentToolCallId);
779
856
  if (session !== rootProviderSessionId && !parent) throw Object.assign(new Error("Native child has no confirmed immediate parent tool owner"), { code: "zcode_parent_identity_missing" });
780
- await send({ bin: options.ddFlowBin, home: options.ddFlowHome,
781
- args: ["zcode", "event", "handle", "--project-root", options.projectRoot ?? options.cwd, "--json"],
782
- payload: { ...message, _meta: { ddZcode: { daemonId: options.daemonId, rootProviderSessionId, parentProviderSessionId: parent } } },
783
- });
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
+ }
784
871
  };
785
872
  }
786
873
 
@@ -918,19 +1005,20 @@ export async function promptSessionWithBridge(bridge, options) {
918
1005
  catch (error) { initial_usage_error = { code: error.code ?? "usage_observation_failed", message: error.message ?? String(error) }; }
919
1006
  // This is the request budget. ACP silence is observed separately and must
920
1007
  // never turn a long native child request into a fabricated provider failure.
921
- const timeoutMs = options.timeoutMs === undefined ? 1_800_000 : options.timeoutMs;
1008
+ const timeoutMs = null;
922
1009
  const textCursor = bridge.assistantTextCursor(identity.adapterSessionId);
923
1010
  options.assertDispatch?.();
1011
+ bridge.dispatchOperationId = options.operationId;
924
1012
  const turn = await bridge.request("session/prompt", { sessionId: identity.adapterSessionId, prompt: [{ type: "text", text: prompt }] }, timeoutMs, {
925
1013
  idleSessionId: identity.adapterSessionId,
926
- idleTimeoutMs: options.livenessTimeoutMs ?? null,
1014
+ idleTimeoutMs: options.livenessTimeoutMs ?? 600_000,
927
1015
  inactivityMode: "diagnostic",
928
1016
  });
929
1017
  const stopReason = typeof turn?.stopReason === "string" ? turn.stopReason : null;
930
1018
  const completionEvidence = turn?._meta?.zcodeCompletionEvidence;
931
1019
  if (completionEvidence === "inferred" || completionEvidence === "failed") {
932
1020
  let observationError = null;
933
- 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); }
934
1022
  const error = new Error(`ZCode completion is ${completionEvidence}, not a confirmed successful native turn`);
935
1023
  error.code = completionEvidence === "inferred" ? "native_outcome_unknown" : "zcode_turn_not_completed";
936
1024
  error.retryable = false;
@@ -942,7 +1030,7 @@ export async function promptSessionWithBridge(bridge, options) {
942
1030
  // collect live evidence first: ordinary inspection is allowed to recover
943
1031
  // an evicted Session, which would undo the just-confirmed close.
944
1032
  let observationError = null;
945
- 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); }
946
1034
  const error = new Error(`ZCode ended the provider request with ${stopReason}, not end_turn`);
947
1035
  error.code = stopReason === "cancelled" ? "zcode_turn_cancelled" : "zcode_turn_not_completed";
948
1036
  error.details = { stop_reason: stopReason, provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, ...(observationError ? { observation_error: observationError } : {}) };
@@ -954,7 +1042,8 @@ export async function promptSessionWithBridge(bridge, options) {
954
1042
  const error = new Error("ZCode completed the native turn, but post-turn observation failed");
955
1043
  error.code = "native_outcome_observation_failed";
956
1044
  error.retryable = false;
957
- 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) };
958
1047
  throw error;
959
1048
  }
960
1049
  evidence.tool_calls = bridge.toolSummary(identity.adapterSessionId);
@@ -987,7 +1076,7 @@ export async function cancelSession(options) {
987
1076
  export async function cancelSessionWithBridge(bridge, options) {
988
1077
  requireJournal(options);
989
1078
  const identity = await controlledIdentity(bridge, options);
990
- 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) }));
991
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) });
992
1081
  }
993
1082
 
@@ -0,0 +1,8 @@
1
+ export const RENEWAL_POLICY: Readonly<{ budgetMs: number; attemptMs: number }>;
2
+ export type RenewalState = { uncertainSince: number | null; attempt: number; exhausted: boolean };
3
+ export function createRenewalState(): RenewalState;
4
+ export function renewalRemaining(state: RenewalState, now?: number): number;
5
+ export function renewalFailure(state: RenewalState, now?: number): number;
6
+ export function renewalDelay(state: RenewalState): number;
7
+ export function renewalConfirmed(state: RenewalState, now?: number): boolean;
8
+ export function maintenanceRetryable(error: unknown): boolean;