@deksden-com/dd-flow-cli 0.9.0-beta.75 → 0.9.0-beta.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +6 -6
- package/dist/cli/command-inputs.js +300 -0
- package/dist/cli/help.js +1 -1
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +115 -0
- package/dist/cli/run-cli.js +1015 -423
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +7 -2
- package/dist/harness-runtime/lib/dd-codex.mjs +137 -13
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -2
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +13 -4
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +49 -8
- package/dist/services/cli-operation-classifier.js +12 -24
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/config.js +11 -8
- package/dist/services/controller-fanout.js +2 -2
- package/dist/services/dashboard.js +94 -31
- package/dist/services/engines.js +75 -61
- package/dist/services/eval-snapshots.js +138 -38
- package/dist/services/execution-policy.js +5 -12
- package/dist/services/hooks.js +226 -203
- package/dist/services/lanes.js +60 -52
- package/dist/services/lifecycle-command.js +13 -1
- package/dist/services/lifecycle-invocations.js +285 -94
- package/dist/services/managed-processes.js +158 -18
- package/dist/services/merge-queue.js +173 -98
- package/dist/services/migrations.js +13 -8
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +11 -2
- package/dist/services/prompts.js +23 -16
- package/dist/services/protocols.js +54 -19
- package/dist/services/recovery-observation-budget.js +1 -1
- package/dist/services/run-controller-adapter.js +31 -20
- package/dist/services/run-controller-state.js +5 -0
- package/dist/services/run-controller.js +102 -56
- package/dist/services/run-fork.js +36 -22
- package/dist/services/run-recovery.js +32 -17
- package/dist/services/runs.js +110 -38
- package/dist/services/runtime-budget.js +120 -48
- package/dist/services/runtime-scope-capture.js +2 -2
- package/dist/services/runtime-scope-control.js +2 -2
- package/dist/services/runtime-scope-resume.js +34 -12
- package/dist/services/runtime-scope-worker.js +49 -16
- package/dist/services/runtime-service.js +8 -7
- package/dist/services/schema-validation.js +9 -7
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +42 -16
- package/dist/services/stage-lifecycle.js +82 -100
- package/dist/services/stage-pause.js +54 -24
- package/dist/services/vnext-code-review.js +82 -32
- package/dist/services/vnext-code.js +78 -45
- package/dist/services/vnext-fanout.js +13 -12
- package/dist/services/vnext-merge.js +68 -35
- package/dist/services/vnext-plan-review.js +84 -41
- package/dist/services/vnext-plan.js +91 -70
- package/dist/services/vnext-protocolize.js +116 -50
- package/dist/services/vnext-specify.js +70 -57
- package/dist/services/work-registry.js +195 -92
- package/dist/services/worktrees.js +85 -35
- package/dist/storage/database.js +104 -10
- package/dist/storage/paths.js +17 -4
- package/package.json +1 -1
package/dist/services/runs.js
CHANGED
|
@@ -37,7 +37,14 @@ const allowedRunFlowKinds = [
|
|
|
37
37
|
"review",
|
|
38
38
|
"custom"
|
|
39
39
|
];
|
|
40
|
-
export function
|
|
40
|
+
export function prepareFlowRunStart(context, input) {
|
|
41
|
+
const subjectId = requiredPlain(input.subjectId, "subject-id");
|
|
42
|
+
const subjectType = requiredPlain(input.subjectType, "subject-type");
|
|
43
|
+
const slug = normalizeSlug(input.slug);
|
|
44
|
+
for (const [name, value] of [["task profile", input.taskProfile], ["protocol overrides", input.protocolOverrides], ["RUN overrides", input.runOverrides]]) {
|
|
45
|
+
if (value !== undefined && (!value || typeof value !== "object" || Array.isArray(value)))
|
|
46
|
+
throw new AppError("validation", `${name} must be a JSON object`, 2);
|
|
47
|
+
}
|
|
41
48
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
42
49
|
const flowKind = parseRunFlowKind(input.flowKind);
|
|
43
50
|
const vnext = flowKind === "vnext_protocolize" || flowKind === "vnext_specify";
|
|
@@ -48,12 +55,7 @@ export function startFlowRun(context, input) {
|
|
|
48
55
|
const flowContract = input.flowKind === "mb-upgrade"
|
|
49
56
|
? loadUpgradeFlowContract(context, projectRoot)
|
|
50
57
|
: loadProjectFlowContract(projectRoot);
|
|
51
|
-
registerProject(context, { root: projectRoot });
|
|
52
|
-
const project = requireProjectByRoot(context, projectRoot);
|
|
53
58
|
const workspaceRoot = resolveWorkspaceRoot(input.workspaceRoot ?? projectRoot);
|
|
54
|
-
const slug = normalizeSlug(input.slug);
|
|
55
|
-
const runId = nextRunId(context, project.id, slug);
|
|
56
|
-
const { shortId } = parseFullEntityId(runId);
|
|
57
59
|
const now = context.now();
|
|
58
60
|
const flowFlags = resolveFlowFlags(flowContract, {
|
|
59
61
|
flowKind,
|
|
@@ -64,6 +66,14 @@ export function startFlowRun(context, input) {
|
|
|
64
66
|
snapshotRevision: 1,
|
|
65
67
|
now
|
|
66
68
|
});
|
|
69
|
+
return { projectRoot, flowKind, routingInput, executionProfile, workspaceRoot, slug, subjectId, subjectType, now, flowFlags };
|
|
70
|
+
}
|
|
71
|
+
export function startFlowRun(context, input, prepared = prepareFlowRunStart(context, input)) {
|
|
72
|
+
const { projectRoot, flowKind, routingInput, executionProfile, workspaceRoot, slug, subjectId, subjectType, now, flowFlags } = prepared;
|
|
73
|
+
registerProject(context, { root: projectRoot });
|
|
74
|
+
const project = requireProjectByRoot(context, projectRoot);
|
|
75
|
+
const runId = nextRunId(context, project.id, slug);
|
|
76
|
+
const { shortId } = parseFullEntityId(runId);
|
|
67
77
|
const runHome = projectRunHome(context.ddFlowHome, project.id, runId);
|
|
68
78
|
const runDirRelative = path.posix.join("runs", runId);
|
|
69
79
|
const runtimePath = projectRunJsonPath(context.ddFlowHome, project.id, runId);
|
|
@@ -73,8 +83,8 @@ export function startFlowRun(context, input) {
|
|
|
73
83
|
short_id: shortId,
|
|
74
84
|
flow_kind: flowKind,
|
|
75
85
|
subject: {
|
|
76
|
-
id:
|
|
77
|
-
title:
|
|
86
|
+
id: subjectId,
|
|
87
|
+
title: subjectId
|
|
78
88
|
},
|
|
79
89
|
project: {
|
|
80
90
|
id: project.id,
|
|
@@ -147,7 +157,7 @@ export function startFlowRun(context, input) {
|
|
|
147
157
|
project.root,
|
|
148
158
|
workspaceRoot,
|
|
149
159
|
flowKind,
|
|
150
|
-
|
|
160
|
+
subjectType,
|
|
151
161
|
index.subject.id,
|
|
152
162
|
index.status,
|
|
153
163
|
index.verdict,
|
|
@@ -323,9 +333,12 @@ export function getFlowRunVariables(context, input) {
|
|
|
323
333
|
return { ok: true, run_id: run.id, variables };
|
|
324
334
|
}
|
|
325
335
|
export function setFlowRunVariable(context, input) {
|
|
336
|
+
return setRunVariable(context, input, input.value, prepareFlowRunVariable(context, input));
|
|
337
|
+
}
|
|
338
|
+
export function prepareFlowRunVariable(context, input) {
|
|
326
339
|
if (!input.key.startsWith("user."))
|
|
327
340
|
throw new AppError("validation", "Only user.* RUN variables may be set directly", 2, { key: input.key });
|
|
328
|
-
return
|
|
341
|
+
return prepareRunVariable(context, input);
|
|
329
342
|
}
|
|
330
343
|
/** Controllers own policy.* and runtime.* values; agents never call this directly. */
|
|
331
344
|
export function setRuntimeRunVariable(context, input) {
|
|
@@ -334,21 +347,7 @@ export function setRuntimeRunVariable(context, input) {
|
|
|
334
347
|
return setRunVariable(context, input, input.value);
|
|
335
348
|
}
|
|
336
349
|
export function setFlowRunConfig(context, input) {
|
|
337
|
-
|
|
338
|
-
throw new AppError("validation", `Unknown RUN config key: ${input.key}`, 2);
|
|
339
|
-
}
|
|
340
|
-
if (!["auto", "off", "standard", "deep"].includes(input.value)) {
|
|
341
|
-
throw new AppError("validation", "--value for review mode must be auto, off, standard, or deep", 2);
|
|
342
|
-
}
|
|
343
|
-
const reason = requiredPlain(input.reason, "reason");
|
|
344
|
-
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
345
|
-
const run = resolveRun(context, project.id, input.runId);
|
|
346
|
-
assertRunMutationAllowed(context, project.id, run.id);
|
|
347
|
-
const index = authoritativeIndex(run);
|
|
348
|
-
const stageName = input.key === "plan_review.mode" ? "plan-review" : "code-review";
|
|
349
|
-
if (index.stage_runs.some((stage) => stage.stage === stageName)) {
|
|
350
|
-
throw new AppError("invalid_run_config_state", `${input.key} is frozen after ${stageName} starts`, 2, { run_id: run.id });
|
|
351
|
-
}
|
|
350
|
+
const { reason, project, run, index } = prepareFlowRunConfig(context, input);
|
|
352
351
|
const now = context.now();
|
|
353
352
|
const settings = runSettings(index);
|
|
354
353
|
const previous = input.key === "plan_review.mode" ? settings.plan_review : settings.code_review;
|
|
@@ -366,6 +365,24 @@ export function setFlowRunConfig(context, input) {
|
|
|
366
365
|
appendRunTimeline(runArtifactRoot(run), { at: now, type: "run_config_set", run_id: run.id, key: input.key, value: mode, source, reason });
|
|
367
366
|
return { ok: true, run_id: run.id, settings: runSettings(index), idempotent: false };
|
|
368
367
|
}
|
|
368
|
+
export function prepareFlowRunConfig(context, input) {
|
|
369
|
+
if (input.key !== "plan_review.mode" && input.key !== "code_review.mode") {
|
|
370
|
+
throw new AppError("validation", `Unknown RUN config key: ${input.key}`, 2);
|
|
371
|
+
}
|
|
372
|
+
if (!["auto", "off", "standard", "deep"].includes(input.value)) {
|
|
373
|
+
throw new AppError("validation", "--value for review mode must be auto, off, standard, or deep", 2);
|
|
374
|
+
}
|
|
375
|
+
const reason = requiredPlain(input.reason, "reason");
|
|
376
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
377
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
378
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
379
|
+
const index = authoritativeIndex(run);
|
|
380
|
+
const stageName = input.key === "plan_review.mode" ? "plan-review" : "code-review";
|
|
381
|
+
if (index.stage_runs.some((stage) => stage.stage === stageName)) {
|
|
382
|
+
throw new AppError("invalid_run_config_state", `${input.key} is frozen after ${stageName} starts`, 2, { run_id: run.id });
|
|
383
|
+
}
|
|
384
|
+
return { reason, project, run, index };
|
|
385
|
+
}
|
|
369
386
|
/** Freeze an automatic review request once from already accepted policy or PLAN facts. */
|
|
370
387
|
export function freezeFlowRunReviewMode(context, input) {
|
|
371
388
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -535,17 +552,22 @@ export function listFlowRuns(context, input) {
|
|
|
535
552
|
.map(flowRunSummary);
|
|
536
553
|
return { ok: true, project, runs };
|
|
537
554
|
}
|
|
538
|
-
export function
|
|
555
|
+
export function prepareFlowRunStageAttachment(context, input) {
|
|
539
556
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
540
557
|
const run = resolveRun(context, project.id, input.runId);
|
|
541
558
|
assertRunMutationAllowed(context, project.id, run.id);
|
|
542
559
|
const index = authoritativeIndex(run);
|
|
543
|
-
const now = context.now();
|
|
544
560
|
const stage = requiredPlain(input.stage, "stage");
|
|
545
|
-
const dir =
|
|
561
|
+
const dir = prepareStageDirectory(input.dir);
|
|
546
562
|
const status = parseStageStatus(input.status);
|
|
547
563
|
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
548
564
|
assertVnextStageStart(run, index, stage, status, existing);
|
|
565
|
+
return { project, run, index, stage, dir, status, existing };
|
|
566
|
+
}
|
|
567
|
+
export function attachFlowRunStage(context, input) {
|
|
568
|
+
// Mutable stage state is re-read immediately before archiving/persisting.
|
|
569
|
+
const { project, run, index, stage, dir, status, existing } = prepareFlowRunStageAttachment(context, input);
|
|
570
|
+
const now = context.now();
|
|
549
571
|
if (status === "running" && existing) {
|
|
550
572
|
archiveExistingStageAttempt(context, project.id, run.id, runArtifactRoot(run), dir);
|
|
551
573
|
}
|
|
@@ -587,7 +609,17 @@ export function attachFlowRunStage(context, input) {
|
|
|
587
609
|
export function withStageSettlement(context, operation) {
|
|
588
610
|
return context.db.writeTransaction(operation);
|
|
589
611
|
}
|
|
590
|
-
export function
|
|
612
|
+
export function prepareFlowRunStageCompletion(context, input) {
|
|
613
|
+
for (const [name, value] of Object.entries({ stageReport: input.stageReport, data: input.data, dataSchemaId: input.dataSchemaId, report: input.report })) {
|
|
614
|
+
if (value !== undefined)
|
|
615
|
+
requiredPlain(value, name);
|
|
616
|
+
}
|
|
617
|
+
if (input.aliases !== undefined) {
|
|
618
|
+
if (!Array.isArray(input.aliases))
|
|
619
|
+
throw new AppError("validation", "artifact aliases must be an array", 2);
|
|
620
|
+
for (const alias of input.aliases)
|
|
621
|
+
requiredPlain(alias, "artifact alias");
|
|
622
|
+
}
|
|
591
623
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
592
624
|
const run = resolveRun(context, project.id, input.runId);
|
|
593
625
|
if (["done", "failed", "skipped"].includes(input.status))
|
|
@@ -595,13 +627,17 @@ export function completeFlowRunStage(context, input) {
|
|
|
595
627
|
else
|
|
596
628
|
assertRunMutationAllowed(context, project.id, run.id);
|
|
597
629
|
const index = authoritativeIndex(run);
|
|
598
|
-
const now = context.now();
|
|
599
630
|
const stage = requiredPlain(input.stage, "stage");
|
|
600
631
|
const status = parseStageStatus(input.status);
|
|
601
632
|
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
602
633
|
if (!existing) {
|
|
603
634
|
throw new AppError("not_found", `Run stage is not attached: ${stage}`, 1, { run_id: run.id, stage });
|
|
604
635
|
}
|
|
636
|
+
return { project, run, index, stage, status, existing };
|
|
637
|
+
}
|
|
638
|
+
export function completeFlowRunStage(context, input) {
|
|
639
|
+
const { project, run, index, stage, status, existing } = prepareFlowRunStageCompletion(context, input);
|
|
640
|
+
const now = context.now();
|
|
605
641
|
const stageRun = {
|
|
606
642
|
...existing,
|
|
607
643
|
status,
|
|
@@ -717,7 +753,11 @@ export function resumeFlowRunStage(context, input) {
|
|
|
717
753
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
718
754
|
}
|
|
719
755
|
/** Pause the current stage for a non-user runtime blocker. */
|
|
720
|
-
export function
|
|
756
|
+
export function prepareFlowRunStageBlock(context, input) {
|
|
757
|
+
if (!["engine", "harness", "environment"].includes(input.kind) || typeof input.retryable !== "boolean")
|
|
758
|
+
throw new AppError("validation", "stage block requires a valid kind and boolean retryable", 2);
|
|
759
|
+
requiredPlain(input.code, "code");
|
|
760
|
+
requiredPlain(input.summary, "summary");
|
|
721
761
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
722
762
|
const run = resolveRun(context, project.id, input.runId);
|
|
723
763
|
assertRunMutationAllowed(context, project.id, run.id);
|
|
@@ -726,6 +766,10 @@ export function blockFlowRunStage(context, input) {
|
|
|
726
766
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
727
767
|
if (!stageRun || stageRun.status !== "running")
|
|
728
768
|
throw new AppError("invalid_stage_state", "Only a running stage can be blocked", 1, { run_id: run.id, stage, status: stageRun?.status ?? "missing" });
|
|
769
|
+
return { project, run, index, stage, stageRun };
|
|
770
|
+
}
|
|
771
|
+
export function blockFlowRunStage(context, input, prepared = prepareFlowRunStageBlock(context, input)) {
|
|
772
|
+
const { project, run, index, stage, stageRun } = prepared;
|
|
729
773
|
const now = context.now();
|
|
730
774
|
stageRun.status = "blocked";
|
|
731
775
|
stageRun.updated_at = now;
|
|
@@ -743,7 +787,7 @@ export function blockFlowRunStage(context, input) {
|
|
|
743
787
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
744
788
|
}
|
|
745
789
|
/** Continue the same stage after its non-user blocker was repaired externally. */
|
|
746
|
-
export function
|
|
790
|
+
export function prepareFlowRunStageUnblock(context, input) {
|
|
747
791
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
748
792
|
const run = resolveRun(context, project.id, input.runId);
|
|
749
793
|
assertRunMutationAllowed(context, project.id, run.id);
|
|
@@ -752,8 +796,11 @@ export function unblockFlowRunStage(context, input) {
|
|
|
752
796
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
753
797
|
if (!stageRun || stageRun.status !== "blocked" || !stageRun.blocker || stageRun.blocker.work_id !== input.workId)
|
|
754
798
|
throw new AppError("invalid_stage_state", "Stage unblock requires its matching runtime blocker", 1, { run_id: run.id, stage, work_id: input.workId, status: stageRun?.status ?? "missing" });
|
|
799
|
+
return { project, run, index, stage, stageRun, blocker: stageRun.blocker };
|
|
800
|
+
}
|
|
801
|
+
export function unblockFlowRunStage(context, input, prepared = prepareFlowRunStageUnblock(context, input)) {
|
|
802
|
+
const { project, run, index, stage, stageRun, blocker } = prepared;
|
|
755
803
|
const now = context.now();
|
|
756
|
-
const blocker = stageRun.blocker;
|
|
757
804
|
const blockedMs = durationMs(blocker.blocked_at, now) ?? 0;
|
|
758
805
|
stageRun.status = "running";
|
|
759
806
|
stageRun.updated_at = now;
|
|
@@ -793,12 +840,20 @@ export function setFlowRunRecoveryPaused(context, projectId, runId, paused) {
|
|
|
793
840
|
const project = requireProjectByRoot(context, run.project_root);
|
|
794
841
|
persistRunState(context, project, run, index);
|
|
795
842
|
}
|
|
796
|
-
export function
|
|
843
|
+
export function prepareFlowRunCompletion(context, input) {
|
|
844
|
+
if (input.manualOverrideReason !== undefined) {
|
|
845
|
+
requiredPlain(input.manualOverrideReason, "override reason");
|
|
846
|
+
if (!["cancelled", "failed"].includes(input.status))
|
|
847
|
+
throw new AppError("validation", "Manual RUN override may only cancel or fail a RUN", 2);
|
|
848
|
+
}
|
|
849
|
+
if (input.verdict !== undefined)
|
|
850
|
+
requiredPlain(input.verdict, "verdict");
|
|
851
|
+
if (input.nextAction !== undefined)
|
|
852
|
+
requiredPlain(input.nextAction, "next action");
|
|
797
853
|
const project = requireProjectByRoot(context, input.manualOverrideReason ? canonicalPath(input.projectRoot, false) : resolveProjectRoot(input.projectRoot));
|
|
798
854
|
const run = resolveRun(context, project.id, input.runId);
|
|
799
855
|
assertRunMutationAllowed(context, project.id, run.id);
|
|
800
856
|
const index = authoritativeIndex(run);
|
|
801
|
-
const now = context.now();
|
|
802
857
|
const status = parseRunStatus(input.status);
|
|
803
858
|
if (!['done', 'cancelled', 'failed'].includes(status)) {
|
|
804
859
|
throw new AppError('validation', 'completeFlowRun accepts only done, cancelled, or failed; use stage block/unblock for a recoverable blocker', 2, { status });
|
|
@@ -808,6 +863,11 @@ export function completeFlowRun(context, input) {
|
|
|
808
863
|
if (activeWork)
|
|
809
864
|
throw new AppError("active_work", "RUN cannot complete while Work remains active", 2, { run_id: run.id, work_id: activeWork.work_id, status: activeWork.status });
|
|
810
865
|
}
|
|
866
|
+
return { project, run, index, status };
|
|
867
|
+
}
|
|
868
|
+
export function completeFlowRun(context, input) {
|
|
869
|
+
const { project, run, index, status } = prepareFlowRunCompletion(context, input);
|
|
870
|
+
const now = context.now();
|
|
811
871
|
if (input.manualOverrideReason && (status === "cancelled" || status === "failed")) {
|
|
812
872
|
closeOpenStagesForOverride(index, status, now);
|
|
813
873
|
closeOpenWorksForOverride(context, project.id, run.id, status, now);
|
|
@@ -1273,11 +1333,15 @@ function materializedRunVariables(index) {
|
|
|
1273
1333
|
...(index.variables ?? {})
|
|
1274
1334
|
};
|
|
1275
1335
|
}
|
|
1276
|
-
function
|
|
1336
|
+
function prepareRunVariable(context, input) {
|
|
1277
1337
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
1278
1338
|
const run = resolveRun(context, project.id, input.runId);
|
|
1279
1339
|
assertRunMutationAllowed(context, project.id, run.id);
|
|
1280
1340
|
const index = authoritativeIndex(run);
|
|
1341
|
+
return { project, run, index };
|
|
1342
|
+
}
|
|
1343
|
+
function setRunVariable(context, input, value, prepared = prepareRunVariable(context, input)) {
|
|
1344
|
+
const { project, run, index } = prepared;
|
|
1281
1345
|
if (index.variables?.[input.key] === value)
|
|
1282
1346
|
return { ok: true, run_id: run.id, key: input.key, value, idempotent: true };
|
|
1283
1347
|
index.variables = { ...(index.variables ?? {}), [input.key]: value };
|
|
@@ -1328,10 +1392,12 @@ function readRuntimeSnapshot(file) {
|
|
|
1328
1392
|
function authoritativeIndex(run) {
|
|
1329
1393
|
try {
|
|
1330
1394
|
const state = JSON.parse(run.index_json);
|
|
1395
|
+
if (!state || typeof state !== "object" || Array.isArray(state) || !Array.isArray(state.stage_runs))
|
|
1396
|
+
throw new Error("RUN index must contain stage_runs");
|
|
1331
1397
|
return { ...state, schema_id: runSchemaId };
|
|
1332
1398
|
}
|
|
1333
1399
|
catch (error) {
|
|
1334
|
-
throw new AppError("
|
|
1400
|
+
throw new AppError("runtime_state_invalid", "RUN mechanical state in SQLite is missing or invalid", 1, { run_id: run.id, cause: String(error) });
|
|
1335
1401
|
}
|
|
1336
1402
|
}
|
|
1337
1403
|
function sanitizeTimelineValue(value, key) {
|
|
@@ -1486,7 +1552,7 @@ function parseStageStatus(value) {
|
|
|
1486
1552
|
}
|
|
1487
1553
|
return value;
|
|
1488
1554
|
}
|
|
1489
|
-
function
|
|
1555
|
+
export function prepareStageDirectory(value) {
|
|
1490
1556
|
const dir = requiredPlain(value, "dir");
|
|
1491
1557
|
if (!/^\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(dir)) {
|
|
1492
1558
|
throw new AppError("validation", "--dir must use NN-stage-slug format", 2, { dir });
|
|
@@ -1494,6 +1560,8 @@ function requiredStageDir(value) {
|
|
|
1494
1560
|
return dir;
|
|
1495
1561
|
}
|
|
1496
1562
|
function requiredPlain(value, label) {
|
|
1563
|
+
if (typeof value !== "string")
|
|
1564
|
+
throw new AppError("validation", `${label} must be a string`, 2);
|
|
1497
1565
|
const trimmed = value.trim();
|
|
1498
1566
|
if (!trimmed) {
|
|
1499
1567
|
throw new AppError("validation", `${label} must not be empty`, 2);
|
|
@@ -1501,6 +1569,8 @@ function requiredPlain(value, label) {
|
|
|
1501
1569
|
return trimmed;
|
|
1502
1570
|
}
|
|
1503
1571
|
function normalizeSlug(value) {
|
|
1572
|
+
if (typeof value !== "string")
|
|
1573
|
+
throw new AppError("validation", "--slug must be a string", 2);
|
|
1504
1574
|
const slug = value
|
|
1505
1575
|
.normalize("NFKD")
|
|
1506
1576
|
.replace(/[\u0300-\u036f]/g, "")
|
|
@@ -1518,6 +1588,8 @@ function resolveWorkspaceRoot(workspaceRoot) {
|
|
|
1518
1588
|
if (!fs.existsSync(absolute)) {
|
|
1519
1589
|
throw new AppError("not_found", `Workspace root does not exist: ${absolute}`, 1);
|
|
1520
1590
|
}
|
|
1591
|
+
if (!fs.statSync(absolute).isDirectory())
|
|
1592
|
+
throw new AppError("validation", "Workspace root must be a directory", 2, { workspace_root: absolute });
|
|
1521
1593
|
return fs.realpathSync(absolute);
|
|
1522
1594
|
}
|
|
1523
1595
|
/** Move one completed stage attempt and update only known durable path columns.
|
|
@@ -1,28 +1,49 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
5
|
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
-
import { getResourceDatabase } from "../storage/database.js";
|
|
6
|
+
import { getResourceDatabase, withResourceReadSnapshot } from "../storage/database.js";
|
|
6
7
|
import { AppError } from "../shared/errors.js";
|
|
7
8
|
import { harnessKey } from "./harness-config.js";
|
|
8
9
|
import { managedProcessBinding, resourceHome, processIsAlive } from "./managed-processes.js";
|
|
9
10
|
import { recoveryRuntimeAdmission } from "./run-recovery-runtime.js";
|
|
11
|
+
import { prepareTextFile, prepareJsonFile, prepareError } from "../cli/input-preparation.js";
|
|
10
12
|
const digest = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
|
11
13
|
export function validateRuntimeScopeManifest(value, scopeId) {
|
|
12
14
|
const item = value;
|
|
13
15
|
if (!item || typeof item !== "object" || Array.isArray(item) || Object.keys(item).some(key => !["schema_id", "scope_id", "manifest_path", "manifest_sha256", "execution_ids"].includes(key)) || item.schema_id !== "dd-flow/runtime-scope-manifest@1" || item.scope_id !== scopeId || typeof item.manifest_path !== "string" || !path.isAbsolute(item.manifest_path) || typeof item.manifest_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(item.manifest_sha256) || !Array.isArray(item.execution_ids) || item.execution_ids.some(id => typeof id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(id)) || new Set(item.execution_ids).size !== item.execution_ids.length)
|
|
14
16
|
throw new AppError("runtime_scope_manifest_invalid", "Scope manifest requires frozen source identity and unique execution IDs", 2);
|
|
15
|
-
const bytes =
|
|
17
|
+
const bytes = prepareTextFile(item.manifest_path, "scope manifest").text;
|
|
16
18
|
if (digest(bytes) !== item.manifest_sha256)
|
|
17
19
|
throw new AppError("runtime_scope_manifest_changed", "EVAL manifest no longer matches its frozen identity", 1);
|
|
18
|
-
|
|
20
|
+
let source;
|
|
21
|
+
try {
|
|
22
|
+
source = JSON.parse(bytes);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new AppError("runtime_scope_manifest_corrupt", "Retained EVAL manifest contains invalid JSON", 1);
|
|
26
|
+
}
|
|
27
|
+
if (!source || typeof source !== "object" || Array.isArray(source))
|
|
28
|
+
throw new AppError("runtime_scope_manifest_corrupt", "Retained EVAL manifest must be an object", 1);
|
|
19
29
|
if (source.run_id !== scopeId || !Array.isArray(source.executions) || JSON.stringify(source.executions.map(execution => execution?.id)) !== JSON.stringify(item.execution_ids))
|
|
20
30
|
throw new AppError("runtime_scope_manifest_invalid", "Scope execution inventory does not match the retained EVAL manifest", 2);
|
|
21
|
-
return { schema_id: item.schema_id, scope_id: scopeId, manifest_path: item.manifest_path, manifest_sha256: item.manifest_sha256, execution_ids: item.execution_ids };
|
|
31
|
+
return { schema_id: item.schema_id, scope_id: scopeId, manifest_path: item.manifest_path, manifest_sha256: item.manifest_sha256, execution_ids: [...item.execution_ids] };
|
|
22
32
|
}
|
|
23
33
|
export function publicRuntimeScopeWorker(worker) {
|
|
24
34
|
return { status: worker.status, generation: worker.generation, process_id: worker.process_id ?? null, owner_pid: worker.owner?.pid ?? null, error: worker.error ?? null, snapshot: worker.snapshot ?? null };
|
|
25
35
|
}
|
|
36
|
+
/** Captured manifests are runtime authority, not a fresh CLI argument. */
|
|
37
|
+
export function validateRetainedRuntimeScopeManifest(value, scopeId) {
|
|
38
|
+
try {
|
|
39
|
+
return validateRuntimeScopeManifest(value, scopeId);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof AppError && error.exitCode === 2)
|
|
43
|
+
throw new AppError("runtime_scope_manifest_corrupt", "Retained scope manifest is invalid or unavailable", 1, { scope_id: scopeId, cause_code: error.code });
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
26
47
|
/** Prepared native owners cannot send the first recovery Turn until the scope
|
|
27
48
|
* arbiter publishes their exact release. Fence disappearance alone is not ACK. */
|
|
28
49
|
export async function waitForRuntimeScopeRecovery(context, input, assertOwner) {
|
|
@@ -63,30 +84,56 @@ export async function waitForRuntimeScopeRecovery(context, input, assertOwner) {
|
|
|
63
84
|
}
|
|
64
85
|
/** Admission intent only. A persisted request is not physical settlement.
|
|
65
86
|
* The resource registry arbitrates all runtime homes in the selected EVAL. */
|
|
66
|
-
export function
|
|
67
|
-
|
|
68
|
-
if (!
|
|
69
|
-
throw new AppError("runtime_budget_invalid", "Scope control requires
|
|
87
|
+
export function prepareRuntimeScopeControl(scopeId, requestId, mode, manifest, context) {
|
|
88
|
+
prepareRuntimeScopeFence(scopeId, requestId);
|
|
89
|
+
if (!["pause", "stop"].includes(mode))
|
|
90
|
+
throw new AppError("runtime_budget_invalid", "Scope control requires pause or stop", 2);
|
|
91
|
+
const file = context && path.join(resourceHome(context), "runtime.sqlite");
|
|
92
|
+
if (file && fs.existsSync(file)) {
|
|
93
|
+
const db = new DatabaseSync(file, { readOnly: true });
|
|
94
|
+
try {
|
|
95
|
+
db.exec("PRAGMA busy_timeout = 4000");
|
|
96
|
+
// Another process may have created the file but not its first schema yet.
|
|
97
|
+
// No table means no retained receipt; execute rechecks after initialization.
|
|
98
|
+
const ready = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'managed_resources'").get();
|
|
99
|
+
const prior = ready ? db.prepare("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-request' AND resource_key = ?").get(digest(JSON.stringify([scopeId, requestId]))) : undefined;
|
|
100
|
+
if (prior)
|
|
101
|
+
return { receipt: scopeControlReceipt(prior.metadata_json, mode) };
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
db.close();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { manifest: manifest === undefined ? undefined : validateRuntimeScopeManifest(manifest, scopeId) };
|
|
108
|
+
}
|
|
109
|
+
function scopeControlReceipt(json, mode) {
|
|
110
|
+
const receipt = JSON.parse(json);
|
|
111
|
+
if (receipt.requested_mode !== mode)
|
|
112
|
+
throw new AppError("runtime_scope_control_conflict", "Scope request identity was already used for another mode", 1);
|
|
113
|
+
return receipt;
|
|
114
|
+
}
|
|
115
|
+
export function requestRuntimeScopeControl(context, scopeId, requestId, mode, manifest, prepared = prepareRuntimeScopeControl(scopeId, requestId, mode, manifest, context)) {
|
|
116
|
+
if (prepared.receipt)
|
|
117
|
+
return prepared.receipt;
|
|
118
|
+
const declaration = prepared.manifest;
|
|
70
119
|
const db = getResourceDatabase(resourceHome(context));
|
|
71
120
|
const requestKey = digest(JSON.stringify([scopeId, requestId]));
|
|
72
121
|
db.beginWriteTransaction();
|
|
73
122
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
123
|
+
const prior = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-request' AND resource_key = ?", [requestKey]);
|
|
124
|
+
if (prior) {
|
|
125
|
+
const receipt = scopeControlReceipt(prior.metadata_json, mode);
|
|
126
|
+
db.exec("COMMIT");
|
|
127
|
+
return receipt;
|
|
128
|
+
}
|
|
129
|
+
if (declaration !== undefined) {
|
|
130
|
+
const payload = JSON.stringify(declaration);
|
|
76
131
|
const saved = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-manifest' AND resource_key = ?", [scopeId]);
|
|
77
132
|
if (saved && saved.metadata_json !== payload)
|
|
78
133
|
throw new AppError("runtime_scope_manifest_changed", "Scope already belongs to another frozen EVAL manifest", 1);
|
|
79
134
|
if (!saved)
|
|
80
135
|
db.run("INSERT INTO managed_resources (resource_kind,resource_key,owner_id,lease_token,lease_expires_at,process_id,metadata_json,created_at,updated_at) VALUES ('scope-manifest',?,?,?,?,NULL,?,?,?)", [scopeId, scopeId, declaration.manifest_sha256, context.now(), payload, context.now(), context.now()]);
|
|
81
136
|
}
|
|
82
|
-
const prior = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-request' AND resource_key = ?", [requestKey]);
|
|
83
|
-
if (prior) {
|
|
84
|
-
const receipt = JSON.parse(prior.metadata_json);
|
|
85
|
-
if (receipt.requested_mode !== mode)
|
|
86
|
-
throw new AppError("runtime_scope_control_conflict", "Scope request identity was already used for another mode", 1);
|
|
87
|
-
db.exec("COMMIT");
|
|
88
|
-
return receipt;
|
|
89
|
-
}
|
|
90
137
|
if (db.get("SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]))
|
|
91
138
|
throw new AppError("runtime_scope_stopped", "Terminal EVAL cancellation cannot become recoverable control", 1);
|
|
92
139
|
const current = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
|
|
@@ -130,31 +177,43 @@ export function scopeLocalProcessRole(record) {
|
|
|
130
177
|
}
|
|
131
178
|
/** Local checks and EVAL observers share the scope fence, not provider capacity. */
|
|
132
179
|
export function admitRuntimeCheck(context, id, leaseToken) {
|
|
133
|
-
|
|
134
|
-
|
|
180
|
+
if (typeof id !== "string" || !id.trim() || typeof leaseToken !== "string" || !leaseToken.trim())
|
|
181
|
+
throw new AppError("validation", "Check admission requires a process ID and lease token", 2);
|
|
182
|
+
const db = getResourceDatabase(resourceHome(context), "read_existing");
|
|
135
183
|
try {
|
|
136
|
-
|
|
184
|
+
// One statement observes the owner and both admission fences together,
|
|
185
|
+
// without taking a writer lock or initializing an absent registry.
|
|
186
|
+
const record = db.get(`SELECT p.*,
|
|
187
|
+
EXISTS(SELECT 1 FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = json_extract(p.metadata_json, '$.budget.scope_id')) AS scope_fenced,
|
|
188
|
+
(SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = json_extract(p.metadata_json, '$.budget.scope_id')) AS scope_control
|
|
189
|
+
FROM managed_processes p WHERE p.id = ?`, [id]);
|
|
137
190
|
if (!record || record.lease_token !== leaseToken || !["eval-baseline", "eval-observer", "check"].includes(record.kind) || record.state !== "running" || !processIsAlive(record))
|
|
138
191
|
throw new AppError("process_ownership_unknown", "Local process requires its confirmed live owner", 1);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
192
|
+
let budget;
|
|
193
|
+
try {
|
|
194
|
+
const metadata = JSON.parse(record.metadata_json);
|
|
195
|
+
const retained = parseRuntimeBudget(metadata.budget ? JSON.stringify(metadata.budget) : undefined);
|
|
196
|
+
if (!retained || (record.kind !== "check" ? scopeLocalProcessRole(record) === null : metadata.role !== "check" || !record.check_id))
|
|
197
|
+
throw new Error("Missing retained EVAL scope or operation");
|
|
198
|
+
budget = retained;
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
throw new AppError("runtime_artifact_invalid", "Local process has invalid retained scope or operation metadata", 1, { process_id: record.id, cause: String(error) });
|
|
202
|
+
}
|
|
203
|
+
if (record.scope_fenced)
|
|
204
|
+
throw new AppError("runtime_scope_stopped", "This runtime scope no longer admits productive dispatch", 1, { scope_id: budget.scope_id });
|
|
205
|
+
if (record.scope_control)
|
|
206
|
+
throw new AppError("runtime_scope_controlled", "This runtime scope is under operator control", 1, { control: JSON.parse(record.scope_control) });
|
|
145
207
|
return { admitted: true, process_id: record.id, scope_id: budget.scope_id };
|
|
146
208
|
}
|
|
147
|
-
|
|
148
|
-
db.
|
|
149
|
-
throw error;
|
|
209
|
+
finally {
|
|
210
|
+
db.close?.();
|
|
150
211
|
}
|
|
151
212
|
}
|
|
152
213
|
/** Scope selection uses retained registration, never a current profile or PID search. */
|
|
153
214
|
export function runtimeBudgetStatus(context, scopeId) {
|
|
154
215
|
parseRuntimeBudget(JSON.stringify({ schema_id: "dd-flow/runtime-budget@1", scope_id: scopeId, per_harness: {} }));
|
|
155
|
-
|
|
156
|
-
db.exec("SAVEPOINT runtime_scope_status");
|
|
157
|
-
try {
|
|
216
|
+
return withResourceReadSnapshot(resourceHome(context), db => {
|
|
158
217
|
const fence = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'provider-fence' AND resource_key = ?", [scopeId]);
|
|
159
218
|
const control = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ?", [scopeId]);
|
|
160
219
|
const processes = db.all("SELECT * FROM managed_processes WHERE json_extract(metadata_json, '$.budget.scope_id') = ? ORDER BY id", [scopeId]);
|
|
@@ -173,21 +232,18 @@ export function runtimeBudgetStatus(context, scopeId) {
|
|
|
173
232
|
const worker = workerGeneration ? db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-worker' AND resource_key = ?", [digest(JSON.stringify([scopeId, workerGeneration]))]) : null;
|
|
174
233
|
const drain = intent ? db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-drain' AND resource_key = ?", [digest(JSON.stringify([scopeId, intent.generation]))]) : null;
|
|
175
234
|
const result = { scope_id: scopeId, dispatch_blocked: Boolean(fence || control), fence: fence ? JSON.parse(fence.metadata_json) : null, control: intent, worker: worker ? publicRuntimeScopeWorker(JSON.parse(worker.metadata_json)) : null, drain: drain ? JSON.parse(drain.metadata_json) : null, manifest: manifest ? JSON.parse(manifest.metadata_json) : null, resume: resume ? { ...resume, dispatch_blocked: Boolean(fence || control), current: resume.status === "released" ? releaseCurrent : !fence && intent?.generation === resume.generation } : null, release: release ? { ...release, current: releaseCurrent } : null, processes, provider_turns: providerTurns };
|
|
176
|
-
db.exec("RELEASE runtime_scope_status");
|
|
177
235
|
return result;
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
db.exec("ROLLBACK TO runtime_scope_status");
|
|
181
|
-
db.exec("RELEASE runtime_scope_status");
|
|
182
|
-
throw error;
|
|
183
|
-
}
|
|
236
|
+
});
|
|
184
237
|
}
|
|
185
238
|
/** Persist admission shutdown independently of client/process lease expiry.
|
|
186
239
|
* This is a dispatch fence, not evidence that the existing native tree stopped. */
|
|
187
|
-
export function
|
|
240
|
+
export function prepareRuntimeScopeFence(scopeId, requestId) {
|
|
188
241
|
parseRuntimeBudget(JSON.stringify({ schema_id: "dd-flow/runtime-budget@1", scope_id: scopeId, per_harness: {} }));
|
|
189
|
-
if (!requestId || requestId.length > 256)
|
|
190
|
-
throw new AppError("runtime_budget_invalid", "Scope
|
|
242
|
+
if (typeof requestId !== "string" || !requestId.trim() || requestId.length > 256)
|
|
243
|
+
throw new AppError("runtime_budget_invalid", "Scope control requires a nonempty request identity of at most 256 characters", 2);
|
|
244
|
+
}
|
|
245
|
+
export function fenceRuntimeBudget(context, scopeId, requestId) {
|
|
246
|
+
prepareRuntimeScopeFence(scopeId, requestId);
|
|
191
247
|
const db = getResourceDatabase(resourceHome(context));
|
|
192
248
|
db.beginWriteTransaction();
|
|
193
249
|
try {
|
|
@@ -323,9 +379,25 @@ function scopeRecoveryLoadAllowed(context, scopeId, record, input) {
|
|
|
323
379
|
AND json_extract(o.input_json, '$.request_id') = ? AND json_extract(o.input_json, '$.control_id') = ctl.control_id`, [source.control_id, record.owner_id, record.project_id, record.run_id, source.recovery_id, source.resume_request_id]);
|
|
324
380
|
return Boolean(prepared) && recoveryRuntimeAdmission(context, record, { stateDir: metadata.state_dir, daemonId: input.daemonId, operationId: input.operationId, operation: input.operation, nativeSessionId: input.sessionId });
|
|
325
381
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
382
|
+
export function prepareProviderTurnRelease(input) {
|
|
383
|
+
if (Boolean(input.operationId) === Boolean(input.observationFile))
|
|
384
|
+
return prepareError(new AppError("usage", "Provide exactly one of --operation-id or --observation-file", 2));
|
|
385
|
+
if (!input.observationFile)
|
|
386
|
+
return { observation: null };
|
|
387
|
+
// This file is a retained daemon receipt, not model-authored input. Broken
|
|
388
|
+
// receipt syntax/schema is infrastructure failure, never a retryable typo.
|
|
389
|
+
let observed;
|
|
390
|
+
try {
|
|
391
|
+
observed = prepareJsonFile(input.observationFile, "budget observation").value;
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
throw new AppError("runtime_budget_evidence_invalid", "Cannot read valid JSON from the daemon budget observation", 1);
|
|
395
|
+
}
|
|
396
|
+
if (!observed || typeof observed !== "object" || Array.isArray(observed) || typeof observed.daemon_id !== "string" || typeof observed.session_id !== "string" || !observed.session_id || typeof observed.settled !== "boolean" || !Array.isArray(observed.operation_ids) || observed.operation_ids.some(id => typeof id !== "string" || !id))
|
|
397
|
+
throw new AppError("runtime_budget_evidence_invalid", "Budget observation has an invalid receipt shape", 1);
|
|
398
|
+
return { observation: observed };
|
|
399
|
+
}
|
|
400
|
+
export function releaseProviderTurns(context, record, input, prepared = prepareProviderTurnRelease(input)) {
|
|
329
401
|
const metadata = JSON.parse(record.metadata_json);
|
|
330
402
|
if (!metadata.state_dir)
|
|
331
403
|
throw new AppError("process_ownership_unknown", "Budget release has no retained state directory", 1);
|
|
@@ -338,8 +410,8 @@ export function releaseProviderTurns(context, record, input) {
|
|
|
338
410
|
const parent = path.resolve(metadata.state_dir, "budget-observations");
|
|
339
411
|
if (path.dirname(path.resolve(input.observationFile)) !== parent)
|
|
340
412
|
throw new AppError("runtime_budget_evidence_invalid", "Budget observation is outside its daemon directory", 2);
|
|
341
|
-
const observed =
|
|
342
|
-
if (observed.daemon_id !== input.daemonId || observed.settled !== true
|
|
413
|
+
const observed = prepared.observation;
|
|
414
|
+
if (!observed || observed.daemon_id !== input.daemonId || observed.settled !== true)
|
|
343
415
|
throw new AppError("runtime_budget_evidence_invalid", "Budget release requires fresh settled Session evidence", 1);
|
|
344
416
|
sessionId = observed.session_id;
|
|
345
417
|
operationIds = observed.operation_ids;
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
5
|
import { getResourceDatabase } from "../storage/database.js";
|
|
6
6
|
import { resourceHome } from "./managed-processes.js";
|
|
7
|
-
import {
|
|
7
|
+
import { validateRetainedRuntimeScopeManifest } from "./runtime-budget.js";
|
|
8
8
|
/** Archive a complete append-only prefix, not a claim that no later evidence
|
|
9
9
|
* can arrive. The caller holds the registry transaction; never take the EVAL
|
|
10
10
|
* journal lock here (its writer may itself be waiting for the registry).
|
|
@@ -34,7 +34,7 @@ export function captureScopeJournal(context, control, manifest) {
|
|
|
34
34
|
if (request?.data?.request_id !== control.request_id || request.data.mode !== control.requested_mode)
|
|
35
35
|
throw new AppError("runtime_scope_control_superseded", "EVAL journal belongs to another control request", 1);
|
|
36
36
|
const manifestBytes = fs.readFileSync(manifest.manifest_path, "utf8");
|
|
37
|
-
|
|
37
|
+
validateRetainedRuntimeScopeManifest(manifest, control.scope_id);
|
|
38
38
|
const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
|
39
39
|
if (hash(manifestBytes) !== manifest.manifest_sha256 || !fs.readFileSync(source).subarray(0, prefix.length).equals(prefix))
|
|
40
40
|
throw new AppError("runtime_scope_journal_changed", "EVAL source changed during capture", 1);
|
|
@@ -5,7 +5,7 @@ import { createContext } from "../runtime/context.js";
|
|
|
5
5
|
import { getResourceDatabase } from "../storage/database.js";
|
|
6
6
|
import { AppError } from "../shared/errors.js";
|
|
7
7
|
import { finishManagedProcess, managedLeaseReleased, managedProcessBinding, managedProcessStatus, processTreeIsAlive, resourceHome, stopManagedProcess } from "./managed-processes.js";
|
|
8
|
-
import { requestRuntimeScopeControl, runtimeBudgetStatus, scopeLocalProcessRole,
|
|
8
|
+
import { requestRuntimeScopeControl, runtimeBudgetStatus, scopeLocalProcessRole, validateRetainedRuntimeScopeManifest } from "./runtime-budget.js";
|
|
9
9
|
import { nativeInventoryVersion, recordRunControlIntent, retainedDaemonOperations, runControlStatus } from "./run-control.js";
|
|
10
10
|
import { requestRunControl } from "./run-control-worker.js";
|
|
11
11
|
import { adapterSessionId } from "./harness-adapter.js";
|
|
@@ -92,7 +92,7 @@ export function retainScopeDrain(context, control, processes, runs, native, prob
|
|
|
92
92
|
let manifest = null, journal = null, manifestReason = "scope_manifest_required";
|
|
93
93
|
if (latest.manifest) {
|
|
94
94
|
try {
|
|
95
|
-
manifest =
|
|
95
|
+
manifest = validateRetainedRuntimeScopeManifest(latest.manifest, control.scope_id);
|
|
96
96
|
journal = captureScopeJournal(context, control, manifest);
|
|
97
97
|
manifestReason = "scope_operation_reconciliation_required";
|
|
98
98
|
}
|