mixdog 0.9.3 → 0.9.5

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 (382) hide show
  1. package/README.md +112 -38
  2. package/package.json +10 -3
  3. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  4. package/scripts/bench/lead-review-tasks.json +20 -0
  5. package/scripts/bench/r4-mixed-tasks.json +20 -0
  6. package/scripts/bench/r5-orchestrated-task.json +7 -0
  7. package/scripts/bench/review-tasks.json +20 -0
  8. package/scripts/bench/round-codex.json +114 -0
  9. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  10. package/scripts/bench/round-mixdog-lead.json +269 -0
  11. package/scripts/bench/round-mixdog.json +126 -0
  12. package/scripts/bench/round-r10-bigsample.json +679 -0
  13. package/scripts/bench/round-r11-codexalign.json +257 -0
  14. package/scripts/bench/round-r13-clientmeta.json +464 -0
  15. package/scripts/bench/round-r14-betafeatures.json +466 -0
  16. package/scripts/bench/round-r15-fulldefault.json +462 -0
  17. package/scripts/bench/round-r16-sessionid.json +466 -0
  18. package/scripts/bench/round-r17-wirebytes.json +456 -0
  19. package/scripts/bench/round-r18-prewarm.json +468 -0
  20. package/scripts/bench/round-r19-clean.json +472 -0
  21. package/scripts/bench/round-r20-prewarm-clean.json +475 -0
  22. package/scripts/bench/round-r21-delta-retry.json +473 -0
  23. package/scripts/bench/round-r22-full-probe.json +693 -0
  24. package/scripts/bench/round-r23-itemprobe.json +701 -0
  25. package/scripts/bench/round-r24-shapefix.json +677 -0
  26. package/scripts/bench/round-r25-serial.json +464 -0
  27. package/scripts/bench/round-r26-parallel3.json +671 -0
  28. package/scripts/bench/round-r27-parallel10.json +894 -0
  29. package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
  30. package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
  31. package/scripts/bench/round-r30-instid.json +253 -0
  32. package/scripts/bench/round-r31-upgradeprobe.json +256 -0
  33. package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
  34. package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
  35. package/scripts/bench/round-r34-orchestrated.json +120 -0
  36. package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
  37. package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
  38. package/scripts/bench/round-r4-codex.json +114 -0
  39. package/scripts/bench/round-r4-mixed.json +225 -0
  40. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  41. package/scripts/bench/round-r6-codex.json +114 -0
  42. package/scripts/bench/round-r6-solo.json +257 -0
  43. package/scripts/bench/round-r7-full.json +254 -0
  44. package/scripts/bench/round-r8-fulldefault.json +255 -0
  45. package/scripts/bench-run.mjs +251 -32
  46. package/scripts/freevar-smoke.mjs +95 -0
  47. package/scripts/internal-comms-bench.mjs +3 -4
  48. package/scripts/internal-comms-smoke.mjs +10 -9
  49. package/scripts/model-catalog-audit.mjs +209 -0
  50. package/scripts/model-list-sanitize-test.mjs +37 -0
  51. package/scripts/mouse-probe.mjs +45 -0
  52. package/scripts/output-style-bench.mjs +13 -6
  53. package/scripts/output-style-smoke.mjs +4 -4
  54. package/scripts/provider-toolcall-test.mjs +7 -3
  55. package/scripts/recall-bench.mjs +76 -13
  56. package/scripts/recall-quality-cases.json +12 -0
  57. package/scripts/recall-usecase-cases.json +18 -0
  58. package/scripts/session-bench.mjs +152 -6
  59. package/scripts/tool-smoke.mjs +25 -65
  60. package/scripts/tui-render-smoke.mjs +90 -0
  61. package/scripts/webhook-smoke.mjs +208 -0
  62. package/src/agents/debugger/AGENT.md +4 -1
  63. package/src/agents/heavy-worker/AGENT.md +9 -8
  64. package/src/agents/maintainer/AGENT.md +4 -0
  65. package/src/agents/reviewer/AGENT.md +2 -1
  66. package/src/agents/scheduler-task/AGENT.md +2 -3
  67. package/src/agents/webhook-handler/AGENT.md +2 -3
  68. package/src/agents/worker/AGENT.md +10 -7
  69. package/src/app.mjs +12 -1
  70. package/src/headless-role.mjs +7 -1
  71. package/src/lib/rules-builder.cjs +4 -0
  72. package/src/mixdog-session-runtime.mjs +647 -2056
  73. package/src/output-styles/default.md +30 -9
  74. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  75. package/src/output-styles/minimal.md +8 -6
  76. package/src/output-styles/simple.md +21 -7
  77. package/src/rules/agent/00-common.md +6 -3
  78. package/src/rules/agent/30-explorer.md +16 -5
  79. package/src/rules/lead/01-general.md +5 -5
  80. package/src/rules/lead/lead-brief.md +15 -0
  81. package/src/rules/lead/lead-tool.md +6 -15
  82. package/src/rules/shared/01-tool.md +17 -21
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
  84. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  85. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  86. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  87. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  88. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  89. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  90. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  91. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  92. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  93. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  95. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  97. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  100. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  101. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  102. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  103. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
  104. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  109. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  110. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
  111. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  112. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
  113. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
  114. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
  115. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  116. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
  117. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  119. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
  120. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  121. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  122. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  123. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  124. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  125. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  126. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  127. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  128. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  129. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  130. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
  132. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  133. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  134. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  135. package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
  136. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  137. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  138. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  139. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  140. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  141. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  142. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  143. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  144. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  145. package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
  146. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  147. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
  149. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  152. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  155. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  157. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  158. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  159. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  161. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  163. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  164. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  165. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  166. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  167. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  168. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  169. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  170. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  171. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  172. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  173. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  174. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  175. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  176. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  177. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  178. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  179. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  180. package/src/runtime/channels/backends/discord.mjs +27 -318
  181. package/src/runtime/channels/backends/telegram.mjs +8 -12
  182. package/src/runtime/channels/index.mjs +247 -701
  183. package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
  184. package/src/runtime/channels/lib/config.mjs +37 -149
  185. package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
  186. package/src/runtime/channels/lib/event-queue.mjs +78 -13
  187. package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
  188. package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
  189. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  190. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  191. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  192. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  193. package/src/runtime/channels/lib/scheduler.mjs +27 -113
  194. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  195. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  196. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  197. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  198. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  199. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  200. package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
  201. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  202. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  203. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  204. package/src/runtime/channels/lib/webhook.mjs +43 -616
  205. package/src/runtime/channels/tool-defs.mjs +11 -130
  206. package/src/runtime/memory/index.mjs +210 -1948
  207. package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
  208. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  209. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  210. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  211. package/src/runtime/memory/lib/ko-morph.mjs +195 -0
  212. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  213. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  214. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  215. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  216. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  217. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  218. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  219. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  220. package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
  221. package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
  222. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  223. package/src/runtime/memory/lib/query-handlers.mjs +802 -0
  224. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  225. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  226. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  227. package/src/runtime/memory/tool-defs.mjs +5 -13
  228. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  229. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  230. package/src/runtime/search/lib/web-tools.mjs +24 -602
  231. package/src/runtime/shared/atomic-file.mjs +26 -1
  232. package/src/runtime/shared/config.mjs +14 -4
  233. package/src/runtime/shared/launcher-control.mjs +2 -2
  234. package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
  235. package/src/runtime/shared/schedules-store.mjs +13 -3
  236. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  237. package/src/runtime/shared/tool-primitives.mjs +308 -0
  238. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  239. package/src/runtime/shared/tool-surface.mjs +80 -898
  240. package/src/runtime/shared/transcript-writer.mjs +23 -0
  241. package/src/runtime/shared/update-checker.mjs +7 -4
  242. package/src/session-runtime/config-helpers.mjs +119 -2
  243. package/src/session-runtime/config-lifecycle.mjs +232 -0
  244. package/src/session-runtime/cwd-plugins.mjs +226 -0
  245. package/src/session-runtime/mcp-glue.mjs +177 -0
  246. package/src/session-runtime/model-recency.mjs +111 -0
  247. package/src/session-runtime/native-search.mjs +247 -0
  248. package/src/session-runtime/output-styles.mjs +11 -9
  249. package/src/session-runtime/prewarm.mjs +142 -0
  250. package/src/session-runtime/provider-models.mjs +278 -0
  251. package/src/session-runtime/provider-usage.mjs +120 -0
  252. package/src/session-runtime/quick-model-rows.mjs +205 -0
  253. package/src/session-runtime/quick-search-models.mjs +47 -0
  254. package/src/session-runtime/session-hooks.mjs +93 -0
  255. package/src/session-runtime/settings-api.mjs +352 -0
  256. package/src/session-runtime/tool-catalog.mjs +29 -29
  257. package/src/session-runtime/tool-defs.mjs +84 -0
  258. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  259. package/src/session-runtime/workflow.mjs +1 -1
  260. package/src/standalone/agent-tool/helpers.mjs +237 -0
  261. package/src/standalone/agent-tool/notify.mjs +107 -0
  262. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  263. package/src/standalone/agent-tool/render.mjs +152 -0
  264. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  265. package/src/standalone/agent-tool.mjs +138 -669
  266. package/src/standalone/channel-admin.mjs +102 -90
  267. package/src/standalone/channel-worker.mjs +4 -7
  268. package/src/standalone/explore-tool.mjs +64 -14
  269. package/src/standalone/hook-bus/config.mjs +207 -0
  270. package/src/standalone/hook-bus/constants.mjs +90 -0
  271. package/src/standalone/hook-bus/handlers.mjs +481 -0
  272. package/src/standalone/hook-bus/payload.mjs +31 -0
  273. package/src/standalone/hook-bus/rules.mjs +77 -0
  274. package/src/standalone/hook-bus.mjs +77 -870
  275. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  276. package/src/standalone/opencode-go-login.mjs +5 -1
  277. package/src/standalone/provider-admin.mjs +1 -16
  278. package/src/standalone/usage-dashboard.mjs +3 -1
  279. package/src/tui/App.jsx +1059 -8110
  280. package/src/tui/app/app-format.mjs +213 -0
  281. package/src/tui/app/channel-pickers.mjs +508 -0
  282. package/src/tui/app/clipboard.mjs +67 -0
  283. package/src/tui/app/core-memory-picker.mjs +210 -0
  284. package/src/tui/app/extension-pickers.mjs +506 -0
  285. package/src/tui/app/input-parsers.mjs +193 -0
  286. package/src/tui/app/maintenance-pickers.mjs +356 -0
  287. package/src/tui/app/model-options.mjs +334 -0
  288. package/src/tui/app/model-picker.mjs +365 -0
  289. package/src/tui/app/onboarding-steps.mjs +400 -0
  290. package/src/tui/app/project-picker.mjs +247 -0
  291. package/src/tui/app/provider-setup-picker.mjs +580 -0
  292. package/src/tui/app/resume-picker.mjs +55 -0
  293. package/src/tui/app/route-pickers.mjs +419 -0
  294. package/src/tui/app/settings-picker.mjs +489 -0
  295. package/src/tui/app/slash-commands.mjs +101 -0
  296. package/src/tui/app/slash-dispatch.mjs +427 -0
  297. package/src/tui/app/text-layout.mjs +46 -0
  298. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  299. package/src/tui/app/transcript-window.mjs +677 -0
  300. package/src/tui/app/use-mouse-input.mjs +460 -0
  301. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  302. package/src/tui/app/use-transcript-scroll.mjs +512 -0
  303. package/src/tui/app/use-transcript-window.mjs +607 -0
  304. package/src/tui/components/ConfirmBar.jsx +10 -7
  305. package/src/tui/components/Picker.jsx +64 -15
  306. package/src/tui/components/PromptInput.jsx +33 -102
  307. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  308. package/src/tui/components/StatusLine.jsx +69 -15
  309. package/src/tui/components/TextEntryPanel.jsx +11 -0
  310. package/src/tui/components/ToolExecution.jsx +52 -594
  311. package/src/tui/components/TranscriptItem.jsx +105 -0
  312. package/src/tui/components/UsagePanel.jsx +18 -4
  313. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  314. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  315. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  316. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  317. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  318. package/src/tui/display-width.mjs +20 -3
  319. package/src/tui/dist/index.mjs +13553 -12384
  320. package/src/tui/engine/agent-job-feed.mjs +133 -0
  321. package/src/tui/engine/notification-plan.mjs +76 -0
  322. package/src/tui/engine/render-timing.mjs +17 -0
  323. package/src/tui/engine/tool-approval.mjs +94 -0
  324. package/src/tui/engine/tool-card-results.mjs +234 -0
  325. package/src/tui/engine/tool-result-status.mjs +135 -0
  326. package/src/tui/engine.mjs +170 -574
  327. package/src/tui/figures.mjs +5 -0
  328. package/src/tui/index.jsx +65 -1
  329. package/src/tui/input-editing.mjs +2 -2
  330. package/src/tui/markdown/format-token.mjs +4 -1
  331. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  332. package/src/tui/theme.mjs +6 -0
  333. package/src/ui/statusline-agents.mjs +213 -0
  334. package/src/ui/statusline-format.mjs +146 -0
  335. package/src/ui/statusline-segments.mjs +148 -0
  336. package/src/ui/statusline.mjs +77 -501
  337. package/src/ui/tool-card.mjs +0 -1
  338. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  339. package/src/workflows/default/WORKFLOW.md +16 -18
  340. package/src/workflows/sequential/WORKFLOW.md +16 -18
  341. package/vendor/ink/build/display-width.js +19 -3
  342. package/vendor/ink/build/ink.js +112 -7
  343. package/vendor/ink/build/log-update.js +17 -3
  344. package/vendor/ink/build/wrap-text.js +125 -0
  345. package/scripts/_test-folder-dialog.mjs +0 -30
  346. package/scripts/fix-brief-fn.mjs +0 -35
  347. package/scripts/fix-format-tool-surface.mjs +0 -24
  348. package/scripts/fix-tool-exec-visible.mjs +0 -42
  349. package/scripts/patch-agent-brief.mjs +0 -48
  350. package/scripts/patch-app.mjs +0 -21
  351. package/scripts/patch-app2.mjs +0 -18
  352. package/scripts/patch-dist-brief.mjs +0 -96
  353. package/scripts/patch-tool-exec.mjs +0 -70
  354. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  355. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  356. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  357. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  358. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  359. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  360. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  361. package/src/runtime/channels/lib/holidays.mjs +0 -138
  362. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  363. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  364. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  365. package/src/standalone/agent-task-status.test.mjs +0 -76
  366. package/src/tui/components/tool-output-format.test.mjs +0 -399
  367. package/src/tui/display-width.test.mjs +0 -35
  368. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  369. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  370. package/src/tui/input-editing.selection.test.mjs +0 -75
  371. package/src/tui/markdown/format-token.test.mjs +0 -354
  372. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  373. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  374. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  375. package/src/tui/paste-fix.test.mjs +0 -119
  376. package/src/tui/prompt-history-store.test.mjs +0 -52
  377. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  378. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  379. package/src/ui/markdown.test.mjs +0 -70
  380. package/src/ui/statusline-context-label.test.mjs +0 -15
  381. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  382. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -68,27 +68,67 @@ function textOfResult(result) {
68
68
  }
69
69
 
70
70
  function countEntryLines(text) {
71
- const t = String(text || '').trim();
72
- if (!t || t === '(no results)' || t === '(no valid ids)') return 0;
73
- return t.split('\n').filter((line) => line.trim() && !line.startsWith('[recall truncated') && !line.startsWith('note:')).length;
71
+ return resultLines(text).length;
74
72
  }
75
73
 
76
74
  function topN(text, n = 3, maxLen = 140) {
77
- const t = String(text || '').trim();
78
- if (!t || t === '(no results)') return [];
79
- return t.split('\n')
80
- .filter((line) => line.trim() && !line.startsWith('[recall truncated') && !line.startsWith('note:'))
75
+ return resultLines(text)
81
76
  .slice(0, n)
82
77
  .map((line) => (line.length > maxLen ? `${line.slice(0, maxLen - 1)}…` : line));
83
78
  }
84
79
 
85
- function evaluateCase(kase, { count, ms, isError }) {
80
+ // Full ordered list of result lines (no slice), used for topNContains rank
81
+ // scoring. Same filtering rule as countEntryLines/topN.
82
+ function resultLines(text) {
83
+ const t = String(text || '').trim();
84
+ if (!t || t === '(no results)' || t === '(no valid ids)') return [];
85
+ return t.split('\n').filter((line) => {
86
+ const trimmed = line.trim();
87
+ return trimmed
88
+ && !trimmed.startsWith('[recall truncated')
89
+ && !trimmed.startsWith('note:')
90
+ && !/^\[[^\]]+\]$/.test(trimmed);
91
+ });
92
+ }
93
+
94
+ // Score expect.topNContains: for each expected substring, find the 1-indexed
95
+ // rank of the first result line containing it (case-insensitive), over the
96
+ // FULL result list (not just the topN cutoff) so we can tell "missed
97
+ // entirely" from "present but ranked below cutoff". hit@N/MRR are then
98
+ // computed against the topN cutoff.
99
+ function scoreTopNContains(lines, substrings, n) {
100
+ const lower = lines.map((l) => l.toLowerCase());
101
+ const perSubstring = substrings.map((needle) => {
102
+ const hay = String(needle || '').toLowerCase();
103
+ let rank = null;
104
+ for (let i = 0; i < lower.length; i++) {
105
+ if (hay && lower[i].includes(hay)) { rank = i + 1; break; }
106
+ }
107
+ const hit = rank !== null && rank <= n;
108
+ return { needle, rank, hit, rr: hit ? 1 / rank : 0 };
109
+ });
110
+ const total = perSubstring.length || 1;
111
+ const hitAtN = perSubstring.reduce((s, p) => s + (p.hit ? 1 : 0), 0) / total;
112
+ const mrr = perSubstring.reduce((s, p) => s + p.rr, 0) / total;
113
+ return { perSubstring, hitAtN, mrr, n };
114
+ }
115
+
116
+ function evaluateCase(kase, { count, ms, isError }, quality) {
86
117
  const warnings = [];
87
118
  if (isError) warnings.push('error result');
88
119
  if (ms > WARN_LATENCY_MS) warnings.push(`latency ${ms}ms > ${WARN_LATENCY_MS}ms`);
89
120
  if ((kase.expect === 'browse' || kase.expect === 'idlookup') && count === 0) {
90
121
  warnings.push('0 results for a browse/id-lookup case (expected data present)');
91
122
  }
123
+ if (quality) {
124
+ for (const p of quality.perSubstring) {
125
+ if (!p.hit) {
126
+ warnings.push(p.rank === null
127
+ ? `topNContains miss: "${p.needle}" not found in results`
128
+ : `topNContains miss: "${p.needle}" found at rank ${p.rank} > topN ${quality.n}`);
129
+ }
130
+ }
131
+ }
92
132
  const status = warnings.length ? 'WARN' : 'PASS';
93
133
  return { status, warnings };
94
134
  }
@@ -109,7 +149,14 @@ async function runCase(memoryModule, kase) {
109
149
  const ms = Date.now() - started;
110
150
  const text = textOfResult(result);
111
151
  const count = countEntryLines(text);
112
- const evalResult = evaluateCase(kase, { count, ms, isError });
152
+ // expect stays a plain string for legacy cases ('results'/'browse'/'idlookup').
153
+ // New quality cases use an object: { kind?, topNContains: [...], topN? }.
154
+ const expectObj = kase.expect && typeof kase.expect === 'object' ? kase.expect : null;
155
+ const expectKind = expectObj ? expectObj.kind : kase.expect;
156
+ const topNContains = expectObj && Array.isArray(expectObj.topNContains) ? expectObj.topNContains : null;
157
+ const cutoffN = expectObj && Number.isInteger(expectObj.topN) ? expectObj.topN : 5;
158
+ const quality = topNContains ? scoreTopNContains(resultLines(text), topNContains, cutoffN) : null;
159
+ const evalResult = evaluateCase({ ...kase, expect: expectKind }, { count, ms, isError }, quality);
113
160
  return {
114
161
  id: kase.id,
115
162
  label: kase.label,
@@ -119,6 +166,7 @@ async function runCase(memoryModule, kase) {
119
166
  isError,
120
167
  errMsg,
121
168
  top3: topN(text, 3),
169
+ quality,
122
170
  status: evalResult.status,
123
171
  warnings: evalResult.warnings,
124
172
  };
@@ -128,6 +176,12 @@ function printCase(row) {
128
176
  process.stdout.write(`\n[${row.status}] ${row.id} — ${row.label}\n`);
129
177
  process.stdout.write(` params: ${JSON.stringify(row.args)}\n`);
130
178
  process.stdout.write(` results: ${row.count} latency: ${row.ms}ms${row.isError ? ` ERROR: ${row.errMsg}` : ''}\n`);
179
+ if (row.quality) {
180
+ process.stdout.write(` hit@${row.quality.n}: ${row.quality.hitAtN.toFixed(2)} MRR: ${row.quality.mrr.toFixed(2)}\n`);
181
+ for (const p of row.quality.perSubstring) {
182
+ process.stdout.write(` substr "${p.needle}" -> rank ${p.rank ?? 'none'}${p.hit ? '' : ' (miss)'}\n`);
183
+ }
184
+ }
131
185
  if (row.top3.length) {
132
186
  for (const line of row.top3) process.stdout.write(` - ${line}\n`);
133
187
  } else {
@@ -138,18 +192,27 @@ function printCase(row) {
138
192
 
139
193
  function printSummary(rows) {
140
194
  process.stdout.write('\n=== recall-bench summary ===\n');
141
- const widths = { id: 18, results: 8, ms: 8, status: 6 };
195
+ const widths = { id: 18, results: 8, ms: 8, status: 6, hit: 9, mrr: 7 };
142
196
  process.stdout.write(
143
- `${'case'.padEnd(widths.id)}${'results'.padEnd(widths.results)}${'ms'.padEnd(widths.ms)}${'status'.padEnd(widths.status)}notes\n`,
197
+ `${'case'.padEnd(widths.id)}${'results'.padEnd(widths.results)}${'ms'.padEnd(widths.ms)}${'status'.padEnd(widths.status)}${'hit@N'.padEnd(widths.hit)}${'MRR'.padEnd(widths.mrr)}notes\n`,
144
198
  );
145
199
  for (const r of rows) {
200
+ const hitStr = r.quality ? r.quality.hitAtN.toFixed(2) : '-';
201
+ const mrrStr = r.quality ? r.quality.mrr.toFixed(2) : '-';
146
202
  process.stdout.write(
147
- `${String(r.id).padEnd(widths.id)}${String(r.count).padEnd(widths.results)}${String(r.ms).padEnd(widths.ms)}${String(r.status).padEnd(widths.status)}${r.warnings.join('; ')}\n`,
203
+ `${String(r.id).padEnd(widths.id)}${String(r.count).padEnd(widths.results)}${String(r.ms).padEnd(widths.ms)}${String(r.status).padEnd(widths.status)}${hitStr.padEnd(widths.hit)}${mrrStr.padEnd(widths.mrr)}${r.warnings.join('; ')}\n`,
148
204
  );
149
205
  }
150
206
  const warnCount = rows.filter((r) => r.status === 'WARN').length;
151
207
  const totalMs = rows.reduce((s, r) => s + r.ms, 0);
152
- process.stdout.write(`\ncases=${rows.length} pass=${rows.length - warnCount} warn=${warnCount} total_latency=${totalMs}ms\n`);
208
+ const qualityRows = rows.filter((r) => r.quality);
209
+ let aggLine = '';
210
+ if (qualityRows.length) {
211
+ const aggHit = qualityRows.reduce((s, r) => s + r.quality.hitAtN, 0) / qualityRows.length;
212
+ const aggMrr = qualityRows.reduce((s, r) => s + r.quality.mrr, 0) / qualityRows.length;
213
+ aggLine = ` agg_hit@N=${aggHit.toFixed(3)} agg_MRR=${aggMrr.toFixed(3)} (${qualityRows.length} scored cases)`;
214
+ }
215
+ process.stdout.write(`\ncases=${rows.length} pass=${rows.length - warnCount} warn=${warnCount} total_latency=${totalMs}ms${aggLine}\n`);
153
216
  }
154
217
 
155
218
  async function main() {
@@ -0,0 +1,12 @@
1
+ [
2
+ { "id": "q-kr-recall-phrase", "label": "Korean distinctive phrase: persistent memory", "args": { "query": "영속 메모리" }, "expect": { "kind": "results", "topNContains": ["영속 메모리"], "topN": 5 } },
3
+ { "id": "q-en-recall-pipeline", "label": "English distinctive phrase: recall fast-track builder", "args": { "query": "buildRecallFastTrackQuery" }, "expect": { "kind": "results", "topNContains": ["buildRecallFastTrackQuery"], "topN": 5 } },
4
+ { "id": "q-cycle1-drain", "label": "cycle1 ECONNRESET topic", "args": { "query": "cycle1 ECONNRESET" }, "expect": { "kind": "results", "topNContains": ["cycle1", "ECONNRESET"], "topN": 5 } },
5
+ { "id": "q-recall-general", "label": "bare recall keyword", "args": { "query": "recall" }, "expect": { "kind": "results", "topNContains": ["recall"], "topN": 5 } },
6
+ { "id": "q-uc4-topic-kr", "label": "UC4 topic query with Korean qualifier", "args": { "query": "recall period last 개선", "limit": 8 }, "expect": { "kind": "results", "topNContains": ["개선"], "topN": 5 } },
7
+ { "id": "q-recall-improve-kr", "label": "cache improvement topic", "args": { "query": "캐시 개선" }, "expect": { "kind": "results", "topNContains": ["개선"], "topN": 5 } },
8
+ { "id": "q-battle-balance", "label": "ProjectAA topic", "args": { "query": "ProjectAA" }, "expect": { "kind": "results", "topNContains": ["ProjectAA"], "topN": 5 } },
9
+ { "id": "q-fanout-recall-battle", "label": "fan-out array query cache+ProjectAA", "args": { "query": ["캐시 개선", "ProjectAA"], "limit": 5 }, "expect": { "kind": "results", "topNContains": ["개선", "ProjectAA"], "topN": 10 } },
10
+ { "id": "q-short-2tok-cycle", "label": "short 2-token cycle1 query", "args": { "query": "cycle1 drain", "limit": 8 }, "expect": { "kind": "results", "topNContains": ["cycle1"], "topN": 3 } },
11
+ { "id": "q-recall-scope-project", "label": "project-scoped recall query", "args": { "query": "recall", "cwd": "C:\\Project\\mixdog" , "limit": 10 }, "expect": { "kind": "results", "topNContains": ["recall"], "topN": 5 } }
12
+ ]
@@ -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
+ ]
@@ -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}`);
@@ -11,7 +11,7 @@ import { buildHeadlessSpawnArgs } from '../src/headless-role.mjs';
11
11
  import { createStandaloneChannelWorker } from '../src/standalone/channel-worker.mjs';
12
12
  import { OpenAIOAuthProvider, buildRequestBody, sendViaHttpSse } from '../src/runtime/agent/orchestrator/providers/openai-oauth.mjs';
13
13
  import { _test as _anthropicOAuthTest } from '../src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs';
14
- import { _logicalResponseItemMatch, _resolveOpenAiPromptCacheRatePolicy } from '../src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs';
14
+ import { _logicalResponseItemMatch } from '../src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs';
15
15
  import { _mergePendingMessageEntries, applyAskTerminalUsageTotals, closeSession, createSession, drainPendingMessages, enqueuePendingMessage, resumeSession } from '../src/runtime/agent/orchestrator/session/manager.mjs';
16
16
  import {
17
17
  contentHasImage,
@@ -137,46 +137,6 @@ function assertOk(name, result, pattern = null) {
137
137
  }
138
138
 
139
139
  {
140
- const fullPolicy = _resolveOpenAiPromptCacheRatePolicy({}, {
141
- mode: 'full',
142
- frameInputItems: 40,
143
- deltaTokens: 9000,
144
- hasPreviousResponseId: false,
145
- });
146
- assert(fullPolicy.policy === 'full_guard' && fullPolicy.limitPerMin === 12, `full cache lane should keep 12rpm guard: ${JSON.stringify(fullPolicy)}`);
147
-
148
- const deltaPolicy = _resolveOpenAiPromptCacheRatePolicy({}, {
149
- mode: 'delta',
150
- frameInputItems: 1,
151
- deltaTokens: 9000,
152
- hasPreviousResponseId: true,
153
- });
154
- assert(deltaPolicy.policy === 'delta_relaxed' && deltaPolicy.limitPerMin === 60, `small delta should use relaxed cache lane rpm: ${JSON.stringify(deltaPolicy)}`);
155
-
156
- const largeDeltaPolicy = _resolveOpenAiPromptCacheRatePolicy({}, {
157
- mode: 'delta',
158
- frameInputItems: 20,
159
- deltaTokens: 9000,
160
- hasPreviousResponseId: true,
161
- });
162
- assert(largeDeltaPolicy.policy === 'delta_guarded' && largeDeltaPolicy.limitPerMin === 12, `large delta should fall back to full guard: ${JSON.stringify(largeDeltaPolicy)}`);
163
-
164
- const customDeltaPolicy = _resolveOpenAiPromptCacheRatePolicy({ openaiCacheLaneDeltaRateLimitPerMin: 90 }, {
165
- mode: 'delta',
166
- frameInputItems: 1,
167
- deltaTokens: 9000,
168
- hasPreviousResponseId: true,
169
- });
170
- assert(customDeltaPolicy.limitPerMin === 90, `custom delta rpm should be honored: ${JSON.stringify(customDeltaPolicy)}`);
171
-
172
- const unlimitedDeltaPolicy = _resolveOpenAiPromptCacheRatePolicy({ openaiCacheLaneDeltaRateLimitPerMin: 0 }, {
173
- mode: 'delta',
174
- frameInputItems: 1,
175
- deltaTokens: 9000,
176
- hasPreviousResponseId: true,
177
- });
178
- assert(unlimitedDeltaPolicy.policy === 'delta_unlimited' && unlimitedDeltaPolicy.limitPerMin === 0, `delta rpm=0 should disable delta rate wait: ${JSON.stringify(unlimitedDeltaPolicy)}`);
179
-
180
140
  const originalFunctionCall = {
181
141
  type: 'function_call',
182
142
  call_id: 'call_tool_1',
@@ -589,20 +549,28 @@ if (grepContextPolicyArgs['-C'] !== GREP_CONTEXT_MAX || Object.prototype.hasOwnP
589
549
  throw new Error(`grep context policy must canonicalize and clamp explicit context: ${JSON.stringify(grepContextPolicyArgs)}`);
590
550
  }
591
551
 
592
- const invalidGrepPath = validateBuiltinArgs('grep', {
552
+ // Multiple absolute paths in one string are now auto-split into a path array
553
+ // (arg-guard splitMultipleAbsoluteWindowsPaths) instead of rejected.
554
+ const multiGrepPathArgs = {
593
555
  pattern: 'providerStatus',
594
556
  path: 'C:\\Project\\mixdog\\src\\tui C:\\Project\\mixdog\\src\\mixdog-session-runtime.mjs',
595
- });
596
- if (!/multiple absolute paths/i.test(invalidGrepPath || '')) {
597
- throw new Error(`grep multi-path guard failed: ${invalidGrepPath}`);
557
+ };
558
+ const multiGrepPathErr = validateBuiltinArgs('grep', multiGrepPathArgs);
559
+ if (multiGrepPathErr) {
560
+ throw new Error(`grep multi-path auto-split should pass validation: ${multiGrepPathErr}`);
561
+ }
562
+ if (!Array.isArray(multiGrepPathArgs.path) || multiGrepPathArgs.path.length !== 2) {
563
+ throw new Error(`grep multi-path auto-split should coerce to 2-element array: ${JSON.stringify(multiGrepPathArgs.path)}`);
598
564
  }
599
565
 
600
- const invalidGrepLookaround = validateBuiltinArgs('grep', {
566
+ // Lookaround/backrefs are no longer rejected at validation time: search-tool
567
+ // routes them to rg --pcre2 at runtime (arg-guard.mjs comment near globKeys).
568
+ const lookaroundGrepErr = validateBuiltinArgs('grep', {
601
569
  pattern: 'C:\\\\Project(?!\\\\mixdog)',
602
570
  path: root,
603
571
  });
604
- if (!/lookaround\/backrefs/i.test(invalidGrepLookaround || '')) {
605
- throw new Error(`grep unsupported-regex guard failed: ${invalidGrepLookaround}`);
572
+ if (lookaroundGrepErr) {
573
+ throw new Error(`grep lookaround pattern should pass validation (PCRE2 runtime routing): ${lookaroundGrepErr}`);
606
574
  }
607
575
 
608
576
  // Windows drive path + no explicit shell used to be rejected with a retry hint;
@@ -991,10 +959,7 @@ setInternalToolsProvider({
991
959
  { name: 'recall', description: 'Memory recall surface.', inputSchema: { type: 'object', properties: {} }, annotations: { readOnlyHint: true } },
992
960
  { name: 'search', description: 'Web search surface.', inputSchema: { type: 'object', properties: {} }, annotations: { readOnlyHint: true, openWorldHint: true } },
993
961
  { name: 'reply', description: 'Channel reply surface.', inputSchema: { type: 'object', properties: {} }, annotations: { destructiveHint: true } },
994
- { name: 'edit_message', description: 'Channel edit surface.', inputSchema: { type: 'object', properties: {} }, annotations: { destructiveHint: true } },
995
962
  { name: 'web_fetch', description: 'Web fetch surface.', inputSchema: { type: 'object', properties: {} }, annotations: { readOnlyHint: true, openWorldHint: true } },
996
- { name: 'reload_config', description: 'Config reload surface.', inputSchema: { type: 'object', properties: {} }, annotations: { destructiveHint: true } },
997
- { name: 'inject_command', description: 'Command injection surface.', inputSchema: { type: 'object', properties: {} }, annotations: { destructiveHint: true } },
998
963
  ],
999
964
  });
1000
965
  {
@@ -1197,12 +1162,12 @@ setInternalToolsProvider({
1197
1162
  if (readTools.includes('shell') || writeTools.includes('shell')) {
1198
1163
  throw new Error(`read/read-write agent schemas must omit shell: read=${readTools.join(', ')} write=${writeTools.join(', ')}`);
1199
1164
  }
1200
- for (const name of ['shell', 'apply_patch', 'task', 'diagnostics', 'open_config']) {
1165
+ for (const name of ['shell', 'apply_patch', 'task']) {
1201
1166
  if (readTools.includes(name)) {
1202
1167
  throw new Error(`read agent schema must omit non-read tool ${name}: read=${readTools.join(', ')}`);
1203
1168
  }
1204
1169
  }
1205
- for (const name of ['apply_patch', 'task', 'diagnostics']) {
1170
+ for (const name of ['apply_patch', 'task']) {
1206
1171
  if (name === 'apply_patch' && !writeTools.includes(name)) {
1207
1172
  throw new Error(`read-write agent schema must preserve apply_patch: write=${writeTools.join(', ')}`);
1208
1173
  }
@@ -1210,12 +1175,7 @@ setInternalToolsProvider({
1210
1175
  throw new Error(`read-write agent schema must omit non-edit tool ${name}: write=${writeTools.join(', ')}`);
1211
1176
  }
1212
1177
  }
1213
- for (const name of ['open_config']) {
1214
- if (writeTools.includes(name)) {
1215
- throw new Error(`read-write agent schema must omit config/skill tool ${name}: write=${writeTools.join(', ')}`);
1216
- }
1217
- }
1218
- for (const name of ['memory', 'recall', 'reply', 'edit_message', 'reload_config', 'inject_command']) {
1178
+ for (const name of ['memory', 'recall', 'reply']) {
1219
1179
  if (readTools.includes(name) || writeTools.includes(name)) {
1220
1180
  throw new Error(`read/read-write agent schema must not expose full-runtime internal tool ${name}: read=${readTools.join(', ')} write=${writeTools.join(', ')}`);
1221
1181
  }
@@ -1293,7 +1253,7 @@ setInternalToolsProvider({
1293
1253
  const hiddenAgents = JSON.parse(readFileSync(join(root, 'src', 'defaults', 'agents.json'), 'utf8')).agents || [];
1294
1254
  const hiddenPreset = { id: 'hidden-smoke', name: 'hidden-smoke', type: 'agent', provider: 'openai-oauth', model: 'tool-smoke-model', tools: 'full' };
1295
1255
  const hiddenRuntimeSpec = { scopeKey: 'hidden-role-smoke', lane: 'agent' };
1296
- const hiddenBadTools = new Set(['shell', 'task', 'diagnostics', 'open_config', 'Skill', 'memory', 'reply', 'edit_message', 'recall', 'reload_config', 'inject_command']);
1256
+ const hiddenBadTools = new Set(['shell', 'task', 'Skill', 'memory', 'reply', 'recall']);
1297
1257
  const expectedForHiddenAgent = (permission, schemaAllowedTools) => {
1298
1258
  if (Array.isArray(schemaAllowedTools)) return schemaAllowedTools.slice();
1299
1259
  if (permission === 'none') return [];
@@ -1517,13 +1477,13 @@ if (!/before text grep/i.test(codeGraphDescription)) {
1517
1477
  if (!/repo-local/i.test(codeGraphDescription) || !/NOT web search|not web/i.test(codeGraphDescription)) {
1518
1478
  throw new Error('code_graph description must mark itself repo-local (not web search)');
1519
1479
  }
1520
- if (!/repo-local|not web/i.test(codeGraphProps.mode?.description || '') || !/source file/i.test(codeGraphProps.file?.description || '')) {
1480
+ if (!/repo-local|not web/i.test(codeGraphProps.mode?.description || '') || !/source file/i.test(codeGraphProps.files?.description || '')) {
1521
1481
  throw new Error('code_graph schema must keep compact, repo-local field descriptions');
1522
1482
  }
1523
1483
  const recallTool = MEMORY_TOOL_DEFS.find((tool) => tool.name === 'recall');
1524
1484
  const recallProps = recallTool?.inputSchema?.properties || {};
1525
- if (!/when in doubt, recall first/i.test(recallTool?.description || '') || !recallProps.id?.anyOf || !/Do not invent ids/i.test(recallProps.id?.description || '')) {
1526
- throw new Error('recall schema must preserve prior-context guidance and id lookup shape');
1485
+ if (!/Use only to check prior conversation or past events/i.test(recallTool?.description || '') || !recallProps.id?.anyOf || !/Do not invent ids/i.test(recallProps.id?.description || '')) {
1486
+ throw new Error('recall schema must preserve scoped prior-context guidance and id lookup shape');
1527
1487
  }
1528
1488
  if (!/array for independent fan-out/i.test(recallProps.query?.description || '') || !/Project pool selector/i.test(recallProps.projectScope?.description || '')) {
1529
1489
  throw new Error('recall schema must explain fan-out query and project scope filters');
@@ -1588,7 +1548,7 @@ if (!recallProps.sessionOnly || !/session only/i.test(recallProps.sessionOnly?.d
1588
1548
  }
1589
1549
  const memoryTool = MEMORY_TOOL_DEFS.find((tool) => tool.name === 'memory');
1590
1550
  const memoryProps = memoryTool?.inputSchema?.properties || {};
1591
- if (!/explicit mutation/i.test(memoryTool?.description || '') || !/Destructive jobs require exact confirm/i.test(memoryTool?.description || '') || !/Exact confirmation phrase/i.test(memoryProps.confirm?.description || '')) {
1551
+ if (!/mutation/i.test(memoryTool?.description || '') || !/Exact confirmation phrase/i.test(memoryProps.confirm?.description || '')) {
1592
1552
  throw new Error('memory schema must preserve mutation/destructive confirmation guidance');
1593
1553
  }
1594
1554
  const searchTool = SEARCH_TOOL_DEFS.find((tool) => tool.name === 'search');
@@ -1783,7 +1743,7 @@ if (grepTool?.inputSchema?.properties?.head_limit?.minimum !== 0 || !/keep small
1783
1743
  if (grepTool?.inputSchema?.properties?.type) {
1784
1744
  throw new Error('grep type schema must stay hidden; prefer glob for extension narrowing');
1785
1745
  }
1786
- if (!/code flow before text search/i.test(codeGraphProps.mode?.description || '') || !/one anchor is enough/i.test(codeGraphProps.symbol?.description || '') || !/one call/i.test(codeGraphProps.symbols?.description || '')) {
1746
+ if (!/code flow before text search/i.test(codeGraphProps.mode?.description || '') || !/one anchor is enough/i.test(codeGraphProps.symbols?.description || '') || !/one call/i.test(codeGraphProps.symbols?.description || '')) {
1787
1747
  throw new Error('code_graph schema fields must steer away from repeated lookup loops');
1788
1748
  }
1789
1749