arcane-os 0.3.3 → 0.3.5

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 (39) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -7
  3. package/browser-runtime/ai/browser-wasm-llm-provider.mjs +315 -119
  4. package/browser-runtime/ai/model-controller.mjs +439 -95
  5. package/package.json +1 -1
  6. package/runtime/arcane/components/assistant-panel.html +2 -1
  7. package/runtime/arcane/components/chat.html +466 -206
  8. package/runtime/arcane/components/speech.html +107 -30
  9. package/runtime/arcane/components/voice-transcription.html +27 -3
  10. package/runtime/arcane/entities/Chat.js +165 -97
  11. package/runtime/arcane/entities/IntentEnvelope.js +52 -118
  12. package/runtime/arcane/entities/TWiNPolicyDecision.js +33 -130
  13. package/runtime/arcane/entities/User.js +38 -44
  14. package/runtime/arcane/modules/AI.js +569 -302
  15. package/runtime/arcane/modules/AIProviderRuntime.js +765 -135
  16. package/runtime/arcane/modules/AIRuntimeState.js +22 -24
  17. package/runtime/arcane/modules/ApiModelDatabase.js +54 -48
  18. package/runtime/arcane/modules/CaseEvidenceIndexer.js +6 -10
  19. package/runtime/arcane/modules/CommunicationHub.js +90 -94
  20. package/runtime/arcane/modules/ComponentContracts.js +34 -9
  21. package/runtime/arcane/modules/ConfiguredAIChatSession.js +77 -77
  22. package/runtime/arcane/modules/ConversationTimebox.js +76 -104
  23. package/runtime/arcane/modules/DBLS.js +14 -12
  24. package/runtime/arcane/modules/DBOPFS.js +20 -15
  25. package/runtime/arcane/modules/Errors.js +196 -436
  26. package/runtime/arcane/modules/HTMLImport.js +49 -32
  27. package/runtime/arcane/modules/Ollama.js +16 -14
  28. package/runtime/arcane/modules/PersistentAIChatSession.js +174 -93
  29. package/runtime/arcane/modules/RecordReviewStore.js +40 -35
  30. package/runtime/arcane/modules/TerminalClient.js +12 -14
  31. package/runtime/arcane/modules/ThemeBootstrap.js +7 -7
  32. package/runtime/arcane/modules/ThemeManager.js +5 -5
  33. package/runtime/arcane/modules/TimeGuard.js +5 -65
  34. package/runtime/arcane/modules/WaitForComponent.js +43 -40
  35. package/src/cli/main.mjs +12 -11
  36. package/src/installed-sdk-runtime.mjs +11 -1
  37. package/src/mail-server.mjs +12 -5
  38. package/src/mail.mjs +25 -19
  39. package/src/workspace.mjs +32 -9
@@ -149,7 +149,8 @@
149
149
  subscribeAIRuntimeState
150
150
  } = await import('../modules/AIRuntimeState.js');
151
151
  const {
152
- createSTTActivationController
152
+ createSTTActivationController,
153
+ formatAIRuntimeProgress
153
154
  } = await import('../modules/ComponentContracts.js');
154
155
 
155
156
  const host = this;
@@ -333,8 +334,13 @@
333
334
  muteButton.addEventListener('click', muteToggle, lifecycleListenerOptions);
334
335
  window.addEventListener(
335
336
  'pagehide',
336
- destroy,
337
- { once: true, signal: runtimeStateAbortController.signal }
337
+ handlePageHide,
338
+ { signal: runtimeStateAbortController.signal }
339
+ );
340
+ window.addEventListener(
341
+ 'pageshow',
342
+ handlePageShow,
343
+ { signal: runtimeStateAbortController.signal }
338
344
  );
339
345
 
340
346
  subscribeAIRuntimeState(
@@ -723,10 +729,10 @@
723
729
  return 'Voice release requested.';
724
730
  }
725
731
  if (ttsRole.state === 'loading') {
726
- return `Voice ${formatRoleProgress(ttsRole.progress, 'loading')}; Cancel is available.`;
732
+ return `Voice ${formatAIRuntimeProgress(ttsRole.progress, 'loading')}; Cancel is available.`;
727
733
  }
728
734
  if (ttsRole.state === 'unloading') {
729
- return `Voice ${formatRoleProgress(ttsRole.progress, 'releasing')}.`;
735
+ return `Voice ${formatAIRuntimeProgress(ttsRole.progress, 'releasing')}.`;
730
736
  }
731
737
  if (ttsRole.state === 'ready') {
732
738
  if (host.muted) {
@@ -746,13 +752,6 @@
746
752
  return 'Voice unavailable.';
747
753
  }
748
754
 
749
- function formatRoleProgress(progress, fallback) {
750
- if (!progress) {
751
- return fallback;
752
- }
753
- return progress.phase;
754
- }
755
-
756
755
  function visibleErrorMessage(error, fallback) {
757
756
  try {
758
757
  if (error?.userSafe === true) {
@@ -1235,11 +1234,14 @@
1235
1234
  }
1236
1235
 
1237
1236
  const generation = ++recordingGeneration;
1238
- transcriptionOperationId = nextSpeechOperationId('transcription');
1237
+ const operationId = nextSpeechOperationId('transcription');
1238
+ transcriptionOperationId = operationId;
1239
1239
  recordingRequested = true;
1240
1240
  localStatus = {
1241
1241
  message: 'Recording. Release to transcribe.',
1242
- tone: ''
1242
+ tone: '',
1243
+ owner: 'recording',
1244
+ generation
1243
1245
  };
1244
1246
  renderControls();
1245
1247
  renderStatus();
@@ -1248,6 +1250,9 @@
1248
1250
  try {
1249
1251
  stream = await navigator.mediaDevices.getUserMedia({ audio: true });
1250
1252
  } catch (error) {
1253
+ if (generation !== recordingGeneration || destroyed) {
1254
+ return false;
1255
+ }
1251
1256
  recordingRequested = false;
1252
1257
  resetRecordPress();
1253
1258
  if (
@@ -1268,7 +1273,7 @@
1268
1273
  {
1269
1274
  bubbles: true,
1270
1275
  composed: true,
1271
- operationId: transcriptionOperationId,
1276
+ operationId,
1272
1277
  publicDetail: {
1273
1278
  ...publicSpeechErrorFields(
1274
1279
  error,
@@ -1278,7 +1283,9 @@
1278
1283
  }
1279
1284
  }
1280
1285
  );
1281
- transcriptionOperationId = null;
1286
+ if (transcriptionOperationId === operationId) {
1287
+ transcriptionOperationId = null;
1288
+ }
1282
1289
  } else {
1283
1290
  reportTranscriptionError(error);
1284
1291
  }
@@ -1287,15 +1294,25 @@
1287
1294
  return false;
1288
1295
  }
1289
1296
 
1297
+ const currentGeneration = generation === recordingGeneration;
1290
1298
  if (
1291
1299
  !recordingRequested
1292
- || generation !== recordingGeneration
1300
+ || !currentGeneration
1293
1301
  || sttRole.state !== 'ready'
1294
1302
  || sttRole.busy
1295
1303
  || destroyed
1296
1304
  ) {
1297
1305
  stopMediaStream(stream);
1298
- resetRecordPress();
1306
+ if (currentGeneration) {
1307
+ recordingRequested = false;
1308
+ resetRecordPress();
1309
+ clearRecordingStatus(generation);
1310
+ if (transcriptionOperationId === operationId) {
1311
+ transcriptionOperationId = null;
1312
+ }
1313
+ renderControls();
1314
+ renderStatus();
1315
+ }
1299
1316
  return false;
1300
1317
  }
1301
1318
 
@@ -1311,6 +1328,7 @@
1311
1328
  const processor = audioContext.createScriptProcessor(4096, 1, 1);
1312
1329
  session = {
1313
1330
  generation,
1331
+ operationId,
1314
1332
  recorder,
1315
1333
  stream,
1316
1334
  audioContext,
@@ -1349,7 +1367,6 @@
1349
1367
  recorder.start();
1350
1368
  return true;
1351
1369
  } catch (error) {
1352
- recordingRequested = false;
1353
1370
  if (session) {
1354
1371
  session.cancelled = true;
1355
1372
  releaseCaptureSession(session);
@@ -1359,8 +1376,11 @@
1359
1376
  } else {
1360
1377
  stopMediaStream(stream);
1361
1378
  }
1362
- resetRecordPress();
1363
- reportTranscriptionError(error);
1379
+ if (generation === recordingGeneration) {
1380
+ recordingRequested = false;
1381
+ resetRecordPress();
1382
+ reportTranscriptionError(error);
1383
+ }
1364
1384
  return false;
1365
1385
  }
1366
1386
  }
@@ -1377,9 +1397,34 @@
1377
1397
  }
1378
1398
  }
1379
1399
 
1400
+ function clearRecordingStatus(generation) {
1401
+ if (
1402
+ localStatus?.owner === 'recording'
1403
+ && localStatus.generation === generation
1404
+ ) {
1405
+ localStatus = null;
1406
+ }
1407
+ }
1408
+
1380
1409
  function stop() {
1410
+ const pendingCaptureStart = recordingRequested && !captureSession;
1411
+ const generation = recordingGeneration;
1412
+ const operationId = transcriptionOperationId;
1381
1413
  recordingRequested = false;
1382
1414
  clearSilenceTimer();
1415
+ if (pendingCaptureStart) {
1416
+ // getUserMedia cannot be cancelled synchronously. Retire this
1417
+ // generation so its eventual settlement cannot touch a retry.
1418
+ recordingGeneration += 1;
1419
+ resetRecordPress();
1420
+ clearRecordingStatus(generation);
1421
+ if (transcriptionOperationId === operationId) {
1422
+ transcriptionOperationId = null;
1423
+ }
1424
+ renderControls();
1425
+ renderStatus();
1426
+ return true;
1427
+ }
1383
1428
  if (captureSession && captureSession.recorder.state !== 'inactive') {
1384
1429
  captureSession.recorder.stop();
1385
1430
  }
@@ -1387,22 +1432,32 @@
1387
1432
  }
1388
1433
 
1389
1434
  async function finalizeCapture(session) {
1435
+ const currentGeneration = session.generation === recordingGeneration;
1436
+ const currentOperation = transcriptionOperationId === session.operationId;
1390
1437
  releaseCaptureSession(session);
1391
1438
  if (captureSession === session) {
1392
1439
  captureSession = null;
1393
1440
  }
1394
- recordingRequested = false;
1395
- resetRecordPress();
1441
+ if (currentGeneration) {
1442
+ recordingRequested = false;
1443
+ resetRecordPress();
1444
+ }
1396
1445
 
1397
1446
  if (
1398
1447
  session.cancelled
1399
- || session.generation !== recordingGeneration
1448
+ || !currentGeneration
1400
1449
  || sttRole.state !== 'ready'
1401
1450
  || sttRole.busy
1402
1451
  || destroyed
1403
1452
  ) {
1404
- renderControls();
1405
- renderStatus();
1453
+ if (currentGeneration) {
1454
+ clearRecordingStatus(session.generation);
1455
+ if (currentOperation) {
1456
+ transcriptionOperationId = null;
1457
+ }
1458
+ renderControls();
1459
+ renderStatus();
1460
+ }
1406
1461
  return false;
1407
1462
  }
1408
1463
 
@@ -1415,12 +1470,13 @@
1415
1470
  'audio.webm',
1416
1471
  { type: 'audio/webm' }
1417
1472
  );
1418
- return transcribeAudio(audioFile);
1473
+ return transcribeAudio(audioFile, session.operationId);
1419
1474
  }
1420
1475
 
1421
- async function transcribeAudio(audioFile) {
1476
+ async function transcribeAudio(audioFile, operationId = null) {
1422
1477
  const generation = ++transcriptionGeneration;
1423
- transcriptionOperationId = transcriptionOperationId
1478
+ transcriptionOperationId = operationId
1479
+ || transcriptionOperationId
1424
1480
  || nextSpeechOperationId('transcription');
1425
1481
  const controller = new AbortController();
1426
1482
  transcriptionAbortController?.abort();
@@ -1636,6 +1692,26 @@
1636
1692
  keyboardRecording = false;
1637
1693
  }
1638
1694
 
1695
+ function handlePageHide(event) {
1696
+ if (event.persisted === true) {
1697
+ return;
1698
+ }
1699
+ destroy();
1700
+ }
1701
+
1702
+ function handlePageShow(event) {
1703
+ if (event.persisted !== true || destroyed) {
1704
+ return;
1705
+ }
1706
+ restoreFromPageCache();
1707
+ }
1708
+
1709
+ function restoreFromPageCache() {
1710
+ synchronizeAIMutedState();
1711
+ renderControls();
1712
+ renderStatus();
1713
+ }
1714
+
1639
1715
  function destroy() {
1640
1716
  if (destroyed) {
1641
1717
  return;
@@ -1644,7 +1720,8 @@
1644
1720
  destroyed = true;
1645
1721
  runtimeStateAbortController.abort();
1646
1722
  sttActivationController.destroy();
1647
- window.removeEventListener('pagehide', destroy);
1723
+ window.removeEventListener('pagehide', handlePageHide);
1724
+ window.removeEventListener('pageshow', handlePageShow);
1648
1725
  microphonePermissionStatus?.removeEventListener?.(
1649
1726
  'change',
1650
1727
  microphonePermissionListener
@@ -212,8 +212,13 @@
212
212
  completeButton.addEventListener('click',completeStream,lifecycleListenerOptions);
213
213
  window.addEventListener(
214
214
  'pagehide',
215
- destroy,
216
- {once:true,signal:runtimeStateAbortController.signal}
215
+ handlePageHide,
216
+ {signal:runtimeStateAbortController.signal}
217
+ );
218
+ window.addEventListener(
219
+ 'pageshow',
220
+ handlePageShow,
221
+ {signal:runtimeStateAbortController.signal}
217
222
  );
218
223
 
219
224
  const sttActivationController=createSTTActivationController(
@@ -982,6 +987,24 @@
982
987
  };
983
988
  }
984
989
 
990
+ function handlePageHide(event){
991
+ if(event.persisted===true){
992
+ return;
993
+ }
994
+ destroy();
995
+ }
996
+
997
+ function handlePageShow(event){
998
+ if(event.persisted!==true||destroyed){
999
+ return;
1000
+ }
1001
+ restoreFromPageCache();
1002
+ }
1003
+
1004
+ function restoreFromPageCache(){
1005
+ renderState();
1006
+ }
1007
+
985
1008
  function destroy(){
986
1009
  if(destroyed){
987
1010
  return true;
@@ -989,7 +1012,8 @@
989
1012
  destroyed=true;
990
1013
  runtimeStateAbortController.abort();
991
1014
  sttActivationController.destroy();
992
- window.removeEventListener('pagehide',destroy);
1015
+ window.removeEventListener('pagehide',handlePageHide);
1016
+ window.removeEventListener('pageshow',handlePageShow);
993
1017
  if(!cancelSTTOperation('component-destroyed')){
994
1018
  sessionGeneration++;
995
1019
  }