super-dsh 0.1.3 → 0.1.5
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.
- package/README.md +3 -3
- package/agent-agy/cordis.patch.yml +8 -4
- package/agent-agy/dist/agent-preset-agy.d.ts +23 -43
- package/agent-agy/dist/agent-preset-agy.js +29 -166
- package/agent-agy/dist/agent.d.ts +2 -1
- package/agent-agy/dist/agent.js +3 -2
- package/agent-agy/dist/agy-sessions.d.ts +2 -1
- package/agent-agy/dist/agy-sessions.js +2 -11
- package/agent-agy/dist/index.js +13 -12
- package/agent-agy/dist/world-plugin.d.ts +2 -1
- package/agent-agy/dist/world-plugin.js +31 -51
- package/agent-agy/package.json +6 -6
- package/agent-claude/cordis.patch.yml +8 -4
- package/agent-claude/dist/agent-preset-claude.js +29 -166
- package/agent-claude/dist/agent.js +10 -10
- package/agent-claude/dist/claude-client.js +22 -3
- package/agent-claude/dist/claude-home.js +0 -8
- package/agent-claude/dist/index.js +12 -15
- package/agent-claude/dist/session-map.js +2 -5
- package/agent-claude/dist/world-plugin.js +3 -3
- package/agent-claude/package.json +9 -9
- package/agent-codex/README.md +8 -8
- package/agent-codex/cordis.patch.yml +9 -5
- package/agent-codex/dist/agent-preset-codex.js +29 -168
- package/agent-codex/dist/agent.js +17 -12
- package/agent-codex/dist/codex-store.js +0 -9
- package/agent-codex/dist/index.js +13 -16
- package/agent-codex/dist/session-map.js +2 -5
- package/agent-codex/dist/world-plugin.js +31 -51
- package/agent-codex/package.json +14 -11
- package/agent-hermes/README.md +9 -8
- package/agent-hermes/cordis.patch.yml +8 -4
- package/agent-hermes/dist/agent-preset-hermes.js +29 -168
- package/agent-hermes/dist/agent.js +33 -19
- package/agent-hermes/dist/hermes-client.js +5 -5
- package/agent-hermes/dist/hermes-store.js +2 -11
- package/agent-hermes/dist/index.js +13 -12
- package/agent-hermes/dist/world-plugin.js +31 -52
- package/agent-hermes/package.json +14 -11
- package/agent-omp/README.md +2 -2
- package/agent-omp/cordis.patch.yml +12 -7
- package/agent-omp/dist/agent-preset-omp.js +30 -169
- package/agent-omp/dist/agent.js +13 -5
- package/agent-omp/dist/index.js +12 -22
- package/agent-omp/dist/mobile/gesture.js +2 -2
- package/agent-omp/dist/mobile/zoom-guard.js +4 -4
- package/agent-omp/dist/mobile-boot.js +2 -2
- package/agent-omp/dist/omp-cli.js +1 -1
- package/agent-omp/dist/omp-disk-discovery.js +1 -1
- package/agent-omp/dist/protocol.js +1 -1
- package/agent-omp/dist/sidecar-client.js +1 -1
- package/agent-omp/dist/world-plugin.js +37 -50
- package/agent-omp/package.json +18 -14
- package/agent-omp/sidecar/main.ts +2 -2
- package/agent-pi/README.md +5 -5
- package/agent-pi/cordis.patch.yml +8 -4
- package/agent-pi/dist/agent-preset-pi.js +29 -166
- package/agent-pi/dist/agent.js +18 -12
- package/agent-pi/dist/index.js +14 -16
- package/agent-pi/dist/pi-home.js +2 -12
- package/agent-pi/dist/world-plugin.js +31 -51
- package/agent-pi/package.json +14 -11
- package/client/index.js +1994 -150
- package/cordis.patch.yml +38 -15
- package/package.json +32 -30
- package/{agent-hub → superd-hub}/cordis.patch.yml +3 -3
- package/{agent-hub → superd-hub}/dist/agent-roster.d.ts +3 -1
- package/{agent-hub → superd-hub}/dist/agent-roster.js +4 -2
- package/superd-hub/dist/carrier.d.ts +62 -0
- package/superd-hub/dist/carrier.js +213 -0
- package/superd-hub/dist/fusion-transport.d.ts +148 -0
- package/superd-hub/dist/fusion-transport.js +397 -0
- package/{agent-hub → superd-hub}/dist/gateway.d.ts +9 -4
- package/superd-hub/dist/gateway.js +44 -0
- package/superd-hub/dist/hub-route.d.ts +74 -0
- package/superd-hub/dist/hub-route.js +188 -0
- package/{agent-hub → superd-hub}/dist/index.d.ts +14 -6
- package/{agent-hub → superd-hub}/dist/index.js +14 -6
- package/{agent-hub → superd-hub}/dist/roster.d.ts +2 -2
- package/{agent-hub → superd-hub}/dist/rpc.js +1 -1
- package/{agent-hub → superd-hub}/dist/spawn-world.d.ts +27 -6
- package/superd-hub/dist/spawn-world.js +279 -0
- package/superd-hub/dist/switch-core.d.ts +630 -0
- package/superd-hub/dist/switch-core.js +734 -0
- package/superd-hub/dist/system-head.d.ts +69 -0
- package/superd-hub/dist/system-head.js +194 -0
- package/superd-hub/dist/web-trust.d.ts +112 -0
- package/superd-hub/dist/web-trust.js +126 -0
- package/superd-hub/dist/world-entry.d.ts +38 -0
- package/superd-hub/dist/world-entry.js +47 -0
- package/{agent-hub → superd-hub}/dist/world-host.d.ts +15 -20
- package/superd-hub/dist/world-host.js +65 -0
- package/{agent-hub → superd-hub}/dist/world-join.d.ts +2 -1
- package/{agent-hub → superd-hub}/dist/world-join.js +30 -50
- package/superd-hub/dist/world-mount.d.ts +82 -0
- package/superd-hub/dist/world-mount.js +105 -0
- package/{agent-hub → superd-hub}/dist/world-mux.d.ts +21 -6
- package/superd-hub/dist/world-mux.js +392 -0
- package/{agent-hub → superd-hub}/dist/world-provision.d.ts +20 -4
- package/superd-hub/dist/world-provision.js +203 -0
- package/superd-hub/dist/world-web-server.d.ts +113 -0
- package/superd-hub/dist/world-web-server.js +99 -0
- package/superd-hub/lib/client/index.js +2086 -0
- package/{agent-hub → superd-hub}/package.json +12 -7
- package/agent-agy/dist/agent-preset-projection.d.ts +0 -31
- package/agent-agy/dist/agent-preset-projection.js +0 -33
- package/agent-claude/dist/agent-preset-projection.js +0 -30
- package/agent-codex/dist/agent-preset-projection.js +0 -37
- package/agent-codex/dist/app-home.js +0 -65
- package/agent-codex/dist/spike.js +0 -46
- package/agent-hermes/dist/agent-preset-projection.js +0 -34
- package/agent-hub/dist/carrier.d.ts +0 -43
- package/agent-hub/dist/carrier.js +0 -241
- package/agent-hub/dist/client-shim.d.ts +0 -28
- package/agent-hub/dist/client-shim.js +0 -251
- package/agent-hub/dist/gateway.js +0 -18
- package/agent-hub/dist/index-pass.d.ts +0 -22
- package/agent-hub/dist/index-pass.js +0 -47
- package/agent-hub/dist/labels.d.ts +0 -45
- package/agent-hub/dist/labels.js +0 -83
- package/agent-hub/dist/spawn-world.js +0 -72
- package/agent-hub/dist/world-entry.d.ts +0 -31
- package/agent-hub/dist/world-entry.js +0 -51
- package/agent-hub/dist/world-host.js +0 -44
- package/agent-hub/dist/world-mount.d.ts +0 -52
- package/agent-hub/dist/world-mount.js +0 -52
- package/agent-hub/dist/world-mux.js +0 -224
- package/agent-hub/dist/world-provision.js +0 -122
- package/agent-hub/dist/world-web-server.d.ts +0 -162
- package/agent-hub/dist/world-web-server.js +0 -248
- package/agent-hub/lib/client/index.js +0 -242
- package/agent-omp/dist/agent-preset-projection.js +0 -36
- package/agent-omp/dist/app-home.js +0 -86
- package/agent-pi/dist/agent-preset-projection.js +0 -29
- /package/{agent-hub → superd-hub}/dist/envelope.d.ts +0 -0
- /package/{agent-hub → superd-hub}/dist/envelope.js +0 -0
- /package/{agent-hub → superd-hub}/dist/ownership.d.ts +0 -0
- /package/{agent-hub → superd-hub}/dist/ownership.js +0 -0
- /package/{agent-hub → superd-hub}/dist/roster.js +0 -0
- /package/{agent-hub → superd-hub}/dist/rpc.d.ts +0 -0
- /package/{agent-hub → superd-hub}/dist/targets.d.ts +0 -0
- /package/{agent-hub → superd-hub}/dist/targets.js +0 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-world Remote stream mux server (AW-B DL6).
|
|
3
|
+
*
|
|
4
|
+
* Upstream's `RemoteStreamMuxServer` owns `/api/remote.mux` for the native
|
|
5
|
+
* gateway and is NOT reachable from outside its package (root exports omit it;
|
|
6
|
+
* `./src/*` cannot be imported at runtime). So this is our own wire-compatible
|
|
7
|
+
* implementation of the same protocol (dsh 0.1.7 `stream-protocol.ts`), bound
|
|
8
|
+
* to the ADDRESSED world's `openWireStream`:
|
|
9
|
+
*
|
|
10
|
+
* browser → server { type:'open', streamId, endpoint, payload }
|
|
11
|
+
* { type:'item', streamId, value? } (uplink)
|
|
12
|
+
* { type:'end', streamId } (uplink half-close)
|
|
13
|
+
* { type:'cancel', streamId }
|
|
14
|
+
* server → browser { type:'item', streamId, value }
|
|
15
|
+
* { type:'end', streamId }
|
|
16
|
+
* { type:'error', streamId, error:{code,message,details} }
|
|
17
|
+
*
|
|
18
|
+
* Dispatch semantics mirror upstream `RemoteStreamMuxConnection`: `item`,
|
|
19
|
+
* `end`, and `cancel` for a stream this connection no longer owns are dropped;
|
|
20
|
+
* a duplicate `open` is the one violation that closes the socket. Uplink items
|
|
21
|
+
* wait in a bounded inbox created before the opener resolves, so items sent
|
|
22
|
+
* right after `open` are never lost. Heartbeat: ping every interval, terminate
|
|
23
|
+
* after two missed pongs.
|
|
24
|
+
*/
|
|
25
|
+
/** Specifier is widened on purpose: `ws` is a runtime peer, not a typed dep. */
|
|
26
|
+
const WS_MODULE = 'ws';
|
|
27
|
+
const MAX_MISSED_HEARTBEATS = 2;
|
|
28
|
+
const DEFAULT_HEARTBEAT_MS = 2_000;
|
|
29
|
+
const DEFAULT_STREAM_INBOX_BYTES = 262_144;
|
|
30
|
+
const UPLINK_DONE = { value: undefined, done: true };
|
|
31
|
+
function isRecord(value) {
|
|
32
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
function exactKeys(value, keys) {
|
|
35
|
+
const own = Object.keys(value);
|
|
36
|
+
return own.length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
37
|
+
}
|
|
38
|
+
function validId(value) {
|
|
39
|
+
return typeof value === 'string' && value.length > 0;
|
|
40
|
+
}
|
|
41
|
+
/** An abort reason carrying wire failure fields (upstream `remoteErrorOf`). */
|
|
42
|
+
function isRemoteFailure(reason) {
|
|
43
|
+
return isRecord(reason) && typeof reason['code'] === 'string';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse one browser text frame.
|
|
47
|
+
* @param text - complete WebSocket text message.
|
|
48
|
+
* @returns the validated request.
|
|
49
|
+
* @throws {Error} on a malformed frame.
|
|
50
|
+
*/
|
|
51
|
+
export function parseClientMessage(text) {
|
|
52
|
+
let raw;
|
|
53
|
+
try {
|
|
54
|
+
raw = JSON.parse(text);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new Error('world-mux: invalid client message');
|
|
58
|
+
}
|
|
59
|
+
if (!isRecord(raw))
|
|
60
|
+
throw new Error('world-mux: invalid client message');
|
|
61
|
+
if ((raw['type'] === 'cancel' || raw['type'] === 'end')
|
|
62
|
+
&& exactKeys(raw, ['type', 'streamId']) && validId(raw['streamId'])) {
|
|
63
|
+
return { type: raw['type'], streamId: raw['streamId'] };
|
|
64
|
+
}
|
|
65
|
+
if (raw['type'] === 'item'
|
|
66
|
+
&& (exactKeys(raw, ['type', 'streamId']) || exactKeys(raw, ['type', 'streamId', 'value']))
|
|
67
|
+
&& validId(raw['streamId'])) {
|
|
68
|
+
return Object.hasOwn(raw, 'value')
|
|
69
|
+
? { type: 'item', streamId: raw['streamId'], value: raw['value'] }
|
|
70
|
+
: { type: 'item', streamId: raw['streamId'] };
|
|
71
|
+
}
|
|
72
|
+
if (raw['type'] === 'open'
|
|
73
|
+
&& exactKeys(raw, ['type', 'streamId', 'endpoint', 'payload'])
|
|
74
|
+
&& validId(raw['streamId'])
|
|
75
|
+
&& typeof raw['endpoint'] === 'string'
|
|
76
|
+
&& raw['endpoint'].length > 0) {
|
|
77
|
+
return { type: 'open', streamId: raw['streamId'], endpoint: raw['endpoint'], payload: raw['payload'] };
|
|
78
|
+
}
|
|
79
|
+
throw new Error('world-mux: invalid client message');
|
|
80
|
+
}
|
|
81
|
+
/** Default failure mapper: keep the error code when it looks like one. */
|
|
82
|
+
export function defaultFailure(error) {
|
|
83
|
+
if (isRecord(error)) {
|
|
84
|
+
const code = typeof error['code'] === 'string' ? error['code'] : undefined;
|
|
85
|
+
const message = typeof error['message'] === 'string' ? error['message'] : undefined;
|
|
86
|
+
if (code !== undefined) {
|
|
87
|
+
const details = isRecord(error['details']) ? error['details'] : {};
|
|
88
|
+
return { code, message: message ?? code, details };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { code: 'gateway/stream-error', message: error instanceof Error ? error.message : String(error), details: {} };
|
|
92
|
+
}
|
|
93
|
+
/** One uplink violation: an abort reason that must reach the client as a failure frame. */
|
|
94
|
+
function uplinkViolation(code, message, endpoint) {
|
|
95
|
+
return Object.assign(new Error(message), { code, details: { endpoint } });
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Bounded single-consumer uplink queue of one logical stream, the source the
|
|
99
|
+
* world method reads as its uplink iterable. Mirrors upstream `UplinkInbox`:
|
|
100
|
+
* overflow, and an item after the client's `end`, fail the queue and abort the
|
|
101
|
+
* logical stream with a wire failure.
|
|
102
|
+
*/
|
|
103
|
+
class UplinkInbox {
|
|
104
|
+
maxBytes;
|
|
105
|
+
endpoint;
|
|
106
|
+
onViolation;
|
|
107
|
+
queue = [];
|
|
108
|
+
bytes = 0;
|
|
109
|
+
ended = false;
|
|
110
|
+
closed = false;
|
|
111
|
+
taken = false;
|
|
112
|
+
failure;
|
|
113
|
+
wake;
|
|
114
|
+
constructor(maxBytes, endpoint, onViolation) {
|
|
115
|
+
this.maxBytes = maxBytes;
|
|
116
|
+
this.endpoint = endpoint;
|
|
117
|
+
this.onViolation = onViolation;
|
|
118
|
+
}
|
|
119
|
+
push(value, frameBytes) {
|
|
120
|
+
if (this.failure !== undefined || this.closed)
|
|
121
|
+
return;
|
|
122
|
+
if (this.ended) {
|
|
123
|
+
this.violate(uplinkViolation('gateway/protocol', 'world-mux: Remote stream uplink item after end', this.endpoint));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (this.bytes + frameBytes > this.maxBytes) {
|
|
127
|
+
this.violate(uplinkViolation('gateway/uplink-overflow', `world-mux: Remote stream uplink exceeded ${String(this.maxBytes)} buffered bytes`, this.endpoint));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
this.queue.push({ value, bytes: frameBytes });
|
|
131
|
+
this.bytes += frameBytes;
|
|
132
|
+
this.signal();
|
|
133
|
+
}
|
|
134
|
+
/** Client half-close; idempotent. */
|
|
135
|
+
end() {
|
|
136
|
+
if (this.ended)
|
|
137
|
+
return;
|
|
138
|
+
this.ended = true;
|
|
139
|
+
this.signal();
|
|
140
|
+
}
|
|
141
|
+
/** End the consumer's next read with `error`; idempotent, drops buffered items. */
|
|
142
|
+
fail(error) {
|
|
143
|
+
if (this.failure !== undefined)
|
|
144
|
+
return;
|
|
145
|
+
this.failure = error;
|
|
146
|
+
this.queue.length = 0;
|
|
147
|
+
this.bytes = 0;
|
|
148
|
+
this.signal();
|
|
149
|
+
}
|
|
150
|
+
[Symbol.asyncIterator]() {
|
|
151
|
+
if (this.taken)
|
|
152
|
+
throw new Error('world-mux: Remote stream uplink inbox already has a consumer');
|
|
153
|
+
this.taken = true;
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
async next() {
|
|
157
|
+
while (true) {
|
|
158
|
+
if (this.closed)
|
|
159
|
+
return UPLINK_DONE;
|
|
160
|
+
const entry = this.queue.shift();
|
|
161
|
+
if (entry !== undefined) {
|
|
162
|
+
this.bytes -= entry.bytes;
|
|
163
|
+
return { value: entry.value, done: false };
|
|
164
|
+
}
|
|
165
|
+
if (this.failure !== undefined)
|
|
166
|
+
throw this.failure;
|
|
167
|
+
if (this.ended)
|
|
168
|
+
return UPLINK_DONE;
|
|
169
|
+
if (this.wake !== undefined)
|
|
170
|
+
throw new Error('world-mux: Remote stream uplink inbox has one pending read');
|
|
171
|
+
await new Promise((resolve) => { this.wake = resolve; });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** Consumer stopped reading: later items are dropped, a pending read ends. */
|
|
175
|
+
return() {
|
|
176
|
+
this.closed = true;
|
|
177
|
+
this.queue.length = 0;
|
|
178
|
+
this.bytes = 0;
|
|
179
|
+
this.signal();
|
|
180
|
+
return Promise.resolve(UPLINK_DONE);
|
|
181
|
+
}
|
|
182
|
+
violate(error) {
|
|
183
|
+
this.fail(error);
|
|
184
|
+
this.onViolation(error);
|
|
185
|
+
}
|
|
186
|
+
signal() {
|
|
187
|
+
const wake = this.wake;
|
|
188
|
+
this.wake = undefined;
|
|
189
|
+
wake?.();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** One world's mux endpoint. */
|
|
193
|
+
export class WorldMuxServer {
|
|
194
|
+
open;
|
|
195
|
+
options;
|
|
196
|
+
server;
|
|
197
|
+
ws;
|
|
198
|
+
heartbeat;
|
|
199
|
+
connections = new Set();
|
|
200
|
+
missed = new WeakMap();
|
|
201
|
+
closed = false;
|
|
202
|
+
constructor(open, options = {}) {
|
|
203
|
+
this.open = open;
|
|
204
|
+
this.options = options;
|
|
205
|
+
}
|
|
206
|
+
/** Take over one accepted upgrade request. */
|
|
207
|
+
async handleUpgrade(req, socket, head) {
|
|
208
|
+
const { server, ws } = await this.load();
|
|
209
|
+
server.handleUpgrade(req, socket, head, (client) => {
|
|
210
|
+
this.missed.set(client, 0);
|
|
211
|
+
client.on('pong', () => { this.missed.set(client, 0); });
|
|
212
|
+
this.startHeartbeat();
|
|
213
|
+
const done = this.runConnection(client, ws);
|
|
214
|
+
this.connections.add(done);
|
|
215
|
+
void done.then(() => { this.connections.delete(done); });
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/** Terminate every socket and wait for the active streams to unwind. */
|
|
219
|
+
async close() {
|
|
220
|
+
this.closed = true;
|
|
221
|
+
clearInterval(this.heartbeat);
|
|
222
|
+
this.heartbeat = undefined;
|
|
223
|
+
const server = this.server;
|
|
224
|
+
if (server !== undefined) {
|
|
225
|
+
for (const client of server.clients)
|
|
226
|
+
client.terminate();
|
|
227
|
+
await new Promise((resolve) => { server.close(() => resolve()); });
|
|
228
|
+
}
|
|
229
|
+
await Promise.all([...this.connections]);
|
|
230
|
+
}
|
|
231
|
+
async load() {
|
|
232
|
+
if (this.server !== undefined && this.ws !== undefined)
|
|
233
|
+
return { server: this.server, ws: this.ws };
|
|
234
|
+
const mod = (await import(WS_MODULE));
|
|
235
|
+
const ws = typeof mod.WebSocketServer === 'function' ? mod : mod.default;
|
|
236
|
+
const server = new ws.WebSocketServer({ noServer: true });
|
|
237
|
+
this.server = server;
|
|
238
|
+
this.ws = ws;
|
|
239
|
+
return { server, ws };
|
|
240
|
+
}
|
|
241
|
+
async runConnection(client, ws) {
|
|
242
|
+
const streams = new Map();
|
|
243
|
+
let writes = Promise.resolve();
|
|
244
|
+
const send = (message) => {
|
|
245
|
+
let text;
|
|
246
|
+
try {
|
|
247
|
+
text = JSON.stringify(message);
|
|
248
|
+
}
|
|
249
|
+
catch (cause) {
|
|
250
|
+
return Promise.reject(new Error('world-mux: item is not JSON serializable', { cause }));
|
|
251
|
+
}
|
|
252
|
+
const delivery = writes.then(() => new Promise((resolve, reject) => {
|
|
253
|
+
if (client.readyState !== ws.WebSocket.OPEN) {
|
|
254
|
+
reject(new Error('world-mux: socket is closed'));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
client.send(text, (error) => { if (error)
|
|
258
|
+
reject(error);
|
|
259
|
+
else
|
|
260
|
+
resolve(); });
|
|
261
|
+
}));
|
|
262
|
+
writes = delivery.catch(() => undefined);
|
|
263
|
+
return delivery;
|
|
264
|
+
};
|
|
265
|
+
const failureOf = this.options.failure ?? defaultFailure;
|
|
266
|
+
const sendFailure = async (streamId, error) => {
|
|
267
|
+
if (client.readyState !== ws.WebSocket.OPEN)
|
|
268
|
+
return;
|
|
269
|
+
try {
|
|
270
|
+
await send({ type: 'error', streamId, error: failureOf(error) });
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
client.close(1011, 'Remote stream failure could not be delivered');
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
const pump = async (streamId, endpoint, payload, active) => {
|
|
277
|
+
let failed;
|
|
278
|
+
try {
|
|
279
|
+
const source = await this.open(endpoint, payload, active.inbox, active.abort);
|
|
280
|
+
for await (const value of source) {
|
|
281
|
+
await send({ type: 'item', streamId, value });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
failed = { error };
|
|
286
|
+
}
|
|
287
|
+
// The downlink has settled; later uplink frames cannot change the outcome.
|
|
288
|
+
active.inbox.fail(new Error('world-mux: Remote stream ended'));
|
|
289
|
+
if (active.abort.signal.aborted) {
|
|
290
|
+
// A wire failure as the abort reason is this mux failing the stream (a
|
|
291
|
+
// rejected or overflowing uplink item, or an item after end); the client
|
|
292
|
+
// is still waiting for that terminal frame. Any other abort is a
|
|
293
|
+
// cancellation and stays silent.
|
|
294
|
+
const reason = active.abort.signal.reason;
|
|
295
|
+
if (isRemoteFailure(reason))
|
|
296
|
+
await sendFailure(streamId, reason);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (failed !== undefined) {
|
|
300
|
+
await sendFailure(streamId, failed.error);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
await send({ type: 'end', streamId });
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
await sendFailure(streamId, error);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
await new Promise((resolve) => {
|
|
311
|
+
client.once('close', () => { resolve(); });
|
|
312
|
+
client.on('error', () => { client.terminate(); });
|
|
313
|
+
client.on('message', (data, isBinary) => {
|
|
314
|
+
if (isBinary === true) {
|
|
315
|
+
client.close(1003, 'text messages required');
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const text = String(data);
|
|
319
|
+
let message;
|
|
320
|
+
try {
|
|
321
|
+
message = parseClientMessage(text);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
client.close(1008, 'invalid Remote stream request');
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (message.type === 'open') {
|
|
328
|
+
if (streams.has(message.streamId)) {
|
|
329
|
+
client.close(1008, 'duplicate Remote stream id');
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const abort = new AbortController();
|
|
333
|
+
// Created before the opener resolves so items the client sends right
|
|
334
|
+
// after `open` wait in the inbox instead of being lost.
|
|
335
|
+
const inbox = new UplinkInbox(this.options.streamInboxBytes ?? DEFAULT_STREAM_INBOX_BYTES, message.endpoint, (error) => { abort.abort(error); });
|
|
336
|
+
const active = {
|
|
337
|
+
abort,
|
|
338
|
+
inbox,
|
|
339
|
+
stop: (reason) => {
|
|
340
|
+
abort.abort(reason);
|
|
341
|
+
inbox.fail(reason);
|
|
342
|
+
},
|
|
343
|
+
done: Promise.resolve(),
|
|
344
|
+
};
|
|
345
|
+
streams.set(message.streamId, active);
|
|
346
|
+
const done = pump(message.streamId, message.endpoint, message.payload, active);
|
|
347
|
+
active.done = done;
|
|
348
|
+
void done.then(() => streams.delete(message.streamId), () => streams.delete(message.streamId));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
// `item`, `end`, and `cancel` for a stream this connection no longer
|
|
352
|
+
// owns are dropped: a finished stream leaves the table while the
|
|
353
|
+
// client's in-flight frames are still arriving.
|
|
354
|
+
if (message.type === 'item') {
|
|
355
|
+
streams.get(message.streamId)?.inbox.push(message.value, Buffer.byteLength(text, 'utf8'));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (message.type === 'end') {
|
|
359
|
+
streams.get(message.streamId)?.inbox.end();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
streams.get(message.streamId)?.stop(new Error('world-mux: Remote stream cancelled'));
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
const active = [...streams.values()];
|
|
366
|
+
for (const stream of active)
|
|
367
|
+
stream.stop(new Error('world-mux: Remote stream socket closed'));
|
|
368
|
+
await Promise.all(active.map(stream => stream.done));
|
|
369
|
+
}
|
|
370
|
+
startHeartbeat() {
|
|
371
|
+
if (this.heartbeat !== undefined || this.closed)
|
|
372
|
+
return;
|
|
373
|
+
const interval = this.options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
|
|
374
|
+
this.heartbeat = setInterval(() => {
|
|
375
|
+
const server = this.server;
|
|
376
|
+
if (server === undefined)
|
|
377
|
+
return;
|
|
378
|
+
for (const client of server.clients) {
|
|
379
|
+
if (client.readyState !== (this.ws?.WebSocket.OPEN ?? 1))
|
|
380
|
+
continue;
|
|
381
|
+
const missed = this.missed.get(client) ?? 0;
|
|
382
|
+
if (missed >= MAX_MISSED_HEARTBEATS) {
|
|
383
|
+
client.terminate();
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
this.missed.set(client, missed + 1);
|
|
387
|
+
client.ping();
|
|
388
|
+
}
|
|
389
|
+
}, interval);
|
|
390
|
+
this.heartbeat.unref();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the running dsh installation's package.json — the anchor worlds
|
|
3
|
+
* resolve `@deepseek-ai` bundles and the fallback closure from.
|
|
4
|
+
*
|
|
5
|
+
* Precedence: `$SUPERD_DSH_ANCHOR` (the repo dev lines' contract — the
|
|
6
|
+
* launcher pins the checkout build), then a walk UP from the
|
|
7
|
+
* `@deepseek-ai/dsh-app-boot` this module resolved (the host always provides
|
|
8
|
+
* it) to the nearest ancestor package named `@deepseek-ai/dsh` — the
|
|
9
|
+
* packaged-install layout (`<install>/@deepseek-ai/dsh/node_modules/
|
|
10
|
+
* @deepseek-ai/dsh-app-boot`) lands on the install root in two hops. No
|
|
11
|
+
* machine-specific path ships in code.
|
|
12
|
+
* @throws when neither yields an installation (mis-launched composition).
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveInstallAnchor(host?: {
|
|
15
|
+
get(name: string): unknown;
|
|
16
|
+
}): string;
|
|
1
17
|
export interface ProvisionWorldOptions {
|
|
2
18
|
/** Runtime key — also the mount label and the `agents/<key>` home segment. */
|
|
3
19
|
key: string;
|
|
@@ -10,11 +26,11 @@ export interface ProvisionWorldOptions {
|
|
|
10
26
|
}
|
|
11
27
|
/**
|
|
12
28
|
* Provision one world's nested profile (idempotent: rewrites profile files,
|
|
13
|
-
* re-links packages — lstat-safe against dangling links). Returns
|
|
14
|
-
*
|
|
29
|
+
* re-links packages — lstat-safe against dangling links). Returns whether
|
|
30
|
+
* the layout landed.
|
|
15
31
|
*
|
|
16
|
-
* Never throws: any provisioning failure is logged and returns
|
|
32
|
+
* Never throws: any provisioning failure is logged and returns false so the
|
|
17
33
|
* calling world plugin degrades to a not-ready roster row instead of failing
|
|
18
34
|
* the loader entry — a failed apply would take down the whole host boot.
|
|
19
35
|
*/
|
|
20
|
-
export declare function provisionWorldProfile(opts: ProvisionWorldOptions):
|
|
36
|
+
export declare function provisionWorldProfile(opts: ProvisionWorldOptions): boolean;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nested-world profile provisioning — the publish story.
|
|
3
|
+
*
|
|
4
|
+
* The dev line (test/smoke.mjs) hand-provisions every world's nested profile
|
|
5
|
+
* (`<home>/agents/<label>/profiles/web`) and the `@pgmi-builds/*` links its
|
|
6
|
+
* resolution needs. A foreign install has NO line bootstrap:
|
|
7
|
+
*
|
|
8
|
+
* dsh plugin --profile <name> add super-dsh
|
|
9
|
+
*
|
|
10
|
+
* mounts the hub + adapter world plugins into the consumer's profile, and the
|
|
11
|
+
* spawn call sites (adapter world-plugins, hub join) must provision the same
|
|
12
|
+
* layout THEMSELVES or every world spawn fails on a missing profile.
|
|
13
|
+
*
|
|
14
|
+
* This module is the library form of that bootstrap, called by all six spawn
|
|
15
|
+
* sites. Layout invariants (super-dsh/AGENTS.md S7):
|
|
16
|
+
*
|
|
17
|
+
* - `<worldHome>/profiles/web/package.json` — bundles `[dsh-base, dsh-web-app,
|
|
18
|
+
* adapterPkg]`; the world tree is a full sibling root, nothing shared.
|
|
19
|
+
* - `<worldHome>/profiles/web/cordis.patch.yml` — per-world home pinning:
|
|
20
|
+
* every home-resolving DSH plugin reads THIS tree's nested root.
|
|
21
|
+
* - `<worldHome>/profiles/web/node_modules/@pgmi-builds/{adapter,hub}` —
|
|
22
|
+
* the plugin-add shape: with the host table widened (spawnWorld, spec
|
|
23
|
+
* §2.1), these profile-local links serve the local-physical leg for the
|
|
24
|
+
* world's own packages. No `@deepseek-ai` scope anywhere — those names
|
|
25
|
+
* ride the host's installation entries.
|
|
26
|
+
*
|
|
27
|
+
* Link identity rule: both links point INSIDE the running line tree — the hub
|
|
28
|
+
* at this module's own package root, the adapter at its sibling
|
|
29
|
+
* `agent-<key>` directory — identical in the dev repo layout and inside the
|
|
30
|
+
* published single `super-dsh` package, so ctx0 and every world root share
|
|
31
|
+
* one hub/adapter instance with zero registry involvement.
|
|
32
|
+
*
|
|
33
|
+
* Returns whether the layout was provisioned; false (logged) when the
|
|
34
|
+
* adapter sibling does not exist next to the hub — a foreign composition
|
|
35
|
+
* this module does not own.
|
|
36
|
+
*/
|
|
37
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
38
|
+
import { dirname, join } from 'node:path';
|
|
39
|
+
import { fileURLToPath } from 'node:url';
|
|
40
|
+
import { createRequire } from 'node:module';
|
|
41
|
+
const HUB_SCOPE = '@pgmi-builds';
|
|
42
|
+
/** The profile bundles every world tree composes besides its adapter. */
|
|
43
|
+
const WORLD_BASE_BUNDLES = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'];
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the running dsh installation's package.json — the anchor worlds
|
|
46
|
+
* resolve `@deepseek-ai` bundles and the fallback closure from.
|
|
47
|
+
*
|
|
48
|
+
* Precedence: `$SUPERD_DSH_ANCHOR` (the repo dev lines' contract — the
|
|
49
|
+
* launcher pins the checkout build), then a walk UP from the
|
|
50
|
+
* `@deepseek-ai/dsh-app-boot` this module resolved (the host always provides
|
|
51
|
+
* it) to the nearest ancestor package named `@deepseek-ai/dsh` — the
|
|
52
|
+
* packaged-install layout (`<install>/@deepseek-ai/dsh/node_modules/
|
|
53
|
+
* @deepseek-ai/dsh-app-boot`) lands on the install root in two hops. No
|
|
54
|
+
* machine-specific path ships in code.
|
|
55
|
+
* @throws when neither yields an installation (mis-launched composition).
|
|
56
|
+
*/
|
|
57
|
+
export function resolveInstallAnchor(host) {
|
|
58
|
+
// Spec §2.3: the host's launcher facts win — a world plugin runs in ctx0,
|
|
59
|
+
// where `profileContext` carries the exact anchor the live table was
|
|
60
|
+
// computed from. Env and the realpath walk remain for table-less harnesses.
|
|
61
|
+
const launcher = host?.get('profileContext');
|
|
62
|
+
if (launcher?.installAnchor !== undefined)
|
|
63
|
+
return launcher.installAnchor;
|
|
64
|
+
const fromEnv = process.env.SUPERD_DSH_ANCHOR;
|
|
65
|
+
if (fromEnv !== undefined && fromEnv !== '')
|
|
66
|
+
return fromEnv;
|
|
67
|
+
let dir = dirname(resolveHostPackageDir('@deepseek-ai/dsh-app-boot'));
|
|
68
|
+
while (true) {
|
|
69
|
+
const manifest = join(dir, 'package.json');
|
|
70
|
+
if (existsSync(manifest)) {
|
|
71
|
+
try {
|
|
72
|
+
const name = JSON.parse(readFileSync(manifest, 'utf8')).name;
|
|
73
|
+
if (name === '@deepseek-ai/dsh')
|
|
74
|
+
return manifest;
|
|
75
|
+
}
|
|
76
|
+
catch { /* malformed intermediate manifest — keep walking */ }
|
|
77
|
+
}
|
|
78
|
+
const parent = dirname(dir);
|
|
79
|
+
if (parent === dir) {
|
|
80
|
+
throw new Error('super-dsh: cannot derive the dsh installation anchor (set SUPERD_DSH_ANCHOR)');
|
|
81
|
+
}
|
|
82
|
+
dir = parent;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** First `node_modules` search-path candidate that exists for a specifier,
|
|
86
|
+
* resolved to its REALPATH — farm and pnpm links must not leak into the
|
|
87
|
+
* ancestor walk (the install tree only exists behind the symlink). */
|
|
88
|
+
function resolveHostPackageDir(spec) {
|
|
89
|
+
const require = createRequire(import.meta.url);
|
|
90
|
+
// Interception first: under the launcher's resolution table the profile
|
|
91
|
+
// tree holds NO physical @deepseek-ai, but require.resolve goes through
|
|
92
|
+
// the patched Module._resolveFilename and lands on the installation's
|
|
93
|
+
// copy directly. resolve.paths() cannot see the table — it only lists
|
|
94
|
+
// physical walk-up candidates (the retired profiles-level farm is what
|
|
95
|
+
// used to plug this gap on consumer machines; 0.1.3-r prod evidence).
|
|
96
|
+
try {
|
|
97
|
+
return realpathSync(dirname(require.resolve(`${spec}/package.json`)));
|
|
98
|
+
}
|
|
99
|
+
catch { /* not intercepted or ./package.json not exported — walk */ }
|
|
100
|
+
for (const searchPath of require.resolve.paths(`${spec}/x`) ?? []) {
|
|
101
|
+
const candidate = join(searchPath, spec);
|
|
102
|
+
if (existsSync(join(candidate, 'package.json')))
|
|
103
|
+
return realpathSync(candidate);
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`super-dsh: cannot resolve ${spec} from the running host (set SUPERD_DSH_ANCHOR)`);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Provision one world's nested profile (idempotent: rewrites profile files,
|
|
109
|
+
* re-links packages — lstat-safe against dangling links). Returns whether
|
|
110
|
+
* the layout landed.
|
|
111
|
+
*
|
|
112
|
+
* Never throws: any provisioning failure is logged and returns false so the
|
|
113
|
+
* calling world plugin degrades to a not-ready roster row instead of failing
|
|
114
|
+
* the loader entry — a failed apply would take down the whole host boot.
|
|
115
|
+
*/
|
|
116
|
+
export function provisionWorldProfile(opts) {
|
|
117
|
+
try {
|
|
118
|
+
return provisionWorldProfileUnchecked(opts);
|
|
119
|
+
}
|
|
120
|
+
catch (cause) {
|
|
121
|
+
console.error(`[super-dsh] provisioning world '${opts.key}' failed:`, cause);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function provisionWorldProfileUnchecked(opts) {
|
|
126
|
+
// Single-pack topology: this module lives at `<line>/superd-hub/dist/`, and
|
|
127
|
+
// every adapter is a SIBLING directory (`<line>/agent-<key>`) — the same
|
|
128
|
+
// layout in the repo (dev link: lines) and inside the published `super-dsh`
|
|
129
|
+
// package. The adapter link therefore points at the sibling; no registry
|
|
130
|
+
// resolution is involved anywhere.
|
|
131
|
+
const hubRoot = dirname(dirname(fileURLToPath(import.meta.url))); // <line>/superd-hub
|
|
132
|
+
const lineRoot = dirname(hubRoot); // monolith root / repo super-dsh/
|
|
133
|
+
const adapterDir = join(lineRoot, `agent-${opts.key}`);
|
|
134
|
+
if (!existsSync(join(adapterDir, 'package.json')))
|
|
135
|
+
return false;
|
|
136
|
+
const profileName = opts.profileName ?? 'web';
|
|
137
|
+
const profDir = join(opts.worldHome, 'profiles', profileName);
|
|
138
|
+
mkdirSync(profDir, { recursive: true });
|
|
139
|
+
writeFileSync(join(profDir, 'package.json'), JSON.stringify({
|
|
140
|
+
name: `superd-${opts.key}-world-profile`,
|
|
141
|
+
private: true,
|
|
142
|
+
dsh: { profile: { bundles: [...WORLD_BASE_BUNDLES, opts.adapterPkg] } },
|
|
143
|
+
}, null, 2) + '\n');
|
|
144
|
+
// Per-world home pinning (smoke.mjs parity): settings, credentials,
|
|
145
|
+
// attachments, shell env and skills all land inside `agents/<key>/`.
|
|
146
|
+
// (`agent-instructions` keeps its base `maxBytes` home — instruction
|
|
147
|
+
// discovery root, not agent data.)
|
|
148
|
+
//
|
|
149
|
+
// 0.1.7: the profile hmr row arms itself wherever a profileContext exists,
|
|
150
|
+
// and its config reload recomposes the tree from THE PROFILE LAYERS ALONE —
|
|
151
|
+
// spawn-time extraPatches do not survive it. The world posture is therefore
|
|
152
|
+
// PERSISTED in this layer (webserver disabled + ephemeral port, hmr itself
|
|
153
|
+
// off, and — T-D — the webui rows `web-runtime`/`client-hmr` off: a world's
|
|
154
|
+
// composition is a spawn-time fact and must not be re-derived), exactly
|
|
155
|
+
// mirroring worldMountPatches.
|
|
156
|
+
writeFileSync(join(profDir, 'cordis.patch.yml'), [
|
|
157
|
+
'# Per-world home pinning (dshHomePath is this tree\'s nested home).',
|
|
158
|
+
...['settings', 'credentials', 'attachment-local', 'shell-env', 'skill-filesystem'].flatMap((id) => [`- id: ${id}`, ' config:', ' dshHome: !!js dshHomePath()']),
|
|
159
|
+
'# Zero-listener posture (0.1.7): persisted so hmr/launcher recomposition',
|
|
160
|
+
'# cannot re-enable the world\'s own listener (default port 3080).',
|
|
161
|
+
'- id: webserver',
|
|
162
|
+
' disabled: true',
|
|
163
|
+
' config:',
|
|
164
|
+
' host: 127.0.0.1',
|
|
165
|
+
' port: 0',
|
|
166
|
+
'- id: hmr',
|
|
167
|
+
' disabled: true',
|
|
168
|
+
'# Webui-less posture (T-D): the world page rows stay off across any',
|
|
169
|
+
'# recomposition — worlds are pure data planes, `/hub` is the sole entry.',
|
|
170
|
+
'- id: web-runtime',
|
|
171
|
+
' disabled: true',
|
|
172
|
+
'- id: client-hmr',
|
|
173
|
+
' disabled: true',
|
|
174
|
+
'',
|
|
175
|
+
].join('\n'));
|
|
176
|
+
// @pgmi-builds links, plugin-add shape: adapter + hub into the world
|
|
177
|
+
// profile's OWN node_modules (resolveBundleDir consults the profile
|
|
178
|
+
// directory; the local-physical leg of the widened host table consumes
|
|
179
|
+
// the same links for row imports — spec §2.1). Dangling links first
|
|
180
|
+
// (lstat: existsSync is false for them, which skipped rmSync and crashed
|
|
181
|
+
// symlinkSync with EEXIST after repo moves).
|
|
182
|
+
const links = [
|
|
183
|
+
[opts.adapterPkg.slice(HUB_SCOPE.length + 1), adapterDir],
|
|
184
|
+
['superd-hub', hubRoot],
|
|
185
|
+
];
|
|
186
|
+
const linkRoot = join(profDir, 'node_modules');
|
|
187
|
+
mkdirSync(join(linkRoot, HUB_SCOPE), { recursive: true });
|
|
188
|
+
for (const [name, target] of links) {
|
|
189
|
+
const dst = join(linkRoot, HUB_SCOPE, name);
|
|
190
|
+
const st = lstatSync(dst, { throwIfNoEntry: false });
|
|
191
|
+
if (st)
|
|
192
|
+
rmSync(dst, { recursive: true, force: true });
|
|
193
|
+
symlinkSync(target, dst);
|
|
194
|
+
}
|
|
195
|
+
// Retired machinery (spec §3): scrub the profiles-level farm this function
|
|
196
|
+
// wrote before 2026-09-25. Stale links there sit inside the widened
|
|
197
|
+
// local-physical walk and would shadow the host table's installation
|
|
198
|
+
// entries with frozen-generation targets.
|
|
199
|
+
const staleFarm = join(opts.worldHome, 'profiles', 'node_modules');
|
|
200
|
+
rmSync(join(staleFarm, '@deepseek-ai'), { recursive: true, force: true });
|
|
201
|
+
rmSync(join(staleFarm, HUB_SCOPE), { recursive: true, force: true });
|
|
202
|
+
return true;
|
|
203
|
+
}
|