@ran-sh/dsh-crew 1.9.0 → 1.10.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.
Files changed (151) hide show
  1. package/.claude-plugin/marketplace.json +16 -16
  2. package/.claude-plugin/plugin.json +14 -14
  3. package/LICENSE +21 -21
  4. package/README.de.md +359 -359
  5. package/README.es.md +359 -359
  6. package/README.fr.md +359 -359
  7. package/README.hi.md +359 -359
  8. package/README.id.md +359 -359
  9. package/README.ja.md +359 -359
  10. package/README.ko.md +359 -359
  11. package/README.md +126 -126
  12. package/README.pt.md +359 -359
  13. package/README.ru.md +359 -359
  14. package/README.th.md +359 -359
  15. package/README.tr.md +359 -359
  16. package/README.vi.md +359 -359
  17. package/README.zh-TW.md +359 -359
  18. package/README.zh.md +116 -116
  19. package/agents/ds-flash.md +24 -24
  20. package/agents/ds-pro.md +24 -24
  21. package/agents/ds-reviewer.md +23 -23
  22. package/agents/ds-worker.md +23 -23
  23. package/bin/dsh-crew.mjs +16 -16
  24. package/codex/agents/ds-flash.toml +34 -34
  25. package/codex/agents/ds-pro.toml +34 -34
  26. package/codex/agents/ds-reviewer.toml +32 -32
  27. package/codex/agents/ds-worker.toml +32 -32
  28. package/codex/prompts/dsh-config.md +19 -19
  29. package/codex/prompts/dsh-status.md +5 -5
  30. package/commands/config.md +23 -23
  31. package/commands/off.md +5 -5
  32. package/commands/on.md +5 -5
  33. package/commands/status.md +9 -9
  34. package/cordis.patch.yml +4 -4
  35. package/docs/gpt-relay-extension.md +103 -103
  36. package/docs/installation.md +138 -138
  37. package/docs/job-contracts.md +103 -103
  38. package/docs/readiness-matrix.md +85 -85
  39. package/docs/ui-surfaces.md +107 -107
  40. package/official-web-bridge/cordis.patch.yml +4 -4
  41. package/official-web-bridge/entry.mjs +1 -1
  42. package/official-web-bridge/overlay-entry.mjs +59 -59
  43. package/official-web-bridge/package.json +25 -25
  44. package/package.json +3 -2
  45. package/scripts/build-client.mjs +49 -49
  46. package/scripts/live-crew-smoke.mjs +39 -39
  47. package/scripts/live-policy-matrix.mjs +177 -177
  48. package/scripts/policy-probe.mjs +101 -101
  49. package/scripts/remove-legacy-official-bridge.ps1 +89 -89
  50. package/scripts/setup.mjs +393 -393
  51. package/scripts/smoke-real.mjs +110 -110
  52. package/scripts/smoke.mjs +78 -78
  53. package/scripts/verify-crew-ui-polish.mjs +145 -145
  54. package/scripts/verify-history-ui.mjs +97 -97
  55. package/scripts/verify-installer-fix.mjs +26 -26
  56. package/scripts/verify-npm-install.mjs +311 -311
  57. package/scripts/verify-official-bridge-e2e.mjs +192 -192
  58. package/src/adaptive-routing.mjs +260 -260
  59. package/src/client/activation-summary.tsx +64 -64
  60. package/src/client/collapsible-sections.mjs +55 -55
  61. package/src/client/history-panel.tsx +108 -108
  62. package/src/client/host-readiness.mjs +71 -71
  63. package/src/client/index.tsx +1711 -1711
  64. package/src/client/model-callability-view.mjs +17 -17
  65. package/src/client/panel-chrome.tsx +40 -40
  66. package/src/client/quick-entry.tsx +10 -10
  67. package/src/client/quick-panel.tsx +275 -275
  68. package/src/client/readiness-envelope.mjs +34 -34
  69. package/src/client/surface-detection.mjs +43 -43
  70. package/src/config-readiness.mjs +226 -226
  71. package/src/credential-reference.mjs +38 -38
  72. package/src/delivery.mjs +205 -205
  73. package/src/dsh-cli-runtime.mjs +1021 -1021
  74. package/src/dsh-cohort.mjs +20 -20
  75. package/src/extension-contract.mjs +104 -104
  76. package/src/failure-classification.mjs +201 -201
  77. package/src/history/admission-gate.mjs +67 -67
  78. package/src/history/archive-store.mjs +272 -272
  79. package/src/history/cleanup-plan.mjs +89 -89
  80. package/src/history/http.mjs +32 -32
  81. package/src/history/operation.mjs +86 -86
  82. package/src/history/runner-detach.mjs +34 -34
  83. package/src/history/runner.mjs +52 -52
  84. package/src/history/runtime.mjs +36 -36
  85. package/src/history/service.mjs +177 -177
  86. package/src/history/state.mjs +27 -27
  87. package/src/hub/entry.mjs +104 -104
  88. package/src/hub/index.mjs +2694 -2694
  89. package/src/hub-client.mjs +154 -154
  90. package/src/hub-compatibility.mjs +42 -42
  91. package/src/i18n.mjs +19 -19
  92. package/src/information-flow.mjs +67 -67
  93. package/src/install/cli.mjs +28 -28
  94. package/src/install/install-legacy.mjs +711 -711
  95. package/src/install/install.mjs +483 -483
  96. package/src/install/npx-lifecycle.mjs +3456 -3456
  97. package/src/install/official-frontend-assets.mjs +78 -78
  98. package/src/install/official-web.mjs +95 -95
  99. package/src/install/payload-content.mjs +88 -88
  100. package/src/install/windows-startup.mjs +236 -236
  101. package/src/install/windows-supervisor-adapter.mjs +443 -443
  102. package/src/install/windows-supervisor-lifecycle.mjs +782 -782
  103. package/src/install/zcode.mjs +397 -397
  104. package/src/job-contracts.mjs +255 -255
  105. package/src/local-request-guard.mjs +60 -60
  106. package/src/mcp-runtime.mjs +340 -340
  107. package/src/model-callability-contract.mjs +79 -79
  108. package/src/model-catalog.mjs +180 -180
  109. package/src/model-routing.mjs +586 -586
  110. package/src/model-schedule.mjs +207 -207
  111. package/src/official-web-bridge.mjs +447 -447
  112. package/src/policy.mjs +235 -235
  113. package/src/provider-delete-adapters.mjs +1934 -1934
  114. package/src/provider-health.mjs +130 -130
  115. package/src/provider-inventory.mjs +182 -182
  116. package/src/provider-layer-migration-adapters.mjs +759 -759
  117. package/src/provider-layer-migration.mjs +198 -198
  118. package/src/provider-lifecycle-state.mjs +103 -103
  119. package/src/provider-lifecycle.mjs +252 -252
  120. package/src/provider-profile-store.mjs +390 -390
  121. package/src/provider-settings-store.mjs +633 -633
  122. package/src/provider-store-lock.mjs +67 -67
  123. package/src/readiness-matrix.mjs +181 -181
  124. package/src/removable-waiter.mjs +29 -29
  125. package/src/role-profiles.mjs +107 -107
  126. package/src/runtime-controls.mjs +84 -84
  127. package/src/runtime-identity-contract.mjs +34 -34
  128. package/src/runtime-identity.mjs +235 -235
  129. package/src/runtime-readiness-snapshot.mjs +285 -285
  130. package/src/server.mjs +581 -581
  131. package/src/session-origins.mjs +60 -60
  132. package/src/standalone-sdk.mjs +23 -23
  133. package/src/status-shard.mjs +63 -63
  134. package/src/structured-error-code.mjs +38 -38
  135. package/src/supervisor/restart-request.mjs +256 -256
  136. package/src/workflow-runtime.mjs +739 -739
  137. package/src/workflow.mjs +155 -155
  138. package/src/workspace-audit.mjs +231 -231
  139. package/src/workspace-context.mjs +146 -146
  140. package/src/workspace-isolation.mjs +463 -455
  141. package/src/workspace-readiness.mjs +32 -32
  142. package/statusline/statusline.sh +14 -14
  143. package/statusline/worker-segment.sh +35 -35
  144. package/windows/start-dsh-crew.cmd +57 -57
  145. package/windows/start-dsh-crew.ps1 +1302 -1302
  146. package/windows/supervisor-control.ps1 +467 -467
  147. package/worker.cordis.yml +67 -67
  148. package/zcode/agents/ds-reviewer.md +31 -31
  149. package/zcode/agents/ds-worker.md +31 -31
  150. package/zcode/commands/dsh-config.md +17 -17
  151. package/zcode/commands/dsh-status.md +5 -5
@@ -1,586 +1,586 @@
1
- // Pure Harness-backed worker model selection. A worker tier describes a role,
2
- // not a fixed model: explicit provider/model priorities win, fresh configs may
3
- // use a tier-specific preferred model id, and Harness Default is the final
4
- // fallback. Catalog membership is advisory; provider registration is the
5
- // routing boundary.
6
-
7
- import { rankAdaptiveCandidates } from './adaptive-routing.mjs';
8
- import { scheduleAdmission } from './model-schedule.mjs';
9
-
10
- export const DEFAULT_TIER_MODEL_PREFERENCES = Object.freeze({
11
- flash: 'deepseek-v4-flash',
12
- pro: 'deepseek-v4-pro',
13
- });
14
-
15
- // v0.2 role → default preferred model class. A role describes who does the
16
- // work; the model class is only the fresh-config recommendation until a
17
- // priority list or Harness Default takes over.
18
- export const DEFAULT_ROLE_MODEL_PREFERENCES = Object.freeze({
19
- worker: 'deepseek-v4-flash',
20
- reviewer: 'deepseek-v4-pro',
21
- });
22
-
23
- export const MODEL_FALLBACKS = ['harness-default'];
24
- export const NO_WORKER_MODEL_AVAILABLE = 'NO_WORKER_MODEL_AVAILABLE';
25
- export const MODEL_SELECTION_TRACE_VERSION = 1;
26
- export const MODEL_SELECTION_REASON_CODES = Object.freeze({
27
- PROVIDER_UNAVAILABLE: 'PROVIDER_UNAVAILABLE',
28
- PREFERRED_MODEL_UNAVAILABLE: 'PREFERRED_MODEL_UNAVAILABLE',
29
- PREFERRED_MODEL_AMBIGUOUS: 'PREFERRED_MODEL_AMBIGUOUS',
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',
40
- HARNESS_DEFAULT_PROVIDER_UNAVAILABLE: 'HARNESS_DEFAULT_PROVIDER_UNAVAILABLE',
41
- PRIMARY_CANDIDATES_EXHAUSTED: 'PRIMARY_CANDIDATES_EXHAUSTED',
42
- ESCALATION_CANDIDATES_EXHAUSTED: 'ESCALATION_CANDIDATES_EXHAUSTED',
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
- });
63
-
64
- export function normalizeModelRef(raw) {
65
- if (!raw || typeof raw !== 'object') return null;
66
- const provider = typeof raw.provider === 'string' ? raw.provider.trim() : '';
67
- const model = typeof raw.model === 'string' ? raw.model.trim() : '';
68
- return provider && model ? { provider, model } : null;
69
- }
70
-
71
- export function modelRefKey(raw) {
72
- const ref = normalizeModelRef(raw);
73
- return ref ? `${ref.provider}\0${ref.model}` : '';
74
- }
75
-
76
- export function normalizeModelPriority(raw) {
77
- if (!Array.isArray(raw)) return [];
78
- const seen = new Set();
79
- const result = [];
80
- for (const value of raw) {
81
- const ref = normalizeModelRef(value);
82
- if (!ref) continue;
83
- const key = modelRefKey(ref);
84
- if (seen.has(key)) continue;
85
- seen.add(key);
86
- result.push(ref);
87
- }
88
- return result;
89
- }
90
-
91
- function providerMap(catalog) {
92
- const map = new Map();
93
- for (const raw of catalog?.providers ?? []) {
94
- if (!raw || typeof raw.id !== 'string' || raw.id === '') continue;
95
- map.set(raw.id, raw);
96
- }
97
- return map;
98
- }
99
-
100
- function normalizeAttempt(value) {
101
- return Number.isInteger(value) && value >= 0 ? value : 0;
102
- }
103
-
104
- function normalizeModelClassHint(value) {
105
- return value === 'flash' || value === 'pro' ? value : null;
106
- }
107
-
108
- function traceBase({
109
- role = 'worker',
110
- logicalAttempt = 0,
111
- modelClassHint = null,
112
- strategy = 'legacy-tier',
113
- candidateSet = 'primary',
114
- escalationReason = null,
115
- } = {}) {
116
- return {
117
- version: MODEL_SELECTION_TRACE_VERSION,
118
- role: role === 'reviewer' ? 'reviewer' : 'worker',
119
- logical_attempt: normalizeAttempt(logicalAttempt),
120
- model_class_hint: normalizeModelClassHint(modelClassHint),
121
- strategy: typeof strategy === 'string' && strategy ? strategy : 'legacy-tier',
122
- candidate_set: typeof candidateSet === 'string' && candidateSet ? candidateSet : 'primary',
123
- ordered_candidates: [],
124
- selected: null,
125
- selection_source: null,
126
- fallback_reason: null,
127
- escalation_reason: typeof escalationReason === 'string' && escalationReason ? escalationReason : null,
128
- };
129
- }
130
-
131
- function candidateDecision(ref, source, status, { reasonCode, advertised } = {}) {
132
- return {
133
- provider: ref?.provider ?? null,
134
- model: ref?.model ?? null,
135
- source,
136
- status,
137
- ...(reasonCode ? { reason_code: reasonCode } : {}),
138
- ...(advertised === false ? { advertised: false } : {}),
139
- };
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
- }
161
-
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 }));
174
- trace.selected = { provider: ref.provider, model: ref.model, source };
175
- trace.selection_source = source;
176
- trace.fallback_reason = fallbackReason ?? null;
177
- return trace;
178
- }
179
-
180
- function rankForTrace(trace, candidates, {
181
- adaptive,
182
- adaptiveHealth,
183
- explicitPriority = false,
184
- } = {}) {
185
- const ranked = rankAdaptiveCandidates(candidates, {
186
- config: adaptive,
187
- healthStore: adaptiveHealth,
188
- role: trace.role,
189
- explicitPriority,
190
- });
191
- if (ranked.trace.enabled) trace.adaptive = ranked.trace;
192
- return ranked;
193
- }
194
-
195
- /**
196
- * Build a one-candidate trace for transports that intentionally bypass the
197
- * Harness catalog (DeepSeek Official strict mode / standalone legacy mode).
198
- */
199
- export function buildDirectSelectionTrace({
200
- role = 'worker',
201
- logicalAttempt = 0,
202
- modelClassHint = null,
203
- strategy = 'legacy-strict',
204
- candidateSet = 'primary',
205
- provider,
206
- model,
207
- source = 'legacy-strict',
208
- escalationReason = null,
209
- peakAdvisory = false,
210
- } = {}) {
211
- const trace = traceBase({ role, logicalAttempt, modelClassHint, strategy, candidateSet, escalationReason });
212
- const ref = normalizeModelRef({ provider, model });
213
- if (!ref) {
214
- trace.fallback_reason = MODEL_SELECTION_REASON_CODES.NO_AVAILABLE_MODEL;
215
- return trace;
216
- }
217
- return selectTrace(trace, ref, source, {
218
- reasonCode: peakAdvisory ? MODEL_SELECTION_REASON_CODES.PEAK_ADVISORY : null,
219
- });
220
- }
221
-
222
- /**
223
- * Add workflow-only context after a transport has resolved the model. This is
224
- * intentionally metadata-only: it never changes provider/model selection.
225
- */
226
- export function enrichSelectionTrace(trace, {
227
- role,
228
- logicalAttempt,
229
- modelClassHint,
230
- escalationReason,
231
- } = {}) {
232
- const base = trace && typeof trace === 'object'
233
- ? {
234
- ...trace,
235
- ordered_candidates: Array.isArray(trace.ordered_candidates)
236
- ? trace.ordered_candidates.map((item) => ({ ...item }))
237
- : [],
238
- selected: trace.selected && typeof trace.selected === 'object' ? { ...trace.selected } : null,
239
- ...(trace.adaptive && typeof trace.adaptive === 'object'
240
- ? {
241
- adaptive: {
242
- ...trace.adaptive,
243
- candidates: Array.isArray(trace.adaptive.candidates)
244
- ? trace.adaptive.candidates.map((item) => ({ ...item }))
245
- : [],
246
- },
247
- }
248
- : {}),
249
- }
250
- : traceBase({ role, logicalAttempt, modelClassHint, escalationReason });
251
- if (role === 'worker' || role === 'reviewer') base.role = role;
252
- if (Number.isInteger(logicalAttempt) && logicalAttempt >= 0) base.logical_attempt = logicalAttempt;
253
- if (modelClassHint === 'flash' || modelClassHint === 'pro' || modelClassHint === null) {
254
- base.model_class_hint = modelClassHint;
255
- }
256
- if (typeof escalationReason === 'string' && escalationReason) base.escalation_reason = escalationReason;
257
- else if (logicalAttempt === 0) base.escalation_reason = null;
258
- return base;
259
- }
260
-
261
- export function resolveWorkerModel({
262
- tier,
263
- priority,
264
- priorityConfigured = false,
265
- catalog,
266
- harnessDefault,
267
- fallback = 'harness-default',
268
- preferredModelId = DEFAULT_TIER_MODEL_PREFERENCES[tier],
269
- traceContext = {},
270
- adaptive,
271
- adaptiveHealth,
272
- healthStore,
273
- healthGate,
274
- allowFallback = true,
275
- tombstones,
276
- schedule,
277
- at,
278
- } = {}) {
279
- const providers = providerMap(catalog);
280
- const normalizedPriority = normalizeModelPriority(priority);
281
- const prioritySource = typeof traceContext.prioritySource === 'string' && traceContext.prioritySource
282
- ? traceContext.prioritySource
283
- : 'priority';
284
- const trace = traceBase({
285
- role: traceContext.role ?? 'worker',
286
- logicalAttempt: traceContext.logicalAttempt ?? 0,
287
- modelClassHint: traceContext.modelClassHint ?? null,
288
- strategy: traceContext.strategy ?? 'legacy-tier',
289
- candidateSet: traceContext.candidateSet ?? 'primary',
290
- escalationReason: traceContext.escalationReason ?? null,
291
- });
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
-
299
- // Explicit priority — including an intentionally empty configured list — is
300
- // authoritative. Adaptive mode records that bypass but never reorders it.
301
- if (priorityConfigured || normalizedPriority.length > 0) {
302
- rankForTrace(trace, normalizedPriority, { adaptive, adaptiveHealth, explicitPriority: true });
303
- }
304
-
305
- for (let index = 0; index < normalizedPriority.length; index++) {
306
- const ref = normalizedPriority[index];
307
- const provider = providers.get(ref.provider);
308
- if (!provider) {
309
- trace.ordered_candidates.push(candidateDecision(ref, prioritySource, 'skipped', {
310
- reasonCode: MODEL_SELECTION_REASON_CODES.PROVIDER_UNAVAILABLE,
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
- });
337
- return {
338
- ok: true,
339
- ...ref,
340
- source: 'priority',
341
- matchedPriorityIndex: index,
342
- ...(advertised ? {} : { advertised: false }),
343
- ...(peak?.mode === 'warn' ? { peak_advisory: true } : {}),
344
- selection_trace: trace,
345
- };
346
- }
347
-
348
- // A manually managed list, including an intentionally empty list, replaces
349
- // the fresh-config recommendation rather than silently re-inserting it.
350
- if (!priorityConfigured && normalizedPriority.length === 0 && typeof preferredModelId === 'string') {
351
- const matches = [];
352
- for (const provider of providers.values()) {
353
- if ((provider.models ?? []).some((model) => model?.id === preferredModelId)) {
354
- matches.push({ provider: provider.id, model: preferredModelId });
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) {
429
- if (candidate.provider === match.provider && candidate.model === match.model) continue;
430
- trace.ordered_candidates.push(candidateDecision(candidate, 'preferred-default', 'skipped', {
431
- reasonCode: adaptiveChoice
432
- ? MODEL_SELECTION_REASON_CODES.ADAPTIVE_DEPRIORITIZED
433
- : MODEL_SELECTION_REASON_CODES.PREFERRED_MODEL_AMBIGUOUS,
434
- }));
435
- }
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
- };
447
- }
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) {
451
- trace.ordered_candidates.push(candidateDecision(candidate, 'preferred-default', 'skipped', {
452
- reasonCode: MODEL_SELECTION_REASON_CODES.PREFERRED_MODEL_AMBIGUOUS,
453
- }));
454
- }
455
- } else {
456
- rankForTrace(trace, [], { adaptive, adaptiveHealth });
457
- trace.ordered_candidates.push(candidateDecision(
458
- { provider: null, model: preferredModelId },
459
- 'preferred-default',
460
- 'skipped',
461
- { reasonCode: MODEL_SELECTION_REASON_CODES.PREFERRED_MODEL_UNAVAILABLE },
462
- ));
463
- }
464
- }
465
-
466
- if (adaptive?.enabled === true && trace.adaptive === undefined) {
467
- rankForTrace(trace, [], { adaptive, adaptiveHealth });
468
- }
469
-
470
- if (fallback === 'harness-default') {
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;
479
- if (!defaultRef) {
480
- trace.ordered_candidates.push(candidateDecision(null, 'harness-default', 'skipped', {
481
- reasonCode: MODEL_SELECTION_REASON_CODES.HARNESS_DEFAULT_INVALID,
482
- }));
483
- } else if (alreadyJudged) {
484
- // Keep its existing verdict; nothing further to record.
485
- } else if (!providers.has(defaultRef.provider)) {
486
- trace.ordered_candidates.push(candidateDecision(defaultRef, 'harness-default', 'skipped', {
487
- reasonCode: MODEL_SELECTION_REASON_CODES.HARNESS_DEFAULT_PROVIDER_UNAVAILABLE,
488
- }));
489
- } else {
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 {
507
- ok: true,
508
- ...defaultRef,
509
- source: 'harness-default',
510
- ...(peak?.mode === 'warn' ? { peak_advisory: true } : {}),
511
- ...(typeof harnessDefault.reasoningEffort === 'string' && harnessDefault.reasoningEffort
512
- ? { reasoningEffort: harnessDefault.reasoningEffort }
513
- : {}),
514
- selection_trace: trace,
515
- };
516
- }
517
- }
518
- }
519
- trace.fallback_reason = MODEL_SELECTION_REASON_CODES.NO_AVAILABLE_MODEL;
520
- return {
521
- ok: false,
522
- code: NO_WORKER_MODEL_AVAILABLE,
523
- message: `No Harness model is available for the ${tier ?? 'requested'} worker.`,
524
- selection_trace: trace,
525
- };
526
- }
527
-
528
- /**
529
- * v0.2 role-based model selection. Turns a role's model policy (from
530
- * policy.resolveModelPolicy) into an ordered selection:
531
- * attempt 0 → policy.priority (primary / cheap candidates)
532
- * attempt >= 1 → policy.escalation_priority (strong / escalation candidates)
533
- * otherwise → Harness Default fallback
534
- * The output shape matches resolveWorkerModel (provider/model/source/...) plus
535
- * role + attempt so selection provenance lands in job metadata.
536
- */
537
- export function resolveModel({
538
- role = 'worker',
539
- attempt = 0,
540
- policy,
541
- catalog,
542
- harnessDefault,
543
- adaptiveHealth,
544
- healthStore,
545
- healthGate,
546
- allowFallback = true,
547
- tombstones,
548
- schedule,
549
- at,
550
- } = {}) {
551
- const p = policy && typeof policy === 'object' ? policy : {};
552
- const escalated = Number.isInteger(attempt) && attempt > 0;
553
- const candidates = escalated ? p.escalation_priority : p.priority;
554
- const configured = escalated
555
- ? p.escalation_priority_configured === true || (Array.isArray(candidates) && candidates.length > 0)
556
- : p.priorityConfigured === true || (Array.isArray(candidates) && candidates.length > 0);
557
- // Escalation never re-picks the fresh "preferred" role default: an empty
558
- // escalation pool falls through to Harness Default instead.
559
- const preferredModelId = escalated ? undefined : DEFAULT_ROLE_MODEL_PREFERENCES[role] ?? undefined;
560
- const result = resolveWorkerModel({
561
- tier: role,
562
- priority: candidates,
563
- priorityConfigured: configured,
564
- catalog,
565
- harnessDefault,
566
- fallback: p.fallback === 'harness-default' ? 'harness-default' : 'harness-default',
567
- preferredModelId,
568
- adaptive: p.adaptive,
569
- adaptiveHealth,
570
- healthStore,
571
- healthGate,
572
- allowFallback,
573
- tombstones,
574
- schedule,
575
- at,
576
- traceContext: {
577
- role,
578
- logicalAttempt: attempt,
579
- strategy: p.strategy ?? (role === 'reviewer' ? 'strong' : 'balanced'),
580
- candidateSet: escalated ? 'escalation' : 'primary',
581
- prioritySource: escalated ? 'escalation-priority' : 'priority',
582
- },
583
- });
584
- if (!result.ok) return { ...result, role, attempt };
585
- return { ...result, role, attempt };
586
- }
1
+ // Pure Harness-backed worker model selection. A worker tier describes a role,
2
+ // not a fixed model: explicit provider/model priorities win, fresh configs may
3
+ // use a tier-specific preferred model id, and Harness Default is the final
4
+ // fallback. Catalog membership is advisory; provider registration is the
5
+ // routing boundary.
6
+
7
+ import { rankAdaptiveCandidates } from './adaptive-routing.mjs';
8
+ import { scheduleAdmission } from './model-schedule.mjs';
9
+
10
+ export const DEFAULT_TIER_MODEL_PREFERENCES = Object.freeze({
11
+ flash: 'deepseek-v4-flash',
12
+ pro: 'deepseek-v4-pro',
13
+ });
14
+
15
+ // v0.2 role → default preferred model class. A role describes who does the
16
+ // work; the model class is only the fresh-config recommendation until a
17
+ // priority list or Harness Default takes over.
18
+ export const DEFAULT_ROLE_MODEL_PREFERENCES = Object.freeze({
19
+ worker: 'deepseek-v4-flash',
20
+ reviewer: 'deepseek-v4-pro',
21
+ });
22
+
23
+ export const MODEL_FALLBACKS = ['harness-default'];
24
+ export const NO_WORKER_MODEL_AVAILABLE = 'NO_WORKER_MODEL_AVAILABLE';
25
+ export const MODEL_SELECTION_TRACE_VERSION = 1;
26
+ export const MODEL_SELECTION_REASON_CODES = Object.freeze({
27
+ PROVIDER_UNAVAILABLE: 'PROVIDER_UNAVAILABLE',
28
+ PREFERRED_MODEL_UNAVAILABLE: 'PREFERRED_MODEL_UNAVAILABLE',
29
+ PREFERRED_MODEL_AMBIGUOUS: 'PREFERRED_MODEL_AMBIGUOUS',
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',
40
+ HARNESS_DEFAULT_PROVIDER_UNAVAILABLE: 'HARNESS_DEFAULT_PROVIDER_UNAVAILABLE',
41
+ PRIMARY_CANDIDATES_EXHAUSTED: 'PRIMARY_CANDIDATES_EXHAUSTED',
42
+ ESCALATION_CANDIDATES_EXHAUSTED: 'ESCALATION_CANDIDATES_EXHAUSTED',
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
+ });
63
+
64
+ export function normalizeModelRef(raw) {
65
+ if (!raw || typeof raw !== 'object') return null;
66
+ const provider = typeof raw.provider === 'string' ? raw.provider.trim() : '';
67
+ const model = typeof raw.model === 'string' ? raw.model.trim() : '';
68
+ return provider && model ? { provider, model } : null;
69
+ }
70
+
71
+ export function modelRefKey(raw) {
72
+ const ref = normalizeModelRef(raw);
73
+ return ref ? `${ref.provider}\0${ref.model}` : '';
74
+ }
75
+
76
+ export function normalizeModelPriority(raw) {
77
+ if (!Array.isArray(raw)) return [];
78
+ const seen = new Set();
79
+ const result = [];
80
+ for (const value of raw) {
81
+ const ref = normalizeModelRef(value);
82
+ if (!ref) continue;
83
+ const key = modelRefKey(ref);
84
+ if (seen.has(key)) continue;
85
+ seen.add(key);
86
+ result.push(ref);
87
+ }
88
+ return result;
89
+ }
90
+
91
+ function providerMap(catalog) {
92
+ const map = new Map();
93
+ for (const raw of catalog?.providers ?? []) {
94
+ if (!raw || typeof raw.id !== 'string' || raw.id === '') continue;
95
+ map.set(raw.id, raw);
96
+ }
97
+ return map;
98
+ }
99
+
100
+ function normalizeAttempt(value) {
101
+ return Number.isInteger(value) && value >= 0 ? value : 0;
102
+ }
103
+
104
+ function normalizeModelClassHint(value) {
105
+ return value === 'flash' || value === 'pro' ? value : null;
106
+ }
107
+
108
+ function traceBase({
109
+ role = 'worker',
110
+ logicalAttempt = 0,
111
+ modelClassHint = null,
112
+ strategy = 'legacy-tier',
113
+ candidateSet = 'primary',
114
+ escalationReason = null,
115
+ } = {}) {
116
+ return {
117
+ version: MODEL_SELECTION_TRACE_VERSION,
118
+ role: role === 'reviewer' ? 'reviewer' : 'worker',
119
+ logical_attempt: normalizeAttempt(logicalAttempt),
120
+ model_class_hint: normalizeModelClassHint(modelClassHint),
121
+ strategy: typeof strategy === 'string' && strategy ? strategy : 'legacy-tier',
122
+ candidate_set: typeof candidateSet === 'string' && candidateSet ? candidateSet : 'primary',
123
+ ordered_candidates: [],
124
+ selected: null,
125
+ selection_source: null,
126
+ fallback_reason: null,
127
+ escalation_reason: typeof escalationReason === 'string' && escalationReason ? escalationReason : null,
128
+ };
129
+ }
130
+
131
+ function candidateDecision(ref, source, status, { reasonCode, advertised } = {}) {
132
+ return {
133
+ provider: ref?.provider ?? null,
134
+ model: ref?.model ?? null,
135
+ source,
136
+ status,
137
+ ...(reasonCode ? { reason_code: reasonCode } : {}),
138
+ ...(advertised === false ? { advertised: false } : {}),
139
+ };
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
+ }
161
+
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 }));
174
+ trace.selected = { provider: ref.provider, model: ref.model, source };
175
+ trace.selection_source = source;
176
+ trace.fallback_reason = fallbackReason ?? null;
177
+ return trace;
178
+ }
179
+
180
+ function rankForTrace(trace, candidates, {
181
+ adaptive,
182
+ adaptiveHealth,
183
+ explicitPriority = false,
184
+ } = {}) {
185
+ const ranked = rankAdaptiveCandidates(candidates, {
186
+ config: adaptive,
187
+ healthStore: adaptiveHealth,
188
+ role: trace.role,
189
+ explicitPriority,
190
+ });
191
+ if (ranked.trace.enabled) trace.adaptive = ranked.trace;
192
+ return ranked;
193
+ }
194
+
195
+ /**
196
+ * Build a one-candidate trace for transports that intentionally bypass the
197
+ * Harness catalog (DeepSeek Official strict mode / standalone legacy mode).
198
+ */
199
+ export function buildDirectSelectionTrace({
200
+ role = 'worker',
201
+ logicalAttempt = 0,
202
+ modelClassHint = null,
203
+ strategy = 'legacy-strict',
204
+ candidateSet = 'primary',
205
+ provider,
206
+ model,
207
+ source = 'legacy-strict',
208
+ escalationReason = null,
209
+ peakAdvisory = false,
210
+ } = {}) {
211
+ const trace = traceBase({ role, logicalAttempt, modelClassHint, strategy, candidateSet, escalationReason });
212
+ const ref = normalizeModelRef({ provider, model });
213
+ if (!ref) {
214
+ trace.fallback_reason = MODEL_SELECTION_REASON_CODES.NO_AVAILABLE_MODEL;
215
+ return trace;
216
+ }
217
+ return selectTrace(trace, ref, source, {
218
+ reasonCode: peakAdvisory ? MODEL_SELECTION_REASON_CODES.PEAK_ADVISORY : null,
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Add workflow-only context after a transport has resolved the model. This is
224
+ * intentionally metadata-only: it never changes provider/model selection.
225
+ */
226
+ export function enrichSelectionTrace(trace, {
227
+ role,
228
+ logicalAttempt,
229
+ modelClassHint,
230
+ escalationReason,
231
+ } = {}) {
232
+ const base = trace && typeof trace === 'object'
233
+ ? {
234
+ ...trace,
235
+ ordered_candidates: Array.isArray(trace.ordered_candidates)
236
+ ? trace.ordered_candidates.map((item) => ({ ...item }))
237
+ : [],
238
+ selected: trace.selected && typeof trace.selected === 'object' ? { ...trace.selected } : null,
239
+ ...(trace.adaptive && typeof trace.adaptive === 'object'
240
+ ? {
241
+ adaptive: {
242
+ ...trace.adaptive,
243
+ candidates: Array.isArray(trace.adaptive.candidates)
244
+ ? trace.adaptive.candidates.map((item) => ({ ...item }))
245
+ : [],
246
+ },
247
+ }
248
+ : {}),
249
+ }
250
+ : traceBase({ role, logicalAttempt, modelClassHint, escalationReason });
251
+ if (role === 'worker' || role === 'reviewer') base.role = role;
252
+ if (Number.isInteger(logicalAttempt) && logicalAttempt >= 0) base.logical_attempt = logicalAttempt;
253
+ if (modelClassHint === 'flash' || modelClassHint === 'pro' || modelClassHint === null) {
254
+ base.model_class_hint = modelClassHint;
255
+ }
256
+ if (typeof escalationReason === 'string' && escalationReason) base.escalation_reason = escalationReason;
257
+ else if (logicalAttempt === 0) base.escalation_reason = null;
258
+ return base;
259
+ }
260
+
261
+ export function resolveWorkerModel({
262
+ tier,
263
+ priority,
264
+ priorityConfigured = false,
265
+ catalog,
266
+ harnessDefault,
267
+ fallback = 'harness-default',
268
+ preferredModelId = DEFAULT_TIER_MODEL_PREFERENCES[tier],
269
+ traceContext = {},
270
+ adaptive,
271
+ adaptiveHealth,
272
+ healthStore,
273
+ healthGate,
274
+ allowFallback = true,
275
+ tombstones,
276
+ schedule,
277
+ at,
278
+ } = {}) {
279
+ const providers = providerMap(catalog);
280
+ const normalizedPriority = normalizeModelPriority(priority);
281
+ const prioritySource = typeof traceContext.prioritySource === 'string' && traceContext.prioritySource
282
+ ? traceContext.prioritySource
283
+ : 'priority';
284
+ const trace = traceBase({
285
+ role: traceContext.role ?? 'worker',
286
+ logicalAttempt: traceContext.logicalAttempt ?? 0,
287
+ modelClassHint: traceContext.modelClassHint ?? null,
288
+ strategy: traceContext.strategy ?? 'legacy-tier',
289
+ candidateSet: traceContext.candidateSet ?? 'primary',
290
+ escalationReason: traceContext.escalationReason ?? null,
291
+ });
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
+
299
+ // Explicit priority — including an intentionally empty configured list — is
300
+ // authoritative. Adaptive mode records that bypass but never reorders it.
301
+ if (priorityConfigured || normalizedPriority.length > 0) {
302
+ rankForTrace(trace, normalizedPriority, { adaptive, adaptiveHealth, explicitPriority: true });
303
+ }
304
+
305
+ for (let index = 0; index < normalizedPriority.length; index++) {
306
+ const ref = normalizedPriority[index];
307
+ const provider = providers.get(ref.provider);
308
+ if (!provider) {
309
+ trace.ordered_candidates.push(candidateDecision(ref, prioritySource, 'skipped', {
310
+ reasonCode: MODEL_SELECTION_REASON_CODES.PROVIDER_UNAVAILABLE,
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
+ });
337
+ return {
338
+ ok: true,
339
+ ...ref,
340
+ source: 'priority',
341
+ matchedPriorityIndex: index,
342
+ ...(advertised ? {} : { advertised: false }),
343
+ ...(peak?.mode === 'warn' ? { peak_advisory: true } : {}),
344
+ selection_trace: trace,
345
+ };
346
+ }
347
+
348
+ // A manually managed list, including an intentionally empty list, replaces
349
+ // the fresh-config recommendation rather than silently re-inserting it.
350
+ if (!priorityConfigured && normalizedPriority.length === 0 && typeof preferredModelId === 'string') {
351
+ const matches = [];
352
+ for (const provider of providers.values()) {
353
+ if ((provider.models ?? []).some((model) => model?.id === preferredModelId)) {
354
+ matches.push({ provider: provider.id, model: preferredModelId });
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) {
429
+ if (candidate.provider === match.provider && candidate.model === match.model) continue;
430
+ trace.ordered_candidates.push(candidateDecision(candidate, 'preferred-default', 'skipped', {
431
+ reasonCode: adaptiveChoice
432
+ ? MODEL_SELECTION_REASON_CODES.ADAPTIVE_DEPRIORITIZED
433
+ : MODEL_SELECTION_REASON_CODES.PREFERRED_MODEL_AMBIGUOUS,
434
+ }));
435
+ }
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
+ };
447
+ }
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) {
451
+ trace.ordered_candidates.push(candidateDecision(candidate, 'preferred-default', 'skipped', {
452
+ reasonCode: MODEL_SELECTION_REASON_CODES.PREFERRED_MODEL_AMBIGUOUS,
453
+ }));
454
+ }
455
+ } else {
456
+ rankForTrace(trace, [], { adaptive, adaptiveHealth });
457
+ trace.ordered_candidates.push(candidateDecision(
458
+ { provider: null, model: preferredModelId },
459
+ 'preferred-default',
460
+ 'skipped',
461
+ { reasonCode: MODEL_SELECTION_REASON_CODES.PREFERRED_MODEL_UNAVAILABLE },
462
+ ));
463
+ }
464
+ }
465
+
466
+ if (adaptive?.enabled === true && trace.adaptive === undefined) {
467
+ rankForTrace(trace, [], { adaptive, adaptiveHealth });
468
+ }
469
+
470
+ if (fallback === 'harness-default') {
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;
479
+ if (!defaultRef) {
480
+ trace.ordered_candidates.push(candidateDecision(null, 'harness-default', 'skipped', {
481
+ reasonCode: MODEL_SELECTION_REASON_CODES.HARNESS_DEFAULT_INVALID,
482
+ }));
483
+ } else if (alreadyJudged) {
484
+ // Keep its existing verdict; nothing further to record.
485
+ } else if (!providers.has(defaultRef.provider)) {
486
+ trace.ordered_candidates.push(candidateDecision(defaultRef, 'harness-default', 'skipped', {
487
+ reasonCode: MODEL_SELECTION_REASON_CODES.HARNESS_DEFAULT_PROVIDER_UNAVAILABLE,
488
+ }));
489
+ } else {
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 {
507
+ ok: true,
508
+ ...defaultRef,
509
+ source: 'harness-default',
510
+ ...(peak?.mode === 'warn' ? { peak_advisory: true } : {}),
511
+ ...(typeof harnessDefault.reasoningEffort === 'string' && harnessDefault.reasoningEffort
512
+ ? { reasoningEffort: harnessDefault.reasoningEffort }
513
+ : {}),
514
+ selection_trace: trace,
515
+ };
516
+ }
517
+ }
518
+ }
519
+ trace.fallback_reason = MODEL_SELECTION_REASON_CODES.NO_AVAILABLE_MODEL;
520
+ return {
521
+ ok: false,
522
+ code: NO_WORKER_MODEL_AVAILABLE,
523
+ message: `No Harness model is available for the ${tier ?? 'requested'} worker.`,
524
+ selection_trace: trace,
525
+ };
526
+ }
527
+
528
+ /**
529
+ * v0.2 role-based model selection. Turns a role's model policy (from
530
+ * policy.resolveModelPolicy) into an ordered selection:
531
+ * attempt 0 → policy.priority (primary / cheap candidates)
532
+ * attempt >= 1 → policy.escalation_priority (strong / escalation candidates)
533
+ * otherwise → Harness Default fallback
534
+ * The output shape matches resolveWorkerModel (provider/model/source/...) plus
535
+ * role + attempt so selection provenance lands in job metadata.
536
+ */
537
+ export function resolveModel({
538
+ role = 'worker',
539
+ attempt = 0,
540
+ policy,
541
+ catalog,
542
+ harnessDefault,
543
+ adaptiveHealth,
544
+ healthStore,
545
+ healthGate,
546
+ allowFallback = true,
547
+ tombstones,
548
+ schedule,
549
+ at,
550
+ } = {}) {
551
+ const p = policy && typeof policy === 'object' ? policy : {};
552
+ const escalated = Number.isInteger(attempt) && attempt > 0;
553
+ const candidates = escalated ? p.escalation_priority : p.priority;
554
+ const configured = escalated
555
+ ? p.escalation_priority_configured === true || (Array.isArray(candidates) && candidates.length > 0)
556
+ : p.priorityConfigured === true || (Array.isArray(candidates) && candidates.length > 0);
557
+ // Escalation never re-picks the fresh "preferred" role default: an empty
558
+ // escalation pool falls through to Harness Default instead.
559
+ const preferredModelId = escalated ? undefined : DEFAULT_ROLE_MODEL_PREFERENCES[role] ?? undefined;
560
+ const result = resolveWorkerModel({
561
+ tier: role,
562
+ priority: candidates,
563
+ priorityConfigured: configured,
564
+ catalog,
565
+ harnessDefault,
566
+ fallback: p.fallback === 'harness-default' ? 'harness-default' : 'harness-default',
567
+ preferredModelId,
568
+ adaptive: p.adaptive,
569
+ adaptiveHealth,
570
+ healthStore,
571
+ healthGate,
572
+ allowFallback,
573
+ tombstones,
574
+ schedule,
575
+ at,
576
+ traceContext: {
577
+ role,
578
+ logicalAttempt: attempt,
579
+ strategy: p.strategy ?? (role === 'reviewer' ? 'strong' : 'balanced'),
580
+ candidateSet: escalated ? 'escalation' : 'primary',
581
+ prioritySource: escalated ? 'escalation-priority' : 'priority',
582
+ },
583
+ });
584
+ if (!result.ok) return { ...result, role, attempt };
585
+ return { ...result, role, attempt };
586
+ }