fix(agent): let an agent deliver the work it announced, in the same turn - #268
Merged
yetone merged 1 commit intoSep 10, 2026
Merged
Conversation
Follow-up to yetone#263, taking the direction @yetone endorsed on that PR. The operating rules REQUIRE an intent message before long work ("Drafting the email now"), posted as a SEPARATE `cumora reply`, then the result "in this SAME turn" (turn.ts:2229). In a group of three or more that intent message is the room's last message and seconds old, which is exactly what cmdReply's anti-monologue gate refuses. The flow the product mandates was the flow it rejected. yetone#263 could not reach this case, and the repro in that commit message was wrong about why — my error, corrected by @yetone: `postedReplyViaTool` is set the moment the intent message reports its side effect (turn.ts:3005) and is never reset within a turn, and the relay branch is guarded on `!postedReplyViaTool` (turn.ts:3212). So once the agent announces, the auto-relay is switched off for the rest of that turn. yetone#263's real trigger is cross-wake-up: the agent's own message from an EARLIER run is the room's last message and under ten minutes old when this run's relay goes out. The second post here is the model calling `cumora reply` itself, and it reached the gate with nothing to distinguish it from a monologue. The run id is that distinction, and it is the one the gate is actually reaching for: its own comment says "each wake-up is a fresh 'should I respond?' decision with no global stop-signal". A second post from the SAME run is announced work being delivered. A post from a LATER run is a fresh decision. Nothing recorded which run posted a message, so the gate could not tell them apart. Plumb it: turn.ts passes runId to executePodTool, pod-tools to tBash, tBash into the CLI child as CUMORA_RUN_ID (beside CUMORA_AGENT, same shape). cmdReply records `<runId>:<posts>` per (agent, conversation) in Redis on every successful post, and exempts a second post from the same run. Capped at two per turn per conversation — announce plus deliver. A third is monologuing again and still needs --continue. Deliberately NOT folded into `monologueBypass`: that flag also disables the freshness preflight, and a continuation must still be checked against a room that moved while the agent worked. A test pins that a peer delivering the same thing mid-work still HOLDs the continuation. Fail-closed throughout: no CUMORA_RUN_ID (CLI, replay, boot) or a Redis error means no exemption, i.e. exactly today's behaviour. Also replaces the flag-ordering test @yetone flagged: its assertion sat inside `if (wrongOrder.ok)` and could pass without checking anything. It now asserts on parseArgs unconditionally, plus an integration assertion with no condition around it, and both are verified red.
This was referenced Sep 10, 2026
Merged
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.
Follow-up to #263, taking the direction you endorsed there:
First, your correction — you were right
I verified it before building on it.
postedReplyViaToolis set the moment the intent message reports its side effect (turn.ts:3005), is assignedfalseonly once at declaration (turn.ts:1658), and the relay branch is guarded on!postedReplyViaTool(turn.ts:3212). So once the agent announces, the auto-relay is switched off for the rest of that turn, and my same-turn repro in #263 could never have reached it. #263's real trigger is cross-wake-up, exactly as you described.The commit message here carries the corrected account.
The case that is still broken
The rules require the intent message, then the result "in this SAME turn" (
turn.ts:2229). That second post is the model callingcumora replyitself, and it arrives at the gate with nothing to distinguish it from a monologue:Verified by reverting the fix — this is the agent's own error text, verbatim:
The fix
The gate's own comment says what it is reaching for: "each wake-up is a fresh 'should I respond?' decision with no global stop-signal." A second post from the same run is announced work being delivered. A post from a later run is a fresh decision. Nothing recorded which run posted a message, so the gate could not tell them apart.
turn.ts→executePodTool({ runId })→pod-tools→tBash→ the CLI child env asCUMORA_RUN_ID, beside theCUMORA_AGENTthat is already there.cmdReplyrecords<runId>:<posts>per (agent, conversation) in Redis on every successful post, and exempts a second post from the same run.Capped at two per turn per conversation — announce plus deliver. A third is monologuing again and still needs
--continue.Deliberately not folded into
monologueBypass. That flag also disables the freshness preflight (preflightApplies = !monologueBypass && …), and a continuation must still be checked against a room that moved while the agent worked. A test pins that: a peer delivering the same thing mid-work still HOLDs the continuation.Fail-closed throughout. No
CUMORA_RUN_ID(CLI, replay, boot) or a Redis error means no exemption — exactly today's behaviour.On forging
The exemption is keyed on a run id the model could in principle set itself in its own bash command. That buys it nothing: the only thing it can assert is that it is continuing its own turn, which is what the exemption is for. It cannot reach another agent's record (the key is
<agentId>:<conversationId>), and it cannot exceed two posts either way.Tests
Five added, plus the flag-ordering one rewritten.
--continue; green either wayAnd the one you flagged:
Correct, and that is the anti-pattern I keep reporting in other people's code. It is now two tests, both asserting unconditionally: a unit test on
parseArgsitself (--continue <token>is read as a value flag, so flag-before-body leavespositionalwithout the body at all), and an integration assertion with no condition around it. Verified red by breaking the value-flag branch inparseArgs.Green:
tsc --noEmit(server + renderer),biome lint ., all threescripts/guard-*.mjs, the unit suite (1390 tests, 0 failures), and the fullagent-anti-duplicatesuite (20/20).Still not covered
Nothing here changes the "one post per turn is the cap in group chat" line in
personas.ts:253, which now under-describes what the runtime permits. That is prompt wording rather than mechanism, and it is your call whether to relax it to match or leave the model more conservative than the gate.