feat: tune-out — subconscious summaries instead of unsubscribing (#77) - #115
Conversation
Concurrent PRs editing the shared '## Unreleased' section of CHANGELOG.md conflict whenever one PR outlives another merge (e.g. anima-research#115, whose only textual conflict against main is CHANGELOG.md). Entries now land as uniquely-named fragment files (changelog.d/<slug>.<category>.md), which git merges without conflict; the npm-version hook folds them into the release section and deletes them. Direct '## Unreleased' edits remain supported and are merged at the same point, so in-flight PRs need no rework. Tag-time publish guard and github-release job are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
slimepriestess
left a comment
There was a problem hiding this comment.
CHANGES REQUESTED at head acdc692 — same shape as my cm#54 review: the design and code are good, verified end-to-end locally; what blocks is one encoding defect and the rebase both PRs need anyway. Fix those and this has my approve, contingent on cm#54 landing first (reviewed there).
Blocker 1 — src/tune-out/coordinator.ts is a binary file. Raw NUL byte at line 81: `${serverId}\x00${channelId}` — the composite-key separator written as the literal byte instead of the \0 escape. Runtime-identical; catastrophic for reviewability: GitHub shows "Binary file not shown" for the 548-line file that is the feature. Nobody can have line-reviewed this in the web UI, and it would land unreadable in history. Same one-character fix as cm#54's test file (which has the same artifact — worth grepping the branch for any other raw \x00 before pushing: git grep -P '\x00' -- '*.ts').
Blocker 2 — rebase onto current main (CONFLICTING; main has moved through #136–#140 since 8/18). Plus the declared dependency flip once cm#54 publishes.
Verified first-hand — I read the coordinator, the framework wiring, the gate diff, and the registry's lifecycle-log additions in full, and ran everything:
- Zero new test failures. Branch suite vs merge-base baseline under identical env: failure sets identical (41 pre-existing env-dependent failures on both sides — 429/auxiliary/puppet/changelog classes; none tune-out, none yours). All five of your new test files pass. Build/tsc clean. (The claimed green is real — the baseline noise is our environment, not your branch.)
- Durable wake accounting is right. Counts live in the lifecycle log, replay folds them only while the recording epoch is still the active desired state — so restart can't reset a hammered channel's budget AND a superseded epoch's counts can't leak into a fresh one (your test pins both directions). Deadline-passed-during-downtime cancels on resume: the resident gets the backlog on the schedule they committed to. Good.
- Gate composition is exactly what antra specced: the resident's verdict rides the event as a precondition; a gate-muted author's mention diverts with no ack — the comment names why (a reaction would leak the mute to its subject), and the test pins it.
- Voice doctrine is mechanically enforced, not aspirational: host framing is
[Tune-out: …]brackets and the backlog wrapper undersystemmetadata; the subconscious's words travel only verbatim under its own name;proseRouting: 'explicit'+ the deliberately small tool surface make the leak structurally unavailable rather than merely discouraged.speak_in_channeldefaulting off pending the canary matches the issue. - Config spread inheritance: principal's config as base with only identity/surface overrides,
systemPromptREQUIRED on SubconsciousConfig (so the explicit override can't silently clobber the principal's prompt with undefined), strategy owned by its CM. The self-wake fix in event-gate (notice delivered to the waking agent, not narrated into the primary) is a real pre-existing-bug fix riding along — good catch. - The stamp-permanent / dump-as-delivery choice is the right KV call, and it composes with cm#54's determinism contract: visibility is decided once at ingestion, forever.
Nonblocking notes, none needing a round-trip:
backlogCap: 0is accepted (Math.max(0, …)) and produces an empty<tuned-out-backlog>wrapper — "summary only, no raw dump" seems intentional and useful; one line in the tool description would make it official.- The mention-burst that trips max-wakes gets
suppressed-tuned-outacks and then immediate backlog delivery via auto-cancel — the ack slightly overstates suppression for that final burst. Cosmetic; the mention-er's signal ("they didn't see this live") stays true. - After the oldest epoch cancels, the subconscious anchor stays at its old position until the next enter/restart recomputes — conservative direction, and the dump path is unwindowed so delivery is never affected; noting so nobody reads the anchor as "start of oldest ACTIVE epoch" invariantly.
durationSecondssilently floors to 60s (Math.max(60, …)) — fine, but the tool schema doesn't say so.
The arc holds end-to-end: divert→stamp→exclude→ack→coalesced wake→durable count→auto-cancel→capped dump→report, each joint pinned by a test that actually exercises it (the real-MCPL-fixture e2e especially). With the encoding fixed and the rebase done, this is the best-argued feature PR I've reviewed in this repo.
…nima-research#77) 'tuned-out' joins open/closed in the mcpl/channel-lifecycle append-log, carrying its epoch params (epochId, cadenceSeconds, backlogCap, maxWakes, startedAtSequence). Transport-wise a tuned-out channel is OPEN — traffic must keep arriving for the subconscious — so reconcile treats it as open and only main's wake/visibility diverts, downstream at ingestion (next commits). Wake counts are durable 'tune-out-wake' records in the same log, replayed last-record-wins and epoch-checked, because gate runtime stats die with the process and a restart must not hand a hammered channel a fresh max-wakes budget. Re-entering under a new epoch supersedes the old count. Guard rails: channel_open on a tuned-out channel refuses with a pointer at the cancel flow (a plain open would silently discard the epoch and its pending backlog dump); machine-sourced closes treat tuned-out as stated agent intent, same as explicit opens; desired-state records for 'tuned-out' without params are dropped at replay (old/foreign writers degrade safely — pre-existing behavior, now pinned by a test). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ing (anima-research#77) The full arc: tune_out (per channel: cadence, backlog cap, max-wakes) → incoming traffic stamped (metadata.tuneOut={epochId}), stored in the shared slot, diverted from resident wake AND compiled view (cm viewFilter at the strategy-view choke point — no emission leak, no L1-compression leak) → addressed messages get the deterministic channels/acknowledge (intent 'suppressed-tuned-out', grant-gated) and, with gate-privileged authors, a coalesced subconscious wake → durable wake counting with max-wakes auto-cancel → cancel delivers the system-framed capped <tuned-out-backlog> dump plus one resident wake, and gives the subconscious its own report turn; stamped originals stay view-excluded permanently (the dump is the delivery — KV-prefix-stable append). The subconscious: a persistent resident on the conversation-fork template — isolated slot subconscious/<primary>, merged unfiltered view (own + shared slots) under WindowedPassthroughStrategy, excluded from every broadcast fan-out, never primary, proseRouting explicit, tool surface deliver_summary / cancel_tuneout / note_disposition / speak_in_channel (off by default pending the voice-block canary) plus think/skip_reply/end_turn. Voice doctrine mechanically enforced: its words reach the resident verbatim under participant 'Subconscious' (Context Manager precedent); host framing is system-styled bracket notices and the backlog wrapper, never voiced. Plumbing folded in (would have been separate PRs): targetAgents honored on the channel-incoming fan-out (declared-but-dead field goes live, mirroring the push path; untargeted broadcast unchanged); per-agent message delivery (addMessage {forAgent} with deferral/turn-alive evaluated against the target's state, flushed at the target's boundaries; gate self-wake notices deliver to the waking agent). TEMPORARY: @animalabs/context-manager pinned to the cm PR branch (Meganeuridae/context-manager#feat/strategy-view-composition); flips to the published version at integration, after cm#54 merges. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tune-out-lifecycle: durable third state — round-trip, restart-surviving wake counts, epoch supersession, malformed-record replay safety. channel-incoming-targeting: fan-out asserted at the pendingRequests seam (MockMembrane hands its whole queue to the first stream, so concurrent two-agent turns starve one and restart-loop — harness limitation). per-agent-delivery: target-scoped deferral, survival across the primary's boundary, loud unknown-target drop. tune-out-e2e: the full arc against a real stdio MCPL fixture (0.5 handshake, initial-policy Request, itemized register): divert/stamp/ exclude + merged-view visibility + suppressed-mention ack + coalesced wake with durable count + max-wakes auto-cancel + capped dump + the subconscious's cancel-report notice + permanent exclusion after cancel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Antra's requirement for named side agents: same settings as the main agent for reasoning effort, attachment size, and kin. The subconscious's AgentConfig now spreads the primary's config as its base (the conversation-fork idiom), overriding only identity- and surface-defining fields (name, mode-block systemPrompt, strategy instance, explicit proseRouting, restricted tool surface). thinking, providerParams, maxTokens/maxStreamTokens, contextBudgetTokens, cacheTtl, promptCaching, temperature, and refusalHandling — present and future fields alike — ride along automatically. Its windowed strategy mirrors the principal's strategy.maxMessageTokens so both truncate oversized tool results and attachments under the same policy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per antra (#tuneout-talk): the subconscious is affected BY main's wake gate, it does not replace it. The gate verdict (already riding the incoming event as triggerInference) composes as a conjunct on the divert-wake condition: gate-suppressed events — muted authors, rate-limited patterns, whatever the resident configured — divert silently into the backlog and surface in cadence summaries, but wake nobody. Gate-suppressed mentions also get NO deterministic reaction: main would not have signaled either, and reacting would leak the resident's standing mute to its subject. Four-case unit coverage (gate × addressed/privileged), incl. the still-stamped guarantee on the suppressed path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Approved by antra (#tuneout-talk): optional. The enter params gain durationSeconds; when set, the epoch carries an absolute expiresAtMs deadline and auto-cancels through the standard cancel flow (dump + notices + resident wake, reason 'duration elapsed'). Addresses the forgotten-channel failure mode: a quiet diverted channel now has a guaranteed return path the resident committed to at entry. Deadlines survive restarts — an epoch that expired while the host was down cancels at resume, delivering the backlog on the promised schedule. Unset = until cancelled, exactly as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per antra (#tuneout-talk): dispositions need not repeat per invocation — but they must never wash away, because the subconscious has no long-term memory. Both constraints pick the same mechanism: a system-position ContextInjection on its compiles. Never in the timeline (no repetition), structurally outside window content (cannot scroll out), and the bytes change only when a disposition changes (KV-friendly). Invocation notices are now pure triggers; the durable snapshot slot and note_disposition are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…very Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t a raw NUL byte The raw byte flagged coordinator.ts binary, so GitHub rendered the feature's core file as 'Binary file not shown' (review on anima-research#115). Runtime-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Both blockers addressed.
Nonblocking notes 1–4 noted; I'll fold the |
acdc692 to
588a30a
Compare
…e_out tool Review on anima-research#115 (slimepriestess, nonblocking 1 and 4): backlogCap 0 is a supported "summary only, no raw dump" mode and now says so; the 30 s cadence, 1 wake and 60 s duration floors are stated in the schema rather than applied silently. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Follow-up on the nonblocking notes, plus the cm-side carry-over:
|
|
Note on the red CI run at |
slimepriestess
left a comment
There was a problem hiding this comment.
APPROVE at head fb82bd9, contingent as before on cm#54 (approved there) landing first and the dependency pin flipping at integration.
Verified: coordinator.ts is text again (\0 escape at the composite key, dedicated commit), and git grep -P '\x00' across the branch is clean — the two remaining repo NULs are on main from the kv-unified landing, not yours; handling separately. Rebase onto current main is in (MERGEABLE), suite verified locally at 712 pass / 0 fail / 4 skipped on a clean build.
Dispositions on the nonblocking notes, all accepted:
- 1 & 4 fixed:
backlogCap: 0= summary-only is now stated, and the floors (30s/1/60s) are named in the schema instead of applied silently — an agent reading the tool now knows the contract it's actually under. - 2 (final-burst ack overstates): your reasoning is right — the mention-er's signal stays true and the backlog arrives seconds later; a "suppressed-then-delivered" intent would be wire vocabulary for a cosmetic distinction. Agreed leave-as-is.
- 3 (anchor after oldest-epoch cancel): agreed, and the honest comment ("oldest epoch at last (re)computation") is exactly what I wanted — the behavior was fine, the risk was someone reading the old comment as a stronger invariant.
- The cm-side carry-over is verified in code over there: the windowed strategy re-derives anchor AND previous-compile identity on observed branch change, pinned by the raw-
switchBranchtest — so the subconscious can't hold stale state across an undo/redo without any AF change. Clean division.
With both halves approved this pair is antra's to sequence: cm#54 → publish → pin flip → this. The arc held up through two review rounds and got structurally better both times — the branch-scoping work in particular turned a latent class of bug into a tested contract.
Two conflicts against the 7 commits main gained since 7324521 (anima-research#141 turn-provenance batched wakes, anima-research#142, anima-research#143, 0.12.0), both keep-both: - driveStream pre-stream: main's ephemeral-disposal ownership check runs first, then this branch's dispositions injection for the subconscious. - driveStream finally: main's `frameReachedTerminal` guard on the deferred flush, over this branch's per-agent `drainDeferredFor`. The auto-merged channel-incoming wake loop keeps anima-research#141's `counterparty` / `addressed` provenance under this branch's `targetAgents` routing. Bug fixed while reviewing: `getAllTools()` pushed TUNE_OUT_TOOL onto the array `getChannelTools()` returns, which is the registry's module constant CHANNEL_TOOL_DEFINITIONS — every call appended another `tune_out` to the shared list. The array is now copied before the push; tune-out-e2e asserts the resident's board carries `tune_out` exactly once across repeated calls. Verified against a build of context-manager#54 (main 92f5a65 + ef11162): tsc clean, suite 725 / 0 fail / 4 skipped, tune-out suites 22/22. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bm7wvGLvxF6RGAKCRB6MH7
|
Re-verified for landing (antra wants tune-out moving again). Pushed a merge of Conflicts (2, both keep-both): driveStream pre-stream (main's ephemeral-disposal ownership check, then your dispositions injection) and the finally-flush (main's Bug fixed: Receipts on main + this branch, built against context-manager#54 (main Everything else I read matches the approval: coordinator (coalesced wakes → durable count → auto-cancel → capped system-framed dump), the Sequencing from here: cm#54 → CM release → flip this pin to the published version → CI → merge. Host-side recipe plumbing for |
context-manager#54 (strategy-view composition: viewFilter, auxiliary merged views, WindowedPassthroughStrategy) is released as 0.8.0; the git-ref pin to the PR branch was the one intended change between approval and merge. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bm7wvGLvxF6RGAKCRB6MH7
Tune-out: subconscious summaries instead of unsubscribing (#77)
Implements the full design from #77 (design comment + antra's decisions: no staged intermediate; Disco-Elysium-style delivery; no words in models' mouths). Reviewed as one unit with context-manager#54 (draft, stacked): cm supplies
viewFilter, merged auxiliary views, andWindowedPassthroughStrategy; this PR supplies everything else.The arc (pinned end-to-end by a real-MCPL-fixture test)
tune_out(agent tool, per channel: cadence / backlog cap / max-wakes / optional duration) → the channel's third desired statetuned-out(durable in themcpl/channel-lifecycleappend-log, transport stays open) → incoming traffic is stamped (metadata.tuneOut = {epochId}), stored in the shared slot, and never wakes the residents nor enters their compiled view (cmviewFilterat the view choke point — no emission leak, no L1-compression leak) → addressed messages that pass the resident's wake gate get a deterministicchannels/acknowledge(intent: 'suppressed-tuned-out', grant-gated, thechannel_declineprecedent) and a coalesced subconscious wake; gate-privileged authors wake identically → wakes are counted durably in the lifecycle log (restart cannot reset the budget) with auto-cancel past max-wakes → cancel (agent, subconscious, auto, or duration expiry) delivers a system-framed capped dump (<tuned-out-backlog …>, the<backscroll>precedent) plus one resident wake and a subconscious report turn; stamped originals stay view-excluded permanently (the dump is the delivery — KV-prefix-stable append, no retro-insertion).The subconscious
A persistent registered resident on the conversation-fork template: isolated slot
subconscious/<primary>, merged view (own slot + residents' shared slot, unfiltered — the backlog is precisely what it exists to see) underWindowedPassthroughStrategy(coarse anchor = start of the oldest active tune-out). Excluded from every broadcast fan-out; never primary;proseRouting: 'explicit'so a timer-triggered turn can never leak bare prose to the default locus. Tool surface:deliver_summary/cancel_tuneout/note_disposition/speak_in_channel(the last off by default pending the voice-block canary), plus think/skip_reply/end_turn — no channel lifecycle, no gate rules, no workspace. Its AgentConfig spreads the principal's config as its base (fork-template idiom) — reasoning effort (thinking), provider params, token ceilings, caching, refusal handling all inherit, present and future fields alike — and its strategy mirrors the principal'smaxMessageTokens(antra: named side agents run under the same settings as the main agent).Voice doctrine, mechanically enforced: everything the resident reads from the subconscious is its verbatim text under its own participant name (
Subconscious— theContext Managerprecedent). Host-authored framing is system-styled and never voiced:[Tune-out: …]bracket notices (the[Gate: …]precedent) and the backlog wrapper. The mode block is recipe-side config (subconscious.systemPrompt), co-authored with the resident, so the Mica→Fable canary iterates without code changes. Register: second person toward the resident; report-shaped.Designer review (antra, #tuneout-talk, 2026-08-08 → 2026-08-18)
Every deviation below was reviewed with antra directly. Outcomes: registry-owned routing + chronicle-durable state endorsed; gate composition added at her direction — the resident's wake gate PRECONDITIONS subconscious wakes: the subconscious is affected by main's gate, not a replacement for it, so a gate-muted author's mention diverts silently with no reaction that would leak the mute; optional
durationSecondsapproved and added — a tune-out can carry a bounded deadline that auto-cancels through the standard flow and survives restarts (closes the forgotten-channel failure mode); the five minor deviations approved as a batch, with the dispositions carrier walked through separately. She also flagged the pre-existingcanBeTriggeredByfan-out inconsistency as possibly warranting its own issue — tracked separately; this PR only makes the channel-incoming path honortargetAgents.Plumbing that rides along (would have been separate PRs; folded per review preference)
targetAgentshonored on the channel-incoming fan-out — the declared-but-dead field goes live, mirroring the push path; untargeted events keep the historical broadcast byte-for-byte.addMessage(…, {forAgent})with the deferral/turn-alive guard evaluated against the target's turn state and flushed at the target's boundaries; gate self-wake notices deliver to the waking agent. Default path byte-identical (pinned by test).channel_openon a tuned-out channel refuses with a pointer at the cancel flow; machine-sourced closes treat tuned-out as stated agent intent; malformed tuned-out records drop at replay (old readers degrade safely).Deviations from the issue text (each deliberate, none silent)
A clause-by-clause drift review against #77's design write-up ran before this PR; two drifts it caught were fixed rather than shipped (gate-privileged authors wake like mentions; every non-subconscious-initiated cancel gives the subconscious a report turn). What remains deviates knowingly, each reviewed above:
triggerSources/canBeTriggeredByplumbing). The gate's decision is a bare boolean at the registry seam, its wake fan-outs bypasscanBeTriggeredBy, and itsmatchCountdies with the process — while the registry holds serverId+channelId+tags+desired-state and the lifecycle log gives max-wakes restart durability. The gate still governs everything as a precondition (see Designer review).durationSecondsdeadline covers the total-quiet return path.Subconscious(issue example:<agent>-subconscious) — per the Disco-Elysium decision and theContext Managertitle-case-functional-voice precedent. Recipe-configurable.note_dispositionare exactly as specced.tune-out-compression-framing.test.ts). The builders strip exactly four classes and preserve participants, so the backlog wrapper, notices, andSubconsciousattribution reach the summarizer intact. First-person output folding is deliberately unconstrained (antra: "fine and expected").period/summary cadence: read as apposition, then resolved with antra directly — optionaldurationSecondsadded ("optional is fine"). Expiry runs the standard cancel flow, reason "duration elapsed"; deadlines survive restarts.Dependency note (temporary)
@animalabs/context-manageris pinned to the cm PR branch (github:Meganeuridae/context-manager#feat/strategy-view-composition). At integration: merge cm#54 → publish → flip this to the published version. That flip is the one intended change between approval and merge.Tests
tune-out-lifecycle.test.ts— durable state: round-trip, restart-surviving wake counts, epoch supersession, malformed-record replay.tune-out-gate-composition.test.ts— gate preconditions wakes and reactions (4 cases) + duration expiry (immediate-at-resume, on-schedule, param stamping).channel-incoming-targeting.test.ts/per-agent-delivery.test.ts— plumbing neutrality + targeting, asserted at the fan-out seam.tune-out-e2e.test.ts— the full arc against a real stdio MCPL fixture (0.5 handshake, initial-policy Request, itemized register): divert/stamp/exclude + merged-view visibility + ack + coalesced wake with durable count + max-wakes auto-cancel + capped dump + the subconscious's cancel-report notice + permanent exclusion after cancel.Not in this PR
Fleet deployment (gated on the voice-block canary via Mica, per #77), conhost recipe surface for
subconscious/tune_outconfig, the possiblecanBeTriggeredBycleanup issue, and mid-run per-channel cadence adjustment (cancel + re-enter covers v1).🤖 Generated with Claude Code