@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,14 +1,109 @@
1
1
  /** Path of a JSON file under `<stateRoot>/schedule/`. */
2
2
  export declare function scheduleFile(stateRoot: string, name: string): string;
3
- /**
4
- * Read a JSON state file. Missing is normal (first run → undefined); a corrupt file degrades visibly
5
- * (warn + undefined) — schedule state is recoverable, not worth refusing to boot over; an unreadable
6
- * file (permissions/IO) throws — a real environment fault the operator must fix, not hide behind empty
7
- * state. The caller owns shape validation (an IO boundary).
8
- */
9
3
  export declare function readScheduleFile(path: string): unknown;
10
4
  export declare function writeScheduleFile(path: string, value: unknown): void;
11
- /** name → last-fired ISO timestamp. */
12
- export type Fires = Record<string, string>;
13
- export declare function loadFires(stateRoot: string): Fires;
14
- export declare function saveFires(stateRoot: string, fires: Fires): void;
5
+ /**
6
+ * How a fired slot ended. There is no `deferred` or `stale` here because neither has a claim: a deferred wake-up was
7
+ * never claimed, and a stale delivery is refused before one is taken. Both are log lines.
8
+ */
9
+ /**
10
+ * How a claimed occurrence ended.
11
+ *
12
+ * `skipped` is NOT a failure and is the reason this is an enum rather than a boolean: a schedule's turns share one
13
+ * `routine:<name>` session, so an occurrence arriving while the previous one is still running is refused by that
14
+ * session, and recording it as `failed` made "the last run was still going" indistinguishable from "the model call
15
+ * died" — two things an operator reacts to completely differently. Every scheduler names this: Kubernetes calls it
16
+ * `concurrencyPolicy: Forbid`, Temporal calls it the `Skip` overlap policy, and neither treats it as an error.
17
+ */
18
+ export type FireOutcome = "completed" | "failed" | "skipped" | "interrupted";
19
+ /** One fired slot, as its claim file records it. `outcome` absent = claimed, never settled. */
20
+ export interface Fire {
21
+ /** The slot this fire was FOR (the claim's name). */
22
+ slot: string;
23
+ /** When the claim was taken (falls back to `slot` when the stamp is unusable). */
24
+ firedAt: string;
25
+ outcome?: FireOutcome;
26
+ ms?: number;
27
+ }
28
+ /**
29
+ * A schedule name becomes a path segment (its claims live in `claims/<name>/`), so the rule is exactly the safety
30
+ * boundary and nothing more: a name may not leave that directory. The name comes from a filename under `routines/`,
31
+ * which already cannot contain a separator — so what a stricter rule would actually reject is legal filenames
32
+ * (`每日简报`, `my schedule`), whose only symptom would be a schedule that silently never fires again.
33
+ *
34
+ * One definition, two enforcers: `discover.ts` refuses an illegal one where the author can see which file is wrong,
35
+ * and `claimDir` asserts it again for a programmatic caller.
36
+ */
37
+ export declare function isSafeScheduleName(name: string): boolean;
38
+ /**
39
+ * Why a slot is not ours. `duplicate` is ordinary (the same delivery arrived twice, or another scheduler took it and
40
+ * is running it now); `stale` means the schedule has moved past this instant and the slot will never run — a turn
41
+ * missing from the bill, which reads differently in a log.
42
+ */
43
+ export type SlotClaimOutcome = {
44
+ taken: true;
45
+ }
46
+ /** The same delivery arrived twice, or another scheduler took this slot and may be running it now. */
47
+ | {
48
+ taken: false;
49
+ why: "duplicate";
50
+ }
51
+ /** The schedule has moved past this instant: `newest` holds it, and this slot will never run. */
52
+ | {
53
+ taken: false;
54
+ why: "stale";
55
+ newest: string;
56
+ };
57
+ /**
58
+ * Take a cron slot, or report that it is not ours to take.
59
+ *
60
+ * `O_EXCL` is the whole mechanism: creating the file IS the decision, and the kernel gives it to exactly one creator,
61
+ * even with a second scheduler on the same state (two `start`s, a restart overlapping its predecessor, an external
62
+ * clock racing the resident one).
63
+ *
64
+ * Two ways to lose it, and both must hold beyond the pruning window: the slot's own claim exists, or a LATER slot has
65
+ * already been claimed — a delivery older than the newest claim is a stale replay (AWS keeps retrying an event for up
66
+ * to 24h), and firing it would bill a turn for an instant the schedule has already moved past.
67
+ *
68
+ * The claim carries the wall-clock instant it was taken (as `{"firedAt":"…"}`, the shape `settleClaim` writes the
69
+ * outcome back into), so the next boot can tell a fire that never reported from one that did
70
+ * (`markInterruptedFire`) without depending on a second file being written after it.
71
+ */
72
+ export declare function claimSlot(stateRoot: string, name: string, slot: Date, firedAt: Date): SlotClaimOutcome;
73
+ /**
74
+ * Every fire this state root still keeps for `name`, oldest first — bounded by `KEEP_CLAIMS` because the claims ARE
75
+ * the history. Nothing here grows: the pruning that keeps the claim gate cheap keeps the history's size fixed too,
76
+ * which is why there is no separate audit file to rotate (what the turn SAID is in its session, not here).
77
+ *
78
+ * THE HISTORY read, and its only caller is the read-only `routine history` — which translates a read fault into a
79
+ * one-line refusal. The serving boot reads `latestFire` instead: it asks about ONE claim, and a window of files kept
80
+ * for an operator to look at has no business deciding whether a service starts.
81
+ */
82
+ export declare function readFires(stateRoot: string, name: string): Fire[];
83
+ /**
84
+ * The NEWEST fire recorded for `name` — the one claim the serving boot reads, for the two planes that must agree
85
+ * about it: where catch-up resumes (`firedAt`) and whether that fire ever reported (`outcome`). Deriving those
86
+ * separately is how they come to disagree about the same file.
87
+ *
88
+ * Only the newest matters to either. A schedule's resident loop runs one turn at a time, so it can leave at most one
89
+ * unsettled claim behind; an older unsettled one needs a second writer, and the second-writer topology (an external
90
+ * clock) skips the reconciler entirely. Reading the whole retained window here would instead make a file that is
91
+ * 400 fires old — kept only so an operator can look at it — able to fail a boot.
92
+ *
93
+ * A read fault on THIS claim stays fatal: it decides the stale gate and the resume point, and running blind on it
94
+ * fires a slot twice or never.
95
+ */
96
+ export declare function latestFire(stateRoot: string, name: string): Fire | undefined;
97
+ /**
98
+ * Write back how the claimed fire ended, into the claim file itself.
99
+ *
100
+ * The SAME file, because a second one would reintroduce the window this whole design closes: a killed process would
101
+ * leave a claimed slot that nothing accounts for. An unsettled claim IS the record of an interrupted fire.
102
+ *
103
+ * NOT `writeFileAtomic`: its temp would land in the directory `claimSlot` lists (`<slot>.tmp` is not a claim name,
104
+ * so `listClaims` skips a leftover one, but the rename would still be a second writer of the same directory for no
105
+ * gain) — and a rename would re-create a name a concurrent prune had just removed, which the open below exists to
106
+ * prevent. A torn write degrades the way an unusable stamp already does (`parseClaim`) — the slot is re-read as
107
+ * unsettled, which is visible, not lost.
108
+ */
109
+ export declare function settleClaim(stateRoot: string, name: string, slot: Date, outcome: FireOutcome, ms?: number): void;
@@ -1,11 +1,5 @@
1
- /**
2
- * Durable scheduler state for a SINGLE-PROCESS deployment, under `<stateRoot>/schedule/`. Small JSON
3
- * files, atomic (tmp+rename) so a crash never leaves a torn file:
4
- * - `fires.json` — schedule name → last-fired ISO (durability for the cron catch-up-once);
5
- * - `wakeups.json` — the agent's pending self-scheduled one-shot wake-ups (wakeups.ts).
6
- * No .gitignore is written here: the agent's own (scaffolded by `init`) excludes `.state/`.
7
- */
8
- import { readFileSync } from "node:fs";
1
+ /** Durable scheduler state under `<stateRoot>/schedule/`. */
2
+ import { closeSync, ftruncateSync, mkdirSync, openSync, readFileSync, readdirSync, unlinkSync, writeFileSync, writeSync, } from "node:fs";
9
3
  import { join } from "node:path";
10
4
  import { writeFileAtomic } from "../atomic-write.js";
11
5
  import { log } from "../log.js";
@@ -13,12 +7,6 @@ import { log } from "../log.js";
13
7
  export function scheduleFile(stateRoot, name) {
14
8
  return join(stateRoot, "schedule", `${name}.json`);
15
9
  }
16
- /**
17
- * Read a JSON state file. Missing is normal (first run → undefined); a corrupt file degrades visibly
18
- * (warn + undefined) — schedule state is recoverable, not worth refusing to boot over; an unreadable
19
- * file (permissions/IO) throws — a real environment fault the operator must fix, not hide behind empty
20
- * state. The caller owns shape validation (an IO boundary).
21
- */
22
10
  export function readScheduleFile(path) {
23
11
  let raw;
24
12
  try {
@@ -42,10 +30,296 @@ export function readScheduleFile(path) {
42
30
  export function writeScheduleFile(path, value) {
43
31
  writeFileAtomic(path, JSON.stringify(value));
44
32
  }
45
- export function loadFires(stateRoot) {
46
- const v = readScheduleFile(scheduleFile(stateRoot, "fires"));
47
- return v && typeof v === "object" && !Array.isArray(v) ? v : {};
33
+ // ── claims/: one file per fired slot — the DECISION to fire, made atomically ──
34
+ /**
35
+ * How many claims to keep per schedule — and therefore how far back `fastagent routine history` can see, since
36
+ * the claims ARE the history. The gate below reads only the newest, so this number answers the history's question,
37
+ * "did last night's run silently fail?": at 512 a minute cron keeps ~8.5 hours, an hourly one ~3 weeks.
38
+ *
39
+ * Raise it by FILE COUNT, not by byte count: 512 short lines are ~25 KB of content but ~2 MiB of blocks and 512
40
+ * inodes per schedule on a 4 KiB-block filesystem. Pruning removes the OLDEST names only, so the newest claim never
41
+ * goes — which is what keeps the gate working past the window.
42
+ */
43
+ const KEEP_CLAIMS = 512;
44
+ /** A slot instant as a filename (ISO minus the characters a path cannot carry); sorts in slot order. */
45
+ const claimName = (slot) => slot.toISOString().replace(/[:.]/g, "-");
46
+ /** The instant a claim's file name stands for — `claimName` read backwards. */
47
+ const slotInstant = (name) => name.replace(/-(\d{2})-(\d{2})-(\d{3})Z$/, ":$1:$2.$3Z");
48
+ /**
49
+ * Is this file name one `claimName` produced? Defined by the round trip, so the answer is exactly "this is a slot
50
+ * instant this code could have written", with no second spelling to keep in sync.
51
+ *
52
+ * It has to be asked because a claims directory is a directory, and a foreign name breaks things two ways. A name
53
+ * that sorts AFTER every real claim (`<slot>.tmp`, an editor's `<slot>~`) would be read as the newest one — which
54
+ * decides whether the next slot is refused as stale, where catch-up resumes, and which fire the boot reconciler
55
+ * settles. Any other name (`.DS_Store`, which sorts before all of them) cannot be turned back into a slot instant,
56
+ * and the reconciler's `new Date(fire.slot)` on it throws `RangeError: Invalid time value` out of `start()`.
57
+ */
58
+ const isClaimName = (name) => {
59
+ const instant = Date.parse(slotInstant(name));
60
+ return !Number.isNaN(instant) && claimName(new Date(instant)) === name;
61
+ };
62
+ /**
63
+ * THE listing of a claims directory: claim files only, oldest slot first. Every reader goes through it — the gate,
64
+ * the history, and the pruning — so "what counts as a claim" is decided once instead of by whoever reads next.
65
+ */
66
+ function listClaims(dir) {
67
+ let names;
68
+ try {
69
+ names = readdirSync(dir);
70
+ }
71
+ catch (e) {
72
+ if (e.code === "ENOENT")
73
+ return []; // never fired here yet
74
+ throw e; // unreadable state: the boot fails on it rather than running blind
75
+ }
76
+ const claims = [];
77
+ for (const name of names.sort()) {
78
+ if (isClaimName(name))
79
+ claims.push(name);
80
+ // Not an error of ours and not the operator's to fix on a schedule's behalf (macOS writes `.DS_Store` into any
81
+ // directory someone opens), so it is skipped at debug level rather than warned about on every single fire.
82
+ else
83
+ log.debug(`[schedule] ignoring ${join(dir, name)}: not a fired-slot claim`);
84
+ }
85
+ return claims;
48
86
  }
49
- export function saveFires(stateRoot, fires) {
50
- writeScheduleFile(scheduleFile(stateRoot, "fires"), fires);
87
+ const isOutcome = (s) => s === "completed" || s === "failed" || s === "skipped" || s === "interrupted";
88
+ /**
89
+ * Read one claim file: `{"firedAt":"…"}`, plus `outcome` and `ms` once the turn has reported.
90
+ *
91
+ * JSON, and not a line this module splits itself, because the write is deliberately not atomic (`settleClaim` may
92
+ * not leave a temp file in the directory `claimSlot` lists). A torn write must therefore be DETECTABLE, and that is
93
+ * exactly what JSON gives for free: half an object does not parse, so a partially written claim reads as unsettled
94
+ * — the safe state this design already handles — instead of as a record whose third field happened to look like a
95
+ * number. The positional form cost two shipped bugs of that shape (a torn duration read as `0ms`, an absent one
96
+ * coerced to the same), and the next field anyone adds would inherit them.
97
+ *
98
+ * Anything unusable — a torn write, a missing stamp, a date that is not one — falls back to the slot instant in the
99
+ * file name. That is the earliest moment the fire can have happened, so the only degradation is catching up one run
100
+ * that already ran: too many rather than too few.
101
+ */
102
+ function parseClaim(raw, dir, name) {
103
+ const fire = { slot: slotInstant(name), firedAt: slotInstant(name) };
104
+ let parsed;
105
+ try {
106
+ parsed = JSON.parse(raw);
107
+ }
108
+ catch {
109
+ parsed = undefined;
110
+ }
111
+ // `null` and a bare `12` parse FINE and are not records — destructuring them throws, and this runs on the
112
+ // synchronous boot path, so the throw would be a service that does not start over one unreadable file. Empty is
113
+ // ordinary (the process died between `openSync` creating the claim and the stamp being written), and so is an
114
+ // earlier format's bare timestamp, which is why the rest is debug: the consequence is already visible as a fire
115
+ // that reads `unreported`.
116
+ if (parsed === null || typeof parsed !== "object") {
117
+ if (raw.trim() !== "") {
118
+ log.debug(`[schedule] claim ${join(dir, name)} is not a claim record (torn write, or an earlier format)`);
119
+ }
120
+ return fire;
121
+ }
122
+ const { firedAt, outcome, ms } = parsed;
123
+ if (typeof firedAt === "string" && !Number.isNaN(Date.parse(firedAt)))
124
+ fire.firedAt = firedAt;
125
+ else
126
+ log.warn(`[schedule] claim ${join(dir, name)} carries an unreadable stamp — using the slot instant`);
127
+ if (isOutcome(outcome)) {
128
+ fire.outcome = outcome;
129
+ // A settled fire may carry NO duration: nobody timed an `interrupted` one (see `settleClaim`). Absent stays
130
+ // absent — `0` would print as a turn that really did finish instantly, the one value this record will not invent.
131
+ if (typeof ms === "number" && Number.isFinite(ms))
132
+ fire.ms = ms;
133
+ }
134
+ else if (outcome !== undefined) {
135
+ // A word we do not know is not an outcome: the fire reads as unsettled, which the next boot reports.
136
+ log.warn(`[schedule] claim ${join(dir, name)} carries an unknown outcome — reading it as unsettled: ${outcome}`);
137
+ }
138
+ return fire;
139
+ }
140
+ function readClaim(dir, name) {
141
+ let raw;
142
+ try {
143
+ raw = readFileSync(join(dir, name), "utf8");
144
+ }
145
+ catch (e) {
146
+ if (e.code === "ENOENT")
147
+ return undefined;
148
+ throw e; // unreadable state (EACCES, EIO): the boot fails on it rather than running blind
149
+ }
150
+ return parseClaim(raw, dir, name);
151
+ }
152
+ /**
153
+ * A schedule name becomes a path segment (its claims live in `claims/<name>/`), so the rule is exactly the safety
154
+ * boundary and nothing more: a name may not leave that directory. The name comes from a filename under `routines/`,
155
+ * which already cannot contain a separator — so what a stricter rule would actually reject is legal filenames
156
+ * (`每日简报`, `my schedule`), whose only symptom would be a schedule that silently never fires again.
157
+ *
158
+ * One definition, two enforcers: `discover.ts` refuses an illegal one where the author can see which file is wrong,
159
+ * and `claimDir` asserts it again for a programmatic caller.
160
+ */
161
+ export function isSafeScheduleName(name) {
162
+ return name !== "" && !/[/\\]/.test(name) && !name.includes("\u0000") && name !== "." && name !== "..";
163
+ }
164
+ /** This function builds a path from it, so a name that can leave `claims/<name>/` is a bug, not an input. */
165
+ function claimDir(stateRoot, name) {
166
+ if (!isSafeScheduleName(name)) {
167
+ throw new Error(`refusing to build a claim path for the unsafe schedule name ${JSON.stringify(name)}`);
168
+ }
169
+ return join(stateRoot, "schedule", "claims", name);
170
+ }
171
+ /**
172
+ * Take a cron slot, or report that it is not ours to take.
173
+ *
174
+ * `O_EXCL` is the whole mechanism: creating the file IS the decision, and the kernel gives it to exactly one creator,
175
+ * even with a second scheduler on the same state (two `start`s, a restart overlapping its predecessor, an external
176
+ * clock racing the resident one).
177
+ *
178
+ * Two ways to lose it, and both must hold beyond the pruning window: the slot's own claim exists, or a LATER slot has
179
+ * already been claimed — a delivery older than the newest claim is a stale replay (AWS keeps retrying an event for up
180
+ * to 24h), and firing it would bill a turn for an instant the schedule has already moved past.
181
+ *
182
+ * The claim carries the wall-clock instant it was taken (as `{"firedAt":"…"}`, the shape `settleClaim` writes the
183
+ * outcome back into), so the next boot can tell a fire that never reported from one that did
184
+ * (`markInterruptedFire`) without depending on a second file being written after it.
185
+ */
186
+ export function claimSlot(stateRoot, name, slot, firedAt) {
187
+ const dir = claimDir(stateRoot, name);
188
+ mkdirSync(dir, { recursive: true });
189
+ const taken = listClaims(dir);
190
+ const wanted = claimName(slot);
191
+ const newest = taken.at(-1);
192
+ if (newest !== undefined && wanted < newest)
193
+ return { taken: false, why: "stale", newest };
194
+ let fd;
195
+ try {
196
+ fd = openSync(join(dir, wanted), "wx");
197
+ }
198
+ catch (e) {
199
+ if (e.code === "EEXIST")
200
+ return { taken: false, why: "duplicate" };
201
+ throw e; // a real IO fault: the caller reports it and leaves the schedule armed
202
+ }
203
+ try {
204
+ writeFileSync(fd, JSON.stringify({ firedAt: firedAt.toISOString() }));
205
+ }
206
+ catch (e) {
207
+ // The claim exists from `openSync` on, so a failed stamp (ENOSPC, EIO) would leave a slot that can only ever be
208
+ // read as `duplicate` — taken, never run, never reported. Remove it so the failure this rethrows costs a retry
209
+ // instead of the slot itself. The cleanup's own failure is reported but never replaces `e`: the caller needs
210
+ // the reason the write failed, not the reason the rollback did.
211
+ try {
212
+ closeSync(fd);
213
+ unlinkSync(join(dir, wanted));
214
+ }
215
+ catch (cleanup) {
216
+ log.warn(`[schedule] ${name}: could not remove the unstamped claim ${wanted} — that slot is now taken but will never run: ${String(cleanup)}`);
217
+ }
218
+ throw e;
219
+ }
220
+ closeSync(fd);
221
+ pruneClaims(dir, taken);
222
+ return { taken: true };
223
+ }
224
+ /**
225
+ * Every fire this state root still keeps for `name`, oldest first — bounded by `KEEP_CLAIMS` because the claims ARE
226
+ * the history. Nothing here grows: the pruning that keeps the claim gate cheap keeps the history's size fixed too,
227
+ * which is why there is no separate audit file to rotate (what the turn SAID is in its session, not here).
228
+ *
229
+ * THE HISTORY read, and its only caller is the read-only `routine history` — which translates a read fault into a
230
+ * one-line refusal. The serving boot reads `latestFire` instead: it asks about ONE claim, and a window of files kept
231
+ * for an operator to look at has no business deciding whether a service starts.
232
+ */
233
+ export function readFires(stateRoot, name) {
234
+ const dir = claimDir(stateRoot, name);
235
+ // A claim pruned between the listing and the read is not a fire this state root still keeps, so it is dropped
236
+ // rather than reported as an unsettled one — which is what `unreported` would claim about a run nobody skipped.
237
+ return listClaims(dir).flatMap((slot) => readClaim(dir, slot) ?? []);
238
+ }
239
+ /**
240
+ * The NEWEST fire recorded for `name` — the one claim the serving boot reads, for the two planes that must agree
241
+ * about it: where catch-up resumes (`firedAt`) and whether that fire ever reported (`outcome`). Deriving those
242
+ * separately is how they come to disagree about the same file.
243
+ *
244
+ * Only the newest matters to either. A schedule's resident loop runs one turn at a time, so it can leave at most one
245
+ * unsettled claim behind; an older unsettled one needs a second writer, and the second-writer topology (an external
246
+ * clock) skips the reconciler entirely. Reading the whole retained window here would instead make a file that is
247
+ * 400 fires old — kept only so an operator can look at it — able to fail a boot.
248
+ *
249
+ * A read fault on THIS claim stays fatal: it decides the stale gate and the resume point, and running blind on it
250
+ * fires a slot twice or never.
251
+ */
252
+ export function latestFire(stateRoot, name) {
253
+ const dir = claimDir(stateRoot, name);
254
+ for (const slot of listClaims(dir).reverse()) {
255
+ // `undefined` = pruned between the listing and the read; the one before it is then the newest that still exists.
256
+ const fire = readClaim(dir, slot);
257
+ if (fire !== undefined)
258
+ return fire;
259
+ }
260
+ return undefined;
261
+ }
262
+ /**
263
+ * Write back how the claimed fire ended, into the claim file itself.
264
+ *
265
+ * The SAME file, because a second one would reintroduce the window this whole design closes: a killed process would
266
+ * leave a claimed slot that nothing accounts for. An unsettled claim IS the record of an interrupted fire.
267
+ *
268
+ * NOT `writeFileAtomic`: its temp would land in the directory `claimSlot` lists (`<slot>.tmp` is not a claim name,
269
+ * so `listClaims` skips a leftover one, but the rename would still be a second writer of the same directory for no
270
+ * gain) — and a rename would re-create a name a concurrent prune had just removed, which the open below exists to
271
+ * prevent. A torn write degrades the way an unusable stamp already does (`parseClaim`) — the slot is re-read as
272
+ * unsettled, which is visible, not lost.
273
+ */
274
+ export function settleClaim(stateRoot, name, slot, outcome, ms) {
275
+ const dir = claimDir(stateRoot, name);
276
+ const file = claimName(slot);
277
+ let fd;
278
+ try {
279
+ // `r+`, so this can only ever write a claim that EXISTS. A concurrent `claimSlot` may prune this slot while its
280
+ // turn is still running; reading first and then writing by path would put the pruned file back — a fire in the
281
+ // history that this state root had already decided to forget. Held open, a later unlink leaves the writes on a
282
+ // dead inode instead, which is exactly the outcome being dropped along with the slot.
283
+ fd = openSync(join(dir, file), "r+");
284
+ }
285
+ catch (e) {
286
+ if (e.code === "ENOENT")
287
+ return; // pruned: the outcome goes with the slot, silently
288
+ // EACCES/EIO. The fire happened; killing the schedule's loop over its bookkeeping would cost every later fire
289
+ // too, so this is reported instead. The outcome is then lost: the slot stays `unreported` in the history, and
290
+ // the next boot settles it as `interrupted` ONLY if it is still the newest claim by then (`markInterruptedFire`
291
+ // reads that one) — a later fire on the same schedule leaves it unreported for good.
292
+ log.warn(`[schedule] ${name}: could not open the claim for slot ${file} to settle it: ${String(e)}`);
293
+ return;
294
+ }
295
+ try {
296
+ // The stamp is preserved, not rewritten: `firedAt` is what catch-up resumes from. A duration is written only
297
+ // when one was measured: nobody timed an `interrupted` fire, and writing `0` for it would print as a turn that
298
+ // took no time — the same false value the history deliberately leaves blank.
299
+ const claimed = parseClaim(readFileSync(fd, "utf8"), dir, file);
300
+ const record = { firedAt: claimed.firedAt, outcome, ...(ms === undefined ? {} : { ms: Math.round(ms) }) };
301
+ // Truncate first: a shorter record would otherwise leave the old tail behind. With JSON that tail can only
302
+ // produce a parse failure rather than a plausible field, but a file that says one thing is still worth having.
303
+ ftruncateSync(fd, 0);
304
+ writeSync(fd, JSON.stringify(record), 0);
305
+ }
306
+ catch (e) {
307
+ // Housekeeping: the turn itself already happened. The outcome is lost the same way the open failure above
308
+ // loses it — `unreported` in the history, settled as `interrupted` by the next boot only while it is still the
309
+ // newest claim.
310
+ log.warn(`[schedule] ${name}: could not record the ${outcome} outcome of slot ${file}: ${String(e)}`);
311
+ }
312
+ finally {
313
+ closeSync(fd);
314
+ }
315
+ }
316
+ function pruneClaims(dir, taken) {
317
+ try {
318
+ for (const stale of taken.slice(0, Math.max(0, taken.length + 1 - KEEP_CLAIMS)))
319
+ unlinkSync(join(dir, stale));
320
+ }
321
+ catch (e) {
322
+ // Pruning is housekeeping: a failure leaves files behind, never an unclaimed slot.
323
+ log.warn(`[schedule] could not prune fired-slot claims in ${dir}: ${String(e)}`);
324
+ }
51
325
  }
@@ -1,36 +1,23 @@
1
+ import * as Effect from "effect/Effect";
1
2
  import { type WakeAlarm } from "../channels/agentcore-protocol.ts";
2
3
  import { type Wakeup } from "./wakeups.ts";
3
4
  /**
4
- * Persist the forwarder URL the adapter saw in an envelope (write-if-changed — envelopes arrive on
5
- * every turn, the file should not churn). Durable under <stateRoot>/schedule/ so a freshly booted
6
- * container whose FIRST action is a wake fire (recurring advance → save → sink) knows the URL
7
- * before any envelope of its own has arrived.
5
+ * Persist the forwarder URL the adapter saw in an envelope (write-if-changed — envelopes arrive on every turn, the
6
+ * file should not churn).
8
7
  */
9
8
  export declare function rememberWakeAlarmUrl(stateRoot: string, url: string): void;
10
9
  /** The persisted forwarder URL, or undefined before the first envelope ever seen. */
11
10
  export declare function readWakeAlarmUrl(stateRoot: string): string | undefined;
12
- /** How many times one alarm sync is retried before giving up (each store mutation and every boot
13
- * restart the cycle, and the pending store is the durable desired state — so "give up" means "until
14
- * the next mirror", never "lost"). */
11
+ /**
12
+ * How many times one alarm sync is retried before giving up (each store mutation and every boot restart the cycle, and
13
+ * the pending store is the durable desired state — so "give up" means "until the next mirror", never "lost").
14
+ */
15
15
  export declare const MAX_SYNC_ATTEMPTS = 5;
16
16
  /** Pending wake-ups → the desired alarm set, minus already-due entries (see {@link DUE_MARGIN_MS}). */
17
17
  export declare function toAlarms(pending: Wakeup[], now: Date): WakeAlarm[];
18
18
  /**
19
- * Build the wakeups sink for an AgentCore deployment (registered via `setWakeupsSink` by `start`
20
- * when `FASTAGENT_AGENTCORE=1` + `FASTAGENT_WAKE_SECRET` are present). Fire-and-forget by contract:
21
- * failures are logged, never thrown — a broken alarm degrades to the pre-alarm behavior (the wake
22
- * still fires on the next time the box happens to be awake), it must never break the store write.
23
- *
24
- * A single-flight RECONCILER, not a per-mutation delivery: a notification only marks the desired
25
- * state dirty, and the loop re-derives it from the store before every attempt. So a mutation during
26
- * a retry needs no ordering rule — there is one desired state, never two snapshots to rank — and the
27
- * {@link DUE_MARGIN_MS} filter is re-applied with a fresh clock, which a captured payload could not
28
- * do: entries that became due mid-retry would keep being POSTed with a past `at()`, a rejection no
29
- * number of retries could clear.
30
- *
31
- * Assumes ONE sink per process over ONE state root (what `start` builds): the loop keeps the root of
32
- * the call that started it, and its dirty flag is shared, so a second root would fold into the first
33
- * one's pass.
19
+ * Build the wakeups sink for an AgentCore deployment (registered via `setWakeupsSink` by `start` when
20
+ * `FASTAGENT_AGENTCORE=1` + `FASTAGENT_WAKE_SECRET` are present).
34
21
  */
35
22
  export declare function createWakeAlarmSink(options: {
36
23
  secret: string;
@@ -39,4 +26,4 @@ export declare function createWakeAlarmSink(options: {
39
26
  now?: () => Date;
40
27
  /** Injectable retry pause (tests); defaults to exponential-ish backoff off RETRY_BASE_MS. */
41
28
  delay?: (ms: number) => Promise<void>;
42
- }): (stateRoot: string) => void;
29
+ }): Effect.Effect<(stateRoot: string) => void>;