@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,82 +1,26 @@
1
1
  /**
2
- * Durable turn intent: the L1 half of durable execution (the L2 exactly-once / deterministic
3
- * step-replay layer is the K-axis backend — docs/design/core.md §11). Persists an accepted turn BEFORE
4
- * the webhook 200 (pre-ACK, like the telegram context-buffer) and removes it when the turn ENDS — the
5
- * runner's `finally`: a completed turn OR any caught error both remove it. Precisely, a completed turn's
6
- * removal fires at the `completed` event (the session is committed), which is BEFORE the reply reaches
7
- * the chat platform: so L1 recovers the ACKed-but-un-COMPLETED window, not un-DELIVERED. A crash in
8
- * that narrow tail (completed, session-committed, but the message not yet sent) leaves the answer in the
9
- * session history undelivered and is deliberately NOT replayed — replaying a session-committed turn
10
- * would double-append it; the asker re-asks (and sees the prior answer in history). L1's scope is the
11
- * INTERRUPTED-run window (the `finally` never runs); a caught error is NOT retried here — a `failed`
12
- * event already told the user, and a transport throw is dropped exactly like the pre-L1 in-memory queue
13
- * did (replaying it could double-send). Only an interrupted run leaves the record on disk — and
14
- * "interrupted" is not just a rare crash: `runStart` has no graceful drain (cli.ts), so a SIGTERM exits
15
- * mid-turn too, i.e. EVERY rolling deploy that catches an in-flight turn. Recovery re-enqueues it next start.
2
+ * Durable turn intent: the at-least-once half of durable execution. Exactly-once execution needs a different
3
+ * backend and is out of scope (docs/design/core.md §10).
16
4
  *
17
- * This recovers the ACKed-but-unfinished window the in-memory turn-queue drops (turn-queue.ts). Weigh
18
- * the trade before trusting it: the alternative (dropping the turn) fails VISIBLY and self-corrects
19
- * (the turn vanished, the asker re-asks); replay's sharpest cost is the opposite — invisible. It is
20
- * at-least-once, not exactly-once:
21
- * - PRIMARY cost: replay re-runs the WHOLE turn, so every external side effect happens AGAIN — re-sent
22
- * messages, re-fired tool actions — and nobody may notice (unlike the visible loss it replaces).
23
- * And the trigger is not rare (above): it fires on every deploy that interrupts an in-flight turn,
24
- * not just on crashes. So "safe only if the turn's tools are idempotent" is a bar to judge against
25
- * DEPLOY frequency: it holds for a Q&A bot, and is the gate for adding side-effecting tools. (A
26
- * mid-stream interruption also leaves an orphan "💭 Thinking…" preview — cosmetic.)
27
- * - The pre-ACK window overlaps webhook redelivery: a crash AFTER the persist but BEFORE the 200
28
- * means recovery replays the turn AND the platform (never-ACKed) redelivers the same event — same
29
- * id, and unless the channel dedups accepted ids, the turn can run twice. Exactly-once (a persisted
30
- * delivery key) is L2.
31
- *
32
- * Channel-neutral, generic over the record shape: the channel supplies its own record type (everything
33
- * its runner needs to re-execute the turn), the shape validator (state files are an IO boundary), the
34
- * arrival ordering for recovery, and its log label. Telegram, Feishu/Lark, and Slack consume it.
35
- *
36
- * ponytail: at-least-once with a per-turn EXECUTION ceiling. A poison turn that deterministically
37
- * crashes the process would replay forever under a container restart policy. The counter is bumped at
38
- * `startAttempt` — when a turn is about to RUN, not per restart cycle — so it counts a turn's OWN
39
- * execution attempts: `recover()` re-enqueues every surviving turn without touching its count, and a
40
- * poison turn at a session's head is dropped on its own N+1th run WITHOUT penalizing the never-run turns
41
- * queued behind it (they keep their full budget and get their turn once the poison one is gone). On drop,
42
- * the runner notifies the asker (the chain's end must get a signal — a log line the user can't see isn't).
43
- * The bump is the ONE post-ACK write that fails CLOSED (not best-effort): if it can't be persisted the
44
- * turn is DEFERRED — skipped this cycle and left on disk to replay on the next start — rather than run
45
- * untracked, because an unpersisted count lets recover() re-run a poison turn every restart (the ceiling
46
- * would never advance on disk). It replays on the next START — a restart is required; disk recovery alone
47
- * does not re-run a deferred turn — and the asker is NOT told (a transient system degrade, not a user-
48
- * actionable failure — telling them "ask again" would double-answer on that replay).
49
- * Single-process, single-writer: same durability model as state.ts (crash-safe via atomic rename;
50
- * power-loss is best-effort — no fsync, consistent with the rest of the channel's state).
5
+ * ponytail: at-least-once with a per-turn EXECUTION ceiling ({@link MAX_TURN_ATTEMPTS}). A poison turn that
6
+ * deterministically crashes the process would otherwise replay forever under a container restart policy; a real
7
+ * exactly-once story needs the different backend above.
51
8
  */
52
9
  import { log } from "../../log.js";
53
10
  import { loadStateFile, saveStateFile } from "./state.js";
54
- /**
55
- * How many times a turn may START without finishing before it is dropped rather than run again — the
56
- * poison-turn ceiling described at length above. One value for every channel: it prices replay against
57
- * DEPLOY frequency, which is a property of how fastagent is operated, not of which chat platform is in
58
- * front of it.
59
- *
60
- * Known limitation: the count cannot tell a self-inflicted crash from an external SIGTERM (there is no
61
- * graceful drain), so a legitimately long turn interrupted by this many successive deploys is dropped
62
- * as if it were poison. Three is a bet that such a turn is an outlier, not a defence against one:
63
- * catching SIGTERM to spare it would reintroduce the drain the design refuses — raise this constant
64
- * instead if such turns are expected.
65
- */
11
+ /** How many times a turn may START without finishing before it is dropped rather than run again. */
66
12
  const MAX_TURN_ATTEMPTS = 3;
67
13
  /**
68
- * End an ANSWERED turn: drop its durable intent, then commit the discussion it folded in.
14
+ * An ANSWERED turn: record the reply for delivery, then commit the discussion it folded in. The intent is NOT
15
+ * dropped here — generating an answer is not the same event as that answer reaching the chat, and the runner drops
16
+ * the record only once delivery has settled.
69
17
  *
70
- * The ORDER is the safety property, which is why this is a function and not two lines at each call
71
- * site. A crash between the two writes may re-fold already-answered context into the next summon —
72
- * additive and harmless. The reverse order leaves intent on disk with its context already consumed,
73
- * so the replay runs the same turn with its context stripped.
74
- *
75
- * Called from the turn's `completed` event, when the fold provably lives in the durable session.
18
+ * Returns whether the answer became recoverable, so a caller does not claim a recovery that has nothing on disk.
76
19
  */
77
20
  export function commitAnsweredTurn(store, buffer, turn) {
78
- store.remove(turn.id);
21
+ const recorded = store.answered(turn.id, turn.answer);
79
22
  buffer.commit(turn.bufferKey, turn.consumed);
23
+ return recorded;
80
24
  }
81
25
  export function createTurnStore(path, opts) {
82
26
  const { label, isRecord, order } = opts;
@@ -84,7 +28,9 @@ export function createTurnStore(path, opts) {
84
28
  const raw = loadStateFile(path);
85
29
  if (raw === undefined)
86
30
  return new Map();
87
- if (typeof raw === "object" && raw !== null && !Array.isArray(raw) && Object.values(raw).every(isRecord)) {
31
+ // `answer` belongs to the generic record, so its shape is checked here rather than in each channel's validator.
32
+ const valid = (t) => isRecord(t) && (t.answer === undefined || typeof t.answer === "string");
33
+ if (typeof raw === "object" && raw !== null && !Array.isArray(raw) && Object.values(raw).every(valid)) {
88
34
  return new Map(Object.entries(raw));
89
35
  }
90
36
  log.warn(`${label} unexpected shape in ${path} — starting with no pending turns`);
@@ -92,20 +38,22 @@ export function createTurnStore(path, opts) {
92
38
  };
93
39
  const turns = load();
94
40
  const persist = () => saveStateFile(path, Object.fromEntries(turns));
95
- // Post-ACK writes (remove, startAttempt) must not abort a turn: log a failed write, never throw.
41
+ // Post-ACK writes (remove, startAttempt) must not abort a turn: log a failed write, never throw. The result is
42
+ // returned because whether the write landed is what a caller may be reporting to the operator.
96
43
  const persistBestEffort = (what) => {
97
44
  try {
98
45
  persist();
46
+ return true;
99
47
  }
100
48
  catch (e) {
101
49
  log.error(`${label} turn-store ${what} write failed post-ACK: ${String(e)}`);
50
+ return false;
102
51
  }
103
52
  };
104
53
  return {
105
54
  add(rec) {
106
- // Idempotent on re-add: a redelivery (pre-ACK crash → never-ACKed event replayed) re-submits an id
107
- // already in the store. Skip it — the intent is already durable, and overwriting would RESET this
108
- // turn's execution count, handing a poison turn a fresh ceiling budget.
55
+ // Idempotent on re-add: a redelivery (pre-ACK crash → never-ACKed event replayed) re-submits an id already in
56
+ // the store.
109
57
  if (turns.has(rec.id))
110
58
  return;
111
59
  turns.set(rec.id, rec);
@@ -113,20 +61,32 @@ export function createTurnStore(path, opts) {
113
61
  persist(); // pre-ACK: the transport maps this throw to HTTP/WS 500, so the platform redelivers
114
62
  }
115
63
  catch (e) {
116
- // Roll the memory back so it matches disk (mirrors context-buffer.push): otherwise the phantom
117
- // entry makes the redelivery's `add` short-circuit on `turns.has` — running the turn with its
118
- // intent never persisted, defeating this module — and gets flushed later by an unrelated persist.
64
+ // Roll the memory back so it matches disk (mirrors context-buffer.push): otherwise the phantom entry makes
65
+ // the redelivery's `add` short-circuit on `turns.has`.
119
66
  turns.delete(rec.id);
120
67
  throw e;
121
68
  }
122
69
  },
70
+ answered(id, answer) {
71
+ const rec = turns.get(id);
72
+ if (!rec) {
73
+ // An untracked run (see startAttempt): there is no record to write the answer to, so a delivery that fails
74
+ // from here loses it. Said out loud, because the alternative is a silent gap under a "kept for retry" log.
75
+ log.warn(`${label} turn ${id} answered with no record on disk (untracked run) — its answer is not recoverable`);
76
+ return false;
77
+ }
78
+ turns.set(id, { ...rec, answer });
79
+ // Post-ACK, so it cannot throw: a failed write costs the recovery copy, not the delivery about to happen. It
80
+ // does decide the answer of this function — an answer only in memory survives nothing.
81
+ return persistBestEffort("answer (a crash before delivery would lose the answer)");
82
+ },
123
83
  remove(id) {
124
84
  if (turns.delete(id))
125
- persistBestEffort("remove (a restart may replay an answered turn)");
85
+ persistBestEffort("remove (a restart may re-deliver an answered turn)");
126
86
  },
127
87
  recover() {
128
- // The channel's arrival order, applied explicitly rather than leaning on JS object-key enumeration
129
- // happening to survive the load's JSON round-trip.
88
+ // The channel's arrival order, applied explicitly rather than leaning on JS object-key enumeration happening to
89
+ // survive the load's JSON round-trip.
130
90
  return [...turns.values()].sort(order);
131
91
  },
132
92
  startAttempt(id) {
@@ -135,10 +95,11 @@ export function createTurnStore(path, opts) {
135
95
  return "run"; // no record — run untracked (a redelivery double-run whose first run removed it)
136
96
  const attempts = rec.attempts + 1;
137
97
  if (attempts > MAX_TURN_ATTEMPTS) {
138
- // State the fact, not a cause the counter can't prove: a turn killed mid-run every time bumps
139
- // this whether IT poisoned the process or a deploy/OOM took it down each time.
98
+ // State the fact, not a cause the counter can't prove: a turn killed mid-run every time bumps this whether IT
99
+ // poisoned the process or a deploy/OOM took it down each time.
140
100
  log.error(`${label} dropping turn ${id} after starting ${rec.attempts} time(s) without finishing ` +
141
- `(session=${rec.session}) — it may be crashing the process, or was killed mid-run each time; notifying the asker`);
101
+ `(session=${rec.session}) — it may be crashing the process, or was killed mid-run each time; it will not ` +
102
+ `be run again`);
142
103
  turns.delete(id);
143
104
  persistBestEffort("drop");
144
105
  return "exceeded";
@@ -148,12 +109,8 @@ export function createTurnStore(path, opts) {
148
109
  persist();
149
110
  }
150
111
  catch (e) {
151
- // The bump MUST be durable, unlike remove/drop: if it isn't, a restart's recover() reads the old
152
- // count and RE-RUNS this turn — a poison turn would re-execute forever (the ceiling never advances
153
- // on disk). Fail closed: roll the memory bump back (match disk) and DEFER — the record stays on
154
- // disk with its old count and replays on the next start (a restart is required; disk recovery
155
- // alone won't re-run it). Not a drop: under disk failure the removal couldn't persist anyway.
156
- // The runner skips silently (no notify).
112
+ // The bump MUST be durable, unlike remove/drop: if it isn't, a restart's recover() reads the old count and
113
+ // RE-RUNS this turn.
157
114
  turns.set(id, rec);
158
115
  log.error(`${label} cannot persist turn ${id}'s attempt count — deferring it to the next start rather ` +
159
116
  `than run it untracked (session=${rec.session}): ${String(e)}`);
@@ -1,8 +1,4 @@
1
- /**
2
- * Lark-international compatibility adapter. Feishu is the canonical protocol/runtime implementation;
3
- * this module binds it to Lark's cloud profile and exposes natural Lark-branded public names. Lark's
4
- * weaker control-plane capabilities live in onboarding/registration, not in a fork of the turn engine.
5
- */
1
+ /** Lark-international compatibility adapter. */
6
2
  import type { ChannelModule, LongConnectionChannelModule } from "../../channel.ts";
7
3
  import { type FeishuChannelOptions, type FeishuFailure, type FeishuMessage, type FeishuMessageEvent, type FeishuRoute, type FeishuWebSocketChannelOptions, defaultFeishuRoute } from "../feishu/feishu.ts";
8
4
  export type LarkChannelOptions = FeishuChannelOptions;
@@ -1,13 +1,5 @@
1
1
  import type { FeishuGroupBehavior, FeishuSubscriptionMode } from "../feishu/setup-mode.ts";
2
- /**
3
- * Guided Lark-international onboarding. The intl cloud cannot complete the BOUND scan-to-create flow,
4
- * so a new/partial setup opens its unbound one-click launcher and collects one App-scoped credential
5
- * set; a complete existing ID/Secret pair resumes that App directly. Then optimistically run Feishu's
6
- * webhook-mode + Verification-Token bootstrap against THIS app: a successful PATCH captures the token
7
- * and flips Subscription mode; only a definitive config-route 404 falls back to the token the console
8
- * displays + a manual mode switch. IO is injected so the workflow is testable without a terminal or
9
- * browser.
10
- */
2
+ /** Guided Lark-international onboarding. */
11
3
  export declare const LARK_CONSOLE_URL = "https://open.larksuite.com/page/launcher?from=backend_oneclick";
12
4
  /** Directly where the fallback token lives (and where the successful automatic mode change is visible). */
13
5
  export declare function larkEventSecurityUrl(appId: string): string;
@@ -25,7 +17,7 @@ interface LarkBootstrapResult {
25
17
  manualReason?: string;
26
18
  }
27
19
  export interface LarkOnboardOptions {
28
- /** Existing active .env values. A complete credential pair is reused (and still validated). */
20
+ /** Existing active .env values. */
29
21
  existing?: Readonly<Record<string, string | undefined>>;
30
22
  ingress?: FeishuSubscriptionMode;
31
23
  groupBehavior?: FeishuGroupBehavior;
@@ -37,7 +29,6 @@ export interface LarkOnboardCredentials {
37
29
  LARK_APP_SECRET: string;
38
30
  LARK_VERIFICATION_TOKEN?: string;
39
31
  }
40
- /** Open the stable app console and collect everything the runtime needs. Cancellation is a visible
41
- * failure: the scaffold remains and `add lark` is deliberately re-runnable to resume onboarding. */
32
+ /** Open the stable app console and collect everything the runtime needs. */
42
33
  export declare function onboardLarkApp(io: LarkOnboardIO, opts: LarkOnboardOptions): Promise<LarkOnboardCredentials>;
43
34
  export {};
@@ -1,12 +1,4 @@
1
- /**
2
- * Guided Lark-international onboarding. The intl cloud cannot complete the BOUND scan-to-create flow,
3
- * so a new/partial setup opens its unbound one-click launcher and collects one App-scoped credential
4
- * set; a complete existing ID/Secret pair resumes that App directly. Then optimistically run Feishu's
5
- * webhook-mode + Verification-Token bootstrap against THIS app: a successful PATCH captures the token
6
- * and flips Subscription mode; only a definitive config-route 404 falls back to the token the console
7
- * displays + a manual mode switch. IO is injected so the workflow is testable without a terminal or
8
- * browser.
9
- */
1
+ /** Guided Lark-international onboarding. */
10
2
  export const LARK_CONSOLE_URL = "https://open.larksuite.com/page/launcher?from=backend_oneclick";
11
3
  /** Directly where the fallback token lives (and where the successful automatic mode change is visible). */
12
4
  export function larkEventSecurityUrl(appId) {
@@ -18,14 +10,12 @@ function required(value, name) {
18
10
  throw new Error(`${name} is required — re-run \`fastagent add lark\` to continue setup`);
19
11
  return trimmed;
20
12
  }
21
- /** Open the stable app console and collect everything the runtime needs. Cancellation is a visible
22
- * failure: the scaffold remains and `add lark` is deliberately re-runnable to resume onboarding. */
13
+ /** Open the stable app console and collect everything the runtime needs. */
23
14
  export async function onboardLarkApp(io, opts) {
24
15
  const existingId = opts.existing?.LARK_APP_ID?.trim();
25
16
  const existingSecret = opts.existing?.LARK_APP_SECRET?.trim();
26
17
  const reuseExistingApp = Boolean(existingId && existingSecret);
27
- // Credentials are one App-scoped set. Reuse only a COMPLETE ID/Secret pair; a partial pair starts a
28
- // fresh launcher/input path, and its unrelated old Token must not be attached to the newly-entered App.
18
+ // Credentials are one App-scoped set.
29
19
  if (reuseExistingApp) {
30
20
  io.note(`Reusing Lark app ${existingId}; opening its Events & Callbacks configuration directly.`);
31
21
  }
@@ -1,4 +1,5 @@
1
1
  import { larkChannel } from "@fastagent-sh/fastagent/lark";
2
+ import { defineChannel } from "@fastagent-sh/fastagent";
2
3
 
3
4
  // larkChannel is the branded compatibility adapter over fastagent's canonical Feishu engine, configured
4
5
  // with YOUR policy. fastagent discovers this file, mounts POST /lark, and pipes the agent + state home
@@ -18,23 +19,30 @@ import { larkChannel } from "@fastagent-sh/fastagent/lark";
18
19
  // the URL automatically. If this app returns a config-API 404, do both BY HAND in the console
19
20
  // with the server running (the platform verifies https://your.host/lark with a challenge).
20
21
  // 5. create a version and publish the app (a tenant admin approves it), then add the bot to a chat
21
- export default larkChannel({
22
- appId: process.env.LARK_APP_ID ?? "", // missing → fails at startup (no replies could be sent)
23
- appSecret: process.env.LARK_APP_SECRET ?? "",
24
- verificationToken: process.env.LARK_VERIFICATION_TOKEN ?? "", // authenticates inbound events
25
- encryptKey: process.env.LARK_ENCRYPT_KEY || undefined, // optional; when set, plaintext events are refused
26
- // No session modes: a chat is one session and a thread is another, and where the answer goes follows
27
- // from that (docs/design/participant-model.md).
28
- // Dev/personal bot: surface raw errors to the chat so you (and your AI agent) can act on them. The
29
- // chat is customer-facing by default — for a public bot, drop this or return a neutral string;
30
- // full details always go to the server log regardless.
31
- onError: (failed) => `⚠️ ${failed.details}`,
32
- // The channel owns transport + format (markdown card) + attachments (image→vision, file→disk) +
33
- // the live streaming preview. `route` (POLICY) is OPTIONAL — omitted, it uses defaultLarkRoute:
34
- // p2p chats always answer; groups answer on @this-bot, plus bare messages in a thread where the
35
- // Agent takes part and exactly ONE human does. Other human group/thread discussion buffers until
36
- // that place's next answered turn; @other-only messages buffer rather than triggering the Agent.
37
- // Override to customise explicit routing, reusing the export:
38
- // route: (e) => defaultLarkRoute(e, { botOpenId: "ou_xxx" }) && { session: `user:${e.sender?.sender_id?.open_id}` },
39
- // route: (e) => defaultLarkRoute(e, { botOpenId: "ou_xxx" }) && { text: `${larkEnvelope(e)}\n[extra]` },
22
+ export default defineChannel({
23
+ // The env vars this channel needs. fastagent carries them to a deployed box and refuses to serve
24
+ // while one is unset — a bare process.env read gets neither guarantee. LARK_ENCRYPT_KEY is
25
+ // optional, so it is NOT declared here: an undeclared read stays undeclared on purpose.
26
+ secrets: ["LARK_APP_ID", "LARK_APP_SECRET", "LARK_VERIFICATION_TOKEN"],
27
+ channel: (secrets) =>
28
+ larkChannel({
29
+ appId: secrets.LARK_APP_ID,
30
+ appSecret: secrets.LARK_APP_SECRET,
31
+ verificationToken: secrets.LARK_VERIFICATION_TOKEN, // authenticates inbound events
32
+ encryptKey: process.env.LARK_ENCRYPT_KEY || undefined, // optional; when set, plaintext events are refused
33
+ // No session modes: a chat is one session and a thread is another, and where the answer goes follows
34
+ // from that (docs/design/participant-model.md).
35
+ // Dev/personal bot: surface raw errors to the chat so you (and your AI agent) can act on them. The
36
+ // chat is customer-facing by default — for a public bot, drop this or return a neutral string;
37
+ // full details always go to the server log regardless.
38
+ onError: (failed) => `⚠️ ${failed.details}`,
39
+ // The channel owns transport + format (markdown card) + attachments (image→vision, file→disk) +
40
+ // the live streaming preview. `route` (POLICY) is OPTIONAL — omitted, it uses defaultLarkRoute:
41
+ // p2p chats always answer; groups answer on @this-bot, plus bare messages in a thread where the
42
+ // Agent takes part and exactly ONE human does. Other human group/thread discussion buffers until
43
+ // that place's next answered turn; @other-only messages buffer rather than triggering the Agent.
44
+ // Override to customise explicit routing, reusing the export:
45
+ // route: (e) => defaultLarkRoute(e, { botOpenId: "ou_xxx" }) && { session: `user:${e.sender?.sender_id?.open_id}` },
46
+ // route: (e) => defaultLarkRoute(e, { botOpenId: "ou_xxx" }) && { text: `${larkEnvelope(e)}\n[extra]` },
47
+ }),
40
48
  });
@@ -0,0 +1,31 @@
1
+ import { larkWebSocketChannel } from "@fastagent-sh/fastagent/lark";
2
+ import { defineChannel } from "@fastagent-sh/fastagent";
3
+
4
+ // Lark WebSocket long connection: the process connects OUT to the platform, so no public URL,
5
+ // Verification Token, Encrypt Key, or --tunnel is needed. In Events & Callbacks choose long
6
+ // connection, subscribe im.message.receive_v1, then publish the app version. Keep one process
7
+ // running in production: scale-to-zero/App Sleeping would disconnect ingress.
8
+ export default defineChannel({
9
+ // The env vars this channel needs. fastagent carries them to a deployed box and refuses to serve
10
+ // while one is unset — a bare process.env read gets neither guarantee.
11
+ secrets: ["LARK_APP_ID", "LARK_APP_SECRET"],
12
+ channel: (secrets) =>
13
+ larkWebSocketChannel({
14
+ appId: secrets.LARK_APP_ID,
15
+ appSecret: secrets.LARK_APP_SECRET,
16
+ // No session modes: a chat is one session and a thread is another, and where the answer goes follows
17
+ // from that (docs/design/participant-model.md).
18
+ // Dev/personal bot: surface raw errors to the chat so you (and your AI agent) can act on them. The
19
+ // chat is customer-facing by default — for a public bot, drop this or return a neutral string;
20
+ // full details always go to the server log regardless.
21
+ onError: (failed) => `⚠️ ${failed.details}`,
22
+ // The channel owns transport + format (markdown card) + attachments (image→vision, file→disk) +
23
+ // the live streaming preview. `route` (POLICY) is OPTIONAL — omitted, it uses defaultLarkRoute:
24
+ // p2p chats always answer; groups answer on @this-bot, plus bare messages in a thread where the
25
+ // Agent takes part and exactly ONE human does. Other human group/thread discussion buffers until
26
+ // that place's next answered turn; @other-only messages buffer rather than triggering the Agent.
27
+ // Override to customise explicit routing, reusing the export:
28
+ // route: (e) => defaultLarkRoute(e, { botOpenId: "ou_xxx" }) && { session: `user:${e.sender?.sender_id?.open_id}` },
29
+ // route: (e) => defaultLarkRoute(e, { botOpenId: "ou_xxx" }) && { text: `${larkEnvelope(e)}\n[extra]` },
30
+ }),
31
+ });
@@ -1,44 +1,18 @@
1
1
  /**
2
- * SHARED: the webhook registrars' outcome. A registrar reports its own FACT; what to do about it (gate
3
- * the deploy or not, and with what remediation) is the CALLER's policy — `deploy --run` gates on
4
- * "failed", the tunnel (a long-running dev process) ignores the result entirely.
2
+ * SHARED: the webhook registrars' outcome. A registrar reports its own FACT; what to do about it is the CALLER's
3
+ * policy — `deploy --run` gates on "failed", the tunnel ignores the result entirely.
5
4
  *
6
- * - "registered": the platform accepted the webhook / event URL.
7
- * - "manual": this run did not fail, but an operator-facing step remains (the registrar printed the
8
- * instructions). Two sub-states differ on re-runnability: credentials not configured (re-run after
9
- * setting .env DOES auto-register; on the deploy path this is pre-gated by missingSecrets and
10
- * unreachable) and a cloud without the config API (the Lark cloud-lag 404 — no re-run can ever
11
- * register it; the console is the only path).
12
- * - "failed": this run ends with the webhook NOT registered, and acting + re-running can fix it
13
- * (a permanent config error, or the platform still unable to reach the URL when the retries ran out).
5
+ * - `registered`: the platform accepted the webhook / event URL.
6
+ * - `manual`: this run did not fail, but an operator-facing step remains (the registrar printed it). Re-runnability
7
+ * differs by sub-state: credentials not configured re-registers on a re-run; a cloud without the config API never
8
+ * will (the console is the only path).
9
+ * - `failed`: the webhook is NOT registered, and acting plus re-running can fix it.
14
10
  */
15
11
  export type RegistrationOutcome = "registered" | "manual" | "failed";
16
12
  export declare const REGISTRATION_RETRY_MS = 10000;
17
- /**
18
- * What `deploy --run` spends instead: 180s, because a host CLI returns before the deployment serves —
19
- * the same gap a tunnel has, on a scale no local signal reports (`docker compose --tunnel` is the one
20
- * host whose ingress does report it, and its driver waits on it the way the tunnel does).
21
- *
22
- * `railway up --ci` returns when the BUILD ends — container start, healthcheck and a freshly minted
23
- * domain's DNS all happen after that, and railway-deploy.live.test.ts allows 180s for exactly this.
24
- * Registration failure GATES the deploy (registration-gate.ts), so a budget shorter than the host's
25
- * own start-up turns a working deployment into a re-run instruction.
26
- *
27
- * N attempts buy (N - 1) waits — {@link retryWhile} only waits BETWEEN calls — so 180s of patience is
28
- * 19, not 18. At 18 the last call went out at t=170s and a host that started answering in the final
29
- * ten seconds was still reported as a deploy to re-run.
30
- */
13
+ /** What `deploy --run` spends instead: 180s, because a host CLI returns before the deployment serves. */
31
14
  export declare const DEPLOY_REGISTRATION_ATTEMPTS = 19;
32
- /**
33
- * Call, and while `retryable` says the failure is the platform not reaching the URL YET, call again —
34
- * the one retry loop every registrar spends {@link REGISTRATION_ATTEMPTS} through. Retryability and
35
- * what a final failure MEANS stay with each platform (their vocabularies differ, and one registrar's
36
- * terminal state is another's `"manual"`); the counting, the announcement and the wait do not.
37
- *
38
- * `onRetry` runs BEFORE the wait — registrars announce there (a silent minute reads as a hang), and
39
- * `add slack` also drops its duplicate-guard marker so it never spans a sleep. The last error is
40
- * thrown, so a caller can tell "still unreachable" from "a config error" in one place.
41
- */
15
+ /** Call, and while `retryable` says the failure is the platform not reaching the URL YET, call again. */
42
16
  export declare function retryWhile<T>(call: () => Promise<T>, retryable: (error: unknown) => boolean, options?: {
43
17
  attempts?: number;
44
18
  retryMs?: number;
@@ -1,45 +1,14 @@
1
- /**
2
- * SHARED: how long a registrar waits for the PLATFORM to be able to reach a freshly minted public URL.
3
- * One judgement (a new tunnel's warm-up), so one pair of numbers — telegram, feishu, slack registration
4
- * and `add slack` all spend it.
5
- *
6
- * Sized for a tunnel, which is live before its URL is HANDED OVER — not before it is printed, which is
7
- * what this budget once had to absorb: a quick tunnel's hostname is published when the tunnel registers
8
- * an edge connection, seconds after cloudflared prints it, and no budget covers a platform that already
9
- * answered "cannot resolve" (#435). `startCloudflareTunnel` waits for that connection, so what is left
10
- * here is the platform's own warm-up. Applied by {@link retryWhile} as its default; a DEPLOY is slower
11
- * and its callers pass {@link DEPLOY_REGISTRATION_ATTEMPTS} instead.
12
- */
1
+ /** SHARED: how long a registrar waits for the PLATFORM to be able to reach a freshly minted public URL. */
13
2
  const REGISTRATION_ATTEMPTS = 8;
14
3
  export const REGISTRATION_RETRY_MS = 10_000;
15
- /**
16
- * What `deploy --run` spends instead: 180s, because a host CLI returns before the deployment serves —
17
- * the same gap a tunnel has, on a scale no local signal reports (`docker compose --tunnel` is the one
18
- * host whose ingress does report it, and its driver waits on it the way the tunnel does).
19
- *
20
- * `railway up --ci` returns when the BUILD ends — container start, healthcheck and a freshly minted
21
- * domain's DNS all happen after that, and railway-deploy.live.test.ts allows 180s for exactly this.
22
- * Registration failure GATES the deploy (registration-gate.ts), so a budget shorter than the host's
23
- * own start-up turns a working deployment into a re-run instruction.
24
- *
25
- * N attempts buy (N - 1) waits — {@link retryWhile} only waits BETWEEN calls — so 180s of patience is
26
- * 19, not 18. At 18 the last call went out at t=170s and a host that started answering in the final
27
- * ten seconds was still reported as a deploy to re-run.
28
- */
4
+ /** What `deploy --run` spends instead: 180s, because a host CLI returns before the deployment serves. */
29
5
  export const DEPLOY_REGISTRATION_ATTEMPTS = 19;
30
- /** Sleep on the GLOBAL timer (not `node:timers/promises`) so tests can drive it with fake timers — the
31
- * same reason feishu-api.ts does. What must happen BEFORE this wait is the point of `onRetry`. */
32
- const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
33
6
  /**
34
- * Call, and while `retryable` says the failure is the platform not reaching the URL YET, call again —
35
- * the one retry loop every registrar spends {@link REGISTRATION_ATTEMPTS} through. Retryability and
36
- * what a final failure MEANS stay with each platform (their vocabularies differ, and one registrar's
37
- * terminal state is another's `"manual"`); the counting, the announcement and the wait do not.
38
- *
39
- * `onRetry` runs BEFORE the wait — registrars announce there (a silent minute reads as a hang), and
40
- * `add slack` also drops its duplicate-guard marker so it never spans a sleep. The last error is
41
- * thrown, so a caller can tell "still unreachable" from "a config error" in one place.
7
+ * Sleep on the GLOBAL timer (not `node:timers/promises`) so tests can drive it with fake timers — the same reason
8
+ * feishu-api.ts does.
42
9
  */
10
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
11
+ /** Call, and while `retryable` says the failure is the platform not reaching the URL YET, call again. */
43
12
  export async function retryWhile(call, retryable, options = {}) {
44
13
  const attempts = options.attempts ?? REGISTRATION_ATTEMPTS;
45
14
  const retryMs = options.retryMs ?? REGISTRATION_RETRY_MS;
@@ -1,9 +1,6 @@
1
1
  /**
2
- * The ONE constant-time comparison every shared-secret gate uses: a webhook secret token, a
3
- * signature, a bearer token, an envelope secret. Timing-safe on equal lengths; a length mismatch
4
- * answers false without leaking a prefix. Non-string input reads as no secret — and an EMPTY
5
- * expected value never matches, so a gate whose secret was never configured cannot be passed by
6
- * sending none.
2
+ * The ONE constant-time comparison every shared-secret gate uses: a webhook secret token, a signature, a bearer token,
3
+ * an envelope secret.
7
4
  */
8
5
  import { Buffer } from "node:buffer";
9
6
  import { timingSafeEqual } from "node:crypto";
@@ -1,68 +1,71 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { ChannelHandler, Routes } from "../channel.ts";
3
- /** Parse a route key: `"METHOD /path"` → `{ method, path }`, or `"/path"` → `{ path }` (any method).
4
- * An empty method (`" /x"`) parses as `""`, which {@link assertRouteKey} refuses — see there. */
3
+ /** Parse a route key: `"METHOD /path"` → `{ method, path }`, or `"/path"` → `{ path }` (any method). */
5
4
  export declare function parseRouteKey(key: string): {
6
5
  method?: string;
7
6
  path: string;
8
7
  };
9
- /**
10
- * A route key is `"METHOD /path"` or `"/path"`, with a literal path.
11
- *
12
- * Every rule here exists because breaking it costs SOMEONE ELSE. Two keys naming one route, a route
13
- * under a mount, a path a URL rewrites (`/a/../x` and `/x` are one route spelled two ways, while
14
- * the conflict check compares strings) — in each case a channel goes dark and its author did
15
- * nothing wrong.
16
- *
17
- * Mistakes that only cost their own author a debugging session are NOT policed: `:id` and `*` are
18
- * literal path characters here, so such a key simply never matches, and an unusual method is the
19
- * same. Refusing those would trade a working capability for a lecture.
20
- */
8
+ /** A route key is `"METHOD /path"` or `"/path"`, with a literal path. */
21
9
  export declare function assertRouteKey(key: string, describe: (problem: string) => string): void;
22
- /** Do these two keys fight over the same request? Equal paths, and a method each answers; a key
23
- * with no method answers all of them. */
10
+ /** Do these two keys fight over the same request? */
24
11
  export declare function routeKeysConflict(a: string, b: string): boolean;
25
- /** A handler owning a path prefix and everything beneath it — the session control plane is the one
26
- * user. Kept out of {@link Routes} so a key is always a literal path and collision checks stay
27
- * comparisons. */
12
+ /** A handler owning a path prefix and everything beneath it — the session control plane is the one user. */
28
13
  export interface PrefixMount {
29
- /** Absolute, no trailing slash, and not `/` (`/control`). Owns `/control` and everything below it.
30
- * The root is excluded deliberately: a handler owning every path is that handler, and routing to
31
- * it through here would only add a table nothing can reach. */
14
+ /** Absolute, no trailing slash, and not `/` (`/control`). */
32
15
  prefix: string;
33
16
  handler: ChannelHandler;
34
17
  }
35
- /** Same status and headers, no content (RFC 9110's HEAD). The discarded body is cancelled, or a
36
- * streaming producer keeps running with no reader. Shared with the control plane, which answers
37
- * HEAD too. */
18
+ /** Same status and headers, no content (RFC 9110's HEAD). */
38
19
  export declare function withoutBody(res: Response): Response;
39
- /** Is `path` inside `prefix`? Segment-wise, so `/controlled` is not inside `/control`. */
40
20
  export declare function pathUnderPrefix(path: string, prefix: string): boolean;
41
21
  /**
42
- * Compose a {@link Routes} table and its {@link PrefixMount}s into one handler.
22
+ * Refuse a cross-origin allow-list that cannot match anything.
43
23
  *
44
- * Refuses at assembly anything that could not receive a request: a key naming the same route as
45
- * another (`"/x"` and `"GET /x"`), a route inside a mount, two mounts claiming the same ground. A
46
- * channel must never go dark unannounced.
24
+ * ONE check for BOTH ways in: `http.cors` in a config file, and `corsOrigins` handed to `mountAgentService` by an
25
+ * embedder. `allowedOrigin` compares exact strings, so `"https://app.example.com/"` — one trailing slash — never
26
+ * matches and the front end is refused with nothing pointing at the rule. A rule that silently matches nothing is
27
+ * worse than one that refuses to load.
28
+ */
29
+ export declare function assertCorsOrigins(origins: unknown, where: string): asserts origins is string[];
30
+ /**
31
+ * The tables a router composes, NAMED rather than ordered.
47
32
  *
48
- * 404 and 405 stay distinct: a remote client reads 404 as "this serve predates the route" (version
49
- * skew) rather than as a fault.
33
+ * Two same-typed positional arguments encoded which side of the trust axis a table was on, and getting them the
34
+ * wrong way round type-checked: the AgentCore adapter passed its IAM-gated routes as `unverified` and they were
35
+ * reported as unauthenticated for two commits. A caller now spells the axis at the call site.
50
36
  */
51
- export declare function router(routes: Routes, mounts?: readonly PrefixMount[]): ChannelHandler;
52
- /** The node:http adapter for a Fetch handler — the embedded server uses it, and an embedder mounting
53
- * fastagent on its OWN node:http server can too.
37
+ export interface RouterSurface {
38
+ /** Routes that authenticate NOBODY — the router applies the JSON body gate and the cross-origin policy to them. */
39
+ unverified?: Routes;
40
+ /** Routes that verify their own caller (a channel's platform signature, a host's IAM). Neither guard applies. */
41
+ selfVerifying?: Routes;
42
+ /** Prefix-owning handlers. Always treated as unverified — the control plane is the only one. */
43
+ mounts?: readonly PrefixMount[];
44
+ /** `http.cors` — exact origins, or `["*"]`. Set, it REPLACES the `*` default; unset, the default stands. */
45
+ corsOrigins?: readonly string[];
46
+ }
47
+ /**
48
+ * Compose the routes that AUTHENTICATE NOBODY, the self-verifying channel routes, and the {@link PrefixMount}s
49
+ * into one handler.
54
50
  *
55
- * Takes the same {@link ChannelHandler} `serveNode` does, so both doors accept the same thing: a
56
- * handler may answer synchronously, and `router()` returns exactly that. Requiring a Promise here
57
- * made the most natural mount — `nodeListener(router(routes))`, the whole agent on the app's own
58
- * server — a type error. */
59
- export declare function nodeListener(handler: ChannelHandler): (req: IncomingMessage, res: ServerResponse) => void;
51
+ * That is the axis, not authorship. Two things follow from it and nothing else does: whether a browser may drive
52
+ * the route, and whether its body must declare JSON. A channel route is exempt from both because it checks its
53
+ * platform's signature inside itself — a Telegram webhook is public on purpose. Ours check nothing, so the router
54
+ * checks for them.
55
+ *
56
+ * TWO TABLES, not one table plus a list of which keys are which, so the three readers (browser reachability, the JSON
57
+ * body gate, the startup report) cannot answer differently.
58
+ *
59
+ * CAVEAT, stated because we cannot enforce it: a CUSTOM channel that verifies nothing lands in `selfVerifying`
60
+ * anyway and gets neither guard. That is decision B (docs/design/session-control.md §14) and it belongs to the
61
+ * channel's author, who owns the credential the platform issued — but it is an assumption here, not a property.
62
+ */
63
+ export declare function router(surface: RouterSurface): ChannelHandler;
60
64
  /**
61
- * Serve `handler` on a Node HTTP server. Thin mechanism: bind, report the port, let the caller stop
62
- * accepting or force-close active connections — no logging/signals/exit (the CLI owns those).
63
- * `listening` resolves with the bound port (useful for port 0) or rejects on a bind error.
64
- * `host` is the bind address; unset means all interfaces (what containers need).
65
+ * The node:http adapter for a Fetch handler — the embedded server uses it, and an embedder mounting fastagent on its
66
+ * OWN node:http server can too.
65
67
  */
68
+ export declare function nodeListener(handler: ChannelHandler): (req: IncomingMessage, res: ServerResponse) => void;
66
69
  export declare function serveNode(handler: ChannelHandler, options: {
67
70
  port: number;
68
71
  host?: string;