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
@@ -1,842 +1,30 @@
1
- const __mixdogMemoryStderrWrite = process.stderr.write.bind(process.stderr);
2
- function __mixdogMemoryLog(...args) {
3
- if (process.env.MIXDOG_QUIET_MEMORY_LOG) return true;
4
- return __mixdogMemoryStderrWrite(...args);
5
- }
6
-
7
- import { existsSync, readFileSync } from 'fs'
8
- import { join } from 'path'
9
- import { fileURLToPath } from 'url'
10
- import { resolveMaintenancePreset } from '../../shared/llm/index.mjs'
11
- import { callAgentDispatch } from './agent-ipc.mjs'
12
- import {
13
- syncRootEmbedding, deleteRootEmbedding, flushEmbeddingDirty,
14
- } from './memory-embed.mjs'
15
- import { listCore, backfillCoreEmbeddings, CORE_SUMMARY_MAX } from './core-memory-store.mjs'
1
+ // memory-cycle2.mjs facade for the cycle2 maintenance cluster.
2
+ // The bulk was split into cohesive sub-modules; this file keeps the top-level
3
+ // orchestrator (runCycle2 / _runCycle2Impl) plus parseInterval, and re-exports
4
+ // the extracted members so existing importers resolve unchanged:
5
+ // memory-cycle2-shared.mjs — log/abort/resourceDir helpers
6
+ // memory-cycle2-mutations.mjs — status/update/merge writers + runPhaseMerge
7
+ // memory-cycle2-gate.mjs — prompt/parse/validate + runUnifiedGate + cascade
8
+ import { flushEmbeddingDirty } from './memory-embed.mjs'
9
+ import { backfillCoreEmbeddings, nominateCoreCandidates, CORE_SUMMARY_MAX } from './core-memory-store.mjs'
16
10
  import { markCycleRequest, consumeCycleRequests, resolveCoalesceMaxDrains, scheduleCoalescedCycleRetry, makeCycleRequestSignature, resolveCoalesceMaxRetries } from './memory-cycle-requests.mjs'
17
-
18
- export const CYCLE2_ACTIVE_TARGET_CAP = 100
19
- const TIER1_THRESHOLD = 0.78
20
-
21
- const TIER2_LOW = 0.65
22
- const LLM_JUDGE_CAP = 20
23
-
24
- function throwIfAborted(signal) {
25
- if (signal?.aborted) throw signal.reason ?? new Error('aborted')
26
- }
27
-
28
- // Status-based verb whitelist. 3-tier policy: pending → active/archived,
29
- // active → active/archived/update/merge.
30
- const STATUS_ALLOWED_VERBS = {
31
- pending: new Set(['active', 'archived']),
32
- active: new Set(['active', 'archived', 'update', 'merge']),
33
- }
34
- const NON_ARCHIVE_VERBS = new Set(['active', 'update', 'merge'])
35
- // Union of every primary (status) verb across all statuses, plus the two
36
- // non-verb line kinds. Used by the stray-index shift guard to decide whether
37
- // a `idx|id|verb` line had a leading row index prepended by the LLM.
38
- const ALL_PRIMARY_VERBS = new Set(['active', 'archived', 'update', 'merge'])
39
- const isShiftFollowToken = (tok) => {
40
- const v = String(tok ?? '').trim().toLowerCase()
41
- return ALL_PRIMARY_VERBS.has(v) || v === 'why' || v === 'core'
42
- }
43
-
44
- function resourceDir() {
45
- return process.env.MIXDOG_ROOT || fileURLToPath(new URL('../../../..', import.meta.url))
46
- }
47
-
48
- async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispatch) {
49
- return await llmCall({
50
- agent: 'cycle2-agent',
51
- taskType: 'maintenance',
52
- mode,
53
- preset,
54
- timeout,
55
- cwd: null,
56
- }, prompt)
57
- }
58
-
59
- function buildPidMap(rowSets) {
60
- const pids = [...new Set(rowSets.flat().map(r => r.project_id).filter(Boolean))].sort()
61
- return new Map(pids.map((p, i) => [p, `P${i + 1}`]))
62
- }
63
-
64
- function formatEntriesForPromotePrompt(rows, pidMap, opts = {}) {
65
- if (!rows || rows.length === 0) return '(none)'
66
- const map = pidMap ?? buildPidMap([rows])
67
- // When numbered, prefix each row with its 1-based prompt-order ordinal so the
68
- // gate LLM can echo a row number it can see, instead of inventing one. The
69
- // ordinal domain (1..N) and the 5-digit batch-id domain must stay disjoint —
70
- // see the ordinalToId invariant in runUnifiedGate.
71
- const numbered = opts.numbered === true
72
- const lines = rows.map((r, i) => {
73
- const tag = r.project_id ? (map.get(r.project_id) ?? 'C') : 'C'
74
- const stat = r.status ? `[${r.status}]` : '[?]'
75
- const prefix = numbered ? `${i + 1}. ` : '- '
76
- return `${prefix}id:${r.id} ${stat} ${tag} ${r.category} s:${r.score ?? 'n'} el:${r.element} sm:${String(r.summary || '').slice(0, 100)}`
77
- })
78
- if (map.size === 0) return lines.join('\n')
79
- const legend = [...map.entries()].map(([p, t]) => `${t}=${p}`).concat('C=COMMON').join(', ')
80
- return `# pid: ${legend}\n` + lines.join('\n')
81
- }
82
-
83
- // User-curated rows from core_entries — id-less, no status, no score; the
84
- // LLM only needs element + summary + project tag to detect overlap with
85
- // candidate entries below. Format kept terse so the prompt budget stays small.
86
- function formatUserCoreForPrompt(rows, pidMap) {
87
- if (!rows || rows.length === 0) return '(none)'
88
- const map = pidMap ?? new Map()
89
- return rows.map(r => {
90
- const tag = r.project_id ? (map.get(r.project_id) ?? 'C') : 'C'
91
- const sm = String(r.summary || '').slice(0, 200)
92
- return `- ${tag} ${r.category}: ${r.element}${sm && sm !== r.element ? ` — ${sm}` : ''}`
93
- }).join('\n')
94
- }
95
-
96
- // Parse pipe-format unified verdicts. Each line: <id>|<verb> [|...].
97
- // Verbs validated against the row's current status via STATUS_ALLOWED_VERBS.
98
- // Returns { actions, rejected } or null when no parseable lines.
99
- function parseUnifiedFormat(raw, statusById, ordinalToId = null) {
100
- if (raw == null) return null
101
- const text = String(raw).trim()
102
- if (!text) return { actions: [], rejected: new Set() }
103
- const lines = text.split('\n')
104
- const actions = []
105
- const rejected = new Set()
106
- const support = new Map()
107
- let sawValid = false
108
- // Resolve a first-field/merge token to a real batch id. The gate may echo
109
- // either the exact 5-digit batch id OR the 1-based row ordinal shown in the
110
- // numbered Entries block. The two domains are disjoint (asserted in
111
- // runUnifiedGate), so an exact-id hit always wins and an unmatched value
112
- // falls back to ordinal lookup; anything else is NaN (line treated invalid).
113
- const resolveId = (tok) => {
114
- const n = Number(String(tok ?? '').trim())
115
- if (!Number.isFinite(n)) return NaN
116
- if (statusById.has(n)) return n
117
- if (ordinalToId && ordinalToId.has(n)) return ordinalToId.get(n)
118
- return NaN
119
- }
120
- for (const rawLine of lines) {
121
- const line = rawLine.trim()
122
- if (!line) continue
123
- if (line.startsWith('//') || line.startsWith('#')) continue
124
- if (line.startsWith('```')) continue
125
- const parts = line.split('|')
126
- if (parts.length < 2) continue
127
- // LLM sometimes prefixes a row index, emitting `idx|id|verdict` instead of
128
- // `id|verdict`; parts[0] (the index) is a stray token and the line must be
129
- // shifted before parsing. Strict invariant so a real 2-field `id|verdict`
130
- // is never shifted into a 1-field line (which would throw on parts[1]):
131
- // parts.length >= 3 AND parts[1] is a known batch id AND parts[2] is a
132
- // valid primary verb / why / core (the shifted verdict slot).
133
- // Trigger when EITHER parts[0] is not a known id (classic stray index) OR
134
- // parts[0] IS known but parts[1] is not itself a valid verb — that covers
135
- // `1|1|active`, where the stray index collides with a real batch id and the
136
- // un-shifted reading would verb-reject the wrong row.
137
- if (
138
- parts.length >= 3 &&
139
- statusById.has(Number(parts[1].trim())) &&
140
- isShiftFollowToken(parts[2]) &&
141
- (!statusById.has(Number(parts[0].trim())) || !isShiftFollowToken(parts[1]))
142
- ) {
143
- parts.shift()
144
- }
145
- const entryId = resolveId(parts[0])
146
- const action = parts[1].trim().toLowerCase()
147
- if (!Number.isFinite(entryId) || !action) continue
148
- const status = statusById.get(entryId)
149
- if (!status) continue
150
- // Only mark as parse-ok when the id is known to the batch; a response
151
- // composed entirely of unknown ids would otherwise return parse-ok with
152
- // zero actions/rejections, leaving the rows un-reviewed and re-queued.
153
- sawValid = true
154
- if (action === 'core') {
155
- actions.push({ entry_id: entryId, action: 'core', core_summary: parts.slice(2).join('|').trim().slice(0, 120) })
156
- continue
157
- }
158
- if (action === 'why') {
159
- const kind = (parts[2] ?? '').trim().toUpperCase()
160
- const reason = parts.slice(3).join('|').replace(/\s+/g, ' ').trim().slice(0, 240)
161
- if ((kind === 'A' || kind === 'B') && reason) {
162
- support.set(entryId, { kind, reason })
163
- }
164
- continue
165
- }
166
- const allowed = STATUS_ALLOWED_VERBS[status]
167
- if (!allowed || !allowed.has(action)) {
168
- __mixdogMemoryLog(`[cycle2] verb rejected: id=${entryId} status=${status} verb=${action}\n`)
169
- rejected.add(entryId)
170
- continue
171
- }
172
- if (action === 'update') {
173
- actions.push({
174
- entry_id: entryId, action,
175
- element: (parts[2] ?? '').trim(),
176
- summary: parts.slice(3).join('|').trim(),
177
- })
178
- } else if (action === 'merge') {
179
- const targetId = resolveId(parts[2])
180
- const sourceIds = [...new Set((parts[3] ?? '').split(',').map(s => resolveId(s)).filter(Number.isFinite))]
181
- if (!Number.isFinite(targetId) || sourceIds.length === 0) {
182
- __mixdogMemoryLog(`[cycle2] merge rejected: id=${entryId} invalid target/sources\n`)
183
- rejected.add(entryId)
184
- continue
185
- }
186
- if (targetId !== entryId && !sourceIds.includes(entryId)) {
187
- __mixdogMemoryLog(
188
- `[cycle2] merge rejected: id=${entryId} must be target or listed source (target=${targetId} sources=${sourceIds.join(',')})\n`,
189
- )
190
- rejected.add(entryId)
191
- continue
192
- }
193
- actions.push({
194
- entry_id: entryId, action,
195
- target_id: targetId,
196
- source_ids: sourceIds,
197
- element: (parts[4] ?? '').trim(),
198
- summary: parts.slice(5).join('|').trim(),
199
- })
200
- } else {
201
- actions.push({ entry_id: entryId, action })
202
- }
203
- }
204
- if (!sawValid && rejected.size === 0) return null
205
- return { actions, rejected, support }
206
- }
207
-
208
- // Batch CTE UPDATE for status-only verdicts (active/archived from pending or active rows).
209
- // Trigger handles score recompute automatically — no app-side score writes.
210
- async function applyBatchStatusVerdicts(db, batch, nowMs) {
211
- if (!batch || batch.length === 0) return { promoted: 0, archived: 0 }
212
- const valueRows = batch.map((item, i) => {
213
- const base = i * 3
214
- return `($${base + 1}::bigint, $${base + 2}::text, $${base + 3}::boolean)`
215
- })
216
- const params = []
217
- for (const item of batch) {
218
- params.push(item.entry_id, item.new_status, item.was_pending)
219
- }
220
- params.push(nowMs)
221
- const lastParam = `$${params.length}`
222
- const res = await db.query(
223
- `WITH actions(entry_id, new_status, was_pending) AS (
224
- VALUES ${valueRows.join(', ')}
225
- )
226
- UPDATE entries
227
- SET status = a.new_status::entry_status,
228
- last_seen_at = ${lastParam},
229
- promoted_at = CASE
230
- WHEN a.was_pending AND a.new_status = 'active' THEN ${lastParam}
231
- ELSE promoted_at
232
- END
233
- FROM actions a
234
- WHERE entries.id = a.entry_id AND entries.is_root = 1
235
- RETURNING entries.id, entries.status, a.was_pending, a.new_status`,
236
- params,
237
- )
238
- let promoted = 0
239
- let archived = 0
240
- for (const r of (res.rows ?? [])) {
241
- if (r.was_pending && r.new_status === 'active') promoted += 1
242
- else if (r.new_status === 'archived') archived += 1
243
- }
244
- return { promoted, archived }
245
- }
246
-
247
- // Generic status update for archived/active terminal transitions.
248
- export async function applySimpleStatus(db, entryId, nextStatus) {
249
- const res = await db.query(
250
- `UPDATE entries SET status = $1 WHERE id = $2 AND is_root = 1`,
251
- [nextStatus, entryId],
252
- )
253
- return Number(res.rowCount ?? res.affectedRows ?? 0) > 0
254
- }
255
-
256
- export async function applyUpdate(db, entryId, element, summary, options = {}) {
257
- const setClauses = []
258
- const params = []
259
- let paramIdx = 1
260
- const newElement = (typeof element === 'string' && element.trim()) ? element.trim() : null
261
- const newSummary = (typeof summary === 'string' && summary.trim()) ? summary.trim() : null
262
- if (newElement) {
263
- setClauses.push(`element = $${paramIdx++}`); params.push(newElement)
264
- }
265
- if (newSummary) {
266
- setClauses.push(`summary = $${paramIdx++}`); params.push(newSummary)
267
- setClauses.push('summary_hash = NULL')
268
- }
269
- if (setClauses.length === 0) return false
270
- params.push(entryId)
271
- const res = await db.query(
272
- `UPDATE entries SET ${setClauses.join(', ')} WHERE id = $${paramIdx} AND is_root = 1`,
273
- params,
274
- )
275
- if (Number(res.rowCount ?? res.affectedRows ?? 0) === 0) return false
276
- await syncRootEmbedding(db, entryId, options)
277
- return true
278
- }
279
-
280
- export async function applyMerge(db, targetId, sourceIds, options = {}) {
281
- const signal = options?.signal
282
- throwIfAborted(signal)
283
- if (!Number.isFinite(targetId)) return 0
284
- const targetRes = await db.query(
285
- `SELECT id, project_id FROM entries WHERE id = $1 AND is_root = 1`,
286
- [targetId],
287
- )
288
- throwIfAborted(signal)
289
- const target = targetRes.rows[0]
290
- if (!target) return 0
291
- let moved = 0
292
- for (const src of sourceIds) {
293
- throwIfAborted(signal)
294
- const sid = Number(src)
295
- if (!Number.isFinite(sid) || sid === targetId) continue
296
- const srcRes = await db.query(
297
- `SELECT id, project_id, status FROM entries WHERE id = $1 AND is_root = 1`,
298
- [sid],
299
- )
300
- throwIfAborted(signal)
301
- const srcRow = srcRes.rows[0]
302
- if (!srcRow) continue
303
- if (target.project_id !== srcRow.project_id) {
304
- __mixdogMemoryLog(
305
- `[cycle2] merge rejected: cross-pool (target=${targetId} project_id=${target.project_id ?? 'COMMON'} src=${sid} project_id=${srcRow.project_id ?? 'COMMON'})\n`,
306
- )
307
- continue
308
- }
309
- try {
310
- // One source merge is the mutation unit: DB reassignment/archive plus
311
- // embedding cleanup. The next abort checkpoint is before the next source.
312
- await db.transaction(async (tx) => {
313
- await tx.query(
314
- `UPDATE entries SET chunk_root = $1, project_id = $2 WHERE chunk_root = $3 AND id != $4 AND is_root = 0`,
315
- [targetId, target.project_id, sid, sid],
316
- )
317
- await tx.query(
318
- `UPDATE entries SET status = 'archived' WHERE id = $1 AND is_root = 1`,
319
- [sid],
320
- )
321
- })
322
- await deleteRootEmbedding(db, sid)
323
- moved += 1
324
- } catch (err) {
325
- __mixdogMemoryLog(`[cycle2] merge failed (target=${targetId} src=${sid}): ${err.message}\n`)
326
- }
327
- }
328
- return moved
329
- }
330
-
331
- // ─── phase_merge: cosine-similarity dedup pass ───────────────────────────────
332
-
333
- function _pickKeeper(a, b) {
334
- if ((a.score ?? 0) !== (b.score ?? 0)) return (a.score ?? 0) > (b.score ?? 0) ? a : b
335
- if ((a.last_seen_at ?? 0) !== (b.last_seen_at ?? 0)) return (a.last_seen_at ?? 0) > (b.last_seen_at ?? 0) ? a : b
336
- return a.id < b.id ? a : b
337
- }
338
-
339
- async function _llmJudgePair(summaryA, summaryB, siblingContext = [], options = {}) {
340
- const signal = options?.signal
341
- throwIfAborted(signal)
342
- const llmCall = typeof options?.callLlm === 'function' ? options.callLlm : callAgentDispatch
343
- const siblings = Array.isArray(siblingContext) && siblingContext.length > 0
344
- ? `\n\nSibling near-matches (recall context only — do not absorb these into the verdict):\n${siblingContext.slice(0, 5).map((p, i) => `${i + 1}. ${String(p.a?.summary ?? '')} ↔ ${String(p.b?.summary ?? '')}`).join('\n')}`
345
- : ''
346
- const prompt =
347
- `Two memory entries below. Are they restating the same principle? Reply ONE WORD: merge or distinct.\n\nA: ${summaryA}\nB: ${summaryB}${siblings}`
348
- try {
349
- const raw = await llmCall({
350
- agent: 'cycle2-agent',
351
- taskType: 'maintenance',
352
- mode: 'cycle2-phase_merge_judge',
353
- preset: 'HAIKU',
354
- timeout: 30000,
355
- cwd: null,
356
- }, prompt)
357
- throwIfAborted(signal)
358
- return String(raw ?? '').trim().toLowerCase().startsWith('merge')
359
- } catch (err) {
360
- if (signal?.aborted) throw signal.reason ?? err
361
- __mixdogMemoryLog(`[cycle2] phase_merge llm-judge error: ${err.message}\n`)
362
- return false
363
- }
364
- }
365
-
366
- export async function runPhaseMerge(db, options = {}) {
367
- const signal = options?.signal
368
- throwIfAborted(signal)
369
- // PG-side lateral nearest-neighbor via HNSW index — replaces JS O(n²) double loop.
370
- const pairRes = await db.query(
371
- `WITH active AS (
372
- SELECT id, category, summary, score, last_seen_at, status, embedding, project_id
373
- FROM entries
374
- WHERE is_root = 1 AND status = 'active' AND embedding IS NOT NULL
375
- )
376
- SELECT a.id AS a_id, a.category AS a_category, a.summary AS a_summary, a.score AS a_score, a.last_seen_at AS a_last_seen_at, a.status AS a_status,
377
- b.id AS b_id, b.category AS b_category, b.summary AS b_summary, b.score AS b_score, b.last_seen_at AS b_last_seen_at, b.status AS b_status,
378
- 1 - (a.embedding <=> b.embedding)::float8 AS sim
379
- FROM active a
380
- CROSS JOIN LATERAL (
381
- SELECT id, category, summary, score, last_seen_at, status, embedding
382
- FROM active inner_b
383
- WHERE inner_b.id != a.id AND inner_b.category = a.category
384
- AND inner_b.project_id IS NOT DISTINCT FROM a.project_id
385
- ORDER BY inner_b.embedding <=> a.embedding
386
- LIMIT 8
387
- ) b
388
- WHERE a.id < b.id
389
- AND 1 - (a.embedding <=> b.embedding) >= $1
390
- ORDER BY sim DESC`,
391
- [TIER2_LOW],
392
- )
393
- throwIfAborted(signal)
394
-
395
- const tier1Pairs = []
396
- const tier2Pairs = []
397
- for (const row of pairRes.rows) {
398
- throwIfAborted(signal)
399
- const a = { id: row.a_id, category: row.a_category, summary: row.a_summary, score: row.a_score, last_seen_at: row.a_last_seen_at, status: row.a_status }
400
- const b = { id: row.b_id, category: row.b_category, summary: row.b_summary, score: row.b_score, last_seen_at: row.b_last_seen_at, status: row.b_status }
401
- if (row.sim >= TIER1_THRESHOLD) tier1Pairs.push({ a, b, sim: row.sim })
402
- else tier2Pairs.push({ a, b, sim: row.sim })
403
- }
404
-
405
- // No active/active similarity pairs is NOT a reason to skip the
406
- // core_entries overlap sweep below — that pass archives active entries
407
- // that restate a user-curated core row and is independent of intra-
408
- // entry pairing. Falling through with merged=0 keeps the cross-table
409
- // sweep running and the per-phase log shape intact.
410
- let merged = 0
411
- let llmCalls = 0
412
- const mergedIds = new Set()
413
-
414
- const doMerge = async (a, b, sim) => {
415
- throwIfAborted(signal)
416
- if (mergedIds.has(a.id) || mergedIds.has(b.id)) return
417
- const keeper = _pickKeeper(a, b)
418
- const loser = keeper.id === a.id ? b : a
419
- const moved = await applyMerge(db, keeper.id, [loser.id], { signal })
420
- if (moved > 0) {
421
- merged += moved
422
- mergedIds.add(loser.id)
423
- __mixdogMemoryLog(
424
- `[cycle2] phase_merge merged id=${loser.id} -> keeper=${keeper.id} category=${keeper.category} sim=${typeof sim === 'number' ? sim.toFixed(3) : '?'}\n`,
425
- )
426
- }
427
- }
428
-
429
- // Only tier1 pairs enter the LLM judge. Tier2 pairs (0.65 ≤ sim < 0.78)
430
- // are recall context only — passed as sibling examples to the judge, never
431
- // as judge input themselves, and never archived here.
432
- for (const pair of tier1Pairs) {
433
- throwIfAborted(signal)
434
- if (llmCalls >= LLM_JUDGE_CAP) break
435
- if (mergedIds.has(pair.a.id) || mergedIds.has(pair.b.id)) continue
436
- llmCalls++
437
- const shouldMerge = await _llmJudgePair(
438
- String(pair.a.summary ?? ''),
439
- String(pair.b.summary ?? ''),
440
- tier2Pairs,
441
- { signal },
442
- )
443
- throwIfAborted(signal)
444
- if (shouldMerge) await doMerge(pair.a, pair.b, pair.sim)
445
- }
446
-
447
- // Cross-table sweep: surface every active entry whose embedding sits near
448
- // a user-curated core_entries row (sim ≥ TIER2_LOW for broad recall) and
449
- // ask the LLM whether the entry is a restatement of that user rule. Only
450
- // the LLM verdict moves the entry to archived — embedding sim alone is
451
- // never authoritative. Project-scoped core only matches the same pool;
452
- // COMMON core is global and may absorb duplicate generated project memory.
453
- throwIfAborted(signal)
454
- const coreOverlapRes = await db.query(
455
- `WITH active_e AS (
456
- SELECT id, project_id, summary, embedding
457
- FROM entries
458
- WHERE is_root = 1 AND status = 'active' AND embedding IS NOT NULL
459
- )
460
- SELECT e.id AS entry_id, e.summary AS entry_summary, c.core_id, c.core_summary, c.sim
461
- FROM active_e e
462
- CROSS JOIN LATERAL (
463
- SELECT inner_c.id AS core_id, inner_c.summary AS core_summary,
464
- 1 - (e.embedding <=> inner_c.embedding)::float8 AS sim
465
- FROM core_entries inner_c
466
- WHERE inner_c.embedding IS NOT NULL
467
- AND (inner_c.project_id IS NULL OR inner_c.project_id IS NOT DISTINCT FROM e.project_id)
468
- ORDER BY
469
- CASE WHEN inner_c.project_id IS NOT DISTINCT FROM e.project_id THEN 0 ELSE 1 END,
470
- inner_c.embedding <=> e.embedding
471
- LIMIT 1
472
- ) c
473
- WHERE c.sim >= $1`,
474
- [TIER1_THRESHOLD],
475
- )
476
- throwIfAborted(signal)
477
- let coreOverlap = 0
478
- for (const row of coreOverlapRes.rows) {
479
- throwIfAborted(signal)
480
- if (llmCalls >= LLM_JUDGE_CAP) break
481
- llmCalls++
482
- const verdictMerge = await _llmJudgePair(
483
- String(row.entry_summary ?? ''),
484
- String(row.core_summary ?? ''),
485
- [],
486
- { signal },
487
- )
488
- throwIfAborted(signal)
489
- if (!verdictMerge) continue
490
- // Archiving one overlap and deleting its embedding is one mutation unit;
491
- // cancellation resumes at the next row boundary.
492
- const r = await db.query(
493
- `UPDATE entries SET status = 'archived' WHERE id = $1 AND is_root = 1 AND status = 'active'`,
494
- [Number(row.entry_id)],
495
- )
496
- if (Number(r.rowCount ?? r.affectedRows ?? 0) > 0) {
497
- coreOverlap++
498
- await deleteRootEmbedding(db, Number(row.entry_id))
499
- }
500
- }
501
- throwIfAborted(signal)
502
- if (coreOverlap > 0) {
503
- __mixdogMemoryLog(
504
- `[cycle2] phase_merge core_overlap archived=${coreOverlap} (LLM-judged restatements of user-curated core_entries)\n`,
505
- )
506
- }
507
-
508
- __mixdogMemoryLog(
509
- `[cycle2] phase_merge tier1_pairs=${tier1Pairs.length} tier2_pairs=${tier2Pairs.length}` +
510
- ` llm_calls=${llmCalls} merged=${merged} core_overlap=${coreOverlap}\n`,
511
- )
512
-
513
- return { merged, llm_calls: llmCalls, tier1_pairs: tier1Pairs.length, tier2_pairs: tier2Pairs.length, core_overlap: coreOverlap }
514
- }
515
-
516
- // ─── Current rules digest cache ──────────────────────────────────────────────
517
-
518
- let _currentRulesDigest = null
519
- let _currentRulesDigestTs = 0
520
- export function loadCurrentRulesDigest() {
521
- const now = Date.now()
522
- if (_currentRulesDigest && now - _currentRulesDigestTs < 60_000) return _currentRulesDigest
523
- const sources = [
524
- join(resourceDir(), 'rules', 'shared', '01-general.md'),
525
- join(resourceDir(), 'rules', 'shared', '01-tool.md'),
526
- join(resourceDir(), 'rules', 'shared', '04-memory.md'),
527
- join(resourceDir(), 'rules', 'shared', '06-team.md'),
528
- join(resourceDir(), 'rules', 'shared', '07-workflow.md'),
529
- ]
530
- const parts = []
531
- for (const p of sources) {
532
- try {
533
- if (!existsSync(p)) continue
534
- const txt = readFileSync(p, 'utf8').trim()
535
- if (txt) parts.push(`# Source: ${p}\n${txt}`)
536
- } catch {}
537
- }
538
- const joined = parts.join('\n\n---\n\n')
539
- const CAP = 40_000
540
- _currentRulesDigest = joined.length > CAP ? joined.slice(0, CAP) + '\n…[truncated]' : joined
541
- _currentRulesDigestTs = now
542
- return _currentRulesDigest
543
- }
544
-
545
- function uniqueIds(values) {
546
- return [...new Set(values
547
- .map(id => Number(id))
548
- .filter(id => Number.isFinite(id)))]
549
- }
550
-
551
- function validateUnifiedGate(parsed, statusById) {
552
- const actions = Array.isArray(parsed?.actions) ? parsed.actions : []
553
- const primary = actions.filter(a => a?.action !== 'core')
554
- const verdictCounts = new Map()
555
- for (const action of primary) {
556
- const id = Number(action?.entry_id)
557
- if (!Number.isFinite(id)) continue
558
- verdictCounts.set(id, (verdictCounts.get(id) || 0) + 1)
559
- }
560
- const expectedIds = [...statusById.keys()]
561
- const missingVerdictIds = expectedIds.filter(id => !verdictCounts.has(id))
562
- const duplicateVerdictIds = [...verdictCounts.entries()]
563
- .filter(([, count]) => count > 1)
564
- .map(([id]) => id)
565
- const support = parsed?.support instanceof Map ? parsed.support : new Map()
566
- const coreIds = new Set(actions
567
- .filter(a => a?.action === 'core')
568
- .map(a => Number(a.entry_id))
569
- .filter(id => Number.isFinite(id)))
570
- const missingSupportIds = []
571
- const missingCoreIds = []
572
- for (const action of primary) {
573
- if (!NON_ARCHIVE_VERBS.has(action?.action)) continue
574
- const id = Number(action.entry_id)
575
- if (!Number.isFinite(id)) continue
576
- const coreId = action.action === 'merge' && Number.isFinite(Number(action.target_id))
577
- ? Number(action.target_id)
578
- : id
579
- const hasSupport = support.has(id) || (action.action === 'merge' && support.has(coreId))
580
- if (!hasSupport) missingSupportIds.push(id)
581
- if (!coreIds.has(coreId)) missingCoreIds.push(id)
582
- }
583
- return {
584
- missingVerdictIds: uniqueIds(missingVerdictIds),
585
- duplicateVerdictIds: uniqueIds(duplicateVerdictIds),
586
- missingSupportIds: uniqueIds(missingSupportIds),
587
- missingCoreIds: uniqueIds(missingCoreIds),
588
- }
589
- }
590
-
591
- function gateQualitySummary(quality) {
592
- const parts = []
593
- if (quality?.missingVerdictIds?.length) parts.push(`missing verdict ids=${quality.missingVerdictIds.join(',')}`)
594
- if (quality?.duplicateVerdictIds?.length) parts.push(`duplicate verdict ids=${quality.duplicateVerdictIds.join(',')}`)
595
- if (quality?.missingSupportIds?.length) parts.push(`missing why ids=${quality.missingSupportIds.join(',')}`)
596
- if (quality?.missingCoreIds?.length) parts.push(`missing core ids=${quality.missingCoreIds.join(',')}`)
597
- return parts.join('; ')
598
- }
599
-
600
- function stripUnsupportedPromotions(parsed, unsupportedIds) {
601
- const ids = new Set(uniqueIds(unsupportedIds))
602
- if (ids.size === 0) return parsed
603
- const rejected = new Set(parsed?.rejected || [])
604
- for (const id of ids) rejected.add(id)
605
- const actions = (parsed?.actions || []).filter(a => {
606
- if (a?.action === 'core') return true
607
- return !ids.has(Number(a?.entry_id))
608
- })
609
- return { ...parsed, actions, rejected }
610
- }
611
-
612
- function requiredCoreIdForAction(action) {
613
- if (action?.action === 'merge' && Number.isFinite(Number(action.target_id))) {
614
- return Number(action.target_id)
615
- }
616
- return Number(action?.entry_id)
617
- }
618
-
619
- // ─── Unified gate ────────────────────────────────────────────────────────────
620
-
621
- // Single LLM pass over rows whose status is in {pending, active}.
622
- // Returns { actions, rejected, parseOk } following parseUnifiedFormat shape.
623
- export async function runUnifiedGate(db, rows, activeContext, config = {}, options = {}) {
624
- const signal = options?.signal
625
- throwIfAborted(signal)
626
- if (!rows || rows.length === 0) return { actions: [], rejected: new Set(), parseOk: true }
627
- const promptPath = join(resourceDir(), 'defaults', 'memory-promote-prompt.md')
628
- if (!existsSync(promptPath)) {
629
- throw new Error(`runCycle2: prompt file missing at ${promptPath}`)
630
- }
631
- const template = readFileSync(promptPath, 'utf8')
632
- const userCoreRows = options.dataDir ? await listCore(options.dataDir, '*').catch(() => []) : []
633
- throwIfAborted(signal)
634
- const sharedPidMap = buildPidMap([activeContext ?? [], rows ?? [], userCoreRows ?? []])
635
- const rulesDigest = loadCurrentRulesDigest() || '(no current rules digest available)'
636
- const activeCount = activeContext?.length ?? 0
637
- const activeCap = options.activeCap ?? CYCLE2_ACTIVE_TARGET_CAP
638
-
639
- const prompt = template
640
- .replace('{{CURRENT_RULES}}', rulesDigest)
641
- .replace('{{USER_CORE}}', formatUserCoreForPrompt(userCoreRows, sharedPidMap))
642
- .replace('{{CORE_MEMORY}}', formatEntriesForPromotePrompt(activeContext, sharedPidMap))
643
- .replace('{{ITEMS}}', formatEntriesForPromotePrompt(rows, sharedPidMap, { numbered: true }))
644
- .replace('{{ACTIVE_COUNT}}', String(activeCount))
645
- .replace('{{ACTIVE_CAP}}', String(activeCap))
646
-
647
- const preset = options.preset || resolveMaintenancePreset('memory')
648
- const timeout = Number(config?.cycle2?.timeout ?? 600000)
649
- const mode = 'cycle2-unified'
650
-
651
- const previewRaw = (raw) => String(raw ?? '').replace(/\s+/g, ' ').slice(0, 200)
652
- const callOnce = async (extraTag) => {
653
- throwIfAborted(signal)
654
- const p = extraTag ? `${prompt}\n\n[retry:${extraTag}]` : prompt
655
- const raw = await invokeLlm(p, mode, preset, timeout, options.callLlm)
656
- throwIfAborted(signal)
657
- return raw
658
- }
659
-
660
- const statusById = new Map(rows.map(r => [Number(r.id), String(r.status)]))
661
- // Ordinal → batch-id map, keyed by 1-based prompt order (the same order the
662
- // numbered Entries block uses). The gate may echo either the real 5-digit
663
- // batch id or the row ordinal 1..N; the parser resolves both. The two domains
664
- // MUST be disjoint or an ordinal could shadow a real id (ids are 5-digit,
665
- // ordinals are <= rows.length, so disjointness always holds in practice).
666
- // On a violation a row-number line is indistinguishable from an exact-id
667
- // line, so no safe interpretation exists — skip this batch (gate failure)
668
- // rather than risk applying a verdict to the wrong entry. The cycle itself
669
- // proceeds; the batch re-queues for a later run.
670
- const ordinalToId = new Map(rows.map((r, i) => [i + 1, Number(r.id)]))
671
- const minBatchId = Math.min(...[...statusById.keys()])
672
- if (Number.isFinite(minBatchId) && minBatchId <= rows.length) {
673
- __mixdogMemoryLog(`[cycle2] batch id ${minBatchId} collides with ordinal range 1..${rows.length} — skipping batch (no safe id resolution)\n`)
674
- return { actions: null, rejected: new Set(), parseOk: false }
675
- }
676
-
677
- __mixdogMemoryLog(`[cycle2-diag] unified prompt=${prompt.length} bytes; rows=${rows.length}\n`)
678
-
679
- let raw
680
- try {
681
- raw = await callOnce(null)
682
- } catch (err) {
683
- if (signal?.aborted) throw signal.reason ?? err
684
- __mixdogMemoryLog(`[cycle2] unified LLM error: ${err.message}\n`)
685
- return { actions: null, rejected: new Set(), parseOk: false }
686
- }
687
- throwIfAborted(signal)
688
- __mixdogMemoryLog(`[cycle2-diag] unified raw (first 1500): ${String(raw ?? '').replace(/\n/g, '⏎').slice(0, 1500)}\n`)
689
-
690
- let parsed = parseUnifiedFormat(raw, statusById, ordinalToId)
691
- let quality = parsed ? validateUnifiedGate(parsed, statusById) : null
692
- const qualityIssue = () => gateQualitySummary(quality)
693
- if (!parsed || qualityIssue()) {
694
- throwIfAborted(signal)
695
- const issue = parsed ? qualityIssue() : `unparseable (${previewRaw(raw)})`
696
- __mixdogMemoryLog(`[cycle2] unified quality retry: ${issue}\n`)
697
- // Preserve the first pass before retrying. A retry fired for a mere quality
698
- // issue (e.g. a few missing verdicts) must not throw away an otherwise-valid
699
- // first-pass parse if the retry comes back unparseable.
700
- const firstParsed = parsed
701
- const firstQuality = quality
702
- try {
703
- const retryTag = parsed
704
- ? 'complete-verdicts-with-why-and-core-lines'
705
- : 'first-field-must-be-the-listed-row-number'
706
- const raw2 = await callOnce(retryTag)
707
- const retryParsed = parseUnifiedFormat(raw2, statusById, ordinalToId)
708
- if (retryParsed) {
709
- parsed = retryParsed
710
- quality = validateUnifiedGate(retryParsed, statusById)
711
- } else if (firstParsed) {
712
- __mixdogMemoryLog(`[cycle2] unparseable after retry — falling back to first-pass parse (${previewRaw(raw2)})\n`)
713
- parsed = firstParsed
714
- quality = firstQuality
715
- } else {
716
- __mixdogMemoryLog(`[cycle2] unparseable after retry — skipping batch (${previewRaw(raw2)})\n`)
717
- return { actions: null, rejected: new Set(), parseOk: false }
718
- }
719
- } catch (err) {
720
- if (signal?.aborted) throw signal.reason ?? err
721
- if (firstParsed) {
722
- __mixdogMemoryLog(`[cycle2] retry LLM error: ${err.message} — falling back to first-pass parse\n`)
723
- parsed = firstParsed
724
- quality = firstQuality
725
- } else {
726
- __mixdogMemoryLog(`[cycle2] retry LLM error: ${err.message}\n`)
727
- return { actions: null, rejected: new Set(), parseOk: false }
728
- }
729
- }
730
- }
731
- const finalIssue = gateQualitySummary(quality)
732
- // duplicateVerdictIds are genuinely ambiguous (the same row got two conflicting
733
- // verbs) — keep the full-skip. missingVerdictIds, by contrast, used to skip the
734
- // WHOLE batch, so a handful of persistently-missing poison rows could livelock
735
- // the gate. Partial-apply instead: keep the valid verdicts we did receive, just
736
- // log the missing ids and leave those rows for a later run.
737
- if (quality?.duplicateVerdictIds?.length) {
738
- __mixdogMemoryLog(`[cycle2] duplicate verdict coverage after retry — skipping batch (${finalIssue})\n`)
739
- return { actions: null, rejected: new Set(), parseOk: false }
740
- }
741
- if (quality?.missingVerdictIds?.length) {
742
- __mixdogMemoryLog(`[cycle2] missing verdicts after retry — partial apply, leaving ids=${quality.missingVerdictIds.join(',')} for a later run (${finalIssue})\n`)
743
- }
744
- // A response made up solely of why/core lines parses "ok" yet carries zero
745
- // primary (status-verb) verdicts. Without this guard parseOk stays true and
746
- // the caller treats the batch as a clean no-op, masking the coverage failure
747
- // and marking the rows reviewed. Fail the parse so the rows are re-queued.
748
- const primaryCount = (parsed.actions || []).filter(a => a?.action !== 'core').length
749
- if (rows.length > 0 && primaryCount === 0) {
750
- __mixdogMemoryLog(`[cycle2] gate produced zero primary verdicts for ${rows.length} rows — failing parse\n`)
751
- return { actions: null, rejected: new Set(), parseOk: false, missingIds: [...statusById.keys()] }
752
- }
753
- const incompletePromotionIds = uniqueIds([
754
- ...(quality?.missingSupportIds || []),
755
- ...(quality?.missingCoreIds || []),
756
- ])
757
- if (incompletePromotionIds.length > 0) {
758
- __mixdogMemoryLog(`[cycle2] incomplete non-archive verdicts rejected after retry ids=${incompletePromotionIds.join(',')} (${finalIssue})\n`)
759
- parsed = stripUnsupportedPromotions(parsed, incompletePromotionIds)
760
- }
761
- return {
762
- actions: parsed.actions,
763
- rejected: parsed.rejected,
764
- parseOk: true,
765
- missingIds: quality?.missingVerdictIds || [],
766
- }
11
+ import { __mixdogMemoryLog, throwIfAborted } from './memory-cycle2-shared.mjs'
12
+ import {
13
+ applyBatchStatusVerdicts, applySimpleStatus, applyUpdate, applyMerge, runPhaseMerge,
14
+ } from './memory-cycle2-mutations.mjs'
15
+ import {
16
+ CYCLE2_ACTIVE_TARGET_CAP, loadCurrentRulesDigest, runUnifiedGate, sonnetCascade,
17
+ NON_ARCHIVE_VERBS, requiredCoreIdForAction,
18
+ } from './memory-cycle2-gate.mjs'
19
+
20
+ // Re-export the extracted public surface so importers of this path (and the
21
+ // memory-cycle.mjs barrel) keep resolving the same names.
22
+ export {
23
+ CYCLE2_ACTIVE_TARGET_CAP,
24
+ applySimpleStatus, applyUpdate, applyMerge,
25
+ runPhaseMerge, loadCurrentRulesDigest, runUnifiedGate,
767
26
  }
768
27
 
769
- // ─── Sonnet cascade ──────────────────────────────────────────────────────────
770
-
771
- // Sonnet re-judge over first-pass keep verdicts. Sonnet sees rules + summary
772
- // and returns binary keep/drop. Failures fail-open (preserve first-pass).
773
- async function sonnetCascade(candidates, rulesDigest, options = {}) {
774
- const signal = options?.signal
775
- throwIfAborted(signal)
776
- if (!candidates || candidates.length === 0) return new Map()
777
- const lines = candidates.map(c =>
778
- `id:${c.id} status:${c.status} verb:${c.verb} cat:${c.category} el:${c.element} sm:${String(c.summary || '').slice(0, 200)}${c.core ? ` core:${String(c.core).slice(0, 200)}` : ''}`,
779
- ).join('\n')
780
- const prompt = [
781
- `Final gate over first-pass keep verdicts.`,
782
- `Keep a candidate ONLY if it lands in one of three layers: L1 relationship/communication`,
783
- `(user identity, address form, reply-style preferences, disliked patterns); L2 behavior rules`,
784
- `(principles the user corrected/insisted on, hard safety boundaries, quality bars); or L3 current`,
785
- `map (one-line project-landscape summaries, live long-running goals, environment anchors documented`,
786
- `nowhere else). For a past decision/failure, keep only the one-line lesson that still constrains`,
787
- `behavior, else drop. DROP anything whose source of truth is code, rules files, or skill docs, plus`,
788
- `implementation specs, code-internal constants, measurements, resolved-bug stories, status snapshots,`,
789
- `and duplicates of source-of-truth rules.`,
790
- `When a candidate has a core: field, judge THAT extracted one-line lesson (the entry will live as`,
791
- `that line), not the raw narrative in el:/sm:.`,
792
- ``,
793
- `Source-of-truth rules (excerpt — DO NOT duplicate in memory):`,
794
- String(rulesDigest || '').slice(0, 4000),
795
- ``,
796
- `Candidates:`,
797
- lines,
798
- ``,
799
- `Reply one line per id: "<id>|keep" to retain, "<id>|drop" to reject.`,
800
- `NO prose, NO preamble, NO meta-commentary. First character must be a digit.`,
801
- ].join('\n')
802
-
803
- // Hardcoded — resolveMaintenancePreset falls back to first preset (HAIKU)
804
- // when no binding exists, which would defeat the cascade. SONNET HIGH
805
- // matches the worker pool's default preset id from agent-config.
806
- const preset = options.cascadePreset || 'SONNET HIGH'
807
- const llmCall = typeof options?.callLlm === 'function' ? options.callLlm : callAgentDispatch
808
- let raw
809
- try {
810
- raw = await llmCall({
811
- agent: 'cycle2-agent',
812
- taskType: 'maintenance',
813
- mode: 'cycle2-cascade',
814
- preset,
815
- timeout: 600000,
816
- cwd: null,
817
- }, prompt)
818
- } catch (err) {
819
- if (signal?.aborted) throw signal.reason ?? err
820
- __mixdogMemoryLog(`[cycle2] cascade error: ${err.message} — fail-open\n`)
821
- return new Map()
822
- }
823
- throwIfAborted(signal)
824
-
825
- const verdicts = new Map()
826
- for (const line of String(raw ?? '').split('\n')) {
827
- throwIfAborted(signal)
828
- const trimmed = line.trim()
829
- if (!trimmed) continue
830
- if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('```')) continue
831
- const parts = trimmed.split('|')
832
- if (parts.length < 2) continue
833
- const id = Number(parts[0].trim())
834
- const v = parts[1].trim().toLowerCase()
835
- if (Number.isFinite(id) && (v === 'keep' || v === 'drop')) verdicts.set(id, v)
836
- }
837
- __mixdogMemoryLog(`[cycle2] cascade evaluated=${candidates.length} drops=${[...verdicts.values()].filter(v => v === 'drop').length}\n`)
838
- return verdicts
839
- }
840
28
 
841
29
  // ─── runCycle2 ───────────────────────────────────────────────────────────────
842
30
 
@@ -865,6 +53,7 @@ function mergeCycle2Results(a, b) {
865
53
  merge_rejected: Number(a.merge_rejected || 0) + Number(b.merge_rejected || 0),
866
54
  missing_core_summary: Number(a.missing_core_summary || 0) + Number(b.missing_core_summary || 0),
867
55
  core_embedding_backfill: Number(a.core_embedding_backfill || 0) + Number(b.core_embedding_backfill || 0),
56
+ core_candidates_nominated: Number(a.core_candidates_nominated || 0) + Number(b.core_candidates_nominated || 0),
868
57
  rescore: mergeNestedNumeric(a.rescore, b.rescore),
869
58
  phase_merge: mergeNestedNumeric(a.phase_merge, b.phase_merge),
870
59
  cascade: mergeNestedNumeric(a.cascade, b.cascade),
@@ -1007,6 +196,7 @@ async function _runCycle2Impl(db, config = {}, options = {}, dataDir = null) {
1007
196
  merge_rejected: 0,
1008
197
  missing_core_summary: 0,
1009
198
  core_embedding_backfill: 0,
199
+ core_candidates_nominated: 0,
1010
200
  rescore: { updated: 0 },
1011
201
  phase_merge: { merged: 0, llm_calls: 0, tier1_pairs: 0, tier2_pairs: 0, core_overlap: 0 },
1012
202
  cascade: { evaluated: 0, dropped: 0 },
@@ -1360,11 +550,55 @@ async function _runCycle2Impl(db, config = {}, options = {}, dataDir = null) {
1360
550
  throwIfAborted(signal)
1361
551
  stats.phase_merge = phaseMergeStats
1362
552
 
553
+ // Core-candidate nomination (proposal mode): flag strong durable active
554
+ // roots as core-memory candidates for user approval. Runs AFTER phase_merge
555
+ // so its core_overlap sweep has already archived active entries that restate
556
+ // an existing core row — nomination never re-surfaces those. NEVER
557
+ // auto-inserts into core_entries; the user promotes via action:'core'
558
+ // op:'promote'. Best-effort: a failure here must not fail the cycle.
559
+ if (dataDir) {
560
+ try {
561
+ throwIfAborted(signal)
562
+ stats.core_candidates_nominated = await nominateCoreCandidates(dataDir, { signal })
563
+ throwIfAborted(signal)
564
+ } catch (err) {
565
+ if (signal?.aborted) throw signal.reason ?? err
566
+ __mixdogMemoryLog(`[cycle2] core-candidate nomination failed: ${err.message}\n`)
567
+ }
568
+ }
569
+
1363
570
  // Active-cap enforcement is delegated to the gate (phases 1-3): the prompt
1364
571
  // exposes Active/cap counts and instructs aggressive `archived` verdicts on
1365
572
  // overflow. No deterministic safety net here — if the gate ever fails to
1366
573
  // contain growth, fix the prompt, not bolt a fallback back on.
1367
574
 
575
+ // Chronic gate-failure sweep: pending roots that have failed the gate 5+
576
+ // times AND are 30+ days old will realistically never pass (their content
577
+ // keeps breaking the parse — 2026-07 drain left 154 such rows cycling as
578
+ // permanent noop batches). Terminal-archive them (status only, no data
579
+ // deletion) so they stop occupying gate batches. Same cooldown semantics
580
+ // as the parse-failure reviewed_at advance above.
581
+ try {
582
+ throwIfAborted(signal)
583
+ const sweepRes = await db.query(
584
+ `UPDATE entries
585
+ SET status = 'archived', reviewed_at = $1
586
+ WHERE is_root = 1 AND status = 'pending'
587
+ AND COALESCE(error_count, 0) >= 5
588
+ AND ts < $2
589
+ RETURNING id`,
590
+ [Date.now(), Date.now() - 30 * 86_400_000],
591
+ )
592
+ const swept = sweepRes?.rows?.length ?? 0
593
+ if (swept > 0) {
594
+ stats.chronic_swept = swept
595
+ __mixdogMemoryLog(`[cycle2] chronic gate-failure sweep archived=${swept}\n`)
596
+ }
597
+ } catch (err) {
598
+ if (signal?.aborted) throw signal.reason ?? err
599
+ __mixdogMemoryLog(`[cycle2] chronic sweep failed: ${err.message}\n`)
600
+ }
601
+
1368
602
  __mixdogMemoryLog(
1369
603
  `[cycle2] rescore=${stats.rescore.updated}` +
1370
604
  ` core_backfill=${stats.core_embedding_backfill}` +
@@ -1375,7 +609,8 @@ async function _runCycle2Impl(db, config = {}, options = {}, dataDir = null) {
1375
609
  ` missing_core=${stats.missing_core_summary}` +
1376
610
  ` | cascade eval=${stats.cascade.evaluated} drop=${stats.cascade.dropped}` +
1377
611
  ` | phase_merge merged=${stats.phase_merge.merged} core_overlap=${stats.phase_merge.core_overlap || 0}` +
1378
- ` llm=${stats.phase_merge.llm_calls}\n`,
612
+ ` llm=${stats.phase_merge.llm_calls}` +
613
+ ` | core_candidates=${stats.core_candidates_nominated || 0}\n`,
1379
614
  )
1380
615
 
1381
616
  return stats