fix(routing): a turn from a non-channel surface pins no inferred locus - #190
rufalupa666-netizen wants to merge 3 commits into
Conversation
|
… mixed batches, mid-turn re-pin, and the false send-failed marker Addresses the four review findings on anima-research#190: 1. The guard matched WebUI chat (external-message/tui) but missed the API message.send ingress (api:message, source unknown). Classify private origin once at enqueue as InferenceRequest.nonChannelOrigin and read the flag at the freeze instead of reconstructing it from reason/source strings. 2. A private trigger could borrow a sibling channel request's channelId and addressed flag from the same batch (pre-existing coalescer semantics, framework.ts ~7576). A private trigger now keeps channelId undefined and addressed false. Regression test in turn-trigger-provenance.test.ts. 3. A mid-turn addressed injection could re-pin a private turn's null locus (framework.ts ~6471). Skipped when the active trigger is nonChannelOrigin; channel-origin turns keep the ratified re-pin. 4. routeSpeech(null) produced a false [discord-send-failed] marker for a reply the WebUI had already shown. Private turns now use the existing prose suppression accounting (as proseRouting=disabled does) and receive a '[delivered] nothing — kept private (non-channel turn)' receipt; routeSpeech is not called. Full suite 972 (968 pass, 4 skipped), tsc --noEmit clean, git diff --check clean.
|
Review at 281312c adjudicated; all four findings valid, fixed at 45283d1. Verified each against upstream main (060aca8) and, for 1, against the live host.
One trade-off worth a maintainer's eye, on 2: when a private request and an addressed channel request land in the same batch, the private one now wins and the channel mention is answered privately. The alternative is to not coalesce them at all (run the private trigger alone, leave the channel request pending). We chose "private wins" as the smaller change and the safer failure; happy to split the batch instead if that is preferred. Receipts at 45283d1 (on 060aca8): |
|
@greptileai bump |
439ae26 to
f458e99
Compare
|
Round 2 (439ae26) adjudicated; three findings, all valid, fixed at f458e99.
Receipts at f458e99 (on 5498805): 973 pass, 0 fail; @greptileai bump |
A WebUI/TUI/CLI/headless/API turn has no channel. The freeze at turn start treated every no-channel turn as ambient and fell through to home ?? activeChannel ?? defaultPublishChannel, where the last is the process-global most recent incoming channel. So a private WebUI reply inherited whichever Discord channel last spoke. Three incidents in one week on a production resident (2026-09-15, 09-22, 09-23). Freeze null before resolveLocus when the trigger is an external-message from a non-channel source (tui/cli/headless/api); the source is already on the InferenceRequest. Channel-triggered and heartbeat turns are unchanged. No gate/wake provenance involved. channel_open's current-turn repin (the 2026-07-31 call) is deliberately untouched — separate question for its owner. Tests: four cases in test/trunk-channel-routing.test.ts. Full suite 971 (967 pass, 4 skipped), tsc --noEmit clean, git diff --check clean. Co-developed with qa-engineer (connectome-host resident).
… mixed batches, mid-turn re-pin, and the false send-failed marker Addresses the four review findings on anima-research#190: 1. The guard matched WebUI chat (external-message/tui) but missed the API message.send ingress (api:message, source unknown). Classify private origin once at enqueue as InferenceRequest.nonChannelOrigin and read the flag at the freeze instead of reconstructing it from reason/source strings. 2. A private trigger could borrow a sibling channel request's channelId and addressed flag from the same batch (pre-existing coalescer semantics, framework.ts ~7576). A private trigger now keeps channelId undefined and addressed false. Regression test in turn-trigger-provenance.test.ts. 3. A mid-turn addressed injection could re-pin a private turn's null locus (framework.ts ~6471). Skipped when the active trigger is nonChannelOrigin; channel-origin turns keep the ratified re-pin. 4. routeSpeech(null) produced a false [discord-send-failed] marker for a reply the WebUI had already shown. Private turns now use the existing prose suppression accounting (as proseRouting=disabled does) and receive a '[delivered] nothing — kept private (non-channel turn)' receipt; routeSpeech is not called. Full suite 972 (968 pass, 4 skipped), tsc --noEmit clean, git diff --check clean.
f458e99 to
e3054d0
Compare
|
Rebased onto main df97a85 (over #216); round 3 (f458e99) adjudicated; fixed at e3054d0. Late open redirects private speech — valid. A On #216: we looked at reusing its Rebase conflicts and resolutions recorded; nothing semantic changed. Receipts at e3054d0 (on df97a85): 1128 pass, 0 fail; @greptileai bump |
| // requests[0] in that mixed batch bypassed the turn lock because a | ||
| // restart existed, then treated the continuation as a fresh turn. | ||
| const trigger = budgetRestart ?? requests[0]; | ||
| if (trigger.nonChannelOrigin) { |
There was a problem hiding this comment.
Private reply can publish When a silent heartbeat is queued before a private WebUI/API message, this check sees only the heartbeat and does not separate the private request. The mixed batch also removes the heartbeat’s prose suppression. If the resident has a home or last-inbound channel, a reply informed by the private message can be published there instead of staying private.
How this was verified: The private message enters the same request batch, but the selected trigger has no private-origin flag and the mixed-batch suppression check permits channel speech delivery.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 8187
Comment:
**Private reply can publish** When a silent heartbeat is queued before a private WebUI/API message, this check sees only the heartbeat and does not separate the private request. The mixed batch also removes the heartbeat’s prose suppression. If the resident has a home or last-inbound channel, a reply informed by the private message can be published there instead of staying private.
**How this was verified:** The private message enters the same request batch, but the selected trigger has no private-origin flag and the mixed-batch suppression check permits channel speech delivery.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.|
Want your agent to iterate on Greptile's feedback? Try greploops. |
What happened in production
Three times in one week a private WebUI reply from a connectome-host resident was published to a Discord channel (2026-09-15 noticed by Lari; 2026-09-22 18:48 UTC and 2026-09-23 12:32 UTC on our QA resident, journal lines in the trace linked below). The resident had not called a send tool.
Mechanism
A turn triggered from WebUI/TUI/CLI/headless/API has no channel. At the turn-start freeze every no-channel turn was treated as ambient and fell through
home ?? activeChannel ?? defaultPublishChannel— the last being the process-global most recent incoming channel (channel-registry.ts:471-495,:900). The WebUI event carriessource: 'tui', but nothing at the freeze looked at it. So a private reply inherited whichever Discord channel last spoke.Change
src/framework.ts, at the freeze: when the trigger is anexternal-messagefrom a non-channel source (tui/cli/headless/api) with nochannelId, pinnullbeforeresolveLocus— so home, active and global fallback are all bypassed.routeSpeech(null)then emits its existing no-locus marker instead of guessing. Ten lines.Deliberately not touched:
channel_openrepinning the current turn — the 2026-07-31 call; the "open for reading" counter-case is a separate question for its owner.This is the same rule the framework already applies to subagents (
test/subagent-prose-routing.test.ts) and to the subconscious (forced explicit): a turn that didn't come from a channel doesn't get a guessed one.Tests
test/trunk-channel-routing.test.ts, four cases: WebUI turn after Discord traffic → no pin,resolveLocusnot called,routeSpeechreceivesnull; WebUI turn for a resident with a home channel → still no pin; channel-triggered turn → pins its channel; heartbeat → global fallback unchanged.Receipts on 060aca8 (v0.19.0):
npm run build && npm test→ 971 tests, 967 pass, 0 fail, 4 skipped;tsc --noEmitclean;git diff --checkclean. Changelog fragmentchangelog.d/non-channel-origin-locus.changed.mdnames who is affected.Adjacent work
No overlap found with #177 (failed sends release held prose — under this change the release goes nowhere for a WebUI turn, which is the intended fail-closed) or #182 (outbox sits in
routeSpeech, downstream of locus resolution). Discussed in #connectome-qa (2026-09-25 / 09-28); Ferry confirmed the boundary and the "override home too" sharpening.Co-developed with qa-engineer, the connectome-host resident that hit this.