mixdog 0.7.1

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 (404) hide show
  1. package/.claude-plugin/marketplace.json +31 -0
  2. package/.claude-plugin/plugin.json +20 -0
  3. package/.gitattributes +34 -0
  4. package/.mcp.json +14 -0
  5. package/ARCHITECTURE.md +77 -0
  6. package/CHANGELOG.md +7 -0
  7. package/CONTRIBUTING.md +45 -0
  8. package/DATA-FLOW.md +79 -0
  9. package/LICENSE +21 -0
  10. package/README.md +389 -0
  11. package/SECURITY.md +138 -0
  12. package/UNINSTALL.md +112 -0
  13. package/agents/maintenance.md +5 -0
  14. package/agents/memory-classification.md +30 -0
  15. package/agents/scheduler-task.md +18 -0
  16. package/agents/webhook-handler.md +27 -0
  17. package/agents/worker.md +24 -0
  18. package/bin/bridge +133 -0
  19. package/bin/statusline-launcher.mjs +78 -0
  20. package/bin/statusline-lib.mjs +550 -0
  21. package/bin/statusline.mjs +607 -0
  22. package/bun.lock +802 -0
  23. package/commands/config.md +16 -0
  24. package/commands/doctor.md +13 -0
  25. package/commands/setup.md +17 -0
  26. package/defaults/cycle3-review-prompt.md +90 -0
  27. package/defaults/hidden-roles.json +65 -0
  28. package/defaults/memory-chunk-prompt.md +63 -0
  29. package/defaults/memory-promote-prompt.md +135 -0
  30. package/defaults/mixdog-config.template.json +27 -0
  31. package/defaults/user-workflow.json +8 -0
  32. package/defaults/user-workflow.md +12 -0
  33. package/hooks/hooks.json +73 -0
  34. package/hooks/lib/active-instance.cjs +77 -0
  35. package/hooks/lib/permission-evaluator.cjs +411 -0
  36. package/hooks/lib/permission-route.cjs +63 -0
  37. package/hooks/lib/permission-rules.cjs +170 -0
  38. package/hooks/lib/settings-loader.cjs +116 -0
  39. package/hooks/post-tool-use.cjs +84 -0
  40. package/hooks/pre-mcp-sandbox.cjs +158 -0
  41. package/hooks/pre-tool-subagent.cjs +253 -0
  42. package/hooks/session-start.cjs +1372 -0
  43. package/hooks/turn-timer.cjs +82 -0
  44. package/lib/claude-md-writer.cjs +386 -0
  45. package/lib/config-cjs.cjs +61 -0
  46. package/lib/hook-pipe-path.cjs +10 -0
  47. package/lib/keychain-cjs.cjs +263 -0
  48. package/lib/plugin-paths.cjs +61 -0
  49. package/lib/rules-builder.cjs +241 -0
  50. package/lib/text-utils.cjs +61 -0
  51. package/native/README.md +117 -0
  52. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  53. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  54. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  55. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  56. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  57. package/package.json +107 -0
  58. package/prompts/code-review.txt +16 -0
  59. package/prompts/security-audit.txt +17 -0
  60. package/rules/bridge/00-common.md +39 -0
  61. package/rules/bridge/20-skip-protocol.md +18 -0
  62. package/rules/bridge/30-explorer.md +33 -0
  63. package/rules/bridge/40-cycle1-agent.md +52 -0
  64. package/rules/bridge/41-cycle2-agent.md +62 -0
  65. package/rules/bridge/42-cycle3-agent.md +44 -0
  66. package/rules/lead/00-tool-lead.md +61 -0
  67. package/rules/lead/01-general.md +23 -0
  68. package/rules/lead/02-channels.md +49 -0
  69. package/rules/lead/03-team.md +27 -0
  70. package/rules/lead/04-workflow.md +20 -0
  71. package/rules/shared/00-language.md +14 -0
  72. package/rules/shared/01-tool.md +138 -0
  73. package/scripts/bootstrap.mjs +184 -0
  74. package/scripts/bridge-unify-smoke.mjs +308 -0
  75. package/scripts/build-runtime-linux.sh +348 -0
  76. package/scripts/build-runtime-macos.sh +217 -0
  77. package/scripts/build-runtime-windows.ps1 +242 -0
  78. package/scripts/builtin-utils-smoke.mjs +392 -0
  79. package/scripts/check-json.mjs +45 -0
  80. package/scripts/check-syntax-changed.mjs +102 -0
  81. package/scripts/check-syntax.mjs +58 -0
  82. package/scripts/code-graph-batch.test.mjs +33 -0
  83. package/scripts/config-preserve-smoke.mjs +180 -0
  84. package/scripts/doctor.mjs +484 -0
  85. package/scripts/edit-normalize-fuzz.mjs +130 -0
  86. package/scripts/edit-normalize-smoke.mjs +401 -0
  87. package/scripts/edit-operation-smoke.mjs +369 -0
  88. package/scripts/edit2-smoke.mjs +63 -0
  89. package/scripts/fuzzy-e2e.mjs +28 -0
  90. package/scripts/fuzzy-smoke.mjs +26 -0
  91. package/scripts/generate-runtime-manifest.mjs +166 -0
  92. package/scripts/guard-smoke.mjs +66 -0
  93. package/scripts/hidden-role-schema-smoke.mjs +162 -0
  94. package/scripts/hook-routing-smoke.mjs +29 -0
  95. package/scripts/inject-input.ps1 +204 -0
  96. package/scripts/io-complex-smoke.mjs +667 -0
  97. package/scripts/io-explore-bench.mjs +424 -0
  98. package/scripts/io-guardrails-smoke.mjs +205 -0
  99. package/scripts/io-mini-bench-baseline.json +11 -0
  100. package/scripts/io-mini-bench.mjs +216 -0
  101. package/scripts/io-route-harness.mjs +933 -0
  102. package/scripts/io-telemetry-report.mjs +691 -0
  103. package/scripts/mutation-bench.mjs +564 -0
  104. package/scripts/mutation-io-smoke.mjs +1081 -0
  105. package/scripts/native-patch-bridge-smoke.mjs +288 -0
  106. package/scripts/native-patch-smoke.mjs +304 -0
  107. package/scripts/patch-interior-context-smoke.mjs +49 -0
  108. package/scripts/patch-newline-utf8-smoke.mjs +157 -0
  109. package/scripts/perf-hook-smoke.mjs +71 -0
  110. package/scripts/permission-eval-smoke.mjs +426 -0
  111. package/scripts/prep-patch.mjs +53 -0
  112. package/scripts/prep-shim.mjs +96 -0
  113. package/scripts/provider-cache-smoke.mjs +687 -0
  114. package/scripts/report-runtime-health.mjs +132 -0
  115. package/scripts/run-mcp.mjs +1547 -0
  116. package/scripts/salvage-v4a-shatter.test.mjs +58 -0
  117. package/scripts/scoped-cache-io-smoke.mjs +103 -0
  118. package/scripts/shell-policy-round3-smoke.mjs +46 -0
  119. package/scripts/smoke-runtime-negative.ps1 +100 -0
  120. package/scripts/smoke-runtime-negative.sh +95 -0
  121. package/scripts/stall-policy-smoke.mjs +50 -0
  122. package/scripts/start-memory-worker.mjs +23 -0
  123. package/scripts/statusline-launcher-smoke.mjs +82 -0
  124. package/scripts/stress-atomic-write.mjs +1028 -0
  125. package/scripts/test-config-rmw-restore.mjs +122 -0
  126. package/scripts/test-fault-inject.mjs +164 -0
  127. package/scripts/test-large-file.mjs +174 -0
  128. package/scripts/tool-edge-smoke.mjs +209 -0
  129. package/scripts/uninstall.mjs +201 -0
  130. package/scripts/webhook-selfheal-smoke.mjs +29 -0
  131. package/scripts/write-overwrite-guard-smoke.mjs +56 -0
  132. package/server-main.mjs +3055 -0
  133. package/server.mjs +468 -0
  134. package/setup/config-merge.mjs +254 -0
  135. package/setup/install.mjs +120 -0
  136. package/setup/launch-core.mjs +507 -0
  137. package/setup/launch.mjs +101 -0
  138. package/setup/setup-server.mjs +3206 -0
  139. package/setup/setup.html +3693 -0
  140. package/skills/retro-skill-proposer/SKILL.md +92 -0
  141. package/skills/schedule-add/SKILL.md +77 -0
  142. package/skills/setup/SKILL.md +346 -0
  143. package/skills/webhook-add/SKILL.md +81 -0
  144. package/src/agent/bridge-stall-watchdog.mjs +337 -0
  145. package/src/agent/index.mjs +2138 -0
  146. package/src/agent/orchestrator/activity-bus.mjs +38 -0
  147. package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
  148. package/src/agent/orchestrator/bridge-retry.mjs +220 -0
  149. package/src/agent/orchestrator/bridge-trace.mjs +583 -0
  150. package/src/agent/orchestrator/cache-mtime.mjs +58 -0
  151. package/src/agent/orchestrator/config.mjs +358 -0
  152. package/src/agent/orchestrator/context/collect.mjs +651 -0
  153. package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
  154. package/src/agent/orchestrator/drain-registry.mjs +50 -0
  155. package/src/agent/orchestrator/explore-validator.mjs +8 -0
  156. package/src/agent/orchestrator/internal-roles.mjs +118 -0
  157. package/src/agent/orchestrator/internal-tools.mjs +88 -0
  158. package/src/agent/orchestrator/jobs.mjs +116 -0
  159. package/src/agent/orchestrator/mcp/client.mjs +364 -0
  160. package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
  161. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
  162. package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
  163. package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
  164. package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
  165. package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
  166. package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
  167. package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
  168. package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
  169. package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
  170. package/src/agent/orchestrator/providers/registry.mjs +192 -0
  171. package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
  172. package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
  173. package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
  174. package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
  175. package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
  176. package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
  177. package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
  178. package/src/agent/orchestrator/session/cache/util.mjs +49 -0
  179. package/src/agent/orchestrator/session/loop.mjs +1478 -0
  180. package/src/agent/orchestrator/session/manager.mjs +1975 -0
  181. package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
  182. package/src/agent/orchestrator/session/result-classification.mjs +65 -0
  183. package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
  184. package/src/agent/orchestrator/session/store.mjs +624 -0
  185. package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
  186. package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
  187. package/src/agent/orchestrator/session/trim.mjs +491 -0
  188. package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
  189. package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
  190. package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
  191. package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
  192. package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
  193. package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
  194. package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
  195. package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
  196. package/src/agent/orchestrator/stall-policy.mjs +195 -0
  197. package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
  198. package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
  199. package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
  200. package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
  201. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
  202. package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
  203. package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
  204. package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
  205. package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
  206. package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
  207. package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
  208. package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
  209. package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
  210. package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
  211. package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
  212. package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
  213. package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
  214. package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
  215. package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
  216. package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
  217. package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
  218. package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
  219. package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
  220. package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
  221. package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
  222. package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
  223. package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
  224. package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
  225. package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
  226. package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
  227. package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
  228. package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
  229. package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
  230. package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
  231. package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
  232. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
  233. package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
  234. package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
  235. package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
  236. package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
  237. package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
  238. package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
  239. package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
  240. package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
  241. package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
  242. package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
  243. package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
  244. package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
  245. package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
  246. package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
  247. package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
  248. package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
  249. package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
  250. package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
  251. package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
  252. package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
  253. package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
  254. package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
  255. package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
  256. package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
  257. package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
  258. package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
  259. package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
  260. package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
  261. package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
  262. package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
  263. package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
  264. package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
  265. package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
  266. package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
  267. package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
  268. package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
  269. package/src/agent/orchestrator/tools/builtin.mjs +500 -0
  270. package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
  271. package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
  272. package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
  273. package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
  274. package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
  275. package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
  276. package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
  277. package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
  278. package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
  279. package/src/agent/orchestrator/tools/host-input.mjs +204 -0
  280. package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
  281. package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
  282. package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
  283. package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
  284. package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
  285. package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
  286. package/src/agent/orchestrator/tools/patch.mjs +2754 -0
  287. package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
  288. package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
  289. package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
  290. package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
  291. package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
  292. package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
  293. package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
  294. package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
  295. package/src/agent/orchestrator/workflow-store.mjs +93 -0
  296. package/src/agent/tool-defs.mjs +103 -0
  297. package/src/channels/backends/discord.mjs +784 -0
  298. package/src/channels/data/voice-runtime-manifest.json +138 -0
  299. package/src/channels/index.mjs +3229 -0
  300. package/src/channels/lib/cli-worker-host.mjs +12 -0
  301. package/src/channels/lib/config-lock.mjs +13 -0
  302. package/src/channels/lib/config.mjs +292 -0
  303. package/src/channels/lib/drop-trace.mjs +71 -0
  304. package/src/channels/lib/event-pipeline.mjs +81 -0
  305. package/src/channels/lib/event-queue.mjs +345 -0
  306. package/src/channels/lib/executor.mjs +168 -0
  307. package/src/channels/lib/format.mjs +188 -0
  308. package/src/channels/lib/holidays.mjs +138 -0
  309. package/src/channels/lib/hook-pipe-server.mjs +802 -0
  310. package/src/channels/lib/interaction-workflows.mjs +184 -0
  311. package/src/channels/lib/memory-client.mjs +149 -0
  312. package/src/channels/lib/output-forwarder.mjs +765 -0
  313. package/src/channels/lib/runtime-paths.mjs +479 -0
  314. package/src/channels/lib/scheduler.mjs +723 -0
  315. package/src/channels/lib/session-control.mjs +36 -0
  316. package/src/channels/lib/session-discovery.mjs +103 -0
  317. package/src/channels/lib/settings.mjs +11 -0
  318. package/src/channels/lib/state-file.mjs +68 -0
  319. package/src/channels/lib/status-snapshot.mjs +219 -0
  320. package/src/channels/lib/tool-format.mjs +140 -0
  321. package/src/channels/lib/transcript-discovery.mjs +195 -0
  322. package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
  323. package/src/channels/lib/webhook.mjs +1179 -0
  324. package/src/channels/lib/whisper-server.mjs +477 -0
  325. package/src/channels/tool-defs.mjs +170 -0
  326. package/src/daemon/host.mjs +118 -0
  327. package/src/daemon/mcp-transport.mjs +47 -0
  328. package/src/daemon/session.mjs +100 -0
  329. package/src/daemon/thin-client.mjs +71 -0
  330. package/src/daemon/transport.mjs +163 -0
  331. package/src/memory/data/runtime-manifest.json +40 -0
  332. package/src/memory/index.mjs +3305 -0
  333. package/src/memory/lib/agent-ipc.mjs +93 -0
  334. package/src/memory/lib/bridge-trace-queries.mjs +120 -0
  335. package/src/memory/lib/core-memory-store.mjs +330 -0
  336. package/src/memory/lib/embedding-provider.mjs +269 -0
  337. package/src/memory/lib/embedding-worker.mjs +323 -0
  338. package/src/memory/lib/llm-worker-host.mjs +17 -0
  339. package/src/memory/lib/memory-cycle.mjs +11 -0
  340. package/src/memory/lib/memory-cycle1.mjs +641 -0
  341. package/src/memory/lib/memory-cycle2.mjs +1284 -0
  342. package/src/memory/lib/memory-cycle3.mjs +540 -0
  343. package/src/memory/lib/memory-embed.mjs +299 -0
  344. package/src/memory/lib/memory-extraction.mjs +5 -0
  345. package/src/memory/lib/memory-maintenance-store.mjs +32 -0
  346. package/src/memory/lib/memory-ops-policy.mjs +190 -0
  347. package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
  348. package/src/memory/lib/memory-recall-read-query.mjs +7 -0
  349. package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
  350. package/src/memory/lib/memory-recall-store.mjs +621 -0
  351. package/src/memory/lib/memory-retrievers.mjs +112 -0
  352. package/src/memory/lib/memory-score.mjs +71 -0
  353. package/src/memory/lib/memory-text-utils.mjs +58 -0
  354. package/src/memory/lib/memory.mjs +412 -0
  355. package/src/memory/lib/model-profile.mjs +85 -0
  356. package/src/memory/lib/pg/adapter.mjs +308 -0
  357. package/src/memory/lib/pg/process.mjs +360 -0
  358. package/src/memory/lib/pg/supervisor.mjs +396 -0
  359. package/src/memory/lib/project-id-resolver.mjs +86 -0
  360. package/src/memory/lib/runtime-fetcher.mjs +442 -0
  361. package/src/memory/lib/trace-store.mjs +728 -0
  362. package/src/memory/tool-defs.mjs +79 -0
  363. package/src/search/index.mjs +1173 -0
  364. package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
  365. package/src/search/lib/backends/exa.mjs +50 -0
  366. package/src/search/lib/backends/firecrawl.mjs +61 -0
  367. package/src/search/lib/backends/gemini-api.mjs +83 -0
  368. package/src/search/lib/backends/grok-oauth.mjs +86 -0
  369. package/src/search/lib/backends/index.mjs +150 -0
  370. package/src/search/lib/backends/openai-api.mjs +144 -0
  371. package/src/search/lib/backends/openai-oauth.mjs +98 -0
  372. package/src/search/lib/backends/openai-web-search.mjs +76 -0
  373. package/src/search/lib/backends/tavily.mjs +55 -0
  374. package/src/search/lib/backends/xai-api.mjs +113 -0
  375. package/src/search/lib/cache.mjs +131 -0
  376. package/src/search/lib/config.mjs +192 -0
  377. package/src/search/lib/formatter.mjs +115 -0
  378. package/src/search/lib/provider-usage.mjs +67 -0
  379. package/src/search/lib/providers.mjs +47 -0
  380. package/src/search/lib/search-intent.mjs +109 -0
  381. package/src/search/lib/setup-handler.mjs +261 -0
  382. package/src/search/lib/state.mjs +201 -0
  383. package/src/search/lib/web-tools.mjs +1207 -0
  384. package/src/search/tool-defs.mjs +83 -0
  385. package/src/setup/defender-exclusion.mjs +183 -0
  386. package/src/shared/abort-controller.mjs +15 -0
  387. package/src/shared/atomic-file.mjs +420 -0
  388. package/src/shared/config.mjs +350 -0
  389. package/src/shared/daemon-recycle.mjs +108 -0
  390. package/src/shared/disable-claude-builtins.mjs +88 -0
  391. package/src/shared/err-text.mjs +12 -0
  392. package/src/shared/llm/cost.mjs +66 -0
  393. package/src/shared/llm/http-agent.mjs +123 -0
  394. package/src/shared/llm/index.mjs +41 -0
  395. package/src/shared/llm/pid-cleanup.mjs +27 -0
  396. package/src/shared/llm/usage-log.mjs +47 -0
  397. package/src/shared/plugin-paths.mjs +58 -0
  398. package/src/shared/schedules-store.mjs +70 -0
  399. package/src/shared/seed.mjs +119 -0
  400. package/src/shared/user-cwd.mjs +213 -0
  401. package/src/shared/user-data-guard.mjs +238 -0
  402. package/src/status/aggregator.mjs +584 -0
  403. package/src/status/server.mjs +413 -0
  404. package/tools.json +1653 -0
@@ -0,0 +1,1372 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const http = require('http');
7
+ const net = require('net');
8
+ const { spawn } = require('child_process');
9
+ const { resolvePluginData } = require(path.join(__dirname, '..', 'lib', 'plugin-paths.cjs'));
10
+ const { readSection } = require(path.join(__dirname, '..', 'lib', 'config-cjs.cjs'));
11
+
12
+ // Mirror selected stderr lines to a plugin-data log file so cycle1 traces
13
+ // remain inspectable after the host shell scrolls past. Best-effort: any
14
+ // fs error is swallowed so logging never breaks the hook.
15
+ let _SESSION_START_LOG_PATH = null;
16
+ const SESSION_START_TRACE_ENABLED =
17
+ process.env.MIXDOG_DEBUG_SESSION_START === '1' ||
18
+ process.env.MIXDOG_DEBUG_SESSION_START === 'true';
19
+ function sessionStartLogPath() {
20
+ if (_SESSION_START_LOG_PATH) return _SESSION_START_LOG_PATH;
21
+ const base = (typeof DATA_DIR === 'string' && DATA_DIR)
22
+ ? DATA_DIR
23
+ : path.join(os.homedir(), '.claude', 'plugins', 'data', 'mixdog-trib-plugin');
24
+ _SESSION_START_LOG_PATH = path.join(base, 'session-start.log');
25
+ return _SESSION_START_LOG_PATH;
26
+ }
27
+ // Always append to session-start.log so fail-open reasons (skip / cycle1
28
+ // failure / missing-dirs / null dispatch) stay diagnosable without requiring
29
+ // MIXDOG_DEBUG_SESSION_START to be pre-set. Stderr output remains gated by
30
+ // the trace flag — log file is the durable record, stderr is the live tail.
31
+ // Size-based rotation: when the log exceeds LOG_MAX_BYTES, head-trim down to
32
+ // LOG_KEEP_BYTES so a long-running daemon doesn't grow it unbounded.
33
+ const SESSION_START_LOG_MAX_BYTES = 256 * 1024;
34
+ const SESSION_START_LOG_KEEP_BYTES = 64 * 1024;
35
+ function teeStderr(line) {
36
+ try {
37
+ const p = sessionStartLogPath();
38
+ fs.mkdirSync(path.dirname(p), { recursive: true });
39
+ try {
40
+ const st = fs.statSync(p);
41
+ if (st.size > SESSION_START_LOG_MAX_BYTES) {
42
+ const buf = fs.readFileSync(p);
43
+ fs.writeFileSync(p, buf.subarray(buf.length - SESSION_START_LOG_KEEP_BYTES));
44
+ }
45
+ } catch {}
46
+ fs.appendFileSync(p, line);
47
+ } catch {}
48
+ if (SESSION_START_TRACE_ENABLED) {
49
+ try { process.stderr.write(line); } catch {}
50
+ }
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // argv parsing — supports `--part rules`, `--part=rules`.
55
+ // Invalid/unknown part falls back to `rules`.
56
+ // ---------------------------------------------------------------------------
57
+ function parseArgs(argv) {
58
+ const out = { part: 'rules' };
59
+ for (let i = 2; i < argv.length; i++) {
60
+ const a = argv[i];
61
+ if (!a) continue;
62
+ let key = null;
63
+ let val = null;
64
+ if (a.startsWith('--') && a.includes('=')) {
65
+ const eq = a.indexOf('=');
66
+ key = a.slice(2, eq);
67
+ val = a.slice(eq + 1);
68
+ } else if (a.startsWith('--')) {
69
+ key = a.slice(2);
70
+ const next = argv[i + 1];
71
+ if (typeof next === 'string' && !next.startsWith('--')) {
72
+ val = next;
73
+ i++;
74
+ }
75
+ }
76
+ if (key === 'part' && typeof val === 'string') out.part = val;
77
+ }
78
+ if (!['rules', 'core', 'recap'].includes(out.part)) out.part = 'rules';
79
+ return out;
80
+ }
81
+
82
+ const ARGS = parseArgs(process.argv);
83
+ let PART = ARGS.part;
84
+
85
+ let _event = {};
86
+ const IS_DAEMON_REQUIRE = !!process.env.MIXDOG_SKIP_TOP_STDIN;
87
+ // In-daemon `require()` would otherwise read fd 0 (the daemon's MCP stdio
88
+ // pipe), corrupting it. Skip the top-level stdin read when this env-var is
89
+ // set by hook-pipe-server.mjs around the require boundary.
90
+ if (!IS_DAEMON_REQUIRE) {
91
+ try {
92
+ const _input = fs.readFileSync(0, 'utf8');
93
+ if (_input) _event = JSON.parse(_input);
94
+ } catch {}
95
+ }
96
+
97
+ if (_event.isSidechain) {
98
+ teeStderr(`[session-start] skip PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} reason=isSidechain\n`);
99
+ process.exit(0);
100
+ }
101
+ if (_event.agentId) {
102
+ teeStderr(`[session-start] skip PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} reason=agentId=${_event.agentId}\n`);
103
+ process.exit(0);
104
+ }
105
+ if (_event.is_sidechain) {
106
+ teeStderr(`[session-start] skip PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} reason=is_sidechain\n`);
107
+ process.exit(0);
108
+ }
109
+ if (_event.agent_id) {
110
+ teeStderr(`[session-start] skip PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} reason=agent_id=${_event.agent_id}\n`);
111
+ process.exit(0);
112
+ }
113
+ if (_event.kind && _event.kind !== 'interactive') {
114
+ teeStderr(`[session-start] skip PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} reason=kind=${_event.kind}\n`);
115
+ process.exit(0);
116
+ }
117
+
118
+ const DATA_DIR = resolvePluginData();
119
+ const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT;
120
+ const SESSION_START_CYCLE1_TIMEOUT_MS = Number.parseInt(process.env.MIXDOG_SESSION_START_CYCLE1_TIMEOUT_MS || '110000', 10);
121
+ const MIXDOG_RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT
122
+ ? path.resolve(process.env.MIXDOG_RUNTIME_ROOT)
123
+ : path.join(os.tmpdir(), 'mixdog');
124
+ const ACTIVE_INSTANCE_FILE = path.join(MIXDOG_RUNTIME_ROOT, 'active-instance.json');
125
+ if (!DATA_DIR || !PLUGIN_ROOT) {
126
+ teeStderr(`[session-start] skip PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} reason=missing-dirs DATA_DIR=${!!DATA_DIR} PLUGIN_ROOT=${!!PLUGIN_ROOT}\n`);
127
+ process.exit(0);
128
+ }
129
+
130
+ if (!IS_DAEMON_REQUIRE) {
131
+ teeStderr(`[session-start] enter PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} sessionId=${_event.session_id || _event.sessionId || ''}\n`);
132
+ }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Common helpers (used by all parts).
136
+ // ---------------------------------------------------------------------------
137
+ function readJson(filePath) {
138
+ try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return {}; }
139
+ }
140
+
141
+ function parsePositiveNumber(value) {
142
+ const n = Number(value);
143
+ return Number.isFinite(n) && n > 0 ? n : null;
144
+ }
145
+
146
+ function readAdvertisedMemoryPort() {
147
+ const active = JSON.parse(fs.readFileSync(ACTIVE_INSTANCE_FILE, 'utf8'));
148
+ const port = parsePositiveNumber(active && active.memory_port);
149
+ if (!port) return null;
150
+ const activeServerPid = parsePositiveNumber(active && active.server_pid);
151
+ const memoryServerPid = parsePositiveNumber(active && active.memory_server_pid);
152
+ if (activeServerPid && memoryServerPid && activeServerPid !== memoryServerPid) {
153
+ return { stale: true, port, activeServerPid, memoryServerPid };
154
+ }
155
+ return { stale: false, port, active };
156
+ }
157
+
158
+ function sleepMs(ms) {
159
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
160
+ }
161
+
162
+ function readOwnerSecretFor(ownerInstanceId) {
163
+ if (!ownerInstanceId) return '';
164
+ try {
165
+ const file = path.join(MIXDOG_RUNTIME_ROOT, `owner-secret-${String(ownerInstanceId)}.json`);
166
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
167
+ return parsed && typeof parsed.secret === 'string' ? parsed.secret : '';
168
+ } catch {
169
+ return '';
170
+ }
171
+ }
172
+
173
+ function ownerAuthHeaders(active) {
174
+ const ownerInstanceId = active && active.instanceId;
175
+ const ownerSecret = readOwnerSecretFor(ownerInstanceId);
176
+ if (!ownerSecret) return null;
177
+ return { 'x-owner-token': ownerSecret, 'x-owner-instance': String(ownerInstanceId) };
178
+ }
179
+
180
+ let _emitSink = null;
181
+ function setEmitSink(fn) { _emitSink = fn || null; }
182
+ function emit(additionalContext) {
183
+ if (!additionalContext) {
184
+ teeStderr(`[session-start] emit skipped: falsy context (empty string or null)\n`);
185
+ return;
186
+ }
187
+ const byteLen = Buffer.byteLength(additionalContext, 'utf8');
188
+ teeStderr(`[session-start] emit PART=${PART} bytes=${byteLen}\n`);
189
+ const out = JSON.stringify({
190
+ hookSpecificOutput: {
191
+ hookEventName: 'SessionStart',
192
+ additionalContext,
193
+ },
194
+ });
195
+ if (_emitSink) _emitSink(out);
196
+ else process.stdout.write(out);
197
+ }
198
+ function setEvent(e) { _event = e || {}; }
199
+ function setPart(part) {
200
+ if (['rules', 'core', 'recap'].includes(part)) PART = part;
201
+ }
202
+
203
+ // Memory-service HTTP port discovery. Single source of truth:
204
+ // `<tmpdir>/mixdog/active-instance.json` `memory_port` field, written by
205
+ // supervisor (server-main.mjs) on memory worker `ready` IPC. Throws when
206
+ // missing — caller surfaces the failure rather than silently falling back
207
+ // to a stale or default port.
208
+ function getMemoryServicePort() {
209
+ const advertised = readAdvertisedMemoryPort();
210
+ if (!advertised || advertised.stale) {
211
+ throw new Error(advertised && advertised.stale
212
+ ? 'active-instance.json stale memory_port owner'
213
+ : 'active-instance.json missing memory_port');
214
+ }
215
+ return advertised.port;
216
+ }
217
+
218
+ async function getLiveMemoryServicePort(probeMs = 200) {
219
+ try {
220
+ const advertised = readAdvertisedMemoryPort();
221
+ if (!advertised || advertised.stale) return null;
222
+ const port = advertised.port;
223
+ const alive = await probeTcpPort(port, Math.max(1, probeMs));
224
+ teeStderr(`[session-start] memoryDirect probePort=${port} probeMs=${probeMs} alive=${alive}\n`);
225
+ return alive ? port : null;
226
+ } catch {
227
+ return null;
228
+ }
229
+ }
230
+
231
+ async function memoryServicePost(urlPath, body, timeoutMs = 5000) {
232
+ const port = getMemoryServicePort();
233
+ const res = await httpPostJson({
234
+ hostname: '127.0.0.1',
235
+ port,
236
+ path: urlPath,
237
+ timeoutMs,
238
+ body: body || {},
239
+ });
240
+ if (res.statusCode !== 200) {
241
+ throw new Error(`memory-service ${urlPath} non-200 status=${res.statusCode}`);
242
+ }
243
+ try { return JSON.parse(res.body); }
244
+ catch (e) { throw new Error(`memory-service ${urlPath} invalid JSON: ${e.message}`); }
245
+ }
246
+
247
+ function formatTs(ts) {
248
+ const n = Number(ts);
249
+ if (Number.isFinite(n) && n > 1e12) {
250
+ return new Date(n).toLocaleString('sv-SE').slice(0, 16);
251
+ }
252
+ return String(ts ?? '').slice(0, 16);
253
+ }
254
+
255
+ // MM-DD HH:MM in local time for compact recap rendering.
256
+ function formatTsShort(ts) {
257
+ const n = Number(ts);
258
+ if (!Number.isFinite(n) || n <= 1e12) return String(ts ?? '').slice(0, 16);
259
+ const full = new Date(n).toLocaleString('sv-SE');
260
+ return full.slice(5, 16);
261
+ }
262
+
263
+ // Single source of truth: lib/text-utils.cjs (also imported by memory-extraction.mjs).
264
+ const { cleanMemoryText: cleanText } = require(path.join(PLUGIN_ROOT, 'lib', 'text-utils.cjs'));
265
+
266
+ function resolveCwdScope(cwd) {
267
+ try {
268
+ let dir = path.resolve(cwd || process.cwd());
269
+ while (true) {
270
+ const candidate = path.join(dir, '.mixdog', 'project.id');
271
+ try {
272
+ const val = fs.readFileSync(candidate, 'utf8').trim();
273
+ if (val.toLowerCase() === 'common') return null;
274
+ if (val) {
275
+ // Validate slug before returning — reject path traversal attempts.
276
+ if (val.includes('..') || val.includes('\\')) return null;
277
+ return val;
278
+ }
279
+ } catch {}
280
+ const parent = path.dirname(dir);
281
+ if (parent === dir) break;
282
+ dir = parent;
283
+ }
284
+ } catch {}
285
+ return null;
286
+ }
287
+
288
+ async function buildContext(cwd) {
289
+ try {
290
+ const _t0 = Date.now();
291
+ const result = await memoryServicePost('/session-start/core-memory', {
292
+ cwd: cwd || process.cwd(),
293
+ });
294
+ const _elapsed = Date.now() - _t0;
295
+ if (!result || result.ok !== true) return '';
296
+ const dbLines = Array.isArray(result.dbLines) ? result.dbLines : [];
297
+ const userLines = Array.isArray(result.userLines) ? result.userLines : [];
298
+ teeStderr(`[session-start] buildContext POST /session-start/core-memory elapsed=${_elapsed}ms dbLines=${dbLines.length} userLines=${userLines.length}\n`);
299
+ const seen = new Set();
300
+ const lines = [];
301
+ // userLines (user-curated) and dbLines accumulate under the same char cap.
302
+ // userLines go first so they win the budget. Each line is already short
303
+ // (core_summary <=120, manual core <=120 at write time), so the cap is a
304
+ // safety net rather than the primary limiter.
305
+ const HEADER_LEN = '## Core Memory\n'.length;
306
+ // CAP = 5000 chars — documented host-preview envelope. This is a byte cap,
307
+ // not token-aware (adding a token counter would require a tokeniser dependency).
308
+ // Deferred: token-aware cap once a lightweight counter is available.
309
+ const CAP = 5000;
310
+ let total = HEADER_LEN;
311
+ for (const line of userLines) {
312
+ const key = line.toLowerCase().replace(/\s+/g, ' ').slice(0, 120);
313
+ if (seen.has(key)) continue;
314
+ const add = line.length + 1;
315
+ if (total + add > CAP) break;
316
+ seen.add(key);
317
+ lines.push(line);
318
+ total += add;
319
+ }
320
+ // dbLines: accumulate score-DESC rows until the char cap is reached.
321
+ for (const line of dbLines) {
322
+ const key = line.toLowerCase().replace(/\s+/g, ' ').slice(0, 120);
323
+ if (seen.has(key)) continue;
324
+ const add = line.length + 1;
325
+ if (total + add > CAP) break;
326
+ seen.add(key);
327
+ lines.push(line);
328
+ total += add;
329
+ }
330
+ if (lines.length === 0) return '';
331
+ return `## Core Memory\n${lines.join('\n')}`;
332
+ } catch (e) {
333
+ teeStderr(`[session-start] buildContext catch endpoint=/session-start/core-memory err=${e.message}\n`);
334
+ process.stderr.write(`[session-start] context build failed: ${e.message}\n`);
335
+ return '';
336
+ }
337
+ }
338
+
339
+ // Returns { lines } — chronological "[MM-DD HH:MM] <summary>" entries
340
+ // (oldest → newest), trimmed from the front so the rendered block fits the
341
+ // SessionStart hook output cap (10,000 chars total — leaves margin for the
342
+ // JSON wrapper around additionalContext; header "## Recap\n" reserved).
343
+ async function buildRecapData(cwd) {
344
+ const out = { lines: [] };
345
+ try {
346
+ const _t0 = Date.now();
347
+ const result = await memoryServicePost('/session-start/recap', {
348
+ cwd: cwd || process.cwd(),
349
+ });
350
+ const _elapsed = Date.now() - _t0;
351
+ if (!result || result.ok !== true) return out;
352
+ const rows = Array.isArray(result.rows) ? result.rows : [];
353
+ teeStderr(`[session-start] buildRecapData POST /session-start/recap elapsed=${_elapsed}ms rows=${rows.length}\n`);
354
+ if (rows.length === 0) return out;
355
+
356
+ const rendered = rows.map(r => {
357
+ const tsStr = formatTsShort(r.ts);
358
+ const summary = String(r.summary || '').trim().slice(0, 1000);
359
+ return summary ? `[${tsStr}] ${summary}` : '';
360
+ }).filter(Boolean);
361
+ if (rendered.length === 0) return out;
362
+
363
+ // Dedup by normalized summary — newest-first, so older repeats drop.
364
+ const seen = new Set();
365
+ const uniq = [];
366
+ for (const line of rendered) {
367
+ const key = line.replace(/^\[[^\]]+\]\s*/, '').toLowerCase().replace(/\s+/g, ' ').slice(0, 80);
368
+ if (seen.has(key)) continue;
369
+ seen.add(key);
370
+ uniq.push(line);
371
+ }
372
+
373
+ // Newest → oldest; keep accumulating from the newest end until the
374
+ // running total would exceed the cap, then reverse to chronological.
375
+ const HEADER_LEN = '## Recap\n'.length;
376
+ const CAP = 5000;
377
+ let total = HEADER_LEN;
378
+ const kept = [];
379
+ for (const line of uniq) {
380
+ const add = line.length + 1;
381
+ if (total + add > CAP) break;
382
+ kept.push(line);
383
+ total += add;
384
+ }
385
+ out.lines = kept.reverse();
386
+ return out;
387
+ } catch (e) {
388
+ teeStderr(`[session-start] buildRecapData catch endpoint=/session-start/recap err=${e.message}\n`);
389
+ process.stderr.write(`[session-start] recap build failed: ${e.message}\n`);
390
+ return out;
391
+ }
392
+ }
393
+
394
+ // ---------------------------------------------------------------------------
395
+ // Skip flag — resume / compact reuses the existing context, so re-injecting
396
+ // memory just bloats tokens. Rules still flow through so any rule changes
397
+ // since the last turn take effect.
398
+ // ---------------------------------------------------------------------------
399
+ function _skipMemoryInject() { return _event.source === 'resume' || _event.source === 'compact'; }
400
+ if (!IS_DAEMON_REQUIRE) {
401
+ teeStderr(`[session-start] skipMemoryInject=${_skipMemoryInject()} PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()}\n`);
402
+ }
403
+
404
+ // ---------------------------------------------------------------------------
405
+ // Part: rules (slot 1) — owns ALL one-shot session bootstrap work and emits
406
+ // the rules block. Static .md content only; cycle1 is triggered by
407
+ // core/recap slots (dedupe coalesces concurrent calls into one run).
408
+ // ---------------------------------------------------------------------------
409
+
410
+ function hasManagedClaudeMdBlock(targetPath) {
411
+ if (!targetPath) return false;
412
+ try {
413
+ const { expandHome, MARKER_START, MARKER_END } = require(path.join(PLUGIN_ROOT, 'lib', 'claude-md-writer.cjs'));
414
+ const resolved = expandHome(targetPath);
415
+ if (!resolved || !fs.existsSync(resolved)) return false;
416
+ const content = fs.readFileSync(resolved, 'utf8');
417
+ return content.includes(MARKER_START) && content.includes(MARKER_END);
418
+ } catch {
419
+ return false;
420
+ }
421
+ }
422
+
423
+ // Stable, NON-versioned launcher path in mixdog-owned plugin data — survives
424
+ // cache cleanup and version rotation, so settings.json can point at it forever
425
+ // (the launcher resolves the active install at runtime each tick).
426
+ function stableLauncherPath() {
427
+ return path.join(DATA_DIR, 'statusline-launcher.mjs').replace(/\\/g, '/');
428
+ }
429
+
430
+ function injectStatusLine(pluginRoot) {
431
+ try {
432
+ // Refresh the stable launcher copy from the current install each session
433
+ // start so it tracks the latest committed launcher code.
434
+ const launcherDst = stableLauncherPath();
435
+ try {
436
+ const launcherSrc = path.join(pluginRoot, 'bin', 'statusline-launcher.mjs');
437
+ fs.mkdirSync(path.dirname(launcherDst), { recursive: true });
438
+ fs.copyFileSync(launcherSrc, launcherDst);
439
+ } catch (e) {
440
+ process.stderr.write(`[session-start] statusLine launcher copy failed: ${e.message}\n`);
441
+ }
442
+
443
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
444
+ let raw;
445
+ try { raw = fs.readFileSync(settingsPath, 'utf8'); } catch { return; }
446
+ let settings;
447
+ try { settings = JSON.parse(raw); } catch { return; }
448
+ if (!settings || typeof settings !== 'object') return;
449
+
450
+ const desiredCommand = `bun "${launcherDst}"`;
451
+ // Launcher delegates to the fast native shim when present, so a constant
452
+ // short tick is fine and avoids cadence ping-pong across versions.
453
+ const desiredRefreshInterval = 5;
454
+ const existing = settings.statusLine;
455
+ // Recognize legacy mixdog-managed entries (versioned shim / statusline.mjs
456
+ // commands and the new stable launcher) so old pinned commands are MIGRATED
457
+ // to the stable launcher. A genuine user-custom statusLine is left alone.
458
+ const _cmd = (existing && typeof existing === 'object' && typeof existing.command === 'string')
459
+ ? existing.command : '';
460
+ // Normalize Windows backslashes once, then require a mixdog-SPECIFIC marker
461
+ // so a generic user command that merely mentions statusline.mjs is not
462
+ // hijacked. A bare statusline.mjs NOT under a mixdog path must NOT match.
463
+ const c = _cmd.replace(/\\/g, '/');
464
+ // No standalone --kind=statusline arm: the real mixdog shim command always
465
+ // carries the /mixdog-shim path (caught below), and a bare flag could be a
466
+ // genuine user command (e.g. `node user-tool.mjs --kind=statusline`).
467
+ const _looksMixdog = /\/mixdog-shim(?:\.exe)?/.test(c)
468
+ || c.includes('/trib-plugin/mixdog/')
469
+ || (c.includes('mixdog-trib-plugin') && c.includes('statusline-launcher.mjs'));
470
+ const isOurs = existing && typeof existing === 'object'
471
+ && (existing.source === 'mixdog-auto' || _looksMixdog);
472
+
473
+ if (existing && !isOurs) return;
474
+ if (
475
+ isOurs
476
+ && existing.command === desiredCommand
477
+ && existing.type === 'command'
478
+ && existing.refreshInterval === desiredRefreshInterval
479
+ ) return;
480
+
481
+ settings.statusLine = {
482
+ type: 'command',
483
+ command: desiredCommand,
484
+ refreshInterval: desiredRefreshInterval,
485
+ source: 'mixdog-auto',
486
+ };
487
+
488
+ const tmpPath = settingsPath + '.mixdog-tmp';
489
+ fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n');
490
+ fs.renameSync(tmpPath, settingsPath);
491
+ } catch (e) {
492
+ process.stderr.write(`[session-start] statusLine inject failed: ${e.message}\n`);
493
+ }
494
+ }
495
+
496
+ function cwdToProjectSlug(cwd) {
497
+ return path.resolve(cwd).replace(/\\/g, '/').replace(/^([A-Za-z]):/, '$1-').replace(/\//g, '-');
498
+ }
499
+ function resolveTranscriptPath() {
500
+ const direct = _event.transcript_path || _event.transcriptPath;
501
+ if (typeof direct === 'string' && direct && fs.existsSync(direct)) return direct;
502
+ const sessionId = _event.session_id || _event.sessionId;
503
+ const cwd = _event.cwd || process.cwd();
504
+ if (typeof sessionId === 'string' && sessionId) {
505
+ const candidate = path.join(os.homedir(), '.claude', 'projects', cwdToProjectSlug(cwd), `${sessionId}.jsonl`);
506
+ if (fs.existsSync(candidate)) return candidate;
507
+ }
508
+ return '';
509
+ }
510
+
511
+ function rebindActiveInstance() {
512
+ try {
513
+ const activePath = ACTIVE_INSTANCE_FILE;
514
+ if (!fs.existsSync(activePath)) return;
515
+ const active = JSON.parse(fs.readFileSync(activePath, 'utf8'));
516
+ if (!active.httpPort) return;
517
+ const transcriptPath = resolveTranscriptPath();
518
+ const payload = transcriptPath ? JSON.stringify({ transcriptPath }) : '';
519
+ const authHeaders = ownerAuthHeaders(active);
520
+ if (!authHeaders) return;
521
+ const headers = { 'Content-Type': 'application/json', ...authHeaders };
522
+ if (payload) headers['Content-Length'] = Buffer.byteLength(payload);
523
+ const req2 = http.request({
524
+ hostname: '127.0.0.1',
525
+ port: active.httpPort,
526
+ path: '/rebind',
527
+ method: 'POST',
528
+ timeout: 3000,
529
+ headers,
530
+ });
531
+ req2.on('error', () => {});
532
+ req2.on('timeout', () => req2.destroy());
533
+ if (payload) req2.write(payload);
534
+ req2.end();
535
+ } catch {}
536
+ }
537
+
538
+ // TCP probe — resolves true if the port accepts a connection within probeMs,
539
+ // false on ECONNREFUSED / EHOSTUNREACH / timeout / any other socket error.
540
+ // Used by pollActiveInstance to skip stale active-instance.json entries left
541
+ // over from a previous session whose channels owner is already dead.
542
+ function probeTcpPort(port, probeMs) {
543
+ return new Promise((resolve) => {
544
+ let settled = false;
545
+ const done = (alive) => {
546
+ if (settled) return;
547
+ settled = true;
548
+ try { socket.destroy(); } catch {}
549
+ resolve(alive);
550
+ };
551
+ const socket = net.createConnection({ port, host: '127.0.0.1' });
552
+ socket.setTimeout(Math.max(1, probeMs));
553
+ socket.once('connect', () => done(true));
554
+ socket.once('error', () => done(false));
555
+ socket.once('timeout', () => done(false));
556
+ });
557
+ }
558
+
559
+ // Memory-runtime readiness probe. A promoted-but-uninitialized fork-proxy keeps
560
+ // a live TCP listener while its `db` is still null (it short-circuited
561
+ // _initRuntime), so a raw TCP accept does NOT prove the memory worker can serve
562
+ // queries — recap/core would then 500 and silently drop context. GET /health
563
+ // returns 200 only once the worker is _initialized with a live DB; 503 while
564
+ // booting/promoting and 500 if the DB is broken. Gate awaitMemoryPort on a 200.
565
+ function probeMemoryHealthy(port, timeoutMs) {
566
+ return new Promise((resolve) => {
567
+ let settled = false;
568
+ const done = (ok) => { if (settled) return; settled = true; resolve(ok); };
569
+ let req;
570
+ try {
571
+ req = http.request(
572
+ { host: '127.0.0.1', port, path: '/health', method: 'GET', timeout: Math.max(1, timeoutMs) },
573
+ (res) => {
574
+ const ok = res.statusCode === 200;
575
+ res.resume();
576
+ res.once('end', () => done(ok));
577
+ res.once('error', () => done(false));
578
+ },
579
+ );
580
+ } catch { done(false); return; }
581
+ req.once('error', () => done(false));
582
+ req.once('timeout', () => { try { req.destroy(); } catch {} done(false); });
583
+ req.end();
584
+ });
585
+ }
586
+
587
+ async function pollActiveInstance(graceMs) {
588
+ const activeDir = MIXDOG_RUNTIME_ROOT;
589
+ const activeFile = ACTIVE_INSTANCE_FILE;
590
+ const _pollStart = Date.now();
591
+ const deadline = _pollStart + Math.max(0, graceMs);
592
+ teeStderr(`[session-start] pollActiveInstance start graceMs=${graceMs}\n`);
593
+ try { fs.mkdirSync(activeDir, { recursive: true }); } catch {}
594
+
595
+ // Single read+probe attempt. Stale guard: file may be left by a dead owner,
596
+ // so TCP-probe httpPort before accepting. Returns null on any failure
597
+ // (missing file, missing httpPort, dead port, parse error, deadline hit).
598
+ const tryRead = async () => {
599
+ try {
600
+ if (!fs.existsSync(activeFile)) return null;
601
+ const active = JSON.parse(fs.readFileSync(activeFile, 'utf8'));
602
+ if (!active || !active.httpPort) return null;
603
+ const remaining = deadline - Date.now();
604
+ if (remaining <= 0) return null;
605
+ const probeMs = Math.min(200, remaining);
606
+ const alive = await probeTcpPort(active.httpPort, probeMs);
607
+ teeStderr(`[session-start] pollActiveInstance probePort=${active.httpPort} probeMs=${probeMs} alive=${alive}\n`);
608
+ return alive ? active : null;
609
+ } catch {
610
+ return null;
611
+ }
612
+ };
613
+
614
+ // Fast path: supervisor already up. Most sessions hit this — skips watch setup.
615
+ const initial = await tryRead();
616
+ if (initial) {
617
+ teeStderr(`[session-start] pollActiveInstance done elapsed=${Date.now() - _pollStart}ms port=${initial.httpPort} via=initial\n`);
618
+ return initial;
619
+ }
620
+
621
+ // Bounded poll loop. Windows fs.watch first-event latency averaged ~3.3s
622
+ // on this directory (measured over recent sessions), so a fixed 200ms
623
+ // tick is deterministic and faster on average. server.mjs writes
624
+ // active-instance.json atomically via .tmp + rename, so every poll sees
625
+ // either no file or a fully-written one — no torn-read window. setTimeout
626
+ // caps total wait at graceMs so the function honors its contract.
627
+ return new Promise((resolve) => {
628
+ let settled = false;
629
+ let inFlight = false;
630
+ let pollTimer = null;
631
+ let deadlineTimer = null;
632
+
633
+ const finish = (result, via) => {
634
+ if (settled) return;
635
+ settled = true;
636
+ if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
637
+ if (deadlineTimer) { clearTimeout(deadlineTimer); deadlineTimer = null; }
638
+ if (result) {
639
+ teeStderr(`[session-start] pollActiveInstance done elapsed=${Date.now() - _pollStart}ms port=${result.httpPort} via=${via}\n`);
640
+ } else {
641
+ teeStderr(`[session-start] pollActiveInstance end elapsed=${Date.now() - _pollStart}ms result=null\n`);
642
+ }
643
+ resolve(result);
644
+ };
645
+
646
+ const checkOnce = async () => {
647
+ if (settled || inFlight) return;
648
+ inFlight = true;
649
+ try {
650
+ const a = await tryRead();
651
+ if (a) finish(a, 'poll');
652
+ } finally {
653
+ inFlight = false;
654
+ }
655
+ };
656
+
657
+ // Node setInterval / setTimeout are ref'd by default, so they keep the
658
+ // event loop alive. The outer caller also awaits this Promise, which is
659
+ // itself a liveness guarantee.
660
+ pollTimer = setInterval(checkOnce, 200);
661
+ const remaining = Math.max(0, deadline - Date.now());
662
+ deadlineTimer = setTimeout(() => finish(null, null), remaining);
663
+ });
664
+ }
665
+
666
+ // Wait until memory_port appears in active-instance.json and /health is ready.
667
+ // pollActiveInstance only proves channels httpPort;
668
+ // memory_port is written later via memory worker `ready` IPC. Without this,
669
+ // getMemoryServicePort throws on fast /clear paths and recap emits 0 lines.
670
+ async function awaitMemoryPort(graceMs) {
671
+ const _t0 = Date.now();
672
+ const deadline = _t0 + Math.max(0, graceMs);
673
+ teeStderr(`[session-start] awaitMemoryPort start graceMs=${graceMs}\n`);
674
+
675
+ while (Date.now() < deadline) {
676
+ const remaining = deadline - Date.now();
677
+ if (remaining <= 0) break;
678
+ try {
679
+ if (!fs.existsSync(ACTIVE_INSTANCE_FILE)) {
680
+ await sleepMs(Math.min(200, remaining));
681
+ continue;
682
+ }
683
+ const advertised = readAdvertisedMemoryPort();
684
+ if (!advertised) {
685
+ await sleepMs(Math.min(200, remaining));
686
+ continue;
687
+ }
688
+ if (advertised.stale) {
689
+ teeStderr(`[session-start] awaitMemoryPort stalePort=${advertised.port} activeServerPid=${advertised.activeServerPid} memoryServerPid=${advertised.memoryServerPid}\n`);
690
+ await sleepMs(Math.min(200, remaining));
691
+ continue;
692
+ }
693
+ const port = advertised.port;
694
+ const probeMs = Math.min(200, remaining);
695
+ const healthy = await probeMemoryHealthy(port, probeMs);
696
+ let supersededBy = null;
697
+ let staleAfterProbe = false;
698
+ try {
699
+ const latest = readAdvertisedMemoryPort();
700
+ if (latest && latest.stale) staleAfterProbe = true;
701
+ else if (latest && latest.port !== port) supersededBy = latest.port;
702
+ } catch {}
703
+ const suffix = `${staleAfterProbe ? ' staleAfterProbe=true' : ''}${supersededBy ? ` supersededBy=${supersededBy}` : ''}`;
704
+ teeStderr(`[session-start] awaitMemoryPort probePort=${port} probeMs=${probeMs} healthy=${healthy}${suffix}\n`);
705
+ if (staleAfterProbe || supersededBy) continue;
706
+ if (healthy) {
707
+ teeStderr(`[session-start] awaitMemoryPort done elapsed=${Date.now() - _t0}ms port=${port} via=poll\n`);
708
+ return port;
709
+ }
710
+ } catch {
711
+ // Re-read active-instance.json on the next tick; atomic rename can
712
+ // transiently hide the file on Windows.
713
+ }
714
+ await sleepMs(Math.min(200, Math.max(0, deadline - Date.now())));
715
+ }
716
+
717
+ teeStderr(`[session-start] awaitMemoryPort end elapsed=${Date.now() - _t0}ms result=null\n`);
718
+ return null;
719
+ }
720
+
721
+ function httpPostJson({ hostname, port, path: urlPath, timeoutMs, body, ownerActive, authHeaders: explicitAuthHeaders }) {
722
+ return new Promise((resolve, reject) => {
723
+ const _t0 = Date.now();
724
+ const payload = typeof body === 'string' ? body : JSON.stringify(body);
725
+ const authHeaders = explicitAuthHeaders || (ownerActive ? ownerAuthHeaders(ownerActive) : {});
726
+ if (ownerActive && !authHeaders) {
727
+ resolve({
728
+ statusCode: 0,
729
+ body: JSON.stringify({ ok: false, reason: 'owner-route-unavailable' }),
730
+ ownerRouteUnavailable: true,
731
+ });
732
+ return;
733
+ }
734
+ const req = http.request({
735
+ hostname,
736
+ port,
737
+ path: urlPath,
738
+ method: 'POST',
739
+ headers: {
740
+ 'Content-Type': 'application/json',
741
+ 'Content-Length': Buffer.byteLength(payload),
742
+ ...authHeaders,
743
+ },
744
+ timeout: Math.max(1, timeoutMs),
745
+ }, (res) => {
746
+ const chunks = [];
747
+ res.on('data', (c) => chunks.push(c));
748
+ res.on('end', () => {
749
+ const _body = Buffer.concat(chunks).toString('utf8');
750
+ const _elapsed = Date.now() - _t0;
751
+ let _bodyReason = null;
752
+ try { const _p = JSON.parse(_body); if (_p && typeof _p.reason === 'string') _bodyReason = _p.reason; } catch {}
753
+ teeStderr(`[session-start] httpPostJson path=${urlPath} port=${port} timeoutMs=${timeoutMs} statusCode=${res.statusCode} bodyReason=${_bodyReason || ''} elapsed=${_elapsed}ms\n`);
754
+ resolve({ statusCode: res.statusCode, body: _body });
755
+ });
756
+ });
757
+ req.on('error', (e) => reject(e));
758
+ req.on('timeout', () => { req.destroy(new Error('timeout')); });
759
+ req.end(payload);
760
+ });
761
+ }
762
+
763
+ // Pull cycle1 signals from a cycle1 response. Memory worker returns an MCP
764
+ // envelope { content:[{type:'text', text:'cycle1: chunks=N processed=M
765
+ // skipped=K pending=P inFlight=B'}], isError }; channels owner returns
766
+ // { ok, result } where result may carry the same text shape. Each field is
767
+ // null when not parseable (treated downstream as "unknown / cannot verify").
768
+ function extractCycleSignals(parsed) {
769
+ const empty = { processed: null, pendingRows: null, skippedInFlight: null };
770
+ if (!parsed) return empty;
771
+ let text = '';
772
+ if (Array.isArray(parsed.content) && parsed.content[0] && typeof parsed.content[0].text === 'string') {
773
+ text = parsed.content[0].text;
774
+ } else if (parsed.result && typeof parsed.result === 'object'
775
+ && Array.isArray(parsed.result.content)
776
+ && parsed.result.content[0]
777
+ && typeof parsed.result.content[0].text === 'string') {
778
+ // Channels owner wraps the memory worker's MCP envelope as
779
+ // { ok, result: { content:[{type:'text',text:'cycle1: ...'}], isError } },
780
+ // so the signal text lives at parsed.result.content[0].text.
781
+ text = parsed.result.content[0].text;
782
+ } else if (parsed.result && typeof parsed.result === 'object' && typeof parsed.result.text === 'string') {
783
+ text = parsed.result.text;
784
+ } else if (typeof parsed.result === 'string') {
785
+ text = parsed.result;
786
+ }
787
+ if (typeof text !== 'string') return empty;
788
+ const proc = text.match(/processed=(\d+)/);
789
+ const pend = text.match(/pending=(\d+)/);
790
+ const inflight = text.match(/inFlight=(true|false)/);
791
+ return {
792
+ processed: proc ? parseInt(proc[1], 10) : null,
793
+ pendingRows: pend ? parseInt(pend[1], 10) : null,
794
+ skippedInFlight: inflight ? inflight[1] === 'true' : null,
795
+ };
796
+ }
797
+
798
+ async function requestCycle1MemoryDirect(deadline, opts = {}, priorReason = '') {
799
+ const slot = opts.slot || 'unknown';
800
+ const start = Date.now();
801
+ const remainingForProbe = deadline - Date.now();
802
+ if (remainingForProbe <= 0) {
803
+ teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct reason=timeout-before-probe prior=${priorReason} elapsed=${Date.now() - start}ms\n`);
804
+ return null;
805
+ }
806
+ const port = await getLiveMemoryServicePort(Math.min(200, remainingForProbe));
807
+ if (!port) {
808
+ teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct reason=no-memory-port prior=${priorReason} elapsed=${Date.now() - start}ms\n`);
809
+ return null;
810
+ }
811
+ const remaining = deadline - Date.now();
812
+ if (remaining <= 0) {
813
+ teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct reason=timeout-before-post prior=${priorReason} elapsed=${Date.now() - start}ms\n`);
814
+ return null;
815
+ }
816
+
817
+ try {
818
+ const ON_DEMAND_CYCLE1_ARGS = { min_batch: 1, session_cap: 5, batch_size: 20, concurrency: 5 };
819
+ const res = await httpPostJson({
820
+ hostname: '127.0.0.1',
821
+ port,
822
+ path: '/api/tool',
823
+ timeoutMs: remaining,
824
+ body: {
825
+ name: 'memory',
826
+ arguments: {
827
+ action: 'cycle1',
828
+ ...ON_DEMAND_CYCLE1_ARGS,
829
+ _callerDeadlineMs: remaining,
830
+ },
831
+ },
832
+ });
833
+ if (res.statusCode !== 200) {
834
+ teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct reason=non-200 statusCode=${res.statusCode} prior=${priorReason} elapsed=${Date.now() - start}ms\n`);
835
+ return { ok: false, reason: 'memory-direct-non-200', statusCode: res.statusCode, bodyReason: null, elapsedMs: Date.now() - start, route: 'memory-direct' };
836
+ }
837
+ let parsed;
838
+ try { parsed = JSON.parse(res.body); }
839
+ catch {
840
+ teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct reason=parse-error prior=${priorReason} elapsed=${Date.now() - start}ms\n`);
841
+ return { ok: false, reason: 'memory-direct-parse-error', statusCode: 200, bodyReason: null, elapsedMs: Date.now() - start, route: 'memory-direct' };
842
+ }
843
+ if (parsed && parsed.isError) {
844
+ const sigText = Array.isArray(parsed.content) && parsed.content[0] ? parsed.content[0].text : '';
845
+ teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct reason=body-is-error text=${String(sigText || '').slice(0, 200)} prior=${priorReason} elapsed=${Date.now() - start}ms\n`);
846
+ return { ok: false, reason: 'memory-direct-body-is-error', statusCode: 200, bodyReason: null, elapsedMs: Date.now() - start, route: 'memory-direct' };
847
+ }
848
+ const sig = extractCycleSignals(parsed);
849
+ const procStr = sig.processed != null ? sig.processed : '?';
850
+ const pendStr = sig.pendingRows != null ? sig.pendingRows : '?';
851
+ const inFlightStr = sig.skippedInFlight === true ? 'true'
852
+ : sig.skippedInFlight === false ? 'false' : '?';
853
+ teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct reason=ok processed=${procStr} pending=${pendStr} inFlight=${inFlightStr} prior=${priorReason} elapsed=${Date.now() - start}ms\n`);
854
+ return {
855
+ ok: true,
856
+ processed: sig.processed,
857
+ pendingRows: sig.pendingRows,
858
+ skippedInFlight: sig.skippedInFlight,
859
+ route: 'memory-direct',
860
+ elapsedMs: Date.now() - start,
861
+ };
862
+ } catch (e) {
863
+ const msg = (e && e.message) || e;
864
+ const reason = /timeout/i.test(String(msg)) ? 'memory-direct-timeout' : 'memory-direct-http-error';
865
+ teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct reason=${reason} err=${String(msg).slice(0, 200)} prior=${priorReason} elapsed=${Date.now() - start}ms\n`);
866
+ return { ok: false, reason, statusCode: null, bodyReason: null, elapsedMs: Date.now() - start, route: 'memory-direct' };
867
+ }
868
+ }
869
+
870
+ async function waitCycle1MemoryDirect(deadline, opts = {}, waitMs = 1800) {
871
+ const slot = opts.slot || 'unknown';
872
+ const start = Date.now();
873
+ const waitUntil = Math.min(deadline, start + Math.max(0, waitMs));
874
+ const pollMs = 50;
875
+ let attempt = 0;
876
+ while (Date.now() < waitUntil) {
877
+ const sleepMs = Math.min(pollMs, waitUntil - Date.now());
878
+ if (sleepMs > 0) await new Promise((r) => setTimeout(r, sleepMs));
879
+ const direct = await requestCycle1MemoryDirect(deadline, opts, `pre-channels-wait-${attempt}`);
880
+ if (direct && direct.ok) {
881
+ teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct wait-hit attempt=${attempt} elapsed=${Date.now() - start}ms\n`);
882
+ return direct;
883
+ }
884
+ if (direct && !direct.ok) {
885
+ teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct wait-stop attempt=${attempt} reason=${direct.reason || 'unknown'} elapsed=${Date.now() - start}ms\n`);
886
+ return null;
887
+ }
888
+ attempt++;
889
+ }
890
+ teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct wait-miss attempts=${attempt} elapsed=${Date.now() - start}ms\n`);
891
+ return null;
892
+ }
893
+
894
+ // One full cycle1 attempt. Prefer the memory service when it is already
895
+ // advertised and alive: channels /cycle1 is only a proxy to the same memory
896
+ // action, while the channels owner can lag during session handoff. If memory
897
+ // is not ready, fall back to the channels owner readiness path.
898
+ async function requestCycle1Once(deadline, opts) {
899
+ const slot = opts.slot || 'unknown';
900
+ const graceMs = Number.isFinite(opts.graceMs) ? opts.graceMs : 5000;
901
+ const start = Date.now();
902
+
903
+ const finish = (payload) => {
904
+ const elapsedMs = Date.now() - start;
905
+ if (payload.ok) {
906
+ const procStr = payload.processed != null ? payload.processed : '?';
907
+ const pendStr = payload.pendingRows != null ? payload.pendingRows : '?';
908
+ const inFlightStr = payload.skippedInFlight === true ? 'true'
909
+ : payload.skippedInFlight === false ? 'false' : '?';
910
+ teeStderr(`[session-start] cycle1 slot=${slot} route=channels reason=ok processed=${procStr} pending=${pendStr} inFlight=${inFlightStr} elapsed=${elapsedMs}ms\n`);
911
+ return {
912
+ ok: true,
913
+ processed: payload.processed,
914
+ pendingRows: payload.pendingRows,
915
+ skippedInFlight: payload.skippedInFlight,
916
+ route: 'channels',
917
+ elapsedMs,
918
+ };
919
+ }
920
+ const sc = payload.statusCode != null ? ` statusCode=${payload.statusCode}` : '';
921
+ teeStderr(`[session-start] cycle1 slot=${slot} route=channels reason=${payload.reason}${sc} elapsed=${elapsedMs}ms\n`);
922
+ return { ok: false, reason: payload.reason, statusCode: payload.statusCode, bodyReason: payload.bodyReason || null, elapsedMs, route: 'channels' };
923
+ };
924
+
925
+ const classifyError = (e) => {
926
+ const msg = (e && e.message) || '';
927
+ if (/timeout/i.test(msg)) return 'timeout';
928
+ if ((e && e.code === 'ECONNREFUSED') || /ECONNREFUSED/i.test(msg)) return 'connect-refused';
929
+ return 'http-error';
930
+ };
931
+
932
+ const directFirst = await requestCycle1MemoryDirect(deadline, opts, 'pre-channels');
933
+ if (directFirst && directFirst.ok) return directFirst;
934
+ if (!directFirst) {
935
+ const directAfterWait = await waitCycle1MemoryDirect(deadline, opts, Math.min(graceMs, 1800));
936
+ if (directAfterWait && directAfterWait.ok) return directAfterWait;
937
+ }
938
+
939
+ const remainingForGrace = deadline - Date.now();
940
+ if (remainingForGrace <= 0) return finish({ ok: false, reason: 'timeout' });
941
+ const active = await pollActiveInstance(Math.min(graceMs, remainingForGrace));
942
+ const tPollEnd = Date.now();
943
+ if (!active) {
944
+ const direct = await requestCycle1MemoryDirect(deadline, opts, 'no-active-instance');
945
+ if (direct && direct.ok) return direct;
946
+ const reason = (Date.now() >= deadline) ? 'timeout' : 'no-active-instance';
947
+ return finish({ ok: false, reason });
948
+ }
949
+ const port = active.httpPort;
950
+ const remaining = deadline - Date.now();
951
+ if (remaining <= 0) return finish({ ok: false, reason: 'timeout' });
952
+ const authHeaders = ownerAuthHeaders(active);
953
+ if (!authHeaders) return finish({ ok: false, reason: 'owner-route-unavailable' });
954
+
955
+ try {
956
+ const tPostStart = Date.now();
957
+ // On-demand cycle1 (SessionStart hook path): min_batch=1 triggers on a
958
+ // single pending row; session_cap=5 × batch_size=20 caps a single hook
959
+ // pass at 100 rows so wallclock stays low. Use modest 2-way fan-out when
960
+ // multiple windows are ready; periodic path also runs 2×50 in parallel.
961
+ const ON_DEMAND_CYCLE1_ARGS = { min_batch: 1, session_cap: 5, batch_size: 20, concurrency: 5 };
962
+ const res = await httpPostJson({
963
+ hostname: '127.0.0.1',
964
+ port,
965
+ path: '/cycle1',
966
+ timeoutMs: remaining,
967
+ ownerActive: active,
968
+ authHeaders,
969
+ body: { timeout_ms: remaining, args: ON_DEMAND_CYCLE1_ARGS },
970
+ });
971
+ teeStderr(`[session-start] cycle1 slot=${slot} timing pollMs=${tPollEnd - start} postMs=${Date.now() - tPostStart}\n`);
972
+ if (res.ownerRouteUnavailable) {
973
+ return finish({ ok: false, reason: 'owner-route-unavailable' });
974
+ }
975
+ if (res.statusCode !== 200) {
976
+ // Surface the channels endpoint's body `reason` (memory-not-ready,
977
+ // worker-unavailable, ipc-error, memory-timeout, ...) so downstream
978
+ // retry logic and operators can see the precise transient class.
979
+ let bodyReason = null;
980
+ try {
981
+ const parsed = JSON.parse(res.body);
982
+ if (parsed && typeof parsed.reason === 'string') bodyReason = parsed.reason;
983
+ } catch {}
984
+ const reason = bodyReason ? `non-200/${bodyReason}` : 'non-200';
985
+ return finish({ ok: false, reason, statusCode: res.statusCode, bodyReason });
986
+ }
987
+ try {
988
+ const parsed = JSON.parse(res.body);
989
+ if (parsed && parsed.ok) {
990
+ const sig = extractCycleSignals(parsed);
991
+ return finish({
992
+ ok: true,
993
+ processed: sig.processed,
994
+ pendingRows: sig.pendingRows,
995
+ skippedInFlight: sig.skippedInFlight,
996
+ });
997
+ }
998
+ return finish({ ok: false, reason: 'body-not-ok', statusCode: 200 });
999
+ } catch {
1000
+ return finish({ ok: false, reason: 'parse-error', statusCode: 200 });
1001
+ }
1002
+ } catch (e) {
1003
+ const reason = classifyError(e);
1004
+ if (reason === 'connect-refused' || reason === 'timeout') {
1005
+ const direct = await requestCycle1MemoryDirect(deadline, opts, reason);
1006
+ if (direct && direct.ok) return direct;
1007
+ }
1008
+ return finish({ ok: false, reason });
1009
+ }
1010
+ }
1011
+
1012
+ // Public entry point. Single in-flight call — server-main.callWorker now
1013
+ // awaits the worker's first 'ready' IPC, so a pre-ready /cycle1 holds until
1014
+ // memory is up instead of bouncing 503. Keep one follow-up retry for the
1015
+ // processed=0 case: that means either an in-flight dedup hit (server
1016
+ // returned the prior run's empty result) or a pre-ingest race
1017
+ // (transcript-watch had not yet ingested pending raw entries). A short sleep
1018
+ // + second call covers both. If the second pass also returns 0, genuinely
1019
+ // empty.
1020
+ async function requestCycle1(timeoutMs, opts = {}) {
1021
+ const slot = opts.slot || 'unknown';
1022
+ const graceMs = Number.isFinite(opts.graceMs) ? opts.graceMs : 5000;
1023
+ const start = Date.now();
1024
+ const deadline = start + Math.max(0, timeoutMs);
1025
+ teeStderr(`[session-start] cycle1 slot=${slot} start graceMs=${graceMs} timeoutMs=${timeoutMs}\n`);
1026
+ teeStderr(`[boot-time] tag=cycle1-entry slot=${slot} tMs=${start}\n`);
1027
+
1028
+ // Boot-race transient classifier: a fresh session can fire cycle1 while the
1029
+ // new owner's channels worker is still binding 3462 (connect-refused), the
1030
+ // active-instance.json is briefly empty (no-active-instance), or channels is
1031
+ // up but the parent's memory worker hasn't sent its first 'ready' IPC yet
1032
+ // (503 with bodyReason in {memory-not-ready, worker-unavailable, ipc-error,
1033
+ // memory-timeout}). Warm peer boots often clear in 1–3s; cold first-boot
1034
+ // runtime init (PG attach + embedding warmup under multi-instance contention)
1035
+ // can take longer, so retry with backoff up to a wider budget rather than
1036
+ // skipping recap entirely.
1037
+ const TRANSIENT_BOOT_BODY_REASONS = new Set([
1038
+ 'memory-not-ready',
1039
+ 'worker-unavailable',
1040
+ 'ipc-error',
1041
+ 'memory-timeout',
1042
+ 'backend-not-ready',
1043
+ 'beacon-booting',
1044
+ ]);
1045
+ // memory-degraded is NOT transient: restart cap exceeded or PG PANIC.
1046
+ // Retrying it within the transient budget (~45s) would only delay the
1047
+ // session with no benefit, so it is excluded before transient classification.
1048
+ const NON_TRANSIENT_BOOT_BODY_REASONS = new Set([
1049
+ 'memory-degraded',
1050
+ ]);
1051
+ const TRANSIENT_TOP_REASONS = new Set([
1052
+ 'connect-refused',
1053
+ 'no-active-instance',
1054
+ ]);
1055
+ const isTransientBootFailure = (r) => {
1056
+ if (!r || r.ok) return false;
1057
+ if (r.bodyReason && NON_TRANSIENT_BOOT_BODY_REASONS.has(r.bodyReason)) return false;
1058
+ if (r.bodyReason && TRANSIENT_BOOT_BODY_REASONS.has(r.bodyReason)) return true;
1059
+ if (TRANSIENT_TOP_REASONS.has(r.reason)) return true;
1060
+ // 5xx + missing body — boot/restart race: a worker socket closed
1061
+ // mid-request surfaces as a channels /cycle1 500 (bodyReason empty), and
1062
+ // a pre-stub 503 looks the same. Both resolve once the worker finishes
1063
+ // (re)starting, so retry within the boot budget instead of aborting
1064
+ // recap/core. Known degraded states carry a bodyReason and are filtered
1065
+ // by NON_TRANSIENT_BOOT_BODY_REASONS above before reaching here.
1066
+ if (r.statusCode >= 500 && !r.bodyReason) return true;
1067
+ return false;
1068
+ };
1069
+ const TRANSIENT_RETRY_DELAY_MS = 250;
1070
+ const TRANSIENT_RETRY_BUDGET_MS = 45000;
1071
+ const transientDeadline = start + TRANSIENT_RETRY_BUDGET_MS;
1072
+
1073
+ try {
1074
+ let r1 = await requestCycle1Once(deadline, opts);
1075
+ let transientAttempt = 0;
1076
+ while (
1077
+ isTransientBootFailure(r1)
1078
+ && Date.now() < transientDeadline
1079
+ && (deadline - Date.now()) > TRANSIENT_RETRY_DELAY_MS + 500
1080
+ ) {
1081
+ transientAttempt++;
1082
+ teeStderr(`[session-start] cycle1 slot=${slot} transient-retry attempt=${transientAttempt} bodyReason=${r1.bodyReason || ''} statusCode=${r1.statusCode != null ? r1.statusCode : ''} elapsed=${r1.elapsedMs}ms nextDelay=${TRANSIENT_RETRY_DELAY_MS}ms reason=${r1.reason}\n`);
1083
+ await new Promise((r) => setTimeout(r, TRANSIENT_RETRY_DELAY_MS));
1084
+ r1 = await requestCycle1Once(deadline, { ...opts, slot: `${slot}:t${transientAttempt}` });
1085
+ }
1086
+ if (!r1.ok) return r1;
1087
+ if (r1.processed != null && r1.processed > 0) return r1;
1088
+ // Genuine empty (no pending raw rows AND no in-flight dedup hit) — retry
1089
+ // would do nothing useful, skip the second pass.
1090
+ if (r1.pendingRows === 0 && r1.skippedInFlight === false) return r1;
1091
+ const RETRY_DELAY_MS = 800;
1092
+ const remaining = deadline - Date.now();
1093
+ if (remaining <= RETRY_DELAY_MS + 200) return r1;
1094
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
1095
+ const r2 = await requestCycle1Once(deadline, { ...opts, slot: `${slot}:r` });
1096
+ if (!r2.ok) {
1097
+ teeStderr(`[session-start] cycle1 slot=${slot} r2-failed stale-fallback r1.pendingRows=${r1.pendingRows != null ? r1.pendingRows : '?'} r1.skippedInFlight=${r1.skippedInFlight != null ? r1.skippedInFlight : '?'}\n`);
1098
+ }
1099
+ return r2.ok ? r2 : r1;
1100
+ } catch (e) {
1101
+ teeStderr(`[session-start] cycle1 slot=${slot} exception=${(e && e.message) || e}\n`);
1102
+ return { ok: false, reason: 'exception', elapsedMs: Date.now() - start };
1103
+ }
1104
+ }
1105
+
1106
+ // Best-effort POST /recap/reset to the channels owner. Used on `/clear` so
1107
+ // the forked status server's recapState (which lives in a child process the
1108
+ // hook can't reach via IPC) drops the prior session's badge. Bounded by
1109
+ // graceMs and silent on failure — recap reset is cosmetic, never block
1110
+ // SessionStart on it.
1111
+ async function requestRecapReset(graceMs) {
1112
+ try {
1113
+ const active = await pollActiveInstance(Math.max(0, graceMs));
1114
+ if (!active || !active.httpPort) {
1115
+ teeStderr('[session-start] recap-reset skipped: no active instance\n');
1116
+ return;
1117
+ }
1118
+ const authHeaders = ownerAuthHeaders(active);
1119
+ if (!authHeaders) {
1120
+ teeStderr('[session-start] recap-reset skipped: owner route unavailable in this session\n');
1121
+ return;
1122
+ }
1123
+ const res = await httpPostJson({
1124
+ hostname: '127.0.0.1',
1125
+ port: active.httpPort,
1126
+ path: '/recap/reset',
1127
+ timeoutMs: 2000,
1128
+ ownerActive: active,
1129
+ authHeaders,
1130
+ body: {},
1131
+ });
1132
+ if (res.statusCode !== 200) {
1133
+ teeStderr(`[session-start] recap-reset non-200 status=${res.statusCode}\n`);
1134
+ }
1135
+ } catch (e) {
1136
+ teeStderr(`[session-start] recap-reset failed: ${(e && e.message) || e}\n`);
1137
+ }
1138
+ }
1139
+
1140
+ async function runRulesPart() {
1141
+ teeStderr(`[session-start] runRulesPart enter PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()}\n`);
1142
+ // First-boot one-shot work — only slot 1 (rules) runs this. Other slots
1143
+ // skip it entirely so they stay read-only and side-effect free.
1144
+ try {
1145
+ const flagPath = path.join(DATA_DIR, '.first-boot-seen');
1146
+ if (!fs.existsSync(flagPath)) {
1147
+ // No first-boot config-window open here: it double-booted the
1148
+ // setup-server against the unconditional every-session `--prewarm`
1149
+ // below (both racing to bind the port). The server is warmed by the
1150
+ // prewarm; the window opens only on an explicit `/mixdog:config`.
1151
+ fs.writeFileSync(flagPath, '');
1152
+ }
1153
+ } catch {}
1154
+
1155
+ // Every-session pre-warm: boot the config-UI setup-server in the
1156
+ // background (window-free) so the first `/mixdog:config` open is instant.
1157
+ // Fire-and-forget + hidden; `--prewarm` makes launch-core boot the server
1158
+ // only when it is not already alive and never opens a browser window, so
1159
+ // this is a cheap no-op once the server is running.
1160
+ try {
1161
+ spawn('bun', [path.join(PLUGIN_ROOT, 'setup', 'launch.mjs'), '--prewarm'], {
1162
+ detached: true,
1163
+ stdio: 'ignore',
1164
+ windowsHide: true,
1165
+ }).unref();
1166
+ } catch {}
1167
+
1168
+ try {
1169
+ const asp = path.join(DATA_DIR, 'active-session.txt');
1170
+ if (fs.existsSync(asp)) fs.unlinkSync(asp);
1171
+ } catch {}
1172
+
1173
+ // Persist user cwd so the MCP server (spawned from cache dir) can resolve
1174
+ // the correct sandbox root. Atomic rename prevents partial reads.
1175
+ try {
1176
+ const eventCwd = typeof _event.cwd === 'string' ? _event.cwd.trim() : '';
1177
+ if (eventCwd) {
1178
+ const cwdTxtPath = path.join(DATA_DIR, 'user-cwd.txt');
1179
+ const cwdTmpPath = cwdTxtPath + '.tmp';
1180
+ fs.writeFileSync(cwdTmpPath, eventCwd);
1181
+ fs.renameSync(cwdTmpPath, cwdTxtPath);
1182
+ }
1183
+ } catch {}
1184
+
1185
+ try {
1186
+ const stalePending = path.join(DATA_DIR, 'recap-pending.json');
1187
+ if (fs.existsSync(stalePending)) fs.unlinkSync(stalePending);
1188
+ } catch {}
1189
+
1190
+ injectStatusLine(PLUGIN_ROOT);
1191
+ rebindActiveInstance();
1192
+
1193
+ let _channelsConfig = {};
1194
+ try { _channelsConfig = readSection('channels'); } catch {}
1195
+ const injection = _channelsConfig && typeof _channelsConfig.promptInjection === 'object' ? _channelsConfig.promptInjection : {};
1196
+ const claudeMdMode = injection.mode === 'claude_md';
1197
+ const claudeMdTargetPath = typeof injection.targetPath === 'string' && injection.targetPath
1198
+ ? injection.targetPath
1199
+ : '~/.claude/CLAUDE.md';
1200
+ const needsBootstrapInjection = claudeMdMode && !hasManagedClaudeMdBlock(claudeMdTargetPath);
1201
+
1202
+ let additionalContext = '';
1203
+ if (!claudeMdMode || needsBootstrapInjection) {
1204
+ try {
1205
+ const { buildInjectionContent } = require(path.join(PLUGIN_ROOT, 'lib', 'rules-builder.cjs'));
1206
+ additionalContext = buildInjectionContent({ PLUGIN_ROOT, DATA_DIR }) || '';
1207
+ } catch {}
1208
+ }
1209
+
1210
+ // claude_md mode + missing managed block: persist the file alongside emit so the
1211
+ // next session boots from the managed block instead of the inline fallback.
1212
+ if (claudeMdMode && needsBootstrapInjection && additionalContext) {
1213
+ try {
1214
+ const { upsertManagedBlock } = require(path.join(PLUGIN_ROOT, 'lib', 'claude-md-writer.cjs'));
1215
+ upsertManagedBlock(claudeMdTargetPath, additionalContext);
1216
+ teeStderr(`[session-start] claude_md: regenerated ${claudeMdTargetPath} after missing managed block\n`);
1217
+ } catch (e) {
1218
+ teeStderr(`[session-start] claude_md regenerate failed: ${e && e.message || e}\n`);
1219
+ }
1220
+ }
1221
+
1222
+ // On `/clear`, drop the prior session's recap badge from the forked status
1223
+ // server. Hook runs in a separate cjs process with no IPC handle to that
1224
+ // child, so we POST /recap/reset to the channels owner instead. Best
1225
+ // effort, short grace — channels owner is usually already up on /clear.
1226
+ if (_event.source === 'clear') {
1227
+ // Fire-and-forget — recap reset is cosmetic, never block response path.
1228
+ requestRecapReset(3000).catch(() => {});
1229
+ }
1230
+
1231
+ // Surface the session-entry working directory so the Lead knows where
1232
+ // relative paths resolve before any `cwd set`. Reads the user-cwd.txt seed
1233
+ // written just above (mirrors the host's _event.cwd). Best-effort — never
1234
+ // let it break rule injection.
1235
+ let _startDir = '';
1236
+ try {
1237
+ _startDir = fs.readFileSync(path.join(DATA_DIR, 'user-cwd.txt'), 'utf8').trim();
1238
+ if (_startDir) {
1239
+ const _startBlock = `## Starting directory\n${_startDir}`;
1240
+ additionalContext = additionalContext ? `${additionalContext}\n\n${_startBlock}` : _startBlock;
1241
+ }
1242
+ } catch {}
1243
+
1244
+ // Other owned directories — full paths (the cwd tool's background scan
1245
+ // persists them to cwd-projects.json). Best-effort — never let it break
1246
+ // rule injection. Dynamic, so it lives here not in static rules.
1247
+ try {
1248
+ const _projParsed = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'cwd-projects.json'), 'utf8'));
1249
+ const _projects = Array.isArray(_projParsed && _projParsed.projects) ? _projParsed.projects : [];
1250
+ const _paths = _projects
1251
+ .map((p) => String(p.path || '').trim())
1252
+ .filter((p) => p && p !== _startDir);
1253
+ if (_paths.length) {
1254
+ const _otherBlock = `## Other directories\n${_paths.map((p) => `- ${p}`).join('\n')}`;
1255
+ additionalContext = additionalContext ? `${additionalContext}\n\n${_otherBlock}` : _otherBlock;
1256
+ }
1257
+ } catch {}
1258
+
1259
+ emit(additionalContext);
1260
+ teeStderr(`[session-start] runRulesPart done\n`);
1261
+ }
1262
+
1263
+ // ---------------------------------------------------------------------------
1264
+ // Part: core (slot 2) — Core Memory only. Runs in its own process so each
1265
+ // SessionStart additionalContext is sized independently against the host
1266
+ // preview cap. Pairs with recap (slot 3); both are spawned in parallel by
1267
+ // the host and share the cycle1 await on the server side.
1268
+ // ---------------------------------------------------------------------------
1269
+ async function runCorePart() {
1270
+ if (_skipMemoryInject()) {
1271
+ teeStderr(`[session-start] runCorePart skip PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} reason=skipMemoryInject\n`);
1272
+ return;
1273
+ }
1274
+ // graceMs=8000 covers supervisor cold-start ceiling. fs.watch unblocks
1275
+ // immediately on active-instance.json creation, so normal boots pay only
1276
+ // the actual startup time (no extra wait). Without this, first attempt
1277
+ // timed out at 3000ms before supervisor finished spawning, forcing a
1278
+ // transient-retry round-trip that added ~3s to wall-clock.
1279
+ const r = await requestCycle1(SESSION_START_CYCLE1_TIMEOUT_MS, { graceMs: 8000, slot: 'core' });
1280
+ if (r.ok !== true) {
1281
+ if (r.reason === 'owner-route-unavailable') {
1282
+ teeStderr('[session-start] core cycle1 skipped: owner route unavailable in this session\n');
1283
+ } else {
1284
+ teeStderr(`[session-start] core aborted: cycle1 await failed reason=${r.reason}\n`);
1285
+ return;
1286
+ }
1287
+ }
1288
+ // cycle1 ok guarantees channels owner liveness only; memory_port lands
1289
+ // later via worker `ready` IPC. Wait so getMemoryServicePort below does
1290
+ // not throw on fast /clear paths.
1291
+ const memPort = await awaitMemoryPort(8000);
1292
+ if (!memPort) {
1293
+ teeStderr(`[session-start] core aborted: memory_port unavailable within graceMs\n`);
1294
+ return;
1295
+ }
1296
+ const tStart = Date.now();
1297
+ try {
1298
+ const tCtxStart = Date.now();
1299
+ const ctx = await buildContext(_event.cwd || process.cwd());
1300
+ teeStderr(`[session-start] core stage=buildContext elapsed=${Date.now() - tCtxStart}ms hasCtx=${!!ctx}\n`);
1301
+ if (ctx) {
1302
+ const tEmitStart = Date.now();
1303
+ emit(ctx);
1304
+ teeStderr(`[session-start] core stage=emit elapsed=${Date.now() - tEmitStart}ms\n`);
1305
+ }
1306
+ } catch (e) {
1307
+ process.stderr.write(`[session-start] core build failed: ${e.message}\n`);
1308
+ }
1309
+ teeStderr(`[session-start] core done totalElapsed=${Date.now() - tStart}ms\n`);
1310
+ }
1311
+
1312
+ // ---------------------------------------------------------------------------
1313
+ // Part: recap (slot 3) — Recap entries only. Spawned in parallel with core.
1314
+ // ---------------------------------------------------------------------------
1315
+ async function runRecapPart() {
1316
+ if (_skipMemoryInject()) {
1317
+ teeStderr(`[session-start] runRecapPart skip PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} reason=skipMemoryInject\n`);
1318
+ return;
1319
+ }
1320
+ // graceMs=8000: see runCorePart for invariant rationale.
1321
+ const r = await requestCycle1(SESSION_START_CYCLE1_TIMEOUT_MS, { graceMs: 8000, slot: 'recap' });
1322
+ if (r.ok !== true) {
1323
+ if (r.reason === 'owner-route-unavailable') {
1324
+ teeStderr('[session-start] recap cycle1 skipped: owner route unavailable in this session\n');
1325
+ } else {
1326
+ teeStderr(`[session-start] recap aborted: cycle1 await failed reason=${r.reason}\n`);
1327
+ return;
1328
+ }
1329
+ }
1330
+ // See runCorePart: cycle1 success does not imply memory_port readiness.
1331
+ const memPort = await awaitMemoryPort(8000);
1332
+ if (!memPort) {
1333
+ teeStderr(`[session-start] recap aborted: memory_port unavailable within graceMs\n`);
1334
+ return;
1335
+ }
1336
+ const tStart = Date.now();
1337
+ try {
1338
+ const tRecapStart = Date.now();
1339
+ const recapData = await buildRecapData(_event.cwd || process.cwd());
1340
+ const lines = (recapData && recapData.lines) || [];
1341
+ teeStderr(`[session-start] recap stage=buildRecapData elapsed=${Date.now() - tRecapStart}ms lines=${lines.length}\n`);
1342
+ if (lines.length > 0) {
1343
+ const tEmitStart = Date.now();
1344
+ emit(`## Recap\n${lines.join('\n')}`);
1345
+ teeStderr(`[session-start] recap stage=emit elapsed=${Date.now() - tEmitStart}ms\n`);
1346
+ }
1347
+ } catch (e) {
1348
+ process.stderr.write(`[session-start] recap build failed: ${e.message}\n`);
1349
+ }
1350
+ teeStderr(`[session-start] recap done totalElapsed=${Date.now() - tStart}ms\n`);
1351
+ }
1352
+
1353
+ // ---------------------------------------------------------------------------
1354
+ // Exports for in-process daemon use (mixdog-shim → hook-pipe-server.mjs).
1355
+ // ---------------------------------------------------------------------------
1356
+ module.exports = { runRulesPart, runCorePart, runRecapPart, setEvent, setEmitSink, setPart };
1357
+
1358
+ // ---------------------------------------------------------------------------
1359
+ // Main IIFE — dispatch on PART. Only runs when invoked as the entry script;
1360
+ // require'd from the daemon stays a no-op (PART is undefined).
1361
+ // ---------------------------------------------------------------------------
1362
+ if (require.main === module) {
1363
+ (async () => {
1364
+ if (PART === 'rules') {
1365
+ await runRulesPart();
1366
+ } else if (PART === 'core') {
1367
+ await runCorePart();
1368
+ } else if (PART === 'recap') {
1369
+ await runRecapPart();
1370
+ }
1371
+ })();
1372
+ }