@deksden-com/dd-flow-cli 0.9.0-beta.96 → 0.9.0-beta.98
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/dist/build-info.json +3 -3
- package/dist/harness-runtime/bin/dd-codex.mjs +2 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +1 -1
- package/dist/harness-runtime/bin/dd-zcode.mjs +2 -2
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +17 -6
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +4 -4
- package/dist/harness-runtime/lib/dd-grok.mjs +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +2 -2
- package/dist/services/controller-fanout.js +3 -1
- package/dist/services/eval-snapshots.js +114 -32
- package/dist/services/hooks.js +4 -3
- package/dist/services/native-session-control.js +1 -1
- package/dist/services/run-control.js +2 -2
- package/dist/services/run-controller-capture.js +6 -4
- package/dist/services/vnext-fanout.js +5 -1
- package/dist/services/work-registry.js +2 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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.98",
|
|
4
|
+
"cli_commit": "a06d4c113d22f31d344bfdf5b740ce5371da8ae2",
|
|
5
|
+
"built_at": "2026-09-22T19:40:49.454Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
7
|
"version": "4.1.1",
|
|
8
8
|
"commit": "678daa038287c948ada5b2d785a6dcc925c7b891",
|
|
@@ -3,8 +3,8 @@ import { durableDaemonStart, inspectDaemonOperation, interruptDaemonOperation, s
|
|
|
3
3
|
import { cancelSession, createSession, doctor, inspectSession, promptFromFile, promptSession, startSession } from "../lib/dd-codex.mjs";
|
|
4
4
|
import { callDaemon, serveDaemon, startDaemon, stopDaemon } from "../lib/dd-codex-daemon.mjs";
|
|
5
5
|
|
|
6
|
-
function parse(argv) { const positional = []; const options = {}; for (let i = 0; i < argv.length; i += 1) { const token = argv[i]; if (!token.startsWith("--")) { positional.push(token); continue; } const key = token.slice(2); if (["json", "cancel-tree"].includes(key)) { options[key] = true; continue; } const value = argv[++i]; if (value === undefined) throw new Error(`--${key} requires a value`); options[key] = value; } return { positional, options }; }
|
|
7
|
-
function common(options) { return { bin: options["codex-bin"], journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, sessionId: options["session-id"], turnId: options["turn-id"], model: options.model, reasoning: options.reasoning, ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], timeoutMs: options.timeout ? Number(options.timeout) * 1000 : undefined }; }
|
|
6
|
+
function parse(argv) { const positional = []; const options = {}; for (let i = 0; i < argv.length; i += 1) { const token = argv[i]; if (!token.startsWith("--")) { positional.push(token); continue; } const key = token.slice(2); if (["json", "cancel-tree", "no-flow"].includes(key)) { options[key] = true; continue; } const value = argv[++i]; if (value === undefined) throw new Error(`--${key} requires a value`); options[key] = value; } return { positional, options }; }
|
|
7
|
+
function common(options) { return { bin: options["codex-bin"], journal: options.journal, stateDir: options["state-dir"], cwd: options.cwd, sessionId: options["session-id"], turnId: options["turn-id"], model: options.model, reasoning: options.reasoning, ddFlowBin: options["dd-flow-bin"], ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], noFlow: options["no-flow"] === true, timeoutMs: options.timeout ? Number(options.timeout) * 1000 : undefined }; }
|
|
8
8
|
try {
|
|
9
9
|
const { positional, options } = parse(process.argv.slice(2)); const [family, command, modifier] = positional; const shared = common(options); let result;
|
|
10
10
|
if (family === "doctor") result = await doctor(shared);
|
|
@@ -27,7 +27,7 @@ try {
|
|
|
27
27
|
else if (family === "daemon" && command === "operation") result = await inspectDaemonOperation(config.stateDir, options["operation-id"]);
|
|
28
28
|
else if (family === "daemon" && command === "stop") result = await stopDaemon({ stateDir: config.stateDir, cancelTree: options["cancel-tree"] });
|
|
29
29
|
else if (family === "hook" && command === "handle") { const payload = JSON.parse(await input()); const eventId = randomUUID(); result = await callDaemon(config.stateDir, "hook.observe", { payload, eventId }, 25_000, eventId); }
|
|
30
|
-
else if (family === "session") { if (command === "fork") throw new Error("Droid fork is not qualified for this adapter"); const prompt = options["prompt-file"] ? await readFile(options["prompt-file"], "utf8") : options.prompt; result = await callDaemon(config.stateDir, `session.${command === "status" ? "inspect" : command}`, { sessionId: options["session-id"], ...(prompt ? { prompt } : {}) }); }
|
|
30
|
+
else if (family === "session") { if (command === "fork") throw new Error("Droid fork is not qualified for this adapter"); const prompt = options["prompt-file"] ? await readFile(options["prompt-file"], "utf8") : options.prompt; result = await callDaemon(config.stateDir, `session.${command === "status" ? "inspect" : command}`, { sessionId: options["session-id"], ...(config.noFlow ? { noFlow: true } : {}), ...(prompt ? { prompt } : {}) }); }
|
|
31
31
|
else throw new Error("usage: dd-droid doctor | daemon start|status|operation|stop | session create|resume|prompt|inspect|cancel");
|
|
32
32
|
process.stdout.write(`${JSON.stringify(family === "hook" ? { ...(result.hookSpecificOutput ? { hookSpecificOutput: result.hookSpecificOutput } : {}), ...(typeof result.continue === "boolean" ? { continue: result.continue } : {}), ...(result.stopReason ? { stopReason: result.stopReason } : {}) } : { ok: true, ...result })}\n`);
|
|
33
33
|
} catch (error) { process.stderr.write(`${JSON.stringify({ ok: false, error: errorRecord(error) })}\n`); process.exitCode = process.argv[2] === "hook" ? 2 : 1; }
|
|
@@ -9,7 +9,7 @@ function parse(argv) {
|
|
|
9
9
|
for (let index = 0; index < argv.length; index += 1) {
|
|
10
10
|
const token = argv[index];
|
|
11
11
|
if (!token.startsWith("--")) { positional.push(token); continue; }
|
|
12
|
-
const key = token.slice(2); if (["json", "cancel-tree"].includes(key)) { options[key] = true; continue; }
|
|
12
|
+
const key = token.slice(2); if (["json", "cancel-tree", "no-flow"].includes(key)) { options[key] = true; continue; }
|
|
13
13
|
const value = argv[++index]; if (value === undefined) throw new Error(`--${key} requires a value`); options[key] = value;
|
|
14
14
|
}
|
|
15
15
|
return { positional, options };
|
|
@@ -29,7 +29,7 @@ function common(options) {
|
|
|
29
29
|
childSessionId: options["child-session-id"],
|
|
30
30
|
model: options.model, reasoning: options.reasoning, mode: options.mode,
|
|
31
31
|
permission: options.permission ?? "deny", ddFlowBin: options["dd-flow-bin"],
|
|
32
|
-
ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"],
|
|
32
|
+
ddFlowHome: options["dd-flow-home"], projectRoot: options["project-root"], noFlow: options["no-flow"] === true,
|
|
33
33
|
env: options["dd-flow-home"] ? { DD_FLOW_HOME: options["dd-flow-home"] } : undefined,
|
|
34
34
|
timeoutMs: seconds("timeout"),
|
|
35
35
|
livenessTimeoutMs: seconds("liveness-timeout"),
|
|
@@ -276,7 +276,7 @@ export class Runtime {
|
|
|
276
276
|
// child can close while the asynchronous write is still pending.
|
|
277
277
|
if (!this.init && event.event === "result" && event.result?.status === "ERROR") this.startupError = new DaemonError("agy_provider_rejected", String(event.result.error ?? "Antigravity rejected the Session before initialization"), false, { provider_result: event.result });
|
|
278
278
|
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 ?? {}); } } }
|
|
279
|
-
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:
|
|
279
|
+
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" }); } }
|
|
280
280
|
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 }; }
|
|
281
281
|
async finishTurn(result) {
|
|
282
282
|
if (result.status === "RUNNING") { await this.persist({ active_tree: true }); return; }
|
|
@@ -341,7 +341,7 @@ export class Runtime {
|
|
|
341
341
|
return { harness: "antigravity-cli", runtime_family: "antigravity", provider_session_id: root ?? null, adapter_session_id: root ?? null, cwd: this.config.cwd, profile: this.profile, result, ...(assistantText === null ? {} : { assistant_text: assistantText }), usage: usageSnapshot(result ?? {}, this.toolSnapshot()), usage_ingest: this.usageIngest, descendants: [...this.descendants.values()], transcript_path: observation?.transcript_path ?? null, settled: Boolean(!this.state.unclaimed_activity && (!this.active || terminalPending) && result && ["SUCCESS", "ERROR", "CANCELLED"].includes(result.status) && treeSettled && rootSettled) };
|
|
342
342
|
}
|
|
343
343
|
async zeroUsage() { await forwardUsage(this.config, this.init.conversation_id, { usage: {} }, this.toolSnapshot(), []); }
|
|
344
|
-
async prompt(text, assertDispatch = captureDispatchGuard(this)) {
|
|
344
|
+
async prompt(text, assertDispatch = captureDispatchGuard(this), operationId = null) {
|
|
345
345
|
if (!text) throw new DaemonError("prompt_required", "prompt is required");
|
|
346
346
|
await this.start(); await this.draining;
|
|
347
347
|
if (this.active) throw new DaemonError("operation_in_progress", "an Antigravity turn is already running");
|
|
@@ -350,14 +350,14 @@ export class Runtime {
|
|
|
350
350
|
const reply = new Promise((resolve, reject) => { resolveTurn = resolve; rejectTurn = reject; });
|
|
351
351
|
// Cancellation can reject while admission persistence is still pending.
|
|
352
352
|
void reply.catch(() => {});
|
|
353
|
-
const current = { resolve: resolveTurn, reject: rejectTurn, generation: ++this.turnGeneration, previousError: this.lastResult?.error ?? null };
|
|
353
|
+
const current = { resolve: resolveTurn, reject: rejectTurn, generation: ++this.turnGeneration, operationId, previousError: this.lastResult?.error ?? null };
|
|
354
354
|
// Reserve before persistence so different operation IDs cannot overlap.
|
|
355
355
|
this.active = current;
|
|
356
356
|
const root = this.init?.conversation_id, prior = this.sessionObservations.get(root) ?? {};
|
|
357
357
|
this.sessionObservations.set(root, { ...prior, step_floor: prior.last_step_index ?? null, stop: null, last_hook: null });
|
|
358
358
|
this.lastProviderEvent = null;
|
|
359
359
|
try {
|
|
360
|
-
await this.persist({ active_tree: true });
|
|
360
|
+
await this.persist({ active_tree: true, last_hook_rejection: null });
|
|
361
361
|
assertDispatch();
|
|
362
362
|
if (this.active !== current) throw new DaemonError("operation_cancelled", "prompt reservation was superseded");
|
|
363
363
|
this.child.stdin.write(`${JSON.stringify({ event: "user", message: { content: text } })}\n`, error => {
|
|
@@ -378,8 +378,16 @@ export class Runtime {
|
|
|
378
378
|
while (!this.descendants.has(conversationId) && Date.now() < deadline) await new Promise(resolve => setTimeout(resolve, 10));
|
|
379
379
|
const child = this.descendants.get(conversationId);
|
|
380
380
|
if (!child) throw new DaemonError("agy_child_identity_unconfirmed", "Antigravity hook has no confirmed native parent", true, { conversation_id: conversationId });
|
|
381
|
+
if (child.parent_provider_session_id !== this.init?.conversation_id) throw new DaemonError("agy_child_parent_unqualified", "Nested Antigravity worker is not a direct child of the controlled Session", false, { conversation_id: conversationId, parent_conversation_id: child.parent_provider_session_id });
|
|
381
382
|
return child;
|
|
382
383
|
}
|
|
384
|
+
async rejectHook(error, event, payload) {
|
|
385
|
+
const current = this.active;
|
|
386
|
+
const rejection = { code: error.code ?? "agy_hook_rejected", message: error.message ?? String(error), event, conversation_id: payload.conversationId ?? null, tool: payload.toolCall?.name ?? null, daemon_id: this.config.daemonId, operation_id: current?.operationId ?? null, turn_generation: current?.generation ?? null };
|
|
387
|
+
try { await this.journal("hook_rejected", { rejection }); await this.persist({ last_hook_rejection: rejection }); }
|
|
388
|
+
catch (storageError) { if (current && this.active === current) { this.active = null; current.reject(storageError); } throw storageError; }
|
|
389
|
+
if (current && this.active === current) { this.active = null; current.reject(new DaemonError(rejection.code, rejection.message, false, rejection)); }
|
|
390
|
+
}
|
|
383
391
|
async observeHook(event, payload) { const conversationId = payload.conversationId ?? this.init?.conversation_id; const child = await this.childForHook(conversationId); 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;
|
|
384
392
|
const observed = payload.model ? observedProfile(payload) : {};
|
|
385
393
|
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" });
|
|
@@ -427,11 +435,14 @@ async function dispatch(runtime, request) {
|
|
|
427
435
|
if (request.schema_id !== REQUEST_SCHEMA) throw new DaemonError("daemon_protocol_mismatch", "unsupported dd-agy request schema"); const params = request.params ?? {};
|
|
428
436
|
if (request.operation === "daemon.status") return { daemon_id: runtime.config.daemonId, shutdown_state: runtime.state.shutdown_state, active_tree: runtime.state.active_tree, 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() };
|
|
429
437
|
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(); }
|
|
430
|
-
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); }
|
|
438
|
+
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); }
|
|
431
439
|
if (request.operation === "session.inspect") { runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
|
|
432
440
|
if (request.operation === "session.resume") { await runtime.start(params.sessionId ?? runtime.init?.conversation_id ?? null); runtime.requireSessionIdentity(params.sessionId); return runtime.receipt(); }
|
|
433
441
|
if (request.operation === "session.cancel") { runtime.requireSessionIdentity(params.sessionId); return await runtime.cancel(); }
|
|
434
|
-
if (request.operation === "hook.observe")
|
|
442
|
+
if (request.operation === "hook.observe") {
|
|
443
|
+
try { return await runtime.observeHook(params.event, params.payload ?? {}); }
|
|
444
|
+
catch (error) { await runtime.rejectHook(error, params.event, params.payload ?? {}); throw error; }
|
|
445
|
+
}
|
|
435
446
|
if (request.operation === "daemon.stop") { await runtime.close(params.cancelTree === true); await runtime.persist({ shutdown_state: "clean", active_tree: false }); await runtime.finishResource(); setImmediate(() => runtime.server?.close()); return { stopped: true, clean: true, settled: true, daemon_id: runtime.config.daemonId }; }
|
|
436
447
|
throw new DaemonError("unknown_operation", `unknown dd-agy daemon operation: ${request.operation}`);
|
|
437
448
|
}
|
|
@@ -63,7 +63,7 @@ export async function serveDaemon(stateDir) {
|
|
|
63
63
|
const params = request.params ?? {}, operation = request.operation;
|
|
64
64
|
if (operation === "daemon.status") { let observationError; try { await runtime.refreshTopology(); } catch (error) { observationError = errorRecord(error); } return { observation_error: observationError ?? null, daemon_id: state.daemon_id, pid: process.pid, config: state.config, shutdown_state: state.shutdown_state, provider_ready: Boolean(runtime.child && !runtime.closed), active_operation: busy, active_tree: Boolean(runtime.active || runtime.workingState !== "idle" || runtime.topologyIncomplete || [...runtime.topology.values()].some(child => !["completed", "cancelled", "failed"].includes(child.status))), sessions: runtime.rootId ? [runtime.rootId] : [], provider_pid: runtime.closed ? null : runtime.child?.pid }; }
|
|
65
65
|
if (operation === "hook.observe") return await runtime.observeHook(params.payload, params.eventId);
|
|
66
|
-
if (operation === "session.inspect") return await runtime.inspect(params.sessionId);
|
|
66
|
+
if (operation === "session.inspect") return await runtime.inspect(params.sessionId, { ingestUsage: params.noFlow !== true });
|
|
67
67
|
if (operation === "session.cancel") {
|
|
68
68
|
runtime.requireIdentity(params.sessionId); const cleanup = await runtime.closeTree(true); let inspected;
|
|
69
69
|
try { inspected = await runtime.inspect(params.sessionId, { ignoreActive: true }); }
|
|
@@ -372,13 +372,13 @@ export class DroidRuntime {
|
|
|
372
372
|
}
|
|
373
373
|
return [...this.topology.values()];
|
|
374
374
|
}
|
|
375
|
-
async snapshotUsage(id) {
|
|
375
|
+
async snapshotUsage(id, { ingest = true } = {}) {
|
|
376
376
|
const native = await readDroidSession(this.paths.factory, id); if (!native) return { status: "unavailable", provider_session_id: id };
|
|
377
377
|
this.journaledTools ??= new Map();
|
|
378
378
|
journalToolObservations(this.config.journal, [...native.records.flatMap(record => droidToolObservations(record, id)), ...native.tool_calls.reasons.map(reason => ({ reason }))], this.journaledTools);
|
|
379
379
|
const measured = native.usage;
|
|
380
380
|
const payload = { provider_session_id: id, daemon_id: this.config.daemonId, observed_at: new Date().toISOString(), usage_scope: "physical_session", completeness: native.complete && measured ? "complete" : "partial", usage: measured, tool_calls: native.tool_calls, source: { path: native.transcript_path, sha256: native.source_sha256 } };
|
|
381
|
-
if (measured && this.config.ddFlowBin) await this.flow("usage", "ingest", payload);
|
|
381
|
+
if (ingest && measured && this.config.ddFlowBin) await this.flow("usage", "ingest", payload);
|
|
382
382
|
return { status: measured ? "measured" : "unavailable", ...payload };
|
|
383
383
|
}
|
|
384
384
|
async flow(family, command, payload) {
|
|
@@ -390,9 +390,9 @@ export class DroidRuntime {
|
|
|
390
390
|
child.on("error", error => { clearTimeout(timer); reject(error); }); child.on("close", code => { clearTimeout(timer); if (code !== 0) return reject(new DroidError("droid_flow_rejected", stderr || "dd-flow rejected Droid evidence")); try { resolve(JSON.parse(stdout)); } catch (error) { reject(error); } }); child.stdin.end(`${JSON.stringify(payload)}\n`);
|
|
391
391
|
});
|
|
392
392
|
}
|
|
393
|
-
async inspect(id, { ignoreActive = false } = {}) {
|
|
393
|
+
async inspect(id, { ignoreActive = false, ingestUsage = true } = {}) {
|
|
394
394
|
this.requireIdentity(id); await this.captureProcesses(); const children = await this.refreshTopology(); const source = await readDroidSession(this.paths.factory, id);
|
|
395
|
-
const usage = []; for (const sessionId of [id, ...children.map(child => child.provider_session_id)]) usage.push(await this.snapshotUsage(sessionId));
|
|
395
|
+
const usage = []; for (const sessionId of [id, ...children.map(child => child.provider_session_id)]) usage.push(await this.snapshotUsage(sessionId, { ingest: ingestUsage }));
|
|
396
396
|
const settled = !this.topologyIncomplete && (!this.active || ignoreActive) && (this.closed || this.workingState === "idle") && children.every(child => terminal(child.status));
|
|
397
397
|
return { harness: "droid-cli", runtime_family: "droid", session: { harness_id: "droid-cli", session_id: id }, provider_session_id: id, adapter_session_id: id, cwd: this.config.cwd, observed_profile: source?.settings ? await this.observeSettings(id, source.settings, { path: source.transcript_path, sha256: source.source_sha256 }) : this.observedProfile, observed_runtime: { droid: this.config.version, factory_protocol: this.protocol ?? DROID_PROTOCOL, dd_harness_contract: DROID_CONTRACT }, status: settled ? "idle" : "running", settled, descendants: children, transcript_path: source?.transcript_path ?? null, usage, result: this.lastResult, provider_pid: this.closed ? null : this.child?.pid ?? null };
|
|
398
398
|
}
|
|
@@ -82,7 +82,7 @@ async function inspect(bridge, sessionId, options) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
async function forwardUsage(options, providerSessionId, usage, toolCalls) {
|
|
85
|
-
if (!options.ddFlowBin || !usage || usage.unavailable) return;
|
|
85
|
+
if (options.noFlow || !options.ddFlowBin || !usage || usage.unavailable) return;
|
|
86
86
|
const measured = usage.usage && typeof usage.usage === "object" ? usage.usage : usage;
|
|
87
87
|
const input = JSON.stringify({ provider_session_id: providerSessionId, daemon_id: options.daemonId ?? null, observed_at: new Date().toISOString(), usage: measured, tool_calls: toolCalls, usage_scope: usageScope(options, providerSessionId), completeness: measured.usageIsIncomplete ? "partial" : "complete" });
|
|
88
88
|
const target = executable(options.ddFlowBin, ["grok", "usage", "ingest", "--project-root", absolute(options.projectRoot, "--project-root"), "--json"]);
|
|
@@ -54,8 +54,8 @@ export const lifecycleRetryInstruction = "If the CLI rejects the call with effec
|
|
|
54
54
|
export function workerDelegationTask({ workId, startCommand, launchPolicy }) {
|
|
55
55
|
if (!workId || !startCommand) throw new Error("ready fan-out Work lacks its exact start command");
|
|
56
56
|
const context = launchPolicy === "fresh_agent_required"
|
|
57
|
-
? "
|
|
58
|
-
: "
|
|
57
|
+
? "You are the already-selected fresh leaf worker for this Work. Execute its lifecycle start yourself; do not delegate it to another child."
|
|
58
|
+
: "You are the already-selected worker. Execute this Work yourself; do not delegate it to another child.";
|
|
59
59
|
return [
|
|
60
60
|
`Complete one already-declared Work: ${workId}.`,
|
|
61
61
|
context,
|
|
@@ -31,7 +31,9 @@ export function controllerStageEntryPrompt(stage, command) {
|
|
|
31
31
|
}
|
|
32
32
|
/** The shared controller owns this former eval-only Work-graph decision. */
|
|
33
33
|
export async function nextControllerFanout(context, input) {
|
|
34
|
-
|
|
34
|
+
// Tree settlement is not a Work result. Keep these children visible to the
|
|
35
|
+
// association check, especially when a hook rejected work start before CLI.
|
|
36
|
+
const children = input.children;
|
|
35
37
|
const observations = { harness_id: input.harness, parent_session_id: input.sessionId, children };
|
|
36
38
|
if (children.length) {
|
|
37
39
|
const observed = observeVnextNativeChildren(context, { ...input, observations });
|
|
@@ -75,12 +75,13 @@ export function restoreEvalBootstrapSnapshot(_context, input, prepared = prepare
|
|
|
75
75
|
export function createEvalRunSnapshot(context, input) {
|
|
76
76
|
const { output } = prepareEvalRunSnapshotCreation(context, input);
|
|
77
77
|
const temporary = `${output}.partial-${crypto.randomUUID()}`;
|
|
78
|
+
let sourceVersion = null;
|
|
78
79
|
try {
|
|
79
80
|
input.boundary?.verify();
|
|
80
|
-
|
|
81
|
+
sourceVersion = input.boundary ? snapshotSourceVersion(context, input) : null;
|
|
81
82
|
const result = captureEvalRunSnapshot(context, { ...input, output: temporary });
|
|
82
83
|
if (input.boundary && sourceVersion) {
|
|
83
|
-
Object.assign(result, { consistency: "controller_boundary_writer_barrier@1", boundary_capture: { controller_id: input.boundary.controllerId, operation_id: input.boundary.operationId, boundary_key: input.boundary.boundaryKey, source_sha256: sourceVersion, observation: input.boundary.observation } });
|
|
84
|
+
Object.assign(result, { consistency: "controller_boundary_writer_barrier@1", boundary_capture: { controller_id: input.boundary.controllerId, operation_id: input.boundary.operationId, boundary_key: input.boundary.boundaryKey, source_sha256: sourceVersion.sha256, observation: input.boundary.observation } });
|
|
84
85
|
writeJson(path.join(temporary, "snapshot.json"), Object.fromEntries(Object.entries(result).filter(([key]) => key !== "ok" && key !== "snapshot")));
|
|
85
86
|
}
|
|
86
87
|
if (input.recoveryId) {
|
|
@@ -93,8 +94,20 @@ export function createEvalRunSnapshot(context, input) {
|
|
|
93
94
|
throw new AppError("snapshot_exists", "Snapshot output already exists", 1, { output });
|
|
94
95
|
syncSnapshotTree(temporary);
|
|
95
96
|
if (input.boundary) {
|
|
96
|
-
if (sourceVersion
|
|
97
|
-
|
|
97
|
+
if (sourceVersion) {
|
|
98
|
+
let after;
|
|
99
|
+
try {
|
|
100
|
+
after = snapshotSourceVersion(context, input, sourceVersion.databases);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error.code !== "ENOENT")
|
|
104
|
+
throw error;
|
|
105
|
+
const missing = error.path;
|
|
106
|
+
throw new AppError("snapshot_source_changed", "Snapshot source disappeared during boundary capture", 1, { components: [missing && path.isAbsolute(missing) && isInside(context.ddFlowHome, missing) ? `runtime/${path.relative(context.ddFlowHome, missing)}` : "source_path_disappeared"] });
|
|
107
|
+
}
|
|
108
|
+
if (sourceVersion.sha256 !== after.sha256)
|
|
109
|
+
throw new AppError("snapshot_source_changed", "Snapshot source files or Git state changed during boundary capture", 1, { components: snapshotSourceChanges(sourceVersion.entries, after.entries), changes: snapshotSourceChangeProofs(sourceVersion.entries, after.entries) });
|
|
110
|
+
}
|
|
98
111
|
input.boundary.verify();
|
|
99
112
|
}
|
|
100
113
|
fs.renameSync(temporary, output);
|
|
@@ -105,6 +118,10 @@ export function createEvalRunSnapshot(context, input) {
|
|
|
105
118
|
fs.rmSync(temporary, { recursive: true, force: true });
|
|
106
119
|
throw error;
|
|
107
120
|
}
|
|
121
|
+
finally {
|
|
122
|
+
for (const db of sourceVersion?.databases.values() ?? [])
|
|
123
|
+
db.close();
|
|
124
|
+
}
|
|
108
125
|
}
|
|
109
126
|
export function prepareEvalRunSnapshotCreation(context, input) {
|
|
110
127
|
if (Number(Boolean(input.candidate)) + Number(Boolean(input.incomplete)) + Number(input.stageEntry !== undefined) + Number(input.recoveryId !== undefined) !== 1)
|
|
@@ -801,23 +818,101 @@ function snapshotPayloadAllowed(source) {
|
|
|
801
818
|
function runtimePayloadAllowed(source) {
|
|
802
819
|
return snapshotPayloadAllowed(source) && !["auth.json", "auth.bk", "credentials.json", ".credentials.json", "credentials", ".credentials", "id_rsa", "id_ed25519", "engines", "checkouts"].includes(path.basename(source));
|
|
803
820
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
821
|
+
function sqliteFile(file) {
|
|
822
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile())
|
|
823
|
+
return false;
|
|
824
|
+
const descriptor = fs.openSync(file, "r");
|
|
825
|
+
try {
|
|
826
|
+
const header = Buffer.alloc(16);
|
|
827
|
+
return fs.readSync(descriptor, header, 0, 16, 0) === 16 && header.toString() === "SQLite format 3\0";
|
|
828
|
+
}
|
|
829
|
+
finally {
|
|
830
|
+
fs.closeSync(descriptor);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
/** A WAL is copied through its SQLite read snapshot, not as raw bytes. */
|
|
834
|
+
function runtimeSnapshotKind(file, excluded) {
|
|
835
|
+
if (!runtimePayloadAllowed(file) || excluded.includes(file))
|
|
836
|
+
return "excluded";
|
|
837
|
+
if (fs.lstatSync(file).isFile()) {
|
|
838
|
+
if (/-(?:wal|shm|journal)$/.test(file) && sqliteFile(file.replace(/-(?:wal|shm|journal)$/, "")))
|
|
839
|
+
return "sidecar";
|
|
840
|
+
if (sqliteFile(file))
|
|
841
|
+
return "sqlite";
|
|
842
|
+
}
|
|
843
|
+
return "ordinary";
|
|
844
|
+
}
|
|
845
|
+
/** Productive writers are fenced separately. Compare exactly the copied
|
|
846
|
+
* source set; SQLite serialization includes committed WAL pages without
|
|
847
|
+
* mistaking a physical checkpoint for new logical content. */
|
|
848
|
+
function snapshotSourceVersion(context, input, retainedDatabases) {
|
|
849
|
+
const databases = retainedDatabases ?? new Map();
|
|
807
850
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
808
851
|
const project = requireProjectByRoot(context, projectRoot);
|
|
809
852
|
const run = context.db.get("SELECT workspace_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
|
|
810
853
|
if (!run)
|
|
811
854
|
throw new AppError("not_found", "Snapshot RUN is not registered", 1);
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
855
|
+
const entries = new Map();
|
|
856
|
+
const excluded = codexHomeSnapshotExclusions(context, project.id, input.runId);
|
|
857
|
+
const visit = (directory) => {
|
|
858
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
859
|
+
const file = path.join(directory, entry.name);
|
|
860
|
+
const kind = runtimeSnapshotKind(file, excluded);
|
|
861
|
+
if (kind === "excluded" || kind === "sidecar")
|
|
862
|
+
continue;
|
|
863
|
+
const key = `runtime/${path.relative(context.ddFlowHome, file)}`;
|
|
864
|
+
const stat = fs.lstatSync(file);
|
|
865
|
+
if (stat.isDirectory()) {
|
|
866
|
+
entries.set(key, "directory");
|
|
867
|
+
visit(file);
|
|
868
|
+
}
|
|
869
|
+
else if (stat.isSymbolicLink())
|
|
870
|
+
entries.set(key, `link:${fs.readlinkSync(file)}`);
|
|
871
|
+
else if (kind === "sqlite") {
|
|
872
|
+
// Resource heartbeats are copied consistently but are not productive
|
|
873
|
+
// RUN writers; owner identity is fenced by boundary.verify().
|
|
874
|
+
if (file === path.join(context.ddFlowHome, "runtime.sqlite")) {
|
|
875
|
+
entries.set(key, `resource-sqlite:${stat.dev}:${stat.ino}:${stat.mode & 0o111}`);
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
let db = databases.get(file);
|
|
879
|
+
if (!db) {
|
|
880
|
+
db = new DatabaseSync(file, { readOnly: true });
|
|
881
|
+
databases.set(file, db);
|
|
882
|
+
}
|
|
883
|
+
entries.set(key, `sqlite:${crypto.createHash("sha256").update(db.serialize()).digest("hex")}:${stat.dev}:${stat.ino}:${stat.mode & 0o111}`);
|
|
884
|
+
}
|
|
885
|
+
else if (stat.isFile())
|
|
886
|
+
entries.set(key, `file:${crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex")}:${stat.mode & 0o111}`);
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
try {
|
|
890
|
+
visit(context.ddFlowHome);
|
|
891
|
+
for (const root of new Set([projectRoot, run.workspace_root]))
|
|
892
|
+
entries.set(`source/${root}`, JSON.stringify([
|
|
893
|
+
treeHash(root, true), treeModesHash(root, true), git(root, ["symbolic-ref", "--quiet", "HEAD"]), gitBytes(root, ["show-ref", "--head"]).toString("base64"), indexEntries(root).toString("base64")
|
|
894
|
+
]));
|
|
895
|
+
return { entries, databases, sha256: crypto.createHash("sha256").update(JSON.stringify([...entries].sort(([a], [b]) => a.localeCompare(b)))).digest("hex") };
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
if (!retainedDatabases)
|
|
899
|
+
for (const db of databases.values())
|
|
900
|
+
try {
|
|
901
|
+
db.close();
|
|
902
|
+
}
|
|
903
|
+
catch { /* Preserve the original source error. */ }
|
|
904
|
+
throw error;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
function snapshotSourceChanges(before, after) {
|
|
908
|
+
return [...new Set([...before.keys(), ...after.keys()])].filter(key => before.get(key) !== after.get(key)).sort().slice(0, 8);
|
|
909
|
+
}
|
|
910
|
+
function snapshotSourceChangeProofs(before, after) {
|
|
911
|
+
return snapshotSourceChanges(before, after).map(component => ({
|
|
912
|
+
component,
|
|
913
|
+
before: before.has(component) ? crypto.createHash("sha256").update(before.get(component)).digest("hex") : null,
|
|
914
|
+
after: after.has(component) ? crypto.createHash("sha256").update(after.get(component)).digest("hex") : null
|
|
915
|
+
}));
|
|
821
916
|
}
|
|
822
917
|
/** Managed Codex homes share host configuration, not owned snapshot payload. */
|
|
823
918
|
function codexHomeSnapshotExclusions(context, projectId, runId) {
|
|
@@ -856,23 +951,12 @@ function copyRuntimeTree(source, destination, scoped = true, excluded = []) {
|
|
|
856
951
|
if (isInside(root, physicalPath(destination)))
|
|
857
952
|
throw new AppError("snapshot_output_inside_runtime", "Recovery output must be outside its source artifact tree", 1);
|
|
858
953
|
const databases = [];
|
|
859
|
-
const sqlite = (file) => {
|
|
860
|
-
if (!fs.existsSync(file) || !fs.statSync(file).isFile())
|
|
861
|
-
return false;
|
|
862
|
-
const descriptor = fs.openSync(file, "r");
|
|
863
|
-
try {
|
|
864
|
-
const header = Buffer.alloc(16);
|
|
865
|
-
return fs.readSync(descriptor, header, 0, 16, 0) === 16 && header.toString() === "SQLite format 3\0";
|
|
866
|
-
}
|
|
867
|
-
finally {
|
|
868
|
-
fs.closeSync(descriptor);
|
|
869
|
-
}
|
|
870
|
-
};
|
|
871
954
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
872
955
|
fs.cpSync(source, destination, {
|
|
873
956
|
recursive: true, verbatimSymlinks: true,
|
|
874
957
|
filter: file => {
|
|
875
|
-
|
|
958
|
+
const kind = runtimeSnapshotKind(file, excluded);
|
|
959
|
+
if (kind === "excluded" || kind === "sidecar")
|
|
876
960
|
return false;
|
|
877
961
|
const stat = fs.lstatSync(file);
|
|
878
962
|
if (stat.isSymbolicLink()) {
|
|
@@ -883,9 +967,7 @@ function copyRuntimeTree(source, destination, scoped = true, excluded = []) {
|
|
|
883
967
|
if (!stat.isDirectory() && !stat.isFile())
|
|
884
968
|
return false;
|
|
885
969
|
if (stat.isFile()) {
|
|
886
|
-
if (
|
|
887
|
-
return false;
|
|
888
|
-
if (sqlite(file)) {
|
|
970
|
+
if (kind === "sqlite") {
|
|
889
971
|
databases.push(file);
|
|
890
972
|
return false;
|
|
891
973
|
}
|
package/dist/services/hooks.js
CHANGED
|
@@ -743,9 +743,10 @@ export function handleAgyEvent(context, input) {
|
|
|
743
743
|
throw new AppError("agy_session_identity_invalid", "Antigravity event is missing trusted physical identity", 1);
|
|
744
744
|
const rawInput = objectRecord(event.input);
|
|
745
745
|
const command = stringValue(rawInput.command) ?? stringValue(rawInput.cmd) ?? stringValue(rawInput.CommandLine);
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
746
|
+
const invalidStep = !Number.isSafeInteger(event.step_index) || Number(event.step_index) < 0;
|
|
747
|
+
const invalidExecution = event.execution_num !== undefined && (!Number.isSafeInteger(event.execution_num) || Number(event.execution_num) < 0);
|
|
748
|
+
if (event.phase === "before" && command && (lifecycleFacts(command) || continuationCommand(command)) && (invalidStep || invalidExecution)) {
|
|
749
|
+
throw new AppError("agy_native_call_identity_missing", "Participating Antigravity lifecycle calls require a native step identity and a valid execution identity when provided", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
|
|
749
750
|
}
|
|
750
751
|
const translated = {
|
|
751
752
|
schema_id: "dd-flow/opencode-tool-event@1", source_harness: "antigravity-cli", phase: event.phase, event_id: eventId,
|
|
@@ -29,7 +29,7 @@ export function assertNativeCancellationReceipt(value, sessionId) {
|
|
|
29
29
|
/** Native observation is shared by RUN and non-Work EVAL owners. */
|
|
30
30
|
export async function inspectControlledNativeSession(input) {
|
|
31
31
|
input.assertCurrent();
|
|
32
|
-
const receipt = await runHarnessAdapter({ executable: input.executable, args: ["session", "inspect", "--session-id", input.sessionId, "--state-dir", input.stateDir, "--json"], env: input.env, operationId: input.operationId, timeoutMs: 30_000 });
|
|
32
|
+
const receipt = await runHarnessAdapter({ executable: input.executable, args: ["session", "inspect", "--session-id", input.sessionId, "--state-dir", input.stateDir, ...(input.settlementObservation ? ["--no-flow"] : []), "--json"], env: input.env, operationId: input.operationId, timeoutMs: 30_000 });
|
|
33
33
|
if (adapterSessionId(receipt) !== input.sessionId)
|
|
34
34
|
throw new AppError("controller_operation_mismatch", "Session observation belongs to another target", 1);
|
|
35
35
|
if (input.harness && receipt.harness !== undefined && receipt.harness !== input.harness)
|
|
@@ -474,7 +474,7 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
|
|
|
474
474
|
: { session_id: session.id, state_dir: session.state_dir, harness, status: "pending", reason: "shutdown_session_unproven" };
|
|
475
475
|
if (!interrupt) {
|
|
476
476
|
try {
|
|
477
|
-
const receipt = await inspectControlledNativeSession({ executable, sessionId: session.id, stateDir: session.state_dir, harness, env, operationId: `${observationId}:inspect:${crypto.randomUUID()}`, assertCurrent });
|
|
477
|
+
const receipt = await inspectControlledNativeSession({ executable, sessionId: session.id, stateDir: session.state_dir, harness, env, operationId: `${observationId}:inspect:${crypto.randomUUID()}`, assertCurrent, settlementObservation: true });
|
|
478
478
|
return { session_id: session.id, state_dir: session.state_dir, harness: harness ?? (typeof receipt.harness === "string" ? receipt.harness : null), status: receipt.settled === true ? "completed" : "pending", receipt };
|
|
479
479
|
}
|
|
480
480
|
catch (error) {
|
|
@@ -511,7 +511,7 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
|
|
|
511
511
|
return { operation_id: operationId, session_id: session.id, status: "outcome_unknown", error: cancellationError };
|
|
512
512
|
}
|
|
513
513
|
try {
|
|
514
|
-
const observed = await inspectControlledNativeSession({ executable, sessionId: session.id, stateDir: session.state_dir, harness, env, operationId: `${observationId}:inspect:${crypto.randomUUID()}`, assertCurrent });
|
|
514
|
+
const observed = await inspectControlledNativeSession({ executable, sessionId: session.id, stateDir: session.state_dir, harness, env, operationId: `${observationId}:inspect:${crypto.randomUUID()}`, assertCurrent, settlementObservation: true });
|
|
515
515
|
return { operation_id: operationId, session_id: session.id, status: observed.settled === true ? "completed" : "pending", receipt: observed, cancellation, reused: !inserted.changes, ...(cancellationError ? { cancellation_error: cancellationError } : {}) };
|
|
516
516
|
}
|
|
517
517
|
catch (error) {
|
|
@@ -80,10 +80,11 @@ export async function captureControllerBoundary(context, row, manifest, stage, n
|
|
|
80
80
|
}
|
|
81
81
|
else {
|
|
82
82
|
let lastPending = "";
|
|
83
|
+
let lastCaptureMismatch = null;
|
|
83
84
|
for (;;) {
|
|
84
85
|
if (observationBudget.exhausted()) {
|
|
85
|
-
context.db.run("UPDATE run_controller_operations SET receipt_json = json_set(COALESCE(receipt_json, '{}'), '$.recovery_observation', json(?)), updated_at = ? WHERE operation_id = ?", [JSON.stringify(observationBudget.state()), context.now(), operationId]);
|
|
86
|
-
throw new AppError("recovery_observation_budget_exhausted", "Boundary capture has no stable settlement within the active observation budget", 1, { recovery_observation: observationBudget.state() });
|
|
86
|
+
context.db.run("UPDATE run_controller_operations SET receipt_json = json_set(json_set(COALESCE(receipt_json, '{}'), '$.recovery_observation', json(?)), '$.last_capture_mismatch', json(?)), updated_at = ? WHERE operation_id = ?", [JSON.stringify(observationBudget.state()), JSON.stringify(lastCaptureMismatch), context.now(), operationId]);
|
|
87
|
+
throw new AppError("recovery_observation_budget_exhausted", "Boundary capture has no stable settlement within the active observation budget", 1, { recovery_observation: observationBudget.state(), last_capture_mismatch: lastCaptureMismatch });
|
|
87
88
|
}
|
|
88
89
|
assertCurrent();
|
|
89
90
|
let verify = () => { throw new AppError("controller_boundary_unsettled", "Boundary has no stable writer observation", 1); };
|
|
@@ -99,14 +100,15 @@ export async function captureControllerBoundary(context, row, manifest, stage, n
|
|
|
99
100
|
throw error;
|
|
100
101
|
observation.settled = false;
|
|
101
102
|
observation.pending_reasons = [error.code];
|
|
103
|
+
lastCaptureMismatch = { code: error.code, components: error.details?.components ?? [], changes: error.details?.changes ?? [] };
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
106
|
observationBudget.exhausted();
|
|
105
|
-
const published = { ...observation, recovery_observation: observationBudget.state() };
|
|
107
|
+
const published = { ...observation, recovery_observation: observationBudget.state(), last_capture_mismatch: lastCaptureMismatch };
|
|
106
108
|
const pending = digest(observation.pending_reasons);
|
|
107
109
|
context.db.run("UPDATE run_controller_operations SET receipt_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify(published), context.now(), operationId]);
|
|
108
110
|
if (pending !== lastPending) {
|
|
109
|
-
appendEvent(context, row.controller_id, "boundary_capture_pending", { operation_id: operationId, pending_reasons: observation.pending_reasons });
|
|
111
|
+
appendEvent(context, row.controller_id, "boundary_capture_pending", { operation_id: operationId, pending_reasons: observation.pending_reasons, last_capture_mismatch: lastCaptureMismatch });
|
|
110
112
|
lastPending = pending;
|
|
111
113
|
}
|
|
112
114
|
await delay(observationBudget.nextDelay());
|
|
@@ -158,6 +158,10 @@ export async function reconcileVnextFanout(context, input) {
|
|
|
158
158
|
issues.push({ code: "child_descendants_unsettled", work_id: link.work_id });
|
|
159
159
|
continue;
|
|
160
160
|
}
|
|
161
|
+
if (child.status === "settled_by_root") {
|
|
162
|
+
issues.push({ code: "child_execution_outcome_unconfirmed", work_id: link.work_id, session_id: child.session_id });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
161
165
|
if (child.status === "completed") {
|
|
162
166
|
issues.push({ code: "execution_ended_without_work_result", work_id: link.work_id, session_id: child.session_id });
|
|
163
167
|
continue;
|
|
@@ -178,7 +182,7 @@ export async function reconcileVnextFanout(context, input) {
|
|
|
178
182
|
export function prepareNativeObservations(observations) {
|
|
179
183
|
const value = observations;
|
|
180
184
|
if (!value || typeof value.harness_id !== "string" || !value.harness_id.trim() || typeof value.parent_session_id !== "string" || !value.parent_session_id.trim() || !Array.isArray(value.children)
|
|
181
|
-
|| value.children.some(child => !child || typeof child.session_id !== "string" || !child.session_id.trim() || typeof child.status !== "string" || !["running", "unknown", "completed", "failed", "cancelled"].includes(child.status)))
|
|
185
|
+
|| value.children.some(child => !child || typeof child.session_id !== "string" || !child.session_id.trim() || typeof child.status !== "string" || !["running", "unknown", "completed", "failed", "cancelled", "settled_by_root"].includes(child.status)))
|
|
182
186
|
throw new AppError("usage", "Expected native observations: harness_id, parent_session_id and children[{session_id,status}]", 2);
|
|
183
187
|
if (new Set(value.children.map(child => child.session_id)).size !== value.children.length)
|
|
184
188
|
throw new AppError("usage", "Native observations must contain each child Session exactly once", 2);
|
|
@@ -880,6 +880,8 @@ function bindSession(context, work, run, identity, now) {
|
|
|
880
880
|
});
|
|
881
881
|
if (work.parent_work_id && !parentSession)
|
|
882
882
|
throw new AppError("parent_session_required", "Child Work requires a confirmed parent Work/Session link", 1, { work_id: work.work_id, parent_work_id: work.parent_work_id });
|
|
883
|
+
if (work.launch_policy === "fresh_agent_required" && inferredParentSession && identity.parentSessionId && identity.parentSessionId !== inferredParentSession)
|
|
884
|
+
throw new AppError("native_work_parent_mismatch", "Fresh native Work must be started by a direct child of its assigned parent Session", 1, { work_id: work.work_id, effect: "no_effect", ...identityDetails(), observed_provider_parent_session_key: identity.parentSessionId });
|
|
883
885
|
if (work.launch_policy === "fresh_agent_required" && (identity.sessionId === parentSession || context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ? LIMIT 1", [work.project_id, work.run_id, identity.sessionId])))
|
|
884
886
|
throw new AppError("fresh_session_required", "This Work requires a fresh Session in this RUN", 1, { work_id: work.work_id, effect: "no_effect", recoverable: true, handoff_required: true, ...identityDetails() });
|
|
885
887
|
const existing = context.db.get("SELECT session_id, parent_session_id, provider_parent_session_id FROM sessions WHERE project_id = ? AND session_id = ?", [work.project_id, identity.sessionId]);
|