Skip to content

Carry Matrix threads in both directions, and read channel state back - #17

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

TroyHernandez merged 5 commits into
mainfrom
thread-relations

Conversation

@TroyHernandez

@TroyHernandez TroyHernandez commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Two additions chat.api needs before a threaded reply can be routed and rehydrated.

Threads, both directions. Inbound, chat_poll and chat_history map an event's m.relates_to onto chat_message$thread when its rel_type is m.thread. Only that rel_type counts — a rich reply carries an m.in_reply_to with no rel_type, and reading that as a thread would report every quoted reply in a room as one, which chat_message has no reply_to slot to undo afterwards. A threaded event carries an m.in_reply_to too (its reply fallback), so the test is rel_type rather than the relation's presence.

Outbound, chat_send passes thread through to mx_send_text, and the rich path builds the same relation itself since it deliberately bypasses that function. The argument is named only when a thread is given, so an mx.client predating it keeps working for ordinary replies instead of failing with an unused-argument error.

thread_replies is now TRUE: reading a relation needs nothing but the extractor field every supported mx.client carries. threads (the send side) is TRUE only when the installed mx.client can attach a relation (0.2.0.5, probed rather than assumed) AND the client is not e2ee — the encrypted send path has nowhere to put an m.relates_to, so a threaded send there is refused rather than posted to the room's main timeline. That is files' bargain rather than rich's: a reply that leaves its thread is misrouted, landing where nobody is looking and where a consumer keying state off the thread will never see it, not merely undecorated.

chat_get_state(). The counterpart of chat_set_state(), on the same set_state capability — a transport with durable channel state has both halves or neither. State never written reads as NULL rather than erroring, because "no marker here" is an ordinary answer for a caller checking whether one is set; a store that cannot be reached at all still errors, since that is a different fact. Threaded rehydration maps a thread root to the archive it stands for by reading an index the fold writes into the topic room's state, and with only a setter that lookup would have to drop below the transport seam.

The mx.client Suggests floor deliberately stays at 0.2.0.4: threaded sending degrades honestly through the capability probe, so forcing an upgrade on consumers that never thread would be wrong.

Second of three PRs wiring threaded replies up the stack. Depends on cornball-ai/mx.client#23 for the send half only; merges and passes independently of it (the probe reports FALSE until that lands). Four existing assertions that pinned the old "threads are unreachable" contract are updated to the new one, and the real-extractor integration test now round-trips a threaded event through the installed mx.client.

Inbound: chat_poll and chat_history map an event's m.relates_to onto
chat_message$thread when its rel_type is m.thread. Only that rel_type
counts -- a rich reply carries an m.in_reply_to with no rel_type, and
reading that as a thread would report every quoted reply as one, which
chat_message has no reply_to slot to undo. A threaded event carries an
m.in_reply_to too, as its fallback, so the test is rel_type and not
the relation's presence.

Outbound: chat_send passes thread through to mx_send_text, and the
rich path builds the same relation itself since it bypasses that
function. The argument is named only when there is a thread, so an
mx.client predating it keeps working for ordinary replies.

thread_replies is now TRUE. threads is TRUE when the installed
mx.client can attach a relation (0.2.0.5) and the client is not e2ee:
the encrypted send path has nowhere to put an m.relates_to, so a
threaded send there is refused rather than posted to the room's main
timeline -- files' bargain, not rich's, because a reply that leaves
its thread is misrouted rather than merely undecorated.
chat_get_state() is the counterpart of chat_set_state(), on the same
set_state capability: a transport with durable channel state has both
halves or neither. Matrix reads through mx.api::mx_get_state with an
injectable .get_state seam; loopback reads its keyed store.

State that was never written reads as NULL rather than erroring --
"no marker here" is an ordinary answer for a caller checking whether
one is set, and it should not need a handler to get it. A state store
that cannot be reached at all still errors, because that is a
different fact.

Needed by threaded rehydration, which maps a thread root to the
archive it stands for by reading an index the fold wrote into the
topic room's state; with only a setter that lookup would have to drop
below the transport seam.
@TroyHernandez TroyHernandez changed the title Carry Matrix threads in both directions Carry Matrix threads in both directions, and read channel state back Aug 20, 2026
@TroyHernandez
TroyHernandez merged commit 26db88d into main Aug 20, 2026
2 checks passed
@TroyHernandez
TroyHernandez deleted the thread-relations branch August 20, 2026 15:45
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