arcane-os 0.71.1 → 0.72.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 +24 -0
- package/README.md +1 -1
- package/browser-runtime/ai/browser-decisions.mjs +90 -13
- package/browser-runtime/ai/kokoro-complete-input.mjs +116 -0
- package/browser-runtime/ai/speech-worker-runtime.mjs +62 -45
- package/docs/reference/README.md +1 -0
- package/docs/reference/ai/browser-decisions.md +26 -11
- package/docs/reference/ai/browser-speech.md +26 -5
- package/docs/reference/core-native-packaging.md +74 -7
- package/docs/reference/core-shared-host.md +43 -4
- package/docs/reference/inventory/package-api.json +3 -3
- package/package.json +2 -2
- package/schemas/arcane-app.schema.json +4 -0
- package/src/app-descriptor.mjs +7 -1
- package/src/core/host.mjs +73 -13
- package/src/core/hosts/macos/ArcaneLauncher.m +14 -3
- package/src/core/hosts/windows/ArcaneLauncher.cs +25 -5
- package/src/native/portable-layout.mjs +9 -1
- package/src/native/portable-provider.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.72.0
|
|
4
|
+
|
|
5
|
+
- Add optional `native.launchContext` defaults for ordinary packaged launch and
|
|
6
|
+
public `resolveNativeLaunchContext()` through `arcane-os/core/host`. Native
|
|
7
|
+
applications and external MCP can select the same app-owned state, workspace,
|
|
8
|
+
shared endpoint and log without reconstructing platform paths.
|
|
9
|
+
- Extend `readCoreLaunchContext()` with explicit defaults and opt-in native
|
|
10
|
+
resolution. Preserve complete explicit launch records, selected workspace
|
|
11
|
+
values, ordinary stdio ownership and the existing no-option entry path.
|
|
12
|
+
- Forward packaged launch defaults through Windows and macOS launchers while
|
|
13
|
+
preserving window, profile, service and shared-host lifecycle behavior. No
|
|
14
|
+
stored-data migration or dependency change is introduced.
|
|
15
|
+
|
|
16
|
+
## 0.71.2
|
|
17
|
+
|
|
18
|
+
- Preserve complete browser Kokoro synthesis input through the selected runtime's
|
|
19
|
+
phonemization and tokenizer. Process every token in ordered model-capacity
|
|
20
|
+
segments and retain all resulting audio, with one model owner and request-local
|
|
21
|
+
cancellation. Keep short-input behavior, voice, speed and the public API intact.
|
|
22
|
+
- Include the browser decision runtime entry in an explicitly configured DBOPFS
|
|
23
|
+
model store. Retain the original resource URL across activations, import the
|
|
24
|
+
complete stored self-contained module, and join cancellation cleanup before
|
|
25
|
+
settling callers. Preserve direct loading when no store is configured.
|
|
26
|
+
|
|
3
27
|
## 0.71.1
|
|
4
28
|
|
|
5
29
|
- Correct explicit disabled PWA lifecycle in standalone source development by
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
|
|
|
20
20
|
share the theme, packaging, event, cancellation,
|
|
21
21
|
and browser run contracts while retaining their selected application layout.
|
|
22
22
|
|
|
23
|
-
This checkout defines the `0.
|
|
23
|
+
This checkout defines the `0.72.0` SDK contract. Development applications declare
|
|
24
24
|
`arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
|
|
25
25
|
designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
|
|
26
26
|
to refresh and compare the installed SDK and lockfile with the published channel;
|
|
@@ -204,6 +204,9 @@ class BrowserDecisionModel {
|
|
|
204
204
|
pending: new Map(),
|
|
205
205
|
evaluations: new Set(),
|
|
206
206
|
resources: null,
|
|
207
|
+
runtimeController: null,
|
|
208
|
+
runtimePreparation: null,
|
|
209
|
+
runtimeUrl: null,
|
|
207
210
|
cleanup: null,
|
|
208
211
|
reason: undefined
|
|
209
212
|
};
|
|
@@ -263,9 +266,58 @@ class BrowserDecisionModel {
|
|
|
263
266
|
}
|
|
264
267
|
const id = this.#nextId++;
|
|
265
268
|
activation.pending.set(id, activation.load);
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
+
if (this.#store) {
|
|
270
|
+
activation.runtimeController = new AbortController();
|
|
271
|
+
// Retain the operation before a store progress callback can
|
|
272
|
+
// synchronously unload this activation.
|
|
273
|
+
activation.runtimePreparation = Promise.resolve().then(
|
|
274
|
+
async function prepareDecisionRuntimeEntry() {
|
|
275
|
+
if (!model.#isCurrent(activation)) return;
|
|
276
|
+
const configuration = model.#configuration;
|
|
277
|
+
const sourceUrl = new URL(configuration.runtime.moduleUrl, import.meta.url).href;
|
|
278
|
+
const response = await model.#store.fetchResource(sourceUrl, {
|
|
279
|
+
signal: activation.runtimeController.signal,
|
|
280
|
+
onProgress: function reportRuntimeEntryProgress(progress) {
|
|
281
|
+
if (!model.#isCurrent(activation)) return;
|
|
282
|
+
model.#progress = progress;
|
|
283
|
+
model.#publish();
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
if (!model.#isCurrent(activation)) return;
|
|
287
|
+
if (response.status < 200 || response.status >= 300) {
|
|
288
|
+
const error = new Error(
|
|
289
|
+
`The decision runtime returned HTTP ${response.status} ${response.statusText}.`
|
|
290
|
+
);
|
|
291
|
+
error.code = 'ARCANE_DECISION_RUNTIME_DOWNLOAD_FAILED';
|
|
292
|
+
error.response = response;
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
// Import the complete stored module without source
|
|
296
|
+
// rewriting. This activation owns its URL until stop.
|
|
297
|
+
activation.runtimeUrl = URL.createObjectURL(
|
|
298
|
+
new Blob([response.file], {type: 'text/javascript'})
|
|
299
|
+
);
|
|
300
|
+
worker.postMessage({
|
|
301
|
+
id,
|
|
302
|
+
op: 'load',
|
|
303
|
+
payload: {
|
|
304
|
+
...configuration,
|
|
305
|
+
runtime: {...configuration.runtime, moduleUrl: activation.runtimeUrl}
|
|
306
|
+
},
|
|
307
|
+
storedResources: true
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
activation.runtimePreparation.catch(
|
|
312
|
+
function stopFailedRuntimePreparation(error) {
|
|
313
|
+
if (model.#isCurrent(activation)) model.#stop(activation, error, 'error');
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
} else {
|
|
317
|
+
worker.postMessage(
|
|
318
|
+
{id, op: 'load', payload: this.#configuration, storedResources: false}
|
|
319
|
+
);
|
|
320
|
+
}
|
|
269
321
|
} catch (error) {
|
|
270
322
|
this.#stop(activation, error, 'error');
|
|
271
323
|
}
|
|
@@ -333,11 +385,42 @@ class BrowserDecisionModel {
|
|
|
333
385
|
this.#state = state;
|
|
334
386
|
this.#progress = null;
|
|
335
387
|
this.#error = reason;
|
|
336
|
-
|
|
388
|
+
const cleanup = [];
|
|
389
|
+
activation.runtimeController?.abort(reason);
|
|
390
|
+
if (activation.runtimePreparation) {
|
|
391
|
+
cleanup.push(activation.runtimePreparation.catch(
|
|
392
|
+
function preserveRuntimePreparationFailure(error) {
|
|
393
|
+
if (error === reason || error?.name === 'AbortError') return;
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
));
|
|
397
|
+
}
|
|
398
|
+
if (activation.resources) cleanup.push(activation.resources.close(reason));
|
|
399
|
+
if (activation.worker) {
|
|
400
|
+
for (const [type, listener] of Object.entries(activation.listeners ?? {})) {
|
|
401
|
+
activation.worker.removeEventListener(type, listener);
|
|
402
|
+
}
|
|
403
|
+
activation.worker.terminate();
|
|
404
|
+
}
|
|
405
|
+
if (activation.runtimeUrl !== null) {
|
|
406
|
+
try {
|
|
407
|
+
URL.revokeObjectURL(activation.runtimeUrl);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
cleanup.push(Promise.reject(error));
|
|
410
|
+
}
|
|
411
|
+
activation.runtimeUrl = null;
|
|
412
|
+
}
|
|
413
|
+
if (cleanup.length > 0) {
|
|
337
414
|
const model = this;
|
|
338
|
-
activation.cleanup =
|
|
339
|
-
function
|
|
340
|
-
|
|
415
|
+
activation.cleanup = Promise.allSettled(cleanup).then(
|
|
416
|
+
function settleDecisionResourceCleanup(results) {
|
|
417
|
+
const failures = results.filter(function failedCleanup(result) {
|
|
418
|
+
return result.status === 'rejected';
|
|
419
|
+
}).map(function cleanupReason(result) {
|
|
420
|
+
return result.reason;
|
|
421
|
+
});
|
|
422
|
+
if (failures.length === 0) return;
|
|
423
|
+
activation.reason = new AggregateError([reason, ...failures], 'Unable to settle decision model resources.');
|
|
341
424
|
throw activation.reason;
|
|
342
425
|
}
|
|
343
426
|
);
|
|
@@ -350,12 +433,6 @@ class BrowserDecisionModel {
|
|
|
350
433
|
}
|
|
351
434
|
);
|
|
352
435
|
}
|
|
353
|
-
if (activation.worker) {
|
|
354
|
-
for (const [type, listener] of Object.entries(activation.listeners ?? {})) {
|
|
355
|
-
activation.worker.removeEventListener(type, listener);
|
|
356
|
-
}
|
|
357
|
-
activation.worker.terminate();
|
|
358
|
-
}
|
|
359
436
|
activation.load.reject(reason);
|
|
360
437
|
for (const operation of activation.pending.values()) operation.reject(reason);
|
|
361
438
|
activation.pending.clear();
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import Is from '../dependencies/strong-type/index.js';
|
|
2
|
+
|
|
3
|
+
const is = new Is(false);
|
|
4
|
+
|
|
5
|
+
function throwIfCancelled(signal) {
|
|
6
|
+
if (signal?.aborted) {
|
|
7
|
+
throw signal.reason ?? new DOMException('Speech synthesis was cancelled.', 'AbortError');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Compose the selected Kokoro public classes without changing loaded instances.
|
|
13
|
+
* The original synthesizer remains the sole owner of its model and disposal.
|
|
14
|
+
*/
|
|
15
|
+
export async function createCompleteKokoroSynthesis({
|
|
16
|
+
KokoroTTS,
|
|
17
|
+
synthesizer,
|
|
18
|
+
repository,
|
|
19
|
+
signal,
|
|
20
|
+
onProgress,
|
|
21
|
+
}) {
|
|
22
|
+
throwIfCancelled(signal);
|
|
23
|
+
const Tokenizer = synthesizer.tokenizer.constructor;
|
|
24
|
+
class CompleteKokoroTokenizer extends Tokenizer {
|
|
25
|
+
_call(text, options = {}) {
|
|
26
|
+
return super._call(text, { ...options, truncation: false });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Kokoro 1.2.1's factory uses the tokenizer's default revision. Reuse that
|
|
30
|
+
// same public loading contract through the existing Worker resource router.
|
|
31
|
+
const tokenizer = await CompleteKokoroTokenizer.from_pretrained(repository, {
|
|
32
|
+
progress_callback: onProgress,
|
|
33
|
+
});
|
|
34
|
+
throwIfCancelled(signal);
|
|
35
|
+
const contextLength = tokenizer.model_max_length;
|
|
36
|
+
if (!is.safeInteger(contextLength) || contextLength < 3) {
|
|
37
|
+
throw new Error('The selected Kokoro tokenizer does not expose its model context length.');
|
|
38
|
+
}
|
|
39
|
+
const contentCapacity = contextLength - 2;
|
|
40
|
+
function boundaryIds(text) {
|
|
41
|
+
const encoded = tokenizer(text, {
|
|
42
|
+
add_special_tokens: false,
|
|
43
|
+
return_tensor: false,
|
|
44
|
+
truncation: false,
|
|
45
|
+
});
|
|
46
|
+
return new Set(encoded.input_ids.map(function tokenId(id) { return BigInt(id); }));
|
|
47
|
+
}
|
|
48
|
+
const punctuation = boundaryIds(';:,.!?—…');
|
|
49
|
+
const spaces = boundaryIds(' ');
|
|
50
|
+
|
|
51
|
+
function segmentEnd(tokens, start, limit, contentEnd) {
|
|
52
|
+
if (limit === contentEnd) return limit;
|
|
53
|
+
for (const boundaries of [punctuation, spaces]) {
|
|
54
|
+
for (let index = limit - 1; index >= start; index -= 1) {
|
|
55
|
+
if (boundaries.has(tokens[index])) return index + 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return limit;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class CompleteKokoroTTS extends KokoroTTS {
|
|
62
|
+
#signal;
|
|
63
|
+
|
|
64
|
+
constructor(requestSignal) {
|
|
65
|
+
super(synthesizer.model, tokenizer);
|
|
66
|
+
this.#signal = requestSignal;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async generate_from_ids(inputIds, options = {}) {
|
|
70
|
+
throwIfCancelled(this.#signal);
|
|
71
|
+
if (inputIds.dims.at(-1) <= contextLength) {
|
|
72
|
+
const output = await super.generate_from_ids(inputIds, options);
|
|
73
|
+
throwIfCancelled(this.#signal);
|
|
74
|
+
return output;
|
|
75
|
+
}
|
|
76
|
+
const Tensor = inputIds.constructor;
|
|
77
|
+
const tokens = inputIds.data;
|
|
78
|
+
const contentEnd = tokens.length - 1;
|
|
79
|
+
const outputs = [];
|
|
80
|
+
let sampleCount = 0;
|
|
81
|
+
for (let start = 1; start < contentEnd;) {
|
|
82
|
+
throwIfCancelled(this.#signal);
|
|
83
|
+
const end = segmentEnd(tokens, start, Math.min(start + contentCapacity, contentEnd), contentEnd);
|
|
84
|
+
// Only the original outer padding positions are replaced. All
|
|
85
|
+
// content tokens, including boundary punctuation/space, survive.
|
|
86
|
+
const data = new BigInt64Array(end - start + 2);
|
|
87
|
+
data[0] = tokens[0];
|
|
88
|
+
data.set(tokens.subarray(start, end), 1);
|
|
89
|
+
data[data.length - 1] = tokens[tokens.length - 1];
|
|
90
|
+
const ids = new Tensor('int64', data, [1, data.length]);
|
|
91
|
+
const output = await super.generate_from_ids(ids, options);
|
|
92
|
+
throwIfCancelled(this.#signal);
|
|
93
|
+
outputs.push(output);
|
|
94
|
+
sampleCount += output.audio.length;
|
|
95
|
+
start = end;
|
|
96
|
+
}
|
|
97
|
+
const audio = new Float32Array(sampleCount);
|
|
98
|
+
let offset = 0;
|
|
99
|
+
for (const output of outputs) {
|
|
100
|
+
audio.set(output.audio, offset);
|
|
101
|
+
offset += output.audio.length;
|
|
102
|
+
}
|
|
103
|
+
throwIfCancelled(this.#signal);
|
|
104
|
+
const RawAudio = outputs[0].constructor;
|
|
105
|
+
return new RawAudio(audio, outputs[0].sampling_rate);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return async function synthesizeCompleteKokoro(text, { voice, speed, signal: requestSignal } = {}) {
|
|
110
|
+
throwIfCancelled(requestSignal);
|
|
111
|
+
// Each invocation retains its own signal while sharing the one loaded
|
|
112
|
+
// model. Inherited generate() still owns all upstream phonemization.
|
|
113
|
+
const request = new CompleteKokoroTTS(requestSignal);
|
|
114
|
+
return request.generate(text, { voice, speed });
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Is from "../dependencies/strong-type/index.js";
|
|
2
2
|
import { createModelResourceClient, createModelResourceHost } from './model-resource-bridge.mjs';
|
|
3
|
+
import { createCompleteKokoroSynthesis } from './kokoro-complete-input.mjs';
|
|
3
4
|
|
|
4
5
|
const is = new Is(false);
|
|
5
6
|
|
|
@@ -1493,54 +1494,70 @@ async function createWhisperEngine(namespace, configuration, signal, report) {
|
|
|
1493
1494
|
}
|
|
1494
1495
|
|
|
1495
1496
|
async function createKokoroEngine(namespace, configuration, signal, report) {
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1497
|
+
if (!is.function(namespace?.KokoroTTS?.from_pretrained)) {
|
|
1498
|
+
throw workerError(
|
|
1499
|
+
'ARCANE_AI_PROVIDER_UNAVAILABLE',
|
|
1500
|
+
'The Kokoro runtime does not export KokoroTTS.',
|
|
1501
|
+
undefined,
|
|
1502
|
+
'kokoro-tts-constructor-export-missing',
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
throwIfAborted(signal, 'tts-load-cancelled');
|
|
1506
|
+
const synthesizer = await namespace.KokoroTTS.from_pretrained(
|
|
1507
|
+
configuration.model.repository,
|
|
1508
|
+
{
|
|
1509
|
+
device: configuration.execution?.device ?? 'wasm',
|
|
1510
|
+
dtype: configuration.model.dtype ?? 'q8',
|
|
1511
|
+
revision: configuration.model.revision,
|
|
1512
|
+
progress_callback: report,
|
|
1513
|
+
},
|
|
1502
1514
|
);
|
|
1503
|
-
|
|
1504
|
-
throwIfAborted(signal, "tts-load-cancelled");
|
|
1505
|
-
const synthesizer = await namespace.KokoroTTS.from_pretrained(
|
|
1506
|
-
configuration.model.repository,
|
|
1507
|
-
{
|
|
1508
|
-
device: configuration.execution?.device ?? "wasm",
|
|
1509
|
-
dtype: configuration.model.dtype ?? "q8",
|
|
1510
|
-
revision: configuration.model.revision,
|
|
1511
|
-
progress_callback: report,
|
|
1512
|
-
},
|
|
1513
|
-
);
|
|
1514
|
-
try {
|
|
1515
|
-
throwIfAborted(signal, "tts-load-cancelled");
|
|
1516
|
-
} catch (error) {
|
|
1515
|
+
let synthesize;
|
|
1517
1516
|
try {
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1517
|
+
throwIfAborted(signal, 'tts-load-cancelled');
|
|
1518
|
+
synthesize = await createCompleteKokoroSynthesis({
|
|
1519
|
+
KokoroTTS: namespace.KokoroTTS,
|
|
1520
|
+
synthesizer,
|
|
1521
|
+
repository: configuration.model.repository,
|
|
1522
|
+
signal,
|
|
1523
|
+
onProgress: report,
|
|
1524
|
+
});
|
|
1525
|
+
throwIfAborted(signal, 'tts-load-cancelled');
|
|
1526
|
+
} catch (error) {
|
|
1527
|
+
try {
|
|
1528
|
+
await disposeEngine(synthesizer);
|
|
1529
|
+
} catch (cleanupError) {
|
|
1530
|
+
throw new AggregateError([error, cleanupError], 'Kokoro preparation and model cleanup failed.', { cause: error });
|
|
1531
|
+
}
|
|
1532
|
+
throwIfAborted(signal, 'tts-load-cancelled');
|
|
1533
|
+
throw error;
|
|
1521
1534
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1535
|
+
return completeValue({
|
|
1536
|
+
async synthesize(input, { signal: requestSignal } = {}) {
|
|
1537
|
+
throwIfAborted(requestSignal, 'tts-synthesis-cancelled');
|
|
1538
|
+
let output;
|
|
1539
|
+
try {
|
|
1540
|
+
output = await synthesize(input.text, {
|
|
1541
|
+
voice: input.voice,
|
|
1542
|
+
speed: input.speed,
|
|
1543
|
+
signal: requestSignal,
|
|
1544
|
+
});
|
|
1545
|
+
} catch (error) {
|
|
1546
|
+
throwIfAborted(requestSignal, 'tts-synthesis-cancelled');
|
|
1547
|
+
throw error;
|
|
1548
|
+
}
|
|
1549
|
+
throwIfAborted(requestSignal, 'tts-synthesis-cancelled');
|
|
1550
|
+
const audio = is.instanceCheck(output?.audio, Float32Array)
|
|
1551
|
+
? output.audio
|
|
1552
|
+
: new Float32Array(output?.audio ?? []);
|
|
1553
|
+
return completeValue({
|
|
1554
|
+
audio,
|
|
1555
|
+
sampleRate: output?.sampling_rate,
|
|
1556
|
+
voice: input.voice,
|
|
1557
|
+
});
|
|
1558
|
+
},
|
|
1559
|
+
dispose: function disposeKokoroEngine() { return disposeEngine(synthesizer); },
|
|
1560
|
+
});
|
|
1544
1561
|
}
|
|
1545
1562
|
|
|
1546
1563
|
function callerVoiceIds(configuration) {
|
package/docs/reference/README.md
CHANGED
|
@@ -43,6 +43,7 @@ alone does not make bare module names resolve in a browser.
|
|
|
43
43
|
| Use the `arcane` command | [CLI reference](cli.md) |
|
|
44
44
|
| Install an app and cache selected resources offline | [Progressive web applications](pwa.md) |
|
|
45
45
|
| Disable source PWA generation or explicitly retire confirmed generated output | [Source PWA retirement](pwa.md#disable-source-pwa-generation-and-retire-generated-output) |
|
|
46
|
+
| Retire the prior worker after explicitly disabling standalone source PWA | [Disabled-worker browser lifecycle](pwa.md#disabled-worker-retirement-on-the-source-server) |
|
|
46
47
|
| Generate named browser imports or inspect the selected physical runtime | [`arcane import-map`](cli.md#arcane-import-map) and [browser runtime delivery](protocols.md#browser-runtime-delivery) |
|
|
47
48
|
| Connect an application through the shared WebSocket transport | [Shared WebSocket clients](websocket-client.md) |
|
|
48
49
|
| Choose browser, native, cloud, or cross-host behavior | [Availability and normalization](availability-and-normalization.md) |
|
|
@@ -68,9 +68,9 @@ 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.moduleUrl` | Defaults to the self-contained CDN entry `https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0/dist/transformers.min.js`. The `.web.js` build expects a bundler to resolve its bare ONNX Runtime import and is unsuitable for a native module Worker. An explicit compatible public runtime URL may be supplied. |
|
|
71
|
+
| `runtime.moduleUrl` | Defaults to the self-contained CDN entry `https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.3.0/dist/transformers.min.js`. The `.web.js` build expects a bundler to resolve its bare ONNX Runtime import and is unsuitable for a native module Worker. An explicit compatible public runtime URL may be supplied. With `store`, the SDK stores and materializes the complete selected entry for that activation. |
|
|
72
72
|
| `runtime.wasmPaths` | Optional upstream ONNX WASM location passed to the selected runtime's public environment API. |
|
|
73
|
-
| `store` | Optional existing SDK DBOPFS model or speech artifact store exposing `fetchResource()`. The store remains outside Worker configuration; the SDK bridges the loader's actual resource requests to it. |
|
|
73
|
+
| `store` | Optional existing SDK DBOPFS model or speech artifact store exposing `fetchResource()`. The store remains outside Worker configuration; the SDK uses it for the runtime entry and bridges the loader's actual resource requests to it. |
|
|
74
74
|
|
|
75
75
|
Without `store`, the runtime, tokenizer and weights use normal upstream loading
|
|
76
76
|
and caching. With `store`, the selected loader still owns filenames and model
|
|
@@ -110,17 +110,27 @@ await decisions.load();
|
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
The same `store` may serve other SDK model clients. The application supplies no
|
|
113
|
-
tokenizer/configuration/graph manifest and performs no file routing.
|
|
114
|
-
|
|
113
|
+
tokenizer/configuration/graph manifest and performs no file routing. Explicit
|
|
114
|
+
activation first opens `runtime.moduleUrl` through `store.fetchResource()`.
|
|
115
|
+
The SDK materializes the complete stored entry as a JavaScript object URL for
|
|
116
|
+
the dedicated Worker, without rewriting its content. The configured source URL
|
|
117
|
+
is retained for later activations, so they reuse the same stored resource rather
|
|
118
|
+
than saving a temporary object URL. Construction still starts no download.
|
|
119
|
+
|
|
120
|
+
Inside the dedicated decision Worker, Transformers.js's public `env.fetch` hook routes the
|
|
115
121
|
actual selected tokenizer, configuration, ONNX graph and external-data requests
|
|
116
122
|
to `store.fetchResource(input, options)`. Its competing browser, custom and
|
|
117
123
|
filesystem model caches are disabled only for this explicit stored mode. The
|
|
118
124
|
default upstream ONNX WASM binary and factory preloads use this same hook.
|
|
119
125
|
Native support-file fetches in the dedicated Worker also use the store. The
|
|
120
|
-
runtime
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
126
|
+
default runtime entry is self-contained. Entry materialization does not rewrite
|
|
127
|
+
dependency imports in custom modules or preserve their original `import.meta.url`
|
|
128
|
+
base. Custom stored entries must work from an object URL; relative imports or
|
|
129
|
+
support-file URLs resolved against that module's original location require a
|
|
130
|
+
compatible self-contained entry. Native absolute ESM dependencies and an upstream
|
|
131
|
+
custom factory import outside its preload path remain browser-owned. No second
|
|
132
|
+
application cache or model substitution is introduced. Without `store`, the
|
|
133
|
+
configured runtime URL keeps its direct native module-loading behavior.
|
|
124
134
|
|
|
125
135
|
The store returns `{file, status, statusText, headers, url, redirected}`. The
|
|
126
136
|
Worker reconstructs the complete response with its actual HTTP status and
|
|
@@ -138,9 +148,14 @@ url}`. `completed` counts closed persistent shards; `total` is `null` until the
|
|
|
138
148
|
resource finishes. This progress carries no network-size, rate or ETA fields.
|
|
139
149
|
Cancellation aborts the owned fetch and joins reader/writer cleanup before the
|
|
140
150
|
resource owner becomes available to a later request. Interrupted closed shards
|
|
141
|
-
are retained.
|
|
142
|
-
|
|
143
|
-
|
|
151
|
+
are retained. Each activation owns its runtime object URL and revokes it after
|
|
152
|
+
terminating the Worker on cancellation, failure, unload or disposal. A cancelled
|
|
153
|
+
runtime-entry fetch is joined before its waiting calls settle; a late response
|
|
154
|
+
cannot activate a replacement Worker. Runtime-entry HTTP failures retain the
|
|
155
|
+
complete stored response in `error.response` for developer diagnostics.
|
|
156
|
+
`unload()` and `dispose()` still acknowledge their lifecycle transition
|
|
157
|
+
synchronously; outstanding `load()`/`evaluate()` calls settle after their
|
|
158
|
+
resource cleanup.
|
|
144
159
|
|
|
145
160
|
## Complete rows and results
|
|
146
161
|
|
|
@@ -1065,11 +1065,32 @@ owners. Only `'plain'` selects exact input; omission and other values retain
|
|
|
1065
1065
|
the existing narration cleanup. Applications never supply the internal
|
|
1066
1066
|
`speechInputPrepared` argument.
|
|
1067
1067
|
|
|
1068
|
-
This selection governs SDK text treatment, not
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1068
|
+
This selection governs SDK text treatment, not pronunciation. The browser Kokoro
|
|
1069
|
+
adapter preserves the selected runtime's own text normalization and phonemization,
|
|
1070
|
+
then uses a genuine tokenizer subclass with truncation disabled. Its Kokoro
|
|
1071
|
+
subclass sends every resulting content token through the public
|
|
1072
|
+
`generate_from_ids()` method. Short inputs retain one inference; longer inputs
|
|
1073
|
+
use ordered segments within the selected engine's inference capacity and return
|
|
1074
|
+
all audio in order. This is token-based engine transport, not a character limit,
|
|
1075
|
+
text estimate, summary, or input discard.
|
|
1076
|
+
|
|
1077
|
+
The [selected Kokoro model](https://huggingface.co/onnx-community/Kokoro-82M-v1.0-ONNX/blob/1939ad2a8e416c0acfeecc08a694d14ef25f2231/README.md#python)
|
|
1078
|
+
accepts 510 content tokens plus its two outer padding positions per inference.
|
|
1079
|
+
Each segment receives that padding independently. Upstream voice-style selection,
|
|
1080
|
+
speed, waveform generation and sample rate remain upstream-owned. The SDK reuses
|
|
1081
|
+
one loaded model; it does not copy the private phonemizer, replace instance methods,
|
|
1082
|
+
load a second model, or import another Transformers runtime. Its tokenizer
|
|
1083
|
+
reconstruction uses the same effective selection as the
|
|
1084
|
+
[Kokoro 1.2.1 factory](https://github.com/hexgrad/kokoro/blob/664c76a704021239ba59c84dcbaa4d3dece01fe9/kokoro.js/src/kokoro.js).
|
|
1085
|
+
That factory ignores a `revision` option; this change does not establish revision
|
|
1086
|
+
pinning that the upstream factory does not implement.
|
|
1087
|
+
|
|
1088
|
+
Cancellation is request-local and checked between inference segments and before
|
|
1089
|
+
returning audio. As with the existing Worker contract, cancellation prevents later
|
|
1090
|
+
work and output while an already-running upstream inference may finish. Unload
|
|
1091
|
+
and disposal retain their existing Worker-termination boundary. Source and
|
|
1092
|
+
controlled-fixture coverage of token/audio preservation are distinct from speech
|
|
1093
|
+
quality or execution evidence for a particular model, browser and device.
|
|
1073
1094
|
|
|
1074
1095
|
## Choose a device or reduce memory use
|
|
1075
1096
|
|
|
@@ -210,6 +210,64 @@ details and [the client contract](core-client.md) for native bridge integration.
|
|
|
210
210
|
|
|
211
211
|
## Launch-time locations
|
|
212
212
|
|
|
213
|
+
An application can select defaults for ordinary double-click launch in its
|
|
214
|
+
descriptor, without a replacement launcher or a required command-line file:
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"native": {
|
|
219
|
+
"launchContext": {
|
|
220
|
+
"sharedHost": {},
|
|
221
|
+
"dispatch": {"destination": "Moon Cheese Receiving Dock"}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
This is the `native` fragment of the application's complete descriptor. The
|
|
228
|
+
optional `native.launchContext` object is copied intact to the portable
|
|
229
|
+
manifest and generated Core entry. Its presence explicitly selects native
|
|
230
|
+
location defaults. `sharedHost:{}` opts into the shared Core endpoint and log;
|
|
231
|
+
omitting `sharedHost` retains stdio-owned Core lifetime. Omitting
|
|
232
|
+
`native.launchContext` retains the previous generated-entry behavior entirely.
|
|
233
|
+
Windows and macOS launchers use the same packaged defaults for native state
|
|
234
|
+
selection, so an ordinary executable launch and external MCP can select the
|
|
235
|
+
same app-owned Core.
|
|
236
|
+
|
|
237
|
+
`resolveNativeLaunchContext({appId,context={}})` from `arcane-os/core/host` is
|
|
238
|
+
the public, synchronous location owner for native applications and external MCP.
|
|
239
|
+
It returns a new complete context and fills only omitted fields:
|
|
240
|
+
|
|
241
|
+
| Field | SDK default |
|
|
242
|
+
|---|---|
|
|
243
|
+
| `stateRoot` on Windows | `%LOCALAPPDATA%/Arcane/<appId>`; standard home `AppData/Local` fallback when the environment value is absent |
|
|
244
|
+
| `stateRoot` on macOS | `~/Library/Application Support/Arcane/<appId>` |
|
|
245
|
+
| `stateRoot` on Linux | Absolute `$XDG_DATA_HOME/Arcane/<appId>`, otherwise `~/.local/share/Arcane/<appId>` |
|
|
246
|
+
| `workspaceRoot` | `<resolved stateRoot>/Workspace` |
|
|
247
|
+
| `sharedHost.endpoint`, only when `sharedHost` is selected | Windows named pipe `\\.\pipe\Arcane-` followed by the URL-encoded complete resolved state path; Linux/macOS socket `<resolved stateRoot>/core.sock` |
|
|
248
|
+
| `sharedHost.logFile`, only when `sharedHost` is selected | `<resolved stateRoot>/Core.log` |
|
|
249
|
+
|
|
250
|
+
Windows/macOS roots retain the existing native host locations. Linux is the
|
|
251
|
+
portable Node host convention; an external composing host supplies its actual
|
|
252
|
+
state root when it selects another location. Android and other host adaptations
|
|
253
|
+
must supply `stateRoot` explicitly and own native IPC/process adaptation.
|
|
254
|
+
The resolver never reads saved preferences, creates directories, changes the
|
|
255
|
+
environment, starts services, or migrates existing data. The generated opted-in
|
|
256
|
+
shared entry creates its selected state directory because its socket/log need
|
|
257
|
+
that parent; application services continue to own workspace I/O. Custom
|
|
258
|
+
endpoint/log parent directories retain the existing shared-host requirements.
|
|
259
|
+
|
|
260
|
+
Explicit `stateRoot`, `workspaceRoot`, endpoint, log filename and additional
|
|
261
|
+
fields remain authored values; the resolver derives omitted paths from the
|
|
262
|
+
absolute selected state location. Relative explicit paths retain the caller's
|
|
263
|
+
working-directory meaning, so UI and MCP callers must select the same location.
|
|
264
|
+
No hashing, app-local OS path composition, directory scanning, or new repository
|
|
265
|
+
location rule is involved. `resolveArcaneDataPaths()` continues to own its
|
|
266
|
+
separate `ArcaneData/Repos` contract. A selected native context needs a nonempty
|
|
267
|
+
string `appId` and state directory; a selected `sharedHost` must be an object.
|
|
268
|
+
Malformed resolver inputs report `TypeError`; ordinary OS endpoint requirements
|
|
269
|
+
remain at the IPC operation.
|
|
270
|
+
|
|
213
271
|
For one app-owned Core shared by the native window and a separate MCP process,
|
|
214
272
|
the launch context may explicitly select `sharedHost:{endpoint,logFile}`. The
|
|
215
273
|
generated native child then connects through a stdio bridge, starting an
|
|
@@ -224,27 +282,36 @@ or `null`; the shared host replays that current listener to a newly attached
|
|
|
224
282
|
macOS bridge, without restarting the server or retaining historical events.
|
|
225
283
|
|
|
226
284
|
The generated Core entry accepts `--arcane-launch-config <path>`. The public
|
|
227
|
-
`readCoreLaunchContext({argv=process.argv.slice(2)}={})`
|
|
228
|
-
`arcane-os/core/host` reads that explicitly selected JSON object.
|
|
285
|
+
`readCoreLaunchContext({argv=process.argv.slice(2),defaults={},appId}={})`
|
|
286
|
+
function in `arcane-os/core/host` reads that explicitly selected JSON object.
|
|
287
|
+
Precedence is packaged `defaults`, then the native host's actual state-root
|
|
288
|
+
argument, then every complete top-level field from the explicit JSON object.
|
|
289
|
+
Nested records replace their default field as a whole; they are not deep-merged.
|
|
290
|
+
An `appId` explicitly opts the result into `resolveNativeLaunchContext`; the
|
|
291
|
+
generated entry supplies it only when `native.launchContext` is present.
|
|
292
|
+
A native host
|
|
229
293
|
can separately supply `--arcane-host-state-root <directory>` for the reader's
|
|
230
294
|
`stateRoot` default. Every field in the explicit JSON object takes precedence
|
|
231
295
|
unchanged, including an explicitly present `stateRoot` with a relative, null or
|
|
232
|
-
other application-owned value
|
|
296
|
+
other application-owned value on the ordinary non-resolving reader. With neither
|
|
297
|
+
argument nor defaults/appId the reader still returns `{}`.
|
|
233
298
|
Missing files, missing argument values and malformed JSON report their actual
|
|
234
299
|
errors; the reader does not search for configuration, resolve or validate the
|
|
235
|
-
state directory, rewrite the file or change the process environment.
|
|
300
|
+
state directory, rewrite the file or change the process environment. The
|
|
301
|
+
explicitly opted-in resolver supplies its documented defaults after this merge.
|
|
236
302
|
|
|
237
303
|
The generated entry supplies an artifact-derived `appRoot` and then applies the
|
|
238
304
|
complete explicit launch context. Every service factory receives that same
|
|
239
305
|
object as its second argument, preserving its authored first argument. A
|
|
240
306
|
launcher can supply the user's selected `workspaceRoot` and writable
|
|
241
307
|
`stateRoot` at launch instead of embedding this machine's paths in an app
|
|
242
|
-
package.
|
|
243
|
-
|
|
308
|
+
package. Explicit product selections take precedence over the opted-in SDK
|
|
309
|
+
location defaults. The SDK chooses no saved preference filename or model. JSON values and additional
|
|
244
310
|
application-owned fields remain unchanged, including an app-selected
|
|
245
311
|
`preferencesFile`. The state directory supplies no preference filename and
|
|
246
312
|
triggers no stored-data discovery or migration. Linux and other composing hosts
|
|
247
|
-
may pass the same separate argument; the reader chooses no platform
|
|
313
|
+
may pass the same separate argument; the ordinary reader chooses no platform
|
|
314
|
+
directory until native resolution is explicitly selected.
|
|
248
315
|
|
|
249
316
|
## Selected local-AI runtimes
|
|
250
317
|
|
|
@@ -146,8 +146,46 @@ events may change; applications own those domain methods and state.
|
|
|
146
146
|
|
|
147
147
|
## Native launch and platforms
|
|
148
148
|
|
|
149
|
-
The generated portable Core entry keeps ordinary stdio behavior unless
|
|
150
|
-
|
|
149
|
+
The generated portable Core entry keeps ordinary stdio behavior unless its
|
|
150
|
+
selected launch context contains `sharedHost`. An application can opt in for
|
|
151
|
+
ordinary packaged executable launch with `native.launchContext:{sharedHost:{}}`
|
|
152
|
+
in its complete descriptor. The SDK supplies the app's platform state root,
|
|
153
|
+
workspace, endpoint and diagnostic log through `resolveNativeLaunchContext`.
|
|
154
|
+
The same public owner is available to external MCP:
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
import {readCoreLaunchContext, connectSharedCoreHost} from 'arcane-os/core/host';
|
|
158
|
+
|
|
159
|
+
const context = await readCoreLaunchContext({
|
|
160
|
+
appId: appDescriptor.id,
|
|
161
|
+
defaults: appDescriptor.native.launchContext
|
|
162
|
+
});
|
|
163
|
+
const connection = await connectSharedCoreHost({
|
|
164
|
+
endpoint: context.sharedHost.endpoint
|
|
165
|
+
});
|
|
166
|
+
// Invoke the application's actual services through connection.client.
|
|
167
|
+
// Disconnect this MCP client without shutting down the application's host.
|
|
168
|
+
await connection.close();
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
This example connects to a running native/headless host. The existing `start`
|
|
172
|
+
option can start the packaged Core entry explicitly when independent MCP-first
|
|
173
|
+
startup is needed; the entry applies those same packaged defaults. Pass the
|
|
174
|
+
same explicit `--arcane-launch-config` file to both entries for user-selected
|
|
175
|
+
locations. A caller that already owns a complete context can use the pure
|
|
176
|
+
`resolveNativeLaunchContext({appId,context})` directly instead of reading argv.
|
|
177
|
+
For MCP-first startup, prepare the returned `context.stateRoot` with Node's
|
|
178
|
+
`mkdir(context.stateRoot,{recursive:true})` before selecting `start`, because
|
|
179
|
+
the parent's startup diagnostic log opens before the headless child runs.
|
|
180
|
+
This uses the resolved directory directly, without duplicating platform path
|
|
181
|
+
rules. Pass `context.sharedHost.logFile` to that existing `start` record and
|
|
182
|
+
the same launch file to the packaged Core command. Custom endpoint/log parent
|
|
183
|
+
directories remain caller-selected.
|
|
184
|
+
See [launch-time locations](core-native-packaging.md#launch-time-locations) for
|
|
185
|
+
the exact platform defaults, precedence, location override and directory rules.
|
|
186
|
+
No app must reconstruct OS directory rules.
|
|
187
|
+
|
|
188
|
+
An explicit launch file can also select the existing full endpoint contract:
|
|
151
189
|
|
|
152
190
|
```json
|
|
153
191
|
{
|
|
@@ -163,8 +201,9 @@ option, the native Core child is a stdio-to-shared-Core bridge. It connects or
|
|
|
163
201
|
launches the same entry with `--arcane-core-headless`; the headless entry claims
|
|
164
202
|
the endpoint before importing services. Closing the native window ends its
|
|
165
203
|
bridge, while the independent Core host remains available. Service options and
|
|
166
|
-
the complete launch context retain their existing values.
|
|
167
|
-
|
|
204
|
+
the complete launch context retain their existing values. Shared mode remains
|
|
205
|
+
explicit: the new descriptor option selects defaults only for that application,
|
|
206
|
+
and the existing no-option stdio path is unchanged.
|
|
168
207
|
|
|
169
208
|
`startSharedCoreBridge({endpoint,start,input=process.stdin,
|
|
170
209
|
output=process.stdout,signal,onError})` is also public. It returns
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"sdkVersion": "0.71.
|
|
3
|
+
"sdkVersion": "0.71.1",
|
|
4
4
|
"environment": {
|
|
5
5
|
"runtime": "Node.js for Node entrypoints; browser for browser-only entrypoints",
|
|
6
6
|
"minimumVersion": "22.23.2 for Node entrypoints",
|
|
@@ -1185,7 +1185,7 @@
|
|
|
1185
1185
|
],
|
|
1186
1186
|
"primaryImport": "arcane-os",
|
|
1187
1187
|
"group": "Headless toolchain operations",
|
|
1188
|
-
"summary": "Starts one owned browser development server for the selected application.",
|
|
1188
|
+
"summary": "Starts one owned browser development server for the selected application, including native worker retirement when standalone source PWA is explicitly disabled and its root worker is absent.",
|
|
1189
1189
|
"availability": "Node; selected operation may produce browser or native output",
|
|
1190
1190
|
"protocol": "Shared headless operation API + arcane-cli-events/1",
|
|
1191
1191
|
"normalization": "SDK-normalized inputs, errors, events, and documented result"
|
|
@@ -2446,7 +2446,7 @@
|
|
|
2446
2446
|
],
|
|
2447
2447
|
"primaryImport": "arcane-os",
|
|
2448
2448
|
"group": "Workspace, doctor, repository, and server",
|
|
2449
|
-
"summary": "Starts an owned source or packaged browser server with complete selected routes, HTTPS by default, explicit HTTP selection, listener readiness and shutdown.",
|
|
2449
|
+
"summary": "Starts an owned source or packaged browser server with complete selected routes, HTTPS by default, explicit HTTP selection, listener readiness and shutdown; explicitly disabled standalone source PWA retires an absent root worker through its native lifecycle.",
|
|
2450
2450
|
"availability": "Node control plane; browser data plane",
|
|
2451
2451
|
"protocol": "Node ESM",
|
|
2452
2452
|
"normalization": "SDK-normalized inputs, errors, events, and documented result"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.72.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",
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
"test": "npm run test:unit && npm run test:functional && npm run test:integration && npm run test:regression",
|
|
124
124
|
"test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
|
|
125
125
|
"test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/app-selection.test.mjs test/contracts.test.mjs test/conversation-closing-report.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/native-windows-icon.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
|
|
126
|
-
"test:functional": "node ./bin/arcane-test.mjs test/ai-ollama-readiness.test.mjs test/core-ollama-lifecycle.test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-decisions.test.mjs test/browser-speech-providers.test.mjs test/browser-speech-artifacts.test.mjs test/browser-speech-resource-bridge.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/codex-app-server.test.mjs test/mcp-stdio-server.test.mjs test/core-classic-source.test.mjs test/core-client.test.mjs test/core-client-installation.test.mjs test/core-ai-installation.test.mjs test/core-llama-lifecycle.test.mjs test/core-development.test.mjs test/core-host.test.mjs test/core-shared-host.test.mjs test/core-model-assets.test.mjs test/core-native-services.test.mjs test/core-packaged-web.test.mjs test/core-runtime.test.mjs test/core-stdio.test.mjs test/dbopfs-committed-events.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-model-resources.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/local-image-editing.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/model-resource-bridge.test.mjs test/native-decisions.test.mjs test/native-onnx.test.mjs test/native-module-packaging.test.mjs test/native-module-resource-references.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/rendered-print.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/theme-manager.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/websocket-client-import-map.test.mjs test/websocket.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
|
|
126
|
+
"test:functional": "node ./bin/arcane-test.mjs test/ai-ollama-readiness.test.mjs test/core-ollama-lifecycle.test.mjs test/ai-request-model.test.mjs test/ai-request-retry.test.mjs test/ai-stream-cancellation.test.mjs test/browser-decisions.test.mjs test/browser-kokoro-complete-input.test.mjs test/browser-speech-providers.test.mjs test/browser-speech-artifacts.test.mjs test/browser-speech-resource-bridge.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/chat-input.test.mjs test/chat-stream-speech.test.mjs test/chat-transcript-scroll.test.mjs test/chat-transient-retention.test.mjs test/cli.test.mjs test/continuous-voice-capture.test.mjs test/codex-app-server.test.mjs test/mcp-stdio-server.test.mjs test/core-classic-source.test.mjs test/core-client.test.mjs test/core-client-installation.test.mjs test/core-ai-installation.test.mjs test/core-llama-lifecycle.test.mjs test/core-development.test.mjs test/core-host.test.mjs test/core-shared-host.test.mjs test/core-model-assets.test.mjs test/core-native-services.test.mjs test/core-packaged-web.test.mjs test/core-runtime.test.mjs test/core-stdio.test.mjs test/dbopfs-committed-events.test.mjs test/dbopfs-document-library.test.mjs test/dbopfs-model-resources.test.mjs test/dbopfs-png.test.mjs test/dbopfs.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/file-preview.test.mjs test/import-map.test.mjs test/local-image-editing.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/markdown-editor.test.mjs test/markdown-media.test.mjs test/markdown-source-map.test.mjs test/modal.test.mjs test/model-resource-bridge.test.mjs test/native-decisions.test.mjs test/native-onnx.test.mjs test/native-module-packaging.test.mjs test/native-module-resource-references.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-install.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/rendered-print.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/theme-manager.test.mjs test/twin-cloud-images.test.mjs test/twin-cloud.test.mjs test/websocket-client-import-map.test.mjs test/websocket.test.mjs test/update-check.test.mjs test/user-explicit-updates.test.mjs",
|
|
127
127
|
"test:integration": "node ./bin/arcane-test.mjs test/installed-package-runtime.test.mjs test/root-app-layout.test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-macos-provider.test.mjs test/native-plan.test.mjs test/native-portable-provider.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
|
|
128
128
|
"test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/html-import-packaged-script.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
|
|
129
129
|
"check": "node tools/check-source.mjs && npm test",
|
|
@@ -132,6 +132,10 @@
|
|
|
132
132
|
}
|
|
133
133
|
},
|
|
134
134
|
"localAI": { "$ref": "#/$defs/localAI" },
|
|
135
|
+
"launchContext": {
|
|
136
|
+
"type": "object",
|
|
137
|
+
"description": "Optional complete native launch defaults. Presence opts into SDK per-app native locations; sharedHost:{} explicitly selects the shared endpoint. Explicit launch JSON fields take precedence unchanged."
|
|
138
|
+
},
|
|
135
139
|
"window": {
|
|
136
140
|
"type": "object",
|
|
137
141
|
"additionalProperties": false,
|
package/src/app-descriptor.mjs
CHANGED
|
@@ -254,7 +254,7 @@ function validateNativeWindow(value){
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
function validateNative(value,appId){
|
|
257
|
-
assertOnlyKeys(value,new Set(['type','icon','order','bundledApps','documentCatalog','localAI','services','window']),'descriptor.native');
|
|
257
|
+
assertOnlyKeys(value,new Set(['type','icon','order','bundledApps','documentCatalog','localAI','services','window','launchContext']),'descriptor.native');
|
|
258
258
|
if(!NATIVE_TYPES.has(value.type))fail('descriptor.native.type is unsupported.');
|
|
259
259
|
const icon=value.icon===null?null:normalizeRelativePath(value.icon,'descriptor.native.icon');
|
|
260
260
|
if(!is.integer(value.order)||value.order<0){
|
|
@@ -265,6 +265,9 @@ function validateNative(value,appId){
|
|
|
265
265
|
});
|
|
266
266
|
const documentCatalog=validateDocumentCatalog(value.documentCatalog);
|
|
267
267
|
const window=validateNativeWindow(value.window);
|
|
268
|
+
if(value.launchContext!==undefined&&!isObject(value.launchContext)){
|
|
269
|
+
fail('descriptor.native.launchContext must be an object.');
|
|
270
|
+
}
|
|
268
271
|
let services;
|
|
269
272
|
if(value.services!==undefined){
|
|
270
273
|
if(!is.array(value.services))fail('descriptor.native.services must be an array.');
|
|
@@ -279,6 +282,7 @@ function validateNative(value,appId){
|
|
|
279
282
|
order:value.order,
|
|
280
283
|
bundledApps,
|
|
281
284
|
...(window===undefined?{}:{window}),
|
|
285
|
+
...(value.launchContext===undefined?{}:{launchContext:value.launchContext}),
|
|
282
286
|
...(services===undefined?{}:{services}),
|
|
283
287
|
...(value.localAI===undefined?{}:{localAI:normalizeLocalAIConfig(value.localAI)}),
|
|
284
288
|
...(documentCatalog?{documentCatalog}:{})
|
|
@@ -416,6 +420,7 @@ export function projectNativeDescriptor(descriptor,{source}={}){
|
|
|
416
420
|
...(value.native.documentCatalog?{documentCatalog:value.native.documentCatalog}:{}),
|
|
417
421
|
...(value.native.services===undefined?{}:{services:value.native.services}),
|
|
418
422
|
...(value.native.window===undefined?{}:{window:value.native.window}),
|
|
423
|
+
...(value.native.launchContext===undefined?{}:{launchContext:value.native.launchContext}),
|
|
419
424
|
...(value.native.localAI===undefined?{}:{localAI:value.native.localAI}),
|
|
420
425
|
include:[...value.package.include]
|
|
421
426
|
};
|
|
@@ -462,6 +467,7 @@ function synthesizedDescriptor(packageManifest,nativeDescriptor){
|
|
|
462
467
|
bundledApps:[...(native.bundledApps??[])],
|
|
463
468
|
...(native.services===undefined?{}:{services:native.services}),
|
|
464
469
|
...(native.window===undefined?{}:{window:native.window}),
|
|
470
|
+
...(native.launchContext===undefined?{}:{launchContext:native.launchContext}),
|
|
465
471
|
...((packageManifest.localAI??native.localAI)===undefined?{}:{localAI:packageManifest.localAI??native.localAI}),
|
|
466
472
|
...(native.documentCatalog?{documentCatalog:native.documentCatalog}:{})
|
|
467
473
|
},
|
package/src/core/host.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {readFile} from 'node:fs/promises';
|
|
2
|
+
import {homedir} from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import Is from 'strong-type';
|
|
3
5
|
import {createCoreRuntime} from './runtime.mjs';
|
|
4
6
|
import {startCoreStdio} from './stdio.mjs';
|
|
@@ -7,32 +9,90 @@ export {startSharedCoreHost, connectSharedCoreHost, runSharedCoreHost, startShar
|
|
|
7
9
|
|
|
8
10
|
const is = new Is(false);
|
|
9
11
|
|
|
12
|
+
/** Resolve one app's native locations without creating or migrating data. */
|
|
13
|
+
export function resolveNativeLaunchContext({appId, context = {}} = {}) {
|
|
14
|
+
if (!is.string(appId) || !appId) throw new TypeError('Native launch resolution requires an appId.');
|
|
15
|
+
if (context === null || !is.object(context) || is.array(context)) {
|
|
16
|
+
throw new TypeError('Native launch context must be an object.');
|
|
17
|
+
}
|
|
18
|
+
let stateRoot = context.stateRoot;
|
|
19
|
+
if (stateRoot === undefined) {
|
|
20
|
+
const home = homedir();
|
|
21
|
+
switch (process.platform) {
|
|
22
|
+
case 'win32':
|
|
23
|
+
stateRoot = path.join(process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'Arcane', appId);
|
|
24
|
+
break;
|
|
25
|
+
case 'darwin':
|
|
26
|
+
stateRoot = path.join(home, 'Library', 'Application Support', 'Arcane', appId);
|
|
27
|
+
break;
|
|
28
|
+
case 'linux': {
|
|
29
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
30
|
+
stateRoot = path.join(xdg && path.isAbsolute(xdg) ? xdg : path.join(home, '.local', 'share'), 'Arcane', appId);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
default:
|
|
34
|
+
throw new TypeError('This native host must supply its persistent stateRoot.');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!is.string(stateRoot) || !stateRoot) throw new TypeError('Native stateRoot must be a nonempty directory path.');
|
|
38
|
+
const root = path.resolve(stateRoot);
|
|
39
|
+
const resolved = {
|
|
40
|
+
...context,
|
|
41
|
+
stateRoot,
|
|
42
|
+
workspaceRoot: context.workspaceRoot === undefined ? path.join(root, 'Workspace') : context.workspaceRoot
|
|
43
|
+
};
|
|
44
|
+
if (context.sharedHost !== undefined) {
|
|
45
|
+
if (context.sharedHost === null || !is.object(context.sharedHost) || is.array(context.sharedHost)) {
|
|
46
|
+
throw new TypeError('Native sharedHost must be an object.');
|
|
47
|
+
}
|
|
48
|
+
// Encode the complete selected Windows path as a pipe-name component;
|
|
49
|
+
// Unix IPC uses an ordinary file in that same persistent app directory.
|
|
50
|
+
const endpoint = process.platform === 'win32'
|
|
51
|
+
? '\\\\.\\pipe\\Arcane-' + encodeURIComponent(root)
|
|
52
|
+
: path.join(root, 'core.sock');
|
|
53
|
+
resolved.sharedHost = {
|
|
54
|
+
...context.sharedHost,
|
|
55
|
+
endpoint: context.sharedHost.endpoint === undefined ? endpoint : context.sharedHost.endpoint,
|
|
56
|
+
logFile: context.sharedHost.logFile === undefined ? path.join(root, 'Core.log') : context.sharedHost.logFile
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return resolved;
|
|
60
|
+
}
|
|
61
|
+
|
|
10
62
|
/**
|
|
11
|
-
*
|
|
63
|
+
* Packaged defaults precede the host's actual state-root argument.
|
|
12
64
|
* Every field in an explicitly selected launch JSON object takes precedence
|
|
13
65
|
* unchanged; field meanings belong to the launch owner and individual services.
|
|
66
|
+
* appId explicitly selects native location resolution; ordinary calls keep the
|
|
67
|
+
* existing read-only launch-file contract without choosing any locations.
|
|
14
68
|
*/
|
|
15
|
-
export async function readCoreLaunchContext({argv = process.argv.slice(2)} = {}) {
|
|
69
|
+
export async function readCoreLaunchContext({argv = process.argv.slice(2), defaults = {}, appId} = {}) {
|
|
70
|
+
if (defaults === null || !is.object(defaults) || is.array(defaults)) {
|
|
71
|
+
throw new TypeError('Core launch defaults must be an object.');
|
|
72
|
+
}
|
|
16
73
|
const stateFlag = argv.indexOf('--arcane-host-state-root');
|
|
17
|
-
const
|
|
74
|
+
const context = {...defaults};
|
|
18
75
|
if (stateFlag !== -1) {
|
|
19
76
|
const stateRoot = argv[stateFlag + 1];
|
|
20
77
|
if (stateRoot === undefined || stateRoot === '') {
|
|
21
78
|
throw new TypeError('--arcane-host-state-root requires a directory.');
|
|
22
79
|
}
|
|
23
|
-
|
|
80
|
+
context.stateRoot = stateRoot;
|
|
24
81
|
}
|
|
25
82
|
const flag = argv.indexOf('--arcane-launch-config');
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
83
|
+
let selected = context;
|
|
84
|
+
if (flag !== -1) {
|
|
85
|
+
const filename = argv[flag + 1];
|
|
86
|
+
if (filename === undefined || filename === '') {
|
|
87
|
+
throw new TypeError('--arcane-launch-config requires a filename.');
|
|
88
|
+
}
|
|
89
|
+
const explicit = JSON.parse(await readFile(filename, 'utf8'));
|
|
90
|
+
if (explicit === null || !is.object(explicit) || is.array(explicit)) {
|
|
91
|
+
throw new TypeError('Core launch configuration must be a JSON object.');
|
|
92
|
+
}
|
|
93
|
+
selected = {...context, ...explicit};
|
|
34
94
|
}
|
|
35
|
-
return {
|
|
95
|
+
return appId === undefined ? selected : resolveNativeLaunchContext({appId, context: selected});
|
|
36
96
|
}
|
|
37
97
|
|
|
38
98
|
/** Compose app services with the shared dispatcher and draining transport. */
|
|
@@ -231,14 +231,25 @@ typedef NS_ENUM(NSUInteger, ArcaneDiagnosticStream) {
|
|
|
231
231
|
NSString *appID = ArcaneRequiredString(app, @"id", error);
|
|
232
232
|
if (!appID) return NO;
|
|
233
233
|
_title = [app[@"displayName"] isKindOfClass:NSString.class] ? app[@"displayName"] : appID;
|
|
234
|
+
id defaults = manifest[@"launchContext"];
|
|
235
|
+
if (defaults && ![defaults isKindOfClass:NSDictionary.class]) {
|
|
236
|
+
*error = ArcaneLauncherError(@"CORE_LAUNCH_MANIFEST_INVALID", @"The native launchContext must be an object.", defaults);
|
|
237
|
+
return NO;
|
|
238
|
+
}
|
|
239
|
+
NSMutableDictionary *launchContext = defaults ? [defaults mutableCopy] : [NSMutableDictionary dictionary];
|
|
234
240
|
if (_launchFilename) {
|
|
235
|
-
|
|
236
|
-
if (!
|
|
241
|
+
NSDictionary *explicitContext = ArcaneReadRecord([NSURL fileURLWithPath:_launchFilename], error);
|
|
242
|
+
if (!explicitContext) return NO;
|
|
243
|
+
[launchContext addEntriesFromDictionary:explicitContext];
|
|
237
244
|
}
|
|
245
|
+
_launchContext = launchContext;
|
|
246
|
+
// The opted-in Node resolver uses HOME when supplied; retain the previous
|
|
247
|
+
// native default unchanged for applications without launchContext.
|
|
248
|
+
NSString *nativeHome = defaults ? (NSProcessInfo.processInfo.environment[@"HOME"] ?: NSHomeDirectory()) : NSHomeDirectory();
|
|
238
249
|
id selectedState = _launchContext[@"stateRoot"];
|
|
239
250
|
_stateRoot = [selectedState isKindOfClass:NSString.class]
|
|
240
251
|
? ArcaneAbsolutePath(selectedState, _workingDirectory)
|
|
241
|
-
: [[
|
|
252
|
+
: [[nativeHome stringByAppendingPathComponent:@"Library/Application Support/Arcane"] stringByAppendingPathComponent:appID];
|
|
242
253
|
[self scheduleDiagnostics];
|
|
243
254
|
NSString *clientPath = ArcaneRequiredString(client, @"source", error);
|
|
244
255
|
if (!clientPath) return NO;
|
|
@@ -57,13 +57,33 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
57
57
|
Dictionary<string, object> host = manifest.TryGetValue("host", out hostValue) ? hostValue as Dictionary<string, object> : null;
|
|
58
58
|
string icon = host != null && host.TryGetValue("icon", out iconValue) ? iconValue as string : null;
|
|
59
59
|
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
Dictionary<string, object> launchContext =
|
|
60
|
+
// Native UI and generated Core use the same packaged defaults.
|
|
61
|
+
// Core rereads the explicit file; every explicit field wins.
|
|
62
|
+
Dictionary<string, object> launchContext = manifest.ContainsKey("launchContext")
|
|
63
|
+
? new Dictionary<string, object>(RequiredRecord(manifest, "launchContext"))
|
|
64
|
+
: new Dictionary<string, object>();
|
|
65
|
+
if (launchFilename != null)
|
|
66
|
+
{
|
|
67
|
+
foreach (KeyValuePair<string, object> field in ReadRecord(launchFilename))
|
|
68
|
+
launchContext[field.Key] = field.Value;
|
|
69
|
+
}
|
|
70
|
+
string defaultStateBase = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
|
71
|
+
if (manifest.ContainsKey("launchContext"))
|
|
72
|
+
{
|
|
73
|
+
// Match the public Node resolver for this opt-in contract,
|
|
74
|
+
// including an explicitly selected process environment.
|
|
75
|
+
defaultStateBase = Environment.GetEnvironmentVariable("LOCALAPPDATA");
|
|
76
|
+
if (String.IsNullOrEmpty(defaultStateBase))
|
|
77
|
+
{
|
|
78
|
+
string home = Environment.GetEnvironmentVariable("USERPROFILE");
|
|
79
|
+
if (String.IsNullOrEmpty(home)) home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
|
80
|
+
defaultStateBase = Path.Combine(home, "AppData", "Local");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
63
83
|
object stateRoot;
|
|
64
|
-
string profileParent = launchContext
|
|
84
|
+
string profileParent = launchContext.TryGetValue("stateRoot", out stateRoot) && stateRoot is string
|
|
65
85
|
? Path.GetFullPath((string)stateRoot)
|
|
66
|
-
: Path.Combine(
|
|
86
|
+
: Path.Combine(defaultStateBase, "Arcane", appId);
|
|
67
87
|
OpenDiagnostics(Path.Combine(profileParent, "Diagnostics"));
|
|
68
88
|
ArcaneHostOptions options = new ArcaneHostOptions
|
|
69
89
|
{
|
|
@@ -151,6 +151,7 @@ export async function copyCoreRuntime(root, files, signal, {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
export function coreEntrySource(application, version, services, {localAI, runtimes = [], packagedWeb = false} = {}) {
|
|
154
|
+
const launchDefaults = application.native?.launchContext;
|
|
154
155
|
const imageSelected = localAI?.runtimes?.some(function selectedImage(requirement) {
|
|
155
156
|
return (typeof requirement === 'string' ? requirement : requirement.id) === 'stable-diffusion.cpp';
|
|
156
157
|
}) ?? false;
|
|
@@ -199,6 +200,7 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
199
200
|
"import {fileURLToPath} from 'node:url';",
|
|
200
201
|
"import path from 'node:path';",
|
|
201
202
|
"import {isSea} from 'node:sea';",
|
|
203
|
+
...(launchDefaults === undefined ? [] : ["import {mkdir} from 'node:fs/promises';"]),
|
|
202
204
|
"import {readCoreLaunchContext, startCoreHost, runSharedCoreHost, startSharedCoreBridge} from 'arcane-os/core/host';",
|
|
203
205
|
'',
|
|
204
206
|
...(localAI === undefined ? [] : [
|
|
@@ -241,9 +243,15 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
241
243
|
' return resolved;',
|
|
242
244
|
' }),'
|
|
243
245
|
]),
|
|
244
|
-
' ...await readCoreLaunchContext()'
|
|
246
|
+
launchDefaults === undefined ? ' ...await readCoreLaunchContext()'
|
|
247
|
+
: ` ...await readCoreLaunchContext({appId: ${JSON.stringify(application.id)}, defaults: JSON.parse(${JSON.stringify(JSON.stringify(launchDefaults))})})`,
|
|
245
248
|
'};',
|
|
246
249
|
'',
|
|
250
|
+
...(launchDefaults === undefined ? [] : [
|
|
251
|
+
'// The shared endpoint and diagnostic log require their selected state directory.',
|
|
252
|
+
'if (context.sharedHost !== undefined) await mkdir(context.stateRoot, {recursive: true});',
|
|
253
|
+
''
|
|
254
|
+
]),
|
|
247
255
|
...(packagedWeb ? ['let packagedWeb;', ''] : []),
|
|
248
256
|
'async function createServices(register) {',
|
|
249
257
|
` const [${imports.map(function binding(selection) { return selection.binding; }).join(', ')}] = await Promise.all([`,
|
|
@@ -197,6 +197,7 @@ export function createPortableNativeProvider({services, packagedWeb = false, web
|
|
|
197
197
|
webRoot: 'app',
|
|
198
198
|
start: release.manifest?.app?.start ?? './index.html',
|
|
199
199
|
...(appDescriptor.native?.window === undefined ? {} : {window: {...appDescriptor.native.window}}),
|
|
200
|
+
...(appDescriptor.native?.launchContext === undefined ? {} : {launchContext: appDescriptor.native.launchContext}),
|
|
200
201
|
core: {
|
|
201
202
|
entry: 'runtime/arcane-core.mjs', transport: 'stdio', protocol: 'arcane/1', services: selectedServices,
|
|
202
203
|
...(localAI === undefined ? {} : {localAIRuntimes: bundled.runtimes})
|