mixdog 0.9.2 → 0.9.4

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 (417) hide show
  1. package/package.json +8 -3
  2. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  3. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  4. package/scripts/bench/lead-review-tasks.json +20 -0
  5. package/scripts/bench/r4-mixed-tasks.json +20 -0
  6. package/scripts/bench/review-tasks.json +20 -0
  7. package/scripts/bench/round-codex.json +114 -0
  8. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  9. package/scripts/bench/round-mixdog-lead.json +269 -0
  10. package/scripts/bench/round-mixdog.json +126 -0
  11. package/scripts/bench/round-r10-bigsample.json +679 -0
  12. package/scripts/bench/round-r11-codexalign.json +257 -0
  13. package/scripts/bench/round-r4-codex.json +114 -0
  14. package/scripts/bench/round-r4-mixed.json +225 -0
  15. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  16. package/scripts/bench/round-r6-codex.json +114 -0
  17. package/scripts/bench/round-r6-solo.json +257 -0
  18. package/scripts/bench/round-r7-full.json +254 -0
  19. package/scripts/bench/round-r8-fulldefault.json +255 -0
  20. package/scripts/bench-run.mjs +215 -29
  21. package/scripts/build-tui.mjs +13 -1
  22. package/scripts/explore-bench.mjs +124 -0
  23. package/scripts/freevar-smoke.mjs +95 -0
  24. package/scripts/hook-bus-test.mjs +191 -0
  25. package/scripts/internal-comms-bench.mjs +1 -0
  26. package/scripts/internal-comms-smoke.mjs +10 -9
  27. package/scripts/mouse-probe.mjs +45 -0
  28. package/scripts/output-style-bench.mjs +13 -6
  29. package/scripts/output-style-smoke.mjs +4 -4
  30. package/scripts/path-suffix-test.mjs +57 -0
  31. package/scripts/provider-toolcall-test.mjs +7 -3
  32. package/scripts/recall-bench.mjs +207 -0
  33. package/scripts/recall-usecase-cases.json +18 -0
  34. package/scripts/recall-usecase-probe.json +6 -0
  35. package/scripts/session-bench.mjs +152 -6
  36. package/scripts/tool-smoke.mjs +30 -67
  37. package/scripts/tui-render-smoke.mjs +90 -0
  38. package/scripts/webhook-smoke.mjs +208 -0
  39. package/src/agents/debugger/AGENT.md +5 -2
  40. package/src/agents/heavy-worker/AGENT.md +21 -11
  41. package/src/agents/maintainer/AGENT.md +4 -0
  42. package/src/agents/reviewer/AGENT.md +3 -2
  43. package/src/agents/worker/AGENT.md +21 -11
  44. package/src/lib/rules-builder.cjs +4 -0
  45. package/src/mixdog-session-runtime.mjs +933 -3731
  46. package/src/output-styles/default.md +34 -9
  47. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  48. package/src/output-styles/minimal.md +4 -1
  49. package/src/output-styles/simple.md +22 -7
  50. package/src/repl.mjs +5 -5
  51. package/src/rules/agent/00-common.md +2 -0
  52. package/src/rules/agent/30-explorer.md +8 -11
  53. package/src/rules/lead/lead-brief.md +12 -0
  54. package/src/rules/lead/lead-tool.md +2 -9
  55. package/src/rules/shared/01-tool.md +11 -5
  56. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  57. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  58. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  59. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  60. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  61. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  62. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  63. package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
  64. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  65. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +63 -21
  66. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  67. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  68. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  69. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +97 -1343
  70. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  71. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +78 -10
  72. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
  73. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  74. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  75. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  76. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  77. package/src/runtime/agent/orchestrator/providers/gemini.mjs +44 -1014
  78. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +18 -4
  79. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  80. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
  81. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
  82. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  83. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  84. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
  85. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  86. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  87. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  88. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +41 -1142
  89. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
  90. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  91. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +303 -2119
  92. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +140 -995
  93. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
  94. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  95. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
  96. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  97. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
  99. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
  100. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  101. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  102. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  103. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  104. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  105. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  106. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  107. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  108. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  109. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +274 -0
  110. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  111. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  112. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  113. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  114. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +47 -0
  115. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
  116. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  117. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  118. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  119. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  120. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  121. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  122. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  123. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  124. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  125. package/src/runtime/agent/orchestrator/session/loop.mjs +409 -1304
  126. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  127. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +230 -0
  128. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  129. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +149 -0
  130. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  131. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  132. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  133. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  134. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  135. package/src/runtime/agent/orchestrator/session/manager.mjs +226 -2114
  136. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  137. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  138. package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
  139. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  140. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  141. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +33 -42
  142. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  143. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  144. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  145. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  146. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
  147. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  148. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  149. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -87
  152. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +78 -304
  155. package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -6
  156. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  157. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  158. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  159. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +551 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  161. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  163. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  164. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  165. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  166. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1080 -0
  167. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  168. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  169. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  170. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  171. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  172. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  173. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
  174. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  175. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  176. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  177. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  178. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  179. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  180. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  181. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  182. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  183. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  184. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -23
  185. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +10 -74
  186. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  187. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  188. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  189. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  190. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  191. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  192. package/src/runtime/channels/backends/discord.mjs +12 -292
  193. package/src/runtime/channels/index.mjs +241 -894
  194. package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
  195. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  196. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  197. package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
  198. package/src/runtime/channels/lib/event-queue.mjs +63 -4
  199. package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
  200. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  201. package/src/runtime/channels/lib/output-forwarder.mjs +9 -1
  202. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  203. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  204. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  205. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  206. package/src/runtime/channels/lib/telegram-format.mjs +19 -22
  207. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  208. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  209. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  210. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  211. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  212. package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
  213. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  214. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  215. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  216. package/src/runtime/channels/lib/webhook.mjs +36 -570
  217. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  218. package/src/runtime/channels/tool-defs.mjs +11 -130
  219. package/src/runtime/memory/index.mjs +258 -2050
  220. package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
  221. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  222. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  223. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  224. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  225. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  226. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  227. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  228. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  229. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  230. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  231. package/src/runtime/memory/lib/memory-cycle2.mjs +72 -837
  232. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  233. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  234. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  235. package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
  236. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  237. package/src/runtime/memory/lib/memory.mjs +20 -0
  238. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  239. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  240. package/src/runtime/memory/lib/query-handlers.mjs +780 -0
  241. package/src/runtime/memory/lib/recall-format.mjs +238 -0
  242. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  243. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  244. package/src/runtime/memory/tool-defs.mjs +6 -14
  245. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  246. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  247. package/src/runtime/search/lib/web-tools.mjs +24 -602
  248. package/src/runtime/shared/abort-controller.mjs +1 -1
  249. package/src/runtime/shared/atomic-file.mjs +26 -1
  250. package/src/runtime/shared/background-tasks.mjs +2 -3
  251. package/src/runtime/shared/buffered-appender.mjs +149 -0
  252. package/src/runtime/shared/launcher-control.mjs +2 -2
  253. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  254. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  255. package/src/runtime/shared/tool-primitives.mjs +308 -0
  256. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  257. package/src/runtime/shared/tool-surface.mjs +80 -898
  258. package/src/runtime/shared/transcript-writer.mjs +52 -2
  259. package/src/runtime/shared/update-checker.mjs +7 -4
  260. package/src/session-runtime/config-helpers.mjs +291 -0
  261. package/src/session-runtime/config-lifecycle.mjs +232 -0
  262. package/src/session-runtime/cwd-plugins.mjs +226 -0
  263. package/src/session-runtime/effort.mjs +128 -0
  264. package/src/session-runtime/fs-utils.mjs +10 -0
  265. package/src/session-runtime/mcp-glue.mjs +177 -0
  266. package/src/session-runtime/model-capabilities.mjs +130 -0
  267. package/src/session-runtime/model-recency.mjs +111 -0
  268. package/src/session-runtime/native-search.mjs +247 -0
  269. package/src/session-runtime/output-styles.mjs +126 -0
  270. package/src/session-runtime/plugin-mcp.mjs +114 -0
  271. package/src/session-runtime/prewarm.mjs +142 -0
  272. package/src/session-runtime/provider-models.mjs +278 -0
  273. package/src/session-runtime/provider-usage.mjs +120 -0
  274. package/src/session-runtime/quick-model-rows.mjs +170 -0
  275. package/src/session-runtime/quick-search-models.mjs +46 -0
  276. package/src/session-runtime/session-hooks.mjs +93 -0
  277. package/src/session-runtime/session-text.mjs +100 -0
  278. package/src/session-runtime/settings-api.mjs +319 -0
  279. package/src/session-runtime/statusline-route.mjs +35 -0
  280. package/src/session-runtime/tool-catalog.mjs +720 -0
  281. package/src/session-runtime/tool-defs.mjs +84 -0
  282. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  283. package/src/session-runtime/workflow.mjs +358 -0
  284. package/src/standalone/agent-tool/helpers.mjs +237 -0
  285. package/src/standalone/agent-tool/notify.mjs +107 -0
  286. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  287. package/src/standalone/agent-tool/render.mjs +152 -0
  288. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  289. package/src/standalone/agent-tool.mjs +155 -677
  290. package/src/standalone/channel-worker.mjs +7 -9
  291. package/src/standalone/explore-tool.mjs +40 -12
  292. package/src/standalone/hook-bus/config.mjs +207 -0
  293. package/src/standalone/hook-bus/constants.mjs +90 -0
  294. package/src/standalone/hook-bus/handlers.mjs +481 -0
  295. package/src/standalone/hook-bus/payload.mjs +31 -0
  296. package/src/standalone/hook-bus/rules.mjs +77 -0
  297. package/src/standalone/hook-bus.mjs +110 -746
  298. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  299. package/src/standalone/opencode-go-login.mjs +125 -0
  300. package/src/standalone/provider-admin.mjs +15 -19
  301. package/src/standalone/usage-dashboard.mjs +3 -1
  302. package/src/tui/App.jsx +1163 -7571
  303. package/src/tui/app/app-format.mjs +206 -0
  304. package/src/tui/app/channel-pickers.mjs +510 -0
  305. package/src/tui/app/clipboard.mjs +67 -0
  306. package/src/tui/app/core-memory-picker.mjs +210 -0
  307. package/src/tui/app/extension-pickers.mjs +506 -0
  308. package/src/tui/app/input-parsers.mjs +193 -0
  309. package/src/tui/app/maintenance-pickers.mjs +324 -0
  310. package/src/tui/app/model-options.mjs +330 -0
  311. package/src/tui/app/model-picker.mjs +365 -0
  312. package/src/tui/app/onboarding-steps.mjs +400 -0
  313. package/src/tui/app/project-picker.mjs +247 -0
  314. package/src/tui/app/provider-setup-picker.mjs +580 -0
  315. package/src/tui/app/resume-picker.mjs +55 -0
  316. package/src/tui/app/route-pickers.mjs +419 -0
  317. package/src/tui/app/settings-picker.mjs +490 -0
  318. package/src/tui/app/slash-commands.mjs +101 -0
  319. package/src/tui/app/slash-dispatch.mjs +427 -0
  320. package/src/tui/app/text-layout.mjs +46 -0
  321. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  322. package/src/tui/app/transcript-window.mjs +671 -0
  323. package/src/tui/app/use-mouse-input.mjs +460 -0
  324. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  325. package/src/tui/app/use-transcript-scroll.mjs +510 -0
  326. package/src/tui/app/use-transcript-window.mjs +589 -0
  327. package/src/tui/components/ConfirmBar.jsx +1 -1
  328. package/src/tui/components/Picker.jsx +32 -4
  329. package/src/tui/components/PromptInput.jsx +259 -80
  330. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  331. package/src/tui/components/StatusLine.jsx +63 -12
  332. package/src/tui/components/TextEntryPanel.jsx +11 -0
  333. package/src/tui/components/ToolExecution.jsx +56 -588
  334. package/src/tui/components/TranscriptItem.jsx +105 -0
  335. package/src/tui/components/UsagePanel.jsx +18 -4
  336. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  337. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  338. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  339. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  340. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  341. package/src/tui/components/tool-output-format.mjs +2 -2
  342. package/src/tui/display-width.mjs +20 -3
  343. package/src/tui/dist/index.mjs +18034 -17188
  344. package/src/tui/engine/agent-envelope.mjs +296 -0
  345. package/src/tui/engine/agent-job-feed.mjs +133 -0
  346. package/src/tui/engine/boot-profile.mjs +21 -0
  347. package/src/tui/engine/labels.mjs +67 -0
  348. package/src/tui/engine/notice-text.mjs +112 -0
  349. package/src/tui/engine/notification-plan.mjs +76 -0
  350. package/src/tui/engine/queue-helpers.mjs +161 -0
  351. package/src/tui/engine/render-timing.mjs +17 -0
  352. package/src/tui/engine/session-stats.mjs +46 -0
  353. package/src/tui/engine/tool-approval.mjs +94 -0
  354. package/src/tui/engine/tool-call-fields.mjs +23 -0
  355. package/src/tui/engine/tool-card-results.mjs +234 -0
  356. package/src/tui/engine/tool-result-status.mjs +135 -0
  357. package/src/tui/engine/tool-result-text.mjs +126 -0
  358. package/src/tui/engine.mjs +405 -1385
  359. package/src/tui/figures.mjs +5 -0
  360. package/src/tui/index.jsx +105 -0
  361. package/src/tui/input-editing.mjs +60 -10
  362. package/src/tui/keyboard-protocol.mjs +2 -2
  363. package/src/tui/lib/voice-recorder.mjs +35 -19
  364. package/src/tui/markdown/format-token.mjs +11 -9
  365. package/src/tui/paste-attachments.mjs +38 -0
  366. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  367. package/src/tui/theme.mjs +6 -0
  368. package/src/tui/themes/base.mjs +2 -2
  369. package/src/tui/themes/kanagawa.mjs +4 -4
  370. package/src/tui/themes/teal.mjs +4 -5
  371. package/src/tui/themes/utils.mjs +1 -1
  372. package/src/ui/statusline-agents.mjs +213 -0
  373. package/src/ui/statusline-format.mjs +146 -0
  374. package/src/ui/statusline-segments.mjs +148 -0
  375. package/src/ui/statusline.mjs +77 -462
  376. package/src/ui/tool-card.mjs +0 -1
  377. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  378. package/src/workflows/default/WORKFLOW.md +16 -9
  379. package/src/workflows/sequential/WORKFLOW.md +16 -11
  380. package/src/workflows/solo/WORKFLOW.md +5 -1
  381. package/vendor/ink/build/display-width.js +19 -3
  382. package/vendor/ink/build/ink.js +103 -6
  383. package/vendor/ink/build/log-update.js +17 -3
  384. package/vendor/ink/build/wrap-text.js +125 -0
  385. package/scripts/_test-folder-dialog.mjs +0 -30
  386. package/scripts/fix-brief-fn.mjs +0 -35
  387. package/scripts/fix-format-tool-surface.mjs +0 -24
  388. package/scripts/fix-tool-exec-visible.mjs +0 -42
  389. package/scripts/patch-agent-brief.mjs +0 -48
  390. package/scripts/patch-app.mjs +0 -21
  391. package/scripts/patch-app2.mjs +0 -18
  392. package/scripts/patch-dist-brief.mjs +0 -96
  393. package/scripts/patch-tool-exec.mjs +0 -70
  394. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  395. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  396. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  397. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  398. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  399. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  400. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  401. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  402. package/src/standalone/agent-task-status.test.mjs +0 -76
  403. package/src/tui/components/tool-output-format.test.mjs +0 -399
  404. package/src/tui/display-width.test.mjs +0 -35
  405. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  406. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  407. package/src/tui/markdown/format-token.test.mjs +0 -354
  408. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  409. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  410. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  411. package/src/tui/prompt-history-store.test.mjs +0 -52
  412. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  413. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  414. package/src/ui/markdown.test.mjs +0 -70
  415. package/src/ui/statusline-context-label.test.mjs +0 -15
  416. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  417. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -0,0 +1,780 @@
1
+ // Recall / search / dump / stats query handlers extracted from index.mjs.
2
+ //
3
+ // These six functions form the read-side query cluster: they share the same
4
+ // live DB handle plus a small set of recall-format / retriever helpers, and a
5
+ // few facade-local pieces of state (the cold-recall log throttle and the
6
+ // optional trace DB used for recall
7
+ // telemetry). All of that is injected through createQueryHandlers({...}) so the
8
+ // module holds no state of its own and the facade keeps ownership of `db`,
9
+ // `_traceDb`, etc. (getBootTimestamp stays injected for facade compatibility
10
+ // but period='last' no longer reads it — 'last' is an unbounded newest-first
11
+ // browse.)
12
+
13
+ import {
14
+ parsePeriod,
15
+ coreRecallTerms,
16
+ normalizeRecallProjectScope,
17
+ sessionRecallTerms,
18
+ interleaveRawRows,
19
+ renderEntryLines,
20
+ renderSessionGroupedLines,
21
+ } from './recall-format.mjs'
22
+ import { searchRelevantHybrid } from './memory-recall-store.mjs'
23
+ import { fetchEntriesByIdsScoped } from './memory-recall-id-patch.mjs'
24
+ import { retrieveEntries } from './memory-retrievers.mjs'
25
+ import { cleanMemoryText } from './memory.mjs'
26
+ import { insertTraceEvents } from './trace-store.mjs'
27
+ import {
28
+ embedText,
29
+ embedTexts,
30
+ isEmbeddingModelReady,
31
+ warmupEmbeddingProvider,
32
+ } from './embedding-provider.mjs'
33
+
34
+ export function createQueryHandlers({
35
+ getDb,
36
+ log,
37
+ resolveProjectScope,
38
+ embeddingWarmupCanStart,
39
+ getBootTimestamp,
40
+ getTraceDb,
41
+ }) {
42
+ // Facade-owned cold-recall log throttle. Kept module-local to this factory
43
+ // instance (one memory runtime = one factory) so the 10s de-dup window
44
+ // behaves exactly as it did when it was a top-level `let` in index.mjs.
45
+ let _embeddingColdRecallLogAt = 0
46
+
47
+ // Raw-row priority lookup for narrow-window queries. Raw rows (is_root=0,
48
+ // chunk_root IS NULL) are inserted immediately by ingestTranscriptFile before
49
+ // cycle1 runs, so they always carry the freshest turns in the DB.
50
+ async function readRawRowsInWindow(db, tsFromMs, tsToMs, hardLimit = 10, { projectScope, sessionId, terms } = {}) {
51
+ try {
52
+ // Composable WHERE assembly (mirrors retrieveEntries' filter semantics so
53
+ // raw and chunked legs stay in filter parity: projectScope AND sessionId
54
+ // apply identically to both pools).
55
+ const where = ['chunk_root IS NULL', 'is_root = 0', 'ts >= $1', 'ts <= $2']
56
+ const params = [tsFromMs ?? 0, tsToMs ?? Date.now()]
57
+ if (projectScope === 'common') {
58
+ where.push('project_id IS NULL')
59
+ } else if (projectScope && projectScope !== 'all') {
60
+ params.push(projectScope)
61
+ where.push(`(project_id IS NULL OR project_id = $${params.length})`)
62
+ }
63
+ const sid = String(sessionId || '').trim()
64
+ if (sid) {
65
+ params.push(sid)
66
+ where.push(`session_id = $${params.length}`)
67
+ }
68
+ if (Array.isArray(terms) && terms.length > 0) {
69
+ const textExpr = `lower(coalesce(content, '') || ' ' || coalesce(element, '') || ' ' || coalesce(summary, ''))`
70
+ const clauses = terms.map((term) => {
71
+ params.push(`%${term}%`)
72
+ return `(CASE WHEN ${textExpr} LIKE $${params.length} THEN 1 ELSE 0 END)`
73
+ })
74
+ // Multi-token queries: from 3+ terms require at least 2 matching terms
75
+ // so one common token ("대화", "recall") can't drag unrelated raw rows
76
+ // into the page. 1-2 term queries keep single-hit contains semantics —
77
+ // short Korean queries are often exactly two meaningful tokens and a
78
+ // 2-of-2 requirement silently emptied the raw leg for them.
79
+ const minHits = terms.length >= 3 ? 2 : 1
80
+ where.push(`(${clauses.join(' + ')}) >= ${minHits}`)
81
+ }
82
+ params.push(hardLimit)
83
+ const sql = `SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
84
+ element, category, summary, status, score, last_seen_at, project_id
85
+ FROM entries
86
+ WHERE ${where.join(' AND ')}
87
+ ORDER BY ts DESC
88
+ LIMIT $${params.length}`
89
+ const rows = (await db.query(sql, params)).rows
90
+ return rows.map(r => ({ ...r, retrievalScore: 0, rrf: 0 }))
91
+ } catch { return [] }
92
+ }
93
+
94
+ async function recallSessionRows(args = {}) {
95
+ const db = getDb()
96
+ const sessionId = String(args.sessionId || args.session_id || '').trim()
97
+ if (!sessionId) return { text: '(no current session)' }
98
+ const limit = Math.max(1, Math.min(100, Number(args.limit) || 20))
99
+ const terms = sessionRecallTerms(args.query)
100
+ const params = [sessionId]
101
+ // Roots + not-yet-chunked leaves only. Once cycle1 turns raw leaves into
102
+ // (root, members) pairs, selecting every row unfiltered emitted the root's
103
+ // summary AND its own member rows in the same browse — duplicate content.
104
+ // A committed member (is_root=0 with a chunk_root) is always reachable via
105
+ // its root's `members` expansion below, so it never needs to be selected
106
+ // directly here.
107
+ const where = ['session_id = $1', '(is_root = 1 OR chunk_root IS NULL OR chunk_root = id)']
108
+ // Current-turn cutoff: the newest unchunked row is very often the calling
109
+ // turn's OWN recall request/tool-args, still being written when this query
110
+ // runs. Exclude it from a bare (no-query) browse so the in-flight turn
111
+ // doesn't self-echo; a query browse (explicit search intent) keeps it.
112
+ // Only treat the newest unchunked turn as "in-flight" when its latest row
113
+ // is fresh (within FRESHNESS_MS of now) — an older newest-unchunked-turn
114
+ // is completed history (cycle1 just hasn't gotten to it, or drain timed
115
+ // out) and must stay visible, not be silently hidden every browse.
116
+ const IN_FLIGHT_TURN_FRESHNESS_MS = 5 * 60 * 1000
117
+ let excludeSourceTurnId = null
118
+ if (terms.length === 0) {
119
+ try {
120
+ const r = await db.query(
121
+ `SELECT source_turn t, MAX(ts) last_ts FROM entries
122
+ WHERE session_id = $1 AND chunk_root IS NULL
123
+ GROUP BY source_turn ORDER BY source_turn DESC LIMIT 1`,
124
+ [sessionId],
125
+ )
126
+ const t = r.rows?.[0]?.t
127
+ const lastTs = Number(r.rows?.[0]?.last_ts)
128
+ if (t != null && Number.isFinite(lastTs) && (Date.now() - lastTs) <= IN_FLIGHT_TURN_FRESHNESS_MS) {
129
+ excludeSourceTurnId = Number(t)
130
+ }
131
+ } catch {}
132
+ }
133
+ if (Number.isFinite(excludeSourceTurnId)) {
134
+ params.push(excludeSourceTurnId)
135
+ where.push(`NOT (chunk_root IS NULL AND source_turn = $${params.length})`)
136
+ }
137
+ if (terms.length > 0) {
138
+ const textExpr = `lower(coalesce(content, '') || ' ' || coalesce(element, '') || ' ' || coalesce(summary, ''))`
139
+ const clauses = terms.map((term) => {
140
+ params.push(`%${term}%`)
141
+ return `${textExpr} LIKE $${params.length}`
142
+ })
143
+ where.push(`(${clauses.join(' OR ')})`)
144
+ }
145
+ params.push(limit)
146
+ let rows = (await db.query(`
147
+ SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
148
+ element, category, summary, status, score, last_seen_at, project_id
149
+ FROM entries
150
+ WHERE ${where.join(' AND ')}
151
+ ORDER BY ts DESC, id DESC
152
+ LIMIT $${params.length}
153
+ `, params)).rows
154
+ if (rows.length < limit) {
155
+ const seen = new Set(rows.map((row) => Number(row.id)).filter((id) => Number.isFinite(id)))
156
+ const fillLimit = Math.max(0, limit - rows.length)
157
+ const fillWhere = ['session_id = $1', 'id <> ALL($2::bigint[])', '(is_root = 1 OR chunk_root IS NULL OR chunk_root = id)']
158
+ const fillParams = [sessionId, [...seen]]
159
+ if (Number.isFinite(excludeSourceTurnId)) {
160
+ fillParams.push(excludeSourceTurnId)
161
+ fillWhere.push(`NOT (chunk_root IS NULL AND source_turn = $${fillParams.length})`)
162
+ }
163
+ fillParams.push(fillLimit)
164
+ const fillRows = fillLimit > 0
165
+ ? (await db.query(`
166
+ SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
167
+ element, category, summary, status, score, last_seen_at, project_id
168
+ FROM entries
169
+ WHERE ${fillWhere.join(' AND ')}
170
+ ORDER BY ts DESC, id DESC
171
+ LIMIT $${fillParams.length}
172
+ `, fillParams)).rows
173
+ : []
174
+ if (fillRows.length > 0) rows = [...rows, ...fillRows]
175
+ }
176
+ if (args.includeMembers === true) {
177
+ const rootIds = rows
178
+ .filter((row) => Number(row.is_root) === 1)
179
+ .map((row) => Number(row.id))
180
+ .filter((id) => Number.isFinite(id))
181
+ if (rootIds.length > 0) {
182
+ const members = (await db.query(`
183
+ SELECT id, ts, role, content, session_id, source_turn, project_id, chunk_root
184
+ FROM entries
185
+ WHERE chunk_root = ANY($1::bigint[]) AND is_root = 0
186
+ ORDER BY chunk_root ASC, COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
187
+ `, [rootIds])).rows
188
+ const byRoot = new Map(rootIds.map((id) => [id, []]))
189
+ for (const member of members) {
190
+ const root = Number(member.chunk_root)
191
+ if (byRoot.has(root)) byRoot.get(root).push(member)
192
+ }
193
+ for (const row of rows) {
194
+ const id = Number(row.id)
195
+ if (byRoot.has(id)) row.members = byRoot.get(id)
196
+ }
197
+ }
198
+ }
199
+ return { text: renderEntryLines(rows) }
200
+ }
201
+
202
+ async function recallCoreRows(query, { projectScope, category, limit } = {}) {
203
+ const db = getDb()
204
+ const terms = coreRecallTerms(query)
205
+ if (terms.length === 0) return []
206
+
207
+ const params = []
208
+ const where = []
209
+ const scope = normalizeRecallProjectScope(projectScope)
210
+ if (scope === null) {
211
+ where.push('project_id IS NULL')
212
+ } else if (scope !== '*') {
213
+ params.push(scope)
214
+ where.push(`(project_id IS NULL OR project_id = $${params.length})`)
215
+ }
216
+ if (category != null) {
217
+ const cats = (Array.isArray(category) ? category : [category])
218
+ .map((value) => String(value || '').trim().toLowerCase())
219
+ .filter(Boolean)
220
+ if (cats.length > 0) {
221
+ const placeholders = cats.map((cat) => {
222
+ params.push(cat)
223
+ return `$${params.length}`
224
+ })
225
+ where.push(`category IN (${placeholders.join(', ')})`)
226
+ }
227
+ }
228
+
229
+ const textExpr = `lower(coalesce(element, '') || ' ' || coalesce(summary, ''))`
230
+ const termClauses = terms.map((term) => {
231
+ params.push(`%${term}%`)
232
+ return `${textExpr} LIKE $${params.length}`
233
+ })
234
+ where.push(`(${termClauses.join(' OR ')})`)
235
+ const hitExpr = termClauses.map((clause) => `CASE WHEN ${clause} THEN 1 ELSE 0 END`).join(' + ')
236
+ const rowLimit = Math.max(1, Math.min(10, Number(limit) || 5))
237
+ params.push(rowLimit)
238
+
239
+ const rows = (await db.query(`
240
+ SELECT id, element, summary, category, project_id, created_at, updated_at,
241
+ (${hitExpr}) AS hit_count
242
+ FROM core_entries
243
+ WHERE ${where.join(' AND ')}
244
+ ORDER BY hit_count DESC, updated_at DESC, id ASC
245
+ LIMIT $${params.length}
246
+ `, params)).rows
247
+
248
+ return rows.map((row) => ({
249
+ ...row,
250
+ id: `core:${row.id}`,
251
+ ts: row.updated_at || row.created_at || Date.now(),
252
+ is_root: 1,
253
+ }))
254
+ }
255
+
256
+ async function handleSearch(args, signal) {
257
+ const db = getDb()
258
+ const _traceDb = getTraceDb()
259
+ // Cooperative abort check: throw early if the caller already aborted
260
+ // (IPC cancel handler signals the AbortController before re-entry).
261
+ if (signal?.aborted) throw signal.reason ?? new Error('aborted')
262
+ // No pre-search drain: recall NEVER runs LLM chunking inline. Unchunked
263
+ // rows are served directly by the raw leg (readRawRowsInWindow on the
264
+ // query path, the chunk_root IS NULL selection in recallSessionRows) and
265
+ // are dense-searchable via the always-on raw-embedding flush (post-ingest
266
+ // + checkCycles tick). Chunked/scored upgrades arrive from the background
267
+ // cycle1 sweep on its own schedule.
268
+ // #id lookup normalization: search_memories and memory action:'search'
269
+ // callers pass a single `id` (or an id array under that same key), not
270
+ // the `ids` array below. Normalize once here so every dispatch path gets
271
+ // exact-id lookup, not just callers who already knew to use `ids`.
272
+ if (!Array.isArray(args.ids) && args.id != null) {
273
+ args = { ...args, ids: Array.isArray(args.id) ? args.id : [args.id] }
274
+ }
275
+ if (args?.currentSession === true || args?.sessionId || args?.session_id) {
276
+ return await recallSessionRows(args)
277
+ }
278
+ // id mode (follow-up lookup): caller passed `#N` markers from a prior
279
+ // recall result. Fetch those rows directly + their chunk members,
280
+ // bypassing hybrid search entirely. Output reuses renderEntryLines so
281
+ // the shape stays identical to the search path (chunk members first,
282
+ // root summary fallback).
283
+ if (Array.isArray(args.ids) && args.ids.length > 0) {
284
+ const ids = args.ids
285
+ .map(v => Number(v))
286
+ .filter(v => Number.isInteger(v) && v > 0)
287
+ if (ids.length === 0) return { text: '(no valid ids)' }
288
+ const includeArchived = args.includeArchived !== false
289
+ const category = args.category
290
+ const period = String(args.period ?? '').trim() || undefined
291
+ const temporal = parsePeriod(period, false)
292
+ let projectScope
293
+ if (typeof args.projectScope === 'string' && args.projectScope) {
294
+ projectScope = args.projectScope
295
+ } else {
296
+ const projectId = resolveProjectScope(typeof args.cwd === 'string' && args.cwd ? args.cwd : null)
297
+ projectScope = projectId !== null ? projectId : 'common'
298
+ }
299
+ const excludeStatuses = includeArchived ? [] : ['archived']
300
+ const rows = await fetchEntriesByIdsScoped(db, ids, {
301
+ ts_from: temporal?.startMs,
302
+ ts_to: temporal?.endMs,
303
+ excludeStatuses,
304
+ category,
305
+ projectScope,
306
+ })
307
+ if (rows.length === 0) return { text: '(no results)' }
308
+ // Members for any root rows in the result set.
309
+ const rootIds = rows.filter(r => r.is_root === 1).map(r => Number(r.id))
310
+ const memberLeafIds = new Set()
311
+ if (rootIds.length > 0) {
312
+ const { rows: memberRows } = await db.query(
313
+ `SELECT id, ts, role, content, chunk_root
314
+ FROM entries WHERE chunk_root = ANY($1::bigint[]) AND is_root = 0
315
+ ORDER BY ts ASC, id ASC`,
316
+ [rootIds],
317
+ )
318
+ const membersByRoot = new Map()
319
+ for (const m of memberRows) {
320
+ const k = Number(m.chunk_root)
321
+ if (!membersByRoot.has(k)) membersByRoot.set(k, [])
322
+ membersByRoot.get(k).push(m)
323
+ memberLeafIds.add(Number(m.id))
324
+ }
325
+ for (const r of rows) {
326
+ if (r.is_root === 1) r.members = membersByRoot.get(Number(r.id)) ?? []
327
+ }
328
+ }
329
+ // Preserve caller-supplied id order; drop leaves already inlined as a
330
+ // root's chunk member to prevent double emission when the caller names
331
+ // a root and one of its leaves in the same batch.
332
+ const byId = new Map(rows.map(r => [Number(r.id), r]))
333
+ const ordered = ids
334
+ .map(id => byId.get(id))
335
+ .filter(Boolean)
336
+ .filter(r => !(r.is_root === 0 && memberLeafIds.has(Number(r.id))))
337
+ return { text: renderEntryLines(ordered) }
338
+ }
339
+ // Array query — fan out in parallel, each query runs its own hybrid search
340
+ // path, and results are grouped in the response so the caller sees one
341
+ // ranked list per angle. Collapses what would otherwise be N sequential
342
+ // tool calls into a single invocation.
343
+ if (Array.isArray(args.query)) {
344
+ // Dedup + fan-out cap. The cap protects the result envelope from
345
+ // over-eager callers (20+ near-duplicate queries N× the IO) without
346
+ // silently swallowing the caller's intent: when the input exceeds
347
+ // QUERIES_CAP, prepend a one-line note so the caller can see the
348
+ // truncation and re-shape their query list.
349
+ const QUERIES_CAP = 5
350
+ const dedup = [...new Set(args.query.map(q => String(q || '').trim()).filter(Boolean))]
351
+ if (dedup.length === 0) return { text: '' }
352
+ const queries = dedup.slice(0, QUERIES_CAP)
353
+ const dropped = dedup.length - queries.length
354
+ const rest = { ...args }
355
+ delete rest.query
356
+ const deadlineSec = Math.max(1, Number(process.env.MEMORY_FANOUT_DEADLINE_S) || 180)
357
+ const deadlineMs = deadlineSec * 1000
358
+ const fanOutAbort = new AbortController()
359
+ let deadlineTimer
360
+ const deadlineRace = new Promise((_res, rej) => {
361
+ deadlineTimer = setTimeout(() => {
362
+ fanOutAbort.abort(new Error(`memory fan-out deadline exceeded (${deadlineSec}s)`))
363
+ rej(Object.assign(new Error(`memory fan-out deadline exceeded (${deadlineSec}s)`), { _deadline: true }))
364
+ }, deadlineMs)
365
+ })
366
+ let settled
367
+ try {
368
+ // Pre-warm only when the embedding model is already resident. If the
369
+ // process is still cold, keep recall responsive and let the background
370
+ // warmup finish independently instead of making the first query pay the
371
+ // ONNX session-create cost.
372
+ if (isEmbeddingModelReady()) {
373
+ // Race against the same deadline as the fan-out itself: a stuck
374
+ // embedding worker would previously park here indefinitely because
375
+ // the timer hadn't been started yet from the fan-out's perspective.
376
+ await Promise.race([embedTexts(queries), deadlineRace])
377
+ } else if (embeddingWarmupCanStart()) {
378
+ void warmupEmbeddingProvider().catch((err) => {
379
+ log(`[memory-service] embedding warmup after cold fan-out skipped dense search: ${err?.message || err}\n`)
380
+ })
381
+ }
382
+ settled = await Promise.race([
383
+ Promise.all(queries.map(async (q) => {
384
+ if (fanOutAbort.signal.aborted) throw fanOutAbort.signal.reason
385
+ if (signal?.aborted) throw signal.reason ?? new Error('aborted')
386
+ const sub = await handleSearch({ ...rest, query: q }, signal)
387
+ return `[${q}]\n${sub.text || '(no results)'}`
388
+ })),
389
+ deadlineRace,
390
+ ])
391
+ } finally {
392
+ clearTimeout(deadlineTimer)
393
+ }
394
+ const parts = settled
395
+ const header = dropped > 0
396
+ ? `note: ${dedup.length} queries received, ${queries.length} processed, ${dropped} dropped (cap ${QUERIES_CAP})\n\n`
397
+ : ''
398
+ return { text: header + parts.join('\n\n') }
399
+ }
400
+ const query = String(args.query ?? '').trim()
401
+ let period = String(args.period ?? '').trim() || undefined
402
+ // Period and sort are caller-supplied only. Lead is responsible for
403
+ // mapping vague time phrases / chronological intent into the period
404
+ // argument before calling; the engine does not infer them from query
405
+ // text.
406
+ const RECALL_LIMIT_CAP = 100
407
+ const RECALL_OFFSET_CAP = 500
408
+ const requestedLimit = Number(args.limit)
409
+ const requestedOffset = Number(args.offset)
410
+ let limit = Math.max(1, Number.isFinite(requestedLimit) ? requestedLimit : 10)
411
+ let offset = Math.max(0, Number.isFinite(requestedOffset) ? requestedOffset : 0)
412
+ const recallCapNotes = []
413
+ if (Number.isFinite(requestedLimit) && requestedLimit > RECALL_LIMIT_CAP) {
414
+ limit = RECALL_LIMIT_CAP
415
+ recallCapNotes.push(`limit capped to ${RECALL_LIMIT_CAP} (requested ${requestedLimit})`)
416
+ } else {
417
+ limit = Math.min(RECALL_LIMIT_CAP, limit)
418
+ }
419
+ if (Number.isFinite(requestedOffset) && requestedOffset > RECALL_OFFSET_CAP) {
420
+ offset = RECALL_OFFSET_CAP
421
+ recallCapNotes.push(`offset capped to ${RECALL_OFFSET_CAP} (requested ${requestedOffset})`)
422
+ } else {
423
+ offset = Math.min(RECALL_OFFSET_CAP, offset)
424
+ }
425
+ const recallCapPrefix = recallCapNotes.length ? `${recallCapNotes.join('; ')}\n` : ''
426
+ // Recent-browsing default: a query-less recall is a "show me the latest
427
+ // messages" browse, not a relevance search — chronological order is the
428
+ // only ordering that makes sense there, so sort defaults to 'date' when
429
+ // no query is present (explicit args.sort still wins). Query recalls keep
430
+ // the importance default.
431
+ const hasQueryForSort = Array.isArray(args.query)
432
+ ? args.query.some((v) => String(v || '').trim())
433
+ : String(args.query ?? '').trim() !== ''
434
+ const sort = args.sort != null ? String(args.sort) : (hasQueryForSort ? 'importance' : 'date')
435
+ // Chunk content is the primary recall output. Members default to true so
436
+ // callers receive the raw chunk leaves (the cycle1-produced semantic
437
+ // chunks) rather than just the root's cycle2-compressed summary line.
438
+ // Explicit `includeMembers:false` keeps the legacy summary-only mode.
439
+ const includeMembers = args.includeMembers !== false
440
+ // Raw leg defaults ON: recall never drains/chunks inline anymore, so raw
441
+ // (unchunked) rows — embedded by the always-on post-ingest/tick flush —
442
+ // are the only way to see content cycle1 hasn't reached yet, regardless
443
+ // of the recap toggle. The importance/query search leg only ranks chunked
444
+ // entries; readRawRowsInWindow interleaves the raw rows into the hybrid
445
+ // list. Explicit includeRaw:false keeps the chunked-only view.
446
+ const includeRaw = args.includeRaw !== false
447
+ const includeArchived = args.includeArchived !== false
448
+ const category = args.category
449
+ const temporal = parsePeriod(period, Boolean(query))
450
+
451
+ // Derive projectScope from caller cwd (falls back to process.cwd()).
452
+ // Explicit args.projectScope (string) takes priority so callers can
453
+ // override to 'all', 'common', or a specific slug.
454
+ let projectScope
455
+ if (typeof args.projectScope === 'string' && args.projectScope) {
456
+ projectScope = args.projectScope
457
+ } else {
458
+ const projectId = resolveProjectScope(typeof args.cwd === 'string' && args.cwd ? args.cwd : null)
459
+ projectScope = projectId !== null ? projectId : 'common'
460
+ }
461
+
462
+ // R11 reviewer M4: calendar-bounded periods disable freshness decay
463
+ // so within-period ranking doesn't downgrade Mon entries vs Sun.
464
+ const CALENDAR_PERIODS = new Set(['yesterday', 'today', 'this_week', 'last_week'])
465
+ const isCalendarPeriod = period != null
466
+ && (CALENDAR_PERIODS.has(period) || /^\d{4}-\d{2}-\d{2}/.test(period) || /^\d{1,2}:\d{2}~/.test(period))
467
+ const applyFreshness = !isCalendarPeriod
468
+
469
+ // period='last': no time window and no session exclusion — 'last' is a
470
+ // plain "walk back through ALL prior conversation, newest first" browse.
471
+ // No boot-timestamp cap (the old cap hid every session that ran while a
472
+ // long-lived daemon stayed up), no gap-bounded burst, no current-session
473
+ // filter: limit/offset page through history and the grouped renderer
474
+ // separates sessions. temporal stays unbounded (mode marker only).
475
+
476
+ if (query) {
477
+ const _t0 = Date.now()
478
+ if (signal?.aborted) throw signal.reason ?? new Error('aborted')
479
+ let queryVector = null
480
+ if (isEmbeddingModelReady()) {
481
+ queryVector = await embedText(query)
482
+ } else {
483
+ const now = Date.now()
484
+ if (now - _embeddingColdRecallLogAt > 10_000) {
485
+ _embeddingColdRecallLogAt = now
486
+ log('[recall] embedding model cold; returning lexical results while background warmup continues\n')
487
+ }
488
+ if (embeddingWarmupCanStart()) {
489
+ void warmupEmbeddingProvider().catch((err) => {
490
+ log(`[memory-service] embedding warmup after cold recall failed: ${err?.message || err}\n`)
491
+ })
492
+ }
493
+ }
494
+ if (signal?.aborted) throw signal.reason ?? new Error('aborted')
495
+ const _t1 = Date.now()
496
+ if (process.env.MIXDOG_DEBUG_MEMORY) {
497
+ log(`[search-time] embed=${_t1 - _t0}ms query="${query.slice(0, 60)}"\n`)
498
+ }
499
+ // Push ts and status filters into the hybrid candidate query so FTS / vec
500
+ // rank inside the requested window, not the whole tree. The previous post-
501
+ // filter approach silently emptied results when relevant matches sat
502
+ // outside `period` (default 30d) and could not bubble through.
503
+ // Recall is history-first: archived roots hold most prior work. Callers
504
+ // that need only live invariants can pass includeArchived:false.
505
+ const excludeStatuses = includeArchived ? [] : ['archived']
506
+ const results = await searchRelevantHybrid(db, query, {
507
+ limit: limit + offset,
508
+ queryVector: Array.isArray(queryVector) ? queryVector : null,
509
+ includeMembers,
510
+ ts_from: temporal?.startMs,
511
+ ts_to: temporal?.endMs,
512
+ applyFreshness,
513
+ projectScope,
514
+ category,
515
+ excludeStatuses,
516
+ // useHotActive was set to true here so default (no-period) calls
517
+ // routed through the mv_hot_active materialized view — a narrow
518
+ // active-roots-only pool. Live usage is dominated by vague-time
519
+ // queries ("recent / lately") where Lead callers omit the period
520
+ // filter, leaving the MV as the sole source. That hid every
521
+ // orphan leaf and every pending root — fresh work from the last 1-60
522
+ // minutes never surfaced. Now that the entries-table CTE legs run
523
+ // against broaden HNSW + GIN trgm partial indexes (the
524
+ // is_root=1 predicate was dropped in the same revision), the
525
+ // entries path is fast enough (1-2 ms ANN on ~10K rows, O(log N)
526
+ // through 1M+) to be the single source of truth. The MV is left in
527
+ // place for now but no longer routed to from search; cycle2 may stop
528
+ // refreshing it in a follow-up commit once nothing else reads it.
529
+ useHotActive: false,
530
+ })
531
+ let filtered = results
532
+ if (sort === 'date') {
533
+ // R11 reviewer L5: NaN guard — entries with null/undefined ts default
534
+ // to 0 so the comparator stays numeric and stable.
535
+ filtered.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0))
536
+ } else {
537
+ filtered.sort((a, b) => {
538
+ const sa = (v) => { const n = Number(v); return Number.isFinite(n) ? n : 0 }
539
+ return (sa(b.retrievalScore ?? b.rrf ?? 0) - sa(a.retrievalScore ?? a.rrf ?? 0))
540
+ || (sa(b.score ?? 0) - sa(a.score ?? 0))
541
+ || (sa(b.ts ?? 0) - sa(a.ts ?? 0))
542
+ || (Number(a.id ?? 0) - Number(b.id ?? 0))
543
+ })
544
+ }
545
+ if (includeRaw) {
546
+ // Raw rows (chunk_root IS NULL) carry no retrievalScore, so a naive
547
+ // append-after-hybrid under sort=importance always lands them past
548
+ // slice(offset, offset+limit) once the hybrid pool exceeds one page —
549
+ // every page beyond the first silently drops them. Fetch a wider raw
550
+ // window (bounded like the hybrid candidate pool) and spread the
551
+ // fetched raw rows evenly across the WHOLE hybrid list before slicing,
552
+ // so every offset page gets its proportional share instead of only
553
+ // page 0. Same projectScope/ts window as the hybrid leg — filter
554
+ // parity (item 3) is deliberate, not accidental.
555
+ const RAW_FETCH = Math.min(500, Math.max(20, limit + offset))
556
+ const rawRows = await readRawRowsInWindow(
557
+ db,
558
+ temporal?.startMs ?? null,
559
+ temporal?.endMs ?? Date.now(),
560
+ RAW_FETCH,
561
+ { projectScope, terms: sessionRecallTerms(query) },
562
+ )
563
+ const seenIds = new Set(filtered.map(r => r.id))
564
+ const newRaw = rawRows.filter(r => !seenIds.has(r.id))
565
+ if (sort === 'date') {
566
+ for (const r of newRaw) filtered.push(r)
567
+ filtered.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0))
568
+ } else {
569
+ // sort=importance: interleave raw rows at a fixed stride through the
570
+ // full (pre-slice) hybrid list instead of appending at the tail, so
571
+ // offset > 0 pages also draw from the raw pool proportionally.
572
+ filtered = interleaveRawRows(filtered, newRaw)
573
+ }
574
+ }
575
+ const coreRows = await recallCoreRows(query, { projectScope, category, limit })
576
+ if (coreRows.length > 0) {
577
+ filtered = [...coreRows, ...filtered]
578
+ }
579
+ const sliced = filtered.slice(offset, offset + limit)
580
+ const _t2 = Date.now()
581
+ if (process.env.MIXDOG_DEBUG_MEMORY) {
582
+ log(`[search-time] hybrid+sort+raw=${_t2 - _t1}ms rows=${filtered.length} sliced=${sliced.length}\n`)
583
+ }
584
+ // Emit a recall trace event so getTraceWithEntries() can correlate
585
+ // this search with the top-ranked memory entry. One event per
586
+ // handleSearch call (not per returned row) — cheapest meaningful link.
587
+ // parent_span_id left null: the agent-side span id is only known after
588
+ // the DB insert of the loop/tool events, which happens async on the
589
+ // client side and is not available here.
590
+ if (_traceDb && filtered.length > 0) {
591
+ const topHit = filtered[0]
592
+ const topId = topHit?.id != null ? Number(topHit.id) : null
593
+ if (topId !== null && Number.isFinite(topId)) {
594
+ insertTraceEvents(_traceDb, [{
595
+ ts: Date.now(),
596
+ kind: 'recall',
597
+ entry_id: topId,
598
+ payload: { query: query.slice(0, 200), hit_count: filtered.length },
599
+ }]).catch(e => log(`[trace] insertTraceEvents error: ${e?.message}\n`))
600
+ }
601
+ }
602
+ const out = { text: recallCapPrefix + renderEntryLines(sliced) }
603
+ if (process.env.MIXDOG_DEBUG_MEMORY) {
604
+ log(`[search-time] render+trace=${Date.now() - _t2}ms total=${Date.now() - _t0}ms textLen=${out.text.length}\n`)
605
+ }
606
+ return out
607
+ }
608
+
609
+ const filters = { limit: limit + offset }
610
+ if (temporal?.startMs != null) { filters.ts_from = temporal.startMs; filters.ts_to = temporal.endMs }
611
+ filters.projectScope = projectScope
612
+ if (category != null) filters.category = category
613
+ filters.sort = sort
614
+ if (!includeArchived) filters.excludeStatuses = ['archived']
615
+ if (includeMembers) filters.includeMembers = true
616
+ const rows = await retrieveEntries(db, filters)
617
+ // Recent-browsing raw merge: a query-less recall must show the freshest
618
+ // turns even when cycle1 hasn't chunked them yet. Roots lag ingest by up
619
+ // to a cycle interval, so on sort=date pull the raw (unchunked) window
620
+ // too and merge chronologically — original text first, no summaries.
621
+ // Query-less + includeRaw:false callers keep the roots-only view.
622
+ let merged = rows
623
+ if (sort === 'date' && args.includeRaw !== false) {
624
+ const rawRows = await readRawRowsInWindow(
625
+ db,
626
+ filters.ts_from ?? temporal?.startMs ?? null,
627
+ filters.ts_to ?? temporal?.endMs ?? Date.now(),
628
+ Math.min(500, Math.max(20, limit + offset)),
629
+ { projectScope },
630
+ )
631
+ const seenIds = new Set(rows.map(r => Number(r.id)))
632
+ // Drop raw leaves already inlined as some returned root's member.
633
+ for (const r of rows) {
634
+ if (Array.isArray(r.members)) for (const m of r.members) seenIds.add(Number(m.id))
635
+ }
636
+ const newRaw = rawRows.filter(r => !seenIds.has(Number(r.id)))
637
+ if (newRaw.length > 0) {
638
+ merged = [...rows, ...newRaw]
639
+ merged.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0) || (Number(b.id) || 0) - (Number(a.id) || 0))
640
+ }
641
+ }
642
+ const sliced = merged.slice(offset, offset + limit)
643
+ // Multi-session grouping: a GLOBAL query-less browse ("recent work") spans
644
+ // sessions — render grouped per session (newest activity first) with the
645
+ // caller's own session marked "(current)" via the currentSessionId hint.
646
+ // Falls through to the flat list when everything is one session.
647
+ const _currentSessionHint = String(args?.currentSessionId || '').trim()
648
+ return { text: recallCapPrefix + renderSessionGroupedLines(sliced, { currentSessionId: _currentSessionHint }) }
649
+ }
650
+
651
+ async function dumpSessionRootChunks(args = {}) {
652
+ const db = getDb()
653
+ const sessionId = String(args.sessionId || args.session_id || '').trim()
654
+ if (!sessionId) return { text: '(no current session)', rows: [], chunks: [], isError: true }
655
+ const includeRaw = args.includeRaw !== false
656
+ const limit = Math.max(1, Math.min(1000, Number(args.limit) || 1000))
657
+ const rootRows = (await db.query(`
658
+ SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
659
+ element, category, summary, status, score, last_seen_at, project_id
660
+ FROM entries
661
+ WHERE session_id = $1 AND is_root = 1
662
+ ORDER BY COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
663
+ LIMIT $2
664
+ `, [sessionId, limit])).rows
665
+ const roots = rootRows.map((r) => ({ ...r, members: [] }))
666
+ const rootIds = roots.map((r) => Number(r.id)).filter((id) => Number.isFinite(id))
667
+ const memberRows = rootIds.length > 0
668
+ ? (await db.query(`
669
+ SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root, project_id
670
+ FROM entries
671
+ WHERE chunk_root = ANY($1::bigint[]) AND is_root = 0
672
+ ORDER BY chunk_root ASC, COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
673
+ `, [rootIds])).rows
674
+ : []
675
+ const byRoot = new Map(roots.map((r) => [Number(r.id), r]))
676
+ for (const m of memberRows) {
677
+ const root = byRoot.get(Number(m.chunk_root))
678
+ if (root) root.members.push(m)
679
+ }
680
+ let rawRows = []
681
+ if (includeRaw) {
682
+ rawRows = (await db.query(`
683
+ SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root, project_id
684
+ FROM entries
685
+ WHERE session_id = $1
686
+ AND is_root = 0
687
+ AND (chunk_root IS NULL OR chunk_root = id)
688
+ ORDER BY COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
689
+ LIMIT $2
690
+ `, [sessionId, limit])).rows
691
+ }
692
+ const chunks = []
693
+ for (const root of roots) {
694
+ const memberText = root.members
695
+ .map((m) => `${m.role === 'assistant' ? 'assistant' : m.role === 'user' ? 'user' : m.role}: ${cleanMemoryText(String(m.content ?? ''))}`)
696
+ .filter(Boolean)
697
+ .join('\n')
698
+ const summary = [root.element, root.summary].map((v) => String(v || '').trim()).filter(Boolean).join(' — ')
699
+ chunks.push({
700
+ id: Number(root.id),
701
+ kind: 'root',
702
+ ts: Number(root.ts) || 0,
703
+ sourceTurn: root.source_turn ?? null,
704
+ category: root.category || null,
705
+ summary,
706
+ text: memberText || cleanMemoryText(String(root.content ?? '')),
707
+ members: root.members,
708
+ })
709
+ }
710
+ for (const raw of rawRows) {
711
+ chunks.push({
712
+ id: Number(raw.id),
713
+ kind: 'raw',
714
+ chunkRoot: raw.chunk_root ?? null,
715
+ ts: Number(raw.ts) || 0,
716
+ sourceTurn: raw.source_turn ?? null,
717
+ category: null,
718
+ summary: '',
719
+ text: `${raw.role === 'assistant' ? 'assistant' : raw.role === 'user' ? 'user' : raw.role}: ${cleanMemoryText(String(raw.content ?? ''))}`,
720
+ members: [],
721
+ })
722
+ }
723
+ chunks.sort((a, b) => {
724
+ const at = Number.isFinite(Number(a.sourceTurn)) ? Number(a.sourceTurn) : 2147483647
725
+ const bt = Number.isFinite(Number(b.sourceTurn)) ? Number(b.sourceTurn) : 2147483647
726
+ return (at - bt) || ((a.ts || 0) - (b.ts || 0)) || ((a.id || 0) - (b.id || 0))
727
+ })
728
+ const text = chunks.length
729
+ ? chunks.map((chunk, idx) => {
730
+ const label = chunk.kind === 'root'
731
+ ? `# chunk ${idx + 1} root=${chunk.id}${chunk.category ? ` category=${chunk.category}` : ''}`
732
+ : `${chunk.chunkRoot == null ? '# raw_pending' : '# raw_terminal'} ${idx + 1} id=${chunk.id}`
733
+ const summary = chunk.summary ? `summary: ${chunk.summary}\n` : ''
734
+ return `${label}\n${summary}${chunk.text}`.trim()
735
+ }).join('\n\n')
736
+ : '(no results)'
737
+ return { text, rows: [...roots, ...rawRows], chunks }
738
+ }
739
+
740
+ async function entryStats() {
741
+ const db = getDb()
742
+ return await db.transaction(async (tx) => {
743
+ const total = (await tx.query(`SELECT COUNT(*) c FROM entries`)).rows[0].c
744
+ const roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1`)).rows[0].c
745
+ const active_roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'active'`)).rows[0].c
746
+ const archived_roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'archived'`)).rows[0].c
747
+ const unchunked_leaves = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE chunk_root IS NULL`)).rows[0].c
748
+ const cycle2_pending_roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'pending'`)).rows[0].c
749
+ const core_entries = (await tx.query(`SELECT COUNT(*) c FROM core_entries`)).rows[0].c
750
+ const core_embed_null = (await tx.query(`SELECT COUNT(*) c FROM core_entries WHERE embedding IS NULL`)).rows[0].c
751
+ const active_core_summaries = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'active' AND core_summary IS NOT NULL`)).rows[0].c
752
+ const active_core_summary_missing = (await tx.query(`
753
+ SELECT COUNT(*) c
754
+ FROM entries
755
+ WHERE is_root = 1
756
+ AND status = 'active'
757
+ AND (core_summary IS NULL OR btrim(core_summary) = '')
758
+ `)).rows[0].c
759
+ const byStatus = (await tx.query(`SELECT status, COUNT(*) c FROM entries WHERE is_root = 1 GROUP BY status`)).rows
760
+ const byCategory = (await tx.query(`SELECT category, COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'active' GROUP BY category ORDER BY c DESC`)).rows
761
+ const mvRows = (await tx.query(`SELECT relispopulated FROM pg_class WHERE relname = 'mv_hot_active' LIMIT 1`)).rows
762
+ const mv_hot_active_populated = mvRows.length ? Boolean(mvRows[0].relispopulated) : null
763
+ return {
764
+ total, roots, active_roots, archived_roots, unchunked_leaves, cycle2_pending_roots,
765
+ core_entries, core_embed_null, active_core_summaries, active_core_summary_missing,
766
+ mv_hot_active_populated,
767
+ byStatus, byCategory,
768
+ }
769
+ })
770
+ }
771
+
772
+ return {
773
+ readRawRowsInWindow,
774
+ recallSessionRows,
775
+ recallCoreRows,
776
+ handleSearch,
777
+ dumpSessionRootChunks,
778
+ entryStats,
779
+ }
780
+ }