Acknowledged messages are replayed again into a running session, so a lead can act on the same instruction twice #127
Labels
No labels
blocked
bug
documentation
duplicate
enhancement
good first issue
help wanted
invalid
question
waiting-on-julian
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
jlxq0/matrix-mcp#127
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Observed by Mantis, 2026-08-26 evening: the same twenty messages replayed into one running session four times in a few minutes, with
mark_readcalled on the newest event each time and returning success. Same event ids, arriving repeatedly, after acknowledgement.Documented behaviour is that unacknowledged events are re-delivered at the next session start. This is re-delivery within one running session, of events already marked read.
Four replays is four session initializes, and that may not be our bug
Replay is not on a timer.
on_initializedspawnsreplay_missed(src/mcp.rs:7131-7140) and nothing else calls it, so four replays in a few minutes is fourinitializerequests in a few minutes, into what the user experienced as one running session. That is an inference from the call graph rather than a log of four handshakes, and it is the only path by which a replay can happen.That number corroborates jlxq0/caldav-mcp#21, where twenty-four calendar events cost twenty-four
initializerequests and the reading was that the connector opens a fresh session per tool call. That issue says plainly that it is "a well-supported reading of a count, not a measurement". This is a second, independent sighting from a different server and a different signal, andm365-mcphas a third.It reframes this issue rather than closing it. If reconnection is normal and frequent, then a watermark that goes stale between initializes is not an occasional annoyance, it is a defect that fires on ordinary use, and the fix is more urgent rather than less.
One thing to establish before the three sightings are treated as one phenomenon.
caldav-mcp#21is about the MCP connector on claude.ai. The mount here is a Claude Code channel session. If those are different clients, then two clients show the same shape, which is a stronger fact about the ecosystem and a weaker one about any single implementation. If they are the same client, it is one bug seen from three angles. Nobody has checked which, and the two conclusions differ.What would turn the inference into a measurement, and it is the same thing
caldav-mcp#21asks for: log theinitializerequests with their session ids, and show consecutive ones carrying different ids within one conversation. Cheap here, and it also settles the client question, since the two clients present differently.The cost is #125's cost, with a wider input
#125 is an edit arriving as a second message. This is any message arriving as a second message. A session that cannot tell a replay it has already handled from a new instruction will act twice, and these carry
replayed="true"on content that has already been acted on. It was harmless tonight because the session recognised the messages; a lead meeting an old instruction it has not seen before will not.What the source already settles, so nobody re-derives it
The replay query does consult the receipt.
replay_roomloads the watermark atsrc/channel.rs:977-982viaload_user_receipt(StoredReceiptType::Read, ReceiptThread::Unthreaded, user_id)and walks backwards from the newest event, breaking at it (src/channel.rs:996-1005). So this is not "the acknowledgement is inert because the query ignores it".The receipt type matches.
mark_readsendsReceiptType::ReadwithReceiptThread::Unthreaded(src/mcp.rs:4618-4620), which is the pairreplay_roomloads. Not the published-identifier-versus-accepted-identifier mismatch that would make it fail forever.Replay fires on every session initialize.
on_initializedspawnsreplay_missed(src/mcp.rs:7131-7140). It is not on a timer, so four replays in a few minutes means four initializes — the mount reconnecting.replay_missedholds a per-identity lock (src/channel.rs:934-947) which prevents two concurrent passes and does nothing about four sequential ones.No receipt is sent by the push paths themselves, live or replay, by design: the agent's
mark_readis what retires an event (src/channel.rs:1655).What is left, and it is two different fixes with one observable
The write may not be locally visible in time.
send_single_receiptsends the receipt to the homeserver;load_user_receiptreads the local state store. If the store is only updated when the receipt comes back down/sync, then a re-initialize seconds later reads a stale watermark and replays everything again — and every return value along the way is honest. Whether matrix-sdk 0.17 echoes it into the store synchronously is unverified and is the thing to check first.Or the write does not land at all.
mark_readreturningOkis not evidence that anything was written. Today has three instances of exactly that shape in this fleet: a JMAPnotCreatedunder an HTTP 200, a formatter allowlist discarding what the app logged, and adockerstatus tick that is a skip.The diagnostic that separates them, from Mantis and it is the right one: one
mark_read, then a direct read ofload_user_receiptfor that room, before any replay happens. Receipt absent → the write. Receipt present and the batch still arrives → the query, or the staleness above. It needs to be run against a live homeserver, not reasoned about; the two branches are indistinguishable from the observable alone.Worth measuring in the same pass: how long after
mark_readthe local store reflects it, since a fix that waits for the echo needs a number rather than a guess, and a fixed sleep is the wrong shape.Related
A single event now reproduces it, which removes every question about batching.
$35EIP1_W-d_Pz-qSTE98X3DIcJ_uFAU1JRDyaPGVkN4was delivered to one session three times.mark_readwas called on that exact id after the second delivery, returned success, and the event arrived again after that call.As a test: call
mark_readwith a specificevent_id, receive success, receive the same event again. One id, one acknowledgement, one repeat. Nothing about watermark walking, ordering, truncation orREPLAY_MAXis involved, so none of it has to be excluded first.It also lands squarely on the branch this issue had narrowed to.
send_single_receiptwrites to the homeserver;load_user_receiptreads the local state store. If the store is only updated when the receipt returns via/sync, the second replay reads a watermark that predates the acknowledgement and every return value along the path is honest.The diagnostic is now two calls rather than a replay cycle.
mark_readon that id, thenload_user_receiptfor that room, read directly. Receipt absent immediately after a success is the write. Receipt present and the event still delivered is the query or the staleness. Either answer arrives without waiting for a reconnection.Worth taking in the same pass, since it decides the shape of the fix rather than merely confirming it: how long after
mark_readthe local store reflects it. A fix that waits for the echo needs a number, and a fixed sleep chosen without one is the wrong shape.CI note for whoever runs it: this needs a live homeserver. It is not reachable from the unit suite, and reasoning about matrix-sdk's store semantics from the source is what produced the two branches rather than an answer.
Closing as a duplicate of
#140, which carries the worse consequence.Same defect: events re-delivered after
mark_readreturned success on those same ids.#140adds what makes it dangerous rather than merely noisy, that two of the four replayed events were authorisations, and a replayed authorisation is indistinguishable from a fresh one.The observation here is not lost and belongs on
#140: the same twenty messages replayed into one running session four times in a few minutes, withmark_readcalled on the newest event each time and returning success. Documented behaviour is re-delivery at the next session start; this is re-delivery within one session.Work it on
#140.