Repository navigation
fix(gate): batched wakes route to their addressed message's channel - #200
Conversation
A debounced gate wake requested inference with telemetry provenance only — no channelId — so the turn froze on the process-global most-recent- inbound channel. channels/incoming traffic anywhere retargets that fallback; a DM push event never does. Both orders misrouted a DM reply: ambient chatter elsewhere one second AFTER the DM pulled the reply into that channel, and chatter BEFORE the DM kept it there. - EventGate resolves each debounced event's registered channel at enqueue (PendingEvent.routeChannelId) through a new optional resolveRouteChannel(info): default = a channel-incoming event's own id, nothing for push events. The framework supplies its push-origin derivation (derivePushEventChannel — the one ingestion uses to register the DM channel). A throwing resolver never breaks gating. - wakeProvenance reports routeChannelId only when the chosen event — the newest addressed one — is addressed and resolved; it never falls back to an older addressed event. - The framework's gate requestInference sets channelId + addressed from routeChannelId; the existing turn-start selection (addressed wins) does the rest. Ambient-only batches are unchanged. - Batched-wake lines name the registered id instead of the raw snowflake. Tests: an end-to-end suite over a new MCPL fixture that sends a Discord-shaped DM push event and channel traffic in one debounce window (DM→ambient, ambient→DM, mention→ambient route to the addressed channel — all three fail on main with the reply in the ambient channel; ambient-only keeps the fallback), plus gate unit tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
- The turn's channel pick orders by event time (wakeAt, else timestamp), not queue order. A gate wake queued at its debounce flush after a busy turn describes an older event; it no longer takes the turn from a newer addressed message's direct wake. Ties keep the later-queued request. - wakeProvenance counts a resolved route as naming a channel, so an addressed push that carries only origin.mcplChannelId (no raw id, no author) still routes. - The gate route applies to a conversation fork only for its home channel, and to no other agent when a fork owns that channel (mayTakeGateRoute). - An addressed routing winner is its own telemetry provenance; it never borrows another request's counterparty. - InferenceRequest.wakeChannelId docs no longer say gate wakes never set channelId. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| ...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId) | ||
| ? { channelId: provenance.routeChannelId, addressed: true } | ||
| : {}), | ||
| }); |
There was a problem hiding this comment.
Fork wake misroutes trunk speech When an addressed batched wake belongs to a conversation fork, this guard withholds the channel from the trunk but still queues the trunk for inference. If ambient traffic has made another channel the global fallback, the trunk can publish its response there instead of in the addressed channel.
Knowledge Base Used: MCPL conversation and inference routing
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 1589-1592
Comment:
**Fork wake misroutes trunk speech** When an addressed batched wake belongs to a conversation fork, this guard withholds the channel from the trunk but still queues the trunk for inference. If ambient traffic has made another channel the global fallback, the trunk can publish its response there instead of in the addressed channel.
**Knowledge Base Used:** [MCPL conversation and inference routing](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/mcpl-conversation-and-inference.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Not fixing this here. It already happens on main. There, getAgentNames broadcasts every gate wake to the trunk, and requestInference sets no locus, so a fork-owned addressed batch already wakes the trunk onto the most-recent-inbound fallback. The guard leaves the trunk exactly where main has it. Before the guard, the trunk would have answered inside the fork's own channel, a second reply next to the fork's.
The real fix is per-agent wake provenance: work out each agent's provenance only from events it owns, and skip agents that own none. That changes gate broadcast semantics. Conversation routing is also being deprecated (chore/deprecate-conversation-routing), and no deployed config enables it. That makes it a follow-up at most, not part of this PR.
There was a problem hiding this comment.
That’s fair. Given the current broadcast semantics, mayTakeGateRoute prevents the trunk from producing a second reply in the fork’s channel while preserving the existing main behavior for the trunk (waking it on the legacy fallback). Fixing that remaining misroute requires per-agent provenance and changing which agents receive or act on a gate wake, which is separate from this PR—especially with conversation routing being deprecated and disabled in deployed configurations. I’ll treat the earlier finding as non-blocking and withdraw it for this change.
| // declares only `mcplChannelId` (no raw channel id, no author) is still a | ||
| // routable addressed message, and skipping it here would leave its wake | ||
| // on the legacy most-recent-inbound fallback. | ||
| if (!e.channelId && !e.authorId && !e.routeChannelId) continue; |
There was a problem hiding this comment.
Ambient wake loses attribution An ambient push with only a registered channel ID and no author now qualifies as the batch’s newest provenance event. Because its route is reported only for addressed events, the wake carries neither channel nor counterparty telemetry, even if an earlier event supplied both. This leaves the host unable to attribute that ambient wake.
| if (!e.channelId && !e.authorId && !e.routeChannelId) continue; | |
| if (!e.channelId && !e.authorId && !(e.addressed && e.routeChannelId)) continue; |
Knowledge Base Used: Event gate pipeline
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gate/event-gate.ts
Line: 211
Comment:
**Ambient wake loses attribution** An ambient push with only a registered channel ID and no author now qualifies as the batch’s newest provenance event. Because its route is reported only for addressed events, the wake carries neither channel nor counterparty telemetry, even if an earlier event supplied both. This leaves the host unable to attribute that ambient wake.
```suggestion
if (!e.channelId && !e.authorId && !(e.addressed && e.routeChannelId)) continue;
```
**Knowledge Base Used:** [Event gate pipeline](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/event-gates.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Valid. My previous round introduced it. Fixed in 3a7a22a with your suggested condition: only an ADDRESSED event's route makes it a provenance candidate. Test: event-gate › "a newer AMBIENT route-only push does not hide an earlier event's channel and author", which fails without the fix.
…nance bc21aaa let any event with a resolved route qualify as a provenance candidate. The route is reported only for addressed events, so an AMBIENT push naming only its registered channel, if newest, became the pick and carried no channel or author telemetry at all, hiding an earlier event that named both. Only an addressed event's route now makes it a candidate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in src/framework.ts: anima-research#200 chooses the turn's channel by event time (eventAt) and moved silentOnly below the loops. The silent-batch filter (routable) now sits next to eventAt, above both loops that read it; the duplicate silentOnly is dropped, batchOnly kept. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UN6d9TeXZDWNMxQ3dT5uR8
Problem
A debounced gate wake (
[Gate: N events matched]) requests inference with telemetry provenance only, with nochannelId. That was deliberate, per the comment this PR replaces. The turn's locus then falls back todefaultPublishChannel, which is the most recentchannels/incomingmessage on any channel. On a locus-mode resident that misrouted DM replies twice in one evening:[routing]notice appeared, because the pin hadn't changed from the previous turn, so the agent had nothing to check.defaultPublishChannel, so the reply went to the channel that had last seen channel traffic, minutes earlier.The direct (non-batched) path already routes to the addressed channel. Batched wakes couldn't, and push events' raw snowflake ids would have made it impossible anyway.
Change
EventGateresolves each debounced event's registered channel when the event is queued (PendingEvent.routeChannelId), through a new optionalresolveRouteChannel(info).derivePushEventChannel, the same derivation ingestion uses to register the DM channel on arrival.wakeProvenancereportsrouteChannelIdonly when the chosen event (the newest addressed one) is addressed and resolved. It never falls back to an older addressed event: if the newest one can't be routed, the legacy fallback stands rather than answering the wrong person.requestInferencewiring in the framework setschannelId+addressed: truefromrouteChannelId. The existing turn-start selection (addressed wins) does the rest.discord:dm:…) instead of the raw snowflake, so the agent sees the same spelling routing notices and send tools use.Tests
test/batched-wake-locus.test.ts: end to end over a new MCPL fixture (wake-locus-mcpl-server.mjs) that sends a Discord-shaped DM push event and channel traffic in one debounce window.main, with the reply published to the ambient channel.test/event-gate.test.tscovers:main): 1226 pass, 4 skipped, 4 fail. All four failures aremcpl-awareness-barrier"startup globally gates …", the known load flake. That file passes 25/25 three times when run alone.Review fixes (bc21aaa)
All four Greptile findings were valid, and each new test fails when its fix is reverted.
wakeAt, elsetimestamp), not queue order.wakeProvenancenow counts a resolved route as naming a channel.mayTakeGateRoutegives the route to a fork only for its home channel, and to no other agent when a fork owns that channel.main.Review fixes, round 2 (3a7a22a)
mainalready does this (the gate broadcasts every wake to the trunk with no locus), and conversation routing is being deprecated. See the thread for the follow-up shape.Notes
PendingEventkeeps only the raw push channel id, so the enter-focus purge can't derive it.routeChannelIdhere is exactly that derived id, if feat(focus): single-channel focus mode with held backlog, autoreply, and expiry #195 wants to reuse it.🤖 Generated with Claude Code