@ran-sh/dsh-crew 1.3.2 → 1.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/.claude-plugin/plugin.json +1 -1
- package/lib/client.js +502 -12
- package/package.json +1 -1
- package/src/client/history-panel.tsx +4 -4
- package/src/client/index.tsx +159 -7
- package/src/history/cleanup-plan.mjs +30 -5
- package/src/history/runner-detach.mjs +34 -0
- package/src/history/runtime.mjs +9 -2
- package/src/history/service.mjs +13 -2
- package/src/hub/index.mjs +55 -0
- package/src/install/install-legacy.mjs +8 -0
- package/src/install/install.mjs +13 -5
- package/src/jobs.mjs +24 -2
- package/src/model-routing.mjs +250 -147
- package/src/model-schedule.mjs +207 -0
- package/src/policy.mjs +69 -62
- package/src/runtime-identity.mjs +1 -1
- package/src/server.mjs +6 -1
- package/src/session-origins.mjs +60 -0
package/src/jobs.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// never passes an explicit dshBin that would bypass it. The worker rides the
|
|
9
9
|
// `sdk-minimal` profile with the Crew overlay applied as a --patch.
|
|
10
10
|
|
|
11
|
-
import { createStandaloneHarness } from './standalone-sdk.mjs';
|
|
11
|
+
import { createStandaloneHarness } from './standalone-sdk.mjs';
|
|
12
12
|
import { readFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
13
13
|
import { createShardWriter } from './status-shard.mjs';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
@@ -19,6 +19,9 @@ import { captureWorkspaceBaseline, captureWorkspaceDiff, NOT_A_GIT_REPOSITORY }
|
|
|
19
19
|
import { buildOutcome, JOB_PHASES } from './workflow.mjs';
|
|
20
20
|
import { raceWaiters } from './removable-waiter.mjs';
|
|
21
21
|
import { buildDirectSelectionTrace } from './model-routing.mjs';
|
|
22
|
+
import { normalizeModelSchedule, scheduleAdmission } from './model-schedule.mjs';
|
|
23
|
+
import { appendSessionOrigin } from './session-origins.mjs';
|
|
24
|
+
import { readGlobalConfig } from './install/install.mjs';
|
|
22
25
|
|
|
23
26
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
24
27
|
const CONFIG_DIR = join(homedir(), '.config', 'dsh-crew');
|
|
@@ -26,6 +29,13 @@ const CREW_DSH_HOME = join(homedir(), '.config', 'dsh-crew', 'harness');
|
|
|
26
29
|
const STATUS_FILE = join(CONFIG_DIR, 'status.json');
|
|
27
30
|
const CORDIS = join(ROOT, 'worker.cordis.yml');
|
|
28
31
|
|
|
32
|
+
// Read the operator's schedule once per dispatch. An unreadable config must not
|
|
33
|
+
// block a job: a missing or damaged file means no restriction, matching how the
|
|
34
|
+
// router treats an absent schedule.
|
|
35
|
+
function readModelSchedule() {
|
|
36
|
+
try { return normalizeModelSchedule(readGlobalConfig().model_schedule); } catch { return normalizeModelSchedule(undefined); }
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
export const TIERS = {
|
|
30
40
|
flash: { model: 'deepseek-v4-flash', label: 'V4 Flash' },
|
|
31
41
|
pro: { model: 'deepseek-v4-pro', label: 'V4 Pro' },
|
|
@@ -109,6 +119,14 @@ export function startJob({
|
|
|
109
119
|
}) {
|
|
110
120
|
const tierInfo = TIERS[tier];
|
|
111
121
|
if (!tierInfo) throw new Error(`unknown tier "${tier}" (expected: ${Object.keys(TIERS).join(', ')})`);
|
|
122
|
+
// Standalone dispatch names its provider/model directly rather than walking a
|
|
123
|
+
// catalog, but a peak `block` still applies: without this the schedule would be
|
|
124
|
+
// silently ignored on every standalone job.
|
|
125
|
+
const standaloneRef = { provider: 'deepseek-official', model: tierInfo.model };
|
|
126
|
+
const standaloneAt = new Date();
|
|
127
|
+
if (scheduleAdmission(readModelSchedule(), standaloneRef, standaloneAt)?.mode === 'block') {
|
|
128
|
+
throw Object.assign(new Error('MODEL_BLOCKED_PEAK'), { policyCode: 'MODEL_BLOCKED_PEAK' });
|
|
129
|
+
}
|
|
112
130
|
if (!ROLES[role]) throw new Error(`unknown role "${role}" (expected: ${Object.keys(ROLES).join(', ')})`);
|
|
113
131
|
if (!['off', 'high', 'max'].includes(effort)) throw new Error(`unknown effort "${effort}" (expected: off, high, max)`);
|
|
114
132
|
if (!existsSync(CORDIS)) throw new Error(`worker overlay not found at ${CORDIS}`);
|
|
@@ -123,7 +141,7 @@ export function startJob({
|
|
|
123
141
|
throw new Error(`DEEPSEEK_API_KEY not found in env or ${join(CONFIG_DIR, '.env')}`);
|
|
124
142
|
}
|
|
125
143
|
|
|
126
|
-
const harness = createStandaloneHarness({
|
|
144
|
+
const harness = createStandaloneHarness({
|
|
127
145
|
profile: 'sdk-minimal',
|
|
128
146
|
patches: [CORDIS],
|
|
129
147
|
dshHome: CREW_DSH_HOME,
|
|
@@ -178,6 +196,10 @@ export function startJob({
|
|
|
178
196
|
.catch(() => ({ kind: 'no-git', reason: NOT_A_GIT_REPOSITORY, error: 'workspace audit failed' }));
|
|
179
197
|
|
|
180
198
|
const sessionId = id;
|
|
199
|
+
// Standalone dispatches write into the same shared session store as the hub,
|
|
200
|
+
// so they need the same provenance record or a Crew-scoped cleanup would leave
|
|
201
|
+
// them behind.
|
|
202
|
+
appendSessionOrigin({ sessionId, role, jobId: id });
|
|
181
203
|
const onNotification = (n) => {
|
|
182
204
|
if (n.method === 'session.status') return;
|
|
183
205
|
if (n.method !== 'session.event') return;
|
package/src/model-routing.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// routing boundary.
|
|
6
6
|
|
|
7
7
|
import { rankAdaptiveCandidates } from './adaptive-routing.mjs';
|
|
8
|
+
import { scheduleAdmission } from './model-schedule.mjs';
|
|
8
9
|
|
|
9
10
|
export const DEFAULT_TIER_MODEL_PREFERENCES = Object.freeze({
|
|
10
11
|
flash: 'deepseek-v4-flash',
|
|
@@ -26,36 +27,39 @@ export const MODEL_SELECTION_REASON_CODES = Object.freeze({
|
|
|
26
27
|
PROVIDER_UNAVAILABLE: 'PROVIDER_UNAVAILABLE',
|
|
27
28
|
PREFERRED_MODEL_UNAVAILABLE: 'PREFERRED_MODEL_UNAVAILABLE',
|
|
28
29
|
PREFERRED_MODEL_AMBIGUOUS: 'PREFERRED_MODEL_AMBIGUOUS',
|
|
29
|
-
ADAPTIVE_DEPRIORITIZED: 'ADAPTIVE_DEPRIORITIZED',
|
|
30
|
-
PROVIDER_TOMBSTONED: 'PROVIDER_TOMBSTONED',
|
|
31
|
-
CREDENTIAL_MISSING: 'CREDENTIAL_MISSING',
|
|
32
|
-
QUOTA_EXHAUSTED: 'QUOTA_EXHAUSTED',
|
|
33
|
-
RATE_LIMITED: 'RATE_LIMITED',
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
ADAPTIVE_DEPRIORITIZED: 'ADAPTIVE_DEPRIORITIZED',
|
|
31
|
+
PROVIDER_TOMBSTONED: 'PROVIDER_TOMBSTONED',
|
|
32
|
+
CREDENTIAL_MISSING: 'CREDENTIAL_MISSING',
|
|
33
|
+
QUOTA_EXHAUSTED: 'QUOTA_EXHAUSTED',
|
|
34
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
35
|
+
PEAK_RESTRICTED: 'PEAK_RESTRICTED',
|
|
36
|
+
PEAK_ADVISORY: 'PEAK_ADVISORY',
|
|
37
|
+
PROBE_TIMEOUT: 'PROBE_TIMEOUT',
|
|
38
|
+
PROVIDER_INTERNAL_ERROR: 'PROVIDER_INTERNAL_ERROR',
|
|
39
|
+
HARNESS_DEFAULT_INVALID: 'HARNESS_DEFAULT_INVALID',
|
|
37
40
|
HARNESS_DEFAULT_PROVIDER_UNAVAILABLE: 'HARNESS_DEFAULT_PROVIDER_UNAVAILABLE',
|
|
38
41
|
PRIMARY_CANDIDATES_EXHAUSTED: 'PRIMARY_CANDIDATES_EXHAUSTED',
|
|
39
42
|
ESCALATION_CANDIDATES_EXHAUSTED: 'ESCALATION_CANDIDATES_EXHAUSTED',
|
|
40
|
-
NO_AVAILABLE_MODEL: 'NO_AVAILABLE_MODEL',
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const HEALTH_BLOCK_REASONS = Object.freeze({
|
|
44
|
-
'credential-missing': MODEL_SELECTION_REASON_CODES.CREDENTIAL_MISSING,
|
|
45
|
-
'quota-exhausted': MODEL_SELECTION_REASON_CODES.QUOTA_EXHAUSTED,
|
|
46
|
-
'rate-limited': MODEL_SELECTION_REASON_CODES.RATE_LIMITED,
|
|
47
|
-
timeout: MODEL_SELECTION_REASON_CODES.PROBE_TIMEOUT,
|
|
48
|
-
'internal-error': MODEL_SELECTION_REASON_CODES.PROVIDER_INTERNAL_ERROR,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const BLOCKED_MODEL_CODES = Object.freeze({
|
|
52
|
-
[MODEL_SELECTION_REASON_CODES.CREDENTIAL_MISSING]: 'MODEL_BLOCKED_CREDENTIAL',
|
|
53
|
-
[MODEL_SELECTION_REASON_CODES.QUOTA_EXHAUSTED]: 'MODEL_BLOCKED_QUOTA',
|
|
54
|
-
[MODEL_SELECTION_REASON_CODES.RATE_LIMITED]: 'MODEL_BLOCKED_RATE_LIMIT',
|
|
55
|
-
[MODEL_SELECTION_REASON_CODES.PROBE_TIMEOUT]: 'MODEL_BLOCKED_TIMEOUT',
|
|
56
|
-
[MODEL_SELECTION_REASON_CODES.PROVIDER_INTERNAL_ERROR]: 'MODEL_BLOCKED_PROVIDER',
|
|
57
|
-
[MODEL_SELECTION_REASON_CODES.PROVIDER_TOMBSTONED]: 'MODEL_BLOCKED_TOMBSTONED',
|
|
58
|
-
|
|
43
|
+
NO_AVAILABLE_MODEL: 'NO_AVAILABLE_MODEL',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const HEALTH_BLOCK_REASONS = Object.freeze({
|
|
47
|
+
'credential-missing': MODEL_SELECTION_REASON_CODES.CREDENTIAL_MISSING,
|
|
48
|
+
'quota-exhausted': MODEL_SELECTION_REASON_CODES.QUOTA_EXHAUSTED,
|
|
49
|
+
'rate-limited': MODEL_SELECTION_REASON_CODES.RATE_LIMITED,
|
|
50
|
+
timeout: MODEL_SELECTION_REASON_CODES.PROBE_TIMEOUT,
|
|
51
|
+
'internal-error': MODEL_SELECTION_REASON_CODES.PROVIDER_INTERNAL_ERROR,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const BLOCKED_MODEL_CODES = Object.freeze({
|
|
55
|
+
[MODEL_SELECTION_REASON_CODES.CREDENTIAL_MISSING]: 'MODEL_BLOCKED_CREDENTIAL',
|
|
56
|
+
[MODEL_SELECTION_REASON_CODES.QUOTA_EXHAUSTED]: 'MODEL_BLOCKED_QUOTA',
|
|
57
|
+
[MODEL_SELECTION_REASON_CODES.RATE_LIMITED]: 'MODEL_BLOCKED_RATE_LIMIT',
|
|
58
|
+
[MODEL_SELECTION_REASON_CODES.PROBE_TIMEOUT]: 'MODEL_BLOCKED_TIMEOUT',
|
|
59
|
+
[MODEL_SELECTION_REASON_CODES.PROVIDER_INTERNAL_ERROR]: 'MODEL_BLOCKED_PROVIDER',
|
|
60
|
+
[MODEL_SELECTION_REASON_CODES.PROVIDER_TOMBSTONED]: 'MODEL_BLOCKED_TOMBSTONED',
|
|
61
|
+
[MODEL_SELECTION_REASON_CODES.PEAK_RESTRICTED]: 'MODEL_BLOCKED_PEAK',
|
|
62
|
+
});
|
|
59
63
|
|
|
60
64
|
export function normalizeModelRef(raw) {
|
|
61
65
|
if (!raw || typeof raw !== 'object') return null;
|
|
@@ -124,7 +128,7 @@ function traceBase({
|
|
|
124
128
|
};
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
function candidateDecision(ref, source, status, { reasonCode, advertised } = {}) {
|
|
131
|
+
function candidateDecision(ref, source, status, { reasonCode, advertised } = {}) {
|
|
128
132
|
return {
|
|
129
133
|
provider: ref?.provider ?? null,
|
|
130
134
|
model: ref?.model ?? null,
|
|
@@ -133,28 +137,40 @@ function candidateDecision(ref, source, status, { reasonCode, advertised } = {})
|
|
|
133
137
|
...(reasonCode ? { reason_code: reasonCode } : {}),
|
|
134
138
|
...(advertised === false ? { advertised: false } : {}),
|
|
135
139
|
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function healthAdmissionReason(ref, { healthStore, healthGate, tombstones } = {}) {
|
|
139
|
-
if (tombstones?.[ref?.provider] === 'absent') return MODEL_SELECTION_REASON_CODES.PROVIDER_TOMBSTONED;
|
|
140
|
-
if (healthGate !== 'hard-failures' || typeof healthStore?.get !== 'function') return null;
|
|
141
|
-
const observation = healthStore.get(ref?.provider, ref?.model);
|
|
142
|
-
if (observation?.fresh !== true) return null;
|
|
143
|
-
return HEALTH_BLOCK_REASONS[observation.state] ?? null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function healthAdmissionReason(ref, { healthStore, healthGate, tombstones } = {}) {
|
|
143
|
+
if (tombstones?.[ref?.provider] === 'absent') return MODEL_SELECTION_REASON_CODES.PROVIDER_TOMBSTONED;
|
|
144
|
+
if (healthGate !== 'hard-failures' || typeof healthStore?.get !== 'function') return null;
|
|
145
|
+
const observation = healthStore.get(ref?.provider, ref?.model);
|
|
146
|
+
if (observation?.fresh !== true) return null;
|
|
147
|
+
return HEALTH_BLOCK_REASONS[observation.state] ?? null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Peak/off-peak admission for one candidate.
|
|
152
|
+
*
|
|
153
|
+
* Returns `null` when the model is unrestricted or off-peak; otherwise the
|
|
154
|
+
* schedule's verdict, which the caller applies. A `warn` rule is advisory — the
|
|
155
|
+
* candidate is still selected, and the trace records why.
|
|
156
|
+
*/
|
|
157
|
+
function peakAdmission(schedule, ref, at) {
|
|
158
|
+
if (!schedule) return null;
|
|
159
|
+
return scheduleAdmission(schedule, ref, at);
|
|
160
|
+
}
|
|
155
161
|
|
|
156
|
-
function
|
|
157
|
-
trace.
|
|
162
|
+
function blockedSelection(trace, ref, source, reasonCode) {
|
|
163
|
+
trace.fallback_reason = reasonCode;
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
code: BLOCKED_MODEL_CODES[reasonCode] ?? 'MODEL_BLOCKED_PROVIDER',
|
|
167
|
+
message: `Model ${ref?.provider ?? 'unknown'}/${ref?.model ?? 'unknown'} is blocked by ${reasonCode}.`,
|
|
168
|
+
selection_trace: trace,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function selectTrace(trace, ref, source, { advertised, fallbackReason, reasonCode } = {}) {
|
|
173
|
+
trace.ordered_candidates.push(candidateDecision(ref, source, 'selected', { advertised, reasonCode }));
|
|
158
174
|
trace.selected = { provider: ref.provider, model: ref.model, source };
|
|
159
175
|
trace.selection_source = source;
|
|
160
176
|
trace.fallback_reason = fallbackReason ?? null;
|
|
@@ -190,6 +206,7 @@ export function buildDirectSelectionTrace({
|
|
|
190
206
|
model,
|
|
191
207
|
source = 'legacy-strict',
|
|
192
208
|
escalationReason = null,
|
|
209
|
+
peakAdvisory = false,
|
|
193
210
|
} = {}) {
|
|
194
211
|
const trace = traceBase({ role, logicalAttempt, modelClassHint, strategy, candidateSet, escalationReason });
|
|
195
212
|
const ref = normalizeModelRef({ provider, model });
|
|
@@ -197,7 +214,9 @@ export function buildDirectSelectionTrace({
|
|
|
197
214
|
trace.fallback_reason = MODEL_SELECTION_REASON_CODES.NO_AVAILABLE_MODEL;
|
|
198
215
|
return trace;
|
|
199
216
|
}
|
|
200
|
-
return selectTrace(trace, ref, source
|
|
217
|
+
return selectTrace(trace, ref, source, {
|
|
218
|
+
reasonCode: peakAdvisory ? MODEL_SELECTION_REASON_CODES.PEAK_ADVISORY : null,
|
|
219
|
+
});
|
|
201
220
|
}
|
|
202
221
|
|
|
203
222
|
/**
|
|
@@ -239,7 +258,7 @@ export function enrichSelectionTrace(trace, {
|
|
|
239
258
|
return base;
|
|
240
259
|
}
|
|
241
260
|
|
|
242
|
-
export function resolveWorkerModel({
|
|
261
|
+
export function resolveWorkerModel({
|
|
243
262
|
tier,
|
|
244
263
|
priority,
|
|
245
264
|
priorityConfigured = false,
|
|
@@ -248,13 +267,15 @@ export function resolveWorkerModel({
|
|
|
248
267
|
fallback = 'harness-default',
|
|
249
268
|
preferredModelId = DEFAULT_TIER_MODEL_PREFERENCES[tier],
|
|
250
269
|
traceContext = {},
|
|
251
|
-
adaptive,
|
|
252
|
-
adaptiveHealth,
|
|
253
|
-
healthStore,
|
|
254
|
-
healthGate,
|
|
255
|
-
allowFallback = true,
|
|
256
|
-
tombstones,
|
|
257
|
-
|
|
270
|
+
adaptive,
|
|
271
|
+
adaptiveHealth,
|
|
272
|
+
healthStore,
|
|
273
|
+
healthGate,
|
|
274
|
+
allowFallback = true,
|
|
275
|
+
tombstones,
|
|
276
|
+
schedule,
|
|
277
|
+
at,
|
|
278
|
+
} = {}) {
|
|
258
279
|
const providers = providerMap(catalog);
|
|
259
280
|
const normalizedPriority = normalizeModelPriority(priority);
|
|
260
281
|
const prioritySource = typeof traceContext.prioritySource === 'string' && traceContext.prioritySource
|
|
@@ -269,37 +290,57 @@ export function resolveWorkerModel({
|
|
|
269
290
|
escalationReason: traceContext.escalationReason ?? null,
|
|
270
291
|
});
|
|
271
292
|
|
|
293
|
+
// One clock read for the whole resolution. Calling `new Date()` per admission
|
|
294
|
+
// would let a resolution that straddles a peak boundary judge candidates
|
|
295
|
+
// against different instants — the filter and the all-blocked rescan below
|
|
296
|
+
// could then disagree about the same candidate.
|
|
297
|
+
const selectionAt = at ?? new Date();
|
|
298
|
+
|
|
272
299
|
// Explicit priority — including an intentionally empty configured list — is
|
|
273
300
|
// authoritative. Adaptive mode records that bypass but never reorders it.
|
|
274
301
|
if (priorityConfigured || normalizedPriority.length > 0) {
|
|
275
302
|
rankForTrace(trace, normalizedPriority, { adaptive, adaptiveHealth, explicitPriority: true });
|
|
276
303
|
}
|
|
277
304
|
|
|
278
|
-
for (let index = 0; index < normalizedPriority.length; index++) {
|
|
279
|
-
const ref = normalizedPriority[index];
|
|
280
|
-
const provider = providers.get(ref.provider);
|
|
305
|
+
for (let index = 0; index < normalizedPriority.length; index++) {
|
|
306
|
+
const ref = normalizedPriority[index];
|
|
307
|
+
const provider = providers.get(ref.provider);
|
|
281
308
|
if (!provider) {
|
|
282
309
|
trace.ordered_candidates.push(candidateDecision(ref, prioritySource, 'skipped', {
|
|
283
310
|
reasonCode: MODEL_SELECTION_REASON_CODES.PROVIDER_UNAVAILABLE,
|
|
284
|
-
}));
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
const advertised = (provider.models ?? []).some((model) => model?.id === ref.model);
|
|
288
|
-
const healthReason = healthAdmissionReason(ref, { healthStore, healthGate, tombstones });
|
|
289
|
-
if (healthReason) {
|
|
290
|
-
trace.ordered_candidates.push(candidateDecision(ref, prioritySource, 'skipped', {
|
|
291
|
-
reasonCode: healthReason, advertised,
|
|
292
|
-
}));
|
|
293
|
-
if (allowFallback === false) return blockedSelection(trace, ref, prioritySource, healthReason);
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
|
|
311
|
+
}));
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const advertised = (provider.models ?? []).some((model) => model?.id === ref.model);
|
|
315
|
+
const healthReason = healthAdmissionReason(ref, { healthStore, healthGate, tombstones });
|
|
316
|
+
if (healthReason) {
|
|
317
|
+
trace.ordered_candidates.push(candidateDecision(ref, prioritySource, 'skipped', {
|
|
318
|
+
reasonCode: healthReason, advertised,
|
|
319
|
+
}));
|
|
320
|
+
if (allowFallback === false) return blockedSelection(trace, ref, prioritySource, healthReason);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
// A peak-blocked model is skipped so the next candidate can serve the job;
|
|
324
|
+
// only an explicit no-fallback call turns it into a hard failure.
|
|
325
|
+
const peak = peakAdmission(schedule, ref, selectionAt);
|
|
326
|
+
if (peak?.mode === 'block') {
|
|
327
|
+
trace.ordered_candidates.push(candidateDecision(ref, prioritySource, 'skipped', {
|
|
328
|
+
reasonCode: MODEL_SELECTION_REASON_CODES.PEAK_RESTRICTED, advertised,
|
|
329
|
+
}));
|
|
330
|
+
if (allowFallback === false) return blockedSelection(trace, ref, prioritySource, MODEL_SELECTION_REASON_CODES.PEAK_RESTRICTED);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
selectTrace(trace, ref, prioritySource, {
|
|
334
|
+
advertised,
|
|
335
|
+
reasonCode: peak?.mode === 'warn' ? MODEL_SELECTION_REASON_CODES.PEAK_ADVISORY : null,
|
|
336
|
+
});
|
|
297
337
|
return {
|
|
298
338
|
ok: true,
|
|
299
339
|
...ref,
|
|
300
340
|
source: 'priority',
|
|
301
341
|
matchedPriorityIndex: index,
|
|
302
342
|
...(advertised ? {} : { advertised: false }),
|
|
343
|
+
...(peak?.mode === 'warn' ? { peak_advisory: true } : {}),
|
|
303
344
|
selection_trace: trace,
|
|
304
345
|
};
|
|
305
346
|
}
|
|
@@ -312,47 +353,79 @@ export function resolveWorkerModel({
|
|
|
312
353
|
if ((provider.models ?? []).some((model) => model?.id === preferredModelId)) {
|
|
313
354
|
matches.push({ provider: provider.id, model: preferredModelId });
|
|
314
355
|
}
|
|
315
|
-
}
|
|
316
|
-
if (matches.length === 1) {
|
|
317
|
-
const preferred = matches[0];
|
|
318
|
-
const healthReason = healthAdmissionReason(preferred, { healthStore, healthGate, tombstones });
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
+
}
|
|
357
|
+
if (matches.length === 1) {
|
|
358
|
+
const preferred = matches[0];
|
|
359
|
+
const healthReason = healthAdmissionReason(preferred, { healthStore, healthGate, tombstones });
|
|
360
|
+
const peak = healthReason ? null : peakAdmission(schedule, preferred, selectionAt);
|
|
361
|
+
const blockReason = healthReason ?? (peak?.mode === 'block' ? MODEL_SELECTION_REASON_CODES.PEAK_RESTRICTED : null);
|
|
362
|
+
if (blockReason) {
|
|
363
|
+
trace.ordered_candidates.push(candidateDecision(preferred, 'preferred-default', 'skipped', {
|
|
364
|
+
reasonCode: blockReason,
|
|
365
|
+
}));
|
|
366
|
+
if (allowFallback === false) return blockedSelection(trace, preferred, 'preferred-default', blockReason);
|
|
367
|
+
} else {
|
|
368
|
+
rankForTrace(trace, matches, { adaptive, adaptiveHealth });
|
|
369
|
+
selectTrace(trace, preferred, 'preferred-default', {
|
|
370
|
+
reasonCode: peak?.mode === 'warn' ? MODEL_SELECTION_REASON_CODES.PEAK_ADVISORY : null,
|
|
371
|
+
});
|
|
372
|
+
return {
|
|
373
|
+
ok: true,
|
|
374
|
+
...preferred,
|
|
375
|
+
source: 'preferred-default',
|
|
376
|
+
...(peak?.mode === 'warn' ? { peak_advisory: true } : {}),
|
|
377
|
+
selection_trace: trace,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (matches.length > 1) {
|
|
382
|
+
// Keep each admitted candidate's advisory verdict alongside it: a `warn`
|
|
383
|
+
// rule is only advisory if the caller learns about it, and recomputing it
|
|
384
|
+
// after the pick would re-evaluate a clock already read once.
|
|
385
|
+
const verdicts = new Map();
|
|
386
|
+
const availableMatches = matches.filter((candidate) => {
|
|
387
|
+
const healthReason = healthAdmissionReason(candidate, { healthStore, healthGate, tombstones });
|
|
388
|
+
const peak = healthReason ? null : peakAdmission(schedule, candidate, selectionAt);
|
|
389
|
+
const reason = healthReason ?? (peak?.mode === 'block' ? MODEL_SELECTION_REASON_CODES.PEAK_RESTRICTED : null);
|
|
390
|
+
if (!reason) {
|
|
391
|
+
verdicts.set(modelRefKey(candidate), peak ?? null);
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
trace.ordered_candidates.push(candidateDecision(candidate, 'preferred-default', 'skipped', {
|
|
395
|
+
reasonCode: reason,
|
|
396
|
+
}));
|
|
397
|
+
return false;
|
|
398
|
+
});
|
|
399
|
+
if (availableMatches.length === 0) {
|
|
400
|
+
const blocked = matches.find((candidate) => healthAdmissionReason(candidate, { healthStore, healthGate, tombstones })
|
|
401
|
+
|| peakAdmission(schedule, candidate, selectionAt)?.mode === 'block');
|
|
402
|
+
if (blocked && allowFallback === false) {
|
|
403
|
+
const reason = healthAdmissionReason(blocked, { healthStore, healthGate, tombstones })
|
|
404
|
+
?? MODEL_SELECTION_REASON_CODES.PEAK_RESTRICTED;
|
|
405
|
+
return blockedSelection(trace, blocked, 'preferred-default', reason);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const preferredProvider = normalizeModelRef(harnessDefault)?.provider;
|
|
409
|
+
const deterministicMatch = availableMatches.find((candidate) => candidate.provider === preferredProvider) ?? null;
|
|
410
|
+
// Filtering can leave exactly one admissible candidate while removing the
|
|
411
|
+
// one that matched the Harness Default provider, which leaves both the
|
|
412
|
+
// deterministic match and the adaptive choice empty. Ambiguity only means
|
|
413
|
+
// something with two or more candidates left, so a lone survivor is picked
|
|
414
|
+
// outright rather than reported as ambiguous and dropped.
|
|
415
|
+
const soleMatch = availableMatches.length === 1 ? availableMatches[0] : null;
|
|
416
|
+
const baseline = deterministicMatch
|
|
417
|
+
? [deterministicMatch, ...availableMatches.filter((candidate) => candidate.provider !== deterministicMatch.provider)]
|
|
418
|
+
: availableMatches;
|
|
419
|
+
// Ranking still runs for a lone survivor so `trace.adaptive` reflects the
|
|
420
|
+
// configured policy in this shape too; the result is not consulted, because
|
|
421
|
+
// ranking orders candidates rather than admitting them and must never
|
|
422
|
+
// discard the only one that passed admission.
|
|
423
|
+
const ranked = rankForTrace(trace, soleMatch ? [soleMatch] : baseline, { adaptive, adaptiveHealth });
|
|
424
|
+
const adaptiveChoice = !soleMatch && ranked?.trace.decision_supported ? ranked.candidates[0] : null;
|
|
425
|
+
const match = soleMatch ?? adaptiveChoice ?? deterministicMatch;
|
|
426
|
+
if (match) {
|
|
427
|
+
const decisionOrder = soleMatch ? [] : adaptiveChoice ? ranked.candidates : availableMatches;
|
|
428
|
+
for (const candidate of decisionOrder) {
|
|
356
429
|
if (candidate.provider === match.provider && candidate.model === match.model) continue;
|
|
357
430
|
trace.ordered_candidates.push(candidateDecision(candidate, 'preferred-default', 'skipped', {
|
|
358
431
|
reasonCode: adaptiveChoice
|
|
@@ -360,10 +433,21 @@ export function resolveWorkerModel({
|
|
|
360
433
|
: MODEL_SELECTION_REASON_CODES.PREFERRED_MODEL_AMBIGUOUS,
|
|
361
434
|
}));
|
|
362
435
|
}
|
|
363
|
-
|
|
364
|
-
|
|
436
|
+
const verdict = verdicts.get(modelRefKey(match)) ?? null;
|
|
437
|
+
selectTrace(trace, match, 'preferred-default', {
|
|
438
|
+
reasonCode: verdict?.mode === 'warn' ? MODEL_SELECTION_REASON_CODES.PEAK_ADVISORY : null,
|
|
439
|
+
});
|
|
440
|
+
return {
|
|
441
|
+
ok: true,
|
|
442
|
+
...match,
|
|
443
|
+
source: 'preferred-default',
|
|
444
|
+
...(verdict?.mode === 'warn' ? { peak_advisory: true } : {}),
|
|
445
|
+
selection_trace: trace,
|
|
446
|
+
};
|
|
365
447
|
}
|
|
366
|
-
|
|
448
|
+
// Only the candidates still in play can be ambiguous; one already rejected
|
|
449
|
+
// for a concrete reason keeps that reason and appears once.
|
|
450
|
+
for (const candidate of availableMatches) {
|
|
367
451
|
trace.ordered_candidates.push(candidateDecision(candidate, 'preferred-default', 'skipped', {
|
|
368
452
|
reasonCode: MODEL_SELECTION_REASON_CODES.PREFERRED_MODEL_AMBIGUOUS,
|
|
369
453
|
}));
|
|
@@ -385,37 +469,52 @@ export function resolveWorkerModel({
|
|
|
385
469
|
|
|
386
470
|
if (fallback === 'harness-default') {
|
|
387
471
|
const defaultRef = normalizeModelRef(harnessDefault);
|
|
472
|
+
// The Harness Default is often also a priority or preferred candidate, and
|
|
473
|
+
// that candidate has already been judged. Recording it a second time with a
|
|
474
|
+
// different reason would make the trace contradict itself, so a ref that
|
|
475
|
+
// already has a verdict keeps it.
|
|
476
|
+
const alreadyJudged = defaultRef
|
|
477
|
+
? trace.ordered_candidates.some((row) => row.provider === defaultRef.provider && row.model === defaultRef.model)
|
|
478
|
+
: false;
|
|
388
479
|
if (!defaultRef) {
|
|
389
480
|
trace.ordered_candidates.push(candidateDecision(null, 'harness-default', 'skipped', {
|
|
390
481
|
reasonCode: MODEL_SELECTION_REASON_CODES.HARNESS_DEFAULT_INVALID,
|
|
391
482
|
}));
|
|
483
|
+
} else if (alreadyJudged) {
|
|
484
|
+
// Keep its existing verdict; nothing further to record.
|
|
392
485
|
} else if (!providers.has(defaultRef.provider)) {
|
|
393
486
|
trace.ordered_candidates.push(candidateDecision(defaultRef, 'harness-default', 'skipped', {
|
|
394
487
|
reasonCode: MODEL_SELECTION_REASON_CODES.HARNESS_DEFAULT_PROVIDER_UNAVAILABLE,
|
|
395
488
|
}));
|
|
396
489
|
} else {
|
|
397
|
-
const fallbackReason = traceContext.candidateSet === 'escalation'
|
|
398
|
-
? MODEL_SELECTION_REASON_CODES.ESCALATION_CANDIDATES_EXHAUSTED
|
|
399
|
-
: MODEL_SELECTION_REASON_CODES.PRIMARY_CANDIDATES_EXHAUSTED;
|
|
400
|
-
const healthReason = healthAdmissionReason(defaultRef, { healthStore, healthGate, tombstones });
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
490
|
+
const fallbackReason = traceContext.candidateSet === 'escalation'
|
|
491
|
+
? MODEL_SELECTION_REASON_CODES.ESCALATION_CANDIDATES_EXHAUSTED
|
|
492
|
+
: MODEL_SELECTION_REASON_CODES.PRIMARY_CANDIDATES_EXHAUSTED;
|
|
493
|
+
const healthReason = healthAdmissionReason(defaultRef, { healthStore, healthGate, tombstones });
|
|
494
|
+
const peak = healthReason ? null : peakAdmission(schedule, defaultRef, selectionAt);
|
|
495
|
+
const blockReason = healthReason ?? (peak?.mode === 'block' ? MODEL_SELECTION_REASON_CODES.PEAK_RESTRICTED : null);
|
|
496
|
+
if (blockReason) {
|
|
497
|
+
trace.ordered_candidates.push(candidateDecision(defaultRef, 'harness-default', 'skipped', {
|
|
498
|
+
reasonCode: blockReason,
|
|
499
|
+
}));
|
|
500
|
+
if (allowFallback === false) return blockedSelection(trace, defaultRef, 'harness-default', blockReason);
|
|
501
|
+
} else {
|
|
502
|
+
selectTrace(trace, defaultRef, 'harness-default', {
|
|
503
|
+
fallbackReason,
|
|
504
|
+
reasonCode: peak?.mode === 'warn' ? MODEL_SELECTION_REASON_CODES.PEAK_ADVISORY : null,
|
|
505
|
+
});
|
|
506
|
+
return {
|
|
409
507
|
ok: true,
|
|
410
508
|
...defaultRef,
|
|
411
509
|
source: 'harness-default',
|
|
510
|
+
...(peak?.mode === 'warn' ? { peak_advisory: true } : {}),
|
|
412
511
|
...(typeof harnessDefault.reasoningEffort === 'string' && harnessDefault.reasoningEffort
|
|
413
512
|
? { reasoningEffort: harnessDefault.reasoningEffort }
|
|
414
513
|
: {}),
|
|
415
|
-
selection_trace: trace,
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
}
|
|
514
|
+
selection_trace: trace,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
}
|
|
419
518
|
}
|
|
420
519
|
trace.fallback_reason = MODEL_SELECTION_REASON_CODES.NO_AVAILABLE_MODEL;
|
|
421
520
|
return {
|
|
@@ -435,18 +534,20 @@ export function resolveWorkerModel({
|
|
|
435
534
|
* The output shape matches resolveWorkerModel (provider/model/source/...) plus
|
|
436
535
|
* role + attempt so selection provenance lands in job metadata.
|
|
437
536
|
*/
|
|
438
|
-
export function resolveModel({
|
|
537
|
+
export function resolveModel({
|
|
439
538
|
role = 'worker',
|
|
440
539
|
attempt = 0,
|
|
441
540
|
policy,
|
|
442
541
|
catalog,
|
|
443
542
|
harnessDefault,
|
|
444
|
-
adaptiveHealth,
|
|
445
|
-
healthStore,
|
|
446
|
-
healthGate,
|
|
447
|
-
allowFallback = true,
|
|
448
|
-
tombstones,
|
|
449
|
-
|
|
543
|
+
adaptiveHealth,
|
|
544
|
+
healthStore,
|
|
545
|
+
healthGate,
|
|
546
|
+
allowFallback = true,
|
|
547
|
+
tombstones,
|
|
548
|
+
schedule,
|
|
549
|
+
at,
|
|
550
|
+
} = {}) {
|
|
450
551
|
const p = policy && typeof policy === 'object' ? policy : {};
|
|
451
552
|
const escalated = Number.isInteger(attempt) && attempt > 0;
|
|
452
553
|
const candidates = escalated ? p.escalation_priority : p.priority;
|
|
@@ -464,12 +565,14 @@ export function resolveModel({
|
|
|
464
565
|
harnessDefault,
|
|
465
566
|
fallback: p.fallback === 'harness-default' ? 'harness-default' : 'harness-default',
|
|
466
567
|
preferredModelId,
|
|
467
|
-
adaptive: p.adaptive,
|
|
468
|
-
adaptiveHealth,
|
|
469
|
-
healthStore,
|
|
470
|
-
healthGate,
|
|
471
|
-
allowFallback,
|
|
472
|
-
tombstones,
|
|
568
|
+
adaptive: p.adaptive,
|
|
569
|
+
adaptiveHealth,
|
|
570
|
+
healthStore,
|
|
571
|
+
healthGate,
|
|
572
|
+
allowFallback,
|
|
573
|
+
tombstones,
|
|
574
|
+
schedule,
|
|
575
|
+
at,
|
|
473
576
|
traceContext: {
|
|
474
577
|
role,
|
|
475
578
|
logicalAttempt: attempt,
|