@pedrohnas/opencode-telegram 1.2.0 → 1.3.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.
Files changed (53) hide show
  1. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-41-55-194Z.yml +36 -0
  2. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-42-17-115Z.yml +36 -0
  3. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-43-15-988Z.yml +26 -0
  4. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-43-26-107Z.yml +26 -0
  5. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-45-03-139Z.yml +29 -0
  6. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-45-21-579Z.yml +29 -0
  7. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-45-48-051Z.yml +30 -0
  8. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-46-27-632Z.yml +33 -0
  9. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-46-46-519Z.yml +33 -0
  10. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-47-28-491Z.yml +349 -0
  11. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-47-34-834Z.yml +349 -0
  12. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-47-54-066Z.yml +168 -0
  13. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-48-19-667Z.yml +219 -0
  14. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-49-32-311Z.yml +221 -0
  15. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-49-57-109Z.yml +230 -0
  16. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-50-24-052Z.yml +235 -0
  17. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-50-41-148Z.yml +248 -0
  18. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-51-10-916Z.yml +234 -0
  19. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-51-28-271Z.yml +234 -0
  20. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-52-32-324Z.yml +234 -0
  21. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-52-47-801Z.yml +196 -0
  22. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-56-07-361Z.yml +203 -0
  23. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-56-35-534Z.yml +49 -0
  24. package/.claude/skills/playwright-cli/data/page-2026-02-09T01-57-04-658Z.yml +52 -0
  25. package/docs/AUDIT.md +193 -0
  26. package/docs/PROGRESS.md +188 -0
  27. package/docs/plans/phase-5.md +410 -0
  28. package/docs/plans/phase-6.5.md +426 -0
  29. package/docs/plans/phase-6.md +349 -0
  30. package/e2e/helpers.ts +34 -0
  31. package/e2e/phase-5.test.ts +295 -0
  32. package/e2e/phase-6.5.test.ts +239 -0
  33. package/e2e/phase-6.test.ts +302 -0
  34. package/package.json +6 -3
  35. package/src/api-server.test.ts +309 -0
  36. package/src/api-server.ts +201 -0
  37. package/src/bot.test.ts +354 -0
  38. package/src/bot.ts +200 -2
  39. package/src/config.test.ts +16 -0
  40. package/src/config.ts +4 -0
  41. package/src/event-bus.test.ts +337 -1
  42. package/src/event-bus.ts +83 -3
  43. package/src/handlers/agents.test.ts +122 -0
  44. package/src/handlers/agents.ts +93 -0
  45. package/src/handlers/media.test.ts +264 -0
  46. package/src/handlers/media.ts +168 -0
  47. package/src/handlers/models.test.ts +319 -0
  48. package/src/handlers/models.ts +191 -0
  49. package/src/index.ts +15 -0
  50. package/src/send/draft-stream.test.ts +76 -0
  51. package/src/send/draft-stream.ts +13 -1
  52. package/src/session-manager.test.ts +46 -0
  53. package/src/session-manager.ts +10 -1
@@ -0,0 +1,49 @@
1
+ - generic [ref=e3]:
2
+ - region "Site notifications" [ref=e4]:
3
+ - generic [ref=e8]:
4
+ - generic [ref=e9]: ⚠️
5
+ - alert [ref=e11]:
6
+ - text: "Security Update: Classic tokens have been revoked. Granular tokens are now limited to 90 days and require 2FA by default. Update your CI/CD workflows to avoid disruption."
7
+ - link "Learn more about npm authentication changes" [ref=e12] [cursor=pointer]:
8
+ - /url: https://gh.io/all-npm-classic-tokens-revoked
9
+ - text: Learn more
10
+ - text: .
11
+ - button "Close notification" [ref=e13] [cursor=pointer]: ×
12
+ - generic [ref=e14]:
13
+ - banner [ref=e15]:
14
+ - link "Npm" [ref=e18] [cursor=pointer]:
15
+ - /url: /
16
+ - img [ref=e19]
17
+ - main [ref=e21]:
18
+ - generic [ref=e22]:
19
+ - heading "Manage Recovery Codes" [level=1] [ref=e23]
20
+ - generic [ref=e25]:
21
+ - heading "Manage Recovery Codes" [level=2] [ref=e26]
22
+ - generic [ref=e27]:
23
+ - generic [ref=e28]:
24
+ - img [ref=e30]
25
+ - heading "Recovery Codes" [level=3] [ref=e36]
26
+ - paragraph [ref=e37]: Please make sure you have saved your codes in a secure place.
27
+ - generic [ref=e38]:
28
+ - button "65f3c8c151b712df1aafdbd003dd13eadb9c6ee9d687c2e3013d8f6add1fc36a 05a76c8c99cb696bd35b7caac484098cb19c653ea310483d6c695de277461398 4c70ca25505284883c2e225357d90fd9fb55ad56eae75a81bd1bb5486f7cb315 a6278db03b5e5f380f865550d119f6235517eced0e5bee47b25c603b4c23ad1c e614086e2300d1679d3e7dea55ab11d4bbe8d1a2843a1a0eefd28a62bf4dbd1e" [ref=e39]:
29
+ - paragraph [ref=e40]: 65f3c8c151b712df1aafdbd003dd13eadb9c6ee9d687c2e3013d8f6add1fc36a
30
+ - paragraph [ref=e41]: 05a76c8c99cb696bd35b7caac484098cb19c653ea310483d6c695de277461398
31
+ - paragraph [ref=e42]: 4c70ca25505284883c2e225357d90fd9fb55ad56eae75a81bd1bb5486f7cb315
32
+ - paragraph [ref=e43]: a6278db03b5e5f380f865550d119f6235517eced0e5bee47b25c603b4c23ad1c
33
+ - paragraph [ref=e44]: e614086e2300d1679d3e7dea55ab11d4bbe8d1a2843a1a0eefd28a62bf4dbd1e
34
+ - generic [ref=e46]:
35
+ - button "Copy" [ref=e47]:
36
+ - img [ref=e48]
37
+ - text: Copy
38
+ - button "Download" [ref=e50] [cursor=pointer]:
39
+ - img [ref=e51]
40
+ - text: Download
41
+ - button "Print" [ref=e55]:
42
+ - img [ref=e56]
43
+ - text: Print
44
+ - generic [ref=e58]:
45
+ - heading "Generate New Recovery Codes" [level=3] [ref=e59]
46
+ - paragraph [ref=e60]: When you generate new recovery codes, you must download or print the new codes. Your old codes won’t work anymore.
47
+ - button "Generate new recovery codes" [ref=e62]
48
+ - link "Go back to settings" [ref=e64] [cursor=pointer]:
49
+ - /url: /settings/pedrohnas/tfa/list
@@ -0,0 +1,52 @@
1
+ - generic [ref=e3]:
2
+ - region "Site notifications" [ref=e4]:
3
+ - generic [ref=e8]:
4
+ - generic [ref=e9]: ⚠️
5
+ - alert [ref=e11]:
6
+ - text: "Security Update: Classic tokens have been revoked. Granular tokens are now limited to 90 days and require 2FA by default. Update your CI/CD workflows to avoid disruption."
7
+ - link "Learn more about npm authentication changes" [ref=e12] [cursor=pointer]:
8
+ - /url: https://gh.io/all-npm-classic-tokens-revoked
9
+ - text: Learn more
10
+ - text: .
11
+ - button "Close notification" [ref=e13] [cursor=pointer]: ×
12
+ - generic [ref=e16]:
13
+ - alert [ref=e18]: Successfully generated new recovery codes.
14
+ - button "Close notification" [active] [ref=e19] [cursor=pointer]: ×
15
+ - generic [ref=e20]:
16
+ - banner [ref=e21]:
17
+ - link "Npm" [ref=e24] [cursor=pointer]:
18
+ - /url: /
19
+ - img [ref=e25]
20
+ - main [ref=e27]:
21
+ - generic [ref=e28]:
22
+ - heading "Manage Recovery Codes" [level=1] [ref=e29]
23
+ - generic [ref=e31]:
24
+ - heading "Manage Recovery Codes" [level=2] [ref=e32]
25
+ - generic [ref=e33]:
26
+ - generic [ref=e34]:
27
+ - img [ref=e36]
28
+ - heading "Recovery Codes" [level=3] [ref=e42]
29
+ - paragraph [ref=e43]: Please make sure you have saved your codes in a secure place.
30
+ - generic [ref=e44]:
31
+ - button "578abdc2cb48fb0b41b1412a2b02024955524ec30cb2d04e810da940dd264567 fcaae8cf53d18eb3d551e9b1c94a59eb9d0ff92375aacbc12f9761d34b7e74c1 996ce0478fa870cf6eec988e5a5e53b432f10fd1c2b6c3f430edc9f1a6b96905 3685bbc53649f71a076068bab844c9d9702d17d00858ccefdbd5bfefcc35aa3c 35c5f6a1fd6cefa84d2e6c4f399a5a821c4764ae46fd3b4f88a1523b463d2966" [ref=e45]:
32
+ - paragraph [ref=e46]: 578abdc2cb48fb0b41b1412a2b02024955524ec30cb2d04e810da940dd264567
33
+ - paragraph [ref=e47]: fcaae8cf53d18eb3d551e9b1c94a59eb9d0ff92375aacbc12f9761d34b7e74c1
34
+ - paragraph [ref=e48]: 996ce0478fa870cf6eec988e5a5e53b432f10fd1c2b6c3f430edc9f1a6b96905
35
+ - paragraph [ref=e49]: 3685bbc53649f71a076068bab844c9d9702d17d00858ccefdbd5bfefcc35aa3c
36
+ - paragraph [ref=e50]: 35c5f6a1fd6cefa84d2e6c4f399a5a821c4764ae46fd3b4f88a1523b463d2966
37
+ - generic [ref=e52]:
38
+ - button "Copy" [ref=e53]:
39
+ - img [ref=e54]
40
+ - text: Copy
41
+ - button "Download" [ref=e56] [cursor=pointer]:
42
+ - img [ref=e57]
43
+ - text: Download
44
+ - button "Print" [ref=e61]:
45
+ - img [ref=e62]
46
+ - text: Print
47
+ - generic [ref=e64]:
48
+ - heading "Generate New Recovery Codes" [level=3] [ref=e65]
49
+ - paragraph [ref=e66]: When you generate new recovery codes, you must download or print the new codes. Your old codes won’t work anymore.
50
+ - button "Generate new recovery codes" [ref=e68]
51
+ - link "Go back to settings" [ref=e70] [cursor=pointer]:
52
+ - /url: /settings/pedrohnas/tfa/list
package/docs/AUDIT.md ADDED
@@ -0,0 +1,193 @@
1
+ # Audit Report — OpenCode Telegram Bot vs OpenClaw Reference
2
+
3
+ **Date:** 2026-02-08
4
+ **Scope:** Phases 0-6 complete, comparison against OpenClaw Telegram integration (~3,500 LOC, 41 files)
5
+
6
+ ---
7
+
8
+ ## 1. Project Stats
9
+
10
+ | Metric | Our Bot | OpenClaw |
11
+ |--------|---------|----------|
12
+ | Source LOC | 2,580 (21 files) | ~3,500 (41 files) |
13
+ | Unit tests | 252 | ~200+ (estimated) |
14
+ | E2E tests | 32 | Manual |
15
+ | Total LOC (incl tests + E2E) | ~7,000 | ~10,000+ |
16
+ | Architecture | SDK HTTP bridge | Direct agent integration |
17
+ | Framework | Grammy | Grammy |
18
+ | Runtime | Bun | Node.js |
19
+
20
+ ---
21
+
22
+ ## 2. Quality Assessment by Phase
23
+
24
+ ### Phases 0-4: Excellent
25
+
26
+ The core foundation is solid:
27
+ - **Anti-leak architecture** consistently applied (LRU, AbortController, bounded Maps)
28
+ - **TDD discipline** maintained — every file has colocated tests
29
+ - **EventBus** single SSE design is clean and memory-efficient
30
+ - **DraftStream** throttled editing works well
31
+ - **Permission/Question** handling with PendingRequests + inline keyboards is correct
32
+ - **Session management** with restore-on-restart is production-ready
33
+
34
+ ### Phase 5 (Model & Agent Selection): Good
35
+
36
+ - Clean separation: `models.ts` (139 LOC) and `agents.ts` (93 LOC)
37
+ - Callback data fits 64-byte limit using direct IDs
38
+ - Override persistence across `/new` and session switches — properly fixed
39
+ - 28 unit tests + 9 E2E tests
40
+ - **Minor issue:** No pagination for providers with many models (deferred, acceptable)
41
+
42
+ ### Phase 6 (Media & Files): Good
43
+
44
+ - Download/conversion pipeline is correct and well-tested
45
+ - MIME map is comprehensive (43 extensions)
46
+ - File size limit enforcement works
47
+ - DraftStream race condition properly fixed with `sending` flag
48
+ - 21 unit tests + 6 E2E tests (including fragmentation regression)
49
+ - **Minor issue:** `handleMedia` in bot.ts is an inline anonymous function, not exported for direct unit testing. It works via E2E tests, but differs from the pattern of other exported handlers.
50
+
51
+ ---
52
+
53
+ ## 3. Production Gaps (vs OpenClaw)
54
+
55
+ ### Critical — Should fix before production
56
+
57
+ | # | Gap | Impact | Effort |
58
+ |---|-----|--------|--------|
59
+ | **G1** | **EventBus has no auto-reconnect** | SSE stream break = bot stops receiving events silently. No exponential backoff. | Medium |
60
+ | **G2** | **No Grammy `apiThrottler()` middleware** | Telegram API 429 rate limits not handled. Under high load, API calls fail. | Low |
61
+ | **G3** | **No `sequentialize()` middleware** | Race conditions possible if Grammy processes concurrent updates for same chat (unlikely in polling mode, but risky with webhooks). | Low |
62
+
63
+ ### Important — Should fix for robustness
64
+
65
+ | # | Gap | Impact | Effort |
66
+ |---|-----|--------|--------|
67
+ | **G4** | **No error classification** | Network errors (ECONNRESET, ETIMEDOUT) not distinguished from fatal errors. All treated the same. | Medium |
68
+ | **G5** | **EventBus `listen()` doesn't retry** | If the SSE stream ends (server restart, network issue), the bot goes deaf. No automatic reconnection. | Medium |
69
+ | **G6** | **No update deduplication** | Telegram can deliver duplicate updates. We process them again. | Low |
70
+
71
+ ### Nice to have — For future phases
72
+
73
+ | # | Gap | Impact | Effort |
74
+ |---|-----|--------|--------|
75
+ | G7 | No text fragment assembly | Users who paste >4096 chars get each fragment processed separately | Medium |
76
+ | G8 | No media group buffering | Multi-photo sends = N separate prompts instead of one combined | Medium |
77
+ | G9 | No inbound debouncing | Rapid messages processed individually (can flood the AI) | Low |
78
+ | G10 | No sent-message cache | Can't deduplicate outbound messages | Low |
79
+ | G11 | No update offset persistence | Bot restart may re-process recent updates | Low |
80
+
81
+ ---
82
+
83
+ ## 4. Code Structure Analysis
84
+
85
+ ### Strengths
86
+
87
+ 1. **Clean module boundaries** — Each handler file is self-contained with pure functions + async handlers
88
+ 2. **Dependency injection** — `DraftStreamDeps`, `BotDeps`, SDK as params (not globals)
89
+ 3. **Testability** — Every handler can be tested without Grammy context
90
+ 4. **Anti-leak guarantees** — AbortController per turn, LRU bounded SessionManager, TTL on PendingRequests
91
+ 5. **Session restore** — Bot survives restarts by matching "Telegram {chatId}" title pattern
92
+
93
+ ### Areas for Improvement
94
+
95
+ 1. **`bot.ts` is growing** (479 LOC) — Approaching the split threshold. Consider extracting:
96
+ - Callback routing → `handlers/callbacks.ts`
97
+ - `handleMedia` → export from `handlers/media.ts` for testability
98
+ - `handleMessage` is already well-structured
99
+
100
+ 2. **`index.ts` does too much** (283 LOC) — Event routing and `finalizeResponse` live in the entry point. Should be in a dedicated `event-router.ts` for unit testability.
101
+
102
+ 3. **Event types are `any`** — EventBus uses `any` for event types. Type-safe event handling would catch bugs at compile time.
103
+
104
+ 4. **No structured logging** — All errors go to `console.error`. No log levels, no structured format, no error context.
105
+
106
+ ---
107
+
108
+ ## 5. What We Do Better Than OpenClaw
109
+
110
+ | Aspect | Our Advantage |
111
+ |--------|--------------|
112
+ | **Architecture** | SDK bridge is thinner (~2,580 LOC vs ~3,500). Delegates AI complexity to server. |
113
+ | **Testing** | 252 unit + 32 E2E with TDD rigor. Phase-gated with full regression at each step. |
114
+ | **Memory safety** | Explicit anti-leak design: LRU, AbortController, bounded everything. |
115
+ | **Deployment** | Simple env vars (4-5 vars). No YAML config, no multi-account, no pairing system. |
116
+ | **Session management** | Server-side persistence with local LRU cache. Clean restore on restart. |
117
+ | **Phase gating** | Clear boundaries, docs per phase, regression testing. Sustainable development. |
118
+
119
+ ---
120
+
121
+ ## 6. Feature Parity Matrix
122
+
123
+ | Feature | Our Bot | OpenClaw | Notes |
124
+ |---------|---------|----------|-------|
125
+ | Text messages | ✅ | ✅ | |
126
+ | Photo/Document/Audio/Video | ✅ | ✅ | Phase 6 |
127
+ | Voice messages | ✅ | ✅ | |
128
+ | Streaming (edit draft) | ✅ | ✅ | With `sending` fix |
129
+ | Permissions (inline buttons) | ✅ | N/A | OpenClaw has no permission system |
130
+ | Questions (inline buttons) | ✅ | N/A | OpenClaw has no question system |
131
+ | /cancel (abort) | ✅ | ✅ | |
132
+ | Session management | ✅ | ✅ | /list, /rename, /delete, /info, /history |
133
+ | Session restore on restart | ✅ | ✅ | Title pattern matching |
134
+ | Model selection | ✅ | ✅ | Phase 5 |
135
+ | Agent selection | ✅ | N/A | OpenClaw has different agent system |
136
+ | Allowlist | ✅ | ✅ | |
137
+ | Markdown → HTML | ✅ | ✅ | |
138
+ | Message chunking | ✅ | ✅ | |
139
+ | HTML fallback | ✅ | ✅ | |
140
+ | Typing indicator | ✅ | ✅ | |
141
+ | Tool progress | ✅ | N/A | |
142
+ | Auto-reconnect SSE | ❌ | ✅ | **Gap G1/G5** |
143
+ | Rate limit middleware | ❌ | ✅ | **Gap G2** |
144
+ | Sequentialize middleware | ❌ | ✅ | **Gap G3** |
145
+ | Text fragment assembly | ❌ | ✅ | Gap G7 |
146
+ | Media group buffering | ❌ | ✅ | Gap G8 |
147
+ | Inbound debouncing | ❌ | ✅ | Gap G9 |
148
+ | Group chat support | ❌ | ✅ | Phase 8 planned |
149
+ | Forum topics | ❌ | ✅ | Phase 8 planned |
150
+ | Webhook mode | ❌ | ✅ | Phase 9 planned |
151
+ | Sticker vision cache | ❌ | ✅ | Nice to have |
152
+ | Voice → voice bubble | ❌ | ✅ | Nice to have |
153
+ | Multi-account | ❌ | ✅ | Not needed |
154
+ | Audit logging | ❌ | ✅ | Not needed |
155
+ | Update offset persistence | ❌ | ✅ | Gap G11 |
156
+
157
+ ---
158
+
159
+ ## 7. Recommendations
160
+
161
+ ### Before Production (Priority)
162
+
163
+ 1. **Fix EventBus reconnection (G1+G5)** — Add exponential backoff loop in `listen()`. When SSE stream ends or errors, wait 2s→30s (1.8x factor) and reconnect. This is the most critical gap.
164
+
165
+ 2. **Add `apiThrottler()` (G2)** — One line of Grammy middleware. Prevents 429 errors.
166
+
167
+ 3. **Add `sequentialize()` (G3)** — One line of Grammy middleware. Prevents per-chat race conditions (important if switching to webhooks later).
168
+
169
+ ### Before Phase 7 (Recommended)
170
+
171
+ 4. **Extract event routing from index.ts** — Move `onEvent` callback and `finalizeResponse` to `event-router.ts`. Enables unit testing of event logic.
172
+
173
+ 5. **Type SSE events** — Replace `any` with typed event union in EventBus.
174
+
175
+ ### For Future Phases
176
+
177
+ 6. **Text fragment assembly (G7)** — Buffer near-limit messages for 1500ms, merge.
178
+ 7. **Media group buffering (G8)** — Buffer same `media_group_id` for 200ms.
179
+ 8. **Inbound debouncing (G9)** — 300-500ms buffer for rapid messages.
180
+
181
+ ---
182
+
183
+ ## 8. Overall Assessment
184
+
185
+ **The codebase quality is good.** Phases 5 and 6 maintained the same standards as earlier phases:
186
+ - Clean module boundaries
187
+ - Comprehensive test coverage
188
+ - Anti-leak design throughout
189
+ - Bugs found via manual testing were properly diagnosed and fixed with tests
190
+
191
+ **The main gaps are infrastructure-level** (reconnection, rate limiting, middleware), not code quality issues. These are expected at the Phase 6 stage and should be addressed in Phase 9 (Infrastructure & Security) or as a hardening pass before production deployment.
192
+
193
+ **LOC target from spec was ~2,000 for Phase 1.** At Phase 6 we're at 2,580 for all source — well within the expected trajectory toward the spec's full target.
package/docs/PROGRESS.md CHANGED
@@ -325,3 +325,191 @@ e2e/
325
325
  - `sdk.session.abort()` is fire-and-forget — errors logged but don't block new turn
326
326
  - Phase 3 streaming E2E was flaky: finalizeResponse can shorten text (strips tool suffix), so assert "same message ID + has content" instead of "text grew"
327
327
  - `sdk.session.summarize()` requires providerID + modelID — simpler to guide users to ask the AI directly
328
+
329
+ ---
330
+
331
+ ## Phase 5 — Model & Agent Selection ✅
332
+
333
+ **Status:** Complete
334
+ **Date:** 2026-02-08
335
+ **Plan:** See `docs/plans/phase-5.md`
336
+
337
+ ### Delivered
338
+ - **`/model` command** — Shows providers as inline keyboard. Clicking a provider shows its models. Selecting a model stores a `modelOverride` in SessionEntry, passed to every `session.prompt()`.
339
+ - **`/agent` command** — Shows available agents (filtered: non-hidden) as inline keyboard. Selecting stores `agentOverride` in SessionEntry.
340
+ - **Model reset** — "Reset to default" button clears override.
341
+ - **Agent reset** — Same pattern.
342
+ - **Back navigation** — Model selection supports back→provider list.
343
+ - **Override persistence** — Model/agent overrides survive `/new` and session switches (`/list`).
344
+ - **Callback data encoding** — Uses `mdl:{providerID}:{modelID}` directly (fits 64-byte limit for most providers). Falls back to truncation.
345
+
346
+ ### Tests
347
+ | Type | Count | Status |
348
+ |------|-------|--------|
349
+ | Unit (bun test src/) | 222 | ✅ all pass |
350
+ | E2E Phase 0-4 (regression) | 17 | ✅ all pass |
351
+ | E2E Phase 5 | 3 | ✅ all pass |
352
+
353
+ ### Files Created
354
+ ```
355
+ src/
356
+ handlers/
357
+ models.ts ← formatProviderList, formatModelList, parseModelCallback, handleModel, handleModelSelect
358
+ models.test.ts ← 18 tests
359
+ agents.ts ← formatAgentList, parseAgentCallback, handleAgent, handleAgentSelect
360
+ agents.test.ts ← 10 tests
361
+ e2e/
362
+ phase-5.test.ts ← 3 E2E tests
363
+ ```
364
+
365
+ ### Files Modified
366
+ ```
367
+ src/
368
+ session-manager.ts ← Added modelOverride? and agentOverride? to SessionEntry
369
+ session-manager.test.ts ← 3 new tests for override fields
370
+ bot.ts ← /model, /agent commands; mdl: + agt: callback routing;
371
+ pass overrides in handleMessage prompt call
372
+ bot.test.ts ← 6 new tests (override passing, callbacks)
373
+ index.ts ← setMyCommands includes /model and /agent
374
+ ```
375
+
376
+ ### Key Design Decisions
377
+ - **Direct ID encoding in callback data** — `mdl:anthropic:claude-sonnet-4-5-20250929` fits 64 bytes. No need for index-based lookup maps.
378
+ - **Override persistence across `/new`** — overrides are per-user preferences, not per-session. Preserved by saving before remove and restoring after create.
379
+ - **Hidden agents filtered** — `sdk.app.agents()` may return hidden agents; we filter them out.
380
+ - **No pagination** — Most providers have <10 models. Deferred for later.
381
+
382
+ ### Lessons Learned
383
+ - SDK v2 uses flat params: `sdk.session.prompt({ sessionID, parts, model: { providerID, modelID } })`, not nested path/body.
384
+ - `sdk.provider.list()` returns `{ data: { all: Provider[] } }` where each Provider has `models: { [modelID]: Model }`.
385
+
386
+ ---
387
+
388
+ ## Phase 6 — Media & Files ✅
389
+
390
+ **Status:** Complete
391
+ **Date:** 2026-02-08
392
+ **Plan:** See `docs/plans/phase-6.md`
393
+
394
+ ### Delivered
395
+ - **Photo handling** — Highest resolution photo downloaded, converted to base64 data URL, sent as `FilePartInput`.
396
+ - **Document handling** — Any document type (PDF, code, text) downloaded and sent as file part. Original filename preserved.
397
+ - **Voice/Audio handling** — Voice messages (OGG) and audio files downloaded and sent as file parts.
398
+ - **Video handling** — Video files downloaded and sent as file parts.
399
+ - **Caption support** — Caption becomes `TextPartInput` alongside `FilePartInput`.
400
+ - **File size limit** — 20MB check before download (Telegram Bot API limit).
401
+ - **MIME detection** — Extension-based fallback when Telegram doesn't provide MIME type (43 extensions mapped).
402
+ - **DraftStream race condition fix** — Added `sending` flag to prevent concurrent `sendMessage` calls when multiple SSE events arrive before first message is created. Fixes fragmentation with fast models (Gemini Flash).
403
+ - **Override persistence fix** — `handleNew` and `handleSessionCallback` now preserve model/agent overrides across session changes.
404
+
405
+ ### Tests
406
+ | Type | Count | Status |
407
+ |------|-------|--------|
408
+ | Unit (bun test src/) | 252 | ✅ all pass |
409
+ | E2E Phase 0-5 (regression) | 26 | ✅ all pass |
410
+ | E2E Phase 6 | 6 | ✅ all pass |
411
+
412
+ ### Files Created
413
+ ```
414
+ src/
415
+ handlers/
416
+ media.ts ← extractFileRef, downloadTelegramFile, bufferToDataUrl,
417
+ buildFilePart, buildMediaParts, getMimeFromFileName
418
+ media.test.ts ← 21 tests
419
+ e2e/
420
+ phase-6.test.ts ← 6 E2E tests (photo, document, caption, regression text,
421
+ fragmentation, interruption)
422
+ ```
423
+
424
+ ### Files Modified
425
+ ```
426
+ src/
427
+ bot.ts ← handleMedia function, 5 Grammy media handlers (photo,
428
+ document, voice, audio, video) BEFORE message:text;
429
+ handleMessage accepts optional parts param;
430
+ handleNew/handleSessionCallback preserve overrides
431
+ bot.test.ts ← 6 new tests (media parts, override persistence)
432
+ send/draft-stream.ts ← Added `sending` flag to prevent concurrent sendMessage race
433
+ send/draft-stream.test.ts ← 3 new tests for race condition scenarios
434
+ e2e/
435
+ phase-5.test.ts ← 6 new tests (model override + persistence + E2E)
436
+ ```
437
+
438
+ ### Key Design Decisions
439
+ - **Grammy handler ordering** — Media handlers (`message:photo`, etc.) must come BEFORE `message:text` because photos with captions also match `message:text`.
440
+ - **Shared `handleMedia` function** — All 5 media types share the same handler logic.
441
+ - **Data URL approach** — Download → Buffer → base64 data URL. Simple, no temp files, works within HTTP body limits even for 20MB files (~27MB base64).
442
+ - **`sending` flag pattern** — Based on OpenClaw's `inFlight` pattern: first `update()` sets flag, concurrent calls just store `pending`, flag cleared after `sendMessage` completes.
443
+
444
+ ### Bugs Fixed This Phase
445
+ 1. **Streaming fragmentation** — With fast models (Gemini Flash), multiple SSE events called `DraftStream.update()` concurrently. All saw `messageId === null` and each called `sendMessage`, creating N separate messages. Fixed with `sending` guard flag.
446
+ 2. **Model/agent override lost on `/new`** — `sessionManager.remove()` deleted the entry with overrides, then `getOrCreate()` created a clean one. Fixed by saving overrides before remove and restoring after.
447
+
448
+ ### Lessons Learned
449
+ - Telegram requires valid PNG encoding — hand-crafted zlib fails, must use `zlib.deflateSync()`.
450
+ - gramjs `CustomFile(name, size, path, buffer)` for sending files in E2E tests.
451
+ - E2E fragmentation test: count bot messages between events, assert ≤ expected (not exact count).
452
+ - `DraftStream` race condition only manifests with fast models (high SSE event rate).
453
+
454
+ ---
455
+
456
+ ## Phase 6.5 — Production Hardening + Bot Control API ✅
457
+
458
+ **Status:** Complete (unit tests pass, E2E written — awaiting E2E run)
459
+ **Date:** 2026-02-08
460
+ **Plan:** See `docs/plans/phase-6.5.md`
461
+ **Audit:** See `docs/AUDIT.md`
462
+
463
+ ### Delivered
464
+ - **EventBus auto-reconnect (G1/G5)** — SSE stream break → auto-reconnect with exponential backoff (2s→30s, 1.8x factor, ±25% jitter). Cancellable via `stop()`.
465
+ - **Grammy `apiThrottler()` (G2)** — API transformer that queues and retries on Telegram 429 rate limits.
466
+ - **Grammy `sequentialize()` (G3)** — First middleware, ensures per-chat serial update processing. Prevents race conditions with webhooks.
467
+ - **Smart /model filtering (G12)** — Only connected providers, models grouped by family (most recent per family), active model marked with ✓. Reduces ~400 models → ~16.
468
+ - **Bot Control API** — Hono HTTP server on `127.0.0.1:4097` exposing 7 endpoints for AI-driven model/agent/session control.
469
+ - **SKILL.md** — Teaches AI how to discover its sessionId and use the Bot Control API.
470
+
471
+ ### Tests
472
+ | Type | Count | Status |
473
+ |------|-------|--------|
474
+ | Unit (bun test src/) | 295 | ✅ all pass |
475
+ | E2E Phase 6.5 | 8 | Written, pending run |
476
+
477
+ ### Files Created
478
+ ```
479
+ src/
480
+ api-server.ts ← Hono app + createApiServer (Bot Control API)
481
+ api-server.test.ts ← 15 tests (all endpoints via app.request())
482
+ deploy/
483
+ skills/
484
+ telegram-control/
485
+ SKILL.md ← AI skill doc for Bot Control API
486
+ e2e/
487
+ phase-6.5.test.ts ← 8 E2E tests (/model filtering + Bot Control API)
488
+ ```
489
+
490
+ ### Files Modified
491
+ ```
492
+ src/
493
+ event-bus.ts ← reconnectLoop + exponential backoff + cancellable sleep
494
+ event-bus.test.ts ← 13 new tests (reconnect, backoff, jitter, error recovery)
495
+ bot.ts ← sequentialize() middleware + filterModels in mdl: callback
496
+ bot.test.ts ← 3 new tests (sequentialize, throttler import)
497
+ config.ts ← Added apiPort config (TELEGRAM_API_PORT, default 4097)
498
+ config.test.ts ← 2 new tests for apiPort
499
+ index.ts ← apiThrottler + createApiServer + apiServer.stop in shutdown
500
+ handlers/models.ts ← filterModels(), formatProviderList(connected), formatModelList(activeModelID)
501
+ handlers/models.test.ts ← 8 new tests (filtering, connected, ✓ marker)
502
+ package.json ← Added hono, @grammyjs/transformer-throttler, @grammyjs/runner
503
+ ```
504
+
505
+ ### New Dependencies
506
+ - `hono` ^4.x — HTTP framework for Bot Control API
507
+ - `@grammyjs/transformer-throttler` ^1.x — Telegram API 429 rate limit handling
508
+ - `@grammyjs/runner` ^2.x — sequentialize middleware for per-chat serial processing
509
+
510
+ ### Key Design Decisions
511
+ - **Bot Control API on localhost** — AI calls it via `curl` from bash. No auth needed (bound to 127.0.0.1 only).
512
+ - **AI self-identification** — AI discovers its sessionId by calling OpenCode session list (`localhost:4096/session`), matches title pattern `"Telegram {chatId}"`.
513
+ - **filterModels pattern** — Group by family, sort by release_date descending, pick first. Handles missing family (uses model id as key).
514
+ - **Hono test pattern** — `app.request()` — no real Bun.serve needed in tests.
515
+ - **EventBus backoff** — `delay = min(maxDelay, initialDelay × factor^attempt) × (1 ± jitter)`. Cancellable sleep via sleepReject.