Repository navigation
Continue a folded conversation from its thread - #178
Merged
Merged
Conversation
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.
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.
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 ownroom_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./clearinside 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_MINboth move to 0.0.1.23, so CI here fails until that merges. Two existing test doubles forbot_reply_sendgain...— their old fixed signature turned the newthreadargument 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.