@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.156
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 +31 -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 +274 -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 +115 -17
- 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 +2 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
- 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-child-repair.js +95 -0
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +19 -9
- 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 +250 -33
- 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 +274 -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 +25 -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 +257 -44
- 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 +255 -69
- 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,16 @@ 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";
|
|
58
|
+
import { completedChildRepair, reserveChildRepair } from "./controller-child-repair.js";
|
|
59
|
+
import { getVnextFanoutStatus } from "./vnext-fanout.js";
|
|
56
60
|
import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
|
|
61
|
+
import { recordTechnicalNote } from "./technical-notes.js";
|
|
62
|
+
import { acceptedChildWork, acceptedChildrenRetired, controllerObligation, controllerObligationAccepted, lateNativeFailure } from "./controller-result-authority.js";
|
|
57
63
|
export { resumeRunController } from "./run-controller-recovery.js";
|
|
58
64
|
const preparedLaunchInputs = new WeakMap();
|
|
59
65
|
function launchInputDigest(input) {
|
|
@@ -113,7 +119,7 @@ export function prepareRunControllerLaunch(context, input) {
|
|
|
113
119
|
}
|
|
114
120
|
/** Retain the existing launch intent and frozen inputs without starting a
|
|
115
121
|
* process. Dispatch remains exclusively owned by launchRunController. */
|
|
116
|
-
export function retainRunControllerLaunch(context, input, prepared = prepareRunControllerLaunch(context, input)) {
|
|
122
|
+
export async function retainRunControllerLaunch(context, input, prepared = prepareRunControllerLaunch(context, input)) {
|
|
117
123
|
const selection = preparedLaunchInputs.get(prepared);
|
|
118
124
|
if (!selection || selection.home !== context.ddFlowHome || selection.sha256 !== launchInputDigest(input)) {
|
|
119
125
|
throw new AppError("runtime_request_conflict", "Controller launch differs from its prepared request", 1, { effect: "no_effect" });
|
|
@@ -126,7 +132,14 @@ export function retainRunControllerLaunch(context, input, prepared = prepareRunC
|
|
|
126
132
|
return { controller: current, reused: true };
|
|
127
133
|
}
|
|
128
134
|
const { project, run, request, requestHash, generation, budget, routing, contexts, adapters } = prepared;
|
|
129
|
-
|
|
135
|
+
const assertCurrent = () => {
|
|
136
|
+
if (selection.sha256 !== launchInputDigest(input))
|
|
137
|
+
throw new AppError("runtime_request_conflict", "Controller launch changed during physical observation", 1, { effect: "no_effect" });
|
|
138
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
139
|
+
if (requireRun(context, project.id, run.id).status !== "running")
|
|
140
|
+
throw new AppError("invalid_run_state", "Managed launch requires a running logical RUN", 1);
|
|
141
|
+
};
|
|
142
|
+
const launcher = await physicalStartup(context.databaseDeadlineAt).birth(assertCurrent);
|
|
130
143
|
ensureRunControllerStorage(context.db);
|
|
131
144
|
const controllerId = `DRV-${crypto.randomUUID()}`;
|
|
132
145
|
const root = path.join(run.run_root, "controllers", controllerId);
|
|
@@ -145,8 +158,8 @@ export function retainRunControllerLaunch(context, input, prepared = prepareRunC
|
|
|
145
158
|
const token = crypto.randomUUID();
|
|
146
159
|
const now = context.now();
|
|
147
160
|
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
161
|
const duplicate = context.db.writeTransaction(() => {
|
|
162
|
+
assertCurrent();
|
|
150
163
|
if (routing.bundle)
|
|
151
164
|
assertPreparedRunBundleSqlAuthority(context, { projectId: project.id, runId: run.id }, routing.bundle);
|
|
152
165
|
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 +184,7 @@ export function retainRunControllerLaunch(context, input, prepared = prepareRunC
|
|
|
171
184
|
return { controller: duplicate ?? requireController(context, controllerId), reused: duplicate !== null };
|
|
172
185
|
}
|
|
173
186
|
export async function launchRunController(context, input, prepared = prepareRunControllerLaunch(context, input)) {
|
|
174
|
-
const retained = retainRunControllerLaunch(context, input, prepared);
|
|
187
|
+
const retained = await retainRunControllerLaunch(context, input, prepared);
|
|
175
188
|
// A simultaneous publisher already owns dispatch. Preserve the original
|
|
176
189
|
// race behavior: only an explicit replay may reconcile that existing owner.
|
|
177
190
|
if (prepared.kind === "create" && retained.reused)
|
|
@@ -216,7 +229,7 @@ export function runControllerStatus(context, input) {
|
|
|
216
229
|
if (!row)
|
|
217
230
|
return { ok: true, run_id: run.id, controller: null, events: [], next_cursor: input.after ?? 0 };
|
|
218
231
|
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 };
|
|
232
|
+
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
233
|
}
|
|
221
234
|
export async function serveRunController(context, input) {
|
|
222
235
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -305,7 +318,7 @@ export async function serveRunController(context, input) {
|
|
|
305
318
|
await resumeControllerSession(context, row, manifest, currentState, assertPhysicalOwner);
|
|
306
319
|
await executeController(context, row, manifest, currentState, assertOwner, assertPhysicalOwner, async () => { await lease.ensureConfirmed(assertLeaseFence); if (currentState.merge_claim)
|
|
307
320
|
await mergeLease.ensureConfirmed(assertLeaseFence); });
|
|
308
|
-
return controllerReceipt(requireController(context, row.controller_id));
|
|
321
|
+
return controllerReceipt(requireController(context, row.controller_id), context);
|
|
309
322
|
}
|
|
310
323
|
catch (error) {
|
|
311
324
|
primaryFailure = error;
|
|
@@ -372,8 +385,7 @@ export async function serveRunController(context, input) {
|
|
|
372
385
|
finishManagedProcess(context, { id: row.process_id, leaseToken: row.process_lease_token, state: "stopped", reason: "controller_owner_exited" });
|
|
373
386
|
}
|
|
374
387
|
catch (cleanup) {
|
|
375
|
-
|
|
376
|
-
throw cleanup;
|
|
388
|
+
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
389
|
retainSecondary("finish_controller_process", cleanup);
|
|
378
390
|
}
|
|
379
391
|
}
|
|
@@ -546,6 +558,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
546
558
|
};
|
|
547
559
|
let lastCapture = null;
|
|
548
560
|
let terminalFinalization = false;
|
|
561
|
+
let technicalPauseRequested = false;
|
|
549
562
|
let fatal;
|
|
550
563
|
let stopping;
|
|
551
564
|
const cancellation = new AbortController();
|
|
@@ -603,6 +616,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
603
616
|
return Promise.resolve();
|
|
604
617
|
if (retainControllerLeaseUncertainty(context, row, error))
|
|
605
618
|
return Promise.resolve();
|
|
619
|
+
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)
|
|
620
|
+
return pauseDependency(state.sessions[state.current_session]).catch(cause => {
|
|
621
|
+
if (!(cause instanceof AppError && cause.code === "controller_technical_dependency_paused"))
|
|
622
|
+
return stopAfterFailure(new AppError("controller_dependency_control_unavailable", "The dependency pause could not be published safely", 1, { primary: errorRecord(error), control_error: errorRecord(cause) }));
|
|
623
|
+
});
|
|
606
624
|
if (stopping)
|
|
607
625
|
return stopping;
|
|
608
626
|
fatal = error;
|
|
@@ -637,7 +655,51 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
637
655
|
}
|
|
638
656
|
});
|
|
639
657
|
};
|
|
640
|
-
const call = (session, args, productive = true) =>
|
|
658
|
+
const call = async (session, args, productive = true, operationId) => {
|
|
659
|
+
const receipt = await controllerAdapter(executionContext, row, manifest, session, args, productive ? assertOwner : undefined, operationId, productive ? cancellation.signal : undefined, productive ? renewLease : undefined);
|
|
660
|
+
const previous = state.last_receipt;
|
|
661
|
+
// A read refreshes native observations, not the already accepted engine
|
|
662
|
+
// boundary. Never copy native readiness/settlement fields from the old read.
|
|
663
|
+
return args[0] === "session" && args[1] === "inspect" && previous?.accepted_boundary && adapterSessionId(previous) === session.id
|
|
664
|
+
? { ...receipt, accepted_boundary: previous.accepted_boundary } : receipt;
|
|
665
|
+
};
|
|
666
|
+
const note = (session, code, phase, consequence, operationId, noteState) => recordTechnicalNote(context, row.project_id, row.run_id, {
|
|
667
|
+
code, message: "The accepted result is retained independently of native session retirement or observation.", phase,
|
|
668
|
+
generation: row.generation, ...(state.stage ? { stage: state.stage } : {}), harness: runtimeHarness(session.profile),
|
|
669
|
+
resource_identity: session.state_dir, diagnostic_reference: session.state_dir, consequence, ...(operationId ? { source_operation_id: operationId } : {}), ...(noteState ? { state: noteState } : {})
|
|
670
|
+
});
|
|
671
|
+
const failedChildren = (session) => (childObserver(session)?.failed() ?? []).filter(child => {
|
|
672
|
+
if (!acceptedChildWork(context, row, runtimeHarness(session.profile), session.id, child.session_id))
|
|
673
|
+
return true;
|
|
674
|
+
note(session, "accepted_work_native_child_failed", "native_completion", "cleanup_pending", child.terminal_turn_id);
|
|
675
|
+
return false;
|
|
676
|
+
});
|
|
677
|
+
const pauseDependency = async (session, sourceOperationId) => {
|
|
678
|
+
// The control intent contains the retained productive failure; it must not
|
|
679
|
+
// require that same failure to disappear before the owner can pause it.
|
|
680
|
+
assertPhysicalOwner();
|
|
681
|
+
note(session, "native_writer_dependency_blocked", "stage_transition", "dependency_blocked");
|
|
682
|
+
const requestId = `technical:${row.controller_id}:${row.generation}:${digest([state.stage, session.state_dir]).slice(0, 20)}`;
|
|
683
|
+
const input = { projectRoot: manifest.request.project_root, runId: row.run_id, requestId, action: "pause" };
|
|
684
|
+
// Publish source binding in the same SQL transaction as the control
|
|
685
|
+
// intent. A detached worker must never observe an unlabelled pause.
|
|
686
|
+
const intent = recordRunControlIntent(executionContext, input, receipt => {
|
|
687
|
+
const current = requireController(context, row.controller_id);
|
|
688
|
+
if (current.owner_token !== row.owner_token || current.generation !== row.generation || current.process_id !== row.process_id)
|
|
689
|
+
throw new AppError("controller_owner_mismatch", "Controller changed before its technical control was recorded", 1);
|
|
690
|
+
const controlId = receipt.control_id;
|
|
691
|
+
if (typeof controlId !== "string")
|
|
692
|
+
return;
|
|
693
|
+
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 ?? ""]);
|
|
694
|
+
if (!prior)
|
|
695
|
+
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 } : {}) });
|
|
696
|
+
});
|
|
697
|
+
if (typeof intent.control_id !== "string")
|
|
698
|
+
throw new AppError("controller_dependency_control_unavailable", "Technical control was not created", 1, { intent });
|
|
699
|
+
technicalPauseRequested = true;
|
|
700
|
+
const control = await requestRunControl(executionContext, { ...input, waitMs: 0 });
|
|
701
|
+
throw new AppError("controller_technical_dependency_paused", "Conflicting native execution requires control reconciliation before continuation", 1, { control, effect: "no_effect" });
|
|
702
|
+
};
|
|
641
703
|
const confirmStopped = async (stateDir, receipt) => {
|
|
642
704
|
const fail = () => new AppError("controller_native_shutdown_unproven", "The managed daemon is not proven clean and closed", 1, { state_dir: stateDir, receipt });
|
|
643
705
|
const binding = requireManagedDaemonBinding(executionContext, { stateDir, projectId: row.project_id, runId: row.run_id });
|
|
@@ -661,25 +723,75 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
661
723
|
saveState(context, row, state);
|
|
662
724
|
appendEvent(context, row.controller_id, "session_stopped", { session_id: session.id || null, state_dir: session.state_dir, receipt });
|
|
663
725
|
};
|
|
726
|
+
const retireSession = async (session) => {
|
|
727
|
+
session.retired = true;
|
|
728
|
+
saveState(context, row, state);
|
|
729
|
+
try {
|
|
730
|
+
await stopSession(session);
|
|
731
|
+
session.cleanup_pending = false;
|
|
732
|
+
note(session, "native_session_cleanup_pending", "native_cleanup", "none", undefined, "resolved");
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
session.cleanup_pending = true;
|
|
737
|
+
saveState(context, row, state);
|
|
738
|
+
note(session, "native_session_cleanup_pending", "native_cleanup", "cleanup_pending");
|
|
739
|
+
// Full cleanup remains truthful. Only physical absence or an already
|
|
740
|
+
// accepted native execution-settlement proof permits conflicting reuse.
|
|
741
|
+
try {
|
|
742
|
+
const binding = requireManagedDaemonBinding(executionContext, { stateDir: session.state_dir, projectId: row.project_id, runId: row.run_id });
|
|
743
|
+
if (managedDaemonExecutionAbsent(executionContext, { stateDir: session.state_dir, projectId: row.project_id, runId: row.run_id }))
|
|
744
|
+
return true;
|
|
745
|
+
return managedExecutionReusable(binding.state, runtimeHarness(session.profile));
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
};
|
|
664
752
|
const settle = async () => {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
753
|
+
await Promise.all(state.sessions.map(retireSession));
|
|
754
|
+
};
|
|
755
|
+
const captureBoundary = async (stage, nextStage) => {
|
|
756
|
+
try {
|
|
757
|
+
await captureControllerBoundary(executionContext, row, manifest, stage, nextStage, assertPhysicalOwner);
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
const code = String(error?.code ?? "capture_unavailable");
|
|
761
|
+
if (["controller_owner_mismatch", "controller_capture_owner_lost", "run_recovery_guarded", "run_recovery_generation_stale"].includes(code))
|
|
762
|
+
throw error;
|
|
763
|
+
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" });
|
|
764
|
+
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]);
|
|
765
|
+
// A missing report is not a failed Stage. A real unstable source still
|
|
766
|
+
// fences its dependent successor until the existing control can seal it.
|
|
767
|
+
if (nextStage && ["recovery_observation_budget_exhausted", "controller_boundary_changed", "snapshot_source_changed", "owned_inventory_changed"].includes(code) && state.current_session !== null)
|
|
768
|
+
await pauseDependency(state.sessions[state.current_session]);
|
|
769
|
+
}
|
|
669
770
|
};
|
|
670
771
|
const reconcilePromptPredecessor = async (session) => {
|
|
671
772
|
const prior = state.last_receipt?.settlement;
|
|
773
|
+
const accepted = state.last_receipt?.accepted_boundary;
|
|
672
774
|
if (!prior || typeof prior !== "object" || Array.isArray(prior) || !["pending", "blocked"].includes(String(prior.state)))
|
|
673
775
|
return;
|
|
674
|
-
const receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false)
|
|
776
|
+
const receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false).catch(async (error) => {
|
|
777
|
+
if (accepted && controllerObligationAccepted(context, row, accepted) && lateNativeFailure(error, session.id))
|
|
778
|
+
await pauseDependency(session);
|
|
779
|
+
throw error;
|
|
780
|
+
});
|
|
675
781
|
state.last_receipt = receipt;
|
|
676
782
|
saveState(context, row, state);
|
|
677
783
|
const current = receipt.settlement;
|
|
678
784
|
if (current && typeof current === "object" && !Array.isArray(current) && ["pending", "blocked"].includes(String(current.state))) {
|
|
785
|
+
if (accepted && controllerObligationAccepted(context, row, accepted)) {
|
|
786
|
+
note(session, "accepted_boundary_settlement_pending", "native_observation", "cleanup_pending");
|
|
787
|
+
if (receipt.native_settled === true && receipt.settled === true)
|
|
788
|
+
return;
|
|
789
|
+
await pauseDependency(session);
|
|
790
|
+
}
|
|
679
791
|
throw new AppError("settlement_reconciliation_required", "The managed Session still has an unsettled productive predecessor", 1, { session_id: session.id, settlement: current });
|
|
680
792
|
}
|
|
681
793
|
};
|
|
682
|
-
const prompt = async (session, text, operationId, kind) => {
|
|
794
|
+
const prompt = async (session, text, operationId, kind, boundary = "finish", assignedWorkIds) => {
|
|
683
795
|
const hitl = kind === "hitl";
|
|
684
796
|
const pending = state.pending_capacity;
|
|
685
797
|
if (pending && (pending.session_id !== session.id || pending.generation !== row.generation || pending.kind !== (kind ?? "stage") || operationId && operationId !== pending.root_operation_id))
|
|
@@ -687,6 +799,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
687
799
|
if (!pending)
|
|
688
800
|
await reconcilePromptPredecessor(session);
|
|
689
801
|
const firstOperationId = pending?.root_operation_id ?? operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
|
|
802
|
+
const expectedBoundary = state.stage ? controllerObligation(context, row, state.stage, hitl ? "resume" : boundary, assignedWorkIds) : undefined;
|
|
690
803
|
const hitlText = async () => {
|
|
691
804
|
const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
|
|
692
805
|
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 +845,13 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
732
845
|
const dispatch = JSON.parse(retained.input_json);
|
|
733
846
|
if (![value, `${value}\n`].some(text => dispatch.prompt_sha256 === crypto.createHash("sha256").update(text).digest("hex")))
|
|
734
847
|
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);
|
|
848
|
+
return controllerAdapter(executionContext, row, manifest, session, dispatch.args, assertOwner, id, cancellation.signal, renewLease, JSON.parse(retained.input_json).obligation);
|
|
736
849
|
}
|
|
737
850
|
const file = path.join(row.state_dir, `turn-${String(state.turns + 1).padStart(5, "0")}.md`);
|
|
738
851
|
fs.writeFileSync(file, value, { flag: "wx", mode: 0o600 });
|
|
739
852
|
state.turns += 1;
|
|
740
853
|
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);
|
|
854
|
+
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
855
|
};
|
|
743
856
|
let receipt;
|
|
744
857
|
try {
|
|
@@ -754,8 +867,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
754
867
|
throw new AppError("capacity_continuation_conflict", "Retained capacity root has no failure receipt", 1);
|
|
755
868
|
throw JSON.parse(root.error_json);
|
|
756
869
|
}
|
|
757
|
-
const
|
|
758
|
-
if (
|
|
870
|
+
const failures = failedChildren(session);
|
|
871
|
+
if (failures.length && !(kind === "child" && childCapacityDispatchAllowed(state, failures, session.id, firstOperationId, digestBytes(text))))
|
|
759
872
|
throw new AppError("fanout_dispatch_fenced", "A retained child failure forbids another coordinator dispatch", 1);
|
|
760
873
|
promptPending = true;
|
|
761
874
|
observedPromptOperation = firstOperationId;
|
|
@@ -834,14 +947,20 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
834
947
|
await childObserver(session)?.drain();
|
|
835
948
|
childObserver(session)?.completePostTurn(receipt);
|
|
836
949
|
const observationGap = childObserver(session)?.gap();
|
|
837
|
-
if (observationGap)
|
|
838
|
-
|
|
950
|
+
if (observationGap) {
|
|
951
|
+
if (expectedBoundary && controllerObligationAccepted(context, row, expectedBoundary))
|
|
952
|
+
note(session, "accepted_boundary_observation_gap", "native_observation", "cleanup_pending", firstOperationId);
|
|
953
|
+
else
|
|
954
|
+
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 });
|
|
955
|
+
}
|
|
839
956
|
if (fatal)
|
|
840
957
|
throw fatal;
|
|
841
958
|
}
|
|
842
959
|
catch (error) {
|
|
843
960
|
if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
|
|
844
961
|
throw error;
|
|
962
|
+
if (!fatal && lateNativeFailure(error, session.id))
|
|
963
|
+
await pauseDependency(session, firstOperationId);
|
|
845
964
|
await stopAfterFailure(error);
|
|
846
965
|
if (!capacityUncertain(error) && !(error instanceof AppError && error.code === "controller_lease_unconfirmed"))
|
|
847
966
|
cancellation.abort();
|
|
@@ -867,6 +986,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
867
986
|
monitor.unref();
|
|
868
987
|
try {
|
|
869
988
|
for (;;) {
|
|
989
|
+
if (technicalPauseRequested)
|
|
990
|
+
return;
|
|
870
991
|
// Renewal touches the resource registry; never do it from a callback that
|
|
871
992
|
// may already own the RUN writer.
|
|
872
993
|
await renewLease();
|
|
@@ -901,14 +1022,22 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
901
1022
|
}
|
|
902
1023
|
}
|
|
903
1024
|
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
|
-
|
|
1025
|
+
if (state.current_session !== null && (completed || next.stage !== state.stage)) {
|
|
1026
|
+
const session = state.sessions[state.current_session], failures = failedChildren(session);
|
|
1027
|
+
if (failures.length && !completed)
|
|
1028
|
+
throw new AppError("fanout_dispatch_fenced", "A retained child failure forbids advancing an unaccepted Stage", 1);
|
|
1029
|
+
// The Stage gate is authoritative even for a native helper with no Work
|
|
1030
|
+
// association. This does not claim that helper succeeded or stopped:
|
|
1031
|
+
// normal Session retirement/predecessor reconciliation still fences writes.
|
|
1032
|
+
for (const child of failures)
|
|
1033
|
+
note(session, "accepted_stage_native_child_failed", "native_completion", "cleanup_pending", child.terminal_turn_id);
|
|
1034
|
+
}
|
|
906
1035
|
if (completed && manifest.request.stop_after === state.stage) {
|
|
907
1036
|
// A bounded run is final evidence, not an entry fixture for its successor.
|
|
908
1037
|
// Stop owned observers before capture; keep the controller lease/fence.
|
|
909
1038
|
await settle();
|
|
910
1039
|
if (manifest.request.capture_root)
|
|
911
|
-
await
|
|
1040
|
+
await captureBoundary(completed, null);
|
|
912
1041
|
setFlowRunRecoveryPaused(context, row.project_id, row.run_id, true);
|
|
913
1042
|
setStatus("stop_target_reached", { stage: state.stage, attempt: completed.attempt ?? null });
|
|
914
1043
|
return;
|
|
@@ -916,7 +1045,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
916
1045
|
if (completed && manifest.request.capture_root && ["start_stage", "terminal"].includes(next.kind)) {
|
|
917
1046
|
const key = JSON.stringify([completed.stage, completed.attempt, completed.started_at, completed.completed_at]);
|
|
918
1047
|
if (key !== lastCapture) {
|
|
919
|
-
await
|
|
1048
|
+
await captureBoundary(completed, next.kind === "start_stage" ? next.stage : null);
|
|
920
1049
|
lastCapture = key;
|
|
921
1050
|
}
|
|
922
1051
|
}
|
|
@@ -1016,7 +1145,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1016
1145
|
if (routeBinding(requireRun(context, row.project_id, row.run_id)) !== preparedRoute) {
|
|
1017
1146
|
const current = state.current_session === null ? null : state.sessions[state.current_session];
|
|
1018
1147
|
if (current && !current.stopped)
|
|
1019
|
-
await
|
|
1148
|
+
await retireSession(current);
|
|
1020
1149
|
throw new AppError("workspace_route_changed", "RUN workspace changed during native Session preparation", 1, { effect: "no_effect", stage: next.stage });
|
|
1021
1150
|
}
|
|
1022
1151
|
};
|
|
@@ -1026,8 +1155,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1026
1155
|
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
1156
|
}
|
|
1028
1157
|
if (!session || transition.session_mode === "new_session" || !sameSessionProfile(session.profile, transition.coordinator) || path.resolve(session.cwd) !== stageCwd) {
|
|
1029
|
-
if (session)
|
|
1030
|
-
await
|
|
1158
|
+
if (session && !await retireSession(session))
|
|
1159
|
+
await pauseDependency(session);
|
|
1031
1160
|
await renewLease();
|
|
1032
1161
|
assertOwner();
|
|
1033
1162
|
const directory = path.join(row.state_dir, `session-${state.sessions.length + 1}`);
|
|
@@ -1072,9 +1201,10 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1072
1201
|
}
|
|
1073
1202
|
const responseFile = path.join(row.state_dir, `${next.stage}-${next.attempt ?? 1}.stage-start-response.json`);
|
|
1074
1203
|
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`);
|
|
1204
|
+
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
1205
|
await assertRoute();
|
|
1076
1206
|
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));
|
|
1207
|
+
await prompt(session, controllerStageEntryPrompt(next.stage, command), undefined, undefined, "entry");
|
|
1078
1208
|
continue;
|
|
1079
1209
|
}
|
|
1080
1210
|
if (state.current_session === null)
|
|
@@ -1109,7 +1239,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1109
1239
|
if (reconcileChildCapacity(state, children, session.id))
|
|
1110
1240
|
saveState(context, row, state);
|
|
1111
1241
|
childObserver(session)?.observeSettledChildren(children);
|
|
1112
|
-
const retainedFailures =
|
|
1242
|
+
const retainedFailures = failedChildren(session);
|
|
1113
1243
|
children = controllerNativeChildren({ descendants: [...children, ...retainedFailures] }, session.id);
|
|
1114
1244
|
const closedChildEpisodes = closeSuccessfulChildCapacity(state, children, session.id);
|
|
1115
1245
|
if (closedChildEpisodes.length)
|
|
@@ -1117,7 +1247,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1117
1247
|
saveState(context, row, state);
|
|
1118
1248
|
appendEvent(context, row.controller_id, "capacity_child_episodes_completed", { child_session_ids: closedChildEpisodes });
|
|
1119
1249
|
});
|
|
1120
|
-
const queuedChild = children.find(child => state.child_capacity?.[child.session_id]?.phase === "native_message_accepted"
|
|
1250
|
+
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
1251
|
&& (child.status === "failed" && state.child_capacity[child.session_id].turn_id === child.terminal_turn_id
|
|
1122
1252
|
|| child.status === "running" && child.continuation_admission?.proof_kind === "parent_tool_admission"));
|
|
1123
1253
|
if (queuedChild) {
|
|
@@ -1145,12 +1275,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1145
1275
|
saveState(context, row, state);
|
|
1146
1276
|
continue;
|
|
1147
1277
|
}
|
|
1148
|
-
if (children.some(child => child.status === "failed" && renderNativeChildFollowupInstruction(childHarness, child))) {
|
|
1278
|
+
if (children.some(child => child.status === "failed" && !acceptedChildWork(context, row, childHarness, session.id, child.session_id) && renderNativeChildFollowupInstruction(childHarness, child))) {
|
|
1149
1279
|
const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
|
|
1150
1280
|
state.last_receipt = inspected;
|
|
1151
1281
|
saveState(context, row, state);
|
|
1152
1282
|
children = controllerNativeChildren(inspected, session.id).filter(child => !baseline.has(child.session_id));
|
|
1153
|
-
const failed = children.filter(child => child.status === "failed");
|
|
1283
|
+
const failed = children.filter(child => child.status === "failed" && !acceptedChildWork(context, row, childHarness, session.id, child.session_id));
|
|
1154
1284
|
// Freeze each distinct child refusal on its first exact observation,
|
|
1155
1285
|
// even while a healthy sibling still owns an active native Turn.
|
|
1156
1286
|
for (const failedChild of failed.filter(item => eligibleProviderChildOverload(item, session.id))) {
|
|
@@ -1268,8 +1398,51 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1268
1398
|
continue;
|
|
1269
1399
|
}
|
|
1270
1400
|
}
|
|
1271
|
-
const
|
|
1272
|
-
|
|
1401
|
+
const repairBlockedWorks = [];
|
|
1402
|
+
if (!retainedFailures.length && children.some(child => child.status === "completed" && !acceptedChildWork(context, row, childHarness, session.id, child.session_id))) {
|
|
1403
|
+
const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
|
|
1404
|
+
state.last_receipt = inspected;
|
|
1405
|
+
saveState(context, row, state);
|
|
1406
|
+
children = controllerNativeChildren(inspected, session.id).filter(child => !baseline.has(child.session_id));
|
|
1407
|
+
// Do not compete with an active parent/healthy sibling or turn a known
|
|
1408
|
+
// repair into a replacement fanout wave.
|
|
1409
|
+
if (inspected.settled !== true || children.some(child => ["running", "unknown"].includes(child.status))) {
|
|
1410
|
+
setStatus("waiting_for_children");
|
|
1411
|
+
await delay(1000, undefined, { signal: cancellation.signal });
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
let repair = null;
|
|
1415
|
+
const blockers = [];
|
|
1416
|
+
for (const child of children) {
|
|
1417
|
+
try {
|
|
1418
|
+
repair ??= completedChildRepair(executionContext, row, { stage: next.stage, harness: childHarness, parentId: session.id, children: [child] });
|
|
1419
|
+
}
|
|
1420
|
+
catch (error) {
|
|
1421
|
+
if (!(error instanceof AppError) || error.code !== "child_work_repair_blocked")
|
|
1422
|
+
throw error;
|
|
1423
|
+
blockers.push(error);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (repair) {
|
|
1427
|
+
reserveChildRepair(executionContext, row, repair, assertOwner);
|
|
1428
|
+
await prompt(session, repair.prompt, repair.operation_id, undefined, "work", [repair.work_id]);
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
if (blockers.length) {
|
|
1432
|
+
const graph = getVnextFanoutStatus(executionContext, { projectRoot: run.project_root, runId: run.id, stage: next.stage }).orchestration;
|
|
1433
|
+
if (!graph?.works.ready.length)
|
|
1434
|
+
throw blockers[0];
|
|
1435
|
+
for (const blocker of blockers) {
|
|
1436
|
+
const workId = String(blocker.details.work_id);
|
|
1437
|
+
repairBlockedWorks.push(workId);
|
|
1438
|
+
recordTechnicalNote(context, row.project_id, row.run_id, { code: "child_work_repair_blocked", message: blocker.message,
|
|
1439
|
+
phase: "work_repair", generation: row.generation, stage: next.stage, work_id: workId, harness: childHarness,
|
|
1440
|
+
resource_identity: workId, consequence: "dependency_blocked", diagnostic_reference: String(blocker.details.reason) });
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
const memoryFlow = ["mb-init", "mb-upgrade", "mb-fix", "interactive"].includes(run.flow_kind) || isSourceSnapshotFlow(run.flow_kind) || routing.kind === "descriptor" && isDeliveryFlow(run.flow_kind);
|
|
1445
|
+
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, repairBlockedWorks };
|
|
1273
1446
|
const fanout = memoryFlow ? await runControllerMemoryTask(executionContext, { operation: "fanout", input: fanoutInput }, cancellation.signal)
|
|
1274
1447
|
: await nextControllerFanout(executionContext, fanoutInput);
|
|
1275
1448
|
if (memoryFlow)
|
|
@@ -1302,6 +1475,29 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1302
1475
|
throw new AppError("incomplete_subject_turn", "Managed Turn ended without a completed Stage or declared continuation", 1, { stage: next.stage });
|
|
1303
1476
|
}
|
|
1304
1477
|
if (fanout.kind === "wait") {
|
|
1478
|
+
if (fanout.reason === "accepted_work_native_cleanup") {
|
|
1479
|
+
const pending = children.filter(child => ["running", "unknown"].includes(child.status) && acceptedChildWork(context, row, childHarness, session.id, child.session_id));
|
|
1480
|
+
for (const child of pending) {
|
|
1481
|
+
note(session, "accepted_work_native_child_pending", "native_completion", "cleanup_pending", child.session_id);
|
|
1482
|
+
try {
|
|
1483
|
+
await call(session, ["session", "cancel", "--session-id", child.session_id, ...sessionArguments(context, manifest, session)], false);
|
|
1484
|
+
}
|
|
1485
|
+
catch {
|
|
1486
|
+
await pauseDependency(session);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
const operationId = `${row.controller_id}:child-retirement:${crypto.randomUUID()}`;
|
|
1490
|
+
const observed = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false, operationId).catch(async (error) => {
|
|
1491
|
+
if (lateNativeFailure(error, session.id))
|
|
1492
|
+
await pauseDependency(session);
|
|
1493
|
+
throw error;
|
|
1494
|
+
});
|
|
1495
|
+
state.last_receipt = observed;
|
|
1496
|
+
saveState(context, row, state);
|
|
1497
|
+
if (!acceptedChildrenRetired(pending.map(child => child.session_id), observed, session.id, operationId))
|
|
1498
|
+
await pauseDependency(session);
|
|
1499
|
+
continue;
|
|
1500
|
+
}
|
|
1305
1501
|
setStatus(fanout.reason === "qualified_capacity_required" ? "waiting_for_capacity" : "waiting_for_children");
|
|
1306
1502
|
await delay(1000);
|
|
1307
1503
|
state.last_receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
|
|
@@ -1310,6 +1506,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1310
1506
|
}
|
|
1311
1507
|
setStatus("running");
|
|
1312
1508
|
if (fanout.kind === "external") {
|
|
1509
|
+
let cleanupConflict = false;
|
|
1313
1510
|
const results = await Promise.allSettled(fanout.works.map(async (work) => {
|
|
1314
1511
|
try {
|
|
1315
1512
|
await renewLease();
|
|
@@ -1329,18 +1526,32 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1329
1526
|
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
1527
|
const resolved = JSON.parse(launch.profile_json);
|
|
1331
1528
|
const executable = manifest.adapters[runtimeHarness(resolved.worker)].executable;
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1529
|
+
try {
|
|
1530
|
+
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` });
|
|
1531
|
+
await confirmStopped(launch.state_dir, stopped);
|
|
1532
|
+
appendEvent(context, row.controller_id, "external_work_settled", { work_id: work.work_id, receipt: stopped });
|
|
1533
|
+
}
|
|
1534
|
+
catch {
|
|
1535
|
+
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" });
|
|
1536
|
+
try {
|
|
1537
|
+
cleanupConflict ||= !managedDaemonExecutionAbsent(executionContext, { stateDir: launch.state_dir, projectId: row.project_id, runId: row.run_id });
|
|
1538
|
+
}
|
|
1539
|
+
catch {
|
|
1540
|
+
cleanupConflict = true;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1335
1543
|
}
|
|
1336
1544
|
catch (error) {
|
|
1337
|
-
|
|
1545
|
+
if (!(error instanceof AppError && error.code === "controller_technical_dependency_paused"))
|
|
1546
|
+
await stopAfterFailure(error);
|
|
1338
1547
|
throw error;
|
|
1339
1548
|
}
|
|
1340
1549
|
}));
|
|
1341
1550
|
const rejected = results.find((result) => result.status === "rejected");
|
|
1342
1551
|
if (rejected)
|
|
1343
1552
|
throw fatal ?? rejected.reason;
|
|
1553
|
+
if (cleanupConflict)
|
|
1554
|
+
await pauseDependency(session);
|
|
1344
1555
|
state.last_continuation = null;
|
|
1345
1556
|
saveState(context, row, state);
|
|
1346
1557
|
continue;
|
|
@@ -1349,10 +1560,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1349
1560
|
assertNoCorrelatedRejection(context, { projectId: row.project_id, runId: row.run_id, generation: row.generation, stage: next.stage, turn: state.turns });
|
|
1350
1561
|
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
1562
|
saveState(context, row, state);
|
|
1352
|
-
await prompt(session, fanout.prompt);
|
|
1563
|
+
await prompt(session, fanout.prompt, undefined, undefined, fanout.action === "materialize_graph" ? "graph" : fanout.action === "dispatch_work" ? "work" : "finish", fanout.work_ids);
|
|
1353
1564
|
}
|
|
1354
1565
|
}
|
|
1355
1566
|
catch (error) {
|
|
1567
|
+
if (technicalPauseRequested || error instanceof AppError && error.code === "controller_technical_dependency_paused")
|
|
1568
|
+
return;
|
|
1356
1569
|
if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
|
|
1357
1570
|
throw error;
|
|
1358
1571
|
let cause = fatal ?? error;
|