| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 9 | 3 | 0% |
| Commands | 0 | 0 | 2 | 0% |
| Section tags | 0 | 5 | 3 | 0% |
What each file covers
Sections
0 shared · 9 only in A · 3 only in B- − Telegram Plugin Guide
- − Reliability Invariants
- − Core drain contracts (do not re-implement in Telegram)
- − Telegram-owned (transport + channel policy)
- − Streaming
- − Telegram API Ownership
- − Context And Authorization
- − Interactive Surfaces
- − Review Standard
- + Agent Tools Performance
- + Guardrails
- + Verification
Commands
0 shared · 0 only in A · 2 only in B- + pnpm test <file>
- + pnpm build
Section tags
0 shared · 5 only in A · 3 only in B- − code-style
- − git-pr
- − security
- − api
- − do-not
- + test
- + performance
- + agent-behaviour
Line diff
openclaw/openclaw · extensions/telegram/AGENTS.md
@@ −1 @@
1# Telegram Plugin Guide
2
3Read this before any change under `extensions/telegram/`. These are intentional
4maintainer decisions and review-binding invariants, not incidental
5implementation details. Also read `extensions/AGENTS.md` for the plugin
6boundary rules.
7
8Verified against Telegram Bot API 10.2, July 14 2026.
9
10## Reliability Invariants
11
12### Core drain contracts (do not re-implement in Telegram)
13
14Owned by `src/channels/message/ingress-drain.ts` (+ claim-owner, retry-policy).
15Proof: `src/channels/message/ingress-drain.test.ts`,
16`ingress-claim-owner.test.ts`, `ingress-retry-policy.test.ts`.
17
18- Completed rows tombstone via `complete()`, never `delete`.
19- Complete at turn adoption, not settle. Deferred holds the claim; watchdog
20 stays armed through deferral; dead-letter reason `handler-timeout`.
21- One retry policy: attempt floor **and** age gate (defaults 8 / 24h).
22- Claim refresh heartbeat while dispatching/deferred (`claimLeaseMs / 3`).
23- Never silently complete on transient failure — release/fail via disposition.
24- Pre-adoption supersede tombstones; post-adoption interruption is core-owned
25 (reply-run registry / queue interrupt).
26
27### Telegram-owned (transport + channel policy)
28
29- Durable-before-ack on both transports. Polling: ingress worker advances its
30 offset only after the parent's committed spool enqueue
31 (`writeTelegramSpooledUpdate`). Webhook: respond 200 only after the spool
32 write; non-200 on write failure is the redelivery contract.
33- `update_id`↔event-id encoding and per-chat/topic lane derivation stay in
34 `telegram-ingress-spool.ts` / sequential-key.
35- Polling and webhook both: enqueue then pump
36 `createTelegramTransportIngressDrain(...).drainOnce()` — no private claim
37 loops.
38- Stall timeout: `OPENCLAW_TELEGRAM_SPOOLED_HANDLER_TIMEOUT_MS` →
39 `adoptionStallTimeoutMs` (default 5 min) via
40 `resolveTelegramAdoptionStallTimeoutMs`.
41- Non-retryable classifier: `telegram-ingress-non-retryable.ts`
42 (missing harness, dispatch-dedupe rollback).
43- Supersede predicate: `telegram-ingress-supersede.ts` — only abort text /
44 authorized-looking explicit commands (and ambient room_event pending) may
45 supersede pre-adoption work. Normal messages never supersede.
46- room_event ambient work shares the sequential lane so a later user turn can
47 supersede it pre-adoption; adopted user turns are never touched (core drain
48 supersede is pre-adoption only).
49- No per-message full-store writes. Hot-path SQLite writes are per-entry.
50 Rewriting a cache on every send or read stalls the event loop, and that
51 stall masquerades as a polling stall (the sent-message-cache regression).
52- Transport error classification. The getUpdates worker retries Bot API 5xx
53 and 429 locally, honoring `parameters.retry_after`; 401/404 stay fatal; 409
54 must propagate to the parent session, which owns webhook-conflict recovery.
55 Bot API errors carry `error_code`, not `.code`; parse non-2xx bodies
56 defensively (a 502 HTML page is not JSON).
57- Send funnel parity. The durable funnel (`send.ts`) and the streaming funnel
58 (`bot/delivery.*`) must degrade identically: rich-entity 400 falls back to
59 plain text, caption parse 400 falls back to a plain caption, quote-not-found
60 400 falls back to a legacy reply. New recoveries go into the shared
61 predicates (`send-error-predicates.ts`, `reply-parameters.ts`), never into
62 one funnel only.
63- Outbound flood waits honor `retry_after` up to
64 `TELEGRAM_OUTBOUND_RETRY_AFTER_CAP_MS`; do not re-clamp Telegram sends to the
65 generic channel retry ceiling.
66- Webhook security ordering. The secret header is validated first
67 (constant-time compare, single-header enforcement, connection close on 401);
68 the request rate limit budgets only failed-auth attempts so Telegram's own
69 delivery is never throttled.
70- Every owned undici transport gets closed on all exit paths: polling session,
71 webhook shutdown and startup failure, probe-cache eviction.
72
73## Streaming
74
75- Do not reintroduce `sendMessageDraft` for answer streaming. Telegram drafts
76 are ephemeral 30-second previews in private chats; final delivery still
77 requires a separate `sendMessage`. OpenClaw uses `sendMessage` plus
78 `editMessageText`, then finalizes in place so the user sees one persistent
79 answer.
80- Streaming owns one visible preview message. Edit it forward. Do not send an
81 extra final bubble unless the final edit genuinely failed.
82- Keep the first-preview debounce. If a provider sends token-sized deltas,
83 coalesce them into cumulative preview text instead of removing the debounce.
84- Respect Telegram limits in the Telegram layer. Text over 4096 chars chains
85 into continuation messages. Polls keep the current Bot API 12-option cap.
86
87## Telegram API Ownership
88
89- Prefer grammY primitives and Telegram-native helpers when they model the
90 behavior directly. Avoid custom Bot API wrappers for behavior grammY already
91 owns.
92- Throttling is bot-token scoped. All Telegram API clients for the same token
93 share one grammY `apiThrottler()` instance.
94- Do not silently retry failed topic sends without topic metadata. A
95 wrong-surface success is worse than a loud Telegram error.
96- DM topics and forum topics are distinct. `direct_messages_topic_id` and
97 `message_thread_id` are not interchangeable.
98
99## Context And Authorization
100
101- Reply context comes from OpenClaw-observed messages. Bot API updates expose
102 `reply_to_message`, but there is no arbitrary `getMessage(chat, id)`
103 hydration path later.
104- Current local chat context must outrank stale reply ancestry in the prompt.
105 Old replied-to messages should not look like the active conversation.
106- The group history window is always on for groups and bounded by
107 `historyLimit`. Do not reintroduce prompt-history gating modes; that
108 regression blinded ambient rooms.
109- The group history window is rolling. Use self-entry watermark selection for
110 "since your last reply" views; do not reintroduce destructive clears because
111 room events are not persisted to the session and cleared context is
112 unrecoverable.
113- Pairing is DM-only. Group and topic authorization need explicit config
114 allowlists.
115- Telegram allowlists use numeric sender IDs. Usernames are optional, mutable,
116 and not a reliable arbitrary-user lookup key in the Bot API.
117- Group and channel visible replies are policy-controlled. Normal room replies
118 stay private unless `messages.groupChat.visibleReplies: "automatic"` is set
119 or the agent explicitly calls `message.send`.
120
121## Interactive Surfaces
122
123- Native callbacks stay structured. Approval, native command, plugin, select,
124 and multiselect callbacks must not fall through as raw callback text.
125- Preserve callback values exactly, including delimiters such as `env|prod`.
126- Native slash commands should remain fast-pathable before full workspace and
127 agent-turn setup.
128
129## Review Standard
130
131- Telegram behavior PRs need real Telegram proof when they touch transport,
132 streaming, topics, callbacks, authorization, or reply context. Prefer the
133 bot-to-bot QA lane or an equivalent live Telegram probe over synthetic-only
134 validation.
135- Reliability PRs (spool, drain, retry, ack, offset paths) need crash-window
136 or restart-replay test proof, not just happy-path tests.
137
openclaw/openclaw · src/agents/tools/AGENTS.md
@@ +1 @@
1<!-- Agent tool test performance notes for avoiding heavyweight plugin/channel runtime loads. -->
2
3# Agent Tools Performance
4
5Tool tests should not load full channel or plugin runtimes for static tool
6descriptions.
7
8## Guardrails
9
10- Message-tool discovery should flow through shared discovery helpers and
11 lightweight channel artifacts before falling back to a full channel plugin
12 load.
13- Channel-specific tool schemas, action lists, and static capabilities belong
14 in plugin-owned helpers that are reused by both the full plugin and the
15 lightweight artifact.
16- Do not add direct bundled-plugin imports to agent tool tests for schema or
17 capability assertions. If the production path needs the same data, promote a
18 small public artifact instead.
19- If a single assertion starts paying multi-second import/setup cost, split the
20 static descriptor path from runtime execution instead of adding more mocks
21 around the broad import.
22
23## Verification
24
25- For `src/agents/tools/*.test.ts` performance work, compare targeted file
26 runtime with `pnpm test <file>` before/after.
27- Run `pnpm build` when adding or changing bundled plugin artifacts.
28
@@ −1 +1 @@
1−# Telegram Plugin Guide
1+<!-- Agent tool test performance notes for avoiding heavyweight plugin/channel runtime loads. -->
22
3−Read this before any change under `extensions/telegram/`. These are intentional
4−maintainer decisions and review-binding invariants, not incidental
5−implementation details. Also read `extensions/AGENTS.md` for the plugin
6−boundary rules.
3+# Agent Tools Performance
74
8−Verified against Telegram Bot API 10.2, July 14 2026.
5+Tool tests should not load full channel or plugin runtimes for static tool
6+descriptions.
97
10−## Reliability Invariants
8+## Guardrails
119
12−### Core drain contracts (do not re-implement in Telegram)
10+- Message-tool discovery should flow through shared discovery helpers and
11+ lightweight channel artifacts before falling back to a full channel plugin
12+ load.
13+- Channel-specific tool schemas, action lists, and static capabilities belong
14+ in plugin-owned helpers that are reused by both the full plugin and the
15+ lightweight artifact.
16+- Do not add direct bundled-plugin imports to agent tool tests for schema or
17+ capability assertions. If the production path needs the same data, promote a
18+ small public artifact instead.
19+- If a single assertion starts paying multi-second import/setup cost, split the
20+ static descriptor path from runtime execution instead of adding more mocks
21+ around the broad import.
1322
14−Owned by `src/channels/message/ingress-drain.ts` (+ claim-owner, retry-policy).
15−Proof: `src/channels/message/ingress-drain.test.ts`,
16−`ingress-claim-owner.test.ts`, `ingress-retry-policy.test.ts`.
23+## Verification
1724
18−- Completed rows tombstone via `complete()`, never `delete`.
19−- Complete at turn adoption, not settle. Deferred holds the claim; watchdog
20− stays armed through deferral; dead-letter reason `handler-timeout`.
21−- One retry policy: attempt floor **and** age gate (defaults 8 / 24h).
22−- Claim refresh heartbeat while dispatching/deferred (`claimLeaseMs / 3`).
23−- Never silently complete on transient failure — release/fail via disposition.
24−- Pre-adoption supersede tombstones; post-adoption interruption is core-owned
25− (reply-run registry / queue interrupt).
26−
27−### Telegram-owned (transport + channel policy)
28−
29−- Durable-before-ack on both transports. Polling: ingress worker advances its
30− offset only after the parent's committed spool enqueue
31− (`writeTelegramSpooledUpdate`). Webhook: respond 200 only after the spool
32− write; non-200 on write failure is the redelivery contract.
33−- `update_id`↔event-id encoding and per-chat/topic lane derivation stay in
34− `telegram-ingress-spool.ts` / sequential-key.
35−- Polling and webhook both: enqueue then pump
36− `createTelegramTransportIngressDrain(...).drainOnce()` — no private claim
37− loops.
38−- Stall timeout: `OPENCLAW_TELEGRAM_SPOOLED_HANDLER_TIMEOUT_MS` →
39− `adoptionStallTimeoutMs` (default 5 min) via
40− `resolveTelegramAdoptionStallTimeoutMs`.
41−- Non-retryable classifier: `telegram-ingress-non-retryable.ts`
42− (missing harness, dispatch-dedupe rollback).
43−- Supersede predicate: `telegram-ingress-supersede.ts` — only abort text /
44− authorized-looking explicit commands (and ambient room_event pending) may
45− supersede pre-adoption work. Normal messages never supersede.
46−- room_event ambient work shares the sequential lane so a later user turn can
47− supersede it pre-adoption; adopted user turns are never touched (core drain
48− supersede is pre-adoption only).
49−- No per-message full-store writes. Hot-path SQLite writes are per-entry.
50− Rewriting a cache on every send or read stalls the event loop, and that
51− stall masquerades as a polling stall (the sent-message-cache regression).
52−- Transport error classification. The getUpdates worker retries Bot API 5xx
53− and 429 locally, honoring `parameters.retry_after`; 401/404 stay fatal; 409
54− must propagate to the parent session, which owns webhook-conflict recovery.
55− Bot API errors carry `error_code`, not `.code`; parse non-2xx bodies
56− defensively (a 502 HTML page is not JSON).
57−- Send funnel parity. The durable funnel (`send.ts`) and the streaming funnel
58− (`bot/delivery.*`) must degrade identically: rich-entity 400 falls back to
59− plain text, caption parse 400 falls back to a plain caption, quote-not-found
60− 400 falls back to a legacy reply. New recoveries go into the shared
61− predicates (`send-error-predicates.ts`, `reply-parameters.ts`), never into
62− one funnel only.
63−- Outbound flood waits honor `retry_after` up to
64− `TELEGRAM_OUTBOUND_RETRY_AFTER_CAP_MS`; do not re-clamp Telegram sends to the
65− generic channel retry ceiling.
66−- Webhook security ordering. The secret header is validated first
67− (constant-time compare, single-header enforcement, connection close on 401);
68− the request rate limit budgets only failed-auth attempts so Telegram's own
69− delivery is never throttled.
70−- Every owned undici transport gets closed on all exit paths: polling session,
71− webhook shutdown and startup failure, probe-cache eviction.
72−
73−## Streaming
74−
75−- Do not reintroduce `sendMessageDraft` for answer streaming. Telegram drafts
76− are ephemeral 30-second previews in private chats; final delivery still
77− requires a separate `sendMessage`. OpenClaw uses `sendMessage` plus
78− `editMessageText`, then finalizes in place so the user sees one persistent
79− answer.
80−- Streaming owns one visible preview message. Edit it forward. Do not send an
81− extra final bubble unless the final edit genuinely failed.
82−- Keep the first-preview debounce. If a provider sends token-sized deltas,
83− coalesce them into cumulative preview text instead of removing the debounce.
84−- Respect Telegram limits in the Telegram layer. Text over 4096 chars chains
85− into continuation messages. Polls keep the current Bot API 12-option cap.
86−
87−## Telegram API Ownership
88−
89−- Prefer grammY primitives and Telegram-native helpers when they model the
90− behavior directly. Avoid custom Bot API wrappers for behavior grammY already
91− owns.
92−- Throttling is bot-token scoped. All Telegram API clients for the same token
93− share one grammY `apiThrottler()` instance.
94−- Do not silently retry failed topic sends without topic metadata. A
95− wrong-surface success is worse than a loud Telegram error.
96−- DM topics and forum topics are distinct. `direct_messages_topic_id` and
97− `message_thread_id` are not interchangeable.
98−
99−## Context And Authorization
100−
101−- Reply context comes from OpenClaw-observed messages. Bot API updates expose
102− `reply_to_message`, but there is no arbitrary `getMessage(chat, id)`
103− hydration path later.
104−- Current local chat context must outrank stale reply ancestry in the prompt.
105− Old replied-to messages should not look like the active conversation.
106−- The group history window is always on for groups and bounded by
107− `historyLimit`. Do not reintroduce prompt-history gating modes; that
108− regression blinded ambient rooms.
109−- The group history window is rolling. Use self-entry watermark selection for
110− "since your last reply" views; do not reintroduce destructive clears because
111− room events are not persisted to the session and cleared context is
112− unrecoverable.
113−- Pairing is DM-only. Group and topic authorization need explicit config
114− allowlists.
115−- Telegram allowlists use numeric sender IDs. Usernames are optional, mutable,
116− and not a reliable arbitrary-user lookup key in the Bot API.
117−- Group and channel visible replies are policy-controlled. Normal room replies
118− stay private unless `messages.groupChat.visibleReplies: "automatic"` is set
119− or the agent explicitly calls `message.send`.
120−
121−## Interactive Surfaces
122−
123−- Native callbacks stay structured. Approval, native command, plugin, select,
124− and multiselect callbacks must not fall through as raw callback text.
125−- Preserve callback values exactly, including delimiters such as `env|prod`.
126−- Native slash commands should remain fast-pathable before full workspace and
127− agent-turn setup.
128−
129−## Review Standard
130−
131−- Telegram behavior PRs need real Telegram proof when they touch transport,
132− streaming, topics, callbacks, authorization, or reply context. Prefer the
133− bot-to-bot QA lane or an equivalent live Telegram probe over synthetic-only
134− validation.
135−- Reliability PRs (spool, drain, retry, ack, offset paths) need crash-window
136− or restart-replay test proof, not just happy-path tests.
25+- For `src/agents/tools/*.test.ts` performance work, compare targeted file
26+ runtime with `pnpm test <file>` before/after.
27+- Run `pnpm build` when adding or changing bundled plugin artifacts.
13728
