@roj-ai/sdk 0.1.35 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (908) hide show
  1. package/dist/bootstrap.d.ts +420 -26
  2. package/dist/bootstrap.d.ts.map +1 -1
  3. package/dist/bootstrap.js +55 -24
  4. package/dist/bootstrap.js.map +1 -1
  5. package/dist/bun-platform/fs.d.ts.map +1 -1
  6. package/dist/bun-platform/fs.js +1 -0
  7. package/dist/bun-platform/fs.js.map +1 -1
  8. package/dist/bun-platform/index.d.ts +4 -1
  9. package/dist/bun-platform/index.d.ts.map +1 -1
  10. package/dist/bun-platform/index.js +7 -1
  11. package/dist/bun-platform/index.js.map +1 -1
  12. package/dist/bun-platform/server-lifecycle.d.ts +30 -0
  13. package/dist/bun-platform/server-lifecycle.d.ts.map +1 -0
  14. package/dist/bun-platform/server-lifecycle.js +182 -0
  15. package/dist/bun-platform/server-lifecycle.js.map +1 -0
  16. package/dist/bun-platform/shell.d.ts +33 -0
  17. package/dist/bun-platform/shell.d.ts.map +1 -0
  18. package/dist/bun-platform/shell.js +322 -0
  19. package/dist/bun-platform/shell.js.map +1 -0
  20. package/dist/config.d.ts +34 -0
  21. package/dist/config.d.ts.map +1 -1
  22. package/dist/config.js +97 -0
  23. package/dist/config.js.map +1 -1
  24. package/dist/core/agents/agent.d.ts +42 -7
  25. package/dist/core/agents/agent.d.ts.map +1 -1
  26. package/dist/core/agents/agent.js +209 -90
  27. package/dist/core/agents/agent.js.map +1 -1
  28. package/dist/core/agents/config.d.ts +8 -1
  29. package/dist/core/agents/config.d.ts.map +1 -1
  30. package/dist/core/agents/config.js +5 -1
  31. package/dist/core/agents/config.js.map +1 -1
  32. package/dist/core/agents/retry.d.ts +1 -1
  33. package/dist/core/agents/retry.d.ts.map +1 -1
  34. package/dist/core/agents/retry.js +14 -16
  35. package/dist/core/agents/retry.js.map +1 -1
  36. package/dist/core/agents/state.d.ts +27 -1
  37. package/dist/core/agents/state.d.ts.map +1 -1
  38. package/dist/core/agents/state.js +1 -1
  39. package/dist/core/agents/state.js.map +1 -1
  40. package/dist/core/context/state.d.ts +3 -3
  41. package/dist/core/context/state.d.ts.map +1 -1
  42. package/dist/core/context/state.js +6 -2
  43. package/dist/core/context/state.js.map +1 -1
  44. package/dist/core/events/base-event-store.d.ts.map +1 -1
  45. package/dist/core/events/base-event-store.js +22 -14
  46. package/dist/core/events/base-event-store.js.map +1 -1
  47. package/dist/core/events/file.d.ts +39 -1
  48. package/dist/core/events/file.d.ts.map +1 -1
  49. package/dist/core/events/file.js +159 -20
  50. package/dist/core/events/file.js.map +1 -1
  51. package/dist/core/events/types.d.ts +1 -1
  52. package/dist/core/file-store/containment.d.ts +19 -0
  53. package/dist/core/file-store/containment.d.ts.map +1 -0
  54. package/dist/core/file-store/containment.js +62 -0
  55. package/dist/core/file-store/containment.js.map +1 -0
  56. package/dist/core/file-store/file-store.d.ts +22 -2
  57. package/dist/core/file-store/file-store.d.ts.map +1 -1
  58. package/dist/core/file-store/file-store.js +88 -20
  59. package/dist/core/file-store/file-store.js.map +1 -1
  60. package/dist/core/file-store/types.d.ts +13 -1
  61. package/dist/core/file-store/types.d.ts.map +1 -1
  62. package/dist/core/image/image-processor.d.ts.map +1 -1
  63. package/dist/core/image/image-processor.js +3 -1
  64. package/dist/core/image/image-processor.js.map +1 -1
  65. package/dist/core/image/types.d.ts +2 -0
  66. package/dist/core/image/types.d.ts.map +1 -1
  67. package/dist/core/image/vips-resizer.d.ts.map +1 -1
  68. package/dist/core/image/vips-resizer.js +81 -33
  69. package/dist/core/image/vips-resizer.js.map +1 -1
  70. package/dist/core/llm/anthropic.d.ts +0 -1
  71. package/dist/core/llm/anthropic.d.ts.map +1 -1
  72. package/dist/core/llm/anthropic.js +101 -74
  73. package/dist/core/llm/anthropic.js.map +1 -1
  74. package/dist/core/llm/logger.d.ts +14 -2
  75. package/dist/core/llm/logger.d.ts.map +1 -1
  76. package/dist/core/llm/logger.js +161 -36
  77. package/dist/core/llm/logger.js.map +1 -1
  78. package/dist/core/llm/openrouter.d.ts +0 -1
  79. package/dist/core/llm/openrouter.d.ts.map +1 -1
  80. package/dist/core/llm/openrouter.js +70 -74
  81. package/dist/core/llm/openrouter.js.map +1 -1
  82. package/dist/core/llm/provider-request.d.ts +10 -0
  83. package/dist/core/llm/provider-request.d.ts.map +1 -0
  84. package/dist/core/llm/provider-request.js +41 -0
  85. package/dist/core/llm/provider-request.js.map +1 -0
  86. package/dist/core/llm/provider.d.ts +45 -3
  87. package/dist/core/llm/provider.d.ts.map +1 -1
  88. package/dist/core/llm/provider.js +26 -0
  89. package/dist/core/llm/provider.js.map +1 -1
  90. package/dist/core/llm/state.d.ts +4 -2
  91. package/dist/core/llm/state.d.ts.map +1 -1
  92. package/dist/core/llm/state.js +15 -4
  93. package/dist/core/llm/state.js.map +1 -1
  94. package/dist/core/plugins/plugin-builder.d.ts +57 -3
  95. package/dist/core/plugins/plugin-builder.d.ts.map +1 -1
  96. package/dist/core/plugins/plugin-builder.js +68 -14
  97. package/dist/core/plugins/plugin-builder.js.map +1 -1
  98. package/dist/core/preset/preset-builder.d.ts +3 -2
  99. package/dist/core/preset/preset-builder.d.ts.map +1 -1
  100. package/dist/core/preset/preset-builder.js +7 -16
  101. package/dist/core/preset/preset-builder.js.map +1 -1
  102. package/dist/core/sessions/context.d.ts +23 -5
  103. package/dist/core/sessions/context.d.ts.map +1 -1
  104. package/dist/core/sessions/runtime-activity.d.ts +33 -0
  105. package/dist/core/sessions/runtime-activity.d.ts.map +1 -0
  106. package/dist/core/sessions/runtime-activity.js +55 -0
  107. package/dist/core/sessions/runtime-activity.js.map +1 -0
  108. package/dist/core/sessions/schema.d.ts +23 -2
  109. package/dist/core/sessions/schema.d.ts.map +1 -1
  110. package/dist/core/sessions/schema.js +24 -3
  111. package/dist/core/sessions/schema.js.map +1 -1
  112. package/dist/core/sessions/session-manager.d.ts +83 -6
  113. package/dist/core/sessions/session-manager.d.ts.map +1 -1
  114. package/dist/core/sessions/session-manager.js +517 -118
  115. package/dist/core/sessions/session-manager.js.map +1 -1
  116. package/dist/core/sessions/session-store.d.ts +18 -0
  117. package/dist/core/sessions/session-store.d.ts.map +1 -1
  118. package/dist/core/sessions/session-store.js +33 -0
  119. package/dist/core/sessions/session-store.js.map +1 -1
  120. package/dist/core/sessions/session.d.ts +42 -6
  121. package/dist/core/sessions/session.d.ts.map +1 -1
  122. package/dist/core/sessions/session.js +176 -44
  123. package/dist/core/sessions/session.js.map +1 -1
  124. package/dist/core/sessions/state.d.ts +10 -1
  125. package/dist/core/sessions/state.d.ts.map +1 -1
  126. package/dist/core/sessions/state.js +16 -1
  127. package/dist/core/sessions/state.js.map +1 -1
  128. package/dist/core/system.d.ts +9 -0
  129. package/dist/core/system.d.ts.map +1 -1
  130. package/dist/core/system.js +4 -1
  131. package/dist/core/system.js.map +1 -1
  132. package/dist/core/tools/executor.d.ts +1 -1
  133. package/dist/core/wake-key.d.ts +34 -0
  134. package/dist/core/wake-key.d.ts.map +1 -0
  135. package/dist/core/wake-key.js +51 -0
  136. package/dist/core/wake-key.js.map +1 -0
  137. package/dist/index.d.ts +24 -13
  138. package/dist/index.d.ts.map +1 -1
  139. package/dist/index.js +22 -8
  140. package/dist/index.js.map +1 -1
  141. package/dist/lib/archive/archive-inspection.d.ts +48 -0
  142. package/dist/lib/archive/archive-inspection.d.ts.map +1 -0
  143. package/dist/lib/archive/archive-inspection.fixtures.d.ts +7 -0
  144. package/dist/lib/archive/archive-inspection.fixtures.d.ts.map +1 -0
  145. package/dist/lib/archive/archive-inspection.fixtures.js +134 -0
  146. package/dist/lib/archive/archive-inspection.fixtures.js.map +1 -0
  147. package/dist/lib/archive/archive-inspection.js +253 -0
  148. package/dist/lib/archive/archive-inspection.js.map +1 -0
  149. package/dist/lib/archive/index.d.ts +3 -0
  150. package/dist/lib/archive/index.d.ts.map +1 -0
  151. package/dist/lib/archive/index.js +2 -0
  152. package/dist/lib/archive/index.js.map +1 -0
  153. package/dist/lib/logger/console.d.ts +5 -12
  154. package/dist/lib/logger/console.d.ts.map +1 -1
  155. package/dist/lib/logger/console.js +5 -47
  156. package/dist/lib/logger/console.js.map +1 -1
  157. package/dist/lib/logger/file.d.ts +8 -11
  158. package/dist/lib/logger/file.d.ts.map +1 -1
  159. package/dist/lib/logger/file.js +26 -37
  160. package/dist/lib/logger/file.js.map +1 -1
  161. package/dist/lib/logger/index.d.ts +3 -1
  162. package/dist/lib/logger/index.d.ts.map +1 -1
  163. package/dist/lib/logger/index.js +3 -1
  164. package/dist/lib/logger/index.js.map +1 -1
  165. package/dist/lib/logger/jsonl.d.ts +21 -0
  166. package/dist/lib/logger/jsonl.d.ts.map +1 -0
  167. package/dist/lib/logger/jsonl.js +29 -0
  168. package/dist/lib/logger/jsonl.js.map +1 -0
  169. package/dist/lib/logger/logger.d.ts +22 -0
  170. package/dist/lib/logger/logger.d.ts.map +1 -1
  171. package/dist/lib/logger/logger.js +37 -0
  172. package/dist/lib/logger/logger.js.map +1 -1
  173. package/dist/lib/logger/session-log.d.ts +21 -0
  174. package/dist/lib/logger/session-log.d.ts.map +1 -0
  175. package/dist/lib/logger/session-log.js +37 -0
  176. package/dist/lib/logger/session-log.js.map +1 -0
  177. package/dist/lib/utils/concurrency.d.ts +1 -1
  178. package/dist/lib/utils/concurrency.d.ts.map +1 -1
  179. package/dist/lib/utils/concurrency.js +30 -7
  180. package/dist/lib/utils/concurrency.js.map +1 -1
  181. package/dist/lib/utils/fs-batch.d.ts +17 -0
  182. package/dist/lib/utils/fs-batch.d.ts.map +1 -0
  183. package/dist/lib/utils/fs-batch.js +38 -0
  184. package/dist/lib/utils/fs-batch.js.map +1 -0
  185. package/dist/lib/utils/result.d.ts +7 -22
  186. package/dist/lib/utils/result.d.ts.map +1 -1
  187. package/dist/lib/utils/result.js +6 -17
  188. package/dist/lib/utils/result.js.map +1 -1
  189. package/dist/lib/utils/sleep.d.ts +10 -0
  190. package/dist/lib/utils/sleep.d.ts.map +1 -0
  191. package/dist/lib/utils/sleep.js +26 -0
  192. package/dist/lib/utils/sleep.js.map +1 -0
  193. package/dist/platform/fs-revision.d.ts +21 -0
  194. package/dist/platform/fs-revision.d.ts.map +1 -0
  195. package/dist/platform/fs-revision.js +9 -0
  196. package/dist/platform/fs-revision.js.map +1 -0
  197. package/dist/platform/fs.d.ts +104 -1
  198. package/dist/platform/fs.d.ts.map +1 -1
  199. package/dist/platform/git.d.ts +59 -0
  200. package/dist/platform/git.d.ts.map +1 -0
  201. package/dist/platform/git.js +8 -0
  202. package/dist/platform/git.js.map +1 -0
  203. package/dist/platform/index.d.ts +40 -1
  204. package/dist/platform/index.d.ts.map +1 -1
  205. package/dist/platform/index.js +1 -1
  206. package/dist/platform/index.js.map +1 -1
  207. package/dist/platform/llm-call-log.d.ts +80 -0
  208. package/dist/platform/llm-call-log.d.ts.map +1 -0
  209. package/dist/platform/llm-call-log.js +12 -0
  210. package/dist/platform/llm-call-log.js.map +1 -0
  211. package/dist/platform/scheduler.d.ts +32 -0
  212. package/dist/platform/scheduler.d.ts.map +1 -0
  213. package/dist/platform/scheduler.js +46 -0
  214. package/dist/platform/scheduler.js.map +1 -0
  215. package/dist/platform/session-log.d.ts +36 -0
  216. package/dist/platform/session-log.d.ts.map +1 -0
  217. package/dist/platform/session-log.js +12 -0
  218. package/dist/platform/session-log.js.map +1 -0
  219. package/dist/platform/shell.d.ts +83 -0
  220. package/dist/platform/shell.d.ts.map +1 -0
  221. package/dist/platform/shell.js +13 -0
  222. package/dist/platform/shell.js.map +1 -0
  223. package/dist/plugins/agent-status/plugin.d.ts.map +1 -1
  224. package/dist/plugins/agent-status/plugin.js +1 -0
  225. package/dist/plugins/agent-status/plugin.js.map +1 -1
  226. package/dist/plugins/agents/plugin.d.ts.map +1 -1
  227. package/dist/plugins/agents/plugin.js +129 -62
  228. package/dist/plugins/agents/plugin.js.map +1 -1
  229. package/dist/plugins/filesystem/listing.d.ts +3 -0
  230. package/dist/plugins/filesystem/listing.d.ts.map +1 -1
  231. package/dist/plugins/filesystem/listing.js +66 -10
  232. package/dist/plugins/filesystem/listing.js.map +1 -1
  233. package/dist/plugins/filesystem/plugin.d.ts.map +1 -1
  234. package/dist/plugins/filesystem/plugin.js +8 -6
  235. package/dist/plugins/filesystem/plugin.js.map +1 -1
  236. package/dist/plugins/git-status/plugin.d.ts +34 -7
  237. package/dist/plugins/git-status/plugin.d.ts.map +1 -1
  238. package/dist/plugins/git-status/plugin.js +168 -64
  239. package/dist/plugins/git-status/plugin.js.map +1 -1
  240. package/dist/plugins/llm-debug/plugin.d.ts.map +1 -1
  241. package/dist/plugins/llm-debug/plugin.js +1 -0
  242. package/dist/plugins/llm-debug/plugin.js.map +1 -1
  243. package/dist/plugins/logs/plugin.d.ts +1 -4
  244. package/dist/plugins/logs/plugin.d.ts.map +1 -1
  245. package/dist/plugins/logs/plugin.js +7 -0
  246. package/dist/plugins/logs/plugin.js.map +1 -1
  247. package/dist/plugins/mailbox/helpers.d.ts +2 -2
  248. package/dist/plugins/mailbox/helpers.js +1 -1
  249. package/dist/plugins/mailbox/plugin.d.ts.map +1 -1
  250. package/dist/plugins/mailbox/plugin.js +38 -43
  251. package/dist/plugins/mailbox/plugin.js.map +1 -1
  252. package/dist/plugins/mailbox/query.d.ts +7 -2
  253. package/dist/plugins/mailbox/query.d.ts.map +1 -1
  254. package/dist/plugins/mailbox/query.js +14 -7
  255. package/dist/plugins/mailbox/query.js.map +1 -1
  256. package/dist/plugins/mailbox/state.d.ts +5 -4
  257. package/dist/plugins/mailbox/state.d.ts.map +1 -1
  258. package/dist/plugins/mailbox/state.js +6 -5
  259. package/dist/plugins/mailbox/state.js.map +1 -1
  260. package/dist/plugins/resources/filename.d.ts +4 -0
  261. package/dist/plugins/resources/filename.d.ts.map +1 -0
  262. package/dist/plugins/resources/filename.js +18 -0
  263. package/dist/plugins/resources/filename.js.map +1 -0
  264. package/dist/plugins/resources/plugin.d.ts +3 -0
  265. package/dist/plugins/resources/plugin.d.ts.map +1 -1
  266. package/dist/plugins/resources/plugin.js +97 -60
  267. package/dist/plugins/resources/plugin.js.map +1 -1
  268. package/dist/plugins/resources/state.d.ts +1 -1
  269. package/dist/plugins/resources/state.js +1 -1
  270. package/dist/plugins/result-eviction/plugin.d.ts.map +1 -1
  271. package/dist/plugins/result-eviction/plugin.js +6 -1
  272. package/dist/plugins/result-eviction/plugin.js.map +1 -1
  273. package/dist/plugins/services/plugin.d.ts +4 -1
  274. package/dist/plugins/services/plugin.d.ts.map +1 -1
  275. package/dist/plugins/services/plugin.js +251 -100
  276. package/dist/plugins/services/plugin.js.map +1 -1
  277. package/dist/plugins/services/prompt.d.ts.map +1 -1
  278. package/dist/plugins/services/prompt.js +1 -4
  279. package/dist/plugins/services/prompt.js.map +1 -1
  280. package/dist/plugins/services/schema.d.ts +15 -5
  281. package/dist/plugins/services/schema.d.ts.map +1 -1
  282. package/dist/plugins/services/service.d.ts +100 -18
  283. package/dist/plugins/services/service.d.ts.map +1 -1
  284. package/dist/plugins/services/service.js +680 -230
  285. package/dist/plugins/services/service.js.map +1 -1
  286. package/dist/plugins/session-lifecycle/plugin.d.ts +21 -1
  287. package/dist/plugins/session-lifecycle/plugin.d.ts.map +1 -1
  288. package/dist/plugins/session-lifecycle/plugin.js +53 -40
  289. package/dist/plugins/session-lifecycle/plugin.js.map +1 -1
  290. package/dist/plugins/session-state/plugin.d.ts +21 -13
  291. package/dist/plugins/session-state/plugin.d.ts.map +1 -1
  292. package/dist/plugins/session-state/plugin.js +214 -79
  293. package/dist/plugins/session-state/plugin.js.map +1 -1
  294. package/dist/plugins/session-stats/plugin.d.ts.map +1 -1
  295. package/dist/plugins/session-stats/plugin.js +1 -0
  296. package/dist/plugins/session-stats/plugin.js.map +1 -1
  297. package/dist/plugins/shell/executor.d.ts +17 -15
  298. package/dist/plugins/shell/executor.d.ts.map +1 -1
  299. package/dist/plugins/shell/executor.js +130 -195
  300. package/dist/plugins/shell/executor.js.map +1 -1
  301. package/dist/plugins/shell/index.d.ts +2 -2
  302. package/dist/plugins/shell/index.d.ts.map +1 -1
  303. package/dist/plugins/shell/index.js +2 -2
  304. package/dist/plugins/shell/index.js.map +1 -1
  305. package/dist/plugins/shell/plugin.d.ts +5 -4
  306. package/dist/plugins/shell/plugin.d.ts.map +1 -1
  307. package/dist/plugins/shell/plugin.js +4 -8
  308. package/dist/plugins/shell/plugin.js.map +1 -1
  309. package/dist/plugins/skills/discovery.d.ts.map +1 -1
  310. package/dist/plugins/skills/discovery.js +10 -8
  311. package/dist/plugins/skills/discovery.js.map +1 -1
  312. package/dist/plugins/skills/index.d.ts +1 -1
  313. package/dist/plugins/skills/index.d.ts.map +1 -1
  314. package/dist/plugins/skills/plugin.d.ts +1 -1
  315. package/dist/plugins/skills/plugin.d.ts.map +1 -1
  316. package/dist/plugins/skills/plugin.js +23 -30
  317. package/dist/plugins/skills/plugin.js.map +1 -1
  318. package/dist/plugins/skills/prompts.d.ts +3 -3
  319. package/dist/plugins/skills/prompts.d.ts.map +1 -1
  320. package/dist/plugins/skills/prompts.js.map +1 -1
  321. package/dist/plugins/skills/schema.d.ts +4 -2
  322. package/dist/plugins/skills/schema.d.ts.map +1 -1
  323. package/dist/plugins/todo/schema.d.ts +1 -1
  324. package/dist/plugins/uploads/plugin.d.ts +11 -4
  325. package/dist/plugins/uploads/plugin.d.ts.map +1 -1
  326. package/dist/plugins/uploads/plugin.js +891 -225
  327. package/dist/plugins/uploads/plugin.js.map +1 -1
  328. package/dist/plugins/uploads/preprocessor.d.ts +14 -0
  329. package/dist/plugins/uploads/preprocessor.d.ts.map +1 -1
  330. package/dist/plugins/uploads/preprocessor.js +11 -0
  331. package/dist/plugins/uploads/preprocessor.js.map +1 -1
  332. package/dist/plugins/uploads/preprocessors/image-classifier.d.ts +1 -1
  333. package/dist/plugins/uploads/preprocessors/image-classifier.d.ts.map +1 -1
  334. package/dist/plugins/uploads/preprocessors/image-classifier.js +51 -26
  335. package/dist/plugins/uploads/preprocessors/image-classifier.js.map +1 -1
  336. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.d.ts +2 -2
  337. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.d.ts.map +1 -1
  338. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.js +41 -16
  339. package/dist/plugins/uploads/preprocessors/markitdown-preprocessor.js.map +1 -1
  340. package/dist/plugins/uploads/preprocessors/pdf-preprocessor.d.ts +1 -1
  341. package/dist/plugins/uploads/preprocessors/pdf-preprocessor.d.ts.map +1 -1
  342. package/dist/plugins/uploads/preprocessors/pdf-preprocessor.js +29 -14
  343. package/dist/plugins/uploads/preprocessors/pdf-preprocessor.js.map +1 -1
  344. package/dist/plugins/uploads/preprocessors/zip-preprocessor.d.ts +1 -1
  345. package/dist/plugins/uploads/preprocessors/zip-preprocessor.d.ts.map +1 -1
  346. package/dist/plugins/uploads/preprocessors/zip-preprocessor.js +62 -34
  347. package/dist/plugins/uploads/preprocessors/zip-preprocessor.js.map +1 -1
  348. package/dist/plugins/uploads/schema.d.ts +25 -2
  349. package/dist/plugins/uploads/schema.d.ts.map +1 -1
  350. package/dist/plugins/uploads/schema.js +24 -3
  351. package/dist/plugins/uploads/schema.js.map +1 -1
  352. package/dist/plugins/uploads/state.d.ts +13 -1
  353. package/dist/plugins/uploads/state.d.ts.map +1 -1
  354. package/dist/plugins/uploads/state.js +4 -1
  355. package/dist/plugins/uploads/state.js.map +1 -1
  356. package/dist/plugins/user-chat/plugin.d.ts +12 -5
  357. package/dist/plugins/user-chat/plugin.d.ts.map +1 -1
  358. package/dist/plugins/user-chat/plugin.js +141 -32
  359. package/dist/plugins/user-chat/plugin.js.map +1 -1
  360. package/dist/plugins/workers/context.d.ts +35 -13
  361. package/dist/plugins/workers/context.d.ts.map +1 -1
  362. package/dist/plugins/workers/context.js +82 -32
  363. package/dist/plugins/workers/context.js.map +1 -1
  364. package/dist/plugins/workers/definition.d.ts +7 -0
  365. package/dist/plugins/workers/definition.d.ts.map +1 -1
  366. package/dist/plugins/workers/definition.js.map +1 -1
  367. package/dist/plugins/workers/index.d.ts +4 -4
  368. package/dist/plugins/workers/index.d.ts.map +1 -1
  369. package/dist/plugins/workers/index.js +2 -2
  370. package/dist/plugins/workers/index.js.map +1 -1
  371. package/dist/plugins/workers/plugin.d.ts +10 -57
  372. package/dist/plugins/workers/plugin.d.ts.map +1 -1
  373. package/dist/plugins/workers/plugin.js +385 -123
  374. package/dist/plugins/workers/plugin.js.map +1 -1
  375. package/dist/plugins/workers/state.d.ts +69 -0
  376. package/dist/plugins/workers/state.d.ts.map +1 -0
  377. package/dist/plugins/workers/state.js +52 -0
  378. package/dist/plugins/workers/state.js.map +1 -0
  379. package/dist/plugins/workers/worker.d.ts +15 -4
  380. package/dist/plugins/workers/worker.d.ts.map +1 -1
  381. package/dist/testing/conformance.d.ts +106 -0
  382. package/dist/testing/conformance.d.ts.map +1 -0
  383. package/dist/testing/conformance.js +1463 -0
  384. package/dist/testing/conformance.js.map +1 -0
  385. package/dist/testing/node-platform.d.ts.map +1 -1
  386. package/dist/testing/node-platform.js +5 -0
  387. package/dist/testing/node-platform.js.map +1 -1
  388. package/dist/testing/test-harness.d.ts +11 -1
  389. package/dist/testing/test-harness.d.ts.map +1 -1
  390. package/dist/testing/test-harness.js +17 -37
  391. package/dist/testing/test-harness.js.map +1 -1
  392. package/dist/testing/wait-helpers.d.ts.map +1 -1
  393. package/dist/testing/wait-helpers.js +1 -3
  394. package/dist/testing/wait-helpers.js.map +1 -1
  395. package/dist/transport/adapter/client-adapter.d.ts +6 -0
  396. package/dist/transport/adapter/client-adapter.d.ts.map +1 -1
  397. package/dist/transport/adapter/client-adapter.js +50 -3
  398. package/dist/transport/adapter/client-adapter.js.map +1 -1
  399. package/dist/transport/adapter/heartbeat.d.ts +16 -0
  400. package/dist/transport/adapter/heartbeat.d.ts.map +1 -0
  401. package/dist/transport/adapter/heartbeat.js +41 -0
  402. package/dist/transport/adapter/heartbeat.js.map +1 -0
  403. package/dist/transport/adapter/server-adapter.d.ts.map +1 -1
  404. package/dist/transport/adapter/server-adapter.js +6 -0
  405. package/dist/transport/adapter/server-adapter.js.map +1 -1
  406. package/dist/transport/http/app.d.ts +3 -30
  407. package/dist/transport/http/app.d.ts.map +1 -1
  408. package/dist/transport/http/app.js +3 -7
  409. package/dist/transport/http/app.js.map +1 -1
  410. package/dist/transport/http/context.d.ts +40 -0
  411. package/dist/transport/http/context.d.ts.map +1 -0
  412. package/dist/transport/http/context.js +17 -0
  413. package/dist/transport/http/context.js.map +1 -0
  414. package/dist/transport/http/fetch-guard.d.ts +49 -0
  415. package/dist/transport/http/fetch-guard.d.ts.map +1 -0
  416. package/dist/transport/http/fetch-guard.js +312 -0
  417. package/dist/transport/http/fetch-guard.js.map +1 -0
  418. package/dist/transport/http/index.d.ts +1 -1
  419. package/dist/transport/http/index.d.ts.map +1 -1
  420. package/dist/transport/http/middleware/bearer-auth.d.ts +1 -1
  421. package/dist/transport/http/middleware/bearer-auth.d.ts.map +1 -1
  422. package/dist/transport/http/middleware/error-handler.d.ts +2 -2
  423. package/dist/transport/http/middleware/error-handler.d.ts.map +1 -1
  424. package/dist/transport/http/path-containment.d.ts +20 -0
  425. package/dist/transport/http/path-containment.d.ts.map +1 -0
  426. package/dist/transport/http/path-containment.js +31 -0
  427. package/dist/transport/http/path-containment.js.map +1 -0
  428. package/dist/transport/http/responses.d.ts +30 -0
  429. package/dist/transport/http/responses.d.ts.map +1 -0
  430. package/dist/transport/http/responses.js +5 -0
  431. package/dist/transport/http/responses.js.map +1 -0
  432. package/dist/transport/http/routes/files.d.ts +1 -1
  433. package/dist/transport/http/routes/files.d.ts.map +1 -1
  434. package/dist/transport/http/routes/files.js +54 -155
  435. package/dist/transport/http/routes/files.js.map +1 -1
  436. package/dist/transport/http/routes/resources.d.ts +1 -1
  437. package/dist/transport/http/routes/resources.d.ts.map +1 -1
  438. package/dist/transport/http/routes/resources.js +92 -57
  439. package/dist/transport/http/routes/resources.js.map +1 -1
  440. package/dist/transport/http/routes/rpc.d.ts +6 -1
  441. package/dist/transport/http/routes/rpc.d.ts.map +1 -1
  442. package/dist/transport/http/routes/rpc.js +51 -21
  443. package/dist/transport/http/routes/rpc.js.map +1 -1
  444. package/dist/transport/http/routes/upload.d.ts +1 -1
  445. package/dist/transport/http/routes/upload.d.ts.map +1 -1
  446. package/dist/transport/http/routes/upload.js +256 -169
  447. package/dist/transport/http/routes/upload.js.map +1 -1
  448. package/dist/user-config.d.ts +34 -7
  449. package/dist/user-config.d.ts.map +1 -1
  450. package/dist/user-config.js +71 -0
  451. package/dist/user-config.js.map +1 -1
  452. package/package.json +18 -8
  453. package/src/bootstrap.ts +119 -35
  454. package/src/bun-platform/fs.ts +1 -0
  455. package/src/bun-platform/index.ts +18 -1
  456. package/src/bun-platform/server-lifecycle.ts +218 -0
  457. package/src/bun-platform/shell.ts +362 -0
  458. package/src/config.ts +130 -0
  459. package/src/core/agents/agent.ts +219 -95
  460. package/src/core/agents/config.ts +8 -0
  461. package/src/core/agents/retry.ts +16 -17
  462. package/src/core/agents/state.ts +28 -0
  463. package/src/core/context/state.ts +4 -0
  464. package/src/core/events/base-event-store.ts +24 -16
  465. package/src/core/events/file.ts +198 -22
  466. package/src/core/file-store/containment.ts +75 -0
  467. package/src/core/file-store/file-store.ts +106 -25
  468. package/src/core/file-store/types.ts +14 -1
  469. package/src/core/image/image-processor.ts +3 -1
  470. package/src/core/image/types.ts +2 -0
  471. package/src/core/image/vips-resizer.ts +102 -37
  472. package/src/core/llm/anthropic.ts +127 -82
  473. package/src/core/llm/logger.ts +175 -37
  474. package/src/core/llm/openrouter.ts +82 -81
  475. package/src/core/llm/provider-request.ts +40 -0
  476. package/src/core/llm/provider.ts +72 -3
  477. package/src/core/llm/state.ts +12 -0
  478. package/src/core/plugins/plugin-builder.ts +106 -14
  479. package/src/core/preset/preset-builder.ts +12 -22
  480. package/src/core/sessions/context.ts +18 -0
  481. package/src/core/sessions/runtime-activity.ts +78 -0
  482. package/src/core/sessions/schema.ts +29 -3
  483. package/src/core/sessions/session-manager.ts +560 -140
  484. package/src/core/sessions/session-store.ts +38 -0
  485. package/src/core/sessions/session.ts +187 -47
  486. package/src/core/sessions/state.ts +16 -0
  487. package/src/core/system.ts +15 -0
  488. package/src/core/wake-key.ts +65 -0
  489. package/src/index.ts +31 -12
  490. package/src/lib/archive/archive-inspection.fixtures.ts +138 -0
  491. package/src/lib/archive/archive-inspection.ts +359 -0
  492. package/src/lib/archive/index.ts +17 -0
  493. package/src/lib/logger/console.ts +7 -57
  494. package/src/lib/logger/file.ts +28 -50
  495. package/src/lib/logger/index.ts +3 -1
  496. package/src/lib/logger/jsonl.ts +36 -0
  497. package/src/lib/logger/logger.ts +48 -0
  498. package/src/lib/logger/session-log.ts +42 -0
  499. package/src/lib/utils/concurrency.ts +37 -8
  500. package/src/lib/utils/fs-batch.ts +49 -0
  501. package/src/lib/utils/result.ts +7 -31
  502. package/src/lib/utils/sleep.ts +23 -0
  503. package/src/platform/fs-revision.ts +21 -0
  504. package/src/platform/fs.ts +109 -1
  505. package/src/platform/git.ts +67 -0
  506. package/src/platform/index.ts +51 -1
  507. package/src/platform/llm-call-log.ts +86 -0
  508. package/src/platform/scheduler.ts +71 -0
  509. package/src/platform/session-log.ts +39 -0
  510. package/src/platform/shell.ts +89 -0
  511. package/src/plugins/agent-status/plugin.ts +1 -0
  512. package/src/plugins/agents/plugin.ts +134 -81
  513. package/src/plugins/filesystem/listing.ts +70 -12
  514. package/src/plugins/filesystem/plugin.ts +8 -6
  515. package/src/plugins/git-status/plugin.ts +204 -77
  516. package/src/plugins/llm-debug/plugin.ts +1 -0
  517. package/src/plugins/logs/plugin.ts +9 -0
  518. package/src/plugins/mailbox/plugin.ts +46 -64
  519. package/src/plugins/mailbox/query.ts +15 -7
  520. package/src/plugins/mailbox/state.ts +1 -0
  521. package/src/plugins/resources/filename.ts +19 -0
  522. package/src/plugins/resources/plugin.ts +111 -59
  523. package/src/plugins/result-eviction/plugin.ts +6 -1
  524. package/src/plugins/services/plugin.ts +254 -98
  525. package/src/plugins/services/prompt.ts +1 -6
  526. package/src/plugins/services/schema.ts +17 -3
  527. package/src/plugins/services/service.ts +702 -252
  528. package/src/plugins/session-lifecycle/plugin.ts +56 -41
  529. package/src/plugins/session-state/plugin.ts +242 -88
  530. package/src/plugins/session-stats/plugin.ts +1 -0
  531. package/src/plugins/shell/executor.ts +144 -226
  532. package/src/plugins/shell/index.ts +1 -4
  533. package/src/plugins/shell/plugin.ts +7 -9
  534. package/src/plugins/skills/discovery.ts +10 -8
  535. package/src/plugins/skills/index.ts +1 -1
  536. package/src/plugins/skills/plugin.ts +19 -27
  537. package/src/plugins/skills/prompts.ts +3 -3
  538. package/src/plugins/skills/schema.ts +5 -2
  539. package/src/plugins/uploads/plugin.ts +991 -273
  540. package/src/plugins/uploads/preprocessor.ts +25 -0
  541. package/src/plugins/uploads/preprocessors/image-classifier.ts +56 -25
  542. package/src/plugins/uploads/preprocessors/markitdown-preprocessor.ts +41 -15
  543. package/src/plugins/uploads/preprocessors/pdf-preprocessor.ts +32 -16
  544. package/src/plugins/uploads/preprocessors/zip-preprocessor.ts +67 -38
  545. package/src/plugins/uploads/schema.ts +30 -3
  546. package/src/plugins/uploads/state.ts +12 -0
  547. package/src/plugins/user-chat/plugin.ts +196 -40
  548. package/src/plugins/workers/context.ts +102 -39
  549. package/src/plugins/workers/definition.ts +7 -0
  550. package/src/plugins/workers/index.ts +4 -4
  551. package/src/plugins/workers/plugin.ts +461 -151
  552. package/src/plugins/workers/state.ts +64 -0
  553. package/src/plugins/workers/worker.ts +15 -4
  554. package/src/testing/conformance.ts +1751 -0
  555. package/src/testing/node-platform.ts +5 -0
  556. package/src/testing/test-harness.ts +21 -38
  557. package/src/testing/wait-helpers.ts +1 -4
  558. package/src/transport/adapter/client-adapter.ts +60 -3
  559. package/src/transport/adapter/heartbeat.ts +50 -0
  560. package/src/transport/adapter/server-adapter.ts +6 -0
  561. package/src/transport/http/app.ts +7 -35
  562. package/src/transport/http/context.ts +46 -0
  563. package/src/transport/http/fetch-guard.ts +318 -0
  564. package/src/transport/http/index.ts +1 -1
  565. package/src/transport/http/middleware/bearer-auth.ts +1 -1
  566. package/src/transport/http/middleware/error-handler.ts +1 -1
  567. package/src/transport/http/path-containment.ts +44 -0
  568. package/src/transport/http/responses.ts +20 -0
  569. package/src/transport/http/routes/files.ts +69 -159
  570. package/src/transport/http/routes/resources.ts +124 -93
  571. package/src/transport/http/routes/rpc.ts +63 -26
  572. package/src/transport/http/routes/upload.ts +327 -258
  573. package/src/user-config.ts +93 -7
  574. package/dist/config.test.d.ts +0 -2
  575. package/dist/config.test.d.ts.map +0 -1
  576. package/dist/config.test.js +0 -155
  577. package/dist/config.test.js.map +0 -1
  578. package/dist/core/agent-loop.integration.test.d.ts +0 -2
  579. package/dist/core/agent-loop.integration.test.d.ts.map +0 -1
  580. package/dist/core/agent-loop.integration.test.js +0 -414
  581. package/dist/core/agent-loop.integration.test.js.map +0 -1
  582. package/dist/core/agents/agent-config.test.d.ts +0 -2
  583. package/dist/core/agents/agent-config.test.d.ts.map +0 -1
  584. package/dist/core/agents/agent-config.test.js +0 -194
  585. package/dist/core/agents/agent-config.test.js.map +0 -1
  586. package/dist/core/agents/agent-shutdown.test.d.ts +0 -6
  587. package/dist/core/agents/agent-shutdown.test.d.ts.map +0 -1
  588. package/dist/core/agents/agent-shutdown.test.js +0 -180
  589. package/dist/core/agents/agent-shutdown.test.js.map +0 -1
  590. package/dist/core/agents/agent.test.d.ts +0 -2
  591. package/dist/core/agents/agent.test.d.ts.map +0 -1
  592. package/dist/core/agents/agent.test.js +0 -313
  593. package/dist/core/agents/agent.test.js.map +0 -1
  594. package/dist/core/agents/communicator.d.ts +0 -3
  595. package/dist/core/agents/communicator.d.ts.map +0 -1
  596. package/dist/core/agents/communicator.js +0 -13
  597. package/dist/core/agents/communicator.js.map +0 -1
  598. package/dist/core/agents/handler-events.test.d.ts +0 -5
  599. package/dist/core/agents/handler-events.test.d.ts.map +0 -1
  600. package/dist/core/agents/handler-events.test.js +0 -115
  601. package/dist/core/agents/handler-events.test.js.map +0 -1
  602. package/dist/core/agents/response-sanitizer.test.d.ts +0 -2
  603. package/dist/core/agents/response-sanitizer.test.d.ts.map +0 -1
  604. package/dist/core/agents/response-sanitizer.test.js +0 -101
  605. package/dist/core/agents/response-sanitizer.test.js.map +0 -1
  606. package/dist/core/error-recovery.integration.test.d.ts +0 -2
  607. package/dist/core/error-recovery.integration.test.d.ts.map +0 -1
  608. package/dist/core/error-recovery.integration.test.js +0 -288
  609. package/dist/core/error-recovery.integration.test.js.map +0 -1
  610. package/dist/core/event-sourcing.integration.test.d.ts +0 -2
  611. package/dist/core/event-sourcing.integration.test.d.ts.map +0 -1
  612. package/dist/core/event-sourcing.integration.test.js +0 -154
  613. package/dist/core/event-sourcing.integration.test.js.map +0 -1
  614. package/dist/core/events/file.test.d.ts +0 -2
  615. package/dist/core/events/file.test.d.ts.map +0 -1
  616. package/dist/core/events/file.test.js +0 -284
  617. package/dist/core/events/file.test.js.map +0 -1
  618. package/dist/core/events/memory.test.d.ts +0 -2
  619. package/dist/core/events/memory.test.d.ts.map +0 -1
  620. package/dist/core/events/memory.test.js +0 -502
  621. package/dist/core/events/memory.test.js.map +0 -1
  622. package/dist/core/image/image-processor.test.d.ts +0 -2
  623. package/dist/core/image/image-processor.test.d.ts.map +0 -1
  624. package/dist/core/image/image-processor.test.js +0 -171
  625. package/dist/core/image/image-processor.test.js.map +0 -1
  626. package/dist/core/image/vips-resizer.test.d.ts +0 -2
  627. package/dist/core/image/vips-resizer.test.d.ts.map +0 -1
  628. package/dist/core/image/vips-resizer.test.js +0 -336
  629. package/dist/core/image/vips-resizer.test.js.map +0 -1
  630. package/dist/core/llm/anthropic.test.d.ts +0 -2
  631. package/dist/core/llm/anthropic.test.d.ts.map +0 -1
  632. package/dist/core/llm/anthropic.test.js +0 -464
  633. package/dist/core/llm/anthropic.test.js.map +0 -1
  634. package/dist/core/llm/cache-breakpoints.test.d.ts +0 -2
  635. package/dist/core/llm/cache-breakpoints.test.d.ts.map +0 -1
  636. package/dist/core/llm/cache-breakpoints.test.js +0 -69
  637. package/dist/core/llm/cache-breakpoints.test.js.map +0 -1
  638. package/dist/core/llm/cache-live.test.d.ts +0 -16
  639. package/dist/core/llm/cache-live.test.d.ts.map +0 -1
  640. package/dist/core/llm/cache-live.test.js +0 -137
  641. package/dist/core/llm/cache-live.test.js.map +0 -1
  642. package/dist/core/llm/logger.test.d.ts +0 -7
  643. package/dist/core/llm/logger.test.d.ts.map +0 -1
  644. package/dist/core/llm/logger.test.js +0 -228
  645. package/dist/core/llm/logger.test.js.map +0 -1
  646. package/dist/core/llm/message-sanitization.test.d.ts +0 -2
  647. package/dist/core/llm/message-sanitization.test.d.ts.map +0 -1
  648. package/dist/core/llm/message-sanitization.test.js +0 -181
  649. package/dist/core/llm/message-sanitization.test.js.map +0 -1
  650. package/dist/core/llm/mock.test.d.ts +0 -2
  651. package/dist/core/llm/mock.test.d.ts.map +0 -1
  652. package/dist/core/llm/mock.test.js +0 -318
  653. package/dist/core/llm/mock.test.js.map +0 -1
  654. package/dist/core/llm/openrouter-mapping.test.d.ts +0 -2
  655. package/dist/core/llm/openrouter-mapping.test.d.ts.map +0 -1
  656. package/dist/core/llm/openrouter-mapping.test.js +0 -172
  657. package/dist/core/llm/openrouter-mapping.test.js.map +0 -1
  658. package/dist/core/llm/openrouter.test.d.ts +0 -2
  659. package/dist/core/llm/openrouter.test.d.ts.map +0 -1
  660. package/dist/core/llm/openrouter.test.js +0 -377
  661. package/dist/core/llm/openrouter.test.js.map +0 -1
  662. package/dist/core/llm/provider-integration.test.d.ts +0 -12
  663. package/dist/core/llm/provider-integration.test.d.ts.map +0 -1
  664. package/dist/core/llm/provider-integration.test.js +0 -350
  665. package/dist/core/llm/provider-integration.test.js.map +0 -1
  666. package/dist/core/llm/routing-provider.test.d.ts +0 -2
  667. package/dist/core/llm/routing-provider.test.d.ts.map +0 -1
  668. package/dist/core/llm/routing-provider.test.js +0 -94
  669. package/dist/core/llm/routing-provider.test.js.map +0 -1
  670. package/dist/core/llm/snapshot-middleware.test.d.ts +0 -2
  671. package/dist/core/llm/snapshot-middleware.test.d.ts.map +0 -1
  672. package/dist/core/llm/snapshot-middleware.test.js +0 -144
  673. package/dist/core/llm/snapshot-middleware.test.js.map +0 -1
  674. package/dist/core/multi-agent.integration.test.d.ts +0 -2
  675. package/dist/core/multi-agent.integration.test.d.ts.map +0 -1
  676. package/dist/core/multi-agent.integration.test.js +0 -298
  677. package/dist/core/multi-agent.integration.test.js.map +0 -1
  678. package/dist/core/plugin-hooks.integration.test.d.ts +0 -2
  679. package/dist/core/plugin-hooks.integration.test.d.ts.map +0 -1
  680. package/dist/core/plugin-hooks.integration.test.js +0 -458
  681. package/dist/core/plugin-hooks.integration.test.js.map +0 -1
  682. package/dist/core/session-lifecycle.integration.test.d.ts +0 -2
  683. package/dist/core/session-lifecycle.integration.test.d.ts.map +0 -1
  684. package/dist/core/session-lifecycle.integration.test.js +0 -159
  685. package/dist/core/session-lifecycle.integration.test.js.map +0 -1
  686. package/dist/core/sessions/fork-utils.test.d.ts +0 -2
  687. package/dist/core/sessions/fork-utils.test.d.ts.map +0 -1
  688. package/dist/core/sessions/fork-utils.test.js +0 -129
  689. package/dist/core/sessions/fork-utils.test.js.map +0 -1
  690. package/dist/core/sessions/session-overrides.integration.test.d.ts +0 -2
  691. package/dist/core/sessions/session-overrides.integration.test.d.ts.map +0 -1
  692. package/dist/core/sessions/session-overrides.integration.test.js +0 -154
  693. package/dist/core/sessions/session-overrides.integration.test.js.map +0 -1
  694. package/dist/core/sessions/session.test.d.ts +0 -2
  695. package/dist/core/sessions/session.test.d.ts.map +0 -1
  696. package/dist/core/sessions/session.test.js +0 -1100
  697. package/dist/core/sessions/session.test.js.map +0 -1
  698. package/dist/core/tools/executor.test.d.ts +0 -2
  699. package/dist/core/tools/executor.test.d.ts.map +0 -1
  700. package/dist/core/tools/executor.test.js +0 -144
  701. package/dist/core/tools/executor.test.js.map +0 -1
  702. package/dist/lib/logger/console.test.d.ts +0 -2
  703. package/dist/lib/logger/console.test.d.ts.map +0 -1
  704. package/dist/lib/logger/console.test.js +0 -258
  705. package/dist/lib/logger/console.test.js.map +0 -1
  706. package/dist/lib/never.d.ts +0 -2
  707. package/dist/lib/never.d.ts.map +0 -1
  708. package/dist/lib/never.js +0 -4
  709. package/dist/lib/never.js.map +0 -1
  710. package/dist/lib/utils/concurrency.test.d.ts +0 -2
  711. package/dist/lib/utils/concurrency.test.d.ts.map +0 -1
  712. package/dist/lib/utils/concurrency.test.js +0 -135
  713. package/dist/lib/utils/concurrency.test.js.map +0 -1
  714. package/dist/plugins/agent-status/agent-status.test.d.ts +0 -2
  715. package/dist/plugins/agent-status/agent-status.test.d.ts.map +0 -1
  716. package/dist/plugins/agent-status/agent-status.test.js +0 -136
  717. package/dist/plugins/agent-status/agent-status.test.js.map +0 -1
  718. package/dist/plugins/agents/agents.integration.test.d.ts +0 -2
  719. package/dist/plugins/agents/agents.integration.test.d.ts.map +0 -1
  720. package/dist/plugins/agents/agents.integration.test.js +0 -830
  721. package/dist/plugins/agents/agents.integration.test.js.map +0 -1
  722. package/dist/plugins/agents/supervision.integration.test.d.ts +0 -2
  723. package/dist/plugins/agents/supervision.integration.test.d.ts.map +0 -1
  724. package/dist/plugins/agents/supervision.integration.test.js +0 -215
  725. package/dist/plugins/agents/supervision.integration.test.js.map +0 -1
  726. package/dist/plugins/context-compact/compaction-live.test.d.ts +0 -17
  727. package/dist/plugins/context-compact/compaction-live.test.d.ts.map +0 -1
  728. package/dist/plugins/context-compact/compaction-live.test.js +0 -177
  729. package/dist/plugins/context-compact/compaction-live.test.js.map +0 -1
  730. package/dist/plugins/context-compact/context-compact.integration.test.d.ts +0 -2
  731. package/dist/plugins/context-compact/context-compact.integration.test.d.ts.map +0 -1
  732. package/dist/plugins/context-compact/context-compact.integration.test.js +0 -364
  733. package/dist/plugins/context-compact/context-compact.integration.test.js.map +0 -1
  734. package/dist/plugins/context-compact/context-compactor.test.d.ts +0 -2
  735. package/dist/plugins/context-compact/context-compactor.test.d.ts.map +0 -1
  736. package/dist/plugins/context-compact/context-compactor.test.js +0 -858
  737. package/dist/plugins/context-compact/context-compactor.test.js.map +0 -1
  738. package/dist/plugins/context-compact/history-offloader.test.d.ts +0 -2
  739. package/dist/plugins/context-compact/history-offloader.test.d.ts.map +0 -1
  740. package/dist/plugins/context-compact/history-offloader.test.js +0 -77
  741. package/dist/plugins/context-compact/history-offloader.test.js.map +0 -1
  742. package/dist/plugins/filesystem/filesystem.integration.test.d.ts +0 -2
  743. package/dist/plugins/filesystem/filesystem.integration.test.d.ts.map +0 -1
  744. package/dist/plugins/filesystem/filesystem.integration.test.js +0 -447
  745. package/dist/plugins/filesystem/filesystem.integration.test.js.map +0 -1
  746. package/dist/plugins/filesystem/schema.d.ts +0 -7
  747. package/dist/plugins/filesystem/schema.d.ts.map +0 -1
  748. package/dist/plugins/filesystem/schema.js +0 -2
  749. package/dist/plugins/filesystem/schema.js.map +0 -1
  750. package/dist/plugins/limits-guard/limit-guard.test.d.ts +0 -2
  751. package/dist/plugins/limits-guard/limit-guard.test.d.ts.map +0 -1
  752. package/dist/plugins/limits-guard/limit-guard.test.js +0 -194
  753. package/dist/plugins/limits-guard/limit-guard.test.js.map +0 -1
  754. package/dist/plugins/limits-guard/limits-guard.integration.test.d.ts +0 -2
  755. package/dist/plugins/limits-guard/limits-guard.integration.test.d.ts.map +0 -1
  756. package/dist/plugins/limits-guard/limits-guard.integration.test.js +0 -887
  757. package/dist/plugins/limits-guard/limits-guard.integration.test.js.map +0 -1
  758. package/dist/plugins/llm-debug/llm-debug.integration.test.d.ts +0 -2
  759. package/dist/plugins/llm-debug/llm-debug.integration.test.d.ts.map +0 -1
  760. package/dist/plugins/llm-debug/llm-debug.integration.test.js +0 -157
  761. package/dist/plugins/llm-debug/llm-debug.integration.test.js.map +0 -1
  762. package/dist/plugins/mailbox/mailbox.integration.test.d.ts +0 -2
  763. package/dist/plugins/mailbox/mailbox.integration.test.d.ts.map +0 -1
  764. package/dist/plugins/mailbox/mailbox.integration.test.js +0 -678
  765. package/dist/plugins/mailbox/mailbox.integration.test.js.map +0 -1
  766. package/dist/plugins/resources/resources.integration.test.d.ts +0 -2
  767. package/dist/plugins/resources/resources.integration.test.d.ts.map +0 -1
  768. package/dist/plugins/resources/resources.integration.test.js +0 -62
  769. package/dist/plugins/resources/resources.integration.test.js.map +0 -1
  770. package/dist/plugins/result-eviction/result-eviction.integration.test.d.ts +0 -2
  771. package/dist/plugins/result-eviction/result-eviction.integration.test.d.ts.map +0 -1
  772. package/dist/plugins/result-eviction/result-eviction.integration.test.js +0 -294
  773. package/dist/plugins/result-eviction/result-eviction.integration.test.js.map +0 -1
  774. package/dist/plugins/services/pid-registry.test.d.ts +0 -2
  775. package/dist/plugins/services/pid-registry.test.d.ts.map +0 -1
  776. package/dist/plugins/services/pid-registry.test.js +0 -161
  777. package/dist/plugins/services/pid-registry.test.js.map +0 -1
  778. package/dist/plugins/services/services.integration.test.d.ts +0 -2
  779. package/dist/plugins/services/services.integration.test.d.ts.map +0 -1
  780. package/dist/plugins/services/services.integration.test.js +0 -1057
  781. package/dist/plugins/services/services.integration.test.js.map +0 -1
  782. package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.d.ts +0 -2
  783. package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.d.ts.map +0 -1
  784. package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.js +0 -498
  785. package/dist/plugins/session-lifecycle/session-lifecycle.integration.test.js.map +0 -1
  786. package/dist/plugins/session-stats/plugin.test.d.ts +0 -2
  787. package/dist/plugins/session-stats/plugin.test.d.ts.map +0 -1
  788. package/dist/plugins/session-stats/plugin.test.js +0 -145
  789. package/dist/plugins/session-stats/plugin.test.js.map +0 -1
  790. package/dist/plugins/shell/shell.integration.test.d.ts +0 -2
  791. package/dist/plugins/shell/shell.integration.test.d.ts.map +0 -1
  792. package/dist/plugins/shell/shell.integration.test.js +0 -234
  793. package/dist/plugins/shell/shell.integration.test.js.map +0 -1
  794. package/dist/plugins/shell/shell.test.d.ts +0 -2
  795. package/dist/plugins/shell/shell.test.d.ts.map +0 -1
  796. package/dist/plugins/shell/shell.test.js +0 -236
  797. package/dist/plugins/shell/shell.test.js.map +0 -1
  798. package/dist/plugins/skills/discovery.test.d.ts +0 -2
  799. package/dist/plugins/skills/discovery.test.d.ts.map +0 -1
  800. package/dist/plugins/skills/discovery.test.js +0 -312
  801. package/dist/plugins/skills/discovery.test.js.map +0 -1
  802. package/dist/plugins/skills/skills.integration.test.d.ts +0 -2
  803. package/dist/plugins/skills/skills.integration.test.d.ts.map +0 -1
  804. package/dist/plugins/skills/skills.integration.test.js +0 -475
  805. package/dist/plugins/skills/skills.integration.test.js.map +0 -1
  806. package/dist/plugins/todo/todo.integration.test.d.ts +0 -2
  807. package/dist/plugins/todo/todo.integration.test.d.ts.map +0 -1
  808. package/dist/plugins/todo/todo.integration.test.js +0 -605
  809. package/dist/plugins/todo/todo.integration.test.js.map +0 -1
  810. package/dist/plugins/uploads/preprocessors/image-classifier.test.d.ts +0 -2
  811. package/dist/plugins/uploads/preprocessors/image-classifier.test.d.ts.map +0 -1
  812. package/dist/plugins/uploads/preprocessors/image-classifier.test.js +0 -113
  813. package/dist/plugins/uploads/preprocessors/image-classifier.test.js.map +0 -1
  814. package/dist/plugins/uploads/uploads.integration.test.d.ts +0 -2
  815. package/dist/plugins/uploads/uploads.integration.test.d.ts.map +0 -1
  816. package/dist/plugins/uploads/uploads.integration.test.js +0 -639
  817. package/dist/plugins/uploads/uploads.integration.test.js.map +0 -1
  818. package/dist/plugins/user-chat/user-chat.integration.test.d.ts +0 -2
  819. package/dist/plugins/user-chat/user-chat.integration.test.d.ts.map +0 -1
  820. package/dist/plugins/user-chat/user-chat.integration.test.js +0 -754
  821. package/dist/plugins/user-chat/user-chat.integration.test.js.map +0 -1
  822. package/dist/plugins/workers/workers.integration.test.d.ts +0 -2
  823. package/dist/plugins/workers/workers.integration.test.d.ts.map +0 -1
  824. package/dist/plugins/workers/workers.integration.test.js +0 -629
  825. package/dist/plugins/workers/workers.integration.test.js.map +0 -1
  826. package/dist/prompts/macros.test.d.ts +0 -2
  827. package/dist/prompts/macros.test.d.ts.map +0 -1
  828. package/dist/prompts/macros.test.js +0 -80
  829. package/dist/prompts/macros.test.js.map +0 -1
  830. package/dist/testing/test-harness.test.d.ts +0 -2
  831. package/dist/testing/test-harness.test.d.ts.map +0 -1
  832. package/dist/testing/test-harness.test.js +0 -92
  833. package/dist/testing/test-harness.test.js.map +0 -1
  834. package/dist/transport/http/routes/rpc.integration.test.d.ts +0 -2
  835. package/dist/transport/http/routes/rpc.integration.test.d.ts.map +0 -1
  836. package/dist/transport/http/routes/rpc.integration.test.js +0 -189
  837. package/dist/transport/http/routes/rpc.integration.test.js.map +0 -1
  838. package/dist/transport/http/routes/rpc.test.d.ts +0 -2
  839. package/dist/transport/http/routes/rpc.test.d.ts.map +0 -1
  840. package/dist/transport/http/routes/rpc.test.js +0 -316
  841. package/dist/transport/http/routes/rpc.test.js.map +0 -1
  842. package/src/config.test.ts +0 -174
  843. package/src/core/agent-loop.integration.test.ts +0 -503
  844. package/src/core/agents/agent-config.test.ts +0 -240
  845. package/src/core/agents/agent-shutdown.test.ts +0 -236
  846. package/src/core/agents/agent.test.ts +0 -387
  847. package/src/core/agents/communicator.ts +0 -16
  848. package/src/core/agents/handler-events.test.ts +0 -167
  849. package/src/core/agents/response-sanitizer.test.ts +0 -137
  850. package/src/core/error-recovery.integration.test.ts +0 -343
  851. package/src/core/event-sourcing.integration.test.ts +0 -191
  852. package/src/core/events/file.test.ts +0 -436
  853. package/src/core/events/memory.test.ts +0 -741
  854. package/src/core/image/image-processor.test.ts +0 -218
  855. package/src/core/image/vips-resizer.test.ts +0 -388
  856. package/src/core/llm/anthropic.test.ts +0 -546
  857. package/src/core/llm/cache-breakpoints.test.ts +0 -85
  858. package/src/core/llm/cache-live.test.ts +0 -155
  859. package/src/core/llm/logger.test.ts +0 -270
  860. package/src/core/llm/message-sanitization.test.ts +0 -217
  861. package/src/core/llm/mock.test.ts +0 -402
  862. package/src/core/llm/openrouter-mapping.test.ts +0 -210
  863. package/src/core/llm/openrouter.test.ts +0 -499
  864. package/src/core/llm/provider-integration.test.ts +0 -408
  865. package/src/core/llm/routing-provider.test.ts +0 -113
  866. package/src/core/llm/snapshot-middleware.test.ts +0 -168
  867. package/src/core/multi-agent.integration.test.ts +0 -340
  868. package/src/core/plugin-hooks.integration.test.ts +0 -568
  869. package/src/core/session-lifecycle.integration.test.ts +0 -202
  870. package/src/core/sessions/fork-utils.test.ts +0 -158
  871. package/src/core/sessions/session-overrides.integration.test.ts +0 -210
  872. package/src/core/sessions/session.test.ts +0 -1736
  873. package/src/core/tools/executor.test.ts +0 -161
  874. package/src/lib/logger/console.test.ts +0 -348
  875. package/src/lib/never.ts +0 -3
  876. package/src/lib/utils/concurrency.test.ts +0 -169
  877. package/src/plugins/agent-status/agent-status.test.ts +0 -164
  878. package/src/plugins/agents/agents.integration.test.ts +0 -976
  879. package/src/plugins/agents/supervision.integration.test.ts +0 -249
  880. package/src/plugins/context-compact/compaction-live.test.ts +0 -221
  881. package/src/plugins/context-compact/context-compact.integration.test.ts +0 -418
  882. package/src/plugins/context-compact/context-compactor.test.ts +0 -1036
  883. package/src/plugins/context-compact/history-offloader.test.ts +0 -100
  884. package/src/plugins/filesystem/filesystem.integration.test.ts +0 -529
  885. package/src/plugins/filesystem/schema.ts +0 -6
  886. package/src/plugins/limits-guard/limit-guard.test.ts +0 -253
  887. package/src/plugins/limits-guard/limits-guard.integration.test.ts +0 -1012
  888. package/src/plugins/llm-debug/llm-debug.integration.test.ts +0 -192
  889. package/src/plugins/mailbox/mailbox.integration.test.ts +0 -794
  890. package/src/plugins/resources/resources.integration.test.ts +0 -68
  891. package/src/plugins/result-eviction/result-eviction.integration.test.ts +0 -332
  892. package/src/plugins/services/pid-registry.test.ts +0 -180
  893. package/src/plugins/services/services.integration.test.ts +0 -1223
  894. package/src/plugins/session-lifecycle/session-lifecycle.integration.test.ts +0 -639
  895. package/src/plugins/session-stats/plugin.test.ts +0 -160
  896. package/src/plugins/shell/shell.integration.test.ts +0 -273
  897. package/src/plugins/shell/shell.test.ts +0 -314
  898. package/src/plugins/skills/discovery.test.ts +0 -395
  899. package/src/plugins/skills/skills.integration.test.ts +0 -580
  900. package/src/plugins/todo/todo.integration.test.ts +0 -687
  901. package/src/plugins/uploads/preprocessors/image-classifier.test.ts +0 -142
  902. package/src/plugins/uploads/uploads.integration.test.ts +0 -831
  903. package/src/plugins/user-chat/user-chat.integration.test.ts +0 -893
  904. package/src/plugins/workers/workers.integration.test.ts +0 -778
  905. package/src/prompts/macros.test.ts +0 -107
  906. package/src/testing/test-harness.test.ts +0 -114
  907. package/src/transport/http/routes/rpc.integration.test.ts +0 -243
  908. package/src/transport/http/routes/rpc.test.ts +0 -372
@@ -1,23 +1,29 @@
1
1
  import z from 'zod/v4'
2
- import { ValidationErrors } from '~/core/errors.js'
2
+ import type { AgentId } from '~/core/agents/schema.js'
3
+ import { createDomainError, type DomainError, ValidationErrors } from '~/core/errors.js'
3
4
  import type { FileStore } from '~/core/file-store/types.js'
5
+ import type { InferenceContext } from '~/core/llm/provider.js'
4
6
  import { definePlugin } from '~/core/plugins/plugin-builder.js'
7
+ import type { SessionContext } from '~/core/sessions/context.js'
8
+ import type { RuntimeLeaseRelease, SessionRuntimeActivity } from '~/core/sessions/runtime-activity.js'
5
9
  import { SessionId } from '~/core/sessions/schema.js'
6
10
  import { getEntryAgentId } from '~/core/sessions/state.js'
7
- import { Err, Ok } from '~/lib/utils/result.js'
8
- import type { PreprocessorRegistry } from './preprocessor.js'
9
- import { generateUploadId, type MessageAttachment, UploadId, type UploadMetadata } from './schema.js'
10
- import { type PendingUpload, uploadEvents, type UploadsState } from './state.js'
11
+ import { ArchiveBudget, type ArchiveLimitOverrides } from '~/lib/archive/index.js'
12
+ import type { Logger } from '~/lib/logger/logger.js'
13
+ import { Err, Ok, type Result } from '~/lib/utils/result.js'
14
+ import type { PreprocessorRegistry, PreprocessorResult } from './preprocessor.js'
15
+ import { generateUploadId, type MessageAttachment, parseUploadId, UploadId, type UploadMetadata, uploadIdSchema } from './schema.js'
16
+ import { type PendingUpload, type UploadsState, uploadEvents } from './state.js'
11
17
 
12
18
  // ============================================================================
13
19
  // Notification schemas
14
20
  // ============================================================================
15
21
 
22
+ // No producer sets extractedContent: it can be megabytes and clients read it from the state projection.
16
23
  const statusChangedSchema = z.object({
17
24
  sessionId: z.string(),
18
25
  uploadId: z.string(),
19
26
  status: z.enum(['processing', 'ready', 'failed']),
20
- extractedContent: z.string().optional(),
21
27
  error: z.string().optional(),
22
28
  })
23
29
 
@@ -45,6 +51,13 @@ const ALLOWED_MIME_TYPES = [
45
51
  ]
46
52
 
47
53
  const PROCESSING_TIMEOUT_MS = 120_000 // 120 seconds
54
+ const PROCESSING_ABORT_GRACE_MS = 1_000
55
+ const CLOSE_DRAIN_TIMEOUT_MS = 30_000
56
+ const TERMINAL_APPEND_ATTEMPTS = 3
57
+ const TERMINAL_APPEND_RETRY_DELAY_MS = 25
58
+
59
+ /** Store faults are internal: the client gets this fixed text, never the driver's own message. */
60
+ const uploadStorageError = (): DomainError => createDomainError('upload_storage_error', 'Failed to write file', 500)
48
61
 
49
62
  // ============================================================================
50
63
  // Config
@@ -53,6 +66,66 @@ const PROCESSING_TIMEOUT_MS = 120_000 // 120 seconds
53
66
  export interface UploadsPluginConfig {
54
67
  dataFileStore: FileStore
55
68
  preprocessorRegistry?: PreprocessorRegistry
69
+ processingTimeoutMs?: number
70
+ processingAbortGraceMs?: number
71
+ /** Bound in ms on the close-time wait for in-flight uploads and mutations. Must be positive; anything else uses the 30s default. */
72
+ closeDrainTimeoutMs?: number
73
+ archiveLimits?: ArchiveLimitOverrides
74
+ }
75
+
76
+ interface UploadInput {
77
+ sessionId: string
78
+ filename: string
79
+ mimeType: string
80
+ size: number
81
+ fileBuffer: Buffer
82
+ }
83
+
84
+ interface UploadDescriptor {
85
+ filename: string
86
+ mimeType: string
87
+ size: number
88
+ }
89
+
90
+ interface UploadResult {
91
+ status: 'ready' | 'failed'
92
+ extractedContent?: string
93
+ derivedPaths?: string[]
94
+ error?: string
95
+ }
96
+
97
+ interface ActiveUpload {
98
+ controller: AbortController
99
+ processingCompletion: Promise<void>
100
+ completion: Promise<void>
101
+ holdTerminalMutation(): RuntimeLeaseRelease
102
+ }
103
+
104
+ interface UploadsPluginContext {
105
+ activeUploads: Map<string, ActiveUpload>
106
+ /** Completion promise -> the lease reason, so an expired close drain can name what is still running. */
107
+ activeMutations: Map<Promise<void>, string>
108
+ deletedUploads: Set<string>
109
+ operationQueues: Map<string, Promise<void>>
110
+ closing: boolean
111
+ }
112
+
113
+ interface PreparedUpload {
114
+ uploadId: ReturnType<typeof generateUploadId>
115
+ uploadIdStr: string
116
+ uploadStore: FileStore
117
+ /** The name as written. Callers report THIS, never their own input — see toStoredFilename. */
118
+ filename: string
119
+ filePath: string
120
+ createdAt: number
121
+ lifecycle: UploadLifecycle
122
+ }
123
+
124
+ interface UploadLifecycle {
125
+ controller: AbortController
126
+ start<T>(run: (controller: AbortController) => Promise<T>): Promise<T>
127
+ attachRuntimeLease(release: RuntimeLeaseRelease): void
128
+ abandon(): void
56
129
  }
57
130
 
58
131
  // ============================================================================
@@ -60,15 +133,7 @@ export interface UploadsPluginConfig {
60
133
  // ============================================================================
61
134
 
62
135
  function isAllowedMimeType(mimeType: string): boolean {
63
- return ALLOWED_MIME_TYPES.some((allowed) =>
64
- allowed.endsWith('/')
65
- ? mimeType.startsWith(allowed)
66
- : mimeType === allowed
67
- )
68
- }
69
-
70
- function sleep(ms: number): Promise<void> {
71
- return new Promise((resolve) => setTimeout(resolve, ms))
136
+ return ALLOWED_MIME_TYPES.some((allowed) => (allowed.endsWith('/') ? mimeType.startsWith(allowed) : mimeType === allowed))
72
137
  }
73
138
 
74
139
  function formatUploadsForLLM(uploads: PendingUpload[], sessionRoot: string): string {
@@ -80,67 +145,512 @@ function formatUploadsForLLM(uploads: PendingUpload[], sessionRoot: string): str
80
145
  return blocks.join('\n')
81
146
  }
82
147
 
148
+ function withoutKeys<T>(
149
+ values: Record<string, T>,
150
+ uploadIds: readonly string[],
151
+ ): Record<string, T> {
152
+ if (uploadIds.every((uploadId) => values[uploadId] === undefined)) return values
153
+ const next = { ...values }
154
+ for (const uploadId of uploadIds) delete next[uploadId]
155
+ return next
156
+ }
157
+
83
158
  /**
84
- * Run preprocessor (with timeout) and persist final upload metadata to disk.
85
- * Returns the resolved status + extracted/derived data for the caller to emit.
159
+ * True when the value names one entry inside the upload dir and cannot walk anywhere.
160
+ *
161
+ * Deliberately duplicates the download route's boundary check: this is where the bytes
162
+ * are written, and prepareUpload is reachable from callers that never touch HTTP.
86
163
  */
87
- async function runPreprocessAndPersist(args: {
88
- uploadId: string
89
- sessionId: SessionId
164
+ function isBasename(value: string): boolean {
165
+ if (value === '' || value === '.' || value === '..') return false
166
+ return !/[/\\\0]/.test(value)
167
+ }
168
+
169
+ /**
170
+ * The single spelling of a filename this plugin stores and reports.
171
+ *
172
+ * Unicode lets the same name be two different byte strings: macOS hands over the decomposed form
173
+ * (`á` as `a` + U+0301), while a model asked to repeat that name writes the composed one. The two
174
+ * do not compare equal, so every `cp`, `read_file` and shell redirect the agent builds from the
175
+ * name it was given misses the file that is actually on disk. Composing on the way in makes the
176
+ * name the agent produces the name that is there.
177
+ */
178
+ function toStoredFilename(filename: string): string {
179
+ return filename.normalize('NFC')
180
+ }
181
+
182
+ function validateUploadInput(input: Pick<UploadInput, 'size' | 'mimeType' | 'filename'>): string | undefined {
183
+ if (!isBasename(input.filename)) return 'Invalid filename'
184
+ if (input.size > MAX_FILE_SIZE) return `File too large: max ${MAX_FILE_SIZE / (1024 * 1024)}MB`
185
+ if (!isAllowedMimeType(input.mimeType)) return `Unsupported file type: ${input.mimeType}`
186
+ return undefined
187
+ }
188
+
189
+ function reserveUploadLifecycle(pluginContext: UploadsPluginContext, uploadId: string): UploadLifecycle {
190
+ const controller = new AbortController()
191
+ if (pluginContext.closing) controller.abort(new Error('Session is closing'))
192
+ let resolveProcessingCompletion = () => {}
193
+ const processingCompletion = new Promise<void>((resolve) => {
194
+ resolveProcessingCompletion = resolve
195
+ })
196
+ let resolveCompletion = () => {}
197
+ const completion = new Promise<void>((resolve) => {
198
+ resolveCompletion = resolve
199
+ })
200
+ let processingSettled = false
201
+ let terminalHolds = 0
202
+ let settled = false
203
+ let releaseRuntimeLease: RuntimeLeaseRelease | undefined
204
+ let operation: ActiveUpload
205
+ const settle = () => {
206
+ if (!processingSettled || terminalHolds !== 0 || settled) return
207
+ settled = true
208
+ if (pluginContext.activeUploads.get(uploadId) === operation) pluginContext.activeUploads.delete(uploadId)
209
+ releaseRuntimeLease?.()
210
+ releaseRuntimeLease = undefined
211
+ resolveCompletion()
212
+ }
213
+ const finishProcessing = () => {
214
+ if (processingSettled) return
215
+ processingSettled = true
216
+ resolveProcessingCompletion()
217
+ settle()
218
+ }
219
+ operation = {
220
+ controller,
221
+ processingCompletion,
222
+ completion,
223
+ holdTerminalMutation() {
224
+ if (settled) return () => {}
225
+ terminalHolds++
226
+ let released = false
227
+ return () => {
228
+ if (released) return
229
+ released = true
230
+ terminalHolds--
231
+ settle()
232
+ }
233
+ },
234
+ }
235
+ pluginContext.activeUploads.set(uploadId, operation)
236
+ return {
237
+ controller,
238
+ start<T>(run: (signalController: AbortController) => Promise<T>): Promise<T> {
239
+ let result: Promise<T>
240
+ try {
241
+ result = run(controller)
242
+ } catch (error) {
243
+ finishProcessing()
244
+ return Promise.reject(error)
245
+ }
246
+ result.then(finishProcessing, finishProcessing)
247
+ return result
248
+ },
249
+ attachRuntimeLease(release) {
250
+ if (settled) {
251
+ release()
252
+ return
253
+ }
254
+ if (releaseRuntimeLease) {
255
+ release()
256
+ throw new Error(`Upload ${uploadId} already has a runtime lease`)
257
+ }
258
+ releaseRuntimeLease = release
259
+ },
260
+ abandon: finishProcessing,
261
+ }
262
+ }
263
+
264
+ function runTrackedMutation<T>(args: {
265
+ pluginContext: UploadsPluginContext
266
+ runtimeActivity: SessionRuntimeActivity
267
+ reason: string
268
+ run: () => Promise<T>
269
+ }): Promise<T> {
270
+ if (args.pluginContext.closing) return Promise.reject(new Error('Session is closing'))
271
+ const releaseRuntimeLease = args.runtimeActivity.acquire(args.reason)
272
+ let resolveCompletion = () => {}
273
+ const completion = new Promise<void>((resolve) => {
274
+ resolveCompletion = resolve
275
+ })
276
+ args.pluginContext.activeMutations.set(completion, args.reason)
277
+ return (async () => {
278
+ try {
279
+ return await args.run()
280
+ } finally {
281
+ args.pluginContext.activeMutations.delete(completion)
282
+ releaseRuntimeLease()
283
+ resolveCompletion()
284
+ }
285
+ })()
286
+ }
287
+
288
+ async function prepareUpload(args: {
289
+ pluginContext: UploadsPluginContext
290
+ dataFileStore: FileStore
291
+ logger: Logger
292
+ input: UploadInput
293
+ }): Promise<Result<PreparedUpload, DomainError>> {
294
+ const filename = toStoredFilename(args.input.filename)
295
+ const validationError = validateUploadInput({ ...args.input, filename })
296
+ if (validationError) return Err(ValidationErrors.invalid(validationError))
297
+
298
+ const uploadId = generateUploadId()
299
+ const uploadIdStr = String(uploadId)
300
+ const lifecycle = reserveUploadLifecycle(args.pluginContext, uploadIdStr)
301
+ let prepared: PreparedUpload | undefined
302
+ try {
303
+ if (lifecycle.controller.signal.aborted) return Err(ValidationErrors.invalid('Session is closing'))
304
+ const uploadStore = args.dataFileStore.scoped(`sessions/${args.input.sessionId}/uploads/${uploadId}`)
305
+ const writeResult = await uploadStore.write(filename, args.input.fileBuffer)
306
+ if (!writeResult.ok) {
307
+ args.logger.error('Could not write upload file', undefined, { uploadId: uploadIdStr, filename, error: writeResult.error })
308
+ return Err(uploadStorageError())
309
+ }
310
+ if (args.pluginContext.closing) {
311
+ await uploadStore.remove(filename)
312
+ return Err(ValidationErrors.invalid('Session is closing'))
313
+ }
314
+ prepared = {
315
+ uploadId,
316
+ uploadIdStr,
317
+ uploadStore,
318
+ filename,
319
+ filePath: writeResult.value.path,
320
+ createdAt: Date.now(),
321
+ lifecycle,
322
+ }
323
+ return Ok(prepared)
324
+ } catch (error) {
325
+ // An embedder-supplied store throws its own driver text (bucket, endpoint, request id) — log it, never echo it.
326
+ args.logger.error('Could not write upload file', error instanceof Error ? error : undefined, { uploadId: uploadIdStr, filename })
327
+ return Err(uploadStorageError())
328
+ } finally {
329
+ // The lifecycle is registered before any fs work, so every failure path must settle it —
330
+ // an orphan never resolves its completion and onSessionClose waits on that.
331
+ if (!prepared) lifecycle.abandon()
332
+ }
333
+ }
334
+
335
+ type PreprocessorOutcome = { kind: 'completed'; result: Result<PreprocessorResult, Error> } | { kind: 'threw'; error: unknown }
336
+
337
+ async function runPreprocessor(args: {
90
338
  uploadStore: FileStore
91
339
  filePath: string
92
- filename: string
93
340
  mimeType: string
94
- size: number
95
- createdAt: number
96
341
  preprocessorRegistry?: PreprocessorRegistry
97
- }): Promise<{
98
- status: 'ready' | 'failed'
99
- extractedContent?: string
100
- derivedPaths?: string[]
101
- error?: string
102
- }> {
342
+ processingTimeoutMs?: number
343
+ processingAbortGraceMs?: number
344
+ archiveLimits?: ArchiveLimitOverrides
345
+ controller: AbortController
346
+ inferenceContext?: Omit<InferenceContext, 'signal'>
347
+ }): Promise<UploadResult> {
103
348
  const preprocessor = args.preprocessorRegistry?.getForMimeType(args.mimeType)
349
+ if (!preprocessor) return { status: 'ready' }
104
350
 
105
- let status: 'ready' | 'failed' = 'ready'
106
- let extractedContent: string | undefined
107
- let derivedPaths: string[] | undefined
108
- let errorMessage: string | undefined
109
-
110
- if (preprocessor) {
111
- const processPromise = preprocessor.process(args.filePath, args.mimeType, {
112
- files: args.uploadStore,
351
+ let timedOut = false
352
+ const processPromise: Promise<PreprocessorOutcome> = (async () => {
353
+ try {
354
+ return {
355
+ kind: 'completed',
356
+ result: await preprocessor.process(args.filePath, args.mimeType, {
357
+ files: args.uploadStore,
358
+ signal: args.controller.signal,
359
+ inferenceContext: args.inferenceContext,
360
+ archiveBudget: new ArchiveBudget(args.archiveLimits),
361
+ }),
362
+ }
363
+ } catch (error) {
364
+ return { kind: 'threw', error }
365
+ }
366
+ })()
367
+ let resolveAbort: (() => void) | undefined
368
+ const aborted = new Promise<{ kind: 'aborted' }>((resolve) => {
369
+ if (args.controller.signal.aborted) {
370
+ resolve({ kind: 'aborted' })
371
+ return
372
+ }
373
+ resolveAbort = () => resolve({ kind: 'aborted' })
374
+ args.controller.signal.addEventListener('abort', resolveAbort, {
375
+ once: true,
113
376
  })
114
- const timeoutPromise = sleep(PROCESSING_TIMEOUT_MS).then(() => ({
115
- ok: false as const,
116
- error: new Error('Processing timeout'),
117
- }))
118
- const result = await Promise.race([processPromise, timeoutPromise])
119
- if (result.ok) {
120
- extractedContent = result.value.extractedContent
121
- derivedPaths = result.value.derivedPaths
377
+ })
378
+ const timeoutId = setTimeout(() => {
379
+ timedOut = true
380
+ args.controller.abort(new Error('Processing timeout'))
381
+ }, args.processingTimeoutMs ?? PROCESSING_TIMEOUT_MS)
382
+
383
+ let outcome: PreprocessorOutcome | undefined
384
+ let graceTimeoutId: ReturnType<typeof setTimeout> | undefined
385
+ try {
386
+ const first = await Promise.race([processPromise, aborted])
387
+ if (first.kind === 'aborted') {
388
+ const grace = new Promise<{ kind: 'grace-expired' }>((resolve) => {
389
+ graceTimeoutId = setTimeout(() => resolve({ kind: 'grace-expired' }), args.processingAbortGraceMs ?? PROCESSING_ABORT_GRACE_MS)
390
+ })
391
+ const afterAbort = await Promise.race([processPromise, grace])
392
+ if (afterAbort.kind !== 'grace-expired') outcome = afterAbort
122
393
  } else {
123
- status = 'failed'
124
- errorMessage = result.error.message
394
+ outcome = first
125
395
  }
396
+ } finally {
397
+ clearTimeout(timeoutId)
398
+ if (graceTimeoutId) clearTimeout(graceTimeoutId)
399
+ if (resolveAbort) args.controller.signal.removeEventListener('abort', resolveAbort)
126
400
  }
127
401
 
128
- const metadata: UploadMetadata = {
129
- uploadId: UploadId(args.uploadId),
402
+ if (args.controller.signal.aborted) {
403
+ return {
404
+ status: 'failed',
405
+ error: timedOut ? 'Processing timeout' : 'Processing cancelled',
406
+ }
407
+ }
408
+ if (outcome?.kind === 'threw') {
409
+ return {
410
+ status: 'failed',
411
+ error: outcome.error instanceof Error ? outcome.error.message : String(outcome.error),
412
+ }
413
+ }
414
+ if (!outcome) return { status: 'failed', error: 'Processing failed' }
415
+ if (!outcome.result.ok) return { status: 'failed', error: outcome.result.error.message }
416
+ return {
417
+ status: 'ready',
418
+ extractedContent: outcome.result.value.extractedContent,
419
+ derivedPaths: outcome.result.value.derivedPaths,
420
+ }
421
+ }
422
+
423
+ async function writeMetadata(uploadStore: FileStore, metadata: UploadMetadata): Promise<void> {
424
+ const result = await uploadStore.write('meta.json', JSON.stringify(metadata, null, 2))
425
+ if (!result.ok) throw new Error(result.error)
426
+ }
427
+
428
+ function createFinalMetadata(args: { prepared: PreparedUpload; sessionId: SessionId; upload: UploadDescriptor; result: UploadResult }): UploadMetadata {
429
+ return {
430
+ uploadId: args.prepared.uploadId,
130
431
  sessionId: args.sessionId,
131
- filename: args.filename,
132
- mimeType: args.mimeType,
133
- size: args.size,
134
- path: args.filePath,
135
- status,
136
- extractedContent,
137
- derivedPaths,
138
- createdAt: args.createdAt,
432
+ filename: args.upload.filename,
433
+ mimeType: args.upload.mimeType,
434
+ size: args.upload.size,
435
+ path: args.prepared.filePath,
436
+ status: args.result.status,
437
+ extractedContent: args.result.extractedContent,
438
+ derivedPaths: args.result.derivedPaths,
439
+ error: args.result.error,
440
+ createdAt: args.prepared.createdAt,
139
441
  completedAt: Date.now(),
140
442
  }
141
- await args.uploadStore.write('meta.json', JSON.stringify(metadata, null, 2))
443
+ }
444
+
445
+ function createUploadResultEvent(upload: UploadDescriptor, uploadId: ReturnType<typeof generateUploadId>, result: UploadResult) {
446
+ return uploadEvents.create('attachment_uploaded', {
447
+ uploadId,
448
+ filename: upload.filename,
449
+ mimeType: upload.mimeType,
450
+ size: upload.size,
451
+ status: result.status,
452
+ extractedContent: result.extractedContent,
453
+ derivedPaths: result.derivedPaths,
454
+ error: result.error,
455
+ })
456
+ }
457
+
458
+ async function withUploadLock<T>(
459
+ pluginContext: UploadsPluginContext,
460
+ uploadId: string,
461
+ run: () => Promise<T>,
462
+ options?: { allowDuringClose?: boolean },
463
+ ): Promise<T> {
464
+ if (pluginContext.closing && !options?.allowDuringClose) throw new Error('Session is closing')
465
+ const previous = pluginContext.operationQueues.get(uploadId) ?? Promise.resolve()
466
+ let release = () => {}
467
+ const current = new Promise<void>((resolve) => {
468
+ release = resolve
469
+ })
470
+ pluginContext.operationQueues.set(uploadId, current)
471
+ await previous
472
+ try {
473
+ if (pluginContext.closing && !options?.allowDuringClose) throw new Error('Session is closing')
474
+ return await run()
475
+ } finally {
476
+ release()
477
+ if (pluginContext.operationQueues.get(uploadId) === current) pluginContext.operationQueues.delete(uploadId)
478
+ }
479
+ }
480
+
481
+ /** Resolves true when everything settled, false when the bound expired first. */
482
+ async function settleWithTimeout(pending: readonly Promise<unknown>[], timeoutMs: number): Promise<boolean> {
483
+ if (pending.length === 0) return true
484
+ let timeoutId: ReturnType<typeof setTimeout> | undefined
485
+ const expired = new Promise<boolean>((resolve) => {
486
+ timeoutId = setTimeout(() => resolve(false), timeoutMs)
487
+ })
488
+ try {
489
+ return await Promise.race([Promise.allSettled(pending).then(() => true), expired])
490
+ } finally {
491
+ if (timeoutId !== undefined) clearTimeout(timeoutId)
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Appends the terminal event, retrying a transient store failure.
497
+ *
498
+ * Losing the append means the projection never gains the attachment, so it is never
499
+ * auto-dequeued to the agent while the caller was already told the upload is ready.
500
+ * Metadata makes it recoverable, but only when the runtime is next built — with idle
501
+ * eviction off (the default) that is a process restart, so retrying here is the fix.
502
+ */
503
+ async function appendTerminalEvent(args: {
504
+ pluginContext: UploadsPluginContext
505
+ append: () => Promise<void>
506
+ logger: Logger
507
+ uploadId: string
508
+ filename: string
509
+ }): Promise<boolean> {
510
+ for (let attempt = 1; attempt <= TERMINAL_APPEND_ATTEMPTS; attempt++) {
511
+ try {
512
+ await args.append()
513
+ return true
514
+ } catch (error) {
515
+ const giveUp = attempt === TERMINAL_APPEND_ATTEMPTS || args.pluginContext.closing
516
+ args.logger.error(
517
+ giveUp ? 'Could not persist upload terminal event' : 'Retrying upload terminal event append',
518
+ error instanceof Error ? error : undefined,
519
+ { uploadId: args.uploadId, filename: args.filename, attempt },
520
+ )
521
+ if (giveUp) return false
522
+ await new Promise<void>((resolve) => setTimeout(resolve, TERMINAL_APPEND_RETRY_DELAY_MS * attempt))
523
+ }
524
+ }
525
+ return false
526
+ }
527
+
528
+ async function processUpload(args: {
529
+ pluginContext: UploadsPluginContext
530
+ prepared: PreparedUpload
531
+ sessionId: SessionId
532
+ upload: UploadDescriptor
533
+ config: UploadsPluginConfig
534
+ logger: Logger
535
+ inferenceContext?: Omit<InferenceContext, 'signal'>
536
+ onTerminal: (result: UploadResult) => Promise<void>
537
+ /** Runs once the terminal status is durable in metadata, even if its event append failed. */
538
+ onPersisted: (result: UploadResult) => Promise<void>
539
+ }): Promise<UploadResult> {
540
+ const result = await runPreprocessor({
541
+ uploadStore: args.prepared.uploadStore,
542
+ filePath: args.prepared.filePath,
543
+ mimeType: args.upload.mimeType,
544
+ preprocessorRegistry: args.config.preprocessorRegistry,
545
+ processingTimeoutMs: args.config.processingTimeoutMs,
546
+ processingAbortGraceMs: args.config.processingAbortGraceMs,
547
+ archiveLimits: args.config.archiveLimits,
548
+ controller: args.prepared.lifecycle.controller,
549
+ inferenceContext: args.inferenceContext,
550
+ })
551
+ await withUploadLock(args.pluginContext, args.prepared.uploadIdStr, async () => {
552
+ if (args.pluginContext.deletedUploads.has(args.prepared.uploadIdStr)) return
553
+ const metadata = createFinalMetadata({
554
+ prepared: args.prepared,
555
+ sessionId: args.sessionId,
556
+ upload: args.upload,
557
+ result,
558
+ })
559
+ await writeMetadata(args.prepared.uploadStore, metadata)
560
+ if (args.pluginContext.closing) return
561
+ const appended = await appendTerminalEvent({
562
+ pluginContext: args.pluginContext,
563
+ append: () => args.onTerminal(result),
564
+ logger: args.logger,
565
+ uploadId: args.prepared.uploadIdStr,
566
+ filename: args.upload.filename,
567
+ })
568
+ if (appended) {
569
+ metadata.terminalEventPersisted = true
570
+ try {
571
+ await writeMetadata(args.prepared.uploadStore, metadata)
572
+ } catch (error) {
573
+ // Only the flag write failed; the event is in the log, so onSessionReady reconciles the metadata.
574
+ args.logger.error(
575
+ 'Could not record the upload terminal event flag',
576
+ error instanceof Error ? error : undefined,
577
+ { uploadId: args.prepared.uploadIdStr, filename: args.upload.filename },
578
+ )
579
+ }
580
+ }
581
+ // The client has no polling fallback, so it is told either way — even when the event never landed.
582
+ await args.onPersisted(result)
583
+ }, { allowDuringClose: true })
584
+ return result
585
+ }
586
+
587
+ function startAsyncUpload(args: {
588
+ pluginContext: UploadsPluginContext
589
+ prepared: PreparedUpload
590
+ sessionId: SessionId
591
+ upload: UploadDescriptor
592
+ config: UploadsPluginConfig
593
+ inferenceContext?: Omit<InferenceContext, 'signal'>
594
+ emitEvent: SessionContext['emitEvent']
595
+ notify: (type: 'uploadStatusChanged', payload: z.infer<typeof statusChangedSchema>) => void
596
+ logger: Logger
597
+ entryAgentId: AgentId | null
598
+ scheduleAgent: (agentId: AgentId) => void
599
+ }): void {
600
+ // start() rejects synchronously when `run` throws before its first await — that rejection needs a handler.
601
+ void args.prepared.lifecycle.start(async () => {
602
+ try {
603
+ await processUpload({
604
+ pluginContext: args.pluginContext,
605
+ prepared: args.prepared,
606
+ sessionId: args.sessionId,
607
+ upload: args.upload,
608
+ config: args.config,
609
+ logger: args.logger,
610
+ inferenceContext: args.inferenceContext,
611
+ onTerminal: async (result) => {
612
+ await args.emitEvent(createUploadResultEvent(args.upload, args.prepared.uploadId, result))
613
+ },
614
+ onPersisted: async (result) => {
615
+ if (result.status === 'ready' && args.entryAgentId) args.scheduleAgent(args.entryAgentId)
616
+ args.notify('uploadStatusChanged', {
617
+ sessionId: String(args.sessionId),
618
+ uploadId: args.prepared.uploadIdStr,
619
+ status: result.status,
620
+ error: result.error,
621
+ })
622
+ },
623
+ })
624
+ } catch (error) {
625
+ args.logger.error('Async upload processing crashed', error instanceof Error ? error : undefined, {
626
+ uploadId: args.prepared.uploadIdStr,
627
+ filename: args.upload.filename,
628
+ })
629
+ }
630
+ }).catch((error: unknown) => {
631
+ args.logger.error('Async upload never started', error instanceof Error ? error : undefined, {
632
+ uploadId: args.prepared.uploadIdStr,
633
+ filename: args.upload.filename,
634
+ })
635
+ })
636
+ }
142
637
 
143
- return { status, extractedContent, derivedPaths, error: errorMessage }
638
+ async function removeUploadFiles(uploadStore: FileStore, path = ''): Promise<Result<void, string>> {
639
+ const listResult = await uploadStore.list(path, { maxDepth: 1 })
640
+ if (!listResult.ok) return listResult
641
+ for (const entry of listResult.value) {
642
+ const entryPath = path ? `${path}/${entry.name}` : entry.name
643
+ if (entry.type === 'directory') {
644
+ const nested = await removeUploadFiles(uploadStore, entryPath)
645
+ if (!nested.ok) return nested
646
+ continue
647
+ }
648
+ if (entryPath === 'meta.json') continue
649
+ if (entry.type !== 'file' && entry.type !== 'symlink') return Err(`Unsupported upload entry: ${entryPath}`)
650
+ const removed = await uploadStore.remove(entryPath)
651
+ if (!removed.ok) return removed
652
+ }
653
+ return Ok(undefined)
144
654
  }
145
655
 
146
656
  // ============================================================================
@@ -148,16 +658,23 @@ async function runPreprocessAndPersist(args: {
148
658
  // ============================================================================
149
659
 
150
660
  export const uploadsPlugin = definePlugin('uploads')
661
+ .order(70)
151
662
  .pluginConfig<UploadsPluginConfig>()
152
663
  .events([uploadEvents])
153
664
  .notification('uploadStatusChanged', { schema: statusChangedSchema })
154
665
  .state<UploadsState>({
155
666
  key: 'uploads',
156
- initial: (): UploadsState => ({ pending: [] }),
667
+ initial: (): UploadsState => ({ pending: [], terminal: {} }),
157
668
  reduce: (state, event) => {
158
669
  switch (event.type) {
159
670
  case 'attachment_uploaded': {
160
- if (event.status !== 'ready') return state
671
+ const pending = state.pending.filter((upload) => String(upload.uploadId) !== String(event.uploadId))
672
+ if (event.status === 'processing') return { ...state, pending }
673
+ const terminal = {
674
+ ...state.terminal,
675
+ [String(event.uploadId)]: event.status,
676
+ }
677
+ if (event.status !== 'ready') return { ...state, pending, terminal }
161
678
  const upload: PendingUpload = {
162
679
  uploadId: event.uploadId,
163
680
  filename: event.filename,
@@ -167,13 +684,22 @@ export const uploadsPlugin = definePlugin('uploads')
167
684
  extractedContent: event.extractedContent,
168
685
  derivedPaths: event.derivedPaths,
169
686
  }
170
- return { ...state, pending: [...state.pending, upload] }
687
+ return { ...state, pending: [...pending, upload], terminal }
171
688
  }
172
689
  case 'attachments_consumed': {
173
690
  const consumedIds = new Set(event.uploadIds.map(String))
174
691
  return {
175
692
  ...state,
176
693
  pending: state.pending.filter((u) => !consumedIds.has(String(u.uploadId))),
694
+ terminal: withoutKeys(state.terminal, [...consumedIds]),
695
+ }
696
+ }
697
+ case 'attachment_deletion_completed': {
698
+ const uploadId = String(event.uploadId)
699
+ return {
700
+ ...state,
701
+ pending: state.pending.filter((upload) => String(upload.uploadId) !== uploadId),
702
+ terminal: withoutKeys(state.terminal, [uploadId]),
177
703
  }
178
704
  }
179
705
  default:
@@ -181,6 +707,15 @@ export const uploadsPlugin = definePlugin('uploads')
181
707
  }
182
708
  },
183
709
  })
710
+ .context(
711
+ async (): Promise<UploadsPluginContext> => ({
712
+ activeUploads: new Map(),
713
+ activeMutations: new Map(),
714
+ deletedUploads: new Set(),
715
+ operationQueues: new Map(),
716
+ closing: false,
717
+ }),
718
+ )
184
719
  .dequeue({
185
720
  hasPendingMessages: (ctx) => {
186
721
  const uploads = ctx.pluginState
@@ -191,30 +726,42 @@ export const uploadsPlugin = definePlugin('uploads')
191
726
  if (uploads.pending.length === 0) return null
192
727
  const sessionRoot = ctx.files.getRoots().session
193
728
  return {
194
- messages: [{
195
- role: 'user',
196
- content: formatUploadsForLLM(uploads.pending, sessionRoot),
197
- }],
729
+ messages: [
730
+ {
731
+ role: 'user',
732
+ content: formatUploadsForLLM(uploads.pending, sessionRoot),
733
+ },
734
+ ],
198
735
  token: uploads.pending.map((u) => u.uploadId),
199
736
  }
200
737
  },
201
738
  markConsumed: async (ctx, token) => {
202
- await ctx.emitEvent(uploadEvents.create('attachments_consumed', {
203
- agentId: ctx.agentId,
204
- uploadIds: token.map(UploadId),
205
- }))
739
+ await runTrackedMutation({
740
+ pluginContext: ctx.pluginContext,
741
+ runtimeActivity: ctx.runtimeActivity,
742
+ reason: 'uploads:consume',
743
+ run: async () => {
744
+ await ctx.emitEvent(
745
+ uploadEvents.create('attachments_consumed', {
746
+ agentId: ctx.agentId,
747
+ uploadIds: token.map(UploadId),
748
+ }),
749
+ )
206
750
 
207
- // Also mark as used on disk
208
- const { dataFileStore } = ctx.pluginConfig
209
- for (const uploadIdStr of token) {
210
- const uploadStore = dataFileStore.scoped(`sessions/${ctx.sessionId}/uploads/${uploadIdStr}`)
211
- const metaResult = await uploadStore.read('meta.json')
212
- if (metaResult.ok) {
213
- const meta: UploadMetadata = JSON.parse(metaResult.value)
214
- meta.usedInMessageId = 'auto-dequeued'
215
- await uploadStore.write('meta.json', JSON.stringify(meta, null, 2))
216
- }
217
- }
751
+ const { dataFileStore } = ctx.pluginConfig
752
+ for (const uploadIdStr of token) {
753
+ await withUploadLock(ctx.pluginContext, uploadIdStr, async () => {
754
+ const uploadStore = dataFileStore.scoped(`sessions/${ctx.sessionId}/uploads/${uploadIdStr}`)
755
+ const metaResult = await uploadStore.read('meta.json')
756
+ if (!metaResult.ok) return
757
+ const meta: UploadMetadata = JSON.parse(metaResult.value)
758
+ if (meta.status === 'deleted') return
759
+ meta.usedInMessageId = 'auto-dequeued'
760
+ await writeMetadata(uploadStore, meta)
761
+ }, { allowDuringClose: true })
762
+ }
763
+ },
764
+ })
218
765
  },
219
766
  })
220
767
  .method('listPending', {
@@ -222,14 +769,16 @@ export const uploadsPlugin = definePlugin('uploads')
222
769
  sessionId: z.string(),
223
770
  }),
224
771
  output: z.object({
225
- uploads: z.array(z.object({
226
- uploadId: z.string(),
227
- filename: z.string(),
228
- mimeType: z.string(),
229
- size: z.number(),
230
- status: z.enum(['processing', 'ready', 'failed']),
231
- createdAt: z.number(),
232
- })),
772
+ uploads: z.array(
773
+ z.object({
774
+ uploadId: z.string(),
775
+ filename: z.string(),
776
+ mimeType: z.string(),
777
+ size: z.number(),
778
+ status: z.enum(['processing', 'ready', 'failed']),
779
+ createdAt: z.number(),
780
+ }),
781
+ ),
233
782
  }),
234
783
  handler: async (ctx, input) => {
235
784
  const { dataFileStore } = ctx.pluginConfig
@@ -282,39 +831,69 @@ export const uploadsPlugin = definePlugin('uploads')
282
831
  .method('delete', {
283
832
  input: z.object({
284
833
  sessionId: z.string(),
285
- uploadId: z.string(),
834
+ uploadId: uploadIdSchema,
286
835
  }),
287
836
  output: z.object({}),
288
837
  handler: async (ctx, input) => {
289
838
  const { dataFileStore } = ctx.pluginConfig
290
839
  const uploadStore = dataFileStore.scoped(`sessions/${input.sessionId}/uploads/${input.uploadId}`)
291
- const metaResult = await uploadStore.read('meta.json')
292
-
293
- if (!metaResult.ok) {
294
- return Err(ValidationErrors.invalid(`Upload not found: ${input.uploadId}`))
295
- }
296
-
297
- const meta: UploadMetadata = JSON.parse(metaResult.value)
298
-
299
- if (meta.sessionId !== input.sessionId) {
300
- return Err(ValidationErrors.invalid('Upload does not belong to this session'))
301
- }
840
+ try {
841
+ return await runTrackedMutation({
842
+ pluginContext: ctx.pluginContext,
843
+ runtimeActivity: ctx.runtimeActivity,
844
+ reason: `upload:${input.uploadId}:delete`,
845
+ run: async () => {
846
+ let releaseTerminalHold: RuntimeLeaseRelease | undefined
847
+ try {
848
+ const reservation = await withUploadLock(ctx.pluginContext, input.uploadId, async () => {
849
+ const metaResult = await uploadStore.read('meta.json')
850
+ if (!metaResult.ok) return Err(ValidationErrors.invalid(`Upload not found: ${input.uploadId}`))
851
+ const meta: UploadMetadata = JSON.parse(metaResult.value)
852
+ if (meta.sessionId !== input.sessionId) {
853
+ return Err(ValidationErrors.invalid('Upload does not belong to this session'))
854
+ }
855
+ if (meta.usedInMessageId) {
856
+ return Err(ValidationErrors.invalid('Cannot delete an upload that has been sent in a message'))
857
+ }
858
+ ctx.pluginContext.deletedUploads.add(input.uploadId)
859
+ const active = ctx.pluginContext.activeUploads.get(input.uploadId)
860
+ releaseTerminalHold = active?.holdTerminalMutation()
861
+ active?.controller.abort(new Error('Upload deleted'))
862
+ return Ok(active?.processingCompletion)
863
+ }, { allowDuringClose: true })
864
+ if (!reservation.ok) return reservation
865
+ await reservation.value
302
866
 
303
- if (meta.usedInMessageId) {
304
- return Err(ValidationErrors.invalid('Cannot delete an upload that has been sent in a message'))
867
+ return await withUploadLock(ctx.pluginContext, input.uploadId, async () => {
868
+ const metaResult = await uploadStore.read('meta.json')
869
+ if (!metaResult.ok) return Err(ValidationErrors.invalid(`Upload not found: ${input.uploadId}`))
870
+ const meta: UploadMetadata = JSON.parse(metaResult.value)
871
+ meta.status = 'deleted'
872
+ await writeMetadata(uploadStore, meta)
873
+ const cleanup = await removeUploadFiles(uploadStore)
874
+ if (!cleanup.ok) return Err(ValidationErrors.invalid(`Could not remove upload: ${input.uploadId}`))
875
+ await ctx.emitEvent(
876
+ uploadEvents.create('attachment_deletion_completed', {
877
+ uploadId: input.uploadId,
878
+ }),
879
+ )
880
+ return Ok({})
881
+ }, { allowDuringClose: true })
882
+ } finally {
883
+ ctx.pluginContext.deletedUploads.delete(input.uploadId)
884
+ releaseTerminalHold?.()
885
+ }
886
+ },
887
+ })
888
+ } catch (error) {
889
+ return Err(ValidationErrors.invalid(error instanceof Error ? error.message : 'Could not delete upload'))
305
890
  }
306
-
307
- // Mark as deleted
308
- meta.status = 'deleted'
309
- await uploadStore.write('meta.json', JSON.stringify(meta, null, 2))
310
-
311
- return Ok({})
312
891
  },
313
892
  })
314
893
  .method('loadAttachments', {
315
894
  input: z.object({
316
895
  sessionId: z.string(),
317
- uploadIds: z.array(z.string()),
896
+ uploadIds: z.array(uploadIdSchema),
318
897
  }),
319
898
  output: z.object({
320
899
  attachments: z.array(z.unknown()),
@@ -357,24 +936,62 @@ export const uploadsPlugin = definePlugin('uploads')
357
936
  .method('markUsed', {
358
937
  input: z.object({
359
938
  sessionId: z.string(),
360
- uploadIds: z.array(z.string()),
939
+ // The id lands in a path, so it is validated here rather than inside the loop below.
940
+ uploadIds: z.array(uploadIdSchema),
361
941
  messageId: z.string(),
362
942
  }),
363
943
  output: z.object({}),
364
944
  handler: async (ctx, input) => {
365
945
  const { dataFileStore } = ctx.pluginConfig
946
+ try {
947
+ return await runTrackedMutation({
948
+ pluginContext: ctx.pluginContext,
949
+ runtimeActivity: ctx.runtimeActivity,
950
+ reason: 'uploads:mark-used',
951
+ run: async () => {
952
+ const successfulIds: string[] = []
953
+ const emitSuccessfulConsumption = async () => {
954
+ if (successfulIds.length === 0) return
955
+ await ctx.emitEvent(
956
+ uploadEvents.create('attachments_consumed', {
957
+ agentId: getEntryAgentId(ctx.sessionState) ?? 'uploads.markUsed',
958
+ uploadIds: successfulIds.map(UploadId),
959
+ }),
960
+ )
961
+ }
366
962
 
367
- for (const uploadIdStr of input.uploadIds) {
368
- const uploadStore = dataFileStore.scoped(`sessions/${input.sessionId}/uploads/${uploadIdStr}`)
369
- const metaResult = await uploadStore.read('meta.json')
370
- if (metaResult.ok) {
371
- const meta: UploadMetadata = JSON.parse(metaResult.value)
372
- meta.usedInMessageId = input.messageId
373
- await uploadStore.write('meta.json', JSON.stringify(meta, null, 2))
374
- }
963
+ for (const uploadIdStr of input.uploadIds) {
964
+ let result: Result<Record<string, never>, ReturnType<typeof ValidationErrors.invalid>>
965
+ try {
966
+ result = await withUploadLock(ctx.pluginContext, uploadIdStr, async () => {
967
+ const uploadStore = dataFileStore.scoped(`sessions/${input.sessionId}/uploads/${uploadIdStr}`)
968
+ const metaResult = await uploadStore.read('meta.json')
969
+ if (!metaResult.ok || ctx.pluginContext.deletedUploads.has(uploadIdStr)) {
970
+ return Err(ValidationErrors.invalid(`Upload not found: ${uploadIdStr}`))
971
+ }
972
+ const meta: UploadMetadata = JSON.parse(metaResult.value)
973
+ if (meta.status === 'deleted') return Err(ValidationErrors.invalid(`Upload not found: ${uploadIdStr}`))
974
+ meta.usedInMessageId = input.messageId
975
+ await writeMetadata(uploadStore, meta)
976
+ return Ok({})
977
+ }, { allowDuringClose: true })
978
+ } catch (error) {
979
+ await emitSuccessfulConsumption()
980
+ throw error
981
+ }
982
+ if (!result.ok) {
983
+ await emitSuccessfulConsumption()
984
+ return result
985
+ }
986
+ successfulIds.push(uploadIdStr)
987
+ }
988
+ await emitSuccessfulConsumption()
989
+ return Ok({})
990
+ },
991
+ })
992
+ } catch (error) {
993
+ return Err(ValidationErrors.invalid(error instanceof Error ? error.message : 'Could not mark upload as used'))
375
994
  }
376
-
377
- return Ok({})
378
995
  },
379
996
  })
380
997
  .method('upload', {
@@ -391,56 +1008,54 @@ export const uploadsPlugin = definePlugin('uploads')
391
1008
  extractedContent: z.string().optional(),
392
1009
  }),
393
1010
  handler: async (ctx, input) => {
394
- const { dataFileStore, preprocessorRegistry } = ctx.pluginConfig
395
-
396
- if (input.size > MAX_FILE_SIZE) {
397
- return Err(ValidationErrors.invalid(`File too large: max ${MAX_FILE_SIZE / (1024 * 1024)}MB`))
398
- }
399
- if (!isAllowedMimeType(input.mimeType)) {
400
- return Err(ValidationErrors.invalid(`Unsupported file type: ${input.mimeType}`))
401
- }
402
-
403
- const uploadId = generateUploadId()
404
- const uploadStore = dataFileStore.scoped(`sessions/${input.sessionId}/uploads/${uploadId}`)
405
-
406
- const writeResult = await uploadStore.write(input.filename, input.fileBuffer)
407
- if (!writeResult.ok) {
408
- return Err(ValidationErrors.invalid('Failed to write file'))
409
- }
410
-
411
- const result = await runPreprocessAndPersist({
412
- uploadId: String(uploadId),
413
- sessionId: ctx.sessionId,
414
- uploadStore,
415
- filePath: writeResult.value.path,
416
- filename: input.filename,
417
- mimeType: input.mimeType,
418
- size: input.size,
419
- createdAt: Date.now(),
420
- preprocessorRegistry,
1011
+ const preparedResult = await prepareUpload({
1012
+ pluginContext: ctx.pluginContext,
1013
+ dataFileStore: ctx.pluginConfig.dataFileStore,
1014
+ logger: ctx.logger,
1015
+ input,
421
1016
  })
422
-
423
- await ctx.emitEvent(uploadEvents.create('attachment_uploaded', {
424
- uploadId,
425
- filename: input.filename,
1017
+ if (!preparedResult.ok) return Err(preparedResult.error)
1018
+ const prepared = preparedResult.value
1019
+ // Built from the stored name, not from `input`: the metadata and the event tell the agent
1020
+ // where the file is, and a spelling that differs from the directory entry is unusable.
1021
+ const upload: UploadDescriptor = {
1022
+ filename: prepared.filename,
426
1023
  mimeType: input.mimeType,
427
1024
  size: input.size,
428
- status: result.status,
429
- extractedContent: result.extractedContent,
430
- derivedPaths: result.derivedPaths,
431
- error: result.error,
432
- }))
433
-
1025
+ }
434
1026
  const entryAgentId = getEntryAgentId(ctx.sessionState)
435
- if (result.status === 'ready' && entryAgentId) {
436
- ctx.scheduleAgent(entryAgentId)
1027
+ try {
1028
+ const result = await prepared.lifecycle.start(() =>
1029
+ processUpload({
1030
+ pluginContext: ctx.pluginContext,
1031
+ prepared,
1032
+ sessionId: ctx.sessionId,
1033
+ upload,
1034
+ config: ctx.pluginConfig,
1035
+ logger: ctx.logger,
1036
+ inferenceContext: entryAgentId
1037
+ ? {
1038
+ sessionId: String(ctx.sessionId),
1039
+ agentId: String(entryAgentId),
1040
+ fileStore: ctx.files,
1041
+ }
1042
+ : undefined,
1043
+ onTerminal: async (finalResult) => {
1044
+ await ctx.emitEvent(createUploadResultEvent(upload, prepared.uploadId, finalResult))
1045
+ },
1046
+ onPersisted: async (finalResult) => {
1047
+ if (finalResult.status === 'ready' && entryAgentId) ctx.scheduleAgent(entryAgentId)
1048
+ },
1049
+ }),
1050
+ )
1051
+ return Ok({
1052
+ uploadId: prepared.uploadIdStr,
1053
+ status: result.status,
1054
+ extractedContent: result.extractedContent,
1055
+ })
1056
+ } catch (error) {
1057
+ return Err(ValidationErrors.invalid(error instanceof Error ? error.message : 'Upload processing failed'))
437
1058
  }
438
-
439
- return Ok({
440
- uploadId: String(uploadId),
441
- status: result.status,
442
- extractedContent: result.extractedContent,
443
- })
444
1059
  },
445
1060
  })
446
1061
  .method('uploadAsync', {
@@ -456,126 +1071,229 @@ export const uploadsPlugin = definePlugin('uploads')
456
1071
  status: z.enum(['processing']),
457
1072
  }),
458
1073
  handler: async (ctx, input) => {
459
- const { dataFileStore, preprocessorRegistry } = ctx.pluginConfig
460
-
461
- if (input.size > MAX_FILE_SIZE) {
462
- return Err(ValidationErrors.invalid(`File too large: max ${MAX_FILE_SIZE / (1024 * 1024)}MB`))
463
- }
464
- if (!isAllowedMimeType(input.mimeType)) {
465
- return Err(ValidationErrors.invalid(`Unsupported file type: ${input.mimeType}`))
466
- }
467
-
468
- const uploadId = generateUploadId()
469
- const uploadIdStr = String(uploadId)
470
- const uploadStore = dataFileStore.scoped(`sessions/${input.sessionId}/uploads/${uploadId}`)
471
-
472
- const writeResult = await uploadStore.write(input.filename, input.fileBuffer)
473
- if (!writeResult.ok) {
474
- return Err(ValidationErrors.invalid('Failed to write file'))
1074
+ const preparedResult = await prepareUpload({
1075
+ pluginContext: ctx.pluginContext,
1076
+ dataFileStore: ctx.pluginConfig.dataFileStore,
1077
+ logger: ctx.logger,
1078
+ input,
1079
+ })
1080
+ if (!preparedResult.ok) return Err(preparedResult.error)
1081
+ const prepared = preparedResult.value
1082
+ // Built from the stored name, not from `input`: the metadata and the event tell the agent
1083
+ // where the file is, and a spelling that differs from the directory entry is unusable.
1084
+ const upload: UploadDescriptor = {
1085
+ filename: prepared.filename,
1086
+ mimeType: input.mimeType,
1087
+ size: input.size,
475
1088
  }
476
-
477
- const filePath = writeResult.value.path
478
- const createdAt = Date.now()
479
-
480
- // Persist initial 'processing' metadata so listPending sees it before preprocessor finishes.
481
1089
  const processingMeta: UploadMetadata = {
482
- uploadId,
1090
+ uploadId: prepared.uploadId,
483
1091
  sessionId: ctx.sessionId,
484
- filename: input.filename,
485
- mimeType: input.mimeType,
486
- size: input.size,
487
- path: filePath,
1092
+ filename: upload.filename,
1093
+ mimeType: upload.mimeType,
1094
+ size: upload.size,
1095
+ path: prepared.filePath,
488
1096
  status: 'processing',
489
- createdAt,
490
- completedAt: createdAt,
1097
+ createdAt: prepared.createdAt,
1098
+ }
1099
+ try {
1100
+ await writeMetadata(prepared.uploadStore, processingMeta)
1101
+ await ctx.emitEvent(
1102
+ uploadEvents.create('attachment_uploaded', {
1103
+ uploadId: prepared.uploadId,
1104
+ filename: upload.filename,
1105
+ mimeType: upload.mimeType,
1106
+ size: upload.size,
1107
+ status: 'processing',
1108
+ }),
1109
+ )
1110
+ } catch (error) {
1111
+ prepared.lifecycle.abandon()
1112
+ return Err(ValidationErrors.invalid(error instanceof Error ? error.message : 'Could not start upload'))
491
1113
  }
492
- await uploadStore.write('meta.json', JSON.stringify(processingMeta, null, 2))
1114
+ try {
1115
+ const releaseRuntimeLease = ctx.runtimeActivity.acquire(`upload:${prepared.uploadIdStr}:processing`)
1116
+ prepared.lifecycle.attachRuntimeLease(releaseRuntimeLease)
1117
+ } catch (error) {
1118
+ prepared.lifecycle.abandon()
1119
+ return Err(ValidationErrors.invalid(error instanceof Error ? error.message : 'Could not start upload'))
1120
+ }
1121
+ // Everything from here to startAsyncUpload must be guarded: the lifecycle
1122
+ // only settles once lifecycle.start() runs, so a throw before it leaks the
1123
+ // runtime lease AND hangs onSessionClose, which awaits this completion.
1124
+ // ctx.notify is the realistic trigger — it reaches embedder-supplied
1125
+ // onUserOutput, which in standalone/sandbox writes to sockets.
1126
+ try {
1127
+ ctx.notify('uploadStatusChanged', {
1128
+ sessionId: input.sessionId,
1129
+ uploadId: prepared.uploadIdStr,
1130
+ status: 'processing',
1131
+ })
493
1132
 
494
- await ctx.emitEvent(uploadEvents.create('attachment_uploaded', {
495
- uploadId,
496
- filename: input.filename,
497
- mimeType: input.mimeType,
498
- size: input.size,
499
- status: 'processing',
500
- }))
501
- ctx.notify('uploadStatusChanged', {
502
- sessionId: input.sessionId,
503
- uploadId: uploadIdStr,
1133
+ const sessionId = ctx.sessionId
1134
+ const sessionIdString = String(sessionId)
1135
+ const entryAgentId = getEntryAgentId(ctx.sessionState)
1136
+ const inferenceContext = entryAgentId
1137
+ ? {
1138
+ sessionId: sessionIdString,
1139
+ agentId: String(entryAgentId),
1140
+ fileStore: ctx.files,
1141
+ }
1142
+ : undefined
1143
+ startAsyncUpload({
1144
+ pluginContext: ctx.pluginContext,
1145
+ prepared,
1146
+ sessionId,
1147
+ upload,
1148
+ config: ctx.pluginConfig,
1149
+ inferenceContext,
1150
+ emitEvent: ctx.emitEvent,
1151
+ notify: ctx.notify,
1152
+ logger: ctx.logger,
1153
+ entryAgentId,
1154
+ scheduleAgent: ctx.scheduleAgent,
1155
+ })
1156
+ } catch (error) {
1157
+ prepared.lifecycle.abandon()
1158
+ return Err(ValidationErrors.invalid(error instanceof Error ? error.message : 'Could not start upload'))
1159
+ }
1160
+
1161
+ return Ok({
1162
+ uploadId: prepared.uploadIdStr,
504
1163
  status: 'processing',
505
1164
  })
1165
+ },
1166
+ })
1167
+ .sessionHook('onSessionReady', async (ctx) => {
1168
+ const uploadsPath = `sessions/${ctx.sessionId}/uploads`
1169
+ const listResult = await ctx.pluginConfig.dataFileStore.list(uploadsPath)
1170
+ if (!listResult.ok) return
1171
+ const seenUploadIds = new Set<string>()
506
1172
 
507
- // Capture refs from ctx before the handler returns — `notify`/`emitEvent`
508
- // closures stay valid for the lifetime of the session, which in roj
509
- // outlives any single handler call.
510
- const { emitEvent, notify, logger, scheduleAgent } = ctx
511
- const sessionId = ctx.sessionId
512
- const entryAgentId = getEntryAgentId(ctx.sessionState)
513
-
514
- void (async () => {
515
- try {
516
- const result = await runPreprocessAndPersist({
517
- uploadId: uploadIdStr,
518
- sessionId,
519
- uploadStore,
520
- filePath,
521
- filename: input.filename,
522
- mimeType: input.mimeType,
523
- size: input.size,
524
- createdAt,
525
- preprocessorRegistry,
526
- })
527
-
528
- await emitEvent(uploadEvents.create('attachment_uploaded', {
529
- uploadId,
530
- filename: input.filename,
531
- mimeType: input.mimeType,
532
- size: input.size,
533
- status: result.status,
534
- extractedContent: result.extractedContent,
535
- derivedPaths: result.derivedPaths,
536
- error: result.error,
537
- }))
538
- if (result.status === 'ready' && entryAgentId) {
539
- scheduleAgent(entryAgentId)
540
- }
541
- notify('uploadStatusChanged', {
542
- sessionId: input.sessionId,
543
- uploadId: uploadIdStr,
544
- status: result.status,
545
- extractedContent: result.extractedContent,
546
- error: result.error,
547
- })
548
- } catch (err) {
549
- const message = err instanceof Error ? err.message : String(err)
550
- logger.error('Async upload processing crashed', err instanceof Error ? err : undefined, {
551
- uploadId: uploadIdStr,
552
- filename: input.filename,
553
- })
554
- try {
555
- await emitEvent(uploadEvents.create('attachment_uploaded', {
556
- uploadId,
557
- filename: input.filename,
558
- mimeType: input.mimeType,
559
- size: input.size,
560
- status: 'failed',
561
- error: message,
1173
+ for (const entry of listResult.value) {
1174
+ if (entry.type !== 'directory') continue
1175
+ seenUploadIds.add(entry.name)
1176
+ const uploadStore = ctx.pluginConfig.dataFileStore.scoped(`${uploadsPath}/${entry.name}`)
1177
+ try {
1178
+ const metaResult = await uploadStore.read('meta.json')
1179
+ if (!metaResult.ok) continue
1180
+ const metadata: UploadMetadata = JSON.parse(metaResult.value)
1181
+ if (String(metadata.sessionId) !== String(ctx.sessionId)) continue
1182
+ // The agent can write into the upload dir, and this id becomes a path handed back to it.
1183
+ const parsedUploadId = parseUploadId(String(metadata.uploadId))
1184
+ if (!parsedUploadId.ok) {
1185
+ ctx.logger.warn('Skipping upload metadata with an invalid upload id', { entry: entry.name })
1186
+ continue
1187
+ }
1188
+ const uploadId = String(parsedUploadId.value)
1189
+ seenUploadIds.add(uploadId)
1190
+ const projection = ctx.pluginState.terminal[uploadId]
1191
+ if (metadata.usedInMessageId) {
1192
+ if (projection) {
1193
+ await ctx.emitEvent(uploadEvents.create('attachments_consumed', {
1194
+ agentId: 'uploads.recovery',
1195
+ uploadIds: [metadata.uploadId],
562
1196
  }))
563
- } catch {
564
- // Even event emission failed — best-effort; nothing useful left to do.
565
1197
  }
566
- notify('uploadStatusChanged', {
567
- sessionId: input.sessionId,
568
- uploadId: uploadIdStr,
569
- status: 'failed',
570
- error: message,
571
- })
1198
+ continue
1199
+ }
1200
+ if (metadata.status === 'deleted') {
1201
+ if (projection) {
1202
+ await ctx.emitEvent(uploadEvents.create('attachment_deletion_completed', { uploadId: metadata.uploadId }))
1203
+ }
1204
+ continue
572
1205
  }
573
- })()
1206
+ if (projection) {
1207
+ if (metadata.status !== projection || !metadata.terminalEventPersisted) {
1208
+ metadata.status = projection
1209
+ // createFinalMetadata already wrote result.error before the terminal event.
1210
+ metadata.error = projection === 'failed' ? metadata.error ?? 'Processing failed' : undefined
1211
+ if (projection === 'ready') {
1212
+ const pending = ctx.pluginState.pending.find((upload) => String(upload.uploadId) === uploadId)
1213
+ if (!pending) throw new Error('Ready terminal event has no pending projection')
1214
+ metadata.extractedContent = pending.extractedContent
1215
+ metadata.derivedPaths = pending.derivedPaths
1216
+ } else {
1217
+ metadata.extractedContent = undefined
1218
+ metadata.derivedPaths = undefined
1219
+ }
1220
+ metadata.completedAt ??= Date.now()
1221
+ metadata.terminalEventPersisted = true
1222
+ await writeMetadata(uploadStore, metadata)
1223
+ }
1224
+ continue
1225
+ }
1226
+ if (metadata.terminalEventPersisted) continue
574
1227
 
575
- return Ok({
576
- uploadId: uploadIdStr,
577
- status: 'processing' as const,
1228
+ if (metadata.status === 'processing') {
1229
+ metadata.status = 'failed'
1230
+ metadata.error = 'Processing interrupted by restart'
1231
+ metadata.completedAt = Date.now()
1232
+ await writeMetadata(uploadStore, metadata)
1233
+ }
1234
+ const result: UploadResult = metadata.status === 'ready'
1235
+ ? {
1236
+ status: 'ready',
1237
+ extractedContent: metadata.extractedContent,
1238
+ derivedPaths: metadata.derivedPaths,
1239
+ }
1240
+ : { status: 'failed', error: metadata.error }
1241
+ const recoveredUpload: UploadDescriptor = {
1242
+ filename: metadata.filename,
1243
+ mimeType: metadata.mimeType,
1244
+ size: metadata.size,
1245
+ }
1246
+ await ctx.emitEvent(createUploadResultEvent(recoveredUpload, metadata.uploadId, result))
1247
+ metadata.terminalEventPersisted = true
1248
+ await writeMetadata(uploadStore, metadata)
1249
+ ctx.notify('uploadStatusChanged', {
1250
+ sessionId: String(ctx.sessionId),
1251
+ uploadId,
1252
+ status: result.status,
1253
+ error: result.error,
1254
+ })
1255
+ } catch (error) {
1256
+ ctx.logger.warn('Could not recover interrupted upload', {
1257
+ uploadId: entry.name,
1258
+ error: error instanceof Error ? error.message : String(error),
1259
+ })
1260
+ }
1261
+ }
1262
+ // A fork copies the event log but not the uploads dir, so every inherited upload looks
1263
+ // orphaned here — sweeping them would delete attachments the fork still shows.
1264
+ if (ctx.sessionState.forkedFrom) return
1265
+ for (const uploadId of Object.keys(ctx.pluginState.terminal)) {
1266
+ if (seenUploadIds.has(uploadId)) continue
1267
+ await ctx.emitEvent(uploadEvents.create('attachment_deletion_completed', { uploadId: UploadId(uploadId) }))
1268
+ }
1269
+ })
1270
+ .sessionHook('onSessionClose', async (ctx) => {
1271
+ ctx.pluginContext.closing = true
1272
+ const active = [...ctx.pluginContext.activeUploads.values()]
1273
+ for (const upload of active) upload.controller.abort(new Error('Session is closing'))
1274
+ const configuredDrainTimeoutMs = ctx.pluginConfig.closeDrainTimeoutMs
1275
+ const settled = await settleWithTimeout(
1276
+ [...active.map((upload) => upload.completion), ...ctx.pluginContext.activeMutations.keys()],
1277
+ configuredDrainTimeoutMs !== undefined && configuredDrainTimeoutMs > 0 ? configuredDrainTimeoutMs : CLOSE_DRAIN_TIMEOUT_MS,
1278
+ )
1279
+ if (!settled) {
1280
+ // Releasing the manager's map entry is not the same as freeing the session: a wedged lifecycle
1281
+ // still holds emitEvent/notify/scheduleAgent closures, so the runtime graph outlives disposal
1282
+ // until it settles — bounded here only so disposal itself never hangs on it.
1283
+ ctx.logger.warn('Upload lifecycles did not settle before close', {
1284
+ sessionId: String(ctx.sessionId),
1285
+ reason: ctx.reason,
1286
+ pendingUploadIds: [...ctx.pluginContext.activeUploads.keys()],
1287
+ pendingMutations: [...ctx.pluginContext.activeMutations.values()],
578
1288
  })
579
- },
1289
+ }
1290
+ ctx.pluginContext.activeUploads.clear()
1291
+ ctx.pluginContext.activeMutations.clear()
1292
+ if (settled) {
1293
+ // Work that outlived the drain still reads these: clearing them un-guards a deleted upload
1294
+ // against its own processUpload and drops the lock that keeps the two ordered.
1295
+ ctx.pluginContext.deletedUploads.clear()
1296
+ ctx.pluginContext.operationQueues.clear()
1297
+ }
580
1298
  })
581
1299
  .build()