@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
@@ -46,19 +46,43 @@ const PORT_CONFLICT_PATTERN = /EADDRINUSE|address already in use/i;
46
46
  * `failed` instead of looping forever.
47
47
  */
48
48
  const MAX_PORT_CONFLICT_RETRIES = 3;
49
+ /**
50
+ * Deadline for embedder callbacks and PID-registry writes inside a transition.
51
+ *
52
+ * A transition holds the runtime lease that idle eviction waits on, so a resolver
53
+ * or a registry write that never settles pins the session resident forever and
54
+ * defeats the bound the lease exists to enforce. None of them is meant to take
55
+ * 30 s — the deadline only turns "wedged" into "failed".
56
+ */
57
+ const DEFAULT_SERVICE_HOOK_TIMEOUT_MS = 30_000;
49
58
  /** `restartPolicy` defaults — see ServiceConfig.restartPolicy for the rationale. */
50
59
  const DEFAULT_MAX_RESTART_RETRIES = 3;
51
60
  const DEFAULT_RESTART_DELAY_MS = 1000;
52
61
  const DEFAULT_MAX_RESTART_DELAY_MS = 30_000;
53
62
  const DEFAULT_RESTART_HEALTHY_AFTER_MS = 60_000;
63
+ let serviceExecutorObserverForTesting;
64
+ /** Observe executor creation in integration tests without exposing lifecycle methods as plugin API. */
65
+ export function setServiceExecutorObserverForTesting(observer) {
66
+ const previous = serviceExecutorObserverForTesting;
67
+ serviceExecutorObserverForTesting = observer;
68
+ return () => {
69
+ if (serviceExecutorObserverForTesting === observer)
70
+ serviceExecutorObserverForTesting = previous;
71
+ };
72
+ }
54
73
  export class ServiceExecutor {
55
74
  services = new Map();
56
75
  allocatedPorts = new Map();
57
76
  waiters = new Map();
58
77
  /** Per-type lock collapsing concurrent start() calls onto a single in-flight start. */
59
78
  startInFlight = new Map();
79
+ /** Per-type stop operation awaited by runtime disposal. */
80
+ stopInFlight = new Map();
81
+ /** Registry deletions that must settle before a replacement can record its PID. */
82
+ pidForgets = new Map();
60
83
  /** Per-type counter bounding automatic EADDRINUSE port re-allocation retries. */
61
84
  portConflictRetries = new Map();
85
+ portConflictTimers = new Map();
62
86
  /** Per-type counter bounding `restartPolicy` revivals after an unexpected exit. */
63
87
  restartRetries = new Map();
64
88
  /** Pending `restartPolicy` timers, cancelled when someone stops or starts the service by hand. */
@@ -68,14 +92,44 @@ export class ServiceExecutor {
68
92
  fs;
69
93
  processRunner;
70
94
  pidRegistry;
95
+ killProcess;
96
+ hookTimeoutMs;
97
+ closing = false;
98
+ closePromise;
71
99
  /** Optional callback invoked on every service status change */
72
100
  onStatusChanged;
101
+ /** Invoked after every owned start attempt, including deferred retries. */
102
+ onStartSettled;
73
103
  constructor(logger, portPool, deps) {
74
104
  this.logger = logger;
75
105
  this.portPool = portPool;
76
106
  this.fs = deps.fs;
77
107
  this.processRunner = deps.process;
78
108
  this.pidRegistry = deps.pidRegistry;
109
+ this.killProcess = deps.kill ?? process.kill;
110
+ this.hookTimeoutMs = deps.hookTimeoutMs ?? DEFAULT_SERVICE_HOOK_TIMEOUT_MS;
111
+ serviceExecutorObserverForTesting?.(this);
112
+ }
113
+ /**
114
+ * Reject when `operation` outlives {@link DEFAULT_SERVICE_HOOK_TIMEOUT_MS}.
115
+ *
116
+ * The operation itself cannot be cancelled — an embedder callback keeps running —
117
+ * but the transition stops waiting on it, which is what releases the lease.
118
+ */
119
+ async withDeadline(label, operation) {
120
+ let timer;
121
+ try {
122
+ return await Promise.race([
123
+ (async () => await operation())(),
124
+ new Promise((_, reject) => {
125
+ timer = setTimeout(() => reject(new Error(`${label} did not settle within ${this.hookTimeoutMs}ms`)), this.hookTimeoutMs);
126
+ }),
127
+ ]);
128
+ }
129
+ finally {
130
+ if (timer)
131
+ clearTimeout(timer);
132
+ }
79
133
  }
80
134
  notifyStatusChanged(sessionId, serviceType, status, details = {}) {
81
135
  this.onStatusChanged?.(sessionId, serviceType, status, details);
@@ -87,7 +141,7 @@ export class ServiceExecutor {
87
141
  if (!config.availableWhen)
88
142
  return Ok(true);
89
143
  try {
90
- return Ok(await config.availableWhen({ sessionId: String(sessionId), workspaceDir }));
144
+ return Ok(await this.withDeadline(`Service '${config.type}' availability check`, () => config.availableWhen?.({ sessionId: String(sessionId), workspaceDir }) ?? true));
91
145
  }
92
146
  catch (error) {
93
147
  const message = error instanceof Error ? error.message : String(error);
@@ -103,7 +157,8 @@ export class ServiceExecutor {
103
157
  recoverable: true,
104
158
  });
105
159
  }
106
- const raw = await config.cwd({ sessionId: String(sessionId), workspaceDir });
160
+ const cwdResolver = config.cwd;
161
+ const raw = await this.withDeadline(`Service '${config.type}' cwd resolver`, () => cwdResolver({ sessionId: String(sessionId), workspaceDir }));
107
162
  return Ok(resolve(workspaceDir, raw));
108
163
  }
109
164
  if (config.cwd !== undefined) {
@@ -172,6 +227,8 @@ export class ServiceExecutor {
172
227
  * Deduping here guarantees exactly one process per type.
173
228
  */
174
229
  async start(config, sessionId, workspaceDir, preferredPort) {
230
+ if (this.closing)
231
+ return Err({ message: 'Service executor is closing', recoverable: false });
175
232
  const inFlight = this.startInFlight.get(config.type);
176
233
  if (inFlight)
177
234
  return inFlight;
@@ -182,16 +239,43 @@ export class ServiceExecutor {
182
239
  }
183
240
  finally {
184
241
  this.startInFlight.delete(config.type);
242
+ this.onStartSettled?.(config.type);
185
243
  }
186
244
  }
187
245
  async startInternal(config, sessionId, workspaceDir, preferredPort) {
246
+ if (this.closing)
247
+ return Err({ message: 'Service executor is closing', recoverable: false });
188
248
  // This start supersedes any revival the policy had queued.
189
249
  this.cancelPendingRestart(config.type);
190
250
  const existing = this.services.get(config.type);
191
251
  if (existing && (existing.status === 'starting' || existing.status === 'ready')) {
192
252
  return Ok(undefined);
193
253
  }
254
+ if (existing?.status === 'stopping') {
255
+ return Err({ message: `Service '${config.type}' is stopping, cannot start`, recoverable: true });
256
+ }
257
+ if (existing) {
258
+ // Whatever sits under this type is about to be overwritten — in this map and in
259
+ // the pid registry, both keyed by (session, type) — and to lose its port to the
260
+ // process spawned below, since a restart deliberately reuses it. A `failed`
261
+ // entry can still own a live process group, so reclaim it before its handle is
262
+ // dropped or nothing will ever be able to find it again.
263
+ const reaped = await this.reapEntry(existing, 'superseded by a new start');
264
+ if (!reaped.ok) {
265
+ this.logger.warn('Could not reclaim the superseded service process group', {
266
+ serviceType: config.type,
267
+ pid: existing.pid,
268
+ error: reaped.error.message,
269
+ });
270
+ }
271
+ await existing.forgetPid();
272
+ }
273
+ await this.pidForgets.get(config.type);
274
+ if (this.closing)
275
+ return Err({ message: 'Service executor is closing', recoverable: false });
194
276
  const availability = await this.isAvailable(config, sessionId, workspaceDir);
277
+ if (this.closing)
278
+ return Err({ message: 'Service executor is closing', recoverable: false });
195
279
  if (!availability.ok)
196
280
  return availability;
197
281
  if (!availability.value) {
@@ -219,9 +303,29 @@ export class ServiceExecutor {
219
303
  return cwdResult;
220
304
  }
221
305
  const cwd = cwdResult.value;
222
- const logBufferSize = config.logBufferSize ?? 200;
306
+ if (this.closing) {
307
+ if (allocatedNow) {
308
+ this.allocatedPorts.delete(config.type);
309
+ this.portPool.release(port);
310
+ }
311
+ return Err({ message: 'Service executor is closing', recoverable: false });
312
+ }
313
+ const logBufferSize = Math.max(1, config.logBufferSize ?? 200);
223
314
  const startupTimeoutMs = config.startupTimeoutMs ?? 30_000;
224
315
  const readyRegex = config.readyPattern ? new RegExp(config.readyPattern) : undefined;
316
+ const logs = new RingBuffer(logBufferSize);
317
+ const maxDiagnosticLength = 16_384;
318
+ const startTime = Date.now();
319
+ let portConflictDetected = false;
320
+ let setupComplete = false;
321
+ let retryPortConflictDuringSetup = false;
322
+ let readyDetectedDuringSetup = false;
323
+ let readyLineDuringSetup;
324
+ let stdoutPartial = '';
325
+ let stderrPartial = '';
326
+ let outputSequence = 0;
327
+ let stdoutPartialSequence = 0;
328
+ let stderrPartialSequence = 0;
225
329
  const startArgs = {
226
330
  port,
227
331
  sessionId: String(sessionId),
@@ -231,12 +335,14 @@ export class ServiceExecutor {
231
335
  let command;
232
336
  let serviceEnv;
233
337
  try {
234
- command = typeof config.command === 'function'
235
- ? await config.command(startArgs)
236
- : config.command;
237
- serviceEnv = typeof config.env === 'function'
238
- ? await config.env(startArgs)
239
- : config.env;
338
+ const commandResolver = config.command;
339
+ command = typeof commandResolver === 'function'
340
+ ? await this.withDeadline(`Service '${config.type}' command resolver`, () => commandResolver(startArgs))
341
+ : commandResolver;
342
+ const envResolver = config.env;
343
+ serviceEnv = typeof envResolver === 'function'
344
+ ? await this.withDeadline(`Service '${config.type}' env resolver`, () => envResolver(startArgs))
345
+ : envResolver;
240
346
  }
241
347
  catch (error) {
242
348
  const message = error instanceof Error ? error.message : String(error);
@@ -248,6 +354,13 @@ export class ServiceExecutor {
248
354
  this.notifyStatusChanged(sessionId, config.type, 'failed', { port, cwd, error: errorMessage });
249
355
  return Err({ message: errorMessage, recoverable: true });
250
356
  }
357
+ if (this.closing) {
358
+ if (allocatedNow) {
359
+ this.allocatedPorts.delete(config.type);
360
+ this.portPool.release(port);
361
+ }
362
+ return Err({ message: 'Service executor is closing', recoverable: false });
363
+ }
251
364
  const shell = process.platform === 'win32' ? 'cmd.exe' : '/bin/sh';
252
365
  const shellFlag = process.platform === 'win32' ? '/c' : '-c';
253
366
  const child = this.processRunner.spawn(shell, [shellFlag, command], {
@@ -263,10 +376,18 @@ export class ServiceExecutor {
263
376
  const current = this.services.get(config.type);
264
377
  if (current && current.process === child && current.status !== 'stopped' && current.status !== 'failed') {
265
378
  current.status = 'failed';
379
+ // The pid rides along on purpose — see the startup timeout below.
266
380
  this.notifyStatusChanged(sessionId, config.type, 'failed', {
267
381
  error: error.message,
268
382
  cwd,
269
383
  command,
384
+ pid: current.pid,
385
+ pidStartTime: current.pidStartTime,
386
+ });
387
+ void this.reapEntry(current, 'process error').then((reaped) => {
388
+ if (!reaped.ok) {
389
+ this.logger.error('Could not kill errored service process group', reaped.error, { serviceType: config.type, pid: current.pid });
390
+ }
270
391
  });
271
392
  }
272
393
  });
@@ -278,29 +399,137 @@ export class ServiceExecutor {
278
399
  this.notifyStatusChanged(sessionId, config.type, 'failed', { port, cwd, command, error: 'Failed to spawn process' });
279
400
  return Err({ message: 'Failed to spawn service process', recoverable: true });
280
401
  }
402
+ // Bind the narrowed pid: the deferred registry call below cannot carry the
403
+ // narrowing across the closure boundary.
404
+ const pid = child.pid;
405
+ const matches = (regex, content) => {
406
+ regex.lastIndex = 0;
407
+ const matched = regex.test(content);
408
+ regex.lastIndex = 0;
409
+ return matched;
410
+ };
411
+ const recordDiagnostic = (line, prefix) => {
412
+ const maxContentLength = Math.max(0, maxDiagnosticLength - prefix.length);
413
+ const bounded = line.length > maxContentLength ? line.slice(-maxContentLength) : line;
414
+ logs.push(`${prefix}${bounded}`);
415
+ };
416
+ // Detection sees the full incoming content before diagnostics are truncated.
417
+ const appendOutput = (partial, data, prefix) => {
418
+ const combined = partial + data.toString();
419
+ if (matches(PORT_CONFLICT_PATTERN, combined))
420
+ portConflictDetected = true;
421
+ const lines = combined.split('\n');
422
+ const nextPartial = lines.pop() ?? '';
423
+ const readyMatched = readyRegex ? matches(readyRegex, combined) || lines.some((line) => matches(readyRegex, line)) : false;
424
+ if (readyMatched && !setupComplete) {
425
+ readyDetectedDuringSetup = true;
426
+ readyLineDuringSetup = combined.slice(-maxDiagnosticLength);
427
+ }
428
+ for (const line of lines)
429
+ recordDiagnostic(line, prefix);
430
+ if (setupComplete) {
431
+ const current = this.services.get(config.type);
432
+ if (current?.process === child && current.status === 'starting') {
433
+ this.logger.debug('Service output', { serviceType: config.type });
434
+ if (readyMatched)
435
+ markReady(combined.slice(-maxDiagnosticLength));
436
+ }
437
+ void checkReadyWhen();
438
+ }
439
+ // Retain enough suffix for markers split across chunks, never an entire line.
440
+ return nextPartial.length > maxDiagnosticLength ? nextPartial.slice(-maxDiagnosticLength) : nextPartial;
441
+ };
442
+ const onStdout = (data) => {
443
+ stdoutPartial = appendOutput(stdoutPartial, data, '');
444
+ stdoutPartialSequence = stdoutPartial ? ++outputSequence : 0;
445
+ };
446
+ const onStderr = (data) => {
447
+ stderrPartial = appendOutput(stderrPartial, data, '[stderr] ');
448
+ stderrPartialSequence = stderrPartial ? ++outputSequence : 0;
449
+ };
450
+ let exitDuringSetup;
451
+ let forgetPromise;
452
+ const forgetPid = () => {
453
+ if (forgetPromise)
454
+ return forgetPromise;
455
+ const previous = this.pidForgets.get(config.type);
456
+ const runForget = () => this.withDeadline(`Service '${config.type}' PID forget`, () => this.pidRegistry?.forget(String(sessionId), config.type) ?? Promise.resolve());
457
+ const operation = previous ? previous.then(runForget, runForget) : runForget();
458
+ forgetPromise = operation;
459
+ this.pidForgets.set(config.type, operation);
460
+ void operation.then(() => {
461
+ if (this.pidForgets.get(config.type) === operation)
462
+ this.pidForgets.delete(config.type);
463
+ }, () => {
464
+ if (this.pidForgets.get(config.type) === operation)
465
+ this.pidForgets.delete(config.type);
466
+ if (forgetPromise === operation)
467
+ forgetPromise = undefined;
468
+ });
469
+ return operation;
470
+ };
471
+ const bufferClose = (code) => {
472
+ exitDuringSetup = { code };
473
+ };
474
+ child.stdout?.on('data', onStdout);
475
+ child.stderr?.on('data', onStderr);
476
+ child.on('close', bufferClose);
281
477
  // Capture start time immediately so a later PID-reuse check can distinguish
282
478
  // "our process" from "an unrelated process that grabbed this PID after ours died"
283
479
  const pidStartTime = await getProcessStartTime(this.fs, child.pid);
284
480
  // Durably record the process so a future agent can reap it if we die before the
285
481
  // `close` handler gets to forget it. The session projection cannot serve that
286
482
  // purpose — see ServicePidRegistry.
287
- await this.pidRegistry?.record({ sessionId: String(sessionId), serviceType: config.type, pid: child.pid, pidStartTime, command });
483
+ try {
484
+ await this.withDeadline(`Service '${config.type}' PID record`, () => this.pidRegistry?.record({ sessionId: String(sessionId), serviceType: config.type, pid, pidStartTime, command })
485
+ ?? Promise.resolve());
486
+ }
487
+ catch (error) {
488
+ // The child is already running but unrecorded, so nothing could reap it
489
+ // after a crash — tear it down here rather than leak it.
490
+ const message = error instanceof Error ? error.message : String(error);
491
+ const errorMessage = `Service '${config.type}' could not record its PID: ${message}`;
492
+ try {
493
+ this.killProcess(-child.pid, 'SIGKILL');
494
+ }
495
+ catch {
496
+ // The process may already be gone.
497
+ }
498
+ if (allocatedNow) {
499
+ this.allocatedPorts.delete(config.type);
500
+ this.portPool.release(port);
501
+ }
502
+ this.notifyStatusChanged(sessionId, config.type, 'failed', { port, cwd, command, error: errorMessage });
503
+ return Err({ message: errorMessage, recoverable: true });
504
+ }
505
+ if (this.closing) {
506
+ try {
507
+ this.killProcess(-child.pid, 'SIGKILL');
508
+ }
509
+ catch {
510
+ // The process may already be gone.
511
+ }
512
+ await forgetPid();
513
+ if (allocatedNow) {
514
+ this.allocatedPorts.delete(config.type);
515
+ this.portPool.release(port);
516
+ }
517
+ return Err({ message: 'Service executor is closing', recoverable: false });
518
+ }
288
519
  // Emit starting event with PID, port, resolved cwd/command, and start time.
289
520
  this.notifyStatusChanged(sessionId, config.type, 'starting', { port, pid: child.pid, pidStartTime, cwd, command });
290
- const logs = new RingBuffer(logBufferSize);
291
- const startTime = Date.now();
292
- // Set when the child reports a port bind-conflict; read by the close handler
293
- // to decide between a fresh-port retry and a terminal failure.
294
- let portConflictDetected = false;
295
521
  const entry = {
296
522
  config,
297
523
  process: child,
298
524
  pid: child.pid,
525
+ pidStartTime,
299
526
  status: 'starting',
300
527
  port,
301
528
  cwd,
302
529
  command,
303
530
  logs,
531
+ forgetPid,
532
+ exited: false,
304
533
  };
305
534
  this.services.set(config.type, entry);
306
535
  let startupTimer;
@@ -359,24 +588,6 @@ export class ServiceExecutor {
359
588
  readyCheckInFlight = false;
360
589
  }
361
590
  };
362
- const processLine = (line) => {
363
- logs.push(line);
364
- if (PORT_CONFLICT_PATTERN.test(line)) {
365
- portConflictDetected = true;
366
- }
367
- const current = this.services.get(config.type);
368
- if (!current || current.process !== child)
369
- return;
370
- if (current.status === 'starting') {
371
- this.logger.debug('Service output', { serviceType: config.type, line });
372
- }
373
- if (readyRegex && current.status === 'starting') {
374
- if (readyRegex.test(line)) {
375
- markReady(line);
376
- }
377
- }
378
- void checkReadyWhen();
379
- };
380
591
  // Startup timeout — mark as failed if not ready in time
381
592
  if (readyRegex || config.readyWhen) {
382
593
  startupTimer = setTimeout(() => {
@@ -387,19 +598,22 @@ export class ServiceExecutor {
387
598
  const errorMsg = `Service startup timed out after ${startupTimeoutMs}ms`;
388
599
  this.logger.error(errorMsg, undefined, { serviceType: config.type });
389
600
  clearReadinessTimers();
601
+ // The pid rides along on purpose: should this agent die before the reap
602
+ // below completes, the recorded pid is all a later boot has to find the
603
+ // survivor. The child's exit takes the claim away again.
390
604
  this.notifyStatusChanged(sessionId, config.type, 'failed', {
391
605
  port: current.port,
392
606
  cwd: current.cwd,
393
607
  command: current.command,
394
608
  error: errorMsg,
609
+ pid: current.pid,
610
+ pidStartTime: current.pidStartTime,
611
+ });
612
+ void this.reapEntry(current, 'startup timeout').then((reaped) => {
613
+ if (!reaped.ok) {
614
+ this.logger.error('Could not kill timed-out service process group', reaped.error, { serviceType: config.type, pid: current.pid });
615
+ }
395
616
  });
396
- // Kill the timed-out process
397
- try {
398
- process.kill(-current.pid, 'SIGKILL');
399
- }
400
- catch {
401
- // Already gone
402
- }
403
617
  }, startupTimeoutMs);
404
618
  }
405
619
  if (config.readyWhen) {
@@ -407,52 +621,38 @@ export class ServiceExecutor {
407
621
  readyCheckTimer = setInterval(() => {
408
622
  void checkReadyWhen();
409
623
  }, intervalMs);
410
- void checkReadyWhen();
411
624
  }
412
- // Pipe stdout/stderr line by line
413
- let stdoutPartial = '';
414
- child.stdout?.on('data', (data) => {
415
- stdoutPartial += data.toString();
416
- const lines = stdoutPartial.split('\n');
417
- stdoutPartial = lines.pop();
418
- for (const line of lines) {
419
- processLine(line);
420
- }
421
- });
422
- let stderrPartial = '';
423
- child.stderr?.on('data', (data) => {
424
- stderrPartial += data.toString();
425
- const lines = stderrPartial.split('\n');
426
- stderrPartial = lines.pop();
427
- for (const line of lines) {
428
- processLine(`[stderr] ${line}`);
429
- }
430
- });
431
- // Handle unexpected exit
432
- child.on('close', (code) => {
625
+ // Handle unexpected exit. Named and guarded because it also has to be
626
+ // replayable — see the exit-during-setup check after markReady() below.
627
+ let closeHandled = false;
628
+ const handleClose = (code) => {
629
+ if (closeHandled)
630
+ return;
631
+ closeHandled = true;
632
+ entry.exited = true;
433
633
  clearReadinessTimers();
434
- // The process is gone, so its durable record has nothing left to reap.
435
- void this.pidRegistry?.forget(String(sessionId), config.type);
436
- // Flush remaining partial lines
437
- if (stdoutPartial) {
438
- processLine(stdoutPartial);
439
- stdoutPartial = '';
440
- }
441
- if (stderrPartial) {
442
- processLine(`[stderr] ${stderrPartial}`);
443
- stderrPartial = '';
634
+ // A replacement must not be recorded until this deletion finishes.
635
+ void forgetPid();
636
+ const partials = [
637
+ { content: stdoutPartial, prefix: '', sequence: stdoutPartialSequence },
638
+ { content: stderrPartial, prefix: '[stderr] ', sequence: stderrPartialSequence },
639
+ ];
640
+ for (const partial of partials.filter(({ content }) => content).sort((left, right) => left.sequence - right.sequence)) {
641
+ recordDiagnostic(partial.content, partial.prefix);
444
642
  }
643
+ stdoutPartial = '';
644
+ stderrPartial = '';
445
645
  const current = this.services.get(config.type);
446
646
  if (!current || current.process !== child)
447
647
  return;
448
648
  if (current.status === 'stopping') {
449
- // Expected stop
649
+ // Expected stop — the child's exit, not stopInternal, is what publishes it.
450
650
  current.status = 'stopped';
451
- this.notifyStatusChanged(sessionId, config.type, 'stopped');
651
+ this.notifyStatusChanged(sessionId, config.type, 'stopped', { stoppedBy: current.stopSource ?? 'agent' });
452
652
  }
453
653
  else if (current.status === 'starting' || current.status === 'ready') {
454
654
  const retries = this.portConflictRetries.get(config.type) ?? 0;
455
- if (portConflictDetected && retries < MAX_PORT_CONFLICT_RETRIES) {
655
+ if (!this.closing && current.status === 'starting' && portConflictDetected && retries < MAX_PORT_CONFLICT_RETRIES) {
456
656
  // The chosen port is held by a foreign process (e.g. a service
457
657
  // leaked from another session, or a survivor of a previous boot).
458
658
  // Re-allocate a fresh port and retry so the service recovers on a
@@ -469,7 +669,18 @@ export class ServiceExecutor {
469
669
  });
470
670
  this.allocatedPorts.delete(config.type);
471
671
  this.services.delete(config.type);
472
- void this.start(config, sessionId, workspaceDir);
672
+ if (!setupComplete) {
673
+ retryPortConflictDuringSetup = true;
674
+ }
675
+ else {
676
+ const retryTimer = setTimeout(() => {
677
+ this.portConflictTimers.delete(config.type);
678
+ if (!this.closing)
679
+ void this.start(config, sessionId, workspaceDir);
680
+ }, 0);
681
+ retryTimer.unref?.();
682
+ this.portConflictTimers.set(config.type, retryTimer);
683
+ }
473
684
  return;
474
685
  }
475
686
  // Unexpected exit
@@ -491,7 +702,16 @@ export class ServiceExecutor {
491
702
  code,
492
703
  });
493
704
  }
494
- });
705
+ else if (current.status === 'failed') {
706
+ // The process this entry still claimed is now confirmed gone — a startup
707
+ // timeout or a process error published the pid without being able to
708
+ // confirm the kill. Take the claim away so no later boot goes looking for
709
+ // it; the status and the error that explain the failure stay as they are.
710
+ this.notifyStatusChanged(sessionId, config.type, 'failed');
711
+ }
712
+ };
713
+ child.on('close', handleClose);
714
+ child.off('close', bufferClose);
495
715
  this.logger.info('Service starting', {
496
716
  serviceType: config.type,
497
717
  pid: child.pid,
@@ -501,10 +721,37 @@ export class ServiceExecutor {
501
721
  readyPattern: config.readyPattern,
502
722
  startupTimeoutMs,
503
723
  });
504
- // If no ready pattern, immediately mark as ready
505
- if (!readyRegex && !config.readyWhen) {
724
+ // A service that died inside the bookkeeping above closed while only the
725
+ // setup close collector was listening, so replay that close now.
726
+ // `alreadyReaped` covers the in-between state: the exit is recorded but
727
+ // 'close' has not fired yet because it waits for the stdio EOF. The listener
728
+ // above is guaranteed to receive it, so calling handleClose here would only
729
+ // drop the tail of the log the failure has to explain — while marking such a
730
+ // child ready would be a lie.
731
+ const alreadyReaped = child.exitCode != null || child.signalCode != null;
732
+ if (exitDuringSetup) {
733
+ handleClose(exitDuringSetup.code);
734
+ }
735
+ setupComplete = true;
736
+ if (retryPortConflictDuringSetup) {
737
+ await forgetPid();
738
+ if (this.closing)
739
+ return Err({ message: 'Service executor is closing', recoverable: false });
740
+ return this.startInternal(config, sessionId, workspaceDir);
741
+ }
742
+ if (exitDuringSetup || alreadyReaped) {
743
+ return Ok(undefined);
744
+ }
745
+ if (readyDetectedDuringSetup) {
746
+ markReady(readyLineDuringSetup);
747
+ }
748
+ else if (!readyRegex && !config.readyWhen) {
749
+ // If no ready condition is configured, a live child is ready immediately.
506
750
  markReady();
507
751
  }
752
+ else if (config.readyWhen) {
753
+ void checkReadyWhen();
754
+ }
508
755
  return Ok(undefined);
509
756
  }
510
757
  /**
@@ -515,6 +762,8 @@ export class ServiceExecutor {
515
762
  * change, or undefined when the service is left failed for good.
516
763
  */
517
764
  scheduleRestart(config, sessionId, workspaceDir, uptimeMs, exitCode) {
765
+ if (this.closing)
766
+ return undefined;
518
767
  const policy = config.restartPolicy;
519
768
  if (!policy)
520
769
  return undefined;
@@ -546,7 +795,8 @@ export class ServiceExecutor {
546
795
  });
547
796
  const timer = setTimeout(() => {
548
797
  this.restartTimers.delete(config.type);
549
- void this.start(config, sessionId, workspaceDir);
798
+ if (!this.closing)
799
+ void this.start(config, sessionId, workspaceDir);
550
800
  }, delayMs);
551
801
  // A pending revival must not keep the process alive on its own.
552
802
  timer.unref?.();
@@ -562,69 +812,379 @@ export class ServiceExecutor {
562
812
  this.restartTimers.delete(serviceType);
563
813
  return true;
564
814
  }
565
- /** Whether a `restartPolicy` revival is queued — the service is down but coming back. */
815
+ /**
816
+ * Whether the service is down but coming back on its own — a `restartPolicy`
817
+ * revival or a queued port-conflict retry. Both are timers this executor
818
+ * owns, so both need the runtime to outlive the call that armed them.
819
+ */
566
820
  hasScheduledRestart(serviceType) {
567
- return this.restartTimers.has(serviceType);
821
+ return this.restartTimers.has(serviceType) || this.portConflictTimers.has(serviceType);
822
+ }
823
+ /**
824
+ * Stop accepting work and drain all runtime-owned processes and timers.
825
+ *
826
+ * `reason: 'evicted'` only parks the session: every service keeps its full
827
+ * graceful window and is recorded as stopped by the runtime, which is what
828
+ * lets autoStart bring it back once the runtime is rebuilt. The other
829
+ * reasons end the session, so a process group that ignores SIGTERM is taken
830
+ * down without waiting.
831
+ */
832
+ close(sessionId, reason = 'closed') {
833
+ this.closing = true;
834
+ this.closePromise ??= this.performClose(sessionId, reason).finally(() => {
835
+ this.onStatusChanged = undefined;
836
+ this.onStartSettled = undefined;
837
+ });
838
+ return this.closePromise;
839
+ }
840
+ async performClose(sessionId, reason) {
841
+ this.cancelPortConflictTimers();
842
+ await Promise.allSettled([...this.startInFlight.values()]);
843
+ this.cancelPortConflictTimers();
844
+ const concurrentStopTypes = new Set(this.stopInFlight.keys());
845
+ await Promise.allSettled([...this.stopInFlight.values()]);
846
+ // One service that cannot be signalled must not strand the others, and must not
847
+ // skip the tail — portPool is process-global, so a skipped release loses those
848
+ // ports for the life of the server.
849
+ const failures = [];
850
+ const parking = reason === 'evicted';
851
+ try {
852
+ for (const [serviceType, entry] of [...this.services]) {
853
+ if (entry.status === 'stopped')
854
+ continue;
855
+ if (entry.status === 'starting' || entry.status === 'ready' || entry.status === 'stopping' || this.restartTimers.has(serviceType)) {
856
+ const forcedGraceMs = parking ? entry.config.gracefulStopMs ?? 5000 : 0;
857
+ try {
858
+ if (concurrentStopTypes.has(serviceType)) {
859
+ await this.forceStopForClose(serviceType, entry, sessionId, forcedGraceMs);
860
+ continue;
861
+ }
862
+ const stopped = await this.stop(serviceType, sessionId, 'eviction');
863
+ if (!stopped.ok)
864
+ await this.forceStopForClose(serviceType, entry, sessionId, forcedGraceMs);
865
+ }
866
+ catch (error) {
867
+ failures.push(error);
868
+ }
869
+ }
870
+ }
871
+ }
872
+ finally {
873
+ this.cancelAllDeferredStarts();
874
+ await Promise.allSettled([...this.pidForgets.values()]);
875
+ for (const port of this.allocatedPorts.values())
876
+ this.portPool.release(port);
877
+ this.allocatedPorts.clear();
878
+ this.services.clear();
879
+ for (const serviceType of this.waiters.keys())
880
+ this.resolveWaiters(serviceType, 'stopped');
881
+ }
882
+ // Still reject — a close that could not terminate a process is not a clean close.
883
+ if (failures.length === 1)
884
+ throw failures[0];
885
+ if (failures.length > 1) {
886
+ throw new AggregateError(failures, `Failed to stop ${failures.length} services during close`);
887
+ }
888
+ }
889
+ async forceStopForClose(serviceType, entry, sessionId, gracefulStopMs) {
890
+ const previousStatus = entry.status;
891
+ this.recordStopSource(entry, 'eviction');
892
+ if (entry.status !== 'stopping') {
893
+ entry.status = 'stopping';
894
+ this.notifyStatusChanged(sessionId, serviceType, 'stopping');
895
+ }
896
+ const stopped = await this.terminateProcessGroup(entry.pid, gracefulStopMs);
897
+ if (!stopped.ok) {
898
+ // Mirror stopInternal: no recovery path matches a stranded `stopping`, so a
899
+ // force-stop that failed has to leave a status the next runtime can reap.
900
+ if (entry.status === 'stopping' && previousStatus !== 'stopping') {
901
+ entry.status = previousStatus;
902
+ this.notifyStatusChanged(sessionId, serviceType, previousStatus, {
903
+ port: entry.port,
904
+ cwd: entry.cwd,
905
+ command: entry.command,
906
+ error: stopped.error.message,
907
+ });
908
+ }
909
+ throw stopped.error;
910
+ }
911
+ // The group is confirmed gone, so nothing may offer this pid up for a reap again.
912
+ entry.exited = true;
913
+ await entry.forgetPid();
914
+ if (this.services.get(serviceType) === entry) {
915
+ entry.status = 'stopped';
916
+ this.notifyStatusChanged(sessionId, serviceType, 'stopped', { stoppedBy: entry.stopSource ?? 'eviction' });
917
+ }
918
+ }
919
+ /** An agent stop stays the agent's decision even when a lifecycle stop is what finishes it. */
920
+ recordStopSource(entry, stoppedBy) {
921
+ if (stoppedBy === 'agent' || entry.stopSource === undefined)
922
+ entry.stopSource = stoppedBy;
923
+ }
924
+ cancelAllDeferredStarts() {
925
+ for (const timer of this.restartTimers.values())
926
+ clearTimeout(timer);
927
+ this.restartTimers.clear();
928
+ this.cancelPortConflictTimers();
929
+ }
930
+ cancelPortConflictTimers() {
931
+ for (const timer of this.portConflictTimers.values())
932
+ clearTimeout(timer);
933
+ this.portConflictTimers.clear();
934
+ }
935
+ errorWithContext(error, context) {
936
+ const cause = error instanceof Error ? error.message : String(error);
937
+ return new Error(`${context}: ${cause}`);
938
+ }
939
+ hasErrorCode(error, code) {
940
+ return error instanceof Error && 'code' in error && error.code === code;
941
+ }
942
+ async hasNonZombieProcessGroupMember(pid) {
943
+ if (process.platform !== 'linux')
944
+ return true;
945
+ let entries;
946
+ try {
947
+ entries = await this.fs.readdir('/proc');
948
+ }
949
+ catch {
950
+ return true;
951
+ }
952
+ let foundGroupMember = false;
953
+ for (const entry of entries) {
954
+ if (!/^\d+$/.test(entry))
955
+ continue;
956
+ try {
957
+ const stat = await this.fs.readFile(`/proc/${entry}/stat`, 'utf-8');
958
+ const rparen = stat.lastIndexOf(')');
959
+ if (rparen === -1)
960
+ continue;
961
+ const fields = stat.slice(rparen + 2).split(' ');
962
+ if (Number(fields[2]) !== pid)
963
+ continue;
964
+ foundGroupMember = true;
965
+ if (fields[0] !== 'Z')
966
+ return true;
967
+ }
968
+ catch {
969
+ // Processes can disappear while /proc is scanned.
970
+ }
971
+ }
972
+ // A successful group probe with no readable member is still evidence of life.
973
+ return !foundGroupMember;
974
+ }
975
+ async probeProcessGroup(pid) {
976
+ try {
977
+ this.killProcess(-pid, 0);
978
+ }
979
+ catch (error) {
980
+ if (this.hasErrorCode(error, 'ESRCH'))
981
+ return { state: 'gone' };
982
+ if (this.hasErrorCode(error, 'EPERM'))
983
+ return { state: 'alive' };
984
+ return { state: 'error', error: this.errorWithContext(error, `Failed to probe process group ${pid}`) };
985
+ }
986
+ return await this.hasNonZombieProcessGroupMember(pid) ? { state: 'alive' } : { state: 'gone' };
987
+ }
988
+ async signalProcessGroup(pid, signal) {
989
+ try {
990
+ this.killProcess(-pid, signal);
991
+ return { state: 'alive' };
992
+ }
993
+ catch (error) {
994
+ if (this.hasErrorCode(error, 'ESRCH'))
995
+ return { state: 'gone' };
996
+ return { state: 'error', error: this.errorWithContext(error, `Failed to send ${signal} to process group ${pid}`) };
997
+ }
998
+ }
999
+ async waitForProcessGroupExit(pid, timeoutMs) {
1000
+ const initial = await this.probeProcessGroup(pid);
1001
+ if (initial.state !== 'alive')
1002
+ return initial;
1003
+ return new Promise((resolve) => {
1004
+ let settled = false;
1005
+ const finish = (result) => {
1006
+ if (settled)
1007
+ return;
1008
+ settled = true;
1009
+ clearInterval(checkInterval);
1010
+ clearTimeout(timeout);
1011
+ resolve(result);
1012
+ };
1013
+ const checkInterval = setInterval(() => {
1014
+ void this.probeProcessGroup(pid).then((result) => {
1015
+ if (result.state !== 'alive')
1016
+ finish(result);
1017
+ });
1018
+ }, 50);
1019
+ const timeout = setTimeout(() => {
1020
+ void this.probeProcessGroup(pid).then(finish);
1021
+ }, timeoutMs);
1022
+ });
1023
+ }
1024
+ /**
1025
+ * Reclaim a service process group we spawned, unless the kernel recycled its pid.
1026
+ *
1027
+ * Every path that gives up on a service comes through here, because ending the
1028
+ * *wait* is not ending the *process*: a dev server that missed its readiness
1029
+ * window keeps its several hundred MB of RSS until somebody kills it, which on a
1030
+ * memory-starved box is what makes the next startup miss its window too.
1031
+ *
1032
+ * `pidStartTime` was captured at spawn, so a different current start time means
1033
+ * the pid now belongs to an unrelated process and killing it would be disastrous.
1034
+ * An unknown start time (non-Linux, no /proc) leaves only the kill-and-hope path.
1035
+ */
1036
+ async reapProcessGroup(target, context = {}) {
1037
+ const currentStartTime = await getProcessStartTime(this.fs, target.pid);
1038
+ if (target.pidStartTime !== undefined && currentStartTime !== undefined && currentStartTime !== target.pidStartTime) {
1039
+ this.logger.warn('PID reuse detected — refusing to kill service process group', {
1040
+ ...context,
1041
+ pid: target.pid,
1042
+ storedStartTime: target.pidStartTime,
1043
+ currentStartTime,
1044
+ });
1045
+ return Ok('pid-reused');
1046
+ }
1047
+ const killed = this.killProcessGroup(target.pid);
1048
+ if (!killed.ok)
1049
+ return killed;
1050
+ return Ok('killed');
1051
+ }
1052
+ /**
1053
+ * Reclaim the process group of an entry we are giving up on or replacing.
1054
+ *
1055
+ * A no-op once the child's `close` has fired: there is nothing left to reclaim,
1056
+ * and skipping it also keeps the non-Linux kill-and-hope path away from a pid the
1057
+ * kernel may since have handed to somebody else. A settled reap ends our claim,
1058
+ * so the entry is never offered up twice.
1059
+ */
1060
+ async reapEntry(entry, reason) {
1061
+ if (entry.exited)
1062
+ return Ok(undefined);
1063
+ const reaped = await this.reapProcessGroup({ pid: entry.pid, pidStartTime: entry.pidStartTime }, { serviceType: entry.config.type, reason });
1064
+ if (!reaped.ok)
1065
+ return reaped;
1066
+ entry.exited = true;
1067
+ return Ok(undefined);
1068
+ }
1069
+ /**
1070
+ * SIGKILL a process group through the injectable kill seam, without waiting
1071
+ * for it to go. Reached through {@link reapProcessGroup} wherever a recorded
1072
+ * pid could have been recycled.
1073
+ */
1074
+ killProcessGroup(pid) {
1075
+ try {
1076
+ this.killProcess(-pid, 'SIGKILL');
1077
+ return Ok(undefined);
1078
+ }
1079
+ catch (error) {
1080
+ if (this.hasErrorCode(error, 'ESRCH'))
1081
+ return Ok(undefined); // Already gone.
1082
+ return Err(this.errorWithContext(error, `Failed to SIGKILL process group ${pid}`));
1083
+ }
1084
+ }
1085
+ async terminateProcessGroup(pid, gracefulStopMs) {
1086
+ const initial = await this.probeProcessGroup(pid);
1087
+ if (initial.state === 'gone')
1088
+ return Ok(undefined);
1089
+ if (initial.state === 'error')
1090
+ return Err(initial.error);
1091
+ const term = await this.signalProcessGroup(pid, 'SIGTERM');
1092
+ if (term.state === 'gone')
1093
+ return Ok(undefined);
1094
+ if (term.state === 'error')
1095
+ return Err(term.error);
1096
+ const graceful = await this.waitForProcessGroupExit(pid, gracefulStopMs);
1097
+ if (graceful.state === 'gone')
1098
+ return Ok(undefined);
1099
+ if (graceful.state === 'error')
1100
+ return Err(graceful.error);
1101
+ const kill = await this.signalProcessGroup(pid, 'SIGKILL');
1102
+ if (kill.state === 'gone')
1103
+ return Ok(undefined);
1104
+ if (kill.state === 'error')
1105
+ return Err(kill.error);
1106
+ const forced = await this.waitForProcessGroupExit(pid, 5000);
1107
+ if (forced.state === 'gone')
1108
+ return Ok(undefined);
1109
+ if (forced.state === 'error')
1110
+ return Err(forced.error);
1111
+ return Err(new Error(`Process group ${pid} survived SIGKILL`));
568
1112
  }
569
1113
  /**
570
1114
  * Stop a running service gracefully.
571
1115
  * Port is NOT released — kept for session-level stability across restarts.
572
1116
  */
573
- async stop(serviceType, sessionId) {
1117
+ async stop(serviceType, sessionId, stoppedBy = 'agent') {
1118
+ const inFlight = this.stopInFlight.get(serviceType);
1119
+ if (inFlight)
1120
+ return inFlight;
1121
+ let operation;
1122
+ operation = this.stopInternal(serviceType, sessionId, stoppedBy).finally(() => {
1123
+ if (this.stopInFlight.get(serviceType) === operation)
1124
+ this.stopInFlight.delete(serviceType);
1125
+ });
1126
+ this.stopInFlight.set(serviceType, operation);
1127
+ return operation;
1128
+ }
1129
+ async stopInternal(serviceType, sessionId, stoppedBy) {
574
1130
  const hadPendingRestart = this.cancelPendingRestart(serviceType);
575
1131
  this.restartRetries.delete(serviceType);
576
1132
  const entry = this.services.get(serviceType);
577
1133
  if (!entry) {
578
1134
  return Err({ message: `Service '${serviceType}' not found`, recoverable: false });
579
1135
  }
580
- if (entry.status !== 'starting' && entry.status !== 'ready' && entry.status !== 'paused') {
581
- // A failed service with a revival queued is really "about to restart",
582
- // and calling that off is a legitimate stop rather than an error.
583
- if (hadPendingRestart) {
1136
+ if (entry.status !== 'starting' && entry.status !== 'ready') {
1137
+ // `failed` says the start gave up, not that the process did — and refusing to
1138
+ // stop it left the platform-side reaper, the one caller that can free a leaked
1139
+ // dev server's memory, with no way to do so. A failed service with a revival
1140
+ // queued is on top of that really "about to restart", and calling that off is a
1141
+ // legitimate stop rather than an error.
1142
+ if (entry.status === 'failed' || hadPendingRestart) {
1143
+ const reaped = await this.reapEntry(entry, 'stopped while failed');
1144
+ if (!reaped.ok)
1145
+ return Err({ message: reaped.error.message, recoverable: true });
1146
+ try {
1147
+ await entry.forgetPid();
1148
+ }
1149
+ catch (error) {
1150
+ return Err({ message: this.errorWithContext(error, `Failed to forget PID for service '${serviceType}'`).message, recoverable: true });
1151
+ }
584
1152
  entry.status = 'stopped';
585
- this.notifyStatusChanged(sessionId, serviceType, 'stopped');
1153
+ this.notifyStatusChanged(sessionId, serviceType, 'stopped', { stoppedBy });
586
1154
  return Ok(undefined);
587
1155
  }
588
1156
  return Err({ message: `Service '${serviceType}' is ${entry.status}, cannot stop`, recoverable: false });
589
1157
  }
1158
+ const previousStatus = entry.status;
1159
+ this.recordStopSource(entry, stoppedBy);
590
1160
  entry.status = 'stopping';
591
1161
  this.notifyStatusChanged(sessionId, serviceType, 'stopping');
592
1162
  const gracefulStopMs = entry.config.gracefulStopMs ?? 5000;
593
- // Send SIGTERM
1163
+ const stopped = await this.terminateProcessGroup(entry.pid, gracefulStopMs);
1164
+ if (!stopped.ok) {
1165
+ if (entry.status === 'stopping') {
1166
+ entry.status = previousStatus;
1167
+ this.notifyStatusChanged(sessionId, serviceType, previousStatus, {
1168
+ port: entry.port,
1169
+ cwd: entry.cwd,
1170
+ command: entry.command,
1171
+ error: stopped.error.message,
1172
+ });
1173
+ }
1174
+ return Err({ message: stopped.error.message, recoverable: true });
1175
+ }
1176
+ // The group is confirmed gone, so nothing may offer this pid up for a reap again.
1177
+ entry.exited = true;
594
1178
  try {
595
- process.kill(-entry.pid, 'SIGTERM');
1179
+ await entry.forgetPid();
596
1180
  }
597
- catch {
598
- // Process already gone
1181
+ catch (error) {
1182
+ return Err({ message: this.errorWithContext(error, `Failed to forget PID for service '${serviceType}'`).message, recoverable: true });
1183
+ }
1184
+ if (entry.status === 'stopping') {
599
1185
  entry.status = 'stopped';
600
- this.notifyStatusChanged(sessionId, serviceType, 'stopped');
601
- return Ok(undefined);
1186
+ this.notifyStatusChanged(sessionId, serviceType, 'stopped', { stoppedBy: entry.stopSource ?? stoppedBy });
602
1187
  }
603
- // Wait for graceful shutdown, then SIGKILL
604
- await new Promise((resolve) => {
605
- const checkInterval = setInterval(() => {
606
- try {
607
- // Check if process is still alive (signal 0 doesn't kill, just checks)
608
- process.kill(entry.pid, 0);
609
- }
610
- catch {
611
- // Process gone
612
- clearInterval(checkInterval);
613
- clearTimeout(killTimeout);
614
- resolve();
615
- }
616
- }, 200);
617
- const killTimeout = setTimeout(() => {
618
- clearInterval(checkInterval);
619
- try {
620
- process.kill(-entry.pid, 'SIGKILL');
621
- }
622
- catch {
623
- // Already gone
624
- }
625
- resolve();
626
- }, gracefulStopMs);
627
- });
628
1188
  this.logger.info('Service stopped', { serviceType });
629
1189
  return Ok(undefined);
630
1190
  }
@@ -633,61 +1193,13 @@ export class ServiceExecutor {
633
1193
  */
634
1194
  async restart(config, sessionId, workspaceDir, preferredPort) {
635
1195
  const entry = this.services.get(config.type);
636
- if (entry && (entry.status === 'starting' || entry.status === 'ready' || entry.status === 'paused')) {
1196
+ if (entry && (entry.status === 'starting' || entry.status === 'ready')) {
637
1197
  const stopResult = await this.stop(config.type, sessionId);
638
1198
  if (!stopResult.ok)
639
1199
  return stopResult;
640
1200
  }
641
1201
  return this.start(config, sessionId, workspaceDir, preferredPort);
642
1202
  }
643
- /**
644
- * Pause a running service (SIGSTOP).
645
- */
646
- async pause(serviceType, sessionId) {
647
- const entry = this.services.get(serviceType);
648
- if (!entry) {
649
- return Err({ message: `Service '${serviceType}' not found`, recoverable: false });
650
- }
651
- if (entry.status !== 'ready') {
652
- return Err({ message: `Service '${serviceType}' is ${entry.status}, cannot pause`, recoverable: false });
653
- }
654
- try {
655
- process.kill(entry.pid, 'SIGSTOP');
656
- }
657
- catch {
658
- return Err({ message: `Failed to pause service '${serviceType}'`, recoverable: false });
659
- }
660
- entry.status = 'paused';
661
- this.notifyStatusChanged(sessionId, serviceType, 'paused');
662
- this.logger.info('Service paused', { serviceType });
663
- return Ok(undefined);
664
- }
665
- /**
666
- * Resume a paused service (SIGCONT).
667
- */
668
- async resume(config, sessionId, _workspaceDir) {
669
- const entry = this.services.get(config.type);
670
- if (!entry) {
671
- return Err({ message: `Service '${config.type}' not found`, recoverable: false });
672
- }
673
- if (entry.status !== 'paused') {
674
- return Err({ message: `Service '${config.type}' is ${entry.status}, cannot resume`, recoverable: false });
675
- }
676
- try {
677
- process.kill(entry.pid, 'SIGCONT');
678
- }
679
- catch {
680
- return Err({ message: `Failed to resume service '${config.type}'`, recoverable: false });
681
- }
682
- entry.status = 'ready';
683
- this.notifyStatusChanged(sessionId, config.type, 'ready', {
684
- port: entry.port,
685
- cwd: entry.cwd,
686
- command: entry.command,
687
- });
688
- this.logger.info('Service resumed', { serviceType: config.type });
689
- return Ok(undefined);
690
- }
691
1203
  /**
692
1204
  * Get recent log lines for a service.
693
1205
  */
@@ -711,67 +1223,5 @@ export class ServiceExecutor {
711
1223
  const status = this.getStatus(serviceType);
712
1224
  return status === 'starting' || status === 'ready';
713
1225
  }
714
- /**
715
- * Shutdown all services and release all ports back to pool.
716
- * Called on session close.
717
- */
718
- async shutdown() {
719
- // Nothing queued may spawn after the executor is gone.
720
- for (const serviceType of [...this.restartTimers.keys()])
721
- this.cancelPendingRestart(serviceType);
722
- this.restartRetries.clear();
723
- const promises = [];
724
- for (const [serviceType, entry] of this.services) {
725
- if (entry.status === 'starting' || entry.status === 'ready' || entry.status === 'paused') {
726
- const gracefulStopMs = entry.config.gracefulStopMs ?? 5000;
727
- const killPromise = new Promise((resolve) => {
728
- try {
729
- process.kill(-entry.pid, 'SIGTERM');
730
- }
731
- catch {
732
- resolve();
733
- return;
734
- }
735
- const killTimeout = setTimeout(() => {
736
- try {
737
- process.kill(-entry.pid, 'SIGKILL');
738
- }
739
- catch {
740
- // Already gone
741
- }
742
- resolve();
743
- }, gracefulStopMs);
744
- const checkInterval = setInterval(() => {
745
- try {
746
- process.kill(entry.pid, 0);
747
- }
748
- catch {
749
- clearInterval(checkInterval);
750
- clearTimeout(killTimeout);
751
- resolve();
752
- }
753
- }, 200);
754
- });
755
- promises.push(killPromise);
756
- entry.status = 'stopping';
757
- this.logger.info('Shutting down service', { serviceType });
758
- }
759
- }
760
- await Promise.all(promises);
761
- this.services.clear();
762
- // Drain all waiters with error
763
- for (const [serviceType, pending] of this.waiters) {
764
- for (const waiter of pending) {
765
- clearTimeout(waiter.timer);
766
- waiter.resolve(Err({ message: `Service '${serviceType}' shut down`, recoverable: false }));
767
- }
768
- }
769
- this.waiters.clear();
770
- // Release all allocated ports back to pool
771
- for (const port of this.allocatedPorts.values()) {
772
- this.portPool.release(port);
773
- }
774
- this.allocatedPorts.clear();
775
- }
776
1226
  }
777
1227
  //# sourceMappingURL=service.js.map