dsh-plugin-wsl-env 0.1.0

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.
@@ -0,0 +1,306 @@
1
+ # The bundle layer this package contributes:
2
+ #
3
+ # "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
4
+ #
5
+ # `dsh plugin --profile <name> add dsh-plugin-wsl-env` installs the package and
6
+ # appends this file as one layer — there is nothing to merge by hand. It is
7
+ # still an ordinary patch layer: a top-level YAML array of loader patch entries
8
+ # (id-targeted config overrides, disables, and insert lists; `!!js` expressions
9
+ # allowed).
10
+ #
11
+ # Layers compose in the order bundles, the profile's own `cordis.patch.yml`,
12
+ # the home-level `cordis.patch.yml`, then `--patch` overlays, and later layers
13
+ # win per row. A row's `config` is replaced whole, never deep-merged, so a user
14
+ # who wants a different value overrides that row by id in their own layer —
15
+ # examples/profile.cordis.patch.yml shows what such a layer looks like.
16
+ #
17
+ # ═════════════════════════════════════════════════════════════════════════════
18
+ # The GUI terminal window is a COMPOSITION-level environment, not a preset one.
19
+ #
20
+ # The right-sidebar terminal is dsh-api-terminal-controller. It resolves its
21
+ # execution world from the Session's own context —
22
+ # agent.ctx.get("subprocess")
23
+ # — and that context is created by the agent loop under the ROOT realm
24
+ # (dsh-agent-loop: `createScope(loopCtx, …)`, loopCtx = the root `ctx.agents`),
25
+ # while a preset's isolate realms are created by dsh-agent-preset-registry under
26
+ # the REGISTRY's context (`createScope(this.owner, …)`). The two subtrees never
27
+ # meet, so a `subprocess` provider mounted inside preset-wsl — the natural-looking
28
+ # design, since `shell` and `fs` live there — is invisible to the terminal
29
+ # window. The registry ships `agentPresets.serviceFor(agent, name)` for exactly
30
+ # that gap; the terminal controller does not use it.
31
+ #
32
+ # So the terminal is bound here, once, for the whole composition:
33
+ #
34
+ # 1. the shipped local provider is disabled and replaced by a subclass whose
35
+ # ONLY override is `spawnTerminal`, which rewrites a `wsl.exe` launch into
36
+ # `wsl.exe -d <distro> --cd <linux dir>` for the directory the Session
37
+ # supplies. Ordinary `spawn()` — every command, the host ripgrep search,
38
+ # every other consumer — reaches the shipped implementation untouched;
39
+ # 2. the controller is given the one shell profile it should resolve, so the
40
+ # window's default is a distro shell rather than `cmd.exe` inside a UNC
41
+ # workspace. Its shell menu is reduced to that single entry on purpose:
42
+ # shell discovery also runs against the root provider and receives no
43
+ # directory, so offering host executables there would advertise shells this
44
+ # terminal never starts.
45
+ #
46
+ # Consequence: the terminal follows the composition, not the Session. A Session
47
+ # whose workspace is a Windows folder gets the same distro terminal, started in
48
+ # that folder as `/mnt/<drive>/…`. That is the honest limit of a per-session
49
+ # execution world that no Session identity reaches.
50
+ # ═════════════════════════════════════════════════════════════════════════════
51
+ - id: subprocess
52
+ disabled: true
53
+
54
+ - id: terminal-controller
55
+ name: '@deepseek-ai/dsh-api-terminal-controller'
56
+ config:
57
+ shell:
58
+ path: wsl.exe
59
+ name: WSL
60
+ args: []
61
+ shellCandidates: []
62
+
63
+ - insert:
64
+ - id: subprocess-wsl
65
+ name: 'dsh-plugin-wsl-env/subprocess'
66
+ config:
67
+ distro: ''
68
+
69
+ # ═════════════════════════════════════════════════════════════════════════════
70
+ # Environment is per-session, not per-process.
71
+ #
72
+ # There is no DSH_WSL switch any more. The profile keeps the SHIPPED environment
73
+ # for host workspaces — sandboxed PowerShell executor, ACL-confined filesystem,
74
+ # and therefore the Permissions selector — while the wsl agent preset (appended
75
+ # below) supplies the WSL environment to agents that select it, isolated to that
76
+ # preset's own service realm. Both can run at the same time in one process.
77
+ #
78
+ # dsh-plugin-wsl-env/auto-preset binds the WSL preset automatically when a new
79
+ # session's workspace directory is inside a distro, and leaves an explicit
80
+ # operator choice alone.
81
+ # ═════════════════════════════════════════════════════════════════════════════
82
+
83
+ # ── The workspace picker serves BOTH worlds ──────────────────────────────────
84
+ #
85
+ # Kept from the previous design: the shipped chooser resolves to the native OS
86
+ # dialog on Windows, which cannot browse a distro. This one lists the Windows home
87
+ # directory and every installed distro in its root level, so it is the single
88
+ # picker for host and WSL workspaces alike — and the directory it returns is what
89
+ # auto-preset reads to choose the environment.
90
+ - id: directory-picker
91
+ disabled: true
92
+
93
+ - insert:
94
+ - id: directory-picker-wsl
95
+ name: 'dsh-plugin-wsl-env/picker'
96
+ config:
97
+ preferredDistro: ''
98
+ includeHostHome: true
99
+ maxEntries: 1000
100
+
101
+ - id: directory-picker-browse-surface
102
+ name: '@deepseek-ai/dsh-client-ui-directory-picker-browse'
103
+ # Contributes the WSL environment's facts (distro / real shell / home) to the
104
+ # managed DSH_* namespace that dsh-shell-env injects into every model shell
105
+ # call. That is DSH's sanctioned channel for shell-environment facts.
106
+ - id: wsl-shell-env
107
+ name: 'dsh-plugin-wsl-env/shell-env'
108
+
109
+ # Per-session environment selection from the workspace directory. It only
110
+ # rewrites a preset that still equals the registry default, so an operator's
111
+ # explicit choice is never overridden.
112
+ - id: auto-preset
113
+ name: 'dsh-plugin-wsl-env/auto-preset'
114
+
115
+ - insert:
116
+ # ═════════════════════════════════════════════════════════════════════════════
117
+ # preset-wsl — the WSL environment, scoped to agents that select this preset.
118
+ #
119
+ # This is what makes environments per-session instead of per-process: the shell
120
+ # and fs services are isolated to this preset's realm, so an agent on the host
121
+ # preset keeps the profile's global sandboxed PowerShell executor and ACL-confined
122
+ # filesystem while an agent on this preset gets the distro's login shell and its
123
+ # UNC-backed filesystem — concurrently, in one process.
124
+ #
125
+ # The wrapping group is required by dsh-agent-preset-registry, which audits every
126
+ # preset mount and rejects a provider that is not behind an isolate realm:
127
+ # "Preset services require isolate realms: shell, fs"
128
+ # Because a preset's plugins mount under a REGISTRY-owned scope (not this row's
129
+ # ctx), the realm has to be declared here, inside config.plugins.
130
+ #
131
+ # Generated from the shipped standard preset by build-preset-wsl.mjs so the
132
+ # embedded plan-mode section and every nested group survive byte-for-byte.
133
+ # Re-run it after an app update that changes the preset.
134
+ # ═════════════════════════════════════════════════════════════════════════════
135
+ - id: preset-wsl
136
+ name: '@deepseek-ai/dsh-agent-preset'
137
+ config:
138
+ id: wsl
139
+ order: 2
140
+ plugins:
141
+ - id: wsl-env
142
+ name: cordis:group
143
+ group: true
144
+ isolate:
145
+ shell: true
146
+ fs: true
147
+ config:
148
+ # ── the WSL environment for this preset only ────────────────────
149
+ # No `cwd`: an empty default workdir means the distro user's home,
150
+ # asked of the distro itself. Pin a directory here to override it —
151
+ # a value that is right on one machine is wrong on every other.
152
+ #
153
+ # Each command is confined inside the distro by bubblewrap, in the
154
+ # policy's mode, and the file tools are fenced by the same policy.
155
+ # Install bubblewrap there (`sudo apt install bubblewrap`); without
156
+ # it the providers fail closed rather than run unconfined. Add
157
+ # `sandbox: false` to either provider for the old, unconfined
158
+ # posture.
159
+ - id: wsl-shell
160
+ name: 'dsh-plugin-wsl-env'
161
+ config:
162
+ distro: ''
163
+ loginShell: true
164
+ timeoutMs: 120000
165
+ maxTimeoutMs: 600000
166
+ - id: wsl-fs
167
+ name: 'dsh-plugin-wsl-env/fs'
168
+ config:
169
+ distro: ''
170
+ restrictToDistro: true
171
+ - id: persona
172
+ name: '@deepseek-ai/dsh-persona'
173
+ config:
174
+ suffix: Your working directory is {{cwd}}.
175
+ prefix: You are a coding agent powered by the {{model}} model.
176
+ - id: agent-instructions
177
+ name: '@deepseek-ai/dsh-agent-instructions'
178
+ config:
179
+ maxBytes: 65536
180
+ - id: tool-bash
181
+ name: '@deepseek-ai/dsh-tool-bash'
182
+ disabled: false
183
+ - id: tool-pwsh
184
+ name: '@deepseek-ai/dsh-tool-pwsh'
185
+ disabled: true
186
+ - id: tool-fs
187
+ name: '@deepseek-ai/dsh-tool-fs'
188
+ - id: tool-fs-search
189
+ name: '@deepseek-ai/dsh-tool-fs-search'
190
+ config:
191
+ sampleOverCapGlobResults: false
192
+ - id: tool-jobs
193
+ name: '@deepseek-ai/dsh-tool-jobs'
194
+ - id: skill-filesystem
195
+ name: '@deepseek-ai/dsh-skill-filesystem'
196
+ - id: tool-skill
197
+ name: '@deepseek-ai/dsh-tool-skill'
198
+ - id: command-goal
199
+ name: '@deepseek-ai/dsh-command-goal'
200
+ - id: tool-goal
201
+ name: '@deepseek-ai/dsh-tool-goal'
202
+ - id: planning
203
+ name: cordis:group
204
+ group: true
205
+ isolate:
206
+ planMode: true
207
+ config:
208
+ - id: plan-mode
209
+ name: '@deepseek-ai/dsh-plan-mode'
210
+ config:
211
+ section: |
212
+ You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
213
+
214
+ Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
215
+
216
+ The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
217
+
218
+ Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
219
+
220
+ Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
221
+
222
+ When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
223
+ - id: compaction
224
+ name: cordis:group
225
+ group: true
226
+ isolate:
227
+ compaction: true
228
+ toolResultPruner: true
229
+ config:
230
+ - id: compaction-basic
231
+ name: '@deepseek-ai/dsh-compaction-basic'
232
+ - id: command-compact
233
+ name: '@deepseek-ai/dsh-command-compact'
234
+ - id: tool-result-pruner
235
+ name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
236
+ config:
237
+ thresholdChars: 8192
238
+ headChars: 4096
239
+ tailChars: 1024
240
+ - id: delegation
241
+ name: cordis:group
242
+ group: true
243
+ isolate:
244
+ workflowEngine: true
245
+ config:
246
+ - id: tool-subagent-control
247
+ name: '@deepseek-ai/dsh-tool-subagent-control'
248
+ - id: tool-subagent-list-agents
249
+ name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
250
+ - id: tool-subagent
251
+ name: '@deepseek-ai/dsh-tool-subagent'
252
+ config:
253
+ provider: spawn
254
+ toolName: subagent
255
+ modelSelectionSettings: true
256
+ backgroundMode: continuable
257
+ - id: tool-subagent-fork
258
+ name: '@deepseek-ai/dsh-tool-subagent'
259
+ config:
260
+ provider: fork
261
+ toolName: subagent_fork
262
+ backgroundMode: continuable
263
+ - id: tool-subagent-codex
264
+ name: '@deepseek-ai/dsh-tool-subagent'
265
+ disabled: true
266
+ config:
267
+ provider: codex
268
+ toolName: subagent_codex
269
+ backgroundMode: one-shot
270
+ maxDepth: provider-managed
271
+ - id: tool-subagent-claude-code
272
+ name: '@deepseek-ai/dsh-tool-subagent'
273
+ disabled: true
274
+ config:
275
+ provider: claude-code
276
+ toolName: subagent_claude_code
277
+ backgroundMode: one-shot
278
+ maxDepth: provider-managed
279
+ - id: workflow-ptc
280
+ name: '@deepseek-ai/dsh-workflow-ptc'
281
+ config:
282
+ provider: spawn
283
+ - id: tool-workflow
284
+ name: '@deepseek-ai/dsh-tool-workflow'
285
+ - id: tool-ralph
286
+ name: '@deepseek-ai/dsh-tool-ralph'
287
+ disabled: true
288
+ config:
289
+ subagentProvider: spawn
290
+ maxRounds: 64
291
+ - id: tool-ask-user
292
+ name: '@deepseek-ai/dsh-tool-ask-user'
293
+ - id: tool-todo
294
+ name: '@deepseek-ai/dsh-tool-todo'
295
+ config:
296
+ allowParallelInProgress: true
297
+ - id: tool-web
298
+ name: '@deepseek-ai/dsh-tool-web'
299
+ config:
300
+ fetch: true
301
+ searchTimeoutMs: 60000
302
+ - id: present
303
+ name: '@deepseek-ai/dsh-tool-present'
304
+ - id: tool-plugin-manager
305
+ name: '@deepseek-ai/dsh-plugin-manager/tools'
306
+ disabled: true
@@ -0,0 +1,36 @@
1
+ # A PROFILE patch layer — `$DSH_HOME/profiles/<name>/cordis.patch.yml` — not part
2
+ # of this plugin's bundle. It is applied after every bundle layer, so it is where
3
+ # a machine's own preferences and row overrides belong.
4
+ #
5
+ # This copy is the author's own machine: account/model defaults and the UI
6
+ # verbosity that suits it. Copy the rows you want, not the file, and expect the
7
+ # values to differ on your machine. The plugin's rows are NOT repeated here —
8
+ # they arrive with the bundle (see the package's cordis.patch.yml).
9
+ - id: agent-default-model
10
+ name: "@deepseek-ai/dsh-agent-default-model"
11
+ config:
12
+ provider: deepseek-account
13
+ model: deepseek-flash
14
+ reasoningEffort: high
15
+
16
+ - id: ui-settings-account
17
+ name: "@deepseek-ai/dsh-client-ui-settings-account"
18
+ config:
19
+ version: 1
20
+ step: done
21
+ purpose: development
22
+ process: detailed
23
+ completion: completed
24
+ usage: detailed
25
+ developerTools: true
26
+
27
+ - id: ui-chat
28
+ name: "@deepseek-ai/dsh-client-ui-chat"
29
+ config:
30
+ transcriptView: detailed
31
+ performanceUsage: detailed
32
+
33
+ - id: ui-settings
34
+ name: "@deepseek-ai/dsh-client-ui-settings"
35
+ config:
36
+ enabled: true
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Auto-preset: bind an agent to the WSL preset when its workspace directory
3
+ * lives inside a distro.
4
+ *
5
+ * Why an event hook and not configuration: the environment is chosen in
6
+ * `dsh-api-session-controller`'s `composeAgent(presetId)`, whose signature has no
7
+ * working directory — the cwd only exists one frame up in
8
+ * `ensureSession(sessionId, cwd, ...)`. So the path-aware decision has to be made
9
+ * where the path IS available. `agent/created` carries it:
10
+ *
11
+ * ctx.serial(carrier, "agent/created", { agent, source, signal })
12
+ *
13
+ * and `agent.session.header.cwd` is the session's workspace directory.
14
+ *
15
+ * Two details are load-bearing, both established by experiment:
16
+ *
17
+ * 1. **Respect an explicit choice.** By the time the event fires, the resolved
18
+ * preset is already mounted (`composeAgent` mounts request-or-default in
19
+ * `setup`). We cannot tell which of the two it was — so we only ever rewrite
20
+ * when the mounted preset equals the registry default, which means the client
21
+ * omitted it. An operator's pick is never overridden.
22
+ *
23
+ * 2. **Switch after creation, not during.** Switching inside the listener
24
+ * collides with the previous generation's tools:
25
+ * Error: tool "subagent" is already registered in this scope
26
+ * Deferring past the creating call fixes it — the session is still blank
27
+ * (no turn has started), so `select()` remains legal. A short retry loop
28
+ * absorbs the remaining ordering slack instead of relying on one timer.
29
+ *
30
+ * @module dsh-plugin-wsl/auto-preset
31
+ */
32
+
33
+ import { appendFileSync } from "node:fs";
34
+ import z from "@deepseek-ai/schemastery";
35
+ import { isWslUnc } from "./paths.js";
36
+
37
+ /**
38
+ * Set `DSH_WSL_TRACE` to a file path to record every decision this plugin makes.
39
+ *
40
+ * `ctx.logger` is not an injected service here, so a failure inside it would
41
+ * swallow the message — and a silently-swallowed decision is exactly how a real
42
+ * bug in this plugin stayed invisible once already.
43
+ */
44
+ const TRACE = process.env.DSH_WSL_TRACE;
45
+ function trace(message) {
46
+ if (TRACE === void 0 || TRACE === "") return;
47
+ try {
48
+ appendFileSync(TRACE, `${new Date().toISOString()} ${message}\n`);
49
+ } catch {
50
+ /* a trace is never worth failing a session over */
51
+ }
52
+ }
53
+
54
+ export const name = "auto-preset";
55
+
56
+ export const inject = ["agents", "agentPresets"];
57
+
58
+ export const Config = z.object({
59
+ /** Preset to bind for a WSL workspace. */
60
+ preset: z.string().default("wsl"),
61
+ /** Attempts before giving up; each waits `retryMs`. */
62
+ attempts: z.natural().min(1).default(6),
63
+ /** Gap between attempts, in milliseconds. */
64
+ retryMs: z.natural().default(150),
65
+ });
66
+
67
+ export async function apply(ctx, config = {}) {
68
+ const target = config.preset ?? "wsl";
69
+ const attempts = config.attempts ?? 6;
70
+ const retryMs = config.retryMs ?? 150;
71
+ const warn = (message) => {
72
+ trace(`WARN ${message}`);
73
+ // `logger` is not injectable here, so an unavailable or throwing logger must
74
+ // not turn this into a silent no-op: fall back to stderr.
75
+ let logged = false;
76
+ try {
77
+ ctx.logger?.warn?.(`auto-preset: ${message}`);
78
+ logged = ctx.logger !== void 0;
79
+ } catch {
80
+ logged = false;
81
+ }
82
+ if (!logged) {
83
+ try {
84
+ process.stderr.write(`auto-preset: ${message}\n`);
85
+ } catch {
86
+ /* logging is never worth failing a session over */
87
+ }
88
+ }
89
+ };
90
+
91
+ const consider = (agent) => {
92
+ if (agent === void 0) return;
93
+
94
+ let cwd;
95
+ let current;
96
+ try {
97
+ cwd = agent.session?.header?.cwd;
98
+ current = ctx.agentPresets.composedPreset(agent.ctx);
99
+ } catch (error) {
100
+ warn(`cannot inspect the new agent: ${error?.message ?? error}`);
101
+ return;
102
+ }
103
+ trace(`consider agent=${agent.id} cwd=${JSON.stringify(cwd)} current=${String(current)}`);
104
+
105
+ // Only a workspace inside a distro, and only when the preset is still the
106
+ // registry default (i.e. the operator did not ask for one). `undefined` means
107
+ // the creating caller mounted nothing at all — the headless runner does this
108
+ // — which is likewise "no explicit choice".
109
+ if (!isWslUnc(cwd)) return;
110
+ let fallback;
111
+ try {
112
+ fallback = ctx.agentPresets.defaultId;
113
+ } catch {
114
+ fallback = void 0;
115
+ }
116
+ if (current !== fallback && current !== void 0) return;
117
+ trace(` -> acting: fallback=${String(fallback)} isWsl=${isWslUnc(cwd)}`);
118
+
119
+ // Deferred on purpose — see the module comment.
120
+ void (async () => {
121
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
122
+ try {
123
+ const chosen = await ctx.agentPresets.select(agent, target);
124
+ ctx.logger?.debug?.(`auto-preset: "${cwd}" -> preset "${chosen}"`);
125
+ return;
126
+ } catch (error) {
127
+ const code = error?.code;
128
+ // The session already started a turn: its preset is fixed, and that is
129
+ // a legitimate outcome (the operator drove it before we got here).
130
+ if (code === "agent-preset/locked") {
131
+ warn(`"${cwd}" already began its first turn; leaving the preset as "${String(current)}"`);
132
+ return;
133
+ }
134
+ if (code === "agent-preset/not-found") {
135
+ warn(`preset "${target}" is not composed in this profile; skipping "${cwd}"`);
136
+ return;
137
+ }
138
+ if (attempt === attempts) {
139
+ warn(`could not bind preset "${target}" for "${cwd}": ${code ?? error?.name}: ${error?.message}`);
140
+ return;
141
+ }
142
+ await new Promise((resolve) => setTimeout(resolve, retryMs));
143
+ }
144
+ }
145
+ })();
146
+ };
147
+
148
+ ctx.on("agent/created", (payload) => consider(payload?.agent));
149
+
150
+ // ── Make the INITIAL preset correct, instead of correcting it afterwards ────
151
+ //
152
+ // A WSL workspace mounted on the HOST preset puts the Windows ACL sandbox in
153
+ // front of a `\\wsl.localhost` path, which cannot carry security descriptors:
154
+ // the shell tool then dies in sandbox preparation (`GetNamedSecurityInfoW
155
+ // failed (Win32 1)`) and the workspace is unusable — not merely mis-labelled.
156
+ // Correcting the preset after creation loses whenever the first turn does not
157
+ // wait, so the choice is made where the cwd still exists: the controller
158
+ // resolves the preset in `composeAgent(presetId)`, which has no cwd, but it
159
+ // calls `agents.ensureSession(sessionId, cwd, …, presetId)` — and that frame
160
+ // holds both. Filling the preset there makes the very first mount already
161
+ // correct, so the host sandbox never meets the distro path at all.
162
+ ctx.inject(["sessionController"], (controllerCtx) => {
163
+ const facade = controllerCtx.sessionController?.agents;
164
+ if (facade === void 0 || typeof facade.ensureSession !== "function") {
165
+ warn("sessionController exposes no agents.ensureSession(); the creation-time preset fix is inactive");
166
+ return;
167
+ }
168
+ const original = facade.ensureSession;
169
+ facade.ensureSession = async function (sessionId, cwd, checkPersistedIdentity, presetId) {
170
+ // An explicit choice always wins, and only a distro workspace is affected.
171
+ if (presetId !== void 0 || !isWslUnc(cwd)) {
172
+ return original.call(this, sessionId, cwd, checkPersistedIdentity, presetId);
173
+ }
174
+ try {
175
+ return await original.call(this, sessionId, cwd, checkPersistedIdentity, target);
176
+ } catch (error) {
177
+ // A session already stored under another preset rejects a different one.
178
+ // Fall back rather than turn adoption into a new failure mode.
179
+ if (!String(error?.message ?? "").includes("agent preset")) throw error;
180
+ trace(`creation-time preset "${target}" refused for ${sessionId}; falling back to the recorded one`);
181
+ return original.call(this, sessionId, cwd, checkPersistedIdentity, presetId);
182
+ }
183
+ };
184
+ trace("creation-time preset fix installed");
185
+ controllerCtx.effect(() => () => {
186
+ facade.ensureSession = original;
187
+ });
188
+ });
189
+ // Mount order is not a guarantee: a runner that creates its session during
190
+ // startup can finish before this plugin mounts and miss the event entirely
191
+ // (dsh-headless does exactly that). Sweep the agents already live so the
192
+ // decision never depends on who mounted first.
193
+ try {
194
+ for (const agent of ctx.agents.list()) consider(agent);
195
+ } catch (error) {
196
+ warn(`could not sweep already-live agents: ${error?.message ?? error}`);
197
+ }
198
+ }
package/lib/fs.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * `ctx.fs` entrypoint — load as `dsh-plugin-wsl/fs` in a profile patch.
3
+ * @module dsh-plugin-wsl/fs
4
+ */
5
+ export { WslFileSystem as default, WslFileSystem } from "./index.js";