@deksden-com/dd-flow-cli 0.9.0-beta.138 → 0.9.0-beta.139

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/build-info.json +6 -6
  3. package/dist/cli/runtime-process.js +1 -1
  4. package/dist/cli.js +2 -0
  5. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +49 -20
  6. package/dist/harness-runtime/lib/dd-agy.mjs +16 -5
  7. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +1 -1
  8. package/dist/harness-runtime/lib/dd-codex.mjs +2 -2
  9. package/dist/harness-runtime/lib/dd-droid.mjs +30 -8
  10. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +7 -1
  11. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +95 -12
  12. package/dist/harness-runtime/lib/dd-opencode.mjs +51 -20
  13. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +13 -1
  14. package/dist/harness-runtime/lib/dd-zcode.mjs +17 -4
  15. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
  16. package/dist/harness-runtime/lib/delegation-instructions.mjs +11 -0
  17. package/dist/harness-runtime/lib/lease-renewal.mjs +2 -1
  18. package/dist/harness-runtime/lib/native-children.d.mts +2 -0
  19. package/dist/harness-runtime/lib/native-children.mjs +20 -1
  20. package/dist/harness-runtime/lib/opencode-plugin.mjs +30 -9
  21. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +13 -0
  22. package/dist/harness-runtime/lib/provider-overload-policy.mjs +138 -0
  23. package/dist/schemas/flow-run.schema.json +4 -0
  24. package/dist/services/controller-child-capacity.js +13 -0
  25. package/dist/services/controller-child-observer.js +18 -2
  26. package/dist/services/controller-execution.js +6 -6
  27. package/dist/services/external-work-launch.js +2 -4
  28. package/dist/services/frozen-engine.js +16 -8
  29. package/dist/services/managed-processes.js +31 -10
  30. package/dist/services/memory-init-native-capacity.js +65 -10
  31. package/dist/services/memory-init-preflight.js +21 -3
  32. package/dist/services/memory-init-project-bootstrap.js +3 -3
  33. package/dist/services/memory-init-start.js +6 -0
  34. package/dist/services/memory-init-worktrunk-capability.js +9 -3
  35. package/dist/services/merge-server.js +2 -4
  36. package/dist/services/{codex-capacity.js → provider-capacity.js} +34 -16
  37. package/dist/services/recovery-snapshot-database.js +1 -1
  38. package/dist/services/run-controller-adapter.js +1 -1
  39. package/dist/services/run-controller-recovery.js +2 -4
  40. package/dist/services/run-controller.js +59 -26
  41. package/dist/services/run-recovery-guard.js +3 -3
  42. package/dist/services/run-recovery-runtime.js +3 -3
  43. package/dist/services/runs.js +102 -13
  44. package/dist/services/vnext-code-review.js +1 -1
  45. package/dist/services/vnext-code.js +3 -3
  46. package/dist/services/vnext-fanout.js +3 -2
  47. package/dist/services/vnext-plan-review.js +1 -1
  48. package/dist/services/vnext-plan.js +3 -3
  49. package/dist/services/vnext-protocolize.js +20 -6
  50. package/dist/services/vnext-workspace-policy.js +7 -2
  51. package/package.json +1 -1
  52. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +0 -10
  53. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +0 -102
@@ -38,12 +38,14 @@ import { assertNoCorrelatedRejection } from "./continuation-outcome.js";
38
38
  import { spawnController, recordControllerLauncher, claimControllerProcess, markControllerDispatchReady, assertControllerLeaseAllowed } from "./run-controller-process.js";
39
39
  import { controllerAdapter, controllerEnvironment, sessionArguments } from "./run-controller-adapter.js";
40
40
  import { retainedChildObserver } from "./controller-child-observer.js";
41
- import { capacityDelay, continueCodexCapacity, eligibleCodexChildOverload } from "./codex-capacity.js";
41
+ import { capacityDelay, continueProviderCapacity, eligibleProviderChildOverload, providerChildFailure } from "./provider-capacity.js";
42
42
  import { resumeControllerSession, failControllerRecovery } from "./run-controller-recovery.js";
43
43
  import { captureControllerBoundary } from "./run-controller-capture.js";
44
44
  import { requestRunControl } from "./run-control-worker.js";
45
45
  import { storageSessionId } from "./session-identity.js";
46
- import { CAPACITY_POLICY, nativeItemsSettled, overloadBurst, refusalObservation, terminalCodexOverload } from "../harness-runtime/lib/codex-capacity-policy.mjs";
46
+ import { CAPACITY_POLICY, nativeItemsSettled, overloadExhausted, refusalObservation, terminalProviderOverload } from "../harness-runtime/lib/provider-overload-policy.mjs";
47
+ import { renderNativeChildFollowupInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
48
+ import { closeSuccessfulChildCapacity } from "./controller-child-capacity.js";
47
49
  import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
48
50
  export { resumeRunController } from "./run-controller-recovery.js";
49
51
  const preparedLaunchInputs = new WeakMap();
@@ -247,7 +249,10 @@ export async function serveRunController(context, input) {
247
249
  lease.assertNoRenewal();
248
250
  if (state.merge_claim)
249
251
  mergeLease.assertNoRenewal();
250
- assertManagedProcessLease({ ...context, databaseDeadlineAt: Date.now() + 5_000 }, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid });
252
+ // This isolated read already bounds SQLite contention. A wall deadline
253
+ // before opening the reader mistakes scheduler delay for heartbeat loss;
254
+ // the observed row's actual expiry/token still fences productive dispatch.
255
+ assertManagedProcessLease(context, { id: row.process_id, leaseToken: row.process_lease_token, pid: process.pid });
251
256
  const current = requireController(context, row.controller_id);
252
257
  if (current.owner_pid !== process.pid || current.owner_token !== row.owner_token || !["running", "capturing", "waiting_for_user", "waiting_for_context", "waiting_for_children", "waiting_for_capacity", "waiting_for_merge"].includes(current.status))
253
258
  throw new AppError("controller_owner_mismatch", "Controller no longer owns productive dispatch", 1);
@@ -681,14 +686,12 @@ async function executeController(context, row, manifest, state, assertOwnership,
681
686
  }
682
687
  }
683
688
  catch (error) {
684
- if (!["codex-desktop", "opencode-server"].includes(runtimeHarness(session.profile)))
685
- throw error;
686
689
  const resumeCommand = hitl ? text.split("\n\n")[1] : undefined;
687
690
  if (hitl && !resumeCommand?.includes(" stage resume "))
688
691
  throw error;
689
692
  const inspect = (args, id) => runHarnessAdapter({ executable: manifest.adapters[runtimeHarness(session.profile)].executable,
690
693
  args, env: controllerEnvironment(context, manifest, session), runtimeOwner: session.owner, operationId: id, timeoutMs: 30_000, signal: cancellation.signal });
691
- const resumed = await continueCodexCapacity({ error, sessionId: session.id, operationId: firstOperationId, signal: cancellation.signal, allowSettledDescendants: true, atomicIntent: true,
694
+ const resumed = await continueProviderCapacity({ error, sessionId: session.id, operationId: firstOperationId, signal: cancellation.signal, allowSettledDescendants: true, atomicIntent: true,
692
695
  retainedIntents: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ?", [row.controller_id, row.generation])
693
696
  .flatMap(item => { const retained = JSON.parse(item.receipt_json || "null"); return retained?.capacity_intent?.root_operation_id === firstOperationId ? [retained.capacity_intent] : []; }).sort((a, b) => a.ordinal - b.ordinal),
694
697
  retainedFailures: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ?", [row.controller_id, row.generation])
@@ -874,7 +877,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
874
877
  loadExternalStageContext({ file: stageContext.file, expectedSha256: stageContext.sha256, stage: next.stage });
875
878
  const continuation = JSON.stringify(["start_stage", next.stage, next.attempt ?? null]);
876
879
  retainControllerContinuation(state, continuation, { code: "stage_entry_nonprogressing", message: "The coordinator returned without starting the assigned Stage", details: { stage: next.stage, attempt: next.attempt ?? null, last_receipt: state.last_receipt ?? null } });
877
- const transition = routing.transition(next.stage, state.stage ?? undefined);
880
+ const transition = routing.transition(next.stage, state.stage ?? undefined, run);
878
881
  if (routing.kind === "sdlc" && next.stage === "merge" && routing.mergeMode === "server" && !state.merge_claim) {
879
882
  const request = context.db.get("SELECT merge_request_id, status FROM merge_requests WHERE project_id = ? AND run_id = ? ORDER BY created_at DESC, merge_request_id DESC LIMIT 1", [row.project_id, row.run_id]);
880
883
  if (!request || request.status !== "queued")
@@ -890,7 +893,22 @@ async function executeController(context, row, manifest, state, assertOwnership,
890
893
  appendEvent(context, row.controller_id, "merge_claimed", { request_id: claim.request.merge_request_id });
891
894
  await renewLease();
892
895
  }
893
- const stageCwd = routing.cwd(next.stage, state.merge_claim?.target_workspace);
896
+ const stageCwd = routing.cwd(next.stage, state.merge_claim?.target_workspace, run);
897
+ const routeBinding = (current) => {
898
+ const index = JSON.parse(current.index_json);
899
+ return JSON.stringify([current.project_root, current.workspace_root, index.workspace_route ?? null, state.merge_claim ?? null]);
900
+ };
901
+ const preparedRoute = routeBinding(run);
902
+ const assertRoute = async () => {
903
+ assertOwner();
904
+ if (routeBinding(requireRun(context, row.project_id, row.run_id)) !== preparedRoute) {
905
+ const current = state.current_session === null ? null : state.sessions[state.current_session];
906
+ if (current && !current.stopped)
907
+ await stopSession(current);
908
+ throw new AppError("workspace_route_changed", "RUN workspace changed during native Session preparation", 1, { effect: "no_effect", stage: next.stage });
909
+ }
910
+ };
911
+ await assertRoute();
894
912
  let session = state.current_session === null ? null : state.sessions[state.current_session];
895
913
  if (session && routing.kind === "sdlc" && next.stage === "merge" && routing.mergeMode === "same_session" && path.resolve(session.cwd) !== stageCwd) {
896
914
  throw new AppError("merge_session_workspace_conflict", "Same-session MERGE cannot change the native Session working directory", 1, { session_cwd: session.cwd, target_cwd: stageCwd });
@@ -942,6 +960,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
942
960
  }
943
961
  const responseFile = path.join(row.state_dir, `${next.stage}-${next.attempt ?? 1}.stage-start-response.json`);
944
962
  const command = managedLifecycleCommand(executionContext, `${flowCommand(executionContext)} stage start ${run.id} --stage ${next.stage} --project-root ${JSON.stringify(run.project_root)} --context-file ${JSON.stringify(stageContext.file)} --context-sha256 ${stageContext.sha256} --require-session-binding --response-file ${JSON.stringify(responseFile)} --json --progress-jsonl`);
963
+ await assertRoute();
945
964
  appendEvent(context, row.controller_id, "stage_entered", { stage: next.stage, session_id: session.id, context_sha256: stageContext.sha256 });
946
965
  await prompt(session, controllerStageEntryPrompt(next.stage, command));
947
966
  continue;
@@ -949,11 +968,19 @@ async function executeController(context, row, manifest, state, assertOwnership,
949
968
  if (state.current_session === null)
950
969
  throw new AppError("controller_attach_required", "An unfinished Stage has no managed coordinator", 1);
951
970
  const session = state.sessions[state.current_session];
971
+ const childHarness = runtimeHarness(session.profile);
952
972
  const baseline = new Set(state.native_baseline ?? []);
953
973
  let children = controllerNativeChildren(state.last_receipt ?? {}, session.id).filter(child => !baseline.has(child.session_id));
974
+ childObserver(session)?.observeSettledChildren(children);
954
975
  const retainedFailures = childObserver(session)?.failed() ?? [];
955
976
  children = controllerNativeChildren({ descendants: [...children, ...retainedFailures] }, session.id);
956
- if (runtimeHarness(session.profile) === "codex-desktop" && children.some(child => child.status === "failed")) {
977
+ const closedChildEpisodes = closeSuccessfulChildCapacity(state, children, session.id);
978
+ if (closedChildEpisodes.length)
979
+ await prepareMutation(() => {
980
+ saveState(context, row, state);
981
+ appendEvent(context, row.controller_id, "capacity_child_episodes_completed", { child_session_ids: closedChildEpisodes });
982
+ });
983
+ if (children.some(child => child.status === "failed" && renderNativeChildFollowupInstruction(childHarness, child.session_id))) {
957
984
  const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
958
985
  state.last_receipt = inspected;
959
986
  saveState(context, row, state);
@@ -961,32 +988,31 @@ async function executeController(context, row, manifest, state, assertOwnership,
961
988
  const failed = children.filter(child => child.status === "failed");
962
989
  // Freeze each distinct child refusal on its first exact observation,
963
990
  // even while a healthy sibling still owns an active native Turn.
964
- for (const failedChild of failed.filter(item => eligibleCodexChildOverload(item, session.id))) {
965
- const linked = context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: "codex-desktop", session_id: failedChild.session_id })]);
991
+ for (const failedChild of failed.filter(item => eligibleProviderChildOverload(item, session.id))) {
992
+ const linked = context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: childHarness, session_id: failedChild.session_id })]);
966
993
  if (linked.length > 1 || linked.some(link => link.status !== "running"))
967
994
  continue;
968
995
  const retained = state.child_capacity?.[failedChild.session_id];
969
996
  const refusals = [...(retained?.refusals ?? [])];
970
997
  if (refusals.some(item => item.turn_id === failedChild.terminal_turn_id))
971
998
  continue;
972
- const nativeFailure = { code: "turn_interrupted", details: { provider_session_id: failedChild.session_id, turn_id: failedChild.terminal_turn_id, native_turn_id: failedChild.terminal_turn_id, terminal_status: "failed", provider_error: failedChild.provider_error, observed_at: failedChild.terminal_observed_at } };
999
+ const nativeFailure = providerChildFailure(failedChild);
973
1000
  refusals.push(refusalObservation(nativeFailure, retained?.attempts ?? 0, failedChild.terminal_observed_at ?? context.now()));
974
1001
  state.child_capacity ??= {};
975
1002
  state.child_capacity[failedChild.session_id] = { ...retained, turn_id: retained?.turn_id ?? failedChild.terminal_turn_id, attempts: retained?.attempts ?? 0, policy: CAPACITY_POLICY, refusals };
976
1003
  await prepareMutation(() => { saveState(context, row, state); appendEvent(context, row.controller_id, "capacity_child_refusal", { child_session_id: failedChild.session_id, refusal: refusals.at(-1) }); });
977
- const burst = overloadBurst(refusals);
978
- if (burst)
979
- throw new AppError("provider_overload_burst", "Native child had two continuation refusals within two minutes", 1, { ...burst, child_session_id: failedChild.session_id, primary_error: nativeFailure });
1004
+ const exhausted = overloadExhausted(refusals);
1005
+ if (exhausted)
1006
+ throw new AppError("provider_unavailable", "Native child exhausted its overload continuation attempts", 1, { ...exhausted, reason: "overload_retries_exhausted", child_session_id: failedChild.session_id, primary_error: nativeFailure });
980
1007
  }
981
1008
  // Continue one existing child at a time; another failed child does not
982
1009
  // create a second fanout wave or share this child's refusal counter.
983
- const child = children.every(item => item.status === "failed" || item.status === "completed") ? failed.find(item => item.parent_session_id === session.id && item.terminal_turn_id && nativeItemsSettled(item.native_turn_items) && terminalCodexOverload({ code: "turn_interrupted", details: { provider_session_id: item.session_id, turn_id: item.terminal_turn_id, native_turn_id: item.terminal_turn_id, terminal_status: "failed", provider_error: item.provider_error } }, item.session_id)) : undefined;
1010
+ const child = children.every(item => item.status === "failed" || item.status === "completed") ? failed.find(item => renderNativeChildFollowupInstruction(childHarness, item.session_id) && eligibleProviderChildOverload(item, session.id)) : undefined;
984
1011
  const turnId = child?.terminal_turn_id;
985
- const native = child && turnId ? { code: "turn_interrupted", details: { provider_session_id: child.session_id, turn_id: turnId, native_turn_id: turnId,
986
- terminal_status: "failed", provider_error: child.provider_error, observed_at: child.terminal_observed_at } } : null;
1012
+ const native = child && turnId ? providerChildFailure(child) : null;
987
1013
  const prior = child && state.child_capacity?.[child.session_id];
988
- const links = child ? context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: "codex-desktop", session_id: child.session_id })]) : [];
989
- if (child && turnId && native && terminalCodexOverload(native, child.session_id, turnId)
1014
+ const links = child ? context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: childHarness, session_id: child.session_id })]) : [];
1015
+ if (child && turnId && native && terminalProviderOverload(native, child.session_id, turnId)
990
1016
  && nativeItemsSettled(child.native_turn_items)
991
1017
  && inspected.settled === true && child.parent_session_id === session.id && links.length <= 1
992
1018
  && (!links.length || links[0].status === "running")) {
@@ -1004,10 +1030,17 @@ async function executeController(context, row, manifest, state, assertOwnership,
1004
1030
  state.child_capacity[child.session_id] = { ...prior, turn_id: prior?.turn_id ?? turnId, attempts: prior?.attempts ?? 0, policy: CAPACITY_POLICY, refusals };
1005
1031
  saveState(context, row, state);
1006
1032
  }
1007
- const burst = overloadBurst(refusals);
1008
- if (burst)
1009
- throw new AppError("provider_overload_burst", "Native child had two continuation refusals within two minutes", 1, { ...burst, child_session_id: child.session_id, primary_error: native });
1010
- const wait = prepared ? Math.max(0, Date.parse(prepared.not_before) - Date.now()) : capacityDelay(native, attempt);
1033
+ const exhausted = overloadExhausted(refusals);
1034
+ if (exhausted)
1035
+ throw new AppError("provider_unavailable", "Native child exhausted its overload continuation attempts", 1, { ...exhausted, reason: "overload_retries_exhausted", child_session_id: child.session_id, primary_error: native });
1036
+ let wait;
1037
+ try {
1038
+ wait = prepared ? Math.max(0, Date.parse(prepared.not_before) - Date.now()) : capacityDelay(native, attempt);
1039
+ }
1040
+ catch (error) {
1041
+ const failure = error;
1042
+ throw new AppError(failure.code ?? "capacity_retry_metadata_invalid", failure.message ?? "Invalid child retry delay", 1, { ...failure.details, child_session_id: child.session_id, root_operation_id: prior?.root_operation_id ?? null, attempts: refusals.length, primary_error: native });
1043
+ }
1011
1044
  if (!Number.isFinite(wait) || Date.now() + wait > 8.64e15)
1012
1045
  throw new AppError("capacity_retry_metadata_invalid", "Native child retry time is invalid", 1, { child_session_id: child.session_id, delay_ms: wait });
1013
1046
  const rootOperationId = prior?.root_operation_id ?? `${row.controller_id}:child:${digest([child.session_id, turnId]).slice(0, 16)}`;
@@ -1030,7 +1063,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
1030
1063
  throw new AppError("capacity_child_followup_blocked", "RUN changed during native child capacity wait", 1, { child_session_id: child.session_id });
1031
1064
  const fresh = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1032
1065
  const still = controllerNativeChildren(fresh, session.id).find(item => item.session_id === child.session_id);
1033
- const currentLinks = context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: "codex-desktop", session_id: child.session_id })]);
1066
+ const currentLinks = context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: childHarness, session_id: child.session_id })]);
1034
1067
  if (currentLinks.length === 1 && ["completed", "cancelled", "paused"].includes(currentLinks[0].status)) {
1035
1068
  state.last_receipt = fresh;
1036
1069
  saveState(context, row, state);
@@ -1042,7 +1075,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
1042
1075
  throw new AppError("capacity_child_followup_blocked", "Native child or Work changed during capacity wait", 1, { child_session_id: child.session_id, turn_id: turnId });
1043
1076
  await renewLease();
1044
1077
  assertOwner();
1045
- const text = `The existing native child ${child.session_id} has a confirmed terminal overloaded Turn. Use the native followup_task tool exactly once for that same child ID with this short message: "Continue your assigned Work from its current durable state. Do not repeat an observed lifecycle command. Finish only the original Work." Do not spawn another child, claim another Work, or perform the child's Work in this parent Turn. End this Turn after the followup_task response.`;
1078
+ const text = renderNativeChildFollowupInstruction(childHarness, child.session_id);
1046
1079
  await prompt(session, text, operationId, "child");
1047
1080
  state.child_capacity[child.session_id].dispatched = true;
1048
1081
  saveState(context, row, state);
@@ -1,5 +1,5 @@
1
1
  import { AppError } from "../shared/errors.js";
2
- import { CAPACITY_POLICY, overloadBurst } from "../harness-runtime/lib/codex-capacity-policy.mjs";
2
+ import { CAPACITY_POLICY, MAX_OVERLOAD_RETRIES, overloadExhausted } from "../harness-runtime/lib/provider-overload-policy.mjs";
3
3
  import { assertRunStartupSettled } from "./run-start-custody.js";
4
4
  export function recoveryGuard(context, projectId, runId) { return context.db.get("SELECT recovery_id, project_id, run_id, generation, status, interruption_json, settlement_json, capture_path, created_at, updated_at, sealed_at, resumed_at FROM run_recovery_guards WHERE project_id = ? AND run_id = ? ORDER BY generation DESC LIMIT 1", [projectId, runId]) ?? null; }
5
5
  export function assertRunRecoveryAllowed(context, projectId, runId, expectedGeneration) {
@@ -43,7 +43,7 @@ export function recoveryRuntimeAdmission(context, record, input) {
43
43
  if (active) {
44
44
  const ordinal = dispatch.capacity_intents.length;
45
45
  const predecessor = ordinal === 1 ? dispatch.operation_id : dispatch.capacity_intents[ordinal - 2].operation_id;
46
- if (active.policy !== CAPACITY_POLICY || active.ordinal !== ordinal || active.root_operation_id !== dispatch.operation_id || active.session_id !== binding.provider_session_id
46
+ if (ordinal > MAX_OVERLOAD_RETRIES || active.policy !== CAPACITY_POLICY || active.ordinal !== ordinal || active.root_operation_id !== dispatch.operation_id || active.session_id !== binding.provider_session_id
47
47
  || active.predecessor !== predecessor || active.operation_id !== `${dispatch.operation_id}:capacity:${ordinal}:session.prompt`
48
48
  || active.refusal?.policy !== CAPACITY_POLICY || active.refusal.ordinal !== ordinal - 1 || active.refusal.turn_id !== active.turn_id
49
49
  || active.refusal.session_id !== binding.provider_session_id || active.deadline !== null || !Number.isFinite(Date.parse(active.not_before)))
@@ -52,7 +52,7 @@ export function recoveryRuntimeAdmission(context, record, input) {
52
52
  if (!failure || JSON.stringify(failure) !== JSON.stringify(active.refusal))
53
53
  return false;
54
54
  try {
55
- if (overloadBurst(dispatch.capacity_failures))
55
+ if (overloadExhausted(dispatch.capacity_failures))
56
56
  return false;
57
57
  }
58
58
  catch {
@@ -8,7 +8,7 @@ import { managedProcessBinding, processIsAlive, resourceHome } from "./managed-p
8
8
  import { getResourceDatabase } from "../storage/database.js";
9
9
  import { requireManagedDaemonBinding } from "./managed-daemon-binding.js";
10
10
  import { lifecycleRetryInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
11
- import { CAPACITY_POLICY, overloadBurst } from "../harness-runtime/lib/codex-capacity-policy.mjs";
11
+ import { CAPACITY_POLICY, MAX_OVERLOAD_RETRIES, overloadExhausted } from "../harness-runtime/lib/provider-overload-policy.mjs";
12
12
  /** Pin retained Session loading and its first acknowledgement Turn, never a general fence bypass. */
13
13
  export function prepareRunRecoveryDispatch(context, input) {
14
14
  if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(input.operationId) || !path.isAbsolute(input.stateDir))
@@ -60,7 +60,7 @@ export function prepareRunRecoveryDispatch(context, input) {
60
60
  /** Publish only an exact successor to a failed, effect-free recovery ACK Turn. */
61
61
  export function retainRecoveryCapacityIntent(context, input) {
62
62
  const { intent } = input;
63
- if (!Number.isSafeInteger(intent.ordinal) || intent.ordinal < 1 || !intent.turn_id || !Number.isFinite(Date.parse(intent.not_before)) || intent.policy !== CAPACITY_POLICY
63
+ if (!Number.isSafeInteger(intent.ordinal) || intent.ordinal < 1 || intent.ordinal > MAX_OVERLOAD_RETRIES || !intent.turn_id || !Number.isFinite(Date.parse(intent.not_before)) || intent.policy !== CAPACITY_POLICY
64
64
  || !intent.refusal || intent.refusal.policy !== CAPACITY_POLICY || intent.refusal.ordinal !== intent.ordinal - 1 || intent.refusal.turn_id !== intent.turn_id
65
65
  || !Number.isFinite(Date.parse(intent.refusal.first_observed_at)) || intent.deadline !== null)
66
66
  throw new AppError("recovery_capacity_intent_invalid", "Recovery capacity successor is invalid", 1);
@@ -91,7 +91,7 @@ export function retainRecoveryCapacityIntent(context, input) {
91
91
  }
92
92
  const refusals = dispatch.capacity_failures ?? [];
93
93
  const observed = refusals.find(item => item.ordinal === intent.refusal.ordinal);
94
- if (!observed || JSON.stringify(observed) !== JSON.stringify(intent.refusal) || overloadBurst(refusals))
94
+ if (!observed || JSON.stringify(observed) !== JSON.stringify(intent.refusal) || overloadExhausted(refusals))
95
95
  throw new AppError("recovery_capacity_intent_conflict", "Recovery successor lacks an immutable non-burst refusal binding", 1);
96
96
  const predecessor = intent.ordinal === 1 ? dispatch.operation_id : prior[intent.ordinal - 2]?.operation_id;
97
97
  if (prior.length !== intent.ordinal - 1 || predecessor !== intent.predecessor || intent.operation_id !== `${dispatch.operation_id}:capacity:${intent.ordinal}:session.prompt`)
@@ -26,6 +26,7 @@ import { flowCommand } from "./stage-pause.js";
26
26
  import { RetainedExecutionBundle } from "./execution-bundles.js";
27
27
  import { admitBundledFlow, bundledHandlerContext } from "./stage-handlers.js";
28
28
  import { hybridDigest } from "./hybrid-contracts.js";
29
+ import { projectPacketLocators, runContextRelocations } from "./stage-context.js";
29
30
  const runSchemaId = "dd-flow/flow-run@3";
30
31
  const runtimeSchemaId = "dd-flow/flow-run@3";
31
32
  const allowedRunFlowKinds = [
@@ -284,20 +285,82 @@ export function importFlowRunExecutionProfile(context, input) {
284
285
  export function rebindFlowRunWorkspace(context, input) {
285
286
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
286
287
  const run = resolveRun(context, project.id, input.runId);
287
- assertRunMutationAllowed(context, project.id, run.id);
288
+ const generation = assertRunMutationAllowed(context, project.id, run.id, input.expectedGeneration);
288
289
  const workspaceRoot = resolveWorkspaceRoot(input.workspaceRoot);
289
290
  const index = authoritativeIndex(run);
291
+ const receipt = input.routeReceipt ? { ...input.routeReceipt, policy: { ...input.routeReceipt.policy,
292
+ worktree_path: input.routeReceipt.policy.worktree_path ? resolveWorkspaceRoot(input.routeReceipt.policy.worktree_path) : null } } : undefined;
293
+ if (index.execution_bundle && !["vnext_specify", "vnext_protocolize"].includes(run.flow_kind)) {
294
+ if (workspaceRoot === run.workspace_root)
295
+ return { ok: true, changed: false };
296
+ throw new AppError("workspace_route_invalid", "Only SDLC may provision a different RUN workspace", 1, { effect: "no_effect" });
297
+ }
298
+ if (workspaceRoot === run.workspace_root) {
299
+ if (receipt && index.workspace_route && hybridDigest(retainedWorkspaceRoute(context, run).receipt) !== hybridDigest(receipt)) {
300
+ throw new AppError("workspace_route_conflict", "Committed workspace route differs from the repeated request", 1, { effect: "no_effect" });
301
+ }
302
+ publishRunProjections(context, project.id, run.id);
303
+ return { ok: true, changed: false, workspace_root: workspaceRoot };
304
+ }
305
+ if (index.workspace_route)
306
+ throw new AppError("workspace_route_conflict", "RUN already has a committed workspace route", 1, { effect: "no_effect" });
307
+ if (input.expectedRevision !== undefined && (index.runtime_revision ?? 0) !== input.expectedRevision)
308
+ throw new AppError("run_revision_changed", "RUN changed while its workspace was provisioned", 1, { effect: "no_effect", expected_revision: input.expectedRevision, actual_revision: index.runtime_revision ?? 0 });
309
+ if (index.execution_bundle && !receipt)
310
+ throw new AppError("workspace_route_missing", "Bundled SDLC rebind requires its prepared route receipt", 1, { effect: "no_effect" });
311
+ if (receipt && (run.workspace_root !== project.root || receipt.schema_id !== "dd-flow/vnext-workspace-route@1" || receipt.policy.route !== "feature_worktree"
312
+ || !receipt.policy.provisioned || receipt.policy.worktree_path !== workspaceRoot || !receipt.created || !receipt.bootstrap)) {
313
+ throw new AppError("workspace_route_invalid", "Prepared workspace route does not match this RUN", 1, { effect: "no_effect" });
314
+ }
290
315
  const now = context.now();
291
316
  const facts = gitFacts(workspaceRoot);
317
+ if (receipt && (facts.branch !== receipt.policy.feature_branch || !receipt.policy.base_ref || !facts.head
318
+ || spawnSync("git", ["-C", workspaceRoot, "merge-base", "--is-ancestor", receipt.policy.base_ref, facts.head]).status !== 0)) {
319
+ throw new AppError("workspace_route_invalid", "Prepared checkout differs from the feature branch/base receipt", 1, { effect: "no_effect", run_id: run.id });
320
+ }
292
321
  index.workspace = { ...index.workspace, workspace_path: workspaceRoot, branch: facts.branch ?? "unknown", base: facts.head ?? "unknown", manager: "official-worktrunk" };
293
322
  index.execution = { ...index.execution, project_root: project.root, workspace_root: workspaceRoot, git: facts };
294
323
  index.updated_at = now;
295
- context.db.run("UPDATE runs SET workspace_root = ? WHERE project_id = ? AND id = ?", [workspaceRoot, project.id, run.id]);
296
- persistRunState(context, project, { ...run, workspace_root: workspaceRoot }, index);
297
- appendAudit(context, { projectId: project.id, eventType: "flow_run.workspace_rebound", payload: { run_id: run.id, workspace_root: workspaceRoot, reason: input.reason } });
298
- appendRunTimeline(context, project.id, run.id, { at: now, type: "workspace_rebound", run_id: run.id, workspace_root: workspaceRoot, reason: input.reason, git: facts });
324
+ const route = receipt ? { schema_id: "dd-flow/run-workspace-route@1", run_id: run.id, project_id: project.id, project_root: project.root,
325
+ generation, previous_workspace: run.workspace_root, workspace_root: workspaceRoot, revision: (index.runtime_revision ?? 0) + 1,
326
+ git: facts, receipt, receipt_sha256: hybridDigest(receipt) } : undefined;
327
+ if (route)
328
+ index.workspace_route = { revision: route.revision, sha256: hybridDigest(route) };
329
+ context.db.writeTransaction(() => {
330
+ assertRunMutationAllowed(context, project.id, run.id, generation);
331
+ const latest = resolveRun(context, project.id, run.id);
332
+ if (latest.workspace_root !== run.workspace_root)
333
+ throw new AppError("workspace_route_changed", "RUN route changed during preparation", 1, { effect: "no_effect" });
334
+ context.db.run("UPDATE runs SET workspace_root = ? WHERE project_id = ? AND id = ?", [workspaceRoot, project.id, run.id]);
335
+ persistRunState(context, project, { ...run, workspace_root: workspaceRoot }, index);
336
+ appendAudit(context, { projectId: project.id, eventType: "flow_run.workspace_rebound", payload: { run_id: run.id, workspace_root: workspaceRoot, reason: input.reason, ...(route ? { route } : {}) } });
337
+ appendRunTimeline(context, project.id, run.id, { at: now, type: "workspace_rebound", run_id: run.id, workspace_root: workspaceRoot, reason: input.reason, git: facts });
338
+ }, "runs.rebindWorkspace");
299
339
  return { ok: true, run_id: run.id, project_root: project.root, workspace_root: workspaceRoot, git: facts };
300
340
  }
341
+ function retainedWorkspaceRoute(context, run) {
342
+ const index = authoritativeIndex(run), binding = index.workspace_route;
343
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.workspace_rebound' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.route.revision') = ?", [run.project_id, run.id, binding?.revision ?? -1]);
344
+ const original = rows.length === 1 ? JSON.parse(rows[0].payload_json).route : undefined;
345
+ if (!binding || !original || original.schema_id !== "dd-flow/run-workspace-route@1" || hybridDigest(original) !== binding.sha256 || hybridDigest(original.receipt) !== original.receipt_sha256) {
346
+ throw new AppError("workspace_route_invalid", "RUN committed workspace route evidence changed", 1, { effect: "no_effect", run_id: run.id });
347
+ }
348
+ // Relocation receipts are admitted by snapshot import; never rewrite bundle
349
+ // or source route authority merely because a physical checkout moved.
350
+ const route = JSON.parse(projectPacketLocators(JSON.stringify(original), runContextRelocations(context, run.project_id, run.id)));
351
+ if (route.receipt_sha256 !== original.receipt_sha256
352
+ || route.run_id !== run.id || route.project_id !== run.project_id || route.project_root !== run.project_root || route.workspace_root !== run.workspace_root
353
+ || index.workspace.workspace_path !== run.workspace_root || index.execution?.workspace_root !== run.workspace_root || index.execution?.project_root !== run.project_root
354
+ || index.workspace.manager !== "official-worktrunk" || index.workspace.branch !== route.receipt.policy.feature_branch
355
+ // execution.git is the latest observation (stage completion/refresh updates
356
+ // it). The immutable review base is workspace.base, not that moving HEAD.
357
+ || index.workspace.base !== route.git.head
358
+ || !Number.isInteger(route.generation) || route.generation < 0
359
+ || route.receipt.policy.worktree_path !== run.workspace_root) {
360
+ throw new AppError("workspace_route_invalid", "RUN has no exact coherent committed workspace route", 1, { effect: "no_effect", run_id: run.id });
361
+ }
362
+ return route;
363
+ }
301
364
  function loadUpgradeFlowContract(context, projectRoot) {
302
365
  const canon = resolveCanonRoot(context, context.env.DD_MEMORYBANK ? { explicitRoot: context.env.DD_MEMORYBANK } : {});
303
366
  if (!canon.ok || !canon.canon) {
@@ -1608,6 +1671,17 @@ export function publishRunProjections(context, projectId, runId) {
1608
1671
  if (!latest)
1609
1672
  throw new AppError("not_found", "RUN is not registered", 1, { run_id: runId });
1610
1673
  const index = authoritativeIndex(latest);
1674
+ if (index.workspace_route) {
1675
+ const route = retainedWorkspaceRoute(context, latest);
1676
+ const file = path.join(runArtifactRoot(latest), "02-protocolize", "workspace-route.json");
1677
+ ensureDir(path.dirname(file));
1678
+ try {
1679
+ writeJsonFile(file, route.receipt);
1680
+ }
1681
+ catch (error) {
1682
+ throw new AppError("workspace_route_publication_failed", "Committed route projection could not be published", 1, { effect: "committed", run_id: runId, route_sha256: index.workspace_route.sha256, cause: String(error) });
1683
+ }
1684
+ }
1611
1685
  const mutations = context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'flow_run_flag_mutations'") ? context.db.all("SELECT resulting_revision, result_json, created_at, idempotency_key FROM flow_run_flag_mutations WHERE project_id = ? AND run_id = ? ORDER BY resulting_revision DESC LIMIT 32", [projectId, runId]) : [];
1612
1686
  const history = mutations.reverse().map((row) => ({
1613
1687
  revision: row.resulting_revision,
@@ -1661,11 +1735,26 @@ function authoritativeIndex(run) {
1661
1735
  throw new AppError("runtime_state_invalid", "RUN mechanical state in SQLite is missing or invalid", 1, { run_id: run.id, cause: String(error) });
1662
1736
  }
1663
1737
  }
1664
- function assertBundleRunIdentity(run, bundle) {
1665
- const scope = bundle.execution.snapshot().scope;
1738
+ function assertBundleWorkspaceIdentity(context, run, bundle) {
1739
+ const scope = currentBundleScope(context, run, bundle);
1740
+ const route = authoritativeIndex(run).workspace_route ? retainedWorkspaceRoute(context, run) : undefined;
1741
+ if (scope.workspace === run.workspace_root)
1742
+ return true;
1743
+ if (bundle.descriptor.descriptor.id !== "sdlc" || !["vnext_specify", "vnext_protocolize"].includes(run.flow_kind) || scope.workspace !== run.project_root)
1744
+ return false;
1745
+ if (!route)
1746
+ return false;
1747
+ return route.previous_workspace === scope.workspace && route.receipt.policy.route === "feature_worktree"
1748
+ && route.receipt.policy.base_ref !== null && route.receipt.policy.feature_branch !== null;
1749
+ }
1750
+ function currentBundleScope(context, run, bundle) {
1751
+ return JSON.parse(projectPacketLocators(JSON.stringify(bundle.execution.snapshot().scope), runContextRelocations(context, run.project_id, run.id)));
1752
+ }
1753
+ function assertBundleRunIdentity(context, run, bundle) {
1754
+ const scope = currentBundleScope(context, run, bundle);
1666
1755
  const flowId = bundle.descriptor.descriptor.id;
1667
1756
  const matchesFlow = flowId === "sdlc" ? ["vnext_specify", "vnext_protocolize"].includes(run.flow_kind) : flowId === run.flow_kind;
1668
- if (bundle.manifest.run_id !== run.id || scope.projectRoot !== run.project_root || scope.workspace !== run.workspace_root || !matchesFlow) {
1757
+ if (bundle.manifest.run_id !== run.id || scope.projectRoot !== run.project_root || !assertBundleWorkspaceIdentity(context, run, bundle) || !matchesFlow) {
1669
1758
  throw new AppError("run_bundle_identity_invalid", "Execution bundle belongs to another RUN or workspace", 1, { phase: "prepare", effect: "no_effect", run_id: run.id });
1670
1759
  }
1671
1760
  const blobs = new Map(bundle.manifest.inventory.map(ref => [ref.path, bundle.blob(ref.path)]));
@@ -1697,7 +1786,7 @@ export function runBundleAuthorityMetadata(bundle) {
1697
1786
  */
1698
1787
  export function assertPreparedRunBundleAuthority(context, scope, bundle) {
1699
1788
  const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
1700
- assertBundleRunIdentity(run, bundle);
1789
+ assertBundleRunIdentity(context, run, bundle);
1701
1790
  }
1702
1791
  /** SQL-only fence after the caller has admitted compiled handlers outside the
1703
1792
  * writer. Descriptor schema loading is deliberately excluded from this fence.
@@ -1713,9 +1802,9 @@ export function assertPreparedRunBundleSqlAuthority(context, scope, bundle) {
1713
1802
  || anchor.descriptor_sha256 !== bundle.manifest.descriptor.sha256 || anchor.flow_id !== bundle.manifest.flow_id
1714
1803
  || anchor.admission_sha256 !== hybridDigest(bundleAuthority(bundle.admission())))
1715
1804
  throw new AppError("run_bundle_anchor_invalid", "Prepared bundle differs from authoritative RUN", 1);
1716
- const execution = bundle.execution.snapshot().scope;
1805
+ const execution = currentBundleScope(context, run, bundle);
1717
1806
  const flow = bundle.descriptor.descriptor.id;
1718
- if (bundle.manifest.run_id !== run.id || execution.projectRoot !== run.project_root || execution.workspace !== run.workspace_root
1807
+ if (bundle.manifest.run_id !== run.id || execution.projectRoot !== run.project_root || !assertBundleWorkspaceIdentity(context, run, bundle)
1719
1808
  || !(flow === "sdlc" ? ["vnext_specify", "vnext_protocolize"].includes(run.flow_kind) : flow === run.flow_kind)) {
1720
1809
  throw new AppError("run_bundle_identity_invalid", "Execution bundle belongs to another RUN or workspace", 1, { effect: "no_effect", run_id: run.id });
1721
1810
  }
@@ -1755,7 +1844,7 @@ export function readFlowRunExecutionBundle(context, input) {
1755
1844
  }
1756
1845
  const admission = { ...retained, receipts: new Map(retained.receipts), descriptorContext: bundledHandlerContext(new Map()) };
1757
1846
  const bundle = RetainedExecutionBundle.read(path.join(runArtifactRoot(run), "execution-bundle"), anchor.manifest_sha256, admission);
1758
- assertBundleRunIdentity(run, bundle);
1847
+ assertBundleRunIdentity(context, run, bundle);
1759
1848
  if (bundle.manifest.descriptor.sha256 !== anchor.descriptor_sha256 || bundle.manifest.flow_id !== anchor.flow_id) {
1760
1849
  throw new AppError("run_bundle_anchor_invalid", "RUN bundle descriptor identity changed", 1);
1761
1850
  }
@@ -1766,7 +1855,7 @@ export function bindFlowRunExecutionBundle(context, input) {
1766
1855
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
1767
1856
  const run = resolveRun(context, project.id, input.runId), index = authoritativeIndex(run);
1768
1857
  const generation = assertRunMutationAllowed(context, project.id, run.id);
1769
- assertBundleRunIdentity(run, input.bundle);
1858
+ assertBundleRunIdentity(context, run, input.bundle);
1770
1859
  if (input.bundle.manifest.flow_id !== "sdlc") {
1771
1860
  throw new AppError("run_bundle_start_required", "Memory RUN and its bundle authority must be allocated together through the request-bound start", 1, { phase: "prepare", effect: "no_effect", run_id: run.id });
1772
1861
  }
@@ -42,7 +42,7 @@ export function prepareVnextCodeReviewStart(context, input) {
42
42
  const run = requireRun(context, project.id, input.runId);
43
43
  const home = requireHome(run);
44
44
  assertRunMutationAllowed(context, project.id, run.id);
45
- requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
45
+ requireVnextWorkspaceRoute(context, { projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
46
46
  if (!fs.existsSync(path.join(home, "05-code", "stage-report.json")))
47
47
  throw new AppError("runtime_artifact_missing", "CODE-REVIEW requires an accepted CODE report", 1);
48
48
  const root = path.join(home, stageDir);
@@ -42,7 +42,7 @@ export async function prepareVnextCodeWorkspace(context, input) {
42
42
  const project = requireProjectByRoot(context, projectRoot);
43
43
  const run = requireRun(context, project.id, input.runId);
44
44
  assertRunMutationAllowed(context, project.id, run.id);
45
- requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, stage: input.stage ?? stage });
45
+ requireVnextWorkspaceRoute(context, { projectRoot, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, stage: input.stage ?? stage });
46
46
  const root = path.join(requireHome(run), stageDir);
47
47
  fs.mkdirSync(root, { recursive: true });
48
48
  return ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress, { force: input.stage !== "code-review" });
@@ -53,7 +53,7 @@ export function prepareVnextCodeStart(context, input) {
53
53
  const run = requireRun(context, project.id, input.runId);
54
54
  assertRunMutationAllowed(context, project.id, run.id);
55
55
  const home = requireHome(run);
56
- requireVnextWorkspaceRoute({ projectRoot, runId: input.runId, runHome: home, workspaceRoot: run.workspace_root, stage });
56
+ requireVnextWorkspaceRoute(context, { projectRoot, runId: input.runId, runHome: home, workspaceRoot: run.workspace_root, stage });
57
57
  const existingRoot = path.join(home, stageDir);
58
58
  const existingPrompt = path.join(existingRoot, "stage-prompt.md");
59
59
  const existingStage = stageStatus(run, stage);
@@ -158,7 +158,7 @@ export async function finishVnextCode(context, input) {
158
158
  const home = requireHome(run);
159
159
  const root = path.join(home, stageDir);
160
160
  const generation = assertRunMutationAllowed(context, project.id, run.id);
161
- requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
161
+ requireVnextWorkspaceRoute(context, { projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
162
162
  if (stageStatus(run, stage) === "done" && fs.existsSync(path.join(root, "stage-report.json"))) {
163
163
  resumeAcceptedCodeStage(context, run, projectRoot, stage, JSON.parse(fs.readFileSync(path.join(root, "stage-report.json"), "utf8")), nextAction(run, changedPathsFromReport(path.join(root, "stage-report.json"))));
164
164
  return { ok: true, resumed: true, run_id: run.id, stage, outcome: "accepted", report_path: path.join(root, "stage-report.json"), next_action: nextAction(run, changedPathsFromReport(path.join(root, "stage-report.json"))) };
@@ -10,7 +10,8 @@ import { storageSessionId } from "./session-identity.js";
10
10
  import { reconcileUnfinishedChecks } from "./code-checks.js";
11
11
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
12
12
  import { stageWorkIds, stageGraphScope } from "./stage-work-graph.js";
13
- import { eligibleCodexChildOverload } from "./codex-capacity.js";
13
+ import { eligibleProviderChildOverload } from "./provider-capacity.js";
14
+ import { renderNativeChildFollowupInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
14
15
  import { readDescriptorRootWork } from "./descriptor-root-work.js";
15
16
  import { selectMemoryCompositionRegistration } from "./memory-composition-registration.js";
16
17
  import { safeNativeFailure } from "../harness-runtime/lib/native-children.mjs";
@@ -262,7 +263,7 @@ export async function reconcileVnextFanout(context, input) {
262
263
  continue;
263
264
  }
264
265
  const link = active[0];
265
- if (value.harness_id === "codex-desktop" && eligibleCodexChildOverload(child, value.parent_session_id)) {
266
+ if (renderNativeChildFollowupInstruction(String(value.harness_id), String(child.session_id)) && eligibleProviderChildOverload(child, value.parent_session_id)) {
266
267
  issues.push({ code: "child_capacity_pending", work_id: link.work_id, session_id: child.session_id });
267
268
  continue;
268
269
  }
@@ -50,7 +50,7 @@ export function prepareVnextPlanReviewStart(context, input) {
50
50
  return { ...common, state: "terminal", report, outcome };
51
51
  throw new AppError("invalid_work_state", "PLAN-REVIEW is already terminal; corrections are applied in the same review Work and are not re-reviewed automatically", 2, { run_id: run.id, outcome });
52
52
  }
53
- const workspaceRoute = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan-review" });
53
+ const workspaceRoute = requireVnextWorkspaceRoute(context, { projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan-review" });
54
54
  if (!fs.existsSync(path.join(planRoot, "stage-report.json")) || !fs.existsSync(batch))
55
55
  throw new AppError("runtime_artifact_missing", "PLAN-REVIEW requires the accepted PLAN report and proposed CODE batch", 1);
56
56
  const requested = requestedMode(run);
@@ -33,7 +33,7 @@ export function prepareVnextPlanStart(context, input) {
33
33
  const project = requireProjectByRoot(context, projectRoot);
34
34
  const run = requireRun(context, projectRoot, input.runId);
35
35
  const home = requireHome(run);
36
- const workspaceRoute = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan" });
36
+ const workspaceRoute = requireVnextWorkspaceRoute(context, { projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan" });
37
37
  const protocols = protocolIds(home);
38
38
  if (!protocols.length)
39
39
  throw new AppError("runtime_artifact_missing", "PLAN requires accepted PROTOCOLIZE protocols", 1);
@@ -136,7 +136,7 @@ export function startVnextPlan(context, input, prepared = prepareVnextPlanStart(
136
136
  export function prepareVnextPlanFinish(context, input) {
137
137
  const projectRoot = resolveProjectRoot(input.projectRoot);
138
138
  const run = requireRun(context, projectRoot, input.runId), home = requireHome(run);
139
- requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan" });
139
+ requireVnextWorkspaceRoute(context, { projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan" });
140
140
  const protocols = protocolIds(home);
141
141
  assertProtocolWorkspace(run.workspace_root, protocols);
142
142
  const prepared = prepareVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols });
@@ -153,7 +153,7 @@ function finishVnextPlanOwned(context, input) {
153
153
  const run = requireRun(context, projectRoot, input.runId);
154
154
  const home = requireHome(run);
155
155
  const root = path.join(home, "03-plan");
156
- requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan" });
156
+ requireVnextWorkspaceRoute(context, { projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan" });
157
157
  const protocols = protocolIds(home);
158
158
  if (!protocols.length)
159
159
  throw new AppError("runtime_artifact_missing", "PLAN requires accepted PROTOCOLIZE protocols", 1);