@lsctech/polaris 0.4.9 → 0.5.0
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/autoresearch/proposal.js +70 -1
- package/dist/autoresearch/score.js +97 -0
- package/dist/cli/init.js +17 -0
- package/dist/cluster-state/store.js +35 -0
- package/dist/config/defaults.js +8 -0
- package/dist/config/validator.js +146 -0
- package/dist/loop/adapters/agent-subtask.js +19 -0
- package/dist/loop/adapters/terminal-cli.js +194 -11
- package/dist/loop/checkpoint.js +40 -0
- package/dist/loop/dispatch.js +193 -22
- package/dist/loop/orphan-recovery.js +56 -0
- package/dist/loop/parent.js +148 -21
- package/dist/loop/router/engine.js +229 -0
- package/dist/loop/router/index.js +5 -0
- package/dist/loop/router/types.js +2 -0
- package/dist/loop/worker-packet.js +16 -0
- package/dist/runtime/scheduling/child-selector.js +81 -0
- package/package.json +1 -1
|
@@ -64,6 +64,42 @@ function hasTelemetryEvent(telemetryFile, eventName, childId) {
|
|
|
64
64
|
catch { /* ignore */ }
|
|
65
65
|
return false;
|
|
66
66
|
}
|
|
67
|
+
function latestProviderFailureReason(telemetryFile, childId, dispatchId) {
|
|
68
|
+
if (!(0, node_fs_1.existsSync)(telemetryFile))
|
|
69
|
+
return null;
|
|
70
|
+
try {
|
|
71
|
+
const lines = (0, node_fs_1.readFileSync)(telemetryFile, "utf-8").trim().split("\n").filter(Boolean);
|
|
72
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
73
|
+
const line = lines[i];
|
|
74
|
+
if (!line)
|
|
75
|
+
continue;
|
|
76
|
+
try {
|
|
77
|
+
const event = JSON.parse(line);
|
|
78
|
+
if (event.child_id !== childId)
|
|
79
|
+
continue;
|
|
80
|
+
if (event.dispatch_id && event.dispatch_id !== dispatchId)
|
|
81
|
+
continue;
|
|
82
|
+
const category = typeof event.failure_category === "string" ? event.failure_category : undefined;
|
|
83
|
+
const reason = typeof event.reason === "string" ? event.reason : undefined;
|
|
84
|
+
if (category === "quota-exhausted" || reason === "quota-exhausted") {
|
|
85
|
+
return "quota-exhausted";
|
|
86
|
+
}
|
|
87
|
+
if (category === "provider-unavailable" ||
|
|
88
|
+
reason === "provider-unavailable" ||
|
|
89
|
+
event.event === "provider-probe-failed") {
|
|
90
|
+
return "provider-unavailable";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
67
103
|
// ── Emit recovery telemetry events ───────────────────────────────────────────
|
|
68
104
|
function emitRecoveryInitiated(telemetryFile, childId, dispatchId, reason) {
|
|
69
105
|
appendTelemetry(telemetryFile, {
|
|
@@ -187,6 +223,26 @@ function checkOrphans(options) {
|
|
|
187
223
|
if (dr && dr.runtime_state !== "completed" && dr.runtime_state !== "failed") {
|
|
188
224
|
const dispatchedAt = new Date(dr.dispatched_at).getTime();
|
|
189
225
|
const elapsed = now - dispatchedAt;
|
|
226
|
+
const providerFailure = latestProviderFailureReason(telemetryFile, state.active_child, dr.dispatch_id);
|
|
227
|
+
// Provider launch failure detected before worker-start evidence.
|
|
228
|
+
// Safe to requeue because no worker result artifact exists and there is
|
|
229
|
+
// no heartbeat/acknowledgment evidence that execution started.
|
|
230
|
+
if (providerFailure &&
|
|
231
|
+
!safeResultExists(dr.expected_result_path) &&
|
|
232
|
+
!dr.first_heartbeat_at) {
|
|
233
|
+
const detection = {
|
|
234
|
+
childId: state.active_child,
|
|
235
|
+
dispatchId: dr.dispatch_id,
|
|
236
|
+
reason: providerFailure,
|
|
237
|
+
requiresApproval: false,
|
|
238
|
+
};
|
|
239
|
+
detected.push(detection);
|
|
240
|
+
emitRecoveryInitiated(telemetryFile, state.active_child, dr.dispatch_id, providerFailure);
|
|
241
|
+
emitChildOrphaned(telemetryFile, state.active_child, dr.dispatch_id, null);
|
|
242
|
+
const newId = resetForRedispatch(options.stateFile, state, state.active_child);
|
|
243
|
+
emitChildRequeued(telemetryFile, state.active_child, newId, dr.dispatch_id);
|
|
244
|
+
return { detected, checked };
|
|
245
|
+
}
|
|
190
246
|
// Scenario A: packet written, no worker_id after launch_timeout
|
|
191
247
|
if (!dr.worker_id && elapsed > timeouts.launchTimeoutMs) {
|
|
192
248
|
const detection = {
|
package/dist/loop/parent.js
CHANGED
|
@@ -39,8 +39,11 @@ const dispatch_boundary_js_1 = require("./dispatch-boundary.js");
|
|
|
39
39
|
const run_bootstrap_js_1 = require("./run-bootstrap.js");
|
|
40
40
|
const ledger_js_1 = require("./ledger.js");
|
|
41
41
|
const dispatch_js_1 = require("./dispatch.js");
|
|
42
|
-
const index_js_1 = require("
|
|
42
|
+
const index_js_1 = require("./router/index.js");
|
|
43
|
+
const child_selector_js_1 = require("../runtime/scheduling/child-selector.js");
|
|
44
|
+
const index_js_2 = require("../tracker/index.js");
|
|
43
45
|
const lifecycle_transition_js_1 = require("../tracker/lifecycle-transition.js");
|
|
46
|
+
const local_graph_js_1 = require("../tracker/local-graph.js");
|
|
44
47
|
const CLAIM_TTL_MS = 30 * 60 * 1000;
|
|
45
48
|
/**
|
|
46
49
|
* Returns the list of files touched by a git commit, or null when the commit
|
|
@@ -130,13 +133,6 @@ function emitBootstrapContextSize(telemetryFile, runId, childId, stateFile, pack
|
|
|
130
133
|
timestamp: new Date().toISOString(),
|
|
131
134
|
});
|
|
132
135
|
}
|
|
133
|
-
/**
|
|
134
|
-
* Select the next open child that is not Done/blocked.
|
|
135
|
-
* Returns null when all children are completed.
|
|
136
|
-
*/
|
|
137
|
-
function selectNextChild(state) {
|
|
138
|
-
return state.open_children[0] ?? null;
|
|
139
|
-
}
|
|
140
136
|
/**
|
|
141
137
|
* Returns true when the given child is detected as an analyze issue.
|
|
142
138
|
* Detection is intentionally conservative: title prefix or label match only.
|
|
@@ -212,7 +208,7 @@ function appendRunStartedLedgerEvent(repoRoot, state) {
|
|
|
212
208
|
timestamp: new Date().toISOString(),
|
|
213
209
|
});
|
|
214
210
|
}
|
|
215
|
-
function appendChildCompletedLedgerEvent(repoRoot, state, completedChild, lastCommit, validation) {
|
|
211
|
+
function appendChildCompletedLedgerEvent(repoRoot, state, completedChild, lastCommit, validation, completion) {
|
|
216
212
|
const writer = new ledger_js_1.LedgerWriter((0, node_path_1.join)(repoRoot, ledger_js_1.DEFAULT_LEDGER_PATH));
|
|
217
213
|
const base = {
|
|
218
214
|
schema_version: 1,
|
|
@@ -237,6 +233,18 @@ function appendChildCompletedLedgerEvent(repoRoot, state, completedChild, lastCo
|
|
|
237
233
|
validation: typeof validation === "object" && validation !== null
|
|
238
234
|
? { status: "complete", ...validation }
|
|
239
235
|
: { status: "complete" },
|
|
236
|
+
...(completion?.dispatchId ? { dispatch_id: completion.dispatchId } : {}),
|
|
237
|
+
provider: completion?.provider ?? null,
|
|
238
|
+
model: completion?.model ?? null,
|
|
239
|
+
...(completion?.elapsedSeconds !== undefined ? { elapsed_seconds: completion.elapsedSeconds } : {}),
|
|
240
|
+
...(completion?.commitFiles !== undefined ? { commit_files: completion.commitFiles } : {}),
|
|
241
|
+
...(completion?.completionStatus ? { completion_status: completion.completionStatus } : {}),
|
|
242
|
+
...(completion?.routerSelectionReason
|
|
243
|
+
? { router_selection_reason: completion.routerSelectionReason }
|
|
244
|
+
: {}),
|
|
245
|
+
...(completion?.providersTried?.length
|
|
246
|
+
? { providers_tried: completion.providersTried }
|
|
247
|
+
: {}),
|
|
240
248
|
});
|
|
241
249
|
}
|
|
242
250
|
function appendClusterCompletedLedgerEvent(repoRoot, state) {
|
|
@@ -275,7 +283,7 @@ function appendClusterCompletedLedgerEvent(repoRoot, state) {
|
|
|
275
283
|
* @param resultFile - Optional override path where the worker should write its result
|
|
276
284
|
* @returns The compiled WorkerPacket configured for `activeChild`
|
|
277
285
|
*/
|
|
278
|
-
function buildPacket(state, activeChild, stateFile, telemetryFile, repoRoot, resultFile) {
|
|
286
|
+
function buildPacket(state, activeChild, stateFile, telemetryFile, repoRoot, resultFile, maxConcurrentWorkers) {
|
|
279
287
|
const branch = state.branch ?? getCurrentBranch(repoRoot);
|
|
280
288
|
const childMeta = state.open_children_meta?.[activeChild];
|
|
281
289
|
// Hydrate body from cluster snapshot when runtime state lacks it.
|
|
@@ -312,7 +320,7 @@ function buildPacket(state, activeChild, stateFile, telemetryFile, repoRoot, res
|
|
|
312
320
|
telemetryFile,
|
|
313
321
|
issueContext,
|
|
314
322
|
allowedScope: resolvedScope.length > 0 ? resolvedScope : undefined,
|
|
315
|
-
maxConcurrentWorkers
|
|
323
|
+
maxConcurrentWorkers,
|
|
316
324
|
promptMode,
|
|
317
325
|
resultFile,
|
|
318
326
|
});
|
|
@@ -416,6 +424,8 @@ function buildParentDispatchRecord(args) {
|
|
|
416
424
|
packet_path: args.packetPath,
|
|
417
425
|
expected_result_path: args.resultPath,
|
|
418
426
|
provider: args.provider,
|
|
427
|
+
provider_selection_reason: args.providerSelectionReason,
|
|
428
|
+
providers_tried: args.providersTried,
|
|
419
429
|
dispatched_at: args.dispatchedAt,
|
|
420
430
|
status: "dispatched",
|
|
421
431
|
dispatch_mode: "direct-worker",
|
|
@@ -487,6 +497,26 @@ function advanceState(state, completedChild, lastCommit) {
|
|
|
487
497
|
},
|
|
488
498
|
};
|
|
489
499
|
}
|
|
500
|
+
function withWorkerPoolState(state, maxConcurrentWorkers, slotClaims) {
|
|
501
|
+
return {
|
|
502
|
+
...state,
|
|
503
|
+
worker_pool_state: {
|
|
504
|
+
max_concurrent: maxConcurrentWorkers,
|
|
505
|
+
slot_claims: slotClaims,
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function pruneWorkerPoolClaimsForChild(state, childId) {
|
|
510
|
+
if (!state.worker_pool_state)
|
|
511
|
+
return state;
|
|
512
|
+
return {
|
|
513
|
+
...state,
|
|
514
|
+
worker_pool_state: {
|
|
515
|
+
...state.worker_pool_state,
|
|
516
|
+
slot_claims: state.worker_pool_state.slot_claims.filter((claim) => claim.child_id !== childId),
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
}
|
|
490
520
|
async function syncClusterCompletion(args) {
|
|
491
521
|
const clusterState = await (0, store_js_1.readClusterState)(args.clusterId, args.repoRoot);
|
|
492
522
|
if (!clusterState) {
|
|
@@ -651,8 +681,12 @@ async function runParentLoop(options) {
|
|
|
651
681
|
const notificationFormat = config.orchestration?.notification_format ?? (orchestrationMode === 'auto' ? 'terse' : 'verbose');
|
|
652
682
|
const adapterName = legacyEphemeralMode ? "agent-subtask" : (options.adapter ?? config.execution?.adapter ?? "terminal-cli");
|
|
653
683
|
let providerName;
|
|
684
|
+
let providerSelectionReason;
|
|
685
|
+
let providersTried;
|
|
654
686
|
if (adapterName === "agent-subtask") {
|
|
655
687
|
providerName = "agent-subtask";
|
|
688
|
+
providerSelectionReason = "agent-subtask-adapter";
|
|
689
|
+
providersTried = ["agent-subtask"];
|
|
656
690
|
}
|
|
657
691
|
else {
|
|
658
692
|
let evidence;
|
|
@@ -668,6 +702,8 @@ async function runParentLoop(options) {
|
|
|
668
702
|
};
|
|
669
703
|
}
|
|
670
704
|
providerName = evidence.provider ?? "default";
|
|
705
|
+
providerSelectionReason = evidence.selectionReason;
|
|
706
|
+
providersTried = evidence.providersTried;
|
|
671
707
|
}
|
|
672
708
|
const executionConfig = adapterName === "agent-subtask"
|
|
673
709
|
? { ...(config.execution ?? { providers: {} }), adapter: "agent-subtask" }
|
|
@@ -675,6 +711,7 @@ async function runParentLoop(options) {
|
|
|
675
711
|
const adapter = (0, registry_js_1.createAdapter)(adapterName, executionConfig);
|
|
676
712
|
const budgetPolicy = (0, budget_js_1.policyFromConfig)(state.context_budget, config.budget);
|
|
677
713
|
const allowAnalyzeChildren = allowAnalyzeChildrenFlag || (config.budget?.allow_analyze_children === true);
|
|
714
|
+
const maxConcurrentWorkers = config.execution?.routerPolicy?.defaultWorkerPool?.maxActiveWorkers ?? 1;
|
|
678
715
|
const telemetryFile = resolveTelemetryFile(state, repoRoot);
|
|
679
716
|
// Enforce provider policy for explicit --provider flag before entering the loop.
|
|
680
717
|
// resolveProviderAndMode handles policy-filtered rotation; this gate blocks an
|
|
@@ -725,18 +762,80 @@ async function runParentLoop(options) {
|
|
|
725
762
|
if (!dryRun) {
|
|
726
763
|
flushBodiesToClusterSnapshot(state, repoRoot);
|
|
727
764
|
}
|
|
765
|
+
let localGraph = null;
|
|
766
|
+
try {
|
|
767
|
+
localGraph = await local_graph_js_1.LocalGraph.load(state.cluster_id, repoRoot);
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
localGraph = null;
|
|
771
|
+
}
|
|
728
772
|
// ── Lifecycle manager: enforce one-active-worker policy ─────────────────
|
|
729
773
|
// forceReleaseAll() clears any orphaned registrations from a previous
|
|
730
774
|
// crashed session. Registrations are session-memory only; they are not
|
|
731
775
|
// persisted to disk, so a fresh loop start always begins clean.
|
|
732
|
-
const lifecycle = new lifecycle_js_1.WorkerLifecycleManager(
|
|
776
|
+
const lifecycle = new lifecycle_js_1.WorkerLifecycleManager(maxConcurrentWorkers);
|
|
733
777
|
lifecycle.forceReleaseAll();
|
|
734
778
|
// ── Main dispatch loop ───────────────────────────────────────────────────
|
|
735
779
|
// eslint-disable-next-line no-constant-condition
|
|
736
780
|
while (true) {
|
|
737
781
|
// ── Step 02: Select next open child ─────────────────────────────────
|
|
738
|
-
const
|
|
782
|
+
const existingSlotClaims = state.worker_pool_state?.slot_claims ?? [];
|
|
783
|
+
if (!dryRun) {
|
|
784
|
+
const clusterState = await (0, store_js_1.readClusterState)(state.cluster_id, repoRoot);
|
|
785
|
+
if (clusterState) {
|
|
786
|
+
const pruned = (0, store_js_1.pruneExpiredClaims)(clusterState);
|
|
787
|
+
if (pruned.expiredChildIds.length > 0) {
|
|
788
|
+
await (0, store_js_1.writeClusterState)(state.cluster_id, pruned.state, repoRoot);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
const selection = (0, child_selector_js_1.selectChildSlotClaims)({
|
|
793
|
+
open_children: state.open_children,
|
|
794
|
+
completed_children: state.completed_children,
|
|
795
|
+
active_child: state.active_child || null,
|
|
796
|
+
existing_claims: existingSlotClaims,
|
|
797
|
+
max_concurrent: maxConcurrentWorkers,
|
|
798
|
+
claim_ttl_ms: CLAIM_TTL_MS,
|
|
799
|
+
get_dependencies: (childId) => localGraph?.getDependencies(childId) ?? [],
|
|
800
|
+
decide_route: ({ activeSlotsByProvider }) => (0, index_js_1.decideWorkerRoute)({
|
|
801
|
+
role: "worker",
|
|
802
|
+
taskType: "impl",
|
|
803
|
+
adapter: adapterName,
|
|
804
|
+
providerOverride: options.provider,
|
|
805
|
+
providers: Object.keys(config.execution?.providers ?? {}),
|
|
806
|
+
rotation: config.execution?.rotation ?? [],
|
|
807
|
+
rolePolicy: config.execution?.providerPolicy?.worker,
|
|
808
|
+
roleConfiguredProvider: config.execution?.roles?.worker?.provider,
|
|
809
|
+
routerPolicy: config.execution?.routerPolicy,
|
|
810
|
+
constraints: {
|
|
811
|
+
requiredCapabilities: ["implementation"],
|
|
812
|
+
},
|
|
813
|
+
runtime: {
|
|
814
|
+
activeSlotsByProvider,
|
|
815
|
+
},
|
|
816
|
+
compatibilityMode: false,
|
|
817
|
+
}),
|
|
818
|
+
});
|
|
819
|
+
const nextSlotClaims = selection.slot_claims;
|
|
820
|
+
const nextChild = selection.selected_child;
|
|
739
821
|
if (nextChild === null) {
|
|
822
|
+
if (state.open_children.length > 0) {
|
|
823
|
+
if (!dryRun) {
|
|
824
|
+
appendTelemetry(telemetryFile, {
|
|
825
|
+
event: "scheduler-no-eligible-child",
|
|
826
|
+
run_id: state.run_id,
|
|
827
|
+
open_children: state.open_children,
|
|
828
|
+
rejected_children: selection.rejected_children,
|
|
829
|
+
slot_claims: nextSlotClaims,
|
|
830
|
+
timestamp: new Date().toISOString(),
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
return {
|
|
834
|
+
haltReason: "blocked",
|
|
835
|
+
childrenDispatched,
|
|
836
|
+
message: "No schedulable child matched dependency and router slot constraints.",
|
|
837
|
+
};
|
|
838
|
+
}
|
|
740
839
|
const autoFinalizeRequested = orchestrationMode === "auto" && config.orchestration?.auto_finalize === true;
|
|
741
840
|
// All children completed — write final state and halt
|
|
742
841
|
if (!dryRun) {
|
|
@@ -979,7 +1078,7 @@ async function runParentLoop(options) {
|
|
|
979
1078
|
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(resultFile), { recursive: true });
|
|
980
1079
|
const statePreDispatch = state;
|
|
981
1080
|
let stateBeforeDispatch = state;
|
|
982
|
-
const packet = buildPacket(state, nextChild, stateFile, telemetryFile, repoRoot, resultFile);
|
|
1081
|
+
const packet = buildPacket(state, nextChild, stateFile, telemetryFile, repoRoot, resultFile, maxConcurrentWorkers);
|
|
983
1082
|
if (!dryRun) {
|
|
984
1083
|
try {
|
|
985
1084
|
writePacketArtifact(packetPath, packet);
|
|
@@ -1009,11 +1108,13 @@ async function runParentLoop(options) {
|
|
|
1009
1108
|
packetPath,
|
|
1010
1109
|
resultPath: resultFile,
|
|
1011
1110
|
provider: providerName,
|
|
1111
|
+
providerSelectionReason,
|
|
1112
|
+
providersTried,
|
|
1012
1113
|
workerId,
|
|
1013
1114
|
dispatchedAt,
|
|
1014
1115
|
});
|
|
1015
1116
|
const stateWithDispatch = {
|
|
1016
|
-
...withChildDispatchMetadata(state, nextChild, resultFile, dispatchRecord),
|
|
1117
|
+
...withWorkerPoolState(withChildDispatchMetadata(state, nextChild, resultFile, dispatchRecord), maxConcurrentWorkers, nextSlotClaims),
|
|
1017
1118
|
active_child: nextChild,
|
|
1018
1119
|
step_cursor: "dispatch",
|
|
1019
1120
|
dispatch_boundary: (0, dispatch_boundary_js_1.advanceDispatchEpoch)(state.dispatch_boundary, nextChild),
|
|
@@ -1066,6 +1167,7 @@ async function runParentLoop(options) {
|
|
|
1066
1167
|
if (!dryRun) {
|
|
1067
1168
|
const totalChildren = state.open_children.length + state.completed_children.length;
|
|
1068
1169
|
const childIndex = state.completed_children.length + 1;
|
|
1170
|
+
const selectedSlotClaim = nextSlotClaims.find((claim) => claim.child_id === nextChild);
|
|
1069
1171
|
logStatus(notificationFormat, `RUNNING ${nextChild} (${childIndex}/${totalChildren})`);
|
|
1070
1172
|
appendTelemetry(telemetryFile, {
|
|
1071
1173
|
event: "child-dispatched",
|
|
@@ -1080,6 +1182,8 @@ async function runParentLoop(options) {
|
|
|
1080
1182
|
packet_path: packetPath,
|
|
1081
1183
|
expected_result_path: resultFile,
|
|
1082
1184
|
dry_run: dryRun,
|
|
1185
|
+
selected_slot_claim: selectedSlotClaim ?? null,
|
|
1186
|
+
slot_claims: nextSlotClaims,
|
|
1083
1187
|
timestamp: new Date().toISOString(),
|
|
1084
1188
|
});
|
|
1085
1189
|
emitBootstrapContextSize(telemetryFile, state.run_id, nextChild, stateFile, packet);
|
|
@@ -1229,7 +1333,7 @@ async function runParentLoop(options) {
|
|
|
1229
1333
|
// Errors are logged to telemetry but do not fail the halt.
|
|
1230
1334
|
let adapter;
|
|
1231
1335
|
try {
|
|
1232
|
-
adapter = (0,
|
|
1336
|
+
adapter = (0, index_js_2.loadTrackerAdapter)(config);
|
|
1233
1337
|
}
|
|
1234
1338
|
catch (err) {
|
|
1235
1339
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -1423,6 +1527,14 @@ async function runParentLoop(options) {
|
|
|
1423
1527
|
finalWorkerSummary?.['commit_hash'];
|
|
1424
1528
|
const validationSummary = finalWorkerSummary?.['validation'] ??
|
|
1425
1529
|
finalWorkerSummary?.['validation_summary'];
|
|
1530
|
+
const dispatchRecord = state.open_children_meta?.[nextChild]?.dispatch_record;
|
|
1531
|
+
const providerUsed = finalWorkerSummary?.['provider'] ??
|
|
1532
|
+
finalWorkerSummary?.['provider_used'] ??
|
|
1533
|
+
dispatchRecord?.provider ??
|
|
1534
|
+
null;
|
|
1535
|
+
const modelUsed = finalWorkerSummary?.['model'] ??
|
|
1536
|
+
finalWorkerSummary?.['model_used'] ??
|
|
1537
|
+
null;
|
|
1426
1538
|
if (!dryRun && workerStatus === 'done' && (!lastCommit || lastCommit.trim().length === 0)) {
|
|
1427
1539
|
const errMsg = `Worker reported done for ${nextChild} without commit evidence`;
|
|
1428
1540
|
appendTelemetry(telemetryFile, {
|
|
@@ -1561,14 +1673,14 @@ async function runParentLoop(options) {
|
|
|
1561
1673
|
}
|
|
1562
1674
|
// Advance state, including continue_epoch to match the consumed dispatch
|
|
1563
1675
|
const advanced = advanceState(state, nextChild, lastCommit);
|
|
1564
|
-
state = {
|
|
1676
|
+
state = pruneWorkerPoolClaimsForChild({
|
|
1565
1677
|
...advanced,
|
|
1566
1678
|
dispatch_boundary: (0, dispatch_boundary_js_1.advanceContinueEpoch)(state.dispatch_boundary),
|
|
1567
1679
|
completed_children_results: {
|
|
1568
1680
|
...advanced.completed_children_results,
|
|
1569
1681
|
[nextChild]: workerResult,
|
|
1570
1682
|
},
|
|
1571
|
-
};
|
|
1683
|
+
}, nextChild);
|
|
1572
1684
|
childrenDispatched += 1;
|
|
1573
1685
|
// Worker did not write its own completion — orchestrator fills the gap.
|
|
1574
1686
|
if (!dryRun) {
|
|
@@ -1578,14 +1690,14 @@ async function runParentLoop(options) {
|
|
|
1578
1690
|
else {
|
|
1579
1691
|
// Worker wrote its own completion — advance continue_epoch to stay in sync.
|
|
1580
1692
|
// The worker does not manage dispatch_boundary, so the parent must do it.
|
|
1581
|
-
state = {
|
|
1693
|
+
state = pruneWorkerPoolClaimsForChild({
|
|
1582
1694
|
...state,
|
|
1583
1695
|
dispatch_boundary: (0, dispatch_boundary_js_1.advanceContinueEpoch)(state.dispatch_boundary),
|
|
1584
1696
|
completed_children_results: {
|
|
1585
1697
|
...state.completed_children_results,
|
|
1586
1698
|
[nextChild]: workerResult,
|
|
1587
1699
|
},
|
|
1588
|
-
};
|
|
1700
|
+
}, nextChild);
|
|
1589
1701
|
childrenDispatched += 1;
|
|
1590
1702
|
if (!dryRun) {
|
|
1591
1703
|
(0, checkpoint_js_1.writeStateAtomic)(stateFile, state);
|
|
@@ -1641,15 +1753,30 @@ async function runParentLoop(options) {
|
|
|
1641
1753
|
child_id: nextChild,
|
|
1642
1754
|
children_completed: state.context_budget.children_completed,
|
|
1643
1755
|
validation_summary: validationSummary,
|
|
1756
|
+
completion_status: workerStatus,
|
|
1644
1757
|
commit_hash: lastCommit,
|
|
1645
1758
|
commit_files: commitFiles,
|
|
1759
|
+
dispatch_id: dispatchRecord?.dispatch_id,
|
|
1760
|
+
provider: providerUsed,
|
|
1761
|
+
model: modelUsed,
|
|
1762
|
+
router_selection_reason: dispatchRecord?.provider_selection_reason,
|
|
1763
|
+
providers_tried: dispatchRecord?.providers_tried,
|
|
1646
1764
|
timestamp: new Date().toISOString(),
|
|
1647
1765
|
};
|
|
1648
1766
|
if (elapsedSeconds !== undefined) {
|
|
1649
1767
|
telemetryEvent.elapsed_seconds = elapsedSeconds;
|
|
1650
1768
|
}
|
|
1651
1769
|
appendTelemetry(telemetryFile, telemetryEvent);
|
|
1652
|
-
appendChildCompletedLedgerEvent(repoRoot, state, nextChild, lastCommit ?? null, validationSummary
|
|
1770
|
+
appendChildCompletedLedgerEvent(repoRoot, state, nextChild, lastCommit ?? null, validationSummary, {
|
|
1771
|
+
dispatchId: dispatchRecord?.dispatch_id,
|
|
1772
|
+
provider: providerUsed,
|
|
1773
|
+
model: modelUsed,
|
|
1774
|
+
elapsedSeconds,
|
|
1775
|
+
commitFiles,
|
|
1776
|
+
completionStatus: workerStatus,
|
|
1777
|
+
routerSelectionReason: dispatchRecord?.provider_selection_reason,
|
|
1778
|
+
providersTried: dispatchRecord?.providers_tried,
|
|
1779
|
+
});
|
|
1653
1780
|
}
|
|
1654
1781
|
if (orchestrationMode === 'supervised') {
|
|
1655
1782
|
return {
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decideWorkerRoute = decideWorkerRoute;
|
|
4
|
+
const TRUST_RANK = {
|
|
5
|
+
sandbox: 0,
|
|
6
|
+
standard: 1,
|
|
7
|
+
trusted: 2,
|
|
8
|
+
};
|
|
9
|
+
const COST_RANK = {
|
|
10
|
+
low: 0,
|
|
11
|
+
medium: 1,
|
|
12
|
+
high: 2,
|
|
13
|
+
};
|
|
14
|
+
function defaultCapabilitiesForTask(taskType) {
|
|
15
|
+
switch (taskType) {
|
|
16
|
+
case "startup":
|
|
17
|
+
return ["orchestration"];
|
|
18
|
+
case "analyze":
|
|
19
|
+
return ["analysis"];
|
|
20
|
+
case "impl":
|
|
21
|
+
return ["implementation"];
|
|
22
|
+
case "repair":
|
|
23
|
+
return ["repair"];
|
|
24
|
+
case "docs":
|
|
25
|
+
return ["docs"];
|
|
26
|
+
case "finalize":
|
|
27
|
+
return ["finalization"];
|
|
28
|
+
default:
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function orderedProviders(input) {
|
|
33
|
+
if (input.providerOverride) {
|
|
34
|
+
return [input.providerOverride];
|
|
35
|
+
}
|
|
36
|
+
const roleProviders = input.rolePolicy?.providers ?? [];
|
|
37
|
+
const rotation = input.rotation ?? [];
|
|
38
|
+
if (input.compatibilityMode !== false) {
|
|
39
|
+
if (roleProviders.length > 0) {
|
|
40
|
+
const filteredRotation = rotation.filter((provider) => roleProviders.includes(provider));
|
|
41
|
+
if (filteredRotation.length > 0) {
|
|
42
|
+
return filteredRotation;
|
|
43
|
+
}
|
|
44
|
+
const configuredProviders = new Set(input.providers);
|
|
45
|
+
return roleProviders.filter((provider) => configuredProviders.has(provider));
|
|
46
|
+
}
|
|
47
|
+
if (input.roleConfiguredProvider) {
|
|
48
|
+
return [input.roleConfiguredProvider];
|
|
49
|
+
}
|
|
50
|
+
if (rotation.length > 0) {
|
|
51
|
+
return rotation;
|
|
52
|
+
}
|
|
53
|
+
return input.providers;
|
|
54
|
+
}
|
|
55
|
+
const preferred = rotation.length > 0 ? rotation : input.providers;
|
|
56
|
+
if (roleProviders.length === 0) {
|
|
57
|
+
return preferred;
|
|
58
|
+
}
|
|
59
|
+
const configuredAndAllowed = new Set(roleProviders.filter((provider) => input.providers.includes(provider)));
|
|
60
|
+
const ordered = preferred.filter((provider) => configuredAndAllowed.has(provider));
|
|
61
|
+
if (ordered.length > 0) {
|
|
62
|
+
return ordered;
|
|
63
|
+
}
|
|
64
|
+
return roleProviders.filter((provider) => input.providers.includes(provider));
|
|
65
|
+
}
|
|
66
|
+
function compatibilitySelectionReason(input, selectedProvider) {
|
|
67
|
+
if (input.providerOverride)
|
|
68
|
+
return "cli-provider-override";
|
|
69
|
+
if (!selectedProvider)
|
|
70
|
+
return "delegated-no-provider";
|
|
71
|
+
const roleProviders = input.rolePolicy?.providers ?? [];
|
|
72
|
+
if (roleProviders.length > 0) {
|
|
73
|
+
const rotation = input.rotation ?? [];
|
|
74
|
+
if (rotation.filter((provider) => roleProviders.includes(provider))[0] === selectedProvider) {
|
|
75
|
+
return "policy-filtered-rotation";
|
|
76
|
+
}
|
|
77
|
+
return "role-policy";
|
|
78
|
+
}
|
|
79
|
+
if (input.roleConfiguredProvider === selectedProvider)
|
|
80
|
+
return "role-config";
|
|
81
|
+
if ((input.rotation ?? [])[0] === selectedProvider)
|
|
82
|
+
return "config-rotation";
|
|
83
|
+
return "config-first-provider";
|
|
84
|
+
}
|
|
85
|
+
function compatibilityExhaustedReason(input) {
|
|
86
|
+
const roleProviders = input.rolePolicy?.providers ?? [];
|
|
87
|
+
if (roleProviders.length > 0)
|
|
88
|
+
return "role-policy-no-configured-provider";
|
|
89
|
+
return "no-configured-provider";
|
|
90
|
+
}
|
|
91
|
+
function topRejectionReason(candidates) {
|
|
92
|
+
const orderedReasons = [
|
|
93
|
+
"role-disabled",
|
|
94
|
+
"not-in-policy",
|
|
95
|
+
"quota-exhausted",
|
|
96
|
+
"trust-too-low",
|
|
97
|
+
"capability-mismatch",
|
|
98
|
+
"cost-policy",
|
|
99
|
+
"no-slot",
|
|
100
|
+
];
|
|
101
|
+
for (const reason of orderedReasons) {
|
|
102
|
+
if (candidates.some((candidate) => candidate.rejectionReasons.includes(reason))) {
|
|
103
|
+
return reason;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
function decideWorkerRoute(input) {
|
|
109
|
+
const compatibilityMode = input.compatibilityMode !== false;
|
|
110
|
+
const providerRegistry = input.routerPolicy?.providerRegistry ?? {};
|
|
111
|
+
const roleProviders = input.rolePolicy?.providers ?? [];
|
|
112
|
+
const requiredCapabilities = input.constraints?.requiredCapabilities ?? defaultCapabilitiesForTask(input.taskType);
|
|
113
|
+
const ordered = orderedProviders(input);
|
|
114
|
+
const activeSlotsByProvider = input.runtime?.activeSlotsByProvider ?? {};
|
|
115
|
+
const quotaAvailableByProvider = input.runtime?.quotaAvailableByProvider ?? {};
|
|
116
|
+
const globalSlotLimit = input.routerPolicy?.defaultWorkerPool?.maxActiveSlots;
|
|
117
|
+
const candidates = [];
|
|
118
|
+
if (roleProviders.length === 0 && input.rolePolicy) {
|
|
119
|
+
return {
|
|
120
|
+
selectedProvider: undefined,
|
|
121
|
+
selectedWorker: { role: input.role, taskType: input.taskType },
|
|
122
|
+
mode: "delegated",
|
|
123
|
+
selectionReason: compatibilityMode ? "delegated-no-provider" : "router-role-disabled",
|
|
124
|
+
exhaustedReason: "role-disabled",
|
|
125
|
+
compatibilityMode,
|
|
126
|
+
providersTried: [],
|
|
127
|
+
candidates: [],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
for (const [orderIndex, provider] of ordered.entries()) {
|
|
131
|
+
const policy = providerRegistry[provider];
|
|
132
|
+
const rejectionReasons = [];
|
|
133
|
+
const eligibleRoleSet = new Set(policy?.eligibleRoles ?? []);
|
|
134
|
+
const policyMatched = !policy || eligibleRoleSet.size === 0 || eligibleRoleSet.has(input.role);
|
|
135
|
+
if (!policyMatched) {
|
|
136
|
+
rejectionReasons.push("role-disabled");
|
|
137
|
+
}
|
|
138
|
+
if (roleProviders.length > 0 && !roleProviders.includes(provider)) {
|
|
139
|
+
rejectionReasons.push("not-in-policy");
|
|
140
|
+
}
|
|
141
|
+
const capabilitySet = new Set(policy?.capabilities ?? []);
|
|
142
|
+
const allowedTaskSet = new Set(policy?.taskTypes ?? []);
|
|
143
|
+
if ((capabilitySet.size > 0 && requiredCapabilities.some((capability) => !capabilitySet.has(capability))) ||
|
|
144
|
+
(allowedTaskSet.size > 0 && !allowedTaskSet.has(input.taskType))) {
|
|
145
|
+
rejectionReasons.push("capability-mismatch");
|
|
146
|
+
}
|
|
147
|
+
const providerTrust = policy?.trustTier;
|
|
148
|
+
const minTrust = input.constraints?.minTrustTier;
|
|
149
|
+
if (providerTrust && minTrust && TRUST_RANK[providerTrust] < TRUST_RANK[minTrust]) {
|
|
150
|
+
rejectionReasons.push("trust-too-low");
|
|
151
|
+
}
|
|
152
|
+
const providerCost = policy?.costTier;
|
|
153
|
+
const maxCost = input.constraints?.maxCostTier;
|
|
154
|
+
if (providerCost && maxCost && COST_RANK[providerCost] > COST_RANK[maxCost]) {
|
|
155
|
+
rejectionReasons.push("cost-policy");
|
|
156
|
+
}
|
|
157
|
+
const disallowedQuota = new Set(input.constraints?.disallowedQuotaPolicies ?? []);
|
|
158
|
+
if (policy?.quotaPolicy && disallowedQuota.has(policy.quotaPolicy)) {
|
|
159
|
+
rejectionReasons.push("cost-policy");
|
|
160
|
+
}
|
|
161
|
+
if (policy?.quotaPolicy && quotaAvailableByProvider[provider] === false) {
|
|
162
|
+
rejectionReasons.push("quota-exhausted");
|
|
163
|
+
}
|
|
164
|
+
const activeSlots = activeSlotsByProvider[provider] ?? 0;
|
|
165
|
+
const providerSlotLimit = policy?.maxActiveSlots;
|
|
166
|
+
// Provider-specific maxActiveSlots overrides the global default when present.
|
|
167
|
+
const effectiveSlotLimit = providerSlotLimit ?? globalSlotLimit;
|
|
168
|
+
if (effectiveSlotLimit !== undefined && activeSlots >= effectiveSlotLimit) {
|
|
169
|
+
rejectionReasons.push("no-slot");
|
|
170
|
+
}
|
|
171
|
+
const score = {
|
|
172
|
+
orderScore: Math.max(0, 100 - orderIndex),
|
|
173
|
+
trustScore: providerTrust ? TRUST_RANK[providerTrust] * 1_000 : 0,
|
|
174
|
+
costScore: providerCost ? (3 - COST_RANK[providerCost]) * 10 : 0,
|
|
175
|
+
total: 0,
|
|
176
|
+
};
|
|
177
|
+
score.total = score.orderScore + score.trustScore + score.costScore;
|
|
178
|
+
candidates.push({
|
|
179
|
+
provider,
|
|
180
|
+
eligible: rejectionReasons.length === 0,
|
|
181
|
+
score,
|
|
182
|
+
rejectionReasons,
|
|
183
|
+
evidence: {
|
|
184
|
+
orderIndex,
|
|
185
|
+
trustTier: providerTrust,
|
|
186
|
+
costTier: providerCost,
|
|
187
|
+
quotaPolicy: policy?.quotaPolicy,
|
|
188
|
+
activeSlots,
|
|
189
|
+
slotLimit: providerSlotLimit ?? globalSlotLimit,
|
|
190
|
+
policyMatched,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const eligibleCandidates = candidates.filter((candidate) => candidate.eligible);
|
|
195
|
+
const selected = eligibleCandidates
|
|
196
|
+
.slice()
|
|
197
|
+
.sort((a, b) => {
|
|
198
|
+
if (a.score.total !== b.score.total)
|
|
199
|
+
return b.score.total - a.score.total;
|
|
200
|
+
if (a.evidence.orderIndex !== b.evidence.orderIndex)
|
|
201
|
+
return a.evidence.orderIndex - b.evidence.orderIndex;
|
|
202
|
+
return a.provider.localeCompare(b.provider);
|
|
203
|
+
})[0];
|
|
204
|
+
const selectedProvider = selected?.provider;
|
|
205
|
+
if (!selectedProvider) {
|
|
206
|
+
const rejection = topRejectionReason(candidates);
|
|
207
|
+
const exhaustedReason = rejection ?? (compatibilityMode ? compatibilityExhaustedReason(input) : "no-eligible-provider");
|
|
208
|
+
const selectionReason = compatibilityMode ? "delegated-no-provider" : "router-no-eligible-provider";
|
|
209
|
+
return {
|
|
210
|
+
selectedProvider: undefined,
|
|
211
|
+
selectedWorker: { role: input.role, taskType: input.taskType },
|
|
212
|
+
mode: "delegated",
|
|
213
|
+
selectionReason,
|
|
214
|
+
exhaustedReason,
|
|
215
|
+
compatibilityMode,
|
|
216
|
+
providersTried: ordered,
|
|
217
|
+
candidates,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
selectedProvider,
|
|
222
|
+
selectedWorker: { role: input.role, taskType: input.taskType },
|
|
223
|
+
mode: "direct-worker",
|
|
224
|
+
selectionReason: compatibilityMode ? compatibilitySelectionReason(input, selectedProvider) : "policy-router",
|
|
225
|
+
compatibilityMode,
|
|
226
|
+
providersTried: ordered,
|
|
227
|
+
candidates,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decideWorkerRoute = void 0;
|
|
4
|
+
var engine_js_1 = require("./engine.js");
|
|
5
|
+
Object.defineProperty(exports, "decideWorkerRoute", { enumerable: true, get: function () { return engine_js_1.decideWorkerRoute; } });
|