Review round three found four seam defects:
- A watch() disposer only removed the observer from the set; an invocation
already chained onto the watcher tail still ran after disposal. Watchers
now carry an active flag checked when a queued invocation would start,
and the service dispose drain awaits started invocations (pendingTails)
beside the write queues, so disposal is quiescent.
- The settings/updated manual fan-out caught only synchronous throws; an
async listener rejection escaped as an unhandled rejection. Thenable
returns are now contained through the shared listener diagnostic, and
the event contract documents that the INVARIANT rethrow serves
synchronous listeners only.
- structuredClone admitted Dates, Maps, BigInts, and cycles that YAML/JSON
storage silently distorts on reload (a Date lands as a timestamp string,
a Map as a plain map, a BigInt as a number). The write snapshot is now a
single-pass cloneJsonShaped walk that rejects non-JSON values with their
path before anything persists.
- mergeLayers' per-entry undefined guard became dead code once the clone
strips undefined entries at the boundary; removed, with the sparse-patch
contract restated at its enforcement point.
Both files fit their ceilings on each parent; the merge union of this
branch's settings rows with master's typert row and source-launch rewrite
overflows by 6 and 2 words. Every added row is a fixed-format layout or
package-table entry with nothing to relocate, so the ceilings move to the
union size.
`isInheritedSeq` had no production caller — only its own tests — so it was
a public core export shaped by nothing. A bracket owner reads the boundary
positionally; the helper belongs with the compaction seam, where a real
consumer decides its signature.
Also condense the `session/inherited` and `lastActivityTime` docs.
A plugin owning a standalone open/close bracket cannot tell a dead marker
from a live one: an unmatched `compact/start` reads identically whether the
previous writer died mid-compaction or a compaction is running now.
`Session.firstLiveSeq` already holds that answer exactly, but only in memory.
Append the log-only `session/inherited` event at that seq from the seeded
constructor — the single waist all six seeded-start paths pass through
(resume, configured startup on a persisted id, `sessions.fork()`, a subagent
fork child, `adopt()`'s live prefix, and a bare seeded `create`). Read it
through the new `isInheritedSeq(events, seq)`.
The constructor placement means persistence needs no changes: the marker is
already in `events` when a backend captures the creation seed, so it rides
the ordinary seed path with no load-time write. It also covers fork, where
the inherited bracket's owner may still be running — the case a
persistence-layer boundary could not reach.
Activity ordering excludes the boundary through `lastActivityTime()`, since
lazy resume makes browsing a pickup and the three call sites would otherwise
float every opened session to the top of a picker or list.
The seat's height publisher needs a ResizeObserver stub in every spec that
renders ConversationRoot (jsdom has none), and the two branch-tail StatsLine
assertions move to the grouped detail-row output.