mixdog 1.2.0 → 1.2.2

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 (386) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/scripts/gate-local.mjs +19 -3
  4. package/scripts/model-catalog-audit.mjs +1 -15
  5. package/src/agents/reviewer/AGENT.md +3 -3
  6. package/src/agents/reviewer/agent.json +1 -1
  7. package/src/defaults/shipped-history.json +5 -0
  8. package/src/defaults/skills/pptx/SKILL.md +6 -6
  9. package/src/defaults/skills/pptx/references/review.md +6 -4
  10. package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
  11. package/src/defaults/skills/setup/references/actions.md +13 -3
  12. package/src/defaults/skills/visualize/SKILL.md +12 -9
  13. package/src/repl/slash-commands.mjs +16 -1
  14. package/src/rules/shared/60-verification.md +3 -1
  15. package/src/rules/shared/70-delivery.md +4 -2
  16. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -4
  17. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +10 -0
  18. package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
  19. package/src/runtime/agent/orchestrator/config.mjs +10 -4
  20. package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
  21. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
  22. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
  23. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +122 -15
  24. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +54 -11
  25. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +104 -9
  26. package/src/runtime/agent/orchestrator/mcp/client.mjs +358 -140
  27. package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
  28. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +103 -0
  29. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +150 -0
  30. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +111 -0
  31. package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +87 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +95 -0
  33. package/src/runtime/agent/orchestrator/mcp/oauth-request.mjs +122 -0
  34. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +9 -0
  35. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +229 -0
  36. package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
  37. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +294 -44
  38. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +2 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +94 -31
  40. package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +96 -0
  41. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +39 -18
  42. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +55 -0
  43. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +25 -21
  44. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +3 -1
  45. package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +18 -4
  46. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +13 -1
  47. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-midstream.mjs +10 -1
  49. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-recovery.mjs +23 -9
  50. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +46 -20
  51. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +14 -9
  52. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +73 -35
  53. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +69 -0
  54. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +4 -0
  55. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -1
  56. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +53 -18
  57. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +30 -9
  58. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  59. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +30 -14
  60. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +2 -1
  61. package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +93 -0
  62. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +82 -9
  63. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
  64. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
  65. package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
  66. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +447 -0
  67. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +38 -5
  68. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  69. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +97 -2
  70. package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +102 -23
  71. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +103 -33
  72. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +29 -6
  73. package/src/runtime/agent/orchestrator/providers/cursor.mjs +18 -1
  74. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
  75. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +86 -19
  76. package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +1 -1
  77. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +29 -8
  78. package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +191 -0
  79. package/src/runtime/agent/orchestrator/providers/gemini-family.mjs +9 -0
  80. package/src/runtime/agent/orchestrator/providers/gemini-history-signatures.mjs +9 -4
  81. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +3 -1
  82. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +180 -0
  83. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +21 -82
  84. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +4 -1
  85. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -12
  86. package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +21 -20
  87. package/src/runtime/agent/orchestrator/providers/gemini.mjs +97 -51
  88. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  89. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +68 -19
  90. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
  91. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +3 -0
  92. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +2 -1
  93. package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
  94. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +15 -0
  95. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +35 -0
  96. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +4 -9
  97. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -0
  98. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
  99. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +5 -3
  100. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +22 -0
  101. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +1 -0
  102. package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +50 -0
  103. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +23 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +16 -8
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-state.mjs +3 -2
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +94 -13
  109. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +193 -0
  110. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +66 -2
  111. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -1
  112. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +6 -0
  113. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +6 -3
  114. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -14
  115. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +26 -6
  116. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +42 -25
  117. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +274 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +72 -32
  119. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +18 -11
  120. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +1 -2
  121. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-stamps.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +2 -3
  123. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +2 -2
  124. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +1 -3
  125. package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +34 -3
  126. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +5 -1
  127. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +22 -2
  128. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -0
  129. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +5 -9
  130. package/src/runtime/agent/orchestrator/providers/openai-retry-policy.mjs +9 -0
  131. package/src/runtime/agent/orchestrator/providers/openai-ws-handshake.mjs +16 -8
  132. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +1 -2
  133. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/policy.mjs +25 -14
  134. package/src/runtime/agent/orchestrator/providers/openai-ws-terminal.mjs +18 -11
  135. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +35 -24
  136. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +581 -0
  137. package/src/runtime/agent/orchestrator/providers/registry.mjs +35 -9
  138. package/src/runtime/agent/orchestrator/providers/retry-backoff.mjs +46 -7
  139. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +118 -219
  140. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -0
  141. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +148 -45
  142. package/src/runtime/agent/orchestrator/providers/retry-midstream.mjs +81 -16
  143. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -0
  144. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +3 -0
  145. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
  146. package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
  147. package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +0 -18
  148. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +57 -28
  149. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +19 -16
  150. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +268 -181
  151. package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
  152. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
  153. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
  154. package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
  155. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +270 -0
  156. package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
  157. package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
  158. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +6 -2
  159. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
  160. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
  161. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
  162. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
  163. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
  164. package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
  165. package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
  166. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
  167. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
  168. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +3 -2
  169. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -4
  170. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +4 -3
  171. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +102 -18
  172. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
  173. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +3 -1
  174. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +42 -5
  175. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +14 -3
  176. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +20 -12
  177. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +50 -23
  178. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +4 -0
  179. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +22 -14
  180. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  181. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +18 -7
  182. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
  183. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +4 -3
  184. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +155 -29
  185. package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
  186. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +6 -3
  187. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
  188. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +16 -0
  189. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +22 -10
  190. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +819 -0
  191. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +12 -0
  192. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +22 -12
  193. package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
  194. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +183 -374
  195. package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
  196. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
  197. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +41 -3
  198. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +38 -17
  199. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
  200. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +12 -15
  201. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
  202. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
  203. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  204. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
  205. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
  206. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +173 -22
  207. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
  208. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +5 -4
  209. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +15 -2
  210. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
  211. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
  212. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
  213. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
  214. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  215. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
  216. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
  217. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  218. package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
  219. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
  220. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
  221. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
  222. package/src/runtime/attachments/store.d.mts +3 -0
  223. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
  224. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
  225. package/src/runtime/channels/lib/worker-main.mjs +17 -14
  226. package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
  227. package/src/runtime/effort-judge/judge-client.mjs +56 -18
  228. package/src/runtime/memory/lib/memory.mjs +1 -1
  229. package/src/runtime/memory/lib/pg/process.mjs +34 -24
  230. package/src/runtime/memory/lib/pg/supervisor.mjs +16 -13
  231. package/src/runtime/memory/lib/session-ingest.mjs +2 -2
  232. package/src/runtime/office/README.md +1 -1
  233. package/src/runtime/office/shared/values.mjs +0 -5
  234. package/src/runtime/shared/atomic-file.mjs +25 -4
  235. package/src/runtime/shared/background-tasks.mjs +92 -14
  236. package/src/runtime/shared/err-text.mjs +25 -0
  237. package/src/runtime/shared/error-presentation.mjs +159 -19
  238. package/src/runtime/shared/extension-scopes.mjs +5 -3
  239. package/src/runtime/shared/file-lock.mjs +37 -11
  240. package/src/runtime/shared/llm/anthropic-betas.mjs +4 -3
  241. package/src/runtime/shared/llm/anthropic-effort.mjs +134 -131
  242. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +50 -14
  243. package/src/runtime/shared/llm/model-catalog.mjs +53 -52
  244. package/src/runtime/shared/llm/openai-daybreak.mjs +4 -16
  245. package/src/runtime/shared/llm/provider-catalog-registry.mjs +17 -0
  246. package/src/runtime/shared/llm/provider-model-identities.mjs +13 -4
  247. package/src/runtime/shared/llm/usage-rollup.mjs +0 -1
  248. package/src/runtime/shared/model-display.mjs +13 -0
  249. package/src/runtime/shared/plugin-manifest.mjs +18 -9
  250. package/src/runtime/shared/pristine-execution-contract.json +9 -2
  251. package/src/runtime/shared/pristine-execution.mjs +28 -4
  252. package/src/runtime/shared/provider-accounts.mjs +124 -20
  253. package/src/runtime/shared/provider-api-key.mjs +12 -0
  254. package/src/runtime/shared/quota-resume-store.mjs +11 -0
  255. package/src/runtime/shared/session-baseline-store.mjs +960 -0
  256. package/src/runtime/shared/shell-display.mjs +9 -3
  257. package/src/runtime/shared/skill-mentions.d.mts +10 -0
  258. package/src/runtime/shared/skill-mentions.mjs +133 -0
  259. package/src/runtime/shared/skill-script-runtime.mjs +264 -0
  260. package/src/runtime/shared/statusline/statusline-format.mjs +4 -4
  261. package/src/runtime/shared/task-notification-envelope.mjs +2 -2
  262. package/src/runtime/shared/tool-primitives.mjs +35 -2
  263. package/src/runtime/shared/turn-changes.mjs +447 -219
  264. package/src/runtime/shared/turn-snapshot-store.mjs +182 -1
  265. package/src/runtime/shared/turn-snapshot.mjs +170 -48
  266. package/src/runtime/shared/turn-worktree-snapshot.mjs +160 -51
  267. package/src/runtime/shared/worker-requests.mjs +15 -2
  268. package/src/runtime/shared/worktrees/contract.mjs +0 -24
  269. package/src/runtime/shared/worktrees/test-support.mjs +19 -4
  270. package/src/session-runtime/automation-agents.mjs +8 -2
  271. package/src/session-runtime/boot/apis.mjs +61 -0
  272. package/src/session-runtime/boot/config.mjs +5 -2
  273. package/src/session-runtime/boot/facade.mjs +1 -0
  274. package/src/session-runtime/boot/infrastructure.mjs +35 -18
  275. package/src/session-runtime/conversation-import/discovery.mjs +698 -0
  276. package/src/session-runtime/conversation-import/service.mjs +369 -0
  277. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +10 -5
  278. package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
  279. package/src/session-runtime/lifecycle/session-catalog.mjs +2 -0
  280. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  281. package/src/session-runtime/mcp-glue/connect.mjs +83 -16
  282. package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
  283. package/src/session-runtime/mcp-glue/server-catalog.mjs +135 -33
  284. package/src/session-runtime/mcp-glue.mjs +25 -2
  285. package/src/session-runtime/mcp-oauth-login.mjs +174 -0
  286. package/src/session-runtime/model-route/model-switch-target.mjs +34 -0
  287. package/src/session-runtime/model-route/set-route.mjs +83 -15
  288. package/src/session-runtime/model-route/web-search-route.mjs +1 -1
  289. package/src/session-runtime/native-web-search/routes.mjs +22 -3
  290. package/src/session-runtime/new-session-config.mjs +3 -2
  291. package/src/session-runtime/notification-bus.mjs +9 -2
  292. package/src/session-runtime/provider-auth/accounts.mjs +33 -15
  293. package/src/session-runtime/provider-models/model-meta.mjs +12 -3
  294. package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
  295. package/src/session-runtime/provider-readiness.mjs +16 -10
  296. package/src/session-runtime/quick-model-rows/web-search-rows.mjs +13 -8
  297. package/src/session-runtime/quick-web-search-models.mjs +13 -38
  298. package/src/session-runtime/resource-extension-sync.mjs +24 -5
  299. package/src/session-runtime/resource-mcp-api.mjs +198 -78
  300. package/src/session-runtime/resource-plugins-api.mjs +29 -41
  301. package/src/session-runtime/resource-scope-decorate.mjs +6 -8
  302. package/src/session-runtime/resource-skills-api.mjs +23 -2
  303. package/src/session-runtime/runtime-review-api.mjs +13 -0
  304. package/src/session-runtime/schedule-session-run.mjs +1 -1
  305. package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +13 -1
  306. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +7 -2
  307. package/src/session-runtime/services/agent-tool/send-flow.mjs +22 -4
  308. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +2 -0
  309. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +22 -5
  310. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -1
  311. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -2
  312. package/src/session-runtime/services/agent-tool/worker-rows.mjs +17 -0
  313. package/src/session-runtime/services/channel-session-router.mjs +4 -1
  314. package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
  315. package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
  316. package/src/session-runtime/services/provider-admin.mjs +64 -18
  317. package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
  318. package/src/session-runtime/settings-compaction-api.mjs +1 -0
  319. package/src/session-runtime/setup-tool/executor.mjs +12 -1
  320. package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
  321. package/src/session-runtime/setup-tool/settings-contract.mjs +1 -0
  322. package/src/session-runtime/setup-tool/tool-defs.mjs +7 -1
  323. package/src/session-runtime/skills-api.mjs +7 -3
  324. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
  325. package/src/session-runtime/webhook-session-run.mjs +16 -4
  326. package/src/skill-script-entry.mjs +36 -0
  327. package/src/standalone/daemon-drain-policy.mjs +8 -0
  328. package/src/standalone/daemon-stored-session-views.mjs +11 -3
  329. package/src/standalone/daemon-telemetry.mjs +55 -0
  330. package/src/standalone/daemon.mjs +44 -11
  331. package/src/standalone/session-protocol.mjs +12 -0
  332. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +33 -2
  333. package/src/standalone/session-service/agent-tree/agent-turns.mjs +26 -10
  334. package/src/standalone/session-service/agent-tree.mjs +12 -0
  335. package/src/standalone/session-service/quota-resume.mjs +8 -0
  336. package/src/standalone/session-service/retention/busy.mjs +15 -1
  337. package/src/standalone/session-service/session-calls/session-action.mjs +6 -1
  338. package/src/standalone/session-service-api.mjs +5 -0
  339. package/src/standalone/session-service.mjs +1 -0
  340. package/src/tui/App.jsx +9 -1
  341. package/src/tui/app/app-view/floating-panel.jsx +9 -1
  342. package/src/tui/app/app-view.jsx +7 -0
  343. package/src/tui/app/create-app-pickers.mjs +4 -0
  344. package/src/tui/app/extension-pickers/mcp-login.mjs +107 -0
  345. package/src/tui/app/extension-pickers/mcp-servers-picker.mjs +2 -1
  346. package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
  347. package/src/tui/app/extension-pickers.mjs +2 -1
  348. package/src/tui/app/model-options.mjs +13 -16
  349. package/src/tui/app/skill-mention-palette.mjs +88 -0
  350. package/src/tui/app/slash-commands.mjs +7 -2
  351. package/src/tui/app/slash-dispatch/panel-commands.mjs +16 -1
  352. package/src/tui/app/slash-palette.mjs +23 -7
  353. package/src/tui/app/transcript-row-estimate.mjs +11 -3
  354. package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
  355. package/src/tui/app/use-prompt-state.mjs +5 -0
  356. package/src/tui/components/Message.jsx +22 -1
  357. package/src/tui/components/PromptInput.jsx +21 -3
  358. package/src/tui/components/SlashCommandPalette.jsx +12 -3
  359. package/src/tui/components/StatusLine.jsx +2 -0
  360. package/src/tui/components/TranscriptItem.jsx +9 -1
  361. package/src/tui/components/prompt-input/key-handler.mjs +21 -8
  362. package/src/tui/components/status-line/local-line.mjs +5 -2
  363. package/src/tui/components/status-line/use-statusline-text.mjs +7 -1
  364. package/src/tui/components/user-message-skills.mjs +38 -0
  365. package/src/tui/dist/index.mjs +1323 -214
  366. package/src/tui/session/queue-helpers.mjs +7 -2
  367. package/src/tui/session/quota-resume.mjs +46 -14
  368. package/src/tui/session/session-api/commands.mjs +33 -24
  369. package/src/tui/session/session-api/extensions.mjs +26 -2
  370. package/src/tui/session/session-api/integrations.mjs +7 -0
  371. package/src/tui/session/session-api/lifecycle.mjs +5 -1
  372. package/src/tui/session/session-api/routes.mjs +71 -7
  373. package/src/tui/session/session-api/settings.mjs +12 -14
  374. package/src/tui/session/session-flow/drain.mjs +21 -5
  375. package/src/tui/session/session-flow/failed-turn-rewind.mjs +10 -4
  376. package/src/tui/session/tui-steering-persist.mjs +25 -9
  377. package/src/tui/session/turn.mjs +55 -15
  378. package/src/tui/statusline-ansi-bridge.mjs +1 -1
  379. package/src/ui/statusline-agents.mjs +15 -4
  380. package/src/ui/statusline.mjs +7 -3
  381. package/src/ui/streaming-markdown-heal.mjs +34 -3
  382. package/src/vendor/statusline/src/gateway/claude-current.mjs +14 -4
  383. package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
  384. package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
  385. package/src/runtime/memory/lib/memory-session-merge.mjs +0 -32
  386. package/src/runtime/shared/llm/usage-session-history.mjs +0 -266
@@ -6,7 +6,6 @@
6
6
  * unless a durable turn checkpoint exists; only then does it lazily enter the
7
7
  * reconnect recovery boundary so restored panes never paint a stale prompt.
8
8
  */
9
- import { readdirSync } from 'node:fs';
10
9
  import { readFile } from 'node:fs/promises';
11
10
  import { join } from 'node:path';
12
11
  // Leaf helpers only (no store.mjs, no workers, no config): the three-way
@@ -14,8 +13,15 @@ import { join } from 'node:path';
14
13
  // authoritative store uses, so the cold catalog cannot disagree with it.
15
14
  import { probePath, readTextFile, PROBE_PRESENT, PROBE_ABSENT } from './store/fs-probe.mjs';
16
15
  import { readTopLevelLifecycleRecord, isLifecycleUnreadable } from './lifecycle-scan.mjs';
17
- import { isAgentOnlySession, isRootLeadSession, sessionVisibility } from './store-summary-visibility.mjs';
18
- import { applySummaryLogText, readSummaryLogText } from './store-summary-log.mjs';
16
+ import { sessionVisibility } from './store-summary-visibility.mjs';
17
+ import {
18
+ STAMP_ABSENT_CODES,
19
+ createOrdinaryCatalog,
20
+ indexListing,
21
+ observeStamp,
22
+ scanSessionFiles,
23
+ stampedDerivation,
24
+ } from './store-summary-catalog.mjs';
19
25
  import { createStoredTranscriptCache, nextProjectionStamp } from './store-transcript-cache.mjs';
20
26
  import {
21
27
  OFFLOAD_MIN_CHARS,
@@ -24,9 +30,11 @@ import {
24
30
  } from './store-transcript-worker.mjs';
25
31
  import { projectStoredTranscript } from './store-transcript-projection.mjs';
26
32
  import { dataDir, sessionHeartbeatMtimes } from './store-summary-locations.mjs';
27
- import { desktopSession, isStoredSessionId, positiveNumber, workspaceSummary } from './store-summary-fields.mjs';
33
+ import { isStoredSessionId, workspaceSummary } from './store-summary-fields.mjs';
28
34
  import { readArchivedAgentResult } from './store-summary-archived-agent.mjs';
29
35
  import { compactionArchivePath, readCompactionArchivePage } from './compaction-archive.mjs';
36
+ import { currentSessionCatalogDb, onSessionCatalogDbChange, sessionCatalogDb } from './catalog-db/index.mjs';
37
+ import { decodeCatalogCursor, encodeCatalogCursor, isAfterCursor } from './catalog-db/schema.mjs';
30
38
  import {
31
39
  readCanonicalSessionRecord as readCanonicalLifecycle,
32
40
  offerStrictSessionRecord,
@@ -65,331 +73,120 @@ export function storedTranscriptCacheStats() {
65
73
  return storedTranscriptCache.stats();
66
74
  }
67
75
 
68
- // ── Stamp-keyed strict derivations ──────────────────────────────────────────
69
- // Session metadata and catalog rows are small values derived from a strict
70
- // parse of the whole record. They are reused only while the file still has
71
- // the FULL stamp { dev, ino, size, mtimeNs, ctimeNs } the parsed bytes were
72
- // proven to come from (stat → read → stat) and that stamp is past the racy
73
- // window. Any other file — changed, replaced by another writer, unreadable —
74
- // is read and strictly parsed again.
75
- const STAMPED_DERIVATION_LIMIT = 2048;
76
- const stampedDerivations = new Map(); // `${kind}|${path}` → { stamp, value }
76
+ // ── Ordinary catalog ────────────────────────────────────────────────────────
77
+ // The JSON path is the catalog engine of store-summary-catalog.mjs on this
78
+ // thread: every process but the daemon, and the daemon whenever its session
79
+ // catalog DB cannot answer (gate, off-switch, backfill, rebuild, any error).
80
+ // In the daemon, the catalog DB worker runs the same engine off this thread
81
+ // and an ordinary listing is one request to it (catalog-db/index.mjs).
82
+ const ordinaryCatalog = createOrdinaryCatalog();
83
+ const catalogListeners = new Set();
77
84
 
78
- function observeStamp(path) {
79
- try {
80
- return statSessionStamp(path);
81
- } catch {
82
- return null;
83
- }
84
- }
85
-
86
- /** `{ state, value }`: `derive(text)` for the record at `path`, or its cached
87
- * result while the file is provably unchanged. */
88
- function stampedDerivation(kind, path, derive) {
89
- const key = `${kind}|${path}`;
90
- const before = observeStamp(path);
91
- const cached = stampedDerivations.get(key);
92
- if (before && cached && sameSessionStamp(cached.stamp, before)) {
93
- stampedDerivations.delete(key);
94
- stampedDerivations.set(key, cached);
95
- return { state: PROBE_PRESENT, value: cached.value };
96
- }
97
- stampedDerivations.delete(key);
98
- const read = readTextFile(path);
99
- if (read.state !== PROBE_PRESENT) return { state: read.state, value: null };
100
- const value = derive(read.text);
101
- if (isSettledSessionStamp(before) && sameSessionStamp(before, observeStamp(path))) {
102
- stampedDerivations.set(key, { stamp: before, value });
103
- if (stampedDerivations.size > STAMPED_DERIVATION_LIMIT) {
104
- stampedDerivations.delete(stampedDerivations.keys().next().value);
85
+ function notifyCatalogListeners() {
86
+ for (const listener of catalogListeners) {
87
+ try {
88
+ listener();
89
+ } catch {
90
+ /* a listener's failure is its own */
105
91
  }
106
92
  }
107
- return { state: PROBE_PRESENT, value };
108
93
  }
109
94
 
110
- /** Test seam: forget every stamp-keyed derivation. */
111
- export function clearStoredSessionDerivations() {
112
- stampedDerivations.clear();
113
- }
114
-
115
- const SESSION_SUMMARY_INDEX_VERSION = 2;
116
-
117
- // Mirror of lifecycle-api.mjs listLeadSessions visibility: the cold catalog
118
- // must never surface worker/agent dispatches (memory ingest chunks, judges,
119
- // spawned agents) — the authoritative engine excludes them, so a click on
120
- // such a row dead-ends in "Session is not available." (user report).
121
- const LEAD_OWNERS = new Set(['cli', 'user', 'mixdog']);
95
+ ordinaryCatalog.subscribe(notifyCatalogListeners);
96
+ onSessionCatalogDbChange(notifyCatalogListeners);
122
97
 
123
- function isLeadVisibleRow(row) {
124
- const owner = String(row.owner || 'user')
125
- .trim()
126
- .toLowerCase();
127
- if (isAgentOnlySession(row)) return false;
128
- if (!isRootLeadSession(row) && owner && !LEAD_OWNERS.has(owner)) return false;
129
- // Mirror listLeadSessions: a previewless zero-message row is an unusable
130
- // scratch (desktop boot leftovers, crashed first turns) — resuming it
131
- // shows an empty conversation, so the catalog hides it.
132
- if (!row.preview && row.messageCount === 0) return false;
133
- const sourceType = String(row.sourceType || '')
134
- .trim()
135
- .toLowerCase();
136
- // Mirror listLeadSessions: one-shot `mixdog exec` runs stay out of Recent.
137
- if (sourceType === 'exec') return false;
138
- const sourceName = String(row.sourceName || '')
139
- .trim()
140
- .toLowerCase();
141
- const agent = String(row.agent || '')
142
- .trim()
143
- .toLowerCase();
144
- return (
145
- agent === 'lead' ||
146
- sourceType === 'lead' ||
147
- sourceType === 'cli' ||
148
- sourceType === 'schedule' ||
149
- sourceType === 'webhook' ||
150
- (!sourceType && !sourceName && owner !== 'agent')
151
- );
98
+ /** `listener()` runs when a background pass finds a session file that changed
99
+ * without a directory or index change, i.e. when an ordinary listing would
100
+ * now differ from the last one. Returns the unsubscribe function. */
101
+ export function subscribeStoredSessionCatalogChanges(listener) {
102
+ catalogListeners.add(listener);
103
+ return () => catalogListeners.delete(listener);
152
104
  }
153
105
 
154
- function cleanText(value, maximum = 240) {
155
- return (
156
- String(value || '')
157
- .replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, ' ')
158
- .replace(/<mcp-instructions>[\s\S]*?<\/mcp-instructions>/gi, ' ')
159
- // Session-context envelope (mirror of session-text.mjs
160
- // stripSessionDisplayEnvelope): the "# Session / Cwd / Model /
161
- // Workflow" header must never become a Recent title.
162
- .replace(/^\s*# Session\r?\n(?:(?:Cwd|Model|Workflow):[^\r\n]*(?:\r?\n|$))+(?:\r?\n)?/i, ' ')
163
- .replace(/^\s*#\s*Session\s+Cwd:\s+\S+(?:\s+Model:[^\r\n]*?)?(?:\s+Workflow:\s+\S+)?\s*/i, ' ')
164
- .replace(/\s+/g, ' ')
165
- .trim()
166
- .slice(0, maximum)
167
- );
106
+ /** Run a background verification of the ordinary catalog now (or join the
107
+ * running one) — here and in the catalog DB worker; resolves when done. */
108
+ export async function verifyStoredSessionCatalog() {
109
+ await Promise.all([ordinaryCatalog.verify(), currentSessionCatalogDb(dataDir())?.verifyFiles()]);
168
110
  }
169
111
 
170
- function messageText(content) {
171
- if (typeof content === 'string') return content;
172
- if (!Array.isArray(content)) return '';
173
- return content
174
- .map((part) => (typeof part === 'string' ? part : String(part?.text || part?.content || '')))
175
- .filter(Boolean)
176
- .join(' ');
177
- }
178
-
179
- function normalizedRow(row, heartbeatAt = 0) {
180
- if (!row || typeof row.id !== 'string' || !isStoredSessionId(row.id)) return null;
181
- return {
182
- id: row.id,
183
- updatedAt: positiveNumber(row.updatedAt, 0),
184
- // Conversation-activity timestamp (mirror of listLeadSessions):
185
- // detach/resume bookkeeping bumps updatedAt in bulk on restarts, so
186
- // Recent must order by lastUsedAt or every restart reshuffles rows.
187
- lastUsedAt: positiveNumber(row.lastUsedAt, 0),
188
- createdAt: positiveNumber(row.createdAt, 0),
189
- lastHeartbeatAt: positiveNumber(row.lastHeartbeatAt, 0),
190
- // Liveness comes from the .hb sidecar mtime alone: stored row fields
191
- // (summary index / final session save) survive completion and must not
192
- // keep the desktop working indicator on after the sidecar is deleted.
193
- heartbeatAt: positiveNumber(heartbeatAt, 0),
194
- closed: row.closed === true,
195
- status: String(row.status || (row.closed === true ? 'closed' : 'idle')),
196
- owner: row.owner || 'user',
197
- agent: row.agent || null,
198
- sourceType: row.sourceType || null,
199
- sourceName: row.sourceName || null,
200
- scopeKey: row.scopeKey || null,
201
- ownerSessionId: row.ownerSessionId || row.parentSessionId || null,
202
- visibility: sessionVisibility(row),
203
- clientHostPid: positiveNumber(row.clientHostPid, 0) || null,
204
- cwd: row.cwd || '',
205
- desktopSession: desktopSession(row.desktopSession, row.cwd),
206
- ...(workspaceSummary(row.workspace) ? { workspace: workspaceSummary(row.workspace) } : {}),
207
- provider: row.provider || null,
208
- model: row.model || null,
209
- agentTag: row.agentTag || null,
210
- task_id: row.task_id || row.taskId || null,
211
- permission: row.permission || null,
212
- toolPermission: row.toolPermission || null,
213
- messageCount: Math.max(0, Math.floor(Number(row.messageCount) || 0)),
214
- title: cleanText(row.title, 100),
215
- preview: cleanText(row.preview),
216
- generation: typeof row.generation === 'number' ? row.generation : 0,
217
- storageMtimeMs: positiveNumber(row.storageMtimeMs, 0),
218
- storageSize: positiveNumber(row.storageSize, 0),
219
- detachedReason: row.detachedReason || null,
220
- };
112
+ /** Test seam: forget the ordinary catalog and the parsed summary index. */
113
+ export function clearStoredSessionCatalog() {
114
+ ordinaryCatalog.clear();
221
115
  }
222
116
 
223
- function leadRowsWithAgentHeartbeat(rows) {
224
- const agentHeartbeatByOwner = new Map();
225
- for (const row of rows) {
226
- const heartbeatAt = positiveNumber(row?.heartbeatAt, 0);
227
- const ownerSessionId = String(row?.ownerSessionId || '').trim();
228
- const owner = String(row?.owner || '')
229
- .trim()
230
- .toLowerCase();
231
- const agent = String(row?.agent || '')
232
- .trim()
233
- .toLowerCase();
234
- if (!heartbeatAt || !ownerSessionId || (owner !== 'agent' && (!agent || agent === 'lead'))) continue;
235
- agentHeartbeatByOwner.set(ownerSessionId, Math.max(agentHeartbeatByOwner.get(ownerSessionId) || 0, heartbeatAt));
117
+ export function listStoredSessionSummaries(options = {}) {
118
+ // The index is consulted on EVERY path: its rows are the authority that
119
+ // must be retained whenever storage cannot be enumerated or an individual
120
+ // session file cannot be read.
121
+ const index = ordinaryCatalog.loadIndex();
122
+ // An index that EXISTS but is unreadable (EACCES/EIO) is neither missing
123
+ // nor empty: the scan still runs, and when IT cannot enumerate either, the
124
+ // catalog reports nothing rather than inventing an empty truth.
125
+ if (options.refreshFromStorage === true) {
126
+ const heartbeatMtimes = sessionHeartbeatMtimes();
127
+ return scanSessionFiles(heartbeatMtimes, index.rowsById, true) ?? indexListing(index, heartbeatMtimes) ?? [];
236
128
  }
237
- return rows
238
- .filter((row) => row && isLeadVisibleRow(row))
239
- .map((row) => {
240
- const agentHeartbeatAt = agentHeartbeatByOwner.get(row.id) || 0;
241
- return agentHeartbeatAt > 0 ? { ...row, agentHeartbeatAt } : row;
242
- });
129
+ if (options.rebuildIfMissing === false) return indexListing(index, sessionHeartbeatMtimes()) ?? [];
130
+ // A missing/malformed index leaves every file to be parsed (once per
131
+ // stamp); a present one lets unchanged files reuse their durable summary.
132
+ return ordinaryCatalog.refresh(index).rows ?? indexListing(index, sessionHeartbeatMtimes()) ?? [];
243
133
  }
244
134
 
245
- function rowFromSession(session, heartbeatAt = 0) {
246
- const messages = Array.isArray(session?.messages) ? session.messages : [];
247
- const preview =
248
- messages
249
- .filter((message) => message?.role === 'user')
250
- // Cold-path mirror of isSessionPreviewNoise's synthetic skips: compact
251
- // handoffs and runtime notices must not become session titles.
252
- .filter(
253
- (message) =>
254
- !/^\s*(?:a previous model worked on this task|re-attached after compaction\b|reference files:\s|\[mixdog-runtime\]|the async (?:agent|shell) task\b)/i.test(
255
- messageText(message.content)
256
- )
257
- )
258
- .map((message) => cleanText(messageText(message.content)))
259
- .find(Boolean) || '';
260
- return normalizedRow(
261
- {
262
- ...session,
263
- messageCount: messages.filter((message) => message?.role === 'user' || message?.role === 'assistant').length,
264
- preview,
265
- },
266
- heartbeatAt
267
- );
135
+ /** The catalog DB's answer to an ordinary listing (`{ rows, more }`), or
136
+ * null when this process has none or it cannot answer. No filesystem work
137
+ * on this thread: the worker checks storage, applies changes, answers. */
138
+ async function catalogDbListing(page) {
139
+ const db = sessionCatalogDb(dataDir());
140
+ if (!db) return null;
141
+ const answer = await db.list(page);
142
+ // While the DB answers, the JSON path's reconcile state is dead weight.
143
+ if (answer) ordinaryCatalog.clear();
144
+ return answer;
268
145
  }
269
146
 
270
147
  /**
271
- * Read-only scan of the authoritative session files.
272
- * Returns `null` when the directory itself is NOT enumerable (unreadable
273
- * stat / readdir): that is not "no sessions", and the caller must keep the
274
- * authority it already has (the index rows) instead of publishing an empty
275
- * catalog. `indexRowsById` carries those rows so unchanged files can reuse the
276
- * durable summary and a single UNREADABLE file retains its last known row
277
- * instead of vanishing.
148
+ * listStoredSessionSummaries for async callers (the daemon's session views).
149
+ * In the daemon the ordinary listing comes from the session catalog DB
150
+ * worker; anywhere else, and whenever the DB cannot answer, it is the JSON
151
+ * path. `refreshFromStorage` and `rebuildIfMissing: false` always take the
152
+ * JSON/file path.
278
153
  */
279
- function scanSessionFiles(
280
- heartbeatMtimes = sessionHeartbeatMtimes(),
281
- indexRowsById = new Map(),
282
- { forceRead = false } = {}
283
- ) {
284
- const directory = join(dataDir(), 'sessions');
285
- const dirProbe = probePath(directory);
286
- if (dirProbe.state === PROBE_ABSENT) return [];
287
- if (dirProbe.state !== PROBE_PRESENT) return null;
288
- let entries;
289
- try {
290
- entries = readdirSync(directory);
291
- } catch {
292
- return null;
154
+ export async function listStoredSessionSummariesAsync(options = {}) {
155
+ if (options.refreshFromStorage === true || options.rebuildIfMissing === false) {
156
+ return listStoredSessionSummaries(options);
293
157
  }
294
- const rows = [];
295
- for (const filename of entries) {
296
- if (!filename.endsWith('.json')) continue;
297
- const storageId = filename.slice(0, -5);
298
- const path = join(directory, filename);
299
- const retained = indexRowsById.get(storageId);
300
- const probe = probePath(path);
301
- // Provably gone: the row legitimately disappears with the file.
302
- if (probe.state === PROBE_ABSENT) continue;
303
- if (probe.state !== PROBE_PRESENT) {
304
- if (retained) rows.push(retained);
305
- continue;
306
- }
307
- if (!forceRead && retained) {
308
- const exactFingerprint =
309
- retained.storageMtimeMs > 0 &&
310
- retained.storageSize > 0 &&
311
- retained.storageMtimeMs === probe.mtimeMs &&
312
- retained.storageSize === probe.size;
313
- if (exactFingerprint) {
314
- rows.push(retained);
315
- continue;
316
- }
317
- }
318
- // A file the index does not describe (never saved by the store, or
319
- // saved after the index was written) is parsed once per full stamp.
320
- const read = stampedDerivation('row', path, (text) => {
321
- // Same strict authority as the store: a duplicate/ambiguous top-level
322
- // record or a foreign identity is not this file's session.
323
- const record = readTopLevelLifecycleRecord(text);
324
- if (isLifecycleUnreadable(record) || record.id !== storageId) return null;
325
- return rowFromSession(record.doc, 0);
326
- });
327
- // Provably gone: the row legitimately disappears with the file.
328
- if (read.state === PROBE_ABSENT) continue;
329
- if (read.state !== PROBE_PRESENT) {
330
- // Exists and could not be read (EACCES/EIO): retain the last known
331
- // row rather than let a transient IO error delete it from view.
332
- if (retained) rows.push(retained);
333
- continue;
334
- }
335
- if (read.value) {
336
- rows.push({
337
- ...structuredClone(read.value),
338
- heartbeatAt: positiveNumber(heartbeatMtimes.get(storageId), 0),
339
- storageMtimeMs: probe.mtimeMs,
340
- storageSize: probe.size,
341
- });
342
- }
343
- }
344
- return leadRowsWithAgentHeartbeat(rows).sort(byRecentActivity);
158
+ const answer = await catalogDbListing(null);
159
+ return answer ? answer.rows : listStoredSessionSummaries();
345
160
  }
346
161
 
347
- // Newest conversation activity first (see normalizedRow's lastUsedAt note).
348
- function byRecentActivity(left, right) {
349
- return (right.lastUsedAt || right.updatedAt || 0) - (left.lastUsedAt || left.updatedAt || 0);
350
- }
162
+ const DEFAULT_PAGE_LIMIT = 50;
163
+ const MAX_PAGE_LIMIT = 1000;
351
164
 
352
- export function listStoredSessionSummaries(options = {}) {
353
- const heartbeatMtimes = sessionHeartbeatMtimes();
354
- const indexPath = join(dataDir(), 'session-summaries.json');
355
- // The index is read on EVERY path (it is one small file): its rows are the
356
- // authority that must be retained whenever storage cannot be enumerated or
357
- // an individual session file cannot be read.
358
- const indexRowsById = new Map();
359
- let indexRows = null;
360
- // The delta log (per-session updates since the last compaction) is read
361
- // before the base; see store-summary-log.mjs.
362
- const logText = readSummaryLogText(indexPath);
363
- const indexRead = readTextFile(indexPath);
364
- if (indexRead.state === PROBE_PRESENT) {
365
- try {
366
- const index = applySummaryLogText(JSON.parse(indexRead.text), logText);
367
- if (Number(index?.version) === SESSION_SUMMARY_INDEX_VERSION) {
368
- const normalizedRows = (Array.isArray(index.rows) ? index.rows : [])
369
- .map((row) => normalizedRow(row, heartbeatMtimes.get(row?.id) || 0))
370
- .filter(Boolean);
371
- for (const row of normalizedRows) indexRowsById.set(row.id, row);
372
- indexRows = leadRowsWithAgentHeartbeat(normalizedRows).sort(byRecentActivity);
373
- }
374
- } catch {
375
- /* malformed sidecar: the files below are the authority */
376
- }
377
- }
378
- // An index that EXISTS but is unreadable (EACCES/EIO) is neither missing
379
- // nor empty: the scan below still runs, and when IT cannot enumerate
380
- // either, the catalog reports nothing rather than inventing an empty truth.
381
- const scan = (forceRead = false) => scanSessionFiles(heartbeatMtimes, indexRowsById, { forceRead });
382
- if (options.refreshFromStorage === true) return scan(true) ?? indexRows ?? [];
383
- if (indexRows) {
384
- if (options.rebuildIfMissing === false) return indexRows;
385
- // Enumerate identities to detect deletions, stat known files, and parse
386
- // only rows newer than the index. Cold-start cost therefore scales with
387
- // changed transcripts rather than total transcript bytes.
388
- return scan(false) ?? indexRows;
165
+ /**
166
+ * One page of the ordinary listing: `{ sessions, nextCursor }` in catalog
167
+ * order (newest activity first, ties by id), `limit` rows (default 50, at
168
+ * most 1000) after the keyset `cursor` (an earlier page's `nextCursor`;
169
+ * absent = first page). Answered like listStoredSessionSummariesAsync; from
170
+ * the DB only the requested rows are transferred.
171
+ */
172
+ export async function listStoredSessionSummaryPage(options = {}) {
173
+ const requested = Math.floor(Number(options.limit));
174
+ const limit = requested > 0 ? Math.min(requested, MAX_PAGE_LIMIT) : DEFAULT_PAGE_LIMIT;
175
+ const after = decodeCatalogCursor(options.cursor);
176
+ const answer = await catalogDbListing({ limit, after });
177
+ if (answer) {
178
+ return {
179
+ sessions: answer.rows,
180
+ nextCursor: answer.more && answer.rows.length ? encodeCatalogCursor(answer.rows.at(-1)) : null,
181
+ };
389
182
  }
390
- // Missing/malformed index is the sole cold path that must rebuild every
391
- // summary from canonical session bytes.
392
- return options.rebuildIfMissing === false ? [] : (scan(true) ?? indexRows ?? []);
183
+ // The JSON listing is already in catalog order.
184
+ const ordered = listStoredSessionSummaries().filter((row) => isAfterCursor(row, after));
185
+ const sessions = ordered.slice(0, limit);
186
+ return {
187
+ sessions,
188
+ nextCursor: ordered.length > limit ? encodeCatalogCursor(sessions.at(-1)) : null,
189
+ };
393
190
  }
394
191
 
395
192
  /** Exact, fail-closed existence check for a durable session address.
@@ -464,8 +261,6 @@ function storedTranscriptInputPaths(sessionId) {
464
261
  };
465
262
  }
466
263
 
467
- const STAMP_ABSENT_CODES = new Set(['ENOENT', 'ENOTDIR']);
468
-
469
264
  /** Settled identity of the files readStoredSessionTranscript projects from:
470
265
  * the canonical record and its turn-checkpoint sidecar. Null while either is
471
266
  * too fresh to vouch for by stat alone (isSettledSessionStamp), unreadable,
@@ -490,6 +285,72 @@ export function storedSessionTranscriptStamp(id) {
490
285
  return archive ? `${record}|${checkpoint}|${archive}` : null;
491
286
  }
492
287
 
288
+ /** Projects the strictly parsed record `text` (read at full stamp `stamp`, or
289
+ * null when not provably stable) into the cached transcript value. */
290
+ async function produceStoredTranscript(text, stamp, context) {
291
+ const { sessionId, recordPath, itemLimit, includeMessages, mode, offloadable, checkpointAbsent, timing } = context;
292
+ // A large record with no checkpoint sidecar parses and projects on a
293
+ // worker thread: the same pipeline, off the daemon event loop. It hands
294
+ // no parse to the load cache (a later resume reads the file itself).
295
+ if (offloadable && text.length >= OFFLOAD_MIN_CHARS) {
296
+ try {
297
+ timing.worker = storedTranscriptWorkerRunning() ? 'warm' : 'cold';
298
+ const workerStartedAt = performance.now();
299
+ const answer = await projectStoredTranscriptOffThread({ sessionId, text, itemLimit });
300
+ timing.workerMs = performance.now() - workerStartedAt;
301
+ Object.assign(timing, answer.timing);
302
+ if (answer.unreadable) return null;
303
+ if (stamp) readCanonicalLifecycle.rememberStrictVerdict(recordPath, stamp, answer.lifecycle);
304
+ // Stamps are process-unique per module realm; re-issue in this one.
305
+ answer.value.projectionStamp = nextProjectionStamp();
306
+ return answer.value;
307
+ } catch {
308
+ // Worker unavailable: fall through to the in-process pipeline.
309
+ }
310
+ }
311
+ const parseStartedAt = performance.now();
312
+ const record = readTopLevelLifecycleRecord(text);
313
+ timing.parseMs = performance.now() - parseStartedAt;
314
+ if (isLifecycleUnreadable(record) || record.id !== sessionId) return null;
315
+ // The same strict verdict the lifecycle authority would reach for these
316
+ // bytes (storedSessionExists runs next when the pane's runtime loads).
317
+ if (stamp) readCanonicalLifecycle.rememberStrictVerdict(recordPath, stamp, record);
318
+ // Hand the parse to the session load cache BEFORE projecting: a resume
319
+ // that runs while this projection is still being built (the daemon
320
+ // resumes the panes it restores) claims it instead of reading the file
321
+ // again. The projection path loads the store anyway, so loading its
322
+ // cache first costs nothing and gives the hand-off somewhere to go on
323
+ // the process's first read.
324
+ let handedOver = false;
325
+ if (stamp) {
326
+ await import('./store/load-cache.mjs');
327
+ handedOver = offerStrictSessionRecord(recordPath, stamp, record, text.length);
328
+ }
329
+ // Once handed over, the record is a runtime's mutable session. The
330
+ // projection is then built from a private parse of the same bytes, so
331
+ // nothing it retains aliases that session and it needs no copying. With
332
+ // a turn checkpoint the projection recovers through loadSession, the very
333
+ // load the hand-off serves, and projects the load cache's session
334
+ // exactly as before; only that projection is detached.
335
+ const projectStartedAt = performance.now();
336
+ const value = await projectStoredTranscript(
337
+ sessionId,
338
+ handedOver && checkpointAbsent ? JSON.parse(text) : record.doc,
339
+ {
340
+ itemLimit,
341
+ includeMessages,
342
+ checkpointAbsent,
343
+ }
344
+ );
345
+ timing.projectMs = performance.now() - projectStartedAt;
346
+ if (!handedOver || checkpointAbsent || !value || mode !== 'items') return value;
347
+ try {
348
+ return structuredClone(value);
349
+ } catch {
350
+ return value;
351
+ }
352
+ }
353
+
493
354
  /** Read exactly one persisted session for a visible desktop pane. This never
494
355
  * enumerates siblings. Normal reads stay independent of runtime ownership;
495
356
  * interrupted turns conditionally use the same durable reconnect recovery as
@@ -526,10 +387,21 @@ export async function readStoredSessionTranscript(id, options = {}) {
526
387
  let readStamp = null;
527
388
  // The strict record parse (full JSON + duplicate-key scan) is itself a
528
389
  // large share of a cold read, so it only runs when the content is new.
529
- const mode = options.includeMessages === true ? 'messages' : 'items';
390
+ const includeMessages = options.includeMessages === true;
391
+ const mode = includeMessages ? 'messages' : 'items';
392
+ const checkpointAbsent = checkpoint.state === PROBE_ABSENT;
530
393
  // Large, checkpoint-free item projections run off the daemon event loop.
531
- const offloadable =
532
- options.includeMessages !== true && checkpoint.state === PROBE_ABSENT && recordStat.size >= OFFLOAD_MIN_CHARS;
394
+ const offloadable = !includeMessages && checkpointAbsent && recordStat.size >= OFFLOAD_MIN_CHARS;
395
+ const produceContext = {
396
+ sessionId,
397
+ recordPath,
398
+ itemLimit,
399
+ includeMessages,
400
+ mode,
401
+ offloadable,
402
+ checkpointAbsent,
403
+ timing,
404
+ };
533
405
  const { value, hit, read } = await storedTranscriptCache.read({
534
406
  key: `${sessionId}|${itemLimit}|${mode}`,
535
407
  // Growing history windows of one session replace each other.
@@ -566,70 +438,7 @@ export async function readStoredSessionTranscript(id, options = {}) {
566
438
  readStamp = before && sameSessionStamp(before, observeStamp(recordPath)) ? before : null;
567
439
  return body.text;
568
440
  },
569
- produce: async (text) => {
570
- const stamp = readStamp;
571
- // A large record with no checkpoint sidecar parses and projects on a
572
- // worker thread: the same pipeline, off the daemon event loop. It hands
573
- // no parse to the load cache (a later resume reads the file itself).
574
- if (offloadable && text.length >= OFFLOAD_MIN_CHARS) {
575
- try {
576
- timing.worker = storedTranscriptWorkerRunning() ? 'warm' : 'cold';
577
- const workerStartedAt = performance.now();
578
- const answer = await projectStoredTranscriptOffThread({ sessionId, text, itemLimit });
579
- timing.workerMs = performance.now() - workerStartedAt;
580
- Object.assign(timing, answer.timing);
581
- if (answer.unreadable) return null;
582
- if (stamp) readCanonicalLifecycle.rememberStrictVerdict(recordPath, stamp, answer.lifecycle);
583
- // Stamps are process-unique per module realm; re-issue in this one.
584
- answer.value.projectionStamp = nextProjectionStamp();
585
- return answer.value;
586
- } catch {
587
- // Worker unavailable: fall through to the in-process pipeline.
588
- }
589
- }
590
- const parseStartedAt = performance.now();
591
- const record = readTopLevelLifecycleRecord(text);
592
- timing.parseMs = performance.now() - parseStartedAt;
593
- if (isLifecycleUnreadable(record) || record.id !== sessionId) return null;
594
- // The same strict verdict the lifecycle authority would reach for these
595
- // bytes (storedSessionExists runs next when the pane's runtime loads).
596
- if (stamp) readCanonicalLifecycle.rememberStrictVerdict(recordPath, stamp, record);
597
- const checkpointAbsent = checkpoint.state === PROBE_ABSENT;
598
- // Hand the parse to the session load cache BEFORE projecting: a resume
599
- // that runs while this projection is still being built (the daemon
600
- // resumes the panes it restores) claims it instead of reading the file
601
- // again. The projection path loads the store anyway, so loading its
602
- // cache first costs nothing and gives the hand-off somewhere to go on
603
- // the process's first read.
604
- let handedOver = false;
605
- if (stamp) {
606
- await import('./store/load-cache.mjs');
607
- handedOver = offerStrictSessionRecord(recordPath, stamp, record, text.length);
608
- }
609
- // Once handed over, the record is a runtime's mutable session. The
610
- // projection is then built from a private parse of the same bytes, so
611
- // nothing it retains aliases that session and it needs no copying. With
612
- // a turn checkpoint the projection recovers through loadSession, the very
613
- // load the hand-off serves, and projects the load cache's session
614
- // exactly as before; only that projection is detached.
615
- const projectStartedAt = performance.now();
616
- const value = await projectStoredTranscript(
617
- sessionId,
618
- handedOver && checkpointAbsent ? JSON.parse(text) : record.doc,
619
- {
620
- itemLimit,
621
- includeMessages: options.includeMessages === true,
622
- checkpointAbsent,
623
- }
624
- );
625
- timing.projectMs = performance.now() - projectStartedAt;
626
- if (!handedOver || checkpointAbsent || !value || mode !== 'items') return value;
627
- try {
628
- return structuredClone(value);
629
- } catch {
630
- return value;
631
- }
632
- },
441
+ produce: (text) => produceStoredTranscript(text, readStamp, produceContext),
633
442
  });
634
443
  // The record vanished between stat and read: same answer as an absent probe.
635
444
  if (readState === PROBE_ABSENT) return readArchivedAgentResult(sessionId);