@fastagent-sh/fastagent 0.21.1 → 0.22.0

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 +209 -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 +153 -228
  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,28 +1,21 @@
1
1
  import { truncateCodePointPrefix, truncateCodePointSuffix } from "./text.js";
2
- /** The customer-facing default: neutral, no leaked internals. Differentiate on retryability and always
3
- * offer a next step (Slack agent-design: never leave the user with a dead-end "something went wrong").
4
- * The non-retryable branch keeps the "something went wrong" phrase deliberately — it is neutral (we only
5
- * know a boolean, never the specific limitation) and shared verbatim across channels. */
2
+ /** The customer-facing default: neutral, no leaked internals. */
6
3
  export function defaultErrorMessage(failed) {
7
4
  return failed.retryable
8
5
  ? "⚠️ Temporary problem — please try again in a moment."
9
6
  : "⚠️ Sorry, something went wrong. Try rephrasing, or check I have access to what you need.";
10
7
  }
11
- /** Customer-facing live-preview line for an engine-internal retry backoff (the advisory `retrying`
12
- * event): neutral, no leaked internals — the reason stays in operator logs. */
8
+ /**
9
+ * Customer-facing live-preview line for an engine-internal retry backoff (the advisory `retrying` event): neutral, no
10
+ * leaked internals — the reason stays in operator logs.
11
+ */
13
12
  export const RETRY_NOTICE = "⏳ Temporary problem — retrying…";
14
13
  /** The placeholder shown before any reasoning/tool/text arrives. */
15
14
  export const THINKING_PLACEHOLDER = "💭 Thinking…";
16
15
  export function createTurnView() {
17
16
  return { thinking: "", tools: [], toolById: new Map(), answer: "", retrying: false };
18
17
  }
19
- /**
20
- * Apply one event to the view state. Returns true when the view changed (the caller repaints).
21
- * This is the ONE place the shared view rules live: tool labels are humanized with a compact arg
22
- * summary, and any progress event closes an open retry notice (a stale "retrying" line must never
23
- * outlive actual progress). Terminal events only close the notice — they are the caller's business.
24
- */
25
- export function applyTurnEvent(view, e) {
18
+ export function applyTurnEvent(view, e, now = Date.now()) {
26
19
  const closedRetry = view.retrying && e.type !== "retrying";
27
20
  if (closedRetry)
28
21
  view.retrying = false;
@@ -30,7 +23,7 @@ export function applyTurnEvent(view, e) {
30
23
  case "text":
31
24
  view.answer += e.delta;
32
25
  if (view.answerSince === undefined && view.answer.trim() !== "")
33
- view.answerSince = Date.now();
26
+ view.answerSince = now;
34
27
  return true;
35
28
  case "thinking":
36
29
  view.thinking += e.delta;
@@ -61,25 +54,19 @@ const TOOL_MARK = { running: "…", ok: "✓", error: "✗" };
61
54
  export function toolLines(view) {
62
55
  return view.tools.map((t) => `🔧 ${t.label} ${TOOL_MARK[t.status]}`).join("\n");
63
56
  }
64
- /** The reasoning peek: the most recent tail of the (growing) reasoning, one line, code-point safe.
65
- * Process, not the answer — renderers show it live only, never in the persisted final message. */
57
+ /** The reasoning peek: the most recent tail of the (growing) reasoning, one line, code-point safe. */
66
58
  export function thinkingLine(view, maxTail) {
67
59
  const t = view.thinking.replace(/\s+/g, " ").trim();
68
60
  return t === "" ? "" : `💭 ${truncateCodePointSuffix(t, maxTail)}`;
69
61
  }
70
62
  /**
71
- * The shared answer-reveal policy: the answer stays hidden until its first delta has aged one
72
- * throttle window (`ageMs` — each platform passes its own pacing constant). The pump's leading-edge
73
- * flush would otherwise turn the very first content delta (often a lone character or unbalanced
74
- * markup) into its own frame — the short-reply flicker (placeholder → "O" → "OK."). Aging is
75
- * anchored at delta ARRIVAL (`answerSince`, set by the reducer) so an in-flight write can't skew the
76
- * clock, and there is deliberately NO timer at the boundary: a young answer surfaces on the next
77
- * content-driven pass, so a turn completing within the window delivers the final answer only.
63
+ * The shared answer-reveal policy: the answer stays hidden until its first delta has aged one throttle window (`ageMs`
64
+ * — each platform passes its own pacing constant).
78
65
  */
79
- export function revealedAnswer(view, ageMs) {
66
+ export function revealedAnswer(view, ageMs, now = Date.now()) {
80
67
  if (view.answer.trim() === "" || view.answerSince === undefined)
81
68
  return "";
82
- return Date.now() - view.answerSince >= ageMs ? view.answer : "";
69
+ return now - view.answerSince >= ageMs ? view.answer : "";
83
70
  }
84
71
  /** Compose body parts (thinking/tools/retry/answer) into one frame: skip empties, blank-line joins. */
85
72
  export function composeTurnBody(parts) {
@@ -88,71 +75,21 @@ export function composeTurnBody(parts) {
88
75
  .join("\n\n")
89
76
  .trim();
90
77
  }
91
- export function createPreviewPump(opts) {
92
- let dirty = false;
93
- let pumping = false;
94
- let stopped = false;
95
- let errored = false;
96
- let pumpDone;
97
- let wakeThrottle; // set while mid-throttle; finish() cuts it short
98
- const runPump = async () => {
99
- pumping = true;
100
- try {
101
- while (dirty && !stopped) {
102
- dirty = false;
103
- try {
104
- await opts.flush();
105
- }
106
- catch (error) {
107
- if (!errored) {
108
- errored = true;
109
- opts.onError(error);
110
- }
111
- }
112
- if (dirty && !stopped) {
113
- await new Promise((resolve) => {
114
- const t = setTimeout(resolve, opts.throttleMs);
115
- wakeThrottle = () => {
116
- clearTimeout(t);
117
- resolve();
118
- };
119
- });
120
- wakeThrottle = undefined;
121
- }
122
- }
123
- }
124
- finally {
125
- pumping = false;
126
- }
127
- };
128
- return {
129
- touch() {
130
- dirty = true;
131
- if (!pumping)
132
- pumpDone = runPump();
133
- },
134
- async finish() {
135
- stopped = true;
136
- wakeThrottle?.();
137
- await pumpDone?.catch(() => { });
138
- },
139
- };
140
- }
141
- /** Max length (code points) of a tool's arg preview. Slack's native stream cannot be retracted, so
142
- * this is a disclosure bound, not only a layout one. */
78
+ /** Max length (code points) of a tool's arg preview. */
143
79
  const TOOL_ARG_MAX = 48;
144
80
  /** Max length (code points) of a humanized tool label. */
145
81
  const TOOL_NAME_MAX = 80;
146
- /** One-line, truncated at code-point boundaries: collapse whitespace so a multi-line command/arg
147
- * stays on one line, and never tear a surrogate pair mid-emoji. */
82
+ /**
83
+ * One-line, truncated at code-point boundaries: collapse whitespace so a multi-line command/arg stays on one line, and
84
+ * never tear a surrogate pair mid-emoji.
85
+ */
148
86
  function clip(s) {
149
87
  const one = s.replace(/\s+/g, " ").trim();
150
88
  return truncateCodePointPrefix(one, TOOL_ARG_MAX);
151
89
  }
152
90
  /**
153
- * A compact, human-readable preview of a tool call's args so the live view reads `🔧 read AGENTS.md`
154
- * rather than just `🔧 read`. Generic (a channel knows no tool schemas): show the salient value — the
155
- * first primitive field, conventionally the subject (path / command / query / url) — else compact JSON.
91
+ * A compact, human-readable preview of a tool call's args so the live view reads `🔧 read AGENTS.md` rather than just
92
+ * `🔧 read`.
156
93
  */
157
94
  export function summarizeToolArgs(args) {
158
95
  if (args === null || typeof args !== "object" || Array.isArray(args))
@@ -164,12 +101,9 @@ export function summarizeToolArgs(args) {
164
101
  return values.length > 0 ? clip(JSON.stringify(args)) : "";
165
102
  }
166
103
  /**
167
- * A plain-language label for a tool call, following Slack's agent-design guidance to name what a tool
168
- * does rather than expose a raw identifier ("Create issue", not "create_issue"; "Github: create issue",
169
- * not "mcp__github__create_issue"). Deliberately generic and engine-neutral: it only reshapes the
170
- * identifier string — it never invents semantics and never exposes arguments. An `mcp__server__tool`
171
- * identifier becomes `server: tool`; any other identifier has its separators normalized to spaces and
172
- * its first letter capitalized.
104
+ * A plain-language label for a tool call, following Slack's agent-design guidance to name what a tool does rather than
105
+ * expose a raw identifier ("Create issue", not "create_issue"; "Github: create issue", not
106
+ * "mcp__github__create_issue").
173
107
  */
174
108
  export function humanizeToolName(name) {
175
109
  const normalize = (s) => s
@@ -1,12 +1,6 @@
1
+ /** The replay window every signed webhook ingress needs. */
1
2
  /**
2
- * The replay window every signed webhook ingress needs. A signature covers its timestamp but proves
3
- * nothing about it, so without a window a captured body plus its signed headers replays forever.
4
- * The window LENGTH is the caller's: it is set by the platform's own redelivery schedule, not by us.
5
- */
6
- /**
7
- * Whether a Unix-SECONDS timestamp header is within `maxAgeS` of now, in either direction (a clock
8
- * ahead of ours is as suspect as one behind). Non-numeric is not fresh: the header is part of the
9
- * signed material, so a value the signature commits to but this cannot read is a reason to refuse,
10
- * not to wave through.
3
+ * Whether a Unix-SECONDS timestamp header is within `maxAgeS` of now, in either direction (a clock ahead of ours is as
4
+ * suspect as one behind).
11
5
  */
12
6
  export declare function signatureIsFresh(timestamp: string, maxAgeS: number, nowMs?: number): boolean;
@@ -1,13 +1,7 @@
1
+ /** The replay window every signed webhook ingress needs. */
1
2
  /**
2
- * The replay window every signed webhook ingress needs. A signature covers its timestamp but proves
3
- * nothing about it, so without a window a captured body plus its signed headers replays forever.
4
- * The window LENGTH is the caller's: it is set by the platform's own redelivery schedule, not by us.
5
- */
6
- /**
7
- * Whether a Unix-SECONDS timestamp header is within `maxAgeS` of now, in either direction (a clock
8
- * ahead of ours is as suspect as one behind). Non-numeric is not fresh: the header is part of the
9
- * signed material, so a value the signature commits to but this cannot read is a reason to refuse,
10
- * not to wave through.
3
+ * Whether a Unix-SECONDS timestamp header is within `maxAgeS` of now, in either direction (a clock ahead of ours is as
4
+ * suspect as one behind).
11
5
  */
12
6
  export function signatureIsFresh(timestamp, maxAgeS, nowMs = Date.now()) {
13
7
  if (!/^\d+$/.test(timestamp))
@@ -1,6 +1,5 @@
1
1
  /** Create the channel's state home — the one shared spelling of it, so no channel invents its own. */
2
2
  export declare function ensureStateHome(dir: string): void;
3
- /** Returns `unknown` on purpose — no generic pretending otherwise: the file is an IO boundary, and the
4
- * caller owns shape validation (a `<T>` here would be an unchecked cast wearing a type). */
3
+ /** Returns `unknown` on purpose — no generic pretending otherwise. */
5
4
  export declare function loadStateFile(path: string): unknown;
6
5
  export declare function saveStateFile(path: string, value: unknown): void;
@@ -1,16 +1,8 @@
1
1
  /**
2
- * Durable channel state for a SINGLE-PROCESS deployment (the supported production shape — no
3
- * cross-instance locking; two processes must not share a state dir). Small JSON files, written
4
- * atomically (tmp + rename), so a crash leaves the previous version on disk, never a torn file.
5
- * Writes are synchronous: the files are KB-sized and a write that completes BEFORE the transport ACK
6
- * is what makes the state actually durable (an ACKed delivery is not redelivered).
7
- * Channel-neutral: every stateful channel (Telegram, Feishu/Lark, Slack) derives its home from the ctx state root
8
- * and persists through these three primitives.
9
- *
10
- * Failure split: a CORRUPT file (bad JSON) degrades visibly — log.warn + start empty — because channel
11
- * state is recoverable context, not worth refusing to boot over. An unreadable file (permissions, IO)
12
- * is an ENVIRONMENT error the operator must fix: it throws, and construction fails loudly — booting
13
- * with silently-empty state would hide real data behind a config mistake.
2
+ * Durable channel state, with no cross-instance locking: every file here is keyed by CHANNEL KIND
3
+ * (`<stateRoot>/channels/<kind>/`), so what must not happen is two processes serving the SAME channel — which also
4
+ * means one ingress credential in two places, and no local guard can see that. Two processes serving different
5
+ * channels of one agent never touch the same file (docs/design/core.md, "the shipped file-backed implementations").
14
6
  */
15
7
  import { mkdirSync, readFileSync } from "node:fs";
16
8
  import { writeFileAtomic } from "../../atomic-write.js";
@@ -19,8 +11,7 @@ import { log } from "../../log.js";
19
11
  export function ensureStateHome(dir) {
20
12
  mkdirSync(dir, { recursive: true });
21
13
  }
22
- /** Returns `unknown` on purpose — no generic pretending otherwise: the file is an IO boundary, and the
23
- * caller owns shape validation (a `<T>` here would be an unchecked cast wearing a type). */
14
+ /** Returns `unknown` on purpose — no generic pretending otherwise. */
24
15
  export function loadStateFile(path) {
25
16
  let raw;
26
17
  try {
@@ -1,6 +1,5 @@
1
1
  import { type SessionControl } from "../../session.ts";
2
2
  /** Bare stop word for summon-body matching (Slack/Feishu); Telegram uses its native `/stop` command. */
3
3
  export declare function isStopText(text: string): boolean;
4
- /** Dispatch `abort` for the session and map the outcome to the customer-facing line. Never throws;
5
- * full details go to the operator log. */
4
+ /** Dispatch `abort` for the session and map the outcome to the customer-facing line. */
6
5
  export declare function dispatchStop(control: SessionControl | undefined, session: string, label: string): Promise<string>;
@@ -1,10 +1,6 @@
1
1
  /**
2
- * SHARED user-facing stop command: the chat channels (telegram, feishu/lark, slack) map an explicit
3
- * "stop" ask onto the session control plane's `abort`. Policy decisions (design session-control.md
4
- * §15): the stop message is a CONTROL ACTION, never a turn (it must not queue behind the run it
5
- * stops); only the ACTIVE run is aborted — queued durable turns are independent asks and keep their
6
- * at-least-once floor; and the hub stays gated by `config.sessionControl`, so without it the command
7
- * degrades to a visible "not enabled" notice, never a silent ignore.
2
+ * SHARED user-facing stop command: the chat channels (telegram, feishu/lark, slack) map an explicit "stop" ask onto
3
+ * the session control plane's `abort`.
8
4
  */
9
5
  import { log } from "../../log.js";
10
6
  import { NO_ACTIVE_RUN_CODE } from "../../session.js";
@@ -14,9 +10,10 @@ export function isStopText(text) {
14
10
  }
15
11
  const STOPPED_NOTICE = "⏹ Stopped.";
16
12
  const NOTHING_RUNNING_NOTICE = "Nothing is running.";
17
- const STOP_UNAVAILABLE_NOTICE = "⚠️ Stop isn't enabled on this deployment (set sessionControl: true in fastagent.config).";
18
- /** Dispatch `abort` for the session and map the outcome to the customer-facing line. Never throws;
19
- * full details go to the operator log. */
13
+ // A serve always builds the hub, so this is only reachable when an embedder mounted a service without one — never by
14
+ // a configuration an author can change, which is why it no longer names a setting to turn on.
15
+ const STOP_UNAVAILABLE_NOTICE = "⚠️ Stop isn't available on this deployment.";
16
+ /** Dispatch `abort` for the session and map the outcome to the customer-facing line. */
20
17
  export async function dispatchStop(control, session, label) {
21
18
  if (!control)
22
19
  return STOP_UNAVAILABLE_NOTICE;
@@ -1,5 +1,5 @@
1
1
  export interface TaskTracker {
2
- /** Track one task. The caller keeps its own `.catch` — rejections must already be handled. */
2
+ /** Track already-started work. Rejections are logged without failing the drain. */
3
3
  track(task: Promise<unknown>): void;
4
4
  /** Resolves when every currently-tracked task has settled. */
5
5
  drain(): Promise<void>;
@@ -1,31 +1,18 @@
1
- /**
2
- * SHARED fire-and-forget side-task tracking. Channels launch work off the request path (stop
3
- * feedback, DM welcomes) that must not block the transport ACK but MUST be drained on shutdown
4
- * (`turnsIdle`) — otherwise a reply in flight when the process exits is silently dropped. Error
5
- * handling stays with the caller: track() only guarantees the drain sees the task SETTLE, and settle
6
- * includes reject. A caller that handles its error on a separate branch (`p.catch(log); track(p)`)
7
- * still hands us a promise that rejects, and a drain that propagated it would fail the channel's whole
8
- * `turnsIdle` over one side task. A rejection that reaches us is logged — we
9
- * cannot tell a missing `.catch` from one on a separate branch, so the line is a visibility floor
10
- * rather than a diagnosis, and without it a dropped side task leaves no trace anywhere.
11
- */
1
+ /** ACK-independent side-task tracking. Drains are observation hooks, not service shutdown. */
2
+ import * as Effect from "effect/Effect";
3
+ import * as Fiber from "effect/Fiber";
4
+ import { portError, portJoin } from "../../effect-port.js";
12
5
  import { beginWork } from "../busy.js";
13
6
  import { log } from "../../log.js";
14
7
  export function createTaskTracker(label) {
15
8
  const tasks = new Set();
16
9
  return {
17
10
  track(task) {
18
- tasks.add(task);
19
- // Tracked side tasks count as process-wide in-flight work (busy.ts) — same signal the turn
20
- // queue reports, read by serving surfaces that must not idle while background work runs.
21
11
  const workDone = beginWork();
22
- void task
23
- .finally(() => {
24
- workDone();
25
- tasks.delete(task);
26
- })
27
- .catch((error) => log.warn(`${label} side task rejected: ${String(error)}`));
12
+ const fiber = Effect.runFork(portJoin(() => task).pipe(Effect.asVoid, Effect.catchCause((cause) => Effect.sync(() => log.warn(`${label} side task rejected: ${String(portError(cause))}`))), Effect.ensuring(Effect.sync(workDone))));
13
+ tasks.add(fiber);
14
+ fiber.addObserver(() => tasks.delete(fiber));
28
15
  },
29
- drain: () => Promise.allSettled(tasks).then(() => undefined),
16
+ drain: () => Effect.runPromise(Effect.asVoid(Fiber.awaitAll([...tasks]))),
30
17
  };
31
18
  }
@@ -1,19 +1,5 @@
1
- /** Pure Unicode-safe text slicing helpers shared by channel rendering paths (Feishu/Lark cards, the preview kit). JavaScript string
2
- * indexes are UTF-16 code units, so direct `slice()` can tear a surrogate pair and send replacement
3
- * characters after JSON/UTF-8 encoding. These helpers only cut at Unicode code-point boundaries. */
4
- /**
5
- * How much of a replied-to message is quoted back into a prompt.
6
- *
7
- * A referent is the exact text the asker is pointing AT, not a summary of it, so the bound is a
8
- * fidelity bound: it must clear the largest message a chat platform will accept (Telegram's 4096 is
9
- * the tightest of ours) or a perfectly legal message loses its tail and the agent answers about text
10
- * it cannot see — silently. Past that point it is only a guard against a pathological message on a
11
- * platform with no practical cap. One constant for every channel: the failure this replaces was two
12
- * channels picking their own number and drifting 14x apart.
13
- *
14
- * Distinct from a context-buffer line (see BUFFER_LINE_MAX_CHARS), which is a digest competing for a
15
- * shared budget — different job, different unit, must not share a number.
16
- */
1
+ /** Pure Unicode-safe text slicing helpers shared by channel rendering paths (Feishu/Lark cards, the preview kit). */
2
+ /** How much of a replied-to message is quoted back into a prompt. */
17
3
  export declare const REFERENT_MAX_CODE_POINTS = 4096;
18
4
  /** Take at most `maxPoints` Unicode code points from the start, without adding a marker. */
19
5
  export declare function codePointPrefix(text: string, maxPoints: number): string;
@@ -1,19 +1,5 @@
1
- /** Pure Unicode-safe text slicing helpers shared by channel rendering paths (Feishu/Lark cards, the preview kit). JavaScript string
2
- * indexes are UTF-16 code units, so direct `slice()` can tear a surrogate pair and send replacement
3
- * characters after JSON/UTF-8 encoding. These helpers only cut at Unicode code-point boundaries. */
4
- /**
5
- * How much of a replied-to message is quoted back into a prompt.
6
- *
7
- * A referent is the exact text the asker is pointing AT, not a summary of it, so the bound is a
8
- * fidelity bound: it must clear the largest message a chat platform will accept (Telegram's 4096 is
9
- * the tightest of ours) or a perfectly legal message loses its tail and the agent answers about text
10
- * it cannot see — silently. Past that point it is only a guard against a pathological message on a
11
- * platform with no practical cap. One constant for every channel: the failure this replaces was two
12
- * channels picking their own number and drifting 14x apart.
13
- *
14
- * Distinct from a context-buffer line (see BUFFER_LINE_MAX_CHARS), which is a digest competing for a
15
- * shared budget — different job, different unit, must not share a number.
16
- */
1
+ /** Pure Unicode-safe text slicing helpers shared by channel rendering paths (Feishu/Lark cards, the preview kit). */
2
+ /** How much of a replied-to message is quoted back into a prompt. */
17
3
  export const REFERENT_MAX_CODE_POINTS = 4096;
18
4
  /** Take at most `maxPoints` Unicode code points from the start, without adding a marker. */
19
5
  export function codePointPrefix(text, maxPoints) {
@@ -27,29 +13,35 @@ export function codePointPrefix(text, maxPoints) {
27
13
  }
28
14
  return out.join("");
29
15
  }
30
- /** Ellipsize from the right while keeping the result within `maxPoints` Unicode code points. */
31
- export function truncateCodePointPrefix(text, maxPoints, marker = "…") {
16
+ /**
17
+ * How much of `text` survives beside `marker`, or the finished answer when the marker alone decides it: nothing to
18
+ * cut, no room at all, or room for less than the marker. Both truncations answer those three the same way, so they
19
+ * are decided once.
20
+ */
21
+ function budget(text, maxPoints, marker) {
32
22
  const points = Array.from(text);
33
23
  if (points.length <= maxPoints)
34
- return text;
24
+ return { done: text };
35
25
  if (maxPoints <= 0)
36
- return "";
26
+ return { done: "" };
37
27
  const markerPoints = Array.from(marker);
38
28
  if (markerPoints.length >= maxPoints)
39
- return markerPoints.slice(0, maxPoints).join("");
40
- return `${points.slice(0, maxPoints - markerPoints.length).join("")}${marker}`;
29
+ return { done: markerPoints.slice(0, maxPoints).join("") };
30
+ return { points, keep: maxPoints - markerPoints.length };
31
+ }
32
+ /** Ellipsize from the right while keeping the result within `maxPoints` Unicode code points. */
33
+ export function truncateCodePointPrefix(text, maxPoints, marker = "…") {
34
+ const fit = budget(text, maxPoints, marker);
35
+ if ("done" in fit)
36
+ return fit.done;
37
+ return `${fit.points.slice(0, fit.keep).join("")}${marker}`;
41
38
  }
42
39
  /** Ellipsize from the left while keeping the result within `maxPoints` Unicode code points. */
43
40
  export function truncateCodePointSuffix(text, maxPoints, marker = "…") {
44
- const points = Array.from(text);
45
- if (points.length <= maxPoints)
46
- return text;
47
- if (maxPoints <= 0)
48
- return "";
49
- const markerPoints = Array.from(marker);
50
- if (markerPoints.length >= maxPoints)
51
- return markerPoints.slice(0, maxPoints).join("");
52
- return `${marker}${points.slice(points.length - (maxPoints - markerPoints.length)).join("")}`;
41
+ const fit = budget(text, maxPoints, marker);
42
+ if ("done" in fit)
43
+ return fit.done;
44
+ return `${marker}${fit.points.slice(fit.points.length - fit.keep).join("")}`;
53
45
  }
54
46
  /** Largest code-point-aligned prefix whose UTF-8 encoding fits `maxBytes`. */
55
47
  export function utf8Prefix(text, maxBytes) {
@@ -1,25 +1,12 @@
1
1
  export interface ThreadParticipants {
2
2
  /**
3
- * The participant model's summon rule (docs/design/participant-model.md §3): does a bare message in
4
- * this thread address the agent? True where it takes part AND no second human has been heard. Lives
5
- * here rather than in each channel because it is one rule over one store — `<= 1` is an easy edge to
6
- * get wrong twice, and "a second human restores the mention requirement" must have one place to change.
3
+ * The participant model's summon rule (docs/design/participant-model.md §3): does a bare message in this thread
4
+ * address the agent?
7
5
  */
8
6
  admitsBareMessage(key: string): boolean;
9
- /**
10
- * Has the agent answered into this thread before — the "first answered turn" fact
11
- * (participant-model.md §8), unlike {@link ThreadParticipants.admitsBareMessage} which also weighs
12
- * the second-human rule. An evicted record answers false (this store is a cache — see the header),
13
- * so gate a repeatable read on it, never a durable claim.
14
- */
7
+ /** Has the agent answered into this thread before. */
15
8
  agentSpokeIn(key: string): boolean;
16
- /**
17
- * Merge in what was just heard. Idempotent; a failed write is a warning, never a failed delivery.
18
- *
19
- * The parameter only admits values the store can honour: observations accumulate, so `agentSpoke`
20
- * can be set but never cleared and `humans` unions. Passing `false` would compile and do nothing,
21
- * so the type refuses it — "never shed" is an invariant, not a convention.
22
- */
9
+ /** `agentSpoke?: true` and not `boolean`: observations only ever accumulate, so there is no "un-speak" to pass. */
23
10
  merge(key: string, heard: {
24
11
  humans?: string[];
25
12
  agentSpoke?: true;
@@ -1,45 +1,12 @@
1
1
  /**
2
- * SHARED: who the agent has HEARD in a group thread — the sole input to the participant model's summon
3
- * rule. **The derivation lives in docs/design/participant-model.md §3** (why the rule is defined over
4
- * observation rather than the thread's true membership, what the weaker claim costs in both
5
- * directions, and why recording is never gated on configuration). Repeating it here would mean two
6
- * copies to keep true; what follows is only what a caller of this module must not get wrong.
7
- *
8
- * - **The key MUST be the string the session uses.** A record here is a claim about that session's
9
- * memory, so the two cannot be keyed independently — including the channel brand
10
- * (`feishu:<chat>:<thread>`, `slack:<team>:<channel>:<thread_ts>`). This file is already per-channel
11
- * and would not need the prefix, but SESSION ids share one namespace across every channel in a
12
- * deployment, so dropping it would key participation to a session that is not the one that answered.
13
- * - **Write both halves under one condition**, and gate that condition on STRUCTURAL facts only (is
14
- * this a group? a thread? a human speaking?) — never on configuration, which changes while records
15
- * outlive the change.
16
- * - **Observations only accumulate.** Nothing here sheds: no platform signals that someone stopped
17
- * taking part, and the error directions are not symmetric — over-counting humans makes the agent ask
18
- * to be named, under-counting makes it speak into a crowd.
19
- * - Keyed by `thread_id`, never a reply-chain root: Feishu's `root_id` moves with the chain, so it
20
- * cannot identify a side conversation at all.
2
+ * SHARED: who the agent has HEARD in a group thread — the sole input to the participant model's summon rule
3
+ * (docs/design/participant-model.md §3). The key MUST be the string the SESSION uses: a record here is a claim about
4
+ * that session's memory, so the two cannot be keyed independently.
21
5
  */
22
6
  import { log } from "../../log.js";
23
7
  import { loadStateFile, saveStateFile } from "./state.js";
24
- /** Cap on remembered threads. Losing one costs a mention to re-enter that thread, so an unbounded file
25
- * buys little — and a merge that carries new information rewrites the whole map synchronously, so the
26
- * map's size is the cost of every such write. What keeps that bounded is that only NEW information
27
- * writes at all (a repeat speaker, or any message once MAX_HUMANS is reached, returns before
28
- * persisting), and that the map stays small enough for state.ts's premise to hold — these writes land
29
- * synchronously on the acceptance path, so the whole file is the cost of each one. A thousand records
30
- * is tens of KB, and the records that matter (threads the agent takes part in) are far fewer than
31
- * that; the dominant traffic is bystanders, which is what the eviction policy below is aimed at.
32
- *
33
- * Eviction prefers BYSTANDER threads — ones the agent has only listened to. They are written on the
34
- * same path and vastly outnumber the rest (every thread in every visible channel), yet losing one
35
- * costs nothing: the summon rule refuses a thread the agent has not spoken in anyway, so the record
36
- * would have to be rebuilt by the mention that admits it. Evicting purely by age would let this
37
- * traffic push out the threads the agent is actively serving, silently reverting them to
38
- * mention-only. This is also what makes it safe to record threads no rule currently reads (those
39
- * behind a custom route, or under a posture whose summon rule is off — see the header). */
40
8
  const MAX_THREADS = 1000;
41
- /** Cap on remembered humans per thread. The rule only asks "have I heard a second one?", so two is
42
- * already the whole answer and anything beyond it is weight nothing reads. */
9
+ /** Cap on remembered humans per thread. */
43
10
  const MAX_HUMANS = 2;
44
11
  function isStoredParticipation(value) {
45
12
  const record = value;
@@ -87,26 +54,13 @@ export function createThreadParticipants(path, label) {
87
54
  const unchanged = previous !== undefined &&
88
55
  previous.agentSpoke === next.agentSpoke &&
89
56
  previous.humans.length === next.humans.length;
90
- // Re-insert so insertion order is "least recently TOUCHED first" — including when nothing
91
- // changed. A thread in its steady state (the agent answers, the same person keeps talking) stops
92
- // carrying new information and would otherwise never refresh its position again, leaving the
93
- // thread being served right now at the head of the eviction order.
94
- //
95
- // PROCESS-LOCAL: a touch alone never writes, and such a thread has nothing left to write (it has
96
- // reached MAX_HUMANS), so the refreshed order survives only until restart. That is the right
97
- // trade for a cache — persisting recency would mean a whole-map write per message — and the
98
- // consequence is bounded: after a restart, eviction order among participant threads is the order
99
- // they last carried new information.
57
+ // Re-insert so insertion order is "least recently TOUCHED first" — including when nothing changed.
100
58
  records.delete(key);
101
59
  records.set(key, next);
102
60
  if (unchanged)
103
61
  return;
104
62
  while (records.size > MAX_THREADS) {
105
- // Oldest bystander first; only when every record is a thread the agent takes part in does age
106
- // alone decide. NEVER the key just merged: it is the most recently touched record, so evicting
107
- // it contradicts the recency policy re-established above — and worse, a channel that records a
108
- // thread's humans and its own participation in two steps would then write the second half onto
109
- // an empty record, producing "answered here, heard nobody" and a permanent bare-message admit.
63
+ // Oldest bystander first; only when every record is a thread the agent takes part in does age alone decide.
110
64
  let evict;
111
65
  for (const [candidate, record] of records) {
112
66
  if (candidate === key)
@@ -125,9 +79,8 @@ export function createThreadParticipants(path, label) {
125
79
  saveStateFile(path, Object.fromEntries(records));
126
80
  }
127
81
  catch (error) {
128
- // Cache only: memory stays correct for this process, and the whole map is rewritten on the
129
- // next successful merge — so a failed write costs durability only until then, and a thread
130
- // whose record is lost simply needs a mention to re-enter.
82
+ // Cache only: memory stays correct for this process, and the whole map is rewritten on the next successful
83
+ // merge.
131
84
  log.warn(`${label} could not persist thread participation ${path}: ${String(error)}`);
132
85
  }
133
86
  },
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The one thing a chat transport needs from its CALLER, and the one place that decision is written.
3
+ *
4
+ * How a platform SIGNALS a rate limit and how long it asks us to wait are real differences and stay
5
+ * in each `*-api.ts` (Telegram's `parameters.retry_after`, Slack's `retry-after` header, Feishu's
6
+ * `code`). WHETHER A WRITE IS WORTH WAITING FOR is not a platform difference at all: it is a fact
7
+ * about the write, and only the caller has it.
8
+ *
9
+ * A live-preview FRAME is a full snapshot — the next frame redraws it and the terminal write
10
+ * supersedes it — so absorbing a rate-limit backoff for one parks the answer, and every turn queued
11
+ * behind it, to deliver a view nobody needs (a single 429 on a preview edit can cost ~90 s on Telegram and
12
+ * Slack). Such a write passes
13
+ * {@link DROPPABLE_FRAME}: the frame is lost, the answer is not.
14
+ *
15
+ * NOT every write. A write whose content exists only in that call keeps the default budget —
16
+ * Telegram's placeholder `sendMessage` (every later frame needs its id), Slack's ordered
17
+ * `chat.appendStream` (each append carries its own content), any terminal/settle write.
18
+ */
19
+ /** Per-call transport options, for the distinction a transport cannot make for itself. Readonly:
20
+ * {@link DROPPABLE_FRAME} is one process-wide instance handed to three different transports. */
21
+ export interface CallOptions {
22
+ /** Rate-limit attempts this call absorbs. Omitted = the transport's default budget. */
23
+ readonly retries?: number;
24
+ }
25
+ /** A write the next one supersedes: never wait out a rate limit for it. */
26
+ export declare const DROPPABLE_FRAME: CallOptions;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The one thing a chat transport needs from its CALLER, and the one place that decision is written.
3
+ *
4
+ * How a platform SIGNALS a rate limit and how long it asks us to wait are real differences and stay
5
+ * in each `*-api.ts` (Telegram's `parameters.retry_after`, Slack's `retry-after` header, Feishu's
6
+ * `code`). WHETHER A WRITE IS WORTH WAITING FOR is not a platform difference at all: it is a fact
7
+ * about the write, and only the caller has it.
8
+ *
9
+ * A live-preview FRAME is a full snapshot — the next frame redraws it and the terminal write
10
+ * supersedes it — so absorbing a rate-limit backoff for one parks the answer, and every turn queued
11
+ * behind it, to deliver a view nobody needs (a single 429 on a preview edit can cost ~90 s on Telegram and
12
+ * Slack). Such a write passes
13
+ * {@link DROPPABLE_FRAME}: the frame is lost, the answer is not.
14
+ *
15
+ * NOT every write. A write whose content exists only in that call keeps the default budget —
16
+ * Telegram's placeholder `sendMessage` (every later frame needs its id), Slack's ordered
17
+ * `chat.appendStream` (each append carries its own content), any terminal/settle write.
18
+ */
19
+ /** A write the next one supersedes: never wait out a rate limit for it. */
20
+ export const DROPPABLE_FRAME = { retries: 0 };