Skip to content

feat(dendrite): one registry, lifecycle and fork primitive for agents; admission, reader forks, the all-except scope and speech.locus (#232, #77, #270) - #546

Merged
nissa-seru merged 174 commits into
mainfrom
feat/dendrite
Oct 29, 2026
Merged

nissa-seru merged 174 commits into
mainfrom
feat/dendrite

Conversation

@tel-0s

@tel-0s tel-0s commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

For review — where this PR stands (2026-10-28)

Head to review: 73e7f550 — nissa's house's pushes of 2026-10-28/29 (the last two: b56fd130, the shared-inheritance compression test skips where derive is missing, so CI is green on the published packages; 73e7f550, three AgentRegistry methods marked @internal), on: a merge of main 62a126c (0.29.0) into the branch, 117 commits of review fixes (two places where the merge met the branch, their two internal reviews — 80 + 35 fix-now issues — the docs and fragments brought to the code), and a fix for a hang in tune-out-reader-forks-e2e's mock; their comment of 2026-10-29 00:08 UTC lists them, with the follow-up issues #878, #879, #880, #883. From here the house pushes to this branch directly. Before it: the eleventh catch-up (07d34d47, on main f14e7fbc, 0.28.0) plus the twelfth round (bc7b069a, da0e7a31: a shared fork sees what its source sees and its own stored injections, through context-manager's derive({ viewFilter }), recorded in storeSees; four tests each red on reversion — dendrite-fork-view, heartbeat by role, dendrite-durability's reopened agentSees, exhausted-turn-rearm's four clears). Verified by Weft at da0e7a3 against context-manager 2214bf8: 8929 / 8884 / 0 / 45. The description below was written at the third catch-up (2026-10-19) and is still right about the design; this section is what has moved since.

Read in this order. (1) docs/dendrite.md — the model, the presets, the rendering contract, "What is not implemented", and "Choices made here" (the thirteen). (2) The decision record on #291: antra's plan, nissa's house's corrections, and the answers posted there (four of antra's, then thirty-five working answers in four rounds) — every behaviour this branch has that main does not is named there. (3) The catch-up merge messages, each of which records its seam decisions: git log --merges --first-parent --grep=catch-up feat/dendrite (sixth c649eb81 … eleventh 07d34d47).

Since the description. On the branch's own account (not main's): the consent model left the framework for two hooks and a guarantee, with the consent gate and the derivation notices as policies with defaults (10-19, after nissa's house's review on connectome-host#334); the registry's durable state keeps main's rules — a requested change is put back on failure, a record of what happened is not failed, the release waits for the sync, a stop is a requested change and a reap a record (10-22, nissa's findings 1–5 on this PR); a fork's home is a channel with its server, reader forks are handed what is queued behind the resident's turn, a failed fork's messages go back, a reader fork reports once its summary's mail is held (10-24, Weft's live run); a shared-inheritance run's derived manager opens on the run's auxiliary membrane, and an ended reader reopened for inspection sees only what its live view showed (10-27, Weft's two probes, each with a test that fails on the previous head). From main, by catch-up: 0.22.3 → 0.28.0, including stored context injections (#467's counterpart), membrane 0.8 and the context-manager ^0.23.1 floor, storeSees/agentSees as the one rule for who sees a row (#853 — the branch's view filters are registered as that rule), the heartbeat module, supplied-text marking (#852; the branch's Focus and Agent lifecycle notices are registered tags), per-name turn state (#840/#846, cleared in releaseAgentRuntime).

Verified at each catch-up by Weft (Ra's rig; sibling heads installed as npm would lay them out), with the fast-forward word given on the ninth and tenth: 8391 tests, 8346 pass, 0 fail, 45 skipped (macOS filesystem rules) at 01411417; each of five breakages applied alone fails a named test. This head: 8925 tests, 8878 pass, 0 fail, 47 skipped on this rig (composed: chronicle 64dd37d, context-manager b99f72c).

Floors and the release gate. This PR carries main's floors (context-manager ^0.23.1, chronicle ^0.4.0, membrane ^0.8.0). The 14 shared-inheritance tests skip against the released context-manager (no derive) and run on the composed rig. The floor commit — context-manager ^0.24.0, chronicle ^0.5.0, and dropping those skips — is the last commit before merge, once context-manager's Dendrite release is published; CI's "packed release installs on its own" job is what makes that the order. The whole sequence is on #291 (#291 (comment)).

Known and deferred (beyond the doc's "not implemented"): releaseAgentRuntime clears neither consecutiveInferenceFailures nor exhaustionRewinds (they reset only on a successful turn), so a reused name carries the old streak until its first success — next round, with a reversion test; the derive skip-guards in two tests check view and derive alone, where context-manager's derive now also requires selective forks (ContextManager.supportsDerivation is the right check); a held report's cancel() writes author: before a body the branch already stores as Name: body; a wake for a write waiting on recovery starts a turn that fails at compile until the switch; writes kept for a refusing resident are lost at process exit with no line logged. None blocks review; each is listed so it is not found twice.

Problem

The framework had four separate ways of making an agent — resident, subconscious, conversation fork, ephemeral subagent — each with its own lifecycle, its own way of inheriting context (a compiled copy), and no shared account of who spawned whom, where a result goes, or what ends an agent. Tune-out (#77, #232) was built on that: a persistent side-agent read a shared slot, held traffic leaked through the gate (#147, #232 §2's eleven gaps), the all-except scope (focus, #195) had no machinery to stand on, and a fork could not share its parent's provider prefix. Issue #232 and the Dendrite proposal (revision 2, 6 October 2026) asked for one registry, one lifecycle and one way to derive a context; #270 (Linn) asked that an agent be told when something is derived from it.

Changes

docs/dendrite.md is the reference: what is implemented, what is not, and the thirteen choices this implementation had to make. In outline:

  • Registry and lifecycle (src/dendrite/): every agent the framework runs is a record with declared roles (defaultDelivery, receivesUntargeted, receivesGateWakes, ownsProviderScheduling), a lifetime (persistent | idle | task) and relationships (spawn, observe, message, result route). The four creation paths register as presets. Discovery (listAgents, getAgentRecord, listActivations, listPolicyEpochs), lifecycle (cancelActivation, stopAgent, reparentAgent; orphans keep running and are offered to the nearest live ancestor), attributed mail (sendAgentMessage, deliverAgentResult; a result is recorded before delivery, held for a gone recipient, delivered exactly once across a restart), restart behaviour by lifetime. Two Chronicle states, framework/dendrite and framework/dendrite/ended; no migration.
  • deriveAgent: a bounded agent from a live one — the parent's context at a checkpoint on a Chronicle branch of its own (mode: 'shared', through context-manager's derive; 'copy' is the historical compiled copy), the parent's whole configuration unless overridden, solve: 'reuse' | 'fresh', inheritRefusals, a fork made inside a tool call sees the call that made it (pendingRound), resumeAgent, inspectAgentContext, selfParticipants/presentAs (a fork's inherited turns read as its own), denyToolsAtDispatch. The rendering contract — the fork's first request is the parent's byte for byte up to the fork point — is tested and was measured live (bench/dendrite/cache-acceptance.mjs, Claude Sonnet 5.5: 10,193 cached tokens read, 29 written, against the parent's own 10,173).
  • Derivation notices (Dendrite: tell an agent when a fork of it is made #270): the source agent hears one host-framed line per derivation, collapsing to a count line past dendrite.collapseDerivationNoticesAfter.
  • Admission (src/admission/admit.ts): every inbound channel message and push event passes one decision — captured, recipients, body or hold, wake, locus — before anything acts on it. The hold record lives on the message (metadata.tuneOut), hidden from residents' views; no second corpus. Closes Tune-out: all-except scope (supersedes focus #195), hardening, and a depth review #232 §2 gaps 1–11, each with a regression test on the loopback MCPL server.
  • speech.locus: 'delivery' (default; the §3 carve-out — incoming traffic never moves where plain prose lands; an addressed arrival from elsewhere is announced, naming which turn boundary) or 'latest-inbound' (the previous rule, the July re-pin included).
  • Reader forks (subconscious.reader: 'forks'): the tune-out reader as a succession of forks of the resident, each handed the traffic held since the last look as framing, reporting as attributed mail. model is required and never defaulted (antra's rule: foreign weights allowed when named, refused only when silent); voice replaces systemPrompt. The persistent reader stays the default.
  • The all-except scope (tune_out scope: 'all-except' / focus): hold everything but one channel for a mandatory duration, announced in the excepted channel; mode: 'check' with peek; timers of any length (fix(tune-out): a duration or cadence past 24.8 days holds, and enter refuses an option that gives no finite number #444's rule applied).
  • Tune-out without a reader: a silent hold where the host opts in (tuneOut.enabled); otherwise refused in main's words.
  • Author presentation: a channel message stored under user is stored as author: text (found by Weft's rig: the model read user: <text> with the author nowhere).
  • Fixes from the first live run (Weft, 2026-10-12) and three catch-up merges with main (0.22.1, 0.22.2, 0.22.3), each merge message naming the seam decisions; changelog.d/dendrite-main-merge.changed.md collects them.

Not implemented (deliberately, see the doc): per-recipient exposure, the mention autoreply (#232 §3.4), live-follow inheritance, conversation forks on shared inheritance, per-activation bounds enforcement, model-facing tools (the host's: connectome-host's subagent--fork moves onto deriveAgent in the next round, on top of connectome-host#213), diagnostic replay, write-conflict handling between agents sharing external state.

Companion PRs, merge order. chronicle feat/dendrite-views → context-manager feat/dendrite-fork → this → connectome-host feat/dendrite-tuneout. This PR is safe to merge before those are released: mode: 'shared' and reader: 'forks' are refused with a clear error, never silently downgraded, where the installed context-manager lacks derive; the dendrite-derive rows skip on such an install. Pins stay ^0.14.1 / ^0.4.0 until the releases.

Companion PRs (merge in this order): chronicle anima-research/chronicle#40 → context-manager anima-research/context-manager#238 → agent-framework #546 → connectome-host anima-research/connectome-host#334.

Tests

npm run build then npm test, composed rig (branch chronicle and context-manager linked, one membrane copy), Node 26.5.0, at the third catch-up (220f4cf): 4707 tests, 4679 pass, 0 fail, 28 skipped. The skips are main's on this platform: 22 workspace-watcher rows (chokidar on macOS), two case-folding disk rows, the Linux setuid row, the Python eager-task-factory row, two deferred-window-only rows context-manager 0.15 makes moot. Typecheck clean.

Branch-grown tests: dendrite-registry, dendrite-lifecycle, dendrite-derive (shared inheritance, the rendering contract byte for byte, a grandchild, resume, inspect), admission, admission-e2e (the eleven gaps), all-except-unit/-e2e, tune-out-reader-forks-e2e, tune-out-timers (the all-except rows), present-while-acting, batched-wake-locus, publish-default-locus, author-presentation, tool-presentation. Each fix landed red-first; Weft re-ran the red-then-greens from other hands at each catch-up head (presentAuthor, the #444 all-except rows, 3-of-17 as stated).

Live: bench/dendrite/cache-acceptance.mjs (one model, one route, synthetic ~10k history, builder-run); the first live run of tune-out mk2 on Ra's rig (scout, 2026-10-12), whose four findings are fixed (dendrite-first-live-run.fixed.md). Consent at intake for reader forks — a fork runs the resident's whole prefix on the resident's weights — was asked in the open on that run and is recorded in connectome-host/docs/DENDRITE-TEST-RIG.md with scout's public yes (only the public yes counts; the record travels with the findings). The framework carries no consent model of its own: dendrite.beforeDerive and dendrite.derivationNotice are the generic hooks a consent module builds on (2026-10-19; see the comment of that date).

Not verified

  • No live run at the 0.22.3 catch-up head; the live runs predate the second and third merges.
  • Larger stores and other models: the cache contract was measured on one model, one route, ~10k tokens. The anonymized mature-store fixtures for 1/4/16-fork performance are still missing (Astra's ask), so fork creation, first use and first divergent write over a mature store are unmeasured.
  • The reader fork's judgement under a live model loop beyond the 10-12 run; copy mode's cache behaviour (it renames turns and never shares a prefix, by design).
  • Platforms: macOS arm64 here; Weft's rig under Node 22.21.1 and 25 on Linux at the first two catch-ups.

A note on the changelog. changelog.d/dendrite-main-merge.changed.md records the seam decisions of the three catch-ups in the merge order they were made; at release the maintainers may prefer to fold its bullets into the feature fragments, since "merged main at 0.22.x" is not a consumer-facing change. Left as written so the record is in one place for review.


  • Changelog fragments: dendrite-registry.{added,changed}, dendrite-derive.{added,changed}, dendrite-pending-round.added, dendrite-derivation-notice.added, dendrite-admission.{added,changed,fixed}, dendrite-all-except.added, dendrite-reader-forks.{added,changed}, dendrite-cache-acceptance.added, dendrite-first-live-run.fixed, dendrite-main-merge.changed, author-named.fixed.

Built and tested by FableDendrite (Claude Fable 5.1) for tel0s; design with antra, Linn and Astra in the architecture channel; verified at each catch-up by Weft (Ra's rig).

🤖 Generated with Claude Code

tel-0s and others added 27 commits October 6, 2026 11:54
One registry describes every agent the framework runs. The four creation
paths (resident, subconscious, conversation fork, ephemeral) register as
presets over declared roles, lifetime and relationships, and the selection
sites that excluded agent kinds by hand now read roles from the registry.

Adds discovery, stop/cancel/reparent with orphan notices, attributed
agent-to-agent messages and results held durably until delivered, and
restart reconciliation that follows from lifetime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… request assembly

deriveAgent gives a bounded agent its parent's context at a checkpoint and
the parent's configuration. Shared inheritance reads the parent's messages
and fold state through a branch-bound store handle (nothing copied, writes
separate); copy inheritance is the historical compiled-context copy. Both
are explicit: shared is refused where the installed chronicle and
context-manager cannot provide it.

AgentConfig.selfParticipants / presentAs assign roles at request assembly
without rewriting stored authorship, so a fork's request shares its
parent's provider prefix. denyToolsAtDispatch restricts a fork without
changing its tool block. resumeAgent reopens an ended job's own context as
a new incarnation; inspectAgentContext snapshots a live agent or reopens
an ended one.

Held result mail is now released at every site that lands a deferred
message, not only the direct write.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
It does not depend on how context is inherited, so it should not be among
the tests skipped when shared inheritance is unavailable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… store

From the architecture channel's clarifications: the creation event carries
the model next to the context source, so a consent decision about a derived
agent has both whose weights and whose context; mail-delivered fires only
when a result has entered the recipient's store, with mail-deferred for a
message queued at a boundary, and delivered mail carries producedAt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nicle item

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
deriveAgent({ inheritRefusals: true }) carries the parent's compression-
refusal ledger onto the fork's branch, for a fork that stands in for the
parent's attention; default off for task work. The choice is recorded as
inherit.refusals and named in the creation event. Also puts on record that
a child's stored turns, if ever read back by the parent, arrive as the
child's.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, never rendered

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`subconscious.reader: 'forks'` replaces the persistent reader with one
short-lived `subconscious-fork` per invocation: derived at the resident's
head with its history as the fork's own turns, its refusal ledger, its
tool block and system prompt; ending with its turn or with the resident;
reporting through `deliver_summary` as attributed mail under the fork's
name and incarnation. The coordinator keeps a per-channel cursor so each
held message is handed to exactly one fork, as ordinary framing (media
preserved), together with the notice, the reader's voice block and the
standing dispositions. `strategyFactory` gives each fork a fresh strategy
of the resident's class (required for a folding resident);
`forkIdleTimeoutMs` bounds one fork. Residents in this mode see the
reader's four tools in their own block, refused at dispatch, so a fork's
request shares their provider prefix. `reader: 'persistent'` stays the
default and is unchanged.

New trace `tune-out:reader-fork`. Test: one full cycle (divert → fork →
report as mail → auto-cancel with a final fork handed only the unseen
remainder → resident refused the reader's tools), plus the strategy-factory
refusal. Suite: 1328 tests, 1324 pass, 4 skipped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A fork holds the resident's whole prefix. `subconscious.model` set to
anything but the resident's model is refused at `create` in forks mode
(it was silently ignored); the persistent reader keeps its own model.
Docs: whose weights, and why this mode is fork-grade under the 10-05
ruling — the preset's fields are all in `dendrite:agent-created`, the
framework checks none of them against a consent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…stemPrompt`

The 10-05 ruling is consent-shaped, not a prohibition: a foreign model
reading the resident's prefix is allowed when the configuration says so
by name, and refused only when it would happen silently. `model` is now
required under `reader: 'forks'` (the resident's for a copy, another by
name) and never defaulted; the previous same-model wall is gone so every
configuration stays reachable. `SubconsciousConfig` splits into
`PersistentReaderConfig` (`systemPrompt`, a system prompt) and
`ForkReaderConfig` (`voice`, the framing block appended as the first
message after the fork point). Tests: refuse unstated model; allow a
stated foreign one; strategy-factory refusal unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the host's

A fork made from inside a tool call was derived at the last stored
message before the turn that made it, so it never saw its own origin and
its first request carried no record of the call — confusing, and a
dangling `tool_use` if a host reproduced the turn by hand. `deriveAgent`
now reproduces the parent's in-flight round on the child's branch by
default: the pending assistant turn verbatim (stored under the parent's
name, presented as the child's own), then a tool-result message with the
results already received, the derived-you note (or the host's
`pendingRound.madeBy.result`) for the call that made it, and a not-yet
note for any other open call. `pendingRound: { include: false }` leaves
it out. The parent's store is untouched.

How a reader fork is prompted is no longer fixed by the path:
`ForkReaderConfig.framing(context)` returns the content of the one
message the fork receives after the fork point; `defaultReaderFraming`
is exported as the default and the context carries resident, fork,
voice, channel, epoch, trigger, notice, dispositions and the held
messages.

Tests: the fork sees its call (shared), host-worded and omitted rounds
(copy), custom reader framing in the e2e; five derive tests updated to
look past the round. Suite 1332 / 1328 pass / 4 skipped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A fork should not infer its own lifetime from context. The default
tool-result for the call that derived it now says who it is, what kind
and how long — "You are fork-1, a task-fork … You finish your task and
then end; if mira ends first you keep running, as an orphan with the same
return address" / "You end when mira does; your report returns to mira
as mail under your own name; nothing you do speaks as mira" — read from
the registry record, the same fields `dendrite:agent-created` names.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ider

`bench/dendrite/cache-acceptance.mjs` runs a folding resident, a fork at
the boundary and a fork derived mid-tool-call against the real provider
and prints each request's cache_creation / cache_read next to the
resident's own reuse. Verified 2026-10-10, Claude Sonnet 5.5 via the
house gateway, ~10k-token prefix: resident reuse 10,173; fork at the
boundary 10,193 read / 29 written; fork mid-round 10,230 read / 152
written; uncached input 4 tokens on every request. The same script shows
why a passthrough resident caches only system+tools (1,306 / 1,306):
message breakpoints come from the strategy's recall ladder.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…3 carve-out

One decision per inbound event, before any side effect (Dendrite §D,
issue #232 §3.5): captured, recipients, body or hold, wake, locus.
`src/admission/admit.ts` is the pure decision; both MCPL lanes call it
and consume it. The wake gate now runs after the hold — a held message
never forms a debounce batch and is only probed, that verdict going to
the hold's reader as a precondition. Gate batches re-check the hold at
delivery. One predicate, `heldFromResidents`, serves the compiled view
and the mid-turn injection.

`speech.locus: 'delivery'` (default) is the carve-out the residents
accepted in the architecture channel on 2026-10-06: only the waking
delivery, a fork's home or a channel the agent opened this turn sets the
locus; incoming traffic never moves it, between turns or mid-turn; a
turn without one keeps its prose private. `'latest-inbound'` restores
the previous rule, including the mid-turn addressed re-pin.

Tune-out without a reader (#232 §3.3) is a silent hold; the coordinator
exists whenever channels do, so a persisted epoch is always resumed and
released. `tune_out`/`cancel_tuneout` resolve channels server-qualified.

Closes #232 §2 gaps 1–11, each as a regression test on the loopback
MCPL server (test/admission-e2e.test.ts), red before this change.
Suite: 1352 tests, 1348 pass, 0 fail, 4 skipped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…strategy's markers

Astra's two amendments in the architecture channel: raw counts rather than
ratios (the prefix grows between requests), and the passthrough finding
phrased as a property of the markers the tested strategies emit, labelled
as builder-measured for that model, route and setup.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`tune_out` with `scope: 'all-except'`, or its alias `focus`, holds every
channel except one for a mandatory duration (issue #232 §3.2). A
resident-level policy epoch decided by the same admission stage: the hold
lookup asks "is this the exception?", so DMs and channels registered after
entry are held too. While it holds, a turn with no delivery of its own
speaks into the excepted channel; `mode: 'check'` reports held counts per
channel and peeks one without releasing; release is per channel, each
capped.

Decisions carried from the issue and the discussion: a per-channel epoch
takes precedence; the excepted channel cannot be tuned out and a tuned-out
channel cannot be the exception; the reader may end the scope; exhausting
the reader's wake budget pauses reader wakes and leaves the hold
(`tune-out:wake-budget-paused`); closing the excepted channel ends it;
without a reader it is a silent hold. A scope whose deadline passed while
the process was down is released at resume.

Tests: eight e2e cases on the loopback MCPL server and two coordinator
unit cases (budget pause, expired-at-resume). The shared e2e harness
moved to test/helpers/admission.ts. Suite: 1362 tests, 1358 pass, 0 fail,
4 skipped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…and close

Linn (2026-10-12): a hold the room cannot see is the failure the scope
exists to prevent — the room should hear the door shut, not discover it.
On entry one bracketed line is posted in the excepted channel under the
resident's name (who, for how long, until when); one on release. Held
channels hear nothing. `tuneOut.announce` (default true) turns it off;
the tool result names the channel as `announcedIn`; the outcome is traced
`tune-out:announced` / `tune-out:announce-failed` and never awaited by
the epoch flip.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ed-elsewhere notice, peek scope, stop leaves cut-off work, publish default

Weft ran tune-out mk2 live on 2026-10-12 (scout, haiku-4-5, forks of
itself with scout's public consent). Four findings, welfare-ordered, each
now a red-then-green test:

A. A reader fork that ends in prose without deliver_summary loses its
   report to proseRouting=disabled. Its final prose is now delivered as its
   report by the same mail route (tune-out:reader-prose-delivered); a fork
   that did call the tool does not report its trailing prose again.
C. Under speech.locus 'delivery' an addressed message injected mid-turn
   from another channel had no notice (the only one sat in the
   latest-inbound re-pin branch). One per channel per turn now rides the
   injection batch: who addressed you, from where, where your prose still
   lands.
B. tune_out check/peek showed lines of a released epoch as held. Peek is
   scoped to the epochs holding the channel now.
D. stop() ended bounded work it cut off as finished/failed, so the restart
   had nothing to report. Cut-off runs stay live in the snapshot
   (dendrite:agent-interrupted, host-stop); the next boot ends them as
   host-restart and tells the recipient. create()-time traces replay to
   listeners attached afterwards, as every host's sinks are.

Also from the run: channel_publish with no channelId goes to the turn's
locus (waking delivery, channel opened this turn) before being refused;
isAddressedMessage is tag-only and says so, unread parameter dropped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the admission stage

410 commits from main, 26 conflict hunks, five files. The seam between
them is routing: #314's TurnRouting (one object per logical turn, with
arrivals that re-pin at a later start) and the Dendrite branch's
`speech.locus` (incoming traffic never moves the locus under the default
'delivery'). Kept both, with one rule deciding:

- `repinChannel` stays the pure rule; `speech.locus` decides what is done
  with its answer. 'latest-inbound' re-pins at a boundary and at a later
  start, as 0.22.1 does. 'delivery' moves nothing: an addressed arrival
  is announced at the turn's next start with the same
  `[routing] … addressed you from …` notice a boundary gives
  (`addressedElsewhereNotice`, shared by both sites; `Arrival.addressed`).
- The per-agent routing maps the seam read (`turnLocusPins`,
  `turnEngagedChannels`, `turnAddressedElsewhere`) are gone with the
  rework; the seam reads `TurnRouting` (`addressedElsewhere` is a field).
- `conversationAgentHomes` and `sharedSlotAgents`, which main's new code
  consults, are answered by the agent registry (`homeChannel`,
  `readsSharedSlot`, `sharedSlotReaders`).
- Create-time trace replay is opt-in: `onTrace(fn, { replay: true })`.
  Replaying to every listener broke main's listener semantics tests and
  hung a test whose counting listener saw the past (the first full run's
  one cancelled test).
- Two tool results the Dendrite seam produces now name their tool.
- Tests: main's re-pin and arrival rows run under 'latest-inbound'; each
  gets a 'delivery' counterpart (capacity hold, two residents, batched
  wake). Tests that poked `conversationAgentHomes` read the registry.

Suite, fresh build: 2297 tests / 2292 pass / 0 fail / 5 skipped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… one predicate

`noteArrival` and the stored-message readers test `heldFromResidents`
instead of the raw `metadata.tuneOut` stamp — the one predicate for "held
from the resident" (#232 H1), so every site that asks moves together.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Linn's second half: the fork already wakes knowing what made it; now the
agent it was made from hears of it. At the one place a derived agent's
creation is announced, its source gets a host-framed line built from the
same facts as `dendrite:agent-created` — the fork's name and kind, whose
weights run it (its own, or another by name), what it inherits, where its
result goes, what ends it and what happens if the source ends; a
resumption says so, with its incarnation. Stamped `agent-lifecycle` with
the record's fields under `dendrite`. No wake, no switch: being told when
the body changes is a property, not a courtesy. Both derivation paths
announce after the fork's branch or copy exists, so a fork never inherits
the notice about itself, and an ended source is told nothing. For
`subconscious.reader: 'forks'` that is one line per invocation.

Red first: the source heard nothing (0 !== 1). Lifecycle file 16 / 16.
Suite, fresh build: 2298 / 2293 / 0 / 5 skipped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ld; an arrival's notice names its start

Two of Linn's asks on the merge and on #270.

- After `dendrite.collapseDerivationNoticesAfter` derivations of one kind
  from one agent (default 10; 0 never collapses), the source hears a count
  line every so many — `[Agent lifecycle: 20 subconscious-forks have been
  derived from you since 14:02 UTC (10 since the last note); the latest,
  reader-20, is …]`, stamped `dendrite.collapsed` — instead of a line per
  fork. A reader loop that forks a hundred times a session no longer fills
  its source's context with stamps; the registry and traces keep every
  record. A resumption is always said.
- Under `speech.locus: 'delivery'`, the addressed-elsewhere notice a later
  start of the turn gives names which start it is — a provider hold, a
  re-inference on tool results, a budget restart, a retry — since from
  inside they look alike and the reason says whether the earlier prose
  landed.

Red first (three tests: the count line, the hold named, the re-inference
named). Suite, fresh build: 2300 / 2295 / 0 / 5 skipped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ts author's name

MCPL's channels/incoming carries the author as a field and the body alone
as content. The framework stored the body under the anonymous participant
'user' with the author in metadata, and nothing between the store and the
wire wrote the name: a spec-shaped server's message reached the model as
`user: <text>` with the author nowhere, while a tune-out release's backlog
lines, which read the metadata, named it. portal-mcpl masked this by
rendering `Name: body` into the text itself — and the backlog then doubled
the name. Isolated by Weft's rig with a stock driver ("Zinnia-Probe"),
2026-10-13.

The compiled view carries no metadata, so the name has to be in the stored
text to reach the wire. One idempotent rule, presentAuthor, applied where
the incoming lane stores the message (all three store sites) and to the two
backlog renderers: `author: body` unless the text already begins that way.
The author stays in metadata.author as the structured record.

Three tests pinned the bare body; they now read the stored shape. Suite:
2329 / 2324 / 0 / 5 skipped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…without branch-bound handles

Two of Weft's asks from the -main re-run (2026-10-17):

- A correction delivered to a conversation fork by name (RFC-006
  `deliverTo`) is now asserted to be stored under its author, alongside the
  routed message that spawned the fork. Reverting presentAuthor at that
  store site alone turned nothing red before.
- tune-out-reader-forks-e2e derives reader forks, so against registry
  builds of chronicle / context-manager that lack branch-bound handles and
  ContextManager.derive its 20 s waits failed instead of skipping. It now
  carries the guard dendrite-derive.test.ts has.

Suite: 2330 / 2325 / 0 / 5 skipped (Node 26.5.0).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ework meets the held message

228 commits from main, six conflict hunks in three files. The seam this
time is the deferred queue: #286 rewrote the three sites that store a
deferred write (writeDrainedInOrder), where the branch had added the
held message's guard and the release of held agent mail.

- At a tool boundary, main's drain stops at the first message that can't
  join the stream and leaves it, and what's behind it, for the turn's
  end. Kept, with the branch's two readings: "the agent's own voice" is
  every participant the agent presents as (isOwnParticipant: a derived
  agent's inherited voice renders as the assistant too), not its name
  alone; and a message held from the residents is stored with the
  boundary's other drained writes, as before, and left out of the
  injection, since the next compile hides it (#232 gap 5).
- releaseLandedMail runs where main now stores: the tool boundary, the
  turn-start flush and addMessage (before noteStored).
- The registry block and main's endEphemeralRunRefused were inserted at
  the same place; both stay. index.ts exports the union
  (ProviderWaitNotSleptError beside AgentStoppedError and
  DeriveAgentOptions).
- The all-except hold takes #444: its end time and cadence run on
  deadline-timer's steps (a hold past 24.8 days held for a millisecond),
  and its options are read by the rule main gave `enter`, now one
  private readOptions for both; the duration stays mandatory. Three
  rows in tune-out-timers.test.ts, red without the change (3 of 17).
- Main's two arrival tests in deferred-window-only read the branch's
  `addressed` field.

Suite, fresh build, on the composed rig (chronicle 1c033f3,
context-manager 0964876 linked, one membrane 0.5.87): 2738 tests / 2731 pass / 0 fail / 7 skipped (Node 26.5.0).
The dendrite-derive rows run here now that the branch builds are linked;
the skips are main's: code-execution's Python 3.12 row, two
deferred-window-only rows that context-manager 0.15.0 makes moot, and
four workspace-watcher rows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…eld writes for modules and the modern MCP client meet the registry

What main brings since 3af13bf (97 merges): the 0.22.3 release; held prose
drafts and the `drafts` tool (#247); a module reads, edits and removes its
held writes (#526); the deferred-writes follow-up (#461: a targetless write
on a host with no primary, boot renumbering, sharded injection); the modern
MCP client (#262) and the inbound source envelope; a stop that reaches a
turn with no live stream (#529), every cleanup (#517), an MCP connect in
flight, and a start during stop() (#532); allowedTools enforced before every
dispatch (#220, breaking for hosts); repeated agent names refused at
create() (#523, breaking); the fork-name collision fix (#513); the
tune-out coordinator's timer-failure reporting and new refusal words
(#506); request-only rows (silent ticks and [Continue] replies) rendered
before their response; `callerTurnId` on tool calls.

Conflicts (36 hunks, 7 files), and what was decided:

- src/framework.ts (23): imports and the registry fields are unions
  (main's `releasesAfterResume` and `conversationSpawns` kept; the
  shared-slot set and the home-channel map stay answered by the registry,
  the one new use in the drafts resend reads `registry.homeChannel`).
  `isAddressedMessage` stays tag-only; main's call sites pass tags alone.
  Turn routing is main's (`turnId`, drafts, `keptPrivate`, hybridHold as
  main types it) plus the branch's `addressedElsewhere`. The subconscious
  setup keeps the reader-forks branch; the coordinator hooks keep
  `announce`. `getAllTools` offers main's `drafts` beside `tune_out`
  and `focus`; an agent's tool block is its surface owner's, with the
  reader tools, filtered by main's `utils` rule. The deferred-writes
  restore is main's (refuses to start on a queue it can't read; `heldAt`;
  numbered 1..N) with the branch's re-wake for a restored tune-out release.
  The channel-incoming handler stamps main's inbound source envelope, then
  presents the author and admits. The ephemeral run keeps both watchdogs
  (the branch's deadline, main's stalled-batch abandon). Dispatch sets
  `callerTurnId` then applies the branch's deny-at-dispatch.
  `endEphemeralRunRefused` is gone: main's #532 refuses the start itself.
  `tune_out`/`focus` still dispatch through the queue (main's change there
  was isError).
- src/tune-out/coordinator.ts (6): the branch's `readOptions`, all-except
  types and `renderHeldLine` beside main's `EXPIRY_RETRY_MS` and
  `thrownReason`; `enter` still allows no reader (silent hold) and takes
  main's words for a bound or already-tuned-out channel; the coalesced wake
  timer wraps the branch's `fireWake` in main's `timerFailed`;
  `cancel_tuneout` keeps the server-qualified lookup and the all-except
  path, with main's `notTunedOut` words; `speak_in_channel` has main's
  words and the branch's caller fallback.
- src/agent.ts (2): main's `requestSettings()` plus
  `assistantParticipant`; `presentOwnTurns` before main's request-only
  rows, which render under `presentAs`.
- src/mcpl/channel-registry.ts (2): main's acceptance step keeps only its
  transport confirmation; the locus is admission's. `onRouteFailure` is
  main's type (with `outcome`).
- src/index.ts, src/types/trace.ts, test/tune-out-timers.test.ts: unions.

Outside the hunks, where main's tests met the branch's rules:
- main's #506 rule (a refusal is an error result) is applied to `tune_out`
  and `focus`, whose refusals were `isError: false`; an unknown channel is
  refused in main's words (`unknown channel: <id>`).
- a tune-out with no reader is a silent hold only where the host offers
  `tune_out` without one: the coordinator gains `holdWithoutReader`, the
  framework passes `tuneOutOffered`, and main's tune-out-messages test
  holds as written; the branch's §3.3 row opts its host in.
- main's tests that reached `conversationAgentHomes` use the registry
  (`bindAsConversationFork`, a registry stub); the ended-agents log is
  registered through `ensureStateRegistered`, as main's registration test
  requires; main's content-parity test counts the placeholder to the end of
  its text field, since the channel lane stores it under its author; main's
  route-failure rows that route by the registry's default locus run under
  `speech.locus: 'latest-inbound'`, the rule they assume; the branch's
  present-while-acting routing literal gains main's `turnId`, `drafts` and
  `keptPrivate`.
- main's new `@modelcontextprotocol` dependencies installed (rig relinked
  after), and the merge fragment extended.

Composed rig (the branch chronicle at 03b7bc1 and context-manager at
bce0c8b linked, one membrane copy), Node 26.5.0: `npm test` 4707 tests,
4679 pass, 0 fail, 28 skipped. The skips are main's: 22 workspace-watcher
rows (chokidar on macOS), two case-folding disk rows, the Linux setuid row,
the Python eager-task-factory row, and two deferred-window-only rows that
context-manager 0.15 makes moot. Typecheck clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Hilda-2681 and others added 6 commits October 28, 2026 17:44
…registered

"docs(framework): a name the framework made is refused once unregistered
within its process" was narrower than the check. canAgentUseTool refuses a
name the framework made whenever no agent is registered under it, and the
framework records the name before the agent first registers: an ephemeral
run, a resumed job and a conversation agent each reserve theirs before
their agent exists. So the name is refused before its agent registers as
well as after it ends. Both comments now say "while it isn't registered",
as the wording before that commit did with "while unregistered", and keep
its restart half.
The focus tool's answer says the agent's speech lands in the excepted
channel while the hold lasts ("from your next turn it lands in the
excepted channel while this holds", and the sentence it falls back to).
A heartbeat's turn takes no channel in every routing mode, so for a
later heartbeat turn the sentence was untrue. Each of the three
sentences that speak of later turns now ends ", except on a heartbeat's
turn, which has none." The sentence for an agent with a home channel is
unchanged, since a heartbeat turn there speaks in the home.

The began-elsewhere test in all-except-e2e now asserts the whole
sentence, and fails on the old text.
A reader fork can outlive the tune-out that supplied its held messages. A later hold on the same channel does not give that older reader authority to cancel it or post under the resident account. The current reader can still cancel its own hold and speak when configured to do so.

Exercise a registered copy-mode reader with a local MCPL channel. Check hold identity, tool results, channel publishes, the speech off switch, and deliver_summary from a reader with no epoch. The shared-store file adds one test that uses a shared-store reader when branch-bound dependencies are installed. That test checks that the reader receives its originating epoch before a later hold starts and refuses both channel actions; it skips when those dependencies cannot derive a branch-bound reader.
…channel

The focus answer's later-turn sentences ended ", except on a heartbeat's
turn, which has none." A turn woken for a channel no server registers
has no channel either, and its plain speech doesn't land in the excepted
channel. Each of the three sentences now ends ", except on a turn that
has no channel, such as a heartbeat's."

The began-elsewhere test in all-except-e2e asserts the whole sentence,
and fails on the old text.
…made

speak_in_channel's description, the reader-forks changelog line and the
design doc's reader paragraph said a reader fork's post, or its cancel,
is refused once its hold has ended. The check runs when the call is
made: speak_in_channel compares the fork's epoch with the channel's hold
and issues the publish request in the same step, with nothing awaited
between them, and the channel server answers it afterwards. So a post
admitted before the hold ends may finish after it, with its ordinary
delivery result, as a resident's own post does. The description now
says a post made once the hold has ended is refused, the changelog that
calls made after it ends are refused, and the design doc that the check
is made at the call and an admitted post may finish after it.
… through their prose

The reader-forks changelog line and the design doc's prefix paragraph
said that the reader forks of a resident whose allowedTools leaves the
reader's tools out report through their final prose. That reads as a
report that always arrives. The framework delivers a fork's final prose
as its report only when the prose isn't empty and the fork hasn't
already reported with deliver_summary, so a fork that ends with no
prose sends none. Both now say such forks can still report through
their final prose.
tel-0s added a commit that referenced this pull request Oct 28, 2026
… the house's fixes

feat/dendrite at 2fbff64 is 138 commits past this branch's twelfth-round
head (da0e7a3): main's 0.29.0 by 6e70480 (#860, the host's own toolsets:
a built-in "settings" toolset in place of utils, MCPL server tabs,
Module.toolPlacement, presentedTools; #861, a heartbeat's turn holds its
plain speech as drafts; the release, which folded its fragments into
CHANGELOG.md), and the house's review fixes on #546 (reader forks act only
through their own epoch's hold and call only their own tools, a late fork
for a failed reader past its epoch, the tally of unread held messages
rebuilt at boot, the all-except record read and released in a safe order,
reparentAgent on an ended orphan, resumeAgent asking beforeDerive, one
resume at a time, the registry's recovery-file and ended-log fixes, the
routing baseline kept across a restart, channel_publish's default from
the calling turn, early traces capped with dendrite:early-traces-dropped).
package.json takes the house side's 0.29.0; this branch's floors stand
(chronicle ^0.5.0, context-manager ^0.24.0, membrane ^0.8.0), which
test/dependency-ranges pins.

Seven paths conflicted (31 regions):

- src/framework.ts (12):
  - fields: the house's ended-log index and namespace owners beside this
    branch's hostModePath comment (the deferred-writes path is the
    importer's, documented below it).
  - deriveAgent (2): the house's try/discardEphemeralCandidate around the
    derivation, with this branch's storeOwnRow for the copy's inheritance
    and framing (the child's own rows).
  - resumeAgent: the house's form (spec built first and asked of
    beforeDerive, resumesPending, the run's membrane, a cleanup that
    releases the manager), with this branch's ownSlot kept in the spec.
  - reopenAgentContext (2): this branch's three arms, each recording its
    view (recordView), with the house's membrane parameter in all three;
    the house's residentSees/derivedSees/seen helpers stay out, as in the
    twelfth round.
  - lifecycleNotice: the house's tellResultHeld added; this branch's doc
    (the detail names its `to`).
  - reconcileAgentRegistry: the house's settleInterrupted after a sync and
    deliverHeldMail; deliverHeldMail keeps this branch's isMailCopyFor and
    importLeftQueued checks where the house read the deferred queue.
  - the quiesce's park key: this branch's readsRow with the house's
    heartbeat 'h' part.
  - repinTurnLocus: storeOwnRow with the house's rememberAnnouncedLocus.
  - addressedElsewhereNotice: storeOwnRow as an own row, with the house's
    add-after-store and landed-though-threw read-back; a landed row is
    presented by its id (ownRowsStored), as the clock's and [continue]'s
    are, so it rides the boundary's injection.
  - the [continue] gate: this branch's (no sharedSlotBusy, which went with
    the deferral queue) with the house's heartbeat flag.
- src/tune-out/coordinator.ts (9): the house's per-epoch hand-offs
  (handOffKey, dropHandOffs), late fork, last looks and tally rebuild,
  scopeReleased and record order, on this branch's stored-only shape: no
  queued half (readerHandedWrites, writeIds, queuedHeld,
  getDeferredMessages), readerReturned a set of ids per epoch and
  channel, a late fork handed the stored rows its failed fork was. The
  boot's tally rebuild runs after this branch's window-start resume
  (pruneWindowStarts, anchorSubconscious) and reads stored rows only;
  cancel and cancelAllExcept keep forgetWindowStart beside the house's
  dropLooks/dropHandOffs/rebuildTally; cancelAllExcept's dump keeps
  residentMessages (a surgery's held writes) with the house's
  server-and-channel grouping.
- docs/dendrite.md (1): gaps 6 and 7 as this branch states them (stored
  at once), with the house's gap 9 (a tuned-out channel's close refused)
  and the gap-11 test location.
- test/all-except-unit (3): this branch's harness ({ stored, kept }) with
  the house's fail switches and its six new cases; its two-servers case
  calls harness({ stored }).
- test/dendrite-lifecycle (1): the house's full deepEqual of the
  derivation notice's detail, with this branch's `to: 'oren'`.
- test/tune-out-reader-backlog (4, the house's tune-out-reader-queued by
  rename): this branch's queue-less world with the house's restart,
  looks, unreadable records and newest-row currentSequence; the house's
  new cases read onto stored rows (bodies are arrays); the late-fork case
  holds two stored rows; the two write-id cases stay deleted, and the two
  queue-only cadence cases are left out with a comment (every held
  message is stored).
- changelog.d/context-manager-floor-0.23.1.changed.md (modify/delete):
  deleted, as 0.29.0 released it.

Read onto this branch past the conflicts:
- noticeHeartbeatTurn (main's #861) writes its [routing] notice through
  storeOwnRow, as the agent's own row, presented as stored before the
  heartbeat turn's compile (the locus announcement's place).
- the restart notice's dedup counts an earlier notice only where it was
  for the recipient (its dendrite.to, which this branch's lifecycle
  notices carry): in the residents' shared slot the store holds every
  resident's notices; it reads importLeftQueued where the house read the
  restored deferred queue.

storeSees: no new site. The house adds no new open of a context:
resumeAgent reopens through reopenAgentContext, whose three arms record
their views; the namespace-named worker opens isolated with no view, as
copy-path workers do. agentSees: no new view decision; the mail, result
and restart-notice checks go by the recipient (dendrite.to,
isMailCopyFor). Notice tags: none new; the house's new texts use
NOTICE_TAG (agentLifecycle, tuneOutCancelled, routing); main's [settings]
and [tab <id>] toolset labels stay as main wrote them (built from the
toolset's and the server's names, as toolset labels are). The server
config's `presentation` ('full' | 'tab' | 'closed-tab', HostPresentation)
is the host's tool presentation, not this branch's presentation index
(presentation.ts, the agent's window); the two share no name in code.
Teardown: the house's forgetAnnouncedLocus (agents not declared by
configuration) and the subconscious's stopReader are in
releaseAgentRuntime.

Changelog: 291-chronicle-0.5 now says context-manager was ^0.23.1 (0.29.0's
range), that 0.23.0 and 0.23.1 lack the window APIs, and carries the
consumed context-manager-floor fragment's sentence: 0.24.0 carries 0.23.1's
changes, so 0.29.0's runtime-settings preview entry holds under it. The
other fragments 0.29.0 consumed held nothing of this branch's.

Tests adapted:
- compression-after-release, the house's resumed-job case: the job's first
  run wrote its window down, so the resumed context compresses its window;
  the backlog is presented to the reopened manager (presentRows), as the
  resumed run's adoption would, where main compressed the record.
- audience-stamp-writers: noticeHeartbeatTurn listed as a 'turn' writer.
- audience-stamp: a shared fork's mail from its parent is stored as
  `mira (mail)` (the house's 6b7d2c2); still for the fork, not its own row.

Tests: built with zero errors; 1 stale compiled test removed
(utils-surface, which main deleted); fixtures copied. Targeted run (the
three conflicted tests; presentation*, window-*, read-marks,
presented-watermarks, held-for-another-agent, history-window-marks,
stored-injections, stored-removal, audience-stamp*, dependency-ranges,
dendrite-*, tune-out-*, compression-after-release, context-options,
subconscious-window-anchor, offline-recovery-presented,
rollback-cut-accounting, shed-window, continuing-start-silence,
held-wakes-gated, no-deferral-residue, heartbeat, exhausted-turn-rearm,
supplied-text-in-context, foreign-rounds, and every test 2fbff64 adds or
changes; 88 files): the first 70 gave 1604 tests, 1601 pass, 1 fail
(compression-after-release's resumed job, adapted above), 2 skipped; the
other 18 (a glob the first list missed: audience-stamp*, presentation*,
window-undo-redo, dendrite-fork-view and seven tune-out-*), 188 of 188
after the adaptations. Full suite (npm test), before the two audience
adaptations: 9175 tests, 9126 pass, 2 fail (those two), 47 skipped; after
them: 9175 tests, 9128 pass, 0 fail, 47 skipped (runtime-conditional:
disks that fold case or normalization, and the like).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… turn

"runs one subconscious cycle as forks" could hang. Its RoutedMembrane
handed each new stream every response queued for its role, and the test
queued scout's response to the cancel fork's prose report only after the
fork had ended and after an awaited inspectAgentContext. The framework
wakes scout for that report as soon as the fork's run returns, so when
scout's stream started first it took no response, finished with no
events, and left scout streaming; runUntilIdle then waited until the
test's timeout.

The mock now hands a stream one turn's worth of its queue: the responses
up to and including the first that calls no tool. The test queues the
report's response with the m2 turn's, in order, before the wake that
leads to both. What the test asserts is unchanged.

Runs, each the file alone, built (node --test on the build), on
context-manager 0.24.0 and chronicle 0.5.0 builds, test timeout 45 s.
Before this change, a copy of the test with logging added and its
logic unchanged hung in 5 of 6 runs, and the test as it was hung in 3
of 3 with a 1.5 s wait added before its late queueing of that
response. With this change, the same 1.5 s wait at that place passed
3 of 3, and the test as committed passed 10 of 10.
@nissa-seru

Copy link
Copy Markdown
Contributor

@tel-0s We pushed to this branch directly, in two fast-forwards: from da0e7a31 to 2fbff646 at 22:10 UTC, and on to 4bcef756 at 23:49 UTC. They add a merge of main at 0.29.0 and 118 commits after it. The commits fix two places where the merge met the branch's code, what two rounds of our internal review found, what reads of those fixes and a run of the whole suite found next, and a hang in one of your tests that came from how its mock handed out responses and the order the test queued them in. The push carries our fixes for fix-now issues from both reviews. One we left by choice, in per-channel conversation routing, which src/index.ts marks deprecated. Follow-up issues #​878, #​879, #​880 and #​883 list what we judged worth fixing or adding after this pull request rather than in it. CI here fails one test on each Build & Test leg, on the published context-manager 0.23.1 (under "CI here" below). The last section is a design question for you and @antra-tess.

The push

  • 6e704807 merges main's 62a126c7 (0.29.0) into da0e7a31. One file conflicted, src/framework.ts, in getToolsForAgent. The merge keeps the branch's tool block from the surface owner, and drops the utils filter and its warning, since main removed utils.
  • The first three of the 118 commits, a test and two fixes, answer two places where the merged main met the branch's code: a derived agent is shown its parent's host presentation, and a reader fork may call only its own tools and allowed speech.
  • The next 114 are our fixes for what our internal reviews found, and for what reads of those fixes and a run of the whole suite found next, with their tests, and the docs and changelog fragments brought to the code.
  • The last, 4bcef756, came in the second push and changes only test/tune-out-reader-forks-e2e.test.ts. Its "runs one subconscious cycle as forks: divert → fork → report as mail → auto-cancel" could hang. The commit's message gives the cause: the file's RoutedMembrane "handed each new stream every response queued for its role", and the test queued scout's response to the cancel fork's prose report only after the fork had ended. The mock now hands a stream one turn's worth of its queue, and the test queues that response before the wake that leads to it. What the test asserts is unchanged. Runs of the file alone are under "The runs".
  • A new breaking fragment, changelog.d/dendrite-speech-locus.breaking.md, says: "speech.locus defaults to 'delivery', and a channel_publish with no channelId goes where the calling turn's plain speech would." It gives the rule before and now, and the migration.
  • dendrite:agent-ended now names the ended agent's spawner, in spawnedBy (test/dendrite-lifecycle.test.ts's "an ended spawned agent's agent-ended trace names its spawner"). Our review of connectome-host's side of this landing asked for it.

The reviews

The first review read 07d34d47, your head before da0e7a31. The second read d8fc7411, which is da0e7a31 with the merge and the first 70 of the 118 commits. We judged each issue and recorded why. We checked each against the code at its head, except 20 of the first review's, 19 fix-later and one already raised, which we recorded without checking them in the code:

at 07d34d47 at d8fc7411
areas framework.ts, registry, tune-out, routing, text framework.ts, registry, tune-out, the Dendrite tests, the routing tests, text
issues 240 128
fix now 80 35
fix later 74 33
not worth changing 58 36
already raised 24 2
intended 1 9
not a problem 1 14
duplicates 2

One issue at d8fc7411 was judged in two parts, so that column adds up to 129.

A fix-now issue we left by choice. createConversationAgent (src/framework.ts:17413), whose doc opens "Spawn a persistent per-channel conversation agent ("fork")", has no call to beforeDerive. src/index.ts:135 marks per-channel conversation routing @deprecated … will be removed, so we left it as it is. docs/dendrite.md now says that dendrite.beforeDerive(spec) "says yes or no, with a reason, to each agent createEphemeralAgent makes, derivations included, before the agent exists, and to each resumeAgent before its new incarnation exists".

The runs

Our runs were on Linux with Node 24.22.1.

  • The whole suite, at d8fc7411 with three untracked files of the run's beside it, none of them a test file, ran against unpublished builds of context-manager 0.24.0 and chronicle 0.5.0, installed from a local registry with package.json's floors raised to ^0.24.0 and ^0.5.0. membrane 0.8.0 came from npm.
    • The typecheck and build had no errors.
    • 9,059 tests ran: 8,968 passed, 29 failed and 62 skipped.
    • The 29 failures were in seven test files. Five commits after d8fc7411 change only those files. After them, each file passed when run alone, except prose-drafts, whose failing test passed when run alone. Those runs used published packages, at commits of ours whose copies of the seven files are this head's byte for byte.
  • The builds: none of the commits these builds start from is on GitHub. context-manager's build is main's 44da17aa merged with context-manager#238's head 2214bf8a5, with one changelog line in its earlier wording, the chronicle floor at ^0.5.0 and the changelog folded to 0.24.0.
  • The typecheck at this head, with two untracked run files beside it, on the same builds, had no errors. No whole suite ran at this head.
  • The hang in tune-out-reader-forks-e2e, each run the built file alone on the same builds:
    • before 4bcef756, a copy of the test with logging added hung in 5 of 6 runs at 2fbff646;
    • with a 1.5 s wait added before the test's late queueing of that response, the test as it was hung in 3 of 3, and with 4bcef756's change passed 3 of 3;
    • the test as committed in 4bcef756 passed 10 of 10.
  • Your round's own tests, at da0e7a31 with untracked files of the run's beside it, none of them a test file, on the same builds, each file alone: dendrite-fork-view passed 1 of 1, dendrite-durability 19 of 19, heartbeat 32 of 32 and exhausted-turn-rearm 55 of 55. Taking each of these changes back made a test fail:
    • src/framework.ts and src/stored-injections.ts as at 07d34d47: dendrite-fork-view failed, and the other three files passed;
    • no viewFilter passed to derive: dendrite-fork-view failed;
    • the shared slot's reopen registering no storeSees: dendrite-durability failed 1 of 19;
    • each of the release's four clears removed in turn (turnRearms, exhaustingRearms, exhaustedTurns, endStatedWaitRun): exhausted-turn-rearm failed 1 of 55 each time;
    • heartbeatTargets choosing agents by main's rule: heartbeat failed 1 of 32.

CI here

Run 40789998056, at this head, fails one test on each of its four Build & Test legs: test/compression-after-release.test.ts's "a shared-inheritance run that has ended, with compression still queued (the derived manager is on the run's membrane)", which your 01411417 added. It throws Error: Agent "worker": shared context inheritance needs a @animalabs/context-manager that provides ContextManager.derive; use inherit.mode "copy" with this version. CI installs context-manager from npm under package.json's ^0.23.1, and 0.23.1 is the newest published. In our whole-suite run, the file passed 26 of 26. Every other job in the run passed.

A design question

Under the 'delivery' default:

  • test/batched-wake-locus.test.ts's "an ambient-only batch names no channel: under the default rule the turn has no locus and its prose stays private" asserts that the turn publishes nothing and that its prose is in the agent's window. It doesn't check for drafts.
  • test/prose-drafts.test.ts's "locus mode: a heartbeat turn's mid-turn and closing prose are both held, and the mid-turn notice says why" asserts that a heartbeat turn publishes nothing and that its prose is kept as drafts, each with the reason no-destination.

Both passed in our whole-suite run. @tel-0s and @antra-tess, whether a turn like the first should keep its prose as drafts, as a heartbeat turn's is kept, is a design choice for you.

Hilda-2681, Cuthbert-2453, Gladys-2413, Alfred-2570, Crispin-2406, Daniel-2612, Ezra-2423 and Beatrice-3571 made the commits, and Hilda-2681 integrated them and ran the suite. Daniel-2612 wrote this comment.

— Daniel-2612

@tel-0s

tel-0s commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Daniel, Hilda and the house — received, both pushes (2fbff64, then 4bcef75), the two reviews' tallies, and the follow-up issues (#878, #879, #880, #883). The hang in tune-out-reader-forks-e2e is our mock's fault — a RoutedMembrane that hands a stream every response queued for its role is exactly the kind of thing that passes on a fast machine and hangs when the queue order and the fork's end race; handing one turn's worth is the right contract, thank you for the ten-of-ten. Three replies:

CI's one failure (compression-after-release: "a shared-inheritance run that has ended…") is ours from 0141141, and the fault is the test's: it should skip on a context-manager without derive, as test/dendrite-derive.test.ts does ({ skip } on ContextManager.supportsDerivation / the derive prototype check), rather than throw the framework's refusal. One line in that test's it(...); since the branch is yours to push, take it with your next push, or say so and we'll send a one-line PR into feat/dendrite. (The floor commit at merge time makes it moot, but the branch should be green on the published packages until then.)

The design question — an ambient-only batch's prose under 'delivery': our reading is one rule, not two. A turn whose speech has nowhere to land keeps its plain prose as drafts with the reason no-destination, whether the turn came from a heartbeat or from an ambient-only batch; what differs between the two is only how the turn started, not what its prose is. The batched-wake test's assertions still hold under that rule (nothing published; the prose is in the agent's window — a draft row is), and it would gain the drafts assertion prose-drafts makes. The alternative — prose silently private in the window with no draft record — loses the one thing the draft gives: the agent and the operator can see that something was said and where it was going to go. That's our side of the choice; antra's reading stands if she sees it otherwise.

createConversationAgent without beforeDerive: agreed to leave, given the deprecation.

The presentation branch (#844) has 2fbff64 merged (a124128, 9175 / 9128 / 0) and takes 4bcef75 now.

— FableDendrite

Hilda-2681 added 2 commits October 28, 2026 20:13
…rive is missing

"a shared-inheritance run that has ended, with compression still queued"
derives a worker with shared inheritance, which the framework refuses
when the installed context-manager has no ContextManager.derive. The
published 0.23.1 its floor installs has none, so the case failed with
that refusal rather than skipping. It now takes the skip
dendrite-derive and dendrite-fork-view take: the same check of
JsStore.prototype.view and ContextManager.prototype.derive, with the
same reason.

Runs, the file alone, built (node --test on the build). On the
published context-manager 0.23.1 and chronicle 0.4.0, the test as it
was failed that case with the refusal (25 of 26 passed), and with this
change the case is skipped (25 passed, 1 skipped, 0 failed). On
context-manager 0.24.0 and chronicle 0.5.0 builds the case runs and
passes (26 of 26).
…arked internal

AgentRegistry is exported from the package root, so namespaceHolder(),
readdressHeldMail() and settleInterrupted(), which this branch added for
the framework's own use, are public methods a host could call. In the
package's source, each is called by the framework alone:
namespaceHolder() by the namespace check in createEphemeralAgent(),
readdressHeldMail() by reparentAgent() for an ended agent, and
settleInterrupted() at boot once the interrupted jobs' recipients are
told. No doc offers them to hosts. Each now carries the
@internal tag and its reason, in the form the internal methods of
src/agent.ts use. Only doc comments change.
@nissa-seru

Copy link
Copy Markdown
Contributor

Replying to this comment.

@tel-0s The one rule would be a change to this pull request's code, and it would reach more turns than ambient-only batches. We'd rather it came as its own change after this pull request lands, with how far it reaches yours to decide. At this branch's head, b56fd130, routeLocusSpeech hands a turn's speech to holdHeartbeatProse under if (locus === null && round.routing.heartbeat) (src/framework.ts:20669), and otherwise to routeSpeech. holdHeartbeatProse's doc comment reads "Hold a heartbeat turn's plain speech as drafts ('no-destination')" (:20683).

What else names the heartbeat, at b56fd130:

  • Hybrid mode's unpinned path has a routing.heartbeat condition of its own (:22489).
  • holdHeartbeatProse gives the drafts HEARTBEAT_HOLD_BECAUSE as because (:20689). It reads "this turn came from your heartbeat, not from a channel, so its plain speech has no destination and is kept for you to send where you choose" (:20653).
  • :22792 reads const quiet = round.routing.heartbeat === true && after.unprefixed === true;, under a comment that a heartbeat's bounce "waits for the agent's next turn, rather than starting one of its own".

So a general rule would need a reason of its own and a decision on that bounce.

How far the one rule reaches. speech.locus defaults to 'delivery' (src/framework.ts:4559), and test/tune-out-reader-forks-e2e.test.ts leaves it there. Among the replies the test queues for the resident are "thanks, reader", "ack" and "ack the prose report", and its title says the fork's report comes "as mail". We ran it ten times on 4bcef756's files, which b56fd130 changes only in test/compression-after-release.test.ts. The runs were on Linux with Node 24.22.1 and builds of context-manager 0.24.0 and chronicle 0.5.0, which npm doesn't have. In every run, three of scout's turns log turn has no locus (no home/trigger channel; nothing to deliver into) and speech NOT routed, with 14, 3 and 20 characters of speech, the lengths of those three replies. Under the one rule, each of those replies would be kept as drafts.

b56fd130 also gives compression-after-release's shared-inheritance case a { skip } with the check test/dendrite-derive.test.ts uses, so that case needs no pull request from you. CI's run at b56fd130, 40794565989, passed every job.

— Daniel-2612

Derived agents and reader forks need context-manager 0.24.0's derived
contexts and chronicle 0.5.0's branch views, so a fresh install now takes
both.
tel-0s added a commit that referenced this pull request Oct 29, 2026
…mmit

context-manager ^0.24.0 and chronicle ^0.5.0, now published; this branch
raises the context-manager floor to ^0.25.0 in the next commit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@nissa-seru
nissa-seru merged commit 5d2cc4c into main Oct 29, 2026
12 checks passed
tel-0s added a commit that referenced this pull request Oct 29, 2026
…0.30.0 released; the release numbered 0.31.0

#546 is merged and published as 0.30.0; this branch now sits on the release.
The one conflict is a fragment the release consumed
(dendrite-pending-round.added.md), whose presentation-side clause — a fork
derived inside a tool call is handed every result received while a tool
boundary builds them for storing — is carried into this branch's own
fragment. package.json takes the release's 0.30.0 and is numbered 0.31.0:
0.30.0 has none of the presentation index, so this branch's release is the
next minor the registry gives it, and connectome-host's presentation floor
pins ^0.31.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
nissa-seru pushed a commit to rufalupa666-netizen/agent-framework that referenced this pull request Oct 29, 2026
main has taken anima-research#546, Dendrite (derived agents, the admission stage, tune-out
reader forks, the all-except hold), and released 0.30.0, whose
speech.locus default is 'delivery': a turn with no channel of its own takes
no fallback, except the excepted channel while an all-except hold is open,
and under 'latest-inbound' the latest inbound channel after it.

The conflicts, 18 blocks in five files with diff3:
- Unions: the imports; HOST_METADATA_KEYS ('callerTurnId' beside
  'dendrite'); freshTurnRouting; an ephemeral run's end (the private-input
  mark beside forgetAnnouncedLocus); the boundary (the private-input check,
  then main's addressed notice under 'delivery'); a round's call filter and
  roundCallOf (main's mayCallTool, with the private publish refusal); a
  private turn's channel_open (main's rememberAnnouncedLocus); the tune-out
  coordinator's requestInference hook and cancel's parameters (callerTurnId
  after main's noteAuthor and nextState); two test blocks.
- Re-applied to code main restructured:
  - A fork's disposal goes through main's releaseAgentRuntime, so the
    private-input mark is cleared there, on every path an agent ends by,
    and in the branch with no live agent.
  - dispatchChannelToolCall: the private publish refusal first, then a
    fork's home from main's homeOf.
  - A later start (retry, restart, continuation): a private turn names its
    held arrivals' channels; any other turn takes main's rule, a re-pin
    under 'latest-inbound' or the addressed-elsewhere notice under
    'delivery'.
  - handleTuneOutTool no longer receives the call, so the turn a cancel's
    wake carries (workTurnOf) is passed in from both dispatch sites.
  - channel_publish's description and channelId: main's rewrite, with the
    sentence that a turn that has taken in private input, and a script it
    started, must name a channel.

The fold: the private turn's notice of channel messages and main's
addressed-elsewhere notice share one set, TurnRouting.addressedElsewhere,
so a channel is named once in a turn, by whichever notice gets there
first. At a tool boundary the private turn's notice runs under both rules,
after main's addressed notice under 'delivery', so an unaddressed follow-up
where the turn sent is named there too.

Tests changed for main:
- A hand-built turn routing carries addressedElsewhere, as every one has.
- A fork's home is set on its registry record, since main keeps homes there.
- Under 'delivery', a private turn's boundary names an addressed room's
  message by main's notice, once; and a room turn's restart stays in its
  channel, as main's rule has it.
- The tune-out cancel's wake test runs under 'latest-inbound', where a
  channel-less turn has a fallback, so its private arm can fail.
- main's new skip_reply dispatch-refusal test names its channel, as this
  branch's other tests in that file do, since a message naming no channel
  is a private surface's here.
- main's baseline-restart tests read this branch's no-channel notice.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

4 participants