@deksden-com/dd-flow-cli 0.9.0-beta.7 → 0.9.0-beta.74
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 +396 -0
- package/README.md +65 -0
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +81 -9
- package/dist/cli/run-cli.js +442 -46
- package/dist/harness-runtime/bin/dd-agy.mjs +37 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +25 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +33 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +30 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +21 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +83 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +215 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +423 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +59 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +141 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +438 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +125 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +470 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +274 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +174 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +198 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +98 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +644 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +910 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +129 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +260 -0
- package/dist/harness-runtime/lib/model-observations.mjs +77 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +34 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +36 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +19 -0
- package/dist/harness-runtime/lib/process-json.mjs +70 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +17 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +153 -0
- package/dist/runtime/context.js +3 -1
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +2 -2
- package/dist/schemas/code-work-batch.schema.json +4 -3
- package/dist/schemas/code-work-result.schema.json +4 -4
- package/dist/schemas/harness-config.schema.json +23 -0
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/run-control-receipt.schema.json +99 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/cleanup.js +8 -5
- package/dist/services/cli-operation-classifier.js +11 -1
- package/dist/services/code-checks.js +543 -96
- package/dist/services/controller-fanout.js +120 -0
- package/dist/services/dashboard.js +9 -0
- package/dist/services/engines.js +50 -19
- package/dist/services/eval-snapshots.js +639 -60
- package/dist/services/execution-policy.js +177 -0
- package/dist/services/external-work-launch.js +110 -0
- package/dist/services/harness-adapter.js +147 -24
- package/dist/services/harness-config.js +68 -0
- package/dist/services/hooks.js +413 -168
- package/dist/services/lanes.js +1 -1
- package/dist/services/lifecycle-command.js +74 -6
- package/dist/services/lifecycle-invocations.js +637 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-processes.js +138 -22
- package/dist/services/merge-queue.js +6 -6
- package/dist/services/merge-server.js +75 -29
- package/dist/services/migrations.js +1 -1
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/projects.js +7 -3
- package/dist/services/prompts.js +4 -2
- package/dist/services/protocols.js +11 -2
- package/dist/services/recovery-observation-budget.js +61 -0
- package/dist/services/recovery-snapshot-database.js +107 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +363 -0
- package/dist/services/run-control.js +876 -0
- package/dist/services/run-controller-adapter.js +211 -0
- package/dist/services/run-controller-capture.js +134 -0
- package/dist/services/run-controller-process.js +189 -0
- package/dist/services/run-controller-recovery.js +222 -0
- package/dist/services/run-controller-state.js +31 -0
- package/dist/services/run-controller.js +733 -0
- package/dist/services/run-engine-bindings.js +20 -62
- package/dist/services/run-fork.js +124 -0
- package/dist/services/run-observations.js +112 -0
- package/dist/services/run-recovery-runtime.js +69 -0
- package/dist/services/run-recovery.js +327 -0
- package/dist/services/runs.js +280 -40
- package/dist/services/runtime-budget.js +362 -0
- package/dist/services/runtime-scope-capture.js +50 -0
- package/dist/services/runtime-scope-control.js +450 -0
- package/dist/services/runtime-scope-resume.js +543 -0
- package/dist/services/runtime-scope-stop.js +99 -0
- package/dist/services/runtime-scope-worker.js +210 -0
- package/dist/services/runtime-service.js +96 -0
- package/dist/services/schema-validation.js +12 -12
- package/dist/services/stage-context.js +2 -2
- package/dist/services/stage-lifecycle.js +32 -10
- package/dist/services/stage-pause.js +25 -13
- package/dist/services/usage.js +37 -93
- package/dist/services/vnext-code-review.js +149 -89
- package/dist/services/vnext-code.js +223 -215
- package/dist/services/vnext-execution-profile.js +5 -3
- package/dist/services/vnext-fanout.js +135 -21
- package/dist/services/vnext-merge.js +258 -89
- package/dist/services/vnext-plan-review.js +75 -64
- package/dist/services/vnext-plan.js +77 -29
- package/dist/services/vnext-protocolize.js +21 -14
- package/dist/services/vnext-specify.js +26 -16
- package/dist/services/work-registry.js +592 -154
- package/dist/services/workspace-bootstrap.js +76 -0
- package/dist/shared/errors.js +12 -0
- package/dist/storage/database.js +341 -31
- package/dist/storage/writer-contract.js +77 -0
- package/dist/storage/writer-migration.js +102 -0
- package/package.json +20 -13
- package/tools/repair-paused-run-status.mjs +59 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
|
+
import { createContext } from "../runtime/context.js";
|
|
6
|
+
import { AppError } from "../shared/errors.js";
|
|
7
|
+
import { getResourceDatabase } from "../storage/database.js";
|
|
8
|
+
import { managedProcessBinding, processIsAlive, processTreeIsAlive, resourceHome } from "./managed-processes.js";
|
|
9
|
+
import { runtimeBudgetStatus, scopeLocalProcessRole, validateRuntimeScopeManifest } from "./runtime-budget.js";
|
|
10
|
+
import { nativeInventoryVersion, retainedDaemonOperations } from "./run-control.js";
|
|
11
|
+
import { adapterSessionId, runHarnessAdapter } from "./harness-adapter.js";
|
|
12
|
+
import { recordControllerResume, resumeRunController } from "./run-controller-recovery.js";
|
|
13
|
+
import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
|
|
14
|
+
import { assertHistoricalDaemonSettlement } from "./native-daemon-history.js";
|
|
15
|
+
const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
|
16
|
+
const nativeLaunchEnvironmentKeys = ["CODEX_HOME", "DD_FLOW_CONFIG_HOME", "DD_FLOW_HOME", "DD_FLOW_BIN", "DD_FLOW_RESOURCE_HOME"];
|
|
17
|
+
function nativeLaunchEnvironment(context, saved) {
|
|
18
|
+
const env = { ...context.env };
|
|
19
|
+
for (const name of ["DD_FLOW_CONTROLLER_TOKEN", "DD_FLOW_CONTROL_TOKEN", "DD_FLOW_SCOPE_CONTROL_TOKEN", "DD_FLOW_DAEMON_ID", "DD_FLOW_RUNTIME_OWNER"])
|
|
20
|
+
delete env[name];
|
|
21
|
+
if (!saved || Object.keys(saved).some(name => !nativeLaunchEnvironmentKeys.includes(name)))
|
|
22
|
+
throw new AppError("runtime_scope_native_launch_required", "Native launch has an unsupported environment", 1);
|
|
23
|
+
for (const name of nativeLaunchEnvironmentKeys) {
|
|
24
|
+
if (saved[name] === null)
|
|
25
|
+
delete env[name];
|
|
26
|
+
else if (typeof saved[name] === "string")
|
|
27
|
+
env[name] = saved[name];
|
|
28
|
+
else
|
|
29
|
+
throw new AppError("runtime_scope_native_launch_required", "Native launch must retain explicit configuration presence", 1);
|
|
30
|
+
}
|
|
31
|
+
return env;
|
|
32
|
+
}
|
|
33
|
+
/** The existing detached scope owner drives preparation; clients never own
|
|
34
|
+
* the lifecycle. All-role release remains distinct from these RUN receipts. */
|
|
35
|
+
export async function prepareRuntimeScopeResume(context, scopeId, generation, releaseRequestId) {
|
|
36
|
+
const db = getResourceDatabase(resourceHome(context));
|
|
37
|
+
const assertCurrent = () => {
|
|
38
|
+
const latest = runtimeBudgetStatus(context, scopeId);
|
|
39
|
+
if (latest.fence || latest.control?.generation !== generation)
|
|
40
|
+
throw new AppError("runtime_scope_control_superseded", "Scope resume preparation lost its generation", 1);
|
|
41
|
+
};
|
|
42
|
+
const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume' AND resource_key = ?", [scopeId]);
|
|
43
|
+
if (!row)
|
|
44
|
+
return null;
|
|
45
|
+
const intent = JSON.parse(row.metadata_json);
|
|
46
|
+
if (intent.generation !== generation)
|
|
47
|
+
return null;
|
|
48
|
+
if (releaseRequestId !== undefined && releaseRequestId !== intent.request_id)
|
|
49
|
+
throw new AppError("runtime_scope_resume_conflict", "Release belongs to another resume request", 1);
|
|
50
|
+
if (releaseRequestId !== undefined && intent.status === "released") {
|
|
51
|
+
const prior = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-release' AND resource_key = ?", [hash(JSON.stringify([scopeId, generation]))]);
|
|
52
|
+
if (!prior)
|
|
53
|
+
throw new AppError("runtime_scope_release_unproven", "Released scope lost its retained grant", 1);
|
|
54
|
+
const latest = runtimeBudgetStatus(context, scopeId);
|
|
55
|
+
const last = db.get("SELECT MAX(json_extract(metadata_json, '$.generation')) AS generation FROM managed_resources WHERE resource_kind = 'scope-control-request' AND owner_id = ?", [scopeId]);
|
|
56
|
+
return { ...JSON.parse(prior.metadata_json), status: "released", dispatch_blocked: latest.dispatch_blocked, current: !latest.fence && !latest.control && last?.generation === generation, settled: false, reused: true };
|
|
57
|
+
}
|
|
58
|
+
assertCurrent();
|
|
59
|
+
const runs = [];
|
|
60
|
+
for (const source of intent.runs) {
|
|
61
|
+
let target;
|
|
62
|
+
try {
|
|
63
|
+
target = createContext({ ...context.env, DD_FLOW_HOME: source.runtime_home, DD_FLOW_RESOURCE_HOME: resourceHome(context), DD_FLOW_ENGINE_MODE: "1" });
|
|
64
|
+
const project = target.db.get("SELECT root FROM projects WHERE id = ?", [source.project_id]);
|
|
65
|
+
const controller = target.db.get("SELECT manifest_json FROM run_controllers WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','cancelled','superseded')", [source.project_id, source.run_id]);
|
|
66
|
+
const manifest = controller ? JSON.parse(controller.manifest_json) : null;
|
|
67
|
+
if (!project || manifest?.budget?.scope_id !== scopeId || manifest.resource_home !== resourceHome(context))
|
|
68
|
+
throw new AppError("runtime_scope_owner_unknown", "Resume RUN no longer belongs to this retained scope", 1);
|
|
69
|
+
const input = { projectRoot: project.root, runId: source.run_id, requestId: source.resume_request_id, controlId: source.control_id };
|
|
70
|
+
db.beginWriteTransaction();
|
|
71
|
+
try {
|
|
72
|
+
assertCurrent();
|
|
73
|
+
recordControllerResume(target, input);
|
|
74
|
+
db.exec("COMMIT");
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
db.exec("ROLLBACK");
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
assertCurrent();
|
|
81
|
+
const receipt = await resumeRunController(target, { ...input, waitMs: 0 });
|
|
82
|
+
assertCurrent();
|
|
83
|
+
runs.push({ ...source, receipt });
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
runs.push({ ...source, pending: true, reason: error instanceof AppError ? error.code : "runtime_scope_resume_failed", message: String(error) });
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
target?.db.close?.();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const native = [];
|
|
93
|
+
for (const source of intent.native) {
|
|
94
|
+
try {
|
|
95
|
+
const key = hash(JSON.stringify([scopeId, generation, source.process_id]));
|
|
96
|
+
const saved = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]);
|
|
97
|
+
const restarting = saved && ["starting", "restarted"].includes(JSON.parse(saved.metadata_json).status);
|
|
98
|
+
const retired = restarting ? null : await retireScopeNative(context, intent, source, assertCurrent);
|
|
99
|
+
native.push(restarting || retired?.status === "retired" ? await restartScopeNative(context, intent, source, assertCurrent) : retired);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
native.push({ ...source, pending: true, reason: error instanceof AppError ? error.code : "runtime_scope_resume_failed", message: String(error) });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
assertCurrent();
|
|
106
|
+
// Recheck all RUNs together after asynchronous preparation of every role.
|
|
107
|
+
// These locks will also be the boundary for the eventual release publication.
|
|
108
|
+
const lockedRuns = new Map(), heldRuns = [];
|
|
109
|
+
let registryHeld = false, runPreparationConsistent = false;
|
|
110
|
+
let released = null, releaseError = null, releaseMessage = null;
|
|
111
|
+
try {
|
|
112
|
+
for (const home of [...new Set(intent.runs.map((source) => source.runtime_home))].sort())
|
|
113
|
+
lockedRuns.set(home, createContext({ ...context.env, DD_FLOW_HOME: home, DD_FLOW_RESOURCE_HOME: resourceHome(context), DD_FLOW_ENGINE_MODE: "1" }));
|
|
114
|
+
db.beginWriteTransaction();
|
|
115
|
+
registryHeld = true;
|
|
116
|
+
for (const target of lockedRuns.values()) {
|
|
117
|
+
target.db.beginWriteTransaction();
|
|
118
|
+
heldRuns.push(target);
|
|
119
|
+
}
|
|
120
|
+
assertCurrent();
|
|
121
|
+
for (const [index, source] of intent.runs.entries()) {
|
|
122
|
+
const target = lockedRuns.get(source.runtime_home);
|
|
123
|
+
const proof = preparedScopeRun(target, { scopeId, requestId: source.resume_request_id, projectId: source.project_id, runId: source.run_id, controlId: source.control_id, recoveryId: source.recovery_id }, assertCurrent);
|
|
124
|
+
runs[index] = { ...runs[index], ...proof, ...(proof.prepared === true ? { pending: false, reason: null } : {}) };
|
|
125
|
+
}
|
|
126
|
+
runPreparationConsistent = true;
|
|
127
|
+
if (releaseRequestId !== undefined) {
|
|
128
|
+
const receipt = publishScopeRelease(context, intent, runs, native);
|
|
129
|
+
db.exec("COMMIT");
|
|
130
|
+
registryHeld = false;
|
|
131
|
+
released = receipt;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
releaseError = error instanceof AppError ? error.code : "scope_writer_boundary_unavailable";
|
|
136
|
+
releaseMessage = error instanceof Error ? error.message : String(error);
|
|
137
|
+
for (const [index, run] of runs.entries())
|
|
138
|
+
runs[index] = { ...run, prepared: false, pending: true, reason: error instanceof AppError ? error.code : "scope_writer_boundary_unavailable" };
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
try {
|
|
142
|
+
for (const target of heldRuns.reverse())
|
|
143
|
+
target.db.exec("ROLLBACK");
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
try {
|
|
147
|
+
if (registryHeld)
|
|
148
|
+
db.exec("ROLLBACK");
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
for (const target of lockedRuns.values())
|
|
152
|
+
target.db.close?.();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (released)
|
|
157
|
+
return { ...released, status: "released", dispatch_blocked: false, current: true, settled: false, reused: false };
|
|
158
|
+
assertCurrent();
|
|
159
|
+
return { scope_id: scopeId, generation, request_id: intent.request_id, status: "preparing", dispatch_blocked: true, settled: false, release_error: releaseError ? { code: releaseError, message: releaseMessage } : null, run_preparation_consistent: runPreparationConsistent, runs, native, probes: intent.probes, pending_reasons: [...new Set(["scope_release_required", ...(releaseError ? [releaseError] : []), ...(!runPreparationConsistent ? ["scope_writer_boundary_unavailable"] : []), ...(runs.some(item => item.prepared !== true) ? ["run_recovery_required"] : []), ...(native.some(item => item?.prepared !== true) ? ["non_work_recovery_required"] : []), ...(intent.probes.length ? ["probe_reconciliation_required"] : [])])] };
|
|
160
|
+
}
|
|
161
|
+
/** Called only inside the registry + all-RUN writer boundary above. */
|
|
162
|
+
function publishScopeRelease(context, intent, runs, native) {
|
|
163
|
+
const db = getResourceDatabase(resourceHome(context));
|
|
164
|
+
if (runs.some(run => run.prepared !== true) || native.some(owner => owner?.prepared !== true))
|
|
165
|
+
throw new AppError("runtime_scope_preparation_required", "Every owned RUN and native Session must be prepared", 1);
|
|
166
|
+
const manifest = validateRuntimeScopeManifest(intent.manifest, intent.scope_id);
|
|
167
|
+
const artifactRow = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-capture' AND resource_key = ?", [intent.capture_key]);
|
|
168
|
+
const artifact = artifactRow ? JSON.parse(artifactRow.metadata_json) : null;
|
|
169
|
+
const journal = path.join(path.dirname(manifest.manifest_path), "events.jsonl");
|
|
170
|
+
if (artifact?.scope_id !== intent.scope_id || artifact.generation !== intent.generation || artifact.journal_source !== journal || typeof artifact.journal_bytes !== "string" || !fs.readFileSync(journal).equals(Buffer.from(artifact.journal_bytes)))
|
|
171
|
+
throw new AppError("runtime_scope_journal_changed", "Release source changed after capture", 1);
|
|
172
|
+
const operations = new Map();
|
|
173
|
+
const events = artifact.journal_bytes.trimEnd().split("\n").map((line) => JSON.parse(line));
|
|
174
|
+
for (const event of events) {
|
|
175
|
+
if (!event.type.startsWith("dev.dd.eval.operation."))
|
|
176
|
+
continue;
|
|
177
|
+
const phase = event.type.slice("dev.dd.eval.operation.".length), id = event.data.operation_id;
|
|
178
|
+
const prior = operations.get(id);
|
|
179
|
+
if (typeof id !== "string" || !id || typeof event.data.operation !== "string" || !event.data.operation || !["requested", "started", "suspended", "observation_lost", "completed", "failed", "cancelled"].includes(phase) || !["requested", "started"].includes(phase) && !prior?.started || prior?.terminal && ["requested", "started"].includes(phase))
|
|
180
|
+
throw new AppError("runtime_scope_journal_invalid", "Release operation has invalid dispatch history", 1);
|
|
181
|
+
if (prior && (prior.operation !== event.data.operation || prior.execution !== (event.executionid ?? null)))
|
|
182
|
+
throw new AppError("runtime_scope_journal_invalid", "Release operation changed its ownership", 1);
|
|
183
|
+
const terminal = ["completed", "failed", "cancelled"].includes(phase) ? phase : null;
|
|
184
|
+
const terminalHash = terminal ? hash(JSON.stringify({ terminal, result: terminal === "completed" ? event.data.result : { error: event.data.error ?? null } })) : null;
|
|
185
|
+
if (terminal && prior?.terminal && (prior.terminal !== terminal || prior.terminal_hash !== terminalHash))
|
|
186
|
+
throw new AppError("runtime_scope_journal_invalid", "Release operation has conflicting terminal outcomes", 1);
|
|
187
|
+
operations.set(id, { operation: event.data.operation, execution: event.executionid ?? null, started: prior?.started === true || phase === "started", terminal: terminal ?? prior?.terminal ?? null, terminal_hash: terminalHash ?? prior?.terminal_hash ?? null });
|
|
188
|
+
}
|
|
189
|
+
const probes = intent.probes.map(raw => {
|
|
190
|
+
const probe = raw;
|
|
191
|
+
const record = db.get("SELECT * FROM managed_processes WHERE id = ?", [probe?.process_id ?? ""]);
|
|
192
|
+
if (record?.kind === "eval-observer") {
|
|
193
|
+
if (scopeLocalProcessRole(record) !== "observer" || record.owner_id !== intent.scope_id || probe.physical_settled !== true || processTreeIsAlive(record))
|
|
194
|
+
throw new AppError("scope_operation_reconciliation_required", "EVAL observer requires its retired physical owner", 1);
|
|
195
|
+
// This proves only local writer retirement. The operation loop below
|
|
196
|
+
// still requires every interrupted experiment operation to reconcile.
|
|
197
|
+
return { process_id: record.id, operation_id: record.operation_id, role: "observer", outcome: "retired", reconciliation: "captured_journal" };
|
|
198
|
+
}
|
|
199
|
+
const execution = manifest.execution_ids.find(id => record?.operation_id?.startsWith(`${intent.scope_id}:${id}:launch:`));
|
|
200
|
+
const operationId = execution ? `${intent.scope_id}:${execution}:launch` : "", checkId = record?.operation_id?.slice(operationId.length + 1);
|
|
201
|
+
const parent = operations.get(operationId);
|
|
202
|
+
const references = events.filter((event) => event.type === "dev.dd.eval.execution.context_prepared" && event.executionid === execution).map((event) => event.data.baseline_admission);
|
|
203
|
+
const reference = references[0], file = execution ? path.join(path.dirname(manifest.manifest_path), "executions", execution, "baseline-admission", "receipt.json") : "";
|
|
204
|
+
if (!record || record.kind !== "eval-baseline" || record.owner_id !== intent.scope_id || JSON.parse(record.metadata_json).role !== "probe" || probe.physical_settled !== true || processTreeIsAlive(record) || !execution || !checkId || !/^[a-z0-9-]+$/.test(checkId) || !parent?.started || parent.operation !== `execution.${execution}.launch` || parent.execution !== execution || !reference || reference.file !== file || !/^[a-f0-9]{64}$/.test(reference.sha256 ?? "") || references.some((item) => JSON.stringify(item) !== JSON.stringify(reference)))
|
|
205
|
+
throw new AppError("probe_reconciliation_required", "Local probe has no retained baseline admission outcome", 1);
|
|
206
|
+
const bytes = fs.readFileSync(file), receipt = JSON.parse(bytes.toString("utf8"));
|
|
207
|
+
const source = JSON.parse(artifact.manifest_bytes);
|
|
208
|
+
if (hash(bytes) !== reference.sha256 || receipt.schema_id !== "dd-eval/baseline-admission@1" || receipt.status !== "passed" || receipt.checkpoint_id !== source.input_checkpoint?.id || receipt.checkpoint_sha256 !== source.input_checkpoint?.sha256 || !receipt.checkpoint_id || !receipt.checkpoint_sha256 || receipt.policy_sha256 !== reference.policy_sha256 || !/^[a-f0-9]{64}$/.test(receipt.policy_sha256 ?? "") || !Array.isArray(receipt.checks) || receipt.checks.filter((check) => check.id === checkId).length !== 1 || receipt.checks.some((check) => check.exit_code !== 0 || check.timed_out !== false))
|
|
209
|
+
throw new AppError("probe_reconciliation_required", "Local probe baseline evidence is incomplete or changed", 1);
|
|
210
|
+
return { process_id: record.id, operation_id: record.operation_id, parent_operation_id: operationId, outcome: "completed", receipt_file: file, receipt_sha256: reference.sha256 };
|
|
211
|
+
});
|
|
212
|
+
for (const [id, operation] of operations) {
|
|
213
|
+
if (!operation.started || operation.terminal === "completed" || operation.terminal === "cancelled")
|
|
214
|
+
continue;
|
|
215
|
+
if (operation.terminal || !operation.execution || !manifest.execution_ids.includes(operation.execution) || operation.operation !== `execution.${operation.execution}.launch` || id !== `${intent.scope_id}:${operation.execution}:launch`)
|
|
216
|
+
throw new AppError("scope_operation_reconciliation_required", "Release has an unresolved EVAL operation", 1);
|
|
217
|
+
const retained = JSON.parse(fs.readFileSync(path.join(path.dirname(manifest.manifest_path), "executions", operation.execution, "managed-runtime.json"), "utf8"));
|
|
218
|
+
if (retained.schema_id !== "dd-eval/managed-runtime@1" || !runs.some(run => run.runtime_home === retained.runtime_root && run.run_id === retained.run_id))
|
|
219
|
+
throw new AppError("scope_operation_reconciliation_required", "Unresolved execution has no prepared retained RUN", 1);
|
|
220
|
+
}
|
|
221
|
+
const current = runtimeBudgetStatus(context, intent.scope_id);
|
|
222
|
+
if (current.fence || current.control?.generation !== intent.generation || current.provider_turns.length)
|
|
223
|
+
throw new AppError("runtime_scope_inventory_changed", "Release lost its scope generation or provider settlement", 1);
|
|
224
|
+
const bindings = [...intent.process_bindings];
|
|
225
|
+
for (const run of runs)
|
|
226
|
+
bindings.push(...run.process_bindings);
|
|
227
|
+
for (const owner of native)
|
|
228
|
+
bindings.push(owner.replacement_binding);
|
|
229
|
+
for (const proof of [...runs.map(run => run.native), ...native]) {
|
|
230
|
+
if (nativeInventoryVersion(proof.state_dir) !== proof.inventory_version)
|
|
231
|
+
throw new AppError("runtime_scope_inventory_changed", "Native preparation changed before release", 1);
|
|
232
|
+
const state = readNativeDaemonState(proof.state_dir);
|
|
233
|
+
if (state.provider_process) {
|
|
234
|
+
const daemon = db.get("SELECT * FROM managed_processes WHERE id = ?", [state.resource_process?.id ?? ""]);
|
|
235
|
+
const provider = db.get("SELECT * FROM managed_processes WHERE id = ?", [state.provider_process.id ?? ""]);
|
|
236
|
+
const daemonMetadata = daemon ? JSON.parse(daemon.metadata_json) : null, providerMetadata = provider ? JSON.parse(provider.metadata_json) : null;
|
|
237
|
+
if (!daemon || !provider || !bindings.some(binding => JSON.stringify(binding) === JSON.stringify(managedProcessBinding(daemon, resourceHome(context))))
|
|
238
|
+
|| daemon.pid !== state.pid || daemon.lease_token !== state.resource_process?.lease_token
|
|
239
|
+
|| provider.kind !== daemon.kind.replace(/-daemon$/, "-provider") || provider.pid !== state.provider_pid || !provider.pid || !provider.pid_started_at || provider.lease_token !== state.provider_process.lease_token
|
|
240
|
+
|| provider.owner_id !== daemon.owner_id || provider.project_id !== daemon.project_id || provider.run_id !== daemon.run_id || provider.work_id !== daemon.work_id
|
|
241
|
+
|| providerMetadata.owner_pid !== daemon.pid || providerMetadata.owner_pid_started_at !== daemon.pid_started_at
|
|
242
|
+
|| providerMetadata.state_dir !== proof.state_dir || providerMetadata.dd_flow_home !== daemonMetadata.dd_flow_home || providerMetadata.role !== daemonMetadata.role || JSON.stringify(providerMetadata.budget) !== JSON.stringify(daemonMetadata.budget))
|
|
243
|
+
throw new AppError("runtime_scope_owner_unknown", "Native provider has no exact prepared daemon ownership", 1);
|
|
244
|
+
bindings.push(managedProcessBinding(provider, resourceHome(context)));
|
|
245
|
+
}
|
|
246
|
+
if (nativeInventoryVersion(proof.state_dir) !== proof.inventory_version)
|
|
247
|
+
throw new AppError("runtime_scope_inventory_changed", "Native preparation changed while checking its provider", 1);
|
|
248
|
+
}
|
|
249
|
+
const actual = current.processes.map(record => managedProcessBinding(record, resourceHome(context)));
|
|
250
|
+
if (actual.some(binding => !bindings.some(expected => JSON.stringify(binding) === JSON.stringify(expected))) || bindings.some(expected => !actual.some(binding => JSON.stringify(binding) === JSON.stringify(expected))))
|
|
251
|
+
throw new AppError("runtime_scope_inventory_changed", "Release has an unverified process registration", 1);
|
|
252
|
+
const key = hash(JSON.stringify([intent.scope_id, intent.generation])), now = context.now();
|
|
253
|
+
const receipt = { scope_id: intent.scope_id, generation: intent.generation, request_id: intent.request_id, source_request_id: intent.source_request_id, capture_key: intent.capture_key, journal_sha256: hash(artifact.journal_bytes), run_operations: runs.map(run => run.run_operation), native, probes, process_bindings: actual, released_at: now };
|
|
254
|
+
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-resume-release',?,?,?,?,NULL,?,?,?)", [key, intent.scope_id, key, now, JSON.stringify(receipt), now, now]);
|
|
255
|
+
if (db.run("DELETE FROM managed_resources WHERE resource_kind = 'scope-control' AND resource_key = ? AND json_extract(metadata_json, '$.generation') = ?", [intent.scope_id, intent.generation]).changes !== 1)
|
|
256
|
+
throw new AppError("runtime_scope_control_superseded", "Release lost its control generation", 1);
|
|
257
|
+
db.run("UPDATE managed_resources SET metadata_json = json_set(metadata_json, '$.status', 'released'), updated_at = ? WHERE resource_kind = 'scope-resume' AND resource_key = ?", [now, intent.scope_id]);
|
|
258
|
+
return receipt;
|
|
259
|
+
}
|
|
260
|
+
/** A preparation snapshot, not an unfence. The release arbiter must repeat
|
|
261
|
+
* these reads while holding every participating RUN writer boundary. */
|
|
262
|
+
function preparedScopeRun(context, input, assertCurrent) {
|
|
263
|
+
const registry = getResourceDatabase(resourceHome(context));
|
|
264
|
+
registry.exec("SAVEPOINT scope_run_registry");
|
|
265
|
+
let readingRun = false;
|
|
266
|
+
try {
|
|
267
|
+
context.db.exec("SAVEPOINT scope_run_preparation");
|
|
268
|
+
readingRun = true;
|
|
269
|
+
assertCurrent();
|
|
270
|
+
const controller = context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','cancelled','superseded')", [input.projectId, input.runId]);
|
|
271
|
+
if (!controller)
|
|
272
|
+
throw new AppError("runtime_scope_owner_unknown", "Prepared RUN lost its controller", 1);
|
|
273
|
+
const state = JSON.parse(controller.state_json), manifest = JSON.parse(controller.manifest_json);
|
|
274
|
+
const operation = context.db.get(`SELECT o.operation_id, o.receipt_json FROM run_controller_operations o
|
|
275
|
+
JOIN run_controls ctl ON ctl.control_id = ? AND ctl.project_id = ? AND ctl.run_id = ?
|
|
276
|
+
JOIN run_recovery_guards g ON g.recovery_id = ctl.recovery_id
|
|
277
|
+
WHERE o.controller_id = ? AND o.generation = ? AND o.status = 'accepted'
|
|
278
|
+
AND ctl.status = 'resuming' AND ctl.recovery_id = ? AND g.generation = o.generation AND g.status = 'resuming'
|
|
279
|
+
AND json_extract(o.input_json, '$.action') = 'control_resume' AND json_extract(o.input_json, '$.request_id') = ?`, [input.controlId, input.projectId, input.runId, controller.controller_id, controller.generation, input.recoveryId, input.requestId]);
|
|
280
|
+
const prepared = operation?.receipt_json ? JSON.parse(operation.receipt_json) : null;
|
|
281
|
+
if (prepared?.prepared !== true)
|
|
282
|
+
return { prepared: false, pending: true, reason: "run_recovery_preparation_required" };
|
|
283
|
+
const session = state.current_session === null ? null : state.sessions[state.current_session];
|
|
284
|
+
const owner = registry.get("SELECT * FROM managed_processes WHERE id = ?", [controller.process_id]);
|
|
285
|
+
if (!session || manifest.budget?.scope_id !== input.scopeId || manifest.resource_home !== resourceHome(context) || state.recovery?.operation_id !== operation.operation_id || state.recovery?.control_id !== input.controlId || state.recovery?.recovery_id !== input.recoveryId || prepared.recovery_id !== input.recoveryId || prepared.session_id !== session.id || !owner || owner.lease_token !== controller.process_lease_token || owner.pid !== controller.owner_pid || owner.owner_id !== controller.controller_id || !processIsAlive(owner))
|
|
286
|
+
throw new AppError("runtime_scope_owner_unknown", "RUN preparation lost its current owner or Session", 1);
|
|
287
|
+
const native = requireManagedDaemonBinding(context, { stateDir: session.state_dir, projectId: input.projectId, runId: input.runId, ownerId: session.owner.owner_id });
|
|
288
|
+
const loaded = context.db.get("SELECT receipt_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'completed'", [prepared.resume_operation_id, controller.controller_id, controller.generation]);
|
|
289
|
+
const loadReceipt = loaded?.receipt_json ? JSON.parse(loaded.receipt_json) : null;
|
|
290
|
+
if (!processIsAlive(native.process) || native.process.id !== prepared.process_id || native.state.daemon_id !== prepared.daemon_id || loadReceipt?.settled !== true || adapterSessionId(loadReceipt) !== session.id)
|
|
291
|
+
throw new AppError("runtime_scope_owner_unknown", "RUN preparation has no current native load acknowledgement", 1);
|
|
292
|
+
return { prepared: true, run_operation: { runtime_home: context.ddFlowHome, controller_id: controller.controller_id, operation_id: operation.operation_id }, process_bindings: [managedProcessBinding(owner, resourceHome(context)), managedProcessBinding(native.process, resourceHome(context))], native: { state_dir: session.state_dir, daemon_id: prepared.daemon_id, session_id: session.id, inventory_version: nativeInventoryVersion(session.state_dir) } };
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
try {
|
|
296
|
+
if (readingRun)
|
|
297
|
+
context.db.exec("RELEASE scope_run_preparation");
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
registry.exec("RELEASE scope_run_registry");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function restartScopeNative(context, intent, source, assertCurrent) {
|
|
305
|
+
const db = getResourceDatabase(resourceHome(context)), key = hash(JSON.stringify([intent.scope_id, intent.generation, source.process_id]));
|
|
306
|
+
const saved = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]);
|
|
307
|
+
const operation = JSON.parse(saved.metadata_json);
|
|
308
|
+
const startup = source.operations.find(item => item.operation === "daemon.start" && item.result?.daemon_id === source.daemon_id);
|
|
309
|
+
const launch = startup.launch;
|
|
310
|
+
const state = () => JSON.parse(fs.readFileSync(path.join(source.state_dir, "daemon.json"), "utf8"));
|
|
311
|
+
const assertRetired = () => {
|
|
312
|
+
assertCurrent();
|
|
313
|
+
const old = db.get("SELECT * FROM managed_processes WHERE id = ?", [source.process_id]);
|
|
314
|
+
if (!old || processTreeIsAlive(old) || JSON.stringify(managedProcessBinding(old, resourceHome(context))) !== JSON.stringify(intent.process_bindings.find(item => item.process_id === source.process_id)))
|
|
315
|
+
throw new AppError("runtime_scope_owner_unknown", "The retired native tree changed or is still alive", 1);
|
|
316
|
+
};
|
|
317
|
+
assertRetired();
|
|
318
|
+
let dispatch = false;
|
|
319
|
+
db.beginWriteTransaction();
|
|
320
|
+
try {
|
|
321
|
+
assertRetired();
|
|
322
|
+
if (operation.status === "retired") {
|
|
323
|
+
const old = state();
|
|
324
|
+
if (old.daemon_id !== source.daemon_id || old.shutdown_state !== "clean")
|
|
325
|
+
throw new AppError("runtime_scope_owner_unknown", "Native restart lost its clean predecessor", 1);
|
|
326
|
+
operation.status = "starting";
|
|
327
|
+
operation.restart_operation_id = `scope-restart:${key}`;
|
|
328
|
+
dispatch = true;
|
|
329
|
+
db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [JSON.stringify(operation), context.now(), key]);
|
|
330
|
+
}
|
|
331
|
+
db.exec("COMMIT");
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
db.exec("ROLLBACK");
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
if (!operation.restart_receipt) {
|
|
338
|
+
if (dispatch) {
|
|
339
|
+
const env = nativeLaunchEnvironment(context, launch.env);
|
|
340
|
+
assertRetired();
|
|
341
|
+
operation.restart_receipt = await runHarnessAdapter({ executable: launch.executable, args: launch.args, cwd: launch.cwd, env, runtimeOwner: source.runtime_owner, operationId: operation.restart_operation_id, timeoutMs: 30_000 });
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
const retained = retainedDaemonOperations(source.state_dir).find(item => item.operation_id === operation.restart_operation_id && item.operation === "daemon.start" && item.predecessor_daemon_id === source.daemon_id && item.state === "completed");
|
|
345
|
+
if (!retained)
|
|
346
|
+
return { ...source, ...operation, pending: true, reason: "native_restart_outcome_unknown" };
|
|
347
|
+
operation.restart_receipt = retained.result;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
assertRetired();
|
|
351
|
+
const daemon = state(), replacement = db.get("SELECT * FROM managed_processes WHERE id = ?", [daemon.resource_process?.id ?? ""]);
|
|
352
|
+
const metadata = replacement ? JSON.parse(replacement.metadata_json) : null;
|
|
353
|
+
if (!replacement || !replacement.kind.endsWith("-daemon") || replacement.id === source.process_id || !processIsAlive(replacement) || replacement.pid !== daemon.pid || replacement.lease_token !== daemon.resource_process?.lease_token || replacement.owner_id !== source.runtime_owner.owner_id || replacement.run_id || replacement.work_id || replacement.project_id !== (source.runtime_owner.project_id ?? null) || metadata?.role !== source.role || metadata?.state_dir !== source.state_dir || metadata?.dd_flow_home !== source.runtime_owner.dd_flow_home || metadata?.adapter_executable !== launch.executable || JSON.stringify(metadata?.budget) !== JSON.stringify(source.runtime_owner.budget) || daemon.daemon_id === source.daemon_id || daemon.daemon_id !== operation.restart_receipt.daemon_id || operation.restart_receipt.already_running === true || JSON.stringify(daemon.config?.runtime_owner) !== JSON.stringify(source.runtime_owner))
|
|
354
|
+
throw new AppError("runtime_scope_owner_unknown", "Native restart has no fresh bound physical owner", 1);
|
|
355
|
+
const binding = managedProcessBinding(replacement, resourceHome(context));
|
|
356
|
+
if (operation.replacement_binding && JSON.stringify(operation.replacement_binding) !== JSON.stringify(binding))
|
|
357
|
+
throw new AppError("runtime_scope_owner_unknown", "Native replacement registration changed", 1);
|
|
358
|
+
operation.status = "restarted";
|
|
359
|
+
operation.replacement_binding = binding;
|
|
360
|
+
db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [JSON.stringify(operation), context.now(), key]);
|
|
361
|
+
const assertReplacement = () => {
|
|
362
|
+
assertRetired();
|
|
363
|
+
const current = db.get("SELECT * FROM managed_processes WHERE id = ?", [replacement.id]), latest = state();
|
|
364
|
+
if (!current || !processIsAlive(current) || JSON.stringify(managedProcessBinding(current, resourceHome(context))) !== JSON.stringify(binding) || latest.daemon_id !== daemon.daemon_id || latest.resource_process?.id !== current.id || latest.resource_process?.lease_token !== current.lease_token || JSON.stringify(latest.config?.runtime_owner) !== JSON.stringify(source.runtime_owner))
|
|
365
|
+
throw new AppError("runtime_scope_owner_unknown", "Session loading lost its native replacement", 1);
|
|
366
|
+
};
|
|
367
|
+
if (!Array.isArray(source.sessions) || !source.sessions.length || source.sessions.some(item => typeof item.session_id !== "string" || !item.session_id) || new Set(source.sessions.map(item => item.session_id)).size !== source.sessions.length)
|
|
368
|
+
throw new AppError("runtime_scope_owner_unknown", "Native recovery has no exact Session inventory", 1);
|
|
369
|
+
const sessions = [];
|
|
370
|
+
for (const session of source.sessions) {
|
|
371
|
+
const operationId = `scope-load:${hash(JSON.stringify([intent.scope_id, intent.generation, source.process_id, session.session_id]))}`;
|
|
372
|
+
let load, dispatchLoad = false;
|
|
373
|
+
db.beginWriteTransaction();
|
|
374
|
+
try {
|
|
375
|
+
assertReplacement();
|
|
376
|
+
const row = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native-load' AND resource_key = ?", [operationId]);
|
|
377
|
+
if (row)
|
|
378
|
+
load = JSON.parse(row.metadata_json);
|
|
379
|
+
else {
|
|
380
|
+
load = { scope_id: intent.scope_id, generation: intent.generation, request_id: intent.request_id, source_process_id: source.process_id, process_id: replacement.id, daemon_id: daemon.daemon_id, session_id: session.session_id, status: "dispatching" };
|
|
381
|
+
const now = context.now();
|
|
382
|
+
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-resume-native-load',?,?,?,?,NULL,?,?,?)", [operationId, intent.scope_id, operationId, now, JSON.stringify(load), now, now]);
|
|
383
|
+
dispatchLoad = true;
|
|
384
|
+
}
|
|
385
|
+
if (load.scope_id !== intent.scope_id || load.generation !== intent.generation || load.request_id !== intent.request_id || load.source_process_id !== source.process_id || load.process_id !== replacement.id || load.daemon_id !== daemon.daemon_id || load.session_id !== session.session_id)
|
|
386
|
+
throw new AppError("runtime_scope_owner_unknown", "Retained Session load belongs to another physical owner", 1);
|
|
387
|
+
db.exec("COMMIT");
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
db.exec("ROLLBACK");
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
if (!load.receipt) {
|
|
394
|
+
if (dispatchLoad) {
|
|
395
|
+
assertReplacement();
|
|
396
|
+
const args = launch.args.slice(2).filter((value, index, all) => value !== "--session-id" && all[index - 1] !== "--session-id");
|
|
397
|
+
load.receipt = await runHarnessAdapter({ executable: launch.executable, args: ["session", "resume", ...args, "--session-id", session.session_id, "--json"], cwd: launch.cwd, env: nativeLaunchEnvironment(context, launch.env), runtimeOwner: source.runtime_owner, operationId, timeoutMs: 30_000 });
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
const retained = retainedDaemonOperations(source.state_dir).find(item => item.operation_id === operationId && item.operation === "session.resume" && item.daemon_id === daemon.daemon_id && item.session_id === session.session_id && item.state === "completed");
|
|
401
|
+
if (!retained)
|
|
402
|
+
return { ...source, ...operation, sessions, pending: true, reason: "native_session_load_outcome_unknown" };
|
|
403
|
+
load.receipt = retained.result;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
assertReplacement();
|
|
407
|
+
if (adapterSessionId(load.receipt) !== session.session_id || load.receipt.settled !== true)
|
|
408
|
+
throw new AppError("runtime_scope_session_unsettled", "Native load did not confirm the same settled Session", 1);
|
|
409
|
+
load.status = "prepared";
|
|
410
|
+
db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-resume-native-load' AND resource_key = ?", [JSON.stringify(load), context.now(), operationId]);
|
|
411
|
+
sessions.push({ session_id: session.session_id, operation_id: operationId, receipt: load.receipt, prepared: true });
|
|
412
|
+
}
|
|
413
|
+
return { ...source, ...operation, inventory_version: nativeInventoryVersion(source.state_dir), sessions, prepared: true, pending: true, reason: "scope_release_required" };
|
|
414
|
+
}
|
|
415
|
+
async function retireScopeNative(context, intent, source, assertCurrent) {
|
|
416
|
+
const db = getResourceDatabase(resourceHome(context)), key = hash(JSON.stringify([intent.scope_id, intent.generation, source.process_id]));
|
|
417
|
+
const record = db.get("SELECT * FROM managed_processes WHERE id = ?", [source.process_id]);
|
|
418
|
+
const frozen = intent.process_bindings.find((binding) => binding.process_id === source.process_id);
|
|
419
|
+
const state = () => JSON.parse(fs.readFileSync(path.join(source.state_dir, "daemon.json"), "utf8"));
|
|
420
|
+
const assertOwner = () => {
|
|
421
|
+
assertCurrent();
|
|
422
|
+
const current = db.get("SELECT * FROM managed_processes WHERE id = ?", [source.process_id]), daemon = state();
|
|
423
|
+
if (!current || current.run_id || !["judge", "probe"].includes(source.role) || JSON.stringify(managedProcessBinding(current, resourceHome(context))) !== JSON.stringify(frozen) || daemon.daemon_id !== source.daemon_id || daemon.resource_process?.id !== current.id || daemon.resource_process?.lease_token !== current.lease_token || JSON.stringify(daemon.config?.runtime_owner) !== JSON.stringify(source.runtime_owner))
|
|
424
|
+
throw new AppError("runtime_scope_owner_unknown", "Native retirement lost its frozen physical owner", 1);
|
|
425
|
+
};
|
|
426
|
+
assertOwner();
|
|
427
|
+
const startup = source.operations?.filter(operation => operation.operation === "daemon.start" && operation.state === "completed" && operation.result?.daemon_id === source.daemon_id);
|
|
428
|
+
const launch = (startup?.length === 1 ? startup[0].launch : null);
|
|
429
|
+
let launchOwner;
|
|
430
|
+
try {
|
|
431
|
+
launchOwner = JSON.parse(launch?.runtime_owner ?? "null");
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
launchOwner = null;
|
|
435
|
+
}
|
|
436
|
+
if (!launch || launch.schema_id !== "dd-flow/native-launch@1" || launch.executable !== source.runtime_owner?.adapter_executable || !path.isAbsolute(launch.executable) || !path.isAbsolute(launch.cwd) || !Array.isArray(launch.args) || launch.args[0] !== "daemon" || launch.args[1] !== "start" || launch.args[launch.args.indexOf("--state-dir") + 1] !== source.state_dir || !isDeepStrictEqual(launchOwner, source.runtime_owner))
|
|
437
|
+
throw new AppError("runtime_scope_native_launch_required", "Native recovery requires the original owned launch contract", 1);
|
|
438
|
+
if (!launch.env || Object.keys(launch.env).some(name => !nativeLaunchEnvironmentKeys.includes(name)) || nativeLaunchEnvironmentKeys.some(name => launch.env[name] !== null && typeof launch.env[name] !== "string") || launch.args.some(arg => typeof arg !== "string"))
|
|
439
|
+
throw new AppError("runtime_scope_native_launch_required", "Native recovery requires explicit configuration presence before retiring its owner", 1);
|
|
440
|
+
let operation;
|
|
441
|
+
let dispatch = false;
|
|
442
|
+
db.beginWriteTransaction();
|
|
443
|
+
try {
|
|
444
|
+
assertOwner();
|
|
445
|
+
const saved = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [key]);
|
|
446
|
+
if (saved)
|
|
447
|
+
operation = JSON.parse(saved.metadata_json);
|
|
448
|
+
else {
|
|
449
|
+
if (nativeInventoryVersion(source.state_dir) !== source.inventory_version)
|
|
450
|
+
throw new AppError("runtime_scope_inventory_changed", "Native owner changed before retirement", 1);
|
|
451
|
+
operation = { scope_id: intent.scope_id, generation: intent.generation, request_id: intent.request_id, process_id: source.process_id, daemon_id: source.daemon_id, operation_id: `scope-retire:${key}`, status: "retiring" };
|
|
452
|
+
const now = context.now();
|
|
453
|
+
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-resume-native',?,?,?,?,NULL,?,?,?)", [key, intent.scope_id, key, now, JSON.stringify(operation), now, now]);
|
|
454
|
+
dispatch = true;
|
|
455
|
+
}
|
|
456
|
+
db.exec("COMMIT");
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
db.exec("ROLLBACK");
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
if (!operation.receipt) {
|
|
463
|
+
if (dispatch) {
|
|
464
|
+
assertOwner();
|
|
465
|
+
operation.receipt = await runHarnessAdapter({ executable: launch.executable, args: ["daemon", "stop", "--state-dir", source.state_dir, "--json"], env: { ...context.env, DD_FLOW_HOME: source.runtime_owner.dd_flow_home, DD_FLOW_RESOURCE_HOME: resourceHome(context) }, operationId: operation.operation_id, timeoutMs: 30_000 });
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
const retained = retainedDaemonOperations(source.state_dir).find(item => item.operation_id === operation.operation_id && item.operation === "daemon.stop" && item.daemon_id === source.daemon_id && item.state === "completed");
|
|
469
|
+
if (!retained)
|
|
470
|
+
return { ...source, ...operation, pending: true, reason: "native_retirement_outcome_unknown" };
|
|
471
|
+
operation.receipt = retained.result;
|
|
472
|
+
}
|
|
473
|
+
assertOwner();
|
|
474
|
+
db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [JSON.stringify(operation), context.now(), key]);
|
|
475
|
+
}
|
|
476
|
+
if (operation.receipt.stopped !== true || operation.receipt.clean !== true || state().shutdown_state !== "clean" || processTreeIsAlive(record))
|
|
477
|
+
return { ...source, ...operation, pending: true, reason: "native_retirement_settlement_required" };
|
|
478
|
+
assertOwner();
|
|
479
|
+
operation.status = "retired";
|
|
480
|
+
db.run("UPDATE managed_resources SET metadata_json = ?, updated_at = ? WHERE resource_kind = 'scope-resume-native' AND resource_key = ?", [JSON.stringify(operation), context.now(), key]);
|
|
481
|
+
return { ...source, ...operation, pending: true, reason: "native_restart_required" };
|
|
482
|
+
}
|
|
483
|
+
/** Freeze the preparation plan under the same arbiter as stop. This accepts
|
|
484
|
+
* ownership work, not productive dispatch; only all-role release may unfence. */
|
|
485
|
+
export function recordRuntimeScopeResume(context, input) {
|
|
486
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !Number.isSafeInteger(input.generation) || input.generation < 1 || !/^[a-f0-9]{64}$/.test(input.captureKey))
|
|
487
|
+
throw new AppError("runtime_scope_resume_invalid", "Scope resume requires its exact generation, capture and request identity", 2);
|
|
488
|
+
const db = getResourceDatabase(resourceHome(context)), key = hash(JSON.stringify([input.scopeId, input.requestId]));
|
|
489
|
+
db.beginWriteTransaction();
|
|
490
|
+
try {
|
|
491
|
+
const latest = runtimeBudgetStatus(context, input.scopeId);
|
|
492
|
+
const control = latest.control;
|
|
493
|
+
const prior = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume-request' AND resource_key = ?", [key]);
|
|
494
|
+
if (prior) {
|
|
495
|
+
const receipt = JSON.parse(prior.metadata_json);
|
|
496
|
+
if (receipt.generation !== input.generation || receipt.capture_key !== input.captureKey)
|
|
497
|
+
throw new AppError("runtime_scope_resume_conflict", "Resume request identity was used for another source", 1);
|
|
498
|
+
const active = latest.resume;
|
|
499
|
+
const sameRequest = active?.generation === input.generation && active.request_id === input.requestId;
|
|
500
|
+
db.exec("COMMIT");
|
|
501
|
+
return { ...receipt, ...(sameRequest ? active : {}), ...(sameRequest && active.status === "released" ? { release: latest.release } : {}), current: sameRequest && active.current === true, reused: true };
|
|
502
|
+
}
|
|
503
|
+
if (latest.fence || control?.generation !== input.generation)
|
|
504
|
+
throw new AppError("runtime_scope_control_superseded", "Resume source is not the current recoverable scope control", 1);
|
|
505
|
+
const existing = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-resume' AND resource_key = ?", [input.scopeId]);
|
|
506
|
+
if (existing && JSON.parse(existing.metadata_json).generation === input.generation)
|
|
507
|
+
throw new AppError("runtime_scope_resume_conflict", "This scope generation already has a resume owner", 1);
|
|
508
|
+
const drainRow = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-drain' AND resource_key = ?", [hash(JSON.stringify([input.scopeId, input.generation]))]);
|
|
509
|
+
const drain = drainRow ? JSON.parse(drainRow.metadata_json) : null;
|
|
510
|
+
if (drain?.scope_id !== input.scopeId || drain.generation !== input.generation || drain.requested_mode !== control.requested_mode || drain.physical_settled !== true || drain.capture?.journal?.artifact_key !== input.captureKey || drain.capture.journal.trailing_bytes !== 0 || !Array.isArray(drain.capture.run_captures) || drain.capture.run_captures.some((run) => run.status !== "verified"))
|
|
511
|
+
throw new AppError("runtime_scope_capture_required", "Resume requires a complete all-role drain and verified RUN captures", 1);
|
|
512
|
+
const manifest = validateRuntimeScopeManifest(drain.capture.manifest, input.scopeId);
|
|
513
|
+
if (JSON.stringify(manifest) !== JSON.stringify(latest.manifest))
|
|
514
|
+
throw new AppError("runtime_scope_manifest_changed", "Resume scope manifest changed after capture", 1);
|
|
515
|
+
const bindings = latest.processes.map(record => managedProcessBinding(record, resourceHome(context)));
|
|
516
|
+
const ordered = (items) => JSON.stringify([...items].sort((a, b) => a.process_id.localeCompare(b.process_id)));
|
|
517
|
+
if (!Array.isArray(drain.process_bindings) || ordered(bindings) !== ordered(drain.process_bindings) || latest.provider_turns.length)
|
|
518
|
+
throw new AppError("runtime_scope_inventory_changed", "Owned process or provider inventory changed after capture", 1);
|
|
519
|
+
for (const owner of drain.native ?? []) {
|
|
520
|
+
if (owner.historical === true) {
|
|
521
|
+
assertHistoricalDaemonSettlement(context, input.scopeId, owner);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (typeof owner.state_dir !== "string" || owner.native_settled !== true || nativeInventoryVersion(owner.state_dir) !== owner.inventory_version)
|
|
525
|
+
throw new AppError("runtime_scope_inventory_changed", "Native owner changed after capture", 1);
|
|
526
|
+
}
|
|
527
|
+
const artifactRow = db.get("SELECT metadata_json FROM managed_resources WHERE resource_kind = 'scope-control-capture' AND resource_key = ?", [input.captureKey]);
|
|
528
|
+
const artifact = artifactRow ? JSON.parse(artifactRow.metadata_json) : null;
|
|
529
|
+
if (artifact?.scope_id !== input.scopeId || artifact.generation !== input.generation || typeof artifact.manifest_bytes !== "string" || typeof artifact.journal_bytes !== "string" || artifact.journal_source !== path.join(path.dirname(manifest.manifest_path), "events.jsonl") || hash(artifact.manifest_bytes) !== manifest.manifest_sha256 || hash(JSON.stringify([input.scopeId, input.generation, manifest.manifest_sha256, hash(artifact.journal_bytes)])) !== input.captureKey || !fs.readFileSync(artifact.journal_source).equals(Buffer.from(artifact.journal_bytes)))
|
|
530
|
+
throw new AppError("runtime_scope_journal_changed", "Resume requires the current immutable EVAL journal capture", 1);
|
|
531
|
+
const runs = drain.capture.run_captures.map((run) => ({ ...run, resume_request_id: `scope-resume:${hash(JSON.stringify([input.scopeId, input.generation, input.requestId, run.runtime_home, run.project_id, run.run_id]))}` }));
|
|
532
|
+
const receipt = { scope_id: input.scopeId, generation: input.generation, request_id: input.requestId, source_request_id: control.request_id, capture_key: input.captureKey, status: "preparing", dispatch_blocked: true, manifest, process_bindings: bindings, runs, native: drain.capture.native_bindings, probes: drain.capture.probe_operations, requested_at: context.now() };
|
|
533
|
+
const payload = JSON.stringify(receipt), now = context.now();
|
|
534
|
+
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-resume-request',?,?,?,?,NULL,?,?,?)", [key, input.scopeId, key, now, payload, now, now]);
|
|
535
|
+
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-resume',?,?,?,?,NULL,?,?,?) ON CONFLICT(resource_kind,resource_key) DO UPDATE SET lease_token = excluded.lease_token, metadata_json = excluded.metadata_json, updated_at = excluded.updated_at", [input.scopeId, input.scopeId, key, now, payload, now, now]);
|
|
536
|
+
db.exec("COMMIT");
|
|
537
|
+
return { ...receipt, current: true, reused: false };
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
db.exec("ROLLBACK");
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
}
|