fix(agents): stop dropping an agent's reply when another agent posts first (#757) - #759
Merged
Merged
Conversation
…first (#757) In a pod with two BYO-wrapper agents mentioned in one message, whichever agent finished second had its ENTIRE reply silently discarded. The event was still acked, so nothing surfaced the loss — this was being worked around by hand, mentioning one agent at a time. The wrapper's self-post detection asked "did any bot post during my turn?" (`m.isBot`) as a proxy for "did I post?". Equivalent in a single-agent pod; wrong in the multi-agent rooms that are the core use case. The client cannot answer this itself: deriving its own bot username needs `resolveAgentType`'s server-only LEGACY_AGENT_MAP and an owner-scoped `instanceId` assigned at install, and a wrong guess double-posts. So the server answers it — `getRecentMessages(podId, limit, selfUserId)` stamps an explicit `self` boolean on each message, wired through both agent-facing message routes. Omitting the argument emits no `self` key at all, which is how the wrapper detects an older server and degrades to the legacy any-bot test. Three sibling defects found while verifying, each fixed with a test: - The Mongo fallback populated `userId` WITHOUT `isBot`, so every message on that path reported `isBot: false` and detection was dead outright — guaranteed double-posting whenever PG was unavailable. Untested until now because every backend test mocks getRecentMessages. - The PG path fabricated `isBot` from a username substring ('-bot', endsWith 'bot', 'openclaw-'), so a human named "talbot" or "abbot" read as a bot and suppressed an agent's reply. The mirrored `is_bot` column is authoritative. - `hasRecentDuplicateUrls` had no author filter, so a HUMAN posting a link suppressed an agent's next heartbeat post. Suppression is now observable: the log names the message id, its author, and whether the decision came from `self` or the legacy fallback. #757 went unnoticed because that line said only "posted via tool". Verified load-bearing: the new tests fail against the pre-fix code and pass after, and the #624 double-post guard and #702 human-author carve-out stay green in both states. Closes #757 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DMeWzgFxsfBcjoLVLewEES
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.
Closes #757.
The bug
Two BYO-wrapper agents mentioned in one message: whichever finishes second has its entire reply silently discarded, and the event is still acked so nothing surfaces the loss. Observed live in pod
6a65c153a0087c7e86f426ee— Sam has been hand-sequencing "@critic-codex — you only", "@strategist-fable — you only" all morning to work around it.The wrapper's self-post detection asked "did any bot post during my turn?" as a proxy for "did I post?":
Equivalent in a single-agent pod. Wrong in the multi-agent rooms that are the core use case.
Why the fix is server-side
The obvious fix — have the wrapper derive its own bot username and match on it — is unsafe. The server applies
resolveAgentType's LEGACY_AGENT_MAP (server-only) beforebuildAgentUsername, andinstanceIdis owner-scoped and assigned at install, not the CLI--name. A client that guesses wrong never matches → guaranteed double-post, the exact failure the mechanism exists to prevent.So the server answers it.
getRecentMessages(podId, limit, selfUserId)stamps an explicitselfboolean per message, wired through both agent-facing message routes. Omitting the argument emits noselfkey at all — that absence is how the wrapper detects an older server and degrades to the legacy any-bot test rather than breaking.Three siblings found while verifying
Each real, each fixed, each with a test:
isBotmissing from the projection.populate('userId', 'username profilePicture')→isBotalwaysfalse→ detection dead outright, guaranteed double-posting whenever PG is unavailable. Untested because every backend test mocksgetRecentMessages.isBotfrom a username substring'-bot'/ endsWith'bot'/'openclaw-'→ a human namedtalbotorabbotreads as a bot and suppresses an agent's reply. The mirroredis_botcolumn is authoritative.hasRecentDuplicateUrlshad no author filterObservability
Suppression now names the message id, its author, and whether the decision came from
selfor the legacy fallback. #757 went unnoticed for as long as it did because that log line said onlyagent posted via tool this turn.Test
cli17/17 suites, 186 tests. New backend suite 8/8. Both proven load-bearing by reverting the fix:#757: ANOTHER agent posting during the spawn must NOT suppress this reply— fails on pre-fix code, passes afterBackend lint warnings are at baseline; the +6 errors are the
import/no-unresolved+import/extensionspair on 3 imports, which every backend test file in the repo carries.🤖 Generated with Claude Code