@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.
- 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 +209 -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 +153 -228
- 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,82 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Durable turn intent: the
|
|
3
|
-
*
|
|
4
|
-
* the webhook 200 (pre-ACK, like the telegram context-buffer) and removes it when the turn ENDS — the
|
|
5
|
-
* runner's `finally`: a completed turn OR any caught error both remove it. Precisely, a completed turn's
|
|
6
|
-
* removal fires at the `completed` event (the session is committed), which is BEFORE the reply reaches
|
|
7
|
-
* the chat platform: so L1 recovers the ACKed-but-un-COMPLETED window, not un-DELIVERED. A crash in
|
|
8
|
-
* that narrow tail (completed, session-committed, but the message not yet sent) leaves the answer in the
|
|
9
|
-
* session history undelivered and is deliberately NOT replayed — replaying a session-committed turn
|
|
10
|
-
* would double-append it; the asker re-asks (and sees the prior answer in history). L1's scope is the
|
|
11
|
-
* INTERRUPTED-run window (the `finally` never runs); a caught error is NOT retried here — a `failed`
|
|
12
|
-
* event already told the user, and a transport throw is dropped exactly like the pre-L1 in-memory queue
|
|
13
|
-
* did (replaying it could double-send). Only an interrupted run leaves the record on disk — and
|
|
14
|
-
* "interrupted" is not just a rare crash: `runStart` has no graceful drain (cli.ts), so a SIGTERM exits
|
|
15
|
-
* mid-turn too, i.e. EVERY rolling deploy that catches an in-flight turn. Recovery re-enqueues it next start.
|
|
2
|
+
* Durable turn intent: the at-least-once half of durable execution. Exactly-once execution needs a different
|
|
3
|
+
* backend and is out of scope (docs/design/core.md §10).
|
|
16
4
|
*
|
|
17
|
-
*
|
|
18
|
-
* the
|
|
19
|
-
*
|
|
20
|
-
* at-least-once, not exactly-once:
|
|
21
|
-
* - PRIMARY cost: replay re-runs the WHOLE turn, so every external side effect happens AGAIN — re-sent
|
|
22
|
-
* messages, re-fired tool actions — and nobody may notice (unlike the visible loss it replaces).
|
|
23
|
-
* And the trigger is not rare (above): it fires on every deploy that interrupts an in-flight turn,
|
|
24
|
-
* not just on crashes. So "safe only if the turn's tools are idempotent" is a bar to judge against
|
|
25
|
-
* DEPLOY frequency: it holds for a Q&A bot, and is the gate for adding side-effecting tools. (A
|
|
26
|
-
* mid-stream interruption also leaves an orphan "💭 Thinking…" preview — cosmetic.)
|
|
27
|
-
* - The pre-ACK window overlaps webhook redelivery: a crash AFTER the persist but BEFORE the 200
|
|
28
|
-
* means recovery replays the turn AND the platform (never-ACKed) redelivers the same event — same
|
|
29
|
-
* id, and unless the channel dedups accepted ids, the turn can run twice. Exactly-once (a persisted
|
|
30
|
-
* delivery key) is L2.
|
|
31
|
-
*
|
|
32
|
-
* Channel-neutral, generic over the record shape: the channel supplies its own record type (everything
|
|
33
|
-
* its runner needs to re-execute the turn), the shape validator (state files are an IO boundary), the
|
|
34
|
-
* arrival ordering for recovery, and its log label. Telegram, Feishu/Lark, and Slack consume it.
|
|
35
|
-
*
|
|
36
|
-
* ponytail: at-least-once with a per-turn EXECUTION ceiling. A poison turn that deterministically
|
|
37
|
-
* crashes the process would replay forever under a container restart policy. The counter is bumped at
|
|
38
|
-
* `startAttempt` — when a turn is about to RUN, not per restart cycle — so it counts a turn's OWN
|
|
39
|
-
* execution attempts: `recover()` re-enqueues every surviving turn without touching its count, and a
|
|
40
|
-
* poison turn at a session's head is dropped on its own N+1th run WITHOUT penalizing the never-run turns
|
|
41
|
-
* queued behind it (they keep their full budget and get their turn once the poison one is gone). On drop,
|
|
42
|
-
* the runner notifies the asker (the chain's end must get a signal — a log line the user can't see isn't).
|
|
43
|
-
* The bump is the ONE post-ACK write that fails CLOSED (not best-effort): if it can't be persisted the
|
|
44
|
-
* turn is DEFERRED — skipped this cycle and left on disk to replay on the next start — rather than run
|
|
45
|
-
* untracked, because an unpersisted count lets recover() re-run a poison turn every restart (the ceiling
|
|
46
|
-
* would never advance on disk). It replays on the next START — a restart is required; disk recovery alone
|
|
47
|
-
* does not re-run a deferred turn — and the asker is NOT told (a transient system degrade, not a user-
|
|
48
|
-
* actionable failure — telling them "ask again" would double-answer on that replay).
|
|
49
|
-
* Single-process, single-writer: same durability model as state.ts (crash-safe via atomic rename;
|
|
50
|
-
* power-loss is best-effort — no fsync, consistent with the rest of the channel's state).
|
|
5
|
+
* ponytail: at-least-once with a per-turn EXECUTION ceiling ({@link MAX_TURN_ATTEMPTS}). A poison turn that
|
|
6
|
+
* deterministically crashes the process would otherwise replay forever under a container restart policy; a real
|
|
7
|
+
* exactly-once story needs the different backend above.
|
|
51
8
|
*/
|
|
52
9
|
import { log } from "../../log.js";
|
|
53
10
|
import { loadStateFile, saveStateFile } from "./state.js";
|
|
54
|
-
/**
|
|
55
|
-
* How many times a turn may START without finishing before it is dropped rather than run again — the
|
|
56
|
-
* poison-turn ceiling described at length above. One value for every channel: it prices replay against
|
|
57
|
-
* DEPLOY frequency, which is a property of how fastagent is operated, not of which chat platform is in
|
|
58
|
-
* front of it.
|
|
59
|
-
*
|
|
60
|
-
* Known limitation: the count cannot tell a self-inflicted crash from an external SIGTERM (there is no
|
|
61
|
-
* graceful drain), so a legitimately long turn interrupted by this many successive deploys is dropped
|
|
62
|
-
* as if it were poison. Three is a bet that such a turn is an outlier, not a defence against one:
|
|
63
|
-
* catching SIGTERM to spare it would reintroduce the drain the design refuses — raise this constant
|
|
64
|
-
* instead if such turns are expected.
|
|
65
|
-
*/
|
|
11
|
+
/** How many times a turn may START without finishing before it is dropped rather than run again. */
|
|
66
12
|
const MAX_TURN_ATTEMPTS = 3;
|
|
67
13
|
/**
|
|
68
|
-
*
|
|
14
|
+
* An ANSWERED turn: record the reply for delivery, then commit the discussion it folded in. The intent is NOT
|
|
15
|
+
* dropped here — generating an answer is not the same event as that answer reaching the chat, and the runner drops
|
|
16
|
+
* the record only once delivery has settled.
|
|
69
17
|
*
|
|
70
|
-
*
|
|
71
|
-
* site. A crash between the two writes may re-fold already-answered context into the next summon —
|
|
72
|
-
* additive and harmless. The reverse order leaves intent on disk with its context already consumed,
|
|
73
|
-
* so the replay runs the same turn with its context stripped.
|
|
74
|
-
*
|
|
75
|
-
* Called from the turn's `completed` event, when the fold provably lives in the durable session.
|
|
18
|
+
* Returns whether the answer became recoverable, so a caller does not claim a recovery that has nothing on disk.
|
|
76
19
|
*/
|
|
77
20
|
export function commitAnsweredTurn(store, buffer, turn) {
|
|
78
|
-
store.
|
|
21
|
+
const recorded = store.answered(turn.id, turn.answer);
|
|
79
22
|
buffer.commit(turn.bufferKey, turn.consumed);
|
|
23
|
+
return recorded;
|
|
80
24
|
}
|
|
81
25
|
export function createTurnStore(path, opts) {
|
|
82
26
|
const { label, isRecord, order } = opts;
|
|
@@ -84,7 +28,9 @@ export function createTurnStore(path, opts) {
|
|
|
84
28
|
const raw = loadStateFile(path);
|
|
85
29
|
if (raw === undefined)
|
|
86
30
|
return new Map();
|
|
87
|
-
|
|
31
|
+
// `answer` belongs to the generic record, so its shape is checked here rather than in each channel's validator.
|
|
32
|
+
const valid = (t) => isRecord(t) && (t.answer === undefined || typeof t.answer === "string");
|
|
33
|
+
if (typeof raw === "object" && raw !== null && !Array.isArray(raw) && Object.values(raw).every(valid)) {
|
|
88
34
|
return new Map(Object.entries(raw));
|
|
89
35
|
}
|
|
90
36
|
log.warn(`${label} unexpected shape in ${path} — starting with no pending turns`);
|
|
@@ -92,20 +38,22 @@ export function createTurnStore(path, opts) {
|
|
|
92
38
|
};
|
|
93
39
|
const turns = load();
|
|
94
40
|
const persist = () => saveStateFile(path, Object.fromEntries(turns));
|
|
95
|
-
// Post-ACK writes (remove, startAttempt) must not abort a turn: log a failed write, never throw.
|
|
41
|
+
// Post-ACK writes (remove, startAttempt) must not abort a turn: log a failed write, never throw. The result is
|
|
42
|
+
// returned because whether the write landed is what a caller may be reporting to the operator.
|
|
96
43
|
const persistBestEffort = (what) => {
|
|
97
44
|
try {
|
|
98
45
|
persist();
|
|
46
|
+
return true;
|
|
99
47
|
}
|
|
100
48
|
catch (e) {
|
|
101
49
|
log.error(`${label} turn-store ${what} write failed post-ACK: ${String(e)}`);
|
|
50
|
+
return false;
|
|
102
51
|
}
|
|
103
52
|
};
|
|
104
53
|
return {
|
|
105
54
|
add(rec) {
|
|
106
|
-
// Idempotent on re-add: a redelivery (pre-ACK crash → never-ACKed event replayed) re-submits an id
|
|
107
|
-
//
|
|
108
|
-
// turn's execution count, handing a poison turn a fresh ceiling budget.
|
|
55
|
+
// Idempotent on re-add: a redelivery (pre-ACK crash → never-ACKed event replayed) re-submits an id already in
|
|
56
|
+
// the store.
|
|
109
57
|
if (turns.has(rec.id))
|
|
110
58
|
return;
|
|
111
59
|
turns.set(rec.id, rec);
|
|
@@ -113,20 +61,32 @@ export function createTurnStore(path, opts) {
|
|
|
113
61
|
persist(); // pre-ACK: the transport maps this throw to HTTP/WS 500, so the platform redelivers
|
|
114
62
|
}
|
|
115
63
|
catch (e) {
|
|
116
|
-
// Roll the memory back so it matches disk (mirrors context-buffer.push): otherwise the phantom
|
|
117
|
-
//
|
|
118
|
-
// intent never persisted, defeating this module — and gets flushed later by an unrelated persist.
|
|
64
|
+
// Roll the memory back so it matches disk (mirrors context-buffer.push): otherwise the phantom entry makes
|
|
65
|
+
// the redelivery's `add` short-circuit on `turns.has`.
|
|
119
66
|
turns.delete(rec.id);
|
|
120
67
|
throw e;
|
|
121
68
|
}
|
|
122
69
|
},
|
|
70
|
+
answered(id, answer) {
|
|
71
|
+
const rec = turns.get(id);
|
|
72
|
+
if (!rec) {
|
|
73
|
+
// An untracked run (see startAttempt): there is no record to write the answer to, so a delivery that fails
|
|
74
|
+
// from here loses it. Said out loud, because the alternative is a silent gap under a "kept for retry" log.
|
|
75
|
+
log.warn(`${label} turn ${id} answered with no record on disk (untracked run) — its answer is not recoverable`);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
turns.set(id, { ...rec, answer });
|
|
79
|
+
// Post-ACK, so it cannot throw: a failed write costs the recovery copy, not the delivery about to happen. It
|
|
80
|
+
// does decide the answer of this function — an answer only in memory survives nothing.
|
|
81
|
+
return persistBestEffort("answer (a crash before delivery would lose the answer)");
|
|
82
|
+
},
|
|
123
83
|
remove(id) {
|
|
124
84
|
if (turns.delete(id))
|
|
125
|
-
persistBestEffort("remove (a restart may
|
|
85
|
+
persistBestEffort("remove (a restart may re-deliver an answered turn)");
|
|
126
86
|
},
|
|
127
87
|
recover() {
|
|
128
|
-
// The channel's arrival order, applied explicitly rather than leaning on JS object-key enumeration
|
|
129
|
-
//
|
|
88
|
+
// The channel's arrival order, applied explicitly rather than leaning on JS object-key enumeration happening to
|
|
89
|
+
// survive the load's JSON round-trip.
|
|
130
90
|
return [...turns.values()].sort(order);
|
|
131
91
|
},
|
|
132
92
|
startAttempt(id) {
|
|
@@ -135,10 +95,11 @@ export function createTurnStore(path, opts) {
|
|
|
135
95
|
return "run"; // no record — run untracked (a redelivery double-run whose first run removed it)
|
|
136
96
|
const attempts = rec.attempts + 1;
|
|
137
97
|
if (attempts > MAX_TURN_ATTEMPTS) {
|
|
138
|
-
// State the fact, not a cause the counter can't prove: a turn killed mid-run every time bumps
|
|
139
|
-
//
|
|
98
|
+
// State the fact, not a cause the counter can't prove: a turn killed mid-run every time bumps this whether IT
|
|
99
|
+
// poisoned the process or a deploy/OOM took it down each time.
|
|
140
100
|
log.error(`${label} dropping turn ${id} after starting ${rec.attempts} time(s) without finishing ` +
|
|
141
|
-
`(session=${rec.session}) — it may be crashing the process, or was killed mid-run each time;
|
|
101
|
+
`(session=${rec.session}) — it may be crashing the process, or was killed mid-run each time; it will not ` +
|
|
102
|
+
`be run again`);
|
|
142
103
|
turns.delete(id);
|
|
143
104
|
persistBestEffort("drop");
|
|
144
105
|
return "exceeded";
|
|
@@ -148,12 +109,8 @@ export function createTurnStore(path, opts) {
|
|
|
148
109
|
persist();
|
|
149
110
|
}
|
|
150
111
|
catch (e) {
|
|
151
|
-
// The bump MUST be durable, unlike remove/drop: if it isn't, a restart's recover() reads the old
|
|
152
|
-
//
|
|
153
|
-
// on disk). Fail closed: roll the memory bump back (match disk) and DEFER — the record stays on
|
|
154
|
-
// disk with its old count and replays on the next start (a restart is required; disk recovery
|
|
155
|
-
// alone won't re-run it). Not a drop: under disk failure the removal couldn't persist anyway.
|
|
156
|
-
// The runner skips silently (no notify).
|
|
112
|
+
// The bump MUST be durable, unlike remove/drop: if it isn't, a restart's recover() reads the old count and
|
|
113
|
+
// RE-RUNS this turn.
|
|
157
114
|
turns.set(id, rec);
|
|
158
115
|
log.error(`${label} cannot persist turn ${id}'s attempt count — deferring it to the next start rather ` +
|
|
159
116
|
`than run it untracked (session=${rec.session}): ${String(e)}`);
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lark-international compatibility adapter. Feishu is the canonical protocol/runtime implementation;
|
|
3
|
-
* this module binds it to Lark's cloud profile and exposes natural Lark-branded public names. Lark's
|
|
4
|
-
* weaker control-plane capabilities live in onboarding/registration, not in a fork of the turn engine.
|
|
5
|
-
*/
|
|
1
|
+
/** Lark-international compatibility adapter. */
|
|
6
2
|
import type { ChannelModule, LongConnectionChannelModule } from "../../channel.ts";
|
|
7
3
|
import { type FeishuChannelOptions, type FeishuFailure, type FeishuMessage, type FeishuMessageEvent, type FeishuRoute, type FeishuWebSocketChannelOptions, defaultFeishuRoute } from "../feishu/feishu.ts";
|
|
8
4
|
export type LarkChannelOptions = FeishuChannelOptions;
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import type { FeishuGroupBehavior, FeishuSubscriptionMode } from "../feishu/setup-mode.ts";
|
|
2
|
-
/**
|
|
3
|
-
* Guided Lark-international onboarding. The intl cloud cannot complete the BOUND scan-to-create flow,
|
|
4
|
-
* so a new/partial setup opens its unbound one-click launcher and collects one App-scoped credential
|
|
5
|
-
* set; a complete existing ID/Secret pair resumes that App directly. Then optimistically run Feishu's
|
|
6
|
-
* webhook-mode + Verification-Token bootstrap against THIS app: a successful PATCH captures the token
|
|
7
|
-
* and flips Subscription mode; only a definitive config-route 404 falls back to the token the console
|
|
8
|
-
* displays + a manual mode switch. IO is injected so the workflow is testable without a terminal or
|
|
9
|
-
* browser.
|
|
10
|
-
*/
|
|
2
|
+
/** Guided Lark-international onboarding. */
|
|
11
3
|
export declare const LARK_CONSOLE_URL = "https://open.larksuite.com/page/launcher?from=backend_oneclick";
|
|
12
4
|
/** Directly where the fallback token lives (and where the successful automatic mode change is visible). */
|
|
13
5
|
export declare function larkEventSecurityUrl(appId: string): string;
|
|
@@ -25,7 +17,7 @@ interface LarkBootstrapResult {
|
|
|
25
17
|
manualReason?: string;
|
|
26
18
|
}
|
|
27
19
|
export interface LarkOnboardOptions {
|
|
28
|
-
/** Existing active .env values.
|
|
20
|
+
/** Existing active .env values. */
|
|
29
21
|
existing?: Readonly<Record<string, string | undefined>>;
|
|
30
22
|
ingress?: FeishuSubscriptionMode;
|
|
31
23
|
groupBehavior?: FeishuGroupBehavior;
|
|
@@ -37,7 +29,6 @@ export interface LarkOnboardCredentials {
|
|
|
37
29
|
LARK_APP_SECRET: string;
|
|
38
30
|
LARK_VERIFICATION_TOKEN?: string;
|
|
39
31
|
}
|
|
40
|
-
/** Open the stable app console and collect everything the runtime needs.
|
|
41
|
-
* failure: the scaffold remains and `add lark` is deliberately re-runnable to resume onboarding. */
|
|
32
|
+
/** Open the stable app console and collect everything the runtime needs. */
|
|
42
33
|
export declare function onboardLarkApp(io: LarkOnboardIO, opts: LarkOnboardOptions): Promise<LarkOnboardCredentials>;
|
|
43
34
|
export {};
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Guided Lark-international onboarding. The intl cloud cannot complete the BOUND scan-to-create flow,
|
|
3
|
-
* so a new/partial setup opens its unbound one-click launcher and collects one App-scoped credential
|
|
4
|
-
* set; a complete existing ID/Secret pair resumes that App directly. Then optimistically run Feishu's
|
|
5
|
-
* webhook-mode + Verification-Token bootstrap against THIS app: a successful PATCH captures the token
|
|
6
|
-
* and flips Subscription mode; only a definitive config-route 404 falls back to the token the console
|
|
7
|
-
* displays + a manual mode switch. IO is injected so the workflow is testable without a terminal or
|
|
8
|
-
* browser.
|
|
9
|
-
*/
|
|
1
|
+
/** Guided Lark-international onboarding. */
|
|
10
2
|
export const LARK_CONSOLE_URL = "https://open.larksuite.com/page/launcher?from=backend_oneclick";
|
|
11
3
|
/** Directly where the fallback token lives (and where the successful automatic mode change is visible). */
|
|
12
4
|
export function larkEventSecurityUrl(appId) {
|
|
@@ -18,14 +10,12 @@ function required(value, name) {
|
|
|
18
10
|
throw new Error(`${name} is required — re-run \`fastagent add lark\` to continue setup`);
|
|
19
11
|
return trimmed;
|
|
20
12
|
}
|
|
21
|
-
/** Open the stable app console and collect everything the runtime needs.
|
|
22
|
-
* failure: the scaffold remains and `add lark` is deliberately re-runnable to resume onboarding. */
|
|
13
|
+
/** Open the stable app console and collect everything the runtime needs. */
|
|
23
14
|
export async function onboardLarkApp(io, opts) {
|
|
24
15
|
const existingId = opts.existing?.LARK_APP_ID?.trim();
|
|
25
16
|
const existingSecret = opts.existing?.LARK_APP_SECRET?.trim();
|
|
26
17
|
const reuseExistingApp = Boolean(existingId && existingSecret);
|
|
27
|
-
// Credentials are one App-scoped set.
|
|
28
|
-
// fresh launcher/input path, and its unrelated old Token must not be attached to the newly-entered App.
|
|
18
|
+
// Credentials are one App-scoped set.
|
|
29
19
|
if (reuseExistingApp) {
|
|
30
20
|
io.note(`Reusing Lark app ${existingId}; opening its Events & Callbacks configuration directly.`);
|
|
31
21
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { larkChannel } from "@fastagent-sh/fastagent/lark";
|
|
2
|
+
import { defineChannel } from "@fastagent-sh/fastagent";
|
|
2
3
|
|
|
3
4
|
// larkChannel is the branded compatibility adapter over fastagent's canonical Feishu engine, configured
|
|
4
5
|
// with YOUR policy. fastagent discovers this file, mounts POST /lark, and pipes the agent + state home
|
|
@@ -18,23 +19,30 @@ import { larkChannel } from "@fastagent-sh/fastagent/lark";
|
|
|
18
19
|
// the URL automatically. If this app returns a config-API 404, do both BY HAND in the console
|
|
19
20
|
// with the server running (the platform verifies https://your.host/lark with a challenge).
|
|
20
21
|
// 5. create a version and publish the app (a tenant admin approves it), then add the bot to a chat
|
|
21
|
-
export default
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
22
|
+
export default defineChannel({
|
|
23
|
+
// The env vars this channel needs. fastagent carries them to a deployed box and refuses to serve
|
|
24
|
+
// while one is unset — a bare process.env read gets neither guarantee. LARK_ENCRYPT_KEY is
|
|
25
|
+
// optional, so it is NOT declared here: an undeclared read stays undeclared on purpose.
|
|
26
|
+
secrets: ["LARK_APP_ID", "LARK_APP_SECRET", "LARK_VERIFICATION_TOKEN"],
|
|
27
|
+
channel: (secrets) =>
|
|
28
|
+
larkChannel({
|
|
29
|
+
appId: secrets.LARK_APP_ID,
|
|
30
|
+
appSecret: secrets.LARK_APP_SECRET,
|
|
31
|
+
verificationToken: secrets.LARK_VERIFICATION_TOKEN, // authenticates inbound events
|
|
32
|
+
encryptKey: process.env.LARK_ENCRYPT_KEY || undefined, // optional; when set, plaintext events are refused
|
|
33
|
+
// No session modes: a chat is one session and a thread is another, and where the answer goes follows
|
|
34
|
+
// from that (docs/design/participant-model.md).
|
|
35
|
+
// Dev/personal bot: surface raw errors to the chat so you (and your AI agent) can act on them. The
|
|
36
|
+
// chat is customer-facing by default — for a public bot, drop this or return a neutral string;
|
|
37
|
+
// full details always go to the server log regardless.
|
|
38
|
+
onError: (failed) => `⚠️ ${failed.details}`,
|
|
39
|
+
// The channel owns transport + format (markdown card) + attachments (image→vision, file→disk) +
|
|
40
|
+
// the live streaming preview. `route` (POLICY) is OPTIONAL — omitted, it uses defaultLarkRoute:
|
|
41
|
+
// p2p chats always answer; groups answer on @this-bot, plus bare messages in a thread where the
|
|
42
|
+
// Agent takes part and exactly ONE human does. Other human group/thread discussion buffers until
|
|
43
|
+
// that place's next answered turn; @other-only messages buffer rather than triggering the Agent.
|
|
44
|
+
// Override to customise explicit routing, reusing the export:
|
|
45
|
+
// route: (e) => defaultLarkRoute(e, { botOpenId: "ou_xxx" }) && { session: `user:${e.sender?.sender_id?.open_id}` },
|
|
46
|
+
// route: (e) => defaultLarkRoute(e, { botOpenId: "ou_xxx" }) && { text: `${larkEnvelope(e)}\n[extra]` },
|
|
47
|
+
}),
|
|
40
48
|
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { larkWebSocketChannel } from "@fastagent-sh/fastagent/lark";
|
|
2
|
+
import { defineChannel } from "@fastagent-sh/fastagent";
|
|
3
|
+
|
|
4
|
+
// Lark WebSocket long connection: the process connects OUT to the platform, so no public URL,
|
|
5
|
+
// Verification Token, Encrypt Key, or --tunnel is needed. In Events & Callbacks choose long
|
|
6
|
+
// connection, subscribe im.message.receive_v1, then publish the app version. Keep one process
|
|
7
|
+
// running in production: scale-to-zero/App Sleeping would disconnect ingress.
|
|
8
|
+
export default defineChannel({
|
|
9
|
+
// The env vars this channel needs. fastagent carries them to a deployed box and refuses to serve
|
|
10
|
+
// while one is unset — a bare process.env read gets neither guarantee.
|
|
11
|
+
secrets: ["LARK_APP_ID", "LARK_APP_SECRET"],
|
|
12
|
+
channel: (secrets) =>
|
|
13
|
+
larkWebSocketChannel({
|
|
14
|
+
appId: secrets.LARK_APP_ID,
|
|
15
|
+
appSecret: secrets.LARK_APP_SECRET,
|
|
16
|
+
// No session modes: a chat is one session and a thread is another, and where the answer goes follows
|
|
17
|
+
// from that (docs/design/participant-model.md).
|
|
18
|
+
// Dev/personal bot: surface raw errors to the chat so you (and your AI agent) can act on them. The
|
|
19
|
+
// chat is customer-facing by default — for a public bot, drop this or return a neutral string;
|
|
20
|
+
// full details always go to the server log regardless.
|
|
21
|
+
onError: (failed) => `⚠️ ${failed.details}`,
|
|
22
|
+
// The channel owns transport + format (markdown card) + attachments (image→vision, file→disk) +
|
|
23
|
+
// the live streaming preview. `route` (POLICY) is OPTIONAL — omitted, it uses defaultLarkRoute:
|
|
24
|
+
// p2p chats always answer; groups answer on @this-bot, plus bare messages in a thread where the
|
|
25
|
+
// Agent takes part and exactly ONE human does. Other human group/thread discussion buffers until
|
|
26
|
+
// that place's next answered turn; @other-only messages buffer rather than triggering the Agent.
|
|
27
|
+
// Override to customise explicit routing, reusing the export:
|
|
28
|
+
// route: (e) => defaultLarkRoute(e, { botOpenId: "ou_xxx" }) && { session: `user:${e.sender?.sender_id?.open_id}` },
|
|
29
|
+
// route: (e) => defaultLarkRoute(e, { botOpenId: "ou_xxx" }) && { text: `${larkEnvelope(e)}\n[extra]` },
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
@@ -1,44 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SHARED: the webhook registrars' outcome. A registrar reports its own FACT; what to do about it
|
|
3
|
-
*
|
|
4
|
-
* "failed", the tunnel (a long-running dev process) ignores the result entirely.
|
|
2
|
+
* SHARED: the webhook registrars' outcome. A registrar reports its own FACT; what to do about it is the CALLER's
|
|
3
|
+
* policy — `deploy --run` gates on "failed", the tunnel ignores the result entirely.
|
|
5
4
|
*
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* register it; the console is the only path).
|
|
12
|
-
* - "failed": this run ends with the webhook NOT registered, and acting + re-running can fix it
|
|
13
|
-
* (a permanent config error, or the platform still unable to reach the URL when the retries ran out).
|
|
5
|
+
* - `registered`: the platform accepted the webhook / event URL.
|
|
6
|
+
* - `manual`: this run did not fail, but an operator-facing step remains (the registrar printed it). Re-runnability
|
|
7
|
+
* differs by sub-state: credentials not configured re-registers on a re-run; a cloud without the config API never
|
|
8
|
+
* will (the console is the only path).
|
|
9
|
+
* - `failed`: the webhook is NOT registered, and acting plus re-running can fix it.
|
|
14
10
|
*/
|
|
15
11
|
export type RegistrationOutcome = "registered" | "manual" | "failed";
|
|
16
12
|
export declare const REGISTRATION_RETRY_MS = 10000;
|
|
17
|
-
/**
|
|
18
|
-
* What `deploy --run` spends instead: 180s, because a host CLI returns before the deployment serves —
|
|
19
|
-
* the same gap a tunnel has, on a scale no local signal reports (`docker compose --tunnel` is the one
|
|
20
|
-
* host whose ingress does report it, and its driver waits on it the way the tunnel does).
|
|
21
|
-
*
|
|
22
|
-
* `railway up --ci` returns when the BUILD ends — container start, healthcheck and a freshly minted
|
|
23
|
-
* domain's DNS all happen after that, and railway-deploy.live.test.ts allows 180s for exactly this.
|
|
24
|
-
* Registration failure GATES the deploy (registration-gate.ts), so a budget shorter than the host's
|
|
25
|
-
* own start-up turns a working deployment into a re-run instruction.
|
|
26
|
-
*
|
|
27
|
-
* N attempts buy (N - 1) waits — {@link retryWhile} only waits BETWEEN calls — so 180s of patience is
|
|
28
|
-
* 19, not 18. At 18 the last call went out at t=170s and a host that started answering in the final
|
|
29
|
-
* ten seconds was still reported as a deploy to re-run.
|
|
30
|
-
*/
|
|
13
|
+
/** What `deploy --run` spends instead: 180s, because a host CLI returns before the deployment serves. */
|
|
31
14
|
export declare const DEPLOY_REGISTRATION_ATTEMPTS = 19;
|
|
32
|
-
/**
|
|
33
|
-
* Call, and while `retryable` says the failure is the platform not reaching the URL YET, call again —
|
|
34
|
-
* the one retry loop every registrar spends {@link REGISTRATION_ATTEMPTS} through. Retryability and
|
|
35
|
-
* what a final failure MEANS stay with each platform (their vocabularies differ, and one registrar's
|
|
36
|
-
* terminal state is another's `"manual"`); the counting, the announcement and the wait do not.
|
|
37
|
-
*
|
|
38
|
-
* `onRetry` runs BEFORE the wait — registrars announce there (a silent minute reads as a hang), and
|
|
39
|
-
* `add slack` also drops its duplicate-guard marker so it never spans a sleep. The last error is
|
|
40
|
-
* thrown, so a caller can tell "still unreachable" from "a config error" in one place.
|
|
41
|
-
*/
|
|
15
|
+
/** Call, and while `retryable` says the failure is the platform not reaching the URL YET, call again. */
|
|
42
16
|
export declare function retryWhile<T>(call: () => Promise<T>, retryable: (error: unknown) => boolean, options?: {
|
|
43
17
|
attempts?: number;
|
|
44
18
|
retryMs?: number;
|
|
@@ -1,45 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SHARED: how long a registrar waits for the PLATFORM to be able to reach a freshly minted public URL.
|
|
3
|
-
* One judgement (a new tunnel's warm-up), so one pair of numbers — telegram, feishu, slack registration
|
|
4
|
-
* and `add slack` all spend it.
|
|
5
|
-
*
|
|
6
|
-
* Sized for a tunnel, which is live before its URL is HANDED OVER — not before it is printed, which is
|
|
7
|
-
* what this budget once had to absorb: a quick tunnel's hostname is published when the tunnel registers
|
|
8
|
-
* an edge connection, seconds after cloudflared prints it, and no budget covers a platform that already
|
|
9
|
-
* answered "cannot resolve" (#435). `startCloudflareTunnel` waits for that connection, so what is left
|
|
10
|
-
* here is the platform's own warm-up. Applied by {@link retryWhile} as its default; a DEPLOY is slower
|
|
11
|
-
* and its callers pass {@link DEPLOY_REGISTRATION_ATTEMPTS} instead.
|
|
12
|
-
*/
|
|
1
|
+
/** SHARED: how long a registrar waits for the PLATFORM to be able to reach a freshly minted public URL. */
|
|
13
2
|
const REGISTRATION_ATTEMPTS = 8;
|
|
14
3
|
export const REGISTRATION_RETRY_MS = 10_000;
|
|
15
|
-
/**
|
|
16
|
-
* What `deploy --run` spends instead: 180s, because a host CLI returns before the deployment serves —
|
|
17
|
-
* the same gap a tunnel has, on a scale no local signal reports (`docker compose --tunnel` is the one
|
|
18
|
-
* host whose ingress does report it, and its driver waits on it the way the tunnel does).
|
|
19
|
-
*
|
|
20
|
-
* `railway up --ci` returns when the BUILD ends — container start, healthcheck and a freshly minted
|
|
21
|
-
* domain's DNS all happen after that, and railway-deploy.live.test.ts allows 180s for exactly this.
|
|
22
|
-
* Registration failure GATES the deploy (registration-gate.ts), so a budget shorter than the host's
|
|
23
|
-
* own start-up turns a working deployment into a re-run instruction.
|
|
24
|
-
*
|
|
25
|
-
* N attempts buy (N - 1) waits — {@link retryWhile} only waits BETWEEN calls — so 180s of patience is
|
|
26
|
-
* 19, not 18. At 18 the last call went out at t=170s and a host that started answering in the final
|
|
27
|
-
* ten seconds was still reported as a deploy to re-run.
|
|
28
|
-
*/
|
|
4
|
+
/** What `deploy --run` spends instead: 180s, because a host CLI returns before the deployment serves. */
|
|
29
5
|
export const DEPLOY_REGISTRATION_ATTEMPTS = 19;
|
|
30
|
-
/** Sleep on the GLOBAL timer (not `node:timers/promises`) so tests can drive it with fake timers — the
|
|
31
|
-
* same reason feishu-api.ts does. What must happen BEFORE this wait is the point of `onRetry`. */
|
|
32
|
-
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
33
6
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* what a final failure MEANS stay with each platform (their vocabularies differ, and one registrar's
|
|
37
|
-
* terminal state is another's `"manual"`); the counting, the announcement and the wait do not.
|
|
38
|
-
*
|
|
39
|
-
* `onRetry` runs BEFORE the wait — registrars announce there (a silent minute reads as a hang), and
|
|
40
|
-
* `add slack` also drops its duplicate-guard marker so it never spans a sleep. The last error is
|
|
41
|
-
* thrown, so a caller can tell "still unreachable" from "a config error" in one place.
|
|
7
|
+
* Sleep on the GLOBAL timer (not `node:timers/promises`) so tests can drive it with fake timers — the same reason
|
|
8
|
+
* feishu-api.ts does.
|
|
42
9
|
*/
|
|
10
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
/** Call, and while `retryable` says the failure is the platform not reaching the URL YET, call again. */
|
|
43
12
|
export async function retryWhile(call, retryable, options = {}) {
|
|
44
13
|
const attempts = options.attempts ?? REGISTRATION_ATTEMPTS;
|
|
45
14
|
const retryMs = options.retryMs ?? REGISTRATION_RETRY_MS;
|
package/dist/channels/secret.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The ONE constant-time comparison every shared-secret gate uses: a webhook secret token, a
|
|
3
|
-
*
|
|
4
|
-
* answers false without leaking a prefix. Non-string input reads as no secret — and an EMPTY
|
|
5
|
-
* expected value never matches, so a gate whose secret was never configured cannot be passed by
|
|
6
|
-
* sending none.
|
|
2
|
+
* The ONE constant-time comparison every shared-secret gate uses: a webhook secret token, a signature, a bearer token,
|
|
3
|
+
* an envelope secret.
|
|
7
4
|
*/
|
|
8
5
|
import { Buffer } from "node:buffer";
|
|
9
6
|
import { timingSafeEqual } from "node:crypto";
|
package/dist/channels/serve.d.ts
CHANGED
|
@@ -1,68 +1,71 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { ChannelHandler, Routes } from "../channel.ts";
|
|
3
|
-
/** Parse a route key: `"METHOD /path"` → `{ method, path }`, or `"/path"` → `{ path }` (any method).
|
|
4
|
-
* An empty method (`" /x"`) parses as `""`, which {@link assertRouteKey} refuses — see there. */
|
|
3
|
+
/** Parse a route key: `"METHOD /path"` → `{ method, path }`, or `"/path"` → `{ path }` (any method). */
|
|
5
4
|
export declare function parseRouteKey(key: string): {
|
|
6
5
|
method?: string;
|
|
7
6
|
path: string;
|
|
8
7
|
};
|
|
9
|
-
/**
|
|
10
|
-
* A route key is `"METHOD /path"` or `"/path"`, with a literal path.
|
|
11
|
-
*
|
|
12
|
-
* Every rule here exists because breaking it costs SOMEONE ELSE. Two keys naming one route, a route
|
|
13
|
-
* under a mount, a path a URL rewrites (`/a/../x` and `/x` are one route spelled two ways, while
|
|
14
|
-
* the conflict check compares strings) — in each case a channel goes dark and its author did
|
|
15
|
-
* nothing wrong.
|
|
16
|
-
*
|
|
17
|
-
* Mistakes that only cost their own author a debugging session are NOT policed: `:id` and `*` are
|
|
18
|
-
* literal path characters here, so such a key simply never matches, and an unusual method is the
|
|
19
|
-
* same. Refusing those would trade a working capability for a lecture.
|
|
20
|
-
*/
|
|
8
|
+
/** A route key is `"METHOD /path"` or `"/path"`, with a literal path. */
|
|
21
9
|
export declare function assertRouteKey(key: string, describe: (problem: string) => string): void;
|
|
22
|
-
/** Do these two keys fight over the same request?
|
|
23
|
-
* with no method answers all of them. */
|
|
10
|
+
/** Do these two keys fight over the same request? */
|
|
24
11
|
export declare function routeKeysConflict(a: string, b: string): boolean;
|
|
25
|
-
/** A handler owning a path prefix and everything beneath it — the session control plane is the one
|
|
26
|
-
* user. Kept out of {@link Routes} so a key is always a literal path and collision checks stay
|
|
27
|
-
* comparisons. */
|
|
12
|
+
/** A handler owning a path prefix and everything beneath it — the session control plane is the one user. */
|
|
28
13
|
export interface PrefixMount {
|
|
29
|
-
/** Absolute, no trailing slash, and not `/` (`/control`).
|
|
30
|
-
* The root is excluded deliberately: a handler owning every path is that handler, and routing to
|
|
31
|
-
* it through here would only add a table nothing can reach. */
|
|
14
|
+
/** Absolute, no trailing slash, and not `/` (`/control`). */
|
|
32
15
|
prefix: string;
|
|
33
16
|
handler: ChannelHandler;
|
|
34
17
|
}
|
|
35
|
-
/** Same status and headers, no content (RFC 9110's HEAD).
|
|
36
|
-
* streaming producer keeps running with no reader. Shared with the control plane, which answers
|
|
37
|
-
* HEAD too. */
|
|
18
|
+
/** Same status and headers, no content (RFC 9110's HEAD). */
|
|
38
19
|
export declare function withoutBody(res: Response): Response;
|
|
39
|
-
/** Is `path` inside `prefix`? Segment-wise, so `/controlled` is not inside `/control`. */
|
|
40
20
|
export declare function pathUnderPrefix(path: string, prefix: string): boolean;
|
|
41
21
|
/**
|
|
42
|
-
*
|
|
22
|
+
* Refuse a cross-origin allow-list that cannot match anything.
|
|
43
23
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
24
|
+
* ONE check for BOTH ways in: `http.cors` in a config file, and `corsOrigins` handed to `mountAgentService` by an
|
|
25
|
+
* embedder. `allowedOrigin` compares exact strings, so `"https://app.example.com/"` — one trailing slash — never
|
|
26
|
+
* matches and the front end is refused with nothing pointing at the rule. A rule that silently matches nothing is
|
|
27
|
+
* worse than one that refuses to load.
|
|
28
|
+
*/
|
|
29
|
+
export declare function assertCorsOrigins(origins: unknown, where: string): asserts origins is string[];
|
|
30
|
+
/**
|
|
31
|
+
* The tables a router composes, NAMED rather than ordered.
|
|
47
32
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
33
|
+
* Two same-typed positional arguments encoded which side of the trust axis a table was on, and getting them the
|
|
34
|
+
* wrong way round type-checked: the AgentCore adapter passed its IAM-gated routes as `unverified` and they were
|
|
35
|
+
* reported as unauthenticated for two commits. A caller now spells the axis at the call site.
|
|
50
36
|
*/
|
|
51
|
-
export
|
|
52
|
-
/**
|
|
53
|
-
|
|
37
|
+
export interface RouterSurface {
|
|
38
|
+
/** Routes that authenticate NOBODY — the router applies the JSON body gate and the cross-origin policy to them. */
|
|
39
|
+
unverified?: Routes;
|
|
40
|
+
/** Routes that verify their own caller (a channel's platform signature, a host's IAM). Neither guard applies. */
|
|
41
|
+
selfVerifying?: Routes;
|
|
42
|
+
/** Prefix-owning handlers. Always treated as unverified — the control plane is the only one. */
|
|
43
|
+
mounts?: readonly PrefixMount[];
|
|
44
|
+
/** `http.cors` — exact origins, or `["*"]`. Set, it REPLACES the `*` default; unset, the default stands. */
|
|
45
|
+
corsOrigins?: readonly string[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Compose the routes that AUTHENTICATE NOBODY, the self-verifying channel routes, and the {@link PrefixMount}s
|
|
49
|
+
* into one handler.
|
|
54
50
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
|
|
51
|
+
* That is the axis, not authorship. Two things follow from it and nothing else does: whether a browser may drive
|
|
52
|
+
* the route, and whether its body must declare JSON. A channel route is exempt from both because it checks its
|
|
53
|
+
* platform's signature inside itself — a Telegram webhook is public on purpose. Ours check nothing, so the router
|
|
54
|
+
* checks for them.
|
|
55
|
+
*
|
|
56
|
+
* TWO TABLES, not one table plus a list of which keys are which, so the three readers (browser reachability, the JSON
|
|
57
|
+
* body gate, the startup report) cannot answer differently.
|
|
58
|
+
*
|
|
59
|
+
* CAVEAT, stated because we cannot enforce it: a CUSTOM channel that verifies nothing lands in `selfVerifying`
|
|
60
|
+
* anyway and gets neither guard. That is decision B (docs/design/session-control.md §14) and it belongs to the
|
|
61
|
+
* channel's author, who owns the credential the platform issued — but it is an assumption here, not a property.
|
|
62
|
+
*/
|
|
63
|
+
export declare function router(surface: RouterSurface): ChannelHandler;
|
|
60
64
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* `listening` resolves with the bound port (useful for port 0) or rejects on a bind error.
|
|
64
|
-
* `host` is the bind address; unset means all interfaces (what containers need).
|
|
65
|
+
* The node:http adapter for a Fetch handler — the embedded server uses it, and an embedder mounting fastagent on its
|
|
66
|
+
* OWN node:http server can too.
|
|
65
67
|
*/
|
|
68
|
+
export declare function nodeListener(handler: ChannelHandler): (req: IncomingMessage, res: ServerResponse) => void;
|
|
66
69
|
export declare function serveNode(handler: ChannelHandler, options: {
|
|
67
70
|
port: number;
|
|
68
71
|
host?: string;
|