897 lines
23 KiB
Markdown
897 lines
23 KiB
Markdown
# Messenger Connectors - Uncertainty Pass 34
|
|
|
|
Date: 2026-04-29
|
|
Scope: official shared bot relay transport, webhook ACK semantics, desktop online detection, no durable plaintext backend queue, and local commit guarantees
|
|
|
|
## Executive Delta
|
|
|
|
The weakest reliability boundary is:
|
|
|
|
```text
|
|
Telegram webhook
|
|
-> Agent Teams backend
|
|
-> online desktop relay session
|
|
-> durable local inbound message
|
|
-> lead/team routing
|
|
```
|
|
|
|
The core problem:
|
|
|
|
```text
|
|
If backend returns HTTP 2xx to Telegram before the desktop durably commits the message,
|
|
then a crash, reconnect, or dropped ACK can lose the user message forever.
|
|
```
|
|
|
|
Because the default product decision is "no durable plaintext backend queue", the backend cannot solve this by storing pending message bodies until desktop returns.
|
|
|
|
So the default official-bot rule should be:
|
|
|
|
```text
|
|
Return success to Telegram only after one of these is true:
|
|
|
|
1. Desktop ACKed that it durably committed the inbound message locally.
|
|
2. Backend handled the update terminally, for example no desktop is online and an offline notice was sent.
|
|
3. Backend intentionally rejects the webhook attempt so Telegram retries later.
|
|
```
|
|
|
|
This is the exact bridge that must be designed as a protocol, not as a best-effort event bus.
|
|
|
|
## Source Facts Rechecked
|
|
|
|
Telegram official facts checked on 2026-04-29:
|
|
|
|
- Bot API has two mutually exclusive update delivery modes: `getUpdates` and webhooks.
|
|
- Incoming updates are stored on Telegram servers until the bot receives them, but not longer than 24 hours.
|
|
- `getUpdates.offset` confirms updates when the offset is greater than their `update_id`.
|
|
- `Update.update_id` is useful for ignoring repeated updates or restoring sequence if webhook updates are out of order.
|
|
- `setWebhook.max_connections` controls the maximum simultaneous HTTPS connections Telegram may use for webhook delivery.
|
|
- On webhook delivery, unsuccessful requests are retried for a reasonable number of attempts.
|
|
- `WebhookInfo.pending_update_count`, `last_error_date`, and `last_error_message` expose webhook backlog/error state.
|
|
- `setWebhook.secret_token` adds `X-Telegram-Bot-Api-Secret-Token` to webhook requests.
|
|
|
|
Transport facts checked on 2026-04-29:
|
|
|
|
- Node.js v22 has a stable native WebSocket client API.
|
|
- Node.js v22 does not provide a built-in WebSocket server, so a Node backend still needs a server library.
|
|
- WebSocket is full-duplex over one connection, which fits `offer -> ACK -> control` flows.
|
|
- Server-Sent Events are one-way server-to-client. Client ACKs require a separate HTTP request.
|
|
- SSE supports `id` and reconnection behavior through EventSource, but it is still one-way.
|
|
- Existing repo already uses Fastify 5.7.4. `@fastify/websocket` 11.2.0 is the current npm package for WebSocket support and is built on `ws@8`.
|
|
- Snyk lists `ws@8.20.0` as published March 21, 2026, latest, with no direct vulnerabilities in its database at lookup time.
|
|
|
|
Sources:
|
|
|
|
- https://core.telegram.org/bots/api#getting-updates
|
|
- https://core.telegram.org/bots/api#update
|
|
- https://core.telegram.org/bots/api#setwebhook
|
|
- https://core.telegram.org/bots/api#getwebhookinfo
|
|
- https://nodejs.org/learn/getting-started/websocket
|
|
- https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
|
|
- https://www.npmjs.com/package/%40fastify/websocket
|
|
- https://security.snyk.io/package/npm/ws/8.20.0
|
|
|
|
Local code facts checked:
|
|
|
|
- Existing `HttpServer` is Fastify-based and binds to `127.0.0.1` by default for local app/browser API.
|
|
- Existing `src/main/http/events.ts` implements SSE for local UI clients, with keepalive comments every 30 seconds.
|
|
- That SSE stream has no durable event id/resume model and no client-to-server ACK path. It is fine for local UI refresh, not for Telegram relay commit.
|
|
- The repo already has good local durability patterns:
|
|
- `VersionedJsonStore.updateLocked()`
|
|
- `atomicWriteAsync`
|
|
- `withFileLock`
|
|
- runtime delivery journals with payload hash, pending/committed states, idempotency keys
|
|
- These patterns are directly relevant to the desktop-local inbound store and delivery ledger.
|
|
|
|
Implication:
|
|
|
|
```text
|
|
Do not reuse the existing local SSE event broadcaster as the official bot relay.
|
|
Build a dedicated MessengerRelay protocol.
|
|
```
|
|
|
|
## Top 3 Relay Architecture Options
|
|
|
|
### 1. Desktop outbound WebSocket with local-commit ACK
|
|
|
|
🎯 9 🛡️ 8 🧠 7 Approx change size: 4000-8500 LOC
|
|
|
|
Shape:
|
|
|
|
```text
|
|
desktop main process
|
|
opens WSS connection to Agent Teams relay backend
|
|
authenticates install/binding/session
|
|
sends route inventory hash and heartbeat
|
|
|
|
backend
|
|
receives Telegram webhook
|
|
resolves binding/route/topic
|
|
sends inbound offer over WebSocket
|
|
waits for desktop local-commit ACK
|
|
returns Telegram 2xx only after commit ACK
|
|
```
|
|
|
|
Why this is best:
|
|
|
|
- WebSocket is bidirectional, so `offer -> ack -> cancel -> repair -> heartbeat` stays on one connection.
|
|
- Desktop can use Node 22 native WebSocket client with no new desktop dependency.
|
|
- Backend can use Fastify + `@fastify/websocket` if cloud backend is Node/Fastify.
|
|
- It supports real online presence, route inventory sync, and backpressure.
|
|
- It avoids backend durable plaintext message bodies.
|
|
|
|
Weaknesses:
|
|
|
|
- Needs a real protocol, not just "send JSON over socket".
|
|
- Needs careful ACK timeout and reconnect behavior.
|
|
- Backend still holds plaintext in memory during the webhook attempt.
|
|
- Active-session ACK timeout is ambiguous: desktop might have committed but ACK was lost.
|
|
|
|
Verdict:
|
|
|
|
```text
|
|
Use this for default official shared bot.
|
|
```
|
|
|
|
### 2. SSE downlink plus HTTPS ACK uplink
|
|
|
|
🎯 7 🛡️ 7 🧠 6 Approx change size: 3500-7000 LOC
|
|
|
|
Shape:
|
|
|
|
```text
|
|
desktop opens EventSource/SSE to backend
|
|
backend pushes inbound offers over SSE
|
|
desktop POSTs /ack for local commit
|
|
desktop POSTs heartbeat/inventory separately
|
|
```
|
|
|
|
Why it is attractive:
|
|
|
|
- SSE is simple and HTTP-friendly.
|
|
- Browser/EventSource has reconnect behavior.
|
|
- This resembles the repo's local `/api/events` pattern.
|
|
|
|
Weaknesses:
|
|
|
|
- SSE is one-way, so ACKs and heartbeats need extra HTTP calls.
|
|
- Correlating SSE offer with POST ACK is more complex under reconnect.
|
|
- Existing local SSE implementation lacks durable event ids and Last-Event-ID resume.
|
|
- Browser SSE connection limits matter for renderer use. Desktop main process can avoid browser limits, but the protocol is still less direct than WebSocket.
|
|
|
|
Verdict:
|
|
|
|
```text
|
|
Acceptable fallback if WebSocket is blocked by enterprise proxies.
|
|
Not the primary implementation.
|
|
```
|
|
|
|
### 3. Desktop polling/long-polling relay
|
|
|
|
🎯 6 🛡️ 6 🧠 4 Approx change size: 2200-5000 LOC
|
|
|
|
Shape:
|
|
|
|
```text
|
|
desktop polls backend for pending inbound updates
|
|
backend returns message bodies if any
|
|
desktop commits locally and POSTs ACK
|
|
```
|
|
|
|
Why it is attractive:
|
|
|
|
- Easier to reason about than long-lived sockets.
|
|
- Works in many locked-down networks.
|
|
- Simple to implement initially.
|
|
|
|
Weaknesses:
|
|
|
|
- To avoid message loss, backend must hold pending plaintext while waiting for poll.
|
|
- If backend refuses durable plaintext queue, polling becomes either lossy or high-frequency.
|
|
- Latency is worse.
|
|
- Online/offline state becomes fuzzy.
|
|
|
|
Verdict:
|
|
|
|
```text
|
|
Not recommended for default no-plaintext-queue mode.
|
|
Can be a diagnostics fallback, not the main relay.
|
|
```
|
|
|
|
## Future Reliability Option
|
|
|
|
### Durable encrypted backend queue
|
|
|
|
🎯 8 🛡️ 9 🧠 9 Approx change size: 7000-14000 LOC
|
|
|
|
Shape:
|
|
|
|
```text
|
|
desktop publishes public encryption key during binding
|
|
backend stores only ciphertext message bodies
|
|
desktop decrypts when online
|
|
backend can survive restarts and desktop offline windows
|
|
```
|
|
|
|
This is the right advanced/premium reliability mode, but it is not the MVP default.
|
|
|
|
Why:
|
|
|
|
- Key rotation is non-trivial.
|
|
- Device loss/reinstall can make queued messages undecryptable.
|
|
- Multi-device routing becomes harder.
|
|
- Attachments need a separate encrypted blob policy.
|
|
- User copy must explain exactly who can decrypt what.
|
|
|
|
## Recommended Default Protocol
|
|
|
|
### High-level flow
|
|
|
|
```text
|
|
Telegram -> Backend webhook
|
|
1. verify webhook secret_token
|
|
2. dedupe update_id metadata
|
|
3. resolve binding/route/topic
|
|
4. check active desktop relay session
|
|
5. if no healthy session: send offline notice, return 2xx
|
|
6. if healthy session: offer update to desktop
|
|
7. desktop validates route and commits local inbound message
|
|
8. desktop ACKs local commit
|
|
9. backend returns 2xx to Telegram
|
|
```
|
|
|
|
The backend durable metadata ledger may store:
|
|
|
|
```text
|
|
provider
|
|
bot mode
|
|
update_id
|
|
route id
|
|
binding id
|
|
attempt count
|
|
status
|
|
timestamps
|
|
error class
|
|
payload hash
|
|
```
|
|
|
|
It should not store in default mode:
|
|
|
|
```text
|
|
raw message body
|
|
raw Telegram chat id in logs
|
|
raw Telegram user id in logs
|
|
attachment file bodies
|
|
bot tokens
|
|
```
|
|
|
|
### Webhook ACK invariant
|
|
|
|
```text
|
|
Telegram 2xx means:
|
|
Agent Teams either got the message durably into desktop local storage,
|
|
or intentionally terminal-handled it, for example offline notice.
|
|
```
|
|
|
|
Telegram non-2xx means:
|
|
|
|
```text
|
|
Agent Teams has not accepted responsibility for the update.
|
|
Telegram may retry the same update later.
|
|
```
|
|
|
|
This must be an explicit code invariant.
|
|
|
|
## Active Session Definition
|
|
|
|
Do not define "online" as "there is a socket object".
|
|
|
|
Define it as:
|
|
|
|
```ts
|
|
interface MessengerRelaySession {
|
|
sessionId: string;
|
|
installId: string;
|
|
bindingId: string;
|
|
authenticatedAt: string;
|
|
lastHeartbeatAt: string;
|
|
lastPongAt: string;
|
|
routeInventoryHash: string;
|
|
protocolVersion: number;
|
|
status: 'ready' | 'stale' | 'draining' | 'closed';
|
|
}
|
|
```
|
|
|
|
A session is healthy only if:
|
|
|
|
```text
|
|
status == ready
|
|
authenticated install secret is valid
|
|
binding is active
|
|
route inventory hash is current or compatible
|
|
last pong is recent
|
|
desktop protocol version is supported
|
|
no newer session has stolen the lease
|
|
```
|
|
|
|
Suggested timing:
|
|
|
|
```text
|
|
ping interval: 15s
|
|
stale after: 45s
|
|
hard close after: 75s
|
|
inbound offer ACK deadline: 3-8s
|
|
```
|
|
|
|
Use jitter for reconnect:
|
|
|
|
```text
|
|
initial reconnect: 1s
|
|
max reconnect: 30s
|
|
jitter: 20-40 percent
|
|
```
|
|
|
|
## Single Active Session Lease
|
|
|
|
MVP should allow one active relay session per binding.
|
|
|
|
Rule:
|
|
|
|
```text
|
|
New authenticated session for the same bindingId steals the lease.
|
|
Old session transitions to draining/closed and cannot ACK new offers.
|
|
```
|
|
|
|
Why:
|
|
|
|
- Prevents two desktop processes writing the same Telegram update to different local stores.
|
|
- Avoids split-brain if user launches two app instances.
|
|
- Keeps support/debugging simpler.
|
|
|
|
Later multi-device mode can use:
|
|
|
|
```text
|
|
bindingId + deviceId + route assignment
|
|
```
|
|
|
|
But do not start there.
|
|
|
|
## Inbound Offer Envelope
|
|
|
|
Provider-neutral envelope:
|
|
|
|
```ts
|
|
interface MessengerInboundOffer {
|
|
type: 'messenger.inbound.offer';
|
|
protocolVersion: 1;
|
|
deliveryId: string;
|
|
provider: 'telegram';
|
|
bindingId: string;
|
|
routeId: string;
|
|
orderingKey: string;
|
|
providerUpdateId: string;
|
|
providerMessageId?: string;
|
|
providerMessageThreadId?: string;
|
|
providerDate?: string;
|
|
receivedAt: string;
|
|
expiresAt: string;
|
|
payloadHash: string;
|
|
payload: MessengerInboundPayload;
|
|
}
|
|
```
|
|
|
|
Payload:
|
|
|
|
```ts
|
|
interface MessengerInboundPayload {
|
|
kind: 'text' | 'command' | 'unsupported';
|
|
text?: string;
|
|
replyTo?: ProviderMessageLink;
|
|
sender: {
|
|
providerUserIdHash: string;
|
|
displayNameSnapshot: string;
|
|
usernameSnapshot?: string;
|
|
};
|
|
}
|
|
```
|
|
|
|
Desktop ACK:
|
|
|
|
```ts
|
|
type MessengerInboundAck =
|
|
| {
|
|
type: 'messenger.inbound.ack';
|
|
deliveryId: string;
|
|
status: 'committed';
|
|
localMessageId: string;
|
|
localCommitHash: string;
|
|
committedAt: string;
|
|
}
|
|
| {
|
|
type: 'messenger.inbound.ack';
|
|
deliveryId: string;
|
|
status: 'duplicate_committed';
|
|
localMessageId: string;
|
|
committedAt: string;
|
|
}
|
|
| {
|
|
type: 'messenger.inbound.ack';
|
|
deliveryId: string;
|
|
status: 'rejected_terminal' | 'rejected_retryable';
|
|
reasonCode: string;
|
|
detail?: string;
|
|
};
|
|
```
|
|
|
|
## Desktop Local Commit Rules
|
|
|
|
Desktop must ACK `committed` only after:
|
|
|
|
```text
|
|
1. Provider update id was deduped locally.
|
|
2. Route binding is still active.
|
|
3. Message payload passed visibility/safety validation.
|
|
4. Message was written to a local durable inbound store.
|
|
5. Local store fsync/atomic-write equivalent completed as far as our platform layer supports.
|
|
```
|
|
|
|
Recommended stores:
|
|
|
|
```text
|
|
MessengerDesktopInboundStore
|
|
durable provider update payloads after acceptance
|
|
|
|
MessengerLocalDeliveryLedger
|
|
tracks delivery from inbound store to lead/team inbox
|
|
|
|
MessengerProviderUpdateLedger
|
|
dedupes providerUpdateId locally
|
|
```
|
|
|
|
Do not ACK based on:
|
|
|
|
```text
|
|
renderer state update
|
|
toast notification shown
|
|
in-memory queue push only
|
|
lead process prompt accepted but not persisted
|
|
```
|
|
|
|
## Backend Webhook Decision Matrix
|
|
|
|
### No active session
|
|
|
|
```text
|
|
send offline notice
|
|
return Telegram 2xx
|
|
record metadata: terminal_offline
|
|
```
|
|
|
|
This matches the current product decision:
|
|
|
|
```text
|
|
desktop offline -> no plaintext queue -> honest offline response
|
|
```
|
|
|
|
### Active session, offer ACKed committed
|
|
|
|
```text
|
|
return Telegram 2xx
|
|
record metadata: desktop_committed
|
|
```
|
|
|
|
### Active session, duplicate committed ACK
|
|
|
|
```text
|
|
return Telegram 2xx
|
|
record metadata: duplicate_desktop_committed
|
|
```
|
|
|
|
### Active session, terminal reject
|
|
|
|
Examples:
|
|
|
|
```text
|
|
route revoked
|
|
unknown topic
|
|
unsupported chat type
|
|
payload rejected by policy
|
|
```
|
|
|
|
Action:
|
|
|
|
```text
|
|
send user-facing rejection if useful
|
|
return Telegram 2xx
|
|
record metadata: terminal_rejected
|
|
```
|
|
|
|
### Active session, retryable reject
|
|
|
|
Examples:
|
|
|
|
```text
|
|
local store locked
|
|
team route repairing
|
|
desktop still loading route inventory
|
|
```
|
|
|
|
Action:
|
|
|
|
```text
|
|
return Telegram 503 for a bounded number of attempts or bounded age
|
|
then fall back to offline/degraded notice and 2xx
|
|
```
|
|
|
|
### Active session, no ACK before deadline
|
|
|
|
This is the hardest case.
|
|
|
|
Recommended:
|
|
|
|
```text
|
|
1. Mark metadata: ack_timeout_ambiguous.
|
|
2. Return Telegram 503 if within retry budget.
|
|
3. On retry, re-offer with same providerUpdateId and payloadHash.
|
|
4. Desktop must return duplicate_committed if it already wrote the message.
|
|
5. If retry budget expires, send "delivery uncertain/offline" notice and return 2xx.
|
|
```
|
|
|
|
Do not immediately send a definitive "not delivered" notice after an ACK timeout, because the desktop might have committed and the ACK may have been lost.
|
|
|
|
## Retry Budget
|
|
|
|
Use Telegram retries only for ambiguous transient failures, not as a product queue.
|
|
|
|
Suggested initial policy:
|
|
|
|
```text
|
|
max retry deferrals per update: 2
|
|
max retry window: 30-60s
|
|
if no healthy desktop by then: offline/degraded notice and 2xx
|
|
```
|
|
|
|
Why:
|
|
|
|
- Keeps the product promise: no default durable backend plaintext queue.
|
|
- Avoids indefinite webhook backlog.
|
|
- Lets short reconnects recover.
|
|
- Does not silently turn Telegram into a long-term queue.
|
|
|
|
## Ordering Rules
|
|
|
|
Telegram `update_id` is useful for duplicate detection, but do not assume every update id is contiguous forever.
|
|
|
|
Route ordering should use:
|
|
|
|
```text
|
|
orderingKey = provider + bindingId + routeId
|
|
providerOrder = update_id plus provider message date/message_id when available
|
|
```
|
|
|
|
Backend:
|
|
|
|
```text
|
|
Use an in-memory per-route serial executor while the process is alive.
|
|
Do not persist plaintext to achieve ordering in default mode.
|
|
```
|
|
|
|
Desktop:
|
|
|
|
```text
|
|
Deduplicate by providerUpdateId.
|
|
Append accepted messages in provider order when possible.
|
|
If out-of-order arrival is detected, store both and mark ordering warning.
|
|
```
|
|
|
|
Webhook setting:
|
|
|
|
```text
|
|
Do not set max_connections to 1 globally for the shared bot unless traffic is tiny.
|
|
Use route-level ordering instead.
|
|
```
|
|
|
|
Reason:
|
|
|
|
```text
|
|
max_connections=1 would serialize every customer through one webhook lane.
|
|
That is safe but does not scale.
|
|
```
|
|
|
|
## Route Inventory Handshake
|
|
|
|
When desktop connects:
|
|
|
|
```text
|
|
1. authenticate install/binding
|
|
2. send protocol version
|
|
3. send route inventory hash
|
|
4. backend responds with active routes known server-side
|
|
5. desktop responds with local route inventory
|
|
6. both sides mark compatible or needs_repair
|
|
```
|
|
|
|
If route inventory mismatches:
|
|
|
|
```text
|
|
do not deliver inbound user messages into uncertain routes
|
|
ask desktop to repair or refresh
|
|
```
|
|
|
|
This protects cases like:
|
|
|
|
- team deleted locally
|
|
- team renamed
|
|
- topic recreated
|
|
- binding revoked on another process
|
|
- local route store restored from old backup
|
|
|
|
## Desktop To Lead Delivery
|
|
|
|
The desktop local commit should not directly mean "agent saw it".
|
|
|
|
Better state split:
|
|
|
|
```text
|
|
provider update accepted locally
|
|
-> local inbound message committed
|
|
-> route to lead/team inbox scheduled
|
|
-> inbox write committed
|
|
-> agent turn started
|
|
-> response captured
|
|
-> outbound provider delivery ledger
|
|
```
|
|
|
|
If inbox write fails after ACKing Telegram:
|
|
|
|
```text
|
|
The message is not lost because it is in MessengerDesktopInboundStore.
|
|
MessengerLocalDeliveryLedger can retry delivery to lead/team inbox.
|
|
```
|
|
|
|
This is the same reliability style as existing runtime delivery journals.
|
|
|
|
## Official Bot vs Own Bot Difference
|
|
|
|
Official shared bot default:
|
|
|
|
```text
|
|
Telegram sends webhooks to our backend.
|
|
Backend must decide quickly whether desktop accepted the update.
|
|
If desktop is offline, backend sends offline notice and ACKs Telegram.
|
|
No catch-up after offline notice.
|
|
```
|
|
|
|
Own bot local mode:
|
|
|
|
```text
|
|
Desktop can use getUpdates long polling directly.
|
|
If desktop is offline, Telegram may retain updates for up to 24 hours.
|
|
When desktop returns, it can catch up, because Telegram is the queue.
|
|
```
|
|
|
|
This means own-bot mode has a surprising reliability advantage:
|
|
|
|
```text
|
|
It can support Telegram-side catch-up without our backend storing plaintext.
|
|
```
|
|
|
|
But UX must say it clearly:
|
|
|
|
```text
|
|
Own bot can catch up recent Telegram updates while your computer was asleep, subject to Telegram retention.
|
|
Default Agent Teams bot replies offline instead of queueing by default.
|
|
```
|
|
|
|
## Technology Recommendation
|
|
|
|
### Desktop main process client
|
|
|
|
Use Node 22 native WebSocket client.
|
|
|
|
🎯 9 🛡️ 8 🧠 4 Approx change size: 700-1400 LOC
|
|
|
|
Why:
|
|
|
|
- No new dependency for desktop client.
|
|
- Node docs say v22.4.0 marked WebSocket stable.
|
|
- Full-duplex fits ACK/control messages.
|
|
|
|
### Backend WebSocket server
|
|
|
|
If backend is Node/Fastify, use `@fastify/websocket` 11.2.0.
|
|
|
|
🎯 8 🛡️ 8 🧠 5 Approx change size: 900-1800 LOC backend-side
|
|
|
|
Why:
|
|
|
|
- Aligns with existing Fastify stack style.
|
|
- Built on `ws@8`.
|
|
- Has TypeScript declarations.
|
|
|
|
Note:
|
|
|
|
```text
|
|
This dependency is for cloud/backend package, not necessarily this Electron app package.
|
|
```
|
|
|
|
### Fallback transport
|
|
|
|
Keep SSE + HTTPS ACK as an optional enterprise fallback.
|
|
|
|
🎯 7 🛡️ 7 🧠 6 Approx change size: +1200-2500 LOC after WebSocket protocol exists
|
|
|
|
Why:
|
|
|
|
- Some networks/proxies break WebSocket.
|
|
- SSE is easier to pass through HTTP infrastructure.
|
|
|
|
But:
|
|
|
|
```text
|
|
Do not implement fallback until WebSocket protocol semantics are stable.
|
|
Otherwise two transports will double the bug surface.
|
|
```
|
|
|
|
## Error Copy Policy
|
|
|
|
Telegram user-facing responses should be honest and short.
|
|
|
|
No desktop session:
|
|
|
|
```text
|
|
Agent Teams desktop is offline for this team. Open the app and resend your message.
|
|
```
|
|
|
|
Route disabled:
|
|
|
|
```text
|
|
This team is no longer connected to Telegram. Reconnect it in Agent Teams.
|
|
```
|
|
|
|
Delivery uncertain after retry budget:
|
|
|
|
```text
|
|
Agent Teams could not confirm delivery to desktop. Check the app or resend.
|
|
```
|
|
|
|
Unsupported media in MVP:
|
|
|
|
```text
|
|
This Telegram connection currently supports text only. Send the details as text.
|
|
```
|
|
|
|
Avoid:
|
|
|
|
```text
|
|
"Message delivered" before desktop commit ACK.
|
|
"Queued" in default mode.
|
|
"We will process this when online" in default mode.
|
|
```
|
|
|
|
## Security Rules
|
|
|
|
Relay authentication:
|
|
|
|
```text
|
|
desktop signs session start with install secret
|
|
backend issues short-lived relay session token
|
|
WebSocket uses WSS only
|
|
old session token cannot ACK after lease is stolen
|
|
ACK includes deliveryId and sessionId
|
|
```
|
|
|
|
Frame validation:
|
|
|
|
```text
|
|
max payload size for text MVP
|
|
strict JSON object shape
|
|
protocolVersion required
|
|
unknown frame types rejected
|
|
provider ids stored as strings
|
|
raw provider ids redacted in logs
|
|
```
|
|
|
|
Replay controls:
|
|
|
|
```text
|
|
providerUpdateId dedupe on backend metadata ledger
|
|
providerUpdateId dedupe on desktop local ledger
|
|
deliveryId unique per backend offer
|
|
payloadHash conflict detection
|
|
duplicate committed ACK path
|
|
```
|
|
|
|
## Edge Cases To Test
|
|
|
|
Webhook and ACK:
|
|
|
|
- Telegram webhook with valid secret token and active desktop returns 2xx only after desktop commit ACK.
|
|
- Telegram webhook with no active desktop sends offline notice and returns 2xx.
|
|
- Active desktop socket exists but heartbeat is stale, backend treats it offline.
|
|
- Desktop commits locally but ACK response is lost, retry returns duplicate committed.
|
|
- Desktop receives offer after `expiresAt`, rejects retryable or terminal by policy.
|
|
- Backend process crashes before returning 2xx, Telegram retries.
|
|
- Backend process crashes after returning 2xx but before metadata update, metadata repair handles it.
|
|
|
|
Ordering and duplicates:
|
|
|
|
- Same `update_id` delivered twice.
|
|
- Two updates for same route arrive concurrently.
|
|
- Out-of-order updates due to parallel webhook connections.
|
|
- Update id jumps after a long quiet period.
|
|
- Payload hash conflict for same update id.
|
|
|
|
Session lifecycle:
|
|
|
|
- Second desktop instance steals lease.
|
|
- Old session tries to ACK after lease stolen.
|
|
- Desktop reconnects with old route inventory hash.
|
|
- Binding revoked while socket is open.
|
|
- Route disabled while offer is in flight.
|
|
|
|
Local delivery:
|
|
|
|
- Desktop commits inbound message, then app crashes before writing team inbox.
|
|
- Local delivery ledger retries inbox write on restart.
|
|
- Inbox path locked temporarily.
|
|
- Team deleted after local commit.
|
|
- Lead process offline after local commit.
|
|
|
|
Privacy:
|
|
|
|
- Backend durable stores contain no plaintext message bodies in default mode.
|
|
- Logs redact raw Telegram ids and message text.
|
|
- Offline notice path does not persist message body.
|
|
- Metrics count event classes without payload.
|
|
|
|
Own bot:
|
|
|
|
- Desktop has no webhook and polls with `getUpdates`.
|
|
- Existing webhook on own bot is detected and not deleted silently.
|
|
- Desktop catches up updates after restart within Telegram retention window.
|
|
- Desktop handles updates older than local route creation as ignored.
|
|
|
|
## Decision Update
|
|
|
|
The feature should introduce:
|
|
|
|
```text
|
|
MessengerRelaySessionManager
|
|
MessengerRelayProtocol
|
|
MessengerBackendUpdateMetadataLedger
|
|
MessengerDesktopInboundStore
|
|
MessengerLocalDeliveryLedger
|
|
```
|
|
|
|
Recommended default:
|
|
|
|
```text
|
|
official shared bot
|
|
WebSocket desktop relay
|
|
local-commit ACK before Telegram 2xx
|
|
offline notice when no healthy desktop session
|
|
bounded Telegram retry only for ambiguous active-session failures
|
|
no durable plaintext backend queue
|
|
```
|
|
|
|
Main open uncertainty left after this pass:
|
|
|
|
```text
|
|
Should official shared bot use limited Telegram webhook retries for active-session ACK timeouts,
|
|
or always terminal-handle ambiguous timeouts with "delivery uncertain" and 2xx?
|
|
```
|
|
|
|
My current recommendation:
|
|
|
|
🎯 8 🛡️ 8 🧠 6 Approx change size: +500-1200 LOC
|
|
|
|
```text
|
|
Use limited retry deferral for active-session ACK timeouts only.
|
|
Never use retry deferral when there is clearly no healthy desktop session.
|
|
```
|
|
|
|
Reason:
|
|
|
|
```text
|
|
This recovers short reconnects and ACK-loss cases without turning default mode into a hidden queue.
|
|
```
|