mixdog 0.9.1 → 0.9.3

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 (301) hide show
  1. package/package.json +9 -1
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  6. package/scripts/background-task-meta-smoke.mjs +1 -1
  7. package/scripts/bench-run.mjs +262 -0
  8. package/scripts/build-tui.mjs +13 -1
  9. package/scripts/compact-smoke.mjs +12 -0
  10. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  11. package/scripts/explore-bench.mjs +124 -0
  12. package/scripts/hook-bus-test.mjs +191 -0
  13. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  14. package/scripts/internal-comms-bench.mjs +727 -0
  15. package/scripts/internal-comms-smoke.mjs +75 -0
  16. package/scripts/lead-workflow-smoke.mjs +4 -4
  17. package/scripts/live-worker-smoke.mjs +9 -9
  18. package/scripts/output-style-bench.mjs +285 -0
  19. package/scripts/output-style-smoke.mjs +13 -10
  20. package/scripts/patch-replay.mjs +90 -0
  21. package/scripts/path-suffix-test.mjs +57 -0
  22. package/scripts/provider-stream-stall-test.mjs +276 -0
  23. package/scripts/provider-toolcall-test.mjs +599 -1
  24. package/scripts/recall-bench.mjs +207 -0
  25. package/scripts/routing-corpus.mjs +281 -0
  26. package/scripts/session-bench.mjs +1526 -0
  27. package/scripts/session-diag.mjs +595 -0
  28. package/scripts/task-bench.mjs +207 -0
  29. package/scripts/tool-failures.mjs +6 -6
  30. package/scripts/tool-smoke.mjs +310 -67
  31. package/scripts/toolcall-args-test.mjs +81 -0
  32. package/src/agents/debugger/AGENT.md +4 -4
  33. package/src/agents/heavy-worker/AGENT.md +20 -9
  34. package/src/agents/reviewer/AGENT.md +4 -4
  35. package/src/agents/worker/AGENT.md +17 -9
  36. package/src/app.mjs +10 -6
  37. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  38. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  39. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  40. package/src/headless-role.mjs +14 -14
  41. package/src/help.mjs +1 -0
  42. package/src/lib/rules-builder.cjs +32 -54
  43. package/src/mixdog-session-runtime.mjs +1040 -2036
  44. package/src/output-styles/default.md +12 -7
  45. package/src/output-styles/minimal.md +25 -0
  46. package/src/output-styles/oneline.md +21 -0
  47. package/src/output-styles/simple.md +10 -9
  48. package/src/repl.mjs +17 -7
  49. package/src/rules/agent/00-common.md +7 -5
  50. package/src/rules/agent/30-explorer.md +8 -12
  51. package/src/rules/lead/01-general.md +3 -1
  52. package/src/rules/lead/lead-tool.md +13 -2
  53. package/src/rules/shared/01-tool.md +23 -12
  54. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  55. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  56. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  57. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  58. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  59. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  60. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  61. package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
  62. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  63. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  64. package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
  65. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  66. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  67. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  68. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  69. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
  70. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
  71. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
  72. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  73. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  74. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  75. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  76. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
  78. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
  79. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
  80. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  82. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
  83. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  84. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
  85. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  86. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  87. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  88. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
  89. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  90. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  91. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
  93. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  94. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  97. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  98. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  99. package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
  100. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
  101. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  102. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
  103. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  104. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  105. package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
  106. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  107. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  108. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  109. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  110. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  113. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  116. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  118. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  119. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  121. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
  123. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  124. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  125. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
  126. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
  127. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  129. package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
  130. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  131. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  132. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  135. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  136. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  137. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  138. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  139. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
  140. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  141. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  142. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
  148. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
  149. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  150. package/src/runtime/channels/backends/discord.mjs +99 -9
  151. package/src/runtime/channels/backends/telegram.mjs +501 -0
  152. package/src/runtime/channels/index.mjs +437 -1439
  153. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  154. package/src/runtime/channels/lib/config.mjs +54 -2
  155. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  156. package/src/runtime/channels/lib/format.mjs +4 -2
  157. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  158. package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
  159. package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
  160. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  161. package/src/runtime/channels/lib/telegram-format.mjs +280 -0
  162. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
  164. package/src/runtime/channels/lib/webhook.mjs +59 -31
  165. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  166. package/src/runtime/channels/tool-defs.mjs +1 -1
  167. package/src/runtime/memory/index.mjs +465 -345
  168. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  169. package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
  170. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  171. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  172. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  173. package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
  174. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  175. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  176. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  177. package/src/runtime/memory/lib/memory.mjs +121 -4
  178. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  179. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  180. package/src/runtime/memory/lib/recall-format.mjs +183 -0
  181. package/src/runtime/memory/lib/session-ingest.mjs +107 -0
  182. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  183. package/src/runtime/memory/tool-defs.mjs +10 -7
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/background-tasks.mjs +2 -3
  186. package/src/runtime/shared/buffered-appender.mjs +149 -0
  187. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  188. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  189. package/src/runtime/shared/config.mjs +9 -0
  190. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  191. package/src/runtime/shared/schedules-store.mjs +21 -19
  192. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  193. package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
  194. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  195. package/src/runtime/shared/tool-surface.mjs +98 -13
  196. package/src/runtime/shared/transcript-writer.mjs +156 -0
  197. package/src/runtime/shared/update-checker.mjs +214 -0
  198. package/src/session-runtime/config-helpers.mjs +209 -0
  199. package/src/session-runtime/effort.mjs +128 -0
  200. package/src/session-runtime/fs-utils.mjs +10 -0
  201. package/src/session-runtime/model-capabilities.mjs +130 -0
  202. package/src/session-runtime/output-styles.mjs +124 -0
  203. package/src/session-runtime/plugin-mcp.mjs +114 -0
  204. package/src/session-runtime/session-text.mjs +100 -0
  205. package/src/session-runtime/statusline-route.mjs +35 -0
  206. package/src/session-runtime/tool-catalog.mjs +720 -0
  207. package/src/session-runtime/workflow.mjs +358 -0
  208. package/src/standalone/agent-tool.mjs +302 -117
  209. package/src/standalone/channel-admin.mjs +133 -40
  210. package/src/standalone/channel-worker.mjs +10 -292
  211. package/src/standalone/explore-tool.mjs +12 -5
  212. package/src/standalone/hook-bus.mjs +165 -8
  213. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  214. package/src/standalone/opencode-go-login.mjs +121 -0
  215. package/src/standalone/provider-admin.mjs +25 -3
  216. package/src/standalone/usage-dashboard.mjs +1 -1
  217. package/src/tui/App.jsx +2883 -778
  218. package/src/tui/components/ConfirmBar.jsx +47 -0
  219. package/src/tui/components/ContextPanel.jsx +5 -3
  220. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  221. package/src/tui/components/Markdown.jsx +22 -98
  222. package/src/tui/components/Message.jsx +14 -35
  223. package/src/tui/components/Picker.jsx +87 -12
  224. package/src/tui/components/PromptInput.jsx +355 -22
  225. package/src/tui/components/QueuedCommands.jsx +1 -1
  226. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  227. package/src/tui/components/Spinner.jsx +7 -7
  228. package/src/tui/components/StatusLine.jsx +40 -21
  229. package/src/tui/components/TextEntryPanel.jsx +51 -7
  230. package/src/tui/components/ToolExecution.jsx +183 -101
  231. package/src/tui/components/TurnDone.jsx +4 -4
  232. package/src/tui/components/UsagePanel.jsx +1 -1
  233. package/src/tui/components/tool-output-format.mjs +161 -23
  234. package/src/tui/components/tool-output-format.test.mjs +87 -0
  235. package/src/tui/display-width.mjs +69 -0
  236. package/src/tui/display-width.test.mjs +35 -0
  237. package/src/tui/dist/index.mjs +6731 -2333
  238. package/src/tui/engine/agent-envelope.mjs +296 -0
  239. package/src/tui/engine/boot-profile.mjs +21 -0
  240. package/src/tui/engine/labels.mjs +67 -0
  241. package/src/tui/engine/notice-text.mjs +112 -0
  242. package/src/tui/engine/queue-helpers.mjs +161 -0
  243. package/src/tui/engine/session-stats.mjs +46 -0
  244. package/src/tui/engine/tool-call-fields.mjs +23 -0
  245. package/src/tui/engine/tool-result-text.mjs +126 -0
  246. package/src/tui/engine.mjs +569 -948
  247. package/src/tui/index.jsx +117 -7
  248. package/src/tui/input-editing.mjs +58 -8
  249. package/src/tui/input-editing.selection.test.mjs +75 -0
  250. package/src/tui/keyboard-protocol.mjs +42 -0
  251. package/src/tui/lib/voice-recorder.mjs +469 -0
  252. package/src/tui/markdown/format-token.mjs +128 -76
  253. package/src/tui/markdown/format-token.test.mjs +61 -19
  254. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  255. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  256. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  257. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  258. package/src/tui/markdown/table-layout.mjs +9 -9
  259. package/src/tui/paste-attachments.mjs +36 -9
  260. package/src/tui/paste-fix.test.mjs +119 -0
  261. package/src/tui/prompt-history-store.mjs +129 -0
  262. package/src/tui/prompt-history-store.test.mjs +52 -0
  263. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  264. package/src/tui/theme.mjs +41 -657
  265. package/src/tui/themes/base.mjs +86 -0
  266. package/src/tui/themes/basic.mjs +85 -0
  267. package/src/tui/themes/catppuccin.mjs +72 -0
  268. package/src/tui/themes/dracula.mjs +70 -0
  269. package/src/tui/themes/everforest.mjs +71 -0
  270. package/src/tui/themes/gruvbox.mjs +71 -0
  271. package/src/tui/themes/index.mjs +71 -0
  272. package/src/tui/themes/indigo.mjs +78 -0
  273. package/src/tui/themes/kanagawa.mjs +80 -0
  274. package/src/tui/themes/light.mjs +81 -0
  275. package/src/tui/themes/nord.mjs +72 -0
  276. package/src/tui/themes/onedark.mjs +16 -0
  277. package/src/tui/themes/rosepine.mjs +70 -0
  278. package/src/tui/themes/teal.mjs +80 -0
  279. package/src/tui/themes/tokyonight.mjs +79 -0
  280. package/src/tui/themes/utils.mjs +106 -0
  281. package/src/tui/themes/warm.mjs +79 -0
  282. package/src/tui/transcript-tool-failures.mjs +13 -2
  283. package/src/ui/markdown.mjs +1 -1
  284. package/src/ui/model-display.mjs +2 -2
  285. package/src/ui/statusline.mjs +75 -27
  286. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  287. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  288. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  289. package/src/workflows/default/WORKFLOW.md +46 -12
  290. package/src/workflows/sequential/WORKFLOW.md +51 -0
  291. package/src/workflows/solo/WORKFLOW.md +12 -1
  292. package/vendor/ink/build/display-width.js +62 -0
  293. package/vendor/ink/build/ink.js +100 -12
  294. package/vendor/ink/build/measure-text.js +4 -1
  295. package/vendor/ink/build/output.js +115 -9
  296. package/vendor/ink/build/render-node-to-output.js +4 -1
  297. package/vendor/ink/build/render.js +4 -0
  298. package/src/output-styles/extreme-simple.md +0 -20
  299. package/src/rules/lead/04-workflow.md +0 -51
  300. package/src/workflows/default/workflow.json +0 -13
  301. package/src/workflows/solo/workflow.json +0 -7
@@ -1,4 +1,5 @@
1
1
  import crypto from 'node:crypto'
2
+ import { isInternalRuntimeNotificationText } from '../../shared/tool-execution-contract.mjs'
2
3
 
3
4
  // Side-effect-free helpers for ingest_session (recall fast-track hydration).
4
5
  // Extracted from memory/index.mjs so the pure logic (stable identity, sensitive
@@ -194,3 +195,109 @@ export function sessionMessageContent(m) {
194
195
  }
195
196
  return parts.join('\n')
196
197
  }
198
+
199
+ // ── Pure-conversation ingest shaping ──────────────────────────────────────
200
+ //
201
+ // ingest_session persists ONLY real conversation (human prompts + model reply
202
+ // prose). sessionMessageContent (above) is the structured-handoff shaper that
203
+ // inlines tool_call / tool_result traces and is consumed by smoke tests; it is
204
+ // intentionally LEFT UNCHANGED. The functions below are ingest-only and strip
205
+ // everything that is mechanical/synthetic so memory episode rows are pure
206
+ // conversation with zero loss of genuine human/model text.
207
+
208
+ // Mirror of compact.mjs SUMMARY_PREFIX (the compaction handoff anchor). Copied
209
+ // locally rather than imported because compact.mjs lives under
210
+ // agent/orchestrator/session and pulls in heavy context/offload modules —
211
+ // importing it into the memory layer would create a layering dependency (memory
212
+ // → orchestrator) and risk a boot-time cycle. Keep this string byte-identical
213
+ // to compact.mjs:9; if that anchor changes, update this copy.
214
+ const SUMMARY_PREFIX_INGEST = 'A previous model worked on this task and produced the compacted handoff summary below. Build on the work already done and avoid duplicating it; treat the summary as authoritative context for continuing the task. You also retain the preserved recent turns that follow.'
215
+
216
+ // Anchored strip of the deterministic user-turn prefix envelopes that
217
+ // manager.mjs prepends to the SINGLE real user message (manager.mjs:3166-3201
218
+ // via prefixUserTurnContent / prefixSessionStartContent / buildSessionStartBlock).
219
+ // Zero-loss design: every rule is anchored to the START of the message and only
220
+ // removes the EXACT shapes manager.mjs produces. A `# Task` / `# Session` etc.
221
+ // appearing mid-message in the human's own text is never touched. When in
222
+ // doubt the rules UNDER-strip (leave content) rather than delete human text.
223
+ function stripUserTurnPrefixEnvelopes(text) {
224
+ let out = String(text ?? '')
225
+ // 1) Leading `# Session` block (buildSessionStartBlock: `# Session\nCwd: ...
226
+ // \nModel: ...\nWorkflow: ...`, joined by prefixSessionStartContent with a
227
+ // trailing `\n\n`). FIELD-ANCHORED: only strip when the line(s) right after
228
+ // `# Session\n` are EXACTLY the fixed fields buildSessionStartBlock emits
229
+ // (`Cwd: `, `Model: `, `Workflow: `, each on its own line) before the
230
+ // blank-line terminator. A human doc that merely STARTS with a `# Session`
231
+ // heading followed by free prose (e.g. `프로젝트 회의록입니다`) does NOT match
232
+ // — its next line is not a `Cwd:/Model:/Workflow:` field — so it is
233
+ // preserved verbatim (zero-loss). Anchored ^.
234
+ out = out.replace(/^# Session\n(?:(?:Cwd|Model|Workflow): [^\n]*\n)+(?:\n|$)/, '')
235
+ // 2) Leading `# Additional context\n<body>\n\n` (manager.mjs:3168). Anchored
236
+ // to start; body runs up to the next `# ` section boundary or end. The
237
+ // `\n\n` separator manager.mjs emits is included.
238
+ out = out.replace(/^# Additional context\n[\s\S]*?(?=\n# |$)/, '')
239
+ out = out.replace(/^\n+/, '')
240
+ // 3) Leading `# Prefetch\n<body>\n\n` (manager.mjs:3171). Same anchoring.
241
+ out = out.replace(/^# Prefetch\n[\s\S]*?(?=\n# |$)/, '')
242
+ out = out.replace(/^\n+/, '')
243
+ // 4) Leading `# Task\n` marker (prefixUserTurnContent: `${contextBlock}# Task\n${content}`).
244
+ // Remove ONLY the marker line; everything after it is the human prompt and
245
+ // is preserved verbatim. Anchored ^ so a `# Task` in the human's own prose
246
+ // (after real text precedes it) is never removed.
247
+ out = out.replace(/^# Task\n/, '')
248
+ return out
249
+ }
250
+
251
+ // Ingest-only message shaper: returns ONLY the human/model prose text, NEVER
252
+ // inlining tool_call / tool_result traces. For user messages, the deterministic
253
+ // manager.mjs prefix envelopes are stripped so only the human's actual prompt
254
+ // remains. The <system-reminder> block is left in place here because
255
+ // cleanMemoryText already removes it downstream (text-utils.cjs:28).
256
+ export function sessionMessageContentForIngest(m) {
257
+ const base = firstTextContent(m?.content)
258
+ if (!base) return ''
259
+ if (normalizeIngestRole(m?.role) === 'user') {
260
+ return stripUserTurnPrefixEnvelopes(base)
261
+ }
262
+ return base
263
+ }
264
+
265
+ // Row-exclusion predicate for ingest_session. Synthetic / non-conversation
266
+ // rows (reference-files injections, compaction summaries, protected-context
267
+ // `.` acks, internal runtime nudges) are dropped ENTIRELY — they are noise,
268
+ // not conversation. Mirrors the predicates in manager.mjs / compact.mjs but is
269
+ // reimplemented locally to avoid a memory→orchestrator layering dependency.
270
+ export function shouldExcludeIngestMessage(m) {
271
+ if (!m || typeof m !== 'object') return true
272
+ const role = normalizeIngestRole(m?.role)
273
+ const raw = firstTextContent(m?.content)
274
+ // `Reference files:` synthetic user rows (manager.mjs isReferenceFilesMessage).
275
+ if (role === 'user' && typeof raw === 'string' && /^Reference files:\s*/i.test(raw.trimStart())) {
276
+ return true
277
+ }
278
+ // Attachment-only placeholder rows (e.g. Discord backend discord.mjs:724
279
+ // `"(attachment)"` fallback when a message carries no text, only files).
280
+ // Carries zero retrievable content — excluded so recall isn't polluted
281
+ // with bare "(attachment)" memory rows.
282
+ if (role === 'user' && typeof raw === 'string' && raw.trim() === '(attachment)') {
283
+ return true
284
+ }
285
+ // Compaction summary user rows (compact.mjs isSummaryMessage / SUMMARY_PREFIX).
286
+ if (role === 'user' && typeof raw === 'string' && raw.startsWith(SUMMARY_PREFIX_INGEST)) {
287
+ return true
288
+ }
289
+ // Protected-context `.` ack assistant rows (compact.mjs isProtectedContextAckMessage):
290
+ // a bare `.` with no tool calls. cleanMemoryText leaves a lone `.` non-empty
291
+ // (no \p{L}\p{N}), so it would otherwise survive the empty-skip — exclude it.
292
+ if (role === 'assistant' && typeof raw === 'string' && raw.trim() === '.' && !Array.isArray(m?.toolCalls)) {
293
+ return true
294
+ }
295
+ // Internal runtime nudge `[mixdog-runtime] ...` user rows (loop.mjs:2014-2020)
296
+ // and other internal runtime notifications (tool-execution-contract).
297
+ if (role === 'user') {
298
+ const text = typeof raw === 'string' ? raw : ''
299
+ if (/^\[mixdog-runtime\]/.test(text.trimStart())) return true
300
+ if (isInternalRuntimeNotificationText(text)) return true
301
+ }
302
+ return false
303
+ }
@@ -56,7 +56,7 @@ async function init(client) {
56
56
  session_id TEXT,
57
57
  iteration INTEGER,
58
58
  kind TEXT NOT NULL,
59
- role TEXT,
59
+ agent TEXT,
60
60
  model TEXT,
61
61
  tool_name TEXT,
62
62
  tool_ms INTEGER,
@@ -103,12 +103,18 @@ async function init(client) {
103
103
  END $$
104
104
  `)
105
105
 
106
+ // Drift repair MUST run before index creation: an old cluster whose
107
+ // trace_events predates the `agent` column would otherwise die right below
108
+ // at idx_trace_agent_ts (CREATE INDEX references the missing column) before
109
+ // initAgentTables() ever gets a chance to repair it. Reviewer High fix.
110
+ await migrateSchemaDrift(client)
111
+
106
112
  // BRIN on ts — ~1000× smaller than btree for append-only timeseries; ideal
107
113
  // for time-window range scans where rows arrive in roughly ts order.
108
114
  await client.query(`CREATE INDEX IF NOT EXISTS idx_trace_ts_brin ON trace_events USING BRIN (ts) WITH (pages_per_range = 32)`)
109
115
  await client.query(`CREATE INDEX IF NOT EXISTS idx_trace_kind_ts ON trace_events(kind, ts DESC)`)
110
116
  await client.query(`CREATE INDEX IF NOT EXISTS idx_trace_session ON trace_events(session_id, ts)`)
111
- await client.query(`CREATE INDEX IF NOT EXISTS idx_trace_role_ts ON trace_events(role, ts DESC)`)
117
+ await client.query(`CREATE INDEX IF NOT EXISTS idx_trace_agent_ts ON trace_events(agent, ts DESC)`)
112
118
  await client.query(`CREATE INDEX IF NOT EXISTS idx_trace_model_ts ON trace_events(model, ts DESC)`)
113
119
  await client.query(`CREATE INDEX IF NOT EXISTS idx_trace_tool ON trace_events(tool_name) WHERE kind = 'tool'`)
114
120
  // Span-tree and cross-schema recall↔trace correlation — partial indexes so
@@ -122,13 +128,45 @@ async function init(client) {
122
128
  await ensureCurrentAndNextMonthPartitions(client)
123
129
  }
124
130
 
131
+ // ---------------------------------------------------------------------------
132
+ // Schema-drift repair — ALTER TABLE ... ADD COLUMN IF NOT EXISTS
133
+ // ---------------------------------------------------------------------------
134
+ // Observed failure: pg.log repeatedly logs
135
+ // column "agent" of relation "trace_events" does not exist
136
+ // and the equivalent for agent_sessions — an older cluster/pgdata created
137
+ // before the `agent` column was added to these two tables, so every INSERT/
138
+ // UPSERT touching it fails forever until manually patched. CREATE TABLE IF
139
+ // NOT EXISTS above is a no-op once the table exists, so it never repairs an
140
+ // already-created table with a stale column set. Run these idempotent
141
+ // ADD COLUMN migrations on every boot (init() and initAgentTables()) so
142
+ // drifted clusters self-heal without a manual ALTER.
143
+ async function migrateSchemaDrift(client) {
144
+ // Bounded lock wait: nullable ADD COLUMN is metadata-only once the ACCESS
145
+ // EXCLUSIVE lock is held, but acquiring that lock can queue behind live
146
+ // readers/writers indefinitely and wedge boot (reviewer Medium). 5s is
147
+ // generous for a metadata change; on timeout we leave the drift in place
148
+ // (inserts keep failing as before — no worse) instead of hanging startup.
149
+ await client.query(`SET LOCAL lock_timeout = '5s'`).catch(() => {})
150
+ try {
151
+ await client.query(`ALTER TABLE IF EXISTS trace_events ADD COLUMN IF NOT EXISTS agent TEXT`)
152
+ await client.query(`ALTER TABLE IF EXISTS agent_sessions ADD COLUMN IF NOT EXISTS agent TEXT`)
153
+ } finally {
154
+ await client.query(`SET LOCAL lock_timeout = DEFAULT`).catch(() => {})
155
+ }
156
+ }
157
+
125
158
  // ---------------------------------------------------------------------------
126
159
  // Agent-specific analytic tables (added post-init via initAgentTables)
127
160
  // ---------------------------------------------------------------------------
128
161
 
129
- // Called once per openTraceDatabase boot (after the advisory lock is released).
130
- // Safe to call concurrently all DDL is IF NOT EXISTS.
162
+ // Called once per openTraceDatabase boot, inside the BOOTSTRAP advisory-lock
163
+ // scope (see openTraceDatabase) so concurrent first-boot processes don't race
164
+ // this DDL. All statements remain IF NOT EXISTS as a second line of defense.
131
165
  export async function initAgentTables(client) {
166
+ // Repair schema drift on already-created tables before anything else
167
+ // (trace_events already exists at this point via init(); agent_sessions
168
+ // is created just below in this same function).
169
+ await migrateSchemaDrift(client)
132
170
  // ── agent_calls: one row per tool invocation ─────────────────────────────
133
171
  await client.query(`
134
172
  CREATE TABLE IF NOT EXISTS agent_calls (
@@ -175,7 +213,7 @@ export async function initAgentTables(client) {
175
213
  await client.query(`
176
214
  CREATE TABLE IF NOT EXISTS agent_sessions (
177
215
  session_id TEXT PRIMARY KEY,
178
- role TEXT,
216
+ agent TEXT,
179
217
  model TEXT,
180
218
  started_at TIMESTAMPTZ,
181
219
  last_seen_at TIMESTAMPTZ,
@@ -186,6 +224,10 @@ export async function initAgentTables(client) {
186
224
  total_output_tokens BIGINT NOT NULL DEFAULT 0
187
225
  )
188
226
  `)
227
+
228
+ // Repair drift again post-creation for agent_sessions (belt-and-suspenders;
229
+ // no-op when the table was freshly created above with the column present).
230
+ await migrateSchemaDrift(client)
189
231
  }
190
232
 
191
233
  // ---------------------------------------------------------------------------
@@ -289,14 +331,14 @@ export async function insertAgentCalls(db, events) {
289
331
  // Upsert session summaries — accumulate from tool+llm rows in this batch
290
332
  const sessionMap = new Map()
291
333
  for (const r of toolRows) {
292
- const s = sessionMap.get(r.session_id) ?? { tool_calls: 0, llm_calls: 0, max_iteration: 0, total_input: 0n, total_output: 0n, ts0: r.ts, ts1: r.ts, role: null, model: null }
334
+ const s = sessionMap.get(r.session_id) ?? { tool_calls: 0, llm_calls: 0, max_iteration: 0, total_input: 0n, total_output: 0n, ts0: r.ts, ts1: r.ts, agent: null, model: null }
293
335
  s.tool_calls += 1
294
336
  if (r.iteration != null && r.iteration > s.max_iteration) s.max_iteration = r.iteration
295
337
  if (r.ts < s.ts0) s.ts0 = r.ts; if (r.ts > s.ts1) s.ts1 = r.ts
296
338
  sessionMap.set(r.session_id, s)
297
339
  }
298
340
  for (const r of llmRows) {
299
- const s = sessionMap.get(r.session_id) ?? { tool_calls: 0, llm_calls: 0, max_iteration: 0, total_input: 0n, total_output: 0n, ts0: r.ts, ts1: r.ts, role: null, model: null }
341
+ const s = sessionMap.get(r.session_id) ?? { tool_calls: 0, llm_calls: 0, max_iteration: 0, total_input: 0n, total_output: 0n, ts0: r.ts, ts1: r.ts, agent: null, model: null }
300
342
  s.llm_calls += 1
301
343
  s.total_input += BigInt(r.input_tokens ?? 0)
302
344
  s.total_output += BigInt(r.output_tokens ?? 0)
@@ -305,13 +347,13 @@ export async function insertAgentCalls(db, events) {
305
347
  if (r.ts < s.ts0) s.ts0 = r.ts; if (r.ts > s.ts1) s.ts1 = r.ts
306
348
  sessionMap.set(r.session_id, s)
307
349
  }
308
- // Also pick up role from preset_assign events in the same batch
350
+ // Also pick up agent from preset_assign events in the same batch
309
351
  for (const ev of events) {
310
- if (ev.kind === 'preset_assign' && ev.role) {
352
+ if (ev.kind === 'preset_assign' && ev.agent) {
311
353
  const sid = ev.session_id ?? ev.sessionId ?? null
312
354
  if (!sid) continue
313
355
  const s = sessionMap.get(sid)
314
- if (s) s.role = ev.role
356
+ if (s) s.agent = ev.agent
315
357
  }
316
358
  }
317
359
  // Fix 5 — upsert sessions for preset_assign-only batches (no tool/llm rows yet)
@@ -326,32 +368,32 @@ export async function insertAgentCalls(db, events) {
326
368
  tool_calls: 0, llm_calls: 0, max_iteration: 0,
327
369
  total_input: 0n, total_output: 0n,
328
370
  ts0: tsIso, ts1: tsIso,
329
- role: ev.role ?? null, model: ev.model ?? null,
371
+ agent: ev.agent ?? null, model: ev.model ?? null,
330
372
  })
331
373
  }
332
374
 
333
375
  // Coalesce agent_sessions upserts: batch all sessions in one unnest INSERT.
334
376
  // Also within the same transaction.
335
377
  if (sessionMap.size > 0) {
336
- const sids = [], roles = [], models = [], ts0s = [], ts1s = [],
378
+ const sids = [], agents = [], models = [], ts0s = [], ts1s = [],
337
379
  tcalls = [], lcalls = [], maxiters = [], tinputs = [], toutputs = []
338
380
  for (const [sid, s] of sessionMap) {
339
- sids.push(sid); roles.push(s.role); models.push(s.model)
381
+ sids.push(sid); agents.push(s.agent); models.push(s.model)
340
382
  ts0s.push(s.ts0); ts1s.push(s.ts1)
341
383
  tcalls.push(s.tool_calls); lcalls.push(s.llm_calls)
342
384
  maxiters.push(s.max_iteration)
343
385
  tinputs.push(String(s.total_input)); toutputs.push(String(s.total_output))
344
386
  }
345
387
  await client.query(`
346
- INSERT INTO agent_sessions (session_id, role, model, started_at, last_seen_at, tool_calls, llm_calls, max_iteration, total_input_tokens, total_output_tokens)
347
- SELECT u.session_id, u.role, u.model,
388
+ INSERT INTO agent_sessions (session_id, agent, model, started_at, last_seen_at, tool_calls, llm_calls, max_iteration, total_input_tokens, total_output_tokens)
389
+ SELECT u.session_id, u.agent, u.model,
348
390
  u.started_at::timestamptz, u.last_seen_at::timestamptz,
349
391
  u.tool_calls::int, u.llm_calls::int, u.max_iteration::int,
350
392
  u.total_input_tokens::bigint, u.total_output_tokens::bigint
351
393
  FROM unnest($1::text[],$2::text[],$3::text[],$4::text[],$5::text[],$6::int[],$7::int[],$8::int[],$9::text[],$10::text[])
352
- AS u(session_id,role,model,started_at,last_seen_at,tool_calls,llm_calls,max_iteration,total_input_tokens,total_output_tokens)
394
+ AS u(session_id,agent,model,started_at,last_seen_at,tool_calls,llm_calls,max_iteration,total_input_tokens,total_output_tokens)
353
395
  ON CONFLICT (session_id) DO UPDATE SET
354
- role = COALESCE(EXCLUDED.role, agent_sessions.role),
396
+ agent = COALESCE(EXCLUDED.agent, agent_sessions.agent),
355
397
  model = COALESCE(EXCLUDED.model, agent_sessions.model),
356
398
  started_at = LEAST(agent_sessions.started_at, EXCLUDED.started_at),
357
399
  last_seen_at = GREATEST(agent_sessions.last_seen_at, EXCLUDED.last_seen_at),
@@ -360,7 +402,7 @@ export async function insertAgentCalls(db, events) {
360
402
  max_iteration = GREATEST(agent_sessions.max_iteration, EXCLUDED.max_iteration),
361
403
  total_input_tokens = agent_sessions.total_input_tokens + EXCLUDED.total_input_tokens,
362
404
  total_output_tokens = agent_sessions.total_output_tokens + EXCLUDED.total_output_tokens
363
- `, [sids, roles, models, ts0s, ts1s, tcalls, lcalls, maxiters, tinputs, toutputs])
405
+ `, [sids, agents, models, ts0s, ts1s, tcalls, lcalls, maxiters, tinputs, toutputs])
364
406
  }
365
407
 
366
408
  await client.query('COMMIT')
@@ -513,14 +555,19 @@ export async function openTraceDatabase(dataDir) {
513
555
  // are pre-created for this boot.
514
556
  await ensureCurrentAndNextMonthPartitions(client)
515
557
  }
558
+ // Agent-specific analytic tables — moved inside the advisory-lock
559
+ // scope (was previously run after client.release() with no lock,
560
+ // letting concurrent first-boot processes race the agent_calls/
561
+ // agent_llm/agent_sessions CREATE TABLE + migrateSchemaDrift DDL).
562
+ // Reuses the same lock-holding client/session; still idempotent
563
+ // (IF NOT EXISTS everywhere) but no longer racy across processes.
564
+ await initAgentTables(client)
516
565
  } finally {
517
566
  await client.query(`SELECT pg_advisory_unlock(${BOOTSTRAP_LOCK_KEY})`)
518
567
  }
519
568
  } finally {
520
569
  client.release()
521
570
  }
522
- // Agent-specific analytic tables — idempotent, no advisory lock needed.
523
- await initAgentTables(db)
524
571
 
525
572
  dbs.set(key, db)
526
573
 
@@ -557,7 +604,7 @@ export async function openTraceDatabase(dataDir) {
557
604
  // ---------------------------------------------------------------------------
558
605
 
559
606
  const TRACE_COLS = [
560
- 'ts', 'session_id', 'iteration', 'kind', 'role', 'model',
607
+ 'ts', 'session_id', 'iteration', 'kind', 'agent', 'model',
561
608
  'tool_name', 'tool_ms', 'input_tokens', 'output_tokens',
562
609
  'cached_tokens', 'cache_write_tokens', 'duration_ms',
563
610
  'error_message', 'payload', 'parent_span_id', 'entry_id',
@@ -755,7 +802,7 @@ export async function insertTraceEvents(db, events) {
755
802
  ev.session_id ?? null,
756
803
  ev.iteration != null ? Number(ev.iteration) : null,
757
804
  String(ev.kind ?? 'unknown'),
758
- ev.role ?? null,
805
+ ev.agent ?? null,
759
806
  ev.model ?? null,
760
807
  ev.tool_name ?? null,
761
808
  ev.tool_ms != null ? Number(ev.tool_ms) : null,
@@ -8,12 +8,12 @@ export const TOOL_DEFS = [
8
8
  name: 'memory',
9
9
  title: 'Memory Cycle',
10
10
  annotations: { title: 'Memory Cycle', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
11
- description: 'Memory status/lifecycle or explicit mutation. Use recall for retrieval. Destructive jobs require exact confirm strings.',
11
+ description: 'Memory status/lifecycle or explicit mutation. Use recall for retrieval. When the user explicitly asks to remember something (e.g. "remember this", a stated preference, or a decision), persist it immediately with action:"core" op:"add" — do not wait for a cycle. Destructive jobs require exact confirm strings.',
12
12
  inputSchema: {
13
13
  type: 'object',
14
14
  properties: {
15
15
  action: { type: 'string', enum: ['core','manage','status','sleep','cycle1','cycle2','cycle3','flush','backfill','prune','rebuild','purge','retro_eval_active'], description: 'Operation.' },
16
- op: { type: 'string', enum: ['add','edit','delete','list'], description: 'Mutation op.' },
16
+ op: { type: 'string', enum: ['add','edit','delete','list','candidates','promote','dismiss'], description: 'Mutation op. candidates/promote/dismiss drive core-memory proposal approval.' },
17
17
  id: { type: 'number', description: 'Exact memory id.' },
18
18
  element: { type: 'string', description: 'Memory key/title.' },
19
19
  summary: { type: 'string', description: 'Memory content.' },
@@ -38,13 +38,13 @@ export const TOOL_DEFS = [
38
38
  name: 'recall',
39
39
  title: 'Recall',
40
40
  annotations: { title: 'Recall', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
41
- description: 'Retrieve stored memory/session history. Use for earlier work, resumes, or possibly decided items; when in doubt, recall first.',
41
+ description: 'Retrieve stored memory/session history. Use for earlier work, resumes, or possibly decided items; when in doubt, recall first. For "what did we just discuss": pass sessionId with no query to browse the current session (includes in-progress content); period:"last" without sessionId browses the previous completed session only.',
42
42
  inputSchema: {
43
43
  type: 'object',
44
44
  properties: {
45
45
  query: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Search text, or array for independent fan-out queries.' },
46
46
  id: { anyOf: [{ type: 'number' }, { type: 'array', items: { type: 'number' }, minItems: 1 }], description: 'Exact #id(s) from recall. Do not invent ids.' },
47
- period: { type: 'string', description: 'last, 24h/7d/30d, all, date, or range.' },
47
+ period: { type: 'string', description: "last (previous completed session; pair with sessionId instead to include the current one), 24h/7d/30d, all, date, or range." },
48
48
  limit: { type: 'number', description: 'Max entries.' },
49
49
  offset: { type: 'number', description: 'Skip entries.' },
50
50
  sort: { type: 'string', enum: ['importance', 'date'], description: 'importance or date.' },
@@ -52,7 +52,9 @@ export const TOOL_DEFS = [
52
52
  includeArchived: { type: 'boolean', description: 'Include archived entries.' },
53
53
  sessionId: { type: 'string', description: 'Scoped session id.' },
54
54
  session_id: { type: 'string', description: 'Alias for sessionId.' },
55
- includeMembers: { type: 'boolean', description: 'Include chunk members.' },
55
+ includeMembers: { type: 'boolean', description: 'Include chunk members in output; does not widen the search pool.' },
56
+ includeRaw: { type: 'boolean', description: 'Include unchunked raw/episode rows.' },
57
+ sessionOnly: { type: 'boolean', description: 'Search this session only.' },
56
58
  projectScope: { type: 'string', description: 'Project pool selector: inferred from cwd, common, all, or slug.' },
57
59
  cwd: { type: 'string', description: 'Infer projectScope.' },
58
60
  },
@@ -74,8 +76,9 @@ export const TOOL_DEFS = [
74
76
  category: { anyOf: [{ type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'] }, { type: 'array', items: { type: 'string', enum: ['rule','constraint','decision','fact','goal','preference','task','issue'] }, minItems: 1 }], description: 'Category filter.' },
75
77
  limit: { type: 'number', default: 30, description: 'Max entries.' },
76
78
  offset: { type: 'number', default: 0, description: 'Skip entries.' },
77
- includeMembers: { type: 'boolean', description: 'Include members.' },
78
- includeRaw: { type: 'boolean', description: 'Include raw rows.' },
79
+ includeMembers: { type: 'boolean', description: 'Include chunk members in output; does not widen the search pool.' },
80
+ includeRaw: { type: 'boolean', description: 'Include unchunked raw/episode rows.' },
81
+ sessionOnly: { type: 'boolean', description: 'Search this session only.' },
79
82
  includeArchived: { type: 'boolean', description: 'Include archived.' },
80
83
  sessionId: { type: 'string', description: 'Scoped session id.' },
81
84
  session_id: { type: 'string', description: 'Alias for sessionId.' },
@@ -1,5 +1,5 @@
1
1
  /**
2
- * AbortController helpers — ported from a reference agent CLI pattern.
2
+ * AbortController helpers.
3
3
  *
4
4
  * `createAbortController()` raises the signal's max listener cap so long-running
5
5
  * sessions with many per-iteration handlers don't trip Node's default warning.
@@ -5,7 +5,7 @@ import {
5
5
  normalizeToolNotifyContext,
6
6
  notifyToolCompletion,
7
7
  } from './tool-execution-contract.mjs';
8
- import { presentErrorText } from './err-text.mjs';
8
+ import { presentErrorText, errorLine } from './err-text.mjs';
9
9
 
10
10
  export {
11
11
  TOOL_ASYNC_EXECUTION_CONTRACT,
@@ -465,8 +465,7 @@ export function renderBackgroundTask(taskOrId, { includeResult = false } = {}) {
465
465
  if (body) {
466
466
  lines.push('', body);
467
467
  } else if (TERMINAL_STATUSES.has(task.status) && task.error) {
468
- const errorText = String(task.error || '').trim();
469
- lines.push('', /^error\s*:/i.test(errorText) ? errorText : `Error: ${errorText}`);
468
+ lines.push('', errorLine(task.error, { surface: task.surface }));
470
469
  } else if (TERMINAL_STATUSES.has(task.status) && task.status === 'completed') {
471
470
  // Terminal-completed task with no extractable body: surface a placeholder
472
471
  // instead of silently omitting the result so the owner isn't left with a
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Per-file buffered append queue.
3
+ *
4
+ * Hot paths (transcript writer, channel worker stderr log, etc.) previously
5
+ * called appendFileSync per entry/chunk, which blocks the event loop on
6
+ * every write. This module coalesces writes per path into an in-memory
7
+ * buffer and flushes asynchronously via fs.promises.appendFile once the
8
+ * buffer reaches BUFFER_FLUSH_BYTES or after DEBOUNCE_MS of inactivity.
9
+ * Flushes are serialized per path so ordering is preserved. On process
10
+ * exit, any remaining buffered bytes for every path are drained with one
11
+ * synchronous appendFileSync call each (best-effort, matching the
12
+ * agent-trace.mjs local spool exit-drain pattern).
13
+ */
14
+ import { appendFileSync } from 'node:fs';
15
+ import { appendFile } from 'node:fs/promises';
16
+
17
+ const BUFFER_FLUSH_BYTES = 32 * 1024;
18
+ const DEBOUNCE_MS = 50;
19
+
20
+ // path -> { chunks: string[], bytes: number, timer, flushing, inFlight, failCount }
21
+ const queues = new Map();
22
+
23
+ function getQueue(path) {
24
+ let q = queues.get(path);
25
+ if (!q) {
26
+ q = { chunks: [], bytes: 0, timer: null, flushing: false, inFlight: null, failCount: 0 };
27
+ queues.set(path, q);
28
+ }
29
+ return q;
30
+ }
31
+
32
+ function scheduleFlush(path, q) {
33
+ if (q.timer) return;
34
+ q.timer = setTimeout(() => {
35
+ q.timer = null;
36
+ _flush(path, q);
37
+ }, DEBOUNCE_MS);
38
+ q.timer.unref?.();
39
+ }
40
+
41
+ function _flush(path, q) {
42
+ if (q.flushing) return;
43
+ if (q.chunks.length === 0) return;
44
+ const data = q.chunks.join('');
45
+ q.chunks = [];
46
+ q.bytes = 0;
47
+ // Hold the in-flight payload until the write settles so an exit-time
48
+ // sync drain can still recover it if it fires mid-write (fix: exit
49
+ // drain must not miss data that was dequeued but not yet persisted).
50
+ q.inFlight = data;
51
+ q.flushing = true;
52
+ appendFile(path, data, 'utf8')
53
+ .then(() => {
54
+ q.failCount = 0;
55
+ })
56
+ .catch(() => {
57
+ // Requeue the failed data so a later flush/exit-drain retries
58
+ // once, instead of silently dropping. Cap retries so a
59
+ // permanently broken path (e.g. deleted dir) cannot loop
60
+ // forever accumulating the same failing chunk.
61
+ q.failCount += 1;
62
+ if (q.failCount <= 3) {
63
+ q.chunks.unshift(data);
64
+ q.bytes += Buffer.byteLength(data, 'utf8');
65
+ }
66
+ })
67
+ .finally(() => {
68
+ q.inFlight = null;
69
+ q.flushing = false;
70
+ if (q.chunks.length > 0) {
71
+ if (q.bytes >= BUFFER_FLUSH_BYTES) _flush(path, q);
72
+ else scheduleFlush(path, q);
73
+ }
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Queue text for buffered append to `path`. Flushes async once the buffer
79
+ * for that path crosses BUFFER_FLUSH_BYTES, or after DEBOUNCE_MS of
80
+ * inactivity, whichever comes first.
81
+ */
82
+ export function appendBuffered(path, text) {
83
+ if (!path || !text) return;
84
+ const q = getQueue(path);
85
+ q.chunks.push(text);
86
+ q.bytes += Buffer.byteLength(text, 'utf8');
87
+ if (q.bytes >= BUFFER_FLUSH_BYTES) {
88
+ if (q.timer) { clearTimeout(q.timer); q.timer = null; }
89
+ _flush(path, q);
90
+ } else {
91
+ scheduleFlush(path, q);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Synchronously drain any remaining buffered bytes for every known path.
97
+ * Intended for process exit hooks only — best-effort, never throws.
98
+ */
99
+ export function drainAllSync() {
100
+ for (const [path, q] of queues) {
101
+ drainPathSync(path);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Synchronously drain the queue for a single path. Writes any in-flight
107
+ * (already-dequeued but not yet persisted) payload FIRST, then any
108
+ * still-queued chunks, so nothing buffered is lost. Best-effort, never
109
+ * throws.
110
+ */
111
+ export function drainPathSync(path) {
112
+ const q = queues.get(path);
113
+ if (!q) return;
114
+ if (q.timer) { clearTimeout(q.timer); q.timer = null; }
115
+ if (q.inFlight) {
116
+ try {
117
+ appendFileSync(path, q.inFlight, 'utf8');
118
+ } catch {
119
+ // Best-effort; nothing to recover from here.
120
+ }
121
+ q.inFlight = null;
122
+ }
123
+ if (q.chunks.length === 0) return;
124
+ const data = q.chunks.join('');
125
+ q.chunks = [];
126
+ q.bytes = 0;
127
+ try {
128
+ appendFileSync(path, data, 'utf8');
129
+ } catch {
130
+ // Best-effort exit drain; nothing to recover from here.
131
+ }
132
+ }
133
+
134
+ /**
135
+ * True if a path currently has an async write in flight. Callers that need
136
+ * to rename/rotate the underlying file (which would race a concurrent
137
+ * appendFile on some platforms, notably Windows) should check this and
138
+ * defer the rename to a later tick rather than racing it.
139
+ */
140
+ export function hasInFlightWrite(path) {
141
+ const q = queues.get(path);
142
+ return Boolean(q && q.inFlight);
143
+ }
144
+
145
+ try {
146
+ process.on('exit', drainAllSync);
147
+ } catch {
148
+ // Ignore lifecycle hook failures in embedded runtimes.
149
+ }
@@ -0,0 +1,12 @@
1
+ export function channelNotificationModelContent(params = {}) {
2
+ const meta = params?.meta && typeof params.meta === 'object' ? params.meta : {};
3
+ if (meta.silent_to_agent === true || meta.silent_to_agent === 'true') return '';
4
+ const instruction = typeof meta.instruction === 'string' ? meta.instruction.trim() : '';
5
+ const content = String(params?.content || '').trim();
6
+ return instruction || content;
7
+ }
8
+
9
+ export function shouldMirrorChannelNotificationToPending(meta = {}) {
10
+ const type = String(meta?.type || '').trim().toLowerCase();
11
+ return type === 'schedule';
12
+ }
@@ -0,0 +1,45 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {
4
+ channelNotificationModelContent,
5
+ shouldMirrorChannelNotificationToPending,
6
+ } from './channel-notification-routing.mjs';
7
+
8
+ test('channel schedule notification uses instruction as model content', () => {
9
+ const content = channelNotificationModelContent({
10
+ content: ' ',
11
+ meta: {
12
+ type: 'schedule',
13
+ instruction: 'scheduled prompt body',
14
+ },
15
+ });
16
+ assert.equal(content, 'scheduled prompt body');
17
+ });
18
+
19
+ test('silent channel notification has no model content', () => {
20
+ const content = channelNotificationModelContent({
21
+ content: 'hidden',
22
+ meta: {
23
+ type: 'schedule',
24
+ silent_to_agent: true,
25
+ },
26
+ });
27
+ assert.equal(content, '');
28
+ });
29
+
30
+ test('non-schedule notification preserves instruction-first routing', () => {
31
+ const content = channelNotificationModelContent({
32
+ content: 'worker result body',
33
+ meta: {
34
+ type: 'webhook',
35
+ instruction: 'relay instruction',
36
+ },
37
+ });
38
+ assert.equal(content, 'relay instruction');
39
+ });
40
+
41
+ test('only schedule channel notifications are mirrored to pending fallback', () => {
42
+ assert.equal(shouldMirrorChannelNotificationToPending({ type: 'schedule' }), true);
43
+ assert.equal(shouldMirrorChannelNotificationToPending({ type: 'webhook' }), false);
44
+ assert.equal(shouldMirrorChannelNotificationToPending({}), false);
45
+ });
@@ -246,6 +246,7 @@ export function getCapabilities() {
246
246
  // the migration logic in seed.mjs.
247
247
  export const SECRET_ACCOUNTS = Object.freeze({
248
248
  discordToken: 'discord.token',
249
+ telegramToken: 'telegram.token',
249
250
  webhookAuth: 'webhook.authtoken',
250
251
  agentApiKey: (provider) => `agent.${provider}.apiKey`,
251
252
  openaiUsageSessionKey: 'agent.openai.usageSessionKey',
@@ -293,6 +294,14 @@ export function getDiscordToken() {
293
294
  return _readSecret(SECRET_ACCOUNTS.discordToken)
294
295
  }
295
296
 
297
+ /**
298
+ * Returns the Telegram bot token.
299
+ * Priority: MIXDOG_TELEGRAM_TOKEN → keychain('telegram.token') → null
300
+ */
301
+ export function getTelegramToken() {
302
+ return _readSecret(SECRET_ACCOUNTS.telegramToken)
303
+ }
304
+
296
305
  /**
297
306
  * Returns the ngrok/webhook authtoken.
298
307
  * Priority: MIXDOG_WEBHOOK_AUTHTOKEN → keychain('webhook.authtoken') → null