@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,57 +1,37 @@
1
1
  /**
2
- * Canonical Feishu live-preview rendering (also reused by Lark compatibility). The preview is ONE
3
- * streaming CARD of TWO elements — the volatile `process` block and the append-only `answer` (see
4
- * card.ts for why the split is the prefix-stability fix) — (create entity → mount it with a
5
- * reply/send → stream full-text snapshots per element with a strictly increasing `sequence`; the
6
- * client renders the typewriter effect); on completion the same card is settled in place with the
7
- * final answer alone (streaming off). Streaming
8
- * updates ride the cardkit quota (50 QPS per app, 10 QPS per card entity, no edit ceiling) — NOT the
9
- * 5 QPS per-chat message quota or
10
- * the 20-edit cap on text messages, which is why the preview is a card and not an edited text message.
2
+ * Canonical Feishu live-preview rendering (also reused by Lark compatibility). The preview is ONE streaming CARD of
3
+ * two elements — the volatile `process` block and the append-only `answer` (card.ts explains why the split is the
4
+ * prefix-stability fix) — streamed as full-text snapshots per element under a strictly increasing `sequence`, then
5
+ * settled in place with the final answer alone. A queued turn mounts the same card early with its queue status and
6
+ * execution takes the entity over, so there is no second reply. Streaming rides the cardkit quota, not the 5 QPS
7
+ * per-chat message quota or the 20-edit cap on text messages — which is why the preview is a card.
11
8
  *
12
- * A queued turn mounts this same card early with its queue status, reply-quoted to that turn's source
13
- * message; when execution starts the preview takes the entity over in place. This mirrors Telegram's
14
- * one-message lifecycle without trying to change a text message into a card (which the platform does
15
- * not support), and keeps multiple queued asks attributable even if their card mounts race visually.
16
- *
17
- * Fallback tier (fail visibly, degrade per turn): if the card cannot be created or mounted, the turn
18
- * runs with a TEXT placeholder and NO live updates (text edits are capped at 20 per message, so the
19
- * text tier spends them only on terminal writes); if the platform closes streaming mid-turn (idle
20
- * timeout), the preview freezes and the settle still lands. The final write is authoritative either
21
- * way, mirroring the telegram preview's terminal-write matrix (completed/failed/abnormal ×
22
- * settle/delete+send/suppress).
9
+ * Degrade tiers (fail visibly, per turn): a card that cannot be created or mounted falls back to a TEXT placeholder
10
+ * with NO live updates; streaming closed mid-turn freezes the preview. The final write is authoritative either way.
23
11
  */
24
12
  import { setTimeout as sleep } from "node:timers/promises";
13
+ import * as Effect from "effect/Effect";
14
+ import * as Clock from "effect/Clock";
15
+ import { previewPump, renderReply } from "../kit/delivery.js";
16
+ import { portJoin } from "../../effect-port.js";
25
17
  import { log } from "../../log.js";
26
18
  import { ANSWER_ELEMENT_ID, CARD_MARKDOWN_MAX_BYTES, PROCESS_ELEMENT_ID, cardEntityContent, finalCardJson, streamingCardJson, } from "./card.js";
27
19
  import { chunkFeishuText, isCardStreamingClosed } from "./feishu-api.js";
28
- import { RETRY_NOTICE, THINKING_PLACEHOLDER, applyTurnEvent, composeTurnBody, createPreviewPump, createTurnView, defaultErrorMessage, revealedAnswer, thinkingLine, toolLines, } from "../kit/preview-kit.js";
20
+ import { DROPPABLE_FRAME } from "../kit/transport.js";
21
+ import { RETRY_NOTICE, THINKING_PLACEHOLDER, applyTurnEvent, composeTurnBody, createTurnView, defaultErrorMessage, revealedAnswer, thinkingLine, toolLines, } from "../kit/preview-kit.js";
29
22
  import { truncateCodePointPrefix, truncateUtf8 } from "../kit/text.js";
30
23
  export { defaultErrorMessage };
31
- /** How often (ms) to push a live-preview snapshot; tool events still flush on the next loop. Cardkit
32
- * allows 10 QPS per card entity (50 per app), but one snapshot a second reads smoothly (the client
33
- * animates between snapshots).
34
- * Doubles as the answer-preview aging window (see answerView). */
24
+ /** How often (ms) to push a live-preview snapshot; tool events still flush on the next loop. */
35
25
  const STREAM_THROTTLE_MS = 1000;
36
26
  /** How much of the (growing) reasoning to peek at in the live view — the most recent tail. */
37
27
  const THINKING_PREVIEW = 280;
38
- /** Cap (code points) on the whole process block — thinking tail + tool lines + retry notice. It
39
- * redraws wholly on change anyway (it is volatile by nature), so over budget the newest COMPLETE
40
- * lines win (see tailLines). ≤1000 points is ≤4 KB UTF-8, which together with the answer's byte cap
41
- * stays inside the 30 KB entity budget. */
28
+ /** Cap (code points) on the whole process block — thinking tail + tool lines + retry notice. */
42
29
  const PROCESS_MAX_POINTS = 1000;
43
- /** Cap the live answer to the card budget, PREFIX-STABLE: the streaming client animates only when the
44
- * old text is a prefix of the new, so an over-budget answer freezes at its head rather than sliding a
45
- * tail window (which would re-type the element every frame). The full answer still lands at settle. */
30
+ /** Cap the live answer to the card budget, PREFIX-STABLE. */
46
31
  function capBytes(s, maxBytes) {
47
32
  return truncateUtf8(s, maxBytes);
48
33
  }
49
- /** Tail-select COMPLETE lines within a code-point budget — the process block's cap. The block's
50
- * lines are semantic units (a `🔧` tool call, the `💭` peek, the `⏳` notice): cutting mid-line
51
- * would orphan a marker or tear a label, so elision happens only at line boundaries, newest lines
52
- * kept, with a leading `…` line marking what was dropped. The in-line guard cannot trigger with the
53
- * bounded renderers (a thinking tail ≤ ~283 points, a tool line ≤ ~135) — it exists so a future
54
- * unbounded line degrades to a head-preserving cut instead of an empty block. */
34
+ /** Tail-select COMPLETE lines within a code-point budget — the process block's cap. */
55
35
  function tailLines(text, maxPoints) {
56
36
  if (Array.from(text).length <= maxPoints)
57
37
  return text;
@@ -70,13 +50,7 @@ function tailLines(text, maxPoints) {
70
50
  return truncateCodePointPrefix(lines.at(-1) ?? "", maxPoints);
71
51
  return `…\n${kept.join("\n")}`;
72
52
  }
73
- /**
74
- * The terminal-write POLICY: resolve the preview into `text`. One card → settle it in place (final
75
- * markdown, streaming off); an over-budget answer settles the card with its first chunk and sends the
76
- * rest as follow-up messages. A failed settle falls back to delete + fresh send, so no "Thinking…" card
77
- * is left pinned above the answer. Text tier → ONE edit into the final text (or delete + fresh sends
78
- * when it doesn't fit). No preview → fresh send. EMPTY text = "say nothing" → just delete the preview.
79
- */
53
+ /** The terminal-write POLICY: resolve the preview into `text`. */
80
54
  async function finalize(api, target, preview, text, seq) {
81
55
  if (text.trim() === "") {
82
56
  if (preview.kind !== "none")
@@ -94,11 +68,7 @@ async function finalize(api, target, preview, text, seq) {
94
68
  // Settle failed (card expired / rejected) — fall through to delete + fresh send below.
95
69
  }
96
70
  if (settled) {
97
- // Threaded continuations must keep reply_in_thread; top-level group replies (target.replyInThread
98
- // unset — a custom route, or an ask outside a thread) intentionally avoid repeating the quote on
99
- // every chunk. sendText owns the same distinction for its own chunking.
100
- // A continuation failure propagates: the card is already authoritative, so deleting it and sending
101
- // the full answer again would deterministically duplicate every continuation that already landed.
71
+ // Threaded continuations must keep reply_in_thread.
102
72
  const continuationTarget = target.replyInThread ? target : { chatId: target.chatId };
103
73
  for (const chunk of rest)
104
74
  await api.sendText(continuationTarget, chunk);
@@ -121,10 +91,8 @@ async function finalize(api, target, preview, text, seq) {
121
91
  await api.sendText(target, text);
122
92
  }
123
93
  /**
124
- * Mount one preview message: preferably a streaming card entity (`initial` seeds the process element;
125
- * the answer element starts empty), with a static text message as the visible fallback. Queue feedback
126
- * and ordinary turn startup share this constructor so a queued card has exactly the same shape the
127
- * stream pump expects to take over later.
94
+ * Mount one preview message: preferably a streaming card entity (`initial` seeds the process element; the answer
95
+ * element starts empty), with a static text message as the visible fallback.
128
96
  */
129
97
  export async function mountFeishuPreview(api, target, initial, label = "[feishu]") {
130
98
  try {
@@ -134,10 +102,7 @@ export async function mountFeishuPreview(api, target, initial, label = "[feishu]
134
102
  ? api.replyMessage(target.replyTo, "interactive", content, { replyInThread: target.replyInThread })
135
103
  : api.sendMessage(target.chatId, "interactive", content);
136
104
  let messageId;
137
- // Field-observed: the mount can reject a JUST-minted card id (code 230099 / "cardid is invalid")
138
- // — the entity is not yet visible to the IM side (eventual consistency between cardkit and IM).
139
- // That specific rejection gets a short backoff and another try before degrading; anything else
140
- // degrades immediately.
105
+ // Field-observed: the mount can reject a JUST-minted card id (code 230099 / "cardid is invalid").
141
106
  for (let attempt = 1;; attempt++) {
142
107
  try {
143
108
  messageId = await mountOnce();
@@ -155,8 +120,8 @@ export async function mountFeishuPreview(api, target, initial, label = "[feishu]
155
120
  return { kind: "card", cardId, messageId };
156
121
  }
157
122
  catch (e) {
158
- // Card tier failed — degrade to a text placeholder with NO live updates (the text tier's 20-edit
159
- // cap is spent on terminal writes only). Visible: the operator learns why the preview is static.
123
+ // Card tier failed — degrade to a text placeholder with NO live updates (the text tier's 20-edit cap is spent on
124
+ // terminal writes only).
160
125
  log.warn(`${label} streaming card unavailable — live preview degrades to a static placeholder: ${String(e)}`);
161
126
  const messageId = target.replyTo !== undefined
162
127
  ? await api.replyMessage(target.replyTo, "text", JSON.stringify({ text: initial }), {
@@ -168,163 +133,99 @@ export async function mountFeishuPreview(api, target, initial, label = "[feishu]
168
133
  return { kind: "text", messageId };
169
134
  }
170
135
  }
171
- /** Settle an already-mounted queue preview without starting an Agent stream (the poison/defer paths).
172
- * Card and text tiers both change in place; only a missing/failed preview sends a fresh message. */
136
+ /**
137
+ * Settle a preview without starting an Agent stream: a queue/poison/defer notice, or a recovered answer whose own
138
+ * preview died with the run that produced it (`undefined` — then it is one fresh message).
139
+ */
173
140
  export async function settleFeishuPreview(api, target, preview, text) {
174
141
  let sequence = 0;
175
142
  await finalize(api, target, preview ?? { kind: "none" }, text, () => ++sequence);
176
143
  }
177
144
  /**
178
- * Consume one turn's event stream into a Feishu-compatible chat, live (see the module header for the preview
179
- * model). Preview updates are best-effort (logged once if they fail); the final write is authoritative
180
- * and surfaces a real failure (bad credentials, etc.). `initialPreview`, when present, is the queued
181
- * turn's already-mounted card/text message: the pump and terminal write mutate that same message rather
182
- * than recalling it and posting another reply.
145
+ * Consume one turn's event stream into a Feishu-compatible chat, live.
183
146
  */
184
- export async function streamFeishuReply(events, api, target, formatError, initialPreview, label = "[feishu]") {
185
- // Event → view-state reduction is the shared machine (preview-kit); this renderer owns the reveal
186
- // policy, the card-budget caps, and delivery below. The card is TWO elements (card.ts): the process
187
- // block's head changes every frame (sliding thinking tail, `…`→`✓` flips), so it must never share
188
- // an element with the answer — the client would re-type the whole card from the divergence point
189
- // once a second. Each view feeds its own element; only the changed one is written.
190
- const turn = createTurnView();
191
- const processView = () => {
192
- const v = composeTurnBody([
193
- thinkingLine(turn, THINKING_PREVIEW),
194
- toolLines(turn),
195
- turn.retrying ? RETRY_NOTICE : "",
196
- ]);
197
- if (v !== "")
198
- return tailLines(v, PROCESS_MAX_POINTS);
199
- // No process content: the placeholder covers only the silence BEFORE the answer reveals — once
200
- // the answer is streaming, an empty block goes (stays) empty; "Thinking…" pinned above a live
201
- // answer would misstate the phase. The empty frame is a real write: it clears a mounted
202
- // placeholder. (The block cannot otherwise flicker: thinking and tools only grow — only the
203
- // retry notice toggles, and its empty state resolves through this same rule.)
204
- return revealedAnswer(turn, STREAM_THROTTLE_MS).trim() === "" ? THINKING_PLACEHOLDER : "";
205
- };
206
- const answerView = () => capBytes(revealedAnswer(turn, STREAM_THROTTLE_MS), CARD_MARKDOWN_MAX_BYTES);
207
- // The live preview is ONE message: either the queue card/text handed in by the wiring, or a preview
208
- // mounted lazily on this turn's first flush. `sequence` must increase strictly per card — the single-
209
- // writer pump guarantees it by construction. A queue card has had no updates yet, so sequence starts
210
- // at zero in both paths.
211
- let preview = initialPreview ?? { kind: "none" };
212
- let setupAttempted = initialPreview !== undefined;
213
- let sequence = 0;
214
- const nextSeq = () => ++sequence;
215
- let streamDead = false; // the platform closed streaming (idle timeout) — freeze the live view
216
- let finalized = false; // a terminal write (completed/failed) ran — the finally skips its orphan cleanup
217
- let lastProcess = "";
218
- let lastAnswer = "";
219
- const flushPreview = async () => {
220
- const process = processView();
221
- if (!setupAttempted) {
222
- setupAttempted = true;
223
- // The mount seeds the process element with the current view; the answer element starts empty
224
- // (card.ts), so the first answer snapshot is a clean prefix extension.
225
- preview = await mountFeishuPreview(api, target, process, label);
226
- lastProcess = process;
227
- return;
228
- }
229
- if (preview.kind !== "card" || streamDead)
230
- return; // text tier / dead stream: frozen until the terminal write
231
- try {
232
- // `last*` advances BEFORE each write: a frame that fails for a non-streaming reason is logged
233
- // once (the pump's onError) and not re-sent until its content actually changes. An EMPTY
234
- // process frame is written like any other — it is the placeholder being cleared (processView).
235
- if (process !== lastProcess) {
147
+ export function feishuReply(events, api, target, formatError, initialPreview, label = "[feishu]", onAnswered) {
148
+ return Effect.gen(function* () {
149
+ const clock = yield* Clock.Clock;
150
+ const now = () => clock.currentTimeMillisUnsafe();
151
+ // Event → view-state reduction is the shared machine (preview-kit); this renderer owns the reveal policy, the
152
+ // card-budget caps, and delivery below.
153
+ const turn = createTurnView();
154
+ const processView = () => {
155
+ const v = composeTurnBody([
156
+ thinkingLine(turn, THINKING_PREVIEW),
157
+ toolLines(turn),
158
+ turn.retrying ? RETRY_NOTICE : "",
159
+ ]);
160
+ if (v !== "")
161
+ return tailLines(v, PROCESS_MAX_POINTS);
162
+ // No process content: the placeholder covers only the silence BEFORE the answer reveals.
163
+ return revealedAnswer(turn, STREAM_THROTTLE_MS, now()).trim() === "" ? THINKING_PLACEHOLDER : "";
164
+ };
165
+ const answerView = () => capBytes(revealedAnswer(turn, STREAM_THROTTLE_MS, now()), CARD_MARKDOWN_MAX_BYTES);
166
+ // The live preview is ONE message: either the queue card/text handed in by the wiring, or a preview mounted
167
+ // lazily on this turn's first flush.
168
+ let preview = initialPreview ?? { kind: "none" };
169
+ let setupAttempted = initialPreview !== undefined;
170
+ let sequence = 0;
171
+ const nextSeq = () => ++sequence;
172
+ let streamDead = false; // the platform closed streaming (idle timeout) — freeze the live view
173
+ let lastProcess = "";
174
+ let lastAnswer = "";
175
+ const flushPreview = async () => {
176
+ const process = processView();
177
+ if (!setupAttempted) {
178
+ setupAttempted = true;
179
+ // The mount seeds the process element with the current view; the answer element starts empty (card.ts), so
180
+ // the first answer snapshot is a clean prefix extension.
181
+ preview = await mountFeishuPreview(api, target, process, label);
236
182
  lastProcess = process;
237
- await api.updateCardElement(preview.cardId, PROCESS_ELEMENT_ID, process, nextSeq());
238
- }
239
- const answer = answerView();
240
- // Never write an empty answer snapshot — the element is born empty and the answer only grows.
241
- if (answer !== "" && answer !== lastAnswer) {
242
- lastAnswer = answer;
243
- await api.updateCardElement(preview.cardId, ANSWER_ELEMENT_ID, answer, nextSeq());
244
- }
245
- }
246
- catch (e) {
247
- if (isCardStreamingClosed(e)) {
248
- // The platform closed streaming (idle timeout). Freeze the live view; the settle write replaces
249
- // the whole entity (streaming off) and still lands.
250
- streamDead = true;
251
- log.warn(`${label} card streaming closed mid-turn — preview frozen; the final answer still lands`);
252
- return;
253
- }
254
- throw e;
255
- }
256
- };
257
- // The shared single-writer pump (preview-kit) serializes snapshots to the one preview — which also
258
- // guarantees the card's strictly-increasing `sequence` lands in order (no concurrent frames).
259
- const { touch, finish } = createPreviewPump({
260
- flush: flushPreview,
261
- throttleMs: STREAM_THROTTLE_MS,
262
- onError: (e) => log.warn(`${label} live preview failed (final reply still sends): ${String(e)}`),
263
- });
264
- touch(); // mount the "💭 Thinking…" preview immediately
265
- /** Terminal write, whatever tier the preview reached. */
266
- const settle = async (text) => {
267
- await finalize(api, target, preview, text, nextSeq);
268
- };
269
- try {
270
- for await (const e of events) {
271
- if (e.type === "completed") {
272
- await finish();
273
- // Settle the preview into the final answer; the persisted card is the answer alone — the
274
- // process (thinking/tools) was preview-only. Mark finalized BEFORE delivering: the terminal was
275
- // reached, so a delivery failure here is a plain failure, not an "abnormal exit" (which would
276
- // wrongly fire the finally's neutral-notice fallback = double delivery + wrong text).
277
- finalized = true;
278
- await settle(turn.answer.trim() !== "" ? turn.answer : "(no reply)");
279
183
  return;
280
184
  }
281
- if (e.type === "failed") {
282
- await finish();
283
- // Two audiences: the chat (customer-facing — formatError, neutral by default) and the operator
284
- // log (dev-facing — the full details, via the throw below + the handler's catch). Same terminal
285
- // write as completed; an empty notice deletes the preview (suppress = no residue). Best-effort —
286
- // we throw below regardless.
287
- finalized = true;
288
- {
289
- const msg = formatError({
290
- details: e.details,
291
- retryable: e.retryable,
292
- ...(e.code !== undefined ? { code: e.code } : {}),
293
- }) ?? "";
294
- try {
295
- await settle(msg);
296
- }
297
- catch (deliveryError) {
298
- // Preserve the Agent failure as the primary error below, but keep the broken final hop in
299
- // the operator-visible chain — otherwise the log falsely implies the user saw the notice.
300
- log.error(`${label} failed to deliver the agent-failure notice: ${String(deliveryError)}`);
301
- }
302
- }
303
- throw new Error(`agent failed: ${e.details} (retryable=${e.retryable})`);
304
- }
305
- if (applyTurnEvent(turn, e))
306
- touch();
307
- }
308
- throw new Error("stream ended without a terminal event"); // violates SPEC MUST 1
309
- }
310
- finally {
311
- await finish();
312
- // Abnormal exit (stream ended without a terminal, the generator threw, or the consumer abandoned):
313
- // no terminal write ran. Show the SAME neutral notice a `failed` event would — the preview may show
314
- // real partial work, so don't delete it silently, and don't leave the user in silence. A suppressing
315
- // onError still collapses to a delete (finalize on empty text).
316
- if (!finalized) {
317
- // retryable:false — an abnormal end (no terminal / a throw) is of UNKNOWN retryability, so use the
318
- // neutral "something went wrong" default rather than promising "try again" that may not help.
319
- const notice = formatError({ details: "the turn ended without completing", retryable: false }) ?? "";
185
+ if (preview.kind !== "card" || streamDead)
186
+ return; // text tier / dead stream: frozen until the terminal write
320
187
  try {
321
- await settle(notice);
188
+ // `last*` advances BEFORE each write: a frame that fails for a non-streaming reason is logged once (the
189
+ // pump's onError) and not re-sent until its content actually changes.
190
+ const answer = answerView();
191
+ // Never write an empty answer snapshot — the element is born empty and the answer only grows.
192
+ if (answer !== "" && answer !== lastAnswer) {
193
+ lastAnswer = answer;
194
+ await api.updateCardElement(preview.cardId, ANSWER_ELEMENT_ID, answer, nextSeq(), DROPPABLE_FRAME);
195
+ }
196
+ if (process !== lastProcess) {
197
+ lastProcess = process;
198
+ await api.updateCardElement(preview.cardId, PROCESS_ELEMENT_ID, process, nextSeq(), DROPPABLE_FRAME);
199
+ }
322
200
  }
323
- catch (deliveryError) {
324
- // The stream's original throw remains primary; this explicit line records that the user-facing
325
- // terminal notice failed too instead of silently breaking the responsibility chain.
326
- log.error(`${label} failed to deliver the abnormal-turn notice: ${String(deliveryError)}`);
201
+ catch (e) {
202
+ if (isCardStreamingClosed(e)) {
203
+ // The platform closed streaming (idle timeout).
204
+ streamDead = true;
205
+ log.warn(`${label} card streaming closed mid-turn — preview frozen; the final answer still lands`);
206
+ return;
207
+ }
208
+ throw e;
327
209
  }
328
- }
329
- }
210
+ };
211
+ // The scoped single writer keeps card sequences ordered through the terminal update.
212
+ const { touch, finish } = yield* previewPump({
213
+ flush: flushPreview,
214
+ throttleMs: STREAM_THROTTLE_MS,
215
+ onError: (e) => log.warn(`${label} live preview failed (final reply still sends): ${String(e)}`),
216
+ });
217
+ touch(); // mount the "💭 Thinking…" preview immediately
218
+ yield* renderReply(events, {
219
+ label,
220
+ finish,
221
+ formatError,
222
+ onEvent: (event) => {
223
+ if (applyTurnEvent(turn, event, now()))
224
+ touch();
225
+ },
226
+ answer: () => (turn.answer.trim() !== "" ? turn.answer : "(no reply)"),
227
+ ...(onAnswered ? { onAnswered } : {}),
228
+ settle: (text) => portJoin(() => finalize(api, target, preview, text, nextSeq)),
229
+ });
230
+ });
330
231
  }
@@ -1,25 +1,13 @@
1
1
  /**
2
- * One-click app creation ("scan to create") — the OAuth 2.0 Device Authorization Grant (RFC 8628)
3
- * flow the platform provides for agent apps: `begin` returns a one-time verification URL the user
4
- * opens in Feishu/Lark and confirms (the platform pre-configures the agent app template: bot
5
- * capability, messaging scopes, event subscriptions); polling returns the new app's credentials.
6
- *
7
- * Hand-rolled on fetch, no SDK: the wire protocol is two form-encoded POSTs to the accounts endpoint
8
- * plus RFC 8628's polling error dance — shared verbatim by all four official SDKs (node/python/java/go),
9
- * which makes it a de-facto stable surface even though only the SDKs document it. Provenance:
10
- * larksuite/node-sdk `scene/registration` (registerApp). If the platform ever moves this behind
11
- * something non-trivial (signed payloads, websockets), adopt the official SDK instead of chasing it —
12
- * the same tripwire as feishu-api.ts.
13
- *
14
- * The scanning user's tenant decides the brand: a Lark-tenant user flips polling to the Lark accounts
15
- * domain mid-flow (`tenant_brand: "lark"`), and the result carries the brand so the caller can point
16
- * everything else (API origin) at the right cloud.
2
+ * One-click app creation ("scan to create") — the OAuth 2.0 Device Authorization Grant (RFC 8628) flow the platform
3
+ * provides for agent apps: `begin` returns a one-time verification URL the user opens in Feishu/Lark and confirms,
4
+ * then polling returns the new app's credentials. Hand-rolled on fetch because the wire protocol (two form-encoded
5
+ * POSTs plus RFC 8628's polling error dance) is shared verbatim by all four official SDKs; if the platform ever moves
6
+ * it behind something non-trivial, adopt the official SDK rather than chasing it.
17
7
  */
18
8
  /**
19
- * Additive app config carried on the confirm-page URL (`addons` query param): extra scopes/events
20
- * merged ON TOP of the platform's agent template — base permissions can never be removed. Shape and
21
- * encoding (JSON → gzip → base64url) follow the official SDKs (provenance: node-sdk
22
- * scene/registration); item names unknown to the platform catalog are silently dropped by the page.
9
+ * Additive app config carried on the confirm-page URL (`addons` query param): extra scopes/events merged ON TOP of the
10
+ * platform's agent template.
23
11
  */
24
12
  interface FeishuAppAddons {
25
13
  scopes?: {
@@ -48,7 +36,6 @@ export interface RegisterFeishuAppOptions {
48
36
  url: string;
49
37
  expiresInS: number;
50
38
  }) => void;
51
- /** Cancel the polling. */
52
39
  signal?: AbortSignal;
53
40
  /** Accounts origins, for tests. */
54
41
  accountsBaseUrl?: string;
@@ -63,9 +50,7 @@ export interface RegisteredFeishuApp {
63
50
  openId?: string;
64
51
  }
65
52
  /**
66
- * Run the scan-to-create flow (module header): begin → hand the verification URL to the caller →
67
- * poll until the user confirms. Resolves with the new app's credentials; rejects on denial, expiry,
68
- * abort, or a transport failure — every rejection is a plain Error whose message says what to do.
53
+ * Run the scan-to-create flow: begin → hand the verification URL to the caller → poll until the user confirms.
69
54
  */
70
55
  export declare function registerFeishuApp(options: RegisterFeishuAppOptions): Promise<RegisteredFeishuApp>;
71
56
  export {};
@@ -1,19 +1,9 @@
1
1
  /**
2
- * One-click app creation ("scan to create") — the OAuth 2.0 Device Authorization Grant (RFC 8628)
3
- * flow the platform provides for agent apps: `begin` returns a one-time verification URL the user
4
- * opens in Feishu/Lark and confirms (the platform pre-configures the agent app template: bot
5
- * capability, messaging scopes, event subscriptions); polling returns the new app's credentials.
6
- *
7
- * Hand-rolled on fetch, no SDK: the wire protocol is two form-encoded POSTs to the accounts endpoint
8
- * plus RFC 8628's polling error dance — shared verbatim by all four official SDKs (node/python/java/go),
9
- * which makes it a de-facto stable surface even though only the SDKs document it. Provenance:
10
- * larksuite/node-sdk `scene/registration` (registerApp). If the platform ever moves this behind
11
- * something non-trivial (signed payloads, websockets), adopt the official SDK instead of chasing it —
12
- * the same tripwire as feishu-api.ts.
13
- *
14
- * The scanning user's tenant decides the brand: a Lark-tenant user flips polling to the Lark accounts
15
- * domain mid-flow (`tenant_brand: "lark"`), and the result carries the brand so the caller can point
16
- * everything else (API origin) at the right cloud.
2
+ * One-click app creation ("scan to create") — the OAuth 2.0 Device Authorization Grant (RFC 8628) flow the platform
3
+ * provides for agent apps: `begin` returns a one-time verification URL the user opens in Feishu/Lark and confirms,
4
+ * then polling returns the new app's credentials. Hand-rolled on fetch because the wire protocol (two form-encoded
5
+ * POSTs plus RFC 8628's polling error dance) is shared verbatim by all four official SDKs; if the platform ever moves
6
+ * it behind something non-trivial, adopt the official SDK rather than chasing it.
17
7
  */
18
8
  import { gzipSync } from "node:zlib";
19
9
  /** Feishu accounts endpoint (the flow starts here for every user; a Lark-tenant scan switches over). */
@@ -32,8 +22,7 @@ function encodeAddons(addons) {
32
22
  }
33
23
  /** RFC 8628 device-flow states whose non-2xx JSON bodies belong to the polling state machine. */
34
24
  const DEVICE_FLOW_ERRORS = new Set(["authorization_pending", "slow_down", "access_denied", "expired_token"]);
35
- /** One registration POST (form-encoded). RFC 8628 delivers polling states (authorization_pending,
36
- * slow_down, …) as HTTP 400 with a JSON body — those parse as data, not as transport failures. */
25
+ /** One registration POST (form-encoded). */
37
26
  async function post(baseUrl, params) {
38
27
  let res;
39
28
  let raw;
@@ -67,9 +56,7 @@ async function post(baseUrl, params) {
67
56
  /** Sleep on the GLOBAL timer (not `node:timers/promises`) so tests can drive it with fake timers. */
68
57
  const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
69
58
  /**
70
- * Run the scan-to-create flow (module header): begin → hand the verification URL to the caller →
71
- * poll until the user confirms. Resolves with the new app's credentials; rejects on denial, expiry,
72
- * abort, or a transport failure — every rejection is a plain Error whose message says what to do.
59
+ * Run the scan-to-create flow: begin → hand the verification URL to the caller → poll until the user confirms.
73
60
  */
74
61
  export async function registerFeishuApp(options) {
75
62
  const feishuBase = options.accountsBaseUrl ?? FEISHU_ACCOUNTS;
@@ -1,13 +1,6 @@
1
1
  import { type RegistrationOutcome } from "../registration.ts";
2
2
  import { type FeishuCloudKind } from "./cloud.ts";
3
- /**
4
- * Register `<baseUrl>/<kind>` as the app's event Request URL (webhook mode). Missing credentials print
5
- * the manual instruction instead of failing. `opts` carries the attempt budget — `--tunnel` takes the
6
- * default, `deploy --run` passes `DEPLOY_REGISTRATION_ATTEMPTS` (a host starts slower than a tunnel) —
7
- * plus `apiBase`, which is a test's fake platform (production derives it from the kind).
8
- *
9
- * Reports its outcome as a {@link RegistrationOutcome} fact; gating policy belongs to the caller.
10
- */
3
+ /** Register `<baseUrl>/<kind>` as the app's event Request URL (webhook mode). */
11
4
  interface FeishuManualRegistration {
12
5
  consoleUrl: string;
13
6
  requestUrl: string;
@@ -16,7 +9,7 @@ export interface RegisterFeishuWebhookOptions {
16
9
  attempts?: number;
17
10
  retryMs?: number;
18
11
  apiBase?: string;
19
- /** Manual fallback after a definitive config error or exhausted retries. Local dev opens this App. */
12
+ /** Manual fallback after a definitive config error or exhausted retries. */
20
13
  onManualRegistration?: (info: FeishuManualRegistration) => void;
21
14
  }
22
15
  export declare function registerFeishuWebhook(baseUrl: string, kind: FeishuCloudKind, opts?: RegisterFeishuWebhookOptions): Promise<RegistrationOutcome>;
@@ -1,24 +1,6 @@
1
1
  /**
2
- * Feishu/Lark event-URL registration — the platform-domain step both `--tunnel` (dev, tunnel.ts) and
3
- * `deploy … --run` (the host runners' post-deploy step) invoke, once per mounted kind. What
4
- * "registering feishu/lark" means lives here, beside the engine it serves; it reads the same .env
5
- * credentials the channel of that kind uses (`FEISHU_*` / `LARK_*`).
6
- *
7
- * Mechanism: the application-v7 config PATCH (`updateEventSubscription`) flips the app's event
8
- * subscription to webhook mode and points it at `<baseUrl>/<kind>`. Two properties make this the full
9
- * telegram-setWebhook parity: the platform applies a request-URL change IMMEDIATELY (no version
10
- * publish), and it VERIFIES the URL with a url_verification challenge during the PATCH — which makes
11
- * the PATCH its own readiness probe, retried below. (It used to poll `<baseUrl>/health` from here
12
- * first; that asked whether THIS machine could reach a freshly minted hostname, which is routinely
13
- * false for a minute or more while the platform reaches it fine — #421.) Requires the
14
- * `application:application:patch` scope (field-tested: `self_manage` does NOT cover this PATCH) —
15
- * `add feishu` requests it at creation via addons; without it the PATCH fails visibly and the manual
16
- * console instruction is printed.
17
- *
18
- * CLOUD LAG: the application-v7 config API exists on open.feishu.cn but (as of 2026-07) is NOT
19
- * deployed on open.larksuite.com — the route 404s there. The registrar still attempts it (the day the
20
- * platform ships it, registration starts working with no change here) and names the real cause in the
21
- * fallback instead of blaming the app's scopes.
2
+ * Feishu/Lark event-URL registration — the platform-domain step both `--tunnel` (dev, tunnel.ts) and `deploy … --run`
3
+ * (the host runners' post-deploy step) invoke, once per mounted kind.
22
4
  */
23
5
  import { log } from "../../log.js";
24
6
  import { retryWhile } from "../registration.js";
@@ -52,10 +34,7 @@ export async function registerFeishuWebhook(baseUrl, kind, opts = {}) {
52
34
  log.warn(`[fastagent] ${kind}: could not open Events & Callbacks: ${String(callbackError)}`);
53
35
  }
54
36
  };
55
- // The PATCH is the probe (same lesson as the token bootstrap): the platform verifies request_url
56
- // with a challenge DURING the call, so its 210042 "request_url validation failed" is the readiness
57
- // signal and is retried with backoff, alongside transient network errors. Only a permanent config
58
- // error (missing scope, app under review, the intl 404) is reported once with the manual path.
37
+ // The PATCH is the probe (same lesson as the token bootstrap).
59
38
  try {
60
39
  await retryWhile(() => api.updateEventSubscription(appId, { subscriptionType: "webhook", requestUrl }), isTransientFeishuRegistrationError, {
61
40
  attempts: opts.attempts,
@@ -63,25 +42,22 @@ export async function registerFeishuWebhook(baseUrl, kind, opts = {}) {
63
42
  onRetry: ({ attempt, attempts }) => log.info(`[fastagent] ${kind}: the platform cannot verify ${requestUrl} yet (attempt ${attempt}/${attempts}); retrying…`),
64
43
  });
65
44
  log.info(`[fastagent] ${kind}: event Request URL registered → ${requestUrl}`);
66
- // Field-tested: a URL change applies immediately, but the MODE flip (the template's long
67
- // connection → webhook) only takes effect when a version is published — the dispatcher serves
68
- // the published snapshot, and version publishing has no open API. One console click, once.
45
+ // Field-tested: a URL change applies immediately, but the MODE flip (the template's long connection → webhook)
46
+ // only takes effect when a version is published.
69
47
  log.info(`[fastagent] ${kind}: if messages do not arrive, publish a version (one click, prompted) — the switch to webhook mode takes effect on publish: ${versionUrl}`);
70
48
  return "registered";
71
49
  }
72
50
  catch (e) {
73
- // A 404 on the config route is the CLOUD lagging, not this app's configuration: the v7 API is
74
- // live on open.feishu.cn but not yet on open.larksuite.com. Name that — "check your scopes"
75
- // would send the operator hunting for a problem they cannot fix. It stays WARN: on that cloud the
76
- // manual path is the known norm, not an exceptional failure.
51
+ // A 404 on the config route is the CLOUD lagging, not this app's configuration: the v7 API is live on
52
+ // open.feishu.cn but not yet on open.larksuite.com.
77
53
  if (isFeishuConfigApiMissing(e)) {
78
54
  log.warn(`[fastagent] ${kind}: this cloud (${apiBase}) returned HTTP 404 for the app-config API — ` +
79
55
  `manual registration is required`);
80
56
  manualRegistration();
81
57
  return "manual"; // that cloud has no config API — the manual path is the norm there
82
58
  }
83
- // Exhausted retries end in the same state as a permanent config error (event URL not registered,
84
- // manual action required) — report at the same level.
59
+ // Exhausted retries end in the same state as a permanent config error (event URL not registered, manual action
60
+ // required) — report at the same level.
85
61
  log.error(isTransientFeishuRegistrationError(e)
86
62
  ? `[fastagent] ${kind}: the platform could not verify ${requestUrl} after retries — manual registration is required`
87
63
  : `[fastagent] ${kind}: could not register the event URL (${String(e)}). ` +