mixdog 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/README.md +47 -23
  2. package/package.json +34 -28
  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 +205 -33
  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 +1222 -90
  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 -29
  62. package/src/lib/plugin-paths.cjs +1 -7
  63. package/src/lib/rules-builder.cjs +242 -98
  64. package/src/lib/text-utils.cjs +1 -1
  65. package/src/mixdog-session-runtime.mjs +2211 -446
  66. package/src/output-styles/default.md +12 -28
  67. package/src/output-styles/extreme-simple.md +9 -6
  68. package/src/output-styles/simple.md +22 -9
  69. package/src/repl.mjs +108 -59
  70. package/src/rules/agent/00-common.md +15 -0
  71. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  72. package/src/rules/agent/30-explorer.md +22 -0
  73. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  75. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  76. package/src/rules/lead/01-general.md +9 -5
  77. package/src/rules/lead/04-workflow.md +51 -12
  78. package/src/rules/lead/lead-tool.md +6 -0
  79. package/src/rules/shared/01-tool.md +12 -1
  80. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  81. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  85. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  86. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  87. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  88. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  89. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  90. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  92. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  93. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  94. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  95. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
  97. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  98. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  100. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  101. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  102. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  107. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
  109. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
  110. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
  113. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  114. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  115. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  116. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  117. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  118. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  119. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  120. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  121. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  122. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  123. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
  124. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
  125. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
  126. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  127. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  138. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  143. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  146. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  147. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  148. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  149. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  150. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  151. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  152. package/src/runtime/channels/index.mjs +149 -51
  153. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  154. package/src/runtime/channels/lib/config.mjs +0 -1
  155. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  156. package/src/runtime/channels/lib/executor.mjs +0 -3
  157. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  158. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  159. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  160. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  161. package/src/runtime/channels/lib/session-discovery.mjs +3 -6
  162. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
  164. package/src/runtime/channels/lib/webhook.mjs +1 -1
  165. package/src/runtime/channels/tool-defs.mjs +29 -29
  166. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  167. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  168. package/src/runtime/memory/index.mjs +519 -118
  169. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  170. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  171. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  172. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  173. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  174. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  175. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  176. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  177. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  178. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
  179. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  180. package/src/runtime/memory/lib/memory.mjs +88 -9
  181. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  182. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  183. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  184. package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
  185. package/src/runtime/memory/lib/session-ingest.mjs +196 -0
  186. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  187. package/src/runtime/memory/tool-defs.mjs +46 -37
  188. package/src/runtime/search/index.mjs +104 -473
  189. package/src/runtime/search/lib/config.mjs +0 -4
  190. package/src/runtime/search/lib/state.mjs +1 -15
  191. package/src/runtime/search/lib/web-tools.mjs +44 -25
  192. package/src/runtime/search/tool-defs.mjs +16 -23
  193. package/src/runtime/shared/abort-controller.mjs +1 -1
  194. package/src/runtime/shared/atomic-file.mjs +4 -3
  195. package/src/runtime/shared/background-tasks.mjs +122 -11
  196. package/src/runtime/shared/child-spawn-gate.mjs +139 -0
  197. package/src/runtime/shared/config.mjs +7 -4
  198. package/src/runtime/shared/err-text.mjs +131 -4
  199. package/src/runtime/shared/llm/cost.mjs +2 -2
  200. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  201. package/src/runtime/shared/llm/index.mjs +34 -11
  202. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  203. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  204. package/src/runtime/shared/singleton-owner.mjs +104 -0
  205. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  206. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  207. package/src/runtime/shared/tool-surface.mjs +624 -95
  208. package/src/runtime/shared/user-data-guard.mjs +0 -2
  209. package/src/standalone/agent-task-status.mjs +203 -0
  210. package/src/standalone/agent-task-status.test.mjs +76 -0
  211. package/src/standalone/agent-tool.mjs +1913 -0
  212. package/src/standalone/channel-worker.mjs +370 -14
  213. package/src/standalone/explore-tool.mjs +165 -70
  214. package/src/standalone/folder-dialog.mjs +314 -0
  215. package/src/standalone/hook-bus.mjs +898 -22
  216. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  217. package/src/standalone/projects.mjs +226 -0
  218. package/src/standalone/provider-admin.mjs +41 -24
  219. package/src/standalone/seeds.mjs +1 -78
  220. package/src/standalone/usage-dashboard.mjs +96 -8
  221. package/src/tui/App.jsx +4800 -2140
  222. package/src/tui/components/AnsiText.jsx +39 -28
  223. package/src/tui/components/ContextPanel.jsx +14 -4
  224. package/src/tui/components/Markdown.jsx +43 -77
  225. package/src/tui/components/MarkdownTable.jsx +9 -184
  226. package/src/tui/components/Message.jsx +28 -11
  227. package/src/tui/components/Picker.jsx +95 -56
  228. package/src/tui/components/PromptInput.jsx +428 -239
  229. package/src/tui/components/QueuedCommands.jsx +1 -1
  230. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  231. package/src/tui/components/Spinner.jsx +67 -38
  232. package/src/tui/components/StatusLine.jsx +606 -38
  233. package/src/tui/components/TextEntryPanel.jsx +128 -9
  234. package/src/tui/components/ToolExecution.jsx +597 -362
  235. package/src/tui/components/TurnDone.jsx +3 -3
  236. package/src/tui/components/UsagePanel.jsx +3 -5
  237. package/src/tui/components/tool-output-format.mjs +499 -0
  238. package/src/tui/components/tool-output-format.test.mjs +312 -0
  239. package/src/tui/dist/index.mjs +9192 -2399
  240. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  241. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  242. package/src/tui/engine.mjs +1455 -279
  243. package/src/tui/figures.mjs +21 -40
  244. package/src/tui/index.jsx +75 -31
  245. package/src/tui/input-editing.mjs +25 -0
  246. package/src/tui/markdown/format-token.mjs +671 -69
  247. package/src/tui/markdown/format-token.test.mjs +312 -0
  248. package/src/tui/markdown/render-ansi.mjs +94 -0
  249. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  250. package/src/tui/markdown/stream-fence.mjs +34 -0
  251. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  252. package/src/tui/markdown/table-layout.mjs +250 -0
  253. package/src/tui/paste-attachments.mjs +0 -7
  254. package/src/tui/spinner-verbs.mjs +1 -2
  255. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  256. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  257. package/src/tui/theme.mjs +756 -24
  258. package/src/tui/time-format.mjs +1 -1
  259. package/src/tui/transcript-tool-failures.mjs +67 -0
  260. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  261. package/src/ui/ansi.mjs +1 -2
  262. package/src/ui/markdown.mjs +85 -26
  263. package/src/ui/markdown.test.mjs +70 -0
  264. package/src/ui/model-display.mjs +121 -0
  265. package/src/ui/session-stats.mjs +44 -0
  266. package/src/ui/statusline-context-label.test.mjs +15 -0
  267. package/src/ui/statusline.mjs +386 -178
  268. package/src/ui/tool-card.mjs +2 -13
  269. package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
  270. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  271. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  272. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  273. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  274. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  275. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  276. package/src/workflows/default/WORKFLOW.md +12 -5
  277. package/src/workflows/default/workflow.json +0 -1
  278. package/src/workflows/solo/WORKFLOW.md +15 -0
  279. package/src/workflows/solo/workflow.json +7 -0
  280. package/vendor/ink/build/ink.js +54 -8
  281. package/vendor/ink/build/output.js +6 -1
  282. package/src/agents/scheduler-task.md +0 -3
  283. package/src/agents/web-researcher/AGENT.md +0 -3
  284. package/src/agents/web-researcher/agent.json +0 -6
  285. package/src/agents/webhook-handler.md +0 -3
  286. package/src/hooks/lib/permission-rules.cjs +0 -170
  287. package/src/hooks/lib/settings-loader.cjs +0 -112
  288. package/src/lib/hook-pipe-path.cjs +0 -10
  289. package/src/rules/bridge/00-common.md +0 -5
  290. package/src/rules/bridge/30-explorer.md +0 -4
  291. package/src/rules/lead/00-tool-lead.md +0 -5
  292. package/src/rules/shared/00-language.md +0 -3
  293. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  294. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  295. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  296. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  297. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  298. package/src/standalone/bridge-tool.mjs +0 -1414
  299. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -11,15 +11,18 @@
11
11
  */
12
12
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
13
13
  import { homedir } from 'node:os';
14
- import { basename, join } from 'node:path';
15
- import { bold, colorEnabled, green, rgb } from './ansi.mjs';
14
+ import { join } from 'node:path';
15
+ import { bold, colorEnabled, rgb } from './ansi.mjs';
16
+ import { displayModelName, shortenModelName } from './model-display.mjs';
17
+ import { createSessionStats } from './session-stats.mjs';
16
18
  import { forEachSessionRuntime } from '../runtime/agent/orchestrator/session/manager.mjs';
17
19
  import { listHiddenRoleNames } from '../runtime/agent/orchestrator/internal-roles.mjs';
18
20
  import { getModelMetadataSync } from '../runtime/agent/orchestrator/providers/model-catalog.mjs';
19
21
  import { readCachedOAuthUsageSnapshot } from '../runtime/agent/orchestrator/providers/oauth-usage.mjs';
20
22
  import { readCachedOpenCodeGoUsageSnapshot } from '../runtime/agent/orchestrator/providers/opencode-go-usage.mjs';
21
23
  import { buildGatewayLimits } from '../runtime/agent/orchestrator/providers/statusline-route-meta.mjs';
22
- import { formatGatewayLimitSegments, loadGatewayStatus } from '../vendor/statusline/bin/statusline-route.mjs';
24
+ import { compactBoundaryForStatus, formatGatewayLimitSegments, loadGatewayStatus } from '../vendor/statusline/bin/statusline-route.mjs';
25
+ export { createSessionStats, applyUsageDelta } from './session-stats.mjs';
23
26
 
24
27
  // Token window used to compute a fallback context% from our own session usage.
25
28
  // The live gateway (when up) overrides this with the real route's window. This
@@ -45,62 +48,63 @@ const CYN = sgr('38;2;136;136;136');
45
48
  const GREY = sgr('38;2;136;136;136');
46
49
  const SHELL_JOBS_SEGMENT_CACHE_MS = 1000;
47
50
  const GATEWAY_QUOTA_STATUS_CACHE_MS = 500;
51
+ const WORKER_SPINNER_FRAMES = Object.freeze(['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']);
52
+ const WORKER_SPINNER_FRAME_MS = 160;
53
+ // L2 segment spinner: reuses the original WORKER_SPINNER_FRAMES dot glyphs (no
54
+ // separate glyph list) but spins them at a faster 120ms step than the worker
55
+ // spinner's 160ms. l2SpinnerFrame() indexes straight into WORKER_SPINNER_FRAMES.
56
+ const L2_SPINNER_FRAME_MS = 120;
57
+ // Keep the last known usage snapshot visible while idle. The runtime still
58
+ // refreshes OAuth usage in the background, but if that refresh is delayed or
59
+ // fails, the statusline should not blink/drop the usage segment; it should hold
60
+ // the last captured 5H/7D values from THIS process until a newer snapshot
61
+ // replaces them. Snapshots from a previous launch stay hidden during boot so the
62
+ // statusline starts empty until the current session captures usage once.
63
+ const STATUSLINE_PROCESS_STARTED_AT_MS = Date.now() - Math.floor((Number(process.uptime?.()) || 0) * 1000);
48
64
  const DEFAULT_HIDDEN_STATUSLINE_ROLES = Object.freeze(['explorer', 'cycle1-agent', 'cycle2-agent', 'cycle3-agent', 'scheduler-task', 'webhook-handler']);
49
- let _shellJobsSegmentCache = { ownerPid: 0, at: 0, value: '' };
65
+ let _shellJobsSegmentCache = { ownerPid: 0, at: 0, value: { count: 0, elapsedLabel: '' } };
50
66
  let _gatewayQuotaStatusCache = { key: '', at: 0, value: null };
51
67
  let _fallbackQuotaStatusCache = { key: '', at: 0, value: null };
52
68
  let _hiddenStatuslineRoles = null;
53
-
54
- function summarizeWorkerTags(tags, limit = 3) {
55
- const cleanTags = [...new Set((Array.isArray(tags) ? tags : [])
56
- .map((tag) => String(tag || '').trim())
69
+ // Option A boot gate: the L1 usage/quota segment stays fully empty until THIS
70
+ // process has captured its FIRST confirmed (current-process) OAuth usage
71
+ // snapshot for THAT provider. The latch is monotonic PER PROVIDER — once a
72
+ // provider arms it stays on for the process lifetime, so its segment turns on
73
+ // exactly once (single clean transition) and then holds. This suppresses the
74
+ // early gateway active-instance quota/balance (from another owning process,
75
+ // which is NOT process-start guarded) and any stale/in-progress reads before
76
+ // the first confirmed snapshot lands. Keyed per provider (not per global) so an
77
+ // in-process route switch (openai-oauth → anthropic-oauth / grok-oauth) re-gates
78
+ // the new provider until ITS own confirmed snapshot exists — otherwise the new
79
+ // provider's stale fallback balance (Credit $…) could leak prematurely.
80
+ const _oauthUsageArmedProviders = new Set();
81
+
82
+ function isConfirmedCurrentProcessSnapshot(snapshot) {
83
+ if (!snapshot || typeof snapshot !== 'object') return false;
84
+ const cachedAt = num(snapshot.cachedAt);
85
+ return cachedAt > 0 && cachedAt >= STATUSLINE_PROCESS_STARTED_AT_MS;
86
+ }
87
+
88
+ function summarizeWorkerTags(workers, limit = 3) {
89
+ const cleanLabels = [...new Set((Array.isArray(workers) ? workers : [])
90
+ .map((worker) => String(worker?.tag || '').trim())
57
91
  .filter(Boolean))];
58
- if (cleanTags.length <= limit) return cleanTags.join(', ');
59
- return `${cleanTags.slice(0, limit).join(', ')}, +${cleanTags.length - limit}`;
92
+ if (cleanLabels.length <= limit) return cleanLabels.join(', ');
93
+ return `${cleanLabels.slice(0, limit).join(', ')}, +${cleanLabels.length - limit}`;
60
94
  }
61
95
 
62
- /** Create a mutable session-usage accumulator. */
63
- export function createSessionStats() {
64
- return {
65
- inputTokens: 0,
66
- outputTokens: 0,
67
- cachedTokens: 0,
68
- cacheWriteTokens: 0,
69
- promptTokens: 0,
70
- latestInputTokens: 0,
71
- latestOutputTokens: 0,
72
- latestCachedTokens: 0,
73
- latestCacheWriteTokens: 0,
74
- latestPromptTokens: 0,
75
- costUsd: 0,
76
- turns: 0,
77
- };
96
+ function workerSpinnerFrame(now = Date.now()) {
97
+ const index = Math.floor(now / WORKER_SPINNER_FRAME_MS) % WORKER_SPINNER_FRAMES.length;
98
+ return WORKER_SPINNER_FRAMES[index] || WORKER_SPINNER_FRAMES[0];
78
99
  }
79
100
 
80
- /**
81
- * Fold one `onUsageDelta` event into the accumulator.
82
- * @param {object} stats — from createSessionStats()
83
- * @param {object} delta — { deltaInput, deltaOutput, deltaCachedRead, deltaCacheWrite, costUsd }
84
- */
85
- export function applyUsageDelta(stats, delta = {}) {
86
- if (!stats || !delta) return stats;
87
- stats.inputTokens += num(delta.deltaInput);
88
- stats.outputTokens += num(delta.deltaOutput);
89
- stats.cachedTokens += num(delta.deltaCachedRead);
90
- stats.cacheWriteTokens += num(delta.deltaCacheWrite);
91
- stats.promptTokens += num(delta.deltaPrompt);
92
- stats.latestInputTokens = num(delta.deltaInput);
93
- stats.latestOutputTokens = num(delta.deltaOutput);
94
- stats.latestCachedTokens = num(delta.deltaCachedRead);
95
- stats.latestCacheWriteTokens = num(delta.deltaCacheWrite);
96
- stats.latestPromptTokens = num(delta.deltaPrompt);
97
- // costUsd from the engine is cumulative-per-call; we sum the per-turn deltas.
98
- stats.costUsd += num(delta.costUsd);
99
- return stats;
101
+ function l2SpinnerFrame(now = Date.now()) {
102
+ const index = Math.floor(now / L2_SPINNER_FRAME_MS) % WORKER_SPINNER_FRAMES.length;
103
+ return WORKER_SPINNER_FRAMES[index] || WORKER_SPINNER_FRAMES[0];
100
104
  }
101
105
 
102
106
  /**
103
- * Build the Claude-Code-shaped JSON the vendored renderer reads, from our REPL
107
+ * Build the status-line JSON the vendored renderer reads, from our REPL
104
108
  * state. Only the fields `renderStatusLine()` actually consumes are emitted:
105
109
  * - display_name → model name (L1)
106
110
  * - effort.level → effort string
@@ -125,22 +129,23 @@ function promptFootprintTokens(provider, stats) {
125
129
  return input;
126
130
  }
127
131
 
128
- function currentContextTokens(provider, stats) {
132
+ function activeContextNumerator(provider, stats) {
129
133
  const s = stats || createSessionStats();
130
134
  const source = String(s.currentContextSource || '').toLowerCase();
131
- const explicit = source === 'estimated'
132
- ? 0
133
- : num(s.currentContextTokens ?? s.contextTokens ?? s.latestPromptTokens);
134
- if (explicit > 0) return explicit;
135
- const latestInput = num(s.latestInputTokens);
136
- const latestCacheRead = num(s.latestCachedTokens);
137
- const latestCacheWrite = num(s.latestCacheWriteTokens);
138
- if (latestInput || latestCacheRead || latestCacheWrite) {
139
- return providerInputExcludesCache(provider)
140
- ? latestInput + latestCacheRead + latestCacheWrite
141
- : latestInput;
135
+ const estimated = num(s.currentEstimatedContextTokens);
136
+ if (estimated > 0) return estimated;
137
+ if (source === 'estimated') return 0;
138
+ if (source === 'last_api_request') {
139
+ const apiUsed = num(s.currentContextTokens);
140
+ if (apiUsed > 0) return apiUsed;
142
141
  }
143
- return promptFootprintTokens(provider, s);
142
+ const explicit = num(s.currentContextTokens ?? s.contextTokens);
143
+ if (explicit > 0) return explicit;
144
+ return 0;
145
+ }
146
+
147
+ function currentContextTokens(provider, stats) {
148
+ return activeContextNumerator(provider, stats);
144
149
  }
145
150
 
146
151
  function modelContextWindow(provider, model, explicitContextWindow = 0) {
@@ -151,7 +156,61 @@ function modelContextWindow(provider, model, explicitContextWindow = 0) {
151
156
  return FALLBACK_CONTEXT_WINDOW;
152
157
  }
153
158
 
154
- function normalizeBridgeWorkerForStatusline(worker = {}) {
159
+ function displayContextBoundary({
160
+ contextWindow = 0,
161
+ displayContextWindow = 0,
162
+ rawContextWindow = 0,
163
+ compactBoundaryTokens = 0,
164
+ autoCompactTokenLimit = 0,
165
+ compact = null,
166
+ } = {}) {
167
+ const boundarySeed = num(compactBoundaryTokens) > 0
168
+ ? num(compactBoundaryTokens)
169
+ : (num(displayContextWindow) > 0 ? num(displayContextWindow) : num(contextWindow));
170
+ const boundary = compactBoundaryForStatus({
171
+ contextWindow: boundarySeed,
172
+ rawContextWindow: num(rawContextWindow),
173
+ autoCompactTokenLimit: num(autoCompactTokenLimit),
174
+ }, compact);
175
+ if (Number.isFinite(boundary) && boundary > 0) return boundary;
176
+ return modelContextWindow('', '', boundarySeed);
177
+ }
178
+
179
+ function resolveContextUsedPct({
180
+ provider = '',
181
+ model = '',
182
+ stats = null,
183
+ contextWindow = 0,
184
+ displayContextWindow = 0,
185
+ rawContextWindow = 0,
186
+ compactBoundaryTokens = 0,
187
+ autoCompactTokenLimit = 0,
188
+ gatewayStatus = null,
189
+ } = {}) {
190
+ const numerator = activeContextNumerator(provider, stats);
191
+ const compact = gatewayStatus?.lastUsage?.compact || null;
192
+ const boundary = displayContextBoundary({
193
+ contextWindow,
194
+ displayContextWindow,
195
+ rawContextWindow,
196
+ compactBoundaryTokens,
197
+ autoCompactTokenLimit,
198
+ compact,
199
+ });
200
+ const gatewayRawPct = gatewayStatus?.contextUsedPct;
201
+ if (
202
+ gatewayStatus
203
+ && gatewayRawPct !== null
204
+ && gatewayRawPct !== undefined
205
+ ) {
206
+ const gatewayPct = Number(gatewayRawPct);
207
+ if (Number.isFinite(gatewayPct)) return gatewayPct;
208
+ }
209
+ if (boundary > 0) return (numerator / boundary) * 100;
210
+ return 0;
211
+ }
212
+
213
+ function normalizeAgentWorkerForStatusline(worker = {}) {
155
214
  const tag = String(worker.tag || worker.role || worker.name || '').trim();
156
215
  if (!tag) return null;
157
216
  const statusText = String(worker.stage || worker.status || '').toLowerCase();
@@ -159,6 +218,7 @@ function normalizeBridgeWorkerForStatusline(worker = {}) {
159
218
  return {
160
219
  tag,
161
220
  status,
221
+ startedAtMs: timeMs(worker.startedAt || worker.startTime || worker.createdAt),
162
222
  role: worker.role || null,
163
223
  stage: worker.stage || worker.status || null,
164
224
  sessionId: worker.sessionId || null,
@@ -167,7 +227,7 @@ function normalizeBridgeWorkerForStatusline(worker = {}) {
167
227
  };
168
228
  }
169
229
 
170
- function normalizeBridgeJobForStatusline(job = {}) {
230
+ function normalizeAgentJobForStatusline(job = {}) {
171
231
  const statusText = String(job.status || job.stage || '').toLowerCase();
172
232
  if (!statusText) return null;
173
233
  const taskId = String(job.task_id || job.taskId || '').trim();
@@ -195,6 +255,7 @@ function normalizeBridgeJobForStatusline(job = {}) {
195
255
  tag,
196
256
  taskId,
197
257
  status: 'running',
258
+ startedAtMs,
198
259
  role: job.role || null,
199
260
  stage: job.stage || job.workerStatus || job.status || null,
200
261
  sessionId: job.sessionId || null,
@@ -203,15 +264,15 @@ function normalizeBridgeJobForStatusline(job = {}) {
203
264
  };
204
265
  }
205
266
 
206
- function bridgeStatuslinePayload(bridgeWorkers = [], bridgeJobs = []) {
267
+ function agentStatuslinePayload(agentWorkers = [], agentJobs = []) {
207
268
  const byTag = new Map();
208
269
  const finishedJobs = [];
209
- for (const worker of Array.isArray(bridgeWorkers) ? bridgeWorkers : []) {
210
- const row = normalizeBridgeWorkerForStatusline(worker);
270
+ for (const worker of Array.isArray(agentWorkers) ? agentWorkers : []) {
271
+ const row = normalizeAgentWorkerForStatusline(worker);
211
272
  if (row) byTag.set(row.tag, row);
212
273
  }
213
- for (const job of Array.isArray(bridgeJobs) ? bridgeJobs : []) {
214
- const row = normalizeBridgeJobForStatusline(job);
274
+ for (const job of Array.isArray(agentJobs) ? agentJobs : []) {
275
+ const row = normalizeAgentJobForStatusline(job);
215
276
  if (!row) continue;
216
277
  if (row.status === 'finished') {
217
278
  finishedJobs.push(row);
@@ -246,23 +307,57 @@ function bridgeStatuslinePayload(bridgeWorkers = [], bridgeJobs = []) {
246
307
  * @param {string} [opts.sessionId]
247
308
  * @returns {Promise<string>}
248
309
  */
249
- export async function renderStatusline({ provider = '', model = '', effort = '', fast = false, cwd = '', stats, sessionId, contextWindow = 0, rawContextWindow = 0, bridgeWorkers = [], bridgeJobs = [], clientHostPid = process.pid } = {}) {
310
+ export async function renderStatusline({
311
+ provider = '', model = '', effort = '', fast = false, cwd = '', stats, sessionId,
312
+ contextWindow = 0, displayContextWindow = 0, rawContextWindow = 0,
313
+ compactBoundaryTokens = 0, autoCompactTokenLimit = 0,
314
+ agentWorkers = [], agentJobs = [], activeTools = null, clientHostPid = process.pid,
315
+ } = {}) {
316
+ const displayArgs = {
317
+ contextWindow, displayContextWindow, rawContextWindow, compactBoundaryTokens, autoCompactTokenLimit,
318
+ };
250
319
  try {
251
- return renderNativeStatusline({ provider, model, effort, fast, cwd, stats, sessionId, contextWindow, rawContextWindow, bridgeWorkers, bridgeJobs, clientHostPid });
320
+ return renderNativeStatusline({
321
+ provider, model, effort, fast, cwd, stats, sessionId, agentWorkers, agentJobs, activeTools, clientHostPid,
322
+ ...displayArgs,
323
+ });
252
324
  } catch {
253
- return fallbackLine({ provider, model, effort, fast, cwd, stats });
325
+ return fallbackLine({ provider, model, effort, fast, cwd, stats, ...displayArgs });
254
326
  }
255
327
  }
256
328
 
257
329
  // --- helpers -----------------------------------------------------------------
258
330
 
259
- function renderNativeStatusline({ provider = '', model = '', effort = '', fast = false, stats, sessionId, contextWindow = 0, rawContextWindow = 0, bridgeWorkers = [], bridgeJobs = [], clientHostPid = process.pid } = {}) {
331
+ function renderNativeStatusline({
332
+ provider = '', model = '', effort = '', fast = false, stats, sessionId,
333
+ contextWindow = 0, displayContextWindow = 0, rawContextWindow = 0,
334
+ compactBoundaryTokens = 0, autoCompactTokenLimit = 0,
335
+ agentWorkers = [], agentJobs = [], activeTools = null, clientHostPid = process.pid,
336
+ } = {}) {
260
337
  const cols = terminalColumns();
261
338
  const s = stats || createSessionStats();
262
- const contextTokens = currentContextTokens(provider, s);
263
- const resolvedContextWindow = modelContextWindow(provider, model, contextWindow);
264
- const ctxPct = resolvedContextWindow > 0 ? clampPct((contextTokens / resolvedContextWindow) * 100) : 0;
265
- const gatewayStatus = loadGatewayQuotaStatus({ provider, sessionId, activeContextTokens: contextTokens, clientHostPid });
339
+ const contextTokens = activeContextNumerator(provider, s);
340
+ const routeContextWindow = num(displayContextWindow) > 0 ? num(displayContextWindow) : num(contextWindow);
341
+ const gatewayStatus = loadGatewayQuotaStatus({
342
+ provider, model, effort, fast,
343
+ contextWindow: routeContextWindow,
344
+ rawContextWindow,
345
+ autoCompactTokenLimit,
346
+ sessionId,
347
+ activeContextTokens: contextTokens,
348
+ clientHostPid,
349
+ });
350
+ const ctxPct = resolveContextUsedPct({
351
+ provider,
352
+ model,
353
+ stats: s,
354
+ contextWindow,
355
+ displayContextWindow,
356
+ rawContextWindow,
357
+ compactBoundaryTokens,
358
+ autoCompactTokenLimit,
359
+ gatewayStatus,
360
+ });
266
361
 
267
362
  const sep = ` ${D}│${R} `;
268
363
  const l1Parts = [];
@@ -273,22 +368,63 @@ function renderNativeStatusline({ provider = '', model = '', effort = '', fast =
273
368
  addL1(formatModelSegment({ provider, model, effort, fast, cols }));
274
369
  addL1(formatContextSegment(ctxPct, cols));
275
370
 
276
- const quotaStatus = gatewayStatus || fallbackQuotaStatus({ provider, model });
371
+ // Option A boot gate: for OAuth routes, render NOTHING for the usage/quota
372
+ // segment until this process has captured its first confirmed (current-
373
+ // process) OAuth usage snapshot. This suppresses the startup jitter where the
374
+ // gateway active-instance quota windows (not process-start guarded) and the
375
+ // boot-guarded OAuth cache windows would otherwise pop in/merge at different
376
+ // ticks. Model + context% always render (built above). Non-OAuth routes are
377
+ // unaffected. Once armed, the latch holds for the process lifetime so the
378
+ // segment turns on exactly once and then holds the last known value as today.
379
+ const usageReady = oauthUsageSegmentReady({ provider, model });
380
+ const quotaStatus = usageReady
381
+ ? mergeQuotaStatus(gatewayStatus, fallbackQuotaStatus({ provider, model }))
382
+ : null;
277
383
  const quotaSegments = quotaStatus
278
384
  ? formatGatewayLimitSegments(quotaStatus, { COLS: cols, D, R, GRN, YLW, RED, colourPct, epochMsToHHMM })
279
385
  : [];
280
386
  for (const seg of quotaSegments) addL1(seg);
281
387
 
282
- const bridgePayload = bridgeStatuslinePayload([
283
- ...(Array.isArray(bridgeWorkers) ? bridgeWorkers : []),
388
+ const agentPayload = agentStatuslinePayload([
389
+ ...(Array.isArray(agentWorkers) ? agentWorkers : []),
284
390
  ...activeHiddenRoleWorkers({ sessionId, clientHostPid }),
285
- ], bridgeJobs);
286
- const { maintenance, runningWorkers } = classifyBridgeWorkers(bridgePayload.workers);
391
+ ], agentJobs);
392
+ const { maintenance, runningWorkers } = classifyAgentWorkers(agentPayload.workers);
287
393
  if (maintenance.length) addL1(maintenance.join(' '));
288
- addL1(formatShellJobsSegment({ clientHostPid }));
394
+ const shellStatus = shellJobsStatus({ clientHostPid });
289
395
 
396
+ const spinnerNow = Date.now();
397
+ const sp = l2SpinnerFrame(spinnerNow);
398
+ const spin = `${GRN}${sp}${R}`;
399
+ const elapsedSuffix = (label) => (label ? ` ${D}·${R} ${label}` : '');
400
+ // Segment order: Running Agents → Exploring → Searching → Running Shells.
290
401
  if (runningWorkers.length) {
291
- addL2(`${GRN}●${R} ${B}${runningWorkers.length} Running${R} ${D}(${R}${B}${summarizeWorkerTags(runningWorkers)}${R}${D})${R}`);
402
+ const n = runningWorkers.length;
403
+ const label = `Running ${n} Agent${n === 1 ? '' : 's'}`;
404
+ const tagSummary = summarizeWorkerTags(runningWorkers);
405
+ const tags = tagSummary ? ` ${D}(${R}${B}${tagSummary}${R}${D})${R}` : '';
406
+ const oldestStart = runningWorkers.reduce((min, w) => {
407
+ const t = num(w?.startedAtMs);
408
+ return t > 0 && t < min ? t : min;
409
+ }, Infinity);
410
+ const elapsed = Number.isFinite(oldestStart) ? formatElapsed(Date.now() - oldestStart) : '';
411
+ addL2(`${spin} ${B}${label}${R}${tags}${elapsedSuffix(elapsed)}`);
412
+ }
413
+ const tools = activeTools && typeof activeTools === 'object' ? activeTools : {};
414
+ const exploreInfo = tools.explore || null;
415
+ const searchInfo = tools.search || null;
416
+ if (exploreInfo && num(exploreInfo.count) > 0) {
417
+ const elapsed = num(exploreInfo.startedAt) > 0 ? formatElapsed(Date.now() - num(exploreInfo.startedAt)) : '';
418
+ addL2(`${spin} ${B}Exploring${R}${elapsedSuffix(elapsed)}`);
419
+ }
420
+ if (searchInfo && num(searchInfo.count) > 0) {
421
+ const elapsed = num(searchInfo.startedAt) > 0 ? formatElapsed(Date.now() - num(searchInfo.startedAt)) : '';
422
+ addL2(`${spin} ${B}Searching${R}${elapsedSuffix(elapsed)}`);
423
+ }
424
+ if (shellStatus.count > 0) {
425
+ const n = shellStatus.count;
426
+ const label = `Running ${n} Shell${n === 1 ? '' : 's'}`;
427
+ addL2(`${spin} ${B}${label}${R}${elapsedSuffix(shellStatus.elapsedLabel)}`);
292
428
  }
293
429
  const l1 = l1Parts.join(sep) || 'mixdog';
294
430
  const l2 = l2Parts.join(sep);
@@ -304,9 +440,15 @@ function dataDir() {
304
440
  return process.env.MIXDOG_DATA_DIR || DEFAULT_STANDALONE_DATA_DIR;
305
441
  }
306
442
 
307
- function loadGatewayQuotaStatus({ provider, sessionId, activeContextTokens, clientHostPid } = {}) {
443
+ function loadGatewayQuotaStatus({
444
+ provider, model, effort, fast, contextWindow, rawContextWindow, autoCompactTokenLimit = 0,
445
+ sessionId, activeContextTokens, clientHostPid,
446
+ } = {}) {
308
447
  const key = [
309
448
  String(provider || ''),
449
+ String(model || ''),
450
+ String(effort || ''),
451
+ fast === true ? 'fast' : '',
310
452
  String(sessionId || ''),
311
453
  String(clientHostPid || ''),
312
454
  Math.floor((Number(activeContextTokens) || 0) / 1024),
@@ -317,7 +459,20 @@ function loadGatewayQuotaStatus({ provider, sessionId, activeContextTokens, clie
317
459
  }
318
460
  let value = null;
319
461
  try {
320
- const status = loadGatewayStatus({ sessionId, activeContextTokens, clientHostPid });
462
+ const status = loadGatewayStatus({
463
+ sessionId,
464
+ activeContextTokens,
465
+ clientHostPid,
466
+ currentRoute: {
467
+ provider,
468
+ model,
469
+ effort,
470
+ fast,
471
+ contextWindow,
472
+ rawContextWindow,
473
+ autoCompactTokenLimit,
474
+ },
475
+ });
321
476
  if (!status) {
322
477
  _gatewayQuotaStatusCache = { key, at: now, value };
323
478
  return value;
@@ -328,6 +483,12 @@ function loadGatewayQuotaStatus({ provider, sessionId, activeContextTokens, clie
328
483
  _gatewayQuotaStatusCache = { key, at: now, value };
329
484
  return value;
330
485
  }
486
+ const statusModel = String(status.model || '').trim();
487
+ const cliModel = String(model || '').trim();
488
+ if (cliModel && statusModel && statusModel !== cliModel) {
489
+ _gatewayQuotaStatusCache = { key, at: now, value };
490
+ return value;
491
+ }
331
492
  value = status;
332
493
  } catch {
333
494
  value = null;
@@ -336,6 +497,38 @@ function loadGatewayQuotaStatus({ provider, sessionId, activeContextTokens, clie
336
497
  return value;
337
498
  }
338
499
 
500
+ // Option A boot gate. Returns true once THIS process has captured its first
501
+ // confirmed (current-process) OAuth usage snapshot for THIS provider, and stays
502
+ // true afterwards (monotonic, per-provider latch). Non-OAuth providers are never
503
+ // gated (they have no async usage fetch on this path). Keyed on provider only:
504
+ // the OAuth cache lookup is provider-keyed and snapshots are stored provider-
505
+ // wide (oauth-usage.mjs writes a provider-only key + uses newestProviderSnapshot
506
+ // fallback), so per-provider arming matches the data granularity — a model
507
+ // switch within one provider shares the same provider-wide snapshot. The latch
508
+ // reads the same cache `fallbackQuotaStatus()` consumes so it flips in lock-step
509
+ // with the data actually becoming renderable — no extra delay, single clean
510
+ // transition.
511
+ function oauthUsageSegmentReady({ provider, model } = {}) {
512
+ const normalizedProvider = String(provider || '').trim().toLowerCase();
513
+ if (!normalizedProvider.includes('oauth')) return true;
514
+ if (_oauthUsageArmedProviders.has(normalizedProvider)) return true;
515
+ let snapshot = null;
516
+ try {
517
+ snapshot = readCachedOAuthUsageSnapshot({
518
+ provider: normalizedProvider,
519
+ model: String(model || '').trim(),
520
+ providerKind: providerKindForQuota(normalizedProvider),
521
+ }, { allowStale: true });
522
+ } catch {
523
+ snapshot = null;
524
+ }
525
+ if (isConfirmedCurrentProcessSnapshot(snapshot)) {
526
+ _oauthUsageArmedProviders.add(normalizedProvider);
527
+ return true;
528
+ }
529
+ return false;
530
+ }
531
+
339
532
  function fallbackQuotaStatus({ provider, model } = {}) {
340
533
  const normalizedProvider = String(provider || '').trim().toLowerCase();
341
534
  if (!normalizedProvider) return null;
@@ -358,9 +551,18 @@ function fallbackQuotaStatus({ provider, model } = {}) {
358
551
  }
359
552
  } else if (normalizedProvider.includes('oauth')) {
360
553
  try {
361
- usageSnapshot = readCachedOAuthUsageSnapshot(routeInfo);
554
+ usageSnapshot = readCachedOAuthUsageSnapshot(routeInfo, { allowStale: true });
362
555
  } catch {}
363
556
  }
557
+ // Boot guard: do not render previous-launch usage before the current runtime
558
+ // has captured at least one snapshot. Once a snapshot is captured in this
559
+ // process, keep it visible while idle even if refreshes are delayed.
560
+ if (usageSnapshot) {
561
+ const cachedAt = num(usageSnapshot.cachedAt, 0);
562
+ if (!cachedAt || cachedAt < STATUSLINE_PROCESS_STARTED_AT_MS) {
563
+ usageSnapshot = { ...usageSnapshot, quotaWindows: [] };
564
+ }
565
+ }
364
566
  try {
365
567
  const limits = buildGatewayLimits(routeInfo, null, usageSnapshot);
366
568
  if (!limits?.quotaWindows?.length && !limits?.balance && !limits?.routeSpend) {
@@ -389,82 +591,53 @@ function providerKindForQuota(provider) {
389
591
  return 'api';
390
592
  }
391
593
 
392
- function formatModelSegment({ provider, model, effort, fast, cols }) {
393
- const modelName = shortenModelName(displayModelName(provider, model), cols);
394
- const bits = [`${CYN}◆${R} ${B}${modelName}${R}`];
395
- if (effort) bits.push(`${B}${String(effort).toUpperCase()}${R}`);
396
- if (fast === true) bits.push(`${B}FAST${R}`);
397
- return bits.join(` ${D}·${R} `);
594
+ function mergeQuotaStatus(primary, fallback) {
595
+ if (!primary) return fallback || null;
596
+ if (!fallback) return primary;
597
+ return {
598
+ ...fallback,
599
+ ...primary,
600
+ quotaWindows: Array.isArray(primary.quotaWindows) && primary.quotaWindows.length
601
+ ? primary.quotaWindows
602
+ : (fallback.quotaWindows || []),
603
+ balance: primary.balance || fallback.balance || null,
604
+ routeSpend: primary.routeSpend || fallback.routeSpend || null,
605
+ providerKind: primary.providerKind || fallback.providerKind || providerKindForQuota(primary.provider || fallback.provider),
606
+ };
398
607
  }
399
608
 
400
- function displayModelName(provider, model) {
609
+ function formatModelSegment({ provider, model, effort, fast, cols }) {
401
610
  const raw = String(model || '').trim();
402
611
  const meta = getModelMetadataSync(raw, provider) || {};
403
- const display = String(meta.displayName || meta.display || meta.name || '').trim();
404
- return display || canonicalModelDisplay(raw, provider) || raw || 'model';
405
- }
406
-
407
- function titleModelPart(part) {
408
- const text = String(part || '').trim();
409
- if (!text) return '';
410
- const lower = text.toLowerCase();
411
- if (lower === 'gpt') return 'GPT';
412
- if (lower === 'api') return 'API';
413
- if (lower === 'v4') return 'V4';
414
- return lower.charAt(0).toUpperCase() + lower.slice(1);
415
- }
416
-
417
- function canonicalModelDisplay(model, provider) {
418
- const raw = String(model || '').trim().replace(/-\d{4}-\d{2}-\d{2}$/, '');
419
- if (!raw) return '';
420
-
421
- const gpt = raw.match(/^gpt-(\d+(?:\.\d+)?)(?:-(.+))?$/i);
422
- if (gpt) {
423
- const suffix = gpt[2]
424
- ? '-' + gpt[2].split('-').map(titleModelPart).filter(Boolean).join('-')
425
- : '';
426
- return `GPT-${gpt[1]}${suffix}`;
427
- }
428
-
429
- const codex = raw.match(/^codex-(.+)$/i);
430
- if (codex) {
431
- return `Codex ${codex[1].split('-').map(titleModelPart).filter(Boolean).join(' ')}`;
432
- }
433
-
434
- const deepseek = raw.match(/^deepseek-(.+)$/i);
435
- if (deepseek) {
436
- return `DeepSeek ${deepseek[1].split('-').map(titleModelPart).filter(Boolean).join(' ')}`;
437
- }
438
-
439
- const grok = raw.match(/^grok-(.+)$/i);
440
- if (grok) {
441
- return `Grok ${grok[1].split('-').map(titleModelPart).filter(Boolean).join(' ')}`;
442
- }
443
-
444
- const claude = raw.match(/^claude-(opus|sonnet|haiku)-(.+)$/i);
445
- if (claude) {
446
- return `Claude ${titleModelPart(claude[1])} ${claude[2].replace(/-/g, '.')}`;
447
- }
448
-
449
- return raw;
612
+ const displayHint = String(meta.displayName || meta.display || meta.name || '').trim();
613
+ const modelName = shortenModelName(displayModelName(raw, provider, displayHint), cols);
614
+ const bits = [`${B}${modelName}${R}`];
615
+ if (effort) bits.push(`${B}${String(effort).toUpperCase()}${R}`);
616
+ if (fast === true) bits.push(`${B}FAST${R}`);
617
+ return bits.join(` ${D}·${R} `);
450
618
  }
451
619
 
452
- function shortenModelName(name, cols) {
453
- let out = String(name || 'model').replace(/\s*\(1M context\)/i, ' (1M)');
454
- out = out.replace(/^Claude\s+/i, '');
455
- out = out.replace(/^OpenAI\s+/i, '');
456
- if (cols < 80 && out.length > 18) return out.slice(0, 17) + '…';
457
- if (cols < 120 && out.length > 28) return out.slice(0, 27) + '…';
458
- return out;
620
+ /** Display label for context % (clamped to 100); raw pct still drives bar/color thresholds. */
621
+ export function contextPctDisplayLabel(ctxPct) {
622
+ const pct = Number(ctxPct);
623
+ if (!Number.isFinite(pct) || pct <= 0) return '0';
624
+ if (pct > 0 && pct < 1) return String(Math.round(pct * 10) / 10);
625
+ return String(Math.floor(Math.min(100, pct)));
459
626
  }
460
627
 
461
628
  function formatContextSegment(ctxPct, cols) {
462
- const pct = clampPct(ctxPct);
629
+ const raw = Number(ctxPct);
630
+ const pct = Number.isFinite(raw) ? Math.max(0, raw) : 0;
631
+ const barPct = clampPct(pct);
463
632
  const fill = pct >= 90 ? RED : pct >= 70 ? YLW : GRN;
464
- const label = pct > 0 && pct < 1 ? String(Math.round(pct * 10) / 10) : String(Math.floor(pct));
465
- const cells = cols >= 120 ? 14 : cols >= 80 ? 8 : 0;
633
+ const label = contextPctDisplayLabel(pct);
634
+ // Keep a full-width bar wherever there is room for one. Below 80 cols the bar
635
+ // is dropped (label-only) so the footer never overflows a narrow terminal;
636
+ // at 80+ it stays a fixed 14 cells instead of shrinking to 8, which read as
637
+ // too small/cramped on mid-width terminals.
638
+ const cells = cols >= 80 ? 14 : 0;
466
639
  if (!cells) return `${fill}${label}%${R}`;
467
- const bar = makeBar(pct, cells);
640
+ const bar = makeBar(barPct, cells);
468
641
  const filled = bar.replace(/░/g, '');
469
642
  const empty = bar.replace(/▓/g, '');
470
643
  return `${fill}${filled}${R}${D}${empty}${R} ${label}%`;
@@ -490,7 +663,7 @@ function epochMsToHHMM(ms) {
490
663
  return d.toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit', hour12: false });
491
664
  }
492
665
 
493
- function classifyBridgeWorkers(workers = []) {
666
+ function classifyAgentWorkers(workers = []) {
494
667
  const maintenance = [];
495
668
  const runningWorkers = [];
496
669
  const seenMaintenance = new Set();
@@ -508,7 +681,7 @@ function classifyBridgeWorkers(workers = []) {
508
681
  }
509
682
  if (w.status !== 'idle' && !seenRunning.has(tag)) {
510
683
  seenRunning.add(tag);
511
- runningWorkers.push(tag);
684
+ runningWorkers.push(w);
512
685
  }
513
686
  }
514
687
  return { maintenance, runningWorkers };
@@ -559,7 +732,7 @@ function activeHiddenRoleWorkers({ sessionId = '', clientHostPid = 0 } = {}) {
559
732
  const status = String(session?.status || stage || '').trim().toLowerCase();
560
733
  if (!isActiveHiddenStatus(stage || status)) continue;
561
734
  rows.push({
562
- tag: String(session?.bridgeTag || `${role}:${id || rows.length}`).trim(),
735
+ tag: String(session?.agentTag || `${role}:${id || rows.length}`).trim(),
563
736
  role,
564
737
  status: 'running',
565
738
  stage: stage || status || 'running',
@@ -594,14 +767,15 @@ function maintenanceLabel(tag) {
594
767
  }
595
768
  }
596
769
 
597
- function formatShellJobsSegment({ clientHostPid } = {}) {
770
+ function shellJobsStatus({ clientHostPid } = {}) {
598
771
  const ownerPid = positiveInt(clientHostPid);
599
- if (!ownerPid) return '';
772
+ const empty = { count: 0, elapsedLabel: '' };
773
+ if (!ownerPid) return empty;
600
774
  const now = Date.now();
601
775
  if (_shellJobsSegmentCache.ownerPid === ownerPid && now - _shellJobsSegmentCache.at < SHELL_JOBS_SEGMENT_CACHE_MS) {
602
- return _shellJobsSegmentCache.value;
776
+ return _shellJobsSegmentCache.value || empty;
603
777
  }
604
- let value = '';
778
+ let value = empty;
605
779
  try {
606
780
  const dir = join(dataDir(), 'shell-jobs');
607
781
  if (!existsSync(dir)) {
@@ -642,10 +816,9 @@ function formatShellJobsSegment({ clientHostPid } = {}) {
642
816
  return value;
643
817
  }
644
818
  const elapsedLabel = Number.isFinite(oldestMs) ? formatElapsed(Date.now() - oldestMs) : '';
645
- const elapsed = elapsedLabel ? ` ${elapsedLabel}` : '';
646
- value = `${GREY}⚙ shell:${count}${elapsed}${R}`;
819
+ value = { count, elapsedLabel };
647
820
  } catch {
648
- value = '';
821
+ value = empty;
649
822
  }
650
823
  _shellJobsSegmentCache = { ownerPid, at: now, value };
651
824
  return value;
@@ -682,29 +855,64 @@ function positiveInt(value) {
682
855
  return Number.isInteger(n) && n > 0 ? n : 0;
683
856
  }
684
857
 
858
+ // Byte-identical replica of src/tui/time-format.mjs formatDuration() with
859
+ // DEFAULT options (no mostSignificantOnly / hideTrailingZeros), wrapped to drop
860
+ // sub-1s like formatElapsed there. Output shape: '' (<1s), `Xs`, `Xm Ys`,
861
+ // `Xh Ym Zs`, `Xd Yh Zm`. statusline.mjs is a standalone UI module that should
862
+ // not depend on the React/ink TUI tree, so the algorithm is replicated rather
863
+ // than imported. Used for ALL L2 elapsed (Agents/Explore/Search/Shell).
685
864
  function formatElapsed(ms) {
686
- const secs = Math.max(0, Math.floor(Number(ms || 0) / 1000));
687
- if (secs < 1) return '';
688
- if (secs < 60) return `${secs}s`;
689
- const mins = Math.floor(secs / 60);
690
- if (mins < 60) return `${mins}m`;
691
- return `${Math.floor(mins / 60)}h`;
865
+ if (!Number.isFinite(Number(ms))) return '';
866
+ const value = Math.max(0, Number(ms) || 0);
867
+ if (value < 60_000) {
868
+ if (value < 1_000) return '';
869
+ return `${Math.floor(value / 1000)}s`;
870
+ }
871
+ const days = Math.floor(value / 86_400_000);
872
+ const hours = Math.floor((value % 86_400_000) / 3_600_000);
873
+ const minutes = Math.floor((value % 3_600_000) / 60_000);
874
+ const seconds = Math.floor((value % 60_000) / 1000);
875
+ if (days > 0) return `${days}d ${hours}h ${minutes}m`;
876
+ if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
877
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
878
+ return `${seconds}s`;
692
879
  }
693
880
 
694
881
  /** Minimal one-line footer used when the vendored renderer is unavailable. */
695
- function fallbackLine({ provider = '', model = '', effort = '', fast = false, cwd = '', stats } = {}) {
882
+ export function fallbackStatusline({
883
+ provider = '', model = '', effort = '', fast = false, cwd = '', stats, contextWindow = 0,
884
+ displayContextWindow = 0, rawContextWindow = 0, compactBoundaryTokens = 0, autoCompactTokenLimit = 0,
885
+ } = {}) {
886
+ return fallbackLine({
887
+ provider, model, effort, fast, cwd, stats, contextWindow, displayContextWindow,
888
+ rawContextWindow, compactBoundaryTokens, autoCompactTokenLimit,
889
+ });
890
+ }
891
+
892
+ function fallbackLine({
893
+ provider = '', model = '', effort = '', fast = false, cwd = '', stats, contextWindow = 0,
894
+ displayContextWindow = 0, rawContextWindow = 0, compactBoundaryTokens = 0, autoCompactTokenLimit = 0,
895
+ } = {}) {
696
896
  const s = stats || createSessionStats();
697
- const sep = statusSubtle(' | ');
698
- const flags = [effort ? String(effort).toUpperCase() : '', fast === true ? 'FAST' : ''].filter(Boolean).join(' | ');
699
- const id = statusAccent(`${provider}/${model}${flags ? ` | ${flags}` : ''}`);
700
- const tokens = statusText(
701
- `${fmt(s.inputTokens)} in / ${fmt(s.outputTokens)} out` +
702
- (s.cachedTokens ? ` / ${fmt(s.cachedTokens)} read` : '') +
703
- (s.cacheWriteTokens ? ` / ${fmt(s.cacheWriteTokens)} write` : ''),
704
- );
705
- const cost = s.costUsd > 0 ? green('$' + s.costUsd.toFixed(4)) : statusSubtle('$0.0000');
706
- const dir = bold(basename(cwd || process.cwd()) || cwd);
707
- return statusSubtle('> ') + [id, tokens, cost, dir].join(sep);
897
+ const cols = terminalColumns();
898
+ const ctxPct = resolveContextUsedPct({
899
+ provider,
900
+ model,
901
+ stats: s,
902
+ contextWindow,
903
+ displayContextWindow,
904
+ rawContextWindow,
905
+ compactBoundaryTokens,
906
+ autoCompactTokenLimit,
907
+ gatewayStatus: null,
908
+ });
909
+ const sep = ` ${D}│${R} `;
910
+ const parts = [
911
+ formatModelSegment({ provider, model, effort, fast, cols }),
912
+ formatContextSegment(ctxPct, cols),
913
+ ].filter(Boolean);
914
+ if (!parts.length) return statusSubtle('> mixdog');
915
+ return parts.join(sep);
708
916
  }
709
917
 
710
918
  function num(v) {