@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 +8 -0
- package/dist/build-info.json +4 -4
- package/dist/cli/run-cli.js +3 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +52 -6
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +21 -5
- package/dist/harness-runtime/lib/dd-codex.mjs +5 -1
- package/dist/harness-runtime/lib/dd-zcode.mjs +15 -4
- package/dist/harness-runtime/lib/managed-daemon.mjs +3 -2
- package/dist/harness-runtime/lib/model-observations.mjs +16 -9
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +52 -11
- package/dist/services/codex-capacity.js +131 -12
- package/dist/services/eval-snapshots.js +13 -8
- package/dist/services/external-work-launch.js +27 -9
- package/dist/services/merge-server.js +26 -9
- package/dist/services/review-copy.js +21 -0
- package/dist/services/run-controller-adapter.js +25 -3
- package/dist/services/run-controller-recovery.js +9 -2
- package/dist/services/run-controller.js +59 -20
- package/dist/services/run-observations.js +7 -2
- package/dist/services/stage-report-renderer.js +25 -2
- package/dist/services/verification-matrix.js +27 -7
- package/dist/services/vnext-code-review.js +6 -4
- package/dist/services/vnext-plan-review.js +7 -2
- package/dist/services/work-registry.js +2 -0
- package/package.json +1 -1
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
|
package/dist/build-info.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-09-
|
|
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": "
|
|
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",
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -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") {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
45
|
-
|
|
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
|
|
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)
|
|
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 =
|
|
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"
|
|
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],
|
|
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,
|
|
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,
|
|
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,
|
|
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;
|