@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 { processSnapshot } from "./process-snapshot.mjs";
2
+ import { ToolObservations, droidToolObservations, journalToolObservations } from "./tool-observations.mjs";
2
3
  export { processSnapshot } from "./process-snapshot.mjs";
3
4
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
4
5
  import { createHash, randomUUID } from "node:crypto";
@@ -19,6 +20,7 @@ import { confirmDaemonProcess, finishDaemonProcess, heartbeatDaemonProcess, regi
19
20
  const run = promisify(execFile);
20
21
  export const DROID_CONTRACT = "dd-droid-harness@1";
21
22
  export const DROID_PROTOCOL = "1.201.0";
23
+ const LATE_RESPONSE_GRACE_MS = 5_000;
22
24
  export class DroidError extends Error {
23
25
  constructor(code, message, details) { super(message); this.code = code; if (details) this.details = details; }
24
26
  }
@@ -26,6 +28,8 @@ export const absolute = (value, label) => { if (!value || !path.isAbsolute(value
26
28
  export const executable = (bin, args = []) => /\.[cm]?js$/.test(bin) ? { command: process.execPath, args: [bin, ...args] } : { command: bin, args };
27
29
  const shell = value => `'${String(value).replaceAll("'", `'"'"'`)}'`;
28
30
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
31
+ const responseSummary = event => ({ request_id: event?.id ?? null, has_result: event?.result !== undefined,
32
+ result_keys: event?.result && typeof event.result === "object" && !Array.isArray(event.result) ? Object.keys(event.result).slice(0, 32) : [], error: event?.error ?? null });
29
33
  export async function readJson(file) { try { return JSON.parse(await readFile(file, "utf8")); } catch (error) { if (error.code === "ENOENT") return null; throw error; } }
30
34
  export function droidPaths(stateDir) {
31
35
  const dir = absolute(stateDir, "state directory"), digest = createHash("sha256").update(dir).digest("hex").slice(0, 24);
@@ -71,13 +75,10 @@ export function parseDroidTranscript(text, settings, expectedId) {
71
75
  for (const line of text.split("\n")) { if (!line.trim()) continue; try { records.push(JSON.parse(line)); } catch { complete = false; } }
72
76
  const header = records.find(record => record.type === "session_start");
73
77
  if (!header || header.id !== expectedId) throw new DroidError("session_identity_mismatch", "Droid transcript has a different or missing native Session ID", { expected_session_id: expectedId, observed_session_id: header?.id ?? null });
74
- const calls = new Map(), failures = new Set();
75
- for (const record of records) for (const block of record.message?.content ?? []) {
76
- if (block.type === "tool_use" && typeof block.id === "string") calls.set(block.id, block.name);
77
- if (block.type === "tool_result" && block.is_error && typeof block.tool_use_id === "string") failures.add(block.tool_use_id);
78
- }
79
- const byTool = {}; for (const name of calls.values()) byTool[name] = (byTool[name] ?? 0) + 1;
80
- return { header, records, outcomes: records.filter(record => record.type === "agent_turn_outcome"), usage: physicalUsage(settings?.tokenUsage), settings, complete, tool_calls: { total: calls.size, failures: [...failures].filter(id => calls.has(id)).length, by_tool: byTool } };
78
+ const tools = new ToolObservations();
79
+ for (const record of records) for (const event of droidToolObservations(record, expectedId)) tools.add(event);
80
+ if (!complete) tools.gaps.add("transcript_incomplete");
81
+ return { header, records, outcomes: records.filter(record => record.type === "agent_turn_outcome"), usage: physicalUsage(settings?.tokenUsage), settings, complete, tool_calls: tools.summary() };
81
82
  }
82
83
  export async function readDroidSession(factory, id) {
83
84
  const file = await metadataReader(factory).locate(id);
@@ -95,7 +96,7 @@ function descendants(rows, rootPid) { const ids = new Set([rootPid]); for (;;) {
95
96
 
96
97
  export class DroidRuntime {
97
98
  constructor(config, state, persist) {
98
- this.config = config; this.state = state; this.paths = droidPaths(config.stateDir); this.persist = persist; this.pending = new Map(); this.active = null; this.child = null; this.closed = true; this.loaded = false; this.ownedProcesses = state.owned_processes ?? []; this.closing = false; this.workingState = "idle"; this.protocol = null; this.rootId = state.root_session_id ?? null; this.topology = new Map(state.descendants?.map(child => [child.provider_session_id, child]) ?? []); this.draining = Promise.resolve(); this.journalling = Promise.resolve(); this.activity = 0; this.providerProcess = null; this.cancelled = false; this.lastResult = state.last_result ?? null; this.metadata = metadataReader(this.paths.factory); this.fatalError = state.fatal_error ? reportedError(state.fatal_error) : null;
99
+ this.config = config; this.state = state; this.paths = droidPaths(config.stateDir); this.persist = persist; this.pending = new Map(); this.lateResponses = new Map(); this.active = null; this.child = null; this.closed = true; this.loaded = false; this.ownedProcesses = state.owned_processes ?? []; this.closing = false; this.workingState = "idle"; this.protocol = null; this.rootId = state.root_session_id ?? null; this.topology = new Map(state.descendants?.map(child => [child.provider_session_id, child]) ?? []); this.draining = Promise.resolve(); this.journalling = Promise.resolve(); this.activity = 0; this.providerProcess = null; this.cancelled = false; this.lastResult = state.last_result ?? null; this.metadata = metadataReader(this.paths.factory); this.fatalError = state.fatal_error ? reportedError(state.fatal_error) : null;
99
100
  }
100
101
  async journal(kind, payload) { const line = JSON.stringify({ schema_id: "dd-droid/event@1", observed_at: new Date().toISOString(), kind, ...payload }); this.journalling = this.journalling.then(() => appendFile(this.config.journal, `${line}\n`, { mode: 0o600 })); return await this.journalling; }
101
102
  async save(patch = {}) { return await this.persist({ root_session_id: this.rootId, descendants: [...this.topology.values()], working_state: this.workingState, last_activity: this.activity, ...patch }); }
@@ -129,13 +130,14 @@ export class DroidRuntime {
129
130
  await this.journal("native", { event });
130
131
  if (event.type === "response") {
131
132
  this.protocol ??= event.factoryProtocolVersion;
132
- const pending = this.pending.get(event.id); if (!pending) return;
133
+ const pending = this.pending.get(event.id); if (!pending) { this.lateResponses.set(event.id, event); while (this.lateResponses.size > 128) this.lateResponses.delete(this.lateResponses.keys().next().value); await this.journal("late_response", responseSummary(event)); return; }
133
134
  this.pending.delete(event.id); clearTimeout(pending.timer);
135
+ if (pending.timedOut) await this.journal("late_response", { ...responseSummary(event), method: pending.method, grace_ms: LATE_RESPONSE_GRACE_MS });
134
136
  event.error ? pending.reject(new DroidError("droid_rpc_error", event.error.message ?? "Droid rejected request", event.error)) : pending.resolve(event.result ?? {}); return;
135
137
  }
136
138
  if (event.method === "droid.request_permission" || event.method === "droid.ask_user") {
137
139
  const result = event.method === "droid.request_permission" ? { selectedOption: "cancel" } : { cancelled: true, answers: [] };
138
- this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", type: "response", factoryApiVersion: "1.0.0", factoryProtocolVersion: DROID_PROTOCOL, id: event.id, result })}\n`);
140
+ this.send({ jsonrpc: "2.0", type: "response", factoryApiVersion: "1.0.0", factoryProtocolVersion: DROID_PROTOCOL, id: event.id, result });
139
141
  await this.journal("unexpected_input_rejected", { method: event.method }); return;
140
142
  }
141
143
  if (event.method !== "droid.session_notification") return;
@@ -162,11 +164,28 @@ export class DroidRuntime {
162
164
  request(method, params, id = randomUUID(), timeoutMs = 30_000) {
163
165
  if (!this.child || this.closed) return Promise.reject(new DroidError("operation_observation_lost", "Droid runtime is not connected"));
164
166
  return new Promise((resolve, reject) => {
165
- const timer = observedTimeout(() => { this.pending.delete(id); reject(new DroidError("rpc_timeout", `${method} has no observed reply`, { request_id: id })); }, timeoutMs);
166
- this.pending.set(id, { resolve, reject, timer });
167
- this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", type: "request", factoryApiVersion: "1.0.0", factoryProtocolVersion: DROID_PROTOCOL, id, method, params })}\n`, error => { if (error) { clearTimeout(timer); this.pending.delete(id); reject(error); } });
167
+ const timer = observedTimeout(() => {
168
+ const pending = this.pending.get(id); if (!pending || pending.timedOut) return;
169
+ pending.timedOut = true;
170
+ const timeoutError = new DroidError("rpc_timeout", `${method} has no observed reply`, { request_id: id, timeout_ms: timeoutMs, grace_ms: LATE_RESPONSE_GRACE_MS });
171
+ void this.journal("request_deadline", { method, request_id: id, timeout_ms: timeoutMs, grace_ms: LATE_RESPONSE_GRACE_MS });
172
+ pending.timer = observedTimeout(() => { if (this.pending.get(id) !== pending) return; this.pending.delete(id); reject(timeoutError); }, LATE_RESPONSE_GRACE_MS);
173
+ }, timeoutMs);
174
+ this.pending.set(id, { resolve, reject, timer, method });
175
+ try { this.send({ jsonrpc: "2.0", type: "request", factoryApiVersion: "1.0.0", factoryProtocolVersion: DROID_PROTOCOL, id, method, params }, error => { if (error) { clearTimeout(timer); this.pending.delete(id); reject(error); } }); }
176
+ catch (error) { clearTimeout(timer); this.pending.delete(id); reject(error); }
168
177
  });
169
178
  }
179
+ send(value, callback) {
180
+ const stdin = this.child?.stdin;
181
+ if (!stdin || stdin.destroyed) {
182
+ const error = new DroidError("bridge_pipe_broken", "Droid runtime stdin is closed");
183
+ this.rejectPending(error);
184
+ throw error;
185
+ }
186
+ try { stdin.write(`${JSON.stringify(value)}\n`, callback); }
187
+ catch (error) { this.rejectPending(error); throw error; }
188
+ }
170
189
  profile(settings) {
171
190
  const model = settings?.modelId ?? settings?.model ?? null;
172
191
  const nativeModel = this.models?.find(item => (item.id ?? item.modelId) === model);
@@ -279,8 +298,15 @@ export class DroidRuntime {
279
298
  assertDispatch();
280
299
  await this.request("droid.add_user_message", { text: prompt, messageId: turnId }, requestId);
281
300
  const outcome = await completed; await this.draining;
282
- const receipt = await this.inspect(id, { ignoreActive: true });
283
- return { ...receipt, result: { ...outcome, status: status(outcome.reason) }, assistant_text: current.text, turn_id: turnId };
301
+ try {
302
+ const receipt = await this.inspect(id, { ignoreActive: true });
303
+ return { ...receipt, result: { ...outcome, status: status(outcome.reason) }, assistant_text: current.text, turn_id: turnId };
304
+ } catch (error) {
305
+ // A terminal native outcome is durable independently of a later
306
+ // filesystem/topology observation. Preserve it and let settlement
307
+ // reconcile; do not convert it into a failed prompt.
308
+ return { provider_session_id: id, adapter_session_id: id, result: { ...outcome, status: status(outcome.reason) }, assistant_text: current.text, turn_id: turnId, settled: false, settlement_observation_error: errorRecord(error) };
309
+ }
284
310
  } catch (error) {
285
311
  await this.journal("prompt_error", { operation_id: operationId, error: errorRecord(error) }); throw error;
286
312
  } finally { clearInterval(timer); if (this.active === current) this.active = null; await this.save({ active_tree: this.workingState !== "idle" }); }
@@ -292,16 +318,23 @@ export class DroidRuntime {
292
318
  const native = await readDroidSession(this.paths.factory, binding.provider_session_id);
293
319
  const outcome = native?.outcomes.find(item => item.turnId === binding.turn_id);
294
320
  if (!outcome || !terminal(outcome.reason)) return null;
295
- const receipt = await this.inspect(binding.provider_session_id, { ignoreActive: true });
296
- if (!receipt.settled) return null;
321
+ let receipt;
322
+ try { receipt = await this.inspect(binding.provider_session_id, { ignoreActive: true }); }
323
+ catch (error) {
324
+ receipt = { provider_session_id: binding.provider_session_id, adapter_session_id: binding.provider_session_id, settled: false, settlement_observation_error: errorRecord(error) };
325
+ }
326
+ // A terminal root turn is not enough to recover the operation while a
327
+ // child Task or owned provider process is still active. Leave the
328
+ // durable binding for the normal settlement observer instead of exposing
329
+ // a misleading successful receipt.
330
+ if (receipt.settled !== true) return null;
297
331
  const current = this.active;
298
332
  if (current?.operationId === operationId && current.turnId === binding.turn_id) {
299
333
  current.outcome = outcome; this.lastResult = outcome;
300
334
  await this.saveBinding(current, { native_outcome: outcome });
301
335
  current.resolve(outcome);
302
- // Let the original dispatch persist its one terminal reply and release
303
- // daemon busy. The next inspection reads that receipt; do not race it
304
- // with a second writer or expose success while prompt still owns busy.
336
+ // Let the original dispatch persist its one terminal reply. Settlement
337
+ // is reconciled separately; do not race a second writer or replay it.
305
338
  return null;
306
339
  }
307
340
  return { ...receipt, result: { ...outcome, status: status(outcome.reason) }, assistant_text: binding.assistant_text ?? "", turn_id: binding.turn_id, recovered_from: "native.agent_turn_outcome" };
@@ -343,6 +376,8 @@ export class DroidRuntime {
343
376
  }
344
377
  async snapshotUsage(id) {
345
378
  const native = await readDroidSession(this.paths.factory, id); if (!native) return { status: "unavailable", provider_session_id: id };
379
+ this.journaledTools ??= new Map();
380
+ journalToolObservations(this.config.journal, [...native.records.flatMap(record => droidToolObservations(record, id)), ...native.tool_calls.reasons.map(reason => ({ reason }))], this.journaledTools);
346
381
  const measured = native.usage;
347
382
  const payload = { provider_session_id: id, daemon_id: this.config.daemonId, observed_at: new Date().toISOString(), usage_scope: "physical_session", completeness: native.complete && measured ? "complete" : "partial", usage: measured, tool_calls: native.tool_calls, source: { path: native.transcript_path, sha256: native.source_sha256 } };
348
383
  if (measured && this.config.ddFlowBin) await this.flow("usage", "ingest", payload);
@@ -1,4 +1,5 @@
1
1
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
2
+ import { ToolObservations, acpToolObservation } from "./tool-observations.mjs";
2
3
  import { observedTimeout } from "./observation-clock.mjs";
3
4
  import { createHash, randomUUID } from "node:crypto";
4
5
  import { chmod, copyFile, cp, lstat, mkdir, open, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
@@ -198,7 +199,7 @@ export class Runtime {
198
199
  }
199
200
  }
200
201
  topology(root) { return [...this.descendants.values()].filter((child) => child.parent_provider_session_id === root); }
201
- observeToolCall(message) { const sessionId = message?.params?.sessionId; const update = message?.params?.update; const id = update?.toolCallId; if (!sessionId || !id) return; this.toolEvents ??= new Map(); const events = this.toolEvents.get(sessionId) ?? { calls: new Map(), failed: new Set() }; if (update.sessionUpdate === "tool_call") events.calls.set(id, update?._meta?.["x.ai/tool"]?.name ?? update?._meta?.claudeCode?.toolName ?? update.title?.split(":", 1)[0] ?? "unknown"); if (update.sessionUpdate === "tool_call_update" && ["failed", "error"].includes(update.status)) events.failed.add(id); this.toolEvents.set(sessionId, events); const by_tool = {}; for (const name of events.calls.values()) by_tool[name] = (by_tool[name] ?? 0) + 1; this.toolUsage.set(sessionId, { total: events.calls.size, failures: events.failed.size, by_tool }); }
202
+ observeToolCall(message) { this.toolObservations ??= new ToolObservations(); this.toolObservations.add(acpToolObservation(message, { harness: "grok-acp" })); const sessionId = message?.params?.sessionId; if (sessionId) this.toolUsage.set(sessionId, this.toolObservations.summary(sessionId)); }
202
203
  running(result) { return result?.evidence?.subagents?.subagents ?? result?.subagents?.subagents ?? []; }
203
204
  treeRunning(result) {
204
205
  const evidence = result?.evidence ?? result;
@@ -232,7 +233,7 @@ export class Runtime {
232
233
  return running;
233
234
  }
234
235
  async requireSettled() { const running = await this.refreshTree(); if (running.length) throw new DaemonError("tree_not_settled", "daemon still owns a running Session tree", false, { sessions: running }); }
235
- async productive(name, task) { if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true); this.active = name; for (const session of this.sessions.values()) session.native_root_receipt = null; try { await this.persist({ active_tree: true, active_operation: name }); const result = await task(); this.track(result); const session = this.sessions.get(result.provider_session_id); if (session && (["session.create", "session.fork"].includes(name) || ["end_turn", "cancelled"].includes(result.turn?.stopReason))) session.native_root_receipt = { kind: name === "session.prompt" ? result.turn.stopReason : "created", daemon_id: this.state.daemon_id }; await this.persist({ active_tree: this.treeRunning(result), active_operation: null }); return { ...result, descendants: this.topology(result.provider_session_id) }; } catch (error) { await this.persist({ active_tree: true, active_operation: null }); throw error; } finally { this.active = null; } }
236
+ async productive(name, task) { if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true); this.active = name; for (const session of this.sessions.values()) session.native_root_receipt = null; try { await this.persist({ active_tree: true, active_operation: name }); const result = await task(); this.track(result); const running = this.treeRunning(result); const session = this.sessions.get(result.provider_session_id); if (session && (["session.create", "session.fork"].includes(name) || ["end_turn", "cancelled"].includes(result.turn?.stopReason))) session.native_root_receipt = { kind: name === "session.prompt" ? result.turn.stopReason : "created", daemon_id: this.state.daemon_id }; await this.persist({ active_tree: running, active_operation: null }); return { ...result, descendants: this.topology(result.provider_session_id), settled: !running }; } catch (error) { await this.persist({ active_tree: true, active_operation: null }); throw error; } finally { this.active = null; } }
236
237
  async dispatch(operation, params) {
237
238
  const assertDispatch = captureDispatchGuard(this);
238
239
  if (operation === "daemon.status") { let observationError; if (!this.active) try { await this.refreshTree(); } catch (error) { observationError = errorPayload(error); } return { observation_error: observationError ?? null, daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, config_isolation: this.state.config_isolation, shutdown_state: this.state.shutdown_state, recovery_status: this.state.recovery_status, auth_status: this.state.auth_status, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config: this.state.config }; }
@@ -29,13 +29,13 @@ function unwrap(value) { return value?.result ?? value ?? {}; }
29
29
  function extension(method) { return method.startsWith("_") ? method : `_${method}`; }
30
30
  function usageScope(options, providerSessionId) { return providerSessionId === options.rootProviderSessionId ? "execution_tree_inclusive" : "physical_session"; }
31
31
  function emptyToolCalls() { return { total: 0, failures: 0, by_tool: {} }; }
32
- function sessionToolCalls(options, bridge, sessionId) { return options.toolUsage ? (options.toolUsage.get(sessionId) ?? emptyToolCalls()) : bridge.toolSummary(); }
32
+ function sessionToolCalls(options, bridge, sessionId) { return options.toolUsage ? (options.toolUsage.get(sessionId) ?? emptyToolCalls()) : bridge.toolSummary(sessionId); }
33
33
  function recordToolCalls(options, bridge, sessionId, cursor) {
34
34
  const result = options.toolUsage
35
35
  ? sessionToolCalls(options, bridge, sessionId)
36
36
  : typeof bridge.toolSummarySince === "function"
37
- ? bridge.toolSummarySince(cursor)
38
- : bridge.toolSummary();
37
+ ? bridge.toolSummarySince(cursor, sessionId)
38
+ : bridge.toolSummary(sessionId);
39
39
  options.toolUsage?.set(sessionId, result);
40
40
  return result;
41
41
  }
@@ -45,6 +45,7 @@ function bridgeOptions(options) {
45
45
  const grokHome = options.grokHome ? absolute(options.grokHome, "--grok-home") : null;
46
46
  return {
47
47
  ...options,
48
+ observationHarness: "grok-acp",
48
49
  // AcpBridge defaults to zcode-acp; Grok must always name its own executable.
49
50
  bin: options.bin ?? process.env.DD_GROK_BIN ?? "grok",
50
51
  commandArgs: ["agent", "--no-leader", "--always-approve", "--model", requested.model ?? "grok-4.6", "--reasoning-effort", requested.reasoning ?? "high", "stdio"],
@@ -91,6 +92,11 @@ async function forwardUsage(options, providerSessionId, usage, toolCalls) {
91
92
  });
92
93
  }
93
94
 
95
+ async function forwardUsageBestEffort(options, providerSessionId, usage, toolCalls) {
96
+ try { await forwardUsage(options, providerSessionId, usage, toolCalls); return null; }
97
+ catch (error) { return { code: error.code ?? "usage_ingest_failed", message: error.message ?? String(error) }; }
98
+ }
99
+
94
100
  async function identity(bridge, options) {
95
101
  const sessionId = text(options.sessionId, "--session-id");
96
102
  if (!options.liveSession) await bridge.request("session/load", { sessionId, cwd: absolute(options.cwd, "--cwd"), mcpServers: [] });
@@ -136,24 +142,24 @@ export async function createSessionWithBridge(bridge, options, initialized) {
136
142
  await observeModel({ journal: options.journal, harness: "grok-acp", sessionId: providerSessionId, requested, observed: observedProfile(initialized, requested), source: "acp.initialize.modelState" });
137
143
  const profileReceipt = assertProfile(requested, observedProfile(initialized, requested));
138
144
  await options.onSessionCreated?.({ provider_session_id: providerSessionId, adapter_session_id: providerSessionId, cwd });
139
- const local = { ...options, initialized, rootProviderSessionId: providerSessionId }; await forwardUsage(local, providerSessionId, await sessionUsage(bridge, providerSessionId), sessionToolCalls(local, bridge, providerSessionId));
140
- await bridge.flush(); const evidence = await inspect(bridge, providerSessionId, local); evidence.tool_calls = sessionToolCalls(local, bridge, providerSessionId); await forwardUsage(local, providerSessionId, evidence.usage, evidence.tool_calls);
145
+ const local = { ...options, initialized, rootProviderSessionId: providerSessionId }; const initial_usage_error = await forwardUsageBestEffort(local, providerSessionId, await sessionUsage(bridge, providerSessionId), sessionToolCalls(local, bridge, providerSessionId));
146
+ await bridge.flush(); const evidence = await inspect(bridge, providerSessionId, local); evidence.tool_calls = sessionToolCalls(local, bridge, providerSessionId); const usage_ingest_error = await forwardUsageBestEffort(local, providerSessionId, evidence.usage, evidence.tool_calls); if (initial_usage_error) evidence.initial_usage_error = initial_usage_error; if (usage_ingest_error) evidence.usage_ingest_error = usage_ingest_error;
141
147
  return receipt(options, { harness: "grok-acp", runtime_family: "grok", provider_session_id: providerSessionId, adapter_session_id: providerSessionId, cwd, initialized, profile: profileReceipt, evidence });
142
148
  }
143
149
 
144
150
  export async function promptSession(options) { requireJournal(options); return await withBridge(options, async (bridge, initialized) => await promptSessionWithBridge(bridge, { ...options, initialized })); }
145
151
  export async function promptSessionWithBridge(bridge, options) {
146
152
  requireJournal(options); const sessionId = await identity(bridge, options); const prompt = text(options.prompt, "prompt"); const requested = requiredProfile(options); const profileReceipt = assertProfile(requested, observedProfile(options.initialized, requested));
147
- const local = { ...options, rootProviderSessionId: options.rootProviderSessionId ?? sessionId }; await forwardUsage(local, sessionId, await sessionUsage(bridge, sessionId), sessionToolCalls(local, bridge, sessionId));
153
+ const local = { ...options, rootProviderSessionId: options.rootProviderSessionId ?? sessionId }; const initial_usage_error = await forwardUsageBestEffort(local, sessionId, await sessionUsage(bridge, sessionId), sessionToolCalls(local, bridge, sessionId));
148
154
  options.assertDispatch?.();
149
155
  const cursor = bridge.toolCursor(); const textCursor = bridge.assistantTextCursor(sessionId); const turn = await bridge.request("session/prompt", { sessionId, prompt: [{ type: "text", text: prompt }] }, null, { idleSessionId: sessionId, idleTimeoutMs: options.timeoutMs ?? 600_000 }); await bridge.flush();
150
- const evidence = await inspect(bridge, sessionId, local); evidence.tool_calls = recordToolCalls(local, bridge, sessionId, cursor); await forwardUsage(local, sessionId, evidence.usage, evidence.tool_calls);
156
+ const evidence = await inspect(bridge, sessionId, local); evidence.tool_calls = recordToolCalls(local, bridge, sessionId, cursor); const usage_ingest_error = await forwardUsageBestEffort(local, sessionId, evidence.usage, evidence.tool_calls); if (initial_usage_error) evidence.initial_usage_error = initial_usage_error; if (usage_ingest_error) evidence.usage_ingest_error = usage_ingest_error;
151
157
  if (!options.allowBackground && (evidence.subagents?.subagents ?? []).length) { await cancelTree(bridge, sessionId); fail("background Grok Build subagents cannot outlive the one-shot dd-grok connection"); }
152
158
  return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, assistant_text: bridge.assistantTextSince(sessionId, textCursor), turn, profile: profileReceipt, evidence });
153
159
  }
154
160
 
155
161
  export async function inspectSession(options) { requireJournal(options); return await withBridge(options, async (bridge, initialized) => await inspectSessionWithBridge(bridge, { ...options, initialized })); }
156
- export async function inspectSessionWithBridge(bridge, options) { requireJournal(options); const sessionId = await identity(bridge, options); const evidence = await inspect(bridge, sessionId, options); evidence.tool_calls = sessionToolCalls(options, bridge, sessionId); await forwardUsage(options, sessionId, evidence.usage, evidence.tool_calls); return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, ...evidence }); }
162
+ export async function inspectSessionWithBridge(bridge, options) { requireJournal(options); const sessionId = await identity(bridge, options); const evidence = await inspect(bridge, sessionId, options); evidence.tool_calls = sessionToolCalls(options, bridge, sessionId); const usage_ingest_error = await forwardUsageBestEffort(options, sessionId, evidence.usage, evidence.tool_calls); if (usage_ingest_error) evidence.usage_ingest_error = usage_ingest_error; return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, ...evidence }); }
157
163
  export async function cancelSession(options) { requireJournal(options); return await withBridge(options, async (bridge) => await cancelSessionWithBridge(bridge, options)); }
158
164
  export async function cancelSessionWithBridge(bridge, options) { requireJournal(options); const sessionId = await identity(bridge, options); return receipt(options, { harness: "grok-acp", provider_session_id: sessionId, adapter_session_id: sessionId, ...await cancelTree(bridge, sessionId) }); }
159
165
  export async function forkSession(options) { requireJournal(options); return await withBridge(options, async (bridge) => await forkSessionWithBridge(bridge, options)); }
@@ -1,4 +1,5 @@
1
1
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
2
+ import { openCodeToolObservation, journalToolObservations } from "./tool-observations.mjs";
2
3
  import { observedTimeout } from "./observation-clock.mjs";
3
4
  import { createHash, randomBytes, randomUUID } from "node:crypto";
4
5
  import { appendFile, chmod, copyFile, lstat, mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
@@ -68,7 +69,7 @@ export class Runtime {
68
69
  persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, sessions: [...this.sessions.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.then(async () => { await writeJson(this.paths.state, snapshot); await heartbeatDaemonProcess(snapshot.config, snapshot.resource_process).catch(() => {}); await heartbeatDaemonProcess(snapshot.config, this.providerProcess).catch(() => {}); }); return this.persisting; }
69
70
  async journal(operation, result) { const line = `${JSON.stringify({ schema_id: "dd-opencode/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), operation, active_tree: this.state.active_tree, session_id: result?.provider_session_id ?? null })}\n`; for (const journal of new Set([this.paths.journal, this.state.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
70
71
  async childTree(id, seen = new Set()) { if (seen.has(id)) return []; seen.add(id); const direct = await this.client.children(id) ?? []; const nested = []; for (const child of direct) nested.push(child, ...await this.childTree(child.id, seen)); return nested; }
71
- async describe(id) { const [info, messages, children, statuses] = await Promise.all([this.client.inspect(id), this.client.messages(id), this.childTree(id), this.client.statuses()]); if (path.resolve(info.directory) !== path.resolve(this.state.config.cwd)) throw new DaemonError("foreign_session", "OpenCode Session directory is outside the controlled workspace", false, { session_directory: info.directory, workspace: this.state.config.cwd }); const status = statuses?.[id]?.type ?? "idle", parent = info.parentID ?? null, existing = this.sessions.get(id); const record = { provider_session_id: id, adapter_session_id: id, parent_provider_session_id: parent, root_provider_session_id: parent ? (this.sessions.get(parent)?.root_provider_session_id ?? parent) : (existing?.root_provider_session_id ?? id), cwd: info.directory }; this.sessions.set(id, record); for (const child of children) if (!this.sessions.has(child.id)) this.sessions.set(child.id, { provider_session_id: child.id, parent_provider_session_id: child.parentID ?? id, root_provider_session_id: record.root_provider_session_id, cwd: child.directory }); const unsettledChildren = children.filter(child => (statuses?.[child.id]?.type ?? "idle") !== "idle").map(child => child.id); const lastAssistant = messages.findLast(message => message.info?.role === "assistant"); const assistantText = (lastAssistant?.parts ?? []).filter(part => part.type === "text").map(part => part.text ?? "").join(""); await this.observeModels(id, messages, parent); return { ...record, observed_profile: { provider: lastAssistant?.info?.providerID ?? null, model: lastAssistant?.info?.modelID ?? null, reasoning: lastAssistant?.info?.variant ?? null, mode: info.agent ?? null }, assistant_text: assistantText, info, status, settled: status === "idle" && unsettledChildren.length === 0, unsettled_children: unsettledChildren, children, usage: usageSnapshot(messages), history_sha256: historyDigest(messages), message_count: messages.length }; }
72
+ async describe(id) { const [info, messages, children, statuses] = await Promise.all([this.client.inspect(id), this.client.messages(id), this.childTree(id), this.client.statuses()]); if (path.resolve(info.directory) !== path.resolve(this.state.config.cwd)) throw new DaemonError("foreign_session", "OpenCode Session directory is outside the controlled workspace", false, { session_directory: info.directory, workspace: this.state.config.cwd }); const status = statuses?.[id]?.type ?? "idle", parent = info.parentID ?? null, existing = this.sessions.get(id); const record = { provider_session_id: id, adapter_session_id: id, parent_provider_session_id: parent, root_provider_session_id: parent ? (this.sessions.get(parent)?.root_provider_session_id ?? parent) : (existing?.root_provider_session_id ?? id), cwd: info.directory }; this.sessions.set(id, record); for (const child of children) if (!this.sessions.has(child.id)) this.sessions.set(child.id, { provider_session_id: child.id, parent_provider_session_id: child.parentID ?? id, root_provider_session_id: record.root_provider_session_id, cwd: child.directory }); const unsettledChildren = children.filter(child => (statuses?.[child.id]?.type ?? "idle") !== "idle").map(child => child.id); const lastAssistant = messages.findLast(message => message.info?.role === "assistant"); const assistantText = (lastAssistant?.parts ?? []).filter(part => part.type === "text").map(part => part.text ?? "").join(""); await this.observeModels(id, messages, parent); this.journaledTools ??= new Map(); journalToolObservations(this.state.config.journal, messages.flatMap(message => (message.parts ?? []).map(part => openCodeToolObservation(part, id, message.info?.id))), this.journaledTools); return { ...record, observed_profile: { provider: lastAssistant?.info?.providerID ?? null, model: lastAssistant?.info?.modelID ?? null, reasoning: lastAssistant?.info?.variant ?? null, mode: info.agent ?? null }, assistant_text: assistantText, info, status, settled: status === "idle" && unsettledChildren.length === 0, unsettled_children: unsettledChildren, children, usage: usageSnapshot(messages, id), history_sha256: historyDigest(messages), message_count: messages.length }; }
72
73
  async observeModels(id, messages, parent = null) {
73
74
  this.modelMessages ??= new Set();
74
75
  for (const message of messages ?? []) {
@@ -82,7 +83,7 @@ export class Runtime {
82
83
  }
83
84
  }
84
85
  async requireSettled(id) { const value = await this.describe(id); if (!value.settled) throw new DaemonError("tree_not_settled", "OpenCode Session is not idle", false, { session_id: id, status: value.status }); return value; }
85
- async reconcileUsage(observed) { await forwardUsage(this.state.config, observed.provider_session_id, observed.usage); for (const child of observed.children.filter(item => (item.parentID ?? observed.provider_session_id) === observed.provider_session_id)) await this.reconcileUsage(await this.describe(child.id)); }
86
+ async reconcileUsage(observed) { const errors = []; try { await forwardUsage(this.state.config, observed.provider_session_id, observed.usage); } catch (error) { errors.push({ session_id: observed.provider_session_id, phase: "usage", ...errorPayload(error) }); } for (const child of observed.children.filter(item => (item.parentID ?? observed.provider_session_id) === observed.provider_session_id)) { try { errors.push(...await this.reconcileUsage(await this.describe(child.id))); } catch (error) { errors.push({ session_id: child.id, phase: "usage_child", ...errorPayload(error) }); } } return errors; }
86
87
  async productive(name, task) { if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true); this.active = name; try { await this.persist({ active_tree: true, active_operation: name }); const result = await task(); await this.persist({ active_tree: !result?.settled, active_operation: null }); await this.journal(name, result); return result; } catch (error) { await this.persist({ active_tree: true, active_operation: null }); await this.journal(`${name}.failed`, null); throw error; } finally { this.active = null; } }
87
88
  async cancelTree(id, seen = new Set()) {
88
89
  if (seen.has(id)) return [];
@@ -102,18 +103,52 @@ export class Runtime {
102
103
  const assertDispatch = captureDispatchGuard(this);
103
104
  if (operation === "daemon.status") { const { password, ...config } = this.state.config; return { daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, auth_status: this.state.auth_status, shutdown_state: this.state.shutdown_state, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config }; }
104
105
  if (operation === "daemon.doctor") { const doc = await this.client.request("GET", "/doc", undefined, { directory: null, timeoutMs: 5_000 }); const apiFingerprint = createHash("sha256").update(typeof doc === "string" ? doc : JSON.stringify(doc)).digest("hex"); if (apiFingerprint !== OPENCODE_BASELINE.api_fingerprint) throw new DaemonError("api_fingerprint_mismatch", "OpenCode API differs from the qualified baseline", false, { expected: OPENCODE_BASELINE.api_fingerprint, actual: apiFingerprint }); return { compatible: true, versions: this.state.versions, api_fingerprint: apiFingerprint, plugin_sha256: this.state.config.pluginSha256, auth_status: this.state.auth_status }; }
105
- if (operation === "session.create") return this.productive(operation, async () => { const info = await this.client.create(params.title); this.sessions.set(info.id, { provider_session_id: info.id, adapter_session_id: info.id, parent_provider_session_id: info.parentID ?? null, root_provider_session_id: info.id, cwd: info.directory }); const result = await this.describe(info.id); await this.reconcileUsage(result); return result; });
106
+ if (operation === "session.create") return this.productive(operation, async () => {
107
+ const info = await this.client.create(params.title);
108
+ const created = { provider_session_id: info.id, adapter_session_id: info.id, parent_provider_session_id: info.parentID ?? null, root_provider_session_id: info.id, cwd: info.directory };
109
+ this.sessions.set(info.id, created);
110
+ try {
111
+ const result = await this.describe(info.id);
112
+ result.usage_ingest_errors = await this.reconcileUsage(result);
113
+ return result;
114
+ } catch (error) {
115
+ // The Session exists even if its optional post-create observation is
116
+ // temporarily unavailable. Preserve the identity; a later inspect
117
+ // owns any profile/topology proof required before a prompt.
118
+ return { ...created, settled: false, settlement_observation_error: errorPayload(error) };
119
+ }
120
+ });
106
121
  if (operation === "session.inspect" || operation === "session.resume") { const result = await this.describe(params.sessionId); await this.persist(); return result; }
107
122
  if (operation === "session.prompt") { await this.requireSettled(params.sessionId); return this.productive(operation, async () => { assertDispatch(); const reply = await this.client.prompt(params.sessionId, { prompt: params.prompt, provider: this.state.config.provider, model: this.state.config.model, variant: this.state.config.variant, agent: this.state.config.agent,
108
123
  assertDispatch,
109
124
  onModelMessages: (id, messages, parent) => this.observeModels(id, messages, parent),
110
125
  onObservationError: error => observeModel({ journal: this.state.config.journal ?? this.paths.journal, harness: "opencode-server", sessionId: params.sessionId, requested: this.state.config, source: "native.message_observer", evidence: "unavailable", reason: error.code ?? "observation_unavailable" })
111
- }); const result = await this.describe(params.sessionId);
112
- result.assistant_text = reply.parts.filter(part => part.type === "text").map(part => part.text ?? "").join("");
126
+ }); const assistant_text = reply.parts.filter(part => part.type === "text").map(part => part.text ?? "").join("");
127
+ const reply_profile = { provider: reply.info.providerID ?? null, model: reply.info.modelID ?? null, reasoning: reply.info.variant ?? null, mode: this.state.config.agent ?? null };
128
+ checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: this.state.config.agent }, reply_profile);
129
+ let result;
130
+ try { result = await this.describe(params.sessionId); }
131
+ catch (error) { return { provider_session_id: params.sessionId, adapter_session_id: params.sessionId, assistant_text, response_message_id: reply.info.id, observed_profile: reply_profile, settled: false, settlement_observation_error: errorPayload(error) }; }
132
+ result.assistant_text = assistant_text;
113
133
  result.response_message_id = reply.info.id;
114
- result.observed_profile = { provider: reply.info.providerID ?? null, model: reply.info.modelID ?? null, reasoning: reply.info.variant ?? null, mode: result.info.agent ?? null };
115
- checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: this.state.config.agent }, result.observed_profile); await this.reconcileUsage(result); return result; }); }
116
- if (operation === "session.fork") { const source = await this.requireSettled(params.sessionId); return this.productive(operation, async () => { const info = await this.client.fork(params.sessionId, params.messageId); const fork = await this.describe(info.id); const sourceMessages = await this.client.messages(params.sessionId), forkMessages = await this.client.messages(info.id); const prefix = JSON.stringify(canonicalHistory(forkMessages)) === JSON.stringify(canonicalHistory(sourceMessages).slice(0, forkMessages.length)); if (!prefix) throw new DaemonError("fork_continuity_failed", "fork history is not a source prefix"); return { ...fork, fork_receipt: { source_provider_session_id: params.sessionId, source_history_sha256: source.history_sha256, fork_history_sha256: fork.history_sha256, history_prefix_proven: true } }; }); }
134
+ result.observed_profile = { ...reply_profile, mode: result.info.agent ?? reply_profile.mode };
135
+ checkObservedProfile({ ...this.state.config, reasoning: this.state.config.variant, mode: this.state.config.agent }, result.observed_profile); result.usage_ingest_errors = await this.reconcileUsage(result); return result; }); }
136
+ if (operation === "session.fork") { const source = await this.requireSettled(params.sessionId); return this.productive(operation, async () => {
137
+ const info = await this.client.fork(params.sessionId, params.messageId);
138
+ const created = { provider_session_id: info.id, adapter_session_id: info.id, parent_provider_session_id: info.parentID ?? params.sessionId, root_provider_session_id: this.sessions.get(params.sessionId)?.root_provider_session_id ?? params.sessionId, cwd: info.directory };
139
+ this.sessions.set(info.id, created);
140
+ try {
141
+ const fork = await this.describe(info.id);
142
+ const sourceMessages = await this.client.messages(params.sessionId), forkMessages = await this.client.messages(info.id);
143
+ const prefix = JSON.stringify(canonicalHistory(forkMessages)) === JSON.stringify(canonicalHistory(sourceMessages).slice(0, forkMessages.length));
144
+ if (!prefix) throw new DaemonError("fork_continuity_failed", "fork history is not a source prefix");
145
+ return { ...fork, fork_receipt: { source_provider_session_id: params.sessionId, source_history_sha256: source.history_sha256, fork_history_sha256: fork.history_sha256, history_prefix_proven: true } };
146
+ } catch (error) {
147
+ // A native fork is materialized already. Do not lose its identity or
148
+ // retry it, but never let an unproven history prefix become usable.
149
+ return { ...created, settled: false, fork_receipt: { source_provider_session_id: params.sessionId, source_history_sha256: source.history_sha256, history_prefix_proven: false }, fork_continuity_error: errorPayload(error) };
150
+ }
151
+ }); }
117
152
  if (operation === "session.cancel") {
118
153
  if (!this.sessions.has(params.sessionId)) throw new DaemonError("foreign_session", "cannot cancel a Session without daemon ownership");
119
154
  cancelPendingDispatch(this);
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { ToolObservations, openCodeToolObservation } from "./tool-observations.mjs";
2
3
  import { observedTimeout } from "./observation-clock.mjs";
3
4
 
4
5
  export class OpenCodeError extends Error {
@@ -89,9 +90,9 @@ export function canonicalHistory(messages) {
89
90
  return messages.map(({ info, parts }) => ({ role: info.role, ...(info.role === "assistant" ? { provider: info.providerID, model: info.modelID, mode: info.mode, finish: info.finish ?? null, error: info.error?.name ?? null } : {}), parts: parts.map(stablePart) }));
90
91
  }
91
92
  export function historyDigest(messages) { return createHash("sha256").update(JSON.stringify(canonicalHistory(messages))).digest("hex"); }
92
- export function usageSnapshot(messages) {
93
+ export function usageSnapshot(messages, sessionId) {
93
94
  const assistants = messages.filter((item) => item.info?.role === "assistant"); const tokens = assistants.reduce((sum, item) => { const value = item.info.tokens ?? {}; const cache = value.cache ?? {}; sum.input += value.input ?? 0; sum.output += value.output ?? 0; sum.reasoning += value.reasoning ?? 0; sum.cache_read += cache.read ?? 0; sum.cache_write += cache.write ?? 0; return sum; }, { input: 0, output: 0, reasoning: 0, cache_read: 0, cache_write: 0 });
94
- const calls = new Map(); let failures = 0; for (const item of messages) for (const part of item.parts ?? []) if (part.type === "tool") { calls.set(part.id ?? `${item.info.id}:${calls.size}`, part.tool ?? "unknown"); if (["error", "failed"].includes(part.state?.status)) failures += 1; }
95
- const by_tool = {}; for (const tool of calls.values()) by_tool[tool] = (by_tool[tool] ?? 0) + 1;
96
- return { input_tokens: tokens.input, output_tokens: tokens.output, reasoning_tokens: tokens.reasoning, cached_input_tokens: tokens.cache_read, cache_write_tokens: tokens.cache_write, total_tokens: tokens.input + tokens.output + tokens.reasoning + tokens.cache_read + tokens.cache_write, tool_calls: { total: calls.size, failures, by_tool }, cost_usd: assistants.reduce((sum, item) => sum + (item.info.cost ?? 0), 0) };
95
+ const tools = new ToolObservations();
96
+ for (const item of messages) for (const part of item.parts ?? []) tools.add(openCodeToolObservation(part, item.info?.sessionID ?? sessionId, item.info?.id));
97
+ return { input_tokens: tokens.input, output_tokens: tokens.output, reasoning_tokens: tokens.reasoning, cached_input_tokens: tokens.cache_read, cache_write_tokens: tokens.cache_write, total_tokens: tokens.input + tokens.output + tokens.reasoning + tokens.cache_read + tokens.cache_write, tool_calls: tools.summary(), cost_usd: assistants.reduce((sum, item) => sum + (item.info.cost ?? 0), 0) };
97
98
  }