arcane-os 0.3.1 → 0.3.2

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 (153) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +86 -117
  3. package/bin/arcane-test.mjs +170 -46
  4. package/browser-runtime/ai/browser-speech-artifacts.mjs +855 -895
  5. package/browser-runtime/ai/browser-speech-providers.mjs +80 -204
  6. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
  7. package/browser-runtime/ai/browser-wasm.mjs +24 -35
  8. package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
  9. package/browser-runtime/ai/model-controller.mjs +584 -181
  10. package/browser-runtime/ai/speech-worker-client.mjs +8 -148
  11. package/browser-runtime/ai/speech-worker-runtime.mjs +642 -374
  12. package/browser-runtime/dom-event-instrumentation.mjs +55 -147
  13. package/browser-runtime/event-manager.mjs +239 -624
  14. package/package.json +5 -6
  15. package/runtime/arcane/components/app-bar.html +3 -15
  16. package/runtime/arcane/components/assistant-panel.html +10 -10
  17. package/runtime/arcane/components/calculator.html +1 -1
  18. package/runtime/arcane/components/chat.html +1359 -135
  19. package/runtime/arcane/components/conversation-view.html +2 -2
  20. package/runtime/arcane/components/document-inspector.html +11 -17
  21. package/runtime/arcane/components/file-manager.html +13 -56
  22. package/runtime/arcane/components/markdown-document.html +82 -281
  23. package/runtime/arcane/components/markdown-editor.html +7 -10
  24. package/runtime/arcane/components/media-embed.html +6 -6
  25. package/runtime/arcane/components/screen-capture.html +4 -4
  26. package/runtime/arcane/components/source-explanation.html +2 -2
  27. package/runtime/arcane/components/speech.html +112 -68
  28. package/runtime/arcane/components/terminal-workspace.html +4 -4
  29. package/runtime/arcane/components/theme-editor.html +1 -1
  30. package/runtime/arcane/components/unified-inbox.html +2 -2
  31. package/runtime/arcane/components/voice-transcription.html +31 -21
  32. package/runtime/arcane/entities/Calculation.js +2 -3
  33. package/runtime/arcane/entities/Chat.js +228 -43
  34. package/runtime/arcane/entities/Preference.js +3 -5
  35. package/runtime/arcane/entities/Weather.js +5 -5
  36. package/runtime/arcane/modules/AI.js +1042 -427
  37. package/runtime/arcane/modules/AIProviderRuntime.js +618 -359
  38. package/runtime/arcane/modules/AIResponseLength.js +9 -19
  39. package/runtime/arcane/modules/AIRuntimeState.js +109 -72
  40. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
  41. package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
  42. package/runtime/arcane/modules/CalculatorEngine.js +9 -9
  43. package/runtime/arcane/modules/CommunicationAppController.js +3 -7
  44. package/runtime/arcane/modules/ComponentContracts.js +30 -32
  45. package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
  46. package/runtime/arcane/modules/ConversationActionItems.js +26 -59
  47. package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
  48. package/runtime/arcane/modules/ConversationTimebox.js +27 -15
  49. package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
  50. package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
  51. package/runtime/arcane/modules/HTMLImport.js +54 -1
  52. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
  53. package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
  54. package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
  55. package/runtime/arcane/modules/MD.js +1 -45
  56. package/runtime/arcane/modules/Mail.js +51 -103
  57. package/runtime/arcane/modules/MailOutbox.mjs +95 -193
  58. package/runtime/arcane/modules/MailTransport.mjs +36 -57
  59. package/runtime/arcane/modules/ModelDefinition.js +22 -106
  60. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
  61. package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
  62. package/runtime/arcane/modules/PreferenceStore.js +102 -30
  63. package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
  64. package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
  65. package/runtime/arcane/modules/ScreenCapture.js +175 -128
  66. package/runtime/arcane/modules/SpeechPlayback.js +46 -149
  67. package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
  68. package/runtime/arcane/modules/ToolCallRouter.js +25 -12
  69. package/runtime/arcane/modules/YouTubeMedia.js +6 -5
  70. package/schemas/arcane-app-bundle.schema.json +13 -78
  71. package/schemas/arcane-app.schema.json +9 -25
  72. package/schemas/arcane-lock.schema.json +18 -151
  73. package/schemas/arcane-package.schema.json +2 -16
  74. package/schemas/native-build-plan.schema.json +119 -122
  75. package/src/app-descriptor.mjs +75 -132
  76. package/src/application-tests.mjs +200 -0
  77. package/src/cli/main.mjs +27 -46
  78. package/src/constants.mjs +3 -4
  79. package/src/dev-server.mjs +30 -324
  80. package/src/doctor.mjs +92 -154
  81. package/src/dom-event-instrumentation.mjs +55 -147
  82. package/src/errors.mjs +2 -3
  83. package/src/event-manager.mjs +239 -624
  84. package/src/event-queue.mjs +3 -3
  85. package/src/import-map.mjs +273 -1028
  86. package/src/index.mjs +14 -16
  87. package/src/installed-sdk-runtime.mjs +27 -67
  88. package/src/integrated-provider-loader.mjs +53 -382
  89. package/src/mail-api.mjs +0 -2
  90. package/src/mail-server.mjs +224 -580
  91. package/src/mail.mjs +4 -10
  92. package/src/native-plan.mjs +163 -598
  93. package/src/native-provider-loader.mjs +104 -1063
  94. package/src/packager/core.mjs +485 -3229
  95. package/src/process.mjs +5 -10
  96. package/src/release-bundle.mjs +292 -2405
  97. package/src/runtime.mjs +76 -396
  98. package/src/scaffold.mjs +30 -80
  99. package/src/sdk-browser-runtime.mjs +70 -626
  100. package/src/source-server.mjs +588 -0
  101. package/src/targets/index.mjs +78 -188
  102. package/src/templates/workspace-template.mjs +19 -135
  103. package/src/testing-loader.mjs +164 -0
  104. package/src/testing.mjs +1 -1
  105. package/src/toolchain.mjs +131 -544
  106. package/src/update-check.mjs +26 -64
  107. package/src/workspace-operation-lock.mjs +139 -430
  108. package/src/workspace-runtime.mjs +109 -1558
  109. package/src/workspace.mjs +40 -302
  110. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
  111. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
  112. package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
  113. package/browser-runtime/ai/internal/sha256.mjs +0 -166
  114. package/docs/architecture.md +0 -344
  115. package/docs/compatibility.md +0 -36
  116. package/docs/event-manager.md +0 -294
  117. package/docs/platform-targets.md +0 -108
  118. package/docs/publishing.md +0 -201
  119. package/docs/reference/README.md +0 -185
  120. package/docs/reference/ai/browser-speech-package-authority.json +0 -835
  121. package/docs/reference/ai/browser-speech.md +0 -1295
  122. package/docs/reference/ai/browser-wasm.md +0 -530
  123. package/docs/reference/arcane-ollama.md +0 -288
  124. package/docs/reference/availability-and-normalization.md +0 -183
  125. package/docs/reference/behavioral-testing.md +0 -133
  126. package/docs/reference/cli.md +0 -779
  127. package/docs/reference/core/README.md +0 -62
  128. package/docs/reference/core/arcane-ai-contracts.md +0 -907
  129. package/docs/reference/core/arcane-api.md +0 -601
  130. package/docs/reference/core/arcane-entities.md +0 -65
  131. package/docs/reference/core/arcane-events.md +0 -134
  132. package/docs/reference/core/ollama-module.md +0 -181
  133. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
  134. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
  135. package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
  136. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
  137. package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
  138. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
  139. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
  140. package/docs/reference/event-manager.md +0 -1511
  141. package/docs/reference/inventory/package-api.json +0 -3284
  142. package/docs/reference/inventory/runtime-components.json +0 -1011
  143. package/docs/reference/inventory/runtime-entities.json +0 -26
  144. package/docs/reference/inventory/runtime-modules.json +0 -1431
  145. package/docs/reference/mail.md +0 -316
  146. package/docs/reference/protocols.md +0 -719
  147. package/docs/reference/runtime-components.md +0 -1366
  148. package/docs/reference/runtime-entities.md +0 -303
  149. package/docs/reference/runtime-modules.md +0 -2965
  150. package/docs/reference/sdk-api.md +0 -6698
  151. package/docs/roadmap.md +0 -79
  152. package/docs/work-amplification.md +0 -129
  153. package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
@@ -1,12 +1,10 @@
1
- const RESPONSE_LENGTH_HEADING='## Application-selected response length';
2
-
3
1
  export const AI_RESPONSE_LENGTH_DEFAULT='medium';
4
2
 
5
- export const AI_RESPONSE_LENGTH_OPTIONS=Object.freeze([
6
- Object.freeze({value:'low',label:'Low (1–5 sentences)',minSentences:1,maxSentences:5}),
7
- Object.freeze({value:'medium',label:'Medium (6–10 sentences)',minSentences:6,maxSentences:10}),
8
- Object.freeze({value:'high',label:'High (11–20 sentences)',minSentences:11,maxSentences:20})
9
- ]);
3
+ export const AI_RESPONSE_LENGTH_OPTIONS=[
4
+ {value:'low',label:'Complete'},
5
+ {value:'medium',label:'Complete'},
6
+ {value:'high',label:'Complete'}
7
+ ];
10
8
 
11
9
  const RESPONSE_LENGTH_BY_VALUE=new Map(
12
10
  AI_RESPONSE_LENGTH_OPTIONS.map(option=>[option.value,option])
@@ -20,11 +18,8 @@ export function normalizeAIResponseLength(value){
20
18
  }
21
19
 
22
20
  export function aiResponseLengthInstruction(value){
23
- const option=RESPONSE_LENGTH_BY_VALUE.get(normalizeAIResponseLength(value));
24
- const sentenceTarget=`Aim for ${option.minSentences} to ${option.maxSentences} sentences.`;
25
-
26
- return `${RESPONSE_LENGTH_HEADING}
27
- ${sentenceTarget} A more specific response-length request in the current user message overrides this saved target. Application requirements, tool or function calls, structured response schemas, safety or crisis requirements, evidence and source attribution, uncertainty, warnings, and next steps override the sentence target. Do not omit required information, invent information, or pad the response merely to reach the target.`;
21
+ normalizeAIResponseLength(value);
22
+ return '';
28
23
  }
29
24
 
30
25
  export function applyAIResponseLength(systemPrompt,value){
@@ -32,11 +27,6 @@ export function applyAIResponseLength(systemPrompt,value){
32
27
  throw new TypeError('systemPrompt must be a string');
33
28
  }
34
29
 
35
- const prompt=systemPrompt.trimEnd();
36
- if(prompt.includes(RESPONSE_LENGTH_HEADING)){
37
- return prompt;
38
- }
39
-
40
- const instruction=aiResponseLengthInstruction(value);
41
- return prompt?`${prompt}\n\n${instruction}`:instruction;
30
+ normalizeAIResponseLength(value);
31
+ return systemPrompt;
42
32
  }
@@ -1,18 +1,20 @@
1
1
  import {createArcaneEventSource} from 'arcane-os/event-manager';
2
2
 
3
+ const completeValue=(value)=>value;
4
+
3
5
  export const AI_RUNTIME_PROTOCOL = 'arcane-ai-runtime-state/1';
4
6
  export const AI_RUNTIME_STATE_EVENT = 'arcane-ai-runtime-state';
5
7
  export const AI_RUNTIME_INTENT_EVENT = 'arcane-ai-runtime-intent';
6
8
  /** Emitted with the text-chat barrier report, not full speech settlement. */
7
9
  export const AI_RUNTIME_STARTUP_EVENT = 'arcane-ai-runtime-startup-settled';
8
10
 
9
- export const AI_RUNTIME_ROLES = Object.freeze([
11
+ export const AI_RUNTIME_ROLES = completeValue([
10
12
  'llm',
11
13
  'stt',
12
14
  'tts'
13
15
  ]);
14
16
 
15
- export const AI_RUNTIME_STATES = Object.freeze([
17
+ export const AI_RUNTIME_STATES = completeValue([
16
18
  'unavailable',
17
19
  'unloaded',
18
20
  'loading',
@@ -22,7 +24,7 @@ export const AI_RUNTIME_STATES = Object.freeze([
22
24
  'disposed'
23
25
  ]);
24
26
 
25
- const ROLE_KEYS = Object.freeze([
27
+ const ROLE_KEYS = completeValue([
26
28
  'role',
27
29
  'state',
28
30
  'providerId',
@@ -34,30 +36,31 @@ const ROLE_KEYS = Object.freeze([
34
36
  'progress',
35
37
  'error'
36
38
  ]);
37
- const PROGRESS_KEYS = Object.freeze([
39
+ const PROGRESS_KEYS = completeValue([
38
40
  'phase',
39
41
  'completed',
40
42
  'total',
41
43
  'unit',
42
44
  'heartbeat'
43
45
  ]);
44
- const ERROR_KEYS = Object.freeze([
46
+ const ERROR_KEYS = completeValue([
45
47
  'code',
46
48
  'message'
47
49
  ]);
48
- const INTENT_KEYS = Object.freeze([
50
+ const INTENT_KEYS = completeValue([
49
51
  'role',
50
52
  'action',
51
53
  'reason'
52
54
  ]);
53
- const SUBSCRIPTION_OPTION_KEYS = Object.freeze([
55
+ const SUBSCRIPTION_OPTION_KEYS = completeValue([
54
56
  'signal',
55
57
  'emitCurrent'
56
58
  ]);
57
- const INTENT_SUBSCRIPTION_OPTION_KEYS = Object.freeze([
59
+ const INTENT_SUBSCRIPTION_OPTION_KEYS = completeValue([
58
60
  'signal'
59
61
  ]);
60
- const STARTUP_OPTION_KEYS = Object.freeze([
62
+ const STARTUP_OPTION_KEYS = completeValue([
63
+ 'startLanguageModel',
61
64
  'startMuted',
62
65
  'startTranscription',
63
66
  'signal'
@@ -85,9 +88,6 @@ const STARTUP_TERMINAL_STATES = new Set([
85
88
  'error',
86
89
  'disposed'
87
90
  ]);
88
- const MAX_IDENTIFIER_LENGTH = 128;
89
- const MAX_PROGRESS_UNIT_LENGTH = 32;
90
- const MAX_ERROR_MESSAGE_LENGTH = 512;
91
91
 
92
92
  /**
93
93
  * @deprecated Subscribe through the focused runtime helpers or arcaneEvents.
@@ -109,14 +109,14 @@ const aiRuntimeEventSource = createArcaneEventSource(
109
109
  aiRuntimeEventCompatibilityView,
110
110
  {
111
111
  source: 'ai-runtime-state',
112
- eventTypes: Object.freeze([
112
+ eventTypes: completeValue([
113
113
  AI_RUNTIME_STATE_EVENT,
114
114
  AI_RUNTIME_INTENT_EVENT,
115
115
  AI_RUNTIME_STARTUP_EVENT
116
116
  ])
117
117
  }
118
118
  );
119
- export const aiRuntimeEvents = Object.freeze(aiRuntimeEventCompatibilityView);
119
+ export const aiRuntimeEvents = completeValue(aiRuntimeEventCompatibilityView);
120
120
 
121
121
  function fail(message) {
122
122
  throw new TypeError(`ARCANE_AI_RUNTIME_STATE_INVALID: ${message}`);
@@ -139,8 +139,8 @@ function assertClosedRecord(value, expectedKeys, label) {
139
139
  fail(`${label} must not contain symbol keys.`);
140
140
  }
141
141
 
142
- const actualKeys = ownKeys.slice().sort();
143
- const requiredKeys = expectedKeys.slice().sort();
142
+ const actualKeys = [...ownKeys].sort();
143
+ const requiredKeys = [...expectedKeys].sort();
144
144
  if (actualKeys.length !== requiredKeys.length
145
145
  || actualKeys.some(function hasUnexpectedKey(key, index) {
146
146
  return key !== requiredKeys[index];
@@ -156,6 +156,16 @@ function assertClosedRecord(value, expectedKeys, label) {
156
156
  }
157
157
  }
158
158
 
159
+ function nextRevision(value) {
160
+ if (typeof value === 'bigint') {
161
+ return value + 1n;
162
+ }
163
+ if (value === Number.MAX_SAFE_INTEGER) {
164
+ return BigInt(value) + 1n;
165
+ }
166
+ return value + 1;
167
+ }
168
+
159
169
  function assertClosedOptions(value, allowedKeys, label) {
160
170
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
161
171
  fail(`${label} must be a plain object.`);
@@ -184,9 +194,8 @@ function assertNullableIdentifier(value, label) {
184
194
 
185
195
  if (typeof value !== 'string'
186
196
  || value.length < 1
187
- || value.length > MAX_IDENTIFIER_LENGTH
188
197
  || value.trim() !== value) {
189
- fail(`${label} must be null or a trimmed 1-${MAX_IDENTIFIER_LENGTH} character string.`);
198
+ fail(`${label} must be null or a nonempty trimmed string.`);
190
199
  }
191
200
  }
192
201
 
@@ -204,9 +213,8 @@ function copyProgress(progress) {
204
213
  assertClosedRecord(progress, PROGRESS_KEYS, 'progress');
205
214
  if (typeof progress.phase !== 'string'
206
215
  || progress.phase.length < 1
207
- || progress.phase.length > MAX_IDENTIFIER_LENGTH
208
216
  || progress.phase.trim() !== progress.phase) {
209
- fail(`progress.phase must be a trimmed 1-${MAX_IDENTIFIER_LENGTH} character string.`);
217
+ fail('progress.phase must be a nonempty trimmed string.');
210
218
  }
211
219
  if (!Number.isSafeInteger(progress.completed) || progress.completed < 0) {
212
220
  fail('progress.completed must be a nonnegative safe integer.');
@@ -218,15 +226,14 @@ function copyProgress(progress) {
218
226
  }
219
227
  if (typeof progress.unit !== 'string'
220
228
  || progress.unit.length < 1
221
- || progress.unit.length > MAX_PROGRESS_UNIT_LENGTH
222
229
  || progress.unit.trim() !== progress.unit) {
223
- fail(`progress.unit must be a trimmed 1-${MAX_PROGRESS_UNIT_LENGTH} character string.`);
230
+ fail('progress.unit must be a nonempty trimmed string.');
224
231
  }
225
232
  if (typeof progress.heartbeat !== 'boolean') {
226
233
  fail('progress.heartbeat must be a boolean.');
227
234
  }
228
235
 
229
- return Object.freeze(
236
+ return completeValue(
230
237
  {
231
238
  phase: progress.phase,
232
239
  completed: progress.completed,
@@ -245,18 +252,15 @@ function copyError(error) {
245
252
  assertClosedRecord(error, ERROR_KEYS, 'error');
246
253
  if (typeof error.code !== 'string'
247
254
  || error.code.length < 1
248
- || error.code.length > MAX_IDENTIFIER_LENGTH
249
255
  || error.code.trim() !== error.code) {
250
- fail(`error.code must be a trimmed 1-${MAX_IDENTIFIER_LENGTH} character string.`);
256
+ fail('error.code must be a nonempty trimmed string.');
251
257
  }
252
258
  if (typeof error.message !== 'string'
253
- || error.message.length < 1
254
- || error.message.length > MAX_ERROR_MESSAGE_LENGTH
255
- || error.message.trim() !== error.message) {
256
- fail(`error.message must be a trimmed 1-${MAX_ERROR_MESSAGE_LENGTH} character string.`);
259
+ || error.message.length < 1) {
260
+ fail('error.message must be a nonempty string.');
257
261
  }
258
262
 
259
- return Object.freeze(
263
+ return completeValue(
260
264
  {
261
265
  code: error.code,
262
266
  message: error.message
@@ -312,7 +316,7 @@ function copyRoleRecord(role, record) {
312
316
  fail('only an error role state may include error details.');
313
317
  }
314
318
 
315
- return Object.freeze(
319
+ return completeValue(
316
320
  {
317
321
  role,
318
322
  state: record.state,
@@ -329,7 +333,7 @@ function copyRoleRecord(role, record) {
329
333
  }
330
334
 
331
335
  function unavailableRole(role) {
332
- return Object.freeze(
336
+ return completeValue(
333
337
  {
334
338
  role,
335
339
  state: 'unavailable',
@@ -347,7 +351,7 @@ function unavailableRole(role) {
347
351
 
348
352
  function publicAIRuntimeState(snapshot, role = null) {
349
353
  const roleState = role === null ? null : snapshot.roles[role];
350
- return Object.freeze(
354
+ return completeValue(
351
355
  {
352
356
  revision: snapshot.revision,
353
357
  ...(roleState
@@ -370,7 +374,7 @@ function publicAIRuntimeState(snapshot, role = null) {
370
374
  }
371
375
 
372
376
  function publicAIRuntimeIntent(intent) {
373
- return Object.freeze(
377
+ return completeValue(
374
378
  {
375
379
  role: intent.role,
376
380
  action: intent.action,
@@ -380,7 +384,7 @@ function publicAIRuntimeIntent(intent) {
380
384
  }
381
385
 
382
386
  function publicAIRuntimeStartup(report) {
383
- return Object.freeze(
387
+ return completeValue(
384
388
  {
385
389
  revision: report.currentRevision,
386
390
  role: 'llm',
@@ -390,11 +394,11 @@ function publicAIRuntimeStartup(report) {
390
394
  }
391
395
 
392
396
  function initialSnapshot() {
393
- return Object.freeze(
397
+ return completeValue(
394
398
  {
395
399
  protocol: AI_RUNTIME_PROTOCOL,
396
400
  revision: 0,
397
- roles: Object.freeze(
401
+ roles: completeValue(
398
402
  {
399
403
  llm: unavailableRole('llm'),
400
404
  stt: unavailableRole('stt'),
@@ -405,6 +409,26 @@ function initialSnapshot() {
405
409
  );
406
410
  }
407
411
 
412
+ function copyRuntimeRoleView(record) {
413
+ return {
414
+ ...record,
415
+ progress: record.progress === null ? null : {...record.progress},
416
+ error: record.error === null ? null : {...record.error}
417
+ };
418
+ }
419
+
420
+ function copyRuntimeSnapshot(snapshot) {
421
+ return {
422
+ protocol: snapshot.protocol,
423
+ revision: snapshot.revision,
424
+ roles: {
425
+ llm: copyRuntimeRoleView(snapshot.roles.llm),
426
+ stt: copyRuntimeRoleView(snapshot.roles.stt),
427
+ tts: copyRuntimeRoleView(snapshot.roles.tts)
428
+ }
429
+ };
430
+ }
431
+
408
432
  let currentSnapshot = initialSnapshot();
409
433
 
410
434
  function assertAbortSignal(signal) {
@@ -468,6 +492,7 @@ function intentSubscriptionOptions(options) {
468
492
  function startupOptions(options) {
469
493
  if (options === undefined) {
470
494
  return {
495
+ startLanguageModel: true,
471
496
  startMuted: true,
472
497
  startTranscription: false,
473
498
  signal: null
@@ -475,6 +500,9 @@ function startupOptions(options) {
475
500
  }
476
501
 
477
502
  assertClosedOptions(options, STARTUP_OPTION_KEYS, 'startup options');
503
+ const startLanguageModel = Object.hasOwn(options, 'startLanguageModel')
504
+ ? options.startLanguageModel
505
+ : true;
478
506
  const startMuted = Object.hasOwn(options, 'startMuted')
479
507
  ? options.startMuted
480
508
  : true;
@@ -482,6 +510,9 @@ function startupOptions(options) {
482
510
  ? options.startTranscription
483
511
  : false;
484
512
  const signal = Object.hasOwn(options, 'signal') ? options.signal : null;
513
+ if (typeof startLanguageModel !== 'boolean') {
514
+ fail('startup options.startLanguageModel must be a boolean.');
515
+ }
485
516
  if (typeof startMuted !== 'boolean') {
486
517
  fail('startup options.startMuted must be a boolean.');
487
518
  }
@@ -491,6 +522,7 @@ function startupOptions(options) {
491
522
  assertAbortSignal(signal);
492
523
 
493
524
  return {
525
+ startLanguageModel,
494
526
  startMuted,
495
527
  startTranscription,
496
528
  signal
@@ -501,10 +533,16 @@ function hasAIRuntimeSelection(roleState) {
501
533
  return roleState.providerId !== null && roleState.modelId !== null;
502
534
  }
503
535
 
504
- function startupRequestedRoles(snapshot, startMuted, startTranscription) {
505
- return Object.freeze(
536
+ function startupRequestedRoles(
537
+ snapshot,
538
+ startLanguageModel,
539
+ startMuted,
540
+ startTranscription
541
+ ) {
542
+ return completeValue(
506
543
  {
507
- llm: hasAIRuntimeSelection(snapshot.roles.llm),
544
+ llm: startLanguageModel
545
+ && hasAIRuntimeSelection(snapshot.roles.llm),
508
546
  stt: startTranscription && hasAIRuntimeSelection(snapshot.roles.stt),
509
547
  tts: !startMuted && hasAIRuntimeSelection(snapshot.roles.tts)
510
548
  }
@@ -518,7 +556,7 @@ function isAIRuntimeStartupRoleSettled(roleState) {
518
556
  }
519
557
 
520
558
  function startupRoleReport(requested, roleState) {
521
- return Object.freeze(
559
+ return completeValue(
522
560
  {
523
561
  requested,
524
562
  state: roleState
@@ -529,19 +567,21 @@ function startupRoleReport(requested, roleState) {
529
567
  function startupReport(
530
568
  snapshot,
531
569
  startRevision,
570
+ startLanguageModel,
532
571
  startMuted,
533
572
  startTranscription,
534
573
  requestedRoles
535
574
  ) {
536
- return Object.freeze(
575
+ return completeValue(
537
576
  {
538
577
  protocol: AI_RUNTIME_PROTOCOL,
539
578
  startRevision,
540
579
  currentRevision: snapshot.revision,
580
+ startLanguageModel,
541
581
  startMuted,
542
582
  startTranscription,
543
583
  chatReady: snapshot.roles.llm.state === 'ready',
544
- roles: Object.freeze(
584
+ roles: completeValue(
545
585
  {
546
586
  llm: startupRoleReport(
547
587
  requestedRoles.llm,
@@ -605,9 +645,9 @@ function subscribe(eventName, listener, normalized, currentValue) {
605
645
  return unsubscribeAIRuntimeEvent;
606
646
  }
607
647
 
608
- /** Returns the current deeply immutable runtime-state snapshot. */
648
+ /** Returns a complete defensive copy of the current runtime-state snapshot. */
609
649
  export function getAIRuntimeState() {
610
- return currentSnapshot;
650
+ return copyRuntimeSnapshot(currentSnapshot);
611
651
  }
612
652
 
613
653
  /**
@@ -619,7 +659,7 @@ export function subscribeAIRuntimeState(listener, options) {
619
659
  AI_RUNTIME_STATE_EVENT,
620
660
  listener,
621
661
  stateSubscriptionOptions(options),
622
- currentSnapshot
662
+ copyRuntimeSnapshot(currentSnapshot)
623
663
  );
624
664
  }
625
665
 
@@ -629,33 +669,27 @@ export function subscribeAIRuntimeState(listener, options) {
629
669
  */
630
670
  export function publishAIRuntimeRoleState(role, completeRecord) {
631
671
  const nextRole = copyRoleRecord(role, completeRecord);
632
- if (currentSnapshot.revision === Number.MAX_SAFE_INTEGER) {
633
- throw new RangeError(
634
- 'ARCANE_AI_RUNTIME_STATE_INVALID: state revision exhausted.'
635
- );
636
- }
637
-
638
672
  const nextRoles = {
639
673
  llm: currentSnapshot.roles.llm,
640
674
  stt: currentSnapshot.roles.stt,
641
675
  tts: currentSnapshot.roles.tts
642
676
  };
643
677
  nextRoles[role] = nextRole;
644
- currentSnapshot = Object.freeze(
678
+ currentSnapshot = completeValue(
645
679
  {
646
680
  protocol: AI_RUNTIME_PROTOCOL,
647
- revision: currentSnapshot.revision + 1,
648
- roles: Object.freeze(nextRoles)
681
+ revision: nextRevision(currentSnapshot.revision),
682
+ roles: completeValue(nextRoles)
649
683
  }
650
684
  );
651
685
  aiRuntimeEventSource.dispatch(
652
686
  AI_RUNTIME_STATE_EVENT,
653
- currentSnapshot,
687
+ copyRuntimeSnapshot(currentSnapshot),
654
688
  {
655
689
  publicDetail: publicAIRuntimeState(currentSnapshot, role)
656
690
  }
657
691
  );
658
- return currentSnapshot;
692
+ return copyRuntimeSnapshot(currentSnapshot);
659
693
  }
660
694
 
661
695
  /**
@@ -665,37 +699,32 @@ export function publishAIRuntimeRoleState(role, completeRecord) {
665
699
  */
666
700
  export function publishAIRuntimeRolesState(completeRecords) {
667
701
  assertClosedRecord(completeRecords, AI_RUNTIME_ROLES, 'runtime role states');
668
- const nextRoles = Object.freeze(
702
+ const nextRoles = completeValue(
669
703
  {
670
704
  llm: copyRoleRecord('llm', completeRecords.llm),
671
705
  stt: copyRoleRecord('stt', completeRecords.stt),
672
706
  tts: copyRoleRecord('tts', completeRecords.tts)
673
707
  }
674
708
  );
675
- if (currentSnapshot.revision === Number.MAX_SAFE_INTEGER) {
676
- throw new RangeError(
677
- 'ARCANE_AI_RUNTIME_STATE_INVALID: state revision exhausted.'
678
- );
679
- }
680
- currentSnapshot = Object.freeze(
709
+ currentSnapshot = completeValue(
681
710
  {
682
711
  protocol: AI_RUNTIME_PROTOCOL,
683
- revision: currentSnapshot.revision + 1,
712
+ revision: nextRevision(currentSnapshot.revision),
684
713
  roles: nextRoles
685
714
  }
686
715
  );
687
716
  aiRuntimeEventSource.dispatch(
688
717
  AI_RUNTIME_STATE_EVENT,
689
- currentSnapshot,
718
+ copyRuntimeSnapshot(currentSnapshot),
690
719
  {
691
720
  publicDetail: publicAIRuntimeState(currentSnapshot)
692
721
  }
693
722
  );
694
- return currentSnapshot;
723
+ return copyRuntimeSnapshot(currentSnapshot);
695
724
  }
696
725
 
697
726
  /**
698
- * Emits an immutable capability-neutral lifecycle request. This function does
727
+ * Emits a complete capability-neutral lifecycle request. This function does
699
728
  * not execute, authorize, fetch, load, unload, dispose, or select a fallback.
700
729
  */
701
730
  export function requestAIRuntimeIntent(intent) {
@@ -708,7 +737,7 @@ export function requestAIRuntimeIntent(intent) {
708
737
  fail('runtime intent.reason must be startup, user, or teardown.');
709
738
  }
710
739
 
711
- const publishedIntent = Object.freeze(
740
+ const publishedIntent = completeValue(
712
741
  {
713
742
  role: intent.role,
714
743
  action: intent.action,
@@ -737,7 +766,9 @@ export function subscribeAIRuntimeIntents(listener, options) {
737
766
 
738
767
  /**
739
768
  * Starts one observational runtime barrier after preferences and provider
740
- * routes are hydrated. `barrier` resolves when LLM is ready or terminal;
769
+ * routes are hydrated. `startLanguageModel:false` keeps the selected LLM
770
+ * unloaded for an explicit user activation intent. `barrier` resolves when a
771
+ * requested LLM is ready or terminal, or immediately when LLM startup was not requested;
741
772
  * `settled` waits for every requested role. Providers own every requested load.
742
773
  */
743
774
  export function startAIRuntime(options) {
@@ -799,6 +830,7 @@ export function startAIRuntime(options) {
799
830
  const report = startupReport(
800
831
  snapshot,
801
832
  startRevision,
833
+ normalized.startLanguageModel,
802
834
  normalized.startMuted,
803
835
  normalized.startTranscription,
804
836
  requestedRoles
@@ -820,6 +852,7 @@ export function startAIRuntime(options) {
820
852
  startupReport(
821
853
  snapshot,
822
854
  startRevision,
855
+ normalized.startLanguageModel,
823
856
  normalized.startMuted,
824
857
  normalized.startTranscription,
825
858
  requestedRoles
@@ -833,7 +866,10 @@ export function startAIRuntime(options) {
833
866
  }
834
867
 
835
868
  if (!barrierResolved
836
- && isAIRuntimeStartupRoleSettled(snapshot.roles.llm)) {
869
+ && (
870
+ !requestedRoles.llm
871
+ || isAIRuntimeStartupRoleSettled(snapshot.roles.llm)
872
+ )) {
837
873
  resolveAIRuntimeStartupBarrier(snapshot);
838
874
  }
839
875
  if (cancelled) {
@@ -910,7 +946,7 @@ export function startAIRuntime(options) {
910
946
  cancelAIRuntimeStartup();
911
947
  }
912
948
 
913
- const handle = Object.freeze(
949
+ const handle = completeValue(
914
950
  {
915
951
  barrier,
916
952
  settled,
@@ -922,6 +958,7 @@ export function startAIRuntime(options) {
922
958
  startRevision = latestSnapshot.revision;
923
959
  requestedRoles = startupRequestedRoles(
924
960
  latestSnapshot,
961
+ normalized.startLanguageModel,
925
962
  normalized.startMuted,
926
963
  normalized.startTranscription
927
964
  );