@fastagent-sh/fastagent 0.21.1 → 0.22.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 (388) hide show
  1. package/README.md +9 -12
  2. package/dist/agent.d.ts +24 -37
  3. package/dist/agent.js +7 -15
  4. package/dist/atomic-write.d.ts +13 -17
  5. package/dist/atomic-write.js +18 -29
  6. package/dist/bind.d.ts +12 -22
  7. package/dist/bind.js +18 -27
  8. package/dist/channel.d.ts +11 -33
  9. package/dist/channels/agentcore-limits.d.ts +2 -13
  10. package/dist/channels/agentcore-limits.js +2 -13
  11. package/dist/channels/agentcore-protocol.d.ts +67 -58
  12. package/dist/channels/agentcore-protocol.js +23 -15
  13. package/dist/channels/agentcore-service.d.ts +19 -33
  14. package/dist/channels/agentcore-service.js +176 -59
  15. package/dist/channels/agentcore.d.ts +39 -33
  16. package/dist/channels/agentcore.js +90 -224
  17. package/dist/channels/body.d.ts +15 -2
  18. package/dist/channels/body.js +24 -2
  19. package/dist/channels/busy.d.ts +1 -21
  20. package/dist/channels/busy.js +2 -39
  21. package/dist/channels/control.d.ts +17 -62
  22. package/dist/channels/control.js +74 -182
  23. package/dist/channels/define-channel.d.ts +39 -0
  24. package/dist/channels/define-channel.js +9 -0
  25. package/dist/channels/discover.d.ts +10 -31
  26. package/dist/channels/discover.js +37 -31
  27. package/dist/channels/feishu/bootstrap-token.d.ts +1 -8
  28. package/dist/channels/feishu/bootstrap-token.js +7 -23
  29. package/dist/channels/feishu/card.d.ts +14 -28
  30. package/dist/channels/feishu/card.js +16 -30
  31. package/dist/channels/feishu/cloud.d.ts +1 -5
  32. package/dist/channels/feishu/context-buffer.d.ts +4 -23
  33. package/dist/channels/feishu/context-buffer.js +8 -31
  34. package/dist/channels/feishu/crypto.d.ts +10 -5
  35. package/dist/channels/feishu/crypto.js +12 -19
  36. package/dist/channels/feishu/feishu-api.d.ts +2 -1
  37. package/dist/channels/feishu/feishu-api.js +5 -4
  38. package/dist/channels/feishu/feishu.d.ts +4 -10
  39. package/dist/channels/feishu/feishu.js +113 -227
  40. package/dist/channels/feishu/invoke-turn.d.ts +5 -4
  41. package/dist/channels/feishu/invoke-turn.js +36 -54
  42. package/dist/channels/feishu/model.d.ts +4 -21
  43. package/dist/channels/feishu/model.js +1 -8
  44. package/dist/channels/feishu/normalize.d.ts +2 -10
  45. package/dist/channels/feishu/normalize.js +7 -42
  46. package/dist/channels/feishu/parse.d.ts +9 -29
  47. package/dist/channels/feishu/parse.js +8 -26
  48. package/dist/channels/feishu/preview.d.ts +12 -14
  49. package/dist/channels/feishu/preview.js +108 -207
  50. package/dist/channels/feishu/register-app.d.ts +8 -23
  51. package/dist/channels/feishu/register-app.js +7 -20
  52. package/dist/channels/feishu/register-webhook.d.ts +2 -9
  53. package/dist/channels/feishu/register-webhook.js +9 -33
  54. package/dist/channels/feishu/scaffold/channel.ts +27 -19
  55. package/dist/channels/feishu/scaffold/channel.websocket.ts +31 -0
  56. package/dist/channels/feishu/setup-mode.d.ts +11 -36
  57. package/dist/channels/feishu/setup-mode.js +7 -26
  58. package/dist/channels/feishu/ws-ingress.d.ts +1 -2
  59. package/dist/channels/feishu/ws-ingress.js +6 -8
  60. package/dist/channels/http.d.ts +15 -15
  61. package/dist/channels/http.js +19 -9
  62. package/dist/channels/kit/attachment-path.d.ts +4 -5
  63. package/dist/channels/kit/attachment-path.js +10 -34
  64. package/dist/channels/kit/context-buffer.d.ts +14 -21
  65. package/dist/channels/kit/context-buffer.js +25 -38
  66. package/dist/channels/kit/delivery.d.ts +35 -0
  67. package/dist/channels/kit/delivery.js +129 -0
  68. package/dist/channels/kit/event-stream.d.ts +5 -0
  69. package/dist/channels/kit/event-stream.js +29 -0
  70. package/dist/channels/kit/invoke-turn-kit.d.ts +53 -26
  71. package/dist/channels/kit/invoke-turn-kit.js +89 -32
  72. package/dist/channels/kit/preview-kit.d.ts +18 -72
  73. package/dist/channels/kit/preview-kit.js +22 -88
  74. package/dist/channels/kit/signature.d.ts +3 -9
  75. package/dist/channels/kit/signature.js +3 -9
  76. package/dist/channels/kit/state.d.ts +1 -2
  77. package/dist/channels/kit/state.js +5 -14
  78. package/dist/channels/kit/stop-command.d.ts +1 -2
  79. package/dist/channels/kit/stop-command.js +6 -9
  80. package/dist/channels/kit/tasks.d.ts +1 -1
  81. package/dist/channels/kit/tasks.js +8 -21
  82. package/dist/channels/kit/text.d.ts +2 -16
  83. package/dist/channels/kit/text.js +23 -31
  84. package/dist/channels/kit/thread-participants.d.ts +4 -17
  85. package/dist/channels/kit/thread-participants.js +8 -55
  86. package/dist/channels/kit/transport.d.ts +26 -0
  87. package/dist/channels/kit/transport.js +20 -0
  88. package/dist/channels/kit/turn-queue.d.ts +7 -11
  89. package/dist/channels/kit/turn-queue.js +21 -39
  90. package/dist/channels/kit/turn-runner.d.ts +32 -18
  91. package/dist/channels/kit/turn-runner.js +98 -53
  92. package/dist/channels/kit/turn-store.d.ts +32 -32
  93. package/dist/channels/kit/turn-store.js +45 -88
  94. package/dist/channels/lark/lark.d.ts +1 -5
  95. package/dist/channels/lark/onboard.d.ts +3 -12
  96. package/dist/channels/lark/onboard.js +3 -13
  97. package/dist/channels/lark/scaffold/channel.ts +27 -19
  98. package/dist/channels/lark/scaffold/channel.websocket.ts +31 -0
  99. package/dist/channels/registration.d.ts +9 -35
  100. package/dist/channels/registration.js +6 -37
  101. package/dist/channels/secret.js +2 -5
  102. package/dist/channels/serve.d.ts +47 -44
  103. package/dist/channels/serve.js +165 -101
  104. package/dist/channels/slack/config-api.d.ts +4 -27
  105. package/dist/channels/slack/config-api.js +5 -34
  106. package/dist/channels/slack/context-buffer.d.ts +2 -3
  107. package/dist/channels/slack/context-buffer.js +5 -5
  108. package/dist/channels/slack/invoke-turn.d.ts +3 -1
  109. package/dist/channels/slack/invoke-turn.js +25 -34
  110. package/dist/channels/slack/manifest.js +1 -3
  111. package/dist/channels/slack/model.d.ts +1 -2
  112. package/dist/channels/slack/model.js +1 -1
  113. package/dist/channels/slack/onboard.d.ts +4 -3
  114. package/dist/channels/slack/onboard.js +8 -11
  115. package/dist/channels/slack/onboarding-state.d.ts +3 -5
  116. package/dist/channels/slack/onboarding-state.js +9 -6
  117. package/dist/channels/slack/parse.d.ts +4 -13
  118. package/dist/channels/slack/parse.js +7 -28
  119. package/dist/channels/slack/preview.d.ts +12 -2
  120. package/dist/channels/slack/preview.js +292 -269
  121. package/dist/channels/slack/reaction.d.ts +11 -3
  122. package/dist/channels/slack/reaction.js +19 -8
  123. package/dist/channels/slack/register-webhook.js +4 -7
  124. package/dist/channels/slack/scaffold/channel.ts +20 -13
  125. package/dist/channels/slack/scaffold/slack-send.ts +2 -2
  126. package/dist/channels/slack/setup-server.d.ts +1 -5
  127. package/dist/channels/slack/setup-server.js +1 -5
  128. package/dist/channels/slack/shared-api.d.ts +1 -4
  129. package/dist/channels/slack/shared-api.js +2 -13
  130. package/dist/channels/slack/slack-api.d.ts +2 -1
  131. package/dist/channels/slack/slack-api.js +6 -4
  132. package/dist/channels/slack/slack.d.ts +11 -14
  133. package/dist/channels/slack/slack.js +121 -122
  134. package/dist/channels/sse.d.ts +1 -1
  135. package/dist/channels/sse.js +1 -1
  136. package/dist/channels/telegram/context-buffer.d.ts +7 -22
  137. package/dist/channels/telegram/context-buffer.js +6 -23
  138. package/dist/channels/telegram/invoke-turn.d.ts +7 -7
  139. package/dist/channels/telegram/invoke-turn.js +24 -43
  140. package/dist/channels/telegram/parse.d.ts +24 -51
  141. package/dist/channels/telegram/parse.js +32 -84
  142. package/dist/channels/telegram/preview.d.ts +10 -1
  143. package/dist/channels/telegram/preview.js +79 -100
  144. package/dist/channels/telegram/register-webhook.js +5 -23
  145. package/dist/channels/telegram/scaffold/channel.ts +25 -18
  146. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -4
  147. package/dist/channels/telegram/telegram-api.d.ts +4 -3
  148. package/dist/channels/telegram/telegram-api.js +9 -7
  149. package/dist/channels/telegram/telegram.d.ts +4 -18
  150. package/dist/channels/telegram/telegram.js +38 -94
  151. package/dist/channels/telegram/turn-store.d.ts +11 -14
  152. package/dist/channels/telegram/turn-store.js +8 -6
  153. package/dist/channels/wait-health.d.ts +4 -6
  154. package/dist/channels/wait-health.js +10 -14
  155. package/dist/cli/add-feishu.d.ts +2 -12
  156. package/dist/cli/add-feishu.js +24 -71
  157. package/dist/cli/add-slack.d.ts +5 -3
  158. package/dist/cli/add-slack.js +53 -76
  159. package/dist/cli/auth-view.d.ts +2 -16
  160. package/dist/cli/auth-view.js +2 -16
  161. package/dist/cli/commands/add.js +25 -40
  162. package/dist/cli/commands/chat.d.ts +0 -1
  163. package/dist/cli/commands/chat.js +5 -6
  164. package/dist/cli/commands/deploy/agentcore.js +60 -65
  165. package/dist/cli/commands/deploy/docker.js +50 -23
  166. package/dist/cli/commands/deploy/fly.js +23 -61
  167. package/dist/cli/commands/deploy/railway.js +23 -18
  168. package/dist/cli/commands/deploy/shared.d.ts +38 -57
  169. package/dist/cli/commands/deploy/shared.js +40 -59
  170. package/dist/cli/commands/deploy.d.ts +10 -27
  171. package/dist/cli/commands/deploy.js +38 -60
  172. package/dist/cli/commands/destroy.d.ts +4 -0
  173. package/dist/cli/commands/destroy.js +56 -0
  174. package/dist/cli/commands/dev.d.ts +2 -1
  175. package/dist/cli/commands/dev.js +20 -23
  176. package/dist/cli/commands/info.d.ts +0 -2
  177. package/dist/cli/commands/info.js +83 -49
  178. package/dist/cli/commands/init.d.ts +1 -2
  179. package/dist/cli/commands/init.js +16 -55
  180. package/dist/cli/commands/invoke.d.ts +0 -1
  181. package/dist/cli/commands/invoke.js +5 -7
  182. package/dist/cli/commands/login.d.ts +2 -1
  183. package/dist/cli/commands/login.js +33 -38
  184. package/dist/cli/commands/logs.js +3 -4
  185. package/dist/cli/commands/models.js +1 -5
  186. package/dist/cli/commands/routine-run.d.ts +6 -0
  187. package/dist/cli/commands/routine-run.js +47 -0
  188. package/dist/cli/commands/routine.d.ts +25 -0
  189. package/dist/cli/commands/routine.js +122 -0
  190. package/dist/cli/commands/start.d.ts +31 -3
  191. package/dist/cli/commands/start.js +131 -81
  192. package/dist/cli/commands/tool.js +26 -12
  193. package/dist/cli/fail.d.ts +13 -23
  194. package/dist/cli/fail.js +20 -23
  195. package/dist/cli/invoke-stream.d.ts +1 -8
  196. package/dist/cli/kernel.d.ts +6 -38
  197. package/dist/cli/kernel.js +14 -77
  198. package/dist/cli/models-view.d.ts +1 -9
  199. package/dist/cli/models-view.js +11 -22
  200. package/dist/cli/program.d.ts +5 -6
  201. package/dist/cli/program.js +120 -157
  202. package/dist/cli/serve.d.ts +57 -41
  203. package/dist/cli/serve.js +94 -121
  204. package/dist/cli/shared.d.ts +15 -61
  205. package/dist/cli/shared.js +55 -181
  206. package/dist/cli.js +1 -8
  207. package/dist/collect.d.ts +6 -0
  208. package/dist/collect.js +6 -0
  209. package/dist/core.d.ts +4 -1
  210. package/dist/core.js +9 -7
  211. package/dist/declared-secrets.d.ts +55 -0
  212. package/dist/declared-secrets.js +88 -0
  213. package/dist/deploy/agentcore/aws-cli.d.ts +91 -0
  214. package/dist/deploy/agentcore/aws-cli.js +64 -0
  215. package/dist/deploy/agentcore/destroy.d.ts +51 -0
  216. package/dist/deploy/agentcore/destroy.js +273 -0
  217. package/dist/deploy/agentcore/forwarder.js +28 -79
  218. package/dist/deploy/agentcore/logs.d.ts +2 -18
  219. package/dist/deploy/agentcore/logs.js +27 -51
  220. package/dist/deploy/agentcore/plan.d.ts +85 -105
  221. package/dist/deploy/agentcore/plan.js +152 -314
  222. package/dist/deploy/agentcore/run.d.ts +28 -28
  223. package/dist/deploy/agentcore/run.js +227 -306
  224. package/dist/deploy/agentcore/zip.d.ts +2 -15
  225. package/dist/deploy/agentcore/zip.js +3 -17
  226. package/dist/deploy/build-context.d.ts +21 -0
  227. package/dist/deploy/build-context.js +137 -0
  228. package/dist/deploy/channel-ingress.d.ts +26 -39
  229. package/dist/deploy/channel-ingress.js +57 -28
  230. package/dist/deploy/container.d.ts +20 -34
  231. package/dist/deploy/container.js +49 -100
  232. package/dist/deploy/docker/plan.d.ts +12 -11
  233. package/dist/deploy/docker/plan.js +43 -24
  234. package/dist/deploy/docker/run.d.ts +22 -25
  235. package/dist/deploy/docker/run.js +39 -37
  236. package/dist/deploy/fly/plan.d.ts +15 -44
  237. package/dist/deploy/fly/plan.js +41 -73
  238. package/dist/deploy/fly/run.d.ts +16 -51
  239. package/dist/deploy/fly/run.js +42 -95
  240. package/dist/deploy/hosts.d.ts +24 -3
  241. package/dist/deploy/hosts.js +24 -3
  242. package/dist/deploy/preflight.d.ts +47 -41
  243. package/dist/deploy/preflight.js +282 -383
  244. package/dist/deploy/railway/plan.d.ts +16 -42
  245. package/dist/deploy/railway/plan.js +42 -60
  246. package/dist/deploy/railway/run.d.ts +30 -68
  247. package/dist/deploy/railway/run.js +86 -100
  248. package/dist/deploy/registration-gate.d.ts +6 -13
  249. package/dist/deploy/residency.d.ts +37 -0
  250. package/dist/deploy/residency.js +24 -0
  251. package/dist/deploy/runner.d.ts +10 -9
  252. package/dist/deploy/runner.js +7 -18
  253. package/dist/deploy/secrets.d.ts +40 -41
  254. package/dist/deploy/secrets.js +117 -103
  255. package/dist/deploy/workspace.d.ts +50 -0
  256. package/dist/deploy/workspace.js +197 -0
  257. package/dist/dev-supervisor.d.ts +1 -9
  258. package/dist/dev-supervisor.js +21 -64
  259. package/dist/effect-port.d.ts +50 -0
  260. package/dist/effect-port.js +77 -0
  261. package/dist/engines/pi/agent-session-factory.d.ts +50 -69
  262. package/dist/engines/pi/agent-session-factory.js +249 -146
  263. package/dist/engines/pi/auth.d.ts +15 -11
  264. package/dist/engines/pi/auth.js +96 -99
  265. package/dist/engines/pi/chat.d.ts +1 -5
  266. package/dist/engines/pi/chat.js +2 -14
  267. package/dist/engines/pi/config.d.ts +53 -51
  268. package/dist/engines/pi/config.js +83 -92
  269. package/dist/engines/pi/create.d.ts +60 -75
  270. package/dist/engines/pi/create.js +108 -141
  271. package/dist/engines/pi/definition.d.ts +14 -58
  272. package/dist/engines/pi/definition.js +21 -85
  273. package/dist/engines/pi/invoke-session.d.ts +5 -25
  274. package/dist/engines/pi/invoke-session.js +233 -244
  275. package/dist/engines/pi/login.d.ts +95 -35
  276. package/dist/engines/pi/login.js +259 -107
  277. package/dist/engines/pi/machine.d.ts +31 -0
  278. package/dist/engines/pi/machine.js +106 -0
  279. package/dist/engines/pi/models.d.ts +70 -89
  280. package/dist/engines/pi/models.js +188 -81
  281. package/dist/engines/pi/open.d.ts +65 -53
  282. package/dist/engines/pi/open.js +129 -69
  283. package/dist/engines/pi/report.d.ts +3 -12
  284. package/dist/engines/pi/report.js +2 -16
  285. package/dist/engines/pi/search-tools.d.ts +1 -9
  286. package/dist/engines/pi/search-tools.js +26 -57
  287. package/dist/engines/pi/service.d.ts +2 -16
  288. package/dist/engines/pi/service.js +2 -16
  289. package/dist/engines/pi/session-builder.d.ts +5 -9
  290. package/dist/engines/pi/session-builder.js +44 -82
  291. package/dist/engines/pi/session-control.js +286 -269
  292. package/dist/engines/pi/session-effects.d.ts +14 -0
  293. package/dist/engines/pi/session-effects.js +30 -0
  294. package/dist/engines/pi/session-inheritance.d.ts +9 -18
  295. package/dist/engines/pi/session-inheritance.js +103 -160
  296. package/dist/engines/pi/session-markers.d.ts +31 -30
  297. package/dist/engines/pi/session-markers.js +26 -25
  298. package/dist/engines/pi/session-settings.d.ts +6 -22
  299. package/dist/engines/pi/session-settings.js +10 -21
  300. package/dist/engines/pi/session-store.d.ts +26 -92
  301. package/dist/engines/pi/session-store.js +96 -310
  302. package/dist/engines/pi/tool-context.d.ts +17 -45
  303. package/dist/engines/pi/tool-context.js +7 -26
  304. package/dist/engines/pi/tool.d.ts +45 -36
  305. package/dist/engines/pi/tool.js +43 -57
  306. package/dist/engines/pi/turn-kit.d.ts +14 -52
  307. package/dist/engines/pi/turn-kit.js +18 -67
  308. package/dist/engines/pi/wake-tool.d.ts +2 -10
  309. package/dist/engines/pi/wake-tool.js +7 -27
  310. package/dist/env.d.ts +27 -28
  311. package/dist/env.js +45 -50
  312. package/dist/index.js +0 -2
  313. package/dist/lark.d.ts +1 -2
  314. package/dist/lark.js +1 -2
  315. package/dist/loader.d.ts +14 -55
  316. package/dist/loader.js +24 -43
  317. package/dist/log.d.ts +2 -12
  318. package/dist/log.js +3 -24
  319. package/dist/node.d.ts +2 -15
  320. package/dist/node.js +2 -18
  321. package/dist/observe.d.ts +2 -8
  322. package/dist/open-url.d.ts +1 -1
  323. package/dist/open-url.js +1 -1
  324. package/dist/paths.d.ts +70 -134
  325. package/dist/paths.js +165 -232
  326. package/dist/pi.d.ts +4 -6
  327. package/dist/pi.js +3 -3
  328. package/dist/proxy.d.ts +16 -6
  329. package/dist/proxy.js +52 -16
  330. package/dist/runtime.d.ts +18 -12
  331. package/dist/runtime.js +18 -11
  332. package/dist/scaffold/add-channel.d.ts +14 -31
  333. package/dist/scaffold/add-channel.js +48 -112
  334. package/dist/scaffold/init.d.ts +9 -31
  335. package/dist/scaffold/init.js +45 -138
  336. package/dist/scaffold/templates/env.example +6 -4
  337. package/dist/scaffold/templates/fastagent.config.ts +36 -0
  338. package/dist/scaffold/templates.d.ts +3 -3
  339. package/dist/scaffold/templates.js +5 -8
  340. package/dist/scaffold/vendor-skill.d.ts +2 -5
  341. package/dist/scaffold/vendor-skill.js +19 -29
  342. package/dist/schedule/cron.d.ts +9 -5
  343. package/dist/schedule/cron.js +9 -11
  344. package/dist/schedule/discover.d.ts +12 -12
  345. package/dist/schedule/discover.js +43 -34
  346. package/dist/schedule/routine.d.ts +43 -0
  347. package/dist/schedule/routine.js +32 -0
  348. package/dist/schedule/run.d.ts +55 -0
  349. package/dist/schedule/run.js +146 -0
  350. package/dist/schedule/scheduler.d.ts +44 -55
  351. package/dist/schedule/scheduler.js +291 -265
  352. package/dist/schedule/state.d.ts +105 -10
  353. package/dist/schedule/state.js +293 -19
  354. package/dist/schedule/wake-alarm.d.ts +10 -23
  355. package/dist/schedule/wake-alarm.js +91 -121
  356. package/dist/schedule/wakeups.d.ts +23 -39
  357. package/dist/schedule/wakeups.js +33 -75
  358. package/dist/secrets-gate.d.ts +32 -0
  359. package/dist/secrets-gate.js +33 -0
  360. package/dist/service.d.ts +137 -98
  361. package/dist/service.js +250 -252
  362. package/dist/session-remote.d.ts +33 -44
  363. package/dist/session-remote.js +203 -192
  364. package/dist/session.d.ts +166 -229
  365. package/dist/session.js +33 -55
  366. package/dist/tunnel.d.ts +8 -36
  367. package/dist/tunnel.js +24 -65
  368. package/dist/version.d.ts +2 -2
  369. package/dist/version.js +2 -2
  370. package/package.json +10 -16
  371. package/dist/channels/agentcore-state.d.ts +0 -77
  372. package/dist/channels/agentcore-state.js +0 -276
  373. package/dist/channels/github/github.d.ts +0 -31
  374. package/dist/channels/github/github.js +0 -89
  375. package/dist/channels/github/scaffold/channel.ts +0 -25
  376. package/dist/cli/commands/attach.d.ts +0 -108
  377. package/dist/cli/commands/attach.js +0 -637
  378. package/dist/cli/commands/fire.d.ts +0 -7
  379. package/dist/cli/commands/fire.js +0 -38
  380. package/dist/cli/commands/schedule.d.ts +0 -12
  381. package/dist/cli/commands/schedule.js +0 -87
  382. package/dist/github.d.ts +0 -3
  383. package/dist/github.js +0 -2
  384. package/dist/scaffold/templates/fastagent.config.mjs +0 -23
  385. package/dist/schedule/audit.d.ts +0 -18
  386. package/dist/schedule/audit.js +0 -57
  387. package/dist/schedule/schedule.d.ts +0 -35
  388. package/dist/schedule/schedule.js +0 -23
@@ -8,7 +8,11 @@ import { secretEquals } from "../secret.js";
8
8
  import { createSeenRing } from "../kit/seen.js";
9
9
  import { signatureIsFresh } from "../kit/signature.js";
10
10
  import { createThreadParticipants } from "../kit/thread-participants.js";
11
+ import * as Effect from "effect/Effect";
12
+ import * as Exit from "effect/Exit";
13
+ import { portJoin } from "../../effect-port.js";
11
14
  import { createTaskTracker } from "../kit/tasks.js";
15
+ import { attachmentsDir } from "../kit/attachment-path.js";
12
16
  import { ensureStateHome } from "../kit/state.js";
13
17
  import { dispatchStop, isStopText } from "../kit/stop-command.js";
14
18
  import { codePointPrefix } from "../kit/text.js";
@@ -16,17 +20,18 @@ import { createTurnRunner } from "../kit/turn-runner.js";
16
20
  import { createTurnStore } from "../kit/turn-store.js";
17
21
  import { discussionBlock } from "../kit/context-buffer.js";
18
22
  import { collectSlackBufferedFiles, createSlackContextBuffer } from "./context-buffer.js";
19
- import { invokeSlackTurn } from "./invoke-turn.js";
23
+ import { slackTurnStream } from "./invoke-turn.js";
20
24
  import { defaultSlackRoute, isSlackDirectMessage, isSlackGroupMessage, hasSlackMention, hasSlackUserMention, isSlackHumanMessage, mentionsSlackUser, stripSlackMentions, slackBufferText, slackEnvelope, slackFileIds, slackMessageText, slackPlaceKey, slackSenderLabel, slackTeamId, } from "./parse.js";
21
- import { defaultErrorMessage, settleSlackPreview, streamSlackReply, } from "./preview.js";
22
- import { resolveReactionEmojis, startSlackReaction } from "./reaction.js";
25
+ import { defaultErrorMessage, deliverSlackAnswer, settleSlackPreview, slackReply, } from "./preview.js";
26
+ import { completeSlackReaction, resolveReactionEmojis, startSlackReaction } from "./reaction.js";
23
27
  import { registerSlackApi } from "./shared-api.js";
24
28
  import { createSlackApi } from "./slack-api.js";
25
29
  import { createWelcomedUsers } from "./welcomed.js";
26
30
  export { defaultSlackRoute, slackEnvelope };
27
31
  const MAX_EVENT_BYTES = 1 << 20;
28
- /** Slack's own documented window — it re-signs every redelivery with a current timestamp, so a tight
29
- * one costs nothing. The Feishu ingress reads hours off the same helper for the opposite reason. */
32
+ /**
33
+ * Slack's own documented window — it re-signs every redelivery with a current timestamp, so a tight one costs nothing.
34
+ */
30
35
  const MAX_SIGNATURE_AGE_S = 5 * 60;
31
36
  const QUEUED_PLACEHOLDER = "⏳ Queued — I’ll start once the current task finishes.";
32
37
  const DEFERRED_PLACEHOLDER = "⏳ Delayed by a temporary system issue — I’ll retry automatically.";
@@ -56,12 +61,50 @@ export function verifySlackSignature(signingSecret, timestamp, signature, rawBod
56
61
  const expected = `v0=${createHmac("sha256", signingSecret).update(`v0:${timestamp}:${rawBody}`).digest("hex")}`;
57
62
  return secretEquals(signature, expected);
58
63
  }
59
- /** The (channel, ts) of the user's triggering message, encoded in the logical turn id `team:channel:ts`
60
- * (Slack ids never contain a colon, so a 3-part split is exact). Used to place the reaction ack. */
64
+ /**
65
+ * The (channel, ts) of the user's triggering message, encoded in the logical turn id `team:channel:ts` (Slack ids
66
+ * never contain a colon, so a 3-part split is exact).
67
+ */
61
68
  function messageRefOf(turnId) {
62
69
  const parts = turnId.split(":");
63
70
  return parts.length === 3 && parts[1] && parts[2] ? { channelId: parts[1], ts: parts[2] } : undefined;
64
71
  }
72
+ /** How long an ingress may wait for `auth.test`: the Events API allows 3s to ACK, so this must fail well inside it. */
73
+ const AUTH_ACK_BUDGET_MS = 2_000;
74
+ function createSlackAuthentication(api, label) {
75
+ let teamId;
76
+ let botUserId;
77
+ let state = "pending";
78
+ const settled = api.authTest().then((identity) => {
79
+ teamId = identity.teamId;
80
+ botUserId = identity.userId;
81
+ state = "ready";
82
+ log.info(`${label} authenticated${identity.teamId ? ` for workspace ${identity.teamId}` : ""}`);
83
+ }, (error) => {
84
+ state = "failed";
85
+ throw new Error(`Slack auth.test failed — fix SLACK_BOT_TOKEN before accepting events: ${String(error)}`, {
86
+ cause: error,
87
+ });
88
+ });
89
+ void settled.catch((error) => log.error(`${label} ${String(error)}`));
90
+ return {
91
+ settled,
92
+ teamId: () => teamId,
93
+ botUserId: () => botUserId,
94
+ ready: () => state !== "pending"
95
+ ? settled
96
+ : new Promise((resolve, reject) => {
97
+ const timeout = setTimeout(() => reject(new Error("Slack auth.test did not finish inside the Events API ACK budget")), AUTH_ACK_BUDGET_MS);
98
+ void settled.then(() => {
99
+ clearTimeout(timeout);
100
+ resolve();
101
+ }, (error) => {
102
+ clearTimeout(timeout);
103
+ reject(error);
104
+ });
105
+ }),
106
+ };
107
+ }
65
108
  export function slackChannel(options) {
66
109
  const { botToken, signingSecret, rendering = "native", aiDisclaimer, welcome = DEFAULT_WELCOME, reactionAck = {}, route, onError, apiBaseUrl = "https://slack.com/api", } = options;
67
110
  if (!["native", "classic"].includes(rendering)) {
@@ -84,46 +127,15 @@ export function slackChannel(options) {
84
127
  ensureStateHome(stateHome);
85
128
  const api = createSlackApi({ botToken, baseUrl: apiBaseUrl });
86
129
  registerSlackApi(stateRoot, api); // the send tool delivers through this one (shared-api.ts)
87
- let authenticatedTeamId;
88
- let botUserId;
89
- let authenticationState = "pending";
90
- const authentication = api.authTest().then((identity) => {
91
- authenticatedTeamId = identity.teamId;
92
- botUserId = identity.userId;
93
- authenticationState = "ready";
94
- log.info(`${label} authenticated${identity.teamId ? ` for workspace ${identity.teamId}` : ""}`);
95
- }, (error) => {
96
- authenticationState = "failed";
97
- throw new Error(`Slack auth.test failed — fix SLACK_BOT_TOKEN before accepting events: ${String(error)}`, {
98
- cause: error,
99
- });
100
- });
101
- // Construction cannot await Web API IO. Keep the rejection observed; recovered turns and ingress
102
- // both gate on the same promise/state below and surface the failure without ACKing new work.
103
- void authentication.catch((error) => log.error(`${label} ${String(error)}`));
104
- const waitForAuthentication = () => {
105
- if (authenticationState !== "pending")
106
- return authentication;
107
- return new Promise((resolve, reject) => {
108
- const timeout = setTimeout(() => reject(new Error("Slack auth.test did not finish inside the Events API ACK budget")), 2_000);
109
- void authentication.then(() => {
110
- clearTimeout(timeout);
111
- resolve();
112
- }, (error) => {
113
- clearTimeout(timeout);
114
- reject(error);
115
- });
116
- });
117
- };
118
- // Side tasks (stop feedback, DM welcomes) run off the ACK path but drain in turnsIdle. Declared
119
- // ahead of its consumers so no construction-time path can reach it in the temporal dead zone.
130
+ const auth = createSlackAuthentication(api, label);
131
+ // Side tasks (stop feedback, DM welcomes) run off the ACK path but drain in turnsIdle.
120
132
  const sideTasks = createTaskTracker(label);
121
133
  const seen = createSeenRing(join(stateHome, "seen.json"), label);
122
134
  const threadParticipants = createThreadParticipants(join(stateHome, "thread-participants.json"), label);
123
- /** A thread's participation is keyed by the SESSION it describes — "the agent answered here" is a
124
- * claim about a memory, so the two must not be re-keyable independently. What keeps the claim true
125
- * under a custom route is the `routed.session === undefined` condition on the write, not the
126
- * absence of a route: a route that supplies its own session records nothing here. */
135
+ /**
136
+ * A thread's participation is keyed by the SESSION it describes — "the agent answered here" is a claim about a
137
+ * memory, so the two must not be re-keyable independently.
138
+ */
127
139
  const threadKey = (teamId, channelId, threadTs) => `slack:${teamId}:${channelId}:${threadTs}`;
128
140
  const welcomed = createWelcomedUsers(join(stateHome, "welcomed.json"), label);
129
141
  const buffer = createSlackContextBuffer(join(stateHome, "buffers.json"), label);
@@ -159,7 +171,7 @@ export function slackChannel(options) {
159
171
  if (nativeDmStatus)
160
172
  turn.nativeQueueStatus = true;
161
173
  return {
162
- done: authentication
174
+ done: auth.settled
163
175
  .then(() => nativeDmStatus
164
176
  ? api.setThreadStatus(targetOf(turn), "is queued behind an earlier request…")
165
177
  : api.postMessage(targetOf(turn), QUEUED_PLACEHOLDER).then((ts) => {
@@ -168,12 +180,10 @@ export function slackChannel(options) {
168
180
  .catch((error) => log.warn(`${label} queue preview failed (the turn stays durable): ${String(error)}`)),
169
181
  };
170
182
  },
171
- // Leave the intent untouched when Slack auth failed. A fixed token + restart replays it;
172
- // running now would execute an Agent turn whose only customer-facing transport is known to be
173
- // unavailable.
183
+ // Leave the intent untouched when Slack auth failed.
174
184
  beforeRun: async (turn) => {
175
185
  try {
176
- await authentication;
186
+ await auth.settled;
177
187
  return true;
178
188
  }
179
189
  catch (error) {
@@ -192,47 +202,67 @@ export function slackChannel(options) {
192
202
  }
193
203
  },
194
204
  notifyDropped,
195
- execute: async (turn, discussion, onCompleted) => {
205
+ deliverAnswer: (turn, answer) => portJoin(async () => {
206
+ // A queue notice THIS process put up outlives the run the answer came from, so it is settled here as it
207
+ // would be by a turn that ran: the status line cleared, the compatibility message taken over.
208
+ if (turn.nativeQueueStatus) {
209
+ await api
210
+ .setThreadStatus(targetOf(turn), "")
211
+ .catch((error) => log.warn(`${label} could not clear a queued Agent status: ${String(error)}`));
212
+ }
213
+ await deliverSlackAnswer(api, targetOf(turn), answer, turn.previewTs);
214
+ // The 👀 on the asker's message was added by the run that produced this answer, and `execute`'s
215
+ // acquire/release — the only thing that turns it into ✅ — is not on this path.
196
216
  const messageRef = messageRefOf(turn.id);
197
- const reaction = reactionEmojis && messageRef
198
- ? await startSlackReaction({
217
+ if (reactionEmojis && messageRef) {
218
+ await completeSlackReaction({
199
219
  api,
200
220
  channelId: messageRef.channelId,
201
221
  ts: messageRef.ts,
202
222
  emojis: reactionEmojis,
203
223
  label,
204
- })
205
- : undefined;
206
- try {
207
- await streamSlackReply(invokeSlackTurn(agent, turn.session, `${discussionBlock(discussion.text)}${turn.baseText}`, { api, channelId: turn.channelId, filesDir: join(stateHome, "files"), label }, {
208
- primaryFileIds: turn.fileIds,
209
- buffered: collectSlackBufferedFiles(discussion.consumed, new Set(turn.fileIds)),
210
- }, onCompleted), api, targetOf(turn), formatError, {
211
- rendering,
212
- initialPreviewTs: turn.previewTs,
213
- threadTitle: turn.threadTitle,
214
- disclaimer: aiDisclaimer,
215
- label,
216
224
  });
217
225
  }
218
- catch (error) {
219
- await reaction?.remove();
220
- throw error;
221
- }
222
- await reaction?.complete();
226
+ }),
227
+ execute: (turn, discussion, onAnswered) => {
228
+ const messageRef = messageRefOf(turn.id);
229
+ return Effect.acquireUseRelease(portJoin(async () => reactionEmojis && messageRef
230
+ ? startSlackReaction({
231
+ api,
232
+ channelId: messageRef.channelId,
233
+ ts: messageRef.ts,
234
+ emojis: reactionEmojis,
235
+ label,
236
+ })
237
+ : undefined), () => Effect.scoped(slackReply(slackTurnStream(agent, turn.session, `${discussionBlock(discussion.text)}${turn.baseText}`, { api, channelId: turn.channelId, filesDir: attachmentsDir(stateHome), label }, {
238
+ primaryFileIds: turn.fileIds,
239
+ buffered: collectSlackBufferedFiles(discussion.consumed, new Set(turn.fileIds)),
240
+ }), api, targetOf(turn), formatError, {
241
+ rendering,
242
+ initialPreviewTs: turn.previewTs,
243
+ threadTitle: turn.threadTitle,
244
+ disclaimer: aiDisclaimer,
245
+ label,
246
+ onAnswered,
247
+ })), (reaction, exit) => portJoin(async () => {
248
+ if (Exit.isSuccess(exit))
249
+ await reaction?.complete();
250
+ else
251
+ await reaction?.remove();
252
+ }).pipe(Effect.orDie));
223
253
  },
224
254
  });
225
255
  let seq = runner.recover().reduce((maximum, turn) => Math.max(maximum, turn.seq), 0);
226
- // Acceptance touches no network, so it stays synchronous inside Slack's ACK window and the
227
- // delivery dedup ring alone is enough — there is no await for a duplicate delivery to race
228
- // through. The minutes-long Agent turn remains fire-and-forget.
256
+ // Acceptance touches no network, so it stays synchronous inside Slack's ACK window and the delivery dedup ring
257
+ // alone is enough.
229
258
  const acceptEvent = (envelope) => {
230
259
  const event = envelope.event;
231
260
  if (!isSlackHumanMessage(event))
232
261
  return;
262
+ const botUserId = auth.botUserId();
233
263
  if (botUserId && event.user === botUserId)
234
264
  return;
235
- const teamId = slackTeamId(envelope) ?? authenticatedTeamId;
265
+ const teamId = slackTeamId(envelope) ?? auth.teamId();
236
266
  if (!teamId) {
237
267
  log.warn(`${label} ignored message ${event.ts}: event carried no workspace/enterprise identity`);
238
268
  return;
@@ -246,37 +276,28 @@ export function slackChannel(options) {
246
276
  const direct = isSlackDirectMessage(event);
247
277
  const rootTs = event.thread_ts ?? event.ts;
248
278
  const bufferKey = slackPlaceKey(teamId, event);
249
- // Listening is not speaking: every message the channel can see refines who takes part in its
250
- // thread, whether or not it is answered. Humans only — a bot's own posts are recorded where they
251
- // are known — this channel answering.
252
- // Structural facts only, never `route` — see thread-participants.ts. Slack
253
- // adds no delta of its own here; its summon rule is the only consumer.
279
+ // Listening is not speaking: every message the channel can see refines who takes part in its thread, whether or
280
+ // not it is answered.
254
281
  if (group && event.thread_ts !== undefined) {
255
282
  threadParticipants.merge(threadKey(teamId, event.channel, event.thread_ts), { humans: [event.user] });
256
283
  }
257
284
  let routed = decide(envelope);
258
- // Two different questions (see parse.ts). `addressesSomeone` is the "@-mentions only other people
259
- // is discussion, never an ask" guard (§3) and counts broadcasts and user groups too;
260
- // `mightBeTheBot` gates the identity-window deferral below, where only a USER mention qualifies.
285
+ // Two different questions (see parse.ts).
261
286
  const addressesSomeone = hasSlackMention(event.text ?? "");
262
287
  const mightBeTheBot = hasSlackUserMention(event.text ?? "");
263
288
  const structurallyMentionsBot = botUserId !== undefined && mentionsSlackUser(event.text ?? "", botUserId);
264
- // app_mention and message.* subscriptions can overlap. If message.* arrives first, structural bot
265
- // identity routes it now; while auth.test is still unresolved, defer any mentioned message rather
266
- // than buffer+dedup it and accidentally suppress the later app_mention callback.
289
+ // app_mention and message.* subscriptions can overlap.
267
290
  if (!routed && route === undefined && group && event.type === "message" && structurallyMentionsBot)
268
291
  routed = {};
269
- // The participant model's thread rule (§3): a bare message reaches the agent while it takes part
270
- // and has not heard a second human. Note the ORDER — the mention guard below runs first and is
271
- // structural: it reads who THIS message addresses, which is the one thing an observation-only
272
- // store cannot know about someone it has never heard.
292
+ // The participant model's thread rule (§3): a bare message reaches the agent while it takes part and has not
293
+ // heard a second human.
273
294
  if (!routed &&
274
295
  route === undefined &&
275
296
  group &&
276
297
  event.thread_ts !== undefined &&
277
298
  event.type !== "app_mention" &&
278
- // Mentioning only other people is targeted discussion, never an ask (§3) — the same guard
279
- // Feishu applies with `hasMentions`.
299
+ // Mentioning only other people is targeted discussion, never an ask (§3) — the same guard Feishu applies with
300
+ // `hasMentions`.
280
301
  !addressesSomeone &&
281
302
  threadParticipants.admitsBareMessage(threadKey(teamId, event.channel, event.thread_ts))) {
282
303
  routed = {};
@@ -303,16 +324,11 @@ export function slackChannel(options) {
303
324
  }
304
325
  const targetChannel = routed.channelId ?? event.channel;
305
326
  const sameChannel = targetChannel === event.channel;
306
- // Answer where asked (participant model §4). Slack has no quote primitive, so the only way to
307
- // attach an answer to its question is a thread on it — which then IS the place, and carries the
308
- // memory (§5). Native streaming additionally REQUIRES a thread, but the shape does not depend on
309
- // the renderer: `classic` attaches its answer the same way.
327
+ // Answer where asked (participant model §4).
310
328
  const defaultThread = event.thread_ts ?? event.ts;
311
329
  const threadTs = routed.threadTs === null ? undefined : (routed.threadTs ?? (sameChannel ? defaultThread : undefined));
312
330
  const defaultSession = threadKey(teamId, event.channel, rootTs);
313
- // Explicit user stop: a control action, never a turn — it must not queue behind the run it
314
- // stops. Match the bare word after stripping the bot mention; record the logical id so a Slack
315
- // redelivery doesn't double-abort or double-notify.
331
+ // Explicit user stop: a control action, never a turn — it must not queue behind the run it stops.
316
332
  if (isStopText(stripSlackMentions(event.text ?? ""))) {
317
333
  seen.add(logicalId);
318
334
  const target = { channelId: event.channel, threadTs: event.thread_ts };
@@ -341,32 +357,15 @@ export function slackChannel(options) {
341
357
  threadTitle: threadTitle || undefined,
342
358
  fileIds,
343
359
  }, true);
344
- // Answering inside a GROUP thread makes the agent a participant of it, which is what lets the
345
- // NEXT bare message address it without a mention.
346
- //
347
- // `group` excludes DMs, whose `threadTs` is always defined (the answer opens its assistant
348
- // thread) and which no rule could ever read — safe because it is structural: a channel never
349
- // becomes a DM.
350
- //
351
- // The two `routed` conditions keep the record describing what it claims. `session` undefined: the
352
- // flag asserts "the agent answered into THIS thread's session", so a route supplying its own
353
- // would record participation in a memory that never held the turn. `threadTs` undefined: a route
354
- // can send the answer to a DIFFERENT thread, where the asker never spoke — recording them there
355
- // would invent a participant. Feishu carries the same two conditions for the same reasons.
356
- //
357
- // Recorded only once the intent is durable: `submit` can throw, and a redelivery must still see
358
- // the thread as the agent has actually left it.
360
+ // Answering inside a GROUP thread makes the agent a participant of it, which is what lets the NEXT bare message
361
+ // address it without a mention.
359
362
  if (group &&
360
363
  threadTs !== undefined &&
361
364
  sameChannel &&
362
365
  routed.session === undefined &&
363
366
  routed.threadTs === undefined) {
364
- // The ASKER counts as heard in this thread too, and both halves are written together so the
365
- // record can never say "the agent takes part and nobody has spoken". When the ask is top
366
- // level, the answer is what CREATES the thread, so the observation above never ran for it (no
367
- // `thread_ts` on the ask) — without this, a thread whose root is a human would not count them,
368
- // and a stranger's first bare reply would read as a two-party exchange. `event.user` is
369
- // guaranteed here (isSlackHumanMessage). Idempotent when the ask was already in the thread.
367
+ // The ASKER counts as heard in this thread too, and both halves are written together so the record can never
368
+ // say "the agent takes part and nobody has spoken".
370
369
  threadParticipants.merge(threadKey(teamId, event.channel, threadTs), {
371
370
  agentSpoke: true,
372
371
  humans: [event.user],
@@ -390,14 +389,14 @@ export function slackChannel(options) {
390
389
  const channelId = event.channel;
391
390
  if (!userId || !channelId)
392
391
  return;
393
- const teamId = slackTeamId(envelope) ?? authenticatedTeamId;
392
+ const teamId = slackTeamId(envelope) ?? auth.teamId();
394
393
  if (!teamId)
395
394
  return;
396
395
  const id = `${teamId}:${userId}`;
397
396
  if (welcomed.has(teamId, userId) || welcomeInFlight.has(id))
398
397
  return;
399
- // Reserve in-memory so rapid re-opens don't double-post; persist to the durable set only on a
400
- // successful post, so a failed post simply retries on the next open.
398
+ // Reserve in-memory so rapid re-opens don't double-post; persist to the durable set only on a successful post,
399
+ // so a failed post simply retries on the next open.
401
400
  welcomeInFlight.add(id);
402
401
  sideTasks.track(api
403
402
  .postMarkdown({ channelId }, body)
@@ -437,7 +436,7 @@ export function slackChannel(options) {
437
436
  if (envelope.type !== "event_callback")
438
437
  return new Response(null, { status: 200 });
439
438
  try {
440
- await waitForAuthentication();
439
+ await auth.ready();
441
440
  }
442
441
  catch (error) {
443
442
  log.error(`${label} refusing to ACK an event because Slack authentication is unavailable: ${String(error)}`);
@@ -1,4 +1,4 @@
1
- /** Shared SSE response lifecycle for invoke and session observation. Fetch-only, with no Node dependencies. */
1
+ /** Shared SSE response lifecycle for invoke and session observation. */
2
2
  /** The remote client's idle watchdog allows three missed heartbeats. */
3
3
  export declare const SSE_HEARTBEAT_MS = 30000;
4
4
  export declare function sseResponse<T>(events: AsyncIterable<T>, project?: (event: T) => unknown): Response;
@@ -1,4 +1,4 @@
1
- /** Shared SSE response lifecycle for invoke and session observation. Fetch-only, with no Node dependencies. */
1
+ /** Shared SSE response lifecycle for invoke and session observation. */
2
2
  /** The remote client's idle watchdog allows three missed heartbeats. */
3
3
  export const SSE_HEARTBEAT_MS = 30_000;
4
4
  const encoder = new TextEncoder();
@@ -1,15 +1,6 @@
1
- /**
2
- * Telegram's half of the shared context buffer (mechanics + consume protocol: ../kit/context-buffer.ts):
3
- * the entry shape, its fold-line rendering, and buffered-attachment selection. Bucketed by Telegram
4
- * "place" (chat[:thread]), not session: an un-summoned message has no route session, and the flush
5
- * feeds whatever turn answers that place.
6
- */
1
+ /** Telegram's half of the shared context buffer (mechanics + consume protocol: ../kit/context-buffer.ts). */
7
2
  import { type ContextBuffer as GenericContextBuffer } from "../kit/context-buffer.ts";
8
- /** One buffered un-summoned message (object identity is the commit key). Besides the sender label and
9
- * one-line body, it carries what a LATER summon needs to resolve references into the discussion:
10
- * message ids ("reply to the one Alex answered"), and attachment file_ids so "summarize the file from
11
- * earlier" can actually open it — an un-summoned attachment otherwise surfaces only as its caption or
12
- * a `[document: …]` label, never the bytes. */
3
+ /** One buffered un-summoned message (object identity is the commit key). */
13
4
  export interface BufferEntry {
14
5
  sender: string;
15
6
  body: string;
@@ -22,22 +13,16 @@ export interface BufferEntry {
22
13
  /** file_ids of photos (usable as vision inputs on a later summon). */
23
14
  imageIds?: string[];
24
15
  }
25
- /** A buffered attachment reference: its file_id plus WHO posted it in WHICH message, so the manifest
26
- * can attribute it ("the file Bob sent") the way the fold attributes text. */
16
+ /**
17
+ * A buffered attachment reference: its file_id plus WHO posted it in WHICH message, so the manifest can attribute it
18
+ * ("the file Bob sent") the way the fold attributes text.
19
+ */
27
20
  export interface BufferedRef {
28
21
  id: string;
29
22
  from: string;
30
23
  msg?: number;
31
24
  }
32
- /**
33
- * The buffered attachment references a summoned turn pulls in with the fold — most recent
34
- * BUFFER_ATTACH_MAX of each kind, MINUS the summoning message's own ids: replying to a still-buffered
35
- * attachment puts its file_id in both sets, and without the filter the same file would download twice
36
- * and appear in the manifest twice (primary wins — it is what the user pointed at this turn).
37
- * Cap-skipped ones are COUNTED, not silently dropped: a fold line may show a [document: …] label, but
38
- * a captioned attachment renders as its caption text alone — without a note, the model holds
39
- * references it silently cannot open and may pretend it read them.
40
- */
25
+ /** The buffered attachment references a summoned turn pulls in with the fold. */
41
26
  export declare function collectAttachments(consumed: BufferEntry[], primary: {
42
27
  files: Set<string>;
43
28
  images: Set<string>;
@@ -1,10 +1,5 @@
1
- /**
2
- * Telegram's half of the shared context buffer (mechanics + consume protocol: ../kit/context-buffer.ts):
3
- * the entry shape, its fold-line rendering, and buffered-attachment selection. Bucketed by Telegram
4
- * "place" (chat[:thread]), not session: an un-summoned message has no route session, and the flush
5
- * feeds whatever turn answers that place.
6
- */
7
- import { BUFFER_ATTACH_MAX, createContextBuffer as createGenericContextBuffer, } from "../kit/context-buffer.js";
1
+ /** Telegram's half of the shared context buffer (mechanics + consume protocol: ../kit/context-buffer.ts). */
2
+ import { capBufferedRefs, createContextBuffer as createGenericContextBuffer, } from "../kit/context-buffer.js";
8
3
  function bufferLine(e) {
9
4
  const meta = [
10
5
  e.messageId !== undefined ? `msg ${e.messageId}` : undefined,
@@ -14,15 +9,7 @@ function bufferLine(e) {
14
9
  .join(", ");
15
10
  return `${e.sender}${meta ? ` (${meta})` : ""}: ${e.body}`;
16
11
  }
17
- /**
18
- * The buffered attachment references a summoned turn pulls in with the fold — most recent
19
- * BUFFER_ATTACH_MAX of each kind, MINUS the summoning message's own ids: replying to a still-buffered
20
- * attachment puts its file_id in both sets, and without the filter the same file would download twice
21
- * and appear in the manifest twice (primary wins — it is what the user pointed at this turn).
22
- * Cap-skipped ones are COUNTED, not silently dropped: a fold line may show a [document: …] label, but
23
- * a captioned attachment renders as its caption text alone — without a note, the model holds
24
- * references it silently cannot open and may pretend it read them.
25
- */
12
+ /** The buffered attachment references a summoned turn pulls in with the fold. */
26
13
  export function collectAttachments(consumed, primary) {
27
14
  const refs = (pick, exclude) => {
28
15
  const seen = new Set();
@@ -37,13 +24,9 @@ export function collectAttachments(consumed, primary) {
37
24
  }
38
25
  return out;
39
26
  };
40
- const files = refs((e) => e.fileIds, primary.files);
41
- const images = refs((e) => e.imageIds, primary.images);
42
- return {
43
- files: files.slice(-BUFFER_ATTACH_MAX),
44
- images: images.slice(-BUFFER_ATTACH_MAX),
45
- skipped: Math.max(0, files.length - BUFFER_ATTACH_MAX) + Math.max(0, images.length - BUFFER_ATTACH_MAX),
46
- };
27
+ const files = capBufferedRefs(refs((e) => e.fileIds, primary.files));
28
+ const images = capBufferedRefs(refs((e) => e.imageIds, primary.images));
29
+ return { files: files.kept, images: images.kept, skipped: files.skipped + images.skipped };
47
30
  }
48
31
  function isBufferEntry(e) {
49
32
  const t = e;
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Run one turn (the IO half of Telegram→Agent translation): assemble its inputs — resolve attachments
3
3
  * (download files to disk, load vision images) — and stream `agent.invoke` with the assembled prompt.
4
- * `invokeTurn` is the export; attachment resolution is an internal step. Split from parse.ts (which is
5
- * pure) because this half touches the Bot API + disk; split from telegram.ts so the factory keeps only
6
- * wiring and the per-turn lifecycle.
4
+ * Split from parse.ts because this half touches the Bot API + disk; split from telegram.ts so the
5
+ * factory keeps only wiring and the per-turn lifecycle.
7
6
  */
8
7
  import type { Agent, AgentEvent } from "../../agent.ts";
8
+ import type * as Stream from "effect/Stream";
9
+ import type { PortFailure } from "../../effect-port.ts";
9
10
  import { type BusyRetry } from "../kit/invoke-turn-kit.ts";
10
11
  import type { BufferedRef } from "./context-buffer.ts";
11
12
  /** Everything the transport needs to fetch a turn's attachments. */
@@ -29,8 +30,7 @@ export interface TurnAttachments {
29
30
  };
30
31
  }
31
32
  /**
32
- * Run one turn: resolve its attachments, then stream agent.invoke with the shared busy-wait
33
- * (invoke-turn-kit — `onCompleted` is the durable-commit point; see streamTurnWithBusyRetry). A
34
- * primary-attachment failure surfaces as a `failed` event (never a silent drop).
33
+ * Run one turn: resolve its attachments, then ask the agent (invoke-turn-kit — a primary-attachment
34
+ * failure surfaces as a `failed` event, never a silent drop).
35
35
  */
36
- export declare function invokeTurn(agent: Agent, session: string, text: string, transport: TurnTransport, attachments: TurnAttachments, onCompleted?: () => void, busyRetry?: BusyRetry): AsyncIterable<AgentEvent>;
36
+ export declare function telegramTurnStream(agent: Agent, session: string, text: string, transport: TurnTransport, attachments: TurnAttachments, busyRetry?: BusyRetry): Stream.Stream<AgentEvent, PortFailure>;
@@ -1,5 +1,4 @@
1
- import { log } from "../../log.js";
2
- import { DEFAULT_BUSY_RETRY, attachedFilesManifest, attributedFileName, missingAttachmentsNote, streamTurnWithBusyRetry, } from "../kit/invoke-turn-kit.js";
1
+ import { DEFAULT_BUSY_RETRY, attachedFilesManifest, attributedFileName, loadBackground, missingAttachmentsNote, turnStream, } from "../kit/invoke-turn-kit.js";
3
2
  import { resolveFiles, resolveImages } from "./telegram-api.js";
4
3
  /** Appended to the prompt (not the system prompt): the channel owns Telegram-HTML formatting. */
5
4
  const HTML_INSTRUCTION = "\n\n(Format your reply in Telegram-supported HTML — <b> <i> <u> <s> <code> <pre> <a href> — not Markdown.)";
@@ -19,33 +18,14 @@ async function resolveTurnAttachments(t, attachments) {
19
18
  const { primary, buffered } = attachments;
20
19
  const images = await resolveImages(api, botToken, primary.imageFileIds);
21
20
  const files = await resolveFiles(api, botToken, primary.fileIds, chatId, filesDir);
22
- const bufferedImages = [];
23
- const bufferedFiles = [];
24
- let lost = 0;
25
- const imageResults = await Promise.allSettled(buffered.images.map((ref) => resolveImages(api, botToken, [ref.id])));
26
- for (const r of imageResults) {
27
- if (r.status === "fulfilled")
28
- bufferedImages.push(...(r.value ?? []));
29
- else {
30
- lost++;
31
- log.warn(`[telegram] could not load an earlier (buffered) photo: ${String(r.reason)}`);
32
- }
33
- }
34
- const fileResults = await Promise.allSettled(buffered.files.map(async (ref) => ({
35
- ref,
36
- files: (await resolveFiles(api, botToken, [ref.id], chatId, filesDir)) ?? [],
37
- })));
38
- for (const r of fileResults) {
39
- if (r.status === "fulfilled") {
40
- for (const file of r.value.files)
41
- bufferedFiles.push({ file, ref: r.value.ref });
42
- }
43
- else {
44
- lost++;
45
- log.warn(`[telegram] could not load an earlier (buffered) attachment: ${String(r.reason)}`);
46
- }
47
- }
48
- const missingNote = missingAttachmentsNote(lost + buffered.skipped);
21
+ const backgroundImages = await loadBackground(buffered.images, (ref) => resolveImages(api, botToken, [ref.id]), {
22
+ label: "[telegram]",
23
+ what: "photo",
24
+ });
25
+ const backgroundFiles = await loadBackground(buffered.files, (ref) => resolveFiles(api, botToken, [ref.id], chatId, filesDir), { label: "[telegram]", what: "attachment" });
26
+ const bufferedImages = backgroundImages.loaded.flatMap(({ value }) => value ?? []);
27
+ const bufferedFiles = backgroundFiles.loaded.flatMap(({ ref, value }) => (value ?? []).map((file) => ({ file, ref })));
28
+ const missingNote = missingAttachmentsNote(backgroundImages.lost + backgroundFiles.lost + buffered.skipped);
49
29
  // PRIMARY first, background after — consistent with "primary wins": what the user pointed at this
50
30
  // turn leads. Buffered file entries are attributed like the fold's text lines ("the file Bob sent"
51
31
  // resolves); buffered PHOTOS cannot be (ImageRef carries no label), so their attribution stops at
@@ -62,19 +42,20 @@ async function resolveTurnAttachments(t, attachments) {
62
42
  };
63
43
  }
64
44
  /**
65
- * Run one turn: resolve its attachments, then stream agent.invoke with the shared busy-wait
66
- * (invoke-turn-kit — `onCompleted` is the durable-commit point; see streamTurnWithBusyRetry). A
67
- * primary-attachment failure surfaces as a `failed` event (never a silent drop).
45
+ * Run one turn: resolve its attachments, then ask the agent (invoke-turn-kit — a primary-attachment
46
+ * failure surfaces as a `failed` event, never a silent drop).
68
47
  */
69
- export async function* invokeTurn(agent, session, text, transport, attachments, onCompleted, busyRetry = DEFAULT_BUSY_RETRY) {
70
- let resolved;
71
- try {
72
- resolved = await resolveTurnAttachments(transport, attachments);
73
- }
74
- catch (e) {
75
- yield { type: "failed", details: `could not load attachment: ${String(e)}`, retryable: true };
76
- return;
77
- }
78
- const prompt = { text: `${text}${resolved.promptSuffix}${HTML_INSTRUCTION}`, images: resolved.images };
79
- yield* streamTurnWithBusyRetry(agent, { session }, prompt, { label: "[telegram]", onCompleted, busyRetry });
48
+ export function telegramTurnStream(agent, session, text, transport, attachments, busyRetry = DEFAULT_BUSY_RETRY) {
49
+ return turnStream({
50
+ agent,
51
+ label: "[telegram]",
52
+ busyRetry,
53
+ resolve: () => resolveTurnAttachments(transport, attachments),
54
+ turn: (resolved) => ({
55
+ scope: { session },
56
+ prompt: { text: `${text}${resolved.promptSuffix}${HTML_INSTRUCTION}`, images: resolved.images },
57
+ }),
58
+ // A Bot API download that failed may simply succeed on the redelivery.
59
+ retryableLoadFailure: () => true,
60
+ });
80
61
  }