arcane-os 0.66.0 → 0.68.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 (38) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/core-image.mjs +84 -34
  4. package/browser-runtime/ai/core-local.mjs +195 -43
  5. package/browser-runtime/ai/core-onnx.mjs +71 -12
  6. package/docs/reference/README.md +6 -0
  7. package/docs/reference/availability-and-normalization.md +1 -0
  8. package/docs/reference/core-native-packaging.md +11 -4
  9. package/docs/reference/core-repositories.md +93 -9
  10. package/docs/reference/core-runtime.md +46 -5
  11. package/docs/reference/inventory/package-api.json +62 -20
  12. package/docs/reference/inventory/runtime-components.json +2 -1
  13. package/docs/reference/inventory/runtime-modules.json +3 -3
  14. package/docs/reference/local-ai.md +112 -2
  15. package/docs/reference/local-image-generation.md +15 -4
  16. package/docs/reference/model-assets.md +50 -3
  17. package/docs/reference/native-decisions.md +181 -0
  18. package/docs/reference/runtime-components.md +62 -5
  19. package/docs/reference/runtime-modules.md +44 -7
  20. package/docs/reference/sdk-api.md +229 -19
  21. package/package.json +5 -2
  22. package/runtime/arcane/components/theme-switcher.html +112 -13
  23. package/runtime/arcane/modules/PreferenceStore.js +18 -13
  24. package/runtime/arcane/modules/SystemAppearance.js +24 -6
  25. package/src/core/repositories.mjs +27 -8
  26. package/src/core/runtime.mjs +26 -1
  27. package/src/core/services/decisions.mjs +144 -0
  28. package/src/core/services/local-ai.mjs +31 -9
  29. package/src/core/services/model-assets.mjs +98 -13
  30. package/src/git-identity.mjs +22 -58
  31. package/src/local-ai/decision-tokenizer-worker.mjs +31 -0
  32. package/src/local-ai/decision-tokenizer.mjs +109 -0
  33. package/src/local-ai/decisions.mjs +273 -0
  34. package/src/local-ai/dev.mjs +8 -2
  35. package/src/local-ai/onnx-worker.mjs +67 -7
  36. package/src/local-ai/onnx.mjs +23 -12
  37. package/src/native/portable-layout.mjs +8 -3
  38. package/src/repository.mjs +144 -4
@@ -95,7 +95,7 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
95
95
 
96
96
  function current(){
97
97
  return {sessions:[...sessions.values()].map(function sessionState(entry){
98
- return structuredClone(entry.state);
98
+ return {...structuredClone(entry.state), stopping:entry.stopping, exited:entry.exited};
99
99
  }),closed};
100
100
  }
101
101
 
@@ -157,20 +157,31 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
157
157
  entry.stopping=true;
158
158
  if(failed)entry.terminalError??=reason;
159
159
  entry.stopTask=Promise.resolve().then(async function releaseSession(){
160
- if(active||failed||entry.terminalError){
161
- entry.terminationRequested=true;
162
- await worker.terminate();
163
- }else{
164
- try{worker.postMessage({operation:'unload',requestId:++nextRequestId});}
165
- catch(error){
166
- entry.releaseError=error;
160
+ let terminationError;
161
+ try{
162
+ if(active||failed||entry.terminalError){
167
163
  entry.terminationRequested=true;
168
164
  await worker.terminate();
165
+ }else{
166
+ try{worker.postMessage({operation:'unload',requestId:++nextRequestId});}
167
+ catch(error){
168
+ entry.releaseError=error;
169
+ entry.terminationRequested=true;
170
+ await worker.terminate();
171
+ }
169
172
  }
173
+ }catch(error){
174
+ terminationError=error;
175
+ reportBackgroundError(error);
170
176
  }
177
+ // Failed termination is not actual worker exit. Keep native file
178
+ // ownership until exit and complete output delivery have settled.
171
179
  await entry.exit;
172
- await Promise.all(entry.outputTasks);
173
- if(entry.releaseError)throw entry.releaseError;
180
+ const outputResults=await Promise.allSettled(entry.outputTasks);
181
+ const failures=[entry.releaseError,terminationError].filter(function present(error){return error!==null&&error!==undefined;});
182
+ for(const result of outputResults)if(result.status==='rejected')failures.push(result.reason);
183
+ if(failures.length>1)throw new AggregateError(failures,'Releasing the ONNX worker failed.');
184
+ if(failures.length)throw failures[0];
174
185
  return structuredClone(entry.state);
175
186
  });
176
187
  // A native call has no AbortSignal. Stop delivery immediately, retain
@@ -262,7 +273,7 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
262
273
  finally{entry.resolveExit(code);}
263
274
  }
264
275
 
265
- async function load({id,model,sessionOptions,signal:operationSignal}={}){
276
+ async function load({id,model,sessionOptions,executionPreference,signal:operationSignal}={}){
266
277
  assertOpen(operationSignal);
267
278
  if(!is.string(id)||!is.string(model))throw new TypeError('ONNX load requires a session id and model path.');
268
279
  const previous=sessions.get(id);
@@ -312,7 +323,7 @@ export function createONNXRuntime({modulePath,onEvent,signal}={}){
312
323
  }
313
324
  // Queue load before publishing readiness so a synchronous subscriber
314
325
  // can enqueue inference or request release without racing ownership.
315
- const task=enqueue(entry,'load',{model,sessionOptions},operationSignal,true);
326
+ const task=enqueue(entry,'load',{model,sessionOptions,executionPreference},operationSignal,true);
316
327
  publish(entry);
317
328
  dispatchNext(entry);
318
329
  return task;
@@ -153,6 +153,9 @@ export function coreEntrySource(application, version, services, {localAI, runtim
153
153
  const imageSelected = localAI?.runtimes?.some(function selectedImage(requirement) {
154
154
  return (typeof requirement === 'string' ? requirement : requirement.id) === 'stable-diffusion.cpp';
155
155
  }) ?? false;
156
+ const modelAssetsSelected = imageSelected || (localAI?.runtimes?.some(function selectedONNX(requirement) {
157
+ return (typeof requirement === 'string' ? requirement : requirement.id) === 'onnx';
158
+ }) ?? false);
156
159
  const whisperSelected = localAI?.runtimes?.some(function selectedWhisper(requirement) {
157
160
  return (typeof requirement === 'string' ? requirement : requirement.id) === 'whisper.cpp';
158
161
  }) ?? false;
@@ -175,10 +178,12 @@ export function coreEntrySource(application, version, services, {localAI, runtim
175
178
  imports.push("import {createLocalAIService} from 'arcane-os/core/local-ai';");
176
179
  definitions.push(` createLocalAIService(JSON.parse(${JSON.stringify(JSON.stringify(chatConfiguration))}), context)`);
177
180
  }
178
- if (imageSelected) {
179
- imports.push("import {createLocalImageService} from 'arcane-os/core/image';");
181
+ if (modelAssetsSelected) {
180
182
  imports.push("import {createModelAssetService} from 'arcane-os/core/model-assets';");
181
183
  definitions.push(' modelAssets');
184
+ }
185
+ if (imageSelected) {
186
+ imports.push("import {createLocalImageService} from 'arcane-os/core/image';");
182
187
  definitions.push(` createLocalImageService(JSON.parse(${JSON.stringify(JSON.stringify(localAI))}), {...context, modelAssets})`);
183
188
  }
184
189
  if (whisperSelected) {
@@ -239,7 +244,7 @@ export function coreEntrySource(application, version, services, {localAI, runtim
239
244
  ' ...await readCoreLaunchContext()',
240
245
  '};',
241
246
  '',
242
- ...(imageSelected ? ['const modelAssets = createModelAssetService({appRoot: context.appRoot});', ''] : []),
247
+ ...(modelAssetsSelected ? ['const modelAssets = createModelAssetService({appRoot: context.appRoot});', ''] : []),
243
248
  ...(whisperSelected ? [
244
249
  'const stt = createWhisperRuntime(',
245
250
  ' {',
@@ -1,10 +1,142 @@
1
1
  import {runProcess} from './process.mjs';
2
2
  import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
3
+ import path from 'node:path';
4
+ import Is from 'strong-type';
5
+
6
+ const is = new Is(false);
3
7
 
4
8
  async function git(args,{run=runProcess,...options}){
5
9
  return run('git',args,options);
6
10
  }
7
11
 
12
+ /** Private shared reader for Git text protocols, retaining native diagnostics. */
13
+ export async function readGitText(args, {
14
+ cwd, signal, onEvent, run = runProcess, allowNonzero = false,
15
+ notTextCode = 'ARCANE_GIT_CONFIGURATION_NOT_TEXT', label = 'Git configuration'
16
+ } = {}) {
17
+ const chunks = [];
18
+ try {
19
+ const result = await run('git', args, {
20
+ cwd, signal, onEvent, allowNonzero,
21
+ outputEncoding: {stdout: null}, captureOutput: {stdout: false}, emitOutputEvents: {stdout: false},
22
+ onOutput: function observeOutput({stream, chunk}) {
23
+ if (stream === 'stdout') chunks.push(chunk);
24
+ }
25
+ });
26
+ try { throwIfAborted(signal); }
27
+ catch (error) { error.details = result; throw error; }
28
+ const rawStdout = Buffer.concat(chunks);
29
+ let output = null;
30
+ if (result.code === 0) {
31
+ try { output = new TextDecoder('utf-8', {fatal: true, ignoreBOM: true}).decode(rawStdout); }
32
+ catch (cause) {
33
+ throw new ArcaneError(notTextCode, `${label} cannot be represented as UTF-8 text.`, {cause, details: result});
34
+ }
35
+ }
36
+ return {result, output, rawStdout};
37
+ } catch (error) {
38
+ const rawStdout = Buffer.concat(chunks);
39
+ let attached = false;
40
+ let attachmentError;
41
+ try {
42
+ if (error !== null && (is.object(error) || is.function(error)) && !('rawStdout' in error)) {
43
+ Object.defineProperty(error, 'rawStdout', {
44
+ value: rawStdout, writable: true, enumerable: true, configurable: true
45
+ });
46
+ attached = true;
47
+ }
48
+ } catch (cause) { attachmentError = cause; }
49
+ if (attached) throw error;
50
+ const failure = new ArcaneError(error?.code ?? ERROR_CODES.operationFailed,
51
+ error instanceof Error ? error.message : String(error),
52
+ {cause: error, details: error?.details, exitCode: error?.exitCode});
53
+ failure.rawStdout = rawStdout;
54
+ if (attachmentError) failure.attachmentError = attachmentError;
55
+ throw failure;
56
+ }
57
+ }
58
+
59
+ /** Capture a caller-selected operation target without changing its strings. */
60
+ export function captureRepositoryTarget(target) {
61
+ if (target === undefined) return undefined;
62
+ const {remote, ref} = target;
63
+ for (const [name, value] of Object.entries({remote, ref})) {
64
+ if (!is.string(value) || value === '' || !value.isWellFormed() || value.includes('\0')) {
65
+ throw new TypeError(`target.${name} must be nonempty text representable as a native process argument.`);
66
+ }
67
+ }
68
+ return {remote, ref};
69
+ }
70
+
71
+ export function repositoryPushArguments(target) {
72
+ return target === undefined ? ['push'] : ['push', '--no-follow-tags', '--', target.remote, `HEAD:${target.ref}`];
73
+ }
74
+
75
+ /** Observe an existing workspace without initializing or refreshing it. */
76
+ export async function repositoryConfiguration({workspaceRoot = process.cwd(), signal, onEvent, run = runProcess} = {}) {
77
+ const repositoryRoot = path.resolve(workspaceRoot);
78
+ const options = {cwd: repositoryRoot, signal, onEvent, run, allowNonzero: true};
79
+ throwIfAborted(signal);
80
+
81
+ function processFailure(observation, message) {
82
+ const failure = new ArcaneError(ERROR_CODES.operationFailed, message, {details: observation.result});
83
+ failure.rawStdout = observation.rawStdout;
84
+ return failure;
85
+ }
86
+
87
+ async function configuration(pattern) {
88
+ const observation = await readGitText(['config', '--includes', '--null', '--get-regexp', pattern], options);
89
+ const values = new Map();
90
+ if (observation.result.code === 1) return values;
91
+ if (observation.result.code !== 0) throw processFailure(observation, 'Git configuration observation failed.');
92
+ for (const record of observation.output.split('\0')) {
93
+ if (record === '') continue;
94
+ const separator = record.indexOf('\n');
95
+ const key = separator === -1 ? record : record.slice(0, separator);
96
+ const value = separator === -1 ? '' : record.slice(separator + 1);
97
+ const entries = values.get(key) ?? [];
98
+ entries.push(value);
99
+ values.set(key, entries);
100
+ }
101
+ return values;
102
+ }
103
+
104
+ function literalPattern(value) {
105
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
106
+ }
107
+
108
+ const root = await readGitText(['rev-parse', '--is-inside-work-tree', '--show-prefix'], options);
109
+ if (root.result.code !== 0) throw processFailure(root, 'Git working repository observation failed.');
110
+ if (root.output !== 'true\n\n' && root.output !== 'true\r\n\r\n') {
111
+ const failure = new ArcaneError('ARCANE_REPOSITORY_DIRECTORY_INVALID',
112
+ 'The selected directory is not a Git working repository root.', {details: root.result});
113
+ failure.rawStdout = root.rawStdout;
114
+ throw failure;
115
+ }
116
+ const head = await readGitText(['symbolic-ref', '--quiet', 'HEAD'], options);
117
+ if (head.result.code !== 0 && head.result.code !== 1) throw processFailure(head, 'Git symbolic HEAD observation failed.');
118
+ const headRef = head.result.code === 1 ? null : head.output.replace(/\r?\n$/u, '');
119
+ let upstream = null;
120
+ let remoteName;
121
+ if (headRef?.startsWith('refs/heads/')) {
122
+ const key = `branch.${headRef.slice('refs/heads/'.length)}`;
123
+ const branch = await configuration(`^${literalPattern(key)}[.](remote|merge)$`);
124
+ const remoteNames = branch.get(`${key}.remote`) ?? [];
125
+ remoteName = remoteNames.at(-1);
126
+ upstream = {remoteNames, mergeRefs: branch.get(`${key}.merge`) ?? [], urls: [], pushUrls: []};
127
+ }
128
+ const selectedNames = ['origin'];
129
+ if (remoteName !== undefined && remoteName !== '.' && remoteName !== 'origin') selectedNames.push(remoteName);
130
+ const names = selectedNames.map(literalPattern).join('|');
131
+ const remotes = await configuration(`^remote[.](${names})[.](url|pushurl)$`);
132
+ const origin = {urls: remotes.get('remote.origin.url') ?? [], pushUrls: remotes.get('remote.origin.pushurl') ?? []};
133
+ if (upstream && remoteName !== undefined && remoteName !== '.') {
134
+ upstream.urls = remotes.get(`remote.${remoteName}.url`) ?? [];
135
+ upstream.pushUrls = remotes.get(`remote.${remoteName}.pushurl`) ?? [];
136
+ }
137
+ return {repositoryRoot, headRef, origin, upstream};
138
+ }
139
+
8
140
  export async function repositoryStatus({
9
141
  workspaceRoot=process.cwd(),
10
142
  signal,
@@ -28,11 +160,13 @@ export async function repositoryStatus({
28
160
 
29
161
  export async function repositoryPull({
30
162
  workspaceRoot=process.cwd(),
163
+ target,
31
164
  signal,
32
165
  onEvent,
33
166
  run=runProcess
34
167
  }={}){
35
168
  throwIfAborted(signal);
169
+ const selectedTarget=captureRepositoryTarget(target);
36
170
  const before=await repositoryStatus({workspaceRoot,signal,onEvent,run});
37
171
  if(!before.clean){
38
172
  throw new ArcaneError(
@@ -41,22 +175,27 @@ export async function repositoryPull({
41
175
  {details:before}
42
176
  );
43
177
  }
44
- const result=await git(['pull','--ff-only'],{cwd:workspaceRoot,signal,onEvent,run});
178
+ const args=selectedTarget===undefined?['pull','--ff-only']
179
+ :['pull','--ff-only','--',selectedTarget.remote,`${selectedTarget.ref}:`];
180
+ const result=await git(args,{cwd:workspaceRoot,signal,onEvent,run});
45
181
  return {
46
182
  action:'pull',
47
183
  repositoryRoot:before.repositoryRoot,
48
184
  branch:before.branch,
49
- output:result.stdout.trim()
185
+ output:result.stdout.trim(),
186
+ ...(selectedTarget===undefined?{}:{target:selectedTarget,stdout:result.stdout,stderr:result.stderr})
50
187
  };
51
188
  }
52
189
 
53
190
  export async function repositoryPush({
54
191
  workspaceRoot=process.cwd(),
192
+ target,
55
193
  signal,
56
194
  onEvent,
57
195
  run=runProcess
58
196
  }={}){
59
197
  throwIfAborted(signal);
198
+ const selectedTarget=captureRepositoryTarget(target);
60
199
  const before=await repositoryStatus({workspaceRoot,signal,onEvent,run});
61
200
  if(!before.branch){
62
201
  throw new ArcaneError(
@@ -64,12 +203,13 @@ export async function repositoryPush({
64
203
  'Refusing to push from a detached HEAD.'
65
204
  );
66
205
  }
67
- const result=await git(['push'],{cwd:workspaceRoot,signal,onEvent,run});
206
+ const result=await git(repositoryPushArguments(selectedTarget),{cwd:workspaceRoot,signal,onEvent,run});
68
207
  return {
69
208
  action:'push',
70
209
  repositoryRoot:before.repositoryRoot,
71
210
  branch:before.branch,
72
- output:(result.stdout||result.stderr).trim()
211
+ output:(result.stdout||result.stderr).trim(),
212
+ ...(selectedTarget===undefined?{}:{target:selectedTarget,stdout:result.stdout,stderr:result.stderr})
73
213
  };
74
214
  }
75
215