12 KiB
Messenger Connectors - Uncertainty Pass 27
Date: 2026-04-28
Scope: remaining low-confidence areas after topic capability design
Context source: previous architecture worktree doc at /Users/belief/dev/projects/claude/_worktrees/claude_team_messenger_connectors/docs/messenger-connectors-architecture.md
Executive Delta
The highest risk is no longer "can Telegram topics work at all". The design now has proof and fallback paths.
The next real risk is identity and lifecycle:
Telegram topic route -> team identity -> member identity -> message identity
Current app code is mostly keyed by teamName. That is workable for UI, but risky for messenger routes because external provider state can outlive local team folders.
Source Facts Rechecked
Telegram official docs checked on 2026-04-28:
getUpdatesupdate ids are useful for ignoring repeated webhook or polling updates.- Telegram stores incoming updates only until the bot receives them, and not longer than 24 hours.
- Webhooks retry on non-2xx responses.
User.has_topics_enabledandUser.allows_users_to_create_topicsare returned only bygetMe.- Bot API 9.4 allowed bots to create topics in private chats and allowed bots to prevent users from creating/deleting topics through BotFather Mini App.
reply_to_messageis only for replies in the same chat and message thread.external_replycan come from another chat or forum topic and must not be used for teammate routing.- MTProto send errors include
TOPIC_CLOSEDandTOPIC_DELETED; Bot API adapter should classify equivalent provider failures into typed sanitized errors.
Local code facts checked:
TeamConfighasname,description,color,members,projectPath,leadSessionId,deletedAt, but no public stableteamId.TeamChangeEventdoes not include delete, restore, permanent-delete or rename event types.deleteTeamsoft-deletes by writingdeletedAtintoconfig.json.restoreTeamremovesdeletedAt.permanentlyDeleteTeamremoves team and task dirs.- Team backup has private
identityIdand writes_backupIdentityIdinto config as a backup guard, but this is not a product-level team identity. - Many runtime paths use
teamNameas the runtime/team id.
Sources:
- https://core.telegram.org/bots/api
- https://core.telegram.org/bots/api-changelog
- https://core.telegram.org/method/messages.sendMessage
1. Team Identity Gap
Messenger routes must not be keyed only by teamName.
Danger scenario:
1. User connects Telegram topic to teamName="frontend".
2. User permanently deletes the team.
3. User later creates a new unrelated team with the same teamName="frontend".
4. Old Telegram topic receives a message.
5. If route is keyed only by teamName, message can route to the new unrelated team.
This is worse than a normal UI cache bug because Telegram routes are external and long-lived.
Top 3 team identity options:
-
Add feature-owned
messengerTeamIdentityIdregistry keyed by currentteamNameand backup marker if available - 🎯 8 🛡️ 8 🧠 5, approx 700-1400 LOC.- Does not require changing global
TeamConfigschema immediately. - Gives messenger routes stable identity.
- Can reconcile with
_backupIdentityIdbut does not depend on it.
- Does not require changing global
-
Promote a stable
teamIdintoTeamConfigglobally - 🎯 7 🛡️ 9 🧠 8, approx 1800-4000 LOC.- Best long-term domain model.
- Larger migration blast radius because many services assume
teamName.
-
Keep
teamNameonly and rely on tombstones - 🎯 5 🛡️ 6 🧠 3, approx 400-900 LOC.- Fast.
- Still fragile when tombstones are pruned or route state is restored from backup.
Recommendation:
Use option 1 for messenger MVP.
Design it so global TeamConfig.teamId can replace it later.
Suggested identity record:
type MessengerTeamIdentityRecord = {
messengerTeamIdentityId: string;
currentTeamName: string;
observedDisplayName: string;
backupIdentityId?: string;
firstSeenAt: string;
lastSeenAt: string;
state:
| "active"
| "soft_deleted"
| "restored_requires_reconnect"
| "permanently_deleted"
| "name_reused_different_identity";
};
Route binding should store both:
teamNameSnapshot
messengerTeamIdentityId
routeGeneration
The runtime delivery adapter can still call existing services by teamName, but only after the identity registry confirms that the route still points to the current team folder.
2. Lifecycle Hooks Need Command-Side Events
File watcher events are not enough for messenger routes.
Why:
- Soft delete and restore are command intents, not just file changes.
- Permanent delete removes files before a watcher can read useful context.
- Connector cleanup must run before or during destructive operations.
- Renderer-only refresh events cannot protect background delivery.
Required main-process lifecycle port:
type MessengerTeamLifecyclePort = {
beforeSoftDeleteTeam(input: { teamName: string }): Promise<void>;
afterSoftDeleteTeam(input: { teamName: string; deletedAt: string }): Promise<void>;
beforeRestoreTeam(input: { teamName: string }): Promise<void>;
afterRestoreTeam(input: { teamName: string }): Promise<void>;
beforePermanentDeleteTeam(input: { teamName: string; deleteLocalConnectorPlaintext: boolean }): Promise<void>;
afterPermanentDeleteTeam(input: { teamName: string }): Promise<void>;
afterTeamConfigChanged(input: { teamName: string; previousDisplayName: string; nextDisplayName: string }): Promise<void>;
};
Top 3 integration points:
-
Call messenger facade directly from team IPC handlers around delete/restore/updateConfig - 🎯 8 🛡️ 9 🧠 6, approx 900-1800 LOC.
- Strong command ordering.
- Easy to test with mocked facade.
-
Emit richer domain events from
TeamDataServiceand subscribe in messenger feature - 🎯 8 🛡️ 9 🧠 7, approx 1200-2500 LOC.- Cleaner long-term.
- Wider refactor.
-
Infer lifecycle from file watcher and config scans - 🎯 5 🛡️ 6 🧠 4, approx 600-1200 LOC.
- Too late for permanent delete.
- Race-prone.
Recommendation:
Use option 1 first.
Keep the facade shape compatible with option 2 later.
3. Member Identity Gap
Team members are also name-keyed.
Risk:
1. Telegram bot sends a teammate message from "Alex".
2. User replies to that bot message later.
3. Meanwhile "Alex" was removed and a different member with same name was added.
4. Reply may route to the wrong teammate unless the message link stores member generation.
Minimum route target identity:
type MessengerRouteTarget =
| { kind: "lead"; teamIdentityId: string; leadSessionId?: string | null }
| {
kind: "teammate";
teamIdentityId: string;
memberNameSnapshot: string;
memberAgentIdSnapshot?: string;
memberRouteGeneration: number;
};
Top 3 member identity strategies:
-
Use
agentIdwhen present, otherwise member name plusmemberRouteGeneration- 🎯 8 🛡️ 8 🧠 5, approx 700-1500 LOC.- Fits current data.
- Avoids blocking MVP on member schema migration.
-
Add stable
memberIdto every member and migrate roster stores - 🎯 7 🛡️ 9 🧠 8, approx 1800-4000 LOC.- Best long-term.
- Larger blast radius.
-
Use member display name only - 🎯 5 🛡️ 5 🧠 2, approx 200-600 LOC.
- Too weak for delayed Telegram replies.
Recommendation:
Use option 1 in MVP.
Store target snapshots in every ProviderMessageLink.
4. ProviderMessageLink Must Be A Contract, Not Cache
The link is the most important durable object in the feature.
Recommended shape:
type ProviderMessageLink = {
linkId: string;
provider: "telegram";
accountBindingId: string;
routeId: string;
routeGeneration: number;
providerChatId: string;
providerThreadId: string | null;
providerMessageId: string;
internalMessageId: string;
internalMessageKind:
| "messenger_inbound"
| "lead_reply"
| "teammate_reply"
| "system_notice"
| "topic_probe";
origin:
| "provider_user"
| "team_lead"
| "team_teammate"
| "connector_system";
target: MessengerRouteTarget;
createdAt: string;
expiresAt?: string;
};
Rules:
- Never trim links only because UI messages were trimmed.
- Links for route targets should outlive
sentMessages.json. - Links for topic probes can have short TTL.
- Links from tombstoned routes should remain as tombstones long enough to block stale replies.
5. Reply Routing Should Be Two-Phase
Do not immediately turn a Telegram reply into a teammate message.
Phase 1 - resolve anchor:
reply_to_message.message_id -> ProviderMessageLink
same chat id?
same thread id?
same account binding?
same route generation?
link target still valid?
Phase 2 - route message:
valid teammate target -> teammate inbox
valid lead target -> lead
missing/stale target -> lead with context
tombstoned route -> reject with reconnect notice
unknown topic -> help flow
Critical rule:
external_reply must never route to a teammate.
Bot API explicitly distinguishes same-thread reply_to_message from external_reply, so adapter normalization must preserve that distinction.
6. Privacy Risk Shift
After the no-plaintext-queue decision, the main privacy risk is not storage. It is accidental logging and diagnostic capture.
High-risk payloads:
Telegram update JSON
callback_query data if it embeds route ids
Bot API error description if request URL/token leaks through HTTP client
message text in failed sends
team display names in topic titles
member names in projected message prefixes
Top 3 diagnostic strategies:
- Feature-owned sanitized diagnostic DTOs plus tests - 🎯 9 🛡️ 9 🧠 5, approx 700-1500 LOC.
- Generic logger wrapper only - 🎯 6 🛡️ 6 🧠 4, approx 400-900 LOC.
- Rely on "do not log raw errors" convention - 🎯 3 🛡️ 3 🧠 1, 0 LOC.
Recommendation:
Use option 1.
Also add Sentry beforeSend scrubbing as defense in depth.
7. Current Lowest-Confidence Map
-
Cross-client Telegram private topic UX - 🎯 5 🛡️ 8 🧠 6.
- Requires live probe.
- Design is resilient because of account-level confirmation and fallback.
-
Stable local team identity for external routes - 🎯 6 🛡️ 8 🧠 6.
- Current app is name-keyed.
- Needs a messenger-owned identity registry before route activation.
-
Member identity for delayed teammate replies - 🎯 6 🛡️ 8 🧠 6.
- Current member names can be reused.
- Store
agentIdand member generation snapshots.
-
Lifecycle ordering on permanent delete - 🎯 7 🛡️ 9 🧠 6.
- Policy is clear.
- Needs command-side hook, not watcher inference.
-
Outbound ambiguous Telegram sends - 🎯 7 🛡️ 9 🧠 6.
- Technical state is clear:
acceptance_unknown. - UX still needs concise wording.
- Technical state is clear:
-
Flat menu fallback correctness - 🎯 8 🛡️ 8 🧠 6.
- Good fallback.
- Needs strict selection lease tests to avoid wrong-team delivery.
8. Revised Next Slice
Before building UI, implement/test these core pieces:
- Messenger identity registry and route generation policy - 🎯 9 🛡️ 9 🧠 6, approx 1000-2200 LOC.
- ProviderMessageLink repository and reply route resolver - 🎯 9 🛡️ 9 🧠 6, approx 1200-2600 LOC.
- Team lifecycle facade hooks around delete/restore/permanent delete/updateConfig - 🎯 8 🛡️ 9 🧠 6, approx 900-1800 LOC.
- Telegram topic live probe fixtures - 🎯 9 🛡️ 9 🧠 5, approx 700-1500 LOC.
This is the point where the design becomes robust against the bugs most likely to happen months later, not only during the happy-path onboarding demo.