| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 3 | 9 | 0% |
| Commands | 0 | 0 | 0 | — |
| Section tags | 1 | 2 | 4 | 14% |
What each file covers
Sections
0 shared · 3 only in A · 9 only in B- − Shared Test Helper Boundary
- − Bundled Plugin Imports
- − Intent
- + 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
Commands
neither file has anySection tags
1 shared · 2 only in A · 4 only in B- − build
- − test
- + git-pr
- + security
- + api
- + do-not
- code-style
Line diff
openclaw/openclaw · test/helpers/AGENTS.md
@@ −1 @@
1# Shared Test Helper Boundary
2
3This directory holds shared test helpers reused by core and bundled plugin
4tests.
5
6## Bundled Plugin Imports
7
8- Shared helpers in this tree must not hardcode repo-relative imports into
9 `extensions/**`.
10- When a helper needs a bundled plugin public surface, go through
11 `src/test-utils/bundled-plugin-public-surface.ts`.
12- Prefer `loadBundledPluginApiSync(...)`,
13 `loadBundledPluginRuntimeApiSync(...)`,
14 `loadBundledPluginContractApiSync(...)`, and
15 `loadBundledPluginTestApiSync(...)` for eager access to exported surfaces.
16- Prefer `resolveRelativeBundledPluginPublicModuleId(...)` or
17 `resolveBundledPluginPublicModulePath(...)` when a helper needs a module id
18 or filesystem path for dynamic import, mocking, or loading a plugin entrypoint
19 such as `index.js`.
20- If `vi.hoisted(...)` is involved, do not call imported helper functions from
21 inside the hoisted callback. Resolve the module id outside the callback or
22 switch to `vi.doMock(...)`.
23- Do not keep plugin-local deep mocks or private `src/**` knowledge in shared
24 helpers. Move those helpers into the owning bundled plugin package instead.
25
26## Intent
27
28- Keep shared helpers aligned with the same public/plugin boundary that
29 production code uses.
30- Avoid shared helper debt that makes core test lanes depend on bundled plugin
31 private layout.
32
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
@@ −1 +1 @@
1−# Shared Test Helper Boundary
1+# Telegram Plugin Guide
22
3−This directory holds shared test helpers reused by core and bundled plugin
4−tests.
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.
57
6−## Bundled Plugin Imports
8+Verified against Telegram Bot API 10.2, July 14 2026.
79
8−- Shared helpers in this tree must not hardcode repo-relative imports into
9− `extensions/**`.
10−- When a helper needs a bundled plugin public surface, go through
11− `src/test-utils/bundled-plugin-public-surface.ts`.
12−- Prefer `loadBundledPluginApiSync(...)`,
13− `loadBundledPluginRuntimeApiSync(...)`,
14− `loadBundledPluginContractApiSync(...)`, and
15− `loadBundledPluginTestApiSync(...)` for eager access to exported surfaces.
16−- Prefer `resolveRelativeBundledPluginPublicModuleId(...)` or
17− `resolveBundledPluginPublicModulePath(...)` when a helper needs a module id
18− or filesystem path for dynamic import, mocking, or loading a plugin entrypoint
19− such as `index.js`.
20−- If `vi.hoisted(...)` is involved, do not call imported helper functions from
21− inside the hoisted callback. Resolve the module id outside the callback or
22− switch to `vi.doMock(...)`.
23−- Do not keep plugin-local deep mocks or private `src/**` knowledge in shared
24− helpers. Move those helpers into the owning bundled plugin package instead.
10+## Reliability Invariants
2511
26−## Intent
12+### Core drain contracts (do not re-implement in Telegram)
2713
28−- Keep shared helpers aligned with the same public/plugin boundary that
29− production code uses.
30−- Avoid shared helper debt that makes core test lanes depend on bundled plugin
31− private layout.
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`.
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.
32137
