The `host/session-removed` invalidation flipped the owned catalog and
addressed children to `parentAvailable:false`, but a `subagent.list` pull
already in flight was requested before the removal and its ok-response
carries the pre-removal `parentAvailable:true` — the response then
overwrote both the catalog and every addressed child, resurrecting the
writable-editor-against-a-dead-continuation-owner bug the invalidation
closes, with no refresh scheduled to converge afterwards.
Mark the owner stale when a pull is in flight at removal time, so one
trailing refresh runs after the in-flight response settles and the
post-removal host truth lands. Adds a regression test: removal mid-pull,
stale ok response, trailing pull, final state stays unavailable on the
catalog and the addressed child.
A removed session can no longer be the delivery owner of its continuable
children, but the `host/session-removed` handler only reconciled the
removed row's own activity. `parentAvailable` was updated exclusively from
`refreshSubagents` success, and removal schedules no catalog refresh — so
after the parent's Activation detaches, an addressed child kept a writable
editor against a dead continuation owner until an unrelated refresh (or
forever, for a closed menu).
Flip `parentAvailable` to false on the owned catalog and push
`handleSubagentParentAvailable(false)` to every addressed child Session at
removal time, matching the refresh path's notification. New Session
instances already read `parentAvailable` from the catalog, so they inherit
the invalidated state.
Adds a regression test: removing the catalog's owning parent flips the
snapshot's `parentAvailable` and notifies the addressed child instance.
`refreshSubagents` single-flights per catalog owner: a request arriving
while a pull is in flight returns the in-flight promise and is silently
coalesced into it. The in-flight response was requested before the
triggering change, so it can never contain that change — a debounced
membership refresh (50ms after `host/session-added`) firing during a slow
pull therefore lost the new child, and the catalog stayed stale until an
unrelated trigger (reselection, menu reopen, reconnect).
Mark the owner stale on coalescing and re-arm one trailing pull in the
settlement `finally`, so every membership change observed during a pull is
carried by a follow-up refresh exactly once. Bounded: the trailing pull
only runs when a refresh request was actually coalesced, and a new
coalescing during the trailing pull re-marks the same set.
Adds a fake-timer regression test: a `host/session-added` debounce firing
mid-pull yields exactly two `subagent.list` calls and the catalog
eventually contains the new child.
The fork button on a stopped assistant message was inert. Frozen
interrupted nodes carry a flow-ordering seq of turnEnd.seq - 0.9, and
session.fork takes a non-negative integer on the wire, so every such
request was rejected as invalid-params before reaching the host — where
an aborted turn's logged turn/end has always made it forkable.
SessionsService.fork floors atSeq at the wire boundary. Flooring stays
inside the anchor's own turn (every turn opens with turn/start), so the
host's first-turn/end-at-or-after cut still closes on that turn.
Public snapshot state stays in the store engine's plain-data vocabulary
(immer drafts reject Sets without the MapSet plugin, which stays off):
manager/service/contract carry readonly SessionId[] in Host order, and
the tree derivations build their own transient Set — the
expandedProjects pattern. Membership-unchanged installs still keep the
array reference for Object.is short-circuits.
- WorkspaceRegistry.archiveSession no longer wraps persistence-listing
failures as WorkspaceUnknownSessionError: only a definite miss (live
lookup, header index, then a fresh list) maps to session-not-found;
storage faults propagate as internal errors, with a negative test.
- The archived-current sweep moves from the unary path into the
projection: any install path (local echo, another tab's frame, a
reconnect baseline) clears a selection that landed in the archive set.
- An archive set installed while workspace.list is in flight supersedes
the stale baseline's set instead of being rolled back by it.
- The workspace-management e2e anchors the archived row by its session
actions button and asserts the single-stray fixture assumption loudly.
- Drop the stale touchSession rows from the workspace READMEs (the
method was removed with its Agent Note).
WorkspaceListState gains archivedSessionIds (ReadonlySet, replaced only
on membership change), installed as full snapshots from the list
baseline, the unary echo, and the changed frame. Archiving the current
session clears the selection into the New Session view state. Test
doubles (test-runtime, fake APIs, fixture client) follow the widened
IWorkspaces/IApiClient faces.
Move COMPACT_CHECKPOINT_SOURCE and isCompactCheckpointSource into a
cordis-free src/checkpoint.ts leaf, re-exported from the root so every
host-side consumer keeps its import. The client can then type-import the
leaf without reaching dsh-session's root, whose Context merge declares the
host sessions service and collides with the client's -- the dsh-commands/brand
shape. Renaming the plugin id now fails the client typecheck.
Also: keep recoverable summary text when a compact/summary mixes text with
other block types, and capture the seeded-history provenance seqs from the
pushes that produce them instead of deriving them by arithmetic.
Replace the surface-ordered fold with a log-ordered human transcript:
append-origin surface events at their own log positions plus one marker per
landed compaction checkpoint. Command folding, the tool-call index, and the
rev-keyed memo carry over unchanged.
Removes foldDegraded, the padding sentinels, baseSeq, and degradedSeqs() --
they existed only to satisfy the core fold's seq === index assertion. That
also closes the pagination hole A1 exposed: a page can carry a checkpoint
whose shadowed range fell outside the window, and nothing resolves
surfaceOp.start anymore.