super-dsh 0.3.2 → 0.3.3-c

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 (61) hide show
  1. package/README.md +15 -14
  2. package/agent-agy/dist/agent.d.ts +4 -1
  3. package/agent-agy/dist/agent.js +15 -1
  4. package/agent-agy/dist/index.js +5 -8
  5. package/agent-agy/package.json +6 -6
  6. package/agent-agy-sdk/dist/agent.d.ts +4 -1
  7. package/agent-agy-sdk/dist/agent.js +15 -1
  8. package/agent-agy-sdk/dist/index.js +5 -8
  9. package/agent-agy-sdk/package.json +5 -5
  10. package/agent-claude/dist/agent.js +15 -1
  11. package/agent-claude/package.json +8 -8
  12. package/agent-codex/dist/agent.js +15 -1
  13. package/agent-codex/package.json +7 -7
  14. package/agent-codex-sdk/dist/agent.js +15 -1
  15. package/agent-codex-sdk/dist/index.js +5 -11
  16. package/agent-codex-sdk/package.json +7 -7
  17. package/agent-hermes/dist/agent.js +15 -1
  18. package/agent-hermes/dist/index.js +5 -8
  19. package/agent-hermes/package.json +7 -7
  20. package/agent-omp/dist/agent.js +26 -2
  21. package/agent-omp/package.json +8 -8
  22. package/agent-omp-sdk/dist/agent.d.ts +4 -1
  23. package/agent-omp-sdk/dist/agent.js +15 -1
  24. package/agent-omp-sdk/package.json +8 -8
  25. package/agent-pi/dist/agent.js +20 -2
  26. package/agent-pi/dist/index.js +5 -11
  27. package/agent-pi/package.json +7 -7
  28. package/client/index.js +106 -249
  29. package/cordis.patch.yml +0 -6
  30. package/package.json +22 -27
  31. package/remote/hub-pty-pump.mjs +97 -0
  32. package/remote/plugin.d.ts +476 -200
  33. package/remote/plugin.js +1448 -667
  34. package/remote/provenance.json +22 -17
  35. package/superd-hub/dist/agent-model-face.d.ts +93 -0
  36. package/superd-hub/dist/agent-model-face.js +113 -0
  37. package/superd-hub/dist/exec.d.ts +3 -2
  38. package/superd-hub/dist/exec.js +6 -5
  39. package/superd-hub/dist/fusion-transport.js +0 -1
  40. package/superd-hub/dist/hub-tool.d.ts +1 -1
  41. package/superd-hub/dist/hub-tool.js +6 -6
  42. package/superd-hub/dist/index.d.ts +1 -0
  43. package/superd-hub/dist/index.js +1 -0
  44. package/superd-hub/dist/remote-provider.js +70 -36
  45. package/superd-hub/dist/world-entry.js +30 -0
  46. package/superd-hub/lib/client/index.js +106 -249
  47. package/superd-hub/package.json +14 -10
  48. package/superd-hub/dist/bundle-setup.d.ts +0 -27
  49. package/superd-hub/dist/bundle-setup.js +0 -94
  50. package/superd-hub/dist/client/companion-api.d.ts +0 -5
  51. package/superd-hub/dist/client/companion-api.js +0 -30
  52. package/superd-hub/dist/companion-contract.d.ts +0 -9
  53. package/superd-hub/dist/companion-contract.js +0 -1
  54. package/superd-hub/dist/companions.d.ts +0 -30
  55. package/superd-hub/dist/companions.js +0 -211
  56. package/superd-hub/dist/exec-pty-channel.d.ts +0 -58
  57. package/superd-hub/dist/exec-pty-channel.js +0 -162
  58. package/superd-hub/dist/exec-pty-framing.d.ts +0 -34
  59. package/superd-hub/dist/exec-pty-framing.js +0 -89
  60. package/superd-hub/dist/exec-pty-session.d.ts +0 -78
  61. package/superd-hub/dist/exec-pty-session.js +0 -228
package/remote/plugin.js CHANGED
@@ -1,6 +1,7 @@
1
+ import "node:module";
1
2
  import { Context } from "@deepseek-ai/cordis";
2
3
  import { chmod, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
3
- import { constants, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
4
+ import { constants, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
4
5
  import { pipeline } from "node:stream/promises";
5
6
  import { dirname, isAbsolute, join, posix, resolve } from "node:path";
6
7
  import { Readable } from "node:stream";
@@ -9,6 +10,7 @@ import { homedir, tmpdir } from "node:os";
9
10
  import { spawn } from "node:child_process";
10
11
  import { createConnection, createServer } from "node:net";
11
12
  import { fileURLToPath } from "node:url";
13
+ import { Buffer as Buffer$1 } from "node:buffer";
12
14
  import { defineTool } from "@deepseek-ai/dsh-tools";
13
15
 
14
16
  //#region third_party/schemastery/node_modules/@deepseek-ai/cosmokit/lib/index.js
@@ -1464,6 +1466,15 @@ function startBinaryProcess(argv, options = {}) {
1464
1466
  let diagnostic;
1465
1467
  let timedOut = false, finished = false, cancelled = false;
1466
1468
  let cleanup;
1469
+ let cancelledNotified = false;
1470
+ const notifyCancel = () => {
1471
+ if (!cancelledNotified) {
1472
+ cancelledNotified = true;
1473
+ try {
1474
+ options.onCancel?.();
1475
+ } catch {}
1476
+ }
1477
+ };
1467
1478
  let timer;
1468
1479
  const signal = (sig) => {
1469
1480
  if (child.pid === void 0) return false;
@@ -1503,6 +1514,7 @@ function startBinaryProcess(argv, options = {}) {
1503
1514
  const cancel = () => {
1504
1515
  if (!finished) {
1505
1516
  cancelled = true;
1517
+ notifyCancel();
1506
1518
  reap();
1507
1519
  }
1508
1520
  };
@@ -2501,150 +2513,6 @@ function sshArgv(connection, options = {}) {
2501
2513
  ], options.extraArgs ?? [], options.command);
2502
2514
  }
2503
2515
 
2504
- //#endregion
2505
- //#region src/remote/input.ts
2506
- /** Local carrier validation precedes any target discovery, connection or spawn. */
2507
- async function prepareRemoteInput(input) {
2508
- if (input.mode !== void 0 && input.mode !== "oneshot" && input.mode !== "pty") throw new Error("[E_BAD_MODE] remote: mode must be oneshot or pty");
2509
- if (input.scriptPath !== void 0) {
2510
- if (input.cmd !== void 0 || input.stdin !== void 0 || input.mode === "pty" || input.spawn !== void 0) throw new Error("[E_SCRIPT_PARAMS] remote: script is oneshot-only and cannot be combined with cmd, stdin, spawn or PTY lanes");
2511
- if (typeof input.scriptPath !== "string" || !isAbsolute(input.scriptPath) || input.scriptPath.includes("\0")) throw new Error("[E_SCRIPT_PATH] remote: script must be a local absolute path without NUL");
2512
- }
2513
- if (!input.target && input.spawn === void 0 && input.ssh === void 0) throw new Error("[E_PARAMS] remote: target is required (spawn / ssh are BYO entry points and may stand alone)");
2514
- if (input.scriptPath !== void 0) {
2515
- let stdin;
2516
- try {
2517
- stdin = await readFile(input.scriptPath, "utf8");
2518
- } catch (error) {
2519
- throw new Error(`[E_SCRIPT_READ] remote: cannot read local script ${JSON.stringify(input.scriptPath)} — ${error instanceof Error ? error.message : String(error)}`);
2520
- }
2521
- return {
2522
- target: input.target,
2523
- ssh: input.ssh,
2524
- cmd: "bash -s",
2525
- stdin,
2526
- mode: "oneshot",
2527
- timeout: input.timeout
2528
- };
2529
- }
2530
- return {
2531
- target: input.target,
2532
- spawn: input.spawn,
2533
- ssh: input.ssh,
2534
- cmd: input.cmd ?? "",
2535
- stdin: input.stdin,
2536
- mode: input.mode,
2537
- timeout: input.timeout
2538
- };
2539
- }
2540
-
2541
- //#endregion
2542
- //#region src/remote/oneshot.ts
2543
- async function runOneShot(argv, opts = {}) {
2544
- const process$1 = startOneShot(argv, opts);
2545
- const result = await process$1.done;
2546
- const stdout = process$1.read("stdout", 0), stderr = process$1.read("stderr", 0);
2547
- return {
2548
- ...result,
2549
- stdout: stdout.text,
2550
- stderr: [stderr.text, stderr.diagnostic].filter(Boolean).join("\n"),
2551
- outputStats: {
2552
- stdout: stdout.stats,
2553
- stderr: stderr.stats
2554
- }
2555
- };
2556
- }
2557
-
2558
- //#endregion
2559
- //#region src/remote/helper.ts
2560
- /** Reads helper-bin/MANIFEST.json next to the compiled module (lib/remote in the npm
2561
- * package, super-dsh/remote in the Super closure — same relative layout). Missing or
2562
- * malformed assets ⇒ null ⇒ shim-only mode; never throws. */
2563
- function helperAssets(baseDir) {
2564
- const dir = baseDir ?? join(dirname(fileURLToPath(import.meta.url)), "helper-bin");
2565
- try {
2566
- const manifest = JSON.parse(readFileSync(join(dir, "MANIFEST.json"), "utf8"));
2567
- if (manifest?.format !== 1 || typeof manifest.ver !== "string" || typeof manifest.protocol !== "number" || !manifest.targets || typeof manifest.targets !== "object") return null;
2568
- return {
2569
- dir,
2570
- manifest
2571
- };
2572
- } catch {
2573
- return null;
2574
- }
2575
- }
2576
- /** `uname -s`/`uname -m` → shipped target triple. Unknown combos ⇒ null (shim). */
2577
- function archTriple(sys, machine, shipped) {
2578
- const s = sys.trim().toLowerCase(), m = machine.trim().toLowerCase();
2579
- const candidates = s === "linux" ? m === "x86_64" || m === "amd64" ? "x86_64-unknown-linux-musl" : m === "aarch64" || m === "arm64" ? "aarch64-unknown-linux-musl" : null : s === "darwin" ? m === "arm64" || m === "aarch64" ? "aarch64-apple-darwin" : null : null;
2580
- if (candidates === null) return null;
2581
- return shipped.has(candidates) ? candidates : null;
2582
- }
2583
- function planKey(plan) {
2584
- switch (plan.kind) {
2585
- case "ssh": {
2586
- const c = plan.connection;
2587
- const host = c?.host ?? plan.host;
2588
- return `ssh|${c?.user ? c.user + "@" : ""}${host}${c?.port ? ":" + c.port : ""}${c?.proxyJump ? "|" + c.proxyJump : ""}`;
2589
- }
2590
- case "docker": return `docker|${plan.container}`;
2591
- case "incus": return `incus|${plan.container}`;
2592
- }
2593
- }
2594
- const PROBE_RE = /^SUPERD-PROBE (\S+) (\S+)$/m;
2595
- const READY_RE = /^SUPERD-READY (\S+)$/m;
2596
- var HelperRegistry = class {
2597
- states = /* @__PURE__ */ new Map();
2598
- constructor(assets, exec, probeArgv, uploadArgv) {
2599
- this.assets = assets;
2600
- this.exec = exec;
2601
- this.probeArgv = probeArgv;
2602
- this.uploadArgv = uploadArgv;
2603
- }
2604
- /** Never throws: any bootstrap failure resolves null (sticky) and the caller stays on the shim. */
2605
- ensure(plan) {
2606
- if (plan.kind !== "ssh" && plan.kind !== "docker" && plan.kind !== "incus") return Promise.resolve(null);
2607
- const key = planKey(plan);
2608
- let entry = this.states.get(key);
2609
- if (entry === void 0) {
2610
- entry = this.bootstrap(plan).catch(() => null);
2611
- entry.catch(() => {});
2612
- this.states.set(key, entry);
2613
- }
2614
- return entry;
2615
- }
2616
- async bootstrap(plan) {
2617
- const { manifest } = this.assets;
2618
- const first = await this.exec(this.probeArgv(plan, this.assets), { timeoutSec: 15 });
2619
- const ready = this.parseReady(first.stdout, manifest.ver, manifest.protocol);
2620
- if (ready !== null) return ready;
2621
- const probe = PROBE_RE.exec(first.stdout);
2622
- if (probe === null || first.exit !== 90 && first.exit !== 0) return null;
2623
- const triple = archTriple(probe[1], probe[2], new Set(Object.keys(manifest.targets)));
2624
- if (triple === null) return null;
2625
- const target = manifest.targets[triple];
2626
- const binary = await readFile(join(this.assets.dir, triple, target.file));
2627
- if ((await this.exec(this.uploadArgv(plan, triple, target.file, this.assets), {
2628
- stdin: binary,
2629
- timeoutSec: 60
2630
- })).exit !== 0) return null;
2631
- const second = await this.exec(this.probeArgv(plan, this.assets), { timeoutSec: 15 });
2632
- return this.parseReady(second.stdout, manifest.ver, manifest.protocol);
2633
- }
2634
- parseReady(stdout, ver, protocol) {
2635
- const ready = READY_RE.exec(stdout);
2636
- const banner = /^SUPERD-RUN proto=(\d+) name=superd-run ver=(\S+) target=(\S+)/m.exec(stdout);
2637
- if (ready === null || banner === null) return null;
2638
- if (banner[1] !== String(protocol) || banner[2] !== ver) return null;
2639
- return {
2640
- binPath: ready[1],
2641
- triple: banner[3],
2642
- ver,
2643
- protocol
2644
- };
2645
- }
2646
- };
2647
-
2648
2516
  //#endregion
2649
2517
  //#region src/remote/transports.ts
2650
2518
  /** POSIX 单引号机械转义(唯一一层转义,spec §三"一层机械转义")。 */
@@ -2673,7 +2541,10 @@ bash -lc ${shQuote(cmd)}`;
2673
2541
  wrapped
2674
2542
  ];
2675
2543
  switch (plan.kind) {
2676
- case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, { command: `setsid --wait bash -c ${shQuote(wrapped)}` });
2544
+ case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, {
2545
+ command: `setsid --wait bash -c ${shQuote(wrapped)}`,
2546
+ extraArgs: options.muxArgv
2547
+ });
2677
2548
  case "docker": return [
2678
2549
  "docker",
2679
2550
  "exec",
@@ -2727,21 +2598,41 @@ function buildSpawnArgv(spawnCommand) {
2727
2598
  "/dev/null"
2728
2599
  ];
2729
2600
  }
2601
+ /** Hub PTY(0.3.2-c;0.3.2-d F4 修正):会话子进程 = 本地 `script` PTY 托管的 WS 字节泵
2602
+ * (hub-pty-pump.mjs)。与 docker exec 的嵌套 PTY 同形,但泵是哑 node 子进程、不像
2603
+ * `ssh -tt` / `docker exec -it` 那样作为前台子进程自己把本地 tty 置 raw——所以本地包装必须
2604
+ * `stty raw`:canonical+ISIG 下 0x03 在本地变 SIGINT 杀泵、0x04 变 EOF 结束泵 stdin
2605
+ * (F4:input ^C/^D 与 job_kill 会打死整条会话)。raw 只作用于本地半段;对端 script PTY
2606
+ * 保持 canonical,^C/^D 在对端产生 SIGINT/EOF 正是我们要的语义(字节原样过线,解释权在
2607
+ * 对端)。kill 链 session 组杀 → 泵死 → WS 关 → 对端组杀,双向自然闭合。连接事实走 wrapper
2608
+ * 内 env 赋值(cookie 不进 argv、不落 ps)。 */
2609
+ function buildHubPtyArgv(endpoint, assets) {
2610
+ const node = assets.nodePath ?? process.execPath;
2611
+ return [
2612
+ "script",
2613
+ "-qfec",
2614
+ `stty raw -echo 2>/dev/null ; ${`export SUPERD_HUB_PTY_URL=${shQuote(endpoint.url)} SUPERD_HUB_PTY_COOKIE=${shQuote(endpoint.cookie)} SUPERD_HUB_PTY_WS=${shQuote(endpoint.wsPath)}`} ; exec ${shQuote(node)} ${shQuote(assets.pumpPath)}`,
2615
+ "/dev/null"
2616
+ ];
2617
+ }
2730
2618
  /** superd-run helper argv (spec: docs/specs/remote-rescope §5). Probe and upload are single
2731
2619
  * `sh -c` hops (busybox/alpine/darwin-safe); run/killgroup over ssh go through exactly one
2732
2620
  * remote-shell parse, same discipline as buildOneShotArgv. */
2733
- function buildHelperProbeArgv(plan, assets) {
2621
+ function buildHelperProbeArgv(plan, assets, muxArgv) {
2734
2622
  const { ver, targets } = assets.manifest;
2735
- return helperShellArgv(plan, `${Object.entries(targets).map(([triple, t]) => `d="$HOME/.cache/superd/run/${triple}-${ver}"; [ -x "$d/${t.file}" ] && { echo "SUPERD-READY $d/${t.file}"; "$d/${t.file}" banner; exit 0; };`).join(" ")} echo "SUPERD-PROBE $(uname -s 2>/dev/null || echo unknown) $(uname -m 2>/dev/null || echo unknown)"; exit 90`);
2623
+ return helperShellArgv(plan, `${Object.entries(targets).map(([triple, t]) => `d="$HOME/.cache/superd/run/${triple}-${ver}"; [ -x "$d/${t.file}" ] && { echo "SUPERD-READY $d/${t.file}"; "$d/${t.file}" banner; exit 0; };`).join(" ")} echo "SUPERD-PROBE $(uname -s 2>/dev/null || echo unknown) $(uname -m 2>/dev/null || echo unknown)"; exit 90`, muxArgv);
2736
2624
  }
2737
- function buildHelperUploadArgv(plan, triple, file, assets) {
2625
+ function buildHelperUploadArgv(plan, triple, file, assets, muxArgv) {
2738
2626
  const dir = `$HOME/.cache/superd/run/${triple}-${assets.manifest.ver}`;
2739
- return helperShellArgv(plan, `mkdir -p "${dir}" && cat > "${dir}/${file}.tmp" && chmod 0755 "${dir}/${file}.tmp" && mv -f "${dir}/${file}.tmp" "${dir}/${file}"`);
2627
+ return helperShellArgv(plan, `mkdir -p "${dir}" && cat > "${dir}/${file}.tmp" && chmod 0755 "${dir}/${file}.tmp" && mv -f "${dir}/${file}.tmp" "${dir}/${file}"`, muxArgv);
2740
2628
  }
2741
2629
  function buildHelperOneShotArgv(plan, binPath, options) {
2742
2630
  const { tag, cmd } = options;
2743
2631
  switch (plan.kind) {
2744
- case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, { command: `exec ${shQuote(binPath)} run --tag ${shQuote(tag)} -- bash -lc ${shQuote(cmd)}` });
2632
+ case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, {
2633
+ command: `exec ${shQuote(binPath)} run --tag ${shQuote(tag)} -- bash -lc ${shQuote(cmd)}`,
2634
+ extraArgs: options.muxArgv
2635
+ });
2745
2636
  case "docker": return [
2746
2637
  "docker",
2747
2638
  "exec",
@@ -2774,9 +2665,12 @@ function buildHelperOneShotArgv(plan, binPath, options) {
2774
2665
  ];
2775
2666
  }
2776
2667
  }
2777
- function buildHelperKillgroupArgv(plan, binPath, tag) {
2668
+ function buildHelperKillgroupArgv(plan, binPath, tag, muxArgv) {
2778
2669
  switch (plan.kind) {
2779
- case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, { command: `${shQuote(binPath)} killgroup --tag ${shQuote(tag)}` });
2670
+ case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, {
2671
+ command: `${shQuote(binPath)} killgroup --tag ${shQuote(tag)}`,
2672
+ extraArgs: muxArgv
2673
+ });
2780
2674
  case "docker": return [
2781
2675
  "docker",
2782
2676
  "exec",
@@ -2800,9 +2694,12 @@ function buildHelperKillgroupArgv(plan, binPath, tag) {
2800
2694
  ];
2801
2695
  }
2802
2696
  }
2803
- function helperShellArgv(plan, script) {
2697
+ function helperShellArgv(plan, script, muxArgv) {
2804
2698
  switch (plan.kind) {
2805
- case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, { command: script });
2699
+ case "ssh": return sshArgv(plan.connection ?? { host: plan.host }, {
2700
+ command: script,
2701
+ extraArgs: muxArgv
2702
+ });
2806
2703
  case "docker": return [
2807
2704
  "docker",
2808
2705
  "exec",
@@ -2827,108 +2724,347 @@ function helperShellArgv(plan, script) {
2827
2724
  }
2828
2725
 
2829
2726
  //#endregion
2830
- //#region src/remote/nonce-framing.ts
2831
- /** 8 字节随机串的 hex 形(spec §五.2:每次下发前生成)。 */
2832
- function genNonce(bytes = 8) {
2833
- return randomBytes(bytes).toString("hex");
2834
- }
2835
- /** 命令整行:花括号复合体 + Nonce 收帧尾(spec §五.2 的 printf 逐字落地 + cwd 第三字段)。 */
2836
- function wrapPtyCommand(cmd, nonce) {
2837
- return `{\n${cmd}\n} ; __superd_e=$? ; printf '\\033]133;E;${nonce};%s\\007' "$(env -0 | base64 | tr -d '\\n\\r')" ; printf '\\033]133;D;${nonce};%s;%s\\007' "$__superd_e" "$PWD" ; unset __superd_e\n`;
2838
- }
2839
- /** init 整行:哑终端(echo/PS1/PS2/TERM)+ 固定 40×120(Non-Goal ③)+ INIT 帧。 */
2840
- function buildInitCommand(nonce) {
2841
- return `stty -echo 2>/dev/null ; stty rows 40 cols 120 2>/dev/null ; PS1='' ; PS2='' ; export TERM=dumb ; printf '\\033]133;E;${nonce};%s\\007' "$(env -0 | base64 | tr -d '\\n\\r')" ; printf '\\033]133;D;${nonce};0;%s\\007' "$PWD"\n`;
2842
- }
2843
- const ANSI_RE = /\x1b(?:\][^\x07\x1b]*(?:\x07|\x1b\\)?|\[[0-9;?]*[ -/]*[@-~]|[@-Z\\-_])/g;
2844
- function stripAnsi(s) {
2845
- return s.replace(ANSI_RE, "");
2846
- }
2847
- function normalizePtyText(s) {
2848
- return stripAnsi(s).replace(/\r\n/g, "\n").replace(/\r/g, "");
2849
- }
2850
- function tailWindow(text, cap, stats) {
2851
- let tail = text.slice(-cap);
2852
- if (tail.charCodeAt(0) >= 56320 && tail.charCodeAt(0) <= 57343) tail = tail.slice(1);
2853
- const returned = stats?.unit === "bytes" ? Buffer.byteLength(tail) : tail.length;
2854
- const total = stats?.total ?? text.length;
2855
- const updated = stats === void 0 ? {} : { stats: {
2856
- ...stats,
2857
- returned
2858
- } };
2859
- if (total <= returned) return {
2860
- text: tail,
2861
- ...updated
2862
- };
2727
+ //#region src/remote/input.ts
2728
+ /** Local carrier validation precedes any target discovery, connection or spawn. */
2729
+ async function prepareRemoteInput(input) {
2730
+ if (input.mode !== void 0 && input.mode !== "oneshot" && input.mode !== "pty") throw new Error("[E_BAD_MODE] remote: mode must be oneshot or pty");
2731
+ if (input.scriptPath !== void 0) {
2732
+ if (input.cmd !== void 0 || input.spawn !== void 0) throw new Error("[E_SCRIPT_PARAMS] remote: script and cmd are alternative carriers — pass one; spawn belongs to BYO PTY creation, not scripts");
2733
+ if (typeof input.scriptPath !== "string" || !isAbsolute(input.scriptPath) || input.scriptPath.includes("\0")) throw new Error("[E_SCRIPT_PATH] remote: script must be a local absolute path without NUL");
2734
+ }
2735
+ if (!input.target && input.spawn === void 0 && input.ssh === void 0) throw new Error("[E_PARAMS] remote: target is required (spawn / ssh are BYO entry points and may stand alone)");
2736
+ if (input.scriptPath !== void 0) {
2737
+ let script;
2738
+ try {
2739
+ script = await readFile(input.scriptPath, "utf8");
2740
+ } catch (error) {
2741
+ throw new Error(`[E_SCRIPT_READ] remote: cannot read local script ${JSON.stringify(input.scriptPath)} — ${error instanceof Error ? error.message : String(error)}`);
2742
+ }
2743
+ if (script.includes("\0")) throw new Error("[E_SCRIPT_NUL] remote: script contains NUL bytes — the command channel cannot carry it; transfer the file and run it with cmd instead");
2744
+ return {
2745
+ target: input.target,
2746
+ ssh: input.ssh,
2747
+ cmd: `bash -c ${shQuote(script)}`,
2748
+ stdin: input.stdin,
2749
+ mode: input.mode,
2750
+ timeout: input.timeout
2751
+ };
2752
+ }
2863
2753
  return {
2864
- text: `[truncated: showing last ${returned} of ${total} ${stats?.unit === "bytes" ? "bytes" : "chars"}${stats !== void 0 && stats.dropped > 0 ? `; retained ${stats.retained}, dropped head ${stats.dropped}, retention limit ${stats.retentionLimit} ${stats.unit === "bytes" ? "bytes" : "UTF-16 code units"}` : ""}]\n${tail}`,
2865
- truncated: total,
2866
- ...updated
2754
+ target: input.target,
2755
+ spawn: input.spawn,
2756
+ ssh: input.ssh,
2757
+ cmd: input.cmd ?? "",
2758
+ stdin: input.stdin,
2759
+ mode: input.mode,
2760
+ timeout: input.timeout
2867
2761
  };
2868
2762
  }
2869
- const FRAME_BODY_RE = /^(\d+);([^\x07\x1b]*)(?:\x07|\x1b\\)/;
2870
- const ENV_BODY_RE = /^([A-Za-z0-9+/=]*)\x07/;
2871
- /**
2872
- * 单 nonce、单帧解析器:一轮 dispatch 一个实例。只认 `\x1b]133;D;<nonce>;`,
2873
- * 错误 nonce 的静态/伪造 marker 永不匹配,按普通输出流过(再经 ANSI 剥洗)。
2874
- * 缓冲尾若可能是 marker 前缀则扣留,flush() 时作为输出放出。
2875
- */
2876
- function createNonceFrameParser(nonce, events) {
2877
- const prefix = `\x1b]133;D;${nonce};`;
2878
- const prefixE = `\x1b]133;E;${nonce};`;
2879
- const prefixes = [prefix, prefixE];
2880
- let buf = "";
2881
- let done = false;
2882
- const emit = (raw) => {
2883
- const text = normalizePtyText(raw);
2884
- if (text.length > 0) events.onOutput?.(text);
2885
- };
2886
- /** 缓冲尾若是任一候选前缀的真前缀则扣留(D/E 共用);flush() 放出残余。 */
2887
- const holdback = () => {
2888
- let best = 0;
2889
- for (const p of prefixes) {
2890
- const max = Math.min(buf.length, p.length - 1);
2891
- for (let k = max; k > best; k--) if (buf.endsWith(p.slice(0, k))) {
2892
- best = k;
2893
- break;
2894
- }
2763
+
2764
+ //#endregion
2765
+ //#region src/remote/oneshot.ts
2766
+ async function runOneShot(argv, opts = {}) {
2767
+ const process$1 = startOneShot(argv, opts);
2768
+ const result = await process$1.done;
2769
+ const stdout = process$1.read("stdout", 0), stderr = process$1.read("stderr", 0);
2770
+ return {
2771
+ ...result,
2772
+ stdout: stdout.text,
2773
+ stderr: [stderr.text, stderr.diagnostic].filter(Boolean).join("\n"),
2774
+ outputStats: {
2775
+ stdout: stdout.stats,
2776
+ stderr: stderr.stats
2895
2777
  }
2896
- return best;
2897
2778
  };
2898
- /** 扫描最早出现的帧:E(env 侧信道)先于 D 下发;D = settle,帧后字节丢弃。 */
2899
- const consume = () => {
2900
- if (done) return;
2901
- let idx = -1;
2902
- let isEnv = false;
2903
- for (const p of prefixes) {
2904
- const i = buf.indexOf(p);
2905
- if (i >= 0 && (idx === -1 || i < idx)) {
2906
- idx = i;
2907
- isEnv = p === prefixE;
2908
- }
2909
- }
2910
- if (idx < 0) {
2911
- const keep = holdback();
2912
- emit(buf.slice(0, buf.length - keep));
2913
- buf = buf.slice(buf.length - keep);
2914
- return;
2915
- }
2916
- emit(buf.slice(0, idx));
2917
- const rest = buf.slice(idx + (isEnv ? prefixE.length : prefix.length));
2918
- if (isEnv) {
2919
- const m$1 = ENV_BODY_RE.exec(rest);
2920
- if (m$1 === null) {
2921
- buf = buf.slice(idx);
2922
- return;
2779
+ }
2780
+
2781
+ //#endregion
2782
+ //#region src/remote/helper.ts
2783
+ /** Reads helper-bin/MANIFEST.json next to the compiled module (lib/remote in
2784
+ * the npm package). Missing or malformed assets ⇒ null ⇒ shim-only mode;
2785
+ * never throws. */
2786
+ function helperAssets(baseDir) {
2787
+ const dir = baseDir ?? join(dirname(fileURLToPath(import.meta.url)), "helper-bin");
2788
+ try {
2789
+ const manifest = JSON.parse(readFileSync(join(dir, "MANIFEST.json"), "utf8"));
2790
+ if (manifest?.format !== 1 || typeof manifest.ver !== "string" || typeof manifest.protocol !== "number" || !manifest.targets || typeof manifest.targets !== "object") return null;
2791
+ return {
2792
+ dir,
2793
+ manifest
2794
+ };
2795
+ } catch {
2796
+ return null;
2797
+ }
2798
+ }
2799
+ /** `uname -s`/`uname -m` → shipped target triple. Unknown combos ⇒ null (shim). */
2800
+ function archTriple(sys, machine, shipped) {
2801
+ const s = sys.trim().toLowerCase(), m = machine.trim().toLowerCase();
2802
+ const candidates = s === "linux" ? m === "x86_64" || m === "amd64" ? "x86_64-unknown-linux-musl" : m === "aarch64" || m === "arm64" ? "aarch64-unknown-linux-musl" : null : s === "darwin" ? m === "arm64" || m === "aarch64" ? "aarch64-apple-darwin" : null : null;
2803
+ if (candidates === null) return null;
2804
+ return shipped.has(candidates) ? candidates : null;
2805
+ }
2806
+ function planKey(plan) {
2807
+ switch (plan.kind) {
2808
+ case "ssh": {
2809
+ const c = plan.connection;
2810
+ const host = c?.host ?? plan.host;
2811
+ return `ssh|${c?.user ? c.user + "@" : ""}${host}${c?.port ? ":" + c.port : ""}${c?.proxyJump ? "|" + c.proxyJump : ""}`;
2812
+ }
2813
+ case "docker": return `docker|${plan.container}`;
2814
+ case "incus": return `incus|${plan.container}`;
2815
+ }
2816
+ }
2817
+ const PROBE_RE = /^SUPERD-PROBE (\S+) (\S+)$/m;
2818
+ const READY_RE = /^SUPERD-READY (\S+)$/m;
2819
+ var HelperRegistry = class {
2820
+ states = /* @__PURE__ */ new Map();
2821
+ constructor(assets, exec, probeArgv, uploadArgv) {
2822
+ this.assets = assets;
2823
+ this.exec = exec;
2824
+ this.probeArgv = probeArgv;
2825
+ this.uploadArgv = uploadArgv;
2826
+ }
2827
+ /** Never throws: any bootstrap failure resolves null (sticky) and the caller stays on the shim. */
2828
+ ensure(plan, muxArgv = []) {
2829
+ if (plan.kind !== "ssh" && plan.kind !== "docker" && plan.kind !== "incus") return Promise.resolve(null);
2830
+ const key = planKey(plan);
2831
+ let entry = this.states.get(key);
2832
+ if (entry === void 0) {
2833
+ entry = this.bootstrap(plan, muxArgv).catch(() => null);
2834
+ entry.catch(() => {});
2835
+ this.states.set(key, entry);
2836
+ }
2837
+ return entry;
2838
+ }
2839
+ async bootstrap(plan, muxArgv) {
2840
+ const { manifest } = this.assets;
2841
+ const first = await this.exec(this.probeArgv(plan, this.assets, muxArgv), { timeoutSec: 15 });
2842
+ const ready = this.parseReady(first.stdout, manifest.ver, manifest.protocol);
2843
+ if (ready !== null) return ready;
2844
+ const probe = PROBE_RE.exec(first.stdout);
2845
+ if (probe === null || first.exit !== 90 && first.exit !== 0) return null;
2846
+ const triple = archTriple(probe[1], probe[2], new Set(Object.keys(manifest.targets)));
2847
+ if (triple === null) return null;
2848
+ const target = manifest.targets[triple];
2849
+ const binary = await readFile(join(this.assets.dir, triple, target.file));
2850
+ if ((await this.exec(this.uploadArgv(plan, triple, target.file, this.assets, muxArgv), {
2851
+ stdin: binary,
2852
+ timeoutSec: 60
2853
+ })).exit !== 0) return null;
2854
+ const second = await this.exec(this.probeArgv(plan, this.assets, muxArgv), { timeoutSec: 15 });
2855
+ return this.parseReady(second.stdout, manifest.ver, manifest.protocol);
2856
+ }
2857
+ parseReady(stdout, ver, protocol) {
2858
+ const ready = READY_RE.exec(stdout);
2859
+ const banner = /^SUPERD-RUN proto=(\d+) name=superd-run ver=(\S+) target=(\S+)/m.exec(stdout);
2860
+ if (ready === null || banner === null) return null;
2861
+ if (banner[1] !== String(protocol) || banner[2] !== ver) return null;
2862
+ return {
2863
+ binPath: ready[1],
2864
+ triple: banner[3],
2865
+ ver,
2866
+ protocol
2867
+ };
2868
+ }
2869
+ };
2870
+
2871
+ //#endregion
2872
+ //#region src/remote/ssh-mux.ts
2873
+ /** SSH oneshot 连接复用(plan 2026-11-01 Task E;探针基线 = 发起端 OpenSSH 10.2p1,2026-11-01 实测):
2874
+ * - master 与客户端完全脱钩(fork 后 PPID=init、自有 pgid/session)→ 本地 killpg 取消/超时永不波及
2875
+ * master,后续 oneshot 继续复用;这正好落在 startBinaryProcess 的组杀清理语义之内。
2876
+ * - ControlPersist 到期 master 自行退出并 unlink socket;`ssh -O exit` 主动拆除亦同。
2877
+ * - master 被 SIGKILL 会留下残 socket 文件:ControlMaster=auto 的下一个客户端会自愈重建
2878
+ * (实测 exit 0),失败语义按 ssh 真实回退,不冒充复用。
2879
+ * - 只注入 SSH oneshot 执行链(POSIX wrapper / helper probe·upload·run·killgroup / cleanup /
2880
+ * status 探测);PTY、scp、rsync、relay 与 copy 的 mkdir pre-hop 一律不注入。
2881
+ * - CLI `-o` 覆盖用户 config 同名项是 OpenSSH 语义(last-option-wins),其余身份选项照常读 config。 */
2882
+ const SSH_MUX_CONTROL_PERSIST_SEC = 600;
2883
+ /** Linux sun_path 上限 108;留余量并在超限时诚实退化为不复用(MUX 是优化,不是前提)。 */
2884
+ const MAX_SOCKET_PATH = 104;
2885
+ function identityKey(identity) {
2886
+ const c = identity.connection;
2887
+ const raw = JSON.stringify([
2888
+ identity.target,
2889
+ c.host,
2890
+ c.user ?? null,
2891
+ c.port ?? null,
2892
+ c.key ?? null,
2893
+ c.proxyJump ?? null
2894
+ ]);
2895
+ return createHash("sha256").update(raw).digest("hex").slice(0, 16);
2896
+ }
2897
+ /** 每 Agent 一个(driver 私有):tool-owned 0700 socket 目录 + 按连接身份的 socket 名。
2898
+ * 目录名含随机 owner 后缀,跨 Agent 天然不共享;dispose 逐 socket `-O exit` 后整目录删除。 */
2899
+ var SshMux = class {
2900
+ persistSec;
2901
+ rootDir;
2902
+ runner;
2903
+ owner = randomUUID().slice(0, 8);
2904
+ sockets = /* @__PURE__ */ new Map();
2905
+ dir;
2906
+ disposal;
2907
+ constructor(options = {}) {
2908
+ this.persistSec = options.persistSec ?? SSH_MUX_CONTROL_PERSIST_SEC;
2909
+ this.rootDir = options.rootDir ?? tmpdir();
2910
+ this.runner = options.runner ?? runOneShot;
2911
+ }
2912
+ /** 注入 SSH argv 的 MUX 选项(trusted internal argv,非模型输入)。目录懒建(纯 docker/hub
2913
+ * Agent 永不触盘);超限/已 dispose 返回 [] = 诚实退化为普通连接。 */
2914
+ argvFor(identity) {
2915
+ if (this.disposal !== void 0) return [];
2916
+ const socket = join(this.dir ?? (this.dir = join(this.rootDir, `superd-mux-${this.owner}`)), identityKey(identity));
2917
+ if (socket.length + 1 > MAX_SOCKET_PATH) return [];
2918
+ try {
2919
+ mkdirSync(this.dir, {
2920
+ recursive: true,
2921
+ mode: 448
2922
+ });
2923
+ } catch {
2924
+ return [];
2925
+ }
2926
+ this.sockets.set(socket, identity.connection.host);
2927
+ return [
2928
+ "-o",
2929
+ `ControlPath=${socket}`,
2930
+ "-o",
2931
+ "ControlMaster=auto",
2932
+ "-o",
2933
+ `ControlPersist=${this.persistSec}`
2934
+ ];
2935
+ }
2936
+ /** Teardown:先 `-O exit` 逐 socket 礼貌拆除 master(不拨号、5s 上限、错误全吞——
2937
+ * master 已死/残 socket 属正常),再删目录。单飞(重复调用同一 Promise)。 */
2938
+ dispose() {
2939
+ if (this.disposal !== void 0) return this.disposal;
2940
+ const sockets = [...this.sockets.entries()];
2941
+ this.sockets.clear();
2942
+ return this.disposal = (async () => {
2943
+ await Promise.allSettled(sockets.map(([socket, host]) => this.runner([
2944
+ "ssh",
2945
+ "-o",
2946
+ `ControlPath=${socket}`,
2947
+ "-O",
2948
+ "exit",
2949
+ "--",
2950
+ host
2951
+ ], { timeoutSec: 5 })));
2952
+ if (this.dir !== void 0) try {
2953
+ rmSync(this.dir, {
2954
+ recursive: true,
2955
+ force: true
2956
+ });
2957
+ } catch {}
2958
+ })();
2959
+ }
2960
+ };
2961
+
2962
+ //#endregion
2963
+ //#region src/remote/nonce-framing.ts
2964
+ /** 8 字节随机串的 hex 形(spec §五.2:每次下发前生成)。 */
2965
+ function genNonce(bytes = 8) {
2966
+ return randomBytes(bytes).toString("hex");
2967
+ }
2968
+ /** 命令整行:花括号复合体 + Nonce 收帧尾(spec §五.2 的 printf 逐字落地 + cwd 第三字段)。 */
2969
+ function wrapPtyCommand(cmd, nonce) {
2970
+ return `{\n${cmd}\n} ; __superd_e=$? ; printf '\\033]133;E;${nonce};%s\\007' "$(env -0 | base64 | tr -d '\\n\\r')" ; printf '\\033]133;D;${nonce};%s;%s\\007' "$__superd_e" "$PWD" ; unset __superd_e\n`;
2971
+ }
2972
+ /** init 整行:哑终端(echo/PS1/PS2/TERM)+ 固定 40×120(Non-Goal ③)+ INIT 帧。 */
2973
+ function buildInitCommand(nonce) {
2974
+ return `stty -echo 2>/dev/null ; stty rows 40 cols 120 2>/dev/null ; PS1='' ; PS2='' ; export TERM=dumb ; printf '\\033]133;E;${nonce};%s\\007' "$(env -0 | base64 | tr -d '\\n\\r')" ; printf '\\033]133;D;${nonce};0;%s\\007' "$PWD"\n`;
2975
+ }
2976
+ const ANSI_RE = /\x1b(?:\][^\x07\x1b]*(?:\x07|\x1b\\)?|\[[0-9;?]*[ -/]*[@-~]|[@-Z\\-_])/g;
2977
+ function stripAnsi(s) {
2978
+ return s.replace(ANSI_RE, "");
2979
+ }
2980
+ function normalizePtyText(s) {
2981
+ return stripAnsi(s).replace(/\r\n/g, "\n").replace(/\r/g, "");
2982
+ }
2983
+ /** 0.3.2-e:自有 30K 返回窗口已删(原生 agent runtime 的 spill/输出保护接管模型上下文
2984
+ * 尺寸)。本 helper 只做 retention 披露:ring 留存(4M UTF-16 / 4MiB bytes)未驱逐时
2985
+ * 全量透传(returned=retained,无 banner);驱逐(dropped>0,仅 >4M 输出发生)时打一行
2986
+ * banner 且 truncated=原始总量。text 已是 ring 尾部,不再二次裁剪。 */
2987
+ function disclose(text, stats) {
2988
+ if (stats === void 0) return { text };
2989
+ const returned = stats.retained;
2990
+ const updated = { stats: {
2991
+ ...stats,
2992
+ returned
2993
+ } };
2994
+ if (stats.dropped <= 0) return {
2995
+ text,
2996
+ ...updated
2997
+ };
2998
+ const unit = stats.unit === "bytes" ? "bytes" : "chars";
2999
+ return {
3000
+ text: `[retention tail: showing last ${stats.retained} of ${stats.total} ${unit}; dropped head ${stats.dropped}, retention limit ${stats.retentionLimit} ${stats.unit === "bytes" ? "bytes" : "UTF-16 code units"}]\n${text}`,
3001
+ truncated: stats.total,
3002
+ ...updated
3003
+ };
3004
+ }
3005
+ const FRAME_BODY_RE = /^(\d+);([^\x07\x1b]*)(?:\x07|\x1b\\)/;
3006
+ const ENV_BODY_RE = /^([A-Za-z0-9+/=]*)\x07/;
3007
+ /**
3008
+ * 单 nonce、单帧解析器:一轮 dispatch 一个实例。只认 `\x1b]133;D;<nonce>;`,
3009
+ * 错误 nonce 的静态/伪造 marker 永不匹配,按普通输出流过(再经 ANSI 剥洗)。
3010
+ * 缓冲尾若可能是 marker 前缀则扣留,flush() 时作为输出放出。
3011
+ */
3012
+ function createNonceFrameParser(nonce, events) {
3013
+ const prefix = `\x1b]133;D;${nonce};`;
3014
+ const prefixE = `\x1b]133;E;${nonce};`;
3015
+ const prefixes = [prefix, prefixE];
3016
+ let buf$1 = "";
3017
+ let done = false;
3018
+ const emit = (raw) => {
3019
+ const text = normalizePtyText(raw);
3020
+ if (text.length > 0) events.onOutput?.(text);
3021
+ };
3022
+ /** 缓冲尾若是任一候选前缀的真前缀则扣留(D/E 共用);flush() 放出残余。 */
3023
+ const holdback = () => {
3024
+ let best = 0;
3025
+ for (const p of prefixes) {
3026
+ const max = Math.min(buf$1.length, p.length - 1);
3027
+ for (let k = max; k > best; k--) if (buf$1.endsWith(p.slice(0, k))) {
3028
+ best = k;
3029
+ break;
3030
+ }
3031
+ }
3032
+ return best;
3033
+ };
3034
+ /** 扫描最早出现的帧:E(env 侧信道)先于 D 下发;D = settle,帧后字节丢弃。 */
3035
+ const consume = () => {
3036
+ if (done) return;
3037
+ let idx = -1;
3038
+ let isEnv = false;
3039
+ for (const p of prefixes) {
3040
+ const i = buf$1.indexOf(p);
3041
+ if (i >= 0 && (idx === -1 || i < idx)) {
3042
+ idx = i;
3043
+ isEnv = p === prefixE;
3044
+ }
3045
+ }
3046
+ if (idx < 0) {
3047
+ const keep = holdback();
3048
+ emit(buf$1.slice(0, buf$1.length - keep));
3049
+ buf$1 = buf$1.slice(buf$1.length - keep);
3050
+ return;
3051
+ }
3052
+ emit(buf$1.slice(0, idx));
3053
+ const rest = buf$1.slice(idx + (isEnv ? prefixE.length : prefix.length));
3054
+ if (isEnv) {
3055
+ const m$1 = ENV_BODY_RE.exec(rest);
3056
+ if (m$1 === null) {
3057
+ buf$1 = buf$1.slice(idx);
3058
+ return;
2923
3059
  }
2924
3060
  if (m$1[1].length > 0) events.onEnv?.(m$1[1]);
2925
- buf = rest.slice(m$1[0].length);
3061
+ buf$1 = rest.slice(m$1[0].length);
2926
3062
  consume();
2927
3063
  return;
2928
3064
  }
2929
3065
  const m = FRAME_BODY_RE.exec(rest);
2930
3066
  if (m === null) {
2931
- buf = buf.slice(idx);
3067
+ buf$1 = buf$1.slice(idx);
2932
3068
  return;
2933
3069
  }
2934
3070
  events.onFrame({
@@ -2936,16 +3072,16 @@ function createNonceFrameParser(nonce, events) {
2936
3072
  cwd: m[2].length > 0 ? m[2] : null
2937
3073
  });
2938
3074
  done = true;
2939
- buf = "";
3075
+ buf$1 = "";
2940
3076
  };
2941
3077
  return {
2942
3078
  feed(chunk) {
2943
- buf += chunk;
3079
+ buf$1 += chunk;
2944
3080
  consume();
2945
3081
  },
2946
3082
  flush() {
2947
- if (buf.length > 0) events.onOutput?.(buf);
2948
- buf = "";
3083
+ if (buf$1.length > 0) events.onOutput?.(buf$1);
3084
+ buf$1 = "";
2949
3085
  }
2950
3086
  };
2951
3087
  }
@@ -2988,6 +3124,67 @@ function diffEnv(prev, next) {
2988
3124
  };
2989
3125
  }
2990
3126
 
3127
+ //#endregion
3128
+ //#region src/remote/trace.ts
3129
+ /**
3130
+ * R1 定位轮插桩(2026-11-01):`SUPERD_REMOTE_TRACE=<file>` 时输出 JSONL 事件,否则全 no-op。
3131
+ * 动机:rig-only「PTY background + startup stdin」丢失只在 rig 长驻进程复现(bg-read 形状 13/13),
3132
+ * 本地全组合 0 复现——必须在实际 rig 宿主进程内拿到第一手事件流。
3133
+ *
3134
+ * 设计约束:
3135
+ * - 热路径(write/onOutput)零同步 IO:事件先进内存缓冲,异步 flush(settle/dispose/满 200 条)。
3136
+ * - env 未设置时每个入口都是一次函数调用 + 立即 return,不改变交付语义。
3137
+ * - 失败静默(trace 永不破坏工具行为);缓冲上限 5000 条防失控。
3138
+ */
3139
+ const TRACE_PATH = process.env.SUPERD_REMOTE_TRACE ?? "";
3140
+ const ENABLED = TRACE_PATH !== "";
3141
+ let buf = [];
3142
+ let flushing = false;
3143
+ function serialize(ev, fields) {
3144
+ let s;
3145
+ try {
3146
+ s = JSON.stringify({
3147
+ t: Date.now(),
3148
+ mono: Number(process.hrtime.bigint() / 1000000n),
3149
+ ev,
3150
+ ...fields
3151
+ });
3152
+ } catch {
3153
+ s = JSON.stringify({
3154
+ t: Date.now(),
3155
+ ev,
3156
+ err: "unserializable-fields"
3157
+ });
3158
+ }
3159
+ return s ?? `{\"t\":${Date.now()},\"ev\":${JSON.stringify(ev)},\"err\":\"null\"}`;
3160
+ }
3161
+ function trace(ev, fields = {}) {
3162
+ if (!ENABLED) return;
3163
+ buf.push(serialize(ev, fields));
3164
+ if (buf.length > 5e3) buf.splice(0, buf.length - 5e3);
3165
+ if (buf.length >= 200) traceFlush();
3166
+ }
3167
+ async function traceFlush() {
3168
+ if (!ENABLED || flushing || buf.length === 0) return;
3169
+ flushing = true;
3170
+ const out = buf.splice(0).join("\n") + "\n";
3171
+ try {
3172
+ const [{ appendFile }, { dirname: dirname$1 }] = await Promise.all([import("node:fs/promises"), import("node:path")]);
3173
+ const dir = dirname$1(TRACE_PATH);
3174
+ try {
3175
+ await (await import("node:fs/promises")).mkdir(dir, { recursive: true });
3176
+ } catch {}
3177
+ await appendFile(TRACE_PATH, out);
3178
+ } catch {}
3179
+ flushing = false;
3180
+ if (buf.length > 0) traceFlush();
3181
+ }
3182
+ /** 事件里的字节安全截断表示(JSON 转义 + 长度上限)。 */
3183
+ function traceRepr(s, max) {
3184
+ const j = JSON.stringify(s) ?? "null";
3185
+ return j.length > max ? `${j.slice(0, max)}…` : j;
3186
+ }
3187
+
2991
3188
  //#endregion
2992
3189
  //#region src/remote/pty-session.ts
2993
3190
  /** spec §五.2 逐字。 */
@@ -2999,29 +3196,27 @@ function displayKey(key) {
2999
3196
  const raw = key.includes("|") ? key.slice(key.indexOf("|") + 1) : key;
3000
3197
  return raw.startsWith("p:") ? `pty:${raw.slice(2)}` : raw;
3001
3198
  }
3002
- const LEDGER_MAX = 20;
3003
- const summaryOf = (cmd) => {
3004
- const first = cmd.trim().split(/\r?\n/, 1)[0] ?? "";
3005
- return first.length > 72 ? `${first.slice(0, 71)}…` : first;
3006
- };
3007
3199
  const DEFAULT_INIT_TIMEOUT_SEC = 30;
3008
3200
  const INTERRUPT_GRACE_MS = 3e3;
3201
+ /** F2(0.3.2-d):\x03 触发 ISIG 时内核 flush 输入队列——130-marker 必须等 flush 完成
3202
+ * 再入队,否则首字节被吞(实测 'rintf' not found → 会话死)。 */
3203
+ const MARKER_SETTLE_MS = 80;
3009
3204
  /** pty 收集内存安全帽(Ruling P15,与 oneshot HARD_CAP 对齐):模型面只见尾窗,保留尾部。 */
3010
3205
  const OUTPUT_HARD_CAP = 4e6;
3011
3206
  var PtySession = class {
3012
3207
  proc = null;
3013
3208
  state = "cold";
3014
3209
  startPromise = null;
3015
- queue = Promise.resolve();
3016
3210
  idleTimer = null;
3017
3211
  onChunk = null;
3018
3212
  deathWaiters = [];
3019
3213
  diag = "";
3020
3214
  inFlight = 0;
3215
+ cmdSeq = 0;
3021
3216
  /** P4/P5:当前占用前台的 detached job(background/超时 handoff)。非 null 时新命令拒绝(Q6)。 */
3022
3217
  detachedJob = null;
3023
- /** 账本(§5):本会话最近 20 条命令(FIFO,超出丢最旧)。 */
3024
- ledger = [];
3218
+ /** §3.2(2026-10-25):当前占用会话的命令——dispatch 同步占位,settle/cleanup 释放;input 的唯一写入对象。 */
3219
+ activeCommand = null;
3025
3220
  /** env 侧信道基线(init E 帧);undefined = 侧信道不可用(不报假 delta)。 */
3026
3221
  envBaseline;
3027
3222
  /** idle 起点(ready / settle 时重置);null = 尚无 idle 时钟(cold 或已 dispose)。 */
@@ -3052,10 +3247,13 @@ var PtySession = class {
3052
3247
  get detachedCommandId() {
3053
3248
  return this.detachedJob?.commandId ?? null;
3054
3249
  }
3055
- /** job 通道收养后回填 jobId(timeout handoff 时 registry id ≠ commandId,账本如实记录两者)。 */
3250
+ /** handoff/card 面(2026-11-01 默认 PTY 轮):当前占用会话的命令与可选 job 句柄;无 = null。 */
3251
+ get active() {
3252
+ return this.activeCommand === null ? null : { ...this.activeCommand };
3253
+ }
3254
+ /** job 通道收养后回填 jobId(timeout handoff 时 registry id ≠ commandId);运行中的命令同步携带(stdin-only 写入回执用)。 */
3056
3255
  attachJob(commandId, jobId) {
3057
- const entry = [...this.ledger].reverse().find((e) => e.commandId === commandId);
3058
- if (entry !== void 0) entry.jobId = jobId;
3256
+ if (this.activeCommand !== null && this.activeCommand.commandId === commandId) this.activeCommand.jobId = jobId;
3059
3257
  }
3060
3258
  /** Workbench 身份(§3):id + 可选 label + connection + byo 标记。 */
3061
3259
  get identity() {
@@ -3067,10 +3265,6 @@ var PtySession = class {
3067
3265
  builtOn: this.opts.builtOn === true
3068
3266
  };
3069
3267
  }
3070
- /** 账本快照(session card 面,只读)。 */
3071
- ledgerSnapshot() {
3072
- return this.ledger;
3073
- }
3074
3268
  /** spawn 创建原文(仅 BYO 会话;v2 起仅存档,不参与复用判定)。 */
3075
3269
  get origin() {
3076
3270
  return this.opts.origin;
@@ -3089,36 +3283,67 @@ var PtySession = class {
3089
3283
  async warm() {
3090
3284
  await this.start();
3091
3285
  }
3092
- /** 同一会话命令严格串行(FIFO);死亡/冷态先重启。P4/P5(2026-10-19):超时不再自动
3093
- * 打断——handoff 成 running job(byte-cursor 轮询 + 显式 interrupt,P8 机制原样保留在
3094
- * job.interrupt);background 选项让 dispatch 立即回执。占用者是 detached job 时新命令
3095
- * 拒绝(Q6:诚实回状态优先于排队),纯同步并发 FIFO 语义不变。 */
3286
+ /** 同一会话命令占位制(2026-10-25 interactive-stdin 轮):dispatch 入口**同步**建立
3287
+ * 命令占位(activeCommand + inFlight + 账本条目)——上一条命令仍在运行(同步等待 /
3288
+ * 等输入 / detached job)时新 cmd 立即 E_SESSION_BUSY;原「纯同步并发排 FIFO」语义
3289
+ * 废除(排队会让等输入的命令与补输入动作互相卡死,也给 busy 留下绕行窗口)。
3290
+ * P4/P5:超时不杀命令而是 handoff 成 running job;background 立即回执;P8 显式
3291
+ * interrupt 保留在 job.interrupt;死亡/冷态先重启(reconnect 语义不变)。 */
3096
3292
  dispatch(cmd, dOpts) {
3293
+ if (this.state === "disposed") return Promise.reject(/* @__PURE__ */ new Error(`[E_SESSION_DISPOSED] remote pty session '${this.opts.id}' was disposed before dispatch`));
3294
+ if (this.activeCommand !== null) {
3295
+ const handle = this.activeCommand.jobId ?? this.activeCommand.commandId;
3296
+ return Promise.reject(/* @__PURE__ */ new Error(`[E_SESSION_BUSY] remote: pty session '${this.opts.id}' still runs a command (${this.activeCommand.commandId}) — feed it more stdin with {action:"exec", target:..., stdin:...} (stdin-only write), follow its output via job_output ${handle} (a synchronous exec hands its job off there on timeout), cancel with job_kill ${handle}, or address other work to another session / oneshot`));
3297
+ }
3298
+ const commandId = dOpts.commandId;
3299
+ this.activeCommand = {
3300
+ commandId,
3301
+ ...dOpts.jobId !== void 0 ? { jobId: dOpts.jobId } : {}
3302
+ };
3303
+ this.inFlight++;
3304
+ this.cmdSeq++;
3097
3305
  const attempt = async () => {
3098
3306
  const started = Date.now();
3099
3307
  const reconnected = this.state === "dead";
3100
- if (this.state !== "ready") await this.start();
3101
- if (this.state === "disposed") throw new Error(`[E_SESSION_DISPOSED] remote pty session '${this.opts.id}' was disposed before dispatch`);
3102
- if (this.detachedJob !== null) throw new Error(`[E_SESSION_BUSY] remote: pty session '${this.opts.id}' still runs a detached command (${this.detachedJob.commandId}) — poll job_output ${this.detachedJob.commandId}, cancel with job_kill ${this.detachedJob.commandId}, or address other work to another session / oneshot`);
3103
- const commandId = dOpts.commandId;
3104
- const entry = {
3105
- commandId,
3106
- summary: summaryOf(cmd),
3107
- exit: null,
3108
- durationMs: 0,
3109
- running: true
3308
+ /** 占位撤销:命令从未真正启动(建连失败 / 建连中被 dispose)——释放 busy 与计数,
3309
+ * 撤掉账本条目(与旧时序等价:条目原本在 start 成功之后才建立)。 */
3310
+ const abandon = () => {
3311
+ this.activeCommand = null;
3312
+ this.inFlight--;
3110
3313
  };
3111
- this.ledger.push(entry);
3112
- if (this.ledger.length > LEDGER_MAX) this.ledger.shift();
3113
- this.inFlight++;
3314
+ if (this.state !== "ready") try {
3315
+ await this.start();
3316
+ } catch (error) {
3317
+ abandon();
3318
+ throw error;
3319
+ }
3320
+ if (this.state === "disposed") {
3321
+ abandon();
3322
+ throw new Error(`[E_SESSION_DISPOSED] remote pty session '${this.opts.id}' was disposed before dispatch`);
3323
+ }
3114
3324
  return await new Promise((resolve$1) => {
3115
3325
  const nonce = genNonce();
3116
3326
  const ring = new Utf16TailRing(OUTPUT_HARD_CAP);
3327
+ let traceChunkSeq = 0;
3328
+ trace("disp", {
3329
+ s: this.opts.id,
3330
+ key: this.opts.key,
3331
+ commandId,
3332
+ bg: dOpts.background === true,
3333
+ stdinGiven: dOpts.stdin !== void 0,
3334
+ cmdLen: cmd.length,
3335
+ stdinLen: dOpts.stdin?.length ?? 0,
3336
+ cmdRepr: traceRepr(cmd, 140),
3337
+ stdinRepr: traceRepr(dOpts.stdin ?? "", 60),
3338
+ state: this.state,
3339
+ priorCmds: this.cmdSeq
3340
+ });
3117
3341
  let interrupted = false;
3118
3342
  let settled = false;
3119
3343
  let jobSettled = false;
3120
3344
  let interruptTimer;
3121
3345
  let killTimer;
3346
+ let markerTimer;
3122
3347
  let jobDone = null;
3123
3348
  let envNext;
3124
3349
  const detach = () => {
@@ -3135,6 +3360,13 @@ var PtySession = class {
3135
3360
  };
3136
3361
  this.detachedJob = job;
3137
3362
  settled = true;
3363
+ trace("detach", {
3364
+ s: this.opts.id,
3365
+ commandId,
3366
+ outLen: ring.text.length,
3367
+ tail: traceRepr(ring.text, 220)
3368
+ });
3369
+ traceFlush();
3138
3370
  resolve$1({
3139
3371
  output: ring.text,
3140
3372
  outputStats: { stdout: ring.stats() },
@@ -3152,13 +3384,25 @@ var PtySession = class {
3152
3384
  const cleanup = () => {
3153
3385
  this.onChunk = null;
3154
3386
  this.detachedJob = null;
3387
+ this.activeCommand = null;
3155
3388
  this.deathWaiters = this.deathWaiters.filter((w) => w !== deathWaiter);
3156
3389
  if (interruptTimer !== void 0) clearTimeout(interruptTimer);
3157
3390
  if (killTimer !== void 0) clearTimeout(killTimer);
3391
+ if (markerTimer !== void 0) clearTimeout(markerTimer);
3158
3392
  };
3159
3393
  const settle = (r) => {
3160
3394
  if (jobSettled) return;
3161
3395
  jobSettled = true;
3396
+ trace("settle", {
3397
+ s: this.opts.id,
3398
+ commandId,
3399
+ exit: r.exit,
3400
+ timedOut: r.timedOut,
3401
+ cwd: r.cwd,
3402
+ outLen: r.output.length,
3403
+ tail: traceRepr(r.output, 260)
3404
+ });
3405
+ traceFlush();
3162
3406
  cleanup();
3163
3407
  this.inFlight--;
3164
3408
  this.idleSince = Date.now();
@@ -3166,9 +3410,6 @@ var PtySession = class {
3166
3410
  if (r.cwd !== null) this.lastCwdSeen = r.cwd;
3167
3411
  const envDelta = envNext !== void 0 ? diffEnv(this.envBaseline, envNext) : void 0;
3168
3412
  if (envNext !== void 0) this.envBaseline = envNext;
3169
- entry.exit = r.exit;
3170
- entry.durationMs = Date.now() - started;
3171
- entry.running = false;
3172
3413
  const full = {
3173
3414
  ...r,
3174
3415
  outputStats: { stdout: ring.stats() },
@@ -3194,6 +3435,13 @@ var PtySession = class {
3194
3435
  const parser = createNonceFrameParser(nonce, {
3195
3436
  onOutput: (t) => {
3196
3437
  ring.push(t);
3438
+ if (traceChunkSeq < 60) trace("out", {
3439
+ s: this.opts.id,
3440
+ commandId,
3441
+ n: traceChunkSeq++,
3442
+ len: t.length,
3443
+ text: traceRepr(t, 180)
3444
+ });
3197
3445
  },
3198
3446
  onEnv: (b64) => {
3199
3447
  const m = decodeEnvSnapshot(b64);
@@ -3209,7 +3457,9 @@ var PtySession = class {
3209
3457
  const twoStage = () => {
3210
3458
  interrupted = true;
3211
3459
  this.write("");
3212
- this.write(`printf '\\033]133;D;${nonce};130;%s\\007' "$PWD"\n`);
3460
+ markerTimer = setTimeout(() => {
3461
+ this.write(`printf '\\033]133;D;${nonce};130;%s\\007' "$PWD"\n`);
3462
+ }, MARKER_SETTLE_MS);
3213
3463
  killTimer = setTimeout(() => {
3214
3464
  this.killTree();
3215
3465
  this.markDead();
@@ -3222,8 +3472,41 @@ var PtySession = class {
3222
3472
  }, INTERRUPT_GRACE_MS);
3223
3473
  };
3224
3474
  this.onChunk = (c) => parser.feed(c);
3225
- this.write(wrapPtyCommand(cmd, nonce));
3226
- if (dOpts.stdin !== void 0) this.write(dOpts.stdin);
3475
+ const startup = dOpts.stdin === void 0 ? wrapPtyCommand(cmd, nonce) : wrapPtyCommand(cmd, nonce) + dOpts.stdin;
3476
+ const wStdin = this.proc?.stdin;
3477
+ if (wStdin?.writable === true) {
3478
+ const wl0 = wStdin.writableLength;
3479
+ let wret;
3480
+ try {
3481
+ wret = wStdin.write(startup);
3482
+ } catch (error) {
3483
+ trace("werr", {
3484
+ s: this.opts.id,
3485
+ commandId,
3486
+ msg: error instanceof Error ? error.message : String(error)
3487
+ });
3488
+ }
3489
+ if (wret !== void 0) {
3490
+ trace("w1", {
3491
+ s: this.opts.id,
3492
+ commandId,
3493
+ bytes: Buffer$1.byteLength(startup),
3494
+ ret: wret,
3495
+ wl0,
3496
+ wl1: wStdin.writableLength,
3497
+ payloadTail: traceRepr(startup.slice(-90), 120)
3498
+ });
3499
+ if (wret === false) wStdin.once("drain", () => {
3500
+ trace("drain", {
3501
+ s: this.opts.id,
3502
+ commandId
3503
+ });
3504
+ });
3505
+ }
3506
+ } else trace("wskip", {
3507
+ s: this.opts.id,
3508
+ commandId
3509
+ });
3227
3510
  this.deathWaiters.push(deathWaiter);
3228
3511
  if (dOpts.background === true) {
3229
3512
  detach();
@@ -3232,9 +3515,39 @@ var PtySession = class {
3232
3515
  interruptTimer = setTimeout(() => detach(), dOpts.timeoutSec === void 0 ? 12e4 : dOpts.timeoutSec * 1e3);
3233
3516
  });
3234
3517
  };
3235
- const run = this.queue.then(attempt, attempt);
3236
- this.queue = run.then(() => void 0, () => void 0);
3237
- return run;
3518
+ return attempt();
3519
+ }
3520
+ /** §3.1(2026-10-25)interactive-stdin:向当前运行中的命令追加 stdin——只写既有通道
3521
+ * 与身份,不新建命令、账本条目或输出游标;不重连、不重放(dead/disposed 明确拒绝)。
3522
+ * 校验与写入全同步:多次 input 按调用顺序整段写入不交错;写入失败如实抛错(私有
3523
+ * write() 的静默忽略不适用于回执语义)。stdin:"" 是零字节写入,不是 EOF。 */
3524
+ input(options) {
3525
+ const fail = (code, hint) => Promise.reject(/* @__PURE__ */ new Error(`${code} remote: ${hint}`));
3526
+ if (this.state === "disposed") return fail("[E_SESSION_DISPOSED]", `pty session '${displayKey(this.opts.key)}' was disposed — input never reaches a disposed session`);
3527
+ if (this.state === "dead") return fail("[E_SESSION_NOT_RUNNING]", `pty session '${displayKey(this.opts.key)}' died — input never reconnects or replays; start a new command (exec reconnects a fresh shell) and re-send only what is still needed`);
3528
+ if (this.state === "starting") return fail("[E_SESSION_NOT_READY]", `pty session '${displayKey(this.opts.key)}' is still initializing — wait for the exec receipt, then input`);
3529
+ const active = this.activeCommand;
3530
+ if (active === null) return fail("[E_SESSION_NOT_RUNNING]", `pty session '${displayKey(this.opts.key)}' has no running command (state ${this.state}) — input writes into a command started by exec; it is never an implicit shell command`);
3531
+ if (options.commandId !== void 0 && options.commandId !== active.commandId) return fail("[E_COMMAND_MISMATCH]", `commandId ${JSON.stringify(options.commandId)} does not match the running command ${active.commandId} — input aimed at an earlier prompt is never written to the next command`);
3532
+ const stdin = this.proc?.stdin;
3533
+ if (stdin?.writable !== true) return fail("[E_SESSION_WRITE_FAILED]", `pty session '${displayKey(this.opts.key)}' has no writable stdin channel (state ${this.state}) — follow the running command via job_output for its settled result`);
3534
+ try {
3535
+ stdin.write(options.stdin);
3536
+ trace("in", {
3537
+ s: this.opts.id,
3538
+ commandId: active.commandId,
3539
+ bytes: Buffer$1.byteLength(options.stdin, "utf8"),
3540
+ repr: traceRepr(options.stdin, 60)
3541
+ });
3542
+ } catch (error) {
3543
+ return fail("[E_SESSION_WRITE_FAILED]", `writing stdin to pty session '${displayKey(this.opts.key)}' failed: ${error instanceof Error ? error.message : String(error)}`);
3544
+ }
3545
+ return Promise.resolve({
3546
+ session: displayKey(this.opts.key),
3547
+ commandId: active.commandId,
3548
+ ...active.jobId !== void 0 ? { jobId: active.jobId } : {},
3549
+ writtenBytes: Buffer$1.byteLength(options.stdin, "utf8")
3550
+ });
3238
3551
  }
3239
3552
  /** TTL / row teardown:尽力 exit,1s 后杀组,出池。 */
3240
3553
  dispose() {
@@ -3242,7 +3555,13 @@ var PtySession = class {
3242
3555
  this.clearIdleTimer();
3243
3556
  this.clearDeathTimer();
3244
3557
  this.state = "disposed";
3558
+ trace("dispose", {
3559
+ s: this.opts.id,
3560
+ key: this.opts.key
3561
+ });
3562
+ traceFlush();
3245
3563
  this.startPromise = null;
3564
+ this.detachedJob?.interrupt();
3246
3565
  const proc = this.proc;
3247
3566
  const close = this.procClose;
3248
3567
  const group = this.reapGroup(proc?.pid, 1e3, close);
@@ -3253,7 +3572,7 @@ var PtySession = class {
3253
3572
  ]).then(() => {
3254
3573
  if (this.proc === proc) this.proc = null;
3255
3574
  });
3256
- this.write("exit\n");
3575
+ if (this.detachedJob === null) this.write("exit\n");
3257
3576
  this.opts.onDead?.();
3258
3577
  return this.disposal;
3259
3578
  }
@@ -3287,6 +3606,12 @@ var PtySession = class {
3287
3606
  detached: true
3288
3607
  });
3289
3608
  this.proc = proc;
3609
+ trace("start", {
3610
+ s: this.opts.id,
3611
+ key: this.opts.key,
3612
+ argv: this.opts.argv,
3613
+ pid: proc.pid
3614
+ });
3290
3615
  proc.stdout.setEncoding("utf8");
3291
3616
  proc.stdout.on("data", (d) => {
3292
3617
  this.onChunk?.(d);
@@ -3294,6 +3619,9 @@ var PtySession = class {
3294
3619
  proc.stderr.setEncoding("utf8").on("data", (d) => {
3295
3620
  this.diag = (this.diag + d).slice(-2e3);
3296
3621
  });
3622
+ proc.stdin.on("error", (err) => {
3623
+ this.diag = (this.diag + `\n[remote: stdin write failed: ${err.message}]`).slice(-2e3);
3624
+ });
3297
3625
  this.procClose = new Promise((resolve$1) => {
3298
3626
  proc.once("close", () => {
3299
3627
  this.markDead();
@@ -3345,6 +3673,11 @@ var PtySession = class {
3345
3673
  return;
3346
3674
  }
3347
3675
  this.state = "dead";
3676
+ trace("dead", {
3677
+ s: this.opts.id,
3678
+ diagTail: this.diag.slice(-500)
3679
+ });
3680
+ traceFlush();
3348
3681
  this.startPromise = null;
3349
3682
  this.clearIdleTimer();
3350
3683
  this.armDeathReaper();
@@ -3557,42 +3890,20 @@ function parseTargetForm(target) {
3557
3890
  form: "hub",
3558
3891
  machine: segs[0]
3559
3892
  };
3560
- if (segs.length === 2 && segs[1] === "pty") return {
3561
- form: "hub-pty",
3562
- machine: segs[0]
3563
- };
3564
- throw new TargetFormatError(`[E_TARGET_FORMAT] remote: ${JSON.stringify(target)} — hub PTY takes no label (hub lane has no session id / parallelism this round)`);
3893
+ if (segs.length > 1) throw new TargetFormatError(`[E_TARGET_FORMAT] remote: ${JSON.stringify(target)} — hub takes a machine id (hub:<m4>); a hub machine's exec opens its default PTY, and handoff detaches it as pty:<id>`);
3565
3894
  }
3566
3895
  for (const transport of TRANSPORTS) {
3567
3896
  if (!target.startsWith(`${transport}:`)) continue;
3568
3897
  const rest = target.slice(transport.length + 1);
3569
3898
  if (rest.length === 0) throw new TargetFormatError(`[E_TARGET_FORMAT] remote: ${JSON.stringify(target)} — ${transport} takes a name`);
3570
- const ptyAt = rest.lastIndexOf(":pty");
3571
- if (ptyAt === -1) return {
3899
+ if (/(^|:)pty(:|$)/.test(rest)) throw new TargetFormatError(`[E_TARGET_FORMAT] remote: ${JSON.stringify(target)} — the :pty creation suffix is gone; a machine's exec opens its default PTY and handoff detaches it as pty:<id>`);
3900
+ return {
3572
3901
  form: "direct",
3573
3902
  transport,
3574
3903
  name: rest
3575
3904
  };
3576
- const base = rest.slice(0, ptyAt);
3577
- if (base.length === 0) throw new TargetFormatError(`[E_TARGET_FORMAT] remote: ${JSON.stringify(target)} — ":pty" needs a transport target before it ("ssh:pty" is the host named pty)`);
3578
- const tail = rest.slice(ptyAt + 4);
3579
- if (tail.startsWith(":")) {
3580
- if (tail.length === 1) throw new TargetFormatError(`[E_TARGET_FORMAT] remote: ${JSON.stringify(target)} — empty label after ":pty:"`);
3581
- return {
3582
- form: "create-pty",
3583
- transport,
3584
- name: base,
3585
- label: tail.slice(1)
3586
- };
3587
- }
3588
- if (tail.length > 0) throw new TargetFormatError(`[E_TARGET_FORMAT] remote: ${JSON.stringify(target)} — unexpected ${JSON.stringify(tail)} after ":pty"`);
3589
- return {
3590
- form: "create-pty",
3591
- transport,
3592
- name: base
3593
- };
3594
3905
  }
3595
- if (target.includes(":")) throw new TargetFormatError(`[E_TARGET_FORMAT] remote: unknown target ${JSON.stringify(target)} — transports are ssh: / docker: / incus: / hub:, the PTY selector is pty:<id|label>, creation appends :pty (roster lists names)`);
3906
+ if (target.includes(":")) throw new TargetFormatError(`[E_TARGET_FORMAT] remote: unknown target ${JSON.stringify(target)} — transports are ssh: / docker: / incus: / hub:, the PTY selector is pty:<id|label>; a machine's exec opens its default PTY and handoff detaches it (the old :pty creation suffix is gone)`);
3596
3907
  return {
3597
3908
  form: "bare",
3598
3909
  name: target
@@ -3736,7 +4047,11 @@ async function probeTarget(plan, opts = {}) {
3736
4047
  const runner = opts.runner ?? runOneShot;
3737
4048
  const started = Date.now();
3738
4049
  const r = await runner(plan.kind === "ssh" ? sshArgv(plan.connection ?? { host: plan.host }, {
3739
- extraArgs: ["-o", `ConnectTimeout=${ct}`],
4050
+ extraArgs: [
4051
+ ...opts.muxArgvFor?.(plan) ?? [],
4052
+ "-o",
4053
+ `ConnectTimeout=${ct}`
4054
+ ],
3740
4055
  command: "true"
3741
4056
  }) : plan.kind === "docker" ? [
3742
4057
  "docker",
@@ -3795,12 +4110,6 @@ function renderProbe(p) {
3795
4110
  case "container": return `probe: container ${p.status} (just now, on demand)`;
3796
4111
  }
3797
4112
  }
3798
- /** session card(§5.5,`pty:<id|label>` 无 cmd):connection/state/cwd/ttl 由调用方拼头行,这里渲染账本。 */
3799
- function renderLedger(ledger) {
3800
- if (ledger.length === 0) return "ledger: empty — no command has run in this session yet";
3801
- const rows = [...ledger].reverse().map((e) => ` ${e.commandId}${e.jobId !== void 0 && e.jobId !== e.commandId ? ` (job ${e.jobId})` : ""} · ${e.running ? "running" : `exit ${e.exit}`}${e.running ? "" : ` · ${(e.durationMs / 1e3).toFixed(1)}s`} · ${e.summary}`);
3802
- return `ledger (last ${ledger.length}):\n${rows.join("\n")}`;
3803
- }
3804
4113
 
3805
4114
  //#endregion
3806
4115
  //#region src/remote/ssh-config.ts
@@ -3951,7 +4260,7 @@ async function scanLocal(opts = {}) {
3951
4260
  };
3952
4261
  }
3953
4262
  /** 统一池视图的 roster 呈现:机器三段 + BYO ssh 同位(built on)+ pty 会话段(caller-scoped)。 */
3954
- function renderRoster(scan, entries, sessions, hubTargets) {
4263
+ function renderRoster(scan, entries, sessions, hubTargets, defaults) {
3955
4264
  const lines = [];
3956
4265
  if (hubTargets !== null) lines.push(`hub machines: ${hubTargets.map((t) => `hub:${t.id} (${t.label}${t.execEnabled ? "" : "; exec disabled"})`).join(", ") || "none"}`);
3957
4266
  if (scan.ssh.error !== void 0) lines.push(`ssh hosts: no readable ~/.ssh/config (${scan.ssh.error})`);
@@ -3973,7 +4282,7 @@ function renderRoster(scan, entries, sessions, hubTargets) {
3973
4282
  const id = s.identity;
3974
4283
  const snap = s.snapshot;
3975
4284
  const state = snap.busy ? "busy" : snap.state === "dead" ? "died" : `idle ${Math.round((snap.idleMs ?? 0) / 1e3)}s`;
3976
- return `pty:${id.id}${id.label !== void 0 ? ` (label ${id.label})` : ""} ${id.connection}${id.byo ? " (byo-pty)" : ""} [${state}]`;
4285
+ return `pty:${id.id}${id.label !== void 0 ? ` (label ${id.label})` : ""} ${id.connection}${id.byo ? " (byo-pty)" : ""} [${state}${defaults?.get(id.id) !== void 0 ? `; default on ${defaults.get(id.id)}` : ""}]`;
3977
4286
  }).join(" · ")}` : "pty sessions: none");
3978
4287
  return lines.join("\n");
3979
4288
  }
@@ -3983,12 +4292,28 @@ function renderRoster(scan, entries, sessions, hubTargets) {
3983
4292
  /** R1:idle TTL 15 min——idle 随活动重置,在用会话永不超时(PtySession 双条件)。 */
3984
4293
  const REMOTE_PTY_IDLE_TTL_SEC = 900;
3985
4294
  /** P3(user 2026-10-19):每 agent 10 个 PTY 会话,跨所有 connection 统一计;仅 error message 披露。 */
3986
- const REMOTE_PTY_SESSION_LIMIT = 10;
4295
+ /** 二/三轮裁决(2026-11-01):每 Agent session 全局 30 个 active PTY(默认/handoff/BYO 全计入);
4296
+ * 仅触达上限时的错误回执与 roster 可见性披露,任何模型可见 surface 不预先公布。 */
4297
+ const REMOTE_PTY_SESSION_LIMIT = 30;
4298
+ /** hub 线 one-shot 监督披露(0.3.2-c):对端 = 我们自己的 hub server(bash -lc 于独立进程组,
4299
+ * Node 原生 TERM→2s→KILL + 输出环 + 租约生命周期);superd-run helper 是 dumb-pipe 专用物,
4300
+ * 从不经 hub 上传(non-goal,docs/10_plans/2026-10-25-hub-remote-parity.md §3 O1/O8)。 */
4301
+ const HUB_RUNNER = "hub-lease (peer bash -lc, peer Node supervision)";
4302
+ /** hub PTY 泵资产:pump 与本模块同目录(npm 包与 Super 闭包同构,helper.ts 同款纪律)。
4303
+ * ws 模块路径由 transport 侧(superd-hub 上下文,分叉的 proven 解析链)解析后经
4304
+ * ptyEndpoint 传入——闭包自身子树无可解析的 ws,按名解析不在闭包里做(0.3.2-c 定稿)。 */
4305
+ function hubPtyAssets() {
4306
+ return { pumpPath: join(dirname(fileURLToPath(import.meta.url)), "hub-pty-pump.mjs") };
4307
+ }
3987
4308
  /** 账本键与 jobId 同构(同格式 `<kind>-<16hex>`);background 通道预传入 registry id 时两者字面相等。 */
3988
4309
  const genCommandId = () => `remote-${randomUUID().replace(/-/g, "").slice(0, 16)}`;
3989
4310
  /** 双轨调度器:参数校验 → 目标语法(parseTargetForm)→ 统一 label 池 → oneshot 子进程 / pty 会话池。 */
3990
4311
  var RemoteDriver = class {
3991
4312
  pool;
4313
+ /** 默认 PTY 槽(spec remote-default-pty §1):sessionKey → 归一 machineKey → pool key。
4314
+ * 会话本体是普通 PtySession(id 创建即认领,roster 全可见);槽登记在 dispose/TTL 后悬空,
4315
+ * 取槽时经 pool.get(disposed 视同出池)惰性清理。 */
4316
+ defaultSlots = /* @__PURE__ */ new Map();
3992
4317
  labels = new LabelPool();
3993
4318
  hubOwner = randomUUID();
3994
4319
  hubProviders = /* @__PURE__ */ new Set();
@@ -4001,8 +4326,10 @@ var RemoteDriver = class {
4001
4326
  runnerOf = /* @__PURE__ */ new WeakMap();
4002
4327
  disposal;
4003
4328
  ptyIdleTtlSec;
4329
+ mux;
4004
4330
  constructor(opts = {}) {
4005
4331
  this.opts = opts;
4332
+ this.mux = opts.sshMux ?? new SshMux();
4006
4333
  this.ptyIdleTtlSec = opts.idleTtlSec ?? REMOTE_PTY_IDLE_TTL_SEC;
4007
4334
  this.pool = new PtyPool({ idleTtlSec: this.ptyIdleTtlSec });
4008
4335
  }
@@ -4041,38 +4368,24 @@ var RemoteDriver = class {
4041
4368
  const hasSsh = params.ssh !== void 0;
4042
4369
  if (hasSpawn && hasSsh) throw new Error("[E_PARAMS] remote: spawn and ssh are two different BYO entry points — pass one, not both");
4043
4370
  const ptyForm = form.form === "pty-selector" || form.form === "empty" || form.form === "bare";
4044
- const sshForm = form.form === "ssh-prefix" || form.form === "empty" || form.form === "bare" || form.form === "direct" && form.transport === "ssh" || form.form === "create-pty" && form.transport === "ssh";
4371
+ const sshForm = form.form === "ssh-prefix" || form.form === "empty" || form.form === "bare" || form.form === "direct" && form.transport === "ssh";
4045
4372
  if (hasSpawn && !ptyForm) throw new Error("[E_PARAMS] remote: spawn pairs only with a pty target (pty, pty:<label>, or a bare label) — machines are dialed, not spawned");
4046
- if (hasSsh && !sshForm) throw new Error("[E_PARAMS] remote: ssh registration pairs with ssh-form targets only (ssh, ssh:<label>, ssh:<label>:pty, a bare label, or no target)");
4373
+ if (hasSsh && !sshForm) throw new Error("[E_PARAMS] remote: ssh registration pairs with ssh-form targets only (ssh, ssh:<label>, a bare label, or no target)");
4047
4374
  }
4048
4375
  async execute(params, form, callCtx) {
4049
4376
  const timeoutSec = params.timeout ?? this.opts.execTimeoutSec ?? 120;
4050
- const cap = this.opts.maxOutputChars ?? 3e4;
4051
4377
  const sessionKey = callCtx.sessionKey ?? "no-session";
4052
4378
  const hasCmd = params.cmd !== void 0 && params.cmd.length > 0;
4053
- params.spawn !== void 0 && params.spawn.length;
4054
- if (form.form === "hub" || form.form === "hub-pty") {
4379
+ const hasSpawn = params.spawn !== void 0 && params.spawn.length > 0;
4380
+ const bareHitsPty = form.form === "bare" && this.labels.resolve(form.name, sessionKey)?.kind === "pty";
4381
+ if ((form.form === "direct" || form.form === "hub" || form.form === "bare" && !bareHitsPty) && params.mode !== "oneshot" && params.ssh === void 0 && !hasSpawn) {
4382
+ const { session } = await this.defaultSlot(form, sessionKey);
4383
+ if (!hasCmd) return this.cardReceipt(session);
4384
+ return await this.dispatchPty(session, params, callCtx, timeoutSec);
4385
+ }
4386
+ if (form.form === "hub") {
4055
4387
  const hub = this.requireHub();
4056
4388
  this.hubProviders.add(hub);
4057
- if (form.form === "hub-pty") {
4058
- if (!hasCmd) throw new Error("[E_PARAMS] remote: hub PTY takes cmd");
4059
- const result$1 = await hub.pty(form.machine, params.cmd, this.hubOwner, {
4060
- stdin: params.stdin,
4061
- timeout: timeoutSec
4062
- });
4063
- const out$1 = tailWindow(result$1.stdout, cap, result$1.outputStats?.stdout);
4064
- return {
4065
- ...result$1,
4066
- target: `hub:${form.machine}`,
4067
- connection: `hub:${form.machine}`,
4068
- stdout: `[hub:${form.machine} pty — no session id, no parallel, no background]\n${out$1.text}`,
4069
- ...out$1.stats ? { outputStats: {
4070
- ...result$1.outputStats,
4071
- stdout: out$1.stats
4072
- } } : {},
4073
- ...out$1.truncated === void 0 ? {} : { truncated: out$1.truncated }
4074
- };
4075
- }
4076
4389
  const process$1 = this.own(await hub.start(form.machine, {
4077
4390
  ...params,
4078
4391
  target: `hub:${form.machine}`,
@@ -4082,8 +4395,8 @@ var RemoteDriver = class {
4082
4395
  if (this.disposed) process$1.cancel("owner disposed");
4083
4396
  const result = await process$1.done;
4084
4397
  const stdout = process$1.read("stdout", 0), stderr = process$1.read("stderr", 0);
4085
- const out = tailWindow(stdout.text, cap, stdout.stats);
4086
- const err = tailWindow(stderr.text, cap, stderr.stats);
4398
+ const out = disclose(stdout.text, stdout.stats);
4399
+ const err = disclose(stderr.text, stderr.stats);
4087
4400
  return {
4088
4401
  ...result,
4089
4402
  mode: "oneshot",
@@ -4091,6 +4404,7 @@ var RemoteDriver = class {
4091
4404
  connection: `hub:${form.machine}`,
4092
4405
  cwd: null,
4093
4406
  reconnected: false,
4407
+ runner: HUB_RUNNER,
4094
4408
  stdout: (!stdout.stats && stdout.lossy ? "[truncated: peer returned only a tail; exact total/retained/dropped unavailable]\n" : "") + out.text,
4095
4409
  stderr: [err.text, stderr.diagnostic].filter(Boolean).join("\n"),
4096
4410
  ...out.stats ? { outputStats: {
@@ -4107,7 +4421,7 @@ var RemoteDriver = class {
4107
4421
  await session.warm();
4108
4422
  return this.warmReceipt(session);
4109
4423
  }
4110
- return await this.dispatchPty(session, params, callCtx, timeoutSec, cap);
4424
+ return await this.dispatchPty(session, params, callCtx, timeoutSec);
4111
4425
  }
4112
4426
  if (params.ssh !== void 0) {
4113
4427
  const label = form.form === "direct" || form.form === "bare" ? form.name : void 0;
@@ -4128,28 +4442,28 @@ var RemoteDriver = class {
4128
4442
  kind: "ssh",
4129
4443
  host: normalizeSshConnection(params.ssh).host,
4130
4444
  connection: normalizeSshConnection(params.ssh)
4131
- }, `ssh:${name$1}`, params, timeoutSec, cap);
4445
+ }, `ssh:${name$1}`, params, timeoutSec);
4132
4446
  }
4133
4447
  if (form.form === "pty-selector" && form.idOrLabel === void 0) throw new Error("[E_PARAMS] remote: \"pty\" alone selects nothing — append an id/label (pty:pty-1a2b, pty:build) or pass spawn to create one");
4134
4448
  if (form.form === "pty-selector") {
4135
4449
  const session = this.resolvePty(form.idOrLabel, sessionKey);
4136
4450
  if (!hasCmd) return this.cardReceipt(session);
4137
- return await this.dispatchPty(session, params, callCtx, timeoutSec, cap);
4451
+ return await this.dispatchPty(session, params, callCtx, timeoutSec);
4138
4452
  }
4139
4453
  if (form.form === "direct") {
4140
4454
  const plan$1 = form.transport === "ssh" ? this.sshPlanFor(form.name, sessionKey) : resolveTarget(`${form.transport}:${form.name}`);
4141
- return await this.oneshot(plan$1, `${form.transport}:${form.name}`, params, timeoutSec, cap);
4455
+ return await this.oneshot(plan$1, `${form.transport}:${form.name}`, params, timeoutSec);
4142
4456
  }
4143
4457
  if (form.form !== "bare") throw new Error("[E_INTERNAL] remote: unreachable target form after routing");
4144
4458
  const entry = await this.resolvePoolEntry(form.name, sessionKey);
4145
4459
  if (entry.kind === "pty") {
4146
4460
  const session = this.resolvePty(form.name, sessionKey);
4147
4461
  if (!hasCmd) return this.cardReceipt(session);
4148
- return await this.dispatchPty(session, params, callCtx, timeoutSec, cap);
4462
+ return await this.dispatchPty(session, params, callCtx, timeoutSec);
4149
4463
  }
4150
- if (entry.kind === "hub") return await this.hubOneshot(entry.machine, `hub:${entry.machine}`, params, timeoutSec, cap);
4464
+ if (entry.kind === "hub") return await this.hubOneshot(entry.machine, `hub:${entry.machine}`, params, timeoutSec);
4151
4465
  const plan = this.planOf(entry);
4152
- return await this.oneshot(plan, form.name, params, timeoutSec, cap);
4466
+ return await this.oneshot(plan, form.name, params, timeoutSec);
4153
4467
  }
4154
4468
  /** Native-first copy routing (spec: remote-native-shortcut) — unchanged by the workbench round. */
4155
4469
  copy(request, signal, callCtx = {}) {
@@ -4265,7 +4579,7 @@ var RemoteDriver = class {
4265
4579
  const parsed = parseFileEndpoint(value);
4266
4580
  if (!parsed.target) return { path: parsed.path };
4267
4581
  const form = parseTargetForm(parsed.target);
4268
- if (form.form === "hub" || form.form === "hub-pty" || form.form === "pty-selector") throw new Error("rsync supports SSH endpoints only (plus one local endpoint); no mixed-transport or copy fallback");
4582
+ if (form.form === "hub" || form.form === "pty-selector") throw new Error("rsync supports SSH endpoints only (plus one local endpoint); no mixed-transport or copy fallback");
4269
4583
  const plan = await this.resolveConnectionTarget(parsed.target, callCtx);
4270
4584
  this.assertOpen();
4271
4585
  combined.throwIfAborted();
@@ -4307,49 +4621,68 @@ var RemoteDriver = class {
4307
4621
  const plan = form.transport === "ssh" ? this.sshPlanFor(form.name, callCtx.sessionKey ?? "no-session") : resolveTarget(`${form.transport}:${form.name}`);
4308
4622
  this.assertOpen();
4309
4623
  signal.throwIfAborted();
4310
- process$1 = await this.nativeProcess(plan, params, params.timeout, signal);
4624
+ process$1 = await this.nativeProcess(plan, `${form.transport}:${form.name}`, params, params.timeout, signal);
4311
4625
  } else if (form.form === "bare") {
4312
4626
  const entry = await this.resolvePoolEntry(form.name, callCtx.sessionKey ?? "no-session");
4313
4627
  this.assertOpen();
4314
4628
  signal.throwIfAborted();
4315
4629
  if (entry.kind === "pty" || entry.kind === "hub") throw new Error("remote: background requires an ssh/docker/incus target");
4316
- process$1 = await this.nativeProcess(this.planOf(entry), params, params.timeout, signal);
4630
+ process$1 = await this.nativeProcess(this.planOf(entry), form.name, params, params.timeout, signal);
4317
4631
  } else throw new Error("remote: background oneshot takes a machine target");
4318
4632
  this.own(process$1);
4319
4633
  if (signal.aborted || this.disposed) process$1.cancel("owner closed during start");
4320
4634
  return process$1;
4321
4635
  }
4322
- async nativeProcess(plan, params, timeoutSec, signal = this.fileAbort.signal) {
4636
+ async nativeProcess(plan, display, params, timeoutSec, signal = this.fileAbort.signal) {
4637
+ const muxArgv = this.muxArgvFor(plan, display);
4323
4638
  const cleanupTag = randomUUID().slice(0, 8);
4324
- const helper = await this.ensureHelper(plan);
4639
+ const helper = await this.ensureHelper(plan, muxArgv);
4325
4640
  if (helper !== null && !this.disposed) {
4641
+ let killed$1 = false;
4642
+ const kill$1 = () => {
4643
+ if (killed$1) return;
4644
+ killed$1 = true;
4645
+ startOneShot(buildHelperKillgroupArgv(plan, helper.binPath, cleanupTag, muxArgv), { timeoutSec: 15 }).done.catch(() => {});
4646
+ };
4326
4647
  const process$2 = startOneShot(buildHelperOneShotArgv(plan, helper.binPath, {
4327
4648
  tag: cleanupTag,
4328
- cmd: params.cmd
4649
+ cmd: params.cmd,
4650
+ ...muxArgv !== void 0 ? { muxArgv } : {}
4329
4651
  }), {
4330
4652
  stdin: params.stdin,
4331
- timeoutSec
4653
+ timeoutSec,
4654
+ onCancel: kill$1
4332
4655
  });
4333
4656
  this.runnerOf.set(process$2, `superd-run ${helper.ver} (${helper.triple})`);
4334
4657
  process$2.done.then((result) => {
4335
4658
  if (!result.cancelled && !result.timedOut) return;
4336
- startOneShot(buildHelperKillgroupArgv(plan, helper.binPath, cleanupTag), { timeoutSec: 15 }).done.catch(() => {});
4659
+ kill$1();
4337
4660
  }).catch(() => {});
4338
4661
  const tracked = Promise.resolve(process$2);
4339
4662
  this.starting.add(tracked);
4340
4663
  tracked.then(() => this.starting.delete(tracked), () => this.starting.delete(tracked));
4341
4664
  return tracked;
4342
4665
  }
4343
- const process$1 = startOneShot((this.opts.oneshotArgvFor ?? buildOneShotArgv)(plan, params.cmd, { cleanupTag }), {
4344
- stdin: params.stdin,
4345
- timeoutSec
4666
+ let killed = false;
4667
+ const kill = () => {
4668
+ if (killed) return;
4669
+ killed = true;
4670
+ const pidfile = `"\${TMPDIR:-/tmp}/superd-oneshot-${cleanupTag}.pgid"`;
4671
+ const cleanup = `p=$(cat ${pidfile} 2>/dev/null) && { kill -TERM -- "-$p" 2>/dev/null; sleep 0.3; kill -KILL -- "-$p" 2>/dev/null; }; rm -f ${pidfile}`;
4672
+ startOneShot((this.opts.oneshotArgvFor ?? buildOneShotArgv)(plan, cleanup, { ...muxArgv !== void 0 ? { muxArgv } : {} }), { timeoutSec: 15 }).done.catch(() => {});
4673
+ };
4674
+ const process$1 = startOneShot((this.opts.oneshotArgvFor ?? buildOneShotArgv)(plan, params.cmd, {
4675
+ cleanupTag,
4676
+ ...muxArgv !== void 0 ? { muxArgv } : {}
4677
+ }), {
4678
+ stdin: params.stdin,
4679
+ timeoutSec,
4680
+ onCancel: kill
4346
4681
  });
4347
4682
  this.runnerOf.set(process$1, "posix-shim");
4348
4683
  process$1.done.then((result) => {
4349
4684
  if (!result.cancelled && !result.timedOut) return;
4350
- const pidfile = `"\${TMPDIR:-/tmp}/superd-oneshot-${cleanupTag}.pgid"`;
4351
- const cleanup = `p=$(cat ${pidfile} 2>/dev/null) && { kill -TERM -- "-$p" 2>/dev/null; sleep 0.3; kill -KILL -- "-$p" 2>/dev/null; }; rm -f ${pidfile}`;
4352
- startOneShot((this.opts.oneshotArgvFor ?? buildOneShotArgv)(plan, cleanup, {}), { timeoutSec: 15 }).done.catch(() => {});
4685
+ kill();
4353
4686
  }).catch(() => {});
4354
4687
  const work = Promise.resolve(process$1);
4355
4688
  this.starting.add(work);
@@ -4358,14 +4691,14 @@ var RemoteDriver = class {
4358
4691
  }
4359
4692
  /** Helper bootstrap is best-effort and sticky: any failure parks null for this driver
4360
4693
  * lifetime and the shim stays the path. First call to a target pays probe (+ upload). */
4361
- async ensureHelper(plan) {
4694
+ async ensureHelper(plan, muxArgv) {
4362
4695
  if (this.helpers === void 0) {
4363
4696
  const assets = this.opts.helperAssetsFor?.() ?? helperAssets();
4364
4697
  this.helpers = assets === null ? null : new HelperRegistry(assets, (argv, options) => this.helperExec(argv, options), buildHelperProbeArgv, buildHelperUploadArgv);
4365
4698
  }
4366
4699
  if (this.helpers === null || this.disposed) return null;
4367
4700
  try {
4368
- return await this.helpers.ensure(plan);
4701
+ return await this.helpers.ensure(plan, muxArgv ?? []);
4369
4702
  } catch {
4370
4703
  return null;
4371
4704
  }
@@ -4478,6 +4811,16 @@ var RemoteDriver = class {
4478
4811
  host: name$1
4479
4812
  };
4480
4813
  }
4814
+ /** Task E MUX 身份:仅 SSH plan 注入(docker/incus/hub 返回 undefined)。身份键 = 原始
4815
+ * target 串 + 有效连接字段(host/user/port/key/ProxyJump)——同身份共享 master,跨身份
4816
+ * 不串 socket;目录/寿命/拆除全部由 SshMux 自持。 */
4817
+ muxArgvFor(plan, target) {
4818
+ if (plan.kind !== "ssh") return void 0;
4819
+ return this.mux.argvFor({
4820
+ target,
4821
+ connection: plan.connection ?? { host: plan.host }
4822
+ });
4823
+ }
4481
4824
  /** BYO ssh 注册(v2:无 spec 对比——label 占用即拒;注册成功才认领)。返回实际名(label 或 s-id)。 */
4482
4825
  registerByoSsh(label, connection, sessionKey) {
4483
4826
  this.assertOpen();
@@ -4507,7 +4850,8 @@ var RemoteDriver = class {
4507
4850
  const live = this.pool.list().filter(({ key: key$1 }) => key$1.startsWith(`${sessionKey}|`)).map(({ key: key$1 }) => this.pool.get(key$1)).filter((s) => s !== void 0 && s.sessionState !== "dead");
4508
4851
  if (live.length >= REMOTE_PTY_SESSION_LIMIT) {
4509
4852
  const listing = live.map(sessionLine).join(" · ");
4510
- throw new Error(`[E_SESSION_LIMIT] remote: this Agent already holds ${live.length} live PTY sessions (limit ${REMOTE_PTY_SESSION_LIMIT}) — close one ({action:"close", target:"pty:<id>"}); idle sessions are reaped after ${this.ttlMinutes()} min.\n live: ${listing}`);
4853
+ throw new Error(`[E_SESSION_LIMIT] remote: this Agent already holds ${live.length} live PTY sessions — the per-Agent session global limit (${REMOTE_PTY_SESSION_LIMIT}) is reached. Check the live sessions below (or {action:"status"} for the roster), then close one ({action:"close", target:"pty:<id>" or the machine name}), kill its running job (job_kill), reuse a session, or run {mode:"oneshot"} instead.
4854
+ live: ${listing}`);
4511
4855
  }
4512
4856
  const { label } = opts;
4513
4857
  if (label !== void 0) validateLabel(label);
@@ -4556,7 +4900,7 @@ var RemoteDriver = class {
4556
4900
  isBuiltOn(connection, sessionKey) {
4557
4901
  return connection.startsWith("ssh:") && this.labels.resolve(connection.slice(4), sessionKey)?.kind === "ssh-byo";
4558
4902
  }
4559
- /** PTY 创建形唯一实现(execute 与 background 预建共用):spawn BYO / ssh BYO / native `<transport>:pty`。 */
4903
+ /** PTY 创建形唯一实现(execute 与 background 预建共用):spawn BYO(spec 2026-11-01:native `:pty`/hub-pty 创建语法已删,machine 会话经默认槽 + handoff)。 */
4560
4904
  createPtyLane(params, form, sessionKey) {
4561
4905
  if (params.spawn !== void 0 && params.spawn.length > 0) {
4562
4906
  const label = form.form === "pty-selector" && form.idOrLabel !== void 0 ? form.idOrLabel : form.form === "bare" ? form.name : void 0;
@@ -4569,30 +4913,246 @@ var RemoteDriver = class {
4569
4913
  origin: params.spawn
4570
4914
  }, sessionKey) };
4571
4915
  }
4572
- if (params.ssh !== void 0 && form.form === "create-pty") {
4573
- const name$1 = this.registerByoSsh(form.name, params.ssh, sessionKey);
4574
- const connection = normalizeSshConnection(params.ssh);
4575
- const plan = {
4576
- kind: "ssh",
4577
- host: connection.host,
4578
- connection
4916
+ }
4917
+ /** 池条目的槽键归一:同一实体(host/container/machine 身份)跨裸名/前缀/撞名前缀共享同一默认槽。 */
4918
+ machineKeyOfEntry(entry) {
4919
+ switch (entry.kind) {
4920
+ case "ssh": return `ssh:${entry.host}`;
4921
+ case "ssh-byo": return `byo:${entry.name}`;
4922
+ case "docker": return `docker:${entry.container}`;
4923
+ case "incus": return `incus:${entry.container}`;
4924
+ case "hub": return `hub:${entry.machine}`;
4925
+ case "pty": throw new Error("remote: a PTY session is not a machine slot");
4926
+ }
4927
+ }
4928
+ /** 默认槽的 machine 规格:归一 key + 创建 argv + 展示 connection(hub 每次经 ptyEndpoint 重验 pairing,P10)。 */
4929
+ async machineSlotSpec(form, sessionKey) {
4930
+ if (form.form === "hub") {
4931
+ const hub = this.requireHub();
4932
+ this.hubProviders.add(hub);
4933
+ const endpoint = hub.ptyEndpoint(form.machine);
4934
+ return {
4935
+ key: `hub:${form.machine}`,
4936
+ argv: this.opts.hubPtyArgvFor !== void 0 ? this.opts.hubPtyArgvFor(endpoint) : buildHubPtyArgv(endpoint, hubPtyAssets()),
4937
+ connection: `hub:${form.machine}`
4579
4938
  };
4580
- return { session: this.createSession({
4939
+ }
4940
+ if (form.form === "direct") {
4941
+ if (form.transport === "ssh") {
4942
+ const entry = this.labels.resolve(form.name, sessionKey);
4943
+ if (entry?.kind === "ssh-byo") {
4944
+ const connection = entry.connection;
4945
+ return {
4946
+ key: `byo:${entry.name}`,
4947
+ argv: (this.opts.ptyArgvFor ?? buildPtyArgv)({
4948
+ kind: "ssh",
4949
+ host: connection.host,
4950
+ connection
4951
+ }),
4952
+ connection: `ssh:${entry.name}`,
4953
+ builtOn: true
4954
+ };
4955
+ }
4956
+ return {
4957
+ key: `ssh:${form.name}`,
4958
+ argv: (this.opts.ptyArgvFor ?? buildPtyArgv)({
4959
+ kind: "ssh",
4960
+ host: form.name
4961
+ }),
4962
+ connection: `ssh:${form.name}`
4963
+ };
4964
+ }
4965
+ const plan = resolveTarget(`${form.transport}:${form.name}`);
4966
+ return {
4967
+ key: `${form.transport}:${form.name}`,
4581
4968
  argv: (this.opts.ptyArgvFor ?? buildPtyArgv)(plan),
4582
- connection: `ssh:${name$1}`,
4583
- byo: false,
4584
- ...form.label !== void 0 ? { label: form.label } : {}
4585
- }, sessionKey) };
4969
+ connection: `${form.transport}:${form.name}`
4970
+ };
4586
4971
  }
4587
- if (form.form === "create-pty") {
4588
- const plan = form.transport === "ssh" ? this.sshPlanFor(form.name, sessionKey) : resolveTarget(`${form.transport}:${form.name}`);
4589
- return { session: this.createSession({
4972
+ if (form.form === "bare") {
4973
+ const entry = await this.resolvePoolEntry(form.name, sessionKey);
4974
+ this.assertOpen();
4975
+ if (entry.kind === "hub") return this.machineSlotSpec({
4976
+ form: "hub",
4977
+ machine: entry.machine
4978
+ }, sessionKey);
4979
+ if (entry.kind === "pty") throw new Error("[E_INTERNAL] remote: bare pty entries route before machine slots");
4980
+ const key = this.machineKeyOfEntry(entry);
4981
+ if (entry.kind === "ssh") return {
4982
+ key,
4983
+ argv: (this.opts.ptyArgvFor ?? buildPtyArgv)({
4984
+ kind: "ssh",
4985
+ host: entry.host
4986
+ }),
4987
+ connection: `ssh:${entry.name}`
4988
+ };
4989
+ if (entry.kind === "ssh-byo") return {
4990
+ key,
4991
+ argv: (this.opts.ptyArgvFor ?? buildPtyArgv)({
4992
+ kind: "ssh",
4993
+ host: entry.connection.host,
4994
+ connection: entry.connection
4995
+ }),
4996
+ connection: `ssh:${entry.name}`,
4997
+ builtOn: true
4998
+ };
4999
+ const plan = entry.kind === "docker" ? resolveTarget(`docker:${entry.container}`) : resolveTarget(`incus:${entry.container}`);
5000
+ return {
5001
+ key,
4590
5002
  argv: (this.opts.ptyArgvFor ?? buildPtyArgv)(plan),
4591
- connection: `${form.transport}:${form.name}`,
5003
+ connection: `${entry.kind === "docker" ? "docker" : "incus"}:${entry.name}`
5004
+ };
5005
+ }
5006
+ throw new Error(`[E_TARGET_FORMAT] remote: a machine target is required (ssh:<name>, docker:<name>, incus:<name>, hub:<m4>, or a bare pool name) — got form ${JSON.stringify(form.form)}`);
5007
+ }
5008
+ /** 取/懒建 machine 的默认 PTY 槽:dead/disposed 槽惰性重建;id 创建即认领(roster 全可见 + 防撞)。 */
5009
+ async defaultSlot(form, sessionKey) {
5010
+ const spec = await this.machineSlotSpec(form, sessionKey);
5011
+ this.assertOpen();
5012
+ const byKey = this.defaultSlots.get(sessionKey) ?? /* @__PURE__ */ new Map();
5013
+ this.defaultSlots.set(sessionKey, byKey);
5014
+ const existingKey = byKey.get(spec.key);
5015
+ if (existingKey !== void 0) {
5016
+ const existing = this.pool.get(existingKey);
5017
+ if (existing !== void 0) return {
5018
+ session: existing,
5019
+ machineKey: spec.key,
5020
+ isNew: false
5021
+ };
5022
+ byKey.delete(spec.key);
5023
+ }
5024
+ const session = this.createSession({
5025
+ argv: spec.argv,
5026
+ connection: spec.connection,
5027
+ byo: false,
5028
+ ...spec.builtOn === true ? { builtOn: true } : {}
5029
+ }, sessionKey);
5030
+ byKey.set(spec.key, session.key);
5031
+ return {
5032
+ session,
5033
+ machineKey: spec.key,
5034
+ isNew: true
5035
+ };
5036
+ }
5037
+ releaseDefaultSlot(machineKey, sessionKey) {
5038
+ this.defaultSlots.get(sessionKey)?.delete(machineKey);
5039
+ }
5040
+ /** 按池键反查并解绑默认槽(background 提交即 handoff 用);非槽会话无操作。 */
5041
+ releaseSlotByPoolKey(poolKey, sessionKey) {
5042
+ const byKey = this.defaultSlots.get(sessionKey);
5043
+ if (byKey === void 0) return;
5044
+ for (const [machineKey, key] of byKey) if (key === poolKey) {
5045
+ byKey.delete(machineKey);
5046
+ return;
5047
+ }
5048
+ }
5049
+ /** roster/status 面:本 session 的默认槽标注(session id → machineKey;悬空条目过滤)。 */
5050
+ defaultMarks(sessionKey) {
5051
+ const marks = /* @__PURE__ */ new Map();
5052
+ const byKey = this.defaultSlots.get(sessionKey);
5053
+ if (byKey === void 0) return marks;
5054
+ for (const [machineKey, poolKey] of byKey) {
5055
+ const s = this.pool.get(poolKey);
5056
+ if (s !== void 0) marks.set(s.identity.id, machineKey);
5057
+ }
5058
+ return marks;
5059
+ }
5060
+ /** spec §1 handoff(2026-11-01 二/三轮裁决):把 machine 默认 PTY 解绑成可寻址 pty:<id>——同引用移动
5061
+ * (不重启、不复制输出流);fresh(尚无默认槽)= 后台创建空闲 shell 再隔离(默认 PTY 思维模型恒
5062
+ * ready,创建时机是实现细节);busy = 附当前命令与 job 句柄。machine 下次 exec 按需新建默认 PTY。 */
5063
+ async handoff(target, callCtx = {}) {
5064
+ this.assertOpen();
5065
+ const sessionKey = callCtx.sessionKey ?? "no-session";
5066
+ const form = parseTargetForm(target);
5067
+ if (form.form !== "direct" && form.form !== "bare" && form.form !== "hub") throw new Error(`[E_TARGET_FORMAT] remote: handoff detaches a machine's default PTY — pass a machine target (ssh:dev4, docker:c, hub:<m4>, or a bare pool name); addressable sessions are already detached, and ${JSON.stringify(target)} is not a machine`);
5068
+ const spec = await this.machineSlotSpec(form, sessionKey);
5069
+ this.assertOpen();
5070
+ let session;
5071
+ const poolKey = this.defaultSlots.get(sessionKey)?.get(spec.key);
5072
+ if (poolKey !== void 0) session = this.pool.get(poolKey);
5073
+ let created = false;
5074
+ if (session === void 0) {
5075
+ this.releaseDefaultSlot(spec.key, sessionKey);
5076
+ session = this.createSession({
5077
+ argv: spec.argv,
5078
+ connection: spec.connection,
4592
5079
  byo: false,
4593
- ...form.label !== void 0 ? { label: form.label } : {}
4594
- }, sessionKey) };
5080
+ ...spec.builtOn === true ? { builtOn: true } : {}
5081
+ }, sessionKey);
5082
+ created = true;
5083
+ await session.warm();
5084
+ }
5085
+ this.releaseDefaultSlot(spec.key, sessionKey);
5086
+ const id = session.identity;
5087
+ const snap = session.snapshot;
5088
+ const activeCmd = session.active;
5089
+ const lines = [
5090
+ `handed off pty:${id.id}${id.label !== void 0 ? ` (label ${id.label})` : ""} — ${id.connection}${id.builtOn ? " (built on)" : ""}`,
5091
+ created ? "fresh idle shell created and detached" : snap.state === "dead" ? "session died — its next dispatch reconnects a fresh shell" : "detached",
5092
+ `this machine's next exec opens a fresh default PTY`
5093
+ ];
5094
+ if (activeCmd !== null) lines.push(`still running: command ${activeCmd.commandId}${activeCmd.jobId !== void 0 ? ` — follow with job_output ${activeCmd.jobId}, cancel with job_kill` : " — a synchronous exec settles into its receipt; a timeout handoff is adoptable via job_output"}`);
5095
+ return {
5096
+ mode: "pty",
5097
+ target: `pty:${id.id}`,
5098
+ session: `pty:${id.id}`,
5099
+ ...id.label !== void 0 ? { label: id.label } : {},
5100
+ connection: id.connection,
5101
+ exit: null,
5102
+ cwd: session.lastCwd,
5103
+ timedOut: false,
5104
+ reconnected: false,
5105
+ stdout: lines.join("\n"),
5106
+ durationMs: 0,
5107
+ ...created ? { created: true } : {},
5108
+ ...activeCmd !== null ? {
5109
+ running: true,
5110
+ commandId: activeCmd.commandId
5111
+ } : {},
5112
+ ...this.ptyTtl(session.key)
5113
+ };
5114
+ }
5115
+ /** close 统一入口(spec §1 + 三轮补充裁决):machine target 只关默认槽;pty selector 关该会话;
5116
+ * 不存在 job 定位——取消运行中命令的唯一入口是官方 job_kill。 */
5117
+ async close(target, sessionKey, force = false) {
5118
+ this.assertOpen();
5119
+ const form = parseTargetForm(target);
5120
+ const bareIsPty = form.form === "bare" && this.labels.resolve(form.name, sessionKey)?.kind === "pty";
5121
+ if (form.form === "direct" || form.form === "hub" || form.form === "bare" && !bareIsPty) return this.closeDefault(form, sessionKey, force);
5122
+ if (form.form === "pty-selector") {
5123
+ if (form.idOrLabel === void 0) throw new Error("[E_PARAMS] remote: \"pty\" alone selects nothing — append an id/label (pty:pty-1a2b, pty:build)");
5124
+ return this.closePty(form.idOrLabel, sessionKey, force);
5125
+ }
5126
+ if (form.form === "bare") return this.closePty(form.name, sessionKey, force);
5127
+ throw new Error(`[E_TARGET_FORMAT] remote: close takes a machine target (closes its default PTY) or pty:<id|label> — got ${JSON.stringify(target)}`);
5128
+ }
5129
+ async closeDefault(form, sessionKey, force) {
5130
+ const spec = await this.machineSlotSpec(form, sessionKey);
5131
+ this.assertOpen();
5132
+ const poolKey = this.defaultSlots.get(sessionKey)?.get(spec.key);
5133
+ if (poolKey === void 0) return {
5134
+ session: spec.key,
5135
+ connection: spec.connection,
5136
+ closed: false,
5137
+ reason: "not-found",
5138
+ killed: [],
5139
+ message: `no default PTY on ${spec.key} — a machine's first exec opens one`
5140
+ };
5141
+ const session = this.pool.get(poolKey);
5142
+ if (session === void 0) {
5143
+ this.releaseDefaultSlot(spec.key, sessionKey);
5144
+ return {
5145
+ session: spec.key,
5146
+ connection: spec.connection,
5147
+ closed: false,
5148
+ reason: "already-closed-or-reaped",
5149
+ killed: [],
5150
+ message: `the default PTY on ${spec.key} was already closed or reaped after ${this.ttlMinutes()} min idle`
5151
+ };
4595
5152
  }
5153
+ const r = this.closePty(session.identity.id, sessionKey, force);
5154
+ if (r.closed) this.releaseDefaultSlot(spec.key, sessionKey);
5155
+ return r;
4596
5156
  }
4597
5157
  /** background PTY lane:会话先占位(不拨号)再进官方 jobs registry——首回执必须能寻址;
4598
5158
  * registry 拒绝时把新建的会话拆掉(复用形的会话绝不拆)。返回 undefined = 走普通 oneshot job。 */
@@ -4604,9 +5164,9 @@ var RemoteDriver = class {
4604
5164
  const form = parseTargetForm(prepared.target);
4605
5165
  const target = (id$1) => ({
4606
5166
  target: `pty:${id$1}`,
4607
- cmd: params.cmd,
4608
- stdin: params.stdin,
4609
- timeout: params.timeout,
5167
+ cmd: prepared.cmd,
5168
+ stdin: prepared.stdin,
5169
+ timeout: prepared.timeout,
4610
5170
  mode: "pty"
4611
5171
  });
4612
5172
  const lane = this.createPtyLane(prepared, form, sessionKey);
@@ -4621,6 +5181,20 @@ var RemoteDriver = class {
4621
5181
  cleanup: () => void 0
4622
5182
  };
4623
5183
  }
5184
+ if (lane === void 0 && (form.form === "direct" || form.form === "hub" || form.form === "bare" && this.labels.resolve(form.name, sessionKey)?.kind !== "pty") && prepared.mode !== "oneshot") {
5185
+ const slot = await this.defaultSlot(form, sessionKey);
5186
+ this.assertOpen();
5187
+ const id$1 = slot.session.identity;
5188
+ return {
5189
+ input: target(id$1.id),
5190
+ session: `pty:${id$1.id}`,
5191
+ connection: id$1.connection,
5192
+ ...id$1.label !== void 0 ? { label: id$1.label } : {},
5193
+ cleanup: () => {
5194
+ if (slot.isNew) slot.session.dispose();
5195
+ }
5196
+ };
5197
+ }
4624
5198
  if (lane === void 0) return void 0;
4625
5199
  const id = lane.session.identity;
4626
5200
  return {
@@ -4680,15 +5254,53 @@ var RemoteDriver = class {
4680
5254
  this.pool.get(key)?.attachJob(commandId, jobId);
4681
5255
  }
4682
5256
  }
4683
- async dispatchPty(session, params, callCtx, timeoutSec, cap) {
5257
+ /** spec remote-default-pty §1(2026-11-01):exec stdin-only 写入核心——向已有 live PTY 的当前运行命令
5258
+ * 追加 exact-byte stdin。寻址:pty:<id|label> / 裸 label 命中 pty 项 / machine target(其默认槽的
5259
+ * 当前命令,无槽不懒建、如实报错)。不触发拨号、创建或重连;回执附 connection/label 供工具层渲染。 */
5260
+ async input(target, options, callCtx = {}) {
5261
+ this.assertOpen();
5262
+ const sessionKey = callCtx.sessionKey ?? "no-session";
5263
+ const form = parseTargetForm(target);
5264
+ let session;
5265
+ if (form.form === "pty-selector") {
5266
+ if (form.idOrLabel === void 0) throw new Error("[E_PARAMS] remote: \"pty\" alone selects nothing — append an id/label (pty:pty-1a2b, pty:build)");
5267
+ session = this.resolvePty(form.idOrLabel, sessionKey);
5268
+ } else if (form.form === "bare" && this.labels.resolve(form.name, sessionKey)?.kind === "pty") session = this.resolvePty(form.name, sessionKey);
5269
+ else if (form.form === "direct" || form.form === "hub" || form.form === "bare") {
5270
+ const spec = await this.machineSlotSpec(form, sessionKey);
5271
+ this.assertOpen();
5272
+ const poolKey = this.defaultSlots.get(sessionKey)?.get(spec.key);
5273
+ const slot = poolKey !== void 0 ? this.pool.get(poolKey) : void 0;
5274
+ if (slot === void 0) throw new Error(`[E_SESSION_NOT_RUNNING] remote: no default PTY on ${spec.key} yet — start a command first ({action:"exec", target:"${spec.key}", cmd:…}); a raw stdin write goes to the command running there`);
5275
+ session = slot;
5276
+ } else throw new Error(`[E_TARGET_FORMAT] remote: a raw stdin write goes into a live PTY session — pass pty:<id|label>, a bare session label, or a machine target (its default PTY); got ${JSON.stringify(target)}`);
5277
+ const receipt = await session.input(options);
5278
+ const id = session.identity;
5279
+ return {
5280
+ ...receipt,
5281
+ connection: id.connection,
5282
+ ...id.label !== void 0 ? { label: id.label } : {}
5283
+ };
5284
+ }
5285
+ async dispatchPty(session, params, callCtx, timeoutSec) {
5286
+ const hubConn = session.identity.connection;
5287
+ if (hubConn.startsWith("hub:")) {
5288
+ const hub = this.requireHub();
5289
+ this.hubProviders.add(hub);
5290
+ hub.ptyEndpoint(hubConn.slice(4));
5291
+ }
4684
5292
  const commandId = callCtx.commandId ?? genCommandId();
4685
5293
  const r = await session.dispatch(params.cmd, {
4686
5294
  stdin: params.stdin,
4687
5295
  timeoutSec,
4688
5296
  commandId,
4689
- ...callCtx.ptyBackground === true ? { background: true } : {}
5297
+ ...callCtx.ptyBackground === true ? {
5298
+ background: true,
5299
+ jobId: commandId
5300
+ } : {}
4690
5301
  });
4691
- const out = tailWindow(r.output, cap, r.outputStats?.stdout);
5302
+ if (callCtx.ptyBackground === true && r.running === true) this.releaseSlotByPoolKey(session.key, callCtx.sessionKey ?? "no-session");
5303
+ const out = disclose(r.output, r.outputStats?.stdout);
4692
5304
  const id = session.identity;
4693
5305
  return {
4694
5306
  mode: "pty",
@@ -4751,14 +5363,14 @@ var RemoteDriver = class {
4751
5363
  cwd: session.lastCwd,
4752
5364
  timedOut: false,
4753
5365
  reconnected: false,
4754
- stdout: [head, renderLedger(session.ledgerSnapshot())].join("\n"),
5366
+ stdout: [head, activeLine(session)].join("\n"),
4755
5367
  durationMs: 0,
4756
5368
  ...this.ptyTtl(session.key)
4757
5369
  };
4758
5370
  }
4759
- async oneshot(plan, display, params, timeoutSec, cap) {
5371
+ async oneshot(plan, display, params, timeoutSec) {
4760
5372
  this.assertOpen();
4761
- const process$1 = this.own(await this.nativeProcess(plan, params, timeoutSec));
5373
+ const process$1 = this.own(await this.nativeProcess(plan, display, params, timeoutSec));
4762
5374
  if (this.disposed) process$1.cancel("owner disposed");
4763
5375
  const result = await process$1.done;
4764
5376
  const stdout = process$1.read("stdout", 0), stderr = process$1.read("stderr", 0);
@@ -4771,8 +5383,8 @@ var RemoteDriver = class {
4771
5383
  stderr: stderr.stats
4772
5384
  }
4773
5385
  };
4774
- const out = tailWindow(r.stdout, cap, r.outputStats?.stdout);
4775
- const err = r.stderr.length > 0 ? tailWindow(r.stderr, cap, r.outputStats?.stderr) : void 0;
5386
+ const out = disclose(r.stdout, r.outputStats?.stdout);
5387
+ const err = r.stderr.length > 0 ? disclose(r.stderr, r.outputStats?.stderr) : void 0;
4776
5388
  return {
4777
5389
  mode: "oneshot",
4778
5390
  target: display,
@@ -4791,13 +5403,13 @@ var RemoteDriver = class {
4791
5403
  ...this.runnerOf.get(process$1) === void 0 ? {} : { runner: this.runnerOf.get(process$1) }
4792
5404
  };
4793
5405
  }
4794
- async hubOneshot(machine, display, params, timeoutSec, cap) {
5406
+ async hubOneshot(machine, display, params, timeoutSec) {
4795
5407
  const process$1 = this.own(await this.hubOneshotProcess(machine, params));
4796
5408
  if (this.disposed) process$1.cancel("owner disposed");
4797
5409
  const result = await process$1.done;
4798
5410
  const stdout = process$1.read("stdout", 0), stderr = process$1.read("stderr", 0);
4799
- const out = tailWindow(stdout.text, cap, stdout.stats);
4800
- const err = tailWindow(stderr.text, cap, stderr.stats);
5411
+ const out = disclose(stdout.text, stdout.stats);
5412
+ const err = disclose(stderr.text, stderr.stats);
4801
5413
  return {
4802
5414
  ...result,
4803
5415
  mode: "oneshot",
@@ -4805,6 +5417,7 @@ var RemoteDriver = class {
4805
5417
  connection: display,
4806
5418
  cwd: null,
4807
5419
  reconnected: false,
5420
+ runner: HUB_RUNNER,
4808
5421
  stdout: (!stdout.stats && stdout.lossy ? "[truncated: peer returned only a tail; exact total/retained/dropped unavailable]\n" : "") + out.text,
4809
5422
  stderr: [err.text, stderr.diagnostic].filter(Boolean).join("\n"),
4810
5423
  ...out.stats ? { outputStats: {
@@ -4843,17 +5456,20 @@ var RemoteDriver = class {
4843
5456
  return s === void 0 ? null : s;
4844
5457
  }).filter((s) => s !== null);
4845
5458
  const hub = this.opts.getHub?.();
4846
- return renderRoster(scan, this.labels.list(callCtx.sessionKey ?? "no-session"), sessions, hub === void 0 ? null : hub.listTargets());
5459
+ return renderRoster(scan, this.labels.list(callCtx.sessionKey ?? "no-session"), sessions, hub === void 0 ? null : hub.listTargets(), this.defaultMarks(callCtx.sessionKey ?? "no-session"));
4847
5460
  }
4848
5461
  /** status probe(target 无 cmd):机器面 = probe + 该 connection 上的会话;pty selector = session card。 */
4849
5462
  async status(target, callCtx = {}) {
4850
5463
  this.assertOpen();
4851
5464
  const sessionKey = callCtx.sessionKey ?? "no-session";
4852
5465
  const form = parseTargetForm(target);
4853
- if (form.form === "hub" || form.form === "hub-pty") {
5466
+ if (form.form === "hub") {
4854
5467
  const row = this.requireHub().listTargets().find((t) => t.id === form.machine);
4855
5468
  if (!row) throw new Error(`unknown remote machine ${form.machine} — select a remote machine from the Hub roster; local execution is not available`);
4856
- return `hub:${form.machine} — Hub remote machine (${row.label}); exec ${row.execEnabled ? "enabled" : "disabled"}; not probed`;
5469
+ const hubSlotKey = `hub:${form.machine}`;
5470
+ const hubSlotPoolKey = this.defaultSlots.get(sessionKey)?.get(hubSlotKey);
5471
+ const hubSlot = hubSlotPoolKey !== void 0 ? this.pool.get(hubSlotPoolKey) : void 0;
5472
+ return [`hub:${form.machine} — Hub remote machine (${row.label}); exec ${row.execEnabled ? "enabled" : "disabled"}; not probed`, hubSlot === void 0 ? `default PTY: none — this machine's first exec opens one` : `default PTY: ${sessionLine(hubSlot)}`].join("\n");
4857
5473
  }
4858
5474
  if (form.form === "pty-selector") {
4859
5475
  if (form.idOrLabel === void 0) throw new Error("[E_PARAMS] remote: \"pty\" alone selects nothing — append an id/label (pty:pty-1a2b, pty:build)");
@@ -4871,15 +5487,23 @@ var RemoteDriver = class {
4871
5487
  const plan = this.planOf(entry);
4872
5488
  const head = `${target} — ${plan.kind === "ssh" ? "ssh host" : `${plan.kind} container`}${entry.kind === "ssh-byo" ? " (built on)" : ""}`;
4873
5489
  const runner = this.opts.probeRunner ?? runOneShot;
4874
- const probe = await probeTarget(plan, { runner: (argv, options) => {
4875
- this.assertOpen();
4876
- return runner(argv, options);
4877
- } });
5490
+ const probe = await probeTarget(plan, {
5491
+ runner: (argv, options) => {
5492
+ this.assertOpen();
5493
+ return runner(argv, options);
5494
+ },
5495
+ muxArgvFor: (p) => this.muxArgvFor(p, target) ?? []
5496
+ });
4878
5497
  const prefix = `${sessionKey}|`;
4879
5498
  const onConnection = this.pool.list().filter(({ key }) => key.startsWith(prefix)).map(({ key }) => this.pool.get(key)).filter((s) => s !== void 0 && s.identity.connection === `${plan.kind === "ssh" ? "ssh" : plan.kind}:${name$1}`);
5499
+ const slotKey = this.machineKeyOfEntry(entry);
5500
+ const slotPoolKey = this.defaultSlots.get(sessionKey)?.get(slotKey);
5501
+ const slotSession = slotPoolKey !== void 0 ? this.pool.get(slotPoolKey) : void 0;
5502
+ const slotLine = slotSession === void 0 ? `default PTY: none — this machine's first exec opens one` : `default PTY: ${sessionLine(slotSession)}`;
4880
5503
  return [
4881
5504
  head,
4882
5505
  renderProbe(probe),
5506
+ slotLine,
4883
5507
  onConnection.length > 0 ? `pty sessions on this connection: ${onConnection.map(sessionLine).join(" · ")}` : "pty sessions on this connection: none"
4884
5508
  ].join("\n");
4885
5509
  }
@@ -4894,8 +5518,7 @@ var RemoteDriver = class {
4894
5518
  this.assertOpen();
4895
5519
  const form = parseTargetForm(target);
4896
5520
  if (form.form === "pty-selector") throw new Error("remote: a PTY session is not a filesystem connection");
4897
- if (form.form === "hub" || form.form === "hub-pty") throw new Error("remote: hub endpoints resolve through the Hub file lane, not a CLI connection");
4898
- if (form.form === "create-pty") throw new Error("remote: a PTY session is not a filesystem connection");
5521
+ if (form.form === "hub") throw new Error("remote: hub endpoints resolve through the Hub file lane, not a CLI connection");
4899
5522
  if (form.form === "direct") return form.transport === "ssh" ? this.sshPlanFor(form.name, callCtx.sessionKey ?? "no-session") : resolveTarget(`${form.transport}:${form.name}`);
4900
5523
  if (form.form === "ssh-prefix" || form.form === "empty") throw new Error("[E_TARGET_FORMAT] remote: connection targets need a name");
4901
5524
  const entry = await this.resolvePoolEntry(form.name, callCtx.sessionKey ?? "no-session");
@@ -4925,6 +5548,7 @@ var RemoteDriver = class {
4925
5548
  if (this.disposal) return this.disposal;
4926
5549
  this.disposed = true;
4927
5550
  this.labels.clear();
5551
+ this.defaultSlots.clear();
4928
5552
  this.fileAbort.abort(/* @__PURE__ */ new Error("remote owner disposed"));
4929
5553
  for (const process$1 of this.processes) process$1.cancel("remote owner disposed");
4930
5554
  return this.disposal = (async () => {
@@ -4935,12 +5559,18 @@ var RemoteDriver = class {
4935
5559
  ...[...this.hubProviders].map((hub) => hub.releaseOwner(this.hubOwner))
4936
5560
  ]);
4937
5561
  this.hubProviders.clear();
5562
+ await this.mux.dispose();
4938
5563
  })();
4939
5564
  }
4940
5565
  };
4941
5566
  /** direct 形态回退到 resolveTarget 可解析的名字(ssh:dev4 → dev4)。 */
4942
5567
  function directName(form) {
4943
- return form.form === "direct" ? form.name : form.form === "create-pty" ? form.name : "";
5568
+ return form.form === "direct" ? form.name : "";
5569
+ }
5570
+ /** session card 的当前命令行(spec 2026-11-01:历史 ledger 已删,只报活动命令)。 */
5571
+ function activeLine(s) {
5572
+ const a = s.active;
5573
+ return a === null ? "no command running" : `running: ${a.commandId}${a.jobId !== void 0 ? ` (job ${a.jobId})` : ""} — feed it stdin with exec stdin-only, follow job_output, cancel job_kill`;
4944
5574
  }
4945
5575
  /** roster/status 一行的会话描述:`p-ab12 (label build) ssh:dev4 idle 12s`。 */
4946
5576
  function sessionLine(s) {
@@ -4962,16 +5592,21 @@ const ptyOutputSources = (holder) => [{
4962
5592
  lossy: false
4963
5593
  }
4964
5594
  }];
4965
- const ptyOutcome = (aborted, result) => ({
4966
- status: aborted ? "killed" : result.exit === null ? "failed" : "completed",
4967
- detail: aborted ? `interrupted: ${result.exit === null ? "session died" : `exit ${result.exit}`}` : result.exit === null ? "session died" : `exit code: ${result.exit}`
4968
- });
5595
+ /** 0.3.2-d CF:timedOut=true 且 aborted=false = 会话侧主动收束(close force / 行回收走的
5596
+ * dispose→interrupt 两段路)——同为 killed;exit 到手用 exit,只剩会话死亡时写 session closed。 */
5597
+ const ptyOutcome = (aborted, result) => {
5598
+ const interrupted = aborted || result.timedOut === true;
5599
+ return {
5600
+ status: interrupted ? "killed" : result.exit === null ? "failed" : "completed",
5601
+ detail: interrupted ? `interrupted: ${result.exit === null ? aborted ? "session died" : "session closed" : `exit ${result.exit}`}` : result.exit === null ? "session died" : `exit code: ${result.exit}`
5602
+ };
5603
+ };
4969
5604
  /** No preparation or discovery before the official registry's admission. */
4970
5605
  function startRemoteJob(registry, input, exec, driver) {
4971
5606
  if (!exec.agent) throw new Error("remote: background jobs require a live Agent owner");
4972
5607
  const form = parseTargetForm(input.target);
4973
- if (form.form === "pty-selector" || form.form === "create-pty" || form.form === "hub-pty" || input.spawn !== void 0 || input.mode === "pty") {
4974
- if (form.form === "hub-pty") throw new Error("remote: hub-machine PTY has no background lane; use a direct ssh/docker/incus target or a pty session");
5608
+ const machineForm = form.form === "direct" || form.form === "bare" || form.form === "hub";
5609
+ if (form.form === "pty-selector" || input.spawn !== void 0 || input.mode === "pty" || machineForm && input.mode !== "oneshot") {
4975
5610
  const holder = {};
4976
5611
  return registry.start({
4977
5612
  kind: "remote",
@@ -5097,116 +5732,123 @@ function adoptRunningPtyJob(registry, exec, label, job) {
5097
5732
 
5098
5733
  //#endregion
5099
5734
  //#region src/remote/tool.ts
5100
- const NO_SESSION_KEY = "remote:no-session";
5735
+ const NO_SESSION_KEY$1 = "remote:no-session";
5101
5736
  /**
5102
- * The model-visible surface of the `remote` tool (workbench round, plan
5103
- * 2026-10-19-pty-workbench). ONE template literal — no backticks, no "${".
5737
+ * The model-visible surface of the `remote` tool (0.3.2-d user-ruled restructure:
5738
+ * Actions / Targets / PTY / Oneshot / Examples — the PTY block is the user's verbatim
5739
+ * final draft; Examples receipts match renderValue byte-for-byte).
5740
+ * ONE template literal — no backticks, no "${".
5741
+ *
5742
+ * 语义目标(surface 就是产品):动作是唯一的派发维度;PTY 四词定位——stateful oneshot /
5743
+ * interact-able stdin / managed output / persistent env——回执形状与 oneshot 相同、只增状态,
5744
+ * 运行中的命令可分步观察后再补输(input 动作);多步与交互任务默认进 PTY。
5104
5745
  */
5105
- const REMOTE_TOOL_DESCRIPTION = `Remote execution and file transfer over ssh / docker / incus, plus stateful PTY workbenches. Pick the call shape:
5106
- {} → roster (local scan; refreshes the name pool: ssh aliases, BYO identities, docker/incus containers, hub machines, live pty sessions)
5107
- {target} → status probe: machine reachability + its pty sessions, or a session card (connection, cwd, ledger of the last 20 commands) for pty:<id|label>
5108
- {target, cmd} → run cmd — oneshot (stateless bash -lc) on any machine target
5109
- {target, scriptPath:"/abs"} → LOCAL script over stdin to a fixed bash -s (oneshot only; no cmd/stdin/spawn mix)
5110
- {target:"ssh:dev4:pty[:label]", cmd?} → create a PTY session on that machine and run cmd in it; no cmd = warm creation returning the id. Tab semantics: every creation call opens a NEW session — parallel is first-class (max 10 per Agent, ssh:dev4 and docker:corti alike).
5111
- {target:"pty:<id|label>", cmd} → run cmd in a live session (cwd/env survive; the receipt carries session, connection, commandId, cwd, env delta, idle-left)
5112
- {spawn, target?:"pty[:<label>]"|"<label>"|"", cmd?} → create a BYO pty from a spawn command (e.g. docker exec -it img bash; nested hops are dumb pipes). spawn present IS the BYO entry point; no target = auto id.
5113
- {ssh:{...}, target?:"ssh[:<label>]"|"<label>"|"", cmd?} → register a BYO ssh identity (ssh present IS the entry point); the label then works everywhere ssh: aliases do (roster marks it "built on"); ssh:<label>:pty opens a session on it. key is an existing origin file, never inline material; nothing is persisted; a new Agent registers again.
5114
- {action:"close", target:"pty:<id|label>", force?} → close a session. Idempotent (unknown id → closed:false). Default rejects while a job is running and names it (job_kill it first, or wait); force:true closes the tab and kills the group in one step.
5115
- {action:"copy"|"rsync", src, dst, argv?} → file transfer (see the action/src/dst fields)
5116
- Bare names address ONE unified pool (machines + identities + sessions — roster lists it; a machine losing its bare name to an earlier claim is shown prefixed docker-foo / incus-foo). Prefixes ssh: / docker: / incus: / hub: force a channel. Creation always claims a fresh name: a taken label answers [E_LABEL_IN_USE] — address it without creation params, or pick another (specs are never compared). PTY sessions idle out after 15 min (running commands exempt; reads do not reset the clock).
5746
+ const REMOTE_TOOL_DESCRIPTION = `Remote shell (SSH, Docker, Incus, Hub): 1 default interactive PTY per machine (retains cwd/env). \`mode:"oneshot"\` = isolated, stateless.
5117
5747
 
5118
- oneshot: stdout/stderr separate, exit code verbatim. Supervision prefers a pinned superd-run helper cached on the endpoint (~/.cache/superd/run; first call uploads it over the same exec lane) and falls back to a POSIX wrapper everywhere else — the receipt field "runner" says which ran. Helper or wrapper, backgrounded children are reaped on completion and the whole group is killed on timeout/cancel; the endpoint needs bash only (no Python).
5748
+ Actions:
5749
+ - exec {target, cmd? | scriptPath?, stdin?, mode?:"pty"|"oneshot"="pty", timeout?, run_in_background?:boolean, commandId?:string}: run command / script / raw stdin write. Machine target = default PTY. PTY \`stdin\` alone = raw write into running command (optional \`commandId\` guard). \`run_in_background:true\` = async job, returns handle instantly, PTY handed off automatically.
5750
+ - handoff {target:<machine>}: detach machine's default PTY -> \`pty:<id>\`. Next exec spawns fresh default.
5751
+ - add {ssh:{...} | spawn:"<cmd>", target?}: register BYO identity/PTY. Returns label. Taken label answers \`[E_LABEL_IN_USE]\`.
5752
+ - status {target?}: roster pool (no target) or machine probe / session card.
5753
+ - close {target, force?:boolean}: close PTY shell. Machine target closes only default PTY. Running command blocks close unless \`force:true\`.
5119
5754
 
5120
- PTY is the stateful superset of oneshot: every command returns synchronously with exit + cwd + an env delta (NAMES only — added/changed/removed; values never leave the endpoint; fetch a value yourself if you need it). A command that outlives its timeout is NOT killed — it hands off as a running job: poll job_output <commandId>, cancel with job_kill (Ctrl-C first; the session survives). run_in_background uses the same lane and its jobId IS the commandId. A busy session rejects new commands ([E_SESSION_BUSY] names the running id and the ways out: job_output / job_kill / another session or oneshot).
5755
+ Targets & Semantics:
5756
+ - Targets: \`ssh:<n>\`, \`docker:<n>\`, \`incus:<n>\`, \`hub:<m4>\`, \`pty:<id|label>\` (or bare label). Busy PTY rejects new cmds (use input/poll/cancel/handoff/oneshot).
5757
+ - PTY: Stream includes echo/stderr. \`envDelta\` lists var names (no values). Expires after 15m idle. Timeout (default 120s) = sync wait limit (command keeps running as job: \`job_output\` / \`job_kill\`).
5758
+ - Oneshot: Separate stdout/stderr, verbatim exit code, stateless. \`stdin\` = startup only. Cached \`superd-run\` runner preferred over POSIX shim.
5759
+ - Script: \`scriptPath\` = local absolute file read before dial, runs as single \`bash -c\` cmd (same size limit). In PTY, runs as child process (\`cd\`/\`export\` do not affect parent shell). Hub inline cmd limit: 4096 chars.
5760
+ - Stdin: oneshot = \`fd0 → pipe\`; PTY = \`fd0 → pty slave\`, line disciplines. Literal bytes (\`\\n\`, \`\\u0003\` = Ctrl-C, \`\\u0004\` = Ctrl-D). Retention: 4M chars/stream (Hub oneshot 4MiB).
5121
5761
 
5122
- Output: pty tails at ~30k chars ("[truncated: …]" header) — redirect bulk output to a file. Hard retention 4,000,000 UTF-16 units per stream (Hub 4MiB bytes); outputStats reports the rest; restart cannot recover discarded output. stdin feeds the command (pty: REPL-style right after the command line — only helps commands that read stdin there). timeout (seconds, default 120): oneshot interrupts then kills the whole group; pty hands off. Native transport errors pass through verbatim — self-correct from them.`;
5762
+ Examples:
5763
+ remote {"action":"exec","target":"ssh:dev4","cmd":"cd /srv/app && pwd"}
5764
+ remote {"action":"exec","target":"ssh:dev4","cmd":"make test","run_in_background":true}
5765
+ remote {"action":"handoff","target":"ssh:dev4"}
5766
+ remote {"action":"exec","target":"pty:pty-irsq","stdin":"yes\\n","commandId":"remote-47676dff2c6443e8"}
5767
+ remote {"action":"exec","target":"ssh:dev4","mode":"oneshot","cmd":"uname -a"}
5768
+ remote {"action":"add","spawn":"ssh -t pc ssh -t dev4","target":"pty:dev4via"}
5769
+ remote {"action":"exec","target":"pty:dev4via","cmd":"hostname"}
5770
+ remote {"action":"add","ssh":{"host":"10.0.0.8","user":"u1","port":22,"key":"~/.ssh/id_key4"},"target":"ssh:lab"}
5771
+ remote_transfer {"mode":"rsync","src":"ssh:lab:/var/log/app/","dst":"~/app-logs/","argv":["-a"]}`;
5772
+ /** action 是唯一派发维度:每个动词声明自己的字段集,超出即报错(不做在场推断、不留别名)。 */
5773
+ const ACTION_PARAMS = {
5774
+ exec: new Set([
5775
+ "action",
5776
+ "target",
5777
+ "cmd",
5778
+ "stdin",
5779
+ "scriptPath",
5780
+ "timeout",
5781
+ "run_in_background",
5782
+ "mode",
5783
+ "commandId"
5784
+ ]),
5785
+ handoff: new Set(["action", "target"]),
5786
+ add: new Set([
5787
+ "action",
5788
+ "target",
5789
+ "ssh",
5790
+ "spawn"
5791
+ ]),
5792
+ status: new Set(["action", "target"]),
5793
+ close: new Set([
5794
+ "action",
5795
+ "target",
5796
+ "force"
5797
+ ])
5798
+ };
5799
+ const ACTION_HINT = {
5800
+ exec: "exec takes target plus cmd (or scriptPath, or stdin alone as a raw write into the running command) and the execution options; ssh/spawn belong to add",
5801
+ handoff: "handoff takes exactly one field besides action: target — the machine whose default PTY becomes pty:<id>",
5802
+ add: "add takes exactly one of ssh or spawn, plus an optional target label",
5803
+ status: "status takes only an optional target — omit it for the roster",
5804
+ close: "close takes a target (machine = its default PTY, or pty:<id|label>) and optional force — never a job id; cancel jobs with job_kill"
5805
+ };
5123
5806
  async function remoteExecute(ENV, args, exec) {
5124
- if (args.action === "copy" || args.action === "rsync") {
5125
- if (!args.src || !args.dst) throw new Error(`${args.action} requires src and dst absolute endpoints`);
5126
- if ([
5127
- args.cmd,
5128
- args.scriptPath,
5129
- args.stdin,
5130
- args.spawn,
5131
- args.ssh,
5132
- args.target,
5133
- args.timeout,
5134
- args.run_in_background,
5135
- args.force
5136
- ].some((value) => value !== void 0)) throw new Error(`${args.action} cannot be combined with execution/PTY/job arguments`);
5137
- const KNOWN_PARAMS = new Set([
5138
- "action",
5139
- "force",
5140
- "src",
5141
- "dst",
5142
- "argv",
5143
- "pullArgv",
5144
- "pushArgv",
5145
- "verify",
5146
- "mkdir",
5147
- "run_in_background",
5148
- "target",
5149
- "ssh",
5150
- "spawn",
5151
- "cmd",
5152
- "scriptPath",
5153
- "stdin",
5154
- "timeout"
5155
- ]);
5156
- for (const key of Object.keys(args)) if (!KNOWN_PARAMS.has(key)) throw new Error(`remote: unknown parameter "${key}" — this surface has no legacy aliases; pass native CLI options through argv instead`);
5157
- if (args.action === "rsync") {
5158
- if (args.pullArgv !== void 0 || args.pushArgv !== void 0) throw new Error("rsync takes argv only; pullArgv/pushArgv are copy-only fields");
5159
- if ([args.verify, args.mkdir].some((value) => value !== void 0)) throw new Error("rsync cannot be combined with copy verify/mkdir options; the Hub/local staged lane is copy-only");
5160
- const start = Date.now();
5161
- const receipt$1 = await ENV.driver.rsync({
5162
- src: args.src,
5163
- dst: args.dst,
5164
- ...args.argv === void 0 ? {} : { argv: args.argv }
5165
- }, exec.signal, { sessionKey: ENV.getSessionKey(exec) });
5166
- if (receipt$1.ok === false || receipt$1.exit !== 0) throw new Error(JSON.stringify(receipt$1));
5167
- const rsyncDuration = Date.now() - start;
5168
- return {
5169
- kind: "rsync",
5170
- text: formatTransferLine("rsync", receipt$1, rsyncDuration),
5171
- receipt: receipt$1,
5172
- exit: receipt$1.exit,
5173
- durationMs: rsyncDuration
5174
- };
5175
- }
5176
- const copyStart = Date.now();
5177
- const receipt = await ENV.driver.copy({
5178
- src: args.src,
5179
- dst: args.dst,
5180
- ...args.verify === void 0 ? {} : { verify: args.verify },
5181
- ...args.mkdir === void 0 ? {} : { mkdir: args.mkdir },
5182
- ...args.argv === void 0 ? {} : { argv: args.argv },
5183
- ...args.pullArgv === void 0 ? {} : { pullArgv: args.pullArgv },
5184
- ...args.pushArgv === void 0 ? {} : { pushArgv: args.pushArgv }
5185
- }, exec.signal, { sessionKey: ENV.getSessionKey(exec) });
5186
- const asReceipt = receipt;
5187
- const exits = asReceipt.exits ?? [];
5188
- if (asReceipt.ok === false || exits.some((code) => code !== 0)) throw new Error(JSON.stringify(receipt));
5189
- const copyDuration = Date.now() - copyStart;
5807
+ const action = args.action;
5808
+ if (action === void 0) throw new Error("[E_PARAMS] remote: action is required — \"exec\" (run a command or raw stdin write), \"handoff\" (detach a machine default PTY), \"add\" (register a BYO endpoint), \"status\" (roster / probe / session card), \"close\" (close a PTY shell)");
5809
+ const allowed = ACTION_PARAMS[action];
5810
+ if (allowed === void 0) throw new Error(`[E_PARAMS] remote: unknown action ${JSON.stringify(String(action))} — one of exec / handoff / add / close / status`);
5811
+ for (const key of Object.keys(args)) if (!allowed.has(key)) throw new Error(`[E_PARAMS] remote: "${key}" does not belong to action "${action}" — ${ACTION_HINT[action]}`);
5812
+ const sessionKey = ENV.getSessionKey(exec);
5813
+ const hasTarget = typeof args.target === "string" && args.target.length > 0;
5814
+ const hasCmd = typeof args.cmd === "string" && args.cmd.length > 0;
5815
+ if (action === "status") {
5816
+ if (!hasTarget) return {
5817
+ kind: "roster",
5818
+ text: await ENV.driver.roster({ sessionKey }),
5819
+ exit: null,
5820
+ durationMs: 0
5821
+ };
5190
5822
  return {
5191
- kind: "copy",
5192
- text: formatTransferLine("copy", receipt, copyDuration),
5193
- receipt,
5194
- exit: exits.length > 0 ? 0 : null,
5195
- durationMs: copyDuration
5823
+ kind: "status",
5824
+ text: await ENV.driver.status(args.target, { sessionKey }),
5825
+ exit: null,
5826
+ durationMs: 0
5196
5827
  };
5197
5828
  }
5198
- if (args.action === "close") {
5199
- if (typeof args.target !== "string" || args.target.length === 0) throw new Error("close requires target pty:<id|label>");
5200
- if ([
5201
- args.cmd,
5202
- args.scriptPath,
5203
- args.stdin,
5204
- args.spawn,
5205
- args.ssh,
5206
- args.timeout,
5207
- args.run_in_background
5208
- ].some((value) => value !== void 0)) throw new Error("[E_PARAMS] close cannot be combined with execution/PTY/job arguments");
5209
- const r$1 = ENV.driver.closePty(args.target.startsWith("pty:") ? args.target.slice(4) : args.target, ENV.getSessionKey(exec), args.force === true);
5829
+ if (action === "handoff") {
5830
+ if (!hasTarget) throw new Error("[E_PARAMS] remote: action \"handoff\" requires target — the machine whose default PTY becomes an addressable pty:<id> (ssh:dev4, docker:c, hub:<m4>, or a bare pool name)");
5831
+ const r$1 = await ENV.driver.handoff(args.target, { sessionKey });
5832
+ return {
5833
+ kind: "handoff",
5834
+ text: r$1.stdout,
5835
+ exit: null,
5836
+ durationMs: 0,
5837
+ session: r$1.session,
5838
+ connection: r$1.connection,
5839
+ ...r$1.label !== void 0 ? { label: r$1.label } : {},
5840
+ ...r$1.cwd !== null && r$1.cwd !== void 0 ? { cwd: r$1.cwd } : {},
5841
+ ...r$1.created === true ? { created: true } : {},
5842
+ ...r$1.running === true && r$1.commandId !== void 0 ? {
5843
+ running: true,
5844
+ commandId: r$1.commandId
5845
+ } : {},
5846
+ ...r$1.ttlSec !== void 0 ? { ttlSec: r$1.ttlSec } : {}
5847
+ };
5848
+ }
5849
+ if (action === "close") {
5850
+ if (!hasTarget) throw new Error("[E_PARAMS] close requires target — a machine (closes its default PTY) or pty:<id|label>");
5851
+ const r$1 = await ENV.driver.close(args.target, sessionKey, args.force === true);
5210
5852
  return {
5211
5853
  kind: "close",
5212
5854
  text: r$1.closed ? `closed ${r$1.session} (${r$1.connection})${r$1.killed.length > 0 ? ` — killed running job ${r$1.killed.join(", ")}` : ""}` : `${r$1.session}: not closed (${r$1.reason}) — ${r$1.message ?? "no live session"}`,
@@ -5218,44 +5860,72 @@ async function remoteExecute(ENV, args, exec) {
5218
5860
  killed: r$1.killed
5219
5861
  };
5220
5862
  }
5221
- if ([
5222
- args.src,
5223
- args.dst,
5224
- args.argv,
5225
- args.pullArgv,
5226
- args.pushArgv,
5227
- args.verify,
5228
- args.mkdir,
5229
- args.force
5230
- ].some((value) => value !== void 0)) throw new Error("file/force fields require an action (copy / rsync / close)");
5231
- const hasCmd = typeof args.cmd === "string" && args.cmd.length > 0;
5232
- const hasTarget = typeof args.target === "string" && args.target.length > 0;
5233
- const hasSpawn = typeof args.spawn === "string" && args.spawn.length > 0;
5234
- if (args.spawn !== void 0 && !hasSpawn) throw new Error("[E_PARAMS] remote: spawn must be a non-empty command string");
5235
- if (args.scriptPath !== void 0 && (hasSpawn || /^pty:/.test(args.target ?? "") || /:pty/.test(args.target ?? ""))) throw new Error("[E_SCRIPT_PARAMS] remote: scriptPath is oneshot-only — PTY lanes take cmd");
5863
+ if (action === "add") {
5864
+ const hasSpawn = typeof args.spawn === "string" && args.spawn.length > 0;
5865
+ if (args.spawn !== void 0 && !hasSpawn) throw new Error("[E_PARAMS] remote: add: spawn must be a non-empty command string");
5866
+ if (args.ssh === void 0 && !hasSpawn) throw new Error("[E_PARAMS] remote: action \"add\" registers a BYO endpoint — pass ssh:{host,...} (identity) or spawn:\"<command>\" (PTY), exactly one");
5867
+ if (args.ssh !== void 0 && hasSpawn) throw new Error("[E_PARAMS] remote: add: ssh and spawn are two different BYO entry points — pass one, not both");
5868
+ const r$1 = await ENV.driver.call({
5869
+ ...hasTarget ? { target: args.target } : {},
5870
+ ...args.ssh === void 0 ? {} : { ssh: args.ssh },
5871
+ ...hasSpawn ? { spawn: args.spawn } : {},
5872
+ cmd: ""
5873
+ }, { sessionKey });
5874
+ const v$1 = workbenchValue(r$1);
5875
+ v$1.created = true;
5876
+ if (!hasSpawn) v$1.notice = `registered — address it as ${r$1.session ?? r$1.target} from now on (the label, id and ssh alias all work)`;
5877
+ return v$1;
5878
+ }
5879
+ if (!hasTarget) throw new Error("[E_PARAMS] remote: action \"exec\" requires target — a machine (ssh:dev4, docker:corti, hub:<m4>, a pool name) or a PTY session (pty:<id|label>, a bare label, or <machine>:pty[:label] to create one). Status probes and the roster live under action \"status\".");
5880
+ const target = args.target;
5881
+ if (args.commandId !== void 0 && args.commandId.length === 0) throw new Error("[E_PARAMS] remote: commandId, when given, must be the non-empty id from the exec receipt / job_output");
5882
+ if (args.scriptPath !== void 0) {
5883
+ if (args.scriptPath.length === 0) throw new Error("[E_PARAMS] remote: scriptPath must be a non-empty absolute path");
5884
+ if (hasCmd) throw new Error("[E_SCRIPT_PARAMS] remote: scriptPath and cmd are alternative carriers — pass one");
5885
+ }
5886
+ if (args.commandId !== void 0 && (hasCmd || args.scriptPath !== void 0)) throw new Error("[E_PARAMS] remote: commandId guards a raw stdin write (stdin alone, no cmd/scriptPath) — drop it for a normal command");
5887
+ if (args.stdin !== void 0 && !hasCmd && args.scriptPath === void 0) {
5888
+ if (args.mode === "oneshot") throw new Error("[E_STDIN_WITHOUT_CMD] remote: oneshot has no interactive lane — stdin without cmd/scriptPath is a raw write into a PTY running command; drop mode:\"oneshot\" (or pass cmd)");
5889
+ if (args.run_in_background === true) throw new Error("[E_PARAMS] remote: a raw stdin write cannot go background — it writes the command currently running in that PTY; follow it with job_output instead");
5890
+ const r$1 = await ENV.driver.input(target, {
5891
+ stdin: args.stdin,
5892
+ ...args.commandId !== void 0 ? { commandId: args.commandId } : {}
5893
+ }, { sessionKey });
5894
+ const handle = r$1.jobId ?? r$1.commandId;
5895
+ return {
5896
+ kind: "input",
5897
+ text: `wrote ${r$1.writtenBytes} byte${r$1.writtenBytes === 1 ? "" : "s"} into ${r$1.session} for command ${r$1.commandId} — this confirms the write, not the program's response; read the response with job_output ${handle}`,
5898
+ exit: null,
5899
+ durationMs: 0,
5900
+ session: r$1.session,
5901
+ connection: r$1.connection,
5902
+ ...r$1.label !== void 0 ? { label: r$1.label } : {},
5903
+ commandId: r$1.commandId,
5904
+ ...r$1.jobId !== void 0 ? { jobId: r$1.jobId } : {},
5905
+ writtenBytes: r$1.writtenBytes
5906
+ };
5907
+ }
5908
+ if (!hasCmd && args.scriptPath === void 0) throw new Error("[E_PARAMS] remote: action \"exec\" must execute something — pass cmd, or scriptPath for a local script file, or stdin alone for a raw write into a PTY running command");
5236
5909
  if (args.run_in_background) {
5237
- if (!hasCmd && args.scriptPath === void 0) throw new Error("remote: background requires cmd or scriptPath");
5238
5910
  const jobs = exec.agent?.ctx.get("jobs");
5239
5911
  if (!jobs) throw new Error("remote: background requires the official DSH jobs service and job_output/job_kill controller; foreground execution remains available");
5240
5912
  const lane = await ENV.driver.preparePtyLane({
5241
5913
  target: args.target,
5242
- spawn: args.spawn,
5243
5914
  cmd: args.cmd,
5244
5915
  scriptPath: args.scriptPath,
5245
- ssh: args.ssh,
5246
5916
  stdin: args.stdin,
5247
- timeout: args.timeout
5248
- }, { sessionKey: ENV.getSessionKey(exec) });
5917
+ timeout: args.timeout,
5918
+ ...args.mode !== void 0 ? { mode: args.mode } : {}
5919
+ }, { sessionKey });
5249
5920
  let jobId;
5250
5921
  try {
5251
5922
  jobId = startRemoteJob(jobs, lane !== void 0 ? lane.input : {
5252
5923
  target: args.target,
5253
- spawn: args.spawn,
5254
5924
  cmd: args.cmd,
5255
5925
  scriptPath: args.scriptPath,
5256
- ssh: args.ssh,
5257
5926
  stdin: args.stdin,
5258
- timeout: args.timeout
5927
+ timeout: args.timeout,
5928
+ ...args.mode !== void 0 ? { mode: args.mode } : {}
5259
5929
  }, exec, ENV.driver);
5260
5930
  } catch (error) {
5261
5931
  lane?.cleanup();
@@ -5269,44 +5939,21 @@ async function remoteExecute(ENV, args, exec) {
5269
5939
  durationMs: 0,
5270
5940
  text: `Started ${jobId}${where}. Use job_output to read and job_kill to cancel; completion is reported by the host.`,
5271
5941
  ...lane !== void 0 ? {
5942
+ commandId: jobId,
5272
5943
  session: lane.session,
5273
5944
  connection: lane.connection,
5274
5945
  ...lane.label !== void 0 ? { label: lane.label } : {}
5275
5946
  } : {}
5276
5947
  };
5277
5948
  }
5278
- if (!hasCmd && args.scriptPath === void 0) {
5279
- if (args.stdin !== void 0) throw new Error("[E_STDIN_WITHOUT_CMD] remote: stdin requires cmd — status probes take no input");
5280
- const t = args.target ?? "";
5281
- if (hasSpawn || args.ssh !== void 0 || /^pty:/.test(t) || /:pty(:|$)/.test(t) && !t.startsWith("hub:")) return workbenchValue(await ENV.driver.call({
5282
- target: args.target,
5283
- spawn: args.spawn,
5284
- ssh: args.ssh,
5285
- cmd: "",
5286
- stdin: void 0
5287
- }, { sessionKey: ENV.getSessionKey(exec) }));
5288
- if (!hasTarget) return {
5289
- kind: "roster",
5290
- text: await ENV.driver.roster({ sessionKey: ENV.getSessionKey(exec) }),
5291
- exit: null,
5292
- durationMs: 0
5293
- };
5294
- return {
5295
- kind: "status",
5296
- text: await ENV.driver.status(args.target, { sessionKey: ENV.getSessionKey(exec) }),
5297
- exit: null,
5298
- durationMs: 0
5299
- };
5300
- }
5301
5949
  const r = await ENV.driver.call({
5302
5950
  target: args.target,
5303
- spawn: args.spawn,
5304
- ssh: args.ssh,
5305
5951
  cmd: args.cmd,
5306
5952
  scriptPath: args.scriptPath,
5307
5953
  stdin: args.stdin,
5308
- timeout: args.timeout
5309
- }, { sessionKey: ENV.getSessionKey(exec) });
5954
+ timeout: args.timeout,
5955
+ ...args.mode !== void 0 ? { mode: args.mode } : {}
5956
+ }, { sessionKey });
5310
5957
  const v = workbenchValue(r);
5311
5958
  if (r.running === true && r.ptyJob !== void 0) {
5312
5959
  const jobs = exec.agent?.ctx.get("jobs");
@@ -5314,7 +5961,7 @@ async function remoteExecute(ENV, args, exec) {
5314
5961
  const jobId = adoptRunningPtyJob(jobs, exec, `${r.target}: ${args.cmd}`, r.ptyJob);
5315
5962
  v.jobId = jobId;
5316
5963
  v.running = true;
5317
- if (r.commandId !== void 0 && jobId !== r.commandId) ENV.driver.attachPtyJob(r.commandId, jobId, ENV.getSessionKey(exec));
5964
+ if (r.commandId !== void 0 && jobId !== r.commandId) ENV.driver.attachPtyJob(r.commandId, jobId, sessionKey);
5318
5965
  } else {
5319
5966
  r.ptyJob.interrupt();
5320
5967
  v.notice = "The command outlived its synchronous wait and no jobs service is available to poll it, so it was interrupted. Use run_in_background:true for long commands.";
@@ -5323,7 +5970,7 @@ async function remoteExecute(ENV, args, exec) {
5323
5970
  return v;
5324
5971
  }
5325
5972
  function createRemoteTool(driver, opts = {}) {
5326
- const getSessionKey = opts.getSessionKey ?? ((exec) => exec.agent?.id ?? NO_SESSION_KEY);
5973
+ const getSessionKey = opts.getSessionKey ?? ((exec) => exec.agent?.id ?? NO_SESSION_KEY$1);
5327
5974
  const tool = defineTool({
5328
5975
  name: "remote",
5329
5976
  description: REMOTE_TOOL_DESCRIPTION,
@@ -5331,58 +5978,47 @@ function createRemoteTool(driver, opts = {}) {
5331
5978
  action: {
5332
5979
  type: "string",
5333
5980
  enum: [
5334
- "copy",
5335
- "rsync",
5981
+ "exec",
5982
+ "handoff",
5983
+ "add",
5984
+ "status",
5336
5985
  "close"
5337
5986
  ],
5338
- description: "Optional action; omit for execution/status/roster. close takes target + optional force."
5339
- },
5340
- force: {
5341
- type: "boolean",
5342
- description: "close only: default false — a session with a running job is refused (job_kill it first). true = kill the group and close in one step; the receipt lists what was killed."
5987
+ required: true,
5988
+ description: "Required. exec = run a command / script / raw stdin write (machine target defaults to its reusable PTY); handoff = detach a machine's default PTY as pty:<id>; add = register a BYO ssh identity or BYO PTY into the pool; status = roster (no target) or probe/session card (target); close = close a PTY shell (no job id — cancel with job_kill)."
5343
5989
  },
5344
- src: {
5990
+ target: {
5345
5991
  type: "string",
5346
- description: "copy/rsync source [target:]absolute path; rsync retains trailing-slash semantics"
5992
+ description: "exec: required — a machine (bare pool name, ssh:<name>, docker:<name>, incus:<name>, hub:<m4>; its default PTY unless mode is oneshot) or a live PTY session (pty:<id|label> or its bare label). handoff: the machine whose default PTY detaches. add: optional label for the BYO entry. status: optional — omit for the roster. close: a machine (its default PTY) or pty:<id|label>."
5347
5993
  },
5348
- dst: {
5994
+ cmd: {
5349
5995
  type: "string",
5350
- description: "copy/rsync destination [target:]absolute path"
5996
+ description: "exec: one raw shell string parsed by the remote bash (multi-line = one compound; exit = last command's). Required unless scriptPath is given."
5351
5997
  },
5352
- argv: {
5353
- type: "array",
5354
- items: { type: "string" },
5355
- description: "Ordered native CLI options and values for one-command routes (e.g. rsync [\"-a\",\"--partial\",\"--dry-run\"], scp [\"-p\",\"-r\"], docker cp [\"-a\"], incus file push/pull). One element = one argv token, appended verbatim before src/dst; no shell parsing, no whitelist, no forced defaults — unknown options fail in the native CLI itself. rsync only (SSH endpoints): explicit -e/--rsh overrides the tool's SSH wiring. docker cp: pass -L to follow symlinks (a symlink source like /etc/os-release fails without it). Two-segment cross-endpoint copy uses pullArgv/pushArgv instead."
5356
- },
5357
- pullArgv: {
5358
- type: "array",
5359
- items: { type: "string" },
5360
- description: "Options for the PULL half (source endpoint → origin staging) of a two-command cross-endpoint copy (e.g. SSH↔Docker, SSH↔Incus, Docker↔Incus). Mutually exclusive with argv; must be paired with pushArgv. Only options valid for the pull half's CLI."
5998
+ stdin: {
5999
+ type: "string",
6000
+ description: "exec: with cmd/scriptPath = startup input (oneshot: piped; pty: REPL-style right after the command line; with scriptPath it is the script process's own stdin). Alone (no cmd/scriptPath) = a raw write into the PTY's RUNNING command — exact bytes; \"\" writes zero bytes (not EOF); \"\\n\" submits a line; \"\\u0003\" = Ctrl-C, \"\\u0004\" = Ctrl-D as raw characters; after a raw \"\\u0003\" settle the job with job_kill."
5361
6001
  },
5362
- pushArgv: {
5363
- type: "array",
5364
- items: { type: "string" },
5365
- description: "Options for the PUSH half (origin staging → destination endpoint) of a two-command cross-endpoint copy. Mutually exclusive with argv; must be paired with pullArgv."
6002
+ commandId: {
6003
+ type: "string",
6004
+ description: "exec stdin-only: optional guard — when given it must equal the running command's id (from the exec receipt or job_output); a stale id refuses the write instead of feeding the next command. Not valid with cmd/scriptPath."
5366
6005
  },
5367
- verify: {
5368
- type: "boolean",
5369
- description: "copy only. Receipts name the verification tier actually performed (D1=A): native CLI routes are \"native-exit-only\" (no independent check — self-verify via oneshot digest commands when needed); the Hub↔local staged lane computes independent SHA-256 digests by default and verify:false skips them (\"not-applicable\"). No hash tier exists on native routes."
6006
+ timeout: {
6007
+ type: "number",
6008
+ description: "exec: per-command timeout in seconds (default 120). oneshot: interrupts then kills the whole group. pty: the command is NOT killed on timeout — it hands off to a running job you can poll (job_output) and cancel (job_kill = Ctrl-C first; the session survives an interrupt)."
5370
6009
  },
5371
- mkdir: {
5372
- type: "boolean",
5373
- description: "copy only, all routes. Default true: before transfer, one best-effort exec pre-hop ensures the destination's PARENT directory exists (the operand itself is never pre-created — each CLI's own layout semantics stay byte-identical). Receipt created field: true (parent created this call) / false (already existed) / \"n-a\" (skipped or pre-hop unavailable, e.g. docker exec on a stopped container — the native CLI then reports the real error itself). false skips the pre-hop."
6010
+ mode: {
6011
+ type: "string",
6012
+ enum: ["pty", "oneshot"],
6013
+ description: "exec: pty (default) — a machine target reuses its default interactive PTY (cwd/env persist); oneshot — isolated, stateless, stdout/stderr split, verbatim exit. Redundant on pty:<id|label> targets; \"oneshot\" on an addressable PTY is an error."
5374
6014
  },
5375
6015
  run_in_background: {
5376
6016
  type: "boolean",
5377
- description: "oneshot AND pty: return a native jobId immediately, read with job_output, cancel with job_kill; output is available while running. On pty lanes the jobId IS the commandId — it tracks the running command inside a persistent session; job_kill sends Ctrl-C and the session survives. Hub-machine PTY has no background lane. Requires the official jobs service/controller."
5378
- },
5379
- target: {
5380
- type: "string",
5381
- description: "Single entry point. Machine targets: bare pool name, ssh:<name>, docker:<name>, incus:<name>, hub:<m4>. PTY: append :pty to create (ssh:dev4:pty, ssh:dev4:pty:build — label optional, every creation opens a new session) or address a live one with pty:<id|label>. BYO: pass spawn (pty) or ssh (identity) and the label lives in the same pool. Omit target → roster (or BYO auto-id when spawn/ssh present)."
6017
+ description: "exec with cmd/scriptPath only: the command went async (not bash &) — a running job returns immediately and a completion notification will follow, so no polling is required. Oneshot runs async; on a machine's default PTY the framed command starts and that PTY is handed off in the same operation; on an addressable PTY it keeps its identity. job_output reads the command's full stream from byte 0 (overlap with the pre-handoff receipt is expected); job_kill cancels. Requires the official jobs service/controller."
5382
6018
  },
5383
6019
  ssh: {
5384
6020
  type: "object",
5385
- description: "BYO ssh registration — the field present IS the entry point. First call registers (no cmd = register only); later calls reuse the label with ssh omitted. No inline keys or arbitrary SSH options.",
6021
+ description: "add: BYO ssh identity to register into the pool (no dialing). Later exec calls address the returned label. No inline keys or arbitrary SSH options; nothing is persisted — a new Agent registers again.",
5386
6022
  additionalProperties: false,
5387
6023
  properties: {
5388
6024
  host: {
@@ -5410,23 +6046,11 @@ function createRemoteTool(driver, opts = {}) {
5410
6046
  },
5411
6047
  spawn: {
5412
6048
  type: "string",
5413
- description: "BYO pty creation — the field present IS the entry point: the full command that starts an interactive shell (e.g. \"docker exec -it img bash\"). Pairs with target pty[:<label>] / a bare <label> / nothing (auto id). Nested hops are dumb pipes (only the innermost bash frames)."
5414
- },
5415
- cmd: {
5416
- type: "string",
5417
- description: "One raw shell string parsed by the remote bash (multi-line = one compound; exit = last command's). Omit with target → status/card; omit everything → roster."
5418
- },
5419
- scriptPath: {
5420
- type: "string",
5421
- description: "LOCAL absolute script path. Read before any dialing, sent literally over stdin to fixed bash -s. Oneshot only (no PTY lanes); cannot combine with cmd, stdin or spawn. Hub inline cmd remains limited to 4096; script body is not argv."
6049
+ description: "add: BYO PTY entry point — the full command that starts an interactive shell (e.g. \"docker exec -it img bash\"). Pairs with target pty:<label> / a bare <label> / nothing (auto id). Nested hops are dumb pipes (only the innermost bash frames)."
5422
6050
  },
5423
- stdin: {
5424
- type: "string",
5425
- description: "Input fed to the command. oneshot: piped to stdin. pty: written right after the command line (REPL-style). Requires cmd."
5426
- },
5427
- timeout: {
5428
- type: "number",
5429
- description: "Per-command timeout in seconds (default 120). oneshot: interrupts then kills the whole group. pty: the command is NOT killed on timeout — it hands off to a running job you can poll (job_output) and cancel (job_kill = Ctrl-C first; the session survives an interrupt)."
6051
+ force: {
6052
+ type: "boolean",
6053
+ description: "close only: default false — a session with a running job is refused (job_kill it first). true = kill the group and close in one step; the receipt lists what was killed."
5430
6054
  }
5431
6055
  },
5432
6056
  output: {
@@ -5439,12 +6063,12 @@ function createRemoteTool(driver, opts = {}) {
5439
6063
  required: true,
5440
6064
  enum: [
5441
6065
  "exec",
6066
+ "handoff",
5442
6067
  "status",
5443
6068
  "roster",
5444
6069
  "job",
5445
- "copy",
5446
- "rsync",
5447
- "close"
6070
+ "close",
6071
+ "input"
5448
6072
  ]
5449
6073
  },
5450
6074
  text: {
@@ -5476,6 +6100,10 @@ function createRemoteTool(driver, opts = {}) {
5476
6100
  type: "string",
5477
6101
  description: "ledger key; equals the jobId whenever a job exists"
5478
6102
  },
6103
+ writtenBytes: {
6104
+ type: "integer",
6105
+ description: "input: UTF-8 bytes actually written; confirms the write, not the program response"
6106
+ },
5479
6107
  envDelta: {
5480
6108
  type: "json",
5481
6109
  description: "names only: {added,changed,removed}"
@@ -5484,7 +6112,7 @@ function createRemoteTool(driver, opts = {}) {
5484
6112
  notice: { type: "string" },
5485
6113
  runner: {
5486
6114
  type: "string",
5487
- description: "oneshot supervision path: superd-run <ver> (<triple>) or posix-shim"
6115
+ description: "oneshot supervision path: superd-run <ver> (<triple>), posix-shim, or hub-lease (peer hub server supervises natively)"
5488
6116
  },
5489
6117
  running: { type: "boolean" },
5490
6118
  ttlSec: { type: "integer" },
@@ -5518,7 +6146,7 @@ function createRemoteTool(driver, opts = {}) {
5518
6146
  const description = tool.description;
5519
6147
  Object.defineProperty(tool, "description", {
5520
6148
  enumerable: true,
5521
- get: () => description + (driver.hasHub() ? "\nHub transport available: hub:<m4> selects a paired remote machine (never self). {} includes the Hub directory even when empty. Uses authenticated Hub HTTP oneshot / persistent WS PTY (hub:<m4>:pty selects the PTY lane); no SSH alias needed. Hub failures never fall back to SSH." : "")
6149
+ get: () => description + (driver.hasHub() ? "\nHub transport available: hub:<m4> selects a paired remote machine (never self; the roster lists them). Hub failures never fall back to SSH.\n- hub:<m4> oneshot: the peer hub server is the runner itself — bash -lc in a detached group, TERM→2s→KILL cleanup; the receipt runner field says \"hub-lease\" (the superd-run helper is a dumb-pipe artifact, never uploaded over the Hub). The peer caps inline commands at 4096 chars — and a scriptPath IS carried as one bash -c command, so hub-oneshot scripts share that cap (split large work or transfer the file and run it with cmd).\n- hub:<m4> as a machine target has the SAME default-PTY semantics as every other transport — exec opens the default session (a local pump child over one authenticated WebSocket to the peer terminal), handoff / background / stdin-only / close / idle TTL all work identically." : "")
5522
6150
  });
5523
6151
  return tool;
5524
6152
  }
@@ -5549,26 +6177,6 @@ function workbenchValue(r) {
5549
6177
  if (r.ttlSec !== void 0) v.ttlSec = r.ttlSec;
5550
6178
  return v;
5551
6179
  }
5552
- /** S8 (2026-10-17): copy/rsync render as one formatted line; the full receipt
5553
- * stays in the receipt field — raw-JSON dumps are gone from text. */
5554
- function formatTransferLine(action, receipt, durationMs) {
5555
- const r = receipt;
5556
- const parts = [];
5557
- if (r.ok === false) parts.push(`✗ ${r.reason ?? "failed"}`);
5558
- parts.push(`${action} ${r.src} → ${r.dst}`);
5559
- if (r.route !== void 0 && r.route !== "") parts.push(`route ${r.route}`);
5560
- if (r.exits !== void 0) parts.push(`exits [${r.exits.join(",")}]`);
5561
- else if (r.exit !== void 0) parts.push(`exit ${r.exit}`);
5562
- if (r.verification !== void 0) parts.push(`verification ${r.verification}`);
5563
- if (r.created === true) parts.push("created yes");
5564
- else if (r.created === false) parts.push("created pre-existing");
5565
- else if (r.created === "n-a") parts.push("created n-a");
5566
- if (r.cleanup !== void 0 && r.cleanup !== "none-required") parts.push(`cleanup ${r.cleanup}`);
5567
- if (typeof r.bytes === "number") parts.push(`${r.bytes} bytes / ${r.entries} entries`);
5568
- parts.push(`${(durationMs / 1e3).toFixed(1)}s`);
5569
- if (r.note !== void 0 && r.note.length > 0) parts.push(`(${r.note})`);
5570
- return parts.join(" · ");
5571
- }
5572
6180
  function renderEnvDelta(d) {
5573
6181
  const bits = [];
5574
6182
  if (d.added.length > 0) bits.push(`+${d.added.join(",+")}`);
@@ -5577,7 +6185,7 @@ function renderEnvDelta(d) {
5577
6185
  return bits.length > 0 ? bits.join(" ") : void 0;
5578
6186
  }
5579
6187
  function renderValue(v) {
5580
- if (v.kind === "status" || v.kind === "roster" || v.kind === "job" || v.kind === "copy" || v.kind === "rsync" || v.kind === "close") return v.text;
6188
+ if (v.kind === "status" || v.kind === "roster" || v.kind === "job" || v.kind === "close" || v.kind === "input" || v.kind === "handoff") return v.text;
5581
6189
  const parts = [];
5582
6190
  if (v.reconnected === true) parts.push(RECONNECT_NOTICE);
5583
6191
  if (typeof v.notice === "string" && v.notice.length > 0) parts.push(v.notice);
@@ -5598,15 +6206,188 @@ function renderValue(v) {
5598
6206
  markers.push(`${secs}s`);
5599
6207
  if (v.running === true) markers.push(`still running — poll job_output${v.jobId !== void 0 ? ` ${v.jobId}` : ""}`);
5600
6208
  if (v.truncated !== void 0) markers.push(`truncated, ${v.outputStatsUnavailable ? "returned peer tail" : "original"} ${v.truncated} ${v.outputStats?.stdout.unit === "bytes" ? "bytes" : "chars"}`);
5601
- if (v.outputStatsUnavailable) markers.push("peer output totals/retention unavailable; returned tail only");
6209
+ if (v.outputStatsUnavailable) markers.push("peer output totals/retention unavailable");
5602
6210
  parts.push(`[${markers.join(" · ")}]`);
5603
6211
  if (v.runner === "posix-shim" && (v.exit !== null && v.exit !== 0 || v.timedOut === true)) parts.push("note: this oneshot ran through the POSIX fallback on this target; PTY lanes use an independent lane and may still work here");
5604
6212
  if (v.stderr !== void 0 && v.stderr.length > 0) parts.push("[stderr]", v.stderr);
5605
6213
  return parts.join("\n");
5606
6214
  }
5607
6215
 
6216
+ //#endregion
6217
+ //#region src/remote/transfer-tool.ts
6218
+ const NO_SESSION_KEY = "remote:no-session";
6219
+ /**
6220
+ * The model-visible surface of the `remote_transfer` tool (split out of
6221
+ * `remote`, 2026-10-25). ONE template literal — no backticks, no "${".
6222
+ */
6223
+ const TRANSFER_TOOL_DESCRIPTION = `File transfer between any two supported endpoints. Every call declares its mode:
6224
+ mode:"copy" → tree copy. Native CLI per endpoint (scp for ssh, docker cp, incus file push/pull), a plain tree copy when both ends are local, and the Hub binary lane when a Hub machine is involved. Directories copy recursively; exit codes are authoritative.
6225
+ mode:"rsync" → rsync over SSH only: at least one endpoint remote SSH, the other local or SSH (both remote legs tunnel through this machine; identities never leave it). Hub and container endpoints are rejected — no silent copy fallback. Trailing slashes keep rsync semantics.
6226
+
6227
+ Endpoints are "[target:]absolute path". No target = the machine running this Agent. Targets are the same unified pool remote uses: a bare pool name, ssh:<name>, docker:<name>, incus:<name>, hub:<m4>. Examples: "/srv/app", "ssh:dev4:/srv/app", "docker:corti:/var/lib/x", "hub:m4:/srv". A path is never guessed: both operands must be absolute.
6228
+
6229
+ Native CLI options travel through argv verbatim — one element = one token, appended before src/dst, no shell parsing and no whitelist: rsync ["-a","--partial"], scp ["-p","-r"], docker cp ["-a","-L"], incus file push ["--recursive"]. Cross-endpoint copies that need two commands (SSH↔Docker, SSH↔Incus, Docker↔Incus) take pullArgv plus pushArgv instead of argv. rsync is a single SSH command, so it takes argv only; an explicit -e/--rsh overrides the wiring.
6230
+
6231
+ Options: verify (copy only) — the Hub↔local staged lane computes independent SHA-256 digests by default and verify:false skips them; native CLI routes have no independent hash tier, so verify is rejected there (self-verify through a oneshot digest command). mkdir (copy only, default true) — one best-effort pre-hop creates the destination PARENT directory; the operand itself is never pre-created, so each CLI's own layout semantics stay byte-identical.
6232
+
6233
+ Failures are loud: a non-zero native exit throws with the receipt instead of returning a normal-looking result. The receipt stays in the receipt field (route, exits, verification, bytes/entries, cleanup); text is one formatted line. For checksum-grade verification of a native route, run a digest command through remote oneshot or PTY instead of trusting the copy receipt.`;
6234
+ const KNOWN_PARAMS = new Set([
6235
+ "mode",
6236
+ "src",
6237
+ "dst",
6238
+ "argv",
6239
+ "pullArgv",
6240
+ "pushArgv",
6241
+ "verify",
6242
+ "mkdir"
6243
+ ]);
6244
+ async function transferExecute(ENV, args, exec) {
6245
+ for (const key of Object.keys(args)) if (!KNOWN_PARAMS.has(key)) throw new Error(`remote_transfer: unknown parameter "${key}" — this surface has no legacy aliases; pass native CLI options through argv instead`);
6246
+ if (args.mode !== "copy" && args.mode !== "rsync") throw new Error(`[E_PARAMS] remote_transfer: mode is required — "copy" (tree copy through each endpoint's native CLI or the Hub lane) or "rsync" (SSH only)`);
6247
+ if (typeof args.src !== "string" || args.src.length === 0 || typeof args.dst !== "string" || args.dst.length === 0) throw new Error(`[E_PARAMS] remote_transfer: ${args.mode} requires src and dst absolute endpoints`);
6248
+ const sessionKey = ENV.getSessionKey(exec);
6249
+ if (args.mode === "rsync") {
6250
+ if (args.pullArgv !== void 0 || args.pushArgv !== void 0) throw new Error("remote_transfer: rsync takes argv only; pullArgv/pushArgv are copy-only fields");
6251
+ if (args.verify !== void 0 || args.mkdir !== void 0) throw new Error("remote_transfer: rsync cannot be combined with copy verify/mkdir options; the Hub/local staged lane is copy-only");
6252
+ const start$1 = Date.now();
6253
+ const receipt$1 = await ENV.driver.rsync({
6254
+ src: args.src,
6255
+ dst: args.dst,
6256
+ ...args.argv === void 0 ? {} : { argv: args.argv }
6257
+ }, exec.signal, { sessionKey });
6258
+ if (receipt$1.ok === false || receipt$1.exit !== 0) throw new Error(JSON.stringify(receipt$1));
6259
+ const durationMs$1 = Date.now() - start$1;
6260
+ return {
6261
+ kind: "rsync",
6262
+ text: formatTransferLine("rsync", receipt$1, durationMs$1),
6263
+ receipt: receipt$1,
6264
+ exit: receipt$1.exit,
6265
+ durationMs: durationMs$1
6266
+ };
6267
+ }
6268
+ const start = Date.now();
6269
+ const receipt = await ENV.driver.copy({
6270
+ src: args.src,
6271
+ dst: args.dst,
6272
+ ...args.verify === void 0 ? {} : { verify: args.verify },
6273
+ ...args.mkdir === void 0 ? {} : { mkdir: args.mkdir },
6274
+ ...args.argv === void 0 ? {} : { argv: args.argv },
6275
+ ...args.pullArgv === void 0 ? {} : { pullArgv: args.pullArgv },
6276
+ ...args.pushArgv === void 0 ? {} : { pushArgv: args.pushArgv }
6277
+ }, exec.signal, { sessionKey });
6278
+ const asReceipt = receipt;
6279
+ const exits = asReceipt.exits ?? [];
6280
+ if (asReceipt.ok === false || exits.some((code) => code !== 0)) throw new Error(JSON.stringify(receipt));
6281
+ const durationMs = Date.now() - start;
6282
+ return {
6283
+ kind: "copy",
6284
+ text: formatTransferLine("copy", receipt, durationMs),
6285
+ receipt,
6286
+ exit: exits.length > 0 ? 0 : null,
6287
+ durationMs
6288
+ };
6289
+ }
6290
+ function createRemoteTransferTool(driver, opts = {}) {
6291
+ const getSessionKey = opts.getSessionKey ?? ((exec) => exec.agent?.id ?? NO_SESSION_KEY);
6292
+ return defineTool({
6293
+ name: "remote_transfer",
6294
+ description: TRANSFER_TOOL_DESCRIPTION,
6295
+ parameters: {
6296
+ mode: {
6297
+ type: "string",
6298
+ enum: ["copy", "rsync"],
6299
+ required: true,
6300
+ description: "Required. copy = tree copy through each endpoint's native CLI, or the Hub binary lane; rsync = SSH only (argv only, no verify/mkdir)."
6301
+ },
6302
+ src: {
6303
+ type: "string",
6304
+ description: "Source [target:]absolute path. No target = the machine running this Agent."
6305
+ },
6306
+ dst: {
6307
+ type: "string",
6308
+ description: "Destination [target:]absolute path. No target = the machine running this Agent."
6309
+ },
6310
+ argv: {
6311
+ type: "array",
6312
+ items: { type: "string" },
6313
+ description: "Ordered native CLI options and values for one-command routes (e.g. rsync [\"-a\",\"--partial\",\"--dry-run\"], scp [\"-p\",\"-r\"], docker cp [\"-a\",\"-L\"], incus file push/pull). One element = one argv token, appended verbatim before src/dst; no shell parsing, no whitelist, no forced defaults — unknown options fail in the native CLI itself. rsync only (SSH endpoints): explicit -e/--rsh overrides the tool's SSH wiring. docker cp: pass -L to follow symlinks (a symlink source like /etc/os-release fails without it). Two-segment cross-endpoint copy uses pullArgv/pushArgv instead."
6314
+ },
6315
+ pullArgv: {
6316
+ type: "array",
6317
+ items: { type: "string" },
6318
+ description: "Options for the PULL half (source endpoint → origin staging) of a two-command cross-endpoint copy (e.g. SSH↔Docker, SSH↔Incus, Docker↔Incus). Mutually exclusive with argv; must be paired with pushArgv. Only options valid for the pull half's CLI."
6319
+ },
6320
+ pushArgv: {
6321
+ type: "array",
6322
+ items: { type: "string" },
6323
+ description: "Options for the PUSH half (origin staging → destination endpoint) of a two-command cross-endpoint copy. Mutually exclusive with argv; must be paired with pullArgv."
6324
+ },
6325
+ verify: {
6326
+ type: "boolean",
6327
+ description: "copy only. Receipts name the verification tier actually performed (D1=A): native CLI routes are \"native-exit-only\" (no independent check — self-verify via oneshot digest commands when needed); the Hub↔local staged lane computes independent SHA-256 digests by default and verify:false skips them (\"not-applicable\"). No hash tier exists on native routes."
6328
+ },
6329
+ mkdir: {
6330
+ type: "boolean",
6331
+ description: "copy only, all routes. Default true: before transfer, one best-effort exec pre-hop ensures the destination's PARENT directory exists (the operand itself is never pre-created — each CLI's own layout semantics stay byte-identical). Receipt created field: true (parent created this call) / false (already existed) / \"n-a\" (skipped or pre-hop unavailable, e.g. docker exec on a stopped container — the native CLI then reports the real error itself). false skips the pre-hop."
6332
+ }
6333
+ },
6334
+ output: {
6335
+ schema: {
6336
+ type: "object",
6337
+ additionalProperties: false,
6338
+ properties: {
6339
+ kind: {
6340
+ type: "string",
6341
+ required: true,
6342
+ enum: ["copy", "rsync"]
6343
+ },
6344
+ text: {
6345
+ type: "string",
6346
+ required: true
6347
+ },
6348
+ receipt: { type: "json" },
6349
+ exit: { oneOf: [{ type: "integer" }, { type: "null" }] },
6350
+ durationMs: { type: "integer" }
6351
+ }
6352
+ },
6353
+ render: (_args, value) => [{
6354
+ type: "text",
6355
+ text: value.text
6356
+ }]
6357
+ },
6358
+ async execute(args, exec) {
6359
+ return transferExecute({
6360
+ driver,
6361
+ getSessionKey
6362
+ }, args, exec);
6363
+ }
6364
+ });
6365
+ }
6366
+ /** S8 (2026-10-17): copy/rsync render as one formatted line; the full receipt
6367
+ * stays in the receipt field — raw-JSON dumps are gone from text. */
6368
+ function formatTransferLine(mode, receipt, durationMs) {
6369
+ const r = receipt;
6370
+ const parts = [];
6371
+ if (r.ok === false) parts.push(`✗ ${r.reason ?? "failed"}`);
6372
+ parts.push(`${mode} ${r.src} → ${r.dst}`);
6373
+ if (r.route !== void 0 && r.route !== "") parts.push(`route ${r.route}`);
6374
+ if (r.exits !== void 0) parts.push(`exits [${r.exits.join(",")}]`);
6375
+ else if (r.exit !== void 0) parts.push(`exit ${r.exit}`);
6376
+ if (r.verification !== void 0) parts.push(`verification ${r.verification}`);
6377
+ if (r.created === true) parts.push("created yes");
6378
+ else if (r.created === false) parts.push("created pre-existing");
6379
+ else if (r.created === "n-a") parts.push("created n-a");
6380
+ if (r.cleanup !== void 0 && r.cleanup !== "none-required") parts.push(`cleanup ${r.cleanup}`);
6381
+ if (typeof r.bytes === "number") parts.push(`${r.bytes} bytes / ${r.entries} entries`);
6382
+ parts.push(`${(durationMs / 1e3).toFixed(1)}s`);
6383
+ if (r.note !== void 0 && r.note.length > 0) parts.push(`(${r.note})`);
6384
+ return parts.join(" · ");
6385
+ }
6386
+
5608
6387
  //#endregion
5609
6388
  //#region src/remote/agent-registration.ts
6389
+ /** The pair a row contributes: execution/PTY and file transfer are separate tools. */
6390
+ const TOOL_NAMES = ["remote", "remote_transfer"];
5610
6391
  /** Host listeners attempt once per official creation edge, never on vacancy. */
5611
6392
  function installAgentRemote(ctx, config) {
5612
6393
  return ctx.on("agent/created", ({ agent }) => {
@@ -5616,12 +6397,14 @@ function installAgentRemote(ctx, config) {
5616
6397
  ...config,
5617
6398
  getHub: () => agent.ctx.get("superdRemoteTransport")
5618
6399
  });
5619
- let unregister;
6400
+ const registered = [];
5620
6401
  try {
5621
- unregister = tools.register(createRemoteTool(driver, { getSessionKey: () => agent.id }));
6402
+ registered.push(tools.register(createRemoteTool(driver, { getSessionKey: () => agent.id })));
6403
+ registered.push(tools.register(createRemoteTransferTool(driver, { getSessionKey: () => agent.id })));
5622
6404
  } catch (error) {
6405
+ for (const undo of registered.splice(0)) undo();
5623
6406
  driver.dispose();
5624
- if (error instanceof Error && error.message === "tool \"remote\" is already registered in this scope" && tools.get("remote", agent) !== void 0) return;
6407
+ if (TOOL_NAMES.find((name$1) => error instanceof Error && error.message === `tool "${name$1}" is already registered in this scope` && tools.get(name$1, agent) !== void 0) !== void 0) return;
5625
6408
  throw error;
5626
6409
  }
5627
6410
  const stopService = agent.ctx.on("internal/service", (name$1) => {
@@ -5633,7 +6416,7 @@ function installAgentRemote(ctx, config) {
5633
6416
  const cleanup = () => {
5634
6417
  if (disposal) return disposal;
5635
6418
  disposal = driver.dispose();
5636
- unregister();
6419
+ for (const undo of registered.splice(0)) undo();
5637
6420
  stopService();
5638
6421
  ownerUndo?.();
5639
6422
  agentUndo?.();
@@ -5689,12 +6472,10 @@ const name = "dashr-remote";
5689
6472
  const inject = [];
5690
6473
  const Config = Schema.object({
5691
6474
  execTimeoutSec: Schema.natural().min(1).default(120),
5692
- idleTtlSec: Schema.natural().min(1).default(REMOTE_PTY_IDLE_TTL_SEC),
5693
- maxOutputChars: Schema.natural().min(1).default(3e4)
6475
+ idleTtlSec: Schema.natural().min(1).default(REMOTE_PTY_IDLE_TTL_SEC)
5694
6476
  }).default({
5695
6477
  execTimeoutSec: 120,
5696
- idleTtlSec: REMOTE_PTY_IDLE_TTL_SEC,
5697
- maxOutputChars: 3e4
6478
+ idleTtlSec: REMOTE_PTY_IDLE_TTL_SEC
5698
6479
  });
5699
6480
  /** MUST stay an arrow(RM0 plugin.ts 同注:function 声明会被 cordis 当构造器 new 掉,返回的 disposer 丢失)。 */
5700
6481
  const apply = (ctx, config) => {
@@ -5708,4 +6489,4 @@ var plugin_default = {
5708
6489
  };
5709
6490
 
5710
6491
  //#endregion
5711
- export { ByteTailRing, Config, FILE_FRAME_BYTES, FILE_MAX_ENTRIES, FILE_MAX_METADATA, PROCESS_CLEANUP_BOUND_MS, Utf16TailRing, apply, checkAbsolute, checkSelection, plugin_default as default, inject, localFileEndpoint, name, readUtf8Tail, runFileWorker, runRsync, sshArgv, startBinaryProcess, startOneShot, startRemoteJob };
6492
+ export { ByteTailRing, Config, FILE_FRAME_BYTES, FILE_MAX_ENTRIES, FILE_MAX_METADATA, PROCESS_CLEANUP_BOUND_MS, PtySession, Utf16TailRing, apply, buildHubPtyArgv, checkAbsolute, checkSelection, plugin_default as default, disclose, hubPtyAssets, inject, localFileEndpoint, name, readUtf8Tail, runFileWorker, runRsync, sshArgv, startBinaryProcess, startOneShot, startRemoteJob };