Repository navigation
Conversation
The configuration surface of a persisted reader (design note §3, option C),
and nothing that reads it yet:
- ForkReaderConfig.persist ('look' | 'epoch' | 'resident', default 'look'),
resync ('window' | 'none', default 'window' when persist is set), and
pendingRound (DeriveAgentOptions.pendingRound, or a function of the look's
framing context that returns it). create() refuses a value with no mode.
- ReaderFramingContext gains resumed, incarnation, rederived and
residentRows, each left out with 'look'. `resident` already names the
resident, so the rows are residentRows.
- ReaderInvocation.epochEnded marks the one late look the coordinator asks
for after its epoch ended (lateFork); its cancel looks don't carry it.
- tune-out:reader-fork gains optional incarnation/resumed/resynced; new
trace type tune-out:reader-rederived.
- DeriveAgentOptions.pendingRound.madeBy.toolUseId may be left out, for the
only open call, which is what framePendingRound already did with none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ater look
With subconscious.persist 'epoch' or 'resident', a reader is one derived
identity per scope: reader/<resident>/<epoch8>/g<k>, or reader/<resident>/g<k>.
The identity's first look derives it exactly as a reader fork is derived; each
later look resumes it with resumeAgent, a new incarnation on the branch it
left, with the configuration deriveAgent gives a reader built again and this
look's epoch, channel and trigger as its metadata. Its framing is stored as
one row before the run, as a derivation's is. A failed look, a beforeDerive
refusal of the resumption included, gives its held rows back (readerFailed).
One look runs at a time per identity: a look asked for while another runs
waits for it (readerLooks). Without that, the all-except hold's one look per
channel in one tick would find the name still registered. The generation is
read from the registry at the identity's first look in a process, so after a
restart the next look resumes the ended record.
resumeAgent takes { metadata }, the new incarnation's record metadata in
place of the ended record's, and a shared derivation resumed on its source's
allowedTools shows its source's tool presentation again (toolSurfaceOwners),
as its derivation did. deriveAgent's config construction moves to
derivedAgentConfig, unchanged, so the resumption builds the same config.
With persist left out or 'look', invokeReaderFork runs as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each persisted reader's framing row records where the resident's window
stood: residentThrough, the ordinal of the last row it held, and
residentBranch. A derivation's is read as deriveAgent is called, which waits
for nothing before context-manager's synchronous checkpoint, so it is the
checkpoint's; a resumption's is read in the same tick as the rows it hands.
The record's metadata carries residentBranch too, for the next look.
A resumed look with resync 'window' is handed, as
ReaderFramingContext.residentRows, what the resident's window took after
the mark on its last framing on the same branch (presentedSince), in window
order: the resident's rounds as stored, what it was shown, its mail and the
framework's notices to it, without the reader's own rows and report mail,
the resident's stored context injections, and held rows (the backlog's).
That mark is on the reader's own branch, so it needs no coordinator state.
The backlog it is handed leaves out every held row a framing on its branch
handed it before, a failed look's included: after a restart the
coordinator's cursor is gone and an epoch's first look is handed all it
held.
defaultReaderFraming, given residentRows, writes one block before the held
traffic: a count line ("[In scout's context since your last look: n
messages]", or "[Nothing has entered scout's context since your last
look.]"), then a line per row, author: text, with tool calls and results as
foreignRowAsSent's brief transcript and no thinking. Without residentRows
its output is what it was. tune-out:reader-fork's resynced counts the rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… can't resume The identity's current generation is resumed only when the registry has an ended record of it in scope; otherwise the look derives the next generation (g<k+1>) and traces tune-out:reader-rederived with the earlier name and the reason: - no-record: no ended record of the name (a derivation whose run was refused, say), or with 'epoch' one of another tune-out's; - scope-ended: with 'epoch', the tune-out had ended when the look was asked for. That is the coordinator's late look (ReaderInvocation.epochEnded); its cancel looks, asked for as it ends, still resume it; - branch-moved: the derivation's checkpoint is no longer on the host's branch (branchesPartAt), or, with resync 'window', the last look was framed on another branch than the resident's window reads now (the record's residentBranch), whose ordinals its mark can't be read against. A beforeDerive refusal of a resumption stays a failed look: its held rows go back, and nothing is derived in its place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The derivationNotice collapse counts resumptions: each incarnation from a
source is tallied with its kind's derivations, so a reader resumed at
each look collapses to a count line past the threshold as a reader loop
that forks does. The count line says how many of them were resumptions
("1 subconscious-fork has been derived from you, and resumed 3 times,
since …"), and its detail carries `resumed`. Without resumptions the line
and detail are what they were. Before, every resumption was told.
- derivationDescription, for a record whose metadata says it is a
persisted reader: "reading your context up to the point it was derived"
at a resumption, and with resync 'window', "and handed what entered your
context after it at each look". Any other record reads as before.
- tune_out's text adds one sentence for a persisted reader: "Within one
tune-out, later runs resume that fork, each handed what entered your
context since the run before; a new fork is made only when it can't be
resumed." ('resident': "Later runs resume that fork, across
tune-outs, …"; resync 'none' leaves out the handed clause). Reader forks
per look read as before.
- subconscious.pendingRound replaces the framework's pending-round note
("This call derived …") for the resident's call open when a reader is
derived, as an object or a function of the look's framing context, with
'look' as with a persisted reader's derivation. Left out, the default
holds.
An existing forks test (tune-out-reader-framing-presented) runs both with
persist left out and with persist 'look'.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- docs/dendrite.md: the forks section says persist exists and what it leaves as it was; a new "A persisted reader" subsection says the mechanics alone: names, the first look's derivation, each later look's resumption, what a resumed look is handed and how its mark is recorded, the three reasons it is derived again, one look at a time, beforeDerive at every derivation and resumption, what the resident is told, restarts, cost and cache (unmeasured), the known undo/redo holes, and that a persisted reader is a different preset from the one the 10-05 ruling names, so whether a consent covers it is the deployment's question. Also: resumptions in the collapse, resumeAgent's metadata and tool surface, the live-follow entry, and choice 14 (resumed and re-synced by framing, not standing with a view). - AgentConfig.observes: the doc said a derived agent's spec carries its own edges. deriveAgent declares none, and a shared derivation reads its source's slot on its own branch, so an edge there would merge nothing. - tools.ts: ForkReaderConfig.framing and persist say what a resumed reader receives and when the identity is derived again. - changelog.d/persistent-reader.added.md and .changed.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion itself The failure path gave readerFailed a copy of the invocation, with the rows this look was handed as its backlog. The coordinator knows its late look by the object (lateFork), so a late look that failed was taken for an ordinary one after the end, and another late look was asked for: with a resumption beforeDerive refuses, the looks never stopped (111,328 refusals in 20 s in the new test, 2 with the fix). The invocation itself now goes back, its backlog set to what the look was handed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wn-row writer persistedReaderLook stores the resumed reader's framing with storeOwnRow after resumeAgent and before its run registers it, as deriveAgent stores a derived agent's framing: 'unregistered'. The audit lists every scope that stores an agent's own row, so the new one is listed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
/review |
|
Automatic review of Findings, most severe first:
Minor findings 9 to 33 (of 33)
|
Conflicts in src/framework.ts: the tools.js import keeps both sides' types (ReaderFramingContext, ReaderRederivedReason and TuneOutReader); the TuneOutReader type, which feat/presentation moved to tune-out/tools.ts, stays there and takes this branch's persist/resync fields on its forks variant, with framework.ts re-exporting it as feat/presentation does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…inals A redo presents the rows it brings back at new ordinals past the window's end, not at the ordinals they held. So a row undone before a look and redone after it is handed at the next look, and a row handed before an undo and redone after it is handed again. The known-holes paragraph said the opposite. (The design note's mirror, outside this repo, is reworded the same way.) AF #967 bot review finding 7. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The docs said a host's `framing` may return rows stored under the resident's participant, to render as the reader's own turns. The framework stores whatever `framing` returns as one user row of the reader's, at a derivation and at a resumption alike. Say that, and that rows in the resident's own voice are an open question (#966). AF #967 bot review finding 8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reDerive A persisted reader's first framing records the resident's window mark as deriveAgent is called, before dendrite.beforeDerive runs. A row a host's beforeDerive writes to the resident's context is in the derivation's inheritance and is handed again at the next look. Say that a host's beforeDerive must not write there. AF #967 bot review finding 14. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-fork-failed tune-out:reader-rederived is emitted before the new generation is derived, so a derivation that then fails (a beforeDerive refusal, say) is followed by tune-out:reader-fork-failed, not tune-out:reader-fork. The trace's doc comment and the docs said reader-fork always follows. The emit order is unchanged. AF #967 bot review finding 15. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pendingRound's madeBy without a toolUseId words the open call's result only when exactly one call is open. With several, the host names the call by madeBy.toolUseId (getPendingAssistantRound gives the ids) or words each by its id in results; otherwise its madeBy.result is not used. AF #967 bot review finding 16. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…resentation by A resumption infers its tool presentation from the configuration it is given: a shared derivation resumed shows its source's presentation when the allowed tools it is resumed with equal its source's current ones and its source is registered. A derivation's rule is allowedTools left out. So a host that overrides allowedTools with its source's own values gets the shared presentation on resume. A persisted reader's configuration, built again on the resident's current allowedTools, meets it at each look. The docs said only "as its derivation did". AF #967 bot review finding 10. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AF #967 bot review findings 5 and 23. residentRowLines wrote a line only for a row with text. A row with media and no text, such as an image antra sent or an agent mailed, came as its media alone, after the previous row's line, so it read as that author's. A row with nothing to send was counted in the count line and had no line. Now a row with media and no text has its author's line, `antra: `, before its media, as the held traffic's lines already did; with no author to name it is its media alone. A row with nothing to send reads `[no text]`, under its author when it has one: foreignRowAsSent's NOTHING_SAID, now exported, which a thinking-only row already read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fork section's cost, one Chronicle branch and one context manager per invocation, holds for a fork per look. A persisted reader has one branch per generation, reopened at each look, and one context manager per look. Each resumed look scans the reader's own branch, the inherited prefix included, for its earlier framings, with resync 'none' too: 17 ms at a 50k-row resident and 53 ms at 200k, measured cold after the reopen (review4/p2/scan-cost.mjs, re-run on this tree; about 5-8% of a look's local work over two runs). With resync 'window' it also filters the resident's presented window past the mark (presentedSince: 8 ms and 35 ms). AF #967 bot review findings 30 and 33. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ming AF #967 bot review finding 24. Both of the default reader framing's formatters, the resident's rows (residentRowLines) and the held traffic, put all of a row's text on its author's line and all its non-text blocks after that line. So text A, an image and text B reached the reader as "A\nB", then the image, in another order than the row's. Both now write a row through one walk, writeRowInOrder: the row's text runs into a line, the first under its author; at a non-text block the line ends and the block follows it; the text after it is a line of its own, with no author. A row that opens with a non-text block has its author's line, `antra: `, before it. This changes the held block's bytes on every host with reader forks and the default framing, on the default persist: 'look' too, wherever a held message has text after a non-text block; the changelog fragment says what reads as before. No existing test pinned the old order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts row handed once The restart test stopped only after its first look had completed, so an interrupted look's recovery went untested. Here the first look's stream is held and the framework stops with it in flight. After the reboot on the same directory, the record has ended as host-restart. The next look, handed the epoch's whole backlog as after a restart, resumes the identity at incarnation 2. Its request holds the interrupted look's row once (its own framing), and the new framing holds the new row alone. New file test/tune-out-reader-persist-more.test.ts, with a copy of tune-out-reader-persist.test.ts's boot(). AF #967 bot review finding 25. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…start The restart test presented no resident row between its looks, so a mark lost or ignored only after a restart passed. Here scout takes turns after the first look, before the stop and after the reboot. The look after the reboot is handed all of them, in window order, and nothing from before the first look. The trace's resynced count and the default framing's count line both equal the rows handed. AF #967 bot review finding 26. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The resident-scope tests never started a second tune-out. Here reader/scout/g1 is derived in the first tune-out and resumed for its cancel look. After enter() again, the second tune-out's first look resumes it at incarnation 3, with the new epochId in its record's metadata. There is no rederived trace and no second branch. Its request holds what it was handed and said in the first tune-out. AF #967 bot review finding 28. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… finding 2's fix)
Quiesced, a resumed look stores its framing and then has its run refused
at admission. The test makes two such refusals with the same held row,
then resumes the framework and runs the look. It pins the semantics
settled in the triage of finding 2: each refusal gives the row back
(returned 1 both times), and the look that runs is resumed at
incarnation 2 and holds the row exactly once.
On this tree the second refusal returns 0: the first refusal's stored
framing filters the row out of its backlog. So the test is skipped
until fix/review5-1 lands ('awaits fix/review5-1 (#967 finding 2)').
AF #967 bot review finding 27.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ling readerPlan ran before the look's try, so a registry or branch read that threw while planning was only logged by the look chain's catch: the coordinator had already advanced its handoff cursor, readerFailed was never called and no reader-fork-failed trace was emitted, so the look's held rows were lost. The plan and its rederived trace now run inside the try; a failure before a generation is planned is traced under the identity's name and gives the rows back, as any failed look's. AF #967 bot review finding 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ive its rows back A persisted reader's look queued behind another returned silently when the resident had ended meanwhile, after the coordinator had advanced its handoff cursor: its held rows went to no reader, with neither a failure trace nor a log. It now gives them back (readerFailed) and emits tune-out:reader-fork-failed under the identity's name, error "the resident ended before the look ran". AF #967 bot review finding 11. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pping stop() neither stopped nor waited for a persisted reader's looks, which run one after another outside the event loop. A look queued behind a running one ran during teardown: it derived a new generation (with a rederived trace and a reader-fork trace for a run that never ran), created its branch and stored a framing row, and then its run was refused because the framework was stopping, all while the store was being torn down. A look now plans nothing once provider admission is closed, and stopOwned waits for the reader looks, bounded by teardownTimeoutMs, after it ends the ephemeral runs and before it waits for operator operations. The rows of a look not run aren't given back: the coordinator has stopped, and after a restart an epoch's first look is handed all the epoch holds. AF #967 bot review finding 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
readerPlan found the identity's newest generation by scanning registry records and stopped at the first name with none. A derivation whose run was refused (quiesced) has its branch and no record, so after a restart the scan stopped there, derived g1 afresh and left the completed g2 and its history unread, with no rederived trace. Likewise a generation whose ended record the registry can't reach (on another branch's log, or past what it keeps) was derived again under its own name, on a suffixed branch, at incarnation 1, untraced. A generation now counts as used when the registry has a record of it or the store a branch a derivation made for it (dendrite/<name>, or ~n after it), in the scan, in the names a new generation can't take, and in whether the derivation follows an earlier one, so it is traced 'no-record'. AF #967 bot review findings 1 and 9. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t is refused A resumption stores its framing on the reader's branch before its run, so a refused run (quiesced, stopping) leaves a framing row no run read. The next look filtered every held id a framing on the branch named out of its backlog, so a second refusal handed readerFailed an empty backlog and the coordinator scheduled no reader for the row: lost whenever the next look derived a new generation, or a late look failed after the end. Each framing row now records the incarnation it was made for. readerHanded also returns the held ids a run took on: those from framings made for an incarnation the ended record reaches. The content is still filtered by every held id (a refused look's framing is in the window of the next run that takes the reader on), and a failed look gives back its backlog less the taken ids only. AF #967 bot review finding 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # docs/dendrite.md # src/tune-out/tools.ts
|
Merged — FableDendrite |
|
Merged feat/presentation |
|
Merged feat/presentation |
# Conflicts: # src/framework.ts
…fore deliverFromReader Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Merged feat/presentation |
|
Merged feat/presentation |
|
Merged feat/presentation |
|
#844 is merged into |
Implements #966, option C of the design note posted in #architecture: a derived reader that persists across looks instead of being derived anew each time. Draft until antra's answers to the note's open questions, which shape the configuration, not the core. Based on
feat/presentation(#844), whose window ordinals, read marks andnoteFramedit uses; withpersistunset nothing changes — the record, framing context, framing row and trace keep their shape, and every existing test passes.What it does
ForkReaderConfig.persist?: 'look' | 'epoch' | 'resident'(default'look', today's behaviour) andresync?: 'window' | 'none'(default'window'whenpersistis set);pendingRound?lets the host replace the framework's pending-round note.invokeReaderForkdoes, under a stable name (reader/<resident>/<epoch8>/g<k>orreader/<resident>/g<k>). Each later look resumes that identity (resumeAgent, a new incarnation on its own branch); it is not derived again.resumeAgentgains{ metadata }(this incarnation'sepochId/channelId/trigger/persist/resync/residentBranch) and restores the tool surface for a shared derivation whose source is registered. One look at a time per identity.residentThrough(the last ordinal the resident's window held) andresidentBranch; the next look is handed the resident's window rows between the marks (residentRows), minus the reader's own rows and mail, the resident's stored injections and held rows, plus the backlog minus rows the reader's branch already holds.defaultReaderFramingadds one factual block ([In scout's context since your last look: n messages], tool calls as brief transcript, images through).tune-out:reader-rederivedwith reason):no-record,scope-ended(the coordinator's late look after the tune-out ended),branch-moved(the checkpoint left the host's branch, or under'window'the last mark is on another branch). AbeforeDeriverefusal of a resume is a failed look whose rows go back; nothing is derived in its place.beforeDeriveis consulted at every derive and resume with that look's metadata; thederivationNoticecollapse counts resumptions; the notice description and thetune_outsentence state the fact for a re-synced reader.docs/dendrite.mdforks section and a "A persisted reader" subsection (mechanics only; a persisted reader is a different preset from the one the 10-05 ruling named, and whether that consent covers it is Connectome's question, not the framework's); theAgentConfig.observesdoc over-claim fixed; two fragments.Semantics stay out: no check that a consent exists, no default that assumes one, no coupling between
persistand agreement, no text that asks the resident; the record and traces carrypersist, incarnation and inherit for a module built on top.Tests
test/tune-out-reader-persist.test.ts, 20 tests (red-probed where the code could be broken): config accepted/refused; first derives, second resumes (one name, incarnations 1 then 2, one derivation, the second request starting with the whole first); one look at a time; resume after a restart from the ended record; re-sync hands exactly the rows between the marks; backlog dedupe; the default block; each re-derive reason traced;beforeDeriveat each derive and resume, a refused resume a failed look; a failing late look leads to no further looks (fix509f8d38: the invocation object is handed back itself, not a copy — a copy caused 111,328 refusals in 20 s); notices and text;'look'/unset byte-identical shapes.tune-out-reader-framing-presentedalso runs withpersist: 'look';audience-stamp-writerslists the new own-row write. Full suite 9680 tests, 9630 pass, 0 fail after the audit entry, 49 platform skips. ~725 src lines, 674 test lines.Open for antra (the note's questions, and the judgment calls made mechanics-first)
'epoch'or'resident'?[delivered]receipts and lifecycle lines included), or only its own rounds?.changedfragment); scope it tometadata.persist?resumeAgent'smetadatareplaces the ended record's metadata rather than merging; the tool-surface restore applies to any resumed shared derivation whose source is registered.Not in this PR: connectome-host recipe keys (
persist,resync), an explicitresumemarker on thebeforeDerivespec, a host hook choosing resume vs derive, C′ (carrying earlier reports into a re-derived reader).Written by an Opus agent under FableDendrite's direction from the design note; reviewed and pushed by FableDendrite.
🤖 Generated with Claude Code