Repository navigation
Keep the mail broker from orphaning a reply to an unknown parent - #15330
teamleaderleo merged 3 commits into
Conversation
Both assertions fail on this commit. append() accepts a reply whose parent it has never seen. normalizeEnvelope falls back to the message's own id for the thread, so the message is stamped kind "reply" and inReplyTo <parent> while rooting a brand new thread, and thread(parent) never returns it. reply() refuses the same input, and its doc comment says it exists so adapters cannot "accidentally create a new thread when they only have a parent message ID" -- which is what the primitive under it does. The second half of the test pins the case that has to keep working: a reply forwarded from another broker, which names its threadId explicitly. append() also reports a conflict when an idempotent retry lists the same recipients in a different order. Recipients are a set, deduped and keyed by address for delivery, but fingerprint() canonicalizes arrays by position, so ["codex","claude"] does not match ["claude","codex"] and the retry looks like someone reused the id with different content. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…etry append() now refuses a reply whose parent it has never seen unless the caller names the thread, which is what reply() already does. The refusal is raised before normalizeEnvelope, so nothing is committed. reply() routes through the same helper, so both paths raise the same message. An explicit threadId still gets through untouched. That is the case worth keeping: a broker replicating a reply from another machine has the thread but not the parent message, and normalizeEnvelope already prefers the declared threadId over the parent's. fingerprint() now sorts recipients before comparing. The envelope keeps the order the first append supplied, so deliveries and the stored recipient list are unchanged; only the equality test becomes order-insensitive, which is what a set-valued field needs for a retry to stay idempotent. references is left in place because its order is the ancestry chain, and a different recipient set still conflicts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
All contributors have signed the CLA ✍️ ✅ |
|
Warning Review limit reachedNext included review available in 12 seconds. View limit detailsLimit details: You’ve used all 10 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository: manaflow-ai/cmux/.coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (2)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The review of manaflow-ai#15330 found two follow-ups in the fix itself. The new failure was a bare Error whose message contained "unknown mail message <id>", which is verbatim what updateDelivery throws for an unknown message id. A caller could not tell the two apart, and the test matched that substring, so it could have passed for the wrong reason. Export MailUnknownParentError with a code and the parent id, matching MailConflictError and MailFanoutError, and assert on the class. The guard also only covered an absent threadId, so createMail slipped past it: it normalizes with no broker to look the parent up in, roots the envelope at its own id, and hands append a threadId that looks explicit. A thread is rooted at a message that is not itself a reply, so a reply naming its own id as its thread is the same orphan rather than a federated one. Refuse that shape, and treat a blank threadId as absent in both the guard and normalizeEnvelope so it cannot name an empty thread the guard and listThread disagree about. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Review: a review subagent went through this at Fixed:
Left:
Verification on |
|
Merging on green under the standing rule for fixes: review subagent done, findings posted above and addressed. No fleet dogfood evidence for this one, and the reason is structural rather than me skipping it: PR #8029 disabled Vercel branch previews, so an unmerged change to the deployed services has no preview URL for an app build to talk to. A fleet build would exercise |
|
Merge receipt for |
ee20686 fix: keep SSH exit prompt off PTY output drain (manaflow-ai#15337) 96e7a27 reload.sh: expand the empty resolver args safely under bash 3.2 (manaflow-ai#15352) 558d6b9 ci: move owned gui jobs to Blacksmith only when its queue is shorter (manaflow-ai#15336) fff0b82 UI fuzzer: seeded action sequences, oracles, minimized repros and deduplicated issues (manaflow-ai#15297) 94a6387 Add an agent activity mode to workspace auto-reordering (manaflow-ai#15216) e5231be CI: post screenshots and a GIF of each app PR's build in its dogfood comment (manaflow-ai#15280) 16f1270 cli: answer queued agent hooks inside the agent's hook timeout (manaflow-ai#14834) 3fd61eb Sidebar: show the most urgent pane's status when panes share an agent key (manaflow-ai#15260) 0975d0b Release discarded CodeRouter response bodies after retry (manaflow-ai#15253) 42f93d4 Re-verify the session against a body-supplied VM billing team (manaflow-ai#15339) bdb6920 Keep the mail broker from orphaning a reply to an unknown parent (manaflow-ai#15330) # Conflicts: # .github/workflows/ci-guards.yml # .github/workflows/ci-macos.yml # .github/workflows/test-e2e.yml
agent-chat/mail/core.tshas two places whereappend()disagrees with the contract the file documents for itself. Both are pure unit-level defects in the broker, found while reading the module for the agent-mail RFC (manaflow-ai/cmuxterm-hq#851).A reply to an unknown parent silently became a new thread root
append()looked upinReplyToand passed the parent'sthreadIdas a fallback. When the parent was unknown the fallback wasundefined, sonormalizeEnvelopefell through to its last resort and used the message's own id as the thread. The result was an envelope stampedkind: "reply"withinReplyTo: <parent>andreferences: [<parent>]that was nonetheless the root of a second thread, sothread(parentId)never returned it and the conversation was split with no error anywhere.reply()refuses the same input, and its doc comment says why it exists:That is exactly what the primitive underneath it did.
append()now raises the same error, beforenormalizeEnvelope, so nothing is committed.The case that has to keep working is a reply forwarded from another broker, which has the thread but not the parent message. Those callers pass
threadIdexplicitly, andnormalizeEnvelopealready prefers a declaredthreadIdover the parent's, so the guard only fires when there is genuinely nothing to attach the reply to. The test pins this. Re-appending a fullMailEnvelopealso keeps working, since a normalized envelope always carries athreadId.An idempotent retry faulted when it reordered the recipients
fingerprint()is the tamper check behindMailConflictError, andcanonicalize()sorts object keys but preserves array order. Recipients are deduped into aSetand never sorted, and deliveries are keyed by address, so the field is set-valued everywhere except in the fingerprint. Retrying["codex","claude"]after["claude","codex"]therefore looked like someone had reused the id with different content, defeating the retry safety the same function goes out of its way to provide for a generatedcreatedAttwo lines below.fingerprint()now sorts recipients for the comparison only. The stored envelope keeps the order the first append supplied, so the recipient list and thedeliveriesarray are unchanged.referencesis deliberately left alone: its order is the ancestry chain. Adding or dropping a recipient still conflicts, which the test asserts.Verification
Red then green on the same command,
bun test ./test/mail.test.tsinagent-chat/:Commit 1 (
1da8a467767, tests only):Commit 2 (
f1a6abd40d3, the fix):Whole package,
bun run-tests.ts:35 pass, 0 failacross 8 files.bun x tsc --noEmit: clean. Both run in CI through theRun agent-chat unit testsstep in.github/workflows/ci-guards.yml.No behavior change for any current caller:
agent-chat/test/mail.test.tsis still the only importer of this module, so there is no wired surface to regress and nothing user-visible to dogfood. The value is that the in-flight mail work does not inherit a broker that splits threads quietly.Changelog
none
🤖 Generated with Claude Code
Summary by cubic
Fixes two defects where the mail broker's
append()disagreed with its own contract: it could orphan a reply as a new thread root, and it could reject an idempotent retry that simply reordered recipients.append()now refuses a reply whose parent it has never seen, throwing an exportedMailUnknownParentError(with a code and parent id) instead of silently rooting a second thread at the reply's own id;reply()raises the same typed error. The guard also rejects a prebuilt envelope whose thread is the reply's own id and treats a blankthreadIdas absent, closing the shapescreateMailcan produce, while an explicit thread name still goes through so replicating a reply from another broker keeps working.fingerprint()now sorts recipients for the comparison only, so a retry listing the same recipients in another order is an idempotent no-op instead of aMailConflictError. The stored envelope keeps the first append's order,referencesorder is preserved, and changing the recipient set still conflicts.Tests added for both cases; no behavior change for current callers since the test file remains the only importer of this module.
Written for commit c6220e5. Summary will update on new commits.