@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.149
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 +130 -0
- package/README.md +13 -8
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +13 -3
- package/dist/cli/command-metadata.js +51 -16
- package/dist/cli/diagnostic-invocation.js +46 -0
- package/dist/cli/diagnostic.js +120 -0
- package/dist/cli/help.js +67 -1
- package/dist/cli/hook-ingress.js +27 -3
- package/dist/cli/memory-flow-admission.js +9 -3
- package/dist/cli/memory-init-invocation.js +10 -3
- package/dist/cli/memory-init.js +77 -68
- package/dist/cli/memory-storage-admission.js +3 -2
- package/dist/cli/memory-storage-qualification.js +105 -0
- package/dist/cli/memory-storage-transition.js +123 -0
- package/dist/cli/memory-upgrade.js +189 -0
- package/dist/cli/native-hook-ingress.js +14 -4
- package/dist/cli/native-hook-preparation.js +55 -0
- package/dist/cli/response-file.js +2 -1
- package/dist/cli/run-cli.js +79 -40
- package/dist/cli/setup.js +2 -1
- package/dist/cli.js +18 -0
- package/dist/domain/diagnostic-aspects.js +103 -0
- package/dist/domain/diagnostic-flows.js +8 -0
- package/dist/domain/stage-handler-identities.js +8 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
- package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
- package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
- package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
- package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
- package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
- package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
- package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
- package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
- package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
- package/dist/harness-runtime/lib/entity-references.mjs +9 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
- package/dist/harness-runtime/lib/native-children.d.mts +8 -1
- package/dist/harness-runtime/lib/native-children.mjs +114 -8
- package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
- package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
- package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
- package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
- package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
- package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +5 -3
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
- package/dist/harness-runtime/lib/runner-events.mjs +3 -1
- package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
- package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
- package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
- package/dist/schemas/diagnostic-work-result.schema.json +296 -0
- package/dist/schemas/execution-bundle.schema.json +7 -1
- package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
- package/dist/schemas/memory-repair-work-result.schema.json +9 -2
- package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
- package/dist/schemas/release-impact.schema.json +3 -3
- package/dist/schemas/stage-start-response.schema.json +4 -0
- package/dist/services/accepted-intent.js +13 -1
- package/dist/services/canon.js +43 -7
- package/dist/services/check-environment.js +21 -0
- package/dist/services/check-runner-evidence.js +267 -15
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/cli-upgrade-allowlist.js +1 -1
- package/dist/services/code-checks.js +34 -19
- package/dist/services/compatibility-preflight.js +5 -2
- package/dist/services/controller-execution.js +10 -4
- package/dist/services/controller-fanout.js +34 -10
- package/dist/services/controller-memory-task.js +45 -0
- package/dist/services/controller-memory-worker.js +49 -0
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-settlement.js +5 -3
- package/dist/services/descriptor-run-start.js +46 -20
- package/dist/services/descriptor-stage-coordinator.js +66 -13
- package/dist/services/descriptor-stage-settlement.js +2 -1
- package/dist/services/descriptor-stage-start.js +40 -4
- package/dist/services/diagnostic-evidence.js +73 -0
- package/dist/services/diagnostic-report-publication.js +72 -0
- package/dist/services/diagnostic-report.js +67 -0
- package/dist/services/diagnostic-run-bundle.js +74 -0
- package/dist/services/diagnostic-snapshot.js +159 -0
- package/dist/services/diagnostic-stage-handlers.js +108 -0
- package/dist/services/diagnostic-stage-results.js +132 -0
- package/dist/services/diagnostic-start.js +84 -0
- package/dist/services/diagnostic-work-batch.js +34 -0
- package/dist/services/diagnostic-work-collection.js +97 -0
- package/dist/services/diagnostic-work-registration.js +101 -0
- package/dist/services/diagnostic-work-results.js +93 -0
- package/dist/services/engines.js +64 -14
- package/dist/services/eval-snapshots.js +186 -119
- package/dist/services/execution-bundles.js +19 -7
- package/dist/services/external-work-launch.js +5 -3
- package/dist/services/flow-catalog.js +13 -4
- package/dist/services/frozen-engine.js +148 -11
- package/dist/services/hook-memory-routing.js +188 -0
- package/dist/services/hook-recovery-guard.js +14 -0
- package/dist/services/hook-session-observation.js +24 -0
- package/dist/services/hooks.js +27 -8
- package/dist/services/hybrid-contracts.js +4 -13
- package/dist/services/hybrid-digest.js +14 -0
- package/dist/services/installed-flow-source.js +47 -4
- package/dist/services/lifecycle-invocations.js +179 -38
- package/dist/services/memory-accepted-composition.js +5 -4
- package/dist/services/memory-completed-correction-wave.js +23 -15
- package/dist/services/memory-compose-handler.js +163 -21
- package/dist/services/memory-composition-acceptance.js +186 -10
- package/dist/services/memory-composition-graph.js +42 -11
- package/dist/services/memory-composition-registration.js +23 -8
- package/dist/services/memory-composition-work-custody.js +14 -9
- package/dist/services/memory-composition-work-results.js +15 -3
- package/dist/services/memory-corrected-review-batch.js +10 -3
- package/dist/services/memory-corrected-review-findings.js +4 -2
- package/dist/services/memory-corrected-review-inputs.js +10 -5
- package/dist/services/memory-corrected-review-receipt.js +17 -9
- package/dist/services/memory-corrected-review-registration.js +3 -2
- package/dist/services/memory-correction-check-receipt.js +20 -4
- package/dist/services/memory-custody-snapshot.js +36 -0
- package/dist/services/memory-finish-evidence.js +44 -22
- package/dist/services/memory-finish-handler.js +37 -23
- package/dist/services/memory-finish-integration.js +218 -48
- package/dist/services/memory-finish-readiness.js +66 -19
- package/dist/services/memory-git-integration.js +87 -18
- package/dist/services/memory-historical-corrected-review.js +21 -15
- package/dist/services/memory-historical-original-review.js +22 -9
- package/dist/services/memory-historical-repair-registration.js +14 -8
- package/dist/services/memory-historical-second-review.js +30 -11
- package/dist/services/memory-init-contexts.js +44 -2
- package/dist/services/memory-init-controller-start.js +27 -8
- package/dist/services/memory-init-git-policy.js +50 -21
- package/dist/services/memory-init-lint-capability.js +24 -6
- package/dist/services/memory-init-native-capacity.js +17 -4
- package/dist/services/memory-init-preflight.js +21 -17
- package/dist/services/memory-init-request-candidate.js +73 -17
- package/dist/services/memory-init-request-input.js +23 -7
- package/dist/services/memory-init-run-bundle.js +49 -12
- package/dist/services/memory-init-storage.js +17 -7
- package/dist/services/memory-init-worktrunk-capability.js +3 -7
- package/dist/services/memory-layer.js +71 -6
- package/dist/services/memory-lint-config.js +111 -2
- package/dist/services/memory-native-review-copy.js +5 -67
- package/dist/services/memory-normal-flow-entry.js +12 -0
- package/dist/services/memory-ordinary-defaults.js +22 -5
- package/dist/services/memory-preflight-handler.js +15 -0
- package/dist/services/memory-repair-current-view.js +1 -1
- package/dist/services/memory-repair-document-delta.js +70 -33
- package/dist/services/memory-repair-history.js +12 -7
- package/dist/services/memory-repair-physical-binding.js +1 -119
- package/dist/services/memory-repair-publication-plan.js +5 -3
- package/dist/services/memory-repair-publication-source.js +1 -1
- package/dist/services/memory-repair-publication.js +3 -2
- package/dist/services/memory-repair-registration.js +2 -1
- package/dist/services/memory-repair-work-baseline.js +4 -2
- package/dist/services/memory-repair-work-completion.js +32 -12
- package/dist/services/memory-repair-work-results.js +21 -2
- package/dist/services/memory-repair-workspace.js +11 -2
- package/dist/services/memory-repair-writer-copy.js +2 -1
- package/dist/services/memory-result-binding.js +78 -12
- package/dist/services/memory-retained-review-authority.js +17 -8
- package/dist/services/memory-retained-second-baseline.js +1 -1
- package/dist/services/memory-retained-second-repairs.js +12 -6
- package/dist/services/memory-review-correction-plan.js +47 -12
- package/dist/services/memory-review-handler.js +17 -17
- package/dist/services/memory-review-inputs.js +55 -9
- package/dist/services/memory-review-limits.js +10 -0
- package/dist/services/memory-review-receipt.js +2 -2
- package/dist/services/memory-review-registration.js +13 -4
- package/dist/services/memory-review-work-custody.js +3 -1
- package/dist/services/memory-review-work-input.js +26 -7
- package/dist/services/memory-review-work-results.js +26 -4
- package/dist/services/memory-second-corrected-review-inputs.js +5 -3
- package/dist/services/memory-second-correction-plan.js +2 -1
- package/dist/services/memory-second-repair-launch.js +4 -4
- package/dist/services/memory-second-repair-source.js +1 -1
- package/dist/services/memory-stage-dispatch.js +38 -1
- package/dist/services/memory-stage-results.js +113 -22
- package/dist/services/memory-tool-probe.js +25 -0
- package/dist/services/memory-upgrade-analysis-handler.js +243 -0
- package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
- package/dist/services/memory-upgrade-evidence.js +100 -0
- package/dist/services/memory-upgrade-inventory.js +442 -0
- package/dist/services/memory-upgrade-publication-custody.js +73 -0
- package/dist/services/memory-upgrade-publication.js +413 -0
- package/dist/services/memory-upgrade-selection.js +430 -0
- package/dist/services/memory-upgrade-start.js +60 -0
- package/dist/services/memory-upgrade-storage-readiness.js +340 -0
- package/dist/services/memory-verification-acceptance.js +38 -17
- package/dist/services/memory-verification-checks.js +92 -27
- package/dist/services/memory-verification-receipt.js +6 -5
- package/dist/services/memory-verify-handler.js +15 -13
- package/dist/services/memory-work-result-json.js +12 -0
- package/dist/services/memory-work-result-preparation.js +5 -4
- package/dist/services/merge-server.js +4 -2
- package/dist/services/migrations.js +127 -98
- package/dist/services/native-daemon-history.js +2 -1
- package/dist/services/physical-startup.js +45 -0
- package/dist/services/project-registry.js +97 -0
- package/dist/services/project-summary.js +1 -1
- package/dist/services/projects.js +3 -93
- package/dist/services/prompt-delivery.js +71 -0
- package/dist/services/provider-capacity.js +112 -46
- package/dist/services/recovery-snapshot-database.js +32 -4
- package/dist/services/release-cache.js +8 -0
- package/dist/services/repair-intents.js +1 -12
- package/dist/services/repair-receipt.js +13 -0
- package/dist/services/run-control-worker.js +35 -18
- package/dist/services/run-control.js +2 -1
- package/dist/services/run-controller-adapter.js +2 -1
- package/dist/services/run-controller-process.js +4 -7
- package/dist/services/run-controller-recovery.js +8 -2
- package/dist/services/run-controller.js +215 -94
- package/dist/services/run-fork.js +1 -1
- package/dist/services/run-recovery-guard.js +30 -0
- package/dist/services/run-recovery.js +11 -44
- package/dist/services/run-start-custody.js +17 -5
- package/dist/services/runs.js +6 -1
- package/dist/services/runtime-scope-resume.js +12 -6
- package/dist/services/runtime-scope-stop.js +3 -2
- package/dist/services/runtime-scope-worker.js +46 -19
- package/dist/services/runtime-service.js +2 -1
- package/dist/services/sessions.js +3 -24
- package/dist/services/setup.js +63 -13
- package/dist/services/stage-handlers.js +22 -7
- package/dist/services/stage-pause.js +111 -44
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code.js +2 -2
- package/dist/services/vnext-fanout.js +51 -9
- package/dist/services/vnext-plan-review.js +3 -5
- package/dist/services/work-registry.js +117 -39
- package/dist/services/workspace-bootstrap.js +2 -1
- package/dist/services/workspace-files.js +1 -1
- package/dist/shared/entity-references.js +1 -8
- package/dist/storage/data-preservation.js +65 -0
- package/dist/storage/database.js +53 -19
- package/dist/storage/import-guard.js +84 -27
- package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
- package/dist/storage/paired-writer-migration.js +424 -0
- package/dist/storage/writer-contract.js +11 -4
- package/dist/storage/writer-migration.js +313 -52
- package/dist/storage/writer-restore.js +217 -0
- package/package.json +13 -6
|
@@ -2,8 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
5
|
import { AppError } from "../shared/errors.js";
|
|
6
|
+
import { validateCheckEnvironment } from "./check-environment.js";
|
|
7
7
|
import { getResourceDatabase } from "../storage/database.js";
|
|
8
8
|
import { finishManagedProcess, managedProcessRuntimeHome, managedProcessStatus, processIsAlive, processStartedAt, processTreeIsAlive, registerManagedProcess } from "./managed-processes.js";
|
|
9
9
|
import { setFlowRunRecoveryPaused } from "./runs.js";
|
|
@@ -133,11 +133,10 @@ function prepareSpawn(context, row) {
|
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
135
|
/** The physical child records its own birth before resource registration. */
|
|
136
|
-
export function claimControllerProcess(context, row) {
|
|
136
|
+
export function claimControllerProcess(context, row, owner = birth()) {
|
|
137
137
|
const launch = launchRecord(context, row);
|
|
138
138
|
if (!launch && (!row.process_id || !row.process_lease_token))
|
|
139
139
|
throw new AppError("controller_owner_mismatch", "Managed registration is not authorized", 1);
|
|
140
|
-
const owner = birth();
|
|
141
140
|
context.db.writeTransaction(() => {
|
|
142
141
|
// Recovery fences are reconciled by serve's retained failure boundary;
|
|
143
142
|
// only terminal admission must reject before claiming a physical owner.
|
|
@@ -173,13 +172,11 @@ export async function spawnController(context, row) {
|
|
|
173
172
|
const manifest = JSON.parse(row.manifest_json);
|
|
174
173
|
const run = requireRun(context, row.project_id, row.run_id);
|
|
175
174
|
const controllerId = row.controller_id, root = row.state_dir, token = row.owner_token;
|
|
176
|
-
const entry = fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
177
175
|
try {
|
|
178
|
-
|
|
179
|
-
throw new AppError("harness_runtime_unbuilt", "Build the CLI before launching its detached controller", 1);
|
|
176
|
+
const entry = (await import("./engines.js")).boundRunEngineEntrypoint(context, { projectRoot: run.project_root, runId: run.id });
|
|
180
177
|
const output = fs.openSync(path.join(root, "controller.log"), "a", 0o600);
|
|
181
178
|
try {
|
|
182
|
-
const child = spawn(process.execPath, [entry, "run", "drive", "serve", "--run", run.id, "--project-root", run.project_root, "--controller-id", controllerId, "--json"], { cwd: run.workspace_root, detached: true, stdio: ["ignore", output, output], env: { ...context.env, DD_FLOW_HOME: context.ddFlowHome, DD_FLOW_RESOURCE_HOME: manifest.resource_home, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_CONTROLLER_TOKEN: token } });
|
|
179
|
+
const child = spawn(process.execPath, [entry, "run", "drive", "serve", "--run", run.id, "--project-root", run.project_root, "--controller-id", controllerId, "--json"], { cwd: run.workspace_root, detached: true, stdio: ["ignore", output, output], env: { ...context.env, ...validateCheckEnvironment(manifest.check_environment), DD_FLOW_HOME: context.ddFlowHome, DD_FLOW_RESOURCE_HOME: manifest.resource_home, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_CONTROLLER_TOKEN: token } });
|
|
183
180
|
await new Promise((resolve, reject) => { child.once("spawn", resolve); child.once("error", reject); });
|
|
184
181
|
context.db.run("UPDATE run_controller_operations SET receipt_json = json_set(receipt_json, '$.child', json(?)), updated_at = ? WHERE operation_id = ?", [JSON.stringify({ pid: child.pid, pid_started_at: child.pid ? processStartedAt(child.pid) : null }), context.now(), launchId(row)]);
|
|
185
182
|
child.unref();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { nativeShutdownConfirmed } from "../harness-runtime/lib/native-children.mjs";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { setTimeout as delay } from "node:timers/promises";
|
|
@@ -148,7 +149,7 @@ export async function resumeControllerSession(context, row, manifest, state, ass
|
|
|
148
149
|
const call = (args, operationId) => controllerAdapter(executionContext, row, manifest, session, args, assertRecoveryOwner, operationId);
|
|
149
150
|
if (processTreeIsAlive(record)) {
|
|
150
151
|
const stopped = await call(["daemon", "stop", "--state-dir", session.state_dir, "--json"]);
|
|
151
|
-
if (stopped.stopped !== true || stopped.
|
|
152
|
+
if (!retired.daemon_id || stopped.stopped !== true || !nativeShutdownConfirmed(stopped, { daemonId: retired.daemon_id }))
|
|
152
153
|
throw new AppError("recovery_native_shutdown_unproven", "Recovery requires a clean native close before replacing its daemon", 1);
|
|
153
154
|
const deadline = Date.now() + 30_000;
|
|
154
155
|
while (processTreeIsAlive(record) && Date.now() < deadline) {
|
|
@@ -255,7 +256,12 @@ export async function resumeControllerSession(context, row, manifest, state, ass
|
|
|
255
256
|
|| current.process.lease_expires_at <= context.now() || metadata.run_generation !== row.generation || !processIsAlive(current.process))
|
|
256
257
|
throw new AppError("recovery_capacity_owner_changed", "Replacement native owner changed during capacity wait", 1);
|
|
257
258
|
},
|
|
258
|
-
decision: () =>
|
|
259
|
+
decision: (boundary) => {
|
|
260
|
+
if (boundary && (boundary.lifecycle_operation !== "recovery_accept" || boundary.recovery_id !== recovery.recovery_id
|
|
261
|
+
|| !context.db.get("SELECT 1 FROM run_recovery_bindings b JOIN hook_events h ON h.event_key = b.hook_event_key WHERE b.recovery_id = ? AND b.status = 'accepted' AND b.daemon_id = ? AND b.provider_session_id = ? AND h.project_id = ? AND h.status = 'claimed' AND h.event_key = ?", [recovery.recovery_id, daemonId, session.id, row.project_id, boundary.native_hook_event_key])))
|
|
262
|
+
return "reconciliation_blocked";
|
|
263
|
+
return recoveryGuard(context, row.project_id, row.run_id)?.status === "recovered" ? "completed" : "continue";
|
|
264
|
+
},
|
|
259
265
|
retainIntent: intent => retainRecoveryCapacityIntent(executionContext, { projectId: row.project_id, runId: row.run_id, recoveryId: recovery.recovery_id,
|
|
260
266
|
controllerId: row.controller_id, generation: row.generation, daemonId, intent, prompt: continuationText }),
|
|
261
267
|
prompt: (operationId, prompt) => sendAck(operationId, prompt) });
|
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import { assertLifecycleOutcomes, currentLifecycleFailure, managedInvocationContext, managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
1
|
+
import { assertLifecycleOutcomes, currentLifecycleFailure, managedInvocationContext, managedLifecycleCommand, awaitLifecycleInvocation, assertLifecycleInvocationCurrent } from "./lifecycle-invocations.js";
|
|
2
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
2
3
|
export function recoveryPacketMatchesStage(packet, scope) {
|
|
3
4
|
return packet.schema_id === 'dd-flow/root-recovery-packet@1' && packet.continuation === 'unfinished_work'
|
|
4
5
|
&& packet.run_id === scope.runId && packet.generation === scope.generation && packet.stage === scope.stage
|
|
5
6
|
&& packet.work_id === scope.workId && packet.paused === false;
|
|
6
7
|
}
|
|
7
8
|
import crypto from "node:crypto";
|
|
9
|
+
import { nativeShutdownConfirmed } from "../harness-runtime/lib/native-children.mjs";
|
|
8
10
|
import fs from "node:fs";
|
|
9
11
|
import path from "node:path";
|
|
10
12
|
import { setTimeout as delay } from "node:timers/promises";
|
|
11
13
|
import { fileURLToPath } from "node:url";
|
|
12
14
|
import { quote } from "shell-quote";
|
|
15
|
+
import { retainedBrowserEnvironment, validateCheckEnvironment } from "./check-environment.js";
|
|
13
16
|
import { ensureRunControllerStorage, getDatabase } from "../storage/database.js";
|
|
14
17
|
import { canonicalPath, resolveProjectRoot } from "../storage/paths.js";
|
|
15
18
|
import { publishImmutableInput } from "../storage/immutable-input.js";
|
|
@@ -20,17 +23,21 @@ import { requireProjectByRoot } from "./projects.js";
|
|
|
20
23
|
import { assertRunMutationAllowed, recoveryGuard } from "./run-recovery.js";
|
|
21
24
|
import { getFlowRunStatus, setFlowRunRecoveryPaused, assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
22
25
|
import { loadExternalStageContext } from "./stage-context.js";
|
|
23
|
-
import { flowCommand } from "./stage-pause.js";
|
|
26
|
+
import { flowCommand, stageResumeSourcePath } from "./stage-pause.js";
|
|
24
27
|
import { sameSessionProfile, runtimeHarness } from "./execution-policy.js";
|
|
25
28
|
import { selectControllerExecution } from "./controller-execution.js";
|
|
26
29
|
import { resolveHarnessCommand, harnessRuntimeArguments } from "./harness-config.js";
|
|
27
30
|
import { adapterSessionId, prepareHarnessFlowExecutable, runHarnessAdapter } from "./harness-adapter.js";
|
|
28
|
-
import { assertManagedProcessLease, confirmManagedProcess, renewManagedProcess, finishManagedProcess, processStartedAt, processTreeIsAlive, resourceHome } from "./managed-processes.js";
|
|
31
|
+
import { assertManagedProcessLease, confirmManagedProcess, prepareManagedProcessConfirmation, renewManagedProcess, finishManagedProcess, processStartedAt, processTreeIsAlive, resourceHome } from "./managed-processes.js";
|
|
29
32
|
import { managedLeaseMonitor } from "./managed-lease-monitor.js";
|
|
33
|
+
import { physicalStartup } from "./physical-startup.js";
|
|
30
34
|
import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
|
|
31
35
|
import { initCodexHome } from "./hooks.js";
|
|
32
36
|
import { launchExternalWork } from "./external-work-launch.js";
|
|
37
|
+
import { runControllerMemoryTask } from "./controller-memory-task.js";
|
|
33
38
|
import { controllerNativeChildren, controllerStageEntryPrompt, controllerStageResumePrompt, nextControllerFanout } from "./controller-fanout.js";
|
|
39
|
+
import { publishPromptDelivery } from "./prompt-delivery.js";
|
|
40
|
+
import { publishStagePacket } from "./stage-report-renderer.js";
|
|
34
41
|
import { claimQueuedMergeRequest } from "./merge-server.js";
|
|
35
42
|
import { prepareVnextCodeWorkspace } from "./vnext-code.js";
|
|
36
43
|
import { requireRun, requireController, controllerReceipt, appendEvent, saveState, retainControllerContinuation, retainControllerCapacityIntent, digest, digestBytes, errorRecord } from "./run-controller-state.js";
|
|
@@ -38,7 +45,7 @@ import { assertNoCorrelatedRejection } from "./continuation-outcome.js";
|
|
|
38
45
|
import { spawnController, recordControllerLauncher, claimControllerProcess, markControllerDispatchReady, assertControllerLeaseAllowed } from "./run-controller-process.js";
|
|
39
46
|
import { controllerAdapter, controllerEnvironment, sessionArguments } from "./run-controller-adapter.js";
|
|
40
47
|
import { retainedChildObserver } from "./controller-child-observer.js";
|
|
41
|
-
import { capacityDelay, continueProviderCapacity, eligibleProviderChildOverload, providerChildFailure } from "./provider-capacity.js";
|
|
48
|
+
import { capacityDelay, continueProviderCapacity, eligibleProviderChildOverload, providerChildFailure, ownedProviderBoundary } from "./provider-capacity.js";
|
|
42
49
|
import { resumeControllerSession, failControllerRecovery } from "./run-controller-recovery.js";
|
|
43
50
|
import { captureControllerBoundary } from "./run-controller-capture.js";
|
|
44
51
|
import { requestRunControl } from "./run-control-worker.js";
|
|
@@ -133,7 +140,8 @@ export function retainRunControllerLaunch(context, input, prepared = prepareRunC
|
|
|
133
140
|
}
|
|
134
141
|
const manifest = { schema_id: routing.kind === "descriptor" ? "dd-flow/run-controller-manifest@2" : "dd-flow/run-controller-manifest@1", request,
|
|
135
142
|
...(routing.kind === "descriptor" ? { execution_binding: routing.binding } : { execution_profile: routing.legacy }),
|
|
136
|
-
contexts: frozenContexts, adapters, flow_executable: flowExecutable, resource_home: resourceHome(context), budget
|
|
143
|
+
contexts: frozenContexts, adapters, flow_executable: flowExecutable, resource_home: resourceHome(context), budget,
|
|
144
|
+
...(retainedBrowserEnvironment(context.env) ? { check_environment: retainedBrowserEnvironment(context.env) } : {}) };
|
|
137
145
|
const token = crypto.randomUUID();
|
|
138
146
|
const now = context.now();
|
|
139
147
|
const state = { stage: null, sessions: [], current_session: null, turns: 0, last_continuation: null };
|
|
@@ -215,92 +223,120 @@ export async function serveRunController(context, input) {
|
|
|
215
223
|
const row = requireController(context, input.controllerId);
|
|
216
224
|
if (row.project_id !== project.id || row.run_id !== input.runId || row.owner_token !== context.env.DD_FLOW_CONTROLLER_TOKEN)
|
|
217
225
|
throw new AppError("controller_owner_mismatch", "Managed serve does not own this launch", 1);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const state = JSON.parse(row.state_json);
|
|
224
|
-
// Resource and RUN commits are distinct effects. A RUN keepalive failure must
|
|
225
|
-
// neither erase a resource receipt nor grant RUN dispatch authority.
|
|
226
|
-
const mergeLease = controllerLeaseMonitor(attemptMs => {
|
|
227
|
-
const expiry = new Date(Date.now() + 120_000).toISOString();
|
|
228
|
-
const db = context.db.withDeadline(Date.now() + attemptMs);
|
|
229
|
-
if (db.get("SELECT 1 FROM merge_requests WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status = 'dispatching' LIMIT 1", [row.project_id, row.run_id, row.controller_id]))
|
|
230
|
-
db.writeTransaction(() => {
|
|
231
|
-
const guard = recoveryGuard({ ...context, db }, row.project_id, row.run_id);
|
|
232
|
-
if ((guard?.generation ?? 0) !== row.generation || ["draining", "sealed", "resuming"].includes(guard?.status ?? ""))
|
|
233
|
-
throw new AppError("controller_lease_lost", "RUN generation changed before MERGE keepalive", 1);
|
|
234
|
-
db.run("UPDATE merge_requests SET dispatch_lease_expires_at = ?, updated_at = ? WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status = 'dispatching'", [expiry, context.now(), row.project_id, row.run_id, row.controller_id]);
|
|
235
|
-
});
|
|
236
|
-
return { owned: true, lease_expires_at: expiry };
|
|
237
|
-
});
|
|
238
|
-
const assertLeaseFence = () => {
|
|
239
|
-
assertControllerLeaseAllowed(context, row);
|
|
240
|
-
};
|
|
241
|
-
const heartbeat = setInterval(() => {
|
|
242
|
-
lease.background(assertLeaseFence, () => { });
|
|
243
|
-
if (state.merge_claim)
|
|
244
|
-
mergeLease.background(assertLeaseFence, () => { });
|
|
245
|
-
}, 30_000);
|
|
246
|
-
heartbeat.unref();
|
|
247
|
-
const manifest = JSON.parse(row.manifest_json);
|
|
248
|
-
const assertPhysicalOwner = () => {
|
|
249
|
-
lease.assertNoRenewal();
|
|
250
|
-
if (state.merge_claim)
|
|
251
|
-
mergeLease.assertNoRenewal();
|
|
252
|
-
// This isolated read already bounds SQLite contention. A wall deadline
|
|
253
|
-
// before opening the reader mistakes scheduler delay for heartbeat loss;
|
|
254
|
-
// the observed row's actual expiry/token still fences productive dispatch.
|
|
255
|
-
assertManagedProcessLease(context, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid });
|
|
256
|
-
const current = requireController(context, row.controller_id);
|
|
257
|
-
if (current.owner_pid !== process.pid || current.owner_token !== row.owner_token || !["running", "capturing", "waiting_for_user", "waiting_for_context", "waiting_for_children", "waiting_for_capacity", "waiting_for_merge"].includes(current.status))
|
|
258
|
-
throw new AppError("controller_owner_mismatch", "Controller no longer owns productive dispatch", 1);
|
|
259
|
-
};
|
|
260
|
-
const assertOwner = () => { assertPhysicalOwner(); assertRunMutationAllowed(context, row.project_id, row.run_id, row.generation); };
|
|
226
|
+
let lease;
|
|
227
|
+
let mergeLease;
|
|
228
|
+
let heartbeat;
|
|
229
|
+
let state;
|
|
230
|
+
let claimedProcess = false;
|
|
261
231
|
let primaryFailure;
|
|
262
232
|
const retainSecondary = (phase, error) => {
|
|
263
233
|
process.stderr.write(`${JSON.stringify({ controller_id: row.controller_id, phase, primary: errorRecord(primaryFailure), secondary: errorRecord(error) })}\n`);
|
|
264
234
|
};
|
|
265
235
|
try {
|
|
236
|
+
const currentState = state = JSON.parse(row.state_json);
|
|
237
|
+
const startup = physicalStartup(context.databaseDeadlineAt);
|
|
238
|
+
const assertStartingOwner = () => {
|
|
239
|
+
const current = requireController(context, row.controller_id);
|
|
240
|
+
if (current.owner_token !== row.owner_token || current.generation !== row.generation || current.owner_pid !== null || current.status !== "starting")
|
|
241
|
+
throw new AppError("controller_owner_mismatch", "Controller startup no longer owns this launch", 1);
|
|
242
|
+
if (["done", "failed", "cancelled"].includes(requireRun(context, row.project_id, row.run_id).status))
|
|
243
|
+
throw new AppError("invalid_run_state", "A terminal RUN cannot start a physical controller", 1);
|
|
244
|
+
};
|
|
245
|
+
const owner = await startup.birth(assertStartingOwner);
|
|
246
|
+
try {
|
|
247
|
+
claimControllerProcess(context, row, owner);
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
claimedProcess = row.owner_pid === process.pid;
|
|
251
|
+
}
|
|
252
|
+
if (!row.process_id || !row.process_lease_token)
|
|
253
|
+
throw new AppError("controller_owner_mismatch", "Managed owner has no resource lease", 1);
|
|
254
|
+
const confirmation = { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid };
|
|
255
|
+
const originalLease = prepareManagedProcessConfirmation(context, confirmation).lease_expires_at;
|
|
256
|
+
const confirmed = await startup.observe(() => confirmManagedProcess(context, confirmation), () => assertControllerLeaseAllowed(context, row), originalLease);
|
|
257
|
+
lease = controllerLeaseMonitor(attemptMs => renewManagedProcess({ ...context, resourceDatabaseMode: "prepared_existing", databaseDeadlineAt: Date.now() + attemptMs }, { id: row.process_id, leaseToken: row.process_lease_token }), confirmed.lease_expires_at);
|
|
258
|
+
// Resource and RUN commits are distinct effects. A RUN keepalive failure must
|
|
259
|
+
// neither erase a resource receipt nor grant RUN dispatch authority.
|
|
260
|
+
mergeLease = controllerLeaseMonitor(attemptMs => {
|
|
261
|
+
const expiry = new Date(Date.now() + 120_000).toISOString();
|
|
262
|
+
const db = context.db.withDeadline(Date.now() + attemptMs);
|
|
263
|
+
if (db.get("SELECT 1 FROM merge_requests WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status = 'dispatching' LIMIT 1", [row.project_id, row.run_id, row.controller_id]))
|
|
264
|
+
db.writeTransaction(() => {
|
|
265
|
+
const guard = recoveryGuard({ ...context, db }, row.project_id, row.run_id);
|
|
266
|
+
if ((guard?.generation ?? 0) !== row.generation || ["draining", "sealed", "resuming"].includes(guard?.status ?? ""))
|
|
267
|
+
throw new AppError("controller_lease_lost", "RUN generation changed before MERGE keepalive", 1);
|
|
268
|
+
db.run("UPDATE merge_requests SET dispatch_lease_expires_at = ?, updated_at = ? WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status = 'dispatching'", [expiry, context.now(), row.project_id, row.run_id, row.controller_id]);
|
|
269
|
+
});
|
|
270
|
+
return { owned: true, lease_expires_at: expiry };
|
|
271
|
+
});
|
|
272
|
+
const assertLeaseFence = () => {
|
|
273
|
+
assertControllerLeaseAllowed(context, row);
|
|
274
|
+
};
|
|
275
|
+
heartbeat = setInterval(() => {
|
|
276
|
+
lease.background(assertLeaseFence, () => { });
|
|
277
|
+
if (currentState.merge_claim)
|
|
278
|
+
mergeLease.background(assertLeaseFence, () => { });
|
|
279
|
+
}, 30_000);
|
|
280
|
+
heartbeat.unref();
|
|
281
|
+
const manifest = JSON.parse(row.manifest_json);
|
|
282
|
+
const assertPhysicalOwner = () => {
|
|
283
|
+
lease.assertNoRenewal();
|
|
284
|
+
if (currentState.merge_claim)
|
|
285
|
+
mergeLease.assertNoRenewal();
|
|
286
|
+
// This isolated read already bounds SQLite contention. A wall deadline
|
|
287
|
+
// before opening the reader mistakes scheduler delay for heartbeat loss;
|
|
288
|
+
// the observed row's actual expiry/token still fences productive dispatch.
|
|
289
|
+
assertManagedProcessLease(context, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid });
|
|
290
|
+
const current = requireController(context, row.controller_id);
|
|
291
|
+
if (current.owner_pid !== process.pid || current.owner_token !== row.owner_token || !["running", "capturing", "waiting_for_user", "waiting_for_context", "waiting_for_children", "waiting_for_capacity", "waiting_for_merge"].includes(current.status))
|
|
292
|
+
throw new AppError("controller_owner_mismatch", "Controller no longer owns productive dispatch", 1);
|
|
293
|
+
};
|
|
294
|
+
const assertOwner = () => { assertPhysicalOwner(); assertRunMutationAllowed(context, row.project_id, row.run_id, row.generation); };
|
|
266
295
|
await lease.ensureConfirmed(assertLeaseFence);
|
|
267
|
-
if (
|
|
296
|
+
if (currentState.merge_claim)
|
|
268
297
|
await mergeLease.ensureConfirmed(assertLeaseFence);
|
|
269
298
|
context.db.writeTransaction(() => {
|
|
270
299
|
markControllerDispatchReady(context, row);
|
|
271
|
-
if (!
|
|
300
|
+
if (!currentState.recovery && (recoveryGuard(context, row.project_id, row.run_id)?.generation ?? 0) === row.generation && requireRun(context, row.project_id, row.run_id).status === "paused")
|
|
272
301
|
setFlowRunRecoveryPaused(context, row.project_id, row.run_id, false);
|
|
273
302
|
});
|
|
274
303
|
appendEvent(context, row.controller_id, "owner_started", { pid: process.pid, generation: row.generation });
|
|
275
|
-
if (
|
|
276
|
-
await resumeControllerSession(context, row, manifest,
|
|
277
|
-
await executeController(context, row, manifest,
|
|
304
|
+
if (currentState.recovery && !["done", "failed", "cancelled"].includes(requireRun(context, row.project_id, row.run_id).status))
|
|
305
|
+
await resumeControllerSession(context, row, manifest, currentState, assertPhysicalOwner);
|
|
306
|
+
await executeController(context, row, manifest, currentState, assertOwner, assertPhysicalOwner, async () => { await lease.ensureConfirmed(assertLeaseFence); if (currentState.merge_claim)
|
|
278
307
|
await mergeLease.ensureConfirmed(assertLeaseFence); });
|
|
279
308
|
return controllerReceipt(requireController(context, row.controller_id));
|
|
280
309
|
}
|
|
281
310
|
catch (error) {
|
|
282
311
|
primaryFailure = error;
|
|
283
312
|
try {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (control) {
|
|
295
|
-
// Control is a later state transition, not evidence that the causal
|
|
296
|
-
// failure which fenced admission disappeared.
|
|
297
|
-
context.db.run("UPDATE run_controllers SET status = CASE WHEN last_error_json IS NULL THEN 'control_requested' ELSE status END, updated_at = ? WHERE controller_id = ? AND owner_token = ?", [context.now(), row.controller_id, row.owner_token]);
|
|
298
|
-
appendEvent(context, row.controller_id, "control_boundary_reached", { control_id: control.control_id, action: control.action, generation: guard.generation, settled: false });
|
|
299
|
-
return controllerReceipt(requireController(context, row.controller_id));
|
|
313
|
+
const controlled = context.db.writeTransaction(() => {
|
|
314
|
+
const currentOwner = requireController(context, row.controller_id);
|
|
315
|
+
const sameBinding = currentOwner.owner_token === row.owner_token && currentOwner.generation === row.generation
|
|
316
|
+
&& currentOwner.process_id === row.process_id && currentOwner.process_lease_token === row.process_lease_token;
|
|
317
|
+
const ownsFailure = claimedProcess ? currentOwner.owner_pid === process.pid : row.owner_pid === null && currentOwner.owner_pid === null && row.status === "starting" && currentOwner.status === "starting";
|
|
318
|
+
if (!sameBinding || !ownsFailure || ["completed", "cancelled", "superseded"].includes(currentOwner.status))
|
|
319
|
+
throw error;
|
|
320
|
+
try {
|
|
321
|
+
if (state)
|
|
322
|
+
failControllerRecovery(context, row, state, error);
|
|
300
323
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
324
|
+
catch (secondary) {
|
|
325
|
+
retainSecondary("fail_controller_recovery", secondary);
|
|
326
|
+
}
|
|
327
|
+
if ((error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale"].includes(error.code) || (recoveryGuard(context, row.project_id, row.run_id)?.generation ?? 0) !== row.generation)
|
|
328
|
+
&& context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_controls'")) {
|
|
329
|
+
const guard = recoveryGuard(context, row.project_id, row.run_id);
|
|
330
|
+
const control = guard && ["draining", "sealed"].includes(guard.status) && context.db.get("SELECT control_id, action FROM run_controls WHERE project_id = ? AND run_id = ? AND recovery_id = ? AND status IN ('requested','settled')", [row.project_id, row.run_id, guard.recovery_id]);
|
|
331
|
+
if (control) {
|
|
332
|
+
// Control is a later state transition, not evidence that the causal
|
|
333
|
+
// failure which fenced admission disappeared.
|
|
334
|
+
context.db.run("UPDATE run_controllers SET status = CASE WHEN last_error_json IS NULL THEN 'control_requested' ELSE status END, updated_at = ? WHERE controller_id = ? AND owner_token = ?", [context.now(), row.controller_id, row.owner_token]);
|
|
335
|
+
appendEvent(context, row.controller_id, "control_boundary_reached", { control_id: control.control_id, action: control.action, generation: guard.generation, settled: false });
|
|
336
|
+
return controllerReceipt(requireController(context, row.controller_id));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
304
340
|
// A failed capture must not reopen admission when this process exits.
|
|
305
341
|
const changed = context.db.run("UPDATE run_controllers SET status = CASE WHEN status IN ('capturing','capture_failed') THEN 'capture_failed' ELSE 'recovery_required' END, last_error_json = ?, updated_at = ? WHERE controller_id = ? AND owner_token = ? AND status NOT IN ('completed','cancelled','superseded')", [JSON.stringify(errorRecord(error)), context.now(), row.controller_id, row.owner_token]);
|
|
306
342
|
context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE project_id = ? AND run_id = ? AND dispatch_owner = ? AND status IN ('dispatching','active')", [JSON.stringify(errorRecord(error)), context.now(), row.project_id, row.run_id, row.controller_id]);
|
|
@@ -308,11 +344,14 @@ export async function serveRunController(context, input) {
|
|
|
308
344
|
setFlowRunRecoveryPaused(context, row.project_id, row.run_id, true);
|
|
309
345
|
}
|
|
310
346
|
appendEvent(context, row.controller_id, "recovery_required", errorRecord(error));
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
347
|
+
}
|
|
348
|
+
catch (failure) {
|
|
349
|
+
retainSecondary("persist_controller_failure", failure);
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
});
|
|
353
|
+
if (controlled)
|
|
354
|
+
return controlled;
|
|
316
355
|
}
|
|
317
356
|
catch (secondary) {
|
|
318
357
|
retainSecondary("controller_failure_boundary", secondary);
|
|
@@ -320,12 +359,15 @@ export async function serveRunController(context, input) {
|
|
|
320
359
|
throw error;
|
|
321
360
|
}
|
|
322
361
|
finally {
|
|
323
|
-
lease
|
|
324
|
-
mergeLease
|
|
325
|
-
|
|
362
|
+
lease?.close();
|
|
363
|
+
mergeLease?.close();
|
|
364
|
+
if (heartbeat)
|
|
365
|
+
clearInterval(heartbeat);
|
|
326
366
|
finishOwner();
|
|
327
367
|
}
|
|
328
368
|
function finishOwner() {
|
|
369
|
+
if (!claimedProcess || row.owner_pid !== process.pid || !row.process_id || !row.process_lease_token)
|
|
370
|
+
return;
|
|
329
371
|
try {
|
|
330
372
|
finishManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token, state: "stopped", reason: "controller_owner_exited" });
|
|
331
373
|
}
|
|
@@ -497,7 +539,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
497
539
|
const routing = selectControllerExecution(context, requireRun(context, row.project_id, row.run_id), manifest);
|
|
498
540
|
const policy = routing.policy;
|
|
499
541
|
const profiles = routing.profiles;
|
|
500
|
-
let executionContext = { ...context, env: { ...context.env, DD_FLOW_BIN: manifest.flow_executable, DD_FLOW_RESOURCE_HOME: manifest.resource_home, DD_FLOW_RUNTIME_BUDGET: manifest.budget ? JSON.stringify(manifest.budget) : undefined, DD_FLOW_CODE_READINESS_OWNER: row.controller_id } };
|
|
542
|
+
let executionContext = { ...context, env: { ...context.env, ...validateCheckEnvironment(manifest.check_environment), DD_FLOW_BIN: manifest.flow_executable, DD_FLOW_RESOURCE_HOME: manifest.resource_home, DD_FLOW_RUNTIME_BUDGET: manifest.budget ? JSON.stringify(manifest.budget) : undefined, DD_FLOW_CODE_READINESS_OWNER: row.controller_id } };
|
|
501
543
|
const bindInvocationScope = (session) => {
|
|
502
544
|
executionContext = managedInvocationContext(executionContext, { harness: runtimeHarness(session.profile), stateDir: session.state_dir, sessionId: session.id,
|
|
503
545
|
projectRoot: manifest.request.project_root, runId: row.run_id, generation: row.generation });
|
|
@@ -598,9 +640,9 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
598
640
|
const call = (session, args, productive = true) => controllerAdapter(executionContext, row, manifest, session, args, productive ? assertOwner : undefined, undefined, productive ? cancellation.signal : undefined, productive ? renewLease : undefined);
|
|
599
641
|
const confirmStopped = async (stateDir, receipt) => {
|
|
600
642
|
const fail = () => new AppError("controller_native_shutdown_unproven", "The managed daemon is not proven clean and closed", 1, { state_dir: stateDir, receipt });
|
|
601
|
-
if (receipt.stopped !== true || receipt.clean !== true)
|
|
602
|
-
throw fail();
|
|
603
643
|
const binding = requireManagedDaemonBinding(executionContext, { stateDir, projectId: row.project_id, runId: row.run_id });
|
|
644
|
+
if (!binding.state.daemon_id || receipt.stopped !== true || !nativeShutdownConfirmed(receipt, { daemonId: binding.state.daemon_id }))
|
|
645
|
+
throw fail();
|
|
604
646
|
const deadline = performance.now() + 30_000;
|
|
605
647
|
while (processTreeIsAlive(binding.process) && performance.now() < deadline)
|
|
606
648
|
await delay(100);
|
|
@@ -645,6 +687,45 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
645
687
|
if (!pending)
|
|
646
688
|
await reconcilePromptPredecessor(session);
|
|
647
689
|
const firstOperationId = pending?.root_operation_id ?? operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
|
|
690
|
+
const hitlText = async () => {
|
|
691
|
+
const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
|
|
692
|
+
const answer = context.db.get("SELECT input_json,receipt_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND status IN ('accepted','completed')", [answerId, row.controller_id]);
|
|
693
|
+
const accepted = answer && JSON.parse(answer.input_json);
|
|
694
|
+
const packet = answer?.receipt_json && JSON.parse(answer.receipt_json).hitl_packet;
|
|
695
|
+
if (!packet || !accepted || packet.generation !== row.generation || packet.pause_id !== accepted.pause_id || packet.stage !== accepted.stage || packet.work_id !== accepted.work_id || packet.answer_file !== accepted.file || packet.answer_sha256 !== accepted.sha256)
|
|
696
|
+
throw new AppError("controller_hitl_packet_invalid", "Accepted HITL continuation lost its exact binding", 1);
|
|
697
|
+
const issued = context.db.get("SELECT id,status,scope_json,command FROM lifecycle_invocations WHERE id = ?", [packet.invocation_id ?? null]);
|
|
698
|
+
if (!issued || issued.scope_json !== executionContext.env.DD_FLOW_INVOCATION_SCOPE)
|
|
699
|
+
throw new AppError("controller_hitl_packet_invalid", "Accepted HITL command belongs to another scope", 1);
|
|
700
|
+
if (issued.status === "executing" || issued.status === "settled") {
|
|
701
|
+
const recovered = await awaitLifecycleInvocation(executionContext, { id: issued.id, command: issued.command, assertCurrent: scope => assertLifecycleInvocationCurrent(executionContext, scope, issued.command) });
|
|
702
|
+
if (!recovered.replay)
|
|
703
|
+
throw new AppError("controller_hitl_reconciliation_required", "Accepted resume is not committed", 1);
|
|
704
|
+
const outcome = recovered.outcome;
|
|
705
|
+
if (!outcome.result)
|
|
706
|
+
throw new AppError("controller_hitl_reconciliation_required", "Accepted resume has no committed continuation", 1);
|
|
707
|
+
if (outcome.result.resume_source_path !== packet.resume_source_path || typeof outcome.result.answer_path !== "string")
|
|
708
|
+
throw new AppError("controller_hitl_packet_invalid", "Committed resume differs from the accepted packet locator", 1);
|
|
709
|
+
publishStagePacket(executionContext, row.project_id, row.run_id, packet.stage, outcome.result.answer_path);
|
|
710
|
+
publishStagePacket(executionContext, row.project_id, row.run_id, packet.stage, packet.resume_source_path);
|
|
711
|
+
const bytes = fs.readFileSync(outcome.result.answer_path);
|
|
712
|
+
if (digestBytes(bytes) !== packet.answer_sha256)
|
|
713
|
+
throw new AppError("context_checksum_mismatch", "Committed answer differs from its accepted bytes", 1);
|
|
714
|
+
fs.mkdirSync(path.dirname(packet.answer_file), { recursive: true, mode: 0o700 });
|
|
715
|
+
publishImmutableInput(packet.answer_file, bytes, "hitl_answer_conflict");
|
|
716
|
+
publishPromptDelivery(outcome.result);
|
|
717
|
+
const delivery = outcome.result.prompt_delivery;
|
|
718
|
+
const promptPath = delivery?.path ?? outcome.result.prompt_path;
|
|
719
|
+
if (typeof promptPath !== "string" || !path.isAbsolute(promptPath))
|
|
720
|
+
throw new AppError("controller_hitl_packet_invalid", "Committed resume has no absolute prompt delivery", 1);
|
|
721
|
+
// Keep continuation bytes stable across a retained overload episode.
|
|
722
|
+
// Its conditional instructions point at this restored exact custody.
|
|
723
|
+
return controllerStageResumePrompt(packet);
|
|
724
|
+
}
|
|
725
|
+
if (issued.status !== "issued" || digestBytes(fs.readFileSync(packet.answer_file)) !== packet.answer_sha256)
|
|
726
|
+
throw new AppError("context_checksum_mismatch", "Accepted HITL answer or command changed before resume", 1);
|
|
727
|
+
return controllerStageResumePrompt(packet);
|
|
728
|
+
};
|
|
648
729
|
const send = (value, id) => {
|
|
649
730
|
const retained = context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [id, row.controller_id, row.generation]);
|
|
650
731
|
if (retained) {
|
|
@@ -678,7 +759,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
678
759
|
promptPending = true;
|
|
679
760
|
observedPromptOperation = firstOperationId;
|
|
680
761
|
try {
|
|
681
|
-
receipt = await send(text, firstOperationId);
|
|
762
|
+
receipt = await send(hitl ? await hitlText() : text, firstOperationId);
|
|
682
763
|
}
|
|
683
764
|
finally {
|
|
684
765
|
promptPending = false;
|
|
@@ -686,9 +767,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
686
767
|
}
|
|
687
768
|
}
|
|
688
769
|
catch (error) {
|
|
689
|
-
const
|
|
690
|
-
if (hitl && !resumeCommand?.includes(" stage resume "))
|
|
691
|
-
throw error;
|
|
770
|
+
const hitlContinuationText = hitl && terminalProviderOverload(error, session.id) ? await hitlText() : undefined;
|
|
692
771
|
const inspect = (args, id) => runHarnessAdapter({ executable: manifest.adapters[runtimeHarness(session.profile)].executable,
|
|
693
772
|
args, env: controllerEnvironment(context, manifest, session), runtimeOwner: session.owner, operationId: id, timeoutMs: 30_000, signal: cancellation.signal });
|
|
694
773
|
const resumed = await continueProviderCapacity({ error, sessionId: session.id, operationId: firstOperationId, signal: cancellation.signal, allowSettledDescendants: true, atomicIntent: true,
|
|
@@ -706,7 +785,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
706
785
|
assertCurrent: () => {
|
|
707
786
|
assertOwner();
|
|
708
787
|
},
|
|
709
|
-
decision: async () => {
|
|
788
|
+
decision: async (boundary) => {
|
|
710
789
|
if (kind === "child") {
|
|
711
790
|
const target = Object.entries(state.child_capacity ?? {}).find(([, retained]) => retained.operation_id === firstOperationId);
|
|
712
791
|
if (!target)
|
|
@@ -718,6 +797,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
718
797
|
}
|
|
719
798
|
const view = getFlowRunStatus(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id });
|
|
720
799
|
const next = view.continuation;
|
|
800
|
+
if (boundary) {
|
|
801
|
+
if (!ownedProviderBoundary(context, { projectId: row.project_id, runId: row.run_id, stage: state.stage, sessionId: session.id, generation: row.generation, scope: "stage" }, boundary))
|
|
802
|
+
return "reconciliation_blocked";
|
|
803
|
+
return boundary.outcome === "paused" ? "paused" : "completed";
|
|
804
|
+
}
|
|
721
805
|
if (next.kind === "blocked")
|
|
722
806
|
return "reconciliation_blocked";
|
|
723
807
|
if (next.kind === "paused" && next.stage !== state.stage)
|
|
@@ -740,7 +824,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
740
824
|
},
|
|
741
825
|
retainIntent: intent => retainControllerCapacityIntent(context, row, state, intent, session.id, kind ?? "stage", assertOwner),
|
|
742
826
|
prompt: (id, value) => send(value, id),
|
|
743
|
-
...(
|
|
827
|
+
...(hitlContinuationText ? { continuationText: hitlContinuationText } : {}) });
|
|
744
828
|
if (!resumed)
|
|
745
829
|
throw error;
|
|
746
830
|
receipt = resumed;
|
|
@@ -804,6 +888,17 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
804
888
|
await prompt(session, fs.readFileSync(file, "utf8"), pending.root_operation_id, pending.kind === "stage" ? undefined : pending.kind);
|
|
805
889
|
continue;
|
|
806
890
|
}
|
|
891
|
+
if (next.kind !== "paused" && state.current_session !== null) {
|
|
892
|
+
const committed = context.db.get("SELECT operation_id,receipt_json FROM run_controller_operations WHERE controller_id = ? AND status = 'accepted' AND json_extract(input_json,'$.action') = 'user_answer' AND json_extract(receipt_json,'$.hitl_packet.generation') = ? AND json_extract(receipt_json,'$.hitl_packet.stage') = ? ORDER BY created_at LIMIT 1", [row.controller_id, row.generation, state.stage]);
|
|
893
|
+
if (committed) {
|
|
894
|
+
const packet = JSON.parse(committed.receipt_json).hitl_packet;
|
|
895
|
+
const receipt = await prompt(state.sessions[state.current_session], controllerStageResumePrompt(packet), `${committed.operation_id}:prompt:${row.generation}`, "hitl");
|
|
896
|
+
context.db.run("UPDATE run_controller_operations SET status = 'completed', receipt_json = json_set(receipt_json,'$.delivered',json('true'),'$.session_id',?), updated_at = ? WHERE operation_id = ? AND status = 'accepted'", [adapterSessionId(receipt), context.now(), committed.operation_id]);
|
|
897
|
+
state.last_continuation = null;
|
|
898
|
+
saveState(context, row, state);
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
807
902
|
const completed = state.stage && view.index.stage_runs.find(stage => stage.stage === state.stage && ["done", "skipped"].includes(stage.status));
|
|
808
903
|
if (state.current_session !== null && (completed || next.stage !== state.stage) && childObserver(state.sessions[state.current_session])?.failed().length)
|
|
809
904
|
throw new AppError("fanout_dispatch_fenced", "A retained child failure forbids completing or advancing its Stage", 1);
|
|
@@ -849,13 +944,29 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
849
944
|
assertOwner();
|
|
850
945
|
// The accepted answer is logical intent; only its generation-bound
|
|
851
946
|
// adapter operation represents a physical dispatch that stop must drain.
|
|
852
|
-
|
|
853
|
-
const
|
|
947
|
+
let invocationId;
|
|
948
|
+
const capture = executionContext.captureLifecycleAssignment;
|
|
949
|
+
executionContext.captureLifecycleAssignment = id => { invocationId = id; capture?.(id); };
|
|
950
|
+
let command;
|
|
951
|
+
try {
|
|
952
|
+
command = managedLifecycleCommand(executionContext, `${flowCommand(executionContext)} stage resume ${run.id} --stage ${next.stage} --work ${input.work_id} --project-root ${quote([run.project_root])} --answer-file ${quote([input.file])} --json`, { replaceUnobservedAnswer: true });
|
|
953
|
+
}
|
|
954
|
+
finally {
|
|
955
|
+
if (capture)
|
|
956
|
+
executionContext.captureLifecycleAssignment = capture;
|
|
957
|
+
else
|
|
958
|
+
delete executionContext.captureLifecycleAssignment;
|
|
959
|
+
}
|
|
960
|
+
if (!invocationId)
|
|
961
|
+
throw new AppError("controller_hitl_packet_invalid", "Accepted resume did not capture its issued identity", 1);
|
|
962
|
+
const packet = { pause_id: pause.id, stage: next.stage, work_id: input.work_id, answer_file: input.file, answer_sha256: input.sha256, command, generation: row.generation, invocation_id: invocationId, resume_source_path: stageResumeSourcePath(pause, fs.readFileSync(input.file, "utf8")) };
|
|
963
|
+
context.db.run("UPDATE run_controller_operations SET receipt_json = ?, updated_at = ? WHERE operation_id = ? AND status = 'accepted'", [JSON.stringify({ hitl_packet: packet }), context.now(), answer.operation_id]);
|
|
964
|
+
const receipt = await prompt(state.sessions[state.current_session], controllerStageResumePrompt(packet), `${answer.operation_id}:prompt:${row.generation}`, "hitl");
|
|
854
965
|
const afterAnswer = getFlowRunStatus(executionContext, { projectRoot: run.project_root, runId: run.id });
|
|
855
966
|
if (afterAnswer.index.stage_runs.some(stage => stage.status === "paused" && stage.pause?.id === pause.id)) {
|
|
856
967
|
throw new AppError("controller_answer_not_applied", "The answer Turn completed without resuming the assigned HITL pause", 1, { pause_id: pause.id, operation_id: answer.operation_id, stage: next.stage, effect: "no_progress" });
|
|
857
968
|
}
|
|
858
|
-
context.db.run("UPDATE run_controller_operations SET status = 'completed', receipt_json =
|
|
969
|
+
context.db.run("UPDATE run_controller_operations SET status = 'completed', receipt_json = json_set(receipt_json,'$.delivered',json('true'),'$.session_id',?), updated_at = ? WHERE operation_id = ?", [adapterSessionId(receipt), context.now(), answer.operation_id]);
|
|
859
970
|
state.last_continuation = null;
|
|
860
971
|
saveState(context, row, state);
|
|
861
972
|
setStatus("running");
|
|
@@ -1088,7 +1199,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1088
1199
|
continue;
|
|
1089
1200
|
}
|
|
1090
1201
|
}
|
|
1091
|
-
const
|
|
1202
|
+
const memoryFlow = ["mb-init", "mb-upgrade"].includes(run.flow_kind) || isDiagnosticFlow(run.flow_kind);
|
|
1203
|
+
const fanoutInput = { projectRoot: run.project_root, runId: run.id, stage: next.stage, harness: runtimeHarness(session.profile), sessionId: session.id, children, policy, profiles, dispatchFence: retainedFailures.length > 0 };
|
|
1204
|
+
const fanout = memoryFlow ? await runControllerMemoryTask(executionContext, { operation: "fanout", input: fanoutInput }, cancellation.signal)
|
|
1205
|
+
: await nextControllerFanout(executionContext, fanoutInput);
|
|
1206
|
+
if (memoryFlow)
|
|
1207
|
+
await renewLease();
|
|
1092
1208
|
assertOwner();
|
|
1093
1209
|
if (fanout.kind === "none") {
|
|
1094
1210
|
// An ACK-only recovery Turn may leave the original root Work unfinished.
|
|
@@ -1129,7 +1245,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1129
1245
|
try {
|
|
1130
1246
|
await renewLease();
|
|
1131
1247
|
assertOwner();
|
|
1132
|
-
const receipt = await
|
|
1248
|
+
const receipt = memoryFlow ? await runControllerMemoryTask(executionContext, { operation: "external", input: { projectRoot: run.project_root, runId: run.id, workId: work.work_id, stage: next.stage } }, cancellation.signal)
|
|
1249
|
+
: await launchExternalWork(executionContext, { workId: work.work_id, stage: next.stage, signal: cancellation.signal });
|
|
1250
|
+
if (memoryFlow) {
|
|
1251
|
+
await renewLease();
|
|
1252
|
+
assertOwner();
|
|
1253
|
+
}
|
|
1133
1254
|
if (fatal)
|
|
1134
1255
|
throw fatal;
|
|
1135
1256
|
appendEvent(context, row.controller_id, "external_work_observed", { work_id: work.work_id, receipt });
|
|
@@ -28,7 +28,7 @@ export function prepareRunFork(input) {
|
|
|
28
28
|
}
|
|
29
29
|
prepareForkEngine(engineVersion, input.integrityChecksum);
|
|
30
30
|
const manifest = prepareEvalRunSnapshot(snapshot);
|
|
31
|
-
if (
|
|
31
|
+
if (!["dd-flow/eval-run-snapshot@5", "dd-flow/eval-run-snapshot@7"].includes(manifest.schema_id) || manifest.purpose !== "stage_entry" || typeof manifest.stage_entry !== "string" || typeof manifest.run_id !== "string") {
|
|
32
32
|
throw new AppError("fork_checkpoint_invalid", "Fork requires a sealed stage-entry snapshot", 2, { snapshot });
|
|
33
33
|
}
|
|
34
34
|
const operational = prepareSnapshotOperationalDecision({ snapshot, runId: manifest.run_id, projectRoot: path.join(output, "project"), now: new Date().toISOString(), ...(input.operationalDecisionFile ? { file: input.operationalDecisionFile } : {}) });
|
|
@@ -1,6 +1,36 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import { AppError } from "../shared/errors.js";
|
|
2
3
|
import { CAPACITY_POLICY, MAX_OVERLOAD_RETRIES, overloadExhausted } from "../harness-runtime/lib/provider-overload-policy.mjs";
|
|
3
4
|
import { assertRunStartupSettled } from "./run-start-custody.js";
|
|
5
|
+
export function recoveryPacketReceipt(binding) {
|
|
6
|
+
if (!binding.packet_json || !binding.packet_sha256 || crypto.createHash("sha256").update(binding.packet_json).digest("hex") !== binding.packet_sha256)
|
|
7
|
+
throw new AppError("recovery_packet_missing", "Recovery requires its original verified Work packet", 1);
|
|
8
|
+
return { packet_json: binding.packet_json, packet_sha256: binding.packet_sha256, packet: JSON.parse(binding.packet_json) };
|
|
9
|
+
}
|
|
10
|
+
/** A cooperative drain permits only the WorkSession present at its fence. */
|
|
11
|
+
export function assertWorkSettlementAllowed(context, projectId, runId, workId) {
|
|
12
|
+
const guard = recoveryGuard(context, projectId, runId);
|
|
13
|
+
if (guard?.status === "draining" && context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_controls'")) {
|
|
14
|
+
const admission = context.db.get("SELECT a.work_session_id FROM run_control_admissions a JOIN run_controls c ON c.control_id = a.control_id JOIN work_sessions ws ON ws.id = a.work_session_id JOIN works w ON w.work_id = a.work_id WHERE c.project_id = ? AND c.run_id = ? AND c.recovery_id = ? AND c.action = 'pause' AND c.status = 'requested' AND a.work_id = ? AND ws.work_id = a.work_id AND ws.session_id = a.session_id AND ws.status = 'running' AND w.status = 'running' AND w.project_id = c.project_id AND w.run_id = c.run_id LIMIT 1", [projectId, runId, guard.recovery_id, workId]);
|
|
15
|
+
if (admission)
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
assertRunMutationAllowed(context, projectId, runId);
|
|
19
|
+
}
|
|
20
|
+
/** Completion rights are pinned to the Stage attempt present at the pause fence. */
|
|
21
|
+
export function assertStageSettlementAllowed(context, projectId, runId, stage, expectedGeneration) {
|
|
22
|
+
const guard = recoveryGuard(context, projectId, runId);
|
|
23
|
+
const generation = guard?.generation ?? 0;
|
|
24
|
+
if (expectedGeneration !== undefined && generation !== expectedGeneration)
|
|
25
|
+
throw new AppError("run_recovery_generation_stale", "RUN execution generation changed while the operation was in flight", 1, { run_id: runId, expected_generation: expectedGeneration, generation });
|
|
26
|
+
if (guard?.status === "draining" && context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_control_stage_admissions'")) {
|
|
27
|
+
const admitted = context.db.get(`SELECT a.work_session_id FROM run_control_stage_admissions a JOIN run_controls c ON c.control_id = a.control_id JOIN run_control_admissions w ON w.control_id = a.control_id AND w.work_session_id = a.work_session_id JOIN work_sessions ws ON ws.id = w.work_session_id JOIN runs r ON r.project_id = c.project_id AND r.id = c.run_id, json_each(r.index_json, '$.stage_runs') live
|
|
28
|
+
WHERE c.project_id = ? AND c.run_id = ? AND c.recovery_id = ? AND c.action = 'pause' AND c.status = 'requested' AND a.stage = ? AND json_extract(live.value, '$.stage') = a.stage AND COALESCE(json_extract(live.value, '$.attempt'), 'try-001') = a.attempt AND ws.work_id = w.work_id AND ws.session_id = w.session_id AND NOT EXISTS (SELECT 1 FROM work_sessions newer WHERE newer.work_id = w.work_id AND newer.status = 'running' AND newer.id <> ws.id) LIMIT 1`, [projectId, runId, guard.recovery_id, stage]);
|
|
29
|
+
if (admitted)
|
|
30
|
+
return generation;
|
|
31
|
+
}
|
|
32
|
+
return assertRunMutationAllowed(context, projectId, runId, expectedGeneration);
|
|
33
|
+
}
|
|
4
34
|
export function recoveryGuard(context, projectId, runId) { return context.db.get("SELECT recovery_id, project_id, run_id, generation, status, interruption_json, settlement_json, capture_path, created_at, updated_at, sealed_at, resumed_at FROM run_recovery_guards WHERE project_id = ? AND run_id = ? ORDER BY generation DESC LIMIT 1", [projectId, runId]) ?? null; }
|
|
5
35
|
export function assertRunRecoveryAllowed(context, projectId, runId, expectedGeneration) {
|
|
6
36
|
const guard = recoveryGuard(context, projectId, runId);
|