@deksden-com/dd-flow-cli 0.9.0-beta.64 → 0.9.0-beta.74

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 (52) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/cli/help.js +2 -0
  3. package/dist/cli/run-cli.js +15 -0
  4. package/dist/harness-runtime/bin/dd-agy.mjs +4 -2
  5. package/dist/harness-runtime/bin/dd-codex.mjs +4 -2
  6. package/dist/harness-runtime/bin/dd-droid.mjs +4 -2
  7. package/dist/harness-runtime/bin/dd-grok.mjs +4 -2
  8. package/dist/harness-runtime/bin/dd-opencode.mjs +4 -2
  9. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  10. package/dist/harness-runtime/lib/daemon-operations.mjs +108 -10
  11. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +5 -4
  12. package/dist/harness-runtime/lib/dd-codex.mjs +38 -4
  13. package/dist/harness-runtime/lib/dd-droid.mjs +55 -20
  14. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +3 -2
  15. package/dist/harness-runtime/lib/dd-grok.mjs +14 -8
  16. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +43 -8
  17. package/dist/harness-runtime/lib/dd-opencode.mjs +5 -4
  18. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +182 -16
  19. package/dist/harness-runtime/lib/dd-zcode.mjs +162 -63
  20. package/dist/harness-runtime/lib/driver-recovery.mjs +2 -2
  21. package/dist/harness-runtime/lib/managed-daemon.mjs +27 -5
  22. package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
  23. package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
  24. package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
  25. package/dist/services/code-checks.js +120 -19
  26. package/dist/services/controller-fanout.js +11 -0
  27. package/dist/services/engines.js +40 -9
  28. package/dist/services/eval-snapshots.js +30 -47
  29. package/dist/services/harness-adapter.js +12 -6
  30. package/dist/services/lifecycle-invocations.js +12 -8
  31. package/dist/services/native-session-control.js +6 -0
  32. package/dist/services/projects.js +6 -2
  33. package/dist/services/recovery-observation-budget.js +9 -1
  34. package/dist/services/run-control-worker.js +19 -0
  35. package/dist/services/run-control.js +87 -13
  36. package/dist/services/run-controller-adapter.js +134 -7
  37. package/dist/services/run-controller.js +11 -3
  38. package/dist/services/run-engine-bindings.js +1 -1
  39. package/dist/services/run-fork.js +124 -0
  40. package/dist/services/run-observations.js +112 -0
  41. package/dist/services/runs.js +1 -11
  42. package/dist/services/schema-validation.js +1 -1
  43. package/dist/services/stage-lifecycle.js +14 -2
  44. package/dist/services/usage.js +20 -87
  45. package/dist/services/vnext-code-review.js +35 -27
  46. package/dist/services/vnext-code.js +62 -26
  47. package/dist/services/vnext-merge.js +8 -8
  48. package/dist/services/vnext-plan-review.js +16 -16
  49. package/dist/services/vnext-plan.js +51 -11
  50. package/dist/services/work-registry.js +48 -27
  51. package/dist/shared/errors.js +12 -0
  52. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { observedTimeout } from "./observation-clock.mjs";
2
+ import { ToolObservations, acpToolObservation, zcodePhysicalSession } from "./tool-observations.mjs";
2
3
  import { invokeNativeHook } from "./native-hook-command.mjs";
3
4
  import { spawn } from "node:child_process";
4
5
  import { createHash } from "node:crypto";
@@ -9,6 +10,10 @@ import { stopProcessGroup } from "./managed-daemon.mjs";
9
10
  import { checkObservedProfile, observeModel, observedFields } from "./model-observations.mjs";
10
11
 
11
12
  const ZCODE_HARNESS_CONTRACT = "dd-zcode-harness@1";
13
+ // A deadline is an observation boundary, not permission to replay an ACP
14
+ // request. Keep the original correlation slot briefly so a slow native
15
+ // response (especially cold-start/session creation) can still settle it.
16
+ const LATE_RESPONSE_GRACE_MS = 5_000;
12
17
 
13
18
  function fail(message, code) { const error = new Error(message); if (code) error.code = code; throw error; }
14
19
  function fatalLifecycleError(error) {
@@ -17,6 +22,11 @@ function fatalLifecycleError(error) {
17
22
  function absolute(value, label) { if (!value || !path.isAbsolute(value)) fail(`${label} must be an absolute path`); return path.resolve(value); }
18
23
  function text(value, label) { if (typeof value !== "string" || !value.trim()) fail(`${label} is required`); return value.trim(); }
19
24
  function now() { return new Date().toISOString(); }
25
+ function responseSummary(message, method = null) {
26
+ const result = message?.result;
27
+ return { request_id: message?.id ?? null, method, has_result: result !== undefined,
28
+ result_keys: result && typeof result === "object" && !Array.isArray(result) ? Object.keys(result).slice(0, 32) : [], error: message?.error ?? null };
29
+ }
20
30
 
21
31
  function executable(command, args = []) {
22
32
  const resolved = command ?? process.env.DD_ZCODE_ACP_BIN ?? "zcode-acp";
@@ -129,13 +139,15 @@ export class AcpBridge {
129
139
  this.options = options;
130
140
  this.nextId = 1;
131
141
  this.pending = new Map();
142
+ this.allocationObservers = new Map();
143
+ this.lateResponses = new Map();
144
+ this.expiredRequests = new Map();
132
145
  this.sessionActivity = new Map();
133
146
  this.notifications = Promise.resolve();
134
147
  this.lifecycleNotifications = new Set();
135
148
  this.notificationError = null;
136
149
  this.bridgeStderr = "";
137
- this.toolCalls = new Map();
138
- this.failedToolCalls = new Set();
150
+ this.toolObservations = new ToolObservations();
139
151
  this.assistantText = new Map();
140
152
  this.journal = new Journal(options.journal);
141
153
  this.modelSessions = new Map(); this.modelProfiles = new Map();
@@ -166,6 +178,11 @@ export class AcpBridge {
166
178
  const cwd = this.options.cwd ? absolute(this.options.cwd, "--cwd") : process.cwd();
167
179
  this.child = spawn(target.command, target.args, { cwd, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...(this.options.env ?? {}) } });
168
180
  this.child.stderr.setEncoding("utf8").on("data", (chunk) => { this.bridgeStderr += String(chunk); this.journal.write("bridge_stderr", { text: String(chunk) }); });
181
+ this.child.stdin?.on("error", (error) => {
182
+ error.details = { system_code: error.code ?? null };
183
+ error.code = "bridge_pipe_broken";
184
+ this.rejectAll(error);
185
+ });
169
186
  this.child.on("error", (error) => { error.code ??= "bridge_exited"; this.rejectAll(error); });
170
187
  this.child.on("exit", (code) => { const error = new Error(`zcode-acp exited ${code}${this.bridgeStderr.trim() ? `: ${this.bridgeStderr.trim()}` : ""}`); error.code = "bridge_exited"; this.rejectAll(error); });
171
188
  readline.createInterface({ input: this.child.stdout }).on("line", (line) => this.receive(line));
@@ -180,7 +197,9 @@ export class AcpBridge {
180
197
  let message;
181
198
  try { message = JSON.parse(line); } catch { this.journal.write("malformed", { line }); return; }
182
199
  const request = message.id !== undefined ? this.pending.get(message.id) : null;
183
- this.journal.writeInbound(message, request?.method);
200
+ const expired = message.id !== undefined ? this.expiredRequests.get(message.id) : null;
201
+ const isResponse = message.id !== undefined && !message.method;
202
+ if (!isResponse || request) this.journal.writeInbound(message, request?.method);
184
203
  if (typeof message.params?.sessionId === "string" && isProductiveSessionNotification(message)) this.sessionActivity.set(message.params.sessionId, Date.now());
185
204
  const update = message.params?.update;
186
205
  // ZCode emits this as `session/update`; Grok Build wraps the same ACP
@@ -195,6 +214,15 @@ export class AcpBridge {
195
214
  const sessionId = message.params.sessionId;
196
215
  this.assistantText.set(sessionId, `${this.assistantText.get(sessionId) ?? ""}${message.params.update.content.text ?? ""}`);
197
216
  }
217
+ if (message.method === "zcode/session/allocated") {
218
+ const identity = message.params;
219
+ const observer = this.allocationObservers.get(identity?.adapter_session_id);
220
+ if (observer && typeof identity?.provider_session_id === "string" && identity.provider_session_id.length > 0) {
221
+ this.allocationObservers.delete(identity.adapter_session_id);
222
+ this.notifications = this.notifications.then(() => observer(identity));
223
+ void this.notifications.catch(error => { this.notificationError ??= error; });
224
+ }
225
+ }
198
226
  this.observeToolCall(message);
199
227
  if (message.method === "session/update" && this.options.onLifecycleNotification) {
200
228
  const observer = this.options.onLifecycleNotification;
@@ -220,17 +248,26 @@ export class AcpBridge {
220
248
  if (message.id !== undefined && message.method) { void this.answer(message).catch((error) => this.rejectAll(error)); return; }
221
249
  if (message.id !== undefined) {
222
250
  const pending = request ?? this.pending.get(message.id);
223
- if (!pending) return;
251
+ if (!pending) {
252
+ this.lateResponses.set(message.id, message);
253
+ while (this.lateResponses.size > 128) this.lateResponses.delete(this.lateResponses.keys().next().value);
254
+ this.journal.write("late_response", responseSummary(message, expired?.method ?? null));
255
+ return;
256
+ }
224
257
  clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); this.pending.delete(message.id);
258
+ if (pending.timedOut) this.journal.write("late_response", { ...responseSummary(message, pending.method), grace_ms: LATE_RESPONSE_GRACE_MS });
225
259
  if (message.error) {
226
- const details = message.error.data?.details ?? message.error.data?.message ?? null;
260
+ const data = message.error.data && typeof message.error.data === "object" && !Array.isArray(message.error.data) ? message.error.data : null;
261
+ const details = data?.details ?? data?.message ?? data ?? null;
227
262
  const provider = pending.providerError;
228
263
  const providerDetails = provider?.message ?? null;
229
- const messageText = [message.error.message, details, providerDetails].filter(Boolean).join(": ") || JSON.stringify(message.error);
264
+ const messageText = [message.error.message, details, providerDetails].filter(Boolean).map(value => typeof value === "string" ? value : JSON.stringify(value)).join(": ") || JSON.stringify(message.error);
230
265
  const error = new Error(messageText);
231
- error.code = /rate limit/i.test(messageText) ? "provider_rate_limited" : /payment required|usage balance exhausted/i.test(messageText) ? "provider_quota_exhausted" : "acp_request_failed";
232
- error.retryable = /rate limit/i.test(messageText);
233
- error.details = { acp_code: message.error.code ?? null, ...(details ? { provider_details: details } : {}), ...(providerDetails ? { provider_update: provider } : {}) };
266
+ const nativeCode = typeof data?.code === "string" ? data.code : null;
267
+ const unknown = ["native_timeout", "native_backend_dead", "native_backend_pipe_broken", "native_outcome_unknown"].includes(nativeCode);
268
+ error.code = unknown ? nativeCode : /rate limit/i.test(messageText) ? "provider_rate_limited" : /payment required|usage balance exhausted/i.test(messageText) ? "provider_quota_exhausted" : "acp_request_failed";
269
+ error.retryable = !unknown && /rate limit/i.test(messageText);
270
+ error.details = { acp_code: message.error.code ?? null, ...(data ? { native: data } : {}), ...(details ? { provider_details: details } : {}), ...(providerDetails ? { provider_update: provider } : {}) };
234
271
  pending.reject(error);
235
272
  }
236
273
  else { pending.resolve(message.result ?? {}); }
@@ -249,35 +286,18 @@ export class AcpBridge {
249
286
  }
250
287
 
251
288
  observeToolCall(message) {
252
- if (message.method !== "session/update") return;
253
- const update = message.params?.update;
254
- if (!update?.toolCallId) return;
255
- const id = JSON.stringify([message.params?.sessionId ?? null, update.toolCallId]);
256
- if (update.sessionUpdate === "tool_call") this.toolCalls.set(id, update?._meta?.claudeCode?.toolName ?? update.title?.split(":", 1)[0] ?? "unknown");
257
- if (update.sessionUpdate === "tool_call_update" && ["failed", "error"].includes(update.status)) this.failedToolCalls.add(id);
289
+ const rootSessionId = this.modelSessions.get(message.params?.sessionId)?.sessionId;
290
+ this.toolObservations.add(acpToolObservation(message, { harness: this.options.observationHarness ?? "zcode-acp", rootSessionId }));
258
291
  }
259
292
 
260
293
  toolSummary(sessionId) {
261
- const by_tool = {};
262
- let total = 0; let failures = 0;
263
- for (const [id, name] of this.toolCalls) {
264
- if (sessionId !== undefined && JSON.parse(id)[0] !== sessionId) continue;
265
- total += 1; by_tool[name] = (by_tool[name] ?? 0) + 1;
266
- if (this.failedToolCalls.has(id)) failures += 1;
267
- }
268
- return { total, failures, by_tool };
294
+ return this.toolObservations.summary(this.modelSessions.get(sessionId)?.sessionId ?? sessionId);
269
295
  }
270
- toolCursor() { return new Set(this.toolCalls.keys()); }
296
+ toolCursor() { return this.toolObservations.cursor(); }
271
297
  assistantTextCursor(sessionId) { return (this.assistantText.get(sessionId) ?? "").length; }
272
298
  assistantTextSince(sessionId, cursor) { return (this.assistantText.get(sessionId) ?? "").slice(cursor); }
273
- toolSummarySince(cursor) {
274
- const by_tool = {}; let total = 0; let failures = 0;
275
- for (const [id, name] of this.toolCalls) {
276
- if (cursor.has(id)) continue;
277
- total += 1; by_tool[name] = (by_tool[name] ?? 0) + 1;
278
- if (this.failedToolCalls.has(id)) failures += 1;
279
- }
280
- return { total, failures, by_tool };
299
+ toolSummarySince(cursor, sessionId) {
300
+ return this.toolObservations.summary(this.modelSessions.get(sessionId)?.sessionId ?? sessionId, cursor);
281
301
  }
282
302
 
283
303
  async answer(message) {
@@ -299,8 +319,9 @@ export class AcpBridge {
299
319
  this.send({ jsonrpc: "2.0", id: message.id, result });
300
320
  }
301
321
 
302
- request(method, params = {}, timeoutMs = 30_000, { idleSessionId = null, idleTimeoutMs = null } = {}) {
322
+ request(method, params = {}, timeoutMs = 30_000, { idleSessionId = null, idleTimeoutMs = null, inactivityMode = "reject" } = {}) {
303
323
  if ((this.notificationError?.code === "profile_integrity_violation" || fatalLifecycleError(this.notificationError)) && ["session/new", "session/prompt", "session/fork"].includes(method)) return Promise.reject(this.notificationError);
324
+ if (this.inactivityError && ["session/new", "session/prompt", "session/fork"].includes(method)) return Promise.reject(this.inactivityError);
304
325
  const id = this.nextId++;
305
326
  return new Promise((resolve, reject) => {
306
327
  const finish = (error) => {
@@ -310,12 +331,27 @@ export class AcpBridge {
310
331
  // Productive Turns use the sliding session-activity timeout below. A
311
332
  // second wall-clock timeout would kill an active, long-running Turn.
312
333
  const timer = Number.isFinite(timeoutMs) && timeoutMs > 0
313
- ? observedTimeout(() => finish(Object.assign(new Error(`${method} timed out`), { code: "rpc_timeout", details: { method, session_id: params.sessionId ?? null } })), timeoutMs)
334
+ ? observedTimeout(() => {
335
+ const pending = this.pending.get(id);
336
+ if (!pending || pending.timedOut) return;
337
+ pending.timedOut = true;
338
+ const timeoutError = Object.assign(new Error(`${method} timed out`), {
339
+ code: "rpc_timeout",
340
+ details: { method, request_id: id, session_id: params.sessionId ?? null, timeout_ms: timeoutMs, grace_ms: LATE_RESPONSE_GRACE_MS },
341
+ });
342
+ this.journal.write("request_deadline", timeoutError.details);
343
+ pending.timer = observedTimeout(() => {
344
+ this.expiredRequests.set(id, { method, sessionId: params.sessionId ?? null });
345
+ while (this.expiredRequests.size > 128) this.expiredRequests.delete(this.expiredRequests.keys().next().value);
346
+ finish(timeoutError);
347
+ }, LATE_RESPONSE_GRACE_MS);
348
+ }, timeoutMs)
314
349
  : null;
315
350
  const validIdleTimeout = Number.isFinite(idleTimeoutMs) && idleTimeoutMs > 0 ? idleTimeoutMs : null;
316
351
  if (idleSessionId) this.sessionActivity.set(idleSessionId, Date.now());
317
352
  let lastTimerAt = Date.now();
318
353
  let resumedAt = null;
354
+ let inactivityRecorded = false;
319
355
  const checkIdle = async () => {
320
356
  if (!this.pending.has(id)) return;
321
357
  const observedAt = Date.now();
@@ -331,45 +367,73 @@ export class AcpBridge {
331
367
  return;
332
368
  }
333
369
  const lastActivity = this.sessionActivity.get(idleSessionId) ?? Date.now();
334
- if (observedAt - Math.max(lastActivity, resumedAt ?? lastActivity) < validIdleTimeout) return;
335
- const error = new Error(`${method} received no session update for ${validIdleTimeout}ms`);
336
- error.code = "subject_liveness_timeout";
337
- error.details = { session_id: idleSessionId, last_activity_at: new Date(lastActivity).toISOString(), idle_timeout_ms: validIdleTimeout };
338
- finish(error);
370
+ if (observedAt - Math.max(lastActivity, resumedAt ?? lastActivity) < validIdleTimeout || inactivityRecorded) return;
371
+ // ACP silence is diagnostic only. A native child can be computing while
372
+ // its Session is non-resident and therefore unsubscribable.
373
+ inactivityRecorded = true;
374
+ this.journal.write("inactivity_detected", { method, request_id: id, session_id: idleSessionId, last_activity_at: new Date(lastActivity).toISOString(), idle_timeout_ms: validIdleTimeout, observed_at: new Date(observedAt).toISOString() });
375
+ if (inactivityMode !== "diagnostic") {
376
+ finish(Object.assign(new Error(`${method} received no session update for ${validIdleTimeout}ms`), { code: "subject_liveness_timeout", details: { session_id: idleSessionId, last_activity_at: new Date(lastActivity).toISOString(), idle_timeout_ms: validIdleTimeout } }));
377
+ return;
378
+ }
379
+ await this.options.onInactivity?.({ method, requestId: id, sessionId: idleSessionId, lastActivityAt: lastActivity, idleTimeoutMs: validIdleTimeout });
339
380
  };
340
- const idleTimer = validIdleTimeout ? setInterval(() => { void checkIdle(); }, Math.max(10, Math.min(1_000, Math.floor(validIdleTimeout / 4)))) : null;
381
+ const idleTimer = validIdleTimeout ? setInterval(() => {
382
+ void checkIdle().catch((error) => {
383
+ // A timer callback is outside the request promise. Preserve the
384
+ // failure for the owning daemon/flush path instead of creating an
385
+ // unhandled rejection that loses the primary request evidence.
386
+ this.inactivityError ??= error;
387
+ void this.journal.write("inactivity_observation_failure", { method, request_id: id, session_id: idleSessionId, error: { code: error?.code ?? "inactivity_observation_failed", message: error?.message ?? String(error) } }).catch(() => {});
388
+ });
389
+ }, Math.max(10, Math.min(1_000, Math.floor(validIdleTimeout / 4)))) : null;
341
390
  this.pending.set(id, { resolve, reject, timer, idleTimer, method, sessionId: params.sessionId ?? null });
342
391
  this.send({ jsonrpc: "2.0", id, method, params });
343
392
  });
344
393
  }
345
394
 
346
395
  notify(method, params = {}) { this.send({ jsonrpc: "2.0", method, params }); }
347
- send(message) { this.journal.write("outbound", message); this.child.stdin.write(`${JSON.stringify(message)}\n`); }
396
+ send(message) {
397
+ this.journal.write("outbound", message);
398
+ const stdin = this.child?.stdin;
399
+ if (!stdin || stdin.destroyed) {
400
+ const error = Object.assign(new Error("zcode-acp stdin is closed"), { code: "bridge_pipe_broken" });
401
+ this.rejectAll(error);
402
+ throw error;
403
+ }
404
+ try { stdin.write(`${JSON.stringify(message)}\n`); }
405
+ catch (error) { error.details = { system_code: error.code ?? null }; error.code = "bridge_pipe_broken"; this.rejectAll(error); throw error; }
406
+ }
348
407
  rejectAll(error) { for (const pending of this.pending.values()) { clearTimeout(pending.timer); if (pending.idleTimer) clearInterval(pending.idleTimer); pending.reject(error); } this.pending.clear(); }
349
408
  async flush() {
350
409
  try { await Promise.all([...this.lifecycleNotifications]); await this.notifications; await this.journal.flush(); }
351
410
  catch (error) { throw this.notificationError ?? error; }
352
411
  if (this.notificationError) throw this.notificationError;
412
+ if (this.inactivityError) throw this.inactivityError;
353
413
  }
354
414
  async close() {
355
- let flushError = null;
356
- try { await this.flush(); } catch (error) { flushError = error; }
415
+ // Productive notification failures belong to flush()/the original Turn.
416
+ // Draining them must not relabel a successful physical shutdown as failed.
417
+ await Promise.allSettled([...this.lifecycleNotifications, this.notifications]);
357
418
  if (this.child) {
358
419
  this.child.stdin.end();
359
420
  await Promise.race([new Promise((resolve) => this.child.once("exit", resolve)), new Promise((resolve) => setTimeout(resolve, 2000))]);
360
421
  await stopProcessGroup(this.child, 2_000);
361
422
  }
362
423
  await this.journal.flush();
363
- if (flushError) throw flushError;
364
424
  }
365
425
  }
366
426
 
367
427
  function isProductiveSessionNotification(message) {
368
428
  if (!["session/update", "_x.ai/session_notification"].includes(message.method)) return false;
369
429
  const update = message.params?.update;
370
- // Transport heartbeat is not evidence of model work. Every other documented
371
- // session update proves that the native session is still responsive.
372
- return update?.sessionUpdate !== "heartbeat" && update?.type !== "heartbeat";
430
+ // Transport state and the bridge's cosmetic thinking hint are not native
431
+ // progress. The latter is identifiable without leaking a ZCode-specific
432
+ // synthetic flag through the ACP wire contract.
433
+ if (update?.sessionUpdate === "heartbeat" || update?.type === "heartbeat") return false;
434
+ if (update?.sessionUpdate === "agent_thought_chunk" && typeof update?.messageId === "string" && update.messageId.startsWith("thinking_")) return false;
435
+ if ((update?.sessionUpdate === "status" || update?.type === "status") && update?.status === "running") return false;
436
+ return true;
373
437
  }
374
438
 
375
439
  export function observedProfile(read) {
@@ -390,14 +454,16 @@ async function resume(bridge, sessionId, cwd) {
390
454
  }
391
455
 
392
456
  async function inspect(bridge, sessionId) {
393
- const [read, subagents, usage] = await Promise.all([
457
+ const [read, subagents] = await Promise.all([
394
458
  bridge.request("zcode/session/read", { sessionId }),
395
459
  bridge.request("zcode/session/subagents", { sessionId }),
396
- bridge.request("zcode/session/usage", { sessionId }),
397
460
  ]);
461
+ let usage = null, usage_observation_error = null;
462
+ try { usage = await bridge.request("zcode/session/usage", { sessionId }); }
463
+ catch (error) { usage_observation_error = { code: error.code ?? "usage_observation_failed", message: error.message ?? String(error) }; }
398
464
  const observed = observedProfile(read);
399
465
  await bridge.recordModel?.(sessionId, observed, "zcode/session/read");
400
- return { read, subagents, usage, observed_profile: observed };
466
+ return { read, subagents, usage, ...(usage_observation_error ? { usage_observation_error } : {}), observed_profile: observed };
401
467
  }
402
468
 
403
469
  // The journal is the immutable transcript. CLI receipts only need the
@@ -590,7 +656,7 @@ function receipt(options, value) { return options.daemonId ? { daemon_id: option
590
656
 
591
657
  async function withBridge(options, operation) {
592
658
  const bridge = new AcpBridge(options);
593
- try { const initialized = await bridge.start(); return await operation(bridge, initialized); }
659
+ try { const initialized = await bridge.start(); const result = await operation(bridge, initialized); await bridge.flush(); return result; }
594
660
  finally { await bridge.close(); }
595
661
  }
596
662
 
@@ -619,8 +685,8 @@ export async function zcodeInvocationObserver(options, rootProviderSessionId, se
619
685
  if (message.method !== "session/update" || update?.sessionUpdate !== "tool_call") return null;
620
686
  if (options.adapterSessionId && message.params?.sessionId !== options.adapterSessionId) return null;
621
687
  const native = update._meta?.zcodeRuntime ?? {};
622
- if (native.source === "subagent" && !native.childSessionId) throw Object.assign(new Error("Native subagent event has no physical child identity"), { code: "zcode_child_identity_missing" });
623
- const session = native.childSessionId ?? rootProviderSessionId;
688
+ const session = zcodePhysicalSession(update, rootProviderSessionId);
689
+ if (!session) throw Object.assign(new Error("Native subagent event has no physical child identity"), { code: "zcode_child_identity_missing" });
624
690
  const key = update.toolCallId;
625
691
  if (!key || typeof session !== "string") return null;
626
692
  if (owners.has(key) && owners.get(key) !== session) throw Object.assign(new Error("Native tool id has conflicting Session ownership"), { code: "zcode_tool_identity_conflict" });
@@ -678,6 +744,11 @@ async function forwardUsage(options, providerSessionId, usage, toolCalls) {
678
744
  await runWithStdin(options.ddFlowBin, ["zcode", "usage", "ingest", "--project-root", projectRoot, "--json"], `${JSON.stringify(payload)}\n`, options.ddFlowHome ? { DD_FLOW_HOME: absolute(options.ddFlowHome, "--dd-flow-home") } : {});
679
745
  }
680
746
 
747
+ async function forwardUsageBestEffort(options, providerSessionId, usage, toolCalls) {
748
+ try { await forwardUsage(options, providerSessionId, usage, toolCalls); return null; }
749
+ catch (error) { return { code: error.code ?? "usage_ingest_failed", message: error.message ?? String(error) }; }
750
+ }
751
+
681
752
  export function zcodeLifecycleQualification(sha256, bridgeCommit, harnessContract) {
682
753
  // Behavioural evidence: dd-eval production-invocations probe, 2026-09-13.
683
754
  // Pin both providers of native event identity, not a string in the bundle.
@@ -712,19 +783,36 @@ export async function createSessionWithBridge(bridge, options, initialized) {
712
783
  let forward;
713
784
  bridge.configure({ ...options, onNotification: async (event) => { await forward?.(event); await options.onNotification?.(event); } });
714
785
  const created = await bridge.request("session/new", { cwd, mcpServers: [] });
786
+ // The ACP alias is the first durable identity available. Persist it before
787
+ // resolving the native Session so a later resolve timeout cannot erase the
788
+ // fact that allocation was already admitted.
789
+ await options.onSessionAliasAllocated?.({ adapter_session_id: created.sessionId, cwd });
790
+ bridge.allocationObservers?.set(created.sessionId, async identity => {
791
+ if (identity.cwd !== cwd) fail("ZCode allocation workspace mismatch", "session_identity_mismatch");
792
+ await options.onSessionAllocated?.(identity);
793
+ });
715
794
  const resolved = await bridge.request("zcode/session/resolve", { sessionId: created.sessionId });
716
795
  const providerSessionId = resolved.providerSessionId;
796
+ // Expose both identities before profile/usage bookkeeping. The owning
797
+ // daemon can persist this allocation even when a later step fails.
798
+ await options.onSessionAllocated?.({
799
+ adapter_session_id: created.sessionId,
800
+ provider_session_id: providerSessionId,
801
+ cwd,
802
+ });
717
803
  bridge.registerModelSession?.(created.sessionId, providerSessionId, requested, null, false);
718
804
  const profile_receipt = await applyProfile(bridge, created.sessionId, requested);
719
805
  forward = flowForwarder(options, providerSessionId, bridge);
720
- const usage = await bridge.request("zcode/session/usage", { sessionId: created.sessionId });
721
- await forwardUsage(options, providerSessionId, usage, bridge.toolSummary(created.sessionId));
806
+ let usage = null, usage_observation_error = null;
807
+ try { usage = await bridge.request("zcode/session/usage", { sessionId: created.sessionId }); }
808
+ catch (error) { usage_observation_error = { code: error.code ?? "usage_observation_failed", message: error.message ?? String(error) }; }
809
+ const usage_ingest_error = await forwardUsageBestEffort(options, providerSessionId, usage, bridge.toolSummary(created.sessionId));
722
810
  await bridge.flush();
723
811
  // ZCode materializes `subagents` only after its first prompt. Session
724
812
  // creation is intentionally nonproductive, so querying that topology here
725
813
  // would both fail on real ZCode and turn the common create contract into an
726
814
  // implicit first turn. The first prompt/inspect records full evidence.
727
- const evidence = { read: null, subagents: { running: [], completed: [] }, usage, observed_profile: profile_receipt.observed, tool_calls: bridge.toolSummary(created.sessionId) };
815
+ const evidence = { read: null, subagents: { running: [], completed: [] }, usage, ...(usage_observation_error ? { usage_observation_error } : {}), ...(usage_ingest_error ? { usage_ingest_error } : {}), observed_profile: profile_receipt.observed, tool_calls: bridge.toolSummary(created.sessionId) };
728
816
  return receipt(options, { harness: "zcode-acp", runtime_family: "zcode", adapter_session_id: created.sessionId, provider_session_id: providerSessionId, cwd, initialized, profile: profile_receipt, evidence: publicEvidence(evidence) });
729
817
  }
730
818
 
@@ -743,25 +831,36 @@ export async function promptSessionWithBridge(bridge, options) {
743
831
  bridge.configure({ onLifecycleNotification: await zcodeInvocationObserver({ ...options, adapterSessionId: identity.adapterSessionId }, identity.providerSessionId) });
744
832
  const profile_receipt = await applyProfile(bridge, identity.adapterSessionId, requested, options.assertDispatch);
745
833
  options.assertDispatch?.();
746
- await forwardUsage(options, identity.providerSessionId, await bridge.request("zcode/session/usage", { sessionId: identity.adapterSessionId }), bridge.toolSummary(identity.adapterSessionId));
747
- const timeoutMs = options.timeoutMs ?? (options.livenessTimeoutMs ? null : 1_800_000);
834
+ let initial_usage_error = null;
835
+ try { initial_usage_error = await forwardUsageBestEffort(options, identity.providerSessionId, await bridge.request("zcode/session/usage", { sessionId: identity.adapterSessionId }), bridge.toolSummary(identity.adapterSessionId)); }
836
+ catch (error) { initial_usage_error = { code: error.code ?? "usage_observation_failed", message: error.message ?? String(error) }; }
837
+ // This is the request budget. ACP silence is observed separately and must
838
+ // never turn a long native child request into a fabricated provider failure.
839
+ const timeoutMs = options.timeoutMs === undefined ? 1_800_000 : options.timeoutMs;
748
840
  const textCursor = bridge.assistantTextCursor(identity.adapterSessionId);
749
841
  options.assertDispatch?.();
750
842
  const turn = await bridge.request("session/prompt", { sessionId: identity.adapterSessionId, prompt: [{ type: "text", text: prompt }] }, timeoutMs, {
751
843
  idleSessionId: identity.adapterSessionId,
752
844
  idleTimeoutMs: options.livenessTimeoutMs ?? null,
845
+ inactivityMode: "diagnostic",
753
846
  });
754
- await bridge.flush();
755
- const evidence = await inspect(bridge, identity.adapterSessionId);
756
- evidence.tool_calls = bridge.toolSummary(identity.adapterSessionId);
757
- await forwardUsage(options, identity.providerSessionId, evidence.usage, evidence.tool_calls);
758
847
  const stopReason = typeof turn?.stopReason === "string" ? turn.stopReason : null;
759
848
  if (stopReason && stopReason !== "end_turn") {
849
+ // A close can settle the pending prompt before its terminal event. Do not
850
+ // collect live evidence first: ordinary inspection is allowed to recover
851
+ // an evicted Session, which would undo the just-confirmed close.
852
+ await bridge.flush();
760
853
  const error = new Error(`ZCode ended the provider request with ${stopReason}, not end_turn`);
761
854
  error.code = stopReason === "cancelled" ? "zcode_turn_cancelled" : "zcode_turn_not_completed";
762
- error.details = { stop_reason: stopReason, provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId, evidence: publicEvidence(evidence) };
855
+ error.details = { stop_reason: stopReason, provider_session_id: identity.providerSessionId, adapter_session_id: identity.adapterSessionId };
763
856
  throw error;
764
857
  }
858
+ await bridge.flush();
859
+ const evidence = await inspect(bridge, identity.adapterSessionId);
860
+ evidence.tool_calls = bridge.toolSummary(identity.adapterSessionId);
861
+ const usage_ingest_error = await forwardUsageBestEffort(options, identity.providerSessionId, evidence.usage, evidence.tool_calls);
862
+ if (initial_usage_error) evidence.initial_usage_error = initial_usage_error;
863
+ if (usage_ingest_error) evidence.usage_ingest_error = usage_ingest_error;
765
864
  if (!options.allowBackground && (evidence.subagents.running ?? []).length) {
766
865
  await cancelTree(bridge, identity.adapterSessionId, evidence.subagents, identity.providerSessionId);
767
866
  fail("background ZCode subagents cannot outlive the one-shot dd-zcode connection; the live child tree was cancelled");
@@ -117,11 +117,11 @@ export async function reconcileDriverReplies(root) {
117
117
  try { entries = await readdir(directory); } catch (error) { if (error.code === "ENOENT") return; throw error; }
118
118
  for (const entry of entries.filter(name => name.endsWith(".json"))) {
119
119
  const file = path.join(directory, entry), record = JSON.parse(await readFile(file, "utf8"));
120
- if (["completed", "failed"].includes(record.state)) continue;
120
+ if (["completed", "failed", "interrupted"].includes(record.state)) continue;
121
121
  let receipt;
122
122
  try { receipt = await inspectDaemonOperation(root, record.operation_id); }
123
123
  catch (error) { if (error.code !== "operation_not_found") throw error; }
124
- if (!["completed", "failed"].includes(receipt?.state)) throw Object.assign(new Error("a previous driver request has an unknown outcome; no new productive request was sent"), {
124
+ if (!["completed", "failed", "interrupted"].includes(receipt?.state)) throw Object.assign(new Error("a previous driver request has an unknown outcome; no new productive request was sent"), {
125
125
  code: "operation_observation_lost", details: { operation_id: record.operation_id, state_dir: root }
126
126
  });
127
127
  await writeJsonAtomic(file, { ...record, state: receipt.state, result: receipt.result, error: receipt.error, reconciled_at: new Date().toISOString() });
@@ -81,7 +81,7 @@ export async function budgetObservationScope(stateDir, sessionId) {
81
81
  try {
82
82
  const requested = JSON.parse(await readFile(path.join(directory, "requested.json"), "utf8"));
83
83
  const result = JSON.parse(await readFile(path.join(directory, "result.json"), "utf8"));
84
- if (requested.daemon_id === daemonId && requested.session_id === sessionId && ["completed", "failed"].includes(result.state) && ["session.prompt", "session.start", "session.resume"].includes(requested.operation)) operationIds.push(requested.operation_id);
84
+ if (requested.daemon_id === daemonId && requested.session_id === sessionId && ["completed", "failed", "interrupted"].includes(result.state) && ["session.prompt", "session.start", "session.resume"].includes(requested.operation)) operationIds.push(requested.operation_id);
85
85
  } catch (error) { if (error.code !== "ENOENT") throw error; }
86
86
  }
87
87
  return { stateDir, state, daemonId, sessionId, operationIds };
@@ -100,12 +100,34 @@ export async function releaseBudgetObservation(scope, observation) {
100
100
 
101
101
  export async function completeBudgetOperation(stateDir, request, result, observe) {
102
102
  const state = await budgetState(stateDir);
103
- if (!state?.config?.runtime_owner?.budget) return;
104
- if (["session.create", "session.fork"].includes(request.operation)) return await releaseBudget(state, { "operation-id": request.id });
103
+ if (!state?.config?.runtime_owner?.budget) return { state: "not_required" };
104
+ if (["session.create", "session.fork"].includes(request.operation)) {
105
+ await releaseBudget(state, { "operation-id": request.id });
106
+ return { state: "settled", operation_ids: [request.id], source: "creation_receipt" };
107
+ }
108
+ const sessionId = request.params?.sessionId;
109
+ // A productive daemon receipt is bound to this exact dispatch. Reuse its
110
+ // complete tree proof instead of immediately asking the provider again.
111
+ if (["session.prompt", "session.start", "session.resume"].includes(request.operation)
112
+ && result?.settled === true
113
+ && typeof sessionId === "string"
114
+ && (result.provider_session_id ?? result.session_id ?? result.adapter_session_id) === sessionId) {
115
+ const scope = { stateDir, state, daemonId: state.daemon_id ?? state.config?.daemonId, sessionId, operationIds: [request.id] };
116
+ await releaseBudgetObservation(scope, result);
117
+ return { state: "settled", operation_ids: [request.id], source: "operation_receipt" };
118
+ }
119
+ // A known live tree is not a reason to poll it again. Preserve capacity and
120
+ // let the next explicit reconciliation obtain fresh proof.
121
+ if (result?.settled === false) return { state: "pending", reason: "tree_unsettled" };
105
122
  const scope = await budgetObservationScope(stateDir, request.params?.sessionId);
106
- if (!scope?.operationIds.length) return;
107
- const observation = observe ? await observe() : result;
123
+ if (!scope?.operationIds.length) return { state: "not_required" };
124
+ // Reconciliation requires a fresh, explicit proof. Treating an arbitrary
125
+ // productive reply as proof would silently release a live turn.
126
+ if (!observe) return { state: "pending", reason: "settlement_evidence_required" };
127
+ const observation = await observe();
128
+ if (observation?.settled !== true) return { state: "pending", reason: "tree_unsettled" };
108
129
  await releaseBudgetObservation(scope, observation);
130
+ return { state: "settled", operation_ids: scope.operationIds, source: "reconciliation_observation" };
109
131
  }
110
132
 
111
133
  async function budgetState(stateDir) {
@@ -1,6 +1,6 @@
1
1
  // An observer losing its response does not establish that a provider Turn failed.
2
2
  export function isObservationLoss(error) {
3
- return ["rpc_timeout", "daemon_timeout", "turn_timeout", "operation_observation_lost", "daemon_connection_closed"].includes(error?.code);
3
+ return ["rpc_timeout", "native_timeout", "native_outcome_unknown", "native_backend_dead", "native_backend_pipe_broken", "bridge_exited", "bridge_pipe_broken", "daemon_timeout", "turn_timeout", "operation_observation_lost", "daemon_connection_closed"].includes(error?.code);
4
4
  }
5
5
 
6
6
  export function errorRecord(error, depth = 0) {
@@ -0,0 +1,17 @@
1
+ export interface ToolEvent {
2
+ session?: { harness_id: string; session_id: string };
3
+ call_id?: string;
4
+ status?: string;
5
+ name?: string;
6
+ reason?: string;
7
+ [key: string]: unknown;
8
+ }
9
+ export class ToolObservations {
10
+ calls: Map<string, ToolEvent>;
11
+ gaps: Set<string>;
12
+ add(event: ToolEvent | null): void;
13
+ summary(sessionId?: string, cursor?: Set<string>): { total: number; failures: number; pending: number; by_tool: Record<string, number>; completeness: string; sessions: Array<{ session: { harness_id: string; session_id: string }; total: number; failures: number; pending: number; by_tool: Record<string, number> }>; reasons: string[] };
14
+ cursor(): Set<string>;
15
+ }
16
+ export function codexToolObservation(record: Record<string, unknown>, sessionId: string): ToolEvent | null;
17
+ export function replayToolJournal(file: string, options: { harness: string; rootSessionId: string; adapterSessionId?: string }, reducer?: ToolObservations): ToolObservations;