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,4 +1,6 @@
1
- // Lightweight always-on completion-notification trace. The display path for
1
+ // Opt-in (MIXDOG_NOTIFY_TRACE=1) completion-notification trace: off by
2
+ // default, so a normal install writes no line per delivery decision (it kept
3
+ // a multi-MB log growing in every data dir). The display path for
2
4
  // background completions is event-time only and has repeatedly failed silently
3
5
  // in the field (2026-08-17: bench shell output never rendered while the model
4
6
  // queue twin worked every time). One append-only line per decision point keeps
@@ -36,7 +38,12 @@ function tracePath() {
36
38
  return join(traceDir, 'notify-trace.log');
37
39
  }
38
40
 
41
+ function notifyTraceEnabled() {
42
+ return process.env.MIXDOG_NOTIFY_TRACE === '1';
43
+ }
44
+
39
45
  export function notifyTrace(stage, fields = {}) {
46
+ if (!notifyTraceEnabled()) return;
40
47
  try {
41
48
  const parts = [`[${new Date().toISOString()}]`, `pid=${process.pid}`, stage];
42
49
  for (const [key, value] of Object.entries(fields)) {
@@ -88,6 +88,11 @@
88
88
  "credentialFile": "antigravity-oauth.json",
89
89
  "credentialPathEnv": "ANTIGRAVITY_OAUTH_CREDENTIALS_PATH",
90
90
  "modelCatalogFile": "antigravity-oauth-models.json"
91
+ },
92
+ "opencode-go-oauth": {
93
+ "credentialFile": "opencode-go-oauth.json",
94
+ "credentialPathEnv": "OPENCODE_GO_OAUTH_CREDENTIALS_PATH",
95
+ "modelCatalogFile": "opencode-go-oauth-models.json"
91
96
  }
92
97
  },
93
98
  "apiKeyProviders": {
@@ -96,7 +101,6 @@
96
101
  "gemini": "GEMINI_API_KEY",
97
102
  "deepseek": "DEEPSEEK_API_KEY",
98
103
  "xai": "XAI_API_KEY",
99
- "opencode-go": "OPENCODE_API_KEY",
100
104
  "openrouter": "OPENROUTER_API_KEY"
101
105
  },
102
106
  "personalStateCounters": [
@@ -0,0 +1,7 @@
1
+ // Worker-thread body for process-runner.mjs: helper children (process-table
2
+ // enumeration, tree kills) are spawned on this thread, never on the host
3
+ // event loop (run-child.mjs).
4
+ import { runChild } from './run-child.mjs';
5
+ import { serveWorkerRequests } from './worker-requests.mjs';
6
+
7
+ serveWorkerRequests(runChild);
@@ -0,0 +1,38 @@
1
+ // Run short helper processes from one shared worker thread, so their
2
+ // synchronous native process creation never stalls the host event loop. A
3
+ // daemon profile under heavy agent load measured single PowerShell spawns
4
+ // blocking the daemon's loop for up to 7 s each. Separate from the git
5
+ // runner's worker: a slow helper start never queues a git child behind it.
6
+ import { SHARE_ENV } from 'node:worker_threads';
7
+ import { runChild } from './run-child.mjs';
8
+ import { createWorkerRequestClient } from './worker-requests.mjs';
9
+
10
+ // Created on first use. A bundled copy of this module (the desktop service's
11
+ // CommonJS bundle) has no module URL and no worker file beside it: there the
12
+ // child is started on the calling thread, exactly as before.
13
+ let requestProcess = null;
14
+ function processClient() {
15
+ if (requestProcess !== null) return requestProcess;
16
+ try {
17
+ // SHARE_ENV: children inherit the live process environment, exactly as a
18
+ // spawn from this thread would.
19
+ requestProcess = createWorkerRequestClient(new URL('./process-runner-worker.mjs', import.meta.url), {
20
+ env: SHARE_ENV,
21
+ });
22
+ } catch {
23
+ requestProcess = false;
24
+ }
25
+ return requestProcess;
26
+ }
27
+
28
+ /**
29
+ * Resolves to { code, stdout, stderr } (a non-zero exit is the caller's to
30
+ * judge). Rejects with code ETIMEDOUT after timeoutMs (the child is killed;
31
+ * 0 sets no deadline),
32
+ * EMAXBUFFER once stdout+stderr exceed maxBytes, or the spawn error itself.
33
+ */
34
+ export function runProcessOffThread(command, args, { cwd, input = '', env = null, timeoutMs, maxBytes }) {
35
+ const request = processClient();
36
+ const payload = { command, args, cwd, input, env, timeoutMs, maxBytes };
37
+ return request ? request(payload) : runChild(payload);
38
+ }
@@ -13,6 +13,7 @@ export const ACCOUNT_PROVIDERS = Object.freeze([
13
13
  'mimo-oauth',
14
14
  'cursor-oauth',
15
15
  'antigravity-oauth',
16
+ 'opencode-go-oauth',
16
17
  ]);
17
18
  const MAX_ACCOUNTS = 20;
18
19
  // `dataDir` defaults to this process's data dir; an isolated runtime passes
@@ -79,7 +80,7 @@ function normalizeLabels(pool) {
79
80
  // only; `limit` keeps the selected account until it reaches a limit and then
80
81
  // moves to the account with the most headroom; `headroom` starts every new
81
82
  // session on the account with the most headroom and keeps it for that session.
82
- export const ACCOUNT_POLICIES = Object.freeze(['manual', 'limit', 'headroom']);
83
+ const ACCOUNT_POLICIES = Object.freeze(['manual', 'limit', 'headroom']);
83
84
 
84
85
  // `auto` predates `policy` and is still written beside it, so a build that
85
86
  // knows only `auto` reads the same switching on/off. Whenever the two
@@ -13,7 +13,6 @@ export const AGENT_PROVIDER_ENV = Object.freeze({
13
13
  gemini: 'GEMINI_API_KEY',
14
14
  deepseek: 'DEEPSEEK_API_KEY',
15
15
  xai: 'XAI_API_KEY',
16
- 'opencode-go': 'OPENCODE_API_KEY',
17
16
  openrouter: 'OPENROUTER_API_KEY',
18
17
  });
19
18
 
@@ -88,7 +88,7 @@ export function readSourceWindow(text) {
88
88
  rows.push({ number, content: row[2] });
89
89
  continue;
90
90
  }
91
- const metadata = /^\[lines (\d+)-(\d+) of (\d+)(?:; pass offset:\d+ to continue)?\]$/.exec(line);
91
+ const metadata = /^\[lines (\d+)-(\d+) of (\d+)(?:; offset:\d+ for more)?\]$/.exec(line);
92
92
  if (!metadata || footer) return null;
93
93
  footer = metadata;
94
94
  totalLines = Number(metadata[3]);
@@ -1,4 +1,4 @@
1
- import { freemem, totalmem } from 'node:os';
1
+ import { availableParallelism, freemem, totalmem } from 'node:os';
2
2
  import { AsyncLocalStorage } from 'node:async_hooks';
3
3
  import { requestMemoryPressureSnapshot } from './memory-snapshot.mjs';
4
4
  import { nonNegativeInt, positiveInt } from './numbers.mjs';
@@ -6,12 +6,20 @@ import { nonNegativeInt, positiveInt } from './numbers.mjs';
6
6
  const MB = 1024 * 1024;
7
7
 
8
8
  // The unified daemon queues bursts instead of starting an unbounded number of
9
- // agent/provider/shell tasks in one process. Environment overrides remain
10
- // available for larger hosts; memory thresholds are diagnostic only.
9
+ // agent/provider tasks in one process. Shell commands start immediately: their
10
+ // only bound is maxShells (64) live foreground commands, a runaway guard
11
+ // rather than a CPU budget; the OS shares the cores. Environment overrides remain available; memory thresholds
12
+ // are diagnostic only.
13
+ // Agent leases are held only while an agent runs tools (they yield during
14
+ // provider I/O) and their tool work is mostly child-process I/O, so two per
15
+ // core keeps the host busy: 30-agent stress on 20 cores halved wall time at
16
+ // 40 versus 8 with no event-loop or memory cost. 8 is the floor for small
17
+ // hosts. maxHighLoad now bounds agents only and defaults to the same.
18
+ const AGENT_LIMIT = Math.max(8, 2 * availableParallelism());
11
19
  const RESOURCE_ADMISSION_DEFAULTS = Object.freeze({
12
- maxAgents: 8,
13
- maxShells: 8,
14
- maxHighLoad: 12,
20
+ maxAgents: AGENT_LIMIT,
21
+ maxShells: 64,
22
+ maxHighLoad: AGENT_LIMIT,
15
23
  maxQueue: 1024,
16
24
  minFreeMemoryMb: 0,
17
25
  maxRssMb: 0,
@@ -79,12 +87,15 @@ class ResourceAdmissionQueueFullError extends ResourcePressureError {
79
87
  * shells. Running work is never revoked: pressure only rejects a new request or
80
88
  * keeps a concurrency-blocked request in the bounded FIFO queue.
81
89
  *
82
- * A shell lease covers only the foreground part of a command: promotion to a
83
- * background job releases it (lib/shell-run-background/promote-job.mjs), so
84
- * background jobs count toward neither maxShells nor the maxHighLoad budget
85
- * shared with agents, and are not capped at all: they are mostly idle
86
- * servers/watchers at below-normal OS priority whose memory lives in their own
87
- * processes. Session/runtime close stops them (shell-jobs shutdownShellJobs).
90
+ * Shell leases are their own pool of maxShells: they are not part of the
91
+ * maxHighLoad budget, do not suspend the calling agent's lease, and never wait
92
+ * behind queued agent work, so a command starts at once unless maxShells
93
+ * commands are already running. A shell lease covers only the foreground part
94
+ * of a command: promotion to a background job releases it
95
+ * (lib/shell-run-background/promote-job.mjs), so background jobs are not
96
+ * capped at all: they are mostly idle servers/watchers at below-normal OS
97
+ * priority whose memory lives in their own processes. Session/runtime close
98
+ * stops them (shell-jobs shutdownShellJobs).
88
99
  */
89
100
  export class ResourceAdmissionController {
90
101
  constructor({ limits = {}, metrics = defaultMetrics, now = Date.now, env = process.env } = {}) {
@@ -98,6 +109,20 @@ export class ResourceAdmissionController {
98
109
  // Live leases for saturation diagnostics only (labels/ages in snapshot()).
99
110
  this.activeLeases = new Set();
100
111
  this.context = new AsyncLocalStorage();
112
+ // Admission waits by pool: first acquisition (agent/shell) and the
113
+ // reacquisition after a yielded provider call (restore). Shows whether
114
+ // the agent cap actually delays work.
115
+ this.waitStats = {};
116
+ }
117
+
118
+ _recordWait(pool, waitedMs) {
119
+ this.waitStats[pool] ??= { admitted: 0, waited: 0, totalWaitMs: 0, maxWaitMs: 0 };
120
+ const stats = this.waitStats[pool];
121
+ stats.admitted += 1;
122
+ if (waitedMs <= 0) return;
123
+ stats.waited += 1;
124
+ stats.totalWaitMs += waitedMs;
125
+ stats.maxWaitMs = Math.max(stats.maxWaitMs, waitedMs);
101
126
  }
102
127
 
103
128
  _recordMemoryPressure(kind) {
@@ -129,9 +154,14 @@ export class ResourceAdmissionController {
129
154
  }
130
155
 
131
156
  _canStart(kind) {
132
- const total = this.active.agent + this.active.shell;
133
- const kindLimit = kind === 'shell' ? this.limits.maxShells : this.limits.maxAgents;
134
- return total < this.limits.maxHighLoad && this.active[kind] < kindLimit;
157
+ if (kind === 'shell') return this.active.shell < this.limits.maxShells;
158
+ return this.active.agent < this.limits.maxHighLoad && this.active.agent < this.limits.maxAgents;
159
+ }
160
+
161
+ // Shell and agent waiters never delay each other's new requests.
162
+ _queuedInPool(lane) {
163
+ const shell = lane === 'shell';
164
+ return this.queue.some((item) => (item.kind === 'shell') === shell);
135
165
  }
136
166
 
137
167
  _adjustOwnerCount(kind, ownerKey, delta) {
@@ -240,6 +270,7 @@ export class ResourceAdmissionController {
240
270
  },
241
271
  };
242
272
  this.activeLeases.add(lease);
273
+ this._recordWait(kind, lease.queuedMs);
243
274
  return lease;
244
275
  }
245
276
 
@@ -294,8 +325,9 @@ export class ResourceAdmissionController {
294
325
  this._recordMemoryPressure(lane);
295
326
  const ambientParent = this.context.getStore();
296
327
  const detachedDependency = dependency === 'detached' && ambientParent?.controller === this;
297
- const parent = detachedDependency ? null : this._suspendParent(ambientParent);
298
- if (detachedDependency && (!this._canStart(lane) || this.queue.length > 0)) {
328
+ // A shell runs beside its agent instead of taking the agent's slot.
329
+ const parent = detachedDependency || lane === 'shell' ? null : this._suspendParent(ambientParent);
330
+ if (detachedDependency && (!this._canStart(lane) || this._queuedInPool(lane))) {
299
331
  const error = new ResourcePressureError(
300
332
  `detached nested ${lane} work has no admission capacity; retry after running work completes`,
301
333
  { kind: lane }
@@ -303,7 +335,7 @@ export class ResourceAdmissionController {
303
335
  error.code = 'ERESOURCEDEPENDENCY';
304
336
  return Promise.reject(error);
305
337
  }
306
- if (this._canStart(lane) && this.queue.length === 0) {
338
+ if (this._canStart(lane) && !this._queuedInPool(lane)) {
307
339
  const lease = this._lease(lane, null, parent, owner, taskPriority);
308
340
  lease.label = label;
309
341
  lease.signal = signal;
@@ -439,6 +471,7 @@ export class ResourceAdmissionController {
439
471
  if (item.restore) {
440
472
  item.parent.restorePending = null;
441
473
  item.parent.counted = true;
474
+ this._recordWait(`${item.kind}Restore`, Math.max(0, this.now() - item.queuedAt));
442
475
  this.active[item.kind] += 1;
443
476
  this._adjustOwnerCount(item.kind, item.ownerKey, 1);
444
477
  item.resolve();
@@ -461,6 +494,15 @@ export class ResourceAdmissionController {
461
494
  shell: this.activeByOwner.shell.size,
462
495
  },
463
496
  queued: this.queue.length,
497
+ waitStats: Object.fromEntries(
498
+ Object.entries(this.waitStats).map(([pool, stats]) => [
499
+ pool,
500
+ {
501
+ ...stats,
502
+ averageQueuedMs: stats.waited > 0 ? Math.round(stats.totalWaitMs / stats.waited) : 0,
503
+ },
504
+ ])
505
+ ),
464
506
  limits: {
465
507
  ...this.limits,
466
508
  maxAgents: wireLimit(this.limits.maxAgents),
@@ -0,0 +1,84 @@
1
+ // One child process run to completion, its output collected — the body of
2
+ // the off-thread runners (git-runner-worker.mjs, process-runner-worker.mjs).
3
+ // Starting a child process is a synchronous native call (CreateProcess on
4
+ // Windows, amplified by on-access scanning and seconds long under load), so
5
+ // those workers spawn here instead of on the host event loop.
6
+ // Resolves to { code, stdout, stderr }; rejects with code ETIMEDOUT after
7
+ // timeoutMs (the child is killed), EMAXBUFFER once stdout+stderr exceed
8
+ // maxBytes, or the spawn error itself.
9
+ import { spawn } from 'node:child_process';
10
+
11
+ export function runChild({ command, args, cwd, input, env, timeoutMs, maxBytes, stdoutEncoding = 'utf8' }) {
12
+ return new Promise((resolvePromise, rejectPromise) => {
13
+ const hasInput = (typeof input === 'string' || ArrayBuffer.isView(input)) && input.length > 0;
14
+ const child = spawn(command, args, {
15
+ cwd,
16
+ ...(env ? { env } : {}),
17
+ windowsHide: true,
18
+ stdio: [hasInput ? 'pipe' : 'ignore', 'pipe', 'pipe'],
19
+ });
20
+ const stdout = [];
21
+ const stderr = [];
22
+ let outputBytes = 0;
23
+ let stdinError = null;
24
+ let settled = false;
25
+ const finish = (error, result) => {
26
+ if (settled) return;
27
+ settled = true;
28
+ clearTimeout(timer);
29
+ if (error) rejectPromise(error);
30
+ else resolvePromise(result);
31
+ };
32
+ // timeoutMs 0: no deadline (the caller's own work bounds the child).
33
+ const timer =
34
+ timeoutMs > 0
35
+ ? setTimeout(() => {
36
+ const error = new Error(`${command} ${args.join(' ')} timed out after ${timeoutMs}ms`);
37
+ error.code = 'ETIMEDOUT';
38
+ try {
39
+ child.kill();
40
+ } catch {}
41
+ finish(error);
42
+ }, timeoutMs)
43
+ : null;
44
+ const collect = (chunks, chunk) => {
45
+ const next = Buffer.from(chunk);
46
+ outputBytes += next.length;
47
+ if (outputBytes > maxBytes) {
48
+ const error = new Error(`${command} ${args.join(' ')} exceeded ${maxBytes} output bytes`);
49
+ error.code = 'EMAXBUFFER';
50
+ try {
51
+ child.kill();
52
+ } catch {}
53
+ finish(error);
54
+ return;
55
+ }
56
+ chunks.push(next);
57
+ };
58
+ child.stdout.on('data', (chunk) => collect(stdout, chunk));
59
+ child.stderr.on('data', (chunk) => collect(stderr, chunk));
60
+ child.once('error', (error) => finish(error));
61
+ child.once('close', (code) => {
62
+ const result = {
63
+ code: Number(code ?? 1),
64
+ // 'base64' keeps non-UTF-8 bytes (a replayable patch) intact across the thread hop.
65
+ stdout: Buffer.concat(stdout).toString(stdoutEncoding === 'base64' ? 'base64' : 'utf8'),
66
+ stderr: Buffer.concat(stderr).toString('utf8'),
67
+ };
68
+ // A failed stdin write only matters when the child otherwise reported success.
69
+ if (stdinError && result.code === 0) {
70
+ finish(stdinError);
71
+ return;
72
+ }
73
+ finish(null, result);
74
+ });
75
+ if (child.stdin) {
76
+ // A short-lived child can close before stdin is flushed. Keep that
77
+ // transport race inside this promise instead of an unhandled EPIPE.
78
+ child.stdin.on('error', (error) => {
79
+ stdinError = error;
80
+ });
81
+ child.stdin.end(input);
82
+ }
83
+ });
84
+ }
@@ -0,0 +1,23 @@
1
+ // session-activity-signal.mjs
2
+ // Process-local "a session's activity state may have changed" pulse: a turn
3
+ // started or settled, a task wait began or ended, a background task was
4
+ // handed off or finished. Status surfaces that project activity from the
5
+ // runtime map (the Agents pool) listen and re-read at once instead of waiting
6
+ // for their next store event. Import-free leaf.
7
+ const listeners = new Set();
8
+
9
+ export function onSessionActivityChange(listener) {
10
+ if (typeof listener !== 'function') return () => {};
11
+ listeners.add(listener);
12
+ return () => listeners.delete(listener);
13
+ }
14
+
15
+ export function emitSessionActivityChange(sessionId = '') {
16
+ for (const listener of [...listeners]) {
17
+ try {
18
+ listener(String(sessionId || ''));
19
+ } catch {
20
+ /* a listener's failure never affects the transition */
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ // Formatting shared by every surface that hands shell output to an agent.
2
+ // Program output is untrusted text: anything shaped like a harness/system tag
3
+ // is escaped so it can never forge a notice, a result boundary or a reminder.
4
+ // (Capping lives in tools/builtin/lib/shell-delivery.mjs, per-job cursors in
5
+ // tools/builtin/lib/shell-task-output.mjs.)
6
+
7
+ // A tag-looking token: `<name`, `</name`, where the name is a known
8
+ // harness/system/transcript word or any hyphenated/underscored/namespaced
9
+ // name (task-notification, system-reminder, tool_result, ns:tag ...). Plain
10
+ // HTML-ish words like <div> or a comparison `a<b` stay readable.
11
+ const HARNESS_TAG =
12
+ /<(\/?)(?=(?:[a-z][\w]*[-_:][\w:-]*|system|result|results|summary|status|error|user|assistant|human|tool|function|functions|instructions|context|notification|reminder|output|invoke|parameter|thinking)\b)/gi;
13
+
14
+ export function escapeHarnessTags(text) {
15
+ return String(text ?? '').replace(HARNESS_TAG, '&lt;$1');
16
+ }
17
+
18
+ export function formatDuration(ms) {
19
+ if (!(ms >= 0)) return '';
20
+ if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
21
+ const minutes = Math.floor(ms / 60_000);
22
+ return `${minutes}m ${Math.round((ms % 60_000) / 1000)}s`;
23
+ }
@@ -1,8 +1,23 @@
1
- import { mkdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
1
+ import { mkdirSync, renameSync, rmdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
+ import { readFile, rename, rm, rmdir, stat, writeFile } from 'node:fs/promises';
2
3
  import { dirname } from 'node:path';
4
+ import { withFileLock, withFileLockSync } from './file-lock.mjs';
3
5
  import { readJsonSafe } from './json-file.mjs';
4
6
  import { isPidAlive } from './pid-liveness.mjs';
5
- import { sleepSync } from './sleep.mjs';
7
+
8
+ // Claim-lock protocol shared by the sync and async paths: `<path>.claim.lock`
9
+ // is a file-lock.mjs lock, published atomically with its holder's pid and
10
+ // token. A holder whose pid is alive is waited out however long it holds the
11
+ // lock — a holder stalled mid-refresh still owns it: reclaiming it on age let
12
+ // a successor claim the record and the stalled refresh then overwrite it. Its
13
+ // lock is reclaimed only once that pid is dead, and only the holder's own
14
+ // acquisition (pid + token) is ever released. A lock naming no holder — the
15
+ // bare directory versions before this one took the lock with — is reclaimed
16
+ // once older than CLAIM_LOCK_STALE_MS, as those versions did.
17
+ const CLAIM_LOCK_TIMEOUT_MS = 500;
18
+ const CLAIM_LOCK_STALE_MS = 5000;
19
+ const CLAIM_LOCK_OPTS = Object.freeze({ timeoutMs: CLAIM_LOCK_TIMEOUT_MS, staleMs: CLAIM_LOCK_STALE_MS });
20
+ const BUSY = Symbol('claim lock busy');
6
21
 
7
22
  function writeJsonAtomic(path, value) {
8
23
  mkdirSync(dirname(path), { recursive: true });
@@ -11,28 +26,82 @@ function writeJsonAtomic(path, value) {
11
26
  renameSync(tmp, path);
12
27
  }
13
28
 
14
- function acquireClaimLock(path) {
29
+ // Held by another live process or this one's async holder ('busy'). Any other
30
+ // failure (an unwritable data directory) is thrown, as before.
31
+ function claimLockHeld(error) {
32
+ return error?.code === 'ELOCKCONTENDED' || (error?.code === 'ELOCKTIMEOUT' && error.cause?.code === 'EEXIST');
33
+ }
34
+
35
+ function legacyLockStale(info) {
36
+ return info.isDirectory() && Date.now() - info.mtimeMs > CLAIM_LOCK_STALE_MS;
37
+ }
38
+
39
+ // An older version's stale bare lock directory is removed with rmdir alone,
40
+ // never recursive or forced: between the stat and the removal a contender may
41
+ // have replaced it with a live owner-named lock file. rmdir refuses a file
42
+ // (ENOENT on Windows, ENOTDIR elsewhere) and a non-empty directory
43
+ // (ENOTEMPTY); a missing path (ENOENT) is already gone. Any refusal (EPERM
44
+ // included) leaves the path untouched to the shared acquire loop below, which
45
+ // waits a live holder out and reclaims only a dead one's lock.
46
+
47
+ // `fn()` under the claim lock, or BUSY. file-lock creates a missing data
48
+ // directory (a first boot, e.g. a daemon an ssh bootstrap starts on a fresh
49
+ // server).
50
+ function withClaimLockSync(path, fn) {
15
51
  const lockPath = `${path}.claim.lock`;
16
- // A first boot (e.g. a daemon an ssh bootstrap starts on a fresh server)
17
- // has no data directory yet: the lock's parent must exist.
18
- mkdirSync(dirname(path), { recursive: true });
19
- for (let i = 0; i < 20; i++) {
20
- try {
21
- mkdirSync(lockPath, { recursive: false });
22
- return lockPath;
23
- } catch (error) {
24
- if (error?.code !== 'EEXIST') throw error;
25
- try {
26
- const ageMs = Date.now() - statSync(lockPath).mtimeMs;
27
- if (ageMs > 5000) {
28
- rmSync(lockPath, { recursive: true, force: true });
29
- continue;
30
- }
31
- } catch {}
32
- sleepSync(25);
33
- }
52
+ try {
53
+ if (legacyLockStale(statSync(lockPath))) rmdirSync(lockPath);
54
+ } catch {}
55
+ let entered = false;
56
+ try {
57
+ return withFileLockSync(
58
+ lockPath,
59
+ () => {
60
+ entered = true;
61
+ return fn();
62
+ },
63
+ CLAIM_LOCK_OPTS
64
+ );
65
+ } catch (error) {
66
+ if (!entered && claimLockHeld(error)) return BUSY;
67
+ throw error;
68
+ }
69
+ }
70
+
71
+ // The same lock taken without blocking the event loop: every file operation
72
+ // and every retry wait is awaited (a synchronous refresh once held the
73
+ // daemon's loop for 20 s in one rmSync under disk contention).
74
+ async function withClaimLock(path, fn) {
75
+ const lockPath = `${path}.claim.lock`;
76
+ try {
77
+ if (legacyLockStale(await stat(lockPath))) await rmdir(lockPath);
78
+ } catch {}
79
+ let entered = false;
80
+ try {
81
+ return await withFileLock(
82
+ lockPath,
83
+ () => {
84
+ entered = true;
85
+ return fn();
86
+ },
87
+ CLAIM_LOCK_OPTS
88
+ );
89
+ } catch (error) {
90
+ if (!entered && claimLockHeld(error)) return BUSY;
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ function namesOtherPid(record, pid) {
96
+ return Boolean(record?.pid) && Number(record.pid) !== Number(pid);
97
+ }
98
+
99
+ async function readJsonSafeAsync(path) {
100
+ try {
101
+ return JSON.parse(await readFile(path, 'utf8'));
102
+ } catch {
103
+ return null;
34
104
  }
35
- return null;
36
105
  }
37
106
 
38
107
  // A live pid alone does not prove the record's process still owns it: the OS
@@ -64,18 +133,15 @@ export function claimSingletonOwner(path, { kind = 'runtime', pid = process.pid,
64
133
  return { owned: false, owner: current };
65
134
  }
66
135
 
67
- const lockPath = acquireClaimLock(path);
68
- if (!lockPath) return { owned: false, owner: readJsonSafe(path) };
69
- try {
136
+ const result = withClaimLockSync(path, () => {
70
137
  const lockedCurrent = readJsonSafe(path);
71
138
  if (livePeer(lockedCurrent)) {
72
139
  return { owned: false, owner: lockedCurrent };
73
140
  }
74
141
  writeJsonAtomic(path, owner);
75
142
  return { owned: true, owner };
76
- } finally {
77
- rmSync(lockPath, { recursive: true, force: true });
78
- }
143
+ });
144
+ return result === BUSY ? { owned: false, owner: readJsonSafe(path) } : result;
79
145
  }
80
146
 
81
147
  export function releaseSingletonOwner(path, pid = process.pid) {
@@ -94,16 +160,37 @@ export function releaseSingletonOwner(path, pid = process.pid) {
94
160
  // naming another pid is reported, never overwritten ('superseded'); a claim
95
161
  // lock held past its wait leaves the record alone ('busy').
96
162
  export function refreshSingletonOwner(path, owner) {
97
- const lockPath = acquireClaimLock(path);
98
- if (!lockPath) return { status: 'busy', owner: null };
99
- try {
163
+ const result = withClaimLockSync(path, () => {
100
164
  const current = readJsonSafe(path);
101
- if (current?.pid && Number(current.pid) !== Number(owner.pid)) return { status: 'superseded', owner: current };
165
+ if (namesOtherPid(current, owner.pid)) return { status: 'superseded', owner: current };
102
166
  writeJsonAtomic(path, owner);
103
167
  return { status: 'refreshed', owner };
104
- } finally {
105
- rmSync(lockPath, { recursive: true, force: true });
106
- }
168
+ });
169
+ return result === BUSY ? { status: 'busy', owner: null } : result;
170
+ }
171
+
172
+ /** refreshSingletonOwner without blocking the event loop: the same claim
173
+ * lock (mutually exclusive with the sync claim/refresh/handoff), the same
174
+ * verdicts, every file operation and retry wait awaited. Its awaited write
175
+ * may stall, so the record is read again right before it is replaced: a
176
+ * record another pid wrote meanwhile is reported, never overwritten. */
177
+ export async function refreshSingletonOwnerAsync(path, owner) {
178
+ const result = await withClaimLock(path, async () => {
179
+ const current = await readJsonSafeAsync(path);
180
+ if (namesOtherPid(current, owner.pid)) return { status: 'superseded', owner: current };
181
+ let tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
182
+ try {
183
+ await writeFile(tmp, `${JSON.stringify(owner, null, 2)}\n`);
184
+ const latest = await readJsonSafeAsync(path);
185
+ if (namesOtherPid(latest, owner.pid)) return { status: 'superseded', owner: latest };
186
+ await rename(tmp, path);
187
+ tmp = null;
188
+ return { status: 'refreshed', owner };
189
+ } finally {
190
+ if (tmp) await rm(tmp, { force: true }).catch(() => {});
191
+ }
192
+ });
193
+ return result === BUSY ? { status: 'busy', owner: null } : result;
107
194
  }
108
195
 
109
196
  // Atomic ownership handoff: replace the owner record from fromPid to toOwner
@@ -113,11 +200,9 @@ export function refreshSingletonOwner(path, owner) {
113
200
  // (someone else won), the handoff is refused (owned:false) and the caller
114
201
  // should fall back to the winner rather than overwrite it.
115
202
  export function handoffSingletonOwner(path, fromPid, toOwner = {}) {
116
- const lockPath = acquireClaimLock(path);
117
- if (!lockPath) return { owned: false, owner: readJsonSafe(path) };
118
- try {
203
+ const result = withClaimLockSync(path, () => {
119
204
  const current = readJsonSafe(path);
120
- if (current?.pid && Number(current.pid) !== Number(fromPid)) {
205
+ if (namesOtherPid(current, fromPid)) {
121
206
  return { owned: false, owner: current };
122
207
  }
123
208
  const owner = {
@@ -127,7 +212,6 @@ export function handoffSingletonOwner(path, fromPid, toOwner = {}) {
127
212
  };
128
213
  writeJsonAtomic(path, owner);
129
214
  return { owned: true, owner };
130
- } finally {
131
- rmSync(lockPath, { recursive: true, force: true });
132
- }
215
+ });
216
+ return result === BUSY ? { owned: false, owner: readJsonSafe(path) } : result;
133
217
  }
@@ -3,6 +3,8 @@
3
3
  // context cost of any one tool call is bounded and consistent. Override with
4
4
  // MIXDOG_TOOL_OUTPUT_MAX_BYTES. Line-based sub-caps (e.g. bash max lines) stay
5
5
  // per-tool; this governs only the byte budget.
6
+ import { capMarker } from './tool-output-marker.mjs';
7
+ export { capMarker, formatCapSize, windowMarker } from './tool-output-marker.mjs';
6
8
  // A positive integer from an env var, or `fallback` when unset or invalid.
7
9
  export function positiveEnvInt(name, fallback) {
8
10
  const v = parseInt(process.env[name], 10);
@@ -53,7 +55,7 @@ export function capLineOrientedToolOutput(result, maxBytes, footerForPrefix) {
53
55
  while (lo <= hi) {
54
56
  const mid = Math.floor((lo + hi) / 2);
55
57
  const kept = lines.slice(0, mid);
56
- const footer = String(footerForPrefix?.(kept, lines) || '... [output budget reached; remainder omitted]');
58
+ const footer = String(footerForPrefix?.(kept, lines) || capMarker({ limit: cap }));
57
59
  const candidate = `${kept.join('\n')}${kept.length ? '\n' : ''}${footer}`;
58
60
  if (Buffer.byteLength(candidate, 'utf8') <= cap) {
59
61
  best = candidate;
@@ -63,5 +65,5 @@ export function capLineOrientedToolOutput(result, maxBytes, footerForPrefix) {
63
65
  }
64
66
  }
65
67
  if (best) return best;
66
- return _prefixByBytes(String(footerForPrefix?.([], lines) || '... [output budget reached; remainder omitted]'), cap);
68
+ return _prefixByBytes(String(footerForPrefix?.([], lines) || capMarker({ limit: cap })), cap);
67
69
  }