@deksden-com/dd-flow-cli 0.9.0-beta.136 → 0.9.0-beta.138
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 +12 -0
- package/README.md +9 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/argument-values.js +5 -5
- package/dist/cli/command-inputs.js +89 -20
- package/dist/cli/command-metadata.js +56 -3
- package/dist/cli/command-option-descriptions.js +5 -0
- package/dist/cli/executor-onboarding.js +8 -3
- package/dist/cli/flows.js +29 -0
- package/dist/cli/help.js +240 -6
- package/dist/cli/input-preparation.js +54 -5
- package/dist/cli/memory-flow-admission.js +44 -0
- package/dist/cli/memory-init-invocation.js +78 -0
- package/dist/cli/memory-init.js +240 -0
- package/dist/cli/memory-storage-admission.js +53 -0
- package/dist/cli/native-hook-ingress.js +1 -1
- package/dist/cli/response-file.js +2 -0
- package/dist/cli/run-cli.js +283 -160
- package/dist/cli/setup.js +9 -7
- package/dist/cli.js +54 -2
- package/dist/domain/stage-handler-identities.js +20 -0
- package/dist/harness-runtime/DD-ZCODE.md +13 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
- package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +20 -5
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +45 -10
- package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
- package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
- package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
- package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
- package/dist/harness-runtime/lib/opencode-plugin.mjs +5 -1
- package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/discovery-result.schema.json +184 -0
- package/dist/schemas/execution-bundle.schema.json +205 -0
- package/dist/schemas/execution-settings.schema.json +162 -0
- package/dist/schemas/flow-descriptor.schema.json +54 -0
- package/dist/schemas/flow-run.schema.json +22 -0
- package/dist/schemas/harness-catalog.schema.json +80 -0
- package/dist/schemas/mb-init-compose-result.schema.json +457 -0
- package/dist/schemas/mb-init-discover-result.schema.json +500 -0
- package/dist/schemas/mb-init-finish-result.schema.json +422 -0
- package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
- package/dist/schemas/mb-init-review-result.schema.json +396 -0
- package/dist/schemas/mb-init-verify-result.schema.json +391 -0
- package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
- package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
- package/dist/schemas/memory-composition-work-result.schema.json +38 -0
- package/dist/schemas/memory-repair-work-result.schema.json +49 -0
- package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
- package/dist/schemas/memory-review-work-result.schema.json +43 -0
- package/dist/schemas/model-catalog.schema.json +153 -0
- package/dist/schemas/profile-settings.schema.json +122 -0
- package/dist/schemas/release-manifest.schema.json +291 -0
- package/dist/schemas/run-engine-binding.schema.json +1 -1
- package/dist/services/check-runner-evidence.js +51 -4
- package/dist/services/cli-operation-classifier.js +8 -47
- package/dist/services/code-checks.js +387 -34
- package/dist/services/controller-child-observer.js +99 -0
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +79 -13
- package/dist/services/dashboard.js +8 -2
- package/dist/services/descriptor-coordinator-session.js +35 -0
- package/dist/services/descriptor-root-work.js +53 -0
- package/dist/services/descriptor-run-settlement.js +105 -0
- package/dist/services/descriptor-run-start.js +449 -0
- package/dist/services/descriptor-stage-coordinator.js +197 -0
- package/dist/services/descriptor-stage-settlement.js +222 -0
- package/dist/services/descriptor-stage-start.js +139 -0
- package/dist/services/engines.js +39 -22
- package/dist/services/eval-snapshots.js +83 -25
- package/dist/services/execution-bundles.js +263 -0
- package/dist/services/execution-policy.js +20 -16
- package/dist/services/external-work-launch.js +91 -11
- package/dist/services/flow-catalog.js +21 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +242 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +28 -2
- package/dist/services/hooks.js +24 -6
- package/dist/services/hybrid-contracts.js +31 -3
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +7 -4
- package/dist/services/installed-flow-source.js +183 -0
- package/dist/services/lifecycle-invocations.js +257 -78
- package/dist/services/memory-accepted-composition.js +77 -0
- package/dist/services/memory-baseline-preservation.js +65 -0
- package/dist/services/memory-completed-correction-wave.js +320 -0
- package/dist/services/memory-compose-handler.js +258 -0
- package/dist/services/memory-composition-acceptance.js +199 -0
- package/dist/services/memory-composition-graph.js +134 -0
- package/dist/services/memory-composition-registration.js +146 -0
- package/dist/services/memory-composition-work-custody.js +151 -0
- package/dist/services/memory-composition-work-results.js +96 -0
- package/dist/services/memory-corrected-review-acceptance.js +129 -0
- package/dist/services/memory-corrected-review-batch.js +30 -0
- package/dist/services/memory-corrected-review-findings.js +106 -0
- package/dist/services/memory-corrected-review-inputs.js +210 -0
- package/dist/services/memory-corrected-review-projection.js +78 -0
- package/dist/services/memory-corrected-review-receipt.js +170 -0
- package/dist/services/memory-corrected-review-registration.js +191 -0
- package/dist/services/memory-correction-check-receipt.js +219 -0
- package/dist/services/memory-correction-input-owner.js +6 -0
- package/dist/services/memory-discover-handler.js +104 -0
- package/dist/services/memory-finish-evidence.js +435 -0
- package/dist/services/memory-finish-handler.js +215 -0
- package/dist/services/memory-finish-integration.js +280 -0
- package/dist/services/memory-finish-readiness.js +122 -0
- package/dist/services/memory-git-integration.js +151 -0
- package/dist/services/memory-historical-corrected-review.js +206 -0
- package/dist/services/memory-historical-original-review.js +130 -0
- package/dist/services/memory-historical-repair-registration.js +99 -0
- package/dist/services/memory-historical-second-review.js +128 -0
- package/dist/services/memory-init-bootstrap-readiness.js +210 -0
- package/dist/services/memory-init-contexts.js +89 -0
- package/dist/services/memory-init-controller-start.js +84 -0
- package/dist/services/memory-init-engine-release.js +223 -0
- package/dist/services/memory-init-git-policy.js +136 -0
- package/dist/services/memory-init-lint-capability.js +185 -0
- package/dist/services/memory-init-native-capacity.js +241 -0
- package/dist/services/memory-init-preflight.js +312 -0
- package/dist/services/memory-init-project-bootstrap.js +599 -0
- package/dist/services/memory-init-request-candidate.js +157 -0
- package/dist/services/memory-init-request-input.js +66 -0
- package/dist/services/memory-init-resource-storage.js +125 -0
- package/dist/services/memory-init-run-allocation.js +81 -0
- package/dist/services/memory-init-run-bundle.js +98 -0
- package/dist/services/memory-init-start.js +175 -0
- package/dist/services/memory-init-startup-custody.js +80 -0
- package/dist/services/memory-init-startup-input.js +240 -0
- package/dist/services/memory-init-storage.js +81 -0
- package/dist/services/memory-init-stores.js +359 -0
- package/dist/services/memory-init-worktrunk-capability.js +172 -0
- package/dist/services/memory-layer-publication.js +611 -0
- package/dist/services/memory-layer.js +156 -0
- package/dist/services/memory-lint-config.js +85 -0
- package/dist/services/memory-native-review-copy.js +165 -0
- package/dist/services/memory-normal-flow-entry.js +75 -0
- package/dist/services/memory-ordinary-defaults.js +112 -0
- package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
- package/dist/services/memory-preflight-handler.js +259 -0
- package/dist/services/memory-repair-current-view.js +299 -0
- package/dist/services/memory-repair-document-delta.js +451 -0
- package/dist/services/memory-repair-history.js +491 -0
- package/dist/services/memory-repair-physical-binding.js +312 -0
- package/dist/services/memory-repair-publication-files.js +152 -0
- package/dist/services/memory-repair-publication-intent.js +27 -0
- package/dist/services/memory-repair-publication-plan.js +160 -0
- package/dist/services/memory-repair-publication-source.js +225 -0
- package/dist/services/memory-repair-publication.js +794 -0
- package/dist/services/memory-repair-registration.js +239 -0
- package/dist/services/memory-repair-work-baseline.js +413 -0
- package/dist/services/memory-repair-work-completion.js +97 -0
- package/dist/services/memory-repair-work-custody.js +73 -0
- package/dist/services/memory-repair-work-results.js +89 -0
- package/dist/services/memory-repair-workspace.js +252 -0
- package/dist/services/memory-repair-writer-copy.js +735 -0
- package/dist/services/memory-result-binding.js +173 -0
- package/dist/services/memory-retained-review-authority.js +129 -0
- package/dist/services/memory-retained-review-results.js +70 -0
- package/dist/services/memory-retained-second-baseline.js +102 -0
- package/dist/services/memory-retained-second-repairs.js +156 -0
- package/dist/services/memory-review-acceptance.js +214 -0
- package/dist/services/memory-review-correction-plan.js +171 -0
- package/dist/services/memory-review-handler.js +585 -0
- package/dist/services/memory-review-inputs.js +200 -0
- package/dist/services/memory-review-receipt.js +180 -0
- package/dist/services/memory-review-registration.js +108 -0
- package/dist/services/memory-review-work-custody.js +124 -0
- package/dist/services/memory-review-work-input.js +72 -0
- package/dist/services/memory-review-work-results.js +73 -0
- package/dist/services/memory-second-corrected-review-inputs.js +165 -0
- package/dist/services/memory-second-correction-plan.js +76 -0
- package/dist/services/memory-second-repair-baseline.js +91 -0
- package/dist/services/memory-second-repair-launch.js +614 -0
- package/dist/services/memory-second-repair-registration.js +88 -0
- package/dist/services/memory-second-repair-source.js +187 -0
- package/dist/services/memory-second-review-acceptance.js +115 -0
- package/dist/services/memory-source-observations.js +117 -0
- package/dist/services/memory-stage-dispatch.js +28 -0
- package/dist/services/memory-stage-results.js +199 -0
- package/dist/services/memory-verification-acceptance.js +135 -0
- package/dist/services/memory-verification-checks.js +252 -0
- package/dist/services/memory-verification-receipt.js +159 -0
- package/dist/services/memory-verify-handler.js +135 -0
- package/dist/services/memory-work-result-preparation.js +63 -0
- package/dist/services/projects.js +17 -2
- package/dist/services/recovery-snapshot-database.js +416 -1
- package/dist/services/repair-intents.js +1 -1
- package/dist/services/review-copy.js +10 -2
- package/dist/services/run-control.js +80 -9
- package/dist/services/run-controller-capture.js +10 -3
- package/dist/services/run-controller-process.js +21 -0
- package/dist/services/run-controller-recovery.js +3 -1
- package/dist/services/run-controller-state.js +1 -1
- package/dist/services/run-controller.js +184 -48
- package/dist/services/run-recovery-guard.js +2 -0
- package/dist/services/run-recovery.js +145 -5
- package/dist/services/run-start-custody.js +177 -0
- package/dist/services/runs.js +319 -25
- package/dist/services/stage-context.js +9 -1
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-lifecycle.js +2 -1
- package/dist/services/stage-report-renderer.js +76 -0
- package/dist/services/stage-work-graph.js +17 -0
- package/dist/services/vnext-code.js +3 -3
- package/dist/services/vnext-execution-profile.js +2 -2
- package/dist/services/vnext-fanout.js +136 -10
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +6 -4
- package/dist/services/vnext-protocolize.js +60 -2
- package/dist/services/work-registry.js +616 -71
- package/dist/services/workspace-files.js +1 -1
- package/dist/storage/database.js +84 -6
- package/dist/storage/writer-contract.js +11 -7
- package/package.json +13 -3
- package/tools/create-canon-release.mjs +33 -0
- package/tools/produce-hybrid-release.mjs +41 -0
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { assertLifecycleOutcomes, currentLifecycleFailure, managedInvocationContext, managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
|
+
export function recoveryPacketMatchesStage(packet, scope) {
|
|
3
|
+
return packet.schema_id === 'dd-flow/root-recovery-packet@1' && packet.continuation === 'unfinished_work'
|
|
4
|
+
&& packet.run_id === scope.runId && packet.generation === scope.generation && packet.stage === scope.stage
|
|
5
|
+
&& packet.work_id === scope.workId && packet.paused === false;
|
|
6
|
+
}
|
|
2
7
|
import crypto from "node:crypto";
|
|
3
8
|
import fs from "node:fs";
|
|
4
9
|
import path from "node:path";
|
|
@@ -13,10 +18,11 @@ import { prepareFileBytes, prepareJsonText } from "../cli/input-preparation.js";
|
|
|
13
18
|
import { parseRuntimeBudget } from "./runtime-budget.js";
|
|
14
19
|
import { requireProjectByRoot } from "./projects.js";
|
|
15
20
|
import { assertRunMutationAllowed, recoveryGuard } from "./run-recovery.js";
|
|
16
|
-
import { getFlowRunStatus, setFlowRunRecoveryPaused } from "./runs.js";
|
|
21
|
+
import { getFlowRunStatus, setFlowRunRecoveryPaused, assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
17
22
|
import { loadExternalStageContext } from "./stage-context.js";
|
|
18
23
|
import { flowCommand } from "./stage-pause.js";
|
|
19
|
-
import {
|
|
24
|
+
import { sameSessionProfile, runtimeHarness } from "./execution-policy.js";
|
|
25
|
+
import { selectControllerExecution } from "./controller-execution.js";
|
|
20
26
|
import { resolveHarnessCommand, harnessRuntimeArguments } from "./harness-config.js";
|
|
21
27
|
import { adapterSessionId, prepareHarnessFlowExecutable, runHarnessAdapter } from "./harness-adapter.js";
|
|
22
28
|
import { assertManagedProcessLease, confirmManagedProcess, renewManagedProcess, finishManagedProcess, processStartedAt, processTreeIsAlive, resourceHome } from "./managed-processes.js";
|
|
@@ -29,8 +35,9 @@ import { claimQueuedMergeRequest } from "./merge-server.js";
|
|
|
29
35
|
import { prepareVnextCodeWorkspace } from "./vnext-code.js";
|
|
30
36
|
import { requireRun, requireController, controllerReceipt, appendEvent, saveState, retainControllerContinuation, retainControllerCapacityIntent, digest, digestBytes, errorRecord } from "./run-controller-state.js";
|
|
31
37
|
import { assertNoCorrelatedRejection } from "./continuation-outcome.js";
|
|
32
|
-
import { spawnController, recordControllerLauncher, claimControllerProcess, markControllerDispatchReady,
|
|
38
|
+
import { spawnController, recordControllerLauncher, claimControllerProcess, markControllerDispatchReady, assertControllerLeaseAllowed } from "./run-controller-process.js";
|
|
33
39
|
import { controllerAdapter, controllerEnvironment, sessionArguments } from "./run-controller-adapter.js";
|
|
40
|
+
import { retainedChildObserver } from "./controller-child-observer.js";
|
|
34
41
|
import { capacityDelay, continueCodexCapacity, eligibleCodexChildOverload } from "./codex-capacity.js";
|
|
35
42
|
import { resumeControllerSession, failControllerRecovery } from "./run-controller-recovery.js";
|
|
36
43
|
import { captureControllerBoundary } from "./run-controller-capture.js";
|
|
@@ -39,6 +46,11 @@ import { storageSessionId } from "./session-identity.js";
|
|
|
39
46
|
import { CAPACITY_POLICY, nativeItemsSettled, overloadBurst, refusalObservation, terminalCodexOverload } from "../harness-runtime/lib/codex-capacity-policy.mjs";
|
|
40
47
|
import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
|
|
41
48
|
export { resumeRunController } from "./run-controller-recovery.js";
|
|
49
|
+
const preparedLaunchInputs = new WeakMap();
|
|
50
|
+
function launchInputDigest(input) {
|
|
51
|
+
return digest([input.projectRoot, input.runId, input.requestId, input.contextFile, input.contextSha256,
|
|
52
|
+
input.stopAfter ?? null, input.captureRoot ?? null]);
|
|
53
|
+
}
|
|
42
54
|
export function prepareRunControllerLaunch(context, input) {
|
|
43
55
|
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !/^[a-f0-9]{64}$/.test(input.contextSha256))
|
|
44
56
|
throw new AppError("usage", "Managed launch needs a request id and context SHA-256", 2);
|
|
@@ -59,58 +71,79 @@ export function prepareRunControllerLaunch(context, input) {
|
|
|
59
71
|
if (existing) {
|
|
60
72
|
if (existing.request_hash !== requestHash)
|
|
61
73
|
throw new AppError("runtime_request_conflict", "Managed launch request id has different inputs", 1);
|
|
62
|
-
|
|
74
|
+
selectControllerExecution(context, run, JSON.parse(existing.manifest_json));
|
|
75
|
+
const prepared = { kind: "replay", existing };
|
|
76
|
+
preparedLaunchInputs.set(prepared, { home: context.ddFlowHome, sha256: launchInputDigest(input) });
|
|
77
|
+
return prepared;
|
|
63
78
|
}
|
|
64
79
|
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
65
80
|
if (run.status !== "running")
|
|
66
81
|
throw new AppError("invalid_run_state", "Managed launch requires a running logical RUN", 1, { status: run.status });
|
|
67
|
-
const
|
|
68
|
-
const execution = index.execution_profile;
|
|
69
|
-
if (!execution?.settings.execution || !execution.agent_profiles)
|
|
70
|
-
throw new AppError("execution_profile_not_frozen", "Managed launch requires the RUN's frozen execution profiles", 1);
|
|
82
|
+
const routing = selectControllerExecution(context, run);
|
|
71
83
|
const contexts = readContexts(input.contextFile, input.contextSha256);
|
|
84
|
+
if (routing.bundle) {
|
|
85
|
+
for (const stage of [...Object.keys(contexts), ...(input.stopAfter ? [input.stopAfter] : [])])
|
|
86
|
+
routing.bundle.descriptor.stage(stage);
|
|
87
|
+
}
|
|
72
88
|
const next = getFlowRunStatus(context, { projectRoot: project.root, runId: run.id }).continuation;
|
|
73
89
|
if (next.kind !== "start_stage" || !next.stage)
|
|
74
90
|
throw new AppError("controller_attach_required", "Managed launch cannot replace an attached or unfinished coordinator; reconcile its existing owner", 1, { continuation: next });
|
|
75
91
|
if (!contexts[next.stage])
|
|
76
92
|
throw new AppError("controller_context_missing", "Managed context omits the first stage", 2, { stage: next.stage });
|
|
77
93
|
const adapters = {};
|
|
78
|
-
for (const profile of Object.values(
|
|
94
|
+
for (const profile of Object.values(routing.profiles)) {
|
|
79
95
|
const harness = runtimeHarness(profile);
|
|
80
96
|
adapters[harness] = { executable: resolveHarnessCommand(context.ddFlowHome, harness, "adapter"), runtime_args: harnessRuntimeArguments(context.ddFlowHome, harness) };
|
|
81
97
|
}
|
|
82
98
|
const entry = fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
83
99
|
if (!fs.existsSync(entry))
|
|
84
100
|
throw new AppError("harness_runtime_unbuilt", "Build the CLI before launching its detached controller", 1);
|
|
85
|
-
|
|
101
|
+
const prepared = { kind: "create", project, run, request, requestHash, generation, budget, routing, contexts, adapters };
|
|
102
|
+
preparedLaunchInputs.set(prepared, { home: context.ddFlowHome, sha256: launchInputDigest(input) });
|
|
103
|
+
return prepared;
|
|
86
104
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
105
|
+
/** Retain the existing launch intent and frozen inputs without starting a
|
|
106
|
+
* process. Dispatch remains exclusively owned by launchRunController. */
|
|
107
|
+
export function retainRunControllerLaunch(context, input, prepared = prepareRunControllerLaunch(context, input)) {
|
|
108
|
+
const selection = preparedLaunchInputs.get(prepared);
|
|
109
|
+
if (!selection || selection.home !== context.ddFlowHome || selection.sha256 !== launchInputDigest(input)) {
|
|
110
|
+
throw new AppError("runtime_request_conflict", "Controller launch differs from its prepared request", 1, { effect: "no_effect" });
|
|
111
|
+
}
|
|
112
|
+
if (prepared.kind === "replay") {
|
|
113
|
+
const current = requireController(context, prepared.existing.controller_id);
|
|
114
|
+
if (current.request_hash !== prepared.existing.request_hash)
|
|
115
|
+
throw new AppError("runtime_request_conflict", "Controller launch authority changed after replay preparation", 1);
|
|
116
|
+
selectControllerExecution(context, requireRun(context, current.project_id, current.run_id), JSON.parse(current.manifest_json));
|
|
117
|
+
return { controller: current, reused: true };
|
|
118
|
+
}
|
|
119
|
+
const { project, run, request, requestHash, generation, budget, routing, contexts, adapters } = prepared;
|
|
91
120
|
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
92
121
|
ensureRunControllerStorage(context.db);
|
|
93
122
|
const controllerId = `DRV-${crypto.randomUUID()}`;
|
|
94
123
|
const root = path.join(run.run_root, "controllers", controllerId);
|
|
95
124
|
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
96
|
-
const flowExecutable = prepareHarnessFlowExecutable(context, root);
|
|
125
|
+
const flowExecutable = prepareHarnessFlowExecutable(context, root, routing.bundle?.engine());
|
|
97
126
|
const frozenContexts = {};
|
|
98
127
|
for (const [stage, value] of Object.entries(contexts)) {
|
|
99
128
|
const file = path.join(root, `${stage}.context.json`);
|
|
100
129
|
fs.writeFileSync(file, value.bytes, { flag: "wx", mode: 0o600 });
|
|
101
130
|
frozenContexts[stage] = { file, sha256: value.sha256 };
|
|
102
131
|
}
|
|
103
|
-
const manifest = { schema_id: "dd-flow/run-controller-manifest@
|
|
132
|
+
const manifest = { schema_id: routing.kind === "descriptor" ? "dd-flow/run-controller-manifest@2" : "dd-flow/run-controller-manifest@1", request,
|
|
133
|
+
...(routing.kind === "descriptor" ? { execution_binding: routing.binding } : { execution_profile: routing.legacy }),
|
|
134
|
+
contexts: frozenContexts, adapters, flow_executable: flowExecutable, resource_home: resourceHome(context), budget };
|
|
104
135
|
const token = crypto.randomUUID();
|
|
105
136
|
const now = context.now();
|
|
106
137
|
const state = { stage: null, sessions: [], current_session: null, turns: 0, last_continuation: null };
|
|
107
138
|
const launcher = { pid: process.pid, pid_started_at: processStartedAt(process.pid) };
|
|
108
139
|
const duplicate = context.db.writeTransaction(() => {
|
|
140
|
+
if (routing.bundle)
|
|
141
|
+
assertPreparedRunBundleSqlAuthority(context, { projectId: project.id, runId: run.id }, routing.bundle);
|
|
109
142
|
const duplicate = context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? AND request_id = ?", [project.id, run.id, input.requestId]);
|
|
110
143
|
if (duplicate) {
|
|
111
144
|
if (duplicate.request_hash !== requestHash)
|
|
112
145
|
throw new AppError("runtime_request_conflict", "Managed launch request id has different inputs", 1);
|
|
113
|
-
return
|
|
146
|
+
return duplicate;
|
|
114
147
|
}
|
|
115
148
|
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
116
149
|
if (requireRun(context, project.id, run.id).status !== "running")
|
|
@@ -125,9 +158,16 @@ export async function launchRunController(context, input, prepared = prepareRunC
|
|
|
125
158
|
appendEvent(context, controllerId, "launch_requested", { request_id: input.requestId, generation, request_hash: requestHash });
|
|
126
159
|
return null;
|
|
127
160
|
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
161
|
+
return { controller: duplicate ?? requireController(context, controllerId), reused: duplicate !== null };
|
|
162
|
+
}
|
|
163
|
+
export async function launchRunController(context, input, prepared = prepareRunControllerLaunch(context, input)) {
|
|
164
|
+
const retained = retainRunControllerLaunch(context, input, prepared);
|
|
165
|
+
// A simultaneous publisher already owns dispatch. Preserve the original
|
|
166
|
+
// race behavior: only an explicit replay may reconcile that existing owner.
|
|
167
|
+
if (prepared.kind === "create" && retained.reused)
|
|
168
|
+
return { ...controllerReceipt(retained.controller), reused: true };
|
|
169
|
+
const receipt = await spawnController(context, retained.controller);
|
|
170
|
+
return retained.reused ? { ...receipt, reused: true } : receipt;
|
|
131
171
|
}
|
|
132
172
|
/**
|
|
133
173
|
* A registry write can be temporarily unavailable while another runtime client
|
|
@@ -178,6 +218,7 @@ export async function serveRunController(context, input) {
|
|
|
178
218
|
throw new AppError("controller_owner_mismatch", "Managed owner has no resource lease", 1);
|
|
179
219
|
const confirmed = confirmManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid, processGroupId: process.pid, ownerPid: process.pid });
|
|
180
220
|
const 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);
|
|
221
|
+
const state = JSON.parse(row.state_json);
|
|
181
222
|
// Resource and RUN commits are distinct effects. A RUN keepalive failure must
|
|
182
223
|
// neither erase a resource receipt nor grant RUN dispatch authority.
|
|
183
224
|
const mergeLease = controllerLeaseMonitor(attemptMs => {
|
|
@@ -193,21 +234,19 @@ export async function serveRunController(context, input) {
|
|
|
193
234
|
return { owned: true, lease_expires_at: expiry };
|
|
194
235
|
});
|
|
195
236
|
const assertLeaseFence = () => {
|
|
196
|
-
|
|
197
|
-
if (current.owner_token !== row.owner_token || current.generation !== row.generation)
|
|
198
|
-
throw new AppError("controller_owner_mismatch", "Lease renewal no longer owns this controller generation", 1);
|
|
199
|
-
assertControllerRecoveryAllowed(context, current);
|
|
237
|
+
assertControllerLeaseAllowed(context, row);
|
|
200
238
|
};
|
|
201
239
|
const heartbeat = setInterval(() => {
|
|
202
240
|
lease.background(assertLeaseFence, () => { });
|
|
203
|
-
|
|
241
|
+
if (state.merge_claim)
|
|
242
|
+
mergeLease.background(assertLeaseFence, () => { });
|
|
204
243
|
}, 30_000);
|
|
205
244
|
heartbeat.unref();
|
|
206
245
|
const manifest = JSON.parse(row.manifest_json);
|
|
207
|
-
const state = JSON.parse(row.state_json);
|
|
208
246
|
const assertPhysicalOwner = () => {
|
|
209
247
|
lease.assertNoRenewal();
|
|
210
|
-
|
|
248
|
+
if (state.merge_claim)
|
|
249
|
+
mergeLease.assertNoRenewal();
|
|
211
250
|
assertManagedProcessLease({ ...context, databaseDeadlineAt: Date.now() + 5_000 }, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid });
|
|
212
251
|
const current = requireController(context, row.controller_id);
|
|
213
252
|
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))
|
|
@@ -220,7 +259,8 @@ export async function serveRunController(context, input) {
|
|
|
220
259
|
};
|
|
221
260
|
try {
|
|
222
261
|
await lease.ensureConfirmed(assertLeaseFence);
|
|
223
|
-
|
|
262
|
+
if (state.merge_claim)
|
|
263
|
+
await mergeLease.ensureConfirmed(assertLeaseFence);
|
|
224
264
|
context.db.writeTransaction(() => {
|
|
225
265
|
markControllerDispatchReady(context, row);
|
|
226
266
|
if (!state.recovery && (recoveryGuard(context, row.project_id, row.run_id)?.generation ?? 0) === row.generation && requireRun(context, row.project_id, row.run_id).status === "paused")
|
|
@@ -229,7 +269,8 @@ export async function serveRunController(context, input) {
|
|
|
229
269
|
appendEvent(context, row.controller_id, "owner_started", { pid: process.pid, generation: row.generation });
|
|
230
270
|
if (state.recovery && !["done", "failed", "cancelled"].includes(requireRun(context, row.project_id, row.run_id).status))
|
|
231
271
|
await resumeControllerSession(context, row, manifest, state, assertPhysicalOwner);
|
|
232
|
-
await executeController(context, row, manifest, state, assertOwner, assertPhysicalOwner, async () => { await lease.ensureConfirmed(assertLeaseFence);
|
|
272
|
+
await executeController(context, row, manifest, state, assertOwner, assertPhysicalOwner, async () => { await lease.ensureConfirmed(assertLeaseFence); if (state.merge_claim)
|
|
273
|
+
await mergeLease.ensureConfirmed(assertLeaseFence); });
|
|
233
274
|
return controllerReceipt(requireController(context, row.controller_id));
|
|
234
275
|
}
|
|
235
276
|
catch (error) {
|
|
@@ -448,8 +489,9 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
448
489
|
}
|
|
449
490
|
}
|
|
450
491
|
};
|
|
451
|
-
const
|
|
452
|
-
const
|
|
492
|
+
const routing = selectControllerExecution(context, requireRun(context, row.project_id, row.run_id), manifest);
|
|
493
|
+
const policy = routing.policy;
|
|
494
|
+
const profiles = routing.profiles;
|
|
453
495
|
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 } };
|
|
454
496
|
const bindInvocationScope = (session) => {
|
|
455
497
|
executionContext = managedInvocationContext(executionContext, { harness: runtimeHarness(session.profile), stateDir: session.state_dir, sessionId: session.id,
|
|
@@ -460,6 +502,52 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
460
502
|
let fatal;
|
|
461
503
|
let stopping;
|
|
462
504
|
const cancellation = new AbortController();
|
|
505
|
+
const observationCancellation = new AbortController();
|
|
506
|
+
const childObservers = new Map();
|
|
507
|
+
let promptPending = false;
|
|
508
|
+
let observedPromptOperation = null;
|
|
509
|
+
const childObserver = (session) => {
|
|
510
|
+
if (runtimeHarness(session.profile) !== "zcode-acp" || !state.stage || !["plan-review", "code", "code-review"].includes(state.stage))
|
|
511
|
+
return undefined;
|
|
512
|
+
if (readNativeDaemonState(session.state_dir).capabilities?.retained_child_observation !== true)
|
|
513
|
+
return undefined;
|
|
514
|
+
const scope = { controller_id: row.controller_id, generation: row.generation, stage: state.stage, root_session_id: session.id };
|
|
515
|
+
const key = JSON.stringify(scope);
|
|
516
|
+
let retained = childObservers.get(key);
|
|
517
|
+
if (!retained) {
|
|
518
|
+
retained = retainedChildObserver({ file: path.join(row.state_dir, "native-child-observations.jsonl"), scope, assertOwner: assertOwnership,
|
|
519
|
+
gap: error => { process.stderr.write(`${JSON.stringify({ event: "native_child_observation_gap", scope, error_code: String(error?.code ?? "observation_unavailable") })}\n`); },
|
|
520
|
+
read: async () => {
|
|
521
|
+
assertOwnership();
|
|
522
|
+
const operation = observedPromptOperation;
|
|
523
|
+
const binding = requireManagedDaemonBinding(executionContext, { stateDir: session.state_dir, projectId: row.project_id, runId: row.run_id });
|
|
524
|
+
let receipt;
|
|
525
|
+
try {
|
|
526
|
+
receipt = await runHarnessAdapter({ executable: manifest.adapters["zcode-acp"].executable,
|
|
527
|
+
args: ["session", "observe-children", "--session-id", session.id, ...sessionArguments(context, manifest, session)],
|
|
528
|
+
env: controllerEnvironment(context, manifest, session), operationId: crypto.randomUUID(), harness: "zcode", timeoutMs: 30_000,
|
|
529
|
+
signal: AbortSignal.any([cancellation.signal, observationCancellation.signal]) });
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
if (!promptPending || observedPromptOperation !== operation)
|
|
533
|
+
return null;
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
if (!promptPending || observedPromptOperation !== operation || receipt.active_operation_id === null)
|
|
537
|
+
return null;
|
|
538
|
+
if (receipt.schema_id !== "dd-zcode/child-observation@1" || receipt.provider_session_id !== session.id || receipt.daemon_id !== binding.state.daemon_id)
|
|
539
|
+
throw new AppError("native_child_observation_conflict", "Retained child observation has foreign daemon/root identity", 1);
|
|
540
|
+
if (receipt.active_operation_id !== operation)
|
|
541
|
+
throw new AppError("native_child_observation_conflict", "Retained observation has a foreign productive operation", 1);
|
|
542
|
+
const children = controllerNativeChildren(receipt, session.id).filter(child => !(state.native_baseline ?? []).includes(child.session_id));
|
|
543
|
+
if (children.some(child => child.status === "failed" && !child.terminal_turn_id))
|
|
544
|
+
throw new AppError("operation_observation_lost", "Retained child terminal identity is unavailable", 1);
|
|
545
|
+
return children;
|
|
546
|
+
} });
|
|
547
|
+
childObservers.set(key, retained);
|
|
548
|
+
}
|
|
549
|
+
return retained;
|
|
550
|
+
};
|
|
463
551
|
const capacityUncertain = (error) => error instanceof AppError && ["capacity_continuation_blocked", "operation_observation_lost"].includes(error.code) && error.details.capacity_uncertainty === true || Boolean(state.pending_capacity) && (String(error?.code) === "capacity_continuation_blocked" || isObservationLoss(error));
|
|
464
552
|
const stopAfterFailure = (error) => {
|
|
465
553
|
// A retained successor may already be running. Fencing/recovery records
|
|
@@ -492,10 +580,15 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
492
580
|
}).finally(() => cancellation.abort());
|
|
493
581
|
return stopping;
|
|
494
582
|
};
|
|
495
|
-
const setStatus = (status) => {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
583
|
+
const setStatus = (status, terminalData) => {
|
|
584
|
+
context.db.writeTransaction(() => {
|
|
585
|
+
const changed = context.db.run("UPDATE run_controllers SET status = ?, updated_at = ? WHERE controller_id = ? AND owner_token = ? AND status <> ?", [status, context.now(), row.controller_id, row.owner_token, status]);
|
|
586
|
+
if (changed.changes) {
|
|
587
|
+
appendEvent(context, row.controller_id, "status_changed", { status, stage: state.stage });
|
|
588
|
+
if (terminalData)
|
|
589
|
+
appendEvent(context, row.controller_id, status, terminalData);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
499
592
|
};
|
|
500
593
|
const call = (session, args, productive = true) => controllerAdapter(executionContext, row, manifest, session, args, productive ? assertOwner : undefined, undefined, productive ? cancellation.signal : undefined, productive ? renewLease : undefined);
|
|
501
594
|
const confirmStopped = async (stateDir, receipt) => {
|
|
@@ -575,7 +668,17 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
575
668
|
throw new AppError("capacity_continuation_conflict", "Retained capacity root has no failure receipt", 1);
|
|
576
669
|
throw JSON.parse(root.error_json);
|
|
577
670
|
}
|
|
578
|
-
|
|
671
|
+
if (childObserver(session)?.failed().length)
|
|
672
|
+
throw new AppError("fanout_dispatch_fenced", "A retained child failure forbids another coordinator dispatch", 1);
|
|
673
|
+
promptPending = true;
|
|
674
|
+
observedPromptOperation = firstOperationId;
|
|
675
|
+
try {
|
|
676
|
+
receipt = await send(text, firstOperationId);
|
|
677
|
+
}
|
|
678
|
+
finally {
|
|
679
|
+
promptPending = false;
|
|
680
|
+
observedPromptOperation = null;
|
|
681
|
+
}
|
|
579
682
|
}
|
|
580
683
|
catch (error) {
|
|
581
684
|
if (!["codex-desktop", "opencode-server"].includes(runtimeHarness(session.profile)))
|
|
@@ -640,6 +743,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
640
743
|
receipt = resumed;
|
|
641
744
|
}
|
|
642
745
|
checkOutcomes();
|
|
746
|
+
await childObserver(session)?.drain();
|
|
747
|
+
childObserver(session)?.completePostTurn(receipt);
|
|
748
|
+
const observationGap = childObserver(session)?.gap();
|
|
749
|
+
if (observationGap)
|
|
750
|
+
throw new AppError("operation_observation_lost", "In-flight child observation remained unavailable at the safe Turn boundary", 1, { observation_error_code: observationGap, provider_session_id: session.id, stage: state.stage });
|
|
643
751
|
if (fatal)
|
|
644
752
|
throw fatal;
|
|
645
753
|
}
|
|
@@ -661,6 +769,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
661
769
|
return;
|
|
662
770
|
try {
|
|
663
771
|
checkOutcomes();
|
|
772
|
+
if (promptPending && state.current_session !== null)
|
|
773
|
+
childObserver(state.sessions[state.current_session])?.tick();
|
|
664
774
|
}
|
|
665
775
|
catch (error) {
|
|
666
776
|
void stopAfterFailure(error);
|
|
@@ -676,7 +786,10 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
676
786
|
if (state.current_session !== null && !state.sessions[state.current_session].stopped)
|
|
677
787
|
bindInvocationScope(state.sessions[state.current_session]);
|
|
678
788
|
const run = requireRun(context, row.project_id, row.run_id);
|
|
679
|
-
|
|
789
|
+
const view = getFlowRunStatus(executionContext, { projectRoot: run.project_root, runId: run.id });
|
|
790
|
+
const next = view.continuation;
|
|
791
|
+
terminalFinalization = next.kind === "terminal";
|
|
792
|
+
if (state.pending_capacity && next.kind !== "terminal" && !["done", "failed", "cancelled"].includes(run.status)) {
|
|
680
793
|
const pending = state.pending_capacity;
|
|
681
794
|
const session = state.sessions.find(item => item.id === pending.session_id && !item.stopped);
|
|
682
795
|
const operation = context.db.get("SELECT input_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [pending.root_operation_id, row.controller_id, row.generation]);
|
|
@@ -688,19 +801,17 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
688
801
|
await prompt(session, fs.readFileSync(file, "utf8"), pending.root_operation_id, pending.kind === "stage" ? undefined : pending.kind);
|
|
689
802
|
continue;
|
|
690
803
|
}
|
|
691
|
-
const view = getFlowRunStatus(executionContext, { projectRoot: run.project_root, runId: run.id });
|
|
692
|
-
const next = view.continuation;
|
|
693
|
-
terminalFinalization = next.kind === "terminal";
|
|
694
804
|
const completed = state.stage && view.index.stage_runs.find(stage => stage.stage === state.stage && ["done", "skipped"].includes(stage.status));
|
|
805
|
+
if (state.current_session !== null && (completed || next.stage !== state.stage) && childObserver(state.sessions[state.current_session])?.failed().length)
|
|
806
|
+
throw new AppError("fanout_dispatch_fenced", "A retained child failure forbids completing or advancing its Stage", 1);
|
|
695
807
|
if (completed && manifest.request.stop_after === state.stage) {
|
|
696
808
|
// A bounded run is final evidence, not an entry fixture for its successor.
|
|
697
809
|
// Stop owned observers before capture; keep the controller lease/fence.
|
|
698
810
|
await settle();
|
|
699
811
|
if (manifest.request.capture_root)
|
|
700
812
|
await captureControllerBoundary(executionContext, row, manifest, completed, null, assertPhysicalOwner);
|
|
701
|
-
setStatus("stop_target_reached");
|
|
702
813
|
setFlowRunRecoveryPaused(context, row.project_id, row.run_id, true);
|
|
703
|
-
|
|
814
|
+
setStatus("stop_target_reached", { stage: state.stage, attempt: completed.attempt ?? null });
|
|
704
815
|
return;
|
|
705
816
|
}
|
|
706
817
|
if (completed && manifest.request.capture_root && ["start_stage", "terminal"].includes(next.kind)) {
|
|
@@ -763,8 +874,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
763
874
|
loadExternalStageContext({ file: stageContext.file, expectedSha256: stageContext.sha256, stage: next.stage });
|
|
764
875
|
const continuation = JSON.stringify(["start_stage", next.stage, next.attempt ?? null]);
|
|
765
876
|
retainControllerContinuation(state, continuation, { code: "stage_entry_nonprogressing", message: "The coordinator returned without starting the assigned Stage", details: { stage: next.stage, attempt: next.attempt ?? null, last_receipt: state.last_receipt ?? null } });
|
|
766
|
-
const transition =
|
|
767
|
-
if (next.stage === "merge" &&
|
|
877
|
+
const transition = routing.transition(next.stage, state.stage ?? undefined);
|
|
878
|
+
if (routing.kind === "sdlc" && next.stage === "merge" && routing.mergeMode === "server" && !state.merge_claim) {
|
|
768
879
|
const request = context.db.get("SELECT merge_request_id, status FROM merge_requests WHERE project_id = ? AND run_id = ? ORDER BY created_at DESC, merge_request_id DESC LIMIT 1", [row.project_id, row.run_id]);
|
|
769
880
|
if (!request || request.status !== "queued")
|
|
770
881
|
throw new AppError("controller_attach_required", "Managed MERGE needs an unclaimed queued request; reconcile an existing owner", 1, { request });
|
|
@@ -777,10 +888,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
777
888
|
state.merge_claim = { request_id: claim.request.merge_request_id, token: claim.token, target_workspace: claim.request.target_workspace, work_id: claim.request.executor_work_id };
|
|
778
889
|
saveState(context, row, state);
|
|
779
890
|
appendEvent(context, row.controller_id, "merge_claimed", { request_id: claim.request.merge_request_id });
|
|
891
|
+
await renewLease();
|
|
780
892
|
}
|
|
781
|
-
const stageCwd =
|
|
893
|
+
const stageCwd = routing.cwd(next.stage, state.merge_claim?.target_workspace);
|
|
782
894
|
let session = state.current_session === null ? null : state.sessions[state.current_session];
|
|
783
|
-
if (session && next.stage === "merge" &&
|
|
895
|
+
if (session && routing.kind === "sdlc" && next.stage === "merge" && routing.mergeMode === "same_session" && path.resolve(session.cwd) !== stageCwd) {
|
|
784
896
|
throw new AppError("merge_session_workspace_conflict", "Same-session MERGE cannot change the native Session working directory", 1, { session_cwd: session.cwd, target_cwd: stageCwd });
|
|
785
897
|
}
|
|
786
898
|
if (!session || transition.session_mode === "new_session" || !sameSessionProfile(session.profile, transition.coordinator) || path.resolve(session.cwd) !== stageCwd) {
|
|
@@ -820,7 +932,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
820
932
|
state.child_capacity = {};
|
|
821
933
|
saveState(context, row, state);
|
|
822
934
|
setStatus("running");
|
|
823
|
-
if (next.stage === "code" || next.stage === "code-review") {
|
|
935
|
+
if (routing.kind === "sdlc" && (next.stage === "code" || next.stage === "code-review")) {
|
|
824
936
|
// A CODE_REVIEW fork has source files but intentionally no installed
|
|
825
937
|
// dependencies. Its repair Work executes checks just like CODE Work.
|
|
826
938
|
const readiness = await prepareVnextCodeWorkspace(executionContext, { projectRoot: run.project_root, runId: run.id, ownerId: row.controller_id, stage: next.stage });
|
|
@@ -839,6 +951,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
839
951
|
const session = state.sessions[state.current_session];
|
|
840
952
|
const baseline = new Set(state.native_baseline ?? []);
|
|
841
953
|
let children = controllerNativeChildren(state.last_receipt ?? {}, session.id).filter(child => !baseline.has(child.session_id));
|
|
954
|
+
const retainedFailures = childObserver(session)?.failed() ?? [];
|
|
955
|
+
children = controllerNativeChildren({ descendants: [...children, ...retainedFailures] }, session.id);
|
|
842
956
|
if (runtimeHarness(session.profile) === "codex-desktop" && children.some(child => child.status === "failed")) {
|
|
843
957
|
const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
|
|
844
958
|
state.last_receipt = inspected;
|
|
@@ -941,9 +1055,28 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
941
1055
|
continue;
|
|
942
1056
|
}
|
|
943
1057
|
}
|
|
944
|
-
const fanout = await nextControllerFanout(executionContext, { projectRoot: run.project_root, runId: run.id, stage: next.stage, harness: runtimeHarness(session.profile), sessionId: session.id, children, policy, profiles });
|
|
1058
|
+
const fanout = await nextControllerFanout(executionContext, { projectRoot: run.project_root, runId: run.id, stage: next.stage, harness: runtimeHarness(session.profile), sessionId: session.id, children, policy, profiles, dispatchFence: retainedFailures.length > 0 });
|
|
945
1059
|
assertOwner();
|
|
946
1060
|
if (fanout.kind === "none") {
|
|
1061
|
+
// An ACK-only recovery Turn may leave the original root Work unfinished.
|
|
1062
|
+
// Continue that retained packet once; never restart its Stage or repeat
|
|
1063
|
+
// an unknown productive dispatch merely because the observer reattached.
|
|
1064
|
+
const recovery = context.db.get(`SELECT o.operation_id,b.packet_json,b.packet_sha256,b.work_id
|
|
1065
|
+
FROM run_controller_operations o JOIN run_recovery_bindings b ON b.recovery_id=json_extract(o.receipt_json,'$.recovery_id')
|
|
1066
|
+
JOIN run_recovery_guards g ON g.recovery_id=b.recovery_id
|
|
1067
|
+
WHERE o.controller_id=? AND o.generation=? AND o.status='completed' AND json_extract(o.input_json,'$.action')='control_resume'
|
|
1068
|
+
AND b.status='accepted' AND g.status='recovered' AND g.project_id=? AND g.run_id=?
|
|
1069
|
+
AND b.provider_session_id=? AND b.daemon_id=json_extract(o.receipt_json,'$.daemon_id') ORDER BY o.created_at DESC LIMIT 1`, [row.controller_id, row.generation, row.project_id, row.run_id, session.id]);
|
|
1070
|
+
if (recovery && digestBytes(recovery.packet_json) === recovery.packet_sha256) {
|
|
1071
|
+
const packet = JSON.parse(recovery.packet_json);
|
|
1072
|
+
const operationId = `${recovery.operation_id}:unfinished-root`;
|
|
1073
|
+
const work = context.db.get('SELECT status FROM works WHERE work_id=? AND project_id=? AND run_id=? AND parent_work_id IS NULL', [recovery.work_id, row.project_id, row.run_id]);
|
|
1074
|
+
if (recoveryPacketMatchesStage(packet, { runId: row.run_id, generation: row.generation, stage: next.stage, workId: recovery.work_id }) && work?.status === 'running'
|
|
1075
|
+
&& !context.db.get('SELECT 1 FROM run_controller_operations WHERE operation_id=?', [operationId])) {
|
|
1076
|
+
await prompt(session, `Continue the original unfinished root Work from this accepted recovery packet. Inspect retained receipts and partial effects first. Do not restart the Stage, repeat observed lifecycle operations, create another Work or start a successor Stage. Preserve the packet write scope and result schema; stop at its original boundary.\n\nAccepted recovery packet (JSON data):\n${recovery.packet_json}`, operationId);
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
947
1080
|
assertNoCorrelatedRejection(context, { projectId: row.project_id, runId: row.run_id, generation: row.generation, stage: next.stage, turn: state.turns });
|
|
948
1081
|
const cause = currentLifecycleFailure(executionContext, { runId: run.id, generation: row.generation, stage: next.stage, turn: state.turns, projectId: row.project_id, projectRoot: run.project_root });
|
|
949
1082
|
if (cause)
|
|
@@ -1013,6 +1146,9 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1013
1146
|
}
|
|
1014
1147
|
finally {
|
|
1015
1148
|
clearInterval(monitor);
|
|
1149
|
+
const drainedObservations = [...childObservers.values()].map(retained => retained.close());
|
|
1150
|
+
observationCancellation.abort();
|
|
1151
|
+
await Promise.allSettled(drainedObservations);
|
|
1016
1152
|
if (stopping)
|
|
1017
1153
|
await stopping;
|
|
1018
1154
|
observer.db.close?.();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AppError } from "../shared/errors.js";
|
|
2
2
|
import { CAPACITY_POLICY, overloadBurst } from "../harness-runtime/lib/codex-capacity-policy.mjs";
|
|
3
|
+
import { assertRunStartupSettled } from "./run-start-custody.js";
|
|
3
4
|
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; }
|
|
4
5
|
export function assertRunRecoveryAllowed(context, projectId, runId, expectedGeneration) {
|
|
5
6
|
const guard = recoveryGuard(context, projectId, runId);
|
|
@@ -11,6 +12,7 @@ export function assertRunRecoveryAllowed(context, projectId, runId, expectedGene
|
|
|
11
12
|
return generation;
|
|
12
13
|
}
|
|
13
14
|
export function assertRunMutationAllowed(context, projectId, runId, expectedGeneration) {
|
|
15
|
+
assertRunStartupSettled(context, projectId, runId);
|
|
14
16
|
const generation = assertRunRecoveryAllowed(context, projectId, runId, expectedGeneration);
|
|
15
17
|
if (context.db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'run_controllers'")
|
|
16
18
|
&& context.db.get("SELECT 1 FROM run_controllers WHERE project_id = ? AND run_id = ? AND status IN ('capturing','capture_failed') LIMIT 1", [projectId, runId]))
|