@deksden-com/dd-flow-cli 0.9.0-beta.33 → 0.9.0-beta.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @deksden-com/dd-flow-cli
2
2
 
3
+ ## 0.9.0-beta.35
4
+
5
+ ### Patch Changes
6
+
7
+ - Wait for npm registry propagation and verify the annotated tag created by Changesets before completing a guarded release.
8
+ - Keep AGY native outcomes separate from settlement, invalidate old child settlement on new activity, and reserve productive operations before persistence across adapters.
9
+ - Retain process-local native Stop observation in no-flow technical probes without installing project hooks or forwarding flow mutations.
10
+ - Publish Work receipts after the authoritative SQL commit with resumable publication intents; preserve conflicting bytes, resume partial retries, and retain non-restorable diagnostic snapshots when receipts disagree.
11
+ - Freeze the release source/canon tuple, reconcile uncertain publication, verify the isolated and exact global consumer, and retain phase receipts without creating another version on retry.
12
+
13
+ ## 0.9.0-beta.34
14
+
15
+ ### Patch Changes
16
+
17
+ - Create and verify the immutable annotated Git tag as part of the guarded release publication.
18
+
3
19
  ## 0.9.0-beta.33
4
20
 
5
21
  ### Patch Changes
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "cli_package": "@deksden-com/dd-flow-cli",
3
- "cli_version": "0.9.0-beta.33",
4
- "cli_commit": "29017c402962ffd27f468a1e88e44947f6513c0b",
5
- "built_at": "2026-09-07T19:23:48.837Z",
3
+ "cli_version": "0.9.0-beta.35",
4
+ "cli_commit": "e2ed74f88c272aa8269bd8be56b1f1d73467f4d7",
5
+ "built_at": "2026-09-07T21:56:49.561Z",
6
6
  "built_with_canon": {
7
7
  "version": "4.0.6",
8
8
  "commit": "c6fc50cb3b5526ea0162ee4454d1ee36f17be2da",
@@ -36,9 +36,15 @@ async function forwardUsage(config, sessionId, result, tools, descendants) {
36
36
  return { status: "ingested" };
37
37
  }
38
38
 
39
- async function prepare(paths, config) {
39
+ export async function prepare(paths, config) {
40
40
  for (const dir of [paths.dir, paths.gemini, paths.runtime, paths.config, paths.temporary]) { await mkdir(dir, { recursive: true, mode: 0o700 }); await chmod(dir, 0o700); }
41
- if (config.noFlow) return;
41
+ if (config.noFlow) {
42
+ // Technical probes still need native settlement, but must not install
43
+ // workspace hooks or forward any lifecycle mutation to dd-flow.
44
+ const command = `${shell(process.execPath)} ${shell(config.entryPath)} hook handle --event Stop --state-dir ${shell(paths.dir)} --json`;
45
+ await writeJson(path.join(paths.config, "hooks.json"), { "dd-flow-observer": { Stop: [{ type: "command", command, timeout: 30 }] } });
46
+ return;
47
+ }
42
48
  const command = `${shell(process.execPath)} ${shell(config.entryPath)} hook handle --event __EVENT__ --state-dir ${shell(paths.dir)} --project-root ${shell(config.projectRoot)} --dd-flow-bin ${shell(config.ddFlowBin)} --dd-flow-home ${shell(config.ddFlowHome)} --json`;
43
49
  const hooks = { "dd-flow": {
44
50
  PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command: command.replace("__EVENT__", "PreToolUse"), timeout: 30 }] }],
@@ -92,7 +98,7 @@ export async function callDaemon(stateDir, operation, params = {}, timeoutMs = 3
92
98
  const observed = Date.parse(status?.last_activity_at ?? "");
93
99
  const progressed = status?.active_tree === true && Number.isFinite(observed) && observed > lastActivity;
94
100
  if (progressed) lastActivity = observed;
95
- if (!clock.sample(progressed ? lastActivity : undefined)) return;
101
+ if (!clock.sample(progressed ? lastActivity : undefined, progressed ? lastActivity : undefined)) return;
96
102
  finish(new DaemonError(status?.active_tree ? "subject_liveness_timeout" : "operation_observation_lost", `${operation} has no recent productive provider activity`, true, {
97
103
  operation_id: request.id,
98
104
  last_activity_at: status?.last_activity_at ?? null,
@@ -204,7 +210,7 @@ export class Runtime {
204
210
  // child can close while the asynchronous write is still pending.
205
211
  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 });
206
212
  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 ?? {}); } } }
207
- observeStep(step) { const sessionId = step.conversation_id ?? this.init?.conversation_id; const observation = this.sessionObservations.get(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) }); if (step.step_type === "tool" && ["DONE", "ERROR"].includes(step.state)) { const stepId = step.step_id ?? step.stepId ?? step.id ?? step.tool_call_id ?? step.toolCallId ?? (Number.isInteger(step.step_index) ? `${sessionId}:${step.step_index}` : null); const duplicate = stepId && this.completedToolSteps.has(String(stepId)); if (!duplicate) { if (stepId) this.completedToolSteps.add(String(stepId)); else this.toolIdentityComplete = false; const name = step.tool_name ?? step.tool_info?.name ?? "unknown", current = this.toolCalls.get(name) ?? { total: 0, failures: 0 }; current.total += 1; if (step.state === "ERROR" || step.tool_info?.error) current.failures += 1; this.toolCalls.set(name, current); } } 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, { provider_session_id: child.conversation_id, parent_provider_session_id: this.init?.conversation_id ?? 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" }); } }
213
+ 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) }); if (step.step_type === "tool" && ["DONE", "ERROR"].includes(step.state)) { const stepId = step.step_id ?? step.stepId ?? step.id ?? step.tool_call_id ?? step.toolCallId ?? (Number.isInteger(step.step_index) ? `${sessionId}:${step.step_index}` : null); const duplicate = stepId && this.completedToolSteps.has(String(stepId)); if (!duplicate) { if (stepId) this.completedToolSteps.add(String(stepId)); else this.toolIdentityComplete = false; const name = step.tool_name ?? step.tool_info?.name ?? "unknown", current = this.toolCalls.get(name) ?? { total: 0, failures: 0 }; current.total += 1; if (step.state === "ERROR" || step.tool_info?.error) current.failures += 1; this.toolCalls.set(name, current); } } 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: this.init?.conversation_id ?? 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" }); } }
208
214
  toolSnapshot() { const byName = {}, failuresByName = {}; let total = 0, failures = 0; for (const [name, value] of this.toolCalls) { byName[name] = value.total; failuresByName[name] = value.failures; total += value.total; failures += value.failures; } return { total, failures, by_name: byName, failures_by_name: failuresByName, completeness: this.toolIdentityComplete ? "complete" : "partial" }; }
209
215
  async finishTurn(result) {
210
216
  if (result.status === "RUNNING") { await this.persist({ active_tree: true }); return; }
@@ -245,7 +251,7 @@ export class Runtime {
245
251
  receipt(result = this.lastResult) {
246
252
  const assistantText = typeof result?.response === "string" ? result.response : typeof result?.text === "string" ? result.text : null;
247
253
  const root = this.init?.conversation_id; const observation = this.sessionObservations.get(root);
248
- const treeSettled = [...this.descendants.values()].every(child => ["completed", "failed", "cancelled", "settled_by_root"].includes(child.status));
254
+ const treeSettled = [...this.descendants.values()].every(child => child.tree_settled === true || ["completed", "failed", "cancelled", "settled_by_root"].includes(child.status));
249
255
  // A terminal root result is the provider's authoritative end-of-turn
250
256
  // observation when AGY does not emit a root Stop hook. A received
251
257
  // non-idle Stop still wins, and every discovered child must be terminal.
@@ -254,7 +260,36 @@ export class Runtime {
254
260
  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.active || terminalPending) && result && ["SUCCESS", "ERROR", "CANCELLED"].includes(result.status) && treeSettled && rootSettled) };
255
261
  }
256
262
  async zeroUsage() { await forwardUsage(this.config, this.init.conversation_id, { usage: {} }, this.toolSnapshot(), []); }
257
- async prompt(text, assertDispatch = captureDispatchGuard(this)) { if (!text) throw new DaemonError("prompt_required", "prompt is required"); await this.start(); await this.draining; if (this.active) throw new DaemonError("operation_in_progress", "an Antigravity turn is already running"); if (this.lastResult && !this.receipt().settled) throw new DaemonError("tree_not_settled", "the prior Antigravity agent tree is not fully settled"); const root = this.init?.conversation_id; const prior = this.sessionObservations.get(root) ?? {}; this.turnGeneration++; this.sessionObservations.set(root, { ...prior, step_floor: prior.last_step_index ?? null, stop: null, last_hook: null }); this.lastProviderEvent = null; await this.persist({ active_tree: true }); return await new Promise((resolve, reject) => { assertDispatch(); this.active = { resolve, reject, generation: this.turnGeneration, previousError: this.lastResult?.error ?? null }; this.child.stdin.write(`${JSON.stringify({ event: "user", message: { content: text } })}\n`, error => { if (error) { this.active = null; reject(error); } }); }); }
263
+ async prompt(text, assertDispatch = captureDispatchGuard(this)) {
264
+ if (!text) throw new DaemonError("prompt_required", "prompt is required");
265
+ await this.start(); await this.draining;
266
+ if (this.active) throw new DaemonError("operation_in_progress", "an Antigravity turn is already running");
267
+ if (this.lastResult && !this.receipt().settled) throw new DaemonError("tree_not_settled", "the prior Antigravity agent tree is not fully settled");
268
+ let resolveTurn, rejectTurn;
269
+ const reply = new Promise((resolve, reject) => { resolveTurn = resolve; rejectTurn = reject; });
270
+ // Cancellation can reject while admission persistence is still pending.
271
+ void reply.catch(() => {});
272
+ const current = { resolve: resolveTurn, reject: rejectTurn, generation: ++this.turnGeneration, previousError: this.lastResult?.error ?? null };
273
+ // Reserve before persistence so different operation IDs cannot overlap.
274
+ this.active = current;
275
+ const root = this.init?.conversation_id, prior = this.sessionObservations.get(root) ?? {};
276
+ this.sessionObservations.set(root, { ...prior, step_floor: prior.last_step_index ?? null, stop: null, last_hook: null });
277
+ this.lastProviderEvent = null;
278
+ try {
279
+ await this.persist({ active_tree: true });
280
+ assertDispatch();
281
+ if (this.active !== current) throw new DaemonError("operation_cancelled", "prompt reservation was superseded");
282
+ this.child.stdin.write(`${JSON.stringify({ event: "user", message: { content: text } })}\n`, error => {
283
+ if (error) { if (this.active === current) this.active = null; current.reject(error); }
284
+ });
285
+ } catch (error) { if (this.active === current) this.active = null; current.reject(error); }
286
+ return await reply;
287
+ }
288
+ childActivity(sessionId) {
289
+ const child = this.descendants.get(sessionId);
290
+ if (!child) return;
291
+ this.descendants.set(sessionId, { ...child, previous_terminal_status: child.status === "unknown" ? child.previous_terminal_status : child.status, status: "unknown", tree_settled: false, settlement_evidence: null, activity_generation: this.turnGeneration });
292
+ }
258
293
  async childForHook(conversationId) {
259
294
  if (conversationId === this.init?.conversation_id) return null;
260
295
  const deadline = Date.now() + 1_000;
@@ -263,7 +298,7 @@ export class Runtime {
263
298
  if (!child) throw new DaemonError("agy_child_identity_unconfirmed", "Antigravity hook has no confirmed native parent", true, { conversation_id: conversationId });
264
299
  return child;
265
300
  }
266
- 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; 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, last_hook: fingerprint, ...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}), ...(event === "Stop" ? { stop: { fullyIdle: payload.fullyIdle === true, terminationReason: payload.terminationReason ?? null, executionNum: payload.executionNum ?? null } } : {}) }); if (child && !stale && event === "Stop" && payload.fullyIdle === true) this.descendants.set(conversationId, { ...child, status: "completed" }); 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 settled = terminalPending ? this.receipt(this.lastResult) : null; const completed = settled?.settled ? this.active : null; if (completed) this.active = null; const parent = child?.parent_provider_session_id ?? null;
301
+ 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)); 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, ...(payload.transcriptPath ? { transcript_path: payload.transcriptPath } : {}), ...(event === "Stop" ? { stop: { fullyIdle: payload.fullyIdle === true, terminationReason: payload.terminationReason ?? null, executionNum: payload.executionNum ?? null } } : {}) }); if (child && !stale && event === "PreToolUse" && prior.last_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 settled = terminalPending ? this.receipt(this.lastResult) : null; const completed = settled?.settled ? this.active : null; if (completed) this.active = null; const parent = child?.parent_provider_session_id ?? null;
267
302
  const observed = payload.model ? observedProfile(payload) : {};
268
303
  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" });
269
304
  const eventId = createHash("sha256").update(JSON.stringify({ daemon: this.config.daemonId, event, conversationId, turn_generation: this.turnGeneration, 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(completed ? { active_tree: false } : {}); 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 }); completed?.resolve(settled); return { event_id: eventId, daemon_id: this.config.daemonId, 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 };
@@ -87,9 +87,9 @@ export async function serveDaemon(stateDir) {
87
87
  return { stopped: true, clean: true, _shutdown: true };
88
88
  }
89
89
  if (active && !["session.inspect", "session.cancel"].includes(operation)) throw Object.assign(new Error(`${active} is in progress`), { code: "operation_busy", retryable: true });
90
- if (operation === "session.create") { active = operation; await save({ active_operation: active }); try { const result = await createSessionWithBridge(bridge, { ...state.config, ...params }); sessions.add(result.provider_session_id); return result; } finally { active = null; await save({ active_operation: null }); } }
91
- if (operation === "session.prompt") { sessions.add(params.sessionId); active = operation; await save({ active_operation: active }); try { return await promptSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch }); } finally { active = null; await save({ active_operation: null }); } }
92
- if (operation === "session.start") { sessions.add(params.sessionId); active = operation; await save({ active_operation: active }); try { return await startSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch }); } finally { active = null; await save({ active_operation: null }); } }
90
+ 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 }); } }
91
+ if (operation === "session.prompt") { sessions.add(params.sessionId); active = operation; try { await save({ active_operation: active }); return await promptSessionWithBridge(bridge, { ...state.config, ...params, assertDispatch }); } finally { active = null; await save({ active_operation: null }); } }
92
+ 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 }); } }
93
93
  if (operation === "session.inspect") return await inspectSessionWithBridge(bridge, { ...state.config, ...params });
94
94
  if (operation === "session.cancel") { if (!sessions.has(params.sessionId)) throw Object.assign(new Error("cancellation Session is not owned by this daemon"), { code: "session_identity_mismatch" }); cancelPendingDispatch(bridge); return await cancelSessionWithBridge(bridge, { ...state.config, ...params }); }
95
95
  throw Object.assign(new Error(`unknown operation: ${operation}`), { code: "unknown_operation" });
@@ -85,8 +85,9 @@ export async function serveDaemon(stateDir) {
85
85
  catch (error) { stopping = false; await save({ shutdown_state: "cleanup_failed", cleanup_error: errorRecord(error) }); throw error; }
86
86
  }
87
87
  if (busy || stopping) throw new DroidError("operation_busy", "Another Droid operation is active");
88
- busy = operation; await save({ active_operation: busy });
88
+ busy = operation;
89
89
  try {
90
+ await save({ active_operation: busy });
90
91
  if (operation === "session.create") { if (params.prompt) throw new DroidError("invalid_create", "create does not execute a prompt"); return await runtime.create(request.id); }
91
92
  if (operation === "session.resume") return await runtime.resume(params.sessionId);
92
93
  if (["session.prompt", "session.start"].includes(operation)) return await runtime.prompt(params.sessionId, params.prompt, request.id);
@@ -241,7 +241,7 @@ export class Runtime {
241
241
  return running;
242
242
  }
243
243
  async requireSettled() { const running = await this.refreshTree(); if (running.length) throw new DaemonError("tree_not_settled", "daemon still owns a running Session tree", false, { sessions: running }); }
244
- async productive(name, task) { if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true); this.active = name; for (const session of this.sessions.values()) session.native_root_receipt = null; await this.persist({ active_tree: true, active_operation: name }); try { const result = await task(); this.track(result); const session = this.sessions.get(result.provider_session_id); if (session && (["session.create", "session.fork"].includes(name) || ["end_turn", "cancelled"].includes(result.turn?.stopReason))) session.native_root_receipt = { kind: name === "session.prompt" ? result.turn.stopReason : "created", daemon_id: this.state.daemon_id }; await this.persist({ active_tree: this.rootRunning(result) || this.running(result).length > 0, active_operation: null }); return { ...result, descendants: this.topology(result.provider_session_id) }; } catch (error) { await this.persist({ active_tree: true, active_operation: null }); throw error; } finally { this.active = null; } }
244
+ async productive(name, task) { if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true); this.active = name; for (const session of this.sessions.values()) session.native_root_receipt = null; try { await this.persist({ active_tree: true, active_operation: name }); const result = await task(); this.track(result); const session = this.sessions.get(result.provider_session_id); if (session && (["session.create", "session.fork"].includes(name) || ["end_turn", "cancelled"].includes(result.turn?.stopReason))) session.native_root_receipt = { kind: name === "session.prompt" ? result.turn.stopReason : "created", daemon_id: this.state.daemon_id }; await this.persist({ active_tree: this.rootRunning(result) || this.running(result).length > 0, active_operation: null }); return { ...result, descendants: this.topology(result.provider_session_id) }; } catch (error) { await this.persist({ active_tree: true, active_operation: null }); throw error; } finally { this.active = null; } }
245
245
  async dispatch(operation, params) {
246
246
  const assertDispatch = captureDispatchGuard(this);
247
247
  if (operation === "daemon.status") { let observationError; if (!this.active) try { await this.refreshTree(); } catch (error) { observationError = errorPayload(error); } return { observation_error: observationError ?? null, daemon_id: this.state.daemon_id, pid: process.pid, socket: this.paths.socket, versions: this.state.versions, config_isolation: this.state.config_isolation, shutdown_state: this.state.shutdown_state, recovery_status: this.state.recovery_status, auth_status: this.state.auth_status, active_tree: this.state.active_tree, active_operation: this.active, sessions: [...this.sessions.values()], config: this.state.config }; }
@@ -62,7 +62,7 @@ export async function startDaemon(options) {
62
62
  }
63
63
  export const stopDaemon = async options => { const timeoutMs = options.timeoutMs ?? 30_000; return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), locations(options.stateDir).socket, timeoutMs); };
64
64
 
65
- class Runtime {
65
+ export class Runtime {
66
66
  constructor(paths, state, client, child, providerProcess) { this.paths = paths; this.state = state; this.client = client; this.child = child; this.providerProcess = providerProcess; this.sessions = new Map((state.sessions ?? []).map(x => [x.provider_session_id, x])); this.active = null; this.persisting = Promise.resolve(); }
67
67
  persist(patch = {}) { Object.assign(this.state, patch, { pid: process.pid, sessions: [...this.sessions.values()] }); const snapshot = structuredClone(this.state); this.persisting = this.persisting.then(async () => { await writeJson(this.paths.state, snapshot); await heartbeatDaemonProcess(snapshot.config, snapshot.resource_process).catch(() => {}); await heartbeatDaemonProcess(snapshot.config, this.providerProcess).catch(() => {}); }); return this.persisting; }
68
68
  async journal(operation, result) { const line = `${JSON.stringify({ schema_id: "dd-opencode/daemon-event@1", event_id: randomUUID(), observed_at: new Date().toISOString(), operation, active_tree: this.state.active_tree, session_id: result?.provider_session_id ?? null })}\n`; for (const journal of new Set([this.paths.journal, this.state.config.journal].filter(Boolean))) { await mkdir(path.dirname(journal), { recursive: true, mode: 0o700 }); await appendFile(journal, line, { mode: 0o600 }); } }
@@ -82,7 +82,7 @@ class Runtime {
82
82
  }
83
83
  async requireSettled(id) { const value = await this.describe(id); if (!value.settled) throw new DaemonError("tree_not_settled", "OpenCode Session is not idle", false, { session_id: id, status: value.status }); return value; }
84
84
  async reconcileUsage(observed) { await forwardUsage(this.state.config, observed.provider_session_id, observed.usage); for (const child of observed.children.filter(item => (item.parentID ?? observed.provider_session_id) === observed.provider_session_id)) await this.reconcileUsage(await this.describe(child.id)); }
85
- async productive(name, task) { if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true); this.active = name; await this.persist({ active_tree: true, active_operation: name }); try { const result = await task(); await this.persist({ active_tree: !result?.settled, active_operation: null }); await this.journal(name, result); return result; } catch (error) { await this.persist({ active_tree: true, active_operation: null }); await this.journal(`${name}.failed`, null); throw error; } finally { this.active = null; } }
85
+ async productive(name, task) { if (this.active) throw new DaemonError("operation_busy", `${this.active} is already running`, true); this.active = name; try { await this.persist({ active_tree: true, active_operation: name }); const result = await task(); await this.persist({ active_tree: !result?.settled, active_operation: null }); await this.journal(name, result); return result; } catch (error) { await this.persist({ active_tree: true, active_operation: null }); await this.journal(`${name}.failed`, null); throw error; } finally { this.active = null; } }
86
86
  async cancelTree(id, seen = new Set()) {
87
87
  if (seen.has(id)) return [];
88
88
  seen.add(id);
@@ -198,7 +198,7 @@ export async function stopDaemon(options) {
198
198
  return await confirmDaemonStopped(() => callDaemon(options.stateDir, "daemon.stop", { cancelTree: options.cancelTree === true }, timeoutMs), socket, timeoutMs);
199
199
  }
200
200
 
201
- class DaemonRuntime {
201
+ export class DaemonRuntime {
202
202
  constructor(paths, state, bridge, initialized) {
203
203
  this.paths = paths; this.state = state; this.bridge = bridge; this.initialized = initialized;
204
204
  this.sessions = new Map((state.sessions ?? []).map((item) => [item.provider_session_id, item]));
@@ -250,8 +250,8 @@ class DaemonRuntime {
250
250
  if (this.activeProductive) throw new DaemonError("operation_busy", `${this.activeProductive} is already running`, true);
251
251
  this.activeProductive = name;
252
252
  this.activeGeneration = this.controlGeneration ?? 0;
253
- await this.persist({ active_tree: true, active_operation: name });
254
253
  try {
254
+ await this.persist({ active_tree: true, active_operation: name });
255
255
  const result = await task();
256
256
  this.track(result);
257
257
  const running = this.treeRunning(result);
@@ -7,7 +7,7 @@ export class ObservationClock {
7
7
  this.lastWall = wall(); this.lastMono = monotonic(); this.elapsed = 0;
8
8
  this.progress = undefined;
9
9
  }
10
- sample(progress) {
10
+ sample(progress, progressAt) {
11
11
  const wall = this.wall(), mono = this.monotonic();
12
12
  const delta = mono - this.lastMono, wallDelta = wall - this.lastWall;
13
13
  const gap = delta > this.gapMs || wallDelta > this.gapMs || delta < 0 || wallDelta < 0;
@@ -18,7 +18,10 @@ export class ObservationClock {
18
18
  this.elapsed = 0;
19
19
  } else this.elapsed += delta;
20
20
  this.lastWall = wall; this.lastMono = mono;
21
- if (progress !== undefined && progress !== this.progress) { this.progress = progress; this.elapsed = 0; }
21
+ if (progress !== undefined && progress !== this.progress) {
22
+ this.progress = progress;
23
+ this.elapsed = !gap && Number.isFinite(progressAt) ? Math.max(0, Math.min(this.timeoutMs, wall - progressAt)) : 0;
24
+ }
22
25
  return this.elapsed >= this.timeoutMs;
23
26
  }
24
27
  }
@@ -8,7 +8,7 @@ import { resolveProjectRoot } from "../storage/paths.js";
8
8
  import { registerProject, requireProjectByRoot } from "./projects.js";
9
9
  import { getFlowRunStatus, startFlowRun } from "./runs.js";
10
10
  import { readVnextFlowDefinition } from "./vnext-specify.js";
11
- import { assertRunWorkResultArtifacts } from "./work-registry.js";
11
+ import { assertRunWorkResultArtifacts, runWorkResultArtifactIssues } from "./work-registry.js";
12
12
  /** Capture the only flow boundary which exists before `stage start --bootstrap` creates a RUN. */
13
13
  export function createEvalBootstrapSnapshot(context, input) {
14
14
  const projectRoot = resolveProjectRoot(input.projectRoot);
@@ -81,7 +81,8 @@ function captureEvalRunSnapshot(context, input) {
81
81
  const project = requireProjectByRoot(context, projectRoot);
82
82
  const status = getFlowRunStatus(context, { projectRoot, runId: input.runId });
83
83
  assertDedicatedHome(context, project.id, input.runId);
84
- assertRunWorkResultArtifacts(context, project.id, input.runId);
84
+ if (!input.incomplete)
85
+ assertRunWorkResultArtifacts(context, project.id, input.runId);
85
86
  const recovery = typeof input.recoveryId === "string";
86
87
  if (recovery) {
87
88
  const guard = context.db.get("SELECT status, recovery_id FROM run_recovery_guards WHERE project_id = ? AND run_id = ? AND recovery_id = ?", [project.id, input.runId, input.recoveryId]);
@@ -130,6 +131,26 @@ function captureEvalRunSnapshot(context, input) {
130
131
  });
131
132
  const projectGit = snapshotProjectGit(projectRoot, output);
132
133
  const workspace = snapshotWorkspace(workspaceRoot, projectRoot, output);
134
+ // Compare the copied files with the copied SQLite receipt, not a later read
135
+ // of the live database. Incomplete captures retain contradictions as evidence.
136
+ const copiedContext = createContext({ ...context.env, DD_FLOW_HOME: path.join(output, "runtime") }, "read_existing");
137
+ let receiptIssues;
138
+ try {
139
+ receiptIssues = runWorkResultArtifactIssues(copiedContext, project.id, input.runId, file => {
140
+ const roots = [[context.ddFlowHome, "runtime"], [workspaceRoot, workspace.kind === "git_worktree" ? "workspace" : "project"], [projectRoot, "project"]];
141
+ for (const [source, payload] of roots) {
142
+ const relative = path.relative(source, file);
143
+ if (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
144
+ return path.join(output, payload, relative);
145
+ }
146
+ throw new AppError("snapshot_receipt_path_invalid", "Work receipt is outside captured payloads", 1, { result_path: file });
147
+ });
148
+ }
149
+ finally {
150
+ copiedContext.db.close?.();
151
+ }
152
+ if (!input.incomplete && receiptIssues.length)
153
+ throw new AppError("snapshot_work_receipt_inconsistent", "Copied Work result does not match its authoritative receipt", 1, { issues: receiptIssues });
133
154
  const manifest = {
134
155
  schema_id: "dd-flow/eval-run-snapshot@5",
135
156
  run_id: input.runId,
@@ -140,6 +161,7 @@ function captureEvalRunSnapshot(context, input) {
140
161
  stage_entry: input.incomplete || recovery || input.candidate ? null : input.stageEntry,
141
162
  ...(input.incomplete ? { consistency: "sqlite_read_snapshot_files_copied_nonatomically", active_child_work_count: activeChildren, restorable: false } : recovery ? { recovery_id: input.recoveryId, consistency: "sealed_writer_barrier_required", active_child_work_count: activeChildren, restorable: "same-runtime-or-explicit-restore" } : {}),
142
163
  created_at: context.now(),
164
+ work_result_integrity: { valid: receiptIssues.length === 0, issues: receiptIssues },
143
165
  runtime_sha256: treeHash(path.join(output, "runtime")),
144
166
  project_sha256: treeHash(path.join(output, "project")),
145
167
  project_git: projectGit,
@@ -8,6 +8,7 @@ import { appendFlowRunTimelineEvent } from "./runs.js";
8
8
  import { verifyRecoverySnapshot } from "./eval-snapshots.js";
9
9
  import { findRecentMatchingHookEvent, hookSessionIdentity, recoveryAcceptMatchKey } from "./hooks.js";
10
10
  import { flowCommand } from "./stage-pause.js";
11
+ import { materializePendingWorkResults } from "./work-registry.js";
11
12
  function recoveryPacketReceipt(binding) {
12
13
  if (!binding.packet_json || !binding.packet_sha256 || crypto.createHash("sha256").update(binding.packet_json).digest("hex") !== binding.packet_sha256)
13
14
  throw new AppError("recovery_packet_missing", "Recovery requires its original verified Work packet", 1);
@@ -119,6 +120,7 @@ export function sealRunRecovery(context, input) {
119
120
  return { ok: true, recovery: { ...guard, settlement: JSON.parse(guard.settlement_json ?? "{}") }, reused: true };
120
121
  if (guard.status !== "draining")
121
122
  throw new AppError("recovery_invalid_state", "Recovery is not draining", 1, { recovery_id: guard.recovery_id, status: guard.status });
123
+ materializePendingWorkResults(context, project.id, run.id);
122
124
  const now = context.now();
123
125
  const updated = context.db.run("UPDATE run_recovery_guards SET status = 'sealed', settlement_json = ?, sealed_at = ?, updated_at = ? WHERE recovery_id = ? AND status = 'draining' AND generation = (SELECT MAX(generation) FROM run_recovery_guards WHERE project_id = ? AND run_id = ?)", [JSON.stringify(settlement), now, now, guard.recovery_id, project.id, run.id]);
124
126
  if (updated.changes !== 1)
@@ -182,15 +182,90 @@ export function listWorks(context, input) {
182
182
  }) };
183
183
  }
184
184
  export function showWork(context, id) { const work = requireWork(context, id); const sessions = context.db.all("SELECT ws.id, ws.work_id, ws.session_id, ws.hook_event_id, ws.status, ws.prompt_path, ws.result_path, ws.created_at, ws.completed_at, s.harness AS harness_id, COALESCE(s.provider_session_id, s.session_id) AS native_session_id FROM work_sessions ws LEFT JOIN sessions s ON s.project_id = ? AND s.session_id = ws.session_id WHERE ws.work_id = ? ORDER BY ws.created_at", [work.project_id, work.work_id]); return { work: { ...work, short_id: shortWorkId(work.work_id), payload: parsePayload(work), payload_json: undefined, depends_on: parseDependencies(work), sessions: sessions.map(({ session_id, harness_id, native_session_id, ...session }) => ({ ...session, ...(harness_id && native_session_id ? { session: publicSessionIdentity({ harness: harness_id, provider_session_id: native_session_id, session_id }) } : {}) })) } }; }
185
- /** DB rows are authoritative; reject snapshots that would preserve a rewritten Work receipt. */
186
- export function assertRunWorkResultArtifacts(context, projectId, runId) {
185
+ /** Inspect materializations without changing either the accepted receipt or conflicting bytes. */
186
+ export function runWorkResultArtifactIssues(context, projectId, runId, materializedPath = (file) => file) {
187
187
  const works = context.db.all(`SELECT w.work_id, w.result, (SELECT ws.result_path FROM work_sessions ws WHERE ws.work_id = w.work_id ORDER BY ws.created_at DESC LIMIT 1) AS result_path FROM works w WHERE w.project_id = ? AND w.run_id = ? AND w.result_schema IS NOT NULL AND w.result IS NOT NULL AND w.status = 'completed'`, [projectId, runId]);
188
+ const issues = [];
188
189
  for (const work of works) {
189
- if (!work.result_path || !fs.existsSync(work.result_path))
190
- throw new AppError("work_result_artifact_missing", "Completed structured Work has no retained result artifact", 1, { work_id: work.work_id, result_path: work.result_path });
191
- const actual = fs.readFileSync(work.result_path, "utf8");
190
+ const file = work.result_path ? materializedPath(work.result_path) : null;
191
+ const actual = file && fs.existsSync(file) ? fs.readFileSync(file, "utf8") : null;
192
192
  if (actual !== work.result)
193
- throw new AppError("work_result_artifact_drift", "Completed Work result artifact no longer matches its authoritative receipt", 1, { work_id: work.work_id, result_path: work.result_path, expected_sha256: crypto.createHash("sha256").update(work.result).digest("hex"), actual_sha256: crypto.createHash("sha256").update(actual).digest("hex") });
193
+ issues.push({ code: actual === null ? "work_result_artifact_missing" : "work_result_artifact_drift", work_id: work.work_id, result_path: work.result_path, expected_sha256: crypto.createHash("sha256").update(work.result).digest("hex"), actual_sha256: actual === null ? null : crypto.createHash("sha256").update(actual).digest("hex") });
194
+ }
195
+ return issues;
196
+ }
197
+ export function assertRunWorkResultArtifacts(context, projectId, runId) {
198
+ const issue = runWorkResultArtifactIssues(context, projectId, runId)[0];
199
+ if (issue)
200
+ throw new AppError(issue.code, issue.code === "work_result_artifact_missing" ? "Completed structured Work has no retained result artifact" : "Completed Work result artifact no longer matches its authoritative receipt", 1, issue);
201
+ }
202
+ function receiptIntentPath(resultPath, sessionId) {
203
+ return `${resultPath}.pending-${crypto.createHash("sha256").update(sessionId).digest("hex")}`;
204
+ }
205
+ function receiptFileHash(file) {
206
+ return fs.existsSync(file) ? crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex") : null;
207
+ }
208
+ function publishReceiptFile(file, contents) {
209
+ const temporary = `${file}.tmp-${crypto.randomUUID()}`;
210
+ try {
211
+ fs.writeFileSync(temporary, contents);
212
+ const descriptor = fs.openSync(temporary, "r");
213
+ try {
214
+ fs.fsyncSync(descriptor);
215
+ }
216
+ finally {
217
+ fs.closeSync(descriptor);
218
+ }
219
+ fs.renameSync(temporary, file);
220
+ const directory = fs.openSync(path.dirname(file), "r");
221
+ try {
222
+ fs.fsyncSync(directory);
223
+ }
224
+ finally {
225
+ fs.closeSync(directory);
226
+ }
227
+ }
228
+ finally {
229
+ fs.rmSync(temporary, { force: true });
230
+ }
231
+ }
232
+ /** Replay only a prepared publication whose exact receipt was committed in SQLite. */
233
+ export function materializePendingWorkResults(context, projectId, runId) {
234
+ context.db.exec("BEGIN IMMEDIATE");
235
+ try {
236
+ const rows = context.db.all(`SELECT w.work_id, w.result, w.status, ws.id, ws.result_path FROM works w JOIN work_sessions ws ON ws.work_id = w.work_id AND ws.status = w.status WHERE w.project_id = ? AND w.run_id = ? AND w.parent_work_id IS NOT NULL AND w.result IS NOT NULL AND w.status IN ('completed', 'failed', 'cancelled') AND ws.result_path IS NOT NULL`, [projectId, runId]);
237
+ for (const row of rows) {
238
+ const intentPath = receiptIntentPath(row.result_path, row.id);
239
+ if (!fs.existsSync(intentPath))
240
+ continue;
241
+ const intent = JSON.parse(fs.readFileSync(intentPath, "utf8"));
242
+ const digest = crypto.createHash("sha256").update(row.result).digest("hex");
243
+ if (intent.work_id !== row.work_id || intent.work_session_id !== row.id || intent.status !== row.status || intent.result_sha256 !== digest)
244
+ throw new AppError("work_result_publication_conflict", "Pending result publication does not identify the committed receipt", 1, { work_id: row.work_id, intent_path: intentPath });
245
+ const actual = receiptFileHash(row.result_path);
246
+ if (actual !== digest && actual !== intent.previous_sha256)
247
+ throw new AppError("work_result_artifact_drift", "Result changed after its publication was prepared; preserve it for diagnosis", 1, { work_id: row.work_id, actual_sha256: actual, expected_sha256: digest });
248
+ if (fs.existsSync(row.result_path)) {
249
+ const previous = fs.readFileSync(row.result_path);
250
+ if (!previous.equals(Buffer.from(row.result))) {
251
+ const retained = `${row.result_path}.conflict-${crypto.createHash("sha256").update(previous).digest("hex")}`;
252
+ try {
253
+ fs.writeFileSync(retained, previous, { flag: "wx", flush: true });
254
+ }
255
+ catch (error) {
256
+ if (error.code !== "EEXIST" || !fs.readFileSync(retained).equals(previous))
257
+ throw error;
258
+ }
259
+ }
260
+ }
261
+ publishReceiptFile(row.result_path, row.result);
262
+ fs.unlinkSync(intentPath);
263
+ }
264
+ context.db.exec("COMMIT");
265
+ }
266
+ catch (error) {
267
+ context.db.exec("ROLLBACK");
268
+ throw error;
194
269
  }
195
270
  }
196
271
  export function mutateWorkDeps(context, input) {
@@ -357,6 +432,8 @@ export function startBoundWork(context, work, run, identity, hookEventId) {
357
432
  const generation = assertRunMutationAllowed(context, work.project_id, work.run_id);
358
433
  if (work.status !== "created")
359
434
  throw new AppError("invalid_work_state", "Work is not created", 2, { work_id: work.work_id, status: work.status });
435
+ if (fs.existsSync(path.join(requireRunHome(run), "works", work.work_id, ".retry.json")))
436
+ throw new AppError("work_retry_incomplete", "Complete the interrupted Work retry before starting its next attempt", 1, { work_id: work.work_id });
360
437
  const now = context.now();
361
438
  const linkId = `WS-${crypto.randomUUID()}`;
362
439
  const directory = path.join(requireRunHome(run), "works", work.work_id);
@@ -409,12 +486,74 @@ export function finishFanInWork(context, id, result) { const work = requireWork(
409
486
  throw new AppError("active_child_work", "Fan-in Work still has active children", 2, { work_id: work.work_id }); const now = context.now(); context.db.run("UPDATE works SET status = 'completed', result = ?, completed_at = ?, updated_at = ? WHERE work_id = ?", [result, now, now, work.work_id]); context.db.run("UPDATE work_sessions SET status = 'completed', completed_at = COALESCE(completed_at, ?), updated_at = ? WHERE work_id = ? AND status = 'running'", [now, now, work.work_id]); refreshRunWorkProjection(context, work.project_id, work.run_id); appendFlowRunTimelineEvent(context, work.project_id, work.run_id, { type: "work_completed", work_id: work.work_id, fan_in: true }); return { ok: true, work_id: work.work_id, status: "completed", fan_in: true }; }
410
487
  export function failWork(context, id, reason) { return settle(context, id, "failed", reason); }
411
488
  export function cancelWork(context, id, reason) { return settle(context, id, "cancelled", reason); }
412
- export function retryWork(context, id, reason) { const work = requireWork(context, id); assertRunMutationAllowed(context, work.project_id, work.run_id); id = work.work_id; if (work.status !== "failed")
413
- throw new AppError("invalid_work_state", "Only failed Work may be retried", 2); const run = requireRun(context, work.project_id, work.run_id); const directory = path.join(requireRunHome(run), "works", id); const attempts = path.join(directory, "attempts"); const number = fs.existsSync(attempts) ? fs.readdirSync(attempts).filter((entry) => /^ATT-\d{3}$/.test(entry)).length + 1 : 1; const archive = path.join(attempts, `ATT-${String(number).padStart(3, "0")}`); fs.mkdirSync(archive, { recursive: true }); for (const file of ["prompt.md", "result.json", "context.json"]) {
414
- const source = path.join(directory, file);
415
- if (fs.existsSync(source))
416
- fs.renameSync(source, path.join(archive, file));
417
- } context.db.run("UPDATE works SET status = 'created', result = NULL, started_at = NULL, completed_at = NULL, updated_at = ? WHERE work_id = ?", [context.now(), id]); refreshRunWorkProjection(context, work.project_id, work.run_id); return { ok: true, work_id: id, archived_attempt: path.relative(requireRunHome(run), archive).split(path.sep).join("/"), reason }; }
489
+ export function retryWork(context, id, reason) {
490
+ const work = requireWork(context, id);
491
+ const run = requireRun(context, work.project_id, work.run_id);
492
+ const directory = path.join(requireRunHome(run), "works", work.work_id);
493
+ const journal = path.join(directory, ".retry.json");
494
+ let archive = "";
495
+ context.db.exec("BEGIN IMMEDIATE");
496
+ try {
497
+ assertRunMutationAllowed(context, work.project_id, work.run_id);
498
+ const current = requireWork(context, work.work_id);
499
+ const pending = fs.existsSync(journal) ? JSON.parse(fs.readFileSync(journal, "utf8")) : null;
500
+ if (pending && (pending.work_id !== work.work_id || !/^ATT-\d{3,}$/.test(pending.archive) || pending.entries.some(file => !["prompt.md", "result.json", "context.json"].includes(file))))
501
+ throw new AppError("work_retry_invalid", "Retry journal does not identify this Work", 1);
502
+ if (current.status !== "failed" && !(pending && current.status === "created"))
503
+ throw new AppError("invalid_work_state", "Only failed Work may be retried", 2);
504
+ const attempts = path.join(directory, "attempts");
505
+ let number = 1;
506
+ while (fs.existsSync(path.join(attempts, `ATT-${String(number).padStart(3, "0")}`)))
507
+ number += 1;
508
+ const prepared = pending ?? { work_id: work.work_id, archive: `ATT-${String(number).padStart(3, "0")}`, entries: ["prompt.md", "result.json", "context.json"].filter(file => fs.existsSync(path.join(directory, file))), result_sha256: current.result === null ? null : crypto.createHash("sha256").update(current.result).digest("hex") };
509
+ archive = path.join(attempts, prepared.archive);
510
+ if (current.status === "failed") {
511
+ if (prepared.result_sha256 !== (current.result === null ? null : crypto.createHash("sha256").update(current.result).digest("hex")))
512
+ throw new AppError("work_retry_conflict", "Failed Work receipt changed during retry", 1);
513
+ fs.mkdirSync(archive, { recursive: true });
514
+ if (!pending)
515
+ publishReceiptFile(journal, JSON.stringify(prepared));
516
+ const receipt = path.join(archive, "accepted-receipt.json");
517
+ const receiptBytes = JSON.stringify({ work_id: current.work_id, status: current.status, result: current.result, result_sha256: prepared.result_sha256 });
518
+ if (fs.existsSync(receipt) && fs.readFileSync(receipt, "utf8") !== receiptBytes)
519
+ throw new AppError("work_retry_conflict", "Archived receipt differs from failed Work", 1);
520
+ if (!fs.existsSync(receipt))
521
+ publishReceiptFile(receipt, receiptBytes);
522
+ for (const file of prepared.entries) {
523
+ const source = path.join(directory, file), target = path.join(archive, file);
524
+ if (fs.existsSync(source)) {
525
+ if (fs.existsSync(target))
526
+ throw new AppError("archive_conflict", "Both live and archived Work artifact exist; preserve both", 1, { source, target });
527
+ fs.renameSync(source, target);
528
+ }
529
+ else if (!fs.existsSync(target))
530
+ throw new AppError("archive_artifact_missing", "Work artifact is absent from both retry locations", 1, { source, target });
531
+ }
532
+ for (const folder of [archive, directory]) {
533
+ const descriptor = fs.openSync(folder, "r");
534
+ try {
535
+ fs.fsyncSync(descriptor);
536
+ }
537
+ finally {
538
+ fs.closeSync(descriptor);
539
+ }
540
+ }
541
+ for (const column of ["prompt_path", "result_path"]) {
542
+ const file = column === "prompt_path" ? "prompt.md" : "result.json";
543
+ context.db.run(`UPDATE work_sessions SET ${column} = ? WHERE work_id = ? AND ${column} = ?`, [path.join(archive, file), work.work_id, path.join(directory, file)]);
544
+ }
545
+ context.db.run("UPDATE works SET status = 'created', result = NULL, started_at = NULL, completed_at = NULL, updated_at = ? WHERE work_id = ? AND status = 'failed'", [context.now(), work.work_id]);
546
+ }
547
+ context.db.exec("COMMIT");
548
+ }
549
+ catch (error) {
550
+ context.db.exec("ROLLBACK");
551
+ throw error;
552
+ }
553
+ fs.rmSync(journal, { force: true });
554
+ refreshRunWorkProjection(context, work.project_id, work.run_id);
555
+ return { ok: true, work_id: work.work_id, archived_attempt: path.relative(requireRunHome(run), archive).split(path.sep).join("/"), reason };
556
+ }
418
557
  async function settle(context, id, status, result, progress) {
419
558
  const work = requireWork(context, id);
420
559
  assertRunMutationAllowed(context, work.project_id, work.run_id);
@@ -461,10 +600,10 @@ async function settle(context, id, status, result, progress) {
461
600
  assertRunMutationAllowed(context, work.project_id, work.run_id);
462
601
  if (link && !context.db.get("SELECT 1 FROM work_sessions WHERE id = ? AND status = 'running'", [link.id]))
463
602
  throw new AppError("work_generation_stale", "Work Session was interrupted or superseded while checks ran", 1, { work_id: id });
603
+ if (link?.result_path && work.parent_work_id !== null)
604
+ publishReceiptFile(receiptIntentPath(link.result_path, link.id), JSON.stringify({ work_id: work.work_id, work_session_id: link.id, status, result_sha256: crypto.createHash("sha256").update(result).digest("hex"), previous_sha256: receiptFileHash(link.result_path) }));
464
605
  context.db.run("UPDATE works SET status = ?, result = ?, completed_at = ?, updated_at = ? WHERE work_id = ?", [status, result, now, now, id]);
465
606
  if (link) {
466
- if (link.result_path && work.parent_work_id !== null)
467
- fs.writeFileSync(link.result_path, result);
468
607
  context.db.run("UPDATE work_sessions SET status = ?, completed_at = ?, updated_at = ? WHERE id = ?", [status, now, now, link.id]);
469
608
  const stillRunning = context.db.get("SELECT 1 FROM work_sessions WHERE session_id = ? AND status = 'running' LIMIT 1", [link.session_id]);
470
609
  if (!stillRunning)
@@ -476,6 +615,7 @@ async function settle(context, id, status, result, progress) {
476
615
  context.db.exec("ROLLBACK");
477
616
  throw error;
478
617
  }
618
+ materializePendingWorkResults(context, work.project_id, work.run_id);
479
619
  settleRecoveryWork(context, work.work_id, status);
480
620
  refreshRunWorkProjection(context, work.project_id, work.run_id);
481
621
  const newlyReady = context.db.all(`SELECT ${workColumns} FROM works WHERE project_id = ? AND run_id = ? AND status = 'created' ORDER BY created_at, work_id`, [work.project_id, work.run_id]).filter((candidate) => isReady(context, candidate)).map((candidate) => ({ work_id: candidate.work_id, launch_policy: candidate.launch_policy, start_command: workStartCommand(context, candidate) }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deksden-com/dd-flow-cli",
3
- "version": "0.9.0-beta.33",
3
+ "version": "0.9.0-beta.35",
4
4
  "description": "Mechanical runtime CLI for dd-flow workflows.",
5
5
  "type": "module",
6
6
  "bin": {