arcane-os 0.65.0 → 0.67.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 (40) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/browser-decisions.mjs +3 -2
  4. package/browser-runtime/ai/core-image.mjs +84 -34
  5. package/browser-runtime/ai/core-local.mjs +195 -43
  6. package/browser-runtime/ai/core-onnx.mjs +71 -12
  7. package/browser-runtime/ai/decision-runtime.mjs +14 -24
  8. package/docs/publishing.md +39 -0
  9. package/docs/reference/README.md +8 -1
  10. package/docs/reference/ai/browser-decisions.md +7 -4
  11. package/docs/reference/core-native-packaging.md +11 -4
  12. package/docs/reference/core-repositories.md +214 -11
  13. package/docs/reference/core-runtime.md +32 -5
  14. package/docs/reference/git-text-snapshot.md +15 -1
  15. package/docs/reference/inventory/package-api.json +41 -13
  16. package/docs/reference/inventory/runtime-modules.json +5 -4
  17. package/docs/reference/local-ai.md +119 -2
  18. package/docs/reference/local-image-generation.md +15 -4
  19. package/docs/reference/local-whisper.md +7 -1
  20. package/docs/reference/mcp-stdio.md +1 -1
  21. package/docs/reference/model-assets.md +50 -3
  22. package/docs/reference/native-decisions.md +181 -0
  23. package/docs/reference/native-speech.md +6 -0
  24. package/docs/reference/sdk-api.md +151 -16
  25. package/package.json +5 -2
  26. package/src/core/repositories.mjs +37 -14
  27. package/src/core/runtime.mjs +11 -0
  28. package/src/core/services/decisions.mjs +144 -0
  29. package/src/core/services/local-ai.mjs +31 -9
  30. package/src/core/services/model-assets.mjs +98 -13
  31. package/src/git-identity.mjs +96 -0
  32. package/src/git-text-snapshot.mjs +5 -3
  33. package/src/local-ai/decision-tokenizer-worker.mjs +31 -0
  34. package/src/local-ai/decision-tokenizer.mjs +109 -0
  35. package/src/local-ai/decisions.mjs +273 -0
  36. package/src/local-ai/dev.mjs +8 -2
  37. package/src/local-ai/onnx-worker.mjs +67 -7
  38. package/src/local-ai/onnx.mjs +23 -12
  39. package/src/native/portable-layout.mjs +8 -3
  40. package/src/repository.mjs +144 -4
@@ -1,5 +1,5 @@
1
1
  import Is from '../dependencies/strong-type/index.js';
2
- import {getInstalledCoreClient} from '../core/client.mjs';
2
+ import {subscribeCoreClient} from '../core/client.mjs';
3
3
 
4
4
  const is = new Is(false);
5
5
  const PROVIDER_PROTOCOL = 'arcane-ai-provider/2';
@@ -22,19 +22,26 @@ function cancelled(reason) {
22
22
  * selects its model; Core owns the running engine and its loaded sessions.
23
23
  * Creating this provider neither connects a transport nor installs a runtime.
24
24
  */
25
- export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id = 'llama.cpp'} = {}) {
25
+ export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.cpp'} = {}) {
26
26
  if (!is.string(id) || !id.trim()) throw new TypeError('The Core AI provider id must be a nonempty string.');
27
- if (client !== null && (!is.function(client?.invoke) || !is.function(client?.events?.on))) {
27
+ if (suppliedClient !== undefined && suppliedClient !== null
28
+ && (!is.function(suppliedClient?.invoke) || !is.function(suppliedClient?.events?.on))) {
28
29
  throw new TypeError('The Core AI client must expose invoke and events.on.');
29
30
  }
30
31
 
31
32
  let runtime = null;
32
33
  let models = [];
33
34
  let selection = null;
35
+ let selectionConnection = null;
34
36
  let disposed = false;
35
37
  let loading = false;
38
+ let unloading = false;
36
39
  let sequence = 0;
37
40
  let lifecycleRevision = 0;
41
+ let client = null;
42
+ let connection = null;
43
+ let stopInstallation;
44
+ let disposing;
38
45
  const requests = new Set();
39
46
 
40
47
  function requireClient() {
@@ -58,7 +65,13 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
58
65
  }
59
66
 
60
67
  function modelReady() {
61
- return runtime?.available === true && runtime.state === 'ready' && Boolean(loadedModel());
68
+ return !unloading && selectionConnection === connection && runtime?.available === true
69
+ && runtime.state === 'ready' && Boolean(loadedModel());
70
+ }
71
+
72
+ function assertCurrentConnection(value) {
73
+ if (disposed || connection !== value) throw cancelled();
74
+ value.controller.signal.throwIfAborted();
62
75
  }
63
76
 
64
77
  function status() {
@@ -67,7 +80,9 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
67
80
  state: disposed ? 'disposed' : loading ? 'loading' : loaded ? 'ready'
68
81
  : runtime?.error ? 'error' : runtime?.available === true ? 'unloaded' : 'unavailable',
69
82
  loaded,
70
- busy: loading || requests.size > 0,
83
+ busy: Boolean(loading || unloading) || [...requests].some(function currentRequest(request) {
84
+ return request.connection === connection && !request.controller.signal.aborted;
85
+ }),
71
86
  modelId: selection?.modelId ?? null,
72
87
  available: !disposed && runtime?.available === true,
73
88
  installed: runtime?.installed === true,
@@ -79,6 +94,7 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
79
94
  runtime = value ?? null;
80
95
  models = is.array(runtime?.models) ? runtime.models : [];
81
96
  for (const request of requests) {
97
+ if (request.connection !== connection || request.modelId === null) continue;
82
98
  const model = models.find(function requestModel(record) { return record.id === request.modelId; });
83
99
  if (model?.loaded === true) request.loaded = true;
84
100
  if (runtime?.available !== true || (request.loaded && (runtime.state !== 'ready' || model?.loaded !== true))) {
@@ -88,13 +104,62 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
88
104
  return status();
89
105
  }
90
106
 
91
- const unsubscribe = client?.events.on('localai.state', function localRuntimeChanged(snapshot) {
92
- if (disposed || !is.array(snapshot?.runtimes)) return;
107
+ function bindClient({client: nextClient, error = null}) {
108
+ if (disposed || (connection && nextClient === client)) return;
109
+ const previous = connection;
110
+ client = nextClient;
111
+ const currentConnection = {client, controller: new AbortController(), unsubscribe: null};
112
+ connection = currentConnection;
93
113
  lifecycleRevision += 1;
94
- acceptRuntime(snapshot.runtimes.find(function llamaRuntime(value) {
95
- return value.id === 'llama.cpp';
96
- }));
97
- }) ?? function noCoreSubscription() {};
114
+ loading = false;
115
+ unloading = false;
116
+ runtime = {installed: false, available: false, state: client ? 'unknown' : 'unavailable', error};
117
+ models = [];
118
+ previous?.unsubscribe?.();
119
+ previous?.controller.abort();
120
+ if (client) {
121
+ currentConnection.unsubscribe = client.events.on(
122
+ 'localai.state',
123
+ function localRuntimeChanged(snapshot) {
124
+ if (disposed || connection !== currentConnection || !is.array(snapshot?.runtimes)) return;
125
+ lifecycleRevision += 1;
126
+ acceptRuntime(snapshot.runtimes.find(function llamaRuntime(value) {
127
+ return value.id === 'llama.cpp';
128
+ }));
129
+ }
130
+ );
131
+ }
132
+ if (client && suppliedClient === undefined) {
133
+ const revision = lifecycleRevision;
134
+ invokeOwned(client, 'llama.status', {}).then(
135
+ function installedRuntimeObserved(value) {
136
+ if (!disposed && connection === currentConnection && lifecycleRevision === revision) acceptRuntime(value);
137
+ },
138
+ function installedRuntimeFailed(error) {
139
+ if (!disposed && connection === currentConnection && lifecycleRevision === revision) {
140
+ acceptRuntime({...runtime, available: false, state: 'error', error});
141
+ }
142
+ }
143
+ );
144
+ }
145
+ }
146
+
147
+ async function invokeOwned(core, method, parameters, signal) {
148
+ const request = ownRequest(signal, null, core);
149
+ request.result = Promise.resolve().then(
150
+ function invokeLocalOperation() {
151
+ request.controller.signal.throwIfAborted();
152
+ return core.invoke(method, parameters, {signal: request.controller.signal});
153
+ }
154
+ );
155
+ try {
156
+ const result = await request.result;
157
+ request.controller.signal.throwIfAborted();
158
+ return result;
159
+ } finally {
160
+ request.release();
161
+ }
162
+ }
98
163
 
99
164
  function authority(value) {
100
165
  return {protocol: AUTHORITY_PROTOCOL, providerId: id, modelId: value.modelId};
@@ -103,12 +168,20 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
103
168
  async function inspect(value, {signal} = {}) {
104
169
  requireSelection(value);
105
170
  if (signal?.aborted) throw cancelled(signal.reason);
171
+ const currentConnection = connection;
172
+ const statusRevision = lifecycleRevision;
106
173
  try {
107
174
  const core = requireClient();
108
- const statusRevision = lifecycleRevision;
109
- const current = await core.invoke('llama.status', {}, {signal});
175
+ const current = await invokeOwned(core, 'llama.status', {}, signal);
176
+ assertCurrentConnection(currentConnection);
110
177
  if (lifecycleRevision === statusRevision) acceptRuntime(current);
111
- if (runtime?.available !== true) {
178
+ const knownModel = models.some(function matchesModel(model) { return model.id === value.modelId; });
179
+ // Inspection describes an explicit load capability, never readiness.
180
+ // Core still verifies the exact model at the actual load boundary.
181
+ const starting = !runtime?.error && ['starting', 'loading'].includes(runtime?.state);
182
+ const released = !runtime?.error && runtime?.released === true && runtime.owned === true
183
+ && runtime.state === 'stopped' && knownModel;
184
+ if (runtime?.error || (runtime?.available !== true && !starting && !released)) {
112
185
  return {
113
186
  available: false,
114
187
  code: 'ARCANE_AI_PROVIDER_UNAVAILABLE',
@@ -116,7 +189,7 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
116
189
  error: runtime?.error ?? null
117
190
  };
118
191
  }
119
- if (!models.some(function matchesModel(model) { return model.id === value.modelId; })) {
192
+ if (!starting && !knownModel) {
120
193
  return {
121
194
  available: false,
122
195
  code: 'ARCANE_AI_MODEL_UNAVAILABLE',
@@ -126,48 +199,63 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
126
199
  return {available: true, authority: authority(value)};
127
200
  } catch (error) {
128
201
  if (signal?.aborted || error?.name === 'AbortError') throw error;
129
- acceptRuntime({...runtime, available: false, state: 'error', error});
202
+ if (!disposed && connection === currentConnection && lifecycleRevision === statusRevision) {
203
+ acceptRuntime({...runtime, available: false, state: 'error', error});
204
+ }
130
205
  return {available: false, code: error.code ?? 'ARCANE_AI_CORE_UNAVAILABLE', message: error.message, error};
131
206
  }
132
207
  }
133
208
 
134
209
  async function load({selection: value, signal, progress} = {}) {
135
210
  const core = requireClient();
211
+ const currentConnection = connection;
136
212
  requireSelection(value);
137
213
  if (signal?.aborted) throw cancelled(signal.reason);
138
214
  selection = value;
139
- loading = true;
215
+ selectionConnection = currentConnection;
216
+ const operation = {};
217
+ loading = operation;
218
+ unloading = false;
219
+ lifecycleRevision += 1;
140
220
  try {
141
221
  progress?.({phase: 'loading', modelId: value.modelId});
142
222
  const revision = lifecycleRevision;
143
- const current = await core.invoke('llama.load', {model: value.modelId}, {signal});
223
+ const current = await invokeOwned(core, 'llama.load', {model: value.modelId}, signal);
224
+ assertCurrentConnection(currentConnection);
225
+ if (loading !== operation || selection !== value) throw cancelled();
144
226
  if (lifecycleRevision === revision) acceptRuntime(current);
145
227
  if (!modelReady()) throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'Core did not report the selected model loaded and ready.', current?.error);
146
228
  progress?.({phase: 'ready', modelId: value.modelId});
229
+ assertCurrentConnection(currentConnection);
230
+ if (loading !== operation || selection !== value) throw cancelled();
147
231
  return {authority: authority(value), status: {...status(), state: 'ready', busy: false}};
148
232
  } finally {
149
- loading = false;
233
+ if (loading === operation) loading = false;
150
234
  }
151
235
  }
152
236
 
153
- function ownRequest(signal, modelId) {
237
+ function ownRequest(signal, modelId, core = requireClient()) {
238
+ const currentConnection = connection;
239
+ if (core !== currentConnection?.client) throw cancelled();
154
240
  const controller = new AbortController();
155
- function forwardAbort() { controller.abort(signal.reason); }
156
- signal?.addEventListener('abort', forwardAbort, {once: true});
157
- if (signal?.aborted) forwardAbort();
158
- const request = {controller, result: null, modelId, loaded: models.some(function loadedTarget(model) {
241
+ const operationSignal = signal
242
+ ? AbortSignal.any([signal, currentConnection.controller.signal]) : currentConnection.controller.signal;
243
+ function forwardAbort() { controller.abort(operationSignal.reason); }
244
+ operationSignal.addEventListener('abort', forwardAbort, {once: true});
245
+ if (operationSignal.aborted) forwardAbort();
246
+ const request = {controller, connection: currentConnection, result: null, modelId, loaded: models.some(function loadedTarget(model) {
159
247
  return model.id === modelId && model.loaded === true;
160
248
  })};
161
249
  requests.add(request);
162
250
  request.release = function releaseRequest() {
163
- signal?.removeEventListener('abort', forwardAbort);
251
+ operationSignal.removeEventListener('abort', forwardAbort);
164
252
  requests.delete(request);
165
253
  };
166
254
  return request;
167
255
  }
168
256
 
169
257
  function streamRequest(core, parameters, signal) {
170
- const request = ownRequest(signal, parameters.model);
258
+ const request = ownRequest(signal, parameters.model, core);
171
259
  const chunks = [];
172
260
  const readers = [];
173
261
  let complete = false;
@@ -195,6 +283,7 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
195
283
  });
196
284
 
197
285
  request.result = Promise.resolve().then(function invokeStream() {
286
+ request.controller.signal.throwIfAborted();
198
287
  return core.invoke('llama.chat', {...parameters, stream: true, streamId}, {signal: request.controller.signal});
199
288
  }).then(function streamCompleted(result) {
200
289
  if (request.controller.signal.aborted) throw cancelled(request.controller.signal.reason);
@@ -237,15 +326,17 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
237
326
 
238
327
  async function request({selection: value, operation, payload, signal} = {}) {
239
328
  const core = requireClient();
329
+ const currentConnection = connection;
240
330
  requireSelection(value);
241
331
  if (signal?.aborted) throw cancelled(signal.reason);
242
- if (loading || selection?.modelId !== value.modelId) {
332
+ if (loading || unloading || selectionConnection !== connection || selection?.modelId !== value.modelId) {
243
333
  throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'Load the selected local model before requesting inference.');
244
334
  }
245
335
  const model = payload?.model ?? value.modelId;
246
336
  if (!models.some(function knownModel(record) { return record.id === model; })) {
247
337
  const revision = lifecycleRevision;
248
- const current = await core.invoke('llama.status', {}, {signal});
338
+ const current = await invokeOwned(core, 'llama.status', {}, signal);
339
+ assertCurrentConnection(currentConnection);
249
340
  if (lifecycleRevision === revision) acceptRuntime(current);
250
341
  }
251
342
  if (!models.some(function knownModel(record) { return record.id === model; })) {
@@ -258,8 +349,9 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
258
349
  const parameters = {model, payload};
259
350
  if (operation === 'stream') return streamRequest(core, parameters, signal);
260
351
  if (operation !== 'chat') throw localAIError('ARCANE_AI_PROVIDER_OPERATION_UNAVAILABLE', 'The local llama.cpp provider supports chat and stream.');
261
- const pending = ownRequest(signal, model);
352
+ const pending = ownRequest(signal, model, core);
262
353
  pending.result = Promise.resolve().then(function invokeChat() {
354
+ pending.controller.signal.throwIfAborted();
263
355
  return core.invoke('llama.chat', {...parameters, stream: false}, {signal: pending.controller.signal});
264
356
  }).then(function chatCompleted(result) {
265
357
  if (pending.controller.signal.aborted) throw cancelled(pending.controller.signal.reason);
@@ -268,26 +360,86 @@ export function createCoreLocalAIProvider({client = getInstalledCoreClient(), id
268
360
  return pending.result;
269
361
  }
270
362
 
271
- async function unload({selection: value = selection, signal} = {}) {
363
+ async function unload({signal} = {}) {
364
+ const value = selection;
272
365
  const core = requireClient();
366
+ const currentConnection = connection;
273
367
  if (signal?.aborted) throw cancelled(signal.reason);
368
+ if (value && value === selection && selectionConnection !== currentConnection) {
369
+ throw localAIError('ARCANE_AI_CORE_UNAVAILABLE', 'The selected model belongs to a retired Core connection.');
370
+ }
371
+ if (value) requireSelection(value);
372
+ const operation = {};
373
+ unloading = operation;
374
+ loading = false;
375
+ lifecycleRevision += 1;
274
376
  for (const request of requests) request.controller.abort();
275
- await Promise.allSettled([...requests].map(function activeResult(request) { return request.result; }));
276
- if (!value) return status();
277
- requireSelection(value);
278
- const revision = lifecycleRevision;
279
- const current = await core.invoke('llama.unload', {model: value.modelId}, {signal});
280
- if (lifecycleRevision === revision) acceptRuntime(current);
281
- selection = null;
282
- return status();
377
+ try {
378
+ await Promise.allSettled([...requests].map(function activeResult(request) { return request.result; }));
379
+ assertCurrentConnection(currentConnection);
380
+ if (unloading !== operation) throw cancelled();
381
+ if (value) {
382
+ const revision = lifecycleRevision;
383
+ const current = await invokeOwned(core, 'llama.unload', {model: value.modelId}, signal);
384
+ assertCurrentConnection(currentConnection);
385
+ if (unloading !== operation) throw cancelled();
386
+ if (lifecycleRevision === revision) acceptRuntime(current);
387
+ }
388
+ if (unloading !== operation) throw cancelled();
389
+ selection = null;
390
+ selectionConnection = null;
391
+ unloading = false;
392
+ return status();
393
+ } catch (error) {
394
+ if (!disposed && connection === currentConnection && unloading === operation) {
395
+ runtime = {...runtime, state: 'error', error};
396
+ }
397
+ throw error;
398
+ } finally {
399
+ if (unloading === operation) unloading = false;
400
+ }
283
401
  }
284
402
 
285
- async function dispose(options = {}) {
286
- if (disposed) return status();
287
- if (selection || requests.size) await unload(options);
403
+ function dispose({signal} = {}) {
404
+ if (disposing) return disposing;
405
+ // The runtime also supplies its configured selection after unload.
406
+ // Only this provider's retained selection owns a native release.
407
+ const value = selection;
408
+ const core = selectionConnection?.client;
288
409
  disposed = true;
289
- unsubscribe();
290
- return status();
410
+ loading = false;
411
+ unloading = false;
412
+ lifecycleRevision += 1;
413
+ stopInstallation?.();
414
+ connection?.unsubscribe?.();
415
+ connection?.controller.abort();
416
+ disposing = Promise.resolve().then(
417
+ async function disposeLocalProvider() {
418
+ await Promise.allSettled([...requests].map(function activeResult(request) { return request.result; }));
419
+ if (value) {
420
+ requireSelection(value);
421
+ if (!core) throw localAIError('ARCANE_AI_CORE_UNAVAILABLE', 'The retired Core connection cannot unload the selected model.');
422
+ // Disposal owns this final release on the captured client only.
423
+ // Subscription cleanup above remains complete if it rejects.
424
+ await core.invoke('llama.unload', {model: value.modelId}, {signal});
425
+ }
426
+ selection = null;
427
+ selectionConnection = null;
428
+ return status();
429
+ }
430
+ ).catch(
431
+ function nativeReleaseFailed(error) {
432
+ runtime = {...runtime, error};
433
+ throw error;
434
+ }
435
+ );
436
+ return disposing;
437
+ }
438
+
439
+ if (suppliedClient === undefined) {
440
+ stopInstallation = subscribeCoreClient(bindClient);
441
+ } else {
442
+ bindClient({client: suppliedClient});
291
443
  }
292
444
 
293
445
  return {
@@ -1,4 +1,4 @@
1
- import {getInstalledCoreClient} from '../core/client.mjs';
1
+ import {subscribeCoreClient} from '../core/client.mjs';
2
2
  import {createArcaneEventSource} from '../event-manager.mjs';
3
3
  import {encodeTensorMap, decodeTensorMap, encodeTensorFetches} from './onnx-tensors.mjs';
4
4
 
@@ -9,16 +9,26 @@ function unavailable(message) {
9
9
  }
10
10
 
11
11
  /** Optional system ONNX access. Construction neither connects nor installs. */
12
- export function createCoreONNXRuntime({client = getInstalledCoreClient()} = {}) {
12
+ export function createCoreONNXRuntime({client: suppliedClient} = {}) {
13
13
  const owner = {};
14
14
  const events = createArcaneEventSource(owner, {source: 'core-onnx', eventTypes: ['onnx.state']});
15
15
  const lifetime = new AbortController();
16
16
  const pending = new Set();
17
+ let client = null;
18
+ let connection = null;
19
+ let stopInstallation;
17
20
  let state = {id: 'onnx', installed: false, available: false, state: 'unknown', models: []};
18
21
  let revision = 0;
19
22
  let closed = false;
20
23
 
21
- function current() { return {...state, busy: pending.size > 0, closed}; }
24
+ function current() {
25
+ const busy = [...pending].some(
26
+ function currentRequest(request) {
27
+ return request.connection === connection && !request.signal.aborted;
28
+ }
29
+ );
30
+ return {...state, busy, closed};
31
+ }
22
32
  function publish() { events.dispatch('onnx.state', current()); }
23
33
  function accept(value) {
24
34
  state = value ?? {id: 'onnx', installed: false, available: false, state: 'unavailable', models: []};
@@ -26,30 +36,66 @@ export function createCoreONNXRuntime({client = getInstalledCoreClient()} = {})
26
36
  publish();
27
37
  }
28
38
 
29
- const unsubscribe = client?.events.on('localai.state', function localStateChanged(snapshot) {
30
- if (!closed) accept(snapshot?.runtimes?.find(function onnxRuntime(runtime) { return runtime.id === 'onnx'; }));
31
- }) ?? function noCoreSubscription() {};
39
+ function bindClient({client: nextClient, error = null}) {
40
+ if (closed || (connection && nextClient === client)) return;
41
+ const previous = connection;
42
+ client = nextClient;
43
+ const currentConnection = {client, controller: new AbortController(), unsubscribe: null};
44
+ connection = currentConnection;
45
+ revision += 1;
46
+ state = {id: 'onnx', installed: false, available: false, state: client ? 'unknown' : 'unavailable', models: [], error};
47
+ previous?.unsubscribe?.();
48
+ previous?.controller.abort();
49
+ if (client) {
50
+ currentConnection.unsubscribe = client.events.on(
51
+ 'localai.state',
52
+ function localStateChanged(snapshot) {
53
+ if (!closed && connection === currentConnection) {
54
+ accept(snapshot?.runtimes?.find(function onnxRuntime(runtime) { return runtime.id === 'onnx'; }));
55
+ }
56
+ }
57
+ );
58
+ }
59
+ publish();
60
+ if (!closed && connection === currentConnection && client && suppliedClient === undefined) {
61
+ inspect().catch(
62
+ function initialONNXStateUnavailable(error) {
63
+ if (!closed && connection === currentConnection && !currentConnection.controller.signal.aborted) {
64
+ globalThis.console?.error('Core ONNX initial state unavailable.', error);
65
+ }
66
+ }
67
+ );
68
+ }
69
+ }
32
70
 
33
71
  async function invoke(method, parameters, {signal, timeoutMs = 0} = {}) {
34
72
  if (closed) throw unavailable('The Core ONNX accessor is closed.');
35
73
  if (!client) throw unavailable('Local ONNX requires an available Core connection.');
36
- const request = {};
37
- const operationSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
74
+ const currentConnection = connection;
75
+ const operationSignal = AbortSignal.any(
76
+ [lifetime.signal, currentConnection.controller.signal, ...(signal ? [signal] : [])]
77
+ );
78
+ const request = {connection: currentConnection, signal: operationSignal};
38
79
  operationSignal.throwIfAborted();
39
80
  pending.add(request);
40
81
  publish();
41
82
  try {
42
- return await client.invoke(method, parameters, {signal: operationSignal, timeoutMs});
83
+ operationSignal.throwIfAborted();
84
+ const result = await currentConnection.client.invoke(method, parameters, {signal: operationSignal, timeoutMs});
85
+ operationSignal.throwIfAborted();
86
+ return result;
43
87
  } finally {
44
88
  pending.delete(request);
45
- if (!closed) publish();
89
+ if (!closed && connection === currentConnection) publish();
46
90
  }
47
91
  }
48
92
 
49
93
  async function inspect(options) {
50
94
  const before = revision;
95
+ const currentConnection = connection;
51
96
  try {
52
97
  const result = await invoke('onnx.status', {}, options);
98
+ currentConnection.controller.signal.throwIfAborted();
53
99
  if (!closed && revision === before) accept(result);
54
100
  return current();
55
101
  } catch (error) {
@@ -61,14 +107,19 @@ export function createCoreONNXRuntime({client = getInstalledCoreClient()} = {})
61
107
  }
62
108
 
63
109
  async function load({id, model, sessionOptions, signal, timeoutMs} = {}) {
64
- return invoke('onnx.load', {id, model, sessionOptions}, {signal, timeoutMs});
110
+ const currentConnection = connection;
111
+ const result = await invoke('onnx.load', {id, model, sessionOptions}, {signal, timeoutMs});
112
+ currentConnection.controller.signal.throwIfAborted();
113
+ return result;
65
114
  }
66
115
 
67
116
  async function run({id, feeds, fetches, runOptions, signal, timeoutMs} = {}) {
117
+ const currentConnection = connection;
68
118
  const result = await invoke('onnx.run', {
69
119
  id, feeds: encodeTensorMap(feeds),
70
120
  fetches: encodeTensorFetches(fetches), runOptions
71
121
  }, {signal, timeoutMs});
122
+ currentConnection.controller.signal.throwIfAborted();
72
123
  return decodeTensorMap(result);
73
124
  }
74
125
 
@@ -86,11 +137,19 @@ export function createCoreONNXRuntime({client = getInstalledCoreClient()} = {})
86
137
  if (closed) return;
87
138
  closed = true;
88
139
  lifetime.abort();
89
- unsubscribe();
140
+ stopInstallation?.();
141
+ connection?.unsubscribe?.();
142
+ connection?.controller.abort();
90
143
  state = {...state, available: false, state: 'closed'};
91
144
  publish();
92
145
  events.dispose();
93
146
  }
94
147
 
148
+ if (suppliedClient === undefined) {
149
+ stopInstallation = subscribeCoreClient(bindClient, {signal: lifetime.signal});
150
+ } else {
151
+ bindClient({client: suppliedClient});
152
+ }
153
+
95
154
  return {load, run, unload, inspect, current, subscribe, close};
96
155
  }
@@ -82,7 +82,8 @@ export function encodeDecisionRows(tokenizer, rows, family) {
82
82
  return {encoded, pad};
83
83
  }
84
84
 
85
- function decisionInputs(Tensor, encoded, pad) {
85
+ /** Plain tensor records shared by browser and native ONNX execution owners. */
86
+ export function createDecisionInputs(encoded, pad) {
86
87
  let sequenceLength = 0;
87
88
  let optionCount = 0;
88
89
  for (const row of encoded) {
@@ -108,26 +109,11 @@ function decisionInputs(Tensor, encoded, pad) {
108
109
  types[rowIndex] = BigInt(row.qtype);
109
110
  }
110
111
  return {
111
- input_ids: new Tensor(
112
- 'int64', ids,
113
- [batch, sequenceLength]
114
- ),
115
- attention_mask: new Tensor(
116
- 'int64', attention,
117
- [batch, sequenceLength]
118
- ),
119
- marker_pos: new Tensor(
120
- 'int64', positions,
121
- [batch, optionCount]
122
- ),
123
- marker_mask: new Tensor(
124
- 'bool', mask,
125
- [batch, optionCount]
126
- ),
127
- qtype: new Tensor(
128
- 'int64', types,
129
- [batch]
130
- )
112
+ input_ids: {type: 'int64', data: ids, dims: [batch, sequenceLength]},
113
+ attention_mask: {type: 'int64', data: attention, dims: [batch, sequenceLength]},
114
+ marker_pos: {type: 'int64', data: positions, dims: [batch, optionCount]},
115
+ marker_mask: {type: 'bool', data: mask, dims: [batch, optionCount]},
116
+ qtype: {type: 'int64', data: types, dims: [batch]}
131
117
  };
132
118
  }
133
119
 
@@ -210,7 +196,8 @@ export function decodeDecisionOutputs(rows, outputs) {
210
196
  }
211
197
 
212
198
  export async function loadDecisionRuntime(configuration, report) {
213
- const {family, model: repository, revision = 'main', device = 'webgpu', runtime} = configuration;
199
+ const {family, model: repository, revision = 'main', device = 'webgpu',
200
+ dtype = family === 'laya' ? 'fp16' : 'fp32', runtime} = configuration;
214
201
  report(
215
202
  {phase: 'loading-runtime'}
216
203
  );
@@ -219,7 +206,7 @@ export async function loadDecisionRuntime(configuration, report) {
219
206
  namespace.env.backends.onnx.wasm.wasmPaths = runtime.wasmPaths;
220
207
  }
221
208
  const {AutoTokenizer, AutoModel, PreTrainedModel, Tensor} = namespace;
222
- const options = {revision, device, dtype: family === 'laya' ? 'fp16' : 'fp32'};
209
+ const options = {revision, device, dtype};
223
210
  // No progress_callback: upstream 4.3 otherwise performs file-size
224
211
  // aggregation. This owner reports actual semantic lifecycle transitions.
225
212
  report(
@@ -261,7 +248,10 @@ export async function loadDecisionRuntime(configuration, report) {
261
248
  async function evaluate(rows) {
262
249
  const {encoded, pad} = encodeDecisionRows(tokenizer, rows, family);
263
250
  if (encoded.length === 0) return {decisions: [], outputs: {}};
264
- const inputs = decisionInputs(Tensor, encoded, pad);
251
+ const inputs = {};
252
+ for (const [name, record] of Object.entries(createDecisionInputs(encoded, pad))) {
253
+ inputs[name] = new Tensor(record.type, record.data, record.dims);
254
+ }
265
255
  let output;
266
256
  try {
267
257
  output = await model(inputs);
@@ -116,6 +116,45 @@ tag and GitHub release title must both be the same bare numeric
116
116
  `MAJOR.MINOR.PATCH`. Prerelease versions do not get a misleading numeric
117
117
  GitHub release, and no release creates a Git branch for an npm dist-tag.
118
118
 
119
+ ## Native runtime assets on numeric releases
120
+
121
+ Default SDK-hosted native asset URLs use the installed SDK's version and its
122
+ matching numeric GitHub release. Publishing npm alone does not make those
123
+ downloads available. Every applicable numeric release therefore includes its
124
+ compatible retained SDK-hosted assets as release follow-through:
125
+
126
+ | Selected capability | Windows x64 release asset |
127
+ | --- | --- |
128
+ | Native application host | `arcane-native-windows-x64.tar.gz` |
129
+ | Ordinary native Whisper helper | `arcane-whisper-windows-x64.tar.gz` |
130
+ | Optional Intel NPU Whisper encoder | `arcane-whisper-openvino-windows-x64.tar.gz` |
131
+
132
+ Attach each asset used by the selected package's default SDK-hosted download
133
+ paths, including later releases whose native implementation is unchanged.
134
+ Include the optional NPU asset when that release supplies `encoder:
135
+ "openvino-npu"`; applications still opt in, and the ordinary Whisper asset
136
+ remains available. Upstream runtime and model versions retain their separate
137
+ upstream distribution URLs; they are not renamed to the SDK version.
138
+
139
+ Reuse the producer's retained compatible archive and its recorded selected
140
+ evidence. Do not rebuild or redownload an unchanged archive for each release.
141
+ An actual native change belongs to its producer under the existing build and
142
+ dependency authority. Preserve the complete runtime closure and its packaging
143
+ notices together. Models continue through their upstream acquisition path
144
+ unless their redistribution is separately requested.
145
+
146
+ The release owner assigns each upload, observes its outcome and records the
147
+ exact asset name and public release URL in the durable release handoff. If an
148
+ upload result is uncertain, inspect that release's existing asset before
149
+ retrying. Retain the exact pending operation, executing owner and last result
150
+ until delivery finishes; reassign idle delegated work rather than treating
151
+ delegation as completion.
152
+
153
+ Source delivery, selected package verification, npm publication, native asset
154
+ delivery, requester notices and documentation/site delivery are distinct.
155
+ Native archive follow-through does not gate npm publication or require a new
156
+ package build. Consumer adoption remains with each application owner.
157
+
119
158
  ## Documentation publication
120
159
 
121
160
  After every successful npm release and its applicable requester notices, the