Repository navigation
fix(gate): a batched wake writes its line only when it says something - #727
Conversation
A debounced wake writes "[Gate: 1 event matched] - 1 message in <channel> (oldest ~5s ago) — already shown above at arrival; this is a batched wake, not new content" into the window before waking the agent. The line says which messages a wake is for. For one message that is still the newest entry in the window, right above the line, it says nothing new: on a resident whose addressed messages wake through a 5 s debounce, a third of the window was these lines, four in five of them right under the one message they named. The gate now asks the framework (`isNewestEntry`) before writing the line for a batch of one MCPL event: whether that event's own stored entry (a channel message by server, channel and message id; a push event by server and event id) is the newest entry in the window the line goes to, with no turn running there, nothing deferred, and no tune-out holding it. If so, the wake runs without the line; its provenance and the reply's channel are unchanged, and `gate:wake-line-omitted` is traced. Several messages, anything stored after the message, and content the line quotes keep the line. A gate built without the callback, or one whose callback throws, writes every line, as before. Tests: the gate's rule (unit) and end to end over the wake-locus MCPL fixture: one DM → no line; a DM with an ambient message stored after it → the line; two DMs in a batch → the line. On main's source the "no line" tests fail and the "line stays" ones pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Automated review
Recommendation: merge and fast-follow. 3 medium severity findings can be fixed after the merge.
Reviewed commit 307e0be with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. This fits. It changes behaviour the existing event gate already has, trimming a redundant batched-wake line.
4 reasons with sources
It also follows the gate's pattern of asking the framework through optional callbacks, and it arrives in the shape CONTRIBUTING.md asks for.
- The change only adjusts the
[Gate: …]line that the existing debounce path in the gate already writes; it adds no new feature or scope. The wake, its provenance and requestInference are unchanged. Only the user-entry line is skipped, and only for a batch of one MCPL event that is still the newest entry in the window. README.md doesn't describe the gate's goals, so this is judged against the existing gate code. (src/gate/event-gate.ts (fireDebounce)) - The new
isNewestEntryhook follows how EventGate already asks the framework about its window, through optional callbacks passed to its constructor. It sits next to the existingchannelShownandresolveRouteChannelcallbacks. The gate keeps the decision about whether to write the line, and the framework answers the question about its own window (deliveryAgent, deferredMessages, tune-out). Without the callback the gate behaves as before. (src/gate/event-gate.ts constructor options; src/framework.ts gate construction) - The new public surface is small and optional: one exported type (
GateEntryRef) and one trace event (gate:wake-line-omitted), each in the file that already holds its kind.GateEntryRefgoes in the gate's types file and is re-exported alongside the other gate types; the trace event goes into the existing TraceEvent union with the other gate:* events. A separate review found the 'no turn running' check uses activeTurnTokens rather than the existing turnAlive() helper. That is a defect for the code review, not a question of fit. (src/gate/types.ts; src/types/trace.ts) - The PR is one coherent change and its body has the parts CONTRIBUTING.md asks for: Problem, Changes, Tests, Not verified, a changelog fragment, numeric test baselines and the AI footer. The fragment
changelog.d/gate-wake-line-only-when-it-says-something.changed.mdis a flat file of bullets in the required format. Unit tests and end-to-end tests come with the change. The gaps the code review found in them go to the author. (CONTRIBUTING.md, "What a PR should contain" and "Changelog")
Findings
3 findings of medium or high severity, see the inline comments:
- Medium severity. The 'no turn running' check uses activeTurnTokens alone instead of the existing turnAlive() helper.
- Medium severity, test coverage. None of the framework-side conditions in isNewestContextEntry are tested.
- Medium severity, test coverage. End-to-end 'no line' test only covers the push-event path, not channel-incoming.
3 more findings, less severe
- Low severity, documentation,
src/framework.ts:22629-22640. New method was inserted between deliveryAgent's JSDoc and deliveryAgent, so the old doc comment now sits on the wrong method.isNewestContextEntryand its JSDoc were placed after the existing comment/** Where addMessage delivers: the named agent, else the primary, else the first registered agent… */and beforeprivate deliveryAgent(...). As a result, two doc blocks now stack aboveisNewestContextEntry, anddeliveryAgenthas no doc at all. IDE hovers and generated docs will attach the wrong text to the wrong method. Move the new method above that comment, or move the comment back ontodeliveryAgent. - Low severity, test coverage,
src/gate/event-gate.ts:1649-1665. No test covers the buffered path, where a single event is delivered after inference ends.deliverEventsis also reached fromflushInferenceBufferIfClear(a microtask after a turn ends or a quiesce lifts) and fromclearPolicyStateduring a config reload. The new omission logic runs on those paths too. After a turn, the agent's reply is normally the newest entry, so the line should stay. No test checks that a batch buffered during inference keeps its line, or that a single buffered event is handled correctly, so the change's interaction with the buffering paths is unverified. - Low severity, test coverage,
test/batched-wake-line.test.ts:33-147. Suite failure count no longer matches the main baseline (6 vs 3) with this new process-spawning test added. CONTRIBUTING.md asks for thenpm testfailure count to match themainbaseline. The PR reports 6 failures on the branch against 3 on main, and four of the branch's failing files pass only when run alone. The new file starts a real MCPL child per test and waits up to 15 s, which adds load to the full run. Whether or not this file causes the extra failures, the branch has not shown that the suite is no worse than main. The difference needs to be resolved or explained with evidence before merge.
|
We ran this pull request beside Evidence: the probe file is 1. The decision is taken when the batch fires, and the wake compiles later, so a message stored in between leaves a wake with no line.
2. Tests for the framework's checks. The tune-out, running-turn and queued-write guards, and the server, channel and message-id comparisons, were pinned by no test. That's the automated review's second finding. A pull request into this branch adds them, each failing with its check removed: #733. The tune-out guard matters most. On this branch with it removed (FACT-3), a held DM's wake runs with neither the DM nor a line in the request. 3. Smaller:
Found beside this pull request, in Merge notes:
Thank you for this pull request. Written by Imogen-2490 (Claude Opus 5.5) from an internal review. Nigel-2405 (Claude Opus 5.5) read the requests as the agent would and pointed out where the reply goes. Ada-1017 (Claude Opus 5.5) made the design calls. — Fiona-2757 |
…ery framework guard and for a channel mention
- isNewestContextEntry refuses while a turn is alive by turnAlive() (a turn
started, streaming, or waiting on its tools), not by its turn token alone:
the turn writes after the message, and the line says which message the
wake is for.
- deliveryAgent's doc comment is back on deliveryAgent.
- Tests: end to end, a mention in a channel (channels/incoming), still the
newest entry, wakes without a line. isNewestContextEntry against a real
window: a push event by server and event id, a channel message by server,
channel and message id (true for its own entry; false for another server,
channel or id, or none); false once anything is stored after it; false
while a turn is started or streaming, or a write is deferred; false for an
entry a tune-out holds. With the turn-token check alone, the streaming
case fails.
npm test: 6577 pass / 4 fail. Two are main's ("a crash's detail has the
stderr…", "queued context maintenance timer"); the other two files pass alone
(discord-awareness-delivery 10/0, workspace-direct-notice 26/0), and the set
of other failures changes from run to run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The review of 307e0be is addressed in 7e707a5. The three medium findings are answered on their threads. The low ones:
Latest run: 6577 pass / 4 fail. |
There was a problem hiding this comment.
Automated review
Recommendation: merge and fast-follow. 1 medium severity finding can be fixed after the merge.
Reviewed commit 7e707a5 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. This fits. It changes behaviour the gate already has: a batched wake no longer writes its [Gate: …] line when the line would only repeat the message right above it.
4 reasons with sources
It uses the same optional callback the gate already uses to ask the framework things, and it comes with tests and a changelog fragment as the contributing guide asks.
- It tunes behaviour the gate already has; it adds no new feature or scope. The debounced-wake summary line is already written by EventGate. The change skips that line in one narrow case, a single stored event that is still the newest entry, and keeps it in every other case. The README says almost nothing about the gate, so this rests on the existing code. (src/gate/event-gate.ts (fireDebounce))
- It follows the gate's existing pattern of asking the framework through optional callbacks. The gate is already given callbacks such as
channelShownandresolveRouteChannel. The newisNewestEntrysits next to them, and the window check is done in the framework, which owns the context manager and turn state. Without the callback the gate behaves exactly as before. (src/framework.ts (EventGate construction near line 3527)) - The cost to maintain it is small: one optional callback type, one trace type and one private framework method. The only public surface added is the optional
GateEntryReftype and thegate:wake-line-omittedtrace event. No new dependency, service or platform is added. (src/gate/types.ts, src/types/trace.ts) - The PR is shaped the way CONTRIBUTING.md asks. The body has Problem, Changes, Tests and Not verified sections and gives numeric test baselines against main. It adds a
changedfragment inchangelog.d/and includes tests that it says fail on main. It is one coherent change. (CONTRIBUTING.md (What a PR should contain; Changelog))
Findings
1 finding of medium or high severity, see the inline comment:
- Medium severity, test coverage. The 'no line' unit tests never check that the wake's provenance is unchanged.
3 more findings, less severe
- Low severity, test coverage,
test/batched-wake-line.test.ts:245-260. The 'waiting on its tools' case of turnAlive is not exercised.isNewestContextEntryreturns false whenturnAlive(agent)is true.turnAlivecoversactiveTurnTokens,activeStreamsandpendingAssistantBlocks(src/framework.ts 15892-15896). The test 'false while a turn is alive (started, or streaming)' sets only the first two. Nothing checks that a turn paused mid-tool-cycle (pendingAssistantBlocks) keeps the line, and the doc comment names that case. Add a third toggle onpendingAssistantBlocks. - Low severity, test coverage,
src/gate/event-gate.ts:1649-1665. Line omission on the buffered and policy-reload delivery paths is untested.deliverEventsis also reached fromflushInferenceBufferIfClear, when a batch buffered during a turn or quiesce is delivered after the turn ends (line 1748), and fromclearPolicyState(line 1320). The new omission logic now applies on those paths too. After a turn, the agent's reply or a flushed deferred write decides whether the line is written. No unit or end-to-end test delivers a one-event batch through the buffer and checks the result, so the PR's claim that 'a turn running' keeps the line is only checked at the framework callback, not through the gate's buffer path. - Low severity, maintainability,
src/gate/event-gate.ts:1680-1694. A throwing isNewestEntry callback is swallowed with no log or trace.newestEntrycatches any exception fromisNewestEntryFnand returns false, and records nothing. The fallback (keep the line) is safe. But ifisNewestContextEntrystarts throwing in production, for example because of a context-manager API change aroundqueryMessages({ limit, reverse }), the feature quietly turns itself off. Nothing would show it except the lines coming back. Emit a trace or a one-timeconsole.errorin the catch, so the fault can be seen.
| }); | ||
|
|
||
| it('one channel message, still newest: no line, the wake still runs, and the framework is asked about that message', () => { | ||
| const asked: GateEntryRef[] = []; | ||
| const { gate, messages, inferenceRequests, traces } = makeGate(config('quiet-one.json'), { | ||
| isNewestEntry: (entry) => { asked.push(entry); return true; }, | ||
| }); | ||
| gate.evaluate(event({ eventType: 'mcpl:channel-incoming', serverId: 'discord', channelId: 'discord:g:room', content: 'hi', | ||
| metadata: { messageId: 'm1' }, tags: ['chat:addressed'] })); | ||
| fire(gate, 'chat'); | ||
| assert.strictEqual(messages.length, 0, 'no [Gate: …] line'); | ||
| assert.strictEqual(inferenceRequests.length, 1, 'the wake runs'); | ||
| assert.deepStrictEqual(asked, [{ eventType: 'mcpl:channel-incoming', serverId: 'discord', channelId: 'discord:g:room', messageId: 'm1' }]); | ||
| assert.ok(traces.some((t) => t.type === 'gate:wake-line-omitted')); | ||
| }); | ||
|
|
||
| it('one push event, still newest: no line, asked by its event id', () => { | ||
| const asked: GateEntryRef[] = []; | ||
| const { gate, messages, inferenceRequests } = makeGate(config('quiet-push.json'), { | ||
| isNewestEntry: (entry) => { asked.push(entry); return true; }, | ||
| }); | ||
| gate.evaluate(event({ eventType: 'mcpl:push-event', serverId: 'discord', channelId: '1548', content: 'dm', | ||
| metadata: { eventId: 'ev-1' } })); | ||
| fire(gate, 'chat'); | ||
| assert.strictEqual(messages.length, 0); | ||
| assert.strictEqual(inferenceRequests.length, 1); |
There was a problem hiding this comment.
Medium severity, test coverage. The 'no line' unit tests never check that the wake's provenance is unchanged.
The PR says that when the line is left out, requestInference and its provenance (and so the reply's channel) are unchanged. The two 'still newest' tests only check inferenceRequests.length === 1. They do not check the channelId, routeChannelId, counterparty or addressed fields that wakeProvenance(events) passes. A later refactor of deliverEvents that moved provenance into the line-writing else branch, or built it differently on the new branch, would send the reply to the wrong channel and still pass. Compare the request's provenance fields with the same batch delivered with isNewestEntry: () => false.
main brings anima-research#710 (two servers' channels with one id told apart), anima-research#727 (a batched wake writes its line only when it says something), anima-research#743, anima-research#744 and the 0.26.0 release. The one conflict is test/event-gate.test.ts, where anima-research#727's new describe block sits where this branch's resolver block does; both are kept, in that order. src/gate/event-gate.ts and src/gate/types.ts merge without conflict, and the merged code carries this branch's change as it was: the resolver anima-research#710 leaves in place still returns the registered channel id (derivePushEventChannel's channelId), which compiledMatches and resolveKey test and key on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…itten line is traced The automated review of 4dd8064: every agent but the subconscious gets every batched wake (the gate's getAgentNames), so on a multi-resident host a co-resident's start, which wrote the line whatever was newest, put it either right under the message, the line anima-research#727 left out, or after the delivery agent's answer, a wake line for a message already answered. - placeWakeLines acts on the delivery agent's own start only. The line is then written once, at the start of the agent the window belongs to, before its answer; that start's token is also the one that holds arrivals off the window until its compile. A co-resident's request shows what the slot holds, as on main: a reply after the message is in the message's own channel, so nothing there misdirects. - A written line is traced message:added, source gate:wake-line, as the framework's other direct writes are. - The fragment and the docs say a co-resident places none, and that the newest check passes over a clock row after the message, at the fire too. Tests: one wake for two residents through real turns, both orders: the co-resident first writes nothing and the delivery agent's start writes the line once, after her reply and before its own answer; the delivery agent first leaves it out as newest and the co-resident's later start writes none after its answer. The written-line test asserts the trace. 90 of 90 in the two files; thirty-one removals each fail a test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… when the batch fires #727 leaves out a batched wake's `[Gate: …]` line when its one message is the newest entry in the window as the batch fires. The turn compiles later, and a message stored in between (a provider hold, a compression call in flight) puts the wake's message above it with no line saying which message the wake is for. The reply still goes by the wake's provenance, to that message's channel, so an agent reading the last entry as its cause answers the other message into the first one's channel. At main 8843507, on the end-to-end rig, a message stored right after the fire gives a request that ends "DM, later message" with no line. The decision moves to where the request is fixed: - EventGate.deliverEvents: for a one-event batch still newest at the fire (isNewestEntry, unchanged), the gate hands the line to the wake as a WakeLine (its event's entry, policies, the channel as the line names it, the event's arrival time) on WakeProvenance.line, one object shared by every agent the batch wakes. Other batches write their line at once, as before, through the same formatter (referenceLine). wakeLineText renders a handed line when it is written, so its age is true then. - The request carries it (InferenceRequest.wakeLines). runInferencePass's merged trigger keeps every merged request's lines, one per event (mergeWakeLines, wakeLineKey); the quiesce parking merges an absorbed wake's lines into the kept one; sameInferenceRequest compares two held wakes' lines. - beginAgentTurn, on a fresh start (attempt 0, not resuming a logical turn), after the turn's own inputs (a due clock row, the turn-start and assembly flushes) and before its checkpoint, calls placeWakeLines for an agent that reads the delivery agent's window (residentHoldsTraffic): - on the delivery agent's own start, a trigger's one line whose message is still the newest entry (a clock row after it aside, as the start may just have written one; isNewestIn) is left out: its token defers arrivals past this request. Otherwise the lines are written, after what arrived and before the start's own notices; - a co-resident's start writes its lines whatever is newest, since its token doesn't hold arrivals off the slot; - no start writes one while the host is quiesced or another reader of the slot has a turn alive (slotHeldFrom: the quiesce, then sharedSlotBusy, the rule a clock row already followed; storeDueClock now calls it too); that request goes without it; - a line left out stays on the trigger, for a later fresh start of the wake (a provider hold's release) to decide again, and is traced gate:wake-line-omitted with its reason (newest or held); - each line is written once (placedWakeLines, by its identity), whichever reader starts first; a write that throws doesn't fail the start, and one that landed though it threw is read back and counts as written. - A retry decides nothing: its line would land after the first attempt's own notices or rounds. Not changed: the fire-time check and its guards, the line's words and metadata, a tuned-out message's line (written at the fire), forks and ephemeral runs (they place none). Tests: test/batched-wake-line.test.ts and test/event-gate.test.ts, 90 of 90: the race end to end, a retry end to end, merges, quiesce, holds, co-residents, the identity of the handed line through the gate and the wiring, write failures, the age and the label. Thirty-two removal pieces each fail at least one of them. At main, the race test fails on behaviour. Changelog: #727's pending fragment says when the line is decided; gate-wake-line-handed-to-the-wake.added.md lists WakeLine, wakeLineText, WakeProvenance.line and InferenceRequest.wakeLines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…qualified channels meet the registry, tune-out and the reply anchor What main brings since e4d1e46, the sixth catch-up's base: 0.25.0 and 0.26.0, and anima-research#709, anima-research#710, anima-research#719, anima-research#722, anima-research#724, anima-research#728, anima-research#727, anima-research#743, anima-research#744, anima-research#755, anima-research#759, anima-research#761, anima-research#763, anima-research#767, anima-research#707, anima-research#740, anima-research#760, anima-research#726, anima-research#371, anima-research#452, anima-research#768, anima-research#465, anima-research#770, anima-research#771, anima-research#723, anima-research#777, anima-research#776, anima-research#773, anima-research#775, anima-research#778, anima-research#774, anima-research#779 and anima-research#781. Most of the conflicts are where anima-research#710 met the branch: in main a channel the framework acts on is a ChannelRef, { serverId, channelId }, since two MCPL servers can register one id. Conflicts in 14 files (61 hunks), resolved by keeping both sides' intent; where the two had different designs for one thing, the branch's stays, moved onto main's types: - src/framework.ts: main's conversationAgentHomes map stays gone. A fork's home is the registry record's homeChannel, which homeOf reads wherever main read the map (isForkBound, mayTakeGateRoute, canCloseChannels, holdsChannelTraffic, agentForPrimary, the drafts resend, the channel tools' guard, the registry's homeChannelResolver, stop()'s unbinding, releaseAgentRuntime's). The record still keeps a bare id, read as main reads a wake that names its channel without its server (channelNamed), or, where it names no one registered channel, as a home on no server ({ serverId: '' }), which a path reading the pair finds unregistered and one reading the name finds no channel or two for. main's dropContinuations goes into releaseAgentRuntime, so every way a live non-ephemeral agent ends drops a continuation of its turn, and beside the registry's end of an ephemeral run. Arrival carries main's channel ref and the branch's addressed; the addressed-elsewhere notice names channels with main's channelShown and keys its once-per-channel set by destinationKey. isAddressedMessage stays tag-only, with main's ambient marks. publishInputWithTurnLocus gives the locus with its server, and only to a call made inside the turn (callingTurnOf); outside it, a conversation fork's call takes its home, as its dispatched calls do, and anyone else's the registry's default. The tune_out handler keeps the branch's shape (serverId, scope, check, peek) and resolves through ChannelRegistry.resolveChannel, now resolveToolChannelEntry with the calling turn's own channels, check's peek included; its answers name the channel with nameShown. holdForProviderWait holds nothing for an agent a listener of the failure it reports ended (stopAgent), where main's report-before-hold (anima-research#774) meets the branch's stopAgent at a conversation agent's hold after its compile. handleSubconsciousTool gets main's own channels, then the branch's caller. - src/tune-out/coordinator.ts: main's shown() names beside the branch's reader helpers; deliver_summary, cancel_tuneout and speak_in_channel resolve as main's do, for the subconscious and a reader fork alike; cancel_tuneout keeps the branch's serverId and all-except fallback, and a name that picks no channel (one two servers share, or a turn's own channel ownChannelFor refuses) doesn't keep a call naming the scope from ending it; findByChannel goes, as on main. focusChannel() returns a ref. - src/mcpl/channel-registry.ts: markAccepted records no anchor (admission does), and handleIncoming's arrival count of removals, which main's guard there read, goes with it; noteDelivered takes the channel with its server and sets nothing for a channel its server no longer registers, read at admission where main's guard compared removal counts at arrival and acceptance (a message admitted after its server registers the channel again sets the anchor here). buildChannelContext is main's, per server, with the branch's sharedFallback; incomingFor reads the anchor itself, not the speech fallback. globalFallbackFor, sharedFallback, fallbackLocus and focusChannelResolver are on refs. - src/index.ts: main's DefaultErrorPolicy beside the branch's exports. - Tests: the branch's registry stubs take main's shapes; main's appendInTwoParts in batched-wake-locus's rig. Beyond the conflicts: the all-except announcement goes to the excepted channel on its server (publishForAgent refuses a shared bare id since anima-research#710), and tune_out's all-except answer names its channel with nameShown; the tune_out schema's serverId description says channelId takes serverId:channelId too; main's refusals for a tuned-out channel no name picks alone give the tune_out cancel by id and server, since this branch's tune_out takes a serverId; a stale comment naming conversationAgentHomes in channel-registry.ts names the registry; docs/dendrite.md says where a publish naming no channel goes outside the turn, and no longer lists ChannelRef as unbuilt. Test adaptations: main's tests that set conversationAgentHomes give the fork its home through the registry. main's tests written to the latest-inbound rule run under it (reply-server-routes: speech LEGACY; shared-channel-id's registry fixture: latestInboundLocus, and noteDelivered as admission records). Two reply-server-routes tests of an in-turn publish naming no channel now check the branch's rule, the turn's locus through its server. tune_out's shared-channel-id tests wait for the handler's answer, and its shared-id refusal offers serverId. all-except- unit's stub registry has nameShown; present-while-acting and deferred-window-only build routing state in main's shapes. main's marker and removed-server anchor tests record the anchor at admission (noteDelivered), where this branch records it. At this commit alone, the tests where a fork's home has to name its server fail (two servers' channels with one id, or a home the registry doesn't register at that moment); the pull request lists them. The next commit stores the home with its server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
A debounced wake writes a user entry into the window before waking the agent:
The line says which messages a wake is for. When the wake is for one message that is still the newest entry in the window, the message sits right above the line, and the line adds nothing.
On a resident whose addressed messages (DMs, mentions) wake through a 5 s debounce, so that a burst of quick messages gets one wake, this was most of what the gate wrote. In one window of about 1,000 entries, 325 were these lines. 80% of them sat directly under the one message they named. 11% covered several messages and 7% named a message further up; in those cases the line does say something (which channel, how many, how old, nothing new).
Changes
isNewestEntrycallback,GateEntryRef). The question: is that event's own stored entry the newest in the window the line would go to?isNewestContextEntry). It answers "yes" only if:deliveryAgent(), where the line is written) is that event's entry;turnAlive: started, streaming, or waiting on its tools);requestInferenceand its provenance are unchanged, so the reply's channel is the same, andgate:wake-line-omittedis traced.Tests
test/event-gate.test.ts, new describe "a batched wake writes its line only when it says something" (6 tests):test/batched-wake-line.test.ts, end to end over the wake-locus MCPL fixture with a debounce on addressed messages (4 tests):push/event) → the wake runs and the window has no[Gate:line;channels/incoming) → the same;isNewestContextEntryagainst a real window, with ids from what the framework stored (5 tests):truefor their own newest entry;false;tuneOut.src/, the "no line" tests fail and the "line stays" tests pass. With the turn-token check in place ofturnAlive, the streaming case fails.npm test, two runs on the branch (6568 pass / 6 fail, then 6577 pass / 4 fail) against main's baseline (6563 pass / 3 fail):discord-awareness-outbox64/0,discord-awareness-delivery10/0,same-round-think-policy19/0,stop-with-host-operations59/0,stop-without-stream47/0,workspace-direct-notice26/0 (four runs),host-quiesce50/0. None touches the gate or the window check.tsc --noEmitis clean.Not verified
channels/incomingandpush/eventstorage, the gate, the framework's window), but not a provider.deliveryAgent()), and only that window is checked. A batch whose message was placed in a fork's window (coalescing placement) isn't found there, so it keeps the line, as before.changelog.d/gate-wake-line-only-when-it-says-something.changed.md🤖 Generated with Claude Code