agent-ecosystem/docs/research/messenger-connectors-uncertainty-pass-27.md
2026-04-28 21:03:21 +03:00

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:

  • getUpdates update 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_enabled and User.allows_users_to_create_topics are returned only by getMe.
  • 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_message is only for replies in the same chat and message thread.
  • external_reply can come from another chat or forum topic and must not be used for teammate routing.
  • MTProto send errors include TOPIC_CLOSED and TOPIC_DELETED; Bot API adapter should classify equivalent provider failures into typed sanitized errors.

Local code facts checked:

  • TeamConfig has name, description, color, members, projectPath, leadSessionId, deletedAt, but no public stable teamId.
  • TeamChangeEvent does not include delete, restore, permanent-delete or rename event types.
  • deleteTeam soft-deletes by writing deletedAt into config.json.
  • restoreTeam removes deletedAt.
  • permanentlyDeleteTeam removes team and task dirs.
  • Team backup has private identityId and writes _backupIdentityId into config as a backup guard, but this is not a product-level team identity.
  • Many runtime paths use teamName as the runtime/team id.

Sources:

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:

  1. Add feature-owned messengerTeamIdentityId registry keyed by current teamName and backup marker if available - 🎯 8 🛡️ 8 🧠 5, approx 700-1400 LOC.

    • Does not require changing global TeamConfig schema immediately.
    • Gives messenger routes stable identity.
    • Can reconcile with _backupIdentityId but does not depend on it.
  2. Promote a stable teamId into TeamConfig globally - 🎯 7 🛡️ 9 🧠 8, approx 1800-4000 LOC.

    • Best long-term domain model.
    • Larger migration blast radius because many services assume teamName.
  3. Keep teamName only 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:

  1. 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.
  2. Emit richer domain events from TeamDataService and subscribe in messenger feature - 🎯 8 🛡️ 9 🧠 7, approx 1200-2500 LOC.

    • Cleaner long-term.
    • Wider refactor.
  3. 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:

  1. Use agentId when present, otherwise member name plus memberRouteGeneration - 🎯 8 🛡️ 8 🧠 5, approx 700-1500 LOC.

    • Fits current data.
    • Avoids blocking MVP on member schema migration.
  2. Add stable memberId to every member and migrate roster stores - 🎯 7 🛡️ 9 🧠 8, approx 1800-4000 LOC.

    • Best long-term.
    • Larger blast radius.
  3. 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.

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:

  1. Feature-owned sanitized diagnostic DTOs plus tests - 🎯 9 🛡️ 9 🧠 5, approx 700-1500 LOC.
  2. Generic logger wrapper only - 🎯 6 🛡️ 6 🧠 4, approx 400-900 LOC.
  3. 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

  1. Cross-client Telegram private topic UX - 🎯 5 🛡️ 8 🧠 6.

    • Requires live probe.
    • Design is resilient because of account-level confirmation and fallback.
  2. Stable local team identity for external routes - 🎯 6 🛡️ 8 🧠 6.

    • Current app is name-keyed.
    • Needs a messenger-owned identity registry before route activation.
  3. Member identity for delayed teammate replies - 🎯 6 🛡️ 8 🧠 6.

    • Current member names can be reused.
    • Store agentId and member generation snapshots.
  4. Lifecycle ordering on permanent delete - 🎯 7 🛡️ 9 🧠 6.

    • Policy is clear.
    • Needs command-side hook, not watcher inference.
  5. Outbound ambiguous Telegram sends - 🎯 7 🛡️ 9 🧠 6.

    • Technical state is clear: acceptance_unknown.
    • UX still needs concise wording.
  6. 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:

  1. Messenger identity registry and route generation policy - 🎯 9 🛡️ 9 🧠 6, approx 1000-2200 LOC.
  2. ProviderMessageLink repository and reply route resolver - 🎯 9 🛡️ 9 🧠 6, approx 1200-2600 LOC.
  3. Team lifecycle facade hooks around delete/restore/permanent delete/updateConfig - 🎯 8 🛡️ 9 🧠 6, approx 900-1800 LOC.
  4. 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.