@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,4 +1,5 @@
1
1
  import { feishuChannel } from "@fastagent-sh/fastagent/feishu";
2
+ import { defineChannel } from "@fastagent-sh/fastagent";
2
3
 
3
4
  // feishuChannel is fastagent's canonical Feishu adapter (verify + run + reply), configured with YOUR
4
5
  // policy. fastagent discovers this file, mounts POST /feishu, and pipes the agent + state home to it.
@@ -20,23 +21,30 @@ import { feishuChannel } from "@fastagent-sh/fastagent/feishu";
20
21
  // 5. create a version and publish the app (a Feishu admin approves it), then add the bot to a chat
21
22
  // `fastagent add feishu` already did steps 1-3 for you (scan-to-create); this walkthrough is for a
22
23
  // hand-made app or for auditing what the scan configured.
23
- export default feishuChannel({
24
- appId: process.env.FEISHU_APP_ID ?? "", // missing → fails at startup (no replies could be sent)
25
- appSecret: process.env.FEISHU_APP_SECRET ?? "",
26
- verificationToken: process.env.FEISHU_VERIFICATION_TOKEN ?? "", // authenticates inbound events
27
- encryptKey: process.env.FEISHU_ENCRYPT_KEY || undefined, // optional; when set, plaintext events are refused
28
- // No session modes: a chat is one session and a thread is another, and where the answer goes follows
29
- // from that (docs/design/participant-model.md).
30
- // Dev/personal bot: surface raw errors to the chat so you (and your AI agent) can act on them. The
31
- // chat is customer-facing by default — for a public bot, drop this or return a neutral string;
32
- // full details always go to the server log regardless.
33
- onError: (failed) => `⚠️ ${failed.details}`,
34
- // The channel owns transport + format (markdown card) + attachments (image→vision, file→disk) +
35
- // the live streaming preview. `route` (POLICY) is OPTIONAL — omitted, it uses defaultFeishuRoute:
36
- // p2p chats always answer; groups answer on @this-bot, plus bare messages in a thread where the
37
- // Agent takes part and exactly ONE human does. Other human group/thread discussion buffers until
38
- // that place's next answered turn; @other-only messages buffer rather than triggering the Agent.
39
- // Override to customise explicit routing, reusing the export:
40
- // route: (e) => defaultFeishuRoute(e, { botOpenId: "ou_xxx" }) && { session: `user:${e.sender?.sender_id?.open_id}` },
41
- // route: (e) => defaultFeishuRoute(e, { botOpenId: "ou_xxx" }) && { text: `${feishuEnvelope(e)}\n[extra]` },
24
+ export default defineChannel({
25
+ // The env vars this channel needs. fastagent carries them to a deployed box and refuses to serve
26
+ // while one is unset — a bare process.env read gets neither guarantee. FEISHU_ENCRYPT_KEY is
27
+ // optional, so it is NOT declared here: an undeclared read stays undeclared on purpose.
28
+ secrets: ["FEISHU_APP_ID", "FEISHU_APP_SECRET", "FEISHU_VERIFICATION_TOKEN"],
29
+ channel: (secrets) =>
30
+ feishuChannel({
31
+ appId: secrets.FEISHU_APP_ID,
32
+ appSecret: secrets.FEISHU_APP_SECRET,
33
+ verificationToken: secrets.FEISHU_VERIFICATION_TOKEN, // authenticates inbound events
34
+ encryptKey: process.env.FEISHU_ENCRYPT_KEY || undefined, // optional; when set, plaintext events are refused
35
+ // No session modes: a chat is one session and a thread is another, and where the answer goes follows
36
+ // from that (docs/design/participant-model.md).
37
+ // Dev/personal bot: surface raw errors to the chat so you (and your AI agent) can act on them. The
38
+ // chat is customer-facing by default — for a public bot, drop this or return a neutral string;
39
+ // full details always go to the server log regardless.
40
+ onError: (failed) => `⚠️ ${failed.details}`,
41
+ // The channel owns transport + format (markdown card) + attachments (image→vision, file→disk) +
42
+ // the live streaming preview. `route` (POLICY) is OPTIONAL — omitted, it uses defaultFeishuRoute:
43
+ // p2p chats always answer; groups answer on @this-bot, plus bare messages in a thread where the
44
+ // Agent takes part and exactly ONE human does. Other human group/thread discussion buffers until
45
+ // that place's next answered turn; @other-only messages buffer rather than triggering the Agent.
46
+ // Override to customise explicit routing, reusing the export:
47
+ // route: (e) => defaultFeishuRoute(e, { botOpenId: "ou_xxx" }) && { session: `user:${e.sender?.sender_id?.open_id}` },
48
+ // route: (e) => defaultFeishuRoute(e, { botOpenId: "ou_xxx" }) && { text: `${feishuEnvelope(e)}\n[extra]` },
49
+ }),
42
50
  });
@@ -0,0 +1,31 @@
1
+ import { feishuWebSocketChannel } from "@fastagent-sh/fastagent/feishu";
2
+ import { defineChannel } from "@fastagent-sh/fastagent";
3
+
4
+ // Feishu 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: ["FEISHU_APP_ID", "FEISHU_APP_SECRET"],
12
+ channel: (secrets) =>
13
+ feishuWebSocketChannel({
14
+ appId: secrets.FEISHU_APP_ID,
15
+ appSecret: secrets.FEISHU_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 defaultFeishuRoute:
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) => defaultFeishuRoute(e, { botOpenId: "ou_xxx" }) && { session: `user:${e.sender?.sender_id?.open_id}` },
29
+ // route: (e) => defaultFeishuRoute(e, { botOpenId: "ou_xxx" }) && { text: `${feishuEnvelope(e)}\n[extra]` },
30
+ }),
31
+ });
@@ -1,28 +1,17 @@
1
1
  /** Feishu/Lark app-level event subscription choice used by onboarding and scaffolding. */
2
2
  export type FeishuSubscriptionMode = "webhook" | "websocket";
3
- /** Onboarding choice for group visibility. `context` needs the tenant-wide group-message scope; the
4
- * runtime remains capability-driven because the platform, not channel source, decides which events
5
- * are delivered. */
3
+ /** Onboarding choice for group visibility. */
6
4
  export type FeishuGroupBehavior = "context" | "mentions";
7
5
  /**
8
- * Configuring THIS app: what every v7 config PATCH needs — registering a webhook Request URL, and
9
- * adding a scope to the app draft during context-aware group setup.
10
- *
11
- * Requested at creation for BOTH ingress modes, though only webhook uses it on day one. A WebSocket
12
- * app that later wants webhook delivery cannot acquire it in passing: changing mode is a migration
13
- * the CLI deliberately refuses to perform (`resolveIngress`), so the app would have to be recreated
14
- * or the scope added by hand in the console after a failed PATCH. It is not a data scope — it reads
15
- * and writes this app's own configuration — so carrying it costs an app nothing and keeps the one
16
- * door open that the alternative nails shut.
6
+ * Configuring THIS app: what every v7 config PATCH needs — registering a webhook Request URL, and adding a scope to
7
+ * the app draft during context-aware group setup.
17
8
  */
18
9
  export declare const FEISHU_APP_CONFIG_SCOPE = "application:application:patch";
19
10
  /** The one event an agent app cannot serve without: an inbound message. */
20
11
  export declare const FEISHU_MESSAGE_RECEIVE_EVENT = "im.message.receive_v1";
21
12
  /**
22
- * What a created app carries on top of the platform's agent template, for EITHER ingress — the
23
- * `addons` merged onto the confirm page. Here rather than inline at the call site because it is a
24
- * policy decision (which capabilities an app is born with), and the CLI layer that requests it is
25
- * terminal wiring with no seam to test through.
13
+ * What a created app carries on top of the platform's agent template, for EITHER ingress — the `addons` merged onto
14
+ * the confirm page.
26
15
  */
27
16
  export declare function feishuAppAddons(): {
28
17
  scopes: {
@@ -36,32 +25,18 @@ export declare function feishuAppAddons(): {
36
25
  };
37
26
  /** The sensitive tenant scope behind both bare replies in the agent's threads and group context buffering. */
38
27
  export declare const FEISHU_GROUP_CONTEXT_SCOPE = "im:message.group_msg";
39
- /** Reading a QUOTED message by id, so an ask carries what it replies to (participant-model.md §8).
40
- * INDEPENDENT of the delivery scope above and of the group posture: the read runs in every chat type
41
- * (a p2p thread's opening ask, any quoted @mention in a group). It is also a softer dependency —
42
- * without it everything still works, and an unreadable quote degrades to a marker in the prompt.
43
- * Bundled into the context-aware request only because that path already needs an approval round; a
44
- * mention-only app wanting referents must add it by hand. */
28
+ /** Reading a QUOTED message by id, so an ask carries what it replies to (participant-model.md §8). */
45
29
  export declare const FEISHU_MESSAGE_READ_SCOPE = "im:message:readonly";
46
- /** A scope the onboarding asks for, plus any BROADER spelling that already satisfies it. One concept,
47
- * so adding a superset means editing the entry — not every call site that tests a scope. */
30
+ /** A scope the onboarding asks for, plus any BROADER spelling that already satisfies it. */
48
31
  export interface FeishuScopeRequest {
49
- /** What to add to the app draft when nothing satisfies it. Always counts as satisfying itself. */
32
+ /** What to add to the app draft when nothing satisfies it. */
50
33
  request: string;
51
- /** EXTRA spellings that also count — supersets. Optional; `request` is implicit. Modelled this way
52
- * so a request that can never be satisfied (a list omitting its own `request`) cannot be
53
- * written: onboarding would add the scope, the tenant would grant it, and it would still read as
54
- * missing forever. */
34
+ /** EXTRA spellings that also count — supersets. */
55
35
  supersets?: string[];
56
36
  }
57
37
  /** Whether `predicate` holds for any spelling that satisfies this request. */
58
38
  export declare function scopeSatisfied(entry: FeishuScopeRequest, predicate: (name: string) => boolean): boolean;
59
- /** Reading a quoted message: `im:message` is the read/write superset, so an app holding it can already
60
- * do so — checking only the readonly spelling would warn a correctly configured app forever and push
61
- * its author into a redundant approval round. Exported on its own because the serving-time capability
62
- * report asks the same question, and a second mechanism there is what this type exists to prevent. */
39
+ /** Reading a quoted message: `im:message` is the read/write superset, so an app holding it can already do so. */
63
40
  export declare const FEISHU_MESSAGE_READ_REQUEST: FeishuScopeRequest;
64
- /** What `--group-behavior context` REQUESTS in one approval round — not a dependency set. Only the
65
- * delivery scope is required for the context path; the read scope rides along because it shares the
66
- * round and its absence merely degrades quoted messages to a marker. */
41
+ /** What `--group-behavior context` REQUESTS in one approval round — not a dependency set. */
67
42
  export declare const FEISHU_CONTEXT_ONBOARDING_SCOPES: FeishuScopeRequest[];
@@ -1,22 +1,13 @@
1
1
  /**
2
- * Configuring THIS app: what every v7 config PATCH needs — registering a webhook Request URL, and
3
- * adding a scope to the app draft during context-aware group setup.
4
- *
5
- * Requested at creation for BOTH ingress modes, though only webhook uses it on day one. A WebSocket
6
- * app that later wants webhook delivery cannot acquire it in passing: changing mode is a migration
7
- * the CLI deliberately refuses to perform (`resolveIngress`), so the app would have to be recreated
8
- * or the scope added by hand in the console after a failed PATCH. It is not a data scope — it reads
9
- * and writes this app's own configuration — so carrying it costs an app nothing and keeps the one
10
- * door open that the alternative nails shut.
2
+ * Configuring THIS app: what every v7 config PATCH needs — registering a webhook Request URL, and adding a scope to
3
+ * the app draft during context-aware group setup.
11
4
  */
12
5
  export const FEISHU_APP_CONFIG_SCOPE = "application:application:patch";
13
6
  /** The one event an agent app cannot serve without: an inbound message. */
14
7
  export const FEISHU_MESSAGE_RECEIVE_EVENT = "im.message.receive_v1";
15
8
  /**
16
- * What a created app carries on top of the platform's agent template, for EITHER ingress — the
17
- * `addons` merged onto the confirm page. Here rather than inline at the call site because it is a
18
- * policy decision (which capabilities an app is born with), and the CLI layer that requests it is
19
- * terminal wiring with no seam to test through.
9
+ * What a created app carries on top of the platform's agent template, for EITHER ingress — the `addons` merged onto
10
+ * the confirm page.
20
11
  */
21
12
  export function feishuAppAddons() {
22
13
  return {
@@ -26,28 +17,18 @@ export function feishuAppAddons() {
26
17
  }
27
18
  /** The sensitive tenant scope behind both bare replies in the agent's threads and group context buffering. */
28
19
  export const FEISHU_GROUP_CONTEXT_SCOPE = "im:message.group_msg";
29
- /** Reading a QUOTED message by id, so an ask carries what it replies to (participant-model.md §8).
30
- * INDEPENDENT of the delivery scope above and of the group posture: the read runs in every chat type
31
- * (a p2p thread's opening ask, any quoted @mention in a group). It is also a softer dependency —
32
- * without it everything still works, and an unreadable quote degrades to a marker in the prompt.
33
- * Bundled into the context-aware request only because that path already needs an approval round; a
34
- * mention-only app wanting referents must add it by hand. */
20
+ /** Reading a QUOTED message by id, so an ask carries what it replies to (participant-model.md §8). */
35
21
  export const FEISHU_MESSAGE_READ_SCOPE = "im:message:readonly";
36
22
  /** Whether `predicate` holds for any spelling that satisfies this request. */
37
23
  export function scopeSatisfied(entry, predicate) {
38
24
  return [entry.request, ...(entry.supersets ?? [])].some(predicate);
39
25
  }
40
- /** Reading a quoted message: `im:message` is the read/write superset, so an app holding it can already
41
- * do so — checking only the readonly spelling would warn a correctly configured app forever and push
42
- * its author into a redundant approval round. Exported on its own because the serving-time capability
43
- * report asks the same question, and a second mechanism there is what this type exists to prevent. */
26
+ /** Reading a quoted message: `im:message` is the read/write superset, so an app holding it can already do so. */
44
27
  export const FEISHU_MESSAGE_READ_REQUEST = {
45
28
  request: FEISHU_MESSAGE_READ_SCOPE,
46
29
  supersets: ["im:message"],
47
30
  };
48
- /** What `--group-behavior context` REQUESTS in one approval round — not a dependency set. Only the
49
- * delivery scope is required for the context path; the read scope rides along because it shares the
50
- * round and its absence merely degrades quoted messages to a marker. */
31
+ /** What `--group-behavior context` REQUESTS in one approval round — not a dependency set. */
51
32
  export const FEISHU_CONTEXT_ONBOARDING_SCOPES = [
52
33
  { request: FEISHU_GROUP_CONTEXT_SCOPE },
53
34
  FEISHU_MESSAGE_READ_REQUEST,
@@ -22,7 +22,6 @@ export interface FeishuWsConnectionOptions {
22
22
  /** Internal SDK seam for deterministic tests. */
23
23
  createClient?: CreateFeishuWsClient;
24
24
  }
25
- /** Open Feishu/Lark's official-SDK WebSocket connection. The SDK ACKs only after `onEvent` settles;
26
- * a persistence throw therefore becomes a 500 response frame and the platform re-pushes the event. */
25
+ /** Open Feishu/Lark's official-SDK WebSocket connection. */
27
26
  export declare function connectFeishuWs(options: FeishuWsConnectionOptions, signal: AbortSignal): LongConnection;
28
27
  export {};
@@ -9,8 +9,8 @@ function sdkLogger(label) {
9
9
  };
10
10
  }
11
11
  async function productionClient(options, callbacks) {
12
- // Webhook-only users stay on the lightweight fetch path; load the proprietary-protocol SDK only
13
- // when a WebSocket connection is actually opened.
12
+ // Webhook-only users stay on the lightweight fetch path; load the proprietary-protocol SDK only when a WebSocket
13
+ // connection is actually opened.
14
14
  const { EventDispatcher, LoggerLevel, WSClient } = await import("@larksuiteoapi/node-sdk");
15
15
  const label = `[${options.kind}:ws]`;
16
16
  const logger = sdkLogger(label);
@@ -36,8 +36,7 @@ async function productionClient(options, callbacks) {
36
36
  close: () => client.close(),
37
37
  };
38
38
  }
39
- /** Open Feishu/Lark's official-SDK WebSocket connection. The SDK ACKs only after `onEvent` settles;
40
- * a persistence throw therefore becomes a 500 response frame and the platform re-pushes the event. */
39
+ /** Open Feishu/Lark's official-SDK WebSocket connection. */
41
40
  export function connectFeishuWs(options, signal) {
42
41
  const label = `[${options.kind}:ws]`;
43
42
  // The SDK only logs + returns for this case (no onError), which would leave readiness pending forever.
@@ -63,8 +62,7 @@ export function connectFeishuWs(options, signal) {
63
62
  return;
64
63
  // Settle-then-close: closedSettled first makes a close()-triggered SDK callback re-entry a no-op.
65
64
  closedSettled = true;
66
- // Terminal failure must release the transport here: the abort listener's close() no-ops once
67
- // closedSettled is set, and only close() destroys SDK-held resources (e.g. its cache sweep timer).
65
+ // Terminal failure must release the transport here.
68
66
  if (client)
69
67
  closeClient(client);
70
68
  const failure = error instanceof Error ? error : new Error(String(error));
@@ -112,8 +110,8 @@ export function connectFeishuWs(options, signal) {
112
110
  if (client)
113
111
  closeClient(client);
114
112
  if (!readySettled) {
115
- // Abort before the first connection: `ready` still settles, and resolution here means
116
- // cancellation, not readiness (the LongConnection contract; serve skips ready-side effects).
113
+ // Abort before the first connection: `ready` still settles, and resolution here means cancellation, not
114
+ // readiness (the LongConnection contract; serve skips ready-side effects).
117
115
  readySettled = true;
118
116
  resolveReady();
119
117
  }
@@ -1,23 +1,23 @@
1
1
  /**
2
- * HTTP/SSE channel: fan one invoke stream out to Server-Sent Events.
3
- *
4
- * The handler is Fetch-shaped (`(Request) => Promise<Response>`) — the cross-runtime form every
5
- * embedding host speaks, so it mounts inside an existing app's own route. It is path-agnostic. The
6
- * web stream primitives give cancellation (consumer disconnect → cancel() → iterator.return() →
7
- * invoke cancellation, SPEC MUST 3), backpressure (pull-based), and the body cap natively.
8
- *
9
- * Serving it is somebody else's job: the node:http bridge and the route table belong to the HOST
10
- * (`./serve.ts`). This file knows only the Agent contract and the wire shape of one stream.
2
+ * HTTP/SSE channel: fan one invoke stream out to Server-Sent Events. The web stream primitives supply cancellation
3
+ * natively (consumer disconnect → `cancel()` → `iterator.return()` → invoke cancellation, SPEC MUST 3), backpressure,
4
+ * and the body cap.
11
5
  */
12
6
  import type { Agent } from "../agent.ts";
13
- /** Request body cap (1 MiB) — shared by every Prompt-bearing wire surface (the control plane's
14
- * dispatch imports it), so the two caps cannot drift apart. */
7
+ /**
8
+ * Request body cap (1 MiB) — shared by every Prompt-bearing wire surface (the control plane's dispatch imports it), so
9
+ * the two caps cannot drift apart.
10
+ */
15
11
  export declare const MAX_BODY_BYTES: number;
16
- /** A valid example request body for the invoke handler — lives HERE, next to the shape check it must
17
- * satisfy, so the CLI's "try it" hint can't drift from the protocol. */
12
+ /**
13
+ * A valid example request body for the invoke handler — lives HERE, next to the shape check it must satisfy, so the
14
+ * CLI's "try it" hint can't drift from the protocol.
15
+ */
18
16
  export declare const INVOKE_EXAMPLE_BODY = "{\"session\":\"dev\",\"text\":\"hello\"}";
19
17
  /**
20
- * Fetch-shaped invoke handler. Mount it at any route in the host app; it accepts POST only.
21
- * Returns SSE (`text/event-stream`) with one `data:` line per AgentEvent.
18
+ * Fetch-shaped invoke handler.
19
+ *
20
+ * No CORS here: who may call this from a browser is decided once, before dispatch, in `channels/serve.ts`'s router
21
+ * — which is also the only layer that can answer the `OPTIONS` preflight, since this route registers only `POST`.
22
22
  */
23
23
  export declare function createInvokeHandler(agent: Agent): (req: Request) => Promise<Response>;
@@ -1,20 +1,32 @@
1
- import { readBodyCapped } from "./body.js";
1
+ import { readBodyCapped, refuseNonJsonBody } from "./body.js";
2
2
  import { text } from "./respond.js";
3
3
  import { sseResponse } from "./sse.js";
4
- /** Request body cap (1 MiB) — shared by every Prompt-bearing wire surface (the control plane's
5
- * dispatch imports it), so the two caps cannot drift apart. */
4
+ /**
5
+ * Request body cap (1 MiB) — shared by every Prompt-bearing wire surface (the control plane's dispatch imports it), so
6
+ * the two caps cannot drift apart.
7
+ */
6
8
  export const MAX_BODY_BYTES = 1 << 20;
7
- /** A valid example request body for the invoke handler — lives HERE, next to the shape check it must
8
- * satisfy, so the CLI's "try it" hint can't drift from the protocol. */
9
+ /**
10
+ * A valid example request body for the invoke handler — lives HERE, next to the shape check it must satisfy, so the
11
+ * CLI's "try it" hint can't drift from the protocol.
12
+ */
9
13
  export const INVOKE_EXAMPLE_BODY = '{"session":"dev","text":"hello"}';
10
14
  /**
11
- * Fetch-shaped invoke handler. Mount it at any route in the host app; it accepts POST only.
12
- * Returns SSE (`text/event-stream`) with one `data:` line per AgentEvent.
15
+ * Fetch-shaped invoke handler.
16
+ *
17
+ * No CORS here: who may call this from a browser is decided once, before dispatch, in `channels/serve.ts`'s router
18
+ * — which is also the only layer that can answer the `OPTIONS` preflight, since this route registers only `POST`.
13
19
  */
14
20
  export function createInvokeHandler(agent) {
15
21
  return async (req) => {
16
22
  if (req.method !== "POST")
17
23
  return text("POST only\n", 405);
24
+ // Also applied by the host router over every unguarded route — kept HERE too because this handler is a
25
+ // PUBLIC standalone artifact (docs/overview.md mounts it as a Next.js route), and a handler you can mount
26
+ // anywhere has to be safe anywhere. The router's copy is what stops a route added later from forgetting.
27
+ const wrongType = refuseNonJsonBody(req);
28
+ if (wrongType)
29
+ return wrongType;
18
30
  const body = await readBodyCapped(req, MAX_BODY_BYTES);
19
31
  if ("tooLarge" in body)
20
32
  return text("body too large\n", 413);
@@ -30,8 +42,6 @@ export function createInvokeHandler(agent) {
30
42
  return text('need { "session": string, "text": string }\n', 400);
31
43
  }
32
44
  // INVOKE_EXAMPLE_BODY must keep satisfying this request shape.
33
- // The OPTIONAL lineage extension (Scope): malformed values are a 400, not a silent drop — a
34
- // caller that sent them meant them.
35
45
  if (parentSession !== undefined && typeof parentSession !== "string") {
36
46
  return text('"parentSession" must be a string\n', 400);
37
47
  }
@@ -1,10 +1,9 @@
1
1
  /**
2
- * Attachment destination for one conversation. Never rejects an id — but the path it returns is not
3
- * promised to be one the filesystem accepts: percent-encoding spends up to 9 bytes per character, so
4
- * an id of ~29 CJK characters exceeds a 255-byte `NAME_MAX` and the caller's `mkdir` fails with
5
- * `ENAMETOOLONG`. Left to fail there rather than truncated here: a digest suffix would buy a length
6
- * no platform id approaches, at the cost of the readability this encoding exists to keep.
2
+ * THE inbound-attachment root inside a channel's state home. One spelling because three channels write here and a
3
+ * conversation id becomes a directory under it; nothing else reads it, and nothing removes it — an attachment is
4
+ * durable conversation data, and this directory grows with the agent's inbound traffic (docs/design/core.md §9).
7
5
  */
6
+ export declare const attachmentsDir: (stateHome: string) => string;
8
7
  export declare function attachmentPath(filesDir: string, conversationId: string | number, fileName: string): {
9
8
  dir: string;
10
9
  name: string;
@@ -1,42 +1,18 @@
1
+ /** Where an inbound attachment lands: `<stateHome>/files/<conversation>/<file>`. */
2
+ import { join, resolve, sep } from "node:path";
1
3
  /**
2
- * Where an inbound attachment lands: `<filesDir>/<conversation>/<file>`.
3
- *
4
- * Both halves come from outside the channel, and they are NOT the same question:
5
- *
6
- * - The conversation is an id, and turning an arbitrary caller id into a storage name is a job this
7
- * repo already answers by ENCODING it, never by rejecting it (`piSessionId` in
8
- * engines/pi/session-store.ts, same shape: a character whitelist under a fixed prefix). An id is
9
- * the caller's — a route returning `../..` gets an odd directory, not a failed turn, because the
10
- * place to notice that id is broken is the reply it cannot deliver, not the disk.
11
- * - The file name is not an id: it is read by a human and by the model, in a path this puts into the
12
- * prompt. Encoding it would cost that, so it is reduced instead — losing the odd character rather
13
- * than the readability. A leading dot survives: `.gitignore` is a name, not an escape, and nothing
14
- * lists this directory, so hiding is not a property anyone here depends on.
15
- *
16
- * The encoding makes containment structural rather than checked: `encodeURIComponent` emits no path
17
- * separator (`/` → `%2F`, `\` → `%5C`, `:` → `%3A`), and the prefix leaves no way to spell `.`, `..`
18
- * or the empty string.
19
- *
20
- * It is lossless for a well-formed id, so `a/b` and `a_b` are different places. Two exceptions, both
21
- * outside what a platform hands over: ids differing only in malformed UTF-16 (see the substitution
22
- * below), and ids differing only in case on a case-insensitive filesystem.
23
- */
24
- import { resolve, sep } from "node:path";
25
- /**
26
- * Attachment destination for one conversation. Never rejects an id — but the path it returns is not
27
- * promised to be one the filesystem accepts: percent-encoding spends up to 9 bytes per character, so
28
- * an id of ~29 CJK characters exceeds a 255-byte `NAME_MAX` and the caller's `mkdir` fails with
29
- * `ENAMETOOLONG`. Left to fail there rather than truncated here: a digest suffix would buy a length
30
- * no platform id approaches, at the cost of the readability this encoding exists to keep.
4
+ * THE inbound-attachment root inside a channel's state home. One spelling because three channels write here and a
5
+ * conversation id becomes a directory under it; nothing else reads it, and nothing removes it — an attachment is
6
+ * durable conversation data, and this directory grows with the agent's inbound traffic (docs/design/core.md §9).
31
7
  */
8
+ export const attachmentsDir = (stateHome) => join(stateHome, "files");
32
9
  export function attachmentPath(filesDir, conversationId, fileName) {
33
- // A lone surrogate makes `encodeURIComponent` throw `URIError`, and this function rejecting an id
34
- // is the one thing it must not do — a permanent bad id would surface as a retryable turn failure.
35
- // The `u` flag matches by code point, so a valid pair (an emoji in a route's id) is left alone.
10
+ // A lone surrogate makes `encodeURIComponent` throw `URIError`, and this function rejecting an id is the one thing
11
+ // it must not do.
36
12
  const id = String(conversationId).replace(/\p{Surrogate}/gu, "\uFFFD");
37
13
  const dir = resolve(filesDir, `c-${encodeURIComponent(id)}`);
38
- // The name keeps its own check because it is NOT encoded: separators go, and `resolve` settles
39
- // whether what is left (`..`, a Windows `D:foo`) still lands in `dir`.
14
+ // The name keeps its own check because it is NOT encoded: separators go, and `resolve` settles whether what is left
15
+ // (`..`, a Windows `D:foo`) still lands in `dir`.
40
16
  const cleaned = fileName.replace(/[/\\]/g, "_");
41
17
  const name = resolve(dir, cleaned).startsWith(dir + sep) ? cleaned : "file";
42
18
  return { dir, name, path: resolve(dir, name) };
@@ -1,36 +1,29 @@
1
- /** Per-message bound INSIDE that budget. The fold is a digest of many messages competing for one
2
- * allowance, so the job here is fairness, not fidelity: one rambler must not price out everyone who
3
- * spoke after them. 280 against 4000 keeps at least ~14 messages in a full buffer. A referent — the
4
- * single message the asker points at — is the opposite job and takes REFERENT_MAX_CODE_POINTS. */
1
+ /** Per-message bound INSIDE that budget. */
5
2
  export declare const BUFFER_LINE_MAX_CHARS = 280;
6
- /** How many buffered files and images (each, most recent first) a summon pulls in with the folded
7
- * discussion — bounds the latency/token cost of "summarize the file from earlier" against a chatty
8
- * group posting many attachments between summons. Skipped ones must be counted into the prompt
9
- * note, so the model never sees an attachment reference it silently cannot open. Shared policy:
10
- * each channel's attachment collector caps against this. */
3
+ /** How many buffered files and images (each, most recent first) a summon pulls in with the folded discussion. */
11
4
  export declare const BUFFER_ATTACH_MAX = 3;
12
5
  /**
13
- * The folded discussion as it reaches the model — the prompt block, or nothing when the buffer is
14
- * empty. One renderer for every channel: what the agent is told about un-summoned discussion should
15
- * not depend on which chat platform delivered it, and three copies of the literal is how that drifts.
16
- *
17
- * A channel that folds a SECOND source (feishu's originating room) labels that one itself — it is a
18
- * different claim about a different place, not this block with another name.
6
+ * Keep the newest {@link BUFFER_ATTACH_MAX} refs of one kind. The overflow is COUNTED, not silently dropped: the
7
+ * prompt note tells the model what it is not holding, so it cannot pretend to have read it.
19
8
  */
9
+ export declare function capBufferedRefs<R>(refs: R[]): {
10
+ kept: R[];
11
+ skipped: number;
12
+ };
13
+ /** The folded discussion as it reaches the model — the prompt block, or nothing when the buffer is empty. */
20
14
  export declare function discussionBlock(text: string): string;
21
15
  export interface ContextBuffer<E> {
22
- /** Record an un-summoned message. Persists BEFORE returning (pre-ACK; see the module header). */
16
+ /** Record an un-summoned message. */
23
17
  push(placeKey: string, entry: E): void;
24
18
  /** Render the fold text and snapshot the consumed entries (see the module header's consume protocol). */
25
19
  peek(placeKey: string): {
26
20
  text: string;
27
21
  consumed: E[];
28
22
  };
29
- /** Remove exactly `consumed` (by identity) — call on the turn's `completed` event, when the folded
30
- * discussion provably lives in the durable session. Consumes entries WHOLE, including ones whose
31
- * attachments failed to load or were cap-skipped: their text is in the session (keeping them would
32
- * re-fold duplicate text), and the prompt note said what is missing; re-post an attachment to use
33
- * it. Post-ACK: a failed write is logged, never thrown (it must not abort the turn's delivery). */
23
+ /**
24
+ * Remove exactly `consumed` (by identity) — call on the turn's `completed` event, when the folded discussion
25
+ * provably lives in the durable session.
26
+ */
34
27
  commit(placeKey: string, consumed: E[]): void;
35
28
  }
36
29
  export declare function createContextBuffer<E>(options: {
@@ -1,52 +1,37 @@
1
1
  /**
2
- * Generic durable context buffer — the SHARED mechanics behind each stateful channel's
3
- * "un-summoned group discussion" module (telegram/feishu/slack `context-buffer.ts`): recent
4
- * un-summoned messages per conversation "place", kept under a char budget and folded into the next
5
- * answered turn in that place, so a summoned agent has the discussion it didn't see turn-by-turn.
2
+ * Generic durable context buffer — the SHARED mechanics behind each stateful channel's "un-summoned group discussion"
3
+ * module (telegram/feishu/slack `context-buffer.ts`). Generic over the entry shape: the channel supplies its entry
4
+ * type, the shape validator, the fold-line renderer and its log label.
6
5
  *
7
- * Channel-neutral and generic over the entry shape (like ../kit/turn-store.ts): the channel supplies its
8
- * entry type, the shape validator (state files are an IO boundary — valid JSON of the WRONG shape
9
- * must degrade exactly like a corrupt file: warn + empty, never flow in as trusted data), the
10
- * fold-line renderer, and its log label. What stays per channel: the entry type itself, place-key
11
- * derivation, and buffered-attachment selection (platform resource shapes are real differences).
12
- *
13
- * DURABLE, with the consume protocol every channel inherits:
14
- * - `push` persists synchronously BEFORE the transport ACK (an ACKed delivery is not redelivered,
15
- * so ACK-then-persist would be a silent-loss window): a throw becomes the webhook's 500 and the
16
- * platform redelivers once the disk recovers — staged on a copy and rolled back on a failed
17
- * write, so the redelivery does not double-append the entry already in memory.
6
+ * The consume protocol every channel inherits:
7
+ * - `push` persists synchronously BEFORE the transport ACK (an ACKed delivery is not redelivered, so ACK-then-persist
8
+ * would be a silent-loss window): a throw becomes the webhook's 500 and the platform redelivers.
18
9
  * - `peek` renders WITHOUT clearing and snapshots exactly which entries it consumed.
19
- * - `commit` removes only that snapshot, by object identity, on the turn's `completed` — so a
20
- * failure or crash before `completed` leaves the discussion intact for the next summon, and a
21
- * message that arrives while the turn runs survives for the next answered turn (a whole-bucket
22
- * delete would lose it).
10
+ * - `commit` removes only that snapshot, by object identity, on the turn's `completed` — so a failure or crash before
11
+ * `completed` leaves the discussion intact, and a message arriving mid-turn survives for the next answered turn.
12
+ *
13
+ * State files are an IO boundary: valid JSON of the WRONG shape degrades exactly like a corrupt file (warn + empty),
14
+ * never flowing in as trusted data.
23
15
  */
24
16
  import { log } from "../../log.js";
25
17
  import { loadStateFile, saveStateFile } from "./state.js";
26
- /** Char budget for the per-place buffer — bounds the cost of folding it into a prompt; when exceeded
27
- * the OLDEST un-summoned messages are dropped (not a time window: a quiet group keeps its
28
- * sparse-but-relevant lines, a busy burst is capped). The `line` renderer is the eviction cost
29
- * basis: the budget must price what the fold actually renders, or it would systematically overrun. */
18
+ /**
19
+ * Char budget for the per-place buffer — bounds the cost of folding it into a prompt; when exceeded the OLDEST
20
+ * un-summoned messages are dropped. Not a time window: a quiet group keeps its sparse-but-relevant lines.
21
+ */
30
22
  const BUFFER_MAX_CHARS = 4000;
31
- /** Per-message bound INSIDE that budget. The fold is a digest of many messages competing for one
32
- * allowance, so the job here is fairness, not fidelity: one rambler must not price out everyone who
33
- * spoke after them. 280 against 4000 keeps at least ~14 messages in a full buffer. A referent — the
34
- * single message the asker points at — is the opposite job and takes REFERENT_MAX_CODE_POINTS. */
23
+ /** Per-message bound INSIDE that budget. */
35
24
  export const BUFFER_LINE_MAX_CHARS = 280;
36
- /** How many buffered files and images (each, most recent first) a summon pulls in with the folded
37
- * discussion — bounds the latency/token cost of "summarize the file from earlier" against a chatty
38
- * group posting many attachments between summons. Skipped ones must be counted into the prompt
39
- * note, so the model never sees an attachment reference it silently cannot open. Shared policy:
40
- * each channel's attachment collector caps against this. */
25
+ /** How many buffered files and images (each, most recent first) a summon pulls in with the folded discussion. */
41
26
  export const BUFFER_ATTACH_MAX = 3;
42
27
  /**
43
- * The folded discussion as it reaches the model — the prompt block, or nothing when the buffer is
44
- * empty. One renderer for every channel: what the agent is told about un-summoned discussion should
45
- * not depend on which chat platform delivered it, and three copies of the literal is how that drifts.
46
- *
47
- * A channel that folds a SECOND source (feishu's originating room) labels that one itself — it is a
48
- * different claim about a different place, not this block with another name.
28
+ * Keep the newest {@link BUFFER_ATTACH_MAX} refs of one kind. The overflow is COUNTED, not silently dropped: the
29
+ * prompt note tells the model what it is not holding, so it cannot pretend to have read it.
49
30
  */
31
+ export function capBufferedRefs(refs) {
32
+ return { kept: refs.slice(-BUFFER_ATTACH_MAX), skipped: Math.max(0, refs.length - BUFFER_ATTACH_MAX) };
33
+ }
34
+ /** The folded discussion as it reaches the model — the prompt block, or nothing when the buffer is empty. */
50
35
  export function discussionBlock(text) {
51
36
  return text ? `[recent group discussion:\n${text}\n]\n\n` : "";
52
37
  }
@@ -83,6 +68,8 @@ export function createContextBuffer(options) {
83
68
  persist();
84
69
  }
85
70
  catch (error) {
71
+ // Staged on a copy, rolled back on a failed write, then rethrown: the throw becomes the webhook's 500 and the
72
+ // platform redelivers, so an entry left in memory here would be appended a second time.
86
73
  if (previous)
87
74
  buffers.set(placeKey, previous);
88
75
  else