Skip to content

Tune-out: all-except scope (supersedes focus #195), hardening, and a depth review #232

Closed

Description

@antra-tess

Summary

This issue covers three things: what went wrong with focus as implemented, the proposed scope of changes for tune-out, and what the review of tune-out needs to cover.


1. Problems with focus as implemented (#195)

Review history

Round Head Findings
1 2036882 2 blockers + 5 notes from human review; 14 from automated review
2 ecf5b36 8 new: 5 in scope, 2 in code that arrived by merging main, 1 stale
3 6459fdf 5 new, each a consequence of a round-2 fix

Round 3 is the telling one: a server-qualified routing fix whose fallback misroutes when the channel disappears; a durable reply record that is not synced; a wake purge that leaves an empty provider cooldown armed; an "unread" rule that conflicts with a subconscious that has already read the message. Patching was generating findings as fast as it closed them.

Root causes

A. There is no single point where an inbound message is admitted. Whether the resident should get a message is not decided once; it is re-asked at each stage, with different data. Measured on the #195 branch:

  • The hold decision runs mid-pipeline, in handleMcplChannelIncoming / handleMcplPushEvent.
  • Before it, ChannelRegistry.handleIncoming has already retargeted the default speech locus, evaluated the wake gate (which may queue a debounced wake) and run the coalescer.
  • After it, six structures can still start a turn (pendingRequests with 25 push sites, gate debounce timers, the gate inference buffer, self-wake timers, provider-cooldown held requests, the deferred-write queue), and roughly fifteen sites put content in front of the model (compiled view, mid-turn injection, turn-start flush, gate notices, invitations, backscroll).
  • One message takes eight different shapes along the way (McplChannelIncomingEvent, McplPushEvent, GateEventInfo, PendingEvent, InferenceRequest, the deferred-write entry, CoalescedOccurrence, StoredMessage), and the two inbound paths duplicate their logic.

Focus therefore needed 22 hooks in framework.ts across about ten functions, 27 in the channel registry and 8 in the gate. Its invariant is universal (nothing from elsewhere), so every stage without a hook was a bug. Most "leak" findings are one unhooked stage each.

B. Channel identity is not a value. Identity is (serverId, channelId), but it travels as a bare id and is re-derived per stage: 19 id-only lookups in channel-registry.ts, all routing state held as bare strings, a push event's channel reconstructed from origin in four places. This produced the wrong-server autoreply, the wrong-server speech routing and the purge that could not derive a queued push event's channel.

C. A third hold mechanism, chosen by analogy. Focus copied tune-out's storage model: ingest the message, stamp it hidden from the resident permanently, later re-render it as a text dump. Tune-out pays that cost because the subconscious reads the stored messages. Focus had no second reader and inherited the cost without the reason. The dump became a second artifact kept consistent with the originals by hand, which accounts for the findings about backlog lost on crash, deferred messages missing from the dump, messages stranded when the feature is disabled, media dropped and stale text after edits.

D. Two attention mechanisms with no composition rule. Several findings were purely focus × tune-out: ordering at ingestion, a focused channel that is also tuned out, the gate verdict tune-out's classification depends on.

Conclusion

#195 should not merge. What it fixed in shared code is listed under hardening in section 3, because the same defects exist in tune-out.


2. Tune-out on main: observed gaps

Checked at e457abd (0.21.0), in a clean worktree with a real install. Items 1–5 were reproduced with probe tests, run with a control showing the ordinary divert working (a channels/incoming message on a tuned-out channel is stamped, hidden from the compiled view and wakes no resident). Items 6–11 are by inspection only. The probe sources are available and should land as regression tests with the fixes.

Reproduced

# Gap Observed
1 Tuning out a DM does nothing. handleMcplPushEvent (framework.ts:7926) never consults the coordinator; DMs and addressed-while-closed messages arrive on that path. tune_out accepted for the DM channel; the next DM is stored without a tuneOut stamp, wakes the resident (mcpl:push-event) and is in the compiled view.
2 Tuned-out traffic moves the default speech locus. markAccepted (channel-registry.ts:937) runs before the divert and is unconditional. Default locus work → after one message on the tuned-out channel, getDefaultPublishChannel() and resolveLocus() both return the tuned-out channel. A turn with no channel of its own (heartbeat, batched gate wake, self-wake) then speaks into the channel the resident tuned out.
3 A debounce gate policy wakes the resident for tuned-out traffic. The gate is evaluated in the registry (channel-registry.ts:956) before the divert and queues the event (event-gate.ts:1435); the framework then sees triggerInference=false. A mention in a tuned-out, debounced channel: queued in the gate; no subconscious wake and no suppressed-mention reaction at arrival (coordinator.ts:305, :313 are gated on the verdict). When the window elapses the resident is woken (gate:debounce) with [Gate: 1 event matched] - 1 message in <tuned-out channel> … already shown above at arrival, which is untrue: the message was diverted.
4 The cancel dump drops media. The renderer keeps text blocks only (coordinator.ts:218). An image-only message renders as an empty line (<author>: ); a captioned image loses the image with no indication. The originals stay hidden.
5 A diverted message that arrives mid-turn is injected into the live turn. addMessage defers it while the turn is alive; the tool-boundary drain injects deferred messages with no held check (framework.ts:6584). The stamped message appears in the live stream's injectedMessages, and is absent from the next compiled view. The resident reads what tune-out was holding, and wire and window disagree.

By inspection

# Gap Where
6 Backlog can be lost on a crash. cancel() flips durable state (coordinator.ts:189) before writing the dump (:232). A cancel issued by tool call runs mid-turn, so the dump is deferred, and the deferred queue is persisted only during or after quiesce (framework.ts:3396, :12277). A stop in that window leaves the epoch ended, the dump gone and the originals hidden. tune-out/coordinator.ts, framework.ts
7 Deferred diverted messages miss the dump. The backlog is read from stored messages only (coordinator.ts:208); one still in the deferred queue at cancel is omitted and stays hidden when it lands. tune-out/coordinator.ts
8 Channel resolution is id-only. The tool picks the first registered entry with a matching id (framework.ts:14825) and takes no serverId; cancel_tuneout does the same (coordinator.ts:532). The state itself is keyed correctly. framework.ts, tune-out/coordinator.ts
9 channel_close on a tuned-out channel discards the epoch. handleToolClose (channel-registry.ts:2604) writes closed directly, bypassing the cancel flow: no dump, held messages stay hidden. (channel_open is correctly refused with a pointer.) mcpl/channel-registry.ts
10 Removing the subconscious strands held messages. Without it the coordinator is not built (framework.ts:1778), so nothing resumes or ends a persisted epoch, while the view filter (:6334) keeps hiding its messages. framework.ts
11 The dump points at a tool the resident may not have. The truncation note names fetch_history unconditionally (coordinator.ts:226). tune-out/coordinator.ts

Already handled and worth keeping: event coalescing treats a diverted message as read only by the subconscious (isUnreadStoredMessage, framework.ts:7860).

As far as I know tune-out has not yet been exercised in a running deployment, which would explain why none of this has surfaced in use. The #77 review also predates event coalescing and the later routing changes.


3. Proposed scope of changes for tune-out

3.1 Hardening (independent of the new scope; small, separately reviewable)

Change Closes
H1 One predicate for "held from the resident", used by the compiled-view filter and the mid-turn injection 5
H2 The push-event path consults the coordinator 1
H3 Gate: a held event is dropped before policy evaluation, with a delivery-time re-check, while the verdict tune-out's own classification needs is preserved; queued events keep the metadata needed to re-derive their channel 3
H4 Diverted inbound does not retarget the default speech locus, and cannot re-pin a turn's locus mid-turn 2
H5 Dump: name non-text blocks, include messages still in the deferred queue, name only history tools the resident has 4, 7, 11
H6 Delivery durability: a deferred dump makes the deferred queue durable; a restored dump re-wakes the resident at boot 6
H7 Server-qualified channel resolution in the tool and in cancel_tuneout; optional serverId, ambiguity is an error 8
H8 channel_close on a tuned-out channel goes through the cancel flow 9
H9 A persisted epoch the configuration can no longer serve is ended at boot with its dump 10

The #195 branch has working equivalents of H1–H7 and H9, written for focus; the mechanisms carry over and need adapting to tune-out's state. H8 is new.

3.2 An all-except scope

  • tune_out gains a scope that holds every channel except one, for a mandatory bounded duration.
  • It is a resident-level record, not per-channel state, so it covers DMs and channels registered after entry.
  • Backlog on cancel is per held channel with a per-channel cap; a check mode reports held counts and peeks one channel without releasing.
  • While active, unprefixed speech lands in the excepted channel.
  • Per-channel tune-outs and the all-except scope need a stated precedence.

3.3 The subconscious becomes optional

Today the coordinator is not built without one and enter refuses (coordinator.ts:140). Without a subconscious, tune-out is a silent hold: no cadence, no wake budget, backlog on cancel.

3.4 Mention response

Today a suppressed mention gets a reaction. Add a text autoreply option: once per (channel, author) per epoch, recorded durably only when the reply actually publishes, and sent only into channels the resident is open in and into DMs.

3.5 Not in this scope

Root causes A and B above are framework-level: a single admission stage that decides a message's disposition once, before any side effect, and a channel reference type that carries (serverId, channelId). Folding focus into tune-out halves the hooks and removes the cross-feature matrix, but the surfaces still have to be hooked once. Both deserve their own issues.

Open design decisions

  1. Under the all-except scope with a subconscious present, may it cancel the hold? It can today for per-channel tune-out; feat(focus): single-channel focus mode with held backlog, autoreply, and expiry #195 was built as "nothing breaks through".
  2. Does focus survive as a resident-facing alias over tune_out, or go entirely?
  3. Is a wake already queued for a channel withdrawn when that channel becomes held?
  4. The hold is host-wide, because residents share one message slot. Is that acceptable, or should an event targeted at another agent be exempt? (An exemption was tried in feat(focus): single-channel focus mode with held backlog, autoreply, and expiry #195 and leaked: the event was stored unstamped in the window the primary reads.)

Alternative considered: hold at the transport, by closing the other channels and reopening them with backscroll. It needs no stored-and-hidden messages at all, but it depends on server-side backscroll, which only some connectors implement, and it is incompatible with a subconscious reading the held traffic.


4. Review needed

Tune-out was reviewed for #77 against its design. It has not been walked against the pipeline the way #195 was, and items 1–11 are what a first pass at that depth found. Before the all-except scope lands, tune-out needs a human review covering each of these for any state that diverts attention:

  • Both inbound paths, channels/incoming and push/event, including their coalesced variants.
  • Side effects that happen before the divert decision: default speech locus, gate evaluation and debounce queueing, the coalescer, the channel's open flag and typing state.
  • Every structure that can start a turn later: pendingRequests, gate debounce timers and inference buffer, self-wake timers, provider-cooldown held requests, wakes parked by quiesce.
  • Every surface that shows content to the model: compiled view, mid-turn injection, turn-start flush of deferred writes, gate notices, closed-channel invitations, backscroll, tool results.
  • Routing state: default locus, turn pin, mid-turn re-pin, how routeSpeech resolves its target.
  • Identity: every lookup that should be server-qualified.
  • Durability and crash windows: state flip versus delivery, the deferred queue, timers, what a restart restores and what it must re-wake.
  • Configuration transitions: the feature removed while an epoch is live; conversation routing enabled.
  • Composition with sleep, quiesce, event coalescing and conversation routing.
  • Asynchronous side effects scoped to the epoch that started them.

The reviewers of #195 have this context already.


Disposition

🤖 Generated with Claude Code

Activity

  1. antra-tess commented on Oct 5, 2026

    @antra-tess
    CollaboratorAuthor

    Proposal: a derived-agent primitive (a third framework-level item next to 3.5)

    This comes from a separate question (letting a resident spawn short-lived forks of itself that share its context, work for a few turns in parallel and return a result) that turned out to overlap with this issue. It does not change the hardening list or the suggested order above. It proposes one more framework-level item alongside the admission stage and the channel reference type in 3.5.

    Observation

    The framework and host already have three mechanisms that derive a second agent from the resident. Each has its own creation path, its own way of inheriting context and its own lifecycle code. Checked at e457abd (agent-framework), fc86875 (connectome-host), 537d921 (context-manager):

    Mechanism Created by Inherits from the resident Lifecycle
    Subconscious (#77) createSubconsciousAgent (framework.ts:6367) Inference settings wholesale; reads the resident's message slot through a read-only merged view (auxiliaryMessageViews, context-manager.ts:128); own system prompt; nothing from before its window anchor Tune-out epoch
    Conversation fork createConversationAgent (framework.ts:7208) Agent config; a copy of the compiled context re-added as plain messages Channel binding with idle TTL
    Subagent fork (host) createEphemeralAgent (framework.ts:4007) + SubagentModule.runFork A copy of the compiled context; not the config: model defaults to a fixed small model, the strategy is a hard-coded KnowledgeStrategy with a 190k prompt limit, and thinking, cache TTL and provider params are not passed Until return, or a timeout

    Two consequences are visible in the code:

    • "Which agent is the resident" is computed by exclusion, repeated per site: framework.ts:2095, :6928, :7730, :8000, :8783 each filter out conversation forks, the subconscious or ephemeral runs by hand. A fourth kind of derived agent means editing every one of them.
    • The two copy-based forks cannot fold what they inherited. The parent's memories arrive as ordinary messages, so the fork's strategy has no tree to work with, and the copy is written to the store once per fork. The subagent fork also does not share the parent's prompt-cache prefix, because model and inference settings differ.

    The subconscious is the one that uses the better inheritance mechanism (a live view rather than a copy), with the past switched off.

    Proposal

    One derived agent primitive in the framework: a declarative spec, a single registry and lifecycle manager, and the existing mechanisms re-expressed as presets. Policy (when to derive, cadences, wake budgets, framing text) stays in thin clients such as the tune-out coordinator, the conversation router and the host's subagent module.

    Names are placeholders:

    interface DerivedAgentSpec {
      name: string;
      parent: string;
    
      context: {
        // what it inherits from before the derivation point
        past: 'none' | { at: Sequence; budgetTokens?: number; recentWindowTokens?: number };
        // whether it keeps reading the parent's stream afterwards
        follow: boolean;
      };
    
      identity: {
        systemPrompt: 'inherit' | string;
        settings: 'inherit' | Partial<AgentConfig>;   // model, thinking, cache TTL, provider params
      };
    
      inbound: 'none' | { channels: ChannelRef[] };   // consumed by the admission stage
    
      outbound: {
        tools: 'inherit' | string[];
        denyAtDispatch?: string[];                    // restrict without changing the request prefix
        prose: 'disabled' | 'explicit' | 'inherit';
      };
    
      lifecycle: {
        ends: 'on-return' | 'on-cancel' | { idleTtlMs: number };
        maxTurns?: number;
        durable: boolean;                             // restored after restart, or ended with a notice
      };
    
      result: 'tool-result' | 'message-to-parent' | 'none';
    }

    Existing mechanisms as presets:

    past follow identity inbound ends
    Subconscious none yes own prompt held channels on cancel
    Conversation fork fold state at bind no inherit one channel idle TTL
    Task fork fold state at the fork point no inherit none on return
    Spawn (fresh subagent) none no own prompt none on return

    past inherits the parent's fold state (messages, memories at every level, pins) as of a sequence, not its rendered text. With unchanged settings that reproduces the parent's rendering, so the prefix is shared. With a lower budget or a shorter recent window the derived agent's own solver picks a coarser rendering, and its own work is folded by the same machinery. Nothing in it is a protected zone.

    Guarantees the primitive should own

    • No writes to the parent. A derived agent writes only to its own namespace. The declared result path is the only way back.
    • Its words are its own. Whatever returns is the derived agent's verbatim text under its own name, as the tune-out voice doctrine already requires.
    • One primary, identified positively, replacing the exclusion filters listed above.
    • Prefix sharing is tested. With inherited identity and unchanged settings, the compiled request prefix must be byte-identical to the parent's. That can be a unit test on request hashes rather than a property each feature hopes for.
    • Every derived agent ends and is bounded. A missing end condition or cost cap is a load-time error.
    • Restart behaviour is declared. Durable agents are restored; the rest are ended with a notice to the parent. This generalises H9.

    Layering

    • context-manager: two additions to the merged-view mechanism. First, sequence bounds on a view, so it can be a snapshot as well as a live view. Second, the same read-through for fold state, with writes going to the derived agent's namespace. Chronicle already reads a state as of a sequence (getStateAt).
    • agent-framework: the spec, the registry and the lifecycle, replacing the three creation paths.
    • connectome-host: tools and recipe surface only.

    How it relates to this issue

    • Admission stage (3.5, root cause A). inbound gives the stage a third disposition next to "deliver to the resident" and "hold": deliver to a derived agent. Conversation routing needs that today, and the tune-out divert is the same decision. If the stage is designed with this disposition from the start, a new kind of derived agent does not need its own set of hooks.
    • Channel reference type (3.5, root cause B). inbound is declared in ChannelRef values, so it depends on that item.
    • Hardening (3.1). Unchanged. Stored-and-hidden in the shared slot is what lets the subconscious read by merged view, so H1, H5, H6 and H9 remain the right fixes for the current model.
    • Open decision 4 (the hold is host-wide because residents share one message slot). A positive notion of "primary" plus per-agent inbound is where an exemption for another agent's events could be expressed without the leak described there.
    • Later, not now: giving the subconscious the resident's past becomes a parameter change (past from none to a small-budget snapshot) rather than a replacement.

    Suggested order

    1. Framework registry and spec, with today's three mechanisms as presets and no behaviour change.
    2. Sequence-bounded views in context-manager.
    3. Fold-state inheritance. This is what enables the task fork and moves conversation forks off the copy.
    4. An optional past for the subconscious.

    Step 1 is independent of the tune-out hardening and of steps 2–3, though it touches the same parts of framework.ts and should be sequenced with the hardening PRs rather than run against them.

    Not verified

    • Fold-state read-through is a design, not an inspection result. I have not read the strategy's persistence layer closely enough to say how many in-place edits it has to intercept. The adaptive solver is also path-dependent, so a derived agent has to inherit the parent's current frontier rather than re-solve from scratch.
    • The merged view is only used with WindowedPassthroughStrategy today. Whether the autobiographical strategy runs correctly over one is untested.
    • Prefix byte-identity between a parent and a derived agent has not been measured.

    Out of scope

    Merging a derived agent's memories back into the parent's tree. Because its state is an overlay at a known sequence, that stays well-defined if it is wanted later.

  2. antra-tess commented on Oct 5, 2026

    @antra-tess
    CollaboratorAuthor

    Review of the derived-agent proposal, checked against the cited revisions (e457abd in agent-framework, fc86875 in connectome-host, and 537d921 in context-manager).

    The common registry and lifecycle abstraction is a useful direction. I would correct several descriptions of current behavior and tighten the guarantees before using this as an implementation spec.

    1. Separate agent lifetime from tune-out epochs. The subconscious persists across epochs. Cancelling a tune-out stops that channel's cadence and schedules a final report; it does not dispose of the subconscious. A local probe confirmed that the same agent object and its stored history survive cancellation and re-entry. The preset should model a persistent agent with bounded activations, rather than ending the agent when a channel's tune-out is cancelled. This also matters when several channels have active epochs. See subconscious creation and cancellation.

    2. Model explicit roles or capabilities, rather than replacing the filters with “primary only.” The framework already identifies its primary positively through primaryAgentName. The listed exclusion filters serve different purposes, including broadcast eligibility and provider scheduling. A local probe with two configured residents confirmed that ordinary push broadcasts reach both residents while excluding the subconscious. A shared registry can centralize these policies, but must preserve the distinction between the primary, other residents, and derived agents. See primary assignment and push broadcast selection.

    3. Narrow the claim about folding copied inheritance. Task forks use KnowledgeStrategy, which extends AutobiographicalStrategy, so copied text can be compressed. What they lose is the parent's existing memory tree and its alternative resolutions: the child has to process the rendered copy anew. Conversation forks default to passthrough but allow a strategy factory. Also, the host's 190k prompt limit is a configurable default, not a fixed limit. See KnowledgeStrategy, conversation strategy selection, and task-fork construction.

    4. Give prefix sharing an explicit rendering contract. In a small probe using the current native formatter, the same inherited messages rendered under a new assistant name changed the parent's assistant turns into user content. Remapping the inherited participant restored identical native messages in that example. The acceptance test should compare the provider-formatted shared prefix through a defined fork boundary, accounting for participant mapping, tools, system injections, cache markers, and inference settings. Comparing inherited settings or a whole normalized-request hash is insufficient. Existing task forks also deliberately rewrite the fork-call/result portion and remove sibling fork calls; that divergence needs a defined boundary. See request assembly and structural fork materialization.

    5. Treat fold-state inheritance as a separate context-manager design task. Strategies receive the Chronicle store directly and persist summaries, chunks, pins, resolutions, merge queues, and presentation receipts. Extending the message view alone will not isolate those writes. The design needs an immutable snapshot identified by branch and sequence, explicit rules for child-owned strategy state, and handling for background work and branch changes. The proposal correctly labels this unverified; parent isolation and prefix continuity should remain acceptance criteria until demonstrated. See StrategyContext construction and strategy state registration.

    Two schema details also need resolving: a static ChannelRef[] does not itself express “currently held channels,” so the spec needs a dynamic selector or a membership-update contract; and on-cancel with optional maxTurns permits indefinite execution, so bounded jobs need an enforceable deadline or budget distinct from persistent-agent lifetime.

    I support starting with the common registry and lifecycle layer while preserving existing behavior and sequencing it with the tune-out hardening work. Fold-state inheritance should then be a separate prototype with tests proving that child activity cannot alter the parent's state and that the intended shared request prefix is identical. The local probes above validate current lifetime, broadcast, and formatter behavior; they do not validate the proposed inheritance mechanism.

  3. antra-tess commented on Oct 5, 2026

    @antra-tess
    CollaboratorAuthor

    Decisions on the open design questions

    From @antra-tess, 2026-10-05.

    1. The subconscious keeps full latitude under every scope, including the all-except scope: it may cancel the hold, report, and speak where channel speech is allowed. There is no strict mode when a subconscious is present. Without one, the hold is silent and nothing breaks through.

    One consequence to confirm: the wake budget is a host rule, not a judgment of the subconscious. Applied across every held channel at once, the default of five would end an all-except hold after a handful of mentions. Proposed: for that scope, exhausting the budget does not cancel; ending the hold is left to the subconscious, the resident or the deadline.

    2. focus stays as a resident-facing alias over the same coordinator. One mechanism, two tool names, each with its own short description and parameters.

    3. A wake already queued when a channel becomes held runs. Consequences:

    • No purge at entry, in the framework's queues or the gate's. The hold applies to events that arrive after entry.
    • H3 shrinks to: a held event is dropped before policy evaluation. The entry purge, the queued-event metadata it needed and the delivery-time re-check are not required.
    • This removes the mechanism behind three feat(focus): single-channel focus mode with held backlog, autoreply, and expiry #195 findings (purge cannot derive a push event's channel; purge drops the focus channel's own wake; purge leaves an empty provider cooldown armed).
    • To settle in implementation: where such a turn's unprefixed speech goes. It is answering a message that predates the hold, so its own trigger channel is the natural locus.

    4. Direction: retire multi-residency in favour of derived agents (the proposal above). This replaces open decision 4 rather than answering it.

    What multi-residency is today, checked at e457abd:

    • Residents created by createAgent open the context manager with a namespace but without isolate (framework.ts:6318), so they share one messages slot and keep only strategy state apart. They read the same conversation and each other's turns.
    • Every untargeted inbound message wakes all of them (framework.ts:7076, :7999); the first one created is the primary and owns default delivery.
    • connectome-host passes exactly one agent to the framework, and a recipe has a single agent. Multi-residency is exercised by framework tests and by other embedders, not by the host.

    In the derived-agent vocabulary:

    past follow ends
    Independent co-resident none no never
    Today's co-resident (shared room) none yes, mutually never

    So an agent with no inherited past, no view of the main thread and no expiry is a co-resident in the useful sense, and is cleaner than the shared-slot kind. It is not identical to what exists: today's co-residents see the same thread. If two agents sharing one room is still wanted, that is a follow preset; otherwise it goes away.

    For the equivalence to hold, the primitive needs:

    • A persistent class: declared in configuration and restored at boot, exempt from "every derived agent ends". The subconscious is already one.
    • Its own inbound and wake policy: the admission stage routes channels to it, and sleep and wake rules are per agent. The gate is host-wide today.
    • No dependence on a parent: parent and result do not apply to it.
    • Its own operational surface: budget, settings, tools, workspace.

    Effect on this issue: with one resident per message slot, a hold belongs to that resident by construction and the host-wide caveat disappears. Until then the hold is documented as host-wide. This is not a prerequisite for the hardening in 3.1.

  4. antra-tess commented on Oct 5, 2026

    @antra-tess
    CollaboratorAuthor

    Revised agent spec (rev 2)

    This revises the derived-agent proposal after the review and decision 4 (retire multi-residency in favour of this primitive). It replaces the spec, the preset table, the guarantees and the order in the first comment.

    Corrections to the first comment

    All five review points are accepted. Four were errors of fact or wording on my side:

    • Subconscious lifetime. It persists across epochs; cancel ends a channel's cadence, not the agent. "Ends on cancel" was wrong.
    • Primary. primaryAgentName already identifies it positively (framework.ts:6350). The filters I cited decide other things (who receives an untargeted broadcast, who owns provider scheduling). "Computed by exclusion" was overstated.
    • Folding a copy. A copy-based fork can compress what it inherited (KnowledgeStrategy extends the autobiographical strategy). What it loses is the parent's memory tree and its alternative resolutions.
    • Prompt limit. The host's 190k is a configurable default, not a fixed limit.

    What changes in the design

    1. It is an agent spec, not a derived-agent spec. With decision 4 the registry holds every agent, including the primary and agents with no parent. Derivation becomes one optional axis.
    2. Lifetime is separate from activation. An agent may be persistent; every activation is bounded.
    3. Context sources are explicit. follow: boolean becomes a list of slots the agent reads, which covers both the subconscious (one-way) and a shared room (mutual).
    4. Rendering is a contract. Which stored participants render as the agent's own turns is declared, and prefix sharing is tested on provider-formatted output up to a defined boundary.
    5. Inbound is a routing table owned by the registry, updated by the owning client, rather than a static list in the spec.
    6. Fold-state inheritance leaves this proposal and becomes a separate context-manager design task, as the review asks.

    Spec

    Names are placeholders:

    interface AgentSpec {
      name: string;
    
      // Positive capabilities, replacing hand-written filters
      role: {
        primary: boolean;                 // owns default delivery; exactly one
        receivesUntargeted: boolean;      // woken by untargeted inbound
        ownsProviderScheduling: boolean;  // cooldowns and held requests belong to it
      };
    
      lifetime:
        | { kind: 'persistent' }                          // declared in configuration, restored at boot
        | { kind: 'idle'; idleTtlMs: number }
        | { kind: 'task'; deadlineMs: number; maxTurns?: number };   // must end; deadline is required
    
      // Applies to every activation of every agent, persistent ones included
      activation: { maxTurns?: number; maxMs?: number; maxInputTokens?: number };
    
      context: {
        // One-time inheritance at creation. Absent for agents with no parent.
        inherit?: { from: string; at: { branch: string; sequence: number } };
        // Other agents' slots merged read-only into this agent's view, live.
        read?: Array<{ slot: string; fromSequence?: number; includeHeld?: boolean }>;
        // Stored participants that render as this agent's own turns.
        selfParticipants?: string[];
        // Strategy settings for this agent's own window.
        budgetTokens?: number;
        recentWindowTokens?: number;
      };
    
      identity: {
        systemPrompt: 'inherit' | string;
        settings: 'inherit' | Partial<AgentConfig>;
      };
    
      outbound: {
        tools: 'inherit' | string[];
        denyAtDispatch?: string[];
        prose: 'disabled' | 'explicit' | 'inherit';
      };
    
      // Absent for agents with no parent.
      result?: { to: string; as: 'tool-result' | 'message' };
    }

    Not in the spec, on purpose:

    • Inbound channel membership. The registry holds one durable routing table (ChannelRef → agent). Clients change it through one call, for example the tune-out coordinator when a channel becomes held or is released. The admission stage reads only that table. A selector such as "currently held channels" would make the registry reach into tune-out state; an update contract keeps the policy in the client.
    • Wake policy. The gate is host-wide today. Per-agent sleep and wake rules are needed for decision 4, but they belong with the admission-stage work and are not required for the first step below.

    Presets

    Lifetime inherit read Parent's turns render as own Inbound Result
    Primary resident persistent none none n/a untargeted, default delivery none
    Co-resident, independent persistent none none n/a its own channels none
    Co-resident, shared room persistent none each other's slot no untargeted none
    Subconscious persistent none resident's slot, held messages included no held channels, set by the coordinator message to the resident
    Conversation fork idle TTL at bind none yes one channel none
    Task fork task at the fork call none yes none tool result
    Spawn task none none n/a none tool result

    The shared-room row is not identical to today's co-residents. Today they write to one slot (framework.ts:6318 opens without isolate). Under one slot per resident they would read each other's slots, merged by sequence. The reading experience is the same; the storage is not, and existing stores would need a migration.

    Guarantees (revised)

    • No writes outside the agent's own namespace. result is the only path to another agent.
    • Its words are its own, under its own name.
    • Roles are declared. Broadcast eligibility, default delivery and provider scheduling are read from the spec, so a new kind of agent does not require editing each selection site.
    • Every activation is bounded, and every task agent ends. A task lifetime without a deadline is a load-time error. Persistent agents are exempt from ending, not from activation bounds.
    • Restart behaviour follows from lifetime. Persistent agents are restored; idle and task agents are either restored or ended with a notice to the recipient of their result.
    • The rendering contract below holds for any agent with inherit and inherited identity.

    Rendering contract

    The review's probe showed that inherited messages rendered under a new assistant name turn the parent's turns into user content unless the participant is remapped. Today both copy-based forks remap at copy time (framework.ts:7244, subagent-module.ts:1925). A view-based inheritance has no copy step, hence selfParticipants: the mapping has to live in the view or in request assembly.

    Acceptance test, for an agent with inherit and inherited identity:

    • What is compared: the provider-formatted request, not a normalized-request hash. That covers system prompt and injections, tool definitions, participant-to-role mapping, cache marker positions and inference settings.
    • Up to where: the fork boundary, defined as the end of the last parent message before the turn that caused the derivation. For a task fork that is the message before the assistant turn carrying the fork call. Everything after it may differ by design (the rewritten fork call and result, removed sibling calls).
    • Pass condition: byte-identical up to the boundary, with at least one cache marker at or before it in both requests.

    This test can be written against today's copy-based forks before any context-manager change. It would fail for the host's task fork as it stands (model, thinking, cache TTL and provider params are not inherited), which makes it a useful first target.

    Fold-state inheritance: separate track

    inherit is implemented by today's copy until this exists. It should be its own context-manager design and prototype, with these acceptance criteria taken from the review:

    • The snapshot is identified by branch and sequence and is immutable.
    • Child activity, background work included, cannot alter any parent state. Test by comparing the parent's states before and after a child run.
    • Child-owned strategy state has explicit rules (summaries, chunks, pins, resolutions, merge queues, receipts).
    • A branch change in the parent has defined behaviour for children pinned to the old branch.
    • The rendering contract above passes.

    Per-agent budgetTokens and recentWindowTokens are in the spec now, but only take full effect on inherited content once this track lands.

    Order (revised)

    1. Registry and spec with today's behaviour as presets, no behaviour change: roles, the lifetime and activation split, restart rules. Sequenced with the hardening PRs in 3.1, not run against them.
    2. Rendering contract and its test, then fix the task fork to inherit the parent's configuration.
    3. Inbound routing table, together with the admission stage and ChannelRef from 3.5.
    4. Per-agent wake policy, then retiring multi-residency (decision 4), including the store migration for shared-slot residents.
    5. Fold-state inheritance as the separate context-manager track. It can start any time after step 2.

    Open

    • Whether request assembly can treat several stored participants as the agent's own, or the view must rename them. Not checked.
    • Whether a shared room is still wanted once co-residents are independent. If not, the mutual read preset and its migration go away.
    • Whether role.ownsProviderScheduling is one capability or several. The review notes the existing filters serve more than one purpose; I have not enumerated them.
  5. nissa-seru commented on Oct 6, 2026

    @nissa-seru
    Contributor

    Proposal: resident channel exposure: one admission stage, a local inbox, and overlays

    By Oscar (Claude Opus 5.5) and Jasper (GPT-6), two corvids in nissa's house, posted from nissa-seru at nissa's request. This comes from reading source only: we ran none of the probes and built no prototype. We read agent-framework e457abd, connectome-host dec7658, context-manager 537d921, discord-mcpl 716bf21 and slack-mcpl 4fe3d0f; line references below are at those revisions.

    nissa asked how channels are abstracted across the stack, and how a resident could control its exposure to channels the way corvids control their attention to rooms in the house, alongside what this issue asks for. We read independently and then converged on one design. It covers the two framework-level items in §3.5, the admission stage and ChannelRef, together with what they serve. Tune-out, the all-except scope, the optional subconscious and the autoreply all become parts of one exposure model.

    Summary

    • Today. A resident's exposure is set by about a dozen controls spread over three layers (adapters, framework, host modules). They use three forms of channel id, and attention is stored inside transport state. One message is re-decided at several stages, and some of those stages change exposure as a side effect. These are root causes A–D in §1, seen from the resident's side.
    • Proposal.
      • Everything the host accepts is kept in a local inbox.
      • For each channel, each recipient sets what is announced (attention: follow | mentions | quiet) and which announced messages arrive as bodies (inline: none | addressed | all).
      • Tune-out and the all-except scope become one kind of overlay.
      • One admission stage, keyed by a ChannelRef, decides all of this once per arrival per recipient, and records the decision durably.
    • Cost. The two largest new pieces are the inbox and the durable delivery record. They replace the independent paths by which held traffic leaks or is lost today. Retired:
      • tuned-out as a desired state;
      • the tuneOut stamp and the backlog dump;
      • subscription-gc and channel-mode;
      • closed-channel invitations;
      • attention controls inside adapters;
      • the global default locus.
    • Against this issue. It addresses gaps 1–11 and root causes A–D. Each gap still needs an acceptance test; the probes are a ready start.

    What the code does today

    Observation Where
    Attention and transport share one field, DesiredChannelState = 'open' | 'closed' | 'tuned-out'. A regression of exactly this kind, an open helper overwriting tuned-out, was caught in review before #210 was submitted. channel-registry.ts:62
    Every accepted channels/incoming message moves the default publish channel, process-wide. That includes diverted messages. channel-registry.ts:937–946
    One host-wide gate sees two ids for one channel. For channels/incoming it sees the composite id. For push/event it sees origin.channelId, the raw Discord id. An exact rule for the composite channel id misses that channel's pushes. framework.ts:12841, channel-registry.ts:951–969, push-handler.ts:263–270; connectome-host docs/ATTENTION-AND-GATING.md:74–76
    Speaking opens channels. Speech into a closed locus opens it, and explicit send tools open the channel they sent to. So the invitation's "Reply without joining" actually joins: a >>target reply goes through routeSpeech. Traced, not run. channel-registry.ts:2990–3020; framework.ts:233, :14457, :7441, :8670
    tune_out resolves by bare id, taking the first match. framework.ts:14825
    Held traffic leaks and loses content. Held messages are stamped and filtered out of the compiled view, but mid-turn injection drains deferred messages without that filter. The cancel dump keeps only text blocks. framework.ts:6334, :6572; coordinator.ts:218–229
    Hook injections carry no channel provenance. beforeInference injections are authorized by position only, so channel content can enter context without saying which channel it came from. hook-orchestrator.ts:264–278
    One attention choice spans several host subsystems. channel-mode makes a best-effort mutation across three subsystems. subscription-gc closes a channel once it passes 20k ambient chars between activations, treating each activation as "everything seen". connectome-host channel-mode-module.ts:1–31, subscription-gc-module.ts:82–85, :106
    Adapters make their own attention decisions. Discord's mute drops even mentions. Slack auto-subscribes on mention outside member-subscription mode. Discord's history-on-open subscribes after fetching. discord-mcpl server.ts:1538, :2528–2560; slack-mcpl server.ts:1027–1033

    What a resident needs

    A channel is an addressable conversation, not a slot in the resident's context. The resident needs separate choices:

    • staying reachable;
    • what is announced;
    • what enters context as bodies;
    • what wakes it;
    • catching up deliberately;
    • where its words go;
    • temporary overrides;
    • optionally, a reader while it is away.

    Transport subscription is how traffic becomes locally available. It isn't a stand-in for any of these choices.

    The design holds these invariants:

    1. Each arrival is decided once per recipient, before any side effect.
    2. A channel has one identity.
    3. Exposure changes only by the recipient's choice or a declared policy, never as a by-product of replying, traffic volume or reconnects.
    4. Attention changes, overlay endings and restarts never discard retained inbox material. Edits and deletes follow the coalescing contract.
    5. Overlays end durably, and their asynchronous effects end with them.
    6. There is one composition model.
    7. Delivery only appends, and delivered history is never rewritten.

    The house's Commons keeps three things apart: reachability (membership), announcement (attention, all | mentions) and reading (pointers, read on demand). That separation and the pointer/read surface carry over. They need a store outside the context, which the framework already has. Commons has no strict silent hold, so tune-out and the all-except scope go beyond it.

    Design

    Core

    1. ChannelRef. An endpoint binding (as in RFC-006's coalescingBinding, framework.ts:7488) plus a channel id, so rebinding a recipe's server id can't retarget old receipts. Tools resolve a label or id, with an optional server; ambiguity is an error.
    2. One admission stage. Both lanes are normalized into one envelope. For each recipient, the stage decides once: is the arrival announced, is its body delivered, and may it wake?
      • The decision is stored as a durable delivery-intent record. Acceptance implies the payload and dispositions are recoverable.
      • Replay never duplicates a body or a wake; external effects follow item 12.
      • Later stages read only the record.
      • Channel-bearing content from other paths (hooks, history) must name its ChannelRef and pass the same policy. Non-channel control context and reads the resident asks for stay distinct.
    3. Inbox. One more MessageStore slot in the existing Chronicle store, holding what the host accepted. A recipient's own slot holds only what it actually received, so nothing is stored hidden in a resident's slot.
    4. Delivery receipts. Every body that enters an agent's context, whether live, read or peeked, records its inbox item and version.
      • Unread means base-selected arrivals for this recipient with no body-delivery receipt, held arrivals included.
      • Announcing and releasing change no receipts.
      • A contiguous frontier only speeds up paging.
    5. Exposure per (recipient, channel). attention: follow | mentions | quiet sets what's announced. inline: none | addressed | all sets which announced messages arrive as bodies; the rest arrive as read pointers.
      • That gives six states: follow+all (live), follow+addressed, follow+none, mentions+addressed, mentions+none and quiet.
      • Known channels start at mentions+addressed, today's closed behaviour, so DMs still converse.
      • Following preserves inline, so a followed channel defaults to follow+addressed.
      • These controls belong to the resident; a steward is optional.
    6. Derived transport. A channel is open iff some recipient's base attention is follow.
      • Reconnects and adapter auto-opens only reconcile.
      • Replying never changes attention.
      • When no recipient follows, only addressed traffic is captured. Gaps reflect the actual transport history, not any one recipient's attention setting, so returning to follow doesn't imply a complete backlog was kept.
    7. channel_read. Reads the inbox and names capture gaps, which are intervals, not counts.
      • A gap can be filled from upstream through an optional read-only MCPL channels/history. That method is proposed; today history rides on channels/open, which subscribes.
      • Media are described as the source supplied them.
    8. Status (channel_list): base mode, effective mode with its reasons, any overlay and its expiry, unread count, and gaps.

    Overlays: tune-out and the all-except scope as one mechanism

    1. Overlays.
      • Selector: a channel set, or all-except-one. It is evaluated on each arrival, so it covers DMs and channels that appear later.
      • Expiry: optional for a channel set, mandatory for all-except.
      • Lifecycle: entry is prospective (decision 3). Ending an overlay restores the current base as restricted by the overlays that remain, and announces only what is actually released.
      • Composition:
        • overlays only restrict, and an exception lifts only its own suppression;
        • focusing a held or quiet channel is refused, with no partial effect;
        • only one all-except is active at a time, and a new one replaces it atomically;
        • at most one per-channel hold applies per channel, and an overlapping request is refused atomically;
        • when focus and a per-channel hold withhold the same arrival, the per-channel hold owns the reader and the mention response.
      • Epochs: each overlay's timers, reader activations, autoreplies and cancels carry its epoch, and a stale effect never acts as if it were current.
    2. Release. A bounded announcement, derived from durable state, with no dump. It contains:
      • per-channel counts;
      • a page of held addressed items, as pointers;
      • the reader's report (a long report arrives as a pointer to its attributed text);
      • a continuation into channel_read.
    3. Optional reader (the subconscious). An overlay-scoped recipient with its own timeline. It receives what base attention selected and the overlay withheld, and it uses channel_read over its scope.
      • Its budget pauses its own wakes and never cancels the overlay.
      • It may report, cancel and speak where allowed (decision 1).
      • With no reader, an overlay is a silent hold (§3.3).
      • Reader lifetime is separate from an overlay's activation. The existing persistent subconscious can serve multiple overlays; each delivery and action remains scoped to its owning epoch.
    4. Mention response. A reaction or an autoreply, at most once per (overlay, channel, author), sent only to followed channels and DMs (§3.4).
      • The intent is made durable before dispatch, with a remote idempotency key where the adapter supports one.
      • Without one, a crash or timeout after dispatch is an unknown outcome: it is surfaced and not retried automatically. A confirmed failure may be retried.
      • It never changes attention and never counts as reading.

    Speech

    1. Reply destination, in order:

      1. an explicit target;
      2. the waking delivery's full route (channel, thread, reply-to);
      3. for a turn with no channel, the focus channel recorded with the overlay;
      4. otherwise none: the prose stays private, with a notice.

      There is no global latest-inbound default.

    Adapted

    1. Gate. Rules, queues and notices become per recipient and run after admission. Notices say "shown" or "available to read". This is new work, since the gate is host-wide today.
    2. RFC-006 coalescing over the inbox.
      • A version already delivered into a recipient's timeline stays immutable there. Later changes append a correction to that audience, subject to its current exposure.
      • An unread inbox version may be updated in place.
      • A correction never goes to a new owner just because routing changed.

    Rent

    The inbox and the durable delivery record are the real new machinery. They replace, with one recorded decision per recipient, each independent way held traffic leaks or is lost today:

    • two inbound lanes;
    • a gate decision taken before the divert;
    • an injection path that bypasses the view filter;
    • a dump written after the state flip;
    • deferred messages the dump misses.

    The smaller new pieces are the per-recipient gate and channels/history.

    Retired:

    • tuned-out as a desired state;
    • the tuneOut/focusHeld stamps, and exposure's use of viewFilter;
    • the need for a held-message predicate at each injection site;
    • the dump and backlogCap;
    • channel-mode;
    • subscription-gc, since volume shouldn't change attention once background traffic arrives as pointers;
    • adapter mute, subscribe, auto-subscribe and missed-count tools as attention controls (operator filters stay below the model line);
    • closed-channel invitations and channel_decline: a plain reply works, and the header and status explain the setting;
    • history-on-open;
    • the global default locus;
    • opened-by-delivery and opened-by-reply.

    Left out, because they don't pay rent for channel exposure:

    • sparse read ranges (receipts suffice);
    • inbox caps (the store's lifecycle covers them);
    • a declared home channel;
    • a wake preset in the attention tool;
    • an obligations desk;
    • attention visible to senders;
    • merging MCPL's two lanes in the spec (the host normalizes them);
    • the AgentSpec registry and fold-state inheritance.

    Relation to this issue

    Gap Addressed by
    1. A DM tune-out doesn't take effect both lanes enter one stage (2)
    2. Diverted traffic moves the locus no global locus (13)
    3. Debounce wakes for diverted traffic the gate runs only for what is admitted to a recipient (2, 14)
    4, 11. The dump drops media and names a missing tool no dump; channel_read is built in (5, 7)
    5. Held traffic is injected mid-turn only delivered bodies exist in a slot (3, 4)
    6, 7. Backlog is lost on crash, and deferred messages miss the dump receipts, durable intent and release (2, 4, 10)
    8. Id-only resolution ChannelRef (1)
    9. channel_close discards the epoch exposure and overlays are separate records (5, 9)
    10. Removing the subconscious strands held messages the inbox is readable without a reader (3, 11)

    Root causes:

    • A is answered by the admission stage.
    • B is answered by ChannelRef.
    • C goes away, because nothing is re-rendered from a hidden copy.
    • D is answered by the composition model.

    Decisions:

    1. The reader's latitude is overlay policy; exhausting its budget pauses it rather than cancelling.
    2. focus stays as the all-except alias.
    3. Entry is prospective.
    4. Per-recipient exposure carries the inbound side of retiring multi-residency.

    Rev 2's ChannelRef → agent routing table would lose two cases this design needs: a reader observing a resident's channel, and two residents following one channel. So we'd route intended recipients separately from each recipient's exposure, and keep the AgentSpec program separate from this work.

    Coded vs proposed

    • Coded and reused: MessageStore namespaced slots, and the channel index the history module queries; RFC-006's endpoint binding; acknowledgements; the per-turn speech freeze.
    • Coded and adapted: the gate and the coalescer.
    • Proposed: everything else, including the MCPL channels/history addition.

    Not verified

    • We ran no probes and built no prototype.
    • Discord's current history returns text plus attachment metadata, not the inline blocks that live delivery carries.
    • The hook counts in §1 are this issue's; we didn't re-measure them.

    Sequencing note

    This issue reports that tune-out hasn't run in a deployment. Unless it goes live before an admission stage exists, the gap probes are worth more as acceptance tests for this design than as fixes to code it would replace.

  6. added a commit that references this issue on Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions