feat: route reply-quotes of past answers back to their source session - #186
Open
loml13 wants to merge 1 commit into
Open
feat: route reply-quotes of past answers back to their source session#186loml13 wants to merge 1 commit into
loml13 wants to merge 1 commit into
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
A run's session is scoped to its chat (
scope = chatId, orchatId:threadIdfor 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 smalldisk-backed ledger stored next to
sessions.json(default<sessionsFile>.routes.json, mirroring the existing.catalog.jsonconvention). 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_idagainst 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_idthat'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
starting a blank one in chat B.
Compatibility
Fully backward compatible and opt-in:
startChannel({ messageRoutes }). Omit the store andnothing changes.
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.
recordnever throws,lookupreturns"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-writerentries 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 realstartChannelpipeline 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