fix(agent-channel): preserve unaddressed continuation paragraphs in single-recipient messages#11
Open
evannadeau wants to merge 1 commit into
Conversation
…ingle-recipient messages The per-paragraph routing filter in `filterParagraphsForReceiver` (agent_channel.ts:608, added in 0.30.22 / work_item b4c37849) splits content on blank lines and only delivers paragraphs that contain an `@SA-<id8>` address. The intent — prevent PA's user-private prose from leaking to SAs in a mixed-audience message — is sound. The empirical consequence is over-aggressive: a sender writing a directive with ordinary structure (intro paragraph with @-address, then bulleted scope, then closing prose) sees only the intro paragraph reach the receiver. Everything from the first blank line onward gets dropped. This affects every multi-paragraph PA→SA dispatch and has been observed recurring across multiple operator sessions. Workaround until now: senders collapse everything to one dense paragraph with semicolons or inline numbering, which is unreadable for the operator watching the channel. Fix: single-recipient-set heuristic. When ALL @-addressed paragraphs in a message route to the same target set, deliver the WHOLE content to that target set (no paragraph filtering). When @-addressed paragraphs route to DIFFERENT target sets, fall back to per-paragraph filtering — preserving the original safety property for genuinely mixed-audience messages. The existing integration test at `tests/integration/agent_channel_routing.test.ts:210` (mixed-audience private-prose-must-not-leak) still passes — it has @-addresses to two DIFFERENT SAs, so the per-paragraph fallback applies and the private paragraphs continue to be filtered out. Added a new integration test covering the single-recipient case (intro + bullets + closing → whole message delivered). Test suite: 517 pass / 0 fail / 1214 expect() calls (up from 516 / 1207). `bun build` regenerates dist/server.js cleanly (249 modules, 0.94 MB).
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The per-paragraph routing filter introduced in 0.30.22 (work_item b4c37849) over-aggressively drops paragraphs from PA→SA dispatches. A directive with ordinary structure — intro paragraph containing the
@SA-<id8>address, followed by bulleted scope and closing prose — delivers ONLY the intro paragraph to the receiver. The bullets and closing get silently filtered out at the receiver's filewatcher.This PR fixes the over-filter without breaking the original safety property (mixed-audience messages must not leak private user-prose to addressed SAs).
The bug, concretely
mcp/engine/agent_channel.tsline 608 (pre-fix):Splits content on blank lines, runs
parseAddressingper paragraph, keeps only paragraphs whose addressing includes the receiver. Continuation paragraphs (bullet lists, follow-up sentences, closing prose) typically don't contain their own@SA-<id8>address — they're scope for the address in the prior paragraph. So they get dropped.Empirical reproduction (multiple operator sessions):
A PA dispatches to an SA:
The receiving SA's filewatcher reports the message arrived as:
That's the entire delivered content. The bullets and the closing are gone. The receiver has no idea what the actual fix scope is — they have to ask for a resend. In practice operators have been working around this by collapsing everything to a single dense paragraph with semicolons or inline numbering, which is unreadable for the operator watching the channel and brittle to write.
Why the existing filter exists (per the comment at the call site, lines 538-548 in the pre-fix file): a sender mixing user-private prose with an
@SA-<id8>directive in one message must not leak the private prose to the addressed SA. The per-paragraph filter is the defense for that. Sound intent; over-aggressive implementation.The fix — single-recipient-set heuristic
When ALL
@-addressed paragraphs in a message route to the same target set, deliver the whole content (no paragraph filtering). When addressed paragraphs route to different target sets (a genuinely mixed-audience message), fall back to per-paragraph filtering — preserving the original safety property.What this changes (cases that now deliver in full)
→ All 3 paragraphs reach SA-X. Common case, now fixed.
→ All 3 paragraphs reach SA-X (single-recipient-set since both addresses go to {X}).
→ Both paragraphs reach both SA-X and SA-Y (single-recipient-set since {X,Y} is consistent).
What this preserves (cases that still apply per-paragraph filtering)
→ Different target sets ({X} vs {Y}). Per-paragraph filtering applies. SA-X gets only
@SA-X directive. SA-Y gets only@SA-Y different directive. Private paragraphs filtered out for both. The existing integration test attests/integration/agent_channel_routing.test.ts:210continues to pass — verified.→ Single-recipient-set ({X}). Whole message to SA-X — INCLUDING the leading "intro prose". This is a behavior change for leading unaddressed prose. Senders who genuinely want it private can write it as a separate message (no
@-addresses → no SA routing happens, original behavior).→ Same as before: no @-addressed paragraphs, falls through to per-paragraph filtering, which keeps nothing for any SA. PA still observes (PA bypasses this function entirely).
Trade-off acknowledged
Senders mixing single-SA dispatch with trailing operator-only prose in the same message will now see that operator-only prose leak to the SA. The recommended discipline (and what operators were doing anyway): send operator-only updates as a separate message. The cost of this regression is much smaller than the cost of the truncation it fixes.
Test plan
bun installclean (98 packages).bun run build→dist/server.js0.94 MB, 249 modules (regenerated to match source).bun test517 pass / 0 fail / 1214 expect() calls (up from 516 / 1207).tests/integration/agent_channel_routing.test.ts— "single-recipient message with unaddressed continuation paragraphs delivers in full" — exercises the exact bug pattern (intro with@SA-<id8>, three bullet paragraphs, closing prose) and asserts the receiving SA gets the whole content.@SA-<id8>addresses → different target sets → per-paragraph filter applies → private paragraphs filtered out.Files changed
plugins/orchestrator/mcp/engine/agent_channel.ts—filterParagraphsForReceiverreshaped with the single-recipient-set heuristic + comment block documenting the trade-off.plugins/orchestrator/tests/integration/agent_channel_routing.test.ts— added single-recipient-with-continuation test.plugins/orchestrator/dist/server.js— regenerated viabun run build.Related
mcp/server.tsfixes)🤖 Generated with Claude Code (Admiral PA orchestrating an upstream-cleanup batch)