mixdog 0.9.151 → 0.9.153

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 (307) hide show
  1. package/LICENSES/browser-import-NOTICE.txt +11 -0
  2. package/NOTICE.md +11 -0
  3. package/README.md +108 -68
  4. package/package.json +17 -4
  5. package/scripts/computer-task-cost-report.mjs +144 -0
  6. package/scripts/run-office-live-tests.mjs +32 -0
  7. package/scripts/tool-stress.mjs +0 -2
  8. package/src/agents/front-worker/AGENT.md +13 -0
  9. package/src/agents/front-worker/agent.json +6 -0
  10. package/src/agents/heavy-worker/AGENT.md +10 -11
  11. package/src/agents/heavy-worker/agent.json +1 -1
  12. package/src/agents/maintainer/AGENT.md +0 -2
  13. package/src/agents/reviewer/AGENT.md +9 -13
  14. package/src/agents/reviewer/agent.json +1 -1
  15. package/src/agents/security/AGENT.md +14 -0
  16. package/src/agents/security/agent.json +6 -0
  17. package/src/agents/worker/AGENT.md +8 -8
  18. package/src/agents/worker/agent.json +1 -1
  19. package/src/agents/writer/AGENT.md +14 -0
  20. package/src/agents/writer/agent.json +6 -0
  21. package/src/defaults/skills/setup/SKILL.md +18 -27
  22. package/src/headless-exec.mjs +3 -10
  23. package/src/headless-exec.test.mjs +2 -7
  24. package/src/rules/agent/00-common.md +4 -7
  25. package/src/rules/lead/01-general.md +0 -3
  26. package/src/rules/shared/10-tool-workflow.md +10 -2
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +183 -26
  28. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.test.mjs +378 -0
  29. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
  30. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +109 -0
  31. package/src/runtime/agent/orchestrator/cache-break-trace.test.mjs +115 -0
  32. package/src/runtime/agent/orchestrator/config.mjs +13 -68
  33. package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
  34. package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
  35. package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
  36. package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
  37. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
  38. package/src/runtime/agent/orchestrator/providers/admission-scheduler-metrics.test.mjs +47 -0
  39. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +16 -3
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
  41. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  43. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  44. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
  45. package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
  46. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +19 -3
  47. package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
  48. package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
  49. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -9
  50. package/src/runtime/agent/orchestrator/providers/media-parity.test.mjs +59 -0
  51. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
  52. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +71 -0
  53. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +18 -26
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
  56. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
  57. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
  58. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +47 -2
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +103 -12
  60. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-turn-state.mjs +68 -0
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-delta-parity.test.mjs +122 -6
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +36 -11
  64. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +144 -93
  65. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +18 -13
  66. package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +288 -0
  67. package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
  68. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
  69. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
  70. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +86 -7
  71. package/src/runtime/agent/orchestrator/session/agent-loop.test.mjs +222 -0
  72. package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.mjs +61 -0
  73. package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.test.mjs +83 -0
  74. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
  75. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +26 -2
  76. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +112 -0
  77. package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +192 -0
  78. package/src/runtime/agent/orchestrator/session/context-usage-restart.test.mjs +72 -0
  79. package/src/runtime/agent/orchestrator/session/context-utils.mjs +100 -15
  80. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +17 -1
  81. package/src/runtime/agent/orchestrator/session/eager-dispatch.test.mjs +74 -0
  82. package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +12 -8
  83. package/src/runtime/agent/orchestrator/session/image-strip-recovery.test.mjs +20 -1
  84. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +110 -2
  85. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +3 -16
  86. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.test.mjs +28 -0
  87. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
  88. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +6 -0
  89. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -1
  90. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -0
  91. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +111 -39
  92. package/src/runtime/agent/orchestrator/session/manager/ask-session.test.mjs +46 -0
  93. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -6
  94. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
  95. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +27 -3
  96. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +26 -54
  97. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +63 -1
  98. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +3 -0
  99. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -110
  100. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +580 -0
  101. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +593 -0
  102. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +119 -190
  103. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
  104. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +43 -8
  105. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +129 -12
  106. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +9 -2
  107. package/src/runtime/agent/orchestrator/session/store/listing.mjs +8 -1
  108. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +15 -1
  109. package/src/runtime/agent/orchestrator/session/store-live-publication.test.mjs +83 -0
  110. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +36 -2
  111. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +157 -30
  112. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +64 -0
  113. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +57 -0
  114. package/src/runtime/agent/orchestrator/session/store-summary-visibility.test.mjs +219 -0
  115. package/src/runtime/agent/orchestrator/session/store.mjs +26 -2
  116. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +22 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
  118. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
  119. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
  120. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
  121. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
  122. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
  123. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
  124. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
  125. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
  126. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +6 -6
  129. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
  130. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
  131. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
  132. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
  135. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
  136. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
  137. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  138. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  139. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +12 -0
  140. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  141. package/src/runtime/attachments/pdf-extract.mjs +3 -0
  142. package/src/runtime/attachments/pdfjs-runtime.mjs +13 -0
  143. package/src/runtime/bridge-clients.test.mjs +827 -0
  144. package/src/runtime/browser-bridge/action-schema.mjs +251 -0
  145. package/src/runtime/browser-bridge/client.mjs +152 -0
  146. package/src/runtime/browser-bridge/tool-defs.mjs +159 -0
  147. package/src/runtime/computer-bridge/action-schema.mjs +653 -0
  148. package/src/runtime/computer-bridge/client.mjs +231 -0
  149. package/src/runtime/computer-bridge/tool-defs.mjs +33 -0
  150. package/src/runtime/media/store.mjs +18 -8
  151. package/src/runtime/media/store.test.mjs +14 -0
  152. package/src/runtime/memory/index.mjs +1 -1
  153. package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
  154. package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
  155. package/src/runtime/office/assurance-benchmark.mjs +358 -0
  156. package/src/runtime/office/assurance.mjs +833 -0
  157. package/src/runtime/office/benchmark.mjs +496 -0
  158. package/src/runtime/office/capabilities.mjs +551 -0
  159. package/src/runtime/office/com-adapter.mjs +283 -0
  160. package/src/runtime/office/content-model.mjs +183 -0
  161. package/src/runtime/office/contract-benchmark.mjs +105 -0
  162. package/src/runtime/office/design-library.mjs +1562 -0
  163. package/src/runtime/office/design-system.mjs +1966 -0
  164. package/src/runtime/office/index.mjs +2413 -0
  165. package/src/runtime/office/journal.mjs +82 -0
  166. package/src/runtime/office/office-assurance.test.mjs +371 -0
  167. package/src/runtime/office/office-com-host.ps1 +3781 -0
  168. package/src/runtime/office/office-com-session-host.ps1 +855 -0
  169. package/src/runtime/office/office-live-runtime.test.mjs +819 -0
  170. package/src/runtime/office/office-runtime.test.mjs +2070 -0
  171. package/src/runtime/office/ooxml-validator.mjs +271 -0
  172. package/src/runtime/office/pagination.mjs +132 -0
  173. package/src/runtime/office/pdf-adapter.mjs +591 -0
  174. package/src/runtime/office/pdf-analysis.mjs +372 -0
  175. package/src/runtime/office/pdf-render.mjs +167 -0
  176. package/src/runtime/office/pdf-security.mjs +63 -0
  177. package/src/runtime/office/polish-benchmark.mjs +190 -0
  178. package/src/runtime/office/portable-ooxml.mjs +1867 -0
  179. package/src/runtime/office/quality-live-benchmark.mjs +486 -0
  180. package/src/runtime/office/quality-pipeline.mjs +136 -0
  181. package/src/runtime/office/tabular.mjs +302 -0
  182. package/src/runtime/office/templates/mixdog-executive.pptx +0 -0
  183. package/src/runtime/office/templates/mixdog-executive.pptx.mixdog.json +190 -0
  184. package/src/runtime/office/tool-defs.mjs +113 -0
  185. package/src/runtime/office/visual-diff.mjs +89 -0
  186. package/src/runtime/office/xlsx-assertions.mjs +172 -0
  187. package/src/runtime/office/xlsx-contract.mjs +147 -0
  188. package/src/runtime/shared/agent-route-config.mjs +9 -0
  189. package/src/runtime/shared/child-spawn-gate.mjs +17 -0
  190. package/src/runtime/shared/child-spawn-remote.mjs +69 -7
  191. package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
  192. package/src/runtime/shared/pristine-execution-contract.json +2 -1
  193. package/src/runtime/shared/provider-api-key.mjs +1 -0
  194. package/src/runtime/shared/session-runtime-health.mjs +91 -0
  195. package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
  196. package/src/runtime/shared/skill-document.mjs +86 -0
  197. package/src/runtime/shared/skill-document.test.mjs +81 -0
  198. package/src/runtime/shared/tool-execution-contract.mjs +16 -0
  199. package/src/runtime/shared/tool-surface.mjs +27 -0
  200. package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
  201. package/src/runtime/shared/turn-snapshot.mjs +32 -9
  202. package/src/runtime/shared/user-cwd.mjs +33 -14
  203. package/src/runtime/shared/user-cwd.test.mjs +43 -0
  204. package/src/session-runtime/agent-disable.test.mjs +20 -1
  205. package/src/session-runtime/context-status.mjs +9 -3
  206. package/src/session-runtime/cwd-plugins.mjs +26 -26
  207. package/src/session-runtime/cwd-plugins.test.mjs +75 -0
  208. package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
  209. package/src/session-runtime/deferred-tool-delta.mjs +104 -0
  210. package/src/session-runtime/deferred-tool-delta.test.mjs +40 -0
  211. package/src/session-runtime/global-extensions.mjs +26 -0
  212. package/src/session-runtime/global-extensions.test.mjs +28 -0
  213. package/src/session-runtime/global-settings-session-identity.test.mjs +76 -0
  214. package/src/session-runtime/goal-runtime.mjs +1141 -0
  215. package/src/session-runtime/goal-runtime.test.mjs +645 -0
  216. package/src/session-runtime/lifecycle-api.mjs +58 -11
  217. package/src/session-runtime/lifecycle-api.test.mjs +183 -0
  218. package/src/session-runtime/mcp-glue.mjs +49 -47
  219. package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
  220. package/src/session-runtime/plugin-mcp.mjs +96 -22
  221. package/src/session-runtime/prewarm.mjs +1 -10
  222. package/src/session-runtime/provider-request-snapshot.mjs +33 -9
  223. package/src/session-runtime/provider-request-snapshot.test.mjs +27 -5
  224. package/src/session-runtime/resource-api-global.test.mjs +109 -0
  225. package/src/session-runtime/resource-api.mjs +168 -116
  226. package/src/session-runtime/runtime-core.mjs +208 -29
  227. package/src/session-runtime/runtime-tunables.mjs +6 -8
  228. package/src/session-runtime/session-lifecycle.mjs +34 -12
  229. package/src/session-runtime/session-title.mjs +2 -2
  230. package/src/session-runtime/session-turn-api.mjs +37 -25
  231. package/src/session-runtime/skills-api.mjs +79 -25
  232. package/src/session-runtime/skills-api.test.mjs +99 -0
  233. package/src/session-runtime/tool-catalog-data.mjs +1 -1
  234. package/src/session-runtime/tool-catalog-schema.mjs +0 -5
  235. package/src/session-runtime/tool-catalog.mjs +42 -112
  236. package/src/session-runtime/tool-defs.mjs +6 -1
  237. package/src/session-runtime/tool-policy-surface.test.mjs +93 -17
  238. package/src/session-runtime/tool-surface.mjs +30 -8
  239. package/src/session-runtime/workflow-agents-api.mjs +8 -25
  240. package/src/session-runtime/workflow.mjs +8 -6
  241. package/src/standalone/agent-dispatch-broker.mjs +5 -9
  242. package/src/standalone/agent-tool/helpers.mjs +2 -2
  243. package/src/standalone/agent-tool/job-views.mjs +3 -1
  244. package/src/standalone/agent-tool/notify.mjs +11 -9
  245. package/src/standalone/agent-tool/notify.test.mjs +37 -0
  246. package/src/standalone/agent-tool/render.mjs +3 -0
  247. package/src/standalone/agent-tool/spawn-flow.mjs +61 -16
  248. package/src/standalone/agent-tool/spawn-flow.test.mjs +41 -0
  249. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  250. package/src/standalone/agent-tool/worker-index.mjs +123 -0
  251. package/src/standalone/agent-tool/worker-index.test.mjs +132 -0
  252. package/src/standalone/agent-tool/worker-rows.mjs +2 -1
  253. package/src/standalone/agent-tool.mjs +57 -10
  254. package/src/standalone/daemon.mjs +113 -15
  255. package/src/standalone/hook-bus/config.mjs +1 -0
  256. package/src/standalone/plugin-admin.mjs +17 -0
  257. package/src/standalone/plugin-admin.test.mjs +7 -0
  258. package/src/standalone/provider-admin.mjs +1 -0
  259. package/src/standalone/session-protocol.mjs +5 -0
  260. package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
  261. package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
  262. package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
  263. package/src/standalone/session-runtime-host-factory.mjs +17 -0
  264. package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
  265. package/src/standalone/session-runtime-host-health.test.mjs +5 -0
  266. package/src/standalone/session-runtime-host.mjs +709 -58
  267. package/src/standalone/session-runtime-inline-host.mjs +258 -0
  268. package/src/standalone/session-runtime-inline-host.test.mjs +385 -0
  269. package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
  270. package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
  271. package/src/standalone/session-runtime-shard-host.test.mjs +642 -0
  272. package/src/standalone/session-runtime-shard-router.mjs +149 -0
  273. package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
  274. package/src/standalone/session-runtime-worker.mjs +302 -26
  275. package/src/standalone/session-service-agent-tree.test.mjs +984 -0
  276. package/src/standalone/session-service-goal-recovery.test.mjs +177 -0
  277. package/src/standalone/session-service.mjs +711 -11
  278. package/src/tui/app/app-view.jsx +1 -0
  279. package/src/tui/app/core-memory-picker.mjs +2 -0
  280. package/src/tui/app/extension-pickers.mjs +1 -1
  281. package/src/tui/app/maintenance-pickers.mjs +3 -3
  282. package/src/tui/app/model-options.mjs +2 -0
  283. package/src/tui/app/model-picker.mjs +14 -4
  284. package/src/tui/app/panel-handoff.test.mjs +311 -0
  285. package/src/tui/app/project-picker.mjs +1 -0
  286. package/src/tui/app/route-pickers.mjs +21 -6
  287. package/src/tui/app/settings-picker.mjs +26 -3
  288. package/src/tui/app/slash-commands.mjs +1 -0
  289. package/src/tui/app/slash-dispatch.mjs +73 -22
  290. package/src/tui/app/theme-effort-pickers.mjs +7 -2
  291. package/src/tui/components/Picker.jsx +10 -1
  292. package/src/tui/dist/index.mjs +187 -45
  293. package/src/tui/lib/voice-setup.mjs +4 -1
  294. package/src/tui/session/agent-message-source.test.mjs +37 -0
  295. package/src/tui/session/completion-card-restore.test.mjs +50 -0
  296. package/src/tui/session/context-state.mjs +3 -13
  297. package/src/tui/session/goal-continuation.mjs +141 -0
  298. package/src/tui/session/goal-continuation.test.mjs +203 -0
  299. package/src/tui/session/queue-helpers.mjs +5 -2
  300. package/src/tui/session/session-action-surface.test.mjs +190 -0
  301. package/src/tui/session/session-api-ext.mjs +48 -8
  302. package/src/tui/session/session-api.mjs +73 -1
  303. package/src/tui/session/session-flow.mjs +31 -1
  304. package/src/tui/session/turn.mjs +88 -15
  305. package/src/tui/session-local.mjs +39 -3
  306. package/src/vendor/statusline/src/gateway/route-meta.mjs +31 -0
  307. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
@@ -0,0 +1,124 @@
1
+ // Machine-wide child-spawn budget: every runtime shard must stay on the
2
+ // daemon-owned gate. Two ways that silently breaks (and multiplies the cap by
3
+ // the shard count) are covered here: an inherited env flag activating the
4
+ // process-local lane in a grandchild, and one failed lease latching a shard
5
+ // off the remote budget for the rest of its life.
6
+ import assert from 'node:assert/strict';
7
+ import { fork } from 'node:child_process';
8
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import test from 'node:test';
13
+
14
+ import { isSessionRuntimeWorkerProcess } from './child-spawn-remote.mjs';
15
+
16
+ const REMOTE_URL = new URL('./child-spawn-remote.mjs', import.meta.url).href;
17
+ const GATE_URL = new URL('./child-spawn-gate.mjs', import.meta.url).href;
18
+
19
+ const CHILD_SOURCE = `
20
+ const inherited = process.env.STUB_PID_MODE === 'inherited';
21
+ process.env.MIXDOG_SESSION_RUNTIME_WORKER = '1';
22
+ // A grandchild inherits BOTH variables; only its own pid differs.
23
+ process.env.MIXDOG_SESSION_RUNTIME_WORKER_PID = inherited
24
+ ? String(process.pid + 1)
25
+ : String(process.pid);
26
+ const remote = await import(process.env.REMOTE_URL);
27
+ const gate = await import(process.env.GATE_URL);
28
+ const report = { enabled: remote.remoteSpawnLeasesEnabled(), mode: gate.snapshot().mode };
29
+ if (!inherited) {
30
+ try {
31
+ await remote.acquireRemoteSpawnLease({ lane: 'search', ownerKey: 'test', waitTimeoutMs: 30000 });
32
+ report.firstLease = 'granted';
33
+ } catch (error) {
34
+ report.firstLease = String(error?.code || error?.message || error);
35
+ }
36
+ report.enabledAfterFailure = remote.remoteSpawnLeasesEnabled();
37
+ report.modeAfterFailure = gate.snapshot().mode;
38
+ const release = await remote.acquireRemoteSpawnLease({ lane: 'search', ownerKey: 'test', waitTimeoutMs: 30000 });
39
+ report.secondLease = typeof release === 'function' ? 'granted' : 'unexpected';
40
+ release();
41
+ }
42
+ process.send({ type: 'report', report });
43
+ setTimeout(() => process.exit(0), 50).unref?.();
44
+ `;
45
+
46
+ async function runChild(mode, { onLease = () => {} } = {}) {
47
+ const dir = await mkdtemp(join(tmpdir(), 'mixdog-spawn-remote-'));
48
+ const entry = join(dir, 'child.mjs');
49
+ await writeFile(entry, CHILD_SOURCE, 'utf8');
50
+ try {
51
+ return await new Promise((resolve, reject) => {
52
+ const child = fork(entry, [], {
53
+ stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
54
+ env: {
55
+ ...process.env,
56
+ STUB_PID_MODE: mode,
57
+ REMOTE_URL,
58
+ GATE_URL,
59
+ },
60
+ });
61
+ const timer = setTimeout(() => {
62
+ try { child.kill(); } catch {}
63
+ reject(new Error('child spawn-lease probe timed out'));
64
+ }, 20_000);
65
+ let stderr = '';
66
+ child.stderr?.on('data', (chunk) => { stderr += String(chunk); });
67
+ child.on('message', (message) => {
68
+ if (message?.type === 'spawn-lease') {
69
+ onLease(child, message);
70
+ return;
71
+ }
72
+ if (message?.type !== 'report') return;
73
+ clearTimeout(timer);
74
+ try { child.kill(); } catch {}
75
+ resolve({ ...message.report, stderr });
76
+ });
77
+ child.on('error', reject);
78
+ });
79
+ } finally {
80
+ await rm(dir, { recursive: true, force: true });
81
+ }
82
+ }
83
+
84
+ test('only the pid-scoped runtime worker uses the machine-wide spawn budget', () => {
85
+ assert.equal(isSessionRuntimeWorkerProcess({ MIXDOG_SESSION_RUNTIME_WORKER_PID: '4242' }, 4242), true);
86
+ assert.equal(isSessionRuntimeWorkerProcess({ MIXDOG_SESSION_RUNTIME_WORKER_PID: '4242' }, 99), false);
87
+ // The inheritable flag alone can never claim the runtime IPC channel.
88
+ assert.equal(isSessionRuntimeWorkerProcess({ MIXDOG_SESSION_RUNTIME_WORKER: '1' }, 99), false);
89
+ assert.equal(isSessionRuntimeWorkerProcess({}, 99), false);
90
+ });
91
+
92
+ test('a grandchild that inherited the worker flag stays on its own local lane', async () => {
93
+ const report = await runChild('inherited');
94
+ assert.equal(report.enabled, false);
95
+ assert.equal(report.mode, 'local');
96
+ });
97
+
98
+ test('a failed lease never latches a shard off the machine-wide budget', async () => {
99
+ let leases = 0;
100
+ const report = await runChild('shard', {
101
+ onLease: (child, message) => {
102
+ leases += 1;
103
+ // First lease fails the way a momentarily unreachable pool does.
104
+ child.send(leases === 1
105
+ ? {
106
+ type: 'spawn-lease-result',
107
+ leaseId: message.leaseId,
108
+ ok: false,
109
+ error: 'pool channel unavailable',
110
+ code: 'ELEASEFALLBACK',
111
+ }
112
+ : { type: 'spawn-lease-result', leaseId: message.leaseId, ok: true });
113
+ },
114
+ });
115
+ assert.equal(report.enabled, true);
116
+ assert.equal(report.mode, 'remote-lease');
117
+ assert.equal(report.firstLease, 'ELEASEFALLBACK');
118
+ // Recovery: the shard is still on the daemon budget and the next lease is
119
+ // granted remotely instead of silently using a per-process lane.
120
+ assert.equal(report.enabledAfterFailure, true);
121
+ assert.equal(report.modeAfterFailure, 'remote-lease');
122
+ assert.equal(report.secondLease, 'granted');
123
+ assert.equal(leases, 2);
124
+ });
@@ -67,7 +67,8 @@
67
67
  "gemini": "GEMINI_API_KEY",
68
68
  "deepseek": "DEEPSEEK_API_KEY",
69
69
  "xai": "XAI_API_KEY",
70
- "opencode-go": "OPENCODE_API_KEY"
70
+ "opencode-go": "OPENCODE_API_KEY",
71
+ "openrouter": "OPENROUTER_API_KEY"
71
72
  },
72
73
  "personalStateCounters": [
73
74
  "behavioralStateFilesCopied",
@@ -13,6 +13,7 @@ export const AGENT_PROVIDER_ENV = Object.freeze({
13
13
  deepseek: 'DEEPSEEK_API_KEY',
14
14
  xai: 'XAI_API_KEY',
15
15
  'opencode-go': 'OPENCODE_API_KEY',
16
+ openrouter: 'OPENROUTER_API_KEY',
16
17
  });
17
18
 
18
19
  export const AGENT_PROVIDER_ENV_ALIASES = Object.freeze({
@@ -1,4 +1,5 @@
1
1
  import { normalize } from 'node:path';
2
+ import { monitorEventLoopDelay } from 'node:perf_hooks';
2
3
 
3
4
  export const SESSION_RUNTIME_WORKER_UNHEALTHY_EVENT = 'mixdog:session-runtime-worker-unhealthy';
4
5
 
@@ -67,6 +68,96 @@ export function reportRuntimeDirectoryReadFailure(path, error, now = Date.now())
67
68
  });
68
69
  }
69
70
 
71
+ // ── Per-shard event-loop lag ───────────────────────────────────────────────
72
+ // Each runtime shard runs provider parsing, transcript projection and tool
73
+ // results on its OWN event loop, so lag is the per-shard saturation signal the
74
+ // daemon cannot see (it only measures itself). Lag is DIAGNOSTIC + ROUTING
75
+ // input, never a kill signal: a saturated shard still owns accepted input and
76
+ // in-flight turns, so the host quarantines it from NEW placement instead of
77
+ // recycling it out from under that work.
78
+ export const RUNTIME_LAG_DEFAULTS = Object.freeze({
79
+ intervalMs: 5_000,
80
+ warnP99Ms: 250,
81
+ degradedP99Ms: 2_000,
82
+ degradedSamples: 3,
83
+ recoverP99Ms: 750,
84
+ });
85
+
86
+ function lagMs(value) {
87
+ return Number.isFinite(value) ? Math.round(value / 1e6) : 0;
88
+ }
89
+
90
+ /**
91
+ * Sample this process's event-loop delay on a fixed interval.
92
+ * Returns a stop function; the timer is unref'd so it never holds the process.
93
+ */
94
+ export function startRuntimeEventLoopLagMonitor({
95
+ intervalMs = RUNTIME_LAG_DEFAULTS.intervalMs,
96
+ resolution = 20,
97
+ onSample = () => {},
98
+ } = {}) {
99
+ const histogram = monitorEventLoopDelay({ resolution });
100
+ histogram.enable();
101
+ const timer = setInterval(() => {
102
+ const sample = {
103
+ p50Ms: lagMs(histogram.percentile(50)),
104
+ p95Ms: lagMs(histogram.percentile(95)),
105
+ p99Ms: lagMs(histogram.percentile(99)),
106
+ maxMs: lagMs(histogram.max),
107
+ meanMs: lagMs(histogram.mean),
108
+ intervalMs,
109
+ at: Date.now(),
110
+ };
111
+ histogram.reset();
112
+ try { onSample(sample); } catch { /* telemetry must never throw upward */ }
113
+ }, intervalMs);
114
+ timer.unref?.();
115
+ return () => {
116
+ clearInterval(timer);
117
+ try { histogram.disable(); } catch {}
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Hysteresis state machine over lag samples: a shard becomes `degraded` only
123
+ * after repeated sustained lag and recovers once it drops well below the
124
+ * trigger, so a single GC pause never quarantines a healthy shard.
125
+ */
126
+ export function createRuntimeLagTracker(overrides = {}) {
127
+ const config = { ...RUNTIME_LAG_DEFAULTS, ...(overrides || {}) };
128
+ let consecutive = 0;
129
+ let degraded = false;
130
+ let sample = null;
131
+ return {
132
+ get config() { return config; },
133
+ get degraded() { return degraded; },
134
+ get sample() { return sample; },
135
+ record(next) {
136
+ sample = next && typeof next === 'object' ? next : null;
137
+ const p99 = Number(sample?.p99Ms) || 0;
138
+ if (p99 >= config.degradedP99Ms) {
139
+ consecutive += 1;
140
+ if (!degraded && consecutive >= config.degradedSamples) {
141
+ degraded = true;
142
+ return { degraded, changed: true, sample, consecutive };
143
+ }
144
+ return { degraded, changed: false, sample, consecutive };
145
+ }
146
+ consecutive = 0;
147
+ if (degraded && p99 <= config.recoverP99Ms) {
148
+ degraded = false;
149
+ return { degraded, changed: true, sample, consecutive };
150
+ }
151
+ return { degraded, changed: false, sample, consecutive };
152
+ },
153
+ reset() {
154
+ consecutive = 0;
155
+ degraded = false;
156
+ sample = null;
157
+ },
158
+ };
159
+ }
160
+
70
161
  export function reportRuntimeAbortListenerPressure(warning, now = Date.now(), retainedListeners = 0) {
71
162
  if (!isCurrentSessionRuntimeWorker()) return false;
72
163
  if (!Number.isFinite(Number(retainedListeners)) || Number(retainedListeners) <= 50) {
@@ -2,10 +2,63 @@ import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
3
 
4
4
  import {
5
+ createRuntimeLagTracker,
5
6
  recordRuntimeDirectoryReadSuccess,
6
7
  reportRuntimeAbortListenerPressure,
8
+ RUNTIME_LAG_DEFAULTS,
9
+ startRuntimeEventLoopLagMonitor,
7
10
  } from './session-runtime-health.mjs';
8
11
 
12
+ test('sustained event-loop lag degrades a shard, a single spike does not', () => {
13
+ const tracker = createRuntimeLagTracker();
14
+ const spike = { p99Ms: RUNTIME_LAG_DEFAULTS.degradedP99Ms + 500 };
15
+ assert.equal(tracker.record(spike).degraded, false);
16
+ assert.equal(tracker.record(spike).degraded, false);
17
+ const degraded = tracker.record(spike);
18
+ assert.equal(degraded.degraded, true);
19
+ assert.equal(degraded.changed, true);
20
+ // Already degraded: no repeated transition.
21
+ assert.equal(tracker.record(spike).changed, false);
22
+ assert.equal(tracker.degraded, true);
23
+
24
+ // Hysteresis: still-elevated lag keeps the quarantine, a clean sample lifts it.
25
+ assert.equal(tracker.record({ p99Ms: RUNTIME_LAG_DEFAULTS.degradedP99Ms - 1 }).degraded, true);
26
+ const recovered = tracker.record({ p99Ms: 10 });
27
+ assert.equal(recovered.degraded, false);
28
+ assert.equal(recovered.changed, true);
29
+
30
+ // A fresh process starts from a clean slate.
31
+ tracker.record(spike);
32
+ tracker.reset();
33
+ assert.equal(tracker.degraded, false);
34
+ assert.equal(tracker.sample, null);
35
+ });
36
+
37
+ test('the event-loop lag monitor samples this process and stops cleanly', async () => {
38
+ const samples = [];
39
+ const stop = startRuntimeEventLoopLagMonitor({
40
+ intervalMs: 20,
41
+ onSample: (sample) => samples.push(sample),
42
+ });
43
+ try {
44
+ const deadline = Date.now() + 2_000;
45
+ while (samples.length === 0 && Date.now() < deadline) {
46
+ await new Promise((resolve) => setTimeout(resolve, 20));
47
+ }
48
+ } finally {
49
+ stop();
50
+ }
51
+ assert.ok(samples.length > 0, 'monitor produced at least one sample');
52
+ const [sample] = samples;
53
+ for (const key of ['p50Ms', 'p95Ms', 'p99Ms', 'maxMs']) {
54
+ assert.equal(Number.isFinite(sample[key]), true, `${key} is numeric`);
55
+ assert.ok(sample[key] >= 0);
56
+ }
57
+ const seen = samples.length;
58
+ await new Promise((resolve) => setTimeout(resolve, 80));
59
+ assert.equal(samples.length, seen, 'no samples after stop');
60
+ });
61
+
9
62
  test('repeated AbortSignal listener pressure marks only the session runtime worker unhealthy', () => {
10
63
  const originalRuntimeWorkerPid = process.env.MIXDOG_SESSION_RUNTIME_WORKER_PID;
11
64
  let detail = null;
@@ -0,0 +1,86 @@
1
+ import { parseDocument } from 'yaml';
2
+
3
+ const SKILL_FRONTMATTER_RE =
4
+ /^(?:\uFEFF)?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
5
+
6
+ const scalarText = (value, field) => {
7
+ if (typeof value !== 'string') {
8
+ throw new Error(`Skill ${field} must be a string.`);
9
+ }
10
+ return value.trim();
11
+ };
12
+
13
+ export function validateSkillName(value) {
14
+ const name = String(value || '').trim();
15
+ if (!name) throw new Error('Skill name is required.');
16
+ if (name.length > 64) throw new Error('Skill name must be 64 characters or fewer.');
17
+ if (!/^[a-z0-9-]+$/.test(name)) {
18
+ throw new Error('Skill name may contain only lowercase letters, numbers, and hyphens.');
19
+ }
20
+ if (name.startsWith('-') || name.endsWith('-') || name.includes('--')) {
21
+ throw new Error('Skill name cannot start or end with a hyphen or contain consecutive hyphens.');
22
+ }
23
+ return name;
24
+ }
25
+
26
+ export function validateSkillDescription(value) {
27
+ const description = String(value || '').trim();
28
+ if (!description) throw new Error('Skill trigger is required.');
29
+ if (description.length > 1024) {
30
+ throw new Error('Skill trigger must be 1024 characters or fewer.');
31
+ }
32
+ return description;
33
+ }
34
+
35
+ function parseFrontmatterDocument(markdown) {
36
+ const source = String(markdown || '');
37
+ const match = source.match(SKILL_FRONTMATTER_RE);
38
+ if (!match) throw new Error('SKILL.md must start with YAML frontmatter.');
39
+ const document = parseDocument(match[1], {
40
+ prettyErrors: false,
41
+ strict: true,
42
+ uniqueKeys: true,
43
+ });
44
+ if (document.errors.length) {
45
+ throw new Error(`Invalid SKILL.md frontmatter: ${document.errors[0].message}`);
46
+ }
47
+ const frontmatter = document.toJS({ maxAliasCount: 100 });
48
+ if (!frontmatter || typeof frontmatter !== 'object' || Array.isArray(frontmatter)) {
49
+ throw new Error('SKILL.md frontmatter must be a YAML mapping.');
50
+ }
51
+ return { source, match, document, frontmatter };
52
+ }
53
+
54
+ export function parseSkillDocument(markdown) {
55
+ const parsed = parseFrontmatterDocument(markdown);
56
+ const name = validateSkillName(scalarText(parsed.frontmatter.name, 'name'));
57
+ const description = validateSkillDescription(
58
+ scalarText(parsed.frontmatter.description, 'description'),
59
+ );
60
+ return {
61
+ name,
62
+ description,
63
+ body: parsed.source.slice(parsed.match[0].length).replace(/^\r?\n/, ''),
64
+ frontmatter: parsed.frontmatter,
65
+ };
66
+ }
67
+
68
+ function renderSkillDocument(document, body) {
69
+ const instructions = String(body || '').replace(/\r\n/g, '\n').trim();
70
+ if (!instructions) throw new Error('Skill instructions are required.');
71
+ return `---\n${String(document).trimEnd()}\n---\n\n${instructions}\n`;
72
+ }
73
+
74
+ export function createSkillDocument({ name, description, body }) {
75
+ const document = parseDocument('');
76
+ document.set('name', validateSkillName(name));
77
+ document.set('description', validateSkillDescription(description));
78
+ return renderSkillDocument(document, body);
79
+ }
80
+
81
+ export function updateSkillDocument(markdown, { name, description, body }) {
82
+ const parsed = parseFrontmatterDocument(markdown);
83
+ parsed.document.set('name', validateSkillName(name));
84
+ parsed.document.set('description', validateSkillDescription(description));
85
+ return renderSkillDocument(parsed.document, body);
86
+ }
@@ -0,0 +1,81 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ createSkillDocument,
6
+ parseSkillDocument,
7
+ updateSkillDocument,
8
+ validateSkillName,
9
+ } from './skill-document.mjs';
10
+
11
+ test('parses standard multiline YAML and the Markdown instructions body', () => {
12
+ const parsed = parseSkillDocument([
13
+ '---',
14
+ 'name: imported-skill',
15
+ 'description: >',
16
+ ' Use when an imported skill',
17
+ ' needs multiline matching.',
18
+ 'metadata:',
19
+ ' author: example',
20
+ 'allowed-tools:',
21
+ ' - shell',
22
+ '---',
23
+ '',
24
+ '# Instructions',
25
+ '',
26
+ 'Read `references/guide.md`.',
27
+ '',
28
+ ].join('\n'));
29
+
30
+ assert.equal(parsed.name, 'imported-skill');
31
+ assert.equal(parsed.description,
32
+ 'Use when an imported skill needs multiline matching.');
33
+ assert.deepEqual(parsed.frontmatter.metadata, { author: 'example' });
34
+ assert.deepEqual(parsed.frontmatter['allowed-tools'], ['shell']);
35
+ assert.match(parsed.body, /^# Instructions/);
36
+ });
37
+
38
+ test('updates the three editable fields without dropping optional frontmatter', () => {
39
+ const source = [
40
+ '---',
41
+ '# retained comment',
42
+ 'name: old-name',
43
+ 'description: Old trigger.',
44
+ 'license: MIT',
45
+ 'compatibility: Requires Python.',
46
+ 'metadata:',
47
+ ' author: example',
48
+ 'allowed-tools: shell',
49
+ '---',
50
+ '',
51
+ 'Old instructions.',
52
+ '',
53
+ ].join('\n');
54
+
55
+ const updated = updateSkillDocument(source, {
56
+ name: 'new-name',
57
+ description: 'Use when the new trigger matches.',
58
+ body: '# New instructions',
59
+ });
60
+ const parsed = parseSkillDocument(updated);
61
+
62
+ assert.equal(parsed.name, 'new-name');
63
+ assert.equal(parsed.description, 'Use when the new trigger matches.');
64
+ assert.equal(parsed.body, '# New instructions\n');
65
+ assert.equal(parsed.frontmatter.license, 'MIT');
66
+ assert.equal(parsed.frontmatter.compatibility, 'Requires Python.');
67
+ assert.deepEqual(parsed.frontmatter.metadata, { author: 'example' });
68
+ assert.equal(parsed.frontmatter['allowed-tools'], 'shell');
69
+ assert.match(updated, /# retained comment/);
70
+ });
71
+
72
+ test('creates a standard document and rejects non-standard names', () => {
73
+ const source = createSkillDocument({
74
+ name: 'project-skill',
75
+ description: 'Use when this project needs the skill.',
76
+ body: '# Instructions',
77
+ });
78
+ assert.equal(parseSkillDocument(source).name, 'project-skill');
79
+ assert.throws(() => validateSkillName('Project_Skill'), /lowercase letters/);
80
+ assert.throws(() => validateSkillName('project--skill'), /consecutive hyphens/);
81
+ });
@@ -185,6 +185,22 @@ const INTERNAL_TRANSCRIPT_CONTEXT_RE =
185
185
  const INTERNAL_TRANSCRIPT_SYNTHETIC_RE =
186
186
  /^(?:\[mixdog-runtime\]|A previous model worked on this task and produced the compacted handoff summary below\b|Re-attached after compaction\b|Reference files:\s)/i;
187
187
  const INTERNAL_TRANSCRIPT_ASYNC_HEAD_RE = /^Async .+ finished\./i;
188
+ const TRANSCRIPT_HIDDEN_CONTROL_TOOL_NAMES = new Set([
189
+ 'goal',
190
+ 'create_goal',
191
+ 'get_goal',
192
+ 'set_goal_tasks',
193
+ 'update_goal',
194
+ 'load_tool',
195
+ 'tool_search',
196
+ 'skill',
197
+ ]);
198
+
199
+ export function isTranscriptHiddenControlToolName(name) {
200
+ const normalized = clean(name).toLowerCase().replace(/^functions\./, '');
201
+ return TRANSCRIPT_HIDDEN_CONTROL_TOOL_NAMES.has(normalized);
202
+ }
203
+
188
204
  // Persisted USER cancellation control rows ("[Request interrupted by user]"
189
205
  // and its tool-use variant) exist for the next model step, not for humans:
190
206
  // the human already saw the cancel they typed. The live engine path already
@@ -153,6 +153,12 @@ export function displayToolName(name, args = {}) {
153
153
  case 'web_fetch':
154
154
  case 'fetch':
155
155
  return 'Fetch';
156
+ case 'browser':
157
+ return 'Browser';
158
+ case 'computer':
159
+ return 'Computer';
160
+ case 'office':
161
+ return 'Office';
156
162
  case 'list_mcp_resources':
157
163
  case 'list_mcp_resource_templates':
158
164
  case 'cwd':
@@ -275,6 +281,21 @@ export function summarizeToolArgs(name, args, { max = DEFAULT_SUMMARY_MAX } = {}
275
281
  return formatCountedUnit(collectionCount(a.url, a.uri), 'URL', 'URLs');
276
282
  }
277
283
  return truncateToolText(a.url || a.uri || '', max);
284
+ case 'browser':
285
+ return compactParts([
286
+ String(a.action || ''),
287
+ a.url ? truncateToolText(a.url, max) : a.ref ? String(a.ref) : '',
288
+ ]);
289
+ case 'computer':
290
+ return compactParts([
291
+ String(a.action || ''),
292
+ a.window ? truncateToolText(a.window, max) : a.ref ? String(a.ref) : a.app ? truncateToolText(a.app, max) : '',
293
+ ]);
294
+ case 'office':
295
+ return compactParts([
296
+ String(a.action || ''),
297
+ a.path ? truncateToolText(a.path, max) : a.session ? String(a.session) : '',
298
+ ]);
278
299
  case 'read_mcp_resource':
279
300
  return truncateToolText(a.uri || '', max);
280
301
  case 'list_mcp_resources':
@@ -559,6 +580,12 @@ export function toolWorkUnit(name, args = {}, category = '') {
559
580
  return unitDescriptor('Web Research', { count: queryCount(a, 'query', 'queries', 'keywords') || 1, noun: 'query', pluralNoun: 'queries' });
560
581
  case 'web_fetch':
561
582
  return unitDescriptor('Web Research', { count: queryCount(a, 'url', 'urls', 'uri', 'uris') || 1, active: 'Fetching', done: 'Fetched', noun: 'URL', pluralNoun: 'URLs' });
583
+ case 'browser':
584
+ return unitDescriptor('Browser', { count: 1, active: 'Browsing', done: 'Browsed', noun: 'action' });
585
+ case 'computer':
586
+ return unitDescriptor('Computer', { count: 1, active: 'Operating', done: 'Operated', noun: 'action' });
587
+ case 'office':
588
+ return unitDescriptor('Office', { count: 1, active: 'Editing', done: 'Edited', noun: 'document action' });
562
589
  case 'fetch': {
563
590
  const fetchLimit = Number(a.limit ?? a.messages);
564
591
  const fetchCount = Number.isFinite(fetchLimit) && fetchLimit > 0
@@ -109,6 +109,7 @@ export async function saveTurnSnapshotRecord(sessionId, turn) {
109
109
  const turns = (existing?.turns || []).filter((entry) => entry?.generation !== generation);
110
110
  turns.push({
111
111
  generation,
112
+ checkpointId: clean(turn?.checkpointId),
112
113
  root,
113
114
  baselineTree,
114
115
  // Relative, forward-slashed paths this session's own tools mutated.