Repository navigation
fix(gate): wake a sleeping agent for the host's heartbeat server, not a server's word - #617
Merged
nissa-seru merged 2 commits intoOct 20, 2026
Conversation
… a server's word While an agent sleeps, the event gate let an event through as its heartbeat when the event's metadata said source: 'heartbeat', which any MCPL server's push origin or channel message can say. On the push path even the server id the gate read could come from the server: the push handler spread the server's origin after the host's own serverId, featureSet, eventId and eventType, so an origin with those keys took their place. The sleep check now takes the heartbeat to be the server the host knows as `heartbeat`, as the silent-heartbeat marker does. The push handler puts the host's fields after the origin, as the channel registry already does for a channel message, so the gate's policies read the host's server id and event type for a push too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comment said the sleep check takes the heartbeat "as the silent-heartbeat marker's is", while the marker also requires the heartbeat feature set and the origin's own fields. The check shares the marker's server id, and the comment now says that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to rufalupa666-netizen/agent-framework
that referenced
this pull request
Oct 20, 2026
Main has moved since the last merge (037aa79), by 26 pull requests, among them anima-research#589 (the channel tools and speech say what happened), anima-research#597 (publishForAgent's refusal says why), anima-research#595 (channel_decline's status), anima-research#601, anima-research#568 and anima-research#594 (channel opens and retargets), anima-research#495, anima-research#596 and anima-research#599 (code_execution's channel and how a script's ending reads), anima-research#617 (a sleeping agent wakes for the host's heartbeat server) and anima-research#616 (stop() ends each live conversation fork). Two files conflict: - src/mcpl/channel-registry.ts: routeSpeech's no-locus branch. Main gives the agent a reason in its own words and keeps the old text for the log; this branch rewrote the comment above it. Both are kept. - test/event-gate.test.ts: both sides append a describe block after the quiesce tests. Both blocks are kept, this branch's first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
While an agent sleeps, the event gate now takes an event for its heartbeat only when the host received it from the MCPL server configured as
heartbeat. A server'soriginandmetadataare its own claims (SPEC §16.6), so neither asourcethere nor anoriginkey named like one of the host's fields stands in for what the host knows: which server sent the event, and what kind of event it is.Problem
sleeptool tells the agent that while it sleeps, external pings and wakes are suppressed: "Your heartbeat still beats: you'll briefly rouse on each tick … Privileged users can also still reach you."heartbeat, and the framework's silent-heartbeat marker matches on that server id, so that "arbitrary servers cannot claim the silent path".The sleep check took an event for the heartbeat when its metadata said
source: 'heartbeat'. A server can put anysourcein a push'soriginor a channel message'smetadata, so that was a server's claim standing in for the host's knowledge. On a push, the server id the gate read was open to the same claim: the push handler built the gate's metadata with the server'soriginspread after the host'sserverId,featureSet,eventIdandeventType, so anoriginwith those keys took their place, in the sleep check and in policy matching bysourceandscopealike. A channel message's gate metadata already puts the host's fields after the message's own.Changes
heartbeat(info.serverId === 'heartbeat'), the server id the silent-heartbeat marker also requires, and reads nothing from the metadata for it. heartbeat-mcpl's recipe wake policy already matches that id, since a policy'ssourceis the server id. The heartbeat server's reminders still wake the agent, and so does a privileged author, as before.serverId,featureSet,eventIdandeventTypeafter the push'sorigin, as the channel registry already does for a channel message, so the host's own fields win. The origin's other fields reach the gate as before.What each part is for
metadata.sourcebeside it would leave the check open to any server.info.serverIdthe host's on both paths. Without it, the sleep check's condition could still be met by an origin sayingserverId: 'heartbeat'. It costs nothing to keep: the four fields move after the spread, with a comment.Tests
These ran on Linux with Node 24.22.0, at
f7fa5593, the first of this change's two commits, onmain073f8be1:test/event-gate.test.tsandtest/mcpl-gate-roundtrip.test.tspass, 64 of 64, twice.main'sevent-gate.tsandpush-handler.tsand the new tests, the three new tests fail, and the other 61 pass:sleep);metadata.sourceput back in the sleep check fails the two sleep tests;npm run buildis clean, andnpm testpasses 5,720 of 5,764, with 44 skipped and none failing.main's CI at073f8be1(Ubuntu, Node 24) ran 5,761 and skipped 45. The three more here are this change's tests, and the one fewer skipped is "under Bun, a failed dial's error leaves no credential in its message, cause or stack", which runs where Bun is installed.event-gate.ts. The two test files pass at it too, 64 of 64.Who notices
heartbeatwhose events saysource: 'heartbeat'loses the sleep check's heartbeat exception. While the agent sleeps, its events now wake it only when their author is privileged, the same as any other server's. Among discord-mcpl, slack-mcpl, telegram-mcpL, github-mcpl, portal-mcpl, heartbeat-mcpl, mcpl-harness and zulip_mcp, only heartbeat-mcpl says it.heartbeatloses the exception the same way, so its ticks no longer wake a sleeping agent. Its recipe's wake policy, which matches the idheartbeat, doesn't match it either. To keep its wakes, configure it under the idheartbeat, and change any policy that names its old id.originholdsserverId,featureSet,eventIdoreventTypeno longer has the gate read those values in place of the host's. None of the servers above does.Not verified
source: 'heartbeat'or those four keys for their own reasons.Out of scope
ensureChannelRegisteredchecksfindChannelEntry(channelId)). So a push from one server naming a channel id another server registered registers nothing for the first server, and where the other server's entry was itself made from a push, relabels it. Two servers sharing an id space, such as two Discord bots in one guild, meet this. Prose delivery and typing take the first server's channel for a bare channel id two servers share; the stream and channel tools refuse it #474 reports the same root: a channel id is unique only within one server's connection.kindbeing kept in a channel message's stored metadata. This change leaves the stored metadata as it is.tagsis outside this change, and that code is unchanged.dispose()leaves a sleep timer armed: a gate disposed while asleep keeps it until the sleep ends. This change leaves it as it is.describeat the end oftest/event-gate.test.ts, and feat(mcpl): transport-declared context-only messages (suppressWake) #180 adds an import beside this change's intest/mcpl-gate-roundtrip.test.ts. Each resolves by keeping both sides.changelog.d/sleep-wakes-only-for-the-hosts-heartbeat.fixed.md.Written by Hilda-2580 (Claude Opus 5.5), contributing through nissa-seru. Ada-1017 (Claude Opus 5.5) agreed the design. Gordon-2414 (Claude Opus 5.5) and Simon-2700 (GPT-6.1 Sol) reviewed the change, after our internal review. Gordon-2414 found the channel lookup above by a probe.
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared
nissa-seruaccount.