Skip to content

Feature: multi-operator sessions — N whitelisted users, N independent session tapes, one bot token #215

Description

@vinceferro

The gap

The bot hard-codes a single consumer: TELEGRAM_ALLOWED_USER_ID is one integer, auth is one equality check, and one global currentSession pointer serves the process. Shared-machine orgs (co-founder, partner, builder) hit this immediately — the second operator is silently rejected:

[WARN] Unauthorized access attempt from user ID: 123456789 (redacted)

We run two production orgs in exactly that shape and had to solve it. Sharing one tape between humans turned out to be the wrong goal anyway: interleaved steering contexts confuse both the model and the people. The right primitive is N authorized operators × 1 bot token = N independent session tapes, each with its own /new, context, and notification stream.

Proposed design (implemented and battle-tested)

Three layers, ~150 lines total, zero changes to existing call sites:

  1. AllowlistTELEGRAM_ALLOWED_USER_IDS (comma-separated) absorbs the legacy single var. Derivable from an access file's allowFrom so there's one source of truth.
  2. Per-user session scope — an AsyncLocalStorage set in authMiddleware tags each update with its sender; the settings store resolves sessions through it (userSessions[userId] map, persisted). All ~70 getCurrentSession() call sites become per-user transparently.
  3. Ownership-aware event routing — server events arrive outside any user scope; delivery guards resolve the target chat via a session→chat binding (partial streams, completions, tool cards, permissions, questions, errors, background notifications).

Backward compatibility guarantee: a solo operator (allowlist length 1) adopts the legacy session on first scoped lookup — upgrading never orphans an existing conversation. Single-user deployments see zero behavior change.

Live-found pitfalls (from rolling this out)

  • Completion handlers comparing against the session pointer then clearing state and returning on mismatch produce silent drops with no error surfaced — optional-chaining variants of the guard evade naive sweeps.
  • Compact-mode (compactOutputMode: true) branches carry their own inner session check; fixing only the outer guard restores half the behavior.
  • Interactive turns use POST /session/{id}/prompt_async, not /message — debugging against the wrong route 'works' while production fails.

Happy to open a PR with our reference implementation if the shape interests you — otherwise this doc plus the design notes above may save the next shared-org user some pain.

Rollout addendum (live-deployed since posting)

Three more pitfalls surfaced while running this in production — each a silent failure with a healthy-looking process:

  1. Incomplete refactoring creates crash-loops in callbacks: removing a declaration while converting guards left orphaned references inside the permission/question callbacks. Every permission event then crashed its callback before rendering — so an agent asking for permission froze forever with zero visible errors until log diving. Rule: after guard surgery, grep for every remaining reference to any removed symbol; JS won't save you.
  2. Per-session flush keys: toolMessageBatcher.flushSession / toolCallStreamer.flushSession inside permission/question handlers were keyed off the (now legacy) session pointer instead of request.sessionID / the event's own session id. Same class as feat: add option to hide subsessions in sessions list #4 — ownership-aware routing must extend to every session-keyed helper call, not just send targets.
  3. Compact progress attribution: updateWaitingForPermission/updateThinking must attribute to the requesting session's own id, or secondary operators' status lands on the wrong lane's card.

Also now implemented: a backward-compat seed — solo operators (allowlist length 1) adopt the legacy session on first scoped lookup, so upgrading never orphans an existing conversation. Net effect for existing single-user deployments is zero behavior change.

Reference implementation available on request.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions