Skip to content

feat: route reply-quotes of past answers back to their source session - #186

Open
loml13 wants to merge 1 commit into
zarazhangrui:mainfrom
loml13:feat/quote-reply-session-routing
Open

feat: route reply-quotes of past answers back to their source session#186
loml13 wants to merge 1 commit into
zarazhangrui:mainfrom
loml13:feat/quote-reply-session-routing

Conversation

@loml13

@loml13 loml13 commented Jul 12, 2026

Copy link
Copy Markdown

Motivation

A run's session is scoped to its chat (scope = chatId, or chatId:threadId
for topics). That means a conversation lives and dies in the chat it started
in: there's no way to pick a past answer back up from somewhere else. A common
want is "reply-quote something the bot told me earlier — even in another chat —
and keep talking to that conversation."

This PR adds an opt-in mechanism for exactly that: quote one of the bot's
earlier replies and the new run resumes the session that produced it.

Change

  • New MessageRouteStore (src/session/message-routes.ts) — a small
    disk-backed ledger stored next to sessions.json (default
    <sessionsFile>.routes.json, mirroring the existing .catalog.json
    convention). It maps an outbound message_id{ scope, sessionId, cwd, ts }.
    Disk is the source of truth (each op reads the file fresh), so the bridge is
    not the only permitted writer: external notification tools that send
    messages "as" the bot through their own path can append their own entries to
    the same JSON object to make those messages reply-quote-routable too. This is
    a documented extension point (see the class doc-comment). Entries are bounded
    (LRU by ts, default 1000).

  • Outbound registration — when a run sends its final reply, the bridge
    records that reply's message_id against the run's (possibly already-routed)
    scope, together with the session id / cwd the run used.

  • Inbound routing — on intake, if a message reply-quotes a message_id
    that's in the ledger, the run's scope is overridden to the recorded scope.
    Because cwd resolution and session resume are both derived from the scope
    downstream, overriding the scope reconnects the run to the source
    conversation — the reply still lands in the chat the user wrote in, but the
    agent continues the quoted session.

Use case

  1. Ask the bot something in chat A; it answers.
  2. Later, in chat B (or a DM), reply-quote that answer and ask a follow-up.
  3. The follow-up resumes chat A's session (same session id + cwd) instead of
    starting a blank one in chat B.

Compatibility

Fully backward compatible and opt-in:

  • The feature is gated on startChannel({ messageRoutes }). Omit the store and
    nothing changes.
  • Routing only ever fires on a ledger hit for a quoted message. Messages
    with no reply-quote, and quotes of messages that aren't in the ledger, take
    the exact same path as before — the non-quote path is untouched.
  • The whole thing is best-effort: record never throws, lookup returns
    "no route" on a missing/corrupt ledger, and any error falls back to the
    normal per-chat scope. A broken ledger degrades to today's behavior, it can't
    break message handling.

Testing

  • pnpm typecheck — clean.
  • pnpm test — full suite green (adds 6 tests).
  • tests/unit/session/message-routes.test.ts — record/lookup, external-writer
    entries surviving a subsequent bridge write, LRU eviction, and graceful
    degradation on a corrupt/missing/malformed ledger.
  • tests/integration/bot/quote-route.test.ts — drives the real startChannel
    pipeline across two chats: a reply-quote of chat A's answer sent in chat B
    resumes chat A's session (sessionId + cwd), while a plain (non-quote)
    message in chat B starts its own session (no routing).

🤖 Generated with Claude Code

…ssion

Add an optional message-route ledger so a user can continue a past
conversation from any chat by reply-quoting one of the bot's earlier
answers.

How it works:
- A new `MessageRouteStore` (`<sessionsFile>.routes.json`, next to
  `sessions.json`) maps an outbound `message_id` → `{scope, sessionId,
  cwd, ts}`. Disk is the source of truth (read fresh per op), so it is
  not the bridge's exclusive writer: external notification tools that
  send messages "as" the bot can append their own entries to make those
  messages reply-quote-routable too. Entries are bounded (LRU by `ts`).
- On each reply, the bridge records the sent `message_id` against the
  run's (possibly already-routed) scope.
- On intake, if an inbound message reply-quotes a recorded `message_id`,
  the run's scope is overridden to the recorded scope; cwd and session
  resume follow from the scope downstream, reconnecting the run to the
  source conversation.

Compatibility: the feature is gated on `startChannel({ messageRoutes })`.
When the store is absent, or a quote hits no ledger entry, or any ledger
operation fails, routing is skipped and behavior is identical to before
— non-quote messages take an unchanged path. `MessageRouteStore.record`
never throws.

Tests: unit coverage for the ledger (record/lookup, external-writer
survival, LRU eviction, corrupt-file tolerance) and an end-to-end test
driving two runs across two chats (a quote resumes the source session; a
plain message starts its own).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant