🤖 feat: intra-tree agent peer messaging via task_send_message - #3941
Conversation
Read-only research: what Claude Code's cross-session messaging does, what Mux's task_send_message / agent_report / workspace-turn mechanisms cover, and where the real gaps are (peer messaging between independent top-level workspaces, discovery, inbound consent, loop throttling, cross-machine). No product code changes.
Rebased onto bc1b4a5 and recheck of all claims. Updates: - task_send_message now reactivates terminal/archived descendants (delivery: 'reactivated' via internal allowAgentWorkspace workspace turn) - sharpened tree-messaging asymmetry: down = any-depth targeted messaging, up = one-hop agent_report to direct parent, sideways = none - workspace-turn scope check gained the descendant-reactivation branch
What sibling/upward messaging inside a task tree would require: same-tree scope check with server-computed relationship, tree-scoped discovery, untrusted peer-message framing, loop throttling, and edge-case exclusions (workflow-owned tasks, best-of candidates, terminal targets for peers).
…nds) Widen task_send_message to any same-tree target: descendant sends keep the trusted guidance path unchanged; sibling/cousin and ancestor (incl. root) targets receive an untrusted <mux_agent_message> JSON envelope with the sender id (reply address) and server-computed relationship. - Guards: same-tree scope only, no self-sends, workflow-owned and best-of endpoints refused, queued/starting/terminal peer targets not_active (reactivation stays ancestor-only). - Loop protection: per-pair and per-target rate windows, duplicate suppression, sealed-queue cap, consecutive peer-wake cap reset by user attention; all in-memory on TaskService. - Discovery: task_list scope:"tree" lists tree members with relationships plus a root row (status "workspace"). - Trust boundary: new <agent-peer-messages> PRELUDE section; peer messages never carry user authority or report-level trust. - UI: collapsible AgentPeerMessage transcript row (sender + relationship, markdown body), rate_limited/refused tool statuses, Storybook story.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5b421f006c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ript renders them The real send path (agentSession internal sends) sets synthetic + uiVisible together; without uiVisible the aggregator hides the rows and the AgentPeerMessages play times out waiting for the toggles.
|
@codex review |
|
Codex Review: Didn't find any major issues. Breezy! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
This comment has been minimized.
This comment has been minimized.
- Validate persisted peer metadata in displayedMessageBuilder; malformed rows fall back to normal user rendering (self-healing). - Count queued peer entries against the consecutive-wake budget so parallel senders cannot enqueue past the advertised maximum while the target is busy. - Refuse agent messages to hard-interrupted targets so a descendant's send cannot undo the user's stop. - Bound peer payloads: per-message char cap, sender-title cap, and shared family-message aggregate budgets (per-pair and per-target), with refund on delivery failure. - Neutralize user-typed <mux_agent_message> lookalikes at request build so the exact wrapper is server provenance and pasted envelopes keep user authority. - Qualify non-addressable rows (self, best-of candidates) in task_list tree scope docs; document the peer send cap on task_send_message. - Exclude peer rows from human-prompt navigation like monitor wakes.
|
@codex review |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bb2ab63a90
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- Charge the consecutive-wake budget at admission (inside the target's event lock) instead of at dispatch, closing the dequeue-to-acceptance window where a parallel sender saw neither a queued entry nor an incremented counter. - Qualify terminal non-descendant rows as unaddressable in the tree-scope note and task_list description (peers cannot reactivate; not_active). - Drop the prose-only tree-note assertion (tautological per AGENTS.md). - Point the research doc at canonical ~/.xum paths (legacy ~/.mux noted as fallback).
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0f9393183f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: 0f9393183f
Only the user who started this review can view the report in Codex.
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
- Neutralization exemption now requires VALID peer metadata (shared getValidAgentPeerMessageMeta validator, also reused by the display builder) AND text that parses as a well-formed envelope; a corrupted row carrying just the discriminator can no longer smuggle the exact wrapper past neutralization and strip user authority. - Remove the unrequested 200ms chevron transition from AgentPeerMessage.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c4ec837464
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Codex round 5 (P1 security): forwarding the envelope as the sendMessage TEXT persisted it as a user row, promoting prompt-injected peer output to user-priority input in the target. Mirror the family-message trust boundary: - The envelope rides as an assistant-role synthetic pre-turn row (preTurnMessages) with the agent-peer-message metadata; the turn is triggered by a fixed-content user message with zero sender-controlled bytes, naming the payload row by server-generated message ID. - Budgets charge envelope + trigger; refund only when nothing persisted (onPreTurnRowsPersisted keys the rollback horizon). - Neutralization now exempts only assistant rows with valid peer metadata whose text parses as an envelope, and also rewrites model-emitted assistant lookalikes (self-spoof) and user rows regardless of metadata. - UI: agentPeerMessage moves to the assistant DisplayedMessage variant; the card renders left-aligned from the payload row; user-row navigation exclusion reverted (payloads are no longer user rows); prompt section documents the delivery shape.
|
@codex review |
- Refuse peer delivery when ANY target ancestor is hard-interrupted, not just the exact target — a descendant's send racing the termination cascade could otherwise start a turn on a lower target the cascade had not reached. - Inherit the nearest ancestor's bestOf marker on tree rows so discovery does not advertise a candidate's nested children as peer-addressable (sendAgentPeerMessage refuses the whole candidate subtree). - Require the parsed envelope's sender fields to MATCH the validated peer metadata before exempting a row from neutralization: inconsistent halves (UI attributes A, provider reads B) are now rewritten.
|
@codex review |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 39aa3133a7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: 39aa3133a7
Only the user who started this review can view the report in Codex.
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
…ery, tool-output neutralization, verified rollback refunds
…n-messaging # Conflicts: # src/node/services/taskService.test.ts # src/node/services/taskService.ts
…ndants with live executions; update revival assertion
|
@codex review Round 17 changes (head
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5eb7ac35d7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
This comment has been minimized.
This comment has been minimized.
…e discovery, rejected-dispatch refunds, rollback propagation, synthetic-provenance neutralization
|
@codex review Round 18 changes (head
Each fix has a dedicated behavioral test. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2521e72925
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: 2521e72925
Only the user who started this review can view the report in Codex.
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
…lement; close the park-after-settlement race
|
@codex review Round 19 changes (head
Both fixes have red-probed regression tests (stale-handle settlement keeps the latch; mid-settlement snapshot park releases immediately). |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d2af01291a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review Round 20 change (head
|
This comment has been minimized.
This comment has been minimized.
…r writes cannot admit peer sends
|
@codex review Round 21 change (head
|
|
Codex Review: Didn't find any major issues. 👍 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: 0c1d48ad22
Only the user who started this review can view the report in Codex.
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
| } | ||
| stoppedTaskIds.push(id); | ||
| } finally { | ||
| releaseStopLatch(); |
There was a problem hiding this comment.
Security: Keep stop latches through initial turn preparation
When an initial child is starting and its accepted send is still PREPARING, AIService.isStreaming() is false and there is no workspace-turn registration. task_stop therefore skips stopStream, persists interrupted, and releases this latch; the still-running startReservedAgentTask() then unconditionally writes running after sendMessage returns. A prompt-influenced child can subsequently send upward or to a cousin, capturing the post-stop epochs as clean and waking an Exec-capable workspace. Fresh evidence beyond the completed-descendant PREPARING finding is this initial-launch path. Retain/cancel the latch until the launcher settles and rechecks the stopped status.
Useful? React with 👍 / 👎.
Summary
Agents in one task tree can now message each other:
task_send_messageaccepts any same-tree target — descendants (unchanged trusted guidance), siblings/cousins, and ancestors including the root workspace (new untrusted-envelope path) — with server-computed relationships, a prompt-level trust boundary, and in-memory loop protection.task_list scope:"tree"provides peer discovery, and peer messages render as collapsible attributed transcript rows.Background
Prior to this change, messaging was topology-restricted: down = any-depth
task_send_message, up = one-hopagent_report, sideways = none. The research comparison against Claude Code's cross-session messaging (docs/research/claude-code-cross-session-messaging-comparison.md, committed on this branch) found Mux's delivery machinery already equivalent, leaving topology and trust framing as the actual gaps. This PR is draft 1 of the design section in that doc.Relationship to #3900 (RLM family messaging): that PR added kernel-surface child→parent and sibling messaging for RLM mode with payload-row + budget accounting. This PR widens the tool surface (
task_send_message) for all agents with envelope framing and throttles. The two mechanisms are parallel routes on different surfaces; the trusted parent→descendant guidance path is shared and byte-for-byte unchanged.Implementation
TaskService.sendAgentTreeMessage): relation computed server-side fromparentWorkspaceIdchains only — a sender can never claim parent authority.target_descendantroutes to the untouchedsendMessageToDescendantAgentTask;peer/target_ancestortake the new path.invalid_scope) → workflow-owned ancestry on either endpoint (refused; peer messages would break WorkflowRunner's durable replay) → best-of chains (refused; candidate independence, no lobbying the selecting ancestor) → target activity (not_activefor queued/starting/terminal peers — reactivation stays ancestor-only so a sibling can never become the continuation owner and rerouteagent_report) → throttles.src/common/utils/agentMessageEnvelope.ts): JSON framing inside<mux_agent_message>with every</escaped as<\/, so sender text can neither spoof nor truncate the envelope while parsing round-trips losslessly. Carriesfrom(the reply address), optional title, and the sender's relationship to the recipient.<agent-peer-messages>PRELUDE section — peer messages are not the user, never carry consent, and get no report-level trust grant.src/constants/agentMessaging.ts, in-memory on TaskService likeconsecutiveAutoResumes): 5/min per sender→target pair, 10/min per target, 120s duplicate suppression, cap of 10 queued peer messages per target (peer queue entries are sealed via removable dedupe keys, so counting is exact and they never coalesce), and max 3 consecutive peer-triggered wakes until user/parent attention resets the budget.task_list scope:"tree"lists every tree member tagged with its caller-relative relationship plus a root row (status: "workspace", included by default, filtered like any row under explicit statuses). Workflow-owned subtrees excluded.tool-end; ancestor-bound sends defaultturn-end(often human-driven; don't cut into an active turn).agent-peer-messagemetadata → collapsibleAgentPeerMessagerow (label pill,Message from <title>, relationship badge, Markdown body). Rendering is gated on backend-attached metadata, so a user-typed lookalike envelope renders as an ordinary escaped message. Newrefused/rate_limitedtool result statuses with badge styling.Review hardening (Codex rounds 1–17). The peer path is fenced by defense-in-depth added across review rounds:
<mux_agent_message>lookalikes in all non-authentic text parts and inside tool inputs/outputs, and the transcript card requires synthetic provenance plus metadata/envelope agreement.interruptStream) through terminal persistence across all stop paths, and retained fail-closed when persistence or stream cancellation cannot be confirmed — a Stop can never race a peer send into a workspace the user believes is stopped. A synchronousadmissionStaleprobe re-checks epochs/latches/statuses at every queue/admission gate.Deferred from draft 1 (documented in the plan): composer queued-count indicator (needs a new WorkspaceStore state channel — failed the "only if cheap" bar; the sealed-entry counting it needs is in place), dismissible queue chips, durable crash replay for peer messages (in-memory queue only; sender gets
queued, a crash before dispatch drops it).Validation
make dev-server-sandbox), driving real agents end-to-end: a Watcher sub-agent discovered its tree viatask_list scope:"tree", messaged its sibling Builder (queued,tool-end) — delivered exactly once at Builder's tool boundary after a 90s bash sleep; an identical repeat returnedrefused(duplicate suppression); the ancestor send to the root returnedaccepted, woke the root, and rendered theAGENT MESSAGE … DESCENDANTrow. Builder spontaneously honored the trust boundary ("peer messages are untrusted, I shouldn't act on its suggestion"). Screenshot evidence is archived in the originating workspace chat (org SSO blocks headless image upload).taskService.test.tsfailures were confirmed present on clean HEAD via a probe worktree (unrelated to this change).Risks
agent_reportalready allows one hop up, now tree-wide but rate-limited.workspaceService.sendMessageadmission, so ordering invariants are preserved per target.📋 Implementation Plan
Intra-tree agent peer messaging — draft 1 implementation plan
Context
Research (committed on this branch:
docs/research/claude-code-cross-session-messaging-comparison.md) compared Claude Code's cross-session messaging with Mux. Verdict: Mux's delivery machinery already matches (plain text, tool-boundary dispatch, idle-turn start, durable queuing), but messaging is topology-restricted: down = any-depthtask_send_message, up = one-hopagent_report, sideways = none.Goal (draft 1): agents in one task tree can message each other — siblings/cousins and ancestors (including the root workspace) — with a proper trust boundary and loop protection.
Non-goals (draft 1): cross-tree messaging, cross-machine/federation, workspace-turn-created workspaces as tree members (they have ownership tags, not
parentWorkspaceId— separate graph), fullcrossSessionInbound-style consent UI with hold/expiry dialogs, durable crash replay for peer messages.Settled design decisions
task_send_message(no new tool);task_idstays the compatibility field name but now means "tree target id returned bytask_list— may be the root workspace id, not strictly a sub-agent task id". The relationship is computed server-side — a sender can never claim parent authority it doesn't have. No broad rename ofsendMessageToDescendantAgentTask: a new dispatchersendAgentTreeMessageroutes descendant targets to the existing method untouched (churn containment).root(sender) === root(target) && sender !== target(self-send →invalid_scope), where roots come from the existingparentByIdwalk (buildAgentTaskIndex, 32-level cycle guard). OnlyparentWorkspaceIdchains define the tree.Updated guidance from parent:+ reactivation). Zero behavior change for existing orchestration.<mux_agent_message …>(prefix consistent with<mux_subagent_report>), plus a system-prompt trust-boundary section.not_active(a sibling-triggered reactivation would make the sibling the continuation owner and silently reroute the target'sagent_reportstream away from its real parent — seecontinuationRecord.ownerWorkspaceIdrouting inreportAgentProgress).workflowTask != nullanywhere on the sender's or target's ancestry chain — reuse the walk pattern ofgetWorkflowOwnedDescendantAgentTaskUsingIndex):refused. Their I/O rides WorkflowRunner's journal; peer messages would break durable replay (same rationale asreportAgentProgress's early return).bestOfmetadata →refused— sibling↔candidate (independence) and candidate→ancestor (no lobbying the parent mid-selection) alike. Only the existing ancestor→candidate guidance path is unchanged.tool-enddefault; ancestor-bound sends default toturn-end(the target is often human-driven; don't cut into an active turn unless the sender explicitly asks). Sibling sends defaulttool-endlike today. Note:turn-endonly matters while the target is busy — an idle root still starts a billable turn immediately; the consecutive-peer-wake cap (Phase 4) is the actual limiter there.7a. Queued/starting non-descendant targets →
not_active. Only an ancestor may mutate a queued child's durable launch prompt (today's ownership semantics). A sibling/peer must never edit another task's launch prompt or bypass scheduling; peers can only message targets with a live session/turn to receive it.resolveParentAutoResumeOptions+skipAutoResumeReset+ dedupe keys (same asreportAgentProgress).queued; a crash before dispatch drops it. Documented limitation;taskPendingGuidancestays descendant-only. (Claude Code's held messages expire too.)src/constants/agentMessaging.ts, state in-memory onTaskService(mirroringconsecutiveAutoResumes).Phase 1 — Same-tree scope + relationship (core)
Files:
src/node/services/taskService.ts,src/node/services/tools/task_send_message.tsresolveTargetRelation(index, senderWorkspaceId, targetId): "target_descendant" | "target_ancestor" | "peer" | "unrelated":target_descendant: existingisDescendantAgentTaskUsingParentById(parentById, sender, target).target_ancestor: inverse check (isDescendantAgentTaskUsingParentById(parentById, target, sender)), including the root (a plain workspace entry, not a task).peer:root(sender) === root(target)otherwise (siblings and cousins).target_ancestor→ sender is yourdescendant;peer→ sender is yoursibling.target_descendantnever emits an envelope (it takes the trusted guidance path), so the envelope enum is exactly"descendant" | "sibling".sendAgentTreeMessage(senderWorkspaceId, targetId, message, queueDispatchMode?): computes the routing relation, routestarget_descendantto the unchangedsendMessageToDescendantAgentTask(guidance framing, queued-prompt append, reactivation,taskPendingGuidancereservation — zero churn), and handlespeer/target_ancestoritself.invalid_scope) → workflow-owned ancestry on either endpoint (refused) →bestOfon either endpoint for non-descendant routing relations (i.e. anything buttarget_descendant: peer↔candidate and candidate→ancestor alike,refused) → targetqueued/starting/terminal/archived (not_active) → throttles (Phase 4).workspaceService.sendMessage(targetId, envelope, options, internal)with correct option placement:options(theSendMessageOptionsthird arg) carriesqueueDispatchModeandmuxMetadata(the newagent-peer-messagevariant);internal(fourth arg) carries{ synthetic: true, agentInitiated: true, startStreamInBackground: true, queueDedupeKey: "agent-msg:<sender>:<uuid>", removableQueueDedupeKey: true }— matching the existing names used byreportAgentProgress— plus the sealed behavior so peer entries never coalesce with other queued messages (coalescing would keep only the first entry's metadata, breaking sender attribution, queue caps, and previews; exposeMessageQueue's internalsealedflag through this path if not already reachable).taskModelString/resolveTaskAgentIdForResume(as the guidance path does). Ancestor targets:resolveParentAutoResumeOptions(targetId, entry, defaultModel)+skipAutoResumeReset: true+startStreamInBackground: true.accepted(dispatched to an idle target) vsqueued(enqueued behind a busy target) suffice — distinguish via the existingonAcceptedcallback pattern; no third peer state.Net product LoC: ~180
Phase 2 — Envelope + trust boundary
Files: new
src/common/utils/agentMessageEnvelope.ts,src/node/services/systemMessage.ts,src/common/types/message.tsformatAgentMessageEnvelope({ fromWorkspaceId, fromTitle, relationship, message })→ JSON payload inside the tags, exactly likeformatSubagentReportEnvelope:Raw sender text never appears between the tags unencoded, and — because JSON alone does not stop a literal
</mux_agent_message>inside a JSON string from terminating a tag-based scan — the serializer escapes closing sequences:JSON.stringify(payload).replaceAll("</", "<\\/")(\/is a legal JSON string escape, so parse round-trips losslessly). A dedicated spoof/truncation test is required (Phase 7).relationshipis always the sender's relationship to the recipient. Thefromid doubles as the reply address; replies are automatically in-scope (symmetric rule).New
MuxMessageMetadatavariant:{ type: "agent-peer-message"; fromWorkspaceId: string; fromTitle?: string; relationship: "sibling" | "descendant" }(sender's relationship to the recipient, mirroring the envelope enum) — drives UI rendering and queue-entry counting.System prompt: add an
<agent-peer-messages>section to the PRELUDE (next to<subagent-reports>), copying Claude Code's boundary nearly verbatim:Downgrade note: old binaries render the raw envelope as synthetic user text (human-readable); unknown metadata variants fall through to plain user-message rendering in
displayedMessageBuilder.ts. Acceptable.Net product LoC: ~120
Phase 3 — Discovery (
task_list scope:"tree")Files:
src/common/utils/tools/toolDefinitions.ts,src/node/services/tools/task_list.ts,src/node/services/taskService.tsTaskListToolArgsSchema+=scope: z.enum(["descendants", "tree"]).nullish()(defaultdescendants;.nullish()per repo tool-schema rule).TaskService.listTaskTreeAgents(workspaceId): walk to root, reuse the descendant enumeration from the root, and return rows for every tree member including the root and the caller, tagged withrelationshiprelative to the caller (self|ancestor|sibling|descendant) and existing fields (taskId,status,title,agentType,depth). Exclude workflow-owned subtrees (unreachable anyway).TaskListToolTaskSchema+= optionalrelationship;parentWorkspaceId(currently required) becomes.nullish()— root rows have no parent; non-root rows keep populating it. Result note explains peers are addressable viatask_send_message."workspace"toTaskListStatusSchema, emitted only for tree-scope root rows (default scope never emits it) — the model gets one uniform list of addressable IDs. Shape:taskId: <rootWorkspaceId>,status: "workspace",relationship: "self"(when the caller is the root) or"ancestor",depth: 0,parentWorkspaceIdandhandleKindabsent. Filtering semantics: withscope:"tree"and nostatusesarg, the root row is always included; when the caller passes explicitstatuses, the root row is filtered like any other row (excluded unless"workspace"is listed).Net product LoC: ~120
Phase 4 — Loop protection
Files: new
src/constants/agentMessaging.ts,src/node/services/taskService.ts,src/node/services/messageQueue.tsApplies to peer/ancestor sends only (descendant guidance unchanged):
PEER_MESSAGE_RATE_LIMIT_MAX = 5perPEER_MESSAGE_RATE_WINDOW_MS = 60_000per sender→target pair. In-memory sliding window onTaskService. Exceeded →rate_limited(includeretryAfterMs).PEER_MESSAGE_TARGET_RATE_LIMIT_MAX = 10per minute per target across all senders — covers many-sender flooding that per-pair limits miss. Exceeded →rate_limited.PEER_MESSAGE_DEDUPE_WINDOW_MS = 120_000: identical (sender, target, trimmed text) within the window →refused(reason: duplicate).MAX_QUEUED_PEER_MESSAGES_PER_TARGET = 10:MessageQueuegainscountEntriesByMetadataType("agent-peer-message")(accurate because peer entries are sealed); at cap →refused.MAX_CONSECUTIVE_PEER_WAKES = 3: mirror ofconsecutiveAutoResumes— increment when a peer message starts a turn on an idle target; reset whereverresetAutoResumeCountfires (user-authored sends already reset there) and on terminal user activity. At cap →refused(reason: target needs user attention).consecutiveAutoResumes), with TTL sweeps so sender/target maps cannot grow unboundedly (evict entries older than the largest window on each check).Net product LoC: ~150
Phase 5 — Tool schema, results, sender-side rendering
Files:
src/common/utils/tools/toolDefinitions.ts,src/node/services/tools/task_send_message.ts,src/browser/features/Tools/TaskToolCall.tsxtask_iddescription → "Tree target ID returned by task or task_list — a sub-agent task ID or, for upward messages, an ancestor/root workspace ID";queue_dispatch_modedescription notes the ancestorturn-enddefault.{ status: "rate_limited", taskId, retryAfterMs? }and{ status: "refused", taskId, reason }.TaskStatusBadgemapsrate_limited/refused→ danger/warning styling;TaskSendMessageToolCallshows relationship + target title when present in the result.Net product LoC: ~60
Phase 6 — Receiving-side UI
Files:
src/browser/utils/messages/displayedMessageBuilder.ts,src/browser/features/Messages/MessageRenderer.tsx, newsrc/browser/features/Messages/AgentPeerMessage.tsx,src/common/orpc/schemas/stream.ts,src/browser/features/ChatInput(optional read-only queued-count indicator only)Required for draft 1 (the message row):
displayedMessageBuilder:muxMetadata.type === "agent-peer-message"→ attachagentPeerMessage: { fromWorkspaceId, fromTitle, relationship }to the userDisplayedMessage.MessageRendereruser-case branch →<AgentPeerMessage>(modeled onSubagentReportMessageContent): collapsible row, labelagent message, headerMessage from <title>+ relationship badge, expanded body = MarkdownRenderer on the inner text. No emoji icons;lucide-reacticon.AgentPeerMessage(collapsed/expanded), pinned mobile viewport per repo Storybook rules.Minimal queue indicator (in scope only if cheap): extend
QueuedMessageChangedEventSchemawith a read-onlyqueuedAgentMessageCount(sourced from sealed peer entries;getVisibleMessages'suserAuthoredfiltering stays untouched) and show a passive indicator near the composer.Deferred past draft 1: dismissible per-message chips + a queue-removal oRPC endpoint (the
removableDedupeKeyplumbing already makes this possible later without rework), unless an existing queue-edit endpoint turns out to make dismissal trivial.Net product LoC: ~110 (row + builder + story + count indicator)
Phase 7 — Tests
Files:
src/node/services/taskService.test.ts(existingcreateTaskServiceHarness— real TaskService/HistoryService, mocked ai/workspace services),src/node/services/tools/task_list.test.ts,src/common/utils/agentMessageEnvelope.test.ts,tests/uiBehavioral matrix (no tautological prose assertions):
invalid_scope; self →invalid_scope.resolveParentAutoResumeOptions+turn-enddefault; sibling gets target-settings +tool-enddefault.refused; bestOf peer →refused;queued/starting/terminal target for peer →not_active(and the queued sibling's durable launch prompt is untouched); ancestor reactivation still works.</mux_agent_message>(and</sequences generally) cannot terminate or forge the envelope, and round-trips losslessly through the<\/-escaped serializer; reply-address presence.refused(in addition to sibling↔candidate).scope:"tree", excluded when explicitstatusesomit"workspace".scope:"tree"returns root + siblings with relationships; workflow subtree exclusion; default scope unchanged.Test LoC: ~700–900 (not counted in product LoC)
Phase 8 — Dogfooding & validation gates
Gates between phases:
make static-check+ targetedbun test src/node/services/taskService.test.ts(and touched suites) must pass before the next phase starts.End-to-end dogfood (required before calling draft 1 done):
dev-server-sandboxskill (tempXUM_ROOT+ free port) ordev-desktop-sandboxif display available.task_send_messageto B (sibling) and to the root (ancestor); from B reply to A using the envelope'sfromid.agent-browsersnapshots + screenshots (attach_file): (a) sibling delivery lands at a tool boundary of B's active turn; (b) ancestor message queuesturn-endon a busy root and renders theMessage fromrow; (c) queued-count indicator visible while queued (if in scope); (d) throttle: 6th rapid send returnsrate_limited; duplicate text returnsrefused.invalid_scope; attempt send to a workflow-owned child →refused.Acceptance criteria
<mux_agent_message>with sender id/title + relationship.turn-end.invalid_scope/refused/not_active/rate_limitedreturned per the guard matrix; all throttles enforce their constants.task_list scope:"tree"exposes addressable tree members with relationships (root rowstatus: "workspace").make static-checkgreen; dogfood evidence (screenshots + recording) captured.Risks & mitigations
refusedon workflow-owned endpoints (both directions).chat.jsonl: old binaries render raw text (readable); no migration needed. New config fields: none (throttle state is in-memory).workspaceEventLocks/tree lifecycle lock as today); no new lock ordering introduced because peers never mutate ancestry (no reactivation).Rollout
Single PR series on one branch, phases 1–2 (core + trust) first, 3–4 (discovery + throttles) second, 5–6 (UI) third, 7–8 woven throughout; each PR passes gates independently. Total net product LoC: ~850–1,050 (+ ~800–1,000 test LoC). The dismissible-queue-chip variant (deferred) would add ~250–300 LoC on top.
Open items settled during implementation
sealed-flag plumbing ifworkspaceService.sendMessage's internal options do not already expose it.Generated with
xum• Model:anthropic:claude-fable-5• Thinking:xhigh• Cost:$116.96