@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.
- package/CHANGELOG.md +12 -0
- package/README.md +17 -22
- package/dist/build-info.json +6 -6
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +26 -5
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +67 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +10 -14
- package/dist/harness-runtime/lib/dd-agy.mjs +3 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +62 -10
- package/dist/harness-runtime/lib/dd-opencode.mjs +32 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/dd-zcode.mjs +37 -12
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +13 -4
- package/dist/harness-runtime/lib/model-observations.mjs +19 -7
- package/dist/harness-runtime/lib/native-children.d.mts +12 -1
- package/dist/harness-runtime/lib/native-children.mjs +47 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +9 -1
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +97 -6
- package/dist/services/check-runner-evidence.js +54 -309
- package/dist/services/check-runner-proof.js +373 -0
- package/dist/services/code-checks.js +31 -12
- package/dist/services/controller-child-capacity.js +58 -0
- package/dist/services/controller-child-observer.js +9 -3
- package/dist/services/memory-init-native-capacity.js +1 -1
- package/dist/services/provider-capacity.js +15 -7
- package/dist/services/repair-intents.js +4 -1
- package/dist/services/run-controller-adapter.js +1 -1
- package/dist/services/run-controller.js +87 -18
- package/dist/services/run-recovery-guard.js +1 -1
- package/dist/services/run-recovery-runtime.js +1 -1
- package/dist/services/stage-context.js +56 -8
- package/dist/services/vnext-code-review.js +1 -1
- package/dist/services/vnext-code.js +3 -2
- package/dist/services/vnext-fanout.js +1 -1
- package/dist/services/vnext-merge.js +1 -1
- package/dist/services/work-registry.js +1 -1
- package/dist/verification-components.json +299 -0
- package/package.json +7 -6
- 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
|
|
50
|
-
&& child.terminal_turn_id && child.terminal_turn_id !== old.terminal_turn_id
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
795
|
-
|
|
796
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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),
|
|
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
|
}
|