@fastagent-sh/fastagent 0.21.0 → 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 -43
  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,11 @@
1
1
  /**
2
- * Wake ALARMS for the AgentCore deployment: the piece that makes the agent's self-scheduled
3
- * wake-ups (`wake`) reliable on a host with NO resident process. The mechanism, end to end:
4
- *
5
- * wake written → wakeups store save → the SINK here → POST the full pending set to the
6
- * forwarder's reserved path → the forwarder (which has the AWS SDK + an IAM role) mirrors each
7
- * pending wake-up into a ONE-SHOT EventBridge schedule (`at(fireAt)`, self-deleting) → at the
8
- * instant, EventBridge pokes the forwarder → InvokeAgentRuntime wakes the container → the boot /
9
- * 30s wake pump finds the due entry and fires it (the existing overdue catch-up — no new fire
10
- * path). A RECURRING wake re-arms itself: its claim advances `fireAt` in the store, which is a
11
- * save, which re-runs this sink, which re-upserts its alarm for the next occurrence.
12
- *
13
- * The container itself never calls AWS (no SDK dependency, no SigV4, no credential chain): it only
14
- * POSTs to its own deployment's public forwarder URL, authenticated by a shared secret
15
- * (`FASTAGENT_WAKE_SECRET`, a CloudFormation NoEcho parameter both sides receive). The URL is not
16
- * baked anywhere — the forwarder INJECTS it into every envelope it forwards (it resolves its own
17
- * Function URL at cold start), and the adapter persists it here; every wake write happens inside a
18
- * turn, and every ingress turn arrived through an envelope, so the URL is always known by then.
19
- *
20
- * Reconciliation is DECLARATIVE (the sink is told only THAT the store changed and reads the full
21
- * pending set back per attempt, so no retry ever carries a stale view) and deletion is lazy: a
22
- * cancelled wake-up's alarm still fires its poke, finds nothing due, and self-deletes — a harmless
23
- * wasted wake-up of the box, traded for never needing list/delete choreography.
2
+ * Wake ALARMS for the AgentCore deployment: the piece that makes the agent's self-scheduled wake-ups (`wake`) reliable
3
+ * on a host with NO resident process.
24
4
  */
25
5
  import { readFileSync } from "node:fs";
6
+ import * as Clock from "effect/Clock";
7
+ import * as Effect from "effect/Effect";
8
+ import { PortFailure, portError, portJoin, portRequest } from "../effect-port.js";
26
9
  import { RESERVED_PATHS } from "../channels/agentcore-protocol.js";
27
10
  import { beginWork } from "../channels/busy.js";
28
11
  import { log } from "../log.js";
@@ -30,10 +13,8 @@ import { scheduleFile, writeScheduleFile } from "./state.js";
30
13
  import { listWakeups } from "./wakeups.js";
31
14
  const URL_FILE = "wake-alarm-url";
32
15
  /**
33
- * Persist the forwarder URL the adapter saw in an envelope (write-if-changed — envelopes arrive on
34
- * every turn, the file should not churn). Durable under <stateRoot>/schedule/ so a freshly booted
35
- * container whose FIRST action is a wake fire (recurring advance → save → sink) knows the URL
36
- * before any envelope of its own has arrived.
16
+ * Persist the forwarder URL the adapter saw in an envelope (write-if-changed — envelopes arrive on every turn, the
17
+ * file should not churn).
37
18
  */
38
19
  export function rememberWakeAlarmUrl(stateRoot, url) {
39
20
  if (readWakeAlarmUrl(stateRoot) === url)
@@ -44,21 +25,24 @@ export function rememberWakeAlarmUrl(stateRoot, url) {
44
25
  export function readWakeAlarmUrl(stateRoot) {
45
26
  try {
46
27
  const v = JSON.parse(readFileSync(scheduleFile(stateRoot, URL_FILE), "utf8"));
47
- return typeof v.url === "string" ? v.url : undefined;
28
+ if (typeof v?.url !== "string" || v.url === "")
29
+ throw new Error("expected a nonempty url string");
30
+ return v.url;
48
31
  }
49
- catch {
50
- return undefined; // absent/corrupt — the next envelope rewrites it
32
+ catch (cause) {
33
+ if (cause.code === "ENOENT")
34
+ return undefined;
35
+ throw new Error(`wake-alarm URL ${scheduleFile(stateRoot, URL_FILE)} is unreadable: ${String(cause)}`, { cause });
51
36
  }
52
37
  }
53
- /** How many times one alarm sync is retried before giving up (each store mutation and every boot
54
- * restart the cycle, and the pending store is the durable desired state — so "give up" means "until
55
- * the next mirror", never "lost"). */
38
+ /**
39
+ * How many times one alarm sync is retried before giving up (each store mutation and every boot restart the cycle, and
40
+ * the pending store is the durable desired state — so "give up" means "until the next mirror", never "lost").
41
+ */
56
42
  export const MAX_SYNC_ATTEMPTS = 5;
57
43
  const RETRY_BASE_MS = 2_000;
58
44
  const SYNC_TIMEOUT_MS = 10_000;
59
- /** Alarms due within this margin are NOT mirrored: the box is awake handling them right now (that is
60
- * how their fireAt got written/claimed), and a past `at()` would only fail at the Scheduler API —
61
- * filtering them client-side keeps every forwarder failure a REAL one worth retrying. */
45
+ /** Alarms due within this margin are NOT mirrored. */
62
46
  const DUE_MARGIN_MS = 5_000;
63
47
  /** Pending wake-ups → the desired alarm set, minus already-due entries (see {@link DUE_MARGIN_MS}). */
64
48
  export function toAlarms(pending, now) {
@@ -67,83 +51,72 @@ export function toAlarms(pending, now) {
67
51
  .map((w) => ({ id: w.id, at: w.fireAt }));
68
52
  }
69
53
  /**
70
- * Build the wakeups sink for an AgentCore deployment (registered via `setWakeupsSink` by `start`
71
- * when `FASTAGENT_AGENTCORE=1` + `FASTAGENT_WAKE_SECRET` are present). Fire-and-forget by contract:
72
- * failures are logged, never thrown — a broken alarm degrades to the pre-alarm behavior (the wake
73
- * still fires on the next time the box happens to be awake), it must never break the store write.
74
- *
75
- * A single-flight RECONCILER, not a per-mutation delivery: a notification only marks the desired
76
- * state dirty, and the loop re-derives it from the store before every attempt. So a mutation during
77
- * a retry needs no ordering rule — there is one desired state, never two snapshots to rank — and the
78
- * {@link DUE_MARGIN_MS} filter is re-applied with a fresh clock, which a captured payload could not
79
- * do: entries that became due mid-retry would keep being POSTed with a past `at()`, a rejection no
80
- * number of retries could clear.
81
- *
82
- * Assumes ONE sink per process over ONE state root (what `start` builds): the loop keeps the root of
83
- * the call that started it, and its dirty flag is shared, so a second root would fold into the first
84
- * one's pass.
54
+ * Build the wakeups sink for an AgentCore deployment (registered via `setWakeupsSink` by `start` when
55
+ * `FASTAGENT_AGENTCORE=1` + `FASTAGENT_WAKE_SECRET` are present).
85
56
  */
86
57
  export function createWakeAlarmSink(options) {
87
- const { secret, fetchImpl = fetch, now = () => new Date() } = options;
88
- const delay = options.delay ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
89
- let running = false;
90
- let dirty = false;
91
- /** One POST of the CURRENT desired set. Returns false to retry, true when there is nothing left
92
- * to do (converged, or nothing this loop can act on). */
93
- async function attemptOnce(stateRoot, attempt) {
94
- const alarms = toAlarms(listWakeups(stateRoot), now());
95
- // Nothing future to mirror: converged. Deletion is lazy by design — alarms already mirrored for
96
- // cancelled wake-ups fire, find nothing, self-delete — so an empty set is never POSTed.
97
- if (alarms.length === 0)
98
- return true;
99
- const url = readWakeAlarmUrl(stateRoot);
100
- if (!url) {
101
- // Before the first envelope, or an unreadable state mount (readWakeAlarmUrl folds both into
102
- // undefined). The wake itself is stored; the next store mutation or boot re-mirrors it.
103
- log.warn("[schedule] wake alarm skipped — forwarder URL unavailable (not seen yet, or unreadable)");
104
- return true;
105
- }
106
- const body = { secret, alarms };
107
- try {
108
- const res = await fetchImpl(`${url.replace(/\/$/, "")}${RESERVED_PATHS.wakeAlarm}`, {
109
- method: "POST",
110
- headers: { "content-type": "application/json" },
111
- body: JSON.stringify(body),
112
- signal: AbortSignal.timeout(SYNC_TIMEOUT_MS),
58
+ return Effect.gen(function* () {
59
+ const clock = yield* Clock.Clock;
60
+ const fork = Effect.runForkWith(yield* Effect.context());
61
+ const { secret, fetchImpl = fetch, now = () => new Date(clock.currentTimeMillisUnsafe()), delay: pause } = options;
62
+ const delay = (ms) => (pause ? portJoin(() => pause(ms)) : Effect.sleep(ms));
63
+ let running = false;
64
+ let dirty = false;
65
+ /** One POST of the CURRENT desired set. */
66
+ const attemptOnce = (stateRoot, attempt) => Effect.gen(function* () {
67
+ const alarms = yield* Effect.try({
68
+ try: () => toAlarms(listWakeups(stateRoot), now()),
69
+ catch: (cause) => new PortFailure(cause),
113
70
  });
114
- if (res.ok)
71
+ // Nothing future to mirror: converged.
72
+ if (alarms.length === 0)
115
73
  return true;
116
- log.warn(`[schedule] wake alarm sync attempt ${attempt}/${MAX_SYNC_ATTEMPTS} failed: HTTP ${res.status}`);
117
- }
118
- catch (e) {
119
- log.warn(`[schedule] wake alarm sync attempt ${attempt}/${MAX_SYNC_ATTEMPTS} failed: ${String(e)}`);
120
- }
121
- return false;
122
- }
123
- async function reconcile(stateRoot) {
124
- // Counted as in-flight work: the retry loop is exactly the window where the box must not be
125
- // reclaimed — idle away mid-retry and a pending wake has no alarm until the next boot.
126
- const workDone = beginWork();
127
- // Consecutive failures ACROSS passes, not within one. A mid-retry mutation restarts the attempt
128
- // sequence (the backoff and the log's `n/N` are about the new desired set), but it must not renew
129
- // the budget: a store mutating faster than the backoff would keep the loop alive forever, and the
130
- // one loud line saying the forwarder is down — the per-attempt warns are filterable — would never
131
- // be reached. This counter is the thing that survives to reach it.
132
- let failures = 0;
133
- try {
74
+ const url = yield* Effect.try({
75
+ try: () => readWakeAlarmUrl(stateRoot),
76
+ catch: (cause) => new PortFailure(cause),
77
+ });
78
+ if (!url) {
79
+ log.warn("[schedule] wake alarm skipped — forwarder URL not seen yet");
80
+ return true;
81
+ }
82
+ const body = { secret, alarms };
83
+ return yield* portRequest(async (signal) => {
84
+ const res = await fetchImpl(`${url.replace(/\/$/, "")}${RESERVED_PATHS.wakeAlarm}`, {
85
+ method: "POST",
86
+ headers: { "content-type": "application/json" },
87
+ body: JSON.stringify(body),
88
+ signal,
89
+ });
90
+ return res;
91
+ }, SYNC_TIMEOUT_MS).pipe(Effect.match({
92
+ onSuccess: (res) => {
93
+ if (res.ok)
94
+ return true;
95
+ log.warn(`[schedule] wake alarm sync attempt ${attempt}/${MAX_SYNC_ATTEMPTS} failed: HTTP ${res.status}`);
96
+ return false;
97
+ },
98
+ onFailure: (error) => {
99
+ log.warn(`[schedule] wake alarm sync attempt ${attempt}/${MAX_SYNC_ATTEMPTS} failed: ${String(error.cause)}`);
100
+ return false;
101
+ },
102
+ }));
103
+ });
104
+ const reconcile = (stateRoot) => Effect.gen(function* () {
105
+ // Consecutive failures ACROSS passes, not within one.
106
+ let failures = 0;
134
107
  while (dirty && failures < MAX_SYNC_ATTEMPTS) {
135
108
  dirty = false;
136
109
  for (let attempt = 1; attempt <= MAX_SYNC_ATTEMPTS; attempt++) {
137
- if (await attemptOnce(stateRoot, attempt)) {
110
+ if (yield* attemptOnce(stateRoot, attempt)) {
138
111
  failures = 0;
139
112
  break;
140
113
  }
141
114
  failures++;
142
115
  if (attempt === MAX_SYNC_ATTEMPTS || failures >= MAX_SYNC_ATTEMPTS)
143
116
  break;
144
- await delay(RETRY_BASE_MS * attempt);
145
- // A mutation landed mid-retry: the desired state moved, so this budget is spent on a set
146
- // that no longer exists. Restart the attempt count against the new one.
117
+ yield* delay(RETRY_BASE_MS * attempt);
118
+ // A mutation landed mid-retry: the desired state moved, so this budget is spent on a set that no longer
119
+ // exists.
147
120
  if (dirty)
148
121
  break;
149
122
  }
@@ -152,26 +125,23 @@ export function createWakeAlarmSink(options) {
152
125
  log.error(`[schedule] wake alarm sync FAILED after ${MAX_SYNC_ATTEMPTS} attempts — pending wake-ups have no ` +
153
126
  `external alarm until the next store change or boot re-mirrors them`);
154
127
  }
155
- }
156
- catch (e) {
157
- // The END of the error path: nothing awaits this loop, so an escape is an unhandled rejection
158
- // that kills the container. `listWakeups` throws by design on an unreadable store (state.ts),
159
- // exactly the fault a wake alarm cannot fix — report it and leave the store write untouched,
160
- // which is this sink's whole contract. The next mutation or boot re-runs the mirror.
161
- log.error(`[schedule] wake alarm reconcile failed (alarms are stale until the next store change): ${String(e)}`);
162
- }
163
- finally {
164
- running = false;
165
- workDone();
166
- }
167
- }
168
- // Single-flight: a save arriving while the loop runs only marks it dirty, so a burst coalesces
169
- // into one more pass instead of one concurrent loop each.
170
- return (stateRoot) => {
171
- dirty = true;
172
- if (running)
173
- return;
174
- running = true;
175
- void reconcile(stateRoot);
176
- };
128
+ });
129
+ // Single-flight: a save arriving while the loop runs only marks it dirty, so a burst coalesces into one more pass
130
+ // instead of one concurrent loop each.
131
+ return (stateRoot) => {
132
+ dirty = true;
133
+ if (running)
134
+ return;
135
+ running = true;
136
+ fork(Effect.acquireUseRelease(Effect.sync(beginWork), () =>
137
+ // Claim notification precedes scheduler admission; retain busy ownership through that handoff.
138
+ Effect.yieldNow.pipe(Effect.andThen(reconcile(stateRoot)), Effect.catchCause((cause) => Effect.sync(() => {
139
+ // Store/clock faults cannot be repaired by another POST. A new mutation may retry the mirror.
140
+ log.error(`[schedule] wake alarm reconcile failed (alarms are stale until the next store change): ${String(portError(cause))}`);
141
+ }))), (done) => Effect.sync(() => {
142
+ running = false;
143
+ done();
144
+ })));
145
+ };
146
+ });
177
147
  }
@@ -4,30 +4,27 @@ export interface Wakeup {
4
4
  session: string;
5
5
  /** The instruction for the woken turn. */
6
6
  prompt: string;
7
- /** When to fire next (ISO). For a recurring wake this advances to the next cron instant on each fire. */
7
+ /** When to fire next (ISO). */
8
8
  fireAt: string;
9
- /** RECURRING: the cron expression (5-field). Absent = one-shot (fires once, removed). */
9
+ /** RECURRING: the cron expression (5-field). */
10
10
  cron?: string;
11
11
  /** IANA timezone for `cron` (default UTC). */
12
12
  tz?: string;
13
- /** ONE-SHOT ONLY: consecutive busy-defer attempts. A one-shot into a BUSY session (`code: session_busy`
14
- * — the turn never started) is deferred, bounded by {@link MAX_WAKE_ATTEMPTS}, then dropped. A RECURRING
15
- * occurrence never defers (this field never increments for it): a busy one is skipped on FIRST contact
16
- * — its claim already advanced the entry, and the next occurrence comes by definition. */
13
+ /** ONE-SHOT ONLY: consecutive busy-defer attempts. */
17
14
  attempts?: number;
18
15
  }
19
16
  /** The minimum delay a wake-up may be scheduled for — rejects a busy-loop (`wake in 1s`, then again…). */
20
17
  export declare const MIN_WAKE_MS = 60000;
21
- /** The cap on pending wake-ups PER SESSION — rejects one conversation's unbounded self-fan-out without
22
- * letting it starve others' quota (a global cap would make one chatty session a DoS on everyone else's
23
- * `wake`, since a multi-user deploy is one session per chat). */
18
+ /**
19
+ * The cap on pending wake-ups PER SESSION — rejects one conversation's unbounded self-fan-out without letting it
20
+ * starve others' quota (a global cap would make one chatty session a DoS on everyone else's `wake`, since a multi-user
21
+ * deploy is one session per chat).
22
+ */
24
23
  export declare const MAX_PENDING_WAKEUPS = 20;
25
- /** How many times a busy/transient wake is retried (deferred) before being dropped. At the ~30s poll a
26
- * wake into an active conversation fires in the first gap between the user's turns; this generous
27
- * ceiling (~1h) only gives up on a pathologically stuck session (then logs, operator-visible). */
24
+ /** How many times a busy/transient wake is retried (deferred) before being dropped. */
28
25
  export declare const MAX_WAKE_ATTEMPTS = 120;
29
26
  export declare function setWakeupsSink(sink: ((stateRoot: string) => void) | undefined): void;
30
- /** The current pending wake-ups (`fastagent schedule list` uses this). */
27
+ /** The current pending wake-ups — read by the wake-alarm sink, which mirrors them into an external clock. */
31
28
  export declare function listWakeups(stateRoot: string): Wakeup[];
32
29
  export type AddWakeupResult = {
33
30
  ok: true;
@@ -37,12 +34,7 @@ export type AddWakeupResult = {
37
34
  ok: false;
38
35
  error: string;
39
36
  };
40
- /**
41
- * Add a wake-up — one-shot (`fireAt`) or recurring (`cron`/`tz`, `fireAt` = the first instant) —
42
- * enforcing the guardrails (min delay / min recurring gap, per-session pending cap). Returns a
43
- * MODEL-facing error string on rejection (the `wake` tool passes it back so the model can adjust),
44
- * never throws for a bad request.
45
- */
37
+ /** Add a wake-up — one-shot (`fireAt`) or recurring (`cron`/`tz`, `fireAt` = the first instant). */
46
38
  export declare function addWakeup(stateRoot: string, input: {
47
39
  session: string;
48
40
  prompt: string;
@@ -51,30 +43,22 @@ export declare function addWakeup(stateRoot: string, input: {
51
43
  tz?: string;
52
44
  }, now?: Date): AddWakeupResult;
53
45
  /**
54
- * Remove a wake-up by id. `session` (when given — the agent's `unwake`) must ALSO match, so a
55
- * conversation can only cancel its OWN wake-ups; the operator's `schedule cancel` passes none.
56
- * Returns whether anything was removed.
46
+ * Remove ONE pending wake-up, and only from the session that made it.
47
+ *
48
+ * SESSION-SCOPED WITH NO WAY AROUND IT, because `unwake` is the only caller: an agent cancels its own alarm, and
49
+ * cancelling another conversation's silently is a cost with no matching benefit. There is no operator command: a
50
+ * wake-up only fires while a serve is running, and a running serve is one whose
51
+ * session can be spoken to (`POST /invoke`, the control plane's `follow_up`, or the chat thread it lives in), so
52
+ * "the alarm is loose" and "the agent is unreachable" cannot both be true. The last resort is editing
53
+ * `<stateRoot>/schedule/wakeups.json`, which needs no command.
57
54
  */
58
- export declare function removeWakeup(stateRoot: string, id: string, session?: string): boolean;
55
+ export declare function removeWakeup(stateRoot: string, id: string, session: string): boolean;
59
56
  /**
60
- * CLAIM the FIRST due wake-up (at or before `now`) in stored order, remove + return it, or undefined if
61
- * none is due. Stored order, not earliest-`fireAt`: the caller drains them all in one poll, so order among
62
- * due entries doesn't matter. One at a time on purpose — the caller fires it before claiming the next, so a
63
- * crash loses AT MOST one wake-up (claiming a whole due batch up front would lose the lot). Claim-before-fire
64
- * = a turn EXECUTES at most once per claim; a wake that couldn't start (busy session) is re-claimed via
65
- * {@link deferWakeup}.
57
+ * CLAIM the FIRST due wake-up (at or before `now`) in stored order, remove + return it, or undefined if none is due.
66
58
  */
67
59
  export declare function takeFirstDueWakeup(stateRoot: string, now?: Date): Wakeup | undefined;
68
60
  /**
69
- * Re-schedule a ONE-SHOT wake whose fire failed TRANSIENTLY (its session was busy — a channel is mid-turn
70
- * on it), deferred to `fireAt`. Returns false (dropped, NOT re-added) once attempts exceed
71
- * {@link MAX_WAKE_ATTEMPTS} — a one-shot has no "next time", so a busy-skip that just dropped it would lose
72
- * it forever; bounded retries, then give up visibly. A RECURRING occurrence is never deferred: its claim
73
- * already advanced the entry, and its next occurrence comes by definition — a busy one is skipped + audited.
74
- *
75
- * Known residual: between the claim (removed from the store) and this re-add there is a microtask-scale
76
- * window (the busy reject yields before any engine IO) where an `unwake` for this id reports "not found"
77
- * and the defer then resurrects it — the one-shot cousin of the recurring resurrection the advance-in-place
78
- * claim eliminated. Accepted: closing it needs a claim-lease with expiry, disproportionate to the window.
61
+ * Re-schedule a ONE-SHOT wake whose fire failed TRANSIENTLY (its session was busy — a channel is mid-turn on it),
62
+ * deferred to `fireAt`.
79
63
  */
80
64
  export declare function deferWakeup(stateRoot: string, w: Wakeup, fireAt: Date): boolean;
@@ -1,14 +1,6 @@
1
1
  /**
2
- * The agent's self-scheduled wake-ups — the SECOND producer of scheduled invocations (the first is the
3
- * author's `schedules/` files). The `wake` tool writes one here; the scheduler polls and fires it back
4
- * into the SAME session so the agent resumes the conversation it was in. Persisted
5
- * (`<stateRoot>/schedule/wakeups.json`) so a wake survives a restart. ONE-SHOT (`in`) or RECURRING
6
- * (`cron` — the entry keeps its id; each fire re-arms `fireAt` to the next cron instant).
7
- *
8
- * Guardrails (self-scheduling is a real runaway surface): a minimum one-shot delay (no busy-looping), a
9
- * minimum RECURRING gap (a high-frequency recurring burns tokens forever — stricter than one-shot), a
10
- * per-session pending cap (no fan-out, no cross-session quota DoS; a recurring occupies a slot for its
11
- * whole life), and the agent's `unwake` / the operator's `schedule cancel` as the kill switches.
2
+ * The agent's self-scheduled wake-ups — the SECOND producer of scheduled invocations (the first is the author's
3
+ * `routines/` files).
12
4
  */
13
5
  import { randomUUID } from "node:crypto";
14
6
  import { log } from "../log.js";
@@ -16,20 +8,17 @@ import { cronError, nextRun } from "./cron.js";
16
8
  import { readScheduleFile, scheduleFile, writeScheduleFile } from "./state.js";
17
9
  /** The minimum delay a wake-up may be scheduled for — rejects a busy-loop (`wake in 1s`, then again…). */
18
10
  export const MIN_WAKE_MS = 60_000; // 1 minute
19
- /** The cap on pending wake-ups PER SESSION — rejects one conversation's unbounded self-fan-out without
20
- * letting it starve others' quota (a global cap would make one chatty session a DoS on everyone else's
21
- * `wake`, since a multi-user deploy is one session per chat). */
11
+ /**
12
+ * The cap on pending wake-ups PER SESSION — rejects one conversation's unbounded self-fan-out without letting it
13
+ * starve others' quota (a global cap would make one chatty session a DoS on everyone else's `wake`, since a multi-user
14
+ * deploy is one session per chat).
15
+ */
22
16
  export const MAX_PENDING_WAKEUPS = 20;
23
- /** How many times a busy/transient wake is retried (deferred) before being dropped. At the ~30s poll a
24
- * wake into an active conversation fires in the first gap between the user's turns; this generous
25
- * ceiling (~1h) only gives up on a pathologically stuck session (then logs, operator-visible). */
17
+ /** How many times a busy/transient wake is retried (deferred) before being dropped. */
26
18
  export const MAX_WAKE_ATTEMPTS = 120;
27
- /** The minimum gap between two consecutive fires of a RECURRING wake — stricter than the one-shot floor:
28
- * a recurring runs forever, so a tight cron is a permanent token burner, not a one-time mistake. */
19
+ /** The minimum gap between two consecutive fires of a RECURRING wake. */
29
20
  const MIN_RECURRING_GAP_MS = 10 * 60_000; // 10 minutes
30
- /** A stored entry is a real Wakeup: the fields are present and `fireAt` is a parseable date. A malformed
31
- * one (bad/missing fireAt) would compare NaN <= now = false forever — never due, never cleared, but still
32
- * eating the pending quota. So validate at this IO boundary and drop it (warn), like a corrupt file. */
21
+ /** A stored entry is a real Wakeup: the fields are present and `fireAt` is a parseable date. */
33
22
  function isWakeup(e) {
34
23
  if (!e || typeof e !== "object")
35
24
  return false;
@@ -41,23 +30,11 @@ function isWakeup(e) {
41
30
  !Number.isNaN(Date.parse(w.fireAt)) &&
42
31
  (w.attempts === undefined || typeof w.attempts === "number") && // a non-number would make deferWakeup's count NaN
43
32
  (w.tz === undefined || typeof w.tz === "string") &&
44
- // A stored cron must PARSE: a bad one would throw inside the claim's nextRun — before the advance-save
45
- // — so its fireAt never moves, it stays first-due forever, and every wakeup behind it starves (a poison
46
- // pill worse than the bad-fireAt zombie this validator already guards).
33
+ // A stored cron must PARSE: a bad one would throw inside the claim's nextRun.
47
34
  (w.cron === undefined ||
48
35
  (typeof w.cron === "string" && cronError(w.cron, typeof w.tz === "string" ? w.tz : undefined) === undefined)));
49
36
  }
50
- /**
51
- * The wake-ALARM sink: notified after EVERY wakeups-store mutation. A notification, not a delivery
52
- * — it carries no set, because the sink reconciles by re-reading the store, and handing it a set
53
- * captured here would go stale in its retries. The AgentCore deployment registers one
54
- * (schedule/wake-alarm.ts) that mirrors pending wake-ups into one-shot EventBridge schedules — the
55
- * external clock that makes `wake` reliable on a host with no resident process. Neutral seam: this
56
- * module knows only "someone wants to observe changes"; a resident host registers nothing and
57
- * behaves exactly as before. The sink is fire-and-forget and must own its errors. The guard below
58
- * catches a SYNCHRONOUS throw so a broken alarm never corrupts a store write; an async sink returns
59
- * before it does any work, so its rejections are ITS to terminate — nothing here can see them.
60
- */
37
+ /** The wake-ALARM sink: notified after EVERY wakeups-store mutation. */
61
38
  let wakeupsSink;
62
39
  export function setWakeupsSink(sink) {
63
40
  wakeupsSink = sink;
@@ -67,8 +44,7 @@ function load(stateRoot) {
67
44
  if (v === undefined)
68
45
  return []; // absent (first run) or a corrupt file readScheduleFile already warned on
69
46
  if (!Array.isArray(v)) {
70
- // Valid JSON but the wrong SHAPE (an object, say) — fail-visible, don't silently drop the whole store
71
- // (a per-entry malformed drop already warns; a whole-store shape error must too).
47
+ // Valid JSON but the wrong SHAPE (an object, say).
72
48
  log.warn(`[schedule] wakeups store is not an array — ignoring it: ${JSON.stringify(v).slice(0, 80)}`);
73
49
  return [];
74
50
  }
@@ -90,25 +66,19 @@ function save(stateRoot, wakeups) {
90
66
  log.error(`[schedule] wake-alarm sink failed (store write is unaffected): ${String(e)}`);
91
67
  }
92
68
  }
93
- /** The current pending wake-ups (`fastagent schedule list` uses this). */
69
+ /** The current pending wake-ups — read by the wake-alarm sink, which mirrors them into an external clock. */
94
70
  export function listWakeups(stateRoot) {
95
71
  return load(stateRoot);
96
72
  }
97
- /**
98
- * Add a wake-up — one-shot (`fireAt`) or recurring (`cron`/`tz`, `fireAt` = the first instant) —
99
- * enforcing the guardrails (min delay / min recurring gap, per-session pending cap). Returns a
100
- * MODEL-facing error string on rejection (the `wake` tool passes it back so the model can adjust),
101
- * never throws for a bad request.
102
- */
73
+ /** Add a wake-up — one-shot (`fireAt`) or recurring (`cron`/`tz`, `fireAt` = the first instant). */
103
74
  export function addWakeup(stateRoot, input, now = new Date()) {
104
75
  let fireAtDate;
105
76
  if (input.cron !== undefined) {
106
77
  const err = cronError(input.cron, input.tz);
107
78
  if (err)
108
79
  return { ok: false, error: `invalid cron/tz: ${err}` };
109
- // A recurring wake runs FOREVER — gate its frequency harder than a one-shot: the gap between the next
110
- // two instants must be ≥ the recurring floor. (First-pair heuristic; an irregular cron with one tight
111
- // pair can slip through — an accepted ceiling, the per-session cap still bounds total load.)
80
+ // A recurring wake runs FOREVER — gate its frequency harder than a one-shot: the gap between the next two
81
+ // instants must be ≥ the recurring floor.
112
82
  const first = nextRun(input.cron, input.tz, now);
113
83
  const second = first && nextRun(input.cron, input.tz, first);
114
84
  if (!first || !second)
@@ -132,9 +102,8 @@ export function addWakeup(stateRoot, input, now = new Date()) {
132
102
  const all = load(stateRoot);
133
103
  const mine = all.filter((w) => w.session === input.session);
134
104
  if (mine.length >= MAX_PENDING_WAKEUPS) {
135
- // List what's pending WITH a prompt preview: "unwake one" is only actionable if the model has the
136
- // ids AND can choose by meaning — both were returned when set, but that may be buried far back in
137
- // the conversation.
105
+ // List what's pending WITH a prompt preview: "unwake one" is only actionable if the model has the ids AND can
106
+ // choose by meaning.
138
107
  const pending = mine
139
108
  .map((w) => `${w.id}${w.cron ? ` (recurring "${w.cron}")` : ""} at ${w.fireAt}: ${w.prompt.length > 60 ? `${w.prompt.slice(0, 60)}…` : w.prompt}`)
140
109
  .join("; ");
@@ -152,25 +121,25 @@ export function addWakeup(stateRoot, input, now = new Date()) {
152
121
  return { ok: true, id, fireAt };
153
122
  }
154
123
  /**
155
- * Remove a wake-up by id. `session` (when given — the agent's `unwake`) must ALSO match, so a
156
- * conversation can only cancel its OWN wake-ups; the operator's `schedule cancel` passes none.
157
- * Returns whether anything was removed.
124
+ * Remove ONE pending wake-up, and only from the session that made it.
125
+ *
126
+ * SESSION-SCOPED WITH NO WAY AROUND IT, because `unwake` is the only caller: an agent cancels its own alarm, and
127
+ * cancelling another conversation's silently is a cost with no matching benefit. There is no operator command: a
128
+ * wake-up only fires while a serve is running, and a running serve is one whose
129
+ * session can be spoken to (`POST /invoke`, the control plane's `follow_up`, or the chat thread it lives in), so
130
+ * "the alarm is loose" and "the agent is unreachable" cannot both be true. The last resort is editing
131
+ * `<stateRoot>/schedule/wakeups.json`, which needs no command.
158
132
  */
159
133
  export function removeWakeup(stateRoot, id, session) {
160
134
  const all = load(stateRoot);
161
- const kept = all.filter((w) => !(w.id === id && (session === undefined || w.session === session)));
135
+ const kept = all.filter((w) => !(w.id === id && w.session === session));
162
136
  if (kept.length === all.length)
163
137
  return false;
164
138
  save(stateRoot, kept);
165
139
  return true;
166
140
  }
167
141
  /**
168
- * CLAIM the FIRST due wake-up (at or before `now`) in stored order, remove + return it, or undefined if
169
- * none is due. Stored order, not earliest-`fireAt`: the caller drains them all in one poll, so order among
170
- * due entries doesn't matter. One at a time on purpose — the caller fires it before claiming the next, so a
171
- * crash loses AT MOST one wake-up (claiming a whole due batch up front would lose the lot). Claim-before-fire
172
- * = a turn EXECUTES at most once per claim; a wake that couldn't start (busy session) is re-claimed via
173
- * {@link deferWakeup}.
142
+ * CLAIM the FIRST due wake-up (at or before `now`) in stored order, remove + return it, or undefined if none is due.
174
143
  */
175
144
  export function takeFirstDueWakeup(stateRoot, now = new Date()) {
176
145
  const all = load(stateRoot);
@@ -179,11 +148,8 @@ export function takeFirstDueWakeup(stateRoot, now = new Date()) {
179
148
  return undefined;
180
149
  const w = all[idx];
181
150
  if (w.cron !== undefined) {
182
- // RECURRING claim = ADVANCE IN PLACE: the entry STAYS in the store with fireAt pushed to the next cron
183
- // instant (attempts cleared). So (a) `unwake`/`schedule cancel` work at ANY moment — including inside
184
- // the woken turn itself, the documented way to stop a done recurring job (a remove+re-add claim would
185
- // resurrect what that turn just cancelled); and (b) a crash mid-fire loses at most ONE occurrence,
186
- // never the recurrence — matching the static cron schedules' claim semantics.
151
+ // RECURRING claim = ADVANCE IN PLACE: the entry STAYS in the store with fireAt pushed to the next cron instant
152
+ // (attempts cleared).
187
153
  const next = nextRun(w.cron, w.tz, now);
188
154
  if (next)
189
155
  all[idx] = { ...w, fireAt: next.toISOString(), attempts: undefined };
@@ -197,16 +163,8 @@ export function takeFirstDueWakeup(stateRoot, now = new Date()) {
197
163
  return w;
198
164
  }
199
165
  /**
200
- * Re-schedule a ONE-SHOT wake whose fire failed TRANSIENTLY (its session was busy — a channel is mid-turn
201
- * on it), deferred to `fireAt`. Returns false (dropped, NOT re-added) once attempts exceed
202
- * {@link MAX_WAKE_ATTEMPTS} — a one-shot has no "next time", so a busy-skip that just dropped it would lose
203
- * it forever; bounded retries, then give up visibly. A RECURRING occurrence is never deferred: its claim
204
- * already advanced the entry, and its next occurrence comes by definition — a busy one is skipped + audited.
205
- *
206
- * Known residual: between the claim (removed from the store) and this re-add there is a microtask-scale
207
- * window (the busy reject yields before any engine IO) where an `unwake` for this id reports "not found"
208
- * and the defer then resurrects it — the one-shot cousin of the recurring resurrection the advance-in-place
209
- * claim eliminated. Accepted: closing it needs a claim-lease with expiry, disproportionate to the window.
166
+ * Re-schedule a ONE-SHOT wake whose fire failed TRANSIENTLY (its session was busy — a channel is mid-turn on it),
167
+ * deferred to `fireAt`.
210
168
  */
211
169
  export function deferWakeup(stateRoot, w, fireAt) {
212
170
  const attempts = (w.attempts ?? 0) + 1;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * THE GATE: refuse to run while a declaration for what is ABOUT TO RUN has no value.
3
+ *
4
+ * `declared-secrets.ts` answers "which env vars did this code declare, and which have no value" —
5
+ * a predicate. This module owns the three decisions that turn that predicate into a refusal, for all five call
6
+ * sites (the agent opener, the scheduler start, channel loading, `fastagent tool`, `fastagent routine run`):
7
+ *
8
+ * 1. WHICH declarations gate this run. A serve mounts every tool, schedule and channel, so all of
9
+ * them gate it; `fastagent tool <name>` / `fire <name>` run exactly one owner, and requiring the
10
+ * credentials of the ones they are not running blocks a machine that is configured correctly for
11
+ * the job at hand. That is why declarations travel keyed BY OWNER.
12
+ * 2. ORDER against load failures. The refusal throws, and a caller that prints `failures` after it
13
+ * gets a value back would lose them: a broken `tools/x.ts` beside an unset declaration would
14
+ * stay invisible until the author fixed the environment and ran again. So the gate reports them
15
+ * itself, on the refusal path, before throwing. A caller that also reports on its own success
16
+ * path is correct; one that reports before gating merely repeats a line, which is cosmetic —
17
+ * the signal cannot be lost either way.
18
+ * 3. The failure SHAPE. This throws a plain `Error` (a user-fixable startup problem). CLI commands
19
+ * whose call is synchronous route it through the process boundary with `gateSecretsOrExit`
20
+ * (cli/fail.ts) so the author sees the one line naming the file, never a Node stack.
21
+ */
22
+ import { type DeclaredSecret } from "./declared-secrets.ts";
23
+ import { type ModuleLoadFailure } from "./loader.ts";
24
+ export declare function gateSecrets(input: {
25
+ /** Owner (tool / schedule / channel name) → what it declared, as the loaders return it. */
26
+ declared: ReadonlyMap<string, readonly DeclaredSecret[]>;
27
+ /** What could not be loaded from the same directory — reported before any refusal (decision 2). */
28
+ failures: readonly ModuleLoadFailure[];
29
+ /** The ONE owner about to run; omitted means every one of them is about to run. */
30
+ owner?: string;
31
+ env?: NodeJS.ProcessEnv;
32
+ }): void;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * THE GATE: refuse to run while a declaration for what is ABOUT TO RUN has no value.
3
+ *
4
+ * `declared-secrets.ts` answers "which env vars did this code declare, and which have no value" —
5
+ * a predicate. This module owns the three decisions that turn that predicate into a refusal, for all five call
6
+ * sites (the agent opener, the scheduler start, channel loading, `fastagent tool`, `fastagent routine run`):
7
+ *
8
+ * 1. WHICH declarations gate this run. A serve mounts every tool, schedule and channel, so all of
9
+ * them gate it; `fastagent tool <name>` / `fire <name>` run exactly one owner, and requiring the
10
+ * credentials of the ones they are not running blocks a machine that is configured correctly for
11
+ * the job at hand. That is why declarations travel keyed BY OWNER.
12
+ * 2. ORDER against load failures. The refusal throws, and a caller that prints `failures` after it
13
+ * gets a value back would lose them: a broken `tools/x.ts` beside an unset declaration would
14
+ * stay invisible until the author fixed the environment and ran again. So the gate reports them
15
+ * itself, on the refusal path, before throwing. A caller that also reports on its own success
16
+ * path is correct; one that reports before gating merely repeats a line, which is cosmetic —
17
+ * the signal cannot be lost either way.
18
+ * 3. The failure SHAPE. This throws a plain `Error` (a user-fixable startup problem). CLI commands
19
+ * whose call is synchronous route it through the process boundary with `gateSecretsOrExit`
20
+ * (cli/fail.ts) so the author sees the one line naming the file, never a Node stack.
21
+ */
22
+ import { allSecrets, describeSecrets, missingSecrets } from "./declared-secrets.js";
23
+ import { reportModuleLoadFailures } from "./loader.js";
24
+ export function gateSecrets(input) {
25
+ const scope = input.owner === undefined ? allSecrets(input.declared) : (input.declared.get(input.owner) ?? []);
26
+ const missing = missingSecrets(scope, input.env);
27
+ if (missing.length === 0)
28
+ return;
29
+ reportModuleLoadFailures(input.failures);
30
+ throw new Error(`missing required secrets: ${describeSecrets(missing)} — set them in .secrets/.env (or the ` +
31
+ `environment). Deployed, they travel as deploy secrets; a tool that should run WITHOUT the ` +
32
+ `value must not declare it.`);
33
+ }