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

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.123
4
+
5
+ ### Patch Changes
6
+
7
+ - Continue confirmed Codex overloads in the retained Session with bounded backoff, exact owner admission and no concurrent productive Turns.
8
+ - Retain content-addressed verification evidence and reviewer input baselines across publication, relocation and review waves.
9
+ - Preserve canonical native model/tool observations and AGY child transcript evidence without rewriting historical results.
10
+
3
11
  ## 0.9.0-beta.122
4
12
 
5
13
  ### Patch Changes
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.122",
4
- "cli_commit": "b03c699ac77fa596e684408c8e46e8710edbe9f6",
5
- "built_at": "2026-09-30T01:56:09.576Z",
3
+ "cli_version": "0.9.0-beta.123",
4
+ "cli_commit": "0d75e9e7e18868fbe9eee8d51e4c4d06ab03a8cd",
5
+ "built_at": "2026-09-30T12:08:03.924Z",
6
6
  "built_with_canon": {
7
7
  "version": "4.1.2",
8
- "commit": "0cf38231406614cfeb0a96001ba0201de2316e47",
8
+ "commit": "bbb3845083d7c1984bb61d813dd6700fe695180a",
9
9
  "flow_contract": "dd-flow-canonical-2026-08",
10
10
  "repo_root": "/home/runner/work/dd-flow-cli/dd-flow-cli/dd-memorybank",
11
11
  "memorybank_root": "/home/runner/work/dd-flow-cli/dd-flow-cli/dd-memorybank/.memory-bank",
@@ -75,6 +75,7 @@ import { launchRuntimeScopeControl, launchRuntimeScopeResume, prepareRuntimeScop
75
75
  import { prepareRuntimeScopeResumeRequest } from "../services/runtime-scope-resume.js";
76
76
  import { launchRunController, prepareRunControllerLaunch, resumeRunController, serveRunController, runControllerStatus, answerRunController, prepareRunControllerAnswer, supplyRunControllerContext, prepareRunControllerContext } from "../services/run-controller.js";
77
77
  import { controllerHitlAdmission } from "../services/run-controller-adapter.js";
78
+ import { assertCapacityOwnerAdmission } from "../services/codex-capacity.js";
78
79
  import { controlLifecycleReply, runControlStatus } from "../services/run-control.js";
79
80
  import { reconcileRunControl, requestRunControl, serveRunControl } from "../services/run-control-worker.js";
80
81
  import { startRuntimeService, prepareRuntimeService } from "../services/runtime-service.js";
@@ -2656,6 +2657,8 @@ async function dispatchRuntime(context, command, parsed, progress, prepared = {}
2656
2657
  const acceptedHitlPrompt = ["paused", "waiting_for_user"].includes(run?.status ?? "") && controllerHitlAdmission(context, record, { stateDir: requiredOption(parsed, "state-dir"), operationId: optionalOption(parsed, "operation-id"), operation: optionalOption(parsed, "operation"), nativeSessionId: optionalOption(parsed, "native-session-id"), promptSha256: optionalOption(parsed, "prompt-sha256") });
2657
2658
  if (!run || run.status !== "running" && !retainedHitlPrompt && !acceptedHitlPrompt && !["judge", "probe"].includes(metadata.role ?? ""))
2658
2659
  throw new AppError("invalid_run_state", "Subject dispatch requires a running RUN or its retained recovery/HITL prompt", 1);
2660
+ if (!recovering)
2661
+ assertCapacityOwnerAdmission(context, record, optionalOption(parsed, "operation-id"));
2659
2662
  }
2660
2663
  const budget = reserveProviderTurn(context, record, { operationId: optionalOption(parsed, "operation-id") ?? "", operation: optionalOption(parsed, "operation") ?? "", sessionId: optionalOption(parsed, "native-session-id"), daemonId: optionalOption(parsed, "daemon-id") });
2661
2664
  return { ok: true, ...budget, process_id: record.id, generation: metadata.run_generation ?? null };
@@ -1,5 +1,7 @@
1
1
  import { captureDispatchGuard, cancelPendingDispatch } from "./dispatch-fence.mjs";
2
- import { ToolObservations, agyToolObservation } from "./tool-observations.mjs";
2
+ import { ToolObservations, agyToolObservation, agyTranscriptObservations } from "./tool-observations.mjs";
3
+ import { constants } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
3
5
  import { createHash, randomUUID } from "node:crypto";
4
6
  import { appendFile, chmod, lstat, mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
5
7
  import { spawn } from "node:child_process";
@@ -16,6 +18,29 @@ import { prepareRuntimeOwner, cleanupFailedStart, cleanupFailedDaemonStart, conf
16
18
 
17
19
  const REQUEST_SCHEMA = "dd-agy/daemon-request@1", RESPONSE_SCHEMA = "dd-agy/daemon-response@1", STATE_SCHEMA = "dd-agy/daemon-state@1";
18
20
  class DaemonError extends AgyError {}
21
+ export async function captureAgyTranscript(runtimeRoot, child, rootSessionId, ownedSessionIds = [rootSessionId]) {
22
+ const session = { harness_id: "antigravity-cli", session_id: child.provider_session_id };
23
+ let handle;
24
+ try {
25
+ if (!rootSessionId || !child.provider_session_id || !ownedSessionIds.includes(child.parent_provider_session_id) || !child.log_uri || !/^[\w-]+$/.test(child.provider_session_id)) throw new Error("unbound native locator");
26
+ const expected = path.join(runtimeRoot, "brain", child.provider_session_id, ".system_generated", "logs", "transcript.jsonl");
27
+ const canonical = path.join(await realpath(runtimeRoot), "brain", child.provider_session_id, ".system_generated", "logs", "transcript.jsonl");
28
+ if (![expected, canonical].includes(fileURLToPath(child.log_uri)) || await realpath(expected) !== canonical) throw new Error("foreign or symlink native locator");
29
+ const before = await lstat(expected);
30
+ if (!before.isFile()) throw new Error("native locator is not a regular file");
31
+ handle = await open(expected, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
32
+ const stat = await handle.stat();
33
+ if (!before.isFile() || !stat.isFile() || stat.dev !== before.dev || stat.ino !== before.ino || await realpath(expected) !== canonical) throw new Error("native locator changed during open");
34
+ const maximum = 8 * 1024 * 1024, buffer = Buffer.alloc(Math.min(stat.size, maximum));
35
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
36
+ const bytes = buffer.subarray(0, bytesRead), prefix = bytes.subarray(0, bytes.lastIndexOf(10) + 1);
37
+ const source = { locator: child.log_uri, byte_start: 0, byte_end: prefix.length, sha256: createHash("sha256").update(prefix).digest("hex") };
38
+ const events = agyTranscriptObservations(prefix.toString("utf8"), child.provider_session_id, child.parent_provider_session_id ?? rootSessionId, source);
39
+ if (prefix.length !== bytesRead || stat.size > maximum) events.push({ session, reason: stat.size > maximum ? "agy_transcript_size_limit" : "agy_transcript_partial_tail" });
40
+ return events;
41
+ } catch (error) { return [{ session, reason: error.code === "ENOENT" ? "agy_transcript_missing" : "agy_transcript_locator_rejected" }]; }
42
+ finally { await handle?.close(); }
43
+ }
19
44
  const absolute = (value, label) => { if (!value || !path.isAbsolute(value)) throw new DaemonError("invalid_path", `${label} must be an absolute path`); return path.resolve(value); };
20
45
  function locations(stateDir) {
21
46
  const dir = absolute(stateDir, "--state-dir"), digest = createHash("sha256").update(dir).digest("hex").slice(0, 22), local = path.join(dir, "daemon.sock");
@@ -192,7 +217,7 @@ export async function startDaemon(options) {
192
217
  const version = (await runAgy(config.bin, ["--version"], { cwd: config.cwd, env: sanitizedEnv(config), timeoutMs: AGY_VERSION_TIMEOUT_MS })).stdout.trim();
193
218
  const resourceProcess = await registerDaemonProcess(config, { kind: "agy-daemon", owner: `agy:${path.basename(paths.dir)}`, operation: `agy-daemon:${path.basename(paths.dir)}`, stdout: paths.log, stderr: paths.log });
194
219
  const retained = retainedAgyState(previous, config.resumeSessionId);
195
- await writeJson(paths.state, { schema_id: STATE_SCHEMA, daemon_id: config.daemonId, shutdown_state: "starting", active_tree: false, unclaimed_activity: retained.unclaimed_activity ?? false, retained_tree_settlement: retained.retained_tree_settlement ?? null, versions: { agy: version }, config, sessions: [], session_observations: retained.session_observations ?? [], descendants: retained.descendants ?? [], tool_calls: retained.tool_calls ?? [], completed_tool_steps: retained.completed_tool_steps ?? [], tool_identity_complete: retained.tool_identity_complete !== false, turn_generation: retained.turn_generation ?? 0, last_result: retained.last_result ?? null, resource_process: resourceProcess });
220
+ await writeJson(paths.state, { schema_id: STATE_SCHEMA, daemon_id: config.daemonId, shutdown_state: "starting", active_tree: false, unclaimed_activity: retained.unclaimed_activity ?? false, retained_tree_settlement: retained.retained_tree_settlement ?? null, versions: { agy: version }, config, sessions: [], session_observations: retained.session_observations ?? [], descendants: retained.descendants ?? [], ...(Array.isArray(retained.tool_observations) ? { tool_observations: retained.tool_observations } : {}), tool_observation_gaps: retained.tool_observation_gaps ?? [], tool_calls: retained.tool_calls ?? [], completed_tool_steps: retained.completed_tool_steps ?? [], tool_identity_complete: retained.tool_identity_complete !== false, turn_generation: retained.turn_generation ?? 0, last_result: retained.last_result ?? null, resource_process: resourceProcess });
196
221
  const log = await open(paths.log, "a", 0o600), child = spawn(process.execPath, [config.entryPath, "daemon", "serve", "--state-dir", paths.dir], { cwd: config.cwd, env: sanitizedEnv(config), detached: true, stdio: ["ignore", log.fd, log.fd] }); child.unref(); await log.close();
197
222
  try {
198
223
  await confirmDaemonProcess(config, resourceProcess, child);
@@ -214,7 +239,7 @@ export const stopDaemon = async options => { const timeoutMs = options.timeoutMs
214
239
 
215
240
  export class Runtime {
216
241
  constructor(paths, state) { this.paths = paths; this.state = state; this.config = state.config; this.child = null; this.providerProcess = state.provider_process ?? null; this.providerFinalization = Promise.resolve(); this.buffer = ""; this.init = null; this.readyPromise = null; this.active = null; this.exited = false; this.toolObservations = new ToolObservations(); for (const reason of state.tool_observation_gaps ?? []) this.toolObservations.gaps.add(reason); for (const event of state.tool_observations ?? []) this.toolObservations.add(event); if (state.tool_calls?.length && !state.tool_observations) this.toolObservations.gaps.add("legacy_tools_not_attributed"); this.turnGeneration = state.turn_generation ?? 0; this.descendants = new Map((state.descendants ?? []).map(child => [child.provider_session_id, child])); this.lastResult = state.last_result ?? null; this.sessionObservations = new Map(state.session_observations ?? []); this.startupError = null; this.persisting = Promise.resolve(); this.draining = Promise.resolve(); this.lastActivityAt = state.last_activity_at ?? null; this.usageIngest = null; this.lastProviderEvent = null; }
217
- persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), last_activity_at: this.lastActivityAt, session_observations: [...this.sessionObservations], tool_observations: [...this.toolObservations.calls.values()], tool_observation_gaps: [...this.toolObservations.gaps], turn_generation: this.turnGeneration, descendants: [...this.descendants.values()], sessions: this.init ? [{ provider_session_id: this.init.conversation_id, root_provider_session_id: this.init.conversation_id, cwd: this.config.cwd }] : [] }); const snapshot = structuredClone(this.state); const queued = performance.now(); this.persisting = this.persisting.then(async () => { const started = performance.now(); await writeJson(this.paths.state, snapshot); this.persistTiming = { queue_ms: Math.round(started - queued), write_ms: Math.round(performance.now() - started) }; }); return this.persisting; }
242
+ persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, updated_at: new Date().toISOString(), last_activity_at: this.lastActivityAt, session_observations: [...this.sessionObservations], tool_observations: [...this.toolObservations.calls.values(), ...this.toolObservations.sources.values()], tool_observation_gaps: [...this.toolObservations.gaps], turn_generation: this.turnGeneration, descendants: [...this.descendants.values()], sessions: this.init ? [{ provider_session_id: this.init.conversation_id, root_provider_session_id: this.init.conversation_id, cwd: this.config.cwd }] : [] }); const snapshot = structuredClone(this.state); const queued = performance.now(); this.persisting = this.persisting.then(async () => { const started = performance.now(); await writeJson(this.paths.state, snapshot); this.persistTiming = { queue_ms: Math.round(started - queued), write_ms: Math.round(performance.now() - started) }; }); return this.persisting; }
218
243
  async journal(kind, payload = {}) { const line = `${JSON.stringify({ schema_id: "dd-agy/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), kind, ...payload })}\n`; for (const journal of new Set([this.paths.journal, this.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
219
244
  args(sessionId) { return ["--input-format", "stream-json", "--output-format", "stream-json", "--model", this.config.model, "--effort", this.config.reasoning, "--mode", this.config.mode, "--dangerously-skip-permissions", "--print-timeout", "6h", `--gemini_dir=${this.paths.gemini}`, "--app_data_dir=runtime", ...(sessionId ? ["--conversation", sessionId] : ["--new-project"])]; }
220
245
  requireSessionIdentity(requestedId) {
@@ -295,6 +320,26 @@ export class Runtime {
295
320
  this.observeProviderActivity(event); await this.journal("provider_event", { event }); if (event.event === "init") { this.init = { conversation_id: event.conversation_id, ...(event.init ?? {}) }; if (!this.init.conversation_id) throw new DaemonError("agy_init_missing", "Antigravity init has no conversation ID"); const requested = { provider: this.config.provider, model: this.config.model, reasoning: this.config.reasoning, mode: this.config.mode, permission_mode: "always-proceed" }; this.profile = assertProfile(requested, observedProfile(this.init, requested)); await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: this.init.conversation_id, requested, observed: this.profile.observed, source: "native.init" }); await this.persist({ shutdown_state: "running", active_tree: false }); readyResolve(this.init); } else if (event.event === "step_update") this.observeStep(event.step_update ?? {}); else if (event.event === "result") { if (this.startupError) throw this.startupError; await this.finishTurn(event.result ?? {}); } } }
296
321
  observeStep(step) { const sessionId = step.conversation_id ?? this.init?.conversation_id; const observation = this.sessionObservations.get(sessionId) ?? {}; if (Number.isInteger(step.step_index) && step.step_index > (observation.last_step_index ?? -1)) this.childActivity(sessionId); if (Number.isInteger(step.step_index)) this.sessionObservations.set(sessionId, { ...observation, last_step_index: Math.max(observation.last_step_index ?? -1, step.step_index) }); this.toolObservations.add(agyToolObservation(step, this.init?.conversation_id)); for (const child of step.subagent_info?.subagents ?? []) if (child.conversation_id) { const prior = this.descendants.get(child.conversation_id); this.descendants.set(child.conversation_id, { ...prior, provider_session_id: child.conversation_id, parent_provider_session_id: sessionId ?? null, role: child.role ?? null, subagent_type: child.type_name ?? null, log_uri: child.log_uri ?? null, workspace_uris: child.workspace_uris ?? [], status: prior?.status ?? "unknown" }); } }
297
322
  toolSnapshot() { const summary = this.toolObservations.summary(); const failures_by_name = {}; for (const event of this.toolObservations.calls.values()) if (event.status === "failed") failures_by_name[event.name] = (failures_by_name[event.name] ?? 0) + 1; return { ...summary, by_name: summary.by_tool, failures_by_name }; }
323
+ async captureChildTools() {
324
+ this.capturedToolFacts ??= new Map();
325
+ const root = this.init?.conversation_id ?? this.state.sessions?.[0]?.provider_session_id ?? this.lastResult?.conversation_id;
326
+ for (const child of this.descendants.values()) {
327
+ const ancestors = new Set(); let parent = child.parent_provider_session_id;
328
+ while (parent && parent !== root && this.descendants.has(parent) && !ancestors.has(parent)) {
329
+ ancestors.add(parent); parent = this.descendants.get(parent).parent_provider_session_id;
330
+ }
331
+ const events = parent === root && root
332
+ ? await captureAgyTranscript(this.paths.runtime, child, root, [root, ...ancestors])
333
+ : [{ session: { harness_id: "antigravity-cli", session_id: child.provider_session_id }, reason: "agy_transcript_ancestry_unproven" }];
334
+ for (const event of events) {
335
+ this.toolObservations.add(event);
336
+ const key = JSON.stringify([event.session, event.call_id, event.reason]), encoded = JSON.stringify(event);
337
+ if (this.capturedToolFacts.get(key) === encoded) continue;
338
+ await this.journal("tool_observation", { schema_id: "dd-flow/tool-observation@1", event });
339
+ this.capturedToolFacts.set(key, encoded);
340
+ }
341
+ }
342
+ }
298
343
  async finishTurn(result) {
299
344
  if (result.status === "RUNNING") { await this.persist({ active_tree: true }); return; }
300
345
  if (!["SUCCESS", "ERROR", "CANCELLED"].includes(result.status)) throw new DaemonError("agy_terminal_result_invalid", "Antigravity result has no recognized terminal status", false, { provider_result: result });
@@ -312,6 +357,7 @@ export class Runtime {
312
357
  // are not request IDs: native command rejections can report zero turns.
313
358
  // prompt() drains earlier events before accepting another input.
314
359
  if (!current && this.lastResult) { await this.journal("stale_terminal_observed", { result }); return; }
360
+ await this.captureChildTools();
315
361
  this.lastResult = result; this.markActivity();
316
362
  const rootStop = this.sessionObservations.get(this.init?.conversation_id)?.stop;
317
363
  const pendingTerminal = current && result.status === "SUCCESS";
@@ -438,7 +484,7 @@ export class Runtime {
438
484
  }
439
485
  async observeHook(event, payload, budget = null) { const conversationId = payload.conversationId ?? this.init?.conversation_id; const child = await this.childForHook(conversationId); budget?.remaining("identity"); const prior = this.sessionObservations.get(conversationId) ?? {}; const execution = Number.isInteger(payload.executionNum) ? payload.executionNum : null; const step = Number.isInteger(payload.stepIdx) ? payload.stepIdx : null; const floor = prior.step_floor; const stale = step !== null && ((Number.isInteger(floor) && step <= floor) || (child && Number.isInteger(prior.last_step_index) && step < prior.last_step_index)); if (stale) { await this.journal("stale_hook", { hook_event: event, conversation_id: conversationId, step_index: step }); return { stale: true }; } const fingerprint = createHash("sha256").update(JSON.stringify({ event, payload })).digest("hex"); if (!stale && prior.last_hook !== fingerprint) this.markActivity(); if (conversationId && !stale) this.sessionObservations.set(conversationId, { ...prior, ...(step !== null ? { last_step_index: Math.max(prior.last_step_index ?? -1, step) } : {}), last_hook: fingerprint, ...(event === "PreToolUse" ? { last_pre_hook: fingerprint } : {}), ...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}), ...(event === "Stop" ? { stop: { fullyIdle: payload.fullyIdle === true, terminationReason: payload.terminationReason ?? null, executionNum: payload.executionNum ?? null } } : {}) }); if (!stale && event === "PreToolUse" && prior.last_pre_hook !== fingerprint) this.childActivity(conversationId); if (child && !stale && event === "Stop" && payload.fullyIdle === true) this.descendants.set(conversationId, { ...this.descendants.get(conversationId), tree_settled: true, settlement_evidence: "child_fully_idle_stop" }); const terminalPending = this.active?.terminalResult === this.lastResult; if (!child && !stale && event === "Stop" && payload.fullyIdle === true && (!this.active || terminalPending) && ["SUCCESS", "ERROR", "CANCELLED"].includes(this.lastResult?.status)) for (const [id, descendant] of this.descendants) if (descendant.status === "unknown") this.descendants.set(id, { ...descendant, status: "settled_by_root", settlement_evidence: "root_fully_idle_stop" }); const parent = child?.parent_provider_session_id ?? null;
440
486
  const observed = payload.model ? observedProfile(payload) : {};
441
- await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: conversationId, parentSessionId: parent, requested: this.config, observed, source: "native.hook", evidence: payload.model ? "configured" : "unavailable", reason: payload.model ? null : "hook_omits_current_model" });
487
+ await observeModel({ journal: this.config.journal, harness: "antigravity-cli", sessionId: conversationId, parentSessionId: parent, requested: this.config, observed, source: { channel: "native.hook", scope: "configured", ...(!payload.model ? { non_asserting: true } : {}) }, evidence: payload.model ? "configured" : "unavailable", reason: payload.model ? null : "hook_omits_current_model" });
442
488
  const eventId = createHash("sha256").update(JSON.stringify({ daemon: this.config.daemonId, event, conversationId, fully_idle: payload.fullyIdle ?? null, termination_reason: payload.terminationReason ?? null, execution: payload.executionNum ?? null, step: payload.stepIdx ?? null, tool: payload.toolCall?.name ?? null, args: payload.toolCall?.args ?? null })).digest("hex"); await this.persist(); await this.journal("hook", { stale, hook_event: event, event_id: eventId, conversation_id: conversationId, turn_generation: this.turnGeneration, execution_num: execution, step_index: step, termination_reason: payload.terminationReason ?? null, fully_idle: payload.fullyIdle ?? null, persistence_timing: this.persistTiming ?? null }); await this.completePendingTurn(); return { event_id: eventId, daemon_id: this.config.daemonId, launch_cwd: this.config.cwd, parent_conversation_id: parent, profile: observed, requested_profile: { provider: this.config.provider, model: this.config.model, reasoning: this.config.reasoning, mode: this.config.mode }, project_root: this.config.projectRoot, dd_flow_bin: this.config.ddFlowBin, dd_flow_home: this.config.ddFlowHome };
443
489
  }
444
490
  async finalizeProviderForControl() {
@@ -462,7 +508,7 @@ export class Runtime {
462
508
  await this.finalizeProviderForControl();
463
509
  await finishDaemonProcess(this.config, providerProcess, "failed", reason);
464
510
  }
465
- async finishResource(state = "stopped", reason = "daemon_stopped") { await finishDaemonProcess(this.config, this.state.resource_process, state, reason); }
511
+ async finishResource(state = "stopped", reason = "daemon_stopped") { await this.captureChildTools(); await finishDaemonProcess(this.config, this.state.resource_process, state, reason); }
466
512
  }
467
513
 
468
514
  export async function serveDaemon(stateDir) {
@@ -485,7 +531,7 @@ async function dispatch(runtime, request) {
485
531
  if (request.operation === "daemon.status") { await runtime.observeHookFailures(); return { daemon_id: runtime.config.daemonId, shutdown_state: runtime.state.shutdown_state, active_tree: runtime.state.active_tree, active_operation: runtime.active?.operationId ?? null, last_hook_rejection: runtime.state.last_hook_rejection ?? null, last_activity_at: runtime.lastActivityAt, provider_ready: Boolean(runtime.init && !runtime.exited), pid: process.pid, provider_pid: runtime.child?.pid ?? null, versions: runtime.state.versions, config: runtime.config, sessions: runtime.state.sessions ?? [], receipt: runtime.receipt() }; }
486
532
  if (request.operation === "session.create") { if (params.sessionId) throw new DaemonError("invalid_create", "create does not accept a Session ID"); if (params.prompt) throw new DaemonError("invalid_create", "create does not execute a prompt; use session.prompt"); await runtime.start(); await runtime.zeroUsage(); return runtime.receipt(); }
487
533
  if (request.operation === "session.prompt") { const assertDispatch = captureDispatchGuard(runtime); await runtime.start(params.sessionId ?? null); runtime.requireSessionIdentity(params.sessionId); return await runtime.prompt(params.prompt, assertDispatch, request.id); }
488
- if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
534
+ if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); await runtime.captureChildTools(); return runtime.receipt(); }
489
535
  if (request.operation === "session.resume") { await runtime.start(params.sessionId ?? runtime.init?.conversation_id ?? null); runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
490
536
  if (request.operation === "session.cancel") { runtime.requireSessionIdentity(params.sessionId); return await runtime.cancel(); }
491
537
  if (request.operation === "hook.observe") {
@@ -9,7 +9,7 @@ import { spawn } from "node:child_process";
9
9
  import { waitForSettlement } from "./session-settlement.mjs";
10
10
  import { errorRecord } from "./operation-errors.mjs";
11
11
  import { CodexBridge, cancelSessionWithBridge, createSessionWithBridge, inspectSessionWithBridge, promptSessionWithBridge, startSessionWithBridge } from "./dd-codex.mjs";
12
- import { prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
12
+ import { assertDaemonOwnership, prepareRuntimeOwner, cleanupFailedDaemonStart, confirmDaemonProcess, confirmDaemonStopped, finishDaemonProcess, scheduleDaemonProcessHeartbeat, registerDaemonProcess, startManagedBridge, stopProcessGroup } from "./managed-daemon.mjs";
13
13
 
14
14
  const requestSchema = "dd-codex/daemon-request@1";
15
15
  const responseSchema = "dd-codex/daemon-response@1";
@@ -38,7 +38,7 @@ async function observeCodexSessions(bridge, options, sessions, active, remember
38
38
  }
39
39
  const unsettled = [], observations = [];
40
40
  for (const sessionId of sessions) {
41
- const observed = await inspectSessionWithBridge(bridge, { ...options, sessionId });
41
+ const observed = await inspectSessionWithBridge(bridge, { ...options, sessionId, turnId: sessionId === options.sessionId ? options.turnId : undefined });
42
42
  for (const child of observed.descendants ?? []) { sessions.add(child.provider_session_id); remember(child.provider_session_id); }
43
43
  const status = observed.status?.type ?? observed.status;
44
44
  const stored = ["notLoaded", "systemError"].includes(status) ? await bridge.request("thread/turns/list", { threadId: sessionId, limit: 1, sortDirection: "desc", itemsView: "summary" }, 5_000) : null;
@@ -101,7 +101,7 @@ export async function serveDaemon(stateDir) {
101
101
  bridge.close = async () => { await closeBridge(); await finishDaemonProcess(state.config, state.provider_process, "stopped", "daemon_stopped"); };
102
102
  scheduleDaemonProcessHeartbeat(state.config, state.resource_process);
103
103
  const save = (patch = {}) => { Object.assign(state, patch, { pid: process.pid, updated_at: new Date().toISOString(), sessions: [...sessions] }); const snapshot = structuredClone(state); persist = persist.then(() => writeState(paths.state, snapshot)); return persist; };
104
- const dispatch = async (operation, params) => {
104
+ const dispatch = async (operation, params, operationId) => {
105
105
  const assertDispatch = captureDispatchGuard(bridge);
106
106
  if (operation === "hook.verify") {
107
107
  if (params.daemonId !== state.daemon_id) throw Object.assign(new Error("Hook daemon identity differs"), { code: "native_hook_unproven" });
@@ -125,7 +125,23 @@ export async function serveDaemon(stateDir) {
125
125
  }
126
126
  if (active && !["session.inspect", "session.cancel"].includes(operation)) throw Object.assign(new Error(`${active} is in progress`), { code: "operation_busy", retryable: true });
127
127
  if (operation === "session.create") { active = operation; try { await save({ active_operation: active }); const result = await createSessionWithBridge(bridge, { ...state.config, ...params }); sessions.add(result.provider_session_id); return result; } finally { active = null; await save({ active_operation: null }); } }
128
- if (operation === "session.prompt") { sessions.add(params.sessionId); active = operation; try { await save({ active_operation: active }); return await promptSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch }); } finally { active = null; await save({ active_operation: null }); } }
128
+ if (operation === "session.prompt") {
129
+ // Admission can wait for a permit. Observe again after it returns, not
130
+ // just before the caller's backoff, so no child Turn races this dispatch.
131
+ // Reserve before the first await: distinct operation IDs are independent
132
+ // journal entries and must not both pass the idle check while inspecting.
133
+ active = operation;
134
+ try {
135
+ const tree = await inspectCodexTree(bridge, { ...state.config, ...params });
136
+ assertDispatch();
137
+ if (tree.settled !== true) throw Object.assign(new Error("native Session tree changed while waiting for admission"), { code: "tree_not_settled", details: { provider_session_id: params.sessionId, unsettled_sessions: tree.unsettled_sessions } });
138
+ if (/:capacity:[12]:session\.prompt$|:child:[a-f0-9]+:followup:[12]$/.test(operationId ?? ""))
139
+ await assertDaemonOwnership(paths.dir, { "operation-id": operationId, operation, "native-session-id": params.sessionId, "prompt-sha256": createHash("sha256").update(params.prompt ?? "").digest("hex") }, assertDispatch);
140
+ sessions.add(params.sessionId);
141
+ await save({ active_operation: active }); return await promptSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch });
142
+ }
143
+ finally { active = null; await save({ active_operation: null }); }
144
+ }
129
145
  if (operation === "session.start") { sessions.add(params.sessionId); active = operation; try { await save({ active_operation: active }); return await startSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch }); } finally { active = null; await save({ active_operation: null }); } }
130
146
  if (operation === "session.inspect" || operation === "session.resume") {
131
147
  const owned = new Set(sessions);
@@ -143,7 +159,7 @@ export async function serveDaemon(stateDir) {
143
159
  }
144
160
  throw Object.assign(new Error(`unknown operation: ${operation}`), { code: "unknown_operation" });
145
161
  };
146
- await removeSocket(paths.socket); server = net.createServer((connection) => { connection.setEncoding("utf8"); let buffer = ""; connection.on("data", (chunk) => { buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; const line = buffer.slice(0, at); buffer = buffer.slice(at + 1); void (async () => { let request; try { request = JSON.parse(line); if (request.schema_id !== requestSchema || !request.id || !request.operation) throw Object.assign(new Error("invalid daemon request"), { code: "daemon_protocol" }); const result = await durableDaemonDispatch(paths.dir, request, () => dispatch(request.operation, request.params ?? {}), () => dispatch("session.inspect", request.params ?? {})); connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`); if (result._shutdown) { await bridge.close(); await save({ shutdown_state: "clean", stopped_at: new Date().toISOString() }); await finishDaemonProcess(state.config, state.resource_process); server.close(() => process.exit(0)); await removeSocket(paths.socket); } } catch (error) { if (connection.writableEnded) { await save({ shutdown_state: "cleanup_failed", cleanup_error: errorPayload(error) }); return; } connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`); } })(); }); });
162
+ await removeSocket(paths.socket); server = net.createServer((connection) => { connection.setEncoding("utf8"); let buffer = ""; connection.on("data", (chunk) => { buffer += chunk; const at = buffer.indexOf("\n"); if (at < 0) return; const line = buffer.slice(0, at); buffer = buffer.slice(at + 1); void (async () => { let request; try { request = JSON.parse(line); if (request.schema_id !== requestSchema || !request.id || !request.operation) throw Object.assign(new Error("invalid daemon request"), { code: "daemon_protocol" }); const result = await durableDaemonDispatch(paths.dir, request, () => dispatch(request.operation, request.params ?? {}, request.id), () => dispatch("session.inspect", request.params ?? {})); connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request.id, ok: true, result: { ...result, _shutdown: undefined } })}\n`); if (result._shutdown) { await bridge.close(); await save({ shutdown_state: "clean", stopped_at: new Date().toISOString() }); await finishDaemonProcess(state.config, state.resource_process); server.close(() => process.exit(0)); await removeSocket(paths.socket); } } catch (error) { if (connection.writableEnded) { await save({ shutdown_state: "cleanup_failed", cleanup_error: errorPayload(error) }); return; } connection.end(`${JSON.stringify({ schema_id: responseSchema, id: request?.id ?? null, ok: false, error: errorPayload(error) })}\n`); } })(); }); });
147
163
  await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); }); await chmod(paths.socket, 0o600); await save({ shutdown_state: "running", active_operation: null }); return await new Promise(() => {});
148
164
  }
149
165
  import { authorizeRetainedDaemonResume } from "./driver-recovery.mjs";
@@ -542,6 +542,8 @@ function nativeTurnError(bridge, sessionId, turn) {
542
542
  function turnItemEvidence(turn) {
543
543
  const items = turn?.items;
544
544
  return Array.isArray(items) && turn?.itemsView !== "notLoaded" ? { observed: true, count: items.length, types: items.map(item => item?.type ?? "unknown"),
545
+ items: items.map(item => ({ id: item?.id ?? null, type: item?.type ?? "unknown", status: item?.status ?? null })),
546
+ pending: items.some(item => !["agentMessage", "reasoning", "userMessage"].includes(item?.type) && !["completed", "failed", "declined"].includes(item?.status)),
545
547
  possible_effects: items.some(item => !["agentMessage", "reasoning", "userMessage"].includes(item?.type)) } : { observed: false };
546
548
  }
547
549
 
@@ -615,7 +617,9 @@ export async function inspectSession(options) {
615
617
 
616
618
  export async function inspectSessionWithBridge(bridge, options) {
617
619
  const sessionId = required(options.sessionId, "--session-id"); const value = await bridge.request("thread/read", { threadId: sessionId, includeTurns: false }); const thread = value?.thread ?? value;
618
- return { harness: "codex-desktop", provider_session_id: sessionId, adapter_session_id: sessionId, status: thread.status ?? null, thread: threadReceipt(thread), descendants: await observedChildren(bridge, sessionId), requested_profile: profile(options), observed_profile: await observeCodexProfile(bridge, options, sessionId, { model: thread.model ?? null, reasoning: thread.reasoningEffort ?? null, provider: thread.modelProvider ?? null }, "thread/read") };
620
+ const terminal = options.turnId ? (await readTurn(bridge, sessionId, options.turnId, true))?.turn : null;
621
+ const exact = terminal?.id === options.turnId && terminal?.status === "failed" ? { terminal_turn_id: terminal.id, terminal_status: terminal.status, provider_error: nativeTurnError(bridge, sessionId, terminal), native_turn_items: turnItemEvidence(terminal) } : {};
622
+ return { harness: "codex-desktop", provider_session_id: sessionId, adapter_session_id: sessionId, status: thread.status ?? null, thread: threadReceipt(thread), ...exact, descendants: await observedChildren(bridge, sessionId), requested_profile: profile(options), observed_profile: await observeCodexProfile(bridge, options, sessionId, { model: thread.model ?? null, reasoning: thread.reasoningEffort ?? null, provider: thread.modelProvider ?? null }, "thread/read") };
619
623
  }
620
624
 
621
625
  export async function cancelSession(options) {
@@ -164,13 +164,15 @@ export class AcpBridge {
164
164
  const identity = this.modelSessions.get(adapterId); if (!identity) return;
165
165
  this.modelProfiles.set(identity.sessionId, observed);
166
166
  await observeModel({ journal: this.options.journal, harness: this.options.clientInfo?.name === "dd-grok" ? "grok-acp" : "zcode-acp",
167
- sessionId: identity.sessionId, parentSessionId: identity.parentSessionId, requested: identity.requested, observed, source });
167
+ sessionId: identity.sessionId, parentSessionId: identity.parentSessionId, requested: identity.requested, observed, source: typeof source === "string" ? { channel: source, scope: "configured", raw_model: observed.raw_model ?? observed.model, raw_provider: observed.provider } : source });
168
168
  }
169
169
  async observeModelUpdate(message) {
170
170
  const update = message.params?.update;
171
171
  if (update?.sessionUpdate !== "config_option_update" || !Array.isArray(update.configOptions)) return;
172
172
  const value = category => update.configOptions.find(option => option.type === "select" && (option.category === category || option.id === category))?.currentValue ?? null;
173
- const observed = { model: value("model"), reasoning: value("thought_level"), mode: value("mode") };
173
+ const raw = value("model");
174
+ const model = this.options.clientInfo?.name === "dd-grok" ? { model: raw } : zcodeModelSelector(raw);
175
+ const observed = { ...model, reasoning: value("thought_level"), mode: value("mode") };
174
176
  const identity = this.modelSessions.get(message.params.sessionId); if (!identity?.armed) return;
175
177
  checkObservedProfile(identity.requested, observed);
176
178
  await this.recordModel(message.params.sessionId, observed, "acp.config_option_update");
@@ -443,11 +445,20 @@ function isProductiveSessionNotification(message) {
443
445
  return true;
444
446
  }
445
447
 
448
+ // The qualified bridge encodes exactly providerId\\modelId; no other suffix or
449
+ // slash normalization is authorized. Bare IDs do not prove account routing.
450
+ export function zcodeModelSelector(raw, provider = null) {
451
+ if (typeof raw !== "string") return { model: null, provider, raw_model: raw };
452
+ const parts = raw.split("\\");
453
+ if (parts.length === 1) return { model: raw || null, provider, raw_model: raw };
454
+ if (parts.length !== 2 || !parts.every(Boolean) || provider && provider !== parts[0]) return { model: null, provider, raw_model: raw };
455
+ return { model: parts[1], provider: parts[0], raw_model: raw };
456
+ }
457
+
446
458
  export function observedProfile(read) {
447
459
  const model = read?.settings?.model?.current ?? {};
448
460
  return {
449
- provider: typeof model.providerId === "string" ? model.providerId : null,
450
- model: typeof model.modelId === "string" ? model.modelId : null,
461
+ ...zcodeModelSelector(model.modelId, typeof model.providerId === "string" ? model.providerId : null),
451
462
  reasoning: read?.settings?.thoughtLevel?.current ?? null,
452
463
  mode: read?.settings?.mode?.current ?? read?.projection?.mode ?? null,
453
464
  };
@@ -293,6 +293,7 @@ export async function stopProcessGroup(child, graceMs = 1_000) {
293
293
  // signal operation; there the direct child is the safe best effort.
294
294
  const target = process.platform === "win32" ? child.pid : -child.pid;
295
295
  const alive = () => { try { process.kill(target, 0); return true; } catch (error) { if (error.code === "ESRCH") return false; if (error.code === "EPERM") return true; throw error; } };
296
+ const signal = name => { try { process.kill(target, name); return true; } catch (error) { if (error.code === "ESRCH") return false; throw error; } };
296
297
  if (!alive()) return;
297
298
  if ((child.exitCode !== undefined && child.exitCode !== null) || (child.signalCode !== undefined && child.signalCode !== null)) {
298
299
  // The leader can exit a few ticks before its helpers. Observe their normal
@@ -302,11 +303,11 @@ export async function stopProcessGroup(child, graceMs = 1_000) {
302
303
  if (!alive()) return;
303
304
  throw Object.assign(new Error("process group remains after its leader exited; ownership cannot be proven from this child handle"), { code: "process_group_ownership_unknown", details: { pid: child.pid } });
304
305
  }
305
- process.kill(target, "SIGTERM");
306
+ if (!signal("SIGTERM")) return; // The group may exit after the liveness read.
306
307
  await new Promise((resolve) => setTimeout(resolve, graceMs));
307
308
  if (!alive()) return;
308
309
  if (child.exitCode != null || child.signalCode != null) return stopProcessGroup(child, graceMs);
309
- process.kill(target, "SIGKILL");
310
+ if (!signal("SIGKILL")) return;
310
311
  // Grace controls escalation, not the OS acknowledgement budget. A caller
311
312
  // testing immediate escalation must still allow the kernel to reap children.
312
313
  const deadline = Date.now() + Math.max(1_000, graceMs);
@@ -5,6 +5,7 @@ import { withRunnerLock } from "./runner-lock.mjs";
5
5
 
6
6
  const text = value => typeof value === "string" && value.trim() ? value.trim() : null;
7
7
  const fields = ["provider", "model", "reasoning", "mode", "permission_mode"];
8
+ const observationScope = source => JSON.stringify([source?.channel ?? null, source?.scope ?? null, source?.attempt_id ?? null, source?.turn_id ?? null, source?.non_asserting === true]);
8
9
  export const observedFields = value => Object.fromEntries(fields.map(key => [key, text(value?.[key])]));
9
10
  export const modelObservationFile = journal => path.join(path.dirname(journal), "model-observations.jsonl");
10
11
 
@@ -31,20 +32,23 @@ export async function readModelObservations(journal) {
31
32
  export async function observeModel({ journal, harness, sessionId, parentSessionId = null, requested = {}, observed = {}, source, evidence = "configured", reason = null, nativeTimestamp = null, previousModel = null }) {
32
33
  if (!journal || !sessionId) return null;
33
34
  if (!source || !["configured", "request", "response", "unavailable"].includes(evidence)) throw new Error("model observation requires a native source and evidence level");
35
+ if (typeof source === "string") source = { channel: source, ...(["configured", "response"].includes(evidence) ? { scope: evidence } : {}) };
34
36
  const file = modelObservationFile(journal), profile = observedFields(observed);
35
37
  await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
36
38
  return withRunnerLock(file, async () => {
37
39
  const events = await readModelObservations(journal);
38
- const replay = source?.event_sha256 ? events.find(item => item.harness === harness && item.session_id === sessionId && item.source?.event_sha256 === source.event_sha256) : null;
40
+ const replay = source?.event_sha256 ? events.find(item => item.harness === harness && item.session_id === sessionId && observationScope(item.source) === observationScope(source) && item.source?.event_sha256 === source.event_sha256) : null;
39
41
  if (replay) return replay;
40
- const channel = source?.channel ?? evidence;
41
- const previous = events.findLast(item => item.harness === harness && item.session_id === sessionId && (item.source?.channel ?? item.evidence) === channel
42
- && (item.source?.attempt_id ?? null) === (source?.attempt_id ?? null));
42
+ const previous = events.findLast(item => item.harness === harness && item.session_id === sessionId && observationScope(item.source) === observationScope(source));
43
43
  const normalizedReason = text(reason)?.slice(0, 200) ?? null;
44
- if (previous && JSON.stringify(previous.observed) === JSON.stringify(profile) && previous.reason === normalizedReason && previous.evidence === evidence) return previous;
45
- const previousKnown = events.findLast(item => item.harness === harness && item.session_id === sessionId && item.evidence === evidence && item.observed?.model);
44
+ if (previous && JSON.stringify(previous.observed) === JSON.stringify(profile) && previous.reason === normalizedReason && previous.evidence === evidence
45
+ && previous.source?.raw_model === source?.raw_model && previous.source?.raw_provider === source?.raw_provider) return previous;
46
+ const previousKnown = events.findLast(item => item.harness === harness && item.session_id === sessionId && item.evidence === evidence && item.observed?.model
47
+ && (item.source?.scope ?? "session") === (source?.scope ?? "session") && (item.source?.attempt_id ?? null) === (source?.attempt_id ?? null));
46
48
  const fromModel = text(previousModel) ?? previousKnown?.observed.model ?? null;
47
- const changed = Boolean(profile.model && fromModel && profile.model !== fromModel);
49
+ const previousRouting = events.findLast(item => item.harness === harness && item.session_id === sessionId && item.evidence === evidence && item.observed?.provider
50
+ && (item.source?.scope ?? "session") === (source?.scope ?? "session") && (item.source?.attempt_id ?? null) === (source?.attempt_id ?? null));
51
+ const changed = Boolean(profile.model && fromModel && (profile.model !== fromModel || profile.provider && previousRouting && profile.provider !== previousRouting.observed.provider));
48
52
  const value = { schema_id: "dd-flow/model-observation@1", sequence: events.length + 1,
49
53
  harness, session_id: sessionId, parent_session_id: parentSessionId,
50
54
  kind: changed ? "model_changed" : !profile.model ? "model_observation_unavailable" : "model_observed",
@@ -76,19 +80,22 @@ export function modelAttribution(events, expectedSessions = []) {
76
80
  const missingSessions = expectedSessions.filter(session => typeof session === "string" ? !observedIds.has(session)
77
81
  : !sessions.has(`${session.harness}:${session.session_id}`));
78
82
  const models = [...new Set(events.map(event => event.observed?.model).filter(Boolean))];
83
+ const providers = [...new Set(events.map(event => event.observed?.provider).filter(Boolean))];
79
84
  const incomplete = !events.length || missingSessions.length > 0 || [...sessions.values()].some(session => {
80
85
  const known = session.observations.filter(event => event.observed?.model);
81
86
  if (!known.length) return true;
82
87
  const knownScopes = new Set(known.map(event => `${event.evidence}:${event.source?.scope ?? "session"}:${event.source?.attempt_id ?? ""}:${event.source?.turn_id ?? ""}`));
83
88
  return session.observations.some(event => {
84
- if (event.evidence !== "unavailable") return false;
85
89
  if (event.source?.non_asserting) return false;
90
+ if (["configured", "response"].includes(event.evidence) && !event.observed?.model) return true;
91
+ if (event.evidence !== "unavailable") return false;
86
92
  const scope = event.source?.scope;
87
93
  return !scope || !knownScopes.has(`configured:${scope}:${event.source?.attempt_id ?? ""}:${event.source?.turn_id ?? ""}`)
88
94
  && !knownScopes.has(`response:${scope}:${event.source?.attempt_id ?? ""}:${event.source?.turn_id ?? ""}`);
89
95
  });
90
96
  });
91
- return { models, mixed: models.length > 1 || events.some(event => event.observed?.model && event.requested?.model && event.observed.model !== event.requested.model),
97
+ return { models, providers, mixed: models.length > 1 || providers.length > 1 || events.some(event => event.observed?.model && event.requested?.model && event.observed.model !== event.requested.model
98
+ || event.observed?.provider && event.requested?.provider && event.observed.provider !== event.requested.provider),
92
99
  observation_completeness: incomplete ? "incomplete" : "available_native_sources", transitions: events.filter(event => event.kind === "model_changed"),
93
100
  sessions: [...sessions.values()], missing_sessions: missingSessions, usage_attribution: "session_totals_unattributed_to_model", note: "Native settings/events do not prove undisclosed server routing or per-model token costs." };
94
101
  }
@@ -10,6 +10,7 @@ export class ToolObservations {
10
10
  calls: Map<string, ToolEvent>;
11
11
  turns: Map<string, { session: { harness_id: string; session_id: string }; turn_id: string; started: boolean; completed: boolean }>;
12
12
  gaps: Set<string>;
13
+ sources: Map<string, ToolEvent>;
13
14
  add(event: ToolEvent | null): void;
14
15
  observeTurn(event: { session: { harness_id: string; session_id: string }; turn_id: string; started?: boolean; completed?: boolean }): void;
15
16
  summary(sessionId?: string, cursor?: Set<string>): { total: number; failures: number; pending: number; by_tool: Record<string, number>; completeness: string; outcome_completeness: string; sessions: Array<{ session: { harness_id: string; session_id: string }; total: number; failures: number; pending: number; by_tool: Record<string, number> }>; reasons: string[] };
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  /** Canonical physical-session tool accounting. Native decoding lives below;
4
4
  * consumers never have to inspect provider envelopes or aggregate tokens. */
5
5
  export class ToolObservations {
6
- constructor() { this.calls = new Map(); this.turns = new Map(); this.gaps = new Set(); }
6
+ constructor() { this.calls = new Map(); this.turns = new Map(); this.gaps = new Set(); this.transcriptSessions = new Set(); this.sources = new Map(); }
7
7
  observeTurn(event) {
8
8
  if (!event?.session?.harness_id || !event.session.session_id || !event.turn_id) { this.gaps.add("turn_identity_missing"); return; }
9
9
  const key = JSON.stringify([event.session, event.turn_id]);
@@ -13,10 +13,19 @@ export class ToolObservations {
13
13
  }
14
14
  add(event) {
15
15
  if (!event) return;
16
- if (event.reason) { this.gaps.add(event.reason); return; }
16
+ if (event.reason) this.gaps.add(event.reason);
17
+ if (event.source_observed && event.session?.harness_id && event.session?.session_id) { this.sources.set(JSON.stringify(event.session), event); return; }
18
+ if (event.reason && !event.call_id) return;
17
19
  const { harness_id, session_id } = event.session ?? {};
18
20
  if (!harness_id || !session_id || !event.call_id) { this.gaps.add("tool_identity_missing"); return; }
19
21
  const key = JSON.stringify([harness_id, session_id, event.epoch ?? "", event.call_id]);
22
+ const physical = JSON.stringify([harness_id, session_id]);
23
+ if (event.source?.channel === "agy.native_transcript") {
24
+ this.transcriptSessions.add(physical);
25
+ for (const [oldKey, call] of this.calls) if (JSON.stringify([call.session.harness_id, call.session.session_id]) === physical && call.source?.channel !== "agy.native_transcript") {
26
+ this.calls.delete(oldKey); this.gaps.add("agy_live_transcript_correlation_unproven");
27
+ }
28
+ } else if (this.transcriptSessions.has(physical)) { this.gaps.add("agy_live_transcript_correlation_unproven"); return; }
20
29
  const old = this.calls.get(key);
21
30
  // Replay and late starts cannot undo a terminal outcome. Conflicting
22
31
  // terminal facts remain visible rather than silently choosing success.
@@ -28,12 +37,12 @@ export class ToolObservations {
28
37
  : old?.status === "unknown" || event.status === "unknown" ? "unknown" : "running" });
29
38
  }
30
39
  summary(sessionId, cursor) {
31
- const by_tool = {}, sessions = new Map(), reasons = new Set(this.gaps); let total = 0, failures = 0, pending = 0;
40
+ const by_tool = Object.create(null), sessions = new Map(), reasons = new Set(this.gaps); let total = 0, failures = 0, pending = 0;
32
41
  for (const [key, value] of this.calls) {
33
42
  if (sessionId !== undefined && value.session.session_id !== sessionId || cursor?.has(key)) continue;
34
43
  const id = JSON.stringify(value.session);
35
44
  if (value.name === "unknown") reasons.add("tool_start_missing");
36
- const session = sessions.get(id) ?? { session: value.session, parent_session: value.parent_session ?? null, total: 0, failures: 0, pending: 0, by_tool: {} };
45
+ const session = sessions.get(id) ?? { session: value.session, parent_session: value.parent_session ?? null, total: 0, failures: 0, pending: 0, by_tool: Object.create(null) };
37
46
  total++; session.total++; by_tool[value.name] = (by_tool[value.name] ?? 0) + 1; session.by_tool[value.name] = (session.by_tool[value.name] ?? 0) + 1;
38
47
  if (value.status === "failed") { failures++; session.failures++; }
39
48
  if (!["completed", "failed"].includes(value.status)) { pending++; session.pending++; }
@@ -45,6 +54,10 @@ export class ToolObservations {
45
54
  sessions.set(id, sessions.get(id) ?? { session: turn.session, parent_session: null, total: 0, failures: 0, pending: 0, by_tool: {} });
46
55
  if (!turn.started || !turn.completed) reasons.add("turn_lifecycle_incomplete");
47
56
  }
57
+ if (!cursor) for (const [id, event] of this.sources) {
58
+ if (sessionId !== undefined && event.session.session_id !== sessionId) continue;
59
+ sessions.set(id, sessions.get(id) ?? { session: event.session, parent_session: event.parent_session ?? null, total: 0, failures: 0, pending: 0, by_tool: {} });
60
+ }
48
61
  return { schema_id: "dd-flow/tool-summary@1", scope: "physical_sessions", measurement: "observed_invocations", origin: "native_tool_events", aggregation: cursor ? "delta" : "cumulative", total, failures, pending, by_tool,
49
62
  sessions: [...sessions.values()], completeness: reasons.size ? "partial" : "complete", outcome_completeness: pending ? "partial" : "complete", reasons: [...reasons] };
50
63
  }
@@ -56,7 +69,7 @@ export class ToolObservations {
56
69
  export function journalToolObservations(file, events, previous) {
57
70
  const batch = new ToolObservations();
58
71
  for (const event of events) batch.add(event);
59
- const records = [...batch.calls.values(), ...[...batch.gaps].map(reason => ({ reason }))];
72
+ const records = [...batch.calls.values(), ...batch.sources.values(), ...[...batch.gaps].map(reason => ({ reason }))];
60
73
  for (const event of records) {
61
74
  const key = JSON.stringify([event.session, event.epoch, event.call_id, event.reason]);
62
75
  const encoded = JSON.stringify(event);
@@ -67,7 +80,9 @@ export function journalToolObservations(file, events, previous) {
67
80
  }
68
81
 
69
82
  const identity = (harness_id, session_id) => ({ harness_id, session_id });
70
- const status = value => ["error", "failed", "ERROR"].includes(value) ? "failed" : ["completed", "success", "DONE"].includes(value) ? "completed" : "running";
83
+ const status = value => ["error", "failed", "ERROR"].includes(value) ? "failed" : ["completed", "success", "DONE"].includes(value) ? "completed"
84
+ : value === undefined || value === null || ["running", "pending", "in_progress", "inProgress", "RUNNING", "PENDING", "IN_PROGRESS"].includes(value) ? "running" : "unknown";
85
+ const nativeStatus = value => ({ status: status(value), ...(status(value) === "unknown" ? { reason: "tool_status_unsupported" } : {}) });
71
86
  export function zcodePhysicalSession(update, rootSessionId) {
72
87
  const native = update?._meta?.zcodeRuntime ?? {};
73
88
  if (native.source === "subagent" && !native.childSessionId) return null;
@@ -84,7 +99,7 @@ export function acpToolObservation(message, { harness = "zcode-acp", rootSession
84
99
  const session = harness === "zcode-acp" ? zcodePhysicalSession(update, root) : harness === "grok-acp" ? transport : root;
85
100
  if (!session || !update.toolCallId) return { reason: "tool_identity_missing" };
86
101
  return { session: identity(harness, session), parent_session: session !== root ? identity(harness, root) : null,
87
- transport_session_id: transport, call_id: update.toolCallId, name: update._meta?.["x.ai/tool"]?.name ?? update._meta?.claudeCode?.toolName ?? update.title?.split(":", 1)[0], status: status(update.status) };
102
+ transport_session_id: transport, call_id: update.toolCallId, name: update._meta?.["x.ai/tool"]?.name ?? update._meta?.claudeCode?.toolName ?? update.title?.split(":", 1)[0], ...nativeStatus(update.status) };
88
103
  }
89
104
 
90
105
  export function droidToolObservations(record, sessionId) {
@@ -130,15 +145,41 @@ export function codexAppServerObservation(message) {
130
145
  export function openCodeToolObservation(part, sessionId, messageId) {
131
146
  if (part.type !== "tool") return null;
132
147
  return { session: identity("opencode-server", part.sessionID ?? sessionId), call_id: part.callID ?? part.id,
133
- epoch: messageId, name: part.tool, status: status(part.state?.status) };
148
+ epoch: messageId, name: part.tool, ...nativeStatus(part.state?.status) };
134
149
  }
135
150
 
136
151
  export function agyToolObservation(step, rootSessionId) {
137
- if (step.step_type !== "tool") return null;
152
+ if (step.step_type !== "tool" && !(step.step_type === "subagent" && step.tool_name === "invoke_subagent")) return null;
138
153
  const sessionId = step.conversation_id ?? rootSessionId;
139
154
  return { session: identity("antigravity-cli", sessionId), parent_session: sessionId !== rootSessionId ? identity("antigravity-cli", rootSessionId) : null,
140
155
  call_id: step.step_id ?? step.stepId ?? step.id ?? step.tool_call_id ?? step.toolCallId ?? (Number.isInteger(step.step_index) ? String(step.step_index) : null),
141
- name: step.tool_name ?? step.tool_info?.name, status: step.tool_info?.error ? "failed" : status(step.state) };
156
+ name: step.tool_name ?? step.tool_info?.name, ...(step.tool_info?.error ? { status: "failed" } : nativeStatus(step.state)) };
157
+ }
158
+
159
+ /** Native AGY JSONL records have planner tool invocations, not matching live
160
+ * step IDs. GENERIC result text is deliberately not guessed into success. */
161
+ export function agyTranscriptObservations(text, sessionId, parentSessionId, source = {}) {
162
+ const events = [], lines = text.split("\n"); let recognized = false;
163
+ const session = identity("antigravity-cli", sessionId);
164
+ for (const [line, bytes] of lines.entries()) {
165
+ if (!bytes.trim()) continue;
166
+ let record; try { record = JSON.parse(bytes); } catch { events.push({ session, reason: line === lines.length - 1 ? "agy_transcript_partial_tail" : "agy_transcript_invalid_record" }); continue; }
167
+ if (!record || typeof record !== "object" || Array.isArray(record)) { events.push({ session, reason: "agy_transcript_shape_unsupported" }); continue; }
168
+ if (record.conversation_id && record.conversation_id !== sessionId) { events.push({ session, reason: "agy_transcript_session_mismatch" }); continue; }
169
+ if (!["SYSTEM_MESSAGE", "USER_INPUT", "PLANNER_RESPONSE", "GENERIC", "TOOL_RESPONSE", "CHECKPOINT"].includes(record.type)) { events.push({ session, reason: "agy_transcript_shape_unsupported" }); continue; }
170
+ recognized = true;
171
+ if (record.tool_calls !== undefined && !Array.isArray(record.tool_calls)) events.push({ session, reason: "agy_transcript_shape_unsupported" });
172
+ if (record.truncated || record.truncation || typeof record.content === "string" && /\[.*truncat|<.*truncat/i.test(record.content)) events.push({ session, reason: "agy_transcript_truncated" });
173
+ for (const [index, call] of (Array.isArray(record.tool_calls) ? record.tool_calls : []).entries()) {
174
+ if (!Number.isInteger(record.step_index) || !call || typeof call.name !== "string" || !call.name) { events.push({ session, reason: "agy_transcript_tool_identity_missing" }); continue; }
175
+ const id = call.id ?? call.tool_call_id;
176
+ events.push({ session, parent_session: identity("antigravity-cli", parentSessionId), epoch: "native_transcript", call_id: id ?? `${record.step_index}:${index}`, name: call.name,
177
+ status: "unknown", reason: "agy_transcript_tool_outcome_unproven", source: { ...source, channel: "agy.native_transcript", line: line + 1, step_index: record.step_index } });
178
+ }
179
+ }
180
+ if (!recognized) events.push({ session, reason: "agy_transcript_shape_unsupported" });
181
+ else events.push({ session, parent_session: identity("antigravity-cli", parentSessionId), source_observed: true, source: { ...source, channel: "agy.native_transcript" } });
182
+ return events;
142
183
  }
143
184
 
144
185
  /** Read-only replay, shared by accounting and offline diagnosis. A journal
@@ -150,7 +191,7 @@ export function replayToolJournal(file, options, reducer = new ToolObservations(
150
191
  for (const [index, line] of lines.entries()) {
151
192
  if (!line.trim()) continue;
152
193
  let e; try { e = JSON.parse(line); } catch { reducer.gaps.add(index === lines.length - 1 ? "journal_partial_tail" : "journal_invalid_record"); continue; }
153
- const add = event => { if (event) reducer.add({ ...event, source: { journal: file, line: index + 1 }, observed_at: e.observed_at ?? e.timestamp ?? e.at ?? e.ts ?? null }); };
194
+ const add = event => { if (event) reducer.add({ ...event, source: { ...event.source, journal: file, journal_line: index + 1 }, observed_at: e.observed_at ?? e.timestamp ?? e.at ?? e.ts ?? null }); };
154
195
  if (e.kind === "tool_observation" && e.schema_id === "dd-flow/tool-observation@1") { add(e.event); continue; }
155
196
  if (e.kind === "notification_failure") reducer.gaps.add("notification_failure");
156
197
  const m = e.payload ?? e;