Skip to content

feat: tune-out — subconscious summaries instead of unsubscribing (#77) - #115

Open
Meganeuridae wants to merge 8 commits into
anima-research:mainfrom
Meganeuridae:feat/tune-out
Open

feat: tune-out — subconscious summaries instead of unsubscribing (#77)#115
Meganeuridae wants to merge 8 commits into
anima-research:mainfrom
Meganeuridae:feat/tune-out

Conversation

@Meganeuridae

Copy link
Copy Markdown

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, and WindowedPassthroughStrategy; 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 state tuned-out (durable in the mcpl/channel-lifecycle append-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 (cm viewFilter at the view choke point — no emission leak, no L1-compression leak) → addressed messages that pass the resident's wake gate get a deterministic channels/acknowledge (intent: 'suppressed-tuned-out', grant-gated, the channel_decline precedent) 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) under WindowedPassthroughStrategy (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's maxMessageTokens (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 — the Context Manager precedent). 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 durationSeconds approved 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-existing canBeTriggeredBy fan-out inconsistency as possibly warranting its own issue — tracked separately; this PR only makes the channel-incoming path honor targetAgents.

Plumbing that rides along (would have been separate PRs; folded per review preference)

  • targetAgents honored 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.
  • Per-agent message deliveryaddMessage(…, {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).
  • Registry guard railschannel_open on 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:

  • Wake routing lives in the ChannelRegistry/coordinator, not gate policy (issue sketched triggerSources/canBeTriggeredBy plumbing). The gate's decision is a bare boolean at the registry seam, its wake fan-outs bypass canBeTriggeredBy, and its matchCount dies 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).
  • Empty cadence periods skip the subconscious turn entirely — "stays silent if nothing merits it" preserved at zero cost; the issue's own cost motivation cuts this way. The durationSeconds deadline covers the total-quiet return path.
  • Participant name is Subconscious (issue example: <agent>-subconscious) — per the Disco-Elysium decision and the Context Manager title-case-functional-voice precedent. Recipe-configurable.
  • Dispositions are a fixed system-position injection on the subconscious's compiles — resolved with antra directly: they need not repeat per invocation, but must never wash away (it has no long-term memory). Out of the timeline entirely, structurally unable to scroll out, bytes change only when a disposition changes. The durable slot and note_disposition are exactly as specced.
  • Compression-payload framing preservation needed no new code — and is pinned by contract (cm#54: tune-out-compression-framing.test.ts). The builders strip exactly four classes and preserve participants, so the backlog wrapper, notices, and Subconscious attribution 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 — optional durationSeconds added ("optional is fine"). Expiry runs the standard cancel flow, reason "duration elapsed"; deadlines survive restarts.

Dependency note (temporary)

@animalabs/context-manager is 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.
  • Both branches rebased onto current mains and fully green (af: suite 600+/0, batteries 20/20 atop v0.10.0; cm: 479/0).

Not in this PR

Fleet deployment (gated on the voice-block canary via Mica, per #77), conhost recipe surface for subconscious/tune_out config, the possible canBeTriggeredBy cleanup issue, and mid-run per-channel cadence adjustment (cancel + re-enter covers v1).

🤖 Generated with Claude Code

Aster and others added 7 commits August 17, 2026 21:25
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant