@ran-sh/dsh-crew 1.1.1 → 1.2.0-rc.5

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 (84) hide show
  1. package/.claude-plugin/marketplace.json +16 -17
  2. package/.claude-plugin/plugin.json +7 -1
  3. package/README.md +44 -15
  4. package/README.zh.md +37 -12
  5. package/agents/ds-flash.md +16 -18
  6. package/agents/ds-pro.md +16 -24
  7. package/agents/ds-reviewer.md +15 -15
  8. package/agents/ds-worker.md +16 -16
  9. package/bin/dsh-crew.mjs +16 -12
  10. package/codex/AGENTS.md +37 -85
  11. package/codex/agents/ds-flash.toml +18 -14
  12. package/codex/agents/ds-pro.toml +18 -15
  13. package/codex/agents/ds-reviewer.toml +17 -13
  14. package/codex/agents/ds-worker.toml +17 -13
  15. package/codex/prompts/dsh-config.md +17 -1
  16. package/codex/prompts/dsh-status.md +5 -1
  17. package/commands/config.md +16 -4
  18. package/commands/status.md +6 -2
  19. package/docs/installation.md +54 -9
  20. package/docs/readiness-matrix.md +14 -6
  21. package/docs/ui-surfaces.md +32 -17
  22. package/lib/client.js +769 -177
  23. package/official-web-bridge/lib/client.js +297 -209
  24. package/official-web-bridge/overlay-entry.mjs +59 -0
  25. package/package.json +6 -3
  26. package/scripts/build-client.mjs +2 -0
  27. package/scripts/setup.mjs +8 -7
  28. package/scripts/verify-crew-ui-polish.mjs +145 -0
  29. package/scripts/verify-history-ui.mjs +97 -0
  30. package/scripts/verify-npm-install.mjs +1 -1
  31. package/src/client/history-panel.tsx +108 -0
  32. package/src/client/host-readiness.mjs +15 -6
  33. package/src/client/index.tsx +91 -69
  34. package/src/client/model-callability-view.mjs +17 -0
  35. package/src/client/panel-chrome.tsx +40 -0
  36. package/src/client/quick-panel.tsx +88 -73
  37. package/src/client/readiness-envelope.mjs +34 -0
  38. package/src/client/task-telemetry.mjs +11 -4
  39. package/src/config-readiness.mjs +181 -115
  40. package/src/extension-contract.mjs +44 -26
  41. package/src/history/admission-gate.mjs +51 -0
  42. package/src/history/archive-store.mjs +261 -0
  43. package/src/history/cleanup-plan.mjs +64 -0
  44. package/src/history/http.mjs +32 -0
  45. package/src/history/operation.mjs +86 -0
  46. package/src/history/runner.mjs +51 -0
  47. package/src/history/runtime.mjs +28 -0
  48. package/src/history/service.mjs +137 -0
  49. package/src/history/state.mjs +27 -0
  50. package/src/hub/index.mjs +141 -24
  51. package/src/hub-client.mjs +17 -15
  52. package/src/image-output.mjs +71 -0
  53. package/src/install/install-legacy.mjs +126 -23
  54. package/src/install/npx-lifecycle.mjs +373 -64
  55. package/src/install/official-frontend-assets.mjs +78 -0
  56. package/src/install/payload-content.mjs +88 -0
  57. package/src/install/windows-startup.mjs +107 -19
  58. package/src/install/windows-supervisor-adapter.mjs +443 -0
  59. package/src/install/windows-supervisor-lifecycle.mjs +782 -0
  60. package/src/install/zcode.mjs +53 -14
  61. package/src/job-contracts.mjs +37 -31
  62. package/src/jobs.mjs +2 -2
  63. package/src/mcp-runtime.mjs +74 -9
  64. package/src/model-callability-contract.mjs +79 -0
  65. package/src/multimodal.mjs +0 -0
  66. package/src/official-web-bridge.mjs +0 -2
  67. package/src/provider-health.mjs +130 -124
  68. package/src/readiness-matrix.mjs +20 -15
  69. package/src/runtime-identity-contract.mjs +34 -0
  70. package/src/runtime-identity.mjs +14 -4
  71. package/src/runtime-readiness-snapshot.mjs +285 -87
  72. package/src/server.mjs +86 -76
  73. package/src/standalone-sdk.mjs +23 -0
  74. package/src/supervisor/restart-request.mjs +64 -10
  75. package/src/workflow-runtime.mjs +28 -13
  76. package/windows/start-dsh-crew.cmd +57 -57
  77. package/windows/start-dsh-crew.ps1 +327 -51
  78. package/windows/supervisor-control.ps1 +467 -0
  79. package/zcode/AGENTS.md +4 -1
  80. package/zcode/agents/ds-reviewer.md +31 -36
  81. package/zcode/agents/ds-worker.md +31 -39
  82. package/zcode/commands/dsh-config.md +17 -6
  83. package/zcode/commands/dsh-status.md +5 -4
  84. package/.mcp.json +0 -8
@@ -1,12 +1,12 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
+ import { PanelHeader, PanelStyles } from './panel-chrome';
2
3
 
3
4
  // DSH Crew QUICK CONTROLS panel for the official 3080 surface.
4
5
  //
5
6
  // Narrow by design: only the user-facing master switch, flash/pro model
6
7
  // priority lists and vision/imagegen toggles — all other operations belong
7
8
  // to the native 3210 full control plane. Talks only to the quick endpoints
8
- // (/_dsh/dsh-crew/quick-config, /quick-status, /runtime/restart-request,
9
- // /runtime/restart-status). NEVER writes to any other surface.
9
+ // (/_dsh/dsh-crew/quick-config, /quick-status). Runtime maintenance stays on 3210.
10
10
 
11
11
  const API = '/_dsh/dsh-crew';
12
12
  const CREW_CONTROL_PLANE_URL = 'http://127.0.0.1:3210/';
@@ -33,14 +33,20 @@ const RESTART_KEYS = new Set([
33
33
  const T = {
34
34
  zh: {
35
35
  title: 'DSH Crew 快捷控制',
36
- openFull: '打开完整设置 →',
37
- running: '运行中',
36
+ openFull: '打开 3210 后台 →',
37
+ running: '后台已连接',
38
+ surface: '3080 / 日常控制',
39
+ description: '常用开关与模型顺序在这里调整,更多设置进入后台。',
40
+ priorityHint: '从上到下按优先级排列;从列表移除不会删除 Provider。',
41
+ empty: '未配置优先模型,按现有后备策略选择。',
42
+ models: (n: number) => `${n} 个模型`,
43
+ moveUp: '上移', moveDown: '下移', remove: '从优先级移除',
38
44
  unavailable: 'Crew 后端不可用',
39
45
  openDiag: '打开诊断',
40
46
  crew: 'Crew',
41
47
  enabled: '启用子 Agent',
42
- flash: 'Flash 模型',
43
- pro: 'Pro 模型',
48
+ flash: 'Worker / Flash',
49
+ pro: 'Reviewer / Pro',
44
50
  addModel: '+ 添加模型',
45
51
  multimodal: '多模态',
46
52
  vision: '视觉',
@@ -55,14 +61,20 @@ const T = {
55
61
  },
56
62
  en: {
57
63
  title: 'DSH Crew Quick Controls',
58
- openFull: 'Open full settings →',
59
- running: 'Running',
64
+ openFull: 'Open Crew backend (3210) →',
65
+ running: 'Backend connected',
66
+ surface: '3080 / DAILY CONTROLS',
67
+ description: 'Manage everyday switches and model order here. Open the backend for advanced settings.',
68
+ priorityHint: 'Ordered by priority. Removing a model here does not delete its provider.',
69
+ empty: 'No priority models. The existing fallback policy applies.',
70
+ models: (n: number) => `${n} models`,
71
+ moveUp: 'Move up', moveDown: 'Move down', remove: 'Remove from priority',
60
72
  unavailable: 'Crew backend unavailable',
61
73
  openDiag: 'Open diagnostics',
62
74
  crew: 'Crew',
63
75
  enabled: 'Enable sub-agents',
64
- flash: 'Flash models',
65
- pro: 'Pro models',
76
+ flash: 'Worker / Flash',
77
+ pro: 'Reviewer / Pro',
66
78
  addModel: '+ Add model',
67
79
  multimodal: 'Multimodal',
68
80
  vision: 'Vision',
@@ -73,23 +85,39 @@ const T = {
73
85
  saved: 'Saved',
74
86
  working: 'Working…',
75
87
  providerPlaceholder: 'provider',
76
- modelPlaceholder: 'model',
88
+ modelPlaceholder: 'Model',
77
89
  },
78
90
  };
79
91
 
80
92
  function LocalStyles() {
81
93
  return (
82
94
  <style>{`
83
- .crew-quick-card { border: 1px solid rgba(128,128,128,0.24); border-radius: 12px; padding: 14px 15px; font-size: 13px; line-height: 1.55; display: flex; flex-direction: column; gap: 10px; background: linear-gradient(135deg, rgba(74,158,255,0.10), rgba(128,128,128,0.025) 56%); }
95
+ .dsh-crew-ui.crew-quick-card { display: flex; flex-direction: column; gap: 12px; }
84
96
  .crew-quick-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
85
97
  .crew-quick-title { font-size: 16px; font-weight: 680; }
86
98
  .crew-quick-section { font-weight: 650; opacity: 0.85; }
87
99
  .crew-quick-chip { border: 1px solid rgba(128,128,128,0.35); border-radius: 999px; padding: 1px 10px; font-size: 12px; }
88
- .crew-quick-btn { border: 1px solid rgba(128,128,128,0.4); border-radius: 8px; padding: 2px 10px; cursor: pointer; background: transparent; font-size: 12.5px; }
100
+ .crew-quick-card .crew-quick-btn { border: 1px solid var(--crew-line); border-radius: 7px; min-height: 30px; padding: 3px 9px; cursor: pointer; background: transparent; color: inherit; font-size: 12px; }
89
101
  .crew-quick-btn.primary { border-color: #4a9eff; color: #4a9eff; font-weight: 650; }
90
- .crew-quick-input { border: 1px solid rgba(128,128,128,0.35); border-radius: 6px; padding: 2px 8px; font-size: 12.5px; width: 130px; background: transparent; color: inherit; }
102
+ .crew-quick-card .crew-quick-input { border: 1px solid var(--crew-line); border-radius: 6px; padding: 6px 9px; font-size: 12px; min-width: 0; width: 100%; background: transparent; color: inherit; }
91
103
  .crew-quick-notice { opacity: 0.75; font-size: 12.5px; }
92
- .crew-quick-model { display: flex; gap: 6px; align-items: center; font-size: 12.5px; }
104
+ .crew-quick-card .crew-quick-master { padding: 11px 14px; border: 1px solid var(--crew-line); border-radius: 10px; justify-content: space-between; }
105
+ .crew-quick-card input[type=checkbox] { accent-color: var(--crew-accent); width: 15px; height: 15px; }
106
+ .crew-quick-card label { display: inline-flex; gap: 7px; align-items: center; }
107
+ .crew-quick-card .crew-quick-group { border: 1px solid var(--crew-line); border-radius: 10px; overflow: hidden; }
108
+ .crew-quick-card .crew-quick-group > summary { cursor: pointer; padding: 12px 14px; }
109
+ .crew-quick-card .crew-quick-count { margin-left: 10px; font-size: 11px; opacity: .65; font-weight: 400; }
110
+ .crew-quick-card .crew-quick-content { padding: 0 14px 14px; }
111
+ .crew-quick-card .crew-quick-list { padding: 0; margin: 10px 0; list-style: none; }
112
+ .crew-quick-card .crew-quick-model { display: grid; grid-template-columns: 22px minmax(0, 1fr) auto; gap: 9px; align-items: center; border-top: 1px solid var(--crew-line); padding: 10px 0; }
113
+ .crew-quick-card .crew-quick-rank { opacity: .5; font-size: 11px; font-variant-numeric: tabular-nums; }
114
+ .crew-quick-card .crew-quick-model-name { font-size: 12.5px; font-weight: 600; overflow-wrap: anywhere; }
115
+ .crew-quick-card .crew-quick-provider { font-size: 11px; opacity: .65; overflow-wrap: anywhere; }
116
+ .crew-quick-card .crew-quick-actions { display: flex; gap: 4px; }
117
+ .crew-quick-card .crew-quick-add > summary { color: var(--crew-accent); cursor: pointer; font-size: 12px; padding: 4px 0; }
118
+ .crew-quick-card .crew-quick-form { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; gap: 7px; margin-top: 9px; }
119
+ .crew-quick-card .crew-quick-media { display: grid; grid-template-columns: 100px minmax(0, 1fr); gap: 10px; align-items: center; margin-top: 10px; }
120
+ @media (max-width: 640px) { .crew-quick-card .crew-quick-form { grid-template-columns: minmax(0, 1fr); } .crew-quick-card .crew-quick-model { grid-template-columns: 18px minmax(0, 1fr); } .crew-quick-card .crew-quick-actions { grid-column: 2; justify-content: flex-end; } }
93
121
  `}</style>
94
122
  );
95
123
  }
@@ -174,108 +202,95 @@ export function QuickPanel({ ctx }: { ctx: any }) {
174
202
  void patch({ [listKey]: list } as unknown as QuickConfig);
175
203
  };
176
204
 
177
- const applyRestart = useCallback(async () => {
178
- setBusy(true); setNotice(t.working);
179
- try {
180
- const created = await readJson(await fetch(`${API}/runtime/restart-request`, {
181
- method: 'POST', headers: { 'content-type': 'application/json' },
182
- body: JSON.stringify({ confirm: true, reason: 'quick panel restart' }),
183
- }));
184
- if (!created.ok) throw new Error(created.code ?? created.error ?? 'restart failed');
185
- const requestId = created.request_id;
186
- const deadline = Date.now() + 90_000;
187
- for (;;) {
188
- await new Promise((r) => setTimeout(r, 1000));
189
- if (Date.now() > deadline) throw new Error('restart timed out');
190
- const status = await readJson(await fetch(`${API}/runtime/restart-status?id=${encodeURIComponent(requestId)}`, { cache: 'no-store' }));
191
- if (!status.ok) continue;
192
- if (status.state === 'VERIFIED') { setRestartPending(false); setNotice(t.saved); return; }
193
- if (status.state !== 'RESTART_REQUESTED') throw new Error(status.state ?? 'restart failed');
194
- }
195
- } catch (e: any) {
196
- setNotice(String(e?.message ?? e));
197
- } finally { setBusy(false); }
198
- }, [t]);
199
-
200
205
  const modelList = (listKey: 'flash_model_priority' | 'pro_model_priority', label: string) => {
201
206
  const list: ModelEntry[] = (config?.[listKey] as ModelEntry[] | undefined) ?? [];
202
207
  const draft = drafts[listKey] ?? { provider: '', model: '' };
203
208
  return (
204
- <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
205
- <div className="crew-quick-section">{label}</div>
209
+ <details className="crew-quick-group" open={listKey === 'flash_model_priority'}>
210
+ <summary><strong>{label}</strong><span className="crew-quick-count">{t.models(list.length)}</span></summary>
211
+ <div className="crew-quick-content">
212
+ <div className="crew-quick-notice">{t.priorityHint}</div>
213
+ {list.length === 0 && <p className="crew-quick-notice">{t.empty}</p>}
214
+ <ol className="crew-quick-list" aria-label={label}>
206
215
  {list.map((entry, i) => (
207
- <div key={`${entry.provider}/${entry.model}/${i}`} className="crew-quick-model">
208
- <span>{i + 1}. {entry.provider} / {entry.model}</span>
209
- <button className="crew-quick-btn" disabled={busy || i === 0} onClick={() => moveModel(listKey, i, -1)}>↑</button>
210
- <button className="crew-quick-btn" disabled={busy || i === list.length - 1} onClick={() => moveModel(listKey, i, 1)}>↓</button>
211
- <button className="crew-quick-btn" disabled={busy} onClick={() => removeModel(listKey, i)}>×</button>
212
- </div>
216
+ <li key={`${entry.provider}/${entry.model}/${i}`} className="crew-quick-model">
217
+ <span className="crew-quick-rank" aria-hidden="true">{i + 1}.</span>
218
+ <div><div className="crew-quick-model-name">{entry.model}</div><div className="crew-quick-provider">{entry.provider}</div></div>
219
+ <div className="crew-quick-actions">
220
+ <button type="button" className="crew-quick-btn" aria-label={`${t.moveUp} ${entry.model}`} title={t.moveUp} disabled={busy || i === 0} onClick={() => moveModel(listKey, i, -1)}>↑</button>
221
+ <button type="button" className="crew-quick-btn" aria-label={`${t.moveDown} ${entry.model}`} title={t.moveDown} disabled={busy || i === list.length - 1} onClick={() => moveModel(listKey, i, 1)}>↓</button>
222
+ <button type="button" className="crew-quick-btn" aria-label={`${t.remove} ${entry.model}`} title={t.remove} disabled={busy} onClick={() => removeModel(listKey, i)}>×</button>
223
+ </div>
224
+ </li>
213
225
  ))}
214
- <div className="crew-quick-row">
215
- <input className="crew-quick-input" placeholder={t.providerPlaceholder} disabled={busy}
226
+ </ol>
227
+ <details className="crew-quick-add">
228
+ <summary>{t.addModel}</summary>
229
+ <div className="crew-quick-form">
230
+ <input className="crew-quick-input" aria-label={`${label} Provider`} placeholder={t.providerPlaceholder} disabled={busy}
216
231
  value={draft.provider} onChange={(e) => setDrafts((d) => ({ ...d, [listKey]: { ...draft, provider: e.target.value } }))} />
217
- <input className="crew-quick-input" placeholder={t.modelPlaceholder} disabled={busy}
232
+ <input className="crew-quick-input" aria-label={`${label} ${locale === 'zh' ? '模型' : 'Model'}`} placeholder={t.modelPlaceholder} disabled={busy}
218
233
  value={draft.model} onChange={(e) => setDrafts((d) => ({ ...d, [listKey]: { ...draft, model: e.target.value } }))} />
219
- <button className="crew-quick-btn" disabled={busy} onClick={() => addModel(listKey)}>{t.addModel}</button>
234
+ <button type="button" className="crew-quick-btn" disabled={busy || !draft.provider.trim() || !draft.model.trim()} onClick={() => addModel(listKey)}>{t.addModel}</button>
220
235
  </div>
221
- </div>
236
+ </details>
237
+ </div>
238
+ </details>
222
239
  );
223
240
  };
224
241
 
225
242
  if (ready === false) {
226
243
  return (
227
- <div className="crew-quick-card">
228
- <LocalStyles />
244
+ <div className="dsh-crew-ui crew-quick-card">
245
+ <PanelStyles /><LocalStyles />
229
246
  <div className="crew-quick-title">{t.title}</div>
230
247
  <div className="crew-quick-notice">{t.unavailable}</div>
231
248
  <div className="crew-quick-row">
232
- <a href={FULL} target="_blank" rel="noreferrer">{t.openDiag}</a>
249
+ <a className="crew-nav-link" href={FULL} target="_blank" rel="noopener noreferrer">{t.openDiag}</a>
233
250
  </div>
234
251
  </div>
235
252
  );
236
253
  }
237
254
  if (config === null) {
238
- return (<div className="crew-quick-card"><LocalStyles /><div className="crew-quick-notice">{t.working}</div></div>);
255
+ return (<div className="dsh-crew-ui crew-quick-card"><PanelStyles /><LocalStyles /><div className="crew-quick-notice" role="status">{t.working}</div></div>);
239
256
  }
240
257
 
241
258
  return (
242
- <div className="crew-quick-card">
243
- <LocalStyles />
244
- <div className="crew-quick-row" style={{ justifyContent: 'space-between' }}>
245
- <span className="crew-quick-title">{t.title}</span>
259
+ <div className="dsh-crew-ui crew-quick-card">
260
+ <PanelStyles /><LocalStyles />
261
+ <PanelHeader title={t.title} eyebrow={t.surface} description={t.description} href={FULL} linkText={t.openFull}>
246
262
  <span className="crew-quick-chip">{t.running} · 3210</span>
247
- <a href={FULL} target="_blank" rel="noreferrer">{t.openFull}</a>
248
- </div>
249
- <div className="crew-quick-row">
263
+ </PanelHeader>
264
+ <div className="crew-quick-row crew-quick-master">
250
265
  <span className="crew-quick-section">{t.crew}</span>
251
266
  <label><input type="checkbox" disabled={busy} checked={config.subagents_enabled !== false}
252
267
  onChange={(e) => toggle('subagents_enabled', e.target.checked)} /> {t.enabled}</label>
253
268
  </div>
254
269
  {modelList('flash_model_priority', t.flash)}
255
270
  {modelList('pro_model_priority', t.pro)}
256
- <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
257
- <div className="crew-quick-section">{t.multimodal}</div>
258
- <div className="crew-quick-row">
271
+ <details className="crew-quick-group">
272
+ <summary><strong>{t.multimodal}</strong><span className="crew-quick-count">{t.vision} / {t.imagegen}</span></summary>
273
+ <div className="crew-quick-content">
274
+ <div className="crew-quick-media">
259
275
  <label><input type="checkbox" disabled={busy} checked={config.vision_enabled === true}
260
276
  onChange={(e) => toggle('vision_enabled', e.target.checked)} /> {t.vision}</label>
261
- <span>{t.provider}</span>
262
- <input className="crew-quick-input" disabled={busy} defaultValue={config.vision_provider ?? ''} key={`vision-${config.vision_provider ?? ''}`}
277
+ <input className="crew-quick-input" aria-label={`${t.vision} Provider`} placeholder={t.provider} disabled={busy} defaultValue={config.vision_provider ?? ''} key={`vision-${config.vision_provider ?? ''}`}
263
278
  onBlur={(e) => setProvider('vision_provider', e.target.value)} />
264
279
  </div>
265
- <div className="crew-quick-row">
280
+ <div className="crew-quick-media">
266
281
  <label><input type="checkbox" disabled={busy} checked={config.imagegen_enabled === true}
267
282
  onChange={(e) => toggle('imagegen_enabled', e.target.checked)} /> {t.imagegen}</label>
268
- <span>{t.provider}</span>
269
- <input className="crew-quick-input" disabled={busy} defaultValue={config.imagegen_provider ?? ''}
283
+ <input className="crew-quick-input" aria-label={`${t.imagegen} Provider`} placeholder={t.provider} disabled={busy} defaultValue={config.imagegen_provider ?? ''}
270
284
  onBlur={(e) => setProvider('imagegen_provider', e.target.value)} />
271
285
  </div>
272
- </div>
286
+ </div>
287
+ </details>
273
288
  {restartPending && (
274
289
  <div className="crew-quick-row">
275
- <button className="crew-quick-btn primary" disabled={busy} onClick={() => void applyRestart()}>{t.applyRestart}</button>
290
+ <a className="crew-quick-btn primary" href={FULL} target="_blank" rel="noopener noreferrer">{t.openFull}</a>
276
291
  </div>
277
292
  )}
278
- {notice && <div className="crew-quick-notice">{notice}</div>}
293
+ {notice && <div className="crew-quick-notice" role="status" aria-live="polite">{notice}</div>}
279
294
  </div>
280
295
  );
281
296
  }
@@ -0,0 +1,34 @@
1
+ import { isExactNativeCrewIdentity, sameCompleteRuntimeIdentity } from '../runtime-identity-contract.mjs';
2
+ import { validateModelCallabilityV2 } from '../model-callability-contract.mjs';
3
+
4
+ export function readinessExpiryDelay(expiresAt, now = Date.now()) {
5
+ return Number.isFinite(expiresAt) ? Math.max(0, expiresAt - now + 10) : null;
6
+ }
7
+
8
+ /**
9
+ * Apply one extension response to the atomic runtime/readiness envelope.
10
+ * Older responses are ignored so a restart cannot be rolled back by a slow
11
+ * request that started before the new runtime identity was observed.
12
+ */
13
+ export function acceptReadinessResponse(response, { generation, latestGeneration } = {}) {
14
+ if (generation !== latestGeneration) return { accepted: false, envelope: null };
15
+ const extensionRuntime = response?.extension?.runtime;
16
+ const snapshot = response?.extension?.readiness_snapshot;
17
+ const valid = response?.ok === true
18
+ && isExactNativeCrewIdentity(extensionRuntime)
19
+ && isExactNativeCrewIdentity(snapshot?.runtime)
20
+ && sameCompleteRuntimeIdentity(extensionRuntime, snapshot.runtime);
21
+ const expectedEnabledRoles = {
22
+ worker: response?.extension?.capabilities?.['deepseek.worker'] === true,
23
+ reviewer: response?.extension?.capabilities?.['deepseek.reviewer'] === true,
24
+ };
25
+ const projection = valid ? validateModelCallabilityV2({
26
+ projection: snapshot.model_callability,
27
+ runtime: extensionRuntime,
28
+ expectedEnabledRoles,
29
+ expectedSelections: { worker: snapshot.worker?.selected ?? null, reviewer: snapshot.reviewer?.selected ?? null },
30
+ }) : { ok: false };
31
+ return { accepted: true, envelope: valid && projection.ok
32
+ ? { runtime: extensionRuntime, snapshot, expiresAt: snapshot.model_callability.expires_at }
33
+ : {} };
34
+ }
@@ -21,9 +21,11 @@ function hasInvocationEvidence(job) {
21
21
  }
22
22
 
23
23
  /**
24
- * Build a bounded, presentation-only model activity summary from the jobs the
25
- * Hub already exposes. No prompts, results, credentials, or new persistence
26
- * are introduced here.
24
+ * Build a bounded, presentation-only, job-level model activity summary from
25
+ * the jobs the Hub already exposes. Each qualifying job contributes one row
26
+ * count for its selected provider/model; the count is not an inferred number
27
+ * of underlying model turns. No prompts, results, credentials, or new
28
+ * persistence are introduced here.
27
29
  */
28
30
  export function aggregateModelInvocations(jobs = []) {
29
31
  const groups = new Map();
@@ -45,7 +47,12 @@ export function aggregateModelInvocations(jobs = []) {
45
47
  current.count += 1;
46
48
  current.taskSources.add(clean(job?.source, 'api'));
47
49
  current.selectionSources.add(clean(job?.selection_source, 'unknown'));
48
- current.roles.add(job?.role === 'reviewer' ? 'reviewer' : 'worker');
50
+ // Do not infer a role from malformed, missing, or future role values. The
51
+ // summary is presentation evidence and must never assert a role the job
52
+ // did not provide.
53
+ if (job?.role === 'worker' || job?.role === 'reviewer') {
54
+ current.roles.add(job.role);
55
+ }
49
56
  const calledAt = validTimestamp(job?.startedAt);
50
57
  if (calledAt && (!current.lastCalledAt || Date.parse(calledAt) > Date.parse(current.lastCalledAt))) {
51
58
  current.lastCalledAt = calledAt;