mixdog 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (417) hide show
  1. package/package.json +8 -3
  2. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  3. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  4. package/scripts/bench/lead-review-tasks.json +20 -0
  5. package/scripts/bench/r4-mixed-tasks.json +20 -0
  6. package/scripts/bench/review-tasks.json +20 -0
  7. package/scripts/bench/round-codex.json +114 -0
  8. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  9. package/scripts/bench/round-mixdog-lead.json +269 -0
  10. package/scripts/bench/round-mixdog.json +126 -0
  11. package/scripts/bench/round-r10-bigsample.json +679 -0
  12. package/scripts/bench/round-r11-codexalign.json +257 -0
  13. package/scripts/bench/round-r4-codex.json +114 -0
  14. package/scripts/bench/round-r4-mixed.json +225 -0
  15. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  16. package/scripts/bench/round-r6-codex.json +114 -0
  17. package/scripts/bench/round-r6-solo.json +257 -0
  18. package/scripts/bench/round-r7-full.json +254 -0
  19. package/scripts/bench/round-r8-fulldefault.json +255 -0
  20. package/scripts/bench-run.mjs +215 -29
  21. package/scripts/build-tui.mjs +13 -1
  22. package/scripts/explore-bench.mjs +124 -0
  23. package/scripts/freevar-smoke.mjs +95 -0
  24. package/scripts/hook-bus-test.mjs +191 -0
  25. package/scripts/internal-comms-bench.mjs +1 -0
  26. package/scripts/internal-comms-smoke.mjs +10 -9
  27. package/scripts/mouse-probe.mjs +45 -0
  28. package/scripts/output-style-bench.mjs +13 -6
  29. package/scripts/output-style-smoke.mjs +4 -4
  30. package/scripts/path-suffix-test.mjs +57 -0
  31. package/scripts/provider-toolcall-test.mjs +7 -3
  32. package/scripts/recall-bench.mjs +207 -0
  33. package/scripts/recall-usecase-cases.json +18 -0
  34. package/scripts/recall-usecase-probe.json +6 -0
  35. package/scripts/session-bench.mjs +152 -6
  36. package/scripts/tool-smoke.mjs +30 -67
  37. package/scripts/tui-render-smoke.mjs +90 -0
  38. package/scripts/webhook-smoke.mjs +208 -0
  39. package/src/agents/debugger/AGENT.md +5 -2
  40. package/src/agents/heavy-worker/AGENT.md +21 -11
  41. package/src/agents/maintainer/AGENT.md +4 -0
  42. package/src/agents/reviewer/AGENT.md +3 -2
  43. package/src/agents/worker/AGENT.md +21 -11
  44. package/src/lib/rules-builder.cjs +4 -0
  45. package/src/mixdog-session-runtime.mjs +933 -3731
  46. package/src/output-styles/default.md +34 -9
  47. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  48. package/src/output-styles/minimal.md +4 -1
  49. package/src/output-styles/simple.md +22 -7
  50. package/src/repl.mjs +5 -5
  51. package/src/rules/agent/00-common.md +2 -0
  52. package/src/rules/agent/30-explorer.md +8 -11
  53. package/src/rules/lead/lead-brief.md +12 -0
  54. package/src/rules/lead/lead-tool.md +2 -9
  55. package/src/rules/shared/01-tool.md +11 -5
  56. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  57. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  58. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  59. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  60. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  61. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  62. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  63. package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
  64. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  65. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +63 -21
  66. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  67. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  68. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  69. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +97 -1343
  70. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  71. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +78 -10
  72. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
  73. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  74. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  75. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  76. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  77. package/src/runtime/agent/orchestrator/providers/gemini.mjs +44 -1014
  78. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +18 -4
  79. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  80. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
  81. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
  82. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  83. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  84. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
  85. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  86. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  87. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  88. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +41 -1142
  89. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
  90. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  91. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +303 -2119
  92. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +140 -995
  93. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
  94. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  95. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
  96. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  97. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
  99. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
  100. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  101. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  102. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  103. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  104. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  105. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  106. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  107. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  108. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  109. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +274 -0
  110. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  111. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  112. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  113. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  114. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +47 -0
  115. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
  116. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  117. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  118. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  119. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  120. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  121. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  122. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  123. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  124. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  125. package/src/runtime/agent/orchestrator/session/loop.mjs +409 -1304
  126. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  127. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +230 -0
  128. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  129. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +149 -0
  130. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  131. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  132. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  133. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  134. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  135. package/src/runtime/agent/orchestrator/session/manager.mjs +226 -2114
  136. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  137. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  138. package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
  139. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  140. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  141. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +33 -42
  142. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  143. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  144. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  145. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  146. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
  147. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  148. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  149. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -87
  152. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +78 -304
  155. package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -6
  156. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  157. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  158. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  159. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +551 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  161. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  163. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  164. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  165. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  166. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1080 -0
  167. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  168. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  169. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  170. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  171. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  172. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  173. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
  174. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  175. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  176. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  177. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  178. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  179. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  180. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  181. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  182. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  183. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  184. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -23
  185. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +10 -74
  186. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  187. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  188. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  189. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  190. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  191. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  192. package/src/runtime/channels/backends/discord.mjs +12 -292
  193. package/src/runtime/channels/index.mjs +241 -894
  194. package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
  195. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  196. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  197. package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
  198. package/src/runtime/channels/lib/event-queue.mjs +63 -4
  199. package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
  200. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  201. package/src/runtime/channels/lib/output-forwarder.mjs +9 -1
  202. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  203. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  204. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  205. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  206. package/src/runtime/channels/lib/telegram-format.mjs +19 -22
  207. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  208. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  209. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  210. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  211. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  212. package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
  213. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  214. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  215. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  216. package/src/runtime/channels/lib/webhook.mjs +36 -570
  217. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  218. package/src/runtime/channels/tool-defs.mjs +11 -130
  219. package/src/runtime/memory/index.mjs +258 -2050
  220. package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
  221. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  222. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  223. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  224. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  225. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  226. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  227. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  228. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  229. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  230. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  231. package/src/runtime/memory/lib/memory-cycle2.mjs +72 -837
  232. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  233. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  234. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  235. package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
  236. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  237. package/src/runtime/memory/lib/memory.mjs +20 -0
  238. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  239. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  240. package/src/runtime/memory/lib/query-handlers.mjs +780 -0
  241. package/src/runtime/memory/lib/recall-format.mjs +238 -0
  242. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  243. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  244. package/src/runtime/memory/tool-defs.mjs +6 -14
  245. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  246. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  247. package/src/runtime/search/lib/web-tools.mjs +24 -602
  248. package/src/runtime/shared/abort-controller.mjs +1 -1
  249. package/src/runtime/shared/atomic-file.mjs +26 -1
  250. package/src/runtime/shared/background-tasks.mjs +2 -3
  251. package/src/runtime/shared/buffered-appender.mjs +149 -0
  252. package/src/runtime/shared/launcher-control.mjs +2 -2
  253. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  254. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  255. package/src/runtime/shared/tool-primitives.mjs +308 -0
  256. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  257. package/src/runtime/shared/tool-surface.mjs +80 -898
  258. package/src/runtime/shared/transcript-writer.mjs +52 -2
  259. package/src/runtime/shared/update-checker.mjs +7 -4
  260. package/src/session-runtime/config-helpers.mjs +291 -0
  261. package/src/session-runtime/config-lifecycle.mjs +232 -0
  262. package/src/session-runtime/cwd-plugins.mjs +226 -0
  263. package/src/session-runtime/effort.mjs +128 -0
  264. package/src/session-runtime/fs-utils.mjs +10 -0
  265. package/src/session-runtime/mcp-glue.mjs +177 -0
  266. package/src/session-runtime/model-capabilities.mjs +130 -0
  267. package/src/session-runtime/model-recency.mjs +111 -0
  268. package/src/session-runtime/native-search.mjs +247 -0
  269. package/src/session-runtime/output-styles.mjs +126 -0
  270. package/src/session-runtime/plugin-mcp.mjs +114 -0
  271. package/src/session-runtime/prewarm.mjs +142 -0
  272. package/src/session-runtime/provider-models.mjs +278 -0
  273. package/src/session-runtime/provider-usage.mjs +120 -0
  274. package/src/session-runtime/quick-model-rows.mjs +170 -0
  275. package/src/session-runtime/quick-search-models.mjs +46 -0
  276. package/src/session-runtime/session-hooks.mjs +93 -0
  277. package/src/session-runtime/session-text.mjs +100 -0
  278. package/src/session-runtime/settings-api.mjs +319 -0
  279. package/src/session-runtime/statusline-route.mjs +35 -0
  280. package/src/session-runtime/tool-catalog.mjs +720 -0
  281. package/src/session-runtime/tool-defs.mjs +84 -0
  282. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  283. package/src/session-runtime/workflow.mjs +358 -0
  284. package/src/standalone/agent-tool/helpers.mjs +237 -0
  285. package/src/standalone/agent-tool/notify.mjs +107 -0
  286. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  287. package/src/standalone/agent-tool/render.mjs +152 -0
  288. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  289. package/src/standalone/agent-tool.mjs +155 -677
  290. package/src/standalone/channel-worker.mjs +7 -9
  291. package/src/standalone/explore-tool.mjs +40 -12
  292. package/src/standalone/hook-bus/config.mjs +207 -0
  293. package/src/standalone/hook-bus/constants.mjs +90 -0
  294. package/src/standalone/hook-bus/handlers.mjs +481 -0
  295. package/src/standalone/hook-bus/payload.mjs +31 -0
  296. package/src/standalone/hook-bus/rules.mjs +77 -0
  297. package/src/standalone/hook-bus.mjs +110 -746
  298. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  299. package/src/standalone/opencode-go-login.mjs +125 -0
  300. package/src/standalone/provider-admin.mjs +15 -19
  301. package/src/standalone/usage-dashboard.mjs +3 -1
  302. package/src/tui/App.jsx +1163 -7571
  303. package/src/tui/app/app-format.mjs +206 -0
  304. package/src/tui/app/channel-pickers.mjs +510 -0
  305. package/src/tui/app/clipboard.mjs +67 -0
  306. package/src/tui/app/core-memory-picker.mjs +210 -0
  307. package/src/tui/app/extension-pickers.mjs +506 -0
  308. package/src/tui/app/input-parsers.mjs +193 -0
  309. package/src/tui/app/maintenance-pickers.mjs +324 -0
  310. package/src/tui/app/model-options.mjs +330 -0
  311. package/src/tui/app/model-picker.mjs +365 -0
  312. package/src/tui/app/onboarding-steps.mjs +400 -0
  313. package/src/tui/app/project-picker.mjs +247 -0
  314. package/src/tui/app/provider-setup-picker.mjs +580 -0
  315. package/src/tui/app/resume-picker.mjs +55 -0
  316. package/src/tui/app/route-pickers.mjs +419 -0
  317. package/src/tui/app/settings-picker.mjs +490 -0
  318. package/src/tui/app/slash-commands.mjs +101 -0
  319. package/src/tui/app/slash-dispatch.mjs +427 -0
  320. package/src/tui/app/text-layout.mjs +46 -0
  321. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  322. package/src/tui/app/transcript-window.mjs +671 -0
  323. package/src/tui/app/use-mouse-input.mjs +460 -0
  324. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  325. package/src/tui/app/use-transcript-scroll.mjs +510 -0
  326. package/src/tui/app/use-transcript-window.mjs +589 -0
  327. package/src/tui/components/ConfirmBar.jsx +1 -1
  328. package/src/tui/components/Picker.jsx +32 -4
  329. package/src/tui/components/PromptInput.jsx +259 -80
  330. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  331. package/src/tui/components/StatusLine.jsx +63 -12
  332. package/src/tui/components/TextEntryPanel.jsx +11 -0
  333. package/src/tui/components/ToolExecution.jsx +56 -588
  334. package/src/tui/components/TranscriptItem.jsx +105 -0
  335. package/src/tui/components/UsagePanel.jsx +18 -4
  336. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  337. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  338. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  339. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  340. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  341. package/src/tui/components/tool-output-format.mjs +2 -2
  342. package/src/tui/display-width.mjs +20 -3
  343. package/src/tui/dist/index.mjs +18034 -17188
  344. package/src/tui/engine/agent-envelope.mjs +296 -0
  345. package/src/tui/engine/agent-job-feed.mjs +133 -0
  346. package/src/tui/engine/boot-profile.mjs +21 -0
  347. package/src/tui/engine/labels.mjs +67 -0
  348. package/src/tui/engine/notice-text.mjs +112 -0
  349. package/src/tui/engine/notification-plan.mjs +76 -0
  350. package/src/tui/engine/queue-helpers.mjs +161 -0
  351. package/src/tui/engine/render-timing.mjs +17 -0
  352. package/src/tui/engine/session-stats.mjs +46 -0
  353. package/src/tui/engine/tool-approval.mjs +94 -0
  354. package/src/tui/engine/tool-call-fields.mjs +23 -0
  355. package/src/tui/engine/tool-card-results.mjs +234 -0
  356. package/src/tui/engine/tool-result-status.mjs +135 -0
  357. package/src/tui/engine/tool-result-text.mjs +126 -0
  358. package/src/tui/engine.mjs +405 -1385
  359. package/src/tui/figures.mjs +5 -0
  360. package/src/tui/index.jsx +105 -0
  361. package/src/tui/input-editing.mjs +60 -10
  362. package/src/tui/keyboard-protocol.mjs +2 -2
  363. package/src/tui/lib/voice-recorder.mjs +35 -19
  364. package/src/tui/markdown/format-token.mjs +11 -9
  365. package/src/tui/paste-attachments.mjs +38 -0
  366. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  367. package/src/tui/theme.mjs +6 -0
  368. package/src/tui/themes/base.mjs +2 -2
  369. package/src/tui/themes/kanagawa.mjs +4 -4
  370. package/src/tui/themes/teal.mjs +4 -5
  371. package/src/tui/themes/utils.mjs +1 -1
  372. package/src/ui/statusline-agents.mjs +213 -0
  373. package/src/ui/statusline-format.mjs +146 -0
  374. package/src/ui/statusline-segments.mjs +148 -0
  375. package/src/ui/statusline.mjs +77 -462
  376. package/src/ui/tool-card.mjs +0 -1
  377. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  378. package/src/workflows/default/WORKFLOW.md +16 -9
  379. package/src/workflows/sequential/WORKFLOW.md +16 -11
  380. package/src/workflows/solo/WORKFLOW.md +5 -1
  381. package/vendor/ink/build/display-width.js +19 -3
  382. package/vendor/ink/build/ink.js +103 -6
  383. package/vendor/ink/build/log-update.js +17 -3
  384. package/vendor/ink/build/wrap-text.js +125 -0
  385. package/scripts/_test-folder-dialog.mjs +0 -30
  386. package/scripts/fix-brief-fn.mjs +0 -35
  387. package/scripts/fix-format-tool-surface.mjs +0 -24
  388. package/scripts/fix-tool-exec-visible.mjs +0 -42
  389. package/scripts/patch-agent-brief.mjs +0 -48
  390. package/scripts/patch-app.mjs +0 -21
  391. package/scripts/patch-app2.mjs +0 -18
  392. package/scripts/patch-dist-brief.mjs +0 -96
  393. package/scripts/patch-tool-exec.mjs +0 -70
  394. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  395. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  396. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  397. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  398. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  399. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  400. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  401. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  402. package/src/standalone/agent-task-status.test.mjs +0 -76
  403. package/src/tui/components/tool-output-format.test.mjs +0 -399
  404. package/src/tui/display-width.test.mjs +0 -35
  405. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  406. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  407. package/src/tui/markdown/format-token.test.mjs +0 -354
  408. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  409. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  410. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  411. package/src/tui/prompt-history-store.test.mjs +0 -52
  412. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  413. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  414. package/src/ui/markdown.test.mjs +0 -70
  415. package/src/ui/statusline-context-label.test.mjs +0 -15
  416. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  417. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -0,0 +1,238 @@
1
+ import { cleanMemoryText } from './memory.mjs'
2
+
3
+ // Recall query/format helpers extracted verbatim from index.mjs
4
+ // (behavior-preserving). Pure string/date logic plus row rendering.
5
+
6
+ export function parsePeriod(period, hasQuery) {
7
+ if (!period && hasQuery) period = '30d'
8
+ if (!period) return null
9
+ if (period === 'all') return null
10
+ if (period === 'last') return { mode: 'last' }
11
+ // Calendar-day windows: 'today' anchors at local midnight rather than
12
+ // rolling 24h. Without this, a query asking 'today' at 01:30 would silently
13
+ // include yesterday's last 22.5h of activity, mislabelling them as
14
+ // 'today's work'. 'yesterday' is the previous calendar day.
15
+ if (period === 'today') {
16
+ const start = new Date()
17
+ start.setHours(0, 0, 0, 0)
18
+ return { startMs: start.getTime(), endMs: Date.now() }
19
+ }
20
+ if (period === 'yesterday') {
21
+ const start = new Date()
22
+ start.setDate(start.getDate() - 1)
23
+ start.setHours(0, 0, 0, 0)
24
+ const end = new Date(start)
25
+ end.setHours(23, 59, 59, 999)
26
+ return { startMs: start.getTime(), endMs: end.getTime() }
27
+ }
28
+ if (period === 'this_week' || period === 'last_week') {
29
+ // R6 P9: calendar Mon-Sun previous/current week. Mon-start ISO
30
+ // convention. Replaces R5 rolling 7-14d range which was empty for
31
+ // sessions where "last week" decisions actually fell on Mon (4/27) of
32
+ // this week. Precise calendar bounds match natural-language intuition.
33
+ const d = new Date()
34
+ d.setHours(0, 0, 0, 0)
35
+ const dayOfWeek = d.getDay()
36
+ const daysSinceMon = (dayOfWeek + 6) % 7
37
+ const thisWeekMon = new Date(d)
38
+ thisWeekMon.setDate(d.getDate() - daysSinceMon)
39
+ if (period === 'this_week') {
40
+ return { startMs: thisWeekMon.getTime(), endMs: Date.now() }
41
+ }
42
+ const lastWeekMon = new Date(thisWeekMon)
43
+ lastWeekMon.setDate(thisWeekMon.getDate() - 7)
44
+ const lastWeekSunEnd = new Date(thisWeekMon.getTime() - 1)
45
+ return { startMs: lastWeekMon.getTime(), endMs: lastWeekSunEnd.getTime() }
46
+ }
47
+ const relMatch = period.match(/^(\d+)(m|h|d)$/)
48
+ if (relMatch) {
49
+ const n = parseInt(relMatch[1])
50
+ const unit = relMatch[2]
51
+ const now = new Date()
52
+ if (unit === 'm') {
53
+ // Minute granularity is for "resume from the previous turn / pick
54
+ // up where we left off" style recall — sub-hour windows where 1h
55
+ // is too coarse. n=0 is invalid (the regex requires \d+ which
56
+ // matches "0" but a zero-width window returns no rows; leave that
57
+ // as caller-supplied no-op).
58
+ const start = new Date(now.getTime() - n * 60_000)
59
+ return { startMs: start.getTime(), endMs: now.getTime() }
60
+ }
61
+ if (unit === 'h') {
62
+ const start = new Date(now.getTime() - n * 3600_000)
63
+ return { startMs: start.getTime(), endMs: now.getTime() }
64
+ }
65
+ const start = new Date(now)
66
+ start.setDate(start.getDate() - n)
67
+ return { startMs: start.getTime(), endMs: now.getTime() }
68
+ }
69
+ const rangeMatch = period.match(/^(\d{4}-\d{2}-\d{2})~(\d{4}-\d{2}-\d{2})$/)
70
+ if (rangeMatch) {
71
+ return {
72
+ startMs: Date.parse(rangeMatch[1] + 'T00:00:00'),
73
+ endMs: Date.parse(rangeMatch[2] + 'T23:59:59.999'),
74
+ }
75
+ }
76
+ // Time-of-day windows: 'HH:MM~HH:MM' (today) or 'YYYY-MM-DD HH:MM~HH:MM'
77
+ // (specific day). Covers "this afternoon 12:00-14:00" style recall that the
78
+ // day-granular date/range forms cannot express. End is inclusive to the
79
+ // minute (:59.999). An end at or before start returns null (invalid window)
80
+ // rather than guessing an overnight wrap.
81
+ const todMatch = period.match(/^(?:(\d{4}-\d{2}-\d{2})[ T])?(\d{1,2}):(\d{2})~(\d{1,2}):(\d{2})$/)
82
+ if (todMatch) {
83
+ const [, day, h1, m1, h2, m2] = todMatch
84
+ const base = day ? new Date(day + 'T00:00:00') : new Date()
85
+ if (Number.isNaN(base.getTime())) return null
86
+ const sh = Number(h1), sm = Number(m1), eh = Number(h2), em = Number(m2)
87
+ if (sh > 23 || eh > 23 || sm > 59 || em > 59) return null
88
+ const start = new Date(base); start.setHours(sh, sm, 0, 0)
89
+ const end = new Date(base); end.setHours(eh, em, 59, 999)
90
+ if (end.getTime() <= start.getTime()) return null
91
+ return { startMs: start.getTime(), endMs: end.getTime(), exact: true }
92
+ }
93
+ const dateMatch = period.match(/^(\d{4}-\d{2}-\d{2})$/)
94
+ if (dateMatch) {
95
+ return {
96
+ startMs: Date.parse(dateMatch[1] + 'T00:00:00'),
97
+ endMs: Date.parse(dateMatch[1] + 'T23:59:59.999'),
98
+ exact: true,
99
+ }
100
+ }
101
+ return null
102
+ }
103
+
104
+ export function formatTs(tsMs) {
105
+ const n = Number(tsMs)
106
+ if (Number.isFinite(n) && n > 1e12) {
107
+ return new Date(n).toLocaleString('sv-SE').slice(0, 16)
108
+ }
109
+ return String(tsMs ?? '').slice(0, 16)
110
+ }
111
+
112
+ export const CORE_RECALL_STOPWORDS = new Set([
113
+ 'about', 'after', 'again', 'before', 'check', 'color', 'decision', 'decided',
114
+ 'earlier', 'memory', 'previous', 'routing', 'stored', 'tell', 'what',
115
+ ])
116
+
117
+ export function coreRecallTerms(query) {
118
+ return [...new Set(String(query || '').toLowerCase().match(/[\p{L}\p{N}_-]{4,}/gu) || [])]
119
+ .filter((term) => !CORE_RECALL_STOPWORDS.has(term))
120
+ .slice(0, 8)
121
+ }
122
+
123
+ export function normalizeRecallProjectScope(projectScope) {
124
+ const raw = String(projectScope || 'common').trim()
125
+ if (!raw || raw.toLowerCase() === 'common') return null
126
+ if (raw.toLowerCase() === 'all') return '*'
127
+ return raw
128
+ }
129
+
130
+ export function sessionRecallTerms(query) {
131
+ return [...new Set(String(query || '').toLowerCase().match(/[\p{L}\p{N}_./:-]{2,}/gu) || [])]
132
+ .filter((term) => !CORE_RECALL_STOPWORDS.has(term))
133
+ .slice(0, 12)
134
+ }
135
+
136
+ export function interleaveRawRows(hybridRows, rawRows) {
137
+ if (!Array.isArray(rawRows) || rawRows.length === 0) return hybridRows
138
+ const out = []
139
+ const stride = Math.max(1, Math.round(hybridRows.length / (rawRows.length + 1)))
140
+ let rawIdx = 0
141
+ for (let i = 0; i < hybridRows.length; i += 1) {
142
+ out.push(hybridRows[i])
143
+ if ((i + 1) % stride === 0 && rawIdx < rawRows.length) {
144
+ out.push(rawRows[rawIdx])
145
+ rawIdx += 1
146
+ }
147
+ }
148
+ while (rawIdx < rawRows.length) out.push(rawRows[rawIdx++])
149
+ return out
150
+ }
151
+
152
+ export function renderEntryLines(rows) {
153
+ if (!rows || rows.length === 0) return '(no results)'
154
+ const lines = []
155
+ // Bound total emitted lines (roots x members) so a many-member recall can't
156
+ // inject unbounded output. Per-line content is already capped at 1000 chars;
157
+ // this caps the line COUNT. Narrow the query (limit/period/projectScope) for more.
158
+ const RECALL_LINE_CAP = 200
159
+ let _capped = false
160
+ outer:
161
+ for (const r of rows) {
162
+ const hasMembers = Array.isArray(r.members) && r.members.length > 0
163
+ if (hasMembers) {
164
+ // Chunks present: emit each member as its own line. Root row is a
165
+ // grouping artifact for retrieval — the caller wants the chunk
166
+ // content (cycle1 raw), not the cycle2-compressed summary.
167
+ for (const m of r.members) {
168
+ if (lines.length >= RECALL_LINE_CAP) { _capped = true; break outer }
169
+ const mTs = formatTs(m.ts)
170
+ const role = m.role === 'user' ? 'u' : m.role === 'assistant' ? 'a' : (m.role || '?')
171
+ const content = cleanMemoryText(String(m.content ?? '')).slice(0, 1000)
172
+ lines.push(`[${mTs}] ${role}: ${content} #${m.id}`)
173
+ }
174
+ } else {
175
+ if (lines.length >= RECALL_LINE_CAP) { _capped = true; break }
176
+ // No chunks (root not yet chunked by cycle1, or orphan leaf): emit
177
+ // the row itself in the same shape. element/summary fall back to
178
+ // raw content when both are absent.
179
+ const ts = formatTs(r.ts)
180
+ const element = r.element ?? ''
181
+ const summary = r.summary ?? ''
182
+ // Standalone leaf rows (is_root=0, no parent chunks_root resolved
183
+ // into a `members` list) carry their u/a role just like inline
184
+ // chunk members — surface it so the format stays consistent across
185
+ // the two emission paths.
186
+ const rolePrefix = r.is_root === 0 && r.role
187
+ ? (r.role === 'user' ? 'u: ' : r.role === 'assistant' ? 'a: ' : `${r.role}: `)
188
+ : ''
189
+ const body = element || summary
190
+ ? `${element}${summary ? ' — ' + summary : ''}`
191
+ : cleanMemoryText(String(r.content ?? '')).slice(0, 1000)
192
+ // Unchunked raw leaf (cycle1 hasn't classified it yet): mark it so
193
+ // callers can tell fresh-but-unprocessed rows from chunked memory.
194
+ const pendingMark = (r.is_root === 0 && r.chunk_root == null) ? ' [pending]' : ''
195
+ lines.push(`[${ts}] ${rolePrefix}${body.slice(0, 1000)}${pendingMark} #${r.id}`)
196
+ }
197
+ }
198
+ if (_capped) lines.push(`[recall truncated — showing first ${RECALL_LINE_CAP} lines; narrow the query (limit/period/projectScope) for the rest]`)
199
+ return lines.join('\n')
200
+ }
201
+
202
+ // Compact session label for group headers: keep short ids verbatim, shorten
203
+ // long ones to a recognizable tail (ids are typically unique in the suffix —
204
+ // timestamp/counter — not the prefix).
205
+ function shortSessionLabel(sid) {
206
+ const s = String(sid || '').trim()
207
+ if (s.length <= 20) return s
208
+ return `…${s.slice(-16)}`
209
+ }
210
+
211
+ // Session-grouped rendering for the GLOBAL query-less browse ("what did we
212
+ // work on recently") when the window spans multiple sessions. A flat ts-desc
213
+ // list interleaves sessions into one indistinguishable stream and lets one
214
+ // chatty session crowd the page; grouping keeps each work-unit readable.
215
+ // Group order = newest activity first (rows arrive ts-desc, first-seen wins).
216
+ // The caller's own session (currentSessionId hint) is marked "(current)".
217
+ // Single-session (or session-less) result sets fall through to the flat
218
+ // renderer — no headers when grouping adds nothing.
219
+ export function renderSessionGroupedLines(rows, { currentSessionId } = {}) {
220
+ if (!rows || rows.length === 0) return '(no results)'
221
+ const groups = new Map()
222
+ for (const r of rows) {
223
+ const sid = String(r?.session_id || '').trim()
224
+ const key = sid || '(no session)'
225
+ if (!groups.has(key)) groups.set(key, [])
226
+ groups.get(key).push(r)
227
+ }
228
+ if (groups.size <= 1) return renderEntryLines(rows)
229
+ const current = String(currentSessionId || '').trim()
230
+ const parts = []
231
+ for (const [sid, groupRows] of groups) {
232
+ const mark = current && sid === current ? ' (current)' : ''
233
+ const label = sid === '(no session)' ? sid : `session ${shortSessionLabel(sid)}`
234
+ parts.push(`## ${label}${mark}`)
235
+ parts.push(renderEntryLines(groupRows))
236
+ }
237
+ return parts.join('\n')
238
+ }
@@ -75,13 +75,18 @@ function isUsableAsset(asset) {
75
75
  // ---------------------------------------------------------------------------
76
76
 
77
77
  async function loadManifest(dataDir) {
78
+ // Bundled manifest FIRST: it always matches the installed mixdog version.
79
+ // A cached <dataDir>/runtime/manifest.json from an older install must never
80
+ // shadow it — stale caches caused sha256 mismatches on already-installed
81
+ // machines after an upgrade. The cache is only a fallback for the rare case
82
+ // where the bundled file is missing (e.g. a stripped checkout).
83
+ if (existsSync(BUNDLED_MANIFEST_PATH)) {
84
+ return JSON.parse(readFileSync(BUNDLED_MANIFEST_PATH, 'utf8'))
85
+ }
78
86
  const runtimeManifestPath = join(dataDir, 'runtime', 'manifest.json')
79
87
  if (existsSync(runtimeManifestPath)) {
80
88
  try { return JSON.parse(readFileSync(runtimeManifestPath, 'utf8')) } catch {}
81
89
  }
82
- if (existsSync(BUNDLED_MANIFEST_PATH)) {
83
- return JSON.parse(readFileSync(BUNDLED_MANIFEST_PATH, 'utf8'))
84
- }
85
90
  const res = await fetch(MANIFEST_URL, { signal: AbortSignal.timeout(30_000) })
86
91
  if (!res.ok) throw new Error(`[runtime-fetcher] manifest fetch failed: ${res.status} ${res.statusText}`)
87
92
  const manifest = await res.json()
@@ -0,0 +1,425 @@
1
+ // Transcript ingest cluster, extracted from index.mjs (pass 2).
2
+ //
3
+ // Owns: transcript offset load/persist, JSONL tail ingestion, cwd extraction,
4
+ // timestamp coercion, and the cross-platform transcript watcher. Everything
5
+ // that touched module-level state in index.mjs (db handle, mainConfig-derived
6
+ // data dir, the _transcriptOffsets / _ingestTranscriptTails maps, the persist
7
+ // tail promise) is now closed over inside createTranscriptIngest via injected
8
+ // accessors, so index.mjs keeps ownership of the live db/config lifecycle and
9
+ // this module stays a pure factory with no import-time side effects.
10
+ import fs from 'node:fs'
11
+ import path from 'node:path'
12
+
13
+ // Pure: coerce a transcript timestamp (seconds, ms, or ISO string) to ms.
14
+ export function parseTsToMs(value) {
15
+ if (typeof value === 'number' && Number.isFinite(value)) return value < 1e12 ? value * 1000 : value
16
+ const parsed = Date.parse(String(value))
17
+ return Number.isFinite(parsed) ? parsed : Date.now()
18
+ }
19
+
20
+ // Pure: extract cwd from the transcript file's JSONL rows. Mixdog embeds the
21
+ // session cwd as a top-level `cwd` field on every message row, so scanning the
22
+ // first few lines is reliable on all platforms without slug-decoding ambiguity.
23
+ // Returns undefined when no cwd is found or the extracted path does not exist
24
+ // on disk (falls back to COMMON).
25
+ export function cwdFromTranscriptPath(fp) {
26
+ let fd
27
+ try {
28
+ fd = fs.openSync(fp, 'r')
29
+ const buf = Buffer.alloc(Math.min(fs.fstatSync(fd).size, 100 * 1024))
30
+ fs.readSync(fd, buf, 0, buf.length, 0)
31
+ fs.closeSync(fd)
32
+ fd = undefined
33
+ const lines = buf.toString('utf8').split('\n')
34
+ for (let i = 0; i < Math.min(lines.length, 5); i++) {
35
+ const line = lines[i].trim()
36
+ if (!line) continue
37
+ try {
38
+ const obj = JSON.parse(line)
39
+ if (typeof obj.cwd === 'string' && obj.cwd) {
40
+ const candidate = obj.cwd
41
+ try { if (fs.statSync(candidate).isDirectory()) return candidate } catch {}
42
+ }
43
+ } catch {}
44
+ }
45
+ } catch {} finally {
46
+ if (fd != null) { try { fs.closeSync(fd) } catch {} }
47
+ }
48
+ return undefined
49
+ }
50
+
51
+ // Factory. All live-state coupling is injected:
52
+ // getDb() -> current pg-shim db handle (or null pre-init)
53
+ // loadMeta() -> Promise<string> raw transcript-offsets meta value
54
+ // persistMeta(json) -> Promise<void> writes serialized offsets to meta
55
+ // projectsRoot() -> string, mixdogHome()/projects
56
+ // resolveProjectId(cwd) -> project id | null
57
+ // firstTextContent / cleanMemoryText -> shared ingest text helpers
58
+ // log(msg) -> stderr logger
59
+ //
60
+ // Returns the same surface index.mjs previously exposed as module functions,
61
+ // plus resetOffsets() for stop()'s teardown.
62
+ export function createTranscriptIngest({
63
+ getDb,
64
+ loadMeta,
65
+ persistMeta,
66
+ projectsRoot,
67
+ resolveProjectId,
68
+ firstTextContent,
69
+ cleanMemoryText,
70
+ log = () => {},
71
+ }) {
72
+ let _transcriptOffsets = new Map()
73
+ /** @type {Map<string, Promise<unknown>>} */
74
+ const _ingestTranscriptTails = new Map()
75
+ let _transcriptOffsetsPersistTail = Promise.resolve()
76
+
77
+ async function loadTranscriptOffsets() {
78
+ try {
79
+ const raw = await loadMeta()
80
+ const obj = JSON.parse(raw)
81
+ _transcriptOffsets = new Map(Object.entries(obj))
82
+ } catch {
83
+ _transcriptOffsets = new Map()
84
+ }
85
+ }
86
+
87
+ async function persistTranscriptOffsets() {
88
+ const run = _transcriptOffsetsPersistTail.catch(() => {}).then(async () => {
89
+ try {
90
+ const obj = Object.fromEntries(_transcriptOffsets)
91
+ await persistMeta(JSON.stringify(obj))
92
+ } catch (e) {
93
+ log(`[memory] persist transcript offsets failed: ${e.message}\n`)
94
+ }
95
+ })
96
+ _transcriptOffsetsPersistTail = run.catch(() => {})
97
+ return run
98
+ }
99
+
100
+ function runTranscriptIngestSerialized(transcriptPath, fn) {
101
+ const key = path.resolve(transcriptPath)
102
+ const prev = _ingestTranscriptTails.get(key) ?? Promise.resolve()
103
+ const run = prev.catch(() => {}).then(fn)
104
+ _ingestTranscriptTails.set(key, run.catch(() => {}))
105
+ return run
106
+ }
107
+
108
+ function snapshotTranscriptOffset(transcriptPath) {
109
+ const stored = _transcriptOffsets.get(transcriptPath)
110
+ if (!stored) return { bytes: 0, lineIndex: 0 }
111
+ return { bytes: Number(stored.bytes) || 0, lineIndex: Number(stored.lineIndex) || 0 }
112
+ }
113
+
114
+ async function ingestTranscriptFileImpl(transcriptPath, { cwd } = {}) {
115
+ const db = getDb()
116
+ let stat
117
+ try { stat = await fs.promises.stat(transcriptPath) } catch { return 0 }
118
+ const sessionUuid = path.basename(transcriptPath, '.jsonl')
119
+ const prev = snapshotTranscriptOffset(transcriptPath)
120
+ if (stat.size < prev.bytes) {
121
+ prev.bytes = 0
122
+ prev.lineIndex = 0
123
+ }
124
+ if (stat.size <= prev.bytes) return 0
125
+
126
+ const fh = await fs.promises.open(transcriptPath, 'r')
127
+ const buf = Buffer.alloc(stat.size - prev.bytes)
128
+ try {
129
+ await fh.read(buf, 0, buf.length, prev.bytes)
130
+ } finally {
131
+ await fh.close()
132
+ }
133
+ const text = buf.toString('utf8')
134
+
135
+ const resolvedCwd = typeof cwd === 'string' && cwd ? cwd : cwdFromTranscriptPath(transcriptPath)
136
+ // No cwd resolved -> classify as COMMON (project_id NULL). Falling back to
137
+ // process.cwd() would misclassify rows under the service/plugin cwd.
138
+ const projectId = resolvedCwd ? resolveProjectId(resolvedCwd) : null
139
+
140
+ let count = 0
141
+ let index = prev.lineIndex
142
+ // Track the byte boundary of the LAST line we fully consumed (parsed +
143
+ // either inserted or intentionally skipped). On parse failure or
144
+ // transient insert error we stop and leave the boundary untouched so the
145
+ // next sweep retries from the same position. This prevents malformed
146
+ // trailing JSONL (mid-write partial lines) and DB hiccups from being
147
+ // silently consumed forever.
148
+ let lastGoodBytes = prev.bytes
149
+ let lastGoodLineIndex = prev.lineIndex
150
+ let cursor = 0
151
+ while (cursor < text.length) {
152
+ const nl = text.indexOf('\n', cursor)
153
+ // No trailing newline -> partial line still being written; stop here
154
+ // without advancing so the rest is re-read once the writer flushes.
155
+ if (nl === -1) break
156
+ const rawLine = text.slice(cursor, nl)
157
+ const consumedBytes = Buffer.byteLength(rawLine, 'utf8') + 1
158
+ cursor = nl + 1
159
+ const line = rawLine.replace(/\r$/, '')
160
+ if (!line) {
161
+ lastGoodBytes += consumedBytes
162
+ continue
163
+ }
164
+ index += 1
165
+ let parsed
166
+ try { parsed = JSON.parse(line) } catch {
167
+ // Malformed line: do not advance past it; retry on next sweep.
168
+ index -= 1
169
+ break
170
+ }
171
+ // Transcript lines carry the role either as message.role (legacy) or
172
+ // as the top-level `type` field ({"type":"assistant","message":{...}}
173
+ // — the current session-runtime writer). Reading only message.role
174
+ // silently skipped EVERY line of current-format transcripts, so the
175
+ // background watcher ingested 0 rows forever.
176
+ const role = parsed.message?.role
177
+ ?? ((parsed.type === 'user' || parsed.type === 'assistant') ? parsed.type : undefined)
178
+ if (role !== 'user' && role !== 'assistant') {
179
+ lastGoodBytes += consumedBytes
180
+ lastGoodLineIndex = index
181
+ continue
182
+ }
183
+ const content = firstTextContent(parsed.message?.content)
184
+ if (!content || !content.trim()) {
185
+ lastGoodBytes += consumedBytes
186
+ lastGoodLineIndex = index
187
+ continue
188
+ }
189
+ const cleaned = cleanMemoryText(content)
190
+ if (!cleaned) {
191
+ lastGoodBytes += consumedBytes
192
+ lastGoodLineIndex = index
193
+ continue
194
+ }
195
+ const tsMs = parseTsToMs(parsed.timestamp ?? parsed.ts ?? Date.now())
196
+ const sourceRef = `transcript:${sessionUuid}#${index}`
197
+ try {
198
+ const result = await db.query(
199
+ `INSERT INTO entries(ts, role, content, source_ref, session_id, source_turn, project_id)
200
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
201
+ ON CONFLICT DO NOTHING`,
202
+ [tsMs, role, cleaned, sourceRef, sessionUuid, index, projectId]
203
+ )
204
+ if (Number(result.rowCount ?? result.affectedRows ?? 0) > 0) count += 1
205
+ lastGoodBytes += consumedBytes
206
+ lastGoodLineIndex = index
207
+ } catch (e) {
208
+ log(`[transcript-watch] insert error (${sourceRef}): ${e.message}\n`)
209
+ // Transient insert failure: leave the boundary before this line so
210
+ // the next sweep retries it. Roll back the line counter too.
211
+ index -= 1
212
+ break
213
+ }
214
+ }
215
+ _transcriptOffsets.set(transcriptPath, {
216
+ bytes: lastGoodBytes,
217
+ lineIndex: lastGoodLineIndex,
218
+ })
219
+ await persistTranscriptOffsets()
220
+ return count
221
+ }
222
+
223
+ async function ingestTranscriptFile(transcriptPath, options = {}) {
224
+ return runTranscriptIngestSerialized(transcriptPath, () => ingestTranscriptFileImpl(transcriptPath, options))
225
+ }
226
+
227
+ function getOffset(fp) {
228
+ return _transcriptOffsets.get(fp)
229
+ }
230
+
231
+ function resetOffsets() {
232
+ _transcriptOffsets = new Map()
233
+ }
234
+
235
+ function initTranscriptWatcher() {
236
+ const root = projectsRoot()
237
+ const SAFETY_POLL_MS = 5 * 60_000
238
+ const DEBOUNCE_MS = 500
239
+ const watchedFiles = new Map()
240
+ const pendingByFile = new Map()
241
+ const watchers = []
242
+ const intervals = []
243
+ const polledFiles = new Set()
244
+ let safetySweepTimeout = null
245
+
246
+ function isWatchable(relOrBase) {
247
+ const base = path.basename(relOrBase)
248
+ if (!base.endsWith('.jsonl') || base.startsWith('agent-')) return false
249
+ if (relOrBase.includes('tmp') || relOrBase.includes('cache') || relOrBase.includes('plugins')) return false
250
+ return true
251
+ }
252
+
253
+ async function ingestOne(fp) {
254
+ try {
255
+ if (!fs.existsSync(fp)) return
256
+ const stat = fs.statSync(fp)
257
+ const mtime = stat.mtimeMs
258
+ const prev = watchedFiles.get(fp)
259
+ if (prev && prev >= mtime) return
260
+ const n = await ingestTranscriptFile(fp, { cwd: cwdFromTranscriptPath(fp) })
261
+ // Only mark this mtime as 'consumed' once the persisted offset has
262
+ // fully advanced past the observed file size. On a transient insert
263
+ // error (or a malformed trailing line) ingestTranscriptFile leaves
264
+ // the persisted offset before the failed line for retry; caching
265
+ // the new mtime unconditionally would suppress the next sweep until
266
+ // the file mutated again, losing the retry. Leave the cache
267
+ // untouched on partial advance so the next sweep re-ingests.
268
+ const off = _transcriptOffsets.get(fp)
269
+ if (off && off.bytes >= stat.size) {
270
+ watchedFiles.set(fp, mtime)
271
+ }
272
+ if (n > 0) {
273
+ log(`[transcript-watch] ingested ${n} entries from ${path.basename(fp)}\n`)
274
+ }
275
+ } catch (e) {
276
+ log(`[transcript-watch] ingest error: ${e.message}\n`)
277
+ }
278
+ }
279
+
280
+ function scheduleIngest(fp) {
281
+ const existing = pendingByFile.get(fp)
282
+ if (existing) clearTimeout(existing)
283
+ const timer = setTimeout(() => {
284
+ pendingByFile.delete(fp)
285
+ ingestOne(fp)
286
+ }, DEBOUNCE_MS)
287
+ pendingByFile.set(fp, timer)
288
+ }
289
+
290
+ async function discoverActiveTranscripts() {
291
+ let topLevel
292
+ try { topLevel = await fs.promises.readdir(root) }
293
+ catch { return [] }
294
+ const files = []
295
+ for (const d of topLevel) {
296
+ if (d.includes('tmp') || d.includes('cache') || d.includes('plugins')) continue
297
+ const full = path.join(root, d)
298
+ let inner
299
+ try { inner = await fs.promises.readdir(full) } catch { continue }
300
+ for (const f of inner) {
301
+ if (!f.endsWith('.jsonl') || f.startsWith('agent-')) continue
302
+ const fp = path.join(full, f)
303
+ try {
304
+ const stat = await fs.promises.stat(fp)
305
+ files.push({ path: fp, mtime: stat.mtimeMs })
306
+ } catch {}
307
+ }
308
+ }
309
+ const cutoff = Date.now() - 30 * 60_000
310
+ return files.filter(f => f.mtime > cutoff)
311
+ }
312
+
313
+ async function safetySweep() {
314
+ try {
315
+ const active = await discoverActiveTranscripts()
316
+ for (const { path: fp } of active) ingestOne(fp)
317
+ } catch (e) {
318
+ log(`[transcript-watch] safety sweep error: ${e.message}\n`)
319
+ }
320
+ }
321
+
322
+ safetySweepTimeout = setTimeout(safetySweep, 3_000)
323
+
324
+ // fs.watch({recursive}) is only reliable on win32.
325
+ // darwin: recursive option unreliable — use flat watch per-entry (glob dirs at start).
326
+ // linux/WSL: recursive not supported — use fs.watchFile polling per file found via
327
+ // the safety sweep, or fall back entirely to safety sweep.
328
+ if (process.platform === 'win32') {
329
+ try {
330
+ const watcher = fs.watch(root, { recursive: true, persistent: true }, (_event, filename) => {
331
+ if (!filename) return
332
+ if (!isWatchable(filename)) return
333
+ const fp = path.join(root, filename)
334
+ scheduleIngest(fp)
335
+ })
336
+ watcher.on('error', (err) => {
337
+ log(`[transcript-watch] fs.watch error: ${err.message}\n`)
338
+ })
339
+ watchers.push(watcher)
340
+ log(`[transcript-watch] fs.watch(recursive) active on ${root}\n`)
341
+ } catch (e) {
342
+ log(`[transcript-watch] fs.watch setup failed: ${e.message} — relying on safety sweep only\n`)
343
+ }
344
+ intervals.push(setInterval(safetySweep, SAFETY_POLL_MS))
345
+ } else if (process.platform === 'darwin') {
346
+ // Flat watch: register a non-recursive watcher on each immediate subdirectory.
347
+ // New subdirs are picked up on the next safety sweep cycle.
348
+ try {
349
+ const registerFlat = (dir) => {
350
+ try {
351
+ const w = fs.watch(dir, { persistent: true }, (_event, filename) => {
352
+ if (!filename) return
353
+ const fp = path.join(dir, filename)
354
+ if (!isWatchable(fp)) return
355
+ scheduleIngest(fp)
356
+ })
357
+ w.on('error', () => { /* ignore individual dir errors */ })
358
+ watchers.push(w)
359
+ } catch { /* dir may not exist yet */ }
360
+ }
361
+ registerFlat(root)
362
+ try {
363
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
364
+ if (entry.isDirectory()) registerFlat(path.join(root, entry.name))
365
+ }
366
+ } catch { /* best effort */ }
367
+ log(`[transcript-watch] flat fs.watch active on ${root} (darwin)\n`)
368
+ } catch (e) {
369
+ log(`[transcript-watch] flat watch setup failed: ${e.message} — relying on safety sweep only\n`)
370
+ }
371
+ intervals.push(setInterval(safetySweep, SAFETY_POLL_MS))
372
+ } else {
373
+ // linux/WSL: fs.watch recursive is unsupported. Use fs.watchFile polling for
374
+ // individual files surfaced by the safety sweep, in addition to the sweep itself.
375
+ log(`[transcript-watch] linux/WSL — using safety sweep + fs.watchFile polling (no recursive watch)\n`)
376
+ // Wrap by reassigning the closure-captured reference is not possible here;
377
+ // instead, register watchFile inside the safety sweep callback by intercepting
378
+ // active file list after each sweep. The interval already calls safetySweep
379
+ // which calls ingestOne; watchFile additions happen as a side-effect of the sweep.
380
+ const _patchedSweep = async () => {
381
+ try {
382
+ const active = await discoverActiveTranscripts()
383
+ for (const { path: fp } of active) {
384
+ if (!polledFiles.has(fp)) {
385
+ polledFiles.add(fp)
386
+ fs.watchFile(fp, { persistent: false, interval: 2000 }, () => {
387
+ if (isWatchable(fp)) scheduleIngest(fp)
388
+ })
389
+ }
390
+ ingestOne(fp)
391
+ }
392
+ } catch (e) {
393
+ log(`[transcript-watch] linux sweep error: ${e.message}\n`)
394
+ }
395
+ }
396
+ // Replace the safety sweep interval with the patched version.
397
+ intervals.push(setInterval(_patchedSweep, SAFETY_POLL_MS))
398
+ }
399
+
400
+ return {
401
+ stop() {
402
+ if (safetySweepTimeout) { clearTimeout(safetySweepTimeout); safetySweepTimeout = null }
403
+ for (const t of pendingByFile.values()) { try { clearTimeout(t) } catch {} }
404
+ pendingByFile.clear()
405
+ for (const i of intervals) { try { clearInterval(i) } catch {} }
406
+ intervals.length = 0
407
+ for (const w of watchers) { try { w.close() } catch {} }
408
+ watchers.length = 0
409
+ for (const fp of polledFiles) { try { fs.unwatchFile(fp) } catch {} }
410
+ polledFiles.clear()
411
+ },
412
+ }
413
+ }
414
+
415
+ return {
416
+ loadTranscriptOffsets,
417
+ persistTranscriptOffsets,
418
+ ingestTranscriptFile,
419
+ cwdFromTranscriptPath,
420
+ parseTsToMs,
421
+ initTranscriptWatcher,
422
+ getOffset,
423
+ resetOffsets,
424
+ }
425
+ }