turbollm 1.10.4 → 1.10.6

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 (122) hide show
  1. package/dist/cli.js +433 -137
  2. package/dist/webdist/assets/{AgentEditPage-DZU5Ps30.js → AgentEditPage-Cjrahm7d.js} +1 -1
  3. package/dist/webdist/assets/{ChatScreen-CWjg1zrs.js → ChatScreen-BE2j_M7n.js} +9 -9
  4. package/dist/webdist/assets/{CodeComposer-C8c0eQe4.js → CodeComposer-CB0nORiZ.js} +1 -1
  5. package/dist/webdist/assets/CodeHomeScreen-Bh0BY6aY.js +1 -0
  6. package/dist/webdist/assets/{CodeSessionScreen-DOVBiJIZ.js → CodeSessionScreen-MM4gt_kH.js} +11 -11
  7. package/dist/webdist/assets/{CustomizeScreen-Dg69A2mU.js → CustomizeScreen-zvF_dtiC.js} +1 -1
  8. package/dist/webdist/assets/{DeveloperScreen-CwW1Z3BJ.js → DeveloperScreen-DXzdDB5X.js} +1 -1
  9. package/dist/webdist/assets/{EnginesScreen-CiMymt2y.js → EnginesScreen-CLkBmRVD.js} +1 -1
  10. package/dist/webdist/assets/{ModelDetailDialog-Ch1FFdDi.js → ModelDetailDialog-CL8_8e9a.js} +1 -1
  11. package/dist/webdist/assets/{ModelLoadMenu-DmJ6pW_S.js → ModelLoadMenu-C6W2D_HT.js} +1 -1
  12. package/dist/webdist/assets/{ModelsScreen-Ch83Gnvx.js → ModelsScreen-CMEUd2XW.js} +1 -1
  13. package/dist/webdist/assets/RoutineEditPage-DNt9jwLQ.js +1 -0
  14. package/dist/webdist/assets/RoutinesPanel-APLk8zAj.js +1 -0
  15. package/dist/webdist/assets/{SettingsScreen-fWaRXbXZ.js → SettingsScreen-CNEhuloT.js} +1 -1
  16. package/dist/webdist/assets/SidebarResizeHandle-BGh24zM4.js +2 -0
  17. package/dist/webdist/assets/{SkillEditPage-Cpg3YkPw.js → SkillEditPage-QktxVVZP.js} +1 -1
  18. package/dist/webdist/assets/{TokensScreen-CYGdXUbH.js → TokensScreen-BQu-wJ5j.js} +1 -1
  19. package/dist/webdist/assets/{ToolApprovalBar-Ba2BoF9X.js → ToolApprovalBar-WuURxg64.js} +3 -3
  20. package/dist/webdist/assets/WorkspaceScreen-DR9iCevI.js +1 -0
  21. package/dist/webdist/assets/{abnfDiagram-N423BO3Z-CxAZuY4F.js → abnfDiagram-N423BO3Z-Xdm1Ge5Z.js} +1 -1
  22. package/dist/webdist/assets/{agent-api-BdnGAV-Y.js → agent-api-Dsb2M3gT.js} +1 -1
  23. package/dist/webdist/assets/{alert-dialog-CEf-XUyg.js → alert-dialog-CLYOFUQB.js} +1 -1
  24. package/dist/webdist/assets/{arc-DEU8Rl3Q.js → arc-D2e7_fvj.js} +1 -1
  25. package/dist/webdist/assets/{architectureDiagram-T3A2C74G-DFWEfaYa.js → architectureDiagram-T3A2C74G-NOGk06k9.js} +1 -1
  26. package/dist/webdist/assets/{arrow-left-BHr6DbXW.js → arrow-left-Be5kJCK_.js} +1 -1
  27. package/dist/webdist/assets/{badge-DiZD__AA.js → badge-ZKEdg9KY.js} +1 -1
  28. package/dist/webdist/assets/{blockDiagram-VBNYF7ZC-BhTU3G_O.js → blockDiagram-VBNYF7ZC-DmcFoOYB.js} +1 -1
  29. package/dist/webdist/assets/{bot-BR1o3u2z.js → bot-oaBq4kgz.js} +1 -1
  30. package/dist/webdist/assets/{c4Diagram-5PPSVZJV-CJNhzTh5.js → c4Diagram-5PPSVZJV-zzYM53Qc.js} +1 -1
  31. package/dist/webdist/assets/channel-BdTqTBNl.js +1 -0
  32. package/dist/webdist/assets/{chat-api-WdKnt_cR.js → chat-api-D7WunS4X.js} +1 -1
  33. package/dist/webdist/assets/{chat-queries--BKqKvZT.js → chat-queries-DfV_X46e.js} +1 -1
  34. package/dist/webdist/assets/{chevron-left-tBRMVkX5.js → chevron-left-DqcpOb9h.js} +1 -1
  35. package/dist/webdist/assets/{chevron-right-B16ur-An.js → chevron-right-D7i8peZR.js} +1 -1
  36. package/dist/webdist/assets/{chunk-2GRJ4B5K-Y-VLx_rW.js → chunk-2GRJ4B5K-tWNhRyV6.js} +1 -1
  37. package/dist/webdist/assets/{chunk-2Q5K7J3B-BUKWb833.js → chunk-2Q5K7J3B-jT4PFPv5.js} +1 -1
  38. package/dist/webdist/assets/{chunk-5RXB4S5H-BV3L2yyf.js → chunk-5RXB4S5H-CBgg6cvO.js} +1 -1
  39. package/dist/webdist/assets/{chunk-5VM5RSS4-xI1dTTCA.js → chunk-5VM5RSS4-Do3-h3Xi.js} +1 -1
  40. package/dist/webdist/assets/{chunk-6Q2QTUOP-C3F611uv.js → chunk-6Q2QTUOP-Y4iDp1I8.js} +1 -1
  41. package/dist/webdist/assets/{chunk-GF5L2VYU-xzyWIvtP.js → chunk-GF5L2VYU-Bj41431x.js} +1 -1
  42. package/dist/webdist/assets/{chunk-JWPE2WC7-PCn1DNAq.js → chunk-JWPE2WC7-CufywS1e.js} +1 -1
  43. package/dist/webdist/assets/{chunk-KBJHAD2P-B2V4Qn1_.js → chunk-KBJHAD2P-BPLIKhbV.js} +1 -1
  44. package/dist/webdist/assets/{chunk-RYQCIY6F-BXnb7awQ.js → chunk-RYQCIY6F-CmcnhnsQ.js} +1 -1
  45. package/dist/webdist/assets/{chunk-XXDRQBXY-BjzDsMy6.js → chunk-XXDRQBXY-onzm5o_Q.js} +1 -1
  46. package/dist/webdist/assets/{circle-check-BHQ4Ky5u.js → circle-check-BY8jczPf.js} +1 -1
  47. package/dist/webdist/assets/{circle-x-BbJarEnu.js → circle-x-BUlKg2cZ.js} +1 -1
  48. package/dist/webdist/assets/classDiagram-JCYQIIEL-R1cuP_Pr.js +1 -0
  49. package/dist/webdist/assets/classDiagram-v2-OCEON4UE-R1cuP_Pr.js +1 -0
  50. package/dist/webdist/assets/{common-B_Ugw5Ue.js → common-BsHtz98M.js} +1 -1
  51. package/dist/webdist/assets/{cose-bilkent-JH36ORCC-8GVWCg2F.js → cose-bilkent-JH36ORCC-8JLhJahr.js} +1 -1
  52. package/dist/webdist/assets/{cynefin-VYW2F7L2-D4EwNxuN.js → cynefin-VYW2F7L2-CBL5ANhm.js} +1 -1
  53. package/dist/webdist/assets/{cynefinDiagram-MW4NZA55-BchWtjzK.js → cynefinDiagram-MW4NZA55-Cmwj48UP.js} +1 -1
  54. package/dist/webdist/assets/{dagre-VZM6K2ZE-C-tKBzor.js → dagre-VZM6K2ZE-DngjyEsb.js} +1 -1
  55. package/dist/webdist/assets/{diagram-7IWD3JNH-CIzXwEou.js → diagram-7IWD3JNH-OQyUakUJ.js} +1 -1
  56. package/dist/webdist/assets/{diagram-B4RE2ZJO-B0-fu81p.js → diagram-B4RE2ZJO-uWCxDSmb.js} +1 -1
  57. package/dist/webdist/assets/{diagram-LBJQPF4R-DKUCmHSn.js → diagram-LBJQPF4R-BYhk_VPg.js} +1 -1
  58. package/dist/webdist/assets/{diagram-Q27KOJAE-BMVmaaBq.js → diagram-Q27KOJAE-CzML3pPm.js} +1 -1
  59. package/dist/webdist/assets/{diagram-UB23O5K3-C-GsMRNn.js → diagram-UB23O5K3-DIL750-6.js} +1 -1
  60. package/dist/webdist/assets/{dropdown-menu-BHlT_PrH.js → dropdown-menu-CCRmjOB8.js} +1 -1
  61. package/dist/webdist/assets/{ebnfDiagram-BXEA7PRR-D2cgRlFo.js → ebnfDiagram-BXEA7PRR-IOFFHUjP.js} +1 -1
  62. package/dist/webdist/assets/{erDiagram-JOGREHBK-MG0Qt0Dc.js → erDiagram-JOGREHBK-CsYREjU2.js} +1 -1
  63. package/dist/webdist/assets/{external-link-BYsIzaRn.js → external-link-CwcqLXeC.js} +1 -1
  64. package/dist/webdist/assets/{file-text-D5LVQPdf.js → file-text-CfktJNKt.js} +1 -1
  65. package/dist/webdist/assets/{flowDiagram-UKHOOZJN-wAus-SqB.js → flowDiagram-UKHOOZJN-D8V23snk.js} +1 -1
  66. package/dist/webdist/assets/{folder-input-CPFYQQYm.js → folder-input-CAX7jGqy.js} +1 -1
  67. package/dist/webdist/assets/{folder-plus-BS4MX3uc.js → folder-plus-DRH9A_nf.js} +1 -1
  68. package/dist/webdist/assets/{ganttDiagram-PKOTCBZU-iFTGTcny.js → ganttDiagram-PKOTCBZU-BNFLscau.js} +1 -1
  69. package/dist/webdist/assets/{git-branch-CH_JRbeH.js → git-branch-D3SWa_HB.js} +1 -1
  70. package/dist/webdist/assets/{gitGraphDiagram-DS77QQ5N-B3Lim-un.js → gitGraphDiagram-DS77QQ5N-CACE08jS.js} +1 -1
  71. package/dist/webdist/assets/{index-m7O9h-cc.js → index-C3DcPrhr.js} +6 -6
  72. package/dist/webdist/assets/{infoDiagram-6WML65LV-2U3_w76t.js → infoDiagram-6WML65LV-Bgac4fco.js} +1 -1
  73. package/dist/webdist/assets/{ishikawaDiagram-WSZJBQD7-8qFZeY06.js → ishikawaDiagram-WSZJBQD7-B-Gx-xHY.js} +1 -1
  74. package/dist/webdist/assets/{journeyDiagram-NVQOT4AX-BiQ4W5bp.js → journeyDiagram-NVQOT4AX-HkIDa9gF.js} +1 -1
  75. package/dist/webdist/assets/{kanban-definition-27J2QSJJ-xUKbXLDF.js → kanban-definition-27J2QSJJ-D9fCO--l.js} +1 -1
  76. package/dist/webdist/assets/{linear-Cfppb_KP.js → linear-BCzUyXir.js} +1 -1
  77. package/dist/webdist/assets/{lock-_kPFqMMF.js → lock-DU6baFK9.js} +1 -1
  78. package/dist/webdist/assets/{mermaid.core-Cqc4S48f.js → mermaid.core-DKhj1zV1.js} +4 -4
  79. package/dist/webdist/assets/{mindmap-definition-FAOFIHXS-DfAXBfEp.js → mindmap-definition-FAOFIHXS-DStYIFGn.js} +1 -1
  80. package/dist/webdist/assets/{pegDiagram-VL7TDLO6-BDnh7A9L.js → pegDiagram-VL7TDLO6-CMkk8HUV.js} +1 -1
  81. package/dist/webdist/assets/{pencil-B65o2Xsj.js → pencil-Bt-RSZUH.js} +1 -1
  82. package/dist/webdist/assets/{personas-B0p0BX69.js → personas-DLa9ZPoi.js} +4 -4
  83. package/dist/webdist/assets/{pieDiagram-7S7Q4E2Y-BoEHVtTF.js → pieDiagram-7S7Q4E2Y-BaK0uEYv.js} +1 -1
  84. package/dist/webdist/assets/{plus-BCCJyhZC.js → plus-CFBywfW5.js} +1 -1
  85. package/dist/webdist/assets/{quadrantDiagram-CIZ2JOQS-BvKFv-w0.js → quadrantDiagram-CIZ2JOQS-C34UZIt1.js} +1 -1
  86. package/dist/webdist/assets/{railroadDiagram-AXF67PYL-BLrX-HQS.js → railroadDiagram-AXF67PYL-Ckb23iRj.js} +1 -1
  87. package/dist/webdist/assets/{refresh-cw-BMe9yq9k.js → refresh-cw-BjYW1yib.js} +1 -1
  88. package/dist/webdist/assets/{requirementDiagram-LRYGKXZP-r4LjKdJa.js → requirementDiagram-LRYGKXZP-CG2IfNZ8.js} +1 -1
  89. package/dist/webdist/assets/{rotate-ccw-QlaGmqOe.js → rotate-ccw-CDrolPd6.js} +1 -1
  90. package/dist/webdist/assets/{sankeyDiagram-W5VNT64P-CbTbNO1R.js → sankeyDiagram-W5VNT64P-C37_2Ghs.js} +1 -1
  91. package/dist/webdist/assets/{sequenceDiagram-SI44F4Z6-C3cZYRGT.js → sequenceDiagram-SI44F4Z6-Cbd9Ydzi.js} +1 -1
  92. package/dist/webdist/assets/{sizeCapture-X5ZJPWSS-CYYQ8wtn.js → sizeCapture-X5ZJPWSS-Db4VW9lY.js} +1 -1
  93. package/dist/webdist/assets/{skeleton-CC9ODrk4.js → skeleton-C8GLzn0j.js} +1 -1
  94. package/dist/webdist/assets/{sliders-horizontal-BFSPlX5c.js → sliders-horizontal-ChT1DRyQ.js} +1 -1
  95. package/dist/webdist/assets/{sparkles-CmEUYmzj.js → sparkles-BgaRXk3Z.js} +1 -1
  96. package/dist/webdist/assets/{star-CyudL0Do.js → star-CVPXfTy4.js} +1 -1
  97. package/dist/webdist/assets/{stateDiagram-OKZ733FA-Bdj39Nof.js → stateDiagram-OKZ733FA-RgxA3rS_.js} +1 -1
  98. package/dist/webdist/assets/stateDiagram-v2-UEYNNEHI-DW4K46xR.js +1 -0
  99. package/dist/webdist/assets/{swimlanes-SLNWSIFB-W3xWeGOJ.js → swimlanes-SLNWSIFB-PmBwCiF3.js} +2 -2
  100. package/dist/webdist/assets/swimlanesDiagram-ULZ7WXOC--nEBdjnh.js +8 -0
  101. package/dist/webdist/assets/{switch-BDarVW8W.js → switch-CjDd4avM.js} +1 -1
  102. package/dist/webdist/assets/{terminal-Bvnu3OHJ.js → terminal-z7vkSxg1.js} +1 -1
  103. package/dist/webdist/assets/{timeline-definition-Z64GVDOM-DnavzBwI.js → timeline-definition-Z64GVDOM-vxaLdkHH.js} +1 -1
  104. package/dist/webdist/assets/{trash-2-C43ex79G.js → trash-2-BNqXKVdk.js} +1 -1
  105. package/dist/webdist/assets/{useIsDesktop-BC85xPnA.js → useIsDesktop-CRP_qB3o.js} +1 -1
  106. package/dist/webdist/assets/{vennDiagram-T6HMQDX7-DdJs_uLj.js → vennDiagram-T6HMQDX7-CY5Ha4JW.js} +1 -1
  107. package/dist/webdist/assets/{vram-BWaHTGD0.js → vram-C6uGfhzc.js} +1 -1
  108. package/dist/webdist/assets/{wardleyDiagram-T6FBY63Y-91gPbjKx.js → wardleyDiagram-T6FBY63Y-DQzV926_.js} +1 -1
  109. package/dist/webdist/assets/{wrench-Dlm2fJiY.js → wrench-Db816rX1.js} +1 -1
  110. package/dist/webdist/assets/{xychartDiagram-ELKLHX3M-DDKDUQ8u.js → xychartDiagram-ELKLHX3M-B7OfhEpC.js} +1 -1
  111. package/dist/webdist/index.html +1 -1
  112. package/package.json +1 -1
  113. package/dist/webdist/assets/CodeHomeScreen-BbrTdGuL.js +0 -1
  114. package/dist/webdist/assets/RoutineEditPage-DqDEDN7O.js +0 -1
  115. package/dist/webdist/assets/RoutinesPanel-C0CgIiju.js +0 -1
  116. package/dist/webdist/assets/SidebarResizeHandle-eJlzaNXV.js +0 -2
  117. package/dist/webdist/assets/WorkspaceScreen-CLXpSocV.js +0 -1
  118. package/dist/webdist/assets/channel-D7Or72nc.js +0 -1
  119. package/dist/webdist/assets/classDiagram-JCYQIIEL-CoD8BgVw.js +0 -1
  120. package/dist/webdist/assets/classDiagram-v2-OCEON4UE-CoD8BgVw.js +0 -1
  121. package/dist/webdist/assets/stateDiagram-v2-UEYNNEHI-B3xKgUl_.js +0 -1
  122. package/dist/webdist/assets/swimlanesDiagram-ULZ7WXOC-DSpzEq_Q.js +0 -8
package/dist/cli.js CHANGED
@@ -5753,6 +5753,12 @@ var SUPPORTED = {
5753
5753
  hermes: { bin: "hermes", label: "Hermes Agent", install: "npm install -g hermes-agent", prepareConfig: prepareHermes },
5754
5754
  pi: { bin: "pi", label: "pi", install: "npm install -g @earendil-works/pi-coding-agent", prepareConfig: preparePi }
5755
5755
  };
5756
+ var CLAUDE_AUTO_COMPACT_WINDOW_MIN = 1e5;
5757
+ var CLAUDE_AUTO_COMPACT_WINDOW_MAX = 1e6;
5758
+ function clampAutoCompactWindow(ctx) {
5759
+ return Math.min(CLAUDE_AUTO_COMPACT_WINDOW_MAX, Math.max(CLAUDE_AUTO_COMPACT_WINDOW_MIN, Math.round(ctx)));
5760
+ }
5761
+ var CLAUDE_AUTOCOMPACT_PCT_OVERRIDE = "80";
5756
5762
  var realSpawn = (cmd, args, opts) => {
5757
5763
  if (!opts?.shell) return spawn4(cmd, args, opts);
5758
5764
  const resolved = resolveExecutable(cmd);
@@ -6180,6 +6186,15 @@ Open ${base2} \u2192 Models \u2192 Load a model, then run this again.
6180
6186
  // Opt into gateway model discovery: Claude Code queries our /v1/models at startup and
6181
6187
  // populates the /model picker with the local library (gateway synthesises the entries).
6182
6188
  CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY: "1",
6189
+ // Founder-reported: the ctx-fill display "jumps between full and empty" and the CLI "never
6190
+ // auto-compacts at 80%, always reaches 100% and fails". The second half is Claude Code
6191
+ // assuming a generic ~200K context window for our unrecognized model id and only compacting
6192
+ // near THAT — see clampAutoCompactWindow's own doc comment for the full root cause and its
6193
+ // documented [100_000, 1_000_000] floor/ceiling. Only set when the daemon actually reports a
6194
+ // real ctx (status.model.ctx) — an absent/zero value means "don't know", and guessing a
6195
+ // window here would be worse than leaving the CLI's own generic default in place.
6196
+ ...status.model?.ctx ? { CLAUDE_CODE_AUTO_COMPACT_WINDOW: String(clampAutoCompactWindow(status.model.ctx)) } : {},
6197
+ CLAUDE_AUTOCOMPACT_PCT_OVERRIDE,
6183
6198
  // Cap background-agent fan-out at what the engine can actually run concurrently.
6184
6199
  //
6185
6200
  // Claude Code spawns subagents in parallel, and each is a full, independent request to the
@@ -7140,13 +7155,16 @@ var AUTO_MODE_ALLOWED_TOOLS = {
7140
7155
  claude: ["WebSearch", "WebFetch"]
7141
7156
  };
7142
7157
  var MAX_SEEDED_MESSAGE_CHARS = 4e3;
7143
- var UNSEEDABLE = /["\u0000-\u001f]/;
7158
+ var UNSEEDABLE = /["\u0000-\u0008\u000b\u000c\u000e-\u001f]/;
7159
+ function normalizeSeededMessage(message) {
7160
+ return message.replace(/[\r\n\t]+/g, " ").trim();
7161
+ }
7144
7162
  function canSeedFirstMessage(message, agent) {
7145
7163
  if (agent !== "claude") return false;
7146
- const trimmed = message.trim();
7147
- if (trimmed.length === 0 || trimmed.length > MAX_SEEDED_MESSAGE_CHARS) return false;
7148
- if (UNSEEDABLE.test(trimmed)) return false;
7149
- if (trimmed.endsWith("\\")) return false;
7164
+ const normalized = normalizeSeededMessage(message);
7165
+ if (normalized.length === 0 || normalized.length > MAX_SEEDED_MESSAGE_CHARS) return false;
7166
+ if (UNSEEDABLE.test(normalized)) return false;
7167
+ if (normalized.endsWith("\\")) return false;
7150
7168
  return true;
7151
7169
  }
7152
7170
  function buildTerminalLaunchCommand(agent, port2, token, sessionId, launchedOnce, permissionMode, mode, firstMessage) {
@@ -7155,7 +7173,7 @@ function buildTerminalLaunchCommand(agent, port2, token, sessionId, launchedOnce
7155
7173
  const modeArg = permissionMode ? ` --permission-mode ${permissionMode}` : "";
7156
7174
  const allowed = mode === "auto" ? AUTO_MODE_ALLOWED_TOOLS[agent] : void 0;
7157
7175
  const allowedArg = allowed?.length ? ` --allowedTools ${allowed.join(",")}` : "";
7158
- const promptArg = firstMessage && canSeedFirstMessage(firstMessage, agent) ? ` ${quotePtyShellArg(firstMessage.trim())}` : "";
7176
+ const promptArg = firstMessage && canSeedFirstMessage(firstMessage, agent) ? ` ${quotePtyShellArg(normalizeSeededMessage(firstMessage))}` : "";
7159
7177
  return `turbollm launch ${agent}${promptArg} --port ${port2} --token ${token}${sessionArg}${modeArg}${allowedArg}`;
7160
7178
  }
7161
7179
  function err(c, s, code, msg2) {
@@ -13488,6 +13506,10 @@ var ConversationStore = class {
13488
13506
  if (!this.hasColumn("api_usage", "harness")) this.db.exec(`ALTER TABLE api_usage ADD COLUMN harness TEXT;`);
13489
13507
  this.db.exec(`PRAGMA user_version = 43;`);
13490
13508
  }
13509
+ if (v < 44) {
13510
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_api_usage_code_session_tokens ON api_usage(code_session_id, prompt_tokens);`);
13511
+ this.db.exec(`PRAGMA user_version = 44;`);
13512
+ }
13491
13513
  }
13492
13514
  listConversations(q, kind = "all") {
13493
13515
  if (q) {
@@ -13874,10 +13896,37 @@ var ConversationStore = class {
13874
13896
  $harness: rec.harness ?? null
13875
13897
  });
13876
13898
  }
13877
- /** Most recent completed gateway request attributed to a terminal-agent Code session
13878
- * (ADR-284) — powers TerminalToolbar.tsx's composer-parity stats row the same way
13879
- * `lastRealStats` already does for a 'turbollm' chat session's last turn. null when the
13880
- * session has made no gateway requests yet (fresh session, or a non-terminal-agent one). */
13899
+ /** The session's own gateway-request stats (ADR-284, revised twice — founder-reported "ctx
13900
+ * fill jumps between full and empty in the same session", then a review pass on that same fix
13901
+ * catching that it had gone on to freeze the LAST-TURN stats too). Two different questions,
13902
+ * two different rows:
13903
+ *
13904
+ * `ctxUsed` is the MAX prompt_tokens row, not literally the most-recently-INSERTED one. Every
13905
+ * request a real `claude` CLI process makes — the main conversation's own turn AND every
13906
+ * parallel Task-tool sub-agent it spawns — shares the SAME code_session_id: `resolveCodeSession`
13907
+ * (gateway.ts) derives it purely from the bearer token, and the CLI mints exactly one token
13908
+ * (ANTHROPIC_AUTH_TOKEN, set once at launch) for its whole process, sub-agents included. A
13909
+ * sub-agent's own prompt is a small, isolated sub-task — nothing like the full resent
13910
+ * conversation — so whichever of the two finishes last used to decide what the ring showed:
13911
+ * the real, large, ever-growing main-turn prompt one poll, a tiny sub-agent prompt the next,
13912
+ * with no change to the actual conversation in between. The MAX is the honest answer to "how
13913
+ * full has this session's context gotten" — a real Claude Code CLI resends the whole
13914
+ * conversation every turn, so its size only grows within one continuous conversation, and a
13915
+ * sub-agent (always smaller) can never legitimately raise that high-water mark. Trade-off,
13916
+ * accepted: after a real `/clear` inside the CLI (invisible to the daemon — a terminal-agent
13917
+ * session's turns are never persisted here, see ADR-297), this keeps reporting the pre-clear
13918
+ * high-water mark until the new conversation grows past it. A temporarily-stale-high ring
13919
+ * reading after a rare, deliberate user action beats an unpredictable full/empty flicker on
13920
+ * every ordinary agentic turn with sub-agents.
13921
+ *
13922
+ * `promptTokens`/`genTokens`/`promptTps`/`genTps` are the temporally-LAST row instead — these
13923
+ * power TerminalToolbar.tsx's composer-parity stats row, which reads as "your last turn", not
13924
+ * "session high-water mark". Sharing the MAX row here (the original version of this fix) meant
13925
+ * a sub-agent call landing after the real turn hid its own genTokens/tps behind the earlier,
13926
+ * larger turn's numbers, and after a `/clear` these froze at the pre-clear turn's numbers
13927
+ * forever, same failure shape as the ctxUsed bug this method was written to fix in the first
13928
+ * place. null when the session has made no gateway requests yet (fresh session, or a
13929
+ * non-terminal-agent one). */
13881
13930
  getLastApiUsageForSession(codeSessionId) {
13882
13931
  const row = this.db.prepare(`
13883
13932
  SELECT prompt_tokens, gen_tokens, duration_ms, prompt_tps, gen_tps FROM api_usage
@@ -13885,8 +13934,12 @@ var ConversationStore = class {
13885
13934
  ORDER BY created_at DESC LIMIT 1
13886
13935
  `).get({ $codeSessionId: codeSessionId });
13887
13936
  if (!row) return null;
13937
+ const ctxUsed = this.db.prepare(`
13938
+ SELECT MAX(prompt_tokens) as maxPromptTokens FROM api_usage WHERE code_session_id = $codeSessionId
13939
+ `).get({ $codeSessionId: codeSessionId }).maxPromptTokens;
13888
13940
  const seconds = row.duration_ms != null && row.duration_ms > 0 ? row.duration_ms / 1e3 : null;
13889
13941
  return {
13942
+ ctxUsed,
13890
13943
  promptTokens: row.prompt_tokens,
13891
13944
  genTokens: row.gen_tokens,
13892
13945
  promptTps: row.prompt_tps ?? (seconds ? row.prompt_tokens / seconds : null),
@@ -15549,6 +15602,17 @@ import { execFile as execFile10 } from "child_process";
15549
15602
  import { readFileSync as readFileSync12 } from "fs";
15550
15603
  import { randomUUID as randomUUID6 } from "crypto";
15551
15604
 
15605
+ // src/bench/spill.ts
15606
+ var SPILL_TOLERANCE_MB = 512;
15607
+ function spillMb(baselineSharedMb, loadedSharedMb) {
15608
+ if (baselineSharedMb === null || loadedSharedMb === null) return null;
15609
+ return Math.max(0, loadedSharedMb - baselineSharedMb);
15610
+ }
15611
+ function isSpilling(spill) {
15612
+ if (spill === null) return false;
15613
+ return spill > SPILL_TOLERANCE_MB;
15614
+ }
15615
+
15552
15616
  // src/api/path-utils.ts
15553
15617
  function inferRepoFromPath(filePath, modelDirs) {
15554
15618
  const norm = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "");
@@ -16476,7 +16540,6 @@ var PREFILL_GRACE_MS = 8e3;
16476
16540
  var TOTAL_BUDGET_MS = 45 * 6e4;
16477
16541
  var OOM_RE = /out of memory|cudaMalloc|failed to allocate|unable to allocate|device not ready|CUDA error/i;
16478
16542
  var QUALITY_KV = ["f16", "q8_0", "turbo4"];
16479
- var OUTPUT_TIE = 0.05;
16480
16543
  var KV_BYTES = {
16481
16544
  f32: 4,
16482
16545
  f16: 2,
@@ -16749,16 +16812,28 @@ var BenchRunner = class {
16749
16812
  const probe2 = await this.probeVram(entry, sys, { ...base2, ngl: mid }, caps);
16750
16813
  this.pushProbe(results, base2, "ngl", mid, probe2);
16751
16814
  await this.settleGpu(sys);
16752
- if (probe2.outcome === "ok" && !overHeadroom(probe2.vramAbsMb, budgetMb, headroomMb)) {
16815
+ const verdict = probeVerdict(probe2, budgetMb, headroomMb);
16816
+ if (verdict.decision === "fits") {
16753
16817
  bestNgl = mid;
16754
16818
  lo = mid + 1;
16755
16819
  } else {
16820
+ if (verdict.reason === "spill") {
16821
+ this.emit(`ngl=${mid} spilling ${Math.round(verdict.shortfallMb ?? 0)} MB to system RAM \u2014 using fewer GPU layers`);
16822
+ }
16756
16823
  hi = mid - 1;
16757
16824
  }
16758
16825
  }
16759
16826
  }
16760
16827
  if (bestNgl === null) return null;
16761
16828
  let found = await this.benchAt(entry, sys, { ...base2, ngl: bestNgl }, caps, results, `ngl=${bestNgl}`);
16829
+ if (found && isSpilling(found.cand.spillMb) && bestNgl > 0) {
16830
+ this.emit(`ngl=${bestNgl} spilled ${Math.round(found.cand.spillMb)} MB once actually generating (the load-only probe could not see this) \u2014 retrying at ngl=${bestNgl - 1}`);
16831
+ const safer = await this.benchAt(entry, sys, { ...base2, ngl: bestNgl - 1 }, caps, results, `ngl=${bestNgl - 1} (spill backoff)`);
16832
+ if (safer) {
16833
+ found = safer;
16834
+ bestNgl = bestNgl - 1;
16835
+ }
16836
+ }
16762
16837
  if (found && overHeadroom(found.cand.vramAbsMb, budgetMb, headroomMb) && bestNgl > 0) {
16763
16838
  this.emit(`ngl=${bestNgl} exceeded headroom after generation (${found.cand.vramAbsMb} MB) \u2014 retrying at ngl=${bestNgl - 1}`);
16764
16839
  const safer = await this.benchAt(entry, sys, { ...base2, ngl: bestNgl - 1 }, caps, results, `ngl=${bestNgl - 1} (headroom backoff)`);
@@ -16792,32 +16867,61 @@ var BenchRunner = class {
16792
16867
  const probe2 = await this.probeVram(entry, sys, { ...base2, nCpuMoe: mid }, caps);
16793
16868
  this.pushProbe(results, base2, "nCpuMoe", mid, probe2);
16794
16869
  await this.settleGpu(sys);
16795
- if (probe2.outcome === "oom" || overHeadroom(probe2.vramAbsMb, budgetMb, headroomMb)) {
16796
- lo = mid + 1;
16797
- } else if (probe2.outcome === "ok") {
16870
+ const verdict = probeVerdict(probe2, budgetMb, headroomMb);
16871
+ if (verdict.decision === "fits") {
16798
16872
  bestN = mid;
16799
16873
  hi = mid - 1;
16800
16874
  } else {
16875
+ if (verdict.reason === "spill") {
16876
+ this.emit(`nCpuMoe=${mid} spilling ${Math.round(verdict.shortfallMb ?? 0)} MB to system RAM \u2014 offloading more`);
16877
+ }
16801
16878
  lo = mid + 1;
16802
16879
  }
16803
16880
  }
16804
16881
  if (bestN === null) return null;
16805
- let found = await this.benchAt(entry, sys, { ...base2, nCpuMoe: bestN }, caps, results, `nCpuMoe=${bestN}`);
16806
- if (found && overHeadroom(found.cand.vramAbsMb, budgetMb, headroomMb) && bestN < maxN) {
16807
- this.emit(`nCpuMoe=${bestN} exceeded headroom after generation (${found.cand.vramAbsMb} MB) \u2014 retrying at nCpuMoe=${bestN + 1}`);
16808
- const safer = await this.benchAt(entry, sys, { ...base2, nCpuMoe: bestN + 1 }, caps, results, `nCpuMoe=${bestN + 1} (headroom backoff)`);
16809
- if (safer) found = safer;
16882
+ let found = null;
16883
+ let foundN = bestN;
16884
+ for (let n = bestN; n <= maxN; n++) {
16885
+ const label = n === bestN ? `nCpuMoe=${n}` : `nCpuMoe=${n} (crash backoff)`;
16886
+ found = await this.benchAt(entry, sys, { ...base2, nCpuMoe: n }, caps, results, label);
16887
+ if (found) {
16888
+ foundN = n;
16889
+ break;
16890
+ }
16891
+ const stopped = this.cancelled || Date.now() > this.deadline;
16892
+ if (stopped) {
16893
+ this.emit(`nCpuMoe=${n} failed to bench (${results[results.length - 1]?.outcome ?? "crash"}) \u2014 run ${this.cancelled ? "cancelled" : "out of time budget"}, stopping`);
16894
+ break;
16895
+ }
16896
+ if (n < maxN) this.emit(`nCpuMoe=${n} failed to bench (${results[results.length - 1]?.outcome ?? "crash"}) \u2014 retrying at nCpuMoe=${n + 1}`);
16897
+ }
16898
+ if (found && isSpilling(found.cand.spillMb) && foundN < maxN) {
16899
+ this.emit(`nCpuMoe=${foundN} spilled ${Math.round(found.cand.spillMb)} MB once actually generating (the load-only probe could not see this) \u2014 retrying at nCpuMoe=${foundN + 1}`);
16900
+ const safer = await this.benchAt(entry, sys, { ...base2, nCpuMoe: foundN + 1 }, caps, results, `nCpuMoe=${foundN + 1} (spill backoff)`);
16901
+ if (safer) {
16902
+ found = safer;
16903
+ foundN = foundN + 1;
16904
+ }
16905
+ }
16906
+ if (found && overHeadroom(found.cand.vramAbsMb, budgetMb, headroomMb) && foundN < maxN) {
16907
+ this.emit(`nCpuMoe=${foundN} exceeded headroom after generation (${found.cand.vramAbsMb} MB) \u2014 retrying at nCpuMoe=${foundN + 1}`);
16908
+ const safer = await this.benchAt(entry, sys, { ...base2, nCpuMoe: foundN + 1 }, caps, results, `nCpuMoe=${foundN + 1} (headroom backoff)`);
16909
+ if (safer) {
16910
+ found = safer;
16911
+ foundN = foundN + 1;
16912
+ }
16810
16913
  }
16811
16914
  if (headroomMb === VRAM_HEADROOM_SPILL_MB && found && found.cand.tps !== null) {
16812
16915
  let current = found;
16813
16916
  while (current.cand.params.nCpuMoe > 0 && !this.cancelled && Date.now() <= this.deadline) {
16814
16917
  const spillN = current.cand.params.nCpuMoe - 1;
16815
- const prev = { tps: current.cand.tps, prefillTps: current.cand.prefillTps };
16816
- const prevLabel = `${prev.tps.toFixed(1)} tok/s (prefill ${prev.prefillTps !== null ? prev.prefillTps.toFixed(1) : "n/a"} tok/s)`;
16918
+ const prev = { ttftMs: current.cand.ttftMs, tps: current.cand.tps };
16919
+ const prevTtf = ttfMs(prev);
16920
+ const prevLabel = prevTtf !== null ? `${(prevTtf / 1e3).toFixed(1)}s to ${TTF_OUTPUT_TOKENS} tokens` : "an unmeasurable result";
16817
16921
  this.emit(`nCpuMoe=${current.cand.params.nCpuMoe} measured ${prevLabel} \u2014 trying VRAM-spill nCpuMoe=${spillN}`);
16818
16922
  const spillFound = await this.benchAt(entry, sys, { ...base2, nCpuMoe: spillN }, caps, results, `nCpuMoe=${spillN} (VRAM-spill hill-climb)`);
16819
16923
  if (!spillImproves(prev, spillFound?.cand ?? null)) {
16820
- this.emit(`nCpuMoe=${spillN} spill did not improve on ${prevLabel} (generation or prefill speed decreased) \u2014 stopping climb, keeping nCpuMoe=${current.cand.params.nCpuMoe}`);
16924
+ this.emit(`nCpuMoe=${spillN} did not beat ${prevLabel} \u2014 stopping climb, keeping nCpuMoe=${current.cand.params.nCpuMoe}`);
16821
16925
  break;
16822
16926
  }
16823
16927
  current = spillFound;
@@ -16834,7 +16938,7 @@ var BenchRunner = class {
16834
16938
  * measuring t/s at every search step would be wasted — we bench ONCE, at the chosen config. */
16835
16939
  async probeVram(entry, sys, profile, caps) {
16836
16940
  const active = this.registry.active();
16837
- if (!active) return { outcome: "crash", vramAbsMb: null };
16941
+ if (!active) return { outcome: "crash", vramAbsMb: null, spillMb: null };
16838
16942
  const testDeadline = Math.min(Date.now() + READY_TIMEOUT_MS + 5e3, this.deadline);
16839
16943
  const opts = {
16840
16944
  engine: active,
@@ -16842,19 +16946,22 @@ var BenchRunner = class {
16842
16946
  modelPath: entry.path,
16843
16947
  extraArgs: profileToArgs(profile, entry, caps, sys.cores, sys, active.binPath)
16844
16948
  };
16949
+ const sharedBefore = await readGpuSharedMb(sys);
16845
16950
  try {
16846
16951
  await this.manager.start(opts);
16847
16952
  } catch {
16848
- return { outcome: "crash", vramAbsMb: null };
16953
+ return { outcome: "crash", vramAbsMb: null, spillMb: null };
16849
16954
  }
16850
16955
  const outcome = await this.awaitReady(testDeadline);
16851
16956
  let vramAbsMb = null;
16957
+ let spill = null;
16852
16958
  if (outcome === "ok") {
16853
16959
  await sleep3(800);
16854
16960
  vramAbsMb = await readGpuVramMb(sys);
16961
+ spill = spillMb(sharedBefore, await readGpuSharedMb(sys));
16855
16962
  }
16856
16963
  await this.stopEngine();
16857
- return { outcome, vramAbsMb };
16964
+ return { outcome, vramAbsMb, spillMb: spill };
16858
16965
  }
16859
16966
  /** Record a VRAM-probe trial in the candidate list (tps/prefill are null — nothing was generated;
16860
16967
  * only the load outcome and absolute VRAM are known). */
@@ -16874,7 +16981,8 @@ var BenchRunner = class {
16874
16981
  prefillTps: null,
16875
16982
  ttftMs: null,
16876
16983
  vramMb: null,
16877
- vramAbsMb: probe2.vramAbsMb
16984
+ vramAbsMb: probe2.vramAbsMb,
16985
+ spillMb: probe2.spillMb
16878
16986
  };
16879
16987
  results.push(probeCand);
16880
16988
  this.emit(`${probeCand.label} \u2192 ${probe2.outcome}${probe2.vramAbsMb != null ? ` (${probe2.vramAbsMb} MB)` : ""}`, probeCand);
@@ -16905,7 +17013,7 @@ var BenchRunner = class {
16905
17013
  kvTypeK: profile.kvTypeK,
16906
17014
  flashAttn: profile.flashAttn
16907
17015
  };
16908
- const fail = (outcome2) => ({ label, params, outcome: outcome2, tps: null, prefillTps: null, ttftMs: null, vramMb: null, vramAbsMb: null });
17016
+ const fail = (outcome2) => ({ label, params, outcome: outcome2, tps: null, prefillTps: null, ttftMs: null, vramMb: null, vramAbsMb: null, spillMb: null });
16909
17017
  const phase = (p) => {
16910
17018
  this.state = { ...this.state, step: `${stepPrefix} \u2014 ${p}` };
16911
17019
  };
@@ -16920,6 +17028,7 @@ var BenchRunner = class {
16920
17028
  extraArgs: profileToArgs(profile, entry, caps, sys.cores, sys, active.binPath)
16921
17029
  };
16922
17030
  const vramBefore = await readGpuVramMb(sys);
17031
+ const sharedBefore = await readGpuSharedMb(sys);
16923
17032
  phase("loading model\u2026");
16924
17033
  try {
16925
17034
  await this.manager.start(opts);
@@ -16947,10 +17056,11 @@ var BenchRunner = class {
16947
17056
  phase("measuring t/s\u2026");
16948
17057
  const measured = await this.runChatWatched(target, benchMessages, 128, remaining(), logPath);
16949
17058
  const vramAfter = await readGpuVramMb(sys);
17059
+ const sharedAfter = await readGpuSharedMb(sys);
16950
17060
  await this.stopEngine();
16951
17061
  if ("fault" in measured) return fail(measured.fault);
16952
17062
  const vramMb = vramBefore !== null && vramAfter !== null ? Math.max(0, vramAfter - vramBefore) : vramAfter;
16953
- return { label, params, outcome: "ok", tps: measured.tps, prefillTps: measured.prefillTps, ttftMs: measured.ttftMs, vramMb, vramAbsMb: vramAfter };
17063
+ return { label, params, outcome: "ok", tps: measured.tps, prefillTps: measured.prefillTps, ttftMs: measured.ttftMs, vramMb, vramAbsMb: vramAfter, spillMb: spillMb(sharedBefore, sharedAfter) };
16954
17064
  }
16955
17065
  /** Poll the manager state until the engine is running, the readiness window
16956
17066
  * elapses (timeout), the process exits (crash), or an OOM line appears in the
@@ -17459,20 +17569,35 @@ function pickKvQuants(baseKv, kvTypes) {
17459
17569
  for (const t of [baseKv, ...QUALITY_KV]) if (supported(t) && !out.includes(t)) out.push(t);
17460
17570
  return out;
17461
17571
  }
17572
+ function probeVerdict(probe2, budgetMb, headroomMb) {
17573
+ if (probe2.outcome === "oom") return { decision: "offload-more", reason: "oom" };
17574
+ if (isSpilling(probe2.spillMb)) {
17575
+ return { decision: "offload-more", reason: "spill", shortfallMb: probe2.spillMb };
17576
+ }
17577
+ if (overHeadroom(probe2.vramAbsMb, budgetMb, headroomMb)) {
17578
+ return { decision: "offload-more", reason: "headroom" };
17579
+ }
17580
+ if (probe2.outcome === "ok") return { decision: "fits" };
17581
+ return { decision: "offload-more", reason: "failed" };
17582
+ }
17583
+ var TTF_OUTPUT_TOKENS = 1e3;
17584
+ function ttfMs(c, outputTokens = TTF_OUTPUT_TOKENS) {
17585
+ if (c.ttftMs === null || c.tps === null || c.tps <= 0) return null;
17586
+ return c.ttftMs + outputTokens / c.tps * 1e3;
17587
+ }
17462
17588
  function betterBySpeed(a, b) {
17463
- const at = a.tps ?? 0;
17464
- const bt = b.tps ?? 0;
17465
- if (bt <= 0) return at > 0;
17466
- const rel = (at - bt) / bt;
17467
- if (rel > OUTPUT_TIE) return true;
17468
- if (rel < -OUTPUT_TIE) return false;
17469
- return (a.prefillTps ?? 0) > (b.prefillTps ?? 0);
17589
+ const at = ttfMs(a);
17590
+ const bt = ttfMs(b);
17591
+ if (at === null) return false;
17592
+ if (bt === null) return true;
17593
+ return at < bt;
17470
17594
  }
17471
17595
  function spillImproves(previous, candidate) {
17472
- if (!candidate || candidate.outcome !== "ok" || candidate.tps === null) return false;
17473
- if (candidate.tps < previous.tps) return false;
17474
- if (previous.prefillTps !== null && (candidate.prefillTps === null || candidate.prefillTps < previous.prefillTps)) return false;
17475
- return true;
17596
+ if (!candidate || candidate.outcome !== "ok") return false;
17597
+ const next = ttfMs(candidate);
17598
+ const prev = ttfMs(previous);
17599
+ if (next === null || prev === null) return false;
17600
+ return next < prev;
17476
17601
  }
17477
17602
  function kvBytes(t) {
17478
17603
  return KV_BYTES[t] ?? 2;
@@ -17538,6 +17663,66 @@ async function readGpuVramMb(sys) {
17538
17663
  if (sys.gpus.some((g) => g.vendor === "amd")) return readRocmVramMb();
17539
17664
  return null;
17540
17665
  }
17666
+ var WDDM_SHARED_PS = "$s=(Get-Counter '\\GPU Adapter Memory(*)\\Shared Usage' -ErrorAction SilentlyContinue).CounterSamples;if(-not $s){exit 1};[math]::Round((($s|Measure-Object CookedValue -Sum).Sum)/1MB,1)";
17667
+ var wddmSharedUnsupported = false;
17668
+ function readWddmSharedMb() {
17669
+ if (process.platform !== "win32" || wddmSharedUnsupported) return Promise.resolve(null);
17670
+ return new Promise((resolve5) => {
17671
+ try {
17672
+ execFile10(
17673
+ "powershell",
17674
+ ["-NoProfile", "-NonInteractive", "-Command", WDDM_SHARED_PS],
17675
+ { timeout: 1e4, windowsHide: true },
17676
+ (err8, stdout) => {
17677
+ if (err8) {
17678
+ if (!err8.killed) wddmSharedUnsupported = true;
17679
+ return resolve5(null);
17680
+ }
17681
+ const mb = parseFloat((stdout || "").trim());
17682
+ if (!Number.isFinite(mb) || mb < 0) {
17683
+ wddmSharedUnsupported = true;
17684
+ return resolve5(null);
17685
+ }
17686
+ resolve5(Math.round(mb));
17687
+ }
17688
+ );
17689
+ } catch {
17690
+ wddmSharedUnsupported = true;
17691
+ resolve5(null);
17692
+ }
17693
+ });
17694
+ }
17695
+ function parseRocmGttUsed(memJson) {
17696
+ try {
17697
+ const mem = JSON.parse(memJson);
17698
+ let total = 0;
17699
+ for (const fields of Object.values(mem)) {
17700
+ const usedKey = Object.keys(fields).find((k) => /GTT Total Used Memory/i.test(k));
17701
+ total += usedKey ? parseInt(String(fields[usedKey]).trim(), 10) || 0 : 0;
17702
+ }
17703
+ return Number.isFinite(total) ? Math.round(total / 1e6) : null;
17704
+ } catch {
17705
+ return null;
17706
+ }
17707
+ }
17708
+ function readRocmGttMb() {
17709
+ return new Promise((resolve5) => {
17710
+ try {
17711
+ execFile10("rocm-smi", ["--showmeminfo", "gtt", "--json"], { timeout: 8e3, windowsHide: true }, (err8, stdout) => {
17712
+ if (err8 || !stdout) return resolve5(null);
17713
+ resolve5(parseRocmGttUsed(stdout));
17714
+ });
17715
+ } catch {
17716
+ resolve5(null);
17717
+ }
17718
+ });
17719
+ }
17720
+ async function readGpuSharedMb(sys) {
17721
+ const win = await readWddmSharedMb();
17722
+ if (win !== null) return win;
17723
+ if (sys.gpus.some((g) => g.vendor === "amd")) return readRocmGttMb();
17724
+ return null;
17725
+ }
17541
17726
  function sleep3(ms) {
17542
17727
  return new Promise((r) => setTimeout(r, ms));
17543
17728
  }
@@ -24252,7 +24437,59 @@ function mapFromOpenAI(oai, modelName) {
24252
24437
  usage: { input_tokens: usage?.prompt_tokens ?? 0, output_tokens: usage?.completion_tokens ?? 0 }
24253
24438
  };
24254
24439
  }
24255
- async function* streamToAnthropic(oaiStream, modelName, msgId, onUsage, onLive, onToolCalls) {
24440
+ var DEFAULT_PING_INTERVAL_MS = 1e4;
24441
+ function waitOrPing(pending2, ms) {
24442
+ return new Promise((resolve5) => {
24443
+ const timer = setTimeout(() => resolve5("ping"), ms);
24444
+ pending2.then(
24445
+ () => {
24446
+ clearTimeout(timer);
24447
+ resolve5("data");
24448
+ },
24449
+ () => {
24450
+ clearTimeout(timer);
24451
+ resolve5("data");
24452
+ }
24453
+ );
24454
+ });
24455
+ }
24456
+ async function pingWhilePending(pending2, emitPing, pingIntervalMs = DEFAULT_PING_INTERVAL_MS, onOrphan) {
24457
+ pending2.catch(() => {
24458
+ });
24459
+ let nextPingAt = Date.now() + pingIntervalMs;
24460
+ while (true) {
24461
+ const remaining = Math.max(0, nextPingAt - Date.now());
24462
+ const outcome = remaining === 0 ? "ping" : await waitOrPing(pending2, remaining);
24463
+ if (outcome === "ping") {
24464
+ try {
24465
+ await emitPing();
24466
+ } catch (e) {
24467
+ if (onOrphan) pending2.then(onOrphan).catch(() => {
24468
+ });
24469
+ throw e;
24470
+ }
24471
+ nextPingAt = Date.now() + pingIntervalMs;
24472
+ continue;
24473
+ }
24474
+ return pending2;
24475
+ }
24476
+ }
24477
+ function messageStartEvent(msgId, modelName) {
24478
+ return sse("message_start", {
24479
+ message: {
24480
+ id: msgId,
24481
+ type: "message",
24482
+ role: "assistant",
24483
+ content: [],
24484
+ model: modelName,
24485
+ stop_reason: null,
24486
+ stop_sequence: null,
24487
+ usage: { input_tokens: 0, output_tokens: 1 }
24488
+ }
24489
+ });
24490
+ }
24491
+ async function* streamToAnthropic(oaiStream, modelName, msgId, opts = {}) {
24492
+ const { onUsage, onLive, onToolCalls, pingIntervalMs = DEFAULT_PING_INTERVAL_MS, skipMessageStart = false } = opts;
24256
24493
  let blockIdx = 0;
24257
24494
  let inThinking = false;
24258
24495
  let inText = false;
@@ -24265,25 +24502,30 @@ async function* streamToAnthropic(oaiStream, modelName, msgId, onUsage, onLive,
24265
24502
  let promptTps = 0;
24266
24503
  let genTps = 0;
24267
24504
  let liveOut = 0;
24268
- yield sse("message_start", {
24269
- message: {
24270
- id: msgId,
24271
- type: "message",
24272
- role: "assistant",
24273
- content: [],
24274
- model: modelName,
24275
- stop_reason: null,
24276
- stop_sequence: null,
24277
- usage: { input_tokens: 0, output_tokens: 1 }
24278
- }
24279
- });
24505
+ if (!skipMessageStart) yield messageStartEvent(msgId, modelName);
24280
24506
  const reader = oaiStream.getReader();
24281
24507
  const dec = new TextDecoder();
24282
24508
  let buf = "";
24283
24509
  let failed = false;
24510
+ let nextPingAt = Date.now() + pingIntervalMs;
24284
24511
  try {
24285
24512
  outer: while (true) {
24286
- const { done, value } = await reader.read();
24513
+ const readPromise = reader.read();
24514
+ readPromise.catch(() => {
24515
+ });
24516
+ let result;
24517
+ while (true) {
24518
+ const remaining = Math.max(0, nextPingAt - Date.now());
24519
+ const outcome = remaining === 0 ? "ping" : await waitOrPing(readPromise, remaining);
24520
+ if (outcome === "ping") {
24521
+ yield sse("ping", {});
24522
+ nextPingAt = Date.now() + pingIntervalMs;
24523
+ continue;
24524
+ }
24525
+ result = await readPromise;
24526
+ break;
24527
+ }
24528
+ const { done, value } = result;
24287
24529
  if (done) break;
24288
24530
  buf += dec.decode(value, { stream: true });
24289
24531
  const lines = buf.split("\n");
@@ -24828,7 +25070,23 @@ function anthropicErrorType(status) {
24828
25070
  function asClientStatus(status) {
24829
25071
  return status >= 400 && status <= 599 ? status : 500;
24830
25072
  }
24831
- function registerGateway(app2, d) {
25073
+ function classifyGateError(e) {
25074
+ const aborted = e.message === "gate_acquire_aborted";
25075
+ return aborted ? { status: 400, type: "invalid_request_error", message: "Client disconnected while queued for the engine." } : { status: 503, type: "overloaded_error", message: "Timed out waiting for a free engine slot." };
25076
+ }
25077
+ function classifyFetchError(e, ac) {
25078
+ const err8 = e;
25079
+ const isAbort = err8.name === "AbortError" || ac.signal.aborted;
25080
+ const cause = err8.cause instanceof Error ? `: ${err8.cause.message}` : "";
25081
+ return {
25082
+ status: 500,
25083
+ type: "api_error",
25084
+ message: isAbort ? "Client disconnected before the engine responded." : `${err8.message || "Engine unreachable."}${cause}`
25085
+ };
25086
+ }
25087
+ function registerGateway(app2, d, opts = {}) {
25088
+ const pingIntervalMs = opts.pingIntervalMs ?? DEFAULT_PING_INTERVAL_MS;
25089
+ const gateAcquireTimeoutMs = opts.gateAcquireTimeoutMs ?? 6e5;
24832
25090
  app2.post("/v1/messages", async (c) => {
24833
25091
  let req;
24834
25092
  try {
@@ -24888,23 +25146,131 @@ function registerGateway(app2, d) {
24888
25146
  if (guidance?.forceTextOnly) oaiBody.tool_choice = "none";
24889
25147
  const oaiAlias = engineModelAlias(d.registry.active()?.kind ?? "");
24890
25148
  if (oaiAlias) oaiBody.model = oaiAlias;
25149
+ const ac = clientAbort(c);
25150
+ if (req.stream) {
25151
+ const msgId = `msg_${randomUUID13().replace(/-/g, "")}`;
25152
+ return streamSSE4(c, async (stream) => {
25153
+ let gateRelease2 = null;
25154
+ let generationStarted = false;
25155
+ stream.onAbort(() => ac.abort());
25156
+ const ping = () => stream.writeSSE({ event: "ping", data: JSON.stringify({ type: "ping" }) });
25157
+ try {
25158
+ try {
25159
+ await stream.writeSSE(messageStartEvent(msgId, modelName));
25160
+ if (d.gate) {
25161
+ try {
25162
+ gateRelease2 = await pingWhilePending(
25163
+ d.gate.acquire("bg", { signal: c.req.raw.signal, timeoutMs: gateAcquireTimeoutMs }),
25164
+ ping,
25165
+ pingIntervalMs,
25166
+ (release) => release()
25167
+ );
25168
+ } catch (e) {
25169
+ const { type, message } = classifyGateError(e);
25170
+ await stream.writeSSE({ event: "error", data: JSON.stringify({ type: "error", error: { type, message } }) });
25171
+ return;
25172
+ }
25173
+ }
25174
+ d.manager.generationStart();
25175
+ generationStarted = true;
25176
+ const requestStart2 = Date.now();
25177
+ let res2;
25178
+ try {
25179
+ res2 = await pingWhilePending(
25180
+ fetch(`${target}/v1/chat/completions`, {
25181
+ method: "POST",
25182
+ headers: { "Content-Type": "application/json" },
25183
+ body: JSON.stringify(oaiBody),
25184
+ signal: ac.signal
25185
+ }),
25186
+ ping,
25187
+ pingIntervalMs
25188
+ // No onOrphan: an orphaned Response has nothing to release — its body is simply
25189
+ // never read and gets garbage-collected, unlike gate.acquire()'s grant above.
25190
+ );
25191
+ } catch (e) {
25192
+ const { type, message } = classifyFetchError(e, ac);
25193
+ await stream.writeSSE({ event: "error", data: JSON.stringify({ type: "error", error: { type, message } }) });
25194
+ return;
25195
+ }
25196
+ if (!res2.ok || !res2.body) {
25197
+ const { message, type } = await describeEngineError(res2);
25198
+ await stream.writeSSE({
25199
+ event: "error",
25200
+ data: JSON.stringify({ type: "error", error: { type: type ?? anthropicErrorType(res2.status), message } })
25201
+ });
25202
+ return;
25203
+ }
25204
+ const gen = streamToAnthropic(res2.body, modelName, msgId, {
25205
+ onUsage: (u) => {
25206
+ try {
25207
+ d.manager.recordCompletion({
25208
+ inputTokens: u.inputTokens,
25209
+ outputTokens: u.outputTokens,
25210
+ promptTps: u.promptTps,
25211
+ genTps: u.genTps
25212
+ });
25213
+ d.db.recordApiUsage({
25214
+ source: "anthropic",
25215
+ modelKey: req.model ?? null,
25216
+ promptTokens: u.inputTokens,
25217
+ genTokens: u.outputTokens,
25218
+ codeSessionId: anthropicCodeSessionId,
25219
+ durationMs: Date.now() - requestStart2,
25220
+ promptTps: u.promptTps,
25221
+ genTps: u.genTps,
25222
+ harness: anthropicHarness
25223
+ });
25224
+ } catch {
25225
+ }
25226
+ },
25227
+ // Live per-request progress for the engine card (prefill % + token count),
25228
+ // so Claude Code traffic shows the same live row as in-app chat.
25229
+ onLive: (live) => {
25230
+ try {
25231
+ d.manager.setLiveGen(live);
25232
+ } catch {
25233
+ }
25234
+ },
25235
+ // Coding-activity attribution for terminal-agent sessions — see
25236
+ // observeCodeSessionTurn. Nothing to attribute for any other client.
25237
+ onToolCalls: (calls) => {
25238
+ if (!anthropicCodeSessionId) return;
25239
+ try {
25240
+ observeCodeSessionTurn(d, anthropicCodeSessionId, calls);
25241
+ } catch {
25242
+ }
25243
+ },
25244
+ skipMessageStart: true
25245
+ // already sent above, before the queue wait
25246
+ });
25247
+ for await (const evt of gen) {
25248
+ await stream.writeSSE({ event: evt.event, data: evt.data });
25249
+ }
25250
+ } finally {
25251
+ ac.abort();
25252
+ if (generationStarted) d.manager.generationEnd();
25253
+ gateRelease2?.();
25254
+ }
25255
+ } catch (e) {
25256
+ await stream.writeSSE({
25257
+ event: "error",
25258
+ data: JSON.stringify({ type: "error", error: { type: "api_error", message: e?.message || "Internal gateway error." } })
25259
+ }).catch(() => {
25260
+ });
25261
+ }
25262
+ });
25263
+ }
24891
25264
  let gateRelease = null;
24892
25265
  if (d.gate) {
24893
25266
  try {
24894
- gateRelease = await d.gate.acquire("bg", { signal: c.req.raw.signal, timeoutMs: 6e5 });
25267
+ gateRelease = await d.gate.acquire("bg", { signal: c.req.raw.signal, timeoutMs: gateAcquireTimeoutMs });
24895
25268
  } catch (e) {
24896
- const aborted = e.message === "gate_acquire_aborted";
24897
- return c.json(
24898
- {
24899
- type: "error",
24900
- error: aborted ? { type: "invalid_request_error", message: "Client disconnected while queued for the engine." } : { type: "overloaded_error", message: "Timed out waiting for a free engine slot." }
24901
- },
24902
- aborted ? 400 : 503
24903
- );
25269
+ const { status: status2, type, message } = classifyGateError(e);
25270
+ return c.json({ type: "error", error: { type, message } }, status2);
24904
25271
  }
24905
25272
  }
24906
25273
  d.manager.generationStart();
24907
- const ac = clientAbort(c);
24908
25274
  const requestStart = Date.now();
24909
25275
  let res;
24910
25276
  try {
@@ -24917,19 +25283,8 @@ function registerGateway(app2, d) {
24917
25283
  } catch (e) {
24918
25284
  d.manager.generationEnd();
24919
25285
  gateRelease?.();
24920
- const err8 = e;
24921
- const isAbort = err8.name === "AbortError" || ac.signal.aborted;
24922
- const cause = err8.cause instanceof Error ? `: ${err8.cause.message}` : "";
24923
- return c.json(
24924
- {
24925
- type: "error",
24926
- error: {
24927
- type: "api_error",
24928
- message: isAbort ? "Client disconnected before the engine responded." : `${err8.message || "Engine unreachable."}${cause}`
24929
- }
24930
- },
24931
- 500
24932
- );
25286
+ const { status: status2, type, message } = classifyFetchError(e, ac);
25287
+ return c.json({ type: "error", error: { type, message } }, status2);
24933
25288
  }
24934
25289
  if (!res.ok || !res.body) {
24935
25290
  d.manager.generationEnd();
@@ -24940,65 +25295,6 @@ function registerGateway(app2, d) {
24940
25295
  asClientStatus(res.status)
24941
25296
  );
24942
25297
  }
24943
- if (req.stream) {
24944
- const msgId = `msg_${randomUUID13().replace(/-/g, "")}`;
24945
- const gen = streamToAnthropic(
24946
- res.body,
24947
- modelName,
24948
- msgId,
24949
- (u) => {
24950
- try {
24951
- d.manager.recordCompletion({
24952
- inputTokens: u.inputTokens,
24953
- outputTokens: u.outputTokens,
24954
- promptTps: u.promptTps,
24955
- genTps: u.genTps
24956
- });
24957
- d.db.recordApiUsage({
24958
- source: "anthropic",
24959
- modelKey: req.model ?? null,
24960
- promptTokens: u.inputTokens,
24961
- genTokens: u.outputTokens,
24962
- codeSessionId: anthropicCodeSessionId,
24963
- durationMs: Date.now() - requestStart,
24964
- promptTps: u.promptTps,
24965
- genTps: u.genTps,
24966
- harness: anthropicHarness
24967
- });
24968
- } catch {
24969
- }
24970
- },
24971
- // Live per-request progress for the engine card (prefill % + token count),
24972
- // so Claude Code traffic shows the same live row as in-app chat.
24973
- (live) => {
24974
- try {
24975
- d.manager.setLiveGen(live);
24976
- } catch {
24977
- }
24978
- },
24979
- // Coding-activity attribution for terminal-agent sessions — see
24980
- // observeCodeSessionTurn. Nothing to attribute for any other client.
24981
- (calls) => {
24982
- if (!anthropicCodeSessionId) return;
24983
- try {
24984
- observeCodeSessionTurn(d, anthropicCodeSessionId, calls);
24985
- } catch {
24986
- }
24987
- }
24988
- );
24989
- return streamSSE4(c, async (stream) => {
24990
- stream.onAbort(() => ac.abort());
24991
- try {
24992
- for await (const evt of gen) {
24993
- await stream.writeSSE({ event: evt.event, data: evt.data });
24994
- }
24995
- } finally {
24996
- ac.abort();
24997
- d.manager.generationEnd();
24998
- gateRelease?.();
24999
- }
25000
- });
25001
- }
25002
25298
  try {
25003
25299
  const oaiRes = await res.json();
25004
25300
  recordOpenAiUsage(d, oaiRes, "anthropic", req.model ?? null, anthropicCodeSessionId, Date.now() - requestStart, anthropicHarness);