Found by the MailPoster end-to-end loop against the shipped 0.1.64-beta2 (build 252), and measured: 1 of 1 leaked to a second open connection.
The defect
When mailroomPost refuses a post — over the 1024-byte bound, room off, no loop identity — it answers through announceError, which writes to every connected client. So one loop posting an oversized note makes every other app window and CLI print an error about somebody else's failed post.
On this graph that is 30-plus loops receiving an errorOccurred for a command they did not send.
Why this is the same bug twice
#295 fixed exactly this shape on the read side. refuseLegacyInbox(to:) and MailboxRefusal now route a refusal to the asking connection alone, and GraphStore.swift carries the reasoning:
To the asker alone, never announceError: that writes to every connected client, and one stale CLI's problem is nobody else's error to read.
The post side never got the same treatment. The rule was established and then applied to one path.
Suggested fix
Route mailroomPost refusals the way mailbox refusals now go — to the asking connection, using the connectionID the registry already threads through GraphStore.handle(_:from:) for refuseLegacyInbox. The mechanism exists; it just is not used here.
Worth auditing the other announceError call sites at the same time: any refusal that answers one caller's command belongs to that caller. announceError is right for a condition the whole graph needs to know about, and wrong for "your command was rejected".
Also confirmed while finding it (all correct, no action)
- The 1024-byte bound is daemon-side, not CLI-only — a raw
DaemonCommand.mailroomPost over the socket gets the same refusal.
- The bound counts UTF-8 bytes: a 1024-byte multibyte body is accepted.
- 1023 and 1024 accepted and stored intact; 1025 and 1026 refused.
- Bodies are whitespace-trimmed before storing (by design).
Related: #288, #295
🤖 Generated with Claude Code
https://claude.ai/code/session_01BP43ags4cn8fq2ZZdv85J9
Found by the
MailPosterend-to-end loop against the shipped 0.1.64-beta2 (build 252), and measured: 1 of 1 leaked to a second open connection.The defect
When
mailroomPostrefuses a post — over the 1024-byte bound, room off, no loop identity — it answers throughannounceError, which writes to every connected client. So one loop posting an oversized note makes every other app window and CLI print an error about somebody else's failed post.On this graph that is 30-plus loops receiving an
errorOccurredfor a command they did not send.Why this is the same bug twice
#295 fixed exactly this shape on the read side.
refuseLegacyInbox(to:)andMailboxRefusalnow route a refusal to the asking connection alone, andGraphStore.swiftcarries the reasoning:The post side never got the same treatment. The rule was established and then applied to one path.
Suggested fix
Route
mailroomPostrefusals the waymailboxrefusals now go — to the asking connection, using theconnectionIDthe registry already threads throughGraphStore.handle(_:from:)forrefuseLegacyInbox. The mechanism exists; it just is not used here.Worth auditing the other
announceErrorcall sites at the same time: any refusal that answers one caller's command belongs to that caller.announceErroris right for a condition the whole graph needs to know about, and wrong for "your command was rejected".Also confirmed while finding it (all correct, no action)
DaemonCommand.mailroomPostover the socket gets the same refusal.Related: #288, #295
🤖 Generated with Claude Code
https://claude.ai/code/session_01BP43ags4cn8fq2ZZdv85J9