super-dsh 0.3.0-e → 0.3.0-g

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 (41) hide show
  1. package/README.md +13 -55
  2. package/client/index.js +141 -10
  3. package/cordis.patch.yml +6 -3
  4. package/package.json +12 -5
  5. package/remote/file-worker.js +341 -0
  6. package/remote/plugin.d.ts +523 -0
  7. package/remote/plugin.js +4979 -0
  8. package/remote/provenance.json +64 -0
  9. package/superd-hub/dist/a2a-row.d.ts +18 -2
  10. package/superd-hub/dist/a2a-row.js +11 -69
  11. package/superd-hub/dist/bundle-setup.d.ts +16 -6
  12. package/superd-hub/dist/bundle-setup.js +84 -148
  13. package/superd-hub/dist/client/companion-api.d.ts +5 -0
  14. package/superd-hub/dist/client/companion-api.js +30 -0
  15. package/superd-hub/dist/companion-contract.d.ts +9 -0
  16. package/superd-hub/dist/companion-contract.js +1 -0
  17. package/superd-hub/dist/companions.d.ts +30 -0
  18. package/superd-hub/dist/companions.js +209 -0
  19. package/superd-hub/dist/exec-pty-framing.d.ts +3 -1
  20. package/superd-hub/dist/exec-pty-framing.js +12 -4
  21. package/superd-hub/dist/exec-pty-session.d.ts +2 -0
  22. package/superd-hub/dist/exec-pty-session.js +7 -13
  23. package/superd-hub/dist/exec.d.ts +5 -2
  24. package/superd-hub/dist/exec.js +34 -54
  25. package/superd-hub/dist/fusion-transport.js +1 -0
  26. package/superd-hub/dist/gateway.js +24 -11
  27. package/superd-hub/dist/hub-tool.d.ts +3 -0
  28. package/superd-hub/dist/hub-tool.js +13 -5
  29. package/superd-hub/dist/remote-file-rpc.d.ts +20 -0
  30. package/superd-hub/dist/remote-file-rpc.js +574 -0
  31. package/superd-hub/dist/remote-job-client.d.ts +3 -0
  32. package/superd-hub/dist/remote-job-client.js +197 -0
  33. package/superd-hub/dist/remote-job-rpc.d.ts +21 -0
  34. package/superd-hub/dist/remote-job-rpc.js +155 -0
  35. package/superd-hub/dist/remote-provider.d.ts +13 -0
  36. package/superd-hub/dist/remote-provider.js +151 -0
  37. package/superd-hub/dist/remote-row.d.ts +6 -0
  38. package/superd-hub/dist/remote-row.js +13 -0
  39. package/superd-hub/lib/client/index.js +141 -10
  40. package/superd-hub/package.json +1 -2
  41. package/superd-hub/scripts/register-companion.mjs +0 -100
@@ -0,0 +1,209 @@
1
+ /** Companion intent and the single native installation operation. */
2
+ import { createHash } from 'node:crypto';
3
+ import { readFile, mkdir, unlink, rmdir } from 'node:fs/promises';
4
+ import { join, dirname } from 'node:path';
5
+ import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
6
+ // done marks a CONFIRMED first use (Better observed installed). A receipt with attempted
7
+ // but neither done nor error is an interrupted first use — e.g. killed by the restart
8
+ // that installing Super itself triggers — and must re-arm, not suppress, the retry.
9
+ const message = (error) => error instanceof Error ? error.message : String(error);
10
+ async function optionalJson(path) {
11
+ try {
12
+ return JSON.parse(await readFile(path, 'utf8'));
13
+ }
14
+ catch (error) {
15
+ if (error.code === 'ENOENT')
16
+ return undefined;
17
+ throw error;
18
+ }
19
+ }
20
+ function nativeFailure(result) {
21
+ if ((result.application === 'applied' || result.application === 'restart-required') && result.error === undefined
22
+ && (result.packageResult === undefined || result.packageResult.exitCode === 0))
23
+ return undefined;
24
+ return [result.application, result.error?.code, result.error?.diagnostic,
25
+ result.packageResult?.kind, result.packageResult?.logPath].filter(Boolean).join(': ');
26
+ }
27
+ /** One controller per native profile; no package, registry, HMR or selection writer of its own. */
28
+ export class BetterDashCompanion {
29
+ version;
30
+ manager;
31
+ directory;
32
+ receipt;
33
+ legacy;
34
+ initialized;
35
+ intent = { attempted: false };
36
+ active;
37
+ stopped = false;
38
+ restartNotice;
39
+ constructor(profile, version, manager) {
40
+ this.version = version;
41
+ this.manager = manager;
42
+ const key = createHash('sha256').update(profile.dir).digest('hex') + '.json';
43
+ this.directory = join(profile.home, 'superd', 'companions');
44
+ this.receipt = join(this.directory, key);
45
+ this.legacy = join(profile.home, 'companion-registration', key);
46
+ }
47
+ initialize() {
48
+ return this.initialized ??= (async () => {
49
+ const saved = await optionalJson(this.receipt);
50
+ if (saved !== undefined) {
51
+ // Never-resurrect applies only to a CONFIRMED first use or a recorded
52
+ // failure; an interrupted first use (no done, no error) re-arms.
53
+ this.intent = saved.attempted && saved.done !== true && saved.error === undefined
54
+ ? { attempted: false } : saved;
55
+ await this.cleanLegacy();
56
+ return;
57
+ }
58
+ const old = await optionalJson(this.legacy);
59
+ // Any old registration was first use. An absent Better now means removal,
60
+ // including when Super has since changed version; never resurrect it.
61
+ if (old?.['super-dsh:better-dsh'] !== undefined) {
62
+ this.intent = { attempted: true };
63
+ await this.save();
64
+ await this.cleanLegacy();
65
+ }
66
+ })();
67
+ }
68
+ async cleanLegacy() {
69
+ try {
70
+ await unlink(this.legacy);
71
+ }
72
+ catch (error) {
73
+ if (error.code !== 'ENOENT')
74
+ throw error;
75
+ }
76
+ try {
77
+ await rmdir(dirname(this.legacy));
78
+ }
79
+ catch (error) {
80
+ if (!['ENOENT', 'ENOTEMPTY'].includes(error.code ?? ''))
81
+ throw error;
82
+ }
83
+ }
84
+ async save() {
85
+ await mkdir(this.directory, { recursive: true, mode: 0o700 });
86
+ await writeFileAtomic(this.receipt, JSON.stringify(this.intent) + '\n', { mode: 0o600 });
87
+ }
88
+ async getBetterDash() {
89
+ const status = { installed: false, installing: this.active !== undefined, requestedVersion: this.version };
90
+ try {
91
+ await this.initialize();
92
+ const manager = this.manager();
93
+ if (manager === undefined)
94
+ throw new Error('Native Plugin Manager is unavailable; enable it and retry Install in Settings.');
95
+ const better = (await manager.listBundles()).find(bundle => bundle.name === 'better-dsh' && bundle.installed);
96
+ if (better !== undefined) {
97
+ status.installed = true;
98
+ if (better.version !== undefined)
99
+ status.version = better.version;
100
+ status.enabled = better.enabled;
101
+ // An installed dependency alone does not prove application succeeded:
102
+ // native configure can fail after writing that dependency. Only a
103
+ // missing -> installed recovery clears a previous failure here.
104
+ if (this.active === undefined && this.intent.attempted
105
+ && (this.intent.done !== true || (this.intent.error !== undefined && this.intent.missingAfterFailure === true))) {
106
+ this.intent.done = true;
107
+ delete this.intent.error;
108
+ delete this.intent.missingAfterFailure;
109
+ await this.save();
110
+ }
111
+ }
112
+ else {
113
+ this.restartNotice = undefined;
114
+ if (this.active === undefined && this.intent.error !== undefined && this.intent.missingAfterFailure !== true) {
115
+ this.intent.missingAfterFailure = true;
116
+ await this.save();
117
+ }
118
+ }
119
+ const error = this.intent.error ?? this.restartNotice;
120
+ if (error !== undefined)
121
+ status.error = error;
122
+ }
123
+ catch (error) {
124
+ status.error = message(error);
125
+ }
126
+ return status;
127
+ }
128
+ ensureAutomatic() { return this.run(false); }
129
+ installBetterDash() { return this.run(true); }
130
+ run(manual) {
131
+ if (this.active !== undefined)
132
+ return this.active;
133
+ const task = this.install(manual).catch(async (error) => {
134
+ this.intent.error = `Better Dash installation failed: ${message(error)}. Check Better Dash in Plugin Manager; if missing, retry Install in Settings.`;
135
+ const status = await this.getBetterDash();
136
+ this.intent.missingAfterFailure = !status.installed;
137
+ try {
138
+ await this.save();
139
+ }
140
+ catch { /* Report the operation error even when persistence is unavailable. */ }
141
+ return status;
142
+ }).then(status => ({ ...status, installing: false })).finally(() => { this.active = undefined; });
143
+ this.active = task;
144
+ return task;
145
+ }
146
+ async install(manual) {
147
+ await this.initialize();
148
+ const before = await this.getBetterDash();
149
+ if (this.stopped)
150
+ return before;
151
+ if (before.installed) {
152
+ this.intent.attempted = true;
153
+ this.intent.done = true;
154
+ if (this.intent.missingAfterFailure === true) {
155
+ delete this.intent.error;
156
+ delete this.intent.missingAfterFailure;
157
+ }
158
+ await this.save();
159
+ return this.getBetterDash(); // Native disablement and installed versions belong to the user.
160
+ }
161
+ // Native changed notifications enter through this operation, so its
162
+ // read is active and deliberately cannot update the receipt itself.
163
+ // Record removal here even when first-use installation is already done.
164
+ if (this.intent.error !== undefined && this.intent.missingAfterFailure !== true) {
165
+ this.intent.missingAfterFailure = true;
166
+ await this.save();
167
+ }
168
+ if (!manual && this.intent.attempted)
169
+ return before;
170
+ this.intent = { attempted: true };
171
+ await this.save(); // First use is durable before native HMR can unload this row.
172
+ const manager = this.manager();
173
+ if (manager === undefined)
174
+ throw new Error('Native Plugin Manager is unavailable');
175
+ if (this.version === 'unknown' || this.version.trim() === '')
176
+ throw new Error('The installed Super Dash package version could not be read');
177
+ const failures = [];
178
+ for (const spec of [`better-dsh@${this.version}`, 'better-dsh@latest', 'better-dsh']) {
179
+ if (this.stopped)
180
+ break;
181
+ try {
182
+ const result = await manager.installBundle(spec);
183
+ const failure = nativeFailure(result);
184
+ if (failure === undefined) {
185
+ const after = await this.getBetterDash();
186
+ if (after.installed) {
187
+ this.intent.done = true;
188
+ delete this.intent.error;
189
+ delete this.intent.missingAfterFailure;
190
+ this.restartNotice = result.application === 'restart-required'
191
+ ? 'Better Dash is installed; restart DSH to apply the native change.' : undefined;
192
+ await this.save();
193
+ return this.getBetterDash();
194
+ }
195
+ failures.push(`${spec}: native manager reported ${result.application}, but Better Dash is not installed`);
196
+ }
197
+ else
198
+ failures.push(`${spec}: ${failure}`);
199
+ }
200
+ catch (error) {
201
+ failures.push(`${spec}: ${message(error)}`);
202
+ }
203
+ }
204
+ throw new Error(failures.join('; ') || 'Super Dash companion component was unloaded');
205
+ }
206
+ /** Read-only observation: an intermediate native event must never trigger reinstatement. */
207
+ async observeNativeChange() { return this.getBetterDash(); }
208
+ dispose() { this.stopped = true; }
209
+ }
@@ -1,3 +1,4 @@
1
+ import type { OutputStats } from '../../remote/plugin.js';
1
2
  /** 收帧帧体:退出码 + 只读回传的 cwd(Non-Goal ②:报告不重建)。 */
2
3
  export interface PtyFrame {
3
4
  exit: number;
@@ -11,9 +12,10 @@ export declare function wrapPtyCommand(cmd: string, nonce: string): string;
11
12
  export declare function buildInitCommand(nonce: string): string;
12
13
  export declare function stripAnsi(s: string): string;
13
14
  export declare function normalizePtyText(s: string): string;
14
- export declare function tailWindow(text: string, cap: number): {
15
+ export declare function tailWindow(text: string, cap: number, stats?: OutputStats): {
15
16
  text: string;
16
17
  truncated?: number;
18
+ stats?: OutputStats;
17
19
  };
18
20
  export interface FrameParserEvents {
19
21
  /** 输出文本:feed 路径经 normalize(剥 ANSI/规整换行);flush 路径为**裸放出**(残余字节原样,勿假设已剥洗)。 */
@@ -23,10 +23,18 @@ export function stripAnsi(s) {
23
23
  export function normalizePtyText(s) {
24
24
  return stripAnsi(s).replace(/\r\n/g, '\n').replace(/\r/g, '');
25
25
  }
26
- export function tailWindow(text, cap) {
27
- if (text.length <= cap)
28
- return { text };
29
- return { text: `[truncated: showing last ${cap} of ${text.length} chars]\n${text.slice(text.length - cap)}`, truncated: text.length };
26
+ export function tailWindow(text, cap, stats) {
27
+ let tail = text.slice(-cap);
28
+ if (tail.charCodeAt(0) >= 0xdc00 && tail.charCodeAt(0) <= 0xdfff)
29
+ tail = tail.slice(1);
30
+ const returned = stats?.unit === 'bytes' ? Buffer.byteLength(tail) : tail.length;
31
+ const total = stats?.total ?? text.length;
32
+ const updated = stats === undefined ? {} : { stats: { ...stats, returned } };
33
+ if (total <= returned)
34
+ return { text: tail, ...updated };
35
+ const unit = stats?.unit === 'bytes' ? 'bytes' : 'chars';
36
+ const eviction = stats !== undefined && stats.dropped > 0 ? `; retained ${stats.retained}, dropped head ${stats.dropped}, retention limit ${stats.retentionLimit} ${stats.unit === 'bytes' ? 'bytes' : 'UTF-16 code units'}` : '';
37
+ return { text: `[truncated: showing last ${returned} of ${total} ${unit}${eviction}]\n${tail}`, truncated: total, ...updated };
30
38
  }
31
39
  const FRAME_BODY_RE = /^(\d+);([^\x07\x1b]*)(?:\x07|\x1b\\)/; // cwd 是尾字段:合法路径可含 `;`,只有 BEL/ESC 终止它
32
40
  /**
@@ -1,3 +1,4 @@
1
+ import { type StreamOutputStats } from '../../remote/plugin.js';
1
2
  import type { PtyChannel } from './exec-pty-channel.js';
2
3
  /** spec §五.2 逐字。 */
3
4
  export declare const RECONNECT_NOTICE = "[remote: session reconnected to fresh shell; cwd reset to default]";
@@ -15,6 +16,7 @@ export interface PtyDispatchOptions {
15
16
  }
16
17
  export interface PtyDispatchResult {
17
18
  output: string;
19
+ outputStats?: StreamOutputStats;
18
20
  exit: number | null;
19
21
  cwd: string | null;
20
22
  timedOut: boolean;
@@ -1,3 +1,4 @@
1
+ import { Utf16TailRing } from '../../remote/plugin.js';
1
2
  // PTY session machinery — PORTED from better-dsh src/remote/pty-session.ts
2
3
  // (spec §8.2 / D12 "原样照抄"), with the ONE sanctioned adaptation: the
3
4
  // subprocess spawn is replaced by the PtyChannel seam (exec-pty-channel.ts) —
@@ -41,12 +42,12 @@ export class PtySession {
41
42
  this.inFlight++;
42
43
  return await new Promise((resolve) => {
43
44
  const nonce = genNonce();
44
- let output = '';
45
+ const ring = new Utf16TailRing(OUTPUT_HARD_CAP);
45
46
  let interrupted = false;
46
47
  let settled = false;
47
48
  let interruptTimer;
48
49
  let killTimer;
49
- const deathWaiter = () => finish({ output, exit: null, cwd: null, timedOut: interrupted });
50
+ const deathWaiter = () => finish({ output: ring.text, exit: null, cwd: null, timedOut: interrupted });
50
51
  const cleanup = () => {
51
52
  this.onChunk = null;
52
53
  // I1(终审):已结算 dispatch 的死亡等待必须摘除——否则闭包钉住整段 output,
@@ -65,20 +66,13 @@ export class PtySession {
65
66
  this.inFlight--;
66
67
  this.lastSettleAt = Date.now();
67
68
  this.resetIdleTimer();
68
- resolve({ ...r, reconnected, durationMs: Date.now() - started });
69
+ resolve({ ...r, outputStats: { stdout: ring.stats() }, reconnected, durationMs: Date.now() - started });
69
70
  };
70
71
  const parser = createNonceFrameParser(nonce, {
71
72
  onOutput: (t) => {
72
- output += t;
73
- // C1(终审):内存安全帽——无帽时一条 cat /dev/urandom 能在 120s 看门狗内
74
- // 吃垮宿主 daemon。帽上切片避开代理对(首字符为低位代理则让出一字符)。
75
- if (output.length > OUTPUT_HARD_CAP) {
76
- output = output.slice(output.length - OUTPUT_HARD_CAP);
77
- if (output.charCodeAt(0) >= 0xdc00 && output.charCodeAt(0) <= 0xdfff)
78
- output = output.slice(1);
79
- }
73
+ ring.push(t);
80
74
  },
81
- onFrame: ({ exit, cwd }) => finish({ output, exit, cwd, timedOut: interrupted }),
75
+ onFrame: ({ exit, cwd }) => finish({ output: ring.text, exit, cwd, timedOut: interrupted }),
82
76
  });
83
77
  this.onChunk = (c) => parser.feed(c);
84
78
  this.write(wrapPtyCommand(cmd, nonce));
@@ -97,7 +91,7 @@ export class PtySession {
97
91
  // (否则 trap-INT 的卡死进程永生,重连后旧组再不可达)
98
92
  this.opts.channel.kill();
99
93
  this.markDead();
100
- finish({ output, exit: null, cwd: null, timedOut: true });
94
+ finish({ output: ring.text, exit: null, cwd: null, timedOut: true });
101
95
  }, INTERRUPT_GRACE_MS);
102
96
  }, dOpts.timeoutSec === undefined ? 120_000 : dOpts.timeoutSec * 1_000);
103
97
  this.deathWaiters.push(deathWaiter);
@@ -1,8 +1,10 @@
1
+ import { type StreamOutputStats } from '../../remote/plugin.js';
1
2
  import type { A2aLogFn } from './a2a-log.js';
2
3
  /** Shared-channel Fetch route path this API owns. */
3
4
  export declare const EXEC_API_BASE = "/api/superd/exec";
4
5
  export interface OneShotExecInput {
5
6
  cmd: string;
7
+ stdin?: string;
6
8
  cwd?: string;
7
9
  timeoutSec?: number;
8
10
  }
@@ -12,6 +14,7 @@ export interface OneShotExecResult {
12
14
  stderr: string;
13
15
  durationMs: number;
14
16
  truncated: boolean;
17
+ outputStats?: StreamOutputStats;
15
18
  timedOut: boolean;
16
19
  /**
17
20
  * Why the process ended (0.1.7-e, first-person finding F1/F2: `exit:null`
@@ -24,9 +27,9 @@ export interface OneShotExecResult {
24
27
  */
25
28
  outcome: 'exited' | 'signaled' | 'timeout' | 'spawn-error';
26
29
  }
27
- /** Per-stream tail window returned to the caller (spec §8.1: 30k). */
30
+ /** Per-stream returned tail: at most 30,000 bytes, beginning on a UTF8 boundary. */
28
31
  export declare const EXEC_TAIL_WINDOW = 30000;
29
- /** Per-stream hard retention while running (spec §8.1: 4M ring). */
32
+ /** Hub oneshot per-stream raw-byte retention: 4MiB, distinct from native/PTy UTF16 caps. */
30
33
  export declare const EXEC_RING_HARD_CAP: number;
31
34
  export declare const EXEC_DEFAULT_TIMEOUT_SEC = 120;
32
35
  export declare const EXEC_MAX_TIMEOUT_SEC = 600;
@@ -12,11 +12,12 @@
12
12
  // process GROUP, 2s grace, SIGKILL to the group — `bash -lc` children must
13
13
  // not outlive their parent's timeout.
14
14
  import { spawn } from 'node:child_process';
15
+ import { ByteTailRing } from '../../remote/plugin.js';
15
16
  /** Shared-channel Fetch route path this API owns. */
16
17
  export const EXEC_API_BASE = '/api/superd/exec';
17
- /** Per-stream tail window returned to the caller (spec §8.1: 30k). */
18
+ /** Per-stream returned tail: at most 30,000 bytes, beginning on a UTF8 boundary. */
18
19
  export const EXEC_TAIL_WINDOW = 30_000;
19
- /** Per-stream hard retention while running (spec §8.1: 4M ring). */
20
+ /** Hub oneshot per-stream raw-byte retention: 4MiB, distinct from native/PTy UTF16 caps. */
20
21
  export const EXEC_RING_HARD_CAP = 4 * 1024 * 1024;
21
22
  export const EXEC_DEFAULT_TIMEOUT_SEC = 120;
22
23
  export const EXEC_MAX_TIMEOUT_SEC = 600;
@@ -44,7 +45,9 @@ export function parseOneShotExecBody(body) {
44
45
  if (raw.timeoutSec !== undefined && (typeof raw.timeoutSec !== 'number' || !Number.isInteger(raw.timeoutSec) || raw.timeoutSec < 1 || raw.timeoutSec > EXEC_MAX_TIMEOUT_SEC)) {
45
46
  return { ok: false, error: `exec: timeoutSec must be an integer in [1, ${EXEC_MAX_TIMEOUT_SEC}]` };
46
47
  }
47
- const known = new Set(['cmd', 'cwd', 'timeoutSec']);
48
+ if (raw.stdin !== undefined && typeof raw.stdin !== 'string')
49
+ return { ok: false, error: 'exec: stdin must be a string' };
50
+ const known = new Set(['cmd', 'cwd', 'timeoutSec', 'stdin']);
48
51
  for (const key of Object.keys(raw)) {
49
52
  if (!known.has(key))
50
53
  return { ok: false, error: `exec: unknown field "${key}"` };
@@ -53,71 +56,29 @@ export function parseOneShotExecBody(body) {
53
56
  ok: true,
54
57
  value: {
55
58
  cmd: raw.cmd,
59
+ ...(raw.stdin === undefined ? {} : { stdin: raw.stdin }),
56
60
  ...(raw.cwd === undefined ? {} : { cwd: raw.cwd }),
57
61
  ...(raw.timeoutSec === undefined ? {} : { timeoutSec: raw.timeoutSec }),
58
62
  },
59
63
  };
60
64
  }
61
- /**
62
- * A capped tail-retaining accumulator: `total` counts every byte ever seen,
63
- * `ring` keeps at most EXEC_RING_HARD_CAP of the most recent ones.
64
- */
65
- class TailRing {
66
- total = 0;
67
- chunks = [];
68
- retained = 0;
69
- push(chunk) {
70
- this.total += chunk.length;
71
- this.chunks.push(chunk);
72
- this.retained += chunk.length;
73
- if (this.retained > EXEC_RING_HARD_CAP) {
74
- let drop = this.retained - EXEC_RING_HARD_CAP;
75
- while (drop > 0 && this.chunks.length > 0) {
76
- const head = this.chunks[0];
77
- if (head.length <= drop) {
78
- drop -= head.length;
79
- this.retained -= head.length;
80
- this.chunks.shift();
81
- }
82
- else {
83
- this.chunks[0] = head.subarray(drop);
84
- this.retained -= drop;
85
- drop = 0;
86
- }
87
- }
88
- }
89
- }
90
- tail(maxBytes) {
91
- if (this.retained <= maxBytes)
92
- return Buffer.concat(this.chunks);
93
- let skip = this.retained - maxBytes;
94
- const out = [];
95
- for (const chunk of this.chunks) {
96
- if (skip >= chunk.length) {
97
- skip -= chunk.length;
98
- continue;
99
- }
100
- out.push(skip === 0 ? chunk : chunk.subarray(skip));
101
- skip = 0;
102
- }
103
- return Buffer.concat(out);
104
- }
105
- }
106
65
  /**
107
66
  * Execute one command locally: `bash -lc <cmd>`, detached into its own
108
67
  * process group so the timeout's group kill cannot leak to the hub process.
109
68
  */
110
69
  export async function runOneShotExec(input, log) {
111
70
  const started = Date.now();
112
- const stdout = new TailRing();
113
- const stderr = new TailRing();
71
+ const stdout = new ByteTailRing(EXEC_RING_HARD_CAP);
72
+ const stderr = new ByteTailRing(EXEC_RING_HARD_CAP);
114
73
  const timeoutSec = input.timeoutSec ?? EXEC_DEFAULT_TIMEOUT_SEC;
115
74
  let terminationRequested = false;
116
75
  const child = spawn('bash', ['-lc', input.cmd], {
117
76
  ...(input.cwd === undefined ? {} : { cwd: input.cwd }),
118
77
  detached: true,
119
- stdio: ['ignore', 'pipe', 'pipe'],
78
+ stdio: ['pipe', 'pipe', 'pipe'],
120
79
  });
80
+ child.stdin.on('error', () => { });
81
+ child.stdin.end(input.stdin);
121
82
  child.stdout.on('data', (chunk) => stdout.push(chunk));
122
83
  child.stderr.on('data', (chunk) => stderr.push(chunk));
123
84
  let graceTimer;
@@ -148,10 +109,11 @@ export async function runOneShotExec(input, log) {
148
109
  clearTimeout(killTimer);
149
110
  if (graceTimer !== undefined)
150
111
  clearTimeout(graceTimer);
151
- const outText = stdout.tail(EXEC_TAIL_WINDOW).toString('utf8');
112
+ const outTail = stdout.tail(EXEC_TAIL_WINDOW), errTail = stderr.tail(EXEC_TAIL_WINDOW);
113
+ const outText = outTail.toString('utf8');
152
114
  const errText = spawnError !== undefined
153
115
  ? `exec: spawn failed — ${spawnError}${input.cwd === undefined ? '' : ` (cwd: ${input.cwd})`}`
154
- : stderr.tail(EXEC_TAIL_WINDOW).toString('utf8');
116
+ : errTail.toString('utf8');
155
117
  const timedOut = terminationRequested;
156
118
  const outcome = spawnError !== undefined
157
119
  ? 'spawn-error'
@@ -165,7 +127,8 @@ export async function runOneShotExec(input, log) {
165
127
  stdout: outText,
166
128
  stderr: errText,
167
129
  durationMs: Date.now() - started,
168
- truncated: stdout.total > EXEC_TAIL_WINDOW || stderr.total > EXEC_TAIL_WINDOW,
130
+ truncated: stdout.total > outTail.length || stderr.total > errTail.length,
131
+ outputStats: { stdout: stdout.stats(outTail.length), stderr: stderr.stats(errTail.length) },
169
132
  timedOut,
170
133
  outcome,
171
134
  };
@@ -221,5 +184,22 @@ export async function postOneShotExec(origin, input, opts = {}) {
221
184
  truncated: raw.truncated === true,
222
185
  timedOut,
223
186
  outcome,
187
+ ...(parseOutputStats(raw.outputStats) === undefined ? {} : { outputStats: parseOutputStats(raw.outputStats) }),
188
+ };
189
+ }
190
+ /** Older peers omit metrics: do not derive exact totals from their returned tail. */
191
+ function parseOutputStats(value) {
192
+ if (typeof value !== 'object' || value === null)
193
+ return undefined;
194
+ const raw = value;
195
+ const valid = (stats) => {
196
+ if (typeof stats !== 'object' || stats === null)
197
+ return false;
198
+ const s = stats;
199
+ return (s.unit === 'bytes' || s.unit === 'utf16') && [s.total, s.retained, s.dropped, s.returned, s.retentionLimit].every(n => Number.isSafeInteger(n) && n >= 0)
200
+ && s.total === s.retained + s.dropped && s.retained <= s.retentionLimit && s.returned <= s.retained;
224
201
  };
202
+ if (!valid(raw.stdout) || (raw.stderr !== undefined && !valid(raw.stderr)))
203
+ return undefined;
204
+ return { stdout: raw.stdout, ...(raw.stderr === undefined ? {} : { stderr: raw.stderr }) };
225
205
  }
@@ -132,6 +132,7 @@ try {
132
132
  // is active.
133
133
  const isExempt = (path) => path === CONTROL || path.startsWith(CONTROL + '/')
134
134
  || path === CONTROL_PREFIX || path.startsWith(CONTROL_PREFIX + '/')
135
+ || path === '/api/superdCompanions' || path.startsWith('/api/superdCompanions/')
135
136
  // Key compare matches the server demux shape: searchParams.get('agent')
136
137
  // looks up PERCENT-DECODED keys, so '%61gent=x' demuxes as agent=x and the
137
138
  // surgery must strip those spellings too (+' ' wins the '+' spelling).
@@ -1,3 +1,5 @@
1
+ import { registerRemoteFileRpc } from './remote-file-rpc.js';
2
+ import { installHubRemoteTransport } from './remote-row.js';
1
3
  import z from '@deepseek-ai/schemastery';
2
4
  import { registerSelectorRpc, registryTargets } from './rpc.js';
3
5
  import { registerMeshRpc } from './mesh-rpc.js';
@@ -60,6 +62,7 @@ export function apply(ctx, config) {
60
62
  const real = host.get('webServer');
61
63
  if (real instanceof WorldWebServer)
62
64
  return;
65
+ installHubRemoteTransport(host, { home });
63
66
  registerPairingAuth(host);
64
67
  const connection = host.get('connection');
65
68
  // The gate is ctx0's authorize source (connection.authorizeIndex); the
@@ -82,24 +85,34 @@ export function apply(ctx, config) {
82
85
  // would multiply probes against every paired remote. The effect's
83
86
  // disposer IS the sweep disposer — the timer dies with the row
84
87
  // (unref'd, overlap-skipping, per-machine errors swallowed; mesh-link).
85
- ctx.effect(() => startReprobeSweep({ home, intervalSec: config?.reprobeIntervalSec, immediate: true }), 'superd-hub: mesh reprobe sweep');
86
- const dispose = registerHubRoute({
88
+ host.effect(() => startReprobeSweep({ home, intervalSec: config?.reprobeIntervalSec, immediate: true }), 'superd-hub: mesh reprobe sweep');
89
+ host.effect(() => registerHubRoute({
87
90
  real: real,
88
91
  authorize: (req, res) => connection.authorizeIndex(req, res),
89
92
  requestRejection: (req) => connection.requestRejection(req),
90
93
  gatewayOf: (label) => (federatedGatewayOf(label) ?? getTarget(label)?.gateway),
91
- });
92
- ctx.effect(() => dispose, 'superd-hub: /hub route family');
94
+ }), 'superd-hub: /hub route family');
93
95
  // P4 §8.2: the PTY executor's upgrade route — same gate as the /hub family
94
96
  // (authorizeIndex cookie auth first, then the browser-trust fence). The
95
- // dumb pump lives in exec-pty-rpc.ts; a2a-row owns the initiator half.
96
- void registerPtyExecutor(real, {
97
+ // dumb pump lives in exec-pty-rpc.ts; remote-provider owns the shared initiator implementation.
98
+ // The async effect belongs to this Connection injection: disposal waits
99
+ // for setup and releases its route even when setup finishes during unload.
100
+ host.effect(() => registerRemoteFileRpc(real, {
97
101
  authorize: (req, res) => connection.authorizeIndex(req, res),
98
- requestRejection: (req) => connection.requestRejection(req),
99
- log: consoleA2aLog(),
100
- }).then((disposePty) => {
101
- ctx.effect(() => disposePty, 'superd-hub: PTY executor upgrade route');
102
- }, () => { });
102
+ requestRejection: req => connection.requestRejection(req),
103
+ }), 'superd-hub: binary file upgrade route');
104
+ host.effect(async () => {
105
+ try {
106
+ return await registerPtyExecutor(real, {
107
+ authorize: (req, res) => connection.authorizeIndex(req, res),
108
+ requestRejection: (req) => connection.requestRejection(req),
109
+ log: consoleA2aLog(),
110
+ });
111
+ }
112
+ catch {
113
+ return () => { };
114
+ }
115
+ }, 'superd-hub: PTY executor upgrade route');
103
116
  });
104
117
  }
105
118
  export default apply;
@@ -18,6 +18,7 @@ export interface HubToolExecFace {
18
18
  */
19
19
  pty(sessionKey: string, machine: string, cmd: string, opts?: {
20
20
  timeoutSec?: number;
21
+ stdin?: string;
21
22
  }): Promise<import('./exec-pty-session.js').PtyDispatchResult>;
22
23
  }
23
24
  import type { A2aAddress, A2aInitiator, A2aService } from './a2a-service.js';
@@ -71,6 +72,8 @@ export interface HubRosterView {
71
72
  }
72
73
  /** Everything the factory needs, all injected (pure module — see header). */
73
74
  export interface HubToolDeps {
75
+ /** Explicit compatibility opt-in, never applied to receiving peer executors. */
76
+ localExec?: boolean;
74
77
  /** The A2A conversation service (T2 core behind T3's host assembly). */
75
78
  service: HubToolService;
76
79
  /** T3's delivery face: live-agent routing + the bounded unread buffer. */