audit-tools 0.33.3 → 0.33.6
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/dist/audit/cli/args.d.ts +1 -1
- package/dist/audit/cli/args.d.ts.map +1 -1
- package/dist/audit/cli/args.js +1 -4
- package/dist/audit/cli/args.js.map +1 -1
- package/dist/audit/cli/dispatch/hostFanoutGate.d.ts +7 -0
- package/dist/audit/cli/dispatch/hostFanoutGate.d.ts.map +1 -1
- package/dist/audit/cli/dispatch/hostFanoutGate.js +18 -4
- package/dist/audit/cli/dispatch/hostFanoutGate.js.map +1 -1
- package/dist/audit/cli/dispatch/quotaPool.d.ts +12 -0
- package/dist/audit/cli/dispatch/quotaPool.d.ts.map +1 -1
- package/dist/audit/cli/dispatch/quotaPool.js +59 -31
- package/dist/audit/cli/dispatch/quotaPool.js.map +1 -1
- package/dist/audit/cli/dispatch/types.d.ts +7 -0
- package/dist/audit/cli/dispatch/types.d.ts.map +1 -1
- package/dist/audit/cli/dispatch.d.ts +19 -0
- package/dist/audit/cli/dispatch.d.ts.map +1 -1
- package/dist/audit/cli/dispatch.js +46 -8
- package/dist/audit/cli/dispatch.js.map +1 -1
- package/dist/audit/cli/hybridDispatch.d.ts +16 -3
- package/dist/audit/cli/hybridDispatch.d.ts.map +1 -1
- package/dist/audit/cli/hybridDispatch.js +16 -23
- package/dist/audit/cli/hybridDispatch.js.map +1 -1
- package/dist/audit/cli/nextStepCommand.d.ts +1 -1
- package/dist/audit/cli/nextStepCommand.d.ts.map +1 -1
- package/dist/audit/cli/nextStepCommand.js +32 -18
- package/dist/audit/cli/nextStepCommand.js.map +1 -1
- package/dist/audit/cli/nextStepHelpers.d.ts.map +1 -1
- package/dist/audit/cli/nextStepHelpers.js +74 -51
- package/dist/audit/cli/nextStepHelpers.js.map +1 -1
- package/dist/audit/cli/reviewRun.d.ts +1 -0
- package/dist/audit/cli/reviewRun.d.ts.map +1 -1
- package/dist/audit/cli/reviewRun.js +5 -1
- package/dist/audit/cli/reviewRun.js.map +1 -1
- package/dist/audit/cli/rollingAuditDispatch.d.ts +12 -30
- package/dist/audit/cli/rollingAuditDispatch.d.ts.map +1 -1
- package/dist/audit/cli/rollingAuditDispatch.js +11 -60
- package/dist/audit/cli/rollingAuditDispatch.js.map +1 -1
- package/dist/audit/cli/semanticReviewStep.d.ts.map +1 -1
- package/dist/audit/cli/semanticReviewStep.js +30 -7
- package/dist/audit/cli/semanticReviewStep.js.map +1 -1
- package/dist/audit/quota/index.d.ts +2 -668
- package/dist/audit/quota/index.d.ts.map +1 -1
- package/dist/audit/quota/index.js +5 -30
- package/dist/audit/quota/index.js.map +1 -1
- package/dist/remediate/steps/dispatch/marshal.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/marshal.js +8 -1
- package/dist/remediate/steps/dispatch/marshal.js.map +1 -1
- package/dist/remediate/steps/dispatch/waveScheduling.d.ts +9 -2
- package/dist/remediate/steps/dispatch/waveScheduling.d.ts.map +1 -1
- package/dist/remediate/steps/dispatch/waveScheduling.js +48 -42
- package/dist/remediate/steps/dispatch/waveScheduling.js.map +1 -1
- package/dist/remediate/steps/nextStep.d.ts +79 -65
- package/dist/remediate/steps/nextStep.d.ts.map +1 -1
- package/dist/remediate/steps/nextStep.js +379 -380
- package/dist/remediate/steps/nextStep.js.map +1 -1
- package/dist/remediate/steps/types.d.ts +7 -27
- package/dist/remediate/steps/types.d.ts.map +1 -1
- package/dist/remediate/steps/types.js +0 -1
- package/dist/remediate/steps/types.js.map +1 -1
- package/dist/shared/dispatch/admissionLoop.d.ts +66 -8
- package/dist/shared/dispatch/admissionLoop.d.ts.map +1 -1
- package/dist/shared/dispatch/admissionLoop.js +168 -3
- package/dist/shared/dispatch/admissionLoop.js.map +1 -1
- package/dist/shared/dispatch/dispatchQuotaContract.d.ts +720 -0
- package/dist/shared/dispatch/dispatchQuotaContract.d.ts.map +1 -0
- package/dist/shared/dispatch/dispatchQuotaContract.js +76 -0
- package/dist/shared/dispatch/dispatchQuotaContract.js.map +1 -0
- package/dist/shared/dispatch/hostDispatchWall.d.ts +39 -0
- package/dist/shared/dispatch/hostDispatchWall.d.ts.map +1 -1
- package/dist/shared/dispatch/hostDispatchWall.js +22 -1
- package/dist/shared/dispatch/hostDispatchWall.js.map +1 -1
- package/dist/shared/dispatch/rollingDispatch.d.ts +18 -1
- package/dist/shared/dispatch/rollingDispatch.d.ts.map +1 -1
- package/dist/shared/dispatch/rollingDispatch.js +35 -9
- package/dist/shared/dispatch/rollingDispatch.js.map +1 -1
- package/dist/shared/dispatch/settledPools.d.ts +32 -0
- package/dist/shared/dispatch/settledPools.d.ts.map +1 -1
- package/dist/shared/dispatch/settledPools.js +37 -0
- package/dist/shared/dispatch/settledPools.js.map +1 -1
- package/dist/shared/index.d.ts +9 -6
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +7 -5
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/providers/inProcessWorkers.d.ts +42 -0
- package/dist/shared/providers/inProcessWorkers.d.ts.map +1 -0
- package/dist/shared/providers/inProcessWorkers.js +64 -0
- package/dist/shared/providers/inProcessWorkers.js.map +1 -0
- package/dist/shared/providers/providerPathGuard.d.ts +33 -4
- package/dist/shared/providers/providerPathGuard.d.ts.map +1 -1
- package/dist/shared/providers/providerPathGuard.js +33 -4
- package/dist/shared/providers/providerPathGuard.js.map +1 -1
- package/dist/shared/providers/proxyCatalog.d.ts.map +1 -1
- package/dist/shared/providers/proxyCatalog.js +21 -4
- package/dist/shared/providers/proxyCatalog.js.map +1 -1
- package/dist/shared/quota/apiPool.d.ts +94 -44
- package/dist/shared/quota/apiPool.d.ts.map +1 -1
- package/dist/shared/quota/apiPool.js +173 -75
- package/dist/shared/quota/apiPool.js.map +1 -1
- package/dist/shared/quota/capacity.d.ts +8 -1
- package/dist/shared/quota/capacity.d.ts.map +1 -1
- package/dist/shared/quota/capacity.js +2 -1
- package/dist/shared/quota/capacity.js.map +1 -1
- package/dist/shared/types/auditorDescriptor.d.ts +0 -8
- package/dist/shared/types/auditorDescriptor.d.ts.map +1 -1
- package/dist/shared/types/auditorDescriptor.js.map +1 -1
- package/dist/shared/validation/sessionConfig.d.ts.map +1 -1
- package/dist/shared/validation/sessionConfig.js +10 -0
- package/dist/shared/validation/sessionConfig.js.map +1 -1
- package/opencode.json +2 -2
- package/package.json +1 -1
|
@@ -3,14 +3,14 @@ import { existsSync, statSync } from "node:fs";
|
|
|
3
3
|
import { mkdir, readFile, rename } from "node:fs/promises";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { StateStore } from "../state/store.js";
|
|
6
|
-
import { readConfirmedDispatchPolicy, resolveDispatchExclusion, readOptionalJsonFile, readValidatedRepoSessionIntent, stagedAndUntracked, writeJsonFile, writeTextFile, buildAuditDeliverablePair, formatValidationIssues, isRecord, withFsRetry, RunLogger, DISPATCH_PROMPT_HANDOFF_NOTE, renderHostScratchNote, hostScratchDir, renderQuotaCoverageNudge, renderTokenBudgetView, coerceJsonObjectArg, driveRolling, resolveLedgerBudgets, setQuotaStateDir, detectHostDispatchWall, admissionBlockedOnBudget, reconcileAdmissionLeasesFromQuotaFile, buildQuotaPausedTerminal, interpretFreeFormIntent, advance, decideFrictionTriage, buildFrictionTriageBlock, planHybridDispatch, readSettledPools, addSettledPool, sourceByPoolId, classifyProvider, selectDispatchDriver, renderDispatchDriverInstruction, HostSessionQuotaSource, buildProviderModelKey, captureStepBoundaryFriction, captureCostDriftFriction, captureCreditExhaustionFriction, captureQuotaUnclassifiedFriction, captureModelUnavailableFriction, capturePacketTooLargeFriction, LENSES, SEVERITIES, resolveHostProviderName,
|
|
6
|
+
import { readConfirmedDispatchPolicy, resolveDispatchExclusion, readOptionalJsonFile, readValidatedRepoSessionIntent, stagedAndUntracked, writeJsonFile, writeTextFile, buildAuditDeliverablePair, formatValidationIssues, isRecord, withFsRetry, RunLogger, DISPATCH_PROMPT_HANDOFF_NOTE, renderHostScratchNote, hostScratchDir, renderQuotaCoverageNudge, renderTokenBudgetView, coerceJsonObjectArg, driveRolling, resolveLedgerBudgets, setQuotaStateDir, detectHostDispatchWall, admissionBlockedOnBudget, reconcileAdmissionLeasesFromQuotaFile, buildQuotaPausedTerminal, interpretFreeFormIntent, advance, decideFrictionTriage, buildFrictionTriageBlock, planHybridDispatch, readSettledPools, addSettledPool, isPoolSettlingOutcome, isInProcessWorkerProvider, sourceByPoolId, classifyProvider, selectDispatchDriver, renderDispatchDriverInstruction, HostSessionQuotaSource, buildProviderModelKey, captureStepBoundaryFriction, captureCostDriftFriction, captureCreditExhaustionFriction, captureQuotaUnclassifiedFriction, captureModelUnavailableFriction, capturePacketTooLargeFriction, LENSES, SEVERITIES, resolveHostProviderName, resolveHostDispatchProviderName, resolveHostDispatchCapability as sharedResolveHostDispatchCapability, resolveAutonomousMode, resolveRollingEngineFlag, DEFAULT_CONTEXT_TOKENS } from "audit-tools/shared";
|
|
7
7
|
import { readRemediationAccessMemory, computeBlockContinuityScores } from "../state/accessMemory.js";
|
|
8
8
|
import { applyPlanPipeline, buildCoverageLedger } from "../phases/plan.js";
|
|
9
9
|
import { groundExtractedFindings } from "../phases/grounding.js";
|
|
10
10
|
import { runTriagePhase } from "../phases/triage.js";
|
|
11
11
|
import { runClosePhase } from "../phases/close.js";
|
|
12
12
|
import { validateRemediationPlan } from "../validation/remediationState.js";
|
|
13
|
-
import { mergeImplementResults, prepareImplementDispatch, readExtractedPlanIfPresent, buildConfirmedPools, executeNodeInWorktree, blockScopesFromPlan,
|
|
13
|
+
import { mergeImplementResults, prepareImplementDispatch, readExtractedPlanIfPresent, buildConfirmedPools, executeNodeInWorktree, blockScopesFromPlan, targetedCommandsForBlock, } from "./dispatch.js";
|
|
14
14
|
import { makeProviderNodeDispatcher } from "./providerNodeDispatch.js";
|
|
15
15
|
import { prepareHostRollingDispatch, nodeClaimRegistry, nodeSettledPoolsPath } from "./rollingSession.js";
|
|
16
16
|
import { ClaimRegistry } from "../../shared/quota/claimRegistry.js";
|
|
@@ -509,12 +509,16 @@ export async function driveRollingDispatch(levels, options) {
|
|
|
509
509
|
...(continuity > 0 ? { continuity } : {}),
|
|
510
510
|
};
|
|
511
511
|
},
|
|
512
|
-
toPacket: (b) =>
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
512
|
+
toPacket: (b) => {
|
|
513
|
+
const requiredTier = options.tierForBlock?.(b);
|
|
514
|
+
return {
|
|
515
|
+
id: b.block_id,
|
|
516
|
+
payload: { block_id: b.block_id },
|
|
517
|
+
estimatedTokens: estimateTokens(b),
|
|
518
|
+
complexity: 0.5,
|
|
519
|
+
...(requiredTier ? { requiredTier } : {}),
|
|
520
|
+
};
|
|
521
|
+
},
|
|
518
522
|
dispatchPacket: async (packet, slot) => options.dispatchNode(blockById.get(packet.payload.block_id), slot),
|
|
519
523
|
...(options.root !== undefined ? { root: options.root } : {}),
|
|
520
524
|
...(options.onCostDrift ? { onCostDrift: options.onCostDrift } : {}),
|
|
@@ -538,44 +542,16 @@ export async function driveRollingDispatch(levels, options) {
|
|
|
538
542
|
levels: run.levels.map((l) => ({ blockIds: l.nodeIds, results: l.results })),
|
|
539
543
|
rebuilds: run.rebuilds,
|
|
540
544
|
...(run.terminal ? { terminal: run.terminal } : {}),
|
|
545
|
+
exhaustedPoolIds: run.exhaustedPoolIds,
|
|
541
546
|
};
|
|
542
547
|
}
|
|
543
|
-
/**
|
|
544
|
-
* Backends the orchestrator can drive IN-PROCESS as the per-node implement worker
|
|
545
|
-
* via `driveRollingImplementDispatch` (it resolves + launches the provider with each
|
|
546
|
-
* node's worktree-rooted prompt, cwd-confined to that worktree). The conversation
|
|
547
|
-
* host (claude-code) and IDE-bound providers (vscode-task / antigravity) are
|
|
548
|
-
* excluded: claude-code self-blocks inside a session, and the IDE providers have no
|
|
549
|
-
* headless invocation. "auto" is intentionally absent — auto-resolution stays on the
|
|
550
|
-
* conversation host-subagent default, so the in-process driver is opt-in via an
|
|
551
|
-
* EXPLICIT backend provider in session config. When one is set, it takes precedence
|
|
552
|
-
* over the host-subagent driver: an operator who configured a backend (e.g. a NIM
|
|
553
|
-
* pool for headless autonomy) wants it to do the implement work, not the host.
|
|
554
|
-
*/
|
|
555
|
-
const IN_PROCESS_DISPATCH_PROVIDERS = new Set([
|
|
556
|
-
"openai-compatible",
|
|
557
|
-
"codex",
|
|
558
|
-
"opencode",
|
|
559
|
-
"subprocess-template",
|
|
560
|
-
"worker-command",
|
|
561
|
-
"agy",
|
|
562
|
-
// The proxied isolated Claude-harness worker (commit 3b/3c) — headless in-process
|
|
563
|
-
// launch from its claude-worker source (mirrors audit's IN_PROCESS_AUDIT_PROVIDERS;
|
|
564
|
-
// both draws must classify the new worker kind or its pools confirm but never route).
|
|
565
|
-
"claude-worker",
|
|
566
|
-
]);
|
|
567
|
-
function resolvesToInProcessDispatchProvider(sessionConfig) {
|
|
568
|
-
const provider = sessionConfig?.provider;
|
|
569
|
-
return provider !== undefined && IN_PROCESS_DISPATCH_PROVIDERS.has(provider);
|
|
570
|
-
}
|
|
571
548
|
/**
|
|
572
549
|
* Whether a confirmed pool is one the orchestrator launches IN-PROCESS this cycle
|
|
573
|
-
* (vs. the conversation host's subagent pool)
|
|
574
|
-
*
|
|
575
|
-
* `resolvesToInProcessDispatchProvider` uses.
|
|
550
|
+
* (vs. the conversation host's subagent pool) — the shared
|
|
551
|
+
* `isInProcessWorkerProvider` predicate (H3), same remediate policy.
|
|
576
552
|
*/
|
|
577
553
|
function isInProcessPool(pool) {
|
|
578
|
-
return
|
|
554
|
+
return isInProcessWorkerProvider(pool.providerName, { commandWorkers: true });
|
|
579
555
|
}
|
|
580
556
|
/**
|
|
581
557
|
* Release a node's shared claim on a terminal accept (token-checked through the
|
|
@@ -607,21 +583,23 @@ export async function driveRollingImplementDispatch(options) {
|
|
|
607
583
|
const { root, artifactsDir, runId } = options;
|
|
608
584
|
// Prepare the dispatch plan (eligible verified-complete frontier) with the SAME
|
|
609
585
|
// quota-derived sizing the wave path uses. This writes per-node prompts +
|
|
610
|
-
// dispatch-plan.json + dispatch-quota.json.
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
586
|
+
// dispatch-plan.json + dispatch-quota.json. A partition-scoped hybrid caller
|
|
587
|
+
// supplies its already-prepared plan instead (planOverride — see its docblock).
|
|
588
|
+
const plan = options.planOverride ??
|
|
589
|
+
(await prepareImplementDispatch({ root, artifactsDir }, runId, undefined, {
|
|
590
|
+
hostMaxConcurrent: options.waveOptions?.hostMaxConcurrent,
|
|
591
|
+
sessionConfig: options.sessionConfig,
|
|
592
|
+
hostContextTokens: options.waveOptions?.hostContextTokens,
|
|
593
|
+
hostOutputTokens: options.waveOptions?.hostOutputTokens,
|
|
594
|
+
hostModels: options.waveOptions?.hostModels,
|
|
595
|
+
hostModelId: options.waveOptions?.hostModelId,
|
|
596
|
+
// Each node runs in its own worktree, so its prompt is rooted there.
|
|
597
|
+
worktreeRootedPrompts: true,
|
|
598
|
+
// The in-process rolling engine admits + leases per-packet itself, so the
|
|
599
|
+
// dispatch-quota grant here must NOT lease (a host grant lease would
|
|
600
|
+
// double-count the same work against the shared account budget).
|
|
601
|
+
grantLeases: false,
|
|
602
|
+
}));
|
|
625
603
|
if (plan.items.length === 0) {
|
|
626
604
|
return null;
|
|
627
605
|
}
|
|
@@ -644,6 +622,12 @@ export async function driveRollingImplementDispatch(options) {
|
|
|
644
622
|
const referencedFilesByBlock = new Map(plan.items
|
|
645
623
|
.filter((i) => typeof i.block_id === "string")
|
|
646
624
|
.map((i) => [i.block_id, i.access?.read_paths ?? []]));
|
|
625
|
+
// Per-block capability floor from the plan's model hints (F4) — the same tier
|
|
626
|
+
// the contract's admission packets carry, so the engine's packet→pool
|
|
627
|
+
// selection enforces what the contract displays.
|
|
628
|
+
const tierByBlock = new Map(plan.items
|
|
629
|
+
.filter((i) => typeof i.block_id === "string")
|
|
630
|
+
.flatMap((i) => (i.model_hint ? [[i.block_id, i.model_hint.tier]] : [])));
|
|
647
631
|
// The RETAINED host-session source: threaded through pool sizing AND the
|
|
648
632
|
// dispatcher's escalation hooks so the bounded re-limit chain (recordLimit →
|
|
649
633
|
// escalate → strand → quota_escalation friction) is fed end-to-end. Its
|
|
@@ -669,20 +653,22 @@ export async function driveRollingImplementDispatch(options) {
|
|
|
669
653
|
},
|
|
670
654
|
});
|
|
671
655
|
// Confirmed pools: quota-derived concurrency, never the raw host flag (INV-QD-11).
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
656
|
+
// A partition-scoped caller supplies its coordinator's already-built set instead.
|
|
657
|
+
const confirmedPools = options.poolsOverride ??
|
|
658
|
+
(await buildConfirmedPools({
|
|
659
|
+
sessionConfig: options.sessionConfig,
|
|
660
|
+
hostMaxConcurrent: options.waveOptions?.hostMaxConcurrent,
|
|
661
|
+
hostContextTokens: options.waveOptions?.hostContextTokens,
|
|
662
|
+
hostOutputTokens: options.waveOptions?.hostOutputTokens,
|
|
663
|
+
hostModels: options.waveOptions?.hostModels,
|
|
664
|
+
hostModelId: options.waveOptions?.hostModelId,
|
|
665
|
+
hostSession,
|
|
666
|
+
// The operator's Gate-0 exclusions, applied as a set-difference over freshly
|
|
667
|
+
// gathered reach. Read here because this layer owns `root`; self-spawn-blocked is
|
|
668
|
+
// recomputed against THIS process's env rather than inherited from the auditor
|
|
669
|
+
// that wrote the confirmation.
|
|
670
|
+
excludedBackends: resolveDispatchExclusion(await readConfirmedDispatchPolicy(options.root)),
|
|
671
|
+
}));
|
|
686
672
|
// The live per-node worker: the configured provider, launched with the node's
|
|
687
673
|
// worktree-rooted prompt and cwd = its worktree. Tests inject `options.dispatchNode`
|
|
688
674
|
// to exercise the engine without spawning a real worker. A node on a source-backed
|
|
@@ -702,11 +688,22 @@ export async function driveRollingImplementDispatch(options) {
|
|
|
702
688
|
if (!state)
|
|
703
689
|
return null;
|
|
704
690
|
const plannedBlockIds = new Set(resultPathByBlock.keys());
|
|
691
|
+
// Partition scope (H2): when the caller assigned this driver a coordinator
|
|
692
|
+
// partition, only those planned blocks are driven — everything else stays
|
|
693
|
+
// pending for the caller's other pools (the host share included).
|
|
694
|
+
const partitionIds = options.blocksOverride !== undefined ? new Set(options.blocksOverride) : null;
|
|
705
695
|
const allLevels = rollingDependencyLevels(state);
|
|
706
696
|
// Keep only the blocks that were actually planned this dispatch (eligible now).
|
|
707
697
|
const levels = allLevels
|
|
708
|
-
.map((level) => level.filter((b) => plannedBlockIds.has(b.block_id)))
|
|
698
|
+
.map((level) => level.filter((b) => plannedBlockIds.has(b.block_id) && (partitionIds === null || partitionIds.has(b.block_id))))
|
|
709
699
|
.filter((level) => level.length > 0);
|
|
700
|
+
// Empty partition → an empty-but-SHAPED result, never null: `null` means "no
|
|
701
|
+
// eligible work, run the merge" to existing callers, and a partition caller
|
|
702
|
+
// following that recipe against the just-written full-frontier plan would
|
|
703
|
+
// terminal-block every undriven block (review h2c2 F1).
|
|
704
|
+
if (partitionIds !== null && levels.length === 0) {
|
|
705
|
+
return { nodes: [], rebuilds: 0, state_status: state.status, exhausted_pool_ids: [] };
|
|
706
|
+
}
|
|
710
707
|
const nodeOutcomes = [];
|
|
711
708
|
// The SAME file-backed claim registry the host-subagent driver claims through
|
|
712
709
|
// (`nodeClaimRegistry`, keyed only to run + artifacts dir). Claiming a node here
|
|
@@ -716,7 +713,9 @@ export async function driveRollingImplementDispatch(options) {
|
|
|
716
713
|
// life of this run: a `rate_limited` re-queue re-enters for the same block, so a
|
|
717
714
|
// node already claimed by THIS driver reuses its token rather than self-colliding.
|
|
718
715
|
const registry = nodeClaimRegistry(artifactsDir, runId);
|
|
719
|
-
|
|
716
|
+
// Seeded with any coordinator-held claims the caller hands off (hybrid partition
|
|
717
|
+
// drive) — an adopted node skips self-claiming and releases with the adopted token.
|
|
718
|
+
const claimTokens = new Map(options.claimOwnerTokens ?? []);
|
|
720
719
|
// Per-node worktree dispatch + verify-before-accept, wrapped so the rolling
|
|
721
720
|
// engine's dispatchNode callback always RESOLVES (never rejects).
|
|
722
721
|
const dispatchNodeWithWorktree = async (block, slot) => {
|
|
@@ -729,7 +728,7 @@ export async function driveRollingImplementDispatch(options) {
|
|
|
729
728
|
if (!claimTokens.has(block.block_id)) {
|
|
730
729
|
const claim = await registry.claim(block.block_id, "in-process");
|
|
731
730
|
if (!claim.acquired) {
|
|
732
|
-
nodeOutcomes.push({ block_id: block.block_id, outcome: "success", verify_passed: false, merged: false });
|
|
731
|
+
nodeOutcomes.push({ block_id: block.block_id, outcome: "success", verify_passed: false, merged: false, pool_id: slot.poolId });
|
|
733
732
|
return {
|
|
734
733
|
packet: { id: block.block_id, payload: { block_id: block.block_id }, estimatedTokens: 0, complexity: 0.5 },
|
|
735
734
|
outcome: "success",
|
|
@@ -762,6 +761,9 @@ export async function driveRollingImplementDispatch(options) {
|
|
|
762
761
|
outcome: accept.outcome,
|
|
763
762
|
verify_passed: accept.verifyPassed,
|
|
764
763
|
merged: accept.merged,
|
|
764
|
+
// Real per-node pool attribution (H2 plan D2) — the slot the engine bound,
|
|
765
|
+
// so a partition caller can settle the RIGHT pool from a node's outcome.
|
|
766
|
+
pool_id: slot.poolId,
|
|
765
767
|
});
|
|
766
768
|
// Release the claim ONLY on a terminal accept. A `rate_limited`,
|
|
767
769
|
// `credit_exhausted`, or `quota_unclassified` worker re-queues (still owned
|
|
@@ -794,6 +796,7 @@ export async function driveRollingImplementDispatch(options) {
|
|
|
794
796
|
hostSession,
|
|
795
797
|
continuityScores,
|
|
796
798
|
scopeForBlock: (block) => writePathsByBlock.get(block.block_id) ?? block.touched_files ?? [],
|
|
799
|
+
tierForBlock: (block) => tierByBlock.get(block.block_id),
|
|
797
800
|
// Reactive cost verification: a declared-free source pool observed charging has
|
|
798
801
|
// been demoted by the engine; surface it as reviewable friction so the operator
|
|
799
802
|
// reconciles the stale `cost_per_mtok:0` (single step-boundary chokepoint).
|
|
@@ -827,6 +830,22 @@ export async function driveRollingImplementDispatch(options) {
|
|
|
827
830
|
capturePacketTooLargeFriction(artifactsDir, runId, info, "remediate-code");
|
|
828
831
|
},
|
|
829
832
|
});
|
|
833
|
+
// Partition-scoped drive (H2): run-level lifecycle belongs to the CALLER — the
|
|
834
|
+
// engine terminal is surfaced on the result but never persisted (a backend-only
|
|
835
|
+
// wall must not pause the whole run while the host share proceeds), and the
|
|
836
|
+
// deterministic merge is the caller's, run once over ALL partitions.
|
|
837
|
+
if (partitionIds !== null) {
|
|
838
|
+
// Freshly persisted status; falls back to the pre-drive snapshot only if
|
|
839
|
+
// state.json vanished mid-drive (review h2c2 F4 — documented, not silent).
|
|
840
|
+
const current = await new StateStore(artifactsDir).loadState();
|
|
841
|
+
return {
|
|
842
|
+
nodes: nodeOutcomes,
|
|
843
|
+
rebuilds: Math.max(0, levels.length - 1),
|
|
844
|
+
state_status: (current ?? state).status,
|
|
845
|
+
exhausted_pool_ids: driven.exhaustedPoolIds,
|
|
846
|
+
...(driven.terminal ? { terminal: driven.terminal } : {}),
|
|
847
|
+
};
|
|
848
|
+
}
|
|
830
849
|
// Piece D — persist the rolling engine's partial-completion terminal onto state
|
|
831
850
|
// BEFORE the merge, so the merge can SKIP-block the quota_paused stranded nodes
|
|
832
851
|
// (their worker rate-limited → no result file, but they must stay PENDING for a
|
|
@@ -848,6 +867,8 @@ export async function driveRollingImplementDispatch(options) {
|
|
|
848
867
|
nodes: nodeOutcomes,
|
|
849
868
|
rebuilds: Math.max(0, levels.length - 1),
|
|
850
869
|
state_status: merged.status,
|
|
870
|
+
exhausted_pool_ids: driven.exhaustedPoolIds,
|
|
871
|
+
...(driven.terminal ? { terminal: driven.terminal } : {}),
|
|
851
872
|
};
|
|
852
873
|
}
|
|
853
874
|
/**
|
|
@@ -857,85 +878,6 @@ export async function driveRollingImplementDispatch(options) {
|
|
|
857
878
|
* uses (`driveRollingDispatch`'s `() => 2000`), keeping the two paths consistent.
|
|
858
879
|
*/
|
|
859
880
|
const HYBRID_NODE_TOKEN_ESTIMATE = 2000;
|
|
860
|
-
/**
|
|
861
|
-
* Run the A-8 coordinator's IN-PROCESS partition this cycle. Each node was already
|
|
862
|
-
* claimed by the coordinator and assigned to a backend pool (NIM / codex / …), so it
|
|
863
|
-
* is launched on THAT pool's provider — binding the slot's providerName to the
|
|
864
|
-
* per-node assignment is what routes a node to its assigned backend — cwd-confined to
|
|
865
|
-
* its worktree, through the shared `executeNodeInWorktree` lifecycle (commit → verify
|
|
866
|
-
* → write-scope → merge); then the coordinator's claim is released.
|
|
867
|
-
*
|
|
868
|
-
* Nodes run concurrently: the coordinator's proactive split already bounded the
|
|
869
|
-
* partition to the backend pools' capacity, so the partition size IS the safe
|
|
870
|
-
* concurrency. The host partition runs in parallel via the host-subagent driver;
|
|
871
|
-
* both write accept-outcome sidecars the run-level `mergeImplementResults` reconciles.
|
|
872
|
-
*
|
|
873
|
-
* Run-once (no in-pass re-queue): a node whose worker rate-limits or errors is not
|
|
874
|
-
* merged (its worktree drops) and is routed to triage by the deterministic merge —
|
|
875
|
-
* bounded, never a livelock. (Cross-cycle settled-pool re-balancing is a follow-up.)
|
|
876
|
-
*/
|
|
877
|
-
export async function executeInProcessPartition(params) {
|
|
878
|
-
const { root, artifactsDir, runId, sessionConfig, partition, plan, coordinator, registry } = params;
|
|
879
|
-
if (partition.length === 0)
|
|
880
|
-
return { nodes: [] };
|
|
881
|
-
const allBlockScopes = blockScopesFromPlan(plan);
|
|
882
|
-
const withBlockId = plan.items.filter((i) => typeof i.block_id === "string");
|
|
883
|
-
const promptPathByBlock = new Map(withBlockId.map((i) => [i.block_id, i.prompt_path]));
|
|
884
|
-
const referencedFilesByBlock = new Map(withBlockId.map((i) => [i.block_id, i.access?.read_paths ?? []]));
|
|
885
|
-
const resultPathByBlock = new Map(withBlockId.map((i) => [i.block_id, i.result_path]));
|
|
886
|
-
const state = await new StateStore(artifactsDir).loadState();
|
|
887
|
-
const blockById = new Map((state?.plan?.blocks ?? []).map((b) => [b.block_id, b]));
|
|
888
|
-
const dispatchNode = params.dispatchNode ??
|
|
889
|
-
makeProviderNodeDispatcher({
|
|
890
|
-
root,
|
|
891
|
-
artifactsDir,
|
|
892
|
-
runId,
|
|
893
|
-
sessionConfig,
|
|
894
|
-
promptPathByBlock,
|
|
895
|
-
referencedFilesByBlock,
|
|
896
|
-
sourceByPoolId: params.sourceByPoolId,
|
|
897
|
-
});
|
|
898
|
-
const nodes = await Promise.all(partition.map(async (a) => {
|
|
899
|
-
// `a` IS the coordinator's NodeAssignment — release it directly on terminal.
|
|
900
|
-
const resultPath = resultPathByBlock.get(a.nodeId);
|
|
901
|
-
if (!resultPath) {
|
|
902
|
-
// No prepared prompt/result for this node — release + mark error (the merge
|
|
903
|
-
// routes it to triage); never silently drop a claimed node.
|
|
904
|
-
await coordinator.release(a);
|
|
905
|
-
return { block_id: a.nodeId, outcome: "error", verify_passed: false, merged: false };
|
|
906
|
-
}
|
|
907
|
-
const block = blockById.get(a.nodeId) ?? { block_id: a.nodeId };
|
|
908
|
-
const slot = {
|
|
909
|
-
providerName: a.providerName,
|
|
910
|
-
hostModel: a.hostModel,
|
|
911
|
-
poolId: a.poolId,
|
|
912
|
-
};
|
|
913
|
-
const { accept } = await executeNodeInWorktree({
|
|
914
|
-
block,
|
|
915
|
-
slot,
|
|
916
|
-
root,
|
|
917
|
-
artifactsDir,
|
|
918
|
-
runId,
|
|
919
|
-
resultPath,
|
|
920
|
-
seedPaths: declaredPathsFromPlan(plan, a.nodeId),
|
|
921
|
-
allBlockScopes,
|
|
922
|
-
additionalVerifyCommands: state ? targetedCommandsForBlock(state, a.nodeId) : [],
|
|
923
|
-
dispatchNode,
|
|
924
|
-
// Merge-time ownership gate (OD3 layer 2): the SAME lease the coordinator
|
|
925
|
-
// claimed `a` under. Omitted when the caller didn't supply a registry.
|
|
926
|
-
...(registry ? { ownership: { registry, nodeId: a.nodeId, ownerToken: a.ownerToken } } : {}),
|
|
927
|
-
});
|
|
928
|
-
// Run-once → terminal; free the coordinator claim (token-checked).
|
|
929
|
-
await coordinator.release(a);
|
|
930
|
-
return {
|
|
931
|
-
block_id: a.nodeId,
|
|
932
|
-
outcome: accept.outcome,
|
|
933
|
-
verify_passed: accept.verifyPassed,
|
|
934
|
-
merged: accept.merged,
|
|
935
|
-
};
|
|
936
|
-
}));
|
|
937
|
-
return { nodes };
|
|
938
|
-
}
|
|
939
881
|
// ---------------------------------------------------------------------------
|
|
940
882
|
// Tool-owned final completion gate (INV-RS-10) + coarse re-block (INV-RS-09)
|
|
941
883
|
// ---------------------------------------------------------------------------
|
|
@@ -1244,80 +1186,23 @@ async function buildImplementDispatchStep(ctx) {
|
|
|
1244
1186
|
hostModels: resolvedHostModels,
|
|
1245
1187
|
hostModelId: resolvedHostModelId,
|
|
1246
1188
|
};
|
|
1247
|
-
//
|
|
1248
|
-
//
|
|
1249
|
-
//
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1252
|
-
//
|
|
1253
|
-
//
|
|
1254
|
-
//
|
|
1255
|
-
//
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
// primary backend provider (one account ⇒ host self-drives as a single pool),
|
|
1264
|
-
// else host + demoted-source pools double-book that one meter.
|
|
1265
|
-
const demoteBackendToSource = shouldDemotePrimaryInProcess({
|
|
1266
|
-
sessionConfig: sessionConfigImpl,
|
|
1267
|
-
hostCanDispatch: canDispatchImpl,
|
|
1268
|
-
});
|
|
1269
|
-
if (rollingEngineEnabled &&
|
|
1270
|
-
!demoteBackendToSource &&
|
|
1271
|
-
resolvesToInProcessDispatchProvider(sessionConfigImpl)) {
|
|
1272
|
-
const driven = await driveRollingImplementDispatch({
|
|
1273
|
-
root,
|
|
1274
|
-
artifactsDir,
|
|
1275
|
-
runId,
|
|
1276
|
-
sessionConfig: sessionConfigImpl ?? null,
|
|
1277
|
-
// Per-node verify (targeted_commands) owns each node's build/test; an
|
|
1278
|
-
// inter-level "shared surface" rebuild is a monorepo-self-remediation concern
|
|
1279
|
-
// the host-driven paths handle, not a generic target-repo step → no-op here.
|
|
1280
|
-
rebuildSharedBetweenLevels: async () => { },
|
|
1281
|
-
waveOptions: {
|
|
1282
|
-
hostMaxConcurrent: resolvedHostMaxConcurrent,
|
|
1283
|
-
hostContextTokens: resolvedHostContextTokens,
|
|
1284
|
-
hostOutputTokens: resolvedHostOutputTokens,
|
|
1285
|
-
hostModels: resolvedHostModels,
|
|
1286
|
-
hostModelId: resolvedHostModelId,
|
|
1287
|
-
},
|
|
1189
|
+
// H2+H4 collapse: ONE fan-out over the eligible pool set. `buildConfirmedPools`
|
|
1190
|
+
// folds the configured primary in-process backend in as a source pool
|
|
1191
|
+
// UNCONDITIONALLY (no demote flag; command-shaped primaries included under
|
|
1192
|
+
// remediate's policy — plan D3) and includes the conversation host as a member
|
|
1193
|
+
// pool iff it can dispatch subagents. Headless is the degenerate "no host pool
|
|
1194
|
+
// in the set" case: the engine drives the whole frontier itself. The same-agent
|
|
1195
|
+
// case (conversation host IS the primary backend) is the shared cross-class
|
|
1196
|
+
// dedup's D1 collision rule — the engine/source pool survives, so one account is
|
|
1197
|
+
// never double-booked across a host pool and a folded source.
|
|
1198
|
+
if (rollingEngineEnabled) {
|
|
1199
|
+
// D5: the host-session quota key follows the DRIVER identity — an in-process
|
|
1200
|
+
// worker primary keys to the conversation host; an explicit IDE/host provider
|
|
1201
|
+
// passes through verbatim (never re-keyed to the literal claude-code, the
|
|
1202
|
+
// founding-bug misattribution class [[capability-is-per-auditor-not-per-audit]]).
|
|
1203
|
+
const hybridProviderName = resolveHostDispatchProviderName(sessionConfigImpl, {
|
|
1204
|
+
commandWorkers: true,
|
|
1288
1205
|
});
|
|
1289
|
-
// null = no eligible pending work this pass; the engine merges internally once
|
|
1290
|
-
// it has run, so only the empty-frontier case needs a merge here. Either way the
|
|
1291
|
-
// implement frontier is resolved — transition on the freshly-merged state so the
|
|
1292
|
-
// engine re-scans (triage / closing) without recursion.
|
|
1293
|
-
if (driven === null) {
|
|
1294
|
-
const merged = await mergeImplementResults({ root, artifactsDir }, runId);
|
|
1295
|
-
return { kind: "transition", state: merged };
|
|
1296
|
-
}
|
|
1297
|
-
return { kind: "transition", state: await store.loadState() };
|
|
1298
|
-
}
|
|
1299
|
-
if (rollingEngineEnabled && canDispatchImpl) {
|
|
1300
|
-
// A-8 hybrid spill: when an in-process backend pool is ALSO confirmed (a
|
|
1301
|
-
// configured NIM/openai-compatible endpoint alongside the conversation host),
|
|
1302
|
-
// split the eligible frontier across BOTH pool classes via the shared
|
|
1303
|
-
// HybridSpillCoordinator (single claimant, proactive capacity split) — the
|
|
1304
|
-
// orchestrator runs the in-process partition THIS cycle while the host spawns
|
|
1305
|
-
// subagents for its partition. Pure host-subagent dispatch falls out when no
|
|
1306
|
-
// backend pool is confirmed (the coordinator has nothing to split against).
|
|
1307
|
-
// Retained host-session source for the hybrid pool-sizing pre-wall throttle
|
|
1308
|
-
// (this branch previously sized pools with no account-wall awareness at all,
|
|
1309
|
-
// unlike the primary path above). This branch has its own already-working,
|
|
1310
|
-
// bounded rate-limited/settle mechanism below (DC-4) rather than routing
|
|
1311
|
-
// through HostSessionQuotaSource.recordLimit/isEscalated, so onEscalation is
|
|
1312
|
-
// unused here — the source only feeds buildConfirmedPools' sizing.
|
|
1313
|
-
// Defect-1: the host-session quota key must follow the CONVERSATION HOST, not a
|
|
1314
|
-
// demoted backend — key it to the auto-detected conversation host (B1: codex
|
|
1315
|
-
// when inside a Codex session, else claude-code; --host-provider overrides)
|
|
1316
|
-
// when the configured primary is a demotable backend that this attended run is
|
|
1317
|
-
// fanning out onto as a source.
|
|
1318
|
-
const hybridProviderName = demoteBackendToSource
|
|
1319
|
-
? resolveConversationHostProvider({ sessionConfig: sessionConfigImpl })
|
|
1320
|
-
: resolveHostProviderName(sessionConfigImpl);
|
|
1321
1206
|
const hybridHostSessionModelKey = buildProviderModelKey(hybridProviderName, sessionConfigImpl
|
|
1322
1207
|
?.block_quota?.host_model ??
|
|
1323
1208
|
resolvedHostModelId ??
|
|
@@ -1333,166 +1218,248 @@ async function buildImplementDispatchStep(ctx) {
|
|
|
1333
1218
|
hostModels: resolvedHostModels,
|
|
1334
1219
|
hostModelId: resolvedHostModelId,
|
|
1335
1220
|
hostSession: hybridHostSession,
|
|
1336
|
-
//
|
|
1337
|
-
|
|
1338
|
-
// source-pool set so the split fans across host + backend + NIM.
|
|
1339
|
-
demotePrimaryInProcess: demoteBackendToSource,
|
|
1221
|
+
// Attendance IS pool-set membership: headless ⇒ no host pool in the set.
|
|
1222
|
+
hostCanDispatch: canDispatchImpl,
|
|
1340
1223
|
// The operator's Gate-0 exclusions, applied as a set-difference over freshly
|
|
1341
1224
|
// gathered reach; self-spawn-blocked recomputed against THIS process's env.
|
|
1342
1225
|
excludedBackends: resolveDispatchExclusion(await readConfirmedDispatchPolicy(root)),
|
|
1343
1226
|
});
|
|
1344
1227
|
const backendPools = confirmedPools.filter(isInProcessPool);
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
//
|
|
1348
|
-
//
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1228
|
+
if (!canDispatchImpl) {
|
|
1229
|
+
// Headless: no host pool in the eligible set — when any dispatchable
|
|
1230
|
+
// backend pool is confirmed, the engine drives the FULL rolling implement
|
|
1231
|
+
// dispatch itself (no blocksOverride: the driver's full path owns terminal
|
|
1232
|
+
// persistence + the final merge), cwd-confined to each node's worktree,
|
|
1233
|
+
// sharing the same `acceptNodeWorktree` core as the host-subagent driver.
|
|
1234
|
+
// No pool at all ⇒ fall through to the sequential host step below.
|
|
1235
|
+
if (backendPools.length > 0) {
|
|
1236
|
+
const driven = await driveRollingImplementDispatch({
|
|
1237
|
+
root,
|
|
1238
|
+
artifactsDir,
|
|
1239
|
+
runId,
|
|
1240
|
+
sessionConfig: sessionConfigImpl ?? null,
|
|
1241
|
+
// Per-node verify (targeted_commands) owns each node's build/test; an
|
|
1242
|
+
// inter-level "shared surface" rebuild is a monorepo-self-remediation concern
|
|
1243
|
+
// the host-driven paths handle, not a generic target-repo step → no-op here.
|
|
1244
|
+
rebuildSharedBetweenLevels: async () => { },
|
|
1245
|
+
waveOptions: {
|
|
1246
|
+
hostMaxConcurrent: resolvedHostMaxConcurrent,
|
|
1247
|
+
hostContextTokens: resolvedHostContextTokens,
|
|
1248
|
+
hostOutputTokens: resolvedHostOutputTokens,
|
|
1249
|
+
hostModels: resolvedHostModels,
|
|
1250
|
+
hostModelId: resolvedHostModelId,
|
|
1251
|
+
},
|
|
1252
|
+
// The eligible pool set built above (source pools only — headless),
|
|
1253
|
+
// so the drive routes across every confirmed backend pool.
|
|
1254
|
+
poolsOverride: backendPools,
|
|
1255
|
+
});
|
|
1256
|
+
// null = no eligible pending work this pass; the engine merges internally once
|
|
1257
|
+
// it has run, so only the empty-frontier case needs a merge here. Either way the
|
|
1258
|
+
// implement frontier is resolved — transition on the freshly-merged state so the
|
|
1259
|
+
// engine re-scans (triage / closing) without recursion.
|
|
1260
|
+
if (driven === null) {
|
|
1261
|
+
const merged = await mergeImplementResults({ root, artifactsDir }, runId);
|
|
1262
|
+
return { kind: "transition", state: merged };
|
|
1263
|
+
}
|
|
1264
|
+
return { kind: "transition", state: await store.loadState() };
|
|
1359
1265
|
}
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
//
|
|
1363
|
-
//
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
registry
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1266
|
+
}
|
|
1267
|
+
if (canDispatchImpl) {
|
|
1268
|
+
// A-8 hybrid spill: when an in-process backend pool is ALSO confirmed (a
|
|
1269
|
+
// configured NIM/openai-compatible endpoint alongside the conversation host),
|
|
1270
|
+
// split the eligible frontier across BOTH pool classes via the shared
|
|
1271
|
+
// HybridSpillCoordinator (single claimant, proactive capacity split) — the
|
|
1272
|
+
// orchestrator runs the in-process partition THIS cycle while the host spawns
|
|
1273
|
+
// subagents for its partition. Pure host-subagent dispatch falls out when no
|
|
1274
|
+
// backend pool is confirmed (the coordinator has nothing to split against).
|
|
1275
|
+
// The hybrid host-session source above fed `buildConfirmedPools`' pool-sizing
|
|
1276
|
+
// pre-wall throttle; this branch has its own bounded rate-limited/settle
|
|
1277
|
+
// mechanism below (DC-4) rather than routing through
|
|
1278
|
+
// HostSessionQuotaSource.recordLimit/isEscalated.
|
|
1279
|
+
let rolling;
|
|
1280
|
+
if (backendPools.length > 0) {
|
|
1281
|
+
// Prepare the frontier ONCE (worktree-rooted prompts) so the coordinator
|
|
1282
|
+
// split, the partition drive (via planOverride), and the host driver all
|
|
1283
|
+
// read the same plan — the partition drive deliberately does NOT re-prepare
|
|
1284
|
+
// (see DriveRollingImplementDispatchOptions.planOverride for the lease
|
|
1285
|
+
// overlap this shape carries and why a re-prepare would clobber the
|
|
1286
|
+
// host-share dispatch-quota).
|
|
1287
|
+
const plan = await prepareImplementDispatch({ root, artifactsDir }, runId, undefined, {
|
|
1288
|
+
...waveOptsImpl,
|
|
1289
|
+
worktreeRootedPrompts: true,
|
|
1290
|
+
});
|
|
1291
|
+
const frontier = plan.items
|
|
1292
|
+
.filter((i) => typeof i.block_id === "string")
|
|
1293
|
+
.map((i) => ({ id: i.block_id, estimatedTokens: HYBRID_NODE_TOKEN_ESTIMATE }));
|
|
1294
|
+
if (frontier.length === 0) {
|
|
1295
|
+
const merged = await mergeImplementResults({ root, artifactsDir }, runId);
|
|
1296
|
+
return { kind: "transition", state: merged };
|
|
1297
|
+
}
|
|
1298
|
+
// One coordinator over the shared claim registry splits + claims each node to
|
|
1299
|
+
// exactly one pool. DC-4: the settled set is cross-cycle (persisted) — a backend
|
|
1300
|
+
// pool that exhausted on a prior cycle is excluded here, so its work falls to the
|
|
1301
|
+
// host-subagent pool instead of re-looping on a dead backend.
|
|
1302
|
+
const settledPath = nodeSettledPoolsPath(artifactsDir, runId);
|
|
1303
|
+
const settled = await readSettledPools(settledPath);
|
|
1304
|
+
// Hoisted so the coordinator's claim registry and the ownership gate's
|
|
1305
|
+
// heartbeat probe are the SAME file-backed instance — never independently
|
|
1306
|
+
// re-derived by path (the coordinator's own `claimRegistry` field is private).
|
|
1307
|
+
const hybridClaimRegistry = nodeClaimRegistry(artifactsDir, runId);
|
|
1308
|
+
const partition = await planHybridDispatch({
|
|
1309
|
+
frontier,
|
|
1310
|
+
pools: confirmedPools,
|
|
1311
|
+
sessionConfig: sessionConfigImpl ?? {},
|
|
1312
|
+
claimRegistry: hybridClaimRegistry,
|
|
1313
|
+
readSettled: () => settled,
|
|
1314
|
+
onSettle: async (id) => {
|
|
1315
|
+
settled.add(id);
|
|
1316
|
+
await addSettledPool(settledPath, id);
|
|
1317
|
+
},
|
|
1318
|
+
isInProcess: isInProcessPool,
|
|
1319
|
+
});
|
|
1320
|
+
// Drive the in-process partition through the ROLLING ENGINE (H2 plan D2 —
|
|
1321
|
+
// `executeInProcessPartition`'s direct Promise.all executor is deleted; one
|
|
1322
|
+
// core, one driver): each node runs on a coordinator-claimed backend pool,
|
|
1323
|
+
// launched FROM that pool's source config, with the engine's full hook set
|
|
1324
|
+
// live (413 → packet_too_large re-queue + friction, verbatim quota harvest,
|
|
1325
|
+
// cost-drift / credit-exhaustion / model-unavailable capture) instead of the
|
|
1326
|
+
// hand-replicated friction blocks this replaces. The coordinator's claims
|
|
1327
|
+
// are ADOPTED (claimOwnerTokens) — same registry, no self-collision — and
|
|
1328
|
+
// run-level lifecycle stays HERE: the drive is partition-scoped
|
|
1329
|
+
// (blocksOverride), so a backend-only wall never persists a run terminal and
|
|
1330
|
+
// the final merge remains this caller's.
|
|
1331
|
+
if (partition.inProcess.length > 0) {
|
|
1332
|
+
// DC-4 (review h2c3 F1): the engine's per-packet selection binds freely
|
|
1333
|
+
// across `poolsOverride`, so a pool settled on a PRIOR cycle must be
|
|
1334
|
+
// filtered out here — the coordinator's claim walk already excluded it,
|
|
1335
|
+
// and re-offering it would re-die on the same dead pool every cycle.
|
|
1336
|
+
const liveBackendPools = backendPools.filter((p) => !settled.has(p.id));
|
|
1337
|
+
const driven = await driveRollingImplementDispatch({
|
|
1338
|
+
root,
|
|
1339
|
+
artifactsDir,
|
|
1340
|
+
runId,
|
|
1341
|
+
sessionConfig: sessionConfigImpl ?? null,
|
|
1342
|
+
rebuildSharedBetweenLevels: async () => { },
|
|
1343
|
+
waveOptions: {
|
|
1344
|
+
hostMaxConcurrent: resolvedHostMaxConcurrent,
|
|
1345
|
+
hostContextTokens: resolvedHostContextTokens,
|
|
1346
|
+
hostOutputTokens: resolvedHostOutputTokens,
|
|
1347
|
+
hostModels: resolvedHostModels,
|
|
1348
|
+
hostModelId: resolvedHostModelId,
|
|
1349
|
+
},
|
|
1350
|
+
blocksOverride: partition.inProcess.map((a) => a.nodeId),
|
|
1351
|
+
poolsOverride: liveBackendPools,
|
|
1352
|
+
planOverride: plan,
|
|
1353
|
+
claimOwnerTokens: new Map(partition.inProcess.map((a) => [a.nodeId, a.ownerToken])),
|
|
1354
|
+
});
|
|
1355
|
+
// Free any coordinator claim still held (a non-terminal outcome keeps its
|
|
1356
|
+
// claim through the drive); terminal accepts already released in-driver,
|
|
1357
|
+
// so this is a token-checked no-op for them.
|
|
1358
|
+
for (const a of partition.inProcess) {
|
|
1359
|
+
await partition.coordinator.release(a);
|
|
1360
|
+
}
|
|
1361
|
+
// DC-4 cross-cycle settle (D2 iii — PRESERVED across the engine
|
|
1362
|
+
// migration): a backend pool whose node rate-limited, credit-exhausted,
|
|
1363
|
+
// went model-unavailable, or died quota-unclassified → settle it
|
|
1364
|
+
// (cross-cycle) so the next cycle routes its share to the host pool. This
|
|
1365
|
+
// partition spans multiple cycles, each with a fresh in-process dispatcher
|
|
1366
|
+
// (fresh in-memory exhaustedPoolIds AND pausedPoolResetAt), so the
|
|
1367
|
+
// engine's reversible pause evaporates at the cycle boundary — the BROAD
|
|
1368
|
+
// `isPoolSettlingOutcome` predicate (incl. reset-bearing 429 +
|
|
1369
|
+
// quota_unclassified; see settledPools.ts for the divergence rationale)
|
|
1370
|
+
// therefore applies over the drive's real per-node pool attribution, plus
|
|
1371
|
+
// the engine's own terminal-exhaustion set. `packet_too_large` is
|
|
1372
|
+
// deliberately NOT a settle trigger (step D): a 413 is a per-(node,pool)
|
|
1373
|
+
// sizing fact, surfaced via the engine's onPacketTooLarge friction hook.
|
|
1374
|
+
// `driven` is null only when the plan/state vanished mid-cycle (nothing was
|
|
1375
|
+
// dispatched) — no outcomes ⇒ nothing to settle.
|
|
1376
|
+
const settlingPoolIds = new Set((driven?.nodes ?? [])
|
|
1377
|
+
.filter((n) => isPoolSettlingOutcome(n.outcome))
|
|
1378
|
+
.map((n) => n.pool_id));
|
|
1379
|
+
for (const poolId of driven?.exhausted_pool_ids ?? []) {
|
|
1380
|
+
settlingPoolIds.add(poolId);
|
|
1381
|
+
}
|
|
1382
|
+
const liveBackendPoolIds = new Set(liveBackendPools.map((p) => p.id));
|
|
1383
|
+
for (const poolId of settlingPoolIds) {
|
|
1384
|
+
if (liveBackendPoolIds.has(poolId)) {
|
|
1385
|
+
await partition.coordinator.settlePool(poolId);
|
|
1386
|
+
// The settle FACT itself is reviewable friction (review h2c3 F2):
|
|
1387
|
+
// the engine hooks capture the per-death evidence (verbatim 429
|
|
1388
|
+
// text, credit exhaustion, 404), but reset-bearing rate limits have
|
|
1389
|
+
// no engine hook, and no hook records "this pool is now settled for
|
|
1390
|
+
// the run". One record per (run, pool) via the dedupe chokepoint.
|
|
1391
|
+
void captureStepBoundaryFriction(artifactsDir, runId, {
|
|
1392
|
+
eventType: "quota_escalation",
|
|
1393
|
+
discriminator: `pool-settled:${poolId}`,
|
|
1394
|
+
note: "Hybrid in-process backend pool settled for this run (rate-limited / credit-exhausted / model-unavailable / quota-unclassified); its remaining share routes to the host on later cycles.",
|
|
1395
|
+
severity: "high",
|
|
1396
|
+
category: "trap",
|
|
1397
|
+
area: "dispatch/quota",
|
|
1398
|
+
}, "remediate-code");
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
// The backend carried the whole batch (or every host node was contested by a
|
|
1403
|
+
// peer driver) → nothing for the host this cycle; merge what landed + transition.
|
|
1404
|
+
if (partition.host.length === 0) {
|
|
1405
|
+
const merged = await mergeImplementResults({ root, artifactsDir }, runId);
|
|
1406
|
+
return { kind: "transition", state: merged };
|
|
1407
|
+
}
|
|
1408
|
+
// Hand the host partition (pre-claimed) to the host-subagent driver.
|
|
1409
|
+
rolling = await prepareHostRollingDispatch({ root, artifactsDir }, runId, waveOptsImpl, {
|
|
1410
|
+
plan,
|
|
1411
|
+
partition: partition.host.map((a) => ({ block_id: a.nodeId, ownerToken: a.ownerToken })),
|
|
1412
|
+
});
|
|
1420
1413
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
// {eventType, runId, discriminator}, so a chronically-exhausted backend pool
|
|
1424
|
-
// re-hitting the same block_id across cycles collapses to one record.
|
|
1425
|
-
for (const blockId of settledOutcomeBlocks) {
|
|
1426
|
-
void captureStepBoundaryFriction(artifactsDir, runId, {
|
|
1427
|
-
eventType: "quota_escalation",
|
|
1428
|
-
discriminator: blockId,
|
|
1429
|
-
note: "A-8 hybrid in-process node rate-limited / credit-exhausted / model-unavailable / packet-too-large / quota-unclassified; its backend pool was settled for this run.",
|
|
1430
|
-
severity: "high",
|
|
1431
|
-
category: "trap",
|
|
1432
|
-
area: "dispatch/quota",
|
|
1433
|
-
}, "remediate-code");
|
|
1414
|
+
else {
|
|
1415
|
+
rolling = await prepareHostRollingDispatch({ root, artifactsDir }, runId, waveOptsImpl);
|
|
1434
1416
|
}
|
|
1435
|
-
//
|
|
1436
|
-
//
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1417
|
+
// Increment B residual (a): the hybrid host-subagent driver hit the cooldown wall
|
|
1418
|
+
// (admission over-granted the throttled set). Reconcile the reserved leases (the
|
|
1419
|
+
// pause skips the merge that would) and set the resumable `quota_paused` terminal
|
|
1420
|
+
// so the `partial_terminal` obligation emits it this same advance; the ungranted
|
|
1421
|
+
// nodes stay PENDING and re-dispatch on resume. The in-process partition (above)
|
|
1422
|
+
// already ran on its own cooldown-safe pool, so its work still lands.
|
|
1423
|
+
if (rolling.wall) {
|
|
1424
|
+
await reconcileAdmissionLeasesFromQuotaFile(rolling.quotaPath);
|
|
1425
|
+
const paused = await store.loadState();
|
|
1426
|
+
if (paused) {
|
|
1427
|
+
paused.partial_completion_terminal = buildQuotaPausedTerminal(rolling.wall.strandedBlockIds, rolling.wall.detected.earliestResetAt);
|
|
1428
|
+
await store.saveState(paused);
|
|
1429
|
+
}
|
|
1430
|
+
return { kind: "transition", state: paused };
|
|
1440
1431
|
}
|
|
1441
|
-
//
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
}
|
|
1447
|
-
else {
|
|
1448
|
-
rolling = await prepareHostRollingDispatch({ root, artifactsDir }, runId, waveOptsImpl);
|
|
1449
|
-
}
|
|
1450
|
-
// Increment B residual (a): the hybrid host-subagent driver hit the cooldown wall
|
|
1451
|
-
// (admission over-granted the throttled set). Reconcile the reserved leases (the
|
|
1452
|
-
// pause skips the merge that would) and set the resumable `quota_paused` terminal
|
|
1453
|
-
// so the `partial_terminal` obligation emits it this same advance; the ungranted
|
|
1454
|
-
// nodes stay PENDING and re-dispatch on resume. The in-process partition (above)
|
|
1455
|
-
// already ran on its own cooldown-safe pool, so its work still lands.
|
|
1456
|
-
if (rolling.wall) {
|
|
1457
|
-
await reconcileAdmissionLeasesFromQuotaFile(rolling.quotaPath);
|
|
1458
|
-
const paused = await store.loadState();
|
|
1459
|
-
if (paused) {
|
|
1460
|
-
paused.partial_completion_terminal = buildQuotaPausedTerminal(rolling.wall.strandedBlockIds, rolling.wall.detected.earliestResetAt);
|
|
1461
|
-
await store.saveState(paused);
|
|
1432
|
+
// Everything eligible may already be done/skipped — fold straight to merge
|
|
1433
|
+
// rather than emitting a dispatch step with zero nodes.
|
|
1434
|
+
if (rolling.session.frontier.length === 0) {
|
|
1435
|
+
await mergeImplementResults({ root, artifactsDir }, runId);
|
|
1436
|
+
return { kind: "transition", state: await store.loadState() };
|
|
1462
1437
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
.join("\n");
|
|
1489
|
-
return { kind: "emit", step: await writeCurrentStep({
|
|
1490
|
-
stepKind: "dispatch_implement_rolling",
|
|
1491
|
-
status: "ready",
|
|
1492
|
-
runId,
|
|
1493
|
-
repoRoot: root,
|
|
1494
|
-
artifactsDir,
|
|
1495
|
-
prompt: `
|
|
1438
|
+
// S-BROKER-WIRING: pick the dispatch DRIVER (delegate the rolling loop to a
|
|
1439
|
+
// dedicated dispatcher subagent vs. drive it from the top host) off the
|
|
1440
|
+
// single classification + the live frontier/slot count — not host prose.
|
|
1441
|
+
const hostProvider = resolveHostProviderName(sessionConfigImpl);
|
|
1442
|
+
const driverSelection = selectDispatchDriver({
|
|
1443
|
+
classification: classifyProvider(hostProvider),
|
|
1444
|
+
eligibleItemCount: rolling.session.frontier.length,
|
|
1445
|
+
// The granted set's size IS the instantaneous admission width — there is no
|
|
1446
|
+
// separate concurrency number. The whole granted set runs at once, so the
|
|
1447
|
+
// driver-selection "slots" is the granted-set size.
|
|
1448
|
+
slots: rolling.session.frontier.length,
|
|
1449
|
+
});
|
|
1450
|
+
const rollMerge = loaderCommand(`merge-implement-results --run-id ${runId}`);
|
|
1451
|
+
const rollNext = loaderCommand("next-step");
|
|
1452
|
+
const acceptCmd = loaderCommand(`accept-node --id <BLOCK_ID> --run-id ${runId}`);
|
|
1453
|
+
const nodeLines = rolling.initial
|
|
1454
|
+
.map((n) => `- \`${n.block_id}\` — prompt: \`${n.prompt_path}\` — worktree (subagent cwd): \`${n.worktree_root}\``)
|
|
1455
|
+
.join("\n");
|
|
1456
|
+
return { kind: "emit", step: await writeCurrentStep({
|
|
1457
|
+
stepKind: "dispatch_implement_rolling",
|
|
1458
|
+
status: "ready",
|
|
1459
|
+
runId,
|
|
1460
|
+
repoRoot: root,
|
|
1461
|
+
artifactsDir,
|
|
1462
|
+
prompt: `
|
|
1496
1463
|
# Dispatch Implementation Work (host-subagent rolling, worktree-isolated)
|
|
1497
1464
|
|
|
1498
1465
|
Each granted node runs in its OWN git worktree (hard isolation between nodes). The
|
|
@@ -1537,10 +1504,11 @@ ${DISPATCH_PROMPT_HANDOFF_NOTE}
|
|
|
1537
1504
|
|
|
1538
1505
|
${renderHostScratchNote(hostScratchDir(artifactsDir, runId))}
|
|
1539
1506
|
`,
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1507
|
+
allowedCommands: [acceptCmd, rollMerge, rollNext],
|
|
1508
|
+
stopCondition: "Stop after every node has been accepted (accept-node returns done), results merged, and next-step has been run.",
|
|
1509
|
+
artifactPaths: { dispatch_plan: rolling.planPath, dispatch_quota: rolling.quotaPath },
|
|
1510
|
+
}) };
|
|
1511
|
+
}
|
|
1544
1512
|
}
|
|
1545
1513
|
// Rolling per-node dispatch: prepare EVERY currently-eligible node (deps all
|
|
1546
1514
|
// verified-complete), never a single artificially-serialized block. There is
|
|
@@ -1627,6 +1595,9 @@ Then run:
|
|
|
1627
1595
|
grantedCount: implQuota?.admission?.granted_packet_ids?.length ?? 0,
|
|
1628
1596
|
cooldownUntil: implQuota?.cooldown_until ?? null,
|
|
1629
1597
|
bindingWindow: implBindingWindow,
|
|
1598
|
+
// Step E parity: classify the zero-grant cause so the quota_paused terminal's
|
|
1599
|
+
// reset semantics stay honest (a no_capable_pool wall has no reset to wait for).
|
|
1600
|
+
explains: implQuota?.admission?.explains ?? [],
|
|
1630
1601
|
now: Date.now(),
|
|
1631
1602
|
});
|
|
1632
1603
|
if (implWall.atWall) {
|
|
@@ -1638,7 +1609,7 @@ Then run:
|
|
|
1638
1609
|
.filter((id) => typeof id === "string");
|
|
1639
1610
|
const paused = await store.loadState();
|
|
1640
1611
|
if (paused) {
|
|
1641
|
-
paused.partial_completion_terminal = buildQuotaPausedTerminal(strandedIds, implWall.earliestResetAt);
|
|
1612
|
+
paused.partial_completion_terminal = buildQuotaPausedTerminal(strandedIds, implWall.earliestResetAt, implWall.emptyGrantCause);
|
|
1642
1613
|
await store.saveState(paused);
|
|
1643
1614
|
}
|
|
1644
1615
|
return { kind: "transition", state: paused };
|
|
@@ -1701,8 +1672,34 @@ Then run:
|
|
|
1701
1672
|
* clean. Emitted (not blocked) so the run is resumable, never a failure.
|
|
1702
1673
|
*/
|
|
1703
1674
|
async function buildQuotaPausedStep(params) {
|
|
1704
|
-
const { root, artifactsDir, runId, strandedIds, resetAt } = params;
|
|
1675
|
+
const { root, artifactsDir, runId, strandedIds, resetAt, emptyGrantCause } = params;
|
|
1705
1676
|
const nextCommand = loaderCommand("next-step");
|
|
1677
|
+
// Honest pause (step E): a no_capable_pool zero-grant is a structural fit
|
|
1678
|
+
// mismatch — telling the operator to wait for a reset that will never clear it
|
|
1679
|
+
// would strand the run indefinitely (D+E review F2).
|
|
1680
|
+
if (emptyGrantCause === "no_capable_pool") {
|
|
1681
|
+
return writeCurrentStep({
|
|
1682
|
+
stepKind: "quota_paused",
|
|
1683
|
+
status: "ready",
|
|
1684
|
+
runId,
|
|
1685
|
+
repoRoot: root,
|
|
1686
|
+
artifactsDir,
|
|
1687
|
+
prompt: `
|
|
1688
|
+
# Remediation paused — no available pool fits the work
|
|
1689
|
+
|
|
1690
|
+
${strandedIds.length} node(s) could not be granted because they exceed the context
|
|
1691
|
+
window (or capability) of every pool currently available — a fit mismatch, NOT a
|
|
1692
|
+
quota wall, so waiting for a reset will not clear it. The nodes remain PENDING.
|
|
1693
|
+
|
|
1694
|
+
Options: free a larger pool (un-exclude one at the provider gate, or declare one),
|
|
1695
|
+
or shrink the oversized nodes' scope; then run:
|
|
1696
|
+
|
|
1697
|
+
\`${nextCommand}\`
|
|
1698
|
+
`,
|
|
1699
|
+
allowedCommands: [nextCommand],
|
|
1700
|
+
stopCondition: "No available pool fits the stranded nodes — free a larger pool or shrink the work, then re-run next-step.",
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1706
1703
|
const resetLine = resetAt
|
|
1707
1704
|
? `The earliest provider reset is \`${resetAt}\`. Wait until then, then run:`
|
|
1708
1705
|
: `Wait for the provider session limit to reset, then run:`;
|
|
@@ -3692,6 +3689,7 @@ function buildMainObligations(ctx) {
|
|
|
3692
3689
|
// the resuming step starts fresh; the pending nodes are the durable signal.
|
|
3693
3690
|
if (terminal.reason === "quota_paused") {
|
|
3694
3691
|
const resetAt = terminal.earliest_reset_at ?? null;
|
|
3692
|
+
const emptyGrantCause = terminal.empty_grant_cause ?? null;
|
|
3695
3693
|
delete s.partial_completion_terminal;
|
|
3696
3694
|
await store.saveState(s);
|
|
3697
3695
|
return {
|
|
@@ -3702,6 +3700,7 @@ function buildMainObligations(ctx) {
|
|
|
3702
3700
|
runId: stateRunId(s),
|
|
3703
3701
|
strandedIds: terminal.stranded_ids ?? [],
|
|
3704
3702
|
resetAt,
|
|
3703
|
+
emptyGrantCause,
|
|
3705
3704
|
}),
|
|
3706
3705
|
};
|
|
3707
3706
|
}
|