arcane-os 0.92.0 → 0.93.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,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.93.0
4
+
5
+ - Add native desktop notification status, submission, state and removal through
6
+ `Arcane.notifications`, with ordered notification state events and an owned
7
+ Windows adapter. The host reports actual submission and OS errors; submission
8
+ does not establish that a person saw or activated the notification.
9
+ - Retain complete native document errors in the Windows host session, including
10
+ `window.error`, `unhandledrejection` and explicit `reportNativeError()` reports.
11
+ Retrieve non-consuming records through `connectAppControl().diagnostics()`
12
+ without foregrounding the app or waiting for Core/model readiness. Records
13
+ survive document navigation within that host session and end with the host.
14
+ - Publish the matching rebuilt Windows host archive with its document-start
15
+ diagnostic helper. Keep ordinary console output separate from retained error
16
+ history; console interception and caught console arguments are outside this
17
+ release. Native notification behavior and application acceptance remain
18
+ consumer-owned verification, distinct from the selected compile and package.
19
+
3
20
  ## 0.92.0
4
21
 
5
22
  - Add opt-in `allowUnborn` Git text snapshots and `initialBranch` repository
package/README.md CHANGED
@@ -25,7 +25,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
25
25
  share the theme, packaging, event, cancellation,
26
26
  and browser run contracts while retaining their selected application layout.
27
27
 
28
- This checkout defines the `0.92.0` SDK contract. Development applications declare
28
+ This checkout defines the `0.93.0` SDK contract. Development applications declare
29
29
  `arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
30
30
  designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
31
31
  to refresh and compare the installed SDK and lockfile with the published channel;
@@ -646,6 +646,24 @@ export function createCoreFacade(client){
646
646
  storage:{list:()=>invoke('storage.list'),get:key=>invoke('storage.get',{key}),set:(key,value)=>invoke('storage.set',{key,value}),delete:key=>invoke('storage.delete',{key})},
647
647
  preferences:{list:()=>invoke('preferences.list'),get:key=>invoke('preferences.get',{key}),set:(key,value)=>invoke('preferences.set',{key,value}),
648
648
  setMany:entries=>invoke('preferences.setMany',{entries}),delete:key=>invoke('preferences.delete',{key})},
649
+ notifications: {
650
+ status(options) {
651
+ return invoke('notifications.status', {}, options).catch(function notificationStatusUnavailable(error) {
652
+ if (error?.code !== 'METHOD_NOT_ALLOWED' || error?.reason !== 'core-namespace-unavailable') throw error;
653
+ return {supported: false, available: false, permissionDisabled: null,
654
+ reason: 'host-notifications-unavailable', error};
655
+ });
656
+ },
657
+ show(request, options) {
658
+ return invoke('notifications.show', request, options);
659
+ },
660
+ state(selection = {}, options) {
661
+ return invoke('notifications.state', selection, options);
662
+ },
663
+ close(selection, options) {
664
+ return invoke('notifications.close', selection, options);
665
+ }
666
+ },
649
667
  window: {
650
668
  state(options) {
651
669
  return invoke('window.state', {}, {signal: options?.signal});
@@ -43,6 +43,26 @@ export const CORE_METHOD_CONTRACTS={
43
43
  output: '{requestedTarget,resolvedDevice,resolution,reason}',
44
44
  meaning: 'Resolve a saved physical identity. Explicit null selects engine-owned automatic resolution; it does not claim a configured or observed device.'
45
45
  },
46
+ 'notifications.status': {
47
+ input: '{}',
48
+ output: '{supported,available,permissionDisabled,reason?,error?}',
49
+ meaning: 'Actual native host support and OS permission state; no preference changes or browser/audio substitution.'
50
+ },
51
+ 'notifications.show': {
52
+ input: '{id,title,body,data}',
53
+ output: 'notification-record-v1',
54
+ meaning: 'Submit one exact application notification. Submitted means native Show returned, not display or human receipt. Caller IDs cannot be reused during this host lifetime.'
55
+ },
56
+ 'notifications.state': {
57
+ input: '{id?}',
58
+ output: '{revision,notifications:notification-record-v1[]}',
59
+ meaning: 'Retained complete records for this application host lifetime, including actual terminal outcomes; accepted notices survive renderer navigation.'
60
+ },
61
+ 'notifications.close': {
62
+ input: '{id}',
63
+ output: 'notification-record-v1',
64
+ meaning: 'Remove only the exact owned notice and retain its record. Cancellation after submission does not reverse human interaction.'
65
+ },
46
66
  'window.state': {
47
67
  input: '{}',
48
68
  output: '{platform,supported,state}',
@@ -0,0 +1,217 @@
1
+ const NATIVE_DIAGNOSTICS_KEY = Symbol.for('arcane-os.native-diagnostics');
2
+ const NATIVE_DOCUMENT_KEY = Symbol.for('arcane-os.app-control.document');
3
+
4
+ /** Report an application-caught error to this document's native host ledger. */
5
+ export async function reportNativeError(error, options = {}) {
6
+ const owner = globalThis[NATIVE_DIAGNOSTICS_KEY];
7
+ if (!owner) throw nativeDiagnosticsUnavailable();
8
+ return owner.report(error, options);
9
+ }
10
+
11
+ /** The native host installs this once, at document creation, before app scripts. */
12
+ export function installNativeDiagnostics(global = globalThis) {
13
+ if (global[NATIVE_DIAGNOSTICS_KEY]) return global[NATIVE_DIAGNOSTICS_KEY];
14
+
15
+ const documentId = global[NATIVE_DOCUMENT_KEY] ??= global.crypto.randomUUID();
16
+ let delivery = Promise.resolve();
17
+ const owner = {
18
+ documentId,
19
+ ready: null,
20
+ report(error, options = {}) {
21
+ return capture('application', error, options);
22
+ }
23
+ };
24
+ global[NATIVE_DIAGNOSTICS_KEY] = owner;
25
+ global.addEventListener('error', captureWindowError);
26
+ global.addEventListener('unhandledrejection', captureUnhandledRejection);
27
+ owner.ready = transmit(envelope('document'));
28
+ owner.ready.catch(reportCaptureFailure);
29
+ return owner;
30
+
31
+ function envelope(type, record) {
32
+ return {
33
+ protocol: 'arcane.native-diagnostics/1',
34
+ type,
35
+ documentId,
36
+ documentUrl: global.location.href,
37
+ time: new Date().toISOString(),
38
+ ...(record === undefined ? {} : {record})
39
+ };
40
+ }
41
+
42
+ async function capture(kind, error, options = {}, observation) {
43
+ // Snapshot synchronously at the reported failure, before any bridge wait.
44
+ const record = {kind, ...encodeDiagnosticValues(error, options)};
45
+ if (observation !== undefined) {
46
+ record.location = observation.location;
47
+ record.message = observation.message;
48
+ }
49
+ return transmit(envelope('error', record));
50
+ }
51
+
52
+ function transmit(frame) {
53
+ // This document has one ordered ingress. A failed write is observed by
54
+ // its caller and does not prevent subsequent independent reports.
55
+ const text = JSON.stringify(frame);
56
+ const pending = delivery.then(async function sendDiagnostic() {
57
+ const windows = global.chrome?.webview?.hostObjects?.arcaneDiagnostics;
58
+ const webkit = global.webkit?.messageHandlers?.arcaneDiagnostics;
59
+ let reply;
60
+ if (windows) reply = await windows.Send(text);
61
+ else if (webkit) reply = await webkit.postMessage(text);
62
+ else throw nativeDiagnosticsUnavailable();
63
+ const result = typeof reply === 'string' ? JSON.parse(reply) : reply;
64
+ if (result?.accepted !== true) {
65
+ const error = new Error('The native host did not retain the diagnostic.', {cause: result?.error});
66
+ error.code = 'ARCANE_NATIVE_DIAGNOSTIC_REJECTED';
67
+ error.details = result;
68
+ throw error;
69
+ }
70
+ return result;
71
+ });
72
+ delivery = pending.catch(function observeOrderedFailure() {});
73
+ return pending;
74
+ }
75
+
76
+ function captureWindowError(event) {
77
+ // Resource-element load failures are a different observation boundary;
78
+ // this listener does not inspect their DOM or request payloads.
79
+ if (event.target !== global) return;
80
+ capture('window.error', event.error, {},
81
+ {message: event.message, location: {file: event.filename, line: event.lineno, column: event.colno}}
82
+ ).catch(function failedWindowCapture(failure) {
83
+ reportCaptureFailure(failure, event.error);
84
+ });
85
+ }
86
+
87
+ function captureUnhandledRejection(event) {
88
+ capture('unhandledrejection', event.reason).catch(function failedRejectionCapture(failure) {
89
+ reportCaptureFailure(failure, event.reason);
90
+ });
91
+ }
92
+
93
+ function reportCaptureFailure(failure, original) {
94
+ // Observe our own asynchronous failure without manufacturing another
95
+ // unhandled rejection. The browser's ordinary error behavior continues.
96
+ global.console.error('Arcane native diagnostic capture failed.', failure, original);
97
+ }
98
+ }
99
+
100
+ function nativeDiagnosticsUnavailable() {
101
+ const error = new Error('This document has no native diagnostic capture host.');
102
+ error.code = 'ARCANE_NATIVE_DIAGNOSTICS_UNAVAILABLE';
103
+ return error;
104
+ }
105
+
106
+ /**
107
+ * Transport-local property graph, not JSON.stringify(error). Descriptor values
108
+ * preserve non-enumerable Error fields, symbols, cycles and shared references.
109
+ * Accessors remain descriptors; inspection never calls their get/set functions.
110
+ * An object that rejects reflection fails the report with that original error.
111
+ */
112
+ function encodeDiagnosticValues(error, options) {
113
+ const references = new Map();
114
+ const pending = [];
115
+ const values = [];
116
+ const record = {valueFormat: 'arcane.diagnostic-value/1', error: encode(error), values};
117
+ const details = Object.getOwnPropertyDescriptor(options, 'details');
118
+ if (details) {
119
+ record.details = Object.hasOwn(details, 'value') ? encode(details.value)
120
+ : {type: 'accessor', get: encode(details.get), set: encode(details.set)};
121
+ }
122
+
123
+ for (let index = 0; index < pending.length; index += 1) {
124
+ const value = pending[index];
125
+ const node = values[index];
126
+ if (typeof value === 'symbol') {
127
+ node.description = encode(value.description);
128
+ node.globalKey = encode(Symbol.keyFor(value));
129
+ continue;
130
+ }
131
+ const descriptors = Object.getOwnPropertyDescriptors(value);
132
+ node.properties = Reflect.ownKeys(descriptors).map(function describeProperty(key) {
133
+ return {key: encode(key), ...encodeDescriptor(descriptors[key])};
134
+ });
135
+ const prototype = Object.getPrototypeOf(value);
136
+ const names = prototypeNames(prototype);
137
+ if (names.length) node.constructorName = names[0];
138
+
139
+ // Error subclasses may supply these on a prototype, including custom
140
+ // accessors. Preserve the descriptor without reading through it.
141
+ node.inherited = [];
142
+ for (const key of ['name', 'message', 'stack', 'cause', 'errors', 'code', 'details']) {
143
+ if (Object.hasOwn(descriptors, key)) continue;
144
+ const descriptor = findDescriptor(prototype, key);
145
+ if (descriptor) node.inherited.push({key, ...encodeDescriptor(descriptor)});
146
+ }
147
+ if (names.includes('Map')) {
148
+ node.entries = Array.from(Map.prototype.entries.call(value), function encodeEntry([key, item]) {
149
+ return [encode(key), encode(item)];
150
+ });
151
+ } else if (names.includes('Set')) {
152
+ node.entries = Array.from(Set.prototype.values.call(value), encode);
153
+ } else if (names.includes('Date')) {
154
+ node.time = encode(Date.prototype.getTime.call(value));
155
+ } else if (names.includes('RegExp')) {
156
+ node.source = Object.getOwnPropertyDescriptor(RegExp.prototype, 'source').get.call(value);
157
+ node.flags = {};
158
+ for (const key of ['hasIndices', 'global', 'ignoreCase', 'multiline', 'dotAll', 'unicode', 'unicodeSets', 'sticky']) {
159
+ const getter = Object.getOwnPropertyDescriptor(RegExp.prototype, key)?.get;
160
+ if (getter) node.flags[key] = getter.call(value);
161
+ }
162
+ }
163
+ }
164
+ return record;
165
+
166
+ function encode(value) {
167
+ const type = typeof value;
168
+ if (value === null || type === 'string' || type === 'boolean') return value;
169
+ if (type === 'number') {
170
+ return Number.isFinite(value) && !Object.is(value, -0)
171
+ ? value : {type, value: Object.is(value, -0) ? '-0' : String(value)};
172
+ }
173
+ if (type === 'undefined') return {type};
174
+ if (type === 'bigint') return {type, value: String(value)};
175
+ if (!references.has(value)) {
176
+ const id = values.length + 1;
177
+ references.set(value, id);
178
+ pending.push(value);
179
+ values.push({id, type: Array.isArray(value) ? 'array' : type});
180
+ }
181
+ return {ref: references.get(value)};
182
+ }
183
+
184
+ function encodeDescriptor(descriptor) {
185
+ const result = {enumerable: descriptor.enumerable, configurable: descriptor.configurable};
186
+ if (Object.hasOwn(descriptor, 'value')) {
187
+ return {...result, value: encode(descriptor.value), writable: descriptor.writable};
188
+ }
189
+ return {...result, get: encode(descriptor.get), set: encode(descriptor.set)};
190
+ }
191
+
192
+ function findDescriptor(prototype, key) {
193
+ const visited = new Set();
194
+ while (prototype && !visited.has(prototype)) {
195
+ visited.add(prototype);
196
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, key);
197
+ if (descriptor) return descriptor;
198
+ prototype = Object.getPrototypeOf(prototype);
199
+ }
200
+ return undefined;
201
+ }
202
+
203
+ function prototypeNames(prototype) {
204
+ const names = [];
205
+ const visited = new Set();
206
+ while (prototype && !visited.has(prototype)) {
207
+ visited.add(prototype);
208
+ const constructor = Object.getOwnPropertyDescriptor(prototype, 'constructor')?.value;
209
+ if (typeof constructor === 'function') {
210
+ const name = Object.getOwnPropertyDescriptor(constructor, 'name')?.value;
211
+ if (typeof name === 'string') names.push(name);
212
+ }
213
+ prototype = Object.getPrototypeOf(prototype);
214
+ }
215
+ return names;
216
+ }
217
+ }
@@ -49,6 +49,7 @@ This inventory covers every enumerable, non-method member exposed directly or th
49
49
  | `Arcane.storage` | Namespace | App-scoped native key/value storage. |
50
50
  | `Arcane.preferences` | Namespace | App-scoped preference reads and atomic writes. |
51
51
  | `Arcane.window` | Namespace | SDK host window presentation for the calling document; adapter availability is independent of the OS-user appearance service. |
52
+ | `Arcane.notifications` | Namespace | Native app-scoped desktop notifications and retained host-lifetime outcomes; see the [notification contract](../core-client.md#native-desktop-notifications). |
52
53
  | `Arcane.appearance` | Namespace | Native appearance state and application. |
53
54
  | `Arcane.session` | Namespace | Current Arcane session lifecycle. |
54
55
  | `Arcane.provisioning` | Namespace | Provisioning-plan operations. |
@@ -394,6 +395,37 @@ product-neutral reference.
394
395
 
395
396
  Terminal output and lifecycle changes are event-driven. The canonical [Arcane event inventory](arcane-events.md#event-inventory) defines `terminal.output`, `terminal.exit`, and the Android `terminal.error` payload. The shared `TerminalClient` adapter correlates those deliveries with owned sessions and re-emits DOM events for reusable UI code.
396
397
 
398
+ ## Native desktop notifications
399
+
400
+ These methods use the actual application's native window bridge. Windows source
401
+ support is pending selected native build and OS delivery verification. Direct
402
+ shared-Core listeners and other unimplemented host adapters are unavailable;
403
+ there is no browser or audio substitute. The [complete contract](../core-client.md#native-desktop-notifications)
404
+ defines records, cancellation, duplicate IDs, permission reporting and recovery.
405
+
406
+ | Method | Parameters | Return | Description |
407
+ | --- | --- | --- | --- |
408
+ | `Arcane.notifications.status(options?)` | `options`: standard `signal`, `timeoutMs`, `onRequest` request options | `Promise<{supported,available,permissionDisabled,reason?,error?}>` | Reports actual host support and permission state without changing preferences. Only an exact unavailable Core namespace is normalized, retaining its original complete error. |
409
+ | `Arcane.notifications.show(request, options?)` | `request`: `{id,title,body,data}`; `options`: standard request options | `Promise<NotificationRecord>` | Submits exact title/body and opaque JSON-compatible data for the application's stable ID. Native submission is distinct from visual display or human receipt; a reused ID rejects with the existing record. |
410
+ | `Arcane.notifications.state(selection?, options?)` | `selection`: `{id?}`, defaults to `{}`; `options`: standard request options | `Promise<{revision,notifications:NotificationRecord[]}>` | Returns complete retained records for the current host lifetime. Subscribe to `notifications.state` first and merge by revision. |
411
+ | `Arcane.notifications.close(selection, options?)` | `selection`: `{id}`; `options`: standard request options | `Promise<NotificationRecord>` | Removes the exact owned notice and retains its record; no implicit resend or replacement. |
412
+
413
+ `NotificationRecord` contains `{id,title,body,data,state,revision}` plus actual
414
+ timestamps, reasons and complete errors. `event` identifies the latest observed
415
+ event and `events` retains its ordered history; activation can race removal
416
+ without changing an already `closed` state. Accepted notices survive renderer
417
+ navigation. Terminal callbacks remain retained; timeout dismissal does not mean
418
+ Notification Center removal. Recipients, triggers, text, deduplication and
419
+ navigation belong to the application. Cross-process persistence and cold-start
420
+ activation are outside this contract.
421
+
422
+ Windows keeps a stable native identity per WebView profile so the OS retains
423
+ that profile's notification preferences. The SDK persists registration metadata
424
+ only; its notification text/data records remain in host memory. A second
425
+ simultaneous notifier for the same profile reports
426
+ `notification-owner-already-running`; distinct profiles remain independent.
427
+ Live COM ownership and notices drain at host shutdown.
428
+
397
429
  ## Platform, installation, users, and system
398
430
 
399
431
  | Method | Parameters | Return | Description |
@@ -26,6 +26,31 @@ Every subscription method returns an unsubscribe function. Call it when the owni
26
26
  | `terminal.exit` | Live, future-only terminal session notification. | Core-backed Arcane Terminal and Android Arcane Terminal hosts. | A terminal process exits and the host removes its session. | `sessionId` identifies the terminal session; `exitCode` is the process exit code when available; `signal` is the terminating signal name or `null`. |
27
27
  | `terminal.error` | Live, future-only terminal session notification. | Android Arcane Terminal hosts. | Android stops a session at its output limit or cannot read one of its output streams. | `sessionId` identifies the terminal session; `message` explains the output-limit or stream-reading failure. |
28
28
  | `appearance.changed` | Live, future-only host notification. | Microsoft NT WebView2 hosts. | The host observes an operating-system appearance change. | `scheme` is the configured host scheme; `effectiveScheme` is the resolved `dark` or `light` scheme; `source` is `windows`. |
29
+ | `notifications.state` | Live, future-only complete notification record; retained state is read separately. | SDK Windows application-window adapter; native build and OS delivery verification pending. Other unimplemented hosts and direct shared-Core listeners are unavailable. | An owned native notification changes submission, activation, dismissal, closure or failure state. | Complete `{id,title,body,data,state,revision}` record plus actual timestamps, reasons and errors. Caller `id` correlates the record; `revision` orders updates. Native submission is not a display or human-receipt acknowledgement. |
30
+
31
+ ### Recover native notification state
32
+
33
+ Subscribe to `notifications.state` before calling `Arcane.notifications.state()`
34
+ and merge each record by revision. The event is not a durable completion event
35
+ for `when()`. A later renderer reads the host's retained records, including
36
+ actual terminal outcomes; accepted notices survive navigation. Retire old-client
37
+ subscriptions and pending snapshots when the installed client changes.
38
+ The record's `event` names its latest observation and `events` retains complete
39
+ ordered history. A raced `event:'activated'` can accompany `state:'closed'`;
40
+ the application decides how to route that actual activation.
41
+
42
+ Windows profile identity/shortcut registration persists across launches to
43
+ preserve OS preferences; the SDK's notification text, data and event records
44
+ remain in host memory for the current lifetime. A second simultaneous notifier
45
+ for that profile is unavailable with `notification-owner-already-running`;
46
+ other profiles are independent. Shutdown drains owned notices and live COM
47
+ ownership.
48
+
49
+ `submitted` means native `Show` returned. Later failure remains possible, and a
50
+ timeout dismissal does not establish removal from Notification Center. Explicit
51
+ `Arcane.notifications.close({id})` removes the owned notice while retaining its
52
+ record. See the [complete notification contract](../core-client.md#native-desktop-notifications)
53
+ for cancellation, permissions, exact content and host-lifetime limits.
29
54
 
30
55
  ### Subscribe and clean up
31
56
 
@@ -284,6 +284,105 @@ forwards the app's computed `--background` and `--text-color` in native hosts.
284
284
  Applications using that owner need no duplicate theme listener. Ordinary
285
285
  browsers keep their CSS presentation without making a native request.
286
286
 
287
+ ## Native desktop notifications
288
+
289
+ `Arcane.notifications` uses the existing Core request transport to the actual
290
+ application window's native host. The Windows adapter is source implementation;
291
+ selected native build and actual OS delivery verification remain pending. A
292
+ matching published host is required for delivery. The portable facade is the
293
+ same on Windows, Linux and macOS, with Android requiring a host adapter; other
294
+ unimplemented hosts and direct shared-Core listener connections are unavailable.
295
+ Those listener connections bypass the window bridge. There is no browser
296
+ `Notification`, audio or simulated delivery substitute.
297
+
298
+ | Method | Parameters | Result |
299
+ | --- | --- | --- |
300
+ | `status(options?)` | Request options only | Actual native `supported`, `available` and `permissionDisabled`, with the host's reason/error when present |
301
+ | `show(request, options?)` | `{id,title,body,data}` | Complete notification record for the actual submission outcome |
302
+ | `state(selection = {}, options?)` | Optional `{id}` selects one owned ID; omission selects all retained records | `{revision,notifications:[complete records]}` |
303
+ | `close(selection, options?)` | `{id}` | Complete record after the exact owned notice's removal operation |
304
+
305
+ Every method accepts the standard `invoke` options `signal`, `timeoutMs` and
306
+ `onRequest({requestId})`. Requests, title/body strings and opaque JSON-compatible
307
+ `data` pass through unchanged. The application owns recipients, triggers, text,
308
+ deduplication and navigation. The OS controls presentation; preserving the
309
+ complete request does not establish that every character was visibly displayed.
310
+
311
+ `status()` changes no OS permissions or preferences. Only the exact Core error
312
+ `METHOD_NOT_ALLOWED` with `reason:'core-namespace-unavailable'` becomes
313
+ `{supported:false,available:false,permissionDisabled:null,
314
+ reason:'host-notifications-unavailable',error}`; `error` is the original complete
315
+ error and no platform is invented. All other invocation rejections propagate
316
+ unchanged. The native adapter can return an unavailable result with the complete
317
+ OS initialization or settings error. `permissionDisabled:null` means the permission state is
318
+ unknown. Use this status operation to discover the window adapter; Core's
319
+ service capability list does not describe host-intercepted notification methods.
320
+ Native `supported:null` means initialization could not establish OS support;
321
+ `available:false` retains its actual reason and complete error when supplied.
322
+
323
+ A retained record contains `{id,title,body,data,state,revision}` and the actual
324
+ timestamps, reasons and complete errors supplied by its owner. `submitted` means
325
+ the native `Show` call returned. It does not establish visible display or human
326
+ receipt. Later activation, dismissal and failure remain observable and retained.
327
+ A dismissal due to timeout does not establish removal from Notification Center.
328
+ Use `close({id})` for explicit OS removal. Closing retains the record; showing
329
+ the same caller ID again during this host lifetime rejects with its existing
330
+ record instead of resending or replacing it.
331
+ The top-level `event` names the latest observation, while `events` retains the
332
+ complete ordered history. An activation racing successful removal can report
333
+ `event:'activated'` with `state:'closed'`; the application owns action routing.
334
+
335
+ The live `notifications.state` event carries one complete updated record. It is
336
+ future-only. Subscribe before requesting the snapshot, then merge records by
337
+ their revisions so a delayed snapshot cannot overwrite a newer event:
338
+
339
+ ```js
340
+ const records = new Map();
341
+ function receiveNotification(record) {
342
+ const previous = records.get(record.id);
343
+ if (!previous || record.revision >= previous.revision) records.set(record.id, record);
344
+ }
345
+ const stop = Arcane.events.on('notifications.state', receiveNotification);
346
+ try {
347
+ const snapshot = await Arcane.notifications.state();
348
+ for (const record of snapshot.notifications) receiveNotification(record);
349
+ } catch (error) {
350
+ stop();
351
+ throw error;
352
+ }
353
+ // Call stop() when this observation ends; it does not remove native notices.
354
+ ```
355
+
356
+ When the installed Core client changes, detach the old event listener and ignore
357
+ its outstanding snapshot response. Recreate the records map for the new
358
+ client/host lifetime so an earlier host's revisions cannot suppress its records.
359
+ Subscribe and read the new client's snapshot
360
+ through [installation observation](#observe-installation-and-retirement).
361
+ Accepted notices and their records survive renderer navigation within the same
362
+ native host. There is no persisted cross-process recovery or cold-process
363
+ activation contract.
364
+
365
+ Windows reuses an SDK-created native identity and shortcut for the existing
366
+ WebView profile across launches, preserving that profile's OS notification
367
+ preferences. Its owned `AppUserModelId` registration stores the application's
368
+ display name, icon and live activator identity without changing notification
369
+ preferences or registering a cold-start launch command.
370
+ The SDK persists registration metadata only; its notification text
371
+ and data records remain in host memory. OS notification retention remains
372
+ platform-owned.
373
+ Distinct profiles have independent notification settings. One live notifier
374
+ owns a profile's COM activation registration; a second simultaneous host using
375
+ that profile reports `available:false` with
376
+ `reason:'notification-owner-already-running'` and may retry explicitly later.
377
+ Shutdown releases live COM ownership and owned notices while preserving the
378
+ identity registration for a later launch.
379
+
380
+ Cancellation observed before submission prevents that pending show from
381
+ submitting. After submission, request cancellation or renderer retirement cannot
382
+ reverse human interaction and does not remove the accepted notice; `close` is
383
+ explicit. Host shutdown drains owned operations, removes owned notices and
384
+ detaches callbacks, surfacing complete cleanup errors.
385
+
287
386
  ## Events and request lifetime
288
387
 
289
388
  `events.on(name, listener)`, `once(name, listener)` and
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "sdkVersion": "0.91.0",
3
+ "sdkVersion": "0.92.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",
@@ -4304,7 +4304,7 @@
4304
4304
  "summary": "Describes existing Core method input, output and semantic shapes.",
4305
4305
  "availability": "Node and browser/bundler",
4306
4306
  "protocol": "arcane/1 method descriptions",
4307
- "normalization": "Descriptive data only, including independent localai.devices inventory and localai.resolveTarget physical-selection records; presence does not establish engine support or observed execution. The active host and its registered services remain the authority for method availability"
4307
+ "normalization": "Descriptive data only, including independent localai.devices inventory and localai.resolveTarget physical-selection records; presence does not establish engine support or observed execution. Native notifications.status/show/state/close describe actual host support, exact caller content, retained host-lifetime outcomes and explicit owned removal; native submission does not establish display or human receipt. The active host and its registered services remain the authority for method availability"
4308
4308
  },
4309
4309
  {
4310
4310
  "id": "core-contracts:CoreError",
@@ -4360,7 +4360,7 @@
4360
4360
  "summary": "Projects a Core client into the established application-facing Arcane namespace.",
4361
4361
  "availability": "Browser and native WebView JavaScript hosts with a Core client",
4362
4362
  "protocol": "arcane/1 compatibility facade",
4363
- "normalization": "Routes namespace methods through client.invoke while retaining operation-specific parameters, correlation, streaming callbacks, timeouts and cancellation; window.state({signal}?) reads the requesting window and window.setState({state},{signal}?) selects normal, maximized or fullscreen. The matching Windows host returns {platform:'windows',supported:true,state}, including observed minimized state, restores normal bounds/frame controls on leaving fullscreen and prevents activation/focus assignment during visible state transitions without changing TopMost, Core, origin or profile. Saved Settings remain application-owned; cancellation does not undo an applied change and unavailable hosts retain their actual errors without fallback. window.setTheme(presentation,{signal}?) forwards the complete per-window backgroundColor/textColor record, with omitted fields unchanged and null reset, and returns the host's actual applied/unsupported result without changing system appearance"
4363
+ "normalization": "Routes namespace methods through client.invoke while retaining operation-specific parameters, correlation, streaming callbacks, timeouts and cancellation; window.state({signal}?) reads the requesting window and window.setState({state},{signal}?) selects normal, maximized or fullscreen. The matching Windows host returns {platform:'windows',supported:true,state}, including observed minimized state, restores normal bounds/frame controls on leaving fullscreen and prevents activation/focus assignment during visible state transitions without changing TopMost, Core, origin or profile. Saved Settings remain application-owned; cancellation does not undo an applied change and unavailable hosts retain their actual errors without fallback. window.setTheme(presentation,{signal}?) forwards the complete per-window backgroundColor/textColor record, with omitted fields unchanged and null reset, and returns the host's actual applied/unsupported result without changing system appearance. notifications.status/show/state/close forward complete caller requests and standard invoke options; status normalizes only METHOD_NOT_ALLOWED with core-namespace-unavailable while retaining the complete original error. Matching native window hosts retain complete notification records and ordered revisions across renderer navigation, with notifications.state events and subscribe-before-snapshot recovery. Submitted means native Show returned, not displayed or read; cancellation after submission does not remove an accepted notice, and explicit close retains its record. Recipient, text, trigger, deduplication and navigation policy remains application-owned"
4364
4364
  },
4365
4365
  {
4366
4366
  "id": "core-client:installCoreClient",