@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
package/dist/services/runs.js
CHANGED
|
@@ -2,6 +2,8 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { withProjectionLock } from "../storage/projection-lock.js";
|
|
6
|
+
import { reconcileRunStageReports } from "./stage-report-renderer.js";
|
|
5
7
|
import { formatFullId, isFullEntityId, isShortEntityId, parseFullEntityId } from "../domain/entity-ids.js";
|
|
6
8
|
import { checksumForFlowFlagValues, isFlowFlagDowngrade, loadCanonicalFlowContract, loadProjectFlowContract, resolveFlowFlags } from "../domain/flow-contract.js";
|
|
7
9
|
import { AppError } from "../shared/errors.js";
|
|
@@ -15,6 +17,7 @@ import { refreshRunSessionProjection } from "./run-projection.js";
|
|
|
15
17
|
import { resolveCanonRoot } from "./canon.js";
|
|
16
18
|
import { bindCurrentEngineToRun } from "./engines.js";
|
|
17
19
|
import { executionProfilePath, loadVnextExecutionProfile } from "./vnext-execution-profile.js";
|
|
20
|
+
import { loadVnextWorkspacePolicy } from "./vnext-workspace-policy.js";
|
|
18
21
|
import { assertSameSessionMerge, loadExecutionRoutingInput, resolveStageTransition, snapshotAgentProfiles } from "./execution-policy.js";
|
|
19
22
|
import { isLegalVnextTransition, vnextStageDirectory, vnextStages } from "../domain/stage-catalog.js";
|
|
20
23
|
import { publicSessionIdentity } from "./session-identity.js";
|
|
@@ -144,7 +147,6 @@ export function startFlowRun(context, input, prepared = prepareFlowRunStart(cont
|
|
|
144
147
|
fs.writeFileSync(path.join(runHome, "execution-routing.input.json"), routingInput.bytes, { flag: "wx", mode: 0o600 });
|
|
145
148
|
bindCurrentEngineToRun(context, { projectRoot, runId, runRoot: runHome });
|
|
146
149
|
const persistedIndex = persistedVnextIndex(index);
|
|
147
|
-
writeJsonFile(runtimePath, runtimeSnapshotForIndex(persistedIndex, 1));
|
|
148
150
|
context.db.run(`INSERT INTO runs
|
|
149
151
|
(id, short_id, slug, project_id, project_root, workspace_root, flow_kind, subject_type, subject_id,
|
|
150
152
|
status, verdict, next_action, runtime_path, run_dir, run_index_path, run_home_path, run_root, layout_version, artifact_root_kind,
|
|
@@ -178,8 +180,9 @@ export function startFlowRun(context, input, prepared = prepareFlowRunStart(cont
|
|
|
178
180
|
eventType: "flow_run.started",
|
|
179
181
|
payload: { run_id: runId, flow_kind: flowKind, subject: index.subject, workspace_root: workspaceRoot, run_home: runHome }
|
|
180
182
|
});
|
|
181
|
-
|
|
182
|
-
appendRunTimeline(
|
|
183
|
+
context.db.afterCommit(() => publishRunProjections(context, project.id, runId));
|
|
184
|
+
appendRunTimeline(context, project.id, runId, { at: now, type: "run_started", run_id: runId });
|
|
185
|
+
appendRunTimeline(context, project.id, runId, {
|
|
183
186
|
at: now,
|
|
184
187
|
type: "flow_flags_resolved",
|
|
185
188
|
run_id: runId,
|
|
@@ -192,7 +195,7 @@ export function startFlowRun(context, input, prepared = prepareFlowRunStart(cont
|
|
|
192
195
|
function snapshotVnextExecutionProfile(projectRoot, ddFlowHome, routingInput) {
|
|
193
196
|
const profile = loadVnextExecutionProfile(projectRoot);
|
|
194
197
|
const sourcePath = executionProfilePath(projectRoot);
|
|
195
|
-
|
|
198
|
+
const frozen = freezeRunExecutionRouting(ddFlowHome, {
|
|
196
199
|
schema_id: "dd-flow/run-execution-profile@1",
|
|
197
200
|
source_path: sourcePath,
|
|
198
201
|
source_checksum: crypto.createHash("sha256").update(fs.readFileSync(sourcePath)).digest("hex"),
|
|
@@ -208,6 +211,12 @@ function snapshotVnextExecutionProfile(projectRoot, ddFlowHome, routingInput) {
|
|
|
208
211
|
...(profile.execution ? { execution: profile.execution } : {})
|
|
209
212
|
}
|
|
210
213
|
}, routingInput);
|
|
214
|
+
if (frozen.settings.merge_mode === "same_session" && frozen.settings.stop_target === "merge_completed"
|
|
215
|
+
&& fs.existsSync(path.join(projectRoot, ".memory-bank", "dd-flow", "project-workspace.json"))
|
|
216
|
+
&& loadVnextWorkspacePolicy(projectRoot).workspace.route === "feature_worktree") {
|
|
217
|
+
throw new AppError("merge_session_workspace_conflict", "Feature-worktree MERGE targets the integration checkout; use server MERGE or a qualified new Session", 2, { project_root: projectRoot });
|
|
218
|
+
}
|
|
219
|
+
return frozen;
|
|
211
220
|
}
|
|
212
221
|
/** Fresh RUN and fresh stage-entry experiments freeze through the same resolver. */
|
|
213
222
|
export function freezeRunExecutionRouting(ddFlowHome, base, input) {
|
|
@@ -238,7 +247,7 @@ export function importFlowRunExecutionProfile(context, input) {
|
|
|
238
247
|
index.execution_profile = { ...input.profile, stage_entry_import: { stage: input.stage, lineage_file: input.lineageFile } };
|
|
239
248
|
index.updated_at = context.now();
|
|
240
249
|
persistRunState(context, project, run, index);
|
|
241
|
-
appendRunTimeline(run.
|
|
250
|
+
appendRunTimeline(context, project.id, run.id, { at: index.updated_at, type: "execution_policy_imported", stage: input.stage, lineage_file: input.lineageFile, source_checksum: input.profile.routing_input?.source_checksum });
|
|
242
251
|
}
|
|
243
252
|
/**
|
|
244
253
|
* A vNext route may materialize a feature checkout after its semantic
|
|
@@ -259,7 +268,7 @@ export function rebindFlowRunWorkspace(context, input) {
|
|
|
259
268
|
context.db.run("UPDATE runs SET workspace_root = ? WHERE project_id = ? AND id = ?", [workspaceRoot, project.id, run.id]);
|
|
260
269
|
persistRunState(context, project, { ...run, workspace_root: workspaceRoot }, index);
|
|
261
270
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.workspace_rebound", payload: { run_id: run.id, workspace_root: workspaceRoot, reason: input.reason } });
|
|
262
|
-
appendRunTimeline(
|
|
271
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "workspace_rebound", run_id: run.id, workspace_root: workspaceRoot, reason: input.reason, git: facts });
|
|
263
272
|
return { ok: true, run_id: run.id, project_root: project.root, workspace_root: workspaceRoot, git: facts };
|
|
264
273
|
}
|
|
265
274
|
function loadUpgradeFlowContract(context, projectRoot) {
|
|
@@ -307,7 +316,7 @@ function runContinuation(context, run, index) {
|
|
|
307
316
|
const command = `${flowCommand(context)} stage start ${run.id} --stage ${next} --project-root ${JSON.stringify(run.project_root)} --json`;
|
|
308
317
|
const profile = index.execution_profile;
|
|
309
318
|
const execution = profile?.settings.execution ? profile.agent_profiles
|
|
310
|
-
? { status: "resolved", ...resolveStageTransition({ ddFlowHome: context.ddFlowHome, policy: profile.settings.execution, profiles: profile.agent_profiles, ...(completed ? { fromStage: completed.stage } : {}), stage: next, stageSessionMode: profile.settings.stage_session_mode, mergeMode: profile.settings.merge_mode }) }
|
|
319
|
+
? { status: "resolved", ...resolveStageTransition({ ddFlowHome: context.ddFlowHome, policy: profile.settings.execution, profiles: profile.agent_profiles, projectRoot: run.project_root, workspaceRoot: run.workspace_root, ...(completed ? { fromStage: completed.stage } : {}), stage: next, stageSessionMode: profile.settings.stage_session_mode, mergeMode: profile.settings.merge_mode }) }
|
|
311
320
|
: { status: "unfrozen", reason: "execution_profile_not_frozen" }
|
|
312
321
|
: undefined;
|
|
313
322
|
return { kind: "start_stage", stage: next, attempt: null, work_id: null, reason: "legal_successor", command, ...(execution ? { execution } : {}) };
|
|
@@ -362,7 +371,7 @@ export function setFlowRunConfig(context, input) {
|
|
|
362
371
|
index.updated_at = now;
|
|
363
372
|
persistRunState(context, project, run, index);
|
|
364
373
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.config_set", payload: { run_id: run.id, key: input.key, value: mode, source, reason } });
|
|
365
|
-
appendRunTimeline(
|
|
374
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "run_config_set", run_id: run.id, key: input.key, value: mode, source, reason });
|
|
366
375
|
return { ok: true, run_id: run.id, settings: runSettings(index), idempotent: false };
|
|
367
376
|
}
|
|
368
377
|
export function prepareFlowRunConfig(context, input) {
|
|
@@ -404,8 +413,8 @@ export function getFlowRunFlagsStatus(context, input) {
|
|
|
404
413
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
405
414
|
const run = resolveRun(context, project.id, input.runId);
|
|
406
415
|
const index = authoritativeIndex(run);
|
|
407
|
-
const
|
|
408
|
-
const flags =
|
|
416
|
+
const legacyRuntime = index.flow_flags ? null : readRuntimeSnapshot(run.runtime_path);
|
|
417
|
+
const flags = index.flow_flags ?? legacyRuntime?.flow_flags;
|
|
409
418
|
if (!flags) {
|
|
410
419
|
return {
|
|
411
420
|
ok: true,
|
|
@@ -423,7 +432,7 @@ export function getFlowRunFlagsStatus(context, input) {
|
|
|
423
432
|
snapshot_revision: flags.snapshot_revision,
|
|
424
433
|
snapshot_checksum: flags.snapshot_checksum,
|
|
425
434
|
flow_flags: flags,
|
|
426
|
-
runtime_revision:
|
|
435
|
+
runtime_revision: index.runtime_revision ?? legacyRuntime?.runtime_revision ?? null
|
|
427
436
|
};
|
|
428
437
|
}
|
|
429
438
|
export function reviseFlowRunFlags(context, input) {
|
|
@@ -431,22 +440,23 @@ export function reviseFlowRunFlags(context, input) {
|
|
|
431
440
|
const run = resolveRun(context, project.id, input.runId);
|
|
432
441
|
assertRunMutationAllowed(context, project.id, run.id);
|
|
433
442
|
const idempotencyKey = requiredPlain(input.idempotencyKey, "idempotency-key");
|
|
434
|
-
context.db.
|
|
435
|
-
|
|
436
|
-
|
|
443
|
+
const priorMutation = context.db.get("SELECT result_json FROM flow_run_flag_mutations WHERE project_id = ? AND run_id = ? AND idempotency_key = ?", [project.id, run.id, idempotencyKey]);
|
|
444
|
+
if (priorMutation)
|
|
445
|
+
return { ...JSON.parse(priorMutation.result_json), idempotent: true };
|
|
446
|
+
// Canonical contract bytes are prepared before taking the writer. Revision
|
|
447
|
+
// and idempotency are compared again against SQL authority under the lock.
|
|
448
|
+
const contract = loadProjectFlowContract(run.project_root);
|
|
449
|
+
return context.db.writeTransaction(() => {
|
|
437
450
|
const existingMutation = context.db.get("SELECT result_json FROM flow_run_flag_mutations WHERE project_id = ? AND run_id = ? AND idempotency_key = ?", [project.id, run.id, idempotencyKey]);
|
|
438
451
|
if (existingMutation) {
|
|
439
452
|
const result = { ...JSON.parse(existingMutation.result_json), idempotent: true };
|
|
440
|
-
context.db.exec("COMMIT");
|
|
441
|
-
committed = true;
|
|
442
453
|
return result;
|
|
443
454
|
}
|
|
444
455
|
// Read the authority only after the write lock: two callers with the same
|
|
445
456
|
// expected revision must not both pass the compare-and-set check.
|
|
446
457
|
const lockedRun = requireRunById(context, project.id, run.id);
|
|
447
458
|
const index = authoritativeIndex(lockedRun);
|
|
448
|
-
const
|
|
449
|
-
const current = runtime?.flow_flags ?? index.flow_flags;
|
|
459
|
+
const current = index.flow_flags;
|
|
450
460
|
if (!current) {
|
|
451
461
|
throw new AppError("validation", "Legacy RUN has no flow-flag snapshot; revise is unavailable", 2, { run_id: run.id });
|
|
452
462
|
}
|
|
@@ -463,7 +473,6 @@ export function reviseFlowRunFlags(context, input) {
|
|
|
463
473
|
});
|
|
464
474
|
}
|
|
465
475
|
const now = context.now();
|
|
466
|
-
const contract = loadProjectFlowContract(lockedRun.project_root);
|
|
467
476
|
const next = resolveFlowFlags(contract, {
|
|
468
477
|
flowKind: index.flow_kind,
|
|
469
478
|
preset: input.preset ?? current.preset.applied,
|
|
@@ -519,14 +528,12 @@ export function reviseFlowRunFlags(context, input) {
|
|
|
519
528
|
context.db.run(`INSERT INTO flow_run_flag_mutations
|
|
520
529
|
(project_id, run_id, idempotency_key, expected_revision, resulting_revision, result_json, created_at)
|
|
521
530
|
VALUES (?, ?, ?, ?, ?, ?, ?)`, [project.id, run.id, idempotencyKey, input.expectedRevision, next.snapshot_revision, JSON.stringify(result), now]);
|
|
522
|
-
context.db.exec("COMMIT");
|
|
523
|
-
committed = true;
|
|
524
531
|
appendAudit(context, {
|
|
525
532
|
projectId: project.id,
|
|
526
533
|
eventType: "flow_run.flags_revised",
|
|
527
534
|
payload: { run_id: run.id, snapshot_revision: next.snapshot_revision, snapshot_checksum: next.snapshot_checksum, ...(reason ? { reason } : {}) }
|
|
528
535
|
});
|
|
529
|
-
appendRunTimeline(
|
|
536
|
+
appendRunTimeline(context, project.id, run.id, {
|
|
530
537
|
at: now,
|
|
531
538
|
type: "flow_flags_revised",
|
|
532
539
|
run_id: run.id,
|
|
@@ -536,12 +543,7 @@ export function reviseFlowRunFlags(context, input) {
|
|
|
536
543
|
...(reason ? { reason } : {})
|
|
537
544
|
});
|
|
538
545
|
return result;
|
|
539
|
-
}
|
|
540
|
-
catch (error) {
|
|
541
|
-
if (!committed)
|
|
542
|
-
context.db.exec("ROLLBACK");
|
|
543
|
-
throw error;
|
|
544
|
-
}
|
|
546
|
+
}, "runs.reviseFlowFlags");
|
|
545
547
|
}
|
|
546
548
|
export function listFlowRuns(context, input) {
|
|
547
549
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -556,9 +558,9 @@ export function prepareFlowRunStageAttachment(context, input) {
|
|
|
556
558
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
557
559
|
const run = resolveRun(context, project.id, input.runId);
|
|
558
560
|
assertRunMutationAllowed(context, project.id, run.id);
|
|
559
|
-
const index = authoritativeIndex(run);
|
|
560
561
|
const stage = requiredPlain(input.stage, "stage");
|
|
561
562
|
const dir = prepareStageDirectory(input.dir);
|
|
563
|
+
const index = authoritativeIndex(run);
|
|
562
564
|
const status = parseStageStatus(input.status);
|
|
563
565
|
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
564
566
|
assertVnextStageStart(run, index, stage, status, existing);
|
|
@@ -567,9 +569,12 @@ export function prepareFlowRunStageAttachment(context, input) {
|
|
|
567
569
|
export function attachFlowRunStage(context, input) {
|
|
568
570
|
// Mutable stage state is re-read immediately before archiving/persisting.
|
|
569
571
|
const { project, run, index, stage, dir, status, existing } = prepareFlowRunStageAttachment(context, input);
|
|
572
|
+
const runRoot = runArtifactRoot(run);
|
|
573
|
+
if (fs.existsSync(path.join(runRoot, `.archive-${dir}.json`)))
|
|
574
|
+
archiveExistingStageAttempt(context, project.id, run.id, runRoot, dir);
|
|
570
575
|
const now = context.now();
|
|
571
576
|
if (status === "running" && existing) {
|
|
572
|
-
archiveExistingStageAttempt(context, project.id, run.id,
|
|
577
|
+
archiveExistingStageAttempt(context, project.id, run.id, runRoot, dir);
|
|
573
578
|
}
|
|
574
579
|
const stageRun = {
|
|
575
580
|
...(existing ?? { order: index.stage_runs.length + 1 }),
|
|
@@ -597,7 +602,8 @@ export function attachFlowRunStage(context, input) {
|
|
|
597
602
|
eventType: "flow_run.stage_attached",
|
|
598
603
|
payload: { run_id: run.id, stage, dir, status }
|
|
599
604
|
});
|
|
600
|
-
appendRunTimeline(
|
|
605
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_attached", run_id: run.id, stage, status, attempt: stageRun.attempt ?? null });
|
|
606
|
+
context.db.afterCommit(() => reconcileRunStageReports(context, project.id, run.id, stage));
|
|
601
607
|
// A stage transition must not rewrite every Session in the RUN. A RUN may
|
|
602
608
|
// contain independent reviewer/worker sessions; their stage is set by the
|
|
603
609
|
// stage/work binding that actually owns that Session.
|
|
@@ -608,7 +614,7 @@ export function attachFlowRunStage(context, input) {
|
|
|
608
614
|
}
|
|
609
615
|
/** Keep synchronous stage settlement atomic; files are projections, SQLite is authoritative. */
|
|
610
616
|
export function withStageSettlement(context, operation) {
|
|
611
|
-
return context.db.writeTransaction(operation);
|
|
617
|
+
return context.db.writeTransaction(operation, "stage.settle");
|
|
612
618
|
}
|
|
613
619
|
export function prepareFlowRunStageCompletion(context, input) {
|
|
614
620
|
for (const [name, value] of Object.entries({ stageReport: input.stageReport, data: input.data, dataSchemaId: input.dataSchemaId, report: input.report })) {
|
|
@@ -634,58 +640,74 @@ export function prepareFlowRunStageCompletion(context, input) {
|
|
|
634
640
|
if (!existing) {
|
|
635
641
|
throw new AppError("not_found", `Run stage is not attached: ${stage}`, 1, { run_id: run.id, stage });
|
|
636
642
|
}
|
|
637
|
-
return { project, run, index, stage, status, existing };
|
|
638
|
-
}
|
|
639
|
-
export function completeFlowRunStage(context, input) {
|
|
640
|
-
const { project, run, index, stage, status, existing } = prepareFlowRunStageCompletion(context, input);
|
|
641
|
-
const now = context.now();
|
|
642
643
|
const dataPath = input.data ? path.join(runArtifactRoot(run), existing.dir, input.data) : null;
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
644
|
+
const dataSha256 = input.dataSha256 ?? (dataPath && fs.existsSync(dataPath) ? crypto.createHash("sha256").update(fs.readFileSync(dataPath)).digest("hex") : null);
|
|
645
|
+
if (dataSha256 && !/^[a-f0-9]{64}$/.test(dataSha256))
|
|
646
|
+
throw new AppError("validation", "Stage data SHA-256 is invalid", 2);
|
|
647
|
+
return { project, run, index, stage, status, existing, dataSha256, git: input.git ?? gitFacts(run.workspace_root), generation: ["done", "failed", "skipped"].includes(status) ? assertStageSettlementAllowed(context, project.id, run.id, stage) : assertRunMutationAllowed(context, project.id, run.id) };
|
|
648
|
+
}
|
|
649
|
+
export function completeFlowRunStage(context, input, prepared = prepareFlowRunStageCompletion(context, input)) {
|
|
650
|
+
const { project, run, stage, status, dataSha256, git, generation } = prepared;
|
|
651
|
+
const completed = context.db.writeTransaction(() => {
|
|
652
|
+
if (["done", "failed", "skipped"].includes(status))
|
|
653
|
+
assertStageSettlementAllowed(context, project.id, run.id, stage, generation);
|
|
654
|
+
else
|
|
655
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
656
|
+
const latest = requireRunById(context, project.id, run.id);
|
|
657
|
+
const index = authoritativeIndex(latest);
|
|
658
|
+
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
659
|
+
if (!existing || existing.attempt !== prepared.existing.attempt || index.runtime_revision !== prepared.index.runtime_revision) {
|
|
660
|
+
throw new AppError("stage_revision_changed", "Stage attempt changed during finish preparation", 1, { run_id: run.id, stage, expected_attempt: prepared.existing.attempt, actual_attempt: existing?.attempt ?? null });
|
|
661
|
+
}
|
|
662
|
+
const now = context.now();
|
|
663
|
+
const stageRun = {
|
|
664
|
+
...existing,
|
|
665
|
+
status,
|
|
666
|
+
...(input.stageReport ? { stage_report: input.stageReport } : {}),
|
|
667
|
+
...(input.data ? { data: input.data } : {}),
|
|
668
|
+
...(dataSha256 ? { data_sha256: dataSha256 } : {}),
|
|
669
|
+
...(input.dataSchemaId ? { data_schema_id: input.dataSchemaId } : {}),
|
|
670
|
+
...(input.report ? { report: input.report } : {}),
|
|
671
|
+
...(input.usage ? { usage: input.usage } : {}),
|
|
672
|
+
...(input.aliases && input.aliases.length > 0 ? { artifact_aliases: input.aliases } : {}),
|
|
673
|
+
updated_at: now,
|
|
674
|
+
...(status === "done" || status === "waiting_for_user" || status === "blocked" || status === "failed" || status === "skipped"
|
|
675
|
+
? { completed_at: now, duration_ms: durationMs(existing.started_at ?? null, now), git }
|
|
676
|
+
: {})
|
|
677
|
+
};
|
|
678
|
+
upsertStage(index, stageRun);
|
|
679
|
+
if (status === "waiting_for_user") {
|
|
680
|
+
index.status = "waiting_for_user";
|
|
681
|
+
index.verdict = "waiting_for_user";
|
|
682
|
+
}
|
|
683
|
+
else if (status === "blocked" || status === "failed") {
|
|
684
|
+
index.status = status;
|
|
685
|
+
index.verdict = status;
|
|
686
|
+
}
|
|
687
|
+
else if (status === "done" && index.status === "waiting_for_user") {
|
|
688
|
+
index.status = "running";
|
|
689
|
+
index.verdict = "running";
|
|
690
|
+
}
|
|
691
|
+
index.current_stage = stage;
|
|
692
|
+
index.attempts = index.stage_runs.map((item) => ({ stage: item.stage, attempt_number: Number((item.attempt ?? "try-001").replace("try-", "")) || 1, root: item.dir, archive: null, status: item.status, started_at: item.started_at ?? now, finished_at: item.completed_at ?? null }));
|
|
693
|
+
index.updated_at = now;
|
|
694
|
+
index.execution = {
|
|
695
|
+
project_root: index.execution?.project_root ?? run.project_root,
|
|
696
|
+
workspace_root: index.execution?.workspace_root ?? run.workspace_root,
|
|
697
|
+
git
|
|
698
|
+
};
|
|
699
|
+
persistRunState(context, project, latest, index);
|
|
700
|
+
context.db.afterCommit(() => refreshRunSessionProjection(context, project.id, run.id));
|
|
701
|
+
appendAudit(context, {
|
|
702
|
+
projectId: project.id,
|
|
703
|
+
eventType: "flow_run.stage_completed",
|
|
704
|
+
payload: { run_id: run.id, stage, status, stage_report: input.stageReport ?? null, data: input.data ?? null }
|
|
705
|
+
});
|
|
706
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_completed", run_id: run.id, stage, status, attempt: stageRun.attempt ?? null });
|
|
707
|
+
return { index, stageRun };
|
|
708
|
+
}, "runs.completeFlowRunStage");
|
|
687
709
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
688
|
-
return { ok: true, run: flowRunSummary(updatedRun), stage_run: stageRun, index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
710
|
+
return { ok: true, run: flowRunSummary(updatedRun), stage_run: completed.stageRun, index: completed.index, flow_guidance: guidanceForRun(context, updatedRun, completed.index) };
|
|
689
711
|
}
|
|
690
712
|
/** Pause one live stage without completing it or creating a new attempt. */
|
|
691
713
|
export function pauseFlowRunStage(context, input) {
|
|
@@ -720,7 +742,7 @@ export function pauseFlowRunStage(context, input) {
|
|
|
720
742
|
persistRunState(context, project, run, index);
|
|
721
743
|
refreshRunSessionProjection(context, project.id, run.id);
|
|
722
744
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_paused", payload: { run_id: run.id, stage, work_id: input.workId, pause_id: input.pauseId, reason: "waiting_for_user" } });
|
|
723
|
-
appendRunTimeline(
|
|
745
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_waiting_for_user", run_id: run.id, stage, work_id: input.workId, pause_id: input.pauseId, question_path: input.questionPath });
|
|
724
746
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
725
747
|
}
|
|
726
748
|
/** Resume the same stage, Work and attempt after a user answer is recorded. */
|
|
@@ -751,8 +773,8 @@ export function resumeFlowRunStage(context, input) {
|
|
|
751
773
|
persistRunState(context, project, run, index);
|
|
752
774
|
refreshRunSessionProjection(context, project.id, run.id);
|
|
753
775
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_resumed", payload: { run_id: run.id, stage, work_id: input.workId, pause_id: pause.id, answer_path: input.answerPath } });
|
|
754
|
-
appendRunTimeline(
|
|
755
|
-
appendRunTimeline(
|
|
776
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "user_answer_received", run_id: run.id, stage, work_id: input.workId, pause_id: pause.id, answer_path: input.answerPath });
|
|
777
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_resumed", run_id: run.id, stage, work_id: input.workId, pause_id: pause.id, paused_ms: pausedMs });
|
|
756
778
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
757
779
|
}
|
|
758
780
|
/** Pause the current stage for a non-user runtime blocker. */
|
|
@@ -786,7 +808,7 @@ export function blockFlowRunStage(context, input, prepared = prepareFlowRunStage
|
|
|
786
808
|
persistRunState(context, project, run, index);
|
|
787
809
|
refreshRunSessionProjection(context, project.id, run.id);
|
|
788
810
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_blocked", payload: { run_id: run.id, stage, work_id: input.workId, kind: input.kind, code: input.code, retryable: input.retryable } });
|
|
789
|
-
appendRunTimeline(
|
|
811
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_blocked", run_id: run.id, stage, work_id: input.workId, kind: input.kind, code: input.code, summary: input.summary, retryable: input.retryable });
|
|
790
812
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
791
813
|
}
|
|
792
814
|
/** Continue the same stage after its non-user blocker was repaired externally. */
|
|
@@ -818,7 +840,7 @@ export function unblockFlowRunStage(context, input, prepared = prepareFlowRunSta
|
|
|
818
840
|
persistRunState(context, project, run, index);
|
|
819
841
|
refreshRunSessionProjection(context, project.id, run.id);
|
|
820
842
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_unblocked", payload: { run_id: run.id, stage, work_id: input.workId, code: blocker.code } });
|
|
821
|
-
appendRunTimeline(
|
|
843
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_unblocked", run_id: run.id, stage, work_id: input.workId, code: blocker.code, blocked_ms: blockedMs });
|
|
822
844
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
823
845
|
}
|
|
824
846
|
function operationalRunStatus(context, projectId, runId) {
|
|
@@ -904,7 +926,7 @@ export function completeFlowRun(context, input) {
|
|
|
904
926
|
eventType: input.manualOverrideReason ? "flow_run.manual_override" : "flow_run.completed",
|
|
905
927
|
payload: { run_id: run.id, status, verdict: index.verdict, next_action: index.next_action, ...(input.manualOverrideReason ? { reason: input.manualOverrideReason } : {}) }
|
|
906
928
|
});
|
|
907
|
-
appendRunTimeline(
|
|
929
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: input.manualOverrideReason ? "run_manual_override" : "run_completed", run_id: run.id, status, ...(input.manualOverrideReason ? { reason: input.manualOverrideReason } : {}) });
|
|
908
930
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
909
931
|
return { ok: true, run: flowRunSummary(updatedRun), index, usage, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
910
932
|
}
|
|
@@ -925,7 +947,7 @@ export function advanceFlowRun(context, input) {
|
|
|
925
947
|
index.updated_at = now;
|
|
926
948
|
persistRunState(context, project, run, index);
|
|
927
949
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.progressed", payload: { run_id: run.id, status: index.status, verdict: input.verdict, next_action: index.next_action } });
|
|
928
|
-
appendRunTimeline(
|
|
950
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "run_progressed", run_id: run.id, status: index.status, next_action: index.next_action });
|
|
929
951
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
930
952
|
return { ok: true, ...(control ? { control } : {}), run: flowRunSummary(updatedRun), index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
931
953
|
}
|
|
@@ -990,7 +1012,7 @@ export function prepareVnextMergeSourceRepairAttempt(context, input) {
|
|
|
990
1012
|
}));
|
|
991
1013
|
persistRunState(context, project, run, index);
|
|
992
1014
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.merge_source_repair_prepared", payload: { run_id: run.id, merge_request_id: input.mergeRequestId, cycle } });
|
|
993
|
-
appendRunTimeline(
|
|
1015
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "merge_source_repair_prepared", run_id: run.id, merge_request_id: input.mergeRequestId, cycle });
|
|
994
1016
|
return { run_id: run.id, cycle };
|
|
995
1017
|
}
|
|
996
1018
|
function closeOpenStagesForOverride(index, status, now) {
|
|
@@ -1039,7 +1061,7 @@ function closeRunOrchestrators(context, projectId, run, status, now) {
|
|
|
1039
1061
|
context.db.run(`UPDATE flow_session_segments SET ended_at = ?
|
|
1040
1062
|
WHERE project_id = ? AND run_id = ? AND session_id IN (${active.map(() => "?").join(", ")}) AND ended_at IS NULL`, [now, projectId, run.id, ...active.map((session) => session.session_id)]);
|
|
1041
1063
|
for (const session of active) {
|
|
1042
|
-
appendRunTimeline(
|
|
1064
|
+
appendRunTimeline(context, projectId, run.id, {
|
|
1043
1065
|
at: now,
|
|
1044
1066
|
type: "session_stopped",
|
|
1045
1067
|
run_id: run.id,
|
|
@@ -1162,7 +1184,7 @@ export function getFlowRunSessions(context, input) {
|
|
|
1162
1184
|
}
|
|
1163
1185
|
export function appendFlowRunTimelineEvent(context, projectId, runId, event) {
|
|
1164
1186
|
const run = requireRunById(context, projectId, runId);
|
|
1165
|
-
appendRunTimeline(
|
|
1187
|
+
appendRunTimeline(context, projectId, run.id, { run_id: run.id, ...event });
|
|
1166
1188
|
}
|
|
1167
1189
|
function guidanceForRun(context, run, index) {
|
|
1168
1190
|
if (run.flow_kind === "vnext_specify")
|
|
@@ -1262,39 +1284,26 @@ function persistRunState(context, project, run, index, options = {}) {
|
|
|
1262
1284
|
if (index.status === "running" && ["draining", "sealed", "resuming"].includes(recoveryGuard(context, project.id, run.id)?.status ?? ""))
|
|
1263
1285
|
index.status = "paused";
|
|
1264
1286
|
const runtimePath = run.runtime_path || projectRunJsonPath(context.ddFlowHome, project.id, run.id);
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
runtimePath,
|
|
1286
|
-
index.updated_at,
|
|
1287
|
-
index.completed_at ?? null,
|
|
1288
|
-
project.id,
|
|
1289
|
-
run.id
|
|
1290
|
-
]);
|
|
1291
|
-
writeJsonFile(runtimePath, runtimeSnapshotForIndex(persistedIndex, runtimeRevision, history));
|
|
1292
|
-
context.db.exec("RELEASE persist_run_state");
|
|
1293
|
-
}
|
|
1294
|
-
catch (error) {
|
|
1295
|
-
context.db.exec("ROLLBACK TO persist_run_state; RELEASE persist_run_state");
|
|
1296
|
-
throw error;
|
|
1297
|
-
}
|
|
1287
|
+
context.db.writeTransaction(() => {
|
|
1288
|
+
const stored = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, run.id]);
|
|
1289
|
+
if (!stored)
|
|
1290
|
+
throw new AppError("not_found", "RUN is not registered", 1, { run_id: run.id });
|
|
1291
|
+
const previous = JSON.parse(stored.index_json);
|
|
1292
|
+
const actualRevision = previous.runtime_revision ?? 0;
|
|
1293
|
+
const expectedRevision = index.runtime_revision ?? 0;
|
|
1294
|
+
if (actualRevision !== expectedRevision)
|
|
1295
|
+
throw new AppError("run_revision_changed", "RUN changed during preparation", 1, { run_id: run.id, expected_revision: expectedRevision, actual_revision: actualRevision });
|
|
1296
|
+
index.runtime_revision = actualRevision + 1;
|
|
1297
|
+
const history = [...(previous.flag_revision_history ?? [])];
|
|
1298
|
+
if (options.flagRevision)
|
|
1299
|
+
history.push(options.flagRevision);
|
|
1300
|
+
index.flag_revision_history = history.slice(-32);
|
|
1301
|
+
const updated = context.db.run(`UPDATE runs SET status = ?, verdict = ?, next_action = ?, index_json = ?, runtime_path = ?, run_index_path = ?, updated_at = ?, completed_at = ?
|
|
1302
|
+
WHERE project_id = ? AND id = ? AND index_json = ?`, [index.status, index.verdict, index.next_action, JSON.stringify(persistedVnextIndex(index)), runtimePath, runtimePath, index.updated_at, index.completed_at ?? null, project.id, run.id, stored.index_json]);
|
|
1303
|
+
if (updated.changes !== 1)
|
|
1304
|
+
throw new AppError("run_revision_changed", "RUN changed during preparation", 1, { run_id: run.id });
|
|
1305
|
+
context.db.afterCommit(() => publishRunProjections(context, project.id, run.id));
|
|
1306
|
+
}, "runs.persistRunState");
|
|
1298
1307
|
}
|
|
1299
1308
|
/** vNext stores derived stage/work state only once: stage_runs, Works and Sessions. */
|
|
1300
1309
|
function persistedVnextIndex(index) {
|
|
@@ -1350,26 +1359,50 @@ function setRunVariable(context, input, value, prepared = prepareRunVariable(con
|
|
|
1350
1359
|
index.variables = { ...(index.variables ?? {}), [input.key]: value };
|
|
1351
1360
|
index.updated_at = context.now();
|
|
1352
1361
|
persistRunState(context, project, run, index);
|
|
1353
|
-
appendRunTimeline(
|
|
1362
|
+
appendRunTimeline(context, project.id, run.id, { at: index.updated_at, type: "run_variable_set", run_id: run.id, key: input.key });
|
|
1354
1363
|
return { ok: true, run_id: run.id, key: input.key, value, idempotent: false };
|
|
1355
1364
|
}
|
|
1356
|
-
function appendRunTimeline(
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1365
|
+
function appendRunTimeline(context, projectId, runId, event) {
|
|
1366
|
+
context.db.writeTransaction(() => {
|
|
1367
|
+
const last = context.db.get("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.timeline' AND json_extract(payload_json, '$.run_id') = ? ORDER BY id DESC LIMIT 1", [projectId, runId]);
|
|
1368
|
+
const sequence = last ? Number(JSON.parse(last.payload_json).sequence) + 1 : 1;
|
|
1369
|
+
const safeEvent = sanitizeTimelineValue(event);
|
|
1370
|
+
const enriched = { event_id: crypto.randomUUID(), sequence, at: typeof safeEvent.at === "string" ? safeEvent.at : context.now(), ...safeEvent };
|
|
1371
|
+
appendAudit(context, { projectId, eventType: "flow_run.timeline", payload: { run_id: runId, sequence, event: enriched } });
|
|
1372
|
+
context.db.afterCommit(() => publishRunProjections(context, projectId, runId));
|
|
1373
|
+
}, "runs.appendRunTimeline");
|
|
1374
|
+
}
|
|
1375
|
+
/** Rebuild projections from committed SQL after taking a per-RUN filesystem lock. */
|
|
1376
|
+
export function publishRunProjections(context, projectId, runId) {
|
|
1377
|
+
const run = context.db.get("SELECT * FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
1378
|
+
if (!run)
|
|
1379
|
+
throw new AppError("not_found", "RUN is not registered", 1, { run_id: runId });
|
|
1380
|
+
const runtimePath = run.runtime_path || projectRunJsonPath(context.ddFlowHome, projectId, runId);
|
|
1381
|
+
ensureDir(path.dirname(runtimePath));
|
|
1382
|
+
withProjectionLock(`${runtimePath}.publish-lock`, () => {
|
|
1383
|
+
// A delayed publisher must observe the latest commit after acquiring its lock.
|
|
1384
|
+
const latest = context.db.get("SELECT * FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
1385
|
+
if (!latest)
|
|
1386
|
+
throw new AppError("not_found", "RUN is not registered", 1, { run_id: runId });
|
|
1387
|
+
const index = authoritativeIndex(latest);
|
|
1388
|
+
const mutations = context.db.all("SELECT resulting_revision, result_json, created_at, idempotency_key FROM flow_run_flag_mutations WHERE project_id = ? AND run_id = ? ORDER BY resulting_revision DESC LIMIT 32", [projectId, runId]);
|
|
1389
|
+
const history = mutations.reverse().map((row) => ({
|
|
1390
|
+
revision: row.resulting_revision,
|
|
1391
|
+
checksum: String(JSON.parse(row.result_json).snapshot_checksum ?? ""),
|
|
1392
|
+
at: row.created_at,
|
|
1393
|
+
idempotency_key: row.idempotency_key
|
|
1394
|
+
}));
|
|
1395
|
+
writeJsonFile(runtimePath, runtimeSnapshotForIndex(index, index.runtime_revision ?? 1, history.length ? history : index.flag_revision_history ?? []));
|
|
1396
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.timeline' AND json_extract(payload_json, '$.run_id') = ? ORDER BY id", [projectId, runId]);
|
|
1397
|
+
if (latest.run_root) {
|
|
1398
|
+
const timelinePath = path.join(latest.run_root, "timeline.jsonl");
|
|
1399
|
+
ensureDir(path.dirname(timelinePath));
|
|
1400
|
+
const bytes = rows.map((row) => JSON.stringify(JSON.parse(row.payload_json).event)).join("\n");
|
|
1401
|
+
const temporary = `${timelinePath}.tmp-${process.pid}-${crypto.randomUUID()}`;
|
|
1402
|
+
fs.writeFileSync(temporary, bytes ? `${bytes}\n` : "");
|
|
1403
|
+
fs.renameSync(temporary, timelinePath);
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1373
1406
|
}
|
|
1374
1407
|
function runtimeSnapshotForIndex(index, runtimeRevision, flagRevisionHistory = []) {
|
|
1375
1408
|
const rest = { ...index };
|
|
@@ -1604,6 +1637,16 @@ export function archiveExistingStageAttempt(context, projectId, runId, runRoot,
|
|
|
1604
1637
|
}
|
|
1605
1638
|
const journal = path.join(runRoot, `.archive-${stageDir}.json`);
|
|
1606
1639
|
const pending = fs.existsSync(journal) ? JSON.parse(fs.readFileSync(journal, "utf8")) : null;
|
|
1640
|
+
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
1641
|
+
const prior = row ? JSON.parse(row.index_json) : null;
|
|
1642
|
+
const live = prior?.stage_runs.find((stage) => stage.dir === stageDir);
|
|
1643
|
+
const generation = row ? assertRunMutationAllowed(context, projectId, runId) : 0;
|
|
1644
|
+
if (pending && (pending.project_id && pending.project_id !== projectId || pending.run_id && pending.run_id !== runId))
|
|
1645
|
+
throw new AppError("archive_owner_changed", "Archive journal belongs to another RUN", 1, { journal });
|
|
1646
|
+
if (pending?.generation !== undefined && pending.generation !== generation)
|
|
1647
|
+
throw new AppError("archive_owner_changed", "RUN generation changed while an archive was pending", 1, { journal, expected_generation: pending.generation, generation });
|
|
1648
|
+
if (pending?.prior_attempt && live?.attempt !== pending.prior_attempt && live?.attempt !== pending.next_attempt)
|
|
1649
|
+
throw new AppError("archive_owner_changed", "Stage attempt changed while an archive was pending", 1, { journal, prior_attempt: pending.prior_attempt, actual_attempt: live?.attempt ?? null });
|
|
1607
1650
|
const entries = pending?.entries ?? fs.readdirSync(currentStageDir).filter((entry) => !/^try-\d{3}$/.test(entry));
|
|
1608
1651
|
if (entries.length === 0 && !pending) {
|
|
1609
1652
|
return;
|
|
@@ -1613,10 +1656,9 @@ export function archiveExistingStageAttempt(context, projectId, runId, runRoot,
|
|
|
1613
1656
|
attempt += 1;
|
|
1614
1657
|
}
|
|
1615
1658
|
const archiveDir = pending?.archiveDir ?? path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`);
|
|
1659
|
+
const intent = pending ?? { archiveDir, entries, stage_run: live ?? null, project_id: projectId, run_id: runId, prior_attempt: live?.attempt ?? null, next_attempt: nextAttempt(live), runtime_revision: prior?.runtime_revision ?? 0, generation };
|
|
1616
1660
|
if (!pending) {
|
|
1617
|
-
|
|
1618
|
-
const prior = stored ? JSON.parse(stored.index_json) : null;
|
|
1619
|
-
writeJsonFile(journal, { archiveDir, entries, stage_run: prior?.stage_runs.find((stage) => stage.dir === stageDir) ?? null });
|
|
1661
|
+
writeJsonFile(journal, intent);
|
|
1620
1662
|
}
|
|
1621
1663
|
ensureDir(archiveDir);
|
|
1622
1664
|
for (const entry of entries) {
|
|
@@ -1630,20 +1672,36 @@ export function archiveExistingStageAttempt(context, projectId, runId, runRoot,
|
|
|
1630
1672
|
else if (!fs.existsSync(target))
|
|
1631
1673
|
throw new AppError("archive_artifact_missing", "Artifact is absent from both archive locations", 2, { source, target });
|
|
1632
1674
|
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1675
|
+
context.db.writeTransaction(() => {
|
|
1676
|
+
const current = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
1677
|
+
const currentIndex = current ? JSON.parse(current.index_json) : null;
|
|
1678
|
+
const currentAttempt = currentIndex?.stage_runs.find((stage) => stage.dir === stageDir)?.attempt;
|
|
1679
|
+
if (currentIndex && intent.runtime_revision !== undefined && (currentIndex.runtime_revision ?? 0) !== intent.runtime_revision && currentAttempt !== intent.next_attempt)
|
|
1680
|
+
throw new AppError("archive_owner_changed", "RUN revision changed while an archive was pending", 1, { journal });
|
|
1681
|
+
if (intent.prior_attempt && currentAttempt !== intent.prior_attempt && currentAttempt !== intent.next_attempt)
|
|
1682
|
+
throw new AppError("archive_owner_changed", "Stage attempt changed while an archive was pending", 1, { journal });
|
|
1683
|
+
if (row && generation !== assertRunMutationAllowed(context, projectId, runId))
|
|
1684
|
+
throw new AppError("archive_owner_changed", "RUN generation changed while an archive was pending", 1, { journal });
|
|
1685
|
+
if (intent.next_attempt === undefined || currentAttempt !== intent.next_attempt) {
|
|
1686
|
+
const oldPrefix = `${currentStageDir}${path.sep}`;
|
|
1687
|
+
const newPrefix = `${archiveDir}${path.sep}`;
|
|
1688
|
+
const rewrite = (table, column, where, values) => {
|
|
1689
|
+
context.db.run(`UPDATE ${table} SET ${column} = ? || substr(${column}, ?) WHERE ${column} LIKE ? AND substr(${column}, ?, 4) != 'try-' AND ${where}`, [newPrefix, oldPrefix.length + 1, `${oldPrefix}%`, oldPrefix.length + 1, ...values]);
|
|
1690
|
+
};
|
|
1691
|
+
const workScope = "work_id IN (SELECT work_id FROM works WHERE project_id = ? AND run_id = ?)";
|
|
1692
|
+
rewrite("work_sessions", "prompt_path", workScope, [projectId, runId]);
|
|
1693
|
+
rewrite("work_sessions", "result_path", workScope, [projectId, runId]);
|
|
1694
|
+
const receiptScope = "project_id = ? AND run_id = ?";
|
|
1695
|
+
for (const column of ["stdout_path", "stderr_path", "receipt_path"])
|
|
1696
|
+
rewrite("check_receipts", column, receiptScope, [projectId, runId]);
|
|
1697
|
+
}
|
|
1698
|
+
context.db.afterCommit(() => {
|
|
1699
|
+
if (!fs.existsSync(journal))
|
|
1700
|
+
return;
|
|
1701
|
+
// Retain original Stage facts with the immutable attempt after SQL paths commit.
|
|
1702
|
+
fs.renameSync(journal, path.join(archiveDir, "archive.json"));
|
|
1703
|
+
});
|
|
1704
|
+
});
|
|
1647
1705
|
}
|
|
1648
1706
|
function writeJsonFile(file, value) {
|
|
1649
1707
|
const tmpFile = `${file}.tmp-${process.pid}-${Date.now()}`;
|