mixdog 1.2.5 → 1.2.6

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 (290) hide show
  1. package/package.json +1 -1
  2. package/scripts/lib/gate-scheduler.mjs +2 -2
  3. package/scripts/lib/run-node-tests.mjs +5 -14
  4. package/scripts/provider-pingpong-live.mjs +9 -9
  5. package/src/agents/advisor/AGENT.md +4 -4
  6. package/src/agents/advisor/agent.json +1 -1
  7. package/src/agents/reviewer/AGENT.md +2 -3
  8. package/src/agents/reviewer/agent.json +1 -1
  9. package/src/defaults/shipped-history.json +10 -0
  10. package/src/defaults/skills/browser-use/SKILL.md +2 -1
  11. package/src/defaults/skills/computer-use/SKILL.md +3 -1
  12. package/src/defaults/skills/visualize/SKILL.md +3 -2
  13. package/src/lib/rules-builder.cjs +1 -1
  14. package/src/output-styles/common.md +22 -6
  15. package/src/output-styles/detailed.md +7 -5
  16. package/src/output-styles/minimal.md +7 -3
  17. package/src/output-styles/simple.md +2 -3
  18. package/src/rules/agent/AGENT.md +3 -1
  19. package/src/rules/lead/LEAD.md +4 -3
  20. package/src/rules/shared/10-tool-workflow.md +2 -1
  21. package/src/rules/shared/60-verification.md +4 -5
  22. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -1
  23. package/src/runtime/agent/orchestrator/config.mjs +6 -0
  24. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +1 -1
  25. package/src/runtime/agent/orchestrator/internal-agents.mjs +10 -0
  26. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +53 -60
  27. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  28. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +2 -3
  29. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +9 -2
  30. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +40 -19
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +3 -2
  32. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +4 -1
  33. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +2 -0
  34. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn.mjs +3 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -1
  36. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -1
  37. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +1 -2
  38. package/src/runtime/agent/orchestrator/providers/catalog-deadline.mjs +163 -0
  39. package/src/runtime/agent/orchestrator/providers/catalog-failures.mjs +34 -0
  40. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +38 -14
  41. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +100 -26
  42. package/src/runtime/agent/orchestrator/providers/lib/send-callbacks.mjs +24 -0
  43. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +13 -0
  44. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +2 -1
  45. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
  46. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +4 -0
  47. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  49. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +49 -4
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +0 -7
  51. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +7 -1
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
  53. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -0
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +5 -1
  56. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +11 -2
  57. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -0
  58. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +45 -16
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +1 -0
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-connect.mjs +196 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +74 -9
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +14 -1
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -0
  64. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-login.mjs +194 -0
  65. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-tokens.mjs +353 -0
  66. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +293 -0
  67. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +33 -100
  68. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +23 -14
  69. package/src/runtime/agent/orchestrator/providers/registry.mjs +41 -27
  70. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +2 -3
  71. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +3 -8
  72. package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +15 -3
  73. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +74 -13
  74. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +102 -0
  75. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -2
  76. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +16 -0
  77. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +9 -0
  78. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +39 -2
  79. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +9 -19
  80. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -3
  81. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -0
  82. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +13 -5
  83. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +10 -7
  84. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +62 -22
  85. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +23 -1
  86. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +81 -34
  87. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -2
  88. package/src/runtime/agent/orchestrator/session/request-fork.mjs +47 -0
  89. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +16 -1
  90. package/src/runtime/agent/orchestrator/session/side-question.mjs +5 -43
  91. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +10 -0
  92. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +73 -6
  93. package/src/runtime/agent/orchestrator/session/store/load-session.mjs +40 -0
  94. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +15 -0
  95. package/src/runtime/agent/orchestrator/session/store/session-paths.mjs +12 -0
  96. package/src/runtime/agent/orchestrator/session/store-agent-worker-header.mjs +36 -0
  97. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +258 -43
  98. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +306 -0
  99. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +70 -21
  100. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +46 -30
  101. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +144 -45
  102. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +32 -13
  103. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +364 -38
  104. package/src/runtime/agent/orchestrator/session/store.mjs +9 -2
  105. package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +43 -12
  106. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +23 -6
  107. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -6
  108. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +13 -7
  109. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +1 -1
  110. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -1
  111. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +40 -7
  112. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +37 -34
  113. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/inline-hoist.mjs +1 -1
  114. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +0 -4
  115. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +29 -27
  117. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -1
  118. package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +28 -11
  119. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +12 -3
  120. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +203 -101
  121. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +90 -41
  122. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +17 -1
  123. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-delivery.mjs +199 -0
  124. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +165 -39
  125. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +25 -13
  126. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +7 -6
  127. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +19 -4
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-line-collector.mjs +5 -3
  129. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +25 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +2 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-render.mjs +3 -1
  132. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +12 -5
  133. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +61 -22
  134. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +32 -8
  135. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +193 -19
  136. package/src/runtime/agent/orchestrator/tools/builtin/shell-direct-exe.mjs +57 -7
  137. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +119 -32
  138. package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +26 -1
  139. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +13 -28
  140. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +90 -11
  141. package/src/runtime/agent/orchestrator/tools/builtin/wait-budgets.mjs +23 -0
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +54 -10
  143. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +22 -28
  144. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +11 -7
  145. package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +126 -9
  146. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  147. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +15 -6
  148. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +40 -2
  149. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +2 -2
  150. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promotion-results.mjs +13 -3
  151. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +2 -0
  152. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -1
  153. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +35 -4
  155. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +7 -2
  156. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +39 -23
  157. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +6 -2
  158. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  159. package/src/runtime/attachments/office-extract.mjs +11 -9
  160. package/src/runtime/memory/lib/embedding-tokenizer.mjs +32 -18
  161. package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -1
  162. package/src/runtime/memory/lib/tool-call-handler.mjs +7 -5
  163. package/src/runtime/office/authoring/pptx-script-normalize.mjs +11 -8
  164. package/src/runtime/office/core/tabular.mjs +1 -1
  165. package/src/runtime/office/design/library/design-template-inspect.mjs +7 -4
  166. package/src/runtime/office/portable/docx-formatting.mjs +1 -1
  167. package/src/runtime/office/portable/docx-runs.mjs +1 -1
  168. package/src/runtime/office/portable/docx-tracked-edits.mjs +2 -2
  169. package/src/runtime/office/portable/portable-docx-edits.mjs +11 -10
  170. package/src/runtime/office/portable/portable-docx-operations.mjs +21 -17
  171. package/src/runtime/office/portable/portable-docx-parts.mjs +15 -9
  172. package/src/runtime/office/portable/portable-docx-styles.mjs +4 -1
  173. package/src/runtime/office/portable/portable-docx-xml.mjs +9 -9
  174. package/src/runtime/office/portable/portable-opc.mjs +27 -13
  175. package/src/runtime/office/portable/portable-pivot.mjs +3 -3
  176. package/src/runtime/office/portable/portable-pptx-chart.mjs +3 -3
  177. package/src/runtime/office/portable/portable-pptx-charts.mjs +24 -18
  178. package/src/runtime/office/portable/portable-pptx-core.mjs +5 -5
  179. package/src/runtime/office/portable/portable-pptx-deck.mjs +8 -4
  180. package/src/runtime/office/portable/portable-pptx-package.mjs +7 -4
  181. package/src/runtime/office/portable/portable-pptx-shapes.mjs +18 -9
  182. package/src/runtime/office/portable/portable-sheet-parts.mjs +9 -3
  183. package/src/runtime/office/portable/portable-sheet-xml.mjs +5 -5
  184. package/src/runtime/office/portable/portable-xlsx-charts.mjs +4 -1
  185. package/src/runtime/office/portable/portable-xlsx-drawings.mjs +10 -6
  186. package/src/runtime/office/portable/portable-xlsx-operations.mjs +8 -5
  187. package/src/runtime/office/portable/portable-xlsx-pivot-ops.mjs +1 -1
  188. package/src/runtime/office/portable/portable-xlsx-reference-shift.mjs +11 -7
  189. package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +6 -3
  190. package/src/runtime/office/portable/portable-xml.mjs +1 -1
  191. package/src/runtime/shared/atomic-file.mjs +22 -13
  192. package/src/runtime/shared/background-tasks.mjs +60 -20
  193. package/src/runtime/shared/child-spawn-gate.mjs +26 -16
  194. package/src/runtime/shared/config.mjs +0 -10
  195. package/src/runtime/shared/file-lock.mjs +7 -1
  196. package/src/runtime/shared/file-permissions-worker.mjs +38 -0
  197. package/src/runtime/shared/file-permissions.d.mts +19 -4
  198. package/src/runtime/shared/file-permissions.mjs +310 -4
  199. package/src/runtime/shared/git-runner-worker.mjs +6 -78
  200. package/src/runtime/shared/llm/http-agent.mjs +7 -3
  201. package/src/runtime/shared/llm/provider-catalog-registry.mjs +10 -0
  202. package/src/runtime/shared/llm/usage-accounting.mjs +2 -0
  203. package/src/runtime/shared/llm/usage-ledger-import.mjs +32 -3
  204. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +12 -5
  205. package/src/runtime/shared/llm/usage-ledger-worker.mjs +1 -1
  206. package/src/runtime/shared/llm/usage-ledger.mjs +20 -5
  207. package/src/runtime/shared/memory-snapshot.mjs +5 -1
  208. package/src/runtime/shared/notify-trace.mjs +8 -1
  209. package/src/runtime/shared/pristine-execution-contract.json +5 -1
  210. package/src/runtime/shared/process-runner-worker.mjs +7 -0
  211. package/src/runtime/shared/process-runner.mjs +38 -0
  212. package/src/runtime/shared/provider-accounts.mjs +2 -1
  213. package/src/runtime/shared/provider-api-key.mjs +0 -1
  214. package/src/runtime/shared/read-row-numbers.mjs +1 -1
  215. package/src/runtime/shared/resource-admission.mjs +60 -18
  216. package/src/runtime/shared/run-child.mjs +84 -0
  217. package/src/runtime/shared/session-activity-signal.mjs +23 -0
  218. package/src/runtime/shared/shell-output-summary.mjs +23 -0
  219. package/src/runtime/shared/singleton-owner.mjs +126 -42
  220. package/src/runtime/shared/tool-output-limit.mjs +4 -2
  221. package/src/runtime/shared/tool-output-marker.mjs +58 -0
  222. package/src/runtime/shared/user-data-guard.mjs +28 -9
  223. package/src/runtime/shared/worktrees/activity-gate.mjs +33 -8
  224. package/src/runtime/shared/worktrees/git-head.mjs +20 -2
  225. package/src/runtime/shared/worktrees/lock.mjs +3 -2
  226. package/src/runtime/terminal-bridge/tool-defs.mjs +1 -1
  227. package/src/runtime/tidy/runners/biome.mjs +3 -1
  228. package/src/runtime/tidy/shadow.mjs +2 -1
  229. package/src/runtime/web-search/index.mjs +5 -7
  230. package/src/runtime/web-search/lib/formatter.mjs +42 -6
  231. package/src/runtime/web-search/tool-defs.mjs +6 -1
  232. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +4 -3
  233. package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -4
  234. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +3 -3
  235. package/src/session-runtime/lifecycle/shared.mjs +19 -2
  236. package/src/session-runtime/lifecycle/teardown/session-close.mjs +2 -0
  237. package/src/session-runtime/lifecycle/teardown.mjs +5 -2
  238. package/src/session-runtime/lifecycle-api.mjs +3 -1
  239. package/src/session-runtime/provider-auth/login.mjs +10 -6
  240. package/src/session-runtime/provider-auth/setup.mjs +19 -2
  241. package/src/session-runtime/provider-catalog-cache.mjs +101 -25
  242. package/src/session-runtime/provider-models/catalog-load.mjs +14 -3
  243. package/src/session-runtime/provider-models/row-hydration.mjs +4 -0
  244. package/src/session-runtime/provider-models.mjs +76 -10
  245. package/src/session-runtime/provider-readiness.mjs +4 -0
  246. package/src/session-runtime/services/agent-tool/close-flow.mjs +2 -1
  247. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +10 -2
  248. package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +13 -4
  249. package/src/session-runtime/services/agent-tool/notify.mjs +15 -4
  250. package/src/session-runtime/services/agent-tool/restart-notify.mjs +134 -0
  251. package/src/session-runtime/services/agent-tool/send-flow.mjs +24 -1
  252. package/src/session-runtime/services/agent-tool/spawn-flow/deferred-spawn.mjs +2 -1
  253. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -3
  254. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +5 -1
  255. package/src/session-runtime/services/agent-tool/tag-registry/tag-lookup.mjs +8 -3
  256. package/src/session-runtime/services/agent-tool/tag-registry.mjs +3 -0
  257. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +4 -0
  258. package/src/session-runtime/services/agent-tool/worker-index.mjs +44 -10
  259. package/src/session-runtime/services/agent-tool.mjs +20 -0
  260. package/src/session-runtime/services/provider-admin.mjs +92 -20
  261. package/src/session-runtime/services/usage-dashboard-model.mjs +2 -2
  262. package/src/session-runtime/services/usage-dashboard.mjs +27 -69
  263. package/src/session-runtime/turn/session-ops.mjs +2 -2
  264. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  265. package/src/session-runtime/usage-stats-api.mjs +6 -1
  266. package/src/standalone/channel-transport/http-routes.mjs +10 -9
  267. package/src/standalone/daemon-desktop-runtime.mjs +20 -7
  268. package/src/standalone/daemon-owner.mjs +117 -25
  269. package/src/standalone/daemon.mjs +43 -13
  270. package/src/standalone/session-call-cache.mjs +6 -0
  271. package/src/standalone/session-protocol.mjs +1 -1
  272. package/src/standalone/session-service/retention/busy.mjs +7 -2
  273. package/src/standalone/session-transport/routes.mjs +7 -6
  274. package/src/tui/app/model-options.mjs +39 -2
  275. package/src/tui/app/model-picker.mjs +16 -2
  276. package/src/tui/app/provider-setup-picker.mjs +1 -2
  277. package/src/tui/dist/index.mjs +385 -104
  278. package/src/tui/session/notice-text.mjs +0 -1
  279. package/src/tui/session/session-api/intake/submission.mjs +7 -13
  280. package/src/tui/session/session-api/integrations.mjs +1 -5
  281. package/src/tui/session/session-flow/drain.mjs +8 -0
  282. package/src/tui/session/session-flow/steering.mjs +46 -0
  283. package/src/tui/session/session-flow.mjs +2 -0
  284. package/src/tui/session/turn-stream.mjs +33 -0
  285. package/src/tui/session/turn.mjs +30 -3
  286. package/src/ui/statusline.mjs +1 -6
  287. package/src/ui/streaming-markdown-heal.mjs +24 -7
  288. package/src/vendor/statusline/bin/statusline-route.mjs +0 -1
  289. package/src/vendor/statusline/src/gateway/route-meta.mjs +0 -1
  290. package/src/workflows/default/WORKFLOW.md +9 -13
@@ -1,11 +1,288 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
3
- import { execFile, execFileSync } from 'node:child_process';
4
- import { promisify } from 'node:util';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { execFile, execFileSync, spawn } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { MessageChannel, receiveMessageOnPort, SHARE_ENV, Worker } from 'node:worker_threads';
5
6
 
6
- const execFileAsync = promisify(execFile);
7
7
  let cachedUserSid = null;
8
8
 
9
+ // Resolved per call (not promisified once at load) so the live binding of
10
+ // child_process.execFile is used.
11
+ function execFileAsync(file, args, options) {
12
+ return new Promise((done, fail) => {
13
+ execFile(file, args, options, (error, stdout, stderr) => {
14
+ if (error) fail(Object.assign(error, { stdout, stderr }));
15
+ else done({ stdout, stderr });
16
+ });
17
+ });
18
+ }
19
+
20
+ // Securing a published secret by spawning PowerShell costs ~150-200 ms per
21
+ // call, and secrets are read far more often than they change. Nothing is
22
+ // cached: every non-fresh async call still reads the file's real DACL and
23
+ // tightens it when it is not owner-only. File metadata (mtime/ctime/size/id)
24
+ // is never treated as evidence of permissions. To make that per-call check
25
+ // cheap, one long-lived PowerShell helper per process answers requests over
26
+ // stdin/stdout ("<id> <base64 path>" in, "<id> ok|fixed|err <base64 detail>"
27
+ // out); concurrent callers share it and are matched by request id. If the
28
+ // helper cannot start, crashes, closes its pipes or misses the request
29
+ // deadline, every request it held (and later ones, once the restart budget is
30
+ // spent) falls back to the spawn-per-call update, so a lost helper never
31
+ // skips a check. A definite ACL failure reported by the helper is an error,
32
+ // exactly like a failed per-call update.
33
+ const ACL_HELPER_REQUEST_TIMEOUT_MS = 15_000;
34
+ // At most this many helper starts per window. A lifetime budget let three
35
+ // unrelated losses (a request deadline missed across a system sleep, an
36
+ // antivirus stall) pin a days-old process to a PowerShell spawn per read.
37
+ const ACL_HELPER_MAX_STARTS = 3;
38
+ const ACL_HELPER_START_WINDOW_MS = 10 * 60_000;
39
+ let aclHelper = null;
40
+ let aclHelperStartTimes = [];
41
+ let aclHelperSeq = 0;
42
+
43
+ function aclHelperScript(sid) {
44
+ return [
45
+ "$ErrorActionPreference = 'Stop';",
46
+ `$sid = [System.Security.Principal.SecurityIdentifier]::new(${quoteForPowerShell(sid)});`,
47
+ '$full = [System.Security.AccessControl.FileSystemRights]::FullControl;',
48
+ '$utf8 = [System.Text.Encoding]::UTF8;',
49
+ '$out = [Console]::Out;',
50
+ 'while ($null -ne ($line = [Console]::In.ReadLine())) {',
51
+ '$parts = $line.Split(@([char]32), 2);',
52
+ 'if ($parts.Count -ne 2) { continue };',
53
+ 'try {',
54
+ '$path = $utf8.GetString([Convert]::FromBase64String($parts[1]));',
55
+ '$directory = [System.IO.Directory]::Exists($path);',
56
+ '$current = if ($directory) { [System.IO.Directory]::GetAccessControl($path) }',
57
+ 'else { [System.IO.File]::GetAccessControl($path) };',
58
+ '$rules = @($current.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier]));',
59
+ '$ownerOnly = $current.AreAccessRulesProtected -and $rules.Count -eq 1 -and',
60
+ "$rules[0].AccessControlType -eq 'Allow' -and $rules[0].IdentityReference -eq $sid -and",
61
+ '$rules[0].FileSystemRights -eq $full;',
62
+ "if ($ownerOnly) { $out.WriteLine($parts[0] + ' ok') } else {",
63
+ '$acl = if ($directory) { [System.Security.AccessControl.DirectorySecurity]::new() }',
64
+ 'else { [System.Security.AccessControl.FileSecurity]::new() };',
65
+ '$acl.SetAccessRuleProtection($true, $false);',
66
+ '$acl.AddAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new(',
67
+ '$sid, $full, [System.Security.AccessControl.AccessControlType]::Allow));',
68
+ 'if ($directory) { [System.IO.Directory]::SetAccessControl($path, $acl) }',
69
+ 'else { [System.IO.File]::SetAccessControl($path, $acl) };',
70
+ "$out.WriteLine($parts[0] + ' fixed') }",
71
+ '} catch {',
72
+ "$out.WriteLine($parts[0] + ' err ' + [Convert]::ToBase64String($utf8.GetBytes([string]$_.Exception.Message)))",
73
+ '};',
74
+ '$out.Flush() }',
75
+ ].join(' ');
76
+ }
77
+
78
+ function startAclHelper(icacls, sid) {
79
+ const now = Date.now();
80
+ aclHelperStartTimes = aclHelperStartTimes.filter((at) => now - at < ACL_HELPER_START_WINDOW_MS);
81
+ if (aclHelperStartTimes.length >= ACL_HELPER_MAX_STARTS) return null;
82
+ aclHelperStartTimes.push(now);
83
+ const command = powerShellCommand(icacls, aclHelperScript(sid));
84
+ let child;
85
+ try {
86
+ // Long-lived: run from System32 so it never pins the caller's working
87
+ // directory (a folder a process's cwd sits in cannot be removed).
88
+ child = spawn(command.executable, command.args, {
89
+ cwd: dirname(icacls),
90
+ stdio: ['pipe', 'pipe', 'ignore'],
91
+ windowsHide: true,
92
+ });
93
+ } catch {
94
+ return null;
95
+ }
96
+ const helper = { child, pending: new Map(), buffer: '', dead: false };
97
+ helper.fail = (cause) => {
98
+ if (aclHelper === helper) aclHelper = null;
99
+ if (helper.dead) return;
100
+ helper.dead = true;
101
+ const lost = [...helper.pending.values()];
102
+ helper.pending.clear();
103
+ for (const request of lost) request.lost(cause);
104
+ try {
105
+ child.kill();
106
+ } catch {}
107
+ };
108
+ child.on('error', helper.fail);
109
+ child.on('exit', () => helper.fail(new Error('Windows ACL helper exited')));
110
+ child.stdin.on('error', helper.fail);
111
+ child.stdout.on('error', helper.fail);
112
+ child.stdout.on('close', () => helper.fail(new Error('Windows ACL helper closed its output')));
113
+ child.stdout.setEncoding('utf8');
114
+ child.stdout.on('data', (chunk) => {
115
+ helper.buffer += chunk;
116
+ let newline = helper.buffer.indexOf('\n');
117
+ while (newline >= 0) {
118
+ const [id, status, detail = ''] = helper.buffer.slice(0, newline).trim().split(' ');
119
+ helper.buffer = helper.buffer.slice(newline + 1);
120
+ newline = helper.buffer.indexOf('\n');
121
+ const request = helper.pending.get(id);
122
+ if (!request) continue;
123
+ helper.pending.delete(id);
124
+ syncAclHelperRef(helper);
125
+ request.answer(status, Buffer.from(detail, 'base64').toString('utf8'));
126
+ }
127
+ });
128
+ syncAclHelperRef(helper);
129
+ return helper;
130
+ }
131
+
132
+ // An idle helper never keeps the process alive; while a request awaits its
133
+ // answer the helper's handles do, so the answer is delivered even when the
134
+ // request's deadline timer is mocked or otherwise not a live handle.
135
+ function syncAclHelperRef(helper) {
136
+ const method = helper.pending.size > 0 ? 'ref' : 'unref';
137
+ helper.child[method]();
138
+ helper.child.stdin[method]?.();
139
+ helper.child.stdout[method]?.();
140
+ }
141
+
142
+ /** Resolves `{ status, detail }` from the helper, or `{ lost }` when it could not answer. */
143
+ function askAclHelper(targetPath, icacls, sid) {
144
+ if (!aclHelper) aclHelper = startAclHelper(icacls, sid);
145
+ const helper = aclHelper;
146
+ if (!helper) return Promise.resolve({ lost: new Error('Windows ACL helper unavailable') });
147
+ return new Promise((done) => {
148
+ aclHelperSeq += 1;
149
+ const id = String(aclHelperSeq);
150
+ const timer = setTimeout(() => {
151
+ helper.pending.delete(id);
152
+ syncAclHelperRef(helper);
153
+ done({ lost: new Error('Windows ACL helper timed out') });
154
+ helper.fail(new Error('Windows ACL helper timed out'));
155
+ }, ACL_HELPER_REQUEST_TIMEOUT_MS);
156
+ helper.pending.set(id, {
157
+ answer(status, detail) {
158
+ clearTimeout(timer);
159
+ done({ status, detail });
160
+ },
161
+ lost(cause) {
162
+ clearTimeout(timer);
163
+ done({ lost: cause });
164
+ },
165
+ });
166
+ syncAclHelperRef(helper);
167
+ helper.child.stdin.write(`${id} ${Buffer.from(targetPath, 'utf8').toString('base64')}\n`);
168
+ });
169
+ }
170
+
171
+ // ── Sync bridge ─────────────────────────────────────────────────────────
172
+ // Synchronous secret writes (token stores, config) cannot await the helper,
173
+ // and a blocking icacls spawn per write held the event loop (and the write's
174
+ // file lock) for seconds under load. A worker thread owns a second helper;
175
+ // a synchronous caller posts its path and blocks in Atomics.wait only for
176
+ // the answer — milliseconds once the helper runs (prewarmOwnerOnlyAclWin32
177
+ // starts it off the write path). Shared slots: [0] answer counter, [1] the
178
+ // worker exited, [2] the worker is ready. A worker not ready within
179
+ // ACL_SYNC_BRIDGE_READY_MS, or gone, sends the caller to the one-shot
180
+ // update; an answer later than ACL_SYNC_BRIDGE_TIMEOUT_MS fails the write.
181
+ const ACL_SYNC_BRIDGE_READY_MS = 10_000;
182
+ // The helper's own deadline plus its one-shot fallback's, with margin.
183
+ const ACL_SYNC_BRIDGE_TIMEOUT_MS = 2 * ACL_HELPER_REQUEST_TIMEOUT_MS + 5_000;
184
+ let aclSyncBridge = null;
185
+ let aclSyncBridgeBroken = false;
186
+
187
+ function syncBridge() {
188
+ if (aclSyncBridge || aclSyncBridgeBroken) return aclSyncBridge;
189
+ try {
190
+ const url = new URL('./file-permissions-worker.mjs', import.meta.url);
191
+ // Bundled copies (the desktop service) ship no worker beside them.
192
+ if (!existsSync(fileURLToPath(url))) throw new Error('Windows ACL bridge worker is not shipped here');
193
+ const { port1, port2 } = new MessageChannel();
194
+ const signal = new Int32Array(new SharedArrayBuffer(12));
195
+ const worker = new Worker(url, {
196
+ env: SHARE_ENV,
197
+ execArgv: [],
198
+ workerData: { port: port2, signal },
199
+ transferList: [port2],
200
+ });
201
+ const bridge = { worker, port: port1, signal, seq: 0, startedAt: Date.now() };
202
+ const drop = () => {
203
+ if (aclSyncBridge === bridge) aclSyncBridge = null;
204
+ };
205
+ worker.on('error', drop);
206
+ worker.on('exit', drop);
207
+ worker.unref();
208
+ port1.unref?.();
209
+ aclSyncBridge = bridge;
210
+ } catch {
211
+ aclSyncBridgeBroken = true;
212
+ }
213
+ return aclSyncBridge;
214
+ }
215
+
216
+ function dropSyncBridge(bridge) {
217
+ if (aclSyncBridge !== bridge) return;
218
+ aclSyncBridge = null;
219
+ void bridge.worker.terminate();
220
+ }
221
+
222
+ /** `{ ok: true }`, `{ ok: false, message, code }` (a definite ACL failure),
223
+ * `{ timedOut: true }`, or `{ lost }` when no bridge could answer. */
224
+ function askSyncBridge(targetPath) {
225
+ const bridge = syncBridge();
226
+ if (!bridge) return { lost: new Error('Windows ACL bridge unavailable') };
227
+ bridge.seq += 1;
228
+ const id = bridge.seq;
229
+ let seen = Atomics.load(bridge.signal, 0);
230
+ bridge.port.postMessage({ id, path: targetPath });
231
+ const deadline = Date.now() + ACL_SYNC_BRIDGE_TIMEOUT_MS;
232
+ for (;;) {
233
+ for (let received = receiveMessageOnPort(bridge.port); received; received = receiveMessageOnPort(bridge.port)) {
234
+ if (received.message?.id === id) return received.message;
235
+ }
236
+ const now = Date.now();
237
+ const ready = Atomics.load(bridge.signal, 2) === 1;
238
+ if (Atomics.load(bridge.signal, 1) === 1 || (!ready && now - bridge.startedAt >= ACL_SYNC_BRIDGE_READY_MS)) {
239
+ dropSyncBridge(bridge);
240
+ return { lost: new Error('Windows ACL bridge exited') };
241
+ }
242
+ if (now >= deadline) return { timedOut: true };
243
+ const wait = ready ? deadline - now : Math.min(deadline - now, bridge.startedAt + ACL_SYNC_BRIDGE_READY_MS - now);
244
+ Atomics.wait(bridge.signal, 0, seen, Math.max(1, wait));
245
+ seen = Atomics.load(bridge.signal, 0);
246
+ }
247
+ }
248
+
249
+ /** Test hook: synchronous calls skip the bridge (its one-shot fallback runs). */
250
+ export function disableOwnerOnlyAclSyncBridgeForTests() {
251
+ aclSyncBridgeBroken = true;
252
+ if (aclSyncBridge) dropSyncBridge(aclSyncBridge);
253
+ }
254
+
255
+ /** Starts, off any write path, what owner-only enforcement needs on Windows:
256
+ * this thread's SID and helper, and the sync bridge with its own helper.
257
+ * Best-effort; never rejects. */
258
+ export async function prewarmOwnerOnlyAclWin32({ bridge = true } = {}) {
259
+ if (process.platform !== 'win32') return;
260
+ try {
261
+ const icacls = icaclsPath();
262
+ if (cachedUserSid === null) cachedUserSid = await resolveCurrentUserPrincipalAsync();
263
+ if (!aclHelper) aclHelper = startAclHelper(icacls, cachedUserSid);
264
+ if (bridge) syncBridge()?.port.postMessage({ warm: true });
265
+ } catch {
266
+ /* enforcement itself reports any failure */
267
+ }
268
+ }
269
+
270
+ /** Test/diagnostic hook: whether a helper is attached, its requests awaiting
271
+ * an answer, and the starts inside the current restart window. */
272
+ export function ownerOnlyAclHelperStats() {
273
+ return {
274
+ alive: aclHelper !== null,
275
+ pid: aclHelper?.child.pid ?? null,
276
+ pending: aclHelper?.pending.size ?? 0,
277
+ recentStarts: aclHelperStartTimes.length,
278
+ };
279
+ }
280
+
281
+ /** Test hook: the live helper process, or null. */
282
+ export function ownerOnlyAclHelperForTests() {
283
+ return aclHelper?.child ?? null;
284
+ }
285
+
9
286
  function systemToolPath(name) {
10
287
  const systemRoot = process.env.SystemRoot || process.env.windir;
11
288
  return systemRoot ? join(systemRoot, 'System32', name) : null;
@@ -155,6 +432,18 @@ function aclFailure(targetPath, cause, label = 'icacls') {
155
432
  export function enforceOwnerOnlyAclWin32(targetPath, { fresh = false } = {}) {
156
433
  if (process.platform !== 'win32') return;
157
434
  const icacls = icaclsPath();
435
+ // Fresh files (the temp and lock files of every synchronous secret write,
436
+ // made while its file lock is held) go to the helper through the sync
437
+ // bridge: one round trip, no process spawned. Only a bridge that is gone
438
+ // falls back to the blocking one-shot update below.
439
+ if (fresh) {
440
+ const reply = askSyncBridge(resolve(targetPath));
441
+ if (reply.ok === true) return;
442
+ if (reply.ok === false) throw Object.assign(new Error(reply.message), { code: reply.code || 'EACLFAIL' });
443
+ if (reply.timedOut) {
444
+ throw aclFailure(targetPath, new Error('the Windows ACL bridge did not answer'), 'Windows ACL helper');
445
+ }
446
+ }
158
447
  if (cachedUserSid === null) cachedUserSid = resolveCurrentUserPrincipal();
159
448
  const command = ownerOnlyAclCommand(targetPath, cachedUserSid, fresh, icacls);
160
449
  try {
@@ -168,8 +457,25 @@ export function enforceOwnerOnlyAclWin32(targetPath, { fresh = false } = {}) {
168
457
  }
169
458
  }
170
459
 
460
+ // Fresh files (unpublished temp/lock files, one or two per secret write) go
461
+ // through the helper too: never owner-only yet, each is secured explicitly by
462
+ // its DACL replacement. An icacls spawn per write, made inside the write's
463
+ // file lock, kept provider-accounts.json locked for 114 s under heavy load
464
+ // (each spawn blocked the event loop for seconds as well).
171
465
  export async function enforceOwnerOnlyAclWin32Async(targetPath, { fresh = false } = {}) {
172
466
  if (process.platform !== 'win32') return;
467
+ const icacls = icaclsPath();
468
+ if (cachedUserSid === null) cachedUserSid = await resolveCurrentUserPrincipalAsync();
469
+ const reply = await askAclHelper(resolve(targetPath), icacls, cachedUserSid);
470
+ if (reply.status === 'ok' || reply.status === 'fixed') return;
471
+ if (reply.status === 'err') {
472
+ throw aclFailure(targetPath, new Error(reply.detail || 'unknown error'), 'Windows ACL helper');
473
+ }
474
+ // The helper could not answer: verify with a one-shot update instead.
475
+ return applyOwnerOnlyAclAsync(targetPath, fresh);
476
+ }
477
+
478
+ async function applyOwnerOnlyAclAsync(targetPath, fresh) {
173
479
  const icacls = icaclsPath();
174
480
  if (cachedUserSid === null) cachedUserSid = await resolveCurrentUserPrincipalAsync();
175
481
  const command = ownerOnlyAclCommand(targetPath, cachedUserSid, fresh, icacls);
@@ -1,80 +1,8 @@
1
- // Worker-thread body for git-runner.mjs. Starting a child process is a
2
- // synchronous native call (CreateProcess on Windows, amplified by on-access
3
- // scanning), so git children are spawned here instead of on the host event
4
- // loop. Each request resolves to { code, stdout, stderr } or an error with the
5
- // same codes the in-process runner used (ETIMEDOUT, EMAXBUFFER, spawn errors).
6
- import { spawn } from 'node:child_process';
1
+ // Worker-thread body for git-runner.mjs: git children are spawned on this
2
+ // thread, never on the host event loop (run-child.mjs). Each request resolves
3
+ // to { code, stdout, stderr } or an error with the same codes the in-process
4
+ // runner used (ETIMEDOUT, EMAXBUFFER, spawn errors).
5
+ import { runChild } from './run-child.mjs';
7
6
  import { serveWorkerRequests } from './worker-requests.mjs';
8
7
 
9
- function runGit({ args, cwd, input, env, timeoutMs, maxBytes, stdoutEncoding = 'utf8' }) {
10
- return new Promise((resolvePromise, rejectPromise) => {
11
- const hasInput = (typeof input === 'string' || ArrayBuffer.isView(input)) && input.length > 0;
12
- const child = spawn('git', args, {
13
- cwd,
14
- ...(env ? { env } : {}),
15
- windowsHide: true,
16
- stdio: [hasInput ? 'pipe' : 'ignore', 'pipe', 'pipe'],
17
- });
18
- const stdout = [];
19
- const stderr = [];
20
- let outputBytes = 0;
21
- let stdinError = null;
22
- let settled = false;
23
- const finish = (error, result) => {
24
- if (settled) return;
25
- settled = true;
26
- clearTimeout(timer);
27
- if (error) rejectPromise(error);
28
- else resolvePromise(result);
29
- };
30
- const timer = setTimeout(() => {
31
- const error = new Error(`git ${args.join(' ')} timed out after ${timeoutMs}ms`);
32
- error.code = 'ETIMEDOUT';
33
- try {
34
- child.kill();
35
- } catch {}
36
- finish(error);
37
- }, timeoutMs);
38
- const collect = (chunks, chunk) => {
39
- const next = Buffer.from(chunk);
40
- outputBytes += next.length;
41
- if (outputBytes > maxBytes) {
42
- const error = new Error(`git ${args.join(' ')} exceeded ${maxBytes} output bytes`);
43
- error.code = 'EMAXBUFFER';
44
- try {
45
- child.kill();
46
- } catch {}
47
- finish(error);
48
- return;
49
- }
50
- chunks.push(next);
51
- };
52
- child.stdout.on('data', (chunk) => collect(stdout, chunk));
53
- child.stderr.on('data', (chunk) => collect(stderr, chunk));
54
- child.once('error', (error) => finish(error));
55
- child.once('close', (code) => {
56
- const result = {
57
- code: Number(code ?? 1),
58
- // 'base64' keeps non-UTF-8 bytes (a replayable patch) intact across the thread hop.
59
- stdout: Buffer.concat(stdout).toString(stdoutEncoding === 'base64' ? 'base64' : 'utf8'),
60
- stderr: Buffer.concat(stderr).toString('utf8'),
61
- };
62
- // A failed stdin write only matters when git otherwise reported success.
63
- if (stdinError && result.code === 0) {
64
- finish(stdinError);
65
- return;
66
- }
67
- finish(null, result);
68
- });
69
- if (child.stdin) {
70
- // A short-lived git command can close before stdin is flushed. Keep
71
- // that transport race inside this promise instead of an unhandled EPIPE.
72
- child.stdin.on('error', (error) => {
73
- stdinError = error;
74
- });
75
- child.stdin.end(input);
76
- }
77
- });
78
- }
79
-
80
- serveWorkerRequests(runGit);
8
+ serveWorkerRequests((request) => runChild({ ...request, command: 'git' }));
@@ -75,8 +75,8 @@ function proxyConfigured() {
75
75
  * The shared singleton dispatcher for LLM API requests. Created on first use.
76
76
  *
77
77
  * keepAliveTimeout (~60s) keeps a socket warm across the typical idle gap
78
- * between agent turns; connections caps concurrent sockets per origin so a
79
- * burst of parallel calls can't open an unbounded number of handshakes.
78
+ * between agent turns; sockets per origin are unbounded unless
79
+ * MIXDOG_LLM_CONNECTIONS sets a cap.
80
80
  *
81
81
  * Returns `undefined` when a proxy / any custom (non-default) global dispatcher
82
82
  * is configured, so `fetch({ dispatcher: undefined })` falls back to that
@@ -93,7 +93,11 @@ export function getLlmDispatcher() {
93
93
  _agent = new (undici().Agent)({
94
94
  keepAliveTimeout: envInt('MIXDOG_LLM_KEEPALIVE_MS', 60_000),
95
95
  keepAliveMaxTimeout: envInt('MIXDOG_LLM_KEEPALIVE_MAX_MS', 90_000),
96
- connections: envInt('MIXDOG_LLM_CONNECTIONS', 16),
96
+ // No per-origin socket cap by default: each streaming (SSE) response
97
+ // holds its HTTP/1.1 socket for the whole generation, so a cap of N
98
+ // queued the (N+1)th concurrent agent until another finished. Rate
99
+ // limiting is the server's call (429 → admission cooldown).
100
+ connections: envInt('MIXDOG_LLM_CONNECTIONS', null),
97
101
  // undici defaults both of these to 300s, which beheaded the Anthropic
98
102
  // NON-STREAMING stall fallback (2026-08-03 regex-chess probe:
99
103
  // UND_ERR_HEADERS_TIMEOUT at exactly 300s) — a non-streaming completion
@@ -93,7 +93,17 @@ const PROVIDER_CATALOGS = Object.freeze({
93
93
  },
94
94
  'cursor-oauth': { relay: true },
95
95
  'cursor-api': { relay: true },
96
+ // The models.dev listing id of the Go gateway: a relay catalog, not a
97
+ // selectable provider (the model lookup needs it flagged as a relay).
96
98
  'opencode-go': { relay: true },
99
+ // The Go gateway through an OpenCode Console login; the organization
100
+ // config's catalog is the live roster.
101
+ 'opencode-go-oauth': {
102
+ modelsDev: 'opencode-go',
103
+ relay: true,
104
+ rosterFiles: ['opencode-go-oauth-models.json'],
105
+ rosterLimitsAuthoritative: true,
106
+ },
97
107
  });
98
108
 
99
109
  // Confirmed upstream catalog gaps, keyed provider → model. Each entry patches
@@ -71,6 +71,8 @@ export async function accountProviderSend(provider, instance, send, model, opts
71
71
  requestedModel: model,
72
72
  pricingModel: result.pricingModel,
73
73
  sessionId,
74
+ // The session this one runs for (an agent's Lead), kept with its spend.
75
+ ownerSessionId: opts.session?.ownerSessionId || opts.session?.parentSessionId,
74
76
  sourceType,
75
77
  inputTokens: usage.inputTokens,
76
78
  inputTokensKnown: usage.inputTokensKnown,
@@ -10,6 +10,18 @@ import { createHash } from 'node:crypto';
10
10
  import { makeUsageRecord, usageRouteKind } from './usage-ledger.mjs';
11
11
  import { priceUsage } from './cost.mjs';
12
12
 
13
+ function legacyRollupDays(parsed) {
14
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
15
+ throw new SyntaxError('legacy rollup root is not an object');
16
+ }
17
+ if (!Object.hasOwn(parsed, 'days')) throw new SyntaxError('legacy rollup days is missing');
18
+ const { days } = parsed;
19
+ // A day map is an object. An array is the other accepted collection; null,
20
+ // strings, and other scalars are not.
21
+ if (days === null || typeof days !== 'object') throw new SyntaxError('legacy rollup days is not an object or array');
22
+ return days;
23
+ }
24
+
13
25
  async function optionalFile(path) {
14
26
  try {
15
27
  return await readFile(path, 'utf8');
@@ -165,9 +177,26 @@ export async function importUsageHistory(ledger, dataDir, { now = Date.now(), re
165
177
  session_id: event.sessionId,
166
178
  }))
167
179
  );
168
- const legacy = await optionalFile(join(dataDir, 'usage-rollup.local.json'));
169
- if (legacy) ledger.preserveLegacyDays(repriceRestoredDays(JSON.parse(legacy).days));
170
- ledger.set('importedThrough', until);
180
+ // A missing file is absence. Empty, unparsable, or wrong-shaped JSON is not:
181
+ // skip only this legacy import and leave importedThrough unset so a repair
182
+ // is retried instead of short-circuiting forever.
183
+ const legacyPath = join(dataDir, 'usage-rollup.local.json');
184
+ const legacy = await optionalFile(legacyPath);
185
+ let legacyReady = true;
186
+ if (legacy !== null) {
187
+ let days;
188
+ try {
189
+ days = legacyRollupDays(JSON.parse(legacy));
190
+ } catch (error) {
191
+ if (!(error instanceof SyntaxError)) throw error;
192
+ legacyReady = false;
193
+ process.stderr.write(
194
+ `[usage-ledger] warning: legacy rollup ${legacyPath} is empty, corrupt, or the wrong shape (${error.message}); skipping its import\n`
195
+ );
196
+ }
197
+ if (legacyReady) ledger.preserveLegacyDays(repriceRestoredDays(days));
198
+ }
199
+ if (legacyReady) ledger.set('importedThrough', until);
171
200
  ledger.set('importReceipt', JSON.stringify({ ...result, until }));
172
201
  return result;
173
202
  }
@@ -292,20 +292,27 @@ function mergeLegacyDays(db, { days, hourly, hourlyDay, fromDay, toDay }) {
292
292
  * Compaction requests run under their own provider session id but are
293
293
  * recorded under the source session, so this is the whole conversation
294
294
  * across compactions. The same route choice as a rollup applies: a gateway
295
- * summary never doubles a detail record.
295
+ * summary never doubles a detail record. `ownerIds` adds every session
296
+ * recorded as run for one of them (a Lead's agents, theirs in turn).
296
297
  */
297
- export function readSessionUsage(db, sessionIds) {
298
+ export function readSessionUsage(db, sessionIds, ownerIds = []) {
298
299
  const total = empty();
299
300
  const rows = db
300
301
  .prepare(`
301
- WITH mine AS (
302
+ WITH RECURSIVE owned(id) AS (
303
+ SELECT o.session FROM usage_session_owners o JOIN usage_sessions s ON s.id=o.owner
304
+ WHERE s.value IN (SELECT value FROM json_each(?))
305
+ UNION
306
+ SELECT o.session FROM usage_session_owners o JOIN owned ON o.owner=owned.id
307
+ ), mine AS (
302
308
  SELECT day,route,COUNT(*) AS turns,SUM(input) AS input,SUM(output) AS output,
303
309
  SUM(cache_read) AS cache_read,SUM(cache_write) AS cache_write,
304
310
  SUM(cost_usd) AS cost_usd,SUM(duration_ms) AS duration_ms,
305
311
  SUM(CASE WHEN duration_ms>0 AND output>0 THEN output ELSE 0 END) AS speed_output,
306
312
  SUM(CASE WHEN duration_ms>0 AND output>0 THEN duration_ms ELSE 0 END) AS speed_ms
307
313
  FROM usage_events WHERE session IN (
308
- SELECT id FROM usage_sessions WHERE value IN (SELECT value FROM json_each(?)))
314
+ SELECT id FROM usage_sessions WHERE value IN (SELECT value FROM json_each(?))
315
+ UNION SELECT id FROM owned)
309
316
  GROUP BY day,route
310
317
  ), attributed AS (
311
318
  SELECT printf('%04d-%02d-%02d',m.day/10000,(m.day/100)%100,m.day%100) AS day,
@@ -320,7 +327,7 @@ export function readSessionUsage(db, sessionIds) {
320
327
  )
321
328
  SELECT a.* FROM attributed a JOIN (${best}) b USING(day,provider,model,rank)
322
329
  `)
323
- .all(JSON.stringify(sessionIds), '0000-01-01', '9999-12-31');
330
+ .all(JSON.stringify(ownerIds), JSON.stringify(sessionIds), '0000-01-01', '9999-12-31');
324
331
  let speedOutput = 0;
325
332
  let speedMs = 0;
326
333
  for (const row of rows) {
@@ -60,7 +60,7 @@ function recordBatch(ledger, rows) {
60
60
  const operations = {
61
61
  rollup: ({ path, options }) => read(path, (db) => rollupUsage(db, options)),
62
62
  rollupDelta: ({ path, options, known, now }) => read(path, (db) => rollupDelta(db, options, known, now)),
63
- sessionUsage: ({ path, sessionIds }) => read(path, (db) => readSessionUsage(db, sessionIds)),
63
+ sessionUsage: ({ path, sessionIds, ownerIds }) => read(path, (db) => readSessionUsage(db, sessionIds, ownerIds)),
64
64
  record: ({ path, rows }) => write(path, (ledger) => recordBatch(ledger, rows)),
65
65
  recordQuota: ({ path, samples }) => write(path, (ledger) => ledger.recordQuota(samples)),
66
66
  quotaHistory: ({ path, options }) => read(path, (db) => readQuotaHistory(db, options)),
@@ -78,6 +78,13 @@ const COMPACT_SCHEMA = `
78
78
  -- One session's lifetime totals (the /context footer) read that session's
79
79
  -- records only instead of scanning every record ever written.
80
80
  CREATE INDEX IF NOT EXISTS usage_events_session ON usage_events(session);
81
+ -- The session each session ran for (an agent's Lead), so a Lead's agents
82
+ -- stay countable after their own session records are gone.
83
+ CREATE TABLE IF NOT EXISTS usage_session_owners (
84
+ session INTEGER PRIMARY KEY REFERENCES usage_sessions(id),
85
+ owner INTEGER NOT NULL REFERENCES usage_sessions(id)
86
+ );
87
+ CREATE INDEX IF NOT EXISTS usage_session_owners_owner ON usage_session_owners(owner);
81
88
  `;
82
89
 
83
90
  // Preserve the old read surface. Only storage changes: every field, including
@@ -109,6 +116,7 @@ function compactEventWriter(db) {
109
116
  const insertSession = db.prepare('INSERT OR IGNORE INTO usage_sessions(value) VALUES (?)');
110
117
  const findSession = db.prepare('SELECT id FROM usage_sessions WHERE value=?');
111
118
  const insert = db.prepare('INSERT INTO usage_events VALUES (?,?,?,?,?,?,?,?,?,?,?)');
119
+ const insertOwner = db.prepare('INSERT OR IGNORE INTO usage_session_owners VALUES (?,?)');
112
120
  return (row) => {
113
121
  for (const key of ['id', 'provider', 'model', 'kind', 'source_type', 'cost_source', 'origin', 'session_id']) {
114
122
  if (row[key] == null) throw new Error(`Usage requires ${key}`);
@@ -133,6 +141,11 @@ function compactEventWriter(db) {
133
141
  const route = findRoute.get(signature).id;
134
142
  insertSession.run(String(row.session_id));
135
143
  const session = findSession.get(String(row.session_id)).id;
144
+ const owner = row.owner_session_id ? String(row.owner_session_id) : '';
145
+ if (owner && owner !== String(row.session_id)) {
146
+ insertSession.run(owner);
147
+ insertOwner.run(session, findSession.get(owner).id);
148
+ }
136
149
  insert.run(
137
150
  id,
138
151
  row.ts,
@@ -201,7 +214,6 @@ function migrateUsageLedger(db, path) {
201
214
 
202
215
  export function usageRouteKind(provider) {
203
216
  if (provider === 'mixdog-local') return 'local';
204
- if (provider === 'opencode-go') return 'quota-api';
205
217
  return provider.endsWith('-oauth') ? 'oauth' : 'api';
206
218
  }
207
219
 
@@ -273,6 +285,7 @@ export function makeUsageRecord(args) {
273
285
  model,
274
286
  kind,
275
287
  sessionId: ['no-session', '(none)'].includes(args.sessionId) ? '' : text(args.sessionId),
288
+ ...(args.ownerSessionId ? { ownerSessionId: text(args.ownerSessionId) } : {}),
276
289
  sourceType: text(args.sourceType),
277
290
  input: priced.input,
278
291
  output: num(args.outputTokens),
@@ -453,6 +466,7 @@ export class UsageLedger {
453
466
  !this.insert({
454
467
  ...row,
455
468
  session_id: row.sessionId,
469
+ owner_session_id: row.ownerSessionId,
456
470
  source_type: row.sourceType,
457
471
  cache_read: row.cacheRead,
458
472
  cache_write: row.cacheWrite,
@@ -558,10 +572,11 @@ export class UsageLedger {
558
572
  return this.workerRequest('quotaWindows', { options });
559
573
  }
560
574
 
561
- /** Every request one session made under any of its ids, compactions included, read on the ledger worker. */
562
- sessionUsageAsync(sessionIds) {
563
- if (this.path === ':memory:') return Promise.resolve().then(() => readSessionUsage(this.db, sessionIds));
564
- return this.workerRequest('sessionUsage', { sessionIds });
575
+ /** Every request one session made under any of its ids, compactions included, read on the ledger worker.
576
+ * `ownerIds` adds every session recorded as run for them, transitively. */
577
+ sessionUsageAsync(sessionIds, ownerIds = []) {
578
+ if (this.path === ':memory:') return Promise.resolve().then(() => readSessionUsage(this.db, sessionIds, ownerIds));
579
+ return this.workerRequest('sessionUsage', { sessionIds, ownerIds });
565
580
  }
566
581
 
567
582
  preserveLegacyDays(days) {
@@ -22,8 +22,12 @@ const PERIODIC_ATTRIBUTION_FREE_BYTES = 2 * 1024 * 1024 * 1024;
22
22
  let periodicTimer = null;
23
23
  let lastPressureSnapshotAt = 0;
24
24
 
25
+ // Opt-in diagnostics (MIXDOG_MEMORY_SNAPSHOT=1): every sample starts a
26
+ // PowerShell process on Windows (two for a full snapshot, requested exactly
27
+ // when the host is under memory pressure), from every process that loads
28
+ // this module. A normal install never pays for that.
25
29
  function enabled() {
26
- return process.env.MIXDOG_MEMORY_SNAPSHOT !== '0';
30
+ return process.env.MIXDOG_MEMORY_SNAPSHOT === '1';
27
31
  }
28
32
 
29
33
  function snapshotPaths() {