Review found three ways the takeover could let two writers overlap.
- A claimant that read an exited PID could remove the lock of a new
holder that reused that PID before the claim. The claimant now probes
the PID again under the claim, after re-reading the record.
- The browser Worker's process shim reports its own PID as absent, so a
second writer in the Worker took over the first one's live lock. A
record naming the contender's own process is never taken over.
- Takeover proves only that the holder exited, while a pnpm run it
started can still be writing the profile. Each profile operation now
records its pnpm run in .plugin-manager/run.json and removes the record
when the run ends; an operation that finds a record waits for the run
within the existing tree bound and otherwise refuses, naming the
process and the record, without running pnpm.
A failed removal of the exited holder's lock now waits instead of
failing the acquisition, the probe range is stated as int32, and the
tests inject exited PIDs instead of reusing real ones, which Windows
reissues at once.
The tools pipeline's ask decision now carries an optional displayReason
that ToolRuntime forwards to the approval service, so the Auto review
prompt is localized while the audited reason stays English.
Adds the auto-review-approval Web snapshot for a user rejection after a
reviewer denial and for a reviewer failure, and updates the permission
preset pages, READMEs, Agent Notes, and child-agent JSDoc that still
described the shared never bundle.
The takeover note said no Windows binding exists for a kernel-released lock; the Session write lease already holds a named semaphore there through koffi.
skippedProfileBundles recomputed the selected-minus-loaded bundle set from
the manifest, a second home for what loadProfileDirectory already records.
Runtime resolution and generateConfigSchema now read
profile.skippedBundles, so generateConfigSchema no longer reads the profile
manifest and drops its binName parameter.
Optional request extension fields had no size bound. A very large field
could exceed the runtime's string limit while the request was serialized,
which failed every attempt, including retries.
- Bound the extension payload carried by one request (8 MiB by default,
configurable) and send the remainder with later requests.
- If the combined request still cannot be serialized, send the base
request without extension fields and log which fields were omitted.
- Add regression tests and update the related references and decision note.
The seat kept the pending row in local state, so a stale settlement after
connection/reset could clear a newer selection's spinner, a remount lost
it, and /model popup selections never marked a row. ModelDirectory now
publishes the submitted selection as pending under its generation guard.
The trigger spins whenever a selection is pending, so leaving the row's
pane keeps the feedback visible.
The hostname guarded only writers on several hosts sharing one file,
which no supported deployment does, and macOS changes its hostname when
networks change, so locks from before the change could not be taken
over. The nonce closed a race that needs the PID space to wrap between
two reads. Keeping the `<pid>\n` record lets processes of both releases
interoperate.
A process that ends without running withFileLock's finally left
<file>.lock behind, and every later writer timed out until an operator
deleted it. `dsh plugin` interrupted with Ctrl-C, `dsh web` exiting
right after disposal while a cancelled install still restores files,
and any crash all left the profile package lock this way.
The lock now records its holder's PID, hostname, and a nonce. A
contender that finds a record from this host whose PID a signal probe
reports absent claims that record through a wx-created claim file,
re-reads the lock, removes it only while it still holds the record, and
retries at once. A PID-only record from an earlier release counts as
written on this host, so locks left before this change are taken over
too. Empty, incomplete, foreign-host, and live-holder records are still
waited for.
loadProfileDirectory printed each skipped bundle on every call, and startup,
runtime resolution, the config editor, the plugin manager, and HMR all load
the profile, so one incompatible bundle printed the same warning six times at
startup and again on each Web page load. Loading now returns the skips in
Profile.skippedBundles, and the CLI and Desktop launchers print them once per
start with reportSkippedBundles.
session.selectModel can take seconds while the Host persists the default
model. The composer seat disabled its rows with no other feedback; the
chosen row now spins in its check slot, and the trigger spins in place of
its chevron while the menu is closed.
- State the contract precisely: the cut never introduces a lone high
surrogate, and an unpaired surrogate the input already carries is not
repaired.
- Document the new export in the package README: a character-budget preview
section naming the persistent bash and pwsh shells and str_replace_editor,
plus a Known Limitations bullet that no longer contradicts it.
- Drop the assertion restatement from the two shell test comments.
- Let the str-replace test helper reuse a root so the clipped context reads
its file inside its own working directory.
The shipped Web bundle gives every live root Agent the Host Schedule
tools, and every other scenario in this file that boots the shipped
base/web bundle with the minimal preset expects ['bash',
...SCHEDULE_TOOLS]. This case uses the same bootWeb composition and adds
only a settings patch, which does not change tool composition, so its
['bash'] expectation failed on every PR.
A reviewer denial now returns the tools pipeline's ask decision when the
Session's approval policy is ask, so the user can approve, reject, or
cancel the call. Selecting Auto writes the ask policy; a recorded Auto
selection still matches never, which delegated children pin, so their
denials stay final.
A reviewer failure now fails the call with its specific error instead of
presenting it as an Auto review denial.
Keep the timing rules that apply to every selector in the create
description once, move the cron day-field and fixed-rate alignment rules
onto their parameters, and drop parameter formats, result fields, and
Host compare-and-set details from the tool descriptions.
Drop the extension-less path hint from read_image and the
returns-before-stopped note from job_kill, and describe send_message
delivery without "resumes a turn". image-compaction lowers its context
window by the 30-token tool-schema estimate reduction.
Tool guidance sections keep only what the tool definitions do not say.
Apply review: reword interrupt_agent and list_agents, carry "a rejected
escalation stays denied" in the rejection error, pin
sandboxPermissionsDescription, and fix stale README and Agent Note
statements. Record the tool-definition rules in agent-experience.
Compaction threshold snapshots shift their context windows by the
370-token system-prompt estimate reduction; escalation-rejected moves to
a v4 fixture for the new rejection text.
Delete constraints the model learns from the call result, shorten
implementation detail, and move per-parameter rules into the owning
parameter descriptions. Bash, pwsh, and fs share one
sandbox_permissions description in dsh-sandbox.
The standard preset's first-turn prompt drops from 7985 to 6551 tokens.
Compaction threshold snapshots lower their context windows by the
1548-token tool-schema estimate reduction so each trigger keeps its
position; their fixtures move to v4 generations.
Broaden the skill description to cover enabling a shipped plugin that a
bundle leaves disabled, add the workspace-bundle recipe for it, forbid
hand-editing profile files that install_bundle owns, and verify installs
through the install result and cordis_inspect_query instead of logs,
process lists, or paged list_plugins calls.
* feat(approval): localize prompts and allow consent for one turn
* test(approval): update cast baseline and Web PTC prompt pin
* fix(approval): cover PTC language and clarify turn consent
* fix(approval): group turn consent under allow menu
* fix(approval): keep turn consent in memory only
* test(snapshot): refresh owners for merged justification guidance
* fix(approval): drop duplicate allow-once from the allow menu
* refactor(approval): derive turn consent expiry from one check
Replace the turn approval bookkeeping (revocation state, incremental log
cursor, and turn-signal listeners) with one side-effect-free expiry check
over the log and lifetime flags. Events now only withdraw open
presentations early; the expiry check decides whether an answer or turn
consent still applies, including for Sessions without event publication.
* refactor(approval): drop one-turn consent and keep localization only
Remove the allowed-turn answer, the live turn-consent state, the allow
menu, and their tests, notes, and recorded Web scenario. The approval
service returns to the master request path; the request keeps the
localized displayReason for presentation, and the card resolves it in
the active UI language.
* test(snapshots): record the localized justification schema text in new scenarios
---------
Co-authored-by: Turtle <[email protected]>
Co-authored-by: Kaige-Gao <[email protected]>
#5108 added this case against a master where the shipped Web bundle had no
Schedule rows; #5123 then made Schedule part of the default Web composition.
Every other minimal-preset session in this file already expects bash plus
the four Schedule tools, so this one does too.
The shipped Web composition gained the four Host Schedule tools, and the
other four minimal-agent assertions in this file already expect them. The
legacy profile-patch scenario still expected bash alone, so master's E2E
lane failed on it. Align it with its siblings.
Conflict resolution:
- docs/config-catalog and docs/module-graph: regenerate with the current
generators, which write the bilingual pages together.
- The conflict came from master's new short-fingerprint pairing records: our
branch still carried the previous full-hash records. Take master's record
format, then re-record the output-retention README pair after regenerating
its two sides.
Broaden the skill description to cover enabling a shipped plugin that a
bundle leaves disabled, add the workspace-bundle recipe for it, forbid
hand-editing profile files that install_bundle owns, and verify installs
through the install result and cordis_inspect_query instead of logs,
process lists, or paged list_plugins calls.
Each record rendered its whole saved prompt, so a long instruction on a
recurring task filled the records tab with the same text once per run.
A prompt longer than two lines now shows two lines and an Expand toggle;
the toggle appears only while the clamp hides text, and a collapsed prompt
re-measures when the panel width changes.
Also drop the stale comment about the removed message-id anchor.
The shipped Web composition now puts four Schedule tools on every live root
Agent, which pushes a full-preset `cordis_inspect_query` answer past the
12,500-token inline budget, so the spill policy retains its head and tail with
a spill path instead of the complete JSON.
Refreshed owners publish a v4 generation, so every scenario that names
another owner's Session fixture must name that owner's highest generation.
The shipped Web composition also puts the Host Schedule tools on every live
root Agent, so the minimal-preset tool-table assertions include them.
* feat(experimental): ship every experimental capability as an optional bundle
dsh-base mounts the registration-only browser-use and computer-use
services; the Playwright MCP, Chrome DevTools MCP, native and MCP Cua
Driver providers, Auto review, and the Inspector become one-row bundles
listed in OPTIONAL_BUNDLES with an icon and locale display metadata.
verify-default-product-isolation requires both for every optional
bundle. Python PTC and Stagehand stay explicit compositions.
* feat(experimental): keep optional bundles inside the install closure
Drop the Playwright MCP, Chrome DevTools MCP, and native Cua Driver
bundles: each is a runtime dependency of every install and would add
about 85 MB of provider runtimes whether or not enabled. dsh-browser-use
leaves dsh-base with them. The three packages keep their locale display
metadata for component rows.
* feat(experimental): drop the Cua Driver MCP bundle and leave dsh-base untouched
* test(web): refresh Official-group goldens for the four optional bundles
* fix(plugins): use an uninstalled bundle as the install example and pin optional-bundle metadata
The install guide's example is now @deepseek-ai/dsh-subagent-codex,
a published bundle no shipped profile carries, since Auto review is an
installed optional bundle. The optional-bundle spec guards its corpus and
pins each bundle's title, description, and icon in both languages; the
Inspector's Loader test mounts its real bundle patch, its Client names
the reload a hot-enabled page needs, and its description states the
loopback DevTools endpoint's reach.
* test(web): refresh GitHub install recovery placeholder snapshot
The Web bundle now mounts `time-context` and `schedule` and leaves the
`ui-schedule` client row enabled, so the Automation tasks page, the Session
reminder catalog, and the `schedule_*` tools work without an overlay. Both
packages join the bundle's declared dependencies, and the example overlay is
deleted: re-inserting its rows would mount both Host plugins twice.
The fixed-rate floor drops from 300 to 60 seconds across the Host decoder,
the create and update validation, the model-visible tool schema, and the
client's per-unit floors.
Web recorded sessions and header sidecars are refreshed for the readings
`time-context` now appends; `normalizeWebSessionVolatiles` tokenizes their
sampled instant, browser zone, and elapsed duration so replay stays stable.
The sidebar toggle is flush with `.logoRow`, which clips its own overflow, so
the standard outward ring lost every pixel that left the row: the reviewer saw
the ring cut on the toggle in both the expanded header and the settled rail.
The 36px rail toggle is also 1px wider than its 35px row, so the control's own
right edge was clipped there. Draw this control's ring inside its own box and
stop clipping the row once the rail has settled; the ring keeps the standard
`--dsw-focus-ring-width` colour and width, and pointer modality still resolves
`--dsw-focus-ring-color` to transparent.
`focus-rings.e2e.ts` asserts the whole ring against every clipping ancestor in
both layouts, in both themes.
Only the ui-theme README pairing record conflicted; re-recorded. The stylesheet
corpus spec passes over master's new client sheets, so no ring needed realigning.
`.selected` still named `--dsw-alias-brand-primary-new-colorprimary-new-color`, the
corrupted token left by a botched rename; its dark value is `deepseek-450` while
every focus ring now uses `deepseek-400`. The selection ring reads the clean
brand-blue alias instead, so one blue covers selection and focus.
- Only the definitive Windows session-end message bypasses the quit
confirmation; the macOS power-off flag clears on the next focus or show
because another application can cancel that shutdown.
- A bypassing quit disposes the pending confirmation so a late inspection
result opens no box and an answered box cannot approve.
- Cancelling a quit that closing the welcome window started shows the
welcome window again.
- The first-hide notice records its marker only after delivery and
withdraws it on failure; the Windows quit dialog uses the multi-size ICO.
- windowShown settles when the awaited window is destroyed.
- Cross-link the partially superseded window-menus note; tidy docs.
Closing the main window hides it on macOS and Windows; the page and the
Host keep running and the next show resumes the same document. Windows
gets a permanent tray icon (open / quit) and a one-time notification the
first time the window is hidden after installation.
Every ordinary quit entry asks the Host over a new quit-inspection IPC
request for active tasks (same rule as the update restart check) and
armed scheduled reminders (schedule family of workspace/session-activity
for loaded sessions). Nothing to interrupt quits silently; otherwise a
native message box explains what stops, with Quit as the default and
Cancel on Esc. Inspection failure or a missed two-second deadline counts
as running tasks. The installer restart, fatal recovery, the development
restart command, and OS session end skip the confirmation.
Also: defer the downloaded-update confirmation while the window is hidden,
render the tray ICO from the vector icon, and point the installer's
running-application message at the tray.
Closes#4013
Resolve local image paths through the authenticated dsh-app file route and allow that route in rewritten Markdown images. Cover desktop rendering, previews, encoded filenames, and unsupported origins.
Fixes#5105.
* feat(desktop): distinguish account quota alerts from API key notices
* docs: condense architecture introduction within existing budget
(cherry picked from commit 01aacaee4d48fd7f4d00acb5e23758084d3f224e)
* test(desktop): cover quota notice lifecycle and remove unknown casts
(cherry picked from commit 7fd09407c482d1f0e30f24ba0ebf23cd844ac9a1)
* chore(account-quota): align tests, catalogs, and bilingual pairs with the account plugin
* fix(ui-settings-account): pass the Platform retry label to the account top-up overlay
* fix(chat): preserve quota failures and global notices
* fix(account): keep active top-up pages across quota notices
* fix(account): share platform page ownership and release quota holds
* fix(account): defer notices behind the shared Platform page
The single native Platform view covers the document, so a quota failure that arrives while a Settings-opened page shows is deferred rather than retained: the newest failure still replaces an older notice and its Modal appears after the viewer returns if still current. Top up remains the retained case, releasing its hold and page request with the entry.
* test(account): align quota fixture with bonus actions
* fix(account): preserve quota notice and Platform return focus
* fix(account): preserve onboarding return reads and failed notices
* docs(account): clarify page opener return ownership
* test(account): drive bonus acknowledgement retry deterministically
---------
Co-authored-by: liyao <[email protected]>
Co-authored-by: lsdsjy <[email protected]>
* fix(plugins): name the default install source by the registry it names
The add-plugin dialog showed "默认安装源" for pnpm's own registry, which
told the reader nothing about where the install would download from. Name
npm's own registry as the official npm registry, and any other registry
pnpm's own configuration names by that registry's name or host, so the
folded control reads by the registry the install actually asks.
* test(plugins): cover comparing an unparseable registry address
---------
Co-authored-by: Turtle <[email protected]>
The archived filter reaches the row toast through the registration's declared
store instead of a registrant-owned view hook, per the cross-entry view-state
rule. The README names the toast action by its actual label, and the
unarchive e2e switches back to Hide archived so it again proves the restored
row is an ordinary row in the default view.
The view-options filter is now hide/all/only radio items, the
archived-only view drops Workspaces without an archived Session, and
tree children of a dropped Workspace nest under the nearest shown
ancestor (#4854).
Only the bilingual pairing records conflicted. Master moved Agent Note records to
per-heading hashes, so this branch's note record was re-recorded in that format;
the ui-primitives and ui-theme README records were re-recorded unchanged. The
stylesheet corpus spec still passes over master's new client sheets, so no ring
needed realigning this time.
The probe needs a value that cannot be inferred from the prompt, not protected information. Describing that generated UUID as a private secret made the API test depend on an unrelated disclosure decision. Use an explicitly synthetic sample label and a benign formatting update while preserving execution audits, first-continuation cache assertions, and disabled retries.
Open the running Think disclosure explicitly when a process group also contains tool updates, and keep the fork process summary expandable in its ARIA expectation. Record the Goal request-series marker in a V4 fixture successor while preserving the historical V3 recording. Production behavior and the scroll assertions stay unchanged.
Record explicit request-series starts and the tool additions admitted when resuming a seeded parent with no tool declarations. Keep tool names and the shifted source-event reference alongside that activation history. Refresh only the affected owner-local outputs to the current writer format; retain historical replay fixtures and the unchanged child expectation.
Tool changes previously forced prompt consolidation even when the prepared
route could activate new tools through history. Rewriting the first system
message, or clearing an earlier appended prompt, reset the declaration
series and turned additions into immediate declarations.
Only routes without toolUpdate need that tool-triggered consolidation.
Capable routes append the full changed prompt and keep existing messages.
Explicit series starts, surface replacements, and unsupported prompt
routes still consolidate; retained-name definition changes still rebuild
tool declarations in Session.
Cover both update modes and reset paths, and pin the combined prompt/tool
transition through the shipped headless profile with shared schema goldens.
Run a real DeepSeek conversation through the built SDK profile and change the scoped tool registry through a model-invoked controller. Compare the first post-addition cache usage with the preceding request after unique user history has been cached; verify deferred declarations, chronological activation, and actual tool execution. Removal remains covered without requiring its cache prefix to survive. Disable test and provider retries so a warmed transition cannot hide a cache regression.
Log tool changes as developer messages independently of model capability,
fold committed headers and updates into Session.toolHistory(), and let the
LLM runtime construct deferred and retained declarations for routes that
declare a toolUpdate mode. DeepSeek serializes updates as system-role
tool_addition/tool_removal blocks with defer_loading and the tool-changes
beta header. An unchanged restored tool continues the series and is
re-offered through its recorded blocks instead of rewriting declarations.
Replace the obsolete rejection tests with wire assertions and add coverage
for the session fold, loop emission, route projection, config validation,
and the beta header. Chat and Trajectory changes land separately.
* 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.
master released 0.1.7-rc.1 while the package added by this branch still declared 0.1.7-alpha.2, which failed Dependency layout (verify-npm-install-layout), Pack npm tarballs (release members must share one version) and node 24 / static (constraints) from one cause. Merge origin/master and bump the new package to the root version.
The page carried a 28px top inset above its head row, so the window's topmost 28px on that page belonged to no drag region — the frame's former band covered it, and the per-row mark did not. The inset moves into the head rows themselves (.pageHead and .detailTop), which now start at the window's top edge while the content keeps its position. The browser lane pins each row's top edge at zero and the negative control confirms both assertions fail on the pre-fix sheet.
* fix(plugins): deny incompatible bundles and clarify compatibility refusals
A bundle package is not a plugin row, so startup admission never read its
own DSH peers: a bundle installed outside DSH or before a DSH upgrade still
applied its patch layer. loadProfileDirectory now skips such a bundle like an
unreadable one unless the profile exempts that exact pair.
A post-install refusal in a profile without a lockfile reinstalls from the
restored manifest instead of failing a frozen install and leaving the
rejected packages in node_modules.
Compatibility warnings now name the `dsh plugin allow-version` command for
CLI-managed profiles, startup denials name the row id instead of a resolved
module URL, and the CLI no longer reports "pnpm failed" when pnpm never ran.
* feat(plugins): report incompatible versions as typed refusals
Compatibility checks now yield the refused packages as data instead of a
profile-specific sentence. runProfilePnpm returns them as
PackageResult.incompatible, and the plugin manager refuses with the
incompatible-version code carrying the same records. Each surface renders
them: the CLI prints the exact allow-version command for its profile, the
Web plugin manager words them through its locale dictionary, and app-boot
keeps one neutral English diagnostic for logs and startup stderr.
* fix(plugin-manager): drop the agent exemption hint from the incompatibility reason
Review caught two real gaps in the previous commit.
The rename dialog's no-op check compared the localized display title, so
on an automatically titled Workspace the seeded prefill equalled it and
Rename stayed disabled — the user had no direct way to pin the visible
name. The dialog now seeds the draft with the label on screen while the
stored title decides whether confirming saves, and the duplicate check
excludes the target by Workspace identity instead of by title, since the
seeded label equals the row's own displayed title.
The registry took the automatic title from the canonical path, so a
symlink at the first-use path titled the Workspace after its target and
the default lost its localized label. The title now comes from the
requested directory's final segment; the path stays canonical.
Also: drop a rotting file count and the stale localized-path wording from
the Agent Notes, record the two cwd-derived Session label fallbacks as
known consequences, trim the WorkspaceBrowser memo comment to the
non-obvious part, and remove the deleted request type from the
hand-maintained catalog LINK_MAP.
* feat(web): restore the process-row polish on the grouped work details
The sweep highlight, shared row colors, expanded-row spacing, and the
12–24px font-size range were developed against the pre-revert work-details
implementation. Re-apply them to the grouped implementation that replaced it.
- TextShimmer keeps its active-driven span and now paints a masked sweep
overlay in the theme's `--dsw-alias-label-shimmer` color, so the base text
inherits the caller's color and hover changes.
- DisclosureRow owns the row color: tertiary at rest, secondary on hover,
with glyphs, titles, and summaries inheriting it; tool, skill, command,
reasoning, and workflow rows drop their own hover darkening.
- ChatView spaces process rows 8px apart and keeps 16px beside Assistant
responses; the grouped header sits 8px above its first member.
- The Settings font-size stepper accepts 12–24px, and the grouped header's
leading box and glyph scale with the content font-size delta.
* fix(web): restore blue running status and refine process disclosures
* fix(web): finalize running snapshots and whale animation
* fix(web): preserve disclosure layout and repair CI fixtures
Resolving packaged-app icons grew the embedded C# until the generated
-EncodedCommand line reached 32,883 characters for a 222-character path,
past the 32767-character CreateProcess limit, so association queries stopped
starting for long multibyte paths. Write the adapter to a private temporary
script and pass it with -File instead; the command line no longer grows with
the path. The temporary directory is removed once the call settles.
The icon fallback also reads the handler name the caller already fetched
instead of calling GetName again, and the README records that icon
visibility depends on resolving the Shell's indirect reference.
With no OS-marked default the split button labelled the first registered
application and marked it "(default)" in the menu, but the primary action
still asked the OS to resolve the association again; on Windows that can
prompt or open a different application than the button promises. Send that
application's id explicitly and keep the association path for the case the
Host actually marked a default.
The snapshot scenario now clears the platform's own default marker and
re-enters the file, so the association is read again instead of reusing the
answer already held, and it asserts the launched target rather than only the
labels.
Review threads: shrink READ_LANG_BY_EXTENSION to the suffixes whose own name is the better label (tsx/jsx/tf/tfvars/gradle) since the other 41 values already equal the language short name; label .hcl as hcl and .tfvars as tfvars instead of tf; accept .groovy alongside .gradle with its own short name; document that LANG_ALIASES also carries the persisted short ids; route the three inline alias lookups through grammarForHint; state that a short name may equal the grammar id; fix the util README wording.
Tests: cover .groovy, the four self-naming suffixes, and every id each suffix reaches through the shipped surfaces (the hand-synced LAZY_ALIASES list could not catch an alias whose target is neither boot nor lazy).
Compatibility checks now yield the refused packages as data instead of a
profile-specific sentence. runProfilePnpm returns them as
PackageResult.incompatible, and the plugin manager refuses with the
incompatible-version code carrying the same records. Each surface renders
them: the CLI prints the exact allow-version command for its profile, the
Web plugin manager words them through its locale dictionary, and app-boot
keeps one neutral English diagnostic for logs and startup stderr.
The retained-tab-layout note still stated that existing window-drag region pulses remain; this layer's pulse is gone and the shell watcher replaces it. Rewrite both sides and re-record the pair.
The recall watcher now tolerates a short quiet window so a report that arrives before a CSS transition moves anything cannot end the loop on its first unchanged sample, re-arms on a transition or animation that touches a marked row, and re-arms on a child list change in a container holding one. A dock picture withdraws its strip's drag claim while a pane floats, because a float sorted before that row would otherwise sit earlier in the document and lose its own subtraction. The settings panel closes when an onboarding step appears, since the step's overlay marks only #root inert. Prose and the two Agent Notes record the Windows narrowing of the Platform overlay return bar, the collection rule, and the rows' one home in the ui-theme manifest.
Node's module-hooks thread serializes resolver errors and returns the stack
accessor as a read-only configurable data property. Rewriting the routed
importer then assigned error.stack, threw a TypeError, and replaced the
original ERR_PACKAGE_PATH_NOT_EXPORTED or ERR_MODULE_NOT_FOUND error, so
plugin metadata reported a failure instead of falling back to package fields.
The importer rewrite now redefines a rejected stack value in place and
rethrows the original error with its code.
Master's newest commits add `.close` in ImageLightbox and `.imageButton` in
MarkdownText, each declaring the brand token directly. Both now read
`--dsw-focus-ring-color` through `--dsw-focus-ring-width`, as the stylesheet
corpus spec requires of every client focus rule.
A bundle package is not a plugin row, so startup admission never read its
own DSH peers: a bundle installed outside DSH or before a DSH upgrade still
applied its patch layer. loadProfileDirectory now skips such a bundle like an
unreadable one unless the profile exempts that exact pair.
A post-install refusal in a profile without a lockfile reinstalls from the
restored manifest instead of failing a frozen install and leaving the
rejected packages in node_modules.
Compatibility warnings now name the `dsh plugin allow-version` command for
CLI-managed profiles, startup denials name the row id instead of a resolved
module URL, and the CLI no longer reports "pnpm failed" when pnpm never ran.
The new no-default scenario cleared the macOS application catalog, which
does nothing on Linux: `gio mime` owns the OS default there, and the
Linux stub never wrote the launch log, so CI timed out waiting for the
application gesture. Clear the marker each platform actually reads, log
launches from both stubs, and compare the launched id against the
platform's own registration id.
Conflict resolution:
- docs/config-catalog: regenerate the English page; the Chinese page follows the
three shifted Config sources and the bilingual pair record is re-recorded.
- docs/module-graph: the generator rewrites all three artifacts together.
- packages/shell/tool-pwsh-persistent/src/index.ts: keep master's rewrite of the
prompt handling and re-apply the surrogate cap and its import.
- The three package.json dependency entries follow master's workspace:* range
convention, and the lockfile importer specifiers match them.
Master's newest agent-preset sheet carries both the new `.helpButton` and the
retained `.iconButton`, whose focus ring still declared the brand token
directly. The stylesheet corpus spec rejects that form, so it now reads
`--dsw-focus-ring-color` with the brand-token fallback.
ui-theme/README.md, README.zh.md and README.i18n.yaml conflicted again: master
extended the `design-platform.css` sentence with the `--dsw-alias-file-diff-*`
palette. Kept that sentence together with this branch's `focus.css` paragraphs
and the seven-sheet entry list, then re-recorded the pairing hashes.
* feat(preset): add creator-mode plugin practices skill
Creator mode mounts packages/preset/agent-preset/skills. The new
cordis-plugin-practices skill states Harness-specific principles and
concrete rules for plugins that survive upgrades and avoid polling or
rescanning session logs: waterfall delegation, restrict/guard over
assemble, agent.ctx ownership, no new session event types from profile
plugins, session projections, and slot-based UI.
* refactor(preset): fold plugin practices into cordis-plugin-development
A separate skill competed with cordis-plugin-development for the same
trigger. The practices now live in references/practices.md, read from
procedure step 2 before choosing an extension point.
* feat(preset): require Harness components and tokens for plugin pages
A creator-built management page served as an iframe with its own
stylesheet could not match the Harness design. The practices reference
now requires slot-rendered React pages built from ui-primitives, theme
tokens from Theme inspection, and light/dark checks in the real slot.
* feat(preset): make host UI consistency a principle and a verification step
Adds principle 6: the rendering surface decides whether a plugin can
match the Harness UI, so it is chosen before any view. Step 4 of
cordis-plugin-development now verifies primitives, token-only styles,
and light/dark rendering beside a comparable host page.
* feat(preset): prefer theme tokens over pre-stable ui-primitives
ui-primitives is a pre-stable platform module; creator plugins are plain
JS without type checks, and a throwing component blanks the slot entry.
The practices reference now treats tokens as the default way to match
the host, limits primitives to behavior that is costly to reproduce, and
tells the agent how to detect a slot entry crash after an upgrade.
* feat(preset): forbid runtime imports of Harness Client packages in plugins
Creator plugins must assume ui-primitives and other Client packages break
without notice. The practices reference now has plugins copy markup, CSS,
and behavior from primitives, or inspect the rendered host control, and
keep --dsw-alias-* tokens as the only shared styling dependency.
* fix(preset): correct plugin practices against runtime behavior
- agent.ctx registrations need a plugin-owned disposer too; plugin
unload does not remove them.
- Timers start work with followup(); inject() does not wake the agent.
- inject() logs agent/inbox/spliced at call time; agent/request cannot
change messages.
- Guards are synchronous; awaited decisions return ask from
tools/pre-execute.
- Chat node renderers are keyed by kind; slot inject reinstalls on
declaration return.
skills.spec now keeps whole-file cordis-plugin-development references
below the pruner threshold as the read tool renders them.
The reported defect is the branch where the Shell lists associated
applications but marks none as the default: the split button promoted
"Show file location" to the primary action and hid the applications
behind the menu.
Record that state in the keyless document-preview scenario. The new
golden pins the primary action to the opening control, and the gesture
log asserts the main button opens the file while the menu's marked
default launches the first registered application.
Two focus treatments still named a colour of their own: the TurnNavigator
mark's tick filled with `state-business-primary` under a comment that already
claimed the brand colour, and the plugin inventory search input drew its focus
border in `state-business-primary` while the shadow beside it read
`--dsw-focus-ring-color`. Both now read the same ring colour as the rest.
`Tooltip` states why it takes `pointerModality()`'s wide answer while focus
rings read the narrower `data-input-modality` attribute the module publishes.
The ui-web source map gains the two window-drag modules and its boot row names the watcher install; ui-settings-general states that the settings panel portals beside #root. Both pairs are re-recorded. The ui-settings-general manifest keeps master's dependency order so only the two new dev entries appear in its diff.
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.
`welcome-startup.spec.ts` imports the unpackaged main entry, which now
requires `DSH_DESKTOP_PRIMARY_RUNTIME_DIR`. Stub it beside the neighbouring
`DSH_DESKTOP_*` values so the suite reaches the welcome startup it covers.
* fix(plugin-manager): bound pnpm runs so a silent child cannot hold the profile lock
A pnpm child that prints its completion line and then never exits holds the
profile write lock until the host is restarted, so every other session and host
on that profile fails after its own lock timeout (issue #4981). execa resolves
its promise only once the piped stdio has ended, so a run now completes on the
process's own exit and drains the pipes under a bound instead of awaiting their
end.
A service run whose captured output stays silent for idleTimeoutMs is
terminated, reports timedOut alongside its exit status, and is not retried on
the next registry; a run with inherited descriptors captures nothing and is
left unbounded for its operator.
* test(plugin-manager): type the mocked child and await the drain descendants
The inherited-descriptor fixture no longer needs its legacy `as unknown` cast,
because the mocked child is typed for the fields the operations read; the
retired baseline entry is pruned (verify-no-unknown-casts).
The drain specs wait for the killed descendant to exit before removing its
directory: Windows refuses to delete a directory a live process still holds.
* test(plugin-manager): stop the drain descendant deterministically
The descendant that holds the pipes now ends on a stop file the teardown
writes, and on its own after a minute, so the fixture leaves no process behind
when a kill cannot be delivered. Windows stops it through taskkill, and the
spec waits for the exit before removing the directory it ran in.
* test(plugin-manager): run the drain descendant from its own script file
Windows cannot carry a nested `-e` script through another spawn argument: the
descendant failed to start there, so the fixture asserted on a process that was
already gone. The descendant now runs from a script file the fixture writes.
* test(plugin-manager): cover the bounded drain everywhere and stage the real descendant on POSIX
Windows does not stage a descendant that keeps the inherited pipes open past
the direct child's exit, so the real-process cases run on POSIX only. The
mocked child now models execa's own completion rule — its promise settles only
once both pipes end — which covers the bounded drain on every platform and
still fails without the raw-exit race.
* test(plugin-manager): cover the drain failure path without a child process
An output consumer failure that the bounded drain cuts short is staged with the
mocked child as well, so the per-file coverage gate also holds where the
real-process cases are skipped.
* fix(plugin-manager): terminate the whole tree of a stalled run
A lifecycle script is a child of pnpm, so signalling pnpm alone left an
approved postinstall writing into the profile after the operation reported a
timeout and released the lock. A stalled run now stops its process group (or
Windows process tree) and waits for it to disappear before the caller restores
the files. The cut tail's close is told apart from a reading failure by where it
happened, so a failure from before the cut still surfaces.
* docs(plugin-manager): regenerate the config catalog after the rebase
The English catalog gains `idleTimeoutMs` from the regenerator, and the Chinese
counterpart carries the same interface line so the pair stays consistent.
* fix(plugin-manager): terminate a run's tree through execa, keeping exit cleanup
Spawning a service run with `detached` made execa skip the handler that kills
the child when the manager exits, so a crashed manager left the pnpm tree
running in the profile. `killDescendants` spawns the run as its own process
group while keeping that cleanup, and it addresses the tree on every kill path,
including cancellation. The module now only decides the probe target and waits
for quiescence.
`prepareDevelopmentProject()` resolved the build target from the host
platform and architecture, so a Linux host threw `unsupported target
linux-x64` while writing the runtime descriptor. Pass the target in
instead: the launcher supplies the target it prepared, and isolated tests
supply a fixed supported target.
Extract `desktopTargetPlatform()` into desktop-build-paths.mjs so the
target-to-payload mapping has one home, and use it for the qualification
runner, which previously derived both its target directory and its
descriptor from the host architecture.
Correct the README limitation to name the module that now performs the
architecture check.
pnpm install --lockfile-only, run to align the new workspace edge's specifiers, also re-resolved unrelated entries: it pinned registry.npmjs.org tarball URLs on the two libreoffice-kit optional platform packages and downgraded glob 7.2.3 to 7.2.0. The tarball URLs fail verify-lockfile-policy against the mirror's published metadata.
Rebuild the file as the base lockfile plus only the three intended importer additions (the new package's block and its two consumers), so the resolution entries stay byte-identical to master.
The Team panel opens on hover with the subagent-catalog grace timings, pins
on click, polls the one-shot view RPC every 20s while open (replacing the
toolbar refresh/close controls), and eagerly loads on mount so the trigger
keeps a stable two-person icon. The lead card navigates from teammate
conversations, the current conversation carries a Current chat tag (selected
ring only among multiple members), task descriptions clamp to two lines
behind an expand toggle, an empty board shows a one-line description, and a
lone member narrows the panel to one column.
The header title row becomes an inline-size container: the preset label
hides first, then the Team trigger collapses to its icon and switches to
click-only opening. The subagent catalog moves to the front of the actions
band so lineage stays beside the title crumbs, and its pinned-menu behavior
matches the Team panel. Tooltip gains a gap prop for tight anchors.
Review follow-up on the tooltip layering fix. The helper probes
`bubble.contains(hit)` instead of identity, so a bubble that grows child
elements still passes, and each probe returns the hit element's tag and
class, so a regression reports what covered the bubble instead of a bare
`expected false to be true` — the pre-fix run now names `DIV.u82K-q_input`
for both probes.
The `portal` JSDoc says the ancestor stacking context confines the
bubble's z-index to that context rather than resolving it against
siblings.
The Summary narrated the change ("the suffixes the old read table recognized keep their historical value") rather than the contract, and at 109 words it exceeded the 100-word package Summary limit.
State the rule a reader at HEAD can act on: a suffix a recorded session already holds keeps its persisted value, every other suffix takes its language's short name. The dropped path-separator and leading-dot detail stays documented under Language selection. Both languages updated and the pairing record re-recorded.
The package joined the workspace as version 0.1.6-alpha.2 while the root release moved on, which failed three CI jobs from one cause: constraints (version must match root), Dependency layout (verify-npm-install-layout finds no workspace version), and Pack npm tarballs (dsh release members must share one version).
The open paragraph accepted Explorer's exit code 1 as a delegated handoff but
did not repeat the reveal paragraph's warning that the acknowledgement proves no
application opened the file, which the Agent Note records for the Remote answer
`{ opened: true }`.
The goal strip and the queue dock ride directly above the input card, and
both create a stacking context (`isolation: isolate`) so their frosted
panel stays behind their own content. A tooltip bubble left inside that
context resolves its z-index there, where the card — painted later in the
same context — wins: the goal strip's bubble showed a two-pixel sliver
and the queue dock's stayed entirely hidden.
Portal those dock tooltips to the body, where the bubble's z-index
resolves above the whole card. The ui-primitives `portal` contract now
names the capping stacking context beside clipped containers, because
neither container clips its bubble.
The queue-actions aria goldens lose the tooltip line: the captured center
column no longer contains the portaled bubble. The scenario still covers
it by role and adds a hit test at the bubble's center and bottom edge, as
does goal-bar.
The new JSDoc and test prose narrated the change instead of the shipped
behavior: "pre-unification", "the old read table", "used to persist ... it
now persists". A reader at HEAD cannot resolve those stages, and the
persisted-value rule they describe is a durability contract that stands on
its own.
Restate each as the present contract: a suffix whose value a recorded
session already holds keeps it byte-identical; every other suffix takes its
language's short name. Rename the test's transcribed expectation away from
the branch vantage and re-wrap the module comment the earlier edit left
over-long.
`package-target.ts` imported `macos-notarization-proxy.ts`, whose top-level
import of `@deepseek-ai/node-addon-system/flock` resolves through the package
`exports` to the unbuilt `lib/flock.js` on a clean checkout. The step that
emits that file runs inside the same packaging script, so `check:package`,
`package:*`, and the `restore:mac-proxy` recovery command all failed with
`ERR_MODULE_NOT_FOUND` before reaching it; running `pnpm run typecheck` first
left the host addon binary missing instead, which failed at lock time.
The proxy lock now loads the entry at first use through `flock-entry.ts`. When
the import is unresolvable or the host platform package lacks a binary listed
in its `prebuilds.json`, the loader runs `pnpm run build:native-system` and
`pnpm --dir native/system run build:ts` before importing again; other errors
and build failures propagate, and a failed load is retried by the next call.
`check:package` no longer touches the entry at all.
Tests cover the loader steps and load the proxy module from a directory where
the addon package cannot be resolved. The Desktop README records the
behavior in both languages.
(cherry picked from commit 2d74cf50310d4efbbccbbe79a105fcc3a468b19d)
On Windows 11 ARM64 build 26200 Explorer splits its own command line at
commas and equals signs and rejects percent-encoded non-ASCII in a file
URI, falling back to the Documents folder in both cases without reporting
it. `explorerTarget()` now escapes `=` next to `,` and decodes Node's
non-ASCII escapes back to the literal characters the shell resolves, so
the two rules stay in one place. The Agent Note and the target-encoding
table record the measured behavior.
The packaged-imports plugin moves from generateBundle to moduleParsed so
external dynamic import() and require() targets, which chunk metadata
omits, fail the bundle too. Each bundle now carries an import policy:
the main process allows Node builtins, sandboxed preloads only the
modules Electron's require polyfill resolves, and the failure names the
importing module and the allowed set. The wine Windows gate bundles
apps/desktop after the Host tsdown pass like build:lib:host, the
ts-build-config note lists that step, and the tsdown docs name the
workspace match exactly.
Fixes#4993
Review follow-up: the caption menu is inert while the Platform view is open,
which the previous geometry hid; state it under Known Limitations, and drop
the duplicated 40px figure the Desktop owns. The style spec's rule lookup
anchors on the rule boundary and asserts with toContain.
The embedded Platform view renders its 48px return bar at the top of the
window. On Windows the Desktop-owned caption menu occupies that strip (fixed
at top 0, 40px high, z-index 1100), so the "Back to DeepSeek Harness" label
and the Application/Edit menu text were painted over each other.
Offset the overlay by --dsh-windows-titlebar-height where the Desktop preload
publishes its caption marker, the same clearance AppFrame and the sidebar
caption controls use. The return bar and the native view bounds, which come
from the viewport element's rectangle, both move below the strip.
Point selectable hover content at HoverCard (Tooltip has no interactive
prop), require a persistently mounted toast host, scope the font-weight
cap to new feature CSS, prefer theme typography tokens, link the styling
owners, date the design-reviewer roster, add the scrollbar containment
rule, and make the skill discoverable from packages/client/AGENTS.md.
Four focus rings added on master after this branch's base still declared their
own colour: PresetGuideDialog's panel, CodeCard's action (state-business-primary),
SegmentedControl's tab, and AccountSection's link button (button-primary-fill).
The stylesheet corpus spec now rejects each of them, so they read
`--dsw-focus-ring-color` with the brand-token fallback like every other ring.
CodeCard keeps its 1px dense width; the other three take
`--dsw-focus-ring-width`.
Conflicts and how they were resolved:
- ui-agent-preset/AgentPresetSection.module.css: master replaced the icon-only
tooltip actions (`.iconButton`, `.iconDanger`, `.revealedPath`) with
`.cardHelp` + `.helpButton`, which deleted the one declaration this branch
edited. Took master's structure and re-applied the branch's change to the two
`.cardMain:focus-visible` rings, which master kept unchanged.
- ui-theme/README.md and README.zh.md: master documented the separately exported
`brand-font.css`; this branch documented `focus.css`. Kept both, with the
dynamic entry's sheet list at seven sheets including `focus.css`.
`README.i18n.yaml` re-recorded with `verify-translation-pairing --write`.
The recorded exit code 0 after about 30 seconds was not reproduced: an SSH
session 0 on Windows 11 ARM64 build 26200 measured exit code 1 after about a
second with no association invoked, the same exit code a delegated open
returns. The README, both Agent Note languages, and the `explorerTarget` JSDoc
now record that measurement, and the `Invoke-Item` contrast keeps its
qualitative part without the timing.
Pointer suppression previously cleared `outline-color` on the focused element
alone, so a ring painted by a descendant, a pseudo-element, or a `box-shadow`
stayed visible, and editable text controls lost the focus feedback they need on
click.
Ring colour now flows through `--dsw-focus-ring-color`, which component outlines
and focus-ring shadows read alongside the brand token. Every ring that inherits
the variable is suppressed together, and `body :focus-visible:not(:read-write)`
limits the suppression to rings while editable controls keep their own feedback.
Keyboard modality returns on Tab, Home, End, PageUp, or PageDown, and on focus
reaching a different control after a non-composing key. IME composition keys,
refocusing the same control, and window blur leave rings silent. The desktop
preload menubar suppresses its own ring inside its shadow root, where the
document-level attribute does not reach.
A verbatim `\\?\` prefix and a `\\.\` device namespace take Node's UNC
resolution, and the comma encoding has to survive them. The drive-root case is
pinned separately because Node restores a trailing separator against the host's
`path.sep`, which differs on the Windows host this opener runs on.
The root tsdown workspace pass builds every member concurrently, so the
Desktop main bundle could resolve its inlined workspace devDependencies
(dsh-home-paths, dsh-app-boot, dsh-deepseek-account) before their lib/
output existed. rolldown then kept them as external imports, which the
asar does not ship, and the packaged application failed at launch with
ERR_MODULE_NOT_FOUND on a clean checkout; stale lib/ output on long-lived
checkouts hid the defect.
apps/desktop leaves the root workspace list and build:lib:host bundles it
after the Host pass. Every Desktop bundle now runs a rolldown plugin that
fails the build when a chunk imports a bare specifier the packaged
application cannot resolve: electron and the manifest dependencies for
the main bundle, electron alone for sandboxed preloads.
Collects the standing judgment rules for product-user-visible GUI work:
token and shared-treatment reuse, light/dark readability, font weight and
size scale, feedback-surface choice (toast vs in-place notice vs empty
state), loading treatments, overlay dismiss/fit/clip checks, macOS/Windows
window adaptation, and spacing review. dsh-code-review links it from its
sources of truth so UI PR reviews load it.
- Agent Notes: cross-link the new decision with the native fatal recovery
and fail-loud release notes both ways, rewrite the older note's
console-retention and dialog-detail facts in place, and state the report's
actual bounds.
- crash-report: cut the rendered error section at 256 KiB and prune only
files matching the generated crash-<time>-<source>.log syntax.
- main: backendReady follows the backend state except during shutdown, so a
failure while tearing down a ready backend still reads as running.
- subprocess-local: the temporary-file-cleaner hint is appended only for
ENOENT; other codes rely on the appended error.
- client-modules: describeError is module-private; BootModuleRow.url's
JSDoc names its fallback role.
The ui-chat README described the visibility rule as "while the built-in browser
is available", a phrase that now reads like an option label. Both languages name
the Sidebar Browser, matching the sentence that follows.
A Host startup failure reached the crash report as a fresh Error built from
a message string three hops away from the original: the Host sent only
error.message over the fatal IPC event, DesktopHostProcess wrapped it in a
new Error, desktopErrorState flattened it to text, and main.ts wrapped that
text again. Stack, cause, code, and path were gone, and the Host's stderr
tail arriving with the later exit was blocked by the first-failure latch.
The Host now sends its complete inspected error alongside the message
(bounded to 64 KiB; carried over IPC rather than stderr because the two
race and the shell reports the first failure it sees). DesktopHostProcess
keeps it on DesktopHostFatalError.diagnostic, the backend error state
carries the original failure object, main.ts reports that object, and the
crash report prints the Host diagnostic verbatim in its own section.
apps/desktop imports @deepseek-ai/dsh-api-gateway/stream-protocol, whose
import chain reaches @deepseek-ai/dsh-typert-protocol and then
@deepseek-ai/cordis. Both intermediate packages declare cordis as a
peerDependency and nothing satisfied it for the application closure, so
electron-builder omitted cordis from app.asar and the packaged main process
died before opening a window with ERR_MODULE_NOT_FOUND. A workspace checkout
resolves the peer through pnpm's own links, so development runs and the unit
lanes stayed green while every packaged build since efbc8e26b6 (2026-09-20)
failed to launch.
The lockfile additionally drops a stale duplicate [email protected] in favour of the
7.2.3 it already carries; any real pnpm install removes it.
Verified with a signed win-x64 installer built from this tree: app.asar now
ships cordis and @standard-schema/spec, and the installed application opens
its main window.
The Settings → General row for Chat link destinations now reads
网页链接默认打开方式 with the description 对话中网页链接的打开位置 and the option
labels 应用内侧边栏 and 默认浏览器. English keeps the title "Open chat links in" and
now reads "Choose where to open web links", "In-App Sidebar", and "Default
Browser".
The `ui-chat.linkOpening` setting, its values, persistence, visibility rule, and
open behavior are unchanged. The row spec, the clickable-link gallery scenario,
and the two package READMEs name the new copy; the bilingual pairing records were
re-confirmed for the README pairs.
Carry the user-scoped agent-experience guidance into the repository unchanged so
project sessions discover it beside the other repository skills. The file,
including its frontmatter, is byte-identical to the personal copy.
Skip the per-owner wake ledger entirely when no cap is configured.
Rewrite the 2026-08-11 Agent Note in place for the opt-in cap and add
a note recording why the default was removed. Keep `subagent`
untranslated in Chinese prose and drop change narration from the test.
Refs #4944
The Contact us action already stopped prefilling the signed-in UID, but a
configured contact form URL could still carry its own prefill_uid or hide_uid.
contactUrl now drops both query parameters unconditionally, with regression
coverage that starts from a form URL containing them.
The contact URL test also pins the shipped default form path, and the injected
contactUs documentation and the AccountProfile summary no longer describe a
support UID that is no longer sent.
* fix(web): resolve XLSX part relationships and namespace aliases
* fix(web): keep Excel browser scenario in the host test program
* fix(excel): preserve XML encodings and relocated workbook parts
* fix(excel): exclude installed launchers from dependency patch
* fix(excel): restore Strict and case-equivalent workbook previews
The account menu entry that opens the questionnaire is now labeled Feedback in
English and 意见反馈 in Chinese. The menu action, contact form URL and its
configuration options keep their existing names.
Expected menu snapshots and the bilingual README record the new copy.
`maxConsecutiveWakes` no longer defaults to 3. Unset, every background
job completion wakes its idle owner; the cap is opt-in.
With the default cap, a session chaining background commands and
one-shot subagents exhausted the budget after three wakes. The fourth
notice was injected into the next-step inbox without waking the owner,
the client showed nothing, and the session stalled until the user
typed something (dsh-external/issues#641).
Closesdsh-external/issues#641
The recorded-session scenario edited with a single-line value, so the ARIA
golden could not catch a return to a newline-stripping editor. Edit with a
two-line value and assert the model-bound user message keeps the line break;
with the previous input element the scenario fails on that assertion. Also
refresh the stale input cast and pin that Shift+Enter is not consumed.
The inline queue editor was a single-line input, and HTML strips newlines
from such a value: opening and saving a multi-line queued message silently
collapsed it into one line before delivery. Edit in a textarea that grows
with its content, capped at six lines; Enter saves, Shift+Enter breaks the
line, and Escape still cancels.
client-modules: a batch that already executed is never requested again,
even when its first importer was a row it did register — every executed
batch URL is remembered and a still-missing row goes straight to its
one-resource URL. The in-process module system test in test-support is
updated to the aggregated arrival message. describeError replaces three
inline non-Error ternaries and is covered.
subprocess-local: the spill path is published only after the exclusive
open succeeds, so a failed open (EEXIST on a planted entry included) can no
longer unlink a path this process did not create; a reporter that throws is
contained and written to stderr; logSpillFailure builds the owner reporter
for both the runtime and the SSH helper so the logging path is exercised
once instead of duplicated. Tests cover the runtime logger, the bare-caller
stderr default, EEXIST, and a throwing reporter.
desktop: a fatal failure during shutdown still writes its report; the
report phase follows the backend state so a failure during a restart is
'startup' again; writeReport is a required recovery operation and the
dialog tests await the write; the shell pid is labelled; the withheld
response-header list states why set-cookie is dropped.
app-boot, READMEs, Agent Note: "never resumes" is restated as control never
returning to the failed operation while the loop runs until release
settles; inspect output is bounded; incident inferences (who removed the
spill directory, which batch failure occurred) are written as inferences.
- Headline skips the archive path 7-Zip echoes before an open failure
(also when the path contains spaces) and prefers the operation line.
- Raw process statuses such as 0xC0000005 are kept and printed in hex
instead of being clamped to INT_MAX.
- The expanded panel excerpts the result and 7-Zip's own lines rather
than the report header.
- The saved report keeps up to 16 MiB of output, records any cut, and
trims a partial UTF-8 sequence before decoding.
- The details view no longer reads the UTF-8 log as ANSI; the test seam
define and the zero-padded GetTime order are documented; the smoke
asserts a year-first report name and normalizes -FrameLibrary.
- README names the real updater cache directory.
master grew a fourth drag owner after this branch: the Platform overlay's return
bar (ui-settings-account) declared `app-region: drag` on its own header. The
branch absorbs it into the contract — the header marks itself `data-window-drag`,
its own app-region drag/no-drag rules are deleted, the ui-theme manifest pins the
row and its 48px height, and the Agent Note's row list names it — so the tree
declares the drag surface in exactly two places again (the shell's mark rule and
the Windows caption row).
Earlier resolutions from the first merge stand: the dockkit strip keeps its mark,
the sidebar-right pulse stays deleted, ui-settings-general keeps upstream's
dsh-settings and this branch's react-dom, and tsconfig.host.json takes upstream's
`files: []`.
Checks: build, build:web, typecheck, test:gui, the ui-theme/base-styles gates, the
window-drag-coverage lane, doc-sync and translation pairing.
Conflict resolutions:
- ui-dockkit TabPanel: keep upstream's reworked tab strip and re-add the row's
`data-window-drag` mark on `css.tabStrip`.
- ui-sidebar-right SidebarRight.tsx/.module.css: keep upstream's retention
rewrite and re-delete its hand-written one-frame app-region pulse; the shell's
window-drag watcher owns recollection.
- ui-settings-general/package.json: keep upstream's `@deepseek-ai/dsh-settings`
dev dependency and this branch's `react-dom`.
- tsconfig.host.json: take upstream's `files: []`; client sources belong to the
client aggregate.
- pnpm-lock.yaml: master's lock plus only the react-dom importer entries.
The fatal recovery dialog showed the last eight lines of the error and
promised that the full diagnostic was in the Electron console, which a
packaged installation never surfaces. Two field failures were diagnosed
from screenshots of those eight lines: a Host killed by an uncaught
exception whose path happened to be in the tail, and a renderer boot
failure whose real import error lived only in the renderer console.
Every reportFatal caller now names its source (host, web-boot, renderer,
main). Before the first dialog opens, the shell writes
crash-<time>-<source>.log under app.getPath('logs') with the source, the
startup or running phase, application and runtime versions, the complete
inspected error with its properties and cause chain, and the primary
window's recent error-level console output (a 64 KiB tail captured from
webContents 'console-message'). The dialog names the file on its own line,
independent of whether the error was shortened and in the listener-conflict
variant too; the wait is bounded to one second and a failed write shows the
dialog without a path. Startup keeps the ten newest reports. Buttons and
recovery actions are unchanged.
forwardWebRequest relayed transfer-encoding, connection, keep-alive, and
the other hop-by-hop headers from the Host's response into the dsh-app://
response, describing a Node fetch connection that Chromium never saw. It
also relayed the Host's `cache-control: public, max-age=31536000,
immutable` on plugin bundles whose revision changes on every launch, so
each start wrote another unreusable copy into the renderer's disk cache.
The forwarder now strips the connection-level header set alongside the
encoding headers it already removed and marks /plugins/ responses
no-store. Whether either header contributed to the observed batch load
failure is unverified; both are corrections in their own right.
One application batch is a single classic script that registers every
package in sequence, requested exactly once during boot. A transient
failure of that one <script> request failed every row in the batch and,
through arriveGraphRow's dependency wait, every row that injects or
requests them; the boot audit then listed each as "import failed (see
console for the import error)" with the real error only in the renderer
console.
Arrival now distinguishes the two failure kinds. A transport failure
(script error event, nothing executed) is retried once on the same URL and
the retry is shared by every row waiting on that batch. A script that
loaded without registering a row is never re-executed, because register()
rejects duplicates and a replay would stop at the first package the
original run did register. Either way each still-missing row then loads its
own one-resource combo URL, which the Host serves for every package. A batch
URL that failed is remembered so later rows skip straight to their
fallback; a one-resource URL is not, so it stays retryable on the next
import as before. Dependency failures are wrapped with the consumer and
dependency names so a cascade reads as a chain.
The module system records the last import() or prefetch() failure per
graph row at the outer operation, so factory execution errors are captured
as well as arrival errors, and clears it on success or invalidation.
assertEntriesActive takes the module system and reports that text per
fiberless entry.
The Desktop Host and every other profile launcher installed a fail-loud
handler for unhandled rejections only. A synchronous throw from a stream
'data' listener fell through to Node's default uncaught-exception path: it
printed and exited without the surface's release hook, and the label and
format differed from every other fatal diagnostic.
installFailLoud now registers the same report for 'uncaughtException' with
the label `fatal uncaught exception`, shares the exit latch and release
timeout with rejections, and never resumes the process. The diagnostic is
util.inspect(err, { depth: 4 }) instead of err.stack so enumerable
properties (a node:fs error's code, syscall, and path) and the cause chain
reach stderr; the `fatal load failure` label for rejections is unchanged
because the Web profile expected-output e2e tests match it.
OutputCollector.spillAll ran openSync/writeSync inside the stream 'data'
listener with no containment. When the private per-process spill directory
had been removed by a temporary-file cleaner, the first overflowing chunk
threw ENOENT from Pipe.onStreamRead, became an uncaught exception in the
Desktop Host, and the Electron shell reported it as a fatal failure.
Spilling is best-effort by contract (spillPath is optional; a failed final
close already withholds it). Every filesystem failure while opening or
appending the spill file now discards the spill, reports once through the
owner's logger, and leaves in-memory collection untouched. Spill inputs move
into one SpillOptions object with the reporter, so tail-only collectors no
longer pass an empty directory; prepareManagedProcessBinding is the explicit
resolve step for the directory and reporter defaults, and the runtime and
SSH helper supply their plugin loggers.
Tests cover ENOENT on open (removed directory), ENOTDIR on open, and ENOSPC
on append after the file exists, asserting the tail stays byte-exact, the
spill file is withdrawn, and the reporter fires once.
The installer showed "Failed to decompress files" and discarded 7-Zip's
error output, so a failure such as security software quarantining an
extracted file could not be told apart from a full disk or a broken
download.
- InstallerExtract returns negated Win32 errors for launch failures so
they no longer collide with 7-Zip exit codes; 7-Zip writes UTF-8 errors.
- window-frame.dll composes a report (result, archive, destination,
Windows version, full 7-Zip output), saves it under the updater cache
directory, and presents a task dialog with the first error line, a
collapsible bounded excerpt, and a Copy error details button. Silent
installs keep only the report; a missing task dialog falls back to a
message box.
- Unsigned Windows builds name their installer *-unsigned.exe so a
shared file cannot pass for a release artifact; version discovery
still counts them.
- The directory smoke asserts the saved report on a broken archive and
smoke-windows.ps1 accepts the prepared DLL.
Refs #4893
Opened and copied authorization links carried no palette, so the Platform
login page rendered its own stored preference. Append the effective Desktop
palette as theme=light or theme=dark, resolving system through the theme
source the preload publishes.
The theme source now reaches nativeTheme on Windows as well: the preload
drops its macOS-only guard, since shouldUseDarkColors is read for every
platform, and gates the DOMContentLoaded listener with once.
master replaced the startup-bound messages constant with a locale that
follows the settings preference; the About dialog now reads the same
locale as the update prompts so the two never mix languages.
The Contact us entry now opens the current Feishu questionnaire, and the URL
no longer carries the signed-in account UID: contactUs stops reading the
account profile, contactUrl stops writing prefill_uid and hide_uid, and the
remaining build and browser context fields keep their existing behavior.
The url, package tests and the bilingual README and account-login note state
the same current facts.
The declarative-preset refactor (#4569) removed the registry's read Remote
together with the YAML editor, and with it the 查看配置 card action that
opened a preset's composition read-only. #4850 brought the Creator entry
back and had to leave 查看配置 out because `agentPresets/read` was gone.
Give the registry a read-only `read` Remote that renders a declaration's
child plugin list from the in-memory definition in the Loader's own
entry-list dialect, so `!!js` conditions read as declared, and put the card
action back at the right end of each card footer: an icon that opens the
YAML in a read-only dialog titled after the preset. Nothing accepts YAML
back, and a broken preset stays readable because its diagnostic points into
that YAML.
Fixes#4895
The settings sheet's Open configuration file action opened nothing on Windows when the user's default lived only in the Explorer per-user record (FileExts\\<ext>\\UserChoiceLatest), which Explorer honors and the association resolver inside the host process does not. Invoke-Item also exits 0 when it finds no application, so the Remote answered { opened: true } and the sheet rendered no error.
Windows opens now hand the path to explorer.exe as one argv element - the resolution a double-click performs - and revealNativePath shares that invocation rule. The shared helper accepts Explorer's delegate exit 1 for an open and a reveal alike; every other failure and every cancellation still rejects. The PowerShell command string and powershellLiteral are gone: the path crosses the process boundary as argv, with no escaping step.
Fixes#4428
The consolidated commit refreshed unrelated transitive resolutions while it
resolved the settings portal's new react-dom dependency. Restore those entries to
their previous versions and keep only the two importer entries the portal needs;
the lockfile already carries both snapshots.
Review pointed out that About read its copy from the renderer-reported
Windows language while the shared update dialog stamps its lang and
close label from the startup locale, so an in-app language switch mixed
languages inside one dialog. About now uses the same startup messages
as Check for Updates. The menu click also logs a dialog that fails to
open instead of leaving an unhandled rejection, and the return type no
longer hides the message-box result behind unknown.
The shared code-language table must not claim a suffix a dedicated viewer
declares, and the Host read card must keep the `lang` values it already
persisted.
- Drop `csv` from the shared table: Code registers before Spreadsheet, so
the earlier body would take `.csv` and the browser snapshot gate fails.
- Add `readLangHintForPath`, the read card's legacy projection over the
shared table: the 43 pre-unification suffixes keep their old short ids
byte for byte, new suffixes use the canonical id.
- Add an ownership regression test over the full preview registry that
turns red when `csv` returns, plus byte-level and alias-normalized tests
for the read projection.
The client UI i18n gate rejects literal message text in main.ts; the
Windows About dialog now reads the product name from the dictionaries
like every other shell string.
Three tools capped the preview text they return with String.prototype.slice
at a UTF-16 code-unit budget. A cut that lands inside a surrogate pair
leaves a lone high surrogate, which is not well-formed text: a strict JSON
reader rejects the durable Session log carrying it, and the preview shows a
broken glyph. A lone surrogate retained in history has already blocked every
later Messages request for one session.
Centralize the cap in output-retention as
truncateWithoutSplittingSurrogatePair, and use it from the persistent bash
and pwsh shells and the string-replace editor. The cut drops the unpaired
half instead of keeping it, so the retained text stays a prefix and is at
most one code unit shorter than before.
The new dependency edges and the shifted Config sources regenerate
docs/module-graph and docs/config-catalog, including the committed
translation pair.
Refs #4817
Electron's About role on Windows falls back to a bare system message
box, which looks nothing like the shell's dimmed update prompts. Design
asked for the version check to match Check for Updates, so the Windows
menu item now opens the shared update dialog with the application name,
localized version line and a single OK button; macOS keeps the native
panel. The About command follows the renderer's chosen language like
the rest of the Windows caption menu.
The document Code preview and the Host read card each owned an
extension-to-language table that had drifted: the preview table had 13
extensions the read card lacked, and the two returned different language
ids for the same suffix. Both now resolve through the new zero-dependency
@deepseek-ai/dsh-util-code-language, which also adds common Windows,
config, markup, and language extensions. Every added grammar stays in the
lazy set; the boot grammars are unchanged.
Fixes#4822
The Windows badge used a 44px radius on its 1024px plate, which reads as
nearly square on the desktop next to other Windows 11 icons. Raise the
radius to 192px (3/16 of the plate, matching Tencent Meeting's corner)
and re-export the transparent 1024×1024 PNG from the SVG; the ICO for
the app, installer and uninstaller and the About panel image all derive
from that PNG.
The manual check dialog explained that the release bundles its matching
dsh runtime before mentioning the restart. Users only need to know the
app restarts to finish updating; the bundling detail belongs in the
README, not the confirmation. Both locales carry the shorter sentence.
The composition suite's idleSilenceMs: 30000 was below the suite's own send
bound (dsh-terminal-bash timeoutMs: 300000), so the comment claiming silence
could settle nothing was wrong: a regressed send still settled as inferred_idle
after ~30 s and only the accumulated case timeout caught it.
The case now records every send's waitReason and requires at least six
stdin_read settlements with no inferred_idle. Reinstating the removed prompt
override fails it in 30 s with 'expected 0 to be greater than or equal to 6'
while the output assertions still pass. The silence tier returns to its product
default.
Also renames the two stale test titles ('private shell prompt', and the
cancellation phase that no longer covers an initialization send), pins the
stdin_read fallback's session retention, and records the interactive-child
consequence of that retention in the package README pair and the Agent Note.
The issue reported .ini files with no preview. On master the ini grammar is
already in CODE_HIGHLIGHT_EXTENSIONS, so the code renderer claims the
suffix and .ini previews. No production change is required; this test
records the admission behavior so a future allowlist change cannot silently
drop the reported suffix.
The static CI gate rejected four new assertions to unknown in the
rewritten Team panel specs. Build the Session list, Session snapshot,
and injected-callback fixtures as typed values, prune the retired
baseline fingerprints, and regenerate the module graph, config, and
persistence catalogs for the removed Remote dependency edges and the
shifted source lines.
The Host's default marker is best effort: on Windows it comes from the
Shell's ASSOCSTR_EXECUTABLE/APPID answer, which does not always name an
enumerated handler. When no application carried that marker the split
button promoted 显示文件位置 to the primary action even though the menu
listed applications that can open the file.
Select the OS-marked default when present, otherwise the first registered
handler in Shell preference order, and mark it as the menu default.
Reveal is promoted only when no application is registered at all.
Windows 11 ships some applications (Notepad, other Store apps) as MSIX
packages whose Shell handler reports an indirect icon reference such as
@{Microsoft.WindowsNotepad_...?ms-resource://.../NotepadAppList.png}
instead of a file path. SHDefExtractIcon cannot read that reference, so
the menu showed the generic fallback glyph.
Resolve an indirect reference with SHLoadIndirectString before extracting,
and fall back to the handler's executable when the resolved source still
yields no image.
The Team panel read a Remote snapshot on open and behind an explicit
refresh button, while the subagent catalog panel updates from Host
projection frames. The Team projection was Host-only and its `apply`
mutated state in place, so the projection registry could never publish
a client view for it.
Publish a durable-only `agentTeam` wire view: roster rows with their
durable phase, non-deleted task views through the same readiness,
owner-name, and write-overlap derivation the task board returns to
agents, and `failure` beside the last valid state when a persisted Team
record is rejected. `apply` now returns a new state per applied Team
event and replaces only the touched collection, so mailbox-only events
reuse the previous view reference and publish nothing.
The Web panel derives every row from the shared Session store: the Lead
from the current Session's subagent address, running/inactive from
Session status, and the model from the member Session's durable
`modelSelection` projection. Opening requests the Lead's projection
baseline when the store has none, a failed read shows a retry control,
and the refresh button and Remote mount are gone. The `agentTeams/view`
Remote method remains for callers that need live availability and the
configured model.
The Team panel web e2e now asserts that Host-side task creation and
claiming reach the open panel without any user action.
tool-pwsh-persistent installs its own `prompt` function after spawn, so
terminal-bash prompt readiness never matched and every send degraded to the
silence tier (idleSilenceMs + handoffGraceMs) under production defaults. This
is the defect the persistent bash tool fixed for the bash stack.
The tool now initializes without a prompt send and detects the no-end-marker
fallback through the seam's stdin_read wait reason instead of matching its own
prompt text. The private prompt constant and its stripping are deleted, so
partial output can carry the backend's own trailing prompt.
Tool calls drop from 8493/3722/3832/3759 ms to 1340/255/251/241 ms (spawn and
initialization plus the first command, then three warm commands; Windows,
production defaults, real Loader composition). The loader composition suite
pushes idleSilenceMs beyond the send bound, so every case fails if prompt
readiness regresses.
Re-render the four installer brand banners from the vector sources:
the badge follows the Figma spec (three-stop gradients, layered drop
shadows) and the whale no longer inherits shape-rendering=crispEdges,
which disabled anti-aliasing and left jagged edges. The wordmark now
reaches its design color instead of a soft #353638.
Draw the progress-page brand with high-quality bicubic interpolation
and half-pixel offset so the always-2x bitmap stays crisp when scaled
down at 96 DPI.
- main-startup: assert primaryRuntime for the unpackaged Host and cover a
missing DSH_DESKTOP_PRIMARY_RUNTIME_DIR as a fatal startup dialog with no
Host started.
- test-workspace-updates: pass the prepared primary-runtime directory instead
of an application/runtime path the runner never creates.
- development-project: derive the descriptor platform/arch from
resolveDesktopBuildTarget so it matches the prepared payload.
- main: resolve the unpackaged primary runtime once at startup instead of
inside the Host factory.
- dev: keep the environment override symmetric with its neighbours.
- README: document the variable as required for unpackaged launches and record
the remaining win32-arm64 payload architecture limitation.
Numeric download progress renders without an ellipsis, and a failure keeps the update chip's palette instead of a separate error color. Pin the progress label and the shared badge background, and let the retry label carry the failure distinction.
The macOS window's draggable area was a hand-written band: a 52px strip, deepened
to 76px by a marker the Conversation header had to keep in step with its own
height. It drifted from the layout — blank runs inside a chrome row did not drag,
and a control a few pixels away dragged the window instead.
Chrome rows now mark their own element (`data-window-drag`) and the shell declares
the surface once (`html[data-platform='darwin'] [data-window-drag]`), so a row's own
box is its draggable geometry and no fixed band has to match any row's height. The
frame band, its 52/76 figures, the marker, and the per-sheet drag rules are gone;
interactive controls and overlays portal beside `#root` where one rule subtracts
them.
Recollection becomes one shell watcher (`window-drag/recall.ts`): Electron rebuilds
the window's drag rects only when a computed app-region value changes
(electron#32341), so the watcher measures every marked row each frame while the
surface moves, sets `data-window-drag-recall` for the frames whose geometry changed,
and clears it on the frame that finds the geometry settled. The right panel's
hand-written pulse is deleted with it.
Coverage is a contract with executable parts. `window-drag/regions.ts` publishes the
composition rule and the interactive selector that both the sheet and its spec
compare against; the ui-theme gate asserts the package tree declares drag in exactly
two places and pairs every row's mark with its sheet, selector, and authored height;
and the browser lane boots the shipped composition in Chromium with the darwin
marker to assert per-row geometry, a content body under a marked head row, a
covering overlay, and the watcher's pulse. The plugin manager's detail views mark
the head row they share rather than the pane they render in, so their text and form
labels stay content.
Both real machines agree: the macOS matrix ran on a real `hiddenInset` window with
HID-level pointer input and window-frame readback, and the Windows matrix on a
Windows 11 ARM64 VM — chrome rows drag, content and the covering overlay do not,
and the controls keep their clicks.
Refs #4619
Every ring that used 2px now reads --dsw-focus-ring-width. The dense tables and
toolbars keep their 1px ring on purpose: at 2px they visibly outweigh the rows
they sit between, so the standard value and the dense exception stay distinct
rather than converging on a number nobody measured.
The action cluster animates its width with overflow hidden, and the search pill
clips to its rounded shape, so an outer ring on the add, search and clear
controls was cut to a sliver. This package declared no focus style at all — every
control fell back to the browser default inside those boxes.
Ring the three controls inside the element, the direction issue #4655 allows.
A tooltip asks what the last input was: after any key the user is working from
the keyboard, so a programmatic focus may raise a bubble. A focus ring asks
something narrower — Chromium reveals an already-focused control as soon as any
key arrives, including a modifier or Escape that never navigated anywhere, which
is the ring users report as unexplained.
Record both answers and publish the ring's narrower one, so tooltips keep their
existing behaviour and rings stay silent for a pointer's focus.
The breadcrumb trail and the Miller column scroll, and the column's left edge
carries no padding, so an outer ring on their buttons was clipped to a sliver.
The TurnNavigator marker sits at the scroller's right edge, which clipped three
of its four sides. Ring those inside the element instead — the direction issue
#4655 allows — and change no layout value.
Clicking a control leaves it focused with :focus-visible false, so nothing is
drawn. Chromium then reveals the ring the moment any key arrives — a modifier,
Escape, a typed character — which is the unexplained focus ring users report:
they never navigated there, yet a ring appears under the pointer's last target.
Record the input modality on the document element, publish the keyboard modality
only for focus navigation (Tab, arrow keys), and let the global focus sheet keep
a pointer's focus transparent. Tooltip's pointer modality moves into the same
module so tooltips and rings follow one rule.
Focus rings named eight different colours across 54 client stylesheets, and a
control that declared no ring fell back to the browser default, whose colour
follows the operating-system accent — so the same build showed orange on one
machine and blue on another.
Every keyboard focus ring now uses --dsw-alias-brand-primary. A global ui-theme
focus.css sheet reaches the controls that declare no ring of their own, and 49
declarations name the same token. Only the colour changed: each declaration
keeps its width, style and offset.
The unpackaged shell derived the primary-runtime directory from process.arch, while preparePrimaryRuntime() resolves the target through resolveDesktopBuildTarget(), which fixes Windows to x64. On a Windows ARM64 host the shell looked under targets/win-arm64 while the payload was under targets/win-x64, and the missing office-skills/scripts/check_office.py became a fatal startup dialog.
The launcher now resolves that directory through the same resolver and passes it to the shell as DSH_DESKTOP_PRIMARY_RUNTIME_DIR.
Windows directory junctions are reparse points that lstat reports as symlinks, so list refused My Music, My Pictures and My Videos with workspace-file/not-directory while its own listing reported them as directories. list now follows a final link, requires the resolved target to be a directory inside the workspace root, and keeps read's no-follow gate.
2026-09-18 20:45:00 +08:00
4061 changed files with 170271 additions and 37423 deletions
`dsh-spill-policy` is a `tools/post-execute` result transformer with one configuration knob:
```ts ignore-check
interface Config {
/** Omitted means no automatic spill policy. Present means apply to oversized plain text tool results. */
maxInlineBytes?: number
}
```
When `maxInlineBytes` is omitted the plugin registers nothing (a true no-op). When set, it applies a default policy to final plain-text tool results:
1. Let the tool run normally, delegating via `next()` so a downstream listener settles the result first.
2. Flatten the accepted final `ContentBlock[]` only when it is entirely plain text; a result with any non-text block is left untouched.
3. If its UTF-8 byte size is at or below `maxInlineBytes`, leave it unchanged.
4. If it is larger, call `ctx.spillStore.saveText()` with the full final text.
5. Replace the model-facing result with a retained head/tail preview plus the spill reference.
The preview is an implementation default owned by the policy: a head/tail split of `maxInlineBytes` via the retention library's `TextRetainer`. Future config can expose preview sizing only after a second deployment needs it.
The replacement text is intentionally generic because the policy only knows the final formatted tool result, not the tool's internal resource:
```text
<retained preview>
(Omitted N bytes. Full formatted result stored at: /.../session-.../....txt. Use read with offset/limit, or grep this path to search within it.)
```
If `ctx.spillStore.saveText()` fails (permissions, ENOSPC, backend unavailable), or the call has no session owner, or no backend is loaded, the plugin logs the reason and returns the original result unchanged. Spill failure never turns a successful tool call into an `isError` result or hides the inline result.
The policy skips `read` to avoid a circular `read -> spill file -> read again` loop. Additional opt-out configuration is deferred until a real second tool needs it.
`dsh-spill-policy` retains ordered text/image ends under `maxInlineTokens` after post-execute policy accepts the result. `saveText()` stores the complete result with readable image paths; image bytes remain in attachment storage. The [multimodal retention decision](../../implemented/bug-fix/2026-09-21-multimodal-tool-result-retention.md) owns image projection order, atomic image omission, and route pricing. An omitted budget installs no listeners, recovery failures keep the original result, and model-facing `read` results skip retention to avoid a read/spill loop.
## Showcase: web_fetch
@@ -129,7 +100,7 @@ With `dsh-spill-policy` configured, a large formatted fetch result is automatica
- id: spill-policy
name: '@deepseek-ai/dsh-spill-policy'
config:
maxInlineBytes: 50000
maxInlineTokens: 12500
```
This separation is important. `web-fetch-http` still owns resource caps (`maxResponseBytes`, `maxBodyChars`) to protect network, memory, and decoding work. `spill-policy` owns only the model-facing context cap after the result already exists. If the provider already returned `truncated: true`, the spill file contains the full formatted result the tool returned, not the full original webpage; the policy does not claim otherwise.
@@ -186,11 +157,11 @@ The local-backend value proposition depends on the existing `read`/`grep` tools
**Snapshot gap.** No ACP snapshot scenario covers the transcript-visible `web_fetch` spill notice yet. The ACP snapshot harness replays keyless and cannot hit the live web, and a `web_fetch` spill requires a real over-cap HTTP body; a deterministic scenario would need a seeded loopback fetch target the replay tree does not currently wire (the examples do not load `tool-web` at all). The behavior is covered instead by the `dsh-tool-web` integration test against a loopback server. Closing the gap is follow-up work: wire `tool-web` + a seeded fetch target into the ACP example, then record a `web-fetch-spill` scenario.
The policy can become too large if it starts owning tool-specific semantics. It stays narrow: plain-text final results only. Tool-owned early spill remains future work.
The policy can become too large if it starts owning tool-specific semantics. It handles accepted text/image sequences without interpreting tool-specific text. Tool-owned early spill remains future work.
## Alternatives considered
**Require each tool to opt in with a retention declaration.** Rejected: the goal is a default behavior similar to Claude Code's generic tool-result persistence. A single `maxInlineBytes` deployment knob is enough to prove the shape.
**Require each tool to opt in with a retention declaration.** Rejected: the goal is a default behavior similar to Claude Code's generic tool-result persistence. A single `maxInlineTokens` deployment knob is enough to prove the shape.
**Make `tool-results` a broad tool-result platform.** Rejected: a broad package name invites retention policy, result replacement, preview wording, search, and early spill into one seam. The shared storage part is smaller: save text and return a locator plus retrieval hint.
@@ -14,6 +14,8 @@ Business events also use different correlation models. Tool has call IDs, Assist
## Decision
The [three-stage Tool lifecycle](2026-09-22-tool-call-three-phases.md) owns transient initialization, repeated start matches for one ID, and tool preparation; this record's other node-assembly responsibilities remain applicable.
Client Runtime provides a target-neutral Conversation Node assembly engine. Business plugins register Event Definitions, and view plugins register per-Session View Builders. `ui-conversation` registers the first built-in Definitions and the `chat` builder; Session only submits the current contiguous `SessionEventLikeEntry` window to the engine and publishes its snapshot instead of interpreting individual conversation businesses. The entry's outer discriminator distinguishes standard and packed records, while both carry an aligned inner `SessionEventLike` for Definition dispatch.
This Note retains the derivation, business-by-business validation, responsibilities, algorithms, and trade-offs that remain relevant after implementation.
@@ -14,13 +14,13 @@ Both values belong only on the official DeepSeek adapter path. Adding them to `G
## Decision
`@deepseek-ai/dsh-deepseek-llm-api-extensions` registers `ctx.deepseekLlmApiExtensions`, an additive registry of top-level fields for `deepseek-official` request bodies. A contributor claims one declaration-merged field with `register()`. The adapter invokes `prepare()` after serializing the exact wire messages, passes the request cancellation signal, rejects preparation or base-field collision before HTTP, merges the detached fields, and calls the captured `accept()` transaction after HTTP 2xx. The registry stops awaiting preparation after cancellation even if a contributor ignores the signal. Acceptance failures remain request failures under `REQUEST_EXTENSION`; transport and non-2xx failures never accept a contribution. A composition without the registry retains the reusable base adapter. Shipped compositions mount the registry and both contributors: both package metadata and Session-log upload are enabled by default; `session-log-deepseek.enabled: false` disables log upload under the [default-upload decision](2026-09-14-session-log-upload-default.md). Keyless `deepseek-official` replay invokes preparation with a synthetic empty base body and the same acceptance transaction before its first recorded chunk, preserving post-2xx extension side effects rather than field bytes.
`@deepseek-ai/dsh-deepseek-llm-api-extensions` registers `ctx.deepseekLlmApiExtensions`, an additive registry of top-level fields for `deepseek-official` request bodies. A contributor claims one declaration-merged field with `register()`. The adapter invokes `prepare()` after serializing the exact wire messages, passes the request cancellation signal, rejects preparation or base-field collision before HTTP, merges the detached fields, and calls the captured `accept()` transaction after HTTP 2xx. A merged body that fails to serialize is sent without extension fields or acceptance under the [bounded-upload decision](2026-09-24-bounded-session-log-upload.md). The registry stops awaiting preparation after cancellation even if a contributor ignores the signal. Acceptance failures remain request failures under `REQUEST_EXTENSION`; transport and non-2xx failures never accept a contribution. A composition without the registry retains the reusable base adapter. Shipped compositions mount the registry and both contributors: both package metadata and Session-log upload are enabled by default; `session-log-deepseek.enabled: false` disables log upload under the [default-upload decision](2026-09-14-session-log-upload-default.md). Keyless `deepseek-official` replay invokes preparation with a synthetic empty base body and the same acceptance transaction before its first recorded chunk, preserving post-2xx extension side effects rather than field bytes.
The provider-neutral `llm` package and `llm-pi-ai` contain no extension type, service lookup, field merge, or acceptance call.
## Incremental session-log field
`@deepseek-ai/dsh-session-log-deepseek` owns the default-on `dsh_session_log` field. When enabled, each request carrying a live Session id sends the contiguous canonical event suffix after the greatest durable `session-log-deepseek/delivery-accepted` watermark for that same Session identity. The field includes the immutable Session header and complete event envelopes. A 2xx appends a new watermark for the transmitted `throughSeq`; that event enters the following request's suffix. Forked logs retain parent watermark ids, so a child starts from sequence zero under its own identity. Concurrent acceptances may arrive out of order, and the maximum watermark remains authoritative. A process-local fold scans each Session event once and incrementally consumes later appends; a new Session object or HMR generation rebuilds the fold from durable history.
`@deepseek-ai/dsh-session-log-deepseek` owns the default-on `dsh_session_log` field. When enabled, each request carrying a live Session id sends the longest contiguous canonical event run after the greatest durable `session-log-deepseek/delivery-accepted` watermark for that same Session identity that fits the `maxBytes` limit of the [bounded-upload decision](2026-09-24-bounded-session-log-upload.md). The field includes the immutable Session header and complete event envelopes. A 2xx appends a new watermark for the transmitted `throughSeq`; that event enters the following request's suffix. Forked logs retain parent watermark ids, so a child starts from sequence zero under its own identity. Concurrent acceptances may arrive out of order, and the maximum watermark remains authoritative. A process-local fold scans each Session event once and incrementally consumes later appends; a new Session object or HMR generation rebuilds the fold from durable history.
The failure direction is at least once. A transport or provider rejection records no watermark. A crash after remote acceptance but before the watermark persists causes replay after resume, never a skipped sequence. Existing session checkpoints persist the event; the upload plugin owns no second store.
@@ -85,7 +85,7 @@ About 98% of the measured v1 real-session events were `assistant/chunk`. Omittin
## Consequences
Official DeepSeek requests carry active package versions to their resolved `baseURL`, including configured gateways. Unless Session-log upload is disabled, each eligible request also carries the complete newly unaccepted Session suffix. The fields are model-hidden and add no prompt tokens or KV-cache changes, but can substantially increase HTTP body size. Manifest resolution, field collision, acceptance logging, or provider schema rejection fails the model request rather than silently dropping metadata.
Official DeepSeek requests carry active package versions to their resolved `baseURL`, including configured gateways. Unless Session-log upload is disabled, each eligible request also carries newly unaccepted Session events, up to `maxBytes` per request. The fields are model-hidden and add no prompt tokens or KV-cache changes, but can substantially increase HTTP body size. Manifest resolution, field collision, acceptance logging, or provider schema rejection fails the model request rather than silently dropping metadata. A merged body that fails to serialize is the exception: the request proceeds without extension fields and logs a warning.
The `delivery-accepted` event becomes part of the canonical log and is itself delivered on a later request. Crash recovery can duplicate a suffix but does not infer acceptance from assistant output or create a second local cursor store. Direct calls without a live Session omit the session field; host package inventory remains available.
Subagent["subagent list / continuation"] --> Corpus
Subagent --> Observe
Subagent["subagent list / continuation"] --> Observe
Corpus --> Cache["projection cache hints"]
Cache --> ClientList["Client Session list"]
Cache -->|"small miss"| Observe
@@ -71,7 +70,7 @@ The registry owns fold state; each domain owns its `init`, `apply`, `view`, sche
`view` remains an uncached synchronous conversion over folded state. Its cost is bounded by the registered projection units and is paid at snapshot publication; introducing a second cache would add invalidation states without reducing event replay.
Corpus listing remains a separate lightweight operation. `listSessions()` returns live-preferred headers without materializing every log. Session list and subagent list first use live projection state or durable projection-cache rows. Session list may take one complete observation for an individually stored artifact within its configured small-log limit when cached metadata cannot establish whether it is blank; a large or unreadable cache miss remains visible with unknown hints.
Corpus listing remains a separate lightweight operation. `listSessions()` returns live-preferred headers without materializing every log. Session list first uses live projection state or durable projection-cache rows. Subagent discovery reads parent-catalog projections through exact observations. Session list may take one complete observation for an individually stored artifact within its configured small-log limit when cached metadata cannot establish whether it is blank; a large or unreadable cache miss remains visible with unknown hints.
`session.follow` publishes a required opening snapshot containing header, cursor, the initial event window, and a complete projection baseline. Reconnect replaces the previous generation from another complete snapshot. `session.page` is reserved for older-history reads and gap repair. Observation-only reads never activate an Agent; only an ordinary follow may retain its prepared observation and request promotion after the opening snapshot has been delivered.
@@ -86,7 +85,7 @@ Each public operation chooses one query and projection policy. The choice is par
| `session.follow` | One exact observation | All, carried in the opening snapshot | Ordinary cold Session only, after snapshot delivery |
| `session.page` | One exact observation | None, except projection-backed subagent authorization | Never |
| Attachment and fork source | One exact observation | None unless authorization requires it | Never for the source |
| Subagent list and continuation | Corpus plus live/cache/observation resolution | All on a cold fallback; audience consumes identity or inherited values | Never for listing; continuation follows its explicit command semantics |
| Subagent list and continuation | Parent-catalog observations for listing; target observations for continuation | All; listing consumes catalog values, continuation consumes identity and inherited values | Never for listing; continuation follows its explicit command semantics |
### Replayable Client facts are projection-owned
@@ -123,7 +122,7 @@ Client-local interaction state also remains local: loading and error status, an
- **Title and list metadata.** Cached projection hints may render an existing title and determine blankness or recency. Missing hints leave those facts unknown; only the bounded small-log policy may resolve them during listing. The Client reconciles list rows with the current metadata projection: nonblank evidence excludes a Session from blank reuse, and the later prompt timestamp supplies recency. Projection stores outlive lazy Client Session instances, so instantiation reads retained metadata even before a list row is available. A stale list response cannot override a newer history or control projection.
- **Model selection.** `model/selection` records a complete provider, model, and optional reasoning effort. `modelSelection` distinguishes the last request's route from a later selection pending consumption by a request header.
- **Agent preset.** The projection initializes from immutable Session metadata and advances on preset-selection events. A missing or `null` value is not replaced with the deployment default for an existing Session.
- **Subagent identity.** The `subagent` unit remains the sole descriptor interpreter. Listing obtains candidates from the shared corpus and resolves values through live state, projection cache, or an observation rather than scanning events itself.
- **Subagent identity.** The `subagent` unit remains the sole descriptor interpreter for continuation and history authorization. Listing derives membership, mode, and label from the `subagentCatalog` projection through parent observations; descendant discovery recursively reads reachable catalogs ([parent-catalog decision](2026-09-01-parent-owned-subagent-catalog.md)).
- **Subagent presentation.** Opening projection values establish timing and identity before the Client declares the child interactive or offline, so transport loading does not masquerade as a durable state.
These migrations remove special-case Client state without making projection own provider catalogs or interaction mechanics. A domain still owns mutations and commands; projection owns only their replayable Session result.
@@ -167,14 +166,14 @@ These rules apply to new Session-derived Client state even when a direct event s
- [Reusable Session preparation](../../archived/architecture/2026-08-05-session-preparation.md) owns cold materialization, repair, reservation, and publication. Observation adds a shared read lease over that prepared object; it does not move preparation into SessionQuery.
- [Session history and Remote event transport](2026-08-18-session-history-and-event-transport.md) owns stream generations and replacement semantics. This decision supplies the exact snapshot that opens each journal generation.
- [Projection state and Client views](../../archived/architecture/2026-08-19-session-projection-state-and-client-views.md) owns the distinction between Host fold state and Client values. This decision governs where those values are consumed and how partial list hints differ from a complete baseline.
- [Subagent identity projection](../../archived/architecture/2026-08-06-subagent-list-identity-projection.md) continues to own descriptor folding, the serializable `null` sentinel, and the own-suffix sequence check. This decision supersedes only its independent corpus merge and direct cold-inspection path: listing now uses SessionQuery's corpus and observation.
- [The parent-catalog decision](2026-09-01-parent-owned-subagent-catalog.md) owns subagent discovery. Descriptor folding, the serializable `null` sentinel, and the own-suffix sequence check remain relevant to continuation and history authorization, not catalog identity.
- The broader [session projection and command-log proposal](../../proposed/architecture/2026-07-27-session-projection-and-command-log.md) remains proposed for the portions not represented by shipped code. This decision records the shipped observation and Client-ownership subset.
## Verification
Persistence and SessionQuery tests pin shared cold loading, cancellation, live-source races, retained observations, disposal, and all-or-none projection calculation. Session Controller and Gateway tests pin snapshot-first opening, replacement reconnect, older-page reads, gap repair, list-cache hints, bounded small-log fallback, and promotion after snapshot delivery.
Client tests pin higher-sequence-wins projection storage, title updates, model catalog and selection readiness, preset roster refresh and Session-specific selection, and subagent loading without transient offline presentation. Subagent tests pin corpus enumeration, cache and observation fallback, lifecycle witnesses, bounded cold reads, and no Agent activation during listing.
Client tests pin higher-sequence-wins projection storage, title updates, model catalog and selection readiness, preset roster refresh and Session-specific selection, and subagent loading without transient offline presentation. Subagent tests pin parent-catalog order, recursive traversal, branch diagnostics, cancellation, fork isolation, and no Agent activation during listing.
- [Projection state 与 Client view](../../archived/architecture/2026-08-19-session-projection-state-and-client-views.md)拥有 Host fold state 和 Client value 的区分。本决策规定这些值在哪里消费,以及部分 list hints 与完整 baseline 的差别。
Loaded 100 of 4061 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.