The regression for `PresetTree.write` lived two layers up, so this layer's own
assertion could not fail: it checked the file after an ordinary teardown, and
the Loader's unload listener returns early when the whole tree is being
disposed, so the override never ran. Moves the self-disposing-row test down to
the layer that adds the override, in a temp root so a committed fixture cannot
be damaged by the run that proves the bug.
The package landed here with one store spec; the rest of its tests were written
three layers up, so this layer and the two above it failed the per-file
coverage gate on 191 locations while every test passed.
Adds the node half's invariant companion, the two components, the registration
apply, and the stores' failure paths. `currentValue` drops a `?? ''` that an
empty roster already returned before reaching — the same shape the later layer
arrived at, so the two converge instead of conflicting.
master made `allowParallelInProgress` a required field on dsh-tool-todo, so
every composition must choose it. The base patch was updated with the field,
but the Web surface takes `tool-todo` from the mounted preset instead, and
that row carried no config — so the `standard` preset failed to mount and
every session on the Web surface died at setup.
Also make the preset tree's write test reach the override. Tearing the agent
down stops in the loader's own "tree is being disposed" case before any write;
a live row reconfiguring itself is the trigger that actually gets there.
ChatView's bottom-follow recognized only wheel gestures as reader input,
so touch panning, scrollbar dragging, and keyboard paging could not leave
the bottom of a streaming transcript. Replace the wheel listener with
device-agnostic attribution: a scroll position deviating from the
observed-top ledger of the last delivered or written scrollTop is reader
input. Adds keyboard-paging and touch-style fling e2e scenarios (red under
the old implementation) and the bilingual Agent Note triplet.
Moving `web-agent-presets` to the built lane took its coverage with it, and the
per-file gate named what it had been the only reach for: the session header's
`agentPreset` type check, and the preset subtree's `write()` override. Neither
belonged to a real-composition boot — a header validation case and a subtree
behaviour are package-level facts.
The `write()` test states why the override exists: the inherited method
persists the tree whenever the Loader thinks the config moved, and disposing an
agent disposes its subtree, so inheriting it truncates the shipped preset to
`[]` the first time a session ends.
Six review findings on the select surface, all reachable from the wire:
**Resume read the header, not the log.** The switch was recorded as
`agent-preset/selected` and every projection resolved from it, but `agentFor`
still composed from `inspected.meta.agentPreset` — the value written once at
creation. A blank session that switched and then ran turns came back after a
restart under the ORIGINAL preset, restoring that history under the tool set it
was not produced with, which is the mismatch this feature exists to prevent.
`inspected` already carries the events.
**Cold summaries dropped the preset entirely.** `summarizeCold` hand-copied
three header fields and omitted the fourth, so a restored session reported no
preset and the picker showed the deployment default. It now uses the same
projection the attached path does.
**`select` had no gate.** Two concurrent selects both passed the blank check;
the second `unmountPresetFor` then found no record, because the first had
already removed it, and both mounts installed into one agent layer. Selects on
one session now queue, and the blank check is re-read inside the queue. This is
not turn admission — a `session.prompt` racing a switch is the agent loop's to
reserve — but it closes the select-versus-select tear-down.
**A same-id restore was skipped.** The roster is a live directory, so "the same
inputs that worked a moment ago" does not hold: a changed file is exactly how a
same-id reselect fails, and skipping the restore left the agent with no
composition at all.
**`writable` was dead state**, initialized true and never set, so the row could
never disable. It now carries `settings.describe`'s bit — a browser that may
not write settings sees the current default and no control, rather than one
whose write answers `settings-not-exposed`.
**`list` was documented as id-ordered.** It is root-precedence order with each
root's own presets sorted, first root to supply an id winning.
The inline comment called every dunder entry "the Python backend's" slot,
but `__debug__` is a CPython compile-time constant, not a seeded slot;
align the comment with the seam declaration, which is the single home for
why each entry is reserved.
- index.ts: RESERVED_ERROR_MEMBERS JSDoc now says `__x__` (non-empty
middle), matching DUNDER_MEMBER and the derivative docs; and
PORTABLE_RESERVED_WORDS is described as covering portable *target*
languages (Python is a later-PR backend, not yet shipped).
- types.ts + type-equiv docs: CodeBindingNamespace.global points to
RESERVED_BINDING_GLOBALS by name with an `e.g.` sample rather than
enumerating the set (single home), no longer implying `__debug__` is a
seeded slot.
- Agent Note: separate `__debug__` (CPython compile-time constant) from
the seeded bootstrap globals.
- README (en/zh): document the four exported exclusion sets and the
portable identifier/naming contract — no cross-package link needed.
- Regenerate cordis services catalog for the shifted source line.
- Agent Note: correct the stale worker-adoption paragraph — the worker
consumes the seam constants directly by name (no RESERVED_WORDS /
RESERVED_ERROR_PROPERTIES re-alias) — and describe DUNDER_MEMBER as
`__x__` (non-empty middle).
- types.ts: document RESERVED_BINDING_GLOBALS on CodeBindingNamespace.global
(names like `__dsh_main__` pass the identifier rule but are still refused)
and the non-empty-middle dunder rule on memberNameProperty; propagate to
the type-equiv derivative docs (both languages, re-recorded).
`____` has an empty middle between the two `__` pairs and does not match
`/^__.+__$/`; assert that (not a match) and add `__x__` as the shortest
real dunder form that does.