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
@@ -0,0 +1,306 @@
1
+ /**
2
+ * Strict session-record read that decodes only what its caller needs.
3
+ *
4
+ * `readTopLevelLifecycleRecord` (lifecycle-scan.mjs) is the authority: a full
5
+ * `JSON.parse`, then a top-level duplicate-key walk. On a transcript that
6
+ * materializes every message object — about 2.5 ms per MB, most of it for
7
+ * tool output nobody reads. This reader reaches the SAME verdict and the same
8
+ * values while decoding only:
9
+ * - top-level values whose key is not in `skip`;
10
+ * - of `messages` (when it is an array): each element whose `role` is in
11
+ * `fullMessageRoles` in full; every other object element as `{ role }`
12
+ * (or `{}` without one); non-object elements in full.
13
+ * Everything else is validated, never decoded.
14
+ *
15
+ * The fast path accepts a document only when it has PROVEN it valid:
16
+ * - no raw byte below 0x20 anywhere (one native regex test; JSON forbids
17
+ * them inside strings, so a document with whitespace other than spaces,
18
+ * i.e. a pretty-printed one, simply takes the authoritative path);
19
+ * - no invalid escape anywhere (one native regex test; a backslash outside
20
+ * a string fails the grammar walk below);
21
+ * - the complete grammar, walked over every byte outside strings: values,
22
+ * separators, exact number and literal spellings, nesting, and nothing
23
+ * but whitespace after the root object;
24
+ * - a root object without any repeated top-level key.
25
+ * Anything it cannot prove — malformed, ambiguous, or merely unusual — is
26
+ * handed to `readTopLevelLifecycleRecord` itself, so every rejection is the
27
+ * authority's own. Decoded values are `JSON.parse` of their exact UTF-8
28
+ * slices, which equal the full parse's sub-values (slices start and end at
29
+ * ASCII delimiters, so UTF-8 decoding agrees too).
30
+ */
31
+ import { lifecycleRecordOf, readTopLevelLifecycleRecord } from './lifecycle-scan.mjs';
32
+
33
+ const QUOTE = 0x22;
34
+ const BACKSLASH = 0x5c;
35
+ const SPACE = 0x20;
36
+ const COMMA = 0x2c;
37
+ const COLON = 0x3a;
38
+ const OPEN_OBJECT = 0x7b;
39
+ const CLOSE_OBJECT = 0x7d;
40
+ const OPEN_ARRAY = 0x5b;
41
+ const CLOSE_ARRAY = 0x5d;
42
+ const MINUS = 0x2d;
43
+ const PLUS = 0x2b;
44
+ const DOT = 0x2e;
45
+ const ZERO = 0x30;
46
+ const NINE = 0x39;
47
+ const EMPTY = new Set();
48
+
49
+ // String-content faults, one native pass each (an alternation of the two is
50
+ // slower than both). Any raw control byte: inside a string JSON.parse rejects
51
+ // it; outside one only \t \n \r are whitespace, and those documents take the
52
+ // full path.
53
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: control bytes are exactly what this detects.
54
+ const CONTROL_BYTE = /[\x00-\x1f]/;
55
+ // An escape that is not one of \" \\ \/ \b \f \n \r \t \uXXXX. The run before
56
+ // it starts after a non-backslash, so escapes pair from the run's start.
57
+ const INVALID_ESCAPE = /(?<!\\)(?:\\\\)*\\(?:[^"\\/bfnrtu]|u(?![0-9a-fA-F]{4}))/;
58
+
59
+ const isDigit = (code) => code >= ZERO && code <= NINE;
60
+
61
+ // Only spaces can remain: CONTROL_BYTE already sent every \t \n \r away.
62
+ function skipSpaces(text, at) {
63
+ let i = at;
64
+ while (text.charCodeAt(i) === SPACE) i++;
65
+ return i;
66
+ }
67
+
68
+ /** Index after the string opening at `at`, or -1. */
69
+ function stringEnd(text, at) {
70
+ let quote = at;
71
+ for (;;) {
72
+ quote = text.indexOf('"', quote + 1);
73
+ if (quote === -1) return -1;
74
+ let run = 0;
75
+ for (let k = quote - 1; text.charCodeAt(k) === BACKSLASH; k--) run++;
76
+ if ((run & 1) === 0) return quote + 1;
77
+ }
78
+ }
79
+
80
+ /** Index after the JSON number at `at`, or -1. */
81
+ function numberEnd(text, at) {
82
+ let i = at;
83
+ if (text.charCodeAt(i) === MINUS) i++;
84
+ if (text.charCodeAt(i) === ZERO) i++;
85
+ else if (isDigit(text.charCodeAt(i))) {
86
+ while (isDigit(text.charCodeAt(i))) i++;
87
+ } else return -1;
88
+ if (text.charCodeAt(i) === DOT) {
89
+ i++;
90
+ if (!isDigit(text.charCodeAt(i))) return -1;
91
+ while (isDigit(text.charCodeAt(i))) i++;
92
+ }
93
+ const exponent = text.charCodeAt(i);
94
+ if (exponent === 0x65 || exponent === 0x45) {
95
+ i++;
96
+ if (text.charCodeAt(i) === PLUS || text.charCodeAt(i) === MINUS) i++;
97
+ if (!isDigit(text.charCodeAt(i))) return -1;
98
+ while (isDigit(text.charCodeAt(i))) i++;
99
+ }
100
+ return i;
101
+ }
102
+
103
+ /** Index after `"key" :` starting at `at`, or -1. */
104
+ function memberValueStart(text, at) {
105
+ if (text.charCodeAt(at) !== QUOTE) return -1;
106
+ const end = stringEnd(text, at);
107
+ if (end === -1) return -1;
108
+ const colon = skipSpaces(text, end);
109
+ return text.charCodeAt(colon) === COLON ? colon + 1 : -1;
110
+ }
111
+
112
+ /** Index after one valid JSON value starting at or after `at`, or -1.
113
+ * Iterative, so nesting depth costs no stack. */
114
+ function valueEnd(text, at) {
115
+ const closers = [];
116
+ let i = at;
117
+ for (;;) {
118
+ i = skipSpaces(text, i);
119
+ const code = text.charCodeAt(i);
120
+ if (code === QUOTE) i = stringEnd(text, i);
121
+ else if (code === OPEN_OBJECT || code === OPEN_ARRAY) {
122
+ const closer = code === OPEN_OBJECT ? CLOSE_OBJECT : CLOSE_ARRAY;
123
+ i = skipSpaces(text, i + 1);
124
+ if (text.charCodeAt(i) === closer) i++;
125
+ else {
126
+ closers.push(closer);
127
+ if (closer === CLOSE_OBJECT) {
128
+ i = memberValueStart(text, i);
129
+ if (i === -1) return -1;
130
+ }
131
+ continue;
132
+ }
133
+ } else if (code === 0x74) i = text.startsWith('true', i) ? i + 4 : -1;
134
+ else if (code === 0x66) i = text.startsWith('false', i) ? i + 5 : -1;
135
+ else if (code === 0x6e) i = text.startsWith('null', i) ? i + 4 : -1;
136
+ else i = numberEnd(text, i);
137
+ if (i === -1) return -1;
138
+ // After a value: close containers or move to the next member/element.
139
+ for (;;) {
140
+ if (closers.length === 0) return i;
141
+ i = skipSpaces(text, i);
142
+ const next = text.charCodeAt(i);
143
+ const closer = closers[closers.length - 1];
144
+ if (next === closer) {
145
+ closers.pop();
146
+ i++;
147
+ continue;
148
+ }
149
+ if (next !== COMMA) return -1;
150
+ i = skipSpaces(text, i + 1);
151
+ if (closer === CLOSE_OBJECT) {
152
+ i = memberValueStart(text, i);
153
+ if (i === -1) return -1;
154
+ }
155
+ break;
156
+ }
157
+ }
158
+ }
159
+
160
+ const decodeSlice = (bytes, start, end) => JSON.parse(bytes.toString('utf8', start, end));
161
+
162
+ /** The key of the string token [start, end): its own bytes unless escaped. */
163
+ function decodeKey(bytes, text, start, end) {
164
+ for (let k = start + 1; k < end - 1; k++) {
165
+ if (text.charCodeAt(k) === BACKSLASH) return decodeSlice(bytes, start, end);
166
+ }
167
+ return bytes.toString('utf8', start + 1, end - 1);
168
+ }
169
+
170
+ function defineMember(target, key, value) {
171
+ // JSON.parse creates own data properties, `__proto__` included.
172
+ Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
173
+ }
174
+
175
+ // "role" spelled with escapes is at most 6 escapes of 6 chars plus quotes.
176
+ const MAX_ESCAPED_ROLE_KEY = 26;
177
+
178
+ /** One `messages` element [start, ...): `{ end, item }`, or null. */
179
+ function messageElement(bytes, text, start, fullRoles) {
180
+ if (text.charCodeAt(start) !== OPEN_OBJECT) {
181
+ const end = valueEnd(text, start);
182
+ return end === -1 ? null : { end, item: decodeSlice(bytes, start, end) };
183
+ }
184
+ let i = skipSpaces(text, start + 1);
185
+ let role;
186
+ let hasRole = false;
187
+ if (text.charCodeAt(i) === CLOSE_OBJECT) return { end: i + 1, item: {} };
188
+ for (;;) {
189
+ if (text.charCodeAt(i) !== QUOTE) return null;
190
+ const keyEnd = stringEnd(text, i);
191
+ if (keyEnd === -1) return null;
192
+ const isRole =
193
+ (keyEnd - i === 6 && text.startsWith('"role"', i)) ||
194
+ (keyEnd - i <= MAX_ESCAPED_ROLE_KEY && keyEnd - i > 6 && decodeKey(bytes, text, i, keyEnd) === 'role');
195
+ const colon = skipSpaces(text, keyEnd);
196
+ if (text.charCodeAt(colon) !== COLON) return null;
197
+ const valueStart = skipSpaces(text, colon + 1);
198
+ const end = valueEnd(text, valueStart);
199
+ if (end === -1) return null;
200
+ if (isRole) {
201
+ // Duplicate members resolve last-wins, as in the full parse.
202
+ role = decodeSlice(bytes, valueStart, end);
203
+ hasRole = true;
204
+ }
205
+ i = skipSpaces(text, end);
206
+ if (text.charCodeAt(i) === COMMA) {
207
+ i = skipSpaces(text, i + 1);
208
+ continue;
209
+ }
210
+ if (text.charCodeAt(i) !== CLOSE_OBJECT) return null;
211
+ const elementEnd = i + 1;
212
+ let item = {};
213
+ if (hasRole && fullRoles.has(role)) item = decodeSlice(bytes, start, elementEnd);
214
+ else if (hasRole) defineMember(item, 'role', role);
215
+ return { end: elementEnd, item };
216
+ }
217
+ }
218
+
219
+ /** The `messages` array [start, ...): `{ end, messages }`, or null. */
220
+ function messagesValue(bytes, text, start, fullRoles) {
221
+ const messages = [];
222
+ let i = skipSpaces(text, start + 1);
223
+ if (text.charCodeAt(i) === CLOSE_ARRAY) return { end: i + 1, messages };
224
+ for (;;) {
225
+ const element = messageElement(bytes, text, i, fullRoles);
226
+ if (!element) return null;
227
+ messages.push(element.item);
228
+ i = skipSpaces(text, element.end);
229
+ if (text.charCodeAt(i) === COMMA) {
230
+ i = skipSpaces(text, i + 1);
231
+ continue;
232
+ }
233
+ return text.charCodeAt(i) === CLOSE_ARRAY ? { end: i + 1, messages } : null;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * The proven-valid, duplicate-free root object of `bytes`, or null when
239
+ * validity could not be proven (or the document is not such an object). Of
240
+ * it only the members `decode(key)` accepts are decoded — `messages`, when an
241
+ * array, as described in the module comment with `fullMessageRoles`; every
242
+ * other member is validated and left out.
243
+ */
244
+ export function provenTopLevelObject(bytes, decode, fullMessageRoles = EMPTY) {
245
+ try {
246
+ return provenDocument(bytes, decode, fullMessageRoles);
247
+ } catch {
248
+ return null;
249
+ }
250
+ }
251
+
252
+ function provenDocument(bytes, decode, fullRoles) {
253
+ const text = bytes.toString('latin1');
254
+ if (CONTROL_BYTE.test(text) || INVALID_ESCAPE.test(text)) return null;
255
+ let i = skipSpaces(text, 0);
256
+ if (text.charCodeAt(i) !== OPEN_OBJECT) return null;
257
+ const doc = {};
258
+ const seen = new Set();
259
+ i = skipSpaces(text, i + 1);
260
+ if (text.charCodeAt(i) === CLOSE_OBJECT) i++;
261
+ else {
262
+ for (;;) {
263
+ if (text.charCodeAt(i) !== QUOTE) return null;
264
+ const keyEnd = stringEnd(text, i);
265
+ if (keyEnd === -1) return null;
266
+ const key = decodeKey(bytes, text, i, keyEnd);
267
+ if (seen.has(key)) return null;
268
+ seen.add(key);
269
+ const colon = skipSpaces(text, keyEnd);
270
+ if (text.charCodeAt(colon) !== COLON) return null;
271
+ const start = skipSpaces(text, colon + 1);
272
+ const wanted = decode(key);
273
+ let end;
274
+ if (wanted && key === 'messages' && text.charCodeAt(start) === OPEN_ARRAY) {
275
+ const value = messagesValue(bytes, text, start, fullRoles);
276
+ if (!value) return null;
277
+ end = value.end;
278
+ defineMember(doc, key, value.messages);
279
+ } else {
280
+ end = valueEnd(text, start);
281
+ if (end === -1) return null;
282
+ if (wanted) defineMember(doc, key, decodeSlice(bytes, start, end));
283
+ }
284
+ i = skipSpaces(text, end);
285
+ if (text.charCodeAt(i) === COMMA) {
286
+ i = skipSpaces(text, i + 1);
287
+ continue;
288
+ }
289
+ if (text.charCodeAt(i) !== CLOSE_OBJECT) return null;
290
+ i++;
291
+ break;
292
+ }
293
+ }
294
+ return skipSpaces(text, i) === text.length ? doc : null;
295
+ }
296
+
297
+ /**
298
+ * `readTopLevelLifecycleRecord` of the UTF-8 document in `bytes`, with `doc`
299
+ * decoded as described above — only for a proven document; a record from the
300
+ * authoritative fallback carries the complete document. Consumers must read
301
+ * nothing but non-skipped fields and, of non-full messages, `role`.
302
+ */
303
+ export function readStrictRecordFields(bytes, { skip = EMPTY, fullMessageRoles = EMPTY } = {}) {
304
+ const doc = provenTopLevelObject(bytes, (key) => !skip.has(key), fullMessageRoles);
305
+ return doc ? lifecycleRecordOf(doc) : readTopLevelLifecycleRecord(bytes.toString('utf8'));
306
+ }
@@ -15,8 +15,9 @@ import { join } from 'node:path';
15
15
  // Leaf helpers only: the three-way present/absent/unreadable classification
16
16
  // and the strict record parser the authoritative store uses, so the catalog
17
17
  // cannot disagree with it.
18
- import { probePath, readTextFile, PROBE_PRESENT, PROBE_ABSENT } from './store/fs-probe.mjs';
19
- import { readTopLevelLifecycleRecord, isLifecycleUnreadable } from './lifecycle-scan.mjs';
18
+ import { probePath, readBytesFile, readTextFile, PROBE_PRESENT, PROBE_ABSENT } from './store/fs-probe.mjs';
19
+ import { isLifecycleUnreadable } from './lifecycle-scan.mjs';
20
+ import { readStrictRecordFields } from './store-record-strict-scan.mjs';
20
21
  import { isAgentOnlySession, isRootLeadSession, sessionVisibility } from './store-summary-visibility.mjs';
21
22
  import { applySummaryLogText, summaryLogPath } from './store-summary-log.mjs';
22
23
  import { dataDir } from './store-summary-locations.mjs';
@@ -43,7 +44,7 @@ export const STAMP_ABSENT_CODES = new Set(['ENOENT', 'ENOTDIR']);
43
44
  /** The one catalog order of every listing path (JSON and DB): newest
44
45
  * conversation activity first (see normalizedRow's lastUsedAt note), ties by
45
46
  * id, so a fallback and the DB never disagree on equal activity times. */
46
- export const byCatalogOrder = byActivityThenId;
47
+ const byCatalogOrder = byActivityThenId;
47
48
 
48
49
  // ── Stamp-keyed strict derivations ──────────────────────────────────────────
49
50
  // Session metadata and catalog rows are small values derived from a strict
@@ -52,8 +53,20 @@ export const byCatalogOrder = byActivityThenId;
52
53
  // proven to come from (stat → read → stat) and that stamp is past the racy
53
54
  // window. Any other file — changed, replaced by another writer, unreadable —
54
55
  // is read and strictly parsed again.
56
+ // Bounded by count and by serialized size: nothing tells this map a session
57
+ // file went away, so a churning host fills it with dead paths until LRU
58
+ // eviction (`delete` + `set` on a hit keeps Map order least recent first).
55
59
  const STAMPED_DERIVATION_LIMIT = 2048;
56
- const stampedDerivations = new Map(); // `${kind}|${path}` → { stamp, value }
60
+ const STAMPED_DERIVATION_MAX_BYTES = 8 * 1024 * 1024;
61
+ const stampedDerivations = new Map(); // `${kind}|${path}` → { stamp, value, bytes }
62
+ let stampedDerivationBytes = 0;
63
+
64
+ function dropStampedDerivation(key) {
65
+ const entry = stampedDerivations.get(key);
66
+ if (!entry) return;
67
+ stampedDerivations.delete(key);
68
+ stampedDerivationBytes -= entry.bytes;
69
+ }
57
70
 
58
71
  export function observeStamp(path) {
59
72
  try {
@@ -64,24 +77,35 @@ export function observeStamp(path) {
64
77
  }
65
78
 
66
79
  /** `{ state, value }`: `derive(text)` for the record at `path`, or its cached
67
- * result while the file is provably unchanged. */
68
- export function stampedDerivation(kind, path, derive) {
80
+ * result while the file is provably unchanged. `bytes`: `derive` gets the
81
+ * raw Buffer instead of text. `stamp`: the caller's own stat of `path`,
82
+ * taken just before, stands for the pre-read stat (one stat fewer). */
83
+ export function stampedDerivation(kind, path, derive, { bytes = false, stamp } = {}) {
69
84
  const key = `${kind}|${path}`;
70
- const before = observeStamp(path);
85
+ const before = stamp === undefined ? observeStamp(path) : stamp;
71
86
  const cached = stampedDerivations.get(key);
72
87
  if (before && cached && sameSessionStamp(cached.stamp, before)) {
73
88
  stampedDerivations.delete(key);
74
89
  stampedDerivations.set(key, cached);
75
90
  return { state: PROBE_PRESENT, value: cached.value };
76
91
  }
77
- stampedDerivations.delete(key);
78
- const read = readTextFile(path);
92
+ dropStampedDerivation(key);
93
+ const read = bytes ? readBytesFile(path) : readTextFile(path);
79
94
  if (read.state !== PROBE_PRESENT) return { state: read.state, value: null };
80
- const value = derive(read.text);
95
+ const value = derive(bytes ? read.bytes : read.text);
81
96
  if (isSettledSessionStamp(before) && sameSessionStamp(before, observeStamp(path))) {
82
- stampedDerivations.set(key, { stamp: before, value });
83
- if (stampedDerivations.size > STAMPED_DERIVATION_LIMIT) {
84
- stampedDerivations.delete(stampedDerivations.keys().next().value);
97
+ // UTF-16 code units of the key and the serialized value approximate the
98
+ // retained bytes; the strict parse just above dwarfs this stringify.
99
+ const size = 2 * (key.length + (JSON.stringify(value)?.length ?? 0));
100
+ if (size <= STAMPED_DERIVATION_MAX_BYTES) {
101
+ stampedDerivations.set(key, { stamp: before, value, bytes: size });
102
+ stampedDerivationBytes += size;
103
+ while (
104
+ stampedDerivations.size > STAMPED_DERIVATION_LIMIT ||
105
+ stampedDerivationBytes > STAMPED_DERIVATION_MAX_BYTES
106
+ ) {
107
+ dropStampedDerivation(stampedDerivations.keys().next().value);
108
+ }
85
109
  }
86
110
  }
87
111
  return { state: PROBE_PRESENT, value };
@@ -199,6 +223,24 @@ function leadRows(rows) {
199
223
  return rows.filter((row) => row && isLeadVisibleRow(row));
200
224
  }
201
225
 
226
+ // What a row derivation decodes of a record (store-record-strict-scan.mjs):
227
+ // rowFromSession reads its user messages whole and only the role of the rest,
228
+ // and never these top-level fields — together ~10% of a record, mostly tool
229
+ // catalogs. Every other top-level field is decoded, so a field a row starts
230
+ // to read is there without touching this list.
231
+ const CATALOG_RECORD_READ = {
232
+ skip: new Set([
233
+ 'tools',
234
+ 'deferredToolCatalog',
235
+ 'deferredLateToolCatalog',
236
+ 'bp3CoreContext',
237
+ 'bp3EnvironmentContext',
238
+ 'compaction',
239
+ 'pendingDeferredToolDelta',
240
+ ]),
241
+ fullMessageRoles: new Set(['user']),
242
+ };
243
+
202
244
  function rowFromSession(session) {
203
245
  const messages = Array.isArray(session?.messages) ? session.messages : [];
204
246
  const preview =
@@ -259,14 +301,21 @@ function reconcileSessionFile(directory, storageId, retained, forceRead) {
259
301
  return { kind: 'indexed', ...fingerprint };
260
302
  }
261
303
  // A file the index does not describe (never saved by the store, or
262
- // saved after the index was written) is parsed once per full stamp.
263
- const read = stampedDerivation('row', path, (text) => {
264
- // Same strict authority as the store: a duplicate/ambiguous top-level
265
- // record or a foreign identity is not this file's session.
266
- const record = readTopLevelLifecycleRecord(text);
267
- if (isLifecycleUnreadable(record) || record.id !== storageId) return null;
268
- return rowFromSession(record.doc);
269
- });
304
+ // saved after the index was written) is read once per full stamp; the
305
+ // stat above is the read's pre-read stat.
306
+ const read = stampedDerivation(
307
+ 'row',
308
+ path,
309
+ (bytes) => {
310
+ // Same strict authority as the store: a duplicate/ambiguous top-level
311
+ // record or a foreign identity is not this file's session. Only what a
312
+ // row is made of is decoded (see CATALOG_RECORD_READ).
313
+ const record = readStrictRecordFields(bytes, CATALOG_RECORD_READ);
314
+ if (isLifecycleUnreadable(record) || record.id !== storageId) return null;
315
+ return rowFromSession(record.doc);
316
+ },
317
+ { bytes: true, stamp }
318
+ );
270
319
  if (read.state === PROBE_ABSENT) return null;
271
320
  // Exists and could not be read (EACCES/EIO): retain the last known row
272
321
  // rather than let a transient IO error delete it from view.
@@ -24,6 +24,7 @@ import {
24
24
  } from './store-summary-catalog.mjs';
25
25
  import { createStoredTranscriptCache, nextProjectionStamp } from './store-transcript-cache.mjs';
26
26
  import {
27
+ forgetStoredTranscriptOffThread,
27
28
  OFFLOAD_MIN_CHARS,
28
29
  projectStoredTranscriptOffThread,
29
30
  storedTranscriptWorkerRunning,
@@ -35,6 +36,7 @@ import { readArchivedAgentResult } from './store-summary-archived-agent.mjs';
35
36
  import { compactionArchivePath, readCompactionArchivePage } from './compaction-archive.mjs';
36
37
  import { currentSessionCatalogDb, onSessionCatalogDbChange, sessionCatalogDb } from './catalog-db/index.mjs';
37
38
  import { decodeCatalogCursor, encodeCatalogCursor, isAfterCursor } from './catalog-db/schema.mjs';
39
+ import { onSessionActivityChange } from '../../../shared/session-activity-signal.mjs';
38
40
  import {
39
41
  readCanonicalSessionRecord as readCanonicalLifecycle,
40
42
  offerStrictSessionRecord,
@@ -45,7 +47,11 @@ import {
45
47
  // Worker-pool row projection (child + Lead indexes, live-work evidence) lives
46
48
  // in store-agent-worker-rows.mjs; re-exported so prior importers of this
47
49
  // module stay unchanged.
48
- export { listStoredAgentWorkerLinks, listStoredAgentWorkers } from './store-agent-worker-rows.mjs';
50
+ export {
51
+ listStoredAgentWorkerLinks,
52
+ listStoredAgentWorkers,
53
+ listStoredAgentWorkersAsync,
54
+ } from './store-agent-worker-rows.mjs';
49
55
  export { storedAgentWorkerIndexPath } from './store-summary-locations.mjs';
50
56
 
51
57
  // One cache per process: the daemon serves every cold pane read from it, and
@@ -59,7 +65,9 @@ export function clearStoredTranscriptCache() {
59
65
 
60
66
  /** Drop one session's cold projections: it went live, or its last view left. */
61
67
  export function forgetStoredSessionTranscript(sessionId) {
62
- storedTranscriptCache.forget(`${String(sessionId || '').trim()}|`);
68
+ const id = String(sessionId || '').trim();
69
+ storedTranscriptCache.forget(`${id}|`);
70
+ if (id) forgetStoredTranscriptOffThread(id);
63
71
  }
64
72
 
65
73
  /** Its last view left: keep the cold projections for a revisit, within the
@@ -82,18 +90,22 @@ export function storedTranscriptCacheStats() {
82
90
  const ordinaryCatalog = createOrdinaryCatalog();
83
91
  const catalogListeners = new Set();
84
92
 
85
- function notifyCatalogListeners() {
93
+ function notifyCatalogListeners(kind = 'catalog') {
86
94
  for (const listener of catalogListeners) {
87
95
  try {
88
- listener();
96
+ listener(kind);
89
97
  } catch {
90
98
  /* a listener's failure is its own */
91
99
  }
92
100
  }
93
101
  }
94
102
 
95
- ordinaryCatalog.subscribe(notifyCatalogListeners);
96
- onSessionCatalogDbChange(notifyCatalogListeners);
103
+ ordinaryCatalog.subscribe(() => notifyCatalogListeners());
104
+ onSessionCatalogDbChange(() => notifyCatalogListeners());
105
+ // Runtime activity transitions (turn start/settle, task wait, background
106
+ // hand-off/finish) change only the Agents pool projection: `agent-activity`
107
+ // lets the reader refresh just that, at once.
108
+ onSessionActivityChange(() => notifyCatalogListeners('agent-activity'));
97
109
 
98
110
  /** `listener()` runs when a background pass finds a session file that changed
99
111
  * without a directory or index change, i.e. when an ordinary listing would
@@ -292,29 +304,32 @@ export function storedSessionTranscriptStamp(id) {
292
304
  return archive ? `${record}|${checkpoint}|${archive}` : null;
293
305
  }
294
306
 
307
+ /** A large record with no checkpoint sidecar is read, digested, parsed and
308
+ * projected on a worker thread: the same pipeline, off the daemon event
309
+ * loop. It hands no parse to the load cache (a later resume reads the file
310
+ * itself). The answer is the cache's `project` contract; a worker failure
311
+ * rejects and the cache runs the in-process pipeline instead. */
312
+ async function projectStoredTranscriptElsewhere(known, context) {
313
+ const { sessionId, recordPath, itemLimit, timing } = context;
314
+ timing.worker = storedTranscriptWorkerRunning(sessionId) ? 'warm' : 'cold';
315
+ const workerStartedAt = performance.now();
316
+ const answer = await projectStoredTranscriptOffThread({ sessionId, recordPath, itemLimit, known });
317
+ timing.workerMs = performance.now() - workerStartedAt;
318
+ Object.assign(timing, answer.timing);
319
+ context.readState = answer.state;
320
+ if (answer.state !== PROBE_PRESENT) return null;
321
+ if (answer.same) return answer;
322
+ if (answer.unreadable) return { ...answer, value: null };
323
+ if (answer.stamp) readCanonicalLifecycle.rememberStrictVerdict(recordPath, answer.stamp, answer.lifecycle);
324
+ // Stamps are process-unique per module realm; re-issue in this one.
325
+ answer.value.projectionStamp = nextProjectionStamp();
326
+ return answer;
327
+ }
328
+
295
329
  /** Projects the strictly parsed record `text` (read at full stamp `stamp`, or
296
330
  * null when not provably stable) into the cached transcript value. */
297
331
  async function produceStoredTranscript(text, stamp, context) {
298
- const { sessionId, recordPath, itemLimit, includeMessages, mode, offloadable, checkpointAbsent, timing } = context;
299
- // A large record with no checkpoint sidecar parses and projects on a
300
- // worker thread: the same pipeline, off the daemon event loop. It hands
301
- // no parse to the load cache (a later resume reads the file itself).
302
- if (offloadable && text.length >= OFFLOAD_MIN_CHARS) {
303
- try {
304
- timing.worker = storedTranscriptWorkerRunning() ? 'warm' : 'cold';
305
- const workerStartedAt = performance.now();
306
- const answer = await projectStoredTranscriptOffThread({ sessionId, text, itemLimit });
307
- timing.workerMs = performance.now() - workerStartedAt;
308
- Object.assign(timing, answer.timing);
309
- if (answer.unreadable) return null;
310
- if (stamp) readCanonicalLifecycle.rememberStrictVerdict(recordPath, stamp, answer.lifecycle);
311
- // Stamps are process-unique per module realm; re-issue in this one.
312
- answer.value.projectionStamp = nextProjectionStamp();
313
- return answer.value;
314
- } catch {
315
- // Worker unavailable: fall through to the in-process pipeline.
316
- }
317
- }
332
+ const { sessionId, recordPath, itemLimit, includeMessages, mode, checkpointAbsent, timing } = context;
318
333
  const parseStartedAt = performance.now();
319
334
  const record = readTopLevelLifecycleRecord(text);
320
335
  timing.parseMs = performance.now() - parseStartedAt;
@@ -388,7 +403,6 @@ export async function readStoredSessionTranscript(id, options = {}) {
388
403
  const archive = probePath(archivePath);
389
404
  const requestedLimit = Number(options.transcriptItemLimit);
390
405
  const itemLimit = Number.isFinite(requestedLimit) && requestedLimit > 0 ? requestedLimit : Number.POSITIVE_INFINITY;
391
- let readState = PROBE_PRESENT;
392
406
  // Full stamp the loaded text provably came from (unchanged around the
393
407
  // read), or null. Only such a parse may serve other readers.
394
408
  let readStamp = null;
@@ -405,9 +419,10 @@ export async function readStoredSessionTranscript(id, options = {}) {
405
419
  itemLimit,
406
420
  includeMessages,
407
421
  mode,
408
- offloadable,
409
422
  checkpointAbsent,
410
423
  timing,
424
+ // How the record read went (the worker's read or loadText below).
425
+ readState: PROBE_PRESENT,
411
426
  };
412
427
  const { value, hit, read } = await storedTranscriptCache.read({
413
428
  key: `${sessionId}|${itemLimit}|${mode}`,
@@ -425,6 +440,7 @@ export async function readStoredSessionTranscript(id, options = {}) {
425
440
  archive.size,
426
441
  ].join(':'),
427
442
  fileStat: recordStat,
443
+ project: offloadable ? (known) => projectStoredTranscriptElsewhere(known, produceContext) : null,
428
444
  loadText: async () => {
429
445
  const readStartedAt = performance.now();
430
446
  const before = observeStamp(recordPath);
@@ -440,7 +456,7 @@ export async function readStoredSessionTranscript(id, options = {}) {
440
456
  }
441
457
  body ||= readTextFile(recordPath);
442
458
  timing.readMs = performance.now() - readStartedAt;
443
- readState = body.state;
459
+ produceContext.readState = body.state;
444
460
  if (body.state !== PROBE_PRESENT) return null;
445
461
  readStamp = before && sameSessionStamp(before, observeStamp(recordPath)) ? before : null;
446
462
  return body.text;
@@ -448,7 +464,7 @@ export async function readStoredSessionTranscript(id, options = {}) {
448
464
  produce: (text) => produceStoredTranscript(text, readStamp, produceContext),
449
465
  });
450
466
  // The record vanished between stat and read: same answer as an absent probe.
451
- if (readState === PROBE_ABSENT) return readVanishedSessionTranscript(sessionId);
467
+ if (produceContext.readState === PROBE_ABSENT) return readVanishedSessionTranscript(sessionId);
452
468
  if (typeof options.trace === 'function') {
453
469
  options.trace({
454
470
  sessionId,