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,721 @@
1
+ // bash_session — persistent shell with state preserved across calls.
2
+ //
3
+ // Companion to the stateless `bash` tool. The default `bash` spawns a fresh
4
+ // subshell every call: cwd, exports, `source`d virtualenvs, shell functions
5
+ // all vanish between invocations. That's the safe default but it's clumsy
6
+ // for the common "cd into a project → activate venv → run three commands"
7
+ // workflow; each step has to reconstruct the prior shell context by hand.
8
+ //
9
+ // bash_session keeps a long-lived `bash` child process per session_id. The
10
+ // caller writes commands to stdin; we frame each command with a sentinel
11
+ // so we know when the command has finished and what its exit code was.
12
+ // State carried automatically: $PWD, exports, shell vars, readline history
13
+ // (not that we use it), aliases, function defs, `source`d files.
14
+ //
15
+ // Session lifecycle:
16
+ // - session_id omitted → mint a fresh id, spawn child, run command
17
+ // - session_id matches pool → reuse existing child
18
+ // - session_id misses pool → spawn child for that id (pool empty after
19
+ // orchestrator restart or idle eviction)
20
+ // - close:true → terminate child after command returns
21
+ // - idle > IDLE_TIMEOUT_MS → reaper removes & kills the child
22
+ // - pool > MAX_SESSIONS → oldest-idle evicted at spawn time
23
+ //
24
+ // Output protocol:
25
+ // write: <command>\necho "__MIXDOG_END__:$?"\n
26
+ // read: everything on stdout up to (not including) the marker line
27
+ // exit: the N in __MIXDOG_END__:N
28
+ // stderr: captured in parallel; sentinel not echoed there, so we flush
29
+ // whatever arrived up to the command's completion. Small
30
+ // quiescence window (STDERR_DRAIN_MS) after the stdout marker
31
+ // so trailing writes on stderr don't get cut off.
32
+ //
33
+ // Safety: same BLOCKED_PATTERNS as the `bash` tool. The session holds state
34
+ // so a dangerous command can't hide in an earlier turn (we scan per call).
35
+ // Same ANSI strip + smart middle-truncate applied to stdout/stderr.
36
+ //
37
+ // This tool takes a command, not a file path — no path-safety check applies.
38
+
39
+ import { spawn } from 'node:child_process';
40
+ import * as nodeUtil from 'node:util';
41
+ import { existsSync } from 'node:fs';
42
+ import { randomUUID } from 'node:crypto';
43
+ import { invalidateBuiltinResultCache, analyzeShellCommandEffects, preflightShellLargeFileProbe } from './builtin.mjs';
44
+ import { markCodeGraphDirtyPaths, drainCodeGraphCache } from './code-graph.mjs';
45
+ import { isBlockedCommand, maybeRewriteWmicProcessCommand } from './shell-policy.mjs';
46
+ import { stripQuotedAndHeredoc, extractShellCInner } from './destructive-warning.mjs';
47
+ import { _maybeEncodePowerShellCommand } from './shell-command.mjs';
48
+ import { _captureTrackedMtimes, _trackedDriftNoteAfter, _injectionBlockTargets, getDedupedDestructiveWarnings } from './builtin/bash-tool.mjs';
49
+ import { scrubLoaderVars, scrubProviderSecrets } from './env-scrub.mjs';
50
+
51
+ // Default 600 s (10 min), max 1800 s. Aligned with the one-shot bash tool's
52
+ // 600 s default (builtin/bash-tool.mjs); the persistent shell carries
53
+ // longer-running pipelines (build/test/install) than one-shot calls, so a
54
+ // 120 s default left those workflows timing out at 2 min. Per-call `timeout`
55
+ // still overrides, capped at MAX_TIMEOUT_MS.
56
+ const DEFAULT_TIMEOUT_MS = 600_000;
57
+ const MAX_TIMEOUT_MS = 1_800_000;
58
+ const IDLE_TIMEOUT_MS = 5 * 60_000;
59
+ const MAX_SESSIONS = 10;
60
+ const STDERR_DRAIN_MS = 25;
61
+ const STDERR_DRAIN_MAX_MS = 250;
62
+ const STDERR_QUIESCENT_MS = 25;
63
+ // SHELL_OUTPUT_MAX_CHARS — output preview cap, matches the `bash` tool.
64
+ // Duplicated here so bash-session stays decoupled from builtin.mjs private constants.
65
+ const SHELL_OUTPUT_MAX_CHARS = 30_000;
66
+ // STREAM_BUF_BYTE_CAP — hard byte cap per in-memory stream buffer. Past the
67
+ // cap data is dropped and a truncation marker injected so a runaway command
68
+ // (e.g. `cat /dev/urandom`) can't OOM the orchestrator.
69
+ const STREAM_BUF_BYTE_CAP = 4 * 1024 * 1024; // 4 MB per stream
70
+ const STREAM_TRUNC_MARKER = '\n... [TRUNCATED — stream cap reached] ...\n';
71
+ // Output truncation runtime envelope: 400 lines / 30 KB total;
72
+ // head=80 + tail=80 lines preserved on middle-truncation.
73
+ const SMART_BASH_MAX_LINES = 400;
74
+ const SMART_BASH_MAX_BYTES = 30 * 1024;
75
+ const SMART_BASH_HEAD_LINES = 80;
76
+ const SMART_BASH_TAIL_LINES = 80;
77
+
78
+ // Marker prefix for per-command sentinels. A random suffix is added on each
79
+ // command so user output that happens to contain the static prefix does not
80
+ // terminate the command early.
81
+ const MARKER_PREFIX = '__MIXDOG_END__';
82
+
83
+ // --- ANSI strip (self-contained; mirrors builtin.mjs's implementation) ---
84
+ const _ANSI_REGEX = /\u001B(?:\[[0-?]*[ -/]*[@-~]|\][\s\S]*?(?:\u0007|\u001B\\|\u009C))/g;
85
+ const _stripAnsi = typeof nodeUtil.stripVTControlCharacters === 'function'
86
+ ? (s) => nodeUtil.stripVTControlCharacters(s)
87
+ : (s) => s.replace(_ANSI_REGEX, () => '');
88
+ function stripAnsi(s) {
89
+ if (typeof s !== 'string' || s.length === 0) return s;
90
+ return _stripAnsi(s);
91
+ }
92
+
93
+ function escapeRegex(s) {
94
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
95
+ }
96
+
97
+ // --- Smart middle-truncate (shared with bash tool) ---
98
+ function smartMiddleTruncate(content) {
99
+ const s = typeof content === 'string' ? content : String(content ?? '');
100
+ if (s.length <= SMART_BASH_MAX_BYTES) {
101
+ const fastLines = s.split('\n');
102
+ if (fastLines.length <= SMART_BASH_MAX_LINES) return s;
103
+ const head = fastLines.slice(0, SMART_BASH_HEAD_LINES).join('\n');
104
+ const tail = fastLines.slice(-SMART_BASH_TAIL_LINES).join('\n');
105
+ const middle = fastLines.length - SMART_BASH_HEAD_LINES - SMART_BASH_TAIL_LINES;
106
+ return `${head}\n\n... [TRUNCATED — ${middle} lines middle elided; total ${fastLines.length} lines. Rerun with tighter filters for more] ...\n\n${tail}`;
107
+ }
108
+ const lines = s.split('\n');
109
+ if (lines.length <= SMART_BASH_MAX_LINES) {
110
+ const head = s.slice(0, SMART_BASH_MAX_BYTES);
111
+ return `${head}\n\n... [TRUNCATED — output exceeded ${Math.round(SMART_BASH_MAX_BYTES / 1024)} KB on a single line] ...`;
112
+ }
113
+ const head = lines.slice(0, SMART_BASH_HEAD_LINES).join('\n');
114
+ const tail = lines.slice(-SMART_BASH_TAIL_LINES).join('\n');
115
+ const middle = lines.length - SMART_BASH_HEAD_LINES - SMART_BASH_TAIL_LINES;
116
+ const totalKb = Math.round(s.length / 1024);
117
+ return `${head}\n\n... [TRUNCATED — ${middle} lines middle elided; total ${lines.length} lines / ${totalKb} KB. Rerun with tighter filters for more] ...\n\n${tail}`;
118
+ }
119
+
120
+ function _prependDestructiveWarning(command, text) {
121
+ const warnings = getDedupedDestructiveWarnings(command);
122
+ if (!warnings.length) return text;
123
+ return `${warnings.map((w) => `⚠️ ${w}`).join('\n')}\n${text}`;
124
+ }
125
+
126
+ // Blocked command check delegated to shell-policy.mjs (shared with
127
+ // builtin.mjs). See that module for the full pattern set and rationale.
128
+ // Locate a usable bash binary on POSIX. Windows intentionally does not
129
+ // support persistent shell sessions; one-shot commands use PowerShell
130
+ // through builtin/bash-tool.mjs. We deliberately pin bash (not sh) since
131
+ // the feature set depended on by the sentinel echo and `$?` is bash-shaped.
132
+ function resolveBash() {
133
+ if (process.platform === 'win32') {
134
+ throw new Error('persistent shell sessions are not supported on Windows; use one-shot PowerShell commands');
135
+ }
136
+ if (existsSync('/bin/bash')) return '/bin/bash';
137
+ if (existsSync('/usr/bin/bash')) return '/usr/bin/bash';
138
+ return '/bin/sh'; // fallback; `$?` + echo still work
139
+ }
140
+
141
+ // --- Session pool ---
142
+ // Map<id, { proc, lastUsed, stdoutBuf, stderrBuf, busy }>
143
+ const _sessions = new Map();
144
+ let _reaperTimer = null;
145
+ // R17 parent-exit hook installed exactly once at first _spawnSession. Without
146
+ // it, persistent bash shells orphan on server-main death (the async
147
+ // 'process-exit' path at the bottom of this module never gets to run its
148
+ // awaits when the host dies abruptly). Sync iteration of _sessions + direct
149
+ // _killProcessTree (sync taskkill on win, sync process.kill on posix).
150
+ let _parentExitInstalled = false;
151
+ function _installParentExitHook() {
152
+ if (_parentExitInstalled) return;
153
+ _parentExitInstalled = true;
154
+ const sweep = () => {
155
+ for (const [, s] of _sessions) {
156
+ try { _killProcessTree(s.proc); } catch { /* ignore */ }
157
+ }
158
+ };
159
+ try { process.on('exit', sweep); } catch { /* ignore */ }
160
+ try { process.on('SIGTERM', sweep); } catch { /* ignore */ }
161
+ try { process.on('SIGINT', sweep); } catch { /* ignore */ }
162
+ }
163
+
164
+ function _startReaper() {
165
+ if (_reaperTimer) return;
166
+ _reaperTimer = setInterval(() => {
167
+ const now = Date.now();
168
+ for (const [id, s] of _sessions) {
169
+ if (!s.busy && now - s.lastUsed > IDLE_TIMEOUT_MS) {
170
+ _killSession(id, 'idle-timeout');
171
+ }
172
+ }
173
+ if (_sessions.size === 0) {
174
+ clearInterval(_reaperTimer);
175
+ _reaperTimer = null;
176
+ }
177
+ }, 30_000);
178
+ // Don't keep the event loop alive just for the reaper.
179
+ if (typeof _reaperTimer.unref === 'function') _reaperTimer.unref();
180
+ }
181
+
182
+ // Kill a spawned shell along with any child processes it forked. Posix path
183
+ // signals the process group (pgid == pid because we spawn with detached:true),
184
+ // so `sleep 1000 &` or a node server started inside the session is reaped
185
+ // instead of being left orphaned holding pipes open. Windows uses taskkill
186
+ // /T /F to walk the process tree. SIGTERM is sent first so well-behaved
187
+ // children can shut down cleanly; a SIGKILL escalation timer (3 s) forces the
188
+ // issue if they don't. Safe to call multiple times — all errors swallowed.
189
+ function _killProcessTree(proc) {
190
+ // proc.killed flips to true the moment a signal is *sent*, NOT when the
191
+ // child actually exits — escalation off `proc.killed` was a no-op. We
192
+ // instead track the real exit/close state via a flag and only escalate
193
+ // when neither has fired by the timer deadline.
194
+ if (!proc) return;
195
+ const pid = proc.pid;
196
+ if (!pid) return;
197
+ let exited = false;
198
+ const onDone = () => { exited = true; };
199
+ try { proc.once('exit', onDone); } catch {}
200
+ try { proc.once('close', onDone); } catch {}
201
+ // Already dead from a prior call? Skip the SIGTERM but still let the
202
+ // listener wiring above clean up if a future exit/close arrives.
203
+ if (proc.exitCode !== null && proc.exitCode !== undefined) {
204
+ exited = true;
205
+ } else if (proc.signalCode) {
206
+ exited = true;
207
+ }
208
+ try {
209
+ if (process.platform === 'win32') {
210
+ // /T walks the tree, /F forces — no graceful SIGTERM on win.
211
+ spawn('taskkill', ['/pid', String(pid), '/t', '/f'], { windowsHide: true, stdio: 'ignore' });
212
+ } else if (!exited) {
213
+ try { process.kill(-pid, 'SIGTERM'); }
214
+ catch { try { proc.kill('SIGTERM'); } catch { /* ignore */ } }
215
+ }
216
+ } catch { /* ignore */ }
217
+ // Escalate to SIGKILL if the child hasn't actually exited by the
218
+ // deadline. Windows taskkill /F is already forceful, so only posix
219
+ // needs the escalation timer.
220
+ if (process.platform !== 'win32') {
221
+ const esc = setTimeout(() => {
222
+ if (exited) return;
223
+ try { process.kill(-pid, 'SIGKILL'); }
224
+ catch { try { proc.kill('SIGKILL'); } catch { /* ignore */ } }
225
+ }, 3000);
226
+ if (typeof esc.unref === 'function') esc.unref();
227
+ }
228
+ }
229
+
230
+ async function _killSession(id, _reason) {
231
+ const s = _sessions.get(id);
232
+ if (!s) return;
233
+ _sessions.delete(id);
234
+ const exited = new Promise((resolve) => {
235
+ s.proc.once('exit', resolve);
236
+ s.proc.once('close', resolve);
237
+ });
238
+ try { s.proc.stdin?.end(); } catch { /* ignore */ }
239
+ _killProcessTree(s.proc);
240
+ await Promise.race([exited, new Promise((r) => setTimeout(r, 3000))]);
241
+ }
242
+
243
+ function shellQuoteSingle(s) {
244
+ return `'${String(s).replace(/'/g, `'\"'\"'`)}'`;
245
+ }
246
+
247
+ function _hostPathToShellPath(p) {
248
+ let s = String(p || '').replace(/\\/g, '/');
249
+ return s;
250
+ }
251
+
252
+ function _shellPwdToHostPath(p) {
253
+ return String(p || '').trim();
254
+ }
255
+
256
+ function _cwdKey(p) {
257
+ let s = _hostPathToShellPath(p).replace(/\/+$/, '');
258
+ if (process.platform === 'win32') s = s.toLowerCase();
259
+ return s || '/';
260
+ }
261
+
262
+ async function _ensureSessionCwd(entry, targetCwd, timeoutMs) {
263
+ if (!targetCwd || _cwdKey(entry.cwd) === _cwdKey(targetCwd)) return null;
264
+ const cdTarget = _hostPathToShellPath(targetCwd);
265
+ const result = await _runCommand(entry, `cd ${shellQuoteSingle(cdTarget)}`, Math.min(timeoutMs, 5000));
266
+ if (result?.exit_code !== 0) {
267
+ const stderr = stripAnsi(result?.stderr || '').trim();
268
+ return `Error: failed to set persistent cwd: ${targetCwd}${stderr ? `\n\n[stderr]\n${stderr}` : ''}`;
269
+ }
270
+ entry.cwd = targetCwd;
271
+ return null;
272
+ }
273
+
274
+ function _evictOldestIfFull() {
275
+ if (_sessions.size < MAX_SESSIONS) return;
276
+ // Prefer an idle session. If all are busy we can't evict safely; throw.
277
+ let oldestId = null;
278
+ let oldestTs = Infinity;
279
+ for (const [id, s] of _sessions) {
280
+ if (s.busy) continue;
281
+ if (s.lastUsed < oldestTs) {
282
+ oldestTs = s.lastUsed;
283
+ oldestId = id;
284
+ }
285
+ }
286
+ if (oldestId) {
287
+ _killSession(oldestId, 'pool-full');
288
+ return;
289
+ }
290
+ throw new Error(`bash_session pool full (${MAX_SESSIONS} concurrent sessions, all busy)`);
291
+ }
292
+
293
+ // Build the env handed to the child bash. This path is POSIX-only; Windows
294
+ // one-shot commands use PowerShell and persistent shell sessions are disabled.
295
+ function buildBashEnv() {
296
+ const env = { ...process.env, LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8' };
297
+ // R5 secret scrub — strip provider/cloud tokens before handing env to
298
+ // the persistent shell. The stateless `bash` tool applies this same
299
+ // sweep in builtin/bash-tool.mjs; the persistent shell's env is
300
+ // constructed here and returns before that site runs, so it must be
301
+ // done independently. Shared with shell-jobs and shell-snapshot via
302
+ // env-scrub.mjs so the prefix/suffix lists never drift.
303
+ scrubProviderSecrets(env);
304
+ // R11 loader/execution scrub (NODE_OPTIONS, LD_PRELOAD, DYLD_*, …).
305
+ scrubLoaderVars(env);
306
+ return env;
307
+ }
308
+
309
+ function _spawnSession(id, initialCwd = process.cwd()) {
310
+ _installParentExitHook();
311
+ _evictOldestIfFull();
312
+ const shell = resolveBash();
313
+ const proc = spawn(shell, [], {
314
+ stdio: ['pipe', 'pipe', 'pipe'],
315
+ env: buildBashEnv(),
316
+ cwd: initialCwd,
317
+ windowsHide: true,
318
+ // detached:true on posix gives the child its own process group so
319
+ // _killProcessTree can signal the whole group (catches `sleep 1000 &`
320
+ // and similar backgrounded children). Skipped on win32 where
321
+ // detached has different semantics (no console attached, used for
322
+ // daemonization — unwanted here).
323
+ detached: process.platform !== 'win32',
324
+ });
325
+ proc.stdout.setEncoding('utf-8');
326
+ proc.stderr.setEncoding('utf-8');
327
+ const entry = {
328
+ proc,
329
+ lastUsed: Date.now(),
330
+ cwd: initialCwd,
331
+ stdoutBuf: '',
332
+ stderrBuf: '',
333
+ busy: false,
334
+ dead: false,
335
+ exitInfo: null,
336
+ };
337
+ // Hard-capped concat: past STREAM_BUF_BYTE_CAP we drop further
338
+ // chunks and stamp a truncation marker once. Without this a runaway
339
+ // command (e.g. `cat /dev/urandom`) blocks until OOM long before any
340
+ // smartMiddleTruncate downstream gets a chance to trim.
341
+ entry.stdoutCapped = false;
342
+ entry.stderrCapped = false;
343
+ proc.stdout.on('data', (chunk) => {
344
+ if (entry.stdoutCapped) return;
345
+ entry.stdoutBuf += chunk;
346
+ if (entry.stdoutBuf.length >= STREAM_BUF_BYTE_CAP) {
347
+ entry.stdoutBuf = entry.stdoutBuf.slice(0, STREAM_BUF_BYTE_CAP) + STREAM_TRUNC_MARKER;
348
+ entry.stdoutCapped = true;
349
+ }
350
+ });
351
+ proc.stderr.on('data', (chunk) => {
352
+ if (entry.stderrCapped) return;
353
+ entry.stderrBuf += chunk;
354
+ if (entry.stderrBuf.length >= STREAM_BUF_BYTE_CAP) {
355
+ entry.stderrBuf = entry.stderrBuf.slice(0, STREAM_BUF_BYTE_CAP) + STREAM_TRUNC_MARKER;
356
+ entry.stderrCapped = true;
357
+ }
358
+ });
359
+ proc.on('error', (err) => {
360
+ entry.dead = true;
361
+ entry.exitInfo = { error: err?.message || String(err) };
362
+ });
363
+ proc.on('exit', (code, signal) => {
364
+ entry.dead = true;
365
+ entry.exitInfo = { code, signal };
366
+ _sessions.delete(id);
367
+ });
368
+ _sessions.set(id, entry);
369
+ _startReaper();
370
+ return entry;
371
+ }
372
+
373
+ function _getOrCreate(sessionId, initialCwd = process.cwd(), opts = {}) {
374
+ const explicit = typeof sessionId === 'string' && sessionId.length > 0;
375
+ const id = explicit ? sessionId : `sess_${randomUUID()}`;
376
+ let entry = _sessions.get(id);
377
+ if (entry && entry.dead) {
378
+ _sessions.delete(id);
379
+ entry = null;
380
+ }
381
+ if (!entry) {
382
+ if (explicit && opts.create !== true) {
383
+ return { error: `Error: unknown session_id "${id}" (pass create:true to start a new persistent session)` };
384
+ }
385
+ entry = _spawnSession(id, initialCwd);
386
+ }
387
+ return { id, entry };
388
+ }
389
+
390
+ // Core command-run: frame with sentinel, wait for marker on stdout, flush
391
+ // stderr with a small drain window, return { stdout, stderr, exit_code }.
392
+ function _runCommand(entry, command, timeoutMs, abortSignal = null) {
393
+ return new Promise((resolve, reject) => {
394
+ entry.busy = true;
395
+ // Reset buffers for this command. Anything left from a prior run is
396
+ // unexpected (we only return after the marker), but be defensive.
397
+ entry.stdoutBuf = '';
398
+ entry.stderrBuf = '';
399
+
400
+ let finished = false;
401
+ let timeoutHandle = null;
402
+ let abortHandler = null;
403
+ const marker = `${MARKER_PREFIX}:${randomUUID()}`;
404
+ const markerRe = new RegExp(`^${escapeRegex(marker)}:(-?\\d+)\\s*$`, 'm');
405
+
406
+ const onExit = () => {
407
+ if (finished) return;
408
+ fail(new Error('bash_session: shell exited before command completed'));
409
+ };
410
+
411
+ const cleanup = () => {
412
+ finished = true;
413
+ entry.busy = false;
414
+ entry.lastUsed = Date.now();
415
+ if (timeoutHandle) clearTimeout(timeoutHandle);
416
+ if (abortSignal && abortHandler) {
417
+ try { abortSignal.removeEventListener('abort', abortHandler); } catch {}
418
+ }
419
+ entry.proc.stdout.removeListener('data', onStdout);
420
+ entry.proc.removeListener('exit', onExit);
421
+ };
422
+
423
+ const settle = (result) => {
424
+ if (finished) return;
425
+ cleanup();
426
+ resolve(result);
427
+ };
428
+
429
+ const fail = (err) => {
430
+ if (finished) return;
431
+ cleanup();
432
+ reject(err);
433
+ };
434
+
435
+ const onStdout = () => {
436
+ const m = markerRe.exec(entry.stdoutBuf);
437
+ if (!m) return;
438
+ const exitCode = Number(m[1]);
439
+ // Everything before the marker line is the real stdout.
440
+ const before = entry.stdoutBuf.slice(0, m.index);
441
+ // Drain pending stderr writes adaptively. The fixed 25 ms
442
+ // window dropped late stderr from forked children that
443
+ // flushed slightly after the parent shell exited. Loop
444
+ // instead: poll the stderr buffer length and finish only
445
+ // once it's been quiescent for STDERR_QUIESCENT_MS, the
446
+ // child closed stderr, or we hit the absolute ceiling.
447
+ const drainStart = Date.now();
448
+ let lastLen = entry.stderrBuf.length;
449
+ let lastChange = drainStart;
450
+ let stderrClosed = false;
451
+ const onStderrEnd = () => { stderrClosed = true; };
452
+ try { entry.proc.stderr.once('end', onStderrEnd); } catch {}
453
+ const finish = () => {
454
+ try { entry.proc.stderr.removeListener('end', onStderrEnd); } catch {}
455
+ const stderr = entry.stderrBuf;
456
+ entry.stdoutBuf = '';
457
+ entry.stderrBuf = '';
458
+ settle({ stdout: before, stderr, exit_code: exitCode });
459
+ };
460
+ const tick = () => {
461
+ if (finished) { try { entry.proc.stderr.removeListener('end', onStderrEnd); } catch {} return; }
462
+ const now = Date.now();
463
+ const curLen = entry.stderrBuf.length;
464
+ if (curLen !== lastLen) {
465
+ lastLen = curLen;
466
+ lastChange = now;
467
+ }
468
+ if (stderrClosed) return finish();
469
+ if (now - drainStart >= STDERR_DRAIN_MAX_MS) return finish();
470
+ if (now - lastChange >= STDERR_QUIESCENT_MS) return finish();
471
+ setTimeout(tick, 10);
472
+ };
473
+ setTimeout(tick, STDERR_DRAIN_MS);
474
+ };
475
+
476
+ // Caller-driven abort (ESC / new prompt / session close). Kill the
477
+ // shell immediately and resolve with an aborted marker so the agent
478
+ // sees the cancellation rather than waiting for timeoutMs.
479
+ if (abortSignal) {
480
+ const fireAbort = () => {
481
+ if (finished) return;
482
+ const partialOut = entry.stdoutBuf || '';
483
+ const partialErr = entry.stderrBuf || '';
484
+ entry.stdoutBuf = '';
485
+ entry.stderrBuf = '';
486
+ _killProcessTree(entry.proc);
487
+ // Mark dead and remove from pool immediately so the next call
488
+ // doesn't pick up a killed shell entry.
489
+ entry.dead = true;
490
+ for (const [sid, s] of _sessions) { if (s === entry) { _sessions.delete(sid); break; } }
491
+ cleanup();
492
+ resolve({
493
+ stdout: partialOut,
494
+ stderr: partialErr,
495
+ exit_code: null,
496
+ signal: 'SIGTERM',
497
+ aborted: true,
498
+ });
499
+ };
500
+ if (abortSignal.aborted) { fireAbort(); return; }
501
+ abortHandler = fireAbort;
502
+ abortSignal.addEventListener('abort', abortHandler, { once: true });
503
+ }
504
+
505
+ entry.proc.stdout.on('data', onStdout);
506
+ // Check the buffer in case the marker already arrived (tiny commands).
507
+ onStdout();
508
+
509
+ entry.proc.on('exit', onExit);
510
+
511
+ timeoutHandle = setTimeout(() => {
512
+ // Timeout: surface what we have but don't leave the shell in a
513
+ // half-run state. Killing the process is the only reliable way
514
+ // to interrupt a stuck command; the caller can mint a new session.
515
+ const partialOut = entry.stdoutBuf;
516
+ const partialErr = entry.stderrBuf;
517
+ entry.stdoutBuf = '';
518
+ entry.stderrBuf = '';
519
+ _killProcessTree(entry.proc);
520
+ cleanup();
521
+ // Return a structured result (not a reject) so the caller
522
+ // renders a proper exit/stderr block instead of a bare Error.
523
+ resolve({
524
+ stdout: partialOut,
525
+ stderr: partialErr,
526
+ exit_code: null,
527
+ signal: 'SIGTERM',
528
+ timed_out: true,
529
+ timeout_ms: timeoutMs,
530
+ });
531
+ }, timeoutMs);
532
+
533
+ // Write the command + sentinel. Newline before `echo` in case the
534
+ // command didn't end with one. `$?` captures the final pipeline's
535
+ // exit status as of bash semantics.
536
+ const _preEncodePolicy = checkExecPolicyMessage(command);
537
+ if (_preEncodePolicy) {
538
+ fail(new Error(_preEncodePolicy.replace(/^Error: /, '')));
539
+ return;
540
+ }
541
+ const encoded = _maybeEncodePowerShellCommand(command);
542
+ const payload = `${encoded}\necho "${marker}:$?"\n`;
543
+ try {
544
+ entry.proc.stdin.write(payload, 'utf-8');
545
+ } catch (err) {
546
+ fail(err);
547
+ }
548
+ });
549
+ }
550
+
551
+ async function _syncSessionCwd(entry, timeoutMs) {
552
+ try {
553
+ const result = await _runCommand(entry, 'pwd', Math.min(timeoutMs, 2000));
554
+ if (result?.exit_code !== 0) return;
555
+ const stdout = stripAnsi(result.stdout || '').trim();
556
+ if (!stdout) return;
557
+ const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
558
+ if (lines.length > 0) entry.cwd = _shellPwdToHostPath(lines[lines.length - 1]);
559
+ } catch (err) {
560
+ entry.syncError = `[bash-session] persistent-session cwd sync failed: ${err.message}`;
561
+ }
562
+ }
563
+
564
+ async function bash_session(args, cwd = process.cwd(), opts = {}) {
565
+ if (process.platform === 'win32') {
566
+ return 'Error: persistent shell sessions are not supported on Windows; use one-shot PowerShell commands without persistent/session_id.';
567
+ }
568
+ const abortSignal = opts && opts.abortSignal ? opts.abortSignal : null;
569
+ let command = typeof args?.command === 'string' ? args.command : '';
570
+ const close = args?.close === true;
571
+ const implicitSessionId = args?.persistent === true && typeof opts?.sessionId === 'string'
572
+ ? `__default__${opts.sessionId}`
573
+ : '';
574
+ const requestedSessionId = typeof args?.session_id === 'string' ? args.session_id : implicitSessionId;
575
+ if (!command && close) {
576
+ if (!requestedSessionId) return 'Error: command is required';
577
+ const existing = _sessions.get(requestedSessionId);
578
+ if (existing?.busy) return `Error: session "${requestedSessionId}" is busy executing a prior command`;
579
+ if (existing) await _killSession(requestedSessionId, 'close-requested');
580
+ return `[session: ${requestedSessionId}]\n[closed]\n\n${existing ? '(no output)' : '(no active session)'}`;
581
+ }
582
+ if (!command) return 'Error: command is required';
583
+ const wmicRewrite = maybeRewriteWmicProcessCommand(command);
584
+ if (wmicRewrite?.error) return `Error: ${wmicRewrite.error}`;
585
+ if (wmicRewrite?.command) command = wmicRewrite.command;
586
+ // R5-③: match the stateless one-shot path's full sweep so callers that
587
+ // reach bash_session directly (via session_id without going through
588
+ // executeBashTool) still get stripQuotedAndHeredoc + extractShellCInner
589
+ // + unquote-span coverage. Persistent:true callers funnel through
590
+ // executeBashTool which now pre-sweeps, but bash_session is also reached
591
+ // by close:true session reuse and by direct tool dispatch.
592
+ const _bsPolicy = checkExecPolicyMessage(command);
593
+ if (_bsPolicy) return _bsPolicy;
594
+ const explicitCwd = typeof args?.cwd === 'string' && args.cwd.trim().length > 0;
595
+ const requestedCwd = cwd || process.cwd();
596
+ const baseCwd = (() => {
597
+ if (explicitCwd) return requestedCwd;
598
+ if (requestedSessionId) {
599
+ const existing = _sessions.get(requestedSessionId);
600
+ if (existing?.cwd) return existing.cwd;
601
+ }
602
+ return requestedCwd;
603
+ })();
604
+ const largeProbe = await preflightShellLargeFileProbe(command, baseCwd);
605
+ if (largeProbe) return `Error: ${largeProbe.message}`;
606
+ const rawTimeout = typeof args?.timeout === 'number' ? args.timeout : DEFAULT_TIMEOUT_MS;
607
+ // Accept seconds OR milliseconds for ergonomics: values ≤ 600 are
608
+ // treated as seconds (matches the spec's "max 600s"); larger values
609
+ // are taken as ms. Cap either way.
610
+ const timeoutMs = rawTimeout <= 600 ? rawTimeout * 1000 : rawTimeout;
611
+ const effectiveTimeout = Math.min(Math.max(timeoutMs, 1000), wmicRewrite?.timeoutMs || MAX_TIMEOUT_MS);
612
+ const resolved = _getOrCreate(requestedSessionId || args?.session_id, baseCwd, { create: args?.create === true });
613
+ if (resolved.error) return resolved.error;
614
+ const { id, entry } = resolved;
615
+ if (entry.syncError) {
616
+ const msg = entry.syncError;
617
+ delete entry.syncError;
618
+ throw new Error(msg);
619
+ }
620
+ if (entry.busy) {
621
+ return `Error: session "${id}" is busy executing a prior command`;
622
+ }
623
+ if (explicitCwd) {
624
+ const cwdErr = await _ensureSessionCwd(entry, requestedCwd, effectiveTimeout);
625
+ if (cwdErr) return cwdErr;
626
+ }
627
+
628
+ let shellEffects;
629
+ try {
630
+ shellEffects = await analyzeShellCommandEffects(command, entry.cwd || baseCwd);
631
+ } catch (err) {
632
+ return `Error: ${err?.message || String(err)}`;
633
+ }
634
+
635
+ const _bsScope = opts?.readStateScope ?? opts?.sessionId ?? null;
636
+ const _bsPreDrift = _captureTrackedMtimes(_bsScope);
637
+ let result;
638
+ try {
639
+ result = await _runCommand(entry, command, effectiveTimeout, abortSignal);
640
+ } catch (err) {
641
+ return `Error: ${err?.message || String(err)}`;
642
+ }
643
+ if (result.exit_code === 0 && shellEffects.finalCwd) {
644
+ entry.cwd = shellEffects.finalCwd;
645
+ }
646
+ // Skip cwd sync on abort: the shell was already killed, so issuing
647
+ // another `pwd` would either hang on a dead pipe or spawn a fresh
648
+ // session against caller intent. Mark dead so the next call mints a
649
+ // new shell rather than reusing this one. Same logic for timeouts.
650
+ if (result.aborted) {
651
+ entry.dead = true;
652
+ } else if (!close && !result.timed_out) {
653
+ await _syncSessionCwd(entry, effectiveTimeout);
654
+ }
655
+ if (shellEffects.mutationMode === 'paths') {
656
+ invalidateBuiltinResultCache(shellEffects.paths);
657
+ markCodeGraphDirtyPaths(shellEffects.paths);
658
+ } else if (shellEffects.mutationMode === 'global') {
659
+ invalidateBuiltinResultCache();
660
+ drainCodeGraphCache();
661
+ }
662
+ const _bsDriftNote = _trackedDriftNoteAfter(_bsScope, _bsPreDrift);
663
+
664
+ if (close) {
665
+ await _killSession(id, 'close-requested');
666
+ }
667
+
668
+ const stdoutClean = stripAnsi(result.stdout || '');
669
+ const stderrClean = stripAnsi(result.stderr || '');
670
+ const stdoutT = smartMiddleTruncate(stdoutClean);
671
+ const stderrT = stderrClean ? smartMiddleTruncate(stderrClean) : '';
672
+
673
+ // Structured header so the agent can parse session_id + exit_code out
674
+ // of the text response without bespoke JSON. Keeps parity with the
675
+ // `bash` tool's free-form `[exit code: N]` marker but additive.
676
+ const headerLines = [`[session: ${id}]`];
677
+ if (wmicRewrite?.note) headerLines.push(wmicRewrite.note);
678
+ if (result.aborted) {
679
+ headerLines.push(`[aborted: caller cancelled — session killed]`);
680
+ } else if (result.timed_out) {
681
+ headerLines.push(`[timeout: ${result.timeout_ms} ms — session killed]`);
682
+ } else if (result.exit_code !== 0 && result.exit_code !== null) {
683
+ headerLines.push(`[exit code: ${result.exit_code}]`);
684
+ }
685
+ if (close) headerLines.push(`[closed]`);
686
+ const header = headerLines.join('\n');
687
+
688
+ const body = stdoutT || (stderrT ? '' : '(no output)');
689
+ const stderrBlock = stderrT ? `\n\n[stderr]\n${stderrT}` : '';
690
+ return _prependDestructiveWarning(command, `${header}\n\n${body}${stderrBlock}${_bsDriftNote}`);
691
+ }
692
+
693
+ // BASH_SESSION_TOOL_DEFS removed in 0.1.126: the `bash` tool's
694
+ // `persistent:true` option absorbed every use case; the dedicated
695
+ // `bash_session` schema only added prompt bytes and triggered LLM
696
+ // hallucinations of the legacy name. Implementation (executeBashSessionTool,
697
+ // closeBashSession) stays — `bash` with persistent:true routes here.
698
+ export async function executeBashSessionTool(name, args, _cwd, opts = {}) {
699
+ switch (name) {
700
+ case 'bash_session':
701
+ return bash_session(args || {}, _cwd || process.cwd(), opts);
702
+ default:
703
+ throw new Error(`Unknown bash-session tool: ${name}`);
704
+ }
705
+ }
706
+
707
+ export function closeBashSession(sessionId, reason = 'external-close') {
708
+ if (!sessionId || !_sessions.has(sessionId)) return false;
709
+ _killSession(sessionId, reason);
710
+ return true;
711
+ }
712
+
713
+ // Best-effort cleanup on process exit so orphan bash children don't linger
714
+ // when the plugin host shuts down. drain-registry runs this on signal exit
715
+ // paths; the bare 'exit' hook stays as an idempotent backup.
716
+ export function drainBashSessions() {
717
+ for (const id of [..._sessions.keys()]) _killSession(id, 'process-exit');
718
+ }
719
+ if (typeof process?.on === 'function') {
720
+ process.on('exit', drainBashSessions);
721
+ }