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,549 @@
1
+ /**
2
+ * dispatch-persist — crash / restart recovery for async dispatch handles.
3
+ *
4
+ * Plugin MCP server can be restarted by Claude Code at any time (idle timeout,
5
+ * user reload, etc.). Any in-flight dispatch whose merge callback had not yet
6
+ * run would otherwise be orphaned silently — handle issued, no result, no
7
+ * abort notification.
8
+ *
9
+ * This module persists the minimum needed to recover:
10
+ * - handle (`dispatch_<tool>_...`)
11
+ * - tool (`recall` / `search` / `explore`)
12
+ * - queries (for the abort message)
13
+ * - createdAt
14
+ *
15
+ * On add: write through to disk. On complete/error: remove entry.
16
+ * On bootstrap: read file, emit one abort Noti per surviving entry, clear.
17
+ *
18
+ * Best-effort everywhere — never let persist IO break the caller.
19
+ */
20
+
21
+ import fs from 'fs';
22
+ import path, { join } from 'path';
23
+ import { writeJsonAtomicSync } from '../../shared/atomic-file.mjs';
24
+
25
+ const TTL_MS = 30 * 60_000;
26
+ const FILE_NAME = 'pending-dispatches.json';
27
+ // Per-entry persisted result body cap. pushDispatchResult already
28
+ // smart-truncates to ~30KB; this is a defense-in-depth ceiling so the
29
+ // pending file cannot balloon if a future caller skips truncation.
30
+ const PERSIST_RESULT_MAX_BYTES = 64 * 1024;
31
+
32
+ // File mode for the on-disk pending-dispatches.json. Matches config/snapshot
33
+ // data-at-rest posture: owner-only read/write. The replayed body may contain
34
+ // tool output bytes (recall/search/explore) that, despite redaction below,
35
+ // should not be world-readable.
36
+ const PERSIST_FILE_MODE = 0o600;
37
+
38
+ // High-confidence secret patterns redacted before the result body is written
39
+ // to pending-dispatches.json. recoverPending replays entry.content verbatim
40
+ // into model context, so an async explore/search/recall result that happened
41
+ // to surface a credential would otherwise duplicate it into this JSON.
42
+ //
43
+ // Conservative on purpose — false negatives are acceptable (already-known
44
+ // truncation defense remains), false positives just redact a token-shaped
45
+ // substring in a transcript. Patterns:
46
+ // - `sk-...` bearer-style API keys (OpenAI / Anthropic family prefix)
47
+ // - `Bearer <token>` Authorization header values
48
+ // - `*_API_KEY` / `*_TOKEN` / `*_SECRET` assignments (env / json / yaml)
49
+ // - JWTs (`eyJ` header + two more base64url segments)
50
+ // - PEM private-key blocks (GCP service-account / RSA)
51
+ // - `"private_key_id": "<40 hex>"` JSON fields (GCP service-account)
52
+ const _SECRET_PATTERNS = [
53
+ // sk-<16+ token chars>
54
+ /\bsk-[A-Za-z0-9_\-]{16,}\b/g,
55
+ // Bearer <token>
56
+ /\bBearer\s+[A-Za-z0-9._\-]{16,}\b/gi,
57
+ // FOO_API_KEY = "..." | FOO_TOKEN: '...' | FOO_SECRET=...
58
+ // Captures the assignment prefix in group 1 so the key name survives.
59
+ /\b([A-Z][A-Z0-9_]*_(?:API_KEY|TOKEN|SECRET|PASSWORD))\s*[:=]\s*["']?([A-Za-z0-9_./+\-]{8,})["']?/g,
60
+ // JWT: three dot-separated base64url segments; header starts `eyJ`
61
+ // (base64 of `{"`). High-precision — the `eyJ` anchor + two segments
62
+ // avoids matching generic base64.
63
+ /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,
64
+ // PEM private-key block — any common label (PKCS#8 plain, RSA, EC, DSA,
65
+ // OPENSSH, ENCRYPTED). Non-greedy so a body with multiple keys redacts
66
+ // each block independently.
67
+ /-----BEGIN (?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY-----/g,
68
+ // GCP service-account `"private_key_id": "<40 lowercase hex>"`.
69
+ // Captures the field prefix (g1) and closing quote (g2) so the JSON key
70
+ // and structure survive.
71
+ /("private_key_id"\s*:\s*")[a-f0-9]{40}(")/g,
72
+ ];
73
+
74
+ function redactSecrets(body) {
75
+ if (typeof body !== 'string' || body.length === 0) return body;
76
+ try {
77
+ let out = body;
78
+ out = out.replace(_SECRET_PATTERNS[0], '[REDACTED:sk-key]');
79
+ out = out.replace(_SECRET_PATTERNS[1], 'Bearer [REDACTED]');
80
+ out = out.replace(_SECRET_PATTERNS[2], (_m, k) => `${k}=[REDACTED]`);
81
+ out = out.replace(_SECRET_PATTERNS[3], '[REDACTED:jwt]');
82
+ out = out.replace(_SECRET_PATTERNS[4], '[REDACTED:private-key]');
83
+ out = out.replace(_SECRET_PATTERNS[5], (_m, k, q) => `${k}[REDACTED]${q}`);
84
+ return out;
85
+ } catch {
86
+ // Fail closed: a redaction failure must NEVER leak the raw/partial body.
87
+ return '[REDACTED: secret-redaction failed]';
88
+ }
89
+ }
90
+
91
+ // Per-dataDir Promise tails — different dataDirs run in parallel.
92
+ // Keyed by normalized absolute dataDir path (path.resolve); value is the
93
+ // current tail Promise. Normalization ensures '/data/x/' and '/data/x' route
94
+ // to the same tail entry.
95
+ const _writeTails = new Map();
96
+
97
+ // Last successfully written payload per dataDir for exit-drain sync flush.
98
+ const _lastPayload = new Map();
99
+
100
+ // In-progress desired state captured at writeAll entry (before the async write
101
+ // completes). exitDrain prefers this over _lastPayload because it is newer —
102
+ // it reflects mutations that queued after the last completed writeAll but
103
+ // before process exit. Cleared once writeAll succeeds.
104
+ const _pendingPayload = new Map();
105
+
106
+ function getTail(dataDir) {
107
+ return _writeTails.get(path.resolve(dataDir)) ?? Promise.resolve();
108
+ }
109
+
110
+ function setTail(dataDir, p) {
111
+ _writeTails.set(path.resolve(dataDir), p);
112
+ }
113
+
114
+ // ── Exit drain: sync-flush in-flight tails on process exit ─────────────────
115
+ // Cannot await on exit; use sync writeFileSync to flush the last known payload.
116
+ //
117
+ // Risk (KEEP): this sync flush bypasses the async cross-process file lock.
118
+ // A concurrent writer from another process may race on the same file during
119
+ // the drain window. The window is bounded (process is exiting) and eliminating
120
+ // it requires a fundamentally different design (e.g. a dedicated lock-owner
121
+ // process). Best-effort is the correct trade-off here.
122
+ export function drainDispatchPersist() {
123
+ // Prefer _pendingPayload (desired state captured at writeAll entry) over
124
+ // _lastPayload (last successfully written state). Pending is strictly
125
+ // newer when a writeAll is still in-flight or queued at process exit.
126
+ const dirs = new Set([..._pendingPayload.keys(), ..._lastPayload.keys()]);
127
+ for (const dataDir of dirs) {
128
+ const payload = _pendingPayload.get(dataDir) ?? _lastPayload.get(dataDir);
129
+ if (!payload) continue;
130
+ try {
131
+ const p = pathFor(dataDir);
132
+ // fsync:false — see writeAll. This file is a best-effort restart-recovery
133
+ // spool; the page cache survives a plugin process restart (the only
134
+ // failure it guards), so we skip the synchronous disk-flush stall. KEEP
135
+ // lock:true: the exit-drain window can still race other processes.
136
+ writeJsonAtomicSync(p, payload, { compact: true, lock: true, mode: PERSIST_FILE_MODE, fsync: false });
137
+ } catch { /* best-effort */ }
138
+ }
139
+ }
140
+
141
+ // SIGTERM/SIGINT drain runs through drain-registry.mjs (single signal
142
+ // handler for the whole orchestrator); the bare 'exit' hook stays as an
143
+ // idempotent backup for cases where the registry already ran.
144
+ process.once('exit', drainDispatchPersist);
145
+
146
+ // ── Cross-process file lock ─────────────────────────────────────────────────
147
+ // Uses O_EXCL (wx flag) on a sibling .lock file so concurrent writers from
148
+ // different processes serialize around the same R/M/W on pending-dispatches.json.
149
+ // Wait briefly with jittered polling; stale lock files are cleared so a crashed
150
+ // writer cannot make every later dispatch persist best-effort-only.
151
+ const LOCK_FILE_NAME = 'pending-dispatches.json.lock';
152
+ const LOCK_WAIT_MS = 8_000;
153
+ const LOCK_POLL_MS = 50;
154
+ const LOCK_STALE_MS = 30_000;
155
+ const LOCK_WAIT_CODES = new Set(['EEXIST', 'EPERM', 'EACCES', 'EBUSY']);
156
+
157
+ function lockPath(dataDir) {
158
+ return join(dataDir, LOCK_FILE_NAME);
159
+ }
160
+
161
+ /**
162
+ * Acquire a cross-process file lock. Returns the lock-file path on success
163
+ * so the caller can pass it to releaseFileLock. Returns null if the lock
164
+ * could not be acquired within the timeout; callers then skip this
165
+ * best-effort persist rather than writing unlocked over another process.
166
+ */
167
+ async function acquireFileLock(dataDir) {
168
+ const lp = lockPath(dataDir);
169
+ const deadline = Date.now() + LOCK_WAIT_MS;
170
+ while (true) {
171
+ try {
172
+ // O_EXCL guarantees atomic create; fails with EEXIST if lock is held.
173
+ const fd = fs.openSync(lp, 'wx');
174
+ try { fs.writeSync(fd, `${process.pid} ${Date.now()}\n`, 0, 'utf8'); } catch { /* best-effort */ }
175
+ fs.closeSync(fd);
176
+ return lp;
177
+ } catch (err) {
178
+ if (!LOCK_WAIT_CODES.has(err?.code)) {
179
+ process.stderr.write(`[dispatch-persist] lock open error: ${err?.code || err?.message}\n`);
180
+ return null;
181
+ }
182
+ try {
183
+ const st = fs.statSync(lp);
184
+ if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
185
+ try { fs.unlinkSync(lp); } catch { /* another process won */ }
186
+ continue;
187
+ }
188
+ } catch { /* stat race; retry */ }
189
+ if (Date.now() >= deadline) {
190
+ process.stderr.write(`[dispatch-persist] lock timeout after ${LOCK_WAIT_MS}ms — skipping this best-effort persist\n`);
191
+ return null;
192
+ }
193
+ await new Promise(r => setTimeout(r, LOCK_POLL_MS + Math.floor(Math.random() * LOCK_POLL_MS)));
194
+ }
195
+ }
196
+ }
197
+
198
+ function releaseFileLock(lp) {
199
+ if (!lp) return;
200
+ try { fs.unlinkSync(lp); } catch { /* best-effort */ }
201
+ }
202
+
203
+ // ───────────────────────────────────────────────────────────────────────────
204
+
205
+ function pathFor(dataDir) {
206
+ return join(dataDir, FILE_NAME);
207
+ }
208
+
209
+ async function readAll(dataDir) {
210
+ try {
211
+ const p = pathFor(dataDir);
212
+ try {
213
+ await fs.promises.access(p);
214
+ } catch {
215
+ return {};
216
+ }
217
+ const raw = await fs.promises.readFile(p, 'utf8');
218
+ if (!raw.trim()) return {};
219
+ const parsed = JSON.parse(raw);
220
+ return (parsed && typeof parsed === 'object') ? parsed : {};
221
+ } catch {
222
+ return {};
223
+ }
224
+ }
225
+
226
+ function readAllSync(dataDir) {
227
+ try {
228
+ const p = pathFor(dataDir);
229
+ try {
230
+ fs.accessSync(p);
231
+ } catch {
232
+ return {};
233
+ }
234
+ const raw = fs.readFileSync(p, 'utf8');
235
+ if (!raw.trim()) return {};
236
+ const parsed = JSON.parse(raw);
237
+ return (parsed && typeof parsed === 'object') ? parsed : {};
238
+ } catch {
239
+ return {};
240
+ }
241
+ }
242
+
243
+ async function writeAll(dataDir, map) {
244
+ try {
245
+ const p = pathFor(dataDir);
246
+ // Capture desired state BEFORE the async write so exitDrain can sync-flush
247
+ // it even if this writeAll is still in-flight at process exit.
248
+ _pendingPayload.set(dataDir, map);
249
+ // fsync:false — pending-dispatches.json is a BEST-EFFORT restart-recovery
250
+ // spool, not durable data. The only event it must survive is a plugin MCP
251
+ // server restart, and the OS page cache already survives that (the bytes
252
+ // are visible to the next process without an fsync). The fsync only buys
253
+ // durability across an OS crash / power loss, which recovery does not rely
254
+ // on — so we skip the synchronous fsyncSync stall on the dispatch hot path.
255
+ // Atomic write-temp + rename ordering is unchanged; only the durability
256
+ // barrier is dropped. Default fsync behaviour is untouched for every other
257
+ // writeJsonAtomicSync caller (session saves, secrets, snapshots).
258
+ writeJsonAtomicSync(p, map, { compact: true, mode: PERSIST_FILE_MODE, fsync: false });
259
+ // Write completed — promote to last-written and clear pending (redundant now).
260
+ _lastPayload.set(dataDir, map);
261
+ _pendingPayload.delete(dataDir);
262
+ } catch { /* best-effort */ }
263
+ }
264
+
265
+ /**
266
+ * Prune expired entries. Returns `{ map, changed }` so callers can decide
267
+ * whether to write the pruned state back to disk. `changed === true` iff
268
+ * at least one entry was deleted (or was present but falsy). addPending
269
+ * always writes regardless, so it does not need the flag; hasPending /
270
+ * recoverPending / removePending use it to persist the pruned map instead
271
+ * of letting expired entries accumulate in pending-dispatches.json across
272
+ * restarts.
273
+ */
274
+ function gc(map) {
275
+ const now = Date.now();
276
+ let changed = false;
277
+ for (const [k, v] of Object.entries(map)) {
278
+ if (!v || (now - (v.createdAt || 0)) > TTL_MS) {
279
+ delete map[k];
280
+ changed = true;
281
+ }
282
+ }
283
+ return { map, changed };
284
+ }
285
+
286
+ function normalizeClientHostPid(v) {
287
+ const n = Number(v);
288
+ return Number.isFinite(n) && n > 0 ? n : null;
289
+ }
290
+
291
+ export function addPending(dataDir, handle, tool, queries, callerSessionId, clientHostPid) {
292
+ if (!dataDir || !handle) return;
293
+ const tail = getTail(dataDir).then(async () => {
294
+ try {
295
+ const lp = await acquireFileLock(dataDir);
296
+ if (!lp) return;
297
+ try {
298
+ const { map } = gc(await readAll(dataDir));
299
+ // Preserve any prior fields (e.g. content from setPendingResult) so a
300
+ // re-add by pushDispatchResult does not erase the persisted body.
301
+ const prior = map[handle] && typeof map[handle] === 'object' ? map[handle] : {};
302
+ const sid = callerSessionId != null && String(callerSessionId)
303
+ ? String(callerSessionId)
304
+ : prior.callerSessionId;
305
+ const hostPid = normalizeClientHostPid(clientHostPid) ?? normalizeClientHostPid(prior.clientHostPid);
306
+ map[handle] = {
307
+ ...prior,
308
+ tool,
309
+ queries: Array.isArray(queries) ? queries : [String(queries)],
310
+ createdAt: prior.createdAt || Date.now(),
311
+ ...(sid ? { callerSessionId: sid } : {}),
312
+ ...(hostPid ? { clientHostPid: hostPid } : {}),
313
+ };
314
+ await writeAll(dataDir, map);
315
+ try {
316
+ process.stderr.write(`[dispatch-persist] persist handle=${handle} tool=${tool} entries=${Object.keys(map).length}\n`);
317
+ } catch { /* best-effort */ }
318
+ } finally {
319
+ releaseFileLock(lp);
320
+ }
321
+ } catch { /* best-effort */ }
322
+ });
323
+ setTail(dataDir, tail);
324
+ }
325
+
326
+ /**
327
+ * Attach the merged dispatch result body to an existing pending entry. Called
328
+ * BEFORE notify so a torn-down transport / mid-push restart can replay the
329
+ * actual answer via recoverPending instead of the generic Aborted boilerplate.
330
+ *
331
+ * Body is truncated to PERSIST_RESULT_MAX_BYTES so a future skipped-truncation
332
+ * caller cannot balloon the pending file. Truncation marker is appended verbatim
333
+ * — the body is already a finished user-facing answer when this runs.
334
+ */
335
+ export function setPendingResult(dataDir, handle, tool, queries, content, isError, callerSessionId, clientHostPid) {
336
+ if (!dataDir || !handle) return Promise.resolve();
337
+ const tail = getTail(dataDir).then(async () => {
338
+ try {
339
+ const lp = await acquireFileLock(dataDir);
340
+ if (!lp) return;
341
+ try {
342
+ const { map } = gc(await readAll(dataDir));
343
+ const prior = map[handle] && typeof map[handle] === 'object' ? map[handle] : {};
344
+ const sid = callerSessionId != null && String(callerSessionId)
345
+ ? String(callerSessionId)
346
+ : prior.callerSessionId;
347
+ const hostPid = normalizeClientHostPid(clientHostPid) ?? normalizeClientHostPid(prior.clientHostPid);
348
+ let body = typeof content === 'string' ? content : String(content ?? '');
349
+ // Redact high-confidence secret patterns BEFORE truncation so a key
350
+ // that lands near the truncation boundary cannot be sliced into the
351
+ // persisted prefix unredacted.
352
+ body = redactSecrets(body);
353
+ if (Buffer.byteLength(body, 'utf8') > PERSIST_RESULT_MAX_BYTES) {
354
+ body = body.slice(0, PERSIST_RESULT_MAX_BYTES) + '\n…[persist-truncated]';
355
+ }
356
+ map[handle] = {
357
+ ...prior,
358
+ tool: tool || prior.tool,
359
+ queries: Array.isArray(queries) ? queries : (prior.queries || []),
360
+ createdAt: prior.createdAt || Date.now(),
361
+ content: body,
362
+ isError: !!isError,
363
+ ...(sid ? { callerSessionId: sid } : {}),
364
+ ...(hostPid ? { clientHostPid: hostPid } : {}),
365
+ };
366
+ await writeAll(dataDir, map);
367
+ try {
368
+ process.stderr.write(`[dispatch-persist] persist-result handle=${handle} tool=${tool} bytes=${Buffer.byteLength(body, 'utf8')}\n`);
369
+ } catch { /* best-effort */ }
370
+ } finally {
371
+ releaseFileLock(lp);
372
+ }
373
+ } catch { /* best-effort */ }
374
+ });
375
+ setTail(dataDir, tail);
376
+ // Return the tail Promise so callers (pushDispatchResult) can actually
377
+ // await the disk flush before notifying. Previously this returned void,
378
+ // letting notify race the debounced write — a crash between the two
379
+ // would lose the persisted body recoverPending depends on.
380
+ return tail;
381
+ }
382
+
383
+ /**
384
+ * Best-effort check: is there at least one non-expired in-flight dispatch
385
+ * recorded for this dataDir? Used by the scheduler's idle-state probe so
386
+ * background tasks stay suppressed while a bridge dispatch is still
387
+ * running. Never throws.
388
+ */
389
+ export function hasPending(dataDir) {
390
+ if (!dataDir) return false;
391
+ try {
392
+ // hasPending is a synchronous probe on the hot path; read without lock is
393
+ // acceptable (observation only). If gc pruned entries, flush asynchronously
394
+ // via per-dataDir tail so the write is still cross-process serialized.
395
+ const p = pathFor(dataDir);
396
+ let raw = '';
397
+ try { raw = fs.readFileSync(p, 'utf8'); } catch { /* missing = empty */ }
398
+ let parsed = {};
399
+ try { if (raw.trim()) parsed = JSON.parse(raw); } catch { /* best-effort */ }
400
+ if (!parsed || typeof parsed !== 'object') parsed = {};
401
+ const { map, changed } = gc(parsed);
402
+ if (changed) {
403
+ const tail = getTail(dataDir).then(async () => {
404
+ const lp = await acquireFileLock(dataDir);
405
+ if (!lp) return;
406
+ try { await writeAll(dataDir, map); } finally { releaseFileLock(lp); }
407
+ });
408
+ setTail(dataDir, tail);
409
+ }
410
+ return Object.keys(map).length > 0;
411
+ } catch {
412
+ return false;
413
+ }
414
+ }
415
+
416
+ export function removePending(dataDir, handle) {
417
+ if (!dataDir || !handle) return;
418
+ const tail = getTail(dataDir).then(async () => {
419
+ try {
420
+ const lp = await acquireFileLock(dataDir);
421
+ if (!lp) return;
422
+ try {
423
+ const { map, changed } = gc(await readAll(dataDir));
424
+ let mutated = changed;
425
+ if (handle in map) {
426
+ delete map[handle];
427
+ mutated = true;
428
+ try {
429
+ process.stderr.write(`[dispatch-persist] ack-pop handle=${handle} entries=${Object.keys(map).length}\n`);
430
+ } catch { /* best-effort */ }
431
+ }
432
+ if (mutated) await writeAll(dataDir, map);
433
+ } finally {
434
+ releaseFileLock(lp);
435
+ }
436
+ } catch { /* best-effort */ }
437
+ });
438
+ setTail(dataDir, tail);
439
+ }
440
+
441
+ /**
442
+ * Called once at plugin bootstrap after the MCP transport is connected.
443
+ * For every pending entry remaining from the previous process lifetime,
444
+ * emit a single Aborted notification with `type: 'dispatch_result'` so the
445
+ * Lead can close the loop on its next turn. Then clear the file.
446
+ *
447
+ * Recovery is chained onto the per-dataDir tail so it serializes with any
448
+ * in-flight addPending / removePending mutations for the same dataDir.
449
+ * Notifications fire asynchronously; the return value is the number of
450
+ * handles queued for recovery (callers use it as bootstrap telemetry).
451
+ */
452
+ export function recoverPending(dataDir, notifyFn, { sessionId, priorSessionId, clientHostPid } = {}) {
453
+ if (!dataDir || typeof notifyFn !== 'function') return 0;
454
+ const { map: snapshot } = gc(readAllSync(dataDir));
455
+ const filterSid = sessionId != null && String(sessionId) ? String(sessionId) : null;
456
+ const priorSid = priorSessionId != null && String(priorSessionId) ? String(priorSessionId) : null;
457
+ const filterHostPid = normalizeClientHostPid(clientHostPid);
458
+ const matchesScope = (entry) => {
459
+ if (!filterSid && !filterHostPid) return true;
460
+ const callerSessionId = entry?.callerSessionId;
461
+ const cid = callerSessionId != null && String(callerSessionId) ? String(callerSessionId) : null;
462
+ if (cid && (cid === filterSid || (priorSid != null && cid === priorSid))) return true;
463
+ const entryHostPid = normalizeClientHostPid(entry?.clientHostPid);
464
+ return filterHostPid != null && entryHostPid === filterHostPid;
465
+ };
466
+ const scoped = filterSid || filterHostPid;
467
+ const queued = scoped
468
+ ? Object.keys(snapshot).filter((h) => matchesScope(snapshot[h])).length
469
+ : Object.keys(snapshot).length;
470
+ const tail = getTail(dataDir).then(async () => {
471
+ const lp = await acquireFileLock(dataDir);
472
+ if (!lp) return;
473
+ try {
474
+ const { map, changed } = gc(await readAll(dataDir));
475
+ const handles = Object.keys(map).filter((handle) => {
476
+ return matchesScope(map[handle]);
477
+ });
478
+ if (handles.length === 0) {
479
+ // No handles to recover for this scope. A gc() pass may still have
480
+ // pruned expired entries — persist the pruned `map` (NOT `{}`): under a
481
+ // session-scoped recovery `handles` is only the reconnecting session's
482
+ // subset, so other sessions' still-live pending entries remain in `map`
483
+ // and must survive. (Unscoped recovery reaches here only when `map` is
484
+ // already empty, so writing `map` is equivalent to writing `{}` there.)
485
+ if (changed) await writeAll(dataDir, map);
486
+ return;
487
+ }
488
+ for (const handle of handles) {
489
+ const entry = map[handle] || {};
490
+ const tool = entry.tool || 'dispatch';
491
+ const queries = Array.isArray(entry.queries) ? entry.queries : [];
492
+ // Two recovery modes:
493
+ // 1. Persisted result body present → re-deliver the actual answer.
494
+ // The result was computed but the channel push didn't ack before
495
+ // the plugin died. setPendingResult wrote it pre-notify.
496
+ // 2. No body → the worker hadn't returned yet at restart; emit the
497
+ // Aborted boilerplate so the Lead can retry.
498
+ let content;
499
+ let isError;
500
+ let kind;
501
+ if (typeof entry.content === 'string' && entry.content.length > 0) {
502
+ content = entry.content;
503
+ isError = !!entry.isError;
504
+ kind = 'replay';
505
+ } else {
506
+ const qSuffix = queries.length === 1 ? '1 query' : `${queries.length} queries`;
507
+ content = `[${tool}] Aborted — plugin restart interrupted dispatch (${qSuffix}). Retry if still needed.`;
508
+ isError = true;
509
+ kind = 'abort';
510
+ }
511
+ const meta = {
512
+ type: 'dispatch_result',
513
+ dispatch_id: handle,
514
+ tool,
515
+ error: String(isError),
516
+ ...(filterSid
517
+ ? { caller_session_id: filterSid }
518
+ : (entry.callerSessionId ? { caller_session_id: entry.callerSessionId } : {})),
519
+ ...(filterHostPid > 0
520
+ ? { client_host_pid: String(filterHostPid) }
521
+ : (entry.clientHostPid > 0 ? { client_host_pid: String(entry.clientHostPid) } : {})),
522
+ instruction: kind === 'replay'
523
+ ? `Earlier ${tool} dispatch (${handle}) result was queued before plugin restart — here it is.`
524
+ : `Earlier ${tool} dispatch (${handle}) was aborted by a plugin restart. Retry if the answer is still needed.`,
525
+ };
526
+ try { process.stderr.write(`[dispatch-persist] recover handle=${handle} tool=${tool} kind=${kind}\n`); } catch { /* best-effort */ }
527
+ // Entry remains on disk until notifyFn resolves. A crash between
528
+ // fire and ack is safe: the entry survives and recoverPending re-fires
529
+ // it on the next restart. Only clear AFTER ack to prevent silent loss.
530
+ try {
531
+ Promise.resolve(notifyFn(content, meta)).then(() => {
532
+ removePending(dataDir, handle);
533
+ }).catch(() => { /* best-effort — entry stays for next recoverPending */ });
534
+ } catch { /* best-effort */ }
535
+ }
536
+ // Do NOT bulk-clear here. Each handle is removed individually above,
537
+ // only after its notifyFn acks. If gc() pruned expired entries, write
538
+ // back the pruned map (without expired keys) — live handles remain on
539
+ // disk until their per-handle removePending calls land.
540
+ if (changed) await writeAll(dataDir, map);
541
+ try {
542
+ process.stderr.write(`[dispatch-persist] recoverPending recovered=${handles.length} entries queued\n`);
543
+ } catch { /* best-effort */ }
544
+ } catch { /* best-effort */ }
545
+ finally { releaseFileLock(lp); }
546
+ });
547
+ setTail(dataDir, tail);
548
+ return queued;
549
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Exit-time drain registry for the cli.mjs process. Single SIGTERM/SIGINT
3
+ * owner — modules must NOT self-install signal handlers (avoids import-order
4
+ * race where the first handler's process.exit suppressed the rest).
5
+ * runAllDrains walks _drainBuckets in priority order; drains are best-effort
6
+ * by contract (never throw). Bucket order/membership pinned by tests.
7
+ */
8
+
9
+ import { drainSessionStore } from './session/store.mjs';
10
+ import { drainCodeGraphCache } from './tools/code-graph.mjs';
11
+ import { drainCacheStats } from './session/cache/scoped-cache.mjs';
12
+ import { drainBridgeTrace } from './bridge-trace.mjs';
13
+ import { drainDispatchPersist } from './dispatch-persist.mjs';
14
+ import { drainJobs } from './jobs.mjs';
15
+ import { drainBashSessions } from './tools/bash-session.mjs';
16
+ import { drainShellSnapshots } from './tools/shell-snapshot.mjs';
17
+ import { drainMcpClients } from './mcp/client.mjs';
18
+ import { drainOpenaiWsPool } from './providers/openai-oauth-ws.mjs';
19
+
20
+ const _drainBuckets = [
21
+ { name: 'data-integrity', drains: [drainSessionStore, drainDispatchPersist, drainJobs] },
22
+ { name: 'external-resource', drains: [drainBashSessions, drainShellSnapshots, drainMcpClients, drainOpenaiWsPool] },
23
+ { name: 'recoverable-cache', drains: [drainCodeGraphCache, drainCacheStats] },
24
+ { name: 'telemetry', drains: [drainBridgeTrace] },
25
+ ];
26
+
27
+ // Atomic single-execution: concurrent callers share the same Promise so
28
+ // each drain fires exactly once across process lifetime.
29
+ let _runningDrain = null;
30
+ export function runAllDrains() {
31
+ if (_runningDrain) return _runningDrain;
32
+ _runningDrain = (async () => {
33
+ for (const bucket of _drainBuckets) {
34
+ for (const fn of bucket.drains) await fn();
35
+ }
36
+ })();
37
+ return _runningDrain;
38
+ }
39
+
40
+ // Test-facing snapshot — tests pin priority order and bucket membership.
41
+ export const _internals = { _drainBuckets };
42
+
43
+ // Side-effect signal handlers — drain on SIGINT/SIGTERM same as normal exit.
44
+ // Exit code 128 + signal (SIGINT=2 → 130, SIGTERM=15 → 143).
45
+ async function _signalDrainExit(code) {
46
+ await runAllDrains();
47
+ process.exit(code);
48
+ }
49
+ process.on('SIGINT', () => { _signalDrainExit(130); });
50
+ process.on('SIGTERM', () => { _signalDrainExit(143); });
@@ -0,0 +1,8 @@
1
+ // Runtime envelope limits for explore output. These are not classifier
2
+ // heuristics — they cap raw string allocation to stay clear of V8's
3
+ // max-string-length (~512 MB) when concatenating subagent responses across
4
+ // a broad cwd (e.g. the whole ~/.claude tree). Lowering these would silently
5
+ // truncate legitimate output; raising them risks OOM crashes in the MCP server.
6
+ export const EXPLORE_OUTPUT_CHAR_CAP = 50_000_000
7
+ export const EXPLORE_PER_PIECE_CHAR_CAP = 5_000_000
8
+ export const EXPLORE_TRUNCATION_MARKER = '\n\n[explore: output truncated at 50MB cap; narrow cwd or split queries to see more]'