@deksden-com/dd-flow-cli 0.9.0-beta.156 → 0.9.0-beta.159
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 +24 -7
- 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 +1 -1
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -0
- 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/continuation-outcome.js +11 -6
- package/dist/services/controller-child-repair.js +46 -24
- package/dist/services/controller-fanout.js +62 -21
- package/dist/services/controller-result-authority.js +13 -8
- package/dist/services/controller-work-failure.js +198 -0
- package/dist/services/lifecycle-invocations.js +280 -96
- package/dist/services/managed-daemon-binding.js +40 -0
- package/dist/services/run-control.js +18 -36
- package/dist/services/run-controller-state.js +8 -0
- package/dist/services/run-controller.js +136 -27
- 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 +59 -12
- package/dist/services/work-retry-context.js +55 -0
- package/dist/verification-components.json +3 -0
- package/package.json +1 -1
|
@@ -2,6 +2,21 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { AppError } from "../shared/errors.js";
|
|
4
4
|
import { managedProcessRuntimeHome, managedProcessStatus, processTreeIsAlive } from "./managed-processes.js";
|
|
5
|
+
function retainedResourceStopped(resource, daemonId) {
|
|
6
|
+
if (resource.state !== "stopped" || resource.observation_conflict)
|
|
7
|
+
return false;
|
|
8
|
+
if (resource.native_retirement?.method)
|
|
9
|
+
return true;
|
|
10
|
+
// Completed native tasks retain an ordered terminal envelope, not a later
|
|
11
|
+
// stop-RPC receipt. Requiring that RPC after provider exit loses valid proof.
|
|
12
|
+
const source = resource.native_source, terminal = resource.terminal_source;
|
|
13
|
+
return Boolean(daemonId && resource.owner_daemon_id === daemonId
|
|
14
|
+
&& resource.provider_session_id && resource.resource_id?.startsWith(`${resource.provider_session_id}/task-`)
|
|
15
|
+
&& source?.locator && source.locator === terminal?.locator
|
|
16
|
+
&& /^[a-f0-9]{64}$/.test(source.sha256 ?? "") && /^[a-f0-9]{64}$/.test(terminal?.sha256 ?? "")
|
|
17
|
+
&& Number.isSafeInteger(source.step_index) && source.step_index >= 0
|
|
18
|
+
&& Number.isSafeInteger(terminal?.step_index) && terminal.step_index > source.step_index);
|
|
19
|
+
}
|
|
5
20
|
/** Native execution and AGY's potentially scheduled writers are separate facts. */
|
|
6
21
|
export function managedExecutionReusable(state, harness) {
|
|
7
22
|
const shutdown = state.shutdown, result = shutdown?.result;
|
|
@@ -85,5 +100,30 @@ export function managedDaemonExecutionAbsent(context, input) {
|
|
|
85
100
|
if (!provider || !managedProviderBelongsToDaemon(provider, binding.process, binding.state))
|
|
86
101
|
return false;
|
|
87
102
|
}
|
|
103
|
+
// The registry leaders are not the entire native inventory: retain detached
|
|
104
|
+
// tools and schedules even after their provider endpoint disappears.
|
|
105
|
+
if (binding.state.native_topology_error || binding.state.native_retirement_notifications?.some(notice => notice.disposition !== "after_owned_native_stop" || notice.owner_daemon_id !== (binding.state.daemon_id ?? binding.state.config?.daemonId)
|
|
106
|
+
|| !Number.isSafeInteger(notice.generation) || notice.revision !== binding.state.native_execution_revision))
|
|
107
|
+
return false;
|
|
108
|
+
const nativeResources = [...(binding.state.native_resources_entries ?? []), ...(binding.state.shutdown?.result?.native_resources?.entries ?? [])];
|
|
109
|
+
const retained = [...(binding.state.owned_processes ?? []), ...(binding.state.shutdown?.owned_processes ?? []),
|
|
110
|
+
...nativeResources.filter(resource => resource.pid !== undefined || !retainedResourceStopped(resource, binding.state.daemon_id ?? binding.state.config?.daemonId))];
|
|
111
|
+
if (nativeResources.some(resource => resource.observation_conflict || resource.owner_daemon_id !== undefined
|
|
112
|
+
&& resource.owner_daemon_id !== (binding.state.daemon_id ?? binding.state.config?.daemonId)))
|
|
113
|
+
return false;
|
|
114
|
+
for (const process of retained) {
|
|
115
|
+
if (!Number.isSafeInteger(process.pid) || process.pid <= 0 || typeof process.started !== "string" || !process.started.trim())
|
|
116
|
+
return false;
|
|
117
|
+
if (records.some(record => record.pid === process.pid && record.pid_started_at === process.started))
|
|
118
|
+
continue;
|
|
119
|
+
try {
|
|
120
|
+
globalThis.process.kill(process.pid, 0);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error.code !== "ESRCH")
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
88
128
|
return true;
|
|
89
129
|
}
|
|
@@ -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));
|
|
@@ -85,6 +85,14 @@ export function retainControllerContinuation(state, fingerprint, error) {
|
|
|
85
85
|
throw new AppError(error.code, error.message, 1, error.details);
|
|
86
86
|
state.last_continuation = fingerprint;
|
|
87
87
|
}
|
|
88
|
+
/** Keep a bounded diagnostic projection, not the whole failing prompt/receipt. */
|
|
89
|
+
export function controllerDependencyDiagnostic(recorded, protectedAction) {
|
|
90
|
+
const details = recorded.details && typeof recorded.details === "object" ? recorded.details : {};
|
|
91
|
+
return { primary: { code: String(recorded.code), message: String(recorded.message) }, protected_action: protectedAction,
|
|
92
|
+
...Object.fromEntries(["reason", "work_id", "operation_id", "refusal_invocation_id", "successor_invocation_id", "stage", "attempt", "action", "invocation_id"].filter(key => typeof details[key] === "string").map(key => [key, details[key]])),
|
|
93
|
+
...(details.checkpoint && typeof details.checkpoint === "object" ? { checkpoint: details.checkpoint } : {}),
|
|
94
|
+
...(details.authority && typeof details.authority === "object" ? { authority: details.authority } : {}) };
|
|
95
|
+
}
|
|
88
96
|
export function digest(value) { return digestBytes(JSON.stringify(value)); }
|
|
89
97
|
export function digestBytes(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
|
|
90
98
|
export function errorRecord(error) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { assertLifecycleOutcomes, currentLifecycleFailure, managedInvocationContext, managedLifecycleCommand, awaitLifecycleInvocation, assertLifecycleInvocationCurrent, bindControllerStageAssignment } from "./lifecycle-invocations.js";
|
|
1
|
+
import { assertLifecycleOutcomes, currentLifecycleFailure, currentStageFinishAuthority, managedInvocationContext, managedLifecycleCommand, awaitLifecycleInvocation, assertLifecycleInvocationCurrent, bindControllerStageAssignment } from "./lifecycle-invocations.js";
|
|
2
|
+
import { stageAttempt } from "./stage-work-graph.js";
|
|
2
3
|
import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
|
|
3
4
|
import { isDeliveryFlow } from "../domain/delivery-flows.js";
|
|
4
5
|
export function recoveryPacketMatchesStage(packet, scope) {
|
|
@@ -36,12 +37,12 @@ import { readNativeDaemonState, requireManagedDaemonBinding, managedDaemonExecut
|
|
|
36
37
|
import { initCodexHome } from "./hooks.js";
|
|
37
38
|
import { launchExternalWork } from "./external-work-launch.js";
|
|
38
39
|
import { runControllerMemoryTask } from "./controller-memory-task.js";
|
|
39
|
-
import { controllerNativeChildren, controllerStageEntryPrompt, controllerStageResumePrompt, nextControllerFanout } from "./controller-fanout.js";
|
|
40
|
+
import { controllerNativeChildren, controllerSemanticCheckpoint, controllerStageEntryPrompt, controllerStageResumePrompt, nextControllerFanout } from "./controller-fanout.js";
|
|
40
41
|
import { publishPromptDelivery } from "./prompt-delivery.js";
|
|
41
42
|
import { publishStagePacket } from "./stage-report-renderer.js";
|
|
42
43
|
import { claimQueuedMergeRequest } from "./merge-server.js";
|
|
43
44
|
import { prepareVnextCodeWorkspace } from "./vnext-code.js";
|
|
44
|
-
import { requireRun, requireController, controllerReceipt, appendEvent, saveState, retainControllerContinuation, retainControllerCapacityIntent, digest, digestBytes, errorRecord } from "./run-controller-state.js";
|
|
45
|
+
import { requireRun, requireController, controllerReceipt, controllerDependencyDiagnostic, appendEvent, saveState, retainControllerContinuation, retainControllerCapacityIntent, digest, digestBytes, errorRecord } from "./run-controller-state.js";
|
|
45
46
|
import { assertNoCorrelatedRejection } from "./continuation-outcome.js";
|
|
46
47
|
import { spawnController, recordControllerLauncher, claimControllerProcess, markControllerDispatchReady, assertControllerLeaseAllowed } from "./run-controller-process.js";
|
|
47
48
|
import { controllerAdapter, controllerEnvironment, sessionArguments } from "./run-controller-adapter.js";
|
|
@@ -56,6 +57,7 @@ import { RECOVERY_POLICY as CAPACITY_POLICY, nativeItemsSettled, recoveryExhaust
|
|
|
56
57
|
import { renderNativeChildFollowupInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
57
58
|
import { childCapacityDispatchAllowed, childCapacityRecoveryDecision, closeSuccessfulChildCapacity, reconcileChildCapacity } from "./controller-child-capacity.js";
|
|
58
59
|
import { completedChildRepair, reserveChildRepair } from "./controller-child-repair.js";
|
|
60
|
+
import { prepareWorkFailureResolution, readFailureDecision, failureDecisionReady, acceptFailureDecision, applyWorkFailureDecision, reconcilePendingWorkFailure, workFailureResolutionPrompt, workFailureDiagnostic, workFailureBlockActive } from "./controller-work-failure.js";
|
|
59
61
|
import { getVnextFanoutStatus } from "./vnext-fanout.js";
|
|
60
62
|
import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
|
|
61
63
|
import { recordTechnicalNote } from "./technical-notes.js";
|
|
@@ -610,14 +612,18 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
610
612
|
};
|
|
611
613
|
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));
|
|
612
614
|
const stopAfterFailure = (error) => {
|
|
615
|
+
// The published pause already fences this owner. A later observer failure
|
|
616
|
+
// must not supersede that same control with a second pause or fatal stop.
|
|
617
|
+
if (technicalPauseRequested)
|
|
618
|
+
return Promise.resolve();
|
|
613
619
|
// A retained successor may already be running. Fencing/recovery records
|
|
614
620
|
// uncertainty; cancellation is not evidence of its missing outcome.
|
|
615
621
|
if (capacityUncertain(error))
|
|
616
622
|
return Promise.resolve();
|
|
617
623
|
if (retainControllerLeaseUncertainty(context, row, error))
|
|
618
624
|
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 => {
|
|
625
|
+
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)
|
|
626
|
+
return pauseDependency(state.sessions[state.current_session], undefined, error, "productive_continuation").catch(cause => {
|
|
621
627
|
if (!(cause instanceof AppError && cause.code === "controller_technical_dependency_paused"))
|
|
622
628
|
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
629
|
});
|
|
@@ -674,11 +680,22 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
674
680
|
note(session, "accepted_work_native_child_failed", "native_completion", "cleanup_pending", child.terminal_turn_id);
|
|
675
681
|
return false;
|
|
676
682
|
});
|
|
677
|
-
const pauseDependency = async (session, sourceOperationId) => {
|
|
683
|
+
const pauseDependency = async (session, sourceOperationId, cause, protectedAction = "stage_transition") => {
|
|
678
684
|
// The control intent contains the retained productive failure; it must not
|
|
679
685
|
// require that same failure to disappear before the owner can pause it.
|
|
680
686
|
assertPhysicalOwner();
|
|
681
|
-
|
|
687
|
+
const recorded = cause ? errorRecord(cause) : { code: "native_writer_dependency_blocked", message: "Conflicting native execution has not settled" };
|
|
688
|
+
const dependency = controllerDependencyDiagnostic(recorded, protectedAction);
|
|
689
|
+
const primary = dependency.primary;
|
|
690
|
+
const details = cause instanceof AppError ? cause.details : {};
|
|
691
|
+
recordTechnicalNote(context, row.project_id, row.run_id, { code: primary.code, message: primary.message,
|
|
692
|
+
phase: protectedAction, generation: row.generation, ...(state.stage ? { stage: state.stage } : {}),
|
|
693
|
+
...(typeof details.work_id === "string" ? { work_id: details.work_id } : {}),
|
|
694
|
+
harness: runtimeHarness(session.profile), resource_identity: session.state_dir,
|
|
695
|
+
consequence: "dependency_blocked", ...(sourceOperationId ? { source_operation_id: sourceOperationId } : {}),
|
|
696
|
+
diagnostic_reference: JSON.stringify(dependency) });
|
|
697
|
+
// Diagnostic observations can race (adapter read vs lifecycle monitor), but
|
|
698
|
+
// they own one pause of this execution, not one control per error message.
|
|
682
699
|
const requestId = `technical:${row.controller_id}:${row.generation}:${digest([state.stage, session.state_dir]).slice(0, 20)}`;
|
|
683
700
|
const input = { projectRoot: manifest.request.project_root, runId: row.run_id, requestId, action: "pause" };
|
|
684
701
|
// Publish source binding in the same SQL transaction as the control
|
|
@@ -692,13 +709,13 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
692
709
|
return;
|
|
693
710
|
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
711
|
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 } : {}) });
|
|
712
|
+
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 } : {}) });
|
|
696
713
|
});
|
|
697
714
|
if (typeof intent.control_id !== "string")
|
|
698
715
|
throw new AppError("controller_dependency_control_unavailable", "Technical control was not created", 1, { intent });
|
|
699
716
|
technicalPauseRequested = true;
|
|
700
717
|
const control = await requestRunControl(executionContext, { ...input, waitMs: 0 });
|
|
701
|
-
throw new AppError("controller_technical_dependency_paused", "
|
|
718
|
+
throw new AppError("controller_technical_dependency_paused", "A retained dependency requires control reconciliation before continuation", 1, { control, ...dependency, effect: "no_effect" });
|
|
702
719
|
};
|
|
703
720
|
const confirmStopped = async (stateDir, receipt) => {
|
|
704
721
|
const fail = () => new AppError("controller_native_shutdown_unproven", "The managed daemon is not proven clean and closed", 1, { state_dir: stateDir, receipt });
|
|
@@ -765,7 +782,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
765
782
|
// A missing report is not a failed Stage. A real unstable source still
|
|
766
783
|
// fences its dependent successor until the existing control can seal it.
|
|
767
784
|
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]);
|
|
785
|
+
await pauseDependency(state.sessions[state.current_session], undefined, error, "boundary_capture");
|
|
769
786
|
}
|
|
770
787
|
};
|
|
771
788
|
const reconcilePromptPredecessor = async (session) => {
|
|
@@ -775,7 +792,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
775
792
|
return;
|
|
776
793
|
const receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false).catch(async (error) => {
|
|
777
794
|
if (accepted && controllerObligationAccepted(context, row, accepted) && lateNativeFailure(error, session.id))
|
|
778
|
-
await pauseDependency(session);
|
|
795
|
+
await pauseDependency(session, undefined, error, "predecessor_settlement");
|
|
779
796
|
throw error;
|
|
780
797
|
});
|
|
781
798
|
state.last_receipt = receipt;
|
|
@@ -786,12 +803,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
786
803
|
note(session, "accepted_boundary_settlement_pending", "native_observation", "cleanup_pending");
|
|
787
804
|
if (receipt.native_settled === true && receipt.settled === true)
|
|
788
805
|
return;
|
|
789
|
-
await pauseDependency(session);
|
|
806
|
+
await pauseDependency(session, undefined, new AppError("accepted_boundary_settlement_pending", "Accepted boundary still has an unsettled native predecessor", 1), "predecessor_settlement");
|
|
790
807
|
}
|
|
791
808
|
throw new AppError("settlement_reconciliation_required", "The managed Session still has an unsettled productive predecessor", 1, { session_id: session.id, settlement: current });
|
|
792
809
|
}
|
|
793
810
|
};
|
|
794
|
-
const prompt = async (session, text, operationId, kind, boundary = "finish", assignedWorkIds) => {
|
|
811
|
+
const prompt = async (session, text, operationId, kind, boundary = "finish", assignedWorkIds, failureResolution) => {
|
|
795
812
|
const hitl = kind === "hitl";
|
|
796
813
|
const pending = state.pending_capacity;
|
|
797
814
|
if (pending && (pending.session_id !== session.id || pending.generation !== row.generation || pending.kind !== (kind ?? "stage") || operationId && operationId !== pending.root_operation_id))
|
|
@@ -800,6 +817,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
800
817
|
await reconcilePromptPredecessor(session);
|
|
801
818
|
const firstOperationId = pending?.root_operation_id ?? operationId ?? `${row.controller_id}:prompt:${crypto.randomUUID()}`;
|
|
802
819
|
const expectedBoundary = state.stage ? controllerObligation(context, row, state.stage, hitl ? "resume" : boundary, assignedWorkIds) : undefined;
|
|
820
|
+
if (expectedBoundary && failureResolution)
|
|
821
|
+
expectedBoundary.failure_resolution = failureResolution;
|
|
803
822
|
const hitlText = async () => {
|
|
804
823
|
const answerId = firstOperationId.slice(0, -`:prompt:${row.generation}`.length);
|
|
805
824
|
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]);
|
|
@@ -959,8 +978,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
959
978
|
catch (error) {
|
|
960
979
|
if (!fatal && error instanceof AppError && ["run_recovery_guarded", "run_recovery_generation_stale", "controller_owner_mismatch"].includes(error.code))
|
|
961
980
|
throw error;
|
|
962
|
-
if (!fatal && lateNativeFailure(error, session.id))
|
|
963
|
-
|
|
981
|
+
if (!fatal && lateNativeFailure(error, session.id)) {
|
|
982
|
+
if (failureResolution && failureDecisionReady(failureResolution))
|
|
983
|
+
acceptFailureDecision(executionContext, row, failureResolution, assertOwner);
|
|
984
|
+
await pauseDependency(session, firstOperationId, error, "productive_continuation");
|
|
985
|
+
}
|
|
964
986
|
await stopAfterFailure(error);
|
|
965
987
|
if (!capacityUncertain(error) && !(error instanceof AppError && error.code === "controller_lease_unconfirmed"))
|
|
966
988
|
cancellation.abort();
|
|
@@ -972,7 +994,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
972
994
|
return receipt;
|
|
973
995
|
};
|
|
974
996
|
const monitor = setInterval(() => {
|
|
975
|
-
if (fatal)
|
|
997
|
+
if (fatal || technicalPauseRequested)
|
|
976
998
|
return;
|
|
977
999
|
try {
|
|
978
1000
|
checkOutcomes();
|
|
@@ -984,6 +1006,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
984
1006
|
}
|
|
985
1007
|
}, 1000);
|
|
986
1008
|
monitor.unref();
|
|
1009
|
+
const resolutionWriterSettled = async (session) => {
|
|
1010
|
+
const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
|
|
1011
|
+
state.last_receipt = inspected;
|
|
1012
|
+
saveState(context, row, state);
|
|
1013
|
+
return inspected.settled === true && !controllerNativeChildren(inspected, session.id).some(child => ["running", "unknown"].includes(child.status));
|
|
1014
|
+
};
|
|
987
1015
|
try {
|
|
988
1016
|
for (;;) {
|
|
989
1017
|
if (technicalPauseRequested)
|
|
@@ -1007,7 +1035,17 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1007
1035
|
const file = args?.includes("--prompt-file") ? args[args.indexOf("--prompt-file") + 1] : undefined;
|
|
1008
1036
|
if (!session || pending.generation !== row.generation || !file || digestBytes(fs.readFileSync(file)) !== dispatch?.prompt_sha256)
|
|
1009
1037
|
throw new AppError("capacity_continuation_conflict", "Retained capacity resume reference has no exact owned prompt", 1);
|
|
1010
|
-
await prompt(session, fs.readFileSync(file, "utf8"), pending.root_operation_id, pending.kind === "stage" ? undefined : pending.kind);
|
|
1038
|
+
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);
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
const failureRecovery = await reconcilePendingWorkFailure(executionContext, row, state, next.kind, run.project_root, assertOwner, resolutionWriterSettled);
|
|
1042
|
+
if (failureRecovery === "waiting") {
|
|
1043
|
+
setStatus("waiting_for_children");
|
|
1044
|
+
await delay(1000);
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
if (failureRecovery === "applied") {
|
|
1048
|
+
saveState(context, row, state);
|
|
1011
1049
|
continue;
|
|
1012
1050
|
}
|
|
1013
1051
|
if (next.kind !== "paused" && state.current_session !== null) {
|
|
@@ -1055,6 +1093,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1055
1093
|
appendEvent(context, row.controller_id, "terminal", { reason: next.reason });
|
|
1056
1094
|
return;
|
|
1057
1095
|
}
|
|
1096
|
+
if ((next.kind === "paused" || next.kind === "blocked") && workFailureBlockActive(executionContext, row, view.index)) {
|
|
1097
|
+
setStatus("waiting_for_dependency");
|
|
1098
|
+
await delay(1000, undefined, { signal: cancellation.signal });
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1058
1101
|
if (next.kind === "paused") {
|
|
1059
1102
|
setStatus("waiting_for_user");
|
|
1060
1103
|
const pause = view.index.stage_runs.find(stage => stage.stage === next.stage)?.pause;
|
|
@@ -1156,7 +1199,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1156
1199
|
}
|
|
1157
1200
|
if (!session || transition.session_mode === "new_session" || !sameSessionProfile(session.profile, transition.coordinator) || path.resolve(session.cwd) !== stageCwd) {
|
|
1158
1201
|
if (session && !await retireSession(session))
|
|
1159
|
-
await pauseDependency(session);
|
|
1202
|
+
await pauseDependency(session, undefined, new AppError("native_session_cleanup_pending", "Retired Session still prevents conflicting workspace reuse", 1), "stage_transition");
|
|
1160
1203
|
await renewLease();
|
|
1161
1204
|
assertOwner();
|
|
1162
1205
|
const directory = path.join(row.state_dir, `session-${state.sessions.length + 1}`);
|
|
@@ -1437,12 +1480,16 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1437
1480
|
repairBlockedWorks.push(workId);
|
|
1438
1481
|
recordTechnicalNote(context, row.project_id, row.run_id, { code: "child_work_repair_blocked", message: blocker.message,
|
|
1439
1482
|
phase: "work_repair", generation: row.generation, stage: next.stage, work_id: workId, harness: childHarness,
|
|
1440
|
-
resource_identity: workId, consequence: "dependency_blocked", diagnostic_reference:
|
|
1483
|
+
resource_identity: workId, consequence: "dependency_blocked", diagnostic_reference: JSON.stringify({
|
|
1484
|
+
protected_action: "work_finish", reason: blocker.details.reason, primary: blocker.details.primary_error,
|
|
1485
|
+
refusal_invocation_id: blocker.details.refusal_invocation_id, successor_invocation_id: blocker.details.successor_invocation_id,
|
|
1486
|
+
operation_id: blocker.details.operation_id
|
|
1487
|
+
}) });
|
|
1441
1488
|
}
|
|
1442
1489
|
}
|
|
1443
1490
|
}
|
|
1444
1491
|
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 };
|
|
1492
|
+
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 } : {}) };
|
|
1446
1493
|
const fanout = memoryFlow ? await runControllerMemoryTask(executionContext, { operation: "fanout", input: fanoutInput }, cancellation.signal)
|
|
1447
1494
|
: await nextControllerFanout(executionContext, fanoutInput);
|
|
1448
1495
|
if (memoryFlow)
|
|
@@ -1474,6 +1521,43 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1474
1521
|
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 });
|
|
1475
1522
|
throw new AppError("incomplete_subject_turn", "Managed Turn ended without a completed Stage or declared continuation", 1, { stage: next.stage });
|
|
1476
1523
|
}
|
|
1524
|
+
if (fanout.kind === "failure") {
|
|
1525
|
+
const resolution = state.work_failure ?? prepareWorkFailureResolution(executionContext, row, next.stage, fanout.failure);
|
|
1526
|
+
if (resolution.work_id !== fanout.failure.work_id || resolution.work_session_id !== fanout.failure.work_session_id)
|
|
1527
|
+
throw new AppError("work_failure_scope_changed", "Pending resolution belongs to another failed attempt", 1);
|
|
1528
|
+
state.work_failure = resolution;
|
|
1529
|
+
saveState(context, row, state);
|
|
1530
|
+
while (!failureDecisionReady(resolution)) {
|
|
1531
|
+
const attempt = state.work_failure.submission_attempt ?? 0;
|
|
1532
|
+
if (attempt >= 2)
|
|
1533
|
+
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");
|
|
1534
|
+
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);
|
|
1535
|
+
try {
|
|
1536
|
+
readFailureDecision(resolution);
|
|
1537
|
+
}
|
|
1538
|
+
catch (error) {
|
|
1539
|
+
state.work_failure.submission_diagnostic = error instanceof Error ? error.message : String(error);
|
|
1540
|
+
}
|
|
1541
|
+
state.work_failure.submission_attempt = attempt + 1;
|
|
1542
|
+
saveState(context, row, state);
|
|
1543
|
+
}
|
|
1544
|
+
const decision = acceptFailureDecision(executionContext, row, resolution, assertOwner);
|
|
1545
|
+
if (decision.action === "block" && fanout.ready) {
|
|
1546
|
+
state.work_failure.deferred = true;
|
|
1547
|
+
saveState(context, row, state);
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1550
|
+
if (!await resolutionWriterSettled(session)) {
|
|
1551
|
+
setStatus("waiting_for_children");
|
|
1552
|
+
await delay(1000);
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
applyWorkFailureDecision(executionContext, row, resolution, decision, run.project_root, assertOwner);
|
|
1556
|
+
delete state.work_failure;
|
|
1557
|
+
state.last_continuation = null;
|
|
1558
|
+
saveState(context, row, state);
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1477
1561
|
if (fanout.kind === "wait") {
|
|
1478
1562
|
if (fanout.reason === "accepted_work_native_cleanup") {
|
|
1479
1563
|
const pending = children.filter(child => ["running", "unknown"].includes(child.status) && acceptedChildWork(context, row, childHarness, session.id, child.session_id));
|
|
@@ -1482,20 +1566,20 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1482
1566
|
try {
|
|
1483
1567
|
await call(session, ["session", "cancel", "--session-id", child.session_id, ...sessionArguments(context, manifest, session)], false);
|
|
1484
1568
|
}
|
|
1485
|
-
catch {
|
|
1486
|
-
await pauseDependency(session);
|
|
1569
|
+
catch (error) {
|
|
1570
|
+
await pauseDependency(session, undefined, error, "accepted_child_retirement");
|
|
1487
1571
|
}
|
|
1488
1572
|
}
|
|
1489
1573
|
const operationId = `${row.controller_id}:child-retirement:${crypto.randomUUID()}`;
|
|
1490
1574
|
const observed = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false, operationId).catch(async (error) => {
|
|
1491
1575
|
if (lateNativeFailure(error, session.id))
|
|
1492
|
-
await pauseDependency(session);
|
|
1576
|
+
await pauseDependency(session, operationId, error, "accepted_child_retirement");
|
|
1493
1577
|
throw error;
|
|
1494
1578
|
});
|
|
1495
1579
|
state.last_receipt = observed;
|
|
1496
1580
|
saveState(context, row, state);
|
|
1497
1581
|
if (!acceptedChildrenRetired(pending.map(child => child.session_id), observed, session.id, operationId))
|
|
1498
|
-
await pauseDependency(session);
|
|
1582
|
+
await pauseDependency(session, operationId, new AppError("accepted_child_retirement_unconfirmed", "Accepted child results are retained but native retirement remains unconfirmed", 1), "accepted_child_retirement");
|
|
1499
1583
|
continue;
|
|
1500
1584
|
}
|
|
1501
1585
|
setStatus(fanout.reason === "qualified_capacity_required" ? "waiting_for_capacity" : "waiting_for_children");
|
|
@@ -1551,13 +1635,38 @@ async function executeController(context, row, manifest, state, assertOwnership,
|
|
|
1551
1635
|
if (rejected)
|
|
1552
1636
|
throw fatal ?? rejected.reason;
|
|
1553
1637
|
if (cleanupConflict)
|
|
1554
|
-
await pauseDependency(session);
|
|
1638
|
+
await pauseDependency(session, undefined, new AppError("external_work_cleanup_pending", "Accepted external Work still has an unretired workspace writer", 1), "workspace_reuse");
|
|
1555
1639
|
state.last_continuation = null;
|
|
1556
1640
|
saveState(context, row, state);
|
|
1557
1641
|
continue;
|
|
1558
1642
|
}
|
|
1559
|
-
if (state.last_continuation === fanout.fingerprint)
|
|
1560
|
-
|
|
1643
|
+
if (state.last_continuation === fanout.fingerprint) {
|
|
1644
|
+
// Re-read committed evidence before pausing; a settlement can race the
|
|
1645
|
+
// graph observation. No new prompt or invocation is issued to reconcile.
|
|
1646
|
+
const latest = executionContext.db.get("SELECT status,index_json FROM runs WHERE project_id=? AND id=?", [row.project_id, row.run_id]);
|
|
1647
|
+
const latestStage = latest && [...(JSON.parse(latest.index_json).stage_runs ?? [])].reverse().find((stage) => stage.stage === next.stage);
|
|
1648
|
+
if (latest?.status === "done" || latestStage?.status === "done")
|
|
1649
|
+
continue;
|
|
1650
|
+
if (fanout.authority) {
|
|
1651
|
+
const current = currentStageFinishAuthority(executionContext, next.stage);
|
|
1652
|
+
if (current?.status === "issued" && current.command) {
|
|
1653
|
+
const checkpoint = controllerSemanticCheckpoint(executionContext, { stage: next.stage, phase: "stage_finish", command: current.command, invocationId: current.id });
|
|
1654
|
+
if (JSON.stringify(checkpoint) !== JSON.stringify(fanout.checkpoint))
|
|
1655
|
+
continue;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
let rejected;
|
|
1659
|
+
try {
|
|
1660
|
+
assertNoCorrelatedRejection(context, { projectId: row.project_id, runId: row.run_id, generation: row.generation, stage: next.stage, turn: state.turns });
|
|
1661
|
+
}
|
|
1662
|
+
catch (error) {
|
|
1663
|
+
if (!(error instanceof AppError))
|
|
1664
|
+
throw error;
|
|
1665
|
+
rejected = error;
|
|
1666
|
+
}
|
|
1667
|
+
await pauseDependency(session, undefined, new AppError(rejected?.code ?? "fanout_stage_nonprogressing", rejected?.message ?? "The coordinator repeated an unchanged continuation", 1, { ...rejected?.details, stage: next.stage, attempt: stageAttempt(executionContext, row.project_id, row.run_id, next.stage), action: fanout.action,
|
|
1668
|
+
checkpoint: JSON.parse(fanout.fingerprint), ...(fanout.authority ? { authority: fanout.authority } : {}), reconciliation_required: true }), "productive_continuation");
|
|
1669
|
+
}
|
|
1561
1670
|
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 } });
|
|
1562
1671
|
saveState(context, row, state);
|
|
1563
1672
|
await prompt(session, fanout.prompt, undefined, undefined, fanout.action === "materialize_graph" ? "graph" : fanout.action === "dispatch_work" ? "work" : "finish", fanout.work_ids);
|
|
@@ -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"
|