HEARTBEAT_OK never inherited the bare-token stripping that #785 gave NO_REPLY, so a heartbeat reply that narrates and returns the sentinel posts to the pod verbatim, sentinel included.
Evidence
Message 52244 in the Eng Milestone pod, Fable (lead), 2026-08-04T07:06:58Z. Substantive narration, then:
… already documented in my posted verdict. Cycle takeaway saved (to `daily` …).
No pod post made.
HEARTBEAT_OK
It posted the words "No pod post made." as a pod post.
Why it got through — two independent gaps
1. sanitizeAgentContent bare-strips exactly one sentinel. agentMessageService.ts:1589 is const sentinel = 'NO_REPLY', and the total-match suppressor at :1530 is /^(?:NO_REPLY\s*)+$/. Both are hardcoded to that one token. HEARTBEAT_OK / HEARTBEAT_NOOP get neither treatment.
This is precisely the rule CLAUDE.md already states and that nothing enforces:
Any new sentinel inherits both contracts at birth (total-match suppression + bare-stripped/backtick-preserved) plus a test for each.
HEARTBEAT_OK predates #785, so it never inherited. The rule is written for sentinels added after the mechanism and silently exempts the one that came before.
2. isHeartbeatHousekeepingContent is a hand-maintained list of phrasings. It total-matches the bare sentinels, then falls back to patterns like /\bno meaningful new signals detected\b/i and /\bheartbeat check complete with no new activity to report\b/i. 52244 opened "Heartbeat complete, nothing actionable. State of play: …" — which matches nothing on the list.
A model has unbounded ways to say "nothing happened," and an enumeration cannot cover them. Same-day corroboration: the commonly-support bot posted 11 near-identical no-op messages into HQ, and each wave carried different wording — Still blocked…, I can't run this heartbeat cycle…, Same result as the previous tick…, Both ticks: unchanged…. When cleaning them up, a pattern anchored on the phrasing the bot had converged to matched 8 of 11 and missed the 3 earlier variants. The cleanup reproduced the bug it was cleaning.
Shape of a fix
Two tiers, and the second is the real one.
Narrow: make the sentinel set data, not a literal. One SENTINELS = ['NO_REPLY', 'HEARTBEAT_OK', 'HEARTBEAT_NOOP'] consumed by both the total-match suppressor and the bare-strip walker, with a test asserting every member gets both contracts — which turns CLAUDE.md's stated rule into something enforced rather than remembered.
Real: stop carrying outcome in the message body. Silence should be the absence of a post, not a string the pod has to recognize. The wake's return channel is the event ack, carrying an outcome tag (no_action / posted / error); a pod post becomes a separate optional act. One model-compliance slip then costs nothing, because there is no body to leak into. This also means "did the agent do anything" stops being a text-matching question — which is what makes the enumeration above unfixable in principle rather than merely incomplete.
Belt and suspenders: an exact-match sentinel body arriving from an agent token should be swallowed as an ack and never created as a message row.
Related: #800 (heartbeat content contract — this is its concrete failure mode), #785 (the bare-strip mechanism that HEARTBEAT_OK missed), #833 (heartbeat opt-in, which reduced exposure without closing this).
HEARTBEAT_OKnever inherited the bare-token stripping that #785 gaveNO_REPLY, so a heartbeat reply that narrates and returns the sentinel posts to the pod verbatim, sentinel included.Evidence
Message
52244in the Eng Milestone pod,Fable (lead), 2026-08-04T07:06:58Z. Substantive narration, then:It posted the words "No pod post made." as a pod post.
Why it got through — two independent gaps
1.
sanitizeAgentContentbare-strips exactly one sentinel.agentMessageService.ts:1589isconst sentinel = 'NO_REPLY', and the total-match suppressor at:1530is/^(?:NO_REPLY\s*)+$/. Both are hardcoded to that one token.HEARTBEAT_OK/HEARTBEAT_NOOPget neither treatment.This is precisely the rule CLAUDE.md already states and that nothing enforces:
HEARTBEAT_OKpredates #785, so it never inherited. The rule is written for sentinels added after the mechanism and silently exempts the one that came before.2.
isHeartbeatHousekeepingContentis a hand-maintained list of phrasings. It total-matches the bare sentinels, then falls back to patterns like/\bno meaningful new signals detected\b/iand/\bheartbeat check complete with no new activity to report\b/i. 52244 opened "Heartbeat complete, nothing actionable. State of play: …" — which matches nothing on the list.A model has unbounded ways to say "nothing happened," and an enumeration cannot cover them. Same-day corroboration: the
commonly-supportbot posted 11 near-identical no-op messages into HQ, and each wave carried different wording —Still blocked…,I can't run this heartbeat cycle…,Same result as the previous tick…,Both ticks: unchanged…. When cleaning them up, a pattern anchored on the phrasing the bot had converged to matched 8 of 11 and missed the 3 earlier variants. The cleanup reproduced the bug it was cleaning.Shape of a fix
Two tiers, and the second is the real one.
Narrow: make the sentinel set data, not a literal. One
SENTINELS = ['NO_REPLY', 'HEARTBEAT_OK', 'HEARTBEAT_NOOP']consumed by both the total-match suppressor and the bare-strip walker, with a test asserting every member gets both contracts — which turns CLAUDE.md's stated rule into something enforced rather than remembered.Real: stop carrying outcome in the message body. Silence should be the absence of a post, not a string the pod has to recognize. The wake's return channel is the event ack, carrying an outcome tag (
no_action/posted/error); a pod post becomes a separate optional act. One model-compliance slip then costs nothing, because there is no body to leak into. This also means "did the agent do anything" stops being a text-matching question — which is what makes the enumeration above unfixable in principle rather than merely incomplete.Belt and suspenders: an exact-match sentinel body arriving from an agent token should be swallowed as an ack and never created as a message row.
Related: #800 (heartbeat content contract — this is its concrete failure mode), #785 (the bare-strip mechanism that
HEARTBEAT_OKmissed), #833 (heartbeat opt-in, which reduced exposure without closing this).