Skip to content

Read invitations through the contract; res$raw goes unread - #172

Merged
TroyHernandez merged 1 commit into
mainfrom
feat/invites-consumer
Aug 7, 2026
Merged

TroyHernandez merged 1 commit into
mainfrom
feat/invites-consumer

Conversation

@TroyHernandez

Copy link
Copy Markdown
Contributor

Blocked on cornball-ai/chat.api#8, which is green on Ubuntu and stuck behind a GitHub Actions outage on macOS (Failed to resolve action download info: Service Unavailable, three runs, before any step). This PR's CI will fail its version gate until #8 merges — by construction, as usual.

The poll loop reads chat_poll()$invites and joins with chat_join().

That was the last $raw consumer. grep 'res\$raw' R/matrix.R in matrix_poll() now finds only the comment saying it goes unread. Every message, decrypted message, reaction and invitation arrives as a record — the migration plan's stated finish line for phase 2.

What moved and why it could

matrix_invite_inviters() is deleted. Reading who sent an invitation meant walking the stripped invite_state, which put Matrix sync-shape knowledge two packages away from the sync. mx.client owns that now and chat_invite() carries the sender — which is the only reason this phase removed the reach-through instead of relocating it.

The gate reports better

An invitation from someone not on the operator list, and one whose sender the homeserver never named, both refuse. They always did. The log now distinguishes them:

matrix: refusing invite to !bad:ex from @jorge:ex (not an operator)
matrix: refusing invite to !blank:ex from an unknown sender (not an operator)

"Someone I do not trust" and "a question that went unanswered" are different things to whoever reads that at 3am.

One bad invite no longer risks the rest

chat_join() raises, so each failure is visible; matrix_accept_invites() catches per room. A revoked invitation, or a room the bot was kicked from between the sync and the join, cannot take the poll and the rooms beside it down with it.

What deliberately stays on a raw sync

The startup catch-up in matrix_run_init(). It needs a full no-since sync to see invitations issued while the bot was down, and chat_poll() only ever resumes from a cursor — that is not expressible in the contract yet, and belongs with the backfill design phase. It goes through the same gate, so the operator policy is still written exactly once.

Testing

2783 assertions. Three mutations, all caught: accepting an invitation whose sender is unknown, bypassing the operator gate entirely, and letting one failed join abort the rest.

Floors: chat.api >= 0.0.1.14, mx.client >= 0.2.0.4, both enforced at runtime.

The poll loop takes chat_poll()$invites and joins with chat_join(). That
was the last thing it read off the raw sync response, so the migration
reaches its stated finish line: every message, decrypted message,
reaction and invitation now arrives as a record, and grep for res$raw in
matrix_poll() finds only the comment saying so.

matrix_invite_inviters() is deleted. Reading who sent an invitation meant
walking the stripped invite_state, which put Matrix sync-shape knowledge
two packages away from the sync. mx.client owns it now and the record
carries it, which is the only reason this phase removed the reach-through
rather than moving it.

The operator gate keeps its behaviour and reports better. An invitation
from someone not on the list and one whose sender the homeserver never
named both refuse -- they always did -- but the log now says which,
because "someone I do not trust" and "a question that went unanswered"
are different things to whoever reads it at 3am.

One invitation that cannot be joined no longer risks the others.
chat_join() raises so each failure is visible; matrix_accept_invites()
catches per room, so a revoked invitation cannot take the poll and the
rooms beside it down too.

The startup catch-up still reads a raw sync, deliberately. It needs a
full no-since sync to see invitations issued while the bot was down, and
chat_poll() only ever resumes from a cursor, so it stays until the
contract grows a verb for that. It goes through the same gate, so the
operator policy is still written once.

2783 assertions. Three mutations, all caught: accepting an invitation
whose sender is unknown, bypassing the operator gate entirely, and
letting one failed join abort the rest.
@TroyHernandez
TroyHernandez merged commit b82584a into main Aug 7, 2026
2 of 4 checks passed
@TroyHernandez
TroyHernandez deleted the feat/invites-consumer branch August 7, 2026 15:32
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