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,491 @@
1
+ import { isOffloadedToolResultText } from './tool-result-offload.mjs';
2
+ import { createHash } from 'node:crypto';
3
+
4
+ // Rough token estimate: ~4 chars per token
5
+ function estimateTokens(text) {
6
+ return Math.ceil(String(text ?? '').length / 4);
7
+ }
8
+ function messageEstimateText(m) {
9
+ if (!m || typeof m !== 'object') return '';
10
+ let text = typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? '');
11
+ if (m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length) {
12
+ try { text += `\n${JSON.stringify(m.toolCalls)}`; }
13
+ catch { text += `\n[${m.toolCalls.length} tool calls]`; }
14
+ }
15
+ if (m.role === 'tool' && m.toolCallId) text += `\n${m.toolCallId}`;
16
+ return text;
17
+ }
18
+ function estimateMessageTokens(m) {
19
+ return estimateTokens(messageEstimateText(m)) + 4;
20
+ }
21
+ function estimateMessagesTokens(messages) {
22
+ return messages.reduce((sum, m) => sum + estimateMessageTokens(m), 0);
23
+ }
24
+
25
+ // Per-request overhead the provider injects that never appears in the
26
+ // `messages` array: function-calling preamble + system-prompt framing the
27
+ // provider wraps around the request. The chars/4 message estimate misses all
28
+ // of it, so a "fits" verdict computed from messages alone is optimistic.
29
+ const REQUEST_OVERHEAD_TOKENS = 512;
30
+
31
+ /**
32
+ * Estimate the token cost of the tool/function schemas a provider appends to
33
+ * the request body. These are NOT part of `messages` (they're a separate
34
+ * argument to provider.send), so estimateMessagesTokens() ignores them
35
+ * entirely — a transcript that "fits" by message tokens can still overflow
36
+ * once N tool schemas are serialized into the same request. Best-effort
37
+ * chars/4 over the JSON-serialized definitions.
38
+ */
39
+ export function estimateToolSchemaTokens(tools) {
40
+ if (!Array.isArray(tools) || tools.length === 0) return 0;
41
+ let text = '';
42
+ try { text = JSON.stringify(tools); }
43
+ catch { text = tools.map(t => String(t?.name ?? '')).join(''); }
44
+ return estimateTokens(text);
45
+ }
46
+
47
+ /**
48
+ * Total headroom the caller should reserve out of the context window before
49
+ * trimming: tool-schema bytes + fixed request framing overhead. Pass the
50
+ * result as `opts.reserveTokens` to trimMessages so the working budget
51
+ * accounts for request-side bytes the message estimate cannot see.
52
+ */
53
+ export function estimateRequestReserveTokens(tools) {
54
+ return estimateToolSchemaTokens(tools) + REQUEST_OVERHEAD_TOKENS;
55
+ }
56
+ const TOOL_MISSING_STUB = '[Older tool result dropped to fit the context window]';
57
+ /**
58
+ * Walk backward from `idx` past consecutive tool messages to the parent
59
+ * assistant message that emitted the tool_calls. Returns an index that points
60
+ * at (or before) that assistant so a byte-budget cut drops the group as a
61
+ * unit rather than leaving orphan tool results. Returns `idx` unchanged if
62
+ * we didn't land inside a group.
63
+ */
64
+ export function alignBoundaryBackward(messages, idx) {
65
+ if (!Array.isArray(messages) || idx <= 0 || idx >= messages.length) return idx;
66
+ let i = idx;
67
+ while (i > 0 && messages[i]?.role === 'tool') i--;
68
+ if (i === idx) return idx;
69
+ const anchor = messages[i];
70
+ if (anchor?.role === 'assistant' && Array.isArray(anchor.toolCalls) && anchor.toolCalls.length) {
71
+ return i;
72
+ }
73
+ return idx;
74
+ }
75
+ /**
76
+ * Post-trim sanitization (Hermes `_sanitize_tool_pairs`):
77
+ * - Drop `tool` messages whose toolCallId has no surviving assistant tool_call.
78
+ * - For surviving assistant tool_calls whose results got trimmed, insert a
79
+ * stub tool message so the provider doesn't reject the request for
80
+ * unmatched tool_use_id.
81
+ * Messages ordering is preserved; stubs are inserted immediately after the
82
+ * assistant message so the tool pair sits adjacent.
83
+ */
84
+ export function sanitizeToolPairs(messages) {
85
+ if (!Array.isArray(messages) || messages.length === 0) return messages;
86
+ const assistantCallIds = new Set();
87
+ for (const m of messages) {
88
+ if (m.role === 'assistant' && Array.isArray(m.toolCalls)) {
89
+ for (const tc of m.toolCalls) {
90
+ if (tc && tc.id) assistantCallIds.add(tc.id);
91
+ }
92
+ }
93
+ }
94
+ const toolById = new Map();
95
+ for (const m of messages) {
96
+ if (m.role === 'tool' && m.toolCallId) toolById.set(m.toolCallId, m);
97
+ }
98
+ const filtered = messages.filter(m => {
99
+ if (m.role !== 'tool') return true;
100
+ if (!m.toolCallId) return true;
101
+ return assistantCallIds.has(m.toolCallId);
102
+ });
103
+ const result = [];
104
+ for (const m of filtered) {
105
+ result.push(m);
106
+ if (m.role !== 'assistant' || !Array.isArray(m.toolCalls)) continue;
107
+ for (const tc of m.toolCalls) {
108
+ if (!tc?.id) continue;
109
+ const existing = toolById.get(tc.id);
110
+ if (existing && filtered.includes(existing)) continue;
111
+ const preserved = existing?.content;
112
+ result.push({
113
+ role: 'tool',
114
+ content: isOffloadedToolResultText(preserved) ? preserved : TOOL_MISSING_STUB,
115
+ toolCallId: tc.id,
116
+ });
117
+ }
118
+ }
119
+ return result;
120
+ }
121
+
122
+ // Minimum body size to consider for hash-based dedup. Small results are
123
+ // cheap to re-deliver and short strings often collide on trivial content
124
+ // like "ok" or "done", so deduplicate only non-trivial bodies.
125
+ const DEDUP_MIN_BYTES = 512;
126
+
127
+ /**
128
+ * Replace duplicate tool-result bodies (2nd+ occurrence of the same content
129
+ * hash) with a compact reference stub. Hash-based dedup avoids re-delivering
130
+ * large identical results (e.g. the same grep output called twice) while
131
+ * keeping the first occurrence intact so the model still has the body.
132
+ *
133
+ * Skip conditions (structural — not heuristic prefix sniffing):
134
+ * - m.toolKind !== 'normal' (and defined): cache-hit / error / ref messages
135
+ * carry a structured kind annotation set by loop.mjs; skip them.
136
+ * - No toolKind (undefined): legacy or intra-turn-dedup stubs — apply dedup
137
+ * (backward compatible; the dedup body IS the meaningful result).
138
+ * - content.length < DEDUP_MIN_BYTES: structural cost optimization.
139
+ * - isOffloadedToolResultText(content): body is on disk, not inline.
140
+ */
141
+ export function dedupToolResultBodies(messages) {
142
+ if (!Array.isArray(messages) || messages.length === 0) return messages;
143
+ const seenHash = new Map(); // hash -> first toolCallId
144
+ return messages.map((m) => {
145
+ if (m?.role !== 'tool' || typeof m.content !== 'string') return m;
146
+ const content = m.content;
147
+ if (content.length < DEDUP_MIN_BYTES) return m;
148
+ if (isOffloadedToolResultText(content)) return m;
149
+ // Structural kind-based skip: non-normal kinds are already stubs/refs —
150
+ // deduping them would nest stubs inside stubs and confuse the model.
151
+ if (m.toolKind !== undefined && m.toolKind !== 'normal') return m;
152
+ const hash = createHash('sha256').update(content).digest('hex').slice(0, 16);
153
+ const first = seenHash.get(hash);
154
+ if (!first) {
155
+ seenHash.set(hash, m.toolCallId || '?');
156
+ return m;
157
+ }
158
+ const stub = `[duplicate-of tool_use_id=${first}] body identical to result of ${first} (sha256 prefix matches; ${content.length} bytes elided).`;
159
+ return { ...m, content: stub };
160
+ });
161
+ }
162
+
163
+ // Match the head of dedupToolResultBodies' stub body so we can detect whether
164
+ // the referenced first-occurrence tool_use_id is still present after later
165
+ // drop passes (safety loop, sanitize). Any stub pointing at an id no longer
166
+ // in the message stream is reconciled back to TOOL_MISSING_STUB so the model
167
+ // never sees `[duplicate-of call_X]` with no call_X.
168
+ const DEDUP_STUB_HEAD_RE = /^\[duplicate-of tool_use_id=([^\]]+)\]/;
169
+ export function reconcileDedupStubs(messages) {
170
+ if (!Array.isArray(messages) || messages.length === 0) return messages;
171
+ const presentIds = new Set();
172
+ for (const m of messages) {
173
+ if (m?.role === 'tool' && m.toolCallId) presentIds.add(m.toolCallId);
174
+ }
175
+ return messages.map((m) => {
176
+ if (m?.role !== 'tool' || typeof m.content !== 'string') return m;
177
+ const match = DEDUP_STUB_HEAD_RE.exec(m.content);
178
+ if (!match) return m;
179
+ if (presentIds.has(match[1])) return m;
180
+ return { ...m, content: TOOL_MISSING_STUB };
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Final-mile pairing for Anthropic API content arrays. Operates on the
186
+ * already-converted format (role: assistant|user|system, content: block[])
187
+ * — the mixdog-internal sanitizeToolPairs only sees toolCalls/toolCallId
188
+ * fields and misses cases where tool_use blocks were pushed directly into
189
+ * content (streaming chunk inserts, salvage paths, etc.). Without this
190
+ * pass, an unmatched tool_use can reach the provider and trigger
191
+ * `messages.N: tool_use ids were found without tool_result blocks
192
+ * immediately after`.
193
+ */
194
+ export function sanitizeAnthropicContentPairs(messages) {
195
+ if (!Array.isArray(messages)) return messages;
196
+ const work = messages.slice();
197
+ const out = [];
198
+ for (let i = 0; i < work.length; i++) {
199
+ let m = work[i];
200
+ // Drop tool_use blocks without an id from assistant messages — these
201
+ // come from partial streaming chunks that never finalised, and the
202
+ // provider rejects them as `tool_use ids were found without
203
+ // tool_result blocks` even though no id was actually emitted.
204
+ if (m?.role === 'assistant' && Array.isArray(m.content)) {
205
+ const cleaned = m.content.filter(
206
+ (b) => !(b?.type === 'tool_use' && !b.id),
207
+ );
208
+ if (cleaned.length !== m.content.length) {
209
+ m = { ...m, content: cleaned };
210
+ work[i] = m;
211
+ }
212
+ }
213
+ out.push(m);
214
+ if (m?.role !== 'assistant' || !Array.isArray(m.content)) continue;
215
+ const toolUseIds = m.content
216
+ .filter((b) => b?.type === 'tool_use' && b.id)
217
+ .map((b) => b.id);
218
+ if (toolUseIds.length === 0) continue;
219
+ const next = work[i + 1];
220
+ const nextResultIds = (next?.role === 'user' && Array.isArray(next.content))
221
+ ? new Set(
222
+ next.content
223
+ .filter((b) => b?.type === 'tool_result' && b.tool_use_id)
224
+ .map((b) => b.tool_use_id),
225
+ )
226
+ : new Set();
227
+ const missing = toolUseIds.filter((id) => !nextResultIds.has(id));
228
+ const stubs = missing.map((id) => ({
229
+ type: 'tool_result',
230
+ tool_use_id: id,
231
+ content: '[tool_result missing — recovered by sanitizeAnthropicContentPairs]',
232
+ is_error: true,
233
+ }));
234
+ if (next?.role === 'user' && Array.isArray(next.content)) {
235
+ // Anthropic requires tool_result blocks to lead the user message
236
+ // when responding to a prior tool_use. Reorder even when no stub
237
+ // was needed; a matching tool_result after text still triggers the
238
+ // same `tool_use ids ... without tool_result blocks immediately
239
+ // after` rejection.
240
+ const existingResults = next.content.filter((b) => b?.type === 'tool_result');
241
+ const nonResults = next.content.filter((b) => b?.type !== 'tool_result');
242
+ const reordered = [...stubs, ...existingResults, ...nonResults];
243
+ const changed = missing.length > 0 || reordered.some((b, idx) => b !== next.content[idx]);
244
+ if (changed) work[i + 1] = { ...next, content: reordered };
245
+ } else {
246
+ if (missing.length === 0) continue;
247
+ out.push({ role: 'user', content: stubs });
248
+ }
249
+ }
250
+ return out;
251
+ }
252
+
253
+ /**
254
+ * Trim messages to fit within a token budget.
255
+ *
256
+ * Single linear path — no fallback chain:
257
+ * 1. Sanitize tool pairs on entry.
258
+ * 2. Dedup repeated large tool-result bodies (hash-based; first occurrence kept).
259
+ * 3. Return deduped messages if already within budget.
260
+ * 4. Drop tool result messages oldest-first; also drop paired assistant
261
+ * once all its tool calls are gone.
262
+ * 5. If still over budget, drop oldest non-system messages respecting
263
+ * tool-call group boundaries.
264
+ * 6. Final sanitize + safety loop absorbs stub-insertion overshoot.
265
+ *
266
+ * budgetTokens MUST be derived from the model's context window by the
267
+ * caller (session.contextWindow * safetyFactor). Passing 0 or a negative
268
+ * value is a caller error and throws immediately.
269
+ *
270
+ * opts.reserveTokens (optional) subtracts request-side bytes the message
271
+ * estimate cannot see (tool schemas + provider framing — see
272
+ * estimateRequestReserveTokens). The reserve is clamped so it can never
273
+ * drive the effective budget to <= 0; at most it leaves 1 token of working
274
+ * room so callers that over-estimate the reserve still get a usable budget
275
+ * rather than an immediate throw.
276
+ */
277
+ export function trimMessages(messages, budgetTokens, opts = {}) {
278
+ if (!(budgetTokens > 0)) throw new Error();
279
+ const reserve = Number(opts?.reserveTokens) || 0;
280
+ if (reserve > 0) {
281
+ // Cap the reserve so it can never consume more than half the budget.
282
+ // An over-estimated reserve (large tool schemas relative to a small
283
+ // context window) would otherwise collapse the effective budget toward
284
+ // 1, making system+latest baseCost exceed it and forcing trimMessages
285
+ // to throw — which defeats the overflow-resilience goal. Half-budget
286
+ // is the floor: the reserve still accounts for tool schemas but can
287
+ // never starve the mandatory system+latest content.
288
+ const effectiveReserve = Math.min(reserve, Math.floor(budgetTokens * 0.5));
289
+ budgetTokens = Math.max(1, budgetTokens - effectiveReserve);
290
+ }
291
+
292
+ const sanitized = sanitizeToolPairs(messages);
293
+ // Cache-anchor invariant: when sanitized messages already fit the budget,
294
+ // return them WITHOUT running dedupToolResultBodies. Dedup creates new
295
+ // object refs on every duplicate hit (sha256 match on ≥512B tool result
296
+ // bodies); messagesArrayChanged in loop.mjs detects those new refs as
297
+ // mutation and drops providerState, which kills the server-side
298
+ // previous_response_id chain that xAI Responses / Codex WS rely on for
299
+ // prefix cache reuse. Skip the payload optimization when it would cost
300
+ // a fresh cache cold-start for no token-budget benefit.
301
+ if (estimateMessagesTokens(sanitized) <= budgetTokens) return reconcileDedupStubs(sanitized);
302
+ // Over budget: dedup-then-fit before drop. If post-dedup body fits the
303
+ // budget, return the deduped result without dropping. Both stub and
304
+ // original are still in the message stream so the stub reference is safe.
305
+ // Survivors-only dedup (Pass 1/2 below) still applies when drop is required.
306
+ const deduped = dedupToolResultBodies(sanitized);
307
+ if (estimateMessagesTokens(deduped) <= budgetTokens) return reconcileDedupStubs(deduped);
308
+
309
+ const system = sanitized.filter(m => m.role === 'system');
310
+ const rest = sanitized.filter(m => m.role !== 'system');
311
+ if (rest.length === 0) return reconcileDedupStubs(system);
312
+
313
+ // The most-recent turn must survive intact. When the last message is a tool
314
+ // result, its owning assistant (and that assistant's sibling tool results)
315
+ // form one atomic "last group" — we keep the whole group, never a bare
316
+ // orphan tool result. The boundary is determined strictly by toolCallId
317
+ // ownership (invariant), not by position.
318
+ const lastMsg = rest[rest.length - 1];
319
+ let lastGroupStart = rest.length - 1;
320
+ if (lastMsg && lastMsg.role === 'tool' && lastMsg.toolCallId) {
321
+ const ownerIdx = rest.findIndex(m =>
322
+ m.role === 'assistant' && Array.isArray(m.toolCalls) &&
323
+ m.toolCalls.some(tc => tc.id === lastMsg.toolCallId)
324
+ );
325
+ if (ownerIdx !== -1) lastGroupStart = ownerIdx;
326
+ }
327
+ const lastGroup = rest.slice(lastGroupStart);
328
+ let middle = rest.slice(0, lastGroupStart);
329
+ const baseCost = estimateMessagesTokens(system) + estimateMessagesTokens(lastGroup);
330
+
331
+ // Exact fit (baseCost === budget) must be allowed — only a true overshoot throws.
332
+ if (baseCost > budgetTokens) throw new Error(`trimMessages: cannot fit even system+last group within budget=${budgetTokens} (base=${baseCost})`);
333
+
334
+ // Pass 1: drop tool results oldest-first; drop paired assistant when all its calls are gone.
335
+ let total = estimateMessagesTokens(middle);
336
+ while (total + baseCost > budgetTokens) {
337
+ const toolIdx = middle.findIndex(m => m.role === 'tool');
338
+ if (toolIdx === -1) break;
339
+ const toolCallId = middle[toolIdx].toolCallId;
340
+ total -= estimateMessageTokens(middle[toolIdx]);
341
+ middle.splice(toolIdx, 1);
342
+ if (toolCallId) {
343
+ const assistantIdx = middle.findIndex(m =>
344
+ m.role === 'assistant' && Array.isArray(m.toolCalls) &&
345
+ m.toolCalls.some(tc => tc.id === toolCallId)
346
+ );
347
+ if (assistantIdx !== -1) {
348
+ const assistantMsg = middle[assistantIdx];
349
+ const remainingCalls = assistantMsg.toolCalls.filter(tc =>
350
+ middle.some(m => m.role === 'tool' && m.toolCallId === tc.id)
351
+ );
352
+ if (remainingCalls.length === 0) {
353
+ total -= estimateMessageTokens(assistantMsg);
354
+ middle.splice(assistantIdx, 1);
355
+ }
356
+ }
357
+ }
358
+ }
359
+
360
+ if (total + baseCost <= budgetTokens) {
361
+ const s = sanitizeToolPairs([...system, ...middle, ...lastGroup]);
362
+ if (estimateMessagesTokens(s) <= budgetTokens) return reconcileDedupStubs(dedupToolResultBodies(s));
363
+ // sanitizeToolPairs stub inserts pushed back over budget — fall through to Pass 2.
364
+ // Recompute the last-group span on the POST-sanitize sequence: stubs may have
365
+ // been inserted adjacent to the last group, so using the original lastGroup.length
366
+ // as a tail offset is off-by-N. Locate by matching the lastGroup subsequence
367
+ // from the END of s — indexOf(lastGroup[0]) is wrong when the same object
368
+ // reference repeats earlier in middle (a duplicate would promote part of
369
+ // middle into the locked last group).
370
+ let anchorIdx = -1;
371
+ let lgMatchIdx = lastGroup.length - 1;
372
+ for (let si = s.length - 1; si >= 0 && lgMatchIdx >= 0; si--) {
373
+ if (s[si] === lastGroup[lgMatchIdx]) {
374
+ if (lgMatchIdx === 0) { anchorIdx = si; break; }
375
+ lgMatchIdx--;
376
+ }
377
+ }
378
+ const postSanitizeLastGroup = anchorIdx === -1 ? lastGroup : s.slice(anchorIdx);
379
+ const nonSystem = s.slice(0, anchorIdx === -1 ? s.length : anchorIdx)
380
+ .filter(m => m.role !== 'system');
381
+ middle = nonSystem;
382
+ // Replace lastGroup binding so downstream baseCost/safety bounds reflect
383
+ // the sanitize-grown tail (stubs inserted around the last group).
384
+ lastGroup.length = 0;
385
+ for (const m of postSanitizeLastGroup) lastGroup.push(m);
386
+ }
387
+
388
+ // Pass 2: drop oldest non-system messages, respecting tool-call group boundaries.
389
+ // Recompute baseCost against the (possibly grown) lastGroup so we don't underflow
390
+ // remaining by the original tail-cost estimate.
391
+ const baseCostP2 = estimateMessagesTokens(system) + estimateMessagesTokens(lastGroup);
392
+ let remaining = budgetTokens - baseCostP2;
393
+ const kept = [];
394
+ // Track which middle messages have already had their cost subtracted from
395
+ // `remaining`. Without this, a paired-assistant charged via the tool→owner
396
+ // lookup gets charged again when the outer loop reaches its own index,
397
+ // double-counting its cost and underflowing `remaining` -> premature break
398
+ // that drops content which still fits.
399
+ const charged = new Set();
400
+ const startIdx = alignBoundaryBackward(middle, middle.length - 1);
401
+ for (let i = startIdx; i >= 0; i--) {
402
+ const m = middle[i];
403
+ const cost = charged.has(m) ? 0 : estimateMessageTokens(m);
404
+ if (remaining - cost < 0) break;
405
+ if (m.role === 'tool' && m.toolCallId) {
406
+ const pairedIdx = middle.findIndex((a, idx) =>
407
+ idx < i && a.role === 'assistant' && Array.isArray(a.toolCalls) &&
408
+ a.toolCalls.some(tc => tc.id === m.toolCallId)
409
+ );
410
+ if (pairedIdx !== -1 && !kept.includes(middle[pairedIdx])) {
411
+ const paired = middle[pairedIdx];
412
+ const pairedCost = charged.has(paired) ? 0 : estimateMessageTokens(paired);
413
+ if (remaining - cost - pairedCost < 0) break;
414
+ remaining -= pairedCost;
415
+ charged.add(paired);
416
+ kept.unshift(paired);
417
+ }
418
+ }
419
+ if (m.role === 'assistant' && Array.isArray(m.toolCalls)) {
420
+ const toolResultCosts = m.toolCalls.reduce((sum, tc) => {
421
+ const toolMsg = middle.find(t =>
422
+ t.role === 'tool' && t.toolCallId === tc.id && !kept.includes(t)
423
+ );
424
+ return sum + (toolMsg && !charged.has(toolMsg) ? estimateMessageTokens(toolMsg) : 0);
425
+ }, 0);
426
+ if (remaining - cost - toolResultCosts < 0) break;
427
+ for (const tc of m.toolCalls) {
428
+ const toolMsg = middle.find(t =>
429
+ t.role === 'tool' && t.toolCallId === tc.id && !kept.includes(t)
430
+ );
431
+ if (toolMsg) {
432
+ if (!charged.has(toolMsg)) {
433
+ remaining -= estimateMessageTokens(toolMsg);
434
+ charged.add(toolMsg);
435
+ }
436
+ kept.push(toolMsg);
437
+ }
438
+ }
439
+ }
440
+ if (!charged.has(m)) {
441
+ remaining -= cost;
442
+ charged.add(m);
443
+ }
444
+ if (!kept.includes(m)) kept.unshift(m);
445
+ }
446
+
447
+ const middleOrder = new Map(middle.map((m, idx) => [m, idx]));
448
+ kept.sort((a, b) => (middleOrder.get(a) ?? 0) - (middleOrder.get(b) ?? 0));
449
+
450
+ // Run sanitizeToolPairs before the safety loop; defer dedupToolResultBodies until after
451
+ // so stubs are never left referencing content that the safety loop subsequently drops.
452
+ let result = sanitizeToolPairs([...system, ...kept, ...lastGroup]);
453
+ let safety = 16;
454
+ while (
455
+ safety-- > 0
456
+ && result.length > system.length + lastGroup.length
457
+ && estimateMessagesTokens(result) > budgetTokens
458
+ ) {
459
+ // Drop a whole aligned tool-group from the head of the non-system region,
460
+ // not a single arbitrary line. Splicing one line at a time can sever an
461
+ // assistant from its tool results (orphaned pair) — sanitizeToolPairs
462
+ // then re-inserts a stub for the dangling tool_use, the byte count drops
463
+ // by less than expected, and the loop oscillates without progress.
464
+ const head = system.length;
465
+ if (head >= result.length) break;
466
+ let dropEnd = head + 1;
467
+ const first = result[head];
468
+ if (first?.role === 'assistant' && Array.isArray(first.toolCalls) && first.toolCalls.length) {
469
+ const callIds = new Set(first.toolCalls.map(tc => tc.id).filter(Boolean));
470
+ while (
471
+ dropEnd < result.length
472
+ && result[dropEnd]?.role === 'tool'
473
+ && callIds.has(result[dropEnd].toolCallId)
474
+ ) dropEnd++;
475
+ }
476
+ // Refuse to cross into the locked last group.
477
+ const lastGroupStartIdx = result.length - lastGroup.length;
478
+ if (dropEnd > lastGroupStartIdx) break;
479
+ result.splice(head, dropEnd - head);
480
+ result = sanitizeToolPairs(result);
481
+ }
482
+ result = dedupToolResultBodies(result);
483
+ // Final stub-vs-survivor reconciliation: any [duplicate-of tool_use_id=X]
484
+ // pointing at an X no longer present in `result` (carried over from a
485
+ // prior trim and orphaned by this trim's drops) is rewritten to the
486
+ // generic missing-stub so the model never sees a reference to an absent id.
487
+ result = reconcileDedupStubs(result);
488
+ const finalTokens = estimateMessagesTokens(result);
489
+ if (finalTokens > budgetTokens) throw new Error(`trimMessages: exhausted drop strategy, result=${finalTokens} > budget=${budgetTokens}`);
490
+ return result;
491
+ }
@@ -0,0 +1,115 @@
1
+ # Bridge Cache-Shard Policy
2
+
3
+ Authoritative policy for prefix-cache shard construction across all bridge sessions. The implementation must satisfy every rule below; reviewers should reject changes that turn stable shared layers into per-call fragments.
4
+
5
+ ## Design philosophy
6
+
7
+ Maximize cross-role / cross-call cache reuse by packing every role's policy into a SHARED monolithic prefix. Bridge sessions across all roles see bit-identical BP1 + BP2. User-defined customizations (roles / schedules / webhooks) are baked into BP1 as a fixed-value block — a user edit invalidates BP1 once and the new prefix re-warms across every role together.
8
+
9
+ The only per-call variance lives in BP4 (5m tail). Lead-only fields (e.g. memory recap) never enter bridge sessions.
10
+
11
+ ## 4-Block Layout
12
+
13
+ | Block | Role | Cache TTL | Hashed by registry | Content | Variability |
14
+ |---|---|---|---|---|---|
15
+ | BP1 baseRules | system | 1h | YES | bridge common rules + tool/memory/search/explore guidance + DATA_DIR roles/schedules/webhooks (monolithic) | Stable across all bridge calls. Invalidated only when a user edits roles/schedules/webhooks or plugin upgrades. |
16
+ | BP2 roleCatalog | system | 1h | YES | scoped role catalog + project context | Stable for a given provider/project. Project context changes re-hash this block. |
17
+ | BP3 sessionMarker | user (`<system-reminder>` with `<!-- bp3-sentinel -->` marker) | 1h | NO | role-specific task instructions, when present | Stable for that bridge session/task. Empty when no role-specific body is present. |
18
+ | BP4 volatileTail | user (`<system-reminder>`, no sentinel) | 5m | NO | role / permission / task-brief | May vary per call. |
19
+
20
+ **Note on tool schemas (Anthropic):** Anthropic's `cache_control` caches every block from the marked block back through the prefix (order: tools → system → messages). The system block's `cache_control` therefore covers the tool-schema array implicitly, so a separate dedicated tools breakpoint slot is not needed. This frees one of the 4 slots for the messages tail. See `anthropic.mjs:211-214` and `anthropic-oauth.mjs` for the layout decision.
21
+
22
+ **Provider cache classes:** Anthropic uses explicit breakpoints and can be marked warm. OpenAI uses a provider cache key/session prefix and can be tracked as a reusable shard. Gemini uses implicit prompt caching only; hits are observed via `cachedContentTokenCount`, but the registry must not treat that as a guaranteed warm shard.
23
+
24
+ ## Hash inputs
25
+
26
+ `registry.markWarm` / `checkWarm` hash exactly two things:
27
+
28
+ 1. `JSON.stringify(systemMessages)` — array of leading system-role messages from `session.messages` (BP1 + BP2 only)
29
+ 2. `tools.map(t => ({ name, description, inputSchema }))` — the bridge tool array (sorted alphabetically by name for bridge sessions)
30
+
31
+ Anything outside these two inputs MUST NOT influence the registry hash. cwd, role, permission, task-brief, memory recap, files, prompt text, tool results, provider/model/effort/fast — all excluded. Project context is part of BP2 when present, so it is included via the leading system-message hash.
32
+
33
+ ## cwd policy
34
+
35
+ - `cwd: null` is a fixed sentinel meaning "no caller workspace context". Internal callers (memory-cycle, scheduler, webhook) pass null deliberately to share one shard.
36
+ - Never upgrade `null` to `process.cwd()` — that defeats fork suppression. The public bridge entry at `src/agent/index.mjs` MUST honor null.
37
+ - Tilde (`~`) and relative paths must be normalized at the entry point. Once inside `prepareBridgeSession`, cwd is either an absolute path or null.
38
+ - cwd does NOT enter the registry hash. cwd-aware tools receive cwd via tool args at call time, not via prompt injection.
39
+
40
+ ## What goes where (must / must-not)
41
+
42
+ **baseRules (BP1) MUST contain (monolithic, fixed value):**
43
+ - `lib/rules-builder.cjs` static bridge injection (tool/memory/search/explore guidance)
44
+ - `rules/bridge/00-common.md`
45
+ - `DATA_DIR/roles/*.md` — every role definition aggregated
46
+ - `DATA_DIR/schedules/*/instructions.md` — every schedule aggregated
47
+ - `DATA_DIR/webhooks/*/instructions.md` — every webhook aggregated
48
+
49
+ User edits to roles/schedules/webhooks invalidate BP1 once. This is acceptable because: (a) edits are infrequent, (b) every role re-warms together to the new prefix, (c) keeping all role policies in BP1 maximizes cross-role hit rate compared to per-call branching.
50
+
51
+ **baseRules (BP1) MUST NOT contain:**
52
+ - per-call values (role identity, permission, task brief, memory recap)
53
+ - cwd, project context, file references
54
+
55
+ **roleCatalog (BP2) MUST contain only:**
56
+ - the scoped role catalog selected by `loadScopedRoleCatalog`
57
+ - `# project-context` when project context is present
58
+
59
+ **roleCatalog (BP2) MUST NOT contain:**
60
+ - role / permission / task-brief markers
61
+ - volatile tool results or user task text
62
+
63
+ **sessionMarker (BP3) MUST contain only:**
64
+ - `<!-- bp3-sentinel -->\n<roleSpecific>` when role-specific instructions are present
65
+ - nothing when role-specific instructions are absent — emit no `<system-reminder>` at all in that case
66
+
67
+ **volatileTail (BP4) MUST contain (any subset):**
68
+ - `# role`, `# permission`, `# task-brief`
69
+
70
+ **Lead-only fields — MUST NOT enter bridge sessions:**
71
+ - `# memory-context` — recap / history context. Reserved for Lead session prompt only. Bridge `composeSystemPrompt` callers must not pass `opts.memoryContext` for `opts.owner === 'bridge'`.
72
+
73
+ ## Provider Tier3 selection
74
+
75
+ Anthropic provider wrapper auto-marks the first user `<system-reminder>` as BP3 (1h). To prevent volatileTail from being mistaken for BP3:
76
+
77
+ - `sessionMarker` MUST carry the explicit BP3 sentinel `<!-- bp3-sentinel -->` at the head.
78
+ - The provider wrapper MUST mark only sentinel-bearing reminders as 1h. Non-sentinel reminders ride 5m default.
79
+ - When sessionMarker is empty, no BP3 mark is emitted; volatileTail stays at 5m.
80
+
81
+ ## Tool list canonicalization
82
+
83
+ Bridge sessions (`opts.owner === 'bridge'`) receive a tool array sorted alphabetically by `tool.name` after the deny-list filter. This eliminates incidental fragmentation from MCP/internal registration order changes.
84
+
85
+ ## Entry-point checklist (cwd handling)
86
+
87
+ Every external entry point that constructs a bridge session must satisfy:
88
+
89
+ 1. `args.cwd` provided → `normalizeInputPath(args.cwd)` (expand `~`, resolve relative)
90
+ 2. `args.cwd` absent and `callerCwd` available → use `callerCwd`
91
+ 3. Both absent → `cwd: null`. NEVER fall back to `process.cwd()`.
92
+
93
+ Current entry points (must remain compliant):
94
+ - `src/agent/index.mjs` `case 'bridge'` (Lead-originated MCP dispatch; the unified `bridge` handler routes type=spawn/send/close/list)
95
+ - `src/agent/orchestrator/smart-bridge/bridge-llm.mjs` `makeBridgeLlm` (internal callers)
96
+
97
+ ## Trigger map (when prefix transitions)
98
+
99
+ Single transition cause → effect:
100
+
101
+ - Edit `lib/rules-builder.cjs` static block, `agents/*.md`, `rules/bridge/*.md`, or any `DATA_DIR/{roles,schedules,webhooks}/*.md` → BP1 or BP2 hash changes once → all roles/presets re-warm together (acceptable, infrequent)
102
+ - Add/remove an MCP or internal tool → tool_schema_hash changes → all bridge sessions re-warm
103
+ - Project switch → project context may change BP2 → provider shard re-warm/observe under a new prefix hash
104
+ - Per-call (role / permission / task-brief change) → only BP4 5m tail differs; BP1/BP2/BP3 untouched
105
+
106
+ If a single per-call change invalidates more than BP4, the policy has been violated.
107
+
108
+ ## Forbidden patterns
109
+
110
+ - per-role tool narrowing in bridge sessions (`opts.allowedTools` whitelist) — fragments BP2 by role group
111
+ - `process.cwd()` fallback in any bridge entry point — breaks the null sentinel
112
+ - emitting cwd or other variant data inside `<system-reminder>` blocks intended for BP3
113
+ - mid-pipeline cwd normalization (do it once at entry)
114
+ - routing memory-context into bridge sessions — Lead-only field
115
+ - changing the registry hash inputs (additions or removals) without updating this document in the same change