mixdog 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +344 -0
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1223 -95
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2325 -450
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +118 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -17,14 +17,23 @@ export const BUILTIN_TOOLS = [
17
17
  name: 'read',
18
18
  title: 'Mixdog Read',
19
19
  annotations: { title: 'Mixdog Read', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false },
20
- description: 'Read a specific file window or symbol body after narrowing the target. Do not reread content already shown by code_graph or grep.',
20
+ description: 'Read known file path(s). Use line+context for small windows. Batch paths.',
21
21
  inputSchema: {
22
22
  type: 'object',
23
23
  properties: {
24
- path: { type: 'string', description: 'File path only; dirs use list.' },
25
- line: { type: 'number', minimum: 1, description: 'Anchor line. Do not combine with offset/limit.' },
26
- context: { type: 'number', minimum: 0, maximum: 200, description: 'Lines around line. Use only with line; max 200.' },
27
- symbol: { type: 'string', description: 'Symbol body via code graph.' },
24
+ path: {
25
+ anyOf: [
26
+ { type: 'string' },
27
+ {
28
+ type: 'array',
29
+ items: { type: 'string' },
30
+ minItems: 1,
31
+ },
32
+ ],
33
+ description: 'File path or array. Dirs use list.',
34
+ },
35
+ line: { type: 'number', minimum: 1, description: 'Anchor line.' },
36
+ context: { type: 'number', minimum: 0, maximum: 200, description: 'Lines around line; max 200.' },
28
37
  },
29
38
  },
30
39
  },
@@ -32,11 +41,11 @@ export const BUILTIN_TOOLS = [
32
41
  name: 'diagnostics',
33
42
  title: 'Mixdog Diagnostics',
34
43
  annotations: { title: 'Mixdog Diagnostics', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true, compressible: true },
35
- description: 'Run the matching type/lint checker under a file or directory (tsc/eslint/ruff/etc.). Default cwd; no LSP.',
44
+ description: 'Run matching type/lint checker. Default cwd; no LSP.',
36
45
  inputSchema: {
37
46
  type: 'object',
38
47
  properties: {
39
- path: { type: 'string', description: 'File or directory to diagnose. Defaults to cwd.' },
48
+ path: { type: 'string', description: 'File/dir. Default cwd.' },
40
49
  },
41
50
  required: [],
42
51
  },
@@ -45,29 +54,29 @@ export const BUILTIN_TOOLS = [
45
54
  name: 'open_config',
46
55
  title: 'Open Config UI',
47
56
  annotations: { title: 'Open Config UI', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
48
- description: 'Open the mixdog settings UI (Providers + Presets) in the browser. Starts the resident config server if needed and returns the UI URL. No params.',
57
+ description: 'Open settings UI; returns URL. No params.',
49
58
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
50
59
  },
51
60
  {
52
61
  name: 'shell',
53
62
  title: 'Mixdog Shell',
54
63
  annotations: { title: 'Mixdog Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
55
- description: `Run an OS shell command for git/build/test/run only. Do NOT use shell grep/cat/head for source; use code_graph/grep/read/list/glob. Set shell explicitly: 'powershell' for PowerShell or 'bash' for Git Bash/POSIX. Use cwd; omit empty workdir. Windows native shell does not support persistent/session_id. mode=async uses the shared background task system and returns task_id. ${TOOL_ASYNC_EXECUTION_CONTRACT}`,
64
+ description: `Run shell for git/build/test/run. Not for source browsing; use code_graph/grep/read/list/glob. Set shell: powershell or bash. ${TOOL_ASYNC_EXECUTION_CONTRACT}`,
56
65
  inputSchema: {
57
66
  type: 'object',
58
67
  properties: {
59
- command: { type: 'string', description: 'Shell command to run.' },
60
- cwd: { type: 'string', description: 'Working directory for the command; omit when using current cwd.' },
68
+ command: { type: 'string', description: 'Command.' },
69
+ cwd: { type: 'string', description: 'Working directory.' },
61
70
  workdir: { type: 'string', description: 'Alias for cwd; omit if empty.' },
62
- timeout: { type: 'number', description: 'Timeout in milliseconds.' },
63
- merge_stderr: { type: 'boolean', description: 'Merge stderr into stdout in the returned output.' },
71
+ timeout: { type: 'number', description: 'Timeout ms.' },
72
+ merge_stderr: { type: 'boolean', description: 'Merge stderr.' },
64
73
  mode: { type: 'string', enum: ['sync', 'async'], description: executionModeSchemaDescription('sync') },
65
74
  run_in_background: { type: 'boolean', description: `Legacy alias for mode=async. ${TOOL_ASYNC_EXECUTION_CONTRACT}` },
66
- persistent: { type: 'boolean', description: 'Persistent shell mode; unsupported on Windows native shell.' },
67
- session_id: { type: 'string', description: 'Persistent shell id; omit on Windows native shell.' },
68
- create: { type: 'boolean', description: 'Allow creating a new persistent session for an explicit session_id.' },
69
- close: { type: 'boolean', description: 'Close the persistent session after this command when supported.' },
70
- shell: { type: 'string', enum: ['bash', 'powershell'], description: "Force the shell. On Windows: 'bash' runs the command through Git Bash (POSIX syntax), 'powershell' forces PowerShell. On POSIX: 'powershell' resolves pwsh if installed (errors if absent); 'bash' is /bin/sh (already the default). Always set this explicitly; omitting uses the OS default (PowerShell on Windows), where POSIX syntax fails to parse." },
75
+ persistent: { type: 'boolean', description: 'Persistent shell mode.' },
76
+ session_id: { type: 'string', description: 'Persistent shell id.' },
77
+ create: { type: 'boolean', description: 'Create session_id if missing.' },
78
+ close: { type: 'boolean', description: 'Close persistent session.' },
79
+ shell: { type: 'string', enum: ['bash', 'powershell'], description: 'Force shell. Windows default is PowerShell; bash means Git Bash/POSIX.' },
71
80
  },
72
81
  required: ['command'],
73
82
  },
@@ -76,14 +85,14 @@ export const BUILTIN_TOOLS = [
76
85
  name: 'task',
77
86
  title: 'Background Task Control',
78
87
  annotations: { title: 'Background Task Control', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
79
- description: `Manual control for an async background task by task_id. action: list | status | read | wait | cancel. ${TOOL_MANUAL_CONTROL_CONTRACT} Not for bridge_*/sess_* ids.`,
88
+ description: `Manual control for shell async task_id. actions: list/status/read/wait/cancel. ${TOOL_MANUAL_CONTROL_CONTRACT} Not sess_* or agent ids.`,
80
89
  inputSchema: {
81
90
  type: 'object',
82
91
  properties: {
83
- task_id: { type: 'string', description: 'task_id from shell mode=async. Use only for manual control; normal completion arrives as a notification.' },
84
- action: { type: 'string', enum: ['list', 'status', 'read', 'wait', 'cancel'], description: 'Manual control only: list all visible tasks; status is non-blocking; read includes available result/output; wait blocks inside this one call until done/timeout; cancel terminates when the task supports it. Do not call repeatedly as a polling loop.' },
85
- timeout_ms: { type: 'number', description: 'Maximum time for a single manual wait call. Prefer the completion notification instead of repeated waits.' },
86
- poll_ms: { type: 'number', description: 'Internal polling interval used only inside one manual wait call.' },
92
+ task_id: { type: 'string', description: 'shell async task_id.' },
93
+ action: { type: 'string', enum: ['list', 'status', 'read', 'wait', 'cancel'], description: 'Manual action. Avoid polling loops.' },
94
+ timeout_ms: { type: 'number', description: 'Wait timeout ms.' },
95
+ poll_ms: { type: 'number', description: 'Wait poll ms.' },
87
96
  },
88
97
  required: [],
89
98
  },
@@ -92,7 +101,7 @@ export const BUILTIN_TOOLS = [
92
101
  name: 'grep',
93
102
  title: 'Mixdog Grep',
94
103
  annotations: { title: 'Mixdog Grep', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
95
- description: 'Search text or regex within a known file or directory.',
104
+ description: 'Search text/logs/regex.',
96
105
  inputSchema: {
97
106
  type: 'object',
98
107
  properties: {
@@ -101,19 +110,19 @@ export const BUILTIN_TOOLS = [
101
110
  { type: 'string' },
102
111
  { type: 'array', items: { type: 'string' }, minItems: 1 },
103
112
  ],
104
- description: 'Literal/substring or ripgrep regex. Array = OR terms. Legacy `\\|` in a single string is accepted as OR.',
113
+ description: 'Text/regex. Array = OR.',
105
114
  },
106
- path: { type: 'string', description: 'One file or directory only; for many paths use common parent + glob.' },
115
+ path: { type: 'string', description: 'File or directory.' },
107
116
  glob: {
108
117
  anyOf: [
109
118
  { type: 'string' },
110
119
  { type: 'array', items: { type: 'string' }, minItems: 1 },
111
120
  ],
112
- description: 'Glob filter(s).',
121
+ description: 'Optional narrowing glob(s).',
113
122
  },
114
- output_mode: { type: 'string', enum: ['files_with_matches', 'content', 'count'], description: 'Default content.' },
115
- head_limit: { type: 'number', description: 'Max result lines.' },
116
- offset: { type: 'number', description: 'Skip N result lines for paging.' },
123
+ output_mode: { type: 'string', enum: ['files_with_matches', 'content', 'count'], description: 'Output mode.' },
124
+ head_limit: { type: 'number', description: 'Max lines.' },
125
+ offset: { type: 'number', description: 'Skip lines.' },
117
126
  },
118
127
  required: [],
119
128
  },
@@ -122,7 +131,7 @@ export const BUILTIN_TOOLS = [
122
131
  name: 'glob',
123
132
  title: 'Mixdog Glob',
124
133
  annotations: { title: 'Mixdog Glob', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
125
- description: 'Find files by glob pattern. Use for broad file-space narrowing.',
134
+ description: 'Find files by glob.',
126
135
  inputSchema: {
127
136
  type: 'object',
128
137
  properties: {
@@ -131,33 +140,47 @@ export const BUILTIN_TOOLS = [
131
140
  { type: 'string' },
132
141
  { type: 'array', items: { type: 'string' }, minItems: 1 },
133
142
  ],
134
- description: 'Glob pattern, or array of patterns.',
143
+ description: 'Glob pattern(s).',
135
144
  },
136
145
  path: {
137
146
  anyOf: [
138
147
  { type: 'string' },
139
148
  { type: 'array', items: { type: 'string' }, minItems: 1 },
140
149
  ],
141
- description: 'Base directory, or array of base directories.',
150
+ description: 'Base directory/directories.',
142
151
  },
143
152
  head_limit: { type: 'number', description: 'Max entries.' },
144
- offset: { type: 'number', description: 'Skip N entries for paging.' },
153
+ offset: { type: 'number', description: 'Skip entries.' },
145
154
  },
146
155
  required: ['pattern'],
147
156
  },
148
157
  },
158
+ {
159
+ name: 'find',
160
+ title: 'Mixdog Find Files',
161
+ annotations: { title: 'Mixdog Find Files', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
162
+ description: 'Fuzzy-find files by partial path/name. Returns paths.',
163
+ inputSchema: {
164
+ type: 'object',
165
+ properties: {
166
+ query: { type: 'string', description: 'Partial path/name words.' },
167
+ path: { type: 'string', description: 'Base directory.' },
168
+ head_limit: { type: 'number', description: 'Max paths.' },
169
+ },
170
+ required: ['query'],
171
+ },
172
+ },
149
173
  {
150
174
  name: 'list',
151
175
  title: 'Mixdog List Directory',
152
176
  annotations: { title: 'Mixdog List Directory', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
153
- description: 'List directory entries or fuzzy-rank filenames. Use for broad directory/file discovery.',
177
+ description: 'List known directory entries. Use glob for broad discovery.',
154
178
  inputSchema: {
155
179
  type: 'object',
156
180
  properties: {
157
- path: { type: 'string', description: 'Directory to list; omit for current cwd.' },
181
+ path: { type: 'string', description: 'Directory.' },
158
182
  head_limit: { type: 'number', description: 'Max entries.' },
159
183
  offset: { type: 'number', description: 'Skip N entries for paging.' },
160
- fuzzy: { type: 'string', description: 'Rank files by partial name.' },
161
184
  },
162
185
  required: [],
163
186
  },
@@ -5,6 +5,7 @@ import { deleteReadRangeIndexForPath } from './read-range-index.mjs';
5
5
  import { resolveAgainstCwd } from './path-utils.mjs';
6
6
 
7
7
  const RESULT_CACHE = new Map(); // key → { ts, value, paths, scopes, readSnapshotMeta, contentPrefixHash, bytes }
8
+ const RESULT_CACHE_INFLIGHT = new Map(); // key → Promise<value>
8
9
  const RESULT_CACHE_TTL_MS = 30_000;
9
10
  const RESULT_CACHE_MAX_ENTRIES = 200;
10
11
  const RESULT_CACHE_MAX_BYTES = (() => {
@@ -140,6 +141,22 @@ export function cacheSet(key, value, meta = {}) {
140
141
  }
141
142
  }
142
143
 
144
+ export async function runResultCacheInFlight(key, compute) {
145
+ const cached = cacheGet(key);
146
+ if (cached !== null) return cached;
147
+ const existing = RESULT_CACHE_INFLIGHT.get(key);
148
+ if (existing) return await existing;
149
+ const promise = Promise.resolve()
150
+ .then(() => compute())
151
+ .finally(() => {
152
+ if (RESULT_CACHE_INFLIGHT.get(key) === promise) {
153
+ RESULT_CACHE_INFLIGHT.delete(key);
154
+ }
155
+ });
156
+ RESULT_CACHE_INFLIGHT.set(key, promise);
157
+ return await promise;
158
+ }
159
+
143
160
  function rawContentCacheDelete(key) {
144
161
  const entry = RAW_CONTENT_CACHE.get(key);
145
162
  if (entry?.rawBuf) RAW_CONTENT_CACHE_BYTES = Math.max(0, RAW_CONTENT_CACHE_BYTES - entry.rawBuf.length);
@@ -230,7 +247,7 @@ export async function statPathsForMtime(paths, workDir, concurrency = 64, opts =
230
247
  // Hard per-stat deadline (0 = disabled, legacy behaviour). A hung stat
231
248
  // (dead mount / unresponsive network path) must not pin a worker forever;
232
249
  // on expiry the entry resolves to null (stat-failed) so glob's post-rg stat
233
- // phase is bounded instead of running to the 600s bridge watchdog.
250
+ // phase is bounded instead of running to the 600s agent watchdog.
234
251
  const deadlineMs = Number(opts.deadlineMs) > 0 ? Number(opts.deadlineMs) : 0;
235
252
  // Injectable stat impl for testing hung-FS behaviour deterministically.
236
253
  const statImpl = typeof opts._statImpl === 'function' ? opts._statImpl : fsPromises.stat;
@@ -293,7 +310,7 @@ export async function lstatPathsForMtime(paths, workDir, concurrency = 64, opts
293
310
  // Hard per-lstat deadline (0 = disabled, legacy behaviour). A hung lstat
294
311
  // (dead mount / unresponsive network path) must not pin a worker forever;
295
312
  // on expiry the entry resolves to null (stat-failed) so list's stat phase
296
- // is bounded instead of running to the 600s bridge watchdog.
313
+ // is bounded instead of running to the 600s agent watchdog.
297
314
  const deadlineMs = Number(opts.deadlineMs) > 0 ? Number(opts.deadlineMs) : 0;
298
315
  // Injectable lstat impl for testing hung-FS behaviour deterministically.
299
316
  const lstatImpl = typeof opts._lstatImpl === 'function' ? opts._lstatImpl : fsPromises.lstat;
@@ -342,6 +359,7 @@ export async function lstatPathsForMtime(paths, workDir, concurrency = 64, opts
342
359
 
343
360
  function cacheInvalidateAll() {
344
361
  RESULT_CACHE.clear();
362
+ RESULT_CACHE_INFLIGHT.clear();
345
363
  RESULT_CACHE_BYTES = 0;
346
364
  STAT_CACHE.clear();
347
365
  RAW_CONTENT_CACHE.clear();
@@ -356,6 +374,7 @@ function cacheInvalidatePaths(paths) {
356
374
  cacheInvalidateAll();
357
375
  return;
358
376
  }
377
+ RESULT_CACHE_INFLIGHT.clear();
359
378
  for (const [key, entry] of RESULT_CACHE) {
360
379
  if (cacheEntryOverlapsPaths(entry, affectedPaths)) {
361
380
  resultCacheDelete(key);
@@ -1,4 +1,4 @@
1
- // G6: device path block list (Claude Code parity). Reading these paths
1
+ // G6: device path block list (reference parity). Reading these paths
2
2
  // would either hang (waiting for stdin / tty) or produce infinite output
3
3
  // (/dev/zero, /dev/random). The device block list catches pseudo-files on
4
4
  // POSIX hosts that a user-allowed path can still hit.
@@ -282,11 +282,4 @@ async function _executeDiagnosticsImpl(args, workDir) {
282
282
  }
283
283
  }
284
284
 
285
- function _isDir(p, workDir) {
286
- try {
287
- const abs = isAbsolute(p) ? p : pathResolve(workDir, p);
288
- return statSync(abs).isDirectory();
289
- } catch { return false; }
290
- }
291
-
292
285
  export default executeDiagnosticsTool;
@@ -118,12 +118,10 @@ export function compileSimpleGlob(pattern) {
118
118
  // simpler and matches the noise patterns developers actually create.
119
119
  export const NOISE_DIR_NAMES = new Set([
120
120
  'node_modules', '.git',
121
- 'dist', 'build', 'out', 'target',
122
121
  '.next', '.nuxt', '.svelte-kit',
123
122
  '.cache', '.parcel-cache', '.turbo',
124
123
  'venv', '.venv', '__pycache__', '.pytest_cache',
125
- '.gradle', 'coverage',
126
- 'refs',
124
+ '.gradle',
127
125
  ]);
128
126
 
129
127
  export const MAX_WALK_ENTRIES = 200_000;
@@ -60,7 +60,7 @@ function listGuardPath(p) {
60
60
 
61
61
  export async function executeListTool(args, workDir, options = {}) {
62
62
  if (typeof args.fuzzy === 'string' && args.fuzzy.length > 0) {
63
- return executeFuzzyFind(args, workDir);
63
+ return executeFuzzyFindTool({ ...args, query: args.fuzzy }, workDir, options);
64
64
  }
65
65
  if (args.mode === 'tree') return executeTreeTool(args, workDir, options);
66
66
  if (args.mode === 'find') return executeFindFilesTool(args, workDir, options);
@@ -296,12 +296,13 @@ export async function executeTreeTool(args, workDir, options = {}) {
296
296
  return out;
297
297
  }
298
298
 
299
- // Fuzzy filename search (codex file-search / nucleo style): collect the file
300
- // list via `rg --files`, then rank by subsequence score. Lets a partial name
301
- // like "edeng" surface matching files in one call instead of guessing an exact
302
- // glob pattern. Honors path/hidden/include_noise/depth/head_limit.
303
- async function executeFuzzyFind(args, workDir) {
304
- const query = String(args.fuzzy);
299
+ // Fuzzy filename search (nucleo-style): collect the file
300
+ // list via `rg --files`, then rank by subsequence score. `list.fuzzy` still
301
+ // routes here for hidden backward compatibility, but the model-facing tool is
302
+ // `find`.
303
+ export async function executeFuzzyFindTool(args, workDir, options = {}) {
304
+ const query = String(args.query ?? args.fuzzy ?? '').trim();
305
+ if (!query) return 'Error: find requires query.';
305
306
  const inputPath = normalizeInputPath(args.path) || '.';
306
307
  const guard = listGuardPath(inputPath);
307
308
  if (guard) return guard;
@@ -310,10 +311,25 @@ async function executeFuzzyFind(args, workDir) {
310
311
  if (guardFull) return guardFull;
311
312
  const hidden = Boolean(args.hidden);
312
313
  const includeNoise = Boolean(args.include_noise);
313
- // head_limit:0 means "no cap" per list semantics keep 0 distinct from default.
314
- const headLimit = normalizeListHeadLimit(args.head_limit, 40);
314
+ // head_limit:0 means "no cap" per list semantics; default is intentionally
315
+ // compact so ambiguous discovery does not dump a huge candidate list.
316
+ const headLimit = normalizeListHeadLimit(args.head_limit, 25);
315
317
  const depth = args.depth != null ? Math.max(parseInt(args.depth, 10) || 1, 1) : null;
316
- const rgArgs = ['--files', '--no-ignore'];
318
+ const cacheKey = buildListCacheKey({
319
+ mode: 'fuzzy_find',
320
+ inputPath: normalizeOutputPath(fullPath),
321
+ depth: depth ?? '',
322
+ hidden,
323
+ sort: 'score',
324
+ typeFilter: 'file',
325
+ headLimit,
326
+ offset: '',
327
+ namePattern: query,
328
+ includeNoise,
329
+ });
330
+ const cached = cacheGet(cacheKey);
331
+ if (cached !== null) return cached;
332
+ const rgArgs = ['--files'];
317
333
  if (hidden) rgArgs.push('--hidden');
318
334
  if (depth != null) rgArgs.push('--max-depth', String(depth));
319
335
  if (!includeNoise) {
@@ -333,12 +349,20 @@ async function executeFuzzyFind(args, workDir) {
333
349
  .map((p) => (p.endsWith('\r') ? p.slice(0, -1) : p))
334
350
  .filter((p) => p.length > 0)
335
351
  .map((p) => ({ path: normalizeOutputPath(p.replace(/^\.[/\\]/, '')) }));
336
- const ranked = fuzzyRank(query, items, headLimit);
352
+ const rankLimit = headLimit > 0 ? headLimit + 1 : headLimit;
353
+ const rankedRaw = fuzzyRank(query, items, rankLimit);
354
+ const hasMore = headLimit > 0 && rankedRaw.length > headLimit;
355
+ const ranked = hasMore ? rankedRaw.slice(0, headLimit) : rankedRaw;
337
356
  if (ranked.length === 0) return `(no fuzzy match for "${query}")`;
338
357
  const out = ranked.map((r) => r.item.path).join('\n');
339
- return headLimit > 0 && ranked.length >= headLimit
358
+ const result = hasMore
340
359
  ? `${out}\n... (top ${headLimit}; raise head_limit for more)`
341
360
  : out;
361
+ cacheSet(cacheKey, result, { scopes: [fullPath] });
362
+ if (typeof options?.onProgress === 'function') {
363
+ try { options.onProgress(`${ranked.length} candidates`); } catch { /* best-effort */ }
364
+ }
365
+ return result;
342
366
  }
343
367
 
344
368
  export async function executeFindFilesTool(args, workDir, options = {}) {
@@ -98,6 +98,8 @@ export function normaliseReadLineWindowArgs(inputArgs, workDir) {
98
98
  args.limit = limit;
99
99
  }
100
100
  }
101
+ delete args.line;
102
+ delete args.context;
101
103
  }
102
104
  return args;
103
105
  }
@@ -3,7 +3,7 @@ import { SMART_READ_MAX_BYTES } from './read-formatting.mjs';
3
3
  // Read tool caps.
4
4
  //
5
5
  // READ_MAX_SIZE_BYTES (10 MB) — fast-path file-size threshold mirroring
6
- // Claude Code's FAST_PATH_MAX_SIZE (readFileInRange.ts:44). Files at or below
6
+ // Reference FAST_PATH_MAX_SIZE (readFileInRange.ts:44). Files at or below
7
7
  // this size use readFile + in-memory split by default, which CC measured at
8
8
  // ~2x faster than createReadStream + readline for typical source. Explicit
9
9
  // offset/limit windows on files above READ_STREAM_RANGE_MIN_BYTES take the
@@ -14,7 +14,7 @@ import { SMART_READ_MAX_BYTES } from './read-formatting.mjs';
14
14
  //
15
15
  // READ_WHOLE_FILE_MAX_BYTES (256 KiB) — soft threshold: default whole-file
16
16
  // reads larger than this prefer stream smart-elide + READ_MAX_OUTPUT_BYTES
17
- // truncation (Codex-style proceed) rather than refusing. Hard in-memory cap
17
+ // truncation (proceed with cap) rather than refusing. Hard in-memory cap
18
18
  // for loading a full file remains READ_MAX_SIZE_BYTES (10 MiB).
19
19
  //
20
20
  // READ_MAX_OUTPUT_BYTES (30 KB) — output-truncation cap. Lead-facing default tightened from 50k. Mirrors CC's
@@ -12,7 +12,7 @@ export const SMART_READ_TAIL_LINES = 100;
12
12
  export const READ_CONTEXT_ADVISORY_BYTES = 40 * 1024;
13
13
  export const READ_MAX_RENDERED_LINE_CHARS = 2_000;
14
14
  // Claude-Code parity: the read line-prefix separator is `→` (the `→`
15
- // arrow), matching Claude Code's default cat -n format `<n>→<content>`. It
15
+ // arrow), matching default cat -n format `<n>→<content>`. It
16
16
  // MUST be a NON-WHITESPACE glyph: a tab/space separator collides with the
17
17
  // content's own leading indentation, so a model hand-reconstructing an edit
18
18
  // old_string cannot tell the separator from the indent and produces
@@ -42,22 +42,15 @@ function rangeFromRenderedReadRows(rows, fallbackStartLine = 1) {
42
42
  };
43
43
  }
44
44
 
45
- // Optional `budget` { maxLines, headLines, tailLines } lets a caller request a
46
- // TIGHTER head+tail elision than the 600-line / 200+100 default (e.g. read
47
- // budget:'compact' or max_lines:N) to bound lead-context cost. Omitted -> the
48
- // standard caps, so existing 4-arg callers are byte-for-byte unchanged.
49
- export function smartReadTruncate(renderedWithLineNos, totalLines, fileBytes, filePath = '', budget = null) {
50
- const maxLines = budget?.maxLines ?? SMART_READ_MAX_LINES;
51
- const headLines = budget?.headLines ?? SMART_READ_HEAD_LINES;
52
- const tailLines = budget?.tailLines ?? SMART_READ_TAIL_LINES;
45
+ export function smartReadTruncate(renderedWithLineNos, totalLines, fileBytes, filePath = '') {
53
46
  const overByBytes = fileBytes > SMART_READ_MAX_BYTES;
54
- const overByLines = totalLines > maxLines;
47
+ const overByLines = totalLines > SMART_READ_MAX_LINES;
55
48
  if (!overByBytes && !overByLines) {
56
49
  return { text: renderedWithLineNos, truncated: false, totalLines, ranges: null };
57
50
  }
58
51
  const rows = renderedWithLineNos.split('\n');
59
- const headCount = Math.min(headLines, rows.length);
60
- const tailStart = Math.max(headCount, rows.length - tailLines);
52
+ const headCount = Math.min(SMART_READ_HEAD_LINES, rows.length);
53
+ const tailStart = Math.max(headCount, rows.length - SMART_READ_TAIL_LINES);
61
54
  const elidedRows = tailStart - headCount;
62
55
  if (elidedRows <= 0) {
63
56
  return { text: renderedWithLineNos, truncated: false, totalLines, ranges: null };
@@ -1,4 +1,4 @@
1
- // Optional-sharp image resize / downsample helper. Mirrors Claude Code's
1
+ // Optional-sharp image resize / downsample helper. Mirrors reference
2
2
  // imageResizer.ts (maybeResizeAndDownsampleImageBuffer +
3
3
  // readImageWithTokenBudget) so a `read` on an image returns a viewable,
4
4
  // budget-bounded image block instead of refusing oversized originals.
@@ -12,23 +12,6 @@ function snapshotBodyWasReturnedByRead(snapshot) {
12
12
  return String(snapshot?.source || '').startsWith('read');
13
13
  }
14
14
 
15
- // Optional context-budget for a whole-file read: `max_lines:N` requests a
16
- // TIGHTER head+tail elision than the default 600-line / 200+100 cap, to bound
17
- // lead-context cost when only a glance is needed. Returns null (= default) when
18
- // unset. No heuristic guessing — the budget is explicit and reuses the existing
19
- // smartReadTruncate head/tail invariant. (`budget:'compact'` is a SEPARATE,
20
- // pre-existing knob handled upstream in read-tool.mjs applyCompactReadBudget —
21
- // it remaps a whole-file read to mode:'count' stats, not head+tail content.)
22
- function resolveReadBudget(args) {
23
- const ml = Number(args?.max_lines);
24
- if (Number.isFinite(ml) && ml > 0) {
25
- const maxLines = Math.trunc(ml);
26
- const headLines = Math.max(1, Math.ceil(maxLines * 0.7));
27
- return { maxLines, headLines, tailLines: Math.max(0, maxLines - headLines) };
28
- }
29
- return null;
30
- }
31
-
32
15
  function withSymbolReadNote(text, args) {
33
16
  const note = typeof args?._symbolReadNote === 'string' ? args._symbolReadNote.trim() : '';
34
17
  if (!note || typeof text !== 'string') return text;
@@ -159,9 +142,6 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
159
142
  // emits a compact truncation marker when rendered bytes overflow
160
143
  // READ_MAX_OUTPUT_BYTES.
161
144
  const limit = parseLineLimitArg(args.limit, wantFull ? Infinity : 2000);
162
- // Context-budget (compact / max_lines) — only meaningful on a whole-file
163
- // read (no range, not full). Ignored otherwise.
164
- const _readBudget = (!hasRangeArgs && !wantFull) ? resolveReadBudget(args) : null;
165
145
  let st;
166
146
  let _statErr;
167
147
  try {
@@ -265,7 +245,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
265
245
  }
266
246
  return _ipynbOut;
267
247
  }
268
- const cacheKey = `read|${fullPath}|${st.mtimeMs}|${st.size}|${hasOffsetArg ? offset : 'd'}|${hasLimitArg ? limit : 'd'}|${wantFull ? 'f' : 's'}|${_readBudget ? `b${_readBudget.maxLines}` : 'd'}`;
248
+ const cacheKey = `read|${fullPath}|${st.mtimeMs}|${st.size}|${hasOffsetArg ? offset : 'd'}|${hasLimitArg ? limit : 'd'}|${wantFull ? 'f' : 's'}`;
269
249
  // Race-guard helper: same-mtime same-size rapid rewrite (NTFS / exFAT 1 s
270
250
  // resolution) can pass mtimeMs+size yet differ in content. When the cache
271
251
  // entry stores a contentPrefixHash, recompute the current prefix and bail
@@ -358,7 +338,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
358
338
  _recordReadSnapshot(fullPath, st, readStateScope, cachedEntry.readSnapshotMeta || { source: 'read_cached' });
359
339
  // G6: file_unchanged stub. The full body is already in the
360
340
  // prior tool_result; resending it wastes cache_creation
361
- // tokens (Claude Code upstream measured ~18% on Read calls).
341
+ // tokens (reference upstream measured ~18% on Read calls).
362
342
  // The stub keeps the snapshot tracking intact (Edit
363
343
  // validation still works) while collapsing the response
364
344
  // payload. Falls back to the full body when the cached
@@ -496,19 +476,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
496
476
  ? await streamSmartReadSummary(fullPath, st, 'read_smart_stream')
497
477
  : null;
498
478
  if (_streamSmart?.text) {
499
- let out = _streamSmart.text;
500
- // Honor a compact/max_lines budget on a large file: the stream
501
- // already elided to head 200/tail 100; re-apply the tighter
502
- // head+tail so the lead sees only the requested glance.
503
- if (_readBudget) {
504
- // Use the file's REAL line count from the stream pass
505
- // (snapshotMeta.fileLineCount — the result has no top-level
506
- // totalLines), not the already-elided output's row count: the
507
- // re-budget marker otherwise reports "[TRUNCATED - 301 lines]"
508
- // for a 3800-line file.
509
- const _rebud = smartReadTruncate(out, _streamSmart.snapshotMeta?.fileLineCount || out.split('\n').length, st.size, filePath, _readBudget);
510
- if (_rebud?.truncated) out = _rebud.text;
511
- }
479
+ const out = _streamSmart.text;
512
480
  const snapshotMeta = _streamSmart.snapshotMeta || {
513
481
  source: 'read_smart_stream',
514
482
  ranges: [],
@@ -625,7 +593,7 @@ export async function executeSingleReadTool(args, workDir, readStateScope, optio
625
593
  // green-light an overwrite against bytes the read never returned.
626
594
  let _byteCapTruncated = false;
627
595
  const smart = (!hasRangeArgs && !wantFull && typeof smartReadTruncate === 'function')
628
- ? smartReadTruncate(rendered, lineCount, st.size, filePath, _readBudget)
596
+ ? smartReadTruncate(rendered, lineCount, st.size, filePath)
629
597
  : null;
630
598
  let _smartTruncated = false;
631
599
  let _smartVisibleRanges = null;
@@ -79,34 +79,6 @@ async function _readReachPreflight(rawPath, workDir, helpers) {
79
79
  catch (e) { return `Error: ${e?.message || e}`; }
80
80
  }
81
81
 
82
- function capPositiveNumber(value, max, fallback = max) {
83
- const n = Number(value);
84
- if (!Number.isFinite(n) || n <= 0) return fallback;
85
- return Math.min(max, Math.max(1, Math.trunc(n)));
86
- }
87
-
88
- function applyCompactReadBudget(entry) {
89
- if (!entry || String(entry.budget || '').toLowerCase() !== 'compact') return entry;
90
- const next = { ...entry };
91
- const isFullMode = !next.mode || next.mode === 'full';
92
- const hasLine = next.line !== undefined && next.line !== null;
93
- const hasRange = next.offset !== undefined || next.limit !== undefined;
94
- const hasPathLine = hasLineCoordinate(next.path);
95
- if (hasLine && (next.context === undefined || next.context === null || Number(next.context) > 20)) {
96
- next.context = 20;
97
- }
98
- if (hasRange && (next.limit === undefined || next.limit === null || Number(next.limit) > 120)) {
99
- next.limit = 120;
100
- }
101
- if ((next.mode === 'head' || next.mode === 'tail') && (next.n === undefined || next.n === null || Number(next.n) > 80)) {
102
- next.n = capPositiveNumber(next.n, 80, 80);
103
- }
104
- if (isFullMode && !hasLine && !hasRange && !hasPathLine && next.full !== true) {
105
- next.mode = 'count';
106
- }
107
- return next;
108
- }
109
-
110
82
  export async function executeReadTool(args, workDir, readStateScope, executeChildBuiltinTool, options = {}, helpers = {}) {
111
83
  const {
112
84
  classifyResultKind,
@@ -217,10 +189,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
217
189
  if (r?.line !== undefined) entry.line = r.line;
218
190
  if (r?.context !== undefined) entry.context = r.context;
219
191
  if (r?.full !== undefined) entry.full = r.full;
220
- if (r?.budget !== undefined) entry.budget = r.budget;
221
- entry = applyCompactReadBudget(entry);
222
192
  entry = normaliseReadLineWindowArgs(entry, workDir);
223
- entry = applyCompactReadBudget(entry);
224
193
  return entry;
225
194
  });
226
195
  const _invertedRawEntry = rawEntries.find((e) => e && e._invertedRangeError);
@@ -269,10 +238,7 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
269
238
  if (args.line !== undefined) entry.line = args.line;
270
239
  if (args.context !== undefined) entry.context = args.context;
271
240
  if (args.full !== undefined) entry.full = args.full;
272
- if (args.budget !== undefined) entry.budget = args.budget;
273
- entry = applyCompactReadBudget(entry);
274
241
  entry = normaliseReadLineWindowArgs(entry, workDir);
275
- entry = applyCompactReadBudget(entry);
276
242
  return entry;
277
243
  });
278
244
  const _invertedStrEntry = entries.find((e) => e && e._invertedRangeError);
@@ -450,9 +416,9 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
450
416
  args.path = normalizeInputPath(args.path);
451
417
  // Symbol reads are span-driven. Models (notably gpt-5.5) co-send the whole
452
418
  // schema filled with placeholder/zero values (offset:0, limit:0, line:0,
453
- // context:0, pages:'', max_lines:0, mode, budget) alongside symbol, which
419
+ // context:0, pages:'', mode) alongside symbol, which
454
420
  // otherwise makes the symbol branch think a window was requested and falls
455
- // back to a 0-window read. Treat those zero/empty/mode/budget params as
421
+ // back to a 0-window read. Treat those zero/empty/mode params as
456
422
  // absent so the whole symbol body returns in ONE call; a MEANINGFUL window
457
423
  // (offset>0 / limit>0 / non-empty pages / real line) is kept and overrides.
458
424
  if (typeof args.symbol === 'string' && args.symbol.trim()) {
@@ -460,11 +426,8 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
460
426
  if (args.limit === 0) delete args.limit;
461
427
  if (args.line === 0) delete args.line;
462
428
  if (args.pages === '') delete args.pages;
463
- if (args.max_lines === 0) delete args.max_lines;
464
- if (args.budget !== undefined) delete args.budget;
465
429
  if (args.mode !== undefined) delete args.mode;
466
430
  }
467
- args = applyCompactReadBudget(args);
468
431
  const sym = typeof args.symbol === 'string' ? args.symbol.trim() : '';
469
432
  if (sym) {
470
433
  const hasOffset = args.offset !== undefined && args.offset !== null;
@@ -515,7 +478,6 @@ export async function executeReadTool(args, workDir, readStateScope, executeChil
515
478
  }
516
479
  args = normaliseReadLineWindowArgs(args, workDir);
517
480
  if (args._invertedRangeError) return args._invertedRangeError;
518
- args = applyCompactReadBudget(args);
519
481
  }
520
482
  // Mode routing. A window already dropped any conflicting head/tail/summary
521
483
  // glance above (so the window is served by executeSingleReadTool); what