@roj-ai/sdk 0.1.36 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (925) 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 +202 -87
  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 +3 -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.js +2 -2
  42. package/dist/core/events/base-event-store.d.ts.map +1 -1
  43. package/dist/core/events/base-event-store.js +22 -14
  44. package/dist/core/events/base-event-store.js.map +1 -1
  45. package/dist/core/events/file.d.ts +39 -1
  46. package/dist/core/events/file.d.ts.map +1 -1
  47. package/dist/core/events/file.js +159 -20
  48. package/dist/core/events/file.js.map +1 -1
  49. package/dist/core/events/types.d.ts +1 -1
  50. package/dist/core/file-store/containment.d.ts +19 -0
  51. package/dist/core/file-store/containment.d.ts.map +1 -0
  52. package/dist/core/file-store/containment.js +62 -0
  53. package/dist/core/file-store/containment.js.map +1 -0
  54. package/dist/core/file-store/file-store.d.ts +22 -2
  55. package/dist/core/file-store/file-store.d.ts.map +1 -1
  56. package/dist/core/file-store/file-store.js +88 -20
  57. package/dist/core/file-store/file-store.js.map +1 -1
  58. package/dist/core/file-store/types.d.ts +13 -1
  59. package/dist/core/file-store/types.d.ts.map +1 -1
  60. package/dist/core/image/image-processor.d.ts.map +1 -1
  61. package/dist/core/image/image-processor.js +3 -1
  62. package/dist/core/image/image-processor.js.map +1 -1
  63. package/dist/core/image/types.d.ts +2 -0
  64. package/dist/core/image/types.d.ts.map +1 -1
  65. package/dist/core/image/vips-resizer.d.ts.map +1 -1
  66. package/dist/core/image/vips-resizer.js +81 -33
  67. package/dist/core/image/vips-resizer.js.map +1 -1
  68. package/dist/core/llm/anthropic.d.ts +0 -1
  69. package/dist/core/llm/anthropic.d.ts.map +1 -1
  70. package/dist/core/llm/anthropic.js +58 -72
  71. package/dist/core/llm/anthropic.js.map +1 -1
  72. package/dist/core/llm/logger.d.ts +14 -2
  73. package/dist/core/llm/logger.d.ts.map +1 -1
  74. package/dist/core/llm/logger.js +161 -36
  75. package/dist/core/llm/logger.js.map +1 -1
  76. package/dist/core/llm/openrouter.d.ts +0 -1
  77. package/dist/core/llm/openrouter.d.ts.map +1 -1
  78. package/dist/core/llm/openrouter.js +53 -66
  79. package/dist/core/llm/openrouter.js.map +1 -1
  80. package/dist/core/llm/provider-request.d.ts +10 -0
  81. package/dist/core/llm/provider-request.d.ts.map +1 -0
  82. package/dist/core/llm/provider-request.js +41 -0
  83. package/dist/core/llm/provider-request.js.map +1 -0
  84. package/dist/core/llm/provider.d.ts +11 -0
  85. package/dist/core/llm/provider.d.ts.map +1 -1
  86. package/dist/core/llm/provider.js +26 -0
  87. package/dist/core/llm/provider.js.map +1 -1
  88. package/dist/core/llm/state.d.ts +2 -2
  89. package/dist/core/llm/state.js +4 -4
  90. package/dist/core/plugins/plugin-builder.d.ts +57 -3
  91. package/dist/core/plugins/plugin-builder.d.ts.map +1 -1
  92. package/dist/core/plugins/plugin-builder.js +68 -14
  93. package/dist/core/plugins/plugin-builder.js.map +1 -1
  94. package/dist/core/preset/preset-builder.d.ts +3 -2
  95. package/dist/core/preset/preset-builder.d.ts.map +1 -1
  96. package/dist/core/preset/preset-builder.js +7 -16
  97. package/dist/core/preset/preset-builder.js.map +1 -1
  98. package/dist/core/sessions/context.d.ts +23 -5
  99. package/dist/core/sessions/context.d.ts.map +1 -1
  100. package/dist/core/sessions/runtime-activity.d.ts +33 -0
  101. package/dist/core/sessions/runtime-activity.d.ts.map +1 -0
  102. package/dist/core/sessions/runtime-activity.js +55 -0
  103. package/dist/core/sessions/runtime-activity.js.map +1 -0
  104. package/dist/core/sessions/schema.d.ts +23 -2
  105. package/dist/core/sessions/schema.d.ts.map +1 -1
  106. package/dist/core/sessions/schema.js +24 -3
  107. package/dist/core/sessions/schema.js.map +1 -1
  108. package/dist/core/sessions/session-manager.d.ts +83 -6
  109. package/dist/core/sessions/session-manager.d.ts.map +1 -1
  110. package/dist/core/sessions/session-manager.js +517 -118
  111. package/dist/core/sessions/session-manager.js.map +1 -1
  112. package/dist/core/sessions/session-store.d.ts +18 -0
  113. package/dist/core/sessions/session-store.d.ts.map +1 -1
  114. package/dist/core/sessions/session-store.js +33 -0
  115. package/dist/core/sessions/session-store.js.map +1 -1
  116. package/dist/core/sessions/session.d.ts +42 -6
  117. package/dist/core/sessions/session.d.ts.map +1 -1
  118. package/dist/core/sessions/session.js +176 -44
  119. package/dist/core/sessions/session.js.map +1 -1
  120. package/dist/core/sessions/state.d.ts +10 -1
  121. package/dist/core/sessions/state.d.ts.map +1 -1
  122. package/dist/core/sessions/state.js +11 -1
  123. package/dist/core/sessions/state.js.map +1 -1
  124. package/dist/core/system.d.ts +9 -0
  125. package/dist/core/system.d.ts.map +1 -1
  126. package/dist/core/system.js +4 -1
  127. package/dist/core/system.js.map +1 -1
  128. package/dist/core/tools/executor.d.ts +1 -1
  129. package/dist/core/wake-key.d.ts +34 -0
  130. package/dist/core/wake-key.d.ts.map +1 -0
  131. package/dist/core/wake-key.js +51 -0
  132. package/dist/core/wake-key.js.map +1 -0
  133. package/dist/index.d.ts +25 -13
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +23 -8
  136. package/dist/index.js.map +1 -1
  137. package/dist/lib/archive/archive-inspection.d.ts +48 -0
  138. package/dist/lib/archive/archive-inspection.d.ts.map +1 -0
  139. package/dist/lib/archive/archive-inspection.fixtures.d.ts +7 -0
  140. package/dist/lib/archive/archive-inspection.fixtures.d.ts.map +1 -0
  141. package/dist/lib/archive/archive-inspection.fixtures.js +134 -0
  142. package/dist/lib/archive/archive-inspection.fixtures.js.map +1 -0
  143. package/dist/lib/archive/archive-inspection.js +253 -0
  144. package/dist/lib/archive/archive-inspection.js.map +1 -0
  145. package/dist/lib/archive/index.d.ts +3 -0
  146. package/dist/lib/archive/index.d.ts.map +1 -0
  147. package/dist/lib/archive/index.js +2 -0
  148. package/dist/lib/archive/index.js.map +1 -0
  149. package/dist/lib/logger/console.d.ts +5 -12
  150. package/dist/lib/logger/console.d.ts.map +1 -1
  151. package/dist/lib/logger/console.js +5 -47
  152. package/dist/lib/logger/console.js.map +1 -1
  153. package/dist/lib/logger/file.d.ts +8 -11
  154. package/dist/lib/logger/file.d.ts.map +1 -1
  155. package/dist/lib/logger/file.js +26 -37
  156. package/dist/lib/logger/file.js.map +1 -1
  157. package/dist/lib/logger/index.d.ts +3 -1
  158. package/dist/lib/logger/index.d.ts.map +1 -1
  159. package/dist/lib/logger/index.js +3 -1
  160. package/dist/lib/logger/index.js.map +1 -1
  161. package/dist/lib/logger/jsonl.d.ts +21 -0
  162. package/dist/lib/logger/jsonl.d.ts.map +1 -0
  163. package/dist/lib/logger/jsonl.js +29 -0
  164. package/dist/lib/logger/jsonl.js.map +1 -0
  165. package/dist/lib/logger/logger.d.ts +22 -0
  166. package/dist/lib/logger/logger.d.ts.map +1 -1
  167. package/dist/lib/logger/logger.js +37 -0
  168. package/dist/lib/logger/logger.js.map +1 -1
  169. package/dist/lib/logger/session-log.d.ts +21 -0
  170. package/dist/lib/logger/session-log.d.ts.map +1 -0
  171. package/dist/lib/logger/session-log.js +37 -0
  172. package/dist/lib/logger/session-log.js.map +1 -0
  173. package/dist/lib/utils/concurrency.d.ts +1 -1
  174. package/dist/lib/utils/concurrency.d.ts.map +1 -1
  175. package/dist/lib/utils/concurrency.js +30 -7
  176. package/dist/lib/utils/concurrency.js.map +1 -1
  177. package/dist/lib/utils/fs-batch.d.ts +17 -0
  178. package/dist/lib/utils/fs-batch.d.ts.map +1 -0
  179. package/dist/lib/utils/fs-batch.js +38 -0
  180. package/dist/lib/utils/fs-batch.js.map +1 -0
  181. package/dist/lib/utils/result.d.ts +7 -22
  182. package/dist/lib/utils/result.d.ts.map +1 -1
  183. package/dist/lib/utils/result.js +6 -17
  184. package/dist/lib/utils/result.js.map +1 -1
  185. package/dist/lib/utils/sleep.d.ts +10 -0
  186. package/dist/lib/utils/sleep.d.ts.map +1 -0
  187. package/dist/lib/utils/sleep.js +26 -0
  188. package/dist/lib/utils/sleep.js.map +1 -0
  189. package/dist/platform/fs-revision.d.ts +21 -0
  190. package/dist/platform/fs-revision.d.ts.map +1 -0
  191. package/dist/platform/fs-revision.js +9 -0
  192. package/dist/platform/fs-revision.js.map +1 -0
  193. package/dist/platform/fs.d.ts +104 -1
  194. package/dist/platform/fs.d.ts.map +1 -1
  195. package/dist/platform/git.d.ts +59 -0
  196. package/dist/platform/git.d.ts.map +1 -0
  197. package/dist/platform/git.js +8 -0
  198. package/dist/platform/git.js.map +1 -0
  199. package/dist/platform/index.d.ts +40 -1
  200. package/dist/platform/index.d.ts.map +1 -1
  201. package/dist/platform/index.js +1 -1
  202. package/dist/platform/index.js.map +1 -1
  203. package/dist/platform/llm-call-log.d.ts +80 -0
  204. package/dist/platform/llm-call-log.d.ts.map +1 -0
  205. package/dist/platform/llm-call-log.js +12 -0
  206. package/dist/platform/llm-call-log.js.map +1 -0
  207. package/dist/platform/scheduler.d.ts +32 -0
  208. package/dist/platform/scheduler.d.ts.map +1 -0
  209. package/dist/platform/scheduler.js +46 -0
  210. package/dist/platform/scheduler.js.map +1 -0
  211. package/dist/platform/session-log.d.ts +36 -0
  212. package/dist/platform/session-log.d.ts.map +1 -0
  213. package/dist/platform/session-log.js +12 -0
  214. package/dist/platform/session-log.js.map +1 -0
  215. package/dist/platform/shell.d.ts +83 -0
  216. package/dist/platform/shell.d.ts.map +1 -0
  217. package/dist/platform/shell.js +13 -0
  218. package/dist/platform/shell.js.map +1 -0
  219. package/dist/plugins/agent-status/plugin.d.ts.map +1 -1
  220. package/dist/plugins/agent-status/plugin.js +1 -0
  221. package/dist/plugins/agent-status/plugin.js.map +1 -1
  222. package/dist/plugins/agents/plugin.d.ts.map +1 -1
  223. package/dist/plugins/agents/plugin.js +129 -62
  224. package/dist/plugins/agents/plugin.js.map +1 -1
  225. package/dist/plugins/filesystem/listing.d.ts +3 -0
  226. package/dist/plugins/filesystem/listing.d.ts.map +1 -1
  227. package/dist/plugins/filesystem/listing.js +66 -10
  228. package/dist/plugins/filesystem/listing.js.map +1 -1
  229. package/dist/plugins/filesystem/plugin.d.ts.map +1 -1
  230. package/dist/plugins/filesystem/plugin.js +8 -6
  231. package/dist/plugins/filesystem/plugin.js.map +1 -1
  232. package/dist/plugins/git-status/index.d.ts +1 -0
  233. package/dist/plugins/git-status/index.d.ts.map +1 -1
  234. package/dist/plugins/git-status/plugin.d.ts +38 -7
  235. package/dist/plugins/git-status/plugin.d.ts.map +1 -1
  236. package/dist/plugins/git-status/plugin.js +169 -64
  237. package/dist/plugins/git-status/plugin.js.map +1 -1
  238. package/dist/plugins/llm-debug/plugin.d.ts.map +1 -1
  239. package/dist/plugins/llm-debug/plugin.js +1 -0
  240. package/dist/plugins/llm-debug/plugin.js.map +1 -1
  241. package/dist/plugins/logs/plugin.d.ts +1 -4
  242. package/dist/plugins/logs/plugin.d.ts.map +1 -1
  243. package/dist/plugins/logs/plugin.js +7 -0
  244. package/dist/plugins/logs/plugin.js.map +1 -1
  245. package/dist/plugins/mailbox/helpers.d.ts +2 -2
  246. package/dist/plugins/mailbox/helpers.js +1 -1
  247. package/dist/plugins/mailbox/plugin.d.ts.map +1 -1
  248. package/dist/plugins/mailbox/plugin.js +38 -43
  249. package/dist/plugins/mailbox/plugin.js.map +1 -1
  250. package/dist/plugins/mailbox/query.d.ts +7 -2
  251. package/dist/plugins/mailbox/query.d.ts.map +1 -1
  252. package/dist/plugins/mailbox/query.js +14 -7
  253. package/dist/plugins/mailbox/query.js.map +1 -1
  254. package/dist/plugins/mailbox/state.d.ts +5 -4
  255. package/dist/plugins/mailbox/state.d.ts.map +1 -1
  256. package/dist/plugins/mailbox/state.js +6 -5
  257. package/dist/plugins/mailbox/state.js.map +1 -1
  258. package/dist/plugins/resources/filename.d.ts +4 -0
  259. package/dist/plugins/resources/filename.d.ts.map +1 -0
  260. package/dist/plugins/resources/filename.js +18 -0
  261. package/dist/plugins/resources/filename.js.map +1 -0
  262. package/dist/plugins/resources/plugin.d.ts +3 -0
  263. package/dist/plugins/resources/plugin.d.ts.map +1 -1
  264. package/dist/plugins/resources/plugin.js +97 -60
  265. package/dist/plugins/resources/plugin.js.map +1 -1
  266. package/dist/plugins/resources/state.d.ts +1 -1
  267. package/dist/plugins/resources/state.js +1 -1
  268. package/dist/plugins/result-eviction/plugin.d.ts.map +1 -1
  269. package/dist/plugins/result-eviction/plugin.js +6 -1
  270. package/dist/plugins/result-eviction/plugin.js.map +1 -1
  271. package/dist/plugins/services/plugin.d.ts +4 -1
  272. package/dist/plugins/services/plugin.d.ts.map +1 -1
  273. package/dist/plugins/services/plugin.js +251 -100
  274. package/dist/plugins/services/plugin.js.map +1 -1
  275. package/dist/plugins/services/prompt.d.ts.map +1 -1
  276. package/dist/plugins/services/prompt.js +1 -4
  277. package/dist/plugins/services/prompt.js.map +1 -1
  278. package/dist/plugins/services/schema.d.ts +15 -5
  279. package/dist/plugins/services/schema.d.ts.map +1 -1
  280. package/dist/plugins/services/service.d.ts +100 -18
  281. package/dist/plugins/services/service.d.ts.map +1 -1
  282. package/dist/plugins/services/service.js +680 -230
  283. package/dist/plugins/services/service.js.map +1 -1
  284. package/dist/plugins/session-lifecycle/plugin.d.ts +21 -1
  285. package/dist/plugins/session-lifecycle/plugin.d.ts.map +1 -1
  286. package/dist/plugins/session-lifecycle/plugin.js +53 -40
  287. package/dist/plugins/session-lifecycle/plugin.js.map +1 -1
  288. package/dist/plugins/session-state/plugin.d.ts +21 -13
  289. package/dist/plugins/session-state/plugin.d.ts.map +1 -1
  290. package/dist/plugins/session-state/plugin.js +214 -79
  291. package/dist/plugins/session-state/plugin.js.map +1 -1
  292. package/dist/plugins/session-stats/plugin.d.ts.map +1 -1
  293. package/dist/plugins/session-stats/plugin.js +1 -0
  294. package/dist/plugins/session-stats/plugin.js.map +1 -1
  295. package/dist/plugins/shell/executor.d.ts +17 -15
  296. package/dist/plugins/shell/executor.d.ts.map +1 -1
  297. package/dist/plugins/shell/executor.js +130 -195
  298. package/dist/plugins/shell/executor.js.map +1 -1
  299. package/dist/plugins/shell/index.d.ts +2 -2
  300. package/dist/plugins/shell/index.d.ts.map +1 -1
  301. package/dist/plugins/shell/index.js +2 -2
  302. package/dist/plugins/shell/index.js.map +1 -1
  303. package/dist/plugins/shell/plugin.d.ts +5 -4
  304. package/dist/plugins/shell/plugin.d.ts.map +1 -1
  305. package/dist/plugins/shell/plugin.js +4 -8
  306. package/dist/plugins/shell/plugin.js.map +1 -1
  307. package/dist/plugins/skills/discovery.d.ts.map +1 -1
  308. package/dist/plugins/skills/discovery.js +10 -8
  309. package/dist/plugins/skills/discovery.js.map +1 -1
  310. package/dist/plugins/skills/index.d.ts +1 -1
  311. package/dist/plugins/skills/index.d.ts.map +1 -1
  312. package/dist/plugins/skills/plugin.d.ts +1 -1
  313. package/dist/plugins/skills/plugin.d.ts.map +1 -1
  314. package/dist/plugins/skills/plugin.js +23 -30
  315. package/dist/plugins/skills/plugin.js.map +1 -1
  316. package/dist/plugins/skills/prompts.d.ts +3 -3
  317. package/dist/plugins/skills/prompts.d.ts.map +1 -1
  318. package/dist/plugins/skills/prompts.js.map +1 -1
  319. package/dist/plugins/skills/schema.d.ts +4 -2
  320. package/dist/plugins/skills/schema.d.ts.map +1 -1
  321. package/dist/plugins/todo/schema.d.ts +1 -1
  322. package/dist/plugins/uploads/plugin.d.ts +11 -4
  323. package/dist/plugins/uploads/plugin.d.ts.map +1 -1
  324. package/dist/plugins/uploads/plugin.js +891 -225
  325. package/dist/plugins/uploads/plugin.js.map +1 -1
  326. package/dist/plugins/uploads/preprocessor.d.ts +14 -0
  327. package/dist/plugins/uploads/preprocessor.d.ts.map +1 -1
  328. package/dist/plugins/uploads/preprocessor.js +11 -0
  329. package/dist/plugins/uploads/preprocessor.js.map +1 -1
  330. package/dist/plugins/uploads/preprocessors/image-classifier.d.ts +1 -1
  331. package/dist/plugins/uploads/preprocessors/image-classifier.d.ts.map +1 -1
  332. package/dist/plugins/uploads/preprocessors/image-classifier.js +51 -26
  333. package/dist/plugins/uploads/preprocessors/image-classifier.js.map +1 -1
  334. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.d.ts +2 -2
  335. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.d.ts.map +1 -1
  336. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.js +41 -16
  337. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.js.map +1 -1
  338. package/dist/plugins/uploads/preprocessors/pdf-preprocessor.d.ts +1 -1
  339. package/dist/plugins/uploads/preprocessors/pdf-preprocessor.d.ts.map +1 -1
  340. package/dist/plugins/uploads/preprocessors/pdf-preprocessor.js +29 -14
  341. package/dist/plugins/uploads/preprocessors/pdf-preprocessor.js.map +1 -1
  342. package/dist/plugins/uploads/preprocessors/zip-preprocessor.d.ts +1 -1
  343. package/dist/plugins/uploads/preprocessors/zip-preprocessor.d.ts.map +1 -1
  344. package/dist/plugins/uploads/preprocessors/zip-preprocessor.js +62 -34
  345. package/dist/plugins/uploads/preprocessors/zip-preprocessor.js.map +1 -1
  346. package/dist/plugins/uploads/schema.d.ts +25 -2
  347. package/dist/plugins/uploads/schema.d.ts.map +1 -1
  348. package/dist/plugins/uploads/schema.js +24 -3
  349. package/dist/plugins/uploads/schema.js.map +1 -1
  350. package/dist/plugins/uploads/state.d.ts +13 -1
  351. package/dist/plugins/uploads/state.d.ts.map +1 -1
  352. package/dist/plugins/uploads/state.js +4 -1
  353. package/dist/plugins/uploads/state.js.map +1 -1
  354. package/dist/plugins/user-chat/plugin.d.ts +12 -5
  355. package/dist/plugins/user-chat/plugin.d.ts.map +1 -1
  356. package/dist/plugins/user-chat/plugin.js +141 -32
  357. package/dist/plugins/user-chat/plugin.js.map +1 -1
  358. package/dist/plugins/workers/context.d.ts +35 -13
  359. package/dist/plugins/workers/context.d.ts.map +1 -1
  360. package/dist/plugins/workers/context.js +82 -32
  361. package/dist/plugins/workers/context.js.map +1 -1
  362. package/dist/plugins/workers/definition.d.ts +7 -0
  363. package/dist/plugins/workers/definition.d.ts.map +1 -1
  364. package/dist/plugins/workers/definition.js.map +1 -1
  365. package/dist/plugins/workers/index.d.ts +4 -4
  366. package/dist/plugins/workers/index.d.ts.map +1 -1
  367. package/dist/plugins/workers/index.js +2 -2
  368. package/dist/plugins/workers/index.js.map +1 -1
  369. package/dist/plugins/workers/plugin.d.ts +10 -57
  370. package/dist/plugins/workers/plugin.d.ts.map +1 -1
  371. package/dist/plugins/workers/plugin.js +385 -123
  372. package/dist/plugins/workers/plugin.js.map +1 -1
  373. package/dist/plugins/workers/state.d.ts +69 -0
  374. package/dist/plugins/workers/state.d.ts.map +1 -0
  375. package/dist/plugins/workers/state.js +52 -0
  376. package/dist/plugins/workers/state.js.map +1 -0
  377. package/dist/plugins/workers/worker.d.ts +15 -4
  378. package/dist/plugins/workers/worker.d.ts.map +1 -1
  379. package/dist/testing/conformance.d.ts +106 -0
  380. package/dist/testing/conformance.d.ts.map +1 -0
  381. package/dist/testing/conformance.js +1463 -0
  382. package/dist/testing/conformance.js.map +1 -0
  383. package/dist/testing/node-platform.d.ts.map +1 -1
  384. package/dist/testing/node-platform.js +5 -0
  385. package/dist/testing/node-platform.js.map +1 -1
  386. package/dist/testing/test-harness.d.ts +11 -1
  387. package/dist/testing/test-harness.d.ts.map +1 -1
  388. package/dist/testing/test-harness.js +17 -37
  389. package/dist/testing/test-harness.js.map +1 -1
  390. package/dist/testing/wait-helpers.d.ts.map +1 -1
  391. package/dist/testing/wait-helpers.js +1 -3
  392. package/dist/testing/wait-helpers.js.map +1 -1
  393. package/dist/transport/adapter/client-adapter.d.ts +6 -0
  394. package/dist/transport/adapter/client-adapter.d.ts.map +1 -1
  395. package/dist/transport/adapter/client-adapter.js +50 -3
  396. package/dist/transport/adapter/client-adapter.js.map +1 -1
  397. package/dist/transport/adapter/heartbeat.d.ts +16 -0
  398. package/dist/transport/adapter/heartbeat.d.ts.map +1 -0
  399. package/dist/transport/adapter/heartbeat.js +41 -0
  400. package/dist/transport/adapter/heartbeat.js.map +1 -0
  401. package/dist/transport/adapter/server-adapter.d.ts.map +1 -1
  402. package/dist/transport/adapter/server-adapter.js +6 -0
  403. package/dist/transport/adapter/server-adapter.js.map +1 -1
  404. package/dist/transport/http/app.d.ts +3 -30
  405. package/dist/transport/http/app.d.ts.map +1 -1
  406. package/dist/transport/http/app.js +3 -7
  407. package/dist/transport/http/app.js.map +1 -1
  408. package/dist/transport/http/context.d.ts +40 -0
  409. package/dist/transport/http/context.d.ts.map +1 -0
  410. package/dist/transport/http/context.js +17 -0
  411. package/dist/transport/http/context.js.map +1 -0
  412. package/dist/transport/http/fetch-guard.d.ts +49 -0
  413. package/dist/transport/http/fetch-guard.d.ts.map +1 -0
  414. package/dist/transport/http/fetch-guard.js +312 -0
  415. package/dist/transport/http/fetch-guard.js.map +1 -0
  416. package/dist/transport/http/index.d.ts +1 -1
  417. package/dist/transport/http/index.d.ts.map +1 -1
  418. package/dist/transport/http/middleware/bearer-auth.d.ts +1 -1
  419. package/dist/transport/http/middleware/bearer-auth.d.ts.map +1 -1
  420. package/dist/transport/http/middleware/error-handler.d.ts +2 -2
  421. package/dist/transport/http/middleware/error-handler.d.ts.map +1 -1
  422. package/dist/transport/http/path-containment.d.ts +20 -0
  423. package/dist/transport/http/path-containment.d.ts.map +1 -0
  424. package/dist/transport/http/path-containment.js +31 -0
  425. package/dist/transport/http/path-containment.js.map +1 -0
  426. package/dist/transport/http/responses.d.ts +30 -0
  427. package/dist/transport/http/responses.d.ts.map +1 -0
  428. package/dist/transport/http/responses.js +5 -0
  429. package/dist/transport/http/responses.js.map +1 -0
  430. package/dist/transport/http/routes/files.d.ts +1 -1
  431. package/dist/transport/http/routes/files.d.ts.map +1 -1
  432. package/dist/transport/http/routes/files.js +54 -155
  433. package/dist/transport/http/routes/files.js.map +1 -1
  434. package/dist/transport/http/routes/resources.d.ts +1 -1
  435. package/dist/transport/http/routes/resources.d.ts.map +1 -1
  436. package/dist/transport/http/routes/resources.js +92 -57
  437. package/dist/transport/http/routes/resources.js.map +1 -1
  438. package/dist/transport/http/routes/rpc.d.ts +6 -1
  439. package/dist/transport/http/routes/rpc.d.ts.map +1 -1
  440. package/dist/transport/http/routes/rpc.js +51 -21
  441. package/dist/transport/http/routes/rpc.js.map +1 -1
  442. package/dist/transport/http/routes/upload.d.ts +1 -1
  443. package/dist/transport/http/routes/upload.d.ts.map +1 -1
  444. package/dist/transport/http/routes/upload.js +256 -169
  445. package/dist/transport/http/routes/upload.js.map +1 -1
  446. package/dist/user-config.d.ts +34 -7
  447. package/dist/user-config.d.ts.map +1 -1
  448. package/dist/user-config.js +71 -0
  449. package/dist/user-config.js.map +1 -1
  450. package/package.json +17 -7
  451. package/src/bootstrap.ts +119 -35
  452. package/src/bun-platform/fs.ts +1 -0
  453. package/src/bun-platform/index.ts +18 -1
  454. package/src/bun-platform/server-lifecycle.ts +218 -0
  455. package/src/bun-platform/shell.ts +362 -0
  456. package/src/config.ts +130 -0
  457. package/src/core/agents/agent.ts +210 -92
  458. package/src/core/agents/config.ts +8 -0
  459. package/src/core/agents/retry.ts +16 -17
  460. package/src/core/agents/state.ts +2 -0
  461. package/src/core/events/base-event-store.ts +24 -16
  462. package/src/core/events/file.ts +198 -22
  463. package/src/core/file-store/containment.ts +75 -0
  464. package/src/core/file-store/file-store.ts +106 -25
  465. package/src/core/file-store/types.ts +14 -1
  466. package/src/core/image/image-processor.ts +3 -1
  467. package/src/core/image/types.ts +2 -0
  468. package/src/core/image/vips-resizer.ts +102 -37
  469. package/src/core/llm/anthropic.ts +63 -79
  470. package/src/core/llm/logger.ts +175 -37
  471. package/src/core/llm/openrouter.ts +57 -72
  472. package/src/core/llm/provider-request.ts +40 -0
  473. package/src/core/llm/provider.ts +28 -0
  474. package/src/core/plugins/plugin-builder.ts +106 -14
  475. package/src/core/preset/preset-builder.ts +12 -22
  476. package/src/core/sessions/context.ts +18 -0
  477. package/src/core/sessions/runtime-activity.ts +78 -0
  478. package/src/core/sessions/schema.ts +29 -3
  479. package/src/core/sessions/session-manager.ts +560 -140
  480. package/src/core/sessions/session-store.ts +38 -0
  481. package/src/core/sessions/session.ts +187 -47
  482. package/src/core/sessions/state.ts +11 -0
  483. package/src/core/system.ts +15 -0
  484. package/src/core/wake-key.ts +65 -0
  485. package/src/index.ts +32 -12
  486. package/src/lib/archive/archive-inspection.fixtures.ts +138 -0
  487. package/src/lib/archive/archive-inspection.ts +359 -0
  488. package/src/lib/archive/index.ts +17 -0
  489. package/src/lib/logger/console.ts +7 -57
  490. package/src/lib/logger/file.ts +28 -50
  491. package/src/lib/logger/index.ts +3 -1
  492. package/src/lib/logger/jsonl.ts +36 -0
  493. package/src/lib/logger/logger.ts +48 -0
  494. package/src/lib/logger/session-log.ts +42 -0
  495. package/src/lib/utils/concurrency.ts +37 -8
  496. package/src/lib/utils/fs-batch.ts +49 -0
  497. package/src/lib/utils/result.ts +7 -31
  498. package/src/lib/utils/sleep.ts +23 -0
  499. package/src/platform/fs-revision.ts +21 -0
  500. package/src/platform/fs.ts +109 -1
  501. package/src/platform/git.ts +67 -0
  502. package/src/platform/index.ts +51 -1
  503. package/src/platform/llm-call-log.ts +86 -0
  504. package/src/platform/scheduler.ts +71 -0
  505. package/src/platform/session-log.ts +39 -0
  506. package/src/platform/shell.ts +89 -0
  507. package/src/plugins/agent-status/plugin.ts +1 -0
  508. package/src/plugins/agents/plugin.ts +134 -81
  509. package/src/plugins/filesystem/listing.ts +70 -12
  510. package/src/plugins/filesystem/plugin.ts +8 -6
  511. package/src/plugins/git-status/index.ts +1 -0
  512. package/src/plugins/git-status/plugin.ts +211 -77
  513. package/src/plugins/llm-debug/plugin.ts +1 -0
  514. package/src/plugins/logs/plugin.ts +9 -0
  515. package/src/plugins/mailbox/plugin.ts +46 -64
  516. package/src/plugins/mailbox/query.ts +15 -7
  517. package/src/plugins/mailbox/state.ts +1 -0
  518. package/src/plugins/resources/filename.ts +19 -0
  519. package/src/plugins/resources/plugin.ts +111 -59
  520. package/src/plugins/result-eviction/plugin.ts +6 -1
  521. package/src/plugins/services/plugin.ts +254 -98
  522. package/src/plugins/services/prompt.ts +1 -6
  523. package/src/plugins/services/schema.ts +17 -3
  524. package/src/plugins/services/service.ts +702 -252
  525. package/src/plugins/session-lifecycle/plugin.ts +56 -41
  526. package/src/plugins/session-state/plugin.ts +242 -88
  527. package/src/plugins/session-stats/plugin.ts +1 -0
  528. package/src/plugins/shell/executor.ts +144 -226
  529. package/src/plugins/shell/index.ts +1 -4
  530. package/src/plugins/shell/plugin.ts +7 -9
  531. package/src/plugins/skills/discovery.ts +10 -8
  532. package/src/plugins/skills/index.ts +1 -1
  533. package/src/plugins/skills/plugin.ts +19 -27
  534. package/src/plugins/skills/prompts.ts +3 -3
  535. package/src/plugins/skills/schema.ts +5 -2
  536. package/src/plugins/uploads/plugin.ts +991 -273
  537. package/src/plugins/uploads/preprocessor.ts +25 -0
  538. package/src/plugins/uploads/preprocessors/image-classifier.ts +56 -25
  539. package/src/plugins/uploads/preprocessors/markitdown-preprocessor.ts +41 -15
  540. package/src/plugins/uploads/preprocessors/pdf-preprocessor.ts +32 -16
  541. package/src/plugins/uploads/preprocessors/zip-preprocessor.ts +67 -38
  542. package/src/plugins/uploads/schema.ts +30 -3
  543. package/src/plugins/uploads/state.ts +12 -0
  544. package/src/plugins/user-chat/plugin.ts +196 -40
  545. package/src/plugins/workers/context.ts +102 -39
  546. package/src/plugins/workers/definition.ts +7 -0
  547. package/src/plugins/workers/index.ts +4 -4
  548. package/src/plugins/workers/plugin.ts +461 -151
  549. package/src/plugins/workers/state.ts +64 -0
  550. package/src/plugins/workers/worker.ts +15 -4
  551. package/src/testing/conformance.ts +1751 -0
  552. package/src/testing/node-platform.ts +5 -0
  553. package/src/testing/test-harness.ts +21 -38
  554. package/src/testing/wait-helpers.ts +1 -4
  555. package/src/transport/adapter/client-adapter.ts +60 -3
  556. package/src/transport/adapter/heartbeat.ts +50 -0
  557. package/src/transport/adapter/server-adapter.ts +6 -0
  558. package/src/transport/http/app.ts +7 -35
  559. package/src/transport/http/context.ts +46 -0
  560. package/src/transport/http/fetch-guard.ts +318 -0
  561. package/src/transport/http/index.ts +1 -1
  562. package/src/transport/http/middleware/bearer-auth.ts +1 -1
  563. package/src/transport/http/middleware/error-handler.ts +1 -1
  564. package/src/transport/http/path-containment.ts +44 -0
  565. package/src/transport/http/responses.ts +20 -0
  566. package/src/transport/http/routes/files.ts +69 -159
  567. package/src/transport/http/routes/resources.ts +124 -93
  568. package/src/transport/http/routes/rpc.ts +63 -26
  569. package/src/transport/http/routes/upload.ts +327 -258
  570. package/src/user-config.ts +93 -7
  571. package/dist/config.test.d.ts +0 -2
  572. package/dist/config.test.d.ts.map +0 -1
  573. package/dist/config.test.js +0 -155
  574. package/dist/config.test.js.map +0 -1
  575. package/dist/core/agent-loop.integration.test.d.ts +0 -2
  576. package/dist/core/agent-loop.integration.test.d.ts.map +0 -1
  577. package/dist/core/agent-loop.integration.test.js +0 -414
  578. package/dist/core/agent-loop.integration.test.js.map +0 -1
  579. package/dist/core/agents/agent-config.test.d.ts +0 -2
  580. package/dist/core/agents/agent-config.test.d.ts.map +0 -1
  581. package/dist/core/agents/agent-config.test.js +0 -194
  582. package/dist/core/agents/agent-config.test.js.map +0 -1
  583. package/dist/core/agents/agent-shutdown.test.d.ts +0 -6
  584. package/dist/core/agents/agent-shutdown.test.d.ts.map +0 -1
  585. package/dist/core/agents/agent-shutdown.test.js +0 -180
  586. package/dist/core/agents/agent-shutdown.test.js.map +0 -1
  587. package/dist/core/agents/agent.test.d.ts +0 -2
  588. package/dist/core/agents/agent.test.d.ts.map +0 -1
  589. package/dist/core/agents/agent.test.js +0 -313
  590. package/dist/core/agents/agent.test.js.map +0 -1
  591. package/dist/core/agents/communicator.d.ts +0 -3
  592. package/dist/core/agents/communicator.d.ts.map +0 -1
  593. package/dist/core/agents/communicator.js +0 -13
  594. package/dist/core/agents/communicator.js.map +0 -1
  595. package/dist/core/agents/handler-events.test.d.ts +0 -5
  596. package/dist/core/agents/handler-events.test.d.ts.map +0 -1
  597. package/dist/core/agents/handler-events.test.js +0 -115
  598. package/dist/core/agents/handler-events.test.js.map +0 -1
  599. package/dist/core/agents/response-sanitizer.test.d.ts +0 -2
  600. package/dist/core/agents/response-sanitizer.test.d.ts.map +0 -1
  601. package/dist/core/agents/response-sanitizer.test.js +0 -101
  602. package/dist/core/agents/response-sanitizer.test.js.map +0 -1
  603. package/dist/core/error-recovery.integration.test.d.ts +0 -2
  604. package/dist/core/error-recovery.integration.test.d.ts.map +0 -1
  605. package/dist/core/error-recovery.integration.test.js +0 -288
  606. package/dist/core/error-recovery.integration.test.js.map +0 -1
  607. package/dist/core/event-sourcing.integration.test.d.ts +0 -2
  608. package/dist/core/event-sourcing.integration.test.d.ts.map +0 -1
  609. package/dist/core/event-sourcing.integration.test.js +0 -154
  610. package/dist/core/event-sourcing.integration.test.js.map +0 -1
  611. package/dist/core/events/file.test.d.ts +0 -2
  612. package/dist/core/events/file.test.d.ts.map +0 -1
  613. package/dist/core/events/file.test.js +0 -284
  614. package/dist/core/events/file.test.js.map +0 -1
  615. package/dist/core/events/memory.test.d.ts +0 -2
  616. package/dist/core/events/memory.test.d.ts.map +0 -1
  617. package/dist/core/events/memory.test.js +0 -502
  618. package/dist/core/events/memory.test.js.map +0 -1
  619. package/dist/core/image/image-processor.test.d.ts +0 -2
  620. package/dist/core/image/image-processor.test.d.ts.map +0 -1
  621. package/dist/core/image/image-processor.test.js +0 -171
  622. package/dist/core/image/image-processor.test.js.map +0 -1
  623. package/dist/core/image/vips-resizer.test.d.ts +0 -2
  624. package/dist/core/image/vips-resizer.test.d.ts.map +0 -1
  625. package/dist/core/image/vips-resizer.test.js +0 -336
  626. package/dist/core/image/vips-resizer.test.js.map +0 -1
  627. package/dist/core/llm/anthropic.test.d.ts +0 -2
  628. package/dist/core/llm/anthropic.test.d.ts.map +0 -1
  629. package/dist/core/llm/anthropic.test.js +0 -464
  630. package/dist/core/llm/anthropic.test.js.map +0 -1
  631. package/dist/core/llm/cache-breakpoints.test.d.ts +0 -2
  632. package/dist/core/llm/cache-breakpoints.test.d.ts.map +0 -1
  633. package/dist/core/llm/cache-breakpoints.test.js +0 -69
  634. package/dist/core/llm/cache-breakpoints.test.js.map +0 -1
  635. package/dist/core/llm/cache-live.test.d.ts +0 -16
  636. package/dist/core/llm/cache-live.test.d.ts.map +0 -1
  637. package/dist/core/llm/cache-live.test.js +0 -137
  638. package/dist/core/llm/cache-live.test.js.map +0 -1
  639. package/dist/core/llm/logger.test.d.ts +0 -7
  640. package/dist/core/llm/logger.test.d.ts.map +0 -1
  641. package/dist/core/llm/logger.test.js +0 -228
  642. package/dist/core/llm/logger.test.js.map +0 -1
  643. package/dist/core/llm/message-sanitization.test.d.ts +0 -2
  644. package/dist/core/llm/message-sanitization.test.d.ts.map +0 -1
  645. package/dist/core/llm/message-sanitization.test.js +0 -181
  646. package/dist/core/llm/message-sanitization.test.js.map +0 -1
  647. package/dist/core/llm/mock.test.d.ts +0 -2
  648. package/dist/core/llm/mock.test.d.ts.map +0 -1
  649. package/dist/core/llm/mock.test.js +0 -318
  650. package/dist/core/llm/mock.test.js.map +0 -1
  651. package/dist/core/llm/model-shortlist-live.test.d.ts +0 -35
  652. package/dist/core/llm/model-shortlist-live.test.d.ts.map +0 -1
  653. package/dist/core/llm/model-shortlist-live.test.js +0 -452
  654. package/dist/core/llm/model-shortlist-live.test.js.map +0 -1
  655. package/dist/core/llm/openrouter-mapping.test.d.ts +0 -2
  656. package/dist/core/llm/openrouter-mapping.test.d.ts.map +0 -1
  657. package/dist/core/llm/openrouter-mapping.test.js +0 -190
  658. package/dist/core/llm/openrouter-mapping.test.js.map +0 -1
  659. package/dist/core/llm/openrouter.test.d.ts +0 -2
  660. package/dist/core/llm/openrouter.test.d.ts.map +0 -1
  661. package/dist/core/llm/openrouter.test.js +0 -377
  662. package/dist/core/llm/openrouter.test.js.map +0 -1
  663. package/dist/core/llm/provider-integration.test.d.ts +0 -12
  664. package/dist/core/llm/provider-integration.test.d.ts.map +0 -1
  665. package/dist/core/llm/provider-integration.test.js +0 -350
  666. package/dist/core/llm/provider-integration.test.js.map +0 -1
  667. package/dist/core/llm/reasoning-details.test.d.ts +0 -2
  668. package/dist/core/llm/reasoning-details.test.d.ts.map +0 -1
  669. package/dist/core/llm/reasoning-details.test.js +0 -144
  670. package/dist/core/llm/reasoning-details.test.js.map +0 -1
  671. package/dist/core/llm/routing-provider.test.d.ts +0 -2
  672. package/dist/core/llm/routing-provider.test.d.ts.map +0 -1
  673. package/dist/core/llm/routing-provider.test.js +0 -94
  674. package/dist/core/llm/routing-provider.test.js.map +0 -1
  675. package/dist/core/llm/snapshot-middleware.test.d.ts +0 -2
  676. package/dist/core/llm/snapshot-middleware.test.d.ts.map +0 -1
  677. package/dist/core/llm/snapshot-middleware.test.js +0 -144
  678. package/dist/core/llm/snapshot-middleware.test.js.map +0 -1
  679. package/dist/core/llm/thinking-blocks.test.d.ts +0 -2
  680. package/dist/core/llm/thinking-blocks.test.d.ts.map +0 -1
  681. package/dist/core/llm/thinking-blocks.test.js +0 -291
  682. package/dist/core/llm/thinking-blocks.test.js.map +0 -1
  683. package/dist/core/multi-agent.integration.test.d.ts +0 -2
  684. package/dist/core/multi-agent.integration.test.d.ts.map +0 -1
  685. package/dist/core/multi-agent.integration.test.js +0 -298
  686. package/dist/core/multi-agent.integration.test.js.map +0 -1
  687. package/dist/core/plugin-hooks.integration.test.d.ts +0 -2
  688. package/dist/core/plugin-hooks.integration.test.d.ts.map +0 -1
  689. package/dist/core/plugin-hooks.integration.test.js +0 -458
  690. package/dist/core/plugin-hooks.integration.test.js.map +0 -1
  691. package/dist/core/reasoning-round-trip.integration.test.d.ts +0 -2
  692. package/dist/core/reasoning-round-trip.integration.test.d.ts.map +0 -1
  693. package/dist/core/reasoning-round-trip.integration.test.js +0 -150
  694. package/dist/core/reasoning-round-trip.integration.test.js.map +0 -1
  695. package/dist/core/session-lifecycle.integration.test.d.ts +0 -2
  696. package/dist/core/session-lifecycle.integration.test.d.ts.map +0 -1
  697. package/dist/core/session-lifecycle.integration.test.js +0 -159
  698. package/dist/core/session-lifecycle.integration.test.js.map +0 -1
  699. package/dist/core/sessions/fork-utils.test.d.ts +0 -2
  700. package/dist/core/sessions/fork-utils.test.d.ts.map +0 -1
  701. package/dist/core/sessions/fork-utils.test.js +0 -129
  702. package/dist/core/sessions/fork-utils.test.js.map +0 -1
  703. package/dist/core/sessions/session-overrides.integration.test.d.ts +0 -2
  704. package/dist/core/sessions/session-overrides.integration.test.d.ts.map +0 -1
  705. package/dist/core/sessions/session-overrides.integration.test.js +0 -154
  706. package/dist/core/sessions/session-overrides.integration.test.js.map +0 -1
  707. package/dist/core/sessions/session.test.d.ts +0 -2
  708. package/dist/core/sessions/session.test.d.ts.map +0 -1
  709. package/dist/core/sessions/session.test.js +0 -1100
  710. package/dist/core/sessions/session.test.js.map +0 -1
  711. package/dist/core/tools/executor.test.d.ts +0 -2
  712. package/dist/core/tools/executor.test.d.ts.map +0 -1
  713. package/dist/core/tools/executor.test.js +0 -144
  714. package/dist/core/tools/executor.test.js.map +0 -1
  715. package/dist/lib/logger/console.test.d.ts +0 -2
  716. package/dist/lib/logger/console.test.d.ts.map +0 -1
  717. package/dist/lib/logger/console.test.js +0 -258
  718. package/dist/lib/logger/console.test.js.map +0 -1
  719. package/dist/lib/never.d.ts +0 -2
  720. package/dist/lib/never.d.ts.map +0 -1
  721. package/dist/lib/never.js +0 -4
  722. package/dist/lib/never.js.map +0 -1
  723. package/dist/lib/utils/concurrency.test.d.ts +0 -2
  724. package/dist/lib/utils/concurrency.test.d.ts.map +0 -1
  725. package/dist/lib/utils/concurrency.test.js +0 -135
  726. package/dist/lib/utils/concurrency.test.js.map +0 -1
  727. package/dist/plugins/agent-status/agent-status.test.d.ts +0 -2
  728. package/dist/plugins/agent-status/agent-status.test.d.ts.map +0 -1
  729. package/dist/plugins/agent-status/agent-status.test.js +0 -136
  730. package/dist/plugins/agent-status/agent-status.test.js.map +0 -1
  731. package/dist/plugins/agents/agents.integration.test.d.ts +0 -2
  732. package/dist/plugins/agents/agents.integration.test.d.ts.map +0 -1
  733. package/dist/plugins/agents/agents.integration.test.js +0 -830
  734. package/dist/plugins/agents/agents.integration.test.js.map +0 -1
  735. package/dist/plugins/agents/supervision.integration.test.d.ts +0 -2
  736. package/dist/plugins/agents/supervision.integration.test.d.ts.map +0 -1
  737. package/dist/plugins/agents/supervision.integration.test.js +0 -215
  738. package/dist/plugins/agents/supervision.integration.test.js.map +0 -1
  739. package/dist/plugins/context-compact/compaction-live.test.d.ts +0 -17
  740. package/dist/plugins/context-compact/compaction-live.test.d.ts.map +0 -1
  741. package/dist/plugins/context-compact/compaction-live.test.js +0 -177
  742. package/dist/plugins/context-compact/compaction-live.test.js.map +0 -1
  743. package/dist/plugins/context-compact/context-compact.integration.test.d.ts +0 -2
  744. package/dist/plugins/context-compact/context-compact.integration.test.d.ts.map +0 -1
  745. package/dist/plugins/context-compact/context-compact.integration.test.js +0 -364
  746. package/dist/plugins/context-compact/context-compact.integration.test.js.map +0 -1
  747. package/dist/plugins/context-compact/context-compactor.test.d.ts +0 -2
  748. package/dist/plugins/context-compact/context-compactor.test.d.ts.map +0 -1
  749. package/dist/plugins/context-compact/context-compactor.test.js +0 -858
  750. package/dist/plugins/context-compact/context-compactor.test.js.map +0 -1
  751. package/dist/plugins/context-compact/history-offloader.test.d.ts +0 -2
  752. package/dist/plugins/context-compact/history-offloader.test.d.ts.map +0 -1
  753. package/dist/plugins/context-compact/history-offloader.test.js +0 -77
  754. package/dist/plugins/context-compact/history-offloader.test.js.map +0 -1
  755. package/dist/plugins/filesystem/filesystem.integration.test.d.ts +0 -2
  756. package/dist/plugins/filesystem/filesystem.integration.test.d.ts.map +0 -1
  757. package/dist/plugins/filesystem/filesystem.integration.test.js +0 -447
  758. package/dist/plugins/filesystem/filesystem.integration.test.js.map +0 -1
  759. package/dist/plugins/filesystem/schema.d.ts +0 -7
  760. package/dist/plugins/filesystem/schema.d.ts.map +0 -1
  761. package/dist/plugins/filesystem/schema.js +0 -2
  762. package/dist/plugins/filesystem/schema.js.map +0 -1
  763. package/dist/plugins/limits-guard/limit-guard.test.d.ts +0 -2
  764. package/dist/plugins/limits-guard/limit-guard.test.d.ts.map +0 -1
  765. package/dist/plugins/limits-guard/limit-guard.test.js +0 -194
  766. package/dist/plugins/limits-guard/limit-guard.test.js.map +0 -1
  767. package/dist/plugins/limits-guard/limits-guard.integration.test.d.ts +0 -2
  768. package/dist/plugins/limits-guard/limits-guard.integration.test.d.ts.map +0 -1
  769. package/dist/plugins/limits-guard/limits-guard.integration.test.js +0 -887
  770. package/dist/plugins/limits-guard/limits-guard.integration.test.js.map +0 -1
  771. package/dist/plugins/llm-debug/llm-debug.integration.test.d.ts +0 -2
  772. package/dist/plugins/llm-debug/llm-debug.integration.test.d.ts.map +0 -1
  773. package/dist/plugins/llm-debug/llm-debug.integration.test.js +0 -157
  774. package/dist/plugins/llm-debug/llm-debug.integration.test.js.map +0 -1
  775. package/dist/plugins/mailbox/mailbox.integration.test.d.ts +0 -2
  776. package/dist/plugins/mailbox/mailbox.integration.test.d.ts.map +0 -1
  777. package/dist/plugins/mailbox/mailbox.integration.test.js +0 -678
  778. package/dist/plugins/mailbox/mailbox.integration.test.js.map +0 -1
  779. package/dist/plugins/resources/resources.integration.test.d.ts +0 -2
  780. package/dist/plugins/resources/resources.integration.test.d.ts.map +0 -1
  781. package/dist/plugins/resources/resources.integration.test.js +0 -62
  782. package/dist/plugins/resources/resources.integration.test.js.map +0 -1
  783. package/dist/plugins/result-eviction/result-eviction.integration.test.d.ts +0 -2
  784. package/dist/plugins/result-eviction/result-eviction.integration.test.d.ts.map +0 -1
  785. package/dist/plugins/result-eviction/result-eviction.integration.test.js +0 -294
  786. package/dist/plugins/result-eviction/result-eviction.integration.test.js.map +0 -1
  787. package/dist/plugins/services/pid-registry.test.d.ts +0 -2
  788. package/dist/plugins/services/pid-registry.test.d.ts.map +0 -1
  789. package/dist/plugins/services/pid-registry.test.js +0 -161
  790. package/dist/plugins/services/pid-registry.test.js.map +0 -1
  791. package/dist/plugins/services/services.integration.test.d.ts +0 -2
  792. package/dist/plugins/services/services.integration.test.d.ts.map +0 -1
  793. package/dist/plugins/services/services.integration.test.js +0 -1057
  794. package/dist/plugins/services/services.integration.test.js.map +0 -1
  795. package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.d.ts +0 -2
  796. package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.d.ts.map +0 -1
  797. package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.js +0 -498
  798. package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.js.map +0 -1
  799. package/dist/plugins/session-stats/plugin.test.d.ts +0 -2
  800. package/dist/plugins/session-stats/plugin.test.d.ts.map +0 -1
  801. package/dist/plugins/session-stats/plugin.test.js +0 -145
  802. package/dist/plugins/session-stats/plugin.test.js.map +0 -1
  803. package/dist/plugins/shell/shell.integration.test.d.ts +0 -2
  804. package/dist/plugins/shell/shell.integration.test.d.ts.map +0 -1
  805. package/dist/plugins/shell/shell.integration.test.js +0 -234
  806. package/dist/plugins/shell/shell.integration.test.js.map +0 -1
  807. package/dist/plugins/shell/shell.test.d.ts +0 -2
  808. package/dist/plugins/shell/shell.test.d.ts.map +0 -1
  809. package/dist/plugins/shell/shell.test.js +0 -236
  810. package/dist/plugins/shell/shell.test.js.map +0 -1
  811. package/dist/plugins/skills/discovery.test.d.ts +0 -2
  812. package/dist/plugins/skills/discovery.test.d.ts.map +0 -1
  813. package/dist/plugins/skills/discovery.test.js +0 -312
  814. package/dist/plugins/skills/discovery.test.js.map +0 -1
  815. package/dist/plugins/skills/skills.integration.test.d.ts +0 -2
  816. package/dist/plugins/skills/skills.integration.test.d.ts.map +0 -1
  817. package/dist/plugins/skills/skills.integration.test.js +0 -475
  818. package/dist/plugins/skills/skills.integration.test.js.map +0 -1
  819. package/dist/plugins/todo/todo.integration.test.d.ts +0 -2
  820. package/dist/plugins/todo/todo.integration.test.d.ts.map +0 -1
  821. package/dist/plugins/todo/todo.integration.test.js +0 -605
  822. package/dist/plugins/todo/todo.integration.test.js.map +0 -1
  823. package/dist/plugins/uploads/preprocessors/image-classifier.test.d.ts +0 -2
  824. package/dist/plugins/uploads/preprocessors/image-classifier.test.d.ts.map +0 -1
  825. package/dist/plugins/uploads/preprocessors/image-classifier.test.js +0 -113
  826. package/dist/plugins/uploads/preprocessors/image-classifier.test.js.map +0 -1
  827. package/dist/plugins/uploads/uploads.integration.test.d.ts +0 -2
  828. package/dist/plugins/uploads/uploads.integration.test.d.ts.map +0 -1
  829. package/dist/plugins/uploads/uploads.integration.test.js +0 -639
  830. package/dist/plugins/uploads/uploads.integration.test.js.map +0 -1
  831. package/dist/plugins/user-chat/user-chat.integration.test.d.ts +0 -2
  832. package/dist/plugins/user-chat/user-chat.integration.test.d.ts.map +0 -1
  833. package/dist/plugins/user-chat/user-chat.integration.test.js +0 -754
  834. package/dist/plugins/user-chat/user-chat.integration.test.js.map +0 -1
  835. package/dist/plugins/workers/workers.integration.test.d.ts +0 -2
  836. package/dist/plugins/workers/workers.integration.test.d.ts.map +0 -1
  837. package/dist/plugins/workers/workers.integration.test.js +0 -629
  838. package/dist/plugins/workers/workers.integration.test.js.map +0 -1
  839. package/dist/prompts/macros.test.d.ts +0 -2
  840. package/dist/prompts/macros.test.d.ts.map +0 -1
  841. package/dist/prompts/macros.test.js +0 -80
  842. package/dist/prompts/macros.test.js.map +0 -1
  843. package/dist/testing/test-harness.test.d.ts +0 -2
  844. package/dist/testing/test-harness.test.d.ts.map +0 -1
  845. package/dist/testing/test-harness.test.js +0 -92
  846. package/dist/testing/test-harness.test.js.map +0 -1
  847. package/dist/transport/http/routes/rpc.integration.test.d.ts +0 -2
  848. package/dist/transport/http/routes/rpc.integration.test.d.ts.map +0 -1
  849. package/dist/transport/http/routes/rpc.integration.test.js +0 -189
  850. package/dist/transport/http/routes/rpc.integration.test.js.map +0 -1
  851. package/dist/transport/http/routes/rpc.test.d.ts +0 -2
  852. package/dist/transport/http/routes/rpc.test.d.ts.map +0 -1
  853. package/dist/transport/http/routes/rpc.test.js +0 -316
  854. package/dist/transport/http/routes/rpc.test.js.map +0 -1
  855. package/src/config.test.ts +0 -174
  856. package/src/core/agent-loop.integration.test.ts +0 -503
  857. package/src/core/agents/agent-config.test.ts +0 -240
  858. package/src/core/agents/agent-shutdown.test.ts +0 -236
  859. package/src/core/agents/agent.test.ts +0 -387
  860. package/src/core/agents/communicator.ts +0 -16
  861. package/src/core/agents/handler-events.test.ts +0 -167
  862. package/src/core/agents/response-sanitizer.test.ts +0 -137
  863. package/src/core/error-recovery.integration.test.ts +0 -343
  864. package/src/core/event-sourcing.integration.test.ts +0 -191
  865. package/src/core/events/file.test.ts +0 -436
  866. package/src/core/events/memory.test.ts +0 -741
  867. package/src/core/image/image-processor.test.ts +0 -218
  868. package/src/core/image/vips-resizer.test.ts +0 -388
  869. package/src/core/llm/anthropic.test.ts +0 -546
  870. package/src/core/llm/cache-breakpoints.test.ts +0 -85
  871. package/src/core/llm/cache-live.test.ts +0 -155
  872. package/src/core/llm/logger.test.ts +0 -270
  873. package/src/core/llm/message-sanitization.test.ts +0 -217
  874. package/src/core/llm/mock.test.ts +0 -402
  875. package/src/core/llm/model-shortlist-live.test.ts +0 -577
  876. package/src/core/llm/openrouter-mapping.test.ts +0 -234
  877. package/src/core/llm/openrouter.test.ts +0 -499
  878. package/src/core/llm/provider-integration.test.ts +0 -408
  879. package/src/core/llm/reasoning-details.test.ts +0 -177
  880. package/src/core/llm/routing-provider.test.ts +0 -113
  881. package/src/core/llm/snapshot-middleware.test.ts +0 -168
  882. package/src/core/llm/thinking-blocks.test.ts +0 -345
  883. package/src/core/multi-agent.integration.test.ts +0 -340
  884. package/src/core/plugin-hooks.integration.test.ts +0 -568
  885. package/src/core/reasoning-round-trip.integration.test.ts +0 -188
  886. package/src/core/session-lifecycle.integration.test.ts +0 -202
  887. package/src/core/sessions/fork-utils.test.ts +0 -158
  888. package/src/core/sessions/session-overrides.integration.test.ts +0 -210
  889. package/src/core/sessions/session.test.ts +0 -1736
  890. package/src/core/tools/executor.test.ts +0 -161
  891. package/src/lib/logger/console.test.ts +0 -348
  892. package/src/lib/never.ts +0 -3
  893. package/src/lib/utils/concurrency.test.ts +0 -169
  894. package/src/plugins/agent-status/agent-status.test.ts +0 -164
  895. package/src/plugins/agents/agents.integration.test.ts +0 -976
  896. package/src/plugins/agents/supervision.integration.test.ts +0 -249
  897. package/src/plugins/context-compact/compaction-live.test.ts +0 -221
  898. package/src/plugins/context-compact/context-compact.integration.test.ts +0 -418
  899. package/src/plugins/context-compact/context-compactor.test.ts +0 -1036
  900. package/src/plugins/context-compact/history-offloader.test.ts +0 -100
  901. package/src/plugins/filesystem/filesystem.integration.test.ts +0 -529
  902. package/src/plugins/filesystem/schema.ts +0 -6
  903. package/src/plugins/limits-guard/limit-guard.test.ts +0 -253
  904. package/src/plugins/limits-guard/limits-guard.integration.test.ts +0 -1012
  905. package/src/plugins/llm-debug/llm-debug.integration.test.ts +0 -192
  906. package/src/plugins/mailbox/mailbox.integration.test.ts +0 -794
  907. package/src/plugins/resources/resources.integration.test.ts +0 -68
  908. package/src/plugins/result-eviction/result-eviction.integration.test.ts +0 -332
  909. package/src/plugins/services/pid-registry.test.ts +0 -180
  910. package/src/plugins/services/services.integration.test.ts +0 -1223
  911. package/src/plugins/session-lifecycle/session-lifecycle.integration.test.ts +0 -639
  912. package/src/plugins/session-stats/plugin.test.ts +0 -160
  913. package/src/plugins/shell/shell.integration.test.ts +0 -273
  914. package/src/plugins/shell/shell.test.ts +0 -314
  915. package/src/plugins/skills/discovery.test.ts +0 -395
  916. package/src/plugins/skills/skills.integration.test.ts +0 -580
  917. package/src/plugins/todo/todo.integration.test.ts +0 -687
  918. package/src/plugins/uploads/preprocessors/image-classifier.test.ts +0 -142
  919. package/src/plugins/uploads/uploads.integration.test.ts +0 -831
  920. package/src/plugins/user-chat/user-chat.integration.test.ts +0 -893
  921. package/src/plugins/workers/workers.integration.test.ts +0 -778
  922. package/src/prompts/macros.test.ts +0 -107
  923. package/src/testing/test-harness.test.ts +0 -114
  924. package/src/transport/http/routes/rpc.integration.test.ts +0 -243
  925. 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