super-dsh 0.1.5 → 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 (79) hide show
  1. package/client/index.js +1010 -345
  2. package/cordis.patch.yml +22 -5
  3. package/package.json +8 -3
  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 +32 -17
  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/fusion-transport.d.ts +23 -21
  41. package/superd-hub/dist/fusion-transport.js +12 -40
  42. package/superd-hub/dist/gateway.d.ts +39 -2
  43. package/superd-hub/dist/gateway.js +62 -3
  44. package/superd-hub/dist/hub-tool.d.ts +174 -0
  45. package/superd-hub/dist/hub-tool.js +464 -0
  46. package/superd-hub/dist/index.d.ts +18 -4
  47. package/superd-hub/dist/index.js +19 -3
  48. package/superd-hub/dist/machines.d.ts +45 -0
  49. package/superd-hub/dist/machines.js +125 -0
  50. package/superd-hub/dist/mesh-federate.d.ts +138 -0
  51. package/superd-hub/dist/mesh-federate.js +223 -0
  52. package/superd-hub/dist/mesh-gateway.d.ts +67 -0
  53. package/superd-hub/dist/mesh-gateway.js +408 -0
  54. package/superd-hub/dist/mesh-link.d.ts +274 -0
  55. package/superd-hub/dist/mesh-link.js +441 -0
  56. package/superd-hub/dist/mesh-rpc.d.ts +123 -0
  57. package/superd-hub/dist/mesh-rpc.js +250 -0
  58. package/superd-hub/dist/mux-client.d.ts +71 -0
  59. package/superd-hub/dist/mux-client.js +281 -0
  60. package/superd-hub/dist/pack-version.d.ts +2 -0
  61. package/superd-hub/dist/pack-version.js +30 -0
  62. package/superd-hub/dist/roster.d.ts +10 -0
  63. package/superd-hub/dist/roster.js +9 -1
  64. package/superd-hub/dist/rpc.d.ts +18 -2
  65. package/superd-hub/dist/rpc.js +34 -4
  66. package/superd-hub/dist/spawn-world.js +3 -1
  67. package/superd-hub/dist/switch-core.d.ts +98 -21
  68. package/superd-hub/dist/switch-core.js +123 -28
  69. package/superd-hub/dist/system-head.d.ts +22 -12
  70. package/superd-hub/dist/system-head.js +100 -84
  71. package/superd-hub/dist/toast-core.d.ts +43 -0
  72. package/superd-hub/dist/toast-core.js +63 -0
  73. package/superd-hub/dist/wire-endpoints.d.ts +264 -0
  74. package/superd-hub/dist/wire-endpoints.js +180 -0
  75. package/superd-hub/dist/world-join.js +8 -3
  76. package/superd-hub/dist/world-mount.d.ts +27 -0
  77. package/superd-hub/dist/world-mount.js +67 -0
  78. package/superd-hub/lib/client/index.js +1010 -345
  79. package/superd-hub/package.json +11 -2
@@ -0,0 +1,250 @@
1
+ import { ensureSelf, listRemotes, removeRemote } from './machines.js';
2
+ import { machineStatus, pairMachine, reprobeMachine, emitMachineRemoved } from './mesh-link.js';
3
+ import { A2A_ENDPOINT_PATH, A2A_OUTBOX_PATH, A2A_WIRE_VERSION, decodeA2aWireFrame } from './a2a-wire.js';
4
+ import { EXEC_API_BASE, parseOneShotExecBody, runOneShotExec } from './exec.js';
5
+ /** The shared-channel Fetch route path this API owns. */
6
+ export const MESH_API_BASE = '/api/superd/machines';
7
+ function json(status, body) {
8
+ return new Response(JSON.stringify(body), {
9
+ status,
10
+ headers: { 'content-type': 'application/json' },
11
+ });
12
+ }
13
+ /** Project one persisted row to its public wire shape (allowlist — never
14
+ * token, never cookie) and join the mesh-link status entry by id. */
15
+ function publicRow(row) {
16
+ return projectRow(row, machineStatus());
17
+ }
18
+ function projectRow(row, status) {
19
+ const entry = status.get(row.id);
20
+ if (entry === undefined)
21
+ return { id: row.id, name: row.name, label: row.label, origin: row.origin, enabled: row.enabled, addedAt: row.addedAt, status: 'pending' };
22
+ const out = { id: row.id, name: row.name, label: row.label, origin: row.origin, enabled: row.enabled, addedAt: row.addedAt, status: entry.status, lastProbeAt: entry.lastProbeAt };
23
+ if (entry.error !== undefined)
24
+ out.error = entry.error;
25
+ if (entry.versionWarning !== undefined)
26
+ out.versionWarning = entry.versionWarning;
27
+ return out;
28
+ }
29
+ function errMsg(err) {
30
+ return err instanceof Error ? err.message : String(err);
31
+ }
32
+ async function readJsonBody(request) {
33
+ let body;
34
+ try {
35
+ body = await request.json();
36
+ }
37
+ catch (err) {
38
+ throw new Error(`mesh-api: request body is not valid JSON — ${errMsg(err)}`);
39
+ }
40
+ if (typeof body !== 'object' || body === null || Array.isArray(body)) {
41
+ throw new Error('mesh-api: request body must be a JSON object');
42
+ }
43
+ return body;
44
+ }
45
+ /**
46
+ * The mesh-machines handler: GET list / POST pair-or-reprobe / DELETE remove.
47
+ * @param request - the incoming `/api/superd/machines` request.
48
+ * @param deps - home + optional fetch override (tests inject a fixture).
49
+ * @returns the JSON response.
50
+ */
51
+ export async function handleMeshMachines(request, deps) {
52
+ const method = request.method;
53
+ if (method === 'GET')
54
+ return handleList(deps);
55
+ if (method === 'POST')
56
+ return handlePost(request, deps);
57
+ if (method === 'DELETE')
58
+ return handleRemove(request, deps);
59
+ return json(405, { error: `mesh-api: method ${method} is not supported on ${MESH_API_BASE}` });
60
+ }
61
+ /** GET: self (lazily minted on first need, spec §3.1) + every remote row in
62
+ * its public projection, status joined from the mesh-link table. */
63
+ function handleList(deps) {
64
+ const self = ensureSelf(deps.home, 'local');
65
+ const status = machineStatus();
66
+ const machines = listRemotes(deps.home).map((row) => projectRow(row, status));
67
+ return json(200, { self, machines });
68
+ }
69
+ /** POST: body-dispatched — `{op: 'reprobe', id}` re-probes, anything else is
70
+ * a pairing input `{name, label?, tokenUrl}`. */
71
+ async function handlePost(request, deps) {
72
+ let body;
73
+ try {
74
+ body = await readJsonBody(request);
75
+ }
76
+ catch (err) {
77
+ return json(400, { error: errMsg(err) });
78
+ }
79
+ if (typeof body.op === 'string' && body.op !== '') {
80
+ if (body.op !== 'reprobe')
81
+ return json(400, { error: `mesh-api: unknown op "${body.op}"` });
82
+ const id = typeof body.id === 'string' && body.id !== '' ? body.id : '';
83
+ if (id === '')
84
+ return json(400, { error: 'mesh-api: reprobe body must be {op: "reprobe", id} with a non-empty id' });
85
+ let probe;
86
+ try {
87
+ probe = await reprobeMachine(deps.home, id, { fetchImpl: deps.fetchImpl });
88
+ }
89
+ catch (err) {
90
+ return json(400, { error: errMsg(err) });
91
+ }
92
+ // A settled probe outcome (even unreachable/auth-expired) is an answer,
93
+ // not an operation failure — the status table records it either way.
94
+ return json(200, { probe });
95
+ }
96
+ const name = typeof body.name === 'string' && body.name !== '' ? body.name : '';
97
+ const tokenUrl = typeof body.tokenUrl === 'string' && body.tokenUrl !== '' ? body.tokenUrl : '';
98
+ if (name === '' || tokenUrl === '') {
99
+ return json(400, { error: 'mesh-api: pair body must be {name, label?, tokenUrl} with non-empty name and tokenUrl' });
100
+ }
101
+ const label = typeof body.label === 'string' && body.label !== '' ? body.label : undefined;
102
+ let outcome;
103
+ try {
104
+ outcome = await pairMachine(deps.home, { name, label, tokenUrl }, { fetchImpl: deps.fetchImpl });
105
+ }
106
+ catch (err) {
107
+ // mesh-link errors are actionable and token-redacted (Task 2 fix round 1).
108
+ return json(400, { error: errMsg(err) });
109
+ }
110
+ return json(201, { machine: publicRow(outcome.row) });
111
+ }
112
+ /** DELETE: drop one remote row by `?id=`; `{removed: false}` for unknown ids. */
113
+ function handleRemove(request, deps) {
114
+ const id = new URL(request.url).searchParams.get('id') ?? '';
115
+ if (id === '')
116
+ return json(400, { error: `mesh-api: DELETE on ${MESH_API_BASE} requires a non-empty ?id= query parameter` });
117
+ const removed = removeRemote(deps.home, id);
118
+ if (removed)
119
+ emitMachineRemoved(id); // federation bus: the registrar drops every row of the machine
120
+ return json(200, { removed });
121
+ }
122
+ /**
123
+ * Register the mesh-machines API on the shared `/api` connection channel
124
+ * (behind the standard browser-trust + auth fence). No-op when the
125
+ * `connection` service is absent (e.g. fake-ctx unit tests).
126
+ */
127
+ export function registerMeshRpc(ctx, deps) {
128
+ ctx.inject(['connection'], (connCtx) => {
129
+ const fetchFace = Reflect.get(connCtx, 'connection')?.fetch;
130
+ if (fetchFace === undefined)
131
+ return; // connection not actually present (test ctx)
132
+ connCtx.effect(() => fetchFace.register({
133
+ path: MESH_API_BASE,
134
+ methods: [...ROUTE_METHODS],
135
+ requestBody: 'buffered',
136
+ fetch: (request) => handleMeshMachines(request, deps),
137
+ }), 'superd-hub: /api/superd/machines route');
138
+ });
139
+ }
140
+ /** Every method our exact paths claim on the shared `/api` channel — unknown
141
+ * methods land in the HANDLER's honest 405 (a PUT on a known path is "method
142
+ * not supported", not a fall-through host 404; 2026-09-29 field finding:
143
+ * PUT /api/superd/machines answered 404 because the registration only
144
+ * claimed GET/POST/DELETE). */
145
+ const ROUTE_METHODS = ['GET', 'POST', 'DELETE', 'PUT', 'PATCH', 'HEAD'];
146
+ // ---------------------------------------------------------------- a2a (postoffice R3)
147
+ // (A2aRpcDeps retired in R4 — registerA2aRpc takes the SHARED ingest instance)
148
+ /**
149
+ * POST /api/superd/a2a — body = one v1 wire frame `{v: 1, envelope}` → the
150
+ * envelope's receipt (new/message: send receipt; stop: stop receipt;
151
+ * reply/notice: the local delivery outcome). Malformed frames are 400s with
152
+ * the decoder's named reason.
153
+ */
154
+ export async function handleA2aIngestPost(request, ingest) {
155
+ if (request.method !== 'POST') {
156
+ return json(405, { error: `mesh-api: method ${request.method} is not supported on ${A2A_ENDPOINT_PATH} (use POST)` });
157
+ }
158
+ let body;
159
+ try {
160
+ body = await request.json();
161
+ }
162
+ catch (err) {
163
+ return json(400, { error: `mesh-api: a2a body is not valid JSON — ${err instanceof Error ? err.message : String(err)}` });
164
+ }
165
+ const decoded = decodeA2aWireFrame(body);
166
+ if (!decoded.ok)
167
+ return json(400, { error: decoded.error });
168
+ const receipt = await ingest.ingest(decoded.envelope);
169
+ return json(200, receipt);
170
+ }
171
+ /**
172
+ * GET /api/superd/a2a/outbox?machine=<m4> — claim & clear the buffered
173
+ * envelopes addressed to the claiming machine (declarative claim, D1).
174
+ */
175
+ export function handleA2aOutboxGet(request, ingest) {
176
+ if (request.method !== 'GET') {
177
+ return json(405, { error: `mesh-api: method ${request.method} is not supported on ${A2A_OUTBOX_PATH} (use GET)` });
178
+ }
179
+ const machine = new URL(request.url).searchParams.get('machine') ?? '';
180
+ if (machine === '') {
181
+ return json(400, { error: 'mesh-api: a2a outbox claim requires a non-empty ?machine= query parameter' });
182
+ }
183
+ return json(200, { v: A2A_WIRE_VERSION, envelopes: ingest.claimOutbox(machine) });
184
+ }
185
+ /**
186
+ * Register the a2a endpoints (POST /api/superd/a2a + GET .../outbox) on the
187
+ * shared `/api` connection channel — same fence as the machines route. The
188
+ * ingest instance is SHARED (R4): the initiator service's self-routing leg
189
+ * calls the very same handler these routes call. No-op when `connection` is
190
+ * absent.
191
+ */
192
+ export function registerA2aRpc(ctx, ingest) {
193
+ ctx.inject(['connection'], (connCtx) => {
194
+ const fetchFace = Reflect.get(connCtx, 'connection')?.fetch;
195
+ if (fetchFace === undefined)
196
+ return; // connection not actually present (test ctx)
197
+ // (the ingest instance is injected — one instance shared with the service)
198
+ connCtx.effect(() => fetchFace.register({
199
+ path: A2A_ENDPOINT_PATH,
200
+ methods: [...ROUTE_METHODS],
201
+ requestBody: 'buffered',
202
+ fetch: (request) => handleA2aIngestPost(request, ingest),
203
+ }), 'superd-hub: /api/superd/a2a route');
204
+ connCtx.effect(() => fetchFace.register({
205
+ path: A2A_OUTBOX_PATH,
206
+ methods: [...ROUTE_METHODS],
207
+ requestBody: 'buffered',
208
+ fetch: (request) => Promise.resolve(handleA2aOutboxGet(request, ingest)),
209
+ }), 'superd-hub: /api/superd/a2a/outbox route');
210
+ });
211
+ }
212
+ // ---------------------------------------------------------------- exec (P4, spec §8.1)
213
+ /**
214
+ * POST /api/superd/exec — one-shot `bash -lc <cmd>` on THIS machine (spec
215
+ * §8.1): tail-windowed stdout/stderr, two-stage group kill on timeout. The
216
+ * auth trust boundary is the shared `/api` fence (same as machines/a2a);
217
+ * the initiator-side per-machine knob (`execEnabled`) lives on the CALLER's
218
+ * registry row, not here.
219
+ */
220
+ export async function handleExecPost(request, log) {
221
+ if (request.method !== 'POST') {
222
+ return json(405, { error: `mesh-api: method ${request.method} is not supported on ${EXEC_API_BASE} (use POST)` });
223
+ }
224
+ let body;
225
+ try {
226
+ body = await request.json();
227
+ }
228
+ catch (err) {
229
+ return json(400, { error: `mesh-api: exec body is not valid JSON — ${err instanceof Error ? err.message : String(err)}` });
230
+ }
231
+ const parsed = parseOneShotExecBody(body);
232
+ if (!parsed.ok)
233
+ return json(400, { error: `mesh-api: ${parsed.error}` });
234
+ const result = await runOneShotExec(parsed.value, log);
235
+ return json(200, result);
236
+ }
237
+ /** Register the exec endpoint on the shared `/api` connection channel. */
238
+ export function registerExecRpc(ctx, log) {
239
+ ctx.inject(['connection'], (connCtx) => {
240
+ const fetchFace = Reflect.get(connCtx, 'connection')?.fetch;
241
+ if (fetchFace === undefined)
242
+ return; // connection not actually present (test ctx)
243
+ connCtx.effect(() => fetchFace.register({
244
+ path: EXEC_API_BASE,
245
+ methods: [...ROUTE_METHODS],
246
+ requestBody: 'buffered',
247
+ fetch: (request) => handleExecPost(request, log),
248
+ }), 'superd-hub: /api/superd/exec route');
249
+ });
250
+ }
@@ -0,0 +1,71 @@
1
+ /** Every client→server stream frame (control replies like `pong` excluded). */
2
+ export type MuxClientFrame = {
3
+ type: 'open';
4
+ streamId: number;
5
+ endpoint: string;
6
+ payload: unknown;
7
+ } | {
8
+ type: 'item';
9
+ streamId: number;
10
+ value?: unknown;
11
+ } | {
12
+ type: 'end';
13
+ streamId: number;
14
+ } | {
15
+ type: 'cancel';
16
+ streamId: number;
17
+ };
18
+ /** The injected text-socket seam (Task 2 adapts `ws` to this shape). */
19
+ export interface MuxClientSocket {
20
+ send(data: string): void;
21
+ onMessage(cb: (data: string) => void): void;
22
+ onClose(cb: () => void): void;
23
+ close(): void;
24
+ }
25
+ /** The frozen openStream handle (Task 2 builds on this exact shape). */
26
+ export interface MuxStreamHandle {
27
+ /** Server items until `end`/`error`/socket close (see the header rulings). */
28
+ items: AsyncIterable<unknown>;
29
+ /** Uplink channel: one `next(value)` read = one item frame, zero buffering. */
30
+ upstream: AsyncIterable<unknown>;
31
+ /** Uplink half-close: sends `{type:'end'}` once; downlink stays live. */
32
+ end(): void;
33
+ /** Cancel the logical stream: one `{type:'cancel'}` frame, local retirement. */
34
+ cancel(): void;
35
+ }
36
+ /** Options. */
37
+ export interface MuxClientOptions {
38
+ /** Server ping cadence used for the silence window (2 × this); upstream default 2s. */
39
+ heartbeatIntervalMs?: number;
40
+ }
41
+ /** A stream/transport failure carrying wire failure fields (or a local cut code). */
42
+ export declare class MuxClientError extends Error {
43
+ readonly code: string;
44
+ readonly details: object;
45
+ constructor(code: string, message: string, details?: object);
46
+ }
47
+ /** The mux client half: pure protocol, socket injected. */
48
+ export declare class MuxClient {
49
+ private readonly socket;
50
+ private readonly heartbeatIntervalMs;
51
+ private nextId;
52
+ private readonly streams;
53
+ private state;
54
+ private watchdog;
55
+ constructor(socket: MuxClientSocket, options?: MuxClientOptions);
56
+ /** Open one logical stream; the handle's lanes are documented in the header. */
57
+ openStream(endpoint: string, payload: unknown): MuxStreamHandle;
58
+ /** Close every stream and the socket. Idempotent; no cancel frames (the server unwinds on socket close). */
59
+ dispose(): void;
60
+ private endUplink;
61
+ private retire;
62
+ /** Serialize one frame; the wire id is the stringified numeric counter. */
63
+ private sendFrame;
64
+ private onData;
65
+ /** The 1008 mirror: an unusable server frame closes the socket. */
66
+ private protocolViolation;
67
+ /** Fail every live stream, drop the table, optionally close the socket. */
68
+ private teardown;
69
+ private armWatchdog;
70
+ private clearWatchdog;
71
+ }
@@ -0,0 +1,281 @@
1
+ /** A stream/transport failure carrying wire failure fields (or a local cut code). */
2
+ export class MuxClientError extends Error {
3
+ code;
4
+ details;
5
+ constructor(code, message, details = {}) {
6
+ super(message);
7
+ this.name = 'MuxClientError';
8
+ this.code = code;
9
+ this.details = details;
10
+ }
11
+ }
12
+ const DEFAULT_HEARTBEAT_MS = 2_000;
13
+ const MAX_SILENT_PERIODS = 2;
14
+ const DONE = { value: undefined, done: true };
15
+ function isRecord(value) {
16
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
17
+ }
18
+ function exactKeys(value, keys) {
19
+ const own = Object.keys(value);
20
+ return own.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
21
+ }
22
+ function validId(value) {
23
+ return typeof value === 'string' && value.length > 0;
24
+ }
25
+ /**
26
+ * Downlink lane of one stream: server items until a terminal frame or a local
27
+ * cut. Mirrors the upstream client's `StreamInbox` semantics — a wire `error`
28
+ * drains queued items first and then throws; local cuts drop the queue.
29
+ */
30
+ class DownlinkQueue {
31
+ queue = [];
32
+ failure;
33
+ thrown = false;
34
+ done = false;
35
+ waiters = [];
36
+ push(value) {
37
+ if (this.done || this.failure !== undefined)
38
+ return;
39
+ this.queue.push(value);
40
+ this.signal();
41
+ }
42
+ /** Terminal server `end` frame: queued items drain, then the iterable finishes. */
43
+ settleEnd() {
44
+ if (this.done || this.failure !== undefined)
45
+ return;
46
+ this.done = true;
47
+ this.signal();
48
+ }
49
+ /** Server `error` frame: queued items drain, then the iterable throws it. */
50
+ settleFailure(error) {
51
+ if (this.done || this.failure !== undefined)
52
+ return;
53
+ this.failure = error;
54
+ this.signal();
55
+ }
56
+ /** Local cancellation: queued items are dropped, the iterable finishes. */
57
+ abort() {
58
+ if (this.done || this.failure !== undefined)
59
+ return;
60
+ this.queue.length = 0;
61
+ this.done = true;
62
+ this.signal();
63
+ }
64
+ /** Local cut (socket close, silence, dispose): drop the queue, throw now. */
65
+ fail(error) {
66
+ if (this.failure !== undefined)
67
+ return;
68
+ this.queue.length = 0;
69
+ this.done = true;
70
+ this.failure = error;
71
+ this.signal();
72
+ }
73
+ [Symbol.asyncIterator]() {
74
+ return this;
75
+ }
76
+ async next() {
77
+ while (true) {
78
+ if (this.queue.length > 0)
79
+ return { value: this.queue.shift(), done: false };
80
+ if (this.failure !== undefined && !this.thrown) {
81
+ this.thrown = true;
82
+ throw this.failure;
83
+ }
84
+ if (this.done || this.thrown)
85
+ return DONE;
86
+ await new Promise((resolve) => { this.waiters.push(resolve); });
87
+ }
88
+ }
89
+ signal() {
90
+ const waiters = [...this.waiters];
91
+ this.waiters.length = 0;
92
+ for (const wake of waiters)
93
+ wake();
94
+ }
95
+ }
96
+ /** The mux client half: pure protocol, socket injected. */
97
+ export class MuxClient {
98
+ socket;
99
+ heartbeatIntervalMs;
100
+ nextId = 1;
101
+ streams = new Map();
102
+ state = 'open';
103
+ watchdog;
104
+ constructor(socket, options = {}) {
105
+ this.socket = socket;
106
+ this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
107
+ socket.onMessage((data) => { this.onData(data); });
108
+ socket.onClose(() => {
109
+ this.teardown(new MuxClientError('gateway/socket-closed', 'mux-client: the server closed the socket'));
110
+ });
111
+ this.armWatchdog();
112
+ }
113
+ /** Open one logical stream; the handle's lanes are documented in the header. */
114
+ openStream(endpoint, payload) {
115
+ if (this.state !== 'open')
116
+ throw new MuxClientError('gateway/disposed', 'mux-client: disposed');
117
+ const id = this.nextId;
118
+ this.nextId += 1;
119
+ const stream = { downlink: new DownlinkQueue(), ended: false, retired: false };
120
+ this.streams.set(String(id), stream);
121
+ this.sendFrame({ type: 'open', streamId: id, endpoint, payload });
122
+ const key = String(id);
123
+ const live = () => this.state === 'open' && !stream.retired;
124
+ const upstream = {
125
+ [Symbol.asyncIterator]: () => ({
126
+ next: (value) => {
127
+ if (!live() || stream.ended)
128
+ return Promise.resolve(DONE);
129
+ this.sendFrame(value === undefined
130
+ ? { type: 'item', streamId: id }
131
+ : { type: 'item', streamId: id, value });
132
+ return Promise.resolve({ value: undefined, done: false });
133
+ },
134
+ return: () => {
135
+ this.endUplink(id, stream);
136
+ return Promise.resolve(DONE);
137
+ },
138
+ }),
139
+ };
140
+ return {
141
+ items: stream.downlink,
142
+ upstream,
143
+ end: () => { this.endUplink(id, stream); },
144
+ cancel: () => {
145
+ if (!live())
146
+ return;
147
+ this.retire(key, stream);
148
+ stream.downlink.abort();
149
+ this.sendFrame({ type: 'cancel', streamId: id });
150
+ },
151
+ };
152
+ }
153
+ /** Close every stream and the socket. Idempotent; no cancel frames (the server unwinds on socket close). */
154
+ dispose() {
155
+ this.teardown(new MuxClientError('gateway/disposed', 'mux-client: disposed'), true);
156
+ }
157
+ endUplink(id, stream) {
158
+ if (this.state !== 'open' || stream.retired || stream.ended)
159
+ return;
160
+ stream.ended = true;
161
+ this.sendFrame({ type: 'end', streamId: id });
162
+ }
163
+ retire(key, stream) {
164
+ stream.retired = true;
165
+ this.streams.delete(key);
166
+ }
167
+ /** Serialize one frame; the wire id is the stringified numeric counter. */
168
+ sendFrame(frame) {
169
+ const wire = { type: frame.type, streamId: String(frame.streamId) };
170
+ if (frame.type === 'open') {
171
+ wire['endpoint'] = frame.endpoint;
172
+ wire['payload'] = frame.payload;
173
+ }
174
+ if (frame.type === 'item' && frame.value !== undefined)
175
+ wire['value'] = frame.value;
176
+ this.socket.send(JSON.stringify(wire));
177
+ }
178
+ onData(data) {
179
+ if (this.state !== 'open')
180
+ return;
181
+ this.armWatchdog(); // any received frame is liveness
182
+ let raw;
183
+ try {
184
+ raw = JSON.parse(data);
185
+ }
186
+ catch {
187
+ this.protocolViolation('malformed server frame');
188
+ return;
189
+ }
190
+ if (!isRecord(raw)) {
191
+ this.protocolViolation('malformed server frame');
192
+ return;
193
+ }
194
+ if (raw['type'] === 'ping') {
195
+ if (!exactKeys(raw, ['type'])) {
196
+ this.protocolViolation('malformed ping frame');
197
+ return;
198
+ }
199
+ this.socket.send('{"type":"pong"}');
200
+ return;
201
+ }
202
+ if (raw['type'] === 'item') {
203
+ if (!(exactKeys(raw, ['type', 'streamId']) || exactKeys(raw, ['type', 'streamId', 'value']))
204
+ || !validId(raw['streamId'])) {
205
+ this.protocolViolation('malformed item frame');
206
+ return;
207
+ }
208
+ const stream = this.streams.get(raw['streamId']);
209
+ if (stream === undefined)
210
+ return; // late frame: stream retired, drop
211
+ stream.downlink.push(Object.hasOwn(raw, 'value') ? raw['value'] : undefined);
212
+ return;
213
+ }
214
+ if (raw['type'] === 'end') {
215
+ if (!exactKeys(raw, ['type', 'streamId']) || !validId(raw['streamId'])) {
216
+ this.protocolViolation('malformed end frame');
217
+ return;
218
+ }
219
+ const stream = this.streams.get(raw['streamId']);
220
+ if (stream === undefined)
221
+ return;
222
+ this.retire(raw['streamId'], stream);
223
+ stream.downlink.settleEnd();
224
+ return;
225
+ }
226
+ if (raw['type'] === 'error') {
227
+ const failure = failureOf(raw['error']);
228
+ if (failure === undefined || !exactKeys(raw, ['type', 'streamId', 'error']) || !validId(raw['streamId'])) {
229
+ this.protocolViolation('malformed error frame');
230
+ return;
231
+ }
232
+ const stream = this.streams.get(raw['streamId']);
233
+ if (stream === undefined)
234
+ return;
235
+ this.retire(raw['streamId'], stream);
236
+ stream.downlink.settleFailure(new MuxClientError(failure.code, failure.message, failure.details));
237
+ return;
238
+ }
239
+ this.protocolViolation(`out-of-protocol server frame (${String(raw['type'])})`);
240
+ }
241
+ /** The 1008 mirror: an unusable server frame closes the socket. */
242
+ protocolViolation(detail) {
243
+ this.teardown(new MuxClientError('gateway/protocol', `mux-client: ${detail} — closing the socket`), true);
244
+ }
245
+ /** Fail every live stream, drop the table, optionally close the socket. */
246
+ teardown(error, close = false) {
247
+ if (this.state !== 'open')
248
+ return;
249
+ this.state = 'closed';
250
+ this.clearWatchdog();
251
+ for (const stream of this.streams.values()) {
252
+ stream.retired = true;
253
+ stream.downlink.fail(error);
254
+ }
255
+ this.streams.clear();
256
+ if (close)
257
+ this.socket.close();
258
+ }
259
+ armWatchdog() {
260
+ this.clearWatchdog();
261
+ const timer = setTimeout(() => {
262
+ this.teardown(new MuxClientError('gateway/heartbeat-silence', `mux-client: no frames for ${String(this.heartbeatIntervalMs * MAX_SILENT_PERIODS)}ms`), true);
263
+ }, this.heartbeatIntervalMs * MAX_SILENT_PERIODS);
264
+ this.watchdog = timer;
265
+ }
266
+ clearWatchdog() {
267
+ if (this.watchdog === undefined)
268
+ return;
269
+ clearTimeout(this.watchdog);
270
+ this.watchdog = undefined;
271
+ }
272
+ }
273
+ /** Validate a wire failure body (`MuxFailure` shape); undefined when malformed. */
274
+ function failureOf(value) {
275
+ if (!isRecord(value))
276
+ return undefined;
277
+ if (typeof value['code'] !== 'string' || typeof value['message'] !== 'string')
278
+ return undefined;
279
+ const details = value['details'];
280
+ return { code: value['code'], message: value['message'], details: isRecord(details) ? details : {} };
281
+ }
@@ -0,0 +1,2 @@
1
+ /** The super-dsh pack version, or 'unknown' when the manifest cannot be read. */
2
+ export declare const PACK_VERSION: string;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The super-dsh PACK version (D6 fix): `super-dsh/package.json`'s version —
3
+ * the number `dsh plugin add` actually versions and the only one that moves
4
+ * with releases. The hub SUBPACKAGE's own manifest is a constant 0.1.0, which
5
+ * made both the served `hub.version` and the I2 comparator dead code exactly
6
+ * in the case they exist for (a stale peer pack).
7
+ *
8
+ * Depth contract: this module compiles to `<pack root>/superd-hub/dist/`, so
9
+ * the pack manifest is exactly two hops up — `../../package.json`. Verified
10
+ * against the built tree in BOTH shapes: repo (`super-dsh/superd-hub/dist/`
11
+ * → `super-dsh/package.json`) and the packed tarball (`package/superd-hub/dist/`
12
+ * → `package/package.json`; npm always includes the root manifest).
13
+ *
14
+ * Unparsable or missing → 'unknown' (warn honestly: the I2 comparator treats
15
+ * an unreadable version as a compatibility fact, which is the truth).
16
+ */
17
+ import { createRequire } from 'node:module';
18
+ function readPackVersion() {
19
+ try {
20
+ const pkg = createRequire(import.meta.url)('../../package.json');
21
+ if (typeof pkg.version === 'string' && pkg.version !== '')
22
+ return pkg.version;
23
+ return 'unknown';
24
+ }
25
+ catch {
26
+ return 'unknown';
27
+ }
28
+ }
29
+ /** The super-dsh pack version, or 'unknown' when the manifest cannot be read. */
30
+ export const PACK_VERSION = readPackVersion();
@@ -15,6 +15,14 @@ export interface RosterEntry {
15
15
  label: string;
16
16
  /** True once the adapter's world context is spawned and its target registered. */
17
17
  ready: boolean;
18
+ /**
19
+ * Owning machine id (m4) for FEDERATED rows (mesh P2 Task 3); absent = this
20
+ * machine's local world. The a6-keyed row's owner; `agentLinks` projects it
21
+ * onto AgentLink.machine and derives remote grouping/ordering from it.
22
+ */
23
+ machine?: string;
24
+ /** True once the world's spawn REJECTED (world-join catch) — stays mounted for diagnosis. */
25
+ failed?: boolean;
18
26
  }
19
27
  /** Register an agent. Duplicate keys are a plugin bug — throw loud. */
20
28
  export declare function registerAgent(entry: RosterEntry): void;
@@ -22,6 +30,8 @@ export declare function registerAgent(entry: RosterEntry): void;
22
30
  export declare function unregisterAgent(key: string): void;
23
31
  /** Flip readiness (world spawned / disposed). Unknown keys are a plugin bug. */
24
32
  export declare function setReady(key: string, ready: boolean): void;
33
+ /** Flip failure (world spawn rejected). Unknown keys are a plugin bug. */
34
+ export declare function setFailed(key: string, failed: boolean): void;
25
35
  /** Snapshot copy — callers can hold it without aliasing the roster. */
26
36
  export declare function listAgents(): RosterEntry[];
27
37
  /** Subscribe to roster changes; returns an unsubscribe function. */
@@ -9,7 +9,7 @@ function emit() {
9
9
  export function registerAgent(entry) {
10
10
  if (entries.has(entry.key))
11
11
  throw new Error(`roster: duplicate agent key ${entry.key}`);
12
- entries.set(entry.key, { ...entry });
12
+ entries.set(entry.key, { ...entry, failed: entry.failed ?? false });
13
13
  emit();
14
14
  }
15
15
  /** Drop an agent (adapter plugin disposed). */
@@ -25,6 +25,14 @@ export function setReady(key, ready) {
25
25
  e.ready = ready;
26
26
  emit();
27
27
  }
28
+ /** Flip failure (world spawn rejected). Unknown keys are a plugin bug. */
29
+ export function setFailed(key, failed) {
30
+ const e = entries.get(key);
31
+ if (!e)
32
+ throw new Error(`roster: unknown agent ${key}`);
33
+ e.failed = failed;
34
+ emit();
35
+ }
28
36
  /** Snapshot copy — callers can hold it without aliasing the roster. */
29
37
  export function listAgents() {
30
38
  return [...entries.values()].map((e) => ({ ...e }));