Skip to content

fix(gate): batched wakes route to their addressed message's channel - #200

Merged
antra-tess merged 3 commits into
mainfrom
fix/batched-wake-addressed-locus
Oct 7, 2026
Merged

antra-tess merged 3 commits into
mainfrom
fix/batched-wake-addressed-locus

Conversation

@antra-tess

@antra-tess antra-tess commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

A debounced gate wake ([Gate: N events matched]) requests inference with telemetry provenance only, with no channelId. That was deliberate, per the comment this PR replaces. The turn's locus then falls back to defaultPublishChannel, which is the most recent channels/incoming message on any channel. On a locus-mode resident that misrouted DM replies twice in one evening:

  1. Ambient after the DM. A DM arrived, and one second later an ambient message landed in a guild channel. The batched wake fired and the DM reply was published into the guild channel. No [routing] notice appeared, because the pin hadn't changed from the previous turn, so the agent had nothing to check.
  2. Ambient before the DM. The DM arrived as a push event, which is how Discord DMs arrive. Push events never update 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

  • EventGate resolves each debounced event's registered channel when the event is queued (PendingEvent.routeChannelId), through a new optional resolveRouteChannel(info).
    • Default: a channel-incoming event's own id, and nothing for push events.
    • The framework supplies derivePushEventChannel, the same derivation ingestion uses to register the DM channel on arrival.
    • A resolver that throws 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: if the newest one can't be routed, the legacy fallback stands rather than answering the wrong person.
  • The gate's requestInference wiring in the framework sets channelId + addressed: true from routeChannelId. The existing turn-start selection (addressed wins) does the rest.
  • Batched-wake lines show the registered id (discord:dm:…) instead of the raw snowflake, so the agent sees the same spelling routing notices and send tools use.
  • Ambient-only batches are unchanged.

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.
    • DM→ambient, ambient→DM and mention→ambient each reply to the addressed channel. All three fail on main, with the reply published to the ambient channel.
    • Ambient-only keeps the most-recent-inbound fallback.
  • test/event-gate.test.ts covers:
    • an addressed batch versus an ambient-only one;
    • the default resolver;
    • a newest-addressed event with no route, which must not guess an older one;
    • a resolver that throws.
  • Full suite (merged onto current main): 1226 pass, 4 skipped, 4 fail. All four failures are mcpl-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.

  • Older gate wake overrode a newer direct wake. The turn's channel pick now orders by event time (wakeAt, else timestamp), not queue order.
  • Route-only addressed push was skipped. wakeProvenance now counts a resolved route as naming a channel.
  • Forks bound elsewhere got the route. mayTakeGateRoute gives the route to a fork only for its home channel, and to no other agent when a fork owns that channel.
  • Borrowed author. An addressed routing winner is its own provenance and never borrows another request's counterparty. This also fixes the same misattribution for direct addressed pushes on main.

Review fixes, round 2 (3a7a22a)

  • Ambient wake lost its attribution. Fixed. Round 1 let an ambient route-only push become the provenance pick, which then carried no telemetry. Now only an addressed event's route makes it a candidate.
  • Fork-owned wake still wakes the trunk. Not changed here. main already 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

🤖 Generated with Claude Code

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>
@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5 Tier: apex

[Medium risk] Fixes message routing for batched wake events.

The PR is not safe to merge while a fork-owned addressed wake can make the trunk publish into an unrelated fallback channel.

Findings

  1. P1 Fork wake misroutes trunk speech ▶
  2. P2 Ambient wake loses attribution ▶
Fix with agent prompt
### Issue 1
src/framework.ts:1589-1592
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.

### Issue 2
src/gate/event-gate.ts:211
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;
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR routes addressed debounced wakes to their registered channel and updates turn selection to prefer the newest addressed event.

  • Adds push-channel resolution and batched-wake routing coverage.
  • The fork route guard still permits a non-owning trunk to answer through an unrelated fallback channel; route-only ambient pushes can lose wake attribution.

Reviews (2) · Last reviewed commit: "fix(gate): batched-wake routing review f..."

Comment thread src/framework.ts Outdated
Comment thread src/gate/event-gate.ts
Comment thread src/framework.ts Outdated
Comment thread src/framework.ts Outdated
- 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>
@antra-tess

Copy link
Copy Markdown
Collaborator Author

@greptileai

Comment thread src/framework.ts
Comment on lines +1589 to 1592
...(provenance?.routeChannelId && framework.mayTakeGateRoute(agentName, provenance.routeChannelId)
? { channelId: provenance.routeChannelId, addressed: true }
: {}),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/gate/event-gate.ts Outdated
// 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@antra-tess
antra-tess merged commit 038b01f into main Oct 7, 2026
5 checks passed
theaspirational added a commit to theaspirational/agent-framework that referenced this pull request Oct 8, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant