@deksden-com/dd-flow-cli 0.9.0-beta.155 → 0.9.0-beta.158
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/error-output.js +11 -1
- package/dist/cli/input-preparation.js +32 -4
- package/dist/cli/run-cli.js +26 -8
- package/dist/harness-runtime/lib/daemon-operations.mjs +46 -11
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +19 -6
- package/dist/harness-runtime/lib/dd-agy.mjs +19 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +2 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +3 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +8 -2
- package/dist/schemas/code-work-batch.schema.json +1 -1
- package/dist/schemas/vnext-protocol-plan.schema.json +1 -1
- package/dist/services/codex-capacity.js +367 -0
- package/dist/services/controller-child-repair.js +117 -0
- package/dist/services/controller-fanout.js +33 -20
- package/dist/services/controller-result-authority.js +13 -8
- package/dist/services/controller-work-failure.js +198 -0
- package/dist/services/lifecycle-invocations.js +117 -25
- package/dist/services/managed-daemon-binding.js +40 -0
- package/dist/services/retained-native-hook.js +34 -5
- package/dist/services/run-control.js +18 -36
- package/dist/services/run-controller-process.js +19 -6
- package/dist/services/run-controller.js +154 -22
- package/dist/services/vnext-code.js +3 -1
- package/dist/services/vnext-plan-review.js +2 -2
- package/dist/services/vnext-plan.js +3 -3
- package/dist/services/work-registry.js +62 -15
- package/dist/services/work-retry-context.js +55 -0
- package/dist/verification-components.json +3 -0
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ import { handleAgyEvent, handleCodexHook, handleDroidEvent, handleOpenCodeEvent,
|
|
|
12
12
|
import { withDaemonProgressObserver } from "../harness-runtime/lib/daemon-observation.mjs";
|
|
13
13
|
const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
14
14
|
const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
|
15
|
+
const diagnostics = new WeakMap();
|
|
15
16
|
async function regularJson(file) {
|
|
16
17
|
try {
|
|
17
18
|
const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
@@ -65,6 +66,23 @@ function payloadCommand(binding) {
|
|
|
65
66
|
* binding from its verified native-delivery map before it becomes a receipt. */
|
|
66
67
|
export async function reconcileRetainedNativeHook(context, input) {
|
|
67
68
|
const { scope, invocationId } = input;
|
|
69
|
+
const diagnostic = (phase, error, binding) => {
|
|
70
|
+
if (!input.onDiagnostic)
|
|
71
|
+
return;
|
|
72
|
+
const supplied = error instanceof Error || object(error) ? error.code : undefined;
|
|
73
|
+
const code = typeof supplied === "string" && /^[A-Za-z][A-Za-z0-9_]{0,95}$/.test(supplied) ? supplied : "native_hook_verification_unavailable";
|
|
74
|
+
// Diagnostics never become execution authority or activity. Keep identifiers
|
|
75
|
+
// and a typed cause, not native messages/configs that can contain credentials.
|
|
76
|
+
const value = { phase, code, invocation_id: invocationId,
|
|
77
|
+
...(typeof binding?.request_id === "string" ? { request_id: binding.request_id } : {}),
|
|
78
|
+
...(typeof binding?.operation_id === "string" ? { operation_id: binding.operation_id } : {}) };
|
|
79
|
+
const seen = diagnostics.get(context) ?? new Set(), key = JSON.stringify(value);
|
|
80
|
+
if (seen.has(key))
|
|
81
|
+
return;
|
|
82
|
+
seen.add(key);
|
|
83
|
+
diagnostics.set(context, seen);
|
|
84
|
+
input.onDiagnostic(value);
|
|
85
|
+
};
|
|
68
86
|
if (!scope.runId || context.env.DD_FLOW_DAEMON_ID !== scope.daemonId)
|
|
69
87
|
return false;
|
|
70
88
|
let owner = null;
|
|
@@ -84,17 +102,22 @@ export async function reconcileRetainedNativeHook(context, input) {
|
|
|
84
102
|
let metadata;
|
|
85
103
|
try {
|
|
86
104
|
const state = readNativeDaemonState(stateDir);
|
|
87
|
-
if ((state.daemon_id ?? state.config?.daemonId) !== scope.daemonId)
|
|
105
|
+
if ((state.daemon_id ?? state.config?.daemonId) !== scope.daemonId) {
|
|
106
|
+
diagnostic("owner", { code: "daemon_owner_mismatch" });
|
|
88
107
|
return false;
|
|
108
|
+
}
|
|
89
109
|
const project = requireProjectByRoot(context, scope.projectRoot);
|
|
90
110
|
owned = requireManagedDaemonBinding(context, { stateDir, projectId: project.id, runId: scope.runId });
|
|
91
111
|
metadata = JSON.parse(owned.process.metadata_json);
|
|
92
112
|
}
|
|
93
|
-
catch {
|
|
113
|
+
catch (error) {
|
|
114
|
+
diagnostic("owner", error);
|
|
94
115
|
return false;
|
|
95
116
|
}
|
|
96
|
-
if (metadata.run_generation !== scope.generation)
|
|
117
|
+
if (metadata.run_generation !== scope.generation) {
|
|
118
|
+
diagnostic("owner", { code: "run_generation_mismatch" });
|
|
97
119
|
return false;
|
|
120
|
+
}
|
|
98
121
|
const operations = path.join(stateDir, "operations");
|
|
99
122
|
if (!await realDirectory(operations))
|
|
100
123
|
return false;
|
|
@@ -151,14 +174,20 @@ export async function reconcileRetainedNativeHook(context, input) {
|
|
|
151
174
|
|| !(resolved.parent_provider_session_id === null || typeof resolved.parent_provider_session_id === "string")
|
|
152
175
|
|| ["request_id", "operation_id", "daemon_id", "turn_generation", "source_harness", "root_provider_session_id", "provider_session_id",
|
|
153
176
|
"tool_call_id", "event", "native_command", "command_sha256", "native_creator", "native_admission"].some(key => resolved[key] !== binding[key])
|
|
154
|
-
|| payloadCommand(resolved) !== binding.native_command)
|
|
177
|
+
|| payloadCommand(resolved) !== binding.native_command) {
|
|
178
|
+
diagnostic("verify", { code: "native_hook_unproven" }, binding);
|
|
155
179
|
continue;
|
|
180
|
+
}
|
|
156
181
|
verified.push(resolved);
|
|
157
182
|
}
|
|
158
183
|
else if (proof.verified === true && JSON.stringify(proof.nativeHookBinding) === JSON.stringify(binding))
|
|
159
184
|
verified.push(binding);
|
|
185
|
+
else
|
|
186
|
+
diagnostic("verify", { code: "native_hook_unproven" }, binding);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
diagnostic("verify", error, binding);
|
|
160
190
|
}
|
|
161
|
-
catch { /* Legacy/retired daemons cannot attest a retained fact. */ }
|
|
162
191
|
}
|
|
163
192
|
// Exact old tool delivery belongs to its immutable prior attempt. A public
|
|
164
193
|
// command may be identical in a fresh no-effect successor; do not repeatedly
|
|
@@ -302,16 +302,19 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
|
|
|
302
302
|
// third-party session/card never acknowledged completion.
|
|
303
303
|
const physicalContext = { ...context, env: { ...context.env, DD_FLOW_RESOURCE_HOME: registeredProcesses.find(item => records.includes(item.record))?.home ?? resourceHome(context) } };
|
|
304
304
|
let executionAbsent = false;
|
|
305
|
-
|
|
305
|
+
let absenceVersion;
|
|
306
|
+
if ((boundary || technicalPause || control?.action === "stop") && !interrupt && !conflicts.has(directory) && !unsafeDaemons.has(canonicalPath(directory, false))) {
|
|
306
307
|
try {
|
|
307
|
-
|
|
308
|
+
absenceVersion = nativeInventoryVersion(directory);
|
|
309
|
+
executionAbsent = managedDaemonExecutionAbsent(physicalContext, { stateDir: directory, projectId: project.id, runId: input.runId })
|
|
310
|
+
&& absenceVersion === nativeInventoryVersion(directory);
|
|
308
311
|
}
|
|
309
312
|
catch { /* Unknown physical ownership remains on strict reconciliation. */ }
|
|
310
313
|
}
|
|
311
314
|
if (executionAbsent) {
|
|
312
315
|
const leader = records.find(record => record.kind.endsWith("-daemon") && record.id === state.resource_process?.id && record.pid === state.pid && record.lease_token === state.resource_process.lease_token);
|
|
313
316
|
if (leader && (state.daemon_id ?? state.config?.daemonId)) {
|
|
314
|
-
nativeVersions.set(directory,
|
|
317
|
+
nativeVersions.set(directory, absenceVersion);
|
|
315
318
|
closedDaemons.set(directory, { source: "owned_execution_absent", daemon_id: state.daemon_id ?? state.config?.daemonId,
|
|
316
319
|
session_ids: state.sessions.map(item => typeof item === "string" ? item : adapterSessionId(item)), execution_settled: true,
|
|
317
320
|
native_cleanup_confirmed: state.shutdown_state === "clean", processes: records.map(record => managedProcessBinding(record, registeredProcesses.find(item => item.record === record).home)) });
|
|
@@ -320,34 +323,6 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
|
|
|
320
323
|
continue;
|
|
321
324
|
}
|
|
322
325
|
}
|
|
323
|
-
// Explicit stop can reconcile a retired owner whose last bookkeeping ACK
|
|
324
|
-
// failed. This is physical retirement, not a fabricated clean shutdown.
|
|
325
|
-
if (control?.action === "stop" && state.shutdown_state === "cleanup_failed")
|
|
326
|
-
try {
|
|
327
|
-
const daemonId = state.daemon_id ?? state.config?.daemonId;
|
|
328
|
-
const daemon = records.filter(record => record.kind.endsWith("-daemon"));
|
|
329
|
-
const provider = records.filter(record => record.kind.endsWith("-provider"));
|
|
330
|
-
const operations = retainedDaemonOperations(directory);
|
|
331
|
-
const owner = state.config?.runtime_owner;
|
|
332
|
-
if (daemonId && !conflicts.has(directory) && !unsafeDaemons.has(canonicalPath(directory, false))
|
|
333
|
-
&& state.active_tree === false && !state.active_operation && state.shutdown?.daemon_id === daemonId
|
|
334
|
-
&& state.shutdown.phases?.tree === true && state.shutdown.phases.provider === true
|
|
335
|
-
&& daemon.length === 1 && provider.length === 1 && records.length === 2
|
|
336
|
-
&& daemon[0].id === state.resource_process?.id && daemon[0].lease_token === state.resource_process.lease_token && daemon[0].pid === state.pid
|
|
337
|
-
&& provider[0].id === state.provider_process?.id && provider[0].lease_token === state.provider_process.lease_token && provider[0].pid === state.provider_pid
|
|
338
|
-
&& JSON.parse(daemon[0].metadata_json).dispatch_barrier === "run-generation@1"
|
|
339
|
-
&& records.every(record => record.pid && record.pid_started_at && ["stopped", "failed"].includes(record.state) && record.finished_at && !processTreeIsAlive(record))
|
|
340
|
-
&& operations.length > 0 && operations.every(operation => ["completed", "failed", "interrupted"].includes(String(operation.state))
|
|
341
|
-
&& (operation.operation === "daemon.start" ? operation.daemon_id === null && operation.launch?.runtime_owner === JSON.stringify(owner)
|
|
342
|
-
: operation.daemon_id === daemonId && ["daemon.stop", "daemon.status", "session.create", "session.prompt", "session.start", "session.fork", "session.resume", "session.cancel", "session.inspect"].includes(String(operation.operation))
|
|
343
|
-
&& (operation.session_id == null || state.sessions.some(item => (typeof item === "string" ? item : adapterSessionId(item)) === operation.session_id))))) {
|
|
344
|
-
nativeVersions.set(directory, nativeInventoryVersion(directory, operations));
|
|
345
|
-
closedDaemons.set(directory, { source: "observed_owned_retirement", daemon_id: daemonId, stopped: true, settled: true,
|
|
346
|
-
session_ids: state.sessions.map(item => typeof item === "string" ? item : adapterSessionId(item)),
|
|
347
|
-
processes: records.map(record => ({ process_id: record.id, pid: record.pid, pid_started_at: record.pid_started_at })) });
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
catch { /* Invalid or unfinished evidence remains on the existing recovery path. */ }
|
|
351
326
|
if (!interrupt) {
|
|
352
327
|
try {
|
|
353
328
|
const nativeOperations = retainedDaemonOperations(directory);
|
|
@@ -477,10 +452,17 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
|
|
|
477
452
|
if (!Array.isArray(args) || args.some(value => typeof value !== "string") || !saved.state_dir || !saved.executable || saved.owner !== operation.controller_id || conflicts.has(saved.state_dir) || directoryAdapters.get(saved.state_dir) !== saved.executable)
|
|
478
453
|
throw new AppError("controller_operation_binding_unknown", "Unfinished controller operation has no matching frozen runtime binding", 1);
|
|
479
454
|
const method = `${args[0]}.${args[1] === "status" ? "inspect" : args[1]}`;
|
|
480
|
-
if (
|
|
481
|
-
&& (["
|
|
455
|
+
if (control?.action === "stop" && closedDaemons.get(saved.state_dir)?.source === "owned_execution_absent"
|
|
456
|
+
&& (!["session.create", "session.prompt", "session.start", "session.fork", "session.cancel", "session.resume", "session.inspect", "daemon.start", "daemon.stop"].includes(method)
|
|
457
|
+
|| !args.includes("--state-dir") || args[args.indexOf("--state-dir") + 1] !== saved.state_dir
|
|
458
|
+
|| args.includes("--session-id") && !targets.has(JSON.stringify([saved.state_dir, args[args.indexOf("--session-id") + 1]]))))
|
|
459
|
+
throw new AppError("controller_operation_binding_unknown", "Fenced operation differs from its retained runtime scope", 1);
|
|
460
|
+
if ((boundary || technicalPause || control?.action === "stop") && closedDaemons.get(saved.state_dir)?.source === "owned_execution_absent"
|
|
461
|
+
&& (control?.action === "stop" || ["daemon.stop", "session.cancel", "session.inspect"].includes(method)
|
|
482
462
|
|| saved.obligation && controllerObligationAccepted(context, requireController(context, operation.controller_id), saved.obligation))) {
|
|
483
|
-
|
|
463
|
+
// This completes the settlement observation, not the original unknown
|
|
464
|
+
// operation. Its retained row/effect remains unknown and is not replayed.
|
|
465
|
+
return { operation_id: operation.operation_id, status: "completed", receipt: { source: control?.action === "stop" ? "fenced_stop_execution_absent" : "accepted_boundary_execution_absent", native_outcome: "unavailable", original_operation_status: operation.status, semantic_outcome_accepted: false, cleanup_pending: true } };
|
|
484
466
|
}
|
|
485
467
|
if (!["session.create", "session.prompt", "session.start", "session.fork", "session.cancel", "session.resume", "session.inspect", "daemon.start", "daemon.stop"].includes(method))
|
|
486
468
|
throw new AppError("controller_operation_receipt_required", "Unfinished controller operation needs its original receipt", 1);
|
|
@@ -733,7 +715,7 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
|
|
|
733
715
|
// remaining idempotent resource finish. Unknown outcomes keep their ID.
|
|
734
716
|
if (sessionsSettled) {
|
|
735
717
|
const remainingFinish = state.shutdown_state === "cleanup_failed" && state.active_tree === false
|
|
736
|
-
&& state.shutdown?.daemon_id === daemonId && state.shutdown.phases?.
|
|
718
|
+
&& state.shutdown?.daemon_id === daemonId && state.shutdown.phases?.execution === true
|
|
737
719
|
&& state.shutdown.required_phases?.includes("daemon_resource")
|
|
738
720
|
&& state.shutdown.phases.daemon_resource !== true
|
|
739
721
|
&& state.shutdown.required_phases.every(phase => phase === "daemon_resource" || state.shutdown.phases[phase] === true);
|
|
@@ -922,7 +904,7 @@ async function reconcileOwnedSessions(context, input, interrupt, boundary) {
|
|
|
922
904
|
}
|
|
923
905
|
export function nativeInventoryVersion(directory, operations = retainedDaemonOperations(directory)) {
|
|
924
906
|
const state = JSON.parse(fs.readFileSync(path.join(directory, "daemon.json"), "utf8"));
|
|
925
|
-
const keys = ["schema_id", "daemon_id", "config", "pid", "provider_pid", "shutdown_state", "shutdown", "active_operation", "active_tree", "sessions", "descendants", "root_session_id", "resource_process", "provider_process", "owned_processes"];
|
|
907
|
+
const keys = ["schema_id", "daemon_id", "config", "pid", "provider_pid", "shutdown_state", "shutdown", "active_operation", "active_tree", "sessions", "descendants", "root_session_id", "resource_process", "provider_process", "owned_processes", "native_execution_revision", "unclaimed_activity", "native_resources_entries", "native_topology_error", "native_retirement_notifications", "terminal_observation_pending", "tool_observation_gaps"];
|
|
926
908
|
// Only these native verbs are read-only. Unknown verbs remain guarded.
|
|
927
909
|
const productive = operations.filter(operation => !isObservationOperation(operation.operation));
|
|
928
910
|
const knownSchema = ["dd-opencode/daemon-state@2", "dd-codex/daemon-state@1", "dd-grok/daemon-state@1", "dd-zcode/daemon-state@1", "dd-agy/daemon-state@1", "dd-droid/daemon-state@1"].includes(String(state.schema_id));
|
|
@@ -79,12 +79,25 @@ async function prepareSpawn(context, row) {
|
|
|
79
79
|
const launch = launchRecord(context, row);
|
|
80
80
|
if (!launch || launch.value.dispatch_ready || !["starting", "running", "recovery_required"].includes(row.status))
|
|
81
81
|
return null;
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
const superseded = new AppError("controller_owner_mismatch", "Controller launch owner changed during physical observation", 1);
|
|
83
|
+
let launcher;
|
|
84
|
+
try {
|
|
85
|
+
launcher = await physicalStartup(context.databaseDeadlineAt).birth(() => {
|
|
86
|
+
const current = requireController(context, row.controller_id);
|
|
87
|
+
if (current.generation !== row.generation)
|
|
88
|
+
throw new AppError("controller_owner_mismatch", "Controller launch generation changed during physical observation", 1);
|
|
89
|
+
if (current.owner_token !== row.owner_token)
|
|
90
|
+
throw superseded;
|
|
91
|
+
assertLaunchAllowed(context, current);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
// Like a lost launch CAS below, a concurrent winner is observed, not
|
|
96
|
+
// retried or replaced. Other ownership/admission errors remain refusals.
|
|
97
|
+
if (error === superseded)
|
|
98
|
+
return null;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
88
101
|
const resourceContext = { ...context, env: { ...context.env, DD_FLOW_RESOURCE_HOME: JSON.parse(row.manifest_json).resource_home } };
|
|
89
102
|
const ours = launch.value.pid === launcher.pid && launch.value.pid_started_at === launcher.pid_started_at;
|
|
90
103
|
let token = row.owner_token;
|
|
@@ -55,6 +55,9 @@ import { storageSessionId } from "./session-identity.js";
|
|
|
55
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";
|
|
56
56
|
import { renderNativeChildFollowupInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
57
57
|
import { childCapacityDispatchAllowed, childCapacityRecoveryDecision, closeSuccessfulChildCapacity, reconcileChildCapacity } from "./controller-child-capacity.js";
|
|
58
|
+
import { completedChildRepair, reserveChildRepair } from "./controller-child-repair.js";
|
|
59
|
+
import { prepareWorkFailureResolution, readFailureDecision, failureDecisionReady, acceptFailureDecision, applyWorkFailureDecision, reconcilePendingWorkFailure, workFailureResolutionPrompt, workFailureDiagnostic, workFailureBlockActive } from "./controller-work-failure.js";
|
|
60
|
+
import { getVnextFanoutStatus } from "./vnext-fanout.js";
|
|
58
61
|
import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
|
|
59
62
|
import { recordTechnicalNote } from "./technical-notes.js";
|
|
60
63
|
import { acceptedChildWork, acceptedChildrenRetired, controllerObligation, controllerObligationAccepted, lateNativeFailure } from "./controller-result-authority.js";
|
|
@@ -608,14 +611,18 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
608
611
|
};
|
|
609
612
|
const capacityUncertain = (error) => error instanceof AppError && ["capacity_continuation_blocked", "operation_observation_lost"].includes(error.code) && error.details.capacity_uncertainty === true || Boolean(state.pending_capacity) && (String(error?.code) === "capacity_continuation_blocked" || isObservationLoss(error));
|
|
610
613
|
const stopAfterFailure = (error) => {
|
|
614
|
+
// The published pause already fences this owner. A later observer failure
|
|
615
|
+
// must not supersede that same control with a second pause or fatal stop.
|
|
616
|
+
if (technicalPauseRequested)
|
|
617
|
+
return Promise.resolve();
|
|
611
618
|
// A retained successor may already be running. Fencing/recovery records
|
|
612
619
|
// uncertainty; cancellation is not evidence of its missing outcome.
|
|
613
620
|
if (capacityUncertain(error))
|
|
614
621
|
return Promise.resolve();
|
|
615
622
|
if (retainControllerLeaseUncertainty(context, row, error))
|
|
616
623
|
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 => {
|
|
624
|
+
if (error instanceof AppError && error.code !== "child_work_repair_blocked" && (error.details.reconciliation_required === true || ["invocation_assignment_unproven", "invocation_outcome_pending", "invocation_receipt_timeout"].includes(error.code)) && state.current_session !== null)
|
|
625
|
+
return pauseDependency(state.sessions[state.current_session], undefined, error, "productive_continuation").catch(cause => {
|
|
619
626
|
if (!(cause instanceof AppError && cause.code === "controller_technical_dependency_paused"))
|
|
620
627
|
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
628
|
});
|
|
@@ -672,9 +679,24 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
672
679
|
note(session, "accepted_work_native_child_failed", "native_completion", "cleanup_pending", child.terminal_turn_id);
|
|
673
680
|
return false;
|
|
674
681
|
});
|
|
675
|
-
const pauseDependency = async (session, sourceOperationId) => {
|
|
676
|
-
|
|
677
|
-
|
|
682
|
+
const pauseDependency = async (session, sourceOperationId, cause, protectedAction = "stage_transition") => {
|
|
683
|
+
// The control intent contains the retained productive failure; it must not
|
|
684
|
+
// require that same failure to disappear before the owner can pause it.
|
|
685
|
+
assertPhysicalOwner();
|
|
686
|
+
const recorded = cause ? errorRecord(cause) : { code: "native_writer_dependency_blocked", message: "Conflicting native execution has not settled" };
|
|
687
|
+
const primary = { code: String(recorded.code), message: String(recorded.message) };
|
|
688
|
+
const details = cause instanceof AppError ? cause.details : {};
|
|
689
|
+
const dependency = { primary, protected_action: protectedAction,
|
|
690
|
+
...(typeof details.reason === "string" ? { reason: details.reason } : {}),
|
|
691
|
+
...Object.fromEntries(["work_id", "operation_id", "refusal_invocation_id", "successor_invocation_id"].filter(key => typeof details[key] === "string").map(key => [key, details[key]])) };
|
|
692
|
+
recordTechnicalNote(context, row.project_id, row.run_id, { code: primary.code, message: primary.message,
|
|
693
|
+
phase: protectedAction, generation: row.generation, ...(state.stage ? { stage: state.stage } : {}),
|
|
694
|
+
...(typeof details.work_id === "string" ? { work_id: details.work_id } : {}),
|
|
695
|
+
harness: runtimeHarness(session.profile), resource_identity: session.state_dir,
|
|
696
|
+
consequence: "dependency_blocked", ...(sourceOperationId ? { source_operation_id: sourceOperationId } : {}),
|
|
697
|
+
diagnostic_reference: JSON.stringify(dependency) });
|
|
698
|
+
// Diagnostic observations can race (adapter read vs lifecycle monitor), but
|
|
699
|
+
// they own one pause of this execution, not one control per error message.
|
|
678
700
|
const requestId = `technical:${row.controller_id}:${row.generation}:${digest([state.stage, session.state_dir]).slice(0, 20)}`;
|
|
679
701
|
const input = { projectRoot: manifest.request.project_root, runId: row.run_id, requestId, action: "pause" };
|
|
680
702
|
// Publish source binding in the same SQL transaction as the control
|
|
@@ -688,13 +710,13 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
688
710
|
return;
|
|
689
711
|
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
712
|
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 } : {}) });
|
|
713
|
+
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, ...dependency, ...(sourceOperationId ? { source_operation_id: sourceOperationId } : {}) });
|
|
692
714
|
});
|
|
693
715
|
if (typeof intent.control_id !== "string")
|
|
694
716
|
throw new AppError("controller_dependency_control_unavailable", "Technical control was not created", 1, { intent });
|
|
695
717
|
technicalPauseRequested = true;
|
|
696
718
|
const control = await requestRunControl(executionContext, { ...input, waitMs: 0 });
|
|
697
|
-
throw new AppError("controller_technical_dependency_paused", "
|
|
719
|
+
throw new AppError("controller_technical_dependency_paused", "A retained dependency requires control reconciliation before continuation", 1, { control, ...dependency, effect: "no_effect" });
|
|
698
720
|
};
|
|
699
721
|
const confirmStopped = async (stateDir, receipt) => {
|
|
700
722
|
const fail = () => new AppError("controller_native_shutdown_unproven", "The managed daemon is not proven clean and closed", 1, { state_dir: stateDir, receipt });
|
|
@@ -761,7 +783,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
761
783
|
// A missing report is not a failed Stage. A real unstable source still
|
|
762
784
|
// fences its dependent successor until the existing control can seal it.
|
|
763
785
|
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]);
|
|
786
|
+
await pauseDependency(state.sessions[state.current_session], undefined, error, "boundary_capture");
|
|
765
787
|
}
|
|
766
788
|
};
|
|
767
789
|
const reconcilePromptPredecessor = async (session) => {
|
|
@@ -771,7 +793,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
771
793
|
return;
|
|
772
794
|
const receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false).catch(async (error) => {
|
|
773
795
|
if (accepted && controllerObligationAccepted(context, row, accepted) && lateNativeFailure(error, session.id))
|
|
774
|
-
await pauseDependency(session);
|
|
796
|
+
await pauseDependency(session, undefined, error, "predecessor_settlement");
|
|
775
797
|
throw error;
|
|
776
798
|
});
|
|
777
799
|
state.last_receipt = receipt;
|
|
@@ -782,12 +804,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
782
804
|
note(session, "accepted_boundary_settlement_pending", "native_observation", "cleanup_pending");
|
|
783
805
|
if (receipt.native_settled === true && receipt.settled === true)
|
|
784
806
|
return;
|
|
785
|
-
await pauseDependency(session);
|
|
807
|
+
await pauseDependency(session, undefined, new AppError("accepted_boundary_settlement_pending", "Accepted boundary still has an unsettled native predecessor", 1), "predecessor_settlement");
|
|
786
808
|
}
|
|
787
809
|
throw new AppError("settlement_reconciliation_required", "The managed Session still has an unsettled productive predecessor", 1, { session_id: session.id, settlement: current });
|
|
788
810
|
}
|
|
789
811
|
};
|
|
790
|
-
const prompt = async (session, text, operationId, kind, boundary = "finish", assignedWorkIds) => {
|
|
812
|
+
const prompt = async (session, text, operationId, kind, boundary = "finish", assignedWorkIds, failureResolution) => {
|
|
791
813
|
const hitl = kind === "hitl";
|
|
792
814
|
const pending = state.pending_capacity;
|
|
793
815
|
if (pending && (pending.session_id !== session.id || pending.generation !== row.generation || pending.kind !== (kind ?? "stage") || operationId && operationId !== pending.root_operation_id))
|
|
@@ -796,6 +818,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
796
818
|
await reconcilePromptPredecessor(session);
|
|
797
819
|
const firstOperationId = pending?.root_operation_id ?? operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
|
|
798
820
|
const expectedBoundary = state.stage ? controllerObligation(context, row, state.stage, hitl ? "resume" : boundary, assignedWorkIds) : undefined;
|
|
821
|
+
if (expectedBoundary && failureResolution)
|
|
822
|
+
expectedBoundary.failure_resolution = failureResolution;
|
|
799
823
|
const hitlText = async () => {
|
|
800
824
|
const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
|
|
801
825
|
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]);
|
|
@@ -955,8 +979,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
955
979
|
catch (error) {
|
|
956
980
|
if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
|
|
957
981
|
throw error;
|
|
958
|
-
if (!fatal && lateNativeFailure(error, session.id))
|
|
959
|
-
|
|
982
|
+
if (!fatal && lateNativeFailure(error, session.id)) {
|
|
983
|
+
if (failureResolution && failureDecisionReady(failureResolution))
|
|
984
|
+
acceptFailureDecision(executionContext, row, failureResolution, assertOwner);
|
|
985
|
+
await pauseDependency(session, firstOperationId, error, "productive_continuation");
|
|
986
|
+
}
|
|
960
987
|
await stopAfterFailure(error);
|
|
961
988
|
if (!capacityUncertain(error) && !(error instanceof AppError && error.code === "controller_lease_unconfirmed"))
|
|
962
989
|
cancellation.abort();
|
|
@@ -968,7 +995,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
968
995
|
return receipt;
|
|
969
996
|
};
|
|
970
997
|
const monitor = setInterval(() => {
|
|
971
|
-
if (fatal)
|
|
998
|
+
if (fatal || technicalPauseRequested)
|
|
972
999
|
return;
|
|
973
1000
|
try {
|
|
974
1001
|
checkOutcomes();
|
|
@@ -980,6 +1007,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
980
1007
|
}
|
|
981
1008
|
}, 1000);
|
|
982
1009
|
monitor.unref();
|
|
1010
|
+
const resolutionWriterSettled = async (session) => {
|
|
1011
|
+
const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
|
|
1012
|
+
state.last_receipt = inspected;
|
|
1013
|
+
saveState(context, row, state);
|
|
1014
|
+
return inspected.settled === true && !controllerNativeChildren(inspected, session.id).some(child => ["running", "unknown"].includes(child.status));
|
|
1015
|
+
};
|
|
983
1016
|
try {
|
|
984
1017
|
for (;;) {
|
|
985
1018
|
if (technicalPauseRequested)
|
|
@@ -1003,7 +1036,17 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1003
1036
|
const file = args?.includes("--prompt-file") ? args[args.indexOf("--prompt-file") + 1] : undefined;
|
|
1004
1037
|
if (!session || pending.generation !== row.generation || !file || digestBytes(fs.readFileSync(file)) !== dispatch?.prompt_sha256)
|
|
1005
1038
|
throw new AppError("capacity_continuation_conflict", "Retained capacity resume reference has no exact owned prompt", 1);
|
|
1006
|
-
await prompt(session, fs.readFileSync(file, "utf8"), pending.root_operation_id, pending.kind === "stage" ? undefined : pending.kind);
|
|
1039
|
+
await prompt(session, fs.readFileSync(file, "utf8"), pending.root_operation_id, pending.kind === "stage" ? undefined : pending.kind, dispatch?.obligation?.action ?? "finish", dispatch?.obligation?.work_ids, dispatch?.obligation?.failure_resolution);
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
const failureRecovery = await reconcilePendingWorkFailure(executionContext, row, state, next.kind, run.project_root, assertOwner, resolutionWriterSettled);
|
|
1043
|
+
if (failureRecovery === "waiting") {
|
|
1044
|
+
setStatus("waiting_for_children");
|
|
1045
|
+
await delay(1000);
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
if (failureRecovery === "applied") {
|
|
1049
|
+
saveState(context, row, state);
|
|
1007
1050
|
continue;
|
|
1008
1051
|
}
|
|
1009
1052
|
if (next.kind !== "paused" && state.current_session !== null) {
|
|
@@ -1051,6 +1094,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1051
1094
|
appendEvent(context, row.controller_id, "terminal", { reason: next.reason });
|
|
1052
1095
|
return;
|
|
1053
1096
|
}
|
|
1097
|
+
if ((next.kind === "paused" || next.kind === "blocked") && workFailureBlockActive(executionContext, row, view.index)) {
|
|
1098
|
+
setStatus("waiting_for_dependency");
|
|
1099
|
+
await delay(1000, undefined, { signal: cancellation.signal });
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1054
1102
|
if (next.kind === "paused") {
|
|
1055
1103
|
setStatus("waiting_for_user");
|
|
1056
1104
|
const pause = view.index.stage_runs.find(stage => stage.stage === next.stage)?.pause;
|
|
@@ -1152,7 +1200,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1152
1200
|
}
|
|
1153
1201
|
if (!session || transition.session_mode === "new_session" || !sameSessionProfile(session.profile, transition.coordinator) || path.resolve(session.cwd) !== stageCwd) {
|
|
1154
1202
|
if (session && !await retireSession(session))
|
|
1155
|
-
await pauseDependency(session);
|
|
1203
|
+
await pauseDependency(session, undefined, new AppError("native_session_cleanup_pending", "Retired Session still prevents conflicting workspace reuse", 1), "stage_transition");
|
|
1156
1204
|
await renewLease();
|
|
1157
1205
|
assertOwner();
|
|
1158
1206
|
const directory = path.join(row.state_dir, `session-${state.sessions.length + 1}`);
|
|
@@ -1394,8 +1442,55 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1394
1442
|
continue;
|
|
1395
1443
|
}
|
|
1396
1444
|
}
|
|
1445
|
+
const repairBlockedWorks = [];
|
|
1446
|
+
if (!retainedFailures.length && children.some(child => child.status === "completed" && !acceptedChildWork(context, row, childHarness, session.id, child.session_id))) {
|
|
1447
|
+
const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
|
|
1448
|
+
state.last_receipt = inspected;
|
|
1449
|
+
saveState(context, row, state);
|
|
1450
|
+
children = controllerNativeChildren(inspected, session.id).filter(child => !baseline.has(child.session_id));
|
|
1451
|
+
// Do not compete with an active parent/healthy sibling or turn a known
|
|
1452
|
+
// repair into a replacement fanout wave.
|
|
1453
|
+
if (inspected.settled !== true || children.some(child => ["running", "unknown"].includes(child.status))) {
|
|
1454
|
+
setStatus("waiting_for_children");
|
|
1455
|
+
await delay(1000, undefined, { signal: cancellation.signal });
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
let repair = null;
|
|
1459
|
+
const blockers = [];
|
|
1460
|
+
for (const child of children) {
|
|
1461
|
+
try {
|
|
1462
|
+
repair ??= completedChildRepair(executionContext, row, { stage: next.stage, harness: childHarness, parentId: session.id, children: [child] });
|
|
1463
|
+
}
|
|
1464
|
+
catch (error) {
|
|
1465
|
+
if (!(error instanceof AppError) || error.code !== "child_work_repair_blocked")
|
|
1466
|
+
throw error;
|
|
1467
|
+
blockers.push(error);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
if (repair) {
|
|
1471
|
+
reserveChildRepair(executionContext, row, repair, assertOwner);
|
|
1472
|
+
await prompt(session, repair.prompt, repair.operation_id, undefined, "work", [repair.work_id]);
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
if (blockers.length) {
|
|
1476
|
+
const graph = getVnextFanoutStatus(executionContext, { projectRoot: run.project_root, runId: run.id, stage: next.stage }).orchestration;
|
|
1477
|
+
if (!graph?.works.ready.length)
|
|
1478
|
+
throw blockers[0];
|
|
1479
|
+
for (const blocker of blockers) {
|
|
1480
|
+
const workId = String(blocker.details.work_id);
|
|
1481
|
+
repairBlockedWorks.push(workId);
|
|
1482
|
+
recordTechnicalNote(context, row.project_id, row.run_id, { code: "child_work_repair_blocked", message: blocker.message,
|
|
1483
|
+
phase: "work_repair", generation: row.generation, stage: next.stage, work_id: workId, harness: childHarness,
|
|
1484
|
+
resource_identity: workId, consequence: "dependency_blocked", diagnostic_reference: JSON.stringify({
|
|
1485
|
+
protected_action: "work_finish", reason: blocker.details.reason, primary: blocker.details.primary_error,
|
|
1486
|
+
refusal_invocation_id: blocker.details.refusal_invocation_id, successor_invocation_id: blocker.details.successor_invocation_id,
|
|
1487
|
+
operation_id: blocker.details.operation_id
|
|
1488
|
+
}) });
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1397
1492
|
const memoryFlow = ["mb-init", "mb-upgrade", "mb-fix", "interactive"].includes(run.flow_kind) || isSourceSnapshotFlow(run.flow_kind) || routing.kind === "descriptor" && isDeliveryFlow(run.flow_kind);
|
|
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 };
|
|
1493
|
+
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, ...(state.work_failure?.deferred ? { deferredFailure: state.work_failure.work_id } : {}) };
|
|
1399
1494
|
const fanout = memoryFlow ? await runControllerMemoryTask(executionContext, { operation: "fanout", input: fanoutInput }, cancellation.signal)
|
|
1400
1495
|
: await nextControllerFanout(executionContext, fanoutInput);
|
|
1401
1496
|
if (memoryFlow)
|
|
@@ -1427,6 +1522,43 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1427
1522
|
throw new AppError(cause.code, cause.message, 1, { ...cause.details, ...(cause.invocation_id ? { invocation_id: cause.invocation_id } : {}), ...(cause.hook_event_id ? { hook_event_id: cause.hook_event_id } : {}), controller_wrapper: "incomplete_subject_turn", stage: next.stage });
|
|
1428
1523
|
throw new AppError("incomplete_subject_turn", "Managed Turn ended without a completed Stage or declared continuation", 1, { stage: next.stage });
|
|
1429
1524
|
}
|
|
1525
|
+
if (fanout.kind === "failure") {
|
|
1526
|
+
const resolution = state.work_failure ?? prepareWorkFailureResolution(executionContext, row, next.stage, fanout.failure);
|
|
1527
|
+
if (resolution.work_id !== fanout.failure.work_id || resolution.work_session_id !== fanout.failure.work_session_id)
|
|
1528
|
+
throw new AppError("work_failure_scope_changed", "Pending resolution belongs to another failed attempt", 1);
|
|
1529
|
+
state.work_failure = resolution;
|
|
1530
|
+
saveState(context, row, state);
|
|
1531
|
+
while (!failureDecisionReady(resolution)) {
|
|
1532
|
+
const attempt = state.work_failure.submission_attempt ?? 0;
|
|
1533
|
+
if (attempt >= 2)
|
|
1534
|
+
await pauseDependency(session, undefined, new AppError("work_failure_decision_invalid", "Coordinator did not submit a valid bound failure decision", 1, workFailureDiagnostic(resolution)), "work_failure_resolution");
|
|
1535
|
+
await prompt(session, workFailureResolutionPrompt(resolution, state.work_failure.submission_diagnostic), `${row.controller_id}:failure-resolution:${digest([resolution.accepted_file, attempt, row.generation])}`, undefined, "failure_resolution", undefined, resolution);
|
|
1536
|
+
try {
|
|
1537
|
+
readFailureDecision(resolution);
|
|
1538
|
+
}
|
|
1539
|
+
catch (error) {
|
|
1540
|
+
state.work_failure.submission_diagnostic = error instanceof Error ? error.message : String(error);
|
|
1541
|
+
}
|
|
1542
|
+
state.work_failure.submission_attempt = attempt + 1;
|
|
1543
|
+
saveState(context, row, state);
|
|
1544
|
+
}
|
|
1545
|
+
const decision = acceptFailureDecision(executionContext, row, resolution, assertOwner);
|
|
1546
|
+
if (decision.action === "block" && fanout.ready) {
|
|
1547
|
+
state.work_failure.deferred = true;
|
|
1548
|
+
saveState(context, row, state);
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
if (!await resolutionWriterSettled(session)) {
|
|
1552
|
+
setStatus("waiting_for_children");
|
|
1553
|
+
await delay(1000);
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
applyWorkFailureDecision(executionContext, row, resolution, decision, run.project_root, assertOwner);
|
|
1557
|
+
delete state.work_failure;
|
|
1558
|
+
state.last_continuation = null;
|
|
1559
|
+
saveState(context, row, state);
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1430
1562
|
if (fanout.kind === "wait") {
|
|
1431
1563
|
if (fanout.reason === "accepted_work_native_cleanup") {
|
|
1432
1564
|
const pending = children.filter(child => ["running", "unknown"].includes(child.status) && acceptedChildWork(context, row, childHarness, session.id, child.session_id));
|
|
@@ -1435,20 +1567,20 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1435
1567
|
try {
|
|
1436
1568
|
await call(session, ["session", "cancel", "--session-id", child.session_id, ...sessionArguments(context, manifest, session)], false);
|
|
1437
1569
|
}
|
|
1438
|
-
catch {
|
|
1439
|
-
await pauseDependency(session);
|
|
1570
|
+
catch (error) {
|
|
1571
|
+
await pauseDependency(session, undefined, error, "accepted_child_retirement");
|
|
1440
1572
|
}
|
|
1441
1573
|
}
|
|
1442
1574
|
const operationId = `${row.controller_id}:child-retirement:${crypto.randomUUID()}`;
|
|
1443
1575
|
const observed = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false, operationId).catch(async (error) => {
|
|
1444
1576
|
if (lateNativeFailure(error, session.id))
|
|
1445
|
-
await pauseDependency(session);
|
|
1577
|
+
await pauseDependency(session, operationId, error, "accepted_child_retirement");
|
|
1446
1578
|
throw error;
|
|
1447
1579
|
});
|
|
1448
1580
|
state.last_receipt = observed;
|
|
1449
1581
|
saveState(context, row, state);
|
|
1450
1582
|
if (!acceptedChildrenRetired(pending.map(child => child.session_id), observed, session.id, operationId))
|
|
1451
|
-
await pauseDependency(session);
|
|
1583
|
+
await pauseDependency(session, operationId, new AppError("accepted_child_retirement_unconfirmed", "Accepted child results are retained but native retirement remains unconfirmed", 1), "accepted_child_retirement");
|
|
1452
1584
|
continue;
|
|
1453
1585
|
}
|
|
1454
1586
|
setStatus(fanout.reason === "qualified_capacity_required" ? "waiting_for_capacity" : "waiting_for_children");
|
|
@@ -1504,7 +1636,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1504
1636
|
if (rejected)
|
|
1505
1637
|
throw fatal ?? rejected.reason;
|
|
1506
1638
|
if (cleanupConflict)
|
|
1507
|
-
await pauseDependency(session);
|
|
1639
|
+
await pauseDependency(session, undefined, new AppError("external_work_cleanup_pending", "Accepted external Work still has an unretired workspace writer", 1), "workspace_reuse");
|
|
1508
1640
|
state.last_continuation = null;
|
|
1509
1641
|
saveState(context, row, state);
|
|
1510
1642
|
continue;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
|
-
import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
2
|
+
import { stageLifecycleInstruction, workStopConditionInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
3
3
|
import { ensureAggregateRepair, ensureRepairIntent } from "./repair-intents.js";
|
|
4
4
|
import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
|
|
5
5
|
import crypto from "node:crypto";
|
|
@@ -546,6 +546,8 @@ export function coordinatorPrompt(context, input) {
|
|
|
546
546
|
"</code_graph>",
|
|
547
547
|
"",
|
|
548
548
|
"<execution_commands>",
|
|
549
|
+
workStopConditionInstruction,
|
|
550
|
+
"An explicit child work fail ends that attempt, not automatically the Stage. The controller supplies a bound failure-resolution packet; submit its requested decision without inventing a retry, mutating runtime state or replaying accepted predecessors.",
|
|
549
551
|
"Do not choose a provider delegation tool from this stage prompt. At each work_fanout boundary, stop at the Work-graph boundary; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. It launches only entries listed in graph.ready and uses at most the qualified capacity. Every registered CODE Work runs in a fresh child Session, including a serial dependency chain. The coordinator launches those children only through the controller-supplied native delegation instruction; it must never invoke a Work start_command itself. Each child receives its complete packet from dd-flow and runs its own exact start_command. After a Work finishes, the controller uses the graph returned by work finish to launch newly ready Work. To refresh the parent graph yourself use the exact command: " + runtimeCommand(context, ["work", "ls", "--run", input.run.id, "--ready", "--project-root", input.projectRoot, "--json"]),
|
|
550
552
|
"A quiet child is still running until the harness reports its turn completed, failed, cancelled or explicitly needs attention. An elapsed nominal wait, silence, or no new artifact is not an unresponsive-worker failure. Never interrupt, replace, relaunch, or stage-block a still-running child for that reason, even if an external controller asks. Long work finish and stage finish commands emit check progress on stderr. After you issue the exact CODE stage finish command, wait for that same command to return once: a completed command with a non-zero exit and structured `code_gate_failed` output is its terminal result, not a reason to keep waiting. A repair_required continuation already registered the repair: end this Turn. Do not invoke that Work's start_command from the coordinator Session, inspect its PID, start a second finish command, or infer failure from quiet output. Close a disposable child only after its Work is accepted or explicitly failed/cancelled and the harness reports the turn settled.",
|
|
551
553
|
`A repairable engine, harness, or environment failure is not a user question. Write the evidence summary to ${path.join(requireHome(input.run), "works", input.rootWork.work_id, "block-summary.md")}, then record it with a separate command: ${runtimeCommand(context, ["stage", "block", input.run.id, "--stage", "code", "--work", input.rootWork.work_id, "--kind", "<engine|harness|environment>", "--code", "<stable-code>", "--summary-file", `@run/works/${input.rootWork.work_id}/block-summary.md`, "--retryable", "--project-root", input.projectRoot, "--json"])}. Choose kind and stable-code from evidence; this example is not an executable continuation. Repair it externally, then run the exact unblock_command returned by dd-flow and continue this same stage.`,
|
|
@@ -21,7 +21,7 @@ import { currentStagePrompt, applyExternalStageContext, prepareExternalStageCont
|
|
|
21
21
|
import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
|
|
22
22
|
import { getStageFanoutCapacity, readFanoutDescriptor, subagentCapacityKey, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
23
23
|
import { stageGraphScope } from "./stage-work-graph.js";
|
|
24
|
-
import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
24
|
+
import { stageLifecycleInstruction, workStopConditionInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
25
25
|
import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, latestRunVerificationMatrix } from "./verification-matrix.js";
|
|
26
26
|
import { reviewerSourceInputs, reviewerSourceInstruction } from "./review-copy.js";
|
|
27
27
|
import { reviewDefEvidence } from "./review-deferral.js";
|
|
@@ -366,7 +366,7 @@ function orchestratorPrompt(context, input) {
|
|
|
366
366
|
const decision = path.join(input.root, "decision.json");
|
|
367
367
|
const workspaceContract = ["<workspace_contract>", `- route: ${input.workspaceRoute.route}`, `- feature branch: ${input.workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${input.workspaceRoute.base_ref ?? "not applicable"}`, `- read/write workspace: ${input.run.workspace_root}`, "The CLI verified this frozen route. All plan and correction writes belong in the named workspace; project root remains only the stable lifecycle identity. Do not create, switch, merge or delete branches/worktrees.", "</workspace_contract>"].join("\n");
|
|
368
368
|
const acceptanceOwnership = "For every deferred acceptance or document update, identify the named CODE Work owner, its causal checks, and when it can truthfully close the obligation. Aggregate checks run only after implementation Work completes: assign a CODE owner to pending documents and use CODE needs_checks → verification_required → repair after those receipts. A promise that MERGE or an unnamed readiness owner will finish acceptance is a material PLAN finding; bootstrap workspace readiness is not proof of the new feature.";
|
|
369
|
-
const template = [acceptanceOwnership, planCheckInstruction,
|
|
369
|
+
const template = [acceptanceOwnership, planCheckInstruction, workStopConditionInstruction, "Review stop_conditions semantically: reject conditions that veto the requested change merely because the baseline differs, turn planned paths into ownership, or expand one unsupported evidence operation into a prohibition on all implementation. Preserve real safety, accepted-decision and required-evidence boundaries.",
|
|
370
370
|
`The accepted check profile is ${path.join(input.run.workspace_root, ".memory-bank", "spec", "engineering", "code-check-profile.json")}. A new check required by a correction is planned, not available: name its provider Work and order consumers after that Work.`,
|
|
371
371
|
input.template].join("\n\n");
|
|
372
372
|
const reviewerLaunch = capacity.source === "external_policy"
|