Skip to content

Continue a folded conversation from its thread - #178

Merged
TroyHernandez merged 5 commits into
mainfrom
thread-rehydrate
Aug 20, 2026
Merged

TroyHernandez merged 5 commits into
mainfrom
thread-rehydrate

Conversation

@TroyHernandez

Copy link
Copy Markdown
Contributor

Phase 4 of the conversation lifecycle: replying in a folded conversation's thread continues that conversation instead of starting a blank one.

Session keying. Sessions are keyed by room and thread root rather than room alone, so a room's main timeline and each of its threads are separate conversations. Without a thread the key is the room id exactly, so every existing room behaves as it did. The separator is \r, which neither a Matrix room id nor an event id can contain, so a thread's key cannot collide with a bare room id. Sessions now carry their own room_id, because the key of a thread is not a room id and the send target, the archive's source, and the room's tool scope all want the room.

Rehydration. The first message in a thread seeds its session from the archive that thread stands for. The mapping is a state event the fold writes into the topic room, keyed by the root event id (ai.cornball.fold/<root> → {segment, vault}), so the lookup is one state read; the alternative was scanning the topic room's history for the root event, which grows with the room and is exactly what the index exists to avoid. The transcript goes in tail-bounded (the last exchanges are the ones the next message answers, and the root message already carries the topic) and as a single framing exchange — replaying it turn by turn would mean parsing the archive's markdown back into roles, and a mis-parse puts words in the user's mouth, which is the one error a resumed conversation cannot survive.

Every failure on this path degrades rather than throws: a transport without durable state, a thread nobody folded into, an index pointing at an archive that is gone, or a homeserver that cannot answer all leave the session unseeded and the reply still happens. An unseeded reply is a worse conversation; a failed one is no conversation.

Replies stay in their thread, and only where the transport can route one — chat_send() refuses a threaded send it cannot deliver, so bot_reply_send() asks the capability first and falls back to the room's main timeline rather than failing a reply someone is waiting on. /clear inside a thread resets that thread's session and does not file a segment: a thread is already the segment of a conversation that ended once, and filing it again would make a segment room whose parent is a topic room.

Archival files under the session's room rather than its registry key, which for a thread is not a room id at all — pinned by a test, since the symptom would have been transcripts filed against rooms no homeserver has heard of.

Depends on cornball-ai/chat.api#17 (chat_get_state + thread support); the Suggests floor and .CHAT_API_MIN both move to 0.0.1.23, so CI here fails until that merges. Two existing test doubles for bot_reply_send gain ... — their old fixed signature turned the new thread argument into an unused-argument error that the call site's tryCatch swallowed, which is how the suite caught it.

Third of three PRs wiring threaded replies up the stack (mx.client#23 → chat.api#17 → this). The cerebro side that writes the fold index is a separate PR.

Sessions are keyed by room and thread root rather than room alone, so
a room's main timeline and each of its threads are separate
conversations. Without a thread the key is the room id exactly, which
is what keeps every existing room behaving as it did.

The first message in a thread seeds its session from the archive the
thread stands for: a folded conversation's root is indexed by the fold
as ai.cornball.fold state in the topic room, keyed by the root event
id, so the lookup is one state read rather than a scan of the topic
room's history. The transcript goes in tail-bounded and as one framing
exchange -- replaying it turn by turn would mean parsing the archive's
markdown back into roles, and a mis-parse puts words in the user's
mouth.

Replies go back into the thread they answer, and only where the
transport can carry one: chat_send() refuses a threaded send it cannot
route, so a reply in an encrypted room falls back to the room rather
than failing outright. Archival files under the session's room, not
its registry key, which for a thread is not a room id at all.

Requires chat.api >= 0.0.1.23 for chat_get_state() and thread
support.
Startup backfill keyed every message by room, so a restart put each
thread's history into the room's main-timeline session -- polluting
unrelated topic context -- and left the thread sessions themselves
empty, losing the recent context of active threads. It now resolves a
session per message with bot_session_key(), the same way the live path
does.

Rehydration is gated on a flag the session carries rather than on
having just been created. Backfill can build a thread's session before
its first live message ever arrives, and the freshness gate meant that
session was never seeded: a restart answered an active thread from the
backfilled tail alone, with the conversation it continues lost. The
flag is set whatever the lookup returns, so a thread nobody folded
into costs one state read rather than one per message, and a
rehydration that throws is contained and still marks the session.
The new tests construct sessions for real, which runs session_setup(),
which refuses a provider whose key is absent. On anthropic that passed
locally off a key in the environment and failed in CI, where there is
none -- so the provider is now ollama, for which there is no key to
check. Nothing in these tests reaches a model.
@TroyHernandez
TroyHernandez merged commit b85b67b into main Aug 20, 2026
2 checks passed
@TroyHernandez
TroyHernandez deleted the thread-rehydrate branch August 20, 2026 16:53
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