mixdog 1.1.2 → 1.2.1

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 (424) hide show
  1. package/README.md +8 -5
  2. package/package.json +2 -1
  3. package/scripts/lib/run-node-tests.mjs +9 -0
  4. package/scripts/prune-desktop-runtime.mjs +3 -0
  5. package/scripts/test-direct.mjs +4 -2
  6. package/src/defaults/shipped-history.json +10 -0
  7. package/src/defaults/skills/pptx/SKILL.md +6 -6
  8. package/src/defaults/skills/pptx/references/review.md +6 -4
  9. package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
  10. package/src/defaults/skills/setup/references/actions.md +18 -5
  11. package/src/defaults/skills/visualize/SKILL.md +160 -0
  12. package/src/defaults/skills/visualize/references/craft.md +88 -0
  13. package/src/headless-command.mjs +56 -3
  14. package/src/headless-exec.mjs +17 -1
  15. package/src/help.mjs +2 -0
  16. package/src/rules/shared/60-verification.md +3 -1
  17. package/src/rules/shared/70-delivery.md +4 -2
  18. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
  19. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
  20. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
  21. package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
  22. package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
  23. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
  24. package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
  25. package/src/runtime/agent/orchestrator/config.mjs +19 -5
  26. package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
  27. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
  28. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
  29. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +133 -15
  30. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +55 -8
  31. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +86 -8
  32. package/src/runtime/agent/orchestrator/mcp/client.mjs +474 -164
  33. package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
  34. package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
  35. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
  36. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
  40. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -12
  41. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
  42. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
  43. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
  44. package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
  45. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
  46. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
  47. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  48. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
  49. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
  50. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +21 -6
  51. package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
  52. package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
  53. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  55. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
  56. package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
  57. package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
  58. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
  59. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
  60. package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
  61. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +82 -0
  62. package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +556 -0
  63. package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
  64. package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
  65. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
  66. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +15 -0
  67. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
  68. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
  69. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
  70. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +15 -4
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
  77. package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -7
  78. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
  79. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
  80. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +15 -11
  81. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
  82. package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
  83. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
  84. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +278 -84
  85. package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
  86. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
  87. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
  88. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -3
  89. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
  90. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
  91. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
  92. package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
  93. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
  94. package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
  95. package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
  96. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
  97. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +24 -13
  98. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
  99. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
  100. package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
  101. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
  102. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
  103. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
  104. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
  105. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
  106. package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
  107. package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
  108. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
  109. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
  110. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
  111. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
  112. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
  113. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
  114. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +5 -3
  115. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +7 -4
  116. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +21 -10
  117. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
  118. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
  119. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
  120. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +15 -3
  121. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +22 -12
  122. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
  123. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +9 -3
  124. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
  125. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
  126. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
  127. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
  128. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
  129. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
  130. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
  131. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
  132. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
  133. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
  134. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
  136. package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
  137. package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
  138. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +33 -22
  139. package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
  140. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
  141. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
  142. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
  143. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
  144. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
  145. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +36 -0
  146. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +23 -12
  147. package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
  148. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +202 -374
  149. package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
  150. package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
  151. package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
  152. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +48 -9
  153. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +161 -0
  154. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
  155. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
  157. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
  158. package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
  159. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
  160. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
  161. package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
  162. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +203 -33
  163. package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
  164. package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
  165. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
  166. package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
  167. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -68
  168. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
  169. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +20 -3
  170. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
  171. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
  172. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
  173. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
  174. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
  175. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
  176. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
  177. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
  178. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
  179. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
  180. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
  181. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
  182. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  183. package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
  184. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
  185. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
  186. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
  187. package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
  188. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
  189. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
  190. package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
  191. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
  192. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
  193. package/src/runtime/attachments/store.d.mts +3 -0
  194. package/src/runtime/attachments/store.mjs +38 -0
  195. package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
  196. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
  197. package/src/runtime/channels/lib/worker-main.mjs +17 -14
  198. package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
  199. package/src/runtime/effort-judge/judge-client.mjs +56 -18
  200. package/src/runtime/memory/lib/memory.mjs +1 -1
  201. package/src/runtime/memory/lib/session-ingest.mjs +2 -2
  202. package/src/runtime/shared/atomic-file.mjs +33 -6
  203. package/src/runtime/shared/best-effort-stderr.mjs +8 -0
  204. package/src/runtime/shared/code-graph-state.mjs +12 -0
  205. package/src/runtime/shared/defaults-separation.mjs +2 -0
  206. package/src/runtime/shared/extension-scopes.mjs +5 -3
  207. package/src/runtime/shared/file-lock.mjs +37 -11
  208. package/src/runtime/shared/git-runner-worker.mjs +3 -2
  209. package/src/runtime/shared/git-runner.mjs +4 -3
  210. package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
  211. package/src/runtime/shared/llm/default-context-window.mjs +6 -0
  212. package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
  213. package/src/runtime/shared/llm/model-catalog.mjs +240 -130
  214. package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
  215. package/src/runtime/shared/llm/provider-catalog-registry.mjs +162 -0
  216. package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
  217. package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
  218. package/src/runtime/shared/model-proxy-mode.mjs +19 -0
  219. package/src/runtime/shared/plugin-manifest.mjs +18 -9
  220. package/src/runtime/shared/pristine-execution-contract.json +9 -2
  221. package/src/runtime/shared/pristine-execution.mjs +28 -4
  222. package/src/runtime/shared/provider-accounts.mjs +125 -21
  223. package/src/runtime/shared/provider-api-key.mjs +33 -1
  224. package/src/runtime/shared/schedule-model-ref.mjs +11 -2
  225. package/src/runtime/shared/session-usage-totals.mjs +19 -0
  226. package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
  227. package/src/runtime/shared/shell-display.mjs +9 -3
  228. package/src/runtime/shared/singleton-owner.mjs +36 -6
  229. package/src/runtime/shared/skill-mentions.d.mts +10 -0
  230. package/src/runtime/shared/skill-mentions.mjs +133 -0
  231. package/src/runtime/shared/skill-script-runtime.mjs +264 -0
  232. package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
  233. package/src/runtime/shared/tool-primitives.mjs +35 -2
  234. package/src/runtime/shared/tool-surface.mjs +7 -0
  235. package/src/runtime/shared/tool-work-units.mjs +14 -0
  236. package/src/runtime/shared/transcript-metadata.mjs +17 -1
  237. package/src/runtime/shared/turn-changes.mjs +791 -0
  238. package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
  239. package/src/runtime/shared/turn-snapshot.mjs +690 -81
  240. package/src/runtime/shared/turn-worktree-snapshot.mjs +185 -15
  241. package/src/runtime/shared/worker-requests.mjs +15 -2
  242. package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
  243. package/src/runtime/shared/worktrees/contract.mjs +551 -0
  244. package/src/runtime/shared/worktrees/disk.mjs +57 -0
  245. package/src/runtime/shared/worktrees/git.mjs +247 -0
  246. package/src/runtime/shared/worktrees/include.mjs +480 -0
  247. package/src/runtime/shared/worktrees/lock.mjs +46 -0
  248. package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
  249. package/src/runtime/shared/worktrees/manager.mjs +1215 -0
  250. package/src/runtime/shared/worktrees/merge.mjs +182 -0
  251. package/src/runtime/shared/worktrees/owner.mjs +46 -0
  252. package/src/runtime/shared/worktrees/paths.mjs +62 -0
  253. package/src/runtime/shared/worktrees/registry.mjs +110 -0
  254. package/src/runtime/shared/worktrees/settings.mjs +84 -0
  255. package/src/runtime/shared/worktrees/slug.mjs +62 -0
  256. package/src/runtime/shared/worktrees/test-support.mjs +96 -0
  257. package/src/runtime/visualize-bridge/client.mjs +181 -0
  258. package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
  259. package/src/session-runtime/automation-agents.mjs +8 -2
  260. package/src/session-runtime/boot/apis.mjs +90 -0
  261. package/src/session-runtime/boot/config.mjs +7 -2
  262. package/src/session-runtime/boot/facade.mjs +21 -0
  263. package/src/session-runtime/boot/infrastructure.mjs +30 -18
  264. package/src/session-runtime/boot/session.mjs +1 -0
  265. package/src/session-runtime/boot/tools.mjs +4 -0
  266. package/src/session-runtime/config-lifecycle.mjs +10 -1
  267. package/src/session-runtime/conversation-import/discovery.mjs +698 -0
  268. package/src/session-runtime/conversation-import/service.mjs +369 -0
  269. package/src/session-runtime/cwd-plugins/cwd-apply.mjs +21 -6
  270. package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
  271. package/src/session-runtime/global-extensions.mjs +2 -2
  272. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
  273. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
  274. package/src/session-runtime/internal-tool-executor.mjs +9 -1
  275. package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
  276. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
  277. package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
  278. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
  279. package/src/session-runtime/mcp-glue/connect.mjs +68 -16
  280. package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
  281. package/src/session-runtime/mcp-glue/server-catalog.mjs +122 -33
  282. package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
  283. package/src/session-runtime/mcp-glue.mjs +22 -2
  284. package/src/session-runtime/model-route/route-persist.mjs +3 -0
  285. package/src/session-runtime/model-route/set-route.mjs +15 -2
  286. package/src/session-runtime/new-session-config.mjs +3 -2
  287. package/src/session-runtime/notification-bus.mjs +18 -3
  288. package/src/session-runtime/provider-auth/accounts.mjs +33 -15
  289. package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
  290. package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
  291. package/src/session-runtime/provider-readiness.mjs +16 -10
  292. package/src/session-runtime/resource-extension-sync.mjs +45 -7
  293. package/src/session-runtime/resource-mcp-api.mjs +121 -84
  294. package/src/session-runtime/resource-plugins-api.mjs +29 -41
  295. package/src/session-runtime/resource-scope-decorate.mjs +6 -8
  296. package/src/session-runtime/resource-skills-api.mjs +23 -2
  297. package/src/session-runtime/route-resolution.mjs +10 -1
  298. package/src/session-runtime/route-state.mjs +16 -2
  299. package/src/session-runtime/runtime-core.mjs +5 -0
  300. package/src/session-runtime/runtime-facade.mjs +8 -0
  301. package/src/session-runtime/runtime-review-api.mjs +9 -0
  302. package/src/session-runtime/schedule-session-run.mjs +1 -1
  303. package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
  304. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
  305. package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
  306. package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
  307. package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
  308. package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
  309. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
  310. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
  311. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
  312. package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
  313. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
  314. package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
  315. package/src/session-runtime/services/agent-tool.mjs +1 -0
  316. package/src/session-runtime/services/channel-session-router.mjs +4 -1
  317. package/src/session-runtime/services/daemon-candidate.mjs +3 -2
  318. package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
  319. package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
  320. package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
  321. package/src/session-runtime/services/provider-admin.mjs +64 -18
  322. package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
  323. package/src/session-runtime/session-create/session-options.mjs +10 -1
  324. package/src/session-runtime/session-turn-api.mjs +1 -0
  325. package/src/session-runtime/settings-api.mjs +4 -2
  326. package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
  327. package/src/session-runtime/settings-worktree-api.mjs +17 -0
  328. package/src/session-runtime/setup-tool/executor.mjs +23 -5
  329. package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
  330. package/src/session-runtime/setup-tool/settings-contract.mjs +4 -1
  331. package/src/session-runtime/setup-tool/tool-defs.mjs +38 -4
  332. package/src/session-runtime/skills-api.mjs +7 -3
  333. package/src/session-runtime/turn/agent-ops.mjs +12 -1
  334. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +14 -3
  335. package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
  336. package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
  337. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
  338. package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
  339. package/src/session-runtime/turn/turn-run.mjs +6 -7
  340. package/src/session-runtime/webhook-session-run.mjs +16 -4
  341. package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
  342. package/src/session-runtime/workspace/admission-gate.mjs +197 -0
  343. package/src/session-runtime/workspace/project-branches.mjs +136 -0
  344. package/src/session-runtime/workspace/transition-steps.mjs +364 -0
  345. package/src/session-runtime/workspace/transitions.mjs +651 -0
  346. package/src/session-runtime/workspace/workspace-state.mjs +112 -0
  347. package/src/skill-script-entry.mjs +36 -0
  348. package/src/standalone/channel-transport/http-routes.mjs +17 -0
  349. package/src/standalone/channel-transport.mjs +13 -1
  350. package/src/standalone/daemon-desktop-runtime.mjs +1 -0
  351. package/src/standalone/daemon-owner.mjs +372 -0
  352. package/src/standalone/daemon-stored-session-views.mjs +15 -4
  353. package/src/standalone/daemon.mjs +179 -8
  354. package/src/standalone/desktop-service-registry.mjs +20 -0
  355. package/src/standalone/local-session-runtime.mjs +11 -0
  356. package/src/standalone/session-client/attachment-calls.mjs +3 -1
  357. package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
  358. package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
  359. package/src/standalone/session-client.mjs +104 -13
  360. package/src/standalone/session-protocol.mjs +21 -0
  361. package/src/standalone/session-proxy.mjs +3 -0
  362. package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
  363. package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
  364. package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
  365. package/src/standalone/session-service/projection/revisions.mjs +2 -0
  366. package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
  367. package/src/standalone/session-service/projection/workspace.mjs +14 -0
  368. package/src/standalone/session-service/session-calls/session-action.mjs +17 -4
  369. package/src/standalone/session-transport/call-route.mjs +13 -0
  370. package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
  371. package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
  372. package/src/standalone/session-transport/client-registry.mjs +20 -3
  373. package/src/standalone/session-transport/routes.mjs +79 -2
  374. package/src/standalone/session-transport/stack.mjs +2 -0
  375. package/src/standalone/session-transport.mjs +24 -1
  376. package/src/tui/App.jsx +9 -1
  377. package/src/tui/app/app-view/floating-panel.jsx +9 -1
  378. package/src/tui/app/app-view.jsx +6 -0
  379. package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
  380. package/src/tui/app/message-selector.mjs +2 -1
  381. package/src/tui/app/model-options.mjs +37 -5
  382. package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
  383. package/src/tui/app/model-picker.mjs +4 -2
  384. package/src/tui/app/skill-mention-palette.mjs +88 -0
  385. package/src/tui/app/slash-palette.mjs +23 -7
  386. package/src/tui/app/transcript-row-estimate.mjs +11 -3
  387. package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
  388. package/src/tui/app/use-prompt-state.mjs +5 -0
  389. package/src/tui/components/Message.jsx +22 -1
  390. package/src/tui/components/PromptInput.jsx +21 -3
  391. package/src/tui/components/SlashCommandPalette.jsx +12 -3
  392. package/src/tui/components/TranscriptItem.jsx +9 -1
  393. package/src/tui/components/prompt-input/key-handler.mjs +21 -8
  394. package/src/tui/components/user-message-skills.mjs +38 -0
  395. package/src/tui/dist/index.mjs +839 -207
  396. package/src/tui/index.jsx +10 -2
  397. package/src/tui/session/compaction-view.mjs +147 -0
  398. package/src/tui/session/context-state.mjs +6 -0
  399. package/src/tui/session/initial-state.mjs +3 -0
  400. package/src/tui/session/queue-helpers.mjs +7 -2
  401. package/src/tui/session/quota-resume.mjs +2 -1
  402. package/src/tui/session/session-api/commands.mjs +10 -2
  403. package/src/tui/session/session-api/controls.mjs +4 -0
  404. package/src/tui/session/session-api/extensions.mjs +4 -2
  405. package/src/tui/session/session-api/intake/rewind.mjs +2 -1
  406. package/src/tui/session/session-api/integrations.mjs +17 -0
  407. package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
  408. package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
  409. package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
  410. package/src/tui/session/session-api/routes.mjs +10 -1
  411. package/src/tui/session/session-api/settings.mjs +9 -0
  412. package/src/tui/session/session-api/worktree.mjs +60 -0
  413. package/src/tui/session/session-api.mjs +2 -0
  414. package/src/tui/session/session-flow/auto-clear.mjs +31 -12
  415. package/src/tui/session/session-flow/drain.mjs +27 -3
  416. package/src/tui/session/session-flow/session-reset.mjs +4 -3
  417. package/src/tui/session/session-flow.mjs +2 -0
  418. package/src/tui/session/transcript-intake.mjs +14 -5
  419. package/src/tui/session/tui-steering-persist.mjs +25 -9
  420. package/src/tui/session/turn-stream.mjs +32 -4
  421. package/src/tui/session/turn-tool-cards.mjs +10 -2
  422. package/src/tui/session/turn.mjs +56 -10
  423. package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
  424. package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
@@ -13,6 +13,7 @@ import { createHash } from 'node:crypto';
13
13
  import { writeJsonAtomicSync } from '../../../../shared/atomic-file.mjs';
14
14
  import { resolvePluginData } from '../../../../shared/plugin-paths.mjs';
15
15
  import { setBoundedTextCacheEntry } from './text-cache-budget.mjs';
16
+ import { canCacheReadFile } from '../../tools/builtin/read-cache-policy.mjs';
16
17
 
17
18
  // Entry cap: MIXDOG_PREFETCH_CACHE_MAX env var, default 200.
18
19
  const _envCap = Number(process.env.MIXDOG_PREFETCH_CACHE_MAX);
@@ -110,12 +111,14 @@ function rememberPrefetch(absPath, entry) {
110
111
  }
111
112
 
112
113
  export function capturePrefetchCacheState(absPath) {
113
- return _statTuple(_normalizeCacheKey(absPath));
114
+ const stat = _statTuple(_normalizeCacheKey(absPath));
115
+ return canCacheReadFile(stat) ? stat : null;
114
116
  }
115
117
 
116
118
  /**
117
119
  * Look up a cached prefetch result for `absPath`. Stat-validates the file on
118
- * every hit. Returns null on miss, TTL expiry, or stat mismatch. Returns
120
+ * every hit. Returns null on miss, TTL expiry, stat mismatch, or a file
121
+ * outside the prefix budget, including a persisted legacy row. Returns
119
122
  * `{ content, ts }` on a fresh hit.
120
123
  */
121
124
  export function tryPrefetchCached(absPath) {
@@ -135,7 +138,7 @@ export function tryPrefetchCached(absPath) {
135
138
  return null;
136
139
  }
137
140
  const fresh = _statTuple(absPath);
138
- if (!_statEqual(entry.stat, fresh)) {
141
+ if (!_statEqual(entry.stat, fresh) || !canCacheReadFile(fresh)) {
139
142
  _prefetchCache.delete(absPath);
140
143
  _deleteDiskEntry(absPath);
141
144
  return null;
@@ -152,7 +155,8 @@ export function tryPrefetchCached(absPath) {
152
155
 
153
156
  /**
154
157
  * Store a per-file prefetch result. Silently skips empty or error-prefixed
155
- * content so only clean read output enters the cache.
158
+ * content, and files outside the prefix budget, so only clean bounded read
159
+ * output enters the cache.
156
160
  * Async callers must pass the state captured before reading; null disables
157
161
  * insertion. Omission is reserved for synchronous current-body insertion.
158
162
  */
@@ -162,7 +166,7 @@ export function setPrefetchCached(absPath, content, readState) {
162
166
  if (classifyResultKind(content) === 'error') return;
163
167
  absPath = _normalizeCacheKey(absPath);
164
168
  const stat = _statTuple(absPath);
165
- if (!stat) return;
169
+ if (!stat || !canCacheReadFile(stat)) return;
166
170
  // Async readers supply the version observed before execution. Never label
167
171
  // an older body with the version that happens to exist at insertion time.
168
172
  if (readState !== undefined && !_statEqual(readState, stat)) return;
@@ -5,6 +5,7 @@ import { clearScopedToolsForSession } from './scoped-cache.mjs';
5
5
  import { registerSessionPurgeHook } from '../store.mjs';
6
6
  import { releaseReadSnapshotScope } from '../../tools/builtin/snapshot-store.mjs';
7
7
  import { setBoundedTextCacheEntry } from './text-cache-budget.mjs';
8
+ import { canCacheReadFile } from '../../tools/builtin/read-cache-policy.mjs';
8
9
 
9
10
  const MAX_PER_SESSION = 100;
10
11
 
@@ -114,6 +115,12 @@ function _arrayStatsValid(statsByAbs) {
114
115
  return true;
115
116
  }
116
117
 
118
+ // Equal stat tuples do not prove bytes past the hashed prefix. Files outside
119
+ // that budget must not be captured, reused, or inserted.
120
+ function _cacheableStats(statsByAbs) {
121
+ return !!statsByAbs && Object.values(statsByAbs).every((stat) => canCacheReadFile(stat));
122
+ }
123
+
117
124
  function _getOrCreate(sessionId) {
118
125
  let m = _bySession.get(sessionId);
119
126
  if (!m) {
@@ -157,20 +164,22 @@ export function captureReadCacheState({ args, cwd }) {
157
164
  const key = _keyFor(args, cwd);
158
165
  if (key === null && Array.isArray(args?.path ?? args?.file_path)) {
159
166
  const parsed = _arrayKeyAndStats(args, cwd);
160
- if (!parsed || Object.values(parsed.statsByAbs).some((stat) => !stat)) return null;
167
+ if (!parsed || !_cacheableStats(parsed.statsByAbs)) return null;
161
168
  return { ...parsed, kind: 'array' };
162
169
  }
163
170
  if (!key) return null;
164
171
  const abs = _absFromKey(key);
165
172
  const stat = _statTuple(abs);
166
- return stat ? { key, statsByAbs: { [abs]: stat }, kind: 'scalar' } : null;
173
+ if (!canCacheReadFile(stat)) return null;
174
+ return { key, statsByAbs: { [abs]: stat }, kind: 'scalar' };
167
175
  }
168
176
 
169
177
  /**
170
178
  * Look up a cached read result for this session+args. Stats the file at
171
179
  * lookup time and only returns when the stat tuple still matches. Returns
172
- * null on miss, on path-form mismatch, or on stat failure. On hit returns
173
- * the full entry { content, firstToolUseId, ts }.
180
+ * null on miss, on path-form mismatch, on stat failure, or when any file
181
+ * exceeds the prefix budget. On hit returns the full entry
182
+ * { content, firstToolUseId, ts }.
174
183
  */
175
184
  export function tryReadCached({ sessionId, args, cwd }) {
176
185
  if (!sessionId) return null;
@@ -182,7 +191,7 @@ export function tryReadCached({ sessionId, args, cwd }) {
182
191
  if (!map) return null;
183
192
  const entry = map.get(parsed.key);
184
193
  if (entry?.kind !== 'array') return null;
185
- if (!_arrayStatsValid(entry.statsByAbs)) {
194
+ if (!_arrayStatsValid(entry.statsByAbs) || !_cacheableStats(entry.statsByAbs)) {
186
195
  map.delete(parsed.key);
187
196
  _ridxPruneKey(sessionId, parsed.key);
188
197
  return null;
@@ -197,7 +206,7 @@ export function tryReadCached({ sessionId, args, cwd }) {
197
206
  const entry = map.get(key);
198
207
  if (!entry) return null;
199
208
  const fresh = _statTuple(_absFromKey(key));
200
- if (!_statEqual(entry.stat, fresh)) {
209
+ if (!_statEqual(entry.stat, fresh) || !canCacheReadFile(fresh)) {
201
210
  map.delete(key);
202
211
  _ridxPruneKey(sessionId, key);
203
212
  return null;
@@ -208,7 +217,8 @@ export function tryReadCached({ sessionId, args, cwd }) {
208
217
  }
209
218
 
210
219
  /**
211
- * Cache a successful read result. Skip caching if the file no longer exists.
220
+ * Cache a successful read result. Skip caching if a file is missing or
221
+ * outside the prefix budget.
212
222
  * `toolUseId` is the tool_use id of the FIRST call that populated the entry.
213
223
  * Async executors pass their pre-execution `readState`; null disables caching
214
224
  * when the source version could not be established. Omission retains direct
@@ -218,7 +228,7 @@ export function setReadCached({ sessionId, args, cwd, content, toolUseId, readSt
218
228
  if (!sessionId) return;
219
229
  if (typeof content !== 'string' || content.length === 0) return;
220
230
  const parsed = captureReadCacheState({ args, cwd });
221
- if (!parsed) return;
231
+ if (!parsed || !_cacheableStats(parsed.statsByAbs)) return;
222
232
  if (
223
233
  readState !== undefined &&
224
234
  (!readState ||
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Session catalog DB, phase 2: in the daemon, SQLite is the read source of
3
+ * the ordinary session catalog.
4
+ *
5
+ * Everything runs on one worker thread (./worker.mjs): the catalog engine's
6
+ * change detection (summary index, sessions directory, per-file stamps, `.hb`
7
+ * liveness, background verification) feeds `<dataDir>/session-catalog.sqlite`
8
+ * and an ordinary listing is one request that answers from it. This thread
9
+ * does no filesystem work for it and never loads node:sqlite.
10
+ *
11
+ * JSON stays what every other process reads and writes: the store keeps
12
+ * writing the summary index exactly as before. When one of the daemon's own
13
+ * writes has changed a session file on disk, the store hints the id here
14
+ * (hintSessionCatalogStorage); the worker re-checks it from storage on the
15
+ * next listing. Hints carry no row data. Whenever the DB cannot answer — not
16
+ * the daemon, not Electron, SQLite gate, another process holds the DB,
17
+ * off-switch, backfill or rebuild in progress, any error — `list()` resolves
18
+ * null and the caller uses the JSON path unchanged.
19
+ */
20
+ import { isMainThread } from 'node:worker_threads';
21
+ import { createWorkerRequestClient } from '../../../../shared/worker-requests.mjs';
22
+ import { dataDir as currentDataDir } from '../store-summary-locations.mjs';
23
+
24
+ export const SESSION_CATALOG_DB_FILE = 'session-catalog.sqlite';
25
+ /** Config: `{ "sessionStore": { "sqliteCatalog": false } }` forces the JSON
26
+ * path (an off-switch); absent or true uses the DB whenever it can. */
27
+ export const SESSION_STORE_CONFIG_SECTION = 'sessionStore';
28
+ export const SESSION_STORE_CONFIG_DEFAULTS = Object.freeze({ sqliteCatalog: true });
29
+ /** Stable prefix of every daemon.log line of the catalog DB. */
30
+ export const SESSION_CATALOG_LOG_PREFIX = '[session-catalog-db]';
31
+
32
+ // A worker that keeps dying is given up on for a while.
33
+ const CRASH_WINDOW_MS = 10 * 60_000;
34
+ const MAX_CRASHES = 3;
35
+ const ERROR_LOG_INTERVAL_MS = 60_000;
36
+
37
+ let testOptions = null;
38
+ let requestWorker = null;
39
+ let controller = null;
40
+ const changeListeners = new Set();
41
+ const noop = () => {};
42
+
43
+ function log(line) {
44
+ const text = `${SESSION_CATALOG_LOG_PREFIX} ${line}`;
45
+ if (typeof testOptions?.log === 'function') {
46
+ testOptions.log(text);
47
+ return;
48
+ }
49
+ // The daemon's stderr is daemon.log (mirrored by the spawner before ready,
50
+ // redirected into the file after).
51
+ try {
52
+ process.stderr.write(`${text}\n`);
53
+ } catch {
54
+ /* logging is best effort */
55
+ }
56
+ }
57
+
58
+ function onWorkerEvent(event) {
59
+ if (event?.type === 'log') log(event.line);
60
+ else if (event?.type === 'changed') {
61
+ for (const listener of changeListeners) {
62
+ try {
63
+ listener();
64
+ } catch {
65
+ /* a listener's failure is its own */
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ function request(payload) {
72
+ requestWorker ||= createWorkerRequestClient(new URL('./worker.mjs', import.meta.url), {
73
+ // The worker is the only node:sqlite user; its experimental warning is noise.
74
+ execArgv: ['--disable-warning=ExperimentalWarning'],
75
+ onEvent: onWorkerEvent,
76
+ });
77
+ return requestWorker(payload);
78
+ }
79
+
80
+ /** Worker settings a test configured (functions stay on this thread). */
81
+ function workerSettings() {
82
+ if (!testOptions) return undefined;
83
+ const settings = {};
84
+ for (const [key, value] of Object.entries(testOptions)) {
85
+ if (typeof value !== 'function' && key !== 'writer') settings[key] = value;
86
+ }
87
+ return settings;
88
+ }
89
+
90
+ /**
91
+ * True only in the daemon process itself, under the packaged or dev Electron
92
+ * runtime: daemon.mjs sets MIXDOG_DAEMON_HOST and MIXDOG_SERVER_PID (its own
93
+ * pid) at startup; children inherit the environment but have another pid,
94
+ * worker threads are not the main thread, and a daemon started by the CLI
95
+ * under system Node stays on JSON.
96
+ */
97
+ export function isSessionCatalogDbWriter() {
98
+ if (testOptions && 'writer' in testOptions) return testOptions.writer === true;
99
+ return (
100
+ isMainThread &&
101
+ Boolean(process.versions.electron) &&
102
+ process.env.MIXDOG_DAEMON_HOST === '1' &&
103
+ process.env.MIXDOG_SERVER_PID === String(process.pid)
104
+ );
105
+ }
106
+
107
+ /** The catalog DB of `dataDir`, or null when this process does not keep one. */
108
+ export function sessionCatalogDb(dataDir) {
109
+ if (!isSessionCatalogDbWriter()) return null;
110
+ if (controller?.dataDir === dataDir) return controller;
111
+ void controller?.close();
112
+ controller = new SessionCatalogDb(dataDir);
113
+ return controller;
114
+ }
115
+
116
+ /** The existing catalog DB of `dataDir` (never creates one). */
117
+ export function currentSessionCatalogDb(dataDir) {
118
+ return controller?.dataDir === dataDir ? controller : null;
119
+ }
120
+
121
+ /** `listener()` runs when the worker's background pass finds a changed
122
+ * session file (an ordinary listing would now differ). */
123
+ export function onSessionCatalogDbChange(listener) {
124
+ changeListeners.add(listener);
125
+ return () => changeListeners.delete(listener);
126
+ }
127
+
128
+ /**
129
+ * Storage hint from the store (store/summary-cache.mjs): a write of this
130
+ * process changed the files of `ids` — a save landed, a delete removed the
131
+ * file. Tagged with the current data dir; the worker re-checks those ids from
132
+ * storage on the next listing. A lost hint only delays the change to the
133
+ * engine's own detection.
134
+ */
135
+ export function hintSessionCatalogStorage(ids) {
136
+ currentSessionCatalogDb(currentDataDir())?.hint(ids);
137
+ }
138
+
139
+ /** Closes the catalog DB (releasing its file, WAL and lock) — the teardown
140
+ * boundary before a data dir is removed or switched. */
141
+ export async function closeSessionCatalogDb() {
142
+ const current = controller;
143
+ controller = null;
144
+ if (current) await current.close();
145
+ }
146
+
147
+ class SessionCatalogDb {
148
+ constructor(dataDir) {
149
+ this.dataDir = dataDir;
150
+ this.state = 'active'; // active | off (final: gate) | failed (until failedUntil) | closed
151
+ this.failedUntil = 0;
152
+ this.hints = new Set();
153
+ this.flushHandle = null;
154
+ this.started = false;
155
+ this.crashes = [];
156
+ this.errorLoggedAt = 0;
157
+ }
158
+
159
+ send(op, payload = {}) {
160
+ // No running worker: this request starts one. After the first start it
161
+ // is a respawn (the last one died): it reopens and backfills by itself.
162
+ if (!requestWorker?.running()) {
163
+ if (this.started) this.noteRespawn();
164
+ this.started = true;
165
+ }
166
+ return request({ op, dataDir: this.dataDir, settings: workerSettings(), ...payload }).catch((error) => {
167
+ this.noteError(error);
168
+ throw error;
169
+ });
170
+ }
171
+
172
+ /** A worker died; more than MAX_CRASHES deaths in the window give up. */
173
+ noteRespawn() {
174
+ const now = Date.now();
175
+ this.crashes = this.crashes.filter((at) => now - at < CRASH_WINDOW_MS);
176
+ this.crashes.push(now);
177
+ if (this.crashes.length > MAX_CRASHES && this.state === 'active') {
178
+ this.state = 'failed';
179
+ this.failedUntil = now + CRASH_WINDOW_MS;
180
+ log(`worker failed ${this.crashes.length} times in 10 min; catalog stays JSON`);
181
+ }
182
+ }
183
+
184
+ noteError(error) {
185
+ const now = Date.now();
186
+ if (now - this.errorLoggedAt >= ERROR_LOG_INTERVAL_MS) {
187
+ this.errorLoggedAt = now;
188
+ log(`request failed: ${error?.message || error}; listing answered from JSON`);
189
+ }
190
+ }
191
+
192
+ usable() {
193
+ if (this.state === 'failed' && Date.now() >= this.failedUntil) {
194
+ this.state = 'active';
195
+ this.crashes = [];
196
+ }
197
+ return this.state === 'active';
198
+ }
199
+
200
+ hint(ids) {
201
+ if (this.state !== 'active') return;
202
+ for (const id of ids) if (id) this.hints.add(String(id));
203
+ if (this.hints.size === 0) return;
204
+ this.flushHandle ||= setImmediate(() => {
205
+ this.flushHandle = null;
206
+ this.flushHints();
207
+ });
208
+ }
209
+
210
+ /** Posts the collected hints ahead of any later request. A worker that is
211
+ * not running gets none: a (re)started worker rescans storage anyway. */
212
+ flushHints() {
213
+ if (this.hints.size === 0) return;
214
+ const ids = [...this.hints];
215
+ this.hints.clear();
216
+ if (!requestWorker?.running()) return;
217
+ this.send('hint', { ids }).catch(noop);
218
+ }
219
+
220
+ /**
221
+ * One ordinary listing (`page` = `{ limit, after }` or null for all):
222
+ * `{ rows, more }`, or null when the DB cannot answer (use JSON). Hints
223
+ * collected before this call are posted first.
224
+ */
225
+ async list(page = null) {
226
+ if (!this.usable()) return null;
227
+ this.flushHints();
228
+ let answer;
229
+ try {
230
+ answer = await this.send('list', { page });
231
+ } catch {
232
+ return null;
233
+ }
234
+ if (answer.status === 'ok') return { rows: answer.rows, more: answer.more };
235
+ if (answer.final && this.state === 'active') this.state = 'off';
236
+ return null;
237
+ }
238
+
239
+ verifyFiles() {
240
+ if (!this.usable()) return Promise.resolve();
241
+ return this.send('verifyFiles').catch(noop);
242
+ }
243
+
244
+ /** Test/benchmark seams. */
245
+ status() {
246
+ return this.send('status');
247
+ }
248
+ shadow() {
249
+ return this.send('shadow');
250
+ }
251
+ settled() {
252
+ this.flushHints();
253
+ return this.send('settle');
254
+ }
255
+ testExec(sql, params = []) {
256
+ return this.send('testExec', { sql, params });
257
+ }
258
+
259
+ async close() {
260
+ if (this.state === 'closed') return;
261
+ this.state = 'closed';
262
+ if (this.flushHandle) clearImmediate(this.flushHandle);
263
+ this.flushHandle = null;
264
+ this.hints.clear();
265
+ // Only a running worker can hold the DB open.
266
+ if (requestWorker?.running()) {
267
+ await request({ op: 'close', dataDir: this.dataDir }).catch(noop);
268
+ }
269
+ }
270
+ }
271
+
272
+ // ── Test seams ──────────────────────────────────────────────────────────────
273
+ /** Test seam: `{ writer, enabled, sqliteVersion, backfillBatch,
274
+ * shadowIntervalMs, retryMs, fenceTimeoutMs, crashAfterBatches, log }`;
275
+ * null restores defaults. */
276
+ export function configureSessionCatalogDbForTest(options) {
277
+ testOptions = options ? { ...options } : null;
278
+ }
279
+
280
+ /** Test seam: post a storage hint tagged with `dataDir` (another data dir's
281
+ * hint must be ignored by the worker). */
282
+ export function postSessionCatalogHintForTest(dataDir, ids) {
283
+ return request({ op: 'hint', dataDir, ids });
284
+ }
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Session catalog DB: table layout, row codec and the listing comparison.
3
+ * Pure (no IO, no node:sqlite) so both the worker and tests can use it.
4
+ *
5
+ * One row per catalog session: every field store-summary-reader.mjs
6
+ * normalizedRow produces, the storage fingerprint (full stamp of the session
7
+ * file the row was judged at), the title normalized at write, and the
8
+ * bookkeeping the mirror needs (lead visibility, sync epoch, row signature).
9
+ */
10
+ import { createHash } from 'node:crypto';
11
+
12
+ /** Bumped whenever the table layout or a codec changes: the DB is then
13
+ * dropped and backfilled again from the JSON catalog. */
14
+ export const SCHEMA_VERSION = 3;
15
+ /** PRAGMA application_id ('MXSC'): a file without it is not this catalog. */
16
+ export const APPLICATION_ID = 0x4d585343;
17
+
18
+ // [row key, column, codec]
19
+ // real/int numbers; bool 0/1; text strings
20
+ // loose whatever the source carried (string/number/null as is, anything
21
+ // else as a JSON BLOB), so the row round-trips exactly
22
+ // json object or null, as JSON text
23
+ // optjson like json, but the key is absent from the row when NULL
24
+ const FIELDS = [
25
+ ['id', 'id', 'text'],
26
+ ['updatedAt', 'updated_at', 'real'],
27
+ ['lastUsedAt', 'last_used_at', 'real'],
28
+ ['createdAt', 'created_at', 'real'],
29
+ ['lastHeartbeatAt', 'last_heartbeat_at', 'real'],
30
+ // Always 0 in a stored row: liveness is overlaid from `.hb` sidecars at read.
31
+ ['heartbeatAt', 'heartbeat_at', 'real'],
32
+ ['closed', 'closed', 'bool'],
33
+ ['status', 'status', 'text'],
34
+ ['owner', 'owner', 'loose'],
35
+ ['agent', 'agent', 'loose'],
36
+ ['sourceType', 'source_type', 'loose'],
37
+ ['sourceName', 'source_name', 'loose'],
38
+ ['scopeKey', 'scope_key', 'loose'],
39
+ ['ownerSessionId', 'owner_session_id', 'loose'],
40
+ ['visibility', 'visibility', 'loose'],
41
+ ['clientHostPid', 'client_host_pid', 'loose'],
42
+ ['cwd', 'cwd', 'loose'],
43
+ ['desktopSession', 'desktop_session', 'json'],
44
+ ['workspace', 'workspace', 'optjson'],
45
+ ['provider', 'provider', 'loose'],
46
+ ['model', 'model', 'loose'],
47
+ ['agentTag', 'agent_tag', 'loose'],
48
+ ['task_id', 'task_id', 'loose'],
49
+ ['permission', 'permission', 'loose'],
50
+ ['toolPermission', 'tool_permission', 'loose'],
51
+ ['messageCount', 'message_count', 'int'],
52
+ ['title', 'title', 'text'],
53
+ ['preview', 'preview', 'text'],
54
+ ['generation', 'generation', 'loose'],
55
+ ['storageMtimeMs', 'storage_mtime_ms', 'real'],
56
+ ['storageSize', 'storage_size', 'real'],
57
+ ['detachedReason', 'detached_reason', 'loose'],
58
+ // importOrigin.key of an imported conversation (schema 3).
59
+ ['importKey', 'import_key', 'loose'],
60
+ ];
61
+
62
+ const COLUMN_TYPES = { text: 'TEXT', real: 'REAL', int: 'INTEGER', bool: 'INTEGER', json: 'TEXT', optjson: 'TEXT' };
63
+
64
+ const columnDefinition = ([, column, codec]) => {
65
+ if (column === 'id') return 'id TEXT PRIMARY KEY';
66
+ // No declared type = no affinity: loose values are stored exactly as bound.
67
+ return codec === 'loose' ? column : `${column} ${COLUMN_TYPES[codec]}`;
68
+ };
69
+
70
+ export const SCHEMA_SQL = `
71
+ CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
72
+ CREATE TABLE IF NOT EXISTS sessions (
73
+ ${FIELDS.map(columnDefinition).join(',\n ')},
74
+ activity REAL NOT NULL,
75
+ title_norm TEXT NOT NULL,
76
+ fingerprint TEXT,
77
+ lead_visible INTEGER NOT NULL,
78
+ row_sig TEXT NOT NULL
79
+ );
80
+ CREATE INDEX IF NOT EXISTS sessions_recent ON sessions(activity DESC, id);
81
+ CREATE INDEX IF NOT EXISTS sessions_lead_recent ON sessions(lead_visible, activity DESC, id);
82
+ CREATE INDEX IF NOT EXISTS sessions_visibility_recent ON sessions(visibility, activity DESC, id);
83
+ CREATE INDEX IF NOT EXISTS sessions_owner_session ON sessions(owner_session_id);
84
+ `;
85
+
86
+ const WRITE_COLUMNS = [
87
+ ...FIELDS.map(([, column]) => column),
88
+ 'activity',
89
+ 'title_norm',
90
+ 'fingerprint',
91
+ 'lead_visible',
92
+ 'row_sig',
93
+ ];
94
+
95
+ export const UPSERT_SQL = `INSERT INTO sessions(${WRITE_COLUMNS.join(',')})
96
+ VALUES (${WRITE_COLUMNS.map(() => '?').join(',')})
97
+ ON CONFLICT(id) DO UPDATE SET ${WRITE_COLUMNS.slice(1)
98
+ .map((column) => `${column}=excluded.${column}`)
99
+ .join(',')}`;
100
+
101
+ /** Recent-activity key of the JSON listing (store-summary-reader byRecentActivity). */
102
+ export function activityOf(row) {
103
+ return row.lastUsedAt || row.updatedAt || 0;
104
+ }
105
+
106
+ /** Newest activity first, ties by id: the DB order and the keyset order.
107
+ * Ids are ASCII ([A-Za-z0-9_-]), so code-unit order equals SQLite BINARY. */
108
+ export function byActivityThenId(left, right) {
109
+ const delta = activityOf(right) - activityOf(left);
110
+ if (delta !== 0) return delta;
111
+ if (left.id === right.id) return 0;
112
+ return left.id < right.id ? -1 : 1;
113
+ }
114
+
115
+ /** Title normalized once at write: what a title lookup or sort compares. */
116
+ export function normalizedTitle(row) {
117
+ return String(row.title || row.preview || '')
118
+ .normalize('NFKC')
119
+ .toLowerCase()
120
+ .replace(/\s+/g, ' ')
121
+ .trim();
122
+ }
123
+
124
+ function encodeLoose(value) {
125
+ if (value === null || value === undefined) return null;
126
+ if (typeof value === 'string' || typeof value === 'number') return value;
127
+ return Buffer.from(JSON.stringify(value), 'utf8');
128
+ }
129
+
130
+ function decodeLoose(value) {
131
+ if (value instanceof Uint8Array) return JSON.parse(Buffer.from(value).toString('utf8'));
132
+ return value;
133
+ }
134
+
135
+ function encodeField(codec, value) {
136
+ switch (codec) {
137
+ case 'bool':
138
+ return value === true ? 1 : 0;
139
+ case 'json':
140
+ case 'optjson':
141
+ return value == null ? null : JSON.stringify(value);
142
+ case 'loose':
143
+ return encodeLoose(value);
144
+ default:
145
+ return value;
146
+ }
147
+ }
148
+
149
+ function decodeField(codec, value) {
150
+ switch (codec) {
151
+ case 'bool':
152
+ return value === 1;
153
+ case 'json':
154
+ case 'optjson':
155
+ return value == null ? null : JSON.parse(value);
156
+ case 'loose':
157
+ return decodeLoose(value);
158
+ default:
159
+ return value;
160
+ }
161
+ }
162
+
163
+ /** Signature of what one mirror entry writes: equal signature, no write. */
164
+ export function rowSignature({ row, fingerprint, leadVisible }) {
165
+ return createHash('sha1')
166
+ .update(JSON.stringify([fingerprint ?? null, leadVisible === true, row]))
167
+ .digest('base64');
168
+ }
169
+
170
+ /** Bind parameters of UPSERT_SQL for one mirror entry. */
171
+ export function upsertParams(entry, signature) {
172
+ const { row } = entry;
173
+ return [
174
+ ...FIELDS.map(([key, , codec]) => encodeField(codec, row[key])),
175
+ activityOf(row),
176
+ normalizedTitle(row),
177
+ entry.fingerprint ?? null,
178
+ entry.leadVisible === true ? 1 : 0,
179
+ signature,
180
+ ];
181
+ }
182
+
183
+ /** A `sessions` row back in normalizedRow shape. */
184
+ export function rowFromDb(record) {
185
+ const row = {};
186
+ for (const [key, column, codec] of FIELDS) {
187
+ const value = decodeField(codec, record[column]);
188
+ if (codec === 'optjson' && value === null) continue;
189
+ row[key] = value;
190
+ }
191
+ return row;
192
+ }
193
+
194
+ // Overlays a listing adds at read time; never part of the comparison.
195
+ const OVERLAY_KEYS = new Set(['heartbeatAt', 'agentHeartbeatAt']);
196
+
197
+ /** Comparable JSON of a row's stored fields (overlays excluded). */
198
+ function comparable(row) {
199
+ const out = [];
200
+ for (const [key] of FIELDS) {
201
+ if (OVERLAY_KEYS.has(key)) continue;
202
+ out.push(row[key] === undefined ? null : row[key]);
203
+ }
204
+ return JSON.stringify(out);
205
+ }
206
+
207
+ const FIRST_IDS = 5;
208
+
209
+ /**
210
+ * Shadow comparison of the DB listing against the JSON listing, both in the
211
+ * one catalog order (byActivityThenId) and compared exactly as given: ids
212
+ * present in only one, positions of the common ids, and stored row fields.
213
+ */
214
+ export function compareListings(json, dbRows) {
215
+ const jsonById = new Map(json.map((row) => [row.id, row]));
216
+ const dbById = new Map(dbRows.map((row) => [row.id, row]));
217
+ const missing = json.filter((row) => !dbById.has(row.id)).map((row) => row.id);
218
+ const extra = dbRows.filter((row) => !jsonById.has(row.id)).map((row) => row.id);
219
+ const commonJson = json.filter((row) => dbById.has(row.id));
220
+ const commonDb = dbRows.filter((row) => jsonById.has(row.id));
221
+ const order = [];
222
+ for (let index = 0; index < commonJson.length; index++) {
223
+ if (commonJson[index].id !== commonDb[index].id) order.push(commonJson[index].id);
224
+ }
225
+ const fields = commonJson.filter((row) => comparable(row) !== comparable(dbById.get(row.id))).map((row) => row.id);
226
+ return {
227
+ json: json.length,
228
+ db: dbRows.length,
229
+ missing: missing.length,
230
+ extra: extra.length,
231
+ order: order.length,
232
+ fields: fields.length,
233
+ ids: {
234
+ missing: missing.slice(0, FIRST_IDS),
235
+ extra: extra.slice(0, FIRST_IDS),
236
+ order: order.slice(0, FIRST_IDS),
237
+ fields: fields.slice(0, FIRST_IDS),
238
+ },
239
+ };
240
+ }
241
+
242
+ // ── Keyset cursor (activity time, id) ──────────────────────────────────────
243
+ /** Opaque cursor naming the last row of a page. */
244
+ export function encodeCatalogCursor(row) {
245
+ return Buffer.from(JSON.stringify([activityOf(row), row.id]), 'utf8').toString('base64url');
246
+ }
247
+
248
+ /** `{ activity, id }` of a cursor, null for none; throws on a malformed one. */
249
+ export function decodeCatalogCursor(cursor) {
250
+ if (cursor === undefined || cursor === null || cursor === '') return null;
251
+ let parsed;
252
+ try {
253
+ parsed = JSON.parse(Buffer.from(String(cursor), 'base64url').toString('utf8'));
254
+ } catch {
255
+ parsed = null;
256
+ }
257
+ if (!Array.isArray(parsed) || !Number.isFinite(parsed[0]) || typeof parsed[1] !== 'string') {
258
+ throw Object.assign(new Error('invalid session catalog cursor'), { code: 'EINVALIDCURSOR' });
259
+ }
260
+ return { activity: parsed[0], id: parsed[1] };
261
+ }
262
+
263
+ /** Whether `row` sorts strictly after the cursor position. */
264
+ export function isAfterCursor(row, after) {
265
+ if (!after) return true;
266
+ const activity = activityOf(row);
267
+ return activity < after.activity || (activity === after.activity && row.id > after.id);
268
+ }