Matrix mouth: m.read follows a chat_read report, presence follows the turn #35

Merged
heimdall merged 4 commits from heimdall/mouth-read-through into main 2026-11-10 17:32:03 +00:00
Member

Do not merge (Persona's brief). Mouth half of "N following the conversation reflects real reading"; the Huginn half is a separate change (below).

What was wrong. ack() posted m.read (tools/persona-matrix-mouth.mjs, old readUpTo), so a receipt tracked the queue, not the model. Huginn acks only after a turn answered (crates/mind-body/src/persona.rs:1640-1665), so receipts also came late and, for an unanswered batch, not at all; presence followed only the brake, so all Personas showed online.

This PR

  • New gamecult.persona.chat_read.v1 = [persona, room_alias, event_id, at_unix_ms] (persona-chat-documents.mjs, readTuple/parseRead), accepted by the RUDP endpoint; handled fire-and-forget so the peer's next request never waits on Matrix.
  • mouth.read() posts m.read only at the reported event id, only for an event this mouth delivered to that Persona (bounded index, 4000 ids), in the room it was delivered for, forward only per room, brake read fresh; returns the reason when it posts nothing (UnknownEvent, RoomMismatch, Behind, Brake, Failed).
  • ack() no longer posts anything.
  • Presence: online within presenceGraceMs (default 5 min) of a read report, a send or an ack; unavailable after; offline under the brake. /sync carries the state and a change is PUT at once.
  • docs/bridge.md updated.

Tests (tests/persona-matrix-mouth.test.mjs): node --test tests/persona-matrix-mouth*.test.mjs = 81 pass, 1 fail (the runtime file, fails to load on main too: CultLib/packages/cultcache-ts/dist/index.js absent). Tests that fail if a receipt fires before the model has seen the event: delivery/pending/ack post none; read posts exactly the reported id (not the room's newest); unknown event and wrong room post none; no backward or repeat; none under the brake, nothing queued. Five hand mutants killed (no delivered check; ack posts a receipt; no forward-only; no brake check; grace ignored). The endpoint test in persona-matrix-mouth-runtime.test.mjs is updated but could not run here (same missing build); a golden fixture for chat_read is not added.

Huginn half (not in this PR). echoed() (persona.rs:~1596-1606) is the moment a written batch is in the model's input and already records held[room] = (seq, event_id). Sending chat_read {persona, room_alias, event_id, at} there, once per room per echo, is the whole report; chat_client.rs needs ChatRead beside ChatAck (:24, :82). Deploy order: mouth first leaves receipts silent and presence unavailable until Huginn reports; a long tool-only turn with no send/ack/read past 5 minutes shows unavailable (Huginn could repeat the report per echoed batch, or the grace is raised).

🤖 Generated with Claude Code

**Do not merge** (Persona's brief). Mouth half of "N following the conversation reflects real reading"; the Huginn half is a separate change (below). **What was wrong.** `ack()` posted `m.read` (`tools/persona-matrix-mouth.mjs`, old `readUpTo`), so a receipt tracked the queue, not the model. Huginn acks only after a turn answered (`crates/mind-body/src/persona.rs:1640-1665`), so receipts also came late and, for an unanswered batch, not at all; presence followed only the brake, so all Personas showed online. **This PR** - New `gamecult.persona.chat_read.v1` = `[persona, room_alias, event_id, at_unix_ms]` (`persona-chat-documents.mjs`, `readTuple`/`parseRead`), accepted by the RUDP endpoint; handled fire-and-forget so the peer's next request never waits on Matrix. - `mouth.read()` posts `m.read` only at the reported event id, only for an event this mouth delivered to that Persona (bounded index, 4000 ids), in the room it was delivered for, forward only per room, brake read fresh; returns the reason when it posts nothing (`UnknownEvent`, `RoomMismatch`, `Behind`, `Brake`, `Failed`). - `ack()` no longer posts anything. - Presence: `online` within `presenceGraceMs` (default 5 min) of a read report, a send or an ack; `unavailable` after; `offline` under the brake. `/sync` carries the state and a change is PUT at once. - `docs/bridge.md` updated. **Tests** (`tests/persona-matrix-mouth.test.mjs`): `node --test tests/persona-matrix-mouth*.test.mjs` = 81 pass, 1 fail (the runtime file, fails to load on main too: `CultLib/packages/cultcache-ts/dist/index.js` absent). Tests that fail if a receipt fires before the model has seen the event: delivery/pending/ack post none; read posts exactly the reported id (not the room's newest); unknown event and wrong room post none; no backward or repeat; none under the brake, nothing queued. Five hand mutants killed (no delivered check; ack posts a receipt; no forward-only; no brake check; grace ignored). The endpoint test in `persona-matrix-mouth-runtime.test.mjs` is updated but could not run here (same missing build); a golden fixture for `chat_read` is not added. **Huginn half (not in this PR).** `echoed()` (`persona.rs:~1596-1606`) is the moment a written batch is in the model's input and already records `held[room] = (seq, event_id)`. Sending `chat_read {persona, room_alias, event_id, at}` there, once per room per echo, is the whole report; `chat_client.rs` needs `ChatRead` beside `ChatAck` (`:24`, `:82`). Deploy order: mouth first leaves receipts silent and presence `unavailable` until Huginn reports; a long tool-only turn with no send/ack/read past 5 minutes shows `unavailable` (Huginn could repeat the report per echoed batch, or the grace is raised). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
An ack posted her m.read, so the receipt tracked the queue, not the model. The mouth now posts m.read only
at the event id Huginn reports in gamecult.persona.chat_read.v1, for an event it delivered to her in that
room, forward only, brake read fresh. The ack posts nothing. Presence is online within presenceGraceMs of
a read report, a send or an ack, unavailable after, offline under the brake; a change is PUT at once.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Author
Member

Steward pass (heimdall-self, 2026-11-10). Merged. Soul read (14 hand mutants, all killed) found one defect: a rejected read run wedged its room and left an unhandled rejection (persona-matrix-mouth.mjs readTails chain). Fixed in 4th commit with a test that fails on the old chain. 177 Node tests pass with #36 merged in. Note for the deploy: until Huginn sends chat_read, no m.read receipt is posted (ack posts none now); the Huginn half is a separate change. A read report for an event evicted from the 4000-entry delivered index, or from before a restart, is dropped silently (UnknownEvent): safe, recorded.

Steward pass (heimdall-self, 2026-11-10). Merged. Soul read (14 hand mutants, all killed) found one defect: a rejected read run wedged its room and left an unhandled rejection (`persona-matrix-mouth.mjs` readTails chain). Fixed in 4th commit with a test that fails on the old chain. 177 Node tests pass with #36 merged in. Note for the deploy: until Huginn sends `chat_read`, no m.read receipt is posted (ack posts none now); the Huginn half is a separate change. A read report for an event evicted from the 4000-entry delivered index, or from before a restart, is dropped silently (UnknownEvent): safe, recorded.
The per-room tail was chained with .then(run), so a rejected run skipped every later report for that room and left the cleanup as an unhandled rejection. Chain on both outcomes and clean up on both. A test reaches it with a presence failure plus a throwing logger and fails on the old chain.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
GameCult/Bifrost!35
No description provided.