arcane-os 0.65.0 → 0.67.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 (40) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/ai/browser-decisions.mjs +3 -2
  4. package/browser-runtime/ai/core-image.mjs +84 -34
  5. package/browser-runtime/ai/core-local.mjs +195 -43
  6. package/browser-runtime/ai/core-onnx.mjs +71 -12
  7. package/browser-runtime/ai/decision-runtime.mjs +14 -24
  8. package/docs/publishing.md +39 -0
  9. package/docs/reference/README.md +8 -1
  10. package/docs/reference/ai/browser-decisions.md +7 -4
  11. package/docs/reference/core-native-packaging.md +11 -4
  12. package/docs/reference/core-repositories.md +214 -11
  13. package/docs/reference/core-runtime.md +32 -5
  14. package/docs/reference/git-text-snapshot.md +15 -1
  15. package/docs/reference/inventory/package-api.json +41 -13
  16. package/docs/reference/inventory/runtime-modules.json +5 -4
  17. package/docs/reference/local-ai.md +119 -2
  18. package/docs/reference/local-image-generation.md +15 -4
  19. package/docs/reference/local-whisper.md +7 -1
  20. package/docs/reference/mcp-stdio.md +1 -1
  21. package/docs/reference/model-assets.md +50 -3
  22. package/docs/reference/native-decisions.md +181 -0
  23. package/docs/reference/native-speech.md +6 -0
  24. package/docs/reference/sdk-api.md +151 -16
  25. package/package.json +5 -2
  26. package/src/core/repositories.mjs +37 -14
  27. package/src/core/runtime.mjs +11 -0
  28. package/src/core/services/decisions.mjs +144 -0
  29. package/src/core/services/local-ai.mjs +31 -9
  30. package/src/core/services/model-assets.mjs +98 -13
  31. package/src/git-identity.mjs +96 -0
  32. package/src/git-text-snapshot.mjs +5 -3
  33. package/src/local-ai/decision-tokenizer-worker.mjs +31 -0
  34. package/src/local-ai/decision-tokenizer.mjs +109 -0
  35. package/src/local-ai/decisions.mjs +273 -0
  36. package/src/local-ai/dev.mjs +8 -2
  37. package/src/local-ai/onnx-worker.mjs +67 -7
  38. package/src/local-ai/onnx.mjs +23 -12
  39. package/src/native/portable-layout.mjs +8 -3
  40. package/src/repository.mjs +144 -4
@@ -0,0 +1,144 @@
1
+ import path from 'node:path';
2
+ import {createNativeDecisionModel} from '../../local-ai/decisions.mjs';
3
+ import {createArcaneEventSource} from '../../event-manager.mjs';
4
+ import {CoreError} from '../../../browser-runtime/core/contracts.mjs';
5
+ import {encodeTensorMap} from '../../../browser-runtime/ai/onnx-tensors.mjs';
6
+
7
+ /** App-owned selection; shared typed-decision execution, without a renderer. */
8
+ export function createNativeDecisionService(configuration = {}, {appRoot = process.cwd()} = {}) {
9
+ const events = createArcaneEventSource({}, {source: 'core-decisions', eventTypes: ['decisions.state']});
10
+ const lifetime = new AbortController();
11
+ let context;
12
+ let model;
13
+ let loading;
14
+ let loadController;
15
+ let releasing;
16
+ let closing;
17
+ let unsubscribe;
18
+
19
+ function current() {
20
+ return model?.current() ?? {
21
+ family: 'laya', model: configuration.model ?? 'onnx-community/laya-typed-decisions-ONNX',
22
+ revision: configuration.revision ?? 'main', dtype: configuration.dtype ?? 'fp32',
23
+ state: closing ? 'disposed' : 'unloaded', loaded: false, busy: Boolean(loading),
24
+ activeRequests: 0, progress: null, error: null, execution: null
25
+ };
26
+ }
27
+
28
+ function publish(value = current()) {
29
+ events.dispatch('decisions.state', value);
30
+ context?.emit('decisions.state', value);
31
+ }
32
+
33
+ function subscribe(listener, {emitCurrent = true, signal} = {}) {
34
+ const stop = events.on('decisions.state', function changed(event) { listener(event.detail); }, {signal});
35
+ try { if (emitCurrent && !signal?.aborted) listener(current()); }
36
+ catch (error) { stop(); throw error; }
37
+ return stop;
38
+ }
39
+
40
+ function operationSignal(signal) { return signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal; }
41
+
42
+ function load({signal} = {}) {
43
+ operationSignal(signal).throwIfAborted();
44
+ if (!context) throw new CoreError({code: 'CORE_NOT_READY', message: 'Register and start the decision service with Core before loading.'});
45
+ if (loading) return observeLoad(loading, loadController, signal);
46
+ if (model?.current().loaded) return Promise.resolve(model.current());
47
+ loadController = new AbortController();
48
+ const controller = loadController;
49
+ const selectedSignal = AbortSignal.any([lifetime.signal, controller.signal]);
50
+ const precedingRelease = releasing;
51
+ loading = Promise.resolve().then(async function loadSelectedModel() {
52
+ selectedSignal.throwIfAborted();
53
+ if (precedingRelease) await precedingRelease;
54
+ selectedSignal.throwIfAborted();
55
+ if (model) await model.dispose();
56
+ unsubscribe?.();
57
+ // Only the explicit load waits for its native dependencies. Merely
58
+ // registering this service does not download a model or delay UI.
59
+ const [localAI, modelAssets] = await Promise.all([
60
+ context.getService('local-ai'),
61
+ configuration.paths ? undefined : context.getService('model-assets')
62
+ ]);
63
+ selectedSignal.throwIfAborted();
64
+ const paths = configuration.paths && Object.fromEntries(
65
+ Object.entries(configuration.paths).map(function nativePath([name, value]) { return [name, path.resolve(appRoot, value)]; })
66
+ );
67
+ model = createNativeDecisionModel({
68
+ ...configuration, paths, onnx: localAI.getONNXRuntime(), modelAssets,
69
+ signal: lifetime.signal
70
+ });
71
+ unsubscribe = model.subscribe(publish);
72
+ return model.load({signal: selectedSignal});
73
+ });
74
+ function settled() { loading = undefined; loadController = undefined; publish(); }
75
+ loading.then(settled, settled);
76
+ publish();
77
+ return observeLoad(loading, controller, signal);
78
+ }
79
+
80
+ function observeLoad(task, controller, signal) {
81
+ function abort() { controller.abort(signal.reason); }
82
+ signal?.addEventListener('abort', abort, {once: true});
83
+ if (signal?.aborted) abort();
84
+ return task.finally(function detachLoadCaller() { signal?.removeEventListener('abort', abort); });
85
+ }
86
+
87
+ async function evaluate(rows, {signal, runOptions} = {}) {
88
+ const selectedSignal = operationSignal(signal);
89
+ selectedSignal.throwIfAborted();
90
+ if (!model) throw new CoreError({code: 'ARCANE_DECISION_NOT_LOADED', message: 'Load the selected native decision model before evaluating.'});
91
+ return model.evaluate(rows, {signal: selectedSignal, runOptions});
92
+ }
93
+
94
+ function unload() {
95
+ if (releasing) return releasing;
96
+ // An unload during model preparation must cancel that accepted load,
97
+ // rather than wait for it to download and activate first.
98
+ loadController?.abort(new CoreError({name: 'AbortError', code: 'ARCANE_AI_REQUEST_ABORTED', message: 'The native decision load was cancelled.'}));
99
+ const task = model?.unload() ?? Promise.resolve();
100
+ const acceptedLoad = loading;
101
+ releasing = Promise.resolve().then(async function releaseSelectedModel() {
102
+ const results = await Promise.allSettled([task, acceptedLoad]);
103
+ if (results[0].status === 'rejected') throw results[0].reason;
104
+ return current();
105
+ });
106
+ function released() { releasing = undefined; }
107
+ releasing.then(released, released);
108
+ return releasing;
109
+ }
110
+
111
+ function dispose() {
112
+ if (closing) return closing;
113
+ const reason = new CoreError({name: 'AbortError', code: 'ARCANE_AI_REQUEST_ABORTED', message: 'The native decision service is closing.'});
114
+ closing = Promise.resolve().then(async function closeDecisionService() {
115
+ const failures = [];
116
+ try { await loading; } catch (error) { if (!lifetime.signal.aborted) failures.push(error); }
117
+ try { await model?.dispose(); } catch (error) { failures.push(error); }
118
+ unsubscribe?.();
119
+ publish();
120
+ events.dispose();
121
+ if (failures.length) throw new AggregateError(failures, 'Closing the decision service failed.');
122
+ });
123
+ lifetime.abort(reason);
124
+ return closing;
125
+ }
126
+
127
+ return {
128
+ name: configuration.name ?? 'decisions', current, subscribe, load, evaluate, classify: evaluate, unload, dispose,
129
+ start(serviceContext) { context = serviceContext; },
130
+ methods: {
131
+ 'decisions.status': current,
132
+ 'decisions.load': function loadRequest(parameters, request) { return load({signal: request.signal}); },
133
+ 'decisions.evaluate': async function evaluateRequest({rows, runOptions}, request) {
134
+ const result = await evaluate(rows, {runOptions, signal: request.signal});
135
+ // Typed arrays retain their complete values through Core's
136
+ // existing JSON tensor encoding, only at this transport edge.
137
+ return {...result, outputs: encodeTensorMap(result.outputs)};
138
+ },
139
+ 'decisions.unload': {lifetime: 'service', handle: unload}
140
+ }
141
+ };
142
+ }
143
+
144
+ export default createNativeDecisionService;
@@ -149,8 +149,13 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
149
149
  };
150
150
  }
151
151
 
152
+ function runtimeRecord(engine) {
153
+ return {...engine.state, models: [...engine.models],
154
+ ...(engine.id === 'llama.cpp' ? {released: engine.released === true} : {})};
155
+ }
156
+
152
157
  function snapshot() {
153
- const records = [...engines.values()].map(function runtimeRecord(engine) { return {...engine.state, models: [...engine.models]}; });
158
+ const records = [...engines.values()].map(runtimeRecord);
154
159
  if (onnxSelected) records.push(onnxStatus());
155
160
  const ollama = records.find(function ollamaRecord(record) { return record.id === 'ollama'; });
156
161
  return {
@@ -283,7 +288,7 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
283
288
  engine.models = [...merged.values()];
284
289
  }
285
290
  publish();
286
- return {...engine.state, models: [...engine.models]};
291
+ return runtimeRecord(engine);
287
292
  }
288
293
 
289
294
  function observeModels(engine) {
@@ -347,7 +352,7 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
347
352
  return recovering;
348
353
  }
349
354
 
350
- async function engineReady(id, signal, {retryLoading = false} = {}) {
355
+ async function engineReady(id, signal, {retryLoading = false, resumeReleased = true} = {}) {
351
356
  signal?.throwIfAborted();
352
357
  const engine = engines.get(id);
353
358
  if (!engine) throw failure('LOCAL_AI_RUNTIME_NOT_SELECTED', `${id} is not selected in this application's localAI configuration.`);
@@ -358,13 +363,28 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
358
363
  if (retryLoading && engine.state.state === 'loading') {
359
364
  await awaitReadiness(restartEngine(engine), signal);
360
365
  }
361
- if (engine.released) { engine.released = false; startEngine(engine); }
366
+ if (engine.released) {
367
+ if (!resumeReleased) return engine;
368
+ engine.released = false;
369
+ startEngine(engine);
370
+ }
362
371
  await awaitReadiness(engine.ready, signal);
363
372
  signal?.throwIfAborted();
364
373
  if (!engine.state.available) throw failure('LOCAL_AI_RUNTIME_UNAVAILABLE', `${id} is unavailable.`);
365
374
  return engine;
366
375
  }
367
376
 
377
+ function observeLlama(signal) {
378
+ signal?.throwIfAborted();
379
+ const engine = engines.get('llama.cpp');
380
+ if (!engine) throw failure('LOCAL_AI_RUNTIME_NOT_SELECTED', "llama.cpp is not selected in this application's localAI configuration.");
381
+ // Observation neither waits for startup nor starts a released engine.
382
+ if (!engine.state.available || engine.released || engine.stopping || engine.recovering || engine.signal.aborted) {
383
+ return runtimeRecord(engine);
384
+ }
385
+ return refreshModels(engine, signal);
386
+ }
387
+
368
388
  function waitForModel(engine, model, signal) {
369
389
  return new Promise(function awaitModel(resolve, reject) {
370
390
  let unsubscribe;
@@ -563,6 +583,9 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
563
583
  const service = {
564
584
  name: 'local-ai',
565
585
  current: snapshot,
586
+ getONNXRuntime() {
587
+ return requireONNX();
588
+ },
566
589
  async start(currentContext) {
567
590
  context = currentContext;
568
591
  if (prepare) {
@@ -633,17 +656,16 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
633
656
  return snapshot();
634
657
  },
635
658
  'llama.status': async function llamaStatus(_parameters, request) {
636
- const engine = await engineReady('llama.cpp', request.signal);
637
- return refreshModels(engine, request.signal);
659
+ return observeLlama(request.signal);
638
660
  },
639
661
  'llama.models': async function llamaModels(_parameters, request) {
640
- const engine = await engineReady('llama.cpp', request.signal);
641
- const status = await refreshModels(engine, request.signal);
662
+ const status = await observeLlama(request.signal);
642
663
  return {models: status.models};
643
664
  },
644
665
  'llama.load': loadLlama,
645
666
  'llama.unload': async function unloadLlama({model}, request) {
646
- const engine = await engineReady('llama.cpp', request.signal);
667
+ const engine = await engineReady('llama.cpp', request.signal, {resumeReleased: false});
668
+ if (engine.released) return {...engine.state, models: [...engine.models], released: true, unloaded: true};
647
669
  engine.loadControllers.get(model)?.abort(failure('LOCAL_AI_MODEL_UNLOADED', `The application unloaded ${model}.`));
648
670
  if (engine.loads.has(model)) await Promise.allSettled([engine.loads.get(model)]);
649
671
  if (!engine.router) {
@@ -10,7 +10,7 @@ function failure(code, message) {
10
10
  return new CoreError({code, message});
11
11
  }
12
12
 
13
- /** Working files for native engines. The browser's stored assets stay authoritative. */
13
+ /** Working files for native engines. Caller-selected source assets stay authoritative. */
14
14
  export function createModelAssetService({appRoot = process.cwd()} = {}) {
15
15
  const projections = new Map();
16
16
  let context;
@@ -63,6 +63,7 @@ export function createModelAssetService({appRoot = process.cwd()} = {}) {
63
63
  const errors = [];
64
64
  let filesClosed = false;
65
65
  try { await record.opening; } catch (error) { record.error ??= error; }
66
+ try { await record.downloading; } catch (error) { record.error ??= error; }
66
67
  try { await record.finishing; } catch (error) { record.error ??= error; }
67
68
  try { await closeMembers(record); filesClosed = true; } catch (error) { errors.push(error); }
68
69
  if (record.directory && filesClosed) {
@@ -87,10 +88,16 @@ export function createModelAssetService({appRoot = process.cwd()} = {}) {
87
88
 
88
89
  function releasePreparation(record) {
89
90
  record.preparationOwned = false;
91
+ record.downloadController?.abort(failure('MODEL_ASSET_PROJECTION_RELEASED', 'Model asset preparation was released.'));
90
92
  publish(record);
91
93
  return cleanup(record);
92
94
  }
93
95
 
96
+ function release(id) {
97
+ const record = projections.get(id);
98
+ return record ? releasePreparation(record) : Promise.resolve({id, state: 'released'});
99
+ }
100
+
94
101
  async function failedOperation(record, error) {
95
102
  record.error = error;
96
103
  record.state = 'error';
@@ -165,19 +172,98 @@ export function createModelAssetService({appRoot = process.cwd()} = {}) {
165
172
  const member = record.members[memberIndex];
166
173
  if (!member) throw new TypeError('Select an existing model asset member.');
167
174
  try {
168
- const task = member.tail.then(async function appendOriginalContent() {
169
- request.signal.throwIfAborted();
170
- if (!record.preparationOwned) throw failure('MODEL_ASSET_PROJECTION_RELEASED', 'Model asset preparation was released.');
171
- await member.handle.appendFile(Buffer.from(contentBase64, 'base64'));
172
- request.signal.throwIfAborted();
173
- });
174
- // File writes remain ordered; unrelated member files write independently.
175
- member.tail = task.catch(function observeWriteFailure() {});
176
- await task;
175
+ await appendMemberContent(record, member, Buffer.from(contentBase64, 'base64'), request.signal);
177
176
  return {id, memberIndex, written: true};
178
177
  } catch (error) { return failedOperation(record, error); }
179
178
  }
180
179
 
180
+ function appendMemberContent(record, member, content, signal) {
181
+ const task = member.tail.then(async function appendOriginalContent() {
182
+ signal.throwIfAborted();
183
+ if (!record.preparationOwned) throw failure('MODEL_ASSET_PROJECTION_RELEASED', 'Model asset preparation was released.');
184
+ await member.handle.appendFile(content);
185
+ signal.throwIfAborted();
186
+ });
187
+ // File writes remain ordered; unrelated member files write independently.
188
+ member.tail = task.catch(function observeWriteFailure() {});
189
+ return task;
190
+ }
191
+
192
+ async function prepare({id, workingDirectory, members, signal, onProgress} = {}) {
193
+ const controller = new AbortController();
194
+ const operationSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
195
+ const request = {signal: operationSignal};
196
+ let record;
197
+ let completed = 0;
198
+ let firstFailure;
199
+
200
+ async function progress(phase, memberIndex) {
201
+ operationSignal.throwIfAborted();
202
+ await onProgress?.({
203
+ phase, completed, total: members.length, unit: 'files',
204
+ ...(memberIndex === undefined ? {} : {memberIndex, path: members[memberIndex].path})
205
+ });
206
+ operationSignal.throwIfAborted();
207
+ }
208
+
209
+ async function downloadMember(member, memberIndex) {
210
+ try {
211
+ await progress('download', memberIndex);
212
+ const response = await fetch(member.url, {signal: operationSignal});
213
+ if (!response.ok) {
214
+ const error = failure('MODEL_ASSET_DOWNLOAD_FAILED', `Model asset request failed: ${response.status} ${response.statusText}`);
215
+ error.url = member.url;
216
+ error.status = response.status;
217
+ error.response = await response.text();
218
+ throw error;
219
+ }
220
+ if (!response.body) throw failure('MODEL_ASSET_DOWNLOAD_FAILED', `Model asset response has no content stream: ${member.url}`);
221
+ for await (const content of response.body) {
222
+ operationSignal.throwIfAborted();
223
+ await appendMemberContent(record, record.members[memberIndex], content, operationSignal);
224
+ await progress('download', memberIndex);
225
+ }
226
+ completed += 1;
227
+ await progress('download', memberIndex);
228
+ } catch (error) {
229
+ firstFailure ??= error;
230
+ controller.abort(error);
231
+ throw error;
232
+ }
233
+ }
234
+
235
+ try {
236
+ await openProjection({id, workingDirectory, members}, request);
237
+ record = requireProjection(id);
238
+ operationSignal.throwIfAborted();
239
+ if (!record.preparationOwned || closing) {
240
+ throw failure('MODEL_ASSET_PROJECTION_RELEASED', 'Model asset preparation was released.');
241
+ }
242
+ record.downloadController = controller;
243
+ // Cleanup joins this transfer-only task. Its rejection handler below
244
+ // may release preparation without waiting on itself.
245
+ record.downloading = Promise.resolve().then(async function downloadMembers() {
246
+ await progress('open');
247
+ const results = await Promise.allSettled(members.map(downloadMember));
248
+ const errors = results.filter(function rejected(result) { return result.status === 'rejected'; })
249
+ .map(function reason(result) { return result.reason; });
250
+ if (errors.length > 1) {
251
+ throw new AggregateError(errors, 'Downloading model asset members failed.', {cause: firstFailure});
252
+ }
253
+ if (errors.length) throw errors[0];
254
+ });
255
+ await record.downloading;
256
+ await progress('complete');
257
+ const projection = await completeProjection({id}, request);
258
+ await progress('ready');
259
+ return projection;
260
+ } catch (error) {
261
+ controller.abort(error);
262
+ if (record) return failedOperation(record, error);
263
+ throw error;
264
+ }
265
+ }
266
+
181
267
  async function completeProjection({id}, request) {
182
268
  const record = requireProjection(id);
183
269
  await record.opening;
@@ -245,7 +331,7 @@ export function createModelAssetService({appRoot = process.cwd()} = {}) {
245
331
  }
246
332
 
247
333
  return {
248
- name: 'model-assets', current, retain,
334
+ name: 'model-assets', current, prepare, retain, release,
249
335
  start(serviceContext) { context = serviceContext; },
250
336
  methods: {
251
337
  'modelAssets.open': openProjection,
@@ -255,8 +341,7 @@ export function createModelAssetService({appRoot = process.cwd()} = {}) {
255
341
  'modelAssets.release': {
256
342
  lifetime: 'service',
257
343
  handle({id}) {
258
- const record = projections.get(id);
259
- return record ? releasePreparation(record) : {id, state: 'released'};
344
+ return release(id);
260
345
  }
261
346
  }
262
347
  },
@@ -0,0 +1,96 @@
1
+ import path from 'node:path';
2
+ import Is from 'strong-type';
3
+ import {ArcaneError, throwIfAborted} from './errors.mjs';
4
+ import {runProcess} from './process.mjs';
5
+ import {createEventQueue} from './event-queue.mjs';
6
+ import {readGitText} from './repository.mjs';
7
+
8
+ const is = new Is(false);
9
+
10
+ /** Read only non-secret identity configuration through ordinary Git scopes. */
11
+ export async function readGitIdentity({directory, signal, onEvent, run = runProcess} = {}) {
12
+ const cwd = directory === undefined ? undefined : path.resolve(directory);
13
+ throwIfAborted(signal);
14
+ const events = createEventQueue(onEvent);
15
+
16
+ async function observeScope(scope) {
17
+ const args = ['config'];
18
+ if (scope !== 'effective') args.push(`--${scope}`);
19
+ args.push('--includes', '--null', '--get-regexp', '^(user[.](name|email)|github[.]user)$');
20
+ const {result, output, rawStdout} = await readGitText(args, {
21
+ cwd, signal, onEvent: events.send, run, allowNonzero: true,
22
+ notTextCode: 'ARCANE_GIT_IDENTITY_NOT_TEXT', label: `Git ${scope} identity configuration`
23
+ });
24
+ const identity = {name: null, email: null, githubUser: null};
25
+ if (result.code === 1) return identity;
26
+ if (result.code !== 0) {
27
+ const failure = new ArcaneError('ARCANE_OPERATION_FAILED',
28
+ `Git ${scope} identity lookup exited with code ${String(result.code)}.`, {details: result});
29
+ failure.rawStdout = rawStdout;
30
+ throw failure;
31
+ }
32
+ const fields = {'user.name': 'name', 'user.email': 'email', 'github.user': 'githubUser'};
33
+ // Git's NUL record framing preserves complete values, including newlines
34
+ // and empty strings. The last occurrence is the effective scalar value.
35
+ for (const record of output.split('\0')) {
36
+ if (record === '') continue;
37
+ const separator = record.indexOf('\n');
38
+ const key = separator === -1 ? record : record.slice(0, separator);
39
+ if (Object.hasOwn(fields, key)) {
40
+ identity[fields[key]] = separator === -1 ? '' : record.slice(separator + 1);
41
+ }
42
+ }
43
+ return identity;
44
+ }
45
+
46
+ const scopes = cwd === undefined ? ['global'] : ['global', 'local', 'effective'];
47
+ const observations = await Promise.allSettled(scopes.map(observeScope));
48
+ const failures = observations.filter(function failed(result) {
49
+ return result.status === 'rejected';
50
+ }).map(function cause(result) { return result.reason; });
51
+ try { await events.drain(); }
52
+ catch (error) {
53
+ const visited = new Set();
54
+ function containsObserverFailure(failure) {
55
+ if (failure === error) return true;
56
+ if (failure === null || failure === undefined || visited.has(failure)) return false;
57
+ visited.add(failure);
58
+ return containsObserverFailure(failure.cause)
59
+ || (is.array(failure.errors) && failure.errors.some(containsObserverFailure));
60
+ }
61
+ const represented = failures.some(containsObserverFailure);
62
+ if (!represented) failures.push(error);
63
+ }
64
+ if (failures.length === 1) throw failures[0];
65
+ if (failures.length) throw new AggregateError(failures, 'Git identity observations failed.');
66
+ const result = {global: null, local: null, effective: null};
67
+ for (const [index, scope] of scopes.entries()) result[scope] = observations[index].value;
68
+ return result;
69
+ }
70
+
71
+ /** Capture one factory's selection without changing Git files or process.env. */
72
+ export function createGitIdentityRunner(run, {name, email, username} = {}) {
73
+ const configuration = [];
74
+ const environment = {};
75
+ for (const [field, value] of Object.entries({name, email, username})) {
76
+ if (value === undefined) continue;
77
+ if (!is.string(value) || !value.isWellFormed() || value.includes('\0')) {
78
+ throw new TypeError(`gitIdentity.${field} must be text representable as a native process argument.`);
79
+ }
80
+ configuration.push('-c', `${field === 'username' ? 'credential.username' : `user.${field}`}=${value}`);
81
+ if (field === 'name') {
82
+ environment.GIT_AUTHOR_NAME = value;
83
+ environment.GIT_COMMITTER_NAME = value;
84
+ } else if (field === 'email') {
85
+ environment.GIT_AUTHOR_EMAIL = value;
86
+ environment.GIT_COMMITTER_EMAIL = value;
87
+ }
88
+ }
89
+ if (!configuration.length) return run;
90
+ const selectsAuthor = name !== undefined || email !== undefined;
91
+ return function runSelectedGit(command, args, options = {}) {
92
+ return run(command, [...configuration, ...args], selectsAuthor
93
+ ? {...options, env: {...options.env, ...environment}}
94
+ : options);
95
+ };
96
+ }
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import {runProcess} from './process.mjs';
5
5
  import {ArcaneError, ERROR_CODES, throwIfAborted} from './errors.mjs';
6
6
  import {createEventQueue} from './event-queue.mjs';
7
+ import {createGitIdentityRunner} from './git-identity.mjs';
7
8
 
8
9
  const is = new Is(false);
9
10
  const snapshotRef = 'refs/arcane/text-snapshot';
@@ -13,11 +14,12 @@ const snapshotRef = 'refs/arcane/text-snapshot';
13
14
  * One application service owns each dedicated cache directory. Construction
14
15
  * performs no I/O; refresh coalesces callers and close drains accepted work.
15
16
  */
16
- export function createGitTextSnapshot({cacheDirectory, remote, ref, selectPath, onEvent, run = runProcess} = {}) {
17
+ export function createGitTextSnapshot({cacheDirectory, remote, ref, selectPath, gitIdentity, onEvent, run = runProcess} = {}) {
17
18
  for (const [name, value] of Object.entries({cacheDirectory, remote, ref})) {
18
19
  if (!is.string(value) || value === '') throw new TypeError(`${name} must be a nonempty string.`);
19
20
  }
20
21
  if (!is.function(selectPath)) throw new TypeError('selectPath must be a function.');
22
+ const execute = createGitIdentityRunner(run, gitIdentity);
21
23
  const directory = path.resolve(cacheDirectory);
22
24
  let prepared = false;
23
25
  let retained = null;
@@ -34,7 +36,7 @@ export function createGitTextSnapshot({cacheDirectory, remote, ref, selectPath,
34
36
  };
35
37
  }
36
38
  try {
37
- return await run('git', ['--git-dir', directory, ...args], execution);
39
+ return await execute('git', ['--git-dir', directory, ...args], execution);
38
40
  } catch (error) {
39
41
  // runProcess first drains its child and preserves complete transport
40
42
  // failures. Keep the Git parser's public code with that full cause.
@@ -50,7 +52,7 @@ export function createGitTextSnapshot({cacheDirectory, remote, ref, selectPath,
50
52
  if (prepared) return;
51
53
  await mkdir(directory, {recursive: true});
52
54
  if ((await readdir(directory)).length === 0) {
53
- await run('git', ['init', '--bare', directory], {onEvent: events.send});
55
+ await execute('git', ['init', '--bare', directory], {onEvent: events.send});
54
56
  }
55
57
  const result = await git(['rev-parse', '--is-bare-repository'], {onEvent: events.send});
56
58
  // This is the selected cache's functional role: never reinterpret an
@@ -0,0 +1,31 @@
1
+ import {readFile} from 'node:fs/promises';
2
+ import {parentPort, workerData} from 'node:worker_threads';
3
+ import {Tokenizer} from '@huggingface/tokenizers';
4
+ import {createDecisionInputs, encodeDecisionRows} from '../../browser-runtime/ai/decision-runtime.mjs';
5
+ import {serializeCoreError} from '../../browser-runtime/core/contracts.mjs';
6
+
7
+ // Parsing and tokenization run here, not on Core's request/rendering thread.
8
+ try {
9
+ const [tokenizerText, configurationText] = await Promise.all([
10
+ readFile(workerData.tokenizerPath, 'utf8'),
11
+ readFile(workerData.tokenizerConfigPath, 'utf8')
12
+ ]);
13
+ const tokenizer = new Tokenizer(JSON.parse(tokenizerText), JSON.parse(configurationText));
14
+ const interfaceAdapter = {
15
+ encode(text, options) { return tokenizer.encode(text, options).ids; },
16
+ convert_tokens_to_ids(token) { return tokenizer.token_to_id(token); }
17
+ };
18
+ parentPort.on('message', function encodeRequest({id, rows}) {
19
+ try {
20
+ const {encoded, pad} = encodeDecisionRows(interfaceAdapter, rows, 'laya');
21
+ const inputs = createDecisionInputs(encoded, pad);
22
+ parentPort.postMessage({id, result: inputs});
23
+ } catch (error) {
24
+ parentPort.postMessage({id, error: serializeCoreError(error)});
25
+ }
26
+ });
27
+ parentPort.postMessage({id: 0, result: {ready: true}});
28
+ } catch (error) {
29
+ parentPort.postMessage({id: 0, error: serializeCoreError(error)});
30
+ parentPort.close();
31
+ }