@lsctech/polaris 0.4.9 → 0.5.2
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/gates.js +47 -0
- package/dist/autoresearch/proposal.js +183 -1
- package/dist/autoresearch/score.js +276 -0
- package/dist/cli/init.js +17 -0
- package/dist/cluster-state/store.js +70 -1
- package/dist/config/defaults.js +25 -0
- package/dist/config/validator.js +390 -0
- package/dist/finalize/artifact-policy.js +3 -1
- package/dist/finalize/index.js +73 -17
- package/dist/finalize/linear.js +45 -0
- package/dist/finalize/run-report.js +68 -12
- package/dist/loop/adapters/agent-subtask.js +19 -0
- package/dist/loop/adapters/cli-subtask-bridge.js +0 -1
- package/dist/loop/adapters/terminal-cli.js +194 -14
- package/dist/loop/checkpoint.js +40 -0
- package/dist/loop/dispatch-boundary.js +29 -0
- package/dist/loop/dispatch.js +193 -22
- package/dist/loop/orphan-recovery.js +56 -0
- package/dist/loop/parent.js +197 -22
- 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/qc/artifacts.js +117 -0
- package/dist/qc/attribution.js +266 -0
- package/dist/qc/autofix.js +77 -0
- package/dist/qc/index.js +30 -0
- package/dist/qc/orchestration.js +150 -0
- package/dist/qc/policy.js +70 -0
- package/dist/qc/provider.js +28 -0
- package/dist/qc/providers/coderabbit.js +214 -0
- package/dist/qc/providers/index.js +5 -0
- package/dist/qc/registry.js +16 -0
- package/dist/qc/routing.js +55 -0
- package/dist/qc/runner.js +137 -0
- package/dist/qc/schemas.js +110 -0
- package/dist/qc/security-category.js +11 -0
- package/dist/qc/severity.js +78 -0
- package/dist/qc/triggers.js +92 -0
- package/dist/qc/types.js +9 -0
- package/dist/runtime/scheduling/child-selector.js +81 -0
- package/package.json +1 -1
package/dist/loop/checkpoint.js
CHANGED
|
@@ -269,6 +269,46 @@ function validateState(state) {
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
|
+
if ("worker_pool_state" in s && s["worker_pool_state"] !== undefined) {
|
|
273
|
+
if (typeof s["worker_pool_state"] !== "object" || s["worker_pool_state"] === null || Array.isArray(s["worker_pool_state"])) {
|
|
274
|
+
errors.push("worker_pool_state must be an object");
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
const pool = s["worker_pool_state"];
|
|
278
|
+
if (typeof pool["max_concurrent"] !== "number" ||
|
|
279
|
+
!Number.isInteger(pool["max_concurrent"]) ||
|
|
280
|
+
pool["max_concurrent"] < 1) {
|
|
281
|
+
errors.push("worker_pool_state.max_concurrent must be a positive integer");
|
|
282
|
+
}
|
|
283
|
+
if (!Array.isArray(pool["slot_claims"])) {
|
|
284
|
+
errors.push("worker_pool_state.slot_claims must be an array");
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
for (const [index, claim] of pool["slot_claims"].entries()) {
|
|
288
|
+
if (typeof claim !== "object" || claim === null || Array.isArray(claim)) {
|
|
289
|
+
errors.push(`worker_pool_state.slot_claims[${index}] must be an object`);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const typed = claim;
|
|
293
|
+
if (typeof typed["child_id"] !== "string" || typed["child_id"].trim().length === 0) {
|
|
294
|
+
errors.push(`worker_pool_state.slot_claims[${index}].child_id must be a non-empty string`);
|
|
295
|
+
}
|
|
296
|
+
if (typed["provider"] !== null && typeof typed["provider"] !== "string") {
|
|
297
|
+
errors.push(`worker_pool_state.slot_claims[${index}].provider must be a string or null`);
|
|
298
|
+
}
|
|
299
|
+
if (typeof typed["claimed_at"] !== "string") {
|
|
300
|
+
errors.push(`worker_pool_state.slot_claims[${index}].claimed_at must be a string`);
|
|
301
|
+
}
|
|
302
|
+
if (typeof typed["expires_at"] !== "string") {
|
|
303
|
+
errors.push(`worker_pool_state.slot_claims[${index}].expires_at must be a string`);
|
|
304
|
+
}
|
|
305
|
+
if (typeof typed["selection_reason"] !== "string") {
|
|
306
|
+
errors.push(`worker_pool_state.slot_claims[${index}].selection_reason must be a string`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
272
312
|
return errors;
|
|
273
313
|
}
|
|
274
314
|
function writeStateAtomic(stateFile, state) {
|
|
@@ -40,8 +40,10 @@ exports.validateTransition = validateTransition;
|
|
|
40
40
|
exports.initialDispatchBoundary = initialDispatchBoundary;
|
|
41
41
|
exports.advanceDispatchEpoch = advanceDispatchEpoch;
|
|
42
42
|
exports.advanceContinueEpoch = advanceContinueEpoch;
|
|
43
|
+
exports.assertChildQcSelectionAllowed = assertChildQcSelectionAllowed;
|
|
43
44
|
const node_fs_1 = require("node:fs");
|
|
44
45
|
const node_path_1 = require("node:path");
|
|
46
|
+
const triggers_js_1 = require("../qc/triggers.js");
|
|
45
47
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
46
48
|
// Error constants
|
|
47
49
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -333,3 +335,30 @@ function advanceContinueEpoch(current) {
|
|
|
333
335
|
last_dispatched_child: current.last_dispatched_child,
|
|
334
336
|
};
|
|
335
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Validate that a child-level QC trigger is only selected when permitted.
|
|
340
|
+
*
|
|
341
|
+
* Child-level QC is opt-in: high-risk scope, route policy, or explicit operator
|
|
342
|
+
* request. If a packet carries child-level QC without meeting one of those
|
|
343
|
+
* conditions, the dispatch boundary is violated.
|
|
344
|
+
*/
|
|
345
|
+
function assertChildQcSelectionAllowed(state, childId, config, allowedScope, labels, telemetryFile) {
|
|
346
|
+
const meta = state.open_children_meta?.[childId];
|
|
347
|
+
if (meta?.qc_trigger !== "child")
|
|
348
|
+
return;
|
|
349
|
+
const routeName = childId; // route-specific overrides are keyed by child/route identifier
|
|
350
|
+
if ((0, triggers_js_1.isChildQcSelected)(config, childId, allowedScope, labels, routeName)) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const reason = `Child-level QC selected for ${childId} but no opt-in condition is met (high-risk scope, route policy, or "qc-child" label)`;
|
|
354
|
+
appendDispatchViolationEvent(telemetryFile, {
|
|
355
|
+
event: "illegal-state-transition",
|
|
356
|
+
run_id: state.run_id,
|
|
357
|
+
child_id: childId,
|
|
358
|
+
from_state: getMachineState(state),
|
|
359
|
+
to_state: "dispatched",
|
|
360
|
+
reason,
|
|
361
|
+
timestamp: new Date().toISOString(),
|
|
362
|
+
});
|
|
363
|
+
throw new Error(reason);
|
|
364
|
+
}
|
package/dist/loop/dispatch.js
CHANGED
|
@@ -15,6 +15,7 @@ const body_parser_js_1 = require("./body-parser.js");
|
|
|
15
15
|
const loader_js_1 = require("../config/loader.js");
|
|
16
16
|
const store_js_1 = require("../cluster-state/store.js");
|
|
17
17
|
const worker_prompt_js_1 = require("./worker-prompt.js");
|
|
18
|
+
const index_js_1 = require("./router/index.js");
|
|
18
19
|
function resolveSimplicityMode(state, config) {
|
|
19
20
|
if (state.simplicity_bypass)
|
|
20
21
|
return "off";
|
|
@@ -23,7 +24,7 @@ function resolveSimplicityMode(state, config) {
|
|
|
23
24
|
const dispatch_boundary_js_1 = require("./dispatch-boundary.js");
|
|
24
25
|
const run_bootstrap_js_1 = require("./run-bootstrap.js");
|
|
25
26
|
const ledger_js_1 = require("./ledger.js");
|
|
26
|
-
const
|
|
27
|
+
const index_js_2 = require("../tracker/index.js");
|
|
27
28
|
const lifecycle_transition_js_1 = require("../tracker/lifecycle-transition.js");
|
|
28
29
|
const CLAIM_TTL_MS = 30 * 60 * 1000;
|
|
29
30
|
/**
|
|
@@ -46,18 +47,51 @@ function roleContextToExecutionRole(role) {
|
|
|
46
47
|
return "worker";
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
50
|
+
function roleContextToTaskType(roleContext) {
|
|
51
|
+
switch (roleContext?.role) {
|
|
52
|
+
case "analyst":
|
|
53
|
+
return "analyze";
|
|
54
|
+
case "librarian":
|
|
55
|
+
return "docs";
|
|
56
|
+
case "foreman":
|
|
57
|
+
return "startup";
|
|
58
|
+
case "worker":
|
|
59
|
+
default:
|
|
60
|
+
return "impl";
|
|
60
61
|
}
|
|
62
|
+
}
|
|
63
|
+
function requiredCapabilitiesForTask(taskType) {
|
|
64
|
+
switch (taskType) {
|
|
65
|
+
case "startup":
|
|
66
|
+
return ["orchestration"];
|
|
67
|
+
case "analyze":
|
|
68
|
+
return ["analysis"];
|
|
69
|
+
case "impl":
|
|
70
|
+
return ["implementation"];
|
|
71
|
+
case "repair":
|
|
72
|
+
return ["repair"];
|
|
73
|
+
case "docs":
|
|
74
|
+
return ["docs"];
|
|
75
|
+
case "finalize":
|
|
76
|
+
return ["finalization"];
|
|
77
|
+
default:
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function collectActiveProviderSlots(state) {
|
|
82
|
+
const counts = {};
|
|
83
|
+
for (const meta of Object.values(state.open_children_meta ?? {})) {
|
|
84
|
+
const dispatch = meta?.dispatch_record;
|
|
85
|
+
if (!dispatch?.provider)
|
|
86
|
+
continue;
|
|
87
|
+
if (dispatch.status !== "dispatched")
|
|
88
|
+
continue;
|
|
89
|
+
counts[dispatch.provider] = (counts[dispatch.provider] ?? 0) + 1;
|
|
90
|
+
}
|
|
91
|
+
return counts;
|
|
92
|
+
}
|
|
93
|
+
function resolveProviderAndMode(options, role, config, routerContext) {
|
|
94
|
+
const defaultAdapter = "terminal-cli";
|
|
61
95
|
// Only catch config-load failures; policy violations must propagate to the caller.
|
|
62
96
|
let loaded;
|
|
63
97
|
let adapter = defaultAdapter;
|
|
@@ -78,6 +112,64 @@ function resolveProviderAndMode(options, role, config) {
|
|
|
78
112
|
};
|
|
79
113
|
}
|
|
80
114
|
const exec = loaded.execution;
|
|
115
|
+
const providerRegistry = exec?.routerPolicy?.providerRegistry ?? {};
|
|
116
|
+
const compatibilityMode = Object.keys(providerRegistry).length === 0;
|
|
117
|
+
if (compatibilityMode) {
|
|
118
|
+
return resolveProviderAndModeLegacy(options, role, adapter, exec);
|
|
119
|
+
}
|
|
120
|
+
const decision = (0, index_js_1.decideWorkerRoute)({
|
|
121
|
+
role,
|
|
122
|
+
taskType: routerContext?.taskType ?? "impl",
|
|
123
|
+
adapter,
|
|
124
|
+
providerOverride: options.provider,
|
|
125
|
+
providers: Object.keys(exec?.providers ?? {}),
|
|
126
|
+
rotation: exec?.rotation ?? [],
|
|
127
|
+
rolePolicy: exec?.providerPolicy?.[role],
|
|
128
|
+
roleConfiguredProvider: exec?.roles?.[role]?.provider,
|
|
129
|
+
routerPolicy: exec?.routerPolicy,
|
|
130
|
+
constraints: {
|
|
131
|
+
minTrustTier: routerContext?.minTrustTier,
|
|
132
|
+
maxCostTier: routerContext?.maxCostTier,
|
|
133
|
+
disallowedQuotaPolicies: routerContext?.disallowedQuotaPolicies,
|
|
134
|
+
requiredCapabilities: routerContext?.requiredCapabilities,
|
|
135
|
+
},
|
|
136
|
+
runtime: {
|
|
137
|
+
activeSlotsByProvider: routerContext?.activeSlotsByProvider,
|
|
138
|
+
quotaAvailableByProvider: routerContext?.quotaAvailableByProvider,
|
|
139
|
+
},
|
|
140
|
+
compatibilityMode: false,
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
provider: decision.selectedProvider,
|
|
144
|
+
mode: decision.mode,
|
|
145
|
+
adapter,
|
|
146
|
+
selectionReason: decision.selectionReason,
|
|
147
|
+
overrideSource: options.provider ? "dispatch-flag" : undefined,
|
|
148
|
+
providersTried: decision.providersTried,
|
|
149
|
+
exhaustedReason: decision.exhaustedReason,
|
|
150
|
+
routerEvidence: decision,
|
|
151
|
+
routerContextSnapshot: {
|
|
152
|
+
taskType: routerContext?.taskType ?? "impl",
|
|
153
|
+
requiredCapabilities: routerContext?.requiredCapabilities,
|
|
154
|
+
minTrustTier: routerContext?.minTrustTier,
|
|
155
|
+
maxCostTier: routerContext?.maxCostTier,
|
|
156
|
+
disallowedQuotaPolicies: routerContext?.disallowedQuotaPolicies,
|
|
157
|
+
activeSlotsByProvider: routerContext?.activeSlotsByProvider,
|
|
158
|
+
quotaAvailableByProvider: routerContext?.quotaAvailableByProvider,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function resolveProviderAndModeLegacy(options, role, adapter, exec) {
|
|
163
|
+
if (options.provider) {
|
|
164
|
+
return {
|
|
165
|
+
provider: options.provider,
|
|
166
|
+
mode: "direct-worker",
|
|
167
|
+
adapter,
|
|
168
|
+
selectionReason: "cli-provider-override",
|
|
169
|
+
overrideSource: "dispatch-flag",
|
|
170
|
+
providersTried: [options.provider],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
81
173
|
const rolePolicy = exec?.providerPolicy?.[role];
|
|
82
174
|
const roleProviders = rolePolicy?.providers ?? [];
|
|
83
175
|
const rotation = exec?.rotation ?? [];
|
|
@@ -326,6 +418,15 @@ function determineRuntimeState(mode) {
|
|
|
326
418
|
return mode === "delegated" ? "delegated" : "packet-created";
|
|
327
419
|
}
|
|
328
420
|
function emitProviderSelected(telemetryFile, dispatchId, runId, childId, decision) {
|
|
421
|
+
const fallbackAttempts = decision.providersTried.map((provider, index) => {
|
|
422
|
+
const candidate = decision.routerEvidence?.candidates.find((entry) => entry.provider === provider);
|
|
423
|
+
return {
|
|
424
|
+
provider,
|
|
425
|
+
attempt_index: index + 1,
|
|
426
|
+
outcome: decision.provider === provider ? "selected" : "rejected",
|
|
427
|
+
rejection_reasons: candidate?.rejectionReasons ?? [],
|
|
428
|
+
};
|
|
429
|
+
});
|
|
329
430
|
appendTelemetry(telemetryFile, {
|
|
330
431
|
event: "provider-selected",
|
|
331
432
|
event_id: (0, node_crypto_1.randomUUID)(),
|
|
@@ -336,10 +437,55 @@ function emitProviderSelected(telemetryFile, dispatchId, runId, childId, decisio
|
|
|
336
437
|
selected_provider: decision.provider ?? null,
|
|
337
438
|
selected_adapter: decision.adapter,
|
|
338
439
|
selection_reason: decision.selectionReason,
|
|
440
|
+
...(decision.routerEvidence
|
|
441
|
+
? {
|
|
442
|
+
router_mode: decision.routerEvidence.mode,
|
|
443
|
+
router_task_type: decision.routerEvidence.selectedWorker.taskType,
|
|
444
|
+
router_compatibility_mode: decision.routerEvidence.compatibilityMode,
|
|
445
|
+
}
|
|
446
|
+
: {}),
|
|
447
|
+
...(decision.routerContextSnapshot
|
|
448
|
+
? {
|
|
449
|
+
router_score_inputs: {
|
|
450
|
+
required_capabilities: decision.routerContextSnapshot.requiredCapabilities,
|
|
451
|
+
min_trust_tier: decision.routerContextSnapshot.minTrustTier,
|
|
452
|
+
max_cost_tier: decision.routerContextSnapshot.maxCostTier,
|
|
453
|
+
disallowed_quota_policies: decision.routerContextSnapshot.disallowedQuotaPolicies,
|
|
454
|
+
active_slots_by_provider: decision.routerContextSnapshot.activeSlotsByProvider,
|
|
455
|
+
quota_available_by_provider: decision.routerContextSnapshot.quotaAvailableByProvider,
|
|
456
|
+
},
|
|
457
|
+
}
|
|
458
|
+
: {}),
|
|
459
|
+
...(fallbackAttempts.length > 0 ? { fallback_attempts: fallbackAttempts } : {}),
|
|
339
460
|
...(decision.overrideSource ? { override_source: decision.overrideSource } : {}),
|
|
340
461
|
...(decision.fallbackFrom ? { fallback_from: decision.fallbackFrom } : {}),
|
|
341
462
|
...(decision.fallbackReason ? { fallback_reason: decision.fallbackReason } : {}),
|
|
342
463
|
...(decision.providersTried.length > 0 ? { providers_tried: decision.providersTried } : {}),
|
|
464
|
+
...(decision.exhaustedReason ? { router_exhausted_reason: decision.exhaustedReason } : {}),
|
|
465
|
+
...(decision.routerEvidence?.candidates?.length
|
|
466
|
+
? {
|
|
467
|
+
router_candidates: decision.routerEvidence.candidates.map((candidate) => ({
|
|
468
|
+
provider: candidate.provider,
|
|
469
|
+
eligible: candidate.eligible,
|
|
470
|
+
rejection_reasons: candidate.rejectionReasons,
|
|
471
|
+
score: {
|
|
472
|
+
order: candidate.score.orderScore,
|
|
473
|
+
trust: candidate.score.trustScore,
|
|
474
|
+
cost: candidate.score.costScore,
|
|
475
|
+
total: candidate.score.total,
|
|
476
|
+
},
|
|
477
|
+
inputs: {
|
|
478
|
+
order_index: candidate.evidence.orderIndex,
|
|
479
|
+
trust_tier: candidate.evidence.trustTier,
|
|
480
|
+
cost_tier: candidate.evidence.costTier,
|
|
481
|
+
quota_policy: candidate.evidence.quotaPolicy,
|
|
482
|
+
active_slots: candidate.evidence.activeSlots,
|
|
483
|
+
slot_limit: candidate.evidence.slotLimit,
|
|
484
|
+
policy_matched: candidate.evidence.policyMatched,
|
|
485
|
+
},
|
|
486
|
+
})),
|
|
487
|
+
}
|
|
488
|
+
: {}),
|
|
343
489
|
timestamp: new Date().toISOString(),
|
|
344
490
|
});
|
|
345
491
|
}
|
|
@@ -355,6 +501,10 @@ function emitProviderFallbackAttempted(telemetryFile, dispatchId, runId, childId
|
|
|
355
501
|
requested_role: "worker",
|
|
356
502
|
fallback_from: decision.fallbackFrom,
|
|
357
503
|
fallback_reason: decision.fallbackReason,
|
|
504
|
+
...(decision.provider ? { fallback_to: decision.provider } : {}),
|
|
505
|
+
...(decision.providersTried.length > 0
|
|
506
|
+
? { fallback_attempt_index: decision.providersTried.length }
|
|
507
|
+
: {}),
|
|
358
508
|
...(decision.providersTried.length > 0 ? { providers_tried: decision.providersTried } : {}),
|
|
359
509
|
timestamp: new Date().toISOString(),
|
|
360
510
|
});
|
|
@@ -664,7 +814,7 @@ function selectChild(state, requestedChild) {
|
|
|
664
814
|
* @param resultFile - Optional path for the expected result file; non-absolute paths are resolved against `repoRoot`
|
|
665
815
|
* @returns The constructed WorkerPacket ready for dispatch
|
|
666
816
|
*/
|
|
667
|
-
function buildPacket(state, childId, stateFile, telemetryFile, repoRoot, resultFile, config) {
|
|
817
|
+
function buildPacket(state, childId, stateFile, telemetryFile, repoRoot, resultFile, maxConcurrentWorkers, config) {
|
|
668
818
|
const childMeta = state.open_children_meta?.[childId];
|
|
669
819
|
// Hydrate body from cluster snapshot when runtime state lacks it.
|
|
670
820
|
const cachedBody = childMeta?.body;
|
|
@@ -697,7 +847,7 @@ function buildPacket(state, childId, stateFile, telemetryFile, repoRoot, resultF
|
|
|
697
847
|
telemetryFile,
|
|
698
848
|
issueContext,
|
|
699
849
|
allowedScope: resolvedScope.length > 0 ? resolvedScope : undefined,
|
|
700
|
-
maxConcurrentWorkers
|
|
850
|
+
maxConcurrentWorkers,
|
|
701
851
|
promptMode: (0, worker_prompt_js_1.selectPromptMode)(childId, state),
|
|
702
852
|
simplicityMode: resolveSimplicityMode(state, config),
|
|
703
853
|
resultFile: canonicalPath(absoluteResultFile(repoRoot, resultFile)),
|
|
@@ -918,7 +1068,7 @@ function runLoopDispatch(options) {
|
|
|
918
1068
|
step_cursor: "dispatch",
|
|
919
1069
|
dispatch_boundary: (0, dispatch_boundary_js_1.advanceDispatchEpoch)(state.dispatch_boundary, childId),
|
|
920
1070
|
};
|
|
921
|
-
const packet = buildPacket(preliminaryState, childId, options.stateFile, telemetryFile, options.repoRoot, canonicalResultFile, loadedConfig);
|
|
1071
|
+
const packet = buildPacket(preliminaryState, childId, options.stateFile, telemetryFile, options.repoRoot, canonicalResultFile, loadedConfig?.execution?.routerPolicy?.defaultWorkerPool?.maxActiveWorkers ?? 1, loadedConfig);
|
|
922
1072
|
const packetFailure = detectPacketGenerationFailure(packet);
|
|
923
1073
|
if (packetFailure) {
|
|
924
1074
|
appendTelemetry(telemetryFile, {
|
|
@@ -932,22 +1082,38 @@ function runLoopDispatch(options) {
|
|
|
932
1082
|
fail(`${packetFailure.message} (missing_field=${packetFailure.missingField}, child_id=${childId})`);
|
|
933
1083
|
}
|
|
934
1084
|
const targetRole = roleContextToExecutionRole(packet.role_context.role);
|
|
1085
|
+
const taskType = roleContextToTaskType(packet.role_context);
|
|
1086
|
+
const activeSlotsByProvider = collectActiveProviderSlots(state);
|
|
935
1087
|
// ── Resolve provider and dispatch mode ─────────────────────────────────────
|
|
936
1088
|
// loadedConfig was loaded earlier (above bootstrap seal check)
|
|
937
1089
|
let providerDecision;
|
|
938
1090
|
try {
|
|
939
|
-
providerDecision = resolveProviderAndMode(options, targetRole, loadedConfig
|
|
1091
|
+
providerDecision = resolveProviderAndMode(options, targetRole, loadedConfig, {
|
|
1092
|
+
taskType,
|
|
1093
|
+
requiredCapabilities: requiredCapabilitiesForTask(taskType),
|
|
1094
|
+
activeSlotsByProvider,
|
|
1095
|
+
});
|
|
940
1096
|
}
|
|
941
1097
|
catch (err) {
|
|
942
1098
|
fail(err instanceof Error ? err.message : String(err));
|
|
943
1099
|
}
|
|
1100
|
+
// ── Handle hard-exhaustion: emit telemetry before failing ──────────────────
|
|
1101
|
+
// resolveProviderAndMode returns (not throws) when all providers are rejected so
|
|
1102
|
+
// emitProviderExhausted/emitProviderSelected can record the candidate breakdown.
|
|
1103
|
+
if (!providerDecision.provider && providerDecision.exhaustedReason && providerDecision.routerEvidence) {
|
|
1104
|
+
emitProviderExhausted(telemetryFile, dispatchId, state.run_id, childId, providerDecision);
|
|
1105
|
+
emitProviderSelected(telemetryFile, dispatchId, state.run_id, childId, providerDecision);
|
|
1106
|
+
fail(`provider dispatch forbidden: router rejected all providers (reason=${providerDecision.exhaustedReason})`);
|
|
1107
|
+
}
|
|
944
1108
|
const { provider: resolvedProvider } = providerDecision;
|
|
945
1109
|
const providerPolicy = loadedConfig?.execution.providerPolicy ?? getProviderPolicy(options.repoRoot);
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1110
|
+
if (!providerDecision.routerEvidence) {
|
|
1111
|
+
try {
|
|
1112
|
+
assertProviderAllowedForRole(targetRole, resolvedProvider, providerPolicy, telemetryFile, dispatchId, state.run_id, childId);
|
|
1113
|
+
}
|
|
1114
|
+
catch (err) {
|
|
1115
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
1116
|
+
}
|
|
951
1117
|
}
|
|
952
1118
|
// ── Provider probe on first dispatch ──────────────────────────────────────
|
|
953
1119
|
// Catches provider configuration failures before packet commit. Only runs on
|
|
@@ -962,6 +1128,11 @@ function runLoopDispatch(options) {
|
|
|
962
1128
|
run_id: state.run_id,
|
|
963
1129
|
child_id: childId,
|
|
964
1130
|
provider: resolvedProvider,
|
|
1131
|
+
failure_origin: "provider-launch",
|
|
1132
|
+
failure_category: "provider-unavailable",
|
|
1133
|
+
pre_dispatch_failure: true,
|
|
1134
|
+
fallback_eligible: true,
|
|
1135
|
+
providers_tried: providerDecision.providersTried,
|
|
965
1136
|
error: probeResult.error,
|
|
966
1137
|
timestamp: new Date().toISOString(),
|
|
967
1138
|
});
|
|
@@ -1080,7 +1251,7 @@ function runLoopDispatch(options) {
|
|
|
1080
1251
|
if (loadedConfig) {
|
|
1081
1252
|
let adapter;
|
|
1082
1253
|
try {
|
|
1083
|
-
adapter = (0,
|
|
1254
|
+
adapter = (0, index_js_2.loadTrackerAdapter)(loadedConfig);
|
|
1084
1255
|
}
|
|
1085
1256
|
catch (err) {
|
|
1086
1257
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -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 = {
|