@deksden-com/dd-flow-cli 0.9.0-beta.124 → 0.9.0-beta.127
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 +16 -0
- package/README.md +10 -1
- package/dist/build-info.json +7 -7
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +4 -3
- package/dist/cli/help.js +1 -0
- package/dist/cli/hook-ingress.js +1 -1
- package/dist/cli/run-cli.js +126 -193
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +51 -0
- package/dist/cli/runtime-process.js +128 -0
- package/dist/cli.js +8 -5
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -2
- package/dist/harness-runtime/bin/dd-codex.mjs +3 -2
- package/dist/harness-runtime/bin/dd-droid.mjs +3 -2
- package/dist/harness-runtime/bin/dd-grok.mjs +16 -6
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -2
- package/dist/harness-runtime/bin/dd-zcode.mjs +20 -16
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +60 -13
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +151 -72
- package/dist/harness-runtime/lib/dd-agy.mjs +11 -23
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +9 -14
- package/dist/harness-runtime/lib/dd-codex.mjs +252 -80
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +14 -19
- package/dist/harness-runtime/lib/dd-droid.mjs +117 -31
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +56 -30
- package/dist/harness-runtime/lib/dd-grok.mjs +15 -13
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +18 -27
- package/dist/harness-runtime/lib/dd-opencode.mjs +44 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +56 -53
- package/dist/harness-runtime/lib/dd-zcode.mjs +200 -111
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +23 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +74 -20
- package/dist/harness-runtime/lib/managed-daemon.d.mts +10 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +228 -40
- package/dist/harness-runtime/lib/native-children.d.mts +13 -0
- package/dist/harness-runtime/lib/native-children.mjs +78 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +4 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +115 -11
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +73 -17
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +105 -8
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +112 -0
- package/dist/harness-runtime/lib/process-json.mjs +136 -38
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +5 -2
- package/dist/harness-runtime/lib/zcode-usage.mjs +3 -2
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/run-control-receipt.schema.json +19 -0
- package/dist/services/code-checks.js +79 -9
- package/dist/services/codex-capacity.js +117 -41
- package/dist/services/codex-hook-delivery.js +2 -2
- package/dist/services/compatibility-preflight.js +4 -1
- package/dist/services/controller-fanout.js +9 -76
- package/dist/services/engines.js +70 -20
- package/dist/services/eval-snapshots.js +179 -40
- package/dist/services/external-work-launch.js +13 -8
- package/dist/services/harness-adapter.js +175 -11
- package/dist/services/hooks.js +65 -21
- package/dist/services/lifecycle-invocations.js +404 -32
- package/dist/services/managed-lease-monitor.js +128 -0
- package/dist/services/managed-processes.js +103 -47
- package/dist/services/merge-server.js +58 -21
- package/dist/services/recovery-observation-budget.js +67 -7
- package/dist/services/recovery-snapshot-database.js +7 -4
- package/dist/services/run-control-receipt.js +1 -1
- package/dist/services/run-control-worker.js +177 -118
- package/dist/services/run-control.js +99 -8
- package/dist/services/run-controller-adapter.js +169 -46
- package/dist/services/run-controller-capture.js +14 -2
- package/dist/services/run-controller-process.js +11 -4
- package/dist/services/run-controller-state.js +68 -7
- package/dist/services/run-controller.js +166 -94
- package/dist/services/run-fork.js +8 -3
- package/dist/services/run-recovery-guard.js +75 -0
- package/dist/services/run-recovery-runtime.js +1 -56
- package/dist/services/run-recovery.js +17 -13
- package/dist/services/runs.js +39 -6
- package/dist/services/runtime-budget.js +32 -15
- package/dist/services/runtime-scope-control.js +8 -0
- package/dist/services/runtime-scope-resume.js +30 -8
- package/dist/services/runtime-scope-worker.js +118 -73
- package/dist/services/runtime-service.js +6 -7
- package/dist/services/scope-writer-cleanup.js +3 -2
- package/dist/services/stage-context.js +135 -0
- package/dist/services/stage-lifecycle.js +3 -15
- package/dist/services/stage-pause.js +3 -2
- package/dist/services/usage.js +14 -3
- package/dist/services/vnext-code-review.js +10 -9
- package/dist/services/vnext-code.js +10 -8
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +75 -2
- package/dist/services/vnext-fanout.js +5 -0
- package/dist/services/vnext-merge.js +10 -9
- package/dist/services/vnext-plan-review.js +21 -17
- package/dist/services/vnext-plan.js +9 -7
- package/dist/services/vnext-protocolize.js +15 -9
- package/dist/services/vnext-specify.js +6 -6
- package/dist/services/work-registry.js +34 -4
- package/dist/services/workspace-bootstrap.js +14 -3
- package/dist/shared/errors.js +11 -6
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +80 -21
- package/package.json +7 -4
|
@@ -8,10 +8,11 @@ import { beginSnapshotImport, completeSnapshotImport } from "../storage/import-g
|
|
|
8
8
|
import { foreignKeyTransaction } from "../storage/foreign-key-transaction.js";
|
|
9
9
|
import { createContext } from "../runtime/context.js";
|
|
10
10
|
import { AppError } from "../shared/errors.js";
|
|
11
|
-
import { resolveProjectRoot, runtimeProtocolDir, runtimeRunDir } from "../storage/paths.js";
|
|
11
|
+
import { canonicalPath, resolveProjectRoot, runtimeProtocolDir, runtimeRunDir } from "../storage/paths.js";
|
|
12
12
|
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
13
13
|
import { freezeRunExecutionRouting, getFlowRunStatus, importFlowRunExecutionProfile, startFlowRun } from "./runs.js";
|
|
14
14
|
import { loadExecutionRoutingInput, resolveExecutionPolicy } from "./execution-policy.js";
|
|
15
|
+
import { validateRunOperationalDecision } from "./vnext-execution-profile.js";
|
|
15
16
|
import { readVnextFlowDefinition } from "./vnext-specify.js";
|
|
16
17
|
import { captureRecoveryDatabase, readRecoveryResourceEvidence } from "./recovery-snapshot-database.js";
|
|
17
18
|
import { resourceHome } from "./managed-processes.js";
|
|
@@ -19,6 +20,7 @@ import { codexHomeSharedEntries } from "./hooks.js";
|
|
|
19
20
|
import { prepareOutputFile } from "../cli/input-preparation.js";
|
|
20
21
|
import { localEnvironmentAllowed, projectServicePathFilter } from "./workspace-files.js";
|
|
21
22
|
import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
|
|
23
|
+
import { isObservationOperation, nativeInventoryVersion, retainedDaemonOperations } from "./run-control.js";
|
|
22
24
|
/** Capture the only flow boundary which exists before `stage start --bootstrap` creates a RUN. */
|
|
23
25
|
export function prepareEvalBootstrapSnapshot(context, input) {
|
|
24
26
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -75,7 +77,7 @@ function bootstrapSourceVersion(projectRoot) {
|
|
|
75
77
|
treeHash(projectRoot, true),
|
|
76
78
|
treeModesHash(projectRoot, true),
|
|
77
79
|
git(projectRoot, ["symbolic-ref", "--quiet", "HEAD"]),
|
|
78
|
-
|
|
80
|
+
git(projectRoot, ["rev-parse", "HEAD"]),
|
|
79
81
|
indexEntries(projectRoot).toString("base64")
|
|
80
82
|
]);
|
|
81
83
|
}
|
|
@@ -108,7 +110,7 @@ export function createEvalRunSnapshot(context, input) {
|
|
|
108
110
|
try {
|
|
109
111
|
input.boundary?.verify();
|
|
110
112
|
sourceVersion = input.boundary ? snapshotSourceVersion(context, input) : null;
|
|
111
|
-
const result = captureEvalRunSnapshot(context, { ...input, output: temporary });
|
|
113
|
+
const result = captureEvalRunSnapshot(context, { ...input, ...(sourceVersion ? { journalCuts: sourceVersion.journals, observationCuts: sourceVersion.observations } : {}), output: temporary });
|
|
112
114
|
if (input.boundary && sourceVersion) {
|
|
113
115
|
Object.assign(result, { consistency: "controller_boundary_writer_barrier@1", boundary_capture: { controller_id: input.boundary.controllerId, operation_id: input.boundary.operationId, boundary_key: input.boundary.boundaryKey, source_sha256: sourceVersion.sha256, observation: input.boundary.observation } });
|
|
114
116
|
writeJson(path.join(temporary, "snapshot.json"), Object.fromEntries(Object.entries(result).filter(([key]) => key !== "ok" && key !== "snapshot")));
|
|
@@ -126,7 +128,7 @@ export function createEvalRunSnapshot(context, input) {
|
|
|
126
128
|
if (sourceVersion) {
|
|
127
129
|
let after;
|
|
128
130
|
try {
|
|
129
|
-
after = snapshotSourceVersion(context, input, sourceVersion.databases);
|
|
131
|
+
after = snapshotSourceVersion(context, input, sourceVersion.databases, sourceVersion.journals);
|
|
130
132
|
}
|
|
131
133
|
catch (error) {
|
|
132
134
|
if (error.code !== "ENOENT")
|
|
@@ -145,6 +147,13 @@ export function createEvalRunSnapshot(context, input) {
|
|
|
145
147
|
}
|
|
146
148
|
catch (error) {
|
|
147
149
|
fs.rmSync(temporary, { recursive: true, force: true });
|
|
150
|
+
// Native journal publication creates its directory before requested.json.
|
|
151
|
+
// Retry a gap during any capture phase, without classifying an incomplete
|
|
152
|
+
// request as read-only. Persistent missing inputs still exhaust the budget.
|
|
153
|
+
if (input.boundary && error.code === "ENOENT") {
|
|
154
|
+
const missing = error.path;
|
|
155
|
+
throw new AppError("snapshot_source_changed", "Snapshot source was not fully published during boundary capture", 1, { components: [missing && path.isAbsolute(missing) && isInside(context.ddFlowHome, missing) ? `runtime/${path.relative(context.ddFlowHome, missing)}` : "source_path_disappeared"] });
|
|
156
|
+
}
|
|
148
157
|
throw error;
|
|
149
158
|
}
|
|
150
159
|
finally {
|
|
@@ -204,17 +213,22 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
204
213
|
fs.mkdirSync(output, { recursive: true, mode: 0o700 });
|
|
205
214
|
const resourceDatabase = path.join(context.ddFlowHome, "runtime.sqlite");
|
|
206
215
|
const databaseFiles = [context.db.path, resourceDatabase].flatMap(file => [file, `${file}-wal`, `${file}-shm`]);
|
|
207
|
-
const codexExclusions = codexHomeSnapshotExclusions(context, project.id, input.runId);
|
|
216
|
+
const codexExclusions = [...codexHomeSnapshotExclusions(context, project.id, input.runId), ...input.journalCuts?.keys() ?? [], ...input.observationCuts?.keys() ?? []];
|
|
208
217
|
if (recovery) {
|
|
209
218
|
if (typeof status.run.run_root !== "string" || !isInside(physicalPath(context.ddFlowHome), physicalPath(status.run.run_root)) || samePath(physicalPath(context.ddFlowHome), physicalPath(status.run.run_root)))
|
|
210
219
|
throw new AppError("snapshot_run_scope_invalid", "Recovery requires a RUN artifact root within its runtime", 1);
|
|
211
220
|
for (const source of new Set([status.run.run_root, runtimeRunDir(context.ddFlowHome, project.id, input.runId)])) {
|
|
212
221
|
if (fs.existsSync(source))
|
|
213
|
-
copyRuntimeTree(source, path.join(output, "runtime", path.relative(context.ddFlowHome, source)), true, codexExclusions);
|
|
222
|
+
copyRuntimeTree(source, path.join(output, "runtime", path.relative(context.ddFlowHome, source)), true, codexExclusions, [], input.observationCuts);
|
|
214
223
|
}
|
|
215
224
|
}
|
|
216
225
|
else
|
|
217
|
-
copyRuntimeTree(context.ddFlowHome, path.join(output, "runtime"), false, [...databaseFiles, ...codexExclusions], grokProviderHomes(context.ddFlowHome));
|
|
226
|
+
copyRuntimeTree(context.ddFlowHome, path.join(output, "runtime"), false, [...databaseFiles, ...codexExclusions], grokProviderHomes(context.ddFlowHome), input.observationCuts);
|
|
227
|
+
for (const [file, cut] of input.journalCuts ?? []) {
|
|
228
|
+
const target = path.join(output, "runtime", path.relative(context.ddFlowHome, file));
|
|
229
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
230
|
+
fs.writeFileSync(target, cut.payload, { mode: cut.mode });
|
|
231
|
+
}
|
|
218
232
|
// VACUUM INTO is a consistent SQLite read snapshot, including committed WAL
|
|
219
233
|
// pages. Copying the live database and its WAL separately is not atomic.
|
|
220
234
|
const databaseCopy = path.join(output, "runtime", path.relative(context.ddFlowHome, context.db.path));
|
|
@@ -275,6 +289,8 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
275
289
|
project_sha256: treeHash(path.join(output, "project")),
|
|
276
290
|
project_payload_policy: "service-files@1",
|
|
277
291
|
...(!recovery ? { runtime_payload_policy: "flow-evidence@1" } : {}),
|
|
292
|
+
...(input.journalCuts ? { journal_cuts: [...input.journalCuts].map(([file, cut]) => ({ path: path.relative(context.ddFlowHome, file), bytes: cut.payload.length, sha256: cut.sha256 })) } : {}),
|
|
293
|
+
...(input.observationCuts ? { observation_cuts: [...input.observationCuts].map(([file, receipt]) => ({ path: path.relative(context.ddFlowHome, file), receipt })) } : {}),
|
|
278
294
|
project_git: projectGit,
|
|
279
295
|
workspace,
|
|
280
296
|
payload_modes: { project: treeModesHash(path.join(output, "project")), runtime: treeModesHash(path.join(output, "runtime")), ...(workspace.kind === "git_worktree" ? { workspace: treeModesHash(path.join(output, "workspace")) } : {}) },
|
|
@@ -325,6 +341,8 @@ export function prepareEvalRunRestore(context, input, preparedManifest) {
|
|
|
325
341
|
const recovery = manifest.purpose === "recovery";
|
|
326
342
|
if (recovery && input.executionRoutingFile !== undefined)
|
|
327
343
|
throw new AppError("snapshot_recovery_policy_frozen", "Recovery must retain its frozen execution policy", 2);
|
|
344
|
+
if (recovery && input.operationalDecisionFile !== undefined)
|
|
345
|
+
throw new AppError("snapshot_recovery_policy_frozen", "Recovery cannot replace its accepted operational decision", 2);
|
|
328
346
|
if (recovery && input.recoveryId !== manifest.recovery_id)
|
|
329
347
|
throw new AppError("snapshot_recovery_requires_explicit_restore", "Recovery snapshots require their explicit --recovery-id", 1, { snapshot });
|
|
330
348
|
if (!recovery && input.recoveryId)
|
|
@@ -340,7 +358,9 @@ export function prepareEvalRunRestore(context, input, preparedManifest) {
|
|
|
340
358
|
const sourceRuntime = path.join(snapshot, "runtime");
|
|
341
359
|
const sourceProject = path.join(snapshot, "project");
|
|
342
360
|
const routingInput = input.executionRoutingFile !== undefined ? loadExecutionRoutingInput(input.executionRoutingFile) : undefined;
|
|
343
|
-
|
|
361
|
+
const acceptedOperational = recovery ? undefined : prepareSnapshotOperationalDecision({ snapshot, runId: manifest.run_id, projectRoot, now: context.now(), ...(input.operationalDecisionFile ? { file: input.operationalDecisionFile } : {}) });
|
|
362
|
+
const operationalDecisionBytes = acceptedOperational?.bytes;
|
|
363
|
+
let selectedProfile = acceptedOperational?.profile;
|
|
344
364
|
if (routingInput) {
|
|
345
365
|
// Resolve selected profiles before replacing the caller's fresh runtime.
|
|
346
366
|
const sourceDb = new DatabaseSync(path.join(sourceRuntime, "db.sqlite"), { readOnly: true });
|
|
@@ -350,7 +370,7 @@ export function prepareEvalRunRestore(context, input, preparedManifest) {
|
|
|
350
370
|
if (!index?.execution_profile || !["vnext_specify", "vnext_protocolize"].includes(index.flow_kind))
|
|
351
371
|
throw new AppError("execution_profile_not_frozen", "Stage-entry routing requires a vNext execution snapshot", 1);
|
|
352
372
|
assertStageEntry(index.stage_runs, manifest.stage_entry);
|
|
353
|
-
selectedProfile = freezeRunExecutionRouting(context.ddFlowHome, index.execution_profile, routingInput);
|
|
373
|
+
selectedProfile = freezeRunExecutionRouting(context.ddFlowHome, selectedProfile ?? index.execution_profile, routingInput);
|
|
354
374
|
for (const row of sourceDb.prepare("SELECT payload_json FROM works WHERE run_id = ? AND status = 'created'").all(manifest.run_id)) {
|
|
355
375
|
const payload = row.payload_json ? JSON.parse(row.payload_json) : null;
|
|
356
376
|
if (payload?.execution_override !== undefined)
|
|
@@ -368,7 +388,34 @@ export function prepareEvalRunRestore(context, input, preparedManifest) {
|
|
|
368
388
|
sourceDb.close();
|
|
369
389
|
}
|
|
370
390
|
}
|
|
371
|
-
return { snapshot, manifest, recovery, projectRoot, sourceRuntime, sourceProject, routingInput, selectedProfile };
|
|
391
|
+
return { snapshot, manifest, recovery, projectRoot, sourceRuntime, sourceProject, routingInput, selectedProfile, operationalDecisionBytes };
|
|
392
|
+
}
|
|
393
|
+
/** Shared no-effect scope preparation for direct restore and run fork. */
|
|
394
|
+
export function prepareSnapshotOperationalDecision(input) {
|
|
395
|
+
const sourceDb = new DatabaseSync(path.join(input.snapshot, "runtime", "db.sqlite"), { readOnly: true });
|
|
396
|
+
try {
|
|
397
|
+
const row = sourceDb.prepare("SELECT index_json FROM runs WHERE id = ?").get(input.runId);
|
|
398
|
+
const profile = row ? JSON.parse(row.index_json).execution_profile : undefined;
|
|
399
|
+
if (profile?.operational_decision && !input.file)
|
|
400
|
+
throw new AppError("snapshot_operational_acceptance_required", "A new stage-entry execution needs its own accepted operational decision", 2);
|
|
401
|
+
if (!input.file)
|
|
402
|
+
return undefined;
|
|
403
|
+
if (!profile)
|
|
404
|
+
throw new AppError("execution_profile_not_frozen", "Operational stage-entry decision needs a frozen vNext profile", 2);
|
|
405
|
+
const bytes = fs.readFileSync(input.file);
|
|
406
|
+
const decision = validateRunOperationalDecision(JSON.parse(bytes.toString("utf8")), profile.settings);
|
|
407
|
+
if (canonicalPath(decision.scope.project_root, false) !== canonicalPath(input.projectRoot, false))
|
|
408
|
+
throw new AppError("operational_decision_scope_mismatch", "Imported decision must name the new project root", 2);
|
|
409
|
+
const sourceProject = path.join(input.snapshot, "project"), policyFile = path.resolve(sourceProject, decision.policy.path);
|
|
410
|
+
if (!isInside(sourceProject, policyFile) || !fs.existsSync(policyFile) || !isInside(fs.realpathSync(sourceProject), fs.realpathSync(policyFile)) || fileHash(policyFile) !== decision.policy.sha256)
|
|
411
|
+
throw new AppError("operational_decision_policy_changed", "Imported decision policy must match the owned frozen project source", 2);
|
|
412
|
+
if (decision.exception?.expires_at && Date.parse(decision.exception.expires_at) <= Date.parse(input.now))
|
|
413
|
+
throw new AppError("operational_decision_expired", "Imported operational decision has explicitly expired", 2);
|
|
414
|
+
return { profile: { ...profile, operational_decision: decision }, bytes };
|
|
415
|
+
}
|
|
416
|
+
finally {
|
|
417
|
+
sourceDb.close();
|
|
418
|
+
}
|
|
372
419
|
}
|
|
373
420
|
function assertRunRestoreTargets(context, input, snapshot, projectRoot, manifest) {
|
|
374
421
|
assertRestoreTargetSeparation(snapshot, [projectRoot, context.ddFlowHome]);
|
|
@@ -392,6 +439,8 @@ function assertRunRestoreTargets(context, input, snapshot, projectRoot, manifest
|
|
|
392
439
|
}
|
|
393
440
|
export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRunRestore(context, input), prearmedImportId) {
|
|
394
441
|
const { snapshot, manifest, recovery, projectRoot, sourceRuntime, sourceProject, routingInput, selectedProfile } = prepared;
|
|
442
|
+
if (input.operationalDecisionFile && (!prepared.operationalDecisionBytes || !fs.readFileSync(input.operationalDecisionFile).equals(prepared.operationalDecisionBytes)))
|
|
443
|
+
throw new AppError("operational_decision_changed", "Accepted operational decision changed after import preparation", 2);
|
|
395
444
|
assertRunRestoreTargets(context, input, snapshot, projectRoot, manifest);
|
|
396
445
|
const sourceManifestSha256 = fileHash(path.join(snapshot, "snapshot.json"));
|
|
397
446
|
const importId = prearmedImportId ?? beginSnapshotImport({ home: context.ddFlowHome, projectRoot,
|
|
@@ -414,18 +463,36 @@ export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRun
|
|
|
414
463
|
const publicationRun = restored.db.get("SELECT project_id, run_root FROM runs WHERE id = ?", [manifest.run_id]);
|
|
415
464
|
if (!originalRunRoot || !publicationRun?.run_root || !isInside(manifest.dd_flow_home, originalRunRoot) || !isInside(context.ddFlowHome, publicationRun.run_root))
|
|
416
465
|
throw new AppError("snapshot_invalid", "Restored publication roots are not bound to their runtime", 1);
|
|
417
|
-
|
|
466
|
+
const pathMapping = [{ from: manifest.dd_flow_home, to: context.ddFlowHome }, { from: sourceWorkspaceRoot(manifest), to: workspaceRoot }, { from: manifest.project_root, to: projectRoot }];
|
|
467
|
+
restored.db.run("INSERT INTO audit_events (project_id, event_type, forced, payload_json, created_at) VALUES (?, 'flow_run.publication_relocation', 0, ?, ?)", [publicationRun.project_id, JSON.stringify({ run_id: manifest.run_id, from: originalRunRoot, to: publicationRun.run_root, source_manifest_sha256: sourceManifestSha256, import_id: importId, path_mapping: pathMapping }), restored.now()]);
|
|
418
468
|
registerProject(restored, { root: projectRoot });
|
|
419
469
|
requireProjectByRoot(restored, projectRoot);
|
|
420
|
-
if (selectedProfile &&
|
|
470
|
+
if (selectedProfile && runtimeLineage) {
|
|
421
471
|
const run = restored.db.get("SELECT run_root FROM runs WHERE id = ?", [manifest.run_id]);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (
|
|
425
|
-
|
|
472
|
+
if (routingInput) {
|
|
473
|
+
const artifact = path.join(run.run_root, selectedProfile.routing_input.artifact);
|
|
474
|
+
if (fs.existsSync(artifact)) {
|
|
475
|
+
if (!fs.readFileSync(artifact).equals(routingInput.bytes))
|
|
476
|
+
throw new AppError("snapshot_checksum_mismatch", "Existing routing input does not match its checksum", 1);
|
|
477
|
+
}
|
|
478
|
+
else
|
|
479
|
+
fs.writeFileSync(artifact, routingInput.bytes, { flag: "wx", mode: 0o600 });
|
|
480
|
+
}
|
|
481
|
+
if (input.operationalDecisionFile) {
|
|
482
|
+
if (!prepared.operationalDecisionBytes || !fs.readFileSync(input.operationalDecisionFile).equals(prepared.operationalDecisionBytes))
|
|
483
|
+
throw new AppError("operational_decision_changed", "Accepted operational decision changed before import publication", 2);
|
|
484
|
+
const binding = { project_id: publicationRun.project_id, run_id: manifest.run_id, ...selectedProfile.operational_decision.scope };
|
|
485
|
+
const policyBytes = fs.readFileSync(path.resolve(projectRoot, selectedProfile.operational_decision.policy.path));
|
|
486
|
+
const policySource = { path: `operational-policy.${selectedProfile.operational_decision.policy.sha256}.source.md`, sha256: crypto.createHash("sha256").update(policyBytes).digest("hex") };
|
|
487
|
+
if (policySource.sha256 !== selectedProfile.operational_decision.policy.sha256)
|
|
488
|
+
throw new AppError("operational_decision_policy_changed", "Imported policy changed before binding", 2);
|
|
489
|
+
if (!fs.existsSync(path.join(run.run_root, policySource.path)))
|
|
490
|
+
fs.writeFileSync(path.join(run.run_root, policySource.path), policyBytes, { flag: "wx" });
|
|
491
|
+
const bytes = `${JSON.stringify({ schema_id: "dd-flow/run-operational-contract@1", binding, settings: { merge_delivery: selectedProfile.settings.merge_delivery, merge_cleanup: selectedProfile.settings.merge_cleanup }, decision: selectedProfile.operational_decision, policy_source: policySource }, null, 2)}\n`;
|
|
492
|
+
const artifact = `operational-contract.${crypto.createHash("sha256").update(bytes).digest("hex")}.json`;
|
|
493
|
+
fs.writeFileSync(path.join(run.run_root, artifact), bytes, { flag: "wx" });
|
|
494
|
+
selectedProfile.operational_contract = { path: artifact, sha256: crypto.createHash("sha256").update(bytes).digest("hex"), policy_source: policySource, binding };
|
|
426
495
|
}
|
|
427
|
-
else
|
|
428
|
-
fs.writeFileSync(artifact, routingInput.bytes, { flag: "wx", mode: 0o600 });
|
|
429
496
|
importFlowRunExecutionProfile(restored, { projectRoot, runId: manifest.run_id, stage: manifest.stage_entry, lineageFile: runtimeLineage, profile: selectedProfile });
|
|
430
497
|
}
|
|
431
498
|
const relocation = recovery ? {
|
|
@@ -637,13 +704,29 @@ function rewriteOperationalRouteReceipts(root, oldRoot, newRoot, oldWorkspace, n
|
|
|
637
704
|
};
|
|
638
705
|
visit(root);
|
|
639
706
|
}
|
|
640
|
-
function rewriteDatabaseColumn(context, table, column, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome) {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
707
|
+
function rewriteDatabaseColumn(context, table, column, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome) {
|
|
708
|
+
for (const row of context.db.all(`SELECT rowid AS rid, ${column} AS value FROM ${table} WHERE ${column} IS NOT NULL`)) {
|
|
709
|
+
if (!row.value)
|
|
710
|
+
continue;
|
|
711
|
+
let value;
|
|
712
|
+
if (column.endsWith("_json") || column === "index_json") {
|
|
713
|
+
const original = JSON.parse(row.value);
|
|
714
|
+
const mapped = rebaseStructuredValue(original, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome);
|
|
715
|
+
if (table === "runs" && column === "index_json" && original.execution_profile) {
|
|
716
|
+
// Accepted source/binding are logical provenance, not mutable physical locators.
|
|
717
|
+
if (original.execution_profile.operational_decision)
|
|
718
|
+
mapped.execution_profile.operational_decision = original.execution_profile.operational_decision;
|
|
719
|
+
if (original.execution_profile.operational_contract)
|
|
720
|
+
mapped.execution_profile.operational_contract = original.execution_profile.operational_contract;
|
|
721
|
+
}
|
|
722
|
+
value = JSON.stringify(mapped);
|
|
723
|
+
}
|
|
724
|
+
else
|
|
725
|
+
value = rebasePathString(row.value, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome);
|
|
726
|
+
if (value !== row.value)
|
|
727
|
+
context.db.run(`UPDATE ${table} SET ${column} = ? WHERE rowid = ?`, [value, row.rid]);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
647
730
|
function rebaseStructuredValue(value, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome) { if (typeof value === "string")
|
|
648
731
|
return rebasePathString(value, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome); if (Array.isArray(value))
|
|
649
732
|
return value.map((item) => rebaseStructuredValue(item, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome)); if (value && typeof value === "object")
|
|
@@ -720,7 +803,7 @@ function prepareSnapshotProjectGit(projectRoot) {
|
|
|
720
803
|
function snapshotProjectGit(projectRoot, output) {
|
|
721
804
|
const { branch, head } = prepareSnapshotProjectGit(projectRoot);
|
|
722
805
|
const bundle = path.join(output, "project.bundle");
|
|
723
|
-
runGit(projectRoot, ["bundle", "create", bundle,
|
|
806
|
+
runGit(projectRoot, ["bundle", "create", bundle, `refs/heads/${branch}`]);
|
|
724
807
|
return { branch, head, bundle_sha256: fileHash(bundle), ...snapshotIndex(projectRoot, output, "project") };
|
|
725
808
|
}
|
|
726
809
|
function restoreProjectGit(snapshot, source, projectRoot) {
|
|
@@ -910,6 +993,17 @@ function grokProviderHomes(runtimeRoot) {
|
|
|
910
993
|
visit(runtimeRoot);
|
|
911
994
|
return homes;
|
|
912
995
|
}
|
|
996
|
+
function nativeDaemonFile(file) {
|
|
997
|
+
if (!fs.lstatSync(file, { throwIfNoEntry: false })?.isFile())
|
|
998
|
+
return false;
|
|
999
|
+
try {
|
|
1000
|
+
const state = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1001
|
+
return typeof state.daemon_id === "string" && /^dd-(codex|zcode|grok|agy|opencode|droid)\/daemon-state@1$/.test(String(state.schema_id));
|
|
1002
|
+
}
|
|
1003
|
+
catch {
|
|
1004
|
+
return false;
|
|
1005
|
+
} // Unrecognized files retain full-byte protection.
|
|
1006
|
+
}
|
|
913
1007
|
function sqliteFile(file) {
|
|
914
1008
|
if (!fs.existsSync(file) || !fs.statSync(file).isFile())
|
|
915
1009
|
return false;
|
|
@@ -943,7 +1037,7 @@ function sqliteValue(value) {
|
|
|
943
1037
|
* This intentionally applies to the small set of managed SQLite files, not
|
|
944
1038
|
* to arbitrary provider databases.
|
|
945
1039
|
*/
|
|
946
|
-
function sqliteSemanticFingerprint(db
|
|
1040
|
+
function sqliteSemanticFingerprint(db) {
|
|
947
1041
|
db.exec("PRAGMA busy_timeout = 4000");
|
|
948
1042
|
db.exec("BEGIN");
|
|
949
1043
|
try {
|
|
@@ -953,9 +1047,8 @@ function sqliteSemanticFingerprint(db, resourceRegistry = false) {
|
|
|
953
1047
|
.sort((left, right) => Number(left.cid ?? 0) - Number(right.cid ?? 0))
|
|
954
1048
|
.map(column => ({ name: String(column.name ?? ""), type: String(column.type ?? ""), notnull: Number(column.notnull ?? 0), default: sqliteValue(column.dflt_value ?? null), pk: Number(column.pk ?? 0) }));
|
|
955
1049
|
const names = columns.map(column => column.name);
|
|
956
|
-
const stableNames = resourceRegistry && row.name === "managed_processes" ? names.filter(name => name !== "lease_expires_at" && name !== "updated_at") : names;
|
|
957
1050
|
const rows = db.prepare(`SELECT * FROM ${sqliteIdentifier(row.name)}`).all()
|
|
958
|
-
.map(record =>
|
|
1051
|
+
.map(record => names.map(name => [name, sqliteValue(record[name])]))
|
|
959
1052
|
.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
960
1053
|
return { name: row.name, columns, rows };
|
|
961
1054
|
});
|
|
@@ -983,8 +1076,9 @@ function runtimeSnapshotKind(file, excluded, providerHomes = []) {
|
|
|
983
1076
|
/** Productive writers are fenced separately. Compare exactly the copied
|
|
984
1077
|
* source set; SQLite serialization includes committed WAL pages without
|
|
985
1078
|
* mistaking a physical checkpoint for new logical content. */
|
|
986
|
-
function snapshotSourceVersion(context, input, retainedDatabases) {
|
|
1079
|
+
function snapshotSourceVersion(context, input, retainedDatabases, retainedJournals) {
|
|
987
1080
|
const databases = retainedDatabases ?? new Map();
|
|
1081
|
+
const journals = retainedJournals ?? new Map();
|
|
988
1082
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
989
1083
|
const project = requireProjectByRoot(context, projectRoot);
|
|
990
1084
|
const run = context.db.get("SELECT workspace_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
|
|
@@ -993,34 +1087,62 @@ function snapshotSourceVersion(context, input, retainedDatabases) {
|
|
|
993
1087
|
const entries = new Map();
|
|
994
1088
|
const excluded = codexHomeSnapshotExclusions(context, project.id, input.runId);
|
|
995
1089
|
const providerHomes = input.recoveryId === undefined ? grokProviderHomes(context.ddFlowHome) : [];
|
|
1090
|
+
const observations = new Map();
|
|
1091
|
+
const operationRoots = new Set();
|
|
996
1092
|
const visit = (directory) => {
|
|
1093
|
+
const daemon = path.join(directory, "daemon.json");
|
|
1094
|
+
const native = nativeDaemonFile(daemon);
|
|
1095
|
+
if (native) {
|
|
1096
|
+
operationRoots.add(path.join(directory, "operations"));
|
|
1097
|
+
for (const operation of retainedDaemonOperations(directory))
|
|
1098
|
+
if (isObservationOperation(operation.operation))
|
|
1099
|
+
observations.set(path.join(directory, "operations", crypto.createHash("sha256").update(String(operation.operation_id)).digest("hex")), operation);
|
|
1100
|
+
}
|
|
997
1101
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
998
1102
|
const file = path.join(directory, entry.name);
|
|
1103
|
+
// These diagnostic cuts are retained, but their creation cannot
|
|
1104
|
+
// invalidate a productive boundary. Unknown operations remain protected.
|
|
1105
|
+
if (observations.has(file))
|
|
1106
|
+
continue;
|
|
999
1107
|
const kind = runtimeSnapshotKind(file, excluded, providerHomes);
|
|
1000
1108
|
if (kind === "excluded" || kind === "sidecar")
|
|
1001
1109
|
continue;
|
|
1002
1110
|
const key = `runtime/${path.relative(context.ddFlowHome, file)}`;
|
|
1003
1111
|
const stat = fs.lstatSync(file);
|
|
1004
1112
|
if (stat.isDirectory()) {
|
|
1005
|
-
|
|
1113
|
+
if (!operationRoots.has(file))
|
|
1114
|
+
entries.set(key, "directory");
|
|
1006
1115
|
visit(file);
|
|
1007
1116
|
}
|
|
1008
1117
|
else if (stat.isSymbolicLink())
|
|
1009
1118
|
entries.set(key, `link:${fs.readlinkSync(file)}`);
|
|
1119
|
+
else if (native && file === daemon)
|
|
1120
|
+
entries.set(key, `daemon:${nativeInventoryVersion(directory)}:${stat.mode & 0o111}`);
|
|
1010
1121
|
else if (kind === "sqlite") {
|
|
1122
|
+
// Their productive identities are checked by boundary.verify(). A
|
|
1123
|
+
// consistent backup is evidence, not a demand for global DB silence.
|
|
1124
|
+
if (file === context.db.path || file === path.join(resourceHome(context), "runtime.sqlite")) {
|
|
1125
|
+
entries.set(key, `managed-sqlite:${stat.mode & 0o111}`);
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1011
1128
|
let db = databases.get(file);
|
|
1012
1129
|
if (!db) {
|
|
1013
1130
|
db = new DatabaseSync(file, { readOnly: true });
|
|
1014
1131
|
databases.set(file, db);
|
|
1015
1132
|
}
|
|
1016
|
-
// Resource heartbeats are copied consistently but are not productive
|
|
1017
|
-
// RUN writers; owner identity is fenced by boundary.verify().
|
|
1018
|
-
if (file === path.join(resourceHome(context), "runtime.sqlite")) {
|
|
1019
|
-
entries.set(key, `resource-sqlite:${sqliteSemanticFingerprint(db, true)}:${stat.mode & 0o111}`);
|
|
1020
|
-
continue;
|
|
1021
|
-
}
|
|
1022
1133
|
entries.set(key, `sqlite:${sqliteSemanticFingerprint(db)}:${stat.mode & 0o111}`);
|
|
1023
1134
|
}
|
|
1135
|
+
else if (stat.isFile() && ["adapter.events.jsonl", "events.jsonl", "model-observations.jsonl", "timeline.jsonl", "daemon.log", "controller.log"].includes(path.basename(file))) {
|
|
1136
|
+
const bytes = fs.readFileSync(file);
|
|
1137
|
+
let cut = journals.get(file);
|
|
1138
|
+
if (!cut && !retainedJournals) {
|
|
1139
|
+
const payload = bytes.subarray(0, bytes.lastIndexOf(10) + 1);
|
|
1140
|
+
cut = { payload, dev: stat.dev, ino: stat.ino, mode: stat.mode, sha256: crypto.createHash("sha256").update(payload).digest("hex") };
|
|
1141
|
+
journals.set(file, cut);
|
|
1142
|
+
}
|
|
1143
|
+
const valid = cut && cut.dev === stat.dev && cut.ino === stat.ino && bytes.length >= cut.payload.length && bytes.subarray(0, cut.payload.length).equals(cut.payload);
|
|
1144
|
+
entries.set(key, valid && cut ? `journal:${cut.sha256}:${cut.payload.length}:${stat.mode & 0o111}` : "journal:changed");
|
|
1145
|
+
}
|
|
1024
1146
|
else if (stat.isFile())
|
|
1025
1147
|
entries.set(key, `file:${crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex")}:${stat.mode & 0o111}`);
|
|
1026
1148
|
}
|
|
@@ -1029,9 +1151,9 @@ function snapshotSourceVersion(context, input, retainedDatabases) {
|
|
|
1029
1151
|
visit(context.ddFlowHome);
|
|
1030
1152
|
for (const root of new Set([projectRoot, run.workspace_root]))
|
|
1031
1153
|
entries.set(`source/${root}`, JSON.stringify([
|
|
1032
|
-
treeHash(root, true), treeModesHash(root, true), git(root, ["symbolic-ref", "--quiet", "HEAD"]),
|
|
1154
|
+
treeHash(root, true), treeModesHash(root, true), git(root, ["symbolic-ref", "--quiet", "HEAD"]), git(root, ["rev-parse", "HEAD"]), indexEntries(root).toString("base64")
|
|
1033
1155
|
]));
|
|
1034
|
-
return { entries, databases, sha256: crypto.createHash("sha256").update(JSON.stringify([...entries].sort(([a], [b]) => a.localeCompare(b)))).digest("hex") };
|
|
1156
|
+
return { entries, databases, journals, observations, sha256: crypto.createHash("sha256").update(JSON.stringify([...entries].sort(([a], [b]) => a.localeCompare(b)))).digest("hex") };
|
|
1035
1157
|
}
|
|
1036
1158
|
catch (error) {
|
|
1037
1159
|
if (!retainedDatabases)
|
|
@@ -1083,7 +1205,7 @@ function codexHomeSnapshotExclusions(context, projectId, runId) {
|
|
|
1083
1205
|
return excluded;
|
|
1084
1206
|
}
|
|
1085
1207
|
/** Copy a dedicated home or scoped recovery tree, preserving SQLite WAL commits. */
|
|
1086
|
-
function copyRuntimeTree(source, destination, scoped = true, excluded = [], providerHomes = []) {
|
|
1208
|
+
function copyRuntimeTree(source, destination, scoped = true, excluded = [], providerHomes = [], observationCuts) {
|
|
1087
1209
|
if (fs.lstatSync(source).isSymbolicLink())
|
|
1088
1210
|
throw new AppError("snapshot_runtime_symlink_outside_scope", "Recovery artifact roots must not be symbolic links", 1, { path: source });
|
|
1089
1211
|
const root = physicalPath(source);
|
|
@@ -1094,6 +1216,23 @@ function copyRuntimeTree(source, destination, scoped = true, excluded = [], prov
|
|
|
1094
1216
|
fs.cpSync(source, destination, {
|
|
1095
1217
|
recursive: true, verbatimSymlinks: true,
|
|
1096
1218
|
filter: file => {
|
|
1219
|
+
if (observationCuts && path.basename(path.dirname(file)) === "operations" && fs.lstatSync(file).isDirectory() && nativeDaemonFile(path.join(path.dirname(path.dirname(file)), "daemon.json"))) {
|
|
1220
|
+
let operations;
|
|
1221
|
+
try {
|
|
1222
|
+
operations = retainedDaemonOperations(path.dirname(path.dirname(file)));
|
|
1223
|
+
}
|
|
1224
|
+
catch (error) {
|
|
1225
|
+
if (error.code !== "ENOENT")
|
|
1226
|
+
throw error;
|
|
1227
|
+
throw new AppError("snapshot_source_changed", "Native operation was not fully published during capture", 1, { components: ["native_operation_publication"] });
|
|
1228
|
+
}
|
|
1229
|
+
const operation = operations.find(row => crypto.createHash("sha256").update(String(row.operation_id)).digest("hex") === path.basename(file));
|
|
1230
|
+
if (operation && isObservationOperation(operation.operation)) {
|
|
1231
|
+
if (!observationCuts.has(file))
|
|
1232
|
+
observationCuts.set(file, operation);
|
|
1233
|
+
return false;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1097
1236
|
const kind = runtimeSnapshotKind(file, excluded, providerHomes);
|
|
1098
1237
|
if (kind === "excluded" || kind === "sidecar")
|
|
1099
1238
|
return false;
|
|
@@ -93,10 +93,16 @@ export async function launchExternalWork(context, input) {
|
|
|
93
93
|
const common = ["--dd-flow-bin", flowExecutable, "--state-dir", root, "--journal", journal, "--cwd", isolatedWorkspace ?? run.workspace_root, "--provider", resolved.worker.provider, "--model", resolved.worker.model, "--reasoning", resolved.worker.reasoning, "--mode", resolved.worker.mode, "--permission", resolved.worker.permission, "--project-root", run.project_root, "--dd-flow-home", context.ddFlowHome, ...harnessRuntimeArguments(context.ddFlowHome, runtimeHarness(resolved.worker)), "--json"];
|
|
94
94
|
const evidence = (value) => fs.appendFileSync(journal, `${JSON.stringify({ type: "adapter_call", at: context.now(), ...value })}\n`);
|
|
95
95
|
try {
|
|
96
|
-
const call = (args, timeoutMs, id = `${operationId}:${args[0]}.${args[1]}`) => runHarnessAdapter({ executable, args, env: runtimeContext.env, operationId: id, runtimeOwner, ...(timeoutMs ? { timeoutMs } : {}), ...(input.signal ? { signal: input.signal } : {}), ...(input.progress ? { progress: input.progress } : {}), onEvidence: evidence
|
|
96
|
+
const call = (args, timeoutMs, id = `${operationId}:${args[0]}.${args[1]}`) => runHarnessAdapter({ executable, args, harness: runtimeHarness(resolved.worker), env: runtimeContext.env, operationId: id, runtimeOwner, ...(timeoutMs !== undefined ? { timeoutMs } : {}), ...(input.signal ? { signal: input.signal } : {}), ...(input.progress ? { progress: input.progress } : {}), onEvidence: evidence, onNativeOutcome: value => {
|
|
97
|
+
if (args[0] !== "session" || args[1] !== "create")
|
|
98
|
+
return;
|
|
99
|
+
const nativeId = adapterSessionId(value);
|
|
100
|
+
if (nativeId)
|
|
101
|
+
context.db.run("UPDATE work_launches SET provider_session_id = ?, adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [nativeId, JSON.stringify({ create: value, settlement_pending: true }), context.now(), operationId]);
|
|
102
|
+
} });
|
|
97
103
|
await call(["daemon", "start", ...common]);
|
|
98
104
|
assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
|
|
99
|
-
const created = await call(["session", "create", ...common]
|
|
105
|
+
const created = await call(["session", "create", ...common]);
|
|
100
106
|
const sessionId = adapterSessionId(created);
|
|
101
107
|
if (!sessionId)
|
|
102
108
|
throw new AppError("harness_adapter_invalid", "Harness adapter did not return a typed Session id", 1, { work_id: work.work_id, harness: runtimeHarness(resolved.worker) });
|
|
@@ -111,11 +117,10 @@ export async function launchExternalWork(context, input) {
|
|
|
111
117
|
const promptFile = path.join(root, "launch.md");
|
|
112
118
|
fs.writeFileSync(promptFile, `Start the assigned Work now. Your first tool call must be this exact standalone command:\n\n${workStartCommand(runtimeContext, work)}\n\nUse the packet returned by that command. Do not claim another Work or run this command more than once.\n\n${lifecycleRetryInstruction}${isolatedWorkspace ? `\n\nThe review copy has no .git. Read the captured status/diff or gitless note in ${path.join(isolatedWorkspace, ".dd-flow", "review-evidence")}; do not run Git inside the copy.\n` : "\n"}`);
|
|
113
119
|
const promptOperationId = `${operationId}:session.prompt`;
|
|
114
|
-
|
|
115
|
-
context.db.run("UPDATE work_launches SET adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'launching'", [JSON.stringify({ create: created, prompt_deadline: promptDeadline }), context.now(), operationId]);
|
|
120
|
+
context.db.run("UPDATE work_launches SET adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'launching'", [JSON.stringify({ create: created, progress_policy: "native-inactivity@1" }), context.now(), operationId]);
|
|
116
121
|
let prompted;
|
|
117
122
|
try {
|
|
118
|
-
prompted = await call(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common],
|
|
123
|
+
prompted = await call(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], null);
|
|
119
124
|
}
|
|
120
125
|
catch (error) {
|
|
121
126
|
if (runtimeHarness(resolved.worker) !== "codex-desktop")
|
|
@@ -124,12 +129,12 @@ export async function launchExternalWork(context, input) {
|
|
|
124
129
|
const capacityIntents = retained?.adapter_receipt_json ? JSON.parse(retained.adapter_receipt_json).capacity_intents ?? [] : [];
|
|
125
130
|
const capacityFailures = retained?.adapter_receipt_json ? JSON.parse(retained.adapter_receipt_json).capacity_failures ?? [] : [];
|
|
126
131
|
const retainCapacity = () => {
|
|
127
|
-
const changed = context.db.run("UPDATE work_launches SET adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'launching'", [JSON.stringify({ create: created,
|
|
132
|
+
const changed = context.db.run("UPDATE work_launches SET adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'launching'", [JSON.stringify({ create: created, progress_policy: "native-inactivity@1", capacity_intents: capacityIntents, capacity_failures: capacityFailures }), context.now(), operationId]);
|
|
128
133
|
if (changed.changes !== 1)
|
|
129
134
|
throw new AppError("capacity_continuation_blocked", "External Work launch owner changed", 1);
|
|
130
135
|
};
|
|
131
136
|
const resumed = await continueCodexCapacity({ error, sessionId, operationId: promptOperationId, allowSettledDescendants: true,
|
|
132
|
-
...(input.signal ? { signal: input.signal } : {}),
|
|
137
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
133
138
|
continuationText: `Continue only Work ${work.work_id} in RUN ${work.run_id}, Stage ${stage ?? "current"}, in the same Session. Inspect its current durable state and the retained Work packet. Never repeat Work start or a completed tool, accepted answer, check or lifecycle command. Use only the current exact issued successor and finish at the original Work boundary.`,
|
|
134
139
|
retainedIntents: () => capacityIntents,
|
|
135
140
|
retainedFailures: () => capacityFailures,
|
|
@@ -160,7 +165,7 @@ export async function launchExternalWork(context, input) {
|
|
|
160
165
|
}
|
|
161
166
|
else
|
|
162
167
|
fs.writeFileSync(file, text, { flag: "wx", mode: 0o600 });
|
|
163
|
-
return call(["session", "prompt", "--session-id", sessionId, "--prompt-file", file, ...common],
|
|
168
|
+
return call(["session", "prompt", "--session-id", sessionId, "--prompt-file", file, ...common], null, id);
|
|
164
169
|
} });
|
|
165
170
|
if (!resumed)
|
|
166
171
|
throw error;
|