dsh-switchman 1.11.0 → 1.11.2
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/README.de.md +6 -6
- package/README.es.md +6 -6
- package/README.fr.md +6 -6
- package/README.it.md +6 -6
- package/README.ja.md +6 -6
- package/README.ko.md +6 -6
- package/README.md +6 -6
- package/README.pt.md +6 -6
- package/README.ru.md +6 -6
- package/README.zh-TW.md +6 -6
- package/README.zh.md +6 -6
- package/client.js +76 -9
- package/cordis.patch.yml +30 -1
- package/host/config.js +28 -13
- package/host/context-trim/index.js +53 -12
- package/host/context-trim/metrics.js +17 -11
- package/host/context-trim/summarizer.js +38 -7
- package/host/context-trim/trigger.js +3 -3
- package/host/context-watch.js +83 -9
- package/host/dispatch.js +251 -8
- package/host/routes.js +44 -8
- package/host/teams.js +4 -2
- package/host/tool-args-sanitize.js +106 -0
- package/index.js +10 -0
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -194,10 +194,11 @@ window.__ModuleLoader__.load({
|
|
|
194
194
|
* Host-side default (also the fallback for malformed stored values). */
|
|
195
195
|
const WM_NUMBER_FIELDS = [
|
|
196
196
|
{ field: 'wmSoftTokens', min: 1000, def: 80000 },
|
|
197
|
-
{ field: 'wmHardTokens', min: 2000, def:
|
|
197
|
+
{ field: 'wmHardTokens', min: 2000, def: 95000 },
|
|
198
198
|
{ field: 'wmForceTokens', min: 3000, def: 150000 },
|
|
199
199
|
{ field: 'wmReadBudgetTokens', min: 200, def: 3000 },
|
|
200
200
|
{ field: 'wmSubagentForceTokens', min: 0, def: 0 },
|
|
201
|
+
{ field: 'wmSubagentWindowRatio', min: 0.1, def: 0.75 },
|
|
201
202
|
];
|
|
202
203
|
|
|
203
204
|
/** The three thresholds that must keep a strict ascending order. */
|
|
@@ -213,6 +214,10 @@ window.__ModuleLoader__.load({
|
|
|
213
214
|
label: 'wmSubagentForceLabel',
|
|
214
215
|
help: 'wmSubagentForceHelp',
|
|
215
216
|
},
|
|
217
|
+
wmSubagentWindowRatio: {
|
|
218
|
+
label: 'wmSubagentWindowRatioLabel',
|
|
219
|
+
help: 'wmSubagentWindowRatioHelp',
|
|
220
|
+
},
|
|
216
221
|
};
|
|
217
222
|
|
|
218
223
|
/** Intent-routing classifier kinds (design: intent-routing.md §2;
|
|
@@ -271,12 +276,12 @@ window.__ModuleLoader__.load({
|
|
|
271
276
|
* write (and vice versa). */
|
|
272
277
|
const TRIM_NUMBER_FIELDS = [
|
|
273
278
|
{ field: 'trimGrowthTokens', min: 1000, def: 22500 },
|
|
274
|
-
{ field: 'trimIncrementTokens', min: 1000, def:
|
|
275
|
-
{ field: 'trimPressurePercent', min: 5, max: 100, def:
|
|
279
|
+
{ field: 'trimIncrementTokens', min: 1000, def: 25000 },
|
|
280
|
+
{ field: 'trimPressurePercent', min: 5, max: 100, def: 65 },
|
|
276
281
|
{ field: 'trimEmergencyPercent', min: 5, max: 100, def: 95 },
|
|
277
282
|
{ field: 'trimWindowTokens', min: 0, def: 0 },
|
|
278
|
-
{ field: 'trimProtectedTail', min: 0, max: 100, def:
|
|
279
|
-
{ field: 'trimMaxFoldsPerHour', min: 1, max: 60, def:
|
|
283
|
+
{ field: 'trimProtectedTail', min: 0, max: 100, def: 10 },
|
|
284
|
+
{ field: 'trimMaxFoldsPerHour', min: 1, max: 60, def: 30 },
|
|
280
285
|
{ field: 'trimLadderT2Tokens', min: 1000, def: 8000 },
|
|
281
286
|
{ field: 'trimLadderT2MinBlocks', min: 2, max: 200, def: 3 },
|
|
282
287
|
{ field: 'trimLadderT3Tokens', min: 1000, def: 6000 },
|
|
@@ -367,12 +372,16 @@ window.__ModuleLoader__.load({
|
|
|
367
372
|
/** Stable identity of one provider/model route. */
|
|
368
373
|
const routeKey = (route) => `${route.provider}\0${route.model}`;
|
|
369
374
|
|
|
370
|
-
/** One well-formed {provider, model} route, or null. */
|
|
375
|
+
/** One well-formed {provider, model[, effort]} route, or null. */
|
|
371
376
|
function sanitizeRoute(route) {
|
|
372
377
|
if (route === null || typeof route !== 'object') return null;
|
|
373
378
|
if (typeof route.provider !== 'string' || route.provider === '') return null;
|
|
374
379
|
if (typeof route.model !== 'string' || route.model === '') return null;
|
|
375
|
-
|
|
380
|
+
// Trim side routes may carry an optional effort pin; keep it on the
|
|
381
|
+
// round-trip so a settings save never strips it (intent routes omit it).
|
|
382
|
+
const out = { provider: route.provider, model: route.model };
|
|
383
|
+
if (typeof route.effort === 'string' && route.effort !== '') out.effort = route.effort;
|
|
384
|
+
return out;
|
|
376
385
|
}
|
|
377
386
|
|
|
378
387
|
/** One well-formed rank entry ({provider, model[, tier]}), or null;
|
|
@@ -896,6 +905,9 @@ window.__ModuleLoader__.load({
|
|
|
896
905
|
wmSubagentForceLabel: '子智能体强制阈值',
|
|
897
906
|
wmSubagentForceHelp:
|
|
898
907
|
'仅覆盖子智能体的 force 档(soft/hard 与主会话共用);0 = 跟随上方强制水位。',
|
|
908
|
+
wmSubagentWindowRatioLabel: '子智能体窗口比例',
|
|
909
|
+
wmSubagentWindowRatioHelp:
|
|
910
|
+
'按子智能体自身模型窗口缩放三档:force = min(静态档,窗口×比例),soft/hard 同比收缩;只收紧不放宽。',
|
|
899
911
|
wmSubagentFollowForce: '跟随 force 档(0)',
|
|
900
912
|
wmCommands:
|
|
901
913
|
'命令:/ctx-pause 暂停水位干预 · /ctx-resume 恢复干预 · /ctx-handover 立即写交接文档并压缩。',
|
|
@@ -912,6 +924,7 @@ window.__ModuleLoader__.load({
|
|
|
912
924
|
trimSideModelLabel: '摘要模型',
|
|
913
925
|
trimSideModelHelp: '折叠摘要使用的模型路由;默认「跟随会话模型」。阶梯蒸馏同样使用该路由。',
|
|
914
926
|
trimFollowSession: '跟随会话模型',
|
|
927
|
+
trimEffortPinLow: '默认(pin low)',
|
|
915
928
|
trimFoldFallbackModelLabel: '折叠异常兜底模型',
|
|
916
929
|
trimFoldFallbackModelHelp: '摘要模型调用失败时改用该模型重试;仍失败则压力折叠退回机械折叠。尽量选择没有冷却、风控或并发限制的模型,避免与主会话共用同一账号。留空 = 无兜底。',
|
|
917
930
|
trimFallbackUnset: '未设置(失败时直接机械折叠)',
|
|
@@ -1160,6 +1173,9 @@ window.__ModuleLoader__.load({
|
|
|
1160
1173
|
wmSubagentForceLabel: 'Subagent force threshold',
|
|
1161
1174
|
wmSubagentForceHelp:
|
|
1162
1175
|
'Overrides only the subagent force tier (soft/hard are shared with the main session); 0 follows the force watermark above.',
|
|
1176
|
+
wmSubagentWindowRatioLabel: 'Subagent window ratio',
|
|
1177
|
+
wmSubagentWindowRatioHelp:
|
|
1178
|
+
'Scale the subagent tiers to its own model window: force = min(static tier, window × ratio); soft/hard shrink by the same factor. Tightens only, never loosens.',
|
|
1163
1179
|
wmSubagentFollowForce: 'Follow the force tier (0)',
|
|
1164
1180
|
wmCommands:
|
|
1165
1181
|
'Commands: /ctx-pause suspends watermark actions · /ctx-resume resumes them · /ctx-handover writes the handover doc and compacts now.',
|
|
@@ -1179,6 +1195,7 @@ window.__ModuleLoader__.load({
|
|
|
1179
1195
|
trimSideModelHelp:
|
|
1180
1196
|
'Route for fold summaries; defaults to following the session model. Also used by ladder distillations.',
|
|
1181
1197
|
trimFollowSession: 'Follow session model',
|
|
1198
|
+
trimEffortPinLow: 'Default (pin low)',
|
|
1182
1199
|
trimFoldFallbackModelLabel: 'Fold fallback model',
|
|
1183
1200
|
trimFoldFallbackModelHelp:
|
|
1184
1201
|
'When the summary route fails, the fold retries on this model before degrading a pressure fold to a mechanical one. Prefer a model with headroom on cooldowns, risk control, and concurrency limits — avoid sharing the pinned account with the main session. Empty = no fallback leg.',
|
|
@@ -1434,6 +1451,9 @@ window.__ModuleLoader__.load({
|
|
|
1434
1451
|
wmSubagentCapHelp: '開啟後子智能體可用下方獨立強制閾值;關閉則完全跟隨上方三檔水位。',
|
|
1435
1452
|
wmSubagentForceLabel: '子智能體強制閾值',
|
|
1436
1453
|
wmSubagentForceHelp: '僅覆蓋子智能體的 force 檔(soft/hard 與主會話共用);0 = 跟隨上方強制水位。',
|
|
1454
|
+
wmSubagentWindowRatioLabel: '子代理視窗比例',
|
|
1455
|
+
wmSubagentWindowRatioHelp:
|
|
1456
|
+
'按子代理自身模型視窗縮放三檔:force = min(靜態檔,視窗×比例),soft/hard 同比收縮;只收緊不放寬。',
|
|
1437
1457
|
wmSubagentFollowForce: '跟隨 force 檔(0)',
|
|
1438
1458
|
wmCommands: '命令:/ctx-pause 暫停水位干預 · /ctx-resume 恢復干預 · /ctx-handover 立即寫交接文件並壓縮。',
|
|
1439
1459
|
trimSectionTitle: '上下文裁剪',
|
|
@@ -1449,6 +1469,7 @@ window.__ModuleLoader__.load({
|
|
|
1449
1469
|
trimSideModelLabel: '摘要模型',
|
|
1450
1470
|
trimSideModelHelp: '摺疊摘要使用的模型路由;預設「跟隨對話模型」。階梯蒸餾同樣使用該路由。',
|
|
1451
1471
|
trimFollowSession: '跟隨對話模型',
|
|
1472
|
+
trimEffortPinLow: '預設(pin low)',
|
|
1452
1473
|
trimFoldFallbackModelLabel: '摺疊異常備援模型',
|
|
1453
1474
|
trimFoldFallbackModelHelp: '摘要模型呼叫失敗時改用該模型重試;仍失敗則壓力摺疊退回機械摺疊。盡量選擇沒有冷卻、風控或並行限制的模型,避免與主對話共用同一帳號。留空 = 無備援。',
|
|
1454
1475
|
trimFallbackUnset: '未設定(失敗時直接機械摺疊)',
|
|
@@ -1677,6 +1698,9 @@ window.__ModuleLoader__.load({
|
|
|
1677
1698
|
wmSubagentCapHelp: 'オンにすると、下の専用強制しきい値を使えます。オフなら上の 3 段階の水位に完全に従います。',
|
|
1678
1699
|
wmSubagentForceLabel: 'サブエージェント強制しきい値',
|
|
1679
1700
|
wmSubagentForceHelp: 'サブエージェントの force 段階のみを上書きします(soft/hard はメインセッションと共通)。0 なら上の強制水位に従います。',
|
|
1701
|
+
wmSubagentWindowRatioLabel: 'サブエージェント窓比率',
|
|
1702
|
+
wmSubagentWindowRatioHelp:
|
|
1703
|
+
'サブエージェント自身のモデル窓に合わせて3段階を縮小:force = min(静的しきい値,窓×比率)。soft/hardも同率で縮小。引き締めのみ。',
|
|
1680
1704
|
wmSubagentFollowForce: 'force 段階に従う(0)',
|
|
1681
1705
|
wmCommands: 'コマンド:/ctx-pause で水位アクションを一時停止 · /ctx-resume で再開 · /ctx-handover で今すぐ引き継ぎ文書を書いて圧縮。',
|
|
1682
1706
|
trimSectionTitle: 'コンテキストトリム',
|
|
@@ -1771,6 +1795,7 @@ window.__ModuleLoader__.load({
|
|
|
1771
1795
|
'分類失敗時:前回ルート → フォールバック → 候補の先頭。空欄は候補の先頭。',
|
|
1772
1796
|
intentRouteUnset: '未設定',
|
|
1773
1797
|
intentEffortFollow: 'モデルのデフォルト',
|
|
1798
|
+
trimEffortPinLow: 'デフォルト(pin low)',
|
|
1774
1799
|
intentDockAuto: '自動',
|
|
1775
1800
|
intentDockUnrouted: '未ルーティング',
|
|
1776
1801
|
intentDockUsing: '使用中',
|
|
@@ -1904,8 +1929,12 @@ window.__ModuleLoader__.load({
|
|
|
1904
1929
|
wmSubagentCapHelp: '켜면 서브에이전트가 아래 전용 강제 임계값을 사용합니다. 끄면 위 세 수위를 그대로 따릅니다.',
|
|
1905
1930
|
wmSubagentForceLabel: '서브에이전트 강제 임계값',
|
|
1906
1931
|
wmSubagentForceHelp: '서브에이전트의 force 단계만 덮어씁니다(soft/hard는 메인 세션과 공유). 0이면 위 강제 수위를 따릅니다.',
|
|
1932
|
+
wmSubagentWindowRatioLabel: '서브에이전트 창 비율',
|
|
1933
|
+
wmSubagentWindowRatioHelp:
|
|
1934
|
+
'서브에이전트 자체 모델 창에 맞춰 3단계 축소: force = min(정적 기준, 창×비율). soft/hard도 같은 비율로 축소. 완화 없이 강화만.',
|
|
1907
1935
|
wmSubagentFollowForce: 'force 단계 따르기(0)',
|
|
1908
1936
|
wmCommands: '명령: /ctx-pause 수위 개입 일시 중지 · /ctx-resume 재개 · /ctx-handover 지금 핸드오버 문서를 작성하고 압축.',
|
|
1937
|
+
trimEffortPinLow: '기본 (low 고정)',
|
|
1909
1938
|
trimSectionTitle: '컨텍스트 트림',
|
|
1910
1939
|
trimModeLabel: '트림 모드',
|
|
1911
1940
|
trimModeHelp:
|
|
@@ -2130,8 +2159,12 @@ window.__ModuleLoader__.load({
|
|
|
2130
2159
|
wmSubagentCapHelp: 'Subagents dürfen die eigene erzwungene Schwelle unten nutzen; aus = vollständig den drei Stufen oben folgen.',
|
|
2131
2160
|
wmSubagentForceLabel: 'Erzwungene Schwelle für Subagents',
|
|
2132
2161
|
wmSubagentForceHelp: 'Überschreibt nur die Force-Stufe der Subagents (soft/hard teilen sich mit der Haupt-Session); 0 folgt dem erzwungenen Wasserstand oben.',
|
|
2162
|
+
wmSubagentWindowRatioLabel: 'Subagent-Fensterquote',
|
|
2163
|
+
wmSubagentWindowRatioHelp:
|
|
2164
|
+
'Skaliert die Stufen auf das eigene Modellfenster des Subagenten: force = min(statische Stufe, Fenster × Quote); soft/hard schrumpfen im selben Faktor. Verengt nur, lockert nie.',
|
|
2133
2165
|
wmSubagentFollowForce: 'Der Force-Stufe folgen (0)',
|
|
2134
2166
|
wmCommands: 'Befehle: /ctx-pause pausiert Wasserstand-Aktionen · /ctx-resume setzt sie fort · /ctx-handover schreibt das Übergabedokument jetzt und komprimiert sofort.',
|
|
2167
|
+
trimEffortPinLow: 'Standard (pin low)',
|
|
2135
2168
|
trimSectionTitle: 'Kontext-Trim',
|
|
2136
2169
|
trimModeLabel: 'Trim-Modus',
|
|
2137
2170
|
trimModeHelp:
|
|
@@ -2359,8 +2392,12 @@ window.__ModuleLoader__.load({
|
|
|
2359
2392
|
wmSubagentCapHelp: 'Permite que los subagentes usen el umbral forzado propio de abajo; desactivado = seguir por completo los tres niveles de arriba.',
|
|
2360
2393
|
wmSubagentForceLabel: 'Umbral forzado de subagentes',
|
|
2361
2394
|
wmSubagentForceHelp: 'Solo sobrescribe el nivel forzado de los subagentes (soft/hard se comparten con la sesión principal); 0 sigue el nivel forzado de arriba.',
|
|
2395
|
+
wmSubagentWindowRatioLabel: 'Ratio de ventana del subagente',
|
|
2396
|
+
wmSubagentWindowRatioHelp:
|
|
2397
|
+
'Escala los niveles a la ventana del propio modelo del subagente: force = min(nivel estático, ventana × ratio); soft/hard se reducen por el mismo factor. Solo restringe, nunca amplía.',
|
|
2362
2398
|
wmSubagentFollowForce: 'Seguir el nivel forzado (0)',
|
|
2363
2399
|
wmCommands: 'Comandos: /ctx-pause suspende las acciones del nivel · /ctx-resume las reanuda · /ctx-handover escribe el documento y compacta ahora.',
|
|
2400
|
+
trimEffortPinLow: 'Predeterminado (pin low)',
|
|
2364
2401
|
trimSectionTitle: 'Recorte de contexto',
|
|
2365
2402
|
trimModeLabel: 'Modo de recorte',
|
|
2366
2403
|
trimModeHelp:
|
|
@@ -2589,8 +2626,12 @@ window.__ModuleLoader__.load({
|
|
|
2589
2626
|
wmSubagentCapHelp: 'Laisse les subagents utiliser le seuil force dédié ci-dessous ; désactivé = suivent intégralement les trois paliers ci-dessus.',
|
|
2590
2627
|
wmSubagentForceLabel: 'Seuil force des subagents',
|
|
2591
2628
|
wmSubagentForceHelp: 'Ne remplace que le palier force des subagents (soft/hard partagés avec la session principale) ; 0 suit la ligne d’eau force ci-dessus.',
|
|
2629
|
+
wmSubagentWindowRatioLabel: 'Ratio de fenêtre du sous-agent',
|
|
2630
|
+
wmSubagentWindowRatioHelp:
|
|
2631
|
+
'Ajuste les paliers à la fenêtre propre du modèle du sous-agent : force = min(palier statique, fenêtre × ratio) ; soft/hard réduits du même facteur. Ne fait que resserrer, jamais relâcher.',
|
|
2592
2632
|
wmSubagentFollowForce: 'Suivre le palier force (0)',
|
|
2593
2633
|
wmCommands: 'Commandes : /ctx-pause suspend les actions de niveau d’eau · /ctx-resume les reprend · /ctx-handover écrit le document et compacte maintenant.',
|
|
2634
|
+
trimEffortPinLow: 'Par défaut (pin low)',
|
|
2594
2635
|
trimSectionTitle: 'Rognage du contexte',
|
|
2595
2636
|
trimModeLabel: 'Mode de rognage',
|
|
2596
2637
|
trimModeHelp:
|
|
@@ -2819,8 +2860,12 @@ window.__ModuleLoader__.load({
|
|
|
2819
2860
|
wmSubagentCapHelp: 'Lascia i subagent usare la soglia force dedicata qui sotto; spento = seguono appieno le tre soglie sopra.',
|
|
2820
2861
|
wmSubagentForceLabel: 'Soglia force dei subagent',
|
|
2821
2862
|
wmSubagentForceHelp: 'Sostituisce solo la soglia force dei subagent (soft/hard restano condivise con la sessione principale); 0 segue la soglia force sopra.',
|
|
2863
|
+
wmSubagentWindowRatioLabel: 'Rapporto finestra del subagente',
|
|
2864
|
+
wmSubagentWindowRatioHelp:
|
|
2865
|
+
'Ridimensiona i livelli alla finestra del modello del subagente: force = min(livello statico, finestra × rapporto); soft/hard si riducono dello stesso fattore. Solo restrizione, mai allentamento.',
|
|
2822
2866
|
wmSubagentFollowForce: 'Segui la soglia force (0)',
|
|
2823
2867
|
wmCommands: 'Comandi: /ctx-pause sospende le azioni da livello dell’acqua · /ctx-resume le riprende · /ctx-handover scrive il documento e compatta subito.',
|
|
2868
|
+
trimEffortPinLow: 'Predefinito (pin low)',
|
|
2824
2869
|
trimSectionTitle: 'Riduzione del contesto',
|
|
2825
2870
|
trimModeLabel: 'Modalità di riduzione',
|
|
2826
2871
|
trimModeHelp:
|
|
@@ -3049,8 +3094,12 @@ window.__ModuleLoader__.load({
|
|
|
3049
3094
|
wmSubagentCapHelp: 'Deixa os subagents usar o limiar force dedicado abaixo; desligado = seguem integralmente os três limiares acima.',
|
|
3050
3095
|
wmSubagentForceLabel: 'Limiar force dos subagents',
|
|
3051
3096
|
wmSubagentForceHelp: 'Sobrepõe apenas o limiar force dos subagents (soft/hard são compartilhados com a sessão principal); 0 segue o limiar force acima.',
|
|
3097
|
+
wmSubagentWindowRatioLabel: 'Razão de janela do subagente',
|
|
3098
|
+
wmSubagentWindowRatioHelp:
|
|
3099
|
+
'Escala os níveis para a janela do próprio modelo do subagente: force = min(nível estático, janela × razão); soft/hard encolhem pelo mesmo fator. Apenas aperta, nunca afrouxa.',
|
|
3052
3100
|
wmSubagentFollowForce: 'Seguir o limiar force (0)',
|
|
3053
3101
|
wmCommands: 'Comandos: /ctx-pause suspende as ações de nível de água · /ctx-resume as retoma · /ctx-handover escreve o documento e compacta agora.',
|
|
3102
|
+
trimEffortPinLow: 'Padrão (pin low)',
|
|
3054
3103
|
trimSectionTitle: 'Corte de contexto',
|
|
3055
3104
|
trimModeLabel: 'Modo de corte',
|
|
3056
3105
|
trimModeHelp:
|
|
@@ -3279,8 +3328,12 @@ window.__ModuleLoader__.load({
|
|
|
3279
3328
|
wmSubagentCapHelp: 'Субагенты используют отдельный порог force ниже; выключено = полностью следуют трём линиям выше.',
|
|
3280
3329
|
wmSubagentForceLabel: 'Порог force субагентов',
|
|
3281
3330
|
wmSubagentForceHelp: 'Меняет только линию force субагентов (soft/hard общие с главной сессией); 0 — следовать линии force выше.',
|
|
3331
|
+
wmSubagentWindowRatioLabel: 'Доля окна субагента',
|
|
3332
|
+
wmSubagentWindowRatioHelp:
|
|
3333
|
+
'Масштабирует уровни под собственное окно модели субагента: force = min(статический уровень, окно × доля); soft/hard сжимаются на тот же коэффициент. Только ужесточает, не смягчает.',
|
|
3282
3334
|
wmSubagentFollowForce: 'Следовать линии force (0)',
|
|
3283
3335
|
wmCommands: 'Команды: /ctx-pause приостанавливает действия по уровню воды · /ctx-resume возобновляет их · /ctx-handover записывает документ передачи и сжимает прямо сейчас.',
|
|
3336
|
+
trimEffortPinLow: 'По умолчанию (pin low)',
|
|
3284
3337
|
trimSectionTitle: 'Обрезка контекста',
|
|
3285
3338
|
trimModeLabel: 'Режим обрезки',
|
|
3286
3339
|
trimModeHelp:
|
|
@@ -5856,6 +5909,7 @@ window.__ModuleLoader__.load({
|
|
|
5856
5909
|
(meta) => meta.model === route.model,
|
|
5857
5910
|
);
|
|
5858
5911
|
edit(field, {
|
|
5912
|
+
...route,
|
|
5859
5913
|
provider: next,
|
|
5860
5914
|
model: kept ? route.model : '',
|
|
5861
5915
|
});
|
|
@@ -5880,18 +5934,31 @@ window.__ModuleLoader__.load({
|
|
|
5880
5934
|
disabled: !editable || route.provider === '',
|
|
5881
5935
|
'aria-label': `${label} · model`,
|
|
5882
5936
|
}, ...trimModelOptions(followMode, route, route.model)),
|
|
5937
|
+
route.provider !== ''
|
|
5938
|
+
? intentEffortSelect(
|
|
5939
|
+
`${trimId}-${field}-effort`,
|
|
5940
|
+
route,
|
|
5941
|
+
route.effort,
|
|
5942
|
+
(next) => edit(field, { ...route, effort: next }),
|
|
5943
|
+
`${label} · effort`,
|
|
5944
|
+
t('trimEffortPinLow'),
|
|
5945
|
+
)
|
|
5946
|
+
: null,
|
|
5883
5947
|
);
|
|
5884
5948
|
};
|
|
5885
5949
|
/** Reasoning-effort select for one intent route: the model's
|
|
5886
5950
|
* wire efforts when the catalog reports them, the EFFORTS
|
|
5887
5951
|
* fallback otherwise; '' = adapter default. A stored value
|
|
5888
|
-
* missing from the list stays selectable.
|
|
5952
|
+
* missing from the list stays selectable. emptyLabel replaces
|
|
5953
|
+
* the '' option label (trim rows pin low instead of following
|
|
5954
|
+
* the model default). */
|
|
5889
5955
|
const intentEffortSelect = (
|
|
5890
5956
|
id,
|
|
5891
5957
|
route,
|
|
5892
5958
|
value,
|
|
5893
5959
|
onCommit,
|
|
5894
5960
|
ariaLabel,
|
|
5961
|
+
emptyLabel,
|
|
5895
5962
|
) => {
|
|
5896
5963
|
const meta = catalogIndex.get(routeKey(route));
|
|
5897
5964
|
const wire =
|
|
@@ -5915,7 +5982,7 @@ window.__ModuleLoader__.load({
|
|
|
5915
5982
|
style: STYLE.effortSelect,
|
|
5916
5983
|
'aria-label': ariaLabel,
|
|
5917
5984
|
},
|
|
5918
|
-
h('option', { value: '' }, t('intentEffortFollow')),
|
|
5985
|
+
h('option', { value: '' }, emptyLabel ?? t('intentEffortFollow')),
|
|
5919
5986
|
...[...wire, ...pending].map((entry) =>
|
|
5920
5987
|
h('option', { value: entry.id }, entry.name ?? entry.id),
|
|
5921
5988
|
),
|
package/cordis.patch.yml
CHANGED
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
- 禁止复活死会话:子智能体达 cap 终止后,用自包含 prompt 新派一个,不 resume 旧壳。
|
|
100
100
|
- 广搜先问:首次整项目范围的搜索先向用户澄清范围,定点检索不受此限。
|
|
101
101
|
- 大额派发先估成本:预计 ≥3 个成员或总消耗 >100k token 时,先报规模与预估请用户确认。
|
|
102
|
-
-
|
|
102
|
+
- 子路由两通道:派发 subagent/workflow 默认显式传 provider/model/effort(按泳道表逐字段拆填,经 DSH 会话级授权白名单校验——设置页「允许 Agent 为子智能体选择模型」的授权列表,与泳道池无关);隐式继承(完全不传三字段)不是省事通道,仅当显式尝试被拒且 list_subagent_models 确认无可用路由后才可兜底,并在回报中声明 IMPLICIT-DOWNGRADED——隐式拿到的是主会话路由,泳道模型丢失。泳道池只是偏好排序,授权边界以 DSH 设置为准,不试图改写它。
|
|
103
103
|
|
|
104
104
|
## 三、分级验证(质量下限,不可省)
|
|
105
105
|
- 改动 >20 行或涉及多个调用点:交 tester 验证。
|
|
@@ -633,3 +633,32 @@
|
|
|
633
633
|
- insert:
|
|
634
634
|
- id: dsh-switchman-host
|
|
635
635
|
name: dsh-switchman
|
|
636
|
+
config:
|
|
637
|
+
langScope: project
|
|
638
|
+
dispatchEnforce: advice
|
|
639
|
+
wmSoftTokens: 80000
|
|
640
|
+
wmHardTokens: 95000
|
|
641
|
+
wmForceTokens: 150000
|
|
642
|
+
wmReadBudgetTokens: 3000
|
|
643
|
+
wmDenyMode: cap
|
|
644
|
+
wmAutoHandover: true
|
|
645
|
+
wmSubagentCap: true
|
|
646
|
+
wmSubagentForceTokens: 0
|
|
647
|
+
wmSubagentWindowRatio: 0.75
|
|
648
|
+
teamsMode: true
|
|
649
|
+
syncWhitelist: true
|
|
650
|
+
trimEnabled: true
|
|
651
|
+
trimMode: auto
|
|
652
|
+
trimGrowthTokens: 22500
|
|
653
|
+
trimIncrementTokens: 25000
|
|
654
|
+
trimPressurePercent: 65
|
|
655
|
+
trimEmergencyPercent: 95
|
|
656
|
+
trimWindowTokens: 0
|
|
657
|
+
trimProtectedTail: 10
|
|
658
|
+
trimMaxFoldsPerHour: 30
|
|
659
|
+
trimLadderEnabled: true
|
|
660
|
+
trimLadderT2Tokens: 8000
|
|
661
|
+
trimLadderT2MinBlocks: 3
|
|
662
|
+
trimLadderT3Tokens: 6000
|
|
663
|
+
trimLadderT3MinBlocks: 2
|
|
664
|
+
surfaceFold: true
|
package/host/config.js
CHANGED
|
@@ -67,10 +67,14 @@ export const IntentCandidateSchema = z.object({
|
|
|
67
67
|
* ModelRouteSchema this accepts the "unset" `{provider:"", model:""}`
|
|
68
68
|
* pair the settings page round-trips — ModelRouteSchema's `.min(1)`
|
|
69
69
|
* would reject that exact declared default on every settings write.
|
|
70
|
-
* Consumers validate non-empty before trusting the route.
|
|
70
|
+
* Consumers validate non-empty before trusting the route. effort follows
|
|
71
|
+
* the optional no-`.required()` style (schemastery passes undefined
|
|
72
|
+
* through): the trim summarizer reads it as a reasoning-effort pin;
|
|
73
|
+
* intent-layer routes leave it unset. */
|
|
71
74
|
const IntentRouteSchema = z.object({
|
|
72
75
|
provider: z.string(),
|
|
73
76
|
model: z.string(),
|
|
77
|
+
effort: z.string(),
|
|
74
78
|
});
|
|
75
79
|
|
|
76
80
|
/** All dsh-switchman settings. Field names are the settings mutation paths. */
|
|
@@ -130,7 +134,8 @@ export const Config = z.object({
|
|
|
130
134
|
/** Soft watermark: advisory banner + delegation nudge (tokens). */
|
|
131
135
|
wmSoftTokens: z.number().min(1000).default(80000).volatile(),
|
|
132
136
|
/** Hard watermark: read budget tightens, wrap-up advised (tokens). 95k
|
|
133
|
-
*
|
|
137
|
+
* sits just under the trim pressure line (65% of the 150k window ≈
|
|
138
|
+
* 97.5k): past
|
|
134
139
|
* this point the fold engine is already folding, so per-call read
|
|
135
140
|
* budgets must tighten at the SAME boundary — agent sessions grow in
|
|
136
141
|
* PULSES (one large file read = tens of thousands of tokens injected
|
|
@@ -152,6 +157,14 @@ export const Config = z.object({
|
|
|
152
157
|
wmSubagentCap: z.boolean().default(true).volatile(),
|
|
153
158
|
/** Subagent hard cap; 0 = share wmForceTokens. */
|
|
154
159
|
wmSubagentForceTokens: z.number().min(0).default(0).volatile(),
|
|
160
|
+
/** Scale subagent watermark tiers to the subagent's own model window:
|
|
161
|
+
* force = min(static force [or wmSubagentForceTokens], window × ratio);
|
|
162
|
+
* soft/hard shrink by the same factor to preserve tier spacing. The ratio
|
|
163
|
+
* can only TIGHTEN tiers, never loosen them; 1 = cap at the full model
|
|
164
|
+
* window (still tightens when the window is smaller than the static
|
|
165
|
+
* force tier). Resolution is async and cached per model route; until it
|
|
166
|
+
* lands, the static tiers apply (fail-open). */
|
|
167
|
+
wmSubagentWindowRatio: z.number().min(0.1).max(1).default(0.75).volatile(),
|
|
155
168
|
|
|
156
169
|
// --- Teams mode: doctrine injection + whitelist sync -------------------
|
|
157
170
|
/** Agent Teams master switch. ON = inject the `[SWITCHMAN:TEAMS]` doctrine
|
|
@@ -247,10 +260,10 @@ export const Config = z.object({
|
|
|
247
260
|
trimGrowthTokens: z.number().min(1000).default(22500).volatile(),
|
|
248
261
|
/** Growth gate companion: the contiguous compressible increment must
|
|
249
262
|
* also reach this many estimated tokens (chars/4 heuristic). */
|
|
250
|
-
trimIncrementTokens: z.number().min(1000).default(
|
|
263
|
+
trimIncrementTokens: z.number().min(1000).default(25000).volatile(),
|
|
251
264
|
/** Pressure tier (tier b): fold when view usage reaches this percent of
|
|
252
265
|
* the context window (requires a resolvable window; otherwise skipped). */
|
|
253
|
-
trimPressurePercent: z.number().min(1).max(100).default(
|
|
266
|
+
trimPressurePercent: z.number().min(1).max(100).default(65).volatile(),
|
|
254
267
|
/** Emergency tier (tier c): forced MECHANICAL fold (no side-model call)
|
|
255
268
|
* at this percent of the window; cooldown-gated. */
|
|
256
269
|
trimEmergencyPercent: z.number().min(1).max(100).default(95).volatile(),
|
|
@@ -260,22 +273,24 @@ export const Config = z.object({
|
|
|
260
273
|
* trimIncrementTokens proportionally makes the growth tier unreachable
|
|
261
274
|
* (a 30k window can never accumulate a 50k increment; observed live:
|
|
262
275
|
* growth fired once while emergency fired 17 times). Rule of thumb:
|
|
263
|
-
* increment ≈ window/
|
|
276
|
+
* increment ≈ window/6, growth ≈ window × 0.15 (~25k/22.5k at 150k,
|
|
277
|
+
* 2026-11-09 steady-growth recalibration — one fold per ~22.5k of new
|
|
278
|
+
* work instead of one every ~2 minutes; pressure pins the 50-100k band
|
|
279
|
+
* ceiling).]
|
|
264
280
|
* Effective-window cap for the tier math: tier points are computed
|
|
265
281
|
* against min(model-resolved window, cap). 0 = auto (cap follows
|
|
266
282
|
* wmForceTokens, so folds always fire ahead of the handover tier). */
|
|
267
283
|
trimWindowTokens: z.number().min(0).default(0).volatile(),
|
|
268
284
|
/** Last N messages of a history are never folded (the live tail the
|
|
269
285
|
* next model call reasons over). */
|
|
270
|
-
trimProtectedTail: z.number().min(0).max(100).default(
|
|
286
|
+
trimProtectedTail: z.number().min(0).max(100).default(10).volatile(),
|
|
271
287
|
/** Side-model fold cadence cap per rolling hour (emergency mechanical
|
|
272
|
-
* folds are exempt; they have their own cooldown).
|
|
273
|
-
* full-rate generation
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
|
|
278
|
-
trimMaxFoldsPerHour: z.number().min(1).max(60).default(16).volatile(),
|
|
288
|
+
* folds are exempt; they have their own cooldown). 30 sits just above
|
|
289
|
+
* the pure T1 demand of continuous full-rate generation (~27 folds/hour
|
|
290
|
+
* at the 22.5k growth cadence and ~170 tok/s output), so it only bites
|
|
291
|
+
* on runaway churn; at the old cap of 16 growth consumed the whole
|
|
292
|
+
* budget and the ladder starved. */
|
|
293
|
+
trimMaxFoldsPerHour: z.number().min(1).max(60).default(30).volatile(),
|
|
279
294
|
/** Route for the side-model summarizer (L3). Empty = inherit the
|
|
280
295
|
* session's current model. */
|
|
281
296
|
trimSideModel: IntentRouteSchema.default({ provider: "", model: "" }).volatile(),
|
|
@@ -58,6 +58,7 @@ import { childSessionIdOf } from "../subagent-model.js";
|
|
|
58
58
|
// it and the stored tally takes over.
|
|
59
59
|
const trimViewBySession = new Map(); // sessionId -> { viewTokens, at, inject, lastNoneAt }
|
|
60
60
|
const trimWireBySession = new Map(); // sessionId -> { promptTokens, at } — last sampled provider prompt of an accepted MAIN dispatch (side calls never write; a tiny inherited-id usage sample must not lower the watermark's wire floor)
|
|
61
|
+
const capturedModelBySession = new Map(); // sessionId -> { provider, model, at } — last stream-captured model route, read by the watermark layer to scale subagent tiers to the session's own model window
|
|
61
62
|
/** Freshness TTL for the in-memory view/injection registry (watermark and
|
|
62
63
|
* banner display only). NEVER a fold gate: gating folds on "did the surface
|
|
63
64
|
* channel ship recently" deadlocks resumed sessions (2026-11-05). The fold
|
|
@@ -112,6 +113,19 @@ export function recentTrimViewTokensOf(sessionId, maxAgeMs = TRIM_VIEW_MAX_AGE_M
|
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
/** Most recent captured model route ({provider, model}) for a session, or
|
|
117
|
+
* null when missing, stale, or malformed. The watermark layer reads this to
|
|
118
|
+
* look up the session's own model window (subagent tier scaling). */
|
|
119
|
+
export function recentTrimModelOf(sessionId, maxAgeMs = TRIM_VIEW_MAX_AGE_MS) {
|
|
120
|
+
try {
|
|
121
|
+
const entry = capturedModelBySession.get(sessionId);
|
|
122
|
+
if (entry === undefined || Date.now() - entry.at > maxAgeMs) return null;
|
|
123
|
+
return typeof entry.provider === "string" && typeof entry.model === "string" ? { provider: entry.provider, model: entry.model } : null;
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
115
129
|
/** Meter-duality bridge, wire domain (design §8): the same number the trim
|
|
116
130
|
* pressure tiers evaluate in maybeFold — Math.max(outbound view estimate,
|
|
117
131
|
* the last sampled provider prompt) — or null when the gate rejects or
|
|
@@ -121,9 +135,10 @@ export function recentTrimViewTokensOf(sessionId, maxAgeMs = TRIM_VIEW_MAX_AGE_M
|
|
|
121
135
|
* system/tool sections (a session-shape-dependent mass — live decision-log
|
|
122
136
|
* pairs drift −5k…+6k between view and wire in steady state), so a
|
|
123
137
|
* view-only watermark tightens every protective tier late by exactly that
|
|
124
|
-
* mass, and the wmHardTokens-95k
|
|
125
|
-
* is silently defeated. The wire half comes from
|
|
126
|
-
* only (noteTrimWireTokens below). Same sticky
|
|
138
|
+
* mass, and the wmHardTokens-95k ≈ trim-pressure-97.5k adjacency (65%
|
|
139
|
+
* of the 150k window) is silently defeated. The wire half comes from
|
|
140
|
+
* accepted MAIN dispatches only (noteTrimWireTokens below). Same sticky
|
|
141
|
+
* gate as the view bridge;
|
|
127
142
|
* fail-open — null means the caller falls back to the stored tally. */
|
|
128
143
|
export function recentTrimPromptTokensOf(sessionId, maxAgeMs = TRIM_VIEW_MAX_AGE_MS) {
|
|
129
144
|
try {
|
|
@@ -233,7 +248,11 @@ function readRoute(config, key) {
|
|
|
233
248
|
let got = value;
|
|
234
249
|
if (value !== null && typeof value === "object" && typeof value.get === "function") got = value.get();
|
|
235
250
|
if (got !== null && typeof got === "object" && typeof got.provider === "string" && got.provider !== "" && typeof got.model === "string" && got.model !== "") {
|
|
236
|
-
|
|
251
|
+
// Pass the configured effort through verbatim (IntentRouteSchema
|
|
252
|
+
// already carries the optional field); absent/empty just omits it.
|
|
253
|
+
return typeof got.effort === "string" && got.effort !== ""
|
|
254
|
+
? { provider: got.provider, model: got.model, effort: got.effort }
|
|
255
|
+
: { provider: got.provider, model: got.model };
|
|
237
256
|
}
|
|
238
257
|
} catch {
|
|
239
258
|
/* fall through */
|
|
@@ -477,11 +496,11 @@ export function applyContextTrim(ctx, config) {
|
|
|
477
496
|
};
|
|
478
497
|
const thresholdsNow = () => ({
|
|
479
498
|
growthTokens: readNumber(config, "trimGrowthTokens", 22_500),
|
|
480
|
-
incrementTokens: readNumber(config, "trimIncrementTokens",
|
|
481
|
-
pressurePercent: readNumber(config, "trimPressurePercent",
|
|
499
|
+
incrementTokens: readNumber(config, "trimIncrementTokens", 25_000),
|
|
500
|
+
pressurePercent: readNumber(config, "trimPressurePercent", 65),
|
|
482
501
|
emergencyPercent: readNumber(config, "trimEmergencyPercent", 95),
|
|
483
|
-
protectedTail: Math.max(0, Math.round(readNumber(config, "trimProtectedTail",
|
|
484
|
-
maxFoldsPerHour: readNumber(config, "trimMaxFoldsPerHour",
|
|
502
|
+
protectedTail: Math.max(0, Math.round(readNumber(config, "trimProtectedTail", 10))),
|
|
503
|
+
maxFoldsPerHour: readNumber(config, "trimMaxFoldsPerHour", 30),
|
|
485
504
|
ladderT2Tokens: readNumber(config, "trimLadderT2Tokens", 8_000),
|
|
486
505
|
ladderT2MinBlocks: readNumber(config, "trimLadderT2MinBlocks", 3),
|
|
487
506
|
ladderT3Tokens: readNumber(config, "trimLadderT3Tokens", 6_000),
|
|
@@ -575,6 +594,10 @@ export function applyContextTrim(ctx, config) {
|
|
|
575
594
|
* copy — message objects are treated as immutable records). */
|
|
576
595
|
function captureOf(sessionId, messages, model, viewTokens) {
|
|
577
596
|
captures.set(sessionId, { messages: messages.slice(), model, viewTokens, at: Date.now() });
|
|
597
|
+
if (model !== null && typeof model === "object" && typeof model.provider === "string" && typeof model.model === "string") {
|
|
598
|
+
capturedModelBySession.set(sessionId, { provider: model.provider, model: model.model, at: Date.now() });
|
|
599
|
+
if (capturedModelBySession.size > 200) capturedModelBySession.delete(capturedModelBySession.keys().next().value);
|
|
600
|
+
}
|
|
578
601
|
// keep the capture map bounded (200 sessions)
|
|
579
602
|
if (captures.size > 200) {
|
|
580
603
|
const oldest = captures.keys().next().value;
|
|
@@ -672,7 +695,15 @@ export function applyContextTrim(ctx, config) {
|
|
|
672
695
|
} else if (view.applied > 0) {
|
|
673
696
|
epochResetDoneBySession.delete(sessionId);
|
|
674
697
|
}
|
|
675
|
-
|
|
698
|
+
// Capture the session's own resolved reasoningEffort alongside
|
|
699
|
+
// provider/model: at stream-listener time DSH has already resolved the
|
|
700
|
+
// effective effort for this dispatch, so the side route can inherit it
|
|
701
|
+
// verbatim instead of re-deriving (and losing) it.
|
|
702
|
+
const model = {
|
|
703
|
+
provider: options.provider ?? null,
|
|
704
|
+
model: options.model ?? null,
|
|
705
|
+
effort: typeof options.reasoningEffort === "string" && options.reasoningEffort !== "" ? options.reasoningEffort : null,
|
|
706
|
+
};
|
|
676
707
|
captureOf(sessionId, rawSnapshot, model, viewTokens);
|
|
677
708
|
metrics.noteView(sessionId, viewTokens, model);
|
|
678
709
|
const priorEntry = trimViewBySession.get(sessionId);
|
|
@@ -693,7 +724,10 @@ export function applyContextTrim(ctx, config) {
|
|
|
693
724
|
if (explicit !== null) return explicit;
|
|
694
725
|
const model = metrics.of(sessionId)?.lastModel ?? null;
|
|
695
726
|
if (model !== null && typeof model.provider === "string" && model.provider !== "" && typeof model.model === "string" && model.model !== "") {
|
|
696
|
-
|
|
727
|
+
// Inherit the session's own resolved effort when one was captured.
|
|
728
|
+
return typeof model.effort === "string" && model.effort !== ""
|
|
729
|
+
? { provider: model.provider, model: model.model, effort: model.effort }
|
|
730
|
+
: { provider: model.provider, model: model.model };
|
|
697
731
|
}
|
|
698
732
|
return null;
|
|
699
733
|
}
|
|
@@ -732,6 +766,7 @@ export function applyContextTrim(ctx, config) {
|
|
|
732
766
|
const summarize = (r, tag) => summarizeWithModel(llm, r, renderLadderMaterial(consumed), {
|
|
733
767
|
systemPrompt: LADDER_SYSTEM_PROMPT,
|
|
734
768
|
onFail: (why) => decisionLog(`session=${sessionId} summary-fail route=${tag} why=${why} blocks=${consumed.length} ~${approxTokens}t ladder=${decision.tier}`),
|
|
769
|
+
onNotice: (why) => decisionLog(`session=${sessionId} summary-retry route=${tag} why=${why} blocks=${consumed.length} ~${approxTokens}t ladder=${decision.tier}`),
|
|
735
770
|
});
|
|
736
771
|
let summary = route !== null ? await summarize(route, "side") : null;
|
|
737
772
|
if (summary === null && fallback !== null && !sameRoute(fallback, route)) summary = await summarize(fallback, "fallback");
|
|
@@ -778,6 +813,7 @@ export function applyContextTrim(ctx, config) {
|
|
|
778
813
|
const material = renderMaterial(capture.messages, startIdx, endIdx, refOf);
|
|
779
814
|
const summarize = (r, tag) => summarizeWithModel(llm, r, material, {
|
|
780
815
|
onFail: (why) => decisionLog(`session=${sessionId} summary-fail route=${tag} why=${why} msgs=${count} ~${approxTokens}t`),
|
|
816
|
+
onNotice: (why) => decisionLog(`session=${sessionId} summary-retry route=${tag} why=${why} msgs=${count} ~${approxTokens}t`),
|
|
781
817
|
});
|
|
782
818
|
summary = route !== null ? await summarize(route, "side") : null;
|
|
783
819
|
if (summary === null && fallback !== null && !sameRoute(fallback, route)) summary = await summarize(fallback, "fallback");
|
|
@@ -1145,14 +1181,19 @@ export function applyContextTrim(ctx, config) {
|
|
|
1145
1181
|
// Fallback fold check on the stream path: the agent/status idle event
|
|
1146
1182
|
// may not reach a globally-registered listener on every host topology,
|
|
1147
1183
|
// so each stream end also runs a throttled (60 s/session) check. The
|
|
1148
|
-
// trigger's own cadence gates still guard over-folding.
|
|
1184
|
+
// trigger's own cadence gates still guard over-folding. After the fold
|
|
1185
|
+
// check, chain a surface sweep: fold blocks commit mid-run even for
|
|
1186
|
+
// sessions whose dispatches are host-frozen (subagent children see
|
|
1187
|
+
// exactly ONE idle — at task end — so without this their wire ships
|
|
1188
|
+
// raw history until they finish; the sweep is the only relief channel
|
|
1189
|
+
// that reaches frozen dispatches). Same 60 s cadence, fail-open.
|
|
1149
1190
|
try {
|
|
1150
1191
|
if (enabledNow() && modeNow() === "auto" && wrappedSession !== null) {
|
|
1151
1192
|
const nowMs = Date.now();
|
|
1152
1193
|
if (nowMs - (lastStreamCheckAt.get(wrappedSession) ?? 0) >= 60_000) {
|
|
1153
1194
|
lastStreamCheckAt.set(wrappedSession, nowMs);
|
|
1154
1195
|
if (lastStreamCheckAt.size > 200) lastStreamCheckAt.delete(lastStreamCheckAt.keys().next().value);
|
|
1155
|
-
void maybeFold(wrappedSession, "stream");
|
|
1196
|
+
void maybeFold(wrappedSession, "stream").finally(() => surfacePass.sweep(wrappedSession, "stream"));
|
|
1156
1197
|
}
|
|
1157
1198
|
}
|
|
1158
1199
|
} catch {
|
|
@@ -67,8 +67,7 @@ export function createMetrics() {
|
|
|
67
67
|
cacheWriteTokens: null, // cache-write bucket of the last sample
|
|
68
68
|
outputTokens: null,
|
|
69
69
|
viewTokens: null, // view estimate noted by the listener pre-request
|
|
70
|
-
lastModel: null, // { provider, model } of the last stream
|
|
71
|
-
lastStreamAt: 0,
|
|
70
|
+
lastModel: null, // { provider, model, effort } of the last stream
|
|
72
71
|
totalStreams: 0,
|
|
73
72
|
};
|
|
74
73
|
}
|
|
@@ -87,8 +86,7 @@ export function createMetrics() {
|
|
|
87
86
|
noteView(sessionId, viewTokens, model) {
|
|
88
87
|
const record = recordOf(sessionId);
|
|
89
88
|
if (Number.isFinite(viewTokens)) record.viewTokens = Math.round(viewTokens);
|
|
90
|
-
if (model !== null && typeof model === "object") record.lastModel = { provider: model.provider ?? null, model: model.model ?? null };
|
|
91
|
-
record.lastStreamAt = Date.now();
|
|
89
|
+
if (model !== null && typeof model === "object") record.lastModel = { provider: model.provider ?? null, model: model.model ?? null, effort: typeof model.effort === "string" && model.effort !== "" ? model.effort : null };
|
|
92
90
|
record.totalStreams += 1;
|
|
93
91
|
return record;
|
|
94
92
|
},
|
|
@@ -110,18 +108,26 @@ export function createMetrics() {
|
|
|
110
108
|
}
|
|
111
109
|
|
|
112
110
|
/** Wrap a stream result so each chunk flows through a (failing-open)
|
|
113
|
-
* sampler before being yielded.
|
|
114
|
-
*
|
|
115
|
-
export function wrapStreamForMetrics(result, onChunk) {
|
|
111
|
+
* sampler before being yielded. An optional completion callback runs when
|
|
112
|
+
* iteration finishes or is closed; non-stream fallbacks pass through untouched. */
|
|
113
|
+
export function wrapStreamForMetrics(result, onChunk, onFinally = null) {
|
|
116
114
|
if (result === null || typeof result !== "object" || typeof result[Symbol.asyncIterator] !== "function") return result;
|
|
117
115
|
async function* sampled() {
|
|
118
|
-
|
|
116
|
+
try {
|
|
117
|
+
for await (const chunk of result) {
|
|
118
|
+
try {
|
|
119
|
+
onChunk(chunk);
|
|
120
|
+
} catch {
|
|
121
|
+
/* sampling must never break the stream */
|
|
122
|
+
}
|
|
123
|
+
yield chunk;
|
|
124
|
+
}
|
|
125
|
+
} finally {
|
|
119
126
|
try {
|
|
120
|
-
|
|
127
|
+
onFinally?.();
|
|
121
128
|
} catch {
|
|
122
|
-
/*
|
|
129
|
+
/* completion callbacks must never break the stream */
|
|
123
130
|
}
|
|
124
|
-
yield chunk;
|
|
125
131
|
}
|
|
126
132
|
}
|
|
127
133
|
return sampled();
|