@deksden-com/dd-flow-cli 0.9.0-beta.150 → 0.9.0-beta.152

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 (40) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +17 -22
  3. package/dist/build-info.json +6 -6
  4. package/dist/harness-runtime/lib/agy-error-boundary.mjs +26 -5
  5. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +67 -0
  6. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +10 -14
  7. package/dist/harness-runtime/lib/dd-agy.mjs +3 -3
  8. package/dist/harness-runtime/lib/dd-droid.mjs +2 -2
  9. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +62 -10
  10. package/dist/harness-runtime/lib/dd-opencode.mjs +32 -14
  11. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
  12. package/dist/harness-runtime/lib/dd-zcode.mjs +37 -12
  13. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -1
  14. package/dist/harness-runtime/lib/delegation-instructions.mjs +13 -4
  15. package/dist/harness-runtime/lib/model-observations.mjs +19 -7
  16. package/dist/harness-runtime/lib/native-children.d.mts +12 -1
  17. package/dist/harness-runtime/lib/native-children.mjs +47 -1
  18. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +9 -1
  19. package/dist/harness-runtime/lib/provider-overload-policy.mjs +97 -6
  20. package/dist/services/check-runner-evidence.js +54 -309
  21. package/dist/services/check-runner-proof.js +373 -0
  22. package/dist/services/code-checks.js +31 -12
  23. package/dist/services/controller-child-capacity.js +58 -0
  24. package/dist/services/controller-child-observer.js +9 -3
  25. package/dist/services/memory-init-native-capacity.js +1 -1
  26. package/dist/services/provider-capacity.js +15 -7
  27. package/dist/services/repair-intents.js +4 -1
  28. package/dist/services/run-controller-adapter.js +1 -1
  29. package/dist/services/run-controller.js +87 -18
  30. package/dist/services/run-recovery-guard.js +1 -1
  31. package/dist/services/run-recovery-runtime.js +1 -1
  32. package/dist/services/stage-context.js +56 -8
  33. package/dist/services/vnext-code-review.js +1 -1
  34. package/dist/services/vnext-code.js +3 -2
  35. package/dist/services/vnext-fanout.js +1 -1
  36. package/dist/services/vnext-merge.js +1 -1
  37. package/dist/services/work-registry.js +1 -1
  38. package/dist/verification-components.json +299 -0
  39. package/package.json +7 -6
  40. package/tools/verification-jobs.mjs +317 -0
@@ -1,3 +1,58 @@
1
+ import { nativeContinuationSuccessor, nativeContinuationAdmitted } from "../harness-runtime/lib/native-children.mjs";
2
+ /** The failure fence forbids fanout, not the exact retained child's recovery. */
3
+ export function childCapacityDispatchAllowed(state, failures, parentId, operationId, promptSha256) {
4
+ return Object.entries(state.child_capacity ?? {}).some(([sessionId, intent]) => intent.operation_id === operationId
5
+ && intent.dispatched === true && intent.phase === "dispatched" && intent.prompt_sha256 === promptSha256
6
+ && failures.some(child => child.session_id === sessionId && child.parent_session_id === parentId
7
+ && child.status === "failed" && child.provenance !== "parent_mismatch" && child.terminal_turn_id === intent.turn_id));
8
+ }
9
+ /** An overloaded parent may have sent the message, but activity is not admission. */
10
+ export function childCapacityRecoveryDecision(state, children, parentId, operationId, parentTurnId) {
11
+ const target = Object.entries(state.child_capacity ?? {}).find(([, intent]) => intent.operation_id === operationId);
12
+ if (!target)
13
+ return "reconciliation_blocked";
14
+ const child = children.find(item => item.session_id === target[0]);
15
+ if (child && (child.parent_session_id !== parentId || child.provenance === "parent_mismatch"))
16
+ return "reconciliation_blocked";
17
+ if (!child)
18
+ return "continue";
19
+ if (parentTurnId && child.continuation_admission?.parent_turn_id === parentTurnId && target[1].phase === "dispatched")
20
+ target[1].parent_turn_id = parentTurnId;
21
+ reconcileChildCapacity(state, [child], parentId);
22
+ if (["native_message_accepted", "successor_observed"].includes(target[1].phase ?? ""))
23
+ return "completed";
24
+ return child.status === "running" || child.terminal_turn_id && child.terminal_turn_id !== target[1].turn_id ? "reconciliation_blocked" : "continue";
25
+ }
26
+ /** Bind native admission to a durable managed parent Turn, never to child prose. */
27
+ export function reconcileChildCapacity(state, children, parentId, observedAt = new Date().toISOString()) {
28
+ let changed = false;
29
+ for (const child of children) {
30
+ const intent = state.child_capacity?.[child.session_id], admission = child.continuation_admission;
31
+ if (!intent?.dispatched || !intent.operation_id || !intent.parent_turn_id || !intent.continuation_ref || !admission
32
+ || child.parent_session_id !== parentId || child.provenance === "parent_mismatch" || admission.parent_session_id !== parentId
33
+ || admission.parent_turn_id !== intent.parent_turn_id || admission.agent_id !== intent.continuation_ref.agent_id)
34
+ continue;
35
+ const bound = { ...admission, operation_id: intent.operation_id, predecessor_turn_id: intent.turn_id };
36
+ child.continuation_admission = bound;
37
+ const successor = nativeContinuationSuccessor(child, intent.turn_id, intent.continuation_ref.ended_at);
38
+ if (admission.proof_kind === "parent_tool_successor" && !successor)
39
+ continue;
40
+ if (admission.proof_kind === "parent_tool_admission" && !nativeContinuationAdmitted(child, intent.turn_id, intent.continuation_ref.ended_at))
41
+ continue;
42
+ const phase = successor ? "successor_observed" : "native_message_accepted";
43
+ if (intent.phase === "successor_observed" && !successor)
44
+ continue;
45
+ if (intent.phase !== phase || JSON.stringify(intent.admission) !== JSON.stringify(bound)) {
46
+ // A refreshed proof digest/status poll is not a new model signal.
47
+ if (intent.phase !== phase || !intent.last_signal_at)
48
+ intent.last_signal_at = observedAt;
49
+ intent.phase = phase;
50
+ intent.admission = bound;
51
+ changed = true;
52
+ }
53
+ }
54
+ return changed;
55
+ }
1
56
  /** Only a proven new successful terminal Turn closes a child's retry episode. */
2
57
  export function closeSuccessfulChildCapacity(state, children, parentId) {
3
58
  const closed = [];
@@ -6,6 +61,9 @@ export function closeSuccessfulChildCapacity(state, children, parentId) {
6
61
  if (!retained || child.parent_session_id !== parentId || child.provenance === "parent_mismatch"
7
62
  || child.status !== "completed" || !child.terminal_turn_id || child.terminal_turn_id === retained.turn_id)
8
63
  continue;
64
+ if (retained.continuation_ref && retained.phase !== "successor_observed")
65
+ continue;
66
+ retained.phase = "settled";
9
67
  delete state.child_capacity[child.session_id];
10
68
  closed.push(child.session_id);
11
69
  }
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs";
2
- import { normalizeNativeChildren, safeNativeFailure } from "../harness-runtime/lib/native-children.mjs";
2
+ import { normalizeNativeChildren, safeNativeFailure, nativeContinuationSuccessor } from "../harness-runtime/lib/native-children.mjs";
3
3
  /** A durable observation is a dispatch fence, not authority to complete Work. */
4
4
  export function retainedChildObserver(input) {
5
5
  let active = true, pending;
@@ -13,6 +13,11 @@ export function retainedChildObserver(input) {
13
13
  return { session_id: child.session_id, parent_session_id: child.parent_session_id, status: "failed", source: child.source, terminal_turn_id: child.terminal_turn_id,
14
14
  ...(typeof child.terminal_observed_at === "string" && Number.isFinite(Date.parse(child.terminal_observed_at)) ? { terminal_observed_at: child.terminal_observed_at } : {}),
15
15
  ...(child.native_source ? { native_source: child.native_source } : {}),
16
+ ...(child.continuation_ref ? { continuation_ref: child.continuation_ref } : {}),
17
+ ...(child.recovery_unavailable_reason ? { recovery_unavailable_reason: child.recovery_unavailable_reason } : {}),
18
+ ...(child.continuation_admission ? { continuation_admission: child.continuation_admission } : {}),
19
+ ...(child.resumed_from_message_id ? { resumed_from_message_id: child.resumed_from_message_id } : {}),
20
+ ...(child.current_turn_id ? { current_turn_id: child.current_turn_id } : {}),
16
21
  ...(child.native_turn_items ? { native_turn_items: {
17
22
  ...(typeof child.native_turn_items.observed === "boolean" ? { observed: child.native_turn_items.observed } : {}),
18
23
  ...(typeof child.native_turn_items.possible_effects === "boolean" ? { possible_effects: child.native_turn_items.possible_effects } : {}),
@@ -46,8 +51,9 @@ export function retainedChildObserver(input) {
46
51
  // This is an authoritative current native snapshot. Older failed Turns
47
52
  // remain in the journal/episode history, not in the current-outcome fence.
48
53
  const resolved = [...retained].filter(([, old]) => children.some(child => child.parent_session_id === input.scope.root_session_id
49
- && child.provenance !== "parent_mismatch" && child.session_id === old.session_id && ["completed", "failed"].includes(child.status)
50
- && child.terminal_turn_id && child.terminal_turn_id !== old.terminal_turn_id)).map(([key]) => key);
54
+ && child.provenance !== "parent_mismatch" && child.session_id === old.session_id
55
+ && (!old.continuation_ref && ["completed", "failed"].includes(child.status) && child.terminal_turn_id && child.terminal_turn_id !== old.terminal_turn_id
56
+ || nativeContinuationSuccessor(child, old.terminal_turn_id, old.continuation_ref?.ended_at)))).map(([key]) => key);
51
57
  if (!failed.length && !resolved.length && !gap)
52
58
  return;
53
59
  const fd = fs.openSync(input.file, "a", 0o600);
@@ -14,7 +14,7 @@ import { inspectDaemonOperation } from "../harness-runtime/lib/daemon-operations
14
14
  import { nativeCommandFingerprint } from "../harness-runtime/lib/native-request-proof.mjs";
15
15
  import { validateMemoryInitStartupScope } from "./memory-init-startup-custody.js";
16
16
  import { continueProviderCapacity } from "./provider-capacity.js";
17
- import { CAPACITY_POLICY, MAX_OVERLOAD_RETRIES, refusalObservation, terminalProviderOverload } from "../harness-runtime/lib/provider-overload-policy.mjs";
17
+ import { RECOVERY_POLICY as CAPACITY_POLICY, MAX_OVERLOAD_RETRIES, recoveryRefusal as refusalObservation, terminalProviderRecovery as terminalProviderOverload } from "../harness-runtime/lib/provider-overload-policy.mjs";
18
18
  const sha = (bytes) => createHash("sha256").update(bytes).digest("hex");
19
19
  function refuse(reason, effect = "no_effect") {
20
20
  throw new AppError("memory_init_native_capacity_required", "Native startup requires its exact qualified probe", 1, { reason, effect, next_action: "reconcile_exact_native_probe" });
@@ -1,6 +1,6 @@
1
1
  import { setTimeout as delay } from "node:timers/promises";
2
2
  import crypto from "node:crypto";
3
- import { CAPACITY_POLICY, MAX_OVERLOAD_RETRIES, capacityBackoff, nativeItemsSettled, normalizeProviderFailure, overloadExhausted, parseCapacityOperation, refusalObservation, terminalProviderOverload, streamRequestKey } from "../harness-runtime/lib/provider-overload-policy.mjs";
3
+ import { RECOVERY_POLICY as CAPACITY_POLICY, MAX_OVERLOAD_RETRIES, classifyRecoveryError, recoveryBackoff as capacityBackoff, nativeItemsSettled, normalizeRecoveryFailure as normalizeProviderFailure, recoveryExhausted as overloadExhausted, parseCapacityOperation, recoveryRefusal as refusalObservation, terminalProviderRecovery as terminalProviderOverload, streamRequestKey } from "../harness-runtime/lib/provider-overload-policy.mjs";
4
4
  import { AppError } from "../shared/errors.js";
5
5
  import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
6
6
  import { parseLifecycleCommand, commandOption } from "./lifecycle-command.js";
@@ -48,12 +48,20 @@ export function ownedProviderBoundary(context, input, boundary) {
48
48
  return boundary.outcome === "completed" && (input.scope === "work" ? rows[0].status === "completed" : stage.status === "done");
49
49
  }
50
50
  export function providerChildFailure(child) {
51
- return child.provider_failure ?? { code: child.native_source ? "provider_capacity_transient" : "turn_interrupted", details: { native_source: child.native_source, provider_session_id: child.session_id, turn_id: child.terminal_turn_id, native_turn_id: child.terminal_turn_id, terminal_status: child.status, observed_at: child.terminal_observed_at ?? null, provider_error: child.provider_error, native_turn_items: child.native_turn_items } };
51
+ return child.provider_failure ?? { code: child.native_source ? classifyRecoveryError(child.provider_error) : "turn_interrupted", details: { native_source: child.native_source, provider_session_id: child.session_id, turn_id: child.terminal_turn_id, native_turn_id: child.terminal_turn_id, terminal_status: child.status, observed_at: child.terminal_observed_at ?? null, provider_error: child.provider_error, native_turn_items: child.native_turn_items } };
52
52
  }
53
53
  export function eligibleProviderChildOverload(child, parentSessionId) {
54
54
  return typeof child.session_id === "string" && child.session_id !== parentSessionId && child.parent_session_id === parentSessionId && child.provenance !== "parent_mismatch" && child.status === "failed" && typeof child.terminal_turn_id === "string" && nativeItemsSettled(child.native_turn_items)
55
55
  && terminalProviderOverload(providerChildFailure(child), child.session_id, child.terminal_turn_id);
56
56
  }
57
+ export function unavailableProviderChildRecovery(child, parentSessionId) {
58
+ if (typeof child.session_id !== "string" || child.parent_session_id !== parentSessionId || child.provenance === "parent_mismatch" || child.status !== "failed"
59
+ || typeof child.terminal_turn_id !== "string" || typeof child.recovery_unavailable_reason !== "string" || !/^[a-z][a-z0-9_]{0,127}$/.test(child.recovery_unavailable_reason))
60
+ return null;
61
+ const primary = providerChildFailure(child);
62
+ return terminalProviderOverload(primary, child.session_id, child.terminal_turn_id)
63
+ ? new AppError("provider_recovery_unavailable", "Native provider refusal is retained, but same-child continuation is not safely supported", 1, { reason: child.recovery_unavailable_reason, child_session_id: child.session_id, terminal_turn_id: child.terminal_turn_id, primary_error: primary }) : null;
64
+ }
57
65
  /** A terminal root is not a settled tree. Only the daemon's exact owned tree
58
66
  * with a settled root and positively live descendants permits waiting. */
59
67
  export function capacityReadiness(seen, sessionId, operationId, turnId, items, allowDescendants = false, requestProof) {
@@ -227,7 +235,7 @@ export async function continueProviderCapacity(input) {
227
235
  while (["prepared", "dispatching", "running", "awaiting_provider", "outcome_unknown"].includes(String(observed.state))) {
228
236
  input.signal?.throwIfAborted();
229
237
  input.assertCurrent();
230
- const boundary = await observe(async () => await input.decision?.() ?? "continue", intent.operation_id);
238
+ const boundary = await observe(async () => await input.decision?.(undefined, error) ?? "continue", intent.operation_id);
231
239
  if (boundary === "completed" || boundary === "paused")
232
240
  return { provider_session_id: input.sessionId, capacity_decision: boundary };
233
241
  if (boundary === "reconciliation_blocked")
@@ -246,7 +254,7 @@ export async function continueProviderCapacity(input) {
246
254
  if (observed.state === "completed" && observed.result && typeof observed.result === "object") {
247
255
  input.signal?.throwIfAborted();
248
256
  input.assertCurrent();
249
- const boundary = await observe(async () => await input.decision?.() ?? "continue", intent.operation_id);
257
+ const boundary = await observe(async () => await input.decision?.(undefined, error) ?? "continue", intent.operation_id);
250
258
  input.signal?.throwIfAborted();
251
259
  input.assertCurrent();
252
260
  if (boundary === "reconciliation_blocked")
@@ -266,7 +274,7 @@ export async function continueProviderCapacity(input) {
266
274
  input.assertCurrent();
267
275
  const normalized = normalizeProviderFailure(error, input.sessionId);
268
276
  const candidate = normalized?.request_proof.kind === "stream_request" ? normalized.primary_error.details?.recovered_error_boundary : undefined;
269
- const decision = await observe(async () => await input.decision?.(candidate?.receipt) ?? "continue", observedOperationId);
277
+ const decision = await observe(async () => await input.decision?.(candidate?.receipt, error) ?? "continue", observedOperationId);
270
278
  input.signal?.throwIfAborted();
271
279
  input.assertCurrent();
272
280
  if (normalized?.request_proof.kind === "stream_request" && ["completed", "paused"].includes(decision) && !candidate?.receipt)
@@ -445,11 +453,11 @@ export async function continueProviderCapacity(input) {
445
453
  // A task can complete/pause while the daemon queues for its permit.
446
454
  // Admission then refuses the stale prompt; accept that owner decision
447
455
  // before treating the transport's rejection as another provider failure.
456
+ error = nextError;
457
+ operationId = nextId;
448
458
  const finished = await decide(nextId);
449
459
  if (finished)
450
460
  return finished;
451
- error = nextError;
452
- operationId = nextId;
453
461
  }
454
462
  }
455
463
  }
@@ -7,7 +7,10 @@ import { assertSourceRepairEligible } from "./code-checks.js";
7
7
  export function ensureAggregateRepair(context, input, create) {
8
8
  if (!input.failures.length)
9
9
  throw new AppError("repair_intent_invalid", "Repair requires candidate-source-backed failed receipts", 1);
10
- assertSourceRepairEligible(input.failures);
10
+ const workspace = context.db.get("SELECT workspace_root FROM runs WHERE project_id=? AND id=?", [input.projectId, input.runId]);
11
+ if (!workspace?.workspace_root && input.failures.some(receipt => receipt.candidate_library_inputs?.length))
12
+ throw new AppError("repair_intent_invalid", "Library-backed repair requires the current RUN workspace", 1);
13
+ assertSourceRepairEligible(input.failures, workspace?.workspace_root);
11
14
  const causes = input.failures.map(receipt => ({ id: receipt.id, epoch: receipt.verification_epoch, fingerprint: receipt.fingerprint_scope === "check_inputs" ? receipt.before_fingerprint : receipt.workspace_fingerprint })).sort((a, b) => a.id.localeCompare(b.id));
12
15
  return ensureRepairIntent(context, { ...input, causes: { receipts: causes, origins: [...(input.origins ?? [])].sort() } }, create);
13
16
  }
@@ -6,7 +6,7 @@ import { adapterSessionId, runHarnessAdapter } from "./harness-adapter.js";
6
6
  import { runtimeHarness } from "./execution-policy.js";
7
7
  import { appendEvent, digest, digestBytes, errorRecord, requireRun } from "./run-controller-state.js";
8
8
  import { managedProcessStatus } from "./managed-processes.js";
9
- import { CAPACITY_POLICY, parseCapacityOperation } from "../harness-runtime/lib/provider-overload-policy.mjs";
9
+ import { RECOVERY_POLICY as CAPACITY_POLICY, parseCapacityOperation } from "../harness-runtime/lib/provider-overload-policy.mjs";
10
10
  import { recoveryObservationBudget } from "./recovery-observation-budget.js";
11
11
  import { canonicalNativeOperation } from "../harness-runtime/lib/adapter-timeouts.mjs";
12
12
  import { retainedDaemonReply } from "../harness-runtime/lib/daemon-observation.mjs";
@@ -45,14 +45,14 @@ import { assertNoCorrelatedRejection } from "./continuation-outcome.js";
45
45
  import { spawnController, recordControllerLauncher, claimControllerProcess, markControllerDispatchReady, assertControllerLeaseAllowed } from "./run-controller-process.js";
46
46
  import { controllerAdapter, controllerEnvironment, sessionArguments } from "./run-controller-adapter.js";
47
47
  import { retainedChildObserver } from "./controller-child-observer.js";
48
- import { capacityDelay, continueProviderCapacity, eligibleProviderChildOverload, providerChildFailure, ownedProviderBoundary } from "./provider-capacity.js";
48
+ import { capacityDelay, continueProviderCapacity, eligibleProviderChildOverload, providerChildFailure, ownedProviderBoundary, unavailableProviderChildRecovery } from "./provider-capacity.js";
49
49
  import { resumeControllerSession, failControllerRecovery } from "./run-controller-recovery.js";
50
50
  import { captureControllerBoundary } from "./run-controller-capture.js";
51
51
  import { requestRunControl } from "./run-control-worker.js";
52
52
  import { storageSessionId } from "./session-identity.js";
53
- import { CAPACITY_POLICY, nativeItemsSettled, overloadExhausted, refusalObservation, terminalProviderOverload } from "../harness-runtime/lib/provider-overload-policy.mjs";
53
+ import { RECOVERY_POLICY as CAPACITY_POLICY, nativeItemsSettled, recoveryExhausted as overloadExhausted, recoveryRefusal as refusalObservation, terminalProviderRecovery as terminalProviderOverload, normalizeRecoveryFailure } from "../harness-runtime/lib/provider-overload-policy.mjs";
54
54
  import { renderNativeChildFollowupInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
55
- import { closeSuccessfulChildCapacity } from "./controller-child-capacity.js";
55
+ import { childCapacityDispatchAllowed, childCapacityRecoveryDecision, closeSuccessfulChildCapacity, reconcileChildCapacity } from "./controller-child-capacity.js";
56
56
  import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
57
57
  export { resumeRunController } from "./run-controller-recovery.js";
58
58
  const preparedLaunchInputs = new WeakMap();
@@ -754,7 +754,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
754
754
  throw new AppError("capacity_continuation_conflict", "Retained capacity root has no failure receipt", 1);
755
755
  throw JSON.parse(root.error_json);
756
756
  }
757
- if (childObserver(session)?.failed().length)
757
+ const failedChildren = childObserver(session)?.failed() ?? [];
758
+ if (failedChildren.length && !(kind === "child" && childCapacityDispatchAllowed(state, failedChildren, session.id, firstOperationId, digestBytes(text))))
758
759
  throw new AppError("fanout_dispatch_fenced", "A retained child failure forbids another coordinator dispatch", 1);
759
760
  promptPending = true;
760
761
  observedPromptOperation = firstOperationId;
@@ -785,15 +786,15 @@ async function executeController(context, row, manifest, state, assertOwnership,
785
786
  assertCurrent: () => {
786
787
  assertOwner();
787
788
  },
788
- decision: async (boundary) => {
789
+ decision: async (boundary, currentFailure) => {
789
790
  if (kind === "child") {
790
- const target = Object.entries(state.child_capacity ?? {}).find(([, retained]) => retained.operation_id === firstOperationId);
791
- if (!target)
792
- return "reconciliation_blocked";
793
791
  const seen = await inspect(["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], `${firstOperationId}:child-followup-observe`);
794
- const child = controllerNativeChildren(seen, session.id).find(item => item.session_id === target[0]);
795
- if (child?.parent_session_id === session.id && (child.status === "running" || child.terminal_turn_id && child.terminal_turn_id !== target[1].turn_id))
796
- return "completed";
792
+ const parent = normalizeRecoveryFailure(currentFailure, session.id);
793
+ const decision = childCapacityRecoveryDecision(state, controllerNativeChildren(seen, session.id), session.id, firstOperationId, parent?.request_proof.kind === "native_turn" ? parent.turn_id : undefined);
794
+ if (decision !== "continue") {
795
+ saveState(context, row, state);
796
+ return decision;
797
+ }
797
798
  }
798
799
  const view = getFlowRunStatus(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id });
799
800
  const next = view.continuation;
@@ -1081,7 +1082,32 @@ async function executeController(context, row, manifest, state, assertOwnership,
1081
1082
  const session = state.sessions[state.current_session];
1082
1083
  const childHarness = runtimeHarness(session.profile);
1083
1084
  const baseline = new Set(state.native_baseline ?? []);
1085
+ for (const intent of Object.values(state.child_capacity ?? {})) {
1086
+ if (!intent.dispatched || !intent.operation_id || intent.parent_turn_id || !intent.prompt_sha256)
1087
+ continue;
1088
+ let saved = context.db.get("SELECT input_json,receipt_json,status FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [intent.operation_id, row.controller_id, row.generation]);
1089
+ if (!saved)
1090
+ continue;
1091
+ const dispatch = JSON.parse(saved.input_json);
1092
+ if (!saved.receipt_json && saved.status === "outcome_unknown" && dispatch.dispatch_phase !== "prepared" && dispatch.prompt_sha256 === intent.prompt_sha256 && Array.isArray(dispatch.args)) {
1093
+ // Existing adapter recovery reads its durable operation; it cannot
1094
+ // redispatch a productive operation past external_dispatch_possible.
1095
+ await controllerAdapter(executionContext, row, manifest, session, dispatch.args, assertOwner, intent.operation_id, cancellation.signal, renewLease);
1096
+ saved = context.db.get("SELECT input_json,receipt_json,status FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [intent.operation_id, row.controller_id, row.generation]);
1097
+ }
1098
+ if (!saved?.receipt_json)
1099
+ continue;
1100
+ const receipt = JSON.parse(saved.receipt_json);
1101
+ if ((saved.status === "completed" || dispatch.dispatch_phase === "receipt_observed") && dispatch.prompt_sha256 === intent.prompt_sha256
1102
+ && receipt.provider_session_id === session.id && receipt.turn?._meta?.zcode?.ddFlowRecovery?.operation_id === intent.operation_id
1103
+ && typeof receipt.turn._meta.zcodeNativeTurnId === "string" && receipt.turn._meta.zcodeNativeTurnId) {
1104
+ intent.parent_turn_id = receipt.turn._meta.zcodeNativeTurnId;
1105
+ saveState(context, row, state);
1106
+ }
1107
+ }
1084
1108
  let children = controllerNativeChildren(state.last_receipt ?? {}, session.id).filter(child => !baseline.has(child.session_id));
1109
+ if (reconcileChildCapacity(state, children, session.id))
1110
+ saveState(context, row, state);
1085
1111
  childObserver(session)?.observeSettledChildren(children);
1086
1112
  const retainedFailures = childObserver(session)?.failed() ?? [];
1087
1113
  children = controllerNativeChildren({ descendants: [...children, ...retainedFailures] }, session.id);
@@ -1091,7 +1117,35 @@ async function executeController(context, row, manifest, state, assertOwnership,
1091
1117
  saveState(context, row, state);
1092
1118
  appendEvent(context, row.controller_id, "capacity_child_episodes_completed", { child_session_ids: closedChildEpisodes });
1093
1119
  });
1094
- if (children.some(child => child.status === "failed" && renderNativeChildFollowupInstruction(childHarness, child.session_id))) {
1120
+ const queuedChild = children.find(child => state.child_capacity?.[child.session_id]?.phase === "native_message_accepted"
1121
+ && (child.status === "failed" && state.child_capacity[child.session_id].turn_id === child.terminal_turn_id
1122
+ || child.status === "running" && child.continuation_admission?.proof_kind === "parent_tool_admission"));
1123
+ if (queuedChild) {
1124
+ const observedAt = Date.parse(state.child_capacity[queuedChild.session_id].last_signal_at ?? "");
1125
+ if (!Number.isFinite(observedAt) || Date.now() - observedAt >= 600_000)
1126
+ throw new AppError("operation_observation_lost", "Accepted native child continuation has no new observed signal", 1, { child_session_id: queuedChild.session_id });
1127
+ setStatus("waiting_for_children");
1128
+ await delay(1000, undefined, { signal: cancellation.signal });
1129
+ state.last_receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1130
+ saveState(context, row, state);
1131
+ continue;
1132
+ }
1133
+ const recoveryUnavailable = children.map(child => {
1134
+ const links = 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 })]);
1135
+ return links.length > 1 || links.some(link => link.status !== "running") ? null : unavailableProviderChildRecovery(child, session.id);
1136
+ }).find(Boolean);
1137
+ if (recoveryUnavailable) {
1138
+ if (children.every(child => ["completed", "failed", "cancelled", "settled_by_root"].includes(child.status)))
1139
+ throw recoveryUnavailable;
1140
+ // A blocked child does not cancel healthy siblings. Observe their own
1141
+ // native completion before reporting the unavailable continuation.
1142
+ setStatus("waiting_for_children");
1143
+ await delay(1000, undefined, { signal: cancellation.signal });
1144
+ state.last_receipt = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1145
+ saveState(context, row, state);
1146
+ continue;
1147
+ }
1148
+ if (children.some(child => child.status === "failed" && renderNativeChildFollowupInstruction(childHarness, child))) {
1095
1149
  const inspected = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1096
1150
  state.last_receipt = inspected;
1097
1151
  saveState(context, row, state);
@@ -1104,6 +1158,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
1104
1158
  if (linked.length > 1 || linked.some(link => link.status !== "running"))
1105
1159
  continue;
1106
1160
  const retained = state.child_capacity?.[failedChild.session_id];
1161
+ if (retained?.policy && retained.policy !== CAPACITY_POLICY)
1162
+ throw new AppError("capacity_policy_mismatch", "Retained child episode requires its original pinned recovery policy", 1);
1107
1163
  const refusals = [...(retained?.refusals ?? [])];
1108
1164
  if (refusals.some(item => item.turn_id === failedChild.terminal_turn_id))
1109
1165
  continue;
@@ -1114,11 +1170,11 @@ async function executeController(context, row, manifest, state, assertOwnership,
1114
1170
  await prepareMutation(() => { saveState(context, row, state); appendEvent(context, row.controller_id, "capacity_child_refusal", { child_session_id: failedChild.session_id, refusal: refusals.at(-1) }); });
1115
1171
  const exhausted = overloadExhausted(refusals);
1116
1172
  if (exhausted)
1117
- 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 });
1173
+ throw new AppError("provider_unavailable", "Native child exhausted its provider continuation attempts", 1, { ...exhausted, child_session_id: failedChild.session_id, primary_error: nativeFailure });
1118
1174
  }
1119
1175
  // Continue one existing child at a time; another failed child does not
1120
1176
  // create a second fanout wave or share this child's refusal counter.
1121
- const child = children.every(item => item.status === "failed" || item.status === "completed") ? failed.find(item => renderNativeChildFollowupInstruction(childHarness, item.session_id) && eligibleProviderChildOverload(item, session.id)) : undefined;
1177
+ const child = children.every(item => item.status === "failed" || item.status === "completed") ? failed.find(item => renderNativeChildFollowupInstruction(childHarness, item) && eligibleProviderChildOverload(item, session.id)) : undefined;
1122
1178
  const turnId = child?.terminal_turn_id;
1123
1179
  const native = child && turnId ? providerChildFailure(child) : null;
1124
1180
  const prior = child && state.child_capacity?.[child.session_id];
@@ -1143,7 +1199,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
1143
1199
  }
1144
1200
  const exhausted = overloadExhausted(refusals);
1145
1201
  if (exhausted)
1146
- 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 });
1202
+ throw new AppError("provider_unavailable", "Native child exhausted its provider continuation attempts", 1, { ...exhausted, child_session_id: child.session_id, primary_error: native });
1147
1203
  let wait;
1148
1204
  try {
1149
1205
  wait = prepared ? Math.max(0, Date.parse(prepared.not_before) - Date.now()) : capacityDelay(native, attempt);
@@ -1157,7 +1213,8 @@ async function executeController(context, row, manifest, state, assertOwnership,
1157
1213
  const rootOperationId = prior?.root_operation_id ?? `${row.controller_id}:child:${digest([child.session_id, turnId]).slice(0, 16)}`;
1158
1214
  const operationId = prepared?.operation_id ?? `${rootOperationId}:followup:${attempt}`;
1159
1215
  state.child_capacity ??= {};
1160
- state.child_capacity[child.session_id] = { turn_id: turnId, attempts: attempt, operation_id: operationId, root_operation_id: rootOperationId, policy: CAPACITY_POLICY, refusals, not_before: prepared?.not_before ?? new Date(Date.now() + wait).toISOString(), dispatched: false };
1216
+ const followupText = renderNativeChildFollowupInstruction(childHarness, child);
1217
+ state.child_capacity[child.session_id] = { turn_id: turnId, attempts: attempt, operation_id: operationId, root_operation_id: rootOperationId, policy: CAPACITY_POLICY, refusals, not_before: prepared?.not_before ?? new Date(Date.now() + wait).toISOString(), dispatched: false, phase: "prepare", prompt_sha256: digestBytes(followupText), ...(child.continuation_ref ? { continuation_ref: child.continuation_ref } : {}) };
1161
1218
  await prepareMutation(() => { saveState(context, row, state); appendEvent(context, row.controller_id, "capacity_child_followup", { child_session_id: child.session_id, turn_id: turnId, attempt, operation_id: operationId }); });
1162
1219
  const until = performance.now() + wait;
1163
1220
  while (performance.now() < until) {
@@ -1186,15 +1243,27 @@ async function executeController(context, row, manifest, state, assertOwnership,
1186
1243
  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 });
1187
1244
  await renewLease();
1188
1245
  assertOwner();
1189
- const text = renderNativeChildFollowupInstruction(childHarness, child.session_id);
1246
+ const text = renderNativeChildFollowupInstruction(childHarness, child);
1247
+ // Fence dispatch before awaiting the parent: a lost ack must never
1248
+ // resend SendMessage into a child which may already be running.
1249
+ state.child_capacity[child.session_id].dispatched = true;
1250
+ state.child_capacity[child.session_id].phase = "dispatched";
1251
+ saveState(context, row, state);
1190
1252
  await prompt(session, text, operationId, "child");
1253
+ const parentTurnId = state.last_receipt?.turn?._meta?.zcodeNativeTurnId;
1254
+ if (typeof parentTurnId === "string" && parentTurnId)
1255
+ state.child_capacity[child.session_id].parent_turn_id = parentTurnId;
1191
1256
  state.child_capacity[child.session_id].dispatched = true;
1192
1257
  saveState(context, row, state);
1193
1258
  const after = await call(session, ["session", "inspect", "--session-id", session.id, ...sessionArguments(context, manifest, session)], false);
1259
+ const afterChildren = controllerNativeChildren(after, session.id);
1260
+ reconcileChildCapacity(state, afterChildren, session.id);
1261
+ if (after.descendants)
1262
+ after.descendants = afterChildren;
1194
1263
  state.last_receipt = after;
1195
1264
  saveState(context, row, state);
1196
1265
  const observed = controllerNativeChildren(after, session.id).find(item => item.session_id === child.session_id);
1197
- if (!observed || observed.status === "failed" && observed.terminal_turn_id === turnId)
1266
+ if (!observed || observed.status === "failed" && observed.terminal_turn_id === turnId && state.child_capacity[child.session_id].phase !== "native_message_accepted")
1198
1267
  throw new AppError("capacity_child_followup_unconfirmed", "Native child did not advance to a new Turn after parent followup", 1, { child_session_id: child.session_id, turn_id: turnId });
1199
1268
  continue;
1200
1269
  }
@@ -1,6 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import { AppError } from "../shared/errors.js";
3
- import { CAPACITY_POLICY, MAX_OVERLOAD_RETRIES, overloadExhausted } from "../harness-runtime/lib/provider-overload-policy.mjs";
3
+ import { RECOVERY_POLICY as CAPACITY_POLICY, MAX_OVERLOAD_RETRIES, recoveryExhausted as overloadExhausted } from "../harness-runtime/lib/provider-overload-policy.mjs";
4
4
  import { assertRunStartupSettled } from "./run-start-custody.js";
5
5
  export function recoveryPacketReceipt(binding) {
6
6
  if (!binding.packet_json || !binding.packet_sha256 || crypto.createHash("sha256").update(binding.packet_json).digest("hex") !== binding.packet_sha256)
@@ -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, MAX_OVERLOAD_RETRIES, overloadExhausted } from "../harness-runtime/lib/provider-overload-policy.mjs";
11
+ import { RECOVERY_POLICY as CAPACITY_POLICY, MAX_OVERLOAD_RETRIES, recoveryExhausted as 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))
@@ -16,6 +16,16 @@ function assertContextRelocation(receipt) {
16
16
  || !Array.isArray(receipt.path_mapping) || receipt.path_mapping.some(item => !item || typeof item.from !== "string" || typeof item.to !== "string" || !path.isAbsolute(item.from) || !path.isAbsolute(item.to)))
17
17
  throw new AppError("context_relocation_invalid", "Stage context relocation lacks a verified import binding", 2);
18
18
  }
19
+ function relocateDeclaredPath(source, receipts) {
20
+ let mapped = source;
21
+ for (const receipt of receipts) {
22
+ assertContextRelocation(receipt);
23
+ const selected = [...receipt.path_mapping].sort((a, b) => b.from.length - a.from.length).find(item => mapped === item.from || mapped.startsWith(`${item.from}${path.sep}`));
24
+ if (selected)
25
+ mapped = path.join(selected.to, path.relative(selected.from, mapped));
26
+ }
27
+ return mapped;
28
+ }
19
29
  /** Physical packet locators may move; accepted decision/provenance blocks may not. */
20
30
  export function projectPacketLocators(text, receipts) {
21
31
  receipts.forEach(assertContextRelocation);
@@ -89,13 +99,7 @@ export function renderRetainedStageContext(input) {
89
99
  || !/^[a-f0-9]{64}$/.test(input.workspaceView.source_sha256) || !/^[a-f0-9]{64}$/.test(input.workspaceView.copy_sha256)))
90
100
  throw new AppError("context_workspace_view_invalid", "Stage context workspace view lacks its bound review-copy proof", 2);
91
101
  const mapPath = (source, workspaceSource = true) => {
92
- let mapped = source;
93
- for (const receipt of input.receipts) {
94
- assertContextRelocation(receipt);
95
- const selected = [...receipt.path_mapping].sort((a, b) => b.from.length - a.from.length).find(item => mapped === item.from || mapped.startsWith(`${item.from}${path.sep}`));
96
- if (selected)
97
- mapped = path.join(selected.to, path.relative(selected.from, mapped));
98
- }
102
+ let mapped = relocateDeclaredPath(source, input.receipts);
99
103
  const view = input.workspaceView;
100
104
  if (view && workspaceSource && (mapped === view.from || mapped.startsWith(`${view.from}${path.sep}`)))
101
105
  mapped = path.join(view.to, path.relative(view.from, mapped));
@@ -149,6 +153,7 @@ export function loadExternalStageContextBytes(input) {
149
153
  return { value, sourcePath, materializedSha256, bytes };
150
154
  }
151
155
  export function prepareExternalStageContext(input) {
156
+ verifyImmutableContextSources(input.loaded.value);
152
157
  const destination = externalStageContextDestination(input);
153
158
  if (!fs.existsSync(destination))
154
159
  prepareOutputFile(destination);
@@ -206,6 +211,7 @@ export function applyExternalStageContext(input) {
206
211
  return installed;
207
212
  }
208
213
  export function renderExternalStageContext(input) {
214
+ verifyImmutableContextSources(input.loaded.value);
209
215
  const value = input.loaded.value;
210
216
  const sources = value.sources ?? [];
211
217
  const taskInput = value.task_input ?? [];
@@ -251,7 +257,7 @@ function validateContext(value, expectedStage) {
251
257
  for (const item of value[key]) {
252
258
  const valid = key === "accepted_decisions" ? text(item) || object(item) && (text(item.id) || text(item.statement)) && [item.id, item.statement].every(field => field === undefined || text(field))
253
259
  : object(item) && text(item.role) && (key === "dynamic_roles" ? item.reason === undefined || text(item.reason)
254
- : text(item.path) && (key === "sources" ? text(item.reason) && (item.required === undefined || typeof item.required === "boolean") && (item.root === undefined || typeof item.root === "string" && ["project", "workspace", "run"].includes(item.root)) : item.sha256 === undefined || text(item.sha256)));
260
+ : text(item.path) && (key === "sources" ? text(item.reason) && (item.sha256 === undefined || typeof item.sha256 === "string" && /^[a-f0-9]{64}$/.test(item.sha256)) && (item.required === undefined || typeof item.required === "boolean") && (item.root === undefined || typeof item.root === "string" && ["project", "workspace", "run"].includes(item.root)) : item.sha256 === undefined || text(item.sha256)));
255
261
  if (!valid)
256
262
  throw new AppError("validation", `Invalid stage context ${key} item`, 2, { parameter: key });
257
263
  }
@@ -261,4 +267,46 @@ function validateContext(value, expectedStage) {
261
267
  throw new AppError("validation", `Stage context ${key} must be a nonempty string`, 2);
262
268
  }
263
269
  }
270
+ function verifyImmutableContextSources(value) {
271
+ for (const source of value.sources ?? []) {
272
+ if (!source.sha256)
273
+ continue; // Ordinary mutable project documents are not frozen.
274
+ try {
275
+ const info = fs.lstatSync(source.path);
276
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 2 * 1024 * 1024 || sha256(fs.readFileSync(source.path)) !== source.sha256)
277
+ throw new Error("changed");
278
+ }
279
+ catch {
280
+ throw new AppError("context_source_checksum_mismatch", "Immutable stage source is unavailable or changed", 2, { role: source.role });
281
+ }
282
+ }
283
+ }
284
+ /** Only an accepted CODE context may bind a diagnostic capability to checks. */
285
+ export function browserCheckCapabilityEnvironment(context, runId, runHome) {
286
+ const row = context.db.get("SELECT project_id, index_json FROM runs WHERE id = ?", [runId]);
287
+ const ref = row ? JSON.parse(row.index_json).execution_profile?.stage_context_inputs?.code : null;
288
+ if (!ref)
289
+ return {};
290
+ const file = path.resolve(runHome, ref.path), relative = path.relative(runHome, file);
291
+ if (relative.startsWith('..') || path.isAbsolute(relative))
292
+ throw new AppError('context_source_checksum_mismatch', 'Stage context escaped RUN', 2);
293
+ const loaded = loadExternalStageContext({ file, expectedSha256: ref.sha256, stage: 'code' });
294
+ const sources = loaded.value.sources?.filter(item => item.role === 'browser_environment_capabilities' && item.root === 'run') ?? [];
295
+ if (!sources.length)
296
+ return {};
297
+ if (sources.length !== 1)
298
+ throw new AppError('context_source_checksum_mismatch', 'Browser capability source is ambiguous', 2);
299
+ const source = { ...sources[0], path: relocateDeclaredPath(sources[0].path, runContextRelocations(context, row.project_id, runId)) };
300
+ const filename = path.basename(source.path), namedHash = filename.match(/^browser-capabilities(?:-([a-f0-9]{64}))?\.json$/);
301
+ const expected = path.join(runHome, 'environment', filename);
302
+ let owned = false;
303
+ try {
304
+ owned = fs.realpathSync(source.path) === path.join(fs.realpathSync(runHome), 'environment', filename);
305
+ }
306
+ catch { /* Missing proof is not a foreign host lookup. */ }
307
+ if (!source.sha256 || !namedHash || namedHash[1] && namedHash[1] !== source.sha256 || path.resolve(source.path) !== expected || !owned)
308
+ throw new AppError('context_source_checksum_mismatch', 'Browser capability lacks its RUN binding', 2);
309
+ verifyImmutableContextSources({ ...loaded.value, sources: [source] });
310
+ return { DD_FLOW_BROWSER_CAPABILITY_PATH: source.path, DD_FLOW_BROWSER_CAPABILITY_SHA256: source.sha256 };
311
+ }
264
312
  function sha256(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
@@ -253,7 +253,7 @@ export async function finishVnextCodeReview(context, input) {
253
253
  const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", receipts, semanticFile: decisionFile });
254
254
  const failed = receipts.filter((receipt) => receipt.status !== "passed");
255
255
  if (failed.length) {
256
- assertSourceRepairEligible(failed);
256
+ assertSourceRepairEligible(failed, run.workspace_root);
257
257
  context.db.writeTransaction(() => acceptVerificationMatrix(context, matrix));
258
258
  publishVerificationMatrix(context, matrix);
259
259
  throw new AppError("code_review_gate_failed", "CODE-REVIEW repair changed the project but the aggregate gate failed", 2, {
@@ -225,7 +225,7 @@ export async function finishVnextCode(context, input) {
225
225
  const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", receipts, semanticFile: input.verificationFile });
226
226
  const matrixInstruction = matrix ? verificationMatrixInstruction(matrix.metadata, home, "semantic verification in code-verification.json") : "";
227
227
  if (failed.length) {
228
- assertSourceRepairEligible(failed);
228
+ assertSourceRepairEligible(failed, run.workspace_root);
229
229
  context.db.writeTransaction(() => acceptVerificationMatrix(context, matrix));
230
230
  publishVerificationMatrix(context, matrix);
231
231
  const continuation = registerAggregateCodeRepair(context, { projectRoot, runId: run.id, stage, failures: failed, checks, generation });
@@ -408,8 +408,9 @@ export function prepareVnextCodeRepair(context, input) {
408
408
  check_receipt_id: input.checkReceiptId
409
409
  });
410
410
  }
411
+ // Aggregate registration validated current inputs before its SQL transaction.
411
412
  if (input.checkReceiptId)
412
- assertSourceRepairEligible(checkReceipts(context, { projectId: project.id, runId: run.id }).filter(item => item.id === input.checkReceiptId));
413
+ assertSourceRepairEligible(checkReceipts(context, { projectId: project.id, runId: run.id }).filter(item => item.id === input.checkReceiptId), input.registrationOwned ? undefined : run.workspace_root);
413
414
  const origins = [...new Set(input.originWorkIds)].map((id) => requireCodeWork(context, project.id, run.id, id));
414
415
  const packets = origins.map((work) => packet(work));
415
416
  const invariantPackets = receipt || input.semanticUnresolved?.length
@@ -305,7 +305,7 @@ export async function reconcileVnextFanout(context, input) {
305
305
  continue;
306
306
  }
307
307
  const link = active[0];
308
- if (renderNativeChildFollowupInstruction(String(value.harness_id), String(child.session_id)) && eligibleProviderChildOverload(child, value.parent_session_id)) {
308
+ if (renderNativeChildFollowupInstruction(String(value.harness_id), child) && eligibleProviderChildOverload(child, value.parent_session_id)) {
309
309
  issues.push({ code: "child_capacity_pending", work_id: link.work_id, session_id: child.session_id });
310
310
  continue;
311
311
  }
@@ -479,7 +479,7 @@ export async function repairVnextMerge(context, input) {
479
479
  if (request.project_id !== project.id || request.status !== "action_required" || errorCode(request) !== "merge_gate_failed") {
480
480
  throw new AppError("invalid_merge_repair_state", "MERGE source repair requires an action-required failed integration gate", 2, { merge_request_id: input.requestId, status: request.status, code: errorCode(request) });
481
481
  }
482
- assertSourceRepairEligible(checkReceipts(context, { projectId: project.id, runId: request.run_id }).filter(receipt => failedReceiptIds(request).includes(receipt.id)));
482
+ assertSourceRepairEligible(checkReceipts(context, { projectId: project.id, runId: request.run_id }).filter(receipt => failedReceiptIds(request).includes(receipt.id)), request.target_workspace);
483
483
  abortIntegration(request);
484
484
  const originWorkIds = sourceCodeWorkIds(context, request.project_id, request.run_id);
485
485
  if (!originWorkIds.length)
@@ -1564,7 +1564,7 @@ async function settle(context, id, status, result, progress, retry, memoryResult
1564
1564
  receipts = await runCodeChecks(context, { projectId: work.project_id, runId: work.run_id, runHome: requireRunHome(run), workspaceRoot: run.workspace_root, workId: work.work_id, artifactDir, scope: "work", checks: requiredChecks, ...(retry ? { retryReceiptId: retry.receiptId, retryReason: retry.reason } : {}), ...(progress ? { progress } : {}) });
1565
1565
  const failed = receipts.filter((receipt) => receipt.status !== "passed");
1566
1566
  if (failed.length)
1567
- assertSourceRepairEligible(failed);
1567
+ assertSourceRepairEligible(failed, run.workspace_root);
1568
1568
  if (failed.length)
1569
1569
  throw new AppError("work_checks_failed", "Work remains running because required checks failed", 2, { work_id: id, failures: failed, all_receipts: receipts });
1570
1570
  }