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,642 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { createSessionRuntimeHost } from './session-runtime-host.mjs';
8
+ import { shardIndexForKey } from './session-runtime-shard-router.mjs';
9
+
10
+ const SHARDS = 2;
11
+
12
+ async function waitFor(predicate, label, timeoutMs = 8_000) {
13
+ const deadline = Date.now() + timeoutMs;
14
+ while (Date.now() < deadline) {
15
+ try {
16
+ const value = await predicate();
17
+ if (value) return value;
18
+ } catch { /* retry until the deadline */ }
19
+ await new Promise((resolve) => setTimeout(resolve, 25));
20
+ }
21
+ throw new Error(`timed out waiting for ${label}`);
22
+ }
23
+
24
+ function keyForShard(index, prefix, shards = SHARDS) {
25
+ for (let n = 0; n < 2_000; n += 1) {
26
+ const key = `${prefix}-${n}`;
27
+ if (shardIndexForKey(key, shards) === index) return key;
28
+ }
29
+ throw new Error(`no ${prefix} key maps to shard ${index}`);
30
+ }
31
+
32
+ function dispatchIdForShard(index, shards = SHARDS) {
33
+ for (let n = 0; n < 2_000; n += 1) {
34
+ const id = `dispatch-${n}`;
35
+ if (shardIndexForKey(`dispatch:${id}`, shards) === index) return id;
36
+ }
37
+ throw new Error(`no dispatch id maps to shard ${index}`);
38
+ }
39
+
40
+ // Stub shard child: answers the runtime protocol and exposes the shard-level
41
+ // signals (lag samples, unhealthy, provider cooldown) on demand.
42
+ const SHARD_STUB = `
43
+ const SHARD = Number(process.env.MIXDOG_SESSION_RUNTIME_SHARD || 0);
44
+ const revisions = new Map();
45
+ const syncs = [];
46
+ const cancels = [];
47
+ const agentJobs = [];
48
+ const agentNotifications = [];
49
+ const agentControlWaiters = new Map();
50
+ let prewarms = 0;
51
+ function send(message) { if (process.connected) process.send(message); }
52
+ function respond(requestId, value) { if (requestId) send({ type: 'response', requestId, ok: true, value }); }
53
+ process.on('message', (message) => {
54
+ if (!message || typeof message !== 'object') return;
55
+ if (message.type === 'provider-cooldown-sync') { syncs.push(message); return; }
56
+ if (message.type === 'agent-control-notification') {
57
+ agentNotifications.push(message);
58
+ return;
59
+ }
60
+ if (message.type === 'agent-control-result') {
61
+ const waitingRequestId = agentControlWaiters.get(String(message.controlId || ''));
62
+ if (!waitingRequestId) return;
63
+ agentControlWaiters.delete(String(message.controlId || ''));
64
+ respond(waitingRequestId, message.ok === false ? { error: message.error?.message } : message.value);
65
+ return;
66
+ }
67
+ const requestId = String(message.requestId || '');
68
+ if (message.type === 'agent-control-status') {
69
+ respond(requestId, {
70
+ workers: agentJobs.map((job) => ({
71
+ tag: job.tag,
72
+ sessionId: job.sessionId,
73
+ agent: 'worker',
74
+ status: 'running',
75
+ provider: 'test',
76
+ model: 'test',
77
+ })),
78
+ jobs: agentJobs,
79
+ scope: { sessionId: message.context?.callerSessionId || null },
80
+ });
81
+ return;
82
+ }
83
+ if (message.type === 'agent-control-local-cancel') {
84
+ respond(requestId, { cancelled: true });
85
+ return;
86
+ }
87
+ if (message.type === 'agent-control-local') {
88
+ const args = message.args || {};
89
+ const type = String(args.type || 'spawn');
90
+ const ownerSessionId = String(message.context?.callerSessionId || '');
91
+ if (type === 'spawn' || type === 'send') {
92
+ const tag = String(args.tag || ('auto-' + SHARD + '-' + (agentJobs.length + 1)));
93
+ const existing = agentJobs.find((job) => job.tag === tag);
94
+ const sessionId = existing?.sessionId || ('sess_agent_' + SHARD + '_' + tag.replace(/[^A-Za-z0-9_-]/g, '_'));
95
+ const task_id = 'task_agent_' + SHARD + '_' + (agentJobs.length + 1);
96
+ const job = { task_id, type, status: 'running', tag, sessionId };
97
+ agentJobs.push(job);
98
+ respond(requestId, [
99
+ 'agent task: ' + task_id,
100
+ 'status: running',
101
+ 'type: ' + type,
102
+ 'target: ' + tag + ' ' + sessionId,
103
+ ].join('\\n'));
104
+ setImmediate(() => send({
105
+ type: 'agent-control-notification',
106
+ ownerSessionId,
107
+ text: 'Async agent task ' + task_id + ' (completed) finished.\\n\\nresult from shard ' + SHARD,
108
+ meta: {
109
+ type: 'agent_task_result',
110
+ execution_surface: 'agent',
111
+ execution_id: task_id,
112
+ status: 'completed',
113
+ },
114
+ }));
115
+ return;
116
+ }
117
+ const found = agentJobs.find((job) => (
118
+ (args.tag && job.tag === args.tag)
119
+ || (args.task_id && job.task_id === args.task_id)
120
+ || (args.sessionId && job.sessionId === args.sessionId)
121
+ ));
122
+ respond(requestId, found
123
+ ? 'agent task: ' + found.task_id + '\\nstatus: ' + found.status + '\\ntarget: ' + found.tag + ' ' + found.sessionId
124
+ : 'Error: agent target not found');
125
+ return;
126
+ }
127
+ if (message.type === 'create') {
128
+ if (message.options?.failCreate === true) {
129
+ send({ type: 'response', requestId, ok: false, error: { message: 'create rejected by stub' } });
130
+ return;
131
+ }
132
+ revisions.set(message.runtimeId, 1);
133
+ send({ type: 'state', runtimeId: message.runtimeId, revision: 1, full: { sessionId: message.options?.sessionId || '' } });
134
+ respond(requestId, { created: true });
135
+ return;
136
+ }
137
+ if (message.type === 'snapshot') {
138
+ const revision = (revisions.get(message.runtimeId) || 1) + 1;
139
+ revisions.set(message.runtimeId, revision);
140
+ send({ type: 'state', runtimeId: message.runtimeId, revision, full: { sessionId: 'shard-session' } });
141
+ respond(requestId, { published: true });
142
+ return;
143
+ }
144
+ if (message.type === 'workload') {
145
+ respond(requestId, {
146
+ shard: SHARD,
147
+ runtimes: revisions.size,
148
+ memory: { rssBytes: 1000 },
149
+ childSpawns: {
150
+ mode: SHARD === 0 ? 'remote-lease' : 'local',
151
+ maxInflight: 4,
152
+ lanes: [{ name: 'search', inflight: SHARD + 1, queued: SHARD, limit: 4, waitTimeoutMs: 30000 }],
153
+ },
154
+ toolIo: { readIo: { active: SHARD + 1, queued: 2, activeMax: 16, maxWaitMs: (SHARD + 1) * 10 } },
155
+ resources: {
156
+ active: { agent: SHARD + 1, shell: 1 },
157
+ queued: SHARD,
158
+ limits: { maxAgents: 8, maxShells: 8 },
159
+ activeLeases: [{ kind: 'agent', label: 'shard-' + SHARD, ageMs: 5 }],
160
+ },
161
+ });
162
+ return;
163
+ }
164
+ if (message.type === 'prewarm') { prewarms += 1; respond(requestId, { ready: true, shard: SHARD }); return; }
165
+ if (message.type === 'agent-dispatch') {
166
+ const reply = () => respond(requestId, { value: { pid: process.pid, shard: SHARD, dispatchId: message.dispatchId } });
167
+ const delay = Number(message.delayMs) || 0;
168
+ if (delay > 0) setTimeout(reply, delay); else reply();
169
+ return;
170
+ }
171
+ if (message.type === 'agent-dispatch-cancel') {
172
+ cancels.push(String(message.dispatchId || ''));
173
+ respond(requestId, { cancelled: true });
174
+ return;
175
+ }
176
+ if (message.type === 'call') {
177
+ const args = Array.isArray(message.args) ? message.args : [];
178
+ const command = args[0];
179
+ if (command === 'canonical-agent-control') {
180
+ const controlId = 'canonical-control-' + SHARD + '-' + requestId;
181
+ agentControlWaiters.set(controlId, requestId);
182
+ send({
183
+ type: 'agent-control',
184
+ controlId,
185
+ args: { type: 'spawn', tag: 'canonical', agent: 'worker', prompt: 'brief' },
186
+ context: {
187
+ callerSessionId: message.options?.sessionId || 'sess_canonical_parent',
188
+ callerCwd: process.cwd(),
189
+ },
190
+ });
191
+ return;
192
+ }
193
+ if (command === 'lag') {
194
+ const value = Number(args[1]) || 0;
195
+ send({ type: 'event-loop-lag', shard: SHARD, sample: {
196
+ p50Ms: value, p95Ms: value, p99Ms: value, maxMs: value, meanMs: value, intervalMs: 5000, at: Date.now(),
197
+ } });
198
+ respond(requestId, { pid: process.pid, shard: SHARD });
199
+ return;
200
+ }
201
+ if (command === 'cooldown') {
202
+ const ms = Number(args[1]) || 60000;
203
+ send({ type: 'provider-cooldown', shard: SHARD, cooldown: {
204
+ untilMs: Date.now() + ms, disabledReason: null, observedAt: Date.now(),
205
+ } });
206
+ respond(requestId, { pid: process.pid, shard: SHARD });
207
+ return;
208
+ }
209
+ if (command === 'admission-cooldown') {
210
+ // Event-driven admission cooldown: no fast-mode change at all.
211
+ send({ type: 'provider-cooldown', shard: SHARD, cooldown: {
212
+ untilMs: 0, disabledReason: null, observedAt: Date.now(),
213
+ }, admission: { type: 'cooldown', key: 'anthropic-oauth:test', cooldownUntil: Date.now() + 30000 } });
214
+ respond(requestId, { pid: process.pid, shard: SHARD });
215
+ return;
216
+ }
217
+ if (command === 'syncs') { respond(requestId, { pid: process.pid, shard: SHARD, syncs, cancels, prewarms }); return; }
218
+ if (command === 'agent-notifications') {
219
+ respond(requestId, { pid: process.pid, shard: SHARD, agentNotifications });
220
+ return;
221
+ }
222
+ if (command === 'unhealthy') {
223
+ respond(requestId, { pid: process.pid, shard: SHARD });
224
+ setImmediate(() => send({ type: 'unhealthy', detail: { reason: 'test shard unhealthy' } }));
225
+ return;
226
+ }
227
+ if (command === 'slow') {
228
+ setTimeout(() => respond(requestId, { pid: process.pid, shard: SHARD }), Number(args[1]) || 500);
229
+ return;
230
+ }
231
+ if (message.method === 'resume') { respond(requestId, true); return; }
232
+ respond(requestId, { pid: process.pid, shard: SHARD, method: message.method, args: message.args });
233
+ return;
234
+ }
235
+ if (message.type === 'shutdown') { respond(requestId, { stopped: true }); setImmediate(() => process.exit(0)); return; }
236
+ respond(requestId, { ready: true, shard: SHARD });
237
+ });
238
+ `;
239
+
240
+ async function withShardHost(run, {
241
+ shardCount = SHARDS,
242
+ env = {},
243
+ executeAgentControl = null,
244
+ } = {}) {
245
+ const dir = await mkdtemp(join(tmpdir(), 'mixdog-runtime-shard-'));
246
+ const workerEntry = join(dir, 'shard-stub.mjs');
247
+ const logs = [];
248
+ await writeFile(workerEntry, SHARD_STUB, 'utf8');
249
+ const host = createSessionRuntimeHost({
250
+ workerEntry,
251
+ cwd: dir,
252
+ env: { ...process.env, ...env },
253
+ shardCount,
254
+ log: (line) => logs.push(line),
255
+ executeAgentControl,
256
+ });
257
+ try {
258
+ await run({ host, logs });
259
+ } finally {
260
+ await host.close('test complete');
261
+ await rm(dir, { recursive: true, force: true });
262
+ }
263
+ }
264
+
265
+ test('independent sessions run on distinct shard processes', async () => {
266
+ await withShardHost(async ({ host }) => {
267
+ const first = await host.create({ sessionId: keyForShard(0, 'sess') });
268
+ const second = await host.create({ sessionId: keyForShard(1, 'sess') });
269
+ const a = await first.submitAsync('ping');
270
+ const b = await second.submitAsync('ping');
271
+ assert.equal(a.shard, 0);
272
+ assert.equal(b.shard, 1);
273
+ assert.notEqual(a.pid, b.pid);
274
+
275
+ // Same session identity always resolves to the same shard/process.
276
+ const sameSession = await host.create({ sessionId: keyForShard(0, 'sess') });
277
+ assert.equal((await sameSession.submitAsync('ping')).pid, a.pid);
278
+
279
+ const status = host.status;
280
+ assert.equal(status.shardCount, SHARDS);
281
+ assert.equal(status.shards.length, SHARDS);
282
+ assert.equal(status.active, true);
283
+ // Aggregate view stays compatible with single-worker status consumers.
284
+ assert.equal(status.worker.runtimes, 3);
285
+ assert.equal(status.worker.pids.length, 2);
286
+ assert.ok(status.worker.pid);
287
+ });
288
+ });
289
+
290
+ test('agent dispatch fans out across shards and cancel follows its shard', async () => {
291
+ await withShardHost(async ({ host }) => {
292
+ const firstId = dispatchIdForShard(0);
293
+ const secondId = dispatchIdForShard(1);
294
+ const first = await host.agentDispatch({ dispatchId: firstId, agent: 'memory' });
295
+ const second = await host.agentDispatch({ dispatchId: secondId, agent: 'memory' });
296
+ assert.equal(first.shard, 0);
297
+ assert.equal(second.shard, 1);
298
+ assert.notEqual(first.pid, second.pid);
299
+
300
+ // An aborted dispatch cancels on the shard that owns it, not on shard 0.
301
+ const controller = new AbortController();
302
+ const cancelId = dispatchIdForShard(1);
303
+ const pending = host.agentDispatch(
304
+ { dispatchId: cancelId, agent: 'memory', delayMs: 400 },
305
+ { signal: controller.signal },
306
+ );
307
+ controller.abort(new Error('test abort'));
308
+ await pending.catch(() => {});
309
+ const owner = await host.create({ sessionId: keyForShard(1, 'sess') });
310
+ const observed = await waitFor(async () => {
311
+ const row = await owner.submitAsync('syncs');
312
+ return row.cancels.includes(cancelId) ? row : null;
313
+ }, `shard 1 cancel for ${cancelId}`);
314
+ assert.equal(observed.shard, 1);
315
+ });
316
+ });
317
+
318
+ test('runtime shard Agent control is executed by the daemon canonical controller', async () => {
319
+ const calls = [];
320
+ await withShardHost(async ({ host }) => {
321
+ const ownerSessionId = keyForShard(0, 'canonical-owner');
322
+ const owner = await host.create({ sessionId: ownerSessionId });
323
+ const result = await owner.submitAsync('canonical-agent-control');
324
+ assert.deepEqual(result, {
325
+ task_id: 'task_agent_canonical',
326
+ status: 'running',
327
+ sessionId: 'sess_canonical_child',
328
+ });
329
+ assert.equal(calls.length, 1);
330
+ assert.equal(calls[0].args.type, 'spawn');
331
+ assert.equal(calls[0].args.tag, 'canonical');
332
+ assert.ok(calls[0].context.signal instanceof AbortSignal);
333
+ assert.equal(host.notifySessionCompletion(
334
+ ownerSessionId,
335
+ 'canonical owner handoff',
336
+ {
337
+ type: 'agent_task_result',
338
+ execution_surface: 'agent',
339
+ execution_id: 'task_agent_canonical',
340
+ status: 'completed',
341
+ },
342
+ ), true);
343
+ const notifications = await waitFor(async () => {
344
+ const row = await owner.submitAsync('agent-notifications');
345
+ return row.agentNotifications.some((entry) => entry.text === 'canonical owner handoff')
346
+ ? row.agentNotifications
347
+ : null;
348
+ }, 'canonical Agent completion owner routing');
349
+ assert.equal(notifications.some((entry) =>
350
+ entry.ownerSessionId === ownerSessionId
351
+ && entry.meta?.execution_id === 'task_agent_canonical'), true);
352
+ }, {
353
+ executeAgentControl: async (args, context) => {
354
+ calls.push({ args, context });
355
+ return {
356
+ task_id: 'task_agent_canonical',
357
+ status: 'running',
358
+ sessionId: 'sess_canonical_child',
359
+ };
360
+ },
361
+ });
362
+ });
363
+
364
+ test.skip('retired shard-local Agent execution is replaced by daemon canonical sessions', async () => {
365
+ await withShardHost(async ({ host }) => {
366
+ const ownerSessionId = keyForShard(0, 'lead-agent-owner');
367
+ const owner = await host.create({ sessionId: ownerSessionId });
368
+ const tagFor = (index) => {
369
+ for (let n = 0; n < 2_000; n += 1) {
370
+ const tag = `fanout-${index}-${n}`;
371
+ if (shardIndexForKey(`agent:${ownerSessionId}:${tag}`, SHARDS) === index) return tag;
372
+ }
373
+ throw new Error(`no agent tag maps to shard ${index}`);
374
+ };
375
+ const tag0 = tagFor(0);
376
+ const tag1 = tagFor(1);
377
+
378
+ const first = await host.agentControl(
379
+ { type: 'spawn', tag: tag0, agent: 'worker', prompt: 'first' },
380
+ { callerSessionId: ownerSessionId, callerCwd: process.cwd() },
381
+ );
382
+ const second = await host.agentControl(
383
+ { type: 'spawn', tag: tag1, agent: 'worker', prompt: 'second' },
384
+ { callerSessionId: ownerSessionId, callerCwd: process.cwd() },
385
+ );
386
+ assert.match(first, /sess_agent_0_/);
387
+ assert.match(second, /sess_agent_1_/);
388
+
389
+ // Same-tag follow-ups preserve the shard-local persistent session.
390
+ const follow0 = await host.agentControl(
391
+ { type: 'send', tag: tag0, message: 'follow zero' },
392
+ { callerSessionId: ownerSessionId, callerCwd: process.cwd() },
393
+ );
394
+ const follow1 = await host.agentControl(
395
+ { type: 'send', tag: tag1, message: 'follow one' },
396
+ { callerSessionId: ownerSessionId, callerCwd: process.cwd() },
397
+ );
398
+ assert.match(follow0, /sess_agent_0_/);
399
+ assert.match(follow1, /sess_agent_1_/);
400
+
401
+ const notifications = await waitFor(async () => {
402
+ const row = await owner.submitAsync('agent-notifications');
403
+ return row.agentNotifications.length >= 4 ? row.agentNotifications : null;
404
+ }, 'cross-shard Agent completion delivery');
405
+ assert.equal(notifications.every((row) => row.ownerSessionId === ownerSessionId), true);
406
+ assert.equal(notifications.some((row) => /shard 0/.test(row.text)), true);
407
+ assert.equal(notifications.some((row) => /shard 1/.test(row.text)), true);
408
+
409
+ const listed = await host.agentControl(
410
+ { type: 'list' },
411
+ { callerSessionId: ownerSessionId, callerCwd: process.cwd() },
412
+ );
413
+ assert.match(listed, /agents:\s*2/);
414
+ assert.match(listed, /tasks:\s*4/);
415
+ });
416
+ });
417
+
418
+ test('an unhealthy shard recycles alone while sibling shards keep serving', async () => {
419
+ await withShardHost(async ({ host, logs }) => {
420
+ const failing = await host.create({ sessionId: keyForShard(0, 'sess') });
421
+ const healthy = await host.create({ sessionId: keyForShard(1, 'sess') });
422
+ const failingPid = (await failing.submitAsync('ping')).pid;
423
+ const healthyPid = (await healthy.submitAsync('ping')).pid;
424
+
425
+ await failing.submitAsync('unhealthy');
426
+ await waitFor(
427
+ () => logs.some((line) => /shard 0 recycling.*unhealthy/.test(line))
428
+ && logs.some((line) => /shard 0 recovered 1 runtime/.test(line)),
429
+ 'shard 0 recycle + recovery',
430
+ );
431
+
432
+ // The sibling shard is untouched: same process, still answering control.
433
+ const stillHealthy = await healthy.submitAsync('ping');
434
+ assert.equal(stillHealthy.pid, healthyPid);
435
+ assert.equal(stillHealthy.shard, 1);
436
+
437
+ // The failed shard is replaced in place: same shard index, new process.
438
+ const recovered = await failing.submitAsync('ping');
439
+ assert.equal(recovered.shard, 0);
440
+ assert.notEqual(recovered.pid, failingPid);
441
+ assert.equal(host.status.shards[1].pid, healthyPid);
442
+ });
443
+ });
444
+
445
+ test('a lagging shard is quarantined from new placement without losing its work', async () => {
446
+ await withShardHost(async ({ host, logs }) => {
447
+ const lagging = await host.create({ sessionId: keyForShard(0, 'sess') });
448
+ const sibling = await host.create({ sessionId: keyForShard(1, 'sess') });
449
+ const laggingPid = (await lagging.submitAsync('ping')).pid;
450
+ const siblingPid = (await sibling.submitAsync('ping')).pid;
451
+
452
+ // One spike is not a quarantine; sustained saturation is.
453
+ await lagging.submitAsync('lag', 3_000);
454
+ await lagging.submitAsync('lag', 3_000);
455
+ assert.equal(host.status.shards[0].degraded, false);
456
+ await lagging.submitAsync('lag', 3_000);
457
+ await waitFor(() => host.status.shards[0].degraded === true, 'shard 0 quarantine');
458
+ assert.ok(logs.some((line) => /shard 0 event-loop saturated/.test(line)));
459
+
460
+ // New work whose home is the saturated shard is placed on a healthy one…
461
+ const placed = await host.create({ sessionId: keyForShard(0, 'sess-late') });
462
+ const placedRow = await placed.submitAsync('ping');
463
+ assert.equal(placedRow.shard, 1);
464
+ assert.equal(placedRow.pid, siblingPid);
465
+
466
+ // …while the saturated shard keeps its own accepted work in the SAME
467
+ // process (never killed, never migrated).
468
+ const stillOwned = await lagging.submitAsync('ping');
469
+ assert.equal(stillOwned.pid, laggingPid);
470
+ assert.equal(host.status.shards[0].lag.p99Ms, 3_000);
471
+
472
+ await lagging.submitAsync('lag', 10);
473
+ await waitFor(() => host.status.shards[0].degraded === false, 'shard 0 lag recovery');
474
+ });
475
+ });
476
+
477
+ test('a provider cooldown discovered in one shard is replayed into the others', async () => {
478
+ await withShardHost(async ({ host }) => {
479
+ const first = await host.create({ sessionId: keyForShard(0, 'sess') });
480
+ const second = await host.create({ sessionId: keyForShard(1, 'sess') });
481
+ await second.submitAsync('ping');
482
+
483
+ await first.submitAsync('cooldown', 120_000);
484
+ const state = await waitFor(async () => {
485
+ const row = await second.submitAsync('syncs');
486
+ return row.syncs.length > 0 ? row : null;
487
+ }, 'cooldown replay into shard 1');
488
+ assert.ok(state.syncs[0].cooldown.untilMs > Date.now());
489
+ assert.ok(host.status.providerCooldown.untilMs > Date.now());
490
+
491
+ // The discovering shard is not echoed its own cooldown back.
492
+ const origin = await first.submitAsync('syncs');
493
+ assert.equal(origin.syncs.length, 0);
494
+ });
495
+ });
496
+
497
+ test('workload telemetry aggregates every live shard', async () => {
498
+ await withShardHost(async ({ host }) => {
499
+ const first = await host.create({ sessionId: keyForShard(0, 'sess') });
500
+ const second = await host.create({ sessionId: keyForShard(1, 'sess') });
501
+ await first.submitAsync('ping');
502
+ await second.submitAsync('ping');
503
+ await host.refreshRuntimeWorkload();
504
+ const workloads = host.workloads;
505
+ assert.equal(workloads.shardCount, SHARDS);
506
+ assert.equal(workloads.shards.length, SHARDS);
507
+ assert.equal(workloads.worker.shards, SHARDS);
508
+ assert.equal(workloads.worker.runtimes, 2);
509
+ assert.equal(workloads.worker.memory.rssBytes, 2000);
510
+ assert.ok(workloads.machineSpawnBudget);
511
+
512
+ // Backward-compatible fields must describe the MACHINE, not shard 0:
513
+ // live load sums across shards while configured caps stay caps.
514
+ const worker = workloads.worker;
515
+ assert.equal(worker.childSpawns.lanes.length, 1);
516
+ assert.equal(worker.childSpawns.lanes[0].inflight, 3);
517
+ assert.equal(worker.childSpawns.lanes[0].queued, 1);
518
+ assert.equal(worker.childSpawns.lanes[0].limit, 4);
519
+ assert.equal(worker.childSpawns.lanes[0].waitTimeoutMs, 30000);
520
+ assert.equal(worker.childSpawns.maxInflight, 4);
521
+ // One shard off the machine budget must be visible, not hidden by shard 0.
522
+ assert.equal(worker.childSpawns.mode, 'mixed');
523
+ assert.equal(worker.toolIo.readIo.active, 3);
524
+ assert.equal(worker.toolIo.readIo.queued, 4);
525
+ assert.equal(worker.toolIo.readIo.activeMax, 16);
526
+ assert.equal(worker.toolIo.readIo.maxWaitMs, 20);
527
+ assert.equal(worker.resources.active.agent, 3);
528
+ assert.equal(worker.resources.active.shell, 2);
529
+ assert.equal(worker.resources.queued, 1);
530
+ assert.equal(worker.resources.limits.maxAgents, 8);
531
+ assert.equal(worker.resources.activeLeases.length, 2);
532
+ });
533
+ });
534
+
535
+ test('an admission cooldown event reaches sibling shards without waiting for a telemetry tick', async () => {
536
+ await withShardHost(async ({ host }) => {
537
+ const origin = await host.create({ sessionId: keyForShard(0, 'sess') });
538
+ const sibling = await host.create({ sessionId: keyForShard(1, 'sess') });
539
+ await sibling.submitAsync('ping');
540
+
541
+ const startedAt = Date.now();
542
+ await origin.submitAsync('admission-cooldown');
543
+ // No fast-mode change at all: only the admission event carries the news,
544
+ // and it must still be forwarded (and far faster than the 5s lag tick).
545
+ const state = await waitFor(async () => {
546
+ const row = await sibling.submitAsync('syncs');
547
+ return row.syncs.length > 0 ? row : null;
548
+ }, 'admission cooldown replay into shard 1', 2_000);
549
+ assert.ok(Date.now() - startedAt < 2_000);
550
+ assert.equal(state.syncs[0].admission.type, 'cooldown');
551
+ assert.equal(state.syncs[0].admission.key, 'anthropic-oauth:test');
552
+ assert.ok(state.syncs[0].admission.cooldownUntil > Date.now());
553
+ // The origin shard is never handed its own event back.
554
+ assert.equal((await origin.submitAsync('syncs')).syncs.length, 0);
555
+ });
556
+ });
557
+
558
+ test('a failed create releases sticky shard ownership exactly once', async () => {
559
+ await withShardHost(async ({ host }) => {
560
+ const sessionId = keyForShard(0, 'sess-owner');
561
+ const holder = await host.create({ sessionId });
562
+ const homePid = (await holder.submitAsync('ping')).pid;
563
+ assert.equal(host.status.shards[0].pid, homePid);
564
+
565
+ // A rejected create must not decrement the claim the live view still holds.
566
+ await assert.rejects(host.create({ sessionId, failCreate: true }), /create rejected/);
567
+
568
+ // Quarantine the home shard: placement of NEW keys moves away, but this
569
+ // session is already owned and must never be split across two shards.
570
+ for (let sample = 0; sample < 3; sample += 1) await holder.submitAsync('lag', 3_000);
571
+ await waitFor(() => host.status.shards[0].degraded === true, 'shard 0 quarantine');
572
+
573
+ const rejoined = await host.create({ sessionId });
574
+ assert.equal((await rejoined.submitAsync('ping')).pid, homePid);
575
+ assert.equal(host.status.shards[1].pid, null);
576
+
577
+ // Both live views release the single claim; the next claim may re-place.
578
+ await holder.dispose('test');
579
+ await rejoined.dispose('test');
580
+ const relocated = await host.create({ sessionId });
581
+ assert.equal((await relocated.submitAsync('ping')).shard, 1);
582
+ });
583
+ });
584
+
585
+ test('an explicit shard count is clamped to the hard maximum', async () => {
586
+ await withShardHost(async ({ host }) => {
587
+ assert.equal(host.status.shardCount, 16);
588
+ assert.equal(host.status.shards.length, 16);
589
+ // Shards stay lazy: clamping must not fork 16 children either.
590
+ assert.equal(host.status.shards.every((shard) => shard.pid === null), true);
591
+ assert.equal(host.status.active, false);
592
+ }, { shardCount: 999 });
593
+ });
594
+
595
+ test('prewarm warms shard 0 once and every later shard on spawn', async () => {
596
+ await withShardHost(async ({ host }) => {
597
+ const ready = await host.prewarm();
598
+ assert.equal(ready.shard, 0);
599
+ const first = await host.create({ sessionId: keyForShard(0, 'sess') });
600
+ // The spawn path and the explicit prewarm share one pass, never two.
601
+ assert.equal((await first.submitAsync('syncs')).prewarms, 1);
602
+
603
+ // A shard that forks later warms itself, so a fan-out burst never starts
604
+ // on a cold shard.
605
+ const second = await host.create({ sessionId: keyForShard(1, 'sess') });
606
+ const row = await waitFor(async () => {
607
+ const value = await second.submitAsync('syncs');
608
+ return value.prewarms > 0 ? value : null;
609
+ }, 'lazily spawned shard prewarm');
610
+ assert.equal(row.shard, 1);
611
+ assert.equal(row.prewarms, 1);
612
+ });
613
+ });
614
+
615
+ test('a single-shard host keeps the historical single-worker behaviour', async () => {
616
+ await withShardHost(async ({ host }) => {
617
+ const runtime = await host.create({ sessionId: 'solo-session' });
618
+ const row = await runtime.submitAsync('ping');
619
+ assert.equal(row.shard, 0);
620
+ assert.equal(host.status.shardCount, 1);
621
+ assert.equal(host.status.worker.pid, row.pid);
622
+ assert.equal(host.status.shards.length, 1);
623
+ }, { shardCount: 1 });
624
+ });
625
+
626
+ test('production ignores the legacy shard env and hosts session actors in one process', async () => {
627
+ await withShardHost(async ({ host }) => {
628
+ const first = await host.create({ sessionId: 'actor-a' });
629
+ const second = await host.create({ sessionId: 'actor-b' });
630
+ const [a, b] = await Promise.all([
631
+ first.submitAsync('ping'),
632
+ second.submitAsync('ping'),
633
+ ]);
634
+ assert.equal(a.pid, b.pid);
635
+ assert.equal(host.status.mode, 'single-runtime');
636
+ assert.equal(host.status.shardCount, 1);
637
+ assert.deepEqual(host.status.worker.pids, [a.pid]);
638
+ }, {
639
+ shardCount: null,
640
+ env: { MIXDOG_SESSION_RUNTIME_SHARDS: '8' },
641
+ });
642
+ });