super-dsh 0.3.2 → 0.3.3

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 (48) hide show
  1. package/agent-agy/dist/agent.d.ts +4 -1
  2. package/agent-agy/dist/agent.js +15 -1
  3. package/agent-agy/dist/index.js +5 -8
  4. package/agent-agy/package.json +6 -6
  5. package/agent-agy-sdk/dist/agent.d.ts +4 -1
  6. package/agent-agy-sdk/dist/agent.js +15 -1
  7. package/agent-agy-sdk/dist/index.js +5 -8
  8. package/agent-agy-sdk/package.json +5 -5
  9. package/agent-claude/dist/agent.js +15 -1
  10. package/agent-claude/package.json +8 -8
  11. package/agent-codex/dist/agent.js +15 -1
  12. package/agent-codex/package.json +7 -7
  13. package/agent-codex-sdk/dist/agent.js +15 -1
  14. package/agent-codex-sdk/dist/index.js +5 -11
  15. package/agent-codex-sdk/package.json +7 -7
  16. package/agent-hermes/dist/agent.js +15 -1
  17. package/agent-hermes/dist/index.js +5 -8
  18. package/agent-hermes/package.json +7 -7
  19. package/agent-omp/dist/agent.js +26 -2
  20. package/agent-omp/package.json +8 -8
  21. package/agent-omp-sdk/dist/agent.d.ts +4 -1
  22. package/agent-omp-sdk/dist/agent.js +15 -1
  23. package/agent-omp-sdk/package.json +8 -8
  24. package/agent-pi/dist/agent.js +20 -2
  25. package/agent-pi/dist/index.js +5 -11
  26. package/agent-pi/package.json +7 -7
  27. package/package.json +21 -21
  28. package/remote/hub-pty-pump.mjs +97 -0
  29. package/remote/plugin.d.ts +406 -203
  30. package/remote/plugin.js +631 -319
  31. package/remote/provenance.json +15 -12
  32. package/superd-hub/dist/agent-model-face.d.ts +93 -0
  33. package/superd-hub/dist/agent-model-face.js +113 -0
  34. package/superd-hub/dist/exec.d.ts +3 -2
  35. package/superd-hub/dist/exec.js +6 -5
  36. package/superd-hub/dist/hub-tool.d.ts +1 -1
  37. package/superd-hub/dist/hub-tool.js +6 -6
  38. package/superd-hub/dist/index.d.ts +1 -0
  39. package/superd-hub/dist/index.js +1 -0
  40. package/superd-hub/dist/remote-provider.js +70 -36
  41. package/superd-hub/dist/world-entry.js +30 -0
  42. package/superd-hub/package.json +14 -10
  43. package/superd-hub/dist/exec-pty-channel.d.ts +0 -58
  44. package/superd-hub/dist/exec-pty-channel.js +0 -162
  45. package/superd-hub/dist/exec-pty-framing.d.ts +0 -34
  46. package/superd-hub/dist/exec-pty-framing.js +0 -89
  47. package/superd-hub/dist/exec-pty-session.d.ts +0 -78
  48. package/superd-hub/dist/exec-pty-session.js +0 -228
@@ -3,24 +3,25 @@
3
3
  "entry": "better-dsh/src/remote/plugin.ts",
4
4
  "workerEntry": "better-dsh/src/remote/file-worker.ts",
5
5
  "sources": {
6
- "better-dsh/src/remote/agent-registration.ts": "459301354ea1ab713984bb01bc491a080256d4e983e55ff074e2665c3213d355",
6
+ "better-dsh/src/remote/agent-registration.ts": "cb08eb9f5d592201054410da69b3bf383e2604899d9494a8cbd8ba2f86b3f7ad",
7
7
  "better-dsh/src/remote/archive.ts": "fd32f96dc63421be7d56eb545badf355c6ee2498599605ffa48958b9efca9a63",
8
8
  "better-dsh/src/remote/copy.ts": "ab2aec5ee8df814f4bc568370977d229c402489248c738eff147431a52650c52",
9
- "better-dsh/src/remote/driver.ts": "3b0ce9edeb3a3b4f6c6e3ff22df2b0bfb64d46944271333f6518c0e25f00e97a",
9
+ "better-dsh/src/remote/driver.ts": "8bfa05e056b9099a1d9fcdf6cb67d15ec6e9a895ea061ea2e65cd3a513fe4fdf",
10
10
  "better-dsh/src/remote/endpoints.ts": "e4ab0fa1afcee815ad78976eba4036e2ed2b21eaea11f7451874b56229f12eac",
11
11
  "better-dsh/src/remote/file-worker.ts": "31ac019a944cf7fdcd0ff044f8b330d923bf4d384c77056f9b7e3032bc8c9598",
12
12
  "better-dsh/src/remote/helper.ts": "789a802610fc480bb1c922dd284787ca22d7394fa906c729ee33d78ade79973c",
13
- "better-dsh/src/remote/hub-contract.ts": "9801ac934553a57fe1a7aeb273175e68be57163e21606c35728901654f702770",
13
+ "better-dsh/src/remote/hub-contract.ts": "a7ae5501c4ffe63ced1526c2d64d7d83798fc2025f9b4aea998e033a124fe598",
14
+ "better-dsh/src/remote/hub-pty-pump.mjs": "dfe0cb331cc585eb4d858fbb6d8f3ff22414f3ee55109c933df0fc14502aee5c",
14
15
  "better-dsh/src/remote/input.ts": "dfe087d34e9e9aefc0d8ec2e33371dc86981b05723d436419ad1afb6ac8426b4",
15
- "better-dsh/src/remote/jobs.ts": "f6c6d1e6fe9e93021923b0f8c3977bb0d6a58d0145764a14df92a84d96d43d0d",
16
+ "better-dsh/src/remote/jobs.ts": "4556bd9f30580a8bf90800c7a945282e276abb548df6db7fe95e7091bb1543c6",
16
17
  "better-dsh/src/remote/labels.ts": "d9dcfdc1c6bb292b37d3250e7897b239c416b55b3102943bcc4f1d0ce7d2e709",
17
18
  "better-dsh/src/remote/native-copy.ts": "3f8691abb14dfb7a94e956956fb4d182b7732ffbe70f1a8ff7dec5dd2b9afb7c",
18
- "better-dsh/src/remote/nonce-framing.ts": "e2590293fe44c2997f6d65e0d6828aa67e51e0fc6d12cb71726063eec2f922b2",
19
+ "better-dsh/src/remote/nonce-framing.ts": "2f2f80d6aa9934f96ba171d575dbe4cddbe2e127da3b0eec3032c5ea522cadc3",
19
20
  "better-dsh/src/remote/oneshot.ts": "99f2c809ba1159c7865ec3cff2e6d7b1f165a7f03185a6fc9505c1a534ba57bc",
20
21
  "better-dsh/src/remote/output-ring.ts": "a79104bd3dcddab06a3fe7e52d239a70becd4f2dcddd0c9b6513f5fe3c4ed0bc",
21
- "better-dsh/src/remote/plugin.ts": "c869e25bedccfa9e018e90e92110a0aaacc847c853a8db0519a6e171d1dd670c",
22
+ "better-dsh/src/remote/plugin.ts": "da6b985cd013ffc253ee8cf5605dc2d729e26534139dd28269db5d246511fac9",
22
23
  "better-dsh/src/remote/process.ts": "f4491c76457b662703e0b1e4a876299b9c8b7b3da5b304c6baee5576017de448",
23
- "better-dsh/src/remote/pty-session.ts": "ee11d10baf52c6326f9cf0f5aa434b6330bb90562ee8871eac0b5a663ce1182a",
24
+ "better-dsh/src/remote/pty-session.ts": "3cb07345b90f0e54cbcea00204ae0b5346d9bb6edf3a784f009fc1b07357f799",
24
25
  "better-dsh/src/remote/roster.ts": "b84f75edc87550bb96803042487cfd7c39135b1710ff9715dd5ca402df0aa378",
25
26
  "better-dsh/src/remote/rsync.ts": "f9c15279fdfa67b3265eeda216bcd0c46d8b4f05c6026cf2750a1c818ff333ba",
26
27
  "better-dsh/src/remote/ssh-argv.ts": "e7f46ef3e53eaba6cfe92033afad046435db18b0b2f4b86832353d609c535662",
@@ -29,9 +30,10 @@
29
30
  "better-dsh/src/remote/ssh-rsync-native-relay.ts": "5d9585ab30a11aeeba19ce7b43aa31f47e2ee0bde2e7c006ac3e811cf7940bdf",
30
31
  "better-dsh/src/remote/ssh-targets.ts": "2a7e754fc26cf34e05ffb5d7f7406f35c09fd0770684db904dbf4c56d2c00beb",
31
32
  "better-dsh/src/remote/status.ts": "e144c1cf98388dbaca8dab5622e6e37ceac85d898119e37d07f88e129113a82e",
32
- "better-dsh/src/remote/target.ts": "d72346382c0b49659a29807141af19e65d17478a7bf7b1ec4032148ba66c4aa3",
33
- "better-dsh/src/remote/tool.ts": "a73b57009a4fcda8c58d57181f5f18bf88c8d6980316714a8c4c8b8835cb1466",
34
- "better-dsh/src/remote/transports.ts": "55dbdef8f2522b262b2df8879884499cdaa7e508fa42f70ae615b5064f43bcc9",
33
+ "better-dsh/src/remote/target.ts": "bce47c6c61975a52e5cb77a6b727f68cbe78ecdcb3866c237fb932444ecc954e",
34
+ "better-dsh/src/remote/tool.ts": "ecb431c4b1dfa4981063ea78b90288ac9ac12070c08a113b51602f18887f194b",
35
+ "better-dsh/src/remote/transfer-tool.ts": "d02b216a31314e98fdf6933230b57adce58b9c8aebe9798aae7be808105b41e2",
36
+ "better-dsh/src/remote/transports.ts": "8a7190d6013f12901d9007e6b30708645baa4aa0770fcb1087ae6fcde656a8d0",
35
37
  "better-dsh/third_party/schemastery/lib/index.cjs": "aca1c1a47c7b6034feee01501aefca3e6dd97a0bac4fe334fd30647ae5a9e480",
36
38
  "better-dsh/third_party/schemastery/lib/index.d.mts": "66847f605af58c42575333c825eefdec4ad38278ad6cb260e37aa0cc63803bbd",
37
39
  "better-dsh/third_party/schemastery/lib/index.mjs": "cfcbe46c7416bdb35ce891c252152bbf519e08199bede5930dd7cb880c65f1ef",
@@ -68,7 +70,8 @@
68
70
  "helper-bin/x86_64-pc-windows-gnu/superd-run.exe": "1372450238c5bda702b4529f4148aa79e80a255c3c125d1a6229afd4693e9d90",
69
71
  "helper-bin/x86_64-unknown-linux-musl/BUILT-VER": "8601d3c2ff79795c9657b02080afffe91c42775534dd6687c6dbdc0f4924db70",
70
72
  "helper-bin/x86_64-unknown-linux-musl/superd-run": "05b428657942abf22a0ad609433d5cab6b6182fe389851345459a0274b5a6533",
71
- "plugin.d.ts": "6ec2c6b6a09425c1f2c30d975cdf86c77f0cb1d23590ef67d87dc78515a915c8",
72
- "plugin.js": "605386387879837631730a74f461c69568126f829c7e9efe470003c24d5367c1"
73
+ "hub-pty-pump.mjs": "dfe0cb331cc585eb4d858fbb6d8f3ff22414f3ee55109c933df0fc14502aee5c",
74
+ "plugin.d.ts": "da9bfcbb1a0f37bd2994452ac4cfed284615e5cea45c05ff97569474a442e91f",
75
+ "plugin.js": "8dd1843f2c57097a1bbcfeddaf50193985bf25b942cc784ca501311d16aae7f3"
73
76
  }
74
77
  }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * agent-model-face.ts — the 0.2.2+ `Agent.model` face for foreign-runtime
3
+ * bridges.
4
+ *
5
+ * dsh 0.2.2 made `model` a required `Agent` member: the Web UI's model switcher
6
+ * reaches every agent — native and foreign alike — through
7
+ * `session/selectModel` → `agent.model.select()` (session-controller
8
+ * commands.ts). The native `AgentModel` (dsh-agent model-selection.ts) records
9
+ * a `model/selection` session event durably and resolves `current()` as
10
+ * pending ?? lastUsed ?? live options.
11
+ *
12
+ * This module hands the foreign bridges (claude/codex/hermes/pi/agy/omp) the
13
+ * same semantics without mounting the projection registry:
14
+ *
15
+ * - {@link recordedModelSelection} — the native `modelSelection` projection
16
+ * fold, replayed as one pass over the session's events. A recorded
17
+ * `model/selection` is the pending choice and survives non-matching
18
+ * `request/header`s; a matching header retires it; `lastUsed` is the last
19
+ * header's config. (The adapters that DO mount `sessionProjections` —
20
+ * claude, codex — may keep reading the projection directly; both produce
21
+ * the same answer.)
22
+ * - {@link createAgentModelFace} — the face itself: `current()` resolves
23
+ * pending → logged header → deployment default → agent options (each level
24
+ * must name both halves), `select()` appends the same durable
25
+ * `model/selection` event as the native face, deduped against the recorded
26
+ * choice.
27
+ *
28
+ * Structural session face on the system-head pattern: the real (generic-typed)
29
+ * `Session` satisfies it without this package taking a dsh-agent dependency.
30
+ *
31
+ * @module superd-hub/agent-model-face
32
+ */
33
+ /** A model selection: both route halves, with an optional explicit effort. */
34
+ export interface ModelSelectionLike {
35
+ provider: string;
36
+ model: string;
37
+ reasoningEffort?: unknown;
38
+ }
39
+ /** The `Agent.model` shape (dsh-agent runtime-types `Model`), over the bridge's own selection type. */
40
+ export interface BridgeModelFace<Sel extends ModelSelectionLike> {
41
+ current(): Readonly<Sel> | undefined;
42
+ select(selection: Readonly<Sel>): void;
43
+ }
44
+ /**
45
+ * The session slice the fold reads. Deliberately loose so the real
46
+ * (generic-typed) `Session` satisfies it; the events the fold inspects arrive
47
+ * as `unknown` data and are narrowed inside this module only.
48
+ */
49
+ export interface ModelSessionFace {
50
+ snapshotEvents(): ReadonlyArray<{
51
+ type: string;
52
+ data: unknown;
53
+ }>;
54
+ requestHeader(): {
55
+ readonly config: {
56
+ readonly provider: string;
57
+ readonly model: string;
58
+ };
59
+ } | undefined;
60
+ }
61
+ /**
62
+ * The native `modelSelection` projection fold over one session's events.
63
+ * @param events - the session's full event snapshot (order is the log order).
64
+ * @returns the recorded choice — a pending selection, else the last request
65
+ * header's config — or `null` when the log names neither.
66
+ */
67
+ export declare function recordedModelSelection(events: ReadonlyArray<{
68
+ type: string;
69
+ data: unknown;
70
+ }>): ModelSelectionLike | null;
71
+ /** Readers the face resolves through; each adapter wires its own sources. */
72
+ export interface AgentModelFaceParts<Sel extends ModelSelectionLike> {
73
+ /** The session whose events and request header back the fold. */
74
+ session: ModelSessionFace;
75
+ /** The agent's live route options (the options fallback names both halves or neither). */
76
+ options: {
77
+ readonly provider?: string;
78
+ readonly model?: string;
79
+ };
80
+ /** Appends the durable `model/selection` event (the adapter's own typed append). */
81
+ appendSelection: (selection: Sel) => void;
82
+ /** Reads the deployment default (`ctx.agentDefaultModel`); absent when the service is not mounted. */
83
+ readDefault?: () => {
84
+ provider: string;
85
+ model: string;
86
+ } | undefined;
87
+ }
88
+ /**
89
+ * Build the `Agent.model` face for one foreign-runtime agent.
90
+ * @param parts - the session, options, typed append, and optional default reader.
91
+ * @returns the face; assign it to the agent's `model` member.
92
+ */
93
+ export declare function createAgentModelFace<Sel extends ModelSelectionLike>(parts: AgentModelFaceParts<Sel>): BridgeModelFace<Sel>;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * agent-model-face.ts — the 0.2.2+ `Agent.model` face for foreign-runtime
3
+ * bridges.
4
+ *
5
+ * dsh 0.2.2 made `model` a required `Agent` member: the Web UI's model switcher
6
+ * reaches every agent — native and foreign alike — through
7
+ * `session/selectModel` → `agent.model.select()` (session-controller
8
+ * commands.ts). The native `AgentModel` (dsh-agent model-selection.ts) records
9
+ * a `model/selection` session event durably and resolves `current()` as
10
+ * pending ?? lastUsed ?? live options.
11
+ *
12
+ * This module hands the foreign bridges (claude/codex/hermes/pi/agy/omp) the
13
+ * same semantics without mounting the projection registry:
14
+ *
15
+ * - {@link recordedModelSelection} — the native `modelSelection` projection
16
+ * fold, replayed as one pass over the session's events. A recorded
17
+ * `model/selection` is the pending choice and survives non-matching
18
+ * `request/header`s; a matching header retires it; `lastUsed` is the last
19
+ * header's config. (The adapters that DO mount `sessionProjections` —
20
+ * claude, codex — may keep reading the projection directly; both produce
21
+ * the same answer.)
22
+ * - {@link createAgentModelFace} — the face itself: `current()` resolves
23
+ * pending → logged header → deployment default → agent options (each level
24
+ * must name both halves), `select()` appends the same durable
25
+ * `model/selection` event as the native face, deduped against the recorded
26
+ * choice.
27
+ *
28
+ * Structural session face on the system-head pattern: the real (generic-typed)
29
+ * `Session` satisfies it without this package taking a dsh-agent dependency.
30
+ *
31
+ * @module superd-hub/agent-model-face
32
+ */
33
+ /** One recorded selection read structurally; incomplete routes answer `null`. */
34
+ function selectionOf(value) {
35
+ if (typeof value !== 'object' || value === null)
36
+ return null;
37
+ const { provider, model } = value;
38
+ if (typeof provider !== 'string' || provider === '' || typeof model !== 'string' || model === '')
39
+ return null;
40
+ const reasoningEffort = value.reasoningEffort;
41
+ return {
42
+ provider,
43
+ model,
44
+ ...(typeof reasoningEffort === 'string' && reasoningEffort !== ''
45
+ ? { reasoningEffort }
46
+ : {}),
47
+ };
48
+ }
49
+ /** Route equality without the effort, and full equality with it (the fold's own rules). */
50
+ function sameRoute(left, right) {
51
+ return left.provider === right.provider && left.model === right.model;
52
+ }
53
+ function sameSelection(left, right) {
54
+ return sameRoute(left, right) && left.reasoningEffort === right.reasoningEffort;
55
+ }
56
+ /**
57
+ * The native `modelSelection` projection fold over one session's events.
58
+ * @param events - the session's full event snapshot (order is the log order).
59
+ * @returns the recorded choice — a pending selection, else the last request
60
+ * header's config — or `null` when the log names neither.
61
+ */
62
+ export function recordedModelSelection(events) {
63
+ let pending = null;
64
+ let lastUsed = null;
65
+ for (const event of events) {
66
+ if (event.type === 'model/selection') {
67
+ const selection = selectionOf(event.data);
68
+ if (selection !== null && !(pending !== null && sameSelection(pending, selection)))
69
+ pending = selection;
70
+ }
71
+ else if (event.type === 'request/header') {
72
+ const header = event.data;
73
+ const config = header === null || typeof header !== 'object' ? undefined : header.header?.config;
74
+ const selection = selectionOf(config);
75
+ if (selection !== null) {
76
+ lastUsed = selection;
77
+ if (pending !== null && sameRoute(pending, selection))
78
+ pending = null;
79
+ }
80
+ }
81
+ }
82
+ return pending ?? lastUsed;
83
+ }
84
+ /**
85
+ * Build the `Agent.model` face for one foreign-runtime agent.
86
+ * @param parts - the session, options, typed append, and optional default reader.
87
+ * @returns the face; assign it to the agent's `model` member.
88
+ */
89
+ export function createAgentModelFace(parts) {
90
+ const recorded = () => recordedModelSelection(parts.session.snapshotEvents());
91
+ const current = () => {
92
+ const fold = recorded();
93
+ if (fold !== null)
94
+ return { ...fold };
95
+ const preferred = parts.readDefault?.();
96
+ if (preferred !== undefined && preferred.provider !== '' && preferred.model !== '') {
97
+ return { provider: preferred.provider, model: preferred.model };
98
+ }
99
+ const { provider, model } = parts.options;
100
+ return provider !== undefined && provider !== '' && model !== undefined && model !== ''
101
+ ? { provider, model }
102
+ : undefined;
103
+ };
104
+ return {
105
+ current,
106
+ select(selection) {
107
+ const fold = recorded();
108
+ if (fold !== null && sameSelection(fold, selection))
109
+ return;
110
+ parts.appendSelection({ ...selection });
111
+ },
112
+ };
113
+ }
@@ -27,8 +27,9 @@ export interface OneShotExecResult {
27
27
  */
28
28
  outcome: 'exited' | 'signaled' | 'timeout' | 'spawn-error';
29
29
  }
30
- /** Per-stream returned tail: at most 30,000 bytes, beginning on a UTF8 boundary. */
31
- export declare const EXEC_TAIL_WINDOW = 30000;
30
+ /** 0.3.2-e: the 30K returned-tail window is REMOVED — output returns in full up to
31
+ * retention and model-context sizing is the native agent runtime's job (spill policy
32
+ * or its foreign equivalent). Retention below stays the memory-safety bound. */
32
33
  /** Hub oneshot per-stream raw-byte retention: 4MiB, distinct from native/PTy UTF16 caps. */
33
34
  export declare const EXEC_RING_HARD_CAP: number;
34
35
  export declare const EXEC_DEFAULT_TIMEOUT_SEC = 120;
@@ -5,7 +5,7 @@
5
5
  // tool); it never decides policy — auth is the shared /api fence's, and the
6
6
  // initiator-side per-machine knob lives on the caller's registry row.
7
7
  //
8
- // Output discipline (better-dash lesson, spec §8.1): return the LAST 30k of
8
+ // Output discipline (0.3.2-e): return output IN FULL up to the 4MiB retention ring;
9
9
  // each stream, retain at most 4M per stream while running (the tail of a
10
10
  // capped ring is still the tail of the stream), mark `truncated` whenever a
11
11
  // stream's total exceeded what is returned. Kill discipline: SIGTERM to the
@@ -15,8 +15,9 @@ import { spawn } from 'node:child_process';
15
15
  import { ByteTailRing } from '../../remote/plugin.js';
16
16
  /** Shared-channel Fetch route path this API owns. */
17
17
  export const EXEC_API_BASE = '/api/superd/exec';
18
- /** Per-stream returned tail: at most 30,000 bytes, beginning on a UTF8 boundary. */
19
- export const EXEC_TAIL_WINDOW = 30_000;
18
+ /** 0.3.2-e: the 30K returned-tail window is REMOVED — output returns in full up to
19
+ * retention and model-context sizing is the native agent runtime's job (spill policy
20
+ * or its foreign equivalent). Retention below stays the memory-safety bound. */
20
21
  /** Hub oneshot per-stream raw-byte retention: 4MiB, distinct from native/PTy UTF16 caps. */
21
22
  export const EXEC_RING_HARD_CAP = 4 * 1024 * 1024;
22
23
  export const EXEC_DEFAULT_TIMEOUT_SEC = 120;
@@ -109,7 +110,7 @@ export async function runOneShotExec(input, log) {
109
110
  clearTimeout(killTimer);
110
111
  if (graceTimer !== undefined)
111
112
  clearTimeout(graceTimer);
112
- const outTail = stdout.tail(EXEC_TAIL_WINDOW), errTail = stderr.tail(EXEC_TAIL_WINDOW);
113
+ const outTail = stdout.tail(), errTail = stderr.tail();
113
114
  const outText = outTail.toString('utf8');
114
115
  const errText = spawnError !== undefined
115
116
  ? `exec: spawn failed — ${spawnError}${input.cwd === undefined ? '' : ` (cwd: ${input.cwd})`}`
@@ -127,7 +128,7 @@ export async function runOneShotExec(input, log) {
127
128
  stdout: outText,
128
129
  stderr: errText,
129
130
  durationMs: Date.now() - started,
130
- truncated: stdout.total > outTail.length || stderr.total > errTail.length,
131
+ truncated: stdout.total > outTail.length || stderr.total > errTail.length, // 0.3.2-e: true 仅当 retention 驱逐(>4MiB)
131
132
  outputStats: { stdout: stdout.stats(outTail.length), stderr: stderr.stats(errTail.length) },
132
133
  timedOut,
133
134
  outcome,
@@ -19,7 +19,7 @@ export interface HubToolExecFace {
19
19
  pty(sessionKey: string, machine: string, cmd: string, opts?: {
20
20
  timeoutSec?: number;
21
21
  stdin?: string;
22
- }): Promise<import('./exec-pty-session.js').PtyDispatchResult>;
22
+ }): Promise<import('../../remote/plugin.js').PtyDispatchResult>;
23
23
  }
24
24
  import type { A2aAddress, A2aInitiator, A2aService } from './a2a-service.js';
25
25
  /** The service face the tool consumes (structural subset of T2/T3's A2aService). */
@@ -36,8 +36,8 @@
36
36
  * - `exec` is the P4 placeholder leg: `"not yet implemented: exec (P4)"`.
37
37
  */
38
38
  import { defineTool } from '@deepseek-ai/dsh-tools';
39
- import { parseOneShotExecBody, EXEC_TAIL_WINDOW } from './exec.js';
40
- import { tailWindow } from './exec-pty-framing.js';
39
+ import { parseOneShotExecBody } from './exec.js';
40
+ import { disclose } from '../../remote/plugin.js';
41
41
  // F2 (0.1.7-n, PC 0.1.7-m first-person): the wire/registry sentinel stays
42
42
  // '[undefined]', but the DISPLAY surface must not show it to a human — D17
43
43
  // already sanctions display-layer beautification.
@@ -190,7 +190,7 @@ const DESCRIPTION = 'SuperD Hub — agent-to-agent messaging across the Super D
190
190
  ' {action:"send", agent?, conversationId?, title?, message, delivery?} → open-or-resume: omit conversationId to OPEN (then agent = the recipient, REQUIRED; title optional; message is the first turn); pass conversationId to RESUME (agent optional — the hub derives the route; a wrong agent is refused). Receipt {outcome: delivered|accepted|failed, messageId, conversationId, sessionId, created, error?, note?}: `delivered` = end-to-end complete; `accepted` = the hub took it and the SAME in-flight request keeps running (its `delivered`/`undelivered` outcome arrives as a notice — do not resend); `failed` = immediate rejection.\n' +
191
191
  ' {action:"read"} → sweep messages buffered for this session while it was not live.\n' +
192
192
  ' {action:"stop", conversationId, agent?, scope?} → cancel the conversation\'s ACTIVE turn (the session owner is derived from the conversation id; agent is optional). `stopped:true` means this call cancelled a live turn; a turn that finished on its own is IDLE, never `stopped` — so stop answers stopped:false with a note when nothing was running.\n' +
193
- ' {action:"exec", cmd, machine?, cwd?, stdin?, timeoutSec?, mode?} → run one shell command on a remote machine (machine required; self/local execution disabled unless localExec is explicitly enabled). mode "oneshot" (default): {exit, stdout (last 30k), stderr, durationMs, truncated, timedOut, outcome}; cwd sets its working directory. String stdin is piped and closed in oneshot; in PTY it is written immediately after cmd (REPL input, not script mode). Hub oneshot hard retention is 4MiB bytes per stream, PTY is 4,000,000 UTF-16 code units. outputStats reports real total/retained/dropped/returned and units; discarded head cannot be recovered by restarting. mode "pty": a PERSISTENT shell per (your session, machine) — the shell OWNS its cwd and env and they SURVIVE across dispatches (cd once inside the command, then keep working); {output (tail 30k), exit, cwd, timedOut, reconnected, durationMs}. cwd is oneshot-only: passing it with mode "pty" is refused (use `cd <dir> && …` inside the command). Prefer oneshot for independent commands; pty when shell state matters.\n' +
193
+ ' {action:"exec", cmd, machine?, cwd?, stdin?, timeoutSec?, mode?} → run one shell command on a remote machine (machine required; self/local execution disabled unless localExec is explicitly enabled). mode "oneshot" (default): {exit, stdout (full up to 4MiB retention; beyond it the retained tail plus outputStats), stderr, durationMs, truncated (retention eviction only), timedOut, outcome}; cwd sets its working directory. String stdin is piped and closed in oneshot; in PTY it is written immediately after cmd (REPL input, not script mode). Hub oneshot hard retention is 4MiB bytes per stream, PTY is 4,000,000 UTF-16 code units. outputStats reports real total/retained/dropped/returned and units; discarded head cannot be recovered by restarting. mode "pty": a PERSISTENT shell per (your session, machine) — the shell OWNS its cwd and env and they SURVIVE across dispatches (cd once inside the command, then keep working); {output (full up to 4M UTF-16 units; beyond it the retained tail plus outputStats), exit, cwd, timedOut, reconnected, durationMs}. cwd is oneshot-only: passing it with mode "pty" is refused (use `cd <dir> && …` inside the command). Prefer oneshot for independent commands; pty when shell state matters.\n' +
194
194
  'Addresses are "<m4>-<a6>" (machine-agent); call agents first to list valid ids. delivery: "queue" (default) is injected into the peer\'s session as a waking turn — it never interrupts an active turn, it queues behind it; "steer" submits steering for the peer\'s nearest step boundary — use it only when urgency demands. NEVER poll for a reply: the peer\'s reply arrives in this conversation as a new message WHEN THE PEER SENDS one (peers that never use the channel get their last output at turn end delivered as their reply instead, form=transcript); a turn that ends with no channel artifact yields one notice whose reason= distinguishes peer-silent (ended, said nothing) / peer-error (stream died or turn aborted) / stopped-by-initiator (you stopped it). read only sweeps what was buffered while this session was down. stop is a hard stop of the peer\'s active turn (the UI stop button); scope "session" degrades to turn-only — no session-close exists upstream — and the conversation stays resumable: send again to resume it. agent_status reads a cache refreshed by the 5-minute sweep (collectedAt says when); the window is alive-first, ≤5 rows, newest first, and not being in it implies nothing; a `note` says why when the window has no collection behind it (the last collection failed, or none has happened yet). MESSAGING HAS NO STATUS QUERY: to check whether a peer is idle or running, call agent_status (a 5-minute cache — never a delivery receipt); if that is not enough and you cannot wait, send another message.';
195
195
  /**
196
196
  * Build the one SuperD Hub tool.
@@ -284,8 +284,8 @@ export function createSuperdHubTool(deps) {
284
284
  count: { type: 'integer' },
285
285
  messages: { type: 'json', description: 'read sweep: buffered messages with from as "<m4>-<a6>"' },
286
286
  exit: { type: 'json', description: 'exec: the command exit code (null when killed by timeout/signal)' },
287
- stdout: { type: 'json', description: 'exec: the last 30k of stdout' },
288
- stderr: { type: 'json', description: 'exec: the last 30k of stderr' },
287
+ stdout: { type: 'json', description: 'exec: full stdout up to retention (4MiB bytes; PTY 4,000,000 UTF-16 units)' },
288
+ stderr: { type: 'json', description: 'exec: full stderr up to retention (4MiB bytes)' },
289
289
  durationMs: { type: 'integer', description: 'exec: wall-clock duration' },
290
290
  outputStats: { type: 'json', description: 'exec per-stream counters with explicit bytes/utf16 units; total, retained, dropped head, returned payload (excludes banners), retentionLimit. Absent on older peers: exact metrics unavailable.' },
291
291
  truncated: { type: 'boolean', description: 'exec: a stream\'s total output exceeded the returned tail window' },
@@ -422,7 +422,7 @@ export function createSuperdHubTool(deps) {
422
422
  const machine = args.machine ?? deps.exec.selfM4();
423
423
  try {
424
424
  const r = await deps.exec.pty(sessionKey, machine, args.cmd, { ...(args.timeoutSec === undefined ? {} : { timeoutSec: args.timeoutSec }), ...(args.stdin === undefined ? {} : { stdin: args.stdin }) });
425
- const out = tailWindow(r.output, EXEC_TAIL_WINDOW, r.outputStats?.stdout);
425
+ const out = disclose(r.output, r.outputStats?.stdout); // 0.3.2-e: 全量返回,仅 retention 驱逐打 banner
426
426
  return {
427
427
  action,
428
428
  mode: 'pty',
@@ -36,6 +36,7 @@ export { createOwnershipIndex, type OwnershipIndex, type OwnershipRow } from './
36
36
  export { registerHostMount, takeHostMount, listHostMounts, noteWorldServer, worldServerOf, type HostMount } from './world-host.js';
37
37
  export { worldMountPatches, superdTrustedHosts, WORLD_WAVE1_DISABLED_ROWS, WORLD_WAVE2_DISABLED_ROWS, WORLD_MOUNT_PLUGIN, WORLD_MOUNT_ROW_ID, WORLD_HUB_PLUGIN, WORLD_HUB_ROW_ID, type WorldPatchLayer, } from './world-mount.js';
38
38
  export { withSystemHead, type HeadAwareSessionFace, type HeadPlaceholder, type WithSystemHead } from './system-head.js';
39
+ export { createAgentModelFace, recordedModelSelection, type AgentModelFaceParts, type BridgeModelFace, type ModelSelectionLike, type ModelSessionFace, } from './agent-model-face.js';
39
40
  export { default as worldApply, name as worldPluginName, type Config as WorldEntryConfig } from './world-entry.js';
40
41
  export { default as joinApply, name as worldJoinPluginName, type Config as WorldJoinConfig } from './world-join.js';
41
42
  export { createA2aService, A2A_MESSAGE_TEMPLATE, A2A_UNTITLED, A2A_BUFFER_MAX, A2A_NO_SESSION_CLOSE_REASON, A2A_NO_TURN_ACTIVE_NOTE, renderA2aEnvelope, loadConversations, saveConversations, shortIdFor, isDegenerateShortId, migrateConversations, type A2aService, subscribeSessionCompletion, sessionRpc, turnLivenessOf, addressErrorBody, type A2aSessionSource, type A2aGateway, type A2aAddress, type A2aInitiator, type A2aDelivery, type A2aSendRequest, type A2aReceipt, type A2aReceiptOutcome, type A2aReceiptError, type A2aConversationRef, type A2aCompletion, type A2aCompletionCb, type A2aErrorCb, type A2aSubscribeOptions, type A2aStopScope, type A2aStopResult, type A2aBufferedMessage, type A2aBufferInput, type ConversationRow, type ConversationsFile, } from './a2a-service.js';
@@ -36,6 +36,7 @@ export { createOwnershipIndex } from './ownership.js';
36
36
  export { registerHostMount, takeHostMount, listHostMounts, noteWorldServer, worldServerOf } from './world-host.js';
37
37
  export { worldMountPatches, superdTrustedHosts, WORLD_WAVE1_DISABLED_ROWS, WORLD_WAVE2_DISABLED_ROWS, WORLD_MOUNT_PLUGIN, WORLD_MOUNT_ROW_ID, WORLD_HUB_PLUGIN, WORLD_HUB_ROW_ID, } from './world-mount.js';
38
38
  export { withSystemHead } from './system-head.js';
39
+ export { createAgentModelFace, recordedModelSelection, } from './agent-model-face.js';
39
40
  export { default as worldApply, name as worldPluginName } from './world-entry.js';
40
41
  export { default as joinApply, name as worldJoinPluginName } from './world-join.js';
41
42
  export { createA2aService, A2A_MESSAGE_TEMPLATE, A2A_UNTITLED, A2A_BUFFER_MAX, A2A_NO_SESSION_CLOSE_REASON, A2A_NO_TURN_ACTIVE_NOTE, renderA2aEnvelope, loadConversations, saveConversations, shortIdFor, isDegenerateShortId, migrateConversations,
@@ -1,13 +1,28 @@
1
1
  import { createRemoteFileEndpoint } from './remote-file-rpc.js';
2
2
  import { startRemoteProcess } from './remote-job-client.js';
3
+ /** Hub transport implementation. HTTP/WS/pool mechanics stay in the existing exec modules. */
4
+ import { PtySession, buildHubPtyArgv, hubPtyAssets } from '../../remote/plugin.js';
5
+ // ws 模块绝对路径:在 superd-hub 上下文解析(分叉的 proven 链——闭包/泵子树解析不到,
6
+ // 0.3.2-c 定稿:按名解析只在 transport 侧做一次,经 ptyEndpoint/argv 下传)。
7
+ const wsModulePath = () => {
8
+ try {
9
+ return createRequire(import.meta.url).resolve('ws');
10
+ }
11
+ catch (error) {
12
+ throw new Error(`[E_HUB_PTY_SETUP] superd-hub: the ws module could not be resolved from the hub tree — hub PTY spawns a local pump child that opens the peer WebSocket with ws; ${error instanceof Error ? error.message : String(error)}`);
13
+ }
14
+ };
15
+ import { randomUUID } from 'node:crypto';
16
+ import { createRequire } from 'node:module';
3
17
  import { ensureSelf, listRemotes } from './machines.js';
4
18
  import { cookieOf } from './mesh-link.js';
5
19
  import { runOneShotExec, postOneShotExec, parseOneShotExecBody } from './exec.js';
6
- import { PtyPool } from './exec-pty-session.js';
7
- import { LocalScriptChannel, WsPtyChannel } from './exec-pty-channel.js';
8
20
  import { PTY_EXEC_PATH } from './exec-pty-rpc.js';
9
21
  /** One shared initiator implementation for legacy tool and the optional remote service. */
10
22
  export function createHubExec(home, log) {
23
+ // 0.3.2-c(Q2):分叉 exec-pty-session 已删——本 face 与 remote 工具共用 canonical
24
+ // PtySession(闭包导出)。per-owner 每机器一个持久 shell(A2A 语义保持:cwd/env 跨调用存活;
25
+ // dead → dispatch 内部重连 = 旧 markDead/重启;disposed → 下次调用重建)。
11
26
  const owners = new Map();
12
27
  let closed = false;
13
28
  const assertOpen = () => { if (closed)
@@ -21,6 +36,16 @@ export function createHubExec(home, log) {
21
36
  throw new Error(`exec is disabled for machine "${machine}" on this registry row (enabled/execEnabled=false)`);
22
37
  return row;
23
38
  };
39
+ const genCommandId = () => `remote-${randomUUID().replace(/-/g, '').slice(0, 16)}`;
40
+ const createPtySession = (machine, row, owner) => new PtySession({
41
+ key: `${owner}|hub-pty:${machine}`,
42
+ argv: row === undefined
43
+ ? ['script', '-qfec', 'bash', '/dev/null']
44
+ : buildHubPtyArgv({ url: `${row.origin.replace(/^http/, 'ws')}${PTY_EXEC_PATH}`, cookie: cookieOf(row.id, home) ?? '', wsPath: wsModulePath() }, hubPtyAssets()),
45
+ id: `p-${randomUUID().slice(0, 8)}`,
46
+ connection: row === undefined ? 'local' : `hub:${machine}`,
47
+ idleTtlSec: 900,
48
+ });
24
49
  const face = {
25
50
  selfM4: () => ensureSelf(home, 'local').id,
26
51
  run: input => { assertOpen(); return runOneShotExec(input, log); },
@@ -37,14 +62,34 @@ export function createHubExec(home, log) {
37
62
  const row = machine === face.selfM4() ? undefined : remote(machine);
38
63
  let pool = owners.get(owner);
39
64
  if (!pool)
40
- owners.set(owner, pool = new PtyPool({ idleTtlSec: 600 }));
41
- const key = `${machine}|${row?.origin ?? 'local'}`;
42
- const session = pool.getOrCreate(key, () => row
43
- ? new WsPtyChannel(`${row.origin.replace(/^http/, 'ws')}${PTY_EXEC_PATH}`, { cookie: cookieOf(row.id, home) })
44
- : new LocalScriptChannel());
45
- const result = await session.dispatch(cmd, opts);
46
- log?.('info', 'exec.pty.run', { machine, cmd: cmd.slice(0, 120), exit: result.exit ?? 'none', durationMs: result.durationMs });
47
- return result;
65
+ owners.set(owner, pool = new Map());
66
+ const attempt = async (session) => {
67
+ const r = await session.dispatch(cmd, { stdin: opts.stdin, timeoutSec: opts.timeoutSec, commandId: genCommandId() });
68
+ // P4 handoff(超时不杀)对 A2A 面保持旧语义:显式两段 ^C 后等帧结算——分叉自动中断的等价物。
69
+ if (r.running === true && r.job !== undefined) {
70
+ r.job.interrupt();
71
+ const settled = await r.job.done;
72
+ return { output: settled.output, outputStats: settled.outputStats, exit: settled.exit, cwd: settled.cwd, timedOut: true, reconnected: r.reconnected, durationMs: r.durationMs, commandId: r.commandId };
73
+ }
74
+ return r;
75
+ };
76
+ const settle = (result) => {
77
+ log?.('info', 'exec.pty.run', { machine, cmd: cmd.slice(0, 120), exit: result.exit ?? 'none', durationMs: result.durationMs });
78
+ return result;
79
+ };
80
+ const existing = pool.get(machine);
81
+ if (existing !== undefined && existing.sessionState !== 'disposed')
82
+ return settle(await attempt(existing));
83
+ const session = createPtySession(machine, row, owner);
84
+ pool.set(machine, session);
85
+ try {
86
+ return settle(await attempt(session));
87
+ }
88
+ catch (err) {
89
+ if (session.sessionState === 'disposed' && pool.get(machine) === session)
90
+ pool.delete(machine); // init 失败/auto-dispose 不留死项
91
+ throw err;
92
+ }
48
93
  },
49
94
  };
50
95
  return {
@@ -52,12 +97,11 @@ export function createHubExec(home, log) {
52
97
  async releaseOwner(owner) {
53
98
  const pool = owners.get(owner);
54
99
  if (pool) {
55
- await pool.disposeAll();
56
- if (owners.get(owner) === pool)
57
- owners.delete(owner);
100
+ await Promise.all([...pool.values()].map(s => s.dispose()));
101
+ owners.delete(owner);
58
102
  }
59
103
  },
60
- async dispose() { closed = true; await Promise.all([...owners.values()].map(p => p.disposeAll())); owners.clear(); },
104
+ async dispose() { closed = true; await Promise.all([...owners.values()].flatMap(p => [...p.values()].map(s => s.dispose()))); owners.clear(); },
61
105
  };
62
106
  }
63
107
  export function createHubRemoteTransport(home, log) {
@@ -67,25 +111,10 @@ export function createHubRemoteTransport(home, log) {
67
111
  const fileAbort = new AbortController();
68
112
  const files = new Set();
69
113
  const processes = new Map();
70
- const pending = new Map();
71
114
  const requireRemote = (machine) => {
72
115
  if (!machine || machine === exec.face.selfM4())
73
116
  throw new Error('Select a remote machine from the Hub roster; local execution is not available through remote');
74
117
  };
75
- const track = async (owner, work) => {
76
- let set = pending.get(owner);
77
- if (!set)
78
- pending.set(owner, set = new Set());
79
- set.add(work);
80
- try {
81
- return await work;
82
- }
83
- finally {
84
- set.delete(work);
85
- if (!set.size)
86
- pending.delete(owner);
87
- }
88
- };
89
118
  return {
90
119
  protocol: 1,
91
120
  listTargets: () => listRemotes(home).filter(r => r.id !== exec.face.selfM4()).map(r => ({ id: r.id, label: r.name, execEnabled: r.enabled && r.execEnabled !== false })),
@@ -110,12 +139,17 @@ export function createHubRemoteTransport(home, log) {
110
139
  processes.delete(owner); });
111
140
  return process;
112
141
  },
113
- async pty(machine, cmd, owner, options) {
142
+ ptyEndpoint(machine) {
114
143
  requireRemote(machine);
115
- if (closed || released.has(owner))
116
- throw new Error('Hub remote owner disposed');
117
- const { output, ...result } = await track(owner, exec.face.pty(owner, machine, cmd, { timeoutSec: options.timeout, stdin: options.stdin }));
118
- return { mode: 'pty', target: `hub:${machine}`, ...result, stdout: output };
144
+ if (closed)
145
+ throw new Error('Hub remote provider disposed');
146
+ const row = listRemotes(home).find(r => r.id === machine);
147
+ if (!row)
148
+ throw new Error(`unknown machine "${machine}"`);
149
+ if (!row.enabled || row.execEnabled === false)
150
+ throw new Error(`exec is disabled for machine "${machine}"`);
151
+ // P10:创建与每次 dispatch 都重取本端点(driver 侧调用)——pairing 复用不豁免。
152
+ return { url: `${row.origin.replace(/^http/, 'ws')}${PTY_EXEC_PATH}`, cookie: cookieOf(row.id, home) ?? '', wsPath: wsModulePath() };
119
153
  },
120
154
  fileEndpoint(machine) {
121
155
  requireRemote(machine);
@@ -135,7 +169,7 @@ export function createHubRemoteTransport(home, log) {
135
169
  const owned = [...processes.get(owner) ?? []];
136
170
  for (const process of owned)
137
171
  process.cancel('owner released');
138
- await Promise.all([exec.releaseOwner(owner), ...owned.map(p => p.done), ...pending.get(owner) ?? []]);
172
+ await Promise.all([exec.releaseOwner(owner), ...owned.map(p => p.done)]);
139
173
  },
140
174
  async dispose() {
141
175
  closed = true;
@@ -145,7 +179,7 @@ export function createHubRemoteTransport(home, log) {
145
179
  for (const process of owned)
146
180
  process.cancel('provider disposed');
147
181
  await exec.dispose();
148
- await Promise.allSettled([...owned.map(p => p.done), ...[...pending.values()].flatMap(s => [...s])]);
182
+ await Promise.allSettled([...owned.map(p => p.done)]);
149
183
  },
150
184
  };
151
185
  }
@@ -1,3 +1,4 @@
1
+ import { modelSelectionProjectionDefinition } from '@deepseek-ai/dsh-agent';
1
2
  import { noteWorldServer, publishWorldFetch, takeHostMount } from './world-host.js';
2
3
  import { WorldWebServer } from './world-web-server.js';
3
4
  import { superdTrustedHosts } from './world-mount.js';
@@ -43,6 +44,35 @@ export function apply(ctx, config) {
43
44
  dropWorldFetch = publishWorldFetch(key, connection.createSharedFetchHandler('/api'), mount);
44
45
  }
45
46
  });
47
+ // 0.2.2: register the `modelSelection` projection in THIS world's registry.
48
+ // Upstream's only registrant is the agent-loop row (agent-loop/src:334),
49
+ // which wave-1 minimization keeps out of world trees — without it the
50
+ // native model seat's projection frame never resolves and the trigger
51
+ // label stays "loading models…" forever (the catalog and selectModel RPC
52
+ // take different paths, which is why they keep working). The definition is
53
+ // a pure event fold (`model/selection` + `request/header`), no loop
54
+ // machinery; register() is ref-counted and duplicate-tolerant, so a future
55
+ // tree that also mounts agent-loop shares the registration cleanly.
56
+ ctx.inject(['sessionProjections'], (projCtx) => {
57
+ const projections = projCtx.get('sessionProjections');
58
+ if (projections === undefined)
59
+ return;
60
+ let disposed = false;
61
+ let handle;
62
+ void projections.register(modelSelectionProjectionDefinition).then((registered) => {
63
+ if (disposed) {
64
+ void registered.dispose();
65
+ return;
66
+ }
67
+ handle = registered;
68
+ }, (error) => {
69
+ ctx.logger?.warn?.(`superd-world-mount: modelSelection projection registration failed: ${String(error)}`);
70
+ });
71
+ return () => {
72
+ disposed = true;
73
+ void handle?.dispose();
74
+ };
75
+ });
46
76
  ctx.effect(() => () => {
47
77
  dropWorldFetch?.();
48
78
  }, 'superd-world-mount: virtual webServer');