arcane-os 0.91.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,38 @@
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
+
20
+ ## 0.92.0
21
+
22
+ - Add opt-in `allowUnborn` Git text snapshots and `initialBranch` repository
23
+ preparation for first publication from a remote with no advertised revision.
24
+ Repository configuration now reports the actual revision and unborn state.
25
+ Preserve existing checkouts, advertised refs, complete diagnostics and real
26
+ Git failures; first publication uses the existing explicit non-force writer.
27
+ - Let the default Windows `git` process runner discover an installed Git for
28
+ Windows registration and append its command directory only to that child's
29
+ PATH. Existing lookup priority, other platforms, explicit executable paths
30
+ and custom adapters retain their behavior; no installation, global setting
31
+ change or command retry is introduced.
32
+ - Reuse the retained Windows host, Whisper and ONNX runtime archives. This
33
+ release changes shared JavaScript repository/process behavior, not those
34
+ native binaries; application adoption remains independently owned.
35
+
3
36
  ## 0.91.0
4
37
 
5
38
  - Persist complete native `modelAssets.prepare()` acquisitions across release,
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.91.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
@@ -47,7 +47,7 @@ const status = await repository.status({signal});
47
47
  await repository.close();
48
48
  ```
49
49
 
50
- `createRepositoryWorkspace({name,directory,dataRoot,remote,remoteBase,branch,longPaths,cloneIdentity,gitIdentity,onEvent,run}={})`
50
+ `createRepositoryWorkspace({name,directory,dataRoot,remote,remoteBase,branch,initialBranch,longPaths,cloneIdentity,gitIdentity,onEvent,run}={})`
51
51
  returns `{directory,open,status,configuration,pull,push,write,close,drain,dispose}`. The `directory`
52
52
  property is the absolute selected working path.
53
53
 
@@ -63,6 +63,10 @@ property is the absolute selected working path.
63
63
  `branch`, when supplied, is passed unchanged to Git's `clone --branch` option;
64
64
  otherwise Git selects the remote's default branch. Existing checkouts keep
65
65
  their own branch, remote, tracked files, untracked files and local changes.
66
+ - Optional `initialBranch` names the branch for first publication from a new
67
+ unborn clone. It is separate from `branch`, which selects an existing remote
68
+ branch or tag. Omit `branch` when the remote has no advertised revision.
69
+ See [first publication](#first-publication-from-an-unborn-remote).
66
70
  - Supplying `remoteBase` explicitly declares this initial-clone `remote` to be
67
71
  a local filesystem locator. Its native absolute command argument is captured
68
72
  during construction. Omission leaves Git's original string interpretation
@@ -77,7 +81,11 @@ property is the absolute selected working path.
77
81
  keep Git's ordinary inheritance; existing checkouts retain their configuration.
78
82
  See [clone-local author selection](#clone-local-author-selection).
79
83
  - `onEvent` and optional `run` use the existing SDK process owner. The default
80
- adapter is `runProcess`; Git must be available on PATH. Credentials and
84
+ adapter is `runProcess`; Git can be available on PATH or through an installed
85
+ Git for Windows registration. The default Windows runner appends that
86
+ installation's `cmd` directory only to the Git child's PATH, preserving
87
+ existing command lookup priority. See [Windows Git discovery](sdk-api.md#runprocess).
88
+ Custom `run` adapters retain their own execution behavior. Credentials and
81
89
  authentication remain with Git and the native host. The SDK adds no download,
82
90
  credential store, polling, network retry or process supervisor.
83
91
 
@@ -147,6 +155,8 @@ Its result is:
147
155
  {
148
156
  repositoryRoot,
149
157
  headRef: fullSymbolicRef | null,
158
+ revision: currentCommit | null,
159
+ unborn: boolean,
150
160
  origin: {urls: [...], pushUrls: [...]},
151
161
  upstream: {remoteNames: [...], mergeRefs: [...], urls: [...], pushUrls: [...]} | null
152
162
  }
@@ -160,6 +170,16 @@ arrays contain that branch's configured `remote` and `merge` values; `urls` and
160
170
  means the local repository and has no remote URL arrays. Origin is observed
161
171
  independently, even when it is not the upstream.
162
172
 
173
+ `revision` and `unborn` come from Git's successful
174
+ [porcelain-v2 branch observation](https://git-scm.com/docs/git-status#_porcelain_format_version_2):
175
+ `(initial)` yields `revision:null, unborn:true`; a current commit yields that
176
+ opaque revision and `unborn:false`, including a commit with an empty tree and
177
+ detached HEAD. Symbolic HEAD alone does not establish an unborn branch.
178
+ The command uses `--no-optional-locks` and omits untracked-file discovery, so
179
+ this observation does not refresh the index on disk. A missing branch-state
180
+ record reports `ARCANE_REPOSITORY_HEAD_UNAVAILABLE`; unsuccessful commands
181
+ retain their actual failures rather than becoming an initial state.
182
+
163
183
  Arrays retain configured order, repeated values, complete UTF-8 strings and
164
184
  empty strings. `[]` means unset; `['']` means an explicit empty value. The
165
185
  reader honors configured includes and ordinary Git configuration precedence.
@@ -172,8 +192,8 @@ Metadata uses the shared raw-Git-text reader. Lifecycle/stderr events remain
172
192
  with the process owner, raw stdout remains parser-owned, and undecodable text
173
193
  reports `ARCANE_GIT_CONFIGURATION_NOT_TEXT`. Failures retain complete raw
174
194
  stdout, process diagnostics and causal errors using the identity reader's
175
- documented attachment behavior. One observation uses four read-only Git
176
- commands for an attached local branch, or three otherwise. Results are not a
195
+ documented attachment behavior. One observation uses five read-only Git
196
+ commands for an attached local branch, or four otherwise. Results are not a
177
197
  transaction with later work or external Git activity. The app owns any decision
178
198
  that the current checkout corresponds to its selected repository and branch.
179
199
 
@@ -212,6 +232,69 @@ observe, refresh, write and publish. See Git's [pull](https://git-scm.com/docs/g
212
232
  [push](https://git-scm.com/docs/git-push) and [configuration](https://git-scm.com/docs/git-config)
213
233
  contracts.
214
234
 
235
+ ### First publication from an unborn remote
236
+
237
+ An opt-in [snapshot refresh](git-text-snapshot.md#unborn-remotes) can report
238
+ `{revision:null,files:[],unborn:true}` after a successful complete empty remote
239
+ advertisement. This means no revision was advertised to this connection; it
240
+ does not assert that hidden refs or unreachable objects cannot exist.
241
+ The application retains that explicit state instead of inventing a commit.
242
+
243
+ For an application-selected new connection with that state, prepare its first
244
+ branch without passing the existing-branch `clone --branch` option:
245
+
246
+ ```javascript
247
+ const repository = createRepositoryWorkspace({
248
+ name: connection.name,
249
+ remote: connection.locator,
250
+ remoteBase: connection.localBaseDirectory,
251
+ initialBranch: 'main'
252
+ });
253
+ await repository.open({signal});
254
+ const observed = await repository.configuration({signal});
255
+ // The application matches observed.headRef, unborn and configured URLs to its connection.
256
+ const publication = await repository.write({
257
+ files: selectedFiles,
258
+ message: authoredCommitMessage,
259
+ target: {remote: connection.locator, remoteBase: connection.localBaseDirectory, ref: 'refs/heads/main'},
260
+ signal
261
+ });
262
+ ```
263
+
264
+ `initialBranch` is captured during construction. After a successful new clone,
265
+ the SDK observes the complete remote advertisement using the clone's original
266
+ working-directory context and captured remote argument. Only empty successful
267
+ output followed by the new checkout's explicit initial state and successful
268
+ empty `git for-each-ref --format=%(refname)` observation selects
269
+ `HEAD` as `refs/heads/<initialBranch>` through
270
+ [Git symbolic-ref](https://git-scm.com/docs/git-symbolic-ref). Git owns branch-name
271
+ validity. Advertised HEAD, branches or tags keep ordinary clone selection;
272
+ an already committed clone and every existing destination remain unchanged.
273
+ Any local ref supplied by Git's template or hook behavior is preserved and
274
+ leaves HEAD unchanged, even when the remote advertises nothing.
275
+ Omission adds no process and preserves the previous open result.
276
+
277
+ For a new clone with `initialBranch`, `open` retains its clone `stdout`/`stderr`
278
+ and adds ordered `outputs:[{operation,stdout,stderr,code,signal}]`. Operations
279
+ are `clone`, `observeRemote`, `observeHead`, `observeRefs` and `selectInitialBranch`, only as
280
+ performed. A later preparation failure preserves completed outputs in
281
+ `error.details:{directory,cloned:true,outputs}` and the original complete
282
+ failure as `cause`; cancellation retains completed observations too. A writer
283
+ that prepares first retains these processes in its existing `stage:'prepare'`
284
+ output records. Failed preparation leaves the actual directory intact and does
285
+ not retry initialization or rename an existing checkout on the next call.
286
+
287
+ There is nothing to pull before the first commit of a confirmed new unborn
288
+ connection. Use the existing exact-file `write` and explicit non-force
289
+ `HEAD:refs/heads/main` publication directly. For a committed remote, retain the
290
+ ordinary selected-branch clone and targeted pull/write composition. An existing
291
+ local checkout, different local branch or intervening remote change needs the
292
+ application's own connection decision; an unborn observation is not permission
293
+ to discard or switch it. Observations and publication are not transactional:
294
+ concurrent remote changes, authentication failures and rejected pushes remain
295
+ actual errors. No pull failure is converted into success, and no retry, force,
296
+ rollback, global setting or fabricated revision is introduced.
297
+
215
298
  ### Relative local repository locators
216
299
 
217
300
  `remoteBase` is an explicit local-path choice, not a hint for a URL parser.