Core decision
+One normalized contract
++ Adapters expose an incoming stream and a discrete send effect. The gateway adds + authorization, routing, and a derived outgoing-stream consumer. +
+diff --git a/.plans/02-chat-gateway.html b/.plans/02-chat-gateway.html new file mode 100644 index 0000000..b9c75e3 --- /dev/null +++ b/.plans/02-chat-gateway.html @@ -0,0 +1,529 @@ + + +
+ + ++ A small, reusable boundary for adapter-qualified channels, finished messages, + local-file attachments, and resilient inbound streams—starting with a complete + Telegram Bot API polling adapter. +
+Core decision
++ Adapters expose an incoming stream and a discrete send effect. The gateway adds + authorization, routing, and a derived outgoing-stream consumer. +
+Access model
+
+ telegram:<chat-id> is the
+ adapter-independent authorization key. Optional topic/thread identity stays separate.
+
Files
+
+ Outbound attachments reference local paths. Inbound media is downloaded before emission
+ and points into ~/.compass/files.
+
01 / Boundary
++ Gateway.incoming + : Stream<IncomingMessage, GatewayError> +
++ Gateway.send(message) + : Effect<SentMessage, GatewayError> +
++ Gateway.sendAll(outgoing, options?) + : Stream<SentMessage, GatewayError | E, + R> +
+
+ send is the reliable primitive: one
+ finished message produces one delivery result or one typed failure.
+ sendAll is the stream-oriented
+ convenience requested by callers and uses explicit, bounded concurrency. It does not
+ silently detach fibers or swallow delivery errors.
+
02 / Architecture
+Application
+Incoming consumer
+Outgoing message stream
+Gateway
+Authorize + route
+Merge adapter streams
+Adapter
+Telegram polling
+Bot API + file transfer
+GatewayConfig
+Schema-decodes gateway.json
+GatewayFiles
+Safe durable file writes
+HttpClient
+Injected Bot API transport
++ The adapter contract contains no Telegram types. Each adapter supplies a stable name, + an incoming stream, and a send implementation. A scoped Telegram layer owns the + long-poll lifecycle, so closing the parent scope interrupts in-flight HTTP and retry + delays cleanly. +
+03 / Configuration
+{
+ "version": 1,
+ "allowedChannels": [
+ "telegram:123456789",
+ "telegram:-100987654321"
+ ],
+ "adapters": {
+ "telegram": {
+ "pollingTimeoutSeconds": 30,
+ "pollingLimit": 100,
+ "maxInboundFileBytes": 52428800,
+ "deleteWebhook": true,
+ "dropPendingUpdates": false
+ }
+ }
+}
+
+ The default path is resolved portably from
+ HOME or
+ USERPROFILE. Missing or invalid
+ configuration fails layer construction with a typed configuration error.
+
+ The bot token remains outside the JSON file and is read through Effect Config from
+ TELEGRAM_BOT_TOKEN. An optional API
+ base URL may be injected for deterministic tests.
+
+ Authorization is exact-match and deny-by-default. Topics inherit their Telegram + chat’s qualified channel authorization; the optional thread identifier only controls + reply placement. +
+04 / Telegram
+getUpdates with the current
+ offset.
+ getFile and enforce its size
+ limit.
+ ~/.compass/files without buffering
+ the full download.
+ sendMessage,
+ sendPhoto,
+ sendAudio,
+ sendVoice, or
+ sendDocument.
+ Schedule and structured logs.
+ Successful polls reset backoff. Telegram API envelopes and update payloads are
+ schema-decoded; malformed individual updates are logged and skipped without killing
+ the adapter stream. Authentication/configuration failures remain terminal.
+ 05 / Files
+packages/contracts/src/
+├── gateway.ts # schemas, domain types, errors, adapter contract
+└── gateway.test.ts
+
+apps/server/src/gateway/
+├── Gateway.ts # service, routing, authorization, stream helpers
+├── GatewayConfig.ts # gateway.json schema + default filesystem layer
+├── GatewayFiles.ts # inbound persistence + outbound path reads
+├── index.ts # public exports
+├── Gateway.test.ts
+├── GatewayConfig.test.ts
+├── GatewayFiles.test.ts
+└── adapters/
+ └── telegram/
+ ├── TelegramAdapter.ts # Effect service/layer + normalization + sending
+ ├── TelegramApi.ts # typed Bot API HttpClient boundary
+ ├── TelegramModels.ts # only the Bot API schemas needed here
+ ├── TelegramAdapter.test.ts
+ └── index.ts
+ 06 / Execution
++ Define Schema-based messages, qualified channel IDs, attachments, delivery + receipts, adapter contract, and tagged error families. +
++ Decode gateway.json, resolve Compass paths, create the files directory, sanitize + filenames, and write downloads atomically. +
++ Use Effect HttpClient and Schema for getMe, deleteWebhook, getUpdates, getFile, + downloads, JSON requests, and multipart uploads. +
++ Create a lazy incoming stream, track offset, normalize supported messages, persist + attachments, and make interruption immediate and leak-free. +
++ Merge adapter streams, enforce allowlisting in both directions, route sends, and + expose bounded outgoing-stream processing. +
++ Exercise config, authorization, ordering, retry, interruption, media + download/upload, then run repository readiness and typecheck gates. +
+07 / Validation
+| Area | +Proof | +Expected outcome | +
|---|---|---|
| Authorization | +Allowed and denied channel tests | +Exact-match, deny-by-default | +
| Polling | +Scripted getUpdates responses | ++ Offsets advance; duplicates are avoided + | +
| Resilience | +Transient failures + test clock | +Capped backoff and recovery | +
| Shutdown | +Scoped stream interruption | +Active long poll is cancelled | +
| Inbound files | +Photo, audio, voice, document fixtures | +Safe files exist before emission | +
| Outbound files | +Multipart request inspection | +Correct Bot API method and caption | +
| Repository | +
+ vp run ready vp run typecheck + |
+ Both commands pass | +
+ 08 / Deliberate limits +
+