mixdog 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/package.json +10 -3
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/background-task-meta-smoke.mjs +1 -1
  6. package/scripts/bench-run.mjs +262 -0
  7. package/scripts/compact-smoke.mjs +12 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  9. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  10. package/scripts/internal-comms-bench.mjs +727 -0
  11. package/scripts/internal-comms-smoke.mjs +75 -0
  12. package/scripts/lead-workflow-smoke.mjs +4 -4
  13. package/scripts/live-worker-smoke.mjs +9 -9
  14. package/scripts/output-style-bench.mjs +285 -0
  15. package/scripts/output-style-smoke.mjs +13 -10
  16. package/scripts/patch-replay.mjs +90 -0
  17. package/scripts/provider-stream-stall-test.mjs +276 -0
  18. package/scripts/provider-toolcall-test.mjs +599 -1
  19. package/scripts/routing-corpus.mjs +281 -0
  20. package/scripts/session-bench.mjs +1526 -0
  21. package/scripts/session-diag.mjs +595 -0
  22. package/scripts/session-ingest-smoke.mjs +2 -2
  23. package/scripts/task-bench.mjs +207 -0
  24. package/scripts/tool-failures.mjs +6 -6
  25. package/scripts/tool-smoke.mjs +306 -66
  26. package/scripts/toolcall-args-test.mjs +81 -0
  27. package/src/agents/debugger/AGENT.md +4 -4
  28. package/src/agents/heavy-worker/AGENT.md +4 -2
  29. package/src/agents/reviewer/AGENT.md +4 -4
  30. package/src/agents/worker/AGENT.md +4 -2
  31. package/src/app.mjs +10 -6
  32. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  33. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  34. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  35. package/src/headless-role.mjs +14 -14
  36. package/src/help.mjs +1 -0
  37. package/src/lib/mixdog-debug.cjs +0 -22
  38. package/src/lib/plugin-paths.cjs +1 -7
  39. package/src/lib/rules-builder.cjs +34 -56
  40. package/src/mixdog-session-runtime.mjs +710 -319
  41. package/src/output-styles/default.md +12 -7
  42. package/src/output-styles/minimal.md +25 -0
  43. package/src/output-styles/oneline.md +21 -0
  44. package/src/output-styles/simple.md +10 -9
  45. package/src/repl.mjs +12 -4
  46. package/src/rules/agent/00-common.md +7 -5
  47. package/src/rules/agent/30-explorer.md +7 -8
  48. package/src/rules/lead/01-general.md +3 -1
  49. package/src/rules/lead/lead-tool.md +7 -0
  50. package/src/rules/shared/01-tool.md +17 -12
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  52. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  54. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  55. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  56. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  57. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
  59. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  60. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  61. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
  62. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  65. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
  66. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
  67. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
  75. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  76. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
  77. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  78. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
  79. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  80. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  81. package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
  82. package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
  83. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  84. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  85. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  86. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  87. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
  89. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
  91. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  92. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  94. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  95. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
  99. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
  101. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  102. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
  104. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  105. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
  106. package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
  107. package/src/runtime/channels/backends/discord.mjs +99 -9
  108. package/src/runtime/channels/backends/telegram.mjs +501 -0
  109. package/src/runtime/channels/index.mjs +441 -1254
  110. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  111. package/src/runtime/channels/lib/config.mjs +54 -3
  112. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  113. package/src/runtime/channels/lib/executor.mjs +0 -3
  114. package/src/runtime/channels/lib/format.mjs +4 -2
  115. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  116. package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
  117. package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
  118. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  119. package/src/runtime/channels/lib/session-discovery.mjs +0 -4
  120. package/src/runtime/channels/lib/telegram-format.mjs +283 -0
  121. package/src/runtime/channels/lib/tool-format.mjs +1 -2
  122. package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
  123. package/src/runtime/channels/lib/webhook.mjs +59 -31
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  126. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  127. package/src/runtime/memory/index.mjs +187 -43
  128. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  129. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  130. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  131. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  132. package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
  133. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  134. package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
  135. package/src/runtime/memory/lib/memory.mjs +101 -4
  136. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  137. package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
  138. package/src/runtime/memory/lib/session-ingest.mjs +116 -7
  139. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  140. package/src/runtime/memory/tool-defs.mjs +6 -3
  141. package/src/runtime/search/index.mjs +2 -7
  142. package/src/runtime/search/lib/config.mjs +0 -4
  143. package/src/runtime/search/lib/state.mjs +1 -15
  144. package/src/runtime/search/lib/web-tools.mjs +0 -1
  145. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  146. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  147. package/src/runtime/shared/child-spawn-gate.mjs +0 -6
  148. package/src/runtime/shared/config.mjs +9 -0
  149. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  150. package/src/runtime/shared/schedules-store.mjs +21 -19
  151. package/src/runtime/shared/tool-surface.mjs +98 -13
  152. package/src/runtime/shared/transcript-writer.mjs +129 -0
  153. package/src/runtime/shared/update-checker.mjs +214 -0
  154. package/src/standalone/agent-tool.mjs +255 -109
  155. package/src/standalone/channel-admin.mjs +133 -40
  156. package/src/standalone/channel-worker.mjs +8 -291
  157. package/src/standalone/explore-tool.mjs +2 -2
  158. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  159. package/src/standalone/provider-admin.mjs +11 -0
  160. package/src/standalone/seeds.mjs +1 -11
  161. package/src/standalone/usage-dashboard.mjs +1 -1
  162. package/src/tui/App.jsx +2137 -750
  163. package/src/tui/components/ConfirmBar.jsx +47 -0
  164. package/src/tui/components/ContextPanel.jsx +5 -3
  165. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  166. package/src/tui/components/Markdown.jsx +22 -98
  167. package/src/tui/components/Message.jsx +14 -35
  168. package/src/tui/components/Picker.jsx +87 -12
  169. package/src/tui/components/PromptInput.jsx +146 -9
  170. package/src/tui/components/QueuedCommands.jsx +1 -1
  171. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  172. package/src/tui/components/Spinner.jsx +7 -7
  173. package/src/tui/components/StatusLine.jsx +40 -21
  174. package/src/tui/components/TextEntryPanel.jsx +51 -7
  175. package/src/tui/components/ToolExecution.jsx +177 -100
  176. package/src/tui/components/TurnDone.jsx +4 -4
  177. package/src/tui/components/UsagePanel.jsx +1 -1
  178. package/src/tui/components/tool-output-format.mjs +312 -40
  179. package/src/tui/components/tool-output-format.test.mjs +180 -1
  180. package/src/tui/display-width.mjs +69 -0
  181. package/src/tui/display-width.test.mjs +35 -0
  182. package/src/tui/dist/index.mjs +7324 -2393
  183. package/src/tui/engine.mjs +287 -126
  184. package/src/tui/index.jsx +117 -7
  185. package/src/tui/keyboard-protocol.mjs +42 -0
  186. package/src/tui/lib/voice-recorder.mjs +453 -0
  187. package/src/tui/markdown/format-token.mjs +354 -142
  188. package/src/tui/markdown/format-token.test.mjs +155 -17
  189. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  190. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  191. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  192. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  193. package/src/tui/markdown/table-layout.mjs +9 -9
  194. package/src/tui/paste-attachments.mjs +0 -11
  195. package/src/tui/prompt-history-store.mjs +129 -0
  196. package/src/tui/prompt-history-store.test.mjs +52 -0
  197. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  198. package/src/tui/theme.mjs +41 -647
  199. package/src/tui/themes/base.mjs +86 -0
  200. package/src/tui/themes/basic.mjs +85 -0
  201. package/src/tui/themes/catppuccin.mjs +72 -0
  202. package/src/tui/themes/dracula.mjs +70 -0
  203. package/src/tui/themes/everforest.mjs +71 -0
  204. package/src/tui/themes/gruvbox.mjs +71 -0
  205. package/src/tui/themes/index.mjs +71 -0
  206. package/src/tui/themes/indigo.mjs +78 -0
  207. package/src/tui/themes/kanagawa.mjs +80 -0
  208. package/src/tui/themes/light.mjs +81 -0
  209. package/src/tui/themes/nord.mjs +72 -0
  210. package/src/tui/themes/onedark.mjs +16 -0
  211. package/src/tui/themes/rosepine.mjs +70 -0
  212. package/src/tui/themes/teal.mjs +81 -0
  213. package/src/tui/themes/tokyonight.mjs +79 -0
  214. package/src/tui/themes/utils.mjs +106 -0
  215. package/src/tui/themes/warm.mjs +79 -0
  216. package/src/tui/transcript-tool-failures.mjs +13 -2
  217. package/src/ui/markdown.mjs +1 -1
  218. package/src/ui/model-display.mjs +2 -2
  219. package/src/ui/statusline.mjs +26 -27
  220. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
  221. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  222. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  223. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  224. package/src/workflows/default/WORKFLOW.md +39 -12
  225. package/src/workflows/sequential/WORKFLOW.md +46 -0
  226. package/src/workflows/solo/WORKFLOW.md +7 -0
  227. package/vendor/ink/build/display-width.js +62 -0
  228. package/vendor/ink/build/ink.js +154 -20
  229. package/vendor/ink/build/measure-text.js +4 -1
  230. package/vendor/ink/build/output.js +115 -9
  231. package/vendor/ink/build/render-node-to-output.js +4 -1
  232. package/vendor/ink/build/render.js +4 -0
  233. package/src/hooks/lib/permission-rules.cjs +0 -170
  234. package/src/hooks/lib/settings-loader.cjs +0 -112
  235. package/src/lib/hook-pipe-path.cjs +0 -10
  236. package/src/output-styles/extreme-simple.md +0 -20
  237. package/src/rules/lead/04-workflow.md +0 -51
  238. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  239. package/src/workflows/default/workflow.json +0 -13
  240. package/src/workflows/solo/workflow.json +0 -7
@@ -184,626 +184,3 @@ function writeStatuslineLastSnapshot(json) {
184
184
  fs.renameSync(tmp, file);
185
185
  } catch {}
186
186
  }
187
-
188
- export async function renderStatusLine(ccJsonInput) {
189
- // ── ANSI palette (identical to bash original) ────────────────────────────────
190
- const R = '\x1b[0m';
191
- const B = '\x1b[1m';
192
- const D = '\x1b[2m';
193
- const RED = '\x1b[38;2;220;70;88m';
194
- const GRN = '\x1b[38;2;0;170;75m';
195
- const YLW = '\x1b[33m';
196
- const CYN = '\x1b[36m';
197
- const GREY = '\x1b[90m';
198
-
199
- // ── Terminal width ──────────────────────────────────────────────────────────
200
- let COLS = parseInt(process.env.COLUMNS || '120', 10);
201
- if (!Number.isFinite(COLS) || COLS <= 0) COLS = 120;
202
-
203
- // ── CC stdin JSON (from caller) ─────────────────────────────────────────────
204
- const CC_JSON = typeof ccJsonInput === 'string' ? ccJsonInput : '';
205
- writeStatuslineLastSnapshot(CC_JSON);
206
- let CURRENT_ROUTE = null;
207
- try {
208
- CURRENT_ROUTE = writeClaudeCodeCurrentSnapshot(CC_JSON);
209
- syncBaseUrlForCurrentModel(CURRENT_ROUTE);
210
- syncCompactWindowForCurrentModel(CURRENT_ROUTE);
211
- } catch {}
212
-
213
- if (process.env.MIXDOG_STATUSLINE_TRACE && CC_JSON) {
214
- try {
215
- fs.writeFileSync(
216
- path.join(pluginDataDir(), 'statusline-stdin.json'),
217
- CC_JSON
218
- );
219
- } catch {}
220
- }
221
-
222
- if (process.env.MIXDOG_STATUSLINE_TRACE) {
223
- try {
224
- const traceFile = path.join(
225
- pluginDataDir(), 'statusline-trace.log'
226
- );
227
- const st = fs.statSync(traceFile);
228
- if (st.size > 5 * 1024 * 1024) fs.writeFileSync(traceFile, '');
229
- } catch {}
230
- }
231
-
232
- // ── helpers ────────────────────────────────────────────────────────────────
233
- function extract(json, re) {
234
- const m = re.exec(json);
235
- return m ? m[1] : '';
236
- }
237
-
238
- function activeContextTokens(json) {
239
- try {
240
- const parsed = JSON.parse(json);
241
- const cw = parsed?.context_window || parsed?.contextWindow || {};
242
- const input = Number(cw.total_input_tokens ?? cw.totalInputTokens);
243
- const output = Number(cw.total_output_tokens ?? cw.totalOutputTokens);
244
- if (Number.isFinite(input) || Number.isFinite(output)) {
245
- return Math.max(0, Number.isFinite(input) ? input : 0) + Math.max(0, Number.isFinite(output) ? output : 0);
246
- }
247
- const currentInput = Number(cw.current_usage?.input_tokens ?? cw.currentUsage?.inputTokens);
248
- const currentOutput = Number(cw.current_usage?.output_tokens ?? cw.currentUsage?.outputTokens);
249
- if (Number.isFinite(currentInput) || Number.isFinite(currentOutput)) {
250
- return Math.max(0, Number.isFinite(currentInput) ? currentInput : 0) + Math.max(0, Number.isFinite(currentOutput) ? currentOutput : 0);
251
- }
252
- } catch {}
253
- return null;
254
- }
255
-
256
- function slice(json, key, stopKey) {
257
- const idx = json.indexOf(key);
258
- if (idx < 0) return null;
259
- const tail = json.slice(idx + key.length);
260
- if (stopKey) {
261
- const stop = tail.indexOf(stopKey);
262
- return stop >= 0 ? tail.slice(0, stop) : tail;
263
- }
264
- return tail;
265
- }
266
-
267
- // ── Extract CC fields ──────────────────────────────────────────────────────
268
- let CC_MODEL = extract(CC_JSON, /"display_name"\s*:\s*"([^"]+)"/);
269
- let CC_CTX_USED = '';
270
- let CC_RL_5H = '';
271
- let CC_RL_7D = '';
272
- let CC_RL_5H_RESET = '';
273
-
274
- const ctxTail = slice(CC_JSON, '"context_window"', '"rate_limits"');
275
- if (ctxTail !== null) {
276
- CC_CTX_USED = extract(ctxTail, /"used_percentage"\s*:\s*([0-9.]+)/);
277
- }
278
- const fiveTail = slice(CC_JSON, '"five_hour"', '"seven_day"');
279
- if (fiveTail !== null) {
280
- CC_RL_5H = extract(fiveTail, /"used_percentage"\s*:\s*([0-9.]+)/);
281
- CC_RL_5H_RESET = extract(fiveTail, /"resets_at"\s*:\s*([0-9]+)/);
282
- }
283
- const sevenTail = slice(CC_JSON, '"seven_day"', null);
284
- if (sevenTail !== null) {
285
- CC_RL_7D = extract(sevenTail, /"used_percentage"\s*:\s*([0-9.]+)/);
286
- }
287
-
288
- const CC_SESSION_ID = extract(CC_JSON, /"session_id"\s*:\s*"([^"]+)"/);
289
-
290
- let CC_EFFORT = extract(CC_JSON, /"effort"\s*:\s*\{[^}]*"level"\s*:\s*"([^"]+)"/);
291
- if (!CC_EFFORT) CC_EFFORT = process.env.MIXDOG_EFFORT_LEVEL || '';
292
- if (!CC_EFFORT) {
293
- try {
294
- const settingsRaw = fs.readFileSync(
295
- path.join(claudeConfigDir(), 'settings.json'), 'utf8'
296
- );
297
- CC_EFFORT = extract(settingsRaw, /"effortLevel"\s*:\s*"([^"]+)"/);
298
- } catch {}
299
- }
300
-
301
- const STATUS_ARGS = (() => {
302
- try {
303
- const parsed = JSON.parse(CC_JSON);
304
- return Array.isArray(parsed?._args) ? parsed._args.map(String) : [];
305
- } catch { return []; }
306
- })();
307
- function statusArg(prefix) {
308
- return STATUS_ARGS.find(arg => arg.startsWith(prefix))?.slice(prefix.length) || '';
309
- }
310
- function positiveInt(value) {
311
- const n = parseInt(String(value || ''), 10);
312
- return Number.isFinite(n) && n > 0 ? n : 0;
313
- }
314
- const CLIENT_HOST_PID_ARG = positiveInt(statusArg('--client-host-pid='));
315
- const CLIENT_HOST_PID = CLIENT_HOST_PID_ARG || positiveInt(process.ppid);
316
- // Bash-jobs scope pid: ONLY the explicitly passed --client-host-pid (the
317
- // shim-provided claude.exe pid). No process.ppid fallback here — under a
318
- // no-shim invocation ppid is the renderer's parent (the daemon/launcher),
319
- // NOT claude.exe, so falling back would count jobs that merely match that
320
- // unrelated pid. Absent ⇒ 0 ⇒ the segment attributes nothing.
321
- const CLIENT_HOST_PID_JOBS = positiveInt(statusArg('--client-host-pid='));
322
-
323
- const GATEWAY_STATUS = shouldLoadGatewayStatus(CURRENT_ROUTE, CC_SESSION_ID, CLIENT_HOST_PID_ARG)
324
- ? loadGatewayStatus({
325
- sessionId: CC_SESSION_ID,
326
- clientHostPid: CLIENT_HOST_PID_ARG,
327
- activeContextTokens: activeContextTokens(CC_JSON),
328
- currentRoute: CURRENT_ROUTE,
329
- })
330
- : null;
331
- if (GATEWAY_STATUS) {
332
- syncBaseUrlForGatewayStatus(GATEWAY_STATUS);
333
- syncCompactWindowForGatewayStatus(GATEWAY_STATUS);
334
- CC_MODEL = GATEWAY_STATUS.modelDisplay || CC_MODEL;
335
- if (GATEWAY_STATUS.contextUsedPct !== null && GATEWAY_STATUS.contextUsedPct !== undefined) {
336
- CC_CTX_USED = String(GATEWAY_STATUS.contextUsedPct);
337
- }
338
- CC_EFFORT = GATEWAY_STATUS.effort || '';
339
- if (GATEWAY_STATUS.fast) CC_EFFORT = CC_EFFORT ? `${CC_EFFORT} · FAST` : 'FAST';
340
- }
341
-
342
- function advertPidAlive(content) {
343
- const pid = positiveInt(extract(content, /"pid"\s*:\s*([0-9]+)/));
344
- if (!pid) return false;
345
- try { process.kill(pid, 0); return true; } catch { return false; }
346
- }
347
- function advertCcMatches(content) {
348
- return !!(CC_SESSION_ID && content.includes('"cc_session_id"') && content.includes(`"${CC_SESSION_ID}"`));
349
- }
350
- function advertClaimed(content) {
351
- return content.includes('"cc_session_id"');
352
- }
353
- function advertClientHostPid(content) {
354
- return positiveInt(extract(content, /"clientHostPid"\s*:\s*([0-9]+)/))
355
- || positiveInt(extract(content, /"client_host_pid"\s*:\s*([0-9]+)/));
356
- }
357
- function advertHostMatches(content, { allowUnclaimed = false } = {}) {
358
- if (!CLIENT_HOST_PID) return true;
359
- const clientHostPid = advertClientHostPid(content);
360
- if (clientHostPid) return clientHostPid === CLIENT_HOST_PID;
361
- if (allowUnclaimed && !advertClaimed(content)) return true;
362
- const ownerHostPid = positiveInt(extract(content, /"ownerHostPid"\s*:\s*([0-9]+)/));
363
- return ownerHostPid === CLIENT_HOST_PID;
364
- }
365
-
366
- // ── Advert routing ─────────────────────────────────────────────────────────
367
- let statusAdvert = '';
368
- let needClaim = false;
369
- const advertDir = path.join(claudeConfigDir(), 'mixdog-status');
370
- const mappingPath = CC_SESSION_ID
371
- ? path.join(advertDir, `.cc-${CC_SESSION_ID}${CLIENT_HOST_PID ? `-host-${CLIENT_HOST_PID}` : ''}.path`)
372
- : '';
373
-
374
- if (mappingPath) {
375
- try {
376
- const cached = fs.readFileSync(mappingPath, 'utf8').trim();
377
- if (cached) {
378
- const cachedAdvert = path.isAbsolute(cached) ? cached : path.join(advertDir, cached);
379
- const advertContent = fs.readFileSync(cachedAdvert, 'utf8');
380
- if (advertPidAlive(advertContent) && advertCcMatches(advertContent) && advertHostMatches(advertContent)) {
381
- statusAdvert = cachedAdvert;
382
- needClaim = false;
383
- } else {
384
- try { fs.unlinkSync(mappingPath); } catch {}
385
- }
386
- } else {
387
- try { fs.unlinkSync(mappingPath); } catch {}
388
- }
389
- } catch {
390
- try { fs.unlinkSync(mappingPath); } catch {}
391
- }
392
- }
393
-
394
- if (!statusAdvert) {
395
- try {
396
- const files = fs.readdirSync(advertDir)
397
- .filter(f => f.endsWith('.json'))
398
- .map(f => path.join(advertDir, f));
399
- for (const f of files) {
400
- let content;
401
- try { content = fs.readFileSync(f, 'utf8'); } catch { continue; }
402
- if (!advertPidAlive(content)) continue;
403
- if (advertCcMatches(content)) {
404
- if (!advertHostMatches(content)) continue;
405
- statusAdvert = f;
406
- needClaim = false;
407
- break;
408
- }
409
- if (!statusAdvert && CC_SESSION_ID && !advertClaimed(content) && advertHostMatches(content, { allowUnclaimed: true })) {
410
- statusAdvert = f;
411
- needClaim = true;
412
- }
413
- }
414
- if (!statusAdvert && !CC_SESSION_ID) {
415
- for (const f of files) {
416
- try { fs.readFileSync(f, 'utf8'); statusAdvert = f; break; } catch {}
417
- }
418
- }
419
- } catch {}
420
- if (statusAdvert && mappingPath && !needClaim) {
421
- writeFileIfChangedSync(mappingPath, statusAdvert);
422
- }
423
- }
424
- if (!statusAdvert && !CC_SESSION_ID) {
425
- statusAdvert = path.join(claudeConfigDir(), 'mixdog-status.json');
426
- }
427
-
428
- // ── Read port from advert ──────────────────────────────────────────────────
429
- let statusPort = '';
430
- try {
431
- const advertContent = fs.readFileSync(statusAdvert, 'utf8');
432
- statusPort = extract(advertContent, /"port"\s*:\s*([0-9]+)/);
433
- } catch {}
434
-
435
- if (needClaim && CC_SESSION_ID && statusPort) {
436
- const claimPayload = { cc_session_id: CC_SESSION_ID };
437
- if (CLIENT_HOST_PID) claimPayload.client_host_pid = CLIENT_HOST_PID;
438
- const body = JSON.stringify(claimPayload);
439
- try {
440
- const req = http.request({
441
- hostname: '127.0.0.1',
442
- port: parseInt(statusPort, 10),
443
- path: '/register-cc-session',
444
- method: 'POST',
445
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
446
- }, (res) => { res.resume(); });
447
- req.on('error', () => {});
448
- req.setTimeout(800, () => { try { req.destroy(); } catch {} });
449
- req.write(body);
450
- req.end();
451
- } catch {}
452
- }
453
-
454
- // ── Fetch /bridge/status ───────────────────────────────────────────────────
455
- let bridgeJson = '';
456
- if (statusPort) {
457
- bridgeJson = await new Promise(resolve => {
458
- try {
459
- const statusUrl = `http://127.0.0.1:${statusPort}/bridge/status?format=statusline-json`
460
- + (CLIENT_HOST_PID ? `&clientHostPid=${CLIENT_HOST_PID}` : '');
461
- const req = http.get(
462
- statusUrl,
463
- { timeout: 500 },
464
- res => {
465
- let data = '';
466
- res.on('data', chunk => { data += chunk; });
467
- res.on('end', () => resolve(data));
468
- }
469
- );
470
- req.on('error', () => resolve(''));
471
- req.on('timeout', () => { req.destroy(); resolve(''); });
472
- } catch { resolve(''); }
473
- });
474
- }
475
- if (!bridgeJson.startsWith('{')) bridgeJson = '';
476
- if (CC_JSON) {
477
- try {
478
- const fallbackBridge = JSON.parse(CC_JSON)?._mixdog_bridge;
479
- if (fallbackBridge && typeof fallbackBridge === 'object' && Array.isArray(fallbackBridge.workers) && fallbackBridge.workers.length) {
480
- bridgeJson = JSON.stringify(fallbackBridge);
481
- }
482
- } catch {}
483
- }
484
-
485
- if (!bridgeJson && process.env.MIXDOG_STATUSLINE_TRACE) {
486
- try {
487
- const traceDir = pluginDataDir();
488
- if (fs.existsSync(traceDir)) {
489
- const advertPresent = (() => { try { fs.accessSync(statusAdvert); return 'present'; } catch { return 'missing'; } })();
490
- const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
491
- fs.appendFileSync(
492
- path.join(traceDir, 'statusline-trace.log'),
493
- `${ts} NOBRIDGE port=${statusPort || '?'} advert=${advertPresent}\n`
494
- );
495
- }
496
- } catch {}
497
- }
498
-
499
- // ── Extract bridge fields ──────────────────────────────────────────────────
500
- let bSessRoles = '';
501
- let bSchedNextAt = '';
502
- let bSchedNextName = '';
503
- // Worker list carrying running/idle status alongside each tag, surfaced by
504
- // the aggregator's sessions.workers segment. Falls back to roles (running-
505
- // only) when an older aggregator payload lacks the workers array.
506
- let bWorkers = [];
507
-
508
- if (bridgeJson) {
509
- const sessRaw = extract(bridgeJson, /"sessions"\s*:\s*\{[^}]*"roles"\s*:\s*\[([^\]]*)\]/);
510
- if (sessRaw) bSessRoles = sessRaw.replace(/"/g, '').replace(/\s/g, '');
511
- const workersRaw = extract(bridgeJson, /"workers"\s*:\s*\[([^\]]*)\]/);
512
- if (workersRaw) {
513
- // Parse [{"tag":"x","status":"running"},...] without a full JSON.parse
514
- // of the whole payload (matches the existing regex-extract approach).
515
- const re = /\{[^}]*?"tag"\s*:\s*"([^"]*)"[^}]*?"status"\s*:\s*"([^"]*)"[^}]*?\}/g;
516
- let m;
517
- while ((m = re.exec(workersRaw)) !== null) {
518
- bWorkers.push({ tag: m[1], status: m[2] === 'idle' ? 'idle' : 'running' });
519
- }
520
- }
521
- bSchedNextAt = extract(bridgeJson, /"next"\s*:\s*\{[^}]*"fireAt"\s*:\s*([0-9]+)/);
522
- bSchedNextName = extract(bridgeJson, /"next"\s*:\s*\{[^}]*"name"\s*:\s*"([^"]*)"/);
523
- }
524
-
525
- // ── Format helpers ─────────────────────────────────────────────────────────
526
- let modelStr = '';
527
- if (CC_MODEL) {
528
- let raw = CC_MODEL.replace('(1M context)', '(1M)');
529
- if (raw.includes('Opus')) modelStr = 'Opus' + raw.slice(raw.indexOf('Opus') + 4);
530
- else if (raw.includes('Sonnet')) modelStr = 'Sonnet' + raw.slice(raw.indexOf('Sonnet') + 6);
531
- else if (raw.includes('Haiku')) modelStr = 'Haiku' + raw.slice(raw.indexOf('Haiku') + 5);
532
- else modelStr = raw;
533
- }
534
- const modelShort = modelStr.split(' ')[0];
535
- const effortStr = CC_EFFORT ? CC_EFFORT.toUpperCase() : '';
536
-
537
- function roundPct(s) {
538
- const n = parseFloat(s);
539
- return Number.isFinite(n) ? Math.floor(n) : null;
540
- }
541
- function contextPct(s) {
542
- const n = parseFloat(s);
543
- return Number.isFinite(n) ? Math.max(0, Math.min(100, n)) : null;
544
- }
545
- function formatContextPct(pct) {
546
- if (pct === null) return '';
547
- if (pct > 0 && pct < 1) return String(Math.round(pct * 10) / 10);
548
- return String(Math.floor(pct));
549
- }
550
-
551
- const ctxPct = contextPct(CC_CTX_USED);
552
- const rl5hInt = roundPct(CC_RL_5H);
553
- const rl7dInt = roundPct(CC_RL_7D);
554
-
555
- function epochMsToHHMM(ms) {
556
- const d = new Date(parseInt(ms, 10));
557
- if (isNaN(d.getTime())) return '';
558
- return d.toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit', hour12: false });
559
- }
560
-
561
- const resetStr = CC_RL_5H_RESET ? epochMsToHHMM(CC_RL_5H_RESET * 1000) : '';
562
- const schedNextHHMM = bSchedNextAt ? epochMsToHHMM(parseInt(bSchedNextAt, 10)) : '';
563
-
564
- function colourPct(p) {
565
- if (p >= 90) return `${RED}${p}%${R}`;
566
- if (p >= 70) return `${YLW}${p}%${R}`;
567
- return `${GRN}${p}%${R}`;
568
- }
569
- function summarizeWorkerTags(tags, limit = 3) {
570
- const cleanTags = [...new Set((Array.isArray(tags) ? tags : [])
571
- .map(tag => String(tag || '').trim())
572
- .filter(Boolean))];
573
- if (cleanTags.length <= limit) return cleanTags.join(', ');
574
- return `${cleanTags.slice(0, limit).join(', ')}, +${cleanTags.length - limit}`;
575
- }
576
- const workerSpinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
577
- function workerSpinnerFrame(now = Date.now()) {
578
- return workerSpinnerFrames[Math.floor(now / 120) % workerSpinnerFrames.length] || workerSpinnerFrames[0];
579
- }
580
-
581
- const gatewayLimitSegments = formatGatewayLimitSegments(GATEWAY_STATUS, {
582
- COLS, D, R, RED, GRN, YLW, colourPct, epochMsToHHMM,
583
- });
584
-
585
- function makeBar(pct, cells) {
586
- if (pct === null || cells <= 0) return '';
587
- let filled = Math.floor(pct * cells / 100);
588
- if (filled < 0) filled = 0;
589
- if (filled > cells) filled = cells;
590
- if (pct >= 1 && filled === 0) filled = 1;
591
- return '▓'.repeat(filled) + '░'.repeat(cells - filled);
592
- }
593
-
594
- // ── Build L1 ───────────────────────────────────────────────────────────────
595
- const SEP = `${D}│${R}`;
596
- const l1Parts = [];
597
- function addL1(seg) { if (seg) l1Parts.push(seg); }
598
-
599
- if (modelStr) {
600
- const m = COLS >= 120 ? modelStr : modelShort;
601
- if (effortStr) {
602
- addL1(`${B}${m}${R} ${D}·${R} ${B}${effortStr}${R}`);
603
- } else {
604
- addL1(`${B}${m}${R}`);
605
- }
606
- }
607
-
608
- if (ctxPct !== null) {
609
- const fill = ctxPct >= 90 ? RED : ctxPct >= 70 ? YLW : GRN;
610
- const ctxLabel = formatContextPct(ctxPct);
611
- let barOut = '';
612
- if (COLS >= 120) barOut = makeBar(ctxPct, 14);
613
- else if (COLS >= 80) barOut = makeBar(ctxPct, 8);
614
- if (barOut) {
615
- const filledPart = barOut.replace(/░/g, '');
616
- const emptyPart = barOut.replace(/▓/g, '');
617
- const bar = `${fill}${filledPart}${R}${D}${emptyPart}${R}`;
618
- addL1(`${bar} ${ctxLabel}%`);
619
- } else {
620
- addL1(`${fill}${ctxLabel}%${R}`);
621
- }
622
- }
623
-
624
- if (gatewayLimitSegments.length) {
625
- for (const seg of gatewayLimitSegments) addL1(seg);
626
- } else {
627
- if (rl5hInt !== null) {
628
- addL1(`${D}5H${R} ${colourPct(rl5hInt)}`);
629
- }
630
- if (COLS >= 80) {
631
- if (rl7dInt !== null) addL1(`${D}7D${R} ${colourPct(rl7dInt)}`);
632
- if (resetStr) addL1(`${D}↻ ${resetStr}${R}`);
633
- }
634
- }
635
-
636
- // ── Role classification ────────────────────────────────────────────────────
637
- let workCount = 0;
638
- let workOrder = '';
639
- let hasCycle1 = false;
640
- let hasCycle2 = false;
641
- let hasCycle3 = false;
642
- let hasSched = false;
643
- let hasWebhook = false;
644
- let hasExplorer = false;
645
-
646
- function classifyMaint(role) {
647
- switch (role) {
648
- case 'cycle1-agent': hasCycle1 = true; return true;
649
- case 'cycle2-agent': hasCycle2 = true; return true;
650
- case 'cycle3-agent': hasCycle3 = true; return true;
651
- case 'scheduler-task': hasSched = true; return true;
652
- case 'webhook-handler': hasWebhook = true; return true;
653
- case 'explorer': hasExplorer = true; return true;
654
- default: return false;
655
- }
656
- }
657
-
658
- // Idle worker tags (greyed in L2), threaded from the aggregator's
659
- // sessions.workers[].status. Running user-workers still feed workCount /
660
- // workOrder so the existing "N Running (tags)" badge is unchanged.
661
- const idleWorkers = [];
662
- if (bWorkers.length) {
663
- // Preferred path: per-worker running/idle status from the aggregator.
664
- for (const w of bWorkers) {
665
- if (classifyMaint(w.tag)) continue; // maintenance → L1, not the worker badge
666
- if (w.status === 'idle') {
667
- idleWorkers.push(w.tag);
668
- } else {
669
- workCount++;
670
- workOrder = workOrder ? `${workOrder}, ${w.tag}` : w.tag;
671
- }
672
- }
673
- } else if (bSessRoles) {
674
- // Fallback: legacy roles array (running-only, no idle/status info).
675
- for (const role of bSessRoles.split(',')) {
676
- if (!role) continue;
677
- if (classifyMaint(role)) continue;
678
- workCount++;
679
- workOrder = workOrder ? `${workOrder}, ${role}` : role;
680
- }
681
- }
682
-
683
- const maintParts = [];
684
- if (hasCycle1) maintParts.push(`${GRN}↻${R} ${B}cycle1${R}`);
685
- if (hasCycle2) maintParts.push(`${GRN}↻${R} ${B}cycle2${R}`);
686
- if (hasCycle3) maintParts.push(`${GRN}↻${R} ${B}cycle3${R}`);
687
- if (hasSched) maintParts.push(`${GRN}↻${R} ${B}scheduler${R}`);
688
- if (hasWebhook) maintParts.push(`${GRN}↻${R} ${B}webhook${R}`);
689
- if (hasExplorer) maintParts.push(`${GRN}↻${R} ${B}explorer${R}`);
690
- if (maintParts.length) addL1(maintParts.join(' '));
691
-
692
- // Background bash jobs: a running job is a `<jobId>.json` whose sibling
693
- // `<jobId>.done` (written on exit) is absent. Surviving candidates are then
694
- // liveness-filtered — the `.json` carries the wrapper `pid`; an orphaned job
695
- // (wrapper crashed, `.done` never written, pid dead for days) is skipped via
696
- // process.kill(pid, 0). Bounded per tick: candidates are ordered newest-first
697
- // (the jobId embeds its spawn Date.now()) and at most JOB_SCAN_CAP of them are
698
- // read — keeping each render O(cap), not O(total on-disk jobs). When the scan
699
- // is truncated a trailing `+` overflow marker is appended to the count. One
700
- // readFileSync/statSync per scanned job for the oldest startedAt (the .json is
701
- // written at job start); tolerate the dir being missing and never throw.
702
- const JOB_SCAN_CAP = 30;
703
- const bashJobsSeg = (() => {
704
- try {
705
- const dir = path.join(pluginDataDir(), 'shell-jobs');
706
- const names = fs.readdirSync(dir);
707
- const done = new Set();
708
- const jobs = [];
709
- const ownerByJob = new Map();
710
- for (const n of names) {
711
- if (n.endsWith('.done')) done.add(n.slice(0, -5));
712
- else if (n.endsWith('.json')) jobs.push(n.slice(0, -5));
713
- else {
714
- // Owner sidecar `<jobId>.owner-<pid>` — a zero-byte marker whose NAME
715
- // carries the owning CC host pid, written next to the .json at spawn.
716
- const i = n.lastIndexOf('.owner-');
717
- if (i > 0) { const pid = positiveInt(n.slice(i + 7)); if (pid) ownerByJob.set(n.slice(0, i), pid); }
718
- }
719
- }
720
- // Owner-filter BEFORE the scan cap, from the directory listing alone: each
721
- // job's owning claude.exe pid is read from its `.owner-<pid>` marker name
722
- // (no JSON read), so another session's newer jobs can never evict ours at
723
- // the cap. Only jobs whose marker pid equals THIS statusline's
724
- // --client-host-pid survive; legacy jobs without a marker — and every job
725
- // when no host pid was passed (CLIENT_HOST_PID_JOBS absent) — are excluded.
726
- // Then ordered newest-first by the spawn timestamp embedded in
727
- // `job_<ms>_<rand>`, so truncation drops only this session's oldest tail.
728
- const jobStampMs = (id) => { const m = /^job_(\d+)/.exec(id); return m ? Number(m[1]) : 0; };
729
- const candidates = jobs
730
- .filter((id) => !done.has(id) && CLIENT_HOST_PID_JOBS && ownerByJob.get(id) === CLIENT_HOST_PID_JOBS)
731
- .sort((a, b) => jobStampMs(b) - jobStampMs(a));
732
- if (candidates.length === 0) return '';
733
- const scan = candidates.slice(0, JOB_SCAN_CAP);
734
- const truncated = candidates.length > JOB_SCAN_CAP;
735
- let count = 0;
736
- let oldestMs = Infinity;
737
- for (const id of scan) {
738
- const p = path.join(dir, `${id}.json`);
739
- let pid, tmo, enforced;
740
- try {
741
- const d = JSON.parse(fs.readFileSync(p, 'utf-8'));
742
- pid = d.pid; tmo = Number(d.timeoutMs);
743
- // Runtime enforcement proof: PS records timeoutEnforced:true; the
744
- // posix wrapper touches <id>.enforced iff its `timeout` branch ran.
745
- enforced = d.timeoutEnforced === true || fs.existsSync(path.join(dir, `${id}.enforced`));
746
- }
747
- catch { continue; } // unreadable/unparseable → skip
748
- let st;
749
- try { st = fs.statSync(p); }
750
- catch { continue; }
751
- // Deadline: the wrapper force-kills at timeoutMs, so a job older than
752
- // timeoutMs + grace is dead even when its pid was recycled by an
753
- // unrelated live process (pid-reuse-proof, mirrors the sweep). Trusted
754
- // only when the record proves in-wrapper enforcement (timeoutEnforced).
755
- if (enforced && Number.isFinite(tmo) && tmo > 0 && (Date.now() - st.mtimeMs) > tmo + 30 * 60_000) continue;
756
- // kill(0, 0) probes the whole process group and "succeeds" — a
757
- // malformed pid (0, "", []) must be rejected before the probe.
758
- pid = Number(pid);
759
- if (!Number.isInteger(pid) || pid <= 0) continue;
760
- // Liveness: process.kill(pid, 0) succeeds or throws EPERM → alive;
761
- // ESRCH/invalid pid → dead → skip.
762
- let alive = false;
763
- try { process.kill(pid, 0); alive = true; }
764
- catch (e) { alive = e && e.code === 'EPERM'; }
765
- if (!alive) continue;
766
- count++;
767
- if (st.mtimeMs < oldestMs) oldestMs = st.mtimeMs;
768
- }
769
- if (count === 0) return '';
770
- let elapsed = '';
771
- if (Number.isFinite(oldestMs)) {
772
- const secs = Math.max(0, Math.floor((Date.now() - oldestMs) / 1000));
773
- elapsed = secs < 60 ? ` ${secs}s` : ` ${Math.floor(secs / 60)}m`;
774
- }
775
- // `+` overflow marker: more live/recent candidates existed than the
776
- // per-tick scan cap, so the rendered count is a floor, not the ground.
777
- const overflow = truncated ? '+' : '';
778
- return `${GREY}⚙ shell:${count}${overflow}${elapsed}${R}`;
779
- } catch { return ''; }
780
- })();
781
- if (bashJobsSeg) addL1(bashJobsSeg);
782
-
783
- // ── Build L2 ───────────────────────────────────────────────────────────────
784
- const l2Parts = [];
785
- function addL2(seg) { if (seg) l2Parts.push(seg); }
786
-
787
- if (workCount > 0 && workOrder) {
788
- addL2(`${GRN}${workerSpinnerFrame()}${R} ${B}${workCount} Running${R} ${D}(${R}${B}${summarizeWorkerTags(workOrder.split(','))}${R}${D})${R}`);
789
- }
790
-
791
- if (idleWorkers.length) {
792
- // Idle workers: filled grey dot (●) + explicit 'idle' marker + tag list.
793
- // Filled (not hollow ○) so the glyph matches the running ● weight; only
794
- // the colour (grey vs green) distinguishes idle from running.
795
- const idleTags = summarizeWorkerTags(idleWorkers);
796
- addL2(`${GREY}● ${idleWorkers.length} idle (${idleTags})${R}`);
797
- }
798
- if (bSchedNextName && schedNextHHMM) {
799
- addL2(`${YLW}⏰${R} ${B}${bSchedNextName}${R} ${D}${schedNextHHMM}${R}`);
800
- }
801
-
802
- const l1 = l1Parts.join(` ${SEP} `) || 'mixdog';
803
- let l2 = l2Parts.join(` ${SEP} `);
804
- if (l2 === 'Idle') l2 = '';
805
-
806
- let out = l1 + '\n';
807
- if (l2) out += l2 + '\n';
808
- return out;
809
- }
@@ -11,6 +11,7 @@ import {
11
11
  readLatestGatewayHostRoute,
12
12
  readGatewaySessionRoute,
13
13
  } from '../src/gateway/session-routes.mjs';
14
+ import { compactBoundaryDenominator } from '../src/gateway/route-meta.mjs';
14
15
 
15
16
  function positiveInt(value) {
16
17
  const n = parseInt(String(value || ''), 10);
@@ -278,16 +279,8 @@ function resolveStatusAutoCompactTokenLimit(configured, active = {}, lastCompact
278
279
  export const _resolveStatusAutoCompactTokenLimit = resolveStatusAutoCompactTokenLimit;
279
280
 
280
281
  export function compactBoundaryForStatus(routeInfo = {}, compact = null) {
281
- const compactLimit = num(routeInfo?.autoCompactTokenLimit ?? compact?.compactLimitTokens, 0);
282
- const contextWindow = num(routeInfo?.contextWindow ?? compact?.contextWindow, 0);
283
- const budgetWindow = num(compact?.budgetWindow, 0);
284
- const rawContextWindow = num(routeInfo?.rawContextWindow ?? compact?.rawContextWindow, 0);
285
- if (compactLimit > 0 && contextWindow > 0) return Math.min(compactLimit, contextWindow);
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;
290
- return rawContextWindow > 0 ? rawContextWindow : null;
282
+ const n = compactBoundaryDenominator(routeInfo, compact);
283
+ return n > 0 ? n : null;
291
284
  }
292
285
 
293
286
  function sessionIdFromTranscriptPath(transcriptPath) {
@@ -378,8 +371,8 @@ function displayForModel(provider, model, info) {
378
371
  const display = cleanString(info?.display) || cleanString(info?.displayName) || cleanString(info?.name);
379
372
  if (display) return display;
380
373
  if (provider === 'anthropic-oauth') {
381
- const m = String(model || '').match(/^claude-(opus|sonnet|haiku)-(\d+)-(\d+)/i);
382
- if (m) return `${m[1][0].toUpperCase()}${m[1].slice(1).toLowerCase()} ${m[2]}.${m[3]}`;
374
+ const m = String(model || '').match(/^claude-(opus|sonnet|haiku|fable)-(\d+)(?:-(\d+))?/i);
375
+ if (m) return `${m[1][0].toUpperCase()}${m[1].slice(1).toLowerCase()} ${m[2]}${m[3] ? `.${m[3]}` : ''}`;
383
376
  }
384
377
  const raw = cleanString(model);
385
378
  const gpt = raw.match(/^gpt[-_](.+)$/i);