mixdog 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +344 -0
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1223 -95
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2325 -450
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +118 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -3,22 +3,21 @@
3
3
  *
4
4
  * Replaces the raw `[tool: name]` line with a styled card that shows the tool
5
5
  * name plus a short, human-readable summary of its most relevant argument
6
- * (path for read/write/edit, command for shell, pattern for grep, etc.).
6
+ * (path for read/apply_patch, command for shell, pattern for grep, etc.).
7
7
  *
8
8
  * Pure formatting: returns a string, never touches stdout. Robust to missing or
9
9
  * malformed argument objects (the engine hands us `{ name, arguments, id }`).
10
10
  */
11
- import { bold, dim, cyan, gray, yellow, green } from './ansi.mjs';
11
+ import { bold, cyan, gray } from './ansi.mjs';
12
12
 
13
13
  const MAX_SUMMARY = 72;
14
14
 
15
15
  /** Map of tool name -> function deriving its one-line summary from args. */
16
16
  const SUMMARIZERS = {
17
17
  read: (a) => a.path ?? a.file,
18
- write: (a) => a.path,
19
18
  apply_patch: (a) => a.path ?? a.base_path ?? firstPatchPath(a.patch),
20
- edit: (a) => a.path ?? (Array.isArray(a.edits) ? a.edits[0]?.path : undefined),
21
19
  list: (a) => a.path ?? a.pattern,
20
+ find: (a) => a.query ?? a.fuzzy ?? a.path,
22
21
  glob: (a) => joinMaybe(a.pattern) ?? a.path,
23
22
  grep: (a) => joinMaybe(a.pattern),
24
23
  shell: (a) => a.command,
@@ -45,18 +44,6 @@ export function renderToolCard(call) {
45
44
  return ` ${bullet} ${label} ${gray(truncate(summary, MAX_SUMMARY))}`;
46
45
  }
47
46
 
48
- /** Render several calls (from one iteration) as a joined block. */
49
- export function renderToolCards(calls) {
50
- return (calls || []).map(renderToolCard).join('\n');
51
- }
52
-
53
- /** A small "done" marker line for a completed tool (optional nicety). */
54
- export function renderToolResult(name, { ok = true, note = '' } = {}) {
55
- const mark = ok ? green('ok') : yellow('x');
56
- const tail = note ? ' ' + dim(truncate(String(note), 48)) : '';
57
- return ` ${mark} ${dim(String(name ?? 'tool'))}${tail}`;
58
- }
59
-
60
47
  // --- helpers -----------------------------------------------------------------
61
48
 
62
49
  function safeSummary(name, args) {
@@ -151,7 +151,7 @@ function compactWindowForRouteLike(route) {
151
151
  const explicit = num(route.autoCompactTokenLimit ?? route.auto_compact_token_limit);
152
152
  const raw = num(route.rawContextWindow ?? route.raw_context_window ?? route.contextWindow ?? route.context_window);
153
153
  const context = num(route.contextWindow ?? route.context_window);
154
- const pct = num(route.effectiveContextWindowPercent ?? route.effective_context_window_percent) || 95;
154
+ const pct = num(route.effectiveContextWindowPercent ?? route.effective_context_window_percent) || 90;
155
155
  const effective = context || (raw > 0 ? Math.max(1, Math.floor(raw * Math.min(100, pct) / 100)) : 0);
156
156
  if (explicit > 0 && effective > 0) return Math.min(explicit, effective);
157
157
  if (explicit > 0) return explicit;
@@ -573,6 +573,10 @@ export async function renderStatusLine(ccJsonInput) {
573
573
  if (cleanTags.length <= limit) return cleanTags.join(', ');
574
574
  return `${cleanTags.slice(0, limit).join(', ')}, +${cleanTags.length - limit}`;
575
575
  }
576
+ const workerSpinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
577
+ function workerSpinnerFrame(now = Date.now()) {
578
+ return workerSpinnerFrames[Math.floor(now / 120) % workerSpinnerFrames.length] || workerSpinnerFrames[0];
579
+ }
576
580
 
577
581
  const gatewayLimitSegments = formatGatewayLimitSegments(GATEWAY_STATUS, {
578
582
  COLS, D, R, RED, GRN, YLW, colourPct, epochMsToHHMM,
@@ -595,9 +599,9 @@ export async function renderStatusLine(ccJsonInput) {
595
599
  if (modelStr) {
596
600
  const m = COLS >= 120 ? modelStr : modelShort;
597
601
  if (effortStr) {
598
- addL1(`${CYN}◆${R} ${B}${m}${R} ${D}·${R} ${B}${effortStr}${R}`);
602
+ addL1(`${B}${m}${R} ${D}·${R} ${B}${effortStr}${R}`);
599
603
  } else {
600
- addL1(`${CYN}◆${R} ${B}${m}${R}`);
604
+ addL1(`${B}${m}${R}`);
601
605
  }
602
606
  }
603
607
 
@@ -781,7 +785,7 @@ export async function renderStatusLine(ccJsonInput) {
781
785
  function addL2(seg) { if (seg) l2Parts.push(seg); }
782
786
 
783
787
  if (workCount > 0 && workOrder) {
784
- addL2(`${GRN}●${R} ${B}${workCount} Running${R} ${D}(${R}${B}${summarizeWorkerTags(workOrder.split(','))}${R}${D})${R}`);
788
+ addL2(`${GRN}${workerSpinnerFrame()}${R} ${B}${workCount} Running${R} ${D}(${R}${B}${summarizeWorkerTags(workOrder.split(','))}${R}${D})${R}`);
785
789
  }
786
790
 
787
791
  if (idleWorkers.length) {
@@ -17,6 +17,11 @@ function positiveInt(value) {
17
17
  return Number.isFinite(n) && n > 0 ? n : 0;
18
18
  }
19
19
 
20
+ // Keep the last known OAuth usage snapshot visible while idle. Live refreshes
21
+ // replace it when available, but a delayed/failed refresh must not make the
22
+ // statusline usage segment disappear. Previous-launch snapshots stay hidden
23
+ // during boot until the current process captures usage once.
24
+ const STATUSLINE_PROCESS_STARTED_AT_MS = Date.now() - Math.floor((Number(process.uptime?.()) || 0) * 1000);
20
25
  function isPidAlive(pid) {
21
26
  const n = positiveInt(pid);
22
27
  if (!n) return false;
@@ -56,6 +61,14 @@ function num(value, fallback = null) {
56
61
  return Number.isFinite(n) ? n : fallback;
57
62
  }
58
63
 
64
+ function firstPositiveWindow(...values) {
65
+ for (const value of values) {
66
+ const n = num(value, null);
67
+ if (n !== null && n > 0) return n;
68
+ }
69
+ return null;
70
+ }
71
+
59
72
  function money(value) {
60
73
  const n = Number(value);
61
74
  if (!Number.isFinite(n)) return '';
@@ -88,6 +101,19 @@ function cleanString(value) {
88
101
  return s || '';
89
102
  }
90
103
 
104
+ // Mirror of route-meta.mjs providerKind(): classify a provider id into a kind
105
+ // bucket. statusline-route.mjs derives its own status snapshot and calls this
106
+ // the same way route-meta does, but never imported/defined it, so any route
107
+ // status build threw `providerKind is not defined`.
108
+ function providerKind(provider) {
109
+ const p = String(provider || '').toLowerCase();
110
+ if (!p) return 'unknown';
111
+ if (p === 'opencode-go') return 'quota-api';
112
+ if (p.includes('oauth')) return 'oauth';
113
+ if (p === 'ollama' || p === 'lmstudio') return 'local';
114
+ return 'api';
115
+ }
116
+
91
117
  function slugSegment(value) {
92
118
  const s = cleanString(value);
93
119
  if (!s) return '';
@@ -163,18 +189,16 @@ function boundedPercent(value, fallback = null) {
163
189
  }
164
190
 
165
191
  function defaultEffectiveContextWindowPercent(_provider) {
166
- return 95;
192
+ return 90;
167
193
  }
168
194
 
169
195
  function routeContextMeta(provider, info = {}, inherited = {}) {
170
- const rawContextWindow = num(
171
- inherited.rawContextWindow
172
- ?? inherited.raw_context_window
173
- ?? info.contextWindow
174
- ?? info.maxContextWindow
175
- ?? info.max_input_tokens
176
- ?? inherited.contextWindow,
177
- null,
196
+ const rawContextWindow = firstPositiveWindow(
197
+ inherited.rawContextWindow,
198
+ inherited.raw_context_window,
199
+ info.contextWindow,
200
+ info.maxContextWindow,
201
+ info.max_input_tokens,
178
202
  );
179
203
  const effectiveContextWindowPercent = boundedPercent(
180
204
  inherited.effectiveContextWindowPercent
@@ -183,9 +207,16 @@ function routeContextMeta(provider, info = {}, inherited = {}) {
183
207
  ?? info.effective_context_window_percent,
184
208
  defaultEffectiveContextWindowPercent(provider),
185
209
  );
186
- const contextWindow = rawContextWindow
210
+ const derivedContextWindow = rawContextWindow
187
211
  ? Math.max(1, Math.floor(rawContextWindow * boundedPercent(effectiveContextWindowPercent, 100) / 100))
188
212
  : null;
213
+ const explicitEffectiveContextWindow = firstPositiveWindow(
214
+ inherited.contextWindow,
215
+ inherited.displayContextWindow,
216
+ inherited.compactBoundaryTokens,
217
+ inherited.compact_boundary_tokens,
218
+ );
219
+ const contextWindow = explicitEffectiveContextWindow ?? derivedContextWindow;
189
220
  const explicitCompactLimit = num(
190
221
  inherited.autoCompactTokenLimit
191
222
  ?? inherited.auto_compact_token_limit
@@ -193,10 +224,18 @@ function routeContextMeta(provider, info = {}, inherited = {}) {
193
224
  ?? info.auto_compact_token_limit,
194
225
  null,
195
226
  );
196
- const derivedCompactLimit = contextWindow || rawContextWindow || null;
197
- const autoCompactTokenLimit = explicitCompactLimit && derivedCompactLimit
198
- ? Math.min(explicitCompactLimit, derivedCompactLimit)
199
- : explicitCompactLimit ?? derivedCompactLimit;
227
+ // autoCompactTokenLimit must mean an EXPLICIT auto-compaction limit only.
228
+ // Do NOT derive it from the context/raw window: the runtime treats a present
229
+ // autoCompactTokenLimit as the compaction trigger, and a full-window value
230
+ // collapses the compaction buffer to 0. Keep boundary/window display via the
231
+ // separate contextWindow/rawContextWindow fields. An explicit limit is only a
232
+ // real trigger when STRICTLY BELOW the boundary; a legacy seed/info value
233
+ // equal to or above it is a derived full-window artifact and is dropped to
234
+ // null (Math.min would still surface the boundary). Never derive a limit.
235
+ const _boundary = contextWindow || rawContextWindow || null;
236
+ const autoCompactTokenLimit = explicitCompactLimit && explicitCompactLimit > 0
237
+ ? (_boundary ? (explicitCompactLimit < _boundary ? explicitCompactLimit : null) : explicitCompactLimit)
238
+ : null;
200
239
  return {
201
240
  contextWindow,
202
241
  rawContextWindow,
@@ -205,14 +244,49 @@ function routeContextMeta(provider, info = {}, inherited = {}) {
205
244
  };
206
245
  }
207
246
 
208
- function compactBoundaryForStatus(routeInfo = {}, compact = null) {
247
+ // Test-only export of the route context-meta resolver for the auto-compact
248
+ // limit migration smoke (scripts/compact-trigger-migration-smoke.mjs).
249
+ export const _routeContextMeta = routeContextMeta;
250
+
251
+ // Resolve the auto-compact limit displayed by loadGatewayStatus(). The active
252
+ // value MUST be validated against the ACTIVE route's own boundary/window, never
253
+ // configured?.contextWindow: the configured window can be larger than the
254
+ // active window, so validating a stale active full-window value (== active
255
+ // window) against the configured window would let it slip through as an
256
+ // explicit limit. The configured route's limit (already sanitized by
257
+ // routeContextMeta) is preferred when a configured route is present, but it
258
+ // never validates a stale active value.
259
+ function resolveStatusAutoCompactTokenLimit(configured, active = {}, lastCompact = null) {
260
+ if (configured) {
261
+ const c = num(configured.autoCompactTokenLimit, 0);
262
+ return c > 0 ? c : null;
263
+ }
264
+ const activeBoundary = num(
265
+ active?.gateway_context_window
266
+ ?? active?.gateway_raw_context_window
267
+ ?? lastCompact?.budgetWindow
268
+ ?? lastCompact?.contextWindow,
269
+ 0,
270
+ );
271
+ const raw = num(active?.gateway_auto_compact_token_limit, 0);
272
+ if (!(raw > 0)) return null;
273
+ if (activeBoundary > 0) return raw < activeBoundary ? raw : null;
274
+ return raw;
275
+ }
276
+
277
+ // Test-only export of the status auto-compact-limit resolver.
278
+ export const _resolveStatusAutoCompactTokenLimit = resolveStatusAutoCompactTokenLimit;
279
+
280
+ export function compactBoundaryForStatus(routeInfo = {}, compact = null) {
209
281
  const compactLimit = num(routeInfo?.autoCompactTokenLimit ?? compact?.compactLimitTokens, 0);
210
282
  const contextWindow = num(routeInfo?.contextWindow ?? compact?.contextWindow, 0);
211
283
  const budgetWindow = num(compact?.budgetWindow, 0);
212
284
  const rawContextWindow = num(routeInfo?.rawContextWindow ?? compact?.rawContextWindow, 0);
213
- if (contextWindow > 0) return contextWindow;
214
- if (budgetWindow > 0) return budgetWindow;
285
+ if (compactLimit > 0 && contextWindow > 0) return Math.min(compactLimit, contextWindow);
215
286
  if (compactLimit > 0) return compactLimit;
287
+ if (budgetWindow > 0 && contextWindow > 0) return Math.min(budgetWindow, contextWindow);
288
+ if (budgetWindow > 0) return budgetWindow;
289
+ if (contextWindow > 0) return contextWindow;
216
290
  return rawContextWindow > 0 ? rawContextWindow : null;
217
291
  }
218
292
 
@@ -237,7 +311,7 @@ function gatewaySessionStatus(sessionId, clientHostPid = null) {
237
311
  function sessionRouteFor(gateway, sessionId, clientHostPid = null) {
238
312
  const sid = cleanString(sessionId);
239
313
  if (!sid) return { route: gateway, sessionScoped: false };
240
- const route = readGatewaySessionRoute(sid, { clientHostPid })
314
+ const route = readGatewaySessionRoute(sid, { clientHostPid, fallbackLegacy: true })
241
315
  || readLatestGatewayHostRoute(clientHostPid, { excludeSessionId: sid });
242
316
  if (!route || typeof route !== 'object') return { route: gateway, sessionScoped: false };
243
317
  const provider = cleanString(route.defaultProvider || route.provider);
@@ -273,10 +347,9 @@ function loadCachedModel(provider, model) {
273
347
  }
274
348
 
275
349
  function cachedQuotaWindowsFallback(provider, model) {
276
- // Resolve quota windows for the CURRENT route only. The oauth usage cache is
277
- // keyed by `${provider}${model}` (with a provider-only fallback key);
278
- // never scan for the "most recent" entry across routes, or a sibling session
279
- // on a different route would clobber this session's 5H/7D/reset display.
350
+ // OAuth quota is provider/account-scoped, not model-scoped. Prefer the
351
+ // current route key, then provider-wide cache, then the freshest same-provider
352
+ // route entry so a model switch keeps showing quota before the next live fetch.
280
353
  if (!provider || !model) return [];
281
354
  try {
282
355
  const cachePath = path.join(pluginDataDir(), 'gateway-oauth-usage-cache.json');
@@ -285,8 +358,17 @@ function cachedQuotaWindowsFallback(provider, model) {
285
358
  if (!routes) return [];
286
359
  const routeKey = `${String(provider).toLowerCase()}${String(model)}`;
287
360
  const providerOnlyKey = String(provider).toLowerCase();
288
- const entry = routes[routeKey] || routes[providerOnlyKey];
289
- return Array.isArray(entry?.quotaWindows) ? entry.quotaWindows : [];
361
+ const routePrefix = `${providerOnlyKey}`;
362
+ const entry = routes[routeKey] || routes[providerOnlyKey] || Object.entries(routes)
363
+ .filter(([key, value]) => key.startsWith(routePrefix) && Array.isArray(value?.quotaWindows))
364
+ .sort((a, b) => (Number(b[1]?.cachedAt) || 0) - (Number(a[1]?.cachedAt) || 0))[0]?.[1];
365
+ if (!Array.isArray(entry?.quotaWindows)) return [];
366
+ // Boot guard: do not render previous-launch usage before the current runtime
367
+ // has captured at least one snapshot. Once captured in this process, hold it
368
+ // instead of blanking it during idle/network gaps.
369
+ const cachedAt = Number(entry.cachedAt);
370
+ if (!Number.isFinite(cachedAt) || cachedAt < STATUSLINE_PROCESS_STARTED_AT_MS) return [];
371
+ return entry.quotaWindows;
290
372
  } catch {
291
373
  return [];
292
374
  }
@@ -336,7 +418,22 @@ function configuredGatewayStatus(options = {}) {
336
418
  const cfgPath = path.join(pluginDataDir(), 'mixdog-config.json');
337
419
  const cfg = readJson(cfgPath);
338
420
  const gatewayBase = cfg?.gateway && typeof cfg.gateway === 'object' ? cfg.gateway : {};
339
- const { route: gateway, sessionScoped } = sessionRouteFor(gatewayBase, options.sessionId, options.clientHostPid);
421
+ let { route: gateway, sessionScoped } = sessionRouteFor(gatewayBase, options.sessionId, options.clientHostPid);
422
+ const currentRoute = options?.currentRoute && typeof options.currentRoute === 'object' ? options.currentRoute : null;
423
+ const currentProvider = cleanString(currentRoute?.provider || currentRoute?.defaultProvider);
424
+ const currentModel = cleanString(currentRoute?.model || currentRoute?.defaultModel);
425
+ if ((!cleanString(gateway?.defaultProvider) || !cleanString(gateway?.defaultModel)) && currentProvider && currentModel) {
426
+ gateway = {
427
+ ...(gateway || {}),
428
+ mode: cleanString(gateway?.mode) || 'fixed',
429
+ defaultProvider: currentProvider,
430
+ defaultModel: currentModel,
431
+ ...(cleanString(currentRoute?.effort ?? currentRoute?.displayEffort) ? { effort: cleanString(currentRoute?.effort ?? currentRoute?.displayEffort) } : {}),
432
+ ...(cleanBool(currentRoute?.fast) !== null ? { fast: cleanBool(currentRoute.fast) } : {}),
433
+ ...(num(currentRoute?.contextWindow, null) ? { contextWindow: num(currentRoute.contextWindow, null) } : {}),
434
+ ...(num(currentRoute?.rawContextWindow, null) ? { rawContextWindow: num(currentRoute.rawContextWindow, null) } : {}),
435
+ };
436
+ }
340
437
  const modules = cfg?.modules && typeof cfg.modules === 'object' ? cfg.modules : {};
341
438
  if (modules.gateway && modules.gateway.enabled === false) return null;
342
439
  const inherit = cleanString(gateway.mode) === CLAUDE_CURRENT_MODE;
@@ -381,11 +478,17 @@ function configuredGatewayStatus(options = {}) {
381
478
  : inherit
382
479
  ? cleanBool(aliasTarget?.fast) ?? cleanBool(inherited?.fast) ?? cleanBool(gateway.fast) ?? cleanBool(preset?.fast) ?? false
383
480
  : cleanBool(gateway.fast) ?? cleanBool(preset?.fast) ?? false;
384
- const contextMeta = routeContextMeta(provider, info, aliasTarget ? {} : (inherited || {}));
481
+ const routeMatchesCurrent = currentProvider === provider && currentModel === model;
482
+ const contextSeed = {
483
+ ...(inherited || {}),
484
+ ...(routeMatchesCurrent ? currentRoute || {} : {}),
485
+ };
486
+ const contextMeta = routeContextMeta(provider, info, aliasTarget ? {} : contextSeed);
385
487
  return {
386
488
  mode: inherit ? CLAUDE_CURRENT_MODE : '',
387
489
  provider,
388
490
  model,
491
+ providerKind: providerKind(provider),
389
492
  modelDisplay: statuslineDisplayForModel(
390
493
  provider,
391
494
  model,
@@ -464,12 +567,29 @@ export function loadGatewayStatus(options = {}) {
464
567
  num(contextBoundary, 0),
465
568
  )
466
569
  : null;
467
- const recomputedContextUsedPct = pctOf(liveContextTokens ?? (compactRequestWithLiveTokens ? liveContextTokens : (overflowContextTokens ?? lastUsage?.promptTokens)), contextBoundary);
570
+ const contextNumerator = liveContextTokens !== null && Number.isFinite(liveContextTokens)
571
+ ? liveContextTokens
572
+ : (overflowContextTokens ?? null);
573
+ const recomputedContextUsedPct = pctOf(contextNumerator, contextBoundary);
468
574
  const activeQuotaWindows = metricsMatch && Array.isArray(active.gateway_quota_windows)
469
575
  ? active.gateway_quota_windows
470
576
  : [];
577
+ const statusProvider = configured?.provider || active.gateway_provider;
578
+ const statusProviderKind = configured?.providerKind
579
+ || (routeMatchesConfigured ? cleanString(active.gateway_provider_kind) : '')
580
+ || providerKind(statusProvider);
581
+ // Sanitize the auto-compact limit before display: a stale boundary/window-
582
+ // sized active.gateway_auto_compact_token_limit must not surface as an
583
+ // explicit autoCompactTokenLimit (the runtime would read it as the trigger
584
+ // and collapse the buffer). The active value must be validated against the
585
+ // ACTIVE route's own boundary/window — NOT configured?.contextWindow. The
586
+ // configured window can be larger than the active window, so using it would
587
+ // let a stale active full-window value (== active window) slip through. Use
588
+ // the active boundary (active context/raw window, or the lastCompact budget
589
+ // when present) and null the active value when it is >= that boundary.
590
+ const statusAutoCompactTokenLimit = resolveStatusAutoCompactTokenLimit(configured, active, lastCompact);
471
591
  const activeStatus = {
472
- provider: configured?.provider || active.gateway_provider,
592
+ provider: statusProvider,
473
593
  model: configured?.model || active.gateway_model,
474
594
  modelDisplay: configured?.modelDisplay
475
595
  || statuslineDisplayForModel(active.gateway_provider, active.gateway_model, {}, active.gateway_model_display)
@@ -479,13 +599,13 @@ export function loadGatewayStatus(options = {}) {
479
599
  contextWindow: configured?.contextWindow ?? num(active.gateway_context_window, null),
480
600
  rawContextWindow: configured?.rawContextWindow ?? num(active.gateway_raw_context_window, null),
481
601
  effectiveContextWindowPercent: configured?.effectiveContextWindowPercent ?? num(active.gateway_effective_context_window_percent, null),
482
- autoCompactTokenLimit: configured?.autoCompactTokenLimit ?? num(active.gateway_auto_compact_token_limit, null),
602
+ autoCompactTokenLimit: statusAutoCompactTokenLimit,
483
603
  contextUsedPct: metricsMatch ? recomputedContextUsedPct ?? num(active.gateway_context_used_pct, null) : null,
484
604
  lastUsage,
485
605
  quotaWindows: activeQuotaWindows.length ? activeQuotaWindows : (configuredStatus?.quotaWindows || []),
486
606
  balance: metricsMatch && active.gateway_balance && typeof active.gateway_balance === 'object' ? active.gateway_balance : (configuredStatus?.balance || null),
487
607
  routeSpend: metricsMatch && active.gateway_route_spend && typeof active.gateway_route_spend === 'object' ? active.gateway_route_spend : (configuredStatus?.routeSpend || null),
488
- providerKind: active.gateway_provider_kind || '',
608
+ providerKind: statusProviderKind,
489
609
  };
490
610
  if (!configured) return activeStatus;
491
611
  return activeStatus;
@@ -510,6 +630,17 @@ export function formatGatewayLimitSegments(status, fmt) {
510
630
  } = fmt || {};
511
631
  const segments = [];
512
632
  const kind = cleanString(status.providerKind || '').toLowerCase();
633
+ const providerId = cleanString(status.provider || '').toLowerCase();
634
+ const statusSource = cleanString(status.source || '').toLowerCase();
635
+ const balanceSource = cleanString(status.balance?.source || '').toLowerCase();
636
+ const isOpenAiOAuth = (kind === 'oauth' || providerId.includes('oauth'))
637
+ && (
638
+ providerId === 'openai-oauth'
639
+ || statusSource.includes('openai')
640
+ || statusSource.includes('codex')
641
+ || balanceSource.includes('openai')
642
+ || balanceSource.includes('codex')
643
+ );
513
644
  const isPlainApi = kind === 'api';
514
645
  const windows = !isPlainApi && Array.isArray(status.quotaWindows) ? status.quotaWindows : [];
515
646
  const maxWindows = COLS >= 120 ? 3 : COLS >= 80 ? 2 : 1;
@@ -537,6 +668,7 @@ export function formatGatewayLimitSegments(status, fmt) {
537
668
  return false;
538
669
  };
539
670
  const addApiBalance = () => {
671
+ if (isOpenAiOAuth) return false;
540
672
  const remaining = num(status.balance?.remainingUsd, null);
541
673
  if (remaining !== null) {
542
674
  segments.push(`${D}Credit${R} ${money(remaining)}`);
@@ -551,12 +683,12 @@ export function formatGatewayLimitSegments(status, fmt) {
551
683
  for (const w of windows.slice(0, maxWindows)) {
552
684
  const label = String(w?.label || 'USE').toUpperCase();
553
685
  const pct = num(w?.usedPct, null);
554
- const remaining = num(w?.remainingUsd, null);
555
- const limit = num(w?.limitUsd, null);
556
- const used = num(w?.usedUsd, null);
557
- const remainingCredits = num(w?.remainingCredits, null);
558
- const limitCredits = num(w?.limitCredits, null);
559
- const usedCredits = num(w?.usedCredits, null);
686
+ const remaining = isOpenAiOAuth ? null : num(w?.remainingUsd, null);
687
+ const limit = isOpenAiOAuth ? null : num(w?.limitUsd, null);
688
+ const used = isOpenAiOAuth ? null : num(w?.usedUsd, null);
689
+ const remainingCredits = isOpenAiOAuth ? null : num(w?.remainingCredits, null);
690
+ const limitCredits = isOpenAiOAuth ? null : num(w?.limitCredits, null);
691
+ const usedCredits = isOpenAiOAuth ? null : num(w?.usedCredits, null);
560
692
  const estimated = isLocalEstimateWindow(w);
561
693
  if (remaining !== null) {
562
694
  const color = estimated ? YLW : remaining <= 1 ? RED : remaining <= 5 ? YLW : GRN;
@@ -581,7 +713,7 @@ export function formatGatewayLimitSegments(status, fmt) {
581
713
  }
582
714
  if (segments.length) return segments;
583
715
 
584
- const remaining = num(status.balance?.remainingUsd, null);
716
+ const remaining = isOpenAiOAuth ? null : num(status.balance?.remainingUsd, null);
585
717
  if (remaining !== null) {
586
718
  segments.push(`${D}Credit${R} ${money(remaining)}`);
587
719
  addRouteSpend();
@@ -0,0 +1,80 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { formatGatewayLimitSegments } from './statusline-route.mjs';
4
+
5
+ test('formatGatewayLimitSegments hides OpenAI OAuth credit balance on L1', () => {
6
+ const segments = formatGatewayLimitSegments({
7
+ provider: 'openai-oauth',
8
+ providerKind: 'oauth',
9
+ source: 'openai-codex-wham',
10
+ quotaWindows: [
11
+ { label: '5H', source: 'openai-codex-oauth', usedPct: 42 },
12
+ { label: '7D', source: 'openai-codex-oauth', usedPct: 5 },
13
+ ],
14
+ balance: {
15
+ source: 'openai-codex-credits',
16
+ remainingUsd: 12.34,
17
+ },
18
+ }, { COLS: 120 });
19
+
20
+ assert.deepEqual(segments, ['5H 42%', '7D 5%']);
21
+ assert.equal(segments.some((segment) => /Credit|\$12\.34/.test(segment)), false);
22
+ });
23
+
24
+ test('formatGatewayLimitSegments falls back to percent for OpenAI OAuth credit windows', () => {
25
+ const segments = formatGatewayLimitSegments({
26
+ provider: 'openai-oauth',
27
+ providerKind: 'oauth',
28
+ quotaWindows: [
29
+ {
30
+ label: '5H',
31
+ source: 'openai-codex-oauth',
32
+ usedPct: 55,
33
+ remainingUsd: 4,
34
+ usedUsd: 6,
35
+ limitUsd: 10,
36
+ remainingCredits: 40,
37
+ usedCredits: 60,
38
+ limitCredits: 100,
39
+ },
40
+ ],
41
+ }, { COLS: 120 });
42
+
43
+ assert.deepEqual(segments, ['5H 55%']);
44
+ });
45
+
46
+ test('formatGatewayLimitSegments keeps non-OpenAI OAuth credit balance on L1', () => {
47
+ const segments = formatGatewayLimitSegments({
48
+ provider: 'anthropic-oauth',
49
+ providerKind: 'oauth',
50
+ source: 'anthropic-oauth',
51
+ quotaWindows: [
52
+ { label: '5H', source: 'anthropic-oauth', usedPct: 20 },
53
+ ],
54
+ balance: {
55
+ source: 'anthropic-oauth-extra',
56
+ remainingUsd: 7.5,
57
+ },
58
+ }, { COLS: 120 });
59
+
60
+ assert.ok(segments.includes('Credit $7.50'));
61
+ });
62
+
63
+ test('formatGatewayLimitSegments suppresses OpenAI OAuth credit via providerId even without a source hint', () => {
64
+ // Mirrors the merged quotaStatus shape (mergeQuotaStatus) where `source` may
65
+ // be absent and `balance.source` is generic: the providerId === openai-oauth
66
+ // branch alone must still suppress any Credit/$ leak on L1.
67
+ const segments = formatGatewayLimitSegments({
68
+ provider: 'openai-oauth',
69
+ providerKind: 'oauth',
70
+ quotaWindows: [
71
+ { label: '5H', source: 'openai-codex-oauth', usedPct: 30 },
72
+ ],
73
+ balance: {
74
+ remainingUsd: 99.99,
75
+ },
76
+ }, { COLS: 120 });
77
+
78
+ assert.deepEqual(segments, ['5H 30%']);
79
+ assert.equal(segments.some((segment) => /Credit|\$/.test(segment)), false);
80
+ });
@@ -139,7 +139,7 @@ export function setAnthropicBaseUrl(url, { dryRun = false, autoCompactWindow = n
139
139
 
140
140
  // RUNTIME base-url sync for live /model changes.
141
141
  //
142
- // Claude Code re-applies settings.env into process.env while the session is
142
+ // The host client re-applies settings.env into process.env while the session is
143
143
  // alive, but deleting ANTHROPIC_BASE_URL does not unset the already-present
144
144
  // process env key. For native Claude choices, write the official Anthropic base
145
145
  // URL instead. For mixdog choices, write the local gateway URL. This helper is
@@ -190,8 +190,8 @@ export function syncAnthropicBaseUrl(url, { dryRun = false, requireExisting = tr
190
190
  }
191
191
 
192
192
  // RUNTIME compact-window sync (Stage 4) — used by the gateway request path when
193
- // the routed model's context window changes at runtime (/model switch). Claude
194
- // Code watches settings.json (chokidar) and re-applies env to process.env
193
+ // the routed model's context window changes at runtime (/model switch). The host
194
+ // client watches settings.json (chokidar) and re-applies env to process.env
195
195
  // without restart, and getEffectiveContextWindowSize reads process.env live, so
196
196
  // rewriting CLAUDE_CODE_AUTO_COMPACT_WINDOW here makes the auto-compact
197
197
  // threshold track the live model next turn.
@@ -313,7 +313,7 @@ export function writeClaudeCodeCurrentSnapshot(ccJsonInput) {
313
313
  }
314
314
 
315
315
  export function formatClaudeCurrentChoiceLabel(route = readClaudeCodeCurrentRoute()) {
316
- const parts = [`Claude Code current: ${route.modelDisplay || route.model || 'current model'}`];
316
+ const parts = [`Current selection: ${route.modelDisplay || route.model || 'current model'}`];
317
317
  if (route.effort) parts.push(String(route.effort).toUpperCase());
318
318
  if (route.fast === true) parts.push('fast');
319
319
  return parts.join(' · ');
@@ -103,7 +103,7 @@ function defaultEffectiveContextWindowPercent(_provider) {
103
103
  // models.dev, or native provider catalogs). Reserve a small universal
104
104
  // headroom for output/tool/system tokens while keeping the raw model window
105
105
  // visible separately.
106
- return 95;
106
+ return 90;
107
107
  }
108
108
 
109
109
  function effectiveContextWindowPercent(provider, info = {}, seed = {}) {
@@ -124,6 +124,16 @@ function effectiveContextWindow(rawContextWindow, percent) {
124
124
  }
125
125
 
126
126
  function autoCompactTokenLimit(provider, rawContextWindow, contextWindow, info = {}, seed = {}) {
127
+ // routeInfo.autoCompactTokenLimit is an EXPLICIT auto-compaction limit only.
128
+ // It must NOT be derived from the effective/raw context window: the runtime
129
+ // (manager.mjs resolveSessionContextMeta / loop.mjs resolveWorkerCompactPolicy)
130
+ // treats autoCompactTokenLimit as the compaction trigger. Deriving it from the
131
+ // full boundary makes autoTriggerTokens == boundary, collapses the compaction
132
+ // buffer to 0, and auto-compaction only fires once the window is already full
133
+ // (where semantic compact overflows and the turn is lost). The boundary/window
134
+ // fallback that status + gateway env still need lives in compactBoundaryForRoute
135
+ // / autoCompactWindowForRoute, which read contextWindow/rawContextWindow
136
+ // directly. Return null when there is no explicit provider/catalog/seed limit.
127
137
  const explicit = num(
128
138
  seed.autoCompactTokenLimit
129
139
  ?? seed.auto_compact_token_limit
@@ -131,12 +141,25 @@ function autoCompactTokenLimit(provider, rawContextWindow, contextWindow, info =
131
141
  ?? info.auto_compact_token_limit,
132
142
  0,
133
143
  );
144
+ if (!(explicit > 0)) return null;
145
+ // An explicit limit is only a real auto-compaction trigger when it sits
146
+ // STRICTLY BELOW the boundary/window. A legacy seed/info value equal to or
147
+ // above the boundary is a derived full-window artifact — returning
148
+ // Math.min(explicit, derived) would still surface the boundary, which the
149
+ // runtime then treats as the trigger and collapses the compaction buffer.
150
+ // Drop those to null so display + downstream fall back to the default
151
+ // boundary trigger.
134
152
  const derived = num(contextWindow, 0) || num(rawContextWindow, 0);
135
- if (explicit > 0 && derived > 0) return Math.min(explicit, derived);
136
- if (explicit > 0) return explicit;
137
- return derived > 0 ? derived : null;
153
+ if (derived > 0) return explicit < derived ? explicit : null;
154
+ // No boundary known: keep the positive explicit value (cannot be proven to
155
+ // be a full-window artifact), but never derive one.
156
+ return explicit;
138
157
  }
139
158
 
159
+ // Test-only export of the explicit-vs-derived auto-compact-limit resolver
160
+ // (see scripts/compact-trigger-migration-smoke.mjs).
161
+ export const _autoCompactTokenLimit = autoCompactTokenLimit;
162
+
140
163
  function routeIdentityKey(routeInfo = {}) {
141
164
  return [
142
165
  routeInfo.provider || '',
@@ -333,7 +356,7 @@ function compactBoundaryForRoute(routeInfo = {}, compact = null) {
333
356
  return rawContextWindow > 0 ? rawContextWindow : 0;
334
357
  }
335
358
 
336
- // Single source of truth for the value handed to Claude Code's
359
+ // Single source of truth for the value synced to host env
337
360
  // CLAUDE_CODE_AUTO_COMPACT_WINDOW. BOTH the --enable-time write
338
361
  // (scripts/gateway-model.mjs) and the runtime sync (src/gateway/server.mjs)
339
362
  // must derive the window the SAME way, or they disagree on the value written to
@@ -418,7 +441,22 @@ export function summarizeGatewayUsage(routeInfo, providerOut, compact = null, du
418
441
  const routeSectionStartedAt = ensureRouteSection(routeInfo);
419
442
  const promptTokens = promptFootprintTokens(routeInfo?.provider, u);
420
443
  const boundaryTokens = contextUsageBoundary(routeInfo, compact);
421
- const contextUsedPct = boundaryTokens > 0 ? round(promptTokens * 100 / boundaryTokens, 2) : null;
444
+ // Prefer the transcript-estimate footprint for the displayed pct: it is
445
+ // monotonic and window-bounded, unlike provider input_tokens which can swing
446
+ // wildly (and produce >1000% pct) on some providers (e.g. OpenAI gpt-5.5).
447
+ // compact.afterTokens / beforeTokens = estimateMessagesTokens(working)+reserve.
448
+ const estimateFootprintTokens = num(compact?.afterTokens, 0) || num(compact?.beforeTokens, 0);
449
+ let contextUsedPct = null;
450
+ if (boundaryTokens > 0) {
451
+ if (estimateFootprintTokens > 0) {
452
+ // Estimate-based numerator may legitimately exceed boundary -> real >100%.
453
+ contextUsedPct = round(estimateFootprintTokens * 100 / boundaryTokens, 2);
454
+ } else {
455
+ // Fallback to provider tokens; clamp to a sane ceiling since these are
456
+ // not reliably window-bounded.
457
+ contextUsedPct = Math.min(100, round(promptTokens * 100 / boundaryTokens, 2));
458
+ }
459
+ }
422
460
  const costUsd = usageCostUsd(routeInfo, u);
423
461
  const usageCompact = compact && typeof compact === 'object' ? { ...compact } : compact;
424
462
  return {
@@ -226,7 +226,7 @@ export function gatewayConfigForSessionBoot(globalGateway = {}, sessionId = null
226
226
  }
227
227
  }
228
228
  }
229
- // 3. If Claude Code did not provide a reliable prior transcript on /new,
229
+ // 3. If the host session did not provide a reliable prior transcript on /new,
230
230
  // carry the last explicit route for this terminal only. This keeps multiple
231
231
  // terminals isolated without falling back to the process-global default.
232
232
  if (pid) {