The client tsdown presets found a package's own manifest by name: a glob of
every packages/*/*/package.json, parsed until the name matched. tsdown's
native config loader imports each package config without module cache, so
the module-level cache never hit and all 82 configs repeated the glob
(40-125 ms each) synchronously on tsdown's main thread, where it delayed
every other package build. It was 34.5% of the client pass's main thread.
- clientBundle, staticLinked and clientLibrary take the package directory
(import.meta.dirname at every call site) and read <dir>/package.json
once; the package name, loader id and externals come from that manifest.
- A missing, unreadable or nameless manifest throws an actionable error.
- The glob, the name search and the module-level caches are removed.
tsdown client pass 12.78 s -> 8.73 s (median of 3 interleaved runs each,
-31.7%); all 11,566 lib outputs byte-identical; all 82 configs resolve the
same externals and banners.
Refs #5852
Delete @deepseek-ai/dsh-invariants, every package's ./invariant companion
and its tests, the invariant gates and Vitest setup, the scoped-event
resolver generator, and the INVARIANT-coded listener rethrow paths.
Update docs, READMEs, Agent Notes, and add an upgrade guide.
* feat(i18n): key translation pairing records by heading section
Replace the whole-file blob hashes in *.i18n.yaml with one entry per
heading section that still holds language-specific content, keyed by the
English heading-slug path. Blocks identical across the pair (generated
table rows, code blocks, shared regions) stay out of the hash, so edits to
different sections and identical regenerations merge with Git's default
text merge, including on GitHub.
Remove the dsh-translation-pairing merge driver, its resolver command and
installer wiring; gen-translation-brief recovers last-confirmed text from
commit history. Migrate all active records, archive the superseded Agent
Note, and record the replayed conflict reduction in the new one.
* test(i18n): refresh translation prompt snapshot for the terminology note
* feat(i18n): exclude only code blocks and generated regions from pairing records
Replace byte-equality masking with an explicit rule: a record hash skips
fenced code blocks and BEGIN/END GENERATED regions, the two kinds of
content the pairing gate already requires to be identical on both sides.
Move generated data into regions written into both pages:
gen-config-catalog renders both languages and the record with per-package
regions labelled by code identifiers; gen-module-graph keeps its table in a
region; gen-doc-graphs splices the event matrix regions into the Chinese
page. Add a generic region splice beside the region grammar.
The first-use directory name and its stored title came from the Client's
startup language, so one product decision carried two incompatible
requirements: a path addressed by shell commands, tool arguments, @path
references, and session logs, and a label a reader reads.
The controller now owns the fixed `default-workspace` directory name and
the registry titles the Workspace after that same segment, so
`workspace.initializeDefault` takes no request at all —
WorkspaceInitializeDefaultRequest and its name validation are gone, and
the registry resolver returns a path instead of a path and title. No
language reaches the Host.
Browser consumers label a Workspace through the new
`workspaceDisplayTitle`: a title still equal to the automatic one reads
as the localized default name from the `common` namespace, every other
title reads verbatim. The Workspace browser resolves it once at the store
read, so rows, hover card, search meta, and the rename and delete dialogs
inherit it; the hero picker and the Conversation chip resolve it too.
Confirming a rename stores the text on screen, which is what stops a row
following the language.
* feat(workspace): stop a Session's running work before archiving it
Archiving was a pure visibility write, so a Session archived mid-turn kept
running out of sight. The Workspace registry now owns archive admission
through two Host events: `workspace/session-activity` reports what still
runs and refuses the plain archive as `workspace/session-active`;
`workspace/session-stop`, requested with `stopActivity`, stops that work the
way the user's own stop actions do before the archive write. The API Session
Controller reports and stops turns, subagent descendants, and owned jobs and
rejects any model step for an archived Session; the Schedule plugin reports
and deletes active reminders. The sidebar turns the refusal into a
stop-and-archive confirmation listing the Host's answer.
* test(workspace): pin the widened archive signature and prove child liveness by heartbeat
The Client inspect catalog spec pins archiveSession's new options
parameter, and the archive e2e reads the Bash child's liveness from a
heartbeat file instead of a pid: the Linux sandbox runs the tool in its own
PID namespace, so the pid it reports is not comparable from the test.
* test(workspace): cover the archive-admission providers and dialog to the per-file gate
Drop the provider's unused disposer and the dialog's unreachable confirm
guard, read reminders without a catch that no composition can reach, and
add the cases the coverage lane found missing: a stop without a jobs
registry, a looping lineage, a descendant whose projections reject, the
dialog's Escape guard and non-Error reason, and a family merged by another
provider.
* fix(workspace): write the archive before stopping, gate subagent lineage, and delete reminders under the schedule barriers
Review round on #4765. The registry now writes the archive set before it
dispatches `workspace/session-stop`, so the durable set already gates
every wake the stops induce; the controller's `agent/pre-step` gate also
blocks subagent descendants of an archived Session and contains each
cancel or kill on its own. The Schedule plugin's stop listener runs as a
management delete in the agent's transaction queue between the same two
persistence barriers the `schedule_delete` tool uses. The confirm dialog
selects singular or plural English lines and keys list rows by index.
Docs: archive-admission section with type-equiv blocks on the workspace
subsystem page, write-first order and residual windows in the READMEs
and the Agent Note, and the missing zh reminders sentence.
* test(web): scope the question composer's New Session wait to the Sessions tree
The wide sidebar's New Session button carries the same visible label as
a fresh session row, so the unscoped `getByText` either settled on the
button before the row existed or hit a strict-mode violation once the
row rendered first, which is how the snapshots lane failed on
ece68c2ab5. Waiting inside the Sessions tree names the row alone.
* fix(schedule): answer archive admission from the owned runtime's fold, not the projection registry
The `sessionProjections.snapshot` API is the Client projection view and
is not a Host read path (review on #4765). `ScheduleRuntime.activeRecords()`
folds the live log the way every drive does, and the archive-admission
listeners ask the owned runtime for that fold: a session without a live
agent or without an owned runtime has nothing armed to fire, so it reports
nothing and has nothing to stop. Covers the runtime-less agent, a fold that
turns unreadable at the persistence barrier, and the stopping/faulted
runtime answering nothing.
* refactor(subagent): report and stop subagent descendants from the Subagent runtime
The `subagent` family of archive admission moves out of the API Session
Controller into `dsh-subagent`, which records the lineage at delegation
and owns the descriptor (review on #4765): `SubagentRuntime` answers
`workspace/session-activity` with the running descendants, labelled from
each child's descriptor through a live Session observation, and
`workspace/session-stop` by cancelling each of them with the parent
cause. The controller keeps turns, owned jobs, and the lineage step gate,
and composes its provider through `ctx.plugin(SessionActivityProvider)`
with declared injections instead of a bare installer call. `dsh-workspace`
becomes an optional type-only peer of `dsh-subagent`.
* refactor(jobs): answer archive admission for owned jobs from the job registry seam
The `job` family moves out of the API Session Controller into `dsh-jobs`
(review on #4765): the seam's constructor installs the
`workspace/session-activity` and `workspace/session-stop` listeners for
every implementation through the abstract `list` and `kill` alone, so the
controller no longer knows the jobs vocabulary and keeps only the turn
family and the archived-lineage step gate. `dsh-workspace` becomes an
optional type-only peer of `dsh-jobs`.
* refactor(workspace): leave SessionActivityKindMap empty and let each provider merge its family
The workspace package no longer names any activity family (review on
#4765): `SessionActivityKindMap` is empty, and each provider merges its own
key from a module both its faces import — `turn` in the API Session
Controller's types, `job` in the job registry seam's view, `subagent` in
the Subagent runtime's control types, `schedule` in the Schedule plugin's
types. The confirm dialog imports those modules for its cases and keeps
the generic line for any other key; the registry and controller suites
merge test-owned families instead of borrowing the providers'.
* refactor(agent): report and stop the running turn from the Agent registry
The `turn` family moves out of the API Session Controller into `dsh-agent`
(review on #4765): the registry's constructor installs the
`workspace/session-activity` and `workspace/session-stop` listeners for
every Agent it publishes, answering with its own lookup and cancelling the
turn with the user cause, and merges the `turn` key from its client-safe
types. The controller keeps only the archived-lineage step gate, renamed
`ArchivedSessionGate`. `dsh-workspace` becomes an optional type-only peer
of `dsh-agent`; the workspace package depends on none of the providers,
so the edge stays acyclic.
Since #3127 a foreground Bash command is a registered job from its start,
so the archive dialog lists it beside the turn; the e2e and its ARIA golden
expect that row, and the capture keeps the running job by design.
Every Typert Remote stream accepts client-to-Host items on the same
logical stream. A method declares the uplink item type in its return
type, RemoteStream<Out, In>, and reads the items through
this.ctx.invocation.uplink(); the invocation also carries the calling
Peer. Client generated methods return a handle that iterates the
downlink and exposes send/end/dispose for the uplink.
Pin sessions to the top of the sidebar and reach archived sessions
through the view-options filter instead of a settings page; archiving
gains an undo/filter toast, and row motion stays reserved for pin jumps
and archive fades.
Both archive verbs answer with the complete set, so a reply that lands
after a newer request or after a pushed increment overwrote it. The
Client model now installs an answer only while it is still the latest
archive-set request, and the follow increment and baseline supersede it.
Also narrow the page's public surface and its states: the locale
namespace stays internal, row derivation is a memo over the framework
hooks, the search input carries one accessible name, the empty-archive
message no longer covers a set whose entries cannot be restored, and the
new package declares no client dependency edge it does not import.
Sessions could be archived from the sidebar row menu but never restored:
the registry-global archive set was a one-way display filter with no
inverse RPC and no surface listing its members.
WorkspaceRegistry.unarchiveSession drops one id from archivedSessionIds in
one durable write on the same operation chain as archive, with no
session-existence probe because a removal cannot introduce an unknown
referent. The Workspace controller exposes it as workspace.unarchiveSession
returning the complete archive set, which the existing archived follow
increment already propagates, so no frame type and no persisted field
changes.
The new client package dsh-client-ui-settings-unarchive-sessions owns the
restore surface: one localized settings.section page listing the archive
set joined with loaded session summaries, newest archive first, searchable
by title or workspace, with one Unarchive action per row.
Spec files that create /tmp/dsh-* directories via mkdtemp now track and
delete them in afterEach/afterAll; module-scope fixture dirs (executor
spill dirs) are removed in afterAll. The file list came from the
observed-residue inventory on the self-hosted CI host: only specs whose
dirs actually accumulated were leak sources (issue #3134), superseding
the kept-but-unmerged CI sweep branch per the #3233 review decision.
Product per-process spill roots (dsh-subprocess-local spawn,
dsh-spill-local store) register a process-exit handler that removes the
memoized dir, so processes that used the spawn/spill path clean up on
normal exit. A SIGKILLed process cannot run in-process teardown; the
machine-side timer remains the backstop for that path.
Agent Note: .agents/notes/implemented/process/2026-08-28-test-temp-dir-self-cleanup.md