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
@@ -25,7 +25,14 @@ import {
25
25
  releaseWriteCommit as _releaseWriteCommit,
26
26
  } from './write-guards.mjs';
27
27
  import { _sessionForDisk } from './serialize.mjs';
28
- import { _commitSessionWrite, _discardSaveTmp, _trackSaveTmp, _untrackSaveTmp } from './save-fault.mjs';
28
+ import {
29
+ _commitSessionWrite,
30
+ _commitSessionWriteAttempt,
31
+ _discardSaveTmp,
32
+ _trackSaveTmp,
33
+ _untrackSaveTmp,
34
+ } from './save-fault.mjs';
35
+ import { sleep } from '../../../../shared/sleep.mjs';
29
36
  import {
30
37
  _droppedSaveIds,
31
38
  _clearLiveSession,
@@ -100,15 +107,25 @@ function _readLifecycleAuthority(id, reason, action, lifecycleOnly = false) {
100
107
  return authority;
101
108
  }
102
109
 
103
- /** Commit a barrier record, bypassing the save queue + guard. False when it did not land. */
104
- function _writeLifecycleRecord(id, record, existing, reason) {
110
+ /** Commit a barrier record, bypassing the save queue + guard. False when it did not land.
111
+ * `attempt` (a number) commits with ONE rename of the retry schedule: a
112
+ * retryable refusal with attempts left discards the scratch file, records
113
+ * nothing and returns a retry ({ retryInMs }) instead of sleeping. */
114
+ function _writeLifecycleRecord(id, record, existing, reason, attempt = null) {
105
115
  const target = sessionPath(id);
106
116
  const tmp = _trackSaveTmp(`${target}.${randomBytes(6).toString('hex')}.tmp`);
107
117
  try {
108
118
  const disk = _sessionForDisk(record);
109
119
  writeFileSync(tmp, JSON.stringify(disk), 'utf-8');
110
120
  const scratch = _stampSessionScratch(tmp);
111
- _commitSessionWrite(tmp, target, id);
121
+ if (attempt === null) _commitSessionWrite(tmp, target, id);
122
+ else {
123
+ const retryInMs = _commitSessionWriteAttempt(tmp, target, id, attempt);
124
+ if (retryInMs !== null) {
125
+ _discardSaveTmp(tmp);
126
+ return { retryInMs };
127
+ }
128
+ }
112
129
  // Read-only lifecycle checks (pending-message gates right after a close/
113
130
  // detach) reuse what this barrier wrote; write authority still re-reads.
114
131
  _readCanonicalRecord.rememberOwnBarrier(target, scratch, _lifecycleOfSessionDocument(disk));
@@ -179,6 +196,10 @@ function _recordAbsent(options) {
179
196
  * saveSession() calls.
180
197
  */
181
198
  export function markSessionClosed(id, reason = 'manual', options = {}) {
199
+ return _markSessionClosed(id, reason, options, null);
200
+ }
201
+
202
+ function _markSessionClosed(id, reason, options, attempt) {
182
203
  // Only a fresh failure from THIS attempt may be observed by closeSession's
183
204
  // durable-barrier check — a veto must not surface a stale error.
184
205
  clearSessionLifecycleCommitError(id);
@@ -232,7 +253,8 @@ export function markSessionClosed(id, reason = 'manual', options = {}) {
232
253
  updatedAt: closeTime,
233
254
  ..._cancelStatusFields(base, reason, options.cancelStatus, closeTime),
234
255
  };
235
- if (!_writeLifecycleRecord(id, tombstone, existing, reason)) return null;
256
+ const wrote = _writeLifecycleRecord(id, tombstone, existing, reason, attempt);
257
+ if (wrote !== true) return wrote || null;
236
258
  _savePending.delete(id);
237
259
  clearSessionSaveError(id);
238
260
  _clearLiveSession(id);
@@ -262,6 +284,10 @@ export function markSessionClosed(id, reason = 'manual', options = {}) {
262
284
  * Returns the new generation, or null if the session file doesn't exist.
263
285
  */
264
286
  export function bumpSessionGeneration(id, reason = 'detach', options = {}) {
287
+ return _bumpSessionGeneration(id, reason, options, null);
288
+ }
289
+
290
+ function _bumpSessionGeneration(id, reason, options, attempt) {
265
291
  clearSessionLifecycleCommitError(id);
266
292
  // The detach barrier is a canonical write and MUST take the same commit
267
293
  // lock as markSessionClosed. Cancellation alone is not a barrier: a writer
@@ -289,7 +315,8 @@ export function bumpSessionGeneration(id, reason = 'detach', options = {}) {
289
315
  if (!existing) return _recordAbsent(options);
290
316
  const newGen = (typeof existing.generation === 'number' ? existing.generation : 0) + 1;
291
317
  const detached = { ...existing, generation: newGen, updatedAt: Date.now(), detachedReason: reason };
292
- if (!_writeLifecycleRecord(id, detached, existing, reason)) return null;
318
+ const wrote = _writeLifecycleRecord(id, detached, existing, reason, attempt);
319
+ if (wrote !== true) return wrote || null;
293
320
  _savePending.delete(id);
294
321
  clearSessionSaveError(id);
295
322
  _clearLiveSession(id);
@@ -300,3 +327,43 @@ export function bumpSessionGeneration(id, reason = 'detach', options = {}) {
300
327
  _releaseWriteCommit(commitControl);
301
328
  }
302
329
  }
330
+
331
+ // ── Non-blocking barriers ────────────────────────────────────────────────────
332
+ // The synchronous barriers above wait out a refused rename (Windows EPERM/
333
+ // EBUSY while a scanner holds the file) with sleepSync INSIDE the commit lock,
334
+ // freezing the whole daemon for up to the full backoff schedule (4.6 s seen).
335
+ // An attempt instead runs the complete barrier — authority read, cancellation,
336
+ // record built from the current copy, ONE rename — under the commit lock and,
337
+ // when the rename is refused with attempts left, returns a retry
338
+ // ({ retryInMs }) having recorded nothing. The lock is released between
339
+ // attempts; the next attempt re-reads everything, so a save that landed in
340
+ // between is folded into the barrier instead of overwriting it. Same backoff
341
+ // schedule and the same final error as the synchronous barrier.
342
+
343
+ /** Whether a barrier attempt asked to be retried after `retryInMs`. */
344
+ export function isLifecycleRetry(result) {
345
+ return typeof result?.retryInMs === 'number';
346
+ }
347
+
348
+ /** Attempt `attempt` of markSessionClosed (see isLifecycleRetry). */
349
+ export function markSessionClosedAttempt(id, reason = 'manual', options = {}, attempt = 0) {
350
+ return _markSessionClosed(id, reason, options, attempt);
351
+ }
352
+
353
+ /** Attempt `attempt` of bumpSessionGeneration (see isLifecycleRetry). */
354
+ export function bumpSessionGenerationAttempt(id, reason = 'detach', options = {}, attempt = 0) {
355
+ return _bumpSessionGeneration(id, reason, options, attempt);
356
+ }
357
+
358
+ async function _runBarrierAttempts(run) {
359
+ for (let attempt = 0; ; attempt++) {
360
+ const result = run(attempt);
361
+ if (!isLifecycleRetry(result)) return result;
362
+ await sleep(result.retryInMs);
363
+ }
364
+ }
365
+
366
+ /** markSessionClosed without ever blocking the event loop on a rename retry. */
367
+ export function markSessionClosedAsync(id, reason = 'manual', options = {}) {
368
+ return _runBarrierAttempts((attempt) => _markSessionClosed(id, reason, options, attempt));
369
+ }
@@ -9,6 +9,7 @@ import { _savePending } from './pending-saves.mjs';
9
9
  import { _liveSessions, _droppedSaveIds, getFailedSaveSnapshot } from './live-state.mjs';
10
10
  import { _readStoredSessionCached } from './load-cache.mjs';
11
11
  import { _ensureLifecycleFields } from './serialize.mjs';
12
+ import { isSettledSessionStamp, sameSessionStamp, statSessionStamp } from './canonical-reader.mjs';
12
13
 
13
14
  export function loadSession(id) {
14
15
  const path = sessionPath(id);
@@ -58,3 +59,42 @@ export function loadSession(id) {
58
59
  if (live?.id === id) _liveSessions.delete(id);
59
60
  return disk.session ? _ensureLifecycleFields(disk.session) : null;
60
61
  }
62
+
63
+ // `status` of loadSession(id), for the agent-status pulse: it reads every
64
+ // worker's status every two seconds, and an idle worker's whole transcript
65
+ // (released from the load cache's small document budget) was re-read and
66
+ // strictly re-parsed on every beat. The answer loadSession gave is reused only
67
+ // while the file keeps the exact settled full stamp it was read under and no
68
+ // in-memory copy (pending save, live snapshot, failed-save evidence) can speak
69
+ // for the id instead; anything else asks loadSession again.
70
+ const STATUS_CACHE_LIMIT = 4096;
71
+ const _statusByPath = new Map(); // path -> { stamp, status }
72
+
73
+ function _observeStamp(path) {
74
+ try {
75
+ return statSessionStamp(path);
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ export function loadSessionStatus(id) {
82
+ const path = sessionPath(id);
83
+ const inMemory = _savePending.has(id) || _liveSessions.has(id) || getFailedSaveSnapshot(id);
84
+ const before = inMemory ? null : _observeStamp(path);
85
+ const cached = before ? _statusByPath.get(path) : null;
86
+ if (cached && sameSessionStamp(cached.stamp, before)) return cached.status;
87
+ _statusByPath.delete(path);
88
+ const status = loadSession(id)?.status ?? null;
89
+ if (
90
+ before &&
91
+ isSettledSessionStamp(before) &&
92
+ !_savePending.has(id) &&
93
+ !_liveSessions.has(id) &&
94
+ sameSessionStamp(before, _observeStamp(path))
95
+ ) {
96
+ if (_statusByPath.size >= STATUS_CACHE_LIMIT) _statusByPath.delete(_statusByPath.keys().next().value);
97
+ _statusByPath.set(path, { stamp: before, status });
98
+ }
99
+ return status;
100
+ }
@@ -21,6 +21,7 @@
21
21
  * another process's ACTIVE commit, which no registry here can see.
22
22
  */
23
23
  import { unlinkSync } from 'node:fs';
24
+ import { renameAttempt } from '../../../../shared/atomic-file.mjs';
24
25
  import { basename } from 'node:path';
25
26
  import { _renameWithRetrySync } from './serialize.mjs';
26
27
 
@@ -318,3 +319,17 @@ export function _commitSessionWrite(tmp, target, id) {
318
319
  if (injected) throw injected;
319
320
  return _renameWithRetrySync(tmp, target);
320
321
  }
322
+
323
+ /**
324
+ * One attempt of _commitSessionWrite's rename schedule (renameAttempt): null
325
+ * when the scratch file landed, else the delay before attempt + 1; throws the
326
+ * final error. The injected fault is taken once per commit, on attempt 0,
327
+ * exactly as _commitSessionWrite takes it.
328
+ */
329
+ export function _commitSessionWriteAttempt(tmp, target, id, attempt) {
330
+ if (attempt === 0) {
331
+ const injected = _takeInjectedCommitFault(id);
332
+ if (injected) throw injected;
333
+ }
334
+ return renameAttempt(tmp, target, attempt);
335
+ }
@@ -34,9 +34,21 @@ function _memoFilePath(dir, id, extension) {
34
34
  }
35
35
  return path;
36
36
  }
37
+ // Every session path lookup ensured the store dir with a synchronous
38
+ // existsSync: the agent-status pulse resolves hundreds of paths per beat, and
39
+ // those probes alone held the daemon loop for seconds per minute. The check is
40
+ // re-run at most once per interval; a dir removed in between is recreated by
41
+ // the next check.
42
+ const STORE_DIR_RECHECK_MS = 1000;
43
+ let _ensuredStoreDir = null;
44
+ let _ensuredStoreDirAt = 0;
37
45
  export function getStoreDir() {
38
46
  const dir = _storeDirPath();
47
+ const now = Date.now();
48
+ if (dir === _ensuredStoreDir && now - _ensuredStoreDirAt < STORE_DIR_RECHECK_MS) return dir;
39
49
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
50
+ _ensuredStoreDir = dir;
51
+ _ensuredStoreDirAt = now;
40
52
  return dir;
41
53
  }
42
54
  function _sessionFileName(id, extension) {
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Top-level field read of a serialized session record without materializing
3
+ * its transcript.
4
+ *
5
+ * The agent pool needs a handful of top-level fields per worker session, yet
6
+ * `JSON.parse` of the whole record built every message object — megabytes per
7
+ * worker — on each rewrite the worker persisted. The record is instead proven
8
+ * valid by the strict grammar walk of store-record-strict-scan.mjs, which
9
+ * decodes only the requested members; a record it cannot prove (malformed,
10
+ * duplicate top-level keys, pretty-printed, non-object root) is read by the
11
+ * full parse itself.
12
+ *
13
+ * Contract: the result equals picking `fields` (in `fields` order, own
14
+ * properties only) from `JSON.parse(bytes.toString('utf8'))` when that is an
15
+ * object or an array (an array root picks none), and is `null` otherwise —
16
+ * a document the full parse rejects included.
17
+ */
18
+ import { provenTopLevelObject } from './store-record-strict-scan.mjs';
19
+
20
+ export function readTopLevelFields(bytes, fields) {
21
+ const wanted = new Set(fields);
22
+ let doc = provenTopLevelObject(bytes, (key) => wanted.has(key));
23
+ if (!doc) {
24
+ try {
25
+ doc = JSON.parse(bytes.toString('utf8'));
26
+ } catch {
27
+ return null;
28
+ }
29
+ if (!doc || typeof doc !== 'object') return null;
30
+ }
31
+ const picked = {};
32
+ for (const field of fields) {
33
+ if (Object.hasOwn(doc, field)) picked[field] = doc[field];
34
+ }
35
+ return picked;
36
+ }
@@ -16,10 +16,17 @@
16
16
  * field name and row key is a consumed contract and is used verbatim.
17
17
  */
18
18
  import { readFileSync } from 'node:fs';
19
+ import { readFile, stat } from 'node:fs/promises';
19
20
  import { join } from 'node:path';
20
- import { probePath, PROBE_PRESENT } from './store/fs-probe.mjs';
21
+ import {
22
+ isSettledSessionStamp,
23
+ sameSessionStamp,
24
+ sessionStampOf,
25
+ statSessionStamp,
26
+ } from './store/canonical-reader.mjs';
21
27
  import { readJsonProjection } from '../../../shared/pending-json-projection.mjs';
22
28
  import { listStoredAgentWorkers as assembleStoredAgentWorkers } from './store-agent-worker-pool.mjs';
29
+ import { readTopLevelFields } from './store-agent-worker-header.mjs';
23
30
  import {
24
31
  TAG_TOMBSTONE_TTL_MS,
25
32
  agentPaneVisible,
@@ -166,16 +173,83 @@ function cancelStage(cancel, declaredStage) {
166
173
  return /^cancel/i.test(declaredStage) || CANCELLED_AGENT_STATUS.test(declaredStage) ? declaredStage : cancel.status;
167
174
  }
168
175
 
169
- function storedAgentWorkerIndex() {
170
- const path = storedAgentWorkerIndexPath();
176
+ // The worker indexes were re-parsed in full on every pool listing and ancestry
177
+ // lookup, changed or not. Each keeps its last parse beside the bytes it came
178
+ // from: the file is still read every time (a few hundred KB), but an
179
+ // unchanged one — byte for byte, so no timestamp collision can serve a stale
180
+ // document — reuses that parse.
181
+ // Before the read, the full file stamp is checked under the same canonical
182
+ // policy the session headers use: a file that still has the exact settled
183
+ // { dev, ino, size, mtimeNs, ctimeNs } its bytes were proven to come from is
184
+ // not read again (each pool listing re-read the whole index synchronously).
185
+ const workerIndexCache = new Map(); // path -> { bytes, doc, stamp|null }
186
+ // The index documents an asynchronous listing read before its (synchronous)
187
+ // assembly; live only during that assembly.
188
+ let passIndexes = null; // path -> doc | null
189
+
190
+ function readWorkerIndexFile(path) {
191
+ if (passIndexes?.has(path)) return passIndexes.get(path);
171
192
  // This process's own queued (not yet persisted) writes win over the file.
172
193
  const projected = readJsonProjection(path);
173
194
  if (projected) return projected;
195
+ const before = observeStamp(path);
196
+ const cached = workerIndexCache.get(path);
197
+ if (before && cached?.stamp && sameSessionStamp(cached.stamp, before)) return cached.doc;
198
+ let bytes;
174
199
  try {
175
- return JSON.parse(readFileSync(path, 'utf8'));
200
+ bytes = readFileSync(path);
176
201
  } catch {
202
+ workerIndexCache.delete(path);
177
203
  return null;
178
204
  }
205
+ const stamp = before && isSettledSessionStamp(before) && sameSessionStamp(before, observeStamp(path)) ? before : null;
206
+ if (cached?.bytes.equals(bytes)) {
207
+ cached.stamp = stamp;
208
+ return cached.doc;
209
+ }
210
+ let doc = null;
211
+ try {
212
+ doc = JSON.parse(bytes.toString('utf8'));
213
+ } catch {
214
+ /* malformed: no rows */
215
+ }
216
+ workerIndexCache.set(path, { bytes, doc, stamp });
217
+ return doc;
218
+ }
219
+
220
+ /** readWorkerIndexFile with every stat and read awaited, under the same
221
+ * stamp and byte-equality cache. */
222
+ async function readWorkerIndexFileAsync(path) {
223
+ const projected = readJsonProjection(path);
224
+ if (projected) return projected;
225
+ const before = await observeStampAsync(path);
226
+ const cached = workerIndexCache.get(path);
227
+ if (before && cached?.stamp && sameSessionStamp(cached.stamp, before)) return cached.doc;
228
+ let bytes;
229
+ try {
230
+ bytes = await readFile(path);
231
+ } catch {
232
+ workerIndexCache.delete(path);
233
+ return null;
234
+ }
235
+ const settled = before && isSettledSessionStamp(before);
236
+ const stamp = settled && sameSessionStamp(before, await observeStampAsync(path)) ? before : null;
237
+ if (cached?.bytes.equals(bytes)) {
238
+ cached.stamp = stamp;
239
+ return cached.doc;
240
+ }
241
+ let doc = null;
242
+ try {
243
+ doc = JSON.parse(bytes.toString('utf8'));
244
+ } catch {
245
+ /* malformed: no rows */
246
+ }
247
+ workerIndexCache.set(path, { bytes, doc, stamp });
248
+ return doc;
249
+ }
250
+
251
+ function storedAgentWorkerIndex() {
252
+ return readWorkerIndexFile(storedAgentWorkerIndexPath());
179
253
  }
180
254
 
181
255
  function storedAgentWorkerIndexRows() {
@@ -211,7 +285,8 @@ export function listStoredAgentWorkerLinks() {
211
285
 
212
286
  // The agent pool only reads top-level session fields, yet each refresh used to
213
287
  // JSON.parse every active worker's whole transcript (megabytes per worker,
214
- // every catalog publish). Cache the header per file fingerprint instead.
288
+ // every catalog publish). Cache the header per full file stamp instead (see
289
+ // probeWorkerSessionHeader).
215
290
  // The cache holds exactly the headers the latest pool assembly read: a fixed
216
291
  // LRU cap below the index size (263 headers against 256 slots) evicted every
217
292
  // entry just before its reuse, so each refresh re-parsed every worker
@@ -250,30 +325,120 @@ const WORKER_HEADER_FIELDS = [
250
325
  ...SESSION_CANCEL_FIELDS,
251
326
  ];
252
327
 
328
+ // One assembly reads a session's header up to three times (reap check, row
329
+ // projection, orphan prune); each read used to stat the file again. Within an
330
+ // assembly the first answer stands, absent or unreadable included.
331
+ let passHeaders = null; // path -> header | null, live only during an assembly
332
+
333
+ function workerSessionHeaderPath(sessionId) {
334
+ return join(dataDir(), 'sessions', `${sessionId}.json`);
335
+ }
336
+
253
337
  function readWorkerSessionHeader(sessionId) {
254
- const path = join(dataDir(), 'sessions', `${sessionId}.json`);
255
- const probe = probePath(path);
256
- if (probe.state !== PROBE_PRESENT) return null;
338
+ const path = workerSessionHeaderPath(sessionId);
339
+ if (passHeaders?.has(path)) return passHeaders.get(path);
340
+ const header = probeWorkerSessionHeader(path);
341
+ passHeaders?.set(path, header);
342
+ return header;
343
+ }
344
+
345
+ let workerHeaderReads = 0;
346
+
347
+ function observeStamp(path) {
348
+ try {
349
+ return statSessionStamp(path);
350
+ } catch {
351
+ return null;
352
+ }
353
+ }
354
+
355
+ // A header is reused only under the canonical full-stamp policy: the file
356
+ // still has the exact { dev, ino, size, mtimeNs, ctimeNs } its bytes were
357
+ // proven to come from (stat → read → stat), and that stamp was past the racy
358
+ // window when read. Size + mtime alone let an equal-length rewrite that kept
359
+ // its mtime serve stale ownership, status and cancel fields.
360
+ function probeWorkerSessionHeader(path) {
361
+ // Absent and unreadable alike: no header.
362
+ const before = observeStamp(path);
363
+ if (!before) return null;
257
364
  const cached = workerSessionHeaderCache.get(path);
258
- if (cached && cached.mtimeMs === probe.mtimeMs && cached.size === probe.size) {
365
+ if (cached && sameSessionStamp(cached.stamp, before)) {
259
366
  cached.pass = headerPass;
260
367
  return cached.header;
261
368
  }
262
- let session = null;
369
+ workerSessionHeaderCache.delete(path);
370
+ // A rewrite re-reads only this record, and only its top-level fields are
371
+ // decoded: the transcript bytes are validated, never materialized.
372
+ let header = null;
373
+ try {
374
+ header = readTopLevelFields(readFileSync(path), WORKER_HEADER_FIELDS);
375
+ } catch {
376
+ return null;
377
+ }
378
+ if (!header) return null;
379
+ workerHeaderReads += 1;
380
+ if (isSettledSessionStamp(before) && sameSessionStamp(before, observeStamp(path))) {
381
+ workerSessionHeaderCache.set(path, { stamp: before, header, pass: headerPass });
382
+ }
383
+ return header;
384
+ }
385
+
386
+ async function observeStampAsync(path) {
387
+ try {
388
+ return sessionStampOf(await stat(path, { bigint: true }));
389
+ } catch {
390
+ return null;
391
+ }
392
+ }
393
+
394
+ /** probeWorkerSessionHeader with every stat and read awaited, under the
395
+ * same full-stamp cache policy. */
396
+ async function probeWorkerSessionHeaderAsync(path) {
397
+ const before = await observeStampAsync(path);
398
+ if (!before) return null;
399
+ const cached = workerSessionHeaderCache.get(path);
400
+ if (cached && sameSessionStamp(cached.stamp, before)) return cached.header;
401
+ workerSessionHeaderCache.delete(path);
402
+ let header = null;
263
403
  try {
264
- session = JSON.parse(readFileSync(path, 'utf8'));
404
+ header = readTopLevelFields(await readFile(path), WORKER_HEADER_FIELDS);
265
405
  } catch {
266
406
  return null;
267
407
  }
268
- if (!session || typeof session !== 'object') return null;
269
- const header = {};
270
- for (const field of WORKER_HEADER_FIELDS) {
271
- if (Object.hasOwn(session, field)) header[field] = session[field];
408
+ if (!header) return null;
409
+ workerHeaderReads += 1;
410
+ if (isSettledSessionStamp(before) && sameSessionStamp(before, await observeStampAsync(path))) {
411
+ workerSessionHeaderCache.set(path, { stamp: before, header, pass: headerPass });
272
412
  }
273
- workerSessionHeaderCache.set(path, { mtimeMs: probe.mtimeMs, size: probe.size, header, pass: headerPass });
274
413
  return header;
275
414
  }
276
415
 
416
+ // Header reads in flight at once: each is a stat (and a read only when the
417
+ // file changed) on the shared libuv pool, which other file I/O also needs.
418
+ const HEADER_READ_CONCURRENCY = 4;
419
+
420
+ async function prefetchWorkerHeaders(sessionIds, headers) {
421
+ const paths = [...new Set([...sessionIds].map(workerSessionHeaderPath))].filter((path) => !headers.has(path));
422
+ let next = 0;
423
+ const drain = async () => {
424
+ while (next < paths.length) {
425
+ const path = paths[next++];
426
+ headers.set(path, await probeWorkerSessionHeaderAsync(path));
427
+ }
428
+ };
429
+ await Promise.all(Array.from({ length: Math.min(HEADER_READ_CONCURRENCY, paths.length) }, drain));
430
+ }
431
+
432
+ /** Test/diagnostic seam: cached headers and the records this process read. */
433
+ export function storedAgentWorkerHeaderStats() {
434
+ return {
435
+ entries: workerSessionHeaderCache.size,
436
+ reads: workerHeaderReads,
437
+ stickyIdle: stickyIdleSince.size,
438
+ indexes: workerIndexCache.size,
439
+ };
440
+ }
441
+
277
442
  /** Owner record of a child row that is a Lead CONVERSATION. Only such an owner
278
443
  * lets a missing pool row mean "the Lead's lease expired"; an Agent-owned or
279
444
  * unreadable owner keeps its child rows visible exactly as before. */
@@ -377,16 +542,8 @@ function projectChildWorkerRow(row, context) {
377
542
  }
378
543
 
379
544
  function storedLeadWorkerRows() {
380
- const path = storedLeadWorkerIndexPath();
381
- let parsed = readJsonProjection(path);
382
- if (!parsed) {
383
- try {
384
- parsed = JSON.parse(readFileSync(path, 'utf8'));
385
- } catch {
386
- /* no resident Lead pool */
387
- }
388
- }
389
- return workerRows(parsed);
545
+ // Absent or unreadable: no resident Lead pool.
546
+ return workerRows(readWorkerIndexFile(storedLeadWorkerIndexPath()));
390
547
  }
391
548
 
392
549
  /** A Lead-index row whose durable session turned out to be an agent's.
@@ -464,6 +621,72 @@ function pruneOrphanChildRows(bySessionId, { liveLeadSessionIds }) {
464
621
  * never projected into either pool. `runningSessionIds` is the daemon's set of
465
622
  * sessions whose turn is in flight; omitted, no row is shown as running. */
466
623
  export function listStoredAgentWorkers({ runningSessionIds = null } = {}) {
624
+ headerPass += 1;
625
+ passHeaders = new Map();
626
+ try {
627
+ return assembleListedWorkers(runningSessionIds);
628
+ } finally {
629
+ passHeaders = null;
630
+ forgetUnreadHeaders();
631
+ }
632
+ }
633
+
634
+ // Keep what the latest assembly read, whatever the index size; forget the rest.
635
+ function forgetUnreadHeaders() {
636
+ for (const [path, entry] of workerSessionHeaderCache) {
637
+ if (entry.pass !== headerPass) workerSessionHeaderCache.delete(path);
638
+ }
639
+ }
640
+
641
+ /** listStoredAgentWorkers without blocking the event loop on the store: both
642
+ * indexes and every header the assembly can consult (each row's session,
643
+ * each Lead row's, and each child's owner) are read first with awaited
644
+ * stats and reads; the assembly then runs over what was read, with no file
645
+ * access left in it (any header it still asks for falls back to a read).
646
+ * `runningSessionIds` may be a function, asked once the reads are done. */
647
+ export async function listStoredAgentWorkersAsync({ runningSessionIds = null } = {}) {
648
+ const agentIndexPath = storedAgentWorkerIndexPath();
649
+ const leadIndexPath = storedLeadWorkerIndexPath();
650
+ const [agentIndex, leadIndex] = await Promise.all([
651
+ readWorkerIndexFileAsync(agentIndexPath),
652
+ readWorkerIndexFileAsync(leadIndexPath),
653
+ ]);
654
+ const headers = new Map();
655
+ const childRows = workerRows(agentIndex);
656
+ const sessionIds = new Set(childRows.map((row) => cleanValue(row?.sessionId)));
657
+ for (const row of workerRows(leadIndex)) {
658
+ const sessionId = cleanValue(row?.sessionId);
659
+ if (sessionId && isStoredSessionId(sessionId)) sessionIds.add(sessionId);
660
+ }
661
+ await prefetchWorkerHeaders(sessionIds, headers);
662
+ const owners = new Set();
663
+ for (const row of childRows) {
664
+ const header = headers.get(workerSessionHeaderPath(cleanValue(row?.sessionId)));
665
+ const owner = row && typeof row === 'object' ? workerOwnerSessionId(row, header) : null;
666
+ if (owner) owners.add(owner);
667
+ }
668
+ await prefetchWorkerHeaders(owners, headers);
669
+ const running = typeof runningSessionIds === 'function' ? runningSessionIds() : runningSessionIds;
670
+ headerPass += 1;
671
+ for (const path of headers.keys()) {
672
+ const entry = workerSessionHeaderCache.get(path);
673
+ if (entry) entry.pass = headerPass;
674
+ }
675
+ passHeaders = headers;
676
+ passIndexes = new Map([
677
+ [agentIndexPath, agentIndex],
678
+ [leadIndexPath, leadIndex],
679
+ ]);
680
+ try {
681
+ return assembleListedWorkers(running);
682
+ } finally {
683
+ passHeaders = null;
684
+ passIndexes = null;
685
+ forgetUnreadHeaders();
686
+ }
687
+ }
688
+
689
+ function assembleListedWorkers(runningSessionIds) {
467
690
  const now = Date.now();
468
691
  const index = storedAgentWorkerIndex();
469
692
  const rows = workerRows(index);
@@ -487,21 +710,13 @@ export function listStoredAgentWorkers({ runningSessionIds = null } = {}) {
487
710
  };
488
711
  return tombstoneBlocksWork(work, findTagTombstone(work, tombstones));
489
712
  };
490
- headerPass += 1;
491
- try {
492
- return assembleStoredAgentWorkers({
493
- now,
494
- runningSessionIds,
495
- storedAgentWorkerIndexRows: () => rows.filter((row) => !isReaped(cleanValue(row.sessionId))),
496
- projectChildWorkerRow,
497
- storedLeadWorkerRows,
498
- projectLeadWorkerRow,
499
- pruneOrphanChildRows,
500
- });
501
- } finally {
502
- // Keep what this assembly read, whatever the index size; forget the rest.
503
- for (const [path, entry] of workerSessionHeaderCache) {
504
- if (entry.pass !== headerPass) workerSessionHeaderCache.delete(path);
505
- }
506
- }
713
+ return assembleStoredAgentWorkers({
714
+ now,
715
+ runningSessionIds,
716
+ storedAgentWorkerIndexRows: () => rows.filter((row) => !isReaped(cleanValue(row.sessionId))),
717
+ projectChildWorkerRow,
718
+ storedLeadWorkerRows,
719
+ projectLeadWorkerRow,
720
+ pruneOrphanChildRows,
721
+ });
507
722
  }