mixdog 1.2.4 → 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 (292) 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/mobile-release.mjs +5 -4
  5. package/scripts/provider-pingpong-live.mjs +9 -9
  6. package/src/agents/advisor/AGENT.md +4 -4
  7. package/src/agents/advisor/agent.json +1 -1
  8. package/src/agents/reviewer/AGENT.md +2 -3
  9. package/src/agents/reviewer/agent.json +1 -1
  10. package/src/defaults/shipped-history.json +11 -0
  11. package/src/defaults/skills/browser-use/SKILL.md +2 -1
  12. package/src/defaults/skills/computer-use/SKILL.md +3 -1
  13. package/src/defaults/skills/visualize/SKILL.md +3 -2
  14. package/src/lib/rules-builder.cjs +1 -1
  15. package/src/output-styles/common.md +22 -5
  16. package/src/output-styles/detailed.md +7 -5
  17. package/src/output-styles/minimal.md +7 -3
  18. package/src/output-styles/simple.md +2 -3
  19. package/src/rules/agent/AGENT.md +3 -1
  20. package/src/rules/lead/LEAD.md +10 -7
  21. package/src/rules/shared/10-tool-workflow.md +2 -1
  22. package/src/rules/shared/60-verification.md +4 -5
  23. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -1
  24. package/src/runtime/agent/orchestrator/config.mjs +6 -0
  25. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/internal-agents.mjs +10 -0
  27. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +53 -60
  28. package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
  29. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +2 -3
  30. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +9 -2
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +40 -19
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +3 -2
  33. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events/content-block-start.mjs +4 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn/turn-events.mjs +2 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-sse-turn.mjs +3 -0
  36. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -1
  37. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +3 -1
  38. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +1 -2
  39. package/src/runtime/agent/orchestrator/providers/catalog-deadline.mjs +163 -0
  40. package/src/runtime/agent/orchestrator/providers/catalog-failures.mjs +34 -0
  41. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +38 -14
  42. package/src/runtime/agent/orchestrator/providers/lib/oauth-page.mjs +100 -26
  43. package/src/runtime/agent/orchestrator/providers/lib/send-callbacks.mjs +24 -0
  44. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +13 -0
  45. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +2 -1
  46. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
  47. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +4 -0
  48. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  49. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +49 -4
  51. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +0 -7
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +7 -1
  53. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
  56. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +5 -1
  57. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +11 -2
  58. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +45 -16
  60. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +1 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-ws-connect.mjs +196 -0
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +74 -9
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +14 -1
  64. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -0
  65. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-login.mjs +194 -0
  66. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth-tokens.mjs +353 -0
  67. package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +293 -0
  68. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +33 -100
  69. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +23 -14
  70. package/src/runtime/agent/orchestrator/providers/registry.mjs +41 -27
  71. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +2 -3
  72. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +3 -8
  73. package/src/runtime/agent/orchestrator/session/agent-pool-progress.mjs +15 -3
  74. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +74 -13
  75. package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +102 -0
  76. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +8 -2
  77. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +16 -0
  78. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +9 -0
  79. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +39 -2
  80. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +9 -19
  81. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -3
  82. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -0
  83. package/src/runtime/agent/orchestrator/session/manager/ask-turn-steering-drain.mjs +13 -5
  84. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +10 -7
  85. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +62 -22
  86. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +23 -1
  87. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +81 -34
  88. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -2
  89. package/src/runtime/agent/orchestrator/session/request-fork.mjs +47 -0
  90. package/src/runtime/agent/orchestrator/session/side-question-store.mjs +16 -1
  91. package/src/runtime/agent/orchestrator/session/side-question.mjs +5 -43
  92. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +10 -0
  93. package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +73 -6
  94. package/src/runtime/agent/orchestrator/session/store/load-session.mjs +40 -0
  95. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +15 -0
  96. package/src/runtime/agent/orchestrator/session/store/session-paths.mjs +12 -0
  97. package/src/runtime/agent/orchestrator/session/store-agent-worker-header.mjs +36 -0
  98. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +255 -32
  99. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +306 -0
  100. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +70 -21
  101. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +46 -30
  102. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +144 -45
  103. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +32 -13
  104. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +364 -38
  105. package/src/runtime/agent/orchestrator/session/store.mjs +9 -2
  106. package/src/runtime/agent/orchestrator/session/task-wait-control.mjs +43 -12
  107. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +23 -6
  108. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -6
  109. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +13 -7
  110. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/bashism-rewrite.mjs +1 -1
  111. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/benign-exit.mjs +7 -1
  112. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +40 -7
  113. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +37 -34
  114. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/inline-hoist.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +0 -4
  116. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +29 -27
  118. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -1
  119. package/src/runtime/agent/orchestrator/tools/builtin/find-files-tool.mjs +28 -11
  120. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +12 -3
  121. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +203 -101
  122. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +90 -41
  123. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +17 -1
  124. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-delivery.mjs +199 -0
  125. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +165 -39
  126. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +25 -13
  127. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +7 -6
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +19 -4
  129. package/src/runtime/agent/orchestrator/tools/builtin/read-line-collector.mjs +5 -3
  130. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +25 -7
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +2 -1
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-render.mjs +3 -1
  133. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +12 -5
  134. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +61 -22
  135. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +32 -8
  136. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +193 -19
  137. package/src/runtime/agent/orchestrator/tools/builtin/shell-direct-exe.mjs +57 -7
  138. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +119 -32
  139. package/src/runtime/agent/orchestrator/tools/builtin/shell-lossless-compact.mjs +26 -1
  140. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +13 -28
  141. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +90 -11
  142. package/src/runtime/agent/orchestrator/tools/builtin/wait-budgets.mjs +23 -0
  143. package/src/runtime/agent/orchestrator/tools/builtin.mjs +54 -10
  144. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +22 -28
  145. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +11 -7
  146. package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +126 -9
  147. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  148. package/src/runtime/agent/orchestrator/tools/lib/owned-shell-trees.mjs +15 -6
  149. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +40 -2
  150. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +2 -2
  151. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promotion-results.mjs +13 -3
  152. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +2 -0
  153. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -1
  154. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +1 -1
  155. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +35 -4
  156. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +7 -2
  157. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +39 -23
  158. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +6 -2
  159. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +13 -13
  160. package/src/runtime/attachments/office-extract.mjs +11 -9
  161. package/src/runtime/memory/lib/embedding-tokenizer.mjs +32 -18
  162. package/src/runtime/memory/lib/runtime-fetcher.mjs +1 -1
  163. package/src/runtime/memory/lib/tool-call-handler.mjs +7 -5
  164. package/src/runtime/office/authoring/pptx-script-normalize.mjs +11 -8
  165. package/src/runtime/office/core/tabular.mjs +1 -1
  166. package/src/runtime/office/design/library/design-template-inspect.mjs +7 -4
  167. package/src/runtime/office/portable/docx-formatting.mjs +1 -1
  168. package/src/runtime/office/portable/docx-runs.mjs +1 -1
  169. package/src/runtime/office/portable/docx-tracked-edits.mjs +2 -2
  170. package/src/runtime/office/portable/portable-docx-edits.mjs +11 -10
  171. package/src/runtime/office/portable/portable-docx-operations.mjs +21 -17
  172. package/src/runtime/office/portable/portable-docx-parts.mjs +15 -9
  173. package/src/runtime/office/portable/portable-docx-styles.mjs +4 -1
  174. package/src/runtime/office/portable/portable-docx-xml.mjs +9 -9
  175. package/src/runtime/office/portable/portable-opc.mjs +27 -13
  176. package/src/runtime/office/portable/portable-pivot.mjs +3 -3
  177. package/src/runtime/office/portable/portable-pptx-chart.mjs +3 -3
  178. package/src/runtime/office/portable/portable-pptx-charts.mjs +24 -18
  179. package/src/runtime/office/portable/portable-pptx-core.mjs +5 -5
  180. package/src/runtime/office/portable/portable-pptx-deck.mjs +8 -4
  181. package/src/runtime/office/portable/portable-pptx-package.mjs +7 -4
  182. package/src/runtime/office/portable/portable-pptx-shapes.mjs +18 -9
  183. package/src/runtime/office/portable/portable-sheet-parts.mjs +9 -3
  184. package/src/runtime/office/portable/portable-sheet-xml.mjs +5 -5
  185. package/src/runtime/office/portable/portable-xlsx-charts.mjs +4 -1
  186. package/src/runtime/office/portable/portable-xlsx-drawings.mjs +10 -6
  187. package/src/runtime/office/portable/portable-xlsx-operations.mjs +8 -5
  188. package/src/runtime/office/portable/portable-xlsx-pivot-ops.mjs +1 -1
  189. package/src/runtime/office/portable/portable-xlsx-reference-shift.mjs +11 -7
  190. package/src/runtime/office/portable/portable-xlsx-sheet-edits.mjs +6 -3
  191. package/src/runtime/office/portable/portable-xml.mjs +1 -1
  192. package/src/runtime/shared/atomic-file.mjs +22 -13
  193. package/src/runtime/shared/background-tasks.mjs +60 -20
  194. package/src/runtime/shared/child-spawn-gate.mjs +26 -16
  195. package/src/runtime/shared/config.mjs +0 -10
  196. package/src/runtime/shared/file-lock.mjs +7 -1
  197. package/src/runtime/shared/file-permissions-worker.mjs +38 -0
  198. package/src/runtime/shared/file-permissions.d.mts +19 -4
  199. package/src/runtime/shared/file-permissions.mjs +310 -4
  200. package/src/runtime/shared/git-runner-worker.mjs +6 -78
  201. package/src/runtime/shared/llm/http-agent.mjs +7 -3
  202. package/src/runtime/shared/llm/provider-catalog-registry.mjs +10 -0
  203. package/src/runtime/shared/llm/usage-accounting.mjs +2 -0
  204. package/src/runtime/shared/llm/usage-ledger-import.mjs +32 -3
  205. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +12 -5
  206. package/src/runtime/shared/llm/usage-ledger-worker.mjs +1 -1
  207. package/src/runtime/shared/llm/usage-ledger.mjs +20 -5
  208. package/src/runtime/shared/memory-snapshot.mjs +5 -1
  209. package/src/runtime/shared/notify-trace.mjs +8 -1
  210. package/src/runtime/shared/pristine-execution-contract.json +5 -1
  211. package/src/runtime/shared/process-runner-worker.mjs +7 -0
  212. package/src/runtime/shared/process-runner.mjs +38 -0
  213. package/src/runtime/shared/provider-accounts.mjs +2 -1
  214. package/src/runtime/shared/provider-api-key.mjs +0 -1
  215. package/src/runtime/shared/read-row-numbers.mjs +1 -1
  216. package/src/runtime/shared/resource-admission.mjs +60 -18
  217. package/src/runtime/shared/run-child.mjs +84 -0
  218. package/src/runtime/shared/session-activity-signal.mjs +23 -0
  219. package/src/runtime/shared/shell-output-summary.mjs +23 -0
  220. package/src/runtime/shared/singleton-owner.mjs +126 -42
  221. package/src/runtime/shared/tool-output-limit.mjs +4 -2
  222. package/src/runtime/shared/tool-output-marker.mjs +58 -0
  223. package/src/runtime/shared/user-data-guard.mjs +28 -9
  224. package/src/runtime/shared/worktrees/activity-gate.mjs +33 -8
  225. package/src/runtime/shared/worktrees/git-head.mjs +20 -2
  226. package/src/runtime/shared/worktrees/lock.mjs +3 -2
  227. package/src/runtime/terminal-bridge/tool-defs.mjs +1 -1
  228. package/src/runtime/tidy/runners/biome.mjs +3 -1
  229. package/src/runtime/tidy/shadow.mjs +2 -1
  230. package/src/runtime/web-search/index.mjs +5 -7
  231. package/src/runtime/web-search/lib/formatter.mjs +42 -6
  232. package/src/runtime/web-search/tool-defs.mjs +6 -1
  233. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +4 -3
  234. package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -4
  235. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +3 -3
  236. package/src/session-runtime/lifecycle/shared.mjs +19 -2
  237. package/src/session-runtime/lifecycle/teardown/session-close.mjs +2 -0
  238. package/src/session-runtime/lifecycle/teardown.mjs +5 -2
  239. package/src/session-runtime/lifecycle-api.mjs +3 -1
  240. package/src/session-runtime/provider-auth/login.mjs +10 -6
  241. package/src/session-runtime/provider-auth/setup.mjs +19 -2
  242. package/src/session-runtime/provider-catalog-cache.mjs +101 -25
  243. package/src/session-runtime/provider-models/catalog-load.mjs +14 -3
  244. package/src/session-runtime/provider-models/row-hydration.mjs +4 -0
  245. package/src/session-runtime/provider-models.mjs +76 -10
  246. package/src/session-runtime/provider-readiness.mjs +4 -0
  247. package/src/session-runtime/services/agent-tool/close-flow.mjs +2 -1
  248. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +10 -2
  249. package/src/session-runtime/services/agent-tool/job-views/session-progress.mjs +13 -4
  250. package/src/session-runtime/services/agent-tool/notify.mjs +15 -4
  251. package/src/session-runtime/services/agent-tool/restart-notify.mjs +134 -0
  252. package/src/session-runtime/services/agent-tool/send-flow.mjs +24 -1
  253. package/src/session-runtime/services/agent-tool/spawn-flow/deferred-spawn.mjs +2 -1
  254. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -3
  255. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +5 -1
  256. package/src/session-runtime/services/agent-tool/tag-registry/tag-lookup.mjs +8 -3
  257. package/src/session-runtime/services/agent-tool/tag-registry.mjs +3 -0
  258. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +4 -0
  259. package/src/session-runtime/services/agent-tool/worker-index.mjs +44 -10
  260. package/src/session-runtime/services/agent-tool.mjs +20 -0
  261. package/src/session-runtime/services/provider-admin.mjs +92 -20
  262. package/src/session-runtime/services/usage-dashboard-model.mjs +2 -2
  263. package/src/session-runtime/services/usage-dashboard.mjs +27 -69
  264. package/src/session-runtime/turn/session-ops.mjs +2 -2
  265. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  266. package/src/session-runtime/usage-stats-api.mjs +6 -1
  267. package/src/standalone/channel-transport/http-routes.mjs +10 -9
  268. package/src/standalone/daemon-desktop-runtime.mjs +20 -7
  269. package/src/standalone/daemon-owner.mjs +117 -25
  270. package/src/standalone/daemon.mjs +43 -13
  271. package/src/standalone/desktop-service-registry.mjs +32 -5
  272. package/src/standalone/session-call-cache.mjs +6 -0
  273. package/src/standalone/session-protocol.mjs +1 -1
  274. package/src/standalone/session-service/retention/busy.mjs +7 -2
  275. package/src/standalone/session-transport/routes.mjs +7 -6
  276. package/src/tui/app/model-options.mjs +39 -2
  277. package/src/tui/app/model-picker.mjs +16 -2
  278. package/src/tui/app/provider-setup-picker.mjs +1 -2
  279. package/src/tui/dist/index.mjs +436 -123
  280. package/src/tui/session/notice-text.mjs +0 -1
  281. package/src/tui/session/session-api/intake/submission.mjs +7 -13
  282. package/src/tui/session/session-api/integrations.mjs +1 -5
  283. package/src/tui/session/session-flow/drain.mjs +8 -0
  284. package/src/tui/session/session-flow/steering.mjs +46 -0
  285. package/src/tui/session/session-flow.mjs +2 -0
  286. package/src/tui/session/turn-stream.mjs +33 -0
  287. package/src/tui/session/turn.mjs +30 -3
  288. package/src/ui/statusline.mjs +1 -6
  289. package/src/ui/streaming-markdown-heal.mjs +94 -25
  290. package/src/vendor/statusline/bin/statusline-route.mjs +0 -1
  291. package/src/vendor/statusline/src/gateway/route-meta.mjs +0 -1
  292. package/src/workflows/default/WORKFLOW.md +9 -13
@@ -2,11 +2,6 @@ import {
2
2
  fetchOAuthUsageSnapshot,
3
3
  readCachedOAuthUsageSnapshot,
4
4
  } from '../../runtime/agent/orchestrator/providers/oauth-usage.mjs';
5
- import {
6
- fetchOpenCodeGoUsageSnapshot,
7
- openCodeGoUsageConfigStatus,
8
- readCachedOpenCodeGoUsageSnapshot,
9
- } from '../../runtime/agent/orchestrator/providers/opencode-go-usage.mjs';
10
5
  import {
11
6
  fetchApiUsageSnapshot,
12
7
  readCachedApiUsageSnapshot,
@@ -118,69 +113,32 @@ async function apiProviderRow(item, { config, providers, preview, refreshFor, sn
118
113
  if (row.authenticated) {
119
114
  let hasQuota = false;
120
115
  let apiUsageSnapshot = null;
121
- if (item.id === 'opencode-go') {
122
- const usageStatus = openCodeGoUsageConfigStatus(config);
123
- try {
124
- let snapshot = null;
125
- if (refreshFor(item.id)) snapshot = await fetchOpenCodeGoUsageSnapshot(config, { force: true });
126
- else snapshot = readCachedOpenCodeGoUsageSnapshot();
127
- if (!snapshot && !refreshFor(item.id) && usageStatus.ready) {
128
- snapshot = await fetchOpenCodeGoUsageSnapshot(config);
129
- }
130
- if (snapshot) {
131
- hasQuota = applyWindowQuota(row, snapshot?.quotaWindows, {
132
- source: 'opencode-go-console',
133
- detail: 'subscription quota',
134
- });
135
- row.updatedAt = num(snapshot?.cachedAt, null);
136
- } else {
137
- row.status = 'missing';
138
- row.source = 'usage-auth-missing';
139
- row.sourceLabel = 'usage auth';
140
- row.primary = '';
141
- row.detail = usageStatus.ready
142
- ? 'OpenCode Go usage not found'
143
- : 'Set an OpenCode console API key (all permissions) for usage';
144
- }
145
- } catch (err) {
146
- if (String(err?.code || '').startsWith('OPENCODE_GO_USAGE_')) {
147
- row.status = 'missing';
148
- row.source = 'usage-auth-missing';
149
- row.sourceLabel = 'usage auth';
150
- row.primary = '';
151
- row.detail = usageStatus.ready
152
- ? 'OpenCode Go usage not found'
153
- : 'Set an OpenCode console API key (all permissions) for usage';
154
- }
155
- }
156
- } else {
157
- const snapshot = await apiSnapshot(item.id, snapshotOptions(item.id));
158
- apiUsageSnapshot = snapshot;
159
- const known = snapshotRemaining(snapshot);
160
- const usage = snapshotUsage(snapshot);
161
- const tokenUsage = snapshotTokenUsage(snapshot);
162
- const windows = normaliseWindows(snapshot?.quotaWindows, clean(snapshot?.source) || 'provider-api');
163
- const estimated = localBudget(providerCfg);
164
- row.updatedAt = num(snapshot?.cachedAt, null);
165
- if (known) {
166
- applyKnownRemaining(row, known, { estimated: false });
167
- row.sourceLabel = 'API';
168
- hasQuota = true;
169
- } else if (windows.length) {
170
- hasQuota = applyWindowQuota(row, windows, {
171
- source: clean(snapshot?.source) || 'provider-api',
172
- detail: 'provider quota',
173
- });
174
- } else if (estimated) {
175
- applyKnownRemaining(row, estimated, { estimated: true });
176
- hasQuota = true;
177
- } else if (usage) {
178
- applyKnownUsage(row, usage);
179
- hasQuota = true;
180
- } else if (tokenUsage) {
181
- applyTokenUsage(row, tokenUsage);
182
- hasQuota = true;
183
- }
116
+ const snapshot = await apiSnapshot(item.id, snapshotOptions(item.id));
117
+ apiUsageSnapshot = snapshot;
118
+ const known = snapshotRemaining(snapshot);
119
+ const usage = snapshotUsage(snapshot);
120
+ const tokenUsage = snapshotTokenUsage(snapshot);
121
+ const windows = normaliseWindows(snapshot?.quotaWindows, clean(snapshot?.source) || 'provider-api');
122
+ const estimated = localBudget(providerCfg);
123
+ row.updatedAt = num(snapshot?.cachedAt, null);
124
+ if (known) {
125
+ applyKnownRemaining(row, known, { estimated: false });
126
+ row.sourceLabel = 'API';
127
+ hasQuota = true;
128
+ } else if (windows.length) {
129
+ hasQuota = applyWindowQuota(row, windows, {
130
+ source: clean(snapshot?.source) || 'provider-api',
131
+ detail: 'provider quota',
132
+ });
133
+ } else if (estimated) {
134
+ applyKnownRemaining(row, estimated, { estimated: true });
135
+ hasQuota = true;
136
+ } else if (usage) {
137
+ applyKnownUsage(row, usage);
138
+ hasQuota = true;
139
+ } else if (tokenUsage) {
140
+ applyTokenUsage(row, tokenUsage);
141
+ hasQuota = true;
184
142
  }
185
143
  if (!row.includeInTotal && !hasQuota) {
186
144
  const estimated = localBudget(providerCfg);
@@ -189,7 +147,7 @@ async function apiProviderRow(item, { config, providers, preview, refreshFor, sn
189
147
  hasQuota = true;
190
148
  }
191
149
  }
192
- if (item.id !== 'opencode-go' && !row.includeInTotal && !hasQuota) {
150
+ if (!row.includeInTotal && !hasQuota) {
193
151
  applyApiUnavailable(row, item.id, apiUsageSnapshot);
194
152
  }
195
153
  }
@@ -105,9 +105,9 @@ export function createSessionOps(deps) {
105
105
  return mode;
106
106
  }
107
107
 
108
- function interruptTaskWait(reason = 'user-message') {
108
+ function interruptTaskWait(reason = 'user-message', detail = null) {
109
109
  const sessionId = getSession()?.id || getReservedSessionId?.() || '';
110
- return interruptTaskWaitForSession(sessionId, reason);
110
+ return interruptTaskWaitForSession(sessionId, reason, detail);
111
111
  }
112
112
 
113
113
  // Raw model-message read for LIVE transcript consumers (remote agent
@@ -86,6 +86,7 @@ export function createAskCallbacks({ transcript, notifyFnForSession, scheduleSea
86
86
  },
87
87
  onUsageDelta: options.onUsageDelta,
88
88
  onAssistantToolCallObserved: options.onAssistantToolCallObserved,
89
+ onStreamItemStart: options.onStreamItemStart,
89
90
  onToolResult: (message) => options.onToolResult?.(message),
90
91
  onToolApproval: options.onToolApproval,
91
92
  onCompactEvent: options.onCompactEvent,
@@ -237,7 +237,8 @@ export function createUsageStatsApi({
237
237
  /**
238
238
  * This runtime's session from its first request, compactions included; null before it has one.
239
239
  * `sessionIds` adds the same totals for each named session (the agents it spawned) under
240
- * `sessions`, each read by its own id only.
240
+ * `sessions`, each read by its own id only. `agents: true` adds `agents`: one total of every
241
+ * session ever run for this one (closed and replaced agents included) and the named sessions.
241
242
  */
242
243
  async getSessionUsage(options = {}) {
243
244
  const sessionId = getSessionId();
@@ -249,6 +250,10 @@ export function createUsageStatsApi({
249
250
  const usage = await store.sessionUsageAsync(sessionUsageIdsFor(scopes, sessionId));
250
251
  const result = { sessionId, ...sessionUsageSnapshot(usage) };
251
252
  const others = otherSessionIds(options, sessionId);
253
+ if (options?.agents === true) {
254
+ const named = others.flatMap((id) => sessionUsageIdsFor(scopes, id));
255
+ result.agents = sessionUsageSnapshot(await store.sessionUsageAsync(named, [sessionId]));
256
+ }
252
257
  if (!others.length) return result;
253
258
  const totals = await Promise.all(
254
259
  others.map(async (id) => [
@@ -31,9 +31,10 @@ export function createChannelRoutes({
31
31
  // Checked on every register/call/agent dispatch, first and again right
32
32
  // before the work is handed on (the lease may have been fenced while the
33
33
  // body was still arriving): a daemon that lost (or cannot re-verify) its
34
- // owner lease serves nothing.
35
- function refuseUnadmitted(res) {
36
- if (admitWork()) return false;
34
+ // owner lease serves nothing. `admitWork` may answer asynchronously (a
35
+ // request arriving during the lease's re-verification waits for it).
36
+ async function refuseUnadmitted(res) {
37
+ if (await admitWork()) return false;
37
38
  sendError(res, 'daemon owner lease is not held', 503);
38
39
  return true;
39
40
  }
@@ -54,10 +55,10 @@ export function createChannelRoutes({
54
55
  }
55
56
 
56
57
  async function register(req, res) {
57
- if (refuseUnadmitted(res)) return;
58
+ if (await refuseUnadmitted(res)) return;
58
59
  if (state.drainCommitted) return sendDraining(res);
59
60
  const body = await readChannelBody(req);
60
- if (refuseUnadmitted(res)) return;
61
+ if (await refuseUnadmitted(res)) return;
61
62
  const clientToken = registry.registerClient({
62
63
  leadPid: body.leadPid,
63
64
  cwd: body.cwd,
@@ -96,11 +97,11 @@ export function createChannelRoutes({
96
97
  // channels client registry alive. The broker itself owns a parallel fair
97
98
  // scheduler and per-call cancellation.
98
99
  async function agentDispatch(req, res) {
99
- if (refuseUnadmitted(res)) return;
100
+ if (await refuseUnadmitted(res)) return;
100
101
  if (state.drainCommitted) return sendDraining(res);
101
102
  if (!agentBroker?.dispatch) return sendError(res, 'agent broker unavailable', 503);
102
103
  const body = await readChannelBody(req);
103
- if (refuseUnadmitted(res)) return;
104
+ if (await refuseUnadmitted(res)) return;
104
105
  const callId = String(body.callId || '').trim();
105
106
  if (!callId) return sendError(res, 'callId required', 400);
106
107
  res.on('close', () => {
@@ -130,10 +131,10 @@ export function createChannelRoutes({
130
131
  }
131
132
 
132
133
  async function call(req, res) {
133
- if (refuseUnadmitted(res)) return;
134
+ if (await refuseUnadmitted(res)) return;
134
135
  if (state.drainCommitted) return sendDraining(res);
135
136
  const body = await readChannelBody(req);
136
- if (refuseUnadmitted(res)) return;
137
+ if (await refuseUnadmitted(res)) return;
137
138
  const clientToken = body.token || null;
138
139
  const c = clientToken ? clients.get(clientToken) : null;
139
140
  if (!c) return sendError(res, 'unknown client token', 404);
@@ -12,22 +12,33 @@
12
12
  * this daemon's in-memory turn activity (every session runs inline here, so
13
13
  * it is the single source of truth; worker index files only declare work). */
14
14
  async function loadSessionStoreWithRuntimeActivity() {
15
- const [store, runtime, progress] = await Promise.all([
15
+ const [store, runtime, progress, tasks] = await Promise.all([
16
16
  import('../runtime/agent/orchestrator/session/store-summary-reader.mjs'),
17
17
  // The import-free leaf holding the runtime map: no session manager graph.
18
18
  import('../runtime/agent/orchestrator/session/manager/runtime-state.mjs'),
19
19
  import('../runtime/agent/orchestrator/session/agent-pool-progress.mjs'),
20
+ import('../runtime/shared/background-tasks.mjs'),
20
21
  ]);
21
22
  return {
22
23
  ...store,
23
24
  // Running rows also carry their turn's progress stamp and stage (the
24
- // quiet-time cue), read from the same runtime map.
25
- listStoredAgentWorkers: () =>
26
- progress.withTurnProgress(
27
- store.listStoredAgentWorkers({ runningSessionIds: runtime.inFlightSessionActivity().working }),
25
+ // quiet-time cue), read from the same runtime map; an idle row whose
26
+ // agent handed off background shell work that still runs reads waiting.
27
+ // The handed-off shell work of every row comes from one task scan. The
28
+ // store is read with awaited I/O (a listing runs on every activity
29
+ // change); the running set is taken once those reads are done.
30
+ listStoredAgentWorkers: async () => {
31
+ const rows = await store.listStoredAgentWorkersAsync({
32
+ runningSessionIds: () => runtime.inFlightSessionActivity().working,
33
+ });
34
+ const awaiting = tasks.activeBackgroundTaskSessionIds({ surface: 'shell', awaitedOnly: true });
35
+ return progress.withTurnProgress(
36
+ rows,
28
37
  (sessionId) => runtime.runtimeState.get(sessionId),
29
- runtime.isTurnInFlight
30
- ),
38
+ runtime.isTurnInFlight,
39
+ (sessionId) => awaiting.has(sessionId)
40
+ );
41
+ },
31
42
  };
32
43
  }
33
44
 
@@ -53,6 +64,8 @@ export function createDesktopRuntime({ getLocalSessionBridge }) {
53
64
  loadStatuslineSegments: () => import('../runtime/shared/statusline/statusline-segments.mjs'),
54
65
  loadConfig: () => import('../runtime/shared/config.mjs'),
55
66
  loadDocumentPreview: () => import('../runtime/office/pdf/document-preview.mjs'),
67
+ // The desktop service's git children start on this worker, off the loop.
68
+ loadProcessRunner: () => import('../runtime/shared/process-runner.mjs'),
56
69
  async executeCodeGraphTool(name, args, cwd) {
57
70
  const graph = await import('../runtime/agent/orchestrator/tools/code-graph/dispatch.mjs');
58
71
  return graph.executeCodeGraphTool(name, args, cwd);
@@ -15,6 +15,14 @@
15
15
  // being renewed, and every register/call asks `admit()` first. After an
16
16
  // event-loop stall nothing is served until a refresh under the claim lock
17
17
  // proves the record still names it — a contender's claim takes that lock.
18
+ // A lease that ran out DURING such a stall (live when the loop stopped) is
19
+ // the one exception to the fence: the holder never had a turn to refresh
20
+ // or to fence, and a contender's reclaim is decided under the claim lock
21
+ // against the record's heartbeat there (daemonOwnerIsStale re-checks it),
22
+ // so a refresh that finds the record still naming this pid proves no
23
+ // contender took it and none can on its earlier proof. Without that, a sole
24
+ // daemon whose loop the machine starved past the lease killed itself and
25
+ // every running turn with no other daemon to take over.
18
26
  // - Holder's record names `livenessPort` from its very first claim: a tiny
19
27
  // loopback listener started BEFORE the claim (startOwnerLivenessListener)
20
28
  // answers /health with the pid, through boot and for the process lifetime.
@@ -32,8 +40,9 @@
32
40
  // daemon.json publishes, answering with that pid, proves them alive.
33
41
  import http from 'node:http';
34
42
  import { readFileSync } from 'node:fs';
43
+ import { readFile } from 'node:fs/promises';
35
44
  import { isPidAlive } from '../runtime/shared/pid-liveness.mjs';
36
- import { readSingletonOwner, refreshSingletonOwner } from '../runtime/shared/singleton-owner.mjs';
45
+ import { readSingletonOwner, refreshSingletonOwnerAsync } from '../runtime/shared/singleton-owner.mjs';
37
46
 
38
47
  export const DAEMON_OWNER_STALE_MS = 60_000;
39
48
  export const OWNER_PROBE_TIMEOUT_MS = 3_000;
@@ -58,11 +67,23 @@ export function daemonOwnerTimings(env = process.env) {
58
67
  }
59
68
 
60
69
  /** daemon.json's pid and session endpoint, or null when missing/unreadable. */
70
+ function discoveryOf(parsed) {
71
+ const session = parsed?.endpoints?.session;
72
+ return { pid: Number(parsed?.pid) || 0, port: Number(session?.port) || 0, token: session?.token || '' };
73
+ }
74
+
61
75
  export function readDiscovery(discoveryPath) {
62
76
  try {
63
- const parsed = JSON.parse(readFileSync(discoveryPath, 'utf8'));
64
- const session = parsed?.endpoints?.session;
65
- return { pid: Number(parsed?.pid) || 0, port: Number(session?.port) || 0, token: session?.token || '' };
77
+ return discoveryOf(JSON.parse(readFileSync(discoveryPath, 'utf8')));
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ /** readDiscovery without blocking the event loop. */
84
+ export async function readDiscoveryAsync(discoveryPath) {
85
+ try {
86
+ return discoveryOf(JSON.parse(await readFile(discoveryPath, 'utf8')));
66
87
  } catch {
67
88
  return null;
68
89
  }
@@ -243,7 +264,10 @@ export function daemonOwnerIsStale(
243
264
  * `tickMs` it checks the lease, notices stalls, and every `heartbeatMs`
244
265
  * rewrites `owner` with a fresh wall-clock `heartbeatAt` under the claim lock
245
266
  * (recreating a deleted record), then calls `onBeat`.
246
- * - No successful refresh for `fenceMs`: fenced, never renewed (`onFenced`).
267
+ * - No successful refresh for `fenceMs`: fenced, never renewed (`onFenced`) —
268
+ * unless the lease ran out inside one stall (live when the loop stopped):
269
+ * then the refresh is retried every tick for `heartbeatMs` after the loop
270
+ * resumes, and fenced only if none succeeds by then.
247
271
  * - The record names another pid: `onSuperseded(record)`.
248
272
  * - A turn gap over `stallMs`: `admit()` refuses work until a refresh succeeds.
249
273
  * The gap is logged once, with `describeStall(fromMono, toMono)` appended.
@@ -258,7 +282,7 @@ export function createOwnerHeartbeat({
258
282
  stallMs,
259
283
  now = Date.now,
260
284
  monoNow = () => performance.now(),
261
- refresh = refreshSingletonOwner,
285
+ refresh = refreshSingletonOwnerAsync,
262
286
  log = () => {},
263
287
  describeStall = null,
264
288
  onBeat = () => {},
@@ -272,6 +296,13 @@ export function createOwnerHeartbeat({
272
296
  let lastBeatAt = leaseFrom;
273
297
  let lastTurnAt = leaseFrom;
274
298
  let needsVerify = false;
299
+ // Monotonic time the loop resumed from a stall that outlived the lease.
300
+ let stallResumedAt = null;
301
+ // The refresh in flight: a promise of its settled status, else null.
302
+ let pending = null;
303
+ // When the latest stall was noticed: only a refresh started after it can
304
+ // re-verify the record (an older one may have read it before the stall).
305
+ let stallNotedAt = Number.NEGATIVE_INFINITY;
275
306
  let timer = null;
276
307
 
277
308
  function halt(next) {
@@ -284,6 +315,12 @@ export function createOwnerHeartbeat({
284
315
  return t - leaseFrom >= fenceMs;
285
316
  }
286
317
 
318
+ // Expired and past re-verification: the refreshes failed while the loop
319
+ // ran, or no refresh landed within `heartbeatMs` of resuming from a stall.
320
+ function mustFence(t) {
321
+ return leaseExpired(t) && !(stallResumedAt !== null && t - stallResumedAt < heartbeatMs);
322
+ }
323
+
287
324
  function fence(t) {
288
325
  if (state !== 'active') return;
289
326
  halt('fenced');
@@ -298,6 +335,8 @@ export function createOwnerHeartbeat({
298
335
  lastTurnAt = t;
299
336
  if (t - from <= stallMs) return;
300
337
  needsVerify = true;
338
+ stallNotedAt = t;
339
+ if (state === 'active' && !leaseExpired(from) && leaseExpired(t)) stallResumedAt = t;
301
340
  let detail = '';
302
341
  try {
303
342
  detail = describeStall?.(from, t) || '';
@@ -310,13 +349,53 @@ export function createOwnerHeartbeat({
310
349
  );
311
350
  }
312
351
 
352
+ // A refresh's verdict, applied once it is known. Fenced, superseded or
353
+ // stopped while it ran: nothing is renewed. A renewal counts from the
354
+ // moment the refresh started (`t`), never from when it answered.
355
+ function settle(t, heartbeatAt, result) {
356
+ if (state !== 'active') return result.status;
357
+ if (result.status === 'refreshed') {
358
+ if (leaseExpired(t)) {
359
+ log(
360
+ `owner lease re-verified ${Math.round(t - leaseFrom)}ms after its last refresh (event-loop stall)` +
361
+ ' — the record still names this process; serving again'
362
+ );
363
+ }
364
+ leaseFrom = t;
365
+ lastRefreshAt = heartbeatAt;
366
+ if (t >= stallNotedAt) {
367
+ needsVerify = false;
368
+ stallResumedAt = null;
369
+ }
370
+ onBeat();
371
+ } else if (result.status === 'superseded') {
372
+ halt('superseded');
373
+ onSuperseded(result.owner);
374
+ } else if (result.status === 'busy') {
375
+ log('owner heartbeat skipped: claim lock busy');
376
+ }
377
+ return result.status;
378
+ }
379
+
380
+ function failedRefresh(error) {
381
+ log(`owner heartbeat failed: ${error?.message || error}`);
382
+ return { status: 'failed' };
383
+ }
384
+
385
+ // One refresh at a time. An asynchronous refresh (the default: its file
386
+ // work and lock retries never block the loop) answers 'pending' here and
387
+ // settles later; the lease and fence keep their monotonic deadlines while
388
+ // it runs — a refresh still unanswered at the fence does not hold it off.
313
389
  function beat() {
314
390
  if (state !== 'active') return null;
391
+ if (pending) return 'pending';
315
392
  const t = monoNow();
316
393
  lastBeatAt = t;
317
- // Checked before every renewal: an overdue beat after a stall must fence,
318
- // not extend a lease a contender may already have reclaimed.
319
- if (leaseExpired(t)) {
394
+ // Checked before every renewal: an overdue beat must fence, not extend a
395
+ // lease a contender may already have reclaimed — except right after a
396
+ // stall that outlived the lease, where the refresh itself (under the
397
+ // claim lock) answers whether one did.
398
+ if (mustFence(t)) {
320
399
  fence(t);
321
400
  return 'fenced';
322
401
  }
@@ -325,28 +404,26 @@ export function createOwnerHeartbeat({
325
404
  try {
326
405
  result = refresh(ownerPath, { ...record, heartbeatAt });
327
406
  } catch (error) {
328
- log(`owner heartbeat failed: ${error?.message || error}`);
329
- result = { status: 'failed' };
330
- }
331
- if (result.status === 'refreshed') {
332
- leaseFrom = t;
333
- lastRefreshAt = heartbeatAt;
334
- needsVerify = false;
335
- onBeat();
336
- } else if (result.status === 'superseded') {
337
- halt('superseded');
338
- onSuperseded(result.owner);
339
- } else if (result.status === 'busy') {
340
- log('owner heartbeat skipped: claim lock busy');
407
+ result = failedRefresh(error);
341
408
  }
342
- return result.status;
409
+ if (typeof result?.then !== 'function') return settle(t, heartbeatAt, result);
410
+ pending = result
411
+ .then(
412
+ (value) => value,
413
+ (error) => failedRefresh(error)
414
+ )
415
+ .then((value) => {
416
+ pending = null;
417
+ return settle(t, heartbeatAt, value);
418
+ });
419
+ return 'pending';
343
420
  }
344
421
 
345
422
  function tick() {
346
423
  if (state !== 'active') return;
347
424
  const t = monoNow();
348
425
  noteTurn(t);
349
- if (leaseExpired(t)) fence(t);
426
+ if (mustFence(t)) fence(t);
350
427
  else if (needsVerify ? t - lastBeatAt >= tickMs : t - lastBeatAt >= heartbeatMs) beat();
351
428
  }
352
429
 
@@ -363,7 +440,7 @@ export function createOwnerHeartbeat({
363
440
  // Shutting down: no ticks run to measure stalls; the lease still bounds it.
364
441
  if (state === 'stopped') return !leaseExpired(t);
365
442
  noteTurn(t);
366
- if (leaseExpired(t)) {
443
+ if (mustFence(t)) {
367
444
  fence(t);
368
445
  return false;
369
446
  }
@@ -372,8 +449,23 @@ export function createOwnerHeartbeat({
372
449
  if (needsVerify && t - lastBeatAt >= tickMs) beat();
373
450
  return state === 'active' && !needsVerify;
374
451
  },
452
+ /** admit(), but a request arriving while the lease is being re-verified
453
+ * waits for the verifying refresh instead of being refused outright: at
454
+ * most two refreshes (one started before the stall cannot verify). */
455
+ async admitAsync() {
456
+ for (let attempt = 0; attempt < 2; attempt += 1) {
457
+ if (this.admit()) return true;
458
+ if (!pending) return false;
459
+ await pending;
460
+ }
461
+ return this.admit();
462
+ },
375
463
  beat,
376
464
  tick,
465
+ /** Settles with the in-flight refresh's status (null when none runs). */
466
+ settled() {
467
+ return pending ?? Promise.resolve(null);
468
+ },
377
469
  /** Stop beating (shutdown). admit() keeps refusing once the lease runs out. */
378
470
  stop() {
379
471
  if (state === 'active') halt('stopped');
@@ -35,7 +35,8 @@ import path from 'node:path';
35
35
  import { rmSync } from 'node:fs';
36
36
  import { fileURLToPath } from 'node:url';
37
37
  import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
38
- import { writeJsonAtomicSync } from '../runtime/shared/atomic-file.mjs';
38
+ import { writeJsonAtomicAsync } from '../runtime/shared/atomic-file.mjs';
39
+ import { prewarmOwnerOnlyAclWin32 } from '../runtime/shared/file-permissions.mjs';
39
40
  import { ensurePrivateRuntimeRoot, resolveRuntimeRoot } from '../runtime/shared/runtime-root.mjs';
40
41
  import { ensureProcessListenerHeadroom } from '../runtime/shared/process-listener-headroom.mjs';
41
42
  import { listenerLeakWarningLine } from '../runtime/shared/listener-leak-warning.mjs';
@@ -46,6 +47,7 @@ import {
46
47
  daemonOwnerTimings,
47
48
  discoveryPid,
48
49
  proveDaemonOwnerRecord,
50
+ readDiscoveryAsync,
49
51
  startOwnerLivenessListener,
50
52
  } from './daemon-owner.mjs';
51
53
  import { remoteIntentPath } from '../runtime/shared/remote-intent.mjs';
@@ -607,11 +609,12 @@ function daemonStatusSnapshot() {
607
609
  }
608
610
 
609
611
  /** The file every attacher reads to find this daemon: wire identity plus both
610
- * loopback endpoints and their privileged tokens. */
612
+ * loopback endpoints and their privileged tokens. Written asynchronously
613
+ * (the secret file's owner-only ACL included), never on the event loop. */
611
614
  function publishDaemonDiscovery({ channel, session, modelProxy = null }) {
612
615
  // A re-publish keeps the first publication's startedAt.
613
616
  publishedEndpoints = { channel, session, modelProxy, startedAt: publishedEndpoints?.startedAt ?? Date.now() };
614
- writeJsonAtomicSync(
617
+ return writeJsonAtomicAsync(
615
618
  DAEMON_DISCOVERY_PATH,
616
619
  {
617
620
  protocol: SESSION_PROTOCOL,
@@ -658,6 +661,13 @@ function admitOwnerWork() {
658
661
  return ownerHeartbeat ? ownerHeartbeat.admit() : true;
659
662
  }
660
663
 
664
+ /** admitOwnerWork for the HTTP front doors: a request that arrives while the
665
+ * lease is being re-verified (the refresh is asynchronous) waits for that
666
+ * refresh instead of being refused. */
667
+ function admitOwnerWorkAsync() {
668
+ return ownerHeartbeat ? ownerHeartbeat.admitAsync() : true;
669
+ }
670
+
661
671
  /** This process no longer holds (or is about to lose) the owner lease: refuse
662
672
  * new work at once, unpublish its endpoint so launchers stop finding it, then
663
673
  * shut down within the shutdown budget — before a contender may reclaim. */
@@ -678,14 +688,31 @@ function fenceDaemon(reason) {
678
688
  /** Heartbeat follow-up while this process still owns the lock: a daemon.json
679
689
  * deleted (TEMP cleanup) or overwritten while it runs is written again, so
680
690
  * new clients keep finding the one daemon that serves this data dir. */
691
+ // Read and rewritten asynchronously, one check at a time: this runs after
692
+ // every heartbeat, and its synchronous read of daemon.json was measured
693
+ // blocking the loop for 5.9 s under disk contention.
694
+ let republishing = false;
681
695
  function republishMissingDiscovery() {
682
- if (!publishedEndpoints || shuttingDown || discoveryPid(DAEMON_DISCOVERY_PATH) === process.pid) return;
683
- try {
684
- publishDaemonDiscovery(publishedEndpoints);
685
- log('daemon.json was missing or named another pid — re-published');
686
- } catch (error) {
687
- log(`daemon.json re-publish failed: ${error?.message || error}`);
688
- }
696
+ if (!publishedEndpoints || shuttingDown || republishing) return;
697
+ republishing = true;
698
+ void (async () => {
699
+ try {
700
+ if ((await readDiscoveryAsync(DAEMON_DISCOVERY_PATH))?.pid === process.pid || shuttingDown) return;
701
+ await publishDaemonDiscovery(publishedEndpoints);
702
+ // Fenced while it was written: take it back down (fenceDaemon already ran).
703
+ if (shuttingDown) {
704
+ if ((await readDiscoveryAsync(DAEMON_DISCOVERY_PATH))?.pid === process.pid) {
705
+ rmSync(DAEMON_DISCOVERY_PATH, { force: true });
706
+ }
707
+ return;
708
+ }
709
+ log('daemon.json was missing or named another pid — re-published');
710
+ } catch (error) {
711
+ log(`daemon.json re-publish failed: ${error?.message || error}`);
712
+ } finally {
713
+ republishing = false;
714
+ }
715
+ })();
689
716
  }
690
717
 
691
718
  /** Ready handshake for the spawner, plus the log-sink handoff pinned to the
@@ -757,7 +784,7 @@ async function startChannelFrontDoor({ channelsRuntime, bootPhases }) {
757
784
  transport = createChannelTransport({
758
785
  handleCall: channelsRuntime.handleCall,
759
786
  agentBroker: agentDispatchBroker,
760
- admitWork: admitOwnerWork,
787
+ admitWork: admitOwnerWorkAsync,
761
788
  log,
762
789
  // The durable channel link lives in this file. Without it a restart loses
763
790
  // the pinned session (nothing to restore) and every catalog reports Remote
@@ -974,7 +1001,7 @@ async function main() {
974
1001
  } catch {}
975
1002
  },
976
1003
  onUpgradeRequested: requestDaemonReplacement,
977
- admitWork: admitOwnerWork,
1004
+ admitWork: admitOwnerWorkAsync,
978
1005
  });
979
1006
  const sessionEndpoint = await bootPhases.measure('session-transport-start', () => sessionTransport.start());
980
1007
  const modelProxyEndpoint = await startModelProxyBridge(bootPhases);
@@ -988,7 +1015,7 @@ async function main() {
988
1015
  exitRecorder.setReason(`owner claim superseded by pid=${ownerPid} before publish`, 0);
989
1016
  process.exit(0);
990
1017
  }
991
- publishDaemonDiscovery({ channel: { port, token }, session: sessionEndpoint, modelProxy: modelProxyEndpoint });
1018
+ await publishDaemonDiscovery({ channel: { port, token }, session: sessionEndpoint, modelProxy: modelProxyEndpoint });
992
1019
  bootPhases.mark('discovery-published');
993
1020
  log(`session front door on 127.0.0.1:${sessionEndpoint.port}`);
994
1021
 
@@ -1000,6 +1027,9 @@ async function main() {
1000
1027
  void sessionRuntimeHost?.warmLocalRuntime?.();
1001
1028
  });
1002
1029
  startPostgresInBackground();
1030
+ // Secret writes (token stores, config) find the ACL helpers already running
1031
+ // instead of starting one under their file lock.
1032
+ void prewarmOwnerOnlyAclWin32();
1003
1033
 
1004
1034
  // Automation may spawn the shared daemon; keep schedules/webhooks live.
1005
1035
  // A TUI-spawned daemon keeps the historical eager start (its channels client