@fastagent-sh/fastagent 0.21.1 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -12
- package/dist/agent.d.ts +24 -37
- package/dist/agent.js +7 -15
- package/dist/atomic-write.d.ts +13 -17
- package/dist/atomic-write.js +18 -29
- package/dist/bind.d.ts +12 -22
- package/dist/bind.js +18 -27
- package/dist/channel.d.ts +11 -33
- package/dist/channels/agentcore-limits.d.ts +2 -13
- package/dist/channels/agentcore-limits.js +2 -13
- package/dist/channels/agentcore-protocol.d.ts +67 -58
- package/dist/channels/agentcore-protocol.js +23 -15
- package/dist/channels/agentcore-service.d.ts +19 -33
- package/dist/channels/agentcore-service.js +176 -59
- package/dist/channels/agentcore.d.ts +39 -33
- package/dist/channels/agentcore.js +90 -224
- package/dist/channels/body.d.ts +15 -2
- package/dist/channels/body.js +24 -2
- package/dist/channels/busy.d.ts +1 -21
- package/dist/channels/busy.js +2 -39
- package/dist/channels/control.d.ts +17 -62
- package/dist/channels/control.js +74 -182
- package/dist/channels/define-channel.d.ts +39 -0
- package/dist/channels/define-channel.js +9 -0
- package/dist/channels/discover.d.ts +10 -31
- package/dist/channels/discover.js +37 -31
- package/dist/channels/feishu/bootstrap-token.d.ts +1 -8
- package/dist/channels/feishu/bootstrap-token.js +7 -23
- package/dist/channels/feishu/card.d.ts +14 -28
- package/dist/channels/feishu/card.js +16 -30
- package/dist/channels/feishu/cloud.d.ts +1 -5
- package/dist/channels/feishu/context-buffer.d.ts +4 -23
- package/dist/channels/feishu/context-buffer.js +8 -31
- package/dist/channels/feishu/crypto.d.ts +10 -5
- package/dist/channels/feishu/crypto.js +12 -19
- package/dist/channels/feishu/feishu-api.d.ts +2 -1
- package/dist/channels/feishu/feishu-api.js +5 -4
- package/dist/channels/feishu/feishu.d.ts +4 -10
- package/dist/channels/feishu/feishu.js +113 -227
- package/dist/channels/feishu/invoke-turn.d.ts +5 -4
- package/dist/channels/feishu/invoke-turn.js +36 -54
- package/dist/channels/feishu/model.d.ts +4 -21
- package/dist/channels/feishu/model.js +1 -8
- package/dist/channels/feishu/normalize.d.ts +2 -10
- package/dist/channels/feishu/normalize.js +7 -42
- package/dist/channels/feishu/parse.d.ts +9 -29
- package/dist/channels/feishu/parse.js +8 -26
- package/dist/channels/feishu/preview.d.ts +12 -14
- package/dist/channels/feishu/preview.js +108 -207
- package/dist/channels/feishu/register-app.d.ts +8 -23
- package/dist/channels/feishu/register-app.js +7 -20
- package/dist/channels/feishu/register-webhook.d.ts +2 -9
- package/dist/channels/feishu/register-webhook.js +9 -33
- package/dist/channels/feishu/scaffold/channel.ts +27 -19
- package/dist/channels/feishu/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/feishu/setup-mode.d.ts +11 -36
- package/dist/channels/feishu/setup-mode.js +7 -26
- package/dist/channels/feishu/ws-ingress.d.ts +1 -2
- package/dist/channels/feishu/ws-ingress.js +6 -8
- package/dist/channels/http.d.ts +15 -15
- package/dist/channels/http.js +19 -9
- package/dist/channels/kit/attachment-path.d.ts +4 -5
- package/dist/channels/kit/attachment-path.js +10 -34
- package/dist/channels/kit/context-buffer.d.ts +14 -21
- package/dist/channels/kit/context-buffer.js +25 -38
- package/dist/channels/kit/delivery.d.ts +35 -0
- package/dist/channels/kit/delivery.js +129 -0
- package/dist/channels/kit/event-stream.d.ts +5 -0
- package/dist/channels/kit/event-stream.js +29 -0
- package/dist/channels/kit/invoke-turn-kit.d.ts +53 -26
- package/dist/channels/kit/invoke-turn-kit.js +89 -32
- package/dist/channels/kit/preview-kit.d.ts +18 -72
- package/dist/channels/kit/preview-kit.js +22 -88
- package/dist/channels/kit/signature.d.ts +3 -9
- package/dist/channels/kit/signature.js +3 -9
- package/dist/channels/kit/state.d.ts +1 -2
- package/dist/channels/kit/state.js +5 -14
- package/dist/channels/kit/stop-command.d.ts +1 -2
- package/dist/channels/kit/stop-command.js +6 -9
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +8 -21
- package/dist/channels/kit/text.d.ts +2 -16
- package/dist/channels/kit/text.js +23 -31
- package/dist/channels/kit/thread-participants.d.ts +4 -17
- package/dist/channels/kit/thread-participants.js +8 -55
- package/dist/channels/kit/transport.d.ts +26 -0
- package/dist/channels/kit/transport.js +20 -0
- package/dist/channels/kit/turn-queue.d.ts +7 -11
- package/dist/channels/kit/turn-queue.js +21 -39
- package/dist/channels/kit/turn-runner.d.ts +32 -18
- package/dist/channels/kit/turn-runner.js +98 -53
- package/dist/channels/kit/turn-store.d.ts +32 -32
- package/dist/channels/kit/turn-store.js +45 -88
- package/dist/channels/lark/lark.d.ts +1 -5
- package/dist/channels/lark/onboard.d.ts +3 -12
- package/dist/channels/lark/onboard.js +3 -13
- package/dist/channels/lark/scaffold/channel.ts +27 -19
- package/dist/channels/lark/scaffold/channel.websocket.ts +31 -0
- package/dist/channels/registration.d.ts +9 -35
- package/dist/channels/registration.js +6 -37
- package/dist/channels/secret.js +2 -5
- package/dist/channels/serve.d.ts +47 -44
- package/dist/channels/serve.js +165 -101
- package/dist/channels/slack/config-api.d.ts +4 -27
- package/dist/channels/slack/config-api.js +5 -34
- package/dist/channels/slack/context-buffer.d.ts +2 -3
- package/dist/channels/slack/context-buffer.js +5 -5
- package/dist/channels/slack/invoke-turn.d.ts +3 -1
- package/dist/channels/slack/invoke-turn.js +25 -34
- package/dist/channels/slack/manifest.js +1 -3
- package/dist/channels/slack/model.d.ts +1 -2
- package/dist/channels/slack/model.js +1 -1
- package/dist/channels/slack/onboard.d.ts +4 -3
- package/dist/channels/slack/onboard.js +8 -11
- package/dist/channels/slack/onboarding-state.d.ts +3 -5
- package/dist/channels/slack/onboarding-state.js +9 -6
- package/dist/channels/slack/parse.d.ts +4 -13
- package/dist/channels/slack/parse.js +7 -28
- package/dist/channels/slack/preview.d.ts +12 -2
- package/dist/channels/slack/preview.js +292 -269
- package/dist/channels/slack/reaction.d.ts +11 -3
- package/dist/channels/slack/reaction.js +19 -8
- package/dist/channels/slack/register-webhook.js +4 -7
- package/dist/channels/slack/scaffold/channel.ts +20 -13
- package/dist/channels/slack/scaffold/slack-send.ts +2 -2
- package/dist/channels/slack/setup-server.d.ts +1 -5
- package/dist/channels/slack/setup-server.js +1 -5
- package/dist/channels/slack/shared-api.d.ts +1 -4
- package/dist/channels/slack/shared-api.js +2 -13
- package/dist/channels/slack/slack-api.d.ts +2 -1
- package/dist/channels/slack/slack-api.js +6 -4
- package/dist/channels/slack/slack.d.ts +11 -14
- package/dist/channels/slack/slack.js +121 -122
- package/dist/channels/sse.d.ts +1 -1
- package/dist/channels/sse.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +7 -22
- package/dist/channels/telegram/context-buffer.js +6 -23
- package/dist/channels/telegram/invoke-turn.d.ts +7 -7
- package/dist/channels/telegram/invoke-turn.js +24 -43
- package/dist/channels/telegram/parse.d.ts +24 -51
- package/dist/channels/telegram/parse.js +32 -84
- package/dist/channels/telegram/preview.d.ts +10 -1
- package/dist/channels/telegram/preview.js +79 -100
- package/dist/channels/telegram/register-webhook.js +5 -23
- package/dist/channels/telegram/scaffold/channel.ts +25 -18
- package/dist/channels/telegram/scaffold/telegram-send.ts +6 -4
- package/dist/channels/telegram/telegram-api.d.ts +4 -3
- package/dist/channels/telegram/telegram-api.js +9 -7
- package/dist/channels/telegram/telegram.d.ts +4 -18
- package/dist/channels/telegram/telegram.js +38 -94
- package/dist/channels/telegram/turn-store.d.ts +11 -14
- package/dist/channels/telegram/turn-store.js +8 -6
- package/dist/channels/wait-health.d.ts +4 -6
- package/dist/channels/wait-health.js +10 -14
- package/dist/cli/add-feishu.d.ts +2 -12
- package/dist/cli/add-feishu.js +24 -71
- package/dist/cli/add-slack.d.ts +5 -3
- package/dist/cli/add-slack.js +53 -76
- package/dist/cli/auth-view.d.ts +2 -16
- package/dist/cli/auth-view.js +2 -16
- package/dist/cli/commands/add.js +25 -40
- package/dist/cli/commands/chat.d.ts +0 -1
- package/dist/cli/commands/chat.js +5 -6
- package/dist/cli/commands/deploy/agentcore.js +60 -65
- package/dist/cli/commands/deploy/docker.js +50 -23
- package/dist/cli/commands/deploy/fly.js +23 -61
- package/dist/cli/commands/deploy/railway.js +23 -18
- package/dist/cli/commands/deploy/shared.d.ts +38 -57
- package/dist/cli/commands/deploy/shared.js +40 -59
- package/dist/cli/commands/deploy.d.ts +10 -27
- package/dist/cli/commands/deploy.js +38 -60
- package/dist/cli/commands/destroy.d.ts +4 -0
- package/dist/cli/commands/destroy.js +56 -0
- package/dist/cli/commands/dev.d.ts +2 -1
- package/dist/cli/commands/dev.js +20 -23
- package/dist/cli/commands/info.d.ts +0 -2
- package/dist/cli/commands/info.js +83 -49
- package/dist/cli/commands/init.d.ts +1 -2
- package/dist/cli/commands/init.js +16 -55
- package/dist/cli/commands/invoke.d.ts +0 -1
- package/dist/cli/commands/invoke.js +5 -7
- package/dist/cli/commands/login.d.ts +2 -1
- package/dist/cli/commands/login.js +33 -38
- package/dist/cli/commands/logs.js +3 -4
- package/dist/cli/commands/models.js +1 -5
- package/dist/cli/commands/routine-run.d.ts +6 -0
- package/dist/cli/commands/routine-run.js +47 -0
- package/dist/cli/commands/routine.d.ts +25 -0
- package/dist/cli/commands/routine.js +122 -0
- package/dist/cli/commands/start.d.ts +31 -3
- package/dist/cli/commands/start.js +131 -81
- package/dist/cli/commands/tool.js +26 -12
- package/dist/cli/fail.d.ts +13 -23
- package/dist/cli/fail.js +20 -23
- package/dist/cli/invoke-stream.d.ts +1 -8
- package/dist/cli/kernel.d.ts +6 -38
- package/dist/cli/kernel.js +14 -77
- package/dist/cli/models-view.d.ts +1 -9
- package/dist/cli/models-view.js +11 -22
- package/dist/cli/program.d.ts +5 -6
- package/dist/cli/program.js +120 -157
- package/dist/cli/serve.d.ts +57 -41
- package/dist/cli/serve.js +94 -121
- package/dist/cli/shared.d.ts +15 -61
- package/dist/cli/shared.js +55 -181
- package/dist/cli.js +1 -8
- package/dist/collect.d.ts +6 -0
- package/dist/collect.js +6 -0
- package/dist/core.d.ts +4 -1
- package/dist/core.js +9 -7
- package/dist/declared-secrets.d.ts +55 -0
- package/dist/declared-secrets.js +88 -0
- package/dist/deploy/agentcore/aws-cli.d.ts +91 -0
- package/dist/deploy/agentcore/aws-cli.js +64 -0
- package/dist/deploy/agentcore/destroy.d.ts +51 -0
- package/dist/deploy/agentcore/destroy.js +273 -0
- package/dist/deploy/agentcore/forwarder.js +28 -79
- package/dist/deploy/agentcore/logs.d.ts +2 -18
- package/dist/deploy/agentcore/logs.js +27 -51
- package/dist/deploy/agentcore/plan.d.ts +85 -105
- package/dist/deploy/agentcore/plan.js +152 -314
- package/dist/deploy/agentcore/run.d.ts +28 -28
- package/dist/deploy/agentcore/run.js +227 -306
- package/dist/deploy/agentcore/zip.d.ts +2 -15
- package/dist/deploy/agentcore/zip.js +3 -17
- package/dist/deploy/build-context.d.ts +21 -0
- package/dist/deploy/build-context.js +137 -0
- package/dist/deploy/channel-ingress.d.ts +26 -39
- package/dist/deploy/channel-ingress.js +57 -28
- package/dist/deploy/container.d.ts +20 -34
- package/dist/deploy/container.js +49 -100
- package/dist/deploy/docker/plan.d.ts +12 -11
- package/dist/deploy/docker/plan.js +43 -24
- package/dist/deploy/docker/run.d.ts +22 -25
- package/dist/deploy/docker/run.js +39 -37
- package/dist/deploy/fly/plan.d.ts +15 -44
- package/dist/deploy/fly/plan.js +41 -73
- package/dist/deploy/fly/run.d.ts +16 -51
- package/dist/deploy/fly/run.js +42 -95
- package/dist/deploy/hosts.d.ts +24 -3
- package/dist/deploy/hosts.js +24 -3
- package/dist/deploy/preflight.d.ts +47 -41
- package/dist/deploy/preflight.js +282 -383
- package/dist/deploy/railway/plan.d.ts +16 -42
- package/dist/deploy/railway/plan.js +42 -60
- package/dist/deploy/railway/run.d.ts +30 -68
- package/dist/deploy/railway/run.js +86 -100
- package/dist/deploy/registration-gate.d.ts +6 -13
- package/dist/deploy/residency.d.ts +37 -0
- package/dist/deploy/residency.js +24 -0
- package/dist/deploy/runner.d.ts +10 -9
- package/dist/deploy/runner.js +7 -18
- package/dist/deploy/secrets.d.ts +40 -41
- package/dist/deploy/secrets.js +117 -103
- package/dist/deploy/workspace.d.ts +50 -0
- package/dist/deploy/workspace.js +197 -0
- package/dist/dev-supervisor.d.ts +1 -9
- package/dist/dev-supervisor.js +21 -64
- package/dist/effect-port.d.ts +50 -0
- package/dist/effect-port.js +77 -0
- package/dist/engines/pi/agent-session-factory.d.ts +50 -69
- package/dist/engines/pi/agent-session-factory.js +249 -146
- package/dist/engines/pi/auth.d.ts +15 -11
- package/dist/engines/pi/auth.js +96 -99
- package/dist/engines/pi/chat.d.ts +1 -5
- package/dist/engines/pi/chat.js +2 -14
- package/dist/engines/pi/config.d.ts +53 -51
- package/dist/engines/pi/config.js +83 -92
- package/dist/engines/pi/create.d.ts +60 -75
- package/dist/engines/pi/create.js +108 -141
- package/dist/engines/pi/definition.d.ts +14 -58
- package/dist/engines/pi/definition.js +21 -85
- package/dist/engines/pi/invoke-session.d.ts +5 -25
- package/dist/engines/pi/invoke-session.js +233 -244
- package/dist/engines/pi/login.d.ts +95 -35
- package/dist/engines/pi/login.js +259 -107
- package/dist/engines/pi/machine.d.ts +31 -0
- package/dist/engines/pi/machine.js +106 -0
- package/dist/engines/pi/models.d.ts +70 -89
- package/dist/engines/pi/models.js +188 -81
- package/dist/engines/pi/open.d.ts +65 -53
- package/dist/engines/pi/open.js +129 -69
- package/dist/engines/pi/report.d.ts +3 -12
- package/dist/engines/pi/report.js +2 -16
- package/dist/engines/pi/search-tools.d.ts +1 -9
- package/dist/engines/pi/search-tools.js +26 -57
- package/dist/engines/pi/service.d.ts +2 -16
- package/dist/engines/pi/service.js +2 -16
- package/dist/engines/pi/session-builder.d.ts +5 -9
- package/dist/engines/pi/session-builder.js +44 -82
- package/dist/engines/pi/session-control.js +286 -269
- package/dist/engines/pi/session-effects.d.ts +14 -0
- package/dist/engines/pi/session-effects.js +30 -0
- package/dist/engines/pi/session-inheritance.d.ts +9 -18
- package/dist/engines/pi/session-inheritance.js +103 -160
- package/dist/engines/pi/session-markers.d.ts +31 -30
- package/dist/engines/pi/session-markers.js +26 -25
- package/dist/engines/pi/session-settings.d.ts +6 -22
- package/dist/engines/pi/session-settings.js +10 -21
- package/dist/engines/pi/session-store.d.ts +26 -92
- package/dist/engines/pi/session-store.js +96 -310
- package/dist/engines/pi/tool-context.d.ts +17 -45
- package/dist/engines/pi/tool-context.js +7 -26
- package/dist/engines/pi/tool.d.ts +45 -36
- package/dist/engines/pi/tool.js +43 -57
- package/dist/engines/pi/turn-kit.d.ts +14 -52
- package/dist/engines/pi/turn-kit.js +18 -67
- package/dist/engines/pi/wake-tool.d.ts +2 -10
- package/dist/engines/pi/wake-tool.js +7 -27
- package/dist/env.d.ts +27 -28
- package/dist/env.js +45 -50
- package/dist/index.js +0 -2
- package/dist/lark.d.ts +1 -2
- package/dist/lark.js +1 -2
- package/dist/loader.d.ts +14 -55
- package/dist/loader.js +24 -43
- package/dist/log.d.ts +2 -12
- package/dist/log.js +3 -24
- package/dist/node.d.ts +2 -15
- package/dist/node.js +2 -18
- package/dist/observe.d.ts +2 -8
- package/dist/open-url.d.ts +1 -1
- package/dist/open-url.js +1 -1
- package/dist/paths.d.ts +70 -134
- package/dist/paths.js +165 -232
- package/dist/pi.d.ts +4 -6
- package/dist/pi.js +3 -3
- package/dist/proxy.d.ts +16 -6
- package/dist/proxy.js +52 -16
- package/dist/runtime.d.ts +18 -12
- package/dist/runtime.js +18 -11
- package/dist/scaffold/add-channel.d.ts +14 -31
- package/dist/scaffold/add-channel.js +48 -112
- package/dist/scaffold/init.d.ts +9 -31
- package/dist/scaffold/init.js +45 -138
- package/dist/scaffold/templates/env.example +6 -4
- package/dist/scaffold/templates/fastagent.config.ts +36 -0
- package/dist/scaffold/templates.d.ts +3 -3
- package/dist/scaffold/templates.js +5 -8
- package/dist/scaffold/vendor-skill.d.ts +2 -5
- package/dist/scaffold/vendor-skill.js +19 -29
- package/dist/schedule/cron.d.ts +9 -5
- package/dist/schedule/cron.js +9 -11
- package/dist/schedule/discover.d.ts +12 -12
- package/dist/schedule/discover.js +43 -34
- package/dist/schedule/routine.d.ts +43 -0
- package/dist/schedule/routine.js +32 -0
- package/dist/schedule/run.d.ts +55 -0
- package/dist/schedule/run.js +146 -0
- package/dist/schedule/scheduler.d.ts +44 -55
- package/dist/schedule/scheduler.js +291 -265
- package/dist/schedule/state.d.ts +105 -10
- package/dist/schedule/state.js +293 -19
- package/dist/schedule/wake-alarm.d.ts +10 -23
- package/dist/schedule/wake-alarm.js +91 -121
- package/dist/schedule/wakeups.d.ts +23 -39
- package/dist/schedule/wakeups.js +33 -75
- package/dist/secrets-gate.d.ts +32 -0
- package/dist/secrets-gate.js +33 -0
- package/dist/service.d.ts +137 -98
- package/dist/service.js +250 -252
- package/dist/session-remote.d.ts +33 -44
- package/dist/session-remote.js +203 -192
- package/dist/session.d.ts +166 -229
- package/dist/session.js +33 -55
- package/dist/tunnel.d.ts +8 -36
- package/dist/tunnel.js +24 -65
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/package.json +10 -16
- package/dist/channels/agentcore-state.d.ts +0 -77
- package/dist/channels/agentcore-state.js +0 -276
- package/dist/channels/github/github.d.ts +0 -31
- package/dist/channels/github/github.js +0 -89
- package/dist/channels/github/scaffold/channel.ts +0 -25
- package/dist/cli/commands/attach.d.ts +0 -108
- package/dist/cli/commands/attach.js +0 -637
- package/dist/cli/commands/fire.d.ts +0 -7
- package/dist/cli/commands/fire.js +0 -38
- package/dist/cli/commands/schedule.d.ts +0 -12
- package/dist/cli/commands/schedule.js +0 -87
- package/dist/github.d.ts +0 -3
- package/dist/github.js +0 -2
- package/dist/scaffold/templates/fastagent.config.mjs +0 -23
- package/dist/schedule/audit.d.ts +0 -18
- package/dist/schedule/audit.js +0 -57
- package/dist/schedule/schedule.d.ts +0 -35
- 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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
71
|
+
// Nothing future to mirror: converged.
|
|
72
|
+
if (alarms.length === 0)
|
|
115
73
|
return true;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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 (
|
|
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
|
-
|
|
145
|
-
// A mutation landed mid-retry: the desired state moved, so this budget is spent on a set
|
|
146
|
-
//
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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).
|
|
7
|
+
/** When to fire next (ISO). */
|
|
8
8
|
fireAt: string;
|
|
9
|
-
/** RECURRING: the cron expression (5-field).
|
|
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.
|
|
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
|
-
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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.
|
|
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
|
|
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
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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;
|
package/dist/schedule/wakeups.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
156
|
-
*
|
|
157
|
-
*
|
|
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 &&
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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
|
+
}
|