@deksden-com/dd-flow-cli 0.9.0-beta.104 → 0.9.0-beta.106
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 +12 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/run-cli.js +23 -8
- package/dist/harness-runtime/bin/dd-agy.mjs +1 -1
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +2 -2
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +3 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -1
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +2 -2
- package/dist/services/cleanup.js +81 -32
- package/dist/services/code-checks.js +18 -19
- package/dist/services/continuation-outcome.js +1 -1
- package/dist/services/controller-fanout.js +12 -4
- package/dist/services/eval-snapshots.js +69 -41
- package/dist/services/execution-policy.js +12 -1
- package/dist/services/external-work-launch.js +2 -1
- package/dist/services/hooks.js +153 -43
- package/dist/services/lanes.js +23 -18
- package/dist/services/lifecycle-command.js +11 -2
- package/dist/services/lifecycle-invocations.js +291 -54
- package/dist/services/managed-processes.js +35 -23
- package/dist/services/merge-queue.js +71 -67
- package/dist/services/merge-server.js +5 -12
- package/dist/services/migrations.js +28 -12
- package/dist/services/plan-runtime.js +8 -4
- package/dist/services/projects.js +3 -11
- package/dist/services/protocols.js +98 -68
- package/dist/services/recovery-snapshot-database.js +43 -8
- package/dist/services/run-control-worker.js +15 -32
- package/dist/services/run-control.js +3 -17
- package/dist/services/run-controller-capture.js +58 -27
- package/dist/services/run-controller-process.js +21 -30
- package/dist/services/run-controller-recovery.js +36 -37
- package/dist/services/run-controller.js +83 -88
- package/dist/services/run-fork.js +10 -6
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-runtime.js +25 -14
- package/dist/services/run-recovery.js +56 -54
- package/dist/services/runs.js +218 -160
- package/dist/services/runtime-budget.js +19 -45
- package/dist/services/runtime-scope-capture.js +8 -6
- package/dist/services/runtime-scope-control.js +49 -39
- package/dist/services/runtime-scope-resume.js +134 -122
- package/dist/services/runtime-scope-worker.js +48 -49
- package/dist/services/scope-writer-cleanup.js +26 -0
- package/dist/services/stage-context.js +6 -1
- package/dist/services/stage-lifecycle.js +1 -1
- package/dist/services/stage-pause.js +13 -29
- package/dist/services/stage-report-renderer.js +116 -6
- package/dist/services/usage.js +2 -9
- package/dist/services/vnext-code-review.js +10 -7
- package/dist/services/vnext-code.js +9 -17
- package/dist/services/vnext-merge.js +64 -21
- package/dist/services/vnext-plan-review.js +43 -30
- package/dist/services/vnext-plan.js +28 -24
- package/dist/services/vnext-protocolize.js +183 -20
- package/dist/services/vnext-specify.js +27 -19
- package/dist/services/work-registry.js +229 -150
- package/dist/storage/database.js +286 -56
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/writer-contract.js +3 -3
- package/dist/storage/writer-migration.js +24 -41
- package/package.json +1 -1
- package/tools/repair-paused-run-status.mjs +48 -23
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { managedLifecycleCommand, recoverLifecycleCommands } from "./lifecycle-invocations.js";
|
|
1
|
+
import { hasLifecyclePacketAssignments, managedLifecycleCommand, recoverLifecycleCommands } from "./lifecycle-invocations.js";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
@@ -91,15 +91,18 @@ export function assertWorkSettlementAllowed(context, projectId, runId, workId) {
|
|
|
91
91
|
assertRunMutationAllowed(context, projectId, runId);
|
|
92
92
|
}
|
|
93
93
|
/** Completion rights are pinned to the Stage attempt present at the pause fence. */
|
|
94
|
-
export function assertStageSettlementAllowed(context, projectId, runId, stage) {
|
|
94
|
+
export function assertStageSettlementAllowed(context, projectId, runId, stage, expectedGeneration) {
|
|
95
95
|
const guard = recoveryGuard(context, projectId, runId);
|
|
96
|
+
const generation = guard?.generation ?? 0;
|
|
97
|
+
if (expectedGeneration !== undefined && generation !== expectedGeneration)
|
|
98
|
+
throw new AppError("run_recovery_generation_stale", "RUN execution generation changed while the operation was in flight", 1, { run_id: runId, expected_generation: expectedGeneration, generation });
|
|
96
99
|
if (guard?.status === "draining" && context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_control_stage_admissions'")) {
|
|
97
100
|
const admitted = context.db.get(`SELECT a.work_session_id FROM run_control_stage_admissions a JOIN run_controls c ON c.control_id = a.control_id JOIN run_control_admissions w ON w.control_id = a.control_id AND w.work_session_id = a.work_session_id JOIN work_sessions ws ON ws.id = w.work_session_id JOIN runs r ON r.project_id = c.project_id AND r.id = c.run_id, json_each(r.index_json, '$.stage_runs') live
|
|
98
101
|
WHERE c.project_id = ? AND c.run_id = ? AND c.recovery_id = ? AND c.action = 'pause' AND c.status = 'requested' AND a.stage = ? AND json_extract(live.value, '$.stage') = a.stage AND COALESCE(json_extract(live.value, '$.attempt'), 'try-001') = a.attempt AND ws.work_id = w.work_id AND ws.session_id = w.session_id AND NOT EXISTS (SELECT 1 FROM work_sessions newer WHERE newer.work_id = w.work_id AND newer.status = 'running' AND newer.id <> ws.id) LIMIT 1`, [projectId, runId, guard.recovery_id, stage]);
|
|
99
102
|
if (admitted)
|
|
100
|
-
return;
|
|
103
|
+
return generation;
|
|
101
104
|
}
|
|
102
|
-
assertRunMutationAllowed(context, projectId, runId);
|
|
105
|
+
return assertRunMutationAllowed(context, projectId, runId, expectedGeneration);
|
|
103
106
|
}
|
|
104
107
|
/** A recovered RUN never grants the retired process a new execution segment. */
|
|
105
108
|
export function assertRecoveryHookCurrent(context, projectId, runId, identity) {
|
|
@@ -117,29 +120,25 @@ export function beginRunRecovery(context, input, interruption = prepareRecoveryP
|
|
|
117
120
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
118
121
|
const run = requireRecoveryRun(context, project.id, input.runId);
|
|
119
122
|
const recoveryId = `RCV-${crypto.randomUUID()}`, now = context.now();
|
|
120
|
-
|
|
121
|
-
context.db.beginWriteTransaction();
|
|
122
|
-
try {
|
|
123
|
+
const accepted = context.db.writeTransaction(() => {
|
|
123
124
|
const prior = recoveryGuard(context, project.id, run.id);
|
|
124
125
|
const supersedes = prior?.status === "resuming" && interruption.recovery_parent_id === prior.recovery_id;
|
|
125
126
|
if (prior && ["draining", "sealed", "resuming"].includes(prior.status) && !supersedes) {
|
|
126
|
-
context.db.exec("COMMIT");
|
|
127
127
|
return { ok: true, recovery: { ...prior, interruption: JSON.parse(prior.interruption_json) }, reused: true };
|
|
128
128
|
}
|
|
129
|
-
generation = (prior?.generation ?? 0) + 1;
|
|
129
|
+
const generation = (prior?.generation ?? 0) + 1;
|
|
130
130
|
if (supersedes) {
|
|
131
131
|
context.db.run("UPDATE run_recovery_guards SET status = 'abandoned', updated_at = ? WHERE recovery_id = ?", [now, prior.recovery_id]);
|
|
132
132
|
context.db.run("UPDATE run_recovery_bindings SET status = 'abandoned' WHERE recovery_id = ? AND status = 'prepared'", [prior.recovery_id]);
|
|
133
133
|
}
|
|
134
134
|
context.db.run("INSERT INTO run_recovery_guards (recovery_id, project_id, run_id, generation, status, interruption_json, settlement_json, capture_path, created_at, updated_at, sealed_at, resumed_at) VALUES (?, ?, ?, ?, 'draining', ?, NULL, NULL, ?, ?, NULL, NULL)", [recoveryId, project.id, run.id, generation, JSON.stringify(interruption), now, now]);
|
|
135
135
|
setFlowRunRecoveryPaused(context, project.id, run.id, true);
|
|
136
|
-
context.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "run_recovery_draining", recovery_id: recoveryId, generation, interruption });
|
|
136
|
+
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "run_recovery_draining", recovery_id: recoveryId, generation, interruption });
|
|
137
|
+
return { ok: true, recovery: { recovery_id: recoveryId, run_id: run.id, generation, status: "draining", interruption }, reused: false };
|
|
138
|
+
});
|
|
139
|
+
if (accepted.reused)
|
|
140
|
+
return accepted;
|
|
141
|
+
const generation = accepted.recovery.generation;
|
|
143
142
|
return { ok: true, recovery: { recovery_id: recoveryId, run_id: run.id, generation, status: "draining", interruption } };
|
|
144
143
|
}
|
|
145
144
|
export function sealRunRecovery(context, input, settlement = prepareRecoveryPayload(input.settlement, "settlement")) {
|
|
@@ -154,18 +153,12 @@ export function sealRunRecovery(context, input, settlement = prepareRecoveryPayl
|
|
|
154
153
|
throw new AppError("recovery_invalid_state", "Recovery is not draining", 1, { recovery_id: guard.recovery_id, status: guard.status });
|
|
155
154
|
materializePendingWorkResults(context, project.id, run.id);
|
|
156
155
|
const now = context.now();
|
|
157
|
-
context.db.
|
|
158
|
-
try {
|
|
156
|
+
context.db.writeTransaction(() => {
|
|
159
157
|
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]);
|
|
160
158
|
if (updated.changes !== 1)
|
|
161
159
|
throw new AppError("recovery_state_changed", "Recovery state changed before it could be sealed", 1, { recovery_id: guard.recovery_id });
|
|
162
160
|
setFlowRunRecoveryPaused(context, project.id, run.id, true);
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
context.db.exec("ROLLBACK TO seal_run_status; RELEASE seal_run_status");
|
|
167
|
-
throw error;
|
|
168
|
-
}
|
|
161
|
+
});
|
|
169
162
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "run_recovery_sealed", recovery_id: guard.recovery_id, generation: guard.generation, settlement });
|
|
170
163
|
return { ok: true, recovery: { recovery_id: guard.recovery_id, run_id: run.id, generation: guard.generation, status: "sealed", settlement } };
|
|
171
164
|
}
|
|
@@ -222,10 +215,18 @@ export function resumeRunRecovery(context, input, prepared = prepareRunRecoveryR
|
|
|
222
215
|
const acceptCommand = acceptanceCommand(context, run.id, guard.recovery_id, run.project_root);
|
|
223
216
|
if (prepared.reused)
|
|
224
217
|
return { ok: true, recovery_id: guard.recovery_id, generation: guard.generation, resumed: guard.status === "recovered", awaiting_acceptance: guard.status === "resuming", accept_command: acceptCommand, reused: true };
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
218
|
+
const authority = () => JSON.stringify([
|
|
219
|
+
recoveryGuard(context, project.id, run.id), requireRecoveryRun(context, project.id, run.id),
|
|
220
|
+
context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY work_id", [project.id, run.id]),
|
|
221
|
+
context.db.all("SELECT ws.* FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? ORDER BY ws.id", [project.id, run.id]),
|
|
222
|
+
context.db.all("SELECT DISTINCT s.* FROM sessions s JOIN work_sessions ws ON ws.session_id = s.session_id JOIN works w ON w.work_id = ws.work_id AND w.project_id = s.project_id WHERE w.project_id = ? AND w.run_id = ? ORDER BY s.session_id", [project.id, run.id]),
|
|
223
|
+
context.db.all("SELECT DISTINCT d.work_id, d.status, d.result FROM works w, json_each(w.depends_on_json) dependency JOIN works d ON d.work_id = dependency.value WHERE w.project_id = ? AND w.run_id = ? ORDER BY d.work_id", [project.id, run.id]),
|
|
224
|
+
context.db.all("SELECT s.* FROM work_recovery_segments s JOIN works w ON w.work_id = s.work_id WHERE w.project_id = ? AND w.run_id = ? ORDER BY s.recovery_id, s.work_id", [project.id, run.id]),
|
|
225
|
+
context.db.all("SELECT id FROM hook_events WHERE project_id = ? AND daemon_id = ? ORDER BY id", [project.id, input.daemonId]),
|
|
226
|
+
context.db.all("SELECT b.recovery_id FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND b.daemon_id = ? ORDER BY b.recovery_id", [project.id, input.daemonId])
|
|
227
|
+
]);
|
|
228
|
+
const publication = context.db.readSnapshot(() => {
|
|
229
|
+
const signature = authority();
|
|
229
230
|
const current = recoveryGuard(context, project.id, run.id);
|
|
230
231
|
if (!current || current.recovery_id !== guard.recovery_id || current.generation !== guard.generation || current.status !== "sealed" || current.capture_path !== guard.capture_path)
|
|
231
232
|
throw new AppError("recovery_state_changed", "Recovery state changed before its resume transaction", 1, { recovery_id: guard.recovery_id });
|
|
@@ -249,23 +250,26 @@ export function resumeRunRecovery(context, input, prepared = prepareRunRecoveryR
|
|
|
249
250
|
if (context.db.get("SELECT id FROM hook_events WHERE project_id = ? AND daemon_id = ? LIMIT 1", [project.id, input.daemonId]) || context.db.get("SELECT b.recovery_id FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND b.daemon_id = ? LIMIT 1", [project.id, input.daemonId]))
|
|
250
251
|
throw new AppError("recovery_daemon_stale", "Replacement daemon already has historical lifecycle authority in this project", 1);
|
|
251
252
|
const packet = rootRecoveryPacket(context, run, guard, owner);
|
|
253
|
+
const running = context.db.all("SELECT work_id, started_at, status FROM works WHERE project_id = ? AND run_id = ? AND work_id <> ? AND parent_work_id IS NOT NULL AND (status = 'running' OR (status = 'created' AND EXISTS (SELECT 1 FROM work_recovery_segments s WHERE s.work_id = works.work_id AND s.status = 'prepared'))) ORDER BY work_id", [project.id, run.id, owner.work_id]);
|
|
254
|
+
const evidence = new Map(running.map(work => [work.work_id, recoveryEvidencePath(context, run, guard, work.work_id)]));
|
|
255
|
+
return { signature, owner, packet, running, evidence };
|
|
256
|
+
});
|
|
257
|
+
const { owner, packet, running } = publication;
|
|
258
|
+
context.db.writeTransaction(() => {
|
|
259
|
+
if (authority() !== publication.signature)
|
|
260
|
+
throw new AppError("recovery_state_changed", "Recovery inputs changed while its packet was prepared", 1, { recovery_id: guard.recovery_id });
|
|
261
|
+
const now = context.now();
|
|
252
262
|
context.db.run("INSERT INTO run_recovery_bindings (recovery_id, work_id, prior_work_session_id, session_id, harness, provider_session_id, daemon_id, status, created_at, packet_json, packet_sha256) VALUES (?, ?, ?, ?, ?, ?, ?, 'prepared', ?, ?, ?)", [guard.recovery_id, owner.work_id, owner.id, owner.session_id, input.harness, input.nativeSessionId, input.daemonId, now, packet, crypto.createHash("sha256").update(packet).digest("hex")]);
|
|
253
|
-
running = context.db.all("SELECT work_id, started_at, status FROM works WHERE project_id = ? AND run_id = ? AND work_id <> ? AND parent_work_id IS NOT NULL AND (status = 'running' OR (status = 'created' AND EXISTS (SELECT 1 FROM work_recovery_segments s WHERE s.work_id = works.work_id AND s.status = 'prepared'))) ORDER BY work_id", [project.id, run.id, owner.work_id]);
|
|
254
263
|
for (const work of running)
|
|
255
264
|
context.db.run("UPDATE work_recovery_segments SET status = 'superseded', updated_at = ? WHERE work_id = ? AND status = 'prepared'", [now, work.work_id]);
|
|
256
265
|
for (const work of running) {
|
|
257
|
-
const evidence =
|
|
266
|
+
const evidence = publication.evidence.get(work.work_id);
|
|
258
267
|
context.db.run("INSERT INTO work_recovery_segments (recovery_id, work_id, generation, prior_started_at, evidence_path, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'prepared', ?, ?)", [guard.recovery_id, work.work_id, guard.generation, work.started_at, evidence, now, now]);
|
|
259
268
|
context.db.run("UPDATE work_sessions SET status = 'interrupted', completed_at = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [now, now, work.work_id]);
|
|
260
269
|
context.db.run("UPDATE works SET status = 'created', result = NULL, completed_at = NULL, updated_at = ? WHERE work_id = ? AND status = 'running'", [now, work.work_id]);
|
|
261
270
|
}
|
|
262
271
|
context.db.run("UPDATE run_recovery_guards SET status = 'resuming', updated_at = ? WHERE recovery_id = ? AND status = 'sealed'", [now, guard.recovery_id]);
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
catch (error) {
|
|
266
|
-
context.db.exec("ROLLBACK");
|
|
267
|
-
throw error;
|
|
268
|
-
}
|
|
272
|
+
});
|
|
269
273
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "run_recovery_prepared", recovery_id: guard.recovery_id, generation: guard.generation, daemon_id: input.daemonId, interrupted_works: running.map((work) => work.work_id) });
|
|
270
274
|
return { ok: true, recovery_id: guard.recovery_id, generation: guard.generation, resumed: false, awaiting_acceptance: true, accept_command: acceptCommand, interrupted_works: running.map((work) => work.work_id) };
|
|
271
275
|
}
|
|
@@ -276,28 +280,28 @@ export function acceptRunRecovery(context, input) {
|
|
|
276
280
|
const eventKey = input.hookEventId ?? findRecentMatchingHookEvent(context, { projectId: project.id, matchKey, errorCode: "trusted_recovery_acceptance_required", operation: "run recovery accept" }).eventKey;
|
|
277
281
|
const identity = hookSessionIdentity(context, project.id, eventKey);
|
|
278
282
|
const now = context.now();
|
|
279
|
-
|
|
280
|
-
context.db.beginWriteTransaction();
|
|
281
|
-
try {
|
|
283
|
+
const result = context.db.writeTransaction(() => {
|
|
282
284
|
const guard = recoveryGuard(context, project.id, run.id);
|
|
283
285
|
const binding = context.db.get("SELECT * FROM run_recovery_bindings WHERE recovery_id = ?", [input.recoveryId]);
|
|
284
286
|
const hook = context.db.get("SELECT status, match_key FROM hook_events WHERE id = ?", [identity.hookEventId]);
|
|
285
287
|
if (!guard || guard.recovery_id !== input.recoveryId || !binding || binding.session_id !== identity.sessionId || binding.harness !== identity.harness || binding.provider_session_id !== identity.providerSessionId || binding.daemon_id !== identity.daemonId || !hook?.match_key || !matchKey.includes(hook.match_key))
|
|
286
288
|
throw new AppError("recovery_binding_mismatch", "Recovery acknowledgement does not belong to the authorized physical Session and generation", 1);
|
|
287
|
-
packetReceipt = recoveryPacketReceipt(binding);
|
|
288
|
-
|
|
289
|
-
|
|
289
|
+
const packetReceipt = recoveryPacketReceipt(binding);
|
|
290
|
+
const retained = packetReceipt.packet;
|
|
291
|
+
if (binding.harness === "zcode-acp" || (binding.harness === "codex-desktop" && hasLifecyclePacketAssignments(context, retained.original_prompt.text))) {
|
|
290
292
|
const scope = { projectRoot: run.project_root, daemonId: binding.daemon_id, rootSessionId: binding.provider_session_id, runId: run.id, generation: guard.generation };
|
|
293
|
+
const assignments = new Set();
|
|
291
294
|
packetReceipt.lifecycle_recovery = {
|
|
292
|
-
commands: retained.continuation === "stage_boundary" ? [] : recoverLifecycleCommands(context, retained.original_prompt.text, scope),
|
|
293
|
-
instruction: "original_prompt is immutable historical evidence. Do not execute its old
|
|
295
|
+
commands: retained.continuation === "stage_boundary" ? [] : recoverLifecycleCommands({ ...context, captureLifecycleAssignment: id => { assignments.add(id); } }, retained.original_prompt.text, scope),
|
|
296
|
+
instruction: "original_prompt is immutable historical evidence. Do not execute its old command authority. Use only replacement commands below; reconcile unknown outcomes before continuing. At a completed stage boundary return control to the runner.",
|
|
294
297
|
work_list_command: `${flowCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: JSON.stringify(scope) } })} work ls --run ${run.id} --ready --project-root ${JSON.stringify(run.project_root)} --json`
|
|
295
298
|
};
|
|
299
|
+
if (context.captureLifecycleAssignment)
|
|
300
|
+
context.db.afterCommit(() => { for (const id of assignments)
|
|
301
|
+
context.captureLifecycleAssignment(id); });
|
|
296
302
|
}
|
|
297
|
-
if (guard.status === "recovered" && binding.status === "accepted" && binding.hook_event_key === eventKey && hook.status === "claimed")
|
|
298
|
-
context.db.exec("COMMIT");
|
|
303
|
+
if (guard.status === "recovered" && binding.status === "accepted" && binding.hook_event_key === eventKey && hook.status === "claimed")
|
|
299
304
|
return { ok: true, accepted: true, recovery_id: guard.recovery_id, generation: guard.generation, reused: true, ...packetReceipt };
|
|
300
|
-
}
|
|
301
305
|
if (guard.status !== "resuming" || binding.status !== "prepared" || hook.status !== "observed")
|
|
302
306
|
throw new AppError("trusted_recovery_acceptance_required", "Recovery requires a fresh native acknowledgement before work can continue", 1);
|
|
303
307
|
const packet = JSON.parse(binding.packet_json);
|
|
@@ -313,14 +317,12 @@ export function acceptRunRecovery(context, input) {
|
|
|
313
317
|
context.db.run("UPDATE run_recovery_bindings SET status = 'accepted', hook_event_key = ?, accepted_at = ? WHERE recovery_id = ?", [eventKey, now, binding.recovery_id]);
|
|
314
318
|
context.db.run("UPDATE run_recovery_guards SET status = 'recovered', resumed_at = ?, updated_at = ? WHERE recovery_id = ?", [now, now, guard.recovery_id]);
|
|
315
319
|
setFlowRunRecoveryPaused(context, project.id, run.id, false);
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
throw error;
|
|
321
|
-
}
|
|
320
|
+
return { ok: true, accepted: true, recovery_id: input.recoveryId, ...packetReceipt };
|
|
321
|
+
});
|
|
322
|
+
if ("reused" in result && result.reused)
|
|
323
|
+
return result;
|
|
322
324
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "run_recovery_accepted", recovery_id: input.recoveryId, daemon_id: identity.daemonId, hook_event_key: eventKey });
|
|
323
|
-
return
|
|
325
|
+
return result;
|
|
324
326
|
}
|
|
325
327
|
function recoveryEvidencePath(context, run, guard, workId) {
|
|
326
328
|
// The verified capture is already immutable evidence. Referencing it avoids
|