/api/dsh-web-all/degraded answered an empty list while /rows still listed the
plugin, and the task board could only translate its absent routes into
"not mounted ... restart the DSH service" - advice that cannot work while
another process holds $DSH_HOME/task-board/ledger-v2.lock. A degraded record
now carries a bounded one-line reason and the task board reads it to report
the owning pid with actionable guidance; a bare 404 stays the only signal
that the routes are absent, and the one-owner-per-ledger contract is
unchanged (no lock change, no cross-process write).
The other four issues live in the skin center and are fixed in that
repository (satellites/dsh-skins, commit 82f42bd): the applied-skin contract
(#1740), the ORCA stage geometry and the semantic adapter's late anchors
(#1732), the maid-atelier trim over the plugin manager page head (#1742), and
the composer frost that made the card a containing block for the shell's
fixed tooltips (#1724). This commit moves the gitlink and rebuilds what the
market reads from it.
Also rebuilt: packages/dsh-web-all/lib (its sources moved), the market dist
for the four touched skins, and the recorded lib fingerprints.
Validation: pnpm typecheck, pnpm test, pnpm test:standards, pnpm docs:check,
pnpm i18n:check, pnpm emoji:check, pnpm aggregate:check, pnpm libs:check,
pnpm sync-shared:check, market-build --check, and the skins suite all pass.
The store installed community plugins through its own writer chain (family
`pluginManager` service to the loopback gateway to the `dsh plugin`
CLI), which the packaged Desktop client refuses. Install through the
official in-process manager's remote face when the host publishes it, keep
the family face as the fallback, add a manage action that hands an
installed bundle to the official Plugins page via
`pluginNavigation.openBundle`, and bridge both faces with `ctx.inject`
as contract observations instead of cross-package imports.
The packaged Desktop client's application-owned profile refuses the
`dsh plugin` CLI, so the Workshop store's one-click install failed with
the launcher refusal while `update()` already took the official
in-process manager. Give `install()` and `remove()` the same writer
selection, keep the CLI path untouched elsewhere, verify the profile
actually moved (an install must add a dependency, a removal must drop
one), and let the HTTP routes demand a `dsh` binary only when the CLI is
the writer that will run.
The create/duplicate/subtask dialog stacked every field in one scrolling
column. It now groups its configuration into collapsible regions, task
content open by default, and every collapsed header keeps a one-line summary
of the values it holds, so the default layout needs no scrollbar; only the
modal body keeps an overflow fallback for short windows, and a region holding
a blocking error is forced open instead of reporting it out of sight.
The agent-preset pin is renamed from "Mode" to "Agent preset" in the create
dialog and the task detail. Its picker keeps the runtime roster as the
authority and groups it into built-in and user presets, shows the four
built-in rows under localized names, and defaults to an inherit choice that
names the preset a run without a pin actually uses. zh/en copy stays in the
package and ru mirrors it in dsh-i18n.
A task row now carries an optional goalRun flag, on by default: the new-task
dialog starts checked and the task detail can turn it off. Only an explicit
false is stored, so a card that never touched the option - and every card
written before the field existed - keeps starting its runs as goal runs.
When it is on, HostExecutionRunner queues the task prompt first and then arms
dsh's built-in /goal with the same composed prompt as its objective, so the
session keeps working automatic continuation rounds until the agent marks the
goal complete. The inspection loop settles such a run from session/projections
(active stays pending, blocked fails with the goal's own reason, and
complete/paused/no goal keeps the turn verdict) instead of the first turn end,
which would have reported unfinished work as done and let a scheduled card
start a second concurrent session for the same objective.
A refused or unacknowledged /goal command is reported and the run continues as
a plain single turn; goalObjective prefixes an objective the command's own
parser would read as a goal operation.
Also updates the agent-tool surface, the zh/en locales, the dsh-i18n ru
dictionary, the package README pair and AGENTS.md, the rebuilt aggregate client
bundle with its fingerprint record, and adds the owning Agent Note plus the
goal-run test suite.
- Remove the sync-manifest entry for shared/host/run-guarded.ts and the three
generated copies under packages/{dsh-usage,dsh-task-board,dsh-git-graph}/src/host/:
nothing in this repository imported the module (the satellite repositories
carry their own copies), so the entry only kept three unread files in sync.
The shared source and its spec stay.
- Delete the unused mobileBundle helper and its node:module createRequire import
from shared/tsdown.client.ts; the standalone mobile bundle has been gone since
0.4.0.
- Update the sync composition guard in scripts/sync-shared.test.mjs (99 -> 96
copies, 48 -> 45 host copies) and refresh scripts/lib-artifact-fingerprints.json.
- Re-point the notes, READMEs, CONTRIBUTING and docs that still linked the
skin-center contract files to the dsh-skins repository, and prune the stale
screenshots.
- Record the decision in
.agents/notes/implemented/simplification/2026-09-26-dead-shared-artifacts-removed.md
and correct the run-guarded facts in the aggregate fault-isolation note.
Follows the dsh-skins fix (that repository's `a342b8e`) that re-anchors the
plugin sidebar rows on the shell's native `sidebar.panellist` rows. Moving the
gitlink is what the market build reads, so the four affected skins' assets and
their try-on transforms are regenerated here together with the aggregate
bundle (which inlines the child plugins' client sources and now carries the
`data-dsh-panel-entry` glyph anchor) and the recorded lib fingerprints.
pnpm market:check, pnpm libs:check, pnpm test:standards, pnpm i18n:check and
pnpm docs:check all pass at this revision.
The shell owns the `sidebar.panellist` row box and exposes no per-entry hook,
while the retired family row attributes were the only way a skin could tell
one plugin row from another. Each panel's own glyph now carries
`data-dsh-panel-entry` (the panel id, shared with its `main` slot key) — the
one piece of that shell-owned row the plugin itself renders.
The skin center's compat adapter keys on it to keep stamping
`data-dsh-part="sidebar-entry"` on these rows, which is what the eight
shipped skins and the L2 contract anchor on (dsh-skins `a342b8e`); without
this half the adapter rule never matches and every row rule stays dead.
Clicking Update on the packaged Desktop client failed with "env: node: No such
file or directory". The host is GUI-launched with PATH=/usr/bin:/bin:/usr/sbin:
/sbin, so findDshBinary() lands on the darwin fallback /opt/homebrew/bin/dsh, a
Node script whose shebang is #!/usr/bin/env node. dshSpawnCommand() returned that
path unchanged off Windows, so the kernel handed the shebang to env and the child
died before the CLI started (exit 127).
Two changes repair the flow:
- A Node-script CLI is never spawned through its shebang. isNodeScript()
classifies the resolved path by extension or by a shebang probe, and the
command then becomes an interpreter that exists without PATH: the node
installed beside the CLI (npm-global and homebrew layouts), otherwise
process.execPath, which the existing ELECTRON_RUN_AS_NODE branch covers for an
Electron host. A native executable or a shell wrapper still spawns as-is, and
an unreadable head falls back to the previous direct spawn. withPrependedPath()
puts the CLI's own directory first on the child PATH, collapsing every case
variant of the key into one PATH (the Windows hazard) because dsh plugin
forwards to the pnpm installed beside it.
- An application-owned profile updates through the official in-process manager:
the CLI refuses "dsh plugin --profile desktop ..." outright, while the running
host mounts @deepseek-ai/dsh-plugin-manager as the pluginManager service with
the launcher's bundled package-manager invocation. CliGateway reads it through
a host-supplied seam and only when facts.desktop is true; the job table,
/status polling, the mutation queue and the version verification are unchanged,
so a green manager call that left the version in place is still a 更新未生效
error. The manager is a contract observation, not an import, so the package
keeps running on a host that mounts none.
Every other runtime keeps the CLI as the single writer. README pair, package
AGENTS.md and the owning Agent Note record the new path.
Validation: pnpm typecheck; pnpm test (plugin-manager 239/239, full suite green);
pnpm test:standards; pnpm docs:check; pnpm i18n:check; pnpm libs:check;
pnpm emoji:check; pnpm aggregate:check.
The panel-mount core and the PANEL_FAMILY occupancy table no longer exist, so
"One center-column panel family" becomes the single owning note for how family
panels occupy the center column: the shell's panellist row plus the layout's
keyed main slot are the occupancy authority, view state lives in a panel
controller, and a long-lived resource is never owned by the page.
The panel-mount-core extraction note is consolidated into it (its rationale,
alternatives and consequences are preserved) and deleted with its sidecar.
Inbound links from the plugin-mutation-cost and idle-resources notes are
repaired, the task-board note's cross-plugin-exclusivity bullet now records
the handshake as the transitional mechanism it was, the skill center's package
AGENTS drops the handshake too, and every touched pair is re-recorded.
ssh is the last panel that took the center column over at the DOM level. It
now registers a row in the shell's own panel list (sidebar.panellist) and a
keyed page in the layout's main slot, like the task board and the skill
center, so the shell owns the row box, label, highlight, rail and switch.
That retires the whole takeover apparatus:
- shared/client/panel-mount-core.ts and shared/client/sidebar-entry-core.ts
are deleted along with every synced copy; ssh's mount.tsx, sidebar-entry.ts,
panel-mount-core.ts and sidebar-entry-core.ts go with them. The
body-mutations hub now serves only the aggregate shell and the usage card.
- The dsh-panel-activate handshake and PANEL_FAMILY occupancy table are gone:
the layout's keyed main slot is the single occupancy authority, so the board
and the skill center no longer coordinate with anyone.
- The terminal survives the move because the PTY session is host-owned (see
the previous commit): the layout unmounting a deselected page costs the view
its xterm instance, not the remote shell, and the terminal tab reattaches by
session id. The panel's tab, connect request and session id live in the
controller rather than in component state.
Tests move with the design: the takeover specs (center-panel-lifecycle,
center-column-css, sidebar-entry, the layout specs, board/ssh coexistence) are
replaced by native-panel-registry specs that drive the real SlotCore, plus a
controller-state spec for ssh's view state.
Validation: pnpm typecheck; pnpm test (ssh 202, task-board 561, skill-explorer
121, shared 107, all packages green); pnpm test:scripts 342/342;
pnpm test:standards; pnpm docs:check; pnpm i18n:check; pnpm aggregate:check;
pnpm libs:check; pnpm market:check.
The PTY shell is now owned by the host, not by the browser view: the upgrade
handler creates one session and any later socket attaches to it by id, so a
view can detach and come back without tearing the remote shell down. This is
the survival property the DOM takeover used to provide by keeping the visited
tree mounted, and it is the prerequisite for the panel moving to the native
layout seats (where the layout unmounts a deselected page).
- engine/terminal-sessions.ts owns the session table: a bounded scrollback
replayed on attach, per-session backpressure across every attached socket,
a grace window for an exited shell, and an idle reap so a detached session
never leaks for the host's lifetime. Routes keeps loopback fencing and just
wires sockets into the registry; the registry is disposed with the routes.
- protocol: the ready frame carries the session id; the client can send
detach (keep the shell) and close (end it).
- client: openTerminal/attachTerminal plus detach()/close(); a deliberate
detach no longer surfaces as a transport error.
- The panel controller owns the live session id and the active tab, and
TerminalTab reattaches on mount and detaches on unmount, so a remount
restores the terminal with its scrollback instead of a blank screen and a
second shell.
Validation: pnpm typecheck; pnpm --filter @linxin666/dsh-ssh test (26 files,
215 tests) including the new terminal-sessions.spec (detach/reattach, expanded
scrollback, explicit close, idle reap, exit grace window).
Note: run this package's tests through pnpm (or its own .bin): the workspace
root .bin resolves vitest against jsdom 29, which serializes a CSS custom
property value without the space after the comma and fails an unrelated
terminal-font assertion.
The skill center stops taking the center column over at the DOM level. It
contributes a row into the shell's own panel list (sidebar.panellist) and a
keyed page into the layout's main slot, and drives ctx.layout.selectPanel —
the same shape the task board already uses, so the shell owns the row box,
the label, the active highlight and the collapsed rail. mount.tsx, the
hand-drawn sidebar row, and the package-local panel-mount-core /
sidebar-entry-core / body-mutations copies are gone.
The panel's tab and editor target move from SkillPanel component state into
PanelController. The layout mounts a keyed page only while its panel is
selected, so component-local state dropped the open tab and any in-progress
edit on every panel switch; SkillPanel now reads the controller snapshot
through useSyncExternalStore. panel-state.spec.ts locks that contract, and
native-panel-registry.spec.ts drives the registration against the real
SlotCore.
ssh still takes the column over at the DOM level, so native-panel.tsx keeps a
transitional dsh-panel-activate handshake with it (and the task board keeps
its side). Both go away when ssh moves to the native seats too.
sync-shared loses the skill center's three copies: the copy-count buckets are
102 total / 42 client, and ssh is now the only consumer of
sidebar-entry-core and panel-mount-core.
Validation: pnpm typecheck, pnpm test (skill-explorer 121, ssh 210,
task-board 565+1 skipped), pnpm test:scripts 342/342, pnpm test:standards,
pnpm docs:check, pnpm i18n:check, pnpm aggregate:check, pnpm libs:check and
pnpm market:check all pass.
Brings in the 11 upstream commits, chiefly 305a3863 (skill-explorer moves
into the center column). Both sides had reworked the same panel subsystem
from opposite directions, so the merge integrates rather than picks a side.
- shared/client/panel-mount-core.ts keeps the upstream PANEL_FAMILY table
(one occupancy row per panel instead of pairwise sibling options) and
documents that the task board left the core for the native layout seats.
- The board keeps its native panel registration and its dsh-panel-activate
handshake, widened from ssh alone to every DOM-takeover family panel:
coordinateWithFamilyPanels plus TAKEOVER_PANEL_NAMES now cover
skill-explorer too, so the new third panel can no longer paint over the
board while its row still looks selected. panel-coexistence.spec.ts
covers the new direction.
- sync-shared drops the board's body-mutations / sidebar-entry-core /
panel-mount-core copies and keeps ssh plus skill-explorer for the core;
copy-count buckets are 105 total / 45 client.
- The panel-mount-core extraction note records the merged design and
refreshed facts; both sides and the sidecar are re-recorded.
Merge conflicts: 14 files. Validation: pnpm typecheck, pnpm test
(ssh 210, task-board 565+1 skipped, skill-explorer 115), pnpm docs:check,
pnpm i18n:check, pnpm test:scripts 342/342, pnpm libs:check,
pnpm market:check, and the panel suites all pass.
pnpm test:standards reports 3 new violation groups, all in
dsh-plugin-manager gateway specs that another session has unstaged; they
are not part of this merge.
Move the task board off its DOM takeover: the board now registers a
sidebar.panellist row and a keyed main page through the official slots
system, and the package-local panel-mount-core, sidebar-entry-core and
body-mutations copies are retired. The shared panel-mount-core returns to
the single ssh consumer with sibling activation names.
Also folds in the plugin-manager update-patch rework (the settings tab is
replaced by a patch row on the official Plugins page, plus the desktop
launch-profile repair) and the matching Agent Notes.
Committed as a checkpoint before merging origin/dev.
A connector that loses its Cloudflare edge registration keeps its process and its metrics port alive while the minted hostname stops resolving. The lifecycle judgement covered only a URL timeout and a process exit, so the manager reported 'running' forever, the relay kept forwarding paired phones to the dead address, and the phone got Cloudflare 530 / Error 1016 instead of the relay's offline page - measured in issue #1723 for over four hours with no self-healing path.
TunnelManager now probes the public URL of a running tunnel every 60s and, after two consecutive failures (DNS failure, refused connection, timeout, or a Cloudflare 5xx such as 530/1033 - an unauthenticated 401/403 still counts as alive), calls the existing fail() path: the process is stopped, the phase goes to 'failed', and the ordinary backoff restart mints a new URL that re-registers the relay. The probe, its interval, its failure budget and its timeout are injectable seams.
The public URL is probed rather than cloudflared's local /ready endpoint: the package's Tunnel handle exposes no readiness face, and the public URL is the address the phone actually uses, so one measurement covers DNS, edge registration and origin reachability.
The LiangShen V4.1 Flash comparison needed evidence before any default could
move, and the tooling could not produce it: a run could not price itself, could
not tell a workspace that failed to reach a host apart from a model that
guessed, and recorded a Windows shim scripting error as the DSH version.
Runner and report:
- add tools/prices/deepseek-flash.json (CNY per million, published off-peak row)
and price runs and the budget gate from it via --budget-cny; cost is no longer
a conversion factor living in a reader's head;
- separate WEB_* transport failures from the tool errors the model caused, so an
unreachable host never reads as a model mistake;
- record research calls and the inspections preceding the first write, the
guardrail the external we-need experiments measured falling from 30 to 6;
- report a metric an older record does not carry as unmeasured rather than as a
measured zero;
- derive the DSH version from a version token and fall back to the package the
dsh shim resolves to.
Corpus and analysis:
- add three external-verification tasks whose facts are published outside the
workspace, plus two workspace-local authority tasks that keep the same
guardrail measurable without the network; every new check fails on its seed;
- state in analyze-session whether the log carried reasoning text, because a
signed block with an empty string makes an empty counter look measured.
Decision record:
- move the improvement plan to implemented with the measured outcome: all five
arms passed 15/15 and every paired comparison was 0.0pp, so the shipped
persona and presentation 'both' stay the defaults; the candidate persona
showed no advantage and the ptc presentation traded a cleaner tool surface for
63 percent more output tokens;
- archive the evaluation snapshot and update the README pair.
Gates: pnpm typecheck, pnpm test (350 tests in the package, 8 focused additions),
pnpm test:standards, pnpm docs:check, pnpm i18n:check. pnpm test:scripts fails on
this host for a pre-existing unrelated reason: scripts/ pass GNU tar's
--force-local while the system tar is bsdtar 3.8.8.
The pet settings page rendered every startup registry warning as a list of\nabsolute local paths above the display fields. Drop that presentation: the\nsatellite commit strips the controller fetch, the snapshot projection and the\ncard block, the zh/en dictionaries lose settings.diagnosticsTitle, and dsh-i18n\nloses the ru key so the i18n gate stays green. The README pair now points at\nnode scripts/dsh-pet validate <dir>. The host keeps the registry diagnostics,\nPetService.diagnostics() and the /api/pet/diagnostics route.
A family settings card addresses its form by PROFILE ENTRY ID, but each card
knew only its own namespace. When the family binder is not loaded the cards
fell back to ctx.configForms.get(namespace) — an entry the Host does not serve
on an aggregate install. The write was rejected with "No configurable plugin
entry \"liangshen\"" while the card reported "the deployment did not accept
these values", which is what the LiangShen settings page showed for every edit.
The fallback now lives once in shared/client/settings/settings-entry-form.ts
(synced to the six packages whose card binds a family namespace) and binds the
entry id the shared describe mirror justifies, REBINDING when the mirror names
a different one of the package rows. A one-shot guess cannot be right for every
deployment: the mirror answers asynchronously — at plugin activation it usually
holds nothing yet, and the aggregate mounts its client children in order — the
bare namespace is no entry id on an aggregate install, and the aggregate row id
is wrong on a standalone one, whose row id is the namespace or the package own
ui-* row. An unanswered or EMPTY mirror counts as unanswered, not as absence;
only a mirror that answers with other packages rows falls back to the
namespace itself, which is the pre-0.1.7 keying shape.
Affected cards: dsh-liangshen, dsh-remote-web-ui, dsh-task-board, dsh-market,
dsh-usage and dsh-session-archive (the last three carried the same defect).
The aggregate and market specs now model a mirror that names the served row
instead of asserting the namespace.
Verified in the real Web GUI: the save now posts ns "web-ui-liangshen" and the
Host answers ok. The refreshed aggregate lib/ and the new fingerprints ship
with the change; the owning 0.1.7 cohort note records the binding rule.
pnpm typecheck, pnpm test, pnpm docs:check, pnpm i18n:check,
pnpm aggregate:check, pnpm libs:check, pnpm sync-shared:check,
pnpm test:standards, pnpm runtime-deps:check and pnpm emoji:check pass.
pnpm test:scripts fails only in the tar --force-local suites, which Windows
bsdtar rejects (GNU flag) and which this change does not touch.
The skill center was a body-level overlay modal and the only family surface
that did not share the center column. It is now a center-column panel like ssh
and the task board: a header with the back-to-conversation control, a
skills/create tab bar, and the family's control vocabulary, still
theme-token-only.
Panels now share one occupancy table: shared/client/panel-mount-core.ts owns
PANEL_FAMILY (activation name and html attribute per panel) instead of the
pairwise sibling options. Opening a panel clears the other rows' attributes and
broadcasts its own name; an open panel closes when the broadcast name is not its
own. The pairwise shape could not express three panels, so a panel that did not
name the third one stayed logically open while invisible and its sidebar row
needed a second click to reopen.
The skills list adopts the family's row treatment, the refresh control hides
while a load is in flight, and the create tab resolves the workspace it needs
when it is the first tab opened. The semantic-attributes contract drops
card/head; the wallpaper-exclusive skin re-anchors its skill center rules on the
panel root plus skill-row (committed in the dsh-skins repository, pinned here).
Generated copies follow their sources: ssh and the task board mounts lose the
sibling options, the skill center gains a panel-mount-core copy, and the
aggregate artifact plus the market dist are rebuilt.
Authored by yupeng.jia <yupeng.jia@momenta.ai> as ad47d3a4 on the contribution
fork; re-applied here on the current dev (the original was based on 53ec2ed1,
108 commits behind) with the generated artifacts rebuilt rather than taken from
the stale base, and the skin-center content re-applied in its own repository.
The agent-preset registry's publication is decided by its own dependency chain
(the official registry injects loader and sessionProjections, and reaches
settings through ctx.inject), so a cold start can activate this row BEFORE the
service exists. The single declaration attempt then warned and gave up, and the
preset stayed missing until an unrelated settings write happened to re-arm the
row -- which is exactly the report: toggle any switch and it appears, and only
then (issue #1721).
The registry cannot simply be injected: a deployment may compose none at all,
and waiting on it would pend the row forever, which the host's boot gate turns
into a failed web boot rather than one dead plugin (issue #1712). So the row
keeps probing and degrading, and ctx.inject supplies only the late edge.
That recovery is gated on a registryMissing flag rather than re-arming
unconditionally: cordis runs the callback asynchronously even when the service
was already present at activation, so an ungated re-arm would release and
re-declare the declaration the activation had just registered. The child fiber
the callback runs under never makes this row pending, so the boot gate stays
out of it.
Also refresh the aggregate lib/ output and its fingerprints, and record the
decision in the 0.1.7 cohort note that owns the volatile settings model.
The wide-foot rules treated the sidebar.footer.action seat as this plugin's
own icon row and gave it a box (flex: none, order, align-items). The seat is
a shared container: as soon as any other plugin registered into it, the seat
became a tall column and the shell's own settings trigger -- an element this
plugin does not own -- was squeezed to 102px and vertically centred inside it,
with the whole footer growing to 515px over the session list (issue #1710).
Drop the seat's box (display: contents) so its occupants answer to the foot's
own wrapping layout, give a non-family registrant a full line of its own, and
keep the family trigger intrinsic-width on the settings line. The intended
result stands: settings on the left taking the remaining width, the family
trigger beside it, every other registrant on its own row above.
Also bump the dsh-skins pin to carry the skin-center and blue-fantasy 0.1.7
adaptations, and refresh the aggregate lib/ output and its fingerprints.
Under the 0.1.7 settings model the plugin's own Config schema is its
settings page: the Host serves a form only for a profile entry that declares
at least one volatile() field, and it refuses a write to a non-volatile path.
Both packages still imported vanilla schemastery, which has no volatile(), so
neither entry was served and neither card could save anything (issue #1717).
Migrate dsh-usage and dsh-remote-web-ui to @deepseek-ai/schemastery, mark the
card-edited fields volatile(), and read every field through its live reference
at use time instead of capturing it at activation. A save now commits into
those references and announces loader/volatile-update, which re-arms the
running instance in place -- so the usage probe cycle and ledger, and the
pairing service, its device sessions, the tunnel and the route registrations,
all survive an edit.
The deployment-level fields (trustedHosts, devicesFile, profile) stay
non-volatile: they belong in the profile patch, so the form offers no control
a live write could not honor. The remote card also stops masking an unserved
namespace with renderChildrenWhenNotExposed/hideNotExposedNotice, so a
deployment that composes no owner shows the reason instead of a doomed form.
`pnpm test` and CI were red on dev. `tests/client-entry-desktop.spec.ts`
pinned the jsdom origin at `dsh-app://app/`, which is an opaque origin:
jsdom's own `localStorage` getter throws `SecurityError: localStorage is not
available for opaque origins` there, and vitest reads that property while it
populates a jsdom environment's globals. The worker therefore died during
environment setup, so vitest reported the file as "failed to start" rather
than as a failing test, and the whole recursive `pnpm test` run aborted on
this package while all 86 remaining assertions passed.
The spec now installs the desktop scheme by replacing the ambient `location`
for the duration of one test and restoring it in a `finally`. The plugin reads
the scheme through `pageProtocolOf()`, which takes a window-like object, so the
same branch is exercised over a usable origin. The spec keeps asserting the
scheme it stands in for, the dictionary registration, and the absent seat.
The new mobile-drawer case in the aggregate's responsive contract also gains
the Given/When/Then marker its lane requires; no assertion changes.
On a <=768px shell the responsive compat layer folds the sidebar drawer on
every sidebar click it reads as a selection. Its selectors also matched a
workspace group row and the row's own actions, so two ordinary gestures were
mistaken for selections (issue #1716): tapping a group row toggled its
aria-expanded and folded the drawer in the same frame, so the sessions the tap
had just revealed were never seen, and tapping the row's ellipsis discarded
the menu before it could be used.
The group row stays open unless the tap is the row's trailing new-session
button, which navigates away and keeps the fold -- the same distinction the
remote package's mobile adaptation already draws for the same row. Session
rows, sidebar entries and other tree rows fold exactly as before, including a
session row nested inside an expanded group.
The regression test drives the four gestures against the stamped frame; the
group-row case fails against the previous source.
The self-update surface lived inside dsh-remote-web-ui, so disabling or removing the remote-access plugin also removed the update entry, and the desktop application - which owns its own updater - had no way to keep the seat off one page while keeping it elsewhere. It now ships as @linxin666/dsh-update with its own bundle row (update / web-ui-update) and its own host half behind the loopback fence.
The aggregate gains the child (aggregate.yml, cordis.patch.yml, children.*), the shared-file sync manifest carries the new consumer targets, the ru dictionary moves its update namespace to the new file, the three old note records are updated in place for the new owner of the surface, and the market's committed lib is rebuilt for the shared guard it carries.
The LAN toggle resolved its profile from config.profile, DSH_PROFILE, then 'web'. The Desktop client boots the desktop profile through runProfile and never exports DSH_PROFILE, so the toggle wrote the managed webserver block into profiles/web while the running host read profiles/desktop: the bind never applied and the card stayed on pendingRestart. resolveManagedProfile now owns the precedence - explicit config, then the launched profile the Host publishes on profileContext, then DSH_PROFILE, then 'web' - and index.ts (in the following commit, which owns that file) reads the runtime fact once per activation.
A Host reload composes the new loader entries before the old ones are torn down, so the reloaded aggregate row asked for a package name the previous entry still held, its mount was dropped, and the previous entry then released the name with nobody left to take it: the row stayed listed as active with no degraded record and served no Host routes until a restart. The guard now queues the refused mount with its own context and replays it one microtask after the release, and a waiter whose fiber dies first is dropped instead of replayed. The Symbol.for registry keeps its Set shape because the satellite repositories compile their own copy of this guard.
The DSH Desktop shell serves the Web GUI from dsh-app://app/ and re-issues every non-static request itself: it deletes origin and sec-fetch-site and attaches the authority-bound dsh-auth-* cookie it redeemed from the launch URL of the Host. Node's fetch adds neither marker, so the board's own /api/task-board/* fetches arrived marker-less, the fence answered 403 forbidden, and the panel rendered board.hostError.unauthorized on the desktop only.
The browser marker now also accepts that credential - the same trust decision dsh-remote-web-ui's loopback proxy already makes - while staying a tripwire: a web-page Origin must still equal the Host header, sec-fetch-site: cross-site is still refused, and a marker-less request without the credential still fails.
The official desktop stylesheet turns every direct body child into a
-webkit-app-region: no-drag region, so a body-level element spanning the
viewport subtracts the whole window from the macOS draggable region and
cancels the official [data-window-drag] chrome rows with it: double-clicking
the title area stops running the system zoom action and the window stops
dragging by its title bar. The skin center mounts six fixed decoration layers
plus a backdrop-blur veil that way at controller creation (no active visual
required), and the aggregate boot splash joins them on every load; all of them
are declared non-interactive, which does not exempt them from that
computation.
The aggregate compat layer now opts those non-interactive body-level overlays
out with an "initial !important" declaration, scoped to the darwin platform and
to direct body children. Rebuilt lib/, refreshed lib fingerprints, added the
responsive-contract regression case and the Agent Note.
Unify the family on 0.4.2: the fifteen published packages and the root
aggregate pin move to the release version, the aggregate's satellite
ranges, the lockfile entries and the release-age ledger move with them,
and the four satellite gitlinks record the commits the satellite
repositories tagged as v0.4.2.
The cohort stays on 0.1.7-rc.2; this commit carries the version, the
rebuilt aggregate and market bundles, the release-preparation table and
the bilingual release notes for the pipeline to stage.
Move every family manifest, the plugin scaffold and the shared workspace
package onto the 0.1.7-rc.2 cohort, together with the surfaces that state
the same fact:
- the dsh.engines.dsh floors and the matching @deepseek-ai/dsh host peers
- the release-age exclusion ledger and the two packageExtensions keys
- the root README badges, the CI/release mount-smoke pins and the
docs/publish-prep.md and docs/plugins.md prose
Two rc.2 facts were verified against the published artifacts rather than
inferred from the version number:
- the shell's frozen static module table is unchanged (the same nine
specifiers in the dsh-web-frontend rc.2 dist bundle), so
shared/web-platform.ts keeps its list and only its provenance comment
moves; the new dsh-client-shortcuts is an ordinary client plugin, not a
static module
- @deepseek-ai/dsh-client-ui-primitives@0.1.7-rc.2 imports a further
undeclared face, @deepseek-ai/dsh-util-code-language, so the
primitives packageExtensions patch gains that pin
The satellite peer floors in pnpm-lock.yaml still read >=0.1.7-rc.1: they
belong to the satellite packages and move when those repositories release
the aligned version.
The aggregate inlines in-repo child client sources, so the route change in
skill-explorer, git-graph, usage, session-archive, plugin-manager, market,
web-all and task-board requires a rebuild of the committed lib/ together with
the recorded fingerprints. pnpm build, pnpm libs:write and pnpm libs:check all
pass.
Refs #1707
inspect() classified a turn as successful unless its turn/end reason was
exactly 'error'. Every other reason the harness appends -- aborted, blocked,
max-tokens, interrupted, forked -- fell through to succeeded, and so did an
unreadable payload. 'interrupted' is what a restart produces: session repair
synthesizes it for a persisted log whose tail turn never ended, which is
exactly an aborted resume. A cron execution that died there was recorded as
succeeded and the schedule failed silently.
Success now requires positive evidence of a completed turn; every other reason
and an unreadable one report failed, keeping the historical wording for the
'error' kind. Five test fixtures used reason.kind 'complete', a value that has
never existed in the harness (the union declares 'completed'), which the
permissive predicate had hidden.
The reuse branch also re-asserted only the pinned permission and model, never
the preset, while the fresh branch passes agentPreset to session/create. It
cannot call that method -- that would create or re-adopt a session the board
only meant to continue -- so assertReusedPreset reads the recorded value
through session/projections (a read that resolves no Agent) and fails closed
on a mismatch or an unreadable value.
Refs #1708
The harness serves the GUI with one document base, injected as a base href
pointing at the entry directory, so a reverse proxy mounting the GUI under a
prefix is a supported shape. The official client already posts
document-relative routes; every plugin browser half wrote root-absolute
literals, which resolve against the origin and miss the mount.
Drop the leading slash from each same-origin route (fetch and EventSource
alike) across skill-explorer, git-graph, usage, session-archive,
plugin-manager, market, web-all, task-board, and remote-web-ui. task-board
derives CLIENT_API_PREFIX from the host's root-absolute TASK_BOARD_API_PREFIX
so the two halves cannot drift, and the skill-explorer contract drift guard
normalizes the relative literal before comparing it with the host ROUTES
table while asserting no absolute route returns.
Refs #1707
Generated-artifact conflicts (packages/dsh-web-all/lib/client.js.map and scripts/lib-artifact-fingerprints.json) were resolved by regenerating rather than hand-merging: pnpm build on the merged sources, then pnpm libs:write. The rebuilt aggregate bundle carries both the usage balance-origin fix and the child mount-claim release.
The client module system replaces a rebuilt application entry in place:
it disposes the old fiber and re-applies the new code without reloading the
page. The aggregate's mount registry never released the claims of the dead
instance, so its successor skipped every family child — usage, market,
plugin-manager and session-archive vanished from the page and
dsh-task-board's DOM outlived the dictionaries it renders through, leaving
a live sidebar row and board showing raw keys (entry.label, board.title,
board.showSubtasks).
- mount-children.ts releases exactly the claims its own fiber added, so a
successor mounts the children again while a sibling instance keeps its own
- dsh-task-board binds its DOM mounts and its settings subscription to the
fiber, matching the discipline dsh-ssh already followed
- regression specs for both, verified to fail against the pre-fix sources
- live A/B on the running host: an in-place replacement of the pre-fix
artifact kills the whole family in an open page, and the same replacement
on the fixed artifact keeps one row, the usage card and the complete
settings nav with no reload and no duplicates
Agent Note: .agents/notes/implemented/bug-fix/2026-09-02-aggregate-client-children-mount.md
Closes#1700.
The lan-required banner told every user to open "Settings -> Web Plugins ->
Remote access". That section only exists when dsh-web-settings is
installed (the family aggregate): a standalone install places the same card
on the plugin's row under the official Plugins page, so following the hint
led nowhere. The copy now names both deployment shapes.
Verified: pnpm --filter @linxin666/dsh-remote-web-ui test (429 passed) and
pnpm i18n:check (zh/en/ru keys stay at parity).
Closes#1688.
`deepseek` is both the official catalog entry and a common id for an
OpenAI-compatible reseller (Alibaba Bailian, ...) whose profile points
`baseURL` at another host. Family alias folding does not merge those - both
are real accounts - but the DeepSeek adapter's balance endpoint is a fixed
official origin, so probing it for the reseller printed the official
account's money under the reseller's row (and merged its usage by family).
The balance half of an adapter may now declare `origin`
(`DEEPSEEK_API_ORIGIN` for DeepSeek), and `balanceAppliesToRoute()` refuses
that endpoint for a route whose configured `baseURL` resolves elsewhere:
such a route issues no request and renders `balanceSupported: false`, the
card's existing unsupported state. A route pinning no `baseURL` (the
catalog alias resolving the family's own credential) still applies, and
adapters whose endpoint follows the profile host declare no origin.
The trade-off is deliberate: the adapter cannot know a reseller's balance
endpoint, and a wrong figure is worse than an absent one.
The balance card now lists such a row with its own explanation instead of
filtering it out behind one generic line.
Verified: pnpm --filter @linxin666/dsh-usage test (11 files, 136 tests),
typecheck. The new service-level test fails against the pre-fix code.
Subtasks and cascade runs
- TaskRecord.parentId with a Host-owned lineage gate (existence, cycle, and
the maxSubtaskDepth limit, 1..3, default 1) shared by every caller in
src/core/subtask.ts.
- run/rerun open one execution per participant under a single runGroupId; a
parent settles only after its own turn and every direct subtask, with
failure dominating. Cron takes the same path.
- Creation inherits the parent workspace, preset and model but NOT the
permission: the binding resolves from the live ancestors at launch, so
detaching a subtask cannot leave a confirmed elevated card behind.
- Subtree archive/restore, delete-with-subtasks refusal, link repair on load,
and the client affordances (link/detach a subtask, badges, parent-only
default view with a roll-up badge, filter auto-expand).
Agent tools
- Eight model-facing tools (task_board_list/get/create/update/set_parent/run/
manage/schedule) drive the same Host ledger the browser drives. They call
no separate business rules, so every gate holds identically.
- Registration follows the enabled switch and resolves the tool registry as an
optional service, so a deployment without one still mounts the board.
- There is deliberately no confirm-permission tool: the human gate stays human.
Agent Team execution (task-level opt-in)
- teamRun switches one cascade from one session per participant to a single
Lead session plus one teammate per subtask, spawned through the optional
agentTeams service; each teammate session is attached to that subtask
execution so the ordinary session monitor settles it.
- Every run states its shape in the launched prompt: the independent sessions
a cascade opens, or the Lead and its named teammates.
- Fail closed: a missing service refuses a manual team run by name (and is
recorded for a scheduled one), and a subtask that pins its own above-default
permission is refused because a teammate cannot carry that pin.
Verification: pnpm typecheck, pnpm test (task-board 551 passed),
pnpm test:standards, pnpm docs:check, pnpm i18n:check, pnpm emoji:check,
pnpm aggregate:check, pnpm libs:check. Takes effect after the user restarts
the running DSH service.
A family row mounts the aggregate shell, so the row's Config belongs to the shell and
the Host settings surface served no form for it: a settings save on a family entry was
rejected with 'No configurable plugin entry', and the browser cards fell back to a
config form the host never served. The shell now declares Config = z.any().volatile():
- 'any' keeps the wrapped plugin's fields at the row config root, where a standalone
install of that package keeps them, so one profile shape serves both mount paths and
no family client needs a path prefix. scripts/aggregate.mjs emits that flat shape.
- '.volatile()' puts the entry on the settings surface at all (a schema with no volatile
field is skipped) and admits a write to any path. The root-volatile update also goes
through the live path: the Loader commits the new config into the running entry's
reference and emits loader/volatile-update, and the shell re-mounts the family plugin
with the committed config instead of remounting the entry.
The wrapped plugin's own schema still validates the values it receives, so a refused
value leaves that one row degraded rather than taking the boot down. The rebuilt lib/
replaces the superseded shell chunk, and the recorded fingerprint covers the new sources.
The preset center now lives in its own repository (dsh-presets) and is consumed
here as the published npm package @linxin666/dsh-client-ui-preset-center, the
way the skin center, the pet plugin and the community plugin index already are.
- packages/dsh-preset-center leaves this tree (plugin sources, tests and the
presets/ catalog; history stays in this repository);
- the aggregate mounts it through an external rows: entry (web-ui-preset-center)
with a hand-written ^0.4.1 range, and ./preset-center stays in the exports map
as a tombstone so profiles written against the patchFrom era keep importing;
- market-inputs.lock.json gains a presets input pinned to the
satellites/dsh-presets gitlink, and scripts/market-build reads
.market-inputs/presets with the in-repo fallback kept for the fixture tests;
- scripts/sync-shared.mjs stops mirroring five host helpers into the package
(101 -> 96 copies); the lib fingerprints, coverage baseline, test-standards
baseline, runtime-deps scan and i18n audit each shrink by one package;
- the contribution gate has no exception left: reject-non-content-pr.yml
redirects a 新预设收录 declaration to dsh-presets, and the PR template,
CONTRIBUTING.md, PR_TRIAGE.md and ISSUE_TRIAGE.md now state that this
repository accepts no external contribution directly;
- docs, the family-satellite owning note and the two preset notes follow, and
the new Agent Note records the split.
Gated by: typecheck, aggregate:check, libs:check, sync-shared:check,
runtime-deps:check, i18n:check, emoji:check, docs:check, market:check,
test:standards, test:scripts (340 tests), test (every package) and build.
Not yet safe to merge on its own: the satellites/dsh-presets gitlink needs that
repository to exist on GitHub, so the satellite commits and this pin have to be
pushed together.
Issues #1675, #1696, #1682, #1678, #1677, #1690, #1672.
remote-web-ui
- #1675 (boot crash): writeLanBind only stripped an empty `[]` placeholder,
so a profile the plugin manager had rewritten into a NON-empty flow array
got the managed block concatenated onto it and the file became two root
documents (YAMLException 7:1 at dsh web startup). A flow-style root is now
re-emitted in block style before the block is appended, which keeps exactly
one valid document; an unparsable base is refused with the file path rather
than written half-valid. Adds the `yaml` dependency, the same document
round-trip dsh-client-ui-plugin-manager already uses.
- #1696 (device-authorization loss): the constructor restored the persisted
device table and immediately capped it at the cap it happened to hold (the
schema default of 4 until the saved settings row is applied), then a normal
heartbeat/sweep wrote the trimmed table to disk. Restore now keeps every
valid persisted session; the cap is enforced in accept(), which evicts FIFO
until a slot is free.
- #1682 (desktop GUI fenced): the local-page test only compared the hostname,
so the DSH Desktop shell's `dsh-app://app/` page was treated as a LAN/tunnel
origin and the whole GUI was replaced by a pairing fence the shell can never
satisfy (it strips every set-cookie). The predicate now also accepts a page
on a desktop delivery scheme or one the official transport already declared
the host owner, and the inlined boot script applies the same rule. The
transport hook alone never unfences a network page: this plugin's own
device-gated landing grants the same hook to a paired LAN/tunnel remote,
and that page must keep riding the gated channel.
- #1678 (rail seat misaligned): the shell renders sidebar.footer.action as a
centered horizontal row in the rail too, so a second registrar made the seat
78px wide against a 35px icon column. A rule scoped to the collapsed frame
and anchored on this plugin's own data-rail occupant stacks the seat.
- #1677 (registry probe burst): the update status call fanned out ~20 registry
probes at once; the fan-out is now a bounded pool of four that preserves the
caller's order.
git-graph
- #1690: auto-isolation probed `workspaces.startSession`, which 0.1.7 moved to
the navigation service `uiWorkspace`. The probe returned null on every boot,
so the feature stayed disabled and warned on every page load. The wrapper
now reads both faces (uiWorkspace for startSession, workspaces for the rows)
and degrades only when one is genuinely missing.
task-board
- #1672: a gateway that withdrew the strict session/list definition spent the
full service-unavailable retry window and then printed a console error the
user has no action for. That condition is now classified as a quiet,
warn-once roster degradation.
Every change ships with tests that pin the new behavior, including the
regression cases the reports described. The aggregate client bundle and the
committed lib fingerprints are rebuilt in this commit because the child
client sources the aggregate inlines have changed.
Verified: pnpm typecheck, pnpm test, pnpm build, pnpm test:standards,
pnpm docs:check, pnpm i18n:check, pnpm emoji:check, pnpm aggregate:check,
pnpm market:check, pnpm libs:check all green.
Node 25 defines localStorage on the global object, and without a usable
--localstorage-file its getter returns an empty object: getItem, setItem and
clear are all missing. Vitest's jsdom environment fills in globals only where
the name is still free, and under vitest window is globalThis, so that stub
survived into every DOM test: the packages whose code reads storage took their
degraded paths instead of the code under test, and dsh-task-board's specs failed
outright on window.localStorage.clear.
The shared setup now installs a Storage in a DOM environment and removes the
stub where there is no DOM, which is what Node 22 leaves behind. dsh-git-graph
and dsh-task-board join its copy manifest, and dsh-skill-explorer's own setup
imports the shared source.
Measured on Node 25 against Node 22, the repair is what moves the numbers back:
dsh-git-graph 79.53 -> 81.32 lines, dsh-remote-web-ui 69.66 -> 70.34,
dsh-skill-explorer 77.61 -> 80.22, dsh-web-settings 78.66 -> 82, with branches,
functions and statements following. Coverage no longer depends on the
interpreter that ran the suite, which is what makes one recorded baseline mean
the same thing in both.
The whole suite is red in this package on Node 25. It defines a localStorage
global whose getter, without --localstorage-file, returns an empty object, so
getItem, setItem and clear are all missing. Vitest's jsdom environment
populates globals only where the name is free, and under vitest window is the
same object as globalThis, so window.localStorage stayed that stub: the two
specs that touch storage failed here and pass on Node 22, which defines no such
global and lets jsdom's own storage through.
The setup file gives a DOM environment a Storage, and removes the stub where
there is no DOM, so a host-side spec sees what it sees on Node 22. Nothing
changes on Node 22: jsdom's storage is already in place and the stub does not
exist. The specs keep their own intent, and the ledger no longer logs a caught
TypeError from the stub.