arcane-os 0.69.0 → 0.71.0

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 (37) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/browser-decisions.mjs +46 -2
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +58 -62
  5. package/browser-runtime/ai/browser-speech-providers.mjs +3 -0
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +76 -164
  7. package/browser-runtime/ai/dbopfs-model-parts.mjs +344 -0
  8. package/browser-runtime/ai/dbopfs-model-resources.mjs +190 -0
  9. package/browser-runtime/ai/decision-runtime.mjs +17 -1
  10. package/browser-runtime/ai/decision-worker.mjs +21 -1
  11. package/browser-runtime/ai/model-resource-bridge.mjs +231 -0
  12. package/browser-runtime/ai/speech-worker-client.mjs +68 -28
  13. package/browser-runtime/ai/speech-worker-runtime.mjs +212 -97
  14. package/browser-runtime/core/contracts.mjs +1 -1
  15. package/docs/reference/README.md +2 -0
  16. package/docs/reference/ai/browser-decisions.md +57 -3
  17. package/docs/reference/ai/browser-speech.md +45 -17
  18. package/docs/reference/ai/browser-wasm.md +28 -20
  19. package/docs/reference/core-native-packaging.md +13 -0
  20. package/docs/reference/core-runtime.md +9 -0
  21. package/docs/reference/core-shared-host.md +190 -0
  22. package/docs/reference/inventory/package-api.json +3 -3
  23. package/docs/reference/inventory/runtime-components.json +2 -2
  24. package/docs/reference/inventory/runtime-modules.json +6 -6
  25. package/docs/reference/runtime-components.md +35 -1
  26. package/docs/reference/runtime-modules.md +97 -7
  27. package/docs/reference/sdk-api.md +69 -31
  28. package/package.json +2 -2
  29. package/runtime/arcane/components/theme-switcher.html +4 -4
  30. package/runtime/arcane/modules/AI.js +2 -18
  31. package/runtime/arcane/modules/AIProviderRuntime.js +77 -3
  32. package/runtime/arcane/modules/DBOPFS.js +180 -26
  33. package/src/core/host.mjs +2 -0
  34. package/src/core/runtime.mjs +9 -4
  35. package/src/core/services/packaged-web.mjs +8 -1
  36. package/src/core/shared-host.mjs +500 -0
  37. package/src/native/portable-layout.mjs +66 -34
@@ -0,0 +1,500 @@
1
+ import {spawn} from 'node:child_process';
2
+ import {open} from 'node:fs/promises';
3
+ import {createConnection, createServer} from 'node:net';
4
+ import {randomUUID} from 'node:crypto';
5
+ import {createCoreClient} from '../../browser-runtime/core/client.mjs';
6
+ import {CORE_PROTOCOL, CoreError, serializeCoreError} from '../../browser-runtime/core/contracts.mjs';
7
+ import {createCoreRuntime} from './runtime.mjs';
8
+ import {createCoreFrameDecoder, encodeCoreFrame} from './stdio.mjs';
9
+
10
+ function failure(code, message) { return new CoreError({code, message}); }
11
+ function reportError(error) { console.error('Shared Arcane Core failed:', error); }
12
+ function report(observer, error) {
13
+ try {
14
+ Promise.resolve(observer(error)).catch(function observerFailed(cause) { console.error(cause, error); });
15
+ } catch (cause) { console.error(cause, error); }
16
+ }
17
+ function event(event, data) {
18
+ return {protocol: CORE_PROTOCOL, type: 'event', event, data, time: new Date().toISOString()};
19
+ }
20
+ function response(id, result, error) {
21
+ return {protocol: CORE_PROTOCOL, type: 'response', id,
22
+ ...(error ? {ok: false, error: serializeCoreError(error)} : {ok: true, result}),
23
+ time: new Date().toISOString()};
24
+ }
25
+
26
+ // A stream write owns terminal events as well as its callback. Some destroyed
27
+ // streams never deliver that callback; retirement must still settle the write.
28
+ function write(output, content) {
29
+ return new Promise(function writeFrame(resolve, reject) {
30
+ let settled = false;
31
+ function finish(error) {
32
+ if (settled) return;
33
+ settled = true;
34
+ output.off('error', finish);
35
+ output.off('close', closed);
36
+ output.off('finish', closed);
37
+ if (error) reject(error);
38
+ else resolve();
39
+ }
40
+ function closed() { finish(failure('CORE_CONNECTION_CLOSED', 'The Core output closed during a write.')); }
41
+ output.once('error', finish);
42
+ output.once('close', closed);
43
+ output.once('finish', closed);
44
+ if (output.destroyed || output.writableEnded) { closed(); return; }
45
+ try { output.write(content, finish); } catch (error) { finish(error); }
46
+ });
47
+ }
48
+
49
+ /** Claim one app-selected pipe/socket before importing or creating services. */
50
+ export async function startSharedCoreHost({
51
+ endpoint, application, version, configure, getReplayEvents, signal, onError = reportError
52
+ } = {}) {
53
+ signal?.throwIfAborted();
54
+ if (typeof endpoint !== 'string' || !endpoint) throw new TypeError('A shared Core endpoint must be a nonempty local pipe/socket path.');
55
+ const peers = new Set();
56
+ const requests = new Map();
57
+ const incoming = new Set();
58
+ const lifetime = new AbortController();
59
+ const prefix = `shared-${randomUUID()}:`;
60
+ let sequence = 0;
61
+ let runtime;
62
+ let unsubscribe;
63
+ let closing;
64
+ let preparation = Promise.resolve();
65
+ let ready = false;
66
+ let terminalError;
67
+ let resolveClosed;
68
+ let rejectClosed;
69
+ const closed = new Promise(function ownHost(resolve, reject) { resolveClosed = resolve; rejectClosed = reject; });
70
+ closed.catch(function observeHostFailure() {});
71
+ function diagnostic(error) {
72
+ report(onError, error);
73
+ }
74
+ function observe(task) {
75
+ incoming.add(task);
76
+ task.then(function completed() { incoming.delete(task); }, function failed(error) {
77
+ incoming.delete(task);
78
+ diagnostic(error);
79
+ });
80
+ return task;
81
+ }
82
+ function send(peer, frame) {
83
+ if (peer.closed) return;
84
+ let content;
85
+ try { content = encodeCoreFrame(frame); }
86
+ catch (error) { diagnostic(error); peer.socket.destroy(); return; }
87
+ peer.writes = peer.writes.then(function writeNext() {
88
+ if (!peer.closed) return write(peer.socket, content);
89
+ }).catch(function outputFailed(error) { diagnostic(error); peer.socket.destroy(); });
90
+ }
91
+ function cancel(record) {
92
+ observe(runtime.handle({protocol: CORE_PROTOCOL, type: 'control',
93
+ control: 'request.cancel', requestId: record.internalId}));
94
+ }
95
+ function disconnect(peer) {
96
+ if (peer.closed) return;
97
+ peer.closed = true;
98
+ peers.delete(peer);
99
+ for (const record of peer.requests.values()) cancel(record);
100
+ }
101
+ function stateFor(peer, state) {
102
+ return {...state, activeRequests: state.activeRequests.map(function requestState(record) {
103
+ const owned = requests.get(record.id);
104
+ return owned?.peer === peer ? {...record, id: owned.id} : record;
105
+ })};
106
+ }
107
+ function replay(peer) {
108
+ const state = runtime.current();
109
+ if (state.state === 'ready') send(peer, event('core.ready', {version: state.version, app: state.application}));
110
+ send(peer, event('core.state', stateFor(peer, state)));
111
+ for (const service of state.services) send(peer, event('core.service.state', service));
112
+ // Domain snapshots belong to the app/service owner, never event history.
113
+ for (const snapshot of getReplayEvents?.() ?? []) send(peer, event(snapshot.event, snapshot.data));
114
+ }
115
+ function route(frame) {
116
+ if (frame.type === 'response') {
117
+ const record = requests.get(frame.id);
118
+ if (record) send(record.peer, {...frame, id: record.id});
119
+ return;
120
+ }
121
+ if (frame.type !== 'event') return;
122
+ if (Object.hasOwn(frame, 'requestId')) {
123
+ const owned = requests.get(frame.requestId);
124
+ if (owned) send(owned.peer, {...frame, requestId: owned.id,
125
+ data: frame.data?.requestId === owned.internalId ? {...frame.data, requestId: owned.id} : frame.data});
126
+ return;
127
+ }
128
+ const id = frame.data?.requestId;
129
+ const record = requests.get(id);
130
+ if (record) {
131
+ send(record.peer, {...frame, data: {...frame.data, requestId: record.id}});
132
+ return;
133
+ }
134
+ // A retired transport correlation is not a new app-wide event.
135
+ if (typeof id === 'string' && id.startsWith(prefix)) return;
136
+ for (const peer of peers) send(peer, frame.event === 'core.state'
137
+ ? {...frame, data: stateFor(peer, frame.data)} : frame);
138
+ }
139
+ async function dispatch(peer, frame) {
140
+ if (frame?.protocol !== CORE_PROTOCOL) throw failure('INVALID_RPC_REQUEST', 'Unknown Core protocol.');
141
+ if (frame.type === 'control') {
142
+ if (frame.control === 'runtime.replay') { replay(peer); return; }
143
+ if (frame.control === 'request.cancel') {
144
+ const record = peer.requests.get(frame.requestId);
145
+ if (record) cancel(record);
146
+ return;
147
+ }
148
+ if (frame.control === 'requests.cancelAll') {
149
+ for (const record of peer.requests.values()) cancel(record);
150
+ return;
151
+ }
152
+ throw failure('INVALID_RPC_CONTROL', 'Unknown Core control.');
153
+ }
154
+ if (frame.type !== 'request' || typeof frame.id !== 'string' || !frame.id || typeof frame.method !== 'string') {
155
+ throw failure('INVALID_RPC_REQUEST', 'A Core request requires an id and method.');
156
+ }
157
+ if (peer.requests.has(frame.id)) throw failure('RPC_REQUEST_ID_ACTIVE', 'The request id is already active on this connection.');
158
+ if (frame.method === 'core.host.shutdown') {
159
+ peer.shutdownIds.push(frame.id);
160
+ close();
161
+ return;
162
+ }
163
+ if (closing) throw failure('CORE_CLOSING', 'Core is closing.');
164
+ const record = {peer, id: frame.id, internalId: `${prefix}${++sequence}`};
165
+ peer.requests.set(record.id, record);
166
+ requests.set(record.internalId, record);
167
+ try {
168
+ // Only protocol correlation changes; parameters remain untouched.
169
+ await runtime.handle({...frame, id: record.internalId});
170
+ } finally {
171
+ peer.requests.delete(record.id);
172
+ requests.delete(record.internalId);
173
+ }
174
+ }
175
+ function receive(peer, frame) {
176
+ if (peer.closed) return;
177
+ observe(dispatch(peer, frame).catch(function dispatchFailed(error) {
178
+ if (frame?.type === 'request') send(peer, response(frame.id, undefined, error));
179
+ else send(peer, event('core.error', serializeCoreError(error)));
180
+ }));
181
+ }
182
+ function accept(socket) {
183
+ const peer = {socket, requests: new Map(), shutdownIds: [], closed: false, writes: Promise.resolve()};
184
+ peers.add(peer);
185
+ const decoder = createCoreFrameDecoder(function receiveFrame(frame) { receive(peer, frame); });
186
+ socket.on('data', function data(chunk) {
187
+ try { decoder.push(chunk); } catch (error) { diagnostic(error); socket.destroy(); }
188
+ });
189
+ socket.on('error', diagnostic);
190
+ socket.on('end', function ended() {
191
+ try { decoder.finish(); } catch (error) { diagnostic(error); }
192
+ disconnect(peer);
193
+ });
194
+ socket.on('close', function connectionClosed() { disconnect(peer); });
195
+ if (ready) socket.resume();
196
+ }
197
+ const server = createServer({pauseOnConnect: true}, accept);
198
+ function close() {
199
+ if (closing) return closing;
200
+ closing = Promise.resolve().then(async function drainHost() {
201
+ try { await preparation; } catch {}
202
+ let error = terminalError;
203
+ try { await runtime?.close(); } catch (cause) {
204
+ error = error ? new AggregateError([error, cause], 'Core host shutdown failed.') : cause;
205
+ }
206
+ await Promise.allSettled([...incoming]);
207
+ // Keep the endpoint claim through service drain/disposal. Releasing
208
+ // a Unix socket sooner could start a second owner during a save.
209
+ const stopped = new Promise(function stopListening(resolve, reject) {
210
+ server.close(function listenerClosed(error) {
211
+ if (error && error.code !== 'ERR_SERVER_NOT_RUNNING') reject(error);
212
+ else resolve();
213
+ });
214
+ });
215
+ stopped.catch(function observeListenerFailure() {});
216
+ for (const peer of peers) {
217
+ for (const id of peer.shutdownIds) send(peer, response(id, {state: 'closed'}, error));
218
+ }
219
+ await Promise.all([...peers].map(function drainWrites(peer) { return peer.writes; }));
220
+ for (const peer of peers) { peer.socket.resume(); peer.socket.end(); }
221
+ await stopped;
222
+ unsubscribe?.();
223
+ signal?.removeEventListener('abort', abortHost);
224
+ if (error) throw error;
225
+ return runtime?.current() ?? {state: 'closed'};
226
+ });
227
+ closing.then(resolveClosed, function failed(error) { diagnostic(error); rejectClosed(error); });
228
+ lifetime.abort(failure('CORE_CLOSING', 'The shared Core host is closing.'));
229
+ return closing;
230
+ }
231
+ function abortHost() { close(); }
232
+ await new Promise(function claimEndpoint(resolve, reject) {
233
+ function failed(error) {
234
+ server.off('listening', listening);
235
+ error.coreHostPhase = 'listen';
236
+ reject(error);
237
+ }
238
+ function listening() { server.off('error', failed); resolve(); }
239
+ server.once('error', failed);
240
+ server.once('listening', listening);
241
+ server.listen({path: endpoint});
242
+ });
243
+ server.on('error', function listenerFailed(error) { terminalError = error; diagnostic(error); close(); });
244
+ try {
245
+ // The endpoint is already owned. A losing launcher never calls configure.
246
+ signal?.throwIfAborted();
247
+ runtime = createCoreRuntime({application, version});
248
+ unsubscribe = runtime.onFrame(route);
249
+ signal?.addEventListener('abort', abortHost, {once: true});
250
+ preparation = Promise.resolve().then(function configureClaimedHost() {
251
+ lifetime.signal.throwIfAborted();
252
+ return configure?.(runtime, {signal: lifetime.signal});
253
+ });
254
+ if (signal?.aborted) close();
255
+ await preparation;
256
+ signal?.throwIfAborted();
257
+ lifetime.signal.throwIfAborted();
258
+ runtime.start();
259
+ ready = true;
260
+ for (const peer of peers) peer.socket.resume();
261
+ if (signal?.aborted) close();
262
+ return {runtime, endpoint, closed, close};
263
+ } catch (error) {
264
+ for (const peer of peers) send(peer, event('core.error', serializeCoreError(error)));
265
+ try { await close(); } catch (closeError) { throw new AggregateError([error, closeError], 'Core startup and cleanup failed.'); }
266
+ throw error;
267
+ }
268
+ }
269
+
270
+ function connectSocket(endpoint, signal) {
271
+ signal?.throwIfAborted();
272
+ if (typeof endpoint !== 'string' || !endpoint) throw new TypeError('A shared Core endpoint must be a nonempty local pipe/socket path.');
273
+ return new Promise(function connect(resolve, reject) {
274
+ const socket = createConnection({path: endpoint});
275
+ socket.pause();
276
+ function cleanup() {
277
+ signal?.removeEventListener('abort', abort);
278
+ socket.off('connect', connected);
279
+ socket.off('error', failed);
280
+ }
281
+ function failed(error) {
282
+ cleanup();
283
+ socket.on('error', reportError);
284
+ socket.destroy();
285
+ reject(error);
286
+ }
287
+ function abort() { failed(signal.reason); }
288
+ function connected() { cleanup(); resolve(socket); }
289
+ socket.once('connect', connected);
290
+ socket.once('error', failed);
291
+ signal?.addEventListener('abort', abort, {once: true});
292
+ if (signal?.aborted) abort();
293
+ });
294
+ }
295
+
296
+ // The child retains its own listener and complete diagnostic file after the
297
+ // launching client leaves. Aborting discovery never kills a shared service.
298
+ async function launch({command, args = [], cwd, env, logFile}, signal) {
299
+ signal?.throwIfAborted();
300
+ if (typeof logFile !== 'string' || !logFile) throw new TypeError('Headless Core launch requires a diagnostic logFile.');
301
+ const log = await open(logFile, 'a');
302
+ let child;
303
+ try {
304
+ signal?.throwIfAborted();
305
+ child = spawn(command, args, {cwd, env, detached: true, windowsHide: true, stdio: ['ignore', log.fd, log.fd, 'ipc']});
306
+ await new Promise(function awaitOwner(resolve, reject) {
307
+ let settled = false;
308
+ function finish(error) {
309
+ if (settled) return;
310
+ settled = true;
311
+ child.off('message', message);
312
+ child.off('error', failed);
313
+ child.off('exit', exited);
314
+ signal?.removeEventListener('abort', aborted);
315
+ // Retain an error observer after relinquishing the child: a
316
+ // startup abort can precede Node's asynchronous spawn error.
317
+ child.on('error', reportError);
318
+ if (child.connected) child.disconnect();
319
+ child.unref();
320
+ if (error) reject(error);
321
+ else resolve();
322
+ }
323
+ function message(value) {
324
+ if (value?.type !== 'arcane.shared-host.started') return;
325
+ finish(value.error ? new CoreError(value.error) : null);
326
+ }
327
+ function failed(error) { finish(error); }
328
+ function exited(code, exitSignal) {
329
+ finish(new CoreError({code: 'CORE_HOST_START_FAILED', message: 'The headless Core exited before startup completed.', exitCode: code, signal: exitSignal, logFile}));
330
+ }
331
+ function aborted() { finish(signal.reason); }
332
+ child.on('message', message);
333
+ child.once('error', failed);
334
+ child.once('exit', exited);
335
+ signal?.addEventListener('abort', aborted, {once: true});
336
+ if (signal?.aborted) aborted();
337
+ });
338
+ } finally { await log.close(); }
339
+ }
340
+
341
+ async function discover({endpoint, start, signal}) {
342
+ try { return await connectSocket(endpoint, signal); }
343
+ catch (error) {
344
+ if (!start || !['ENOENT', 'ECONNREFUSED'].includes(error.code)) throw error;
345
+ }
346
+ await launch(start, signal);
347
+ return connectSocket(endpoint, signal);
348
+ }
349
+
350
+ function launchSelection(start) {
351
+ return start ? {...start, args: [...(start.args ?? [])], ...(start.env ? {env: {...start.env}} : {})} : undefined;
352
+ }
353
+
354
+ /** Connect/reuse, optionally starting an explicit independent headless entry. */
355
+ export async function connectSharedCoreHost({endpoint, start, signal, onError = reportError} = {}) {
356
+ const socket = await discover({endpoint, start: launchSelection(start), signal});
357
+ let client;
358
+ let receive;
359
+ let closing = false;
360
+ let writes = Promise.resolve();
361
+ let resolveClosed;
362
+ let rejectClosed;
363
+ const closed = new Promise(function ownConnection(resolve, reject) { resolveClosed = resolve; rejectClosed = reject; });
364
+ closed.catch(function observeConnectionFailure() {});
365
+ function diagnostic(error) { report(onError, error); }
366
+ function failed(error) { client?.failTransport(error); socket.destroy(); rejectClosed(error); }
367
+ const decoder = createCoreFrameDecoder(function frame(value) { receive(value); });
368
+ socket.on('data', function data(chunk) { try { decoder.push(chunk); } catch (error) { failed(error); } });
369
+ socket.on('error', failed);
370
+ socket.on('end', function ended() {
371
+ try { decoder.finish(); } catch (error) { failed(error); }
372
+ });
373
+ socket.on('close', function disconnected() {
374
+ signal?.removeEventListener('abort', abort);
375
+ if (!closing) client?.failTransport(failure('CORE_CONNECTION_CLOSED', 'The shared Core connection closed.'));
376
+ resolveClosed();
377
+ });
378
+ const transport = {
379
+ name: 'shared-core',
380
+ subscribe(listener) {
381
+ receive = listener;
382
+ return function disconnect() {
383
+ closing = true;
384
+ writes.finally(function retireSocket() { socket.end(); }).catch(diagnostic);
385
+ };
386
+ },
387
+ send(frame) {
388
+ const content = encodeCoreFrame(frame);
389
+ writes = writes.then(function sendFrame() { return write(socket, content); });
390
+ writes.catch(failed);
391
+ return writes;
392
+ }
393
+ };
394
+ client = createCoreClient({transport, replayRuntimeState: true, onError: diagnostic});
395
+ function abort() { client.close(); }
396
+ signal?.addEventListener('abort', abort, {once: true});
397
+ if (signal?.aborted) abort();
398
+ socket.resume();
399
+ return {client, endpoint, closed, close: function close() { client.close(); return closed; },
400
+ async shutdown(options) {
401
+ const result = await client.invoke('core.host.shutdown', {}, options);
402
+ client.close();
403
+ await closed;
404
+ return result;
405
+ }};
406
+ }
407
+
408
+ /** Headless entry hook: report startup to the explicit launcher, never exit. */
409
+ export async function runSharedCoreHost(options) {
410
+ function announce(error) {
411
+ if (!process.connected || typeof process.send !== 'function') return;
412
+ process.send({type: 'arcane.shared-host.started', ...(error ? {error: serializeCoreError(error)} : {})},
413
+ function sent(cause) { if (cause) report(options.onError ?? reportError, cause); });
414
+ }
415
+ try {
416
+ let host;
417
+ try { host = await startSharedCoreHost(options); }
418
+ catch (error) {
419
+ if (error.code !== 'EADDRINUSE' || error.coreHostPhase !== 'listen') throw error;
420
+ const connection = await connectSharedCoreHost({endpoint: options.endpoint, onError: options.onError});
421
+ try { await connection.client.invoke('system.ping'); }
422
+ finally { await connection.close(); }
423
+ announce();
424
+ return null;
425
+ }
426
+ announce();
427
+ return host;
428
+ } catch (error) { announce(error); throw error; }
429
+ }
430
+
431
+ /** Native stdio child acts only as a connection, never as the shared owner. */
432
+ export async function startSharedCoreBridge({endpoint, start, input = process.stdin, output = process.stdout,
433
+ signal, onError = reportError} = {}) {
434
+ const socket = await discover({endpoint, start: launchSelection(start), signal});
435
+ let closing;
436
+ let outbound = Promise.resolve();
437
+ let inbound = Promise.resolve();
438
+ let resolveClosed;
439
+ let rejectClosed;
440
+ let failure;
441
+ const closed = new Promise(function ownBridge(resolve, reject) { resolveClosed = resolve; rejectClosed = reject; });
442
+ closed.catch(function observeBridgeFailure() {});
443
+ function failed(error) {
444
+ failure ??= error;
445
+ report(onError, error);
446
+ socket.destroy();
447
+ close();
448
+ }
449
+ const fromCore = createCoreFrameDecoder(function frame(value) {
450
+ const content = encodeCoreFrame(value);
451
+ inbound = inbound.then(function outputFrame() { return write(output, content); });
452
+ inbound.catch(failed);
453
+ });
454
+ const fromInput = createCoreFrameDecoder(function frame(value) {
455
+ const content = encodeCoreFrame(value);
456
+ outbound = outbound.then(function inputFrame() { return write(socket, content); });
457
+ outbound.catch(failed);
458
+ });
459
+ function inputData(chunk) { try { fromInput.push(chunk); } catch (error) { failed(error); } }
460
+ function inputEnded() { try { fromInput.finish(); } catch (error) { failed(error); } close(); }
461
+ function outputClosed() { failed(failure ?? new CoreError({code: 'CORE_CONNECTION_CLOSED', message: 'The Core bridge output closed.'})); }
462
+ function close() {
463
+ if (closing) return closing;
464
+ input.off('data', inputData);
465
+ input.off('end', inputEnded);
466
+ input.off('close', inputEnded);
467
+ input.pause();
468
+ signal?.removeEventListener('abort', close);
469
+ closing = Promise.resolve().then(async function drainBridge() {
470
+ await Promise.allSettled([outbound]);
471
+ socket.end();
472
+ if (!socket.closed) await new Promise(function waitForClose(resolve) { socket.once('close', resolve); });
473
+ await Promise.allSettled([inbound]);
474
+ input.off('error', failed);
475
+ output.off('error', failed);
476
+ output.off('close', outputClosed);
477
+ output.off('finish', outputClosed);
478
+ if (failure) throw failure;
479
+ });
480
+ closing.then(resolveClosed, rejectClosed);
481
+ return closing;
482
+ }
483
+ socket.on('data', function coreData(chunk) { try { fromCore.push(chunk); } catch (error) { failed(error); } });
484
+ socket.on('error', failed);
485
+ socket.on('end', function coreEnded() { try { fromCore.finish(); } catch (error) { failed(error); } close(); });
486
+ socket.on('close', close);
487
+ input.on('data', inputData);
488
+ input.on('error', failed);
489
+ input.on('end', inputEnded);
490
+ input.on('close', inputEnded);
491
+ output.on('error', failed);
492
+ output.on('close', outputClosed);
493
+ output.on('finish', outputClosed);
494
+ signal?.addEventListener('abort', close, {once: true});
495
+ outbound = write(socket, encodeCoreFrame({protocol: CORE_PROTOCOL, type: 'control', control: 'runtime.replay'}));
496
+ outbound.catch(failed);
497
+ socket.resume();
498
+ if (signal?.aborted || input.readableEnded || input.destroyed) close();
499
+ return {endpoint, closed, close};
500
+ }
@@ -9,6 +9,7 @@ const is = new Is(false);
9
9
  export const SDK_ROOT = fileURLToPath(new URL('../../', import.meta.url));
10
10
  export const CORE_FILES = [
11
11
  'src/core/host.mjs',
12
+ 'src/core/shared-host.mjs',
12
13
  'src/core/runtime.mjs',
13
14
  'src/core/stdio.mjs',
14
15
  'src/event-manager.mjs',
@@ -161,11 +162,11 @@ export function coreEntrySource(application, version, services, {localAI, runtim
161
162
  }) ?? false;
162
163
  const imports = services.map(function serviceImport(service, index) {
163
164
  const specifier = '../app/' + service.module.split('/').map(encodeURIComponent).join('/');
164
- return `import createService${index} from ${JSON.stringify(specifier)};`;
165
+ return {binding: `{default: createService${index}}`, specifier};
165
166
  });
166
167
  // JSON parsing retains authored field names that have object-literal syntax.
167
168
  const definitions = services.map(function serviceDefinition(service, index) {
168
- return ` createService${index}(JSON.parse(${JSON.stringify(JSON.stringify(service.options === undefined ? {} : service.options))}), context)`;
169
+ return ` register(createService${index}(JSON.parse(${JSON.stringify(JSON.stringify(service.options === undefined ? {} : service.options))}), context));`;
169
170
  });
170
171
  if (localAI !== undefined) {
171
172
  const chatConfiguration = {
@@ -175,31 +176,30 @@ export function coreEntrySource(application, version, services, {localAI, runtim
175
176
  return id !== 'stable-diffusion.cpp' && id !== 'whisper.cpp';
176
177
  })
177
178
  };
178
- imports.push("import {createLocalAIService} from 'arcane-os/core/local-ai';");
179
- definitions.push(` createLocalAIService(JSON.parse(${JSON.stringify(JSON.stringify(chatConfiguration))}), context)`);
179
+ imports.push({binding: '{createLocalAIService}', specifier: 'arcane-os/core/local-ai'});
180
+ definitions.push(` register(createLocalAIService(JSON.parse(${JSON.stringify(JSON.stringify(chatConfiguration))}), context));`);
180
181
  }
181
182
  if (modelAssetsSelected) {
182
- imports.push("import {createModelAssetService} from 'arcane-os/core/model-assets';");
183
- definitions.push(' modelAssets');
183
+ imports.push({binding: '{createModelAssetService}', specifier: 'arcane-os/core/model-assets'});
184
184
  }
185
185
  if (imageSelected) {
186
- imports.push("import {createLocalImageService} from 'arcane-os/core/image';");
187
- definitions.push(` createLocalImageService(JSON.parse(${JSON.stringify(JSON.stringify(localAI))}), {...context, modelAssets})`);
186
+ imports.push({binding: '{createLocalImageService}', specifier: 'arcane-os/core/image'});
187
+ definitions.push(` register(createLocalImageService(JSON.parse(${JSON.stringify(JSON.stringify(localAI))}), {...context, modelAssets}));`);
188
188
  }
189
189
  if (whisperSelected) {
190
- imports.push("import path from 'node:path';");
191
- imports.push("import {createSpeechService} from 'arcane-os/core/speech';");
192
- imports.push("import {createWhisperRuntime} from 'arcane-os/local-ai/whisper';");
193
- definitions.push(' createSpeechService({stt, signal: context.signal})');
190
+ imports.push({binding: '{createSpeechService}', specifier: 'arcane-os/core/speech'});
191
+ imports.push({binding: '{createWhisperRuntime}', specifier: 'arcane-os/local-ai/whisper'});
194
192
  }
195
193
  if (packagedWeb) {
196
- imports.push("import {createPackagedWebService} from 'arcane-os/core/packaged-web';");
197
- definitions.push(" createPackagedWebService({artifactRoot: fileURLToPath(new URL('../', import.meta.url))}, context)");
194
+ imports.push({binding: '{createPackagedWebService}', specifier: 'arcane-os/core/packaged-web'});
195
+ definitions.push(" packagedWeb = createPackagedWebService({artifactRoot: fileURLToPath(new URL('../', import.meta.url))}, context);",
196
+ ' register(packagedWeb);');
198
197
  }
199
198
  return [
200
199
  "import {fileURLToPath} from 'node:url';",
201
- "import {readCoreLaunchContext, startCoreHost} from 'arcane-os/core/host';",
202
- ...imports,
200
+ "import path from 'node:path';",
201
+ "import {isSea} from 'node:sea';",
202
+ "import {readCoreLaunchContext, startCoreHost, runSharedCoreHost, startSharedCoreBridge} from 'arcane-os/core/host';",
203
203
  '',
204
204
  ...(localAI === undefined ? [] : [
205
205
  '// Bundled runtime paths are artifact-root-relative POSIX paths.',
@@ -244,30 +244,62 @@ export function coreEntrySource(application, version, services, {localAI, runtim
244
244
  ' ...await readCoreLaunchContext()',
245
245
  '};',
246
246
  '',
247
- ...(modelAssetsSelected ? ['const modelAssets = createModelAssetService({appRoot: context.appRoot});', ''] : []),
247
+ ...(packagedWeb ? ['let packagedWeb;', ''] : []),
248
+ 'async function createServices(register) {',
249
+ ` const [${imports.map(function binding(selection) { return selection.binding; }).join(', ')}] = await Promise.all([`,
250
+ ...imports.map(function importService(selection) { return ` import(${JSON.stringify(selection.specifier)})`; }).map(
251
+ function separateImport(line, index) { return line + (index < imports.length - 1 ? ',' : ''); }
252
+ ),
253
+ ' ]);',
254
+ ...(modelAssetsSelected ? [' const modelAssets = createModelAssetService({appRoot: context.appRoot});', ' register(modelAssets);'] : []),
248
255
  ...(whisperSelected ? [
249
- 'const stt = createWhisperRuntime(',
250
- ' {',
251
- ' runtime: context.runtimes.find(function selectedWhisperRuntime(runtime) {',
252
- " return runtime.id === 'whisper.cpp';",
253
- ' }),',
254
- " temporaryDirectory: path.join(context.stateRoot ?? path.join(context.appRoot, '.arcane'), 'speech', 'whisper'),",
255
- ' onEvent: context.onEvent',
256
- ' }',
257
- ');',
258
- ''
256
+ ' const stt = createWhisperRuntime(',
257
+ ' {',
258
+ ' runtime: context.runtimes.find(function selectedWhisperRuntime(runtime) {',
259
+ " return runtime.id === 'whisper.cpp';",
260
+ ' }),',
261
+ " temporaryDirectory: path.join(context.stateRoot ?? path.join(context.appRoot, '.arcane'), 'speech', 'whisper'),",
262
+ ' onEvent: context.onEvent',
263
+ ' }',
264
+ ' );',
265
+ ' register(createSpeechService({stt, signal: context.signal}));'
259
266
  ] : []),
260
- 'const host = startCoreHost({',
261
- ` application: JSON.parse(${JSON.stringify(JSON.stringify(application))}),`,
262
- ` version: ${JSON.stringify(version)},`,
263
- ` services: [\n${definitions.join(',\n')}\n ]`,
264
- '});',
267
+ ...definitions,
268
+ '}',
269
+ `const application = JSON.parse(${JSON.stringify(JSON.stringify(application))});`,
270
+ `const version = ${JSON.stringify(version)};`,
271
+ 'let host;',
272
+ 'if (context.sharedHost === undefined) {',
273
+ ' const services = [];',
274
+ ' await createServices(function register(service) { services.push(service); });',
275
+ ' host = startCoreHost({application, version, services});',
276
+ "} else if (process.argv.includes('--arcane-core-headless')) {",
277
+ ' host = await runSharedCoreHost({',
278
+ ' endpoint: context.sharedHost.endpoint, application, version,',
279
+ ' configure(runtime) { return createServices(function register(service) { runtime.registerService(service); }); },',
280
+ ...(packagedWeb ? [
281
+ ' getReplayEvents() {',
282
+ ' const current = packagedWeb?.current();',
283
+ " return current ? [{event: 'core.web.ready', data: current}] : [];",
284
+ ' }'
285
+ ] : []),
286
+ ' });',
287
+ '} else {',
288
+ ' host = await startSharedCoreBridge({',
289
+ ' endpoint: context.sharedHost.endpoint,',
290
+ ' start: {',
291
+ ' command: process.execPath,',
292
+ " args: [...(isSea() ? [] : [fileURLToPath(import.meta.url)]), ...process.argv.slice(2), '--arcane-core-headless'],",
293
+ ' logFile: context.sharedHost.logFile',
294
+ ' }',
295
+ ' });',
296
+ '}',
265
297
  '',
266
- 'function closeCore() { host.close(); }',
298
+ 'function closeCore() { host?.close(); }',
267
299
  "process.on('SIGINT', closeCore);",
268
300
  "process.on('SIGTERM', closeCore);",
269
301
  'try {',
270
- ' await host.closed;',
302
+ ' await host?.closed;',
271
303
  '} catch (error) {',
272
304
  " console.error('Arcane Core host failed:', error);",
273
305
  ' process.exitCode = 1;',