Skip to main content
Guides Last updated: 8 October 2026

Routing & sessions

Nathan Schram
By Founder, Little Bear Apps

Untether supports both stateless and chat modes for session handling. In stateless mode, each message starts a new session unless you reply to continue. In chat mode, new messages auto-resume the previous session.

Continuation (how threads persist)

Untether supports four ways to continue a thread:

  1. Reply-to-continue (always available)
    • Reply to any bot message that contains a resume line in the footer.
    • Untether extracts the resume token and resumes that engine thread.
    • Reply resume lines always take precedence over chat sessions or topic storage.
    • The resumed run updates the stored session for that engine when the token is known.
  2. Forum topics (optional)
    • Topics can store resume tokens per topic and auto-resume new messages in that topic.
    • Topic state is stored in telegram_topics_state.json.
    • Reset with /new.
  3. Chat sessions (optional)
    • Set session_mode = "chat" to store one resume token per chat (per sender in groups; per topic in a private chat with topics).
    • Stored sessions are per engine; resuming a different engine does not overwrite others.
    • State is stored in telegram_chat_sessions_state.json.
    • Reset with /new.
  4. Cross-environment resume (/continue)
    • Resume the most recent session in the project directory, regardless of where it was started.
    • Useful for picking up a CLI session (iTerm, tmux, mosh) from Telegram while away from the terminal.
    • Uses each engine’s native “continue” flag (--continue, resume --last, --resume latest).
    • Works with Claude, Codex, OpenCode and Pi (and the deprecated Gemini CLI). Not supported for AMP. See the cross-environment resume guide.

Reply-to-continue works even if topics or chat sessions are enabled.

Listen mode (pre-routing filter)

Before routing, Untether checks the chat’s listen mode (renamed from “trigger mode” in v0.35.3 — #297). In mentions mode, messages that don’t @mention the bot, reply to the bot, or start with a known slash command are silently dropped — they never reach the router. In the default all mode, every message passes through.

Listen mode is configured per chat via /listen (or the deprecated /trigger alias) or /config, with optional per-topic overrides in forum groups. See Group chat for details.

Routing (how Untether picks a runner)

For each message, Untether:

  • parses directive prefixes (/<engine-id>, /<project-alias>, @branch) from the first non-empty line
  • attempts to extract a resume token by polling available runners
  • if a resume token is found, routes to the matching runner; otherwise uses the most specific default engine (topic → chat → project → global)
  • after routing, appends the replied-to message (or a selected quote) to the prompt as bounded, escaped reference data, so reply text can never change the engine, project or session (#736)

Serialisation (why you don’t get overlapping runs)

Untether allows parallel runs across different threads, but enforces serialisation within a thread:

  • Telegram side: jobs are queued FIFO per thread. Prompts sent within about a second of each other are merged into one job first, and /cancel, /new and /continue drop anything still waiting in that window.
  • Runner side: runners enforce per-resume-token locks (so the same session can’t be resumed concurrently). A /continue run locks the real session id it resumes, not a shared placeholder (#817).

Claude Code’s live sessions (v0.36.0) keep this guarantee without a new process per turn: a queued follow-up for a session whose process is still open is written into that process once its current turn ends, rather than waiting for it to exit and resuming. A steered message is the one exception to “one turn at a time”: it is written into the running turn on purpose. See Architecture → Live sessions.

The precise invariants are specified in the Specification.

Was this helpful?

Related Articles