mixdog 0.9.3 → 0.9.5

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 (382) hide show
  1. package/README.md +112 -38
  2. package/package.json +10 -3
  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/r5-orchestrated-task.json +7 -0
  7. package/scripts/bench/review-tasks.json +20 -0
  8. package/scripts/bench/round-codex.json +114 -0
  9. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  10. package/scripts/bench/round-mixdog-lead.json +269 -0
  11. package/scripts/bench/round-mixdog.json +126 -0
  12. package/scripts/bench/round-r10-bigsample.json +679 -0
  13. package/scripts/bench/round-r11-codexalign.json +257 -0
  14. package/scripts/bench/round-r13-clientmeta.json +464 -0
  15. package/scripts/bench/round-r14-betafeatures.json +466 -0
  16. package/scripts/bench/round-r15-fulldefault.json +462 -0
  17. package/scripts/bench/round-r16-sessionid.json +466 -0
  18. package/scripts/bench/round-r17-wirebytes.json +456 -0
  19. package/scripts/bench/round-r18-prewarm.json +468 -0
  20. package/scripts/bench/round-r19-clean.json +472 -0
  21. package/scripts/bench/round-r20-prewarm-clean.json +475 -0
  22. package/scripts/bench/round-r21-delta-retry.json +473 -0
  23. package/scripts/bench/round-r22-full-probe.json +693 -0
  24. package/scripts/bench/round-r23-itemprobe.json +701 -0
  25. package/scripts/bench/round-r24-shapefix.json +677 -0
  26. package/scripts/bench/round-r25-serial.json +464 -0
  27. package/scripts/bench/round-r26-parallel3.json +671 -0
  28. package/scripts/bench/round-r27-parallel10.json +894 -0
  29. package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
  30. package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
  31. package/scripts/bench/round-r30-instid.json +253 -0
  32. package/scripts/bench/round-r31-upgradeprobe.json +256 -0
  33. package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
  34. package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
  35. package/scripts/bench/round-r34-orchestrated.json +120 -0
  36. package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
  37. package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
  38. package/scripts/bench/round-r4-codex.json +114 -0
  39. package/scripts/bench/round-r4-mixed.json +225 -0
  40. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  41. package/scripts/bench/round-r6-codex.json +114 -0
  42. package/scripts/bench/round-r6-solo.json +257 -0
  43. package/scripts/bench/round-r7-full.json +254 -0
  44. package/scripts/bench/round-r8-fulldefault.json +255 -0
  45. package/scripts/bench-run.mjs +251 -32
  46. package/scripts/freevar-smoke.mjs +95 -0
  47. package/scripts/internal-comms-bench.mjs +3 -4
  48. package/scripts/internal-comms-smoke.mjs +10 -9
  49. package/scripts/model-catalog-audit.mjs +209 -0
  50. package/scripts/model-list-sanitize-test.mjs +37 -0
  51. package/scripts/mouse-probe.mjs +45 -0
  52. package/scripts/output-style-bench.mjs +13 -6
  53. package/scripts/output-style-smoke.mjs +4 -4
  54. package/scripts/provider-toolcall-test.mjs +7 -3
  55. package/scripts/recall-bench.mjs +76 -13
  56. package/scripts/recall-quality-cases.json +12 -0
  57. package/scripts/recall-usecase-cases.json +18 -0
  58. package/scripts/session-bench.mjs +152 -6
  59. package/scripts/tool-smoke.mjs +25 -65
  60. package/scripts/tui-render-smoke.mjs +90 -0
  61. package/scripts/webhook-smoke.mjs +208 -0
  62. package/src/agents/debugger/AGENT.md +4 -1
  63. package/src/agents/heavy-worker/AGENT.md +9 -8
  64. package/src/agents/maintainer/AGENT.md +4 -0
  65. package/src/agents/reviewer/AGENT.md +2 -1
  66. package/src/agents/scheduler-task/AGENT.md +2 -3
  67. package/src/agents/webhook-handler/AGENT.md +2 -3
  68. package/src/agents/worker/AGENT.md +10 -7
  69. package/src/app.mjs +12 -1
  70. package/src/headless-role.mjs +7 -1
  71. package/src/lib/rules-builder.cjs +4 -0
  72. package/src/mixdog-session-runtime.mjs +647 -2056
  73. package/src/output-styles/default.md +30 -9
  74. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  75. package/src/output-styles/minimal.md +8 -6
  76. package/src/output-styles/simple.md +21 -7
  77. package/src/rules/agent/00-common.md +6 -3
  78. package/src/rules/agent/30-explorer.md +16 -5
  79. package/src/rules/lead/01-general.md +5 -5
  80. package/src/rules/lead/lead-brief.md +15 -0
  81. package/src/rules/lead/lead-tool.md +6 -15
  82. package/src/rules/shared/01-tool.md +17 -21
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
  84. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  85. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  86. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  87. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  88. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  89. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  90. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  91. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  92. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  93. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  95. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  97. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  100. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  101. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  102. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  103. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
  104. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  109. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  110. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
  111. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  112. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
  113. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
  114. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
  115. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  116. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
  117. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  119. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
  120. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  121. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  122. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  123. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  124. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  125. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  126. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  127. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  128. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  129. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  130. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
  132. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  133. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  134. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  135. package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
  136. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  137. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  138. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  139. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  140. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  141. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  142. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  143. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  144. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  145. package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
  146. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  147. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
  149. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  152. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  155. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  157. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  158. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  159. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  161. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  163. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  164. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  165. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  166. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  167. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  168. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  169. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  170. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  171. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  172. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  173. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  174. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  175. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  176. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  177. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  178. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  179. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  180. package/src/runtime/channels/backends/discord.mjs +27 -318
  181. package/src/runtime/channels/backends/telegram.mjs +8 -12
  182. package/src/runtime/channels/index.mjs +247 -701
  183. package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
  184. package/src/runtime/channels/lib/config.mjs +37 -149
  185. package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
  186. package/src/runtime/channels/lib/event-queue.mjs +78 -13
  187. package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
  188. package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
  189. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  190. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  191. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  192. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  193. package/src/runtime/channels/lib/scheduler.mjs +27 -113
  194. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  195. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  196. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  197. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  198. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  199. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  200. package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
  201. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  202. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  203. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  204. package/src/runtime/channels/lib/webhook.mjs +43 -616
  205. package/src/runtime/channels/tool-defs.mjs +11 -130
  206. package/src/runtime/memory/index.mjs +210 -1948
  207. package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
  208. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  209. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  210. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  211. package/src/runtime/memory/lib/ko-morph.mjs +195 -0
  212. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  213. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  214. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  215. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  216. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  217. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  218. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  219. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  220. package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
  221. package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
  222. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  223. package/src/runtime/memory/lib/query-handlers.mjs +802 -0
  224. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  225. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  226. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  227. package/src/runtime/memory/tool-defs.mjs +5 -13
  228. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  229. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  230. package/src/runtime/search/lib/web-tools.mjs +24 -602
  231. package/src/runtime/shared/atomic-file.mjs +26 -1
  232. package/src/runtime/shared/config.mjs +14 -4
  233. package/src/runtime/shared/launcher-control.mjs +2 -2
  234. package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
  235. package/src/runtime/shared/schedules-store.mjs +13 -3
  236. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  237. package/src/runtime/shared/tool-primitives.mjs +308 -0
  238. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  239. package/src/runtime/shared/tool-surface.mjs +80 -898
  240. package/src/runtime/shared/transcript-writer.mjs +23 -0
  241. package/src/runtime/shared/update-checker.mjs +7 -4
  242. package/src/session-runtime/config-helpers.mjs +119 -2
  243. package/src/session-runtime/config-lifecycle.mjs +232 -0
  244. package/src/session-runtime/cwd-plugins.mjs +226 -0
  245. package/src/session-runtime/mcp-glue.mjs +177 -0
  246. package/src/session-runtime/model-recency.mjs +111 -0
  247. package/src/session-runtime/native-search.mjs +247 -0
  248. package/src/session-runtime/output-styles.mjs +11 -9
  249. package/src/session-runtime/prewarm.mjs +142 -0
  250. package/src/session-runtime/provider-models.mjs +278 -0
  251. package/src/session-runtime/provider-usage.mjs +120 -0
  252. package/src/session-runtime/quick-model-rows.mjs +205 -0
  253. package/src/session-runtime/quick-search-models.mjs +47 -0
  254. package/src/session-runtime/session-hooks.mjs +93 -0
  255. package/src/session-runtime/settings-api.mjs +352 -0
  256. package/src/session-runtime/tool-catalog.mjs +29 -29
  257. package/src/session-runtime/tool-defs.mjs +84 -0
  258. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  259. package/src/session-runtime/workflow.mjs +1 -1
  260. package/src/standalone/agent-tool/helpers.mjs +237 -0
  261. package/src/standalone/agent-tool/notify.mjs +107 -0
  262. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  263. package/src/standalone/agent-tool/render.mjs +152 -0
  264. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  265. package/src/standalone/agent-tool.mjs +138 -669
  266. package/src/standalone/channel-admin.mjs +102 -90
  267. package/src/standalone/channel-worker.mjs +4 -7
  268. package/src/standalone/explore-tool.mjs +64 -14
  269. package/src/standalone/hook-bus/config.mjs +207 -0
  270. package/src/standalone/hook-bus/constants.mjs +90 -0
  271. package/src/standalone/hook-bus/handlers.mjs +481 -0
  272. package/src/standalone/hook-bus/payload.mjs +31 -0
  273. package/src/standalone/hook-bus/rules.mjs +77 -0
  274. package/src/standalone/hook-bus.mjs +77 -870
  275. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  276. package/src/standalone/opencode-go-login.mjs +5 -1
  277. package/src/standalone/provider-admin.mjs +1 -16
  278. package/src/standalone/usage-dashboard.mjs +3 -1
  279. package/src/tui/App.jsx +1059 -8110
  280. package/src/tui/app/app-format.mjs +213 -0
  281. package/src/tui/app/channel-pickers.mjs +508 -0
  282. package/src/tui/app/clipboard.mjs +67 -0
  283. package/src/tui/app/core-memory-picker.mjs +210 -0
  284. package/src/tui/app/extension-pickers.mjs +506 -0
  285. package/src/tui/app/input-parsers.mjs +193 -0
  286. package/src/tui/app/maintenance-pickers.mjs +356 -0
  287. package/src/tui/app/model-options.mjs +334 -0
  288. package/src/tui/app/model-picker.mjs +365 -0
  289. package/src/tui/app/onboarding-steps.mjs +400 -0
  290. package/src/tui/app/project-picker.mjs +247 -0
  291. package/src/tui/app/provider-setup-picker.mjs +580 -0
  292. package/src/tui/app/resume-picker.mjs +55 -0
  293. package/src/tui/app/route-pickers.mjs +419 -0
  294. package/src/tui/app/settings-picker.mjs +489 -0
  295. package/src/tui/app/slash-commands.mjs +101 -0
  296. package/src/tui/app/slash-dispatch.mjs +427 -0
  297. package/src/tui/app/text-layout.mjs +46 -0
  298. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  299. package/src/tui/app/transcript-window.mjs +677 -0
  300. package/src/tui/app/use-mouse-input.mjs +460 -0
  301. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  302. package/src/tui/app/use-transcript-scroll.mjs +512 -0
  303. package/src/tui/app/use-transcript-window.mjs +607 -0
  304. package/src/tui/components/ConfirmBar.jsx +10 -7
  305. package/src/tui/components/Picker.jsx +64 -15
  306. package/src/tui/components/PromptInput.jsx +33 -102
  307. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  308. package/src/tui/components/StatusLine.jsx +69 -15
  309. package/src/tui/components/TextEntryPanel.jsx +11 -0
  310. package/src/tui/components/ToolExecution.jsx +52 -594
  311. package/src/tui/components/TranscriptItem.jsx +105 -0
  312. package/src/tui/components/UsagePanel.jsx +18 -4
  313. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  314. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  315. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  316. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  317. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  318. package/src/tui/display-width.mjs +20 -3
  319. package/src/tui/dist/index.mjs +13553 -12384
  320. package/src/tui/engine/agent-job-feed.mjs +133 -0
  321. package/src/tui/engine/notification-plan.mjs +76 -0
  322. package/src/tui/engine/render-timing.mjs +17 -0
  323. package/src/tui/engine/tool-approval.mjs +94 -0
  324. package/src/tui/engine/tool-card-results.mjs +234 -0
  325. package/src/tui/engine/tool-result-status.mjs +135 -0
  326. package/src/tui/engine.mjs +170 -574
  327. package/src/tui/figures.mjs +5 -0
  328. package/src/tui/index.jsx +65 -1
  329. package/src/tui/input-editing.mjs +2 -2
  330. package/src/tui/markdown/format-token.mjs +4 -1
  331. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  332. package/src/tui/theme.mjs +6 -0
  333. package/src/ui/statusline-agents.mjs +213 -0
  334. package/src/ui/statusline-format.mjs +146 -0
  335. package/src/ui/statusline-segments.mjs +148 -0
  336. package/src/ui/statusline.mjs +77 -501
  337. package/src/ui/tool-card.mjs +0 -1
  338. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  339. package/src/workflows/default/WORKFLOW.md +16 -18
  340. package/src/workflows/sequential/WORKFLOW.md +16 -18
  341. package/vendor/ink/build/display-width.js +19 -3
  342. package/vendor/ink/build/ink.js +112 -7
  343. package/vendor/ink/build/log-update.js +17 -3
  344. package/vendor/ink/build/wrap-text.js +125 -0
  345. package/scripts/_test-folder-dialog.mjs +0 -30
  346. package/scripts/fix-brief-fn.mjs +0 -35
  347. package/scripts/fix-format-tool-surface.mjs +0 -24
  348. package/scripts/fix-tool-exec-visible.mjs +0 -42
  349. package/scripts/patch-agent-brief.mjs +0 -48
  350. package/scripts/patch-app.mjs +0 -21
  351. package/scripts/patch-app2.mjs +0 -18
  352. package/scripts/patch-dist-brief.mjs +0 -96
  353. package/scripts/patch-tool-exec.mjs +0 -70
  354. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  355. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  356. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  357. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  358. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  359. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  360. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  361. package/src/runtime/channels/lib/holidays.mjs +0 -138
  362. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  363. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  364. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  365. package/src/standalone/agent-task-status.test.mjs +0 -76
  366. package/src/tui/components/tool-output-format.test.mjs +0 -399
  367. package/src/tui/display-width.test.mjs +0 -35
  368. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  369. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  370. package/src/tui/input-editing.selection.test.mjs +0 -75
  371. package/src/tui/markdown/format-token.test.mjs +0 -354
  372. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  373. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  374. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  375. package/src/tui/paste-fix.test.mjs +0 -119
  376. package/src/tui/prompt-history-store.test.mjs +0 -52
  377. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  378. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  379. package/src/ui/markdown.test.mjs +0 -70
  380. package/src/ui/statusline-context-label.test.mjs +0 -15
  381. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  382. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -213,6 +213,155 @@ export async function flushEmbeddingDirty(db, options = {}) {
213
213
  }
214
214
  }
215
215
 
216
+ // Embeds raw transcript rows (is_root = 0, non-chunk-member) so the dense
217
+ // recall leg can rank them alongside root/summary entries. Mirrors
218
+ // flushEmbeddingDirty's SKIP LOCKED batch-claim structure but sources the
219
+ // embed text from `content` (raw rows have no element/summary) and honors
220
+ // `limit` as a total cap across the whole call rather than an unbounded loop.
221
+ //
222
+ // Stale-embedding safety when recap toggles back ON: cycle1's promotion
223
+ // UPDATE (memory-cycle1.mjs chunk commit) SETs summary/element, which fires
224
+ // trg_entries_embedding_invalidate (memory.mjs — BEFORE UPDATE OF content,
225
+ // summary, element; NEW.is_root=1 AND summary IS DISTINCT FROM OLD.summary)
226
+ // and NULLs the raw-content embedding written here; cycle1's end-of-cycle
227
+ // flushEmbeddingDirty (is_root=1 AND embedding IS NULL) then re-embeds the
228
+ // root from its fresh element/summary. Member leaves keep their content
229
+ // embedding — the dense recall leg resolves members to their chunk root
230
+ // (memory-recall-store.mjs member→root resolution), so that is leaf-level
231
+ // dense matching, not staleness.
232
+ export async function flushRawEmbeddings(db, options = {}) {
233
+ const { limit = 200, signal } = options ?? {}
234
+ throwIfAborted(signal)
235
+ await ensureEmbCacheTable(db)
236
+ throwIfAborted(signal)
237
+
238
+ let attempted = 0
239
+ let embedded = 0
240
+ let cursor = 0
241
+
242
+ while (attempted < limit) {
243
+ throwIfAborted(signal)
244
+ const batchCap = Math.min(BATCH_SIZE, limit - attempted)
245
+ const client = await db._pool.connect()
246
+ let ids
247
+ try {
248
+ throwIfAborted(signal)
249
+ await client.query('BEGIN')
250
+ const res = await client.query(
251
+ `SELECT id FROM memory.entries
252
+ WHERE is_root = 0 AND chunk_root IS NULL AND embedding IS NULL
253
+ AND NULLIF(btrim(session_id), '') IS NOT NULL
254
+ AND content IS NOT NULL
255
+ AND id > $2
256
+ ORDER BY id
257
+ LIMIT $1
258
+ FOR UPDATE SKIP LOCKED`,
259
+ [batchCap, cursor],
260
+ )
261
+ ids = res.rows.map(r => Number(r.id))
262
+ throwIfAborted(signal)
263
+ } catch (err) {
264
+ try { await client.query('ROLLBACK') } catch {}
265
+ client.release()
266
+ if (signal?.aborted) throw signal.reason ?? err
267
+ __mixdogMemoryLog(`[embed] raw flush SKIP LOCKED claim failed: ${err.message}\n`)
268
+ break
269
+ }
270
+
271
+ if (ids.length === 0) {
272
+ try { await client.query('COMMIT') } catch {}
273
+ client.release()
274
+ break
275
+ }
276
+
277
+ cursor = ids[ids.length - 1]
278
+ attempted += ids.length
279
+ let batchDone = false
280
+ try {
281
+ throwIfAborted(signal)
282
+ const writtenIds = await syncRawBatchEmbeddings(client, ids, { signal })
283
+ embedded += writtenIds.length
284
+ batchDone = true
285
+ } catch (err) {
286
+ if (signal?.aborted) throw signal.reason ?? err
287
+ __mixdogMemoryLog(`[embed] raw batch failed (ids=${ids[0]}..${ids[ids.length-1]}): ${err.message}\n`)
288
+ } finally {
289
+ try {
290
+ if (batchDone) await client.query('COMMIT')
291
+ else await client.query('ROLLBACK')
292
+ } catch {}
293
+ client.release()
294
+ }
295
+
296
+ if (ids.length < batchCap) break
297
+ }
298
+
299
+ return { attempted, embedded }
300
+ }
301
+
302
+ // Batch-embed raw transcript rows: content is the embed source text (raw
303
+ // rows have no element/summary). Mirrors syncBatchEmbeddings' shape.
304
+ async function syncRawBatchEmbeddings(db, ids, options = {}) {
305
+ const signal = options?.signal
306
+ throwIfAborted(signal)
307
+ const rows = (await db.query(
308
+ `SELECT id, content FROM memory.entries
309
+ WHERE id = ANY($1::bigint[]) AND is_root = 0 AND chunk_root IS NULL`,
310
+ [ids],
311
+ )).rows
312
+ throwIfAborted(signal)
313
+ if (rows.length === 0) return []
314
+
315
+ const dimsRow = (await db.query(`SELECT value FROM memory.meta WHERE key = 'embedding.current_dims'`)).rows[0]
316
+ throwIfAborted(signal)
317
+ const expected = Number(dimsRow?.value ?? 0)
318
+
319
+ const texts = rows.map(row => (typeof row.content === 'string' ? row.content.trim() : ''))
320
+ const vectors = await cachedEmbedTextBatch(db, texts.filter(Boolean), { signal })
321
+ throwIfAborted(signal)
322
+ let vecIdx = 0
323
+ const updates = [] // { id, content, vector }
324
+ for (let i = 0; i < rows.length; i++) {
325
+ throwIfAborted(signal)
326
+ const row = rows[i]
327
+ if (!texts[i]) continue
328
+ const vector = vectors[vecIdx++]
329
+ if (!Array.isArray(vector) || vector.length === 0) continue
330
+ if (Number.isFinite(expected) && expected > 0 && vector.length !== expected) {
331
+ __mixdogMemoryLog(`[embed] raw dim mismatch (id=${row.id} got=${vector.length} expected=${expected})\n`)
332
+ continue
333
+ }
334
+ updates.push({ id: Number(row.id), content: row.content, vector })
335
+ }
336
+
337
+ if (updates.length === 0) return []
338
+
339
+ const valClauses = updates.map((u, i) => {
340
+ const base = i * 3
341
+ return `($${base+1}::bigint, $${base+2}::halfvec, $${base+3}::text)`
342
+ })
343
+ const params = []
344
+ for (const u of updates) {
345
+ params.push(u.id, embeddingToSql(u.vector), u.content)
346
+ }
347
+ const res = await db.query(
348
+ `UPDATE memory.entries AS e
349
+ SET embedding = t.vector
350
+ FROM (VALUES ${valClauses.join(',')}) AS t(id, vector, content)
351
+ WHERE e.id = t.id AND e.is_root = 0 AND e.chunk_root IS NULL
352
+ AND e.embedding IS NULL
353
+ AND e.content IS NOT DISTINCT FROM t.content
354
+ RETURNING e.id`,
355
+ params,
356
+ )
357
+ throwIfAborted(signal)
358
+ const writtenIds = (res.rows || []).map(r => Number(r.id))
359
+ if (writtenIds.length < updates.length) {
360
+ __mixdogMemoryLog(`[embed-sync] ${updates.length - writtenIds.length} raw stale-write(s) skipped — content changed since read\n`)
361
+ }
362
+ return writtenIds
363
+ }
364
+
216
365
  // Batch-embed all ids in one DB round-trip for fetching, one API call per unique text,
217
366
  // then one VALUES UPDATE for all results.
218
367
  // Returns the list of ids that were actually UPDATEd with a fresh embedding.
@@ -0,0 +1,162 @@
1
+ // Extracted from index.mjs — single/multi-instance process-lock + owner-election
2
+ // helpers. Pure functions parameterized on lock-file paths so index.mjs retains
3
+ // ownership of DATA_DIR-derived constants and the daemon logger.
4
+ import fs from 'node:fs'
5
+ import { execFileSync } from 'node:child_process'
6
+
7
+ export function parsePositivePid(value) {
8
+ const pid = Number(value)
9
+ return Number.isFinite(pid) && pid > 0 ? pid : null
10
+ }
11
+
12
+ export function isPidAliveLocal(pid) {
13
+ if (!Number.isFinite(pid) || pid <= 0) return false
14
+ try { process.kill(pid, 0); return true }
15
+ catch (e) { return e.code !== 'ESRCH' }
16
+ }
17
+
18
+ export function tryAcquireMemoryOwnerLock(ownerLockFile, log = () => {}) {
19
+ // Returns true on success (this process now owns memory worker for the data
20
+ // dir), false when a live peer holds the lock. Stale locks (dead PID) are
21
+ // unlinked and retried atomically. Throws on unexpected fs errors so callers
22
+ // surface lock-system corruption rather than silently downgrading.
23
+ //
24
+ // EPERM/EBUSY/EACCES at openSync are transient — AV scanners (SignKorea /
25
+ // SKCert / ezPDFWS etc) briefly lock newly-created files during inspection.
26
+ // The 0.1.x baseline threw immediately and the worker promoted to
27
+ // permanentlyDegraded, killing memory tools for the rest of the session.
28
+ // Treat the AV error codes as retryable with bounded backoff (~750ms total)
29
+ // before giving up and rethrowing.
30
+ for (let attempt = 0; attempt < 5; attempt++) {
31
+ try {
32
+ const fd = fs.openSync(ownerLockFile, 'wx')
33
+ fs.writeSync(fd, String(process.pid))
34
+ fs.closeSync(fd)
35
+ return true
36
+ } catch (e) {
37
+ if (e.code === 'EEXIST') {
38
+ let ownerPid = NaN
39
+ try { ownerPid = Number(fs.readFileSync(ownerLockFile, 'utf8').trim()) } catch {}
40
+ if (isPidAliveLocal(ownerPid)) return false
41
+ // Stale lock: dead owner — unlink and retry exclusive create.
42
+ try { fs.unlinkSync(ownerLockFile) } catch {}
43
+ continue
44
+ }
45
+ const transient = e.code === 'EPERM' || e.code === 'EBUSY' || e.code === 'EACCES'
46
+ if (transient && attempt < 4) {
47
+ // Sync busy-wait acceptable here: this runs on memory worker boot
48
+ // path, once per process; the parent handler is not blocked.
49
+ const end = Date.now() + 50 * (attempt + 1)
50
+ while (Date.now() < end) {}
51
+ continue
52
+ }
53
+ throw e
54
+ }
55
+ }
56
+ return false
57
+ }
58
+
59
+ export function releaseMemoryOwnerLock(ownerLockFile) {
60
+ try {
61
+ const ownerPid = Number(fs.readFileSync(ownerLockFile, 'utf8').trim())
62
+ if (ownerPid === process.pid) fs.unlinkSync(ownerLockFile)
63
+ } catch {}
64
+ }
65
+
66
+ export function killPreviousServer(pid, log = () => {}) {
67
+ if (pid <= 0 || pid === process.pid) return false
68
+ if (process.platform === 'win32') {
69
+ try {
70
+ execFileSync('taskkill', ['/F', '/T', '/PID', String(pid)], { encoding: 'utf8', timeout: 5000, windowsHide: true })
71
+ log(`[memory-service] Killed previous server PID ${pid}\n`)
72
+ return true
73
+ } catch (e) {
74
+ // Exit code 128 = process not found; treat stale lock as already-dead = success.
75
+ // Status 128 reliably means "process not found" regardless of locale; no text match needed.
76
+ // Status 1 with English text match handles edge cases on some Windows versions.
77
+ const notFoundText = /not found|no running instance/i.test(e.stdout || '')
78
+ || /not found|no running instance/i.test(e.stderr || '')
79
+ || /not found|no running instance/i.test(e.message || '')
80
+ const alreadyDead = e.status === 128 || (e.status === 1 && notFoundText)
81
+ if (alreadyDead) {
82
+ log(`[memory-service] PID ${pid} already dead (stale lock), proceeding\n`)
83
+ return true
84
+ }
85
+ log(`[memory-service] taskkill failed for PID ${pid}: ${e.message}\n`)
86
+ return false
87
+ }
88
+ } else {
89
+ // Pre-flight: if the process is already gone, treat stale lock as success.
90
+ try {
91
+ process.kill(pid, 0)
92
+ } catch (e) {
93
+ if (e.code === 'ESRCH') {
94
+ log(`[memory-service] PID ${pid} already dead (stale lock), proceeding\n`)
95
+ return true
96
+ }
97
+ }
98
+ try { process.kill(pid, 'SIGTERM') } catch {}
99
+ try { process.kill(pid, 'SIGKILL') } catch {}
100
+ // Poll for death up to 2s
101
+ const deadline = Date.now() + 2000
102
+ while (Date.now() < deadline) {
103
+ try {
104
+ process.kill(pid, 0)
105
+ } catch (e) {
106
+ if (e.code === 'ESRCH') {
107
+ log(`[memory-service] Killed previous server PID ${pid}\n`)
108
+ return true
109
+ }
110
+ }
111
+ // Synchronous 50ms sleep via shared buffer spin
112
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50)
113
+ }
114
+ log(`[memory-service] PID ${pid} still alive after SIGKILL\n`)
115
+ return false
116
+ }
117
+ }
118
+
119
+ export function acquireLock(lockFile, log = () => {}) {
120
+ // Multi-instance guard. In multi-terminal mode the lock owner is a *peer*
121
+ // memory worker serving recall for another CC session. killPreviousServer
122
+ // would taskkill /F that healthy peer mid-flight, then this fork-proxy
123
+ // mode wouldn't even need a lock anyway. Skip the entire kill-the-previous
124
+ // protocol; fork-proxy detection in init() takes priority. If neither
125
+ // proxy nor lock-owner path applies (race window during simultaneous
126
+ // boot), the worker simply continues without the lock — server-main /
127
+ // PG / port-listen handle the actual conflict cases.
128
+ if (process.env.MIXDOG_MULTI_INSTANCE === '1') return
129
+ try {
130
+ if (fs.existsSync(lockFile)) {
131
+ const lockedPid = Number(fs.readFileSync(lockFile, 'utf8').trim())
132
+ if (lockedPid > 0 && lockedPid !== process.pid) {
133
+ const killed = killPreviousServer(lockedPid, log)
134
+ if (!killed) {
135
+ log(`[memory-service] Could not kill previous server PID ${lockedPid}, aborting\n`)
136
+ process.exit(1)
137
+ }
138
+ try { fs.unlinkSync(lockFile) } catch {}
139
+ }
140
+ }
141
+ const fd = fs.openSync(lockFile, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600)
142
+ try {
143
+ fs.writeSync(fd, String(process.pid))
144
+ } finally {
145
+ fs.closeSync(fd)
146
+ }
147
+ } catch (e) {
148
+ if (e.code === 'EEXIST') {
149
+ log(`[memory-service] Lock file exists (EEXIST) — another instance is already running, exiting\n`)
150
+ process.exit(0)
151
+ }
152
+ log(`[memory-service] Lock acquisition failed: ${e.message}\n`)
153
+ process.exit(1)
154
+ }
155
+ }
156
+
157
+ export function releaseLock(lockFile) {
158
+ try {
159
+ const content = fs.readFileSync(lockFile, 'utf8').trim()
160
+ if (Number(content) === process.pid) fs.unlinkSync(lockFile)
161
+ } catch {}
162
+ }
@@ -4,7 +4,7 @@ function __mixdogMemoryLog(...args) {
4
4
  return __mixdogMemoryStderrWrite(...args);
5
5
  }
6
6
 
7
- import { buildFtsQuery } from './memory-text-utils.mjs'
7
+ import { buildFtsQuery, buildFtsPrefixQuery } from './memory-text-utils.mjs'
8
8
  import { VALID_CATEGORY, embeddingToSql } from './memory.mjs'
9
9
  import { freshnessFactor } from './memory-score.mjs'
10
10
  import { buildRecallScopeFilter } from './memory-recall-scope-filter.mjs'
@@ -15,7 +15,34 @@ import { recallReadQuery } from './memory-recall-read-query.mjs'
15
15
  const _MV_HOT_ACTIVE_TTL_MS = 60_000
16
16
  const _mvHotActiveCache = new WeakMap() // db → { populated: boolean, ts: number }
17
17
  const SEMANTIC_ONLY_MIN_SIM = 0.72
18
+ // Cross-language semantic rescue: a query in one language against rows in
19
+ // another rarely clears SEMANTIC_ONLY_MIN_SIM on a small (384-dim) embedding
20
+ // model, yet the TOP dense ranks are still overwhelmingly relevant. Accept
21
+ // semantic-only rows that are both top-ranked in the dense leg AND above a
22
+ // softer floor, instead of dropping everything below 0.72.
23
+ const SEMANTIC_TOP_RANK_MAX = 3
24
+ // Floor for the top-rank rescue. 384-dim MiniLM-class embeddings put UNRELATED
25
+ // text at ~0.65-0.75 sim (measured: gibberish query "zzqqxx ... 999" scored
26
+ // 0.749 against real rows), so anything below ~0.70 is noise regardless of
27
+ // rank; cross-language relevant pairs measured 0.67-0.74. 0.70 keeps most of
28
+ // the cross-language rescue while rejecting pure-noise top ranks.
29
+ const SEMANTIC_TOP_RANK_MIN_SIM = 0.70
18
30
  const SHORT_QUERY_TOKEN_MAX = 2
31
+ // Dense-similarity weighting for retrievalScore. Lexical RRF sums 1/(K+rank)
32
+ // per matching leg (K=60), so a row hitting 2 lexical legs at rank 1 scores
33
+ // rrf ≈ 2/(60+1) = 0.0328 — enough to bury a single semantically-close dense
34
+ // hit that never shares lexical terms. We add a normalized dense-similarity
35
+ // term so a strong dense-only match can outrank stale multi-leg lexical hits:
36
+ // denseTerm = W_DENSE * max(0, sim - SIM_FLOOR) / (1 - SIM_FLOOR) * freshness
37
+ // At sim=0.85: (0.85-0.60)/(1-0.60) = 0.625, so denseTerm = 0.04 * 0.625 = 0.025
38
+ // with freshness=1. That alone doesn't beat 0.0328, but combined with the
39
+ // row's own rrf contribution (a dense-only row still has 1/(60+rank) from its
40
+ // dense rank, e.g. rank 1 → 0.0164) the total ≈ 0.0164 + 0.025 = 0.0414,
41
+ // which clears the 0.0328 two-leg lexical baseline. W_DENSE=0.04 was chosen
42
+ // as the smallest round value satisfying this at sim=0.85; SIM_FLOOR=0.60
43
+ // matches the noise floor used elsewhere for MiniLM-class embeddings.
44
+ const SIM_FLOOR = 0.60
45
+ const W_DENSE = 0.04
19
46
 
20
47
  function buildExactTerms(query) {
21
48
  const clean = String(query ?? '').replace(/\s+/g, ' ').trim()
@@ -62,7 +89,12 @@ function hasFullQueryTextMatch(query, row) {
62
89
  function hasQueryTokenCoverage(row, queryTokenCount) {
63
90
  if (queryTokenCount <= SHORT_QUERY_TOKEN_MAX) return true
64
91
  const hits = Number(row?.exact_hits)
65
- return Number.isFinite(hits) && hits >= queryTokenCount
92
+ // Majority coverage, not full coverage. Requiring EVERY query token to hit
93
+ // made mixed-language queries ("usage spike 원인") return nothing when one
94
+ // token has no lexical counterpart in the row — ~60% of tokens matching is
95
+ // strong enough lexical evidence alongside the semantic leg.
96
+ const required = Math.max(2, Math.ceil(queryTokenCount * 0.6))
97
+ return Number.isFinite(hits) && hits >= required
66
98
  }
67
99
 
68
100
  function exactTextBoost(query, row, exactHits) {
@@ -213,7 +245,13 @@ export async function searchRelevantHybrid(db, query, options = {}) {
213
245
  ? embeddingToSql(options.queryVector)
214
246
  : null
215
247
 
216
- const ftsQuery = clean.length >= 3 ? (buildFtsQuery(clean) ?? null) : null
248
+ // Prefer the Kiwi morph prefix-form query (to_tsquery, ':*' prefix match on
249
+ // content-morpheme stems) when kiwi is ready; else fall back to the plain
250
+ // websearch_to_tsquery path. ftsPrefixMode drives which tsquery ctor the
251
+ // sparse CTE uses (to_tsquery vs websearch_to_tsquery).
252
+ const ftsPrefix = clean.length >= 3 ? buildFtsPrefixQuery(clean) : null
253
+ const ftsQuery = ftsPrefix ? ftsPrefix.query : (clean.length >= 3 ? (buildFtsQuery(clean) ?? null) : null)
254
+ const ftsPrefixMode = Boolean(ftsPrefix)
217
255
  const exactTerms = buildExactTerms(clean)
218
256
  const queryTokenCount = countQueryTokens(clean)
219
257
  const minExactHits = exactTerms.length >= 8 ? 3 : exactTerms.length >= 4 ? 2 : 1
@@ -312,23 +350,29 @@ dense AS (SELECT NULL::bigint AS id, NULL::float8 AS sim, NULL::bigint AS dense_
312
350
 
313
351
  // sparse CTE: active only when ftsQuery is non-null.
314
352
  // useHotActive → queries mv_hot_active GIN index (mv_hot_active_tsv).
353
+ // tsqExpr: to_tsquery for the Kiwi morph prefix-form query ('stem:* & ...'),
354
+ // else websearch_to_tsquery for the plain fallback token string. Both parse
355
+ // $2 under the 'simple' config to match search_tsv's simple-config lexemes.
356
+ const tsqExpr = ftsPrefixMode
357
+ ? `to_tsquery('simple', $2)`
358
+ : `websearch_to_tsquery('simple', $2)`
315
359
  const sparseCte = ftsQuery ? (useHotActive ? `
316
360
  sparse AS (
317
361
  SELECT id,
318
- ts_rank_cd(search_tsv, websearch_to_tsquery('simple', $2)) AS lex,
319
- ROW_NUMBER() OVER (ORDER BY ts_rank_cd(search_tsv, websearch_to_tsquery('simple', $2)) DESC) AS sparse_rank
362
+ ts_rank_cd(search_tsv, ${tsqExpr}) AS lex,
363
+ ROW_NUMBER() OVER (ORDER BY ts_rank_cd(search_tsv, ${tsqExpr}) DESC) AS sparse_rank
320
364
  FROM mv_hot_active
321
- WHERE search_tsv @@ websearch_to_tsquery('simple', $2)
365
+ WHERE search_tsv @@ ${tsqExpr}
322
366
  ${mvFilterClause}
323
367
  ORDER BY lex DESC
324
368
  LIMIT $4
325
369
  ),` : `
326
370
  sparse AS (
327
371
  SELECT id,
328
- ts_rank_cd(search_tsv, websearch_to_tsquery('simple', $2)) AS lex,
329
- ROW_NUMBER() OVER (ORDER BY ts_rank_cd(search_tsv, websearch_to_tsquery('simple', $2)) DESC) AS sparse_rank
372
+ ts_rank_cd(search_tsv, ${tsqExpr}) AS lex,
373
+ ROW_NUMBER() OVER (ORDER BY ts_rank_cd(search_tsv, ${tsqExpr}) DESC) AS sparse_rank
330
374
  FROM entries
331
- WHERE search_tsv @@ websearch_to_tsquery('simple', $2)
375
+ WHERE search_tsv @@ ${tsqExpr}
332
376
  ${filterClause}
333
377
  ORDER BY lex DESC
334
378
  LIMIT $4
@@ -472,14 +516,21 @@ LEFT JOIN exact x ON x.id = c.id`
472
516
  + (exactRank ? 1 / (K + exactRank) : 0)
473
517
  const freshness = applyFreshness ? freshnessFactor(row.ts, nowMs) : 1.0
474
518
  const boost = exactTextBoost(clean, row, row.exact_hits)
475
- return { id, row, rrf, freshness, retrievalScore: (rrf * freshness) + boost }
519
+ const sim = Number(row.dense_sim)
520
+ const denseTerm = Number.isFinite(sim)
521
+ ? W_DENSE * Math.max(0, sim - SIM_FLOOR) / (1 - SIM_FLOOR) * freshness
522
+ : 0
523
+ return { id, row, rrf, freshness, retrievalScore: (rrf * freshness) + boost + denseTerm }
476
524
  })
477
525
  let semanticOnlyDropped = 0
478
526
  let weakTextDropped = 0
479
527
  const scored = scoredAll.filter(({ row }) => {
480
528
  const hasTextSupport = row.sparse_rank != null || row.trgm_rank != null || row.exact_rank != null
481
529
  const sim = Number(row.dense_sim)
482
- const hasSemanticSupport = Number.isFinite(sim) && sim >= SEMANTIC_ONLY_MIN_SIM
530
+ const denseRankNum = row.dense_rank != null ? Number(row.dense_rank) : null
531
+ const hasSemanticSupport = (Number.isFinite(sim) && sim >= SEMANTIC_ONLY_MIN_SIM)
532
+ || (Number.isFinite(sim) && sim >= SEMANTIC_TOP_RANK_MIN_SIM
533
+ && denseRankNum != null && denseRankNum <= SEMANTIC_TOP_RANK_MAX)
483
534
  if (!hasTextSupport) {
484
535
  if (hasSemanticSupport) return true
485
536
  semanticOnlyDropped += 1
@@ -495,7 +546,13 @@ LEFT JOIN exact x ON x.id = c.id`
495
546
  if (hasFullPhrase) return true
496
547
  if (queryTokenCount <= SHORT_QUERY_TOKEN_MAX) return true
497
548
  if (row.sparse_rank != null) return true
498
- if (hasSemanticSupport && hasQueryTokenCoverage(row, queryTokenCount)) return true
549
+ // Semantic support as a second signal only applies when the dense leg
550
+ // actually ran. With a cold embedding model (no queryVector) semantic
551
+ // support is unattainable, and requiring it silently zeroed every
552
+ // multi-token lexical recall until warmup finished. Token coverage alone
553
+ // carries the filter in that degraded mode.
554
+ const semanticLegActive = vecSql != null
555
+ if ((!semanticLegActive || hasSemanticSupport) && hasQueryTokenCoverage(row, queryTokenCount)) return true
499
556
  weakTextDropped += 1
500
557
  return false
501
558
  })
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  cleanMemoryText,
3
3
  } from './memory-extraction.mjs'
4
+ import { isReady as koMorphReady, stems as koMorphStems } from './ko-morph.mjs'
4
5
 
5
6
  const MEMORY_TOKEN_STOPWORDS = new Set([
6
7
  'a', 'an', 'and', 'are', 'as', 'at', 'be', 'but', 'by', 'did', 'do', 'does', 'for', 'from',
@@ -55,4 +56,49 @@ export function buildFtsQuery(text) {
55
56
  return ftsTokens.map(t => t.replace(/["']/g, '')).filter(t => t.length > 0).join(' ')
56
57
  }
57
58
 
59
+ const HANGUL_RE = /[\uAC00-\uD7AF]/
60
+
61
+ // Sanitize a single lexeme for embedding inside a to_tsquery string. Strips the
62
+ // tsquery operator characters so a raw token can never inject syntax.
63
+ function sanitizeLexeme(t) {
64
+ return String(t ?? '').replace(/[&|!():*'"\\\s]+/g, '')
65
+ }
66
+
67
+ // Prefix-form (to_tsquery) builder. Returns null when kiwi morph is not ready,
68
+ // signalling the caller to keep the websearch_to_tsquery fallback path. When
69
+ // ready: Korean tokens → Kiwi content-morpheme stems (NNG/NNP/VV/VA/XR/SL),
70
+ // non-Korean tokens kept as-is; every lexeme gets a ':*' prefix match and the
71
+ // set is '&'-joined to preserve the current AND semantics.
72
+ //
73
+ // Returns { query, prefix:true } on success, or null.
74
+ export function buildFtsPrefixQuery(text) {
75
+ if (!koMorphReady()) return null
76
+ const tokens = tokenizeMemoryText(text)
77
+ if (tokens.length === 0) return null
78
+ const ftsTokens = [...new Set(tokens)].filter(t => t.length >= 3 || (t.length === 2 && HANGUL_RE.test(t)))
79
+ if (ftsTokens.length === 0) return null
80
+
81
+ const lexemes = []
82
+ const seen = new Set()
83
+ for (const tok of ftsTokens) {
84
+ if (HANGUL_RE.test(tok)) {
85
+ // Analyze against the ORIGINAL token (pre suffix-strip) so Kiwi sees the
86
+ // real inflection; normalizeMemoryToken already ran, so also feed the
87
+ // stripped form — union of both keeps recall high.
88
+ const st = koMorphStems(tok)
89
+ const forms = (st && st.length > 0) ? st : [tok]
90
+ for (const f of forms) {
91
+ const lex = sanitizeLexeme(f)
92
+ if (lex.length >= 1 && !seen.has(lex)) { seen.add(lex); lexemes.push(lex) }
93
+ }
94
+ } else {
95
+ const lex = sanitizeLexeme(tok)
96
+ if (lex.length >= 1 && !seen.has(lex)) { seen.add(lex); lexemes.push(lex) }
97
+ }
98
+ }
99
+ if (lexemes.length === 0) return null
100
+ const query = lexemes.map(l => `${l}:*`).join(' & ')
101
+ return { query, prefix: true }
102
+ }
103
+
58
104
 
@@ -194,7 +194,7 @@ function patchActiveInstance(fields) {
194
194
  else merged[k] = v;
195
195
  }
196
196
  return merged;
197
- }, { compact: true, fsyncDir: true });
197
+ }, { compact: true, fsyncDir: true, renameFallback: 'truncate' });
198
198
  } catch (e) {
199
199
  __mixdogMemoryLog(`[supervisor-pg] patchActiveInstance failed: ${e?.message}\n`);
200
200
  }