@deksden-com/dd-flow-cli 0.9.0-beta.103 → 0.9.0-beta.105
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 +6 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/run-cli.js +32 -11
- 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 +2 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +8 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +11 -6
- 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 +3 -3
- package/dist/services/eval-snapshots.js +69 -41
- package/dist/services/execution-policy.js +12 -1
- package/dist/services/hooks.js +150 -42
- 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 +3 -11
- 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 +81 -86
- package/dist/services/run-fork.js +10 -6
- package/dist/services/run-projection.js +19 -64
- package/dist/services/run-recovery-runtime.js +24 -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
|
@@ -326,33 +326,40 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
326
326
|
return reconciled;
|
|
327
327
|
}
|
|
328
328
|
function reserveOrReuseReceipt(context, input) {
|
|
329
|
-
|
|
330
|
-
|
|
329
|
+
// Receipt files and artifact hashes are prepared before reserving SQLite.
|
|
330
|
+
// The compact SQL snapshot below fences races with another check settlement.
|
|
331
|
+
const snapshotSql = "SELECT id, status, exit_code, input_hash, profile_hash, command, gate, retry_of_receipt_id, receipt_path FROM check_receipts WHERE project_id = ? AND run_id = ? ORDER BY reservation_sequence, rowid";
|
|
332
|
+
const snapshot = JSON.stringify(context.db.all(snapshotSql, [input.projectId, input.runId]));
|
|
333
|
+
const receipts = checkReceipts(context, { projectId: input.projectId, runId: input.runId });
|
|
334
|
+
if (JSON.stringify(context.db.all(snapshotSql, [input.projectId, input.runId])) !== snapshot)
|
|
335
|
+
throw new AppError("check_receipt_changed", "Check receipts changed during evidence preparation", 1);
|
|
336
|
+
const prior = receipts.filter((receipt) => receipt.input_hash === input.inputHash && receipt.profile_hash === input.profileHash).at(-1) ?? null;
|
|
337
|
+
const reusablePrior = prior?.status === "passed" && checkReceiptEvidenceValid(prior);
|
|
338
|
+
const retryIds = new Set([...(input.retryReceiptId ? [input.retryReceiptId] : []), ...(input.retryReceiptIds ?? [])]);
|
|
339
|
+
const successor = retryIds.size === 1 ? receipts.find((receipt) => receipt.retry_of_receipt_id === [...retryIds][0] && receipt.input_hash === input.inputHash && receipt.profile_hash === input.profileHash) : null;
|
|
340
|
+
const reusableSuccessor = successor?.status === "passed" && checkReceiptEvidenceValid(successor);
|
|
341
|
+
return context.db.writeTransaction(() => {
|
|
331
342
|
assertRunMutationAllowed(context, input.projectId, input.runId, input.generation);
|
|
332
|
-
|
|
343
|
+
if (JSON.stringify(context.db.all(snapshotSql, [input.projectId, input.runId])) !== snapshot)
|
|
344
|
+
throw new AppError("check_receipt_changed", "Check receipts changed before reservation", 1);
|
|
333
345
|
if (prior?.status === "running")
|
|
334
346
|
throw new AppError("check_in_progress", "A matching check invocation is still running", 1, { check_receipt_id: prior.id });
|
|
335
|
-
const retryIds = new Set([...(input.retryReceiptId ? [input.retryReceiptId] : []), ...(input.retryReceiptIds ?? [])]);
|
|
336
347
|
if (retryIds.size > 1)
|
|
337
348
|
throw new AppError("retry_check_invalid", "A check execution accepts exactly one retry predecessor", 2);
|
|
338
349
|
const retryId = [...retryIds][0];
|
|
339
350
|
if (retryId) {
|
|
340
351
|
if (!input.retryReason?.trim())
|
|
341
352
|
throw new AppError("retry_check_invalid", "A check retry requires the restored-condition reason", 2, { check_receipt_id: retryId });
|
|
342
|
-
const receipts = checkReceipts(context, { projectId: input.projectId, runId: input.runId });
|
|
343
353
|
const predecessor = receipts.find(receipt => receipt.id === retryId);
|
|
344
354
|
if (!predecessor || !["failed", "aborted"].includes(predecessor.status) || predecessor.input_hash !== input.inputHash || predecessor.profile_hash !== input.profileHash || predecessor.command !== input.group.command || predecessor.gate !== input.group.gate)
|
|
345
355
|
throw new AppError("retry_check_invalid", "Retry predecessor does not match this exact check contract", 2, { check_receipt_id: retryId });
|
|
346
|
-
const successor = receipts.find(receipt => receipt.retry_of_receipt_id === retryId && receipt.input_hash === input.inputHash && receipt.profile_hash === input.profileHash);
|
|
347
356
|
if (prior?.id !== (successor?.id ?? predecessor.id))
|
|
348
357
|
throw new AppError("retry_check_invalid", "This retry was superseded by a newer check outcome; inspect the latest receipt", 2, { check_receipt_id: retryId, latest_check_receipt_id: prior?.id ?? null, latest_status: prior?.status ?? null });
|
|
349
358
|
if (successor?.status === "running")
|
|
350
359
|
throw new AppError("check_in_progress", "The requested retry is already running", 1, { check_receipt_id: successor.id, retry_of_receipt_id: retryId });
|
|
351
360
|
if (successor) {
|
|
352
|
-
if (
|
|
353
|
-
context.db.exec("COMMIT");
|
|
361
|
+
if (reusableSuccessor)
|
|
354
362
|
return { receipt: successor };
|
|
355
|
-
}
|
|
356
363
|
throw new AppError("retry_check_invalid", "The requested retry already has a terminal outcome; choose its failed receipt for another retry", 2, { check_receipt_id: successor.id, retry_of_receipt_id: retryId, status: successor.status });
|
|
357
364
|
}
|
|
358
365
|
}
|
|
@@ -360,10 +367,8 @@ function reserveOrReuseReceipt(context, input) {
|
|
|
360
367
|
// failed check again: repairing dependencies/services need not change any
|
|
361
368
|
// tracked input. Lifecycle invocation replay owns command idempotency;
|
|
362
369
|
// the running-receipt guard above still prevents concurrent execution.
|
|
363
|
-
if (prior?.status === "passed" && !retryIds.has(prior.id) && input.group.reusable &&
|
|
364
|
-
context.db.exec("COMMIT");
|
|
370
|
+
if (prior?.status === "passed" && !retryIds.has(prior.id) && input.group.reusable && reusablePrior)
|
|
365
371
|
return { receipt: prior };
|
|
366
|
-
}
|
|
367
372
|
let ordinal = (context.db.get("SELECT COALESCE(MAX(reservation_sequence), 0) + 1 AS sequence FROM check_receipts WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId])?.sequence ?? 1);
|
|
368
373
|
let localId = `RCP-${String(ordinal).padStart(3, "0")}`, id = `${input.workId ?? input.runId}/${localId}`;
|
|
369
374
|
while (context.db.get("SELECT 1 FROM check_receipts WHERE id = ?", [id])) {
|
|
@@ -374,13 +379,8 @@ function reserveOrReuseReceipt(context, input) {
|
|
|
374
379
|
const directory = path.join(input.runHome, input.artifactDir ?? "05-code", "checks", localId);
|
|
375
380
|
const reserved = { id, localId, directory, evidenceDir: path.join(directory, "artifacts"), stdoutPath: path.join(directory, "stdout.log"), stderrPath: path.join(directory, "stderr.log"), receiptPath: path.join(directory, "receipt.json"), completionPath: path.join(directory, "completion.json"), group: input.group, before: input.before, profileHash: input.profileHash, inputHash: input.inputHash, epoch: input.epoch, startedAt: input.startedAt, scope: input.scope, ...(retryId ? { retryOf: retryId, retryReason: input.retryReason } : {}), ...(input.workId ? { workId: input.workId } : {}), projectId: input.projectId, runId: input.runId };
|
|
376
381
|
context.db.run("INSERT INTO check_receipts (id, project_id, run_id, work_id, scope, declaration_id, check_refs_json, gate, command, input_hash, verification_epoch, reservation_sequence, retry_of_receipt_id, retry_reason, status, exit_code, stdout_path, stderr_path, receipt_path, workspace_fingerprint, before_fingerprint, after_fingerprint, profile_hash, mutation_paths_json, artifacts_json, started_at, finished_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'running', NULL, ?, ?, ?, ?, ?, ?, ?, '[]', '[]', ?, ?)", [id, input.projectId, input.runId, input.workId ?? null, input.scope, input.group.declarations[0].id, JSON.stringify(input.group.declarations.map(checkRef).sort()), input.group.gate, input.group.command, input.inputHash, input.epoch, ordinal, reserved.retryOf ?? null, reserved.retryReason ?? null, reserved.stdoutPath, reserved.stderrPath, reserved.receiptPath, input.before.fingerprint, input.before.fingerprint, input.before.fingerprint, input.profileHash, input.startedAt, input.startedAt]);
|
|
377
|
-
context.db.exec("COMMIT");
|
|
378
382
|
return { reserved };
|
|
379
|
-
}
|
|
380
|
-
catch (error) {
|
|
381
|
-
context.db.exec("ROLLBACK");
|
|
382
|
-
throw error;
|
|
383
|
-
}
|
|
383
|
+
}, "checks.reserveOrReuseReceipt");
|
|
384
384
|
}
|
|
385
385
|
function receiptFor(input) { return { id: input.id, local_id: input.localId, declaration_id: input.group.declarations[0].id, check_refs: input.group.declarations.map(checkRef), gate: input.group.gate, scope: input.scope, work_id: input.workId ?? null, command: input.group.command, input_hash: input.inputHash, verification_epoch: input.epoch, retry_of_receipt_id: input.retryOf ?? null, retry_reason: input.retryReason ?? null, status: input.status, exit_code: input.status === "running" ? null : input.result.exitCode, stdout_path: input.stdoutPath, stderr_path: input.stderrPath, receipt_path: input.receiptPath, completion_path: input.completionPath, required_artifacts: input.group.requiredArtifacts, inputs: input.group.inputs, workspace_fingerprint: input.before.fingerprint, before_fingerprint: input.before.fingerprint, after_fingerprint: input.after.fingerprint, profile_hash: input.profileHash, mutation_paths: input.mutationPaths, artifacts: input.artifacts, resources: { ports: input.resources }, started_at: input.startedAt, finished_at: input.status === "running" ? input.startedAt : new Date().toISOString() }; }
|
|
386
386
|
function writeReceipt(file, receipt) { const temporary = `${file}.tmp-${process.pid}-${crypto.randomUUID()}`; fs.writeFileSync(temporary, `${JSON.stringify(receipt, null, 2)}\n`); fs.renameSync(temporary, file); }
|
|
@@ -653,7 +653,6 @@ function explicitInputFiles(root, inputs, ignoredRoots) {
|
|
|
653
653
|
}
|
|
654
654
|
function checkInputHash(workspaceRoot, group, profileHash, inputFingerprint) { return crypto.createHash("sha256").update(JSON.stringify({ workspace_root: path.resolve(workspaceRoot), gate: group.reusable ? null : group.gate, command: group.command, required_artifacts: group.requiredArtifacts, ports: group.ports, inputs: group.inputs, profile_hash: profileHash, environment: environmentFingerprint(workspaceRoot), input_fingerprint: inputFingerprint })).digest("hex"); }
|
|
655
655
|
function bindReceipt(receipt, group) { return { ...receipt, gate: group.gate, declaration_id: group.declarations[0].id, check_refs: group.declarations.map(checkRef), required_artifacts: group.requiredArtifacts, inputs: group.inputs }; }
|
|
656
|
-
function latestApplicableReceipt(context, input) { const candidates = checkReceipts(context, { projectId: input.projectId, runId: input.runId }).filter((receipt) => receipt.input_hash === input.inputHash && receipt.profile_hash === input.profileHash); return candidates.at(-1) ?? null; }
|
|
657
656
|
export function checkReceiptEvidenceValid(receipt) { if (receipt.status !== "passed" || receipt.exit_code !== 0 || receipt.before_fingerprint !== receipt.after_fingerprint || !fs.existsSync(receipt.completion_path) || readCompletion(receipt.completion_path)?.exit_code !== 0)
|
|
658
657
|
return false; const stored = readReceipt(receipt.receipt_path); if (!stored || stored.status !== "passed" || stored.input_hash !== receipt.input_hash || stored.profile_hash !== receipt.profile_hash)
|
|
659
658
|
return false; const artifacts = collectCheckArtifacts(path.join(path.dirname(receipt.receipt_path), "artifacts"), stored.required_artifacts ?? []); return artifacts.complete && JSON.stringify(artifacts.items) === JSON.stringify(stored.artifacts ?? []); }
|
|
@@ -12,7 +12,7 @@ export function assertNoCorrelatedRejection(context, scope) {
|
|
|
12
12
|
const latest = new Map();
|
|
13
13
|
for (const row of rows) {
|
|
14
14
|
const value = JSON.parse(row.outcome_json);
|
|
15
|
-
latest.set(JSON.stringify([value.operation, value.session_id ?? null, value.work_id ?? null, value.attempt ?? null]), value);
|
|
15
|
+
latest.set(JSON.stringify([value.operation, value.session_id ?? null, value.correlation_key ?? value.fingerprint ?? value.work_id ?? null, value.attempt ?? null]), value);
|
|
16
16
|
}
|
|
17
17
|
for (const value of latest.values())
|
|
18
18
|
if (value.error && !isRegisteredRepair(context, value.error.details.continuation, scope))
|
|
@@ -3,7 +3,7 @@ import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout,
|
|
|
3
3
|
import { getFlowRunVariables } from "./runs.js";
|
|
4
4
|
import { dispatchVnextPlanReview } from "./vnext-plan-review.js";
|
|
5
5
|
import { resolveExecutionPolicy } from "./execution-policy.js";
|
|
6
|
-
import { renderDelegationInstructions } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
6
|
+
import { renderDelegationInstructions, stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
7
7
|
import { assertWorkLaunchReady } from "./work-registry.js";
|
|
8
8
|
function semanticCompletionPrompt(stage, completed) {
|
|
9
9
|
const task = stage === "plan-review"
|
|
@@ -15,7 +15,7 @@ function semanticCompletionPrompt(stage, completed) {
|
|
|
15
15
|
`All declared ${stage} child Work has settled (${completed} completed).`,
|
|
16
16
|
task,
|
|
17
17
|
"Then invoke the exact Stage finish command from the original authoritative packet.",
|
|
18
|
-
"
|
|
18
|
+
"If that command returns either outcome = repair_required with a repair Work ID, or error.details.continuation.kind = repair_required, the repair Work is already registered. End this Turn so the controller can dispatch it. Otherwise follow the command's actual result.",
|
|
19
19
|
"Do not create speculative repair Work or start a successor Stage."
|
|
20
20
|
].join(" ");
|
|
21
21
|
}
|
|
@@ -27,7 +27,7 @@ export function controllerStageEntryPrompt(stage, command) {
|
|
|
27
27
|
"Read the complete authoritative packet from the command's response_file result, including worker_prompt_markdown. If a read is truncated, continue until the entire packet has been read.",
|
|
28
28
|
"If the returned packet has orchestration.kind = work_fanout, this Turn is only Stage entry: acknowledge the packet and end your Turn immediately. The packet describes the whole Stage, but its graph/dispatch/finish instructions belong to later controller continuations. Do not list, start, launch or finish Work, create children, change files, run checks, or finish the Stage in this entry Turn. The controller will inspect the graph and send the next assignment with exact issued commands (or dispatch external workers itself).",
|
|
29
29
|
"Otherwise perform this Stage's semantic work and stop at its boundary or a declared HITL boundary. Do not start a successor Stage.",
|
|
30
|
-
|
|
30
|
+
stageLifecycleInstruction
|
|
31
31
|
].join("\n\n");
|
|
32
32
|
}
|
|
33
33
|
/** The shared controller owns this former eval-only Work-graph decision. */
|
|
@@ -4,6 +4,8 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { DatabaseSync } from "node:sqlite";
|
|
6
6
|
import { registerStoreWriter } from "../storage/writer-contract.js";
|
|
7
|
+
import { beginSnapshotImport, completeSnapshotImport } from "../storage/import-guard.js";
|
|
8
|
+
import { foreignKeyTransaction } from "../storage/foreign-key-transaction.js";
|
|
7
9
|
import { createContext } from "../runtime/context.js";
|
|
8
10
|
import { AppError } from "../shared/errors.js";
|
|
9
11
|
import { resolveProjectRoot, runtimeProtocolDir, runtimeRunDir } from "../storage/paths.js";
|
|
@@ -16,6 +18,7 @@ import { resourceHome } from "./managed-processes.js";
|
|
|
16
18
|
import { codexHomeSharedEntries } from "./hooks.js";
|
|
17
19
|
import { prepareOutputFile } from "../cli/input-preparation.js";
|
|
18
20
|
import { localEnvironmentAllowed, projectServicePathFilter } from "./workspace-files.js";
|
|
21
|
+
import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
|
|
19
22
|
/** Capture the only flow boundary which exists before `stage start --bootstrap` creates a RUN. */
|
|
20
23
|
export function prepareEvalBootstrapSnapshot(context, input) {
|
|
21
24
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -387,9 +390,12 @@ function assertRunRestoreTargets(context, input, snapshot, projectRoot, manifest
|
|
|
387
390
|
if (context.db.get("SELECT 1 FROM projects LIMIT 1") || context.db.get("SELECT 1 FROM runs LIMIT 1"))
|
|
388
391
|
throw new AppError("restore_home_not_empty", "Snapshot restore cannot replace an initialized runtime", 1);
|
|
389
392
|
}
|
|
390
|
-
export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRunRestore(context, input)) {
|
|
393
|
+
export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRunRestore(context, input), prearmedImportId) {
|
|
391
394
|
const { snapshot, manifest, recovery, projectRoot, sourceRuntime, sourceProject, routingInput, selectedProfile } = prepared;
|
|
392
395
|
assertRunRestoreTargets(context, input, snapshot, projectRoot, manifest);
|
|
396
|
+
const sourceManifestSha256 = fileHash(path.join(snapshot, "snapshot.json"));
|
|
397
|
+
const importId = prearmedImportId ?? beginSnapshotImport({ home: context.ddFlowHome, projectRoot,
|
|
398
|
+
workspaceRoot: restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome), sourceManifestSha256 });
|
|
393
399
|
clearProjectTree(projectRoot, false);
|
|
394
400
|
restoreProjectGit(snapshot, manifest.project_git, projectRoot);
|
|
395
401
|
replaceProjectTree(projectRoot, sourceProject);
|
|
@@ -399,7 +405,8 @@ export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRun
|
|
|
399
405
|
const workspaceRoot = restoreWorkspace(snapshot, manifest, projectRoot, context.ddFlowHome);
|
|
400
406
|
// The dispatch context was intentionally closed before replacing db.sqlite.
|
|
401
407
|
// A fresh process owns all subsequent normal commands.
|
|
402
|
-
const restored = createContext({ ...context.env, DD_FLOW_HOME: context.ddFlowHome });
|
|
408
|
+
const restored = createContext({ ...context.env, DD_FLOW_HOME: context.ddFlowHome }, "initialize", importId);
|
|
409
|
+
let result, primary, failed = false;
|
|
403
410
|
try {
|
|
404
411
|
const runtimeLineage = recovery ? null : detachSnapshotRuntimeOwners(restored, snapshot, manifest);
|
|
405
412
|
rebaseRuntime(restored, manifest.project_root, projectRoot, sourceWorkspaceRoot(manifest), workspaceRoot, manifest.dd_flow_home, context.ddFlowHome, recovery);
|
|
@@ -433,7 +440,11 @@ export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRun
|
|
|
433
440
|
fs.mkdirSync(path.dirname(relocationFile), { recursive: true });
|
|
434
441
|
writeJson(relocationFile, relocation);
|
|
435
442
|
}
|
|
436
|
-
|
|
443
|
+
const expectedWorkspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome);
|
|
444
|
+
if (physicalPath(workspaceRoot) !== physicalPath(expectedWorkspaceRoot))
|
|
445
|
+
throw new AppError("snapshot_workspace_restore_failed", "Restored workspace differs from the import target", 1, { workspace_root: workspaceRoot, expected_workspace_root: expectedWorkspaceRoot });
|
|
446
|
+
completeSnapshotImport(context.ddFlowHome, importId, { projectRoot, workspaceRoot: expectedWorkspaceRoot, sourceManifestSha256 });
|
|
447
|
+
result = {
|
|
437
448
|
ok: true,
|
|
438
449
|
schema_id: "dd-flow/eval-run-restore@1",
|
|
439
450
|
snapshot,
|
|
@@ -448,9 +459,19 @@ export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRun
|
|
|
448
459
|
next_command: recovery ? `dd-flow run recovery inspect ${status.run.id} --project-root ${JSON.stringify(projectRoot)} --json` : `dd-flow stage start ${status.run.id} --stage ${manifest.stage_entry} --project-root ${JSON.stringify(projectRoot)} --json`
|
|
449
460
|
};
|
|
450
461
|
}
|
|
451
|
-
|
|
452
|
-
|
|
462
|
+
catch (error) {
|
|
463
|
+
primary = error;
|
|
464
|
+
failed = true;
|
|
453
465
|
}
|
|
466
|
+
const cleanup = cleanupScopeWriters([{ name: restored.ddFlowHome, db: restored.db, rollback: false, close: "always" }]);
|
|
467
|
+
if (failed) {
|
|
468
|
+
if (primary instanceof AppError && cleanup.length)
|
|
469
|
+
primary.details.import_cleanup_errors = cleanup;
|
|
470
|
+
throw primary;
|
|
471
|
+
}
|
|
472
|
+
if (cleanup.length)
|
|
473
|
+
throw new AppError("snapshot_import_cleanup_failed", "Snapshot imported but its connection could not be closed", 1, { effect: "committed", import_complete: true, import_cleanup_errors: cleanup });
|
|
474
|
+
return result;
|
|
454
475
|
}
|
|
455
476
|
export function prepareVnextSpecifyRun(context, input) {
|
|
456
477
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -498,10 +519,11 @@ function assertSnapshotRuntimeDetachAllowed(get) {
|
|
|
498
519
|
function detachSnapshotRuntimeOwners(context, snapshot, manifest) {
|
|
499
520
|
const hasTable = (table) => Boolean(context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?", [table]));
|
|
500
521
|
assertSnapshotRuntimeDetachAllowed(sql => context.db.get(sql));
|
|
501
|
-
const detachedTables = ["lifecycle_invocations", "run_control_admissions", "run_control_stage_admissions", "run_control_operations", "run_control_workers", "run_controls", "run_controller_events", "run_controller_operations", "run_controllers", "run_recovery_bindings", "work_recovery_segments", "run_recovery_guards", "work_launches", "merge_servers", "merge_sessions", "lane_locks", "lane_waiters"].filter(hasTable);
|
|
522
|
+
const detachedTables = ["lifecycle_packet_assignments", "lifecycle_invocations", "run_control_admissions", "run_control_stage_admissions", "run_control_operations", "run_control_workers", "run_controls", "run_controller_events", "run_controller_operations", "run_controllers", "run_recovery_bindings", "work_recovery_segments", "run_recovery_guards", "work_launches", "merge_servers", "merge_sessions", "lane_locks", "lane_waiters"].filter(hasTable);
|
|
502
523
|
const runTables = Object.fromEntries([...detachedTables, "runs", "merge_requests", "sessions", "work_sessions"].map(table => [table, context.db.all(`SELECT * FROM ${table}`)]));
|
|
503
524
|
const resourceFile = path.join(context.ddFlowHome, "runtime.sqlite");
|
|
504
525
|
const resources = fs.existsSync(resourceFile) ? new DatabaseSync(resourceFile) : null;
|
|
526
|
+
let resourceHeld = false, resourceCommitted = false, result, primary, failed = false;
|
|
505
527
|
try {
|
|
506
528
|
if (resources)
|
|
507
529
|
registerStoreWriter(resources);
|
|
@@ -512,42 +534,56 @@ function detachSnapshotRuntimeOwners(context, snapshot, manifest) {
|
|
|
512
534
|
fs.writeFileSync(file, `${JSON.stringify({ schema_id: "dd-flow/snapshot-runtime-lineage@1", source_snapshot: snapshot, source_manifest_sha256: fileHash(path.join(snapshot, "snapshot.json")), source_runtime: manifest.dd_flow_home, restored_runtime: context.ddFlowHome, run_id: manifest.run_id, run_tables: runTables, resource_tables: resourceTables }, null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
|
513
535
|
syncPath(file);
|
|
514
536
|
syncPath(directory);
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
context.db.
|
|
518
|
-
for (const table of
|
|
519
|
-
context.db.
|
|
537
|
+
const lineage = JSON.stringify({ lineage_file: file, lineage_sha256: fileHash(file) });
|
|
538
|
+
context.db.writeTransaction(() => {
|
|
539
|
+
assertSnapshotRuntimeDetachAllowed(sql => context.db.get(sql));
|
|
540
|
+
for (const [table, rows] of Object.entries(runTables))
|
|
541
|
+
if (JSON.stringify(context.db.all(`SELECT * FROM ${table}`)) !== JSON.stringify(rows))
|
|
542
|
+
throw new AppError("snapshot_runtime_changed", "Imported runtime changed during lineage preparation", 1, { table });
|
|
543
|
+
context.db.run("UPDATE sessions SET metadata_json = json_set(metadata_json, '$.snapshot_import', json(?))", [lineage]);
|
|
544
|
+
for (const table of detachedTables) {
|
|
545
|
+
try {
|
|
546
|
+
context.db.exec(`DELETE FROM ${table}`);
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
throw new AppError("snapshot_runtime_detach_failed", `Could not detach imported ${table} authority`, 1, { table, cause: String(error) });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
520
552
|
context.db.run("UPDATE merge_requests SET dispatch_owner = NULL, dispatch_lease_token = NULL, dispatch_lease_expires_at = NULL, lock_acquired_at = NULL, adapter_receipt_json = NULL");
|
|
521
553
|
// Only the copied dedicated registry is touched. Source/external homes
|
|
522
554
|
// are historical references in the archive and are never opened here.
|
|
523
555
|
if (resources) {
|
|
524
556
|
resources.exec("BEGIN IMMEDIATE");
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
catch (error) {
|
|
531
|
-
resources.exec("ROLLBACK");
|
|
532
|
-
throw error;
|
|
557
|
+
resourceHeld = true;
|
|
558
|
+
for (const [table, rows] of Object.entries(resourceTables)) {
|
|
559
|
+
if (JSON.stringify(resources.prepare(`SELECT * FROM ${table}`).all()) !== JSON.stringify(rows))
|
|
560
|
+
throw new AppError("snapshot_runtime_changed", "Imported resource evidence changed during lineage preparation", 1, { table });
|
|
561
|
+
resources.exec(`DELETE FROM ${table}`);
|
|
533
562
|
}
|
|
563
|
+
resources.exec("COMMIT");
|
|
564
|
+
resourceHeld = false;
|
|
565
|
+
resourceCommitted = true;
|
|
534
566
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
catch (error) {
|
|
538
|
-
context.db.exec("ROLLBACK");
|
|
539
|
-
throw error;
|
|
540
|
-
}
|
|
541
|
-
return file;
|
|
567
|
+
});
|
|
568
|
+
result = file;
|
|
542
569
|
}
|
|
543
|
-
|
|
544
|
-
|
|
570
|
+
catch (error) {
|
|
571
|
+
primary = error;
|
|
572
|
+
failed = true;
|
|
545
573
|
}
|
|
574
|
+
const cleanup = resources ? cleanupScopeWriters([{ name: resourceFile, db: resources, rollback: resourceHeld, close: "always" }]) : [];
|
|
575
|
+
if (failed || cleanup.length) {
|
|
576
|
+
const details = { effect: resourceCommitted ? "partial" : "unknown", ...(cleanup.length ? { import_cleanup_errors: cleanup } : {}) };
|
|
577
|
+
if (primary instanceof AppError) {
|
|
578
|
+
Object.assign(primary.details, details);
|
|
579
|
+
throw primary;
|
|
580
|
+
}
|
|
581
|
+
throw new AppError("snapshot_runtime_detach_failed", primary instanceof Error ? primary.message : "Imported runtime authority could not be detached", 1, { ...details, ...(failed ? { cause: String(primary) } : {}) });
|
|
582
|
+
}
|
|
583
|
+
return result;
|
|
546
584
|
}
|
|
547
585
|
function rebaseRuntime(context, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome, recovery = false) {
|
|
548
|
-
context.db
|
|
549
|
-
context.db.beginWriteTransaction();
|
|
550
|
-
try {
|
|
586
|
+
foreignKeyTransaction(context.db, () => {
|
|
551
587
|
context.db.run("UPDATE projects SET root = ?", [newRoot]);
|
|
552
588
|
context.db.run("UPDATE runs SET project_root = ?, workspace_root = ?", [newRoot, newWorkspace]);
|
|
553
589
|
for (const column of ["runtime_path", "run_dir", "run_index_path", "run_home_path", "run_root", "index_json"])
|
|
@@ -575,15 +611,7 @@ function rebaseRuntime(context, oldRoot, newRoot, oldWorkspace, newWorkspace, ol
|
|
|
575
611
|
if (!recovery)
|
|
576
612
|
context.db.run("UPDATE work_sessions SET status = CASE WHEN status = 'running' THEN 'completed' ELSE status END, completed_at = COALESCE(completed_at, ?), updated_at = ?", [context.now(), context.now()]);
|
|
577
613
|
context.db.run("DELETE FROM codex_session_bindings");
|
|
578
|
-
|
|
579
|
-
}
|
|
580
|
-
catch (error) {
|
|
581
|
-
context.db.exec("ROLLBACK");
|
|
582
|
-
throw error;
|
|
583
|
-
}
|
|
584
|
-
finally {
|
|
585
|
-
context.db.exec("PRAGMA foreign_keys = ON");
|
|
586
|
-
}
|
|
614
|
+
});
|
|
587
615
|
// Only operational database paths move. Imported packets, results and
|
|
588
616
|
// journals retain their historical bytes, including interrupted JSONL tails.
|
|
589
617
|
// The frozen workspace route is an operational receipt consumed by later
|
|
@@ -660,7 +688,7 @@ function restoreWorkspace(snapshot, manifest, projectRoot, ddFlowHome) {
|
|
|
660
688
|
restoreIndex(snapshot, "workspace", manifest.workspace, workspaceRoot);
|
|
661
689
|
return fs.realpathSync(workspaceRoot);
|
|
662
690
|
}
|
|
663
|
-
function restoredWorkspaceRoot(manifest, projectRoot, ddFlowHome) {
|
|
691
|
+
export function restoredWorkspaceRoot(manifest, projectRoot, ddFlowHome) {
|
|
664
692
|
if (manifest.workspace.kind === "project")
|
|
665
693
|
return projectRoot;
|
|
666
694
|
const managedRoot = physicalPath(manifest.dd_flow_home), sourceRoot = physicalPath(manifest.workspace.source_root);
|
|
@@ -132,16 +132,27 @@ export function assertSameSessionMerge(input) {
|
|
|
132
132
|
throw new AppError("merge_profile_session_conflict", "Same-session MERGE cannot switch execution profiles; select a compatible profile or server MERGE", 2, { previous_stage: stage, previous_profile_id: previous.id, merge_profile_id: merge.id });
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
|
+
/** The RUN identity root and a native Session's fixed launch directory are distinct. */
|
|
136
|
+
export function stageExecutionCwd(input) {
|
|
137
|
+
return path.resolve(input.stage === "specify" || input.stage === "protocolize" ? input.projectRoot
|
|
138
|
+
: input.stage === "merge" ? input.mergeTargetWorkspace ?? input.projectRoot
|
|
139
|
+
: input.workspaceRoot);
|
|
140
|
+
}
|
|
135
141
|
export function resolveStageTransition(input) {
|
|
136
142
|
const target = resolveExecutionPolicy(input);
|
|
137
143
|
const previous = input.fromStage ? resolveExecutionPolicy({ ...input, stage: input.fromStage }).coordinator : null;
|
|
138
144
|
const compatible = !previous || sameSessionProfile(previous, target.coordinator);
|
|
145
|
+
const previousCwd = input.fromStage ? stageExecutionCwd({ ...input, stage: input.fromStage }) : null;
|
|
146
|
+
const targetCwd = stageExecutionCwd(input);
|
|
147
|
+
const sameWorkspace = previousCwd === null || previousCwd === targetCwd;
|
|
139
148
|
if (input.stage === "merge" && input.mergeMode === "same_session") {
|
|
140
149
|
if (!compatible)
|
|
141
150
|
throw new AppError("merge_profile_session_conflict", "Same-session MERGE cannot replace its current coordinator", 2);
|
|
151
|
+
if (!sameWorkspace)
|
|
152
|
+
throw new AppError("merge_session_workspace_conflict", "Same-session MERGE cannot change the native coordinator workspace", 2, { source_workspace: previousCwd, target_workspace: targetCwd });
|
|
142
153
|
return { ...target, session_mode: "same_session" };
|
|
143
154
|
}
|
|
144
|
-
return { ...target, session_mode: !compatible || (input.stage === "merge" && input.mergeMode === "server") ? "new_session" : input.stageSessionMode };
|
|
155
|
+
return { ...target, session_mode: !compatible || !sameWorkspace || (input.stage === "merge" && input.mergeMode === "server") ? "new_session" : input.stageSessionMode };
|
|
145
156
|
}
|
|
146
157
|
function validateDelegation(value) {
|
|
147
158
|
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some(key => !["mode", "agent_profile_id", "max_parallel"].includes(key)))
|