dsh-plugin-wsl-env 0.16.0 → 0.17.1

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.
@@ -1,336 +1,170 @@
1
1
  # Limitations
2
2
 
3
+ *A Chinese translation lives at [LIMITATIONS.zh](LIMITATIONS.zh.md); the English version is authoritative.*
4
+
3
5
  What the plugin does not do, and why. A limitation that is documented is a design
4
- boundary; one that is not is a bug report waiting to happen.
6
+ boundary; one that is not is a bug report waiting to happen. Each entry states the
7
+ limit and the remedy; the reasoning lives in [ARCHITECTURE.md](ARCHITECTURE.md) and
8
+ the documents it links.
5
9
 
6
10
  ## The filesystem substrate (`wsl-fs`)
7
11
 
8
- The WSL-routed file tools use one substrate: the resident in-distro agent —
9
- reads, writes and identities happen on the filesystem backing the target path
10
- (typically ext4 for a distro's own files, but other mounts are possible). The former
11
- Windows-side share backend (`substrate: "share"`) is retired: a profile still
12
- carrying the value is refused at boot with the migration, and WSL-routed file
13
- operations do not use the Windows-side 9p share. What to know about the substrate:
14
-
15
- - **A new file publishes with mode 0600.** That is the host backend's own POSIX
16
- publication semantics (a private 0600 staging file, renamed into place), which
17
- a Linux-host DSH session has always had; the share dropped mode sets and took
18
- the umask default instead, and that behaviour is gone with the share.
19
- Overwrites and edits keep the original bits. Run `chmod` (or ask the model to)
20
- when a new file must be executable.
21
- - **When the agent is out, file operations fail closed.** The distro stopped, or
22
- the resident crashed through its revive budget: `FS_IO_ERROR` naming the
23
- substrate, with the recovery path in the message. A single crash rides the
24
- one rebuild (read-only ops only); a spent rebuild enters the bounded revival
25
- (2/4/8 s), and three deaths make the state permanent with the restart remedy. There is no fallback I/O to degrade
26
- into — deliberately; the old failure message pointed at the share, and the
27
- share is what this plugin exists to keep the model off.
28
- - **The mutation guard survives to the write.** `createIfAbsent` publishes with
29
- a no-replace link inside the distro, which closes the check-then-write window
30
- for creates outright. An overwrite or edit carries the version it was based
31
- on into the write op, and the agent re-verifies it against a fresh stat before
32
- the rename. A mismatch returns `FS_STALE_VERSION`, but a change between the
33
- check and rename can still be overwritten; this is not an atomic
34
- compare-and-swap.
35
- - **A cancelled write can have an unknown outcome.** Cancellation is reported
36
- before the agent necessarily finishes. A write already being published may
37
- still take effect; inspect the file before retrying. The native agent refuses
38
- queued writes when it processes their cancellation before they start. The
39
- serial shell fallback removes requests cancelled while waiting on the host;
40
- a request already sent to the agent may still take effect even if it has not
41
- yet acknowledged the request.
42
- - **A successful write is not guaranteed durable across a crash.** The native
43
- agent syncs the staged file before publication; the shell agent attempts
44
- `sync -f` but ignores failure. Neither syncs the containing directory after
45
- the rename or link. A kernel panic or power loss can therefore lose data or
46
- the published directory entry; the operation is not a crash-consistent store.
47
- - **A Linux filename containing a backslash cannot be addressed.** The UNC
48
- display form (`\\wsl.localhost\<distro>\...`) is the file's identity
49
- everywhere - the session header, the GUI, the caches - and the backslash is
50
- the UNC's separator, so `a\b.ts` in the distro round-trips to `a/b.ts`: the
51
- tools would silently address a different path. This is the coordinate
52
- system's own limit — a backslash-delimited identity cannot spell a name
53
- containing a backslash — not a defect of the substrate; a rename away from
54
- backslash names is the workaround.
55
- - **Stage two routes mutations through a confined resident.** A write or edit
56
- under a confined policy runs on its own long-lived agent, spawned inside the
57
- bwrap profile that binds exactly what the mode grants — so the kernel, not
58
- just the host-side check, refuses a write outside the workspace, and the
59
- refusal carries `FS_SANDBOX_DENIED` (the command path's dialect). Costs to
60
- know: `bubblewrap` becomes required for confined writes (a distro without it
61
- refuses them with the bootstrap command, rather than downgrading); a policy
62
- change addresses a different resident, so the first write after switching the
63
- Permissions selector pays one agent startup; and a write the policy grants to
64
- the distro's `/tmp` lands in the distro's real `/tmp`, because the
65
- file-writes resident binds it read-write — where the read tools, the plain
66
- resident and the user's own shell all see it. The ephemeral tmpfs belongs to
67
- the command side alone, so a file handed over from a command still goes
68
- through the session workspace.
69
- - **`glob` and `grep` run the distro's own rg** (`lib/search-route.js`): the
70
- tool's arguments are forwarded verbatim, so results, output format and exit
71
- codes are rg's own — at ext4 speed, with the distro's `.gitignore` rules.
72
- A distro without rg fails the search outright: exit 127, with the message
73
- from the exec launcher and never from rg itself (which is the missing
74
- program) — `setsid: failed to execute rg: No such file or directory` where
75
- the distro has `setsid`, the script shell's `rg: not found` where it does
76
- not. The install command `scripts/bootstrap.sh` prints — `wsl.exe -d <distro>
77
- -u root -- apt-get install -y bubblewrap git ripgrep inotify-tools`, or the
78
- detected family's spelling — installs it. `--install` runs that command only
79
- when `bubblewrap` itself is missing, so a distro that has bwrap but not rg
80
- needs the printed line run by hand. A search whose directory is a Windows
81
- folder keeps the host binary, which is native there.
82
- - **`watch()` prefers inotify, with a polling fallback.** When `inotifywait`
83
- is installed and the target is an unbounded directory, the watcher uses
84
- recursive inotify watches and coalesces events to one callback per 0.5s.
85
- Otherwise — a file target, a positive `maxDepth`, or no `inotify-tools` — it
86
- is the `find -newer` poll loop (a `stat` fingerprint for a file), so the
87
- coarse-invalidation caveats below apply.
88
- - **With `sessionSandbox: false`, a confined command's `/tmp` is not the write
89
- tool's `/tmp`.** `workspace-write` mounts a fresh tmpfs at `/tmp` for the
90
- command — that is what makes it ephemeral — while the file tools' fence grants
91
- the distro's *real* `/tmp`. Both are writable, so neither side is refused, but a
92
- file the write tool puts in `/tmp` is invisible to `bash` and vice versa. Hand
93
- a file between them through the session workspace. The shipped preset turns
94
- the session runtime on, and `wsl-fs` shares it for confined writes, so
95
- commands and writes bind the same real `/tmp` and workspace.
96
- - A distro that is stopped or unregistered while the app runs takes the file
97
- tools' substrate with it: reads and writes report the substrate-unavailable
98
- error naming the cause. A death with nothing in flight costs nothing — the
99
- next call starts a fresh transport, so a distro that comes back is served
100
- again. A death with a request in flight spends the resident's one permitted
101
- rebuild, and when that rebuild cannot complete — the distro is still down, or
102
- gone — the resident is permanently out: every later file operation fails the
103
- same way until the app restarts.
104
- - `watch()` observes from inside the distro, so callbacks are coarse
105
- invalidation, not per-file notifications. With inotify they arrive as events
106
- (coalesced to 0.5s); with the polling fallback they arrive on the poll
107
- cadence, not instantly. In the polling path a creation is seen even when the
108
- new file's own mtime is old: creating it bumps the containing directory's
109
- mtime, and the scan includes the watched directory itself (deletions are seen
110
- the same way). What escapes the polling stamp is a change that leaves every
111
- scanned mtime older than it — an in-place `cp -p` onto an existing file (new
112
- content, a copied-back mtime), or a `tar -x` whose archive restores the
113
- containing directory's own mtime. That blind spot does not apply to the
114
- inotify path. A FILE target is watched by its `mtime:size` fingerprint: a
115
- rename-over or an in-place write fires, a removal reports the target as gone,
116
- and a change that preserves both mtime and size escapes it. Watching a 9p
117
- share from the Windows side remains unsupported — the loop runs where the
118
- kernel can actually report change.
12
+ One substrate: the resident in-distro agent (ext4 for distro files). The retired
13
+ Windows-side share backend (`substrate: "share"`) is refused at boot; WSL-routed
14
+ operations never use the 9p share.
15
+
16
+ - **A new file publishes with mode 0600.** Overwrites and edits keep the original
17
+ bits; `chmod` when a new file must be executable.
18
+ - **When the agent is out, file operations fail closed** — `FS_IO_ERROR` naming the
19
+ substrate, with the recovery path. One crash rides a single rebuild; a spent
20
+ rebuild enters bounded revival (2/4/8 s); three deaths are permanent until the app
21
+ restarts. There is no fallback I/O — deliberately.
22
+ - **The mutation guard is not atomic compare-and-swap.** Creates use a no-replace
23
+ link; overwrites re-verify the version before the rename. A mismatch returns
24
+ `FS_STALE_VERSION`, but a change between check and rename can still be overwritten.
25
+ - **A cancelled write can have an unknown outcome** — inspect the file before
26
+ retrying.
27
+ - **A successful write is not crash-durable.** The staged file is synced, the
28
+ containing directory is not: a power loss can lose the entry.
29
+ - **A Linux filename containing a backslash cannot be addressed** — the backslash is
30
+ the UNC separator, so `a\b.ts` round-trips to `a/b.ts`. Rename away from backslash
31
+ names; this is the coordinate system's own limit.
32
+ - **Confined writes run on their own bwrap-confined resident** — kernel-enforced,
33
+ refusing outside the workspace with `FS_SANDBOX_DENIED`. Costs: `bubblewrap`
34
+ required; a policy change pays one agent startup; a write granted to `/tmp` lands
35
+ in the distro's real `/tmp`.
36
+ - **`glob`/`grep` run the distro's rg**; a distro without rg fails the search
37
+ (exit 127). Install: `wsl.exe -d <distro> -u root -- apt-get install -y bubblewrap
38
+ git ripgrep inotify-tools` (or the detected family's spelling). `--install` runs it
39
+ only for a missing `bubblewrap`.
40
+ - **`watch()` is coarse invalidation, not per-file notifications**: inotify events
41
+ coalesced to 0.5 s, or a `find -newer` poll loop otherwise. The polling path misses
42
+ an in-place `cp -p` or a `tar -x` that restores older mtimes; a FILE target watches
43
+ an `mtime:size` fingerprint. 9p watching from Windows stays unsupported.
44
+ - **With `sessionSandbox: false`, a confined command's `/tmp` differs from the write
45
+ tools' `/tmp`** — hand files over through the session workspace. The shipped
46
+ preset (`sessionSandbox: true`) shares one real `/tmp` and has no such split.
119
47
  - `editText` reads the whole file into memory before writing it back.
48
+ - A distro that dies with a request in flight spends the resident's one rebuild;
49
+ still down, the resident stays out until the app restarts.
120
50
 
121
51
  ## The GUI workspace-files service
122
52
 
123
- - **The GUI file tree and previews use the shipped `workspace-files` row.** The
124
- plugin deliberately does not replace it: its client half is the browser `file`
125
- resource provider the Sidebar previews need, and a server-only replacement
126
- drops it (the earlier `workspace-files-wsl` takeover did exactly that). The
127
- shipped service reads WSL paths through the routed root filesystem
128
- (`fs-routing`), so the file tree and previews still run inside the distro.
129
-
130
- - **That path costs extra agent round trips.** A listing, `stat` or page read
131
- goes through `ctx.fs` before reaching the resident agent: roughly four to six
132
- round trips instead of one, and a deep page read streams the preceding lines
133
- through the pipe before cutting. Expect the GUI file tree and previews to be
134
- slower than a direct agent call, especially on large files; the model's own
135
- file operations are unaffected — they use the preset filesystem and the
136
- resident agent directly. Measured 2026-10-19 with
137
- [`test/probe/gui-preview.sh`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/gui-preview.sh) (20 iterations on
138
- this machine): the routed backend's `stat` p95 2.7 ms, `listDir` p95 5.1 ms,
139
- and a 1 MiB `readByteRange` p95 **63.5 ms/MiB** — the service's own round
140
- trips sit on top of those, and the 1 s/MiB budget has a wide margin. No
141
- optimization is justified by these numbers; revisit only with a measured
142
- regression.
143
-
144
- - **`fs-routing` is the seam.** The shipped service depends on the routed root
145
- filesystem staying enabled and faithful to the `ctx.fs` contract (`resolve`,
146
- `lstat`, `stat`, `listDir`, `streamText`, `readBytes`, `watch`, `contains`,
147
- `processPath`). Disabling `fs-routing` restores the host 9p-backed root
148
- filesystem, and the GUI file service then crosses 9p for WSL workspaces. The
149
- shipped row's own caps (`maxBytes`, `maxFileBytes`, `maxLines`, `maxEntries`)
150
- can be overridden on the `workspace-files` row.
53
+ - The GUI file tree and previews use the shipped `workspace-files` row, reading WSL
54
+ paths through the routed root filesystem — deliberately not replaced (a server-only
55
+ takeover would drop the browser `file` provider the Sidebar previews need).
56
+ - **Expect extra round trips** (roughly 4-6 per listing/stat/page; measured p95:
57
+ `stat` 2.7 ms, `listDir` 5.1 ms, 1 MiB read 63.5 ms/MiB — 2026-10-19,
58
+ [probe](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/gui-preview.sh)).
59
+ The model's own file operations are unaffected.
60
+ - `fs-routing` is the seam: disabling it restores the host 9p-backed root filesystem.
61
+ The shipped row's caps can be overridden on the `workspace-files` row.
151
62
 
152
63
  ## `@` completion
153
64
 
154
- - **`@` references still walk the 9p share.** The shipped
155
- `file-reference-local` service uses `node:fs/promises` from the Windows
156
- harness process, so a WSL workspace pays a 9p `readdir`/`lstat` per
157
- directory. On a 20-directory / 520-file tree the walk was **84.7 ms**
158
- median over 9p versus **1.5 ms** on ext4 (~56×;
159
- [`probe:file-reference`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/file-reference-9p.sh), 2026-10-19).
160
- [`src/file-reference-wsl.ts`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/src/file-reference-wsl.ts) is the distro-side
161
- traversal (built and unit-tested) but is **not** in `cordis.patch.yml`: the
162
- funnels it would replace are module-internal. The fallback is the 9p walk
163
- until upstream exposes `FileReferenceTraversal`. Cost, seam and wire
164
- trigger: [UPSTREAM-FILE-REFERENCE-SEAM.md](UPSTREAM-FILE-REFERENCE-SEAM.md).
65
+ - **`@` references walk the 9p share** (~56× slower than ext4 on a probed tree,
66
+ 2026-10-19). The distro-side traversal is built but not mounted — it needs the
67
+ upstream `FileReferenceTraversal` seam:
68
+ [UPSTREAM-FILE-REFERENCE-SEAM.md](UPSTREAM-FILE-REFERENCE-SEAM.md).
165
69
 
166
70
  ## The sandbox
167
71
 
168
- - **The one-shot fallback has only the host deadline.** With `agent: false`, or
169
- when the resident is out of service, a command runs through a fresh
170
- `wsl.exe`. The host kills `wsl.exe` at the timeout, but WSL does not relay
171
- that kill into the distro, so the Linux-side process tree can outlive the
172
- call. The session runtime path — the shipped default — is cleaned by its
173
- scope. A systemd-scoped one-shot (with explicit environment pass-through) is
174
- not shipped; prefer `agent: true` and the session runtime for long commands.
175
- - **Batch file reads share one agent per distro.** Since A7 the shared read
176
- agent is the native binary, so 16 concurrent `stat`/`read`/`list` calls
177
- complete in 2.4-4.1 ms (A6 measured 342-614 ms on the shell). The first call
178
- pays the native preparation/cache cost (~70 ms once); `agentBackend: shell`
179
- forces the shell reads. v6's FS concurrency would only shave the native
180
- runtime's 7.7 ms/16 writes and is deferred.
181
- - **The native backend is the default; aarch64 is executed under qemu in CI.**
182
- The `native packages` job builds static-musl x86_64 and aarch64 binaries and
183
- runs the 18-check suite against both the freshly built aarch64 artifact and
184
- the committed `agent/native/linux-aarch64/dsh-wsl-agent` under qemu-user.
185
- Physical aarch64 WSL is out of scope (no Windows-on-ARM machine in reach);
186
- see [COMPATIBILITY.md](COMPATIBILITY.md#scope-decisions).
187
- `agentBackend: auto` still selects the packaged binary on an aarch64 distro;
188
- `native` refuses if preparation fails, and `shell` forces the shell agent.
189
- - **The session runtime needs systemd and a lingering user manager.** The
190
- shipped preset sets `sessionSandbox: true`; a distro without systemd as PID 1,
191
- a reachable `systemd --user`, `systemd-run`, or `Linger` is refused on the
192
- first command with the remedy. Set `sessionSandbox: false` on the preset's
193
- `wsl-shell` to keep the per-command shape. A **hard crash** of the app can
194
- leave a stale `dsh-wsl-*.scope` and its background processes behind; a
195
- graceful session end stops the scope (validated). The session preflight
196
- sweeps stale scopes when the next WSL session opens: it stops a
197
- `dsh-wsl-*.scope` only when its cgroup has no agent process (`wsl-agent.sh`
198
- or the native `dsh-wsl-agent`), so another live session's runtime is never
199
- touched.
200
- - **The sandbox does not govern WSL interop.** A confined command can still run a
201
- Windows program, which escapes the Linux-side boundary. The plugin reports this as
202
- `enforcement: partial`. See [ARCHITECTURE.md](ARCHITECTURE.md#reported-enforcement).
203
- The `maskWindowsDrive` key on `wsl-shell`, `wsl-fs` and `fs-routing` narrows the
204
- hole: the confined profile shadows `/mnt` with an empty tmpfs, so files and
205
- executables at paths under `/mnt` are hidden. Other mounts, alternate access
206
- paths and unrestricted file reads are not covered by this mask. It is not a
207
- closure — a command that can write the workspace can write an executable there and
208
- run it (binfmt interop dispatches on file content, not location) — so enforcement
209
- stays `partial`, the result's sandbox fact reports `windowsDrive: "masked"`, and
210
- distro-level disabling of WSL interop (`[interop] enabled=false` in
211
- `wsl.conf`) must be configured separately. On `wsl-shell`, an explicitly set
212
- `interopVisibility` takes precedence over `maskWindowsDrive`.
213
- - **`bubblewrap` must be installed**, or both providers fail immediately. The
214
- usability probe re-runs after a failure, so installing bubblewrap while the app
215
- runs is believed on the next command; only a success is cached. The refusal
216
- classifies the probe's failure — missing versus present-but-failing — and the
217
- direct install command follows the distro's package family; a probe failure
218
- with no recognisable shape falls back to the install remedy with the raw
219
- output quoted. A session-start preflight (`wsl-preflight`) surfaces the same
220
- text as a warning; it is advisory and never installs.
221
- - `workspace-write` binds the workspace root as writable, and bubblewrap refuses a
222
- bind whose source directory does not exist, so a session whose workspace directory was
223
- deleted fails with a runner error; the plugin does not recreate it. A workdir that
224
- does not exist — a configured `cwd` pointing at a deleted directory, for instance — is
225
- a separate case and is now reported as an error naming that directory, because
226
- `wsl.exe` would otherwise run the command in `/` and exit 0.
72
+ - **The one-shot fallback has only the host deadline** — a killed `wsl.exe` does not
73
+ relay the kill into the distro, so the Linux process tree can outlive the call.
74
+ Prefer `agent: true` and the session runtime for long commands.
75
+ - **Batch reads share one native agent per distro** (16 concurrent calls in
76
+ 2.4-4.1 ms; first call pays ~70 ms preparation).
77
+ - **aarch64 runs under qemu in CI only** (no Windows-on-ARM machine in reach —
78
+ [COMPATIBILITY.md](COMPATIBILITY.md#scope-decisions)). `auto` still selects the
79
+ packaged binary; `native` refuses on preparation failure; `shell` forces the shell
80
+ agent.
81
+ - **The session runtime needs systemd and a lingering user manager** — refused on the
82
+ first command with the remedy otherwise; `sessionSandbox: false` keeps the old
83
+ per-command shape. A hard app crash can leave a stale `dsh-wsl-*.scope`; the
84
+ preflight sweeps stale scopes on the next WSL session (never another live
85
+ session's).
86
+ - **The sandbox does not govern WSL interop** — reported as `enforcement: partial`.
87
+ `maskWindowsDrive` narrows `/mnt` but is not a closure; distro-level
88
+ `[interop] enabled=false` in `wsl.conf` must be configured separately.
89
+ [ARCHITECTURE.md](ARCHITECTURE.md#reported-enforcement).
90
+ - **`bubblewrap` must be installed**, or both providers fail with the install
91
+ command; a session-start preflight surfaces the same text as an advisory warning.
92
+ - A workspace directory deleted mid-session fails with a runner error; the plugin
93
+ does not recreate it. A configured `cwd` that does not exist is reported as an
94
+ error naming the directory.
95
+
96
+ ## Shell startup semantics (`wsl-shell`)
97
+
98
+ Commands run as `<shell> -lc <command>` — login (profile scripts sourced, so version
99
+ managers work) and **non-interactive** (no prompt/plugin output in the model's
100
+ streams).
101
+
102
+ - **`~/.zshrc` / `~/.bashrc` are never read.** Move machine-level PATH setup
103
+ (`fnm`, nvm, cargo) into `~/.zprofile` or `~/.zshenv`; keep prompts and completion
104
+ in the rc file.
105
+ - **fnm's multishell PATH cannot form inside the sandbox** (`/run` is read-only) —
106
+ use the fallback recipe in
107
+ [Configuration](CONFIGURATION.md#zshfnm-recipe-for-model-run-commands).
227
108
 
228
109
  ## Agent diagnostics
229
110
 
230
- - **The doctor's `agent.handshake` check probes a fresh resident, not the live
231
- one.** It starts `agent/wsl-agent.sh` in the distro, reads its HELLO (pid,
232
- protocol, script digest) and stops it, so it proves that the agent path can
233
- start and that the file the distro reads is the shipped one. It cannot read
234
- the running app resident's last error — there is no IPC channel from a plugin
235
- script to the app process — so that error stays in the app log. In-process,
236
- `WslAgent.status()` exposes state, pid, lastError, deaths and pending.
237
- - **`dsh-wsl-*.scope` counts are point-in-time.** The doctor lists and inspects
238
- the user scopes once; a scope created or stopped a moment later is not
239
- reflected. `--fix` runs the same sweep the session preflight uses.
240
-
241
- ## The doctor's sessions table
242
-
243
- - **The sessions table shows distro-side facts only.** The cross-distro view
244
- (`distro → scope → state → resident pids`) attests what the distro can see:
245
- which scopes exist and which pids inside them are agent processes. A
246
- resident's idle countdown and its `lastError` live in the host process that
247
- owns it — there is no IPC channel from a plugin script to the app, so the
248
- table cannot show them (`WSL-313`'s upstream ask). The agent.handshake
249
- check notes a live resident's presence when the scope survey sees one, and
250
- says that its own probe starts a fresh one.
111
+ - **The doctor's `agent.handshake` probes a fresh resident**, not the live one —
112
+ a running resident's `lastError` stays in the app log (`WslAgent.status()` exposes
113
+ it in-process).
114
+ - **`dsh-wsl-*.scope` counts are point-in-time**; `--fix` runs the preflight's sweep.
115
+ - **The doctor's `git.win-safe` probe reports the Windows git trust through interop
116
+ `git.exe`** — the `safe.directory` whitelist that WSL change review needs when
117
+ Windows git is installed (see Terminals below).
251
118
 
252
119
  ## Session-start advisories
253
120
 
254
- - **There is no session-UI notice seam in DSH 0.2.1.** When a distro session
255
- opens, `wsl-preflight` checks `bubblewrap`, systemd as PID 1, the user
256
- manager, `systemd-run` and Linger. A failure is advisory: it never installs
257
- anything, never cancels the session and never changes the fail-closed
258
- behavior of the first command. The finding goes to the host log
259
- (`logger.warn`) and into the session's runtime context as a `wsl-preflight`
260
- entry — model-visible on the next assembly — but the GUI today has no
261
- toast/banner service a plugin can publish to. `pnpm run doctor` is the
262
- deterministic human-facing check; the exact upstream ask is
121
+ - **There is no session-UI notice seam in DSH 0.2.1.** `wsl-preflight` failures go
122
+ to the host log and the session's runtime context (model-visible next assembly) —
123
+ advisory only, never installing or cancelling. The upstream ask:
263
124
  [UPSTREAM-SESSION-NOTICE.md](UPSTREAM-SESSION-NOTICE.md).
264
125
  - **The runtime-context entry is a repeated snapshot, not a dismissible
265
- notification.** It appears in every assembly until the environment is fixed;
266
- it has no severity decoration and no dismiss control. Removing the
267
- `wsl-preflight` row (or setting `sandbox: false` on it) silences the probe
268
- entirely.
126
+ notification.** Removing the `wsl-preflight` row silences the probe entirely.
269
127
 
270
128
  ## Ports
271
129
 
272
- - **The port poller keeps the resident agent permanently warm.** The
273
- `DSH_WSL_PORTS` snapshot refreshes through the shared resident agent every
274
- `portsRefreshMs` (10 s by default, well inside the agent's idle timeout), so
275
- while a distro's session is active the agent never idles out: one `wsl.exe`
276
- and one in-distro agent process stay resident per active distro even with no
277
- command in flight. The poller starts only for a distro a session actually
278
- uses; raising `portsRefreshMs` past the agent's idle timeout restores idle
279
- shutdown, at the cost of staler port facts.
130
+ - **The port poller keeps the resident agent permanently warm** per active distro
131
+ (refresh 10 s). Raising `portsRefreshMs` past the agent's idle timeout restores
132
+ idle shutdown, at the cost of staler port facts.
280
133
 
281
134
  ## The directory picker
282
135
 
283
- - The directory picker lists distro levels from inside the distro — the
284
- resident's `find -L <dir> -maxdepth 1 -mindepth 1 -type d -print0` in one
285
- round trip, a one-shot `wsl.exe --exec find` when it is out, never the 9p
286
- walk the host used to pay per entry. Entries arrive NUL-separated, so a
287
- directory name containing a newline lists as one exact row (WSL-306 retired
288
- the `ls -1ALp` line parse that used to split such a name into two rows or
289
- drop it).
136
+ - Distro levels list from inside the distro in one round trip (NUL-separated, so
137
+ newline-bearing directory names list exactly — WSL-306); a one-shot `wsl.exe --exec
138
+ find` when the resident is out.
290
139
 
291
140
  ## Terminals
292
141
 
293
- - Terminal routing is per-session by workspace coordinates (`subprocess-wsl.hostSessions`,
294
- default on): a WSL-folder session gets the distro shell, a Windows-folder session gets
295
- `powershell.exe` in its own directory. The shell MENU remains the single configured
296
- profile, and a directory-less launch still lands in the distro.
297
- - A host session's TAB TITLE still reads `WSL`, the composition's shell profile
298
- name, over a PowerShell process. The controller titles a tab from that profile
299
- before the provider rewrites the launch, and a plugin cannot correct it: a
300
- patch layer asserts rather than renames a row's module, and the row's id is
301
- load-bearing for the web composition (disabling it fails web boot with
302
- "waiting for service: webTerminals"). The full account and the proposed
303
- upstream change are in [UPSTREAM-TERMINAL-TITLE.md](UPSTREAM-TERMINAL-TITLE.md).
304
- Meanwhile, double-click a tab's title to rename it, or add `shellCandidates`
305
- to the `terminal-controller` row in your own profile layer — a manually
306
- selected shell is titled after itself.
307
- - Distro terminals ARE reclaimed when idle — the observation is distro-side:
308
- each terminal is launched with a `DSH_TERMINAL_ID` marker, and the handle's
309
- `inspectActivity` counts the marked processes through the resident agent
310
- (`lib/terminal-activity.js`). Exactly one (the shell at its prompt) is
311
- `idle`; more than one (a command, a pipeline, a nested shell) is `busy`;
312
- `unknown` — the agent out, the distro stopped — pauses reclamation, because
313
- a probe that cannot answer must not authorize a close. Consequences to
314
- know: a nested shell keeps the terminal busy by design (closing it would
315
- lose that shell), so does any process the shell's startup files spawn with
316
- the inherited marker (an `ssh-agent` launched in `.bashrc` reads as busy);
317
- the idle threshold is the controller's `unattendedTimeoutMs` (2 h by
318
- default); and the host-side shell-activity integration is still not armed
319
- for these launches — `wsl.exe` on win32 fails its gate — which is why the
320
- observation is a scan rather than shell cooperation. `terminalIdleReclaim:
321
- false` on the `subprocess-wsl` row restores close-by-hand: the launch then
322
- carries no `DSH_TERMINAL_ID` marker, and the handle keeps the shipped
323
- `unknown` activity, which never accumulates idle.
324
- - Windows-folder host terminals (the `powershell.exe` branch) keep the shipped
325
- behaviour: `unknown` activity, no reclamation.
142
+ - Terminal routing is per-session by workspace coordinates: a WSL-folder session gets
143
+ the distro shell, a Windows-folder session gets `powershell.exe` in its own
144
+ directory. The shell MENU's default entry stays the configured profile; a
145
+ directory-less launch lands in the distro.
146
+ - **Windows git must trust the share** (`safe.directory`), or WSL workspaces'
147
+ changed-files cards silently disappear (the discovery call answers
148
+ `detected dubious ownership`). One-time remedy on Windows:
149
+ `git config --global --add safe.directory "//wsl.localhost/*"`, then restart the
150
+ app. Two alternatives: the doctor's `git.win-safe` probe reports the state, and a
151
+ machine **without** Windows git needs nothing — the provider routes discovery into
152
+ the distro and translates the output into UNC form.
153
+ - **A NEW tab's title reads `WSL`** whatever it runs — the controller titles from the
154
+ composition's profile before the provider rewrites the launch, and a plugin cannot
155
+ correct the default ([UPSTREAM-TERMINAL-TITLE.md](UPSTREAM-TERMINAL-TITLE.md)).
156
+ The menu ships host candidates (`powershell`, `cmd`): a manually selected shell is
157
+ titled after itself; double-click rename also works.
158
+ - **Distro terminals ARE reclaimed when idle** — the handle counts the marked
159
+ processes through the resident agent (exactly one at its prompt = `idle`; a nested
160
+ shell or an `ssh-agent` from startup files reads `busy` by design). Threshold: the
161
+ controller's `unattendedTimeoutMs` (2 h). `terminalIdleReclaim: false` restores
162
+ close-by-hand. Windows-folder host terminals keep the shipped no-reclamation
163
+ behaviour.
326
164
 
327
165
  ## Host "Open in app"
328
166
 
329
- - **Opening VS Code (or another Electron editor) from the host "Open in app"
330
- menu can fail independently of this plugin.** The DSH Host process runs with
331
- `ELECTRON_RUN_AS_NODE=1`. The open-in-app launcher inherits that variable, so
332
- `Code.exe` starts as Node, exits 1, and the UI reports launch-failed. File
333
- Explorer and other non-Electron launchers are unaffected; the failure is the
334
- same on a Windows folder and on a WSL UNC workspace. This repository does
335
- not own that spawn path. The record and the upstream fix are in
167
+ - **Opening VS Code from "Open in app" can fail independently of this plugin** — the
168
+ Host runs with `ELECTRON_RUN_AS_NODE=1`, which the launcher inherits, so
169
+ `Code.exe` starts as Node and exits 1. Not owned by this repository:
336
170
  [UPSTREAM-OPEN-IN-APP.md](UPSTREAM-OPEN-IN-APP.md).