arcane-os 0.88.0 → 0.89.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/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/browser-runtime/ai/browser-decisions.mjs +31 -4
- package/browser-runtime/ai/core-local.mjs +105 -25
- package/browser-runtime/ai/decision-runtime.mjs +7 -0
- package/browser-runtime/ai/decision-worker.mjs +9 -1
- package/browser-runtime/core/client.mjs +6 -0
- package/browser-runtime/core/contracts.mjs +10 -0
- package/docs/reference/ai/browser-decisions.md +89 -7
- package/docs/reference/cli.md +10 -0
- package/docs/reference/core-client.md +59 -0
- package/docs/reference/core-native-packaging.md +57 -10
- package/docs/reference/core-repositories.md +47 -3
- package/docs/reference/inventory/package-api.json +15 -15
- package/docs/reference/inventory/runtime-components.json +1 -1
- package/docs/reference/inventory/runtime-modules.json +4 -4
- package/docs/reference/local-ai.md +86 -3
- package/docs/reference/mcp-stdio.md +76 -1
- package/docs/reference/native-app-control.md +17 -5
- package/docs/reference/protocols.md +14 -0
- package/docs/reference/runtime-modules.md +9 -3
- package/docs/reference/sdk-api.md +146 -23
- package/package.json +2 -2
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +1 -5
- package/schemas/arcane-app.schema.json +13 -3
- package/schemas/arcane-package.schema.json +10 -1
- package/src/app-descriptor.mjs +6 -2
- package/src/cli/app-control.mjs +1 -1
- package/src/core/hosts/windows/ArcaneHost.cs +273 -5
- package/src/core/hosts/windows/ArcaneLauncher.cs +9 -0
- package/src/core/hosts/windows/ArcaneWindowControl.cs +7 -7
- package/src/core/repositories.mjs +11 -1
- package/src/core/services/local-ai.mjs +183 -22
- package/src/installed-sdk-runtime.mjs +2 -0
- package/src/local-ai/config.mjs +16 -0
- package/src/local-ai/dev.mjs +2 -2
- package/src/local-ai/discover.mjs +3 -0
- package/src/local-ai/install.mjs +130 -20
- package/src/local-ai/llama-execution-windows.ps1 +158 -0
- package/src/local-ai/llama-execution.mjs +169 -0
- package/src/local-ai/server.mjs +38 -8
- package/src/native/portable-layout.mjs +13 -3
- package/src/workspace-operation-lock.mjs +23 -10
- package/src/workspace-runtime.mjs +97 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.89.0
|
|
4
|
+
|
|
5
|
+
- Add explicitly selected clone-local author name/email before the first
|
|
6
|
+
checkout. Existing repositories and omitted settings retain their current
|
|
7
|
+
identity; Git author settings remain distinct from authenticated accounts.
|
|
8
|
+
- Add an app-owned entry point to the packaged native runtime. Applications
|
|
9
|
+
can launch their MCP module through that runtime with complete arguments,
|
|
10
|
+
while ordinary Core startup and app-owned shutdown remain intact.
|
|
11
|
+
- Add native window-state control and Escape app-control support. Use the
|
|
12
|
+
matching Windows host asset and rebuild consumer artifacts through their
|
|
13
|
+
existing public SDK workflow.
|
|
14
|
+
- Support opt-in extension-local decision runtime distribution and projected
|
|
15
|
+
native llama preparation with retained model lifetime and per-host target
|
|
16
|
+
selection. Model acquisition and application preferences remain with their
|
|
17
|
+
existing owners; CUDA acquisition requires its applicable dependency approval.
|
|
18
|
+
- Preserve literal Unicode and whitespace in RiskSignalAnalyzer input.
|
|
19
|
+
- Select the corrected portable Windows x64 ONNX 1.30.0 distribution through
|
|
20
|
+
the matching numeric SDK release. Include its complete native runtime closure
|
|
21
|
+
and notices in the separate retained runtime asset; keep native speech work
|
|
22
|
+
outside this release.
|
|
23
|
+
|
|
24
|
+
## 0.88.1
|
|
25
|
+
|
|
26
|
+
- Recover an ordinary workspace-operation lock when its recorded runner is
|
|
27
|
+
confirmed absent, including before the six-hour expiry. Retrying the public
|
|
28
|
+
operation uses the shared lock owner; existing published records stay readable.
|
|
29
|
+
- Preserve live or indeterminate owners and complete process-probe errors.
|
|
30
|
+
Recheck cancellation, nonce, PID and owner presence after asynchronous recovery
|
|
31
|
+
observation so an observed replacement remains owned by its actual writer.
|
|
32
|
+
The existing cooperative read/remove protocol is not an atomic cross-process
|
|
33
|
+
compare-and-delete.
|
|
34
|
+
|
|
3
35
|
## 0.88.0
|
|
4
36
|
|
|
5
37
|
- Add optional `signal` and `startLanguageModel` controls to `AI.setAI`,
|
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
|
|
|
25
25
|
share the theme, packaging, event, cancellation,
|
|
26
26
|
and browser run contracts while retaining their selected application layout.
|
|
27
27
|
|
|
28
|
-
This checkout defines the `0.88.
|
|
28
|
+
This checkout defines the `0.88.1` SDK contract. Development applications declare
|
|
29
29
|
`arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
|
|
30
30
|
designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
|
|
31
31
|
to refresh and compare the installed SDK and lockfile with the published channel;
|
|
@@ -88,16 +88,43 @@ class BrowserDecisionModel {
|
|
|
88
88
|
throw new TypeError('store must expose fetchResource().');
|
|
89
89
|
}
|
|
90
90
|
this.#store = store;
|
|
91
|
+
const local = runtime.local ?? false;
|
|
92
|
+
if (!is.boolean(local)) throw new TypeError('runtime.local must be a boolean.');
|
|
91
93
|
const moduleUrl = runtime.moduleUrl === undefined
|
|
92
|
-
?
|
|
94
|
+
? local
|
|
95
|
+
? new URL('./decisions-runtime/transformers.min.js', import.meta.url).href
|
|
96
|
+
: DEFAULT_RUNTIME_MODULE
|
|
93
97
|
: runtime.moduleUrl;
|
|
98
|
+
const selectedRuntime = {...runtime, moduleUrl: requiredText(moduleUrl, 'runtime.moduleUrl')};
|
|
99
|
+
if (local) {
|
|
100
|
+
const base = new URL(import.meta.url);
|
|
101
|
+
function localExecutableUrl(value, name) {
|
|
102
|
+
const url = new URL(requiredText(value, name), base);
|
|
103
|
+
if (url.protocol !== base.protocol || url.host !== base.host) {
|
|
104
|
+
throw new TypeError(`${name} must use the decision Worker's local deployment in runtime.local mode.`);
|
|
105
|
+
}
|
|
106
|
+
return url.href;
|
|
107
|
+
}
|
|
108
|
+
const paths = runtime.wasmPaths ?? './decisions-runtime/';
|
|
109
|
+
const wasmPaths = is.string(paths)
|
|
110
|
+
? {
|
|
111
|
+
mjs: new URL('ort-wasm-simd-threaded.asyncify.mjs', new URL(paths, base)).href,
|
|
112
|
+
wasm: new URL('ort-wasm-simd-threaded.asyncify.wasm', new URL(paths, base)).href
|
|
113
|
+
}
|
|
114
|
+
: paths;
|
|
115
|
+
selectedRuntime.moduleUrl = localExecutableUrl(moduleUrl, 'runtime.moduleUrl');
|
|
116
|
+
selectedRuntime.wasmPaths = {
|
|
117
|
+
mjs: localExecutableUrl(wasmPaths?.mjs, 'runtime.wasmPaths.mjs'),
|
|
118
|
+
wasm: localExecutableUrl(wasmPaths?.wasm, 'runtime.wasmPaths.wasm')
|
|
119
|
+
};
|
|
120
|
+
}
|
|
94
121
|
this.#configuration = {
|
|
95
122
|
family,
|
|
96
123
|
model: requiredText(model, 'model'),
|
|
97
124
|
revision: requiredText(revision, 'revision'),
|
|
98
125
|
device: requiredText(device, 'device'),
|
|
99
126
|
dtype: requiredText(dtype, 'dtype'),
|
|
100
|
-
runtime:
|
|
127
|
+
runtime: selectedRuntime
|
|
101
128
|
};
|
|
102
129
|
this.#events = createArcaneEventSource(
|
|
103
130
|
this,
|
|
@@ -266,7 +293,7 @@ class BrowserDecisionModel {
|
|
|
266
293
|
}
|
|
267
294
|
const id = this.#nextId++;
|
|
268
295
|
activation.pending.set(id, activation.load);
|
|
269
|
-
if (this.#store) {
|
|
296
|
+
if (this.#store && !this.#configuration.runtime.local) {
|
|
270
297
|
activation.runtimeController = new AbortController();
|
|
271
298
|
// Retain the operation before a store progress callback can
|
|
272
299
|
// synchronously unload this activation.
|
|
@@ -315,7 +342,7 @@ class BrowserDecisionModel {
|
|
|
315
342
|
);
|
|
316
343
|
} else {
|
|
317
344
|
worker.postMessage(
|
|
318
|
-
{id, op: 'load', payload: this.#configuration, storedResources:
|
|
345
|
+
{id, op: 'load', payload: this.#configuration, storedResources: this.#store !== null}
|
|
319
346
|
);
|
|
320
347
|
}
|
|
321
348
|
} catch (error) {
|
|
@@ -22,17 +22,22 @@ 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: suppliedClient, id = 'llama.cpp'} = {}) {
|
|
25
|
+
export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.cpp', prepareModel} = {}) {
|
|
26
26
|
if (!is.string(id) || !id.trim()) throw new TypeError('The Core AI provider id must be a nonempty string.');
|
|
27
27
|
if (suppliedClient !== undefined && suppliedClient !== null
|
|
28
28
|
&& (!is.function(suppliedClient?.invoke) || !is.function(suppliedClient?.events?.on))) {
|
|
29
29
|
throw new TypeError('The Core AI client must expose invoke and events.on.');
|
|
30
30
|
}
|
|
31
|
+
if (prepareModel !== undefined && prepareModel !== null && !is.function(prepareModel)) {
|
|
32
|
+
throw new TypeError('Core AI model preparation must be a function.');
|
|
33
|
+
}
|
|
31
34
|
|
|
32
35
|
let runtime = null;
|
|
33
36
|
let models = [];
|
|
34
37
|
let selection = null;
|
|
35
38
|
let selectionConnection = null;
|
|
39
|
+
let acceptedLoad = null;
|
|
40
|
+
let retainedSelection = null;
|
|
36
41
|
let disposed = false;
|
|
37
42
|
let loading = false;
|
|
38
43
|
let unloading = false;
|
|
@@ -75,7 +80,7 @@ export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.c
|
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
function status() {
|
|
78
|
-
const loaded = !disposed && modelReady();
|
|
83
|
+
const loaded = !disposed && Boolean(acceptedLoad) && modelReady();
|
|
79
84
|
return {
|
|
80
85
|
state: disposed ? 'disposed' : loading ? 'loading' : loaded ? 'ready'
|
|
81
86
|
: runtime?.error ? 'error' : runtime?.available === true ? 'unloaded' : 'unavailable',
|
|
@@ -86,6 +91,7 @@ export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.c
|
|
|
86
91
|
modelId: selection?.modelId ?? null,
|
|
87
92
|
available: !disposed && runtime?.available === true,
|
|
88
93
|
installed: runtime?.installed === true,
|
|
94
|
+
execution: runtime?.execution ?? null,
|
|
89
95
|
error: runtime?.error ?? null
|
|
90
96
|
};
|
|
91
97
|
}
|
|
@@ -113,6 +119,7 @@ export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.c
|
|
|
113
119
|
lifecycleRevision += 1;
|
|
114
120
|
loading = false;
|
|
115
121
|
unloading = false;
|
|
122
|
+
acceptedLoad = null;
|
|
116
123
|
runtime = {installed: false, available: false, state: client ? 'unknown' : 'unavailable', error};
|
|
117
124
|
models = [];
|
|
118
125
|
previous?.unsubscribe?.();
|
|
@@ -181,7 +188,9 @@ export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.c
|
|
|
181
188
|
const starting = !runtime?.error && ['starting', 'loading'].includes(runtime?.state);
|
|
182
189
|
const released = !runtime?.error && runtime?.released === true && runtime.owned === true
|
|
183
190
|
&& runtime.state === 'stopped' && knownModel;
|
|
184
|
-
|
|
191
|
+
const preparable = is.function(prepareModel) && runtime?.installed === true && runtime.managed === true
|
|
192
|
+
&& !['closed', 'closing'].includes(runtime.state);
|
|
193
|
+
if (runtime?.error || (runtime?.available !== true && !starting && !released && !preparable)) {
|
|
185
194
|
return {
|
|
186
195
|
available: false,
|
|
187
196
|
code: 'ARCANE_AI_PROVIDER_UNAVAILABLE',
|
|
@@ -189,7 +198,7 @@ export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.c
|
|
|
189
198
|
error: runtime?.error ?? null
|
|
190
199
|
};
|
|
191
200
|
}
|
|
192
|
-
if (!starting && !knownModel) {
|
|
201
|
+
if (!starting && !knownModel && !preparable) {
|
|
193
202
|
return {
|
|
194
203
|
available: false,
|
|
195
204
|
code: 'ARCANE_AI_MODEL_UNAVAILABLE',
|
|
@@ -206,31 +215,88 @@ export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.c
|
|
|
206
215
|
}
|
|
207
216
|
}
|
|
208
217
|
|
|
209
|
-
async function load({selection: value, signal, progress} = {}) {
|
|
218
|
+
async function load({selection: value, signal, progress, assetProjectionId, resourcePaths, executionTarget} = {}) {
|
|
210
219
|
const core = requireClient();
|
|
211
220
|
const currentConnection = connection;
|
|
212
221
|
requireSelection(value);
|
|
213
222
|
if (signal?.aborted) throw cancelled(signal.reason);
|
|
223
|
+
loading?.controller?.abort(cancelled());
|
|
214
224
|
selection = value;
|
|
215
225
|
selectionConnection = currentConnection;
|
|
216
|
-
|
|
226
|
+
acceptedLoad = null;
|
|
227
|
+
const operation = ownRequest(signal, null, core);
|
|
228
|
+
let completed = false;
|
|
217
229
|
loading = operation;
|
|
218
230
|
unloading = false;
|
|
219
231
|
lifecycleRevision += 1;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const revision = lifecycleRevision;
|
|
223
|
-
const current = await invokeOwned(core, 'llama.load', {model: value.modelId}, signal);
|
|
224
|
-
assertCurrentConnection(currentConnection);
|
|
225
|
-
if (loading !== operation || selection !== value) throw cancelled();
|
|
226
|
-
if (lifecycleRevision === revision) acceptRuntime(current);
|
|
227
|
-
if (!modelReady()) throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'Core did not report the selected model loaded and ready.', current?.error);
|
|
228
|
-
progress?.({phase: 'ready', modelId: value.modelId});
|
|
232
|
+
|
|
233
|
+
function assertCurrentLoad() {
|
|
229
234
|
assertCurrentConnection(currentConnection);
|
|
235
|
+
operation.controller.signal.throwIfAborted();
|
|
230
236
|
if (loading !== operation || selection !== value) throw cancelled();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function reportProgress(event) {
|
|
240
|
+
if (disposed || connection !== currentConnection || operation.controller.signal.aborted
|
|
241
|
+
|| loading !== operation || selection !== value) return;
|
|
242
|
+
progress?.(event);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
operation.result = Promise.resolve().then(async function prepareAndLoadModel() {
|
|
246
|
+
let preparation;
|
|
247
|
+
let failed = false;
|
|
248
|
+
let failure;
|
|
249
|
+
try {
|
|
250
|
+
assertCurrentLoad();
|
|
251
|
+
reportProgress({phase: 'loading', modelId: value.modelId});
|
|
252
|
+
assertCurrentLoad();
|
|
253
|
+
if (prepareModel) {
|
|
254
|
+
preparation = await prepareModel({selection: value, signal: operation.controller.signal, progress: reportProgress});
|
|
255
|
+
assertCurrentLoad();
|
|
256
|
+
}
|
|
257
|
+
const projectionId = assetProjectionId === undefined ? preparation?.assetProjectionId : assetProjectionId;
|
|
258
|
+
const paths = resourcePaths === undefined ? preparation?.resourcePaths : resourcePaths;
|
|
259
|
+
const target = executionTarget === undefined ? preparation?.executionTarget : executionTarget;
|
|
260
|
+
const parameters = {model: value.modelId};
|
|
261
|
+
if (projectionId !== undefined) parameters.assetProjectionId = projectionId;
|
|
262
|
+
if (paths !== undefined) parameters.resourcePaths = paths;
|
|
263
|
+
if (target !== undefined) parameters.executionTarget = target;
|
|
264
|
+
const revision = lifecycleRevision;
|
|
265
|
+
retainedSelection = {selection: value, connection: currentConnection};
|
|
266
|
+
const current = await core.invoke('llama.load', parameters, {signal: operation.controller.signal});
|
|
267
|
+
assertCurrentLoad();
|
|
268
|
+
if (lifecycleRevision === revision) acceptRuntime(current);
|
|
269
|
+
if (!modelReady()) throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'Core did not report the selected model loaded and ready.', current?.error);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
failed = true;
|
|
272
|
+
failure = error;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
// Core has retained the projection before its load settles.
|
|
276
|
+
// A cancelled preparation still releases any late result.
|
|
277
|
+
if (is.function(preparation?.release)) await preparation.release();
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (failed) throw new AggregateError([failure, error], 'Local model loading and preparation release failed.', {cause: failure});
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
if (failed) throw failure;
|
|
283
|
+
assertCurrentLoad();
|
|
284
|
+
if (!modelReady()) throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'Core no longer reports the selected model loaded and ready.', runtime?.error);
|
|
285
|
+
acceptedLoad = operation;
|
|
286
|
+
reportProgress({phase: 'ready', modelId: value.modelId});
|
|
287
|
+
assertCurrentLoad();
|
|
288
|
+
if (!modelReady()) throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'Core no longer reports the selected model loaded and ready.', runtime?.error);
|
|
289
|
+
completed = true;
|
|
231
290
|
return {authority: authority(value), status: {...status(), state: 'ready', busy: false}};
|
|
291
|
+
});
|
|
292
|
+
try {
|
|
293
|
+
return await operation.result;
|
|
232
294
|
} finally {
|
|
233
|
-
|
|
295
|
+
operation.release();
|
|
296
|
+
if (loading === operation) {
|
|
297
|
+
if (!completed) acceptedLoad = null;
|
|
298
|
+
loading = false;
|
|
299
|
+
}
|
|
234
300
|
}
|
|
235
301
|
}
|
|
236
302
|
|
|
@@ -254,7 +320,7 @@ export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.c
|
|
|
254
320
|
return request;
|
|
255
321
|
}
|
|
256
322
|
|
|
257
|
-
function streamRequest(core, parameters, signal) {
|
|
323
|
+
function streamRequest(core, parameters, signal, assertAcceptedLoad) {
|
|
258
324
|
const request = ownRequest(signal, parameters.model, core);
|
|
259
325
|
const chunks = [];
|
|
260
326
|
const readers = [];
|
|
@@ -284,6 +350,7 @@ export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.c
|
|
|
284
350
|
|
|
285
351
|
request.result = Promise.resolve().then(function invokeStream() {
|
|
286
352
|
request.controller.signal.throwIfAborted();
|
|
353
|
+
assertAcceptedLoad();
|
|
287
354
|
return core.invoke('llama.chat', {...parameters, stream: true, streamId}, {signal: request.controller.signal});
|
|
288
355
|
}).then(function streamCompleted(result) {
|
|
289
356
|
if (request.controller.signal.aborted) throw cancelled(request.controller.signal.reason);
|
|
@@ -329,9 +396,14 @@ export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.c
|
|
|
329
396
|
const currentConnection = connection;
|
|
330
397
|
requireSelection(value);
|
|
331
398
|
if (signal?.aborted) throw cancelled(signal.reason);
|
|
332
|
-
|
|
333
|
-
|
|
399
|
+
const currentLoad = acceptedLoad;
|
|
400
|
+
function assertAcceptedLoad() {
|
|
401
|
+
if (loading || unloading || !currentLoad || acceptedLoad !== currentLoad
|
|
402
|
+
|| selectionConnection !== connection || selection?.modelId !== value.modelId) {
|
|
403
|
+
throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'Load the selected local model before requesting inference.');
|
|
404
|
+
}
|
|
334
405
|
}
|
|
406
|
+
assertAcceptedLoad();
|
|
335
407
|
const model = payload?.model ?? value.modelId;
|
|
336
408
|
if (!models.some(function knownModel(record) { return record.id === model; })) {
|
|
337
409
|
const revision = lifecycleRevision;
|
|
@@ -339,6 +411,7 @@ export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.c
|
|
|
339
411
|
assertCurrentConnection(currentConnection);
|
|
340
412
|
if (lifecycleRevision === revision) acceptRuntime(current);
|
|
341
413
|
}
|
|
414
|
+
assertAcceptedLoad();
|
|
342
415
|
if (!models.some(function knownModel(record) { return record.id === model; })) {
|
|
343
416
|
throw localAIError('ARCANE_AI_MODEL_UNAVAILABLE', 'The requested model is unavailable in the local llama.cpp runtime.');
|
|
344
417
|
}
|
|
@@ -347,11 +420,12 @@ export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.c
|
|
|
347
420
|
throw localAIError('ARCANE_AI_MODEL_NOT_READY', 'The requested local runtime or selected model is not ready.');
|
|
348
421
|
}
|
|
349
422
|
const parameters = {model, payload};
|
|
350
|
-
if (operation === 'stream') return streamRequest(core, parameters, signal);
|
|
423
|
+
if (operation === 'stream') return streamRequest(core, parameters, signal, assertAcceptedLoad);
|
|
351
424
|
if (operation !== 'chat') throw localAIError('ARCANE_AI_PROVIDER_OPERATION_UNAVAILABLE', 'The local llama.cpp provider supports chat and stream.');
|
|
352
425
|
const pending = ownRequest(signal, model, core);
|
|
353
426
|
pending.result = Promise.resolve().then(function invokeChat() {
|
|
354
427
|
pending.controller.signal.throwIfAborted();
|
|
428
|
+
assertAcceptedLoad();
|
|
355
429
|
return core.invoke('llama.chat', {...parameters, stream: false}, {signal: pending.controller.signal});
|
|
356
430
|
}).then(function chatCompleted(result) {
|
|
357
431
|
if (pending.controller.signal.aborted) throw cancelled(pending.controller.signal.reason);
|
|
@@ -361,16 +435,18 @@ export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.c
|
|
|
361
435
|
}
|
|
362
436
|
|
|
363
437
|
async function unload({signal} = {}) {
|
|
364
|
-
const
|
|
438
|
+
const retained = retainedSelection;
|
|
439
|
+
const value = retained?.selection;
|
|
365
440
|
const core = requireClient();
|
|
366
441
|
const currentConnection = connection;
|
|
367
442
|
if (signal?.aborted) throw cancelled(signal.reason);
|
|
368
|
-
if (
|
|
443
|
+
if (retained && retained.connection !== currentConnection) {
|
|
369
444
|
throw localAIError('ARCANE_AI_CORE_UNAVAILABLE', 'The selected model belongs to a retired Core connection.');
|
|
370
445
|
}
|
|
371
446
|
if (value) requireSelection(value);
|
|
372
447
|
const operation = {};
|
|
373
448
|
unloading = operation;
|
|
449
|
+
acceptedLoad = null;
|
|
374
450
|
loading = false;
|
|
375
451
|
lifecycleRevision += 1;
|
|
376
452
|
for (const request of requests) request.controller.abort();
|
|
@@ -388,6 +464,7 @@ export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.c
|
|
|
388
464
|
if (unloading !== operation) throw cancelled();
|
|
389
465
|
selection = null;
|
|
390
466
|
selectionConnection = null;
|
|
467
|
+
retainedSelection = null;
|
|
391
468
|
unloading = false;
|
|
392
469
|
return status();
|
|
393
470
|
} catch (error) {
|
|
@@ -403,10 +480,12 @@ export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.c
|
|
|
403
480
|
function dispose({signal} = {}) {
|
|
404
481
|
if (disposing) return disposing;
|
|
405
482
|
// The runtime also supplies its configured selection after unload.
|
|
406
|
-
// Only
|
|
407
|
-
|
|
408
|
-
const
|
|
483
|
+
// Only a dispatched selection owns a native release; preparation alone
|
|
484
|
+
// must not unload a model owned by another provider.
|
|
485
|
+
const value = retainedSelection?.selection;
|
|
486
|
+
const core = retainedSelection?.connection.client;
|
|
409
487
|
disposed = true;
|
|
488
|
+
acceptedLoad = null;
|
|
410
489
|
loading = false;
|
|
411
490
|
unloading = false;
|
|
412
491
|
lifecycleRevision += 1;
|
|
@@ -425,6 +504,7 @@ export function createCoreLocalAIProvider({client: suppliedClient, id = 'llama.c
|
|
|
425
504
|
}
|
|
426
505
|
selection = null;
|
|
427
506
|
selectionConnection = null;
|
|
507
|
+
retainedSelection = null;
|
|
428
508
|
return status();
|
|
429
509
|
}
|
|
430
510
|
).catch(
|
|
@@ -221,6 +221,13 @@ export async function loadDecisionRuntime(configuration, report, resourceFetch =
|
|
|
221
221
|
if (runtime.wasmPaths !== undefined) {
|
|
222
222
|
namespace.env.backends.onnx.wasm.wasmPaths = runtime.wasmPaths;
|
|
223
223
|
}
|
|
224
|
+
if (runtime.local) {
|
|
225
|
+
// MV3 executes the packaged module directly. Its upstream cache and
|
|
226
|
+
// nested WASM workers would otherwise create executable Blob URLs.
|
|
227
|
+
namespace.env.useWasmCache = false;
|
|
228
|
+
namespace.env.backends.onnx.wasm.proxy = false;
|
|
229
|
+
namespace.env.backends.onnx.wasm.numThreads = 1;
|
|
230
|
+
}
|
|
224
231
|
const {AutoTokenizer, AutoModel, PreTrainedModel, Tensor} = namespace;
|
|
225
232
|
const options = {revision, device, dtype};
|
|
226
233
|
// No progress_callback: upstream 4.3 otherwise performs file-size
|
|
@@ -40,11 +40,17 @@ async function handleDecisionOperation(event) {
|
|
|
40
40
|
try {
|
|
41
41
|
if (op === 'load') {
|
|
42
42
|
if (event.data.storedResources) {
|
|
43
|
+
const localExecutables = payload.runtime.local
|
|
44
|
+
? new Set(
|
|
45
|
+
[payload.runtime.moduleUrl, payload.runtime.wasmPaths.mjs, payload.runtime.wasmPaths.wasm]
|
|
46
|
+
)
|
|
47
|
+
: null;
|
|
43
48
|
// Cover native ONNX support-file fetches as well as env.fetch.
|
|
44
49
|
// Native module imports remain the browser module loader's job.
|
|
45
50
|
globalThis.fetch = function fetchDecisionSupport(input, options) {
|
|
46
51
|
const url = new URL(is.string(input) ? input : input.url ?? input.href, globalThis.location.href);
|
|
47
52
|
if (url.protocol === 'blob:' || url.protocol === 'data:') return nativeFetch(input, options);
|
|
53
|
+
if (localExecutables?.has(url.href)) return nativeFetch(input, options);
|
|
48
54
|
return resources.fetch(input, options);
|
|
49
55
|
};
|
|
50
56
|
}
|
|
@@ -55,7 +61,9 @@ async function handleDecisionOperation(event) {
|
|
|
55
61
|
{id, progress}
|
|
56
62
|
);
|
|
57
63
|
},
|
|
58
|
-
event.data.storedResources
|
|
64
|
+
event.data.storedResources
|
|
65
|
+
? payload.runtime.local ? globalThis.fetch : resources.fetch
|
|
66
|
+
: null
|
|
59
67
|
);
|
|
60
68
|
globalThis.postMessage(
|
|
61
69
|
{id, result: {loaded: true}}
|
|
@@ -647,6 +647,12 @@ export function createCoreFacade(client){
|
|
|
647
647
|
preferences:{list:()=>invoke('preferences.list'),get:key=>invoke('preferences.get',{key}),set:(key,value)=>invoke('preferences.set',{key,value}),
|
|
648
648
|
setMany:entries=>invoke('preferences.setMany',{entries}),delete:key=>invoke('preferences.delete',{key})},
|
|
649
649
|
window: {
|
|
650
|
+
state(options) {
|
|
651
|
+
return invoke('window.state', {}, {signal: options?.signal});
|
|
652
|
+
},
|
|
653
|
+
setState(selection, options) {
|
|
654
|
+
return invoke('window.setState', selection, {signal: options?.signal});
|
|
655
|
+
},
|
|
650
656
|
setTheme(presentation, options) {
|
|
651
657
|
return invoke(
|
|
652
658
|
'window.setTheme', presentation,
|
|
@@ -43,6 +43,16 @@ export const CORE_METHOD_CONTRACTS={
|
|
|
43
43
|
output: '{requestedTarget,resolvedDevice,resolution,reason}',
|
|
44
44
|
meaning: 'Resolve a saved physical identity. Explicit null selects engine-owned automatic resolution; it does not claim a configured or observed device.'
|
|
45
45
|
},
|
|
46
|
+
'window.state': {
|
|
47
|
+
input: '{}',
|
|
48
|
+
output: '{platform,supported,state}',
|
|
49
|
+
meaning: 'Current requesting native window; state may be normal, maximized, fullscreen, or minimized. No saved preference is read.'
|
|
50
|
+
},
|
|
51
|
+
'window.setState': {
|
|
52
|
+
input: '{state:"normal"|"maximized"|"fullscreen"}',
|
|
53
|
+
output: '{platform,supported,state}',
|
|
54
|
+
meaning: 'Apply to the requesting native window and report its resulting state. No persistence, desktop activation, or Core restart.'
|
|
55
|
+
},
|
|
46
56
|
'window.setTheme': {
|
|
47
57
|
input: '{backgroundColor?,textColor?}: null reset or {red,green,blue,alpha}',
|
|
48
58
|
output: '{platform,supported,applied,unsupported}',
|
|
@@ -68,14 +68,16 @@ it does not substitute a quantized graph or another model.
|
|
|
68
68
|
| `revision` | Upstream revision, default `main`; select a stable revision when repeatable model selection is needed. |
|
|
69
69
|
| `device` | Transformers.js backend, default `webgpu`; an explicit supported alternative is caller-owned. No automatic backend fallback. |
|
|
70
70
|
| `dtype` | Exact upstream precision selection; defaults to `fp16` for Laya and `fp32` for Julia. For Laya FP32, supply `dtype: 'fp32'`. Unsupported selections fail at the selected upstream loader rather than being replaced. |
|
|
71
|
-
| `runtime.
|
|
72
|
-
| `runtime.
|
|
73
|
-
| `
|
|
71
|
+
| `runtime.local` | Default `false`. Explicit `true` selects deployment-local executable modules/WASM without executable Blob preparation; see the MV3 section below. |
|
|
72
|
+
| `runtime.moduleUrl` | Defaults to the self-contained CDN entry `https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0/dist/transformers.min.js`, or `./decisions-runtime/transformers.min.js` relative to this SDK module in local mode. The `.web.js` build expects a bundler to resolve its bare ONNX Runtime import and is unsuitable for a native module Worker. Explicit compatible URLs are retained; local mode requires files in the SDK Worker's deployment. With `store` and ordinary non-local mode, the SDK stores and materializes the complete entry for that activation. |
|
|
73
|
+
| `runtime.wasmPaths` | Optional upstream ONNX WASM location passed to the selected runtime's public environment API. Local mode defaults to the matching asyncify module/WASM under `./decisions-runtime/`; it accepts a directory URL or explicit `{mjs, wasm}` URLs resolved relative to this SDK module. |
|
|
74
|
+
| `store` | Optional existing SDK DBOPFS model or speech artifact store exposing `fetchResource()`. It remains outside Worker configuration and owns actual model resources. Ordinary stored mode also owns the runtime entry; local mode loads only the exact selected executable files directly. |
|
|
74
75
|
|
|
75
76
|
Without `store`, the runtime, tokenizer and weights use normal upstream loading
|
|
76
77
|
and caching. With `store`, the selected loader still owns filenames and model
|
|
77
78
|
selection while model resources persist through that existing SDK store. The
|
|
78
|
-
SDK does not
|
|
79
|
+
SDK package does not contain upstream runtimes or models; the explicit local
|
|
80
|
+
materializer below acquires its selected executable distribution. Browser
|
|
79
81
|
network, cross-origin and backend availability still apply. WebGPU needs a
|
|
80
82
|
compatible browser/device context. Unsupported precision or backend errors
|
|
81
83
|
are returned, not replaced with a different model, backend or precision.
|
|
@@ -91,6 +93,85 @@ Julia selects `dtype: 'fp32'`, loading root `model.onnx` with the exact
|
|
|
91
93
|
through the public generic `PreTrainedModel` API, not a text-generation
|
|
92
94
|
pipeline. Existing Wllama, cloud, speech and chat APIs are unchanged.
|
|
93
95
|
|
|
96
|
+
## Extension-local executables (Manifest V3)
|
|
97
|
+
|
|
98
|
+
During the application's ordinary SDK materialization, opt into the upstream
|
|
99
|
+
browser distribution through the public Node API:
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
import {materializeInstalledSdkRuntime} from 'arcane-os';
|
|
103
|
+
|
|
104
|
+
await materializeInstalledSdkRuntime({
|
|
105
|
+
workspaceRoot: process.cwd(),
|
|
106
|
+
browserDecisions: true
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
This acquires the official bundled Transformers.js `4.3.0`
|
|
111
|
+
`transformers.min.js`, matching ONNX Runtime
|
|
112
|
+
`1.31.0-dev.20260914-8d85527a0` `ort-wasm-simd-threaded.asyncify.mjs` and
|
|
113
|
+
`ort-wasm-simd-threaded.asyncify.wasm`, plus `TRANSFORMERS-LICENSE` and
|
|
114
|
+
`ONNX-RUNTIME-LICENSE`. The five complete files are staged under
|
|
115
|
+
`arcane/sdk/ai/decisions-runtime/` before replacing the projected runtime.
|
|
116
|
+
Retain the supplied licenses with the distribution. No bundler, Node engine
|
|
117
|
+
dependency tree, model graph or tokenizer is installed by this option.
|
|
118
|
+
Repeat `browserDecisions: true` on each materialization that should retain
|
|
119
|
+
this closure; whole-tree replacement removes files absent from a later selection.
|
|
120
|
+
|
|
121
|
+
The result's `workspaceRuntime.browserDecisions` contains `directory`,
|
|
122
|
+
`transformersVersion`, `onnxRuntimeVersion` and `files`. The lower-level
|
|
123
|
+
`materializeWorkspaceRuntimeContent()` accepts the same option and returns
|
|
124
|
+
that record directly as `result.browserDecisions`. Omission performs no
|
|
125
|
+
optional network acquisition. The fixed distribution files are acquired
|
|
126
|
+
concurrently, once each per selected materialization. Events
|
|
127
|
+
`workspace.decisions.started` and `workspace.decisions.progress` expose
|
|
128
|
+
`completed`/`total` file counts; each progress event also has `file` and `url`.
|
|
129
|
+
Cancellation and HTTP or observer failure abort/join pending acquisition and
|
|
130
|
+
preserve the prior runtime. An HTTP failure retains the complete response as
|
|
131
|
+
`error.response` (`url`, `status`, `statusText`, `headers`, `content`), including
|
|
132
|
+
inside `AggregateError.errors` when several operations fail.
|
|
133
|
+
|
|
134
|
+
Load from an extension-owned module script, using the deployed SDK path:
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
import {createBrowserDecisionModel} from './arcane/sdk/ai/browser-decisions.mjs';
|
|
138
|
+
|
|
139
|
+
const decisions = createBrowserDecisionModel({
|
|
140
|
+
family: 'laya',
|
|
141
|
+
model: 'onnx-community/laya-typed-decisions-ONNX',
|
|
142
|
+
dtype: 'fp16',
|
|
143
|
+
device: 'webgpu',
|
|
144
|
+
runtime: {local: true},
|
|
145
|
+
store // The application's existing SDK DBOPFS model store.
|
|
146
|
+
});
|
|
147
|
+
await decisions.load();
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
For Julia-1, change the family/model to `julia` /
|
|
151
|
+
`SupersonicLabs/Julia-1-ONNX` and select `dtype: 'fp32'`. Model and tokenizer
|
|
152
|
+
data remain upstream resources served through the same store, preserving its
|
|
153
|
+
complete response, persistent cache, progress and cancellation behavior.
|
|
154
|
+
Construction stays lazy. Only the exact configured local executable URLs use
|
|
155
|
+
native fetch/module loading outside that store; other resources, including
|
|
156
|
+
other same-deployment URLs, retain the model resource bridge.
|
|
157
|
+
|
|
158
|
+
Local mode directly imports the deployment's module and explicit asyncify
|
|
159
|
+
factory, disables the upstream executable WASM cache and proxy, and selects
|
|
160
|
+
one WASM thread to avoid nested executable Blob workers. This leaves
|
|
161
|
+
`device: 'webgpu'` unchanged; it is not a CPU/backend fallback. Compatible
|
|
162
|
+
custom `runtime.moduleUrl` and `runtime.wasmPaths: {mjs, wasm}` selections must
|
|
163
|
+
share the SDK module's protocol and host. Relative selections resolve against
|
|
164
|
+
that module. A `wasmPaths` directory URL selects the same two asyncify filenames.
|
|
165
|
+
|
|
166
|
+
Use external module scripts in extension pages. Chrome's MV3 extension-page
|
|
167
|
+
CSP permits WebAssembly with `script-src 'self' 'wasm-unsafe-eval'; object-src
|
|
168
|
+
'self'`; executable scripts and Workers remain packaged locally. The extension
|
|
169
|
+
owns the permissions required for its selected upstream model-data endpoints.
|
|
170
|
+
See [Chrome's extension CSP contract](https://developer.chrome.com/docs/extensions/reference/manifest/content-security-policy).
|
|
171
|
+
This source integration does not establish an actual Chrome extension load,
|
|
172
|
+
WebGPU inference, or application acceptance; those outcomes require execution
|
|
173
|
+
in the selected extension environment.
|
|
174
|
+
|
|
94
175
|
## Shared DBOPFS model resources
|
|
95
176
|
|
|
96
177
|
```js
|
|
@@ -111,7 +192,7 @@ await decisions.load();
|
|
|
111
192
|
|
|
112
193
|
The same `store` may serve other SDK model clients. The application supplies no
|
|
113
194
|
tokenizer/configuration/graph manifest and performs no file routing. Explicit
|
|
114
|
-
activation first opens `runtime.moduleUrl` through `store.fetchResource()`.
|
|
195
|
+
activation without `runtime.local` first opens `runtime.moduleUrl` through `store.fetchResource()`.
|
|
115
196
|
The SDK materializes the complete stored entry as a JavaScript object URL for
|
|
116
197
|
the dedicated Worker, without rewriting its content. The configured source URL
|
|
117
198
|
is retained for later activations, so they reuse the same stored resource rather
|
|
@@ -122,7 +203,8 @@ actual selected tokenizer, configuration, ONNX graph and external-data requests
|
|
|
122
203
|
to `store.fetchResource(input, options)`. Its competing browser, custom and
|
|
123
204
|
filesystem model caches are disabled only for this explicit stored mode. The
|
|
124
205
|
default upstream ONNX WASM binary and factory preloads use this same hook.
|
|
125
|
-
Native support-file fetches in the dedicated Worker also use the store
|
|
206
|
+
Native support-file fetches in the dedicated Worker also use the store, except
|
|
207
|
+
for the exact deployment-local executable selections in local mode. The
|
|
126
208
|
default runtime entry is self-contained. Entry materialization does not rewrite
|
|
127
209
|
dependency imports in custom modules or preserve their original `import.meta.url`
|
|
128
210
|
base. Custom stored entries must work from an object URL; relative imports or
|
|
@@ -148,7 +230,7 @@ url}`. `completed` counts closed persistent shards; `total` is `null` until the
|
|
|
148
230
|
resource finishes. This progress carries no network-size, rate or ETA fields.
|
|
149
231
|
Cancellation aborts the owned fetch and joins reader/writer cleanup before the
|
|
150
232
|
resource owner becomes available to a later request. Interrupted closed shards
|
|
151
|
-
are retained. Each activation owns its runtime object URL and revokes it after
|
|
233
|
+
are retained. Each non-local stored activation owns its runtime object URL and revokes it after
|
|
152
234
|
terminating the Worker on cancellation, failure, unload or disposal. A cancelled
|
|
153
235
|
runtime-entry fetch is joined before its waiting calls settle; a late response
|
|
154
236
|
cannot activate a replacement Worker. Runtime-entry HTTP failures retain the
|
package/docs/reference/cli.md
CHANGED
|
@@ -405,6 +405,16 @@ not package the app or produce `dist` output. With PWA enabled, the server
|
|
|
405
405
|
generates the installation and offline manifests directly; see
|
|
406
406
|
[PWA development and versioning](pwa.md#development-and-hosting).
|
|
407
407
|
|
|
408
|
+
Ordinary operation acquisition can recover a workspace-operation lock whose
|
|
409
|
+
recorded process is confirmed absent, including before its six-hour expiry.
|
|
410
|
+
Retry the intended command through its usual public entrypoint; no separate
|
|
411
|
+
unlock command or caller-created lease is needed. A live or indeterminate owner
|
|
412
|
+
remains `ARCANE_WORKSPACE_BUSY`, with an indeterminate process-probe error
|
|
413
|
+
retained as the cause. Recovery rechecks cancellation, nonce, PID and owner
|
|
414
|
+
presence after asynchronous observation, preserving a replacement detected
|
|
415
|
+
there. This cooperative read-then-remove protocol does not provide an
|
|
416
|
+
atomic compare-and-delete across competing processes.
|
|
417
|
+
|
|
408
418
|
The startup URL remains the selected app entry. At `/`, the server serves an
|
|
409
419
|
existing authored host-root `index.html` and suppresses the app-entry redirect
|
|
410
420
|
and PWA alias. A root outside the selected app mapping is served unchanged only
|