@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
|
@@ -46,6 +46,71 @@ function resolveCommand(cmd) {
|
|
|
46
46
|
return false;
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
+
function isQuotaExhaustedSignal(...parts) {
|
|
50
|
+
const text = parts.filter((value) => typeof value === "string" && value.length > 0).join("\n").toLowerCase();
|
|
51
|
+
if (!text)
|
|
52
|
+
return false;
|
|
53
|
+
return (text.includes("quota") ||
|
|
54
|
+
text.includes("rate limit") ||
|
|
55
|
+
text.includes("429") ||
|
|
56
|
+
text.includes("resource exhausted") ||
|
|
57
|
+
text.includes("insufficient_quota"));
|
|
58
|
+
}
|
|
59
|
+
function hasWorkerExecutionEvidence(packet, resultFilePath) {
|
|
60
|
+
if (resultFilePath && node_fs_1.default.existsSync(resultFilePath))
|
|
61
|
+
return true;
|
|
62
|
+
if (!packet.telemetry_file || !packet.active_child || !node_fs_1.default.existsSync(packet.telemetry_file))
|
|
63
|
+
return false;
|
|
64
|
+
try {
|
|
65
|
+
const telemetry = node_fs_1.default.readFileSync(packet.telemetry_file, "utf-8").trim();
|
|
66
|
+
if (!telemetry)
|
|
67
|
+
return false;
|
|
68
|
+
const lines = telemetry.split("\n");
|
|
69
|
+
const packetDispatchId = packet.dispatch_id;
|
|
70
|
+
// Scan backwards: when both the packet and the parsed event carry a dispatch_id,
|
|
71
|
+
// use that as the primary scope gate so stale events from a previous attempt are
|
|
72
|
+
// never attributed to the current dispatch. Fall back to the child-dispatched
|
|
73
|
+
// boundary for older telemetry that lacks dispatch_id on individual events.
|
|
74
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
75
|
+
const line = lines[i]?.trim();
|
|
76
|
+
if (!line)
|
|
77
|
+
continue;
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(line);
|
|
80
|
+
if (parsed.child_id !== packet.active_child)
|
|
81
|
+
continue;
|
|
82
|
+
if (parsed.event === "worker-acknowledged" ||
|
|
83
|
+
parsed.event === "worker-heartbeat" ||
|
|
84
|
+
parsed.event === "worker-result") {
|
|
85
|
+
const eventDispatchId = parsed.dispatch_id;
|
|
86
|
+
if (packetDispatchId && eventDispatchId) {
|
|
87
|
+
// Primary gate: dispatch_id available on both sides — match determines
|
|
88
|
+
// whether this event belongs to the current dispatch.
|
|
89
|
+
if (eventDispatchId === packetDispatchId)
|
|
90
|
+
return true;
|
|
91
|
+
// Mismatch — event belongs to a different dispatch; keep scanning.
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// Fallback: dispatch_id not available on one or both sides — accept the
|
|
95
|
+
// event as evidence for the current dispatch (scoped by the
|
|
96
|
+
// child-dispatched boundary below).
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
// Boundary fallback: stop at the child-dispatched event so events from a
|
|
100
|
+
// prior attempt are not counted when dispatch_id matching is unavailable.
|
|
101
|
+
if (parsed.event === "child-dispatched")
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
49
114
|
class TerminalCliAdapter {
|
|
50
115
|
config;
|
|
51
116
|
name = 'terminal-cli';
|
|
@@ -102,6 +167,8 @@ class TerminalCliAdapter {
|
|
|
102
167
|
}
|
|
103
168
|
async dispatch(packet, options) {
|
|
104
169
|
const primaryProvider = options.provider || "terminal-cli";
|
|
170
|
+
const routerEvidence = options.routerDecision;
|
|
171
|
+
const providerAttempts = [];
|
|
105
172
|
if ((0, worker_packet_js_1.isWorkerPacket)(packet) && packet.worker_role === "impl") {
|
|
106
173
|
const allowed = Array.isArray(packet.instructions?.allowed_scope) ? packet.instructions.allowed_scope : [];
|
|
107
174
|
if (allowed.length === 0) {
|
|
@@ -119,6 +186,20 @@ class TerminalCliAdapter {
|
|
|
119
186
|
}),
|
|
120
187
|
stderr: blockedMsg,
|
|
121
188
|
pre_dispatch_failure: true,
|
|
189
|
+
failure_origin: "provider-launch",
|
|
190
|
+
failure_category: "launch-error",
|
|
191
|
+
fallback_eligible: false,
|
|
192
|
+
router_evidence: routerEvidence,
|
|
193
|
+
provider_attempts: [
|
|
194
|
+
{
|
|
195
|
+
provider: primaryProvider,
|
|
196
|
+
failure_origin: "provider-launch",
|
|
197
|
+
failure_category: "launch-error",
|
|
198
|
+
pre_dispatch_failure: true,
|
|
199
|
+
fallback_eligible: false,
|
|
200
|
+
message: blockedMsg,
|
|
201
|
+
},
|
|
202
|
+
],
|
|
122
203
|
};
|
|
123
204
|
}
|
|
124
205
|
}
|
|
@@ -132,11 +213,15 @@ class TerminalCliAdapter {
|
|
|
132
213
|
const policyProviders = (canFallback && Array.isArray(workerPolicy?.providers))
|
|
133
214
|
? workerPolicy.providers
|
|
134
215
|
: [];
|
|
216
|
+
const routerProviders = canFallback && Array.isArray(routerEvidence?.providersTried)
|
|
217
|
+
? routerEvidence.providersTried
|
|
218
|
+
: [];
|
|
219
|
+
const fallbackOrder = routerProviders.length > 0 ? routerProviders : policyProviders;
|
|
135
220
|
const providersToTry = [
|
|
136
221
|
{ name: primaryProvider, cfg: primaryCfg, isPrimary: true },
|
|
137
222
|
];
|
|
138
223
|
if (canFallback) {
|
|
139
|
-
for (const p of
|
|
224
|
+
for (const p of fallbackOrder) {
|
|
140
225
|
if (p !== primaryProvider && p in (this.config.providers ?? {})) {
|
|
141
226
|
try {
|
|
142
227
|
providersToTry.push({ name: p, cfg: this.getProvider(p), isPrimary: false });
|
|
@@ -167,12 +252,30 @@ class TerminalCliAdapter {
|
|
|
167
252
|
provider_used: provider,
|
|
168
253
|
command_run: provider,
|
|
169
254
|
stderr: err instanceof Error ? err.message : String(err),
|
|
255
|
+
pre_dispatch_failure: true,
|
|
256
|
+
failure_origin: "provider-launch",
|
|
257
|
+
failure_category: "launch-error",
|
|
258
|
+
fallback_eligible: true,
|
|
259
|
+
router_evidence: routerEvidence,
|
|
170
260
|
};
|
|
261
|
+
providerAttempts.push({
|
|
262
|
+
provider,
|
|
263
|
+
failure_origin: "provider-launch",
|
|
264
|
+
failure_category: "launch-error",
|
|
265
|
+
pre_dispatch_failure: true,
|
|
266
|
+
fallback_eligible: true,
|
|
267
|
+
message: lastResult.stderr,
|
|
268
|
+
});
|
|
171
269
|
continue;
|
|
172
270
|
}
|
|
173
271
|
const commandLine = formatCommandLine(command, args);
|
|
174
272
|
if (options.dryRun) {
|
|
175
|
-
|
|
273
|
+
const dryRun = this.dryRun(provider, command, args, commandLine, packet, packetFile, workerPrompt);
|
|
274
|
+
return {
|
|
275
|
+
...dryRun,
|
|
276
|
+
router_evidence: routerEvidence,
|
|
277
|
+
provider_attempts: providerAttempts,
|
|
278
|
+
};
|
|
176
279
|
}
|
|
177
280
|
if (!resolveCommand(command)) {
|
|
178
281
|
lastResult = {
|
|
@@ -181,28 +284,108 @@ class TerminalCliAdapter {
|
|
|
181
284
|
command_run: commandLine,
|
|
182
285
|
stderr: `Provider command "${command}" not found on PATH. ` +
|
|
183
286
|
`Install it or update the "command" field for provider "${provider}" in polaris.config.json.`,
|
|
287
|
+
pre_dispatch_failure: true,
|
|
288
|
+
failure_origin: "provider-launch",
|
|
289
|
+
failure_category: "provider-unavailable",
|
|
290
|
+
fallback_eligible: true,
|
|
291
|
+
router_evidence: routerEvidence,
|
|
292
|
+
};
|
|
293
|
+
providerAttempts.push({
|
|
294
|
+
provider,
|
|
295
|
+
failure_origin: "provider-launch",
|
|
296
|
+
failure_category: "provider-unavailable",
|
|
297
|
+
pre_dispatch_failure: true,
|
|
298
|
+
fallback_eligible: true,
|
|
299
|
+
message: lastResult.stderr,
|
|
300
|
+
});
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
let result;
|
|
304
|
+
try {
|
|
305
|
+
result = await this.runProcess(command, args, commandLine, packet, packetFile, provider, workerPrompt);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
309
|
+
lastResult = {
|
|
310
|
+
exit_code: 1,
|
|
311
|
+
provider_used: provider,
|
|
312
|
+
command_run: commandLine,
|
|
313
|
+
stderr: message,
|
|
314
|
+
pre_dispatch_failure: true,
|
|
315
|
+
failure_origin: "provider-launch",
|
|
316
|
+
failure_category: "provider-unavailable",
|
|
317
|
+
fallback_eligible: true,
|
|
318
|
+
router_evidence: routerEvidence,
|
|
184
319
|
};
|
|
320
|
+
providerAttempts.push({
|
|
321
|
+
provider,
|
|
322
|
+
failure_origin: "provider-launch",
|
|
323
|
+
failure_category: "provider-unavailable",
|
|
324
|
+
pre_dispatch_failure: true,
|
|
325
|
+
fallback_eligible: true,
|
|
326
|
+
message,
|
|
327
|
+
});
|
|
185
328
|
continue;
|
|
186
329
|
}
|
|
187
|
-
const result = await this.runProcess(command, args, commandLine, packet, packetFile, provider, workerPrompt);
|
|
188
330
|
// Worker succeeded — return immediately.
|
|
189
331
|
if (result.exit_code === 0) {
|
|
190
|
-
return
|
|
332
|
+
return {
|
|
333
|
+
...result,
|
|
334
|
+
router_evidence: routerEvidence,
|
|
335
|
+
provider_attempts: providerAttempts,
|
|
336
|
+
};
|
|
191
337
|
}
|
|
192
|
-
// If the worker wrote a result file it actually ran (and failed) — don't
|
|
193
|
-
// retry with another provider; that would double-dispatch the same child.
|
|
194
338
|
const resultFilePath = (0, worker_packet_js_1.isWorkerPacket)(packet) ? packet.result_file_contract?.result_file : undefined;
|
|
195
|
-
|
|
196
|
-
|
|
339
|
+
const workerStarted = hasWorkerExecutionEvidence(packet, resultFilePath);
|
|
340
|
+
if (workerStarted) {
|
|
341
|
+
return {
|
|
342
|
+
...result,
|
|
343
|
+
failure_origin: "worker-execution",
|
|
344
|
+
failure_category: "worker-failure",
|
|
345
|
+
fallback_eligible: false,
|
|
346
|
+
router_evidence: routerEvidence,
|
|
347
|
+
provider_attempts: providerAttempts,
|
|
348
|
+
};
|
|
197
349
|
}
|
|
198
|
-
|
|
199
|
-
|
|
350
|
+
const category = isQuotaExhaustedSignal(result.stderr, result.stdout, result.summary)
|
|
351
|
+
? "quota-exhausted"
|
|
352
|
+
: "provider-unavailable";
|
|
353
|
+
const classified = {
|
|
354
|
+
...result,
|
|
355
|
+
pre_dispatch_failure: true,
|
|
356
|
+
failure_origin: "provider-launch",
|
|
357
|
+
failure_category: category,
|
|
358
|
+
fallback_eligible: true,
|
|
359
|
+
router_evidence: routerEvidence,
|
|
360
|
+
};
|
|
361
|
+
providerAttempts.push({
|
|
362
|
+
provider,
|
|
363
|
+
failure_origin: "provider-launch",
|
|
364
|
+
failure_category: category,
|
|
365
|
+
pre_dispatch_failure: true,
|
|
366
|
+
fallback_eligible: true,
|
|
367
|
+
message: result.stderr ?? result.summary,
|
|
368
|
+
});
|
|
369
|
+
lastResult = classified;
|
|
200
370
|
}
|
|
201
|
-
|
|
371
|
+
const exhausted = lastResult ?? {
|
|
202
372
|
exit_code: 1,
|
|
203
373
|
provider_used: primaryProvider,
|
|
204
374
|
command_run: '',
|
|
205
375
|
stderr: 'All configured providers exhausted without starting a worker',
|
|
376
|
+
pre_dispatch_failure: true,
|
|
377
|
+
failure_origin: "provider-launch",
|
|
378
|
+
failure_category: "provider-unavailable",
|
|
379
|
+
fallback_eligible: false,
|
|
380
|
+
router_evidence: routerEvidence,
|
|
381
|
+
provider_attempts: providerAttempts,
|
|
382
|
+
};
|
|
383
|
+
return {
|
|
384
|
+
...exhausted,
|
|
385
|
+
// All providers have been exhausted — no further fallback is possible.
|
|
386
|
+
fallback_eligible: false,
|
|
387
|
+
router_evidence: exhausted.router_evidence ?? routerEvidence,
|
|
388
|
+
provider_attempts: exhausted.provider_attempts ?? providerAttempts,
|
|
206
389
|
};
|
|
207
390
|
}
|
|
208
391
|
finally {
|
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) {
|
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);
|