arcane-os 0.68.0 → 0.70.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 (41) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +9 -4
  3. package/browser-runtime/ai/browser-decisions.mjs +46 -2
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +58 -62
  5. package/browser-runtime/ai/browser-speech-providers.mjs +8 -3
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +76 -164
  7. package/browser-runtime/ai/dbopfs-model-parts.mjs +344 -0
  8. package/browser-runtime/ai/dbopfs-model-resources.mjs +190 -0
  9. package/browser-runtime/ai/decision-runtime.mjs +17 -1
  10. package/browser-runtime/ai/decision-worker.mjs +21 -1
  11. package/browser-runtime/ai/model-resource-bridge.mjs +231 -0
  12. package/browser-runtime/ai/speech-worker-client.mjs +68 -28
  13. package/browser-runtime/ai/speech-worker-runtime.mjs +212 -97
  14. package/browser-runtime/core/contracts.mjs +1 -1
  15. package/docs/reference/README.md +2 -0
  16. package/docs/reference/ai/browser-decisions.md +57 -3
  17. package/docs/reference/ai/browser-speech.md +94 -26
  18. package/docs/reference/ai/browser-wasm.md +28 -20
  19. package/docs/reference/core-native-packaging.md +54 -0
  20. package/docs/reference/core-runtime.md +9 -0
  21. package/docs/reference/core-shared-host.md +190 -0
  22. package/docs/reference/inventory/package-api.json +3 -3
  23. package/docs/reference/inventory/runtime-modules.json +3 -3
  24. package/docs/reference/runtime-components.md +4 -2
  25. package/docs/reference/runtime-modules.md +120 -20
  26. package/docs/reference/sdk-api.md +64 -30
  27. package/package.json +2 -2
  28. package/runtime/arcane/components/theme-switcher.html +1 -1
  29. package/runtime/arcane/modules/AI.js +7 -22
  30. package/runtime/arcane/modules/AIProviderRuntime.js +84 -6
  31. package/runtime/arcane/modules/DBOPFS.js +180 -26
  32. package/schemas/arcane-app.schema.json +10 -0
  33. package/src/app-descriptor.mjs +23 -1
  34. package/src/core/host.mjs +2 -0
  35. package/src/core/hosts/windows/ArcaneHost.cs +46 -0
  36. package/src/core/hosts/windows/ArcaneLauncher.cs +21 -0
  37. package/src/core/runtime.mjs +9 -4
  38. package/src/core/services/packaged-web.mjs +8 -1
  39. package/src/core/shared-host.mjs +500 -0
  40. package/src/native/portable-layout.mjs +66 -34
  41. package/src/native/portable-provider.mjs +1 -0
@@ -805,14 +805,17 @@ does not select another page or speak background work.
805
805
 
806
806
  ### Preparation inputs and reuse
807
807
 
808
- `ai.prepareTTS({parts,storage,identity,signal,onState})` accepts an ordered array
808
+ `ai.prepareTTS({parts,textFormat,storage,identity,signal,onState})` accepts an ordered array
809
809
  of strings or `{input,voice?,speed?,pauseAfterMs?}` records. An omitted voice
810
810
  uses the selected model default; omitted speed uses `ai.voiceSpeed`; omitted
811
811
  pause is zero. The SDK snapshots the selected speech configuration and current
812
812
  punctuation/word-cadence options for that preparation. A part's pause applies
813
813
  after its final extracted segment. Complete original input remains available
814
- for semantic comparison; automatic Markdown cleanup affects only the speech
815
- copy and occurs once before segmentation.
814
+ for semantic comparison; default Markdown cleanup affects only the speech
815
+ copy and occurs once before segmentation. Set `textFormat:'plain'` to preserve
816
+ each supplied input exactly through that segmentation. The prepared segment
817
+ text participates in semantic reuse, so a changed synthesis input does not
818
+ reuse audio produced from the cleaned version.
816
819
 
817
820
  `storage` is optional. When supplied, `{db,table,key}` names the caller's ready
818
821
  DBOPFS instance and its application-owned table/key. The SDK stores complete
@@ -839,6 +842,14 @@ cancellation signal. Cancelling one handle detaches that caller; the shared
839
842
  preparation is aborted only when its last pending caller cancels. Different AI
840
843
  instances do not share an in-flight synthesis operation. Complete stored
841
844
  results remain reusable through the same application storage.
845
+
846
+ Missing audio participates in the provider runtime's shared TTS activation with
847
+ the preparation's cancellation signal. Cancelling the final pending interest
848
+ aborts that activation; another preparation, explicit playback activation, or
849
+ direct TTS load joining it keeps the load alive. Cancellation does not unload a
850
+ model whose activation has already completed. A complete audio-cache hit never
851
+ requests model activation. Provider cancellation and cleanup remain cooperative;
852
+ an aborted caller wait alone is not evidence that native work has stopped.
842
853
  After a durable preparation completes, a later preparation call rereads its
843
854
  manifest and saved-file presence, regenerating only missing segments. A
844
855
  completed preparation without storage reuses its retained Blobs.
@@ -871,9 +882,10 @@ The supplied signal observes preparation until `ready` settles. Manual
871
882
  handle. If a raw audio write has already begun when cancellation arrives,
872
883
  its metadata transaction finishes so that successful audio can be reused;
873
884
  no later queued write starts for that cancelled preparation.
874
- Cancelling preparation prevents later synthesis, but the existing shared
875
- provider load/unmute operation has no per-preparation signal and may finish.
876
- Cancellation does not claim to stop an already-started shared model load.
885
+ Cancelling preparation prevents later synthesis and releases its pending
886
+ activation interest. Other pending interests retain the shared load; cancelling
887
+ the final interest forwards abort to its provider-owned activation. Actual
888
+ provider cleanup remains observable through that operation's settlement.
877
889
 
878
890
  ### Playback controls and independent lifetimes
879
891
 
@@ -977,7 +989,7 @@ speech method retain their existing behavior.
977
989
  ## Automatic speech-input formatting cleanup
978
990
 
979
991
  The shared streaming, audio-file, and `SpeechPlayback` paths remove repeated
980
- same formatting marks from the outbound speech-input copy automatically:
992
+ same formatting marks from the outbound speech-input copy by default:
981
993
 
982
994
  ```javascript
983
995
  ai.streamTTS('## Heading\n**Hello');
@@ -994,10 +1006,13 @@ waits for its next character or the final flush.
994
1006
 
995
1007
  `end:true`, `finishTTS()`, or cancellation clears pending streaming formatting
996
1008
  state. `fetchTTS()`, provider-runtime TTS requests, direct Kokoro provider
997
- requests, and `SpeechPlayback` apply the same cleanup to complete input. An
998
- existing `textFormat` extra is ignored and cannot disable or select cleanup.
1009
+ requests, and `SpeechPlayback` apply the same default cleanup to complete input.
1010
+ Complete-input APIs can explicitly select `textFormat:'plain'` as described
1011
+ below. The streaming `streamTTS()` options still ignore `textFormat`;
1012
+ streaming narration and `SpeechPlayback` keep their existing cleanup.
999
1013
  SDK-internal delegation carries `{speechInputPrepared:true}` outside the speech
1000
- payload only after one cleanup pass, preventing a second non-idempotent pass.
1014
+ payload after its owner selects text treatment, preventing a second
1015
+ non-idempotent cleanup pass or cleanup of explicitly plain input.
1001
1016
  Applications omit that internal metadata. Displayed messages, saved history,
1002
1017
  model input, caller payload objects, language, voice, synthesis capacity, and
1003
1018
  playback timing are not changed by the filter.
@@ -1023,6 +1038,39 @@ console.log(speechText.append('## Head', false)); // ' Head'
1023
1038
  console.log(speechText.append('ing', true)); // ing
1024
1039
  ```
1025
1040
 
1041
+ ### Exact complete synthesis input
1042
+
1043
+ Use the public `textFormat:'plain'` selection for already-authored speech or an
1044
+ exact generated preview. It belongs in `prepareTTS` options, or alongside the
1045
+ `input`/`text` field of a complete synthesis payload:
1046
+
1047
+ ```javascript
1048
+ function preparePreview(ai, preview, signal) {
1049
+ return ai.prepareTTS({parts: [preview], textFormat: 'plain', signal});
1050
+ }
1051
+
1052
+ function fetchPreview(ai, preview, signal) {
1053
+ return ai.fetchTTS({input: preview, textFormat: 'plain'}, signal);
1054
+ }
1055
+ ```
1056
+
1057
+ The same payload field is supported by `AI.prepareTTSPlayback()` for audio-file
1058
+ providers, `AIProviderRuntime.request('tts', {operation:'synthesize', payload,
1059
+ localOnly, signal})`, its `synthesize(payload, options)` alias, and direct
1060
+ Kokoro `request()` calls using either `{text,...}` or `{model,input,...}`.
1061
+ The SDK consumes `textFormat` before engine/host dispatch. The original nonblank input,
1062
+ including whitespace and repeated formatting marks, remains unchanged; voice,
1063
+ speed, selected model, cancellation and audio ordering retain their existing
1064
+ owners. Only `'plain'` selects exact input; omission and other values retain
1065
+ the existing narration cleanup. Applications never supply the internal
1066
+ `speechInputPrepared` argument.
1067
+
1068
+ This selection governs SDK text treatment, not the selected engine's input
1069
+ capacity or pronunciation. [Kokoro.js 1.2.1](https://github.com/hexgrad/kokoro/blob/664c76a704021239ba59c84dcbaa4d3dece01fe9/kokoro.js/src/kokoro.js)
1070
+ still tokenizes with truncation in both `generate()` and `stream()`; plain input
1071
+ does not establish complete audio for an oversized engine input. The SDK's existing complete-text forwarding
1072
+ fixture does not prove upstream synthesis completeness.
1073
+
1026
1074
  ## Choose a device or reduce memory use
1027
1075
 
1028
1076
  Local Whisper and Kokoro roles accept
@@ -1458,23 +1506,39 @@ The Worker installs one private module router before importing the entrypoint:
1458
1506
  - a dynamic import whose target is known imports the materialized URL;
1459
1507
  - a fetch whose target is known reads the materialized URL;
1460
1508
  - a Worker whose target is known starts the SDK role Worker and imports the
1461
- materialized target there; and
1462
- - a Cache match whose target is known returns the materialized file.
1509
+ materialized target there.
1463
1510
 
1464
- Every unmapped operation keeps ordinary browser behavior:
1511
+ Resource ownership extends to ordinary self-contained speech runtimes as well
1512
+ as graph runtimes:
1465
1513
 
1466
1514
  - an unmapped relative or URL-like import resolves against the calling module's
1467
1515
  original source URL, while a bare specifier remains unchanged for native
1468
1516
  import-map resolution;
1469
- - an unmapped fetch calls native `fetch` and preserves the caller's options;
1470
- - an unmapped Worker calls the native `Worker` constructor and preserves the
1471
- caller's options; and
1472
- - an unmapped Cache operation delegates to native Cache Storage.
1473
-
1474
- Cache `put`, `add`, `addAll`, `delete`, and `keys` are normal mutable browser
1475
- operations. The SDK does not replace them with a read-only cache. Relative
1476
- requests are resolved from the calling module's original source URL before the
1477
- native Cache operation.
1517
+ - an unmapped HTTP fetch preserves the caller's request options and reaches
1518
+ the parent DBOPFS resource store; Blob/data resources remain local;
1519
+ - an unmapped module Worker starts through the SDK role Worker and forwards
1520
+ its resource fetches to that same store; and
1521
+ - an unmapped classic Worker retains native execution. Its internal fetches
1522
+ are outside this module-Worker routing contract.
1523
+
1524
+ Inside the dedicated speech Worker, native CacheStorage is temporarily
1525
+ unavailable before runtime import, so the selected runtime takes its existing
1526
+ fetch path. Transformers' public browser/custom/filesystem cache switches are
1527
+ disabled there. The SDK does not fabricate Cache matches or mutation results,
1528
+ and it neither clears nor changes saved native caches. The original Worker
1529
+ environment is restored on unload or failed import. Existing explicit
1530
+ `removeBrowserSpeechModelCache()` remains a separate caller-selected operation.
1531
+
1532
+ The existing speech store exposes `fetchResource(input, options)` for its
1533
+ Worker loaders, returning `{file,status,statusText,headers,url,redirected}`.
1534
+ New declared files and fetched model, voice, configuration, and WASM resources
1535
+ stream into ordered DBOPFS parts even without a response length. Closed-part
1536
+ progress uses `unit:'shards'`; declared-file progress retains `unit:'files'`
1537
+ with the current shard record in `resource`. Complete declared peer files and
1538
+ closed resource parts survive cancellation. Existing whole-file caches remain
1539
+ readable without migration. HTTP resume is used only when the server returns
1540
+ the requested range; otherwise the complete resource is downloaded anew.
1541
+ Worker termination aborts and joins its parent-owned resource operations.
1478
1542
 
1479
1543
  Routing discovery is best effort and is not an admission gate. If the scanner
1480
1544
  cannot interpret a module, that module is left unchanged and follows its native
@@ -1496,11 +1560,14 @@ provider:
1496
1560
  `namespace.env.backends.onnx.wasm.wasmPaths = {mjs,wasm}`, keeps remote model
1497
1561
  loading enabled, and applies caller-selected `numThreads` when present.
1498
1562
 
1499
- Transformers and Kokoro keep their normal provider downloads and Cache behavior
1500
- for routes not materialized by the SDK. Kokoro voice aliases and Transformers
1563
+ Transformers and Kokoro use their normal provider request semantics through
1564
+ the selected DBOPFS resource owner. Kokoro voice aliases and Transformers
1501
1565
  model aliases may be listed in `runtimeRequestUrls` so an upstream request for a
1502
1566
  known file resolves to the already materialized local file. Model, voice, and
1503
1567
  runtime selection remains with the application and upstream publisher.
1568
+ Native ESM imports remain platform-owned; only declared runtime modules are
1569
+ materialized by artifact preparation. Custom classic Worker internals are not
1570
+ claimed as stored by this contract.
1504
1571
 
1505
1572
  ## Providers
1506
1573
 
@@ -1610,10 +1677,11 @@ const result = await kokoro.request({
1610
1677
  The voice belongs to the caller-selected inventory; omission uses that model's
1611
1678
  default voice. The provider-native result is
1612
1679
  `{audio:Float32Array,sampleRate,voice}`. The provider/2 shared request form accepts
1613
- `{model,input,responseFormat:'wav',voice?,speed?}` and returns
1680
+ `{model,input,textFormat?,responseFormat:'wav',voice?,speed?}` and returns
1614
1681
  `{audio:Uint8Array,contentType:'audio/wav'}`. High-level `AI.fetchTTS()` wraps
1615
1682
  that provider result in a WAV `Blob`. Returned provider records remain ordinary
1616
- mutable values.
1683
+ mutable values. Both payload forms accept `textFormat:'plain'` to retain exact
1684
+ input; omission keeps the default speech-formatting cleanup.
1617
1685
 
1618
1686
  ## Lifecycle and cancellation
1619
1687
 
@@ -117,21 +117,15 @@ current mutable status snapshot. Provider states are `unloaded`, `loading`, `rea
117
117
 
118
118
  Browser-WASM model loads report `cache-check`, `download`, and `initialize`
119
119
  phases. Download records preserve the ordered model-file fields `completed`,
120
- `total`, and `unit:'files'`. They add `loadedBytes`, `totalBytes`,
121
- `remainingBytes`, `bytesPerSecond`, `etaSeconds`, `activeTransfers`,
122
- `transferLimit`, and `transferMode`. `loadedBytes` is aggregate downloaded or
123
- restored progress for the current install. Active partial writes count while
124
- they are staged, then are removed from that total if their transfer fails;
125
- completed shards or Range parts reused from an interrupted attempt remain
126
- counted. Unknown totals, remaining counts, rates, and ETAs are `null`;
127
- once known, byte values and seconds are nonnegative numbers. `activeTransfers`
128
- is the current transfer-worker count, `transferLimit` is the selected concurrency
129
- bound, and `transferMode` identifies `probing`, `files`, `ranges`, or `single`.
130
- The store coalesces chunk-driven changes on a 250 ms cadence and publishes
131
- immediately when download starts, the transfer plan or total becomes known,
132
- active-worker count changes, and the download completes. At known completion, remaining bytes and ETA are zero
133
- and active transfers are zero. Applications format the raw measures into B,
134
- KB, MB, GB, transfer-rate, and duration labels.
120
+ `total`, and `unit:'files'`. Their ordered `members` records contain each file's
121
+ `name`, committed-part `completed`, `total`, and `unit:'shards'`. A streaming
122
+ response has `total:null` until its end is observed; completion then supplies
123
+ the actual committed-part count. Previously complete members identify
124
+ `state:'cached'`. `activeTransfers` is the current transfer-worker count,
125
+ `transferLimit` is the selected concurrency, and `transferMode` identifies
126
+ `probing`, `files`, `ranges`, or `single`. No byte progress, rate, or estimated
127
+ completion time is produced. Closed-part and lifecycle changes publish promptly;
128
+ the existing 250 ms heartbeat retains the current semantic state.
135
129
 
136
130
  Initialization keeps `phase:'initialize'` and reports a `stage` and a readable
137
131
  `message` from the packaged runtime's existing log signals: runtime startup,
@@ -172,8 +166,11 @@ optional descriptor `bytes` divides that member into deterministic contiguous
172
166
  parts of roughly 4 MB each, up to 4,096 parts. Each exact completed part is
173
167
  committed separately in OPFS, so retry or refresh fetches only the small
174
168
  in-flight parts plus any other missing parts; the ordered parts are exposed to
175
- Wllama as one logical model Blob. Range offsets remain transport-local; only
176
- aggregate transfer telemetry is public.
169
+ Wllama as one logical model Blob. Ordinary full responses, including unknown
170
+ content lengths and single-file models, also stream directly into ordered OPFS
171
+ parts. Model data is not accumulated in one JavaScript buffer. Range offsets
172
+ and physical part framing remain storage/transport-local; progress describes
173
+ completed files and parts.
177
174
 
178
175
  ## Model selection, optional hardening, and cache
179
176
 
@@ -181,7 +178,7 @@ The canonical model descriptor is
181
178
  `{id, files:[{name?,url,bytes?},...]}`. The ordered `files` array is nonempty;
182
179
  names and URLs are unique. Each URL must be absolute HTTPS without credentials
183
180
  or a fragment. An optional positive safe-integer `bytes` value is used only for
184
- progress and HTTP Range planning. Missing or unusable metadata never blocks a
181
+ HTTP Range planning. Missing or unusable metadata never blocks a
185
182
  download, and a declared value is not a content-length check.
186
183
 
187
184
  Fetch follows HTTPS redirects and records the requested and final HTTPS URL. A
@@ -205,11 +202,22 @@ admission work. Observational byte counting and exact HTTP Range framing remain
205
202
  transport-local. A load succeeds only after Wllama reports that the model is
206
203
  loaded.
207
204
 
208
- The DBOPFS adapter commits each complete member or Range part independently and
209
- exposes only a complete ordered model set as a cache hit.
205
+ The DBOPFS adapter commits ordered parts independently and exposes only a
206
+ complete ordered model set as a cache hit. Existing whole-file and Range-part
207
+ caches remain readable without migration. Closed parts survive interrupted
208
+ writes; a source without supported resume starts that member's next transfer
209
+ from its beginning. Complete peer members remain cached.
210
210
  `load({offline:true})` never performs a model request; it uses a compatible
211
211
  cached entry or rejects with `ARCANE_AI_MODEL_OFFLINE_MISS`.
212
212
 
213
+ The same store exposes `fetchResource(input, options)` for SDK-owned browser
214
+ model loaders. It returns `{file,status,statusText,headers,url,redirected}`
215
+ after storing the complete response through the shared part owner. Ordinary
216
+ Fetch request options, `signal`, and semantic `onProgress` are supported.
217
+ The browser decision model accepts this store through its additive `store`
218
+ option; resource acquisition stays with the SDK while model selection stays
219
+ with the application. See [browser decisions](browser-decisions.md).
220
+
213
221
  ```javascript
214
222
  const {security, cache} = ai.status().llm;
215
223
  console.log(security?.secure === true); // false when the optional field is absent
@@ -210,6 +210,19 @@ details and [the client contract](core-client.md) for native bridge integration.
210
210
 
211
211
  ## Launch-time locations
212
212
 
213
+ For one app-owned Core shared by the native window and a separate MCP process,
214
+ the launch context may explicitly select `sharedHost:{endpoint,logFile}`. The
215
+ generated native child then connects through a stdio bridge, starting an
216
+ independent headless Core only when the selected endpoint is absent. Its
217
+ endpoint claim precedes application service imports/factories. Closing a native
218
+ window disconnects its bridge; explicit shared-host shutdown drains services.
219
+ Omitting `sharedHost` preserves the existing stdio-owned Core lifetime.
220
+ See [shared Core host ownership](core-shared-host.md) for exact startup,
221
+ connection, cancellation, diagnostics, endpoint, and platform contracts.
222
+ The packaged-web service exposes `current()` as its active `{origin,url,port}`
223
+ or `null`; the shared host replays that current listener to a newly attached
224
+ macOS bridge, without restarting the server or retaining historical events.
225
+
213
226
  The generated Core entry accepts `--arcane-launch-config <path>`. The public
214
227
  `readCoreLaunchContext({argv=process.argv.slice(2)}={})` function in
215
228
  `arcane-os/core/host` reads that explicitly selected JSON object. A native host
@@ -372,6 +385,47 @@ observes complete diagnostics and exit. Cancelling that operation closes its
372
385
  input, allowing the window and accepted Core service work to drain without a
373
386
  forced process termination.
374
387
 
388
+ ### Initial native window size
389
+
390
+ An application can select its initial client area and resizing behavior in
391
+ `arcane-app.json`, independently of its page layout:
392
+
393
+ ```json
394
+ {
395
+ "native": {
396
+ "type": "app",
397
+ "icon": "img/icon.png",
398
+ "order": 100,
399
+ "bundledApps": [],
400
+ "window": {"width": 1280, "height": 800, "resizable": true}
401
+ }
402
+ }
403
+ ```
404
+
405
+ This is the `native` portion of the existing app descriptor. Each `window`
406
+ member is optional. `width` and `height` are positive integral client dimensions
407
+ in logical pixels at 96 DPI; `resizable` is a boolean. Omitting a member retains
408
+ the host's existing choice, and omitting `window` preserves existing startup.
409
+ The native registry projection preserves the record; the browser package
410
+ projection gains no window controls. Portable and Windows assembly copy the
411
+ record to `arcane-native.json` as `window`, without changing page or service
412
+ content.
413
+
414
+ The matching Windows host applies the dimensions once on initial load, using
415
+ the display DPI and fitting the outer window within the selected monitor's
416
+ usable work area. This fit does not impose a persistent maximum size. Explicit
417
+ `resizable:false` uses a fixed frame and disables the maximize control; `true`
418
+ permits ordinary resizing and maximizing. Later app/user resizing remains
419
+ untouched. A composing launcher's `Load` handler runs after this initial sizing
420
+ and can restore its own saved window state. The SDK adds no saved-bounds store.
421
+
422
+ Direct Windows host composition uses nullable `ArcaneHostOptions` members
423
+ `InitialClientWidth`, `InitialClientHeight`, and `Resizable` with the same
424
+ semantics. Older precompiled Windows hosts do not read these options; use the
425
+ matching SDK host archive when rebuilding an application. The portable record
426
+ is available to platform adapters; this addition implements its Windows host
427
+ behavior and leaves window sizing on other platforms unchanged.
428
+
375
429
  ### Application icon and window colors
376
430
 
377
431
  `appDescriptor.native.icon` selects the application's existing app-relative
@@ -71,6 +71,15 @@ receives `(parameters, {application, service, emit, getService, requestId, signa
71
71
  `this` bound to its service definition. A hook or handler may publish a complete
72
72
  service-owned event through `emit(event, data)`.
73
73
 
74
+ A request handler's `emit` places its owning request ID in the event frame's
75
+ optional top-level `requestId`, leaving `data` unchanged. Multi-connection hosts
76
+ can route those frames to the requesting connection even when different clients
77
+ choose the same application `streamId`. Lifecycle-hook `emit` and public
78
+ `runtime.emit` remain uncorrelated service-wide events. Retain that service
79
+ context when an application intentionally publishes a change to every client;
80
+ request-context events belong to the originating request. The ordinary Core
81
+ client still delivers the complete original event data to its listeners.
82
+
74
83
  ### Native service composition
75
84
 
76
85
  `await runtime.getService(name)` and `await context.getService(name)` return the
@@ -0,0 +1,190 @@
1
+ # One application, one shared Core host
2
+
3
+ `arcane-os/core/host` provides a Node local-IPC owner for applications whose
4
+ native UI and separate MCP process use the same catalog, drafts, review queue,
5
+ and Git service. The application chooses one endpoint and one set of services.
6
+ The endpoint is a nonempty local pipe/socket path. The SDK connects to it,
7
+ optionally starts an explicit headless entry,
8
+ and keeps client disconnection separate from service shutdown.
9
+
10
+ This is an explicit same-application connection. It does not scan applications
11
+ or processes, share preference files, copy a catalog, retry requests, or retain
12
+ event history. Application methods and policy stay in the application services.
13
+ Use the [MCP server](mcp-stdio.md) for external MCP framing; local Core continues
14
+ to use the existing `arcane/1` protocol and Content-Length stream framing.
15
+
16
+ ## Host entry
17
+
18
+ ```js
19
+ import {runSharedCoreHost} from 'arcane-os/core/host';
20
+
21
+ const host = await runSharedCoreHost({
22
+ endpoint: process.env.MOON_CORE_ENDPOINT,
23
+ application: {id: 'moon-cheese-ledger'},
24
+ version: '1.0.0',
25
+ async configure(runtime, {signal}) {
26
+ // Only the successful endpoint owner imports and constructs services.
27
+ const {default: createLedger} = await import('./ledger.mjs');
28
+ signal.throwIfAborted();
29
+ runtime.registerService(createLedger());
30
+ }
31
+ });
32
+ await host?.closed;
33
+ ```
34
+
35
+ `startSharedCoreHost({endpoint,application,version,configure,getReplayEvents,
36
+ signal,onError})` binds the selected endpoint **before** calling `configure`.
37
+ The callback receives the existing runtime and the host's lifetime signal.
38
+ Register each service as it is constructed so its drain/disposal belongs to the
39
+ runtime even if a later factory fails. Put asynchronous service initialization
40
+ in `start`, as with ordinary Core composition. Factory imports can run
41
+ concurrently; requests wait for composition, then only their own service's
42
+ startup. Dispatcher readiness does not claim model or service readiness.
43
+
44
+ It resolves to `{runtime,endpoint,closed,close}`. `close()` rejects new work,
45
+ joins composition, closes the runtime, drains complete queued frames, and
46
+ closes its connections. It retains the endpoint claim through service cleanup
47
+ before stopping the listener, so a new launcher cannot start a second service
48
+ owner during an accepted save. Accepted service-lifetime requests finish
49
+ before service drain/disposal. A supplied signal begins this same shutdown;
50
+ the lazy factory receives cancellation and is joined before cleanup. Register
51
+ resources before rejecting after creation. Startup and shutdown errors remain
52
+ observable; `closed` rejects on host shutdown failure. `onError` observes
53
+ transport/background errors, with console diagnostics by default.
54
+
55
+ An endpoint already in use rejects with the native `EADDRINUSE` error and
56
+ `coreHostPhase:'listen'`, without invoking the factory.
57
+ `runSharedCoreHost(options)` adds the explicit headless startup handshake: a
58
+ losing launcher connects to the existing owner and confirms dispatcher response
59
+ without importing its own application service modules. It returns `null` in
60
+ that case. The winning launcher returns the same host object. It reports startup
61
+ success or the complete failure to a parent Node IPC channel when present and
62
+ never calls `process.exit()`.
63
+
64
+ ## Connect, reuse, or start headlessly
65
+
66
+ ```js
67
+ import {connectSharedCoreHost} from 'arcane-os/core/host';
68
+
69
+ const connection = await connectSharedCoreHost({
70
+ endpoint: selectedEndpoint,
71
+ start: {
72
+ command: process.execPath,
73
+ args: [selectedHostEntry],
74
+ logFile: selectedDiagnosticLog
75
+ }
76
+ });
77
+ const catalog = await connection.client.invoke('catalog.read', {});
78
+ // The UI or MCP process is finished. Shared services remain alive.
79
+ await connection.close();
80
+ ```
81
+
82
+ `connectSharedCoreHost({endpoint,start,signal,onError})` first connects to the
83
+ selected endpoint. Only connection absence (`ENOENT` or `ECONNREFUSED`) selects
84
+ `start`; other errors propagate unchanged. Omit `start` for connect-only use.
85
+ The optional record is `{command,args=[],cwd,env,logFile}`. Its arguments and
86
+ environment are captured at acceptance. The selected entry must call
87
+ `runSharedCoreHost`. Parent directories for the endpoint and diagnostic file
88
+ must already exist. No shell, runtime installer, service manager, or automatic
89
+ request retry is involved.
90
+
91
+ Headless startup uses an independent child with ignored stdin and complete
92
+ stdout/stderr appended to the explicit diagnostic file. The child reports its
93
+ endpoint claim/composition result through its startup IPC channel; the launcher
94
+ then disconnects that channel and releases the child process reference. An
95
+ aborted connection/start wait rejects or closes the connecting client; it does
96
+ not kill a host that another connection may already use. The host may therefore
97
+ continue running after a launch wait is cancelled. Startup diagnostics and the
98
+ endpoint remain the actual outcome owners. No startup timeout is imposed by
99
+ this layer; supply a signal when the caller needs a bounded wait.
100
+
101
+ The result is `{client,endpoint,closed,close,shutdown}`. `client` is the existing
102
+ [Core client](core-client.md), with full invoke/cancellation/event behavior and
103
+ current-runtime replay enabled. A connected transport is not a ready model.
104
+ `close()` and `client.close()` cancel this connection's request-lifetime work
105
+ and disconnect it. They leave accepted service-lifetime work and the host
106
+ running. `closed` observes connection termination and rejects on a transport
107
+ error. A subsequent connection uses the still-running owner. Reconnection is
108
+ explicit; failed or uncertain requests are never resubmitted.
109
+
110
+ `await connection.shutdown(invokeOptions)` explicitly invokes the host-owned
111
+ `core.host.shutdown` operation. It drains the shared runtime, returns
112
+ `{state:'closed'}` after successful service cleanup, then closes the connection.
113
+ A drain failure returns the full Core error. Shutdown affects **all** clients
114
+ of that app endpoint and belongs to the application owner's explicit lifecycle,
115
+ not ordinary UI/MCP cleanup. The host reserves this one method; application
116
+ services should use their own namespaces. Cancelling the caller's invoke wait
117
+ after shutdown acceptance does not undo the shared shutdown.
118
+
119
+ ## Correlation, events, and state
120
+
121
+ Each connection has its own request-ID space. The transport substitutes an
122
+ internal Core request ID and restores the originating ID on responses and
123
+ the optional top-level event `requestId`. Existing `data.requestId` values that
124
+ carry that same Core correlation ID are restored too. `parameters`, returned results, schemas,
125
+ documents, and content remain complete and unchanged. A handler's
126
+ `context.requestId` is the Core-side correlation ID; put it in the event's
127
+ separate `requestId` field, not inside application content.
128
+
129
+ Responses and request-context events go to the originating connection in
130
+ order. Core supplies the top-level request correlation outside `data`, so two
131
+ clients may choose the same `streamId` without receiving each other's chunks.
132
+ Retired request frames are never routed to another connection. Ordinary
133
+ application/service events from the lifecycle context or `runtime.emit` are broadcast to
134
+ connected clients. Runtime state includes all active work and translates this
135
+ connection's IDs; other connections' active requests retain Core-side IDs.
136
+ `request.cancel` and `requests.cancelAll` affect only the sending connection's
137
+ requests. A disconnect performs the same scoped cancellation. The underlying
138
+ runtime continues to preserve service-lifetime acceptance.
139
+
140
+ `runtime.replay` sends only that connection the current `core.ready`,
141
+ `core.state`, and `core.service.state` snapshot. Optional synchronous
142
+ `getReplayEvents()` returns current `{event,data}` records from their actual
143
+ service owners. It is for current domain state, not historical chunks or
144
+ retired requests. Subscribe before requesting a domain snapshot when its live
145
+ events may change; applications own those domain methods and state.
146
+
147
+ ## Native launch and platforms
148
+
149
+ The generated portable Core entry keeps ordinary stdio behavior unless the
150
+ explicit launch context contains:
151
+
152
+ ```json
153
+ {
154
+ "sharedHost": {
155
+ "endpoint": "APPLICATION-SELECTED-LOCAL-ENDPOINT",
156
+ "logFile": "APPLICATION-SELECTED-DIAGNOSTIC-FILE"
157
+ }
158
+ }
159
+ ```
160
+
161
+ Both native UI and external MCP clients select the same endpoint. With this
162
+ option, the native Core child is a stdio-to-shared-Core bridge. It connects or
163
+ launches the same entry with `--arcane-core-headless`; the headless entry claims
164
+ the endpoint before importing services. Closing the native window ends its
165
+ bridge, while the independent Core host remains available. Service options and
166
+ the complete launch context retain their existing values. There is no implicit
167
+ shared mode or new application descriptor field.
168
+
169
+ `startSharedCoreBridge({endpoint,start,input=process.stdin,
170
+ output=process.stdout,signal,onError})` is also public. It returns
171
+ `{endpoint,closed,close}` and forwards full framed requests, events, and
172
+ responses. EOF or bridge cancellation disconnects that client only. Terminal
173
+ stream errors settle pending writes and reject `closed`; output is neither
174
+ truncated nor replaced with a fabricated response.
175
+
176
+ On Windows use an app-selected named pipe such as
177
+ `\\.\pipe\moon-cheese-core`; on Linux/macOS use an app-selected Unix-domain
178
+ socket path in an existing writable directory. Select the same absolute Unix
179
+ path across UI and MCP launch contexts so differing working directories do not
180
+ select different endpoints. Node and the operating system
181
+ own their native endpoint requirements. A Unix socket left after an abnormal
182
+ host exit is reported as a connection/bind failure; the SDK does not delete an
183
+ existing endpoint to take ownership. The app owner can resolve that actual
184
+ stale endpoint after establishing its stopped lifecycle. Android requires its
185
+ host adaptation for local IPC and independent process ownership; application
186
+ services and `arcane/1` client semantics remain the shared boundary.
187
+
188
+ This source contract is distinct from a selected package/native artifact or
189
+ execution on a particular platform. Applications adopt it through the published
190
+ SDK and their corresponding native package build, not by copying this source.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "sdkVersion": "0.67.0",
3
+ "sdkVersion": "0.68.0",
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",
@@ -1891,7 +1891,7 @@
1891
1891
  "summary": "Default binding for the canonical PreferenceStore runtime class in Node and managed browsers.",
1892
1892
  "availability": "Node with injected adapters, or browser/native WebView storage",
1893
1893
  "protocol": "Node ESM / authenticated managed browser import map",
1894
- "normalization": "Binding identity and namespace come directly from the canonical runtime module"
1894
+ "normalization": "Canonical runtime binding. Default adapter recognizes only exact absent preferences namespace/matching method or existing Android unsupported capability; single operations retry once through existing app-scoped local storage after disposal/cancellation checks, preserving complete values and context. An advertised rejected batch is never retried serially; its original rejection remains before later local operations. Explicit adapters and partial-service/genuine failures retain their behavior."
1895
1895
  },
1896
1896
  {
1897
1897
  "id": "preference-store:PREFERENCE_STORE_ERROR_CODES",
@@ -4318,7 +4318,7 @@
4318
4318
  "summary": "Creates an app-neutral Core dispatcher with explicit services, request lifetimes, events and graceful drain.",
4319
4319
  "availability": "Node on Windows, Linux and macOS native hosts; Android requires its host adaptation",
4320
4320
  "protocol": "arcane/1 dispatcher and service lifecycle",
4321
- "normalization": "Starts services independently; runtime.getService(name) and context.getService(name) return the same registered object after only its shared startup, without a renderer or global barrier. Lookup is readiness, not a lifetime lease; new lookups reject during shutdown. Preserves complete results/errors, cancels request-lifetime work and drains accepted service work; runtime.replay publishes current dispatcher/service state without restart and preserves reentrant lifecycle publication order"
4321
+ "normalization": "Starts services independently; runtime.getService(name) and context.getService(name) return the same registered object after only its shared startup, without a renderer or global barrier. Lookup is readiness, not a lifetime lease; new lookups reject during shutdown. Preserves complete results/errors, cancels request-lifetime work and drains accepted service work; runtime.replay publishes current dispatcher/service state without restart and preserves reentrant lifecycle publication order. Missing dispatch retains METHOD_NOT_ALLOWED with exact method/namespace and core-namespace-unavailable or core-method-unavailable; named services, registered methods and built-in namespaces establish presence. Handler/startup/permission errors remain unchanged."
4322
4322
  },
4323
4323
  {
4324
4324
  "id": "core-stdio:encodeCoreFrame",
@@ -578,11 +578,11 @@
578
578
  "DBOPFS_REASONS",
579
579
  "default"
580
580
  ],
581
- "summary": "Provides app-scoped OPFS tables, explicit non-recursive empty-table removal, worker I/O, backup/restore, compression, and CRUD/batch APIs.",
581
+ "summary": "Provides app-scoped OPFS tables, committed-change subscriptions with targeted cache freshness, explicit non-recursive empty-table removal, worker I/O, backup/restore, compression, and CRUD/batch APIs.",
582
582
  "availability": "Browser / native WebView",
583
- "protocol": "OPFS, DBOPFSWorker, Compression Streams, and per-realm globalThis.arcaneEvents authority",
583
+ "protocol": "OPFS, DBOPFSWorker, Compression Streams, per-realm globalThis.arcaneEvents authority, and native BroadcastChannel for live same-app/storage-partition committed changes",
584
584
  "normalization": "App scope and recognized file parsing are normalized; nonblank unreadable JSONL rows remain complete raw strings, empty-table removal reports removed, absent, or not-empty after a create:false directory lookup and without scanning or recursively clearing a directory, same-named files are preserved, aliases and cached handles are invalidated only when the directory is gone, and unexpected DOM/storage errors are preserved.",
585
- "surface": "default `DBOPFS`; installs `window.dbopfs`, emits `dbopfs-ready`; table/file/backup APIs including removeEmptyTable(tableName). createCompressedPNG({tableNames,additionalTables,signal}) returns a PNG Blob without download or record writes; omitted names select all, [] none, and supplied tables replace matching saved tables in the export. downloadCompressedPNG(name,options) reuses the same preparation and initiates one download; cancellation prevents it. restoreFromPNG(file,{selectTables}) closes the decoded image after pixel extraction, awaits the optional caller selection/projection before any writes, and rejects DBOPFS_RESTORE_WRITE_FAILED AggregateError after all selected table batches settle when records fail, preserving original errors and failures {tableName,fileName,reason}; successful writes remain. No-options restore and setMany retain their contracts."
585
+ "surface": "default `DBOPFS`; installs `window.dbopfs`, emits `dbopfs-ready`; subscribeChanges(listener,{signal,once}) observes DBOPFS_EVENT_TYPES.change (dbopfs-change), returns idempotent unsubscribe/.dispose, and publishes metadata {applicationId,storagePath,tableName,directoryName,fileName,action,originId,sequence,changeId,remote} after successful storage commits only. Actions are write/append/delete/table-delete; batches preserve partial success, remote receipts invalidate targeted aliases before local publication and never rebroadcast, and pagehide/pageshow closes/reopens the native channel and clears memory caches without replay or fabricated changes. Local subscription/storage continue when BroadcastChannel is unavailable. Table/file/backup APIs include removeEmptyTable(tableName). createCompressedPNG({tableNames,additionalTables,signal}) returns a PNG Blob without download or record writes; omitted names select all, [] none, and supplied tables replace matching saved tables in the export. downloadCompressedPNG(name,options) reuses the same preparation and initiates one download; cancellation prevents it. restoreFromPNG(file,{selectTables}) closes the decoded image after pixel extraction, awaits the optional caller selection/projection before any writes, and rejects DBOPFS_RESTORE_WRITE_FAILED AggregateError after all selected table batches settle when records fail, preserving original errors and failures {tableName,fileName,reason}; successful writes remain. No-options restore and setMany retain their contracts."
586
586
  },
587
587
  {
588
588
  "file": "runtime/arcane/modules/DBOPFSDocumentLibrary.js",
@@ -2018,8 +2018,10 @@ Shared dependencies: [`Theme.js`](runtime-entities.md#themejs).
2018
2018
  Selects and refreshes system, light, dark, or custom theme mode. Native buttons
2019
2019
  retain complete readable labels and wrap when their component runs out of room,
2020
2020
  independently of viewport width. Each button has a `2.75rem` minimum target in
2021
- both dimensions. Selected buttons retain `aria-pressed` and an inset outline;
2022
- disabled choices and keyboard focus have distinct presentation.
2021
+ both dimensions. Selected buttons retain `aria-pressed`, their selected colors
2022
+ and an inset outline while hovered; ordinary hover fill applies only to
2023
+ unselected, enabled choices. Disabled choices and keyboard focus have distinct
2024
+ presentation.
2023
2025
 
2024
2026
  ### Public surface
2025
2027