mixdog 0.9.3 → 0.9.4

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 (329) hide show
  1. package/package.json +7 -3
  2. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  3. package/scripts/bench/lead-review-tasks.json +20 -0
  4. package/scripts/bench/r4-mixed-tasks.json +20 -0
  5. package/scripts/bench/review-tasks.json +20 -0
  6. package/scripts/bench/round-codex.json +114 -0
  7. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  8. package/scripts/bench/round-mixdog-lead.json +269 -0
  9. package/scripts/bench/round-mixdog.json +126 -0
  10. package/scripts/bench/round-r10-bigsample.json +679 -0
  11. package/scripts/bench/round-r11-codexalign.json +257 -0
  12. package/scripts/bench/round-r4-codex.json +114 -0
  13. package/scripts/bench/round-r4-mixed.json +225 -0
  14. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  15. package/scripts/bench/round-r6-codex.json +114 -0
  16. package/scripts/bench/round-r6-solo.json +257 -0
  17. package/scripts/bench/round-r7-full.json +254 -0
  18. package/scripts/bench/round-r8-fulldefault.json +255 -0
  19. package/scripts/bench-run.mjs +215 -29
  20. package/scripts/freevar-smoke.mjs +95 -0
  21. package/scripts/internal-comms-bench.mjs +1 -0
  22. package/scripts/internal-comms-smoke.mjs +10 -9
  23. package/scripts/mouse-probe.mjs +45 -0
  24. package/scripts/output-style-bench.mjs +13 -6
  25. package/scripts/output-style-smoke.mjs +4 -4
  26. package/scripts/provider-toolcall-test.mjs +7 -3
  27. package/scripts/recall-usecase-cases.json +18 -0
  28. package/scripts/recall-usecase-probe.json +6 -0
  29. package/scripts/session-bench.mjs +152 -6
  30. package/scripts/tool-smoke.mjs +23 -63
  31. package/scripts/tui-render-smoke.mjs +90 -0
  32. package/scripts/webhook-smoke.mjs +208 -0
  33. package/src/agents/debugger/AGENT.md +4 -1
  34. package/src/agents/heavy-worker/AGENT.md +6 -5
  35. package/src/agents/maintainer/AGENT.md +4 -0
  36. package/src/agents/reviewer/AGENT.md +2 -1
  37. package/src/agents/worker/AGENT.md +8 -4
  38. package/src/lib/rules-builder.cjs +4 -0
  39. package/src/mixdog-session-runtime.mjs +632 -2042
  40. package/src/output-styles/default.md +34 -9
  41. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  42. package/src/output-styles/minimal.md +4 -1
  43. package/src/output-styles/simple.md +22 -7
  44. package/src/rules/agent/00-common.md +2 -0
  45. package/src/rules/lead/lead-brief.md +12 -0
  46. package/src/rules/lead/lead-tool.md +0 -11
  47. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  48. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  49. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  50. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  51. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  52. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  53. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  54. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  55. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  56. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  57. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  58. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  59. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  60. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  61. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  62. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  63. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  64. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  65. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  66. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
  67. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  69. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  70. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +297 -2123
  76. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
  77. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
  78. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  79. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
  80. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  82. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  83. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  84. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  85. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  86. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  87. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  88. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  89. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  90. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  91. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  93. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
  94. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  95. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  97. package/src/runtime/agent/orchestrator/session/loop.mjs +251 -397
  98. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  99. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  100. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  101. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  102. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  103. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  104. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  105. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  106. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  107. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  108. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  109. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +32 -41
  110. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  113. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  115. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  118. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  119. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  120. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  124. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  125. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  126. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  128. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  130. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  131. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  132. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  133. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  134. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  136. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  137. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  138. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  139. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  140. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  141. package/src/runtime/channels/backends/discord.mjs +12 -292
  142. package/src/runtime/channels/index.mjs +229 -663
  143. package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
  144. package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
  145. package/src/runtime/channels/lib/event-queue.mjs +63 -4
  146. package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
  147. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  148. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  149. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  150. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  151. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  152. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  153. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  154. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  155. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  156. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  157. package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
  158. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  159. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  160. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  161. package/src/runtime/channels/lib/webhook.mjs +36 -570
  162. package/src/runtime/channels/tool-defs.mjs +11 -130
  163. package/src/runtime/memory/index.mjs +201 -1948
  164. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  165. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  166. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  167. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  168. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  169. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  170. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  171. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  172. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  173. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  174. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  175. package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  177. package/src/runtime/memory/lib/query-handlers.mjs +780 -0
  178. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  179. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  180. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  181. package/src/runtime/memory/tool-defs.mjs +5 -13
  182. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  183. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  184. package/src/runtime/search/lib/web-tools.mjs +24 -602
  185. package/src/runtime/shared/atomic-file.mjs +26 -1
  186. package/src/runtime/shared/launcher-control.mjs +2 -2
  187. package/src/runtime/shared/tool-primitives.mjs +308 -0
  188. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  189. package/src/runtime/shared/tool-surface.mjs +80 -898
  190. package/src/runtime/shared/transcript-writer.mjs +23 -0
  191. package/src/runtime/shared/update-checker.mjs +7 -4
  192. package/src/session-runtime/config-helpers.mjs +84 -2
  193. package/src/session-runtime/config-lifecycle.mjs +232 -0
  194. package/src/session-runtime/cwd-plugins.mjs +226 -0
  195. package/src/session-runtime/mcp-glue.mjs +177 -0
  196. package/src/session-runtime/model-recency.mjs +111 -0
  197. package/src/session-runtime/native-search.mjs +247 -0
  198. package/src/session-runtime/output-styles.mjs +11 -9
  199. package/src/session-runtime/prewarm.mjs +142 -0
  200. package/src/session-runtime/provider-models.mjs +278 -0
  201. package/src/session-runtime/provider-usage.mjs +120 -0
  202. package/src/session-runtime/quick-model-rows.mjs +170 -0
  203. package/src/session-runtime/quick-search-models.mjs +46 -0
  204. package/src/session-runtime/session-hooks.mjs +93 -0
  205. package/src/session-runtime/settings-api.mjs +319 -0
  206. package/src/session-runtime/tool-catalog.mjs +29 -29
  207. package/src/session-runtime/tool-defs.mjs +84 -0
  208. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  209. package/src/standalone/agent-tool/helpers.mjs +237 -0
  210. package/src/standalone/agent-tool/notify.mjs +107 -0
  211. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  212. package/src/standalone/agent-tool/render.mjs +152 -0
  213. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  214. package/src/standalone/agent-tool.mjs +110 -671
  215. package/src/standalone/channel-worker.mjs +4 -7
  216. package/src/standalone/explore-tool.mjs +30 -9
  217. package/src/standalone/hook-bus/config.mjs +207 -0
  218. package/src/standalone/hook-bus/constants.mjs +90 -0
  219. package/src/standalone/hook-bus/handlers.mjs +481 -0
  220. package/src/standalone/hook-bus/payload.mjs +31 -0
  221. package/src/standalone/hook-bus/rules.mjs +77 -0
  222. package/src/standalone/hook-bus.mjs +77 -870
  223. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  224. package/src/standalone/opencode-go-login.mjs +5 -1
  225. package/src/standalone/provider-admin.mjs +1 -16
  226. package/src/standalone/usage-dashboard.mjs +3 -1
  227. package/src/tui/App.jsx +945 -8094
  228. package/src/tui/app/app-format.mjs +206 -0
  229. package/src/tui/app/channel-pickers.mjs +510 -0
  230. package/src/tui/app/clipboard.mjs +67 -0
  231. package/src/tui/app/core-memory-picker.mjs +210 -0
  232. package/src/tui/app/extension-pickers.mjs +506 -0
  233. package/src/tui/app/input-parsers.mjs +193 -0
  234. package/src/tui/app/maintenance-pickers.mjs +324 -0
  235. package/src/tui/app/model-options.mjs +330 -0
  236. package/src/tui/app/model-picker.mjs +365 -0
  237. package/src/tui/app/onboarding-steps.mjs +400 -0
  238. package/src/tui/app/project-picker.mjs +247 -0
  239. package/src/tui/app/provider-setup-picker.mjs +580 -0
  240. package/src/tui/app/resume-picker.mjs +55 -0
  241. package/src/tui/app/route-pickers.mjs +419 -0
  242. package/src/tui/app/settings-picker.mjs +490 -0
  243. package/src/tui/app/slash-commands.mjs +101 -0
  244. package/src/tui/app/slash-dispatch.mjs +427 -0
  245. package/src/tui/app/text-layout.mjs +46 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  247. package/src/tui/app/transcript-window.mjs +671 -0
  248. package/src/tui/app/use-mouse-input.mjs +460 -0
  249. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  250. package/src/tui/app/use-transcript-scroll.mjs +510 -0
  251. package/src/tui/app/use-transcript-window.mjs +589 -0
  252. package/src/tui/components/ConfirmBar.jsx +1 -1
  253. package/src/tui/components/Picker.jsx +32 -4
  254. package/src/tui/components/PromptInput.jsx +23 -101
  255. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  256. package/src/tui/components/StatusLine.jsx +63 -12
  257. package/src/tui/components/TextEntryPanel.jsx +11 -0
  258. package/src/tui/components/ToolExecution.jsx +52 -594
  259. package/src/tui/components/TranscriptItem.jsx +105 -0
  260. package/src/tui/components/UsagePanel.jsx +18 -4
  261. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  262. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  263. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  264. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  265. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  266. package/src/tui/display-width.mjs +20 -3
  267. package/src/tui/dist/index.mjs +19652 -18630
  268. package/src/tui/engine/agent-job-feed.mjs +133 -0
  269. package/src/tui/engine/notification-plan.mjs +76 -0
  270. package/src/tui/engine/render-timing.mjs +17 -0
  271. package/src/tui/engine/tool-approval.mjs +94 -0
  272. package/src/tui/engine/tool-card-results.mjs +234 -0
  273. package/src/tui/engine/tool-result-status.mjs +135 -0
  274. package/src/tui/engine.mjs +122 -562
  275. package/src/tui/figures.mjs +5 -0
  276. package/src/tui/index.jsx +105 -0
  277. package/src/tui/input-editing.mjs +2 -2
  278. package/src/tui/markdown/format-token.mjs +4 -1
  279. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  280. package/src/tui/theme.mjs +6 -0
  281. package/src/ui/statusline-agents.mjs +213 -0
  282. package/src/ui/statusline-format.mjs +146 -0
  283. package/src/ui/statusline-segments.mjs +148 -0
  284. package/src/ui/statusline.mjs +67 -501
  285. package/src/ui/tool-card.mjs +0 -1
  286. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  287. package/src/workflows/default/WORKFLOW.md +1 -1
  288. package/src/workflows/sequential/WORKFLOW.md +1 -1
  289. package/vendor/ink/build/display-width.js +19 -3
  290. package/vendor/ink/build/ink.js +103 -6
  291. package/vendor/ink/build/log-update.js +17 -3
  292. package/vendor/ink/build/wrap-text.js +125 -0
  293. package/scripts/_test-folder-dialog.mjs +0 -30
  294. package/scripts/fix-brief-fn.mjs +0 -35
  295. package/scripts/fix-format-tool-surface.mjs +0 -24
  296. package/scripts/fix-tool-exec-visible.mjs +0 -42
  297. package/scripts/patch-agent-brief.mjs +0 -48
  298. package/scripts/patch-app.mjs +0 -21
  299. package/scripts/patch-app2.mjs +0 -18
  300. package/scripts/patch-dist-brief.mjs +0 -96
  301. package/scripts/patch-tool-exec.mjs +0 -70
  302. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  303. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  304. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  305. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  306. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  307. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  308. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  309. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  310. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  311. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  312. package/src/standalone/agent-task-status.test.mjs +0 -76
  313. package/src/tui/components/tool-output-format.test.mjs +0 -399
  314. package/src/tui/display-width.test.mjs +0 -35
  315. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  316. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  317. package/src/tui/input-editing.selection.test.mjs +0 -75
  318. package/src/tui/markdown/format-token.test.mjs +0 -354
  319. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  320. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  321. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  322. package/src/tui/paste-fix.test.mjs +0 -119
  323. package/src/tui/prompt-history-store.test.mjs +0 -52
  324. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  325. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  326. package/src/ui/markdown.test.mjs +0 -70
  327. package/src/ui/statusline-context-label.test.mjs +0 -15
  328. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  329. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/freevar-smoke.mjs — undeclared-identifier gate for runtime source.
4
+ *
5
+ * Same engine as tui-render-smoke.mjs (acorn + eslint-scope) but pointed at
6
+ * the plain-ESM runtime sources instead of the TUI bundle. Any reference that
7
+ * resolves to no binding and is not a known Node global is a guaranteed
8
+ * ReferenceError on that code path — exactly the class of bug that section
9
+ * extractions leave behind on cold paths (missed import, orphaned helper).
10
+ *
11
+ * Run: node scripts/freevar-smoke.mjs (or `npm run smoke:freevars`)
12
+ * Exit: 0 = clean, 1 = undeclared identifiers found / parse failure.
13
+ */
14
+ import { readFileSync, readdirSync, statSync } from 'node:fs';
15
+ import { dirname, join, relative } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ import { parse } from 'acorn';
18
+ import { analyze } from 'eslint-scope';
19
+
20
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
21
+ // Plain .mjs source roots. src/tui is covered by smoke:tui on the BUILT
22
+ // bundle (App.jsx is JSX — acorn can't parse it raw); vendor is third-party.
23
+ const SCAN_ROOTS = ['src/runtime', 'src/standalone', 'src/shared', 'scripts'].map(p => join(ROOT, p));
24
+ const SKIP_DIRS = new Set(['node_modules', 'dist', '.git']);
25
+
26
+ const KNOWN_GLOBALS = new Set([
27
+ 'Array', 'ArrayBuffer', 'Atomics', 'BigInt', 'BigInt64Array', 'BigUint64Array', 'Boolean',
28
+ 'DataView', 'Date', 'Error', 'EvalError', 'FinalizationRegistry', 'Float32Array',
29
+ 'Float64Array', 'Function', 'Infinity', 'Int8Array', 'Int16Array', 'Int32Array',
30
+ 'Intl', 'JSON', 'Map', 'Math', 'NaN', 'Number', 'Object', 'Promise', 'Proxy',
31
+ 'RangeError', 'ReferenceError', 'Reflect', 'RegExp', 'Set', 'SharedArrayBuffer',
32
+ 'String', 'Symbol', 'SyntaxError', 'TypeError', 'URIError', 'Uint8Array',
33
+ 'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'WeakMap', 'WeakRef', 'WeakSet',
34
+ 'decodeURI', 'decodeURIComponent', 'encodeURI', 'encodeURIComponent', 'eval',
35
+ 'globalThis', 'isFinite', 'isNaN', 'parseFloat', 'parseInt', 'undefined',
36
+ 'AbortController', 'AbortSignal', 'Blob', 'Buffer', 'ByteLengthQueuingStrategy',
37
+ 'CompressionStream', 'CountQueuingStrategy', 'Crypto', 'CryptoKey', 'CustomEvent',
38
+ 'DecompressionStream', 'Event', 'EventTarget', 'File', 'FormData', 'Headers',
39
+ 'MessageChannel', 'MessageEvent', 'MessagePort', 'Navigator', 'PerformanceEntry',
40
+ 'PerformanceObserver', 'ReadableStream', 'ReadableStreamDefaultReader', 'Request',
41
+ 'Response', 'SubtleCrypto', 'TextDecoder', 'TextDecoderStream', 'TextEncoder',
42
+ 'TextEncoderStream', 'TransformStream', 'URL', 'URLSearchParams', 'WebAssembly',
43
+ 'WebSocket', 'WritableStream', 'atob', 'btoa', 'clearImmediate', 'clearInterval',
44
+ 'clearTimeout', 'console', 'crypto', 'fetch', 'navigator', 'performance',
45
+ 'process', 'queueMicrotask', 'require', 'setImmediate', 'setInterval',
46
+ 'setTimeout', 'structuredClone', 'reportError',
47
+ // Browser globals: legal inside puppeteer page.evaluate() callbacks, which
48
+ // execute in the BROWSER context (web-tools.mjs scrapers).
49
+ 'document', 'window',
50
+ ]);
51
+
52
+ function* walk(dir) {
53
+ for (const name of readdirSync(dir)) {
54
+ if (SKIP_DIRS.has(name)) continue;
55
+ const p = join(dir, name);
56
+ const st = statSync(p);
57
+ if (st.isDirectory()) yield* walk(p);
58
+ else if (name.endsWith('.mjs')) yield p;
59
+ }
60
+ }
61
+
62
+ let failures = 0;
63
+ let scanned = 0;
64
+ for (const root of SCAN_ROOTS) {
65
+ let files;
66
+ try { files = [...walk(root)]; } catch { continue; }
67
+ for (const file of files) {
68
+ scanned++;
69
+ const source = readFileSync(file, 'utf8');
70
+ let ast;
71
+ try {
72
+ ast = parse(source, { ecmaVersion: 'latest', sourceType: 'module', locations: true, ranges: true });
73
+ } catch (err) {
74
+ failures++;
75
+ console.error(`freevar-smoke: PARSE FAIL ${relative(ROOT, file)}: ${err.message}`);
76
+ continue;
77
+ }
78
+ const scopeManager = analyze(ast, { ecmaVersion: 2022, sourceType: 'module' });
79
+ const seen = new Set();
80
+ for (const ref of scopeManager.globalScope.through) {
81
+ const { name } = ref.identifier;
82
+ if (KNOWN_GLOBALS.has(name) || seen.has(name)) continue;
83
+ seen.add(name);
84
+ failures++;
85
+ const loc = ref.identifier.loc?.start;
86
+ console.error(`freevar-smoke: FAIL ${relative(ROOT, file)}:${loc ? `${loc.line}:${loc.column}` : '?'} — undeclared identifier \`${name}\``);
87
+ }
88
+ }
89
+ }
90
+
91
+ if (failures > 0) {
92
+ console.error(`freevar-smoke: ${failures} problem(s) across ${scanned} files — these are ReferenceErrors waiting on their code path.`);
93
+ process.exit(1);
94
+ }
95
+ console.log(`freevar-smoke: ok — ${scanned} files, no undeclared identifiers`);
@@ -36,6 +36,7 @@ const RULE_FILES = [
36
36
  'agents/debugger/AGENT.md',
37
37
  'workflows/default/WORKFLOW.md',
38
38
  'rules/lead/lead-tool.md',
39
+ 'rules/lead/lead-brief.md',
39
40
  ];
40
41
 
41
42
  const DEFAULT_WORKER_PROMPT =
@@ -24,17 +24,18 @@ function flat(text) {
24
24
  return String(text || '').replace(/\s+/g, ' ');
25
25
  }
26
26
 
27
- // --- Lead brief contract: canonical in lead-tool.md, referenced from WORKFLOW -
27
+ // --- Lead brief contract: canonical in lead-brief.md, referenced from WORKFLOW -
28
28
  const workflow = readSrc('workflows', 'default', 'WORKFLOW.md');
29
- const leadTool = readSrc('rules', 'lead', 'lead-tool.md');
29
+ const leadBrief = readSrc('rules', 'lead', 'lead-brief.md');
30
30
  const BRIEF_FIELDS = ['Goal:', 'Anchors:', 'Allow/Forbid:', 'Deliver:', 'Verify:'];
31
- // Canonical brief contract lives in lead-tool.md (Lead tool-use rules).
32
- assert(/minimum characters, maximum information/i.test(flat(leadTool)), 'lead-tool.md: brief must state min-char/max-info principle');
33
- for (const field of BRIEF_FIELDS) assert(leadTool.includes(field), `lead-tool.md: brief missing labeled field ${field}`);
34
- assert(leadTool.includes('Stop:'), 'lead-tool.md: brief must add Stop: for heavy-worker bound');
35
- assert(/role-known|already (?:owns|knows)|wasted cost|wasted/i.test(flat(leadTool)), 'lead-tool.md: brief must ban restating known rules/background as cost');
36
- // WORKFLOW.md must not duplicate the field list; it defers to the lead-tool contract.
37
- assert(/lead-tool brief contract/i.test(flat(workflow)), 'WORKFLOW.md: must defer to the lead-tool brief contract');
31
+ // Canonical brief contract lives in lead-brief.md (Lead brief contract).
32
+ assert(/minimum characters, maximum information/i.test(flat(leadBrief)), 'lead-brief.md: brief must state min-char/max-info principle');
33
+ for (const field of BRIEF_FIELDS) assert(leadBrief.includes(field), `lead-brief.md: brief missing labeled field ${field}`);
34
+ assert(leadBrief.includes('Stop:'), 'lead-brief.md: brief must add Stop: for heavy-worker bound');
35
+ assert(/role-known|already (?:owns|knows)|wasted cost|wasted/i.test(flat(leadBrief)), 'lead-brief.md: brief must ban restating known rules/background as cost');
36
+ assert(/authoritative/i.test(flat(leadBrief)), 'lead-brief.md: brief must state spec-file precedence over summary');
37
+ // WORKFLOW.md must not duplicate the field list; it defers to the lead brief contract.
38
+ assert(/lead brief contract/i.test(flat(workflow)), 'WORKFLOW.md: must defer to the lead brief contract');
38
39
  assert(!BRIEF_FIELDS.every((field) => workflow.includes(field)), 'WORKFLOW.md: must not duplicate the full brief field list');
39
40
 
40
41
  // --- Agent handoff contract (00-common.md) ---------------------------------
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/mouse-probe.mjs — raw SGR mouse event probe.
4
+ *
5
+ * Enables the same SGR mouse tracking the TUI uses and prints every mouse
6
+ * escape sequence it receives, decoded (button id + modifier bits + coords).
7
+ * Use it to see EXACTLY what the terminal sends for ctrl+wheel — if the ctrl
8
+ * bit (16) never appears, the terminal is not forwarding the modifier and the
9
+ * app-side zoom passthrough can never trigger.
10
+ *
11
+ * Run: node scripts/mouse-probe.mjs (q or Ctrl+C to quit)
12
+ */
13
+ const ON = '\x1b[?1000h\x1b[?1002h\x1b[?1006h';
14
+ const OFF = '\x1b[?1006l\x1b[?1002l\x1b[?1000l';
15
+ const MOUSE = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
16
+
17
+ process.stdout.write(ON);
18
+ process.stdout.write('mouse-probe: wheel / ctrl+wheel / click here. q to quit.\n');
19
+ process.stdin.setRawMode(true);
20
+ process.stdin.resume();
21
+
22
+ const cleanup = () => {
23
+ process.stdout.write(OFF);
24
+ process.stdin.setRawMode(false);
25
+ process.exit(0);
26
+ };
27
+ process.on('SIGINT', cleanup);
28
+
29
+ process.stdin.on('data', (buf) => {
30
+ const s = buf.toString('utf8');
31
+ if (s === 'q' || s === '\x03') return cleanup();
32
+ let m; MOUSE.lastIndex = 0;
33
+ let sawMouse = false;
34
+ while ((m = MOUSE.exec(s)) !== null) {
35
+ sawMouse = true;
36
+ const b = Number(m[1]);
37
+ const mods = [b & 4 ? 'shift' : '', b & 8 ? 'alt' : '', b & 16 ? 'ctrl' : ''].filter(Boolean).join('+') || 'none';
38
+ const base = b & ~(4 | 8 | 16);
39
+ const kind = base === 64 ? 'wheel-up' : base === 65 ? 'wheel-down' : base === 0 ? 'left' : base === 2 ? 'right' : (b & 32) ? 'motion' : `btn${base}`;
40
+ process.stdout.write(`button=${b} base=${base} kind=${kind} mods=${mods} x=${m[2]} y=${m[3]} ${m[4] === 'M' ? 'press' : 'release'}\n`);
41
+ }
42
+ if (!sawMouse && s.startsWith('\x1b')) {
43
+ process.stdout.write(`non-mouse escape: ${JSON.stringify(s)}\n`);
44
+ }
45
+ });
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Lead output-style verbosity bench (default >= simple >= minimal >= oneline).
2
+ // Lead output-style verbosity bench (default >= simple >= minimal >= extreme-minimal).
3
3
  import { createRequire } from 'node:module';
4
4
  import { execFileSync } from 'node:child_process';
5
5
  import {
@@ -19,7 +19,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
19
19
  const __dir = dirname(fileURLToPath(import.meta.url));
20
20
  const REPO_ROOT = resolve(__dir, '..');
21
21
  const PLUGIN_ROOT = join(REPO_ROOT, 'src');
22
- const STYLES = ['default', 'simple', 'minimal', 'oneline'];
22
+ const STYLES = ['default', 'simple', 'minimal', 'extreme-minimal'];
23
23
  const DEFAULT_PROMPT =
24
24
  'Reply in plain English only. What is 2+2? Give a short summary suitable for a status report (no tools, no file reads).';
25
25
  const MODEL_ALIASES = {
@@ -85,10 +85,10 @@ function runInjectionScaffold() {
85
85
  ? JSON.parse(readFileSync(templatePath, 'utf8'))
86
86
  : { outputStyle: 'default' };
87
87
  const markers = {
88
- default: 'most detailed of the three',
88
+ default: 'most detailed style',
89
89
  simple: 'Practical concise',
90
90
  minimal: 'a very short summary',
91
- oneline: 'exactly one sentence',
91
+ 'extreme-minimal': 'exactly one sentence',
92
92
  };
93
93
  const snippets = {};
94
94
  try {
@@ -179,11 +179,18 @@ function runLiveLeadTurn({ dataDir, prompt, provider, model, cwd, effort, fast }
179
179
  `let result;`,
180
180
  `try { result = await askSession(session.id, ${JSON.stringify(prompt)}, null, null, ${JSON.stringify(cwd)}); }`,
181
181
  `finally { try { closeSession(session.id, 'output-style-bench'); } catch {} }`,
182
- `process.stdout.write(JSON.stringify({ text: String(result?.text || result?.content || '').trim(), sessionId: session.id }));`,
182
+ // Pooled provider sockets (e.g. openai-oauth WS, 20-min idle TTL) keep the
183
+ // child alive after the turn; flush JSON then force-exit so execFileSync
184
+ // returns as soon as the turn completes.
185
+ `const payload = JSON.stringify({ text: String(result?.text || result?.content || '').trim(), sessionId: session.id });`,
186
+ `await new Promise((r) => process.stdout.write(payload, r));`,
187
+ `process.exit(0);`,
183
188
  ].filter(Boolean).join('\n');
184
189
  const raw = execFileSync('node', ['--input-type=module', '-e', driver], {
185
190
  encoding: 'utf8',
186
191
  maxBuffer: 64 * 1024 * 1024,
192
+ timeout: Number(process.env.OUTPUT_STYLE_BENCH_TURN_TIMEOUT_MS || 10 * 60_000),
193
+ killSignal: 'SIGKILL',
187
194
  env: { ...process.env, MIXDOG_ROOT: PLUGIN_ROOT, MIXDOG_DATA_DIR: dataDir },
188
195
  });
189
196
  const jsonStart = raw.lastIndexOf('{');
@@ -209,7 +216,7 @@ function evaluateVerbosityOrdering(results) {
209
216
  return { orderingOk, verdict, counts: styleCharCountsFromResults(results) };
210
217
  }
211
218
  function printUsage() {
212
- process.stdout.write(`output-style-bench — Lead verbosity ladder (default >= simple >= minimal >= oneline).
219
+ process.stdout.write(`output-style-bench — Lead verbosity ladder (default >= simple >= minimal >= extreme-minimal).
213
220
 
214
221
  Output style is Lead-only (buildLeadMetaContent when owner is not agent).
215
222
  runHeadlessRole worker paths (owner=agent) do NOT inject outputStyle.
@@ -49,8 +49,8 @@ const defaultStyle = readFileSync(join(root, 'src', 'output-styles', 'default.md
49
49
  for (const required of [
50
50
  'name: default',
51
51
  'Concise engineering summaries',
52
- 'Mixdog default — the most detailed of the three styles',
53
- 'match length to the work',
52
+ 'Mixdog default — the most detailed style',
53
+ 'State conclusions, not reasoning',
54
54
  'Use labels such as',
55
55
  '`바뀐 점`, `확인한 것`,',
56
56
  'Synthesize agent or retrieval results',
@@ -72,7 +72,7 @@ for (const required of [
72
72
  'controlled detail',
73
73
  'Synthesize agent or retrieval results',
74
74
  'Do not hide blockers',
75
- 'if verification was',
75
+ 'verification was not run, say so once',
76
76
  ]) {
77
77
  assert(simpleStyle.includes(required), `simple.md missing: ${required}`);
78
78
  }
@@ -81,7 +81,7 @@ for (const [name, style] of Object.entries({
81
81
  default: defaultStyle,
82
82
  simple: simpleStyle,
83
83
  minimal: readFileSync(join(root, 'src', 'output-styles', 'minimal.md'), 'utf8'),
84
- oneline: readFileSync(join(root, 'src', 'output-styles', 'oneline.md'), 'utf8'),
84
+ 'extreme-minimal': readFileSync(join(root, 'src', 'output-styles', 'extreme-minimal.md'), 'utf8'),
85
85
  })) {
86
86
  assert(!style.includes('pre-tool preamble'), `${name} output style must not own language preamble rules`);
87
87
  assert(!style.includes('selected/default user language'), `${name} output style must not duplicate profile language rules`);
@@ -946,17 +946,21 @@ test('anthropic effort: legacy sonnet-4-5 maps effort to thinking budget', () =>
946
946
  assert.equal(modelSupportsEffort(model), false);
947
947
  });
948
948
 
949
- test('anthropic effort: xhigh on opus-4-8 normalizes to max in output_config', () => {
949
+ test('anthropic effort: xhigh on opus-4-8 is a first-class level (not downgraded to max)', () => {
950
+ // Opus 4.8 supports xhigh (modelSupportsXhighEffort), so xhigh is kept
951
+ // verbatim as a first-class effort level — matching codex's ReasoningEffort
952
+ // enum which lists xhigh between high and max. Only models WITHOUT xhigh
953
+ // support clamp it down to high.
950
954
  const model = 'claude-opus-4-8';
951
955
  assert.equal(modelSupportsMaxEffort(model), true);
952
- assert.equal(normalizeAnthropicEffortInput('xhigh', model), 'max');
956
+ assert.equal(normalizeAnthropicEffortInput('xhigh', model), 'xhigh');
953
957
  const body = _buildRequestBodyForCacheSmoke(
954
958
  [{ role: 'user', content: 'hi' }],
955
959
  model,
956
960
  [],
957
961
  { effort: 'xhigh' },
958
962
  );
959
- assert.deepEqual(body.output_config, { effort: 'max' });
963
+ assert.deepEqual(body.output_config, { effort: 'xhigh' });
960
964
  assert.equal(body.thinking, undefined);
961
965
  });
962
966
 
@@ -0,0 +1,18 @@
1
+ [
2
+ { "id": "uc1-last", "label": "UC1 previous conversation (period=last)", "args": { "period": "last", "limit": 12 }, "expect": "browse" },
3
+ { "id": "uc1-last-page2", "label": "UC1 last paging (offset)", "args": { "period": "last", "limit": 12, "offset": 12 }, "expect": "browse" },
4
+ { "id": "uc2-recent-30m", "label": "UC2 recent 30m", "args": { "period": "30m", "limit": 10 }, "expect": "browse" },
5
+ { "id": "uc2-recent-3h", "label": "UC2 recent 3h", "args": { "period": "3h", "limit": 10 }, "expect": "browse" },
6
+ { "id": "uc2-recent-24h", "label": "UC2 recent 24h", "args": { "period": "24h", "limit": 10 }, "expect": "browse" },
7
+ { "id": "uc3-tod-today", "label": "UC3 time-of-day today 12:00~14:00", "args": { "period": "12:00~14:00", "limit": 10 }, "expect": "browse" },
8
+ { "id": "uc3-tod-dated", "label": "UC3 dated time window 2026-07-03 09:00~12:00", "args": { "period": "2026-07-03 09:00~12:00", "limit": 10 }, "expect": "browse" },
9
+ { "id": "uc3-date", "label": "UC3 specific date 2026-07-02", "args": { "period": "2026-07-02", "limit": 10 }, "expect": "browse" },
10
+ { "id": "uc3-range", "label": "UC3 date range 2026-07-01~2026-07-03", "args": { "period": "2026-07-01~2026-07-03", "limit": 10 }, "expect": "browse" },
11
+ { "id": "uc4-topic", "label": "UC4 topic query", "args": { "query": "recall period last 개선", "limit": 8 }, "expect": "results" },
12
+ { "id": "uc4-topic-time", "label": "UC4 topic + time window", "args": { "query": "recall", "period": "24h", "limit": 8 }, "expect": "results" },
13
+ { "id": "uc4-topic-tod", "label": "UC4 topic + time-of-day", "args": { "query": "recall", "period": "12:00~15:00", "limit": 8 }, "expect": "results" },
14
+ { "id": "uc4-fanout", "label": "UC4 fan-out array query", "args": { "query": ["recall 개선", "battle balance"], "limit": 5 }, "expect": "results" },
15
+ { "id": "uc5-yesterday", "label": "UC5 calendar yesterday", "args": { "period": "yesterday", "limit": 10 }, "expect": "browse" },
16
+ { "id": "uc5-thisweek", "label": "UC5 calendar this_week", "args": { "period": "this_week", "limit": 10 }, "expect": "browse" },
17
+ { "id": "uc6-category", "label": "UC6 category decision 7d", "args": { "period": "7d", "category": "decision", "limit": 10 }, "expect": "browse" }
18
+ ]
@@ -0,0 +1,6 @@
1
+ [
2
+ { "id": "t1", "label": "topic ko-en mixed", "args": { "query": "recall period last 개선", "limit": 8 }, "expect": "results" },
3
+ { "id": "t2", "label": "topic ko only", "args": { "query": "recall 개선", "limit": 8 }, "expect": "results" },
4
+ { "id": "t3", "label": "topic en only", "args": { "query": "recall period last improvement", "limit": 8 }, "expect": "results" },
5
+ { "id": "t4", "label": "category no-raw", "args": { "period": "7d", "category": "decision", "limit": 10, "includeRaw": false }, "expect": "browse" }
6
+ ]
@@ -155,6 +155,10 @@ function grepSweepKey(row) {
155
155
 
156
156
  const GREP_SWEEP_MIN_CALLS = 20;
157
157
  const GREP_SWEEP_MIN_UNIQUE = 16;
158
+ const READONLY_TOOL_NAMES = new Set(['read', 'grep', 'glob', 'list', 'find', 'code_graph', 'recall', 'explore', 'search', 'web_fetch']);
159
+ const READONLY_STALL_MIN_RUN = 8;
160
+ const READONLY_ROLE_AGENTS = new Set(['reviewer', 'explore', 'explorer', 'debugger']);
161
+ const IDENTICAL_CALL_MIN_COUNT = 3;
158
162
 
159
163
  function fmtMs(ms) {
160
164
  const n = Number(ms);
@@ -313,6 +317,9 @@ function sessionMatches(id, query) {
313
317
  }
314
318
 
315
319
  function selectSessionIds(sessionMetas, query) {
320
+ if (String(query || '').trim().toLowerCase() === 'all') {
321
+ return sessionMetas.map((m) => m.session_id);
322
+ }
316
323
  let selected;
317
324
  if (!query || query === 'current') selected = chooseCurrentSession(sessionMetas);
318
325
  else selected = sessionMetas.find((m) => sessionMatches(m.session_id, query));
@@ -331,16 +338,27 @@ function selectSessionIds(sessionMetas, query) {
331
338
  function filterAgent(rows, agent) {
332
339
  if (!agent) return rows;
333
340
  const q = String(agent).toLowerCase();
334
- return rows.filter((r) => {
341
+ const rowMatches = (r) => {
335
342
  const values = [
336
- field(r, 'agent'),
337
343
  field(r, 'agent'),
338
344
  field(r, 'sourceName'),
339
345
  field(r, 'preset'),
340
346
  field(r, 'preset_name'),
341
347
  ].filter(Boolean).map((v) => String(v).toLowerCase());
342
348
  return values.some((v) => v.includes(q));
343
- });
349
+ };
350
+ // Row-level agent fields only exist on some kinds (tool/steer); usage_raw and
351
+ // transport rows carry no agent. Filter at SESSION granularity: keep every
352
+ // row of any session where at least one row matches, so per-agent views keep
353
+ // their usage/cache/turn aggregates intact.
354
+ const matchedSessions = new Set();
355
+ for (const r of rows) {
356
+ if (rowMatches(r)) {
357
+ const sid = sessionId(r);
358
+ if (sid) matchedSessions.add(sid);
359
+ }
360
+ }
361
+ return rows.filter((r) => (sessionId(r) ? matchedSessions.has(sessionId(r)) : rowMatches(r)));
344
362
  }
345
363
 
346
364
  function buildRouteGroups(rows) {
@@ -384,6 +402,7 @@ function buildCacheDiagnostics(rows) {
384
402
  const transport = rows.filter((r) => r.kind === 'transport');
385
403
  const usage = rows.filter((r) => r.kind === 'usage_raw');
386
404
  const breaks = rows.filter((r) => r.kind === 'cache_break');
405
+ const misses = rows.filter((r) => r.kind === 'cache_miss');
387
406
  const keyCounts = new Map();
388
407
  for (const r of transport) {
389
408
  const key = field(r, 'cache_key_hash');
@@ -443,6 +462,20 @@ function buildCacheDiagnostics(rows) {
443
462
  ts: r.ts,
444
463
  };
445
464
  }),
465
+ actual_cache_misses: misses.map((r) => ({
466
+ session_id: sessionId(r),
467
+ iteration: num(r, 'iteration'),
468
+ reason: field(r, 'reason') || field(r, 'payload')?.reason || 'unknown',
469
+ ws_mode: field(r, 'ws_mode'),
470
+ cache_key_hash: field(r, 'cache_key_hash'),
471
+ request_has_previous_response_id: field(r, 'request_has_previous_response_id'),
472
+ prompt_tokens: num(r, 'prompt_tokens') || num(field(r, 'payload'), 'prompt_tokens') || 0,
473
+ cached_tokens: num(r, 'cached_tokens') || num(field(r, 'payload'), 'cached_tokens') || 0,
474
+ uncached_tokens: num(r, 'uncached_tokens') || num(field(r, 'payload'), 'uncached_tokens') || 0,
475
+ previous_max_cached_tokens: num(r, 'previous_max_cached_tokens') || num(field(r, 'payload'), 'previous_max_cached_tokens') || 0,
476
+ cache_ratio: num(r, 'cache_ratio') ?? num(field(r, 'payload'), 'cache_ratio'),
477
+ ts: r.ts,
478
+ })),
446
479
  service_tier_downgrades: downgrades,
447
480
  low_cache_turns: usage.map((r) => {
448
481
  const denom = cacheDenom(r);
@@ -452,10 +485,21 @@ function buildCacheDiagnostics(rows) {
452
485
  };
453
486
  }
454
487
 
488
+ function summarizePathArg(pathValue) {
489
+ if (Array.isArray(pathValue)) {
490
+ const parts = pathValue.map((p) => (typeof p === 'string' ? p : `${p?.path || '?'}${p?.offset != null ? `:${p.offset}` : ''}`));
491
+ return `batch[${parts.length}] ${parts.slice(0, 3).join(', ')}${parts.length > 3 ? ', …' : ''}`;
492
+ }
493
+ return String(pathValue || '');
494
+ }
495
+
455
496
  function summarizeToolTarget(row) {
456
497
  const args = toolArgs(row) || {};
457
498
  const name = String(field(row, 'tool_name') || '');
458
- if (name === 'read') return `${args.path || '?'}:${args.line || args.offset || ''}`;
499
+ if (name === 'read') {
500
+ if (Array.isArray(args.path)) return summarizePathArg(args.path);
501
+ return `${args.path || '?'}:${args.line || args.offset || ''}`;
502
+ }
459
503
  if (name === 'grep') return `${args.path || '?'} :: ${String(args.pattern || '').slice(0, 80)}`;
460
504
  if (name === 'code_graph') return `${args.mode || '?'} ${args.file || ''} ${args.symbol || ''}`.trim();
461
505
  if (name === 'find') return `${args.path || '?'} :: ${args.query || ''}`;
@@ -731,6 +775,62 @@ function buildToolDiagnostics(rows, failureRows = []) {
731
775
  };
732
776
  }).sort((a, b) => b.count - a.count || b.tool_ms - a.tool_ms).slice(0, 20);
733
777
 
778
+ const readonlyStalls = [];
779
+ for (const [sid, srows] of groupBy(tools, sessionId).entries()) {
780
+ for (const [agent, arows] of groupBy(srows, (r) => field(r, 'agent') || field(r, 'sourceName') || '').entries()) {
781
+ const sorted = [...arows].sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
782
+ let run = [];
783
+ const flushRun = () => {
784
+ const turns = new Set(run.map((r) => num(r, 'iteration')).filter((n) => n != null)).size;
785
+ if (turns >= READONLY_STALL_MIN_RUN) {
786
+ const pathCounts = countBy(run, (r) => summarizePathArg((toolArgs(r) || {}).path)).filter(([p]) => p).slice(0, 3);
787
+ readonlyStalls.push({
788
+ session_id: sid,
789
+ agent: agent || null,
790
+ run_length: run.length,
791
+ run_turns: turns,
792
+ start_it: num(run[0], 'iteration'),
793
+ end_it: num(run[run.length - 1], 'iteration'),
794
+ top_paths: pathCounts.map(([path, count]) => ({ path, count })),
795
+ });
796
+ }
797
+ run = [];
798
+ };
799
+ for (const row of sorted) {
800
+ const name = String(field(row, 'tool_name') || '');
801
+ if (!READONLY_TOOL_NAMES.has(name)) { flushRun(); continue; }
802
+ // A backwards iteration jump means a new prompt/steer restarted the
803
+ // loop; that is a fresh run, not a continuation of the same stall.
804
+ const it = num(row, 'iteration');
805
+ const prevIt = run.length ? num(run[run.length - 1], 'iteration') : null;
806
+ if (it != null && prevIt != null && it < prevIt) flushRun();
807
+ run.push(row);
808
+ }
809
+ flushRun();
810
+ }
811
+ }
812
+ readonlyStalls.sort((a, b) => (b.run_turns || 0) - (a.run_turns || 0) || b.run_length - a.run_length);
813
+
814
+ const identicalCallRepeats = [];
815
+ for (const [key, trows] of groupBy(
816
+ tools.filter((r) => toolResultKind(r) !== 'error'),
817
+ (r) => `${sessionId(r)}::${field(r, 'tool_name') || ''}::${toolArgsHash(r) || ''}`,
818
+ ).entries()) {
819
+ if (!toolArgsHash(trows[0])) continue;
820
+ const iterations = [...new Set(trows.map((r) => num(r, 'iteration')).filter((n) => n != null))];
821
+ if (iterations.length >= IDENTICAL_CALL_MIN_COUNT) {
822
+ const sorted = [...trows].sort((a, b) => Number(a.ts || 0) - Number(b.ts || 0));
823
+ identicalCallRepeats.push({
824
+ session_id: sessionId(sorted[0]),
825
+ tool: field(sorted[0], 'tool_name'),
826
+ args_hash: toolArgsHash(sorted[0]),
827
+ count: iterations.length,
828
+ iterations: iterations.slice(0, 6),
829
+ });
830
+ }
831
+ }
832
+ identicalCallRepeats.sort((a, b) => b.count - a.count);
833
+
734
834
  return {
735
835
  total_tool_calls: tools.length,
736
836
  total_tool_ms: sum(tools.map((r) => num(r, 'tool_ms'))),
@@ -748,6 +848,8 @@ function buildToolDiagnostics(rows, failureRows = []) {
748
848
  single_tool_batches: singleToolBatches.length,
749
849
  },
750
850
  sequential_tool_clusters: sequentialToolClusters,
851
+ readonly_stalls: readonlyStalls.slice(0, 20),
852
+ identical_call_repeats: identicalCallRepeats.slice(0, 20),
751
853
  };
752
854
  }
753
855
 
@@ -1067,22 +1169,52 @@ function buildCompactDiagnostics(rows, selectedIds) {
1067
1169
 
1068
1170
  function buildIssues(routeGroups, cache, tools) {
1069
1171
  const issues = [];
1172
+ for (const stall of (tools.readonly_stalls || []).slice(0, 10)) {
1173
+ const readonlyRole = READONLY_ROLE_AGENTS.has(String(stall.agent || '').toLowerCase());
1174
+ const turns = stall.run_turns ?? stall.run_length;
1175
+ issues.push({
1176
+ severity: readonlyRole ? 'low' : (turns >= 15 ? 'high' : 'medium'),
1177
+ type: 'readonly_stall',
1178
+ message: `${stall.agent || shortId(stall.session_id)} readonly stall ${turns} turns/${stall.run_length} calls (it=${stall.start_it ?? '-'}..${stall.end_it ?? '-'})${readonlyRole ? ' [readonly role]' : ''} top paths: ${stall.top_paths.map((p) => `${p.path}×${p.count}`).join(', ') || '-'}`,
1179
+ session_id: stall.session_id,
1180
+ });
1181
+ }
1182
+ for (const rep of (tools.identical_call_repeats || []).slice(0, 10)) {
1183
+ issues.push({
1184
+ severity: 'medium',
1185
+ type: 'identical_call_repeat',
1186
+ message: `identical ${rep.tool} call x${rep.count} (hash=${rep.args_hash}) at it=${rep.iterations.join(',')}`,
1187
+ session_id: rep.session_id,
1188
+ });
1189
+ }
1070
1190
  for (const g of routeGroups) {
1191
+ // One-shot pseudo-sessions (native web search stamps `<caller>:native-search:<ts>`
1192
+ // per send) have no loop, so full-WS/low-cache is their normal shape, not an issue.
1193
+ if (/:native-search:/.test(String(g.session_id || ''))) continue;
1071
1194
  if (g.ws_full > 0 && g.ws_delta === 0 && g.transport_rows > 0) {
1072
1195
  issues.push({ severity: 'high', type: 'cache', message: `${g.agent || shortId(g.session_id)} stayed full WS (${g.ws_full} full, 0 delta)`, session_id: g.session_id });
1073
1196
  } else if (g.ws_full > 0) {
1074
1197
  issues.push({ severity: 'medium', type: 'cache', message: `${g.agent || shortId(g.session_id)} had ${g.ws_full} full WS turn(s) before delta`, session_id: g.session_id });
1075
1198
  }
1076
- if (g.cache_ratio != null && g.prompt_tokens > 5000 && g.cache_ratio < 0.25) {
1199
+ // Sessions with <=2 turns (explorer-style ephemeral one-shots) have no
1200
+ // prior turn to cache against; low ratio is structural, not actionable.
1201
+ if (g.cache_ratio != null && g.prompt_tokens > 5000 && g.cache_ratio < 0.25 && (g.turns ?? 0) > 2) {
1077
1202
  issues.push({ severity: 'high', type: 'cache', message: `${g.agent || shortId(g.session_id)} low cache ratio ${fmtPct(g.cache_ratio * 100)}`, session_id: g.session_id });
1078
1203
  }
1079
1204
  }
1080
1205
  for (const b of cache.cache_breaks.slice(0, 10)) {
1081
1206
  issues.push({ severity: b.reason === 'no_anchor' ? 'medium' : 'high', type: 'cache_break', message: `cache_break ${b.reason || 'unknown'} at it=${b.iteration ?? '-'}`, session_id: b.session_id });
1082
1207
  }
1208
+ for (const m of (cache.actual_cache_misses || []).slice(0, 10)) {
1209
+ issues.push({ severity: 'high', type: 'cache_miss', message: `cache_miss ${m.reason || 'unknown'} at it=${m.iteration ?? '-'} uncached=${fmtTok(m.uncached_tokens)}`, session_id: m.session_id });
1210
+ }
1083
1211
  if (cache.service_tier_downgrades.length) {
1084
1212
  const first = cache.service_tier_downgrades[0];
1085
- issues.push({ severity: 'medium', type: 'service_tier', message: `${cache.service_tier_downgrades.length} service tier downgrade(s), e.g. ${first.requested}->${first.response}`, session_id: first.session_id });
1213
+ // priority->default is the backend's normal answer when the account/plan
1214
+ // has no priority lane — expected, not actionable per session. Keep it
1215
+ // visible as info-level; only other downgrade shapes stay medium.
1216
+ const expected = cache.service_tier_downgrades.every((d) => d.requested === 'priority' && d.response === 'default');
1217
+ issues.push({ severity: expected ? 'low' : 'medium', type: 'service_tier', message: `${cache.service_tier_downgrades.length} service tier downgrade(s), e.g. ${first.requested}->${first.response}${expected ? ' (expected: no priority lane on this account)' : ''}`, session_id: first.session_id });
1086
1218
  }
1087
1219
  if (tools.failures.length) {
1088
1220
  const byTool = new Map();
@@ -1146,6 +1278,10 @@ function buildWhySlowRankings(report) {
1146
1278
  const summary = countBy(report.cache.cache_breaks, (b) => `${b.reason || 'unknown'}/${b.phase || 'unknown'}`).slice(0, 3).map(([k, v]) => `${k}×${v}`).join(', ');
1147
1279
  ranks.push({ score: 40 + report.cache.cache_breaks.length, type: 'cache_breaks', message: `${report.cache.cache_breaks.length} cache break(s): ${summary}` });
1148
1280
  }
1281
+ if (report.cache.actual_cache_misses?.length) {
1282
+ const top = [...report.cache.actual_cache_misses].sort((a, b) => (b.uncached_tokens || 0) - (a.uncached_tokens || 0))[0];
1283
+ ranks.push({ score: 60 + (top?.uncached_tokens || 0) / 1000, type: 'cache_miss', message: `${report.cache.actual_cache_misses.length} actual cache miss(es), top uncached=${fmtTok(top?.uncached_tokens || 0)} it=${top?.iteration ?? '-'}` });
1284
+ }
1149
1285
  const topGrowth = report.tokens.growth_turns[0];
1150
1286
  if (topGrowth) {
1151
1287
  ranks.push({ score: Math.max(0, topGrowth.prompt_delta) / 1000, type: 'prompt_growth', message: `${topGrowth.agent || '-'} it=${topGrowth.turn_label} prompt Δ=${fmtTok(topGrowth.prompt_delta)} out=${fmtTok(topGrowth.output_tokens + topGrowth.thinking_tokens)} cache=${fmtPct((topGrowth.cache_ratio ?? 0) * 100)}` });
@@ -1173,6 +1309,10 @@ function buildExecutiveSummary(report) {
1173
1309
  const summary = countBy(report.cache.cache_breaks, (b) => `${b.reason || 'unknown'}/${b.phase || 'unknown'}`).slice(0, 3).map(([k, v]) => `${k}×${v}`).join(', ');
1174
1310
  lines.push(`캐시 깨짐=${report.cache.cache_breaks.length} (${summary})`);
1175
1311
  }
1312
+ if (report.cache.actual_cache_misses?.length) {
1313
+ const top = [...report.cache.actual_cache_misses].sort((a, b) => (b.uncached_tokens || 0) - (a.uncached_tokens || 0))[0];
1314
+ lines.push(`실제 캐시 미스=${report.cache.actual_cache_misses.length} (top uncached=${fmtTok(top?.uncached_tokens || 0)})`);
1315
+ }
1176
1316
  if (report.compact?.cache_breaks?.length) {
1177
1317
  const summary = countBy(report.compact.cache_breaks, (b) => `${b.reason || 'unknown'}/${b.phase || 'unknown'}`).slice(0, 3).map(([k, v]) => `${k}×${v}`).join(', ');
1178
1318
  lines.push(`컴팩트 호출=${report.compact.sessions.length} session, cache reset=${report.compact.cache_breaks.length} (${summary})`);
@@ -1344,6 +1484,12 @@ function renderText(report) {
1344
1484
  lines.push(`- ${shortId(b.session_id)} it=${b.iteration ?? '-'} phase=${b.phase || '-'} reason=${b.reason || '-'} (${b.explanation || '-'}) ws=${b.ws_mode || '-'} prev=${b.request_has_previous_response_id} cache=${fmtPct((b.cache_ratio ?? 0) * 100)} prompt=${fmtTok(b.prompt_tokens)} out=${fmtTok(b.output_tokens)} body/frame=${b.body_input_items ?? '-'}/${b.frame_input_items ?? '-'}`);
1345
1485
  }
1346
1486
  }
1487
+ if (report.cache.actual_cache_misses?.length) {
1488
+ lines.push('actual cache misses:');
1489
+ for (const m of report.cache.actual_cache_misses.slice(0, 10)) {
1490
+ lines.push(`- ${shortId(m.session_id)} it=${m.iteration ?? '-'} reason=${m.reason || '-'} ws=${m.ws_mode || '-'} prev=${m.request_has_previous_response_id} cache=${fmtPct((m.cache_ratio ?? 0) * 100)} prompt=${fmtTok(m.prompt_tokens)} uncached=${fmtTok(m.uncached_tokens)} prevMax=${fmtTok(m.previous_max_cached_tokens)}`);
1491
+ }
1492
+ }
1347
1493
  if (report.cache.service_tier_downgrades.length) {
1348
1494
  lines.push('service tier downgrades:');
1349
1495
  for (const d of report.cache.service_tier_downgrades.slice(0, 10)) lines.push(`- ${shortId(d.session_id)} it=${d.iteration ?? '-'} ${d.requested}->${d.response}`);