arcane-os 0.68.0 → 0.70.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 (41) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +9 -4
  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 +8 -3
  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 +94 -26
  18. package/docs/reference/ai/browser-wasm.md +28 -20
  19. package/docs/reference/core-native-packaging.md +54 -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-modules.json +3 -3
  24. package/docs/reference/runtime-components.md +4 -2
  25. package/docs/reference/runtime-modules.md +120 -20
  26. package/docs/reference/sdk-api.md +64 -30
  27. package/package.json +2 -2
  28. package/runtime/arcane/components/theme-switcher.html +1 -1
  29. package/runtime/arcane/modules/AI.js +7 -22
  30. package/runtime/arcane/modules/AIProviderRuntime.js +84 -6
  31. package/runtime/arcane/modules/DBOPFS.js +180 -26
  32. package/schemas/arcane-app.schema.json +10 -0
  33. package/src/app-descriptor.mjs +23 -1
  34. package/src/core/host.mjs +2 -0
  35. package/src/core/hosts/windows/ArcaneHost.cs +46 -0
  36. package/src/core/hosts/windows/ArcaneLauncher.cs +21 -0
  37. package/src/core/runtime.mjs +9 -4
  38. package/src/core/services/packaged-web.mjs +8 -1
  39. package/src/core/shared-host.mjs +500 -0
  40. package/src/native/portable-layout.mjs +66 -34
  41. package/src/native/portable-provider.mjs +1 -0
@@ -48,6 +48,7 @@ namespace Arcane.Core.Hosts.Windows
48
48
  Dictionary<string, object> manifest = ReadRecord(Path.Combine(directory, "arcane-native.json"));
49
49
  Dictionary<string, object> app = RequiredRecord(manifest, "app");
50
50
  Dictionary<string, object> client = RequiredRecord(manifest, "client");
51
+ Dictionary<string, object> windowOptions = manifest.ContainsKey("window") ? RequiredRecord(manifest, "window") : null;
51
52
  string appId = RequiredString(app, "id");
52
53
  object title;
53
54
  string name = app.TryGetValue("displayName", out title) && title is string ? (string)title : appId;
@@ -71,6 +72,9 @@ namespace Arcane.Core.Hosts.Windows
71
72
  OriginHost = "arcane.local",
72
73
  ProfileDirectory = Path.Combine(profileParent, "WebView2"),
73
74
  Title = name,
75
+ InitialClientWidth = OptionalWindowDimension(windowOptions, "width"),
76
+ InitialClientHeight = OptionalWindowDimension(windowOptions, "height"),
77
+ Resizable = OptionalWindowResizable(windowOptions),
74
78
  IconPath = String.IsNullOrEmpty(icon) ? null : Path.Combine(directory, icon),
75
79
  ClassicClientSource = File.ReadAllText(Path.Combine(directory, RequiredString(client, "source"))),
76
80
  CoreExecutable = Path.Combine(directory, "runtime", "ArcaneCore.exe"),
@@ -164,6 +168,23 @@ namespace Arcane.Core.Hosts.Windows
164
168
  return text;
165
169
  }
166
170
 
171
+ private static double? OptionalWindowDimension(Dictionary<string, object> window, string name)
172
+ {
173
+ object value;
174
+ if (window == null || !window.TryGetValue(name, out value)) return null;
175
+ if (value is int || value is long || value is decimal || value is double)
176
+ return Convert.ToDouble(value, System.Globalization.CultureInfo.InvariantCulture);
177
+ throw new FormatException("The native manifest needs a number at window." + name + ".");
178
+ }
179
+
180
+ private static bool? OptionalWindowResizable(Dictionary<string, object> window)
181
+ {
182
+ object value;
183
+ if (window == null || !window.TryGetValue("resizable", out value)) return null;
184
+ if (value is bool) return (bool)value;
185
+ throw new FormatException("The native manifest needs a boolean at window.resizable.");
186
+ }
187
+
167
188
  private static string QuoteArgument(string value)
168
189
  {
169
190
  // Windows command-line escaping is transport-local. The child
@@ -109,7 +109,12 @@ class CoreRuntime {
109
109
  }
110
110
 
111
111
  emit(event, data) {
112
- const frame = {protocol: CORE_PROTOCOL, type: 'event', event, data, time: new Date().toISOString()};
112
+ return this.#emit(event, data);
113
+ }
114
+
115
+ #emit(event, data, requestId) {
116
+ const frame = {protocol: CORE_PROTOCOL, type: 'event', event, data,
117
+ ...(requestId === undefined ? {} : {requestId}), time: new Date().toISOString()};
113
118
  this.#events.dispatch(FRAME_EVENT, frame);
114
119
  return frame;
115
120
  }
@@ -158,7 +163,7 @@ class CoreRuntime {
158
163
  return service.definition;
159
164
  }
160
165
 
161
- #serviceContext(service) {
166
+ #serviceContext(service, requestId) {
162
167
  const runtime = this;
163
168
  return {
164
169
  application: this.#application,
@@ -167,7 +172,7 @@ class CoreRuntime {
167
172
  return runtime.getService(name);
168
173
  },
169
174
  emit(event, data) {
170
- return runtime.emit(event, data);
175
+ return runtime.#emit(event, data, requestId);
171
176
  }
172
177
  };
173
178
  }
@@ -295,7 +300,7 @@ class CoreRuntime {
295
300
  const result = await handle.call(
296
301
  service.definition,
297
302
  frame.parameters,
298
- {...runtime.#serviceContext(service), requestId: frame.id, signal: controller.signal}
303
+ {...runtime.#serviceContext(service, frame.id), requestId: frame.id, signal: controller.signal}
299
304
  );
300
305
  aborted(controller);
301
306
  return result;
@@ -8,8 +8,10 @@ export function createPackagedWebService({artifactRoot}, context = {}) {
8
8
  let server;
9
9
  let closing = false;
10
10
  let lifetime;
11
+ let current = null;
11
12
  return {
12
13
  name: 'packaged-web',
14
+ current() { return current === null ? null : {...current}; },
13
15
  async start(runtime) {
14
16
  const manifest = JSON.parse(await readFile(path.join(artifactRoot, 'arcane-native.json'), 'utf8'));
15
17
  // The launcher chooses app state independently of its installation.
@@ -48,9 +50,11 @@ export function createPackagedWebService({artifactRoot}, context = {}) {
48
50
  await writeFile(originFile, JSON.stringify({host: '127.0.0.1', port: server.port}) + '\n', {flag: 'wx'});
49
51
  }
50
52
  const url = new URL(manifest.start, server.origin).href;
51
- runtime.emit('core.web.ready', {origin: server.origin, url, port: server.port});
53
+ current = {origin: server.origin, url, port: server.port};
54
+ runtime.emit('core.web.ready', {...current});
52
55
  lifetime = server.lifecycle.then(
53
56
  function packagedServerEnded() {
57
+ current = null;
54
58
  if (!closing) {
55
59
  runtime.emit('core.web.failed', serializeCoreError(
56
60
  new Error('The packaged application listener closed unexpectedly.')
@@ -58,6 +62,7 @@ export function createPackagedWebService({artifactRoot}, context = {}) {
58
62
  }
59
63
  },
60
64
  function packagedServerFailed(error) {
65
+ current = null;
61
66
  if (!closing) runtime.emit('core.web.failed', serializeCoreError(error));
62
67
  throw error;
63
68
  }
@@ -66,6 +71,7 @@ export function createPackagedWebService({artifactRoot}, context = {}) {
66
71
  // now; the native diagnostic event owns immediate reporting.
67
72
  lifetime.catch(function observePackagedServerFailure() {});
68
73
  } catch (error) {
74
+ current = null;
69
75
  closing = true;
70
76
  if (server) {
71
77
  try { await server.close(); }
@@ -78,6 +84,7 @@ export function createPackagedWebService({artifactRoot}, context = {}) {
78
84
  },
79
85
  async drain() {
80
86
  closing = true;
87
+ current = null;
81
88
  if (!server) return;
82
89
  const results = await Promise.allSettled([server.close(), server.closed, lifetime]);
83
90
  const failures = [...new Set(results.filter(function rejected(result) {
@@ -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
+ }