better-dsh 0.3.2-e → 0.3.3-b

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.
@@ -13,10 +13,10 @@ Better Dash and Super Dash each provide the native `remote` tool and REPL route.
13
13
  | Register an Agent-local SSH identity (no dialing) | `remote {action:"add",target:"ssh:build",ssh:{host:"host",user:"user",key:"/origin/key"}}`; omit `target` for an auto id (`ssh-xxxx`) |
14
14
  | Register a BYO PTY from a spawn command | `remote {action:"add",spawn:"docker exec -it img bash",target:"pty:build"}`; then address it like any session |
15
15
  | Stateless command and stdin over an identity | `remote {action:"exec",target:"ssh:build",cmd:"cat",stdin:"hello"}` |
16
- | Stream a local script to fixed `bash -s` | `remote {action:"exec",target:"ssh:alias",scriptPath:"/absolute/script.sh"}` |
17
- | Create a persistent shell (tab semantics) | `remote {action:"exec",target:"ssh:alias:pty",cmd:"cd /tmp; export CHECK=ready"}`; append `:label` (`ssh:alias:pty:build`) to name it |
16
+ | Run a local script as one `bash -c` command (stdin = the script's own input) | `remote {action:"exec",target:"ssh:alias",scriptPath:"/absolute/script.sh",stdin:"payload\n"}` |
17
+ | Use a machine's persistent shell (default PTY) | `remote {action:"exec",target:"ssh:alias",cmd:"cd /tmp; export CHECK=ready"}` — consecutive execs reuse one shell (cwd/env persist) |
18
18
  | Reuse a persistent shell | `remote {action:"exec",target:"pty:<id\|label>",cmd:"…"}` |
19
- | Answer a prompt / feed a running command (password, OTP, token, choice, REPL line, `\u0003`/`\u0004`) | `remote {action:"input",target:"pty:<id\|label>",stdin:"the-answer\n"}` — optional `commandId` guards against stale prompts |
19
+ | Answer a prompt / feed a running command (password, OTP, token, choice, REPL line, `\u0003`/`\u0004`) | `remote {action:"exec",target:"pty:<id\|label>",stdin:"the-answer\n"}` (stdin-only raw write) — optional `commandId` guards against stale prompts |
20
20
  | Close a PTY session | `remote {action:"close",target:"pty:<id\|label>",force:true}` |
21
21
  | Native CLI copy | `remote_transfer {mode:"copy",src:"/absolute/source",dst:"ssh:alias:/absolute/destination",argv:["-p"]}` |
22
22
  | Native rsync | `remote_transfer {mode:"rsync",src:"/absolute/source/",dst:"ssh:alias:/existing/destination/",argv:["-a","--partial","--dry-run"]}` |
@@ -29,16 +29,16 @@ Interactive flow — see a prompt, answer it, read the response:
29
29
 
30
30
  1. Start the program: `exec` with `cmd` (foreground, `run_in_background`, or after a timeout handoff — identity is preserved either way). The running receipt carries the output so far plus `session` / `commandId` / `jobId`.
31
31
  2. Watch for the prompt in that output; use `job_output <jobId>` when you need more.
32
- 3. Send the answer: `{action:"input", target:"pty:<id|label>", stdin:"the-answer\n"}`. `stdin` passes through verbatim — include the trailing `\n` to submit a line; `\u0003` is Ctrl-C and `\u0004` is Ctrl-D as raw characters; a literal backslash-x-0-4 stays four characters; `""` writes zero bytes (not EOF). The optional `commandId` must equal the running command's id; a stale id refuses the write instead of feeding the next command.
32
+ 3. Send the answer: `{action:"exec", target:"pty:<id|label>", stdin:"the-answer\n"}` (stdin alone = raw write; the `input` action is gone). `stdin` passes through verbatim — include the trailing `\n` to submit a line; `\u0003` is Ctrl-C and `\u0004` is Ctrl-D as raw characters; a literal backslash-x-0-4 stays four characters; `""` writes zero bytes (not EOF). The optional `commandId` must equal the running command's id; a stale id refuses the write instead of feeding the next command.
33
33
  4. Read the response with `job_output` on the SAME job. The input receipt only confirms N bytes were written — it never means the password was accepted or the login succeeded; judge from the program's output and the job's exit.
34
34
 
35
35
  Honest limits of the input lane: Ctrl-D is not a universal EOF (on a line without a trailing newline it first delivers that text — send it again on the emptied line to end a reader like `cat`); Ctrl-C only interrupts where the terminal mode allows; `job_kill` remains the explicit cancel; `input` never creates, dials, reconnects or replays — address a live session as `pty:<id|label>`, any transport. A command waiting for input IS the busy state — a new `cmd` gets `[E_SESSION_BUSY]`; feed the running command instead of queueing a second one. On a oneshot, `stdin` is startup-only: known data may ride the initial `exec`, but a oneshot cannot be fed after it starts and its timeout still cancels the whole group — a prompt there means cancel and switch to PTY. Heredocs and input redirection remain valid bulk-input alternatives; there is no global stdin ban. Cancelling is reliable (0.3.2-d): `job_kill` writes its status marker after the terminal's ^C input flush, so even a cancel that follows a bare Ctrl-C settles `killed · exit 130` with the session alive — and hub-lane control bytes cross the local raw half as bytes, interpreted by the peer pty, so ^C/^D behave like every other transport.
36
36
 
37
37
  Use the stateless oneshot when a step is genuinely one-shot (a probe, a pipeline stage, a supervised build you will not revisit) or when the `runner` supervision guarantee matters. On dumb-pipe transports the supervisor is the pinned `superd-run` helper (or its POSIX wrapper) — receipts say which ran. On the Hub lane the peer IS our own server: supervision is native there (`bash -lc` in a detached group, TERM→2s→KILL, output rings, lease lifecycle) and receipts say `hub-lease`; the helper is a dumb-pipe artifact and is never uploaded over the Hub. Note that `exec` always executes something: a warm session is created by running a command (`true` is enough), never by omitting `cmd`.
38
38
 
39
- There is no `mode` field: append `:pty` to a transport target to create, address a live shell as `pty:<id|label>`. `add` only registers (BYO identity or BYO PTY) and never runs a command. `scriptPath` cannot combine with cmd/stdin/PTY and is `exec`-only. Direct local command execution is not provided; use native local shell tools. Background jobs require the official jobs service/controller and are read/killed with `job_output`/`job_kill`; on PTY lanes a background job's jobId is the commandId (a timeout handoff keeps them as separate ledger fields). SSH keys/config stay on the origin; BYO identities are not persisted across new/cold Agents. PTY backlog recovery and live probes are not part of this release.
39
+ A machine target defaults to its reusable PTY; `mode:"oneshot"` selects the isolated stateless lane. The old `:pty` creation suffix and the `input` action are gone — multiple addressable shells come from `handoff {target:<machine>}` (detaches the default as `pty:<id>`), and prompt input is `exec` with `stdin` alone. `add` only registers (BYO identity or BYO PTY) and never runs a command. `scriptPath` is an alternative to `cmd` and may carry `stdin` (startup input rides after the script bytes) in both lanes. Direct local command execution is not provided; use native local shell tools. Background jobs require the official jobs service/controller and are read/killed with `job_output`/`job_kill`; on PTY lanes a background job's jobId is the commandId (a timeout handoff keeps them distinct until adoption backfills the job). SSH keys/config stay on the origin; BYO identities are not persisted across new/cold Agents. PTY backlog recovery and live probes are not part of this release.
40
40
 
41
- PTY state is durable and readable: every dispatch returns exit, cwd and `envDelta` (variable names and change markers only — values never leave the endpoint; fetch them yourself if needed). `close` is idempotent (`closed:false, reason:"not-found"|"already-closed-or-reaped"`); a session with a running job refuses close unless `force:true`, which first interrupts the running command — its job settles in the killed family (`interrupted: exit 130`, or `session closed` when the frame cannot arrive) — and then kills the group. A busy session rejects new commands instead of queueing (`[E_SESSION_BUSY]` names the running id and the ways out: `input` more stdin, `job_output`, `job_kill`, or another session/oneshot); open another PTY or use a oneshot for parallel work. Idle PTYs are reaped after 15 minutes; an Agent holds at most 10 live sessions across connections, and dead sessions do not count. `hub:<m4>:pty[:label]` opens the same session shape over the Hub WebSocket lane — identity, labels, input, parallelism and background work exactly like ssh/docker/incus/BYO.
41
+ PTY state is durable and readable: every dispatch returns exit, cwd and `envDelta` (variable names and change markers only — values never leave the endpoint; fetch them yourself if needed). `close` is idempotent (`closed:false, reason:"not-found"|"already-closed-or-reaped"`); a session with a running job refuses close unless `force:true`, which first interrupts the running command — its job settles in the killed family (`interrupted: exit 130`, or `session closed` when the frame cannot arrive) — and then kills the group. A busy session rejects new commands instead of queueing (`[E_SESSION_BUSY]` names the running id and the ways out: exec stdin-only, `job_output`, `job_kill`, handoff, or another session/oneshot); open another PTY or use a oneshot for parallel work. Idle PTYs are reaped after 15 minutes; an Agent holds at most 30 live sessions across connections (defaults and handed-off shells included; dead sessions do not count; the limit is disclosed only when reached). `hub:<m4>` as a machine target carries the same default-PTY semantics over the Hub WebSocket lane — identity, labels, stdin-only writes, parallelism and background work exactly like ssh/docker/incus/BYO.
42
42
 
43
43
  Output returns IN FULL up to retention (0.3.2-e: the self-made 30K returned-tail window is removed — model-context sizing is the native agent runtime's job, e.g. the DSH spill policy or a foreign runtime's own guard). Native/PTY retains at most 4,000,000 UTF-16 units per stream; Hub oneshot 4 MiB bytes. Beyond retention the returned text is the retained tail, headed by a one-line disclosure banner, with `outputStats` giving total/retained/dropped/returned and `truncated` set to the original total. Read `outputStats` units and dropped/returned counters rather than treating character counts as bytes. Discarded output is not recoverable after restart.
44
44
 
@@ -87,9 +87,9 @@ async function isPathUnder(path$1, root, caseSensitive = process.platform !== "w
87
87
  *
88
88
  * Per-call policy: `read-only` denies every mutation; `workspace-write` allows
89
89
  * a mutation only when the target canonicalizes under the policy's workspace
90
- * root or a platform temp area from the shared `writableRoots` policy;
91
- * `danger-full-access` delegates unfenced. A denial throws the structured
92
- * `FS_SANDBOX_DENIED`.
90
+ * root, an explicit additional writable root, or a platform temp area from the
91
+ * shared `writableRoots` policy; `danger-full-access` delegates unfenced. A
92
+ * denial throws the structured `FS_SANDBOX_DENIED`.
93
93
  *
94
94
  * @module @deepseek-ai/dsh-fs-sandbox
95
95
  */
package/lib/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { t as Schema } from "./index2.js";
2
2
  import { Context, Service } from "@deepseek-ai/cordis";
3
3
  import { ToolDefinition, ToolExecutionInput, ToolRunContext, ToolRuntime } from "@deepseek-ai/dsh-tools";
4
+ import { ToolExecutionPipeline } from "@deepseek-ai/dsh-tools/pipeline";
4
5
  import { ContentBlock, HarnessError } from "@deepseek-ai/dsh-llm";
5
6
  import { Scoped } from "@deepseek-ai/dsh-scope";
6
7
  import { Agent } from "@deepseek-ai/dsh-agent";
@@ -622,10 +623,10 @@ declare module '@deepseek-ai/cordis' {
622
623
  }
623
624
  /**
624
625
  * Capabilities the `eval` bridge closes over, mirroring upstream's
625
- * `RunCodeBridgeOptions` (the `requireRuntime` idiom): the registry-private
626
- * staged scheduler travels through the exported `TOOL_RUNTIME_SCHEDULER`
627
- * symbol-keyed property rather than a closure, because — unlike upstream —
628
- * the registry does not mint this tool for us.
626
+ * `RunCodeBridgeOptions` (the `requireRuntime` idiom): sub-dispatch rides a
627
+ * plugin-built `ToolExecutionPipeline` (0.2.2) rather than the pre-0.2.2
628
+ * registry-private staged scheduler, because — unlike upstream's PTC
629
+ * executor — the registry does not mint this tool for us.
629
630
  */
630
631
  interface RunCellBridgeOptions {
631
632
  /** Resolves `ctx.replRuntime` or throws the loud misconfiguration error (use-time read). */
@@ -649,17 +650,20 @@ interface RunCellBridgeOptions {
649
650
  /**
650
651
  * Build the `eval` {@link ToolDefinition}: required `cell` and
651
652
  * `description` parameters, executed through the dispatch bridge described
652
- * in the module doc. Sub-calls ride the registry's exported staged scheduler
653
- * (`prepare`/`dispatch`/`finalize`/`finish`) under the native concurrency
654
- * contract; each sub-dispatch is logged for reconstruction
655
- * (`tool/ptc-dispatch-start` / `tool/ptc-dispatch`) while only the outer
656
- * curated result enters model history.
657
- * @param registry - the host tool registry (sub-calls go through its staged
658
- * scheduler, bindings cover its registered tools).
653
+ * in the module doc. Sub-calls ride the plugin-built staged pipeline
654
+ * (`prepare`/`dispatch`/`complete` — the 0.2.2 `ToolExecutionPipeline`, the
655
+ * sanctioned successor of the retired `TOOL_RUNTIME_SCHEDULER` symbol) under
656
+ * the native concurrency contract; each sub-dispatch is logged for
657
+ * reconstruction (`tool/ptc-dispatch-start` / `tool/ptc-dispatch`) while only
658
+ * the outer curated result enters model history.
659
+ * @param registry - the host tool registry (bindings cover its registered
660
+ * tools; schema projection reads it live).
661
+ * @param pipeline - the staged invocation pipeline built over the same
662
+ * registry's mounting context (sub-calls dispatch through it).
659
663
  * @param options - the bridge capabilities described above.
660
664
  * @returns the registry-ready definition.
661
665
  */
662
- declare function createRunCellTool(registry: ToolRuntime, options: RunCellBridgeOptions): ToolDefinition;
666
+ declare function createRunCellTool(registry: ToolRuntime, pipeline: ToolExecutionPipeline, options: RunCellBridgeOptions): ToolDefinition;
663
667
  declare function apply(ctx: Context, config: Config): void;
664
668
  declare const _default: {
665
669
  name: string;
package/lib/index.js CHANGED
@@ -2,7 +2,6 @@ import { t as Schema } from "./lib.js";
2
2
  import { a as RESERVED_ERROR_MEMBERS, i as RESERVED_BINDING_GLOBALS, n as DUNDER_MEMBER, o as ReplRuntime, r as PORTABLE_RESERVED_WORDS, t as isFlatBindableName } from "./py-sdk2.js";
3
3
  import { n as resolveKernelEnv } from "./kernel-env2.js";
4
4
  import { t as captureAllTools } from "./native-capture.js";
5
- import { createRequire } from "node:module";
6
5
  import { Context } from "@deepseek-ai/cordis";
7
6
  import { createHash, randomBytes, randomUUID } from "node:crypto";
8
7
  import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
@@ -11,7 +10,8 @@ import { spawn } from "node:child_process";
11
10
  import { createServer } from "node:http";
12
11
  import { createInterface } from "node:readline";
13
12
  import { fileURLToPath } from "node:url";
14
- import { TOOL_RUNTIME_SCHEDULER, defineTool } from "@deepseek-ai/dsh-tools";
13
+ import { defineTool } from "@deepseek-ai/dsh-tools";
14
+ import { ToolExecutionPipeline } from "@deepseek-ai/dsh-tools/pipeline";
15
15
  import * as dshLlm from "@deepseek-ai/dsh-llm";
16
16
  import { BlockAssembler, HarnessError, createUserMessage } from "@deepseek-ai/dsh-llm";
17
17
  import { scopeTarget } from "@deepseek-ai/dsh-scope";
@@ -2165,17 +2165,20 @@ function renderValue(value) {
2165
2165
  /**
2166
2166
  * Build the `eval` {@link ToolDefinition}: required `cell` and
2167
2167
  * `description` parameters, executed through the dispatch bridge described
2168
- * in the module doc. Sub-calls ride the registry's exported staged scheduler
2169
- * (`prepare`/`dispatch`/`finalize`/`finish`) under the native concurrency
2170
- * contract; each sub-dispatch is logged for reconstruction
2171
- * (`tool/ptc-dispatch-start` / `tool/ptc-dispatch`) while only the outer
2172
- * curated result enters model history.
2173
- * @param registry - the host tool registry (sub-calls go through its staged
2174
- * scheduler, bindings cover its registered tools).
2168
+ * in the module doc. Sub-calls ride the plugin-built staged pipeline
2169
+ * (`prepare`/`dispatch`/`complete` — the 0.2.2 `ToolExecutionPipeline`, the
2170
+ * sanctioned successor of the retired `TOOL_RUNTIME_SCHEDULER` symbol) under
2171
+ * the native concurrency contract; each sub-dispatch is logged for
2172
+ * reconstruction (`tool/ptc-dispatch-start` / `tool/ptc-dispatch`) while only
2173
+ * the outer curated result enters model history.
2174
+ * @param registry - the host tool registry (bindings cover its registered
2175
+ * tools; schema projection reads it live).
2176
+ * @param pipeline - the staged invocation pipeline built over the same
2177
+ * registry's mounting context (sub-calls dispatch through it).
2175
2178
  * @param options - the bridge capabilities described above.
2176
2179
  * @returns the registry-ready definition.
2177
2180
  */
2178
- function createRunCellTool(registry, options) {
2181
+ function createRunCellTool(registry, pipeline, options) {
2179
2182
  const { requireRuntime, maxParallel, shapeDispatchLog, warn } = options;
2180
2183
  return defineTool({
2181
2184
  name: EVAL_NAME,
@@ -2320,16 +2323,6 @@ function createRunCellTool(registry, options) {
2320
2323
  parent: exec.token,
2321
2324
  signal: runController.signal
2322
2325
  };
2323
- const scheduler = registry[TOOL_RUNTIME_SCHEDULER];
2324
- if (scheduler === void 0) {
2325
- let resolvedFrom;
2326
- try {
2327
- resolvedFrom = createRequire(import.meta.url).resolve("@deepseek-ai/dsh-tools");
2328
- } catch {
2329
- resolvedFrom = "(unresolvable from the plugin position)";
2330
- }
2331
- throw new Error(`${EVAL_NAME}: the tools service mounted by this composition lacks the TOOL_RUNTIME_SCHEDULER symbol as this plugin sees it — REPL sub-dispatch cannot run. Most likely a dual-copy dsh-tools: the symbol keying the mounted ToolRuntime instance is not the symbol this plugin imported (named methods like executionMode keep working across copies; only the symbol-keyed field goes missing). This plugin resolves @deepseek-ai/dsh-tools from: ${resolvedFrom}. Check the deployed plugin's node_modules for stray @deepseek-ai entries (only schemastery and cosmokit belong there)`);
2332
- }
2333
2326
  const outcome = await new Promise((resolve$1, reject) => {
2334
2327
  let parked;
2335
2328
  const settle = (result) => {
@@ -2375,7 +2368,7 @@ function createRunCellTool(registry, options) {
2375
2368
  pendingQueue.push({
2376
2369
  flight: Promise.resolve(),
2377
2370
  settled: false,
2378
- classify: () => registry.executionMode(input).kind,
2371
+ classify: () => pipeline.executionMode(input).kind,
2379
2372
  abandon: () => {
2380
2373
  reject(/* @__PURE__ */ new Error(`${EVAL_NAME} run is over (${String(runController.signal.reason)}); ${name$1} tool call abandoned`));
2381
2374
  },
@@ -2393,9 +2386,9 @@ function createRunCellTool(registry, options) {
2393
2386
  arguments: normalized.logged
2394
2387
  });
2395
2388
  try {
2396
- const prepared = await scheduler.prepare(input);
2389
+ const prepared = await pipeline.prepare(input);
2397
2390
  if (prepared.kind === "dispatch") {
2398
- this.flight = scheduler.dispatch(prepared.exec).then((dispatchOutcome) => {
2391
+ this.flight = pipeline.dispatch(prepared.exec).then((dispatchOutcome) => {
2399
2392
  parked = {
2400
2393
  kind: dispatchOutcome.kind,
2401
2394
  exec: prepared.exec,
@@ -2422,7 +2415,7 @@ function createRunCellTool(registry, options) {
2422
2415
  async commit() {
2423
2416
  if (parked === void 0) return;
2424
2417
  try {
2425
- const result = parked.kind === "post-result" ? await scheduler.finalize(parked.exec, parked.result) : scheduler.finish(parked.exec, parked.result);
2418
+ const result = await pipeline.complete(parked.exec, parked);
2426
2419
  for (const context of result.additionalContexts ?? []) exec.deferContext(context);
2427
2420
  if (result.concludesTurn) exec.concludeTurn();
2428
2421
  settle(result);
@@ -2552,7 +2545,8 @@ function apply(ctx, config) {
2552
2545
  }
2553
2546
  return runtimeCtx.get("workflowEngine");
2554
2547
  };
2555
- runtimeCtx.tools.register(createRunCellTool(registry, {
2548
+ const pipeline = new ToolExecutionPipeline(runtimeCtx);
2549
+ runtimeCtx.tools.register(createRunCellTool(registry, pipeline, {
2556
2550
  requireRuntime,
2557
2551
  maxParallel,
2558
2552
  shapeDispatchLog,
@@ -148,9 +148,13 @@ interface ProcessOptions {
148
148
  onOutput?: () => void;
149
149
  /** Overlay on process.env (tool-built values only, e.g. RSYNC_RSH). */
150
150
  env?: NodeJS.ProcessEnv;
151
+ /** Task E(SSH MUX):取消(timeout 或显式 cancel)即同步回调,不等 close。mux master 会经
152
+ * fd-passing 持有被杀客户端的会话 fd 直到远端会话结束——若远端清理 killer 等 close 后才发射,
153
+ * 就成了「close 等远端结束、远端结束等 killer」死锁。此处先行启动 killer 才能解开 close。至多一次。 */
154
+ onCancel?: () => void;
151
155
  }
152
156
  /** Real detached process. Completion includes group cleanup, even after its leader closes. */
153
- declare function startBinaryProcess(argv: string[], options?: Pick<ProcessOptions, 'cwd' | 'timeoutSec' | 'env'>): {
157
+ declare function startBinaryProcess(argv: string[], options?: Pick<ProcessOptions, 'cwd' | 'timeoutSec' | 'env' | 'onCancel'>): {
154
158
  stdout: Readable;
155
159
  stderr: Readable;
156
160
  stdin: Writable;
@@ -257,15 +261,6 @@ interface PtyDispatchResult {
257
261
  running?: boolean;
258
262
  job?: PtyRunningJob;
259
263
  }
260
- /** 账本条目(§5):commandId / 摘要 / exit / 耗时;保留最近 20 条(user 定案)。 */
261
- interface LedgerEntry {
262
- commandId: string;
263
- jobId?: string;
264
- summary: string;
265
- exit: number | null;
266
- durationMs: number;
267
- running: boolean;
268
- }
269
264
  type PtyState = 'cold' | 'starting' | 'ready' | 'dead' | 'disposed';
270
265
  /** 会话运行时快照(status 面消费,Ruling 16 词汇源)。 */
271
266
  interface SessionSnapshot {
@@ -284,12 +279,11 @@ declare class PtySession {
284
279
  private deathWaiters;
285
280
  private diag;
286
281
  private inFlight;
282
+ private cmdSeq;
287
283
  /** P4/P5:当前占用前台的 detached job(background/超时 handoff)。非 null 时新命令拒绝(Q6)。 */
288
284
  private detachedJob;
289
285
  /** §3.2(2026-10-25):当前占用会话的命令——dispatch 同步占位,settle/cleanup 释放;input 的唯一写入对象。 */
290
286
  private activeCommand;
291
- /** 账本(§5):本会话最近 20 条命令(FIFO,超出丢最旧)。 */
292
- private ledger;
293
287
  /** env 侧信道基线(init E 帧);undefined = 侧信道不可用(不报假 delta)。 */
294
288
  private envBaseline;
295
289
  /** idle 起点(ready / settle 时重置);null = 尚无 idle 时钟(cold 或已 dispose)。 */
@@ -310,7 +304,12 @@ declare class PtySession {
310
304
  get lastCwd(): string | null;
311
305
  /** 占用前台的 detached job 的 commandId(无 = null;close 拒绝与 busy 文案共用)。 */
312
306
  get detachedCommandId(): string | null;
313
- /** job 通道收养后回填 jobId(timeout handoff 时 registry id ≠ commandId,账本如实记录两者);运行中的命令同步携带(input 回执用)。 */
307
+ /** handoff/card 面(2026-11-01 默认 PTY 轮):当前占用会话的命令与可选 job 句柄;无 = null。 */
308
+ get active(): {
309
+ commandId: string;
310
+ jobId?: string;
311
+ } | null;
312
+ /** job 通道收养后回填 jobId(timeout handoff 时 registry id ≠ commandId);运行中的命令同步携带(stdin-only 写入回执用)。 */
314
313
  attachJob(commandId: string, jobId: string): void;
315
314
  /** Workbench 身份(§3):id + 可选 label + connection + byo 标记。 */
316
315
  get identity(): {
@@ -320,8 +319,6 @@ declare class PtySession {
320
319
  byo: boolean;
321
320
  builtOn: boolean;
322
321
  };
323
- /** 账本快照(session card 面,只读)。 */
324
- ledgerSnapshot(): readonly LedgerEntry[];
325
322
  /** spawn 创建原文(仅 BYO 会话;v2 起仅存档,不参与复用判定)。 */
326
323
  get origin(): string | undefined;
327
324
  /** status 面快照:state + busy(有命令在跑)+ idleMs(上次 settle 至今)+ pid(进程组头,kill 兜底可观测)。 */
@@ -520,6 +517,41 @@ interface RsyncReceipt extends RunResult {
520
517
  */
521
518
  declare function runRsync(request: RsyncRequest, source: RsyncEndpoint, destination: RsyncEndpoint, signal: AbortSignal, options?: RelayRsyncOptions): Promise<RsyncReceipt>;
522
519
  //#endregion
520
+ //#region src/remote/ssh-mux.d.ts
521
+ interface SshMuxIdentity {
522
+ /** 原始 target 串(如 `ssh:dev4` / `dev4`)——同一 effective 身份不同拼法也不串 socket。 */
523
+ target: string;
524
+ connection: SshConnection;
525
+ }
526
+ interface SshMuxOptions {
527
+ persistSec?: number;
528
+ /** socket 目录根(默认 os.tmpdir());测试注入口。 */
529
+ rootDir?: string;
530
+ runner?: (argv: string[], opts?: {
531
+ timeoutSec?: number;
532
+ }) => Promise<{
533
+ exit: number | null;
534
+ }>;
535
+ }
536
+ /** 每 Agent 一个(driver 私有):tool-owned 0700 socket 目录 + 按连接身份的 socket 名。
537
+ * 目录名含随机 owner 后缀,跨 Agent 天然不共享;dispose 逐 socket `-O exit` 后整目录删除。 */
538
+ declare class SshMux {
539
+ private readonly persistSec;
540
+ private readonly rootDir;
541
+ private readonly runner;
542
+ private readonly owner;
543
+ private readonly sockets;
544
+ private dir;
545
+ private disposal;
546
+ constructor(options?: SshMuxOptions);
547
+ /** 注入 SSH argv 的 MUX 选项(trusted internal argv,非模型输入)。目录懒建(纯 docker/hub
548
+ * Agent 永不触盘);超限/已 dispose 返回 [] = 诚实退化为普通连接。 */
549
+ argvFor(identity: SshMuxIdentity): string[];
550
+ /** Teardown:先 `-O exit` 逐 socket 礼貌拆除 master(不拨号、5s 上限、错误全吞——
551
+ * master 已死/残 socket 属正常),再删目录。单飞(重复调用同一 Promise)。 */
552
+ dispose(): Promise<void>;
553
+ }
554
+ //#endregion
523
555
  //#region src/remote/oneshot.d.ts
524
556
  interface OneShotOptions {
525
557
  stdin?: string;
@@ -539,6 +571,8 @@ interface ProbeOptions {
539
571
  connectTimeoutSec?: number;
540
572
  hardTimeoutSec?: number;
541
573
  runner?: (argv: string[], opts?: OneShotOptions) => Promise<OneShotResult>;
574
+ /** Task E:SSH 探测与 oneshot 执行链共用 MUX(同一身份 → 同一 master,探测零额外握手)。 */
575
+ muxArgvFor?: (plan: TargetPlan) => readonly string[];
542
576
  }
543
577
  //#endregion
544
578
  //#region src/remote/roster.d.ts
@@ -603,6 +637,7 @@ interface RemoteDriverOptions {
603
637
  onAudit?: (r: RemoteAuditRecord) => void;
604
638
  oneshotArgvFor?: (plan: TargetPlan, cmd: string, options: {
605
639
  cleanupTag?: string;
640
+ muxArgv?: readonly string[];
606
641
  }) => string[];
607
642
  ptyArgvFor?: (plan: TargetPlan) => string[];
608
643
  spawnArgvFor?: (spawnCommand: string) => string[];
@@ -611,6 +646,8 @@ interface RemoteDriverOptions {
611
646
  probeRunner?: ProbeOptions['runner'];
612
647
  /** 测试/验收缝:helper 资产来源(默认 lib 旁挂 helper-bin);显式 null 强制 shim 路径。 */
613
648
  helperAssetsFor?: () => HelperAssets | null;
649
+ /** 测试/验收缝:SSH oneshot MUX 状态(默认每 driver 一个 SshMux = 每 Agent 隔离)。 */
650
+ sshMux?: SshMux;
614
651
  /** 测试缝:roster 扫描执行器(裸名解析复用)。 */
615
652
  rosterRunner?: RosterOptions['runner'];
616
653
  /** 测试缝:ssh config 读取(裸名解析复用)。 */
@@ -626,6 +663,10 @@ declare function hubPtyAssets(): {
626
663
  declare class RemoteDriver {
627
664
  private readonly opts;
628
665
  private readonly pool;
666
+ /** 默认 PTY 槽(spec remote-default-pty §1):sessionKey → 归一 machineKey → pool key。
667
+ * 会话本体是普通 PtySession(id 创建即认领,roster 全可见);槽登记在 dispose/TTL 后悬空,
668
+ * 取槽时经 pool.get(disposed 视同出池)惰性清理。 */
669
+ private readonly defaultSlots;
629
670
  private readonly labels;
630
671
  private readonly hubOwner;
631
672
  private readonly hubProviders;
@@ -638,6 +679,7 @@ declare class RemoteDriver {
638
679
  private readonly runnerOf;
639
680
  private disposal;
640
681
  private readonly ptyIdleTtlSec;
682
+ private readonly mux;
641
683
  constructor(opts?: RemoteDriverOptions);
642
684
  call(params: RemoteCallParams, callCtx?: {
643
685
  sessionKey?: string;
@@ -676,6 +718,10 @@ declare class RemoteDriver {
676
718
  private planOf;
677
719
  /** ssh:<name> 的池感知解析:BYO 身份 → 其 connection;否则按 config 别名/主机名。 */
678
720
  private sshPlanFor;
721
+ /** Task E MUX 身份:仅 SSH plan 注入(docker/incus/hub 返回 undefined)。身份键 = 原始
722
+ * target 串 + 有效连接字段(host/user/port/key/ProxyJump)——同身份共享 master,跨身份
723
+ * 不串 socket;目录/寿命/拆除全部由 SshMux 自持。 */
724
+ private muxArgvFor;
679
725
  /** BYO ssh 注册(v2:无 spec 对比——label 占用即拒;注册成功才认领)。返回实际名(label 或 s-id)。 */
680
726
  private registerByoSsh;
681
727
  /** PTY 会话创建(native/spawn 同一形状):上限 → label 认领 → 惰性会话(失败由 onDead 释放 label)。 */
@@ -683,8 +729,36 @@ declare class RemoteDriver {
683
729
  private ttlMinutes;
684
730
  /** connection 由 BYO ssh 身份建立 → card 面标 `(built on)`(与 roster 同词,R2 复核 P2-10)。 */
685
731
  private isBuiltOn;
686
- /** PTY 创建形唯一实现(execute 与 background 预建共用):spawn BYO / ssh BYO / native `<transport>:pty`。 */
732
+ /** PTY 创建形唯一实现(execute 与 background 预建共用):spawn BYO(spec 2026-11-01:native `:pty`/hub-pty 创建语法已删,machine 会话经默认槽 + handoff)。 */
687
733
  private createPtyLane;
734
+ /** 池条目的槽键归一:同一实体(host/container/machine 身份)跨裸名/前缀/撞名前缀共享同一默认槽。 */
735
+ private machineKeyOfEntry;
736
+ /** 默认槽的 machine 规格:归一 key + 创建 argv + 展示 connection(hub 每次经 ptyEndpoint 重验 pairing,P10)。 */
737
+ private machineSlotSpec;
738
+ /** 取/懒建 machine 的默认 PTY 槽:dead/disposed 槽惰性重建;id 创建即认领(roster 全可见 + 防撞)。 */
739
+ private defaultSlot;
740
+ private releaseDefaultSlot;
741
+ /** 按池键反查并解绑默认槽(background 提交即 handoff 用);非槽会话无操作。 */
742
+ private releaseSlotByPoolKey;
743
+ /** roster/status 面:本 session 的默认槽标注(session id → machineKey;悬空条目过滤)。 */
744
+ private defaultMarks;
745
+ /** spec §1 handoff(2026-11-01 二/三轮裁决):把 machine 默认 PTY 解绑成可寻址 pty:<id>——同引用移动
746
+ * (不重启、不复制输出流);fresh(尚无默认槽)= 后台创建空闲 shell 再隔离(默认 PTY 思维模型恒
747
+ * ready,创建时机是实现细节);busy = 附当前命令与 job 句柄。machine 下次 exec 按需新建默认 PTY。 */
748
+ handoff(target: string, callCtx?: {
749
+ sessionKey?: string;
750
+ }): Promise<RemoteCallResult>;
751
+ /** close 统一入口(spec §1 + 三轮补充裁决):machine target 只关默认槽;pty selector 关该会话;
752
+ * 不存在 job 定位——取消运行中命令的唯一入口是官方 job_kill。 */
753
+ close(target: string, sessionKey: string, force?: boolean): Promise<{
754
+ session: string;
755
+ connection: string;
756
+ closed: boolean;
757
+ reason?: string;
758
+ killed: string[];
759
+ message?: string;
760
+ }>;
761
+ private closeDefault;
688
762
  /** background PTY lane:会话先占位(不拨号)再进官方 jobs registry——首回执必须能寻址;
689
763
  * registry 拒绝时把新建的会话拆掉(复用形的会话绝不拆)。返回 undefined = 走普通 oneshot job。 */
690
764
  preparePtyLane(params: RemoteCallParams, callCtx?: {
@@ -709,10 +783,9 @@ declare class RemoteDriver {
709
783
  };
710
784
  /** job 通道收养回填(timeout handoff 时 registry id ≠ commandId,账本记录两者)。 */
711
785
  attachPtyJob(commandId: string, jobId: string, sessionKey: string): void;
712
- /** §3.1(2026-10-25)interactive-stdin:input 动作——向已有 live PTY 的当前运行命令
713
- * 追加 stdin。只解析池内已存在的会话(pty:<id|label> / 裸 label 命中 pty 项),
714
- * 不触发发现、拨号、创建或重连;machine target 与创建形(:pty 后缀,含 hub:<m4>:pty)
715
- * 一律明确拒绝并给出可恢复指引。回执附 connection/label 供工具层渲染。 */
786
+ /** spec remote-default-pty §1(2026-11-01):exec stdin-only 写入核心——向已有 live PTY 的当前运行命令
787
+ * 追加 exact-byte stdin。寻址:pty:<id|label> / 裸 label 命中 pty 项 / machine target(其默认槽的
788
+ * 当前命令,无槽不懒建、如实报错)。不触发拨号、创建或重连;回执附 connection/label 供工具层渲染。 */
716
789
  input(target: string, options: PtyInputOptions, callCtx?: {
717
790
  sessionKey?: string;
718
791
  }): Promise<PtyInputReceipt & {