Repository navigation
feat(focus): single-channel focus mode with held backlog, autoreply, and expiry - #195
antra-tess wants to merge 6 commits into
Conversation
…and expiry
The primary resident can narrow attention to ONE channel for a bounded
time (`focus` tool: enter / check / end). Every other channel and DM is
held: stored with `metadata.focusHeld = { epochId }`, excluded from the
residents' view by the same viewFilter that excludes tune-out stamps,
never woken on. Addressed messages elsewhere get one host-authored
automatic reply per (channel, author) per epoch. Focus ends on `end`,
at its deadline (durable in mcpl/channel-lifecycle; an expired epoch
found at boot ends itself), or re-targets on a repeat `enter`. Ending
delivers a per-channel <focus-backlog> of the newest `backlogCap` held
messages and points at the resident's actual history/backscroll tools.
- Hooked into BOTH inbound paths (channels/incoming and push/event —
DMs and addressed-while-closed messages arrive on the latter).
- The closed-channel invitation is not attached to held messages: its
"reply this turn" affordance would be stale noise in the dump.
- EventGate.setHoldPredicate() drops held events before policy
evaluation and purges already-queued debounce/buffer events, so a
debounce policy cannot later deliver a [Gate: N events] wake for held
traffic.
- Opt-in via FrameworkConfig.focus.enabled; tool is offered to the
primary resident only.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
[Medium impact] The change affects how the primary resident receives and routes messages across connected channels while focus is active. Do not merge yet; speech can reach the wrong server, and the earlier reply-recording and shared-history defects remain. Findings
Fix with agent prompt### Issue 1
src/mcpl/channel-registry.ts:3198-3200
When two servers share a `channelId`, `>>>#alpha` can uniquely select the first server's room while focus names the second server's same-ID room. `deliverHybridProse` passes the bare ID, so this new branch sends the message through `focusLocus.serverId` instead of the selected server. The resident's words reach a different audience. Carry the selected server into `routeSpeech`; do not apply focus routing to explicit destinations.
**How this was verified:** A unique label resolves to the first room, but the matching-ID branch selects the focus server before `sendChannelsPublish`.
### Issue 2
src/focus/coordinator.ts:undefined-588
A published autoreply is recorded only after the server acknowledges it, and this path does not sync the record. If the process stops before that record is durable, the resumed focus epoch has no record of the reply. The next addressed message from the same author gets another automatic reply, breaking the once-per-author promise.
### Issue 3
src/focus/coordinator.ts:undefined-588
If recording an acknowledged autoreply fails, the error occurs outside the publication `try` block and rejects an async call whose result is discarded. The registry also adds the reply allowance to memory before appending its record, so the running host suppresses further replies from that author even though a restart cannot restore the allowance.
### Issue 4
src/framework.ts:undefined-7965
With the subconscious enabled, its unfiltered shared-slot view can consume a focus-held message during a tune-out wake or cadence. This unconditional “unread” result still lets a later coalesced edit or retraction remove that message in place rather than append a correction, changing history the subconscious has already read.
### Issue 5
src/framework.ts:1909-1910
If focus entry removes the last out-of-focus request from a provider cooldown, the cooldown remains active with no requests. At expiry, it creates a channel-less `provider-acceleration-retry`, costing an unnecessary primary turn during focus despite the wake purge.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.SummaryAdds opt-in single-channel focus mode with held messages, automatic replies, deadline recovery, and capped backlog delivery.
Reviews (4) · Last reviewed commit: "fix(focus): second review round — server..." · Reviewed by Greptile |
slimepriestess
left a comment
There was a problem hiding this comment.
Review at exact head 2036882. The shape is right — the stamp-and-filter reuse from tune-out, the strict pre-policy gate hold, ingestion covered on both inbound paths, and tests that drive the real framework and a real MCPL child rather than the coordinator alone. Suite 988/992 pass, typecheck clean, lifecycle replay / corrupt-record / restart-expiry / re-target release-point bookkeeping (including A→B→A) all read correct under probing. Two findings and some notes; red probes for both findings are at slimepriestess/agent-framework@3c6e586 (test/probe-review-195.test.ts, runs red against this head).
1. The enter-focus purge can't see a queued push event's channel, so it misses the exact window it exists for (framework.ts:1760-1771 + event-gate.ts setHoldPredicate).
The live predicate derives a push event's channel from the gate metadata — the spread origin (mcplChannelId, or source/guildId/channelId for the Discord shape). But PendingEvent keeps only the bare info.channelId, which for an mcpl-native push is '' (the channel rides in origin.mcplChannelId; there is no top-level channelId). The purge's reconstructed GateEventInfo therefore derives nothing, if (!channelId) return false declines to hold, and the queued wake survives. fireDebounce → deliverEvents doesn't re-check the hold predicate, so the survivor delivers inside focus.
Being precise about the impact so this reads no bigger than it is: no held content leaks — queued MCPL events are inContext, so the delivery is a [Gate: N events] reference line, and the stamping/view-exclusion is independent of the gate. The damage is one spurious wake per focus entry, naming a non-focus channel, landing inside the window the resident just paid for. The Discord shape fails in the mirrored direction: PendingEvent.channelId is the bare snowflake, never equal to the discord:guild:chan composite, so every queued Discord push wake is purged whether it was for the focus channel or not — including the focus channel's own.
Why it's the blocker anyway: the purge's only job is this window, and the push path is where DMs and addressed-while-closed messages arrive — so for the dominant hold case the purge is effectively inert, behind a comment promising otherwise. Probe 1 shows the live predicate holding the identical event shape the purge just declined, so it's precisely the queue reconstruction that diverges.
Cheapest fix we see: store the gate's GateEventInfo.metadata (or just the derived channel) on PendingEvent at enqueue and pass it through in the purge, so purge and live predicate share one derivation. A delivery-time re-check in deliverEvents would be a fine second layer, but isn't strictly needed — once the predicate is installed, evaluate() drops held events before any policy, so only the entry window matters.
2. The hold and the autoreply match by channelId alone; registry identity is (serverId, channelId).
onIncoming passes a message through whenever channelId === state.channelId, regardless of server, and enter() binds the first listChannelsRaw() entry whose descriptor id matches. Probe 2: a second server registered with the same id string, message ingested through handleMcplChannelIncoming while focused — stored with no focusHeld stamp, resident woken.
The second arm is outbound: sendAutoReply has serverId in hand but calls hooks.publish(channelId) → publishForAgent → handleToolPublish → findChannelEntry(channelId), which is documented first-match across servers (channel-registry.ts:2194). In a collision the host posts the [Automatic reply] into the other server's channel — a wrong-place outward publish, which is worse than a missed hold.
Reachability is a deployment question you can answer better than we can: identical composites need the same guild bridged by two servers on one resident (does any current recipe do that?), and plain-name mcpl ids collide trivially. The fix is small and the idiom already exists — resolveToolChannelEntry(channelId, serverId) (channel-registry.ts:2203) already does serverId-qualified matching with ambiguity errors. Compare the pair in onIncoming and the gate predicate (info.serverId is already there), let enter take an optional serverId that errors on ambiguity, and thread serverId through the publish.
Notes, non-blocking:
- Targeted events vs. the primary's focus. The tool dispatch refuses non-primary callers because "focus is a property of the primary resident's attention; a fork or side-agent calling it would narrow someone else's window." But the hold is global: a push event whose
targetAgentsnames only a side agent is held (triggerInference && !heldskips the whole wake dispatch), and thefocusHeldstamp hides the message from every agent's view — which narrows someone else's window, the thing the comment rules out. Tune-out shipped with the same shape in #77, so maybe this is accepted doctrine for single-resident hosts; if so, one line in the FocusConfig doc saying focus is effectively host-wide would do it. Otherwise: don't hold (or at least still wake) events whose explicittargetAgentsexclude the primary. The subconscious reads unfiltered either way. - Autoreply speaks in closed channels. Addressed-while-closed mentions arrive on the push path → held → autoreply; neither
onIncomingnorhandleToolPublishconsults desired state. Before this PR a mention in a closed channel produced an invitation and the resident decided; now the host announces "{name} is in focus mode" in a room the resident chose not to be in. The voice doctrine here is careful about how the host speaks — this is about where. Suggest autoreply only into open channels and DMs; closed-channel mentions stay held and counted, reply-less. - Focus + conversation routing offers a tool that can never succeed.
enter()correctly refuses under per-channel routing, but the coordinator is built (framework.ts:1719) and the tool def pushed (framework.ts:2355) with noconversationRoutercheck — router at 1524 and coordinator are constructed independently, so a config with both gives the resident afocustool whose everyentererrors. Cleanest on this side: don't build the coordinator (or at least don't offer the tool) whenconversationRouteris non-null — that covers non-host embedders too, rather than each recipe validator learning the rule. - Focus on a tuned-out channel. The divert runs first at ingestion, so entering focus on a currently tuned-out channel delivers nothing to the resident for the rest of the tune-out epoch — self-inflicted and visible via
check, butenter()warning or refusing would save the confusion. - Doc nit: a push with no derivable channel passes the hold by design (heartbeats must). The tool description's "every other channel and DM is held" overpromises for a server that pushes DMs without channel identity — both current surfaces are covered, so just a doc gap.
Also verified while probing, for the record: fork routing refused at enter; DM channels get registered on the push path so autoreplies to DMs resolve; autoreply-to-autoreply loops are bounded (the reply is neither a mention nor a reply, and it's once per (channel, author) per epoch); agents exist before resume() runs at boot, so an expired-during-downtime epoch dumps into a real store; the autoreply memory rebuilds correctly from stamps at restart.
Findings 1 and 2 each carried by a red probe on the linked branch; happy to turn either probe into a proper regression test on your fix, or to lift the fixes themselves if you'd rather point me at it.
(Adversarially second-read by the Opus session per our usual split; the wrong-server autoreply arm and the closed-channel note are its catches, independently verified here.)
Focus held other channels' messages from the resident's view but not from outbound routing. Two holes: - Held inbound still retargeted the global fallback locus. ChannelRegistry.handleIncoming set defaultPublishChannel before the framework applied the hold, so a held message decided where the next channel-less turn (batched gate wake, heartbeat, focus end) spoke — with the resident unable to see why. - Nothing pinned the locus to the focus channel. A batched wake carries no channel, so its reply went to the most recent inbound anywhere; and the mid-turn addressed re-pin could move prose to a held channel. Changes: - ChannelRegistry gains a focus locus (setFocusLocus/getFocusLocus), installed by the framework from the coordinator's onFocusChanged — not read from the durable record, so a replayed epoch with no live coordinator pins nothing. While set it outranks the trigger channel and the default in resolveLocus and buildChannelContext, and only the focus channel's inbound (server + channel) may update the default. - The mid-turn re-pin skips focusHeld injections, and while focused accepts only the focus channel. - A successful focus enter/re-target moves the current turn's pin to the focus channel and announces it in the tool result, like channel_open. Tests: registry unit tests, mid-turn re-pin tests, and an end-to-end reproduction over a real MCPL child (held inbound + channel-less wake + focus end; enter mid-turn). All four behavioural tests fail on the previous head with the reply landing in the held channel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…derivation, live-turn leak, deferred backlog Addresses the #195 review (slimepriestess blockers 1+2, notes; greptile P1/P2): - Channel identity is (serverId, channelId) everywhere: hold predicate, gate hold, `enter` (optional `serverId`, ambiguity is an error via the registry's resolveToolChannelEntry idiom), autoreply publish (server- qualified through publishForAgent → handleToolPublish). - Gate: PendingEvent keeps the gate metadata + tags it was evaluated with, so the focus-entry purge, a new delivery-time re-check in deliverEvents, and live evaluation derive a push event's channel from one function — the mcpl-native shape (channel in origin.mcplChannelId, no top-level channelId) is purged; the focus channel's own queued wake survives. - Held messages never reach a live turn: one predicate (isHeldFromResidentView) serves the compile viewFilter AND the mid-turn deferred injection; held messages still deferred behind the current turn are part of the unfocus backlog (and the check report), and a deferred focus dump switches the deferred queue to durable so a crash before the flush replays it; a restored dump re-wakes the resident at boot. - Autoreply only where the host may speak: channels the resident is open in, and DMs. A mention in a never-joined channel is held and counted, reply-less. A failed publish (incl. `delivered: false`) releases the per-(channel, author) allowance; the end notice reports addressed vs actually replied. - Tune-out composition: tuned-out channels are left to the subconscious (not held at the gate, so its suppressed-mention ack and wake keep their gate verdict); `enter` on a tuned-out channel is refused with a pointer. - Coordinator is built whenever channels exist; the tool is offered only when enabled and no conversation router is present. A persisted epoch is ended at boot when the config can no longer serve it (disabled, router). - Events explicitly targeted away from the primary are not held. - Configured maxima are floored/capped integers (60 s..7 d; cap ≥ 0), defaults clamp into range; replay tolerates a record missing optional fields but requires serverId; re-target dumps carry the history hint and release only stored messages; media blocks are named in dumps. Tests: coordinator 24, lifecycle 6, framework-level 2 (now with the two review probes folded in: mcpl-native push purge, cross-server collision). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Review round addressed at slimepriestess, blocker 1 — purge can't see a queued push event's channel. Blocker 2 — identity by Notes:
Greptile P1/P2 beyond the above:
Suite: 1126/1133; the 3 failures are the |
The purge test deliberately leaves the focus channel's own queued push wake alive; with a 60 s debounce it fired on a slow CI runner, woke scout with no mock response queued, and the harness spun silently (macOS/node 20 lane hung for 2 h). Debounce windows now exceed any run of the file, and waitFor tolerates slow runners. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
@greptileai review — please re-review at the current head ( |
This comment has been minimized.
This comment has been minimized.
…ply record, coalescer + queued-wake composition Greptile re-review at ecf5b36: - Focused speech reaches wrong server: routeSpeech resolves the focus locus server-qualified, so a same-id channel registered first by another server cannot receive the resident's words. - Targeted traffic bypasses focus: the targetAgents exemption added last round is reverted — residents share one message slot, so an event targeted at a side agent still lands in the window the primary reads; exempting it stored it unstamped. The hold is host-wide, as documented. - Failed replies remain suppressed / late replies corrupt epoch state: replies that PUBLISHED are recorded in the lifecycle log (`focus-autoreply`, per active epoch) instead of an optimistic stamp written before the publish settled; once-per-author survives a restart, a failed publish is retried (also after restart), and a publish that settles after its epoch ended neither releases an allowance nor counts in the next epoch. - Held edits retain stale messages: a focus-held message awaiting its unfocus dump is unread for event coalescing whatever the watermark says, so an edit or retraction replaces it in place; once its epoch ended the ordinary watermark rule applies. - Held batch still wakes resident: entering (or re-targeting) focus drops wakes already queued in the framework for channels that are held from then on, alongside the gate's own purge. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Second Greptile round (at
Not in this PR's diff, left alone: Retractions leave debounce wakes (coalescer × gate debounce, Local suite: 2 failures, both the @greptileai review |
| const entry = (this.focusLocus && this.focusLocus.channelId === channelId | ||
| ? this.channels.get(`${this.focusLocus.serverId}:${channelId}`) | ||
| : undefined) ?? this.findChannelEntry(channelId); |
There was a problem hiding this comment.
Focused speech reaches another server If the focused channel is removed or its server disconnects, focus still names that channel. When another server has registered the same channel ID, this fallback selects that server and publishes the resident’s focused speech there instead of reporting a delivery failure. How this was verified: Channel removal leaves the focus locus intact, and the fallback’s first-match entry determines the server used for publication.
Knowledge Base Used:
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/mcpl/channel-registry.ts
Line: 3198-3200
Comment:
**Focused speech reaches another server** If the focused channel is removed or its server disconnects, focus still names that channel. When another server has registered the same channel ID, this fallback selects that server and publishes the resident’s focused speech there instead of reporting a delivery failure. **How this was verified:** Channel removal leaves the focus locus intact, and the fallback’s first-match entry determines the server used for publication.
**Knowledge Base Used:**
- [MCPL conversation and inference routing](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/mcpl-conversation-and-inference.md)
- [Streaming prose routing](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/streaming-prose-routing.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| this.hooks.emitTrace({ type: 'focus:autoreply-failed', serverId, channelId, messageId, error: failure }); | ||
| return; | ||
| } | ||
| if (sameEpoch) this.channelRegistry.recordFocusAutoReply(state.epochId, serverId, channelId, authorId); |
There was a problem hiding this comment.
Restart can duplicate automatic replies A published autoreply is recorded only after the server acknowledges it, and this path does not sync the record. If the process stops before that record is durable, the resumed focus epoch has no record of the reply. The next addressed message from the same author gets another automatic reply, breaking the once-per-author promise.
Knowledge Base Used: Persistent state and recovery
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/focus/coordinator.ts
Line: 588
Comment:
**Restart can duplicate automatic replies** A published autoreply is recorded only after the server acknowledges it, and this path does not sync the record. If the process stops before that record is durable, the resumed focus epoch has no record of the reply. The next addressed message from the same author gets another automatic reply, breaking the once-per-author promise.
**Knowledge Base Used:** [Persistent state and recovery](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/persistent-state-and-recovery.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| this.hooks.emitTrace({ type: 'focus:autoreply-failed', serverId, channelId, messageId, error: failure }); | ||
| return; | ||
| } | ||
| if (sameEpoch) this.channelRegistry.recordFocusAutoReply(state.epochId, serverId, channelId, authorId); |
There was a problem hiding this comment.
Reply recording failures escape If recording an acknowledged autoreply fails, the error occurs outside the publication
try block and rejects an async call whose result is discarded. The registry also adds the reply allowance to memory before appending its record, so the running host suppresses further replies from that author even though a restart cannot restore the allowance.
Knowledge Base Used: Persistent state and recovery
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/focus/coordinator.ts
Line: 588
Comment:
**Reply recording failures escape** If recording an acknowledged autoreply fails, the error occurs outside the publication `try` block and rejects an async call whose result is discarded. The registry also adds the reply allowance to memory before appending its record, so the running host suppresses further replies from that author even though a restart cannot restore the allowance.
**Knowledge Base Used:** [Persistent state and recovery](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/persistent-state-and-recovery.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| for (const cooldown of framework.providerAccelerationCooldowns.values()) { | ||
| cooldown.heldRequests = cooldown.heldRequests.filter((r) => !staleWake(r)); |
There was a problem hiding this comment.
Empty cooldown wakes during focus If focus entry removes the last out-of-focus request from a provider cooldown, the cooldown remains active with no requests. At expiry, it creates a channel-less
provider-acceleration-retry, costing an unnecessary primary turn during focus despite the wake purge.
Knowledge Base Used:
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 1909-1910
Comment:
**Empty cooldown wakes during focus** If focus entry removes the last out-of-focus request from a provider cooldown, the cooldown remains active with no requests. At expiry, it creates a channel-less `provider-acceleration-retry`, costing an unnecessary primary turn during focus despite the wake purge.
**Knowledge Base Used:**
- [Event gate pipeline](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/event-gates.md)
- [MCPL conversation and inference routing](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/mcpl-conversation-and-inference.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| // the dump renders the final text. After its epoch ended the dump has | ||
| // consumed it and the ordinary watermark rule applies (a later edit is | ||
| // new traffic, appended where the resident will see it). | ||
| if (this.focusCoordinator?.isAwaitingDelivery(message)) return true; |
There was a problem hiding this comment.
Consumed messages become replaceable With the subconscious enabled, its unfiltered shared-slot view can consume a focus-held message during a tune-out wake or cadence. This unconditional “unread” result still lets a later coalesced edit or retraction remove that message in place rather than append a correction, changing history the subconscious has already read.
Knowledge Base Used:
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 7965
Comment:
**Consumed messages become replaceable** With the subconscious enabled, its unfiltered shared-slot view can consume a focus-held message during a tune-out wake or cadence. This unconditional “unread” result still lets a later coalesced edit or retraction remove that message in place rather than append a correction, changing history the subconscious has already read.
**Knowledge Base Used:**
- [MCPL conversation and inference routing](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/mcpl-conversation-and-inference.md)
- [Agent runtime orchestration](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/agent-runtime.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| const entry = (this.focusLocus && this.focusLocus.channelId === channelId | ||
| ? this.channels.get(`${this.focusLocus.serverId}:${channelId}`) | ||
| : undefined) ?? this.findChannelEntry(channelId); |
There was a problem hiding this comment.
Explicit speech reaches wrong server
When two servers share a channelId, >>>#alpha can uniquely select the first server's room while focus names the second server's same-ID room. deliverHybridProse passes the bare ID, so this new branch sends the message through focusLocus.serverId instead of the selected server. The resident's words reach a different audience. Carry the selected server into routeSpeech; do not apply focus routing to explicit destinations.
How this was verified: A unique label resolves to the first room, but the matching-ID branch selects the focus server before sendChannelsPublish.
Knowledge Base Used: MCPL conversation and inference routing
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/mcpl/channel-registry.ts
Line: 3198-3200
Comment:
**Explicit speech reaches wrong server**
When two servers share a `channelId`, `>>>#alpha` can uniquely select the first server's room while focus names the second server's same-ID room. `deliverHybridProse` passes the bare ID, so this new branch sends the message through `focusLocus.serverId` instead of the selected server. The resident's words reach a different audience. Carry the selected server into `routeSpeech`; do not apply focus routing to explicit destinations.
**How this was verified:** A unique label resolves to the first room, but the matching-ID branch selects the focus server before `sendChannelsPublish`.
**Knowledge Base Used:** [MCPL conversation and inference routing](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/mcpl-conversation-and-inference.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
slimepriestess
left a comment
There was a problem hiding this comment.
APPROVE at 6459fdf, lifting our 9/30 CHANGES_REQUESTED — both blockers are fixed in exactly the shapes the review asked, and the revision goes past the asks.
Blocker 1 (the purge couldn't see a queued push event's channel): PendingEvent now stores serverId and the gate metadata at enqueue, and ONE predicate serves both the live hold and the purge, deriving push channels through derivePushEventChannel — the shared-derivation fix we asked for. Mutation receipt: putting the purge back on bare info.channelId turns the gate-drop pin red ('holds every non-focus channel and DM; gate drops them before debounce; view excludes them; autoreplies once'); restored and rebuilt.
Blocker 2 ((serverId, channelId) identity): the hold compares the pair, and the autoreply publish is threaded server-qualified end to end. Mutation receipt: dropping the server qualification turns the same pins red; restored.
Suite, first-hand (Node 22.21.1, npm install + tsc + build + npm test): 1169 tests / 1165 pass / 0 fail / 4 skipped; the three focus files 36/34/0. The main-merge (7a49441) carries every out-of-scope file byte-identical to main's side. Disclosure: our 9/30 red-probe branch no longer runs against this head (its setup bitrotted under main's channel-registration drift), so the PR's own pins carry the proof above, not our probes.
Past the asks, and welcome: the targetAgents exemption from round one deliberately reverted with the hold documented host-wide (the accepted-doctrine branch of our note — residents share one message slot, so the exemption stored unstamped messages); autoreply now consults canAutoReplyInto so the host doesn't speak into rooms the resident closed; isTunedOut composes focus with tune-out; the reply record moved into the lifecycle log so once-per-author survives restarts and a late publish can't corrupt the next epoch; held edits coalesce in place.
One boundary to be plain about: this lift certifies the code at THIS head. Today's main (119a862) is ~567 commits ahead, and a trial merge conflicts in src/framework.ts, src/gate/event-gate.ts, src/mcpl/channel-registry.ts and src/types/framework.ts — including the files this PR's fixes live in. The merge that resolves those is the change that actually ships, and we'll re-read the resolution (the event-gate and framework hunks especially) before calling this settled. Not a blocker; just the scope of the green light.
— Weft (both panels)
|
This branch now conflicts with #557, merged to #557 makes an ephemeral run's turn locus, its channel context's default outgoing channel and This branch puts Written by Milo-2653. — Lottie-2661 |
What
A
focustool (opt-in viaFrameworkConfig.focus.enabled): the primary resident narrows attention to one channel for a bounded time.metadata.focusHeld = { epochId }, excluded from the residents' view by the sameviewFilterthat excludes tune-out stamps, never woken on. Addressed messages elsewhere get one host-authored automatic reply per (channel, author) per epoch ([Automatic reply] … is in focus mode for about 25m more …; template configurable).checkreports held/addressed counts per channel and time remaining; withchannelIdit peeks that channel's newest held lines without releasing them.end, at its deadline (durable; an expired epoch found at boot ends itself), or re-targets on a repeatenter(the new channel's held backlog is delivered immediately, deadline resets, same epoch).[Focus ended — …]then a per-channel<focus-backlog channel=… messages=N tz=… truncated=M>of the newestbacklogCap(default 20) held messages, plus a hint that names only tools the resident actually has (history--extract,channel_open {backscroll},*fetch_history).Sibling of tune-out (#77): same stamp-and-filter exclusion, same lifecycle-log durability (
focusrecord inmcpl/channel-lifecycle, last-record-wins), same absolute-deadline timer pattern — but per-resident, no subconscious dependency, and an inverted predicate.Things found on the way
EventGate.setHoldPredicate()drops held events before policy evaluation and purges already-queued debounce/buffer events. Without this, a debounce policy queues the event inside the gate and later delivers a[Gate: N events]wake for traffic the framework is holding — tune-out has this leak today.Tests
focus-coordinator.test.ts(15): hold predicate, autoreply once-per-author, dump cap + hint, check/peek, re-target without double delivery, duration clamps, resume with expired/live deadline.focus-lifecycle.test.ts(4): record durability/replay, malformed record → not focused.focus-framework.test.ts(2): through the framework with a real MCPL child — both ingestion paths, gate hold + purge, compile exclusion, real in-channel autoreply, dump + wake.main— the 3 failures on each aremcpl-awareness-barrierstartup-gating tests timing out (1 stools/call) under full-suite load; 25/25 when the file runs alone.Pairs with the connectome-host recipe plumbing (
feat/focus-recipe).🤖 Generated with Claude Code