@a1hvdy/cc-openclaw 0.3.2

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 (491) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +207 -0
  3. package/configs/.gitkeep +0 -0
  4. package/configs/council-reviewer-prompt.md +82 -0
  5. package/configs/council-system-prompt.md +141 -0
  6. package/dist/scripts/bench/ab-harness.d.ts +58 -0
  7. package/dist/scripts/bench/ab-harness.d.ts.map +1 -0
  8. package/dist/scripts/bench/ab-harness.js +78 -0
  9. package/dist/scripts/bench/ab-harness.js.map +1 -0
  10. package/dist/src/channels/adapter.d.ts +103 -0
  11. package/dist/src/channels/adapter.d.ts.map +1 -0
  12. package/dist/src/channels/adapter.js +38 -0
  13. package/dist/src/channels/adapter.js.map +1 -0
  14. package/dist/src/channels/telegram/completion-summary.d.ts +22 -0
  15. package/dist/src/channels/telegram/completion-summary.d.ts.map +1 -0
  16. package/dist/src/channels/telegram/completion-summary.js +186 -0
  17. package/dist/src/channels/telegram/completion-summary.js.map +1 -0
  18. package/dist/src/channels/telegram/error-renderer.d.ts +30 -0
  19. package/dist/src/channels/telegram/error-renderer.d.ts.map +1 -0
  20. package/dist/src/channels/telegram/error-renderer.js +133 -0
  21. package/dist/src/channels/telegram/error-renderer.js.map +1 -0
  22. package/dist/src/channels/telegram/event-reducer.d.ts +34 -0
  23. package/dist/src/channels/telegram/event-reducer.d.ts.map +1 -0
  24. package/dist/src/channels/telegram/event-reducer.js +579 -0
  25. package/dist/src/channels/telegram/event-reducer.js.map +1 -0
  26. package/dist/src/channels/telegram/index.d.ts +14 -0
  27. package/dist/src/channels/telegram/index.d.ts.map +1 -0
  28. package/dist/src/channels/telegram/index.js +14 -0
  29. package/dist/src/channels/telegram/index.js.map +1 -0
  30. package/dist/src/channels/telegram/injector.d.ts +54 -0
  31. package/dist/src/channels/telegram/injector.d.ts.map +1 -0
  32. package/dist/src/channels/telegram/injector.js +200 -0
  33. package/dist/src/channels/telegram/injector.js.map +1 -0
  34. package/dist/src/channels/telegram/live-card.d.ts +230 -0
  35. package/dist/src/channels/telegram/live-card.d.ts.map +1 -0
  36. package/dist/src/channels/telegram/live-card.js +916 -0
  37. package/dist/src/channels/telegram/live-card.js.map +1 -0
  38. package/dist/src/channels/telegram/state-machine.d.ts +23 -0
  39. package/dist/src/channels/telegram/state-machine.d.ts.map +1 -0
  40. package/dist/src/channels/telegram/state-machine.js +72 -0
  41. package/dist/src/channels/telegram/state-machine.js.map +1 -0
  42. package/dist/src/channels/telegram/tool-tracker.d.ts +147 -0
  43. package/dist/src/channels/telegram/tool-tracker.d.ts.map +1 -0
  44. package/dist/src/channels/telegram/tool-tracker.js +520 -0
  45. package/dist/src/channels/telegram/tool-tracker.js.map +1 -0
  46. package/dist/src/circuit-breaker.d.ts +22 -0
  47. package/dist/src/circuit-breaker.d.ts.map +1 -0
  48. package/dist/src/circuit-breaker.js +47 -0
  49. package/dist/src/circuit-breaker.js.map +1 -0
  50. package/dist/src/command-router/cc-handler.d.ts +67 -0
  51. package/dist/src/command-router/cc-handler.d.ts.map +1 -0
  52. package/dist/src/command-router/cc-handler.js +980 -0
  53. package/dist/src/command-router/cc-handler.js.map +1 -0
  54. package/dist/src/command-router/index.d.ts +3 -0
  55. package/dist/src/command-router/index.d.ts.map +1 -0
  56. package/dist/src/command-router/index.js +2 -0
  57. package/dist/src/command-router/index.js.map +1 -0
  58. package/dist/src/constants.d.ts +132 -0
  59. package/dist/src/constants.d.ts.map +1 -0
  60. package/dist/src/constants.js +140 -0
  61. package/dist/src/constants.js.map +1 -0
  62. package/dist/src/council/consensus.d.ts +21 -0
  63. package/dist/src/council/consensus.d.ts.map +1 -0
  64. package/dist/src/council/consensus.js +52 -0
  65. package/dist/src/council/consensus.js.map +1 -0
  66. package/dist/src/council/council.d.ts +68 -0
  67. package/dist/src/council/council.d.ts.map +1 -0
  68. package/dist/src/council/council.js +914 -0
  69. package/dist/src/council/council.js.map +1 -0
  70. package/dist/src/council/index.d.ts +3 -0
  71. package/dist/src/council/index.d.ts.map +1 -0
  72. package/dist/src/council/index.js +3 -0
  73. package/dist/src/council/index.js.map +1 -0
  74. package/dist/src/engines/base-oneshot-session.d.ts +88 -0
  75. package/dist/src/engines/base-oneshot-session.d.ts.map +1 -0
  76. package/dist/src/engines/base-oneshot-session.js +228 -0
  77. package/dist/src/engines/base-oneshot-session.js.map +1 -0
  78. package/dist/src/engines/index.d.ts +7 -0
  79. package/dist/src/engines/index.d.ts.map +1 -0
  80. package/dist/src/engines/index.js +7 -0
  81. package/dist/src/engines/index.js.map +1 -0
  82. package/dist/src/engines/persistent-codex-session.d.ts +17 -0
  83. package/dist/src/engines/persistent-codex-session.d.ts.map +1 -0
  84. package/dist/src/engines/persistent-codex-session.js +106 -0
  85. package/dist/src/engines/persistent-codex-session.js.map +1 -0
  86. package/dist/src/engines/persistent-cursor-session.d.ts +22 -0
  87. package/dist/src/engines/persistent-cursor-session.d.ts.map +1 -0
  88. package/dist/src/engines/persistent-cursor-session.js +242 -0
  89. package/dist/src/engines/persistent-cursor-session.js.map +1 -0
  90. package/dist/src/engines/persistent-custom-session.d.ts +79 -0
  91. package/dist/src/engines/persistent-custom-session.d.ts.map +1 -0
  92. package/dist/src/engines/persistent-custom-session.js +939 -0
  93. package/dist/src/engines/persistent-custom-session.js.map +1 -0
  94. package/dist/src/engines/persistent-gemini-session.d.ts +22 -0
  95. package/dist/src/engines/persistent-gemini-session.d.ts.map +1 -0
  96. package/dist/src/engines/persistent-gemini-session.js +217 -0
  97. package/dist/src/engines/persistent-gemini-session.js.map +1 -0
  98. package/dist/src/engines/persistent-session.d.ts +77 -0
  99. package/dist/src/engines/persistent-session.d.ts.map +1 -0
  100. package/dist/src/engines/persistent-session.js +730 -0
  101. package/dist/src/engines/persistent-session.js.map +1 -0
  102. package/dist/src/health/handler.d.ts +40 -0
  103. package/dist/src/health/handler.d.ts.map +1 -0
  104. package/dist/src/health/handler.js +70 -0
  105. package/dist/src/health/handler.js.map +1 -0
  106. package/dist/src/health/index.d.ts +2 -0
  107. package/dist/src/health/index.d.ts.map +1 -0
  108. package/dist/src/health/index.js +2 -0
  109. package/dist/src/health/index.js.map +1 -0
  110. package/dist/src/index.d.ts +49 -0
  111. package/dist/src/index.d.ts.map +1 -0
  112. package/dist/src/index.js +84 -0
  113. package/dist/src/index.js.map +1 -0
  114. package/dist/src/lib/auto-recovery.d.ts +45 -0
  115. package/dist/src/lib/auto-recovery.d.ts.map +1 -0
  116. package/dist/src/lib/auto-recovery.js +217 -0
  117. package/dist/src/lib/auto-recovery.js.map +1 -0
  118. package/dist/src/lib/cache-parity.d.ts +39 -0
  119. package/dist/src/lib/cache-parity.d.ts.map +1 -0
  120. package/dist/src/lib/cache-parity.js +92 -0
  121. package/dist/src/lib/cache-parity.js.map +1 -0
  122. package/dist/src/lib/circuit-breaker.d.ts +22 -0
  123. package/dist/src/lib/circuit-breaker.d.ts.map +1 -0
  124. package/dist/src/lib/circuit-breaker.js +47 -0
  125. package/dist/src/lib/circuit-breaker.js.map +1 -0
  126. package/dist/src/lib/config.d.ts +74 -0
  127. package/dist/src/lib/config.d.ts.map +1 -0
  128. package/dist/src/lib/config.js +244 -0
  129. package/dist/src/lib/config.js.map +1 -0
  130. package/dist/src/lib/drift-detector.d.ts +47 -0
  131. package/dist/src/lib/drift-detector.d.ts.map +1 -0
  132. package/dist/src/lib/drift-detector.js +192 -0
  133. package/dist/src/lib/drift-detector.js.map +1 -0
  134. package/dist/src/lib/error-formatter.d.ts +78 -0
  135. package/dist/src/lib/error-formatter.d.ts.map +1 -0
  136. package/dist/src/lib/error-formatter.js +149 -0
  137. package/dist/src/lib/error-formatter.js.map +1 -0
  138. package/dist/src/lib/heartbeat-workaround.d.ts +45 -0
  139. package/dist/src/lib/heartbeat-workaround.d.ts.map +1 -0
  140. package/dist/src/lib/heartbeat-workaround.js +61 -0
  141. package/dist/src/lib/heartbeat-workaround.js.map +1 -0
  142. package/dist/src/lib/index.d.ts +8 -0
  143. package/dist/src/lib/index.d.ts.map +1 -0
  144. package/dist/src/lib/index.js +8 -0
  145. package/dist/src/lib/index.js.map +1 -0
  146. package/dist/src/lib/register-guard.d.ts +49 -0
  147. package/dist/src/lib/register-guard.d.ts.map +1 -0
  148. package/dist/src/lib/register-guard.js +73 -0
  149. package/dist/src/lib/register-guard.js.map +1 -0
  150. package/dist/src/lib/route-flag.d.ts +50 -0
  151. package/dist/src/lib/route-flag.d.ts.map +1 -0
  152. package/dist/src/lib/route-flag.js +52 -0
  153. package/dist/src/lib/route-flag.js.map +1 -0
  154. package/dist/src/lib/sysprompt-strip.d.ts +54 -0
  155. package/dist/src/lib/sysprompt-strip.d.ts.map +1 -0
  156. package/dist/src/lib/sysprompt-strip.js +75 -0
  157. package/dist/src/lib/sysprompt-strip.js.map +1 -0
  158. package/dist/src/lib/telemetry.d.ts +39 -0
  159. package/dist/src/lib/telemetry.d.ts.map +1 -0
  160. package/dist/src/lib/telemetry.js +73 -0
  161. package/dist/src/lib/telemetry.js.map +1 -0
  162. package/dist/src/lib/test-mode.d.ts +27 -0
  163. package/dist/src/lib/test-mode.d.ts.map +1 -0
  164. package/dist/src/lib/test-mode.js +38 -0
  165. package/dist/src/lib/test-mode.js.map +1 -0
  166. package/dist/src/lib/vendor-paths.d.ts +15 -0
  167. package/dist/src/lib/vendor-paths.d.ts.map +1 -0
  168. package/dist/src/lib/vendor-paths.js +32 -0
  169. package/dist/src/lib/vendor-paths.js.map +1 -0
  170. package/dist/src/logger.d.ts +17 -0
  171. package/dist/src/logger.d.ts.map +1 -0
  172. package/dist/src/logger.js +46 -0
  173. package/dist/src/logger.js.map +1 -0
  174. package/dist/src/mcp/bridge.d.ts +22 -0
  175. package/dist/src/mcp/bridge.d.ts.map +1 -0
  176. package/dist/src/mcp/bridge.js +78 -0
  177. package/dist/src/mcp/bridge.js.map +1 -0
  178. package/dist/src/mcp/index.d.ts +3 -0
  179. package/dist/src/mcp/index.d.ts.map +1 -0
  180. package/dist/src/mcp/index.js +2 -0
  181. package/dist/src/mcp/index.js.map +1 -0
  182. package/dist/src/models.d.ts +70 -0
  183. package/dist/src/models.d.ts.map +1 -0
  184. package/dist/src/models.js +289 -0
  185. package/dist/src/models.js.map +1 -0
  186. package/dist/src/openai-compat/cli-stream-parser.d.ts +135 -0
  187. package/dist/src/openai-compat/cli-stream-parser.d.ts.map +1 -0
  188. package/dist/src/openai-compat/cli-stream-parser.js +195 -0
  189. package/dist/src/openai-compat/cli-stream-parser.js.map +1 -0
  190. package/dist/src/openai-compat/index.d.ts +2 -0
  191. package/dist/src/openai-compat/index.d.ts.map +1 -0
  192. package/dist/src/openai-compat/index.js +2 -0
  193. package/dist/src/openai-compat/index.js.map +1 -0
  194. package/dist/src/openai-compat/openai-compat.d.ts +281 -0
  195. package/dist/src/openai-compat/openai-compat.d.ts.map +1 -0
  196. package/dist/src/openai-compat/openai-compat.js +939 -0
  197. package/dist/src/openai-compat/openai-compat.js.map +1 -0
  198. package/dist/src/openai-compat/skill-resolver.d.ts +36 -0
  199. package/dist/src/openai-compat/skill-resolver.d.ts.map +1 -0
  200. package/dist/src/openai-compat/skill-resolver.js +134 -0
  201. package/dist/src/openai-compat/skill-resolver.js.map +1 -0
  202. package/dist/src/openai-compat/sse-translator.d.ts +32 -0
  203. package/dist/src/openai-compat/sse-translator.d.ts.map +1 -0
  204. package/dist/src/openai-compat/sse-translator.js +155 -0
  205. package/dist/src/openai-compat/sse-translator.js.map +1 -0
  206. package/dist/src/proxy/anthropic-adapter.d.ts +137 -0
  207. package/dist/src/proxy/anthropic-adapter.d.ts.map +1 -0
  208. package/dist/src/proxy/anthropic-adapter.js +392 -0
  209. package/dist/src/proxy/anthropic-adapter.js.map +1 -0
  210. package/dist/src/proxy/handler.d.ts +40 -0
  211. package/dist/src/proxy/handler.d.ts.map +1 -0
  212. package/dist/src/proxy/handler.js +378 -0
  213. package/dist/src/proxy/handler.js.map +1 -0
  214. package/dist/src/proxy/index.d.ts +5 -0
  215. package/dist/src/proxy/index.d.ts.map +1 -0
  216. package/dist/src/proxy/index.js +5 -0
  217. package/dist/src/proxy/index.js.map +1 -0
  218. package/dist/src/proxy/schema-cleaner.d.ts +12 -0
  219. package/dist/src/proxy/schema-cleaner.d.ts.map +1 -0
  220. package/dist/src/proxy/schema-cleaner.js +34 -0
  221. package/dist/src/proxy/schema-cleaner.js.map +1 -0
  222. package/dist/src/proxy/thought-cache.d.ts +20 -0
  223. package/dist/src/proxy/thought-cache.d.ts.map +1 -0
  224. package/dist/src/proxy/thought-cache.js +53 -0
  225. package/dist/src/proxy/thought-cache.js.map +1 -0
  226. package/dist/src/session/embedded-server.d.ts +26 -0
  227. package/dist/src/session/embedded-server.d.ts.map +1 -0
  228. package/dist/src/session/embedded-server.js +367 -0
  229. package/dist/src/session/embedded-server.js.map +1 -0
  230. package/dist/src/session/inbox-manager.d.ts +39 -0
  231. package/dist/src/session/inbox-manager.d.ts.map +1 -0
  232. package/dist/src/session/inbox-manager.js +111 -0
  233. package/dist/src/session/inbox-manager.js.map +1 -0
  234. package/dist/src/session/index.d.ts +4 -0
  235. package/dist/src/session/index.d.ts.map +1 -0
  236. package/dist/src/session/index.js +4 -0
  237. package/dist/src/session/index.js.map +1 -0
  238. package/dist/src/session/session-manager.d.ts +212 -0
  239. package/dist/src/session/session-manager.d.ts.map +1 -0
  240. package/dist/src/session/session-manager.js +1351 -0
  241. package/dist/src/session/session-manager.js.map +1 -0
  242. package/dist/src/session-bootstrap/cwd-patch.d.ts +51 -0
  243. package/dist/src/session-bootstrap/cwd-patch.d.ts.map +1 -0
  244. package/dist/src/session-bootstrap/cwd-patch.js +955 -0
  245. package/dist/src/session-bootstrap/cwd-patch.js.map +1 -0
  246. package/dist/src/session-bootstrap/index.d.ts +4 -0
  247. package/dist/src/session-bootstrap/index.d.ts.map +1 -0
  248. package/dist/src/session-bootstrap/index.js +4 -0
  249. package/dist/src/session-bootstrap/index.js.map +1 -0
  250. package/dist/src/session-bootstrap/sysprompt-strip.d.ts +26 -0
  251. package/dist/src/session-bootstrap/sysprompt-strip.d.ts.map +1 -0
  252. package/dist/src/session-bootstrap/sysprompt-strip.js +57 -0
  253. package/dist/src/session-bootstrap/sysprompt-strip.js.map +1 -0
  254. package/dist/src/session-bootstrap/think-conflict-resolver.d.ts +33 -0
  255. package/dist/src/session-bootstrap/think-conflict-resolver.d.ts.map +1 -0
  256. package/dist/src/session-bootstrap/think-conflict-resolver.js +234 -0
  257. package/dist/src/session-bootstrap/think-conflict-resolver.js.map +1 -0
  258. package/dist/src/types.d.ts +489 -0
  259. package/dist/src/types.d.ts.map +1 -0
  260. package/dist/src/types.js +8 -0
  261. package/dist/src/types.js.map +1 -0
  262. package/dist/src/validation.d.ts +32 -0
  263. package/dist/src/validation.d.ts.map +1 -0
  264. package/dist/src/validation.js +104 -0
  265. package/dist/src/validation.js.map +1 -0
  266. package/dist/tests/_helpers/subprocess-mock.d.ts +35 -0
  267. package/dist/tests/_helpers/subprocess-mock.d.ts.map +1 -0
  268. package/dist/tests/_helpers/subprocess-mock.js +136 -0
  269. package/dist/tests/_helpers/subprocess-mock.js.map +1 -0
  270. package/dist/tests/auto-recovery.test.d.ts +2 -0
  271. package/dist/tests/auto-recovery.test.d.ts.map +1 -0
  272. package/dist/tests/auto-recovery.test.js +189 -0
  273. package/dist/tests/auto-recovery.test.js.map +1 -0
  274. package/dist/tests/bench-harness.test.d.ts +2 -0
  275. package/dist/tests/bench-harness.test.d.ts.map +1 -0
  276. package/dist/tests/bench-harness.test.js +21 -0
  277. package/dist/tests/bench-harness.test.js.map +1 -0
  278. package/dist/tests/cache-parity.test.d.ts +2 -0
  279. package/dist/tests/cache-parity.test.d.ts.map +1 -0
  280. package/dist/tests/cache-parity.test.js +401 -0
  281. package/dist/tests/cache-parity.test.js.map +1 -0
  282. package/dist/tests/command-router.test.d.ts +2 -0
  283. package/dist/tests/command-router.test.d.ts.map +1 -0
  284. package/dist/tests/command-router.test.js +60 -0
  285. package/dist/tests/command-router.test.js.map +1 -0
  286. package/dist/tests/council.test.d.ts +2 -0
  287. package/dist/tests/council.test.d.ts.map +1 -0
  288. package/dist/tests/council.test.js +20 -0
  289. package/dist/tests/council.test.js.map +1 -0
  290. package/dist/tests/drift-detector.test.d.ts +2 -0
  291. package/dist/tests/drift-detector.test.d.ts.map +1 -0
  292. package/dist/tests/drift-detector.test.js +268 -0
  293. package/dist/tests/drift-detector.test.js.map +1 -0
  294. package/dist/tests/eager-bootstrap-gating.test.d.ts +9 -0
  295. package/dist/tests/eager-bootstrap-gating.test.d.ts.map +1 -0
  296. package/dist/tests/eager-bootstrap-gating.test.js +97 -0
  297. package/dist/tests/eager-bootstrap-gating.test.js.map +1 -0
  298. package/dist/tests/engines.test.d.ts +2 -0
  299. package/dist/tests/engines.test.d.ts.map +1 -0
  300. package/dist/tests/engines.test.js +8 -0
  301. package/dist/tests/engines.test.js.map +1 -0
  302. package/dist/tests/error-formatter.test.d.ts +2 -0
  303. package/dist/tests/error-formatter.test.d.ts.map +1 -0
  304. package/dist/tests/error-formatter.test.js +220 -0
  305. package/dist/tests/error-formatter.test.js.map +1 -0
  306. package/dist/tests/health.test.d.ts +2 -0
  307. package/dist/tests/health.test.d.ts.map +1 -0
  308. package/dist/tests/health.test.js +110 -0
  309. package/dist/tests/health.test.js.map +1 -0
  310. package/dist/tests/heartbeat-workaround.test.d.ts +2 -0
  311. package/dist/tests/heartbeat-workaround.test.d.ts.map +1 -0
  312. package/dist/tests/heartbeat-workaround.test.js +90 -0
  313. package/dist/tests/heartbeat-workaround.test.js.map +1 -0
  314. package/dist/tests/index.test.d.ts +2 -0
  315. package/dist/tests/index.test.d.ts.map +1 -0
  316. package/dist/tests/index.test.js +7 -0
  317. package/dist/tests/index.test.js.map +1 -0
  318. package/dist/tests/lib-sysprompt-strip.test.d.ts +2 -0
  319. package/dist/tests/lib-sysprompt-strip.test.d.ts.map +1 -0
  320. package/dist/tests/lib-sysprompt-strip.test.js +145 -0
  321. package/dist/tests/lib-sysprompt-strip.test.js.map +1 -0
  322. package/dist/tests/listener-activation.test.d.ts +2 -0
  323. package/dist/tests/listener-activation.test.d.ts.map +1 -0
  324. package/dist/tests/listener-activation.test.js +87 -0
  325. package/dist/tests/listener-activation.test.js.map +1 -0
  326. package/dist/tests/mcp-bridge.test.d.ts +2 -0
  327. package/dist/tests/mcp-bridge.test.d.ts.map +1 -0
  328. package/dist/tests/mcp-bridge.test.js +137 -0
  329. package/dist/tests/mcp-bridge.test.js.map +1 -0
  330. package/dist/tests/openai-compat.test.d.ts +2 -0
  331. package/dist/tests/openai-compat.test.d.ts.map +1 -0
  332. package/dist/tests/openai-compat.test.js +8 -0
  333. package/dist/tests/openai-compat.test.js.map +1 -0
  334. package/dist/tests/proxy-heartbeat-integration.test.d.ts +15 -0
  335. package/dist/tests/proxy-heartbeat-integration.test.d.ts.map +1 -0
  336. package/dist/tests/proxy-heartbeat-integration.test.js +122 -0
  337. package/dist/tests/proxy-heartbeat-integration.test.js.map +1 -0
  338. package/dist/tests/proxy.test.d.ts +2 -0
  339. package/dist/tests/proxy.test.d.ts.map +1 -0
  340. package/dist/tests/proxy.test.js +8 -0
  341. package/dist/tests/proxy.test.js.map +1 -0
  342. package/dist/tests/register-guard-stacking.test.d.ts +2 -0
  343. package/dist/tests/register-guard-stacking.test.d.ts.map +1 -0
  344. package/dist/tests/register-guard-stacking.test.js +61 -0
  345. package/dist/tests/register-guard-stacking.test.js.map +1 -0
  346. package/dist/tests/register-guard.test.d.ts +2 -0
  347. package/dist/tests/register-guard.test.d.ts.map +1 -0
  348. package/dist/tests/register-guard.test.js +129 -0
  349. package/dist/tests/register-guard.test.js.map +1 -0
  350. package/dist/tests/route-flag-rollback.test.d.ts +2 -0
  351. package/dist/tests/route-flag-rollback.test.d.ts.map +1 -0
  352. package/dist/tests/route-flag-rollback.test.js +70 -0
  353. package/dist/tests/route-flag-rollback.test.js.map +1 -0
  354. package/dist/tests/route-flag.test.d.ts +2 -0
  355. package/dist/tests/route-flag.test.d.ts.map +1 -0
  356. package/dist/tests/route-flag.test.js +101 -0
  357. package/dist/tests/route-flag.test.js.map +1 -0
  358. package/dist/tests/session-bootstrap.test.d.ts +2 -0
  359. package/dist/tests/session-bootstrap.test.d.ts.map +1 -0
  360. package/dist/tests/session-bootstrap.test.js +183 -0
  361. package/dist/tests/session-bootstrap.test.js.map +1 -0
  362. package/dist/tests/session.test.d.ts +2 -0
  363. package/dist/tests/session.test.d.ts.map +1 -0
  364. package/dist/tests/session.test.js +17 -0
  365. package/dist/tests/session.test.js.map +1 -0
  366. package/dist/tests/state-machine.test.d.ts +2 -0
  367. package/dist/tests/state-machine.test.d.ts.map +1 -0
  368. package/dist/tests/state-machine.test.js +133 -0
  369. package/dist/tests/state-machine.test.js.map +1 -0
  370. package/dist/tests/streaming/cli-stream-parser.test.d.ts +2 -0
  371. package/dist/tests/streaming/cli-stream-parser.test.d.ts.map +1 -0
  372. package/dist/tests/streaming/cli-stream-parser.test.js +233 -0
  373. package/dist/tests/streaming/cli-stream-parser.test.js.map +1 -0
  374. package/dist/tests/streaming/feature-flag.test.d.ts +14 -0
  375. package/dist/tests/streaming/feature-flag.test.d.ts.map +1 -0
  376. package/dist/tests/streaming/feature-flag.test.js +163 -0
  377. package/dist/tests/streaming/feature-flag.test.js.map +1 -0
  378. package/dist/tests/streaming/no-tools-prompt.test.d.ts +17 -0
  379. package/dist/tests/streaming/no-tools-prompt.test.d.ts.map +1 -0
  380. package/dist/tests/streaming/no-tools-prompt.test.js +229 -0
  381. package/dist/tests/streaming/no-tools-prompt.test.js.map +1 -0
  382. package/dist/tests/streaming/skill-plus-tools.test.d.ts +14 -0
  383. package/dist/tests/streaming/skill-plus-tools.test.d.ts.map +1 -0
  384. package/dist/tests/streaming/skill-plus-tools.test.js +234 -0
  385. package/dist/tests/streaming/skill-plus-tools.test.js.map +1 -0
  386. package/dist/tests/streaming/sse-translator.test.d.ts +2 -0
  387. package/dist/tests/streaming/sse-translator.test.d.ts.map +1 -0
  388. package/dist/tests/streaming/sse-translator.test.js +227 -0
  389. package/dist/tests/streaming/sse-translator.test.js.map +1 -0
  390. package/dist/tests/streaming/tool-result-roundtrip.test.d.ts +11 -0
  391. package/dist/tests/streaming/tool-result-roundtrip.test.d.ts.map +1 -0
  392. package/dist/tests/streaming/tool-result-roundtrip.test.js +215 -0
  393. package/dist/tests/streaming/tool-result-roundtrip.test.js.map +1 -0
  394. package/dist/tests/streaming/tool-use-translation.test.d.ts +10 -0
  395. package/dist/tests/streaming/tool-use-translation.test.d.ts.map +1 -0
  396. package/dist/tests/streaming/tool-use-translation.test.js +251 -0
  397. package/dist/tests/streaming/tool-use-translation.test.js.map +1 -0
  398. package/dist/tests/telegram-bridge.test.d.ts +2 -0
  399. package/dist/tests/telegram-bridge.test.d.ts.map +1 -0
  400. package/dist/tests/telegram-bridge.test.js +17 -0
  401. package/dist/tests/telegram-bridge.test.js.map +1 -0
  402. package/dist/tests/telegram-injector.test.d.ts +2 -0
  403. package/dist/tests/telegram-injector.test.d.ts.map +1 -0
  404. package/dist/tests/telegram-injector.test.js +74 -0
  405. package/dist/tests/telegram-injector.test.js.map +1 -0
  406. package/dist/tests/telemetry.test.d.ts +2 -0
  407. package/dist/tests/telemetry.test.d.ts.map +1 -0
  408. package/dist/tests/telemetry.test.js +405 -0
  409. package/dist/tests/telemetry.test.js.map +1 -0
  410. package/dist/tests/test-mode.test.d.ts +2 -0
  411. package/dist/tests/test-mode.test.d.ts.map +1 -0
  412. package/dist/tests/test-mode.test.js +39 -0
  413. package/dist/tests/test-mode.test.js.map +1 -0
  414. package/mcp-config.template.json +13 -0
  415. package/mcp-tools.json +1 -0
  416. package/openclaw-mcp-bridge.cjs +152 -0
  417. package/openclaw.plugin.json +30 -0
  418. package/package.json +45 -0
  419. package/skills/.gitkeep +0 -0
  420. package/stubs/commands-status-deps.runtime.js +10 -0
  421. package/stubs/status.runtime.js +149 -0
  422. package/vendor/base-oneshot-session.d.ts +87 -0
  423. package/vendor/base-oneshot-session.js +227 -0
  424. package/vendor/base-oneshot-session.js.map +1 -0
  425. package/vendor/circuit-breaker.d.ts +21 -0
  426. package/vendor/circuit-breaker.js +47 -0
  427. package/vendor/circuit-breaker.js.map +1 -0
  428. package/vendor/consensus.d.ts +20 -0
  429. package/vendor/consensus.js +52 -0
  430. package/vendor/consensus.js.map +1 -0
  431. package/vendor/constants.d.ts +130 -0
  432. package/vendor/constants.js +139 -0
  433. package/vendor/constants.js.map +1 -0
  434. package/vendor/council.d.ts +67 -0
  435. package/vendor/council.js +913 -0
  436. package/vendor/council.js.map +1 -0
  437. package/vendor/embedded-server.d.ts +25 -0
  438. package/vendor/embedded-server.js +360 -0
  439. package/vendor/embedded-server.js.map +1 -0
  440. package/vendor/inbox-manager.d.ts +38 -0
  441. package/vendor/inbox-manager.js +111 -0
  442. package/vendor/inbox-manager.js.map +1 -0
  443. package/vendor/index.d.ts +63 -0
  444. package/vendor/index.js +705 -0
  445. package/vendor/index.js.map +1 -0
  446. package/vendor/logger.d.ts +16 -0
  447. package/vendor/logger.js +44 -0
  448. package/vendor/logger.js.map +1 -0
  449. package/vendor/models.d.ts +69 -0
  450. package/vendor/models.js +289 -0
  451. package/vendor/models.js.map +1 -0
  452. package/vendor/openai-compat.d.ts +197 -0
  453. package/vendor/openai-compat.js +721 -0
  454. package/vendor/openai-compat.js.map +1 -0
  455. package/vendor/persistent-codex-session.d.ts +16 -0
  456. package/vendor/persistent-codex-session.js +105 -0
  457. package/vendor/persistent-codex-session.js.map +1 -0
  458. package/vendor/persistent-cursor-session.d.ts +21 -0
  459. package/vendor/persistent-cursor-session.js +241 -0
  460. package/vendor/persistent-cursor-session.js.map +1 -0
  461. package/vendor/persistent-custom-session.d.ts +78 -0
  462. package/vendor/persistent-custom-session.js +937 -0
  463. package/vendor/persistent-custom-session.js.map +1 -0
  464. package/vendor/persistent-gemini-session.d.ts +21 -0
  465. package/vendor/persistent-gemini-session.js +216 -0
  466. package/vendor/persistent-gemini-session.js.map +1 -0
  467. package/vendor/persistent-session.d.ts +74 -0
  468. package/vendor/persistent-session.js +684 -0
  469. package/vendor/persistent-session.js.map +1 -0
  470. package/vendor/proxy/anthropic-adapter.d.ts +136 -0
  471. package/vendor/proxy/anthropic-adapter.js +392 -0
  472. package/vendor/proxy/anthropic-adapter.js.map +1 -0
  473. package/vendor/proxy/handler.d.ts +39 -0
  474. package/vendor/proxy/handler.js +323 -0
  475. package/vendor/proxy/handler.js.map +1 -0
  476. package/vendor/proxy/schema-cleaner.d.ts +11 -0
  477. package/vendor/proxy/schema-cleaner.js +34 -0
  478. package/vendor/proxy/schema-cleaner.js.map +1 -0
  479. package/vendor/proxy/thought-cache.d.ts +19 -0
  480. package/vendor/proxy/thought-cache.js +53 -0
  481. package/vendor/proxy/thought-cache.js.map +1 -0
  482. package/vendor/session-manager.d.ts +211 -0
  483. package/vendor/session-manager.js +1345 -0
  484. package/vendor/session-manager.js.map +1 -0
  485. package/vendor/skill-resolver.js +107 -0
  486. package/vendor/types.d.ts +466 -0
  487. package/vendor/types.js +8 -0
  488. package/vendor/types.js.map +1 -0
  489. package/vendor/validation.d.ts +31 -0
  490. package/vendor/validation.js +104 -0
  491. package/vendor/validation.js.map +1 -0
@@ -0,0 +1,1351 @@
1
+ /**
2
+ * SessionManager โ€” manages multiple PersistentClaudeSession instances
3
+ *
4
+ * Replaces the Express server layer. Pure class with no HTTP dependency.
5
+ * Can be used by Plugin tools, CLI, or any other consumer.
6
+ */
7
+ import * as fs from 'node:fs';
8
+ import * as path from 'node:path';
9
+ import * as os from 'node:os';
10
+ import { execFileSync } from 'node:child_process';
11
+ import * as http from 'node:http';
12
+ import { createRequire } from 'node:module';
13
+ const _require = createRequire(import.meta.url);
14
+ function getPluginVersion() {
15
+ try {
16
+ // Walk up from this file to find package.json
17
+ let dir = path.dirname(_require.resolve('./session-manager.js').replace('/dist/', '/'));
18
+ for (let i = 0; i < 5; i++) {
19
+ const pkgPath = path.join(dir, 'package.json');
20
+ if (fs.existsSync(pkgPath)) {
21
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
22
+ if (pkg.version)
23
+ return pkg.version;
24
+ }
25
+ dir = path.dirname(dir);
26
+ }
27
+ }
28
+ catch {
29
+ /* ignore */
30
+ }
31
+ return 'unknown';
32
+ }
33
+ // โ”€โ”€โ”€ Persistence โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
34
+ const PERSIST_DIR = path.join(os.homedir(), '.openclaw');
35
+ const PERSIST_FILE = path.join(PERSIST_DIR, 'claude-sessions.json');
36
+ function loadPersistedSessions() {
37
+ try {
38
+ if (!fs.existsSync(PERSIST_FILE))
39
+ return new Map();
40
+ const raw = fs.readFileSync(PERSIST_FILE, 'utf8');
41
+ const arr = JSON.parse(raw);
42
+ const now = Date.now();
43
+ // Filter out entries older than disk TTL
44
+ const valid = arr.filter((s) => now - s.lastActivity < PERSIST_DISK_TTL_MS);
45
+ return new Map(valid.map((s) => [s.name, s]));
46
+ }
47
+ catch {
48
+ return new Map();
49
+ }
50
+ }
51
+ // Atomic write: write to .tmp then rename to avoid corrupt reads on crash
52
+ function savePersistedSessions(sessions, logger) {
53
+ try {
54
+ fs.mkdirSync(PERSIST_DIR, { recursive: true });
55
+ const arr = Array.from(sessions.values());
56
+ const tmp = PERSIST_FILE + '.tmp';
57
+ fs.writeFileSync(tmp, JSON.stringify(arr, null, 2));
58
+ fs.renameSync(tmp, PERSIST_FILE);
59
+ }
60
+ catch (err) {
61
+ (logger || createConsoleLogger('SessionManager')).warn('Failed to persist sessions:', err.message);
62
+ }
63
+ }
64
+ // Async version for hot-path (sendMessage, TTL cleanup)
65
+ function savePersistedSessionsAsync(sessions, logger) {
66
+ const log = logger || createConsoleLogger('SessionManager');
67
+ const arr = Array.from(sessions.values());
68
+ const tmp = PERSIST_FILE + '.tmp';
69
+ fs.mkdir(PERSIST_DIR, { recursive: true }, (mkdirErr) => {
70
+ if (mkdirErr) {
71
+ log.error('Failed to create persist dir:', mkdirErr.message);
72
+ return;
73
+ }
74
+ fs.writeFile(tmp, JSON.stringify(arr, null, 2), (writeErr) => {
75
+ if (writeErr) {
76
+ log.error('Failed to write session file:', writeErr.message);
77
+ return;
78
+ }
79
+ fs.rename(tmp, PERSIST_FILE, (renameErr) => {
80
+ if (renameErr) {
81
+ log.error('Failed to rename session file:', renameErr.message);
82
+ // Clean up orphan tmp file
83
+ fs.unlink(tmp, () => { });
84
+ }
85
+ });
86
+ });
87
+ });
88
+ }
89
+ // Debounce helper โ€” coalesces rapid writes into one
90
+ function makeDebounced(fn, ms) {
91
+ let timer = null;
92
+ return () => {
93
+ if (timer)
94
+ clearTimeout(timer);
95
+ timer = setTimeout(() => {
96
+ timer = null;
97
+ fn();
98
+ }, ms);
99
+ };
100
+ }
101
+ import { createConsoleLogger } from '../logger.js';
102
+ import { CircuitBreaker } from '../lib/circuit-breaker.js';
103
+ import { InboxManager } from './inbox-manager.js';
104
+ import { sanitizeCwd, validateName } from '../validation.js';
105
+ import { PersistentClaudeSession } from '../engines/persistent-session.js';
106
+ import { PersistentGeminiSession } from '../engines/persistent-gemini-session.js';
107
+ import { PersistentCodexSession } from '../engines/persistent-codex-session.js';
108
+ import { PersistentCursorSession } from '../engines/persistent-cursor-session.js';
109
+ import { PersistentCustomSession } from '../engines/persistent-custom-session.js';
110
+ import { overrideModelPricing, } from '../types.js';
111
+ import { resolveAlias, isClaudeModel } from '../models.js';
112
+ import { Council } from '../council/council.js';
113
+ import { PERSIST_DISK_TTL_MS, DEBOUNCED_SAVE_MS, CLEANUP_INTERVAL_MS, TURN_TIMEOUT_MS, GREP_HISTORY_FETCH, TEAM_LIST_TIMEOUT_MS, TEAM_SEND_TIMEOUT_MS, RESULT_TTL_MS, ULTRAPLAN_TIMEOUT_MS, ULTRAREVIEW_POLL_INTERVAL_MS, STOP_SIGKILL_DELAY_MS, SESSION_EVENT, DEFAULT_HISTORY_LIMIT, } from '../constants.js';
114
+ import { getGatewayUrl, getGatewayKey, getAnthropicApiKey, getOpenaiApiKey, getGeminiApiKey, getGeminiBin, getCodexBin, getCursorBin, } from '../lib/config.js';
115
+ // โ”€โ”€โ”€ SessionManager โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
116
+ export class SessionManager {
117
+ sessions = new Map();
118
+ _pendingSessions = new Map();
119
+ cleanupTimer = null;
120
+ pluginConfig;
121
+ persistedSessions;
122
+ _debouncedSave;
123
+ _proxyServer = null;
124
+ _proxyPort = null;
125
+ _activePids = new Map();
126
+ _circuitBreaker = new CircuitBreaker();
127
+ _inbox = new InboxManager();
128
+ logger;
129
+ constructor(config, logger) {
130
+ this.logger = logger || createConsoleLogger('SessionManager');
131
+ this.pluginConfig = {
132
+ claudeBin: config?.claudeBin || 'claude',
133
+ defaultModel: config?.defaultModel,
134
+ defaultPermissionMode: config?.defaultPermissionMode || 'acceptEdits',
135
+ defaultEffort: config?.defaultEffort || 'auto',
136
+ maxConcurrentSessions: config?.maxConcurrentSessions || 5,
137
+ sessionTtlMinutes: config?.sessionTtlMinutes || 120,
138
+ };
139
+ // Apply pricing overrides if provided
140
+ if (config?.pricingOverrides) {
141
+ overrideModelPricing(config.pricingOverrides);
142
+ }
143
+ // Load persisted session registry from disk
144
+ this.persistedSessions = loadPersistedSessions();
145
+ // Clean up orphaned child processes from a previous unclean exit
146
+ this._cleanupOrphanedPids();
147
+ // Debounced async writer โ€” at most one write per 5 seconds on hot paths
148
+ this._debouncedSave = makeDebounced(() => savePersistedSessionsAsync(this.persistedSessions, this.logger), DEBOUNCED_SAVE_MS);
149
+ // Start TTL cleanup timer
150
+ this.cleanupTimer = setInterval(() => this._cleanupIdleSessions(), CLEANUP_INTERVAL_MS);
151
+ }
152
+ // โ”€โ”€โ”€ Session Lifecycle โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
153
+ async startSession(config) {
154
+ const name = config.name || `session-${Date.now()}`;
155
+ // Check pending first โ€” a concurrent caller may have already started creation
156
+ const pending = this._pendingSessions.get(name);
157
+ if (pending)
158
+ return pending;
159
+ if (this.sessions.has(name)) {
160
+ const existing = this.sessions.get(name);
161
+ return this._toSessionInfo(name, existing);
162
+ }
163
+ // Create the promise and register it in _pendingSessions BEFORE any async work,
164
+ // so concurrent callers arriving between now and completion see the pending entry.
165
+ const promise = this._doStartSession(name, config);
166
+ this._pendingSessions.set(name, promise);
167
+ try {
168
+ return await promise;
169
+ }
170
+ finally {
171
+ this._pendingSessions.delete(name);
172
+ }
173
+ }
174
+ async _doStartSession(name, config) {
175
+ if (this.sessions.size >= this.pluginConfig.maxConcurrentSessions) {
176
+ throw new Error(`Max concurrent sessions (${this.pluginConfig.maxConcurrentSessions}) reached`);
177
+ }
178
+ // Auto-resume: if we have a persisted claudeSessionId for this name, inject it.
179
+ // Skip when config.skipPersistence is set (e.g. openai-compat bridge sessions
180
+ // that must NOT resume stale CLI state from a previous server run).
181
+ const skipPersist = !!config.skipPersistence;
182
+ const persisted = skipPersist ? undefined : this.persistedSessions.get(name);
183
+ // Unified: only use resumeSessionId (claudeResumeId is an internal alias, not exposed)
184
+ const resumeId = config.resumeSessionId ?? persisted?.claudeSessionId;
185
+ const fullConfig = {
186
+ name,
187
+ cwd: config.cwd || persisted?.cwd || process.cwd(),
188
+ permissionMode: config.permissionMode || this.pluginConfig.defaultPermissionMode,
189
+ effort: config.effort || this.pluginConfig.defaultEffort,
190
+ model: config.model || persisted?.model || this.pluginConfig.defaultModel,
191
+ ...config,
192
+ ...(resumeId ? { resumeSessionId: resumeId } : {}),
193
+ };
194
+ // Resolve model alias
195
+ if (fullConfig.model) {
196
+ fullConfig.resolvedModel = this._resolveModel(fullConfig.model, fullConfig.modelOverrides);
197
+ }
198
+ // Auto-inject proxy baseUrl for non-Claude models on the claude engine.
199
+ // Starts a local proxy server that converts Anthropic โ†’ OpenAI format
200
+ // and forwards to the OpenClaw gateway. Zero config required.
201
+ const engine = fullConfig.engine || persisted?.engine || 'claude';
202
+ // Circuit breaker โ€” reject early if engine is in backoff
203
+ this._circuitBreaker.check(engine);
204
+ if (engine === 'claude' && fullConfig.resolvedModel && !fullConfig.baseUrl) {
205
+ if (!isClaudeModel(fullConfig.resolvedModel)) {
206
+ const proxyPort = await this._ensureProxyServer();
207
+ if (proxyPort) {
208
+ fullConfig.baseUrl = `http://127.0.0.1:${proxyPort}`;
209
+ }
210
+ }
211
+ }
212
+ const session = this._createSession(engine, fullConfig);
213
+ session.on(SESSION_EVENT.LOG, (...args) => this.logger.info(`[Session:${name}]`, ...args));
214
+ try {
215
+ await session.start();
216
+ }
217
+ catch (err) {
218
+ this._circuitBreaker.recordFailure(engine);
219
+ throw err;
220
+ }
221
+ // Engine started successfully โ€” reset circuit breaker
222
+ this._circuitBreaker.reset(engine);
223
+ // Track child process PID for orphan cleanup
224
+ if (session.pid) {
225
+ this._activePids.set(name, session.pid);
226
+ this._savePids();
227
+ }
228
+ const managed = {
229
+ session,
230
+ config: fullConfig,
231
+ created: persisted?.originalCreated || new Date().toISOString(),
232
+ lastActivity: Date.now(),
233
+ cwd: fullConfig.cwd,
234
+ claudeSessionId: session.sessionId,
235
+ };
236
+ this.sessions.set(name, managed);
237
+ // Persist registry after session is live (skip for ephemeral sessions
238
+ // like the openai-compat bridge that set skipPersistence: true)
239
+ if (!skipPersist) {
240
+ this._persistSession(name, managed);
241
+ }
242
+ return this._toSessionInfo(name, managed);
243
+ }
244
+ async sendMessage(name,
245
+ // Phase 2 R4 wire-up: accept either a plain string (legacy path) or
246
+ // an array of Anthropic-shaped content blocks (tool-stream mode โ€”
247
+ // native tool_result blocks + optional text follow-up). vendor
248
+ // persistent-session.send() already passes arrays through unchanged.
249
+ message, options = {}) {
250
+ const managed = this._getSession(name);
251
+ // Per-session serialization. Two concurrent sendMessage() calls on the
252
+ // same session previously raced on PersistentClaudeSession._streamCallbacks
253
+ // and the shared TURN_COMPLETE listener โ€” the second caller would receive
254
+ // the first caller's response, and stream callbacks would clobber each
255
+ // other. Chain waiters via a per-session promise so a slow turn blocks
256
+ // (rather than corrupts) subsequent sends.
257
+ const prior = managed.sendChain ?? Promise.resolve();
258
+ let releaseChain;
259
+ const link = new Promise((resolve) => {
260
+ releaseChain = resolve;
261
+ });
262
+ managed.sendChain = prior.then(() => link).catch(() => link);
263
+ try {
264
+ await prior;
265
+ }
266
+ catch {
267
+ /* prior failure shouldn't block this caller */
268
+ }
269
+ try {
270
+ managed.lastActivity = Date.now();
271
+ const sendOpts = {
272
+ waitForComplete: true,
273
+ timeout: options.timeout || TURN_TIMEOUT_MS,
274
+ };
275
+ if (options.effort)
276
+ sendOpts.effort = options.effort;
277
+ if (options.plan)
278
+ sendOpts.plan = true;
279
+ if (options.onEvent || options.onChunk) {
280
+ sendOpts.callbacks = {
281
+ onText: (text) => {
282
+ if (options.onChunk)
283
+ options.onChunk(text);
284
+ if (options.onEvent)
285
+ options.onEvent({ type: 'text', result: text });
286
+ },
287
+ onToolUse: (event) => {
288
+ if (options.onEvent)
289
+ options.onEvent({ type: 'tool_use', ...event });
290
+ },
291
+ onToolResult: (event) => {
292
+ if (options.onEvent)
293
+ options.onEvent({ type: 'tool_result', ...event });
294
+ },
295
+ };
296
+ }
297
+ const result = await managed.session.send(message, sendOpts);
298
+ // Update session ID if available (skip disk persist for ephemeral
299
+ // sessions that were started with skipPersistence)
300
+ if (managed.session.sessionId) {
301
+ managed.claudeSessionId = managed.session.sessionId;
302
+ if (this.persistedSessions.has(name)) {
303
+ this._persistSession(name, managed);
304
+ }
305
+ }
306
+ if ('text' in result) {
307
+ return {
308
+ output: result.text,
309
+ sessionId: managed.claudeSessionId,
310
+ events: [],
311
+ };
312
+ }
313
+ return { output: '', sessionId: managed.claudeSessionId, events: [] };
314
+ }
315
+ finally {
316
+ releaseChain();
317
+ // If this was the tail of the chain, clear it so memory doesn't grow.
318
+ if (managed.sendChain === link)
319
+ managed.sendChain = undefined;
320
+ }
321
+ }
322
+ async stopSession(name) {
323
+ const managed = this._getSession(name);
324
+ managed.session.stop();
325
+ this.sessions.delete(name);
326
+ // Remove PID tracking
327
+ this._activePids.delete(name);
328
+ this._savePids();
329
+ // Explicit stop = user intent to end session โ€” remove from disk too
330
+ this.persistedSessions.delete(name);
331
+ savePersistedSessions(this.persistedSessions, this.logger);
332
+ }
333
+ listSessions() {
334
+ return Array.from(this.sessions.entries()).map(([name, managed]) => this._toSessionInfo(name, managed));
335
+ }
336
+ listPersistedSessions() {
337
+ return Array.from(this.persistedSessions.values());
338
+ }
339
+ getStatus(name) {
340
+ const managed = this._getSession(name);
341
+ return {
342
+ ...this._toSessionInfo(name, managed),
343
+ stats: managed.session.getStats(),
344
+ };
345
+ }
346
+ // โ”€โ”€โ”€ Session Operations โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
347
+ async grepSession(name, pattern, limit = DEFAULT_HISTORY_LIMIT) {
348
+ const managed = this._getSession(name);
349
+ const history = managed.session.getHistory(GREP_HISTORY_FETCH);
350
+ const regex = new RegExp(pattern, 'i');
351
+ return history
352
+ .filter((ev) => regex.test(JSON.stringify(ev)))
353
+ .slice(0, limit)
354
+ .map((ev) => ({
355
+ time: ev.time,
356
+ type: ev.type,
357
+ content: JSON.stringify(ev.event),
358
+ }));
359
+ }
360
+ async compactSession(name, summary) {
361
+ const managed = this._getSession(name);
362
+ await managed.session.compact(summary);
363
+ }
364
+ setEffort(name, level) {
365
+ const managed = this._getSession(name);
366
+ managed.session.setEffort(level);
367
+ managed.config.effort = level;
368
+ }
369
+ /**
370
+ * Switch model for a session.
371
+ * Updates in-memory config only (takes effect on next restart/resume).
372
+ * For immediate effect, call restartWithConfig() explicitly.
373
+ */
374
+ setModel(name, model) {
375
+ const managed = this._getSession(name);
376
+ const resolved = this._resolveModel(model, managed.config.modelOverrides);
377
+ managed.config.model = model;
378
+ managed.config.resolvedModel = resolved;
379
+ }
380
+ /**
381
+ * Switch model immediately by restarting the session with --resume.
382
+ * Conversation history is preserved via the claude session ID.
383
+ *
384
+ * Guards:
385
+ * - Rejects if session is currently processing a message (busy guard)
386
+ * - Validates model string against known aliases before restarting
387
+ * - Rolls back to old session if startSession fails
388
+ */
389
+ async switchModel(name, model) {
390
+ const managed = this._getSession(name);
391
+ // Busy guard โ€” don't restart mid-message
392
+ if (managed.session.isBusy) {
393
+ throw new Error(`Session '${name}' is currently processing a message. Wait for it to finish before switching model.`);
394
+ }
395
+ const sessionId = managed.claudeSessionId || managed.session.sessionId;
396
+ if (!sessionId)
397
+ throw new Error(`Session '${name}' has no claude session ID โ€” cannot resume after restart`);
398
+ // Validate model โ€” must be a known alias or contain a recognisable pattern
399
+ const resolvedModel = this._resolveModel(model, managed.config.modelOverrides);
400
+ const knownPatterns = ['claude-', 'gemini-', 'gpt-', 'anthropic/', 'google/', 'openai/'];
401
+ const looksValid = knownPatterns.some((p) => resolvedModel.includes(p));
402
+ if (!looksValid) {
403
+ throw new Error(`Unknown model '${model}' (resolved: '${resolvedModel}'). Use a known alias (opus, sonnet, haiku, gemini-pro, etc.) or a full provider/model string.`);
404
+ }
405
+ const oldConfig = { ...managed.config };
406
+ managed.session.stop();
407
+ this.sessions.delete(name);
408
+ try {
409
+ return await this.startSession({
410
+ ...oldConfig,
411
+ name,
412
+ model,
413
+ resumeSessionId: sessionId,
414
+ });
415
+ }
416
+ catch (err) {
417
+ // Rollback: restart with original config
418
+ this.logger.error(`switchModel failed for '${name}', attempting rollback:`, err);
419
+ try {
420
+ await this.startSession({ ...oldConfig, name, resumeSessionId: sessionId });
421
+ }
422
+ catch (rollbackErr) {
423
+ this.logger.error(`Rollback also failed for '${name}':`, rollbackErr);
424
+ }
425
+ throw new Error(`Failed to switch model for '${name}': ${err.message}`);
426
+ }
427
+ }
428
+ /**
429
+ * Update allowedTools or disallowedTools at runtime.
430
+ *
431
+ * The claude CLI does not support changing tool lists while running, so
432
+ * the only way to apply new constraints is to restart the process with
433
+ * the updated flags and --resume to replay conversation history.
434
+ *
435
+ * Guards:
436
+ * - Rejects if session is busy
437
+ * - Rolls back to old session if startSession fails
438
+ * - merge:true adds tools; removeTools removes specific tools from the list
439
+ */
440
+ async updateTools(name, opts) {
441
+ const managed = this._getSession(name);
442
+ // Busy guard
443
+ if (managed.session.isBusy) {
444
+ throw new Error(`Session '${name}' is currently processing a message. Wait for it to finish before updating tools.`);
445
+ }
446
+ const sessionId = managed.claudeSessionId || managed.session.sessionId;
447
+ if (!sessionId)
448
+ throw new Error(`Session '${name}' has no claude session ID โ€” cannot resume after restart`);
449
+ const oldConfig = { ...managed.config };
450
+ let newAllowed = opts.allowedTools;
451
+ let newDisallowed = opts.disallowedTools;
452
+ if (opts.merge) {
453
+ newAllowed = opts.allowedTools
454
+ ? [...new Set([...(oldConfig.allowedTools || []), ...opts.allowedTools])]
455
+ : oldConfig.allowedTools;
456
+ newDisallowed = opts.disallowedTools
457
+ ? [...new Set([...(oldConfig.disallowedTools || []), ...opts.disallowedTools])]
458
+ : oldConfig.disallowedTools;
459
+ }
460
+ // Remove specific tools if requested
461
+ if (opts.removeTools?.length) {
462
+ const removeSet = new Set(opts.removeTools);
463
+ if (newAllowed)
464
+ newAllowed = newAllowed.filter((t) => !removeSet.has(t));
465
+ if (newDisallowed)
466
+ newDisallowed = newDisallowed.filter((t) => !removeSet.has(t));
467
+ }
468
+ managed.session.stop();
469
+ this.sessions.delete(name);
470
+ try {
471
+ return await this.startSession({
472
+ ...oldConfig,
473
+ name,
474
+ allowedTools: newAllowed,
475
+ disallowedTools: newDisallowed,
476
+ resumeSessionId: sessionId,
477
+ });
478
+ }
479
+ catch (err) {
480
+ this.logger.error(`updateTools failed for '${name}', attempting rollback:`, err);
481
+ try {
482
+ await this.startSession({ ...oldConfig, name, resumeSessionId: sessionId });
483
+ }
484
+ catch (rollbackErr) {
485
+ this.logger.error(`Rollback also failed for '${name}':`, rollbackErr);
486
+ }
487
+ throw new Error(`Failed to update tools for '${name}': ${err.message}`);
488
+ }
489
+ }
490
+ getCost(name) {
491
+ const managed = this._getSession(name);
492
+ return managed.session.getCost();
493
+ }
494
+ // โ”€โ”€โ”€ Agent/Skill/Rule Management โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
495
+ listAgents(cwd) {
496
+ const safeCwd = sanitizeCwd(cwd);
497
+ const projectDir = path.join(safeCwd || os.homedir(), '.claude', 'agents');
498
+ const globalDir = path.join(os.homedir(), '.claude', 'agents');
499
+ const project = this._listMdFiles(projectDir);
500
+ const global = this._listMdFiles(globalDir);
501
+ const seen = new Set(project.map((a) => a.name));
502
+ return [...project, ...global.filter((a) => !seen.has(a.name))];
503
+ }
504
+ createAgent(name, cwd, description, prompt) {
505
+ validateName(name);
506
+ const safeCwd = sanitizeCwd(cwd);
507
+ const dir = path.join(safeCwd || os.homedir(), '.claude', 'agents');
508
+ fs.mkdirSync(dir, { recursive: true });
509
+ const filePath = path.join(dir, `${name}.md`);
510
+ const content = `---\ndescription: ${description || name}\n---\n\n${prompt || `You are ${name}.`}\n`;
511
+ fs.writeFileSync(filePath, content);
512
+ return filePath;
513
+ }
514
+ listSkills(cwd) {
515
+ const safeCwd = sanitizeCwd(cwd);
516
+ const dirs = [
517
+ path.join(safeCwd || os.homedir(), '.claude', 'skills'),
518
+ path.join(os.homedir(), '.claude', 'skills'),
519
+ ];
520
+ const all = [];
521
+ const seen = new Set();
522
+ for (const dir of dirs) {
523
+ if (!fs.existsSync(dir))
524
+ continue;
525
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
526
+ if (!entry.isDirectory() || seen.has(entry.name))
527
+ continue;
528
+ seen.add(entry.name);
529
+ const skillMd = path.join(dir, entry.name, 'SKILL.md');
530
+ let description = '';
531
+ if (fs.existsSync(skillMd)) {
532
+ const content = fs.readFileSync(skillMd, 'utf8');
533
+ const match = content.match(/^---\n[\s\S]*?description:\s*(.+)/m);
534
+ if (match)
535
+ description = match[1].trim();
536
+ }
537
+ all.push({ name: entry.name, hasSkillMd: fs.existsSync(skillMd), description });
538
+ }
539
+ }
540
+ return all;
541
+ }
542
+ createSkill(name, cwd, opts) {
543
+ validateName(name);
544
+ const safeCwd = sanitizeCwd(cwd);
545
+ const dir = path.join(safeCwd || os.homedir(), '.claude', 'skills', name);
546
+ fs.mkdirSync(dir, { recursive: true });
547
+ const filePath = path.join(dir, 'SKILL.md');
548
+ let content = '---\n';
549
+ if (opts?.description)
550
+ content += `description: ${opts.description}\n`;
551
+ if (opts?.trigger)
552
+ content += `trigger: ${opts.trigger}\n`;
553
+ content += `---\n\n${opts?.prompt || `# ${name}\n\nSkill instructions here.\n`}\n`;
554
+ fs.writeFileSync(filePath, content);
555
+ return filePath;
556
+ }
557
+ listRules(cwd) {
558
+ const safeCwd = sanitizeCwd(cwd);
559
+ const dirs = [path.join(safeCwd || os.homedir(), '.claude', 'rules'), path.join(os.homedir(), '.claude', 'rules')];
560
+ const all = [];
561
+ const seen = new Set();
562
+ for (const dir of dirs) {
563
+ if (!fs.existsSync(dir))
564
+ continue;
565
+ for (const f of fs.readdirSync(dir).filter((f) => f.endsWith('.md'))) {
566
+ const name = f.replace('.md', '');
567
+ if (seen.has(name))
568
+ continue;
569
+ seen.add(name);
570
+ const content = fs.readFileSync(path.join(dir, f), 'utf8');
571
+ const descMatch = content.match(/^---\n[\s\S]*?description:\s*(.+)/m);
572
+ const pathsMatch = content.match(/^---\n[\s\S]*?paths:\s*(.+)/m);
573
+ const ifMatch = content.match(/^---\n[\s\S]*?if:\s*(.+)/m);
574
+ all.push({
575
+ name,
576
+ file: f,
577
+ description: descMatch?.[1]?.trim() || '',
578
+ paths: pathsMatch?.[1]?.trim() || '',
579
+ condition: ifMatch?.[1]?.trim() || '',
580
+ });
581
+ }
582
+ }
583
+ return all;
584
+ }
585
+ createRule(name, cwd, opts) {
586
+ validateName(name);
587
+ const safeCwd = sanitizeCwd(cwd);
588
+ const dir = path.join(safeCwd || os.homedir(), '.claude', 'rules');
589
+ fs.mkdirSync(dir, { recursive: true });
590
+ const filePath = path.join(dir, `${name}.md`);
591
+ let fileContent = '---\n';
592
+ if (opts?.description)
593
+ fileContent += `description: ${opts.description}\n`;
594
+ if (opts?.paths)
595
+ fileContent += `paths: ${opts.paths}\n`;
596
+ if (opts?.condition)
597
+ fileContent += `if: ${opts.condition}\n`;
598
+ fileContent += `---\n\n${opts?.content || `# ${name}\n\nRule instructions here.\n`}\n`;
599
+ fs.writeFileSync(filePath, fileContent);
600
+ return filePath;
601
+ }
602
+ // โ”€โ”€โ”€ Agent Teams โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
603
+ async teamList(name) {
604
+ const managed = this._getSession(name);
605
+ const engine = managed.config.engine || 'claude';
606
+ // Claude: use native /team command
607
+ if (engine === 'claude') {
608
+ const result = await managed.session.send('/team', { waitForComplete: true, timeout: TEAM_LIST_TIMEOUT_MS });
609
+ return 'text' in result ? result.text : '';
610
+ }
611
+ // Codex/Gemini: list other active sessions as virtual teammates
612
+ const teammates = [];
613
+ for (const [sessionName, m] of this.sessions) {
614
+ if (sessionName === name)
615
+ continue;
616
+ const eng = m.config.engine || 'claude';
617
+ const stats = m.session.getStats();
618
+ const status = m.session.isBusy ? 'busy' : m.session.isPaused ? 'paused' : 'idle';
619
+ teammates.push(`- ${sessionName} (${eng}, ${status}, ${stats.turns} turns)`);
620
+ }
621
+ return teammates.length > 0
622
+ ? `Virtual team (${teammates.length} sessions):\n${teammates.join('\n')}`
623
+ : 'No other active sessions';
624
+ }
625
+ async teamSend(name, teammate, message) {
626
+ const managed = this._getSession(name);
627
+ const engine = managed.config.engine || 'claude';
628
+ // Claude: use native @teammate command
629
+ if (engine === 'claude') {
630
+ managed.lastActivity = Date.now();
631
+ const result = await managed.session.send(`@${teammate} ${message}`, {
632
+ waitForComplete: true,
633
+ timeout: TEAM_SEND_TIMEOUT_MS,
634
+ });
635
+ return {
636
+ output: 'text' in result ? result.text : '',
637
+ sessionId: managed.claudeSessionId,
638
+ events: [],
639
+ };
640
+ }
641
+ // Codex/Gemini: route via cross-session messaging
642
+ if (!this.sessions.has(teammate)) {
643
+ throw new Error(`Target session '${teammate}' not found. Use team_list to see available sessions.`);
644
+ }
645
+ const deliveryResult = await this.sessionSendTo(name, teammate, message, `team message from ${name}`);
646
+ return {
647
+ output: deliveryResult.delivered
648
+ ? `Message delivered to ${teammate}`
649
+ : `Message queued for ${teammate} (session is busy)`,
650
+ sessionId: managed.claudeSessionId,
651
+ events: [],
652
+ };
653
+ }
654
+ // โ”€โ”€โ”€ Health โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
655
+ /**
656
+ * Returns an overview of all active sessions โ€” analogous to a dashboard.
657
+ * Unlike claude_session_status (single session), this gives the aggregate
658
+ * view: how many sessions are running, which are busy, total uptime, etc.
659
+ */
660
+ health() {
661
+ const details = Array.from(this.sessions.entries()).map(([name, managed]) => {
662
+ const stats = managed.session.getStats();
663
+ return {
664
+ name,
665
+ ready: stats.isReady,
666
+ busy: managed.session.isBusy,
667
+ paused: managed.session.isPaused,
668
+ turns: stats.turns,
669
+ costUsd: stats.costUsd,
670
+ contextPercent: stats.contextPercent,
671
+ lastActivity: stats.lastActivity,
672
+ };
673
+ });
674
+ return {
675
+ ok: true,
676
+ version: getPluginVersion(),
677
+ sessions: this.sessions.size,
678
+ sessionNames: Array.from(this.sessions.keys()),
679
+ uptime: process.uptime(),
680
+ details,
681
+ circuitBreakers: this._circuitBreaker.getStatus(),
682
+ };
683
+ }
684
+ /** Return plugin version from package.json */
685
+ getVersion() {
686
+ return getPluginVersion();
687
+ }
688
+ // โ”€โ”€โ”€ Shutdown โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
689
+ /**
690
+ * Gracefully shut down the session manager.
691
+ *
692
+ * 1. Cancels the periodic TTL cleanup timer
693
+ * 2. Stops all ultrareview polling intervals
694
+ * 3. Sends SIGTERM to all active session child processes
695
+ * 4. Persists final session registry to disk
696
+ *
697
+ * After shutdown(), no new sessions can be started. Idempotent.
698
+ */
699
+ async shutdown() {
700
+ if (this.cleanupTimer) {
701
+ clearInterval(this.cleanupTimer);
702
+ this.cleanupTimer = null;
703
+ }
704
+ // Stop ultrareview pollers
705
+ for (const [, timer] of this.ultrareviewPollers)
706
+ clearInterval(timer);
707
+ this.ultrareviewPollers.clear();
708
+ // Stop all sessions
709
+ for (const [name, managed] of this.sessions) {
710
+ try {
711
+ managed.session.stop();
712
+ }
713
+ catch {
714
+ // Best-effort โ€” session may already be dead; must not block cleanup
715
+ }
716
+ this.logger.info(`Stopped session: ${name}`);
717
+ }
718
+ this.sessions.clear();
719
+ // Clear PID tracking
720
+ this._activePids.clear();
721
+ this._savePids();
722
+ // Stop proxy server
723
+ if (this._proxyServer) {
724
+ this._proxyServer.close();
725
+ this._proxyServer = null;
726
+ this._proxyPort = null;
727
+ }
728
+ // Persist final state (TTL-expired sessions already removed by cleanup)
729
+ savePersistedSessions(this.persistedSessions, this.logger);
730
+ }
731
+ // โ”€โ”€โ”€ Auto Proxy โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
732
+ /**
733
+ * Read OpenClaw gateway config from ~/.openclaw/openclaw.json.
734
+ * Returns { url, key } or null if not configured.
735
+ */
736
+ _readGatewayConfig() {
737
+ try {
738
+ const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
739
+ if (!fs.existsSync(configPath))
740
+ return null;
741
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
742
+ const gw = config.gateway;
743
+ if (!gw)
744
+ return null;
745
+ const port = gw.port || 18789;
746
+ const auth = gw.auth;
747
+ // Support both password and token auth modes
748
+ const key = auth?.password || auth?.token || '';
749
+ return { url: `http://127.0.0.1:${port}/v1`, key };
750
+ }
751
+ catch {
752
+ return null;
753
+ }
754
+ }
755
+ /**
756
+ * Start a local proxy server (if not running) that converts Anthropic format
757
+ * to OpenAI format and forwards to the OpenClaw gateway.
758
+ * Returns the proxy port, or null if gateway is not available.
759
+ */
760
+ async _ensureProxyServer() {
761
+ if (this._proxyPort)
762
+ return this._proxyPort;
763
+ // Auto-detect gateway config
764
+ const gwConfig = this._readGatewayConfig();
765
+ const gatewayUrl = getGatewayUrl() || gwConfig?.url;
766
+ const gatewayKey = getGatewayKey() || gwConfig?.key;
767
+ if (!gatewayUrl) {
768
+ this.logger.info('No OpenClaw gateway found โ€” proxy not available');
769
+ return null;
770
+ }
771
+ // Lazy import to avoid circular deps
772
+ const { createProxyHandler } = await import('../proxy/handler.js');
773
+ const proxyHandler = createProxyHandler(undefined, {
774
+ anthropicApiKey: getAnthropicApiKey(),
775
+ openaiApiKey: getOpenaiApiKey(),
776
+ geminiApiKey: getGeminiApiKey(),
777
+ gatewayUrl,
778
+ gatewayKey,
779
+ });
780
+ return new Promise((resolve) => {
781
+ const server = http.createServer((req, res) => {
782
+ let body = '';
783
+ req.on('data', (chunk) => {
784
+ body += chunk.toString();
785
+ });
786
+ req.on('end', () => {
787
+ const httpReq = {
788
+ method: req.method || 'GET',
789
+ url: req.url || '/',
790
+ headers: req.headers,
791
+ json: async () => JSON.parse(body),
792
+ };
793
+ const httpRes = {
794
+ status: (code) => {
795
+ res.statusCode = code;
796
+ return httpRes;
797
+ },
798
+ json: (data) => {
799
+ res.setHeader('Content-Type', 'application/json');
800
+ res.end(JSON.stringify(data));
801
+ },
802
+ setHeader: (k, v) => res.setHeader(k, v),
803
+ write: (data) => res.write(data),
804
+ end: () => res.end(),
805
+ flushHeaders: () => res.flushHeaders(),
806
+ };
807
+ proxyHandler(httpReq, httpRes).catch((err) => {
808
+ res.statusCode = 500;
809
+ res.end(JSON.stringify({ error: err.message }));
810
+ });
811
+ });
812
+ });
813
+ server.listen(0, '127.0.0.1', () => {
814
+ const addr = server.address();
815
+ this._proxyServer = server;
816
+ this._proxyPort = addr.port;
817
+ this.logger.info(`Auto-proxy started on port ${addr.port} (gateway: ${gatewayUrl})`);
818
+ resolve(addr.port);
819
+ });
820
+ server.on('error', (err) => {
821
+ this.logger.error('Failed to start proxy server:', err.message);
822
+ resolve(null);
823
+ });
824
+ });
825
+ }
826
+ // โ”€โ”€โ”€ Private โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
827
+ _persistSession(name, managed) {
828
+ if (!managed.claudeSessionId)
829
+ return;
830
+ const existing = this.persistedSessions.get(name);
831
+ this.persistedSessions.set(name, {
832
+ name,
833
+ claudeSessionId: managed.claudeSessionId,
834
+ cwd: managed.cwd,
835
+ model: managed.config.resolvedModel || managed.config.model,
836
+ engine: managed.config.engine,
837
+ originalCreated: existing?.originalCreated || managed.created,
838
+ lastResumed: new Date().toISOString(),
839
+ lastActivity: managed.lastActivity,
840
+ });
841
+ this._debouncedSave();
842
+ }
843
+ // โ”€โ”€โ”€ PID Tracking โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
844
+ static PID_FILE = path.join(os.homedir(), '.openclaw', 'session-pids.json');
845
+ _savePids() {
846
+ try {
847
+ const dir = path.dirname(SessionManager.PID_FILE);
848
+ if (!fs.existsSync(dir))
849
+ fs.mkdirSync(dir, { recursive: true });
850
+ fs.writeFileSync(SessionManager.PID_FILE, JSON.stringify(Object.fromEntries(this._activePids)));
851
+ }
852
+ catch {
853
+ /* best effort */
854
+ }
855
+ }
856
+ /**
857
+ * Verify that a PID belongs to a known coding CLI before killing it.
858
+ * Prevents killing unrelated processes if the OS recycled the PID.
859
+ */
860
+ _isKnownCliProcess(pid) {
861
+ // Match known CLI binaries by basename to avoid false positives
862
+ // (e.g., 'agent' must not match 'ssh-agent' or 'gpg-agent')
863
+ const knownPatterns = [
864
+ /\bclaude\b/, // claude CLI
865
+ /\bcodex\b/, // codex CLI
866
+ /\bgemini\b/, // gemini CLI
867
+ /\bcursor-agent\b/, // cursor-agent CLI
868
+ /(?:^|\/)agent\s/, // 'agent' as standalone command (not ssh-agent etc.)
869
+ ];
870
+ try {
871
+ const cmd = execFileSync('ps', ['-p', String(pid), '-o', 'command='], {
872
+ encoding: 'utf8',
873
+ timeout: 3_000,
874
+ }).trim();
875
+ return knownPatterns.some((pattern) => pattern.test(cmd));
876
+ }
877
+ catch {
878
+ return false; // ps failed โ€” process likely dead or not accessible
879
+ }
880
+ }
881
+ _cleanupOrphanedPids() {
882
+ try {
883
+ if (!fs.existsSync(SessionManager.PID_FILE))
884
+ return;
885
+ const data = JSON.parse(fs.readFileSync(SessionManager.PID_FILE, 'utf8'));
886
+ for (const [name, pid] of Object.entries(data)) {
887
+ try {
888
+ process.kill(pid, 0); // check if alive
889
+ // Alive โ€” but verify it's actually a coding CLI, not a recycled PID
890
+ if (!this._isKnownCliProcess(pid)) {
891
+ this.logger.info(`PID ${pid} (session: ${name}) is alive but not a known CLI โ€” skipping kill`);
892
+ continue;
893
+ }
894
+ this.logger.info(`Killing orphaned process ${pid} (session: ${name})`);
895
+ // Graceful shutdown: SIGTERM first
896
+ try {
897
+ process.kill(-pid, 'SIGTERM');
898
+ }
899
+ catch {
900
+ /* group kill failed */
901
+ }
902
+ try {
903
+ process.kill(pid, 'SIGTERM');
904
+ }
905
+ catch {
906
+ /* individual kill failed */
907
+ }
908
+ // Give process time to shut down, then SIGKILL
909
+ setTimeout(() => {
910
+ try {
911
+ process.kill(pid, 0);
912
+ process.kill(-pid, 'SIGKILL');
913
+ }
914
+ catch {
915
+ /* already dead or group kill failed */
916
+ }
917
+ try {
918
+ process.kill(pid, 0);
919
+ process.kill(pid, 'SIGKILL');
920
+ }
921
+ catch {
922
+ /* already dead */
923
+ }
924
+ }, STOP_SIGKILL_DELAY_MS);
925
+ }
926
+ catch {
927
+ // Process already dead โ€” nothing to do
928
+ }
929
+ }
930
+ }
931
+ catch {
932
+ /* file doesn't exist or parse error */
933
+ }
934
+ // Clear the PID file
935
+ this._savePids();
936
+ }
937
+ // Circuit breaker is delegated to this._circuitBreaker (src/circuit-breaker.ts)
938
+ _getSession(name) {
939
+ const managed = this.sessions.get(name);
940
+ if (!managed)
941
+ throw new Error(`Session '${name}' not found`);
942
+ return managed;
943
+ }
944
+ _toSessionInfo(name, managed) {
945
+ const stats = managed.session.getStats();
946
+ return {
947
+ name,
948
+ claudeSessionId: managed.claudeSessionId,
949
+ created: managed.created,
950
+ cwd: managed.cwd,
951
+ model: managed.config.resolvedModel || managed.config.model,
952
+ paused: false,
953
+ stats,
954
+ };
955
+ }
956
+ _resolveModel(alias, overrides) {
957
+ if (overrides?.[alias])
958
+ return overrides[alias];
959
+ return resolveAlias(alias);
960
+ }
961
+ _listMdFiles(dir) {
962
+ if (!fs.existsSync(dir))
963
+ return [];
964
+ return fs
965
+ .readdirSync(dir)
966
+ .filter((f) => f.endsWith('.md'))
967
+ .map((f) => {
968
+ const content = fs.readFileSync(path.join(dir, f), 'utf8');
969
+ const match = content.match(/^---\n[\s\S]*?description:\s*(.+)/m);
970
+ return { name: f.replace('.md', ''), file: f, description: match?.[1]?.trim() || '' };
971
+ });
972
+ }
973
+ _createSession(engine, config) {
974
+ switch (engine) {
975
+ case 'gemini':
976
+ return new PersistentGeminiSession(config, getGeminiBin());
977
+ case 'codex':
978
+ return new PersistentCodexSession(config, getCodexBin());
979
+ case 'cursor':
980
+ return new PersistentCursorSession(config, getCursorBin());
981
+ case 'custom':
982
+ if (!config.customEngine)
983
+ throw new Error('customEngine config is required for engine type "custom"');
984
+ return new PersistentCustomSession(config);
985
+ case 'claude':
986
+ default:
987
+ return new PersistentClaudeSession(config, this.pluginConfig.claudeBin);
988
+ }
989
+ }
990
+ // โ”€โ”€โ”€ Council โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
991
+ councils = new Map();
992
+ councilCleanupTimers = new Map();
993
+ councilStart(task, config) {
994
+ const council = new Council(config, this, this.logger);
995
+ const initialSession = council.init(task);
996
+ // Store BEFORE running so council_status/abort/inject work while it's active
997
+ this.councils.set(initialSession.id, council);
998
+ // Run in background โ€” callers poll via councilStatus()
999
+ council
1000
+ .run()
1001
+ .then(() => {
1002
+ // Keep completed council queryable; schedule cleanup after TTL
1003
+ this._scheduleCouncilCleanup(initialSession.id);
1004
+ })
1005
+ .catch((err) => {
1006
+ this.logger.error(`Council ${initialSession.id} failed:`, err);
1007
+ this._scheduleCouncilCleanup(initialSession.id);
1008
+ });
1009
+ return initialSession;
1010
+ }
1011
+ _scheduleCouncilCleanup(id) {
1012
+ // Clear any existing timer before scheduling a new one
1013
+ const existing = this.councilCleanupTimers.get(id);
1014
+ if (existing)
1015
+ clearTimeout(existing);
1016
+ const timer = setTimeout(() => {
1017
+ // Abort if still running to prevent orphaned background tasks
1018
+ const council = this.councils.get(id);
1019
+ if (council) {
1020
+ const session = council.getSession();
1021
+ if (session?.status === 'running') {
1022
+ this.logger.info(`Council ${id} still running at TTL expiry โ€” aborting`);
1023
+ council.abort();
1024
+ }
1025
+ }
1026
+ this.councils.delete(id);
1027
+ this.councilCleanupTimers.delete(id);
1028
+ }, RESULT_TTL_MS);
1029
+ this.councilCleanupTimers.set(id, timer);
1030
+ }
1031
+ councilStatus(id) {
1032
+ const council = this.councils.get(id);
1033
+ return council?.getSession();
1034
+ }
1035
+ councilAbort(id) {
1036
+ const council = this.councils.get(id);
1037
+ if (!council)
1038
+ throw new Error(`Council '${id}' not found`);
1039
+ council.abort();
1040
+ this.councils.delete(id);
1041
+ }
1042
+ councilInject(id, message) {
1043
+ const council = this.councils.get(id);
1044
+ if (!council)
1045
+ throw new Error(`Council '${id}' not found`);
1046
+ council.injectMessage(message);
1047
+ }
1048
+ async councilReview(id) {
1049
+ const council = this.councils.get(id);
1050
+ if (!council)
1051
+ throw new Error(`Council '${id}' not found`);
1052
+ this._scheduleCouncilCleanup(id); // reset TTL โ€” user is actively reviewing
1053
+ return council.review();
1054
+ }
1055
+ async councilAccept(id) {
1056
+ const council = this.councils.get(id);
1057
+ if (!council)
1058
+ throw new Error(`Council '${id}' not found`);
1059
+ const result = await council.accept();
1060
+ // Accepted โ€” no longer needed, clean up after short grace period
1061
+ this._scheduleCouncilCleanup(id);
1062
+ return result;
1063
+ }
1064
+ async councilReject(id, feedback) {
1065
+ const council = this.councils.get(id);
1066
+ if (!council)
1067
+ throw new Error(`Council '${id}' not found`);
1068
+ const result = await council.reject(feedback);
1069
+ this._scheduleCouncilCleanup(id); // reset TTL โ€” council may be restarted
1070
+ return result;
1071
+ }
1072
+ // โ”€โ”€โ”€ Inbox (cross-session messaging) โ€” delegated to InboxManager โ”€โ”€โ”€โ”€
1073
+ get _sessionLookup() {
1074
+ return {
1075
+ getSession: (name) => this.sessions.get(name),
1076
+ exists: (name) => this.sessions.has(name),
1077
+ allNames: () => this.sessions.keys(),
1078
+ };
1079
+ }
1080
+ async sessionSendTo(from, to, message, summary) {
1081
+ return this._inbox.sendTo(from, to, message, this._sessionLookup, summary, (name, err) => {
1082
+ this.logger.error(`Broadcast delivery to '${name}' failed:`, err.message);
1083
+ });
1084
+ }
1085
+ sessionInbox(name, unreadOnly = true) {
1086
+ return this._inbox.inbox(name, unreadOnly);
1087
+ }
1088
+ async sessionDeliverInbox(name) {
1089
+ return this._inbox.deliverInbox(name, this._sessionLookup);
1090
+ }
1091
+ // โ”€โ”€โ”€ Ultraplan โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1092
+ ultraplans = new Map();
1093
+ ultraplanStart(task, opts) {
1094
+ const id = `ultraplan-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
1095
+ const sessionName = `ultraplan-${id}`;
1096
+ const timeout = opts?.timeout || ULTRAPLAN_TIMEOUT_MS;
1097
+ const result = {
1098
+ id,
1099
+ status: 'running',
1100
+ sessionName,
1101
+ startTime: new Date().toISOString(),
1102
+ };
1103
+ this.ultraplans.set(id, result);
1104
+ // Run in background
1105
+ this._runUltraplan(id, sessionName, task, opts?.model || 'opus', opts?.cwd || process.cwd(), timeout)
1106
+ .catch((err) => {
1107
+ result.status = 'error';
1108
+ result.error = err.message;
1109
+ result.endTime = new Date().toISOString();
1110
+ })
1111
+ .finally(() => {
1112
+ // Cleanup session
1113
+ this.stopSession(sessionName).catch((err) => {
1114
+ this.logger.error(`Failed to stop ultraplan session '${sessionName}':`, err);
1115
+ });
1116
+ setTimeout(() => {
1117
+ // Mark as error if still running at TTL expiry
1118
+ const plan = this.ultraplans.get(id);
1119
+ if (plan?.status === 'running') {
1120
+ this.logger.info(`Ultraplan ${id} still running at TTL expiry โ€” marking as error`);
1121
+ plan.status = 'error';
1122
+ plan.error = 'Timed out (TTL expired)';
1123
+ plan.endTime = new Date().toISOString();
1124
+ }
1125
+ this.ultraplans.delete(id);
1126
+ }, RESULT_TTL_MS);
1127
+ });
1128
+ return result;
1129
+ }
1130
+ async _runUltraplan(id, sessionName, task, model, cwd, timeout) {
1131
+ const result = this.ultraplans.get(id);
1132
+ await this.startSession({
1133
+ name: sessionName,
1134
+ cwd,
1135
+ model,
1136
+ permissionMode: 'plan',
1137
+ effort: 'max',
1138
+ appendSystemPrompt: 'You are in ultraplan mode. Explore the project thoroughly, analyze feasibility, and produce a detailed, actionable plan. Do NOT write code โ€” plan only. Output your final plan in a clear markdown format.',
1139
+ });
1140
+ const planPrompt = `# Ultraplan Task\n\n${task}\n\nExplore the project, understand the codebase, analyze feasibility, and produce a comprehensive implementation plan. Take your time (up to 30 minutes). Be thorough.`;
1141
+ const sendResult = await this.sendMessage(sessionName, planPrompt, { timeout });
1142
+ // Detect error responses: empty output or output that looks like an error message
1143
+ const output = sendResult.output?.trim() || '';
1144
+ const looksLikeError = !output ||
1145
+ /^(Error|not logged in|authentication|auth failed|permission denied)/i.test(output) ||
1146
+ (sendResult.error && sendResult.error.length > 0);
1147
+ if (looksLikeError) {
1148
+ result.status = 'error';
1149
+ result.error = sendResult.error || output || 'Empty response from engine';
1150
+ }
1151
+ else {
1152
+ result.plan = output;
1153
+ result.status = 'completed';
1154
+ }
1155
+ result.endTime = new Date().toISOString();
1156
+ }
1157
+ ultraplanStatus(id) {
1158
+ return this.ultraplans.get(id);
1159
+ }
1160
+ // โ”€โ”€โ”€ Ultrareview โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1161
+ ultrareviews = new Map();
1162
+ ultrareviewPollers = new Map();
1163
+ ultrareviewStart(cwd, opts) {
1164
+ const id = `ultrareview-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
1165
+ const agentCount = Math.min(20, Math.max(1, opts?.agentCount || 5));
1166
+ const result = {
1167
+ id,
1168
+ status: 'running',
1169
+ councilId: '',
1170
+ agentCount,
1171
+ startTime: new Date().toISOString(),
1172
+ };
1173
+ this.ultrareviews.set(id, result);
1174
+ // Build reviewer agents
1175
+ const reviewAngles = [
1176
+ {
1177
+ name: 'SecurityReviewer',
1178
+ emoji: '๐Ÿ”’',
1179
+ persona: 'You are a security expert. Focus on: injection vulnerabilities, auth flaws, data exposure, OWASP top 10, secrets in code.',
1180
+ },
1181
+ {
1182
+ name: 'LogicReviewer',
1183
+ emoji: '๐Ÿง ',
1184
+ persona: 'You are a logic analyst. Focus on: off-by-one errors, race conditions, null/undefined handling, edge cases, incorrect assumptions.',
1185
+ },
1186
+ {
1187
+ name: 'PerformanceReviewer',
1188
+ emoji: 'โšก',
1189
+ persona: 'You are a performance engineer. Focus on: O(n^2) loops, memory leaks, unnecessary allocations, missing caching, N+1 queries.',
1190
+ },
1191
+ {
1192
+ name: 'APIReviewer',
1193
+ emoji: '๐Ÿ”Œ',
1194
+ persona: 'You are an API design reviewer. Focus on: inconsistent interfaces, missing validation, error handling gaps, backwards compatibility.',
1195
+ },
1196
+ {
1197
+ name: 'TestReviewer',
1198
+ emoji: '๐Ÿงช',
1199
+ persona: 'You are a test coverage analyst. Focus on: untested code paths, missing edge case tests, flaky test patterns, assertion quality.',
1200
+ },
1201
+ {
1202
+ name: 'TypeReviewer',
1203
+ emoji: '๐Ÿ“',
1204
+ persona: 'You are a type safety reviewer. Focus on: any casts, unsafe assertions, missing null checks, generic misuse, type narrowing gaps.',
1205
+ },
1206
+ {
1207
+ name: 'ConcurrencyReviewer',
1208
+ emoji: '๐Ÿ”€',
1209
+ persona: 'You are a concurrency expert. Focus on: race conditions, deadlocks, shared state mutations, async error handling, promise leaks.',
1210
+ },
1211
+ {
1212
+ name: 'ErrorReviewer',
1213
+ emoji: '๐Ÿ’ฅ',
1214
+ persona: 'You are an error handling reviewer. Focus on: swallowed errors, missing try/catch, unhelpful error messages, crash-on-startup paths.',
1215
+ },
1216
+ {
1217
+ name: 'DependencyReviewer',
1218
+ emoji: '๐Ÿ“ฆ',
1219
+ persona: 'You are a dependency auditor. Focus on: outdated packages, known CVEs, unnecessary dependencies, license issues.',
1220
+ },
1221
+ {
1222
+ name: 'ReadabilityReviewer',
1223
+ emoji: '๐Ÿ“–',
1224
+ persona: 'You are a readability reviewer. Focus on: unclear naming, complex functions, missing context, dead code, confusing control flow.',
1225
+ },
1226
+ {
1227
+ name: 'DataReviewer',
1228
+ emoji: '๐Ÿ’พ',
1229
+ persona: 'You are a data integrity reviewer. Focus on: data validation, schema mismatches, migration issues, encoding problems, data loss paths.',
1230
+ },
1231
+ {
1232
+ name: 'ConfigReviewer',
1233
+ emoji: 'โš™๏ธ',
1234
+ persona: 'You are a configuration reviewer. Focus on: hardcoded values, missing env vars, insecure defaults, missing fallbacks.',
1235
+ },
1236
+ {
1237
+ name: 'ScalabilityReviewer',
1238
+ emoji: '๐Ÿ“ˆ',
1239
+ persona: 'You are a scalability reviewer. Focus on: single points of failure, stateful bottlenecks, missing pagination, unbounded growth.',
1240
+ },
1241
+ {
1242
+ name: 'DocReviewer',
1243
+ emoji: '๐Ÿ“',
1244
+ persona: 'You are a documentation reviewer. Focus on: outdated docs, missing API docs, misleading comments, undocumented behavior.',
1245
+ },
1246
+ {
1247
+ name: 'A11yReviewer',
1248
+ emoji: 'โ™ฟ',
1249
+ persona: 'You are an accessibility reviewer. Focus on: missing ARIA labels, keyboard navigation, color contrast, screen reader support.',
1250
+ },
1251
+ {
1252
+ name: 'I18nReviewer',
1253
+ emoji: '๐ŸŒ',
1254
+ persona: 'You are an i18n reviewer. Focus on: hardcoded strings, locale handling, date/number formatting, RTL support.',
1255
+ },
1256
+ {
1257
+ name: 'NetworkReviewer',
1258
+ emoji: '๐ŸŒ',
1259
+ persona: 'You are a network reviewer. Focus on: missing timeouts, retry logic, connection pooling, request size limits.',
1260
+ },
1261
+ {
1262
+ name: 'AuthReviewer',
1263
+ emoji: '๐Ÿ”‘',
1264
+ persona: 'You are an auth reviewer. Focus on: token handling, session management, CSRF protection, permission checks.',
1265
+ },
1266
+ {
1267
+ name: 'CryptoReviewer',
1268
+ emoji: '๐Ÿ”',
1269
+ persona: 'You are a cryptography reviewer. Focus on: weak algorithms, key management, random number generation, hash collisions.',
1270
+ },
1271
+ {
1272
+ name: 'MemoryReviewer',
1273
+ emoji: '๐Ÿงน',
1274
+ persona: 'You are a memory reviewer. Focus on: memory leaks, circular references, large object retention, stream handling.',
1275
+ },
1276
+ ];
1277
+ const agents = reviewAngles.slice(0, agentCount).map((a) => ({
1278
+ ...a,
1279
+ model: opts?.model,
1280
+ }));
1281
+ const maxMinutes = Math.min(25, Math.max(5, opts?.maxDurationMinutes || 10));
1282
+ const focus = opts?.focus || 'Find bugs, security issues, and code quality problems';
1283
+ const councilConfig = {
1284
+ name: 'ultrareview',
1285
+ agents,
1286
+ maxRounds: 2, // Review doesn't need many rounds โ€” find bugs, then synthesize
1287
+ projectDir: cwd,
1288
+ agentTimeoutMs: maxMinutes * 60 * 1000,
1289
+ maxTurnsPerAgent: 20,
1290
+ };
1291
+ const councilSession = this.councilStart(`# Code Review Task\n\nReview the codebase in this project. ${focus}.\n\nEach reviewer: examine the code from your specialty angle, report bugs found with file paths and line numbers. Vote [CONSENSUS: YES] when your review is complete.`, councilConfig);
1292
+ result.councilId = councilSession.id;
1293
+ // Poll council for completion (store ref for shutdown cleanup)
1294
+ const pollInterval = setInterval(() => {
1295
+ try {
1296
+ const status = this.councilStatus(councilSession.id);
1297
+ if (!status || status.status === 'running')
1298
+ return;
1299
+ clearInterval(pollInterval);
1300
+ this.ultrareviewPollers.delete(id);
1301
+ result.status = status.status === 'error' ? 'error' : 'completed';
1302
+ result.endTime = new Date().toISOString();
1303
+ // Synthesize findings from all agent responses
1304
+ if (status.responses.length > 0) {
1305
+ result.findings = status.responses.map((r) => `## ${r.agent}\n\n${r.content}`).join('\n\n---\n\n');
1306
+ }
1307
+ setTimeout(() => this.ultrareviews.delete(id), RESULT_TTL_MS);
1308
+ }
1309
+ catch {
1310
+ // Council may have been cleaned up; stop polling
1311
+ clearInterval(pollInterval);
1312
+ this.ultrareviewPollers.delete(id);
1313
+ }
1314
+ }, ULTRAREVIEW_POLL_INTERVAL_MS);
1315
+ this.ultrareviewPollers.set(id, pollInterval);
1316
+ return result;
1317
+ }
1318
+ ultrareviewStatus(id) {
1319
+ return this.ultrareviews.get(id);
1320
+ }
1321
+ _cleanupIdleSessions() {
1322
+ const ttlMs = this.pluginConfig.sessionTtlMinutes * 60_000;
1323
+ const now = Date.now();
1324
+ for (const [name, managed] of this.sessions) {
1325
+ if (now - managed.lastActivity > ttlMs) {
1326
+ this.logger.info(`Cleaning up idle in-memory session: ${name}`);
1327
+ try {
1328
+ managed.session.stop();
1329
+ }
1330
+ catch {
1331
+ // Best-effort โ€” session may already be dead; must not block TTL cleanup
1332
+ }
1333
+ this.sessions.delete(name);
1334
+ // NOTE: do NOT delete from persistedSessions โ€” idle cleanup is
1335
+ // in-memory only. Persisted entries survive for PERSIST_DISK_TTL_MS
1336
+ // (7 days) so the session can be resumed after a gateway restart.
1337
+ }
1338
+ }
1339
+ // Prune disk entries that exceeded the longer disk TTL
1340
+ let pruned = false;
1341
+ for (const [name, entry] of this.persistedSessions) {
1342
+ if (now - entry.lastActivity > PERSIST_DISK_TTL_MS) {
1343
+ this.persistedSessions.delete(name);
1344
+ pruned = true;
1345
+ }
1346
+ }
1347
+ if (pruned)
1348
+ savePersistedSessionsAsync(this.persistedSessions);
1349
+ }
1350
+ }
1351
+ //# sourceMappingURL=session-manager.js.map