@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.155
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 +23 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +273 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +114 -16
- package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +45 -8
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +46 -29
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +9 -4
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +214 -24
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -7
- package/dist/services/retained-native-hook.js +245 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +12 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +209 -43
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +8 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +252 -66
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +3 -2
- package/tools/verification-jobs.mjs +35 -10
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { assertLifecycleOutcomes, currentLifecycleFailure, managedInvocationContext, managedLifecycleCommand, awaitLifecycleInvocation, assertLifecycleInvocationCurrent } from "./lifecycle-invocations.js";
|
|
2
|
-
import {
|
|
1
|
+
import { assertLifecycleOutcomes, currentLifecycleFailure, managedInvocationContext, managedLifecycleCommand, awaitLifecycleInvocation, assertLifecycleInvocationCurrent, bindControllerStageAssignment } from "./lifecycle-invocations.js";
|
|
2
|
+
import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
|
|
3
|
+
import { isDeliveryFlow } from "../domain/delivery-flows.js";
|
|
3
4
|
export function recoveryPacketMatchesStage(packet, scope) {
|
|
4
5
|
return packet.schema_id === 'dd-flow/root-recovery-packet@1' && packet.continuation === 'unfinished_work'
|
|
5
6
|
&& packet.run_id === scope.runId && packet.generation === scope.generation && packet.stage === scope.stage
|
|
@@ -28,10 +29,10 @@ import { sameSessionProfile, runtimeHarness } from "./execution-policy.js";
|
|
|
28
29
|
import { selectControllerExecution } from "./controller-execution.js";
|
|
29
30
|
import { resolveHarnessCommand, harnessRuntimeArguments } from "./harness-config.js";
|
|
30
31
|
import { adapterSessionId, prepareHarnessFlowExecutable, runHarnessAdapter } from "./harness-adapter.js";
|
|
31
|
-
import { assertManagedProcessLease, confirmManagedProcess, prepareManagedProcessConfirmation, renewManagedProcess, finishManagedProcess,
|
|
32
|
+
import { assertManagedProcessLease, confirmManagedProcess, prepareManagedProcessConfirmation, renewManagedProcess, finishManagedProcess, processTreeIsAlive, resourceHome } from "./managed-processes.js";
|
|
32
33
|
import { managedLeaseMonitor } from "./managed-lease-monitor.js";
|
|
33
34
|
import { physicalStartup } from "./physical-startup.js";
|
|
34
|
-
import { readNativeDaemonState, requireManagedDaemonBinding } from "./managed-daemon-binding.js";
|
|
35
|
+
import { readNativeDaemonState, requireManagedDaemonBinding, managedDaemonExecutionAbsent, managedExecutionReusable } from "./managed-daemon-binding.js";
|
|
35
36
|
import { initCodexHome } from "./hooks.js";
|
|
36
37
|
import { launchExternalWork } from "./external-work-launch.js";
|
|
37
38
|
import { runControllerMemoryTask } from "./controller-memory-task.js";
|
|
@@ -49,11 +50,14 @@ import { capacityDelay, continueProviderCapacity, eligibleProviderChildOverload,
|
|
|
49
50
|
import { resumeControllerSession, failControllerRecovery } from "./run-controller-recovery.js";
|
|
50
51
|
import { captureControllerBoundary } from "./run-controller-capture.js";
|
|
51
52
|
import { requestRunControl } from "./run-control-worker.js";
|
|
53
|
+
import { recordRunControlIntent } from "./run-control.js";
|
|
52
54
|
import { storageSessionId } from "./session-identity.js";
|
|
53
55
|
import { RECOVERY_POLICY as CAPACITY_POLICY, nativeItemsSettled, recoveryExhausted as overloadExhausted, recoveryRefusal as refusalObservation, terminalProviderRecovery as terminalProviderOverload, normalizeRecoveryFailure } from "../harness-runtime/lib/provider-overload-policy.mjs";
|
|
54
56
|
import { renderNativeChildFollowupInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
55
57
|
import { childCapacityDispatchAllowed, childCapacityRecoveryDecision, closeSuccessfulChildCapacity, reconcileChildCapacity } from "./controller-child-capacity.js";
|
|
56
58
|
import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
|
|
59
|
+
import { recordTechnicalNote } from "./technical-notes.js";
|
|
60
|
+
import { acceptedChildWork, acceptedChildrenRetired, controllerObligation, controllerObligationAccepted, lateNativeFailure } from "./controller-result-authority.js";
|
|
57
61
|
export { resumeRunController } from "./run-controller-recovery.js";
|
|
58
62
|
const preparedLaunchInputs = new WeakMap();
|
|
59
63
|
function launchInputDigest(input) {
|
|
@@ -113,7 +117,7 @@ export function prepareRunControllerLaunch(context, input) {
|
|
|
113
117
|
}
|
|
114
118
|
/** Retain the existing launch intent and frozen inputs without starting a
|
|
115
119
|
* process. Dispatch remains exclusively owned by launchRunController. */
|
|
116
|
-
export function retainRunControllerLaunch(context, input, prepared = prepareRunControllerLaunch(context, input)) {
|
|
120
|
+
export async function retainRunControllerLaunch(context, input, prepared = prepareRunControllerLaunch(context, input)) {
|
|
117
121
|
const selection = preparedLaunchInputs.get(prepared);
|
|
118
122
|
if (!selection || selection.home !== context.ddFlowHome || selection.sha256 !== launchInputDigest(input)) {
|
|
119
123
|
throw new AppError("runtime_request_conflict", "Controller launch differs from its prepared request", 1, { effect: "no_effect" });
|
|
@@ -126,7 +130,14 @@ export function retainRunControllerLaunch(context, input, prepared = prepareRunC
|
|
|
126
130
|
return { controller: current, reused: true };
|
|
127
131
|
}
|
|
128
132
|
const { project, run, request, requestHash, generation, budget, routing, contexts, adapters } = prepared;
|
|
129
|
-
|
|
133
|
+
const assertCurrent = () => {
|
|
134
|
+
if (selection.sha256 !== launchInputDigest(input))
|
|
135
|
+
throw new AppError("runtime_request_conflict", "Controller launch changed during physical observation", 1, { effect: "no_effect" });
|
|
136
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
137
|
+
if (requireRun(context, project.id, run.id).status !== "running")
|
|
138
|
+
throw new AppError("invalid_run_state", "Managed launch requires a running logical RUN", 1);
|
|
139
|
+
};
|
|
140
|
+
const launcher = await physicalStartup(context.databaseDeadlineAt).birth(assertCurrent);
|
|
130
141
|
ensureRunControllerStorage(context.db);
|
|
131
142
|
const controllerId = `DRV-${crypto.randomUUID()}`;
|
|
132
143
|
const root = path.join(run.run_root, "controllers", controllerId);
|
|
@@ -145,8 +156,8 @@ export function retainRunControllerLaunch(context, input, prepared = prepareRunC
|
|
|
145
156
|
const token = crypto.randomUUID();
|
|
146
157
|
const now = context.now();
|
|
147
158
|
const state = { stage: null, sessions: [], current_session: null, turns: 0, last_continuation: null };
|
|
148
|
-
const launcher = { pid: process.pid, pid_started_at: processStartedAt(process.pid) };
|
|
149
159
|
const duplicate = context.db.writeTransaction(() => {
|
|
160
|
+
assertCurrent();
|
|
150
161
|
if (routing.bundle)
|
|
151
162
|
assertPreparedRunBundleSqlAuthority(context, { projectId: project.id, runId: run.id }, routing.bundle);
|
|
152
163
|
const duplicate = context.db.get("SELECT * FROM run_controllers WHERE project_id = ? AND run_id = ? AND request_id = ?", [project.id, run.id, input.requestId]);
|
|
@@ -171,7 +182,7 @@ export function retainRunControllerLaunch(context, input, prepared = prepareRunC
|
|
|
171
182
|
return { controller: duplicate ?? requireController(context, controllerId), reused: duplicate !== null };
|
|
172
183
|
}
|
|
173
184
|
export async function launchRunController(context, input, prepared = prepareRunControllerLaunch(context, input)) {
|
|
174
|
-
const retained = retainRunControllerLaunch(context, input, prepared);
|
|
185
|
+
const retained = await retainRunControllerLaunch(context, input, prepared);
|
|
175
186
|
// A simultaneous publisher already owns dispatch. Preserve the original
|
|
176
187
|
// race behavior: only an explicit replay may reconcile that existing owner.
|
|
177
188
|
if (prepared.kind === "create" && retained.reused)
|
|
@@ -216,7 +227,7 @@ export function runControllerStatus(context, input) {
|
|
|
216
227
|
if (!row)
|
|
217
228
|
return { ok: true, run_id: run.id, controller: null, events: [], next_cursor: input.after ?? 0 };
|
|
218
229
|
const events = context.db.all("SELECT sequence, event_type, data_json, created_at FROM run_controller_events WHERE controller_id = ? AND sequence > ? ORDER BY sequence LIMIT 100", [row.controller_id, input.after ?? 0]);
|
|
219
|
-
return { ...controllerReceipt(row), events: events.map(event => ({ sequence: event.sequence, type: event.event_type, data: JSON.parse(event.data_json), at: event.created_at })), next_cursor: events.at(-1)?.sequence ?? input.after ?? 0 };
|
|
230
|
+
return { ...controllerReceipt(row, context), events: events.map(event => ({ sequence: event.sequence, type: event.event_type, data: JSON.parse(event.data_json), at: event.created_at })), next_cursor: events.at(-1)?.sequence ?? input.after ?? 0 };
|
|
220
231
|
}
|
|
221
232
|
export async function serveRunController(context, input) {
|
|
222
233
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -305,7 +316,7 @@ export async function serveRunController(context, input) {
|
|
|
305
316
|
await resumeControllerSession(context, row, manifest, currentState, assertPhysicalOwner);
|
|
306
317
|
await executeController(context, row, manifest, currentState, assertOwner, assertPhysicalOwner, async () => { await lease.ensureConfirmed(assertLeaseFence); if (currentState.merge_claim)
|
|
307
318
|
await mergeLease.ensureConfirmed(assertLeaseFence); });
|
|
308
|
-
return controllerReceipt(requireController(context, row.controller_id));
|
|
319
|
+
return controllerReceipt(requireController(context, row.controller_id), context);
|
|
309
320
|
}
|
|
310
321
|
catch (error) {
|
|
311
322
|
primaryFailure = error;
|
|
@@ -372,8 +383,7 @@ export async function serveRunController(context, input) {
|
|
|
372
383
|
finishManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token, state: "stopped", reason: "controller_owner_exited" });
|
|
373
384
|
}
|
|
374
385
|
catch (cleanup) {
|
|
375
|
-
|
|
376
|
-
throw cleanup;
|
|
386
|
+
recordTechnicalNote(context, row.project_id, row.run_id, { code: "controller_resource_retirement_pending", message: "Controller execution ended; resource-registry retirement remains pending.", phase: "controller_cleanup", generation: row.generation, resource_identity: row.process_id, consequence: "cleanup_pending" });
|
|
377
387
|
retainSecondary("finish_controller_process", cleanup);
|
|
378
388
|
}
|
|
379
389
|
}
|
|
@@ -546,6 +556,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
546
556
|
};
|
|
547
557
|
let lastCapture = null;
|
|
548
558
|
let terminalFinalization = false;
|
|
559
|
+
let technicalPauseRequested = false;
|
|
549
560
|
let fatal;
|
|
550
561
|
let stopping;
|
|
551
562
|
const cancellation = new AbortController();
|
|
@@ -603,6 +614,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
603
614
|
return Promise.resolve();
|
|
604
615
|
if (retainControllerLeaseUncertainty(context, row, error))
|
|
605
616
|
return Promise.resolve();
|
|
617
|
+
if (error instanceof AppError && (error.details.reconciliation_required === true || ["invocation_assignment_unproven", "invocation_outcome_pending", "invocation_receipt_timeout"].includes(error.code)) && state.current_session !== null)
|
|
618
|
+
return pauseDependency(state.sessions[state.current_session]).catch(cause => {
|
|
619
|
+
if (!(cause instanceof AppError && cause.code === "controller_technical_dependency_paused"))
|
|
620
|
+
return stopAfterFailure(new AppError("controller_dependency_control_unavailable", "The dependency pause could not be published safely", 1, { primary: errorRecord(error), control_error: errorRecord(cause) }));
|
|
621
|
+
});
|
|
606
622
|
if (stopping)
|
|
607
623
|
return stopping;
|
|
608
624
|
fatal = error;
|
|
@@ -637,7 +653,49 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
637
653
|
}
|
|
638
654
|
});
|
|
639
655
|
};
|
|
640
|
-
const call = (session, args, productive = true) =>
|
|
656
|
+
const call = async (session, args, productive = true, operationId) => {
|
|
657
|
+
const receipt = await controllerAdapter(executionContext, row, manifest, session, args, productive ? assertOwner : undefined, operationId, productive ? cancellation.signal : undefined, productive ? renewLease : undefined);
|
|
658
|
+
const previous = state.last_receipt;
|
|
659
|
+
// A read refreshes native observations, not the already accepted engine
|
|
660
|
+
// boundary. Never copy native readiness/settlement fields from the old read.
|
|
661
|
+
return args[0] === "session" && args[1] === "inspect" && previous?.accepted_boundary && adapterSessionId(previous) === session.id
|
|
662
|
+
? { ...receipt, accepted_boundary: previous.accepted_boundary } : receipt;
|
|
663
|
+
};
|
|
664
|
+
const note = (session, code, phase, consequence, operationId, noteState) => recordTechnicalNote(context, row.project_id, row.run_id, {
|
|
665
|
+
code, message: "The accepted result is retained independently of native session retirement or observation.", phase,
|
|
666
|
+
generation: row.generation, ...(state.stage ? { stage: state.stage } : {}), harness: runtimeHarness(session.profile),
|
|
667
|
+
resource_identity: session.state_dir, diagnostic_reference: session.state_dir, consequence, ...(operationId ? { source_operation_id: operationId } : {}), ...(noteState ? { state: noteState } : {})
|
|
668
|
+
});
|
|
669
|
+
const failedChildren = (session) => (childObserver(session)?.failed() ?? []).filter(child => {
|
|
670
|
+
if (!acceptedChildWork(context, row, runtimeHarness(session.profile), session.id, child.session_id))
|
|
671
|
+
return true;
|
|
672
|
+
note(session, "accepted_work_native_child_failed", "native_completion", "cleanup_pending", child.terminal_turn_id);
|
|
673
|
+
return false;
|
|
674
|
+
});
|
|
675
|
+
const pauseDependency = async (session, sourceOperationId) => {
|
|
676
|
+
assertOwner();
|
|
677
|
+
note(session, "native_writer_dependency_blocked", "stage_transition", "dependency_blocked");
|
|
678
|
+
const requestId = `technical:${row.controller_id}:${row.generation}:${digest([state.stage, session.state_dir]).slice(0, 20)}`;
|
|
679
|
+
const input = { projectRoot: manifest.request.project_root, runId: row.run_id, requestId, action: "pause" };
|
|
680
|
+
// Publish source binding in the same SQL transaction as the control
|
|
681
|
+
// intent. A detached worker must never observe an unlabelled pause.
|
|
682
|
+
const intent = recordRunControlIntent(executionContext, input, receipt => {
|
|
683
|
+
const current = requireController(context, row.controller_id);
|
|
684
|
+
if (current.owner_token !== row.owner_token || current.generation !== row.generation || current.process_id !== row.process_id)
|
|
685
|
+
throw new AppError("controller_owner_mismatch", "Controller changed before its technical control was recorded", 1);
|
|
686
|
+
const controlId = receipt.control_id;
|
|
687
|
+
if (typeof controlId !== "string")
|
|
688
|
+
return;
|
|
689
|
+
const prior = context.db.get("SELECT 1 FROM run_controller_events WHERE controller_id=? AND event_type='technical_dependency_paused' AND json_extract(data_json,'$.control_id')=? AND COALESCE(json_extract(data_json,'$.source_operation_id'),'')=?", [row.controller_id, controlId, sourceOperationId ?? ""]);
|
|
690
|
+
if (!prior)
|
|
691
|
+
appendEvent(context, row.controller_id, "technical_dependency_paused", { schema_id: "dd-flow/controller-dependency@1", request_id: requestId, control_id: controlId, generation: row.generation, control: receipt, state_dir: session.state_dir, ...(sourceOperationId ? { source_operation_id: sourceOperationId } : {}) });
|
|
692
|
+
});
|
|
693
|
+
if (typeof intent.control_id !== "string")
|
|
694
|
+
throw new AppError("controller_dependency_control_unavailable", "Technical control was not created", 1, { intent });
|
|
695
|
+
technicalPauseRequested = true;
|
|
696
|
+
const control = await requestRunControl(executionContext, { ...input, waitMs: 0 });
|
|
697
|
+
throw new AppError("controller_technical_dependency_paused", "Conflicting native execution requires control reconciliation before continuation", 1, { control, effect: "no_effect" });
|
|
698
|
+
};
|
|
641
699
|
const confirmStopped = async (stateDir, receipt) => {
|
|
642
700
|
const fail = () => new AppError("controller_native_shutdown_unproven", "The managed daemon is not proven clean and closed", 1, { state_dir: stateDir, receipt });
|
|
643
701
|
const binding = requireManagedDaemonBinding(executionContext, { stateDir, projectId: row.project_id, runId: row.run_id });
|
|
@@ -661,25 +719,75 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
661
719
|
saveState(context, row, state);
|
|
662
720
|
appendEvent(context, row.controller_id, "session_stopped", { session_id: session.id || null, state_dir: session.state_dir, receipt });
|
|
663
721
|
};
|
|
722
|
+
const retireSession = async (session) => {
|
|
723
|
+
session.retired = true;
|
|
724
|
+
saveState(context, row, state);
|
|
725
|
+
try {
|
|
726
|
+
await stopSession(session);
|
|
727
|
+
session.cleanup_pending = false;
|
|
728
|
+
note(session, "native_session_cleanup_pending", "native_cleanup", "none", undefined, "resolved");
|
|
729
|
+
return true;
|
|
730
|
+
}
|
|
731
|
+
catch {
|
|
732
|
+
session.cleanup_pending = true;
|
|
733
|
+
saveState(context, row, state);
|
|
734
|
+
note(session, "native_session_cleanup_pending", "native_cleanup", "cleanup_pending");
|
|
735
|
+
// Full cleanup remains truthful. Only physical absence or an already
|
|
736
|
+
// accepted native execution-settlement proof permits conflicting reuse.
|
|
737
|
+
try {
|
|
738
|
+
const binding = requireManagedDaemonBinding(executionContext, { stateDir: session.state_dir, projectId: row.project_id, runId: row.run_id });
|
|
739
|
+
if (managedDaemonExecutionAbsent(executionContext, { stateDir: session.state_dir, projectId: row.project_id, runId: row.run_id }))
|
|
740
|
+
return true;
|
|
741
|
+
return managedExecutionReusable(binding.state, runtimeHarness(session.profile));
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
};
|
|
664
748
|
const settle = async () => {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
749
|
+
await Promise.all(state.sessions.map(retireSession));
|
|
750
|
+
};
|
|
751
|
+
const captureBoundary = async (stage, nextStage) => {
|
|
752
|
+
try {
|
|
753
|
+
await captureControllerBoundary(executionContext, row, manifest, stage, nextStage, assertPhysicalOwner);
|
|
754
|
+
}
|
|
755
|
+
catch (error) {
|
|
756
|
+
const code = String(error?.code ?? "capture_unavailable");
|
|
757
|
+
if (["controller_owner_mismatch", "controller_capture_owner_lost", "run_recovery_guarded", "run_recovery_generation_stale"].includes(code))
|
|
758
|
+
throw error;
|
|
759
|
+
recordTechnicalNote(context, row.project_id, row.run_id, { code: "boundary_capture_unavailable", message: "The Stage result is accepted; its assessment snapshot is unavailable.", phase: "assessment_capture", generation: row.generation, stage: stage.stage, ...(manifest.request.capture_root ? { diagnostic_reference: manifest.request.capture_root } : {}), consequence: "none" });
|
|
760
|
+
context.db.run("UPDATE run_controllers SET status='running',last_error_json=NULL,updated_at=? WHERE controller_id=? AND owner_token=? AND generation=? AND status IN ('capturing','capture_failed')", [context.now(), row.controller_id, row.owner_token, row.generation]);
|
|
761
|
+
// A missing report is not a failed Stage. A real unstable source still
|
|
762
|
+
// fences its dependent successor until the existing control can seal it.
|
|
763
|
+
if (nextStage && ["recovery_observation_budget_exhausted", "controller_boundary_changed", "snapshot_source_changed", "owned_inventory_changed"].includes(code) && state.current_session !== null)
|
|
764
|
+
await pauseDependency(state.sessions[state.current_session]);
|
|
765
|
+
}
|
|
669
766
|
};
|
|
670
767
|
const reconcilePromptPredecessor = async (session) => {
|
|
671
768
|
const prior = state.last_receipt?.settlement;
|
|
769
|
+
const accepted = state.last_receipt?.accepted_boundary;
|
|
672
770
|
if (!prior || typeof prior !== "object" || Array.isArray(prior) || !["pending", "blocked"].includes(String(prior.state)))
|
|
673
771
|
return;
|
|
674
|
-
const receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false)
|
|
772
|
+
const receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false).catch(async (error) => {
|
|
773
|
+
if (accepted && controllerObligationAccepted(context, row, accepted) && lateNativeFailure(error, session.id))
|
|
774
|
+
await pauseDependency(session);
|
|
775
|
+
throw error;
|
|
776
|
+
});
|
|
675
777
|
state.last_receipt = receipt;
|
|
676
778
|
saveState(context, row, state);
|
|
677
779
|
const current = receipt.settlement;
|
|
678
780
|
if (current && typeof current === "object" && !Array.isArray(current) && ["pending", "blocked"].includes(String(current.state))) {
|
|
781
|
+
if (accepted && controllerObligationAccepted(context, row, accepted)) {
|
|
782
|
+
note(session, "accepted_boundary_settlement_pending", "native_observation", "cleanup_pending");
|
|
783
|
+
if (receipt.native_settled === true && receipt.settled === true)
|
|
784
|
+
return;
|
|
785
|
+
await pauseDependency(session);
|
|
786
|
+
}
|
|
679
787
|
throw new AppError("settlement_reconciliation_required", "The managed Session still has an unsettled productive predecessor", 1, { session_id: session.id, settlement: current });
|
|
680
788
|
}
|
|
681
789
|
};
|
|
682
|
-
const prompt = async (session, text, operationId, kind) => {
|
|
790
|
+
const prompt = async (session, text, operationId, kind, boundary = "finish", assignedWorkIds) => {
|
|
683
791
|
const hitl = kind === "hitl";
|
|
684
792
|
const pending = state.pending_capacity;
|
|
685
793
|
if (pending && (pending.session_id !== session.id || pending.generation !== row.generation || pending.kind !== (kind ?? "stage") || operationId && operationId !== pending.root_operation_id))
|
|
@@ -687,6 +795,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
687
795
|
if (!pending)
|
|
688
796
|
await reconcilePromptPredecessor(session);
|
|
689
797
|
const firstOperationId = pending?.root_operation_id ?? operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
|
|
798
|
+
const expectedBoundary = state.stage ? controllerObligation(context, row, state.stage, hitl ? "resume" : boundary, assignedWorkIds) : undefined;
|
|
690
799
|
const hitlText = async () => {
|
|
691
800
|
const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
|
|
692
801
|
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]);
|
|
@@ -732,13 +841,13 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
732
841
|
const dispatch = JSON.parse(retained.input_json);
|
|
733
842
|
if (![value, `${value}\n`].some(text => dispatch.prompt_sha256 === crypto.createHash("sha256").update(text).digest("hex")))
|
|
734
843
|
throw new AppError("capacity_continuation_conflict", "Retained prompt differs from continuation", 1);
|
|
735
|
-
return controllerAdapter(executionContext, row, manifest, session, dispatch.args, assertOwner, id, cancellation.signal, renewLease);
|
|
844
|
+
return controllerAdapter(executionContext, row, manifest, session, dispatch.args, assertOwner, id, cancellation.signal, renewLease, JSON.parse(retained.input_json).obligation);
|
|
736
845
|
}
|
|
737
846
|
const file = path.join(row.state_dir, `turn-${String(state.turns + 1).padStart(5, "0")}.md`);
|
|
738
847
|
fs.writeFileSync(file, value, { flag: "wx", mode: 0o600 });
|
|
739
848
|
state.turns += 1;
|
|
740
849
|
saveState(context, row, state);
|
|
741
|
-
return controllerAdapter(executionContext, row, manifest, session, ["session", "prompt", "--session-id", session.id, "--prompt-file", file, ...sessionArguments(context, manifest, session)], assertOwner, id, cancellation.signal, renewLease);
|
|
850
|
+
return controllerAdapter(executionContext, row, manifest, session, ["session", "prompt", "--session-id", session.id, "--prompt-file", file, ...sessionArguments(context, manifest, session)], assertOwner, id, cancellation.signal, renewLease, expectedBoundary);
|
|
742
851
|
};
|
|
743
852
|
let receipt;
|
|
744
853
|
try {
|
|
@@ -754,8 +863,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
754
863
|
throw new AppError("capacity_continuation_conflict", "Retained capacity root has no failure receipt", 1);
|
|
755
864
|
throw JSON.parse(root.error_json);
|
|
756
865
|
}
|
|
757
|
-
const
|
|
758
|
-
if (
|
|
866
|
+
const failures = failedChildren(session);
|
|
867
|
+
if (failures.length && !(kind === "child" && childCapacityDispatchAllowed(state, failures, session.id, firstOperationId, digestBytes(text))))
|
|
759
868
|
throw new AppError("fanout_dispatch_fenced", "A retained child failure forbids another coordinator dispatch", 1);
|
|
760
869
|
promptPending = true;
|
|
761
870
|
observedPromptOperation = firstOperationId;
|
|
@@ -834,14 +943,20 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
834
943
|
await childObserver(session)?.drain();
|
|
835
944
|
childObserver(session)?.completePostTurn(receipt);
|
|
836
945
|
const observationGap = childObserver(session)?.gap();
|
|
837
|
-
if (observationGap)
|
|
838
|
-
|
|
946
|
+
if (observationGap) {
|
|
947
|
+
if (expectedBoundary && controllerObligationAccepted(context, row, expectedBoundary))
|
|
948
|
+
note(session, "accepted_boundary_observation_gap", "native_observation", "cleanup_pending", firstOperationId);
|
|
949
|
+
else
|
|
950
|
+
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 });
|
|
951
|
+
}
|
|
839
952
|
if (fatal)
|
|
840
953
|
throw fatal;
|
|
841
954
|
}
|
|
842
955
|
catch (error) {
|
|
843
956
|
if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
|
|
844
957
|
throw error;
|
|
958
|
+
if (!fatal && lateNativeFailure(error, session.id))
|
|
959
|
+
await pauseDependency(session, firstOperationId);
|
|
845
960
|
await stopAfterFailure(error);
|
|
846
961
|
if (!capacityUncertain(error) && !(error instanceof AppError && error.code === "controller_lease_unconfirmed"))
|
|
847
962
|
cancellation.abort();
|
|
@@ -867,6 +982,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
867
982
|
monitor.unref();
|
|
868
983
|
try {
|
|
869
984
|
for (;;) {
|
|
985
|
+
if (technicalPauseRequested)
|
|
986
|
+
return;
|
|
870
987
|
// Renewal touches the resource registry; never do it from a callback that
|
|
871
988
|
// may already own the RUN writer.
|
|
872
989
|
await renewLease();
|
|
@@ -901,14 +1018,22 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
901
1018
|
}
|
|
902
1019
|
}
|
|
903
1020
|
const completed = state.stage && view.index.stage_runs.find(stage => stage.stage === state.stage && ["done", "skipped"].includes(stage.status));
|
|
904
|
-
if (state.current_session !== null && (completed || next.stage !== state.stage)
|
|
905
|
-
|
|
1021
|
+
if (state.current_session !== null && (completed || next.stage !== state.stage)) {
|
|
1022
|
+
const session = state.sessions[state.current_session], failures = failedChildren(session);
|
|
1023
|
+
if (failures.length && !completed)
|
|
1024
|
+
throw new AppError("fanout_dispatch_fenced", "A retained child failure forbids advancing an unaccepted Stage", 1);
|
|
1025
|
+
// The Stage gate is authoritative even for a native helper with no Work
|
|
1026
|
+
// association. This does not claim that helper succeeded or stopped:
|
|
1027
|
+
// normal Session retirement/predecessor reconciliation still fences writes.
|
|
1028
|
+
for (const child of failures)
|
|
1029
|
+
note(session, "accepted_stage_native_child_failed", "native_completion", "cleanup_pending", child.terminal_turn_id);
|
|
1030
|
+
}
|
|
906
1031
|
if (completed && manifest.request.stop_after === state.stage) {
|
|
907
1032
|
// A bounded run is final evidence, not an entry fixture for its successor.
|
|
908
1033
|
// Stop owned observers before capture; keep the controller lease/fence.
|
|
909
1034
|
await settle();
|
|
910
1035
|
if (manifest.request.capture_root)
|
|
911
|
-
await
|
|
1036
|
+
await captureBoundary(completed, null);
|
|
912
1037
|
setFlowRunRecoveryPaused(context, row.project_id, row.run_id, true);
|
|
913
1038
|
setStatus("stop_target_reached", { stage: state.stage, attempt: completed.attempt ?? null });
|
|
914
1039
|
return;
|
|
@@ -916,7 +1041,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
916
1041
|
if (completed && manifest.request.capture_root && ["start_stage", "terminal"].includes(next.kind)) {
|
|
917
1042
|
const key = JSON.stringify([completed.stage, completed.attempt, completed.started_at, completed.completed_at]);
|
|
918
1043
|
if (key !== lastCapture) {
|
|
919
|
-
await
|
|
1044
|
+
await captureBoundary(completed, next.kind === "start_stage" ? next.stage : null);
|
|
920
1045
|
lastCapture = key;
|
|
921
1046
|
}
|
|
922
1047
|
}
|
|
@@ -1016,7 +1141,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1016
1141
|
if (routeBinding(requireRun(context, row.project_id, row.run_id)) !== preparedRoute) {
|
|
1017
1142
|
const current = state.current_session === null ? null : state.sessions[state.current_session];
|
|
1018
1143
|
if (current && !current.stopped)
|
|
1019
|
-
await
|
|
1144
|
+
await retireSession(current);
|
|
1020
1145
|
throw new AppError("workspace_route_changed", "RUN workspace changed during native Session preparation", 1, { effect: "no_effect", stage: next.stage });
|
|
1021
1146
|
}
|
|
1022
1147
|
};
|
|
@@ -1026,8 +1151,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1026
1151
|
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 });
|
|
1027
1152
|
}
|
|
1028
1153
|
if (!session || transition.session_mode === "new_session" || !sameSessionProfile(session.profile, transition.coordinator) || path.resolve(session.cwd) !== stageCwd) {
|
|
1029
|
-
if (session)
|
|
1030
|
-
await
|
|
1154
|
+
if (session && !await retireSession(session))
|
|
1155
|
+
await pauseDependency(session);
|
|
1031
1156
|
await renewLease();
|
|
1032
1157
|
assertOwner();
|
|
1033
1158
|
const directory = path.join(row.state_dir, `session-${state.sessions.length + 1}`);
|
|
@@ -1072,9 +1197,10 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1072
1197
|
}
|
|
1073
1198
|
const responseFile = path.join(row.state_dir, `${next.stage}-${next.attempt ?? 1}.stage-start-response.json`);
|
|
1074
1199
|
const command = managedLifecycleCommand(executionContext, `${flowCommand(executionContext)} stage start ${run.id} --stage ${next.stage} --project-root ${JSON.stringify(run.project_root)} --context-file ${JSON.stringify(stageContext.file)} --context-sha256 ${stageContext.sha256} --require-session-binding --response-file ${JSON.stringify(responseFile)} --json --progress-jsonl`);
|
|
1200
|
+
bindControllerStageAssignment(executionContext, command, { controllerId: row.controller_id, sessionId: session.id, stateDir: session.state_dir, stage: next.stage, contextSha256: stageContext.sha256, ...(typeof next.attempt === "number" ? { attempt: next.attempt } : {}) });
|
|
1075
1201
|
await assertRoute();
|
|
1076
1202
|
appendEvent(context, row.controller_id, "stage_entered", { stage: next.stage, session_id: session.id, context_sha256: stageContext.sha256 });
|
|
1077
|
-
await prompt(session, controllerStageEntryPrompt(next.stage, command));
|
|
1203
|
+
await prompt(session, controllerStageEntryPrompt(next.stage, command), undefined, undefined, "entry");
|
|
1078
1204
|
continue;
|
|
1079
1205
|
}
|
|
1080
1206
|
if (state.current_session === null)
|
|
@@ -1109,7 +1235,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1109
1235
|
if (reconcileChildCapacity(state, children, session.id))
|
|
1110
1236
|
saveState(context, row, state);
|
|
1111
1237
|
childObserver(session)?.observeSettledChildren(children);
|
|
1112
|
-
const retainedFailures =
|
|
1238
|
+
const retainedFailures = failedChildren(session);
|
|
1113
1239
|
children = controllerNativeChildren({ descendants: [...children, ...retainedFailures] }, session.id);
|
|
1114
1240
|
const closedChildEpisodes = closeSuccessfulChildCapacity(state, children, session.id);
|
|
1115
1241
|
if (closedChildEpisodes.length)
|
|
@@ -1117,7 +1243,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1117
1243
|
saveState(context, row, state);
|
|
1118
1244
|
appendEvent(context, row.controller_id, "capacity_child_episodes_completed", { child_session_ids: closedChildEpisodes });
|
|
1119
1245
|
});
|
|
1120
|
-
const queuedChild = children.find(child => state.child_capacity?.[child.session_id]?.phase === "native_message_accepted"
|
|
1246
|
+
const queuedChild = children.find(child => !acceptedChildWork(context, row, childHarness, session.id, child.session_id) && state.child_capacity?.[child.session_id]?.phase === "native_message_accepted"
|
|
1121
1247
|
&& (child.status === "failed" && state.child_capacity[child.session_id].turn_id === child.terminal_turn_id
|
|
1122
1248
|
|| child.status === "running" && child.continuation_admission?.proof_kind === "parent_tool_admission"));
|
|
1123
1249
|
if (queuedChild) {
|
|
@@ -1145,12 +1271,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1145
1271
|
saveState(context, row, state);
|
|
1146
1272
|
continue;
|
|
1147
1273
|
}
|
|
1148
|
-
if (children.some(child => child.status === "failed" && renderNativeChildFollowupInstruction(childHarness, child))) {
|
|
1274
|
+
if (children.some(child => child.status === "failed" && !acceptedChildWork(context, row, childHarness, session.id, child.session_id) && renderNativeChildFollowupInstruction(childHarness, child))) {
|
|
1149
1275
|
const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
|
|
1150
1276
|
state.last_receipt = inspected;
|
|
1151
1277
|
saveState(context, row, state);
|
|
1152
1278
|
children = controllerNativeChildren(inspected, session.id).filter(child => !baseline.has(child.session_id));
|
|
1153
|
-
const failed = children.filter(child => child.status === "failed");
|
|
1279
|
+
const failed = children.filter(child => child.status === "failed" && !acceptedChildWork(context, row, childHarness, session.id, child.session_id));
|
|
1154
1280
|
// Freeze each distinct child refusal on its first exact observation,
|
|
1155
1281
|
// even while a healthy sibling still owns an active native Turn.
|
|
1156
1282
|
for (const failedChild of failed.filter(item => eligibleProviderChildOverload(item, session.id))) {
|
|
@@ -1268,7 +1394,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1268
1394
|
continue;
|
|
1269
1395
|
}
|
|
1270
1396
|
}
|
|
1271
|
-
const memoryFlow = ["mb-init", "mb-upgrade"].includes(run.flow_kind) ||
|
|
1397
|
+
const memoryFlow = ["mb-init", "mb-upgrade", "mb-fix", "interactive"].includes(run.flow_kind) || isSourceSnapshotFlow(run.flow_kind) || routing.kind === "descriptor" && isDeliveryFlow(run.flow_kind);
|
|
1272
1398
|
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 };
|
|
1273
1399
|
const fanout = memoryFlow ? await runControllerMemoryTask(executionContext, { operation: "fanout", input: fanoutInput }, cancellation.signal)
|
|
1274
1400
|
: await nextControllerFanout(executionContext, fanoutInput);
|
|
@@ -1302,6 +1428,29 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1302
1428
|
throw new AppError("incomplete_subject_turn", "Managed Turn ended without a completed Stage or declared continuation", 1, { stage: next.stage });
|
|
1303
1429
|
}
|
|
1304
1430
|
if (fanout.kind === "wait") {
|
|
1431
|
+
if (fanout.reason === "accepted_work_native_cleanup") {
|
|
1432
|
+
const pending = children.filter(child => ["running", "unknown"].includes(child.status) && acceptedChildWork(context, row, childHarness, session.id, child.session_id));
|
|
1433
|
+
for (const child of pending) {
|
|
1434
|
+
note(session, "accepted_work_native_child_pending", "native_completion", "cleanup_pending", child.session_id);
|
|
1435
|
+
try {
|
|
1436
|
+
await call(session, ["session", "cancel", "--session-id", child.session_id, ...sessionArguments(context, manifest, session)], false);
|
|
1437
|
+
}
|
|
1438
|
+
catch {
|
|
1439
|
+
await pauseDependency(session);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
const operationId = `${row.controller_id}:child-retirement:${crypto.randomUUID()}`;
|
|
1443
|
+
const observed = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false, operationId).catch(async (error) => {
|
|
1444
|
+
if (lateNativeFailure(error, session.id))
|
|
1445
|
+
await pauseDependency(session);
|
|
1446
|
+
throw error;
|
|
1447
|
+
});
|
|
1448
|
+
state.last_receipt = observed;
|
|
1449
|
+
saveState(context, row, state);
|
|
1450
|
+
if (!acceptedChildrenRetired(pending.map(child => child.session_id), observed, session.id, operationId))
|
|
1451
|
+
await pauseDependency(session);
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1305
1454
|
setStatus(fanout.reason === "qualified_capacity_required" ? "waiting_for_capacity" : "waiting_for_children");
|
|
1306
1455
|
await delay(1000);
|
|
1307
1456
|
state.last_receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
|
|
@@ -1310,6 +1459,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1310
1459
|
}
|
|
1311
1460
|
setStatus("running");
|
|
1312
1461
|
if (fanout.kind === "external") {
|
|
1462
|
+
let cleanupConflict = false;
|
|
1313
1463
|
const results = await Promise.allSettled(fanout.works.map(async (work) => {
|
|
1314
1464
|
try {
|
|
1315
1465
|
await renewLease();
|
|
@@ -1329,18 +1479,32 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1329
1479
|
throw new AppError("external_work_unsettled", "External launch returned without its terminal Work receipt; observe it rather than relaunching", 1, { work_id: work.work_id, status });
|
|
1330
1480
|
const resolved = JSON.parse(launch.profile_json);
|
|
1331
1481
|
const executable = manifest.adapters[runtimeHarness(resolved.worker)].executable;
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1482
|
+
try {
|
|
1483
|
+
const stopped = await runHarnessAdapter({ executable, args: ["daemon", "stop", "--state-dir", launch.state_dir, "--json"], env: controllerEnvironment(context, manifest), operationId: `${row.controller_id}:${work.work_id}:settle` });
|
|
1484
|
+
await confirmStopped(launch.state_dir, stopped);
|
|
1485
|
+
appendEvent(context, row.controller_id, "external_work_settled", { work_id: work.work_id, receipt: stopped });
|
|
1486
|
+
}
|
|
1487
|
+
catch {
|
|
1488
|
+
recordTechnicalNote(context, row.project_id, row.run_id, { code: "external_work_cleanup_pending", message: "The Work outcome is committed; its native resources require retirement.", phase: "external_work_cleanup", generation: row.generation, stage: next.stage, work_id: work.work_id, harness: runtimeHarness(resolved.worker), resource_identity: launch.state_dir, diagnostic_reference: launch.state_dir, source_operation_id: `${row.controller_id}:${work.work_id}:settle`, consequence: "cleanup_pending" });
|
|
1489
|
+
try {
|
|
1490
|
+
cleanupConflict ||= !managedDaemonExecutionAbsent(executionContext, { stateDir: launch.state_dir, projectId: row.project_id, runId: row.run_id });
|
|
1491
|
+
}
|
|
1492
|
+
catch {
|
|
1493
|
+
cleanupConflict = true;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1335
1496
|
}
|
|
1336
1497
|
catch (error) {
|
|
1337
|
-
|
|
1498
|
+
if (!(error instanceof AppError && error.code === "controller_technical_dependency_paused"))
|
|
1499
|
+
await stopAfterFailure(error);
|
|
1338
1500
|
throw error;
|
|
1339
1501
|
}
|
|
1340
1502
|
}));
|
|
1341
1503
|
const rejected = results.find((result) => result.status === "rejected");
|
|
1342
1504
|
if (rejected)
|
|
1343
1505
|
throw fatal ?? rejected.reason;
|
|
1506
|
+
if (cleanupConflict)
|
|
1507
|
+
await pauseDependency(session);
|
|
1344
1508
|
state.last_continuation = null;
|
|
1345
1509
|
saveState(context, row, state);
|
|
1346
1510
|
continue;
|
|
@@ -1349,10 +1513,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1349
1513
|
assertNoCorrelatedRejection(context, { projectId: row.project_id, runId: row.run_id, generation: row.generation, stage: next.stage, turn: state.turns });
|
|
1350
1514
|
retainControllerContinuation(state, fanout.fingerprint, { code: "fanout_stage_nonprogressing", message: "The coordinator repeated an unchanged Work-graph continuation", details: { stage: next.stage, action: fanout.action, before: state.last_continuation, after: fanout.fingerprint, controller_turn: state.turns } });
|
|
1351
1515
|
saveState(context, row, state);
|
|
1352
|
-
await prompt(session, fanout.prompt);
|
|
1516
|
+
await prompt(session, fanout.prompt, undefined, undefined, fanout.action === "materialize_graph" ? "graph" : fanout.action === "dispatch_work" ? "work" : "finish", fanout.work_ids);
|
|
1353
1517
|
}
|
|
1354
1518
|
}
|
|
1355
1519
|
catch (error) {
|
|
1520
|
+
if (technicalPauseRequested || error instanceof AppError && error.code === "controller_technical_dependency_paused")
|
|
1521
|
+
return;
|
|
1356
1522
|
if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
|
|
1357
1523
|
throw error;
|
|
1358
1524
|
let cause = fatal ?? error;
|
|
@@ -20,6 +20,8 @@ import { findRecentMatchingHookEvent, hookSessionIdentity, recoveryAcceptMatchKe
|
|
|
20
20
|
import { flowCommand } from "./stage-pause.js";
|
|
21
21
|
import { materializePendingWorkResults } from "./work-registry.js";
|
|
22
22
|
import { currentStagePrompt } from "./stage-context.js";
|
|
23
|
+
import { stageGraphScope } from "./stage-work-graph.js";
|
|
24
|
+
import { requireManagedDaemonBinding } from "./managed-daemon-binding.js";
|
|
23
25
|
/** Read-only custody permission for one prepared recovery ACK, not lifecycle admission. */
|
|
24
26
|
export class PreparedRecoveryCoordinatorRead {
|
|
25
27
|
#scope;
|
|
@@ -357,6 +359,20 @@ export function acceptRunRecovery(context, input) {
|
|
|
357
359
|
throw new AppError("recovery_packet_missing", "Memory recovery lacks retained Stage delivery and frozen coordinator scope", 1);
|
|
358
360
|
const recoveryRead = coordinatorScope ? PreparedRecoveryCoordinatorRead.prepare(context, coordinatorScope, input.recoveryId, eventKey) : undefined;
|
|
359
361
|
const preparedCustody = coordinatorScope && bundle ? readDescriptorCoordinatorSession(context, { scope: coordinatorScope, bundle, workId: preparedBinding.work_id, recoveryRead: recoveryRead }) : null;
|
|
362
|
+
// Recovered auxiliary permits inherit the exact replacement controller route,
|
|
363
|
+
// not the old packet's controller token or a fabricated native tool receipt.
|
|
364
|
+
const controllers = coordinatorScope && context.db.get("SELECT 1 FROM sqlite_master WHERE name='run_controllers'") ? context.db.all("SELECT controller_id,owner_token,generation,state_json FROM run_controllers WHERE project_id=? AND run_id=? AND status NOT IN ('completed','cancelled','superseded')", [project.id, run.id]) : [];
|
|
365
|
+
const controller = controllers.length === 1 ? controllers[0] : undefined;
|
|
366
|
+
const controllerState = controller && JSON.parse(controller.state_json);
|
|
367
|
+
const currentSession = controllerState && controllerState.current_session !== null ? controllerState.sessions[controllerState.current_session] : undefined;
|
|
368
|
+
let recoveredOwner = null;
|
|
369
|
+
if (controller && currentSession && !currentSession.retired && !currentSession.stopped) {
|
|
370
|
+
const native = requireManagedDaemonBinding(context, { stateDir: currentSession.state_dir, projectId: project.id, runId: run.id, ownerId: controller.controller_id }).state;
|
|
371
|
+
const mapped = native.sessions?.some(item => typeof item === "string" ? item === identity.providerSessionId && item === currentSession.id
|
|
372
|
+
: item.adapter_session_id === currentSession.id && item.provider_session_id === identity.providerSessionId && (item.root_provider_session_id ?? item.provider_session_id) === identity.providerSessionId);
|
|
373
|
+
if (mapped && native.daemon_id === identity.daemonId)
|
|
374
|
+
recoveredOwner = { controller_id: controller.controller_id, controller_owner: { token: controller.owner_token, session_id: currentSession.id } };
|
|
375
|
+
}
|
|
360
376
|
const result = context.db.writeTransaction(() => {
|
|
361
377
|
assertRecoveryRunCanResume(context, project.id, run.id);
|
|
362
378
|
const guard = recoveryGuard(context, project.id, run.id);
|
|
@@ -364,6 +380,8 @@ export function acceptRunRecovery(context, input) {
|
|
|
364
380
|
const hook = context.db.get("SELECT status, match_key FROM hook_events WHERE id = ?", [identity.hookEventId]);
|
|
365
381
|
if (!guard || guard.recovery_id !== input.recoveryId || !binding || binding.session_id !== identity.sessionId || binding.harness !== identity.harness || binding.provider_session_id !== identity.providerSessionId || binding.daemon_id !== identity.daemonId || !hook?.match_key || !matchKey.includes(hook.match_key))
|
|
366
382
|
throw new AppError("recovery_binding_mismatch", "Recovery acknowledgement does not belong to the authorized physical Session and generation", 1);
|
|
383
|
+
if (recoveredOwner && !context.db.get("SELECT controller_id FROM run_controllers WHERE controller_id=? AND project_id=? AND run_id=? AND owner_token=? AND generation=? AND state_json=?", [recoveredOwner.controller_id, project.id, run.id, recoveredOwner.controller_owner.token, guard.generation, controller.state_json]))
|
|
384
|
+
throw new AppError("recovery_owner_changed", "Replacement controller changed before recovery acceptance", 1);
|
|
367
385
|
const packetReceipt = recoveryPacketReceipt(binding);
|
|
368
386
|
const retained = packetReceipt.packet;
|
|
369
387
|
const authorityPrompt = retained.lifecycle_delivery?.text ?? retained.original_prompt.text;
|
|
@@ -387,7 +405,7 @@ export function acceptRunRecovery(context, input) {
|
|
|
387
405
|
throw new AppError("trusted_recovery_acceptance_required", "Recovery requires a fresh native acknowledgement before work can continue", 1);
|
|
388
406
|
const packet = JSON.parse(binding.packet_json);
|
|
389
407
|
const boundary = packet.continuation === "stage_boundary";
|
|
390
|
-
const prior = context.db.get("SELECT ws.prompt_path, ws.result_path FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE ws.id = ? AND ws.work_id = ? AND ws.session_id = ? AND ws.status = ? AND w.status = ?", [binding.prior_work_session_id, binding.work_id, binding.session_id, packet.retained_session_status ?? "running", packet.retained_work_status ?? (JSON.parse(binding.packet_json).paused ? "paused" : "running")]);
|
|
408
|
+
const prior = context.db.get("SELECT ws.prompt_path, ws.result_path,ws.start_receipt_json,ws.start_publication_status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE ws.id = ? AND ws.work_id = ? AND ws.session_id = ? AND ws.status = ? AND w.status = ?", [binding.prior_work_session_id, binding.work_id, binding.session_id, packet.retained_session_status ?? "running", packet.retained_work_status ?? (JSON.parse(binding.packet_json).paused ? "paused" : "running")]);
|
|
391
409
|
if (!prior)
|
|
392
410
|
throw new AppError("recovery_owner_changed", "Retained root WorkSession changed before recovery acceptance", 1);
|
|
393
411
|
let physicalBinding = null;
|
|
@@ -407,7 +425,18 @@ export function acceptRunRecovery(context, input) {
|
|
|
407
425
|
if (!boundary) {
|
|
408
426
|
const recoveredWorkSessionId = `WS-${crypto.randomUUID()}`;
|
|
409
427
|
context.db.run("UPDATE work_sessions SET status = 'interrupted', completed_at = ?, updated_at = ? WHERE id = ?", [now, now, binding.prior_work_session_id]);
|
|
410
|
-
|
|
428
|
+
const recoveredReceipt = { ...(prior.start_receipt_json ? JSON.parse(prior.start_receipt_json) : {}), generation: guard.generation,
|
|
429
|
+
prompt: authorityPrompt, prompt_sha256: crypto.createHash("sha256").update(authorityPrompt).digest("hex"), recovered_from_work_session_id: binding.prior_work_session_id,
|
|
430
|
+
recovery_id: guard.recovery_id, launch_hook_event_key: eventKey };
|
|
431
|
+
context.db.run("INSERT INTO work_sessions (id, work_id, session_id, hook_event_id, status, prompt_path, result_path,start_receipt_json,start_publication_status, created_at, updated_at) VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?, ?, ?)", [recoveredWorkSessionId, binding.work_id, binding.session_id, eventKey, prior.prompt_path, prior.result_path, JSON.stringify(recoveredReceipt), "published", now, now]);
|
|
432
|
+
if (coordinatorScope)
|
|
433
|
+
appendAudit(context, { projectId: project.id, eventType: "lifecycle.coordinator_packet", payload: {
|
|
434
|
+
run_id: run.id, work_session_id: recoveredWorkSessionId, packet_sha256: recoveredReceipt.prompt_sha256, generation: guard.generation,
|
|
435
|
+
stage: coordinatorScope.stage, stage_scope: stageGraphScope(context, project.id, run.id, coordinatorScope.stage),
|
|
436
|
+
...recoveredOwner, daemon_id: identity.daemonId, root_session_id: identity.providerSessionId,
|
|
437
|
+
auxiliary_operations: ["stage_block", ...(coordinatorScope.stage === "code" ? ["work_repair_add"] : [])],
|
|
438
|
+
recovery_id: guard.recovery_id, prompt: authorityPrompt
|
|
439
|
+
} });
|
|
411
440
|
if (physicalBinding)
|
|
412
441
|
appendAudit(context, { projectId: project.id, eventType: "stage.coordinator_binding", payload: recoveredCoordinatorBinding(physicalBinding, {
|
|
413
442
|
projectId: project.id, runId: run.id, stage: coordinatorScope.stage, workId: binding.work_id,
|