@fastagent-sh/fastagent 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -43
- 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,29 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram webhook registration — the TELEGRAM-domain step both `--tunnel` (dev, tunnel.ts) and
|
|
3
|
-
*
|
|
4
|
-
* here, beside the channel it serves; it reads the same .env tokens the channel uses.
|
|
5
|
-
*
|
|
6
|
-
* READINESS IS setWebhook'S OWN VERDICT, not a local probe. Telegram VERIFIES the URL during the call,
|
|
7
|
-
* from Telegram's network — which is the only network that has to reach it. This used to poll
|
|
8
|
-
* `<baseUrl>/health` from here first, and that probe answered a different question badly: a freshly
|
|
9
|
-
* minted hostname (a quick tunnel, a fresh deploy) is routinely unreachable from the machine running
|
|
10
|
-
* the CLI for a minute or more — behind a proxy, or while its own resolver still says ENOTFOUND — long
|
|
11
|
-
* after Telegram can reach it (#421). So the retry loop below IS the wait: Telegram's reachability
|
|
12
|
-
* verdicts ("Failed to resolve host", "Wrong response from the webhook") are retried, and only a
|
|
13
|
-
* configuration error the operator must fix is reported once.
|
|
2
|
+
* Telegram webhook registration — the TELEGRAM-domain step both `--tunnel` (dev, tunnel.ts) and `deploy … --run` (the
|
|
3
|
+
* host runners' post-deploy step) invoke.
|
|
14
4
|
*/
|
|
15
5
|
import { log } from "../../log.js";
|
|
16
6
|
import { retryWhile } from "../registration.js";
|
|
17
7
|
import { callApi } from "./telegram-api.js";
|
|
18
|
-
/**
|
|
19
|
-
* Whether a setWebhook failure is the URL still warming up rather than a configuration error. Two
|
|
20
|
-
* families: transport failures reaching Telegram at all, and Telegram's own verdict after it fails to
|
|
21
|
-
* reach the webhook — which carries its connection layer's words verbatim (`Bad Request: bad webhook:
|
|
22
|
-
* Connection timed out` / `Connection refused` for a host whose DNS answers before anything listens,
|
|
23
|
-
* `Wrong response from the webhook: 530` for a tunnel edge without its origin). This is the ONLY gate
|
|
24
|
-
* between "spend the caller's retry budget" and "fail now", so it is wide on the reachability side.
|
|
25
|
-
* A permanent "bad webhook" — an http:// URL, an unsupported port — matches nothing here and is reported.
|
|
26
|
-
*/
|
|
8
|
+
/** Whether a setWebhook failure is the URL still warming up rather than a configuration error. */
|
|
27
9
|
function isTransientRegistrationError(error) {
|
|
28
10
|
return /resolve host|getaddrinfo|ENOTFOUND|fetch failed|ECONNRESET|timeout|timed out|connection refused|can't connect|connection to the host|wrong response from the webhook/i.test(error);
|
|
29
11
|
}
|
|
@@ -54,8 +36,8 @@ export async function registerTelegramWebhook(baseUrl, opts = {}) {
|
|
|
54
36
|
return "registered";
|
|
55
37
|
}
|
|
56
38
|
catch (e) {
|
|
57
|
-
// Both endings leave the webhook unregistered and the operator with work to do, so both are ERROR;
|
|
58
|
-
//
|
|
39
|
+
// Both endings leave the webhook unregistered and the operator with work to do, so both are ERROR; they differ
|
|
40
|
+
// only in what to do about it.
|
|
59
41
|
const error = String(e);
|
|
60
42
|
log.error(isTransientRegistrationError(error)
|
|
61
43
|
? `[fastagent] telegram: Telegram could not reach ${webhookUrl} after retries (last error: ${error}). ` +
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { telegramChannel } from "@fastagent-sh/fastagent/telegram";
|
|
2
|
+
import { defineChannel } from "@fastagent-sh/fastagent";
|
|
2
3
|
|
|
3
4
|
// A channel = a third-party ADAPTER (telegramChannel: verify + run + reply) configured with YOUR policy.
|
|
4
5
|
// fastagent discovers this file under channels/, mounts POST /telegram, and pipes the agent + state
|
|
@@ -8,22 +9,28 @@ import { telegramChannel } from "@fastagent-sh/fastagent/telegram";
|
|
|
8
9
|
// 3. register the webhook once, pointing Telegram at POST /telegram with that secret:
|
|
9
10
|
// curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" \
|
|
10
11
|
// -d url=https://your.host/telegram -d secret_token=$TELEGRAM_SECRET_TOKEN
|
|
11
|
-
export default
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
12
|
+
export default defineChannel({
|
|
13
|
+
// The env vars this channel needs. fastagent carries them to a deployed box and refuses to serve
|
|
14
|
+
// while one is unset — a bare process.env read gets neither guarantee.
|
|
15
|
+
secrets: ["TELEGRAM_BOT_TOKEN", "TELEGRAM_SECRET_TOKEN"],
|
|
16
|
+
channel: (secrets) =>
|
|
17
|
+
telegramChannel({
|
|
18
|
+
secretToken: secrets.TELEGRAM_SECRET_TOKEN, // verifies inbound updates really come from Telegram
|
|
19
|
+
botToken: secrets.TELEGRAM_BOT_TOKEN, // used to send the agent's reply back to the chat
|
|
20
|
+
// Dev/personal bot: surface raw errors to the chat so you (and your AI agent) can act on them. The
|
|
21
|
+
// chat is customer-facing by default — for a public bot, drop this or return a neutral string;
|
|
22
|
+
// full details always go to the server log regardless.
|
|
23
|
+
onError: (failed) => `⚠️ ${failed.details}`,
|
|
24
|
+
// Your bot's @username. Optional — resolved via getMe when omitted — but set it if you pass a `route`
|
|
25
|
+
// below: the channel needs its own copy to recognise a `/stop` addressed to it.
|
|
26
|
+
// botUsername: "your_bot",
|
|
27
|
+
// The channel owns transport + format (HTML) + attachments (photo→vision, file→disk) + streaming.
|
|
28
|
+
// `route` (POLICY) is OPTIONAL — omitted, it uses defaultTelegramRoute: private chats always answer,
|
|
29
|
+
// groups only on a reply to THIS bot, an @mention of it, or a `/stop` addressed to it. Override to
|
|
30
|
+
// customise, reusing the export — but pass your bot's identity BOTH here and as `botUsername` above
|
|
31
|
+
// (the omitted default gets it from the channel; a bare `defaultTelegramRoute(u)` answers only
|
|
32
|
+
// private chats):
|
|
33
|
+
// route: (u) => defaultTelegramRoute(u, { botUsername: "my_bot" }) && { session: `user:${u.message?.from?.id}` },
|
|
34
|
+
// route: (u) => defaultTelegramRoute(u, { botUsername: "my_bot" }) && { text: `${telegramEnvelope(u.message!)}\n[extra]` },
|
|
35
|
+
}),
|
|
29
36
|
});
|
|
@@ -4,7 +4,7 @@ import { basename } from "node:path";
|
|
|
4
4
|
|
|
5
5
|
// Send a message or a local file back to a Telegram chat. In a CHAT turn the channel delivers the
|
|
6
6
|
// reply itself — this tool is for files, and for turns NO channel is carrying: a scheduled turn
|
|
7
|
-
// (
|
|
7
|
+
// (routines/<name>.ts) or a self-scheduled wake-up, whose plain reply is not delivered anywhere.
|
|
8
8
|
// The chatId comes from the [telegram: chat …] context line in a chat turn; a scheduled turn has no
|
|
9
9
|
// such line, so the schedule's prompt must name the target chat id. tools/ is auto-discovered.
|
|
10
10
|
|
|
@@ -73,9 +73,11 @@ export default defineTool({
|
|
|
73
73
|
asPhoto: z.boolean().optional().describe("send the file as a photo (inline) instead of a document"),
|
|
74
74
|
messageThreadId: z.number().optional().describe("thread to reply into (from the context line), if any"),
|
|
75
75
|
}),
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
// The bot token, declared: fastagent carries it to a deployed box and refuses to start while it is
|
|
77
|
+
// unset, so `execute` gets a value rather than checking for one.
|
|
78
|
+
secrets: ["TELEGRAM_BOT_TOKEN"],
|
|
79
|
+
async execute({ chatId, text, path, caption, asPhoto, messageThreadId }, ctx) {
|
|
80
|
+
const token = ctx.secrets.TELEGRAM_BOT_TOKEN;
|
|
79
81
|
if ((text === undefined) === (path === undefined)) {
|
|
80
82
|
throw new Error("pass exactly one of `text` (a message) or `path` (a file)");
|
|
81
83
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ImageRef } from "../../agent.ts";
|
|
2
|
+
import type { CallOptions } from "../kit/transport.ts";
|
|
2
3
|
/** Telegram's hard text limit per message. */
|
|
3
4
|
export declare const TELEGRAM_MAX_TEXT = 4096;
|
|
4
5
|
/** Where a reply goes: a chat, optionally a thread (Threaded Mode), optionally replying to a message. */
|
|
@@ -37,7 +38,7 @@ interface Api {
|
|
|
37
38
|
* The pipeline: one Bot API call, carrying every transport invariant (see the module header). Returns
|
|
38
39
|
* the method's `result` payload; throws {@link TelegramApiError} on any failure.
|
|
39
40
|
*/
|
|
40
|
-
export declare function callApi<M extends keyof Api>(api: string, botToken: string, method: M, params: Record<string, unknown
|
|
41
|
+
export declare function callApi<M extends keyof Api>(api: string, botToken: string, method: M, params: Record<string, unknown>, opts?: CallOptions): Promise<Api[M]>;
|
|
41
42
|
/**
|
|
42
43
|
* Split text into ≤4096-char chunks (Telegram's limit), preferring a newline boundary. When `html`, it is
|
|
43
44
|
* tag-aware: a tag that would SPAN a boundary is CLOSED at the chunk's end and REOPENED (attributes and
|
|
@@ -59,7 +60,7 @@ export declare function chunkText(text: string, opts?: {
|
|
|
59
60
|
*/
|
|
60
61
|
export declare function sendMessage(api: string, botToken: string, t: Target, body: string, opts?: {
|
|
61
62
|
html?: boolean;
|
|
62
|
-
}): Promise<number | undefined>;
|
|
63
|
+
} & CallOptions): Promise<number | undefined>;
|
|
63
64
|
/**
|
|
64
65
|
* Edit a message in place — the live-preview mechanism (one message, repeatedly updated). Plain by
|
|
65
66
|
* default (a partial preview may contain unbalanced HTML); `html:true` for the final answer, falling
|
|
@@ -68,7 +69,7 @@ export declare function sendMessage(api: string, botToken: string, t: Target, bo
|
|
|
68
69
|
*/
|
|
69
70
|
export declare function editMessageText(api: string, botToken: string, t: Target, messageId: number, body: string, opts?: {
|
|
70
71
|
html?: boolean;
|
|
71
|
-
}): Promise<void>;
|
|
72
|
+
} & CallOptions): Promise<void>;
|
|
72
73
|
/** Fetch the message's images. Throws if any cannot be loaded — the caller surfaces it (no silent drop). */
|
|
73
74
|
export declare function resolveImages(api: string, botToken: string, fileIds: string[] | undefined): Promise<ImageRef[] | undefined>;
|
|
74
75
|
/** Download the message's files into `filesDir` (the channel passes its state home's `files/`).
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* connection cannot hang a turn, its session queue, or webhook registration.
|
|
8
8
|
* 2. Only a 429 is retried (bounded attempts, honouring `retry_after` up to FLOOD_WAIT_MAX_S per
|
|
9
9
|
* wait); a longer flood ban or exhausted retries fail visibly. No other failure class is retried:
|
|
10
|
-
* the request may have been processed, and a retried sendMessage would double-deliver.
|
|
10
|
+
* the request may have been processed, and a retried sendMessage would double-deliver. A caller whose
|
|
11
|
+
* write is DROPPABLE spends no budget on it — see kit/transport.ts.
|
|
11
12
|
* 3. Success requires the body's own `ok:true` — an intermediary's HTTP 200 is not a sent message.
|
|
12
13
|
* 4. Every failure is a {@link TelegramApiError} naming the method: self-description is a property of
|
|
13
14
|
* the error type, not per-call-site string assembly.
|
|
@@ -62,7 +63,8 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
62
63
|
* The pipeline: one Bot API call, carrying every transport invariant (see the module header). Returns
|
|
63
64
|
* the method's `result` payload; throws {@link TelegramApiError} on any failure.
|
|
64
65
|
*/
|
|
65
|
-
export async function callApi(api, botToken, method, params) {
|
|
66
|
+
export async function callApi(api, botToken, method, params, opts = {}) {
|
|
67
|
+
const retries = opts.retries ?? RETRIES;
|
|
66
68
|
for (let attempt = 0;; attempt++) {
|
|
67
69
|
let res;
|
|
68
70
|
let raw;
|
|
@@ -87,7 +89,7 @@ export async function callApi(api, botToken, method, params) {
|
|
|
87
89
|
}
|
|
88
90
|
if (res.ok && data.ok === true)
|
|
89
91
|
return data.result;
|
|
90
|
-
if (res.status === 429 && attempt <
|
|
92
|
+
if (res.status === 429 && attempt < retries) {
|
|
91
93
|
const floodWait = data.parameters?.retry_after ?? attempt + 1; // no retry_after → short linear backoff
|
|
92
94
|
if (floodWait <= FLOOD_WAIT_MAX_S) {
|
|
93
95
|
await wait((floodWait + 1) * 1000);
|
|
@@ -216,7 +218,7 @@ export async function sendMessage(api, botToken, t, body, opts = {}) {
|
|
|
216
218
|
}
|
|
217
219
|
let result;
|
|
218
220
|
try {
|
|
219
|
-
result = await callApi(api, botToken, "sendMessage", mode ? { ...base, parse_mode: "HTML" } : base);
|
|
221
|
+
result = await callApi(api, botToken, "sendMessage", mode ? { ...base, parse_mode: "HTML" } : base, opts);
|
|
220
222
|
}
|
|
221
223
|
catch (e) {
|
|
222
224
|
if (!(mode && e instanceof TelegramApiError && PARSE_ERROR.test(e.description)))
|
|
@@ -229,7 +231,7 @@ export async function sendMessage(api, botToken, t, body, opts = {}) {
|
|
|
229
231
|
continue;
|
|
230
232
|
}
|
|
231
233
|
// A later chunk failed after the first parsed cleanly (rare) — best-effort plain resend of this chunk.
|
|
232
|
-
result = await callApi(api, botToken, "sendMessage", base);
|
|
234
|
+
result = await callApi(api, botToken, "sendMessage", base, opts);
|
|
233
235
|
}
|
|
234
236
|
if (first)
|
|
235
237
|
firstId = result.message_id;
|
|
@@ -251,7 +253,7 @@ export async function editMessageText(api, botToken, t, messageId, body, opts =
|
|
|
251
253
|
};
|
|
252
254
|
const notModified = (e) => e instanceof TelegramApiError && /message is not modified/i.test(e.description);
|
|
253
255
|
try {
|
|
254
|
-
await callApi(api, botToken, "editMessageText", opts.html ? { ...base, parse_mode: "HTML" } : base);
|
|
256
|
+
await callApi(api, botToken, "editMessageText", opts.html ? { ...base, parse_mode: "HTML" } : base, opts);
|
|
255
257
|
}
|
|
256
258
|
catch (e) {
|
|
257
259
|
if (notModified(e))
|
|
@@ -259,7 +261,7 @@ export async function editMessageText(api, botToken, t, messageId, body, opts =
|
|
|
259
261
|
if (!(opts.html && e instanceof TelegramApiError && PARSE_ERROR.test(e.description)))
|
|
260
262
|
throw e;
|
|
261
263
|
try {
|
|
262
|
-
await callApi(api, botToken, "editMessageText", base); // plain fallback for rejected markup
|
|
264
|
+
await callApi(api, botToken, "editMessageText", base, opts); // plain fallback for rejected markup
|
|
263
265
|
}
|
|
264
266
|
catch (e2) {
|
|
265
267
|
if (!notModified(e2))
|
|
@@ -8,28 +8,14 @@ export interface TelegramChannelOptions {
|
|
|
8
8
|
secretToken: string;
|
|
9
9
|
/** Bot token — used to send the agent's reply via the Bot API. */
|
|
10
10
|
botToken: string;
|
|
11
|
-
/** Policy: whether/where to answer an update (return null to ignore).
|
|
11
|
+
/** Policy: whether/where to answer an update (return null to ignore). */
|
|
12
12
|
route?: (update: TelegramUpdate) => TelegramRoute | null;
|
|
13
|
-
/**
|
|
14
|
-
* Customer-facing failure text for the chat (the dev-facing full `details` always go to the operator
|
|
15
|
-
* log). Return a string to send it, or undefined/"" to stay silent. Default: a neutral message keyed
|
|
16
|
-
* on `retryable`. A developer's own bot can surface the raw details, e.g. `(f) => `⚠️ ${f.details}``.
|
|
17
|
-
*/
|
|
13
|
+
/** Customer-facing failure text for the chat (the dev-facing full `details` always go to the operator log). */
|
|
18
14
|
onError?: (failed: TelegramFailure) => string | undefined;
|
|
19
15
|
/** Bot @username for group @mention summon by the default route (else resolved via getMe). */
|
|
20
16
|
botUsername?: string;
|
|
21
|
-
/** Bot API base, for tests.
|
|
17
|
+
/** Bot API base, for tests. */
|
|
22
18
|
apiBaseUrl?: string;
|
|
23
19
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Build a Telegram bot channel: policy options in, a {@link ChannelModule} out. The framework (or an
|
|
26
|
-
* embedder) mounts it with the context — `telegramChannel(opts)` in `channels/telegram.ts` is the whole
|
|
27
|
-
* glue; `agent` and the state root arrive via ctx, never through user code. Mounts `POST /telegram`
|
|
28
|
-
* (the path `--tunnel` webhook registration expects). The adapter owns that route key; to serve the
|
|
29
|
-
* SAME instance at a different path (e.g. behind a rewriting proxy), re-key the returned module:
|
|
30
|
-
* `(ctx) => ({ "POST /bot": telegramChannel(opts)(ctx)["POST /telegram"]! })`.
|
|
31
|
-
* This re-routes ONE instance — it is not a way to run two telegram bots in one workspace: the state
|
|
32
|
-
* home is derived from the channel kind (`<stateRoot>/channels/telegram`), so a second instance would
|
|
33
|
-
* share the first's turn-store/context-buffer. One telegram instance per workspace (single-process).
|
|
34
|
-
*/
|
|
20
|
+
/** Build a Telegram bot channel: policy options in, a {@link ChannelModule} out. */
|
|
35
21
|
export declare function telegramChannel({ secretToken, botToken, route, onError, botUsername, apiBaseUrl, }: TelegramChannelOptions): ChannelModule;
|
|
@@ -1,57 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram bot channel: verify the webhook secret token → decide via `route(update)` → run the turn →
|
|
3
|
-
*
|
|
4
|
-
* and posts the reply itself (chat UX), unlike the github channel's fire-and-forget. No SDK — inbound
|
|
5
|
-
* is a JSON POST, outbound is a `fetch` to the Bot API. The developer writes only `route` (policy); the
|
|
6
|
-
* channel owns transport + format + attachments.
|
|
7
|
-
*
|
|
8
|
-
* This file is the Telegram WIRING: ingress (secret/body cap/JSON) + the per-turn lifecycle + composition.
|
|
9
|
-
* Every other concern lives in its own module, each owning its invariants:
|
|
10
|
-
* - parse.ts pure message parsing: field extraction, prompt envelope, summon/route policy
|
|
11
|
-
* - invoke-turn.ts run one turn: assemble inputs (resolve attachments) + stream `agent.invoke`
|
|
12
|
-
* - kit/turn-runner.ts the durable-turn lifecycle (accept → dequeue → execute → end) over the queue + store
|
|
13
|
-
* - turn-store.ts durable turn intent (L1): pre-ACK persist, replay a crash-surviving turn
|
|
14
|
-
* - context-buffer.ts un-summoned group discussion, folded into the next answered turn
|
|
15
|
-
* - preview.ts the live-preview pump ("💭 Thinking…" → edits → final answer) + terminal writes
|
|
16
|
-
* - telegram-api.ts the single Bot API pipeline (timeouts, 429, ok-gating, HTML-aware split)
|
|
17
|
-
* - state.ts atomic state files under the channel-state home
|
|
18
|
-
*
|
|
19
|
-
* Threaded Mode (topics in private chats, a @BotFather toggle) is auto-adapted: an update carrying
|
|
20
|
-
* message_thread_id replies into that thread; without one the chat is linear. Same code, both modes.
|
|
21
|
-
*
|
|
22
|
-
* Authored against the public `@fastagent-sh/fastagent` surface only (the contract + the channel-authoring
|
|
23
|
-
* kit: readBodyCapped / text), so it is exactly what a third-party `fastagent-channel-*` package would write.
|
|
2
|
+
* Telegram bot channel: verify the webhook secret token → decide via `route(update)` → run the turn → stream the
|
|
3
|
+
* agent's reply back to the chat, ACK 200.
|
|
24
4
|
*/
|
|
25
5
|
import { isAbsolute, join } from "node:path";
|
|
26
6
|
import { log } from "../../log.js";
|
|
27
7
|
import { readBodyCapped } from "../body.js";
|
|
28
8
|
import { text } from "../respond.js";
|
|
29
9
|
import { secretEquals } from "../secret.js";
|
|
30
|
-
import {
|
|
10
|
+
import { telegramTurnStream } from "./invoke-turn.js";
|
|
31
11
|
import { collectAttachments, createContextBuffer } from "./context-buffer.js";
|
|
32
12
|
import { attachmentSummary, defaultTelegramRoute, extractFiles, extractImages, fromLabel, messageText, ownFiles, ownImages, pickMessage, telegramEnvelope, telegramStop, } from "./parse.js";
|
|
33
|
-
import { defaultErrorMessage,
|
|
13
|
+
import { defaultErrorMessage, deliverTelegramAnswer, telegramReply } from "./preview.js";
|
|
14
|
+
import { attachmentsDir } from "../kit/attachment-path.js";
|
|
34
15
|
import { ensureStateHome } from "../kit/state.js";
|
|
35
16
|
import { dispatchStop } from "../kit/stop-command.js";
|
|
36
17
|
import { callApi, editMessageText, sendMessage } from "./telegram-api.js";
|
|
37
18
|
import { createTurnRunner } from "../kit/turn-runner.js";
|
|
38
19
|
import { discussionBlock } from "../kit/context-buffer.js";
|
|
20
|
+
import { portJoin } from "../../effect-port.js";
|
|
39
21
|
import { createTurnStore } from "./turn-store.js";
|
|
40
22
|
// Re-export the public surface authored elsewhere, so `@fastagent-sh/fastagent/telegram` keeps one entry point.
|
|
41
23
|
export { defaultTelegramRoute, telegramEnvelope, telegramStop };
|
|
42
24
|
/** Update body cap — Telegram updates are small JSON; 1 MiB is generous and guards a public endpoint. */
|
|
43
25
|
const MAX_UPDATE_BYTES = 1 << 20;
|
|
44
|
-
/**
|
|
45
|
-
* Build a Telegram bot channel: policy options in, a {@link ChannelModule} out. The framework (or an
|
|
46
|
-
* embedder) mounts it with the context — `telegramChannel(opts)` in `channels/telegram.ts` is the whole
|
|
47
|
-
* glue; `agent` and the state root arrive via ctx, never through user code. Mounts `POST /telegram`
|
|
48
|
-
* (the path `--tunnel` webhook registration expects). The adapter owns that route key; to serve the
|
|
49
|
-
* SAME instance at a different path (e.g. behind a rewriting proxy), re-key the returned module:
|
|
50
|
-
* `(ctx) => ({ "POST /bot": telegramChannel(opts)(ctx)["POST /telegram"]! })`.
|
|
51
|
-
* This re-routes ONE instance — it is not a way to run two telegram bots in one workspace: the state
|
|
52
|
-
* home is derived from the channel kind (`<stateRoot>/channels/telegram`), so a second instance would
|
|
53
|
-
* share the first's turn-store/context-buffer. One telegram instance per workspace (single-process).
|
|
54
|
-
*/
|
|
26
|
+
/** Build a Telegram bot channel: policy options in, a {@link ChannelModule} out. */
|
|
55
27
|
export function telegramChannel({ secretToken, botToken, route, onError, botUsername, apiBaseUrl = "https://api.telegram.org", }) {
|
|
56
28
|
return ({ agent, stateRoot, control }) => {
|
|
57
29
|
// Validate at activation so deploy may inspect the module shape before secrets are provisioned.
|
|
@@ -62,10 +34,9 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
62
34
|
throw new Error("telegramChannel requires a non-empty botToken (used to send the agent's reply)");
|
|
63
35
|
}
|
|
64
36
|
const formatError = onError ?? defaultErrorMessage;
|
|
65
|
-
// One getMe at startup: the bot's @username (for the default route's group @mention summon and for
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
// warn if it is on.
|
|
37
|
+
// One getMe at startup: the bot's @username (for the default route's group @mention summon and for recognising an
|
|
38
|
+
// addressed `/stop`, only when not supplied) and the group-privacy flag — privacy mode off is required to receive
|
|
39
|
+
// the un-summoned group messages that feed the context buffer, so warn if it is on.
|
|
69
40
|
let mentionName = botUsername;
|
|
70
41
|
void callApi(apiBaseUrl, botToken, "getMe", {}).then((me) => {
|
|
71
42
|
if (mentionName === undefined)
|
|
@@ -75,40 +46,31 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
75
46
|
"context (un-summoned messages) won't be captured. Disable it via @BotFather → /setprivacy.");
|
|
76
47
|
}
|
|
77
48
|
},
|
|
78
|
-
// Name every capability the missing username costs, so "the bot ignores /stop@name" is
|
|
79
|
-
//
|
|
80
|
-
// single startup fact on every update.
|
|
49
|
+
// Name every capability the missing username costs, so "the bot ignores /stop@name" is diagnosable from this
|
|
50
|
+
// ONE line.
|
|
81
51
|
(e) => log.warn(`[telegram] getMe failed; @mention summon, addressed /stop, and the privacy check are skipped: ${String(e)}`));
|
|
82
|
-
// A bot token is "<bot_id>:<secret>" — the bot's own id is knowable synchronously, so reply-to-bot
|
|
83
|
-
//
|
|
84
|
-
// Every real token parses; one that doesn't (a mock/test token) degrades visibly: reply summon stays
|
|
85
|
-
// off (fail-closed in repliesToBot) until getMe supplies the username tier.
|
|
52
|
+
// A bot token is "<bot_id>:<secret>" — the bot's own id is knowable synchronously, so reply-to-bot targeting is
|
|
53
|
+
// precise from the first update (no getMe race; getMe only resolves the @username).
|
|
86
54
|
const tokenId = Number(botToken.split(":")[0]);
|
|
87
55
|
const botId = Number.isSafeInteger(tokenId) && tokenId > 0 ? tokenId : undefined;
|
|
88
56
|
if (botId === undefined) {
|
|
89
57
|
log.warn("[telegram] bot token has no parseable bot id — reply-to-bot summon disabled until getMe resolves");
|
|
90
58
|
}
|
|
91
59
|
const decide = route ?? ((update) => defaultTelegramRoute(update, { botUsername: mentionName, botId }));
|
|
92
|
-
// The channel-state convention: this channel's durable home is `<stateRoot>/channels/telegram`
|
|
93
|
-
//
|
|
94
|
-
// the operator's ONE state knob (FASTAGENT_STATE_DIR) can never be silently bypassed by glue.
|
|
95
|
-
// The ctx contract says stateRoot is absolute (loadChannels enforces it); re-assert for embedders
|
|
96
|
-
// that mount without the loader — a silent cwd re-anchor is the bug this contract exists to kill,
|
|
97
|
-
// and every derived path (incl. attachment paths) relies on DownloadedFile's absolute-path contract.
|
|
60
|
+
// The channel-state convention: this channel's durable home is `<stateRoot>/channels/telegram` (engine state at
|
|
61
|
+
// the root, channel state under `channels/<kind>/`).
|
|
98
62
|
if (!isAbsolute(stateRoot)) {
|
|
99
63
|
throw new Error(`telegramChannel requires an absolute ctx.stateRoot, got "${stateRoot}"`);
|
|
100
64
|
}
|
|
101
65
|
const stateHome = join(stateRoot, "channels", "telegram");
|
|
102
66
|
ensureStateHome(stateHome); // buffers/files may carry chat content; the agent .gitignore covers .state/
|
|
103
67
|
const buffer = createContextBuffer(join(stateHome, "buffers.json"));
|
|
104
|
-
// Durable turn intent (L1): persist an accepted turn pre-ACK, remove it when the turn ends; a crash
|
|
105
|
-
//
|
|
68
|
+
// Durable turn intent (L1): persist an accepted turn pre-ACK, remove it when the turn ends; a crash leaves it for
|
|
69
|
+
// replay on the next start.
|
|
106
70
|
const store = createTurnStore(join(stateHome, "turns.json"));
|
|
107
71
|
const targetOf = (r) => ({ chatId: r.chatId, threadId: r.threadId, replyTo: r.replyTo });
|
|
108
|
-
// Tell the asker when a turn is dropped at the execution ceiling: the chain's end needs a signal, not
|
|
109
|
-
//
|
|
110
|
-
// fresh) — leaving it pinned at "Queued" while sending a separate failure would double-post. Best-
|
|
111
|
-
// effort, like the queue notices.
|
|
72
|
+
// Tell the asker when a turn is dropped at the execution ceiling: the chain's end needs a signal, not just an
|
|
73
|
+
// operator log line.
|
|
112
74
|
const notifyDropped = (r) => {
|
|
113
75
|
const body = "⚠️ I couldn’t complete an earlier request — please ask again.";
|
|
114
76
|
const sent = r.previewId !== undefined
|
|
@@ -124,11 +86,8 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
124
86
|
fromStored: ({ attempts: _a, ...intent }) => ({ ...intent, previewId: undefined }),
|
|
125
87
|
bufferKey: (rec) => rec.placeKey,
|
|
126
88
|
where: (rec) => `chat=${rec.chatId}${rec.threadId !== undefined ? ` thread=${rec.threadId}` : ""}`,
|
|
127
|
-
// Queue feedback: when this session already has a turn running/queued, a silent wait reads as "the
|
|
128
|
-
//
|
|
129
|
-
// clear whose ask is queued). Best-effort and post-ACK: a failed notice is a log line, never a
|
|
130
|
-
// failed update. The turn's live preview then edits this same message in place. The runner holds
|
|
131
|
-
// this same `rec` object, so mutating it here (awaited at dequeue) hands the turn its preview id.
|
|
89
|
+
// Queue feedback: when this session already has a turn running/queued, a silent wait reads as "the bot ignored
|
|
90
|
+
// me" once the current turn runs long.
|
|
132
91
|
onQueuedBehind: (rec) => ({
|
|
133
92
|
done: sendMessage(apiBaseUrl, botToken, targetOf(rec), "⏳ Queued — I’ll start once the current task finishes.", {
|
|
134
93
|
html: false,
|
|
@@ -137,8 +96,7 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
137
96
|
rec.previewId = id;
|
|
138
97
|
}, (e) => log.warn(`[telegram] queue notice failed (the turn still runs): ${String(e)}`)),
|
|
139
98
|
}),
|
|
140
|
-
// Its ⏳ notice (if any) now falsely reads "Queued": delete it best-effort
|
|
141
|
-
// sends a fresh preview, and leaving it would orphan a stale message above that.
|
|
99
|
+
// Its ⏳ notice (if any) now falsely reads "Queued": delete it best-effort.
|
|
142
100
|
onDeferred: (rec) => {
|
|
143
101
|
if (rec.previewId !== undefined) {
|
|
144
102
|
void callApi(apiBaseUrl, botToken, "deleteMessage", {
|
|
@@ -148,13 +106,15 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
148
106
|
}
|
|
149
107
|
},
|
|
150
108
|
notifyDropped,
|
|
151
|
-
|
|
109
|
+
// `rec.previewId`: the ⏳ notice THIS process put up if the record waited behind another turn.
|
|
110
|
+
deliverAnswer: (rec, answer) => portJoin(() => deliverTelegramAnswer(apiBaseUrl, botToken, targetOf(rec), answer, rec.previewId)),
|
|
111
|
+
execute: (rec, discussion, onAnswered) => telegramReply(telegramTurnStream(agent, rec.session, `${discussionBlock(discussion.text)}${rec.baseText}`, { api: apiBaseUrl, botToken, chatId: rec.chatId, filesDir: attachmentsDir(stateHome) }, {
|
|
152
112
|
primary: { imageFileIds: rec.imageFileIds, fileIds: rec.fileIds },
|
|
153
113
|
buffered: collectAttachments(discussion.consumed, {
|
|
154
114
|
files: new Set(rec.fileIds),
|
|
155
115
|
images: new Set(rec.imageFileIds),
|
|
156
116
|
}),
|
|
157
|
-
}
|
|
117
|
+
}), apiBaseUrl, botToken, targetOf(rec), formatError, rec.previewId, onAnswered),
|
|
158
118
|
});
|
|
159
119
|
runner.recover();
|
|
160
120
|
const handler = async (req) => {
|
|
@@ -174,27 +134,23 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
174
134
|
catch {
|
|
175
135
|
return text("invalid json\n", 400);
|
|
176
136
|
}
|
|
177
|
-
// Decide whether/where to answer, then run the turn.
|
|
178
|
-
// webhook timeout); lifecycle goes to stderr — after the 200 there is no response body, so those
|
|
179
|
-
// lines are the operator's only signal.
|
|
137
|
+
// Decide whether/where to answer, then run the turn.
|
|
180
138
|
const m = pickMessage(update);
|
|
181
139
|
if (!m)
|
|
182
140
|
return new Response(null, { status: 200 });
|
|
183
141
|
const placeKey = m.message_thread_id ? `${m.chat.id}:${m.message_thread_id}` : `${m.chat.id}`;
|
|
184
142
|
const r = decide(update);
|
|
185
143
|
if (!r) {
|
|
186
|
-
// Not summoned: in a group, record the message so a later summon has the discussion (needs privacy
|
|
187
|
-
//
|
|
144
|
+
// Not summoned: in a group, record the message so a later summon has the discussion (needs privacy off to be
|
|
145
|
+
// delivered here at all).
|
|
188
146
|
const isGroup = m.chat.type === "group" || m.chat.type === "supergroup";
|
|
189
147
|
const content = messageText(m);
|
|
190
148
|
if (isGroup && content) {
|
|
191
|
-
// OWN attachments only: each message is its own buffer entry, so a reply's referenced
|
|
192
|
-
//
|
|
193
|
-
// downloads and squeeze the attachment cap.
|
|
149
|
+
// OWN attachments only: each message is its own buffer entry, so a reply's referenced attachment is already
|
|
150
|
+
// (or will be) the other entry's.
|
|
194
151
|
const fileIds = ownFiles(m);
|
|
195
152
|
const imageIds = ownImages(m);
|
|
196
|
-
// A captioned attachment renders as its caption
|
|
197
|
-
// ALWAYS labels attachments (that label + sender is all the attribution a photo gets).
|
|
153
|
+
// A captioned attachment renders as its caption.
|
|
198
154
|
const summary = attachmentSummary(m);
|
|
199
155
|
const bodyLine = summary && content !== summary ? `${content} ${summary}` : content;
|
|
200
156
|
buffer.push(placeKey, {
|
|
@@ -208,24 +164,14 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
208
164
|
}
|
|
209
165
|
return new Response(null, { status: 200 });
|
|
210
166
|
}
|
|
211
|
-
|
|
167
|
+
// Branded with the channel kind: session ids share one namespace across every channel of a deployment.
|
|
168
|
+
const session = r.session ?? `telegram:${placeKey}`;
|
|
212
169
|
const chatId = r.chatId ?? m.chat.id;
|
|
213
|
-
// Reply to the summoning message in groups (threads the answer under the asker); a 1:1 DM needs no
|
|
214
|
-
// reply-quote. Only when the RESOLVED target is the message's own chat+thread: a route that
|
|
215
|
-
// redirects elsewhere must not carry a reply_parameters that resolves in the wrong place (fail, or
|
|
216
|
-
// quote a same-id message there). Compare VALUES, not whether the route touched the field — a route
|
|
217
|
-
// that explicitly returns the same chat/thread still quotes.
|
|
170
|
+
// Reply to the summoning message in groups (threads the answer under the asker); a 1:1 DM needs no reply-quote.
|
|
218
171
|
const threadId = r.threadId ?? m.message_thread_id;
|
|
219
172
|
const sameTarget = String(chatId) === String(m.chat.id) && threadId === m.message_thread_id;
|
|
220
173
|
const replyTo = m.chat.type !== "private" && sameTarget ? m.message_id : undefined;
|
|
221
|
-
// Explicit user stop (`/stop`): a control action, never a turn — it must not queue behind the
|
|
222
|
-
// run it stops. Awaited before the ACK: dispatch + one sendMessage is fast, and a delivery failure
|
|
223
|
-
// logs instead of failing the webhook. Read AFTER the route, on the route's own session: the route
|
|
224
|
-
// is both the gate (a route that ignores this chat must not have the bot abort or answer in it) and
|
|
225
|
-
// the session authority (a route that remaps `session` would otherwise have its stop abort a
|
|
226
|
-
// session nobody runs). The default route summons an ADDRESSED `/stop` in a group for this reason:
|
|
227
|
-
// the general slash-command refusal would leave it buffered as discussion while the run kept going.
|
|
228
|
-
// An unaddressed one is not a command at all (see telegramStop) and never reaches here.
|
|
174
|
+
// Explicit user stop (`/stop`): a control action, never a turn — it must not queue behind the run it stops.
|
|
229
175
|
if (telegramStop(update, { botUsername: mentionName, botId })) {
|
|
230
176
|
const feedback = await dispatchStop(control, session, "[telegram]");
|
|
231
177
|
await sendMessage(apiBaseUrl, botToken, { chatId, threadId, replyTo }, feedback, { html: false }).catch((e) => log.warn(`[telegram] stop feedback failed: ${String(e)}`));
|
|
@@ -250,9 +196,7 @@ export function telegramChannel({ secretToken, botToken, route, onError, botUser
|
|
|
250
196
|
}
|
|
251
197
|
return new Response(null, { status: 200 });
|
|
252
198
|
};
|
|
253
|
-
// Test/observability seam: await the fire-and-forget turns this handler enqueues.
|
|
254
|
-
// (nothing reads it; the runtime never drains — see turn-queue), it lets a test await a turn
|
|
255
|
-
// deterministically instead of polling for side effects to settle.
|
|
199
|
+
// Test/observability seam: await the fire-and-forget turns this handler enqueues.
|
|
256
200
|
handler.turnsIdle = () => runner.idle();
|
|
257
201
|
return { "POST /telegram": handler };
|
|
258
202
|
};
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The telegram-shaped turn store: the channel's persisted record (what its runner needs to re-execute a
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The telegram-shaped turn store: the channel's persisted record (what its runner needs to re-execute a turn), its
|
|
3
|
+
* IO-boundary shape validator, and its arrival ordering, over the generic L1 turn store (../kit/turn-store.ts — the
|
|
4
|
+
* semantics live there: pre-ACK persist, replay on the next start, the poison-turn execution ceiling, the fail-closed
|
|
5
|
+
* attempt bump).
|
|
6
6
|
*/
|
|
7
|
-
import { type TurnStore } from "../kit/turn-store.ts";
|
|
8
|
-
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
export interface StoredTurn {
|
|
14
|
-
id: string;
|
|
15
|
-
session: string;
|
|
7
|
+
import { type TurnRecordBase, type TurnStore } from "../kit/turn-store.ts";
|
|
8
|
+
/**
|
|
9
|
+
* An accepted turn's persisted intent — the SOURCE for the fields a runner needs to re-execute it (telegram.ts's
|
|
10
|
+
* PendingTurn derives from this, so a new execution field added here propagates and cannot silently drop from the
|
|
11
|
+
* persisted record).
|
|
12
|
+
*/
|
|
13
|
+
export interface StoredTurn extends TurnRecordBase {
|
|
16
14
|
placeKey: string;
|
|
17
15
|
baseText: string;
|
|
18
16
|
chatId: number | string;
|
|
@@ -20,6 +18,5 @@ export interface StoredTurn {
|
|
|
20
18
|
replyTo?: number;
|
|
21
19
|
imageFileIds: string[];
|
|
22
20
|
fileIds: string[];
|
|
23
|
-
attempts: number;
|
|
24
21
|
}
|
|
25
22
|
export declare function createTurnStore(path: string): TurnStore<StoredTurn>;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The telegram-shaped turn store: the channel's persisted record (what its runner needs to re-execute a
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The telegram-shaped turn store: the channel's persisted record (what its runner needs to re-execute a turn), its
|
|
3
|
+
* IO-boundary shape validator, and its arrival ordering, over the generic L1 turn store (../kit/turn-store.ts — the
|
|
4
|
+
* semantics live there: pre-ACK persist, replay on the next start, the poison-turn execution ceiling, the fail-closed
|
|
5
|
+
* attempt bump).
|
|
6
6
|
*/
|
|
7
7
|
import { createTurnStore as createGenericTurnStore } from "../kit/turn-store.js";
|
|
8
|
-
/**
|
|
9
|
-
*
|
|
8
|
+
/**
|
|
9
|
+
* State files are an IO boundary: valid JSON of the WRONG SHAPE must degrade like a corrupt file (warn + empty), not
|
|
10
|
+
* flow in as trusted data (mirrors context-buffer's isBufferEntry).
|
|
11
|
+
*/
|
|
10
12
|
function isStoredTurn(t) {
|
|
11
13
|
const r = t;
|
|
12
14
|
const strings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*/
|
|
6
|
-
export declare function waitForHealth(healthUrl: string, timeoutMs: number, intervalMs: number): Promise<boolean>;
|
|
1
|
+
/** Poll `healthUrl` until it responds 200, or the timeout elapses. */
|
|
2
|
+
export declare function waitForHealth(healthUrl: string, timeoutMs: number, intervalMs: number,
|
|
3
|
+
/** Optional liveness answer from whoever runs the server. */
|
|
4
|
+
stillStarting?: () => Promise<boolean>): Promise<boolean>;
|