arcane-os 0.83.1 → 0.85.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.83.1",
3
+ "version": "0.85.0",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -43,6 +43,7 @@
43
43
  "./ai/core-onnx": "./browser-runtime/ai/core-onnx.mjs",
44
44
  "./ai/core-image": "./browser-runtime/ai/core-image.mjs",
45
45
  "./ai/core-model-assets": "./browser-runtime/ai/core-model-assets.mjs",
46
+ "./ai/model-observation": "./browser-runtime/ai/model-observation.mjs",
46
47
  "./ai/browser-decisions": "./browser-runtime/ai/browser-decisions.mjs",
47
48
  "./ai/tool-text-stream": "./browser-runtime/ai/tool-text-stream.mjs",
48
49
  "./ai/twin-cloud": "./browser-runtime/ai/twin-cloud.mjs",
@@ -75,6 +76,7 @@
75
76
  "./core/image": "./src/core/services/image.mjs",
76
77
  "./core/speech": "./src/core/services/speech.mjs",
77
78
  "./core/model-assets": "./src/core/services/model-assets.mjs",
79
+ "./core/model-observation": "./src/core/services/model-observation.mjs",
78
80
  "./diarization": "./src/diarization/index.mjs",
79
81
  "./diarization/build": "./src/diarization/build.mjs",
80
82
  "./local-ai": "./src/local-ai/install.mjs",
package/src/core/host.mjs CHANGED
@@ -5,7 +5,7 @@ import Is from 'strong-type';
5
5
  import {createCoreRuntime} from './runtime.mjs';
6
6
  import {startCoreStdio} from './stdio.mjs';
7
7
 
8
- export {startSharedCoreHost, connectSharedCoreHost, runSharedCoreHost, startSharedCoreBridge} from './shared-host.mjs';
8
+ export {startCoreListener, startSharedCoreHost, connectSharedCoreHost, runSharedCoreHost, startSharedCoreBridge} from './shared-host.mjs';
9
9
 
10
10
  const is = new Is(false);
11
11
 
@@ -0,0 +1,168 @@
1
+ import {randomUUID} from 'node:crypto';
2
+ import Is from 'strong-type';
3
+ import {CORE_PROTOCOL, CoreError, serializeCoreError} from '../../browser-runtime/core/contracts.mjs';
4
+
5
+ const is = new Is(false);
6
+ const owners = new WeakMap();
7
+
8
+ function failure(code, message) {
9
+ return new CoreError({code, message});
10
+ }
11
+
12
+ function event(event, data) {
13
+ return {protocol: CORE_PROTOCOL, type: 'event', event, data, time: new Date().toISOString()};
14
+ }
15
+
16
+ function runtimeConnections(runtime) {
17
+ const existing = owners.get(runtime);
18
+ if (existing) return existing;
19
+ const connections = new Set();
20
+ const requests = new Map();
21
+ const prefix = `connection-${randomUUID()}:`;
22
+ let sequence = 0;
23
+
24
+ function route(frame) {
25
+ if (frame.type === 'response') {
26
+ const record = requests.get(frame.id);
27
+ if (record) {
28
+ record.release();
29
+ record.connection.send({...frame, id: record.id}, record);
30
+ }
31
+ return;
32
+ }
33
+ if (frame.type !== 'event') return;
34
+ if (Object.hasOwn(frame, 'requestId')) {
35
+ const record = requests.get(frame.requestId);
36
+ if (record) record.connection.send({...frame, requestId: record.id}, record);
37
+ return;
38
+ }
39
+ for (const connection of connections) connection.send(frame);
40
+ }
41
+
42
+ const unsubscribe = runtime.onFrame(route);
43
+ const owner = {
44
+ connections,
45
+ requests,
46
+ allocate(requestPrefix) {
47
+ // Every acceptance gets a fresh transport correlation. A retired
48
+ // service context must never target a later use of its client ID.
49
+ const active = runtime.current().activeRequests;
50
+ function inUse(candidate) {
51
+ return requests.has(candidate) || active.some(
52
+ function activeRequest(request) { return request.id === candidate; }
53
+ );
54
+ }
55
+ let internalId;
56
+ do {
57
+ internalId = `${requestPrefix ?? ''}${prefix}${++sequence}`;
58
+ } while (inUse(internalId));
59
+ return internalId;
60
+ },
61
+ release() {
62
+ if (connections.size || requests.size) return;
63
+ unsubscribe();
64
+ owners.delete(runtime);
65
+ }
66
+ };
67
+ owners.set(runtime, owner);
68
+ return owner;
69
+ }
70
+
71
+ /** One transport connection borrows dispatch; only its host owns runtime.close. */
72
+ export function createCoreRuntimeConnection({
73
+ runtime, send, requestPrefix, mapFrame, getReplayEvents, preserveContextRequestId = true
74
+ }) {
75
+ const owner = runtimeConnections(runtime);
76
+ const requests = new Map();
77
+ const internalRequests = new Map();
78
+ let disconnected = false;
79
+
80
+ function deliver(frame, record) {
81
+ if (disconnected) return;
82
+ const outgoing = mapFrame ? mapFrame(frame, record) : frame;
83
+ if (outgoing !== undefined) send(outgoing);
84
+ }
85
+
86
+ function cancel(record) {
87
+ return runtime.handle(
88
+ {protocol: CORE_PROTOCOL, type: 'control', control: 'request.cancel', requestId: record.internalId}
89
+ );
90
+ }
91
+
92
+ function cancelAll() {
93
+ return Promise.all([...requests.values()].map(cancel));
94
+ }
95
+
96
+ function replay() {
97
+ const state = runtime.current();
98
+ if (state.state === 'ready') deliver(event('core.ready', {version: state.version, app: state.application}));
99
+ deliver(event('core.state', state));
100
+ for (const service of state.services) deliver(event('core.service.state', service));
101
+ // Only the existing owner's current snapshots, never requests/history.
102
+ for (const snapshot of getReplayEvents?.() ?? []) deliver(event(snapshot.event, snapshot.data));
103
+ }
104
+
105
+ async function handle(frame) {
106
+ if (disconnected) throw failure('CORE_CONNECTION_CLOSED', 'The Core connection is closed.');
107
+ if (frame?.protocol !== CORE_PROTOCOL) throw failure('INVALID_RPC_REQUEST', 'Unknown Core protocol.');
108
+ if (frame.type === 'control') {
109
+ if (frame.control === 'runtime.replay') return replay();
110
+ if (frame.control === 'requests.cancelAll') return cancelAll();
111
+ if (frame.control === 'request.cancel') {
112
+ const record = requests.get(frame.requestId);
113
+ return record ? cancel(record) : undefined;
114
+ }
115
+ throw failure('INVALID_RPC_CONTROL', 'Unknown Core control.');
116
+ }
117
+ if (frame.type !== 'request' || !is.string(frame.id) || !frame.id || !is.string(frame.method)) {
118
+ throw failure('INVALID_RPC_REQUEST', 'A Core request requires an id and method.');
119
+ }
120
+ if (requests.has(frame.id)) {
121
+ const answer = {protocol: CORE_PROTOCOL, type: 'response', id: frame.id, ok: false,
122
+ error: serializeCoreError(failure('RPC_REQUEST_ID_ACTIVE', 'The request id is already active on this connection.')),
123
+ time: new Date().toISOString()};
124
+ deliver(answer);
125
+ return answer;
126
+ }
127
+ const record = {connection, id: frame.id, internalId: owner.allocate(requestPrefix), release: releaseRequest};
128
+ function releaseRequest() {
129
+ if (requests.get(record.id) === record) requests.delete(record.id);
130
+ internalRequests.delete(record.internalId);
131
+ owner.requests.delete(record.internalId);
132
+ owner.release();
133
+ }
134
+ requests.set(record.id, record);
135
+ internalRequests.set(record.internalId, record);
136
+ owner.requests.set(record.internalId, record);
137
+ try {
138
+ return await runtime.handle(
139
+ {...frame, id: record.internalId},
140
+ {
141
+ contextRequestId: preserveContextRequestId ? record.id : undefined,
142
+ contextClientRequestId: record.id
143
+ }
144
+ );
145
+ } finally {
146
+ releaseRequest();
147
+ }
148
+ }
149
+
150
+ function close() {
151
+ if (disconnected) return Promise.resolve();
152
+ disconnected = true;
153
+ owner.connections.delete(connection);
154
+ const cancellation = cancelAll();
155
+ owner.release();
156
+ return cancellation;
157
+ }
158
+
159
+ const connection = {
160
+ handle,
161
+ close,
162
+ send: deliver,
163
+ has(id) { return requests.has(id); },
164
+ request(internalId) { return internalRequests.get(internalId); }
165
+ };
166
+ owner.connections.add(connection);
167
+ return connection;
168
+ }
@@ -218,7 +218,7 @@ class CoreRuntime {
218
218
  return this.current();
219
219
  }
220
220
 
221
- async handle(frame) {
221
+ async handle(frame, {contextRequestId, contextClientRequestId} = {}) {
222
222
  if (frame?.protocol !== CORE_PROTOCOL) throw failure('INVALID_RPC_REQUEST', 'Unknown Core protocol.');
223
223
  if (frame.type === 'control') {
224
224
  if (frame.control === 'runtime.replay') {
@@ -235,7 +235,13 @@ class CoreRuntime {
235
235
  if (frame.type !== 'request' || !is.string(frame.id) || !frame.id || !is.string(frame.method)) {
236
236
  throw failure('INVALID_RPC_REQUEST', 'A Core request requires an id and method.');
237
237
  }
238
- const task = this.#answer(frame);
238
+ if (contextRequestId !== undefined && (!is.string(contextRequestId) || !contextRequestId)) {
239
+ throw failure('INVALID_RPC_REQUEST', 'A transport context request id must be a nonempty string.');
240
+ }
241
+ if (contextClientRequestId !== undefined && (!is.string(contextClientRequestId) || !contextClientRequestId)) {
242
+ throw failure('INVALID_RPC_REQUEST', 'A transport client request id must be a nonempty string.');
243
+ }
244
+ const task = this.#answer(frame, contextRequestId, contextClientRequestId);
239
245
  this.#responses.add(task);
240
246
  try {
241
247
  return await task;
@@ -244,7 +250,7 @@ class CoreRuntime {
244
250
  }
245
251
  }
246
252
 
247
- async #answer(frame) {
253
+ async #answer(frame, contextRequestId, contextClientRequestId) {
248
254
  let result;
249
255
  let error;
250
256
  let ok = true;
@@ -254,7 +260,7 @@ class CoreRuntime {
254
260
  if (frame.method === 'system.ping') result = {ok: true};
255
261
  else if (frame.method === 'version.current') result = this.#version;
256
262
  else if (frame.method === 'app.current') result = this.#application;
257
- else result = await this.#request(frame);
263
+ else result = await this.#request(frame, contextRequestId, contextClientRequestId);
258
264
  } catch (caught) {
259
265
  ok = false;
260
266
  error = caught;
@@ -270,7 +276,7 @@ class CoreRuntime {
270
276
  return response;
271
277
  }
272
278
 
273
- #request(frame) {
279
+ #request(frame, contextRequestId, contextClientRequestId) {
274
280
  const registration = this.#methods.get(frame.method);
275
281
  if (!registration) {
276
282
  const namespace = frame.method.split('.')[0];
@@ -300,7 +306,13 @@ class CoreRuntime {
300
306
  const result = await handle.call(
301
307
  service.definition,
302
308
  frame.parameters,
303
- {...runtime.#serviceContext(service, frame.id), requestId: frame.id, signal: controller.signal}
309
+ {
310
+ ...runtime.#serviceContext(service, frame.id),
311
+ requestId: contextRequestId ?? frame.id,
312
+ clientRequestId: contextClientRequestId ?? contextRequestId ?? frame.id,
313
+ coreRequestId: frame.id,
314
+ signal: controller.signal
315
+ }
304
316
  );
305
317
  aborted(controller);
306
318
  return result;
@@ -0,0 +1,231 @@
1
+ import Is from 'strong-type';
2
+ import {CoreError} from '../../../browser-runtime/core/contracts.mjs';
3
+
4
+ const is = new Is(false);
5
+
6
+ function observationError(code, message) {
7
+ return new CoreError({code, message});
8
+ }
9
+
10
+ /** Observe supplied owners without acquiring, starting or replacing a model. */
11
+ export function createModelObservationService({runtime, localAI = null, image = null, decisions = null} = {}) {
12
+ if (!is.function(runtime?.current) || !is.function(runtime?.onFrame)) {
13
+ throw new TypeError('Model observation requires the existing Core runtime.');
14
+ }
15
+ const owners = {localAI, image, decisions};
16
+ const observedMethods = new Set();
17
+ for (const owner of Object.values(owners)) {
18
+ if (owner === null) continue;
19
+ if (!is.function(owner.current)) throw new TypeError('An observed Core model owner must expose current().');
20
+ for (const method of Object.keys(owner.methods ?? {})) observedMethods.add(method);
21
+ }
22
+
23
+ const renderers = new Map();
24
+ const watchers = new Set();
25
+ const requests = new Map();
26
+ let stopFrames = null;
27
+ let disposed = false;
28
+
29
+ function current() {
30
+ return {
31
+ core: runtime.current(),
32
+ native: {
33
+ localAI: localAI?.current() ?? null,
34
+ image: image?.current() ?? null,
35
+ decisions: decisions?.current() ?? null
36
+ },
37
+ renderers: [...renderers.values()].map(function rendererState(renderer) {
38
+ return {
39
+ attachmentRequestId: renderer.request.clientRequestId,
40
+ coreRequestId: renderer.request.coreRequestId,
41
+ owners: renderer.snapshot
42
+ };
43
+ })
44
+ };
45
+ }
46
+
47
+ function requireActive(request) {
48
+ request.signal.throwIfAborted();
49
+ if (disposed) throw observationError('MODEL_OBSERVATION_CLOSED', 'Model observation is closed.');
50
+ }
51
+
52
+ function requireContext(request) {
53
+ requireActive(request);
54
+ if (!is.string(request.clientRequestId) || !is.string(request.coreRequestId)) {
55
+ throw observationError('MODEL_OBSERVATION_CONTEXT_UNAVAILABLE',
56
+ 'Model observation requires the actual client and Core request identities.');
57
+ }
58
+ }
59
+
60
+ function observeActiveRequests(snapshot) {
61
+ for (const request of snapshot.activeRequests) {
62
+ if (observedMethods.has(request.method)) requests.set(request.id, request.method);
63
+ }
64
+ // Core publishes request removal before its response. Keep correlation
65
+ // until that response arrives, rather than treating removal as settlement.
66
+ }
67
+
68
+ function stopFrameObservation() {
69
+ if (watchers.size) return;
70
+ stopFrames?.();
71
+ stopFrames = null;
72
+ requests.clear();
73
+ }
74
+
75
+ function send(watcher, event, value) {
76
+ if (!watchers.has(watcher) || watcher.request.signal.aborted) return;
77
+ try {
78
+ watcher.request.emit(event, {watchRequestId: watcher.request.clientRequestId, ...value});
79
+ } catch (error) {
80
+ watcher.stop(error);
81
+ }
82
+ }
83
+
84
+ function publishState() {
85
+ if (!watchers.size) return;
86
+ let snapshot;
87
+ try {
88
+ snapshot = current();
89
+ } catch (error) {
90
+ for (const watcher of [...watchers]) watcher.stop(error);
91
+ return;
92
+ }
93
+ for (const watcher of [...watchers]) send(watcher, 'model.observation.state', {snapshot});
94
+ }
95
+
96
+ function observeFrame(frame) {
97
+ try {
98
+ if (frame.type === 'response') {
99
+ const method = requests.get(frame.id);
100
+ requests.delete(frame.id);
101
+ if (method !== undefined && frame.ok === false) {
102
+ for (const watcher of [...watchers]) {
103
+ if (watcher.diagnostics) {
104
+ send(watcher, 'model.observation.error', {source: 'core', method, frame});
105
+ }
106
+ }
107
+ }
108
+ return;
109
+ }
110
+ if (frame.type !== 'event') return;
111
+ if (frame.event === 'core.state') {
112
+ observeActiveRequests(frame.data);
113
+ publishState();
114
+ } else if (frame.event === 'localai.state' || frame.event === 'image.state'
115
+ || frame.event === 'decisions.state') {
116
+ publishState();
117
+ }
118
+ } catch (error) {
119
+ // An observation failure must settle observers, never the operation
120
+ // whose existing owner emitted this frame.
121
+ for (const watcher of [...watchers]) watcher.stop(error);
122
+ }
123
+ }
124
+
125
+ function hold(request, release) {
126
+ let reject;
127
+ let stopped = false;
128
+ const task = new Promise(function ownObservationRequest(_resolve, rejectRequest) {
129
+ reject = rejectRequest;
130
+ });
131
+ function stop(error) {
132
+ if (stopped) return;
133
+ stopped = true;
134
+ request.signal.removeEventListener('abort', aborted);
135
+ try {
136
+ release();
137
+ } catch (releaseError) {
138
+ error = new AggregateError([error, releaseError], 'Observation retirement failed.');
139
+ }
140
+ reject(error);
141
+ }
142
+ function aborted() {
143
+ stop(request.signal.reason ?? observationError('ARCANE_REQUEST_ABORTED', 'Model observation was cancelled.'));
144
+ }
145
+ request.signal.addEventListener('abort', aborted, {once: true});
146
+ return {task, stop, checkAbort: function checkObservationAbort() { if (request.signal.aborted) aborted(); }};
147
+ }
148
+
149
+ function watch({diagnostics = false} = {}, request) {
150
+ requireContext(request);
151
+ if (!is.boolean(diagnostics)) throw new TypeError('diagnostics must be a boolean.');
152
+ const watcher = {request, diagnostics, stop: null};
153
+ const owned = hold(request, function releaseWatcher() {
154
+ watchers.delete(watcher);
155
+ stopFrameObservation();
156
+ });
157
+ watcher.stop = owned.stop;
158
+ watchers.add(watcher);
159
+ try {
160
+ if (!stopFrames) stopFrames = runtime.onFrame(observeFrame);
161
+ observeActiveRequests(runtime.current());
162
+ send(watcher, 'model.observation.state', {snapshot: current()});
163
+ owned.checkAbort();
164
+ } catch (error) {
165
+ owned.stop(error);
166
+ }
167
+ return owned.task;
168
+ }
169
+
170
+ function attach({snapshot} = {}, request) {
171
+ requireContext(request);
172
+ if (!snapshot || !is.object(snapshot)) throw new TypeError('Renderer observation requires its current owner snapshots.');
173
+ if (renderers.has(request.coreRequestId)) {
174
+ throw observationError('MODEL_OBSERVATION_ATTACHMENT_ACTIVE', 'This Core attachment request is already active.');
175
+ }
176
+ const renderer = {request, snapshot, stop: null};
177
+ const owned = hold(request, function releaseRenderer() {
178
+ if (renderers.get(request.coreRequestId) === renderer) renderers.delete(request.coreRequestId);
179
+ publishState();
180
+ });
181
+ renderer.stop = owned.stop;
182
+ renderers.set(request.coreRequestId, renderer);
183
+ try {
184
+ request.emit('model.observation.renderer.attached', {
185
+ attachmentRequestId: request.clientRequestId,
186
+ coreRequestId: request.coreRequestId
187
+ });
188
+ publishState();
189
+ owned.checkAbort();
190
+ } catch (error) {
191
+ owned.stop(error);
192
+ }
193
+ return owned.task;
194
+ }
195
+
196
+ function publishRenderer({attachmentRequestId, coreRequestId, snapshot} = {}, request) {
197
+ requireActive(request);
198
+ const renderer = renderers.get(coreRequestId);
199
+ if (!renderer || renderer.request.clientRequestId !== attachmentRequestId || renderer.request.signal.aborted) {
200
+ throw observationError('MODEL_OBSERVATION_ATTACHMENT_RETIRED', 'The selected renderer observation attachment is no longer active.');
201
+ }
202
+ if (!snapshot || !is.object(snapshot)) throw new TypeError('Renderer observation requires its current owner snapshots.');
203
+ renderer.snapshot = snapshot;
204
+ publishState();
205
+ return {attachmentRequestId, coreRequestId};
206
+ }
207
+
208
+ function dispose() {
209
+ if (disposed) return;
210
+ disposed = true;
211
+ const error = observationError('MODEL_OBSERVATION_CLOSED', 'Model observation is closed.');
212
+ for (const watcher of [...watchers]) watcher.stop(error);
213
+ for (const renderer of [...renderers.values()]) renderer.stop(error);
214
+ stopFrameObservation();
215
+ }
216
+
217
+ return {
218
+ name: 'model-observation',
219
+ current,
220
+ dispose,
221
+ methods: {
222
+ 'model.observation.current': function currentObservation(_parameters, request) {
223
+ requireActive(request);
224
+ return current();
225
+ },
226
+ 'model.observation.watch': watch,
227
+ 'model.observation.renderer.attach': attach,
228
+ 'model.observation.renderer.publish': publishRenderer
229
+ }
230
+ };
231
+ }