arcane-os 0.58.0 → 0.59.1

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,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.59.1
4
+
5
+ - Forward the existing AI synthesis signal into native Core synthesis and
6
+ preserve the established AI cancellation error contract.
7
+ - Carry `AI.fetchSTT(audio, signal)` cancellation through
8
+ `Arcane.speech.transcribe(request, {signal})` into the existing Core request
9
+ lifetime. Preserve complete requests and results, one-argument facade calls,
10
+ unrelated errors and the 180-second timeout. Actual inference interruption
11
+ and resource release remain with the selected speech service.
12
+ - No speech engine, model, runtime dependency, default-provider switch or data
13
+ migration is introduced.
14
+
15
+ ## 0.59.0
16
+
17
+ - Reconcile marked generated PWA references when a root application's PWA is
18
+ disabled. Add explicit `importMap({retireGeneratedPwa:true})` retirement for
19
+ the caller-confirmed generated root manifest reference and four standard PWA
20
+ output files. Ordinary disabled refresh preserves unmarked references and
21
+ files; authored content, unselected pages and browser storage remain intact.
22
+ - Forward the native host's selected state directory to Core service factories
23
+ as a launch-context default. Every explicit launch JSON field takes precedence
24
+ unchanged. Windows launcher source now supplies the same separate argument;
25
+ using that launcher correction requires the matching rebuilt host archive.
26
+ - Forward optional `Arcane.speech.synthesize(request, {signal})` cancellation
27
+ through the existing request lifetime, preserving complete payloads and the
28
+ 180,000 ms default. Actual engine interruption remains service-owned; no new
29
+ speech engine, runtime dependency or data migration is introduced.
30
+
3
31
  ## 0.58.0
4
32
 
5
33
  - Add native ONNX sessions through `arcane-os/local-ai/onnx` and browser Core
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.58.0` SDK contract. Development applications declare
23
+ This checkout defines the `0.59.1` 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;
@@ -568,8 +568,8 @@ export function createCoreFacade(client){
568
568
  remove:name=>invoke('environment.delete',{name})},
569
569
  mail:{send:request=>invoke('mail.send',request??{},{timeoutMs:450000})},
570
570
  speech:{status:()=>invoke('speech.status',{},{timeoutMs:10000}),
571
- synthesize:request=>invoke('speech.synthesize',request??{},{timeoutMs:180000}),
572
- transcribe:request=>invoke('speech.transcribe',request??{},{timeoutMs:180000})},
571
+ synthesize:(request,options)=>invoke('speech.synthesize',request??{},{timeoutMs:180000,signal:options?.signal}),
572
+ transcribe:(request,options)=>invoke('speech.transcribe',request??{},{timeoutMs:180000,signal:options?.signal})},
573
573
  localAI:{status:()=>invoke('localai.status',{},{timeoutMs:15000}),
574
574
  ensurePlatform:()=>invoke('localai.platform.ensure',{},long),recover:request=>invoke('localai.services.recover',request??{},long),
575
575
  setParallelRequests:request=>invoke('localai.parallel.requests.set',request??{},long),
@@ -42,6 +42,7 @@ alone does not make bare module names resolve in a browser.
42
42
  | Inspect complete AI and speech calls using the shared developer-mode preference | [Shared logger](sdk-api.md#arcanelogging) and [speech developer diagnostics](ai/browser-speech.md#developer-diagnostics) |
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
+ | Disable source PWA generation or explicitly retire confirmed generated output | [Source PWA retirement](pwa.md#disable-source-pwa-generation-and-retire-generated-output) |
45
46
  | 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) |
46
47
  | Connect an application through the shared WebSocket transport | [Shared WebSocket clients](websocket-client.md) |
47
48
  | Choose browser, native, cloud, or cross-host behavior | [Availability and normalization](availability-and-normalization.md) |
@@ -51,10 +52,16 @@ alone does not make bare module names resolve in a browser.
51
52
  | 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) |
52
53
  | Call `globalThis.Arcane` | [Arcane Core API](core/arcane-api.md) |
53
54
  | Connect a browser to one Core transport | [Core browser client](core-client.md) |
55
+ | Cancel one Core speech synthesis request without changing its payload | [Speech request lifetime](core-client.md#events-and-request-lifetime) and [Core facade](sdk-api.md#createcorefacade) |
54
56
  | Compose native Core dispatch, service lifecycle, and stdio hosting | [Native Core runtime](core-runtime.md) |
55
57
  | Run application-selected llama.cpp, Ollama or ONNX through Core | [Local AI through Core](local-ai.md) |
58
+ | Retain a Nemotron model and diarize caller-fed audio streams | [Native speaker diarization](diarization.md) |
59
+ | Persist application-selected preferences through Core | [Core preferences service](core-preferences.md) |
60
+ | Read complete selected Git text without changing a checkout | [Git text snapshots](git-text-snapshot.md) |
56
61
  | Build an installed-SDK portable Core payload | [Portable Core packaging](core-native-packaging.md) |
62
+ | Pass host-selected state defaults and unchanged explicit launch values to services | [Launch-time locations](core-native-packaging.md#launch-time-locations) |
57
63
  | Build a Windows executable without an OS checkout | [Windows executable packaging](core-native-packaging.md#windows-executable) |
64
+ | Compose a Mac application with an architecture-matched host | [macOS application composition](core-native-packaging.md#macos-application-composition) |
58
65
  | Subscribe to native events | [Arcane event reference](core/arcane-events.md) |
59
66
  | Use provider-neutral AI lifecycle, chat, speech, persistence, or document context | [Normalized AI](#normalized-ai) |
60
67
  | Choose a normalized typed browser decision surface | [Browser typed decisions](ai/browser-decisions.md) |
@@ -58,12 +58,12 @@ false when a probe throws; SDK branching continues through its owning error path
58
58
  policy, package-owned model policy, platform support, and dependency readiness
59
59
  are independent checks.
60
60
 
61
- Windows x64 has an SDK-owned precompiled WebView2/SEA host; Linux and Android
62
- retain their explicit checkout providers where listed. The SDK also includes
63
- macOS Foundation process-adapter source, separately from an executable target,
64
- renderer bridge, artifact or run command. A composed macOS host may combine that
65
- source with the reusable Node Core runtime, stdio transport and portable payload.
66
- Source availability does not establish macOS compilation or execution.
61
+ Windows x64 and macOS ARM64/x64 have SDK-owned provider and host composition
62
+ source. Their assembly paths require the matching numeric release's native
63
+ host archive; npm publication alone does not supply that archive or establish
64
+ platform execution. The macOS provider composes AppKit, WebKit and the shared
65
+ Node Core payload. Linux and Android retain their explicit checkout providers
66
+ where listed. See [native output and host requirements](core-native-packaging.md).
67
67
 
68
68
  ## Capability-first matrix
69
69
 
@@ -88,10 +88,14 @@ Source availability does not establish macOS compilation or execution.
88
88
  | Read host identity, capabilities, storage, preferences, appearance, or platform state | `globalThis.Arcane` | **Cross-host** where the method is implemented and admitted | Promise behavior and `Arcane.Error` are normalized. Result fields are normalized unless the method explicitly documents a platform-dependent snapshot. |
89
89
  | Connect browser code to an existing Core transport | `arcane-os/core/client` and `arcane-os/core/contracts` | **Browser / native WebView** with an admitted transport; explicit development HTTP remains opt-in | The reusable client owns `arcane/1` correlation, readiness, cancellation, events, facades, and complete native error fidelity. It supplies no service, policy, or host process. |
90
90
  | Compose native Core services and framed stdio hosting | `arcane-os/core/runtime`, `arcane-os/core/stdio`, and `arcane-os/core/host` | **Node** on Windows, Linux, or macOS; the composing native host supplies its process and bridge | The SDK owns reusable dispatch, per-service lifecycle, request/service work ownership, cooperative cancellation, drain, and stdio framing. Product identity, policy, privileges, models, storage, and services remain host-owned. |
91
- | Assemble a portable Core payload from the installed SDK | `arcane-os/native/portable-provider` | **Node**; portable platform selection is currently Windows or Linux, while separately composed hosts may use the payload | The provider copies the selected app, dependencies, installed SDK runtime, Core client/runtime, and explicit service composition into a non-executable directory. It uses no `arcaneRoot` by default and does not prove host behavior or supply a platform bridge. |
91
+ | Assemble a portable Core payload from the installed SDK | `arcane-os/native/portable-provider` | **Node**; explicit Windows, Linux or macOS platform selection | The provider copies the selected app, dependencies, installed SDK runtime, Core client/runtime, and explicit service composition into a non-executable directory. It uses no `arcaneRoot` by default and does not prove host behavior or supply a platform bridge. |
92
92
  | Assemble and run a Windows executable | [`arcane-os/native/windows-provider`](core-native-packaging.md#windows-executable) | **Node** for assembly; Windows x64, .NET Framework 4.6.2+ and WebView2 for execution | Uses the installed SDK version's precompiled release asset and explicit app services. Complete diagnostics and accepted service work drain on run cancellation; no OS checkout or application-side native compiler is required. |
93
+ | Compose a macOS application bundle | [`arcane-os/native/macos-provider`](core-native-packaging.md#macos-application-composition) | **Node** assembly with matching host archive; matching Darwin ARM64/x64 for execution | Complete portable content stays in Resources. AppKit/WebKit launch, stable packaged origin, document activation and graceful Core drain remain host-owned. Source implementation is separate from a built/published host archive or actual Mac execution. |
94
+ | Persist application-owned Core preferences | [`arcane-os/core/preferences`](core-preferences.md) | **Node** Core service; renderer uses existing `Arcane.preferences` methods | Application-selected file and exact keys/JSON values, ordered in-process writes and service-lifetime drain. Unreadable storage remains untouched; saved values do not set operating-system preferences. |
95
+ | Read complete selected text from Git | [`createGitTextSnapshot`](git-text-snapshot.md) from `arcane-os` | **Node** with Git on Windows, Linux or macOS; Android requires host adaptation | One selected revision, complete UTF-8 content and independent result copies. A dedicated bare object cache leaves working checkouts untouched; shared refresh and drain own actual completion. |
93
96
  | Use application-selected llama.cpp/Ollama through Core | [`arcane-os/core/local-ai`, `arcane-os/ai/core-local`](local-ai.md) | **Node** native service; **Browser / native WebView** provider through an explicit Core connection | Independent runtime/model lifecycle, complete streaming and cancellation. Official runtime preparation follows explicit app requirements; model selection and business policy stay app-owned. |
94
97
  | Run native ONNX tensors | [`arcane-os/local-ai/onnx`, `arcane-os/ai/core-onnx`](local-ai.md#native-onnx-sessions) | **Node** Windows/Linux/macOS x64/arm64; **Browser / native WebView** through available Core | Explicit graph loading, retained workers and complete tensor outputs. Android requires host adaptation. Native calls can delay worker termination; cleanup observes actual exit. |
98
+ | Label speakers in caller-fed mono audio | [`arcane-os/diarization`, `arcane-os/core/diarization`](diarization.md) | **Node** with built helper, selected official NeMo libraries and Nemotron model; official CPU archives cover Windows x64 and Linux/macOS x64/arm64 | Independent retained streams, complete timed segments and optional probability frames. Generic labels are not personal identities or speech transcripts; cancellation and close observe native cleanup. Android needs host adaptation. |
95
99
  | Use local AI without coupling app code to Ollama HTTP | `Arcane.localAI`, `Arcane.ai`, or `/arcane/modules/Ollama.js` | Primarily **Native**; Android exposes a narrower admitted inference projection | Admission, errors, and managed-operation events are normalized. Direct Ollama response envelopes remain **Provider-native**. |
96
100
  | Use TWiN Cloud from the renderer profile | `/arcane/modules/AI.js` | **Cloud** from an allowed browser/native renderer | High-level chat behavior is normalized by the module. The TWiN access key authenticates remote LLM chat; raw provider diagnostics remain provider-specific. No automatic cloud fallback is inferred from local failure. |
97
101
  | Send one TWiN Cloud request with an explicit key and model | `fetchRequest` from `arcane-os/ai/twin-cloud` | **Node** and **Browser**, using standard Fetch and a remote HTTPS provider | Keeps complete messages and returns the full parsed provider JSON. Rejected Fetch and HTTP 529 share three retries after 3000 ms; HTTP 429 overload retains unlimited three-second retries. Optional `onRetry` reports waiting/requesting phases without blocking recovery. Shared structured-output mapping and cancellation match browser TWiN transport. No browser profile, AI/user singleton, DBOPFS, or retained request history is created. |
@@ -564,7 +564,7 @@ defaulting.
564
564
 
565
565
  ### Speech transcription request
566
566
 
567
- `Arcane.speech.transcribe(request)` accepts an exact
567
+ `Arcane.speech.transcribe(request, options?)` accepts an exact
568
568
  `SpeechTranscriptionRequest`.
569
569
 
570
570
  | Field | Type | Required | Constraints / default | Description |
@@ -573,6 +573,19 @@ defaulting.
573
573
  | `mimeType` | `string` | No | Default `"audio/webm"`; base type must be `audio/webm` | Media type; codec parameters may follow. |
574
574
  | `model` | `string` | No | Default `"whisper-small"`; `whisper(?:[._-][a-z0-9]+)*` | Whisper identifier. The fixed service rejects unsupported variants. |
575
575
 
576
+ ### Speech transcription controls
577
+
578
+ The optional second argument is `SpeechTranscriptionControls`:
579
+
580
+ | Field | Type | Required | Default | Description |
581
+ |---|---|---:|---|---|
582
+ | `signal` | `AbortSignal` | No | No caller signal | Cancels this request through Core. The facade forwards it separately from the complete audio request; the registered service owns actual engine interruption. |
583
+
584
+ Existing one-argument calls retain their behavior. The facade's request timeout
585
+ remains 180 seconds. A pre-aborted signal dispatches no request, and an
586
+ in-flight abort cancels the request and suppresses late responses. Other
587
+ properties of this controls record are unused; they do not enter the payload.
588
+
576
589
  ### Speech transcription result
577
590
 
578
591
  | Field | Type | Required | Constraints / default | Description |
@@ -105,7 +105,7 @@ directly.
105
105
  | `Arcane.localAI.setParallelRequests(request)` | `request`: [`LocalAIParallelRequestsRequest`](arcane-ai-contracts.md#local-ai-parallel-request-configuration) | [`Promise<LocalAIParallelRequestsResult>`](arcane-ai-contracts.md#local-ai-parallel-request-configuration) | Solves one app-owned verified model/context's maximum directly from native model and memory/resource evidence without a product ceiling, clamps a positive request to that result (`0` requests the maximum), and changes the machine-wide managed Ollama setting only when needed. A tentative change uses one fresh stopped-service snapshot before the final value is written, then Core ensures, loads, and confirms the exact model. A restart unloads resident models. Automatic mutation currently requires the dedicated `ai.runtime.manage` capability on Microsoft NT desktop Core and operating-system authorization; Linux returns administrator-managed systemd guidance, and Android user-managed loopback sessions do not invoke it. |
106
106
  | `Arcane.speech.status()` | None | [`Promise<SpeechStatus>`](arcane-ai-contracts.md#speech-status) | Reads bounded, independent Kokoro and Whisper readiness through Core or an admitted Android native host. Requires `ai.inference`. |
107
107
  | `Arcane.speech.synthesize(request)` | `request`: [`SpeechSynthesisRequest`](arcane-ai-contracts.md#speech-synthesis-request) | [`Promise<SpeechSynthesisResult>`](arcane-ai-contracts.md#speech-synthesis-result) | Synthesizes bounded local speech through the native host and fixed loopback runtime. Requires `ai.inference`. |
108
- | `Arcane.speech.transcribe(request)` | `request`: [`SpeechTranscriptionRequest`](arcane-ai-contracts.md#speech-transcription-request) | [`Promise<SpeechTranscriptionResult>`](arcane-ai-contracts.md#speech-transcription-result) | Sends bounded canonical base64 bytes labeled `audio/webm` to local Whisper through the native host. The bridge validates encoding, label, and bounds; the fixed service parses the media container. Requires `ai.inference`. |
108
+ | `Arcane.speech.transcribe(request, options?)` | `request`: [`SpeechTranscriptionRequest`](arcane-ai-contracts.md#speech-transcription-request); `options?`: [`SpeechTranscriptionControls`](arcane-ai-contracts.md#speech-transcription-controls) | [`Promise<SpeechTranscriptionResult>`](arcane-ai-contracts.md#speech-transcription-result) | Sends the complete request to the selected native transcription service; optional cancellation reaches Core's request lifetime. The registered host service owns media decoding and actual engine interruption. |
109
109
  | `Arcane.ollama.version()` | None | [`Promise<OllamaVersionResponse>`](arcane-ai-contracts.md#ollama-provider-responses) | Gets the managed Ollama version for Settings, Terminal, and Shell diagnostics. |
110
110
  | `Arcane.ollama.models()` | None | [`Promise<OllamaModelsResponse>`](arcane-ai-contracts.md#ollama-provider-responses) | Lists the raw provider inventory envelope; it is not the application admission catalog. |
111
111
  | `Arcane.ollama.list()` | None | [`Promise<OllamaModelsResponse>`](arcane-ai-contracts.md#ollama-provider-responses) | Alias of the raw `models()` inventory. |
@@ -703,7 +703,7 @@ console.info(result.contentType, result.audioBase64.length);
703
703
 
704
704
  ### Overview
705
705
 
706
- `Arcane.speech.transcribe(request)` sends bounded caller-supplied audio bytes,
706
+ `Arcane.speech.transcribe(request, options?)` sends bounded caller-supplied audio bytes,
707
707
  labeled as WebM, to the fixed local transcription service. Use it after local
708
708
  capture permission and recording have succeeded; the method itself does not
709
709
  capture audio or grant microphone permission.
@@ -739,18 +739,21 @@ work after `ANDROID_SPEECH_QUEUE_FULL`.
739
739
  ### Streaming, cancellation, and events
740
740
 
741
741
  Transcription resolves one complete text result and emits no method event. The
742
- wrapper timeout is 180 seconds and no signal is exposed. Core and Android do
743
- not promise cancellation when the renderer stops waiting.
742
+ wrapper timeout remains 180 seconds. Optional `{signal}` controls the Core
743
+ request lifetime separately from the complete audio payload. Pre-abort sends
744
+ no request; in-flight abort cancels that request and suppresses late responses.
745
+ The registered native service owns actual inference interruption and resource
746
+ release. `AI.fetchSTT(audio, signal)` forwards its signal through this call.
744
747
 
745
748
  ### Example
746
749
 
747
750
  ```javascript
748
- async function transcribeRecordedWebM(audioBase64) {
751
+ async function transcribeRecordedWebM(audioBase64, signal) {
749
752
  const result = await globalThis.Arcane.speech.transcribe({
750
753
  audioBase64: audioBase64,
751
754
  mimeType: 'audio/webm',
752
755
  model: 'whisper-small'
753
- });
756
+ }, {signal});
754
757
  return result.text;
755
758
  }
756
759
  ```
@@ -77,6 +77,20 @@ The compatibility facade retains its operation-specific timeouts and streaming
77
77
  callbacks. Streaming IDs and isolated-operation IDs are separate protocol
78
78
  correlation fields alongside the supplied request fields.
79
79
 
80
+ `Arcane.speech.synthesize(request, {signal})` forwards the optional cancellation
81
+ signal through the same request lifetime. Existing one-argument calls retain
82
+ the 180,000 ms timeout and pass the complete request unchanged. A pre-aborted
83
+ signal sends no synthesis request; an in-flight abort sends `request.cancel`
84
+ and suppresses late responses. Actual synthesis interruption depends on the
85
+ registered speech service and its engine; the facade supplies no speech engine.
86
+
87
+ `Arcane.speech.transcribe(request, {signal})` forwards cancellation through the
88
+ same request lifetime and retains its 180,000 ms timeout and complete request.
89
+ `AI.fetchSTT(audio, signal)` carries that signal into the native call. A
90
+ pre-aborted signal sends no transcription request; an in-flight abort sends
91
+ `request.cancel` and suppresses late responses. The registered transcription
92
+ service owns interruption and release of its actual engine operation.
93
+
80
94
  ## Events and request lifetime
81
95
 
82
96
  `events.on(name, listener)`, `once(name, listener)` and
@@ -212,10 +212,14 @@ details and [the client contract](core-client.md) for native bridge integration.
212
212
 
213
213
  The generated Core entry accepts `--arcane-launch-config <path>`. The public
214
214
  `readCoreLaunchContext({argv=process.argv.slice(2)}={})` function in
215
- `arcane-os/core/host` reads that explicitly selected JSON object. Without the
216
- flag it returns `{}`. Missing files, missing flag values and malformed JSON
217
- report their actual errors; the reader does not search for configuration,
218
- rewrite it or change the process environment.
215
+ `arcane-os/core/host` reads that explicitly selected JSON object. A native host
216
+ can separately supply `--arcane-host-state-root <directory>` for the reader's
217
+ `stateRoot` default. Every field in the explicit JSON object takes precedence
218
+ unchanged, including an explicitly present `stateRoot` with a relative, null or
219
+ other application-owned value. With neither argument the reader returns `{}`.
220
+ Missing files, missing argument values and malformed JSON report their actual
221
+ errors; the reader does not search for configuration, resolve or validate the
222
+ state directory, rewrite the file or change the process environment.
219
223
 
220
224
  The generated entry supplies an artifact-derived `appRoot` and then applies the
221
225
  complete explicit launch context. Every service factory receives that same
@@ -224,7 +228,10 @@ launcher can supply the user's selected `workspaceRoot` and writable
224
228
  `stateRoot` at launch instead of embedding this machine's paths in an app
225
229
  package. The host owns those selections; the SDK does not choose a product's
226
230
  workspace, namespace, saved preferences or models. JSON values and additional
227
- application-owned fields remain unchanged.
231
+ application-owned fields remain unchanged, including an app-selected
232
+ `preferencesFile`. The state directory supplies no preference filename and
233
+ triggers no stored-data discovery or migration. Linux and other composing hosts
234
+ may pass the same separate argument; the reader chooses no platform directory.
228
235
 
229
236
  ## Selected local-AI runtimes
230
237
 
@@ -305,8 +312,11 @@ Arcane.exe --arcane-launch-config path/to/launch.json
305
312
  The complete JSON object reaches the service factories as their second
306
313
  argument. A supplied `stateRoot` also owns the WebView2 profile and diagnostic
307
314
  directory. Otherwise the launcher uses the current user's local application
308
- data under `Arcane/<app-id>`. The launcher does not invent an application
309
- workspace. Relative launch-file paths resolve from the invocation's working
315
+ data under `Arcane/<app-id>`. It forwards that actual host selection through
316
+ `--arcane-host-state-root`, so ordinary double-click startup supplies `stateRoot`
317
+ to every Core service factory. The complete explicit launch context still takes
318
+ precedence without rewriting its fields. The launcher does not invent an
319
+ application workspace. Relative launch-file paths resolve from the invocation's working
310
320
  directory. `--close-on-stdin-eof` is an explicit process-owner option used by the
311
321
  SDK runner; an ordinary double-clicked window closes through its own UI.
312
322
 
@@ -414,7 +424,9 @@ owner and published `node-http-server`, not a second HTTP implementation.
414
424
  `context.stateRoot` takes precedence over the launcher's separate
415
425
  `--arcane-host-state-root` argument. The launcher defaults to
416
426
  `~/Library/Application Support/Arcane/<app-id>`; an explicitly supplied launch
417
- file remains unchanged and is forwarded to Core. The first successful listener
427
+ file remains unchanged and is forwarded to Core. The shared launch reader also
428
+ supplies that host default to every application service factory, with explicit
429
+ launch fields taking precedence. The first successful listener
418
430
  stores its selected port in `packaged-web-origin.json`. Later launches reuse it;
419
431
  a port conflict or unreadable saved record is reported without changing origins
420
432
  or discarding stored state.
@@ -4175,10 +4175,10 @@
4175
4175
  "entrypoints": ["arcane-os/core/host"],
4176
4176
  "primaryImport": "arcane-os/core/host",
4177
4177
  "group": "Native Core host composition",
4178
- "summary": "Reads the complete JSON object explicitly selected by --arcane-launch-config for native service launch context.",
4178
+ "summary": "Reads explicit launch JSON with the separately supplied native host stateRoot default for service launch context.",
4179
4179
  "availability": "Node, including the selected external-entry SEA launcher",
4180
4180
  "protocol": "Explicit launch arguments and JSON configuration",
4181
- "normalization": "Returns an empty record without the flag; preserves every supplied field and value, surfaces file and JSON failures, and performs no configuration search, rewrite or environment mutation"
4181
+ "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"
4182
4182
  },
4183
4183
  {
4184
4184
  "id": "core-host:startCoreHost",
@@ -94,14 +94,81 @@ packaging use that layout directly:
94
94
  packaged installation ID remains `./`, and an explicit `manifest.id` remains
95
95
  authoritative. An ID identifies the installation; it does not generate a
96
96
  directory or a redirect.
97
- - Authored resources follow their normal include/exclude selection. Generation
98
- does not delete existing files, stored application data, or worker caches.
97
+ - Authored resources follow their normal include/exclude selection. Ordinary
98
+ generation preserves existing files, stored application data, and worker caches.
99
+ Explicit retirement of confirmed generated source output is described below.
99
100
 
100
101
  Generated and offline app files are committed. GitHub Actions consume those
101
102
  committed files rather than generating them. Actual multi-app workspaces keep
102
103
  their explicit `appsRoot: "apps"` layout. This source/package contract does not
103
104
  establish any particular installed application's browser lifecycle.
104
105
 
106
+ ### Disable source PWA generation and retire generated output
107
+
108
+ Set `package.pwa.enabled:false` in `arcane-app.json` (or `pwa.enabled:false` in
109
+ an authored schema-1 `arcane-package.json`), then run the public import-map
110
+ operation. For standalone `appsRoot:"."` applications, disabled refresh removes
111
+ active `data-arcane-pwa` registration scripts and matching manifest relationships
112
+ marked `data-arcane-manifest` from the selected application documents. Root source
113
+ generation marks only manifest links it newly inserts; it does not claim an
114
+ existing unmarked link as its own. A wholly omitted `pwa` record does not select
115
+ source retirement.
116
+
117
+ By default, unmarked manifest references and all existing PWA files remain in
118
+ place. This preserves authored resources that happen to use the standard names.
119
+ Older SDK source generation emitted an unmarked manifest link. When the
120
+ application owner has established that this link and the four standard root
121
+ outputs are SDK-generated, use the same public toolchain with an explicit
122
+ ownership declaration:
123
+
124
+ ```js
125
+ import {createToolchain} from 'arcane-os';
126
+
127
+ const toolchain = createToolchain({workspaceRoot: process.cwd(), appId: 'my-app'});
128
+ await toolchain.importMap({retireGeneratedPwa: true});
129
+ ```
130
+
131
+ Before retirement, remove any explicit entries for these generated outputs from
132
+ the application's `package.include` selection. They are generated output, not
133
+ required source input; leaving a deleted file as a required input would make the
134
+ next inspection fail. Keep authored selection changes with the application
135
+ owner. Confirm that unselected documents or separately hosted pages do not still
136
+ need the retiring files; this operation does not rewrite those pages.
137
+
138
+ With PWA disabled, `retireGeneratedPwa:true` also removes selected-document
139
+ manifest relationships resolving to the root `arcane.webmanifest`, including
140
+ older unmarked generated links, and deletes exactly these confirmed generated
141
+ files at the application root:
142
+
143
+ - `arcane.webmanifest`
144
+ - `arcane-offline.json`
145
+ - `arcane-sw.js`
146
+ - `arcane-pwa.mjs`
147
+
148
+ The option is an application-owner declaration of generated ownership, not a
149
+ filename-based inference. Omit it when any listed file or matching manifest
150
+ reference is authored or its ownership is unknown. It is available through
151
+ `createToolchain().importMap()` and `executeOperation('import-map', options)`;
152
+ there is no new CLI flag. The ordinary CLI refresh performs marked-reference
153
+ retirement only. An enabled descriptor ignores this retirement option and
154
+ continues normal generation.
155
+
156
+ With the source selection updated, repeated retirement accepts already-absent
157
+ generated files. The
158
+ `import-map.root-files.completed` event reports the paths actually removed in
159
+ `retiredPwa`. Re-enabling PWA regenerates its normal root output and references.
160
+ The operation leaves other files (including a separate `manifest.json`), other
161
+ link relationships, inactive HTML, unselected documents, SDK mappings, and
162
+ application content unchanged. It never unregisters an already registered
163
+ service worker, clears caches, or changes browser saved data. Existing browser
164
+ registrations therefore keep their own lifecycle; source retirement is not a
165
+ claim that an installed browser PWA was removed. Package-output behavior below
166
+ is unchanged.
167
+
168
+ Reference updates finish before file removal begins. Cancellation or a file
169
+ error is reported without undoing earlier completed changes; a later call can
170
+ finish retirement. The operation does not claim an all-or-nothing transaction.
171
+
105
172
  ### Authored host root and app entry
106
173
 
107
174
  An authored `index.html` at the source host root keeps `/` separate from the
@@ -58,6 +58,10 @@ runtime layouts.
58
58
  | `arcane-os/core/stdio` | Framed Node stdio transport with graceful runtime drain. |
59
59
  | `arcane-os/core/host` | Node-only reusable Core host lifecycle with explicitly supplied services. |
60
60
  | `arcane-os/core/packaged-web` | Core-owned packaged web listener with a persisted loopback origin and ready/failure events. |
61
+ | `arcane-os/core/preferences` | Existing preference RPC methods with an application-selected file, ordered writes and owned drain. |
62
+ | `arcane-os/core/diarization` | Lazy native speaker-diarization service, explicit stream methods and lifecycle/result events. |
63
+ | `arcane-os/diarization` | Retained Nemotron model helper and independent caller-fed audio streams. |
64
+ | `arcane-os/diarization/build` | First-party helper compilation against explicitly installed NeMo-Speech.cpp libraries. |
61
65
  | `arcane-os/core/local-ai` | Native llama.cpp, Ollama and ONNX Core service, model readiness, complete responses and owned lifecycle. See [local AI through Core](local-ai.md). |
62
66
  | `arcane-os/local-ai` | Official runtime installation and native artifact bundling for selected llama.cpp/Ollama/ONNX requirements and NeMo libraries. |
63
67
  | `arcane-os/local-ai/onnx` | Retained native ONNX worker sessions with explicit graph loading and complete tensor inference. |
@@ -389,6 +393,12 @@ The remaining data-only subpaths are eight JSON Schemas and package metadata.
389
393
  | `arcaneNativeBuilderProvider` | singleton | `arcane-os/native/macos-provider` | macOS native provider | Node assembly; matching Darwin architecture and selected host asset |
390
394
  | `arcaneNativeBuilderProvider default export` | singleton | `arcane-os/native/macos-provider` | macOS native provider | Node assembly; matching Darwin architecture and selected host asset |
391
395
  | `createPackagedWebService()` | function | `arcane-os/core/packaged-web` | Native packaged web serving | Node Core lifetime; actual listener readiness before navigation |
396
+ | `createPreferencesService()` | function | `arcane-os/core/preferences` | Core preference persistence | Node Core runtime with an application-selected file |
397
+ | `createPreferencesService default export` | function | `arcane-os/core/preferences` | Core preference persistence | Same Core service factory |
398
+ | `createDiarization()` | function | `arcane-os/diarization` | Native speaker diarization | Node with a built helper, selected NeMo libraries and caller-selected model |
399
+ | `buildDiarizationHelper()` | function | `arcane-os/diarization/build` | Native speaker diarization | Node with installed CMake/C++17 toolchain and selected runtime |
400
+ | `createDiarizationService()` | function | `arcane-os/core/diarization` | Native speaker diarization | Node Core runtime with explicitly selected helper, runtime and model |
401
+ | `createDiarizationService default export` | function | `arcane-os/core/diarization` | Native speaker diarization | Same Core service factory |
392
402
  | `createCoreLocalAIProvider()` | function | `arcane-os/ai/core-local` | Core local AI | Browser/native WebView with an available Core connection |
393
403
  | `createLocalAIService()` | function | `arcane-os/core/local-ai` | Core local AI | Node with selected llama.cpp/Ollama/ONNX runtimes |
394
404
  | `createONNXRuntime()` | function | `arcane-os/local-ai/onnx` | Core local AI | Node with prepared Microsoft ONNX runtime |
@@ -4553,6 +4563,12 @@ The object includes `importMap(options)`, which merges defaults with explicit
4553
4563
  call options and refreshes every directly navigable descriptor-selected browser
4554
4564
  document for one selected app. The equivalent generic route is
4555
4565
  `execute('import-map', options)`.
4566
+ For a standalone root app with `pwa.enabled:false`, ordinary refresh removes
4567
+ marked SDK-generated PWA references from selected documents. An explicit
4568
+ `importMap({retireGeneratedPwa:true})` also retires the four standard root PWA
4569
+ outputs and matching older manifest references after the application owner
4570
+ confirms they are generated. See [disabled-PWA retirement](pwa.md#disable-source-pwa-generation-and-retire-generated-output)
4571
+ for the required source-selection adjustment and preserved content.
4556
4572
  It also includes `upgrade(options)`, which performs the explicit installed-SDK
4557
4573
  consumer upgrade described by `upgradeApplication()`, and
4558
4574
  `updateCheck(options)`, which invokes `checkSdkUpdate()` once. Constructing the
@@ -4675,6 +4691,25 @@ documents through sequential writes and has no supported dry-run or
4675
4691
  all-file transaction. An error or cancellation can leave completed writes in
4676
4692
  place; rerun the operation after resolving the cause.
4677
4693
 
4694
+ For a standalone `appsRoot:'.'` app with `pwa.enabled:false`, ordinary refresh
4695
+ removes active generated registration scripts and matching manifest relationships
4696
+ marked `data-arcane-manifest`. Unmarked references and existing PWA files remain.
4697
+ Set `retireGeneratedPwa:true` only after confirming generated ownership of the
4698
+ matching root manifest references and all four standard outputs:
4699
+ `arcane.webmanifest`, `arcane-offline.json`, `arcane-sw.js` and `arcane-pwa.mjs`.
4700
+ Remove those generated outputs from required `package.include` entries first,
4701
+ and confirm that unselected documents no longer need them. The operation writes
4702
+ selected reference changes before deleting these files; its
4703
+ `import-map.root-files.completed` event reports actual removed paths in
4704
+ `retiredPwa`. Repeated retirement accepts already-absent output.
4705
+
4706
+ An omitted PWA record selects no retirement; an enabled record keeps normal
4707
+ generation. The option belongs only to this explicit import-map API route.
4708
+ Ordinary development refresh does not forward file-retirement authority, and
4709
+ there is no new CLI flag. Other files, unselected/inactive HTML, authored content
4710
+ and browser registrations, caches and saved data keep their existing ownership.
4711
+ See the complete [source PWA contract](pwa.md#disable-source-pwa-generation-and-retire-generated-output).
4712
+
4678
4713
  The exact command `'upgrade'` dispatches `upgradeApplication(options)`. It is
4679
4714
  external-workspace-only and runs that application's ordinary `npm upgrade`
4680
4715
  command. It does not independently reconcile the installed SDK projection,
@@ -6889,6 +6924,14 @@ Import it from `arcane-os/core/client`. The result includes `runtime`, `events`,
6889
6924
  `Error` and the existing `ai`, `environment`, `mail`, `speech`, `localAI`,
6890
6925
  `ollama`, application, platform, storage and other host namespaces.
6891
6926
 
6927
+ `facade.speech.synthesize(request, {signal})` accepts an optional `AbortSignal`
6928
+ through the same correlated request lifetime. The complete request and existing
6929
+ 180,000 ms timeout remain unchanged. A pre-aborted signal sends no synthesis
6930
+ request; an in-flight abort emits that request's `request.cancel`, rejects with
6931
+ `ARCANE_REQUEST_ABORTED` and leaves unrelated requests active. Late responses
6932
+ cannot settle the cancelled request. Actual native synthesis interruption belongs
6933
+ to the registered service and engine; this facade supplies no engine.
6934
+
6892
6935
  ### Availability and normalization
6893
6936
 
6894
6937
  **Browser and native WebView JavaScript hosts with a Core client.** Creating the
@@ -7415,12 +7458,144 @@ errors, with no silent new origin. Startup rejection belongs to the Core
7415
7458
  and lifecycle completion. No application launch payload is rewritten, no model
7416
7459
  is loaded, and the ordinary server's HTTPS default is unchanged.
7417
7460
 
7461
+ ## createPreferencesService()
7462
+
7463
+ ### Overview
7464
+
7465
+ Supplies the five existing `Arcane.preferences` methods using an application-selected
7466
+ JSON file. Construction performs no filesystem work. Preference names, defaults,
7467
+ schemas and writable location remain application-owned.
7468
+
7469
+ ### Signature and result
7470
+
7471
+ ```text
7472
+ createPreferencesService({file}={})
7473
+ ```
7474
+
7475
+ Import from `arcane-os/core/preferences`. The returned Core service exposes
7476
+ `preferences.list/get/set/setMany/delete`, `drain()` and `dispose()`. Writes use
7477
+ `lifetime:'service'`, preserve complete JSON values and existing document metadata,
7478
+ and acknowledge after the new content is flushed and the selected file replaced.
7479
+ Operations selecting the same resolved filename share an ordered in-process queue.
7480
+ Unreadable or malformed files report their errors without being replaced; a missing
7481
+ file starts with empty entries. Unrepresentable JSON values fail before replacement.
7482
+
7483
+ ```javascript
7484
+ import {createPreferencesService} from 'arcane-os/core/preferences';
7485
+
7486
+ const preferences = createPreferencesService({file: selectedPreferenceFile});
7487
+ ```
7488
+
7489
+ Register the service with the existing Core owner. Its drain/dispose stops new
7490
+ acceptance, waits for accepted work and exposes failures; subsequent calls reject
7491
+ with `CORE_CLOSING`. See [method results, storage and service composition](core-preferences.md).
7492
+
7493
+ ## createPreferencesService default export
7494
+
7495
+ The default export from `arcane-os/core/preferences` is the same
7496
+ `createPreferencesService` factory, with identical arguments, method results
7497
+ and lifecycle:
7498
+
7499
+ ```javascript
7500
+ import createPreferencesService from 'arcane-os/core/preferences';
7501
+ ```
7502
+
7503
+ ## createDiarization()
7504
+
7505
+ ### Overview
7506
+
7507
+ Retains one caller-selected Nemotron model in its first-party native helper and
7508
+ accepts independent mono-audio streams. Results contain generic timed speaker
7509
+ labels; they do not identify people or perform speech recognition.
7510
+
7511
+ ### Signature and result
7512
+
7513
+ ```text
7514
+ createDiarization({executable,modelPath,runtime,onEvent}={})
7515
+ ```
7516
+
7517
+ Import from `arcane-os/diarization`. Supply absolute helper/model paths and the
7518
+ selected runtime's library locations. The returned owner starts the helper and
7519
+ exposes `ready`, `completion`, `current()`, replaying `subscribe()`,
7520
+ `openStream()`, `diarize()` and `close()`. Readiness resolves only after model
7521
+ loading. Each stream owns `push`, `finish`, `cancel`, `completion` and `current`;
7522
+ pushes preserve mono float32 input, use backpressure and return the latest timed
7523
+ segment snapshot. Caller callbacks may receive every probability frame.
7524
+
7525
+ ```javascript
7526
+ import {createDiarization} from 'arcane-os/diarization';
7527
+
7528
+ const speakers = createDiarization({executable, modelPath, runtime});
7529
+ const result = await speakers.diarize({audio: monoSamples, sampleRate: 16000});
7530
+ await speakers.close();
7531
+ ```
7532
+
7533
+ Load, callback, process and cleanup failures remain observable. Cancellation
7534
+ suppresses later results for its stream and closes it after accepted native
7535
+ commands; the C ABI supplies no immediate interrupt. Close drains owned work
7536
+ and observes helper exit. See [complete audio, callback and cancellation contracts](diarization.md).
7537
+
7538
+ ## buildDiarizationHelper()
7539
+
7540
+ ```text
7541
+ async buildDiarizationHelper({runtime,outputRoot,signal,onEvent}={})
7542
+ ```
7543
+
7544
+ Import from `arcane-os/diarization/build`. Builds the first-party helper for the
7545
+ current host using installed CMake/C++17 tools and the selected runtime's
7546
+ `cmakeDirectory`. Returns `{platform,architecture,root,executable,diagnostics}`
7547
+ after configure, compile and install, retaining complete command diagnostics.
7548
+ Missing inputs or actual tool failures reject through the shared process owner;
7549
+ no compiler, model or runtime is downloaded by this function.
7550
+
7551
+ ```javascript
7552
+ import {buildDiarizationHelper} from 'arcane-os/diarization/build';
7553
+
7554
+ const helper = await buildDiarizationHelper({runtime, outputRoot: helperDirectory});
7555
+ ```
7556
+
7557
+ See [runtime preparation, helper placement and platform support](diarization.md#runtime-and-helper).
7558
+
7559
+ ## createDiarizationService()
7560
+
7561
+ ```text
7562
+ createDiarizationService(options={},launch={})
7563
+ ```
7564
+
7565
+ Import from `arcane-os/core/diarization`. This synchronous `native.services`
7566
+ factory accepts `{executable,modelPath,runtime,onEvent}` and the native launch
7567
+ context. Relative executable/model paths use `workspaceRoot`, then `appRoot`,
7568
+ then the current directory. It supplies `diarization.status`, `diarization.load`,
7569
+ `diarization.open`, `diarization.push`, `diarization.finish`, `diarization.cancel`
7570
+ and `diarization.recording`, state/update/probability events and drain/dispose hooks.
7571
+ The first actual operation loads the model, independently of Core startup.
7572
+
7573
+ ```javascript
7574
+ import {createDiarizationService} from 'arcane-os/core/diarization';
7575
+
7576
+ const service = createDiarizationService({executable, modelPath, runtime}, launch);
7577
+ ```
7578
+
7579
+ Session audio has service lifetime and explicit cancellation; a recording's
7580
+ request cancellation closes only that recording. Complete float32 samples use
7581
+ the documented Core transport framing. See [inputs, results, events and lifetime](diarization.md#core-service).
7582
+
7583
+ ## createDiarizationService default export
7584
+
7585
+ The default binding from `arcane-os/core/diarization` is exactly the named
7586
+ `createDiarizationService` factory, with the same selection and service lifecycle:
7587
+
7588
+ ```javascript
7589
+ import createDiarizationService from 'arcane-os/core/diarization';
7590
+ ```
7591
+
7418
7592
  ## readCoreLaunchContext()
7419
7593
 
7420
7594
  ### Overview
7421
7595
 
7422
- Reads only the complete JSON object explicitly selected by
7423
- `--arcane-launch-config <path>`; it does not search for application configuration.
7596
+ Reads the complete JSON object explicitly selected by
7597
+ `--arcane-launch-config <path>`, with an optional separately supplied native host
7598
+ state-directory default. It does not search for application configuration.
7424
7599
 
7425
7600
  ### Signature and result
7426
7601
 
@@ -7428,11 +7603,15 @@ Reads only the complete JSON object explicitly selected by
7428
7603
  async readCoreLaunchContext({argv=process.argv.slice(2)}={})
7429
7604
  ```
7430
7605
 
7431
- Import it from `arcane-os/core/host`. The promise resolves to `{}` when the flag
7432
- is absent, or the complete parsed object when present. A missing flag value,
7433
- unreadable file, malformed JSON or non-object value rejects. Relative paths use
7434
- the process working directory. The reader does not rewrite content or mutate
7435
- the environment.
7606
+ Import it from `arcane-os/core/host`. `--arcane-host-state-root <directory>`
7607
+ supplies a `stateRoot` default; every field from the explicit launch JSON object
7608
+ then takes precedence unchanged, including a relative, null or other explicitly
7609
+ present `stateRoot`. With neither argument, the promise resolves to `{}`.
7610
+ A missing argument value, unreadable launch file, malformed JSON or non-object
7611
+ value rejects. Relative launch-file paths use the process working directory.
7612
+ The reader does not choose, resolve or validate the state directory, rewrite
7613
+ content, discover or migrate stored data, or mutate the environment. An
7614
+ application-selected `preferencesFile` remains a separate unchanged field.
7436
7615
 
7437
7616
  ```javascript
7438
7617
  import {readCoreLaunchContext} from 'arcane-os/core/host';
@@ -7443,7 +7622,10 @@ const launchContext = await readCoreLaunchContext();
7443
7622
  The generated entry supplies artifact-derived `appRoot`, then applies the whole
7444
7623
  explicit context and passes the same object as every service factory's second
7445
7624
  argument. Authored service options remain the first argument. Workspace/state
7446
- locations and additional fields belong to the launcher/application. See
7625
+ locations and additional fields belong to the launcher/application. Windows and
7626
+ Mac launchers forward their actual host-selected state directory through the
7627
+ separate argument; other composing hosts may use the same seam. This JavaScript
7628
+ contract does not establish a compiled host or matching release archive. See
7447
7629
  [launch-time locations](core-native-packaging.md#launch-time-locations).
7448
7630
 
7449
7631
  ## createCoreLocalAIProvider()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.58.0",
3
+ "version": "0.59.1",
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",
@@ -4084,11 +4084,19 @@ class AI {
4084
4084
  if(signal?.aborted){
4085
4085
  throw normalizeAIRequestAbort(signal.reason);
4086
4086
  }
4087
- const response=await nativeSpeech.transcribe({
4088
- audioBase64:this.#arrayBufferToBase64(audioBytes),
4089
- mimeType,
4090
- model
4091
- });
4087
+ let response;
4088
+ try{
4089
+ response=await nativeSpeech.transcribe({
4090
+ audioBase64:this.#arrayBufferToBase64(audioBytes),
4091
+ mimeType,
4092
+ model
4093
+ },{signal});
4094
+ }catch(error){
4095
+ if(isAIRequestAbort(error,signal)){
4096
+ throw normalizeAIRequestAbort(error);
4097
+ }
4098
+ throw error;
4099
+ }
4092
4100
  if(signal?.aborted){
4093
4101
  throw normalizeAIRequestAbort(signal.reason);
4094
4102
  }
@@ -4138,13 +4146,21 @@ class AI {
4138
4146
  const speed=is.finite(payload.speed)?payload.speed:this.voiceSpeed;
4139
4147
  const nativeSpeech=this.#nativeSpeech(this.ttsService,'tts');
4140
4148
  if(nativeSpeech){
4141
- const response=await nativeSpeech.synthesize({
4142
- model,
4143
- voice,
4144
- input,
4145
- responseFormat,
4146
- speed
4147
- });
4149
+ let response;
4150
+ try{
4151
+ response=await nativeSpeech.synthesize({
4152
+ model,
4153
+ voice,
4154
+ input,
4155
+ responseFormat,
4156
+ speed
4157
+ },{signal});
4158
+ }catch(error){
4159
+ if(isAIRequestAbort(error,signal)){
4160
+ throw normalizeAIRequestAbort(error);
4161
+ }
4162
+ throw error;
4163
+ }
4148
4164
  if(signal?.aborted){
4149
4165
  throw normalizeAIRequestAbort(signal.reason);
4150
4166
  }
package/src/core/host.mjs CHANGED
@@ -6,13 +6,22 @@ import {startCoreStdio} from './stdio.mjs';
6
6
  const is = new Is(false);
7
7
 
8
8
  /**
9
- * Read only the explicitly supplied --arcane-launch-config filename.
10
- * Return the complete JSON object for the service factories' second argument;
11
- * field meanings belong to the launch owner and individual services.
9
+ * The host's separate state-root argument supplies a launch-context default.
10
+ * Every field in an explicitly selected launch JSON object takes precedence
11
+ * unchanged; field meanings belong to the launch owner and individual services.
12
12
  */
13
13
  export async function readCoreLaunchContext({argv = process.argv.slice(2)} = {}) {
14
+ const stateFlag = argv.indexOf('--arcane-host-state-root');
15
+ const defaults = {};
16
+ if (stateFlag !== -1) {
17
+ const stateRoot = argv[stateFlag + 1];
18
+ if (stateRoot === undefined || stateRoot === '') {
19
+ throw new TypeError('--arcane-host-state-root requires a directory.');
20
+ }
21
+ defaults.stateRoot = stateRoot;
22
+ }
14
23
  const flag = argv.indexOf('--arcane-launch-config');
15
- if (flag === -1) return {};
24
+ if (flag === -1) return defaults;
16
25
  const filename = argv[flag + 1];
17
26
  if (filename === undefined || filename === '') {
18
27
  throw new TypeError('--arcane-launch-config requires a filename.');
@@ -21,7 +30,7 @@ export async function readCoreLaunchContext({argv = process.argv.slice(2)} = {})
21
30
  if (context === null || !is.object(context) || is.array(context)) {
22
31
  throw new TypeError('Core launch configuration must be a JSON object.');
23
32
  }
24
- return context;
33
+ return {...defaults, ...context};
25
34
  }
26
35
 
27
36
  /** Compose app services with the shared dispatcher and draining transport. */
@@ -69,7 +69,10 @@ namespace Arcane.Core.Hosts.Windows
69
69
  Title = name,
70
70
  ClassicClientSource = File.ReadAllText(Path.Combine(directory, RequiredString(client, "source"))),
71
71
  CoreExecutable = Path.Combine(directory, "runtime", "ArcaneCore.exe"),
72
- CoreArguments = launchFilename == null ? String.Empty : "--arcane-launch-config " + QuoteArgument(launchFilename),
72
+ // Forward the actual host selection separately; Core keeps
73
+ // every explicit launch-record field unchanged.
74
+ CoreArguments = "--arcane-host-state-root " + QuoteArgument(profileParent)
75
+ + (launchFilename == null ? String.Empty : " --arcane-launch-config " + QuoteArgument(launchFilename)),
73
76
  // Relative explicit launch filenames retain their caller's
74
77
  // meaning; the SEA locates its loader beside its executable.
75
78
  CoreWorkingDirectory = Environment.CurrentDirectory
@@ -2091,8 +2091,8 @@ function scanHtmlStructure(html){
2091
2091
  return {scripts,links,bases,metas,elements,styles,headClose,bodyClose};
2092
2092
  }
2093
2093
 
2094
- /** Remove generated PWA references from a selected output document without changing authored content. */
2095
- export function removePwaEntryReferences(html, {documentUrl, manifestUrl} = {}) {
2094
+ /** Retire active PWA references; source refresh may restrict removal to marked ownership. */
2095
+ export function removePwaEntryReferences(html, {documentUrl, manifestUrl, markedManifestOnly = false} = {}) {
2096
2096
  if (!is.string(html)) throw new TypeError('PWA entry HTML must be a string.');
2097
2097
  const structure = scanHtmlStructure(html);
2098
2098
  const manifest = new URL(manifestUrl, documentUrl);
@@ -2106,6 +2106,7 @@ export function removePwaEntryReferences(html, {documentUrl, manifestUrl} = {})
2106
2106
  const edits = [];
2107
2107
  for (const link of structure.links) {
2108
2108
  const attributes = parseTagAttributes(link.open);
2109
+ if (markedManifestOnly && !attributes.has('data-arcane-manifest')) continue;
2109
2110
  const href = htmlAttributeView(attributes.get('href') ?? '');
2110
2111
  const rel = htmlAttributeView(attributes.get('rel') ?? '');
2111
2112
  if (!href?.decoded || !rel) continue;
@@ -2152,7 +2153,7 @@ export function removePwaEntryReferences(html, {documentUrl, manifestUrl} = {})
2152
2153
  }
2153
2154
 
2154
2155
  /** Update only active PWA entry references; registration never orders application startup. */
2155
- export function applyPwaEntryReferences(html,{manifestUrl,bootstrapUrl}={}){
2156
+ export function applyPwaEntryReferences(html,{manifestUrl,bootstrapUrl,markGeneratedManifest=false}={}){
2156
2157
  if(!is.string(html))throw new TypeError('PWA entry HTML must be a string.');
2157
2158
  if(!is.string(manifestUrl)||!manifestUrl||!is.string(bootstrapUrl)||!bootstrapUrl){
2158
2159
  throw new TypeError('PWA manifestUrl and bootstrapUrl must be nonempty strings.');
@@ -2223,7 +2224,10 @@ export function applyPwaEntryReferences(html,{manifestUrl,bootstrapUrl}={}){
2223
2224
  });
2224
2225
  }
2225
2226
  }
2226
- if(!manifestFound)additions.push(`<link rel="manifest" href="${attributeText(manifestUrl)}">`);
2227
+ if(!manifestFound){
2228
+ const marker=markGeneratedManifest?' data-arcane-manifest':'';
2229
+ additions.push(`<link rel="manifest" href="${attributeText(manifestUrl)}"${marker}>`);
2230
+ }
2227
2231
  let bootstrapFound=false;
2228
2232
  for(const script of structure.scripts){
2229
2233
  if(!parseTagAttributes(script.open).has('data-arcane-pwa'))continue;
package/src/toolchain.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import Is from 'strong-type';
2
2
  import path from 'node:path';
3
3
  import {resolveAppRoot,resolvePackageOutputRoot} from './app-layout.mjs';
4
- import {readdir,lstat,realpath,mkdir,readFile,writeFile} from 'node:fs/promises';
4
+ import {readdir,lstat,realpath,mkdir,readFile,writeFile,unlink} from 'node:fs/promises';
5
5
  import {createWorkspace,initWorkspace} from './scaffold.mjs';
6
6
  import {
7
7
  discoverApps as discoverWorkspaceApps,
@@ -12,8 +12,14 @@ import {
12
12
  } from './workspace.mjs';
13
13
  import {loadArcaneIntegratedProvider} from './integrated-provider-loader.mjs';
14
14
  import {collectSourcePwaAssets,startDevServer} from './dev-server.mjs';
15
- import {applyPwaEntryReferences,generateImportMap,readApplicationTestImportMapContext} from './import-map.mjs';
16
- import {createPwaArtifacts} from './pwa.mjs';
15
+ import {applyPwaEntryReferences,generateImportMap,readApplicationTestImportMapContext,removePwaEntryReferences} from './import-map.mjs';
16
+ import {
17
+ createPwaArtifacts,
18
+ PWA_MANIFEST_NAME,
19
+ PWA_OFFLINE_MANIFEST_NAME,
20
+ PWA_WORKER_NAME,
21
+ PWA_BOOTSTRAP_NAME
22
+ } from './pwa.mjs';
17
23
  import {readInstalledSdkLayout} from './sdk-runtime-layout.mjs';
18
24
  import {withWorkspaceOperationLock} from './workspace-operation-lock.mjs';
19
25
  import {refreshAppPackageProjection} from './app-descriptor.mjs';
@@ -293,7 +299,7 @@ async function validatePreparedRuntime(prepared,{signal}={}){
293
299
  return validation;
294
300
  }
295
301
 
296
- async function refreshPreparedImportMap(prepared,{signal,onEvent,workspaceOperationLease}={}){
302
+ async function refreshPreparedImportMap(prepared,{signal,onEvent,workspaceOperationLease}={},retireGeneratedPwa=false){
297
303
  const manifest=prepared.validation.app.manifest;
298
304
  const inspected=await inspectPackagedApp({
299
305
  workspaceRoot:prepared.workspaceRoot,
@@ -322,12 +328,12 @@ async function refreshPreparedImportMap(prepared,{signal,onEvent,workspaceOperat
322
328
  onEvent
323
329
  });
324
330
  if(prepared.validation.config.appsRoot==='.'){
325
- await refreshRootApplicationFiles(prepared,inspected,importMap,{signal,onEvent});
331
+ await refreshRootApplicationFiles(prepared,inspected,importMap,{signal,onEvent,retireGeneratedPwa});
326
332
  }
327
333
  return importMap;
328
334
  }
329
335
 
330
- async function refreshRootApplicationFiles(prepared,inspected,importMap,{signal,onEvent}){
336
+ async function refreshRootApplicationFiles(prepared,inspected,importMap,{signal,onEvent,retireGeneratedPwa=false}){
331
337
  const {workspaceRoot,appId}=prepared;
332
338
  const manifest=prepared.validation.app.manifest;
333
339
  const installed=await readInstalledSdkLayout(workspaceRoot,prepared.validation.config);
@@ -385,21 +391,44 @@ async function refreshRootApplicationFiles(prepared,inspected,importMap,{signal,
385
391
  await mkdir(path.dirname(filePath),{recursive:true});
386
392
  await writeFile(filePath,file.content,'utf8');
387
393
  }
388
- if(pwa){
394
+ const pwaDisabled=manifest.pwa?.enabled===false;
395
+ const retireConfirmedOutput=pwaDisabled&&retireGeneratedPwa===true;
396
+ if(pwa||pwaDisabled){
389
397
  for(const document of inspected.browserDocuments){
390
398
  throwIfAborted(signal);
391
399
  const filePath=path.join(prepared.appRoot,...document.path.split('/'));
392
400
  const content=await readFile(filePath,'utf8');
393
- await writeFile(filePath,applyPwaEntryReferences(content,{
401
+ const updated=pwa?applyPwaEntryReferences(content,{
394
402
  manifestUrl:`/${pwa.entryAssets.manifest}`,
395
- bootstrapUrl:`/${pwa.entryAssets.bootstrap}`
396
- }),'utf8');
403
+ bootstrapUrl:`/${pwa.entryAssets.bootstrap}`,
404
+ markGeneratedManifest:true
405
+ }):removePwaEntryReferences(content,{
406
+ documentUrl:new URL(`/${document.path.split('/').map(encodeURIComponent).join('/')}`,sourceOrigin),
407
+ manifestUrl:`/${PWA_MANIFEST_NAME}`,
408
+ markedManifestOnly:!retireConfirmedOutput
409
+ });
410
+ if(updated!==content)await writeFile(filePath,updated,'utf8');
411
+ }
412
+ }
413
+ const retiredPwa=[];
414
+ if(retireConfirmedOutput){
415
+ // The caller declares these standard outputs generated, not merely same-named authored files.
416
+ // Remove selected references before retiring their targets; browser storage is untouched.
417
+ for(const relative of [PWA_MANIFEST_NAME,PWA_OFFLINE_MANIFEST_NAME,PWA_WORKER_NAME,PWA_BOOTSTRAP_NAME]){
418
+ throwIfAborted(signal);
419
+ try{
420
+ await unlink(path.join(workspaceRoot,relative));
421
+ retiredPwa.push(relative);
422
+ }catch(error){
423
+ if(error.code!=='ENOENT')throw error;
424
+ }
397
425
  }
398
426
  }
399
427
  await emit(onEvent,{
400
428
  type:'import-map.root-files.completed',appId,
401
429
  navigation:[],
402
- pwa:pwa?.files.map(file=>file.path)??[]
430
+ pwa:pwa?.files.map(file=>file.path)??[],
431
+ retiredPwa
403
432
  });
404
433
  }
405
434
 
@@ -408,7 +437,7 @@ async function importMapApplication(options={}){
408
437
  const prepared=await preparedWorkspace({...options,allowMissingManagedImportMap:true});
409
438
  const importMap=await ownedWork(
410
439
  'import-map',
411
- context=>refreshPreparedImportMap(prepared,context),
440
+ context=>refreshPreparedImportMap(prepared,context,options.retireGeneratedPwa===true),
412
441
  {...options,workspaceRoot:prepared.workspaceRoot}
413
442
  );
414
443
  await validatePreparedRuntime(prepared,{signal:options.signal});