mixdog 0.9.1 → 0.9.3

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 (301) hide show
  1. package/package.json +9 -1
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  6. package/scripts/background-task-meta-smoke.mjs +1 -1
  7. package/scripts/bench-run.mjs +262 -0
  8. package/scripts/build-tui.mjs +13 -1
  9. package/scripts/compact-smoke.mjs +12 -0
  10. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  11. package/scripts/explore-bench.mjs +124 -0
  12. package/scripts/hook-bus-test.mjs +191 -0
  13. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  14. package/scripts/internal-comms-bench.mjs +727 -0
  15. package/scripts/internal-comms-smoke.mjs +75 -0
  16. package/scripts/lead-workflow-smoke.mjs +4 -4
  17. package/scripts/live-worker-smoke.mjs +9 -9
  18. package/scripts/output-style-bench.mjs +285 -0
  19. package/scripts/output-style-smoke.mjs +13 -10
  20. package/scripts/patch-replay.mjs +90 -0
  21. package/scripts/path-suffix-test.mjs +57 -0
  22. package/scripts/provider-stream-stall-test.mjs +276 -0
  23. package/scripts/provider-toolcall-test.mjs +599 -1
  24. package/scripts/recall-bench.mjs +207 -0
  25. package/scripts/routing-corpus.mjs +281 -0
  26. package/scripts/session-bench.mjs +1526 -0
  27. package/scripts/session-diag.mjs +595 -0
  28. package/scripts/task-bench.mjs +207 -0
  29. package/scripts/tool-failures.mjs +6 -6
  30. package/scripts/tool-smoke.mjs +310 -67
  31. package/scripts/toolcall-args-test.mjs +81 -0
  32. package/src/agents/debugger/AGENT.md +4 -4
  33. package/src/agents/heavy-worker/AGENT.md +20 -9
  34. package/src/agents/reviewer/AGENT.md +4 -4
  35. package/src/agents/worker/AGENT.md +17 -9
  36. package/src/app.mjs +10 -6
  37. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  38. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  39. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  40. package/src/headless-role.mjs +14 -14
  41. package/src/help.mjs +1 -0
  42. package/src/lib/rules-builder.cjs +32 -54
  43. package/src/mixdog-session-runtime.mjs +1040 -2036
  44. package/src/output-styles/default.md +12 -7
  45. package/src/output-styles/minimal.md +25 -0
  46. package/src/output-styles/oneline.md +21 -0
  47. package/src/output-styles/simple.md +10 -9
  48. package/src/repl.mjs +17 -7
  49. package/src/rules/agent/00-common.md +7 -5
  50. package/src/rules/agent/30-explorer.md +8 -12
  51. package/src/rules/lead/01-general.md +3 -1
  52. package/src/rules/lead/lead-tool.md +13 -2
  53. package/src/rules/shared/01-tool.md +23 -12
  54. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  55. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  56. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  57. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  58. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  59. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  60. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  61. package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
  62. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  63. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  64. package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
  65. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  66. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  67. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  68. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  69. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
  70. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
  71. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
  72. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  73. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  74. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  75. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  76. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
  78. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
  79. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
  80. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  82. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
  83. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  84. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
  85. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  86. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  87. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  88. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
  89. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  90. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  91. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
  93. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  94. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  97. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  98. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  99. package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
  100. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
  101. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  102. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
  103. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  104. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  105. package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
  106. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  107. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  108. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  109. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  110. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  113. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  116. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  118. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  119. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  121. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
  123. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  124. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  125. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
  126. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
  127. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  129. package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
  130. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  131. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  132. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  135. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  136. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  137. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  138. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  139. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
  140. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  141. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  142. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
  148. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
  149. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  150. package/src/runtime/channels/backends/discord.mjs +99 -9
  151. package/src/runtime/channels/backends/telegram.mjs +501 -0
  152. package/src/runtime/channels/index.mjs +437 -1439
  153. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  154. package/src/runtime/channels/lib/config.mjs +54 -2
  155. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  156. package/src/runtime/channels/lib/format.mjs +4 -2
  157. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  158. package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
  159. package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
  160. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  161. package/src/runtime/channels/lib/telegram-format.mjs +280 -0
  162. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
  164. package/src/runtime/channels/lib/webhook.mjs +59 -31
  165. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  166. package/src/runtime/channels/tool-defs.mjs +1 -1
  167. package/src/runtime/memory/index.mjs +465 -345
  168. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  169. package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
  170. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  171. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  172. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  173. package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
  174. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  175. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  176. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  177. package/src/runtime/memory/lib/memory.mjs +121 -4
  178. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  179. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  180. package/src/runtime/memory/lib/recall-format.mjs +183 -0
  181. package/src/runtime/memory/lib/session-ingest.mjs +107 -0
  182. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  183. package/src/runtime/memory/tool-defs.mjs +10 -7
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/background-tasks.mjs +2 -3
  186. package/src/runtime/shared/buffered-appender.mjs +149 -0
  187. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  188. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  189. package/src/runtime/shared/config.mjs +9 -0
  190. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  191. package/src/runtime/shared/schedules-store.mjs +21 -19
  192. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  193. package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
  194. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  195. package/src/runtime/shared/tool-surface.mjs +98 -13
  196. package/src/runtime/shared/transcript-writer.mjs +156 -0
  197. package/src/runtime/shared/update-checker.mjs +214 -0
  198. package/src/session-runtime/config-helpers.mjs +209 -0
  199. package/src/session-runtime/effort.mjs +128 -0
  200. package/src/session-runtime/fs-utils.mjs +10 -0
  201. package/src/session-runtime/model-capabilities.mjs +130 -0
  202. package/src/session-runtime/output-styles.mjs +124 -0
  203. package/src/session-runtime/plugin-mcp.mjs +114 -0
  204. package/src/session-runtime/session-text.mjs +100 -0
  205. package/src/session-runtime/statusline-route.mjs +35 -0
  206. package/src/session-runtime/tool-catalog.mjs +720 -0
  207. package/src/session-runtime/workflow.mjs +358 -0
  208. package/src/standalone/agent-tool.mjs +302 -117
  209. package/src/standalone/channel-admin.mjs +133 -40
  210. package/src/standalone/channel-worker.mjs +10 -292
  211. package/src/standalone/explore-tool.mjs +12 -5
  212. package/src/standalone/hook-bus.mjs +165 -8
  213. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  214. package/src/standalone/opencode-go-login.mjs +121 -0
  215. package/src/standalone/provider-admin.mjs +25 -3
  216. package/src/standalone/usage-dashboard.mjs +1 -1
  217. package/src/tui/App.jsx +2883 -778
  218. package/src/tui/components/ConfirmBar.jsx +47 -0
  219. package/src/tui/components/ContextPanel.jsx +5 -3
  220. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  221. package/src/tui/components/Markdown.jsx +22 -98
  222. package/src/tui/components/Message.jsx +14 -35
  223. package/src/tui/components/Picker.jsx +87 -12
  224. package/src/tui/components/PromptInput.jsx +355 -22
  225. package/src/tui/components/QueuedCommands.jsx +1 -1
  226. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  227. package/src/tui/components/Spinner.jsx +7 -7
  228. package/src/tui/components/StatusLine.jsx +40 -21
  229. package/src/tui/components/TextEntryPanel.jsx +51 -7
  230. package/src/tui/components/ToolExecution.jsx +183 -101
  231. package/src/tui/components/TurnDone.jsx +4 -4
  232. package/src/tui/components/UsagePanel.jsx +1 -1
  233. package/src/tui/components/tool-output-format.mjs +161 -23
  234. package/src/tui/components/tool-output-format.test.mjs +87 -0
  235. package/src/tui/display-width.mjs +69 -0
  236. package/src/tui/display-width.test.mjs +35 -0
  237. package/src/tui/dist/index.mjs +6731 -2333
  238. package/src/tui/engine/agent-envelope.mjs +296 -0
  239. package/src/tui/engine/boot-profile.mjs +21 -0
  240. package/src/tui/engine/labels.mjs +67 -0
  241. package/src/tui/engine/notice-text.mjs +112 -0
  242. package/src/tui/engine/queue-helpers.mjs +161 -0
  243. package/src/tui/engine/session-stats.mjs +46 -0
  244. package/src/tui/engine/tool-call-fields.mjs +23 -0
  245. package/src/tui/engine/tool-result-text.mjs +126 -0
  246. package/src/tui/engine.mjs +569 -948
  247. package/src/tui/index.jsx +117 -7
  248. package/src/tui/input-editing.mjs +58 -8
  249. package/src/tui/input-editing.selection.test.mjs +75 -0
  250. package/src/tui/keyboard-protocol.mjs +42 -0
  251. package/src/tui/lib/voice-recorder.mjs +469 -0
  252. package/src/tui/markdown/format-token.mjs +128 -76
  253. package/src/tui/markdown/format-token.test.mjs +61 -19
  254. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  255. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  256. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  257. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  258. package/src/tui/markdown/table-layout.mjs +9 -9
  259. package/src/tui/paste-attachments.mjs +36 -9
  260. package/src/tui/paste-fix.test.mjs +119 -0
  261. package/src/tui/prompt-history-store.mjs +129 -0
  262. package/src/tui/prompt-history-store.test.mjs +52 -0
  263. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  264. package/src/tui/theme.mjs +41 -657
  265. package/src/tui/themes/base.mjs +86 -0
  266. package/src/tui/themes/basic.mjs +85 -0
  267. package/src/tui/themes/catppuccin.mjs +72 -0
  268. package/src/tui/themes/dracula.mjs +70 -0
  269. package/src/tui/themes/everforest.mjs +71 -0
  270. package/src/tui/themes/gruvbox.mjs +71 -0
  271. package/src/tui/themes/index.mjs +71 -0
  272. package/src/tui/themes/indigo.mjs +78 -0
  273. package/src/tui/themes/kanagawa.mjs +80 -0
  274. package/src/tui/themes/light.mjs +81 -0
  275. package/src/tui/themes/nord.mjs +72 -0
  276. package/src/tui/themes/onedark.mjs +16 -0
  277. package/src/tui/themes/rosepine.mjs +70 -0
  278. package/src/tui/themes/teal.mjs +80 -0
  279. package/src/tui/themes/tokyonight.mjs +79 -0
  280. package/src/tui/themes/utils.mjs +106 -0
  281. package/src/tui/themes/warm.mjs +79 -0
  282. package/src/tui/transcript-tool-failures.mjs +13 -2
  283. package/src/ui/markdown.mjs +1 -1
  284. package/src/ui/model-display.mjs +2 -2
  285. package/src/ui/statusline.mjs +75 -27
  286. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  287. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  288. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  289. package/src/workflows/default/WORKFLOW.md +46 -12
  290. package/src/workflows/sequential/WORKFLOW.md +51 -0
  291. package/src/workflows/solo/WORKFLOW.md +12 -1
  292. package/vendor/ink/build/display-width.js +62 -0
  293. package/vendor/ink/build/ink.js +100 -12
  294. package/vendor/ink/build/measure-text.js +4 -1
  295. package/vendor/ink/build/output.js +115 -9
  296. package/vendor/ink/build/render-node-to-output.js +4 -1
  297. package/vendor/ink/build/render.js +4 -0
  298. package/src/output-styles/extreme-simple.md +0 -20
  299. package/src/rules/lead/04-workflow.md +0 -51
  300. package/src/workflows/default/workflow.json +0 -13
  301. package/src/workflows/solo/workflow.json +0 -7
@@ -161,7 +161,7 @@ function _sessionSummary(session) {
161
161
  closed: session.closed === true,
162
162
  status: String(session.status || (session.closed === true ? 'closed' : 'idle')),
163
163
  owner: session.owner || 'user',
164
- role: session.role || null,
164
+ agent: session.agent || null,
165
165
  sourceType: session.sourceType || null,
166
166
  sourceName: session.sourceName || null,
167
167
  scopeKey: session.scopeKey || null,
@@ -191,7 +191,7 @@ function _normalizeSummaryRow(row) {
191
191
  closed: row.closed === true,
192
192
  status: String(row.status || (row.closed === true ? 'closed' : 'idle')),
193
193
  owner: row.owner || 'user',
194
- role: row.role || null,
194
+ agent: row.agent || null,
195
195
  sourceType: row.sourceType || null,
196
196
  sourceName: row.sourceName || null,
197
197
  scopeKey: row.scopeKey || null,
@@ -655,11 +655,11 @@ export function markSessionClosed(id, reason = 'manual') {
655
655
  const _lifeMs = (typeof existing.createdAt === 'number' && existing.createdAt > 0)
656
656
  ? (tombstone.updatedAt - existing.createdAt)
657
657
  : 0;
658
- const _role = existing.role || '-';
658
+ const _agent = existing.agent || '-';
659
659
  const _owner = existing.owner || '-';
660
660
  void fsp.appendFile(
661
661
  join(_dataDir, 'tool-events.log'),
662
- `[${_ts}] [session-close] owner=${_owner} role=${_role} reason=${reason} lifeMs=${_lifeMs} id=${id}\n`,
662
+ `[${_ts}] [session-close] owner=${_owner} agent=${_agent} reason=${reason} lifeMs=${_lifeMs} id=${id}\n`,
663
663
  ).catch(() => {});
664
664
  }
665
665
  } catch { /* logger never breaks the close path */ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * tool-envelope.mjs — general optional `newMessages` channel for tool results.
3
+ *
4
+ * A tool MAY return either:
5
+ * - legacy: a string (or existing structured media object) — unchanged, OR
6
+ * - an envelope object:
7
+ * { __toolEnvelope: true, result: <string|structured>,
8
+ * newMessages: [{ role:'user', content:'...' }, ...] }
9
+ *
10
+ * The `__toolEnvelope` marker is deliberately namespaced so it can NEVER be
11
+ * confused with the existing structured media content objects that
12
+ * PostToolUse hooks/tests preserve (those use `{ content: [...] }`, never
13
+ * `__toolEnvelope`). `normalizeToolEnvelope` is the single split point: it
14
+ * returns `{ result, newMessages }` so ALL downstream loop code sees only
15
+ * `result` and the loop alone is responsible for flushing `newMessages`.
16
+ */
17
+
18
+ export const TOOL_ENVELOPE_MARKER = '__toolEnvelope';
19
+
20
+ export function isToolEnvelope(value) {
21
+ return !!value
22
+ && typeof value === 'object'
23
+ && !Array.isArray(value)
24
+ && value[TOOL_ENVELOPE_MARKER] === true;
25
+ }
26
+
27
+ function isValidNewMessage(m) {
28
+ return !!m
29
+ && typeof m === 'object'
30
+ && m.role === 'user'
31
+ && typeof m.content === 'string'
32
+ && m.content.length > 0;
33
+ }
34
+
35
+ /**
36
+ * Build a tool-result envelope. `result` is the short stub/data the model
37
+ * sees as the tool_result; `newMessages` are appended (as their own
38
+ * messages, e.g. role:'user') AFTER the batch's tool results.
39
+ */
40
+ export function makeToolEnvelope(result, newMessages = []) {
41
+ return {
42
+ [TOOL_ENVELOPE_MARKER]: true,
43
+ result,
44
+ newMessages: Array.isArray(newMessages) ? newMessages.filter(isValidNewMessage) : [],
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Split a tool return value into `{ result, newMessages }`.
50
+ * - legacy string/object → { result: value, newMessages: [] }
51
+ * - envelope → { result, newMessages } (newMessages validated)
52
+ */
53
+ export function normalizeToolEnvelope(value) {
54
+ if (isToolEnvelope(value)) {
55
+ const newMessages = Array.isArray(value.newMessages)
56
+ ? value.newMessages.filter(isValidNewMessage)
57
+ : [];
58
+ return { result: value.result, newMessages };
59
+ }
60
+ return { result: value, newMessages: [] };
61
+ }
@@ -21,11 +21,16 @@ export const TOOL_RESULT_OFFLOAD_PREFIX = '[tool output offloaded:';
21
21
  // READ_MAX_SIZE_BYTES cap, so persisting to a sidecar to be re-read would be
22
22
  // circular. These values keep context-rich IO tools from turning into "read
23
23
  // saved output" loops while bounding the per-call inline footprint per CC.
24
+ // Skill / skill_view bodies stay inline for the same reason — offloading a
25
+ // loaded SKILL.md would force a read loop and defeat the loaded-skill guard.
24
26
  const INLINE_THRESHOLD_BY_TOOL = new Map([
25
27
  ['read', Infinity],
26
28
  ['head', Infinity],
27
29
  ['tail', Infinity],
28
30
  ['diff', Infinity],
31
+ ['skill', Infinity],
32
+ ['skill_view', Infinity],
33
+ ['skills_list', Infinity],
29
34
  ['grep', TOOL_RESULT_GREP_THRESHOLD_CHARS],
30
35
  ['glob', TOOL_RESULT_SEARCH_THRESHOLD_CHARS],
31
36
  ['list', TOOL_RESULT_SEARCH_THRESHOLD_CHARS],
@@ -1,4 +1,4 @@
1
- import { getHiddenRole } from './internal-roles.mjs';
1
+ import { getHiddenAgent } from './internal-agents.mjs';
2
2
 
3
3
  const SECOND_MS = 1000;
4
4
  const MIN_PROVIDER_TIMEOUT_MS = 30_000;
@@ -68,7 +68,13 @@ export const DEFAULT_ACTIVITY_HEARTBEAT_MS = resolveTimeoutMs(
68
68
 
69
69
  export const PROVIDER_FIRST_BYTE_TIMEOUT_MS = resolveTimeoutMs(
70
70
  'MIXDOG_PROVIDER_FIRST_BYTE_TIMEOUT_MS',
71
- Math.min(120_000, PROVIDER_MAX_BEFORE_WARN_MS),
71
+ // 2026-07 trace audit: heavy-worker claude-sonnet-5 xhigh TTFT distribution
72
+ // showed avg ~25s, max ~120s (15/75 requests over 60s) — the old 120s
73
+ // window sat exactly on the observed tail and killed/retried healthy slow
74
+ // first bytes (9 first-byte retries in one 75-iteration session, plus one
75
+ // full turn lost). 180s clears the tail while the mid-stream retry ladder
76
+ // and the agent stall watchdog still bound a truly dead socket.
77
+ Math.min(180_000, PROVIDER_MAX_BEFORE_WARN_MS),
72
78
  { minMs: MIN_PROVIDER_TIMEOUT_MS, maxMs: PROVIDER_MAX_BEFORE_WARN_MS },
73
79
  );
74
80
 
@@ -125,6 +131,44 @@ export const PROVIDER_SSE_IDLE_TIMEOUT_MS = resolveTimeoutMs(
125
131
  { minMs: 10_000, maxMs: STALL_WARN_MS },
126
132
  );
127
133
 
134
+ // Semantic (last-meaningful-event) idle window. Distinct name from the
135
+ // transport-byte SSE idle so provider loops can key their mid-stream abort off
136
+ // SEMANTIC progress (message/content/tool deltas) rather than raw keepalive
137
+ // bytes (Anthropic `:ping`, comment frames). A truly silent stream — one that
138
+ // emits no semantic event for this window — trips it; a live extended-thinking
139
+ // stream (which emits thinking deltas) stays alive. Default 120s, floor 10s,
140
+ // env-overridable and disablable via MIXDOG_ENABLE_STREAM_WATCHDOG=0.
141
+ export const PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS = resolveTimeoutMs(
142
+ ['MIXDOG_PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS', 'MIXDOG_PROVIDER_SSE_IDLE_TIMEOUT_MS'],
143
+ // Raised 120s→180s alongside PROVIDER_FIRST_BYTE_TIMEOUT_MS (see comment
144
+ // there): observed healthy inter-event gaps on xhigh reasoning streams
145
+ // brushed the old 120s window; stalls after a tool emit are unretryable,
146
+ // so a false trip there costs the whole turn.
147
+ 180_000,
148
+ { minMs: 10_000, maxMs: STALL_WARN_MS },
149
+ );
150
+
151
+ // Named terminal error for a mid-stream SEMANTIC idle abort. Distinct from a
152
+ // user cancel (signal.aborted): the retry-classifier treats this as a terminal
153
+ // STREAM FAILURE so the owner (Lead) receives a failure notification instead of
154
+ // the task hanging. The message keeps "timed out after …ms of inactivity" so
155
+ // the SSE mid-stream classifier's text match (sse_idle_timeout) still fires,
156
+ // and code 'ESTREAMSTALL' routes it through the transient/notify path.
157
+ export function streamStalledError(label, timeoutMs, { emittedToolCall = false } = {}) {
158
+ const err = new Error(`${label} stream timed out after ${timeoutMs}ms of inactivity`);
159
+ err.name = 'StreamStalledError';
160
+ err.code = 'ESTREAMSTALL';
161
+ err.streamStalled = true;
162
+ // Double-dispatch guard (reviewer High): once a tool call has already been
163
+ // emitted this turn (including a recovered text-leaked call), retrying the
164
+ // request would RE-RUN that side-effecting tool. Mark such a stall
165
+ // unsafe-to-retry so withRetry() throws it straight through to the terminal
166
+ // notify path instead of replaying the turn. A stall BEFORE any tool emit is
167
+ // still safely retryable (nothing has executed yet).
168
+ if (emittedToolCall) err.unsafeToRetry = true;
169
+ return err;
170
+ }
171
+
128
172
  export const PROVIDER_WS_HANDSHAKE_TIMEOUT_MS = resolveTimeoutMs(
129
173
  'MIXDOG_PROVIDER_WS_HANDSHAKE_TIMEOUT_MS',
130
174
  30_000,
@@ -137,19 +181,23 @@ export const PROVIDER_WS_ACQUIRE_TIMEOUT_MS = resolveTimeoutMs(
137
181
  { minMs: 5_000, maxMs: PROVIDER_MAX_BEFORE_WARN_MS },
138
182
  );
139
183
 
140
- export const PROVIDER_WS_FIRST_MEANINGFUL_TIMEOUT_MS = resolveTimeoutMs(
141
- 'MIXDOG_PROVIDER_WS_FIRST_MEANINGFUL_TIMEOUT_MS',
142
- Math.min(PROVIDER_FIRST_BYTE_TIMEOUT_MS, PROVIDER_GENERATE_TOTAL_TIMEOUT_MS),
143
- { minMs: MIN_PROVIDER_TIMEOUT_MS, maxMs: PROVIDER_MAX_BEFORE_WARN_MS },
144
- );
145
-
184
+ // Single inter-chunk idle timer (300s), matching the upstream WS provider's
185
+ // default stream idle timeout. Mixdog resets one idle timer on every received
186
+ // WS frame (openai-oauth-ws messageHandler resets on every parsed event). There
187
+ // is deliberately no separate "first-meaningful" watchdog — a live stream
188
+ // (including server-side reasoning ACKed via response.created) keeps this timer
189
+ // fresh, and only true socket silence trips it. Env-tunable.
146
190
  export const PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS = resolveTimeoutMs(
147
191
  'MIXDOG_PROVIDER_WS_INTER_CHUNK_TIMEOUT_MS',
148
- STALL_ABORT_MS,
149
- { minMs: STALL_WARN_MS, maxMs: STALL_ABORT_MS },
192
+ 300_000,
193
+ { minMs: MIN_PROVIDER_TIMEOUT_MS, maxMs: STALL_ABORT_MS },
150
194
  );
151
195
 
152
- export const PROVIDER_RETRY_BACKOFF_MS = Object.freeze([0, 1000, 2000, 4000, 8000]);
196
+ // First retry has a small floor (250ms) instead of 0ms: an immediate reissue on
197
+ // a transient 5xx burst lets parallel workers thundering-herd the backend in
198
+ // lockstep (jitter alone can't decluster a 0ms base). Subsequent steps keep the
199
+ // exponential schedule. Env-tunable via the providers' retryJitterRatio.
200
+ export const PROVIDER_RETRY_BACKOFF_MS = Object.freeze([250, 1000, 2000, 4000, 8000]);
153
201
  export const PROVIDER_RETRY_MAX_ATTEMPTS = PROVIDER_RETRY_BACKOFF_MS.length;
154
202
  export const PROVIDER_RETRY_JITTER_RATIO = (() => {
155
203
  const raw = process.env.MIXDOG_PROVIDER_RETRY_JITTER_RATIO;
@@ -158,8 +206,8 @@ export const PROVIDER_RETRY_JITTER_RATIO = (() => {
158
206
  return 0.2;
159
207
  })();
160
208
 
161
- export function resolveAgentStallThresholds(role, env = process.env) {
162
- const cfg = role ? getHiddenRole(role) : null;
209
+ export function resolveAgentStallThresholds(agent, env = process.env) {
210
+ const cfg = agent ? getHiddenAgent(agent) : null;
163
211
  const cfgAbort = cfg?.stallCap?.idleSeconds > 0 ? cfg.stallCap.idleSeconds : STALL_ABORT_S;
164
212
  const envOverride = envThresholdSeconds(env);
165
213
  const abort = envOverride != null ? envOverride : cfgAbort;
@@ -175,8 +223,8 @@ export function resolveAgentStallThresholds(role, env = process.env) {
175
223
  return { warn, abort, firstByteAbort };
176
224
  }
177
225
 
178
- export function resolveAgentToolThresholdSeconds(role, thresholdSeconds) {
179
- const cfg = role ? getHiddenRole(role) : null;
226
+ export function resolveAgentToolThresholdSeconds(agent, thresholdSeconds) {
227
+ const cfg = agent ? getHiddenAgent(agent) : null;
180
228
  if (cfg?.stallCap?.toolRunningSeconds > 0) return cfg.stallCap.toolRunningSeconds;
181
229
  return thresholdSeconds;
182
230
  }
@@ -9,13 +9,22 @@
9
9
  // callers may pass either spelling (e.g. `glob` alias for grep, or
10
10
  // `file_path` alias for read.path).
11
11
 
12
- const MAX_INT = 100000;
13
- export const GREP_CONTEXT_MAX = 12;
12
+ import { coerceShapeFlex, hasGlobMagic } from './path-utils.mjs';
14
13
 
14
+ const MAX_INT = 100000;
15
+ // Explicit grep context should be large enough to frame a function/block without
16
+ // letting one match explode into a huge tool result. `content_with_context` still
17
+ // defaults to 25 lines; this is only the upper bound for caller-supplied -A/-B/-C.
18
+ export const GREP_CONTEXT_MAX = 200;
19
+
20
+ // ripgrep-flavored aliases: models trained on `rg` emit short flags (-A/-B/-C),
21
+ // long flags (--after-context / --before-context / --context), or snake/camel
22
+ // spellings. All fold onto the canonical -A/-B/-C so a caller can write grep
23
+ // the way they would write ripgrep on the shell.
15
24
  const GREP_CONTEXT_KEY_GROUPS = [
16
- ['-A', ['-A', 'A', 'after', 'after_context', 'afterContext']],
17
- ['-B', ['-B', 'B', 'before', 'before_context', 'beforeContext']],
18
- ['-C', ['-C', 'C', 'context', 'context_lines', 'contextLines']],
25
+ ['-A', ['-A', 'A', 'after', 'after_context', 'afterContext', '--after-context', 'after-context', 'afterLines', 'after_lines']],
26
+ ['-B', ['-B', 'B', 'before', 'before_context', 'beforeContext', '--before-context', 'before-context', 'beforeLines', 'before_lines']],
27
+ ['-C', ['-C', 'C', 'context', 'context_lines', 'contextLines', '--context', 'contextN', 'around', 'surrounding']],
19
28
  ];
20
29
 
21
30
  function grepContextKeyPresent(a, k) {
@@ -98,8 +107,35 @@ function isFiniteInt(v) {
98
107
  return typeof v === 'number' && Number.isFinite(v) && Math.floor(v) === v;
99
108
  }
100
109
 
101
- function checkIntInRange(field, value, min, max) {
110
+ // Lossless numeric-string coercion for integer-shaped args. Models
111
+ // occasionally emit JSON-schema-violating numeric strings ("850",
112
+ // "850.0") for fields the schema types as number (offset/limit/n/
113
+ // head_limit/-A/-B/-C/context/line). Both are unambiguous integer
114
+ // values, so coerce them to a real number before validating rather
115
+ // than rejecting and forcing a retry turn. Non-integer or non-numeric
116
+ // strings ("3.5", "soon") are left untouched and fall through to the
117
+ // existing rejection below.
118
+ function coerceIntegerString(v) {
119
+ if (typeof v !== 'string') return null;
120
+ const t = v.trim();
121
+ if (t === '' || !/^-?\d+(\.\d+)?$/.test(t)) return null;
122
+ const n = Number(t);
123
+ if (!Number.isFinite(n) || !Number.isInteger(n)) return null;
124
+ return n;
125
+ }
126
+
127
+ // Mutates a[field] in place when it is a lossless integer string, then
128
+ // validates the (possibly coerced) value against [min, max].
129
+ function checkIntInRange(a, field, min, max) {
130
+ let value = a[field];
102
131
  if (value === undefined || value === null) return null;
132
+ if (typeof value === 'string') {
133
+ const coerced = coerceIntegerString(value);
134
+ if (coerced !== null) {
135
+ value = coerced;
136
+ a[field] = coerced;
137
+ }
138
+ }
103
139
  if (!isFiniteInt(value)) {
104
140
  return `Error: builtin arg "${field}" must be a finite integer (got ${describeType(value)})`;
105
141
  }
@@ -122,6 +158,38 @@ function hasOwn(o, k) {
122
158
  return o && Object.prototype.hasOwnProperty.call(o, k);
123
159
  }
124
160
 
161
+ function isPresent(o, k) {
162
+ return hasOwn(o, k) && o[k] !== undefined && o[k] !== null;
163
+ }
164
+
165
+ function isNonEmptyPresent(o, k) {
166
+ return isPresent(o, k) && o[k] !== '';
167
+ }
168
+
169
+ // Strip trailing literal artifacts from a grep pattern: a literal two-char
170
+ // "\n" (backslash + n) that ripgrep rejects outside multiline mode ("the
171
+ // literal \"\\n\" is not allowed"), possibly preceded by concatenation
172
+ // debris like `">` that rides along with it (e.g. a stray closing-tag
173
+ // fragment glued on by string interpolation). `">` is ONLY stripped when
174
+ // it is directly followed by one of those newline artifacts — a bare
175
+ // trailing `">` with no newline riding along (e.g. a legit HTML/JSX
176
+ // attribute pattern like `class="active">`) is a real search target and
177
+ // must survive untouched. A \n in the middle of a pattern is also left
178
+ // untouched; only the tail is ever trimmed.
179
+ function stripTrailingPatternArtifacts(v) {
180
+ if (typeof v !== 'string') return v;
181
+ let out = v;
182
+ let changed = true;
183
+ while (changed) {
184
+ changed = false;
185
+ if (out.endsWith('">\n')) { out = out.slice(0, -3); changed = true; continue; }
186
+ if (out.endsWith('">\\n')) { out = out.slice(0, -4); changed = true; continue; }
187
+ if (out.endsWith('\n')) { out = out.slice(0, -1); changed = true; continue; }
188
+ if (out.endsWith('\\n')) { out = out.slice(0, -2); changed = true; continue; }
189
+ }
190
+ return out;
191
+ }
192
+
125
193
  // ---- per-tool guards ----
126
194
 
127
195
  function guardGrep(a) {
@@ -130,6 +198,15 @@ function guardGrep(a) {
130
198
  // glob (file filter) aliases
131
199
  const globKeys = ['glob', 'file_pattern', 'include', 'files'];
132
200
 
201
+ // Lossless cleanup of trailing artifacts before validation (item 5b).
202
+ for (const k of patternKeys) {
203
+ if (hasOwn(a, k)) {
204
+ a[k] = Array.isArray(a[k])
205
+ ? a[k].map(stripTrailingPatternArtifacts)
206
+ : stripTrailingPatternArtifacts(a[k]);
207
+ }
208
+ }
209
+
133
210
  const hasPattern = patternKeys.some((k) => hasOwn(a, k));
134
211
  const hasGlob = globKeys.some((k) => hasOwn(a, k));
135
212
  if (!hasPattern && !hasGlob) {
@@ -157,34 +234,93 @@ function guardGrep(a) {
157
234
  return `Error: grep arg "${k}" contains multiple absolute paths in one string. Use one common parent path plus glob, or separate grep calls.`;
158
235
  }
159
236
  }
160
- // numeric clamps (grep context 0..12, mirrors read line-context tightening)
161
237
  for (const k of ['head_limit', 'offset']) {
162
- const err = checkIntInRange(k, a[k], 0, MAX_INT);
238
+ const err = checkIntInRange(a, k, 0, MAX_INT);
163
239
  if (err) return err;
164
240
  }
165
241
  for (const k of ['-A', '-B', '-C', 'context']) {
166
- const err = checkIntInRange(k, a[k], 0, GREP_CONTEXT_MAX);
242
+ const err = checkIntInRange(a, k, 0, GREP_CONTEXT_MAX);
167
243
  if (err) return err;
168
244
  }
169
245
  // output_mode / mode enum
170
246
  const modeKeys = ['output_mode', 'mode'];
171
- const allowed = new Set(['files_with_matches', 'content', 'count']);
247
+ const allowed = new Set(['files_with_matches', 'content', 'content_with_context', 'count']);
172
248
  for (const k of modeKeys) {
173
249
  if (hasOwn(a, k)) {
250
+ // Some callers concatenate a second field's value onto the enum
251
+ // string with a literal newline (e.g. "content_with_context\ntrue").
252
+ // If the first line/token is a valid enum value, truncate to it
253
+ // losslessly rather than rejecting a shape that unambiguously
254
+ // names a real mode (item 5a).
255
+ if (typeof a[k] === 'string' && a[k].includes('\n')) {
256
+ const firstLine = a[k].split('\n')[0].trim();
257
+ const firstToken = firstLine.split(/\s+/)[0];
258
+ if (allowed.has(firstToken)) a[k] = firstToken;
259
+ }
174
260
  if (!isString(a[k]) || !allowed.has(a[k])) {
175
- return `Error: grep arg "${k}" must be one of files_with_matches|content|count (got ${JSON.stringify(a[k])})`;
261
+ return `Error: grep arg "${k}" must be one of content_with_context|content|files_with_matches|count (got ${JSON.stringify(a[k])})`;
176
262
  }
177
263
  }
178
264
  }
179
265
  return null;
180
266
  }
181
267
 
268
+ // Convert a {line, context} pair into {offset, limit}, matching the
269
+ // read-args.mjs private file#Lx compatibility normalizer's semantics
270
+ // (offset = startLine - 1, limit = endLine - startLine + 1, clamped at 0)
271
+ // generalized to a symmetric window: startLine = line - context,
272
+ // endLine = line + context. offset/limit are treated as authoritative —
273
+ // if either is already present, line/context are dropped unused rather
274
+ // than overriding an explicit window. Mutates obj in place. Returns an
275
+ // error string or null.
276
+ function applyLineContextWindow(obj, labelPrefix) {
277
+ for (const k of ['line', 'context', 'offset', 'limit']) {
278
+ if (isPresent(obj, k) && typeof obj[k] === 'string') {
279
+ const coerced = coerceIntegerString(obj[k]);
280
+ if (coerced !== null) obj[k] = coerced;
281
+ }
282
+ }
283
+ const hasLine = isPresent(obj, 'line');
284
+ const hasContext = isPresent(obj, 'context');
285
+ if (!hasLine && !hasContext) return null;
286
+ if (isPresent(obj, 'offset') || isPresent(obj, 'limit')) {
287
+ delete obj.line;
288
+ delete obj.context;
289
+ return null;
290
+ }
291
+ if (!hasLine) {
292
+ return `Error: read arg "${labelPrefix}context" requires "${labelPrefix}line" to compute a window`;
293
+ }
294
+ if (!isFiniteInt(obj.line) || obj.line < 1) {
295
+ return `Error: read arg "${labelPrefix}line" must be a finite integer >= 1 (got ${describeType(obj.line)})`;
296
+ }
297
+ let ctx = 0;
298
+ if (hasContext) {
299
+ if (!isFiniteInt(obj.context) || obj.context < 0) {
300
+ return `Error: read arg "${labelPrefix}context" must be a non-negative finite integer (got ${describeType(obj.context)})`;
301
+ }
302
+ ctx = obj.context;
303
+ }
304
+ obj.offset = Math.max(0, obj.line - 1 - ctx);
305
+ obj.limit = 2 * ctx + 1;
306
+ delete obj.line;
307
+ delete obj.context;
308
+ return null;
309
+ }
310
+
182
311
  function guardRead(a) {
183
312
  // path / file_path alias OR path may itself be array
184
313
  const hasPath = hasOwn(a, 'path') || hasOwn(a, 'file_path');
185
314
  if (!hasPath) {
186
315
  return 'Error: read requires "path" (or alias file_path).';
187
316
  }
317
+ // Some providers/models send a batched path array as a JSON string despite
318
+ // the schema. The executor already accepts that shape via coerceShapeFlex(),
319
+ // but validation runs first; apply the same lossless shape coercion here so
320
+ // valid batched reads do not waste a retry turn.
321
+ if (hasOwn(a, 'path')) {
322
+ a.path = coerceShapeFlex(a.path);
323
+ }
188
324
  // path can be string | string[] | object[]; file_path is string
189
325
  if (hasOwn(a, 'path')) {
190
326
  const p = a.path;
@@ -192,16 +328,36 @@ function guardRead(a) {
192
328
  if (!ok) {
193
329
  return `Error: read arg "path" must be string, string[], or object[] (got ${describeType(p)})`;
194
330
  }
331
+ if (Array.isArray(p)) {
332
+ for (let i = 0; i < p.length; i++) {
333
+ const entry = p[i];
334
+ if (typeof entry === 'string') continue;
335
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
336
+ return `Error: read arg "path[${i}]" must be string or {path,offset,limit} object (got ${describeType(entry)})`;
337
+ }
338
+ const err = applyLineContextWindow(entry, `path[${i}].`);
339
+ if (err) return err;
340
+ for (const ek of ['offset', 'limit']) {
341
+ const eErr = checkIntInRange(entry, ek, 0, MAX_INT);
342
+ if (eErr) return eErr.replace(`"${ek}"`, `"path[${i}].${ek}"`);
343
+ }
344
+ }
345
+ }
195
346
  }
196
347
  if (hasOwn(a, 'file_path') && !isNonEmptyString(a.file_path)) {
197
348
  return `Error: read arg "file_path" must be a non-empty string (got ${describeType(a.file_path)})`;
198
349
  }
199
- if (hasOwn(a, 'line') && (hasOwn(a, 'offset') || hasOwn(a, 'limit'))) {
200
- return 'Error: read window args conflict use exactly one window family: line+context OR offset+limit OR symbol.';
350
+ // Read's public surface is offset/limit, but a top-level line/context pair
351
+ // is a deterministic, lossless spelling of the same window (matching
352
+ // read-args.mjs's internal file:line normalizer semantics); convert it
353
+ // instead of rejecting. offset/limit, if already present, win outright.
354
+ {
355
+ const err = applyLineContextWindow(a, '');
356
+ if (err) return err;
201
357
  }
202
358
  // offset >=0
203
359
  {
204
- const err = checkIntInRange('offset', a.offset, 0, MAX_INT);
360
+ const err = checkIntInRange(a, 'offset', 0, MAX_INT);
205
361
  if (err) return err;
206
362
  }
207
363
  // limit: >=1 = explicit cap; 0 = unlimited sentinel (read-formatting maps 0 to
@@ -210,12 +366,7 @@ function guardRead(a) {
210
366
  // break that unbounded-batch contract. A placeholder limit:0 from a symbol
211
367
  // read is stripped on the symbol path. Negatives still error.
212
368
  {
213
- const err = checkIntInRange('limit', a.limit, 0, MAX_INT);
214
- if (err) return err;
215
- }
216
- // context 0..200
217
- {
218
- const err = checkIntInRange('context', a.context, 0, 200);
369
+ const err = checkIntInRange(a, 'limit', 0, MAX_INT);
219
370
  if (err) return err;
220
371
  }
221
372
  // n 0..10000 — accept 0 rather than erroring: the read-mode handlers coerce
@@ -224,7 +375,7 @@ function guardRead(a) {
224
375
  // n is moot. Rejecting 0 only forced a wasted retry turn (the whole point of
225
376
  // these reads is to land in one shot). Negatives remain a real error.
226
377
  if (hasOwn(a, 'n') && a.n !== undefined && a.n !== null) {
227
- const err = checkIntInRange('n', a.n, 0, 10000);
378
+ const err = checkIntInRange(a, 'n', 0, 10000);
228
379
  if (err) return err;
229
380
  }
230
381
  return null;
@@ -248,7 +399,10 @@ function guardShell(a) {
248
399
  return 'Error: shell arg "command" must be a non-empty string';
249
400
  }
250
401
  if (process.platform === 'win32' && !hasOwn(a, 'shell') && hasWindowsDrivePath(a.command)) {
251
- return "Error: shell command contains a Windows drive path; set shell:'powershell' or convert paths for shell:'bash'.";
402
+ // A Windows drive path (C:\...) is unambiguous evidence the caller
403
+ // wants the Windows shell; default it losslessly instead of forcing
404
+ // a retry turn just to add shell:'powershell'.
405
+ a.shell = 'powershell';
252
406
  }
253
407
  for (const k of ['cwd', 'workdir']) {
254
408
  if (hasOwn(a, k) && (a[k] === undefined || a[k] === null || a[k] === '')) {
@@ -323,12 +477,28 @@ function guardFind(a) {
323
477
 
324
478
  function guardGlob(a) {
325
479
  // path alias root; pattern aliases glob/name/file_pattern
480
+ const globPatternKeys = ['pattern', 'glob', 'name', 'file_pattern'];
481
+ const hasAnyPattern = globPatternKeys.some((k) => isNonEmptyPresent(a, k));
482
+ // Skip the default when `path` itself carries glob magic (*?[{) — that
483
+ // shape means "path IS the pattern" and is handled by executeGlobTool's
484
+ // own path-magic fallback (splitting path into baseDir + pattern).
485
+ // Injecting pattern:'*' here would override that fallback and silently
486
+ // change "src/**/*.mjs" into "match everything under src/**/*.mjs".
487
+ const pathHasGlobMagic = Array.isArray(a.path)
488
+ ? a.path.some((p) => hasGlobMagic(p))
489
+ : hasGlobMagic(a.path);
490
+ if (!hasAnyPattern && isNonEmptyPresent(a, 'path') && !pathHasGlobMagic) {
491
+ // Missing pattern with a real path is an unambiguous "match
492
+ // everything under this path" request; default it instead of
493
+ // erroring out via globMissingPatternMessage() downstream.
494
+ a.pattern = '*';
495
+ }
326
496
  for (const k of ['path', 'root']) {
327
497
  if (hasOwn(a, k) && !isStringOrStringArray(a[k])) {
328
498
  return `Error: glob arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
329
499
  }
330
500
  }
331
- for (const k of ['pattern', 'glob', 'name', 'file_pattern']) {
501
+ for (const k of globPatternKeys) {
332
502
  if (hasOwn(a, k) && !isStringOrStringArray(a[k])) {
333
503
  return `Error: glob arg "${k}" must be string or string[] (got ${describeType(a[k])})`;
334
504
  }
@@ -351,7 +521,7 @@ function guardGlob(a) {
351
521
  // find_symbol, references, callers, imports, dependents.
352
522
  const CODE_GRAPH_MODES = new Set([
353
523
  'overview', 'imports', 'dependents', 'related', 'impact',
354
- 'symbols', 'find_symbol', 'search', 'references', 'callers', 'callees', 'prewarm',
524
+ 'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers', 'callees', 'prewarm',
355
525
  ]);
356
526
 
357
527
  function guardCodeGraph(a) {