dsh-wsl-workspace 0.7.4 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +761 -0
- package/CHANGELOG.zh.md +279 -0
- package/README.de.md +21 -5
- package/README.es.md +21 -5
- package/README.fr.md +21 -5
- package/README.ja.md +21 -5
- package/README.ko.md +21 -5
- package/README.md +25 -247
- package/README.pt.md +21 -5
- package/README.ru.md +21 -5
- package/README.zh.md +20 -111
- package/TESTING.md +210 -21
- package/lib/client.js +1422 -1048
- package/lib/client.js.map +1 -1
- package/lib/fs.js +12 -16
- package/lib/fs.js.map +1 -1
- package/lib/index.js +887 -139
- package/lib/index.js.map +1 -1
- package/lib/{links-DQ8GgpMj.js → links-CHu17hHQ.js} +8 -11
- package/lib/links-CHu17hHQ.js.map +1 -0
- package/lib/paths-CkIGMcuV.js +133 -0
- package/lib/paths-CkIGMcuV.js.map +1 -0
- package/lib/shell.js +149 -142
- package/lib/shell.js.map +1 -1
- package/lib/wsl-DN2-D6tr.js +231 -0
- package/lib/wsl-DN2-D6tr.js.map +1 -0
- package/lib/wsl-bash-tool-DAjyus4L.js +1923 -0
- package/lib/wsl-bash-tool-DAjyus4L.js.map +1 -0
- package/lib/wsl-bash-tool.js +2 -0
- package/lib/{wsl-credentials-gFHTqaiN.js → wsl-credentials-DzKgEzy7.js} +30 -5
- package/lib/wsl-credentials-DzKgEzy7.js.map +1 -0
- package/lib/wsl-env-sWOHjH2G.js +118 -0
- package/lib/wsl-env-sWOHjH2G.js.map +1 -0
- package/lib/wsl-jobs.js +102 -38
- package/lib/wsl-jobs.js.map +1 -1
- package/lib/wsl-relay.js +99 -15
- package/lib/wsl-relay.js.map +1 -1
- package/lib/wsl-sandbox.js +22 -4
- package/lib/wsl-sandbox.js.map +1 -1
- package/lib/wsl-search.js +56 -20
- package/lib/wsl-search.js.map +1 -1
- package/lib/wsl-terminal-tool.js +260 -0
- package/lib/wsl-terminal-tool.js.map +1 -0
- package/package.json +67 -26
- package/image-2.png +0 -0
- package/image-3.png +0 -0
- package/lib/links-DQ8GgpMj.js.map +0 -1
- package/lib/wsl-BTA7XT8X.js +0 -308
- package/lib/wsl-BTA7XT8X.js.map +0 -1
- package/lib/wsl-credentials-gFHTqaiN.js.map +0 -1
- package/src/client/AddWslWorkspace.tsx +0 -375
- package/src/client/api.ts +0 -145
- package/src/client/help.tsx +0 -99
- package/src/client/index.ts +0 -361
- package/src/client/locales.ts +0 -100
- package/src/client/styles.ts +0 -356
- package/src/fs.ts +0 -451
- package/src/host/variants.ts +0 -479
- package/src/host/wsl-jobs.ts +0 -229
- package/src/host/wsl-relay.ts +0 -89
- package/src/host/wsl-sandbox.ts +0 -47
- package/src/host/wsl-search.ts +0 -1166
- package/src/host/wsl-skills.ts +0 -821
- package/src/index.ts +0 -882
- package/src/shared/links.ts +0 -99
- package/src/shared/paths.ts +0 -175
- package/src/shared/wsl-credentials.ts +0 -135
- package/src/shared/wsl.ts +0 -189
- package/src/shell.ts +0 -451
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `dsh-wsl-workspace`, newest first. Back to the [README](README.md); the Chinese record is [CHANGELOG.zh.md](CHANGELOG.zh.md).
|
|
4
|
+
|
|
5
|
+
## 0.7.7 — 2026-10-07
|
|
6
|
+
- **What a user installs is now the plugin, not a copy of the repository.** `package.json#files` shipped
|
|
7
|
+
`src/` and both README screenshots: a fresh `npm pack` of the previous tree is **2,757,545 B across 79
|
|
8
|
+
members**, of which 2,265,091 B (82.1%) was the 30 source files and the 2 images — neither of which
|
|
9
|
+
anything at runtime reads (the plugin loads `lib/`; the source-reading gates read the repository). The
|
|
10
|
+
same pack is now **492,454 B across 47 members**, and the only difference between the two member lists
|
|
11
|
+
is the 32 removed entries. The nine READMEs point their screenshots at absolute
|
|
12
|
+
`raw.githubusercontent.com/…/HEAD/…` URLs in the same commit, so the npm page still renders without the
|
|
13
|
+
bytes in the tarball — chosen deliberately over relying on the registry rewriting a relative path.
|
|
14
|
+
|
|
15
|
+
- **The deliberate-red ledger is empty, and the ten debts it held are paid.** Every entry was a
|
|
16
|
+
reproduction with a stated repair; all ten are now fixed, and the mechanism outlived them — a NEW red
|
|
17
|
+
nobody declared still fails the gate.
|
|
18
|
+
|
|
19
|
+
- **`check` folded every `stat` failure into `{exists:false}`** (issue #44 §6). A directory nobody can
|
|
20
|
+
read, or an `EIO` over 9P, was reported as *not existing* — the one thing a caller cannot tell apart
|
|
21
|
+
from the truth. Only `ENOENT` answers `exists:false` now; everything else is thrown, and the envelope
|
|
22
|
+
carries the OS's own sentence.
|
|
23
|
+
- **The decode policy was "does this buffer contain a NUL, then it is UTF-16LE"**, so a NUL-delimited
|
|
24
|
+
UTF-8 stream (`find -print0`, `grep -Z`, `git ls-files -z` — which this plugin's own search path
|
|
25
|
+
speaks) came back as mojibake at double length, silently. The sniff is now structural: a BOM, or NULs
|
|
26
|
+
at a regular parity and a real density. A caller that knows its encoding passes it and skips the
|
|
27
|
+
heuristic entirely.
|
|
28
|
+
- **`reg.exe`'s answer was decoded with a hard-coded UTF-8 conversion** while the same module owned that
|
|
29
|
+
decoder, so a UTF-16LE answer yielded `undefined` — an empty picker, no throw, no log. One decode
|
|
30
|
+
policy now serves every captured stream.
|
|
31
|
+
- **The search path decoded stderr at the capture point**, which put a NUL between every character of a
|
|
32
|
+
failure detail, counted the 300-character budget in bytes, and made the invalid-pattern classifier
|
|
33
|
+
unable to ever match. `WslRun.stderr` is bytes; the decode happens where the text is used, the budget
|
|
34
|
+
cuts characters, and the classifier reads decoded text.
|
|
35
|
+
- **A launch failure dropped the reason `wsl.exe` gave** (which distribution is missing) because the
|
|
36
|
+
detail came only from stderr. It now falls back to the first decoded line of stdout.
|
|
37
|
+
- **Three call sites handed an argument list to a command interpreter.** `verify-install.mjs`,
|
|
38
|
+
`verify-artifact-identity.mjs` and `ci/install-pinned.mjs` all fell back to a shell on Windows when
|
|
39
|
+
`npm_execpath` was absent — the shape Node now refuses (DEP0190) and that re-tokenises a spaced path
|
|
40
|
+
and passes `& | > < ^ %` to the interpreter. npm's own `bin/npm-cli.js` is now looked for in three places —
|
|
41
|
+
`npm_execpath`, beside `node.exe`, and through the `npm` on `PATH` — and a missing one is named
|
|
42
|
+
rather than guessed. The third was added because the second is not true on a hosted tool cache,
|
|
43
|
+
where a job that was never broken died with "npm's entry script was not found".
|
|
44
|
+
- **`install-pinned.mjs` created junctions through a shell with its output discarded**, which threw away
|
|
45
|
+
the linker's sentence and left the operator a bare exit number. `fs.symlinkSync` is the same junction
|
|
46
|
+
and arrives as an Error carrying `EEXIST`/`EPERM`.
|
|
47
|
+
|
|
48
|
+
Hazards A, B and E were **measurements of Node and the OS**, not of this repository: each test
|
|
49
|
+
reproduced a shape and watched the information disappear, so fixing the product left them red. They are
|
|
50
|
+
now assertions about this repository's own call sites and fail if a site returns; the measurements stay
|
|
51
|
+
above them as the reason those rules read the way they do. `tests/deliberate-reds.test.mjs` now
|
|
52
|
+
exercises the ledger mechanism against its own synthetic entries, because a control that reads the real
|
|
53
|
+
array stops covering the gate the moment the last debt is closed.
|
|
54
|
+
|
|
55
|
+
- **`bash_background`'s `job_output` was always empty on DSH 0.2.x (issue #56).** `bash_background` (and `bash`'s `run_in_background: true`, which shares the same producer) started the job and `job_list` tracked it, but `job_output` returned `(no new output)` on every read — during the run, after completion, and the completion notice carried no body. The command *did* run and its stdout landed on disk, so the read channel was dead, not the task.
|
|
56
|
+
|
|
57
|
+
The root cause is a registry contract change between release lines. `@deepseek-ai/dsh-jobs-local@0.2.x` fills the job's output ring **only** from the `spec.output` pull-source array (`start()` drains each source's `read(from)` on later ticks); it never inspects the `readOutput` that `run()` returns. This plugin's producer, however, only set `readOutput` (the 0.1.x contract), so `spec.output` was empty, the pump was never armed, and the ring stayed empty. The fake registry the unit tests used implemented the old `run()`-`readOutput` semantics, so the suite stayed green while a real 0.2.0-rc.2 host went red.
|
|
58
|
+
|
|
59
|
+
The producer now feeds **both** channels, and each release line reads the one it understands: a `spec.output` array of two pull-sources — `stdout` and a `stderr`-tagged source — each reading from the shell process's own per-stream `observed` readers (the same independent cursors `readOutput` already drained, so the two never steal bytes from each other), plus the `run()`-returned `readOutput` kept for the 0.1.x registry. `spillPath` flows through unchanged, so an overflowed stream still reports its spill file, and `stderr` stays on its own lane via the channel tag.
|
|
60
|
+
|
|
61
|
+
A unit gate now mirrors the 0.2.x drain loop against the produced `output` sources and would have failed on the old producer (no sources, or none yielding the bytes), so this regression cannot silently return.
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
- **Every `bash` call in a WSL workspace failed on DSH Desktop 0.2.x (issue #51), and the fix is
|
|
65
|
+
three separate things.** (1) *The shell seam*: 0.2.x calls `ctx.shell.execute(spec)` and awaits
|
|
66
|
+
`result()`, while this plugin implemented the 0.1.x `resolve`/`run`/`start` — so **both** the
|
|
67
|
+
persistent path and the one-shot fallback were dead, not one of them. One spawn primitive now
|
|
68
|
+
serves all three faces, with `onExpiry` and `observed` filled in; the CI gate that hid it was the
|
|
69
|
+
typecheck **error-count** budget (the `TS2515` line was in the output the whole time), which now
|
|
70
|
+
reddens on banned codes at any count, self-tests itself, and may only ever be lowered
|
|
71
|
+
(212 → 209). (2) *The readiness contract*: the host injects `PS1`/`PROMPT_COMMAND` as Windows
|
|
72
|
+
variables, which WSL drops unless `WSLENV` names them, so the persistent shell came up with the
|
|
73
|
+
distribution's own prompt and the host could not recognise it — one bridge in the relay, proven on
|
|
74
|
+
a real ConPTY (before: default prompt; after: `prompt=[dsh> ]` with the `133;D;` marker), and the
|
|
75
|
+
contract values now have a parity gate against the installed host instead of a hand copy.
|
|
76
|
+
(3) *The persistent shell itself*: the host's PTY tool decides "finished" by matching a sentinel
|
|
77
|
+
line in terminal text and requiring the exit code to be followed immediately by a newline, and an
|
|
78
|
+
interactive Linux shell repaints with `ESC[<n>X`, filling the cells after the sentinel with
|
|
79
|
+
spaces — measured in a real Desktop session as **three calls hanging 303.8 s each** before the
|
|
80
|
+
host wiped the shell. The world now mounts this plugin's own `bash`: one long-lived
|
|
81
|
+
`bash --norc -i` over **pipes**, completing on a NUL-delimited record carrying a per-command
|
|
82
|
+
nonce, with the login environment sourced silently, history expansion off (which also closes the
|
|
83
|
+
`!` class of failures for this path), a pseudo-terminal of its own for a command caught waiting for
|
|
84
|
+
input (`script -qec`, decided by reading the process rather than its name — see the bullet on the
|
|
85
|
+
reading; `tty: true` asks for one up front), and a session rebuild that replays `cd` and exported
|
|
86
|
+
variables when a call wedges
|
|
87
|
+
*without* executing a merely-slow command a second time.
|
|
88
|
+
Measured on this machine, both build planes: first call ~0.5 s including boot, then a **median of 8 ms**
|
|
89
|
+
(measured 2026-10-06, once the answer stopped waiting for the state record — the same command through
|
|
90
|
+
one `wsl.exe` per call is 222-236 ms, and a person typing into an open terminal sees 16 ms), with
|
|
91
|
+
`cd`/`export`/exit codes/CJK/`!`/`sed -i`/`tar`/`git commit`/`sudo` all verified, while the same
|
|
92
|
+
command through the PTY tier still times out — that control is the last cell of the new
|
|
93
|
+
`bash-session-real` gate, so the replaced behaviour cannot silently come back. Verified end to end
|
|
94
|
+
through **real host tool dispatch** (the host's own `bash` tool calling our executor, in
|
|
95
|
+
`tool-bash-real`), and `DSH_WSL_PTY_SHELL=1` keeps the old tier reachable. **Clicked inside DSH
|
|
96
|
+
Desktop with this build installed**, and the clicks found two defects the offline gates could not
|
|
97
|
+
see — the host's plugin loader unwraps `exports.default`, so a module-level `inject` never reaches
|
|
98
|
+
the fiber and every call failed `cannot get property "subprocess" without inject`; and the shell's
|
|
99
|
+
echo of a frame reaches stderr as only its *tail*, so a fragment of our own framing was in the
|
|
100
|
+
model's body on every call. Both are fixed and both are now gated by cells that enter through the
|
|
101
|
+
channel the product uses.
|
|
102
|
+
|
|
103
|
+
## 0.7.6 — 2026-10-01
|
|
104
|
+
- **A second pass made the session `bash` answer like the host's `bash`, field by field.** A matrix of
|
|
105
|
+
34 everyday commands found four behaviours that were not everyday-good, and all four are fixed:
|
|
106
|
+
`run_in_background: true` was an argument silently ignored (it now delegates to the same jobs
|
|
107
|
+
producer `bash_background` uses); a relative `workdir` resolved to nothing and ran wherever the last
|
|
108
|
+
call had left the shell (it now joins onto the session directory and lets bash's own `cd` error
|
|
109
|
+
speak, as the host's `resolveWorkdir` does); a stream larger than the cap lost its head with only a
|
|
110
|
+
boolean to say so (it now spills to a file and says `[output truncated; full output: …]`, the same
|
|
111
|
+
sentence and the same directory as the one-shot path, one file per command); and a rebuild left
|
|
112
|
+
detached children running (it now reaps exactly the processes carrying its own session token, with a
|
|
113
|
+
control process that must survive). The journal replays shell options, `shopt`s, aliases and
|
|
114
|
+
functions as well as `cd` and exports — chunked into separate frames, because `eval` parses its
|
|
115
|
+
whole string before running any of it, so a `shopt` on line 50 cannot rescue a completion function
|
|
116
|
+
on line 1623 that needs extglob to be *parsed*. What differs is now a table two gates read:
|
|
117
|
+
`tests/wsl-bash-parity.test.ts` against the installed host package, and
|
|
118
|
+
`scripts/compatibility/bash-parity-real.mjs` running one probe script through both tools on a real
|
|
119
|
+
distribution — an undeclared difference, or a declared one that stopped being true, fails the build
|
|
120
|
+
([docs/bash-parity.md](docs/bash-parity.md)). Footprint measured: ~9.1 MB of Windows working set per
|
|
121
|
+
session across two `wsl.exe` processes, 3.4 MB inside the distribution, `vmmem` unchanged, no runtime
|
|
122
|
+
files until a stream overflows.
|
|
123
|
+
|
|
124
|
+
- **An agent's shell now ends with the agent.** The session `bash` registered its cleanup on the
|
|
125
|
+
plugin's scope only, so every agent that had ever called `bash` left a shell behind — two `wsl.exe`
|
|
126
|
+
and about 9 MB of Windows working set — until the whole world was disposed. The cell that found it
|
|
127
|
+
drives a second agent through the same registered tool and asks what happens when one of them stops.
|
|
128
|
+
The other isolation boundaries it now pins were already true and had never been tested: a second
|
|
129
|
+
agent does not inherit the first one's directory, exports or aliases; two calls issued at once each
|
|
130
|
+
settle with their own answer; and a rebuild's process sweep stops only the processes carrying its
|
|
131
|
+
own session token, so one agent's wedged shell cannot reap another's work. `sudo` also says what it
|
|
132
|
+
needs: where the distribution asks for a password, the reply now adds that this shell has nobody to
|
|
133
|
+
type it and names the two ways out, instead of leaving sudo's own line for the model to retry. And an
|
|
134
|
+
alias used on the line that defines it answers 127 here exactly as it does in `bash -ic` on the same
|
|
135
|
+
distribution — measured, and recorded as bash's rule rather than ours.
|
|
136
|
+
|
|
137
|
+
- **Three seams in the pseudo-terminal tier, found by driving it rather than reasoning about it.** An
|
|
138
|
+
emphasising program on a terminal it cannot colour writes overstrike, and a measured `man` page came
|
|
139
|
+
back as `N\bNA\bAM\bME\bE` — unreadable where a person at a real terminal reads `NAME`; the fold now
|
|
140
|
+
resolves the two shapes this distribution was measured emitting (`X\bX`, `_\bX`) the way a terminal
|
|
141
|
+
does. The escalation decision read only the command's first word, so `bash -c 'sudo true'` — a shape
|
|
142
|
+
models write constantly — sat to its deadline returning `(no output)` plus a session rebuild where
|
|
143
|
+
bare `sudo true` answers in 46 ms; one layer of a `bash`/`sh`/`zsh`/`dash` `-c` wrapper is now read,
|
|
144
|
+
and a wrapper whose contents cannot be read is left alone rather than escalated on a guess. And
|
|
145
|
+
`tty: false` did nothing: with only `true` honoured, `man ls` came back through the pty with no way
|
|
146
|
+
back to the plain pipe, so `false` is a veto now. What is *not* fixed, because it is what a terminal
|
|
147
|
+
is: an escalated call has one stream, so the `[stderr]` section the plain path writes cannot appear
|
|
148
|
+
there (the host's own PTY tier is the same — it is a row in [docs/bash-parity.md](docs/bash-parity.md)
|
|
149
|
+
with a probe behind it), and a program on a terminal that nothing can type into used to cost its
|
|
150
|
+
deadline plus a rebuild — measured 5.7-6.1 s with a screen dump — because the frame gives a pty
|
|
151
|
+
`/dev/null` for input rather than letting the program eat the next command's bytes; that case is what
|
|
152
|
+
the reading below now ends early.
|
|
153
|
+
|
|
154
|
+
- **A command waiting for a keyboard is diagnosed by reading the distribution, not by matching its
|
|
155
|
+
name — and it gets an answer instead of a deadline.** Nothing in this release ships a list of command
|
|
156
|
+
names. The path here was: one whitelist of first words → three "classes" (credential / keyboard /
|
|
157
|
+
pager) with an 8-second ceiling on one of them → all of it deleted in favour of a second look taken
|
|
158
|
+
from outside the shell while the call is in flight. Each step is kept in the record because each was
|
|
159
|
+
paid for by a measurement: the whitelist stopped at a hyphen so `ssh-copy-id`/`ssh-keygen`/`redis-cli`
|
|
160
|
+
could never match it; reading only the first word cost **121 703 ms** in a real session, where the
|
|
161
|
+
model sent `{"command":"printf x; vim note.txt","timeoutMs":15000}` and got the screen's raw escapes
|
|
162
|
+
after the two-minute default had run out; honouring a named deadline bought nothing on a wait that can
|
|
163
|
+
never be satisfied, since an agent reads a 16 889 ms silence as "this environment is slow" and
|
|
164
|
+
reprices every later call — while `tar -cf /dev/null /usr` legitimately takes 15 s and `vim -es` over
|
|
165
|
+
**1 000 000 lines** takes 1 s, so any rule that stops a quiet command must be able to tell those apart.
|
|
166
|
+
What ships reads, per sample (200–280 ms; first look at 600 ms of silence, then every 400 ms for the
|
|
167
|
+
first few looks and every 2 s after that): the process state,
|
|
168
|
+
whether the job owns the terminal's foreground group, `/proc/<pid>/wchan`, whether a terminal is among
|
|
169
|
+
the process's descriptors, and whether its CPU is advancing (`/proc/<pid>/schedstat`). `sleep`, a
|
|
170
|
+
network wait and a build are each excluded by a different one of those columns; a program asleep in a
|
|
171
|
+
terminal read is not. It then stops the wait — `SIGCONT` first, because a stopped process ignores
|
|
172
|
+
`SIGTERM` — and runs the command once more on a pseudo-terminal inside the same call, where the
|
|
173
|
+
keyboard read meets end-of-file and the program prints its own complaint. Measured on the real
|
|
174
|
+
session (`D:\Temp\issue51-s0\v5-run.txt`): `sh -c 'read x < /dev/tty'` — a program called `sh`, in no
|
|
175
|
+
list that ever existed — stopped at 628 ms and answered at 1 760 ms; `sleep 4` was untouched;
|
|
176
|
+
`sudo` asking for a password, which hides its `/proc` entries by clearing its dumpable flag, is
|
|
177
|
+
read through the distribution's root rights — a second pass of the same probe, run as root, sees the
|
|
178
|
+
wait its own user cannot; it reads `/proc` and nothing else, runs at most once per call, and a
|
|
179
|
+
distribution where root cannot be used falls back to the unconfirmable reading and says so — and came
|
|
180
|
+
back with sudo's own words at 2 412 ms; a 60 000 ms deadline asked
|
|
181
|
+
of a keyboard wait returned in 1 733 ms. Two things are said out
|
|
182
|
+
loud rather than done quietly: **the command has now run twice** ("anything it had already done before
|
|
183
|
+
that prompt has now been done twice"), because an unannounced second execution is the defect this
|
|
184
|
+
ticket already caught once; and if the `/proc` walk never answers, the body says the check could not
|
|
185
|
+
run instead of leaving a silent deadline — a premise that has gone away is not allowed to look like a
|
|
186
|
+
verdict. And when the walk does answer but this kernel reports no sleep location — which is what the
|
|
187
|
+
WSL1 runner does, `wchan` and `/proc/<pid>/syscall` empty for every process, no foreground job named —
|
|
188
|
+
the deadline body prints the rows it read and says so (`w=not-reported`, beside `w=running` for a
|
|
189
|
+
process on the CPU and `w=0` for one it may not look inside), instead of leaving "timed out" to be read
|
|
190
|
+
as "examined and found ordinary". `tty: true` asks for a terminal up front and `tty: false` vetoes the second attempt; the
|
|
191
|
+
pager and live-display behaviours that the class lists existed to encode are simply what the pipe does
|
|
192
|
+
(`man` prints the page, `top: failed tty get` refuses in 687 ms), and the sheet of which symptom
|
|
193
|
+
belongs to which layer is [docs/tty-triage.md](docs/tty-triage.md).
|
|
194
|
+
|
|
195
|
+
- **A repeated failure is named, and still executed.** The same command bytes failing twice in the same
|
|
196
|
+
shell adds `[this exact command has failed 2 times in this shell with nothing succeeding in it since: …]`,
|
|
197
|
+
and any success clears every streak — the claim is about a shell where nothing has worked since, so an
|
|
198
|
+
`npm install` succeeding between two failing `npm test` calls must not be talked about as a stuck loop.
|
|
199
|
+
Deliberately not a refusal: a tool declining to run
|
|
200
|
+
what it was asked is the shape this ticket is about, and the ordinary case — `npm test` passing on the
|
|
201
|
+
third try after an install — is exactly what a refusal would break.
|
|
202
|
+
|
|
203
|
+
- **`sudo`'s note offers the human first.** The product has interactive terminal tabs in the right sidebar
|
|
204
|
+
(`dsh-client-ui-sidebar-terminal`; the host's own PTY, which this plugin does not touch) where a person
|
|
205
|
+
can type the password. That is now the first way out, with NOPASSWD / `DSH_WSL_USER=root` second as the
|
|
206
|
+
way to make the agent able to run it alone.
|
|
207
|
+
|
|
208
|
+
- **The keyboard hint is given by the reading, not by whether we applied our own bound.** The first live
|
|
209
|
+
session of this on the installed desktop did not send `vim note.txt`; the model sent
|
|
210
|
+
`{"command":"printf x; vim note.txt","timeoutMs":15000}` — naming its own deadline, which under the
|
|
211
|
+
older rule bought it a generic sentence instead of an explanation of the wait. The body now says what
|
|
212
|
+
was seen whatever the call asked for. A second cell makes the timeout wording check itself: it may
|
|
213
|
+
claim a restart only when the session reported one, and the measurement there is that an escalated
|
|
214
|
+
`sleep` really does rebuild the session.
|
|
215
|
+
|
|
216
|
+
- **The ordinary-commands checklist was driven through a real dsh session, and that is what found the
|
|
217
|
+
121 703 ms.** A real DSH 0.2.0-rc.2 instance (its own `DSH_HOME`, its own port, a scripted local
|
|
218
|
+
provider so no inference was bought): 27 rows of everyday commands, read back from the session's own
|
|
219
|
+
durable log rather than from a transcript someone has to relay. Most rows answered as designed —
|
|
220
|
+
`sudo`'s own verdict with the password note, the pager class in 73 ms, `man`/`less` printing whole
|
|
221
|
+
documents — and two rows did not: the compound `printf`-then-editor line above, and a cell whose
|
|
222
|
+
expectation was written for the wrong medium. The pass also caught a false promise in the tool
|
|
223
|
+
description: a live display on a pipe does not print a document, it refuses — `top: failed tty get`,
|
|
224
|
+
exit 1, 687 ms — so the description says that and points at `top -bn1`.
|
|
225
|
+
|
|
226
|
+
- **A failure this tool did not predict now names its own layer.** An escalated call that ends badly
|
|
227
|
+
without matching one of the known shapes (a password, a waiting keyboard) appends
|
|
228
|
+
`[this call ran on a pseudo-terminal (script -qec, one stream): re-run the same command with "tty": false to rule this layer out before looking anywhere else]`,
|
|
229
|
+
and every terminal decision is logged at debug level — `wsl-bash: stopped a command waiting for input
|
|
230
|
+
(terminal at 601ms) and re-running it on a pseudo-terminal` — so a `dsh web`/`headless` log answers
|
|
231
|
+
the question too (the installed desktop keeps its child's stdout in memory only, so there the
|
|
232
|
+
transcript lines are the readable half). The sheet of which symptom belongs to which layer — including
|
|
233
|
+
the host's PTY tier's own sentinel, the one issue #51 was filed against — is
|
|
234
|
+
[docs/tty-triage.md](docs/tty-triage.md), and every line it names is asserted by a cell in
|
|
235
|
+
`bash-session-real` (55 cells now, on both planes).
|
|
236
|
+
|
|
237
|
+
- **A live model found the one shape the cells could not: a builtin that reads the terminal blocks the
|
|
238
|
+
*shell itself*.** The build was driven end to end by a real model on a real token plan — four calls in a
|
|
239
|
+
WSL workspace, every command chosen by the model — and it picked \`read -r line < /dev/tty\` where every
|
|
240
|
+
cell in this repository used \`sh -c …\`. A child that blocks is found by walking the shell's
|
|
241
|
+
descendants; a *builtin* has no child, so nothing was classified and the call died on its 30 s deadline
|
|
242
|
+
with no note (the model's own summary noticed, and said so). Measured while it happens, the shell's own
|
|
243
|
+
row is \`Ss+ pgid==tpgid wchan=wait_woken fd0=/dev/tty\` with CPU flat — the same signature, one level up —
|
|
244
|
+
so the shell is now part of the walk, marked, because between commands it waits on a pipe and must not
|
|
245
|
+
be read as a wait. Freeing it took a measurement rather than a hope: \`kill -INT\` does **not** work (bash
|
|
246
|
+
catches it and the read syscall restarts — the call ran to its deadline with the signal delivered), so
|
|
247
|
+
the shell is stopped like any other wedged process, the session rebuilds from its journal, and the
|
|
248
|
+
command is re-run on a terminal by the same retry path. Same prompt, second run: that call came back in
|
|
249
|
+
**3 104 ms** with \`read-exit=1 x=''\` and the note naming the restart, and the model's closing summary
|
|
250
|
+
quoted it. Three further defects surfaced while landing it, all announced by a cell: the stop set named
|
|
251
|
+
every row in the sample (which now always contains the shell), the privileged path took its stop set
|
|
252
|
+
from the unreadable user-plane rows instead of the witness's, and a watchdog-stopped call was reported
|
|
253
|
+
in the caller-cancel shape — which made the live gate abort its own run.
|
|
254
|
+
|
|
255
|
+
- **The terminal is given by a reading, not by a list, and the second attempt is announced.** Three sets
|
|
256
|
+
of command names — credential, keyboard, pager — decided whether a call got a pseudo-terminal, and how
|
|
257
|
+
long it was allowed to wait. They are gone: they were the maintenance cost of this layer and the source
|
|
258
|
+
of two of its defects, every distribution could disagree with them, and the thing they were standing in
|
|
259
|
+
for is directly observable. What replaces them is one function (`src/host/wsl-bash-starve.ts`) that
|
|
260
|
+
reads the distribution from outside the shell while the call is in flight, and a stop-plus-retry that
|
|
261
|
+
acts on what it finds. The full shape, the measurements behind each threshold and the doors a caller
|
|
262
|
+
can use are in the bullet above and in [docs/tty-triage.md](docs/tty-triage.md).
|
|
263
|
+
|
|
264
|
+
- **A DSH Desktop profile generated no WSL variant at all (issue #47).** The variant
|
|
265
|
+
generator asked the host for two modules at call time — the entry-list dialect and the
|
|
266
|
+
YAML engine under it — on the assumption that host and plugin share a `node_modules`.
|
|
267
|
+
Desktop ships the host inside an archive, and Node finds a bare specifier by walking the
|
|
268
|
+
filesystem upward, so that walk never reaches it. The first healthy source preset threw,
|
|
269
|
+
the throw left the generation loop, and everything after the loop — including the sweep
|
|
270
|
+
of the retired mechanism's leftovers — was skipped. The dialog's only answer was that no
|
|
271
|
+
healthy `wsl` preset exists, and the cause sat in one swallowed host-console line.
|
|
272
|
+
Reproduced on this machine with real packages rather than inferred: a dialect built
|
|
273
|
+
against the release the host umbrella hoists, loaded through the release a sibling plugin
|
|
274
|
+
hoists, fails inside the loader, on wording that names neither package, version nor path.
|
|
275
|
+
- **The dialect is built in the plugin now** (`src/index.ts`), from the same engine
|
|
276
|
+
namespace that parses with it, so schema and engine cannot come from different majors.
|
|
277
|
+
Only the read half of the published schema was ever used here; its write half serves a
|
|
278
|
+
path this plugin does not take. Six lines, no new dependency.
|
|
279
|
+
- **The engine is a real dependency** (`js-yaml: ^4.1.0`, resolving to 4.3.2 — the release
|
|
280
|
+
the host umbrella itself hoists). Declaring it an optional peer with a `*` range had
|
|
281
|
+
handed the choice of major to whichever release the profile happened to carry, and with
|
|
282
|
+
`autoInstallPeers` off an optional peer is never installed at all. The sibling plugin that
|
|
283
|
+
keeps working in the same profile keeps working precisely because it declares a real
|
|
284
|
+
dependency; this now matches. The two peer entries nothing imports anymore are deleted,
|
|
285
|
+
so the manifest stops describing an import that does not exist.
|
|
286
|
+
- **One failing source is one variant's failure.** Generation is fault-tolerant per source,
|
|
287
|
+
the retired-directory sweep runs regardless, and the outcome is a count line —
|
|
288
|
+
`WSL preset variants: n/m registered` — because proving that routes answer had never
|
|
289
|
+
proved that anything was generated. On `dsh web` that line is in the boot log and the
|
|
290
|
+
compatibility matrix now asserts it; **on DSH Desktop it is not persisted anywhere**
|
|
291
|
+
(measured on a real install: its log directory holds only crash bundles, which embed a
|
|
292
|
+
child's stderr solely when that child exits non-zero, and capture only *renderer* console
|
|
293
|
+
output — so a main-process `console.error` has no sink). Making the outcome visible there is
|
|
294
|
+
issue #47's own recommendation #3 and is now filed as a separate ticket, on measured
|
|
295
|
+
grounds rather than as polish. Before this change, a source that vanished
|
|
296
|
+
mid-update preserved the previous complete variant only by accident: the abort skipped
|
|
297
|
+
the sweep that would otherwise delete it. The accident is gone, the contract stays, and
|
|
298
|
+
`tests/host-materialize.mjs` caught the difference on the first run.
|
|
299
|
+
- **A dialect failure names what it stood on** — package, version and path. That act is
|
|
300
|
+
decided on a user's machine by an installer; it has since been measured through a real pnpm
|
|
301
|
+
layout rather than assumed (see the last bullet), and the line still names the copy so a
|
|
302
|
+
frame on some other installer carries its own conclusion.
|
|
303
|
+
- **New gate** `tests/host-profile-isolation.mjs` (`npm run test:profile`): profile-shaped
|
|
304
|
+
trees under the temp dir, each booting the plugin's own copy — the loaned schema absent,
|
|
305
|
+
the hoisted engine on the wrong major, both at once, and one unreadable source among
|
|
306
|
+
healthy ones. Its control arm, its dialect-equivalence check against the pinned host
|
|
307
|
+
schema, and its probe that every provider path a declaration names really imports were
|
|
308
|
+
green on the *same* frame as the eight reds it was written to produce; three separate
|
|
309
|
+
mutations of the fix each reddened a different subset, which is what keeps the two
|
|
310
|
+
defects from being reported as one. `ci/install-pinned.mjs` materialises the hostile
|
|
311
|
+
engine release in a second tree that is never linked into the repo root.
|
|
312
|
+
- **Two older gates were asking the wrong question.** `scripts/verify-install.mjs` now
|
|
313
|
+
asserts the installed tree's runtime surface too: every declared dependency must be
|
|
314
|
+
reachable from the installed `lib/` and on the declared version line, and an empty
|
|
315
|
+
`dependencies` is itself a refusal — its earlier green was *produced* by the engine being
|
|
316
|
+
absent, which is the state the issue reported. `scripts/verify-dsh-compat.sh` asserts the
|
|
317
|
+
outcome count per matrix release: generating less than it was offered is `VARIANTS_FAIL`,
|
|
318
|
+
and a line that never appears is `VARIANTS_NOT_VERIFIED`, never a pass.
|
|
319
|
+
- **What this machine could not decide, and what it did.** Measured for real, with pnpm's
|
|
320
|
+
hoisted linker and the reporter's own `autoInstallPeers: false`: the published 0.7.5 in a
|
|
321
|
+
profile tree whose sibling package hoists `js-yaml` 5.x generates nothing, and this build
|
|
322
|
+
in that same tree gets its engine nested under itself and registers every source it was
|
|
323
|
+
offered. The same pair was then run in this machine's own Desktop profile shape — and it
|
|
324
|
+
turned out the real thing is installed here: `.dsh\profiles\desktop` carries 0.7.5 under
|
|
325
|
+
exactly those settings with no engine and no include package on its walk-up, which is the
|
|
326
|
+
report's shortage sitting on a maintainer machine the whole time. Still unmeasured: the
|
|
327
|
+
packaged host **process** — reading that profile is free, launching a GUI plus its
|
|
328
|
+
self-updater on a machine its owner is using is not, and it waits for a nod. The
|
|
329
|
+
reporter's host-console line, which his report nominated as decisive, turns out not to be
|
|
330
|
+
reachable by him at all: a healthy Desktop boot persists no host output (above), so that
|
|
331
|
+
check is asked of a maintainer with a shell, and the reply drafted for #47 asks him instead
|
|
332
|
+
for the observable — whether the dialog lists the variants.
|
|
333
|
+
Both limits, and the frames that back the rest, are in `docs/compatibility-evidence.md`.
|
|
334
|
+
- **A WSL session's file references preview again (issue #49).** The conversation's file
|
|
335
|
+
links, a tool row's line reference and the turn tail's changed files all open through the
|
|
336
|
+
right Sidebar's navigation controller, and the address they carry holds the path exactly as
|
|
337
|
+
the model wrote it — a Linux path. The host decodes that address and resolves it with
|
|
338
|
+
`node:path.resolve(cwd, path)`, where a POSIX absolute path is root-relative: `/mnt/d/x`
|
|
339
|
+
landed on the workspace drive (`D:\mnt\d\x`), an in-distribution path under the cwd share's
|
|
340
|
+
root, and a `/mnt/<drive>` reference under a UNC cwd became
|
|
341
|
+
`\\wsl.localhost\<distro>\mnt\<drive>\…`, which 9P refuses with `EPERM`. Reproduced on the
|
|
342
|
+
real DSH Desktop both ways before the fix: `文件不存在,可能已被移动或删除` for a file the
|
|
343
|
+
`write` tool had just created at that very path, and `EPERM` for the same reference from a
|
|
344
|
+
UNC workspace.
|
|
345
|
+
- **The repair is client-side, because the host plane offers this plugin no hook.** `fs` and
|
|
346
|
+
the `workspaceFiles` endpoint belong to other plugins, and cordis refuses a second
|
|
347
|
+
`provide` for a name another fiber owns, while neither publishes a path-resolution seam.
|
|
348
|
+
The client half now translates the address before the Sidebar sees it — `/mnt/<drive>/…`
|
|
349
|
+
back to `X:\…`, and every other Linux path through the distribution's UNC share — at
|
|
350
|
+
`openResource` / `openResourceIn`, the one entry every reference surface goes through, so
|
|
351
|
+
the tab's content and the metadata read under the same address are repaired together. A
|
|
352
|
+
translated path inside the workspace becomes the same relative address the Files panel
|
|
353
|
+
builds, so the Sidebar reveals an open tab instead of duplicating it.
|
|
354
|
+
- `listWorkspaceRecords` is a new host route, cached by the client: an in-distribution path
|
|
355
|
+
needs the distribution, which a UNC workspace carries but a `/mnt/<drive>` workspace only
|
|
356
|
+
has in `wsl-workspaces.json`. `host-api.mjs` covers it and its probe floor moved 12 → 13.
|
|
357
|
+
- The address grammar is **mirrored** rather than imported — six of the eleven declared
|
|
358
|
+
releases ship no `@deepseek-ai/dsh-util-workspace-path`, or ship it without the grammar —
|
|
359
|
+
and a unit case cross-checks the mirror against the owning package byte for byte wherever
|
|
360
|
+
it is resolvable. Anything the hook cannot rebuild byte-for-byte, or whose distribution is
|
|
361
|
+
unknown, or a session it does not treat as WSL-bound, is passed through untouched rather
|
|
362
|
+
than guessed at.
|
|
363
|
+
- Only 0.1.5-rc.1 and later ship a right Sidebar, a document preview and the resource model;
|
|
364
|
+
the six earlier declared releases have no reference surface to fix, and the hook simply
|
|
365
|
+
does not install there.
|
|
366
|
+
- Measured for this entry: the eleven declared releases at 14/16 on the harness sweep (the
|
|
367
|
+
same two known baselines as before), `host-api` 13/13 against a live 0.2.0-rc.2 frontend,
|
|
368
|
+
and the reference click driven end-to-end in a real browser on 0.1.5-rc.1, 0.1.5-rc.2,
|
|
369
|
+
0.1.7-rc.1, 0.1.7-rc.2 and 0.2.0-rc.2, plus four configurations on the real DSH Desktop
|
|
370
|
+
(drive-spelling and UNC workspaces × `/mnt/<drive>` and in-distribution paths).
|
|
371
|
+
- **The agent can now type into a terminal of its own** (`wsl_terminal`, issue #51's last gap). A
|
|
372
|
+
pipe cannot be typed into, so every program that asks a question — `sudo` wanting a password,
|
|
373
|
+
`ssh` a first-connection fingerprint, a REPL, an editor, a TUI — was diagnosed, stopped and
|
|
374
|
+
re-run on a one-shot pseudo-terminal only so the model could read the complaint, and a person had
|
|
375
|
+
to take over; the ticket's contract is that *everything a person can run is reachable by the
|
|
376
|
+
agent*, so the unanswered class gets a door. The tool is the model-facing shape over the host's
|
|
377
|
+
own PTY registry and `dsh-terminal-bash` backend pointed at this plugin's relay — no new terminal
|
|
378
|
+
machinery, the same stack behind the right sidebar's terminal tab — with `open`/`send`/`read`/
|
|
379
|
+
`signal`/`close`/`list`, mounted beside the pipe `bash` (which stays the default: it carries every
|
|
380
|
+
byte, a terminal screen is a 160-column rendering with bounded scrollback). Two facts it took
|
|
381
|
+
reading host code to get right: the shell runs as the **workspace's configured user** because the
|
|
382
|
+
host builds a PTY child's environment itself and drops every `DSH_*` variable, so the relay reads
|
|
383
|
+
the workspace store back (the same fix the session `bash` needed — it had been resolving the user
|
|
384
|
+
from the environment alone); and the backend's quiet window is lowered to 1200 ms from its 3000 ms
|
|
385
|
+
default (source note in `src/host/variants.ts`) so a send the host does not recognise as a prompt
|
|
386
|
+
returns in ~1.2–1.8 s instead of ~3.0–3.6 s — the send says which of the two happened rather than
|
|
387
|
+
presenting a quiet screen as a prompt. Verified on a real distribution, both build planes, as
|
|
388
|
+
`root` and as `ruler`: `bash-session-real` now carries the door's own cells (66/66), including a
|
|
389
|
+
keystroke typed into a program blocked on `/dev/tty` reaching it (`GOT=…`), `submit: false` typing
|
|
390
|
+
without running and the next Enter running it, `SIGINT` ending a `sleep 30` with the shell
|
|
391
|
+
surviving, and `close` leaving no `bash -i` behind — `open` measured 477–490 ms. Found on the way:
|
|
392
|
+
`tests/host-profile-isolation.mjs` was red on Windows only (it counted a boot *diagnostic* as a
|
|
393
|
+
failure, so its two healthy-frame cells passed in CI and failed on the platform this plugin exists
|
|
394
|
+
for); those cells now separate diagnostics from failures and say why.
|
|
395
|
+
- **A command can be fed its input now** (`stdin`). The session shell's own stdin *is* the protocol
|
|
396
|
+
channel, so a command's stdin was `/dev/null` by construction — right for most calls, and fatal for
|
|
397
|
+
the class that reads a program's input from a pipe (`cat`, a build tool asking its question on stdin,
|
|
398
|
+
a script that reads data). The call's `stdin` text now travels inside the same frame and is decoded
|
|
399
|
+
into a temporary file the command's stdin is redirected from, removed after the exit status has been
|
|
400
|
+
read; the escalated (`tty: true`) path gets it too, because `script` forwards its stdin to the pty it
|
|
401
|
+
creates, and a background job receives it through the one-shot executor's own `stdin` spec field.
|
|
402
|
+
Over 32 KiB the call is refused by name and nothing runs — the frame's cost is measured (64 kB
|
|
403
|
+
answers in ~3.8 s, 256 kB in ~59 s), and a program fed half its input fails in ways that look like the
|
|
404
|
+
program's fault. Live cells: `cat` printing both fed lines **and the call still settling afterwards**
|
|
405
|
+
(the record that ends a call must not be eaten by the command's own read — that is the failure this
|
|
406
|
+
channel is shaped around), the same on a real pseudo-terminal (`read x; echo GOT=$x`), and the
|
|
407
|
+
refusal's own sentence.
|
|
408
|
+
|
|
409
|
+
- **An outside adversarial run found three things this machine could not see, and each is now closed.**
|
|
410
|
+
(1) *The readiness contract depended on the distribution's startup files leaving it alone.* On a
|
|
411
|
+
distribution shipping `/etc/profile.d/80-systemd-osc-context.sh`, that file runs
|
|
412
|
+
`PROMPT_COMMAND+=(…)`, bash refuses to export an array, and the interactive shell the relay `exec`s
|
|
413
|
+
inherits no `PROMPT_COMMAND` at all — measured here in a simulated profile: `env | grep -c
|
|
414
|
+
'^PROMPT_COMMAND='` goes from `1` to `0` and the `133;D;` marker never reaches the wire, so
|
|
415
|
+
`wsl_terminal` pays its quiet window on every keystroke and the `DSH_WSL_PTY_SHELL=1` tier never
|
|
416
|
+
settles. The relay now bridges the contract a second time under a name no startup file knows
|
|
417
|
+
(`__DSH_READINESS_PROMPT_COMMAND`) and re-asserts it between the login pass and the `exec`, `unset`
|
|
418
|
+
first — assigning to an array only writes element `[0]` and still exports nothing. Four arms measured:
|
|
419
|
+
old command + hostile profile `0`/no marker (the report's shape), old + benign `1`/marker (why this
|
|
420
|
+
machine was green), **new + hostile `1`/marker `prompt=[dsh> ]`**, new + no contract bridged
|
|
421
|
+
`0`/no marker (a host that injects none still gets its own shell). (2) *The function snapshot's cap
|
|
422
|
+
was all-or-nothing*, so on a distribution whose own startup functions are 86,954 bytes the whole
|
|
423
|
+
section — the function the model had just defined included — was dropped, and the journal's own
|
|
424
|
+
`alias, function, shopt and set options survive a restart` cell went red for the same reason. The cap
|
|
425
|
+
is now a per-function budget over only the functions the distribution's own files did not define
|
|
426
|
+
(a rebuilt shell re-sources those), and what does not fit is named. (3) *A rebuild that had something to
|
|
427
|
+
report was not believed by the cell that checks it*: the note renders as `was restarted and its
|
|
428
|
+
directory …` when nothing was left out and `was restarted; not restored: …` when something was, and
|
|
429
|
+
the check matched only the first, so on a reporting machine the claim and the fact were both true and
|
|
430
|
+
the cell still read them as disagreeing. Both wordings count now, and a starve-triggered rebuild
|
|
431
|
+
carries what it could not restore into the note of the call that asked for the terminal, instead of
|
|
432
|
+
losing it with the first attempt.
|
|
433
|
+
|
|
434
|
+
## 0.7.5 — 2026-09-30
|
|
435
|
+
|
|
436
|
+
- **The persistent shell works on DSH Desktop again (issue #40).** The Desktop
|
|
437
|
+
host process *is* the packaged Electron executable running in node mode
|
|
438
|
+
(`ELECTRON_RUN_AS_NODE=1`; the Desktop's own code is
|
|
439
|
+
`new DesktopHostProcess(resources.node, …)` with
|
|
440
|
+
`resources.node = process.execPath`), and the plugin pointed the PTY backend's
|
|
441
|
+
`shellPath` at `process.execPath` with `shellArgs[0] = lib/wsl-relay.js`. An
|
|
442
|
+
Electron binary writes **nothing at all** under a ConPTY: the relay's
|
|
443
|
+
`wsl.exe` child inherits a dead stream, the relay exits 0 with zero bytes, the
|
|
444
|
+
backend's readiness probe never sees a prompt, and **every** `bash` call fails
|
|
445
|
+
with `PTY shell exited during startup`. The `0.1.0-rc.7` one-shot fallback is
|
|
446
|
+
unaffected, which is exactly what hid the failure behind a mode that works.
|
|
447
|
+
Measured with the host's own node-pty: same relay, same `\\wsl.localhost\…`
|
|
448
|
+
cwd, same environment — a real node gives a bash prompt, the Electron binary
|
|
449
|
+
gives 0 bytes and a clean exit.
|
|
450
|
+
- **The relay's interpreter is now resolved deliberately**
|
|
451
|
+
(`src/shared/relay-node.ts`): first the runtime payload the Desktop passes to
|
|
452
|
+
its host process (the `…/resources/runtime/primary-runtime` path in
|
|
453
|
+
`process.argv`, which carries a real node), then the same payload beside the
|
|
454
|
+
running executable (inside `Contents/Resources/…` on macOS), then
|
|
455
|
+
`DSH_DESKTOP_NODE_EXECUTABLE`, then a `node` on `PATH`, and only then
|
|
456
|
+
`process.execPath`. Every candidate is **asked what it is** (`-e` printing
|
|
457
|
+
`process.versions.electron`), because `--version` cannot tell them apart: with
|
|
458
|
+
`ELECTRON_RUN_AS_NODE` inherited the Electron executable answers with the
|
|
459
|
+
*node* version (measured: `v24.18.1`), so any `^v\d+\.\d+\.\d+` test selects
|
|
460
|
+
the broken one. A host that is not Electron (`dsh web`) keeps
|
|
461
|
+
`process.execPath` and spawns no probe at all, so its behaviour is identical
|
|
462
|
+
to 0.7.4; on the Desktop the chosen interpreter and every rejected candidate
|
|
463
|
+
go to the boot log.
|
|
464
|
+
- PR #39 diagnosed this correctly and pointed the same way, but its first
|
|
465
|
+
candidate, `DSH_DESKTOP_NODE_EXECUTABLE`, **is the Electron executable** — the
|
|
466
|
+
Desktop's own `resources/runtime/bin/node.cmd` is `set
|
|
467
|
+
ELECTRON_RUN_AS_NODE=1` followed by that variable — and its `--version` check
|
|
468
|
+
cannot tell the two apart. This fix takes the same direction with the payload
|
|
469
|
+
path first and a discriminator that actually rejects Electron.
|
|
470
|
+
- `tests/relay-node.test.mjs`: candidate derivation (argv / beside the
|
|
471
|
+
executable / the environment variable, including de-duplication and order) and
|
|
472
|
+
the Electron discriminator, including the version-shaped output that must be
|
|
473
|
+
refused.
|
|
474
|
+
- `scripts/compatibility/conpty-relay.mjs`: **a new standing gate** — it drives
|
|
475
|
+
the relay through a real ConPTY with the host's own node-pty and requires the
|
|
476
|
+
resolved interpreter to produce a live bash prompt. That is the invariant
|
|
477
|
+
issue #40 broke, and nothing checked it before. Registered in
|
|
478
|
+
`Run-Checks.ps1` and the harness, so it runs on every declared release.
|
|
479
|
+
- **The second error in issue #40 is fixed too**: `bash_background` handed the jobs
|
|
480
|
+
registry an `owner` of the wrong shape, which failed with
|
|
481
|
+
`session "[object Object]" has no live agent` on `0.1.7` and later. That contract
|
|
482
|
+
**changed at `0.1.7-rc.1`**: `start()` took the agent object before (the registry
|
|
483
|
+
reads `owner.id` and `owner.ctx`) and takes the session id after (resolving it with
|
|
484
|
+
`agents.get(id)`) — the host's own producers moved the same way, from
|
|
485
|
+
`owner: parent` to `owner: parent.id`. The plugin now picks the shape from whether
|
|
486
|
+
the registry offers `resolveOwner`; `tests/wsl-jobs.test.ts`, which had pinned the
|
|
487
|
+
wrong half as correct behaviour, now pins both contracts, and all eleven declared
|
|
488
|
+
releases were exercised one by one.
|
|
489
|
+
- `dsh.compatibility.dshReleases` declares `0.2.0-rc.2` (the DSH release DSH
|
|
490
|
+
Desktop 0.2.0-rc.2 ships), so this build claims eleven releases instead of ten.
|
|
491
|
+
- **The second error in issue #40 is fixed too**: `bash_background` handed the
|
|
492
|
+
jobs registry the agent object as the job owner, but the registry resolves that
|
|
493
|
+
owner with `ctx.agents.get(owner)` and wants the **session id**, so every call
|
|
494
|
+
failed with `session "[object Object]" has no live agent`. The host's own
|
|
495
|
+
producers pass `agent.id`. `tests/wsl-jobs.test.ts` had asserted the broken
|
|
496
|
+
contract as if it were correct; it now asserts the session id.
|
|
497
|
+
- **Verification**: sixteen harness checks on each of the eleven declared
|
|
498
|
+
releases (14/16 everywhere — the two failures are the documented baseline:
|
|
499
|
+
`typecheck`, and `host-api`, which needs a *running* frontend and passes 12/12
|
|
500
|
+
when pointed at one), plus the runbook's six-item frontend pass on all eleven
|
|
501
|
+
(dialog distro list, create & open, write, read, one-shot/persistent bash,
|
|
502
|
+
skills, reopen from the UI) with `web.err` at 0 bytes and every file re-read
|
|
503
|
+
independently on the Linux side.
|
|
504
|
+
- **And a full pass inside the real DSH Desktop.** The official installer was
|
|
505
|
+
unpacked and the Electron application itself was started (isolated `DSH_HOME`,
|
|
506
|
+
window driven over CDP, plugin installed and enabled through the Desktop's own
|
|
507
|
+
plugin panel). With **0.7.4** installed, every `bash` call returned
|
|
508
|
+
`PTY shell exited during startup` and `bash_background` returned
|
|
509
|
+
`session "[object Object]" has no live agent`, while the dialog, the
|
|
510
|
+
distribution list and the skills all worked. With **0.7.5**, bash returned
|
|
511
|
+
`6.18.33.2-microsoft-standard-WSL2` / `/home/mille/fx-3381` / `mille`, a second
|
|
512
|
+
independent `pwd` still said `/tmp`, `bash_background` returned `bash-1` with
|
|
513
|
+
`job_list` reporting `bash-1 [bash] running` and `bg.txt` really containing
|
|
514
|
+
`BG_3381_OK`, and the workspace still opened after a window reload. The boot log
|
|
515
|
+
names the interpreter it chose. The whole run is written up in
|
|
516
|
+
`docs/compatibility-evidence.md`.
|
|
517
|
+
|
|
518
|
+
## 0.7.4 — 2026-09-28
|
|
519
|
+
|
|
520
|
+
- **The dialog works in DSH Desktop again (issues #35, #36).** The Desktop host
|
|
521
|
+
wraps `child_process` before the plugin is loaded: it installs plain
|
|
522
|
+
`exec`/`execFile` wrappers and re-exports them with
|
|
523
|
+
`syncBuiltinESMExports()`. That copy of `execFile` carries no
|
|
524
|
+
`util.promisify.custom`, so `promisify(execFile)` fell back to the generic
|
|
525
|
+
implementation, which resolves with the **first** callback argument only —
|
|
526
|
+
the stdout string — and `result.stdout` was `undefined` on every call. The
|
|
527
|
+
distro lookup read `.stdout` and threw
|
|
528
|
+
`Cannot read properties of undefined (reading 'includes')`, the frontend
|
|
529
|
+
caught it and rendered an empty picker: the dialog opened, the distribution
|
|
530
|
+
list stayed empty, and "Create & open" could not be completed, while
|
|
531
|
+
`wsl.exe -l -q` in a terminal listed the distribution normally. All three
|
|
532
|
+
call sites now use the **callback** form of `execFile`, which no wrapper can
|
|
533
|
+
reshape.
|
|
534
|
+
- **The `wsl.exe` lookup is a candidate list.** `wsl.exe` is tried on `PATH`
|
|
535
|
+
first, then the absolute `%SystemRoot%\System32\wsl.exe`, and a lookup that
|
|
536
|
+
fails now names every candidate it tried and the error each one produced,
|
|
537
|
+
instead of a type error from inside the decoder.
|
|
538
|
+
- `tests/exec-shape.mjs` reproduces the Desktop wrapper (plain wrappers plus
|
|
539
|
+
`syncBuiltinESMExports()`), spawns a probe with and without `--import`,
|
|
540
|
+
asserts the wrapped shape is broken and the new helpers are right, and runs
|
|
541
|
+
the real `listDistros`/`defaultDistro`/`resolveLinuxSymlink` through both
|
|
542
|
+
shapes. It is registered as the `exec-shape` check in
|
|
543
|
+
`scripts/compatibility/Run-Checks.ps1` so a future refactor cannot
|
|
544
|
+
reintroduce the promisified call.
|
|
545
|
+
- `dsh.compatibility.dshReleases` declares `0.1.7-rc.2` as well, so this build
|
|
546
|
+
claims ten releases instead of nine.
|
|
547
|
+
- **Verification**: fifteen harness checks on each of the ten declared releases
|
|
548
|
+
(13/15 everywhere — the two failures are the documented baseline: `typecheck`
|
|
549
|
+
and `host-api`, which needs a *running* frontend and passes 12/12 when pointed
|
|
550
|
+
at one), plus the runbook's six-item frontend pass on all ten (dialog distro
|
|
551
|
+
list, create & open, write, read, one-shot bash, persistent bash, skills) with
|
|
552
|
+
`web.err` at 0 bytes and the file re-read independently on the Linux side.
|
|
553
|
+
The Desktop-wrapper reproduction is in `docs/compatibility-evidence.md`,
|
|
554
|
+
together with the pack-identity hash and the plain-npm install gate.
|
|
555
|
+
|
|
556
|
+
## 0.7.3 — 2026-09-23
|
|
557
|
+
|
|
558
|
+
- **The plugin loads on DSH `0.1.7-rc.1` again, and its modes come back.** The
|
|
559
|
+
`0.1.7` line renamed the host preset face — `read()` became `readDocument()`, which
|
|
560
|
+
returns a document (`{agentPreset, content, name, description}`) instead of the
|
|
561
|
+
composition text, and `AgentPreset` lost its `path` — so the variant generator
|
|
562
|
+
threw `agentPresets.read is not a function` on every boot and no `wsl-*` mode
|
|
563
|
+
ever reached the picker. The roster face is now probed by capability and both
|
|
564
|
+
generations are served: `readDocument()` where it exists, `read()` behind it.
|
|
565
|
+
- **A variant is a declaration row on that line.** `0.1.7` stopped scanning
|
|
566
|
+
`$DSH_HOME/.agent-presets/` — a preset there is a declarative
|
|
567
|
+
`@deepseek-ai/dsh-agent-preset` row, and the directory a variant used to be
|
|
568
|
+
written to is read by nothing at all. The generator now expands the composed
|
|
569
|
+
variant back into an entry list and publishes it through
|
|
570
|
+
`ctx.agentPresets.register()`; the plugin's effect owns the returned disposers, so
|
|
571
|
+
an unload or hot reload retires the variants instead of leaving orphans the next
|
|
572
|
+
apply could not replace (`Duplicate agent preset: wsl-<mode>`). Earlier releases
|
|
573
|
+
keep the directory channel, unchanged.
|
|
574
|
+
- **The world's own providers are named as `file:` URLs on that channel.** A preset
|
|
575
|
+
mounted from a declaration is imported by the registry's entry tree, which —
|
|
576
|
+
unlike the boot-time Include — does not turn an absolute path into a `file:` URL.
|
|
577
|
+
Without the rewrite the provider rows never started, the audit reported each as
|
|
578
|
+
`never started`, and the whole variant was refused as unusable.
|
|
579
|
+
- **The PTC variant is named again on `0.1.7`.** The label table that gives the
|
|
580
|
+
shipped modes their bilingual `WSL · …` names carried the mode's older id
|
|
581
|
+
(`code`) but not `ptc`, the id it has used since `0.1.1`. That stayed invisible
|
|
582
|
+
as long as the release published its own display name, because the lookup fell
|
|
583
|
+
through to it — but `0.1.7` publishes none, so the mode reached the picker as
|
|
584
|
+
`WSL · ptc` with the generic `WSL execution world for ptc: …` description. Both
|
|
585
|
+
ids are covered now, and the variant reads `WSL · PTC mode(PTC 模式)` on every
|
|
586
|
+
channel, like the other three shipped modes.
|
|
587
|
+
- `dsh.compatibility.dshReleases` declares `0.1.7-rc.1`. The two modules the
|
|
588
|
+
declaration channel needs (`@deepseek-ai/cordis-plugin-include`, `js-yaml`) are
|
|
589
|
+
resolved at call time and declared as **optional** peers, so a release that lacks
|
|
590
|
+
them fails one variant rather than refusing to load the plugin.
|
|
591
|
+
|
|
592
|
+
## 0.7.2 — 2026-09-21
|
|
593
|
+
|
|
594
|
+
- **The WSL skill catalog is no longer re-walked on the request path (issue #25).**
|
|
595
|
+
The host rebuilds the catalog during a request and awaits each provider's
|
|
596
|
+
`list()`, and this provider kept its own answer for only 10 s — so every time the
|
|
597
|
+
catalog was re-collected (a new session or scope, or simply a lookup more than
|
|
598
|
+
10 s after the last one) that request paid a full walk of the workspace, one
|
|
599
|
+
directory at a time: two `stat`s and one `readdir` each. A `readdir` over the
|
|
600
|
+
`\\wsl.localhost\…` 9P share measures 3-16 ms here and the walk's budget is 4096
|
|
601
|
+
directories, which is why a large workspace cost 20.4 s, on the request path. A
|
|
602
|
+
published catalog is now served as-is, and only the provider's own change detector
|
|
603
|
+
can drop it: a repeat lookup costs 1-3 ms and no filesystem traffic. The freshness
|
|
604
|
+
contract is unchanged — a new nested skills directory still appears within 30 s,
|
|
605
|
+
and an added, removed or edited skill within 3 s.
|
|
606
|
+
- The walk itself is cheaper: one BFS layer is probed concurrently (bounded) and
|
|
607
|
+
published in frontier order, so the catalog stays deterministic, and a
|
|
608
|
+
directory's `.dsh/skills` / `.agents/skills` are probed only when its own listing
|
|
609
|
+
showed that marker. The budget-sized walk went from 20.4 s to 4.8 s on the same
|
|
610
|
+
machine; node's filesystem thread pool caps the real parallelism.
|
|
611
|
+
|
|
612
|
+
## 0.7.1 — 2026-09-20
|
|
613
|
+
|
|
614
|
+
- **`npm install dsh-wsl-workspace` no longer fails.** Verifying the published
|
|
615
|
+
artifact turned up a regression this release introduced: npm auto-installs
|
|
616
|
+
missing peer dependencies, and the `@deepseek-ai/dsh-tool-fs-search` peer added in
|
|
617
|
+
0.6.0 itself peers on `@deepseek-ai/dsh-retention`, which is **not published** — so
|
|
618
|
+
a plain `npm install` died with `E404 … @deepseek-ai/dsh-retention` (0.4.3 installs
|
|
619
|
+
fine, so it was ours). `dsh plugin add` uses pnpm, which only *warns* about unmet
|
|
620
|
+
peers, which is why every harness run and real install passed. All ten host peers
|
|
621
|
+
are now marked optional in `peerDependenciesMeta`: the package still declares what
|
|
622
|
+
the host must provide, but npm no longer tries to fetch it.
|
|
623
|
+
|
|
624
|
+
## 0.7.0 — 2026-09-20
|
|
625
|
+
|
|
626
|
+
The WSL world now matches the host everywhere a session can tell the difference,
|
|
627
|
+
and the last two known issues are closed. Everything below ships together: a WSL
|
|
628
|
+
variant gets Linux symlinks, the session's access mode, in-distribution search, a
|
|
629
|
+
live skill catalog, a stateful shell, and tracked background jobs.
|
|
630
|
+
|
|
631
|
+
- **The host's `bash` contract is stated in the tool description.** The persistent
|
|
632
|
+
tool wraps each command as `eval -- $'…'`, so a command ending in `&` backgrounds
|
|
633
|
+
the *whole* wrapped command — the call returns immediately with exit code 0 and
|
|
634
|
+
no output while the real output arrives later, possibly inside the next call's.
|
|
635
|
+
And the shell is one process for the whole Agent, so a `cd` carries into the next
|
|
636
|
+
call. The host default says neither, and DSH's own Minimal preset recommends the
|
|
637
|
+
hazardous form (`sleep 10 &`). The world now overrides `description` (a supported
|
|
638
|
+
key on every declared release) with both facts and the safe forms.
|
|
639
|
+
- **`0.1.0-rc.7` falls back to a working one-shot shell.** That release's
|
|
640
|
+
`dsh-subprocess-local` has no Windows process inspector, so the host's PTY-backed
|
|
641
|
+
persistent shell cannot start on Windows at all — every `bash` call failed with
|
|
642
|
+
`subprocess-local: terminal inspection is unsupported on platform win32` (the
|
|
643
|
+
host ships the same gap: its Minimal preset mounts `persistent-bash` there with
|
|
644
|
+
no Windows guard). The plugin now *probes* the substrate instead of assuming —
|
|
645
|
+
it hands `spawnTerminal` a program that cannot exist, which reaches the inspector
|
|
646
|
+
check and nothing else — and when the answer is no, the world keeps the one-shot
|
|
647
|
+
`dsh-tool-bash` row: a working, stateless shell rather than an error per call.
|
|
648
|
+
- **Tracked background jobs, restored.** Replacing the one-shot bash tool with the
|
|
649
|
+
persistent one also removed the only thing that started a registry job, so
|
|
650
|
+
`job_list` always answered "no background jobs" and a `run_in_background: true`
|
|
651
|
+
argument handed to `bash` was silently ignored (the parameter schema allows extra
|
|
652
|
+
properties, so nothing complained). The world now mounts `bash_background`
|
|
653
|
+
(`src/host/wsl-jobs.ts`), a thin producer over the host's own `ctx.jobs.start`
|
|
654
|
+
plus this plugin's `ctx.shell.start`: it returns a job id, and
|
|
655
|
+
`job_list`/`job_output`/`job_kill` work on it as usual. It is mounted only where
|
|
656
|
+
the source mode also mounts the `job_*` tools, and only alongside the persistent
|
|
657
|
+
shell.
|
|
658
|
+
- **Six defects found by hunting the new code with worst-case input**: a hidden-file
|
|
659
|
+
guard that also applied to an explicitly named file (`grep path=.env` returned
|
|
660
|
+
nothing), a discarded `find` exit status (`glob path=/nope-missing` looked like an
|
|
661
|
+
empty directory), a line-terminated glob header (a root whose name contains a
|
|
662
|
+
newline came back truncated), untranslated Windows paths (`grep path='D:\proj'`
|
|
663
|
+
failed where `read` worked), a closed spill schema (which would have failed the
|
|
664
|
+
tool's own output validation on every capped search), and a catalog detector that
|
|
665
|
+
could stack polls on a slow share. Plus two in the new producer: it was mounted in
|
|
666
|
+
a mode with no `job_*` tools to read its ids, and it defaulted a job's working
|
|
667
|
+
directory to the host process's rather than the session workspace.
|
|
668
|
+
- **Verification**: thirteen checks on each of the eight declared releases
|
|
669
|
+
(`0.1.0-rc.7` … `0.1.5-rc.2`) — `search-real` drives the real tools against a real
|
|
670
|
+
distribution fixture — leaving only the two pre-existing baseline failures
|
|
671
|
+
(`typecheck`, and `host-api` which needs a live server). 152 unit tests, including
|
|
672
|
+
a parity check of every renderer against the host suite's own formatters. Real
|
|
673
|
+
browser sessions on five releases for the tool behaviour, and a **frontend pass on
|
|
674
|
+
all eight** (entry button, dialog, path check, create & open, mode picker, help
|
|
675
|
+
panel with v0.7.0 and 8 release chips), with the session log as evidence for the
|
|
676
|
+
tool set, the search results, the catalog replacement, the shell fallback and the
|
|
677
|
+
background-job lifecycle.
|
|
678
|
+
|
|
679
|
+
## 0.6.0 — 2026-09-19
|
|
680
|
+
|
|
681
|
+
- **WSL sessions get `grep` and `glob`**: the host suite spawns the packaged Windows ripgrep and every path the model hands it is a Linux one, so the generated world dropped `tool-fs-search` and left the model to grep through the shell — the last bullet of the panel's known issues. The world now mounts an in-distribution twin that keeps the host suite's contract: the same tool names, parameter schemas, inline caps (250 matches / 100 paths), output schema, `Line N:` grouping, found-count header, capped-result footer, search card and formatted-result spill — the rendering comes from `@deepseek-ai/dsh-tool-fs-search`'s own exported formatters, and the two projections that package keeps private (the card metadata and the glob page) are reproduced and compared against it in unit tests. `grep` runs GNU grep inside the distribution (`-rnIEH -Z`, POSIX ERE, hidden entries and `node_modules` skipped like ripgrep's defaults, no `.gitignore` support), `glob` uses GNU `find` with in-process gitignore-style matching and ripgrep's oldest-first modification order. Model-controlled values travel as separate argv elements after a fixed script, so nothing the model types is ever parsed by a shell.
|
|
682
|
+
- **The skill catalog notices an edited skill, not just an added one**: the catalog message is rebuilt only when the registry's revision moves, and the old detector compared directory listings — so rewriting an existing `SKILL.md` (a description, say) changed nothing it could see and the model kept the old text until a new session. The cheap pass now also stamps every skill file with its modification time and size, and runs every 3 seconds instead of 10. The full re-discovery walk — the only pass that can find a skills directory that did not exist before — moved to its own 30-second cadence, so the change detection is both faster and cheaper than the single 10-second poll it replaces.
|
|
683
|
+
- **`lib/` is rebuilt deterministically**: `tsdown` writes into a committed `lib/`, and stale code-split chunks from an earlier build survived every rebuild (`clean: false` plus two configurations sharing one output directory). Local build tooling now clears the directory first, and the three new runtime peers (`@deepseek-ai/dsh-tool-fs-search`, `@deepseek-ai/dsh-tools`, `@deepseek-ai/schemastery`) are declared, which is also what keeps them external instead of bundling a second copy of DSH's tool stack into this plugin.
|
|
684
|
+
- **Four defects found by hunting the new code with worst-case input**: a hidden-file guard that also applied to an explicitly named file (`grep path=.env` returned nothing), a discarded `find` exit status (`glob path=/nope-missing` looked like an empty directory), a line-terminated glob header (a root whose name contains a newline came back truncated) and untranslated Windows paths (`grep path='D:\proj'` failed where `read` worked). The spill schema was also closed, which would have failed the tool's own output validation on every capped search, and the catalog detector gained an in-flight guard so a slow poll cannot stack `wsl.exe` calls.
|
|
685
|
+
- **The host's `bash` wrapper is documented in the tool description**: the persistent tool wraps each command as `eval -- $'…'`, so a trailing `&` backgrounds the whole wrapped command and the call reports exit code 0 with no output; and the shell is one process, so a `cd` carries into the next call. The host default says neither, and DSH's own Minimal preset recommends `sleep 10 &` — the world now overrides the description with both facts and the safe forms.
|
|
686
|
+
- **`0.1.0-rc.7` no longer gets a broken shell**: that release's `dsh-subprocess-local` has no Windows process inspector, so the host's PTY-backed persistent shell cannot start on Windows at all (the host ships the same gap: its Minimal preset mounts `persistent-bash` there with no Windows guard). The world probes the substrate at startup and, when the answer is no, keeps the one-shot `bash` row — a working stateless shell — instead of failing every call. Verified by a real session on that release.
|
|
687
|
+
- **Tracked background jobs are back in WSL sessions**: replacing the one-shot bash tool with the persistent one also removed the only thing that started a registry job, so `job_list` always said "no background jobs" and `run_in_background: true` handed to `bash` was silently ignored (the parameter schema allows extra properties, so nothing complained) — the exact defect an operator's session surfaced. The world now mounts `bash_background` (`src/host/wsl-jobs.ts`), a thin producer over the host's own `ctx.jobs.start` + this plugin's `ctx.shell.start`: the tool returns a job id, and `job_list`/`job_output`/`job_kill` work on it as usual. Verified in a real session: `started background job bash-1` → `job_list` shows `running` → incremental `job_output` reads (`tick 1`, `tick 2`, then `tick 3`) → `[status: completed, exit code: 0]` plus the runtime's completion notice.
|
|
688
|
+
- **Verification**: thirteen checks on each of the eight declared releases (`0.1.0-rc.7` … `0.1.5-rc.2`) — `search-real` joins the suite, driving the real tools against a real distribution fixture (framing, includes and braces, caps and footers, spill, cards, error codes, argv-safety, explicit dot-files, unreadable roots, odd root names, `/mnt` paths, abort and overflow, glob ordering and pruning) — leaving only the two pre-existing baseline failures (`typecheck`, and `host-api` which needs a live server). Unit tests add `tests/wsl-search.test.ts` (33 cases) and the refresh, cadence, stacking and description cases; `skills-real` proves an edited skill file invalidates the catalog through the share's own modification times. Real browser sessions on five releases confirm the behaviour end to end, including the `0.1.0-rc.7` fallback.
|
|
689
|
+
|
|
690
|
+
## 0.5.0 — 2026-09-19
|
|
691
|
+
|
|
692
|
+
- **The file tools now follow Linux symlinks**: the `\\wsl.localhost` share lists a link entry but cannot describe it — `lstat`, `stat` and `readFile` on the link all fail and `resolve()` hands back a lexical identity for it — so a link path behaved like a missing file and a linked-in project's files could not be read or written at all. `resolve`/`lstat` now ask the distribution (`wsl.exe … readlink -f`, the same resolver the skill scan uses) whenever this share cannot already describe the path, and continue at the real path. A link is never replaced by a regular file, and writing through a dangling link creates its target while keeping the link.
|
|
693
|
+
- **The access mode constrains a WSL session again**: a variant mounts its own `fs` provider in the preset's isolate realm, so the host's `fs-sandbox` wrapper was not in the call path and `workspace-write` did not stop a write outside the workspace (measured before the fix: a Linux path and a `D:\...` path both went through). `writeText`/`editText` now fence the mutation exactly as `@deepseek-ai/dsh-fs-sandbox` does: `ctx.sandboxPolicy` (the tool layer's per-call value, else the service), the same `writableRoots` allow-list plus the distribution's `/tmp`, the same `FS_SANDBOX_DENIED`, and the `sandboxMode` getter the tool reads to advertise escalation. Because the fence runs after link resolution it judges the real path, so a link out of the workspace is an outside write.
|
|
694
|
+
- **Live skill catalog**: with `watch: false` pinned on the UNC-hostile watcher, a skill added while a session ran only appeared in the next session. The provider now keeps a change detector per scan root it has served, re-checking the published directory shape every 10 s (roots plus entry names and kinds, never re-reading skill files) and calling `control.invalidate()` when it changed, which makes the catalog middleware re-collect on the session's next turn.
|
|
695
|
+
- **`bash` is now a stateful WSL shell** — the capability the per-mode matrix kept showing was missing (every `bash` call used to be a fresh process). DSH's PTY registry takes replaceable backends and `@deepseek-ai/dsh-terminal-bash` is a config-driven one, so the world mounts it (inside its own `persistent-shell` group, because the registry is an agent-owned service) with `backendType: wsl` and points it at this plugin's relay (`src/host/wsl-relay.ts` → `lib/wsl-relay.js`) run by the host's own node. The relay resolves the distribution (session UNC cwd → `DSH_WSL_DISTRO` → host default) and the optional `DSH_WSL_USER`, then hands its stdio — the PTY — to `wsl.exe -d … --cd … -e bash -lc 'cd … && exec bash -i'`: login environment, interactive, and the session directory preserved. `@deepseek-ai/dsh-tool-bash-persistent` registers the **`bash`** name, so it takes the place of the one-shot `dsh-tool-bash` row (mounting both fails the whole preset — the same collision DSH's Minimal mode sidesteps by being a persistent-shell-only agent). The world also isolates and provides its own no-op `sandbox` capability: the PTY backend confines through `ctx.sandbox` before spawning, and the host's Windows runner cannot read the security descriptor of a `\\wsl.localhost\…` workspace root (`GetNamedSecurityInfoW failed (Win32 1)`), so a WSL session declares `enforcement: 'partial'` and keeps the policy where it is meaningful — in the file tools.
|
|
696
|
+
- **Verification**: the eight declared releases (`0.1.0-rc.7` … `0.1.5-rc.2`) run twelve harness checks — `fs-real` (link resolution through the real backend, reads through links and chains, dangling-link creation, link preservation, the fence on an outside link target, the distro `/tmp` allowance) and `relay-real` (stateful shell against real WSL, distribution and user resolution, clean exit) are new — with the same two documented baseline failures (`typecheck`, and `host-api` needing a live server). Unit: `tests/fs-policy.test.ts` (7 fence cases) and the skill-provider refresh cases, on top of the existing suite.
|
|
697
|
+
|
|
698
|
+
## 0.4.5 — 2026-09-19
|
|
699
|
+
|
|
700
|
+
- **A project linked into a WSL workspace is discoverable now**: the `\\wsl.localhost` 9P share lists a Linux symlink but cannot resolve its target, so the skill scan — which already followed directory links on substrates that resolve them — skipped every linked-in project, and with it every nested project below it (the layout issue [#10](https://github.com/dsh-wsl-workspace-maintainers/dsh-wsl-workspace/issues/10) describes). When the share reports a link it cannot follow, the provider now asks the distribution itself (`wsl.exe -d <distro> -- readlink -f <linux path>`) and continues the walk at the real path. The fallback is bounded on purpose: at most 32 links per lookup, four calls in flight, a 10 s timeout each, and the existing depth / visited-directory / skill-directory budgets are untouched. Because the walk continues at the resolved path, a project reachable both directly and through a link is visited once, and a link that points back at the workspace root is absorbed by the visited set instead of looping.
|
|
701
|
+
- **What the fallback does not cover**: `read/write/edit` still resolve their paths through `WslFileSystem`, which does not follow Linux links, so reading or writing a link path reports it missing — use the real path. The help panel's known-issues list now states that instead of promising a fallback "not implemented yet".
|
|
702
|
+
- **Why one `wsl.exe` per link** (measured, and worth recording): `wsl.exe` silently drops the arguments that follow a command (`sh -c 'echo $#' sh a b c` answers 0), and its command-line parser truncates an argument containing a double quote, so a batched `sh` loop cannot be made reliable through it. A bare `readlink -f a b c` is no better: GNU `readlink` stops at the first path it cannot resolve and still exits non-zero, which would silently starve the rest of the batch. Passing each path as a process argument to one short call avoids quoting entirely — paths with spaces, quotes and backslashes all resolve — at the cost of one process per link (about 35 ms warm; six links cost 179 ms end to end on this machine, and a workspace with no links never starts a distribution process at all).
|
|
703
|
+
- **Verification**: the eight declared releases (`0.1.0-rc.7` … `0.1.5-rc.2`) pass the same 8/10 harness checks as 0.4.4 — only the documented `typecheck` baseline and the check that needs a live server fail. The real-9P check now builds a fixture whose only path in is a symlink and asserts the linked project, its nested project and its service through `get()`; the same walk with the fallback face removed finds neither, which is the pre-fix behaviour reproduced in the same run. On a live WSL fixture (`/home/mille/symprobe/ws`: a link out of the workspace, a link chain, a file link, a dangling link and a loop back to the root) the catalog went from 2 skills to 5, and `get()` read every body through the resolved locator.
|
|
704
|
+
|
|
705
|
+
## 0.4.4 — 2026-09-19
|
|
706
|
+
|
|
707
|
+
- **A preset built on top of a WSL variant could not be used at all**: this generator recognises its own output by id prefix (`wsl-`), so a user preset that started life as a copy of `wsl-standard` or `wsl-cordis` — a "data mode" that carries its own world, say — was treated as a plain source preset and had a *second* world group appended to it. DSH refuses a composition carrying two `wsl-world` rows, so choosing that mode failed outright with `无法切换到「WSL · <name>」:duplicate loader entry id: wsl-world`; on a release that mounts the group before validating row ids the same duplication surfaces one step later as `tool "str replace editor" is already registered in this scope` (the report in [#24](https://github.com/dsh-wsl-workspace-maintainers/dsh-wsl-workspace/pull/24)). The generator now replaces the world group it finds — identified by the mounted `shell-wsl`/`fs-wsl` provider ids, so a copy whose group was renamed is caught too — and every variant ends up with exactly one world pointing at this installation's providers. A top-level row id that appears twice in a source is reduced to its first occurrence as well, because DSH rejects the whole preset on a duplicate id rather than the offending row.
|
|
708
|
+
- **`tool-str-replace-editor` rows are replaced like the older `str-replace-editor` row** ([#24](https://github.com/dsh-wsl-workspace-maintainers/dsh-wsl-workspace/pull/24)): newer rosters name the editor row that way, and it registers the same `str_replace_editor` tool as the world group's own editor row, so the source row is dropped just like its predecessor and the WSL-aware editor the variant injects stays.
|
|
709
|
+
- **Variant display names are no longer double-quoted**: the variant's `preset.yml` copied the source's `name:` scalar verbatim, so a quoted `name: 'Data mode'` reached the mode picker as `WSL · ''Data mode''`. The scalar is unquoted before it is re-emitted.
|
|
710
|
+
- **Not adopted from [#24](https://github.com/dsh-wsl-workspace-maintainers/dsh-wsl-workspace/pull/24)**: disabling the `tool-cordis` row to avoid a duplicate inspect-provider registration. A `disabled` row never applies, so the WSL variant of Creator mode silently lost `cordis_inspect_list` / `cordis_inspect_query` (checked against 0.4.3, where both are present and answer with the host and the client providers); the PR's own description that the model "can still see the tools in the catalog" is not what happens. The registration their report shows needs that row applied twice, which the row-id reduction above now prevents where a copied preset caused it.
|
|
711
|
+
- **Help panel tidied up**: the panel now opens with a greeting line and the repository link, carries a "What's new" section for this build, and lists only the limitations that still apply — the historical "fixed in 0.4.3" note and the per-generation API walkthrough are gone. The compatibility chips are untouched: they are the manifest this build declares, not history.
|
|
712
|
+
- **Verification**: the eight declared releases (`0.1.0-rc.7` … `0.1.5-rc.2`) pass the same 8/10 harness checks as 0.4.3 — only the documented `typecheck` baseline and the check that needs a live server fail; 136 transforms over every shipped preset of the 17 installed runtimes are unchanged apart from the repair, and all 68 "copied variant" cases resolve to a single fresh world group.
|
|
713
|
+
- **Per-mode matrix with a real model** (every WSL variant, not just the default one): on `0.1.0-rc.7`, `0.1.1-rc.2`, `0.1.3-alpha.2` and `0.1.5-rc.2` each of the four variants — Standard, PTC, Minimal, Creator — was driven through the browser and asked to write a file with its file tool, run `uname -r; pwd; whoami` in bash and land that output in the workspace, then read the file back. Every mode produced `MODE-<mode>-OK` and a WSL2 kernel line in `/home/mille/<workspace>/notes/` with no loader error; the follow-up bash call lands in the workspace again, which is the documented per-call shell (the PTY group stays dropped). `0.1.2-rc.1` and `0.1.5-rc.1` were driven through all four modes without the file/bash assertions.
|
|
714
|
+
- Browser + real-model spot checks of the copied-variant mode (mounts and answers), Creator mode (inspect tools intact) and the `0.1.0-rc.7` standard flow complete the pass.
|
|
715
|
+
|
|
716
|
+
## 0.4.3 — 2026-09-11
|
|
717
|
+
|
|
718
|
+
- **The persona text moved in `0.1.3-alpha.2`** ([#22](https://github.com/dsh-wsl-workspace-maintainers/dsh-wsl-workspace/issues/22)): DSH renamed the persona's model-facing scalar from `text` to an inline `suffix` plus a folded `prefix`, and the variant generator only recognised `text: >-`. On that line the WSL environment sentence was never appended - the session still ran inside the distribution, but the model was never told that its working directory is a Linux path reachable from Windows as `/mnt/<drive>`. The generator now amends `suffix`, `text` or `prefix` (folding an inline scalar into a block scalar when needed, so the sentence joins the working-directory line exactly where the legacy `text` block put it), and a persona carrying `complete: true` is still left alone. Verified on seven releases: the five older ones keep their persona block byte-identical, and the two newer ones now carry the sentence into the model's system message.
|
|
719
|
+
- **Help panel**: the dialog gained a "?" button that opens an in-place panel - the DSH releases this build declares (read from `package.json` through the host route, so the list can never drift from the manifest), how the plugin is used, its features, and the limitations it cannot fix.
|
|
720
|
+
- **The skill catalog now reaches UNC workspaces**: the host skill provider watches a workspace through `chokidar`, and watching a `\\wsl.localhost\...` path fails; the failed watcher makes the skill snapshot report `complete: false`, and `dsh-tool-skill` withholds the *entire* catalog message while a snapshot is incomplete — so a WSL session's model saw no skills at all, not even the ones the plugin had discovered. The variant generator now pins `watch: false` on the `skill-filesystem` row (merged into an existing `config:` block when there is one, and left alone when the source declares `watch` itself), which makes the host collect the catalog once at session start instead. Verified end to end on `0.1.5-rc.2`: the model's context carries the `<available_skills>` list. Trade-off: a skill added mid-session appears in the next session rather than the running one; skill bodies are still read live.
|
|
721
|
+
- **`verify-lib` hardening**: its comment/string stripper could pair a lone apostrophe inside a comment with a later one and swallow the rest of the bundle, which made every `node:*` import look tree-shaken. The quote rules now stop at a newline, exactly as a JavaScript string does.
|
|
722
|
+
|
|
723
|
+
## 0.4.2 — 2026-09-10
|
|
724
|
+
|
|
725
|
+
- **Create & open in a `0.1.2-rc.1` workspace**: the session starter is now resolved when the dialog writes, not when the plugin applies. This plugin applies *before* the UI domain that publishes `uiWorkspace` registers its service, so the lookup cached at apply time stayed `undefined` for the whole page life: `Create & open` created the workspace and then silently opened no session, leaving `sessionIds` empty while the dialog reported success. A release exposing neither `uiWorkspace.startSession` nor `workspaces.startSession` now fails *before* anything is written, instead of leaving an orphaned workspace behind.
|
|
726
|
+
- **Skill body integrity**: skill bodies no longer lose their first character. `findFrontmatterEnd` already returns the index of the body's first character (the closing delimiter's newline plus one), so the slice must start there; the previous offset dropped that character and made the one after the delimiter look like the body. The existing fixtures always put a blank line after the delimiter, which is exactly what hid it.
|
|
727
|
+
- **UTF-8 BOM skills are no longer dropped**: a `SKILL.md` saved with a leading BOM (Notepad, VS Code's "UTF-8 with BOM", PowerShell redirection) did not match the opening `---` and disappeared from the catalog entirely. The parser strips the BOM before the fence check.
|
|
728
|
+
- **Binding converges on late inputs**: the agent-preset roster and the registered `/mnt/<drive>` workspace set are both inputs to binding, and both land asynchronously after the plugin's first pass. Each now re-runs the pass when it arrives instead of waiting for a session-store event that may never come.
|
|
729
|
+
- **Compatibility manifest corrected**: `0.1.3-alpha.1` is not published (`npm view @deepseek-ai/dsh@0.1.3-alpha.1` is a 404), so the declaration could never be verified; it is replaced by the published `0.1.3-alpha.2`.
|
|
730
|
+
- **Reproducible publishes**: a new `.gitattributes` (`* text=auto eol=lf`, `lib/** -text`) pins line endings. `core.autocrlf=true` used to rewrite text files to CRLF on checkout, and since `lib/` is committed and published verbatim the same commit produced different npm tarballs depending on the machine; the repository already stored LF, so no renormalisation was needed.
|
|
731
|
+
- **Closed-loop tests**: `tests/client-lifecycle.test.mjs` drives the browser half through the shipped `lib/client.js` for both service shapes — legacy (`connection.api.agentPresets` + `workspaces.startSession`) and current (`remote.agentPresets` + `uiWorkspace`) — and asserts `Create & open` for the normal, late-registration and no-starter cases. The skill tests now cover a body that starts on the delimiter's next line, for LF and CRLF files.
|
|
732
|
+
|
|
733
|
+
## 0.4.1 — 2026-09-03
|
|
734
|
+
|
|
735
|
+
- **DSH v0.1.2-rc.1 compatibility**: Added backward compatibility support for DSH v0.1.2-rc.1 and later versions through feature detection and compatibility wrappers. The plugin now automatically detects the DSH version at runtime and uses the appropriate API:
|
|
736
|
+
- `uiWorkspace.startSession()` for v0.1.2-rc.1+
|
|
737
|
+
- `workspaces.startSession()` for v0.1.1-rc.2 and earlier
|
|
738
|
+
- `summary.projectionValues?.agentPreset` for v0.1.2-rc.1+
|
|
739
|
+
- `summary.agentPreset` for v0.1.1-rc.2 and earlier
|
|
740
|
+
- Projection-based auto-sync for v0.1.2-rc.1+
|
|
741
|
+
- `sessions.noteAgentPreset()` for v0.1.1-rc.2 and earlier
|
|
742
|
+
- **Updated compatibility manifest**: Added v0.1.2-rc.1 to the `dsh.compatibility.dshReleases` declaration.
|
|
743
|
+
- **Fixed `without inject` crash on v0.1.2-rc.1+**: the agent-preset roster is read through the `remote.agentPresets` namespace service via `ctx.get('remote.agentPresets')` (topology-free store lookup) instead of the `remote` aggregate's `agentPresets` property, which Cordis' associate proxy rejects when the dotted property is not declared in `inject`. `inject` stays limited to the services both DSH generations share (`slots`, `locale`, `sessions`, `workspaces`).
|
|
744
|
+
- **Compatibility manifest**: declared v0.1.3-alpha.1 compatible (its plugin-facing API surface matches v0.1.2-rc.1). Final adaptation notes consolidated in `docs/archive/compatibility-summary-0.4.1.zh.md` (supersedes the root-level draft plans).
|
|
745
|
+
|
|
746
|
+
## 0.4.0 — 2026-08-29
|
|
747
|
+
|
|
748
|
+
Follow-ups from the [#12](https://github.com/dsh-wsl-workspace-maintainers/dsh-wsl-workspace/issues/12) limitation list and the [#13](https://github.com/dsh-wsl-workspace-maintainers/dsh-wsl-workspace/issues/13) compatibility work:
|
|
749
|
+
|
|
750
|
+
- **Lookup cache**: completed skill-catalog lookups are cached per scan root for 10 seconds, so repeated catalog builds no longer rescan the workspace over the slow 9P share; `get()` keeps reading skill bodies live, and freshly added skills appear within the TTL window.
|
|
751
|
+
- **Symlinked projects — investigated in 0.4.0, resolved in 0.4.5**: the discovery walk now recognizes directory symlinks explicitly and prunes them safely (no crashes, no loops), and the probe showed that following them is impossible over the `\\wsl.localhost` share itself (the Windows side cannot resolve Linux symlink targets: `readlink` → `EISDIR`, `stat`/`readdir` → `ENOENT`); 0.4.5 resolves them through the distribution instead, so linked-in projects are discoverable (see that changelog entry). A name+body fingerprint dedupe also guarantees aliased skill files can never publish twice on substrates that do resolve links.
|
|
752
|
+
- **Block-scalar frontmatter**: `description:` / `whenToUse:` written as YAML block scalars (`|` literal, `>` folded) now parse — such skills were silently dropped before.
|
|
753
|
+
- **Compatibility manifest**: `dsh.compatibility.dshReleases` declares per-release compatibility with the official DSH versions, backed by reproducible disposable-Profile install/start/uninstall evidence (`scripts/verify-dsh-compat.sh`), and `engines` declares the Node.js floor.
|
|
754
|
+
- **Guard scripts**: `scripts/check-rank-parity.mjs` fails the release when the copied project-rank constants drift from the host's `dsh-skill-filesystem`.
|
|
755
|
+
|
|
756
|
+
## 0.3.2 — 2026-08-29
|
|
757
|
+
|
|
758
|
+
- **WSL workspace sessions now inject nested-project skill catalogs** ([#10](https://github.com/dsh-wsl-workspace-maintainers/dsh-wsl-workspace/issues/10)): `.dsh/skills` and `.agents/skills` directories of projects nested below the registered workspace root are discovered and published with the host's project ranks and sources, so the model sees the same skill catalog it would see when the session cwd is the project folder itself. Discovery is depth- and budget-bounded, prunes `node_modules`/dot-directories, and leaves non-WSL sessions untouched.
|
|
759
|
+
- **Host-parity scan root**: lookups from inside a project subtree resolve the nearest `.git` ancestor first, so the enclosing project's skills stay visible from deeper cwds; skills above that ancestor do not leak.
|
|
760
|
+
- **Hardening**: the skill-root budget is enforced per push, and the `skills.registerProvider` call is guarded so a host whose `skills` service has a different shape can no longer break plugin load.
|
|
761
|
+
- **Housekeeping**: removed stale prebuilt `lib/` chunks that shipped dead vendor code (including an inlined schemastery copy that triggered dsh.so's `new Function` static rule); added `scripts/repro-setup.sh` plus a nested skill-catalog regression suite, and a matching TESTING.md section.
|