Describe async generator yields, key-conflict reads, and macrotask timing as implemented, restore the API controller and mods bridge sentence in the upgrade guide, record the rejected plugin-disposal alternative, and pin the async generator behavior with a test.
Record the deprecated registration and keyed-read compatibility in the retained-reader decision instead of a separate note, and remove the 2026-10-19 compatibility note.
A legacy plugin may store the registration result as a disposer or ignore it,
so a failed registration became an unhandled rejection that ends a dsh process.
The registry now attaches its own handler: once the current turn ends, a
rejection nobody observed through then/catch/finally is logged through the
calling plugin's logger while the plugin stays active, and one whose owner is
no longer loading or active is dropped. Observed rejections are unchanged.
- register() returns the registration's effect disposer with then/catch/finally
attached; the original body moves unchanged into a private acquire(), keeping
synchronous installation and the asynchronous handle handoff.
- Legacy stateOf(session, key) logs one ctx.logger.warn per key per registry;
registration and calls to its result no longer warn.
- Containing an ignored legacy registration failure is deferred and recorded
under Known Limitations.
Move the session projection handles guide from the released v0.2.1-alpha.3
directory to the current v0.2.1-rc.2 directory and fold in the callable
registration and deprecated keyed-read notes, replacing the separate
session-projection-registration guide.
Adding the `ProjectionKeySelection` import to the projection cache moved its
`Config`/`inject` declaration down one line, so the config catalog's source
reference is regenerated. Only the source location changes.
The fold blank matrix counted every band that no row box covered, including
a band whose rows on both sides had already faded to opacity 0. Such a band
shows the same background as those rows. Kept intermediate replies move with
the layout while closing groups stay in place, so that band could exceed the
transient limit in side/composer/steps without any visible difference.
A band now counts when a visible row borders it on either side, with the
viewport top and the composer treated as visible. A synthetic-DOM case keeps
a band beside a visible row failing the measurement.
- Export `ProjectionKeySelection` (the read faces' key selection) and reuse it
from the projection cache instead of restating the union twice. The Cordis
catalog can now resolve the type its generated signatures reference.
- Make `wire.summary` a plain boolean: omitted means `true`, and a key whose
map entry carries the summary mark declares `false` so the runtime key set
and the client view type agree. The type no longer forbids a declaration.
- Describe the mark accurately: it has no runtime representation; `wire.view`
and `viewSchema` are typed with `SessionProjectionViewValue`, which strips it,
so the registration erase only widens the state type.
- Say plainly in the package README, the subsystem page, the client field
comments, and the Agent Note that a list row carries the summary projection
and never the full projection: a connected Session's store may hold more
values, but those are outside the list contract.
- Document the summary vocabulary on the subsystem page with `ts type-equiv`
blocks for the mark, its stripping, the excluded-key test, the summary value
type, and the key selection; split the prose paragraph.
- Revert the behavior-neutral cache-double refactors in three specs, and record
in the Agent Note why no upgrade guide accompanies the wire change and how a
consumer migrates.
The group-tree JSDoc and the ui-chat README stated that every reply stays outside whole-Turn folding and named only the collapse-timing setting. They now distinguish All replies from Final answer only and link the new setting.
The seeded-history check measured the folded final answer 16px below the Turn control. Hidden process rows separate the two, so the answer now keeps the 12px reply gap.
The producer/consumer matrix points at the declaring line of each event. Adding
the `imageLimits` JSDoc and map-entry wrap moved the `api-session/*` declarations
in `packages/api/session-controller/src/types.ts` down by four lines, so the
graph pair is regenerated. Only source locations change.
`imageLimits` is a boot constant every Session shares: the unit stores no
per-Session state (`stateSchema: z.null()`, `apply: state => state`) and its
view returns `ctx.attachments.imageLimits`. A list row therefore repeated one
host-wide capability fact on every row. It now carries the summary mark like
the three log-derived units, so a summary read skips it while an opened Session
still receives it through `session/follow`, the control baseline, and
`session.projections`.
No other logic changes: the registration, its schema, its constant view, and
the host-side prompt validation keep reading `ctx.attachments` directly.
A folded Turn gave its final answer the control's 16px clearance through
compactAnswer and data-turn-process-answer, because hidden process rows
keep the answer from matching the control's adjacent-row rule. With every
reply kept, the final answer no longer follows the control, so that 16px
only widened the gap above it. In both choices the margin also switched
from 12px to 16px when the closing rows became hidden at the end of a
next-message fold, moving the answer down about 4px.
A reply separated from the control by folded rows now keeps the 12px reply
gap in both choices; only a row directly following the control receives
16px, and no reply changes its spacing when a fold finishes.
Settings goldens gain the Replies After Collapse row; folded-Turn aria
goldens for plan review, goal turns, and turn-tail actions include their
intermediate replies. The folded layout checks measure the intermediate
reply that now follows the Turn control at the 12px reply gap.
Whole-Turn folding chose the final answer by position, so a Turn that
continued after answering (a notice reply, record keeping, or a tool call)
hid the real answer with the process. A folded Turn now hides only its
process; every assistant-step reply row stays visible.
Settings -> General -> Replies After Collapse (ui-chat.foldedReplies,
default "all") switches back to the previous final-answer-only rule; the
choice applies immediately and is hidden in Verbose.
The note records why a Session-list row carries a summary key set instead of
every registered client view: the type-only exclusion mark with its
registration interlock, the derived stable key set reused through the existing
key-taking read faces, the nullable-wrapper rule for `todos`, and the estimated
payload effect of omitting `turnOutline`, `sessionStats`, and `todos`. It lists
the rejected alternatives (a summary-specific read face, a hardcoded key list,
post-hoc filtering, opt-in summaries, and lazy per-value reads) and the costs
the decision accepts.
The catalog pages embed the declaring source location of each documented type.
Adding the summary-exclusion import and map-entry JSDoc to
`packages/todo/tool-todo/src/types.ts` moved `TodoItem` and `todo/write` two
lines down, so both catalog languages and their pairing record are regenerated.
No type fingerprint changes: `verify-persistence-changes` still reports 65
roots matching 16 history records.
A second `gen-persistence-catalog` run is required after a schema change: the
page and the machine inventory are generated in one pass, and the page reads
the inventory as it was on disk.
`docs/persistence-schema.json` records the declaring source location of every
persisted type. Adding the summary-exclusion import and map-entry JSDoc to
`packages/todo/tool-todo/src/types.ts` moved two declarations down by two
lines, so the generated catalog is regenerated. No persisted type, field, or
history record changes: `verify-persistence-changes` reports 65 roots matching
16 history records, and the release/format catalogs are unchanged.
A Session-list row no longer carries every registered client view: the three
units whose values a list row never renders (`turnOutline`, `sessionStats`,
and `todos`) are excluded, which removes the whole-log turn outline and the
figure and todo payloads from every row. An opened Session is unchanged —
`session/follow`, the `session/control` baseline, and `session.projections`
still serve every projected key.
The exclusion is one type-level mark plus one registration interlock:
`SessionProjectionSummaryExcluded<T>` marks a map entry, and
`ProjectionDefinition.wire` requires `summary: false` exactly on a marked key
and rejects it everywhere else, so the mark and the declaration cannot drift.
A nullable entry keeps `null` outside the wrapper, so `todos` stays
`TodoItem[] | null`. `summaryKeys()` returns the derived set, stable until a
registration or its disposal changes the key table, and the existing
key-taking read faces (`cachedSnapshot`, `viewCheckpoint`) accept that set
directly, so summaries need no separate read face and no per-read key copy.
`packages/api/session-controller/src/list.ts` passes the set to the live and
cold cached faces; `cachedPredecessorTitle` is untouched.
Client types only: `SessionSummary.projectionValues` narrows to
`SessionProjectionSummaryValues`, `SessionProjectionSnapshot.values` keeps
every view value with the mark stripped, and no wire field or client name
changed. The cache, subsystem page, and package README document the summary
rule and the two read faces.
Set the autonomy ranges to hint 0-10, assist 15-25, pair 30-50, copilot
55-85, and auto 90-100 in the skill and the template level line, and
refill the example accordingly.
Sharpen the stage criteria where trial runs disagreed: grade design by
who supplied the adopted structure, count only the author in the first
four stages, ignore process and ordering instructions, define the main
change, keep review at n/a until a comment is handled, and separate
copilot from pair by whether every non-wording item got a human
response. Clarify the Changes categories, test evidence, disclosure
placement, language, and when to update the description.
Refs #6034
Correct the downlink-write activity contract for compressed frames: a write
that does not complete at once counts, and a frame above the threshold
deflates asynchronously so its completion always counts. The Host compresses
only its own outbound frames; uplink compression remains the peer's choice.
Stabilize the compressed-write liveness test with a 500 ms heartbeat interval
and assert the bounded half: after the downlink stops, missing Pongs still
terminate the socket.
Refs #6476
Start the template's review line as n/a, as the skill requires at PR
creation, and prompt for an Issue reference under Motivation. Read the
skill's stages only from its Stages and criteria section and reject a
stage name that does not pair one English word with a Chinese name.
Point the GIF disclosure row at record-browser-gif instead of copying
part of its list, merge the calibration notes into one TODO, replace the
design skeleton wording, and reduce the root AGENTS.md rule to a pointer.
Refs #6034
Restore the `ws` default `maxPayload`: the pre-change mux passed no limit, so
the 1 MiB ceiling was an unrelated breaking change. Remove the exported
constant and the oversize-frame test, and drop the matching docs.
Pin the per-frame compression threshold and the accepted compressed-write
liveness consequence with regression tests, correct the queued-write
documentation, move the Agent Note to `implemented/`, and sync the Remote
duplex stream note with `websocketCompression`.
Refs #6476
Replace the pr-declare skill with dsh-pr-description, a repository-only
skill (user-invocable: false) that owns how deepseek-harness PR
descriptions are written and updated: section rules, Issue references,
test design, change-specific disclosures, and the Human and Agent Roles
section with its levels, stages, grading rules, and update timing. The
skill no longer fetches an external specification, asks the user for a
level, or adds badges.
Reduce the pull request template to a skeleton that points to the skill,
point root AGENTS.md at the skill, and make the template test check that
the template's role level line and stage placeholders match the skill.
Closes#6034
Add a `websocketCompression` Gateway config field (`'deflate'` by default,
`'none'` to disable) and negotiate `permessage-deflate` on `/api/remote.mux`.
Frames whose JSON text reaches 1024 characters are compressed in the mux send
path, because `ws` ignores its `threshold` option under the default context
takeover. `maxPayload` is bounded by `max(streamInboxBytes, 1 MiB)` so one
compressed frame cannot inflate past the fixed ceiling.
Fixes#6476
After a stop, steering still pending is claimed with the next prompt and the
AgentLoop logs it first. Once the first of those rows is durable, the
remaining echoes follow the new Turn's process control, which renders empty
while the Turn runs. The 16px clearance after that control applied to them
and moved each echo by 10px before its durable row replaced it. The clearance
now applies after a visible control to every row, and after any control only
to rows that are not local echoes, so the echoes keep the input gap.
Also state that a switch lifted within a move's duration plays the rest of
it, that the steering entrance approximates the status's previous position,
and that the runtime-context notice placement follows from the README rule.
The running status now renders after the transcript rows and before the
steering echoes that are still waiting, so the mid-steer accessibility
snapshots list the status ahead of the interjection echo. Refreshed with
DSH_SNAPSHOT=refresh on the steering scenarios; no other golden changed.
A pinned tail zeroed both running status moves. Following scrolls absorb
output growth, so the status keeps its on-screen position for the first
output, but admitted steering still moves the status past its pending
echo below it: in a long transcript at the floor the status jumped the
height of that echo with no motion. The input-fold switch keeps zeroing
both moves; the pinned switch now repeats only the first-output slide's
structure, so the entrance after an admitted steering row keeps its
duration while the page scrolls by the added slot. Update the README and
the CSS check that the pin applies to the slide structure alone.
The steering entrance and the first-output slide were selected through
`:not([data-input-fold]):not([data-chat-tail-pinned])`. Removing either
attribute added the animation name back while the structure still
matched, so a finished move replayed: scrolling away from a long running
transcript and back short of the floor, or the end of any fold batch,
faded the status in again and slid it one row. The structure alone now
names each move, wrapped in `:where()`, and the two switches only set
`animation-duration: 0s`; turning a switch off changes the duration of a
move that already finished, and turning it on completes a running one.
The tail pin now uses the test fold batches use for a tail landing:
follow intent with no unsampled reader movement, over content that
overflows the scrollport excluding fold room. A reader scroll keeps follow
intent until its 500ms sample settles while growth is not followed, so
the old pin claimed a held status there and skipped a needed move.
ChatReading recomputes the pin where follow intent and sampling settle,
the viewport recomputes it after scroll, scrollend, and each resize's own
follow scroll, reusing the scroll metrics or the resize's clean-layout
measurement, and the attribute is written only when the pin changes.
Record the decision to precompute the steering batch start in Definition
state with process-evidence and reader.previous as an Agent Note.
The first-output slide set its 6px default through a selector that
outranked both variants, so a response (12px) and a new Turn's first
output after the empty process control (16px) travelled 6px and started
with a 6px or 10px jump. The default now sits on the single-class
`.running` rule, which every variant outranks.
When the transcript overflows the viewport and follows its bottom, the
follow scroll of the same resize absorbs new rows and the status keeps its
on-screen position; replaying a move there lifted the status 28-44px and
slid it back. The viewport now marks the column `data-chat-tail-pinned`
while the reader follows overflowing content, updated after each resize's
follow scroll and on scroll, and neither the steering entrance nor the
first-output slide plays under that mark. Update the README and add
viewport and CSS checks.
While a Step has no output yet, the running status sits directly below its
input. When the first process row, group header, or response appears, the
status now slides from that position into its slot below the row instead of
jumping. It travels the row's gap plus one font-scaled 24px row: 6px for a
process row or group header, 12px for a response, and 16px after the running
Turn's process control, which renders empty until the Turn closes. The move
reuses the steering entrance technique with the standard easing over 110ms,
runs once per input-to-output change, and is skipped under reduced motion and
during an input fold.
Group header titles take the process-row line height, so the travel matches
the inserted row in every display mode and font size. Update the README and
the CSS checks that keep the travel equal to the rows that cause it.
After a stop, steering still waiting in the next-step queue is claimed
together with the next idle submission, and the AgentLoop logs it first.
Echoes before the first transcript echo now render with that submission as
the next Turn's opening inputs above the running status, and move ahead of
the new Turn's empty process control together, so the echo order matches the
durable rows. Update the flow-tail spec and README for the echo order.
Render the running status after the transcript rows and opening echoes and
before steering or Inbox echoes, so its position shows whether a waiting
steering message has been consumed. The status uses the completed Turn's
actions-row slot, its divider no longer takes layout space, and it hides once
the Turn tail is rendered.
The first steering row of each next-step claim keeps that 44px slot above it.
A state-only process-evidence Definition records each Step with durable
visible output, and the message Definition marks a claim's first human
message as a batch start when the latest earlier evidence belongs to the same
Turn. Rows claimed together stay 6px apart.
While the right sidebar is collapsed, its docked hosts are translated past
the frame's right edge and widen the frame's scrollable overflow. Outside
macOS, PageUp in a text field moves the caret and reveals it against the
start edge of every scrollable ancestor, so the `overflow: hidden` frame
scrolled sideways and the whole UI shifted left.
Clip the panel's overflow. `overflow: clip` does not make the panel a scroll
container and does not clip the fixed-position floating panels, whose
containing block is the viewport; the DockLayout ancestor contract now says so.
Fixes#6341
The page clock failure depended on the real-to-fixture distance modulo
2^32 ms, so a regression run on today's date alone passes against the old
fixture for half of every period. The test now shifts the Host clock across
eight phases of that period; the page.clock fixture fails every phase,
either with stopped page timers or with a Date that does not advance.
pinHostClock replaces Date.now in the Vitest worker, where Playwright's
server also runs. page.clock.setFixedTime therefore recorded the fixture
day, and after a navigation the page clock replayed the gap to the real
day. The resulting negative timer delay was truncated to a 32-bit integer
and, from 2026-10-15T13:45:20Z, became a delay of weeks that stopped every
page timer, failing the steering and seeded-history suites.
pinBrowserClock now replaces only the page's Date so it reads the fixture
day while native timers and animation frames keep their real schedule; a
regression test navigates twice with both clocks pinned.
The packer images the web template bundles, every optional bundle, and an installation manifest instead of a dumped composition, and the Worker boots that profile through app-boot like dsh web. Package-local lazy client chunks move into content-addressed archives that the VFS fetches on first asynchronous read; client-modules reads chunks asynchronously.
The Worker replaces node-addon-require-builtin with Node v24.18.0 internal-module interfaces and reports Node 24, so plugin metadata, the Cordis Loader, and HMR reach one cascaded loader backed by the Worker loader. Resolution failures carry Node error codes, the empty load cache lets HMR apply profile changes, and every optional bundle switches on and off live. Platform stubs throw ERR_FEATURE_UNAVAILABLE_ON_PLATFORM.
A Client plugin loaded after the page starts fetches its bootstrap once. The Host now registers that route before starting the Worker and answers once the Worker is ready, so enabling Developer Tools on a running page opens the bottom DevTools without a reload. A failed start answers 503.
When the platform cannot run the Worker (ERR_FEATURE_UNAVAILABLE_ON_PLATFORM), the Host warns once and stays active with only the 503 bootstrap route; serve-static and open load after the Worker starts. Every other startup failure still fails the fiber.
verify-package-dependencies now covers every packages/*/* manifest, experimental included, and reads each package's tsdown build entries so Worker and child-process entries count as Host runtime sources. Experimental Client packages no longer declare browser and type relationships as peers.
Package modules that other code imports must not suspend evaluation at module scope. A syntax-aware gate scans packages/*/*/src and excludes only executables (a shebang or a package bin target). The SenseVoice worker, the SSH helper entry, and the Inspector worker entry now run an explicit main().
The provider's Linux decisions now live in LinuxScopeSelector, a state machine
over injected observations: the deep probe, the user-manager listener, the
manager confirmation, and the systemd-run binary identity. Each decision
carries its reason and probe trigger, the runtime logs it at debug level, and
table-driven scenarios cover every transition.
- A positive verdict probes again when the systemd-run found through PATH
changes device, inode, size, or modification time, so an in-place upgrade
past v254 cannot reuse options that no longer keep argv literal.
- The listener is read before the probe, so a manager that starts during a
failed probe counts as a new listener.
- The kernel socket table is decoded as UTF-8, matching runtime directories
outside ASCII.
- LinuxScopeNotStartedError drops its unused code field.
A cached positive verdict let a hung user manager that still listens fail every
later command, and a cached negative verdict ignored a manager that started
later until the provider restarted.
- A scope launch whose bootstrap never consumed its launch request now fails
with LinuxScopeNotStartedError, and the runtime clears the verdict it
launched under, so the next eligible spawn probes again and records the
fallback when that probe fails. A generation counter keeps launches from a
superseded probe from forcing another probe.
- After a negative verdict, the runtime records the /proc/net/unix listener on
$XDG_RUNTIME_DIR/systemd/private and probes again only when a different
listener, meaning a new manager instance, appears.
The real-socket test rejects on a listen error instead of hanging, the table
fixture comment names the accepted-connection row it models, and the remaining
linux-scope module mocks provide confirmLinuxManager. The README notes that
the socket-table read matches only listeners in the same network namespace and
costs time proportional to the table size.
systemd closes the private listening socket on shutdown without unlinking its
path, so the socket file outlives a manager stopped in place. The fast path now
reads /proc/net/unix, where the kernel lists a bound socket only while a
process holds it open, and accepts the manager only when a process listens on
$XDG_RUNTIME_DIR/systemd/private; otherwise the systemctl query decides. The
scope startup error names an unreachable manager as the cause, and the spawn
test again asserts that Windows launches never reach the fallback.
Without a per-spawn check, a user manager that stops after a positive probe,
such as after a logout without lingering, made every later scope launch fail.
Each eligible spawn now confirms the manager: its socket file at
$XDG_RUNTIME_DIR/systemd/private disappears when the manager stops, so its
presence answers with one stat; otherwise the restored systemctl --user show
query decides and the spawn falls back while it fails. A crashed or hung
manager that leaves its socket file still fails the scope launch with the
existing restart hint.
The probe always passes --collect, which systemd-run rejects before v236, so those hosts use the fallback regardless of the --expand-environment=no retry.
With the probe cached for the provider lifetime, a user manager that stops
after a positive probe makes every later scope launch fail. The startup error
now says to restart the provider to probe native containment again. The README
no longer lists older user-systemd among fallback hosts and notes the second
probe attempt for a systemd-run that rejects --expand-environment=no.
The README says the provider also keeps a failed or timed-out first probe and
names the user manager as reachable rather than live, since it is checked once.
The probe cache documents why no later spawn repeats the probe, and the spawn
test now starts two Windows commands to match its per-spawn Job recheck title.
systemd-run before v254 rejects --expand-environment=no, so the scope probe
always failed and those hosts used the weaker fallback. Its scope mode never
expands argv, so the probe retries without the option only when systemd-run
reports it as unrecognized, and scope launches reuse the probed options.
systemctl before v252 rejects --kill-whom. Use --kill-who, the older
spelling, which newer systemctl accepts as an unambiguous prefix.
Every Linux spawn() and spawnTerminal() ran a synchronous spawnSync probe
before launching the command: the full systemd-run scope probe until it first
succeeded (a failed verdict was never cached), then a systemctl manager recheck.
spawnSync blocks the host event loop until the probe exits, so hosts without a
usable user manager paid the full probe on every grep, glob, and shell command.
Keep the first verdict, positive or negative, for the provider lifetime and
drop the per-spawn manager recheck.
A revealing row that ran on its own CSS transition and was then taken
over by a batch entered the landing twice: the measured starting layout
held the height it had already reached, and the growth amount added its
full height again while the batch restarted it from zero. The extra
height raised the tail target above the real floor and stayed as bottom
room. Rows revealed while a batch ran kept their own transitions and were
taken over this way by the next batch.
- A row that starts revealing while a batch runs waits at zero size like
a held closing row; the next flush drives it in a batch or starts its
own transition when nothing closes.
- A batch continues every member from its current size: closing rows
shrink from the height they have now, and revealing rows grow from the
height they reached, so only the remaining growth enters the landing.