@ran-sh/dsh-crew 1.4.0 → 1.5.1
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 +5926 -5451
- package/package.json +1 -1
- package/src/client/index.tsx +159 -7
- package/src/hub/entry.mjs +6 -0
- package/src/hub/index.mjs +50 -0
- package/src/install/install-legacy.mjs +739 -717
- package/src/install/install.mjs +13 -5
- package/src/install/npx-lifecycle.mjs +16 -2
- package/src/jobs.mjs +17 -0
- package/src/model-routing.mjs +250 -147
- package/src/model-schedule.mjs +207 -0
- package/src/multimodal.mjs +0 -0
- package/src/policy.mjs +69 -62
- package/src/release-in-use.mjs +110 -0
- package/src/runtime-identity.mjs +1 -1
- package/src/server.mjs +6 -1
package/package.json
CHANGED
package/src/client/index.tsx
CHANGED
|
@@ -18,6 +18,11 @@ import { CREW_UI_SURFACES, classifyCrewSurface, surfaceResponsibilities } from '
|
|
|
18
18
|
import { aggregateModelInvocations } from './task-telemetry.mjs';
|
|
19
19
|
import { PanelHeader, PanelStyles } from './panel-chrome';
|
|
20
20
|
import { HistoryPanel } from './history-panel';
|
|
21
|
+
// The schedule contract is shared, not mirrored: model-schedule.mjs imports
|
|
22
|
+
// nothing, so the panel can use the same normalization and peak test the router
|
|
23
|
+
// does. A hand-maintained copy could accept less than the server and then write
|
|
24
|
+
// the difference back on save.
|
|
25
|
+
import { normalizeModelSchedule, isPeakAt } from '../model-schedule.mjs';
|
|
21
26
|
|
|
22
27
|
export const inject = ['slots', 'locale'];
|
|
23
28
|
|
|
@@ -42,6 +47,7 @@ function normalizeAdaptive(value: any): AdaptiveConfig {
|
|
|
42
47
|
};
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
|
|
45
51
|
const COPY = {
|
|
46
52
|
zh: {
|
|
47
53
|
label: 'DSH Crew',
|
|
@@ -57,7 +63,7 @@ const COPY = {
|
|
|
57
63
|
expandAll: '全部展开', collapseAll: '全部折叠',
|
|
58
64
|
modelCount: (count: number) => `${count} 个模型`, providerCount: (count: number) => `${count} 个 Provider`,
|
|
59
65
|
jobCount: (count: number) => `${count} 个任务`, runningCount: (count: number) => `${count} 个运行中`,
|
|
60
|
-
sectionNames: { integrations: 'Codex / Claude / ZCode 集成状态', workflow: 'Crew 工作流设置', flash: 'Worker / Flash', pro: 'Reviewer / Pro', dispatch: '模型优先级与派发', adaptive: '自适应路由', runtime: '运行 / 生效边界', multimodal: '视觉与生图', harnessProviders: 'Harness Providers', providers: '多模态适配器', jobs: '任务状态' },
|
|
66
|
+
sectionNames: { integrations: 'Codex / Claude / ZCode 集成状态', workflow: 'Crew 工作流设置', flash: 'Worker / Flash', pro: 'Reviewer / Pro', dispatch: '模型优先级与派发', schedule: '波峰波谷调度', adaptive: '自适应路由', runtime: '运行 / 生效边界', multimodal: '视觉与生图', harnessProviders: 'Harness Providers', providers: '多模态适配器', jobs: '任务状态' },
|
|
61
67
|
harnessHint: 'DeepSeek 官方 Harness 的 dsh-crew profile:Provider、Harness Models 与运行时配置',
|
|
62
68
|
hostReadiness: '宿主集成就绪度', hostReadinessHint: '只使用结构化安装与运行时证据;缺少证据不会显示 READY。',
|
|
63
69
|
readinessLabels: { codex_mcp: 'Codex MCP', ds_worker: 'ds-worker', ds_reviewer: 'ds-reviewer', claude_plugin: 'Claude plugin', zcode_mcp: 'ZCode MCP', crew_harness: 'Crew plugin profile', official_bridge: 'Official bridge' },
|
|
@@ -163,8 +169,14 @@ const COPY = {
|
|
|
163
169
|
save: '保存', saved: '已保存', jobs: 'Worker 任务', empty: '当前没有 Worker / Reviewer 任务。',
|
|
164
170
|
adaptiveTitle: '自适应模型路由(实验)', adaptiveHint: '默认关闭;只重排系统自动候选,手动模型优先级始终保持原序。健康信号仅来自本进程已观察到的成功、失败、超时与粗粒度延迟,重启后清空。',
|
|
165
171
|
adaptiveEnabled: '启用自适应路由', adaptiveWindow: '健康窗口', adaptiveSamples: '最少样本', adaptiveBoundary: '下一个工作流生效',
|
|
166
|
-
|
|
167
|
-
|
|
172
|
+
scheduleTitle: '波峰波谷调度', scheduleHint: '按本地时间限制指定模型:波峰时被“阻止”的模型会被跳过,由下一个候选接替任务;“提醒”的仍会使用并在选择轨迹中标记。未列出的模型不受限制。默认时段为 DeepSeek 公布的波峰时间(工作日 09:00-12:00、14:00-18:00)。',
|
|
173
|
+
scheduleTimezone: '时区(相对 UTC)', scheduleWindows: '波峰时段', schedulePeak: '现在处于波峰', scheduleOffPeak: '现在处于波谷',
|
|
174
|
+
scheduleAddWindow: '添加时段', scheduleStart: '开始', scheduleEnd: '结束',
|
|
175
|
+
scheduleBlock: '阻止', scheduleWarn: '提醒', scheduleOff: '关闭',
|
|
176
|
+
scheduleRestricted: '受限模型', scheduleNoRestricted: '还没有受限模型;未列出的模型不受时间限制。',
|
|
177
|
+
scheduleWeekdays: '生效工作日', scheduleBoundary: '下次派发生效',
|
|
178
|
+
modelActivity: '模型活动概览', modelActivityHint: '聚合本 Hub 内存中最近 500 个含真实调用证据的任务;每个模型按任务计数(不是推测底层轮次),最多显示 50 个模型。不保存提示词、结果或凭据。', noModelActivity: '还没有真实模型活动。',
|
|
179
|
+
calls: '任务数', invocationSource: '任务来源', routingSource: '路由来源', lastCalled: '最近调用', role: '角色', model: '模型', never: '—',
|
|
168
180
|
col: { id: '任务', role: '角色', source: '来源', model: '模型', tier: '档位', status: '状态', progress: '进度', tokens: 'tokens ⇅', task: '内容' },
|
|
169
181
|
working: '处理中…',
|
|
170
182
|
tips: {
|
|
@@ -199,7 +211,7 @@ const COPY = {
|
|
|
199
211
|
expandAll: 'Expand all', collapseAll: 'Collapse all',
|
|
200
212
|
modelCount: (count: number) => `${count} models`, providerCount: (count: number) => `${count} providers`,
|
|
201
213
|
jobCount: (count: number) => `${count} jobs`, runningCount: (count: number) => `${count} running`,
|
|
202
|
-
sectionNames: { integrations: 'Codex / Claude / ZCode integration status', workflow: 'Crew workflow settings', flash: 'Worker / Flash', pro: 'Reviewer / Pro', dispatch: 'Model priority & dispatch', adaptive: 'Adaptive routing', runtime: 'Runtime / activation boundaries', multimodal: 'Vision & image generation', harnessProviders: 'Harness Providers', providers: 'Multimodal adapters', jobs: 'Task status' },
|
|
214
|
+
sectionNames: { integrations: 'Codex / Claude / ZCode integration status', workflow: 'Crew workflow settings', flash: 'Worker / Flash', pro: 'Reviewer / Pro', dispatch: 'Model priority & dispatch', schedule: 'Peak / off-peak scheduling', adaptive: 'Adaptive routing', runtime: 'Runtime / activation boundaries', multimodal: 'Vision & image generation', harnessProviders: 'Harness Providers', providers: 'Multimodal adapters', jobs: 'Task status' },
|
|
203
215
|
harnessHint: 'The official DeepSeek Harness dsh-crew profile: providers, Harness Models, and runtime configuration',
|
|
204
216
|
hostReadiness: 'Host integration readiness', hostReadinessHint: 'Uses structured installer and runtime evidence only; missing evidence is never READY.',
|
|
205
217
|
readinessLabels: { codex_mcp: 'Codex MCP', ds_worker: 'ds-worker', ds_reviewer: 'ds-reviewer', claude_plugin: 'Claude plugin', zcode_mcp: 'ZCode MCP', crew_harness: 'Crew plugin profile', official_bridge: 'Official bridge' },
|
|
@@ -305,8 +317,14 @@ const COPY = {
|
|
|
305
317
|
save: 'Save', saved: 'Saved', jobs: 'Worker jobs', empty: 'No Worker / Reviewer jobs yet.',
|
|
306
318
|
adaptiveTitle: 'Adaptive model routing (experimental)', adaptiveHint: 'Off by default. Only system-derived candidates may be reordered; explicit model priorities always keep their order. Health uses only process-local success, failure, timeout, and coarse latency observations and resets on restart.',
|
|
307
319
|
adaptiveEnabled: 'Enable adaptive routing', adaptiveWindow: 'Health window', adaptiveSamples: 'Minimum samples', adaptiveBoundary: 'Effective for the next workflow',
|
|
308
|
-
|
|
309
|
-
|
|
320
|
+
scheduleTitle: 'Peak / off-peak scheduling', scheduleHint: 'Restricts chosen models by local wall clock. During peak, a "block" model is skipped and the next candidate serves the job; a "warn" model is still used and flagged in the selection trace. Models not listed are unrestricted. Defaults are DeepSeek\'s published peak hours (weekdays 09:00-12:00 and 14:00-18:00).',
|
|
321
|
+
scheduleTimezone: 'Timezone (relative to UTC)', scheduleWindows: 'Peak windows', schedulePeak: 'Peak hours now', scheduleOffPeak: 'Off-peak now',
|
|
322
|
+
scheduleAddWindow: 'Add window', scheduleStart: 'Start', scheduleEnd: 'End',
|
|
323
|
+
scheduleBlock: 'Block', scheduleWarn: 'Warn', scheduleOff: 'Off',
|
|
324
|
+
scheduleRestricted: 'Restricted models', scheduleNoRestricted: 'No restricted models yet; unlisted models have no time limit.',
|
|
325
|
+
scheduleWeekdays: 'Days the windows apply', scheduleBoundary: 'Effective for the next dispatch',
|
|
326
|
+
modelActivity: 'Model activity overview', modelActivityHint: 'Aggregates the latest 500 in-memory Hub jobs with real model-activity evidence; each model is counted once per qualifying job (not an inferred turn count). Shows at most 50 models; prompts, results, and credentials are never stored here.', noModelActivity: 'No real model activity yet.',
|
|
327
|
+
calls: 'Jobs', invocationSource: 'Task source', routingSource: 'Routing source', lastCalled: 'Last called', role: 'Role', model: 'Model', never: '—',
|
|
310
328
|
col: { id: 'job', role: 'role', source: 'source', model: 'model', tier: 'tier', status: 'status', progress: 'progress', tokens: 'tokens ⇅', task: 'task' },
|
|
311
329
|
working: 'Working…',
|
|
312
330
|
tips: {
|
|
@@ -1121,6 +1139,59 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
1121
1139
|
const { next, ordering } = setAdaptiveLocal(candidate);
|
|
1122
1140
|
void applyPatch({ worker: { model_policy: { adaptive: next, ordering } } });
|
|
1123
1141
|
};
|
|
1142
|
+
// ---- per-model peak/off-peak scheduling ----
|
|
1143
|
+
// The schedule replaces as a whole (windows and model modes only mean anything
|
|
1144
|
+
// together), so every edit saves the complete object.
|
|
1145
|
+
const schedule = normalizeModelSchedule(config?.model_schedule);
|
|
1146
|
+
const saveSchedule = (candidate: any) => {
|
|
1147
|
+
const next = normalizeModelSchedule(candidate);
|
|
1148
|
+
setConfig((current: any) => ({ ...current, model_schedule: next }));
|
|
1149
|
+
void applyPatch({ model_schedule: next });
|
|
1150
|
+
};
|
|
1151
|
+
const schedulePeakNow = isPeakAt(schedule);
|
|
1152
|
+
const scheduleModeOf = (ref: any) => schedule.models.find(
|
|
1153
|
+
(entry: any) => entry.provider === ref.provider && entry.model === ref.model,
|
|
1154
|
+
)?.mode ?? 'off';
|
|
1155
|
+
const setScheduleMode = (ref: any, mode: string) => {
|
|
1156
|
+
const others = schedule.models.filter(
|
|
1157
|
+
(entry: any) => !(entry.provider === ref.provider && entry.model === ref.model),
|
|
1158
|
+
);
|
|
1159
|
+
saveSchedule({ ...schedule, models: mode === 'off' ? others : [...others, { ...ref, mode }] });
|
|
1160
|
+
};
|
|
1161
|
+
// Every model the router can actually select, not just what the flat tier
|
|
1162
|
+
// mirrors happen to expose. The Pro mirror collapses worker-escalation and
|
|
1163
|
+
// reviewer priority into one list — it shows whichever is set and hides the
|
|
1164
|
+
// other — so a reviewer-only model would be routable yet impossible to
|
|
1165
|
+
// restrict. Harness Default and the catalog are included for the same reason:
|
|
1166
|
+
// routing can land on a model that is in no priority list at all.
|
|
1167
|
+
const scheduleModelRefs = (() => {
|
|
1168
|
+
const seen = new Map<string, any>();
|
|
1169
|
+
const add = (ref: any) => {
|
|
1170
|
+
if (ref?.provider && ref?.model) seen.set(`${ref.provider}\0${ref.model}`, { provider: ref.provider, model: ref.model });
|
|
1171
|
+
};
|
|
1172
|
+
for (const ref of config?.flash_model_priority ?? []) add(ref);
|
|
1173
|
+
for (const ref of config?.pro_model_priority ?? []) add(ref);
|
|
1174
|
+
for (const role of ['worker', 'review'] as const) {
|
|
1175
|
+
const policy = config?.[role]?.model_policy;
|
|
1176
|
+
for (const ref of policy?.priority ?? []) add(ref);
|
|
1177
|
+
for (const ref of policy?.escalation_priority ?? []) add(ref);
|
|
1178
|
+
}
|
|
1179
|
+
add(modelCatalog?.harness_default);
|
|
1180
|
+
for (const provider of modelCatalog?.providers ?? []) {
|
|
1181
|
+
for (const model of provider?.models ?? []) add({ provider: provider.id, model: model?.id });
|
|
1182
|
+
}
|
|
1183
|
+
for (const entry of schedule.models) add(entry);
|
|
1184
|
+
return [...seen.values()];
|
|
1185
|
+
})();
|
|
1186
|
+
const scheduleWeekdayNames = locale === 'zh'
|
|
1187
|
+
? ['日', '一', '二', '三', '四', '五', '六']
|
|
1188
|
+
: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
1189
|
+
const toggleScheduleWeekday = (day: number) => {
|
|
1190
|
+
const days = schedule.weekdays.includes(day)
|
|
1191
|
+
? schedule.weekdays.filter((value: number) => value !== day)
|
|
1192
|
+
: [...schedule.weekdays, day].sort((a: number, b: number) => a - b);
|
|
1193
|
+
saveSchedule({ ...schedule, weekdays: days });
|
|
1194
|
+
};
|
|
1124
1195
|
const modelActivity = aggregateModelInvocations(jobs);
|
|
1125
1196
|
const modelState = modelCallabilityState(readinessSnapshot?.model_callability, runtimeInfo);
|
|
1126
1197
|
const currentSurfaceResponsibilities = surfaceResponsibilities(surface);
|
|
@@ -1466,6 +1537,87 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
1466
1537
|
</>))}
|
|
1467
1538
|
</CollapsibleSection>
|
|
1468
1539
|
|
|
1540
|
+
<CollapsibleSection sectionId="schedule" title={copy.sectionNames.schedule}
|
|
1541
|
+
summary={sectionSummary(schedulePeakNow ? copy.schedulePeak : copy.scheduleOffPeak,
|
|
1542
|
+
`UTC${schedule.timezone_offset_minutes >= 0 ? '+' : '-'}${Math.abs(schedule.timezone_offset_minutes) / 60}`,
|
|
1543
|
+
config?.model_schedule === undefined ? copy.scheduleOff : `${schedule.models.length} · ${copy.scheduleRestricted}`)}
|
|
1544
|
+
expanded={!!expandedSections.schedule} onToggle={() => toggleSection('schedule')}>
|
|
1545
|
+
{block({ t: copy.scheduleTitle, d: copy.scheduleHint }, (<>
|
|
1546
|
+
<label style={S.field}><span style={S.fieldLabel}>{copy.scheduleTimezone}</span>
|
|
1547
|
+
<CustomSelect value={String(schedule.timezone_offset_minutes)}
|
|
1548
|
+
onChange={(v) => saveSchedule({ ...schedule, timezone_offset_minutes: Number(v) })}
|
|
1549
|
+
options={(() => {
|
|
1550
|
+
// Real-world fixed offsets, not just whole hours: a stored
|
|
1551
|
+
// minute-level offset with no matching option would render as
|
|
1552
|
+
// a different value than it holds.
|
|
1553
|
+
const offsets = new Set<number>();
|
|
1554
|
+
for (let h = -14; h <= 14; h++) offsets.add(h * 60);
|
|
1555
|
+
for (const minutes of [330, 345, 570, -210, -570]) offsets.add(minutes);
|
|
1556
|
+
offsets.add(schedule.timezone_offset_minutes);
|
|
1557
|
+
return [...offsets].sort((a, b) => a - b).map((minutes) => ({
|
|
1558
|
+
value: String(minutes),
|
|
1559
|
+
label: `UTC${minutes >= 0 ? '+' : '-'}${Math.floor(Math.abs(minutes) / 60)}`
|
|
1560
|
+
+ (Math.abs(minutes) % 60 ? `:${String(Math.abs(minutes) % 60).padStart(2, '0')}` : ''),
|
|
1561
|
+
}));
|
|
1562
|
+
})()} /></label>
|
|
1563
|
+
<div style={{ gridColumn: '1 / -1' }}>
|
|
1564
|
+
<span style={{ ...S.fieldLabel, display: 'block', marginBottom: 5 }}>{copy.scheduleWeekdays}</span>
|
|
1565
|
+
<div style={{ display: 'flex', gap: 5, flexWrap: 'wrap' }}>
|
|
1566
|
+
{scheduleWeekdayNames.map((name: string, day: number) => (
|
|
1567
|
+
<button key={day} type="button"
|
|
1568
|
+
style={{ ...S.btn, padding: '3px 9px', opacity: schedule.weekdays.includes(day) ? 1 : 0.45 }}
|
|
1569
|
+
onClick={() => toggleScheduleWeekday(day)}>{name}</button>
|
|
1570
|
+
))}
|
|
1571
|
+
</div>
|
|
1572
|
+
</div>
|
|
1573
|
+
<div style={{ gridColumn: '1 / -1' }}>
|
|
1574
|
+
<span style={{ ...S.fieldLabel, display: 'block', marginBottom: 5 }}>{copy.scheduleWindows}</span>
|
|
1575
|
+
<div style={{ display: 'flex', flexDirection: 'column' as const, gap: 6 }}>
|
|
1576
|
+
{schedule.peak_windows.map((window: any, index: number) => (
|
|
1577
|
+
<div key={`${window.start}-${window.end}-${index}`} style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
|
1578
|
+
<input type="time" value={window.start} style={S.input}
|
|
1579
|
+
onChange={(event) => {
|
|
1580
|
+
const next = schedule.peak_windows.map((row: any, i: number) => i === index ? { ...row, start: event.target.value } : row);
|
|
1581
|
+
saveSchedule({ ...schedule, peak_windows: next });
|
|
1582
|
+
}} />
|
|
1583
|
+
<span style={{ opacity: 0.6 }}>–</span>
|
|
1584
|
+
<input type="time" value={window.end} style={S.input}
|
|
1585
|
+
onChange={(event) => {
|
|
1586
|
+
const next = schedule.peak_windows.map((row: any, i: number) => i === index ? { ...row, end: event.target.value } : row);
|
|
1587
|
+
saveSchedule({ ...schedule, peak_windows: next });
|
|
1588
|
+
}} />
|
|
1589
|
+
<button type="button" style={{ ...S.btn, padding: '2px 8px' }}
|
|
1590
|
+
onClick={() => saveSchedule({ ...schedule, peak_windows: schedule.peak_windows.filter((_: any, i: number) => i !== index) })}>×</button>
|
|
1591
|
+
</div>
|
|
1592
|
+
))}
|
|
1593
|
+
<button type="button" style={{ ...S.btn, alignSelf: 'flex-start' }}
|
|
1594
|
+
onClick={() => saveSchedule({ ...schedule, peak_windows: [...schedule.peak_windows, { start: '09:00', end: '12:00' }] })}>
|
|
1595
|
+
{copy.scheduleAddWindow}</button>
|
|
1596
|
+
</div>
|
|
1597
|
+
</div>
|
|
1598
|
+
<div style={{ gridColumn: '1 / -1' }}>
|
|
1599
|
+
<span style={{ ...S.fieldLabel, display: 'block', marginBottom: 5 }}>{copy.scheduleRestricted}</span>
|
|
1600
|
+
{scheduleModelRefs.length === 0
|
|
1601
|
+
? <span style={{ fontSize: 11.5, opacity: 0.6 }}>{copy.scheduleNoRestricted}</span>
|
|
1602
|
+
: <div style={{ display: 'flex', flexDirection: 'column' as const, gap: 5 }}>
|
|
1603
|
+
{scheduleModelRefs.map((ref: any) => (
|
|
1604
|
+
<div key={`${ref.provider}\0${ref.model}`} style={{ display: 'flex', gap: 8, alignItems: 'center', padding: '5px 8px', border: '1px solid rgba(128,128,128,0.2)', borderRadius: 6 }}>
|
|
1605
|
+
<span style={{ flex: 1, minWidth: 0, fontSize: 11, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis' }}>{ref.provider} / {ref.model}</span>
|
|
1606
|
+
<CustomSelect value={scheduleModeOf(ref)}
|
|
1607
|
+
onChange={(v) => setScheduleMode(ref, v)}
|
|
1608
|
+
options={[
|
|
1609
|
+
{ value: 'off', label: copy.scheduleOff },
|
|
1610
|
+
{ value: 'warn', label: copy.scheduleWarn },
|
|
1611
|
+
{ value: 'block', label: copy.scheduleBlock },
|
|
1612
|
+
]} />
|
|
1613
|
+
</div>
|
|
1614
|
+
))}
|
|
1615
|
+
</div>}
|
|
1616
|
+
<span style={{ display: 'block', fontSize: 11, opacity: 0.55, marginTop: 5 }}>{copy.scheduleBoundary}</span>
|
|
1617
|
+
</div>
|
|
1618
|
+
</>))}
|
|
1619
|
+
</CollapsibleSection>
|
|
1620
|
+
|
|
1469
1621
|
<CollapsibleSection sectionId="runtime" title={copy.sectionNames.runtime}
|
|
1470
1622
|
summary={sectionSummary(config.mode, `${config.default_timeout_seconds}s`, config.hub_url)}
|
|
1471
1623
|
expanded={!!expandedSections.runtime} onToggle={() => toggleSection('runtime')}>
|
|
@@ -1838,7 +1990,7 @@ function WorkersPanel({ ctx }: { ctx: any }) {
|
|
|
1838
1990
|
<div role="row" key={`${entry.provider}/${entry.model}`} style={{ display: 'grid', gridTemplateColumns: 'minmax(190px, 1.35fr) 72px minmax(170px, 1fr) 125px', gap: 10, alignItems: 'center', padding: '7px 6px', borderTop: '1px solid rgba(128,128,128,0.16)' }}>
|
|
1839
1991
|
<span role="cell" style={{ minWidth: 0 }}><strong style={{ display: 'block', fontSize: 12 }}>{entry.model}</strong><span style={{ ...S.mono, opacity: 0.55 }}>{entry.provider}</span></span>
|
|
1840
1992
|
<span role="cell" style={{ ...S.mono, fontSize: 15, fontWeight: 650 }}>{entry.count}</span>
|
|
1841
|
-
<span role="cell" style={{ minWidth: 0, fontSize: 11.5 }}><span style={{ display: 'block' }}>{entry.task_sources.map(sourceLabel).join(' · ')}</span><span style={{ display: 'block', opacity: 0.55 }}>{entry.selection_sources.join(' · ')} · {entry.roles.length > 0 ? entry.roles.join(' / ') : copy.never}</span></span>
|
|
1993
|
+
<span role="cell" style={{ minWidth: 0, fontSize: 11.5 }}><span style={{ display: 'block' }}>{entry.task_sources.map(sourceLabel).join(' · ')}</span><span style={{ display: 'block', opacity: 0.55 }}>{entry.selection_sources.join(' · ')} · {entry.roles.length > 0 ? entry.roles.join(' / ') : copy.never}</span></span>
|
|
1842
1994
|
<span role="cell" style={{ ...S.mono, fontSize: 10.5 }}>{formatTimestamp(entry.last_called_at, locale)}</span>
|
|
1843
1995
|
</div>
|
|
1844
1996
|
))}
|
package/src/hub/entry.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { apply as applyHub, inject, name, WorkerRegistry } from './index.mjs';
|
|
11
11
|
import { getHubRuntimeIdentity } from '../runtime-identity.mjs';
|
|
12
12
|
import { getProcessAdaptiveHealthStore } from '../adaptive-routing.mjs';
|
|
13
|
+
import { claimReleaseInUse } from '../release-in-use.mjs';
|
|
13
14
|
|
|
14
15
|
const RUNTIME_PATH = '/_dsh/dsh-crew/runtime';
|
|
15
16
|
const ADAPTIVE_OBSERVER_INSTALLED = Symbol.for('@ran-sh/dsh-crew/adaptive-observer-installed');
|
|
@@ -92,6 +93,11 @@ export function installAdaptiveHealthObserver() {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
export async function apply(ctx) {
|
|
96
|
+
// Declare this release in use before anything else. The Hub loads several
|
|
97
|
+
// modules lazily with a cache-busting query, so deleting the release under a
|
|
98
|
+
// running process would break those routes with no way to recover but a
|
|
99
|
+
// restart. Retention reads these claims and leaves a live release alone.
|
|
100
|
+
claimReleaseInUse();
|
|
95
101
|
registerRuntimeEndpoint(ctx);
|
|
96
102
|
installAdaptiveHealthObserver();
|
|
97
103
|
return applyHub(ctx);
|
package/src/hub/index.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
resolveRoleTierHint,
|
|
19
19
|
} from '../policy.mjs';
|
|
20
20
|
import { buildDirectSelectionTrace, resolveWorkerModel, resolveModel } from '../model-routing.mjs';
|
|
21
|
+
import { scheduleAdmission } from '../model-schedule.mjs';
|
|
21
22
|
import { readHarnessModelCatalog } from '../model-catalog.mjs';
|
|
22
23
|
import { appendDeliveryInstructions, parseDeliveryReport, formatDeliveryMetadata } from '../delivery.mjs';
|
|
23
24
|
import { captureWorkspaceBaseline, captureWorkspaceDiff, NOT_A_GIT_REPOSITORY } from '../workspace-audit.mjs';
|
|
@@ -702,12 +703,48 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
702
703
|
const getCurrentSelection = () => this.ctx.get('agentDefaultModel')?.currentSelection?.();
|
|
703
704
|
let selection;
|
|
704
705
|
if (workerProviderMode === 'deepseek-official') {
|
|
706
|
+
// Strict mode names its model directly instead of walking a catalog, but a
|
|
707
|
+
// peak `block` must still apply: otherwise enabling the schedule would be
|
|
708
|
+
// silently ignored on every dispatch that uses this provider mode.
|
|
709
|
+
const strictRef = { provider: 'deepseek-official', model: legacyModel };
|
|
710
|
+
const strictAt = new Date();
|
|
711
|
+
const strictPeak = scheduleAdmission(cfg.model_schedule, strictRef, strictAt);
|
|
712
|
+
if (strictPeak?.mode === 'block') {
|
|
713
|
+
// Carry the same structured evidence the catalog path produces. The
|
|
714
|
+
// strict branch has no candidate list to fall through, so the trace is
|
|
715
|
+
// the operator's only record of why the dispatch died.
|
|
716
|
+
const blockedTrace = buildDirectSelectionTrace({
|
|
717
|
+
role: effRole,
|
|
718
|
+
logicalAttempt: attempt,
|
|
719
|
+
modelClassHint: effTier,
|
|
720
|
+
strategy: 'legacy-strict',
|
|
721
|
+
candidateSet: attempt > 0 ? 'escalation' : 'primary',
|
|
722
|
+
provider: strictRef.provider,
|
|
723
|
+
model: strictRef.model,
|
|
724
|
+
source: 'legacy-strict',
|
|
725
|
+
});
|
|
726
|
+
blockedTrace.ordered_candidates = [{
|
|
727
|
+
provider: strictRef.provider,
|
|
728
|
+
model: strictRef.model,
|
|
729
|
+
source: 'legacy-strict',
|
|
730
|
+
status: 'skipped',
|
|
731
|
+
reason_code: 'PEAK_RESTRICTED',
|
|
732
|
+
}];
|
|
733
|
+
blockedTrace.fallback_reason = 'PEAK_RESTRICTED';
|
|
734
|
+
throw Object.assign(new Error('MODEL_BLOCKED_PEAK'), {
|
|
735
|
+
policyCode: 'MODEL_BLOCKED_PEAK',
|
|
736
|
+
provider: strictRef.provider,
|
|
737
|
+
model: strictRef.model,
|
|
738
|
+
selection_trace: blockedTrace,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
705
741
|
selection = {
|
|
706
742
|
ok: true,
|
|
707
743
|
provider: 'deepseek-official',
|
|
708
744
|
model: legacyModel,
|
|
709
745
|
source: 'legacy-strict',
|
|
710
746
|
reasoningEffort: effort,
|
|
747
|
+
...(strictPeak?.mode === 'warn' ? { peak_advisory: true } : {}),
|
|
711
748
|
selection_trace: buildDirectSelectionTrace({
|
|
712
749
|
role: effRole,
|
|
713
750
|
logicalAttempt: attempt,
|
|
@@ -717,6 +754,7 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
717
754
|
provider: 'deepseek-official',
|
|
718
755
|
model: legacyModel,
|
|
719
756
|
source: 'legacy-strict',
|
|
757
|
+
...(strictPeak?.mode === 'warn' ? { peakAdvisory: true } : {}),
|
|
720
758
|
}),
|
|
721
759
|
};
|
|
722
760
|
} else {
|
|
@@ -748,6 +786,7 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
748
786
|
healthGate: policy.health_gate ?? healthGate,
|
|
749
787
|
allowFallback: cfg.allow_fallback !== false,
|
|
750
788
|
tombstones: lifecycleState.tombstones,
|
|
789
|
+
schedule: cfg.model_schedule,
|
|
751
790
|
});
|
|
752
791
|
} else {
|
|
753
792
|
selection = resolveWorkerModel({
|
|
@@ -761,6 +800,7 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
761
800
|
healthGate,
|
|
762
801
|
allowFallback: cfg.allow_fallback !== false,
|
|
763
802
|
tombstones: lifecycleState.tombstones,
|
|
803
|
+
schedule: cfg.model_schedule,
|
|
764
804
|
traceContext: {
|
|
765
805
|
role: effRole,
|
|
766
806
|
logicalAttempt: attempt,
|
|
@@ -817,6 +857,10 @@ export class WorkerRegistry { constructor(ctx) {
|
|
|
817
857
|
const job = {
|
|
818
858
|
id, client_job_id: client_job_id ?? null, sessionId, role: jobRole, attempt, tier: effTier, provider: selection.provider, model: selection.model,
|
|
819
859
|
selection_source: selection.source, selection_trace: selection.selection_trace ?? null,
|
|
860
|
+
// Routing already reports a peak `warn` as a top-level flag; carry it onto
|
|
861
|
+
// the job so a job consumer sees the same signal the router produced
|
|
862
|
+
// without having to re-derive it from the trace.
|
|
863
|
+
peak_advisory: selection.peak_advisory === true,
|
|
820
864
|
effort, reasoning_effort: selection.reasoningEffort,
|
|
821
865
|
task, source, cwd: executionCwd, requested_cwd: cwd,
|
|
822
866
|
isolation: isolatedWorkspace ? 'worktree' : 'shared',
|
|
@@ -1535,6 +1579,9 @@ export async function apply(ctx) {
|
|
|
1535
1579
|
healthGate: policy.health_gate ?? config.health_gate ?? config.worker?.model_policy?.health_gate,
|
|
1536
1580
|
allowFallback: config.allow_fallback !== false,
|
|
1537
1581
|
tombstones: lifecycleState.tombstones,
|
|
1582
|
+
// A projection must reflect the decision dispatch would make, or
|
|
1583
|
+
// the panel reports a model that routing would skip at peak.
|
|
1584
|
+
schedule: config.model_schedule,
|
|
1538
1585
|
});
|
|
1539
1586
|
}
|
|
1540
1587
|
}
|
|
@@ -1773,6 +1820,9 @@ export async function apply(ctx) {
|
|
|
1773
1820
|
healthGate,
|
|
1774
1821
|
allowFallback: config.allow_fallback !== false,
|
|
1775
1822
|
tombstones: lifecycleState.tombstones,
|
|
1823
|
+
// Same reason as the extension projection: report what dispatch
|
|
1824
|
+
// would actually pick.
|
|
1825
|
+
schedule: config.model_schedule,
|
|
1776
1826
|
});
|
|
1777
1827
|
selections[tier] = selected.ok
|
|
1778
1828
|
? { provider: selected.provider, model: selected.model, source: selected.source }
|