@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
@@ -1,29 +1,11 @@
1
1
  /**
2
- * Telegram webhook registration — the TELEGRAM-domain step both `--tunnel` (dev, tunnel.ts) and
3
- * `deploy … --run` (the host runners' post-deploy step) invoke. What "registering telegram" means lives
4
- * here, beside the channel it serves; it reads the same .env tokens the channel uses.
5
- *
6
- * READINESS IS setWebhook'S OWN VERDICT, not a local probe. Telegram VERIFIES the URL during the call,
7
- * from Telegram's network — which is the only network that has to reach it. This used to poll
8
- * `<baseUrl>/health` from here first, and that probe answered a different question badly: a freshly
9
- * minted hostname (a quick tunnel, a fresh deploy) is routinely unreachable from the machine running
10
- * the CLI for a minute or more — behind a proxy, or while its own resolver still says ENOTFOUND — long
11
- * after Telegram can reach it (#421). So the retry loop below IS the wait: Telegram's reachability
12
- * verdicts ("Failed to resolve host", "Wrong response from the webhook") are retried, and only a
13
- * configuration error the operator must fix is reported once.
2
+ * Telegram webhook registration — the TELEGRAM-domain step both `--tunnel` (dev, tunnel.ts) and `deploy … --run` (the
3
+ * host runners' post-deploy step) invoke.
14
4
  */
15
5
  import { log } from "../../log.js";
16
6
  import { retryWhile } from "../registration.js";
17
7
  import { callApi } from "./telegram-api.js";
18
- /**
19
- * Whether a setWebhook failure is the URL still warming up rather than a configuration error. Two
20
- * families: transport failures reaching Telegram at all, and Telegram's own verdict after it fails to
21
- * reach the webhook — which carries its connection layer's words verbatim (`Bad Request: bad webhook:
22
- * Connection timed out` / `Connection refused` for a host whose DNS answers before anything listens,
23
- * `Wrong response from the webhook: 530` for a tunnel edge without its origin). This is the ONLY gate
24
- * between "spend the caller's retry budget" and "fail now", so it is wide on the reachability side.
25
- * A permanent "bad webhook" — an http:// URL, an unsupported port — matches nothing here and is reported.
26
- */
8
+ /** Whether a setWebhook failure is the URL still warming up rather than a configuration error. */
27
9
  function isTransientRegistrationError(error) {
28
10
  return /resolve host|getaddrinfo|ENOTFOUND|fetch failed|ECONNRESET|timeout|timed out|connection refused|can't connect|connection to the host|wrong response from the webhook/i.test(error);
29
11
  }
@@ -54,8 +36,8 @@ export async function registerTelegramWebhook(baseUrl, opts = {}) {
54
36
  return "registered";
55
37
  }
56
38
  catch (e) {
57
- // Both endings leave the webhook unregistered and the operator with work to do, so both are ERROR;
58
- // they differ only in what to do about it.
39
+ // Both endings leave the webhook unregistered and the operator with work to do, so both are ERROR; they differ
40
+ // only in what to do about it.
59
41
  const error = String(e);
60
42
  log.error(isTransientRegistrationError(error)
61
43
  ? `[fastagent] telegram: Telegram could not reach ${webhookUrl} after retries (last error: ${error}). ` +
@@ -1,4 +1,5 @@
1
1
  import { telegramChannel } from "@fastagent-sh/fastagent/telegram";
2
+ import { defineChannel } from "@fastagent-sh/fastagent";
2
3
 
3
4
  // A channel = a third-party ADAPTER (telegramChannel: verify + run + reply) configured with YOUR policy.
4
5
  // fastagent discovers this file under channels/, mounts POST /telegram, and pipes the agent + state
@@ -8,22 +9,28 @@ import { telegramChannel } from "@fastagent-sh/fastagent/telegram";
8
9
  // 3. register the webhook once, pointing Telegram at POST /telegram with that secret:
9
10
  // curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" \
10
11
  // -d url=https://your.host/telegram -d secret_token=$TELEGRAM_SECRET_TOKEN
11
- export default telegramChannel({
12
- secretToken: process.env.TELEGRAM_SECRET_TOKEN ?? "", // missing → fails at startup (would accept forged updates)
13
- botToken: process.env.TELEGRAM_BOT_TOKEN ?? "", // used to send the agent's reply back to the chat
14
- // Dev/personal bot: surface raw errors to the chat so you (and your AI agent) can act on them. The
15
- // chat is customer-facing by default — for a public bot, drop this or return a neutral string;
16
- // full details always go to the server log regardless.
17
- onError: (failed) => `⚠️ ${failed.details}`,
18
- // Your bot's @username. Optional — resolved via getMe when omitted — but set it if you pass a `route`
19
- // below: the channel needs its own copy to recognise a `/stop` addressed to it.
20
- // botUsername: "your_bot",
21
- // The channel owns transport + format (HTML) + attachments (photo→vision, file→disk) + streaming.
22
- // `route` (POLICY) is OPTIONAL — omitted, it uses defaultTelegramRoute: private chats always answer,
23
- // groups only on a reply to THIS bot, an @mention of it, or a `/stop` addressed to it. Override to
24
- // customise, reusing the export — but pass your bot's identity BOTH here and as `botUsername` above
25
- // (the omitted default gets it from the channel; a bare `defaultTelegramRoute(u)` answers only
26
- // private chats):
27
- // route: (u) => defaultTelegramRoute(u, { botUsername: "my_bot" }) && { session: `user:${u.message?.from?.id}` },
28
- // route: (u) => defaultTelegramRoute(u, { botUsername: "my_bot" }) && { text: `${telegramEnvelope(u.message!)}\n[extra]` },
12
+ export default defineChannel({
13
+ // The env vars this channel needs. fastagent carries them to a deployed box and refuses to serve
14
+ // while one is unset — a bare process.env read gets neither guarantee.
15
+ secrets: ["TELEGRAM_BOT_TOKEN", "TELEGRAM_SECRET_TOKEN"],
16
+ channel: (secrets) =>
17
+ telegramChannel({
18
+ secretToken: secrets.TELEGRAM_SECRET_TOKEN, // verifies inbound updates really come from Telegram
19
+ botToken: secrets.TELEGRAM_BOT_TOKEN, // used to send the agent's reply back to the chat
20
+ // Dev/personal bot: surface raw errors to the chat so you (and your AI agent) can act on them. The
21
+ // chat is customer-facing by default — for a public bot, drop this or return a neutral string;
22
+ // full details always go to the server log regardless.
23
+ onError: (failed) => `⚠️ ${failed.details}`,
24
+ // Your bot's @username. Optional — resolved via getMe when omitted — but set it if you pass a `route`
25
+ // below: the channel needs its own copy to recognise a `/stop` addressed to it.
26
+ // botUsername: "your_bot",
27
+ // The channel owns transport + format (HTML) + attachments (photo→vision, file→disk) + streaming.
28
+ // `route` (POLICY) is OPTIONAL — omitted, it uses defaultTelegramRoute: private chats always answer,
29
+ // groups only on a reply to THIS bot, an @mention of it, or a `/stop` addressed to it. Override to
30
+ // customise, reusing the export — but pass your bot's identity BOTH here and as `botUsername` above
31
+ // (the omitted default gets it from the channel; a bare `defaultTelegramRoute(u)` answers only
32
+ // private chats):
33
+ // route: (u) => defaultTelegramRoute(u, { botUsername: "my_bot" }) && { session: `user:${u.message?.from?.id}` },
34
+ // route: (u) => defaultTelegramRoute(u, { botUsername: "my_bot" }) && { text: `${telegramEnvelope(u.message!)}\n[extra]` },
35
+ }),
29
36
  });
@@ -4,7 +4,7 @@ import { basename } from "node:path";
4
4
 
5
5
  // Send a message or a local file back to a Telegram chat. In a CHAT turn the channel delivers the
6
6
  // reply itself — this tool is for files, and for turns NO channel is carrying: a scheduled turn
7
- // (schedules/<name>.ts) or a self-scheduled wake-up, whose plain reply is not delivered anywhere.
7
+ // (routines/<name>.ts) or a self-scheduled wake-up, whose plain reply is not delivered anywhere.
8
8
  // The chatId comes from the [telegram: chat …] context line in a chat turn; a scheduled turn has no
9
9
  // such line, so the schedule's prompt must name the target chat id. tools/ is auto-discovered.
10
10
 
@@ -73,9 +73,11 @@ export default defineTool({
73
73
  asPhoto: z.boolean().optional().describe("send the file as a photo (inline) instead of a document"),
74
74
  messageThreadId: z.number().optional().describe("thread to reply into (from the context line), if any"),
75
75
  }),
76
- async execute({ chatId, text, path, caption, asPhoto, messageThreadId }) {
77
- const token = process.env.TELEGRAM_BOT_TOKEN;
78
- if (!token) throw new Error("TELEGRAM_BOT_TOKEN is not set");
76
+ // The bot token, declared: fastagent carries it to a deployed box and refuses to start while it is
77
+ // unset, so `execute` gets a value rather than checking for one.
78
+ secrets: ["TELEGRAM_BOT_TOKEN"],
79
+ async execute({ chatId, text, path, caption, asPhoto, messageThreadId }, ctx) {
80
+ const token = ctx.secrets.TELEGRAM_BOT_TOKEN;
79
81
  if ((text === undefined) === (path === undefined)) {
80
82
  throw new Error("pass exactly one of `text` (a message) or `path` (a file)");
81
83
  }
@@ -1,4 +1,5 @@
1
1
  import type { ImageRef } from "../../agent.ts";
2
+ import type { CallOptions } from "../kit/transport.ts";
2
3
  /** Telegram's hard text limit per message. */
3
4
  export declare const TELEGRAM_MAX_TEXT = 4096;
4
5
  /** Where a reply goes: a chat, optionally a thread (Threaded Mode), optionally replying to a message. */
@@ -37,7 +38,7 @@ interface Api {
37
38
  * The pipeline: one Bot API call, carrying every transport invariant (see the module header). Returns
38
39
  * the method's `result` payload; throws {@link TelegramApiError} on any failure.
39
40
  */
40
- export declare function callApi<M extends keyof Api>(api: string, botToken: string, method: M, params: Record<string, unknown>): Promise<Api[M]>;
41
+ export declare function callApi<M extends keyof Api>(api: string, botToken: string, method: M, params: Record<string, unknown>, opts?: CallOptions): Promise<Api[M]>;
41
42
  /**
42
43
  * Split text into ≤4096-char chunks (Telegram's limit), preferring a newline boundary. When `html`, it is
43
44
  * tag-aware: a tag that would SPAN a boundary is CLOSED at the chunk's end and REOPENED (attributes and
@@ -59,7 +60,7 @@ export declare function chunkText(text: string, opts?: {
59
60
  */
60
61
  export declare function sendMessage(api: string, botToken: string, t: Target, body: string, opts?: {
61
62
  html?: boolean;
62
- }): Promise<number | undefined>;
63
+ } & CallOptions): Promise<number | undefined>;
63
64
  /**
64
65
  * Edit a message in place — the live-preview mechanism (one message, repeatedly updated). Plain by
65
66
  * default (a partial preview may contain unbalanced HTML); `html:true` for the final answer, falling
@@ -68,7 +69,7 @@ export declare function sendMessage(api: string, botToken: string, t: Target, bo
68
69
  */
69
70
  export declare function editMessageText(api: string, botToken: string, t: Target, messageId: number, body: string, opts?: {
70
71
  html?: boolean;
71
- }): Promise<void>;
72
+ } & CallOptions): Promise<void>;
72
73
  /** Fetch the message's images. Throws if any cannot be loaded — the caller surfaces it (no silent drop). */
73
74
  export declare function resolveImages(api: string, botToken: string, fileIds: string[] | undefined): Promise<ImageRef[] | undefined>;
74
75
  /** Download the message's files into `filesDir` (the channel passes its state home's `files/`).
@@ -7,7 +7,8 @@
7
7
  * connection cannot hang a turn, its session queue, or webhook registration.
8
8
  * 2. Only a 429 is retried (bounded attempts, honouring `retry_after` up to FLOOD_WAIT_MAX_S per
9
9
  * wait); a longer flood ban or exhausted retries fail visibly. No other failure class is retried:
10
- * the request may have been processed, and a retried sendMessage would double-deliver.
10
+ * the request may have been processed, and a retried sendMessage would double-deliver. A caller whose
11
+ * write is DROPPABLE spends no budget on it — see kit/transport.ts.
11
12
  * 3. Success requires the body's own `ok:true` — an intermediary's HTTP 200 is not a sent message.
12
13
  * 4. Every failure is a {@link TelegramApiError} naming the method: self-description is a property of
13
14
  * the error type, not per-call-site string assembly.
@@ -62,7 +63,8 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
62
63
  * The pipeline: one Bot API call, carrying every transport invariant (see the module header). Returns
63
64
  * the method's `result` payload; throws {@link TelegramApiError} on any failure.
64
65
  */
65
- export async function callApi(api, botToken, method, params) {
66
+ export async function callApi(api, botToken, method, params, opts = {}) {
67
+ const retries = opts.retries ?? RETRIES;
66
68
  for (let attempt = 0;; attempt++) {
67
69
  let res;
68
70
  let raw;
@@ -87,7 +89,7 @@ export async function callApi(api, botToken, method, params) {
87
89
  }
88
90
  if (res.ok && data.ok === true)
89
91
  return data.result;
90
- if (res.status === 429 && attempt < RETRIES) {
92
+ if (res.status === 429 && attempt < retries) {
91
93
  const floodWait = data.parameters?.retry_after ?? attempt + 1; // no retry_after → short linear backoff
92
94
  if (floodWait <= FLOOD_WAIT_MAX_S) {
93
95
  await wait((floodWait + 1) * 1000);
@@ -216,7 +218,7 @@ export async function sendMessage(api, botToken, t, body, opts = {}) {
216
218
  }
217
219
  let result;
218
220
  try {
219
- result = await callApi(api, botToken, "sendMessage", mode ? { ...base, parse_mode: "HTML" } : base);
221
+ result = await callApi(api, botToken, "sendMessage", mode ? { ...base, parse_mode: "HTML" } : base, opts);
220
222
  }
221
223
  catch (e) {
222
224
  if (!(mode && e instanceof TelegramApiError && PARSE_ERROR.test(e.description)))
@@ -229,7 +231,7 @@ export async function sendMessage(api, botToken, t, body, opts = {}) {
229
231
  continue;
230
232
  }
231
233
  // A later chunk failed after the first parsed cleanly (rare) — best-effort plain resend of this chunk.
232
- result = await callApi(api, botToken, "sendMessage", base);
234
+ result = await callApi(api, botToken, "sendMessage", base, opts);
233
235
  }
234
236
  if (first)
235
237
  firstId = result.message_id;
@@ -251,7 +253,7 @@ export async function editMessageText(api, botToken, t, messageId, body, opts =
251
253
  };
252
254
  const notModified = (e) => e instanceof TelegramApiError && /message is not modified/i.test(e.description);
253
255
  try {
254
- await callApi(api, botToken, "editMessageText", opts.html ? { ...base, parse_mode: "HTML" } : base);
256
+ await callApi(api, botToken, "editMessageText", opts.html ? { ...base, parse_mode: "HTML" } : base, opts);
255
257
  }
256
258
  catch (e) {
257
259
  if (notModified(e))
@@ -259,7 +261,7 @@ export async function editMessageText(api, botToken, t, messageId, body, opts =
259
261
  if (!(opts.html && e instanceof TelegramApiError && PARSE_ERROR.test(e.description)))
260
262
  throw e;
261
263
  try {
262
- await callApi(api, botToken, "editMessageText", base); // plain fallback for rejected markup
264
+ await callApi(api, botToken, "editMessageText", base, opts); // plain fallback for rejected markup
263
265
  }
264
266
  catch (e2) {
265
267
  if (!notModified(e2))
@@ -8,28 +8,14 @@ export interface TelegramChannelOptions {
8
8
  secretToken: string;
9
9
  /** Bot token — used to send the agent's reply via the Bot API. */
10
10
  botToken: string;
11
- /** Policy: whether/where to answer an update (return null to ignore). Defaults to {@link defaultTelegramRoute}. */
11
+ /** Policy: whether/where to answer an update (return null to ignore). */
12
12
  route?: (update: TelegramUpdate) => TelegramRoute | null;
13
- /**
14
- * Customer-facing failure text for the chat (the dev-facing full `details` always go to the operator
15
- * log). Return a string to send it, or undefined/"" to stay silent. Default: a neutral message keyed
16
- * on `retryable`. A developer's own bot can surface the raw details, e.g. `(f) => `⚠️ ${f.details}``.
17
- */
13
+ /** Customer-facing failure text for the chat (the dev-facing full `details` always go to the operator log). */
18
14
  onError?: (failed: TelegramFailure) => string | undefined;
19
15
  /** Bot @username for group @mention summon by the default route (else resolved via getMe). */
20
16
  botUsername?: string;
21
- /** Bot API base, for tests. Defaults to the public Telegram endpoint. */
17
+ /** Bot API base, for tests. */
22
18
  apiBaseUrl?: string;
23
19
  }
24
- /**
25
- * Build a Telegram bot channel: policy options in, a {@link ChannelModule} out. The framework (or an
26
- * embedder) mounts it with the context — `telegramChannel(opts)` in `channels/telegram.ts` is the whole
27
- * glue; `agent` and the state root arrive via ctx, never through user code. Mounts `POST /telegram`
28
- * (the path `--tunnel` webhook registration expects). The adapter owns that route key; to serve the
29
- * SAME instance at a different path (e.g. behind a rewriting proxy), re-key the returned module:
30
- * `(ctx) => ({ "POST /bot": telegramChannel(opts)(ctx)["POST /telegram"]! })`.
31
- * This re-routes ONE instance — it is not a way to run two telegram bots in one workspace: the state
32
- * home is derived from the channel kind (`<stateRoot>/channels/telegram`), so a second instance would
33
- * share the first's turn-store/context-buffer. One telegram instance per workspace (single-process).
34
- */
20
+ /** Build a Telegram bot channel: policy options in, a {@link ChannelModule} out. */
35
21
  export declare function telegramChannel({ secretToken, botToken, route, onError, botUsername, apiBaseUrl, }: TelegramChannelOptions): ChannelModule;
@@ -1,57 +1,29 @@
1
1
  /**
2
- * Telegram bot channel: verify the webhook secret token → decide via `route(update)` → run the turn →
3
- * stream the agent's reply back to the chat, ACK 200. Reply model A: the channel holds the bot token
4
- * and posts the reply itself (chat UX), unlike the github channel's fire-and-forget. No SDK — inbound
5
- * is a JSON POST, outbound is a `fetch` to the Bot API. The developer writes only `route` (policy); the
6
- * channel owns transport + format + attachments.
7
- *
8
- * This file is the Telegram WIRING: ingress (secret/body cap/JSON) + the per-turn lifecycle + composition.
9
- * Every other concern lives in its own module, each owning its invariants:
10
- * - parse.ts pure message parsing: field extraction, prompt envelope, summon/route policy
11
- * - invoke-turn.ts run one turn: assemble inputs (resolve attachments) + stream `agent.invoke`
12
- * - kit/turn-runner.ts the durable-turn lifecycle (accept → dequeue → execute → end) over the queue + store
13
- * - turn-store.ts durable turn intent (L1): pre-ACK persist, replay a crash-surviving turn
14
- * - context-buffer.ts un-summoned group discussion, folded into the next answered turn
15
- * - preview.ts the live-preview pump ("💭 Thinking…" → edits → final answer) + terminal writes
16
- * - telegram-api.ts the single Bot API pipeline (timeouts, 429, ok-gating, HTML-aware split)
17
- * - state.ts atomic state files under the channel-state home
18
- *
19
- * Threaded Mode (topics in private chats, a @BotFather toggle) is auto-adapted: an update carrying
20
- * message_thread_id replies into that thread; without one the chat is linear. Same code, both modes.
21
- *
22
- * Authored against the public `@fastagent-sh/fastagent` surface only (the contract + the channel-authoring
23
- * kit: readBodyCapped / text), so it is exactly what a third-party `fastagent-channel-*` package would write.
2
+ * Telegram bot channel: verify the webhook secret token → decide via `route(update)` → run the turn → stream the
3
+ * agent's reply back to the chat, ACK 200.
24
4
  */
25
5
  import { isAbsolute, join } from "node:path";
26
6
  import { log } from "../../log.js";
27
7
  import { readBodyCapped } from "../body.js";
28
8
  import { text } from "../respond.js";
29
9
  import { secretEquals } from "../secret.js";
30
- import { invokeTurn } from "./invoke-turn.js";
10
+ import { telegramTurnStream } from "./invoke-turn.js";
31
11
  import { collectAttachments, createContextBuffer } from "./context-buffer.js";
32
12
  import { attachmentSummary, defaultTelegramRoute, extractFiles, extractImages, fromLabel, messageText, ownFiles, ownImages, pickMessage, telegramEnvelope, telegramStop, } from "./parse.js";
33
- import { defaultErrorMessage, streamReply } from "./preview.js";
13
+ import { defaultErrorMessage, deliverTelegramAnswer, telegramReply } from "./preview.js";
14
+ import { attachmentsDir } from "../kit/attachment-path.js";
34
15
  import { ensureStateHome } from "../kit/state.js";
35
16
  import { dispatchStop } from "../kit/stop-command.js";
36
17
  import { callApi, editMessageText, sendMessage } from "./telegram-api.js";
37
18
  import { createTurnRunner } from "../kit/turn-runner.js";
38
19
  import { discussionBlock } from "../kit/context-buffer.js";
20
+ import { portJoin } from "../../effect-port.js";
39
21
  import { createTurnStore } from "./turn-store.js";
40
22
  // Re-export the public surface authored elsewhere, so `@fastagent-sh/fastagent/telegram` keeps one entry point.
41
23
  export { defaultTelegramRoute, telegramEnvelope, telegramStop };
42
24
  /** Update body cap — Telegram updates are small JSON; 1 MiB is generous and guards a public endpoint. */
43
25
  const MAX_UPDATE_BYTES = 1 << 20;
44
- /**
45
- * Build a Telegram bot channel: policy options in, a {@link ChannelModule} out. The framework (or an
46
- * embedder) mounts it with the context — `telegramChannel(opts)` in `channels/telegram.ts` is the whole
47
- * glue; `agent` and the state root arrive via ctx, never through user code. Mounts `POST /telegram`
48
- * (the path `--tunnel` webhook registration expects). The adapter owns that route key; to serve the
49
- * SAME instance at a different path (e.g. behind a rewriting proxy), re-key the returned module:
50
- * `(ctx) => ({ "POST /bot": telegramChannel(opts)(ctx)["POST /telegram"]! })`.
51
- * This re-routes ONE instance — it is not a way to run two telegram bots in one workspace: the state
52
- * home is derived from the channel kind (`<stateRoot>/channels/telegram`), so a second instance would
53
- * share the first's turn-store/context-buffer. One telegram instance per workspace (single-process).
54
- */
26
+ /** Build a Telegram bot channel: policy options in, a {@link ChannelModule} out. */
55
27
  export function telegramChannel({ secretToken, botToken, route, onError, botUsername, apiBaseUrl = "https://api.telegram.org", }) {
56
28
  return ({ agent, stateRoot, control }) => {
57
29
  // Validate at activation so deploy may inspect the module shape before secrets are provisioned.
@@ -62,10 +34,9 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
62
34
  throw new Error("telegramChannel requires a non-empty botToken (used to send the agent's reply)");
63
35
  }
64
36
  const formatError = onError ?? defaultErrorMessage;
65
- // One getMe at startup: the bot's @username (for the default route's group @mention summon and for
66
- // recognising an addressed `/stop`, only when not supplied) and the group-privacy flag — privacy
67
- // mode off is required to receive the un-summoned group messages that feed the context buffer, so
68
- // warn if it is on.
37
+ // One getMe at startup: the bot's @username (for the default route's group @mention summon and for recognising an
38
+ // addressed `/stop`, only when not supplied) and the group-privacy flag — privacy mode off is required to receive
39
+ // the un-summoned group messages that feed the context buffer, so warn if it is on.
69
40
  let mentionName = botUsername;
70
41
  void callApi(apiBaseUrl, botToken, "getMe", {}).then((me) => {
71
42
  if (mentionName === undefined)
@@ -75,40 +46,31 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
75
46
  "context (un-summoned messages) won't be captured. Disable it via @BotFather → /setprivacy.");
76
47
  }
77
48
  },
78
- // Name every capability the missing username costs, so "the bot ignores /stop@name" is
79
- // diagnosable from this ONE line — the alternative, a warn per undecidable message, repeats a
80
- // single startup fact on every update.
49
+ // Name every capability the missing username costs, so "the bot ignores /stop@name" is diagnosable from this
50
+ // ONE line.
81
51
  (e) => log.warn(`[telegram] getMe failed; @mention summon, addressed /stop, and the privacy check are skipped: ${String(e)}`));
82
- // A bot token is "<bot_id>:<secret>" — the bot's own id is knowable synchronously, so reply-to-bot
83
- // targeting is precise from the first update (no getMe race; getMe only resolves the @username).
84
- // Every real token parses; one that doesn't (a mock/test token) degrades visibly: reply summon stays
85
- // off (fail-closed in repliesToBot) until getMe supplies the username tier.
52
+ // A bot token is "<bot_id>:<secret>" — the bot's own id is knowable synchronously, so reply-to-bot targeting is
53
+ // precise from the first update (no getMe race; getMe only resolves the @username).
86
54
  const tokenId = Number(botToken.split(":")[0]);
87
55
  const botId = Number.isSafeInteger(tokenId) && tokenId > 0 ? tokenId : undefined;
88
56
  if (botId === undefined) {
89
57
  log.warn("[telegram] bot token has no parseable bot id — reply-to-bot summon disabled until getMe resolves");
90
58
  }
91
59
  const decide = route ?? ((update) => defaultTelegramRoute(update, { botUsername: mentionName, botId }));
92
- // The channel-state convention: this channel's durable home is `<stateRoot>/channels/telegram`
93
- // (engine state at the root, channel state under `channels/<kind>/`) — derived, not an option, so
94
- // the operator's ONE state knob (FASTAGENT_STATE_DIR) can never be silently bypassed by glue.
95
- // The ctx contract says stateRoot is absolute (loadChannels enforces it); re-assert for embedders
96
- // that mount without the loader — a silent cwd re-anchor is the bug this contract exists to kill,
97
- // and every derived path (incl. attachment paths) relies on DownloadedFile's absolute-path contract.
60
+ // The channel-state convention: this channel's durable home is `<stateRoot>/channels/telegram` (engine state at
61
+ // the root, channel state under `channels/<kind>/`).
98
62
  if (!isAbsolute(stateRoot)) {
99
63
  throw new Error(`telegramChannel requires an absolute ctx.stateRoot, got "${stateRoot}"`);
100
64
  }
101
65
  const stateHome = join(stateRoot, "channels", "telegram");
102
66
  ensureStateHome(stateHome); // buffers/files may carry chat content; the agent .gitignore covers .state/
103
67
  const buffer = createContextBuffer(join(stateHome, "buffers.json"));
104
- // Durable turn intent (L1): persist an accepted turn pre-ACK, remove it when the turn ends; a crash
105
- // leaves it for replay on the next start. See turn-store.ts for the at-least-once semantics.
68
+ // Durable turn intent (L1): persist an accepted turn pre-ACK, remove it when the turn ends; a crash leaves it for
69
+ // replay on the next start.
106
70
  const store = createTurnStore(join(stateHome, "turns.json"));
107
71
  const targetOf = (r) => ({ chatId: r.chatId, threadId: r.threadId, replyTo: r.replyTo });
108
- // Tell the asker when a turn is dropped at the execution ceiling: the chain's end needs a signal, not
109
- // just an operator log line. Take over the ⏳ "Queued" notice in place if the turn had one (else send
110
- // fresh) — leaving it pinned at "Queued" while sending a separate failure would double-post. Best-
111
- // effort, like the queue notices.
72
+ // Tell the asker when a turn is dropped at the execution ceiling: the chain's end needs a signal, not just an
73
+ // operator log line.
112
74
  const notifyDropped = (r) => {
113
75
  const body = "⚠️ I couldn’t complete an earlier request — please ask again.";
114
76
  const sent = r.previewId !== undefined
@@ -124,11 +86,8 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
124
86
  fromStored: ({ attempts: _a, ...intent }) => ({ ...intent, previewId: undefined }),
125
87
  bufferKey: (rec) => rec.placeKey,
126
88
  where: (rec) => `chat=${rec.chatId}${rec.threadId !== undefined ? ` thread=${rec.threadId}` : ""}`,
127
- // Queue feedback: when this session already has a turn running/queued, a silent wait reads as "the
128
- // bot ignored me" once the current turn runs long — tell the asker NOW (reply-quoted, so it is
129
- // clear whose ask is queued). Best-effort and post-ACK: a failed notice is a log line, never a
130
- // failed update. The turn's live preview then edits this same message in place. The runner holds
131
- // this same `rec` object, so mutating it here (awaited at dequeue) hands the turn its preview id.
89
+ // Queue feedback: when this session already has a turn running/queued, a silent wait reads as "the bot ignored
90
+ // me" once the current turn runs long.
132
91
  onQueuedBehind: (rec) => ({
133
92
  done: sendMessage(apiBaseUrl, botToken, targetOf(rec), "⏳ Queued — I’ll start once the current task finishes.", {
134
93
  html: false,
@@ -137,8 +96,7 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
137
96
  rec.previewId = id;
138
97
  }, (e) => log.warn(`[telegram] queue notice failed (the turn still runs): ${String(e)}`)),
139
98
  }),
140
- // Its ⏳ notice (if any) now falsely reads "Queued": delete it best-effort — the eventual replay
141
- // sends a fresh preview, and leaving it would orphan a stale message above that.
99
+ // Its ⏳ notice (if any) now falsely reads "Queued": delete it best-effort.
142
100
  onDeferred: (rec) => {
143
101
  if (rec.previewId !== undefined) {
144
102
  void callApi(apiBaseUrl, botToken, "deleteMessage", {
@@ -148,13 +106,15 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
148
106
  }
149
107
  },
150
108
  notifyDropped,
151
- execute: (rec, discussion, onCompleted) => streamReply(invokeTurn(agent, rec.session, `${discussionBlock(discussion.text)}${rec.baseText}`, { api: apiBaseUrl, botToken, chatId: rec.chatId, filesDir: join(stateHome, "files") }, {
109
+ // `rec.previewId`: the ⏳ notice THIS process put up if the record waited behind another turn.
110
+ deliverAnswer: (rec, answer) => portJoin(() => deliverTelegramAnswer(apiBaseUrl, botToken, targetOf(rec), answer, rec.previewId)),
111
+ execute: (rec, discussion, onAnswered) => telegramReply(telegramTurnStream(agent, rec.session, `${discussionBlock(discussion.text)}${rec.baseText}`, { api: apiBaseUrl, botToken, chatId: rec.chatId, filesDir: attachmentsDir(stateHome) }, {
152
112
  primary: { imageFileIds: rec.imageFileIds, fileIds: rec.fileIds },
153
113
  buffered: collectAttachments(discussion.consumed, {
154
114
  files: new Set(rec.fileIds),
155
115
  images: new Set(rec.imageFileIds),
156
116
  }),
157
- }, onCompleted), apiBaseUrl, botToken, targetOf(rec), formatError, rec.previewId),
117
+ }), apiBaseUrl, botToken, targetOf(rec), formatError, rec.previewId, onAnswered),
158
118
  });
159
119
  runner.recover();
160
120
  const handler = async (req) => {
@@ -174,27 +134,23 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
174
134
  catch {
175
135
  return text("invalid json\n", 400);
176
136
  }
177
- // Decide whether/where to answer, then run the turn. ACK 200 immediately (the turn may outlast the
178
- // webhook timeout); lifecycle goes to stderr — after the 200 there is no response body, so those
179
- // lines are the operator's only signal.
137
+ // Decide whether/where to answer, then run the turn.
180
138
  const m = pickMessage(update);
181
139
  if (!m)
182
140
  return new Response(null, { status: 200 });
183
141
  const placeKey = m.message_thread_id ? `${m.chat.id}:${m.message_thread_id}` : `${m.chat.id}`;
184
142
  const r = decide(update);
185
143
  if (!r) {
186
- // Not summoned: in a group, record the message so a later summon has the discussion (needs privacy
187
- // off to be delivered here at all). Empty/service messages and non-group chats keep no buffer.
144
+ // Not summoned: in a group, record the message so a later summon has the discussion (needs privacy off to be
145
+ // delivered here at all).
188
146
  const isGroup = m.chat.type === "group" || m.chat.type === "supergroup";
189
147
  const content = messageText(m);
190
148
  if (isGroup && content) {
191
- // OWN attachments only: each message is its own buffer entry, so a reply's referenced
192
- // attachment is already (or will be) the other entry's — recounting it here would duplicate
193
- // downloads and squeeze the attachment cap.
149
+ // OWN attachments only: each message is its own buffer entry, so a reply's referenced attachment is already
150
+ // (or will be) the other entry's.
194
151
  const fileIds = ownFiles(m);
195
152
  const imageIds = ownImages(m);
196
- // A captioned attachment renders as its caption — append the attachment marker so the fold
197
- // ALWAYS labels attachments (that label + sender is all the attribution a photo gets).
153
+ // A captioned attachment renders as its caption.
198
154
  const summary = attachmentSummary(m);
199
155
  const bodyLine = summary && content !== summary ? `${content} ${summary}` : content;
200
156
  buffer.push(placeKey, {
@@ -208,24 +164,14 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
208
164
  }
209
165
  return new Response(null, { status: 200 });
210
166
  }
211
- const session = r.session ?? placeKey;
167
+ // Branded with the channel kind: session ids share one namespace across every channel of a deployment.
168
+ const session = r.session ?? `telegram:${placeKey}`;
212
169
  const chatId = r.chatId ?? m.chat.id;
213
- // Reply to the summoning message in groups (threads the answer under the asker); a 1:1 DM needs no
214
- // reply-quote. Only when the RESOLVED target is the message's own chat+thread: a route that
215
- // redirects elsewhere must not carry a reply_parameters that resolves in the wrong place (fail, or
216
- // quote a same-id message there). Compare VALUES, not whether the route touched the field — a route
217
- // that explicitly returns the same chat/thread still quotes.
170
+ // Reply to the summoning message in groups (threads the answer under the asker); a 1:1 DM needs no reply-quote.
218
171
  const threadId = r.threadId ?? m.message_thread_id;
219
172
  const sameTarget = String(chatId) === String(m.chat.id) && threadId === m.message_thread_id;
220
173
  const replyTo = m.chat.type !== "private" && sameTarget ? m.message_id : undefined;
221
- // Explicit user stop (`/stop`): a control action, never a turn — it must not queue behind the
222
- // run it stops. Awaited before the ACK: dispatch + one sendMessage is fast, and a delivery failure
223
- // logs instead of failing the webhook. Read AFTER the route, on the route's own session: the route
224
- // is both the gate (a route that ignores this chat must not have the bot abort or answer in it) and
225
- // the session authority (a route that remaps `session` would otherwise have its stop abort a
226
- // session nobody runs). The default route summons an ADDRESSED `/stop` in a group for this reason:
227
- // the general slash-command refusal would leave it buffered as discussion while the run kept going.
228
- // An unaddressed one is not a command at all (see telegramStop) and never reaches here.
174
+ // Explicit user stop (`/stop`): a control action, never a turn — it must not queue behind the run it stops.
229
175
  if (telegramStop(update, { botUsername: mentionName, botId })) {
230
176
  const feedback = await dispatchStop(control, session, "[telegram]");
231
177
  await sendMessage(apiBaseUrl, botToken, { chatId, threadId, replyTo }, feedback, { html: false }).catch((e) => log.warn(`[telegram] stop feedback failed: ${String(e)}`));
@@ -250,9 +196,7 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
250
196
  }
251
197
  return new Response(null, { status: 200 });
252
198
  };
253
- // Test/observability seam: await the fire-and-forget turns this handler enqueues. Inert in production
254
- // (nothing reads it; the runtime never drains — see turn-queue), it lets a test await a turn
255
- // deterministically instead of polling for side effects to settle.
199
+ // Test/observability seam: await the fire-and-forget turns this handler enqueues.
256
200
  handler.turnsIdle = () => runner.idle();
257
201
  return { "POST /telegram": handler };
258
202
  };
@@ -1,18 +1,16 @@
1
1
  /**
2
- * The telegram-shaped turn store: the channel's persisted record (what its runner needs to re-execute a
3
- * turn), its IO-boundary shape validator, and its arrival ordering, over the generic L1 turn store
4
- * (../kit/turn-store.ts — the semantics live there: pre-ACK persist, replay on the next start, the poison-
5
- * turn execution ceiling, the fail-closed attempt bump).
2
+ * The telegram-shaped turn store: the channel's persisted record (what its runner needs to re-execute a turn), its
3
+ * IO-boundary shape validator, and its arrival ordering, over the generic L1 turn store (../kit/turn-store.ts — the
4
+ * semantics live there: pre-ACK persist, replay on the next start, the poison-turn execution ceiling, the fail-closed
5
+ * attempt bump).
6
6
  */
7
- import { type TurnStore } from "../kit/turn-store.ts";
8
- /** An accepted turn's persisted intent — the SOURCE for the fields a runner needs to re-execute it
9
- * (telegram.ts's PendingTurn derives from this, so a new execution field added here propagates and
10
- * cannot silently drop from the persisted record). Minus the live `previewId` (a restart's queue
11
- * notice is gone; a replayed turn sends a fresh preview). `attempts` counts how many times this turn
12
- * has STARTED executing without finishing (0 until its first run; bumped at each `startAttempt`). */
13
- export interface StoredTurn {
14
- id: string;
15
- session: string;
7
+ import { type TurnRecordBase, type TurnStore } from "../kit/turn-store.ts";
8
+ /**
9
+ * An accepted turn's persisted intent — the SOURCE for the fields a runner needs to re-execute it (telegram.ts's
10
+ * PendingTurn derives from this, so a new execution field added here propagates and cannot silently drop from the
11
+ * persisted record).
12
+ */
13
+ export interface StoredTurn extends TurnRecordBase {
16
14
  placeKey: string;
17
15
  baseText: string;
18
16
  chatId: number | string;
@@ -20,6 +18,5 @@ export interface StoredTurn {
20
18
  replyTo?: number;
21
19
  imageFileIds: string[];
22
20
  fileIds: string[];
23
- attempts: number;
24
21
  }
25
22
  export declare function createTurnStore(path: string): TurnStore<StoredTurn>;
@@ -1,12 +1,14 @@
1
1
  /**
2
- * The telegram-shaped turn store: the channel's persisted record (what its runner needs to re-execute a
3
- * turn), its IO-boundary shape validator, and its arrival ordering, over the generic L1 turn store
4
- * (../kit/turn-store.ts — the semantics live there: pre-ACK persist, replay on the next start, the poison-
5
- * turn execution ceiling, the fail-closed attempt bump).
2
+ * The telegram-shaped turn store: the channel's persisted record (what its runner needs to re-execute a turn), its
3
+ * IO-boundary shape validator, and its arrival ordering, over the generic L1 turn store (../kit/turn-store.ts — the
4
+ * semantics live there: pre-ACK persist, replay on the next start, the poison-turn execution ceiling, the fail-closed
5
+ * attempt bump).
6
6
  */
7
7
  import { createTurnStore as createGenericTurnStore } from "../kit/turn-store.js";
8
- /** State files are an IO boundary: valid JSON of the WRONG SHAPE must degrade like a corrupt file
9
- * (warn + empty), not flow in as trusted data (mirrors context-buffer's isBufferEntry). */
8
+ /**
9
+ * State files are an IO boundary: valid JSON of the WRONG SHAPE must degrade like a corrupt file (warn + empty), not
10
+ * flow in as trusted data (mirrors context-buffer's isBufferEntry).
11
+ */
10
12
  function isStoredTurn(t) {
11
13
  const r = t;
12
14
  const strings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
@@ -1,6 +1,4 @@
1
- /**
2
- * Poll `healthUrl` until it responds 200, or the timeout elapses. Any error (not routable yet, DNS not
3
- * settled, connection refused) is a "keep waiting", not a failure — that IS the readiness signal. Each
4
- * probe has its own short timeout so one slow attempt can't eat the whole budget.
5
- */
6
- export declare function waitForHealth(healthUrl: string, timeoutMs: number, intervalMs: number): Promise<boolean>;
1
+ /** Poll `healthUrl` until it responds 200, or the timeout elapses. */
2
+ export declare function waitForHealth(healthUrl: string, timeoutMs: number, intervalMs: number,
3
+ /** Optional liveness answer from whoever runs the server. */
4
+ stillStarting?: () => Promise<boolean>): Promise<boolean>;