@deksden-com/dd-flow-cli 0.9.0-beta.97 → 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/lib/dd-agy-daemon.mjs +17 -6
- 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/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",
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -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]);
|