Skip to content

Commit bbb005e

Browse files
committed
docs: document perf/reliability fixes — retry, health, backoff, TelegramError
1 parent 05bea8c commit bbb005e

2 files changed

Lines changed: 51 additions & 7 deletions

File tree

docs/DEVELOPMENT.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ internal/
7676
session.go Telegram session store (chat → odek session mapping)
7777
plan.go Plan management (Slugify, ListPlans, ReadPlan, DeletePlan, MostRecentPlan)
7878
download.go Media download (voice, photo → file on disk)
79-
*_test.go 409 tests across all subsystems (86.9% coverage)
79+
health.go HTTP health check endpoint (atomic.Bool ready state, 503→200)
80+
*_test.go 473 tests across all subsystems (87.1% coverage)
8081
cmd/odek/
8182
main.go CLI entry point, flag parsing, commands, sandbox
8283
main_test.go CLI tests (flag parsing, version, init)
@@ -130,7 +131,7 @@ Zero external test dependencies — tests use `httptest`, `testing`, and the sta
130131

131132
| Layer | Runner | Tests | What's tested |
132133
|-------|--------|-------|---------------|
133-
| **Unit** | `go test ./...` | 1890+ | All 14 packages — config, LLM client, loop, sessions, renderer, tools, WS, resources, memory, skills, telegram, danger, security, mcp |
134+
| **Unit** | `go test ./...` | 1954 | All 17 packages — config, LLM client, loop, sessions, renderer, tools, WS, resources, memory, skills, telegram, danger, security, mcp |
134135
| **Contract** | `go test ./cmd/odek/` | 60+ | Sub-agent flag parsing, JSON stdout, exit codes, tool schema, config, serve, shell |
135136
| **E2E** | `KODE_E2E=true go test -run 'TestE2E_'` | 16 | Real subprocess spawning, tool→binary pipeline, concurrency, timeouts, custom prompts |
136137

@@ -150,7 +151,7 @@ Zero external test dependencies — tests use `httptest`, `testing`, and the sta
150151
| `internal/danger` | 281 | Command classification (8 risk classes), config overrides, allow/denylist |
151152
| `internal/memory` | 144 | Facts CRUD, buffer ring, episodes, merge detector (go-vector), ReplaceEntry, AppendEntry, memory tool, security scan, LLM ranking |
152153
| `internal/skills` | 127 | Loading, triggers, self-improvement (5 heuristics), curation, LLM-enhanced generation, import, tools, ValidateSkillName, isPrivateHost, extractRelevantChange |
153-
| `internal/telegram` | 409 | Bot client, long-polling, command handlers, session management, plan CRUD, voice/photo download, media dir management |
154+
| `internal/telegram` | 473 | Bot client, long-polling, command handlers, session management, plan CRUD, voice/photo download, health server, retry/backoff |
154155
| `cmd/odek` | 441 | Flag parsing, init, version, sandbox setup, subagent, serve, security E2E, shell tool danger, browser tool, contract tests |
155156

156157
## Key packages

docs/TELEGRAM.md

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Telegram Bot API ◄── bot.go (HTTP client)
2020
download.go (voice/photo media)
2121
```
2222

23-
The package is self-contained under `internal/telegram/` with 450+ tests and 87% coverage. All Telegram API calls use the Bot struct, which wraps `net/http` with JSON marshaling and multipart upload support. No external Telegram libraries are used.
23+
The package is self-contained under `internal/telegram/` with 473 tests and 87% coverage. All Telegram API calls use the Bot struct, which wraps `net/http` with JSON marshaling, multipart upload support, exponential backoff retry, and typed error handling. No external Telegram libraries are used.
2424

2525
## Configuration
2626

@@ -75,8 +75,10 @@ The `Bot` struct is a lightweight Telegram Bot API client built on the standard
7575

7676
### Underlying HTTP Helpers
7777

78-
- **`doJSON`** — Sends JSON POST requests and unmarshals the `result` field from Telegram's response envelope
79-
- **`doUpload`** — Sends multipart/form-data POST requests for file uploads (photo, voice)
78+
- **`doJSON`** — Sends JSON POST requests with exponential backoff retry. Unmarshals the `result` field from Telegram's response envelope.
79+
- **`doUpload`** — Sends multipart/form-data POST requests for file uploads (photo, voice). Buffers the entire file in memory to enable retry without re-reading from disk.
80+
81+
See [Error Handling & Retry](#error-handling--retry) above for retry strategy, `TelegramError` type, and `isRetryableNetworkError` details.
8082

8183
### Fallback URLs
8284

@@ -93,13 +95,52 @@ The `Bot` struct is a lightweight Telegram Bot API client built on the standard
9395
- No-op when budget is 0 (unlimited, the default)
9496
- Configurable via `ODEK_TELEGRAM_DAILY_TOKEN_BUDGET` env var or `daily_token_budget` in `odek.json`
9597

98+
### Error Handling & Retry
99+
100+
`doJSON` and `doUpload` implement exponential backoff retry with these characteristics:
101+
102+
- **Up to 4 retries** (5 total attempts), with 1s → 2s → 4s → 8s backoff
103+
- **Transient errors that get retried:**
104+
- Network errors with `net.Error.Timeout()` or `net.Error.Temporary()` (timeouts, connection refused). Non-transient network errors (DNS failures) are returned immediately.
105+
- HTTP 429 (rate limit) — server indicates client should slow down
106+
- HTTP 5xx (server error) — may be temporary backend issues
107+
- **Fatal errors that are NOT retried:** HTTP 401, 403, 409, and other 4xx client errors (except 429)
108+
109+
### Typed API Errors
110+
111+
Telegram API errors use the `TelegramError` type instead of opaque strings:
112+
113+
```go
114+
type TelegramError struct {
115+
Method string // API method that failed (e.g. "sendMessage")
116+
Description string // Human-readable error description
117+
Code int // HTTP status code
118+
}
119+
```
120+
121+
`IsFatalAPIError(err)` uses `errors.As` for type-safe code extraction — callers don't depend on error string format. Fatal codes: 401 (Unauthorized), 403 (Forbidden), 409 (Conflict — duplicate polling).
122+
123+
### Upload Memory Tradeoff
124+
125+
`doUpload` buffers the entire file in memory before sending (`bytes.Buffer`). This enables retry without re-reading from disk. Telegram's 50 MB upload limit makes this acceptable for bot use cases.
126+
127+
## Health Server (`health.go`)
128+
129+
The `HealthServer` provides an HTTP `/health` endpoint for monitoring systems.
130+
131+
- **Address**: configurable via `--telegram-health-addr` (e.g. `127.0.0.1:9090`). Empty string disables.
132+
- **`ready` state**: uses `atomic.Bool` for thread-safe access. `/health` returns `503 Service Unavailable` with `{"status": "starting"}` until `SetReady()` is called (after polling begins).
133+
- **`200 OK`**: `{"status": "ok", "uptime_seconds": <N>}` once polling is active.
134+
- Graceful shutdown on context cancellation.
135+
96136
## Long-Polling (`poller.go`)
97137

98138
The `Poller` struct implements Telegram's long-polling update mechanism:
99139

100140
- **Offset tracking**: updates are acknowledged by advancing the offset past the highest received update ID, preventing duplicate processing
101141
- **Configurable timeout**: default 30s long-poll (Telegram holds the connection open until updates arrive or the timeout expires)
102142
- **Polling interval**: 1s between poll cycles (configurable via `PollInterval`)
143+
- **Exponential backoff**: consecutive poll errors trigger increasing delays: `interval × 2^errors`, capped at `60 × interval`. Error count clamps at 30 to prevent integer overflow in the bit-shift. A successful poll resets the error counter.
103144

104145
```go
105146
poller := telegram.NewPoller(bot)
@@ -270,7 +311,9 @@ The package defines Telegram API types used throughout:
270311
| `InlineKeyboardButton` | Inline keyboard button |
271312
| `CallbackQuery` | Inline keyboard callback |
272313
| `HandlerConfig` | Message handler configuration |
314+
| `TelegramError` | Typed API error with `Method`, `Description`, `Code` |
273315
| `TelegramConfig` | Full bot configuration |
316+
| `HealthServer` | HTTP health check server |
274317
| `PlanInfo` | Plan file summary |
275318
| `ChatSession` | Per-chat agent session |
276319
| `SessionManager` | Session lifecycle manager |
@@ -303,7 +346,7 @@ A fire-and-forget goroutine sends `sendChatAction("typing")` every 4 seconds whi
303346

304347
## Testing
305348

306-
The Telegram package has **409 tests** with **86.9% coverage**. Tests use:
349+
The Telegram package has **473 tests** with **87.1% coverage**. Tests use:
307350
- `httptest.NewServer` to mock Telegram API responses
308351
- HTTP handler functions for each API endpoint (getFile, sendMessage, sendDocument, etc.)
309352
- `t.TempDir()` + `t.Setenv("HOME", ...)` for filesystem isolation

0 commit comments

Comments
 (0)