super-dsh 0.1.6 → 0.1.9

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 (87) hide show
  1. package/client/index.js +741 -256
  2. package/cordis.patch.yml +16 -0
  3. package/package.json +7 -2
  4. package/superd-hub/dist/a2a-delivery.d.ts +189 -0
  5. package/superd-hub/dist/a2a-delivery.js +240 -0
  6. package/superd-hub/dist/a2a-host.d.ts +76 -0
  7. package/superd-hub/dist/a2a-host.js +145 -0
  8. package/superd-hub/dist/a2a-ingest.d.ts +31 -0
  9. package/superd-hub/dist/a2a-ingest.js +636 -0
  10. package/superd-hub/dist/a2a-log.d.ts +17 -0
  11. package/superd-hub/dist/a2a-log.js +42 -0
  12. package/superd-hub/dist/a2a-row.d.ts +65 -0
  13. package/superd-hub/dist/a2a-row.js +162 -0
  14. package/superd-hub/dist/a2a-service.d.ts +357 -0
  15. package/superd-hub/dist/a2a-service.js +980 -0
  16. package/superd-hub/dist/a2a-wire.d.ts +220 -0
  17. package/superd-hub/dist/a2a-wire.js +305 -0
  18. package/superd-hub/dist/agent-ids.d.ts +10 -0
  19. package/superd-hub/dist/agent-ids.js +68 -0
  20. package/superd-hub/dist/agent-roster.d.ts +29 -18
  21. package/superd-hub/dist/agent-roster.js +54 -15
  22. package/superd-hub/dist/agent-status.d.ts +119 -0
  23. package/superd-hub/dist/agent-status.js +386 -0
  24. package/superd-hub/dist/client/mesh-api.d.ts +134 -0
  25. package/superd-hub/dist/client/mesh-api.js +219 -0
  26. package/superd-hub/dist/client/mesh-groups.d.ts +63 -0
  27. package/superd-hub/dist/client/mesh-groups.js +66 -0
  28. package/superd-hub/dist/client/roster.d.ts +48 -0
  29. package/superd-hub/dist/client/roster.js +96 -0
  30. package/superd-hub/dist/exec-pty-channel.d.ts +58 -0
  31. package/superd-hub/dist/exec-pty-channel.js +162 -0
  32. package/superd-hub/dist/exec-pty-framing.d.ts +32 -0
  33. package/superd-hub/dist/exec-pty-framing.js +81 -0
  34. package/superd-hub/dist/exec-pty-rpc.d.ts +21 -0
  35. package/superd-hub/dist/exec-pty-rpc.js +128 -0
  36. package/superd-hub/dist/exec-pty-session.d.ts +76 -0
  37. package/superd-hub/dist/exec-pty-session.js +234 -0
  38. package/superd-hub/dist/exec.d.ts +56 -0
  39. package/superd-hub/dist/exec.js +225 -0
  40. package/superd-hub/dist/gateway.d.ts +39 -2
  41. package/superd-hub/dist/gateway.js +62 -3
  42. package/superd-hub/dist/hub-tool.d.ts +174 -0
  43. package/superd-hub/dist/hub-tool.js +464 -0
  44. package/superd-hub/dist/index.d.ts +15 -1
  45. package/superd-hub/dist/index.js +16 -0
  46. package/superd-hub/dist/machines.d.ts +45 -0
  47. package/superd-hub/dist/machines.js +125 -0
  48. package/superd-hub/dist/mesh-federate.d.ts +138 -0
  49. package/superd-hub/dist/mesh-federate.js +223 -0
  50. package/superd-hub/dist/mesh-gateway.d.ts +67 -0
  51. package/superd-hub/dist/mesh-gateway.js +408 -0
  52. package/superd-hub/dist/mesh-link.d.ts +274 -0
  53. package/superd-hub/dist/mesh-link.js +441 -0
  54. package/superd-hub/dist/mesh-rpc.d.ts +123 -0
  55. package/superd-hub/dist/mesh-rpc.js +250 -0
  56. package/superd-hub/dist/mux-client.d.ts +71 -0
  57. package/superd-hub/dist/mux-client.js +281 -0
  58. package/superd-hub/dist/pack-version.d.ts +2 -0
  59. package/superd-hub/dist/pack-version.js +30 -0
  60. package/superd-hub/dist/roster.d.ts +6 -0
  61. package/superd-hub/dist/rpc.d.ts +18 -2
  62. package/superd-hub/dist/rpc.js +34 -4
  63. package/superd-hub/dist/spawn-world.js +3 -1
  64. package/superd-hub/dist/switch-core.d.ts +62 -25
  65. package/superd-hub/dist/switch-core.js +53 -30
  66. package/superd-hub/dist/system-head.d.ts +22 -12
  67. package/superd-hub/dist/system-head.js +100 -84
  68. package/superd-hub/dist/wire-endpoints.d.ts +264 -0
  69. package/superd-hub/dist/wire-endpoints.js +180 -0
  70. package/superd-hub/lib/client/index.js +741 -256
  71. package/superd-hub/package.json +11 -2
  72. package/agent-agy/dist/agent-preset-projection.d.ts +0 -31
  73. package/agent-agy/dist/agent-preset-projection.js +0 -33
  74. package/agent-claude/dist/agent-preset-projection.js +0 -30
  75. package/agent-codex/dist/agent-preset-projection.js +0 -37
  76. package/agent-codex/dist/app-home.js +0 -65
  77. package/agent-codex/dist/spike.js +0 -46
  78. package/agent-hermes/dist/agent-preset-projection.js +0 -34
  79. package/agent-omp/dist/agent-preset-projection.js +0 -36
  80. package/agent-omp/dist/app-home.js +0 -86
  81. package/agent-pi/dist/agent-preset-projection.js +0 -29
  82. package/superd-hub/dist/client-shim.d.ts +0 -28
  83. package/superd-hub/dist/client-shim.js +0 -251
  84. package/superd-hub/dist/index-pass.d.ts +0 -22
  85. package/superd-hub/dist/index-pass.js +0 -47
  86. package/superd-hub/dist/labels.d.ts +0 -45
  87. package/superd-hub/dist/labels.js +0 -83
@@ -0,0 +1,67 @@
1
+ import type { MuxClientSocket } from './mux-client.js';
2
+ import type { GatewayFace } from './targets.js';
3
+ export type { MuxClientSocket } from './mux-client.js';
4
+ /**
5
+ * The default ws seam: connect one `ws` WebSocket to `url` with `headers`
6
+ * riding the upgrade request (the pairing Cookie), presented as T1's
7
+ * {@link MuxClientSocket}.
8
+ *
9
+ * The adapter is crash-proof by construction (fix round 1):
10
+ * - an `'error'` listener is ALWAYS registered: an unreachable peer
11
+ * (ECONNREFUSED) or a rejected upgrade (non-101, e.g. a dead cookie's
12
+ * HTTP 401) emits `'error'` with no listener of its own — an uncaught
13
+ * exception that kills the hub process. The listener is a no-op: ws
14
+ * guarantees `'close'` follows `'error'`, and the close path already
15
+ * classifies (in-flight streams fail `gateway/socket-closed`) and retires
16
+ * the connection for redial. WS-lane auth failure therefore classifies
17
+ * as a socket cut post-fix — acceptable: the next POST-lane call is the
18
+ * one that reports `gateway/auth-expired`.
19
+ * - `ws` THROWS on sends made while CONNECTING, and its post-close send
20
+ * bookkeeping can itself throw in teardown windows, so every send is
21
+ * guarded: pre-open sends queue (flushed in order on 'open'), post-close
22
+ * sends are swallowed — the stream is dead and the close path owns the
23
+ * teardown. WS-level pings stay at the WS layer: `on('message')` fires
24
+ * for text data frames only, so protocol-level ping/pong NEVER surfaces
25
+ * as text.
26
+ */
27
+ export declare function defaultWsFactory(url: string, headers: Record<string, string>): MuxClientSocket;
28
+ /** Options for {@link createRemoteGateway}. */
29
+ export interface RemoteGatewayOptions {
30
+ /** The peer hub origin (`http(s)://host[:port]`; the mux lane maps it to ws(s)). */
31
+ origin: string;
32
+ /** The remote agent's demux key (`?agent=` on both lanes; ignored by the native face). */
33
+ agentKey: string;
34
+ /** The machine's pairing cookie, read per call (the jar fronting `MachineRow.cookie`). */
35
+ cookie: () => string;
36
+ /** Fetch override (tests); defaults to `globalThis.fetch`. */
37
+ fetchImpl?: typeof fetch;
38
+ /** Socket factory override (tests); defaults to {@link defaultWsFactory}. */
39
+ wsFactory?: (url: string, headers: Record<string, string>) => MuxClientSocket;
40
+ /** Which peer surface to address (see the header's face table): 'foreign' =
41
+ * the `/hub` family with the `?agent=` demux (default); 'native' = ctx0's
42
+ * own `/api` surface, no demux key — how a browser ON the peer consumes its
43
+ * native DSH. */
44
+ face?: 'foreign' | 'native';
45
+ /** Idle ms with zero live streams before the ws closes (default 30 s; test hook for short real timers). */
46
+ idleCloseMs?: number;
47
+ }
48
+ /** A wire-form gateway: a {@link GatewayFace} plus the carrier call shape and a hard close. */
49
+ export interface RemoteGateway extends GatewayFace {
50
+ /**
51
+ * CarrierGatewayFace shape — the canonical order `worldMuxOf` calls with
52
+ * (`(endpoint, payload, uplink, peer, signal, control)`); the uplink is
53
+ * bridged into the wire upstream lane.
54
+ */
55
+ openWireStream(endpoint: string, payload: unknown, uplink: AsyncIterable<unknown> | Iterable<unknown>, peer: undefined, signal: AbortSignal, control: AbortController): AsyncIterable<unknown>;
56
+ /** GatewayFace shape — no uplink lane; the stream's uplink half-closes immediately. */
57
+ openWireStream(endpoint: string, payload: unknown, signal: AbortSignal): AsyncIterable<unknown>;
58
+ /** Hard-close the shared connection now (machine unpair); in-flight streams fail `gateway/disposed`. */
59
+ dispose(): void;
60
+ }
61
+ /**
62
+ * Build one remote agent's wire gateway toward a paired hub's `/hub` family.
63
+ * @param options - see {@link RemoteGatewayOptions}.
64
+ * @returns the {@link RemoteGateway} (a GatewayFace; register it with
65
+ * `registerForeignTarget` — callers cannot tell it from a local world gateway).
66
+ */
67
+ export declare function createRemoteGateway(options: RemoteGatewayOptions): RemoteGateway;
@@ -0,0 +1,408 @@
1
+ /**
2
+ * RemoteGatewayFace (machine-mesh P2 Task 2) — the wire form of a
3
+ * {@link GatewayFace}: one paired remote agent's dispatch/stream surface,
4
+ * reached over the PEER hub exactly as that hub would serve it locally —
5
+ * on ONE of two surfaces, selected by {@link RemoteGatewayOptions.face}:
6
+ *
7
+ * foreign (default) — the `/hub` family (hub-route is the server half):
8
+ * dispatchRpc POST <origin>/hub/api?agent=<agentKey>
9
+ * openWireStream WS <origin ws(s)>/hub/api/remote.mux?agent=<agentKey>
10
+ * native — ctx0's OWN envelope+mux surface, exactly how a browser ON the
11
+ * peer consumes its native DSH (NO demux key anywhere — the peer's
12
+ * native agent row federates as `key:'native'` with a minted a6, and the
13
+ * `/hub` family would 404 it as foreign-only):
14
+ * dispatchRpc POST <origin>/api/<endpoint> (endpoint in the PATH)
15
+ * openWireStream WS <origin ws(s)>/api/remote.mux
16
+ *
17
+ * The native unary URL mirrors upstream byte-exactly: the client builds
18
+ * `` `${channel}/${endpoint}`.slice(1) `` and the server decodes with
19
+ * `endpointFromPath` (a raw pathname slice, NO decodeURIComponent) — so the
20
+ * endpoint rides the path RAW, never percent-encoded (an encoded path would
21
+ * fail the server's ENDPOINT_SEGMENT_PATTERN; segments are already
22
+ * restricted to `[A-Za-z0-9_$.-]`, so nothing ever needs encoding). A bare
23
+ * `/api` carries no endpoint → 404.
24
+ *
25
+ * Everything else (envelope shape, cookie, Result verbatim, error taxonomy,
26
+ * connection lifecycle) is face-independent.
27
+ * Every call rides the machine's pairing cookie ({@link RemoteGatewayOptions.cookie},
28
+ * the per-machine jar fronting the persisted `MachineRow.cookie`).
29
+ *
30
+ * ## Mux connection lifecycle — ONE per gateway
31
+ * Lazy (dialed by the first stream), SHARED by every concurrent stream
32
+ * (per-call logical streams multiplex over it, T1 counter ids), IDLE-CLOSED
33
+ * {@link DEFAULT_IDLE_CLOSE_MS} after the last stream retires (the timer is
34
+ * `unref`d), and REDIALED transparently after any transport cut: on socket
35
+ * close the MuxClient fails every in-flight stream with
36
+ * `gateway/socket-closed`, the dead connection is dropped, and the next call
37
+ * dials fresh. The wire stream itself opens lazily on the returned iterable's
38
+ * first read — an iterable never iterated holds no connection and no stream.
39
+ *
40
+ * ## Heartbeat ruling (carried from the T1 review, controller ruling)
41
+ * The remote's heartbeat is WS-LEVEL ping/pong (WorldMuxServer pings its
42
+ * clients and terminates after two missed pongs); `ws` auto-answers those
43
+ * pings at the protocol layer and they NEVER surface as text (`on('message')`
44
+ * fires for data frames only — surfacing them would 1008-close the socket,
45
+ * since the server's parser has no ping case). Accordingly the MuxClient is
46
+ * configured with {@link HEARTBEAT_OFF_MS} (6 h), putting its JSON-text
47
+ * silence watchdog effectively OFF — an idle-but-alive connection is never
48
+ * torn down by the client — and liveness rides WS events alone: a dead peer
49
+ * surfaces as ws `close` → in-flight streams fail, the next call reconnects.
50
+ *
51
+ * ## Call shapes (the carrier lesson: canonical parameter order)
52
+ * `openWireStream` accepts BOTH faces with one runtime dispatch (an uplink
53
+ * iterable is recognizably not an AbortSignal):
54
+ * - GatewayFace: `(endpoint, payload, signal)` — the face `targets.ts`
55
+ * freezes. It carries NO uplink lane, so the stream's uplink is
56
+ * half-closed immediately (`end` right after `open`): a server handler
57
+ * reading uplink sees EOF instead of hanging.
58
+ * - CarrierGatewayFace: `(endpoint, payload, uplink, peer, signal, control)`
59
+ * — the exact order `worldMuxOf` calls with. The uplink is bridged into
60
+ * the wire upstream lane with the T1 channel idiom
61
+ * `for await (const v of uplink) await upstream.next(v); handle.end()`
62
+ * (read-one-send-one, zero buffering; a `done` read — dead stream — stops
63
+ * the pump quietly). `control` is accepted for shape
64
+ * parity only — cancellation rides the mux `cancel` frame, which is what
65
+ * actually stops the server-side stream.
66
+ *
67
+ * ## Error taxonomy (every throw is a plain `{code,message}` body — the
68
+ * brief-pinned shape; `wireErrorOf` normalizes it onto the wire downstream)
69
+ * - `gateway/unreachable` — network failure, or a non-200 answer that is
70
+ * not 401 (the status rides the message).
71
+ * - `gateway/auth-expired` — 401 anywhere: the MACHINE-LEVEL classification
72
+ * (the pairing cookie is dead) the roster downgrade consumes; never a
73
+ * per-request retry.
74
+ * - `gateway/bad-response` — 200 but the body is not a `server-response`
75
+ * envelope for the rpc we sent.
76
+ * - `gateway/aborted` — the caller's AbortSignal fired.
77
+ * - stream cuts — T1 `MuxClientError` codes pass through verbatim:
78
+ * `gateway/socket-closed` (peer cut / transport died),
79
+ * `gateway/disposed`, `gateway/protocol`; `gateway/heartbeat-silence` is
80
+ * dormant by the heartbeat ruling above. A REMOTE error frame keeps the
81
+ * remote's own code/message/details.
82
+ *
83
+ * ## Payload rule (T1 carry)
84
+ * `undefined` payloads are coerced to `null` on both lanes before the wire
85
+ * (T1's serializer drops the key for `undefined`, which the server's parser
86
+ * rejects); wire streamIds are T1's own stringified counters.
87
+ */
88
+ import { createRequire } from 'node:module';
89
+ import { MuxClient } from './mux-client.js';
90
+ /** Text-watchdog-off horizon (see the heartbeat ruling in the header). */
91
+ const HEARTBEAT_OFF_MS = 6 * 60 * 60 * 1000;
92
+ /** Idle window before the shared ws closes with zero live streams. */
93
+ const DEFAULT_IDLE_CLOSE_MS = 30_000;
94
+ /** Specifier widened on purpose: `ws` is a runtime peer, not a typed dep. */
95
+ const WS_MODULE = 'ws';
96
+ let wsConstructor;
97
+ /** Synchronously load the ws constructor once (createRequire — rpc.ts precedent). */
98
+ function loadWs() {
99
+ if (wsConstructor !== undefined)
100
+ return wsConstructor;
101
+ const require = createRequire(import.meta.url);
102
+ const mod = require(WS_MODULE);
103
+ const boxed = mod;
104
+ wsConstructor = (typeof mod === 'function' ? mod : boxed.default);
105
+ return wsConstructor;
106
+ }
107
+ /**
108
+ * The default ws seam: connect one `ws` WebSocket to `url` with `headers`
109
+ * riding the upgrade request (the pairing Cookie), presented as T1's
110
+ * {@link MuxClientSocket}.
111
+ *
112
+ * The adapter is crash-proof by construction (fix round 1):
113
+ * - an `'error'` listener is ALWAYS registered: an unreachable peer
114
+ * (ECONNREFUSED) or a rejected upgrade (non-101, e.g. a dead cookie's
115
+ * HTTP 401) emits `'error'` with no listener of its own — an uncaught
116
+ * exception that kills the hub process. The listener is a no-op: ws
117
+ * guarantees `'close'` follows `'error'`, and the close path already
118
+ * classifies (in-flight streams fail `gateway/socket-closed`) and retires
119
+ * the connection for redial. WS-lane auth failure therefore classifies
120
+ * as a socket cut post-fix — acceptable: the next POST-lane call is the
121
+ * one that reports `gateway/auth-expired`.
122
+ * - `ws` THROWS on sends made while CONNECTING, and its post-close send
123
+ * bookkeeping can itself throw in teardown windows, so every send is
124
+ * guarded: pre-open sends queue (flushed in order on 'open'), post-close
125
+ * sends are swallowed — the stream is dead and the close path owns the
126
+ * teardown. WS-level pings stay at the WS layer: `on('message')` fires
127
+ * for text data frames only, so protocol-level ping/pong NEVER surfaces
128
+ * as text.
129
+ */
130
+ export function defaultWsFactory(url, headers) {
131
+ const WebSocketCtor = loadWs();
132
+ const socket = new WebSocketCtor(url, { headers });
133
+ socket.on('error', () => { }); // crash guard: 'close' (and its classification) always follows
134
+ let open = false;
135
+ let closed = false;
136
+ const pending = [];
137
+ socket.on('open', () => {
138
+ open = true;
139
+ for (const data of pending.splice(0))
140
+ socket.send(data);
141
+ });
142
+ socket.on('close', () => {
143
+ closed = true;
144
+ open = false;
145
+ pending.length = 0;
146
+ });
147
+ return {
148
+ send: (data) => {
149
+ if (closed)
150
+ return;
151
+ if (!open) {
152
+ pending.push(data);
153
+ return;
154
+ }
155
+ try {
156
+ socket.send(data);
157
+ }
158
+ catch {
159
+ // CONNECTING/CLOSING race or sendAfterClose bookkeeping — the close
160
+ // path owns teardown; a lost frame on a dying socket changes nothing.
161
+ }
162
+ },
163
+ onMessage: (cb) => {
164
+ socket.on('message', (data) => { cb(String(data)); });
165
+ },
166
+ onClose: (cb) => { socket.on('close', () => { cb(); }); },
167
+ close: () => { socket.close(); },
168
+ };
169
+ }
170
+ /** The ws(s) URL of the peer's mux lane (foreign: the `/hub` family with
171
+ * the demux key; native: ctx0's own surface, no query). */
172
+ function muxUrlOf(origin, agentKey, face) {
173
+ const url = new URL(origin);
174
+ url.protocol = url.protocol === 'https:' || url.protocol === 'wss:' ? 'wss:' : 'ws:';
175
+ url.pathname = `${url.pathname.replace(/\/+$/, '')}${face === 'native' ? '/api/remote.mux' : '/hub/api/remote.mux'}`;
176
+ if (face === 'foreign')
177
+ url.search = `agent=${encodeURIComponent(agentKey)}`;
178
+ return url.toString();
179
+ }
180
+ function isRecord(value) {
181
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
182
+ }
183
+ /** An uplink lane is recognizable at the seam: iterables (sync or async —
184
+ * `for await` bridges sync ones) are never AbortSignals. */
185
+ function isUplinkLane(value) {
186
+ return typeof value === 'object' && value !== null
187
+ && (Symbol.asyncIterator in value || Symbol.iterator in value);
188
+ }
189
+ /** One gateway/* failure body — the brief-pinned thrown shape. */
190
+ function wireFail(code, message) {
191
+ return { code, message };
192
+ }
193
+ function messageOf(error) {
194
+ return error instanceof Error ? error.message : String(error);
195
+ }
196
+ class RemoteGatewayImpl {
197
+ origin;
198
+ agentKey;
199
+ cookieFn;
200
+ fetchImpl;
201
+ wsFactory;
202
+ idleCloseMs;
203
+ face;
204
+ muxUrl;
205
+ nextRpcId = 1;
206
+ connection;
207
+ idleTimer;
208
+ constructor(options) {
209
+ this.origin = options.origin;
210
+ this.agentKey = options.agentKey;
211
+ this.cookieFn = options.cookie;
212
+ this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
213
+ this.wsFactory = options.wsFactory ?? defaultWsFactory;
214
+ this.idleCloseMs = options.idleCloseMs ?? DEFAULT_IDLE_CLOSE_MS;
215
+ this.face = options.face ?? 'foreign';
216
+ this.muxUrl = muxUrlOf(options.origin, options.agentKey, this.face);
217
+ }
218
+ async dispatchRpc(endpoint, payload, signal) {
219
+ // Foreign: the /hub family with the demux key. Native: ctx0's own
220
+ // surface with the endpoint in the PATH, raw (see the header's URL-truth
221
+ // note) — exactly the URL a browser on the peer posts.
222
+ const url = this.face === 'native'
223
+ ? `${this.origin}/api/${endpoint}`
224
+ : `${this.origin}/hub/api?agent=${encodeURIComponent(this.agentKey)}`;
225
+ const rpcId = String(this.nextRpcId);
226
+ this.nextRpcId += 1;
227
+ const body = JSON.stringify({
228
+ type: 'client-request',
229
+ rpcId,
230
+ method: endpoint,
231
+ payload: payload === undefined ? null : payload, // the payload rule: never undefined
232
+ });
233
+ const cookie = this.cookieFn();
234
+ let response;
235
+ try {
236
+ response = await this.fetchImpl(url, {
237
+ method: 'POST',
238
+ headers: {
239
+ 'content-type': 'application/json',
240
+ ...(cookie === '' ? {} : { cookie }),
241
+ },
242
+ body,
243
+ signal,
244
+ });
245
+ }
246
+ catch (error) {
247
+ if (signal?.aborted)
248
+ throw wireFail('gateway/aborted', `mesh-gateway: ${endpoint} aborted`);
249
+ throw wireFail('gateway/unreachable', `mesh-gateway: POST ${url} failed — ${messageOf(error)}`);
250
+ }
251
+ if (response.status === 401) {
252
+ throw wireFail('gateway/auth-expired', `mesh-gateway: ${endpoint} answered 401 — the pairing cookie is expired (machine-level downgrade material)`);
253
+ }
254
+ if (response.status !== 200) {
255
+ throw wireFail('gateway/unreachable', `mesh-gateway: ${endpoint} answered unexpected status ${response.status} from ${this.origin}`);
256
+ }
257
+ let text;
258
+ try {
259
+ text = await response.text();
260
+ }
261
+ catch (error) {
262
+ if (signal?.aborted)
263
+ throw wireFail('gateway/aborted', `mesh-gateway: ${endpoint} aborted mid-body`);
264
+ throw wireFail('gateway/unreachable', `mesh-gateway: reading ${endpoint} response failed — ${messageOf(error)}`);
265
+ }
266
+ let parsed;
267
+ try {
268
+ parsed = JSON.parse(text);
269
+ }
270
+ catch {
271
+ throw wireFail('gateway/bad-response', `mesh-gateway: ${endpoint} response is not JSON`);
272
+ }
273
+ if (!isRecord(parsed) || parsed['type'] !== 'server-response' || parsed['rpcId'] !== rpcId || !('result' in parsed)) {
274
+ throw wireFail('gateway/bad-response', `mesh-gateway: ${endpoint} response is not a server-response envelope for rpc ${rpcId}`);
275
+ }
276
+ // The Result ({ok,value} | {ok:false,error}) is DATA: returned verbatim,
277
+ // never unwrapped or thrown — the caller classifies.
278
+ return parsed['result'];
279
+ }
280
+ openWireStream(endpoint, payload, uplinkOrSignal, _peer, signal, _control) {
281
+ const carrierShape = isUplinkLane(uplinkOrSignal);
282
+ const uplink = carrierShape ? uplinkOrSignal : undefined;
283
+ const abortSignal = carrierShape ? signal : uplinkOrSignal;
284
+ const gateway = this;
285
+ // The wire stream opens on the first read — an iterable never iterated
286
+ // dials nothing and leaks nothing.
287
+ async function* downlink() {
288
+ if (abortSignal?.aborted)
289
+ return;
290
+ const conn = gateway.acquireConnection();
291
+ let handle;
292
+ try {
293
+ handle = conn.client.openStream(endpoint, payload === undefined ? null : payload);
294
+ }
295
+ catch (error) {
296
+ gateway.releaseConnection(conn);
297
+ throw error;
298
+ }
299
+ if (uplink === undefined) {
300
+ // The GatewayFace shape carries no uplink lane: half-close at once so
301
+ // a server handler reading uplink sees EOF instead of hanging.
302
+ handle.end();
303
+ }
304
+ else {
305
+ // T1 ruling idiom: one read of the channel iterator sends one item
306
+ // frame; empty/errored uplink settles to end/cancel. A `done` read
307
+ // means the stream died (socket cut / cancel) — stop pumping quietly
308
+ // instead of draining the source against a dead wire.
309
+ const push = handle.upstream[Symbol.asyncIterator]();
310
+ void (async () => {
311
+ try {
312
+ for await (const value of uplink) {
313
+ if ((await push.next(value)).done)
314
+ return;
315
+ }
316
+ }
317
+ catch {
318
+ handle.cancel();
319
+ return;
320
+ }
321
+ handle.end();
322
+ })();
323
+ }
324
+ const onAbort = () => { handle.cancel(); };
325
+ abortSignal?.addEventListener('abort', onAbort, { once: true });
326
+ try {
327
+ yield* handle.items;
328
+ }
329
+ finally {
330
+ abortSignal?.removeEventListener('abort', onAbort);
331
+ handle.cancel(); // no-op when the stream settled; cancels the wire on early exit
332
+ gateway.releaseConnection(conn);
333
+ }
334
+ }
335
+ return downlink();
336
+ }
337
+ dispose() {
338
+ this.clearIdleTimer();
339
+ const conn = this.connection;
340
+ this.connection = undefined;
341
+ conn?.client.dispose();
342
+ }
343
+ /** Take a live-stream slot on the shared connection, dialing it if needed. */
344
+ acquireConnection() {
345
+ this.clearIdleTimer();
346
+ let conn = this.connection;
347
+ if (conn === undefined) {
348
+ conn = this.connect();
349
+ this.connection = conn;
350
+ }
351
+ conn.live += 1;
352
+ return conn;
353
+ }
354
+ /** Drop a live-stream slot; the last one out arms the idle-close timer. */
355
+ releaseConnection(conn) {
356
+ conn.live = Math.max(0, conn.live - 1);
357
+ if (conn.live > 0 || this.connection !== conn || this.idleTimer !== undefined)
358
+ return;
359
+ const timer = setTimeout(() => {
360
+ this.idleTimer = undefined;
361
+ if (this.connection === conn && conn.live === 0) {
362
+ this.connection = undefined;
363
+ conn.client.dispose();
364
+ }
365
+ }, this.idleCloseMs);
366
+ timer.unref();
367
+ this.idleTimer = timer;
368
+ }
369
+ /** Dial the mux ws and wrap it so a transport cut retires the connection. */
370
+ connect() {
371
+ const cookie = this.cookieFn();
372
+ const headers = cookie === '' ? {} : { cookie };
373
+ const socket = this.wsFactory(this.muxUrl, headers);
374
+ let conn;
375
+ const wrapped = {
376
+ send: (data) => { socket.send(data); },
377
+ onMessage: (cb) => { socket.onMessage(cb); },
378
+ onClose: (cb) => {
379
+ socket.onClose(() => {
380
+ cb(); // the MuxClient tears its streams down first (T1 contract) …
381
+ if (this.connection === conn) {
382
+ this.connection = undefined; // … then the dead connection is dropped: the next call redials
383
+ this.clearIdleTimer();
384
+ }
385
+ });
386
+ },
387
+ close: () => { socket.close(); },
388
+ };
389
+ const client = new MuxClient(wrapped, { heartbeatIntervalMs: HEARTBEAT_OFF_MS });
390
+ conn = { client, live: 0 };
391
+ return conn;
392
+ }
393
+ clearIdleTimer() {
394
+ if (this.idleTimer === undefined)
395
+ return;
396
+ clearTimeout(this.idleTimer);
397
+ this.idleTimer = undefined;
398
+ }
399
+ }
400
+ /**
401
+ * Build one remote agent's wire gateway toward a paired hub's `/hub` family.
402
+ * @param options - see {@link RemoteGatewayOptions}.
403
+ * @returns the {@link RemoteGateway} (a GatewayFace; register it with
404
+ * `registerForeignTarget` — callers cannot tell it from a local world gateway).
405
+ */
406
+ export function createRemoteGateway(options) {
407
+ return new RemoteGatewayImpl(options);
408
+ }