arcane-os 0.71.2 → 0.73.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 CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.73.0
4
+
5
+ - Add public `resolvePwaRetirementResponse({workspaceRoot, appId})` for existing
6
+ custom Node hosts. It supplies the shared retirement worker response only for
7
+ an explicitly disabled standalone PWA whose authored root worker is absent.
8
+ - Preserve raw `enabled: false` selection before descriptor normalization in
9
+ custom-host and SDK source serving. Omitted settings, enabled PWAs, authored
10
+ workers and other routes keep their existing behavior.
11
+ - Retire only the worker's own registration through the existing browser
12
+ lifecycle. Saved data, caches, DBOPFS and open pages remain untouched; no
13
+ forced reload, polling or replacement server is introduced.
14
+
15
+ ## 0.72.0
16
+
17
+ - Add optional `native.launchContext` defaults for ordinary packaged launch and
18
+ public `resolveNativeLaunchContext()` through `arcane-os/core/host`. Native
19
+ applications and external MCP can select the same app-owned state, workspace,
20
+ shared endpoint and log without reconstructing platform paths.
21
+ - Extend `readCoreLaunchContext()` with explicit defaults and opt-in native
22
+ resolution. Preserve complete explicit launch records, selected workspace
23
+ values, ordinary stdio ownership and the existing no-option entry path.
24
+ - Forward packaged launch defaults through Windows and macOS launchers while
25
+ preserving window, profile, service and shared-host lifecycle behavior. No
26
+ stored-data migration or dependency change is introduced.
27
+
3
28
  ## 0.71.2
4
29
 
5
30
  - Preserve complete browser Kokoro synthesis input through the selected runtime's
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.71.2` SDK contract. Development applications declare
23
+ This checkout defines the `0.73.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;
@@ -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) |
@@ -53,7 +54,7 @@ alone does not make bare module names resolve in a browser.
53
54
  | Choose theme-switcher corner radii, label font and group background through host CSS | [Theme-switcher styling properties](runtime-components.md#theme-switcherhtml) |
54
55
  | Use optional Core preferences and appearance without masking real failures | [PreferenceStore](runtime-modules.md#preferencestorejs), [SystemAppearance](runtime-modules.md#systemappearancejs), and [Core lookup errors](core-runtime.md#state-and-frames) |
55
56
  | Submit complete speech parts immediately and play them in order | [`SpeechPlayback`](runtime-modules.md#speechplaybackjs) and the [basic browser example](ai/browser-speech.md#play-a-complete-array-with-speechplayback) |
56
- | Preserve exact text in an explicit complete synthesis call | [Exact complete synthesis input](ai/browser-speech.md#exact-complete-synthesis-input) and [managed AI narration](sdk-api.md#managed-ai-narration) |
57
+ | Preserve exact speech text and complete browser Kokoro audio | [Exact complete synthesis input](ai/browser-speech.md#exact-complete-synthesis-input) and [managed AI narration](sdk-api.md#managed-ai-narration) |
57
58
  | Select initial native window dimensions and resizing behavior | [Initial native window size](core-native-packaging.md#initial-native-window-size) and [Windows provider](sdk-api.md#createwindowsnativeprovider) |
58
59
  | Call `globalThis.Arcane` | [Arcane Core API](core/arcane-api.md) |
59
60
  | Connect a browser to one Core transport | [Core browser client](core-client.md) |
@@ -61,8 +62,8 @@ alone does not make bare module names resolve in a browser.
61
62
  | Compose native Core dispatch, service lifecycle, and stdio hosting | [Native Core runtime](core-runtime.md) |
62
63
  | Reuse dependency-ready native services without a renderer | [Core service composition](core-runtime.md#native-service-composition) and [existing ONNX owner](local-ai.md#native-service-owners) |
63
64
  | Compose application Core services during source development, independently of local AI | [Development Core services](core-development.md) |
64
- | Share one application service runtime between its native UI and MCP process | [Shared Core host](core-shared-host.md) |
65
- | Store complete browser model resources through one DBOPFS owner | [Model resources](ai/browser-decisions.md#shared-dbopfs-model-resources) and [speech routing](ai/browser-speech.md#ordinary-module-routing) |
65
+ | Share one application service runtime between its ordinary native launch and MCP process | [Shared Core host](core-shared-host.md) and [native launch defaults](core-native-packaging.md#launch-time-locations) |
66
+ | Store complete browser model resources, including the selected decision-runtime entry, through one DBOPFS owner | [Model resources](ai/browser-decisions.md#shared-dbopfs-model-resources) and [speech routing](ai/browser-speech.md#ordinary-module-routing) |
66
67
  | Observe committed storage changes across live application documents | [DBOPFS change subscriptions](runtime-modules.md#dbopfsjs) |
67
68
  | Cancel cold speech preparation while preserving other activation interests | [Prepared narration](ai/browser-speech.md#prepare-narration-once-and-replay-stored-audio) |
68
69
  | Own an explicitly selected native Codex App Server session | [Codex App Server](codex-app-server.md) |
@@ -85,7 +86,7 @@ alone does not make bare module names resolve in a browser.
85
86
  | Write, commit and push complete selected repository text | [Selected-text repository writing](core-repositories.md#write-commit-and-push-selected-text) |
86
87
  | Observe existing checkout configuration and capture an explicit remote/ref | [Repository configuration and selected targets](core-repositories.md#existing-checkout-configuration-and-selected-targets) |
87
88
  | Build an installed-SDK portable Core payload | [Portable Core packaging](core-native-packaging.md) |
88
- | Pass host-selected state defaults and unchanged explicit launch values to services | [Launch-time locations](core-native-packaging.md#launch-time-locations) |
89
+ | Resolve matching native state/workspace/endpoint defaults and preserve explicit launch values | [Launch-time locations](core-native-packaging.md#launch-time-locations) and [resolveNativeLaunchContext()](sdk-api.md#resolvenativelaunchcontext) |
89
90
  | Build a Windows executable without an OS checkout | [Windows executable packaging](core-native-packaging.md#windows-executable) |
90
91
  | Compose a Mac application with an architecture-matched host | [macOS application composition](core-native-packaging.md#macos-application-composition) |
91
92
  | Subscribe to native events | [Arcane event reference](core/arcane-events.md) |
@@ -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)}={})` function in
228
- `arcane-os/core/host` reads that explicitly selected JSON object. A native host
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. With neither argument the reader returns `{}`.
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. The host owns those selections; the SDK does not choose a product's
243
- workspace, namespace, saved preferences or models. JSON values and additional
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 directory.
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 the
150
- explicit launch context contains:
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. There is no implicit
167
- shared mode or new application descriptor field.
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,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "sdkVersion": "0.71.0",
3
+ "sdkVersion": "0.72.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",
7
7
  "moduleSystem": "ESM"
8
8
  },
9
- "memberCount": 297,
9
+ "memberCount": 299,
10
10
  "runtimeSubpathPatterns": {
11
11
  "arcane-os/modules/*": "./runtime/arcane/modules/*",
12
12
  "arcane-os/entities/*": "./runtime/arcane/entities/*"
@@ -542,7 +542,7 @@
542
542
  "summary": "Explicit-load Laya and Julia-1 decisions with caller-selected precision, complete inputs and graph results.",
543
543
  "availability": "Browser module Workers and the selected Transformers.js backend; import/construction starts no model load",
544
544
  "protocol": "Caller-owned state/question/options rows; five-input decision graph; native Worker request/result lifecycle",
545
- "normalization": "Full field tokenization, raw logits and unrounded softmax; dtype defaults to fp16 for Laya and fp32 for Julia, with explicit values forwarded unchanged. Optional existing SDK store routes actual loader resources through fetchResource outside cloned Worker configuration; stored mode disables competing model caches, preserves complete HTTP responses and joins resource cleanup before outstanding calls settle. Omission retains upstream loading; no decision-result persistence or precision fallback"
545
+ "normalization": "Full field tokenization, raw logits and unrounded softmax; dtype defaults to fp16 for Laya and fp32 for Julia, with explicit values forwarded unchanged. Optional existing SDK store routes the complete selected runtime entry and actual loader resources through fetchResource outside cloned Worker configuration. Each activation materializes unchanged entry source with JavaScript MIME and retains its original configured URL for later loads; its object URL is revoked after Worker termination. Stored mode disables competing model caches, preserves complete HTTP responses and joins runtime-entry/resource cleanup before outstanding calls settle. Runtime-entry HTTP failures retain error.response. Custom stored entries must support an object URL and changed import.meta.url; native absolute dependencies and custom factory imports outside upstream preloads remain browser-owned. Omission retains direct native module and upstream model loading; no decision-result persistence or precision fallback"
546
546
  },
547
547
  {
548
548
  "id": "root:acceptWebSocket",
@@ -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"
@@ -2208,6 +2208,20 @@
2208
2208
  "protocol": "arcane-target-adapter/1, arcane-native-build-plan/1, or provider protocol",
2209
2209
  "normalization": "Normalized complete plan and result; target-specific artifact detail preserved"
2210
2210
  },
2211
+ {
2212
+ "id": "root:resolvePwaRetirementResponse",
2213
+ "name": "resolvePwaRetirementResponse",
2214
+ "displayName": "resolvePwaRetirementResponse()",
2215
+ "kind": "function",
2216
+ "signature": "async resolvePwaRetirementResponse({workspaceRoot=process.cwd(), appId}={})",
2217
+ "entrypoints": ["arcane-os"],
2218
+ "primaryImport": "arcane-os",
2219
+ "group": "Workspace, doctor, repository, and server",
2220
+ "summary": "Resolves the shared disabled-PWA root worker response for an existing custom source host, preserving authored workers and browser saved data.",
2221
+ "availability": "Node; browser service-worker lifecycle remains browser-owned",
2222
+ "protocol": "Node ESM and native service-worker lifecycle",
2223
+ "normalization": "Current standalone descriptor and absent-root-worker selection; complete status/headers/body or null"
2224
+ },
2211
2225
  {
2212
2226
  "id": "root:resolveWorkspace",
2213
2227
  "name": "resolveWorkspace",
@@ -2446,7 +2460,7 @@
2446
2460
  ],
2447
2461
  "primaryImport": "arcane-os",
2448
2462
  "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.",
2463
+ "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
2464
  "availability": "Node control plane; browser data plane",
2451
2465
  "protocol": "Node ESM",
2452
2466
  "normalization": "SDK-normalized inputs, errors, events, and documented result"
@@ -3708,7 +3722,7 @@
3708
3722
  "summary": "Creates a local-only Kokoro text-to-speech provider for the provider-neutral AI runtime.",
3709
3723
  "availability": "Browser with DBOPFS, Web Locks, Workers, object URLs, and caller-selected kokoro-js runtime/model content",
3710
3724
  "protocol": "arcane-ai-provider/2 tts/synthesize",
3711
- "normalization": "Independent load, status, request, unload, cancellation and disposal; optional ordinary model.voices records remain complete in the synchronous catalog without loading or constraining synthesis. Provider-native requests return 24000 Hz Float32Array PCM, while shared AI.js requests use wav and return complete Uint8Array audio/wav data; never selects a cloud fallback"
3725
+ "normalization": "Independent load, status, request, unload, cancellation and disposal; optional ordinary model.voices records remain complete in the synchronous catalog without loading or constraining synthesis. Upstream text normalization and phonemization remain selected-runtime behavior. Tokenization disables truncation; short inputs use one inference and longer inputs send every content token through ordered engine-capacity segments, retaining required outer padding and all generated audio in order. Segments share the slot's existing model and disposal owner. Request-local cancellation is observed between segments and before returning audio; an already-running upstream inference may finish. Provider-native requests return 24000 Hz Float32Array PCM, while shared AI.js requests use wav and return complete Uint8Array audio/wav data; never selects a cloud fallback"
3712
3726
  },
3713
3727
  {
3714
3728
  "id": "browser-speech:createBrowserSpeechArtifactGraph",
@@ -4530,19 +4544,33 @@
4530
4544
  "protocol": "arcane/1 preferences methods and schemaVersion 1 entries file",
4531
4545
  "normalization": "Binding identity is exactly equal to the named createPreferencesService export"
4532
4546
  },
4547
+ {
4548
+ "id": "core-host:resolveNativeLaunchContext",
4549
+ "name": "resolveNativeLaunchContext",
4550
+ "displayName": "resolveNativeLaunchContext()",
4551
+ "kind": "function",
4552
+ "signature": "resolveNativeLaunchContext({appId,context={}}={})",
4553
+ "entrypoints": ["arcane-os/core/host"],
4554
+ "primaryImport": "arcane-os/core/host",
4555
+ "group": "Native Core host composition",
4556
+ "summary": "Resolves matching per-app native state, workspace and optional shared endpoint defaults for packaged UI and external MCP.",
4557
+ "availability": "Node native locations on Windows/Linux/macOS; Android and other hosts supply stateRoot and native adaptation",
4558
+ "protocol": "Pure native launch-context resolution and existing local IPC",
4559
+ "normalization": "Returns a new complete context with only omitted stateRoot/workspaceRoot and selected sharedHost endpoint/log fields filled; preserves explicit values and additional payloads. Native launchContext presence opts generated entry into resolution; omitted sharedHost retains stdio. No I/O, saved-state discovery, process startup, migration or ArcaneData/Repos change. Malformed selected appId/stateRoot/sharedHost inputs report TypeError; platform IPC errors remain operation-owned."
4560
+ },
4533
4561
  {
4534
4562
  "id": "core-host:readCoreLaunchContext",
4535
4563
  "name": "readCoreLaunchContext",
4536
4564
  "displayName": "readCoreLaunchContext()",
4537
4565
  "kind": "function",
4538
- "signature": "async readCoreLaunchContext({argv=process.argv.slice(2)}={})",
4566
+ "signature": "async readCoreLaunchContext({argv=process.argv.slice(2),defaults={},appId}={})",
4539
4567
  "entrypoints": ["arcane-os/core/host"],
4540
4568
  "primaryImport": "arcane-os/core/host",
4541
4569
  "group": "Native Core host composition",
4542
- "summary": "Reads explicit launch JSON with the separately supplied native host stateRoot default for service launch context.",
4570
+ "summary": "Combines packaged defaults, actual native host stateRoot and complete explicit launch JSON, with optional native location resolution.",
4543
4571
  "availability": "Node, including the selected external-entry SEA launcher",
4544
4572
  "protocol": "Explicit launch arguments and JSON configuration",
4545
- "normalization": "Optional --arcane-host-state-root <directory> supplies the stateRoot default; every explicit --arcane-launch-config JSON field takes precedence unchanged, including relative or null stateRoot and application-owned preferencesFile. Returns an empty record with neither argument; surfaces missing argument values, file and JSON failures; performs no directory selection, path resolution, configuration search, rewrite, migration or environment mutation"
4573
+ "normalization": "Precedence is defaults, then --arcane-host-state-root, then complete top-level --arcane-launch-config JSON fields; nested values replace without deep merging or payload rewriting. Explicit appId invokes resolveNativeLaunchContext; ordinary calls retain relative/null/custom state values and return an empty record with no inputs. Missing arguments, file/JSON errors and malformed records reject. No configuration search, file rewrite, migration or environment mutation; generated native.launchContext explicitly selects the resolver."
4546
4574
  },
4547
4575
  {
4548
4576
  "id": "core-host:startCoreHost",
@@ -207,6 +207,66 @@ The server path uses the existing portable Node serving owner, and browser
207
207
  retirement requires the browser's native service-worker support and an existing
208
208
  registration eligible for that update.
209
209
 
210
+ ### Retirement through an existing custom host
211
+
212
+ An application that already serves its source through `node-http-server` can
213
+ keep that server and delegate only its root worker route to the public SDK
214
+ representation owner:
215
+
216
+ ```js
217
+ import {Server} from 'node-http-server';
218
+ import {resolvePwaRetirementResponse} from 'arcane-os';
219
+
220
+ // This is the application's existing subclass and workspace root.
221
+ class DragonMailServer extends Server {
222
+ async onRequest(request, response) {
223
+ const pathname = new URL(request.originalUrl || request.url, 'http://localhost').pathname;
224
+ if ((request.method === 'GET' || request.method === 'HEAD') && pathname === '/arcane-sw.js') {
225
+ const retirement = await resolvePwaRetirementResponse({
226
+ workspaceRoot: this.workspaceRoot,
227
+ appId: 'dragon-mail'
228
+ });
229
+ if (retirement) {
230
+ response.statusCode = retirement.statusCode;
231
+ for (const [name, value] of Object.entries(retirement.headers)) {
232
+ response.setHeader(name, value);
233
+ }
234
+ await this.serve(request, response, retirement.body);
235
+ return true;
236
+ }
237
+ }
238
+ // Keep the application's other routes here before ordinary static serving.
239
+ return false;
240
+ }
241
+ }
242
+ ```
243
+
244
+ Use the application's actual root and ID. The SDK reads the current
245
+ `arcane-packager.json` and, for standalone `appsRoot:"."`, the authored root
246
+ `arcane-app.json`; only an absent authored descriptor selects
247
+ `arcane-package.json`. Reads use the existing descriptor projection and package
248
+ contract without rewriting either file or requiring a stale generated projection
249
+ to equal a newly edited authored descriptor. `appId` is optional; when supplied,
250
+ it must match that descriptor's ID.
251
+
252
+ The result is `null` for multi-app workspaces, omitted or enabled PWA, an omitted
253
+ `pwa.enabled` field (including `{pwa:{}}`), and any existing root `arcane-sw.js`
254
+ entry. The author's explicit `pwa.enabled:false` choice and an `ENOENT`
255
+ worker result produce `{statusCode, headers, body}`: status `200`, the shared
256
+ JavaScript content type, `Cache-Control: no-cache`, and the complete existing
257
+ retirement script. Malformed configuration and other filesystem failures reject
258
+ instead of selecting another descriptor or treating a file as absent.
259
+
260
+ The host applies those headers directly; it must not replace `no-cache` with
261
+ its unrelated response defaults or treat a former enabled worker's validator as
262
+ validation of this response. The existing `serve()` method owns response
263
+ completion and HEAD body omission. Keep this exact worker route at the serving
264
+ origin that owns the registration; application HTTPS redirects and other routes
265
+ remain application-owned. This API starts no server, writes no file, performs
266
+ no offline-inventory scan, and touches no browser data. It shares the same
267
+ response policy and worker generator as the SDK source server. The native
268
+ activation and existing-page lifetime limits above also apply here.
269
+
210
270
  ### Authored host root and app entry
211
271
 
212
272
  An authored `index.html` at the source host root keeps `/` separate from the
@@ -57,7 +57,7 @@ runtime layouts.
57
57
  | `arcane-os/core/runtime` | App-neutral native Core dispatcher and service lifecycle. |
58
58
  | `arcane-os/core/development` | App-service composition and document-owned Core transport during source development, independently of local AI. |
59
59
  | `arcane-os/core/stdio` | Framed Node stdio transport with graceful runtime drain. |
60
- | `arcane-os/core/host` | Node-only reusable Core host lifecycle with explicitly supplied services. |
60
+ | `arcane-os/core/host` | Node-only reusable Core host lifecycle, app-selected native launch defaults and shared native location resolution. |
61
61
  | `arcane-os/codex/app-server` | Explicit native Codex App Server session, complete live turns, server requests and recovery. |
62
62
  | `arcane-os/mcp/stdio` | Node MCP STDIO server for app-owned tools and static resources. |
63
63
  | `arcane-os/core/repositories` | Native persistent ArcaneData paths, connected working repositories and scoped non-secret Git identity configuration. |
@@ -429,7 +429,8 @@ The remaining data-only subpaths are eight JSON Schemas and package metadata.
429
429
  | `connectSharedCoreHost()` | function | `arcane-os/core/host` | Shared Core host | Node local IPC and optional explicit headless startup |
430
430
  | `runSharedCoreHost()` | function | `arcane-os/core/host` | Shared Core host | Node headless entry and optional parent startup IPC |
431
431
  | `startSharedCoreBridge()` | function | `arcane-os/core/host` | Shared Core host | Node readable/writable streams and local IPC |
432
- | `readCoreLaunchContext()` | function | `arcane-os/core/host` | Native Core host composition | Node; reads explicitly selected launch JSON |
432
+ | `readCoreLaunchContext()` | function | `arcane-os/core/host` | Native Core host composition | Node; combines packaged defaults, host state and explicitly selected launch JSON |
433
+ | `resolveNativeLaunchContext()` | function | `arcane-os/core/host` | Native Core host composition | Node native paths; Android supplies its adapted state root |
433
434
  | `resolveArcaneDataPaths()` | function | `arcane-os/core/repositories` | Native repository workspaces | Node on Windows, Linux and macOS; Android supplies its data root |
434
435
  | `createRepositoryWorkspace()` | function | `arcane-os/core/repositories` | Native repository workspaces | Node with Git; Android requires host process adaptation |
435
436
  | `readGitIdentity()` | function | `arcane-os/core/repositories` | Native repository workspaces | Node with Git; Android requires host process adaptation |
@@ -4205,6 +4206,39 @@ There is no history scan, app loop, diagnostic payload store or automatic retry.
4205
4206
  The authored `test/websocket.test.mjs` fixtures cover this boundary; source
4206
4207
  inspection is distinct from executing them or verifying a browser deployment.
4207
4208
 
4209
+ ## resolvePwaRetirementResponse()
4210
+
4211
+ ```text
4212
+ async resolvePwaRetirementResponse({workspaceRoot=process.cwd(), appId}={})
4213
+ ```
4214
+
4215
+ Import from `arcane-os`. This Node API supplies the shared disabled-PWA worker
4216
+ response to an application's existing server without starting or replacing it.
4217
+ It reads the current standalone workspace configuration and root authored
4218
+ descriptor, using the package descriptor only when the authored file is absent.
4219
+ The optional `appId` must match the selected descriptor. It uses the existing
4220
+ configuration contracts, performs no writes and rereads current configuration
4221
+ on each call; there is no retained cache, timer, subscription or dispose handle.
4222
+
4223
+ It returns `null` for a non-standalone workspace, omitted or enabled PWA, an
4224
+ omitted `pwa.enabled` field, or an existing root `arcane-sw.js` entry. The author's
4225
+ explicit `pwa.enabled:false` choice plus an `ENOENT` root
4226
+ worker produces `{statusCode:200, headers, body}`, with the existing JavaScript
4227
+ content type, `Cache-Control: no-cache`, and the complete retirement script.
4228
+ Configuration/JSON errors and other filesystem failures reject. The result
4229
+ carries no prior worker `Last-Modified` validator.
4230
+
4231
+ The caller owns its exact GET/HEAD `/arcane-sw.js` route and applies the supplied
4232
+ status, headers and body through its existing `node-http-server` public
4233
+ `serve()` path. A `null` result leaves normal routing intact. There is no request
4234
+ payload transformation, server listener, generated file, cache/storage cleanup,
4235
+ client enumeration or reload. The worker skips waiting on installation and
4236
+ unregisters its own registration on activation; existing-page controller timing
4237
+ remains browser-owned. The [custom-host PWA guide](pwa.md#retirement-through-an-existing-custom-host)
4238
+ shows the complete hook and preservation contract. The Node filesystem path is
4239
+ portable across Windows, Linux and macOS; Android needs its normal Node host
4240
+ adaptation, and browser retirement requires native service-worker support.
4241
+
4208
4242
  ## startDevServer()
4209
4243
 
4210
4244
  ### Overview
@@ -7925,29 +7959,90 @@ The default binding from `arcane-os/core/diarization` is exactly the named
7925
7959
  import createDiarizationService from 'arcane-os/core/diarization';
7926
7960
  ```
7927
7961
 
7962
+ ## resolveNativeLaunchContext()
7963
+
7964
+ ### Overview
7965
+
7966
+ Resolves one application's native state, workspace and optional shared endpoint
7967
+ through the same public owner used by its packaged executable and external MCP.
7968
+ This synchronous function performs no I/O, saved-state discovery, process start
7969
+ or migration.
7970
+
7971
+ ### Signature and result
7972
+
7973
+ ```text
7974
+ resolveNativeLaunchContext({appId,context={}}={})
7975
+ ```
7976
+
7977
+ Import from `arcane-os/core/host`. The result is a new complete context with
7978
+ omitted fields filled. Windows defaults to `%LOCALAPPDATA%/Arcane/<appId>`
7979
+ (home `AppData/Local` fallback), macOS to
7980
+ `~/Library/Application Support/Arcane/<appId>`, and portable Linux Node to
7981
+ absolute `$XDG_DATA_HOME/Arcane/<appId>` or `~/.local/share/Arcane/<appId>`.
7982
+ Android and other adapted hosts provide their actual `stateRoot` and own native
7983
+ IPC/process adaptation.
7984
+
7985
+ An omitted `workspaceRoot` becomes `<absolute selected stateRoot>/Workspace`.
7986
+ Only a selected `sharedHost` object receives a default endpoint and log file:
7987
+ Windows uses a named pipe containing the URL-encoded complete absolute state
7988
+ path; Linux/macOS use `<absolute selected stateRoot>/core.sock`. The log is
7989
+ `<absolute selected stateRoot>/Core.log`. Explicit fields, additional payloads
7990
+ and explicit workspace values remain unchanged. Relative explicit paths retain
7991
+ the caller's working-directory meaning. `appId` and the selected state path
7992
+ must be nonempty strings; a selected `sharedHost` must be an object. Malformed
7993
+ inputs report `TypeError`; native endpoint failures belong to the IPC operation.
7994
+
7995
+ ```javascript
7996
+ import {resolveNativeLaunchContext} from 'arcane-os/core/host';
7997
+
7998
+ const context = resolveNativeLaunchContext({
7999
+ appId: 'moon-cheese-post',
8000
+ context: {sharedHost: {}}
8001
+ });
8002
+ // Use context.sharedHost.endpoint for the native UI and external MCP.
8003
+ // An explicit context.workspaceRoot or context.stateRoot remains selected.
8004
+ ```
8005
+
8006
+ In a complete app descriptor, `native.launchContext:{sharedHost:{}}` selects
8007
+ these defaults for ordinary packaged executable launch. Omitting `sharedHost`
8008
+ keeps stdio-owned lifetime; omitting `native.launchContext` preserves the prior
8009
+ generated-entry path. The selected shared entry creates its state directory
8010
+ for socket/log use; application services own workspace I/O. MCP-first startup
8011
+ prepares that returned state directory before the existing `start` option opens
8012
+ its log. No caller needs to reconstruct platform paths. The separate
8013
+ `resolveArcaneDataPaths()` repository-location contract remains unchanged.
8014
+ See [launch-time locations](core-native-packaging.md#launch-time-locations) and
8015
+ [shared native/MCP hosting](core-shared-host.md#native-launch-and-platforms).
8016
+
7928
8017
  ## readCoreLaunchContext()
7929
8018
 
7930
8019
  ### Overview
7931
8020
 
7932
- Reads the complete JSON object explicitly selected by
7933
- `--arcane-launch-config <path>`, with an optional separately supplied native host
7934
- state-directory default. It does not search for application configuration.
8021
+ Combines complete packaged defaults, a separately supplied native host state
8022
+ directory and the complete JSON object selected by `--arcane-launch-config`.
8023
+ An explicit `appId` selects the native resolver; ordinary calls remain a
8024
+ configuration reader without automatic location selection.
7935
8025
 
7936
8026
  ### Signature and result
7937
8027
 
7938
8028
  ```text
7939
- async readCoreLaunchContext({argv=process.argv.slice(2)}={})
8029
+ async readCoreLaunchContext({argv=process.argv.slice(2),defaults={},appId}={})
7940
8030
  ```
7941
8031
 
7942
- Import it from `arcane-os/core/host`. `--arcane-host-state-root <directory>`
7943
- supplies a `stateRoot` default; every field from the explicit launch JSON object
7944
- then takes precedence unchanged, including a relative, null or other explicitly
7945
- present `stateRoot`. With neither argument, the promise resolves to `{}`.
7946
- A missing argument value, unreadable launch file, malformed JSON or non-object
7947
- value rejects. Relative launch-file paths use the process working directory.
7948
- The reader does not choose, resolve or validate the state directory, rewrite
7949
- content, discover or migrate stored data, or mutate the environment. An
7950
- application-selected `preferencesFile` remains a separate unchanged field.
8032
+ Import from `arcane-os/core/host`. Precedence is `defaults`, then
8033
+ `--arcane-host-state-root <directory>`, then every complete top-level field from
8034
+ the explicit launch JSON object. Nested fields replace rather than deep-merge.
8035
+ The original file and its payload remain unchanged. Supplying `appId` applies
8036
+ `resolveNativeLaunchContext` to that merged record; its defaults and input
8037
+ contract then apply. Without `appId`, relative, null and other explicit state
8038
+ values retain the prior reader semantics. With no arguments or defaults, the
8039
+ ordinary call still resolves to `{}`.
8040
+
8041
+ Missing argument values, unreadable files, malformed JSON, non-object defaults
8042
+ or launch records reject. Relative launch-file paths use the working directory.
8043
+ The reader searches no other configuration, rewrites no content, migrates no
8044
+ data and changes no environment. Application-owned fields such as
8045
+ `preferencesFile` retain their complete values.
7951
8046
 
7952
8047
  ```javascript
7953
8048
  import {readCoreLaunchContext} from 'arcane-os/core/host';
@@ -7956,12 +8051,13 @@ const launchContext = await readCoreLaunchContext();
7956
8051
  ```
7957
8052
 
7958
8053
  The generated entry supplies artifact-derived `appRoot`, then applies the whole
7959
- explicit context and passes the same object as every service factory's second
7960
- argument. Authored service options remain the first argument. Workspace/state
7961
- locations and additional fields belong to the launcher/application. Windows and
7962
- Mac launchers forward their actual host-selected state directory through the
7963
- separate argument; other composing hosts may use the same seam. This JavaScript
7964
- contract does not establish a compiled host or matching release archive. See
8054
+ context and passes the same object as every service factory's second argument.
8055
+ Authored service options remain the first argument. It supplies the descriptor's
8056
+ `native.launchContext` as `defaults` and the app ID only when that optional
8057
+ field is present. Matching Windows and macOS launchers apply the same packaged
8058
+ defaults before explicit launch JSON and forward their actual state directory;
8059
+ other composing hosts can use the same public seam. Compiled host availability
8060
+ and native execution are separate from this JavaScript contract. See
7965
8061
  [launch-time locations](core-native-packaging.md#launch-time-locations).
7966
8062
 
7967
8063
  ## resolveArcaneDataPaths()
@@ -9550,13 +9646,26 @@ No decisions are inserted into chat history or durable storage.
9550
9646
 
9551
9647
  Optional `store` is an existing SDK DBOPFS model or speech store exposing
9552
9648
  `fetchResource(input, options)`. It remains outside cloned Worker configuration.
9553
- The selected loader routes its actual model/configuration/tokenizer resources
9554
- through that owner and disables competing model caches in this explicit mode.
9555
- Omission preserves ordinary upstream loading. Native ESM imports retain their
9556
- platform loader. Resource progress counts closed shards; cancellation joins
9557
- resource cleanup before outstanding `load()`/`evaluate()` calls settle.
9649
+ Explicit activation first stores the complete selected `runtime.moduleUrl`
9650
+ entry and materializes it as a JavaScript object URL without rewriting source.
9651
+ The configured original URL remains the resource selection for later loads.
9652
+ The selected loader also routes its actual model/configuration/tokenizer
9653
+ resources through that owner and disables competing model caches in this mode.
9654
+ Each activation revokes its runtime object URL after terminating its Worker on
9655
+ cancellation, failure, unload or disposal. Resource progress counts closed
9656
+ shards; cancellation joins runtime-entry and resource cleanup before outstanding
9657
+ `load()`/`evaluate()` calls settle. Runtime-entry HTTP failures report
9658
+ `ARCANE_DECISION_RUNTIME_DOWNLOAD_FAILED` with the complete stored response in
9659
+ `error.response` for developer diagnostics.
9558
9660
  `unload()` and `dispose()` acknowledge their state transition synchronously.
9559
9661
 
9662
+ The default runtime entry is self-contained. Custom stored entries must work
9663
+ from an object URL: entry materialization changes `import.meta.url` and does not
9664
+ rewrite relative imports or original-location support-file references. Native
9665
+ absolute ESM dependencies and custom factory imports outside upstream preloads
9666
+ remain browser-owned. Omitting `store` preserves direct native module loading
9667
+ and ordinary upstream model loading. See [shared model resources](ai/browser-decisions.md#shared-dbopfs-model-resources).
9668
+
9560
9669
  ### Example
9561
9670
 
9562
9671
  ```js
@@ -10442,6 +10551,18 @@ mono 16-bit PCM. Unsupported formats fail
10442
10551
  `ARCANE_AI_UNSUPPORTED_RESPONSE_FORMAT`; malformed adapter audio fails
10443
10552
  `ARCANE_AI_INVALID_PROVIDER_RESULT`. Unknown fields and accessors reject as malformed.
10444
10553
 
10554
+ The Kokoro adapter retains upstream text normalization and phonemization, then
10555
+ tokenizes without truncation and sends every content token through the selected
10556
+ engine. Short inputs use one inference; longer inputs use ordered segments
10557
+ within that engine's capacity and return all generated audio in order. Each
10558
+ segment retains the engine's required outer padding, voice-style selection and
10559
+ speed. The segments share the slot's existing loaded model and disposal owner;
10560
+ they do not create another model or provider-pool slot. Cancellation is checked
10561
+ between inference segments and before returning audio; already-running upstream
10562
+ inference may finish. This engine transport is distinct from the SDK's optional
10563
+ `textFormat:'plain'` input treatment. See [complete synthesis input](ai/browser-speech.md#exact-complete-synthesis-input)
10564
+ for the selected tokenizer/model contract and upstream revision behavior.
10565
+
10445
10566
  Automatic execution tries a complete WebNN NPU pool when
10446
10567
  `navigator.ml.createContext` is exposed, then WebGPU when `navigator.gpu` is
10447
10568
  exposed, then CPU through WASM. A failed candidate is cleaned up before a fresh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.71.2",
3
+ "version": "0.73.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",
@@ -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,
@@ -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
- * The host's separate state-root argument supplies a launch-context default.
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 defaults = {};
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
- defaults.stateRoot = stateRoot;
80
+ context.stateRoot = stateRoot;
24
81
  }
25
82
  const flag = argv.indexOf('--arcane-launch-config');
26
- if (flag === -1) return defaults;
27
- const filename = argv[flag + 1];
28
- if (filename === undefined || filename === '') {
29
- throw new TypeError('--arcane-launch-config requires a filename.');
30
- }
31
- const context = JSON.parse(await readFile(filename, 'utf8'));
32
- if (context === null || !is.object(context) || is.array(context)) {
33
- throw new TypeError('Core launch configuration must be a JSON object.');
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 {...defaults, ...context};
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
- _launchContext = ArcaneReadRecord([NSURL fileURLWithPath:_launchFilename], error);
236
- if (!_launchContext) return NO;
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
- : [[NSHomeDirectory() stringByAppendingPathComponent:@"Library/Application Support/Arcane"] stringByAppendingPathComponent:appID];
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
- // Core rereads this exact selected file. Do not normalize,
61
- // rewrite, filter or copy the application's launch record.
62
- Dictionary<string, object> launchContext = launchFilename == null ? null : ReadRecord(launchFilename);
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 != null && launchContext.TryGetValue("stateRoot", out stateRoot) && stateRoot is string
84
+ string profileParent = launchContext.TryGetValue("stateRoot", out stateRoot) && stateRoot is string
65
85
  ? Path.GetFullPath((string)stateRoot)
66
- : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Arcane", appId);
86
+ : Path.Combine(defaultStateBase, "Arcane", appId);
67
87
  OpenDiagnostics(Path.Combine(profileParent, "Diagnostics"));
68
88
  ArcaneHostOptions options = new ArcaneHostOptions
69
89
  {
@@ -17,7 +17,7 @@ import {
17
17
  readWorkspaceAssetVersion,resolveAssetReference,rewriteAssetReferences,versionAssetUrl
18
18
  } from './import-map.mjs';
19
19
  import {createPwaArtifacts,selectPwaFiles} from './pwa.mjs';
20
- import {createPwaRetirementWorkerScript} from './pwa-worker.mjs';
20
+ import {createPwaRetirementResponse} from './pwa-retirement.mjs';
21
21
 
22
22
  const is = new Is(false);
23
23
 
@@ -1074,6 +1074,7 @@ async function startOwnedDevServer({
1074
1074
  currentSourceRoutes = {
1075
1075
  ...routeSet,
1076
1076
  app: manifest,
1077
+ pwaExplicitlyDisabled: (authored ? value.package?.pwa : value.pwa)?.enabled === false,
1077
1078
  rootDocument,
1078
1079
  startPath: applicationSourcePath(routeSet.config,routeSet.appId,manifest.entry),
1079
1080
  mappings: mappings.map(
@@ -1303,26 +1304,18 @@ async function startOwnedDevServer({
1303
1304
  return;
1304
1305
  }
1305
1306
  if (mode === 'source' && selectedRoutes.config.appsRoot === '.'
1306
- && selectedRoutes.app?.pwa?.enabled === false && target.path === '/arcane-sw.js') {
1307
- let rootWorkerExists = true;
1308
- try {
1309
- await lstat(path.join(selectedRoutes.workspaceRoot, 'arcane-sw.js'));
1310
- } catch (error) {
1311
- if (error?.code !== 'ENOENT') throw error;
1312
- rootWorkerExists = false;
1313
- }
1314
- if (!rootWorkerExists) {
1315
- response.setHeader('Cache-Control', 'no-cache');
1316
- await serveGeneratedRepresentation(
1317
- fileServer,
1318
- request,
1319
- response,
1320
- {
1321
- // A former enabled worker's date cannot validate this retirement script.
1322
- contentType: contentTypes.js,
1323
- body: createPwaRetirementWorkerScript
1324
- }
1325
- );
1307
+ && selectedRoutes.pwaExplicitlyDisabled && target.path === '/arcane-sw.js') {
1308
+ const retirement = await createPwaRetirementResponse({
1309
+ workspaceRoot: selectedRoutes.workspaceRoot,
1310
+ pwa: selectedRoutes.app.pwa
1311
+ });
1312
+ if (retirement) {
1313
+ response.statusCode = retirement.statusCode;
1314
+ for (const [name, value] of Object.entries(retirement.headers)) {
1315
+ response.setHeader(name, value);
1316
+ }
1317
+ // A former enabled worker's date cannot validate this retirement script.
1318
+ await fileServer.serve(request, response, retirement.body);
1326
1319
  return;
1327
1320
  }
1328
1321
  }
package/src/index.mjs CHANGED
@@ -146,6 +146,7 @@ export {
146
146
  } from './workspace.mjs';
147
147
  export {materializeInstalledSdkRuntime} from './installed-sdk-runtime.mjs';
148
148
  export {startDevServer} from './dev-server.mjs';
149
+ export {resolvePwaRetirementResponse} from './pwa-retirement.mjs';
149
150
  export {createDevelopmentCore} from './core/development.mjs';
150
151
  export {startSourceExampleServer} from './source-server.mjs';
151
152
  export {acceptWebSocket} from './websocket.mjs';
@@ -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})
@@ -0,0 +1,53 @@
1
+ import {lstat, readFile} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import contentTypes from 'node-http-server/mime-types';
4
+ import {APP_DESCRIPTOR_NAME, projectPackageManifest} from './app-descriptor.mjs';
5
+ import {APP_CONFIG_NAME, validateAppConfig} from './packager/core.mjs';
6
+ import {inspectWorkspaceProfile} from './workspace.mjs';
7
+ import {createPwaRetirementWorkerScript} from './pwa-worker.mjs';
8
+
9
+ export async function resolvePwaRetirementResponse({workspaceRoot = process.cwd(), appId} = {}) {
10
+ const profile = await inspectWorkspaceProfile(workspaceRoot);
11
+ if (profile.config.appsRoot !== '.') return null;
12
+
13
+ let manifestPath = path.join(profile.workspaceRoot, APP_DESCRIPTOR_NAME);
14
+ let source;
15
+ let authored = true;
16
+ try {
17
+ source = await readFile(manifestPath, 'utf8');
18
+ } catch (error) {
19
+ if (error?.code !== 'ENOENT') throw error;
20
+ authored = false;
21
+ manifestPath = path.join(profile.workspaceRoot, APP_CONFIG_NAME);
22
+ source = await readFile(manifestPath, 'utf8');
23
+ }
24
+ const value = JSON.parse(source);
25
+ const manifest = authored ? projectPackageManifest(value) : value;
26
+ validateAppConfig(
27
+ manifest, appId ?? manifest.id, profile.config, manifestPath
28
+ );
29
+ // Normalization defaults an omitted enabled field to false. Retirement
30
+ // instead consumes the author's explicit choice from the original record.
31
+ const pwa = authored ? value.package.pwa : value.pwa;
32
+ return createPwaRetirementResponse({workspaceRoot: profile.workspaceRoot, pwa});
33
+ }
34
+
35
+ // Source serving already owns a current descriptor snapshot. Reuse that snapshot
36
+ // without introducing another descriptor read or changing its refresh lifetime.
37
+ export async function createPwaRetirementResponse({workspaceRoot, pwa}) {
38
+ if (pwa?.enabled !== false) return null;
39
+ try {
40
+ await lstat(path.join(workspaceRoot, 'arcane-sw.js'));
41
+ return null;
42
+ } catch (error) {
43
+ if (error?.code !== 'ENOENT') throw error;
44
+ }
45
+ return {
46
+ statusCode: 200,
47
+ headers: {
48
+ 'Content-Type': contentTypes.js,
49
+ 'Cache-Control': 'no-cache'
50
+ },
51
+ body: createPwaRetirementWorkerScript()
52
+ };
53
+ }