dsh-plugin-wsl-env 0.11.0 → 0.13.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/CONTRIBUTING.md +29 -7
  3. package/README.md +124 -138
  4. package/SECURITY.md +11 -1
  5. package/SUPPORT.md +29 -2
  6. package/cordis.patch.yml +17 -19
  7. package/docs/ARCHITECTURE.md +30 -29
  8. package/docs/CONFIGURATION.md +81 -21
  9. package/docs/LIMITATIONS.md +71 -6
  10. package/docs/MIGRATION.md +98 -0
  11. package/docs/PARITY.md +1 -1
  12. package/docs/PERFORMANCE.md +60 -0
  13. package/docs/QUICKSTART.md +125 -0
  14. package/docs/README.zh.md +117 -137
  15. package/docs/ROADMAP-1.0.md +780 -0
  16. package/docs/ROADMAP.md +4 -0
  17. package/docs/UPSTREAM-FILE-REFERENCE-SEAM.md +80 -0
  18. package/docs/UPSTREAM-LSP-SEAM.md +90 -0
  19. package/docs/UPSTREAM-SESSION-NOTICE.md +95 -0
  20. package/lib/agent-errors.d.ts +19 -0
  21. package/lib/agent-errors.js +22 -0
  22. package/lib/agent-exec.js +2 -2
  23. package/lib/agent.d.ts +68 -5
  24. package/lib/agent.js +129 -22
  25. package/lib/distro-route.d.ts +75 -0
  26. package/lib/distro-route.js +86 -0
  27. package/lib/fs-routing.d.ts +4 -1
  28. package/lib/index.d.ts +166 -37
  29. package/lib/index.js +353 -75
  30. package/lib/paths.d.ts +15 -0
  31. package/lib/paths.js +28 -0
  32. package/lib/preflight.d.ts +46 -3
  33. package/lib/preflight.js +83 -14
  34. package/lib/shell-env.d.ts +9 -3
  35. package/lib/shell-env.js +121 -43
  36. package/lib/subprocess.d.ts +12 -3
  37. package/lib/subprocess.js +21 -3
  38. package/package.json +23 -15
  39. package/scripts/diagnose.sh +9 -88
  40. package/scripts/doctor.mjs +548 -0
  41. package/scripts/profile-preset.mjs +254 -0
  42. package/lib/workspace-files-route.d.ts +0 -136
  43. package/lib/workspace-files-route.js +0 -225
  44. package/lib/workspace-files-wsl.d.ts +0 -257
  45. package/lib/workspace-files-wsl.js +0 -418
package/docs/ROADMAP.md CHANGED
@@ -6,6 +6,10 @@ measurement; the numbers are in the ADR's
6
6
  [FS queue measurement](adr/0002-native-agent.md#fs-queue-measurement-a6-2026-10-18)
7
7
  section.
8
8
 
9
+ > The product and ecosystem plan toward 1.0 is
10
+ > [ROADMAP-1.0.md](ROADMAP-1.0.md). This file remains the current technical
11
+ > plan for the native agent.
12
+
9
13
  ## Done
10
14
 
11
15
  | Step | Outcome |
@@ -0,0 +1,80 @@
1
+ # Upstream seam: `@` references over a WSL workspace
2
+
3
+ Status: measured and deferred (2026-10-19). The `@` completion in
4
+ `@deepseek-ai/dsh-file-reference-local` walks the workspace with
5
+ `node:fs/promises` (`readdir` + `lstat`) from the **Windows** harness process.
6
+ For a WSL workspace that walk crosses the 9p share on every directory; the
7
+ plugin already has a distro-side traversal, but it cannot be wired until
8
+ upstream exposes the funnel. This note records the cost, the seam, and the
9
+ decision.
10
+
11
+ ## The measured cost
12
+
13
+ Probe: [`test/probe/file-reference-9p.sh`](../test/probe/file-reference-9p.sh)
14
+ (three runs, `node:fs` readdir walk, no ranking engine). Tree: 20 directories,
15
+ 520 files, one level of subdirectories; the same tree walked from Windows over
16
+ `\\wsl.localhost\<distro>` and from inside the distro on ext4.
17
+
18
+ | Walk | Median | Samples |
19
+ |---|---|---|
20
+ | Windows over 9p (`\\wsl.localhost\...`) | **84.7 ms** | 192.5, 84.7, 63.0 |
21
+ | Distro on ext4 | **1.5 ms** | 2.4, 1.5, 1.3 |
22
+
23
+ The ratio is ~56× on this tree. Variance on the 9p side is large (63–192 ms
24
+ across three runs), which is itself the point: the cost is per-directory 9p
25
+ traffic, so it grows with the tree and is sensitive to the share's state. A
26
+ synthetic 520-entry tree is a floor for a real repository, not a ceiling.
27
+
28
+ These numbers are the *traversal* only. The fuzzy ranking, generation and
29
+ invalidation logic is not part of the measurement — it is the upstream code the
30
+ seam must keep.
31
+
32
+ ## The seam
33
+
34
+ RFC addendum ① (in [rfc-wsl-workspaces.md](upstream/rfc-wsl-workspaces.md))
35
+ proposes exposing the three module-internal funnels as an injectable strategy:
36
+
37
+ ```ts
38
+ interface FileReferenceTraversal {
39
+ readWorkspaceRoot(absolute: string, signal: AbortSignal): Promise<TraversalDirent[]>
40
+ readDirectory(absolute: string, signal: AbortSignal): Promise<TraversalDirent[]>
41
+ resolveDisplayDirectory(root: string, displayDirectory: string, signal: AbortSignal): Promise<string | undefined>
42
+ }
43
+ ```
44
+
45
+ with three semantics that matter for a remote world:
46
+
47
+ - **Dirent shape**: entries stay `{ name, isDirectory(), isFile() }`; a symlink
48
+ is neither, so the distro listing uses `find -printf '%y\t%f'` (type char `l`)
49
+ rather than resolving links.
50
+ - **Errors**: an unreadable directory yields `[]`; an aborted caller propagates
51
+ the abort.
52
+ - **Coordinates**: the returned absolutes stay in the root's own coordinate
53
+ system (UNC for a distro workspace), because upstream joins children with the
54
+ host `node:path` — a POSIX path from the distro layer would be mangled.
55
+
56
+ ## The plugin side is ready
57
+
58
+ [`src/file-reference-wsl.ts`](../src/file-reference-wsl.ts) implements that
59
+ interface over the resident agent: one `find -printf` per directory, a
60
+ single-exec segment walk for `resolveDisplayDirectory` (instead of N sequential
61
+ `lstat`s), and the UNC/POSIX conversions. It is unit-tested against a fake
62
+ agent and deliberately **not** wired into `cordis.patch.yml`: the funnel
63
+ functions are module-internal, so wiring today would mean subclassing past them
64
+ or re-implementing the ranking engine — the regression path the RFC rules out.
65
+
66
+ ## Decision and trigger
67
+
68
+ **Fallback: keep the 9p walk.** `@` completion works today; it pays the
69
+ per-directory share cost above. There is no plugin-side optimization that is
70
+ safe without the seam, and the plugin does not replace the upstream module.
71
+
72
+ Wire `file-reference-wsl` when either happens:
73
+
74
+ - upstream exposes the traversal strategy (or an equivalent injectable funnel);
75
+ - the plugin's own composition gains a way to supply the strategy without
76
+ patching module internals.
77
+
78
+ At that point: wire the consumer, add an `@`-completion latency probe (the
79
+ traversal probe above is the baseline), and only then claim a speed-up. Until
80
+ then neither the README nor the roadmap promises one.
@@ -0,0 +1,90 @@
1
+ # Upstream seam: LSP navigation for a WSL workspace
2
+
3
+ Status: researched, **prototype deferred** (2026-10-19). No user-facing claim is
4
+ made: the desktop composition does not mount `ctx.lsp` in `0.2.1-rc.2`, so
5
+ `lsp` is not a tool the model can call in this plugin's profiles today. This
6
+ note records what the seam is, what a WSL-backed provider would require, and
7
+ the trigger that turns the prototype back on.
8
+
9
+ ## What exists upstream
10
+
11
+ The LSP capability is split across three packages in the harness tree
12
+ (`packages/lsp/`):
13
+
14
+ | Package | Role |
15
+ |---|---|
16
+ | `@deepseek-ai/dsh-lsp` | the `ctx.lsp` service and the provider registry; four read-only operations: `goToDefinition`, `findReferences`, `goToImplementation`, `hover` |
17
+ | `@deepseek-ai/dsh-lsp-stdio` | the generic stdio provider: a configured language-server command per workspace, transient document open, serialized queries |
18
+ | `@deepseek-ai/dsh-tool-lsp` | the model-facing `lsp` tool, which owns the one-based cursor convention |
19
+
20
+ It is an optional capability, not part of the agent-loop spine: a composition
21
+ must mount the seam, a provider, and the tool; none are in the base profile.
22
+
23
+ **Checked against `dsh-v0.2.1-rc.2`:** the desktop's
24
+ `@deepseek-ai/dsh-base/cordis.patch.yml` (the app's bundled base layer) has no
25
+ `lsp` row, and `packages/bundle/*` reference neither `dsh-lsp` nor
26
+ `dsh-tool-lsp`. The packages exist and are test-covered upstream, but a
27
+ stock desktop session has no LSP surface.
28
+
29
+ ## The pairing the provider assumes
30
+
31
+ `dsh-lsp-stdio` declares `inject = ['fs', 'lsp', 'subprocess']`: source files
32
+ are read through `ctx.fs`, and the language server is spawned through
33
+ `ctx.subprocess`. Both must describe **the same execution world**:
34
+
35
+ - the server's executable is resolved with `ctx.subprocess.resolveExecutable`
36
+ and spawned with `ctx.subprocess.spawn`;
37
+ - the queried source is read through `ctx.fs`, which also produces the
38
+ `fileUrl` sent as the transient document's URI;
39
+ - `workspaceRoot` is caller-supplied and canonicalized by the provider.
40
+
41
+ In a WSL session, `ctx.fs` (the preset's isolated `wsl-fs`) and `ctx.subprocess`
42
+ currently describe **different worlds**: the filesystem is the distro's, the
43
+ subprocess seam is the host's. The plugin's preset isolates `shell` and `fs`
44
+ only; the app-level `subprocess-wsl` row exists for the GUI terminal and is not
45
+ preset-scoped. Mounting `dsh-lsp-stdio` in the WSL preset today would therefore
46
+ launch a Windows language server against distro source and send it URIs it
47
+ cannot open.
48
+
49
+ ## What a WSL-backed provider needs
50
+
51
+ 1. **A preset-scoped `subprocess` implementation.** `resolveExecutable` and
52
+ `spawn` must run inside the distro (argv, cwd, env in Linux coordinates),
53
+ with the same lifetime rules as the shell executor's commands. This is the
54
+ largest piece and has no upstream seam today; the terminal's
55
+ `subprocess-wsl` is a different, app-level consumer.
56
+ 2. **URI and path translation.** The server's world is `file:///home/...`;
57
+ the harness workspace identity is the UNC target
58
+ (`\\wsl.localhost\<distro>\...`). The provider/tool boundary must translate
59
+ both directions (request source, `workspaceFolders`, returned locations),
60
+ or the seam must gain a path/URI adapter instead of providers doing it
61
+ ad hoc.
62
+ 3. **Confinement and lifecycle.** A language server is a long-lived child of
63
+ the session: it should run under the same sandbox policy as commands
64
+ (`bwrap`), die with the session, and be subject to the same idle reclaim as
65
+ the resident agent. "Same execution world" also implies the server sees the
66
+ workspace at the same `/home/...` path the commands see.
67
+ 4. **Configuration per distro.** The `servers` record (id → command, extension →
68
+ language id) and executable resolution belong with the session's distro, not
69
+ the host profile: the same `typescript-language-server` may exist in one
70
+ distro and not another.
71
+
72
+ ## Prototype decision
73
+
74
+ **Deferred, with a trigger.** The prototype cost is dominated by item 1 (a
75
+ preset-scoped subprocess provider) and the tool surface does not exist in the
76
+ shipped composition, so building it now would produce an unreachable code path.
77
+ Revisit when any of these happens:
78
+
79
+ - the desktop/base composition mounts `dsh-lsp` + `dsh-lsp-stdio` +
80
+ `dsh-tool-lsp` (the tool becomes reachable);
81
+ - upstream proposes a preset-scoped subprocess provider or an fs/URI adapter
82
+ for providers;
83
+ - a user asks for semantic navigation in a WSL workspace and accepts the
84
+ dependency on one of the above.
85
+
86
+ Then, in order: a fake stdio language server running **inside the distro**
87
+ through the new subprocess provider (unit-level), a real
88
+ `typescript-language-server` probe on a Tier 1 image, and only then a docs
89
+ claim. Until that proof exists, the plugin's README and ROADMAP say nothing
90
+ about LSP support.
@@ -0,0 +1,95 @@
1
+ # Upstream proposal: a session-visible notice seam for plugin advisories
2
+
3
+ Status: proposed upstream; **not filed yet** — this environment has no issue
4
+ channel, so the text below is ready for the maintainers to file against
5
+ `deepseek-ai/deepseek-harness` (version line `0.2.1-rc.x`; the gap was checked
6
+ against `dsh-v0.2.1-rc.2` on 2026-10-19). The exact filing payload (title, body,
7
+ command) is [proposal-session-notice.md](upstream/proposal-session-notice.md). The plugin ships the workaround
8
+ described at the end: the advisory goes to the log **and** into the session's
9
+ runtime context, which is model-visible and appears in the session transcript,
10
+ but is not a dedicated banner.
11
+
12
+ ## The gap
13
+
14
+ A plugin that discovers an environment problem at session start has no way to
15
+ tell the person looking at the session. The available channels are:
16
+
17
+ - `ctx.logger.warn(...)` — the host log. A user who never opens the log never
18
+ sees it; `docs/LIMITATIONS.md` records this boundary.
19
+ - a runtime-context entry (`ctx.systemPrompt.context(...)`) — model-visible,
20
+ reaches the transcript on the next assembly, but is not a notification and
21
+ has no dismissal or severity semantics.
22
+ - `agent/error` — reserved for failures; an advisory that must never fail a
23
+ session cannot use it.
24
+
25
+ There is no `ctx.notices`/`ctx.ui`/toast service in `0.2.1-rc.2`, and no
26
+ client slot for one. The plugin's `wsl-preflight` therefore warns into the log
27
+ and the runtime context, and the full check is `pnpm run doctor`
28
+ ([`scripts/doctor.mjs`](../scripts/doctor.mjs)).
29
+
30
+ ## What an advisory needs
31
+
32
+ `wsl-preflight` runs when a distro session opens. Its findings are advisory:
33
+ a missing `bubblewrap`, a distro without systemd, a missing `systemd-run`, or a
34
+ user manager without Linger. None of them may fail the session — the first
35
+ command fails closed on its own if it must — and none may install anything.
36
+ What the author needs is exactly what the rule says:
37
+
38
+ - **visible in the session** before the first failed command, not only in the
39
+ log;
40
+ - **associated with a session**, so the notice appears in the session that
41
+ triggered the probe (two sessions on two distros can see different notices);
42
+ - **severity and a remedy**, so the UI can render a warning with a
43
+ copy-pasteable fix rather than a bare string;
44
+ - **disposable**, since a probe re-runs and a fixed environment must not keep
45
+ the old warning forever;
46
+ - **non-fatal**: no turn is cancelled, no tool result is changed.
47
+
48
+ ## Proposed shape
49
+
50
+ A small service plus a client slot, mirroring the approval service's
51
+ session-scoped contract:
52
+
53
+ ```ts
54
+ /** One session-scoped advisory. */
55
+ interface SessionNotice {
56
+ /** Stable producer-owned id; re-notifying replaces by id. */
57
+ readonly id: string
58
+ /** `info` or `warning`; errors stay on `agent/error`. */
59
+ readonly level: 'info' | 'warning'
60
+ /** One-line text and an optional copy-paste remedy. */
61
+ readonly text: string
62
+ readonly remedy?: string
63
+ }
64
+
65
+ interface NoticeService {
66
+ /**
67
+ * Show or replace one notice for a session. Rejects when the session is
68
+ * not open; the producer owns the id and can dismiss it with `clear`.
69
+ */
70
+ notify(session: Session, notice: SessionNotice): Promise<void>
71
+ /** Remove a notice the producer published earlier. */
72
+ clear(session: Session, id: string): Promise<void>
73
+ }
74
+ ```
75
+
76
+ The client renders notices in the session header or above the composer with a
77
+ dismiss control; the host keeps the last value per `(session, id)` so a
78
+ reconnect and a replay agree. Nothing is model-visible unless the producer
79
+ also adds a runtime-context entry, which keeps this seam purely presentational.
80
+
81
+ ## What this plugin would do with it
82
+
83
+ `wsl-preflight` would publish one `SessionNotice` per verdict (`id:
84
+ "wsl-preflight"`, `level: "warning"`, `text` = the one-line finding, `remedy` =
85
+ the existing copy-paste command or `/etc/wsl.conf` change), replace it on the
86
+ next verdict, and `clear` it when the probe passes. The runtime-context entry
87
+ would stay as the model-facing half; the notice would be the human-facing one.
88
+
89
+ ## Workaround until it lands
90
+
91
+ - `ctx.logger.warn` carries the full remedy.
92
+ - A session-scoped runtime-context entry (`wsl-preflight`) repeats it in the
93
+ transcript, so it is visible to the model and in the context snapshot.
94
+ - `pnpm run doctor` is the deterministic, human-facing check; its `--json`
95
+ output is the issue-template ask.
@@ -10,6 +10,13 @@
10
10
  *
11
11
  * @module dsh-plugin-wsl/agent-errors
12
12
  */
13
+ /**
14
+ * The abort reason the execution layer uses when its timer fires. The timeout
15
+ * is enforced in the distro, so the agent layer must not add the out-of-band
16
+ * USR1 on this path: that signal has no request id and would hit whoever runs
17
+ * next.
18
+ */
19
+ export declare const BASH_TIMEOUT_REASON = "BASH_TIMEOUT";
13
20
  export declare class AgentUnavailableError extends Error {
14
21
  /**
15
22
  * @param message - why the agent is out, for the log.
@@ -29,3 +36,15 @@ export declare class CwdError extends Error {
29
36
  */
30
37
  constructor(linuxPath: string);
31
38
  }
39
+ /**
40
+ * The agent could not spawn the request's process at all (reason `spawn`).
41
+ * The native agent reports a missing workdir this way, without naming it; the
42
+ * executor retries such a request on the one-shot path, which can tell a
43
+ * missing directory from a missing executable and report the path.
44
+ */
45
+ export declare class SpawnError extends Error {
46
+ /**
47
+ * @param message - the agent's spawn diagnostic.
48
+ */
49
+ constructor(message: string);
50
+ }
@@ -10,6 +10,13 @@
10
10
  *
11
11
  * @module dsh-plugin-wsl/agent-errors
12
12
  */
13
+ /**
14
+ * The abort reason the execution layer uses when its timer fires. The timeout
15
+ * is enforced in the distro, so the agent layer must not add the out-of-band
16
+ * USR1 on this path: that signal has no request id and would hit whoever runs
17
+ * next.
18
+ */
19
+ export const BASH_TIMEOUT_REASON = "BASH_TIMEOUT";
13
20
  export class AgentUnavailableError extends Error {
14
21
  /**
15
22
  * @param message - why the agent is out, for the log.
@@ -34,3 +41,18 @@ export class CwdError extends Error {
34
41
  this.linuxPath = linuxPath;
35
42
  }
36
43
  }
44
+ /**
45
+ * The agent could not spawn the request's process at all (reason `spawn`).
46
+ * The native agent reports a missing workdir this way, without naming it; the
47
+ * executor retries such a request on the one-shot path, which can tell a
48
+ * missing directory from a missing executable and report the path.
49
+ */
50
+ export class SpawnError extends Error {
51
+ /**
52
+ * @param message - the agent's spawn diagnostic.
53
+ */
54
+ constructor(message) {
55
+ super(message);
56
+ this.name = "SpawnError";
57
+ }
58
+ }
package/lib/agent-exec.js CHANGED
@@ -28,7 +28,7 @@
28
28
  *
29
29
  * @module dsh-plugin-wsl/agent-exec
30
30
  */
31
- import { CwdError } from "./agent-errors.js";
31
+ import { BASH_TIMEOUT_REASON, CwdError } from "./agent-errors.js";
32
32
  /**
33
33
  * A memory-backed output reader in the seam's shape.
34
34
  *
@@ -69,7 +69,7 @@ export function agentExecutionHandle({ agent, cwd, argv, timeoutMs = 0, maxOutpu
69
69
  if (timeoutMs > 0) {
70
70
  setTimeout(() => {
71
71
  timedOut = true;
72
- controller.abort(new Error("BASH_TIMEOUT"));
72
+ controller.abort(new Error(BASH_TIMEOUT_REASON));
73
73
  }, timeoutMs).unref?.();
74
74
  }
75
75
  const settle = agent.exec({ cwd, argv, timeoutMs, maxOutputBytes, signal: controller.signal }).then((result) => ({ ok: true, result }), (error) => {
package/lib/agent.d.ts CHANGED
@@ -10,11 +10,13 @@
10
10
  * command or a write that was in flight has UNKNOWN state — replaying it
11
11
  * would run non-idempotent work twice or report a create that actually
12
12
  * succeeded as a refusal — so it fails instead, and the caller re-reads
13
- * before retrying. A second failure marks the agent permanently dead and
14
- * every future call rejects with {@link AgentUnavailableError} — the
15
- * executor's cue to fall back to the one-shot `wsl.exe` path it shipped
16
- * with, so the worst case of the agent is "today's behaviour, plus one
17
- * failed round trip".
13
+ * before retrying. A failure that spends the rebuild leaves the agent
14
+ * `dead` with a bounded revival: the next call after a 2/4/8-second backoff
15
+ * gets a fresh transport and its own single rebuild; after three deaths the
16
+ * state is permanent and every call rejects with a restart remedy. While
17
+ * dead, calls reject with {@link AgentUnavailableError} — the executor's cue
18
+ * to fall back to the one-shot `wsl.exe` path it shipped with, so the worst
19
+ * case of the agent is "today's behaviour, plus one failed round trip".
18
20
  * 2. **The in-distro timeout is the real one.** The EXEC frame carries the
19
21
  * timeout in whole seconds and the agent SIGTERMs the child itself, so a
20
22
  * timed-out command dies where it lives instead of leaving an orphan
@@ -59,6 +61,8 @@ export interface ExecResult {
59
61
  export interface PendingRequest {
60
62
  /** The request id, as the agent echoes it. */
61
63
  id: string;
64
+ /** The request's numeric sequence; newer requests have a larger one. */
65
+ seq: number;
62
66
  /** Which metric bucket this request belongs to. */
63
67
  kind: "fs" | "exec";
64
68
  /** When the frame was written, for the queue measurement. */
@@ -87,6 +91,10 @@ export interface PendingRequest {
87
91
  cancelled: boolean;
88
92
  /** The caller was already settled; a late answer only cleans up. */
89
93
  settled: boolean;
94
+ /** Whether the agent has answered (RES or ERR); a late signal is then unsafe. */
95
+ answered: boolean;
96
+ /** The delayed cancel signal, so it can be dropped when the request settles. */
97
+ signalTimer: NodeJS.Timeout | null;
90
98
  }
91
99
  /** Per-kind request metrics. */
92
100
  export interface RequestMetrics {
@@ -107,6 +115,21 @@ export interface RequestMetrics {
107
115
  /** Rejected before tracking: queue full, already aborted, or send failed. */
108
116
  rejected: number;
109
117
  }
118
+ /** A read-only snapshot of the resident's lifecycle, for diagnostics. */
119
+ export interface AgentStatus {
120
+ /** The distro served. */
121
+ distro: string;
122
+ /** Lifecycle state. */
123
+ state: "idle" | "starting" | "ready" | "dead";
124
+ /** The HELLO pid, when one is known and addressable. */
125
+ pid: number | undefined;
126
+ /** The last failure, if any. */
127
+ lastError: string;
128
+ /** Deaths observed; the revival budget is spent after {@link MAX_REVIVALS}. */
129
+ deaths: number;
130
+ /** Requests currently in flight. */
131
+ pending: number;
132
+ }
110
133
  /** The agent's cumulative request metrics. */
111
134
  export interface AgentMetrics {
112
135
  /** FS frames. */
@@ -227,8 +250,16 @@ export declare class WslAgent {
227
250
  nextRequestId: number;
228
251
  /** The requests in flight, keyed by their current id. */
229
252
  pending: Map<string, PendingRequest>;
253
+ /** The newest request the agent has ACKed; a cancel older than this is stale. */
254
+ lastAckedSeq: number;
230
255
  /** Whether the one permitted rebuild has been spent. */
231
256
  rebuildUsed: boolean;
257
+ /** Deaths observed; each one past the first consumes the revival budget. */
258
+ deaths: number;
259
+ /** When the next revival may start, epoch ms; 0 when not waiting. */
260
+ deadUntil: number;
261
+ /** The first revival delay; tests shorten it. */
262
+ revivalBackoffMs: number;
232
263
  /** The idle-shutdown timer. */
233
264
  idleTimer: NodeJS.Timeout | null;
234
265
  /** The last failure, for `unavailableReason` and log messages. */
@@ -307,6 +338,8 @@ export declare class WslAgent {
307
338
  maxPending?: number;
308
339
  /** Optional async preparation selecting the entry before the first spawn. */
309
340
  prepare?: () => Promise<WslAgentPrepared | undefined>;
341
+ /** The first revival delay in ms; doubles per death. Tests shorten it. */
342
+ revivalBackoffMs?: number;
310
343
  });
311
344
  /**
312
345
  * Whether the agent is permanently out — the executor's fallback cue.
@@ -316,6 +349,22 @@ export declare class WslAgent {
316
349
  get unavailable(): boolean;
317
350
  /** Human-readable reason for the last transition to `dead`, for logging. */
318
351
  get unavailableReason(): string;
352
+ /**
353
+ * Enter the dead state and schedule the next bounded revival attempt.
354
+ *
355
+ * The delay doubles per death (2 s, 4 s, 8 s, … capped at 30 s). After
356
+ * {@link MAX_REVIVALS} deaths the state stays permanent and the refusal
357
+ * carries the restart remedy.
358
+ */
359
+ markDead(): void;
360
+ /**
361
+ * A read-only lifecycle snapshot for diagnostics. Includes the live pid, the
362
+ * last failure and the remaining revival budget; it never starts or stops
363
+ * anything.
364
+ *
365
+ * @returns the status snapshot.
366
+ */
367
+ status(): AgentStatus;
319
368
  /**
320
369
  * Start the process and wait for the handshake. Re-entrant: concurrent
321
370
  * callers share the one start.
@@ -526,6 +575,20 @@ export declare class WslAgent {
526
575
  * @param pending - the entry to clear.
527
576
  */
528
577
  private clearPending;
578
+ /**
579
+ * Deliver the out-of-band cancel signal after a short grace, and only while
580
+ * the request it was meant for is still the agent's current one.
581
+ *
582
+ * The signal has no request id: it interrupts whatever command the agent is
583
+ * blocked in. A timeout request is usually already being killed by the
584
+ * in-distro watcher, so an immediate signal can land after the agent moved
585
+ * on — the next request then dies with exit 138 (SIGUSR1) and looks
586
+ * `completed`. A newer ACK means the agent has answered the old request, so
587
+ * the signal is dropped; an answered request is dropped too.
588
+ *
589
+ * @param pending - the cancelled request.
590
+ */
591
+ private armCancelSignal;
529
592
  }
530
593
  /**
531
594
  * Rewrite the request id inside a stored frame so a resent request does not