arcane-os 0.57.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +6 -2
  3. package/browser-runtime/ai/core-onnx.mjs +96 -0
  4. package/browser-runtime/ai/onnx-tensors.mjs +85 -0
  5. package/browser-runtime/core/client.mjs +152 -17
  6. package/docs/architecture.md +5 -2
  7. package/docs/platform-targets.md +30 -13
  8. package/docs/reference/README.md +22 -3
  9. package/docs/reference/availability-and-normalization.md +14 -7
  10. package/docs/reference/cli.md +22 -10
  11. package/docs/reference/core-client.md +87 -0
  12. package/docs/reference/core-native-packaging.md +129 -9
  13. package/docs/reference/core-preferences.md +49 -0
  14. package/docs/reference/diarization.md +206 -0
  15. package/docs/reference/git-text-snapshot.md +104 -0
  16. package/docs/reference/inventory/package-api.json +205 -23
  17. package/docs/reference/local-ai.md +145 -11
  18. package/docs/reference/protocols.md +28 -19
  19. package/docs/reference/pwa.md +74 -4
  20. package/docs/reference/sdk-api.md +589 -39
  21. package/package.json +11 -3
  22. package/schemas/arcane-app.schema.json +3 -3
  23. package/schemas/arcane-package.schema.json +2 -2
  24. package/schemas/native-build-plan.schema.json +27 -1
  25. package/schemas/target-adapter.schema.json +2 -0
  26. package/src/app-descriptor.mjs +1 -1
  27. package/src/cli/main.mjs +14 -4
  28. package/src/constants.mjs +2 -0
  29. package/src/core/host.mjs +14 -5
  30. package/src/core/hosts/macos/ArcaneHost.h +86 -0
  31. package/src/core/hosts/macos/ArcaneHost.m +455 -0
  32. package/src/core/hosts/macos/ArcaneLauncher.m +432 -0
  33. package/src/core/hosts/windows/ArcaneLauncher.cs +4 -1
  34. package/src/core/services/local-ai.mjs +82 -4
  35. package/src/core/services/packaged-web.mjs +89 -0
  36. package/src/core/services/preferences.mjs +201 -0
  37. package/src/core-classic-source.mjs +2 -2
  38. package/src/dev-server.mjs +2 -5
  39. package/src/diarization/build.mjs +35 -0
  40. package/src/diarization/index.mjs +432 -0
  41. package/src/diarization/native/CMakeLists.txt +9 -0
  42. package/src/diarization/native/main.cpp +269 -0
  43. package/src/diarization/service.mjs +129 -0
  44. package/src/git-text-snapshot.mjs +265 -0
  45. package/src/import-map.mjs +9 -4
  46. package/src/index.mjs +1 -0
  47. package/src/local-ai/config.mjs +1 -1
  48. package/src/local-ai/install.mjs +134 -19
  49. package/src/local-ai/onnx-worker.mjs +85 -0
  50. package/src/local-ai/onnx.mjs +368 -0
  51. package/src/native/macos-provider.mjs +291 -0
  52. package/src/native/portable-layout.mjs +10 -2
  53. package/src/native/portable-provider.mjs +10 -5
  54. package/src/native-plan.mjs +9 -1
  55. package/src/native-provider-loader.mjs +3 -1
  56. package/src/process.mjs +123 -17
  57. package/src/scaffold.mjs +1 -1
  58. package/src/targets/index.mjs +11 -1
  59. package/src/templates/workspace-template.mjs +6 -5
  60. package/src/toolchain.mjs +42 -13
@@ -0,0 +1,432 @@
1
+ #import "ArcaneHost.h"
2
+ #import <dispatch/dispatch.h>
3
+
4
+ static NSError *ArcaneLauncherError(NSString *code, NSString *message, id detail) {
5
+ NSMutableDictionary *info = [@{NSLocalizedDescriptionKey: message, @"code": code} mutableCopy];
6
+ if (detail) info[@"detail"] = detail;
7
+ return [NSError errorWithDomain:@"ArcaneLauncher" code:1 userInfo:info];
8
+ }
9
+
10
+ static NSString *ArcaneAbsolutePath(NSString *filename, NSString *directory) {
11
+ return [(filename.isAbsolutePath ? filename : [directory stringByAppendingPathComponent:filename])
12
+ stringByStandardizingPath];
13
+ }
14
+
15
+ static NSDictionary *ArcaneReadRecord(NSURL *url, NSError **error) {
16
+ NSData *data = [NSData dataWithContentsOfURL:url options:0 error:error];
17
+ if (!data) return nil;
18
+ id record = [NSJSONSerialization JSONObjectWithData:data options:0 error:error];
19
+ if (!record) return nil;
20
+ if (![record isKindOfClass:NSDictionary.class]) {
21
+ *error = ArcaneLauncherError(@"CORE_LAUNCH_RECORD_INVALID", @"The selected file must contain a JSON object.", url);
22
+ return nil;
23
+ }
24
+ return record;
25
+ }
26
+
27
+ static NSString *ArcaneRequiredString(NSDictionary *record, NSString *key, NSError **error) {
28
+ id value = record[key];
29
+ if ([value isKindOfClass:NSString.class] && [value length]) return value;
30
+ *error = ArcaneLauncherError(@"CORE_LAUNCH_MANIFEST_INVALID",
31
+ [NSString stringWithFormat:@"The native manifest needs a string at %@.", key], record);
32
+ return nil;
33
+ }
34
+
35
+ static BOOL ArcaneExpectedDocumentCancellation(NSError *error) {
36
+ return ([error.domain isEqual:NSURLErrorDomain] && error.code == NSURLErrorCancelled)
37
+ || [error.userInfo[@"code"] isEqual:@"CORE_DOCUMENT_RETIRED"];
38
+ }
39
+
40
+ typedef NS_ENUM(NSUInteger, ArcaneDiagnosticStream) {
41
+ ArcaneDiagnosticNative,
42
+ ArcaneDiagnosticStandardOutput,
43
+ ArcaneDiagnosticStandardError
44
+ };
45
+
46
+ @interface ArcaneLauncher : NSObject <NSApplicationDelegate, ArcaneHostDelegate> {
47
+ ArcaneHost *_host;
48
+ NSString *_title;
49
+ NSString *_stateRoot;
50
+ NSString *_workingDirectory;
51
+ NSString *_launchFilename;
52
+ NSDictionary *_launchContext;
53
+ NSFileHandle *_nativeLog;
54
+ NSFileHandle *_stdoutLog;
55
+ NSFileHandle *_stderrLog;
56
+ dispatch_queue_t _diagnosticQueue;
57
+ BOOL _diagnosticsScheduled;
58
+ BOOL _diagnosticFailed;
59
+ BOOL _closeOnInputEnd;
60
+ BOOL _terminationRequested;
61
+ BOOL _finished;
62
+ BOOL _failurePresented;
63
+ BOOL _webReady;
64
+ int _exitStatus;
65
+ }
66
+ @property(nonatomic, readonly) int exitStatus;
67
+ @end
68
+
69
+ @implementation ArcaneLauncher
70
+
71
+ - (instancetype)init {
72
+ self = [super init];
73
+ if (self) _diagnosticQueue = dispatch_queue_create("org.arcane.launcher.diagnostics", DISPATCH_QUEUE_SERIAL);
74
+ return self;
75
+ }
76
+
77
+ - (int)exitStatus { return _exitStatus; }
78
+
79
+ - (NSFileHandle *)openLog:(NSString *)name inDirectory:(NSString *)directory error:(NSError **)error {
80
+ NSURL *url = [NSURL fileURLWithPath:[directory stringByAppendingPathComponent:name]];
81
+ if (![NSData.data writeToURL:url options:NSDataWritingWithoutOverwriting error:error]) return nil;
82
+ return [NSFileHandle fileHandleForWritingToURL:url error:error];
83
+ }
84
+
85
+ - (BOOL)openDiagnosticsAtRoot:(NSString *)stateRoot error:(NSError **)error {
86
+ NSString *directory = [stateRoot stringByAppendingPathComponent:@"Diagnostics"];
87
+ if (![NSFileManager.defaultManager createDirectoryAtPath:directory
88
+ withIntermediateDirectories:YES attributes:nil error:error]) return NO;
89
+ NSString *name = [NSString stringWithFormat:@"launch-%d-%@", NSProcessInfo.processInfo.processIdentifier, NSUUID.UUID.UUIDString];
90
+ _nativeLog = [self openLog:[name stringByAppendingString:@".jsonl"] inDirectory:directory error:error];
91
+ if (!_nativeLog) return NO;
92
+ _stdoutLog = [self openLog:[name stringByAppendingString:@".stdout"] inDirectory:directory error:error];
93
+ if (!_stdoutLog) return NO;
94
+ _stderrLog = [self openLog:[name stringByAppendingString:@".stderr"] inDirectory:directory error:error];
95
+ return _stderrLog != nil;
96
+ }
97
+
98
+ - (void)scheduleDiagnostics {
99
+ if (_diagnosticsScheduled) return;
100
+ _diagnosticsScheduled = YES;
101
+ NSString *stateRoot = _stateRoot ?: [NSHomeDirectory() stringByAppendingPathComponent:@"Library/Application Support/Arcane"];
102
+ // Setup precedes writes on this queue, not application startup. An
103
+ // unavailable directory leaves complete stderr delivery operational.
104
+ dispatch_async(_diagnosticQueue, ^{
105
+ NSError *error = nil;
106
+ @try {
107
+ if ([self openDiagnosticsAtRoot:stateRoot error:&error]) return;
108
+ } @catch (NSException *exception) {
109
+ error = ArcaneLauncherError(@"CORE_DIAGNOSTIC_OPEN_FAILED", exception.reason ?: exception.name,
110
+ @{ @"name": exception.name, @"userInfo": exception.userInfo ?: @{}, @"callStackSymbols": exception.callStackSymbols });
111
+ }
112
+ self->_diagnosticFailed = YES;
113
+ [self writeDiagnosticError:error ?: ArcaneLauncherError(@"CORE_DIAGNOSTIC_OPEN_FAILED",
114
+ @"The application diagnostic files could not be opened.", nil) toLog:self->_nativeLog];
115
+ });
116
+ }
117
+
118
+ - (void)writeDiagnosticData:(NSData *)data toLog:(NSFileHandle *)log {
119
+ NSError *error = nil;
120
+ if (log && ![log writeData:data error:&error]) {
121
+ _diagnosticFailed = YES;
122
+ // The original chunk is still sent intact to stderr even if its file
123
+ // becomes unavailable. Log failures must remain visible too.
124
+ NSLog(@"Arcane diagnostic file failed: %@", error);
125
+ }
126
+ error = nil;
127
+ if (![NSFileHandle.fileHandleWithStandardError writeData:data error:&error]) {
128
+ _diagnosticFailed = YES;
129
+ NSLog(@"Arcane diagnostic stderr failed: %@", error);
130
+ }
131
+ }
132
+
133
+ - (void)writeData:(NSData *)data stream:(ArcaneDiagnosticStream)stream {
134
+ // Retain the entire native chunk and preserve enqueue order without making
135
+ // the main-thread delegate wait for a file or a redirected stderr reader.
136
+ dispatch_async(_diagnosticQueue, ^{
137
+ NSFileHandle *log = stream == ArcaneDiagnosticNative ? self->_nativeLog
138
+ : stream == ArcaneDiagnosticStandardOutput ? self->_stdoutLog : self->_stderrLog;
139
+ [self writeDiagnosticData:data toLog:log];
140
+ });
141
+ }
142
+
143
+ - (void)writeDiagnosticError:(NSError *)error toLog:(NSFileHandle *)log {
144
+ // Foundation's archive preserves the complete native NSError/userInfo
145
+ // graph, including nested causes, URLs and binary values. JSON carries the
146
+ // reversible archive, not a shortened description of that native graph.
147
+ NSError *archiveError = nil;
148
+ NSData *archive = nil;
149
+ @try {
150
+ archive = [NSKeyedArchiver archivedDataWithRootObject:error requiringSecureCoding:NO error:&archiveError];
151
+ } @catch (NSException *exception) {
152
+ archiveError = ArcaneLauncherError(@"CORE_DIAGNOSTIC_ARCHIVE_FAILED", exception.reason ?: exception.name,
153
+ @{ @"name": exception.name, @"userInfo": exception.userInfo ?: @{} });
154
+ }
155
+ NSMutableDictionary *record = [@{ @"kind": @"native-error", @"domain": error.domain,
156
+ @"nativeCode": @(error.code), @"message": error.localizedDescription } mutableCopy];
157
+ if (archive) record[@"nativeArchiveBase64"] = [archive base64EncodedStringWithOptions:0];
158
+ else {
159
+ _diagnosticFailed = YES;
160
+ // No partial representation is claimed to be the complete native error.
161
+ record[@"representationFailure"] = archiveError.localizedDescription ?: @"The native error could not be archived.";
162
+ NSLog(@"Arcane native diagnostic could not be archived: %@; original: %@", archiveError, error);
163
+ }
164
+ NSError *jsonError = nil;
165
+ NSData *json = [NSJSONSerialization dataWithJSONObject:record options:0 error:&jsonError];
166
+ if (json) {
167
+ NSMutableData *line = [json mutableCopy];
168
+ [line appendData:[@"\n" dataUsingEncoding:NSUTF8StringEncoding]];
169
+ [self writeDiagnosticData:line toLog:log];
170
+ } else {
171
+ _diagnosticFailed = YES;
172
+ NSLog(@"Arcane diagnostic serialization failed: %@; original: %@", jsonError, error);
173
+ }
174
+ }
175
+
176
+ - (void)recordError:(NSError *)error {
177
+ if (!ArcaneExpectedDocumentCancellation(error)) _exitStatus = 1;
178
+ dispatch_async(_diagnosticQueue, ^{ [self writeDiagnosticError:error toLog:self->_nativeLog]; });
179
+ }
180
+
181
+ - (void)presentFailure:(NSString *)message {
182
+ _exitStatus = 1;
183
+ if (_terminationRequested || _finished) return;
184
+ if (_closeOnInputEnd) { [NSApp terminate:nil]; return; }
185
+ if (_failurePresented) return;
186
+ _failurePresented = YES;
187
+ NSAlert *alert = [[NSAlert alloc] init];
188
+ alert.alertStyle = NSAlertStyleCritical;
189
+ alert.messageText = message;
190
+ alert.informativeText = @"Close the application and try opening it again.";
191
+ [alert addButtonWithTitle:@"Close Application"];
192
+ if (_host.window.visible) {
193
+ [alert beginSheetModalForWindow:_host.window completionHandler:^(NSModalResponse response) {
194
+ [NSApp terminate:nil];
195
+ }];
196
+ } else {
197
+ [alert runModal];
198
+ [NSApp terminate:nil];
199
+ }
200
+ }
201
+
202
+ - (BOOL)prepareAndOpen:(NSError **)error {
203
+ _workingDirectory = NSFileManager.defaultManager.currentDirectoryPath;
204
+ NSArray<NSString *> *arguments = NSProcessInfo.processInfo.arguments;
205
+ for (NSUInteger index = 1; index < arguments.count; index++) {
206
+ NSString *argument = arguments[index];
207
+ if ([argument isEqual:@"--arcane-launch-config"]) {
208
+ if (++index == arguments.count || ![arguments[index] length]) {
209
+ *error = ArcaneLauncherError(@"CORE_LAUNCH_ARGUMENT_INVALID", @"--arcane-launch-config needs a filename.", nil);
210
+ return NO;
211
+ }
212
+ _launchFilename = ArcaneAbsolutePath(arguments[index], _workingDirectory);
213
+ } else if ([argument isEqual:@"--close-on-stdin-eof"]) _closeOnInputEnd = YES;
214
+ else {
215
+ *error = ArcaneLauncherError(@"CORE_LAUNCH_ARGUMENT_INVALID", @"Unknown launcher argument.", argument);
216
+ return NO;
217
+ }
218
+ }
219
+
220
+ NSURL *resources = NSBundle.mainBundle.resourceURL;
221
+ NSDictionary *manifest = ArcaneReadRecord([resources URLByAppendingPathComponent:@"arcane-native.json"], error);
222
+ if (!manifest) return NO;
223
+ NSDictionary *app = manifest[@"app"];
224
+ NSDictionary *client = manifest[@"client"];
225
+ NSDictionary *core = manifest[@"core"];
226
+ if (![app isKindOfClass:NSDictionary.class] || ![client isKindOfClass:NSDictionary.class]
227
+ || ![core isKindOfClass:NSDictionary.class]) {
228
+ *error = ArcaneLauncherError(@"CORE_LAUNCH_MANIFEST_INVALID", @"The native manifest needs app, client and core records.", manifest);
229
+ return NO;
230
+ }
231
+ NSString *appID = ArcaneRequiredString(app, @"id", error);
232
+ if (!appID) return NO;
233
+ _title = [app[@"displayName"] isKindOfClass:NSString.class] ? app[@"displayName"] : appID;
234
+ if (_launchFilename) {
235
+ _launchContext = ArcaneReadRecord([NSURL fileURLWithPath:_launchFilename], error);
236
+ if (!_launchContext) return NO;
237
+ }
238
+ id selectedState = _launchContext[@"stateRoot"];
239
+ _stateRoot = [selectedState isKindOfClass:NSString.class]
240
+ ? ArcaneAbsolutePath(selectedState, _workingDirectory)
241
+ : [[NSHomeDirectory() stringByAppendingPathComponent:@"Library/Application Support/Arcane"] stringByAppendingPathComponent:appID];
242
+ [self scheduleDiagnostics];
243
+ NSString *clientPath = ArcaneRequiredString(client, @"source", error);
244
+ if (!clientPath) return NO;
245
+ NSString *corePath = ArcaneRequiredString(core, @"entry", error);
246
+ if (!corePath) return NO;
247
+ NSString *classic = [NSString stringWithContentsOfURL:[resources URLByAppendingPathComponent:clientPath]
248
+ encoding:NSUTF8StringEncoding error:error];
249
+ if (!classic) return NO;
250
+ NSMutableArray<NSString *> *coreArguments = [NSMutableArray arrayWithObjects:
251
+ [resources URLByAppendingPathComponent:corePath].path,
252
+ @"--arcane-host-state-root", _stateRoot, nil];
253
+ if (_launchFilename) [coreArguments addObjectsFromArray:@[@"--arcane-launch-config", _launchFilename]];
254
+
255
+ NSMenu *menu = [[NSMenu alloc] initWithTitle:_title];
256
+ NSMenuItem *applicationItem = [[NSMenuItem alloc] initWithTitle:_title action:nil keyEquivalent:@""];
257
+ NSMenu *applicationMenu = [[NSMenu alloc] initWithTitle:_title];
258
+ [applicationMenu addItemWithTitle:[@"Quit " stringByAppendingString:_title] action:@selector(terminate:) keyEquivalent:@"q"];
259
+ applicationItem.submenu = applicationMenu;
260
+ [menu addItem:applicationItem];
261
+ NSApp.mainMenu = menu;
262
+
263
+ // The generated bundle ID owns its persistent default WebKit store on
264
+ // macOS 11+. stateRoot is Core/diagnostic storage, not a WebKit profile path.
265
+ _host = [ArcaneHost openApplicationURL:nil websiteDataStore:WKWebsiteDataStore.defaultDataStore
266
+ title:_title contentFrame:NSMakeRect(0, 0, 1100, 760) classicSource:classic
267
+ executableURL:[resources URLByAppendingPathComponent:@"runtime/node"] arguments:coreArguments
268
+ workingDirectoryURL:[NSURL fileURLWithPath:_workingDirectory isDirectory:YES] delegate:self];
269
+ [NSApp activateIgnoringOtherApps:YES];
270
+ if (_closeOnInputEnd) [self observeRunnerInput];
271
+ return YES;
272
+ }
273
+
274
+ - (void)observeRunnerInput {
275
+ // The opt-in pipe signals runner lifetime only. It never holds the window
276
+ // or the Core drain open when the user closes the application first.
277
+ __weak ArcaneLauncher *owner = self;
278
+ dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
279
+ @autoreleasepool {
280
+ NSError *error = nil;
281
+ NSFileHandle *input = NSFileHandle.fileHandleWithStandardInput;
282
+ for (;;) {
283
+ NSData *chunk = [input readDataUpToLength:4096 error:&error];
284
+ if (!chunk || !chunk.length) break;
285
+ }
286
+ dispatch_async(dispatch_get_main_queue(), ^{
287
+ ArcaneLauncher *launcher = owner;
288
+ if (!launcher || launcher->_finished) return;
289
+ if (error) [launcher recordError:error];
290
+ [NSApp terminate:nil];
291
+ });
292
+ }
293
+ });
294
+ }
295
+
296
+ - (void)applicationDidFinishLaunching:(NSNotification *)notification {
297
+ NSError *error = nil;
298
+ @try {
299
+ if ([self prepareAndOpen:&error]) return;
300
+ } @catch (NSException *exception) {
301
+ error = ArcaneLauncherError(@"CORE_LAUNCH_FAILED", exception.reason ?: exception.name,
302
+ @{ @"name": exception.name, @"userInfo": exception.userInfo ?: @{}, @"callStackSymbols": exception.callStackSymbols });
303
+ }
304
+ [self scheduleDiagnostics];
305
+ [self recordError:error ?: ArcaneLauncherError(@"CORE_LAUNCH_FAILED", @"Application startup failed.", nil)];
306
+ [self presentFailure:@"The application could not start."];
307
+ }
308
+
309
+ - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { return YES; }
310
+
311
+ - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
312
+ if (_terminationRequested) return NSTerminateLater;
313
+ _terminationRequested = YES;
314
+ // Reply after this callback returns, even if startup failed before a host
315
+ // existed or a directly closed window has already completed its drain.
316
+ dispatch_async(dispatch_get_main_queue(), ^{
317
+ void (^finished)(NSArray<NSError *> *) = ^(NSArray<NSError *> *errors) {
318
+ for (NSError *error in errors) [self recordError:error];
319
+ self->_finished = YES;
320
+ [sender replyToApplicationShouldTerminate:NO];
321
+ // Keep a natural main return with the real failure status instead
322
+ // of AppKit's unconditional successful process termination.
323
+ dispatch_async(dispatch_get_main_queue(), ^{
324
+ [sender stop:nil];
325
+ [sender postEvent:[NSEvent otherEventWithType:NSEventTypeApplicationDefined
326
+ location:NSZeroPoint modifierFlags:0 timestamp:0 windowNumber:0 context:nil
327
+ subtype:0 data1:0 data2:0] atStart:NO];
328
+ });
329
+ };
330
+ if (self->_host) [self->_host closeWithCompletion:finished];
331
+ else finished(@[]);
332
+ });
333
+ return NSTerminateLater;
334
+ }
335
+
336
+ - (void)host:(ArcaneHost *)host didLaunchCoreProcess:(pid_t)processIdentifier {
337
+ NSData *data = [[NSString stringWithFormat:@"{\"kind\":\"core-process-started\",\"pid\":%d}\n", processIdentifier]
338
+ dataUsingEncoding:NSUTF8StringEncoding];
339
+ [self writeData:data stream:ArcaneDiagnosticNative];
340
+ }
341
+
342
+ - (void)host:(ArcaneHost *)host receivedOutputData:(NSData *)data stream:(ArcaneCoreOutputStream)stream {
343
+ [self writeData:data stream:stream == ArcaneCoreStandardOutput
344
+ ? ArcaneDiagnosticStandardOutput : ArcaneDiagnosticStandardError];
345
+ }
346
+
347
+ - (void)host:(ArcaneHost *)host receivedFrameData:(NSData *)data {
348
+ NSError *error = nil;
349
+ id frame = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
350
+ // ArcaneHost diagnoses unreadable frames. Raw transport data has already
351
+ // reached the diagnostic owner, including all unrecognized event fields.
352
+ if (![frame isKindOfClass:NSDictionary.class] || ![frame[@"type"] isEqual:@"event"]) return;
353
+ id detail = frame[@"data"];
354
+ if ([frame[@"event"] isEqual:@"core.web.ready"]) {
355
+ if (_webReady || _terminationRequested) return;
356
+ NSString *url = [detail isKindOfClass:NSDictionary.class] ? detail[@"url"] : nil;
357
+ NSURL *applicationURL = [url isKindOfClass:NSString.class] ? [NSURL URLWithString:url] : nil;
358
+ if (!applicationURL.scheme || !applicationURL.host) {
359
+ [self recordError:ArcaneLauncherError(@"CORE_WEB_READY_INVALID", @"The packaged-web ready event has no application URL.", frame)];
360
+ [self presentFailure:@"The application could not open."];
361
+ return;
362
+ }
363
+ _webReady = YES;
364
+ [host loadApplicationURL:applicationURL];
365
+ } else if ([frame[@"event"] isEqual:@"core.web.failed"]
366
+ || ([frame[@"event"] isEqual:@"core.service.state"]
367
+ && [detail isKindOfClass:NSDictionary.class]
368
+ && [detail[@"name"] isEqual:@"packaged-web"] && [detail[@"state"] isEqual:@"failed"])) {
369
+ // Complete serialized service errors stay in the raw frame log, never
370
+ // in the ordinary native alert or the application's chat history.
371
+ [self presentFailure:_webReady ? @"The application connection stopped." : @"The application could not start."];
372
+ }
373
+ }
374
+
375
+ - (void)host:(ArcaneHost *)host didEncounterError:(NSError *)error {
376
+ [self recordError:error];
377
+ if (!_terminationRequested && !ArcaneExpectedDocumentCancellation(error)) {
378
+ [self presentFailure:@"The application encountered an error."];
379
+ }
380
+ }
381
+
382
+ - (void)host:(ArcaneHost *)host didExitCoreProcessWithStatus:(int)status reason:(NSTaskTerminationReason)reason {
383
+ if (status != 0 || reason != NSTaskTerminationReasonExit) _exitStatus = 1;
384
+ NSData *data = [[NSString stringWithFormat:@"{\"kind\":\"core-process-exited\",\"status\":%d,\"reason\":%ld}\n", status, (long)reason]
385
+ dataUsingEncoding:NSUTF8StringEncoding];
386
+ [self writeData:data stream:ArcaneDiagnosticNative];
387
+ }
388
+
389
+ - (void)host:(ArcaneHost *)host coreDidCompleteWithErrors:(NSArray<NSError *> *)errors {
390
+ for (NSError *error in errors) [self recordError:error];
391
+ if (_terminationRequested) return;
392
+ if (host.isClosing) {
393
+ // A title-bar close already requested the host-owned drain. Join that
394
+ // same completion instead of treating its EOF as a runtime failure.
395
+ [NSApp terminate:nil];
396
+ return;
397
+ }
398
+ [self recordError:ArcaneLauncherError(@"CORE_PROCESS_ENDED_UNEXPECTEDLY",
399
+ _webReady ? @"Core completed without an application or host close request."
400
+ : @"Core completed before the packaged web listener became ready, without a close request.",
401
+ @{ @"webReady": @(_webReady), @"errors": errors })];
402
+ [self presentFailure:_webReady ? @"The application connection stopped." : @"The application could not start."];
403
+ }
404
+
405
+ - (void)closeDiagnostics {
406
+ // AppKit and Core are already drained. This is the first consumer that
407
+ // needs durable completion of every accepted diagnostic write.
408
+ dispatch_sync(_diagnosticQueue, ^{
409
+ for (id file in @[self->_nativeLog ?: NSNull.null, self->_stdoutLog ?: NSNull.null, self->_stderrLog ?: NSNull.null]) {
410
+ if (![file isKindOfClass:NSFileHandle.class]) continue;
411
+ NSError *error = nil;
412
+ if (![file closeAndReturnError:&error]) {
413
+ self->_diagnosticFailed = YES;
414
+ [self writeDiagnosticError:error toLog:nil];
415
+ }
416
+ }
417
+ });
418
+ if (_diagnosticFailed) _exitStatus = 1;
419
+ }
420
+ @end
421
+
422
+ int main(int argc, const char *argv[]) {
423
+ @autoreleasepool {
424
+ NSApplication *application = NSApplication.sharedApplication;
425
+ [application setActivationPolicy:NSApplicationActivationPolicyRegular];
426
+ ArcaneLauncher *launcher = [[ArcaneLauncher alloc] init];
427
+ application.delegate = launcher;
428
+ [application run];
429
+ [launcher closeDiagnostics];
430
+ return launcher.exitStatus;
431
+ }
432
+ }
@@ -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
@@ -1,9 +1,12 @@
1
1
  import {createArcaneEventSource} from '../../event-manager.mjs';
2
2
  import Is from 'strong-type';
3
+ import path from 'node:path';
3
4
  import {CoreError, serializeCoreError} from '../../../browser-runtime/core/contracts.mjs';
4
5
  import {structuredOutputFormat, openAIResponseFormat} from '../../../browser-runtime/ai/twin-cloud.mjs';
5
6
  import {normalizeLocalAIConfig} from '../../local-ai/config.mjs';
6
7
  import {createLocalAIServer} from '../../local-ai/server.mjs';
8
+ import {createONNXRuntime} from '../../local-ai/onnx.mjs';
9
+ import {encodeTensorMap, decodeTensorMap, decodeTensorFetches} from '../../../browser-runtime/ai/onnx-tensors.mjs';
7
10
  import {requestLocalJSON, streamLocalJSON} from '../../local-ai/http.mjs';
8
11
 
9
12
  const is = new Is(false);
@@ -128,9 +131,27 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
128
131
  const events = createArcaneEventSource(owner, {source: 'core-local-ai', eventTypes: ['localai.state']});
129
132
  let context;
130
133
  let closed = false;
134
+ let onnx;
135
+ let onnxRecovery;
136
+ let onnxRecoveryError;
137
+ let stopONNXSubscription;
138
+ const onnxSelected = config.runtimes.some(function selectedONNX(requirement) { return requirement.id === 'onnx'; });
139
+ const onnxRecord = runtimes.find(function installedONNX(runtime) { return runtime.id === 'onnx'; });
140
+
141
+ function onnxStatus() {
142
+ const current = onnx?.current();
143
+ return {
144
+ id: 'onnx', installed: Boolean(onnxRecord?.modulePath),
145
+ available: Boolean(current) && !current.closed && !closed && !onnxRecovery && !onnxRecoveryError && !lifetimeSignal.aborted,
146
+ state: closed ? 'closed' : onnxRecovery ? 'recovering' : onnxRecoveryError ? 'error'
147
+ : current?.closed ? 'closed' : lifetimeSignal.aborted ? 'closing' : onnx ? 'ready' : 'unavailable',
148
+ models: current?.sessions ?? [], error: onnxRecoveryError ?? null
149
+ };
150
+ }
131
151
 
132
152
  function snapshot() {
133
153
  const records = [...engines.values()].map(function runtimeRecord(engine) { return {...engine.state, models: [...engine.models]}; });
154
+ if (onnxSelected) records.push(onnxStatus());
134
155
  const ollama = records.find(function ollamaRecord(record) { return record.id === 'ollama'; });
135
156
  return {
136
157
  runtimes: records,
@@ -145,6 +166,38 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
145
166
  return state;
146
167
  }
147
168
 
169
+ function startONNX() {
170
+ if (!onnxRecord?.modulePath) return;
171
+ onnx = createONNXRuntime({modulePath: onnxRecord.modulePath, signal: lifetimeSignal, onEvent});
172
+ stopONNXSubscription = onnx.subscribe(function onnxChanged() { publish(); });
173
+ }
174
+
175
+ function requireONNX() {
176
+ lifetimeSignal.throwIfAborted();
177
+ if (!onnxSelected) throw failure('LOCAL_AI_RUNTIME_NOT_SELECTED', 'ONNX is not selected in this application configuration.');
178
+ if (!onnx || onnx.current().closed || onnxRecovery || onnxRecoveryError) {
179
+ throw failure('LOCAL_AI_RUNTIME_UNAVAILABLE', 'The selected ONNX runtime is unavailable.');
180
+ }
181
+ return onnx;
182
+ }
183
+
184
+ function recoverONNX() {
185
+ if (onnxRecovery) return onnxRecovery;
186
+ onnxRecoveryError = undefined;
187
+ onnxRecovery = Promise.resolve().then(async function replaceONNXOwner() {
188
+ await onnx?.close();
189
+ stopONNXSubscription?.();
190
+ onnx = undefined;
191
+ lifetimeSignal.throwIfAborted();
192
+ startONNX();
193
+ });
194
+ function recovered() { onnxRecovery = undefined; if (!closed) publish(); }
195
+ function recoveryFailed(error) { onnxRecoveryError = serializeCoreError(error); recovered(); }
196
+ onnxRecovery.then(recovered, recoveryFailed);
197
+ publish();
198
+ return onnxRecovery;
199
+ }
200
+
148
201
  function recordFailure(engine, error) {
149
202
  if (!closed && error.code === 'LOCAL_AI_LOADING') {
150
203
  engine.state = {...engine.state, available: false, state: 'loading', error: null};
@@ -443,11 +496,12 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
443
496
  start(currentContext) {
444
497
  context = currentContext;
445
498
  for (const requirement of config.runtimes) {
446
- if (!['llama.cpp', 'ollama'].includes(requirement.id)) {
499
+ if (!['llama.cpp', 'ollama', 'onnx'].includes(requirement.id)) {
447
500
  throw failure('LOCAL_AI_RUNTIME_UNAVAILABLE', `Core runtime ${requirement.id} is not available in this service.`);
448
501
  }
449
502
  }
450
503
  for (const requirement of config.runtimes) {
504
+ if (requirement.id === 'onnx') { if (!onnx) startONNX(); continue; }
451
505
  if (engines.has(requirement.id)) continue;
452
506
  const engine = {
453
507
  id: requirement.id, models: [], revision: 0, modelChanges: new Map(), loads: new Map(), loadControllers: new Map(),
@@ -459,18 +513,38 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
459
513
  publish();
460
514
  },
461
515
  methods: {
462
- 'localai.services.recover': async function recoverServices({runtimes: selected = [...engines.keys()]} = {}, request) {
516
+ 'localai.services.recover': async function recoverServices({runtimes: selected = [...engines.keys(), ...(onnxSelected ? ['onnx'] : [])]} = {}, request) {
517
+ request.signal.throwIfAborted();
463
518
  const selectedEngines = selected.map(function selectedEngine(id) {
519
+ if (id === 'onnx' && onnxSelected) return {id};
464
520
  const engine = engines.get(id);
465
521
  if (!engine) throw failure('LOCAL_AI_RUNTIME_NOT_SELECTED', `${id} is not selected in this application's localAI configuration.`);
466
522
  return engine;
467
523
  });
468
524
  await Promise.all(selectedEngines.map(async function recoverEngine(engine) {
525
+ if (engine.id === 'onnx') {
526
+ await awaitReadiness(recoverONNX(), request.signal);
527
+ return;
528
+ }
469
529
  await awaitReadiness(restartEngine(engine), request.signal);
470
530
  await awaitReadiness(engine.ready, request.signal);
471
531
  }));
472
532
  return snapshot();
473
533
  },
534
+ 'onnx.status': function currentONNX() { return onnxStatus(); },
535
+ 'onnx.load': function loadONNX({id, model, sessionOptions}, request) {
536
+ return requireONNX().load({id, model: path.resolve(appRoot ?? process.cwd(), model), sessionOptions,
537
+ signal: AbortSignal.any([lifetimeSignal, request.signal])});
538
+ },
539
+ 'onnx.run': async function runONNX({id, feeds, fetches, runOptions}, request) {
540
+ const outputs = await requireONNX().run({id, feeds: decodeTensorMap(feeds),
541
+ fetches: decodeTensorFetches(fetches),
542
+ runOptions, signal: AbortSignal.any([lifetimeSignal, request.signal])});
543
+ return encodeTensorMap(outputs);
544
+ },
545
+ 'onnx.unload': function unloadONNX({id}, request) {
546
+ return requireONNX().unload({id, signal: AbortSignal.any([lifetimeSignal, request.signal])});
547
+ },
474
548
  'localai.status': async function currentStatus(_parameters, request) {
475
549
  await Promise.all([...engines.values()].map(async function refreshEngine(engine) {
476
550
  if (!engine.state.available) return;
@@ -528,8 +602,12 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
528
602
  async dispose() {
529
603
  closed = true;
530
604
  lifetime.abort();
531
- const results = await Promise.allSettled([...engines.values()].map(function stopEngine(engine) { return engine.server.close(); }));
532
- await Promise.allSettled([...jobs, ...[...engines.values()].flatMap(function pendingWork(engine) {
605
+ const results = await Promise.allSettled([
606
+ ...[...engines.values()].map(function stopEngine(engine) { return engine.server.close(); }),
607
+ ...(onnx ? [onnx.close()] : [])
608
+ ]);
609
+ stopONNXSubscription?.();
610
+ await Promise.allSettled([...jobs, ...(onnxRecovery ? [onnxRecovery] : []), ...[...engines.values()].flatMap(function pendingWork(engine) {
533
611
  return [...engine.loads.values(), ...(engine.recovering ? [engine.recovering] : [])];
534
612
  })]);
535
613
  events.dispose();