arcane-os 0.57.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +6 -2
  3. package/browser-runtime/ai/core-onnx.mjs +96 -0
  4. package/browser-runtime/ai/onnx-tensors.mjs +85 -0
  5. package/browser-runtime/core/client.mjs +152 -17
  6. package/docs/architecture.md +5 -2
  7. package/docs/platform-targets.md +30 -13
  8. package/docs/reference/README.md +22 -3
  9. package/docs/reference/availability-and-normalization.md +14 -7
  10. package/docs/reference/cli.md +22 -10
  11. package/docs/reference/core-client.md +87 -0
  12. package/docs/reference/core-native-packaging.md +129 -9
  13. package/docs/reference/core-preferences.md +49 -0
  14. package/docs/reference/diarization.md +206 -0
  15. package/docs/reference/git-text-snapshot.md +104 -0
  16. package/docs/reference/inventory/package-api.json +205 -23
  17. package/docs/reference/local-ai.md +145 -11
  18. package/docs/reference/protocols.md +28 -19
  19. package/docs/reference/pwa.md +74 -4
  20. package/docs/reference/sdk-api.md +589 -39
  21. package/package.json +11 -3
  22. package/schemas/arcane-app.schema.json +3 -3
  23. package/schemas/arcane-package.schema.json +2 -2
  24. package/schemas/native-build-plan.schema.json +27 -1
  25. package/schemas/target-adapter.schema.json +2 -0
  26. package/src/app-descriptor.mjs +1 -1
  27. package/src/cli/main.mjs +14 -4
  28. package/src/constants.mjs +2 -0
  29. package/src/core/host.mjs +14 -5
  30. package/src/core/hosts/macos/ArcaneHost.h +86 -0
  31. package/src/core/hosts/macos/ArcaneHost.m +455 -0
  32. package/src/core/hosts/macos/ArcaneLauncher.m +432 -0
  33. package/src/core/hosts/windows/ArcaneLauncher.cs +4 -1
  34. package/src/core/services/local-ai.mjs +82 -4
  35. package/src/core/services/packaged-web.mjs +89 -0
  36. package/src/core/services/preferences.mjs +201 -0
  37. package/src/core-classic-source.mjs +2 -2
  38. package/src/dev-server.mjs +2 -5
  39. package/src/diarization/build.mjs +35 -0
  40. package/src/diarization/index.mjs +432 -0
  41. package/src/diarization/native/CMakeLists.txt +9 -0
  42. package/src/diarization/native/main.cpp +269 -0
  43. package/src/diarization/service.mjs +129 -0
  44. package/src/git-text-snapshot.mjs +265 -0
  45. package/src/import-map.mjs +9 -4
  46. package/src/index.mjs +1 -0
  47. package/src/local-ai/config.mjs +1 -1
  48. package/src/local-ai/install.mjs +134 -19
  49. package/src/local-ai/onnx-worker.mjs +85 -0
  50. package/src/local-ai/onnx.mjs +368 -0
  51. package/src/native/macos-provider.mjs +291 -0
  52. package/src/native/portable-layout.mjs +10 -2
  53. package/src/native/portable-provider.mjs +10 -5
  54. package/src/native-plan.mjs +9 -1
  55. package/src/native-provider-loader.mjs +3 -1
  56. package/src/process.mjs +123 -17
  57. package/src/scaffold.mjs +1 -1
  58. package/src/targets/index.mjs +11 -1
  59. package/src/templates/workspace-template.mjs +6 -5
  60. package/src/toolchain.mjs +42 -13
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.59.0
4
+
5
+ - Reconcile marked generated PWA references when a root application's PWA is
6
+ disabled. Add explicit `importMap({retireGeneratedPwa:true})` retirement for
7
+ the caller-confirmed generated root manifest reference and four standard PWA
8
+ output files. Ordinary disabled refresh preserves unmarked references and
9
+ files; authored content, unselected pages and browser storage remain intact.
10
+ - Forward the native host's selected state directory to Core service factories
11
+ as a launch-context default. Every explicit launch JSON field takes precedence
12
+ unchanged. Windows launcher source now supplies the same separate argument;
13
+ using that launcher correction requires the matching rebuilt host archive.
14
+ - Forward optional `Arcane.speech.synthesize(request, {signal})` cancellation
15
+ through the existing request lifetime, preserving complete payloads and the
16
+ 180,000 ms default. Actual engine interruption remains service-owned; no new
17
+ speech engine, runtime dependency or data migration is introduced.
18
+
19
+ ## 0.58.0
20
+
21
+ - Add native ONNX sessions through `arcane-os/local-ai/onnx` and browser Core
22
+ access through `arcane-os/ai/core-onnx`. Explicit runtime selection uses the
23
+ upstream Node runtime distribution, retained per-session workers, complete
24
+ tensor transport, authoritative loaded state and owned cancellation/exit.
25
+ Apps select their graphs, feeds and processing; no model is chosen or fetched.
26
+ - Add native Nemotron diarization runtime preparation, helper build and Core
27
+ integration. Preserve complete framed input/output, independent streams,
28
+ cancellation and process drain. Runtime/model selection remains explicit.
29
+ - Add the reusable Core preferences service and Git text snapshots, preserving
30
+ complete caller data and existing application-owned storage and repository
31
+ choices. Extend raw process I/O without changing existing UTF-8 defaults.
32
+ - Add shared WebKit document lifecycle and macOS native host composition source,
33
+ portable target contracts, and explicit packaged HTTP serving through the
34
+ existing HTTP server. Default HTTPS and ordinary browser behavior remain.
35
+ macOS executable build and platform acceptance remain separately pending.
36
+ - No automatic runtime/model installation, application default switch or saved
37
+ data migration is introduced. Native host archive delivery is separate from
38
+ npm publication; this release does not claim an available Windows/Mac archive.
39
+
3
40
  ## 0.57.0
4
41
 
5
42
  - Add local model chat through explicit llama.cpp/Ollama runtime requirements,
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.57.0` SDK contract. Development applications declare
23
+ This checkout defines the `0.59.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;
@@ -131,7 +131,7 @@ each device. Keep these local files ignored by Git. The same certificate pair
131
131
  works for any selected app in that workspace. The command reports missing TLS
132
132
  files instead of starting an HTTP listener.
133
133
 
134
- HTTPS is the default for development and required for packaged browser previews.
134
+ HTTPS is the default for development and packaged browser previews.
135
135
  Plain `npm run dev` binds to localhost with the same workspace certificate pair.
136
136
  An explicit `--host` overrides the bind address, and `--port` selects the HTTPS port.
137
137
  The paired HTTP listener returns 308 redirects and uses an OS-assigned port
@@ -152,6 +152,10 @@ This selects one HTTP content listener and prints HTTP network URLs. It reads
152
152
  no certificate pair and serves the same application and generated PWA routes.
153
153
  PWA and storage availability still depend on the browser's secure-context
154
154
  rules; see [HTTP development](docs/reference/cli.md#explicit-http-development).
155
+ API callers may also explicitly select `http:true` with `mode:'packaged'` to
156
+ serve the selected packaged web root through the same listener owner. See
157
+ [`startDevServer()`](docs/reference/sdk-api.md#startdevserver) for readiness,
158
+ start-URL and shutdown behavior.
155
159
 
156
160
  Open the URL printed by the server. The generated page owns its import map and
157
161
  Arcane theme; its application module is `modules/App.js` at the repository root.
@@ -0,0 +1,96 @@
1
+ import {getInstalledCoreClient} from '../core/client.mjs';
2
+ import {createArcaneEventSource} from '../event-manager.mjs';
3
+ import {encodeTensorMap, decodeTensorMap, encodeTensorFetches} from './onnx-tensors.mjs';
4
+
5
+ function unavailable(message) {
6
+ const error = new Error(message);
7
+ error.code = 'ARCANE_ONNX_CORE_UNAVAILABLE';
8
+ return error;
9
+ }
10
+
11
+ /** Optional system ONNX access. Construction neither connects nor installs. */
12
+ export function createCoreONNXRuntime({client = getInstalledCoreClient()} = {}) {
13
+ const owner = {};
14
+ const events = createArcaneEventSource(owner, {source: 'core-onnx', eventTypes: ['onnx.state']});
15
+ const lifetime = new AbortController();
16
+ const pending = new Set();
17
+ let state = {id: 'onnx', installed: false, available: false, state: 'unknown', models: []};
18
+ let revision = 0;
19
+ let closed = false;
20
+
21
+ function current() { return {...state, busy: pending.size > 0, closed}; }
22
+ function publish() { events.dispatch('onnx.state', current()); }
23
+ function accept(value) {
24
+ state = value ?? {id: 'onnx', installed: false, available: false, state: 'unavailable', models: []};
25
+ revision += 1;
26
+ publish();
27
+ }
28
+
29
+ const unsubscribe = client?.events.on('localai.state', function localStateChanged(snapshot) {
30
+ if (!closed) accept(snapshot?.runtimes?.find(function onnxRuntime(runtime) { return runtime.id === 'onnx'; }));
31
+ }) ?? function noCoreSubscription() {};
32
+
33
+ async function invoke(method, parameters, {signal, timeoutMs = 0} = {}) {
34
+ if (closed) throw unavailable('The Core ONNX accessor is closed.');
35
+ if (!client) throw unavailable('Local ONNX requires an available Core connection.');
36
+ const request = {};
37
+ const operationSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
38
+ operationSignal.throwIfAborted();
39
+ pending.add(request);
40
+ publish();
41
+ try {
42
+ return await client.invoke(method, parameters, {signal: operationSignal, timeoutMs});
43
+ } finally {
44
+ pending.delete(request);
45
+ if (!closed) publish();
46
+ }
47
+ }
48
+
49
+ async function inspect(options) {
50
+ const before = revision;
51
+ try {
52
+ const result = await invoke('onnx.status', {}, options);
53
+ if (!closed && revision === before) accept(result);
54
+ return current();
55
+ } catch (error) {
56
+ if (!closed && !options?.signal?.aborted && revision === before) {
57
+ accept({...state, available: false, state: 'unavailable', error});
58
+ }
59
+ throw error;
60
+ }
61
+ }
62
+
63
+ async function load({id, model, sessionOptions, signal, timeoutMs} = {}) {
64
+ return invoke('onnx.load', {id, model, sessionOptions}, {signal, timeoutMs});
65
+ }
66
+
67
+ async function run({id, feeds, fetches, runOptions, signal, timeoutMs} = {}) {
68
+ const result = await invoke('onnx.run', {
69
+ id, feeds: encodeTensorMap(feeds),
70
+ fetches: encodeTensorFetches(fetches), runOptions
71
+ }, {signal, timeoutMs});
72
+ return decodeTensorMap(result);
73
+ }
74
+
75
+ function unload({id, signal, timeoutMs} = {}) {
76
+ return invoke('onnx.unload', {id}, {signal, timeoutMs});
77
+ }
78
+
79
+ function subscribe(listener) {
80
+ const stop = events.on('onnx.state', function changed(event) { listener(event.detail); });
81
+ try { listener(current()); } catch (error) { stop(); throw error; }
82
+ return stop;
83
+ }
84
+
85
+ function close() {
86
+ if (closed) return;
87
+ closed = true;
88
+ lifetime.abort();
89
+ unsubscribe();
90
+ state = {...state, available: false, state: 'closed'};
91
+ publish();
92
+ events.dispose();
93
+ }
94
+
95
+ return {load, run, unload, inspect, current, subscribe, close};
96
+ }
@@ -0,0 +1,85 @@
1
+ import Is from '../dependencies/strong-type/index.js';
2
+
3
+ const is = new Is(false);
4
+ const typedArrays = {
5
+ Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array,
6
+ Int32Array, Uint32Array, Float32Array, Float64Array,
7
+ BigInt64Array, BigUint64Array, Float16Array: globalThis.Float16Array
8
+ };
9
+
10
+ /** Tensor framing belongs at JSON transport boundaries, outside inference. */
11
+ export function encodeTensorMap(tensors) {
12
+ if (tensors === undefined || tensors === null) return tensors;
13
+ return Object.fromEntries(Object.entries(tensors).map(function encodeEntry([name, tensor]) {
14
+ return [name, tensor === null ? null : {
15
+ type: tensor.type, dims: tensor.dims, data: encodeTensorData(tensor.data)
16
+ }];
17
+ }));
18
+ }
19
+
20
+ export function decodeTensorMap(tensors) {
21
+ if (tensors === undefined || tensors === null) return tensors;
22
+ return Object.fromEntries(Object.entries(tensors).map(function decodeEntry([name, tensor]) {
23
+ return [name, tensor === null ? null : {
24
+ type: tensor.type, dims: tensor.dims, data: decodeTensorData(tensor.data)
25
+ }];
26
+ }));
27
+ }
28
+
29
+ export function encodeTensorFetches(fetches) {
30
+ return is.array(fetches) ? fetches : encodeTensorMap(fetches);
31
+ }
32
+
33
+ export function decodeTensorFetches(fetches) {
34
+ return is.array(fetches) ? fetches : decodeTensorMap(fetches);
35
+ }
36
+
37
+ function encodeTensorData(data) {
38
+ if (is.typedArray(data)) {
39
+ const arrayType = Object.prototype.toString.call(data).slice(8, -1);
40
+ const content = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
41
+ return {encoding: 'base64', arrayType, content: encodeBase64(content)};
42
+ }
43
+ if (is.array(data)) return data.map(encodeScalar);
44
+ return data;
45
+ }
46
+
47
+ function decodeTensorData(data) {
48
+ if (data?.encoding === 'base64') {
49
+ const Constructor = typedArrays[data.arrayType];
50
+ if (!is.function(Constructor)) {
51
+ throw new TypeError(`Tensor data representation ${data.arrayType} is unavailable in this runtime.`);
52
+ }
53
+ const content = decodeBase64(data.content);
54
+ return new Constructor(content.buffer);
55
+ }
56
+ if (is.array(data)) return data.map(decodeScalar);
57
+ return data;
58
+ }
59
+
60
+ function encodeScalar(value) {
61
+ if (is.bigint(value)) return {encoding: 'bigint', value: String(value)};
62
+ if (is.negativeZero(value)) return {encoding: 'number', value: '-0'};
63
+ if (is.number(value) && !is.finite(value)) return {encoding: 'number', value: String(value)};
64
+ return value;
65
+ }
66
+
67
+ function decodeScalar(value) {
68
+ if (value?.encoding === 'bigint') return BigInt(value.value);
69
+ if (value?.encoding === 'number') return Number(value.value);
70
+ return value;
71
+ }
72
+
73
+ function encodeBase64(content) {
74
+ if (is.function(content.toBase64)) return content.toBase64();
75
+ let binary = '';
76
+ for (const value of content) binary += String.fromCharCode(value);
77
+ return btoa(binary);
78
+ }
79
+
80
+ function decodeBase64(content) {
81
+ if (is.function(Uint8Array.fromBase64)) return Uint8Array.fromBase64(content);
82
+ return Uint8Array.from(atob(content), function characterCode(character) {
83
+ return character.charCodeAt(0);
84
+ });
85
+ }
@@ -14,6 +14,7 @@ export function createCoreClient({
14
14
  eventOwnerReady,
15
15
  autoConnect=true,
16
16
  replayRuntimeState=false,
17
+ webKitDocumentLifecycle=false,
17
18
  onError=error=>global.console?.error('Arcane Core client failed.',error)
18
19
  }={}){
19
20
  const pending=new Map();
@@ -29,6 +30,7 @@ export function createCoreClient({
29
30
  let eventOwnerFailure=null;
30
31
  let transportFailure=null;
31
32
  let transportFailureReported=false;
33
+ let webKitDocument=null;
32
34
 
33
35
  function report(error){
34
36
  if(error===transportFailure){
@@ -67,8 +69,13 @@ export function createCoreClient({
67
69
  report(eventOwnerFailure);
68
70
  });
69
71
 
70
- function emit(event,data){
71
- withEvents(()=>source.dispatch(CORE_EVENT,{event,data}));
72
+ function acceptsNativeDelivery(activation){
73
+ return !closed&&(!webKitDocument||webKitDocument.current(activation));
74
+ }
75
+ function emit(event,data,activation=webKitDocument?.activation){
76
+ withEvents(function deliverEvent(){
77
+ if(acceptsNativeDelivery(activation))source.dispatch(CORE_EVENT,{event,data});
78
+ });
72
79
  }
73
80
  function complete(event,data){
74
81
  if(completedEvents.has(event))return false;
@@ -110,13 +117,23 @@ export function createCoreClient({
110
117
  if(typeof listener!=='function')throw new TypeError('Arcane event listener must be a function.');
111
118
  if(!completedEvents.has(event))return events.once(event,listener);
112
119
  const value=completedEvents.get(event);
120
+ const activation=webKitDocument?.activation;
113
121
  let active=!closed;
114
- Promise.resolve().then(function replayReadyEvent(){
115
- if(!active||closed)return;
122
+ let dispose=null;
123
+ function deliverReadyEvent(ready){
124
+ if(!active)return;
116
125
  active=false;
117
- try{listener(value);}catch(error){report(error);}
126
+ dispose?.();
127
+ try{listener(ready);}catch(error){report(error);}
128
+ }
129
+ // A ready-state subscription survives a retirement that happens
130
+ // before its queued replay, just like an ordinary registration.
131
+ if(webKitDocument)dispose=events.once(event,deliverReadyEvent);
132
+ Promise.resolve().then(function replayReadyEvent(){
133
+ if(!active||!acceptsNativeDelivery(activation))return;
134
+ deliverReadyEvent(value);
118
135
  });
119
- return function unsubscribe(){active=false;};
136
+ return function unsubscribe(){active=false;dispose?.();};
120
137
  },
121
138
  completed(event){return CORE_READY_EVENTS.includes(event)&&completedEvents.has(event);}
122
139
  };
@@ -131,8 +148,8 @@ export function createCoreClient({
131
148
  else request.resolve(result);
132
149
  return true;
133
150
  }
134
- function receive(input){
135
- if(closed)return false;
151
+ function receive(input,activation){
152
+ if(!acceptsNativeDelivery(activation))return false;
136
153
  let message=input;
137
154
  if(typeof message==='string'){
138
155
  try{message=JSON.parse(message);}
@@ -149,7 +166,9 @@ export function createCoreClient({
149
166
  // Final responses follow earlier event deliveries, including while a
150
167
  // classic host is connecting the shared SDK event owner. A stream's
151
168
  // completion must not remove its subscription before its chunks arrive.
152
- withEvents(()=>settle(message.id,message.ok?null:new CoreError(message.error),message.result));
169
+ withEvents(function deliverResponse(){
170
+ if(acceptsNativeDelivery(activation))settle(message.id,message.ok?null:new CoreError(message.error),message.result);
171
+ });
153
172
  return true;
154
173
  }
155
174
  function installCallback(name,value){
@@ -182,6 +201,107 @@ export function createCoreClient({
182
201
  catch(error){record.reject(error);}
183
202
  return true;
184
203
  }
204
+ function createWebKitDocumentTransport(webkit) {
205
+ let activation = null;
206
+ let active = false;
207
+ let registration = null;
208
+ function current(candidate) {
209
+ return !closed && active && candidate === activation;
210
+ }
211
+ function inactive() {
212
+ return new CoreError(
213
+ {code: 'ARCANE_WEBKIT_DOCUMENT_INACTIVE', message: 'This document activation no longer owns the native connection.'}
214
+ );
215
+ }
216
+ async function post(record) {
217
+ parseAcknowledgement(
218
+ await webkit.postMessage(record), {required: true}
219
+ );
220
+ }
221
+ function activate(restored = false) {
222
+ if (closed || active) return;
223
+ activation = uuid();
224
+ active = true;
225
+ const candidate = activation;
226
+ // Only transport ingress waits for native attachment. Rendering and
227
+ // the synchronous facade remain independent of this acknowledgement.
228
+ registration = Promise.resolve().then(
229
+ function announceDocument() {
230
+ if (!current(candidate)) throw inactive();
231
+ return post(
232
+ {type: 'activate', activation: candidate}
233
+ );
234
+ }
235
+ );
236
+ complete(
237
+ 'transport.ready', {protocol: CORE_PROTOCOL, transport: 'webkitgtk'}
238
+ );
239
+ registration.then(
240
+ function replayDocumentState() {
241
+ if (current(candidate) && (restored || replayRuntimeState)) {
242
+ sendControl(
243
+ {protocol: CORE_PROTOCOL, type: 'control', control: 'runtime.replay'}
244
+ );
245
+ }
246
+ }
247
+ ).catch(report);
248
+ }
249
+ function retire() {
250
+ if (!active) return false;
251
+ const candidate = activation;
252
+ // Invalidate delivery synchronously, including callbacks queued in
253
+ // WebKit or waiting for the shared event owner. Subscriptions live
254
+ // with this document and remain registered across restoration.
255
+ active = false;
256
+ completedEvents.clear();
257
+ for (const [id, request] of pending) settle(id, aborted(request.method));
258
+ const json = JSON.stringify(
259
+ {protocol: CORE_PROTOCOL, type: 'control', control: 'requests.cancelAll'}
260
+ );
261
+ post(
262
+ {type: 'retire', activation: candidate, json}
263
+ ).catch(report);
264
+ return true;
265
+ }
266
+ function restoredDocument(event) {
267
+ if (event.persisted) activate(true);
268
+ }
269
+ if (typeof global.addEventListener === 'function') {
270
+ global.addEventListener(
271
+ 'pageshow', restoredDocument, {capture: true}
272
+ );
273
+ cleanups.push(
274
+ function removeDocumentRestoration() {
275
+ global.removeEventListener(
276
+ 'pageshow', restoredDocument, {capture: true}
277
+ );
278
+ }
279
+ );
280
+ }
281
+ cleanups.push(
282
+ function releaseDocument() { active = false; }
283
+ );
284
+ installCallback('__arcaneWebKitDocumentCurrent', current);
285
+ webKitDocument = {current, activate, retire, get activation() { return active ? activation : null; }};
286
+ return {
287
+ name: 'webkitgtk',
288
+ async send(frame) {
289
+ const candidate = activation;
290
+ if (!current(candidate)) throw inactive();
291
+ const json = JSON.stringify(frame);
292
+ await registration;
293
+ // A page may retire while native attachment is pending. It
294
+ // must never submit its queued request after that retirement.
295
+ if (!current(candidate)) throw inactive();
296
+ // Abort, timeout or cancelAll may also end a request while
297
+ // this activation remains live. Unsent work stays cancelled.
298
+ if (frame.type === 'request' && !pending.has(frame.id)) return;
299
+ await post(
300
+ {type: 'frame', activation: candidate, json}
301
+ );
302
+ }
303
+ };
304
+ }
185
305
  function chooseTransport(){
186
306
  const webview=global.chrome?.webview;
187
307
  if(webview?.hostObjects){
@@ -204,6 +324,12 @@ export function createCoreClient({
204
324
  const webkit=global.webkit?.messageHandlers?.arcane;
205
325
  if(typeof global.__arcaneWebKitPostMessage==='function'||webkit){
206
326
  installCallback('__arcaneReceive',receive);
327
+ if(webKitDocumentLifecycle){
328
+ if(typeof webkit?.postMessage!=='function'){
329
+ throw new CoreError({code:'ARCANE_TRANSPORT_INVALID',message:'WebKit document lifecycle requires the arcane message handler with replies.'});
330
+ }
331
+ return createWebKitDocumentTransport(webkit);
332
+ }
207
333
  installCallback('__arcaneWebKitAcknowledge',acknowledge);
208
334
  return {name:'webkitgtk',async send(frame){
209
335
  const serialized=JSON.stringify(frame);
@@ -293,8 +419,9 @@ export function createCoreClient({
293
419
  throw error;
294
420
  }
295
421
  if(closed)throw transportFailure??new CoreError({code:'ARCANE_CLIENT_CLOSED',message:'The Core client is closed.'});
422
+ if(webKitDocument)webKitDocument.activate();
296
423
  complete('transport.ready',{protocol:CORE_PROTOCOL,transport:transport.name});
297
- if(replayRuntimeState&&!closed)sendControl({protocol:CORE_PROTOCOL,type:'control',control:'runtime.replay'});
424
+ if(replayRuntimeState&&!closed&&!webKitDocument)sendControl({protocol:CORE_PROTOCOL,type:'control',control:'runtime.replay'});
298
425
  // A native callback may close the client synchronously during replay.
299
426
  if(closed)throw transportFailure??new CoreError({code:'ARCANE_CLIENT_CLOSED',message:'The Core client is closed.'});
300
427
  return transport;
@@ -305,7 +432,7 @@ export function createCoreClient({
305
432
  :typeof global.__arcaneWebKitPostMessage==='function'||global.webkit?.messageHandlers?.arcane?'webkitgtk'
306
433
  :typeof global.arcaneAndroid?.postMessage==='function'?'android-webview'
307
434
  :global.__ARCANE_DEV_HTTP__?'development-http':'standalone');
308
- return {connected:transport!==null&&!closed,transport:name,
435
+ return {connected:transport!==null&&!closed&&(!webKitDocument||webKitDocument.activation!==null),transport:name,
309
436
  native:['webview2','webkitgtk','android-webview'].includes(name),
310
437
  managedLocalAI:['webview2','webkitgtk'].includes(name)};
311
438
  }
@@ -389,18 +516,23 @@ export function createCoreClient({
389
516
  function close(){
390
517
  if(closed)return false;
391
518
  closed=true;
392
- cancelAll();
519
+ if(webKitDocument)webKitDocument.retire();
520
+ else cancelAll();
393
521
  disposeTransport(new CoreError({code:'ARCANE_CLIENT_CLOSED',message:'The Core client is closed.'}));
394
522
  return true;
395
523
  }
524
+ function documentHidden(){
525
+ if(webKitDocument)webKitDocument.retire();
526
+ else cancelAll();
527
+ }
396
528
  if(typeof global.addEventListener==='function'){
397
- global.addEventListener('pagehide',cancelAll,{capture:true});
398
- cleanups.push(()=>global.removeEventListener('pagehide',cancelAll,{capture:true}));
529
+ global.addEventListener('pagehide',documentHidden,{capture:true});
530
+ cleanups.push(()=>global.removeEventListener('pagehide',documentHidden,{capture:true}));
399
531
  }
400
532
  if(autoConnect){
401
533
  try{connect();}catch(error){if(error.code!=='ARCANE_TRANSPORT_UNAVAILABLE')report(error);}
402
534
  }
403
- return {protocol:CORE_PROTOCOL,Error:CoreError,invoke,receive,connect,close,failTransport,cancelAll,events,eventsReady,runtime:{current:runtimeSnapshot},uuid};
535
+ return {protocol:CORE_PROTOCOL,Error:CoreError,invoke,receive,connect,close,failTransport,cancelAll,acceptsNativeDelivery,events,eventsReady,runtime:{current:runtimeSnapshot},uuid};
404
536
  }
405
537
 
406
538
  /** Existing namespace call shapes; method implementations remain host-owned. */
@@ -436,7 +568,7 @@ export function createCoreFacade(client){
436
568
  remove:name=>invoke('environment.delete',{name})},
437
569
  mail:{send:request=>invoke('mail.send',request??{},{timeoutMs:450000})},
438
570
  speech:{status:()=>invoke('speech.status',{},{timeoutMs:10000}),
439
- synthesize:request=>invoke('speech.synthesize',request??{},{timeoutMs:180000}),
571
+ synthesize:(request,options)=>invoke('speech.synthesize',request??{},{timeoutMs:180000,signal:options?.signal}),
440
572
  transcribe:request=>invoke('speech.transcribe',request??{},{timeoutMs:180000})},
441
573
  localAI:{status:()=>invoke('localai.status',{},{timeoutMs:15000}),
442
574
  ensurePlatform:()=>invoke('localai.platform.ensure',{},long),recover:request=>invoke('localai.services.recover',request??{},long),
@@ -512,7 +644,10 @@ export function installCoreClient(global=globalThis,options={}){
512
644
  const previous=global.__arcaneReceive;
513
645
  global.__arcaneReceive=client.receive;
514
646
  const previousFailure=global.__arcaneTransportFailed;
515
- function nativeTransportFailed(error){return client.failTransport(error);}
647
+ function nativeTransportFailed(error,activation){
648
+ if(!client.acceptsNativeDelivery(activation))return false;
649
+ return client.failTransport(error);
650
+ }
516
651
  global.__arcaneTransportFailed=nativeTransportFailed;
517
652
  const close=client.close;
518
653
  const failTransport=client.failTransport;
@@ -191,13 +191,16 @@ redirect listener, defaulting to an OS-assigned port. Existing raw `tls` inputs
191
191
  retain their native HTTPS transport under the SDK, as described by the module's
192
192
  advanced TLS extension guidance; all content uses its public serving methods.
193
193
 
194
- Explicit `arcane dev --http` or source API `http:true` selects the module's
194
+ Explicit `arcane dev --http` or API `http:true` selects the module's
195
195
  single HTTP listener on `port`. It skips certificate resolution and serves the
196
196
  same selected source mappings, generated PWA routes, and conditional responses.
197
+ In API packaged mode, it serves the complete selected packaged web root and
198
+ its declared application start URL instead of source mappings.
197
199
  No HTTPS listener or redirect is started in this mode. Listener readiness,
198
200
  failure, cancellation and shutdown retain the same operation owner; reported
199
201
  URLs use the actual HTTP protocol and bound port. Browser secure-context
200
- requirements remain browser-owned. Packaged previews retain HTTPS.
202
+ requirements remain browser-owned. Source and packaged modes retain HTTPS
203
+ when `http:true` is not selected; a certificate error never switches transport.
201
204
 
202
205
  Development is an intentionally fast feedback loop. Keep each increment small
203
206
  and independently understandable so its effect has one clear cause and a
@@ -16,10 +16,15 @@ toolchain, platform, architecture, format, signing mode, declared dependency
16
16
  releases, and destination. Verification is a separate operation only when the
17
17
  user explicitly selects it for the release artifact.
18
18
 
19
- The portable target pairs with the installed SDK provider by default. An explicit
20
- `--arcane-root` retains fixed-provider selection from an Arcane OS checkout;
21
- Windows, Linux and Android executable targets still require that override and
22
- their platform toolchains. Every native request also requires
19
+ The portable, Windows x64 and macOS targets pair with installed SDK providers by
20
+ default. Windows uses the numeric SDK release's precompiled WebView2/SEA host.
21
+ macOS source composition selects that version's architecture-specific AppKit,
22
+ WebKit and bundled Node host asset; actual asset publication and Mac execution
23
+ are separate release evidence, not implied by the target registry.
24
+ An explicit `--arcane-root` retains fixed-provider selection for the registered
25
+ portable, Windows, Linux and Android checkout targets. macOS uses the installed
26
+ SDK provider and has no checkout override. Linux and Android still require that
27
+ override and their platform toolchains. Every native request also requires
23
28
  the canonical app descriptor to declare the exact target selected on the
24
29
  command line. The SDK package does not silently search for a toolchain, infer a
25
30
  descriptor target, embed the Arcane machine bundle, or substitute browser
@@ -35,8 +40,8 @@ npm exec -- arcane native-doctor --target portable
35
40
  npm exec -- arcane build --target portable
36
41
 
37
42
  # In an app scaffolded with --target windows-x64:
38
- npm exec -- arcane build --target windows-x64 --arcane-root "../Arcane OS"
39
- npm exec -- arcane run --target windows-x64 --arcane-root "../Arcane OS"
43
+ npm exec -- arcane build --target windows-x64
44
+ npm exec -- arcane run --target windows-x64
40
45
 
41
46
  # In an app scaffolded with --target linux-x64:
42
47
  npm exec -- arcane build --target linux-x64 --arcane-root "../Arcane OS"
@@ -62,7 +67,7 @@ from its installed npm dependency. After `npm install`, run
62
67
  browser maps are present. The initial scaffold reports that map as pending while
63
68
  the SDK dependency is absent. The scaffold includes the raster icon required
64
69
  by the current native platform. Use the
65
- matching scaffold target (`portable`, `windows-x64`, `linux-x64`, `linux-arm64`,
70
+ matching scaffold target (`portable`, `windows-x64`, `macos-arm64`, `macos-x64`, `linux-x64`, `linux-arm64`,
66
71
  or `android-arm64`) before running the corresponding command.
67
72
 
68
73
  `native-prepare` remains a standalone diagnostic. The normal build recipe omits
@@ -72,7 +77,8 @@ The portable output is an app-scoped payload directory with complete selected
72
77
  app and dependency files, a generated Core entry and classic client source.
73
78
  It is not an executable and has no direct run operation. Explicit service
74
79
  composition uses `createPortableNativeProvider({services:[{module,options}]})`;
75
- the default adds no OS or application services. See
80
+ the default uses an explicit descriptor service selection when supplied and
81
+ does not discover or copy OS product services. See
76
82
  [Core native packaging](reference/core-native-packaging.md).
77
83
  The external workspace defaults to `build/portable/`. Integrated Arcane work
78
84
  must name an `--output-root` outside that checkout. Explicit checkout providers
@@ -89,19 +95,30 @@ for the complete compatibility and breaking-change rule.
89
95
  |---|---|---|
90
96
  | `browser` | `directory` | Available |
91
97
  | `portable` | `portable` directory | SDK package provider by default; explicit checkout override retained; not executable |
92
- | `windows-x64` | `exe` bundle | Available with explicit `--arcane-root`; unsigned local development only |
98
+ | `windows-x64` | `exe` bundle | SDK package provider and matching precompiled release host by default; explicit checkout override retained |
99
+ | `macos-arm64` | `.app` bundle | SDK provider source; needs a matching selected or published Apple silicon host asset |
100
+ | `macos-x64` | `.app` bundle | SDK provider source; needs a matching selected or published Intel host asset |
93
101
  | `linux-x64` | `deb` | Available with explicit `--arcane-root`; unsigned local development only |
94
102
  | `linux-arm64` | `deb` | Available with explicit `--arcane-root` on a compatible native ARM64 toolchain; unsigned local development only |
95
103
  | `android-arm64` | `apk` | Available with explicit `--arcane-root`; development-signed, architecture-neutral, and physical/native ARM64 for run |
96
104
 
97
105
  macOS can consume browser applications and portable JavaScript capabilities
98
- within their declared runtimes. The current target registry has no macOS native
99
- package adapter, and the portable CLI request currently selects only Windows
100
- or Linux. macOS native packaging and Android host adaptation remain incomplete;
101
- the SDK portable payload does not supply executable hosts. A source-development
106
+ within their declared runtimes. The native target registry includes separate
107
+ Apple silicon and Intel `.app` compositions; portable CLI requests also accept
108
+ macOS. A Mac provider preserves the complete portable payload under its bundle's
109
+ Resources directory and composes the SDK host separately. A source-development
102
110
  native wrapper is also not an exposed `arcane dev` target. Native `run` uses the
103
111
  selected packaged application through its paired provider.
104
112
 
113
+ The package includes macOS `ArcaneCoreProcess`, `ArcaneHost` and launcher source
114
+ for explicit child launch, framed pipes, document lifecycle and graceful drain.
115
+ The SDK provider composes them with an explicitly selected Darwin Node runtime.
116
+ Source availability is not evidence of Mac compilation or execution. See the
117
+ [macOS composition boundary](reference/core-native-packaging.md#macos-application-composition).
118
+ Windows app assembly needs the normal Node/npm toolchain and `tar`, not a C#
119
+ compiler. Its generated executable uses bundled Node and requires .NET Framework
120
+ 4.6.2 or later plus WebView2 Runtime on the destination Windows x64 machine.
121
+
105
122
  Every native target accepts one selected app release and its complete bundled
106
123
  dependency closure through the provider boundary. The providers retain the
107
124
  toolchain state required for build and, where supported, launch; app source and