@roj-ai/sdk 0.1.35 → 0.2.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 (908) hide show
  1. package/dist/bootstrap.d.ts +420 -26
  2. package/dist/bootstrap.d.ts.map +1 -1
  3. package/dist/bootstrap.js +55 -24
  4. package/dist/bootstrap.js.map +1 -1
  5. package/dist/bun-platform/fs.d.ts.map +1 -1
  6. package/dist/bun-platform/fs.js +1 -0
  7. package/dist/bun-platform/fs.js.map +1 -1
  8. package/dist/bun-platform/index.d.ts +4 -1
  9. package/dist/bun-platform/index.d.ts.map +1 -1
  10. package/dist/bun-platform/index.js +7 -1
  11. package/dist/bun-platform/index.js.map +1 -1
  12. package/dist/bun-platform/server-lifecycle.d.ts +30 -0
  13. package/dist/bun-platform/server-lifecycle.d.ts.map +1 -0
  14. package/dist/bun-platform/server-lifecycle.js +182 -0
  15. package/dist/bun-platform/server-lifecycle.js.map +1 -0
  16. package/dist/bun-platform/shell.d.ts +33 -0
  17. package/dist/bun-platform/shell.d.ts.map +1 -0
  18. package/dist/bun-platform/shell.js +322 -0
  19. package/dist/bun-platform/shell.js.map +1 -0
  20. package/dist/config.d.ts +34 -0
  21. package/dist/config.d.ts.map +1 -1
  22. package/dist/config.js +97 -0
  23. package/dist/config.js.map +1 -1
  24. package/dist/core/agents/agent.d.ts +42 -7
  25. package/dist/core/agents/agent.d.ts.map +1 -1
  26. package/dist/core/agents/agent.js +209 -90
  27. package/dist/core/agents/agent.js.map +1 -1
  28. package/dist/core/agents/config.d.ts +8 -1
  29. package/dist/core/agents/config.d.ts.map +1 -1
  30. package/dist/core/agents/config.js +5 -1
  31. package/dist/core/agents/config.js.map +1 -1
  32. package/dist/core/agents/retry.d.ts +1 -1
  33. package/dist/core/agents/retry.d.ts.map +1 -1
  34. package/dist/core/agents/retry.js +14 -16
  35. package/dist/core/agents/retry.js.map +1 -1
  36. package/dist/core/agents/state.d.ts +27 -1
  37. package/dist/core/agents/state.d.ts.map +1 -1
  38. package/dist/core/agents/state.js +1 -1
  39. package/dist/core/agents/state.js.map +1 -1
  40. package/dist/core/context/state.d.ts +3 -3
  41. package/dist/core/context/state.d.ts.map +1 -1
  42. package/dist/core/context/state.js +6 -2
  43. package/dist/core/context/state.js.map +1 -1
  44. package/dist/core/events/base-event-store.d.ts.map +1 -1
  45. package/dist/core/events/base-event-store.js +22 -14
  46. package/dist/core/events/base-event-store.js.map +1 -1
  47. package/dist/core/events/file.d.ts +39 -1
  48. package/dist/core/events/file.d.ts.map +1 -1
  49. package/dist/core/events/file.js +159 -20
  50. package/dist/core/events/file.js.map +1 -1
  51. package/dist/core/events/types.d.ts +1 -1
  52. package/dist/core/file-store/containment.d.ts +19 -0
  53. package/dist/core/file-store/containment.d.ts.map +1 -0
  54. package/dist/core/file-store/containment.js +62 -0
  55. package/dist/core/file-store/containment.js.map +1 -0
  56. package/dist/core/file-store/file-store.d.ts +22 -2
  57. package/dist/core/file-store/file-store.d.ts.map +1 -1
  58. package/dist/core/file-store/file-store.js +88 -20
  59. package/dist/core/file-store/file-store.js.map +1 -1
  60. package/dist/core/file-store/types.d.ts +13 -1
  61. package/dist/core/file-store/types.d.ts.map +1 -1
  62. package/dist/core/image/image-processor.d.ts.map +1 -1
  63. package/dist/core/image/image-processor.js +3 -1
  64. package/dist/core/image/image-processor.js.map +1 -1
  65. package/dist/core/image/types.d.ts +2 -0
  66. package/dist/core/image/types.d.ts.map +1 -1
  67. package/dist/core/image/vips-resizer.d.ts.map +1 -1
  68. package/dist/core/image/vips-resizer.js +81 -33
  69. package/dist/core/image/vips-resizer.js.map +1 -1
  70. package/dist/core/llm/anthropic.d.ts +0 -1
  71. package/dist/core/llm/anthropic.d.ts.map +1 -1
  72. package/dist/core/llm/anthropic.js +101 -74
  73. package/dist/core/llm/anthropic.js.map +1 -1
  74. package/dist/core/llm/logger.d.ts +14 -2
  75. package/dist/core/llm/logger.d.ts.map +1 -1
  76. package/dist/core/llm/logger.js +161 -36
  77. package/dist/core/llm/logger.js.map +1 -1
  78. package/dist/core/llm/openrouter.d.ts +0 -1
  79. package/dist/core/llm/openrouter.d.ts.map +1 -1
  80. package/dist/core/llm/openrouter.js +70 -74
  81. package/dist/core/llm/openrouter.js.map +1 -1
  82. package/dist/core/llm/provider-request.d.ts +10 -0
  83. package/dist/core/llm/provider-request.d.ts.map +1 -0
  84. package/dist/core/llm/provider-request.js +41 -0
  85. package/dist/core/llm/provider-request.js.map +1 -0
  86. package/dist/core/llm/provider.d.ts +45 -3
  87. package/dist/core/llm/provider.d.ts.map +1 -1
  88. package/dist/core/llm/provider.js +26 -0
  89. package/dist/core/llm/provider.js.map +1 -1
  90. package/dist/core/llm/state.d.ts +4 -2
  91. package/dist/core/llm/state.d.ts.map +1 -1
  92. package/dist/core/llm/state.js +15 -4
  93. package/dist/core/llm/state.js.map +1 -1
  94. package/dist/core/plugins/plugin-builder.d.ts +57 -3
  95. package/dist/core/plugins/plugin-builder.d.ts.map +1 -1
  96. package/dist/core/plugins/plugin-builder.js +68 -14
  97. package/dist/core/plugins/plugin-builder.js.map +1 -1
  98. package/dist/core/preset/preset-builder.d.ts +3 -2
  99. package/dist/core/preset/preset-builder.d.ts.map +1 -1
  100. package/dist/core/preset/preset-builder.js +7 -16
  101. package/dist/core/preset/preset-builder.js.map +1 -1
  102. package/dist/core/sessions/context.d.ts +23 -5
  103. package/dist/core/sessions/context.d.ts.map +1 -1
  104. package/dist/core/sessions/runtime-activity.d.ts +33 -0
  105. package/dist/core/sessions/runtime-activity.d.ts.map +1 -0
  106. package/dist/core/sessions/runtime-activity.js +55 -0
  107. package/dist/core/sessions/runtime-activity.js.map +1 -0
  108. package/dist/core/sessions/schema.d.ts +23 -2
  109. package/dist/core/sessions/schema.d.ts.map +1 -1
  110. package/dist/core/sessions/schema.js +24 -3
  111. package/dist/core/sessions/schema.js.map +1 -1
  112. package/dist/core/sessions/session-manager.d.ts +83 -6
  113. package/dist/core/sessions/session-manager.d.ts.map +1 -1
  114. package/dist/core/sessions/session-manager.js +517 -118
  115. package/dist/core/sessions/session-manager.js.map +1 -1
  116. package/dist/core/sessions/session-store.d.ts +18 -0
  117. package/dist/core/sessions/session-store.d.ts.map +1 -1
  118. package/dist/core/sessions/session-store.js +33 -0
  119. package/dist/core/sessions/session-store.js.map +1 -1
  120. package/dist/core/sessions/session.d.ts +42 -6
  121. package/dist/core/sessions/session.d.ts.map +1 -1
  122. package/dist/core/sessions/session.js +176 -44
  123. package/dist/core/sessions/session.js.map +1 -1
  124. package/dist/core/sessions/state.d.ts +10 -1
  125. package/dist/core/sessions/state.d.ts.map +1 -1
  126. package/dist/core/sessions/state.js +16 -1
  127. package/dist/core/sessions/state.js.map +1 -1
  128. package/dist/core/system.d.ts +9 -0
  129. package/dist/core/system.d.ts.map +1 -1
  130. package/dist/core/system.js +4 -1
  131. package/dist/core/system.js.map +1 -1
  132. package/dist/core/tools/executor.d.ts +1 -1
  133. package/dist/core/wake-key.d.ts +34 -0
  134. package/dist/core/wake-key.d.ts.map +1 -0
  135. package/dist/core/wake-key.js +51 -0
  136. package/dist/core/wake-key.js.map +1 -0
  137. package/dist/index.d.ts +24 -13
  138. package/dist/index.d.ts.map +1 -1
  139. package/dist/index.js +22 -8
  140. package/dist/index.js.map +1 -1
  141. package/dist/lib/archive/archive-inspection.d.ts +48 -0
  142. package/dist/lib/archive/archive-inspection.d.ts.map +1 -0
  143. package/dist/lib/archive/archive-inspection.fixtures.d.ts +7 -0
  144. package/dist/lib/archive/archive-inspection.fixtures.d.ts.map +1 -0
  145. package/dist/lib/archive/archive-inspection.fixtures.js +134 -0
  146. package/dist/lib/archive/archive-inspection.fixtures.js.map +1 -0
  147. package/dist/lib/archive/archive-inspection.js +253 -0
  148. package/dist/lib/archive/archive-inspection.js.map +1 -0
  149. package/dist/lib/archive/index.d.ts +3 -0
  150. package/dist/lib/archive/index.d.ts.map +1 -0
  151. package/dist/lib/archive/index.js +2 -0
  152. package/dist/lib/archive/index.js.map +1 -0
  153. package/dist/lib/logger/console.d.ts +5 -12
  154. package/dist/lib/logger/console.d.ts.map +1 -1
  155. package/dist/lib/logger/console.js +5 -47
  156. package/dist/lib/logger/console.js.map +1 -1
  157. package/dist/lib/logger/file.d.ts +8 -11
  158. package/dist/lib/logger/file.d.ts.map +1 -1
  159. package/dist/lib/logger/file.js +26 -37
  160. package/dist/lib/logger/file.js.map +1 -1
  161. package/dist/lib/logger/index.d.ts +3 -1
  162. package/dist/lib/logger/index.d.ts.map +1 -1
  163. package/dist/lib/logger/index.js +3 -1
  164. package/dist/lib/logger/index.js.map +1 -1
  165. package/dist/lib/logger/jsonl.d.ts +21 -0
  166. package/dist/lib/logger/jsonl.d.ts.map +1 -0
  167. package/dist/lib/logger/jsonl.js +29 -0
  168. package/dist/lib/logger/jsonl.js.map +1 -0
  169. package/dist/lib/logger/logger.d.ts +22 -0
  170. package/dist/lib/logger/logger.d.ts.map +1 -1
  171. package/dist/lib/logger/logger.js +37 -0
  172. package/dist/lib/logger/logger.js.map +1 -1
  173. package/dist/lib/logger/session-log.d.ts +21 -0
  174. package/dist/lib/logger/session-log.d.ts.map +1 -0
  175. package/dist/lib/logger/session-log.js +37 -0
  176. package/dist/lib/logger/session-log.js.map +1 -0
  177. package/dist/lib/utils/concurrency.d.ts +1 -1
  178. package/dist/lib/utils/concurrency.d.ts.map +1 -1
  179. package/dist/lib/utils/concurrency.js +30 -7
  180. package/dist/lib/utils/concurrency.js.map +1 -1
  181. package/dist/lib/utils/fs-batch.d.ts +17 -0
  182. package/dist/lib/utils/fs-batch.d.ts.map +1 -0
  183. package/dist/lib/utils/fs-batch.js +38 -0
  184. package/dist/lib/utils/fs-batch.js.map +1 -0
  185. package/dist/lib/utils/result.d.ts +7 -22
  186. package/dist/lib/utils/result.d.ts.map +1 -1
  187. package/dist/lib/utils/result.js +6 -17
  188. package/dist/lib/utils/result.js.map +1 -1
  189. package/dist/lib/utils/sleep.d.ts +10 -0
  190. package/dist/lib/utils/sleep.d.ts.map +1 -0
  191. package/dist/lib/utils/sleep.js +26 -0
  192. package/dist/lib/utils/sleep.js.map +1 -0
  193. package/dist/platform/fs-revision.d.ts +21 -0
  194. package/dist/platform/fs-revision.d.ts.map +1 -0
  195. package/dist/platform/fs-revision.js +9 -0
  196. package/dist/platform/fs-revision.js.map +1 -0
  197. package/dist/platform/fs.d.ts +104 -1
  198. package/dist/platform/fs.d.ts.map +1 -1
  199. package/dist/platform/git.d.ts +59 -0
  200. package/dist/platform/git.d.ts.map +1 -0
  201. package/dist/platform/git.js +8 -0
  202. package/dist/platform/git.js.map +1 -0
  203. package/dist/platform/index.d.ts +40 -1
  204. package/dist/platform/index.d.ts.map +1 -1
  205. package/dist/platform/index.js +1 -1
  206. package/dist/platform/index.js.map +1 -1
  207. package/dist/platform/llm-call-log.d.ts +80 -0
  208. package/dist/platform/llm-call-log.d.ts.map +1 -0
  209. package/dist/platform/llm-call-log.js +12 -0
  210. package/dist/platform/llm-call-log.js.map +1 -0
  211. package/dist/platform/scheduler.d.ts +32 -0
  212. package/dist/platform/scheduler.d.ts.map +1 -0
  213. package/dist/platform/scheduler.js +46 -0
  214. package/dist/platform/scheduler.js.map +1 -0
  215. package/dist/platform/session-log.d.ts +36 -0
  216. package/dist/platform/session-log.d.ts.map +1 -0
  217. package/dist/platform/session-log.js +12 -0
  218. package/dist/platform/session-log.js.map +1 -0
  219. package/dist/platform/shell.d.ts +83 -0
  220. package/dist/platform/shell.d.ts.map +1 -0
  221. package/dist/platform/shell.js +13 -0
  222. package/dist/platform/shell.js.map +1 -0
  223. package/dist/plugins/agent-status/plugin.d.ts.map +1 -1
  224. package/dist/plugins/agent-status/plugin.js +1 -0
  225. package/dist/plugins/agent-status/plugin.js.map +1 -1
  226. package/dist/plugins/agents/plugin.d.ts.map +1 -1
  227. package/dist/plugins/agents/plugin.js +129 -62
  228. package/dist/plugins/agents/plugin.js.map +1 -1
  229. package/dist/plugins/filesystem/listing.d.ts +3 -0
  230. package/dist/plugins/filesystem/listing.d.ts.map +1 -1
  231. package/dist/plugins/filesystem/listing.js +66 -10
  232. package/dist/plugins/filesystem/listing.js.map +1 -1
  233. package/dist/plugins/filesystem/plugin.d.ts.map +1 -1
  234. package/dist/plugins/filesystem/plugin.js +8 -6
  235. package/dist/plugins/filesystem/plugin.js.map +1 -1
  236. package/dist/plugins/git-status/plugin.d.ts +34 -7
  237. package/dist/plugins/git-status/plugin.d.ts.map +1 -1
  238. package/dist/plugins/git-status/plugin.js +168 -64
  239. package/dist/plugins/git-status/plugin.js.map +1 -1
  240. package/dist/plugins/llm-debug/plugin.d.ts.map +1 -1
  241. package/dist/plugins/llm-debug/plugin.js +1 -0
  242. package/dist/plugins/llm-debug/plugin.js.map +1 -1
  243. package/dist/plugins/logs/plugin.d.ts +1 -4
  244. package/dist/plugins/logs/plugin.d.ts.map +1 -1
  245. package/dist/plugins/logs/plugin.js +7 -0
  246. package/dist/plugins/logs/plugin.js.map +1 -1
  247. package/dist/plugins/mailbox/helpers.d.ts +2 -2
  248. package/dist/plugins/mailbox/helpers.js +1 -1
  249. package/dist/plugins/mailbox/plugin.d.ts.map +1 -1
  250. package/dist/plugins/mailbox/plugin.js +38 -43
  251. package/dist/plugins/mailbox/plugin.js.map +1 -1
  252. package/dist/plugins/mailbox/query.d.ts +7 -2
  253. package/dist/plugins/mailbox/query.d.ts.map +1 -1
  254. package/dist/plugins/mailbox/query.js +14 -7
  255. package/dist/plugins/mailbox/query.js.map +1 -1
  256. package/dist/plugins/mailbox/state.d.ts +5 -4
  257. package/dist/plugins/mailbox/state.d.ts.map +1 -1
  258. package/dist/plugins/mailbox/state.js +6 -5
  259. package/dist/plugins/mailbox/state.js.map +1 -1
  260. package/dist/plugins/resources/filename.d.ts +4 -0
  261. package/dist/plugins/resources/filename.d.ts.map +1 -0
  262. package/dist/plugins/resources/filename.js +18 -0
  263. package/dist/plugins/resources/filename.js.map +1 -0
  264. package/dist/plugins/resources/plugin.d.ts +3 -0
  265. package/dist/plugins/resources/plugin.d.ts.map +1 -1
  266. package/dist/plugins/resources/plugin.js +97 -60
  267. package/dist/plugins/resources/plugin.js.map +1 -1
  268. package/dist/plugins/resources/state.d.ts +1 -1
  269. package/dist/plugins/resources/state.js +1 -1
  270. package/dist/plugins/result-eviction/plugin.d.ts.map +1 -1
  271. package/dist/plugins/result-eviction/plugin.js +6 -1
  272. package/dist/plugins/result-eviction/plugin.js.map +1 -1
  273. package/dist/plugins/services/plugin.d.ts +4 -1
  274. package/dist/plugins/services/plugin.d.ts.map +1 -1
  275. package/dist/plugins/services/plugin.js +251 -100
  276. package/dist/plugins/services/plugin.js.map +1 -1
  277. package/dist/plugins/services/prompt.d.ts.map +1 -1
  278. package/dist/plugins/services/prompt.js +1 -4
  279. package/dist/plugins/services/prompt.js.map +1 -1
  280. package/dist/plugins/services/schema.d.ts +15 -5
  281. package/dist/plugins/services/schema.d.ts.map +1 -1
  282. package/dist/plugins/services/service.d.ts +100 -18
  283. package/dist/plugins/services/service.d.ts.map +1 -1
  284. package/dist/plugins/services/service.js +680 -230
  285. package/dist/plugins/services/service.js.map +1 -1
  286. package/dist/plugins/session-lifecycle/plugin.d.ts +21 -1
  287. package/dist/plugins/session-lifecycle/plugin.d.ts.map +1 -1
  288. package/dist/plugins/session-lifecycle/plugin.js +53 -40
  289. package/dist/plugins/session-lifecycle/plugin.js.map +1 -1
  290. package/dist/plugins/session-state/plugin.d.ts +21 -13
  291. package/dist/plugins/session-state/plugin.d.ts.map +1 -1
  292. package/dist/plugins/session-state/plugin.js +214 -79
  293. package/dist/plugins/session-state/plugin.js.map +1 -1
  294. package/dist/plugins/session-stats/plugin.d.ts.map +1 -1
  295. package/dist/plugins/session-stats/plugin.js +1 -0
  296. package/dist/plugins/session-stats/plugin.js.map +1 -1
  297. package/dist/plugins/shell/executor.d.ts +17 -15
  298. package/dist/plugins/shell/executor.d.ts.map +1 -1
  299. package/dist/plugins/shell/executor.js +130 -195
  300. package/dist/plugins/shell/executor.js.map +1 -1
  301. package/dist/plugins/shell/index.d.ts +2 -2
  302. package/dist/plugins/shell/index.d.ts.map +1 -1
  303. package/dist/plugins/shell/index.js +2 -2
  304. package/dist/plugins/shell/index.js.map +1 -1
  305. package/dist/plugins/shell/plugin.d.ts +5 -4
  306. package/dist/plugins/shell/plugin.d.ts.map +1 -1
  307. package/dist/plugins/shell/plugin.js +4 -8
  308. package/dist/plugins/shell/plugin.js.map +1 -1
  309. package/dist/plugins/skills/discovery.d.ts.map +1 -1
  310. package/dist/plugins/skills/discovery.js +10 -8
  311. package/dist/plugins/skills/discovery.js.map +1 -1
  312. package/dist/plugins/skills/index.d.ts +1 -1
  313. package/dist/plugins/skills/index.d.ts.map +1 -1
  314. package/dist/plugins/skills/plugin.d.ts +1 -1
  315. package/dist/plugins/skills/plugin.d.ts.map +1 -1
  316. package/dist/plugins/skills/plugin.js +23 -30
  317. package/dist/plugins/skills/plugin.js.map +1 -1
  318. package/dist/plugins/skills/prompts.d.ts +3 -3
  319. package/dist/plugins/skills/prompts.d.ts.map +1 -1
  320. package/dist/plugins/skills/prompts.js.map +1 -1
  321. package/dist/plugins/skills/schema.d.ts +4 -2
  322. package/dist/plugins/skills/schema.d.ts.map +1 -1
  323. package/dist/plugins/todo/schema.d.ts +1 -1
  324. package/dist/plugins/uploads/plugin.d.ts +11 -4
  325. package/dist/plugins/uploads/plugin.d.ts.map +1 -1
  326. package/dist/plugins/uploads/plugin.js +891 -225
  327. package/dist/plugins/uploads/plugin.js.map +1 -1
  328. package/dist/plugins/uploads/preprocessor.d.ts +14 -0
  329. package/dist/plugins/uploads/preprocessor.d.ts.map +1 -1
  330. package/dist/plugins/uploads/preprocessor.js +11 -0
  331. package/dist/plugins/uploads/preprocessor.js.map +1 -1
  332. package/dist/plugins/uploads/preprocessors/image-classifier.d.ts +1 -1
  333. package/dist/plugins/uploads/preprocessors/image-classifier.d.ts.map +1 -1
  334. package/dist/plugins/uploads/preprocessors/image-classifier.js +51 -26
  335. package/dist/plugins/uploads/preprocessors/image-classifier.js.map +1 -1
  336. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.d.ts +2 -2
  337. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.d.ts.map +1 -1
  338. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.js +41 -16
  339. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.js.map +1 -1
  340. package/dist/plugins/uploads/preprocessors/pdf-preprocessor.d.ts +1 -1
  341. package/dist/plugins/uploads/preprocessors/pdf-preprocessor.d.ts.map +1 -1
  342. package/dist/plugins/uploads/preprocessors/pdf-preprocessor.js +29 -14
  343. package/dist/plugins/uploads/preprocessors/pdf-preprocessor.js.map +1 -1
  344. package/dist/plugins/uploads/preprocessors/zip-preprocessor.d.ts +1 -1
  345. package/dist/plugins/uploads/preprocessors/zip-preprocessor.d.ts.map +1 -1
  346. package/dist/plugins/uploads/preprocessors/zip-preprocessor.js +62 -34
  347. package/dist/plugins/uploads/preprocessors/zip-preprocessor.js.map +1 -1
  348. package/dist/plugins/uploads/schema.d.ts +25 -2
  349. package/dist/plugins/uploads/schema.d.ts.map +1 -1
  350. package/dist/plugins/uploads/schema.js +24 -3
  351. package/dist/plugins/uploads/schema.js.map +1 -1
  352. package/dist/plugins/uploads/state.d.ts +13 -1
  353. package/dist/plugins/uploads/state.d.ts.map +1 -1
  354. package/dist/plugins/uploads/state.js +4 -1
  355. package/dist/plugins/uploads/state.js.map +1 -1
  356. package/dist/plugins/user-chat/plugin.d.ts +12 -5
  357. package/dist/plugins/user-chat/plugin.d.ts.map +1 -1
  358. package/dist/plugins/user-chat/plugin.js +141 -32
  359. package/dist/plugins/user-chat/plugin.js.map +1 -1
  360. package/dist/plugins/workers/context.d.ts +35 -13
  361. package/dist/plugins/workers/context.d.ts.map +1 -1
  362. package/dist/plugins/workers/context.js +82 -32
  363. package/dist/plugins/workers/context.js.map +1 -1
  364. package/dist/plugins/workers/definition.d.ts +7 -0
  365. package/dist/plugins/workers/definition.d.ts.map +1 -1
  366. package/dist/plugins/workers/definition.js.map +1 -1
  367. package/dist/plugins/workers/index.d.ts +4 -4
  368. package/dist/plugins/workers/index.d.ts.map +1 -1
  369. package/dist/plugins/workers/index.js +2 -2
  370. package/dist/plugins/workers/index.js.map +1 -1
  371. package/dist/plugins/workers/plugin.d.ts +10 -57
  372. package/dist/plugins/workers/plugin.d.ts.map +1 -1
  373. package/dist/plugins/workers/plugin.js +385 -123
  374. package/dist/plugins/workers/plugin.js.map +1 -1
  375. package/dist/plugins/workers/state.d.ts +69 -0
  376. package/dist/plugins/workers/state.d.ts.map +1 -0
  377. package/dist/plugins/workers/state.js +52 -0
  378. package/dist/plugins/workers/state.js.map +1 -0
  379. package/dist/plugins/workers/worker.d.ts +15 -4
  380. package/dist/plugins/workers/worker.d.ts.map +1 -1
  381. package/dist/testing/conformance.d.ts +106 -0
  382. package/dist/testing/conformance.d.ts.map +1 -0
  383. package/dist/testing/conformance.js +1463 -0
  384. package/dist/testing/conformance.js.map +1 -0
  385. package/dist/testing/node-platform.d.ts.map +1 -1
  386. package/dist/testing/node-platform.js +5 -0
  387. package/dist/testing/node-platform.js.map +1 -1
  388. package/dist/testing/test-harness.d.ts +11 -1
  389. package/dist/testing/test-harness.d.ts.map +1 -1
  390. package/dist/testing/test-harness.js +17 -37
  391. package/dist/testing/test-harness.js.map +1 -1
  392. package/dist/testing/wait-helpers.d.ts.map +1 -1
  393. package/dist/testing/wait-helpers.js +1 -3
  394. package/dist/testing/wait-helpers.js.map +1 -1
  395. package/dist/transport/adapter/client-adapter.d.ts +6 -0
  396. package/dist/transport/adapter/client-adapter.d.ts.map +1 -1
  397. package/dist/transport/adapter/client-adapter.js +50 -3
  398. package/dist/transport/adapter/client-adapter.js.map +1 -1
  399. package/dist/transport/adapter/heartbeat.d.ts +16 -0
  400. package/dist/transport/adapter/heartbeat.d.ts.map +1 -0
  401. package/dist/transport/adapter/heartbeat.js +41 -0
  402. package/dist/transport/adapter/heartbeat.js.map +1 -0
  403. package/dist/transport/adapter/server-adapter.d.ts.map +1 -1
  404. package/dist/transport/adapter/server-adapter.js +6 -0
  405. package/dist/transport/adapter/server-adapter.js.map +1 -1
  406. package/dist/transport/http/app.d.ts +3 -30
  407. package/dist/transport/http/app.d.ts.map +1 -1
  408. package/dist/transport/http/app.js +3 -7
  409. package/dist/transport/http/app.js.map +1 -1
  410. package/dist/transport/http/context.d.ts +40 -0
  411. package/dist/transport/http/context.d.ts.map +1 -0
  412. package/dist/transport/http/context.js +17 -0
  413. package/dist/transport/http/context.js.map +1 -0
  414. package/dist/transport/http/fetch-guard.d.ts +49 -0
  415. package/dist/transport/http/fetch-guard.d.ts.map +1 -0
  416. package/dist/transport/http/fetch-guard.js +312 -0
  417. package/dist/transport/http/fetch-guard.js.map +1 -0
  418. package/dist/transport/http/index.d.ts +1 -1
  419. package/dist/transport/http/index.d.ts.map +1 -1
  420. package/dist/transport/http/middleware/bearer-auth.d.ts +1 -1
  421. package/dist/transport/http/middleware/bearer-auth.d.ts.map +1 -1
  422. package/dist/transport/http/middleware/error-handler.d.ts +2 -2
  423. package/dist/transport/http/middleware/error-handler.d.ts.map +1 -1
  424. package/dist/transport/http/path-containment.d.ts +20 -0
  425. package/dist/transport/http/path-containment.d.ts.map +1 -0
  426. package/dist/transport/http/path-containment.js +31 -0
  427. package/dist/transport/http/path-containment.js.map +1 -0
  428. package/dist/transport/http/responses.d.ts +30 -0
  429. package/dist/transport/http/responses.d.ts.map +1 -0
  430. package/dist/transport/http/responses.js +5 -0
  431. package/dist/transport/http/responses.js.map +1 -0
  432. package/dist/transport/http/routes/files.d.ts +1 -1
  433. package/dist/transport/http/routes/files.d.ts.map +1 -1
  434. package/dist/transport/http/routes/files.js +54 -155
  435. package/dist/transport/http/routes/files.js.map +1 -1
  436. package/dist/transport/http/routes/resources.d.ts +1 -1
  437. package/dist/transport/http/routes/resources.d.ts.map +1 -1
  438. package/dist/transport/http/routes/resources.js +92 -57
  439. package/dist/transport/http/routes/resources.js.map +1 -1
  440. package/dist/transport/http/routes/rpc.d.ts +6 -1
  441. package/dist/transport/http/routes/rpc.d.ts.map +1 -1
  442. package/dist/transport/http/routes/rpc.js +51 -21
  443. package/dist/transport/http/routes/rpc.js.map +1 -1
  444. package/dist/transport/http/routes/upload.d.ts +1 -1
  445. package/dist/transport/http/routes/upload.d.ts.map +1 -1
  446. package/dist/transport/http/routes/upload.js +256 -169
  447. package/dist/transport/http/routes/upload.js.map +1 -1
  448. package/dist/user-config.d.ts +34 -7
  449. package/dist/user-config.d.ts.map +1 -1
  450. package/dist/user-config.js +71 -0
  451. package/dist/user-config.js.map +1 -1
  452. package/package.json +18 -8
  453. package/src/bootstrap.ts +119 -35
  454. package/src/bun-platform/fs.ts +1 -0
  455. package/src/bun-platform/index.ts +18 -1
  456. package/src/bun-platform/server-lifecycle.ts +218 -0
  457. package/src/bun-platform/shell.ts +362 -0
  458. package/src/config.ts +130 -0
  459. package/src/core/agents/agent.ts +219 -95
  460. package/src/core/agents/config.ts +8 -0
  461. package/src/core/agents/retry.ts +16 -17
  462. package/src/core/agents/state.ts +28 -0
  463. package/src/core/context/state.ts +4 -0
  464. package/src/core/events/base-event-store.ts +24 -16
  465. package/src/core/events/file.ts +198 -22
  466. package/src/core/file-store/containment.ts +75 -0
  467. package/src/core/file-store/file-store.ts +106 -25
  468. package/src/core/file-store/types.ts +14 -1
  469. package/src/core/image/image-processor.ts +3 -1
  470. package/src/core/image/types.ts +2 -0
  471. package/src/core/image/vips-resizer.ts +102 -37
  472. package/src/core/llm/anthropic.ts +127 -82
  473. package/src/core/llm/logger.ts +175 -37
  474. package/src/core/llm/openrouter.ts +82 -81
  475. package/src/core/llm/provider-request.ts +40 -0
  476. package/src/core/llm/provider.ts +72 -3
  477. package/src/core/llm/state.ts +12 -0
  478. package/src/core/plugins/plugin-builder.ts +106 -14
  479. package/src/core/preset/preset-builder.ts +12 -22
  480. package/src/core/sessions/context.ts +18 -0
  481. package/src/core/sessions/runtime-activity.ts +78 -0
  482. package/src/core/sessions/schema.ts +29 -3
  483. package/src/core/sessions/session-manager.ts +560 -140
  484. package/src/core/sessions/session-store.ts +38 -0
  485. package/src/core/sessions/session.ts +187 -47
  486. package/src/core/sessions/state.ts +16 -0
  487. package/src/core/system.ts +15 -0
  488. package/src/core/wake-key.ts +65 -0
  489. package/src/index.ts +31 -12
  490. package/src/lib/archive/archive-inspection.fixtures.ts +138 -0
  491. package/src/lib/archive/archive-inspection.ts +359 -0
  492. package/src/lib/archive/index.ts +17 -0
  493. package/src/lib/logger/console.ts +7 -57
  494. package/src/lib/logger/file.ts +28 -50
  495. package/src/lib/logger/index.ts +3 -1
  496. package/src/lib/logger/jsonl.ts +36 -0
  497. package/src/lib/logger/logger.ts +48 -0
  498. package/src/lib/logger/session-log.ts +42 -0
  499. package/src/lib/utils/concurrency.ts +37 -8
  500. package/src/lib/utils/fs-batch.ts +49 -0
  501. package/src/lib/utils/result.ts +7 -31
  502. package/src/lib/utils/sleep.ts +23 -0
  503. package/src/platform/fs-revision.ts +21 -0
  504. package/src/platform/fs.ts +109 -1
  505. package/src/platform/git.ts +67 -0
  506. package/src/platform/index.ts +51 -1
  507. package/src/platform/llm-call-log.ts +86 -0
  508. package/src/platform/scheduler.ts +71 -0
  509. package/src/platform/session-log.ts +39 -0
  510. package/src/platform/shell.ts +89 -0
  511. package/src/plugins/agent-status/plugin.ts +1 -0
  512. package/src/plugins/agents/plugin.ts +134 -81
  513. package/src/plugins/filesystem/listing.ts +70 -12
  514. package/src/plugins/filesystem/plugin.ts +8 -6
  515. package/src/plugins/git-status/plugin.ts +204 -77
  516. package/src/plugins/llm-debug/plugin.ts +1 -0
  517. package/src/plugins/logs/plugin.ts +9 -0
  518. package/src/plugins/mailbox/plugin.ts +46 -64
  519. package/src/plugins/mailbox/query.ts +15 -7
  520. package/src/plugins/mailbox/state.ts +1 -0
  521. package/src/plugins/resources/filename.ts +19 -0
  522. package/src/plugins/resources/plugin.ts +111 -59
  523. package/src/plugins/result-eviction/plugin.ts +6 -1
  524. package/src/plugins/services/plugin.ts +254 -98
  525. package/src/plugins/services/prompt.ts +1 -6
  526. package/src/plugins/services/schema.ts +17 -3
  527. package/src/plugins/services/service.ts +702 -252
  528. package/src/plugins/session-lifecycle/plugin.ts +56 -41
  529. package/src/plugins/session-state/plugin.ts +242 -88
  530. package/src/plugins/session-stats/plugin.ts +1 -0
  531. package/src/plugins/shell/executor.ts +144 -226
  532. package/src/plugins/shell/index.ts +1 -4
  533. package/src/plugins/shell/plugin.ts +7 -9
  534. package/src/plugins/skills/discovery.ts +10 -8
  535. package/src/plugins/skills/index.ts +1 -1
  536. package/src/plugins/skills/plugin.ts +19 -27
  537. package/src/plugins/skills/prompts.ts +3 -3
  538. package/src/plugins/skills/schema.ts +5 -2
  539. package/src/plugins/uploads/plugin.ts +991 -273
  540. package/src/plugins/uploads/preprocessor.ts +25 -0
  541. package/src/plugins/uploads/preprocessors/image-classifier.ts +56 -25
  542. package/src/plugins/uploads/preprocessors/markitdown-preprocessor.ts +41 -15
  543. package/src/plugins/uploads/preprocessors/pdf-preprocessor.ts +32 -16
  544. package/src/plugins/uploads/preprocessors/zip-preprocessor.ts +67 -38
  545. package/src/plugins/uploads/schema.ts +30 -3
  546. package/src/plugins/uploads/state.ts +12 -0
  547. package/src/plugins/user-chat/plugin.ts +196 -40
  548. package/src/plugins/workers/context.ts +102 -39
  549. package/src/plugins/workers/definition.ts +7 -0
  550. package/src/plugins/workers/index.ts +4 -4
  551. package/src/plugins/workers/plugin.ts +461 -151
  552. package/src/plugins/workers/state.ts +64 -0
  553. package/src/plugins/workers/worker.ts +15 -4
  554. package/src/testing/conformance.ts +1751 -0
  555. package/src/testing/node-platform.ts +5 -0
  556. package/src/testing/test-harness.ts +21 -38
  557. package/src/testing/wait-helpers.ts +1 -4
  558. package/src/transport/adapter/client-adapter.ts +60 -3
  559. package/src/transport/adapter/heartbeat.ts +50 -0
  560. package/src/transport/adapter/server-adapter.ts +6 -0
  561. package/src/transport/http/app.ts +7 -35
  562. package/src/transport/http/context.ts +46 -0
  563. package/src/transport/http/fetch-guard.ts +318 -0
  564. package/src/transport/http/index.ts +1 -1
  565. package/src/transport/http/middleware/bearer-auth.ts +1 -1
  566. package/src/transport/http/middleware/error-handler.ts +1 -1
  567. package/src/transport/http/path-containment.ts +44 -0
  568. package/src/transport/http/responses.ts +20 -0
  569. package/src/transport/http/routes/files.ts +69 -159
  570. package/src/transport/http/routes/resources.ts +124 -93
  571. package/src/transport/http/routes/rpc.ts +63 -26
  572. package/src/transport/http/routes/upload.ts +327 -258
  573. package/src/user-config.ts +93 -7
  574. package/dist/config.test.d.ts +0 -2
  575. package/dist/config.test.d.ts.map +0 -1
  576. package/dist/config.test.js +0 -155
  577. package/dist/config.test.js.map +0 -1
  578. package/dist/core/agent-loop.integration.test.d.ts +0 -2
  579. package/dist/core/agent-loop.integration.test.d.ts.map +0 -1
  580. package/dist/core/agent-loop.integration.test.js +0 -414
  581. package/dist/core/agent-loop.integration.test.js.map +0 -1
  582. package/dist/core/agents/agent-config.test.d.ts +0 -2
  583. package/dist/core/agents/agent-config.test.d.ts.map +0 -1
  584. package/dist/core/agents/agent-config.test.js +0 -194
  585. package/dist/core/agents/agent-config.test.js.map +0 -1
  586. package/dist/core/agents/agent-shutdown.test.d.ts +0 -6
  587. package/dist/core/agents/agent-shutdown.test.d.ts.map +0 -1
  588. package/dist/core/agents/agent-shutdown.test.js +0 -180
  589. package/dist/core/agents/agent-shutdown.test.js.map +0 -1
  590. package/dist/core/agents/agent.test.d.ts +0 -2
  591. package/dist/core/agents/agent.test.d.ts.map +0 -1
  592. package/dist/core/agents/agent.test.js +0 -313
  593. package/dist/core/agents/agent.test.js.map +0 -1
  594. package/dist/core/agents/communicator.d.ts +0 -3
  595. package/dist/core/agents/communicator.d.ts.map +0 -1
  596. package/dist/core/agents/communicator.js +0 -13
  597. package/dist/core/agents/communicator.js.map +0 -1
  598. package/dist/core/agents/handler-events.test.d.ts +0 -5
  599. package/dist/core/agents/handler-events.test.d.ts.map +0 -1
  600. package/dist/core/agents/handler-events.test.js +0 -115
  601. package/dist/core/agents/handler-events.test.js.map +0 -1
  602. package/dist/core/agents/response-sanitizer.test.d.ts +0 -2
  603. package/dist/core/agents/response-sanitizer.test.d.ts.map +0 -1
  604. package/dist/core/agents/response-sanitizer.test.js +0 -101
  605. package/dist/core/agents/response-sanitizer.test.js.map +0 -1
  606. package/dist/core/error-recovery.integration.test.d.ts +0 -2
  607. package/dist/core/error-recovery.integration.test.d.ts.map +0 -1
  608. package/dist/core/error-recovery.integration.test.js +0 -288
  609. package/dist/core/error-recovery.integration.test.js.map +0 -1
  610. package/dist/core/event-sourcing.integration.test.d.ts +0 -2
  611. package/dist/core/event-sourcing.integration.test.d.ts.map +0 -1
  612. package/dist/core/event-sourcing.integration.test.js +0 -154
  613. package/dist/core/event-sourcing.integration.test.js.map +0 -1
  614. package/dist/core/events/file.test.d.ts +0 -2
  615. package/dist/core/events/file.test.d.ts.map +0 -1
  616. package/dist/core/events/file.test.js +0 -284
  617. package/dist/core/events/file.test.js.map +0 -1
  618. package/dist/core/events/memory.test.d.ts +0 -2
  619. package/dist/core/events/memory.test.d.ts.map +0 -1
  620. package/dist/core/events/memory.test.js +0 -502
  621. package/dist/core/events/memory.test.js.map +0 -1
  622. package/dist/core/image/image-processor.test.d.ts +0 -2
  623. package/dist/core/image/image-processor.test.d.ts.map +0 -1
  624. package/dist/core/image/image-processor.test.js +0 -171
  625. package/dist/core/image/image-processor.test.js.map +0 -1
  626. package/dist/core/image/vips-resizer.test.d.ts +0 -2
  627. package/dist/core/image/vips-resizer.test.d.ts.map +0 -1
  628. package/dist/core/image/vips-resizer.test.js +0 -336
  629. package/dist/core/image/vips-resizer.test.js.map +0 -1
  630. package/dist/core/llm/anthropic.test.d.ts +0 -2
  631. package/dist/core/llm/anthropic.test.d.ts.map +0 -1
  632. package/dist/core/llm/anthropic.test.js +0 -464
  633. package/dist/core/llm/anthropic.test.js.map +0 -1
  634. package/dist/core/llm/cache-breakpoints.test.d.ts +0 -2
  635. package/dist/core/llm/cache-breakpoints.test.d.ts.map +0 -1
  636. package/dist/core/llm/cache-breakpoints.test.js +0 -69
  637. package/dist/core/llm/cache-breakpoints.test.js.map +0 -1
  638. package/dist/core/llm/cache-live.test.d.ts +0 -16
  639. package/dist/core/llm/cache-live.test.d.ts.map +0 -1
  640. package/dist/core/llm/cache-live.test.js +0 -137
  641. package/dist/core/llm/cache-live.test.js.map +0 -1
  642. package/dist/core/llm/logger.test.d.ts +0 -7
  643. package/dist/core/llm/logger.test.d.ts.map +0 -1
  644. package/dist/core/llm/logger.test.js +0 -228
  645. package/dist/core/llm/logger.test.js.map +0 -1
  646. package/dist/core/llm/message-sanitization.test.d.ts +0 -2
  647. package/dist/core/llm/message-sanitization.test.d.ts.map +0 -1
  648. package/dist/core/llm/message-sanitization.test.js +0 -181
  649. package/dist/core/llm/message-sanitization.test.js.map +0 -1
  650. package/dist/core/llm/mock.test.d.ts +0 -2
  651. package/dist/core/llm/mock.test.d.ts.map +0 -1
  652. package/dist/core/llm/mock.test.js +0 -318
  653. package/dist/core/llm/mock.test.js.map +0 -1
  654. package/dist/core/llm/openrouter-mapping.test.d.ts +0 -2
  655. package/dist/core/llm/openrouter-mapping.test.d.ts.map +0 -1
  656. package/dist/core/llm/openrouter-mapping.test.js +0 -172
  657. package/dist/core/llm/openrouter-mapping.test.js.map +0 -1
  658. package/dist/core/llm/openrouter.test.d.ts +0 -2
  659. package/dist/core/llm/openrouter.test.d.ts.map +0 -1
  660. package/dist/core/llm/openrouter.test.js +0 -377
  661. package/dist/core/llm/openrouter.test.js.map +0 -1
  662. package/dist/core/llm/provider-integration.test.d.ts +0 -12
  663. package/dist/core/llm/provider-integration.test.d.ts.map +0 -1
  664. package/dist/core/llm/provider-integration.test.js +0 -350
  665. package/dist/core/llm/provider-integration.test.js.map +0 -1
  666. package/dist/core/llm/routing-provider.test.d.ts +0 -2
  667. package/dist/core/llm/routing-provider.test.d.ts.map +0 -1
  668. package/dist/core/llm/routing-provider.test.js +0 -94
  669. package/dist/core/llm/routing-provider.test.js.map +0 -1
  670. package/dist/core/llm/snapshot-middleware.test.d.ts +0 -2
  671. package/dist/core/llm/snapshot-middleware.test.d.ts.map +0 -1
  672. package/dist/core/llm/snapshot-middleware.test.js +0 -144
  673. package/dist/core/llm/snapshot-middleware.test.js.map +0 -1
  674. package/dist/core/multi-agent.integration.test.d.ts +0 -2
  675. package/dist/core/multi-agent.integration.test.d.ts.map +0 -1
  676. package/dist/core/multi-agent.integration.test.js +0 -298
  677. package/dist/core/multi-agent.integration.test.js.map +0 -1
  678. package/dist/core/plugin-hooks.integration.test.d.ts +0 -2
  679. package/dist/core/plugin-hooks.integration.test.d.ts.map +0 -1
  680. package/dist/core/plugin-hooks.integration.test.js +0 -458
  681. package/dist/core/plugin-hooks.integration.test.js.map +0 -1
  682. package/dist/core/session-lifecycle.integration.test.d.ts +0 -2
  683. package/dist/core/session-lifecycle.integration.test.d.ts.map +0 -1
  684. package/dist/core/session-lifecycle.integration.test.js +0 -159
  685. package/dist/core/session-lifecycle.integration.test.js.map +0 -1
  686. package/dist/core/sessions/fork-utils.test.d.ts +0 -2
  687. package/dist/core/sessions/fork-utils.test.d.ts.map +0 -1
  688. package/dist/core/sessions/fork-utils.test.js +0 -129
  689. package/dist/core/sessions/fork-utils.test.js.map +0 -1
  690. package/dist/core/sessions/session-overrides.integration.test.d.ts +0 -2
  691. package/dist/core/sessions/session-overrides.integration.test.d.ts.map +0 -1
  692. package/dist/core/sessions/session-overrides.integration.test.js +0 -154
  693. package/dist/core/sessions/session-overrides.integration.test.js.map +0 -1
  694. package/dist/core/sessions/session.test.d.ts +0 -2
  695. package/dist/core/sessions/session.test.d.ts.map +0 -1
  696. package/dist/core/sessions/session.test.js +0 -1100
  697. package/dist/core/sessions/session.test.js.map +0 -1
  698. package/dist/core/tools/executor.test.d.ts +0 -2
  699. package/dist/core/tools/executor.test.d.ts.map +0 -1
  700. package/dist/core/tools/executor.test.js +0 -144
  701. package/dist/core/tools/executor.test.js.map +0 -1
  702. package/dist/lib/logger/console.test.d.ts +0 -2
  703. package/dist/lib/logger/console.test.d.ts.map +0 -1
  704. package/dist/lib/logger/console.test.js +0 -258
  705. package/dist/lib/logger/console.test.js.map +0 -1
  706. package/dist/lib/never.d.ts +0 -2
  707. package/dist/lib/never.d.ts.map +0 -1
  708. package/dist/lib/never.js +0 -4
  709. package/dist/lib/never.js.map +0 -1
  710. package/dist/lib/utils/concurrency.test.d.ts +0 -2
  711. package/dist/lib/utils/concurrency.test.d.ts.map +0 -1
  712. package/dist/lib/utils/concurrency.test.js +0 -135
  713. package/dist/lib/utils/concurrency.test.js.map +0 -1
  714. package/dist/plugins/agent-status/agent-status.test.d.ts +0 -2
  715. package/dist/plugins/agent-status/agent-status.test.d.ts.map +0 -1
  716. package/dist/plugins/agent-status/agent-status.test.js +0 -136
  717. package/dist/plugins/agent-status/agent-status.test.js.map +0 -1
  718. package/dist/plugins/agents/agents.integration.test.d.ts +0 -2
  719. package/dist/plugins/agents/agents.integration.test.d.ts.map +0 -1
  720. package/dist/plugins/agents/agents.integration.test.js +0 -830
  721. package/dist/plugins/agents/agents.integration.test.js.map +0 -1
  722. package/dist/plugins/agents/supervision.integration.test.d.ts +0 -2
  723. package/dist/plugins/agents/supervision.integration.test.d.ts.map +0 -1
  724. package/dist/plugins/agents/supervision.integration.test.js +0 -215
  725. package/dist/plugins/agents/supervision.integration.test.js.map +0 -1
  726. package/dist/plugins/context-compact/compaction-live.test.d.ts +0 -17
  727. package/dist/plugins/context-compact/compaction-live.test.d.ts.map +0 -1
  728. package/dist/plugins/context-compact/compaction-live.test.js +0 -177
  729. package/dist/plugins/context-compact/compaction-live.test.js.map +0 -1
  730. package/dist/plugins/context-compact/context-compact.integration.test.d.ts +0 -2
  731. package/dist/plugins/context-compact/context-compact.integration.test.d.ts.map +0 -1
  732. package/dist/plugins/context-compact/context-compact.integration.test.js +0 -364
  733. package/dist/plugins/context-compact/context-compact.integration.test.js.map +0 -1
  734. package/dist/plugins/context-compact/context-compactor.test.d.ts +0 -2
  735. package/dist/plugins/context-compact/context-compactor.test.d.ts.map +0 -1
  736. package/dist/plugins/context-compact/context-compactor.test.js +0 -858
  737. package/dist/plugins/context-compact/context-compactor.test.js.map +0 -1
  738. package/dist/plugins/context-compact/history-offloader.test.d.ts +0 -2
  739. package/dist/plugins/context-compact/history-offloader.test.d.ts.map +0 -1
  740. package/dist/plugins/context-compact/history-offloader.test.js +0 -77
  741. package/dist/plugins/context-compact/history-offloader.test.js.map +0 -1
  742. package/dist/plugins/filesystem/filesystem.integration.test.d.ts +0 -2
  743. package/dist/plugins/filesystem/filesystem.integration.test.d.ts.map +0 -1
  744. package/dist/plugins/filesystem/filesystem.integration.test.js +0 -447
  745. package/dist/plugins/filesystem/filesystem.integration.test.js.map +0 -1
  746. package/dist/plugins/filesystem/schema.d.ts +0 -7
  747. package/dist/plugins/filesystem/schema.d.ts.map +0 -1
  748. package/dist/plugins/filesystem/schema.js +0 -2
  749. package/dist/plugins/filesystem/schema.js.map +0 -1
  750. package/dist/plugins/limits-guard/limit-guard.test.d.ts +0 -2
  751. package/dist/plugins/limits-guard/limit-guard.test.d.ts.map +0 -1
  752. package/dist/plugins/limits-guard/limit-guard.test.js +0 -194
  753. package/dist/plugins/limits-guard/limit-guard.test.js.map +0 -1
  754. package/dist/plugins/limits-guard/limits-guard.integration.test.d.ts +0 -2
  755. package/dist/plugins/limits-guard/limits-guard.integration.test.d.ts.map +0 -1
  756. package/dist/plugins/limits-guard/limits-guard.integration.test.js +0 -887
  757. package/dist/plugins/limits-guard/limits-guard.integration.test.js.map +0 -1
  758. package/dist/plugins/llm-debug/llm-debug.integration.test.d.ts +0 -2
  759. package/dist/plugins/llm-debug/llm-debug.integration.test.d.ts.map +0 -1
  760. package/dist/plugins/llm-debug/llm-debug.integration.test.js +0 -157
  761. package/dist/plugins/llm-debug/llm-debug.integration.test.js.map +0 -1
  762. package/dist/plugins/mailbox/mailbox.integration.test.d.ts +0 -2
  763. package/dist/plugins/mailbox/mailbox.integration.test.d.ts.map +0 -1
  764. package/dist/plugins/mailbox/mailbox.integration.test.js +0 -678
  765. package/dist/plugins/mailbox/mailbox.integration.test.js.map +0 -1
  766. package/dist/plugins/resources/resources.integration.test.d.ts +0 -2
  767. package/dist/plugins/resources/resources.integration.test.d.ts.map +0 -1
  768. package/dist/plugins/resources/resources.integration.test.js +0 -62
  769. package/dist/plugins/resources/resources.integration.test.js.map +0 -1
  770. package/dist/plugins/result-eviction/result-eviction.integration.test.d.ts +0 -2
  771. package/dist/plugins/result-eviction/result-eviction.integration.test.d.ts.map +0 -1
  772. package/dist/plugins/result-eviction/result-eviction.integration.test.js +0 -294
  773. package/dist/plugins/result-eviction/result-eviction.integration.test.js.map +0 -1
  774. package/dist/plugins/services/pid-registry.test.d.ts +0 -2
  775. package/dist/plugins/services/pid-registry.test.d.ts.map +0 -1
  776. package/dist/plugins/services/pid-registry.test.js +0 -161
  777. package/dist/plugins/services/pid-registry.test.js.map +0 -1
  778. package/dist/plugins/services/services.integration.test.d.ts +0 -2
  779. package/dist/plugins/services/services.integration.test.d.ts.map +0 -1
  780. package/dist/plugins/services/services.integration.test.js +0 -1057
  781. package/dist/plugins/services/services.integration.test.js.map +0 -1
  782. package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.d.ts +0 -2
  783. package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.d.ts.map +0 -1
  784. package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.js +0 -498
  785. package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.js.map +0 -1
  786. package/dist/plugins/session-stats/plugin.test.d.ts +0 -2
  787. package/dist/plugins/session-stats/plugin.test.d.ts.map +0 -1
  788. package/dist/plugins/session-stats/plugin.test.js +0 -145
  789. package/dist/plugins/session-stats/plugin.test.js.map +0 -1
  790. package/dist/plugins/shell/shell.integration.test.d.ts +0 -2
  791. package/dist/plugins/shell/shell.integration.test.d.ts.map +0 -1
  792. package/dist/plugins/shell/shell.integration.test.js +0 -234
  793. package/dist/plugins/shell/shell.integration.test.js.map +0 -1
  794. package/dist/plugins/shell/shell.test.d.ts +0 -2
  795. package/dist/plugins/shell/shell.test.d.ts.map +0 -1
  796. package/dist/plugins/shell/shell.test.js +0 -236
  797. package/dist/plugins/shell/shell.test.js.map +0 -1
  798. package/dist/plugins/skills/discovery.test.d.ts +0 -2
  799. package/dist/plugins/skills/discovery.test.d.ts.map +0 -1
  800. package/dist/plugins/skills/discovery.test.js +0 -312
  801. package/dist/plugins/skills/discovery.test.js.map +0 -1
  802. package/dist/plugins/skills/skills.integration.test.d.ts +0 -2
  803. package/dist/plugins/skills/skills.integration.test.d.ts.map +0 -1
  804. package/dist/plugins/skills/skills.integration.test.js +0 -475
  805. package/dist/plugins/skills/skills.integration.test.js.map +0 -1
  806. package/dist/plugins/todo/todo.integration.test.d.ts +0 -2
  807. package/dist/plugins/todo/todo.integration.test.d.ts.map +0 -1
  808. package/dist/plugins/todo/todo.integration.test.js +0 -605
  809. package/dist/plugins/todo/todo.integration.test.js.map +0 -1
  810. package/dist/plugins/uploads/preprocessors/image-classifier.test.d.ts +0 -2
  811. package/dist/plugins/uploads/preprocessors/image-classifier.test.d.ts.map +0 -1
  812. package/dist/plugins/uploads/preprocessors/image-classifier.test.js +0 -113
  813. package/dist/plugins/uploads/preprocessors/image-classifier.test.js.map +0 -1
  814. package/dist/plugins/uploads/uploads.integration.test.d.ts +0 -2
  815. package/dist/plugins/uploads/uploads.integration.test.d.ts.map +0 -1
  816. package/dist/plugins/uploads/uploads.integration.test.js +0 -639
  817. package/dist/plugins/uploads/uploads.integration.test.js.map +0 -1
  818. package/dist/plugins/user-chat/user-chat.integration.test.d.ts +0 -2
  819. package/dist/plugins/user-chat/user-chat.integration.test.d.ts.map +0 -1
  820. package/dist/plugins/user-chat/user-chat.integration.test.js +0 -754
  821. package/dist/plugins/user-chat/user-chat.integration.test.js.map +0 -1
  822. package/dist/plugins/workers/workers.integration.test.d.ts +0 -2
  823. package/dist/plugins/workers/workers.integration.test.d.ts.map +0 -1
  824. package/dist/plugins/workers/workers.integration.test.js +0 -629
  825. package/dist/plugins/workers/workers.integration.test.js.map +0 -1
  826. package/dist/prompts/macros.test.d.ts +0 -2
  827. package/dist/prompts/macros.test.d.ts.map +0 -1
  828. package/dist/prompts/macros.test.js +0 -80
  829. package/dist/prompts/macros.test.js.map +0 -1
  830. package/dist/testing/test-harness.test.d.ts +0 -2
  831. package/dist/testing/test-harness.test.d.ts.map +0 -1
  832. package/dist/testing/test-harness.test.js +0 -92
  833. package/dist/testing/test-harness.test.js.map +0 -1
  834. package/dist/transport/http/routes/rpc.integration.test.d.ts +0 -2
  835. package/dist/transport/http/routes/rpc.integration.test.d.ts.map +0 -1
  836. package/dist/transport/http/routes/rpc.integration.test.js +0 -189
  837. package/dist/transport/http/routes/rpc.integration.test.js.map +0 -1
  838. package/dist/transport/http/routes/rpc.test.d.ts +0 -2
  839. package/dist/transport/http/routes/rpc.test.d.ts.map +0 -1
  840. package/dist/transport/http/routes/rpc.test.js +0 -316
  841. package/dist/transport/http/routes/rpc.test.js.map +0 -1
  842. package/src/config.test.ts +0 -174
  843. package/src/core/agent-loop.integration.test.ts +0 -503
  844. package/src/core/agents/agent-config.test.ts +0 -240
  845. package/src/core/agents/agent-shutdown.test.ts +0 -236
  846. package/src/core/agents/agent.test.ts +0 -387
  847. package/src/core/agents/communicator.ts +0 -16
  848. package/src/core/agents/handler-events.test.ts +0 -167
  849. package/src/core/agents/response-sanitizer.test.ts +0 -137
  850. package/src/core/error-recovery.integration.test.ts +0 -343
  851. package/src/core/event-sourcing.integration.test.ts +0 -191
  852. package/src/core/events/file.test.ts +0 -436
  853. package/src/core/events/memory.test.ts +0 -741
  854. package/src/core/image/image-processor.test.ts +0 -218
  855. package/src/core/image/vips-resizer.test.ts +0 -388
  856. package/src/core/llm/anthropic.test.ts +0 -546
  857. package/src/core/llm/cache-breakpoints.test.ts +0 -85
  858. package/src/core/llm/cache-live.test.ts +0 -155
  859. package/src/core/llm/logger.test.ts +0 -270
  860. package/src/core/llm/message-sanitization.test.ts +0 -217
  861. package/src/core/llm/mock.test.ts +0 -402
  862. package/src/core/llm/openrouter-mapping.test.ts +0 -210
  863. package/src/core/llm/openrouter.test.ts +0 -499
  864. package/src/core/llm/provider-integration.test.ts +0 -408
  865. package/src/core/llm/routing-provider.test.ts +0 -113
  866. package/src/core/llm/snapshot-middleware.test.ts +0 -168
  867. package/src/core/multi-agent.integration.test.ts +0 -340
  868. package/src/core/plugin-hooks.integration.test.ts +0 -568
  869. package/src/core/session-lifecycle.integration.test.ts +0 -202
  870. package/src/core/sessions/fork-utils.test.ts +0 -158
  871. package/src/core/sessions/session-overrides.integration.test.ts +0 -210
  872. package/src/core/sessions/session.test.ts +0 -1736
  873. package/src/core/tools/executor.test.ts +0 -161
  874. package/src/lib/logger/console.test.ts +0 -348
  875. package/src/lib/never.ts +0 -3
  876. package/src/lib/utils/concurrency.test.ts +0 -169
  877. package/src/plugins/agent-status/agent-status.test.ts +0 -164
  878. package/src/plugins/agents/agents.integration.test.ts +0 -976
  879. package/src/plugins/agents/supervision.integration.test.ts +0 -249
  880. package/src/plugins/context-compact/compaction-live.test.ts +0 -221
  881. package/src/plugins/context-compact/context-compact.integration.test.ts +0 -418
  882. package/src/plugins/context-compact/context-compactor.test.ts +0 -1036
  883. package/src/plugins/context-compact/history-offloader.test.ts +0 -100
  884. package/src/plugins/filesystem/filesystem.integration.test.ts +0 -529
  885. package/src/plugins/filesystem/schema.ts +0 -6
  886. package/src/plugins/limits-guard/limit-guard.test.ts +0 -253
  887. package/src/plugins/limits-guard/limits-guard.integration.test.ts +0 -1012
  888. package/src/plugins/llm-debug/llm-debug.integration.test.ts +0 -192
  889. package/src/plugins/mailbox/mailbox.integration.test.ts +0 -794
  890. package/src/plugins/resources/resources.integration.test.ts +0 -68
  891. package/src/plugins/result-eviction/result-eviction.integration.test.ts +0 -332
  892. package/src/plugins/services/pid-registry.test.ts +0 -180
  893. package/src/plugins/services/services.integration.test.ts +0 -1223
  894. package/src/plugins/session-lifecycle/session-lifecycle.integration.test.ts +0 -639
  895. package/src/plugins/session-stats/plugin.test.ts +0 -160
  896. package/src/plugins/shell/shell.integration.test.ts +0 -273
  897. package/src/plugins/shell/shell.test.ts +0 -314
  898. package/src/plugins/skills/discovery.test.ts +0 -395
  899. package/src/plugins/skills/skills.integration.test.ts +0 -580
  900. package/src/plugins/todo/todo.integration.test.ts +0 -687
  901. package/src/plugins/uploads/preprocessors/image-classifier.test.ts +0 -142
  902. package/src/plugins/uploads/uploads.integration.test.ts +0 -831
  903. package/src/plugins/user-chat/user-chat.integration.test.ts +0 -893
  904. package/src/plugins/workers/workers.integration.test.ts +0 -778
  905. package/src/prompts/macros.test.ts +0 -107
  906. package/src/testing/test-harness.test.ts +0 -114
  907. package/src/transport/http/routes/rpc.integration.test.ts +0 -243
  908. package/src/transport/http/routes/rpc.test.ts +0 -372
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { resolve } from 'node:path'
10
+ import type { SessionCloseReason } from '~/core/plugins/plugin-builder.js'
10
11
  import type { SessionId } from '~/core/sessions/schema.js'
11
12
  import type { Result } from '~/lib/utils/result.js'
12
13
  import { Err, Ok } from '~/lib/utils/result.js'
@@ -14,9 +15,9 @@ import type { FileSystem } from '~/platform/fs.js'
14
15
  import type { ChildProcess, ProcessRunner } from '~/platform/process.js'
15
16
  import type { PortPool } from '~/plugins/services/port-pool.js'
16
17
  import type { ServicePidRegistry } from '~/plugins/services/pid-registry.js'
17
- import type { ServiceConfig, ServiceStartArgs, ServiceStatus } from '~/plugins/services/schema.js'
18
+ import type { ServiceConfig, ServiceStartArgs, ServiceStatus, ServiceStopSource } from '~/plugins/services/schema.js'
18
19
  import type { ToolError } from '../../core/tools/executor.js'
19
- import type { Logger } from '../../lib/logger/logger.js'
20
+ import type { LogContext, Logger } from '../../lib/logger/logger.js'
20
21
  import { RingBuffer } from '../../lib/logger/ring-buffer.js'
21
22
 
22
23
  // ============================================================================
@@ -56,13 +57,34 @@ interface RunningService {
56
57
  config: ServiceConfig
57
58
  process: ChildProcess
58
59
  pid: number
60
+ /** Captured at spawn so a later reap can tell our process from a recycled pid. */
61
+ pidStartTime?: number
59
62
  status: ServiceStatus
60
63
  port: number
61
64
  cwd?: string
62
65
  command: string
63
66
  logs: RingBuffer
67
+ forgetPid: () => Promise<void>
68
+ /** Set once the child's `close` fired, or once we gave up our claim on the group. */
69
+ exited: boolean
70
+ /** Who asked for the stop in progress — read back when the child's exit lands. */
71
+ stopSource?: ServiceStopSource
72
+ }
73
+
74
+ /** A process we spawned and may still have to reclaim. */
75
+ export interface ReapTarget {
76
+ pid: number
77
+ /** Start time captured at spawn; undefined on non-Linux, or when /proc was unreadable. */
78
+ pidStartTime?: number
64
79
  }
65
80
 
81
+ /**
82
+ * What a guarded reap did. `killed` covers a group that was already gone — the signal
83
+ * had nothing left to reach. `pid-reused` means the kernel handed the pid on and the
84
+ * process under it is somebody else's.
85
+ */
86
+ export type ReapOutcome = 'killed' | 'pid-reused'
87
+
66
88
  export interface ServiceStatusChangeDetails {
67
89
  port?: number
68
90
  error?: string
@@ -70,6 +92,8 @@ export interface ServiceStatusChangeDetails {
70
92
  pidStartTime?: number
71
93
  cwd?: string
72
94
  command?: string
95
+ /** Set on a `stopped` change: who asked for it. See ServiceEntry.stoppedBy. */
96
+ stoppedBy?: ServiceStopSource
73
97
  /** Set on a `failed` change that already has a revival queued — see ServiceEntry.restartAt. */
74
98
  restartAt?: number
75
99
  restartAttempt?: number
@@ -94,6 +118,16 @@ const PORT_CONFLICT_PATTERN = /EADDRINUSE|address already in use/i
94
118
  */
95
119
  const MAX_PORT_CONFLICT_RETRIES = 3
96
120
 
121
+ /**
122
+ * Deadline for embedder callbacks and PID-registry writes inside a transition.
123
+ *
124
+ * A transition holds the runtime lease that idle eviction waits on, so a resolver
125
+ * or a registry write that never settles pins the session resident forever and
126
+ * defeats the bound the lease exists to enforce. None of them is meant to take
127
+ * 30 s — the deadline only turns "wedged" into "failed".
128
+ */
129
+ const DEFAULT_SERVICE_HOOK_TIMEOUT_MS = 30_000
130
+
97
131
  /** `restartPolicy` defaults — see ServiceConfig.restartPolicy for the rationale. */
98
132
  const DEFAULT_MAX_RESTART_RETRIES = 3
99
133
  const DEFAULT_RESTART_DELAY_MS = 1000
@@ -108,7 +142,24 @@ export interface ServiceExecutorDeps {
108
142
  fs: FileSystem
109
143
  process: ProcessRunner
110
144
  /** Durable pid record, swept at agent boot. Optional so embedders without a data dir still work. */
111
- pidRegistry?: ServicePidRegistry
145
+ pidRegistry?: Pick<ServicePidRegistry, 'record' | 'forget'>
146
+ /** Test seam for process-group failure handling. */
147
+ kill?: typeof process.kill
148
+ /** Deadline for embedder callbacks and PID-registry writes; test seam, defaults to 30 s. */
149
+ hookTimeoutMs?: number
150
+ }
151
+
152
+ type ProcessGroupProbe = { state: 'alive' | 'gone' } | { state: 'error'; error: Error }
153
+
154
+ let serviceExecutorObserverForTesting: ((executor: ServiceExecutor) => void) | undefined
155
+
156
+ /** Observe executor creation in integration tests without exposing lifecycle methods as plugin API. */
157
+ export function setServiceExecutorObserverForTesting(observer: (executor: ServiceExecutor) => void): () => void {
158
+ const previous = serviceExecutorObserverForTesting
159
+ serviceExecutorObserverForTesting = observer
160
+ return () => {
161
+ if (serviceExecutorObserverForTesting === observer) serviceExecutorObserverForTesting = previous
162
+ }
112
163
  }
113
164
 
114
165
  export class ServiceExecutor {
@@ -117,8 +168,13 @@ export class ServiceExecutor {
117
168
  private readonly waiters = new Map<string, Array<{ resolve: (result: Result<void, ToolError>) => void; timer: ReturnType<typeof setTimeout> }>>()
118
169
  /** Per-type lock collapsing concurrent start() calls onto a single in-flight start. */
119
170
  private readonly startInFlight = new Map<string, Promise<Result<void, ToolError>>>()
171
+ /** Per-type stop operation awaited by runtime disposal. */
172
+ private readonly stopInFlight = new Map<string, Promise<Result<void, ToolError>>>()
173
+ /** Registry deletions that must settle before a replacement can record its PID. */
174
+ private readonly pidForgets = new Map<string, Promise<void>>()
120
175
  /** Per-type counter bounding automatic EADDRINUSE port re-allocation retries. */
121
176
  private readonly portConflictRetries = new Map<string, number>()
177
+ private readonly portConflictTimers = new Map<string, ReturnType<typeof setTimeout>>()
122
178
  /** Per-type counter bounding `restartPolicy` revivals after an unexpected exit. */
123
179
  private readonly restartRetries = new Map<string, number>()
124
180
  /** Pending `restartPolicy` timers, cancelled when someone stops or starts the service by hand. */
@@ -127,7 +183,11 @@ export class ServiceExecutor {
127
183
  private readonly portPool: PortPool
128
184
  private readonly fs: FileSystem
129
185
  private readonly processRunner: ProcessRunner
130
- private readonly pidRegistry?: ServicePidRegistry
186
+ private readonly pidRegistry?: Pick<ServicePidRegistry, 'record' | 'forget'>
187
+ private readonly killProcess: typeof process.kill
188
+ private readonly hookTimeoutMs: number
189
+ private closing = false
190
+ private closePromise?: Promise<void>
131
191
 
132
192
  /** Optional callback invoked on every service status change */
133
193
  onStatusChanged?: (
@@ -136,6 +196,8 @@ export class ServiceExecutor {
136
196
  status: ServiceStatus,
137
197
  details: ServiceStatusChangeDetails,
138
198
  ) => void
199
+ /** Invoked after every owned start attempt, including deferred retries. */
200
+ onStartSettled?: (serviceType: string) => void
139
201
 
140
202
  constructor(logger: Logger, portPool: PortPool, deps: ServiceExecutorDeps) {
141
203
  this.logger = logger
@@ -143,6 +205,32 @@ export class ServiceExecutor {
143
205
  this.fs = deps.fs
144
206
  this.processRunner = deps.process
145
207
  this.pidRegistry = deps.pidRegistry
208
+ this.killProcess = deps.kill ?? process.kill
209
+ this.hookTimeoutMs = deps.hookTimeoutMs ?? DEFAULT_SERVICE_HOOK_TIMEOUT_MS
210
+ serviceExecutorObserverForTesting?.(this)
211
+ }
212
+
213
+ /**
214
+ * Reject when `operation` outlives {@link DEFAULT_SERVICE_HOOK_TIMEOUT_MS}.
215
+ *
216
+ * The operation itself cannot be cancelled — an embedder callback keeps running —
217
+ * but the transition stops waiting on it, which is what releases the lease.
218
+ */
219
+ private async withDeadline<T>(label: string, operation: () => Promise<T> | T): Promise<T> {
220
+ let timer: ReturnType<typeof setTimeout> | undefined
221
+ try {
222
+ return await Promise.race([
223
+ (async () => await operation())(),
224
+ new Promise<never>((_, reject) => {
225
+ timer = setTimeout(
226
+ () => reject(new Error(`${label} did not settle within ${this.hookTimeoutMs}ms`)),
227
+ this.hookTimeoutMs,
228
+ )
229
+ }),
230
+ ])
231
+ } finally {
232
+ if (timer) clearTimeout(timer)
233
+ }
146
234
  }
147
235
 
148
236
  private notifyStatusChanged(
@@ -161,7 +249,10 @@ export class ServiceExecutor {
161
249
  if (!config.availableWhen) return Ok(true)
162
250
 
163
251
  try {
164
- return Ok(await config.availableWhen({ sessionId: String(sessionId), workspaceDir }))
252
+ return Ok(await this.withDeadline(
253
+ `Service '${config.type}' availability check`,
254
+ () => config.availableWhen?.({ sessionId: String(sessionId), workspaceDir }) ?? true,
255
+ ))
165
256
  } catch (error) {
166
257
  const message = error instanceof Error ? error.message : String(error)
167
258
  return Err({ message: `Service '${config.type}' availability check failed: ${message}`, recoverable: true })
@@ -177,7 +268,11 @@ export class ServiceExecutor {
177
268
  recoverable: true,
178
269
  })
179
270
  }
180
- const raw = await config.cwd({ sessionId: String(sessionId), workspaceDir })
271
+ const cwdResolver = config.cwd
272
+ const raw = await this.withDeadline(
273
+ `Service '${config.type}' cwd resolver`,
274
+ () => cwdResolver({ sessionId: String(sessionId), workspaceDir }),
275
+ )
181
276
  return Ok(resolve(workspaceDir, raw))
182
277
  }
183
278
 
@@ -257,6 +352,7 @@ export class ServiceExecutor {
257
352
  workspaceDir?: string,
258
353
  preferredPort?: number,
259
354
  ): Promise<Result<void, ToolError>> {
355
+ if (this.closing) return Err({ message: 'Service executor is closing', recoverable: false })
260
356
  const inFlight = this.startInFlight.get(config.type)
261
357
  if (inFlight) return inFlight
262
358
 
@@ -266,6 +362,7 @@ export class ServiceExecutor {
266
362
  return await promise
267
363
  } finally {
268
364
  this.startInFlight.delete(config.type)
365
+ this.onStartSettled?.(config.type)
269
366
  }
270
367
  }
271
368
 
@@ -275,6 +372,7 @@ export class ServiceExecutor {
275
372
  workspaceDir?: string,
276
373
  preferredPort?: number,
277
374
  ): Promise<Result<void, ToolError>> {
375
+ if (this.closing) return Err({ message: 'Service executor is closing', recoverable: false })
278
376
  // This start supersedes any revival the policy had queued.
279
377
  this.cancelPendingRestart(config.type)
280
378
 
@@ -282,8 +380,30 @@ export class ServiceExecutor {
282
380
  if (existing && (existing.status === 'starting' || existing.status === 'ready')) {
283
381
  return Ok(undefined)
284
382
  }
383
+ if (existing?.status === 'stopping') {
384
+ return Err({ message: `Service '${config.type}' is stopping, cannot start`, recoverable: true })
385
+ }
386
+ if (existing) {
387
+ // Whatever sits under this type is about to be overwritten — in this map and in
388
+ // the pid registry, both keyed by (session, type) — and to lose its port to the
389
+ // process spawned below, since a restart deliberately reuses it. A `failed`
390
+ // entry can still own a live process group, so reclaim it before its handle is
391
+ // dropped or nothing will ever be able to find it again.
392
+ const reaped = await this.reapEntry(existing, 'superseded by a new start')
393
+ if (!reaped.ok) {
394
+ this.logger.warn('Could not reclaim the superseded service process group', {
395
+ serviceType: config.type,
396
+ pid: existing.pid,
397
+ error: reaped.error.message,
398
+ })
399
+ }
400
+ await existing.forgetPid()
401
+ }
402
+ await this.pidForgets.get(config.type)
403
+ if (this.closing) return Err({ message: 'Service executor is closing', recoverable: false })
285
404
 
286
405
  const availability = await this.isAvailable(config, sessionId, workspaceDir)
406
+ if (this.closing) return Err({ message: 'Service executor is closing', recoverable: false })
287
407
  if (!availability.ok) return availability
288
408
  if (!availability.value) {
289
409
  return Err({ message: `Service '${config.type}' is not available in this workspace`, recoverable: true })
@@ -313,10 +433,30 @@ export class ServiceExecutor {
313
433
  }
314
434
 
315
435
  const cwd = cwdResult.value
316
- const logBufferSize = config.logBufferSize ?? 200
436
+ if (this.closing) {
437
+ if (allocatedNow) {
438
+ this.allocatedPorts.delete(config.type)
439
+ this.portPool.release(port)
440
+ }
441
+ return Err({ message: 'Service executor is closing', recoverable: false })
442
+ }
443
+ const logBufferSize = Math.max(1, config.logBufferSize ?? 200)
317
444
  const startupTimeoutMs = config.startupTimeoutMs ?? 30_000
318
445
 
319
446
  const readyRegex = config.readyPattern ? new RegExp(config.readyPattern) : undefined
447
+ const logs = new RingBuffer(logBufferSize)
448
+ const maxDiagnosticLength = 16_384
449
+ const startTime = Date.now()
450
+ let portConflictDetected = false
451
+ let setupComplete = false
452
+ let retryPortConflictDuringSetup = false
453
+ let readyDetectedDuringSetup = false
454
+ let readyLineDuringSetup: string | undefined
455
+ let stdoutPartial = ''
456
+ let stderrPartial = ''
457
+ let outputSequence = 0
458
+ let stdoutPartialSequence = 0
459
+ let stderrPartialSequence = 0
320
460
 
321
461
  const startArgs = {
322
462
  port,
@@ -328,12 +468,14 @@ export class ServiceExecutor {
328
468
  let command: string
329
469
  let serviceEnv: Record<string, string> | undefined
330
470
  try {
331
- command = typeof config.command === 'function'
332
- ? await config.command(startArgs)
333
- : config.command
334
- serviceEnv = typeof config.env === 'function'
335
- ? await config.env(startArgs)
336
- : config.env
471
+ const commandResolver = config.command
472
+ command = typeof commandResolver === 'function'
473
+ ? await this.withDeadline(`Service '${config.type}' command resolver`, () => commandResolver(startArgs))
474
+ : commandResolver
475
+ const envResolver = config.env
476
+ serviceEnv = typeof envResolver === 'function'
477
+ ? await this.withDeadline(`Service '${config.type}' env resolver`, () => envResolver(startArgs))
478
+ : envResolver
337
479
  } catch (error) {
338
480
  const message = error instanceof Error ? error.message : String(error)
339
481
  const errorMessage = `Service '${config.type}' start resolver failed: ${message}`
@@ -344,6 +486,13 @@ export class ServiceExecutor {
344
486
  this.notifyStatusChanged(sessionId, config.type, 'failed', { port, cwd, error: errorMessage })
345
487
  return Err({ message: errorMessage, recoverable: true })
346
488
  }
489
+ if (this.closing) {
490
+ if (allocatedNow) {
491
+ this.allocatedPorts.delete(config.type)
492
+ this.portPool.release(port)
493
+ }
494
+ return Err({ message: 'Service executor is closing', recoverable: false })
495
+ }
347
496
 
348
497
  const shell = process.platform === 'win32' ? 'cmd.exe' : '/bin/sh'
349
498
  const shellFlag = process.platform === 'win32' ? '/c' : '-c'
@@ -362,10 +511,18 @@ export class ServiceExecutor {
362
511
  const current = this.services.get(config.type)
363
512
  if (current && current.process === child && current.status !== 'stopped' && current.status !== 'failed') {
364
513
  current.status = 'failed'
514
+ // The pid rides along on purpose — see the startup timeout below.
365
515
  this.notifyStatusChanged(sessionId, config.type, 'failed', {
366
516
  error: error.message,
367
517
  cwd,
368
518
  command,
519
+ pid: current.pid,
520
+ pidStartTime: current.pidStartTime,
521
+ })
522
+ void this.reapEntry(current, 'process error').then((reaped) => {
523
+ if (!reaped.ok) {
524
+ this.logger.error('Could not kill errored service process group', reaped.error, { serviceType: config.type, pid: current.pid })
525
+ }
369
526
  })
370
527
  }
371
528
  })
@@ -378,6 +535,88 @@ export class ServiceExecutor {
378
535
  this.notifyStatusChanged(sessionId, config.type, 'failed', { port, cwd, command, error: 'Failed to spawn process' })
379
536
  return Err({ message: 'Failed to spawn service process', recoverable: true })
380
537
  }
538
+ // Bind the narrowed pid: the deferred registry call below cannot carry the
539
+ // narrowing across the closure boundary.
540
+ const pid = child.pid
541
+
542
+ const matches = (regex: RegExp, content: string): boolean => {
543
+ regex.lastIndex = 0
544
+ const matched = regex.test(content)
545
+ regex.lastIndex = 0
546
+ return matched
547
+ }
548
+
549
+ const recordDiagnostic = (line: string, prefix: string) => {
550
+ const maxContentLength = Math.max(0, maxDiagnosticLength - prefix.length)
551
+ const bounded = line.length > maxContentLength ? line.slice(-maxContentLength) : line
552
+ logs.push(`${prefix}${bounded}`)
553
+ }
554
+
555
+ // Detection sees the full incoming content before diagnostics are truncated.
556
+ const appendOutput = (partial: string, data: Buffer, prefix: string): string => {
557
+ const combined = partial + data.toString()
558
+ if (matches(PORT_CONFLICT_PATTERN, combined)) portConflictDetected = true
559
+ const lines = combined.split('\n')
560
+ const nextPartial = lines.pop() ?? ''
561
+ const readyMatched = readyRegex ? matches(readyRegex, combined) || lines.some((line) => matches(readyRegex, line)) : false
562
+ if (readyMatched && !setupComplete) {
563
+ readyDetectedDuringSetup = true
564
+ readyLineDuringSetup = combined.slice(-maxDiagnosticLength)
565
+ }
566
+
567
+ for (const line of lines) recordDiagnostic(line, prefix)
568
+
569
+ if (setupComplete) {
570
+ const current = this.services.get(config.type)
571
+ if (current?.process === child && current.status === 'starting') {
572
+ this.logger.debug('Service output', { serviceType: config.type })
573
+ if (readyMatched) markReady(combined.slice(-maxDiagnosticLength))
574
+ }
575
+ void checkReadyWhen()
576
+ }
577
+
578
+ // Retain enough suffix for markers split across chunks, never an entire line.
579
+ return nextPartial.length > maxDiagnosticLength ? nextPartial.slice(-maxDiagnosticLength) : nextPartial
580
+ }
581
+
582
+ const onStdout = (data: Buffer) => {
583
+ stdoutPartial = appendOutput(stdoutPartial, data, '')
584
+ stdoutPartialSequence = stdoutPartial ? ++outputSequence : 0
585
+ }
586
+ const onStderr = (data: Buffer) => {
587
+ stderrPartial = appendOutput(stderrPartial, data, '[stderr] ')
588
+ stderrPartialSequence = stderrPartial ? ++outputSequence : 0
589
+ }
590
+
591
+ let exitDuringSetup: { code: number | null } | undefined
592
+ let forgetPromise: Promise<void> | undefined
593
+ const forgetPid = (): Promise<void> => {
594
+ if (forgetPromise) return forgetPromise
595
+ const previous = this.pidForgets.get(config.type)
596
+ const runForget = () => this.withDeadline(
597
+ `Service '${config.type}' PID forget`,
598
+ () => this.pidRegistry?.forget(String(sessionId), config.type) ?? Promise.resolve(),
599
+ )
600
+ const operation = previous ? previous.then(runForget, runForget) : runForget()
601
+ forgetPromise = operation
602
+ this.pidForgets.set(config.type, operation)
603
+ void operation.then(
604
+ () => {
605
+ if (this.pidForgets.get(config.type) === operation) this.pidForgets.delete(config.type)
606
+ },
607
+ () => {
608
+ if (this.pidForgets.get(config.type) === operation) this.pidForgets.delete(config.type)
609
+ if (forgetPromise === operation) forgetPromise = undefined
610
+ },
611
+ )
612
+ return operation
613
+ }
614
+ const bufferClose = (code: number | null) => {
615
+ exitDuringSetup = { code }
616
+ }
617
+ child.stdout?.on('data', onStdout)
618
+ child.stderr?.on('data', onStderr)
619
+ child.on('close', bufferClose)
381
620
 
382
621
  // Capture start time immediately so a later PID-reuse check can distinguish
383
622
  // "our process" from "an unrelated process that grabbed this PID after ours died"
@@ -386,26 +625,58 @@ export class ServiceExecutor {
386
625
  // Durably record the process so a future agent can reap it if we die before the
387
626
  // `close` handler gets to forget it. The session projection cannot serve that
388
627
  // purpose — see ServicePidRegistry.
389
- await this.pidRegistry?.record({ sessionId: String(sessionId), serviceType: config.type, pid: child.pid, pidStartTime, command })
628
+ try {
629
+ await this.withDeadline(
630
+ `Service '${config.type}' PID record`,
631
+ () => this.pidRegistry?.record({ sessionId: String(sessionId), serviceType: config.type, pid, pidStartTime, command })
632
+ ?? Promise.resolve(),
633
+ )
634
+ } catch (error) {
635
+ // The child is already running but unrecorded, so nothing could reap it
636
+ // after a crash — tear it down here rather than leak it.
637
+ const message = error instanceof Error ? error.message : String(error)
638
+ const errorMessage = `Service '${config.type}' could not record its PID: ${message}`
639
+ try {
640
+ this.killProcess(-child.pid, 'SIGKILL')
641
+ } catch {
642
+ // The process may already be gone.
643
+ }
644
+ if (allocatedNow) {
645
+ this.allocatedPorts.delete(config.type)
646
+ this.portPool.release(port)
647
+ }
648
+ this.notifyStatusChanged(sessionId, config.type, 'failed', { port, cwd, command, error: errorMessage })
649
+ return Err({ message: errorMessage, recoverable: true })
650
+ }
651
+ if (this.closing) {
652
+ try {
653
+ this.killProcess(-child.pid, 'SIGKILL')
654
+ } catch {
655
+ // The process may already be gone.
656
+ }
657
+ await forgetPid()
658
+ if (allocatedNow) {
659
+ this.allocatedPorts.delete(config.type)
660
+ this.portPool.release(port)
661
+ }
662
+ return Err({ message: 'Service executor is closing', recoverable: false })
663
+ }
390
664
 
391
665
  // Emit starting event with PID, port, resolved cwd/command, and start time.
392
666
  this.notifyStatusChanged(sessionId, config.type, 'starting', { port, pid: child.pid, pidStartTime, cwd, command })
393
667
 
394
- const logs = new RingBuffer(logBufferSize)
395
- const startTime = Date.now()
396
- // Set when the child reports a port bind-conflict; read by the close handler
397
- // to decide between a fresh-port retry and a terminal failure.
398
- let portConflictDetected = false
399
-
400
668
  const entry: RunningService = {
401
669
  config,
402
670
  process: child,
403
671
  pid: child.pid,
672
+ pidStartTime,
404
673
  status: 'starting',
405
674
  port,
406
675
  cwd,
407
676
  command,
408
677
  logs,
678
+ forgetPid,
679
+ exited: false,
409
680
  }
410
681
  this.services.set(config.type, entry)
411
682
 
@@ -467,27 +738,6 @@ export class ServiceExecutor {
467
738
  }
468
739
  }
469
740
 
470
- const processLine = (line: string) => {
471
- logs.push(line)
472
- if (PORT_CONFLICT_PATTERN.test(line)) {
473
- portConflictDetected = true
474
- }
475
- const current = this.services.get(config.type)
476
- if (!current || current.process !== child) return
477
-
478
- if (current.status === 'starting') {
479
- this.logger.debug('Service output', { serviceType: config.type, line })
480
- }
481
-
482
- if (readyRegex && current.status === 'starting') {
483
- if (readyRegex.test(line)) {
484
- markReady(line)
485
- }
486
- }
487
-
488
- void checkReadyWhen()
489
- }
490
-
491
741
  // Startup timeout — mark as failed if not ready in time
492
742
  if (readyRegex || config.readyWhen) {
493
743
  startupTimer = setTimeout(() => {
@@ -498,19 +748,23 @@ export class ServiceExecutor {
498
748
  const errorMsg = `Service startup timed out after ${startupTimeoutMs}ms`
499
749
  this.logger.error(errorMsg, undefined, { serviceType: config.type })
500
750
  clearReadinessTimers()
751
+ // The pid rides along on purpose: should this agent die before the reap
752
+ // below completes, the recorded pid is all a later boot has to find the
753
+ // survivor. The child's exit takes the claim away again.
501
754
  this.notifyStatusChanged(sessionId, config.type, 'failed', {
502
755
  port: current.port,
503
756
  cwd: current.cwd,
504
757
  command: current.command,
505
758
  error: errorMsg,
759
+ pid: current.pid,
760
+ pidStartTime: current.pidStartTime,
506
761
  })
507
762
 
508
- // Kill the timed-out process
509
- try {
510
- process.kill(-current.pid, 'SIGKILL')
511
- } catch {
512
- // Already gone
513
- }
763
+ void this.reapEntry(current, 'startup timeout').then((reaped) => {
764
+ if (!reaped.ok) {
765
+ this.logger.error('Could not kill timed-out service process group', reaped.error, { serviceType: config.type, pid: current.pid })
766
+ }
767
+ })
514
768
  }, startupTimeoutMs)
515
769
  }
516
770
 
@@ -519,55 +773,38 @@ export class ServiceExecutor {
519
773
  readyCheckTimer = setInterval(() => {
520
774
  void checkReadyWhen()
521
775
  }, intervalMs)
522
- void checkReadyWhen()
523
776
  }
524
777
 
525
- // Pipe stdout/stderr line by line
526
- let stdoutPartial = ''
527
- child.stdout?.on('data', (data: Buffer) => {
528
- stdoutPartial += data.toString()
529
- const lines = stdoutPartial.split('\n')
530
- stdoutPartial = lines.pop()!
531
- for (const line of lines) {
532
- processLine(line)
533
- }
534
- })
535
-
536
- let stderrPartial = ''
537
- child.stderr?.on('data', (data: Buffer) => {
538
- stderrPartial += data.toString()
539
- const lines = stderrPartial.split('\n')
540
- stderrPartial = lines.pop()!
541
- for (const line of lines) {
542
- processLine(`[stderr] ${line}`)
543
- }
544
- })
545
-
546
- // Handle unexpected exit
547
- child.on('close', (code) => {
778
+ // Handle unexpected exit. Named and guarded because it also has to be
779
+ // replayable — see the exit-during-setup check after markReady() below.
780
+ let closeHandled = false
781
+ const handleClose = (code: number | null) => {
782
+ if (closeHandled) return
783
+ closeHandled = true
784
+ entry.exited = true
548
785
  clearReadinessTimers()
549
- // The process is gone, so its durable record has nothing left to reap.
550
- void this.pidRegistry?.forget(String(sessionId), config.type)
551
- // Flush remaining partial lines
552
- if (stdoutPartial) {
553
- processLine(stdoutPartial)
554
- stdoutPartial = ''
555
- }
556
- if (stderrPartial) {
557
- processLine(`[stderr] ${stderrPartial}`)
558
- stderrPartial = ''
786
+ // A replacement must not be recorded until this deletion finishes.
787
+ void forgetPid()
788
+ const partials = [
789
+ { content: stdoutPartial, prefix: '', sequence: stdoutPartialSequence },
790
+ { content: stderrPartial, prefix: '[stderr] ', sequence: stderrPartialSequence },
791
+ ]
792
+ for (const partial of partials.filter(({ content }) => content).sort((left, right) => left.sequence - right.sequence)) {
793
+ recordDiagnostic(partial.content, partial.prefix)
559
794
  }
795
+ stdoutPartial = ''
796
+ stderrPartial = ''
560
797
 
561
798
  const current = this.services.get(config.type)
562
799
  if (!current || current.process !== child) return
563
800
 
564
801
  if (current.status === 'stopping') {
565
- // Expected stop
802
+ // Expected stop — the child's exit, not stopInternal, is what publishes it.
566
803
  current.status = 'stopped'
567
- this.notifyStatusChanged(sessionId, config.type, 'stopped')
804
+ this.notifyStatusChanged(sessionId, config.type, 'stopped', { stoppedBy: current.stopSource ?? 'agent' })
568
805
  } else if (current.status === 'starting' || current.status === 'ready') {
569
806
  const retries = this.portConflictRetries.get(config.type) ?? 0
570
- if (portConflictDetected && retries < MAX_PORT_CONFLICT_RETRIES) {
807
+ if (!this.closing && current.status === 'starting' && portConflictDetected && retries < MAX_PORT_CONFLICT_RETRIES) {
571
808
  // The chosen port is held by a foreign process (e.g. a service
572
809
  // leaked from another session, or a survivor of a previous boot).
573
810
  // Re-allocate a fresh port and retry so the service recovers on a
@@ -584,7 +821,16 @@ export class ServiceExecutor {
584
821
  })
585
822
  this.allocatedPorts.delete(config.type)
586
823
  this.services.delete(config.type)
587
- void this.start(config, sessionId, workspaceDir)
824
+ if (!setupComplete) {
825
+ retryPortConflictDuringSetup = true
826
+ } else {
827
+ const retryTimer = setTimeout(() => {
828
+ this.portConflictTimers.delete(config.type)
829
+ if (!this.closing) void this.start(config, sessionId, workspaceDir)
830
+ }, 0)
831
+ retryTimer.unref?.()
832
+ this.portConflictTimers.set(config.type, retryTimer)
833
+ }
588
834
  return
589
835
  }
590
836
 
@@ -606,8 +852,16 @@ export class ServiceExecutor {
606
852
  serviceType: config.type,
607
853
  code,
608
854
  })
855
+ } else if (current.status === 'failed') {
856
+ // The process this entry still claimed is now confirmed gone — a startup
857
+ // timeout or a process error published the pid without being able to
858
+ // confirm the kill. Take the claim away so no later boot goes looking for
859
+ // it; the status and the error that explain the failure stay as they are.
860
+ this.notifyStatusChanged(sessionId, config.type, 'failed')
609
861
  }
610
- })
862
+ }
863
+ child.on('close', handleClose)
864
+ child.off('close', bufferClose)
611
865
 
612
866
  this.logger.info('Service starting', {
613
867
  serviceType: config.type,
@@ -619,9 +873,34 @@ export class ServiceExecutor {
619
873
  startupTimeoutMs,
620
874
  })
621
875
 
622
- // If no ready pattern, immediately mark as ready
623
- if (!readyRegex && !config.readyWhen) {
876
+ // A service that died inside the bookkeeping above closed while only the
877
+ // setup close collector was listening, so replay that close now.
878
+ // `alreadyReaped` covers the in-between state: the exit is recorded but
879
+ // 'close' has not fired yet because it waits for the stdio EOF. The listener
880
+ // above is guaranteed to receive it, so calling handleClose here would only
881
+ // drop the tail of the log the failure has to explain — while marking such a
882
+ // child ready would be a lie.
883
+ const alreadyReaped = child.exitCode != null || child.signalCode != null
884
+ if (exitDuringSetup) {
885
+ handleClose(exitDuringSetup.code)
886
+ }
887
+
888
+ setupComplete = true
889
+ if (retryPortConflictDuringSetup) {
890
+ await forgetPid()
891
+ if (this.closing) return Err({ message: 'Service executor is closing', recoverable: false })
892
+ return this.startInternal(config, sessionId, workspaceDir)
893
+ }
894
+ if (exitDuringSetup || alreadyReaped) {
895
+ return Ok(undefined)
896
+ }
897
+ if (readyDetectedDuringSetup) {
898
+ markReady(readyLineDuringSetup)
899
+ } else if (!readyRegex && !config.readyWhen) {
900
+ // If no ready condition is configured, a live child is ready immediately.
624
901
  markReady()
902
+ } else if (config.readyWhen) {
903
+ void checkReadyWhen()
625
904
  }
626
905
 
627
906
  return Ok(undefined)
@@ -641,6 +920,7 @@ export class ServiceExecutor {
641
920
  uptimeMs: number,
642
921
  exitCode: number | null,
643
922
  ): ScheduledRestart | undefined {
923
+ if (this.closing) return undefined
644
924
  const policy = config.restartPolicy
645
925
  if (!policy) return undefined
646
926
 
@@ -675,7 +955,7 @@ export class ServiceExecutor {
675
955
 
676
956
  const timer = setTimeout(() => {
677
957
  this.restartTimers.delete(config.type)
678
- void this.start(config, sessionId, workspaceDir)
958
+ if (!this.closing) void this.start(config, sessionId, workspaceDir)
679
959
  }, delayMs)
680
960
  // A pending revival must not keep the process alive on its own.
681
961
  timer.unref?.()
@@ -693,151 +973,389 @@ export class ServiceExecutor {
693
973
  return true
694
974
  }
695
975
 
696
- /** Whether a `restartPolicy` revival is queued — the service is down but coming back. */
976
+ /**
977
+ * Whether the service is down but coming back on its own — a `restartPolicy`
978
+ * revival or a queued port-conflict retry. Both are timers this executor
979
+ * owns, so both need the runtime to outlive the call that armed them.
980
+ */
697
981
  hasScheduledRestart(serviceType: string): boolean {
698
- return this.restartTimers.has(serviceType)
982
+ return this.restartTimers.has(serviceType) || this.portConflictTimers.has(serviceType)
699
983
  }
700
984
 
701
985
  /**
702
- * Stop a running service gracefully.
703
- * Port is NOT released — kept for session-level stability across restarts.
986
+ * Stop accepting work and drain all runtime-owned processes and timers.
987
+ *
988
+ * `reason: 'evicted'` only parks the session: every service keeps its full
989
+ * graceful window and is recorded as stopped by the runtime, which is what
990
+ * lets autoStart bring it back once the runtime is rebuilt. The other
991
+ * reasons end the session, so a process group that ignores SIGTERM is taken
992
+ * down without waiting.
704
993
  */
705
- async stop(serviceType: string, sessionId: SessionId): Promise<Result<void, ToolError>> {
706
- const hadPendingRestart = this.cancelPendingRestart(serviceType)
707
- this.restartRetries.delete(serviceType)
708
- const entry = this.services.get(serviceType)
709
- if (!entry) {
710
- return Err({ message: `Service '${serviceType}' not found`, recoverable: false })
994
+ close(sessionId: SessionId, reason: SessionCloseReason = 'closed'): Promise<void> {
995
+ this.closing = true
996
+ this.closePromise ??= this.performClose(sessionId, reason).finally(() => {
997
+ this.onStatusChanged = undefined
998
+ this.onStartSettled = undefined
999
+ })
1000
+ return this.closePromise
1001
+ }
1002
+
1003
+ private async performClose(sessionId: SessionId, reason: SessionCloseReason): Promise<void> {
1004
+ this.cancelPortConflictTimers()
1005
+ await Promise.allSettled([...this.startInFlight.values()])
1006
+ this.cancelPortConflictTimers()
1007
+ const concurrentStopTypes = new Set(this.stopInFlight.keys())
1008
+ await Promise.allSettled([...this.stopInFlight.values()])
1009
+
1010
+ // One service that cannot be signalled must not strand the others, and must not
1011
+ // skip the tail — portPool is process-global, so a skipped release loses those
1012
+ // ports for the life of the server.
1013
+ const failures: unknown[] = []
1014
+ const parking = reason === 'evicted'
1015
+ try {
1016
+ for (const [serviceType, entry] of [...this.services]) {
1017
+ if (entry.status === 'stopped') continue
1018
+ if (entry.status === 'starting' || entry.status === 'ready' || entry.status === 'stopping' || this.restartTimers.has(serviceType)) {
1019
+ const forcedGraceMs = parking ? entry.config.gracefulStopMs ?? 5000 : 0
1020
+ try {
1021
+ if (concurrentStopTypes.has(serviceType)) {
1022
+ await this.forceStopForClose(serviceType, entry, sessionId, forcedGraceMs)
1023
+ continue
1024
+ }
1025
+ const stopped = await this.stop(serviceType, sessionId, 'eviction')
1026
+ if (!stopped.ok) await this.forceStopForClose(serviceType, entry, sessionId, forcedGraceMs)
1027
+ } catch (error) {
1028
+ failures.push(error)
1029
+ }
1030
+ }
1031
+ }
1032
+ } finally {
1033
+ this.cancelAllDeferredStarts()
1034
+ await Promise.allSettled([...this.pidForgets.values()])
1035
+ for (const port of this.allocatedPorts.values()) this.portPool.release(port)
1036
+ this.allocatedPorts.clear()
1037
+ this.services.clear()
1038
+ for (const serviceType of this.waiters.keys()) this.resolveWaiters(serviceType, 'stopped')
711
1039
  }
712
- if (entry.status !== 'starting' && entry.status !== 'ready' && entry.status !== 'paused') {
713
- // A failed service with a revival queued is really "about to restart",
714
- // and calling that off is a legitimate stop rather than an error.
715
- if (hadPendingRestart) {
716
- entry.status = 'stopped'
717
- this.notifyStatusChanged(sessionId, serviceType, 'stopped')
718
- return Ok(undefined)
1040
+
1041
+ // Still reject — a close that could not terminate a process is not a clean close.
1042
+ if (failures.length === 1) throw failures[0]
1043
+ if (failures.length > 1) {
1044
+ throw new AggregateError(failures, `Failed to stop ${failures.length} services during close`)
1045
+ }
1046
+ }
1047
+
1048
+ private async forceStopForClose(serviceType: string, entry: RunningService, sessionId: SessionId, gracefulStopMs: number): Promise<void> {
1049
+ const previousStatus = entry.status
1050
+ this.recordStopSource(entry, 'eviction')
1051
+ if (entry.status !== 'stopping') {
1052
+ entry.status = 'stopping'
1053
+ this.notifyStatusChanged(sessionId, serviceType, 'stopping')
1054
+ }
1055
+ const stopped = await this.terminateProcessGroup(entry.pid, gracefulStopMs)
1056
+ if (!stopped.ok) {
1057
+ // Mirror stopInternal: no recovery path matches a stranded `stopping`, so a
1058
+ // force-stop that failed has to leave a status the next runtime can reap.
1059
+ if (entry.status === 'stopping' && previousStatus !== 'stopping') {
1060
+ entry.status = previousStatus
1061
+ this.notifyStatusChanged(sessionId, serviceType, previousStatus, {
1062
+ port: entry.port,
1063
+ cwd: entry.cwd,
1064
+ command: entry.command,
1065
+ error: stopped.error.message,
1066
+ })
719
1067
  }
720
- return Err({ message: `Service '${serviceType}' is ${entry.status}, cannot stop`, recoverable: false })
1068
+ throw stopped.error
1069
+ }
1070
+ // The group is confirmed gone, so nothing may offer this pid up for a reap again.
1071
+ entry.exited = true
1072
+ await entry.forgetPid()
1073
+ if (this.services.get(serviceType) === entry) {
1074
+ entry.status = 'stopped'
1075
+ this.notifyStatusChanged(sessionId, serviceType, 'stopped', { stoppedBy: entry.stopSource ?? 'eviction' })
721
1076
  }
1077
+ }
722
1078
 
723
- entry.status = 'stopping'
724
- this.notifyStatusChanged(sessionId, serviceType, 'stopping')
1079
+ /** An agent stop stays the agent's decision even when a lifecycle stop is what finishes it. */
1080
+ private recordStopSource(entry: RunningService, stoppedBy: ServiceStopSource): void {
1081
+ if (stoppedBy === 'agent' || entry.stopSource === undefined) entry.stopSource = stoppedBy
1082
+ }
725
1083
 
726
- const gracefulStopMs = entry.config.gracefulStopMs ?? 5000
1084
+ private cancelAllDeferredStarts(): void {
1085
+ for (const timer of this.restartTimers.values()) clearTimeout(timer)
1086
+ this.restartTimers.clear()
1087
+ this.cancelPortConflictTimers()
1088
+ }
1089
+
1090
+ private cancelPortConflictTimers(): void {
1091
+ for (const timer of this.portConflictTimers.values()) clearTimeout(timer)
1092
+ this.portConflictTimers.clear()
1093
+ }
727
1094
 
728
- // Send SIGTERM
1095
+ private errorWithContext(error: unknown, context: string): Error {
1096
+ const cause = error instanceof Error ? error.message : String(error)
1097
+ return new Error(`${context}: ${cause}`)
1098
+ }
1099
+
1100
+ private hasErrorCode(error: unknown, code: string): boolean {
1101
+ return error instanceof Error && 'code' in error && error.code === code
1102
+ }
1103
+
1104
+ private async hasNonZombieProcessGroupMember(pid: number): Promise<boolean> {
1105
+ if (process.platform !== 'linux') return true
1106
+
1107
+ let entries: string[]
729
1108
  try {
730
- process.kill(-entry.pid, 'SIGTERM')
1109
+ entries = await this.fs.readdir('/proc')
731
1110
  } catch {
732
- // Process already gone
733
- entry.status = 'stopped'
734
- this.notifyStatusChanged(sessionId, serviceType, 'stopped')
735
- return Ok(undefined)
1111
+ return true
736
1112
  }
737
1113
 
738
- // Wait for graceful shutdown, then SIGKILL
739
- await new Promise<void>((resolve) => {
740
- const checkInterval = setInterval(() => {
741
- try {
742
- // Check if process is still alive (signal 0 doesn't kill, just checks)
743
- process.kill(entry.pid, 0)
744
- } catch {
745
- // Process gone
746
- clearInterval(checkInterval)
747
- clearTimeout(killTimeout)
748
- resolve()
749
- }
750
- }, 200)
1114
+ let foundGroupMember = false
1115
+ for (const entry of entries) {
1116
+ if (!/^\d+$/.test(entry)) continue
1117
+ try {
1118
+ const stat = await this.fs.readFile(`/proc/${entry}/stat`, 'utf-8')
1119
+ const rparen = stat.lastIndexOf(')')
1120
+ if (rparen === -1) continue
1121
+ const fields = stat.slice(rparen + 2).split(' ')
1122
+ if (Number(fields[2]) !== pid) continue
1123
+ foundGroupMember = true
1124
+ if (fields[0] !== 'Z') return true
1125
+ } catch {
1126
+ // Processes can disappear while /proc is scanned.
1127
+ }
1128
+ }
1129
+ // A successful group probe with no readable member is still evidence of life.
1130
+ return !foundGroupMember
1131
+ }
1132
+
1133
+ private async probeProcessGroup(pid: number): Promise<ProcessGroupProbe> {
1134
+ try {
1135
+ this.killProcess(-pid, 0)
1136
+ } catch (error) {
1137
+ if (this.hasErrorCode(error, 'ESRCH')) return { state: 'gone' }
1138
+ if (this.hasErrorCode(error, 'EPERM')) return { state: 'alive' }
1139
+ return { state: 'error', error: this.errorWithContext(error, `Failed to probe process group ${pid}`) }
1140
+ }
751
1141
 
752
- const killTimeout = setTimeout(() => {
1142
+ return await this.hasNonZombieProcessGroupMember(pid) ? { state: 'alive' } : { state: 'gone' }
1143
+ }
1144
+
1145
+ private async signalProcessGroup(pid: number, signal: NodeJS.Signals): Promise<ProcessGroupProbe> {
1146
+ try {
1147
+ this.killProcess(-pid, signal)
1148
+ return { state: 'alive' }
1149
+ } catch (error) {
1150
+ if (this.hasErrorCode(error, 'ESRCH')) return { state: 'gone' }
1151
+ return { state: 'error', error: this.errorWithContext(error, `Failed to send ${signal} to process group ${pid}`) }
1152
+ }
1153
+ }
1154
+
1155
+ private async waitForProcessGroupExit(pid: number, timeoutMs: number): Promise<ProcessGroupProbe> {
1156
+ const initial = await this.probeProcessGroup(pid)
1157
+ if (initial.state !== 'alive') return initial
1158
+
1159
+ return new Promise((resolve) => {
1160
+ let settled = false
1161
+ const finish = (result: ProcessGroupProbe) => {
1162
+ if (settled) return
1163
+ settled = true
753
1164
  clearInterval(checkInterval)
754
- try {
755
- process.kill(-entry.pid, 'SIGKILL')
756
- } catch {
757
- // Already gone
758
- }
759
- resolve()
760
- }, gracefulStopMs)
1165
+ clearTimeout(timeout)
1166
+ resolve(result)
1167
+ }
1168
+ const checkInterval = setInterval(() => {
1169
+ void this.probeProcessGroup(pid).then((result) => {
1170
+ if (result.state !== 'alive') finish(result)
1171
+ })
1172
+ }, 50)
1173
+ const timeout = setTimeout(() => {
1174
+ void this.probeProcessGroup(pid).then(finish)
1175
+ }, timeoutMs)
761
1176
  })
1177
+ }
762
1178
 
763
- this.logger.info('Service stopped', { serviceType })
1179
+ /**
1180
+ * Reclaim a service process group we spawned, unless the kernel recycled its pid.
1181
+ *
1182
+ * Every path that gives up on a service comes through here, because ending the
1183
+ * *wait* is not ending the *process*: a dev server that missed its readiness
1184
+ * window keeps its several hundred MB of RSS until somebody kills it, which on a
1185
+ * memory-starved box is what makes the next startup miss its window too.
1186
+ *
1187
+ * `pidStartTime` was captured at spawn, so a different current start time means
1188
+ * the pid now belongs to an unrelated process and killing it would be disastrous.
1189
+ * An unknown start time (non-Linux, no /proc) leaves only the kill-and-hope path.
1190
+ */
1191
+ async reapProcessGroup(target: ReapTarget, context: LogContext = {}): Promise<Result<ReapOutcome, Error>> {
1192
+ const currentStartTime = await getProcessStartTime(this.fs, target.pid)
1193
+ if (target.pidStartTime !== undefined && currentStartTime !== undefined && currentStartTime !== target.pidStartTime) {
1194
+ this.logger.warn('PID reuse detected — refusing to kill service process group', {
1195
+ ...context,
1196
+ pid: target.pid,
1197
+ storedStartTime: target.pidStartTime,
1198
+ currentStartTime,
1199
+ })
1200
+ return Ok('pid-reused')
1201
+ }
1202
+
1203
+ const killed = this.killProcessGroup(target.pid)
1204
+ if (!killed.ok) return killed
1205
+ return Ok('killed')
1206
+ }
1207
+
1208
+ /**
1209
+ * Reclaim the process group of an entry we are giving up on or replacing.
1210
+ *
1211
+ * A no-op once the child's `close` has fired: there is nothing left to reclaim,
1212
+ * and skipping it also keeps the non-Linux kill-and-hope path away from a pid the
1213
+ * kernel may since have handed to somebody else. A settled reap ends our claim,
1214
+ * so the entry is never offered up twice.
1215
+ */
1216
+ private async reapEntry(entry: RunningService, reason: string): Promise<Result<void, Error>> {
1217
+ if (entry.exited) return Ok(undefined)
1218
+ const reaped = await this.reapProcessGroup(
1219
+ { pid: entry.pid, pidStartTime: entry.pidStartTime },
1220
+ { serviceType: entry.config.type, reason },
1221
+ )
1222
+ if (!reaped.ok) return reaped
1223
+ entry.exited = true
764
1224
  return Ok(undefined)
765
1225
  }
766
1226
 
767
1227
  /**
768
- * Restart a service (stop + start).
1228
+ * SIGKILL a process group through the injectable kill seam, without waiting
1229
+ * for it to go. Reached through {@link reapProcessGroup} wherever a recorded
1230
+ * pid could have been recycled.
769
1231
  */
770
- async restart(
771
- config: ServiceConfig,
772
- sessionId: SessionId,
773
- workspaceDir?: string,
774
- preferredPort?: number,
775
- ): Promise<Result<void, ToolError>> {
776
- const entry = this.services.get(config.type)
777
- if (entry && (entry.status === 'starting' || entry.status === 'ready' || entry.status === 'paused')) {
778
- const stopResult = await this.stop(config.type, sessionId)
779
- if (!stopResult.ok) return stopResult
1232
+ killProcessGroup(pid: number): Result<void, Error> {
1233
+ try {
1234
+ this.killProcess(-pid, 'SIGKILL')
1235
+ return Ok(undefined)
1236
+ } catch (error) {
1237
+ if (this.hasErrorCode(error, 'ESRCH')) return Ok(undefined) // Already gone.
1238
+ return Err(this.errorWithContext(error, `Failed to SIGKILL process group ${pid}`))
780
1239
  }
1240
+ }
781
1241
 
782
- return this.start(config, sessionId, workspaceDir, preferredPort)
1242
+ private async terminateProcessGroup(pid: number, gracefulStopMs: number): Promise<Result<void, Error>> {
1243
+ const initial = await this.probeProcessGroup(pid)
1244
+ if (initial.state === 'gone') return Ok(undefined)
1245
+ if (initial.state === 'error') return Err(initial.error)
1246
+
1247
+ const term = await this.signalProcessGroup(pid, 'SIGTERM')
1248
+ if (term.state === 'gone') return Ok(undefined)
1249
+ if (term.state === 'error') return Err(term.error)
1250
+ const graceful = await this.waitForProcessGroupExit(pid, gracefulStopMs)
1251
+ if (graceful.state === 'gone') return Ok(undefined)
1252
+ if (graceful.state === 'error') return Err(graceful.error)
1253
+
1254
+ const kill = await this.signalProcessGroup(pid, 'SIGKILL')
1255
+ if (kill.state === 'gone') return Ok(undefined)
1256
+ if (kill.state === 'error') return Err(kill.error)
1257
+ const forced = await this.waitForProcessGroupExit(pid, 5000)
1258
+ if (forced.state === 'gone') return Ok(undefined)
1259
+ if (forced.state === 'error') return Err(forced.error)
1260
+ return Err(new Error(`Process group ${pid} survived SIGKILL`))
783
1261
  }
784
1262
 
785
1263
  /**
786
- * Pause a running service (SIGSTOP).
1264
+ * Stop a running service gracefully.
1265
+ * Port is NOT released — kept for session-level stability across restarts.
787
1266
  */
788
- async pause(serviceType: string, sessionId: SessionId): Promise<Result<void, ToolError>> {
1267
+ async stop(serviceType: string, sessionId: SessionId, stoppedBy: ServiceStopSource = 'agent'): Promise<Result<void, ToolError>> {
1268
+ const inFlight = this.stopInFlight.get(serviceType)
1269
+ if (inFlight) return inFlight
1270
+
1271
+ let operation: Promise<Result<void, ToolError>>
1272
+ operation = this.stopInternal(serviceType, sessionId, stoppedBy).finally(() => {
1273
+ if (this.stopInFlight.get(serviceType) === operation) this.stopInFlight.delete(serviceType)
1274
+ })
1275
+ this.stopInFlight.set(serviceType, operation)
1276
+ return operation
1277
+ }
1278
+
1279
+ private async stopInternal(serviceType: string, sessionId: SessionId, stoppedBy: ServiceStopSource): Promise<Result<void, ToolError>> {
1280
+ const hadPendingRestart = this.cancelPendingRestart(serviceType)
1281
+ this.restartRetries.delete(serviceType)
789
1282
  const entry = this.services.get(serviceType)
790
1283
  if (!entry) {
791
1284
  return Err({ message: `Service '${serviceType}' not found`, recoverable: false })
792
1285
  }
793
- if (entry.status !== 'ready') {
794
- return Err({ message: `Service '${serviceType}' is ${entry.status}, cannot pause`, recoverable: false })
1286
+ if (entry.status !== 'starting' && entry.status !== 'ready') {
1287
+ // `failed` says the start gave up, not that the process did — and refusing to
1288
+ // stop it left the platform-side reaper, the one caller that can free a leaked
1289
+ // dev server's memory, with no way to do so. A failed service with a revival
1290
+ // queued is on top of that really "about to restart", and calling that off is a
1291
+ // legitimate stop rather than an error.
1292
+ if (entry.status === 'failed' || hadPendingRestart) {
1293
+ const reaped = await this.reapEntry(entry, 'stopped while failed')
1294
+ if (!reaped.ok) return Err({ message: reaped.error.message, recoverable: true })
1295
+ try {
1296
+ await entry.forgetPid()
1297
+ } catch (error) {
1298
+ return Err({ message: this.errorWithContext(error, `Failed to forget PID for service '${serviceType}'`).message, recoverable: true })
1299
+ }
1300
+ entry.status = 'stopped'
1301
+ this.notifyStatusChanged(sessionId, serviceType, 'stopped', { stoppedBy })
1302
+ return Ok(undefined)
1303
+ }
1304
+ return Err({ message: `Service '${serviceType}' is ${entry.status}, cannot stop`, recoverable: false })
795
1305
  }
796
1306
 
1307
+ const previousStatus = entry.status
1308
+ this.recordStopSource(entry, stoppedBy)
1309
+ entry.status = 'stopping'
1310
+ this.notifyStatusChanged(sessionId, serviceType, 'stopping')
1311
+
1312
+ const gracefulStopMs = entry.config.gracefulStopMs ?? 5000
1313
+
1314
+ const stopped = await this.terminateProcessGroup(entry.pid, gracefulStopMs)
1315
+ if (!stopped.ok) {
1316
+ if (entry.status === 'stopping') {
1317
+ entry.status = previousStatus
1318
+ this.notifyStatusChanged(sessionId, serviceType, previousStatus, {
1319
+ port: entry.port,
1320
+ cwd: entry.cwd,
1321
+ command: entry.command,
1322
+ error: stopped.error.message,
1323
+ })
1324
+ }
1325
+ return Err({ message: stopped.error.message, recoverable: true })
1326
+ }
1327
+ // The group is confirmed gone, so nothing may offer this pid up for a reap again.
1328
+ entry.exited = true
797
1329
  try {
798
- process.kill(entry.pid, 'SIGSTOP')
799
- } catch {
800
- return Err({ message: `Failed to pause service '${serviceType}'`, recoverable: false })
1330
+ await entry.forgetPid()
1331
+ } catch (error) {
1332
+ return Err({ message: this.errorWithContext(error, `Failed to forget PID for service '${serviceType}'`).message, recoverable: true })
1333
+ }
1334
+ if (entry.status === 'stopping') {
1335
+ entry.status = 'stopped'
1336
+ this.notifyStatusChanged(sessionId, serviceType, 'stopped', { stoppedBy: entry.stopSource ?? stoppedBy })
801
1337
  }
802
1338
 
803
- entry.status = 'paused'
804
- this.notifyStatusChanged(sessionId, serviceType, 'paused')
805
-
806
- this.logger.info('Service paused', { serviceType })
1339
+ this.logger.info('Service stopped', { serviceType })
807
1340
  return Ok(undefined)
808
1341
  }
809
1342
 
810
1343
  /**
811
- * Resume a paused service (SIGCONT).
1344
+ * Restart a service (stop + start).
812
1345
  */
813
- async resume(
1346
+ async restart(
814
1347
  config: ServiceConfig,
815
1348
  sessionId: SessionId,
816
- _workspaceDir?: string,
1349
+ workspaceDir?: string,
1350
+ preferredPort?: number,
817
1351
  ): Promise<Result<void, ToolError>> {
818
1352
  const entry = this.services.get(config.type)
819
- if (!entry) {
820
- return Err({ message: `Service '${config.type}' not found`, recoverable: false })
821
- }
822
- if (entry.status !== 'paused') {
823
- return Err({ message: `Service '${config.type}' is ${entry.status}, cannot resume`, recoverable: false })
824
- }
825
-
826
- try {
827
- process.kill(entry.pid, 'SIGCONT')
828
- } catch {
829
- return Err({ message: `Failed to resume service '${config.type}'`, recoverable: false })
1353
+ if (entry && (entry.status === 'starting' || entry.status === 'ready')) {
1354
+ const stopResult = await this.stop(config.type, sessionId)
1355
+ if (!stopResult.ok) return stopResult
830
1356
  }
831
1357
 
832
- entry.status = 'ready'
833
- this.notifyStatusChanged(sessionId, config.type, 'ready', {
834
- port: entry.port,
835
- cwd: entry.cwd,
836
- command: entry.command,
837
- })
838
-
839
- this.logger.info('Service resumed', { serviceType: config.type })
840
- return Ok(undefined)
1358
+ return this.start(config, sessionId, workspaceDir, preferredPort)
841
1359
  }
842
1360
 
843
1361
  /**
@@ -865,72 +1383,4 @@ export class ServiceExecutor {
865
1383
  const status = this.getStatus(serviceType)
866
1384
  return status === 'starting' || status === 'ready'
867
1385
  }
868
-
869
- /**
870
- * Shutdown all services and release all ports back to pool.
871
- * Called on session close.
872
- */
873
- async shutdown(): Promise<void> {
874
- // Nothing queued may spawn after the executor is gone.
875
- for (const serviceType of [...this.restartTimers.keys()]) this.cancelPendingRestart(serviceType)
876
- this.restartRetries.clear()
877
-
878
- const promises: Promise<void>[] = []
879
-
880
- for (const [serviceType, entry] of this.services) {
881
- if (entry.status === 'starting' || entry.status === 'ready' || entry.status === 'paused') {
882
- const gracefulStopMs = entry.config.gracefulStopMs ?? 5000
883
-
884
- const killPromise = new Promise<void>((resolve) => {
885
- try {
886
- process.kill(-entry.pid, 'SIGTERM')
887
- } catch {
888
- resolve()
889
- return
890
- }
891
-
892
- const killTimeout = setTimeout(() => {
893
- try {
894
- process.kill(-entry.pid, 'SIGKILL')
895
- } catch {
896
- // Already gone
897
- }
898
- resolve()
899
- }, gracefulStopMs)
900
-
901
- const checkInterval = setInterval(() => {
902
- try {
903
- process.kill(entry.pid, 0)
904
- } catch {
905
- clearInterval(checkInterval)
906
- clearTimeout(killTimeout)
907
- resolve()
908
- }
909
- }, 200)
910
- })
911
-
912
- promises.push(killPromise)
913
- entry.status = 'stopping'
914
- this.logger.info('Shutting down service', { serviceType })
915
- }
916
- }
917
-
918
- await Promise.all(promises)
919
- this.services.clear()
920
-
921
- // Drain all waiters with error
922
- for (const [serviceType, pending] of this.waiters) {
923
- for (const waiter of pending) {
924
- clearTimeout(waiter.timer)
925
- waiter.resolve(Err({ message: `Service '${serviceType}' shut down`, recoverable: false }))
926
- }
927
- }
928
- this.waiters.clear()
929
-
930
- // Release all allocated ports back to pool
931
- for (const port of this.allocatedPorts.values()) {
932
- this.portPool.release(port)
933
- }
934
- this.allocatedPorts.clear()
935
- }
936
1386
  }