arcane-os 0.2.2 → 0.3.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 (117) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +8 -8
  3. package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
  4. package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
  5. package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
  6. package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
  7. package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
  8. package/browser-runtime/ai/browser-speech.mjs +2 -0
  9. package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
  10. package/browser-runtime/ai/model-controller.mjs +285 -95
  11. package/browser-runtime/ai/speech-worker-client.mjs +247 -32
  12. package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
  13. package/browser-runtime/event-manager.mjs +1097 -1
  14. package/docs/architecture.md +2 -2
  15. package/docs/event-manager.md +155 -27
  16. package/docs/reference/README.md +27 -27
  17. package/docs/reference/ai/browser-speech-package-authority.json +835 -0
  18. package/docs/reference/ai/browser-speech.md +1162 -246
  19. package/docs/reference/ai/browser-wasm.md +18 -7
  20. package/docs/reference/availability-and-normalization.md +6 -3
  21. package/docs/reference/behavioral-testing.md +29 -6
  22. package/docs/reference/cli.md +117 -9
  23. package/docs/reference/core/arcane-ai-contracts.md +1 -1
  24. package/docs/reference/event-manager.md +577 -32
  25. package/docs/reference/inventory/package-api.json +478 -2
  26. package/docs/reference/inventory/runtime-components.json +108 -44
  27. package/docs/reference/inventory/runtime-modules.json +131 -53
  28. package/docs/reference/mail.md +316 -0
  29. package/docs/reference/protocols.md +157 -43
  30. package/docs/reference/runtime-components.md +258 -83
  31. package/docs/reference/runtime-modules.md +613 -77
  32. package/docs/reference/sdk-api.md +1014 -25
  33. package/package.json +5 -4
  34. package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
  35. package/runtime/arcane/components/app-bar.html +34 -13
  36. package/runtime/arcane/components/assistant-panel.html +110 -57
  37. package/runtime/arcane/components/calculator.html +7 -4
  38. package/runtime/arcane/components/chart.html +58 -17
  39. package/runtime/arcane/components/chat.html +606 -136
  40. package/runtime/arcane/components/conversation-view.html +13 -6
  41. package/runtime/arcane/components/dashboard-config.html +96 -59
  42. package/runtime/arcane/components/data-maintenance.html +69 -14
  43. package/runtime/arcane/components/data-view.html +53 -7
  44. package/runtime/arcane/components/directory-picker.html +118 -32
  45. package/runtime/arcane/components/document-inspector.html +47 -10
  46. package/runtime/arcane/components/file-drop.html +81 -35
  47. package/runtime/arcane/components/file-inspector.html +72 -22
  48. package/runtime/arcane/components/file-manager.html +374 -79
  49. package/runtime/arcane/components/integration-settings.html +12 -5
  50. package/runtime/arcane/components/local-ai-status.html +48 -19
  51. package/runtime/arcane/components/markdown-document.html +161 -68
  52. package/runtime/arcane/components/markdown-editor.html +110 -33
  53. package/runtime/arcane/components/media-embed.html +8 -5
  54. package/runtime/arcane/components/modal.html +15 -5
  55. package/runtime/arcane/components/output-panel.html +28 -23
  56. package/runtime/arcane/components/preferences-form.html +22 -4
  57. package/runtime/arcane/components/record-timeline.html +18 -2
  58. package/runtime/arcane/components/relationship-board.html +23 -3
  59. package/runtime/arcane/components/screen-capture.html +10 -4
  60. package/runtime/arcane/components/source-code-viewer.html +76 -9
  61. package/runtime/arcane/components/source-explanation.html +23 -3
  62. package/runtime/arcane/components/speech.html +462 -384
  63. package/runtime/arcane/components/summary-strip.html +22 -11
  64. package/runtime/arcane/components/table.html +39 -21
  65. package/runtime/arcane/components/task-progress.html +79 -21
  66. package/runtime/arcane/components/terminal-workspace.html +7 -4
  67. package/runtime/arcane/components/theme-editor.html +7 -3
  68. package/runtime/arcane/components/unified-inbox.html +9 -4
  69. package/runtime/arcane/components/voice-transcription.html +639 -98
  70. package/runtime/arcane/components/weather-widget.html +5 -3
  71. package/runtime/arcane/components/web-navigator.html +48 -8
  72. package/runtime/arcane/entities/Chat.js +1 -1
  73. package/runtime/arcane/entities/User.js +110 -23
  74. package/runtime/arcane/modules/AI.js +2109 -130
  75. package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
  76. package/runtime/arcane/modules/AIRuntimeState.js +109 -52
  77. package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
  78. package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
  79. package/runtime/arcane/modules/CalculatorEngine.js +63 -3
  80. package/runtime/arcane/modules/CommunicationAppController.js +588 -28
  81. package/runtime/arcane/modules/CommunicationHub.js +590 -10
  82. package/runtime/arcane/modules/ComponentContracts.js +470 -0
  83. package/runtime/arcane/modules/ConversationTimebox.js +152 -33
  84. package/runtime/arcane/modules/DBLS.js +40 -7
  85. package/runtime/arcane/modules/DBOPFS.js +35 -11
  86. package/runtime/arcane/modules/DataMaintenance.js +12 -2
  87. package/runtime/arcane/modules/Errors.js +65 -7
  88. package/runtime/arcane/modules/HTMLImport.js +198 -14
  89. package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
  90. package/runtime/arcane/modules/Mail.js +738 -115
  91. package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
  92. package/runtime/arcane/modules/MailTransport.mjs +197 -39
  93. package/runtime/arcane/modules/Ollama.js +36 -1
  94. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
  95. package/runtime/arcane/modules/PreferenceStore.js +367 -33
  96. package/runtime/arcane/modules/RecordReviewStore.js +322 -23
  97. package/runtime/arcane/modules/ScreenCapture.js +1397 -15
  98. package/runtime/arcane/modules/SpeechPlayback.js +438 -41
  99. package/runtime/arcane/modules/TerminalClient.js +277 -12
  100. package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
  101. package/runtime/arcane/modules/ThemeManager.js +39 -7
  102. package/runtime/arcane/modules/TimeGuard.js +131 -20
  103. package/runtime/arcane/modules/WaitForComponent.js +386 -33
  104. package/schemas/arcane-lock.schema.json +2 -2
  105. package/src/cli/main.mjs +435 -9
  106. package/src/event-manager.mjs +1097 -1
  107. package/src/import-map.mjs +21 -3
  108. package/src/index.mjs +13 -0
  109. package/src/installed-sdk-runtime.mjs +112 -0
  110. package/src/mail-api.mjs +22 -0
  111. package/src/mail-credentials.mjs +667 -0
  112. package/src/mail-server.mjs +1769 -0
  113. package/src/mail.mjs +261 -0
  114. package/src/sdk-browser-runtime.mjs +85 -41
  115. package/src/testing-loader.mjs +7 -0
  116. package/src/toolchain.mjs +3 -0
  117. package/src/workspace.mjs +1 -1
@@ -684,7 +684,31 @@
684
684
  </section>
685
685
 
686
686
  <script type="module">
687
+ const {
688
+ createArcaneEventSource,
689
+ projectArcaneDOMEvent
690
+ }=await import('arcane-os/event-manager');
691
+
687
692
  const host=this;
693
+ const events=createArcaneEventSource(
694
+ host,
695
+ {
696
+ source:'arcane.component.chat',
697
+ eventTypes:[
698
+ 'chat-ready',
699
+ 'chat-send-message',
700
+ 'chat-send-error',
701
+ 'chat-file-uploaded',
702
+ 'chat-file-upload-error',
703
+ 'chat-language-changed',
704
+ 'chat-language-change-error',
705
+ 'chat-ai-activation-request',
706
+ 'chat-ai-activation-error',
707
+ 'chat-speech-synthesis-error',
708
+ 'conversation-timebox-error'
709
+ ]
710
+ }
711
+ );
688
712
  const shadowRoot=host.shadowRoot;
689
713
  const chatArea=shadowRoot.querySelector('.chat_area');
690
714
  const textArea=shadowRoot.querySelector('#chat_input');
@@ -728,7 +752,176 @@
728
752
  let conversationTimeboxError='';
729
753
  let lastQueuedTimeboxRevision=null;
730
754
  let hostSubmissionGeneration=0;
755
+ let eventOperationSequence=0;
756
+ let destroyed=false;
757
+ const activeSubmissionOwnerships=new Set();
731
758
  const conversationTimeboxRetryDelays=Object.freeze([0,500,1_500]);
759
+ const chatReasons=Object.freeze({
760
+ ready:'chat-ready',
761
+ messageSubmissionRequested:'message-submission-requested',
762
+ messageSubmissionCancelled:'message-submission-cancelled',
763
+ callerSignalAborted:'caller-signal-aborted',
764
+ componentDestroyed:'component-destroyed',
765
+ hostMessageSubmissionRejected:'host-message-submission-rejected',
766
+ fileStorageCompleted:'file-storage-completed',
767
+ fileStorageRejected:'file-storage-rejected',
768
+ languageChangeRequested:'language-change-requested',
769
+ languageChangeCallbackRejected:'language-change-callback-rejected',
770
+ languageModelActivationRequested:'language-model-activation-requested',
771
+ languageModelActivationRejected:'language-model-activation-rejected',
772
+ speechSynthesisRejected:'speech-synthesis-rejected',
773
+ conversationTimeboxDeliveryRejected:'conversation-timebox-delivery-rejected'
774
+ });
775
+ const chatErrorCodes=Object.freeze({
776
+ messageSubmissionAborted:'ARCANE_CHAT_MESSAGE_SUBMISSION_ABORTED',
777
+ languageModelActivationRejected:'ARCANE_CHAT_LANGUAGE_MODEL_ACTIVATION_REQUEST_REJECTED',
778
+ hostMessageSubmissionRejected:'ARCANE_CHAT_HOST_MESSAGE_SUBMISSION_REJECTED',
779
+ fileStorageRejected:'ARCANE_CHAT_FILE_STORAGE_REJECTED',
780
+ languageChangeCallbackRejected:'ARCANE_CHAT_LANGUAGE_CHANGE_CALLBACK_REJECTED',
781
+ speechSynthesisRejected:'ARCANE_CHAT_SPEECH_SYNTHESIS_REQUEST_REJECTED',
782
+ conversationTimeboxDeliveryRejected:'ARCANE_CHAT_CONVERSATION_TIMEBOX_DELIVERY_REJECTED'
783
+ });
784
+
785
+ function nextChatOperationId(kind){
786
+ eventOperationSequence+=1;
787
+ return `${events.descriptor.instanceId}:${kind}:${eventOperationSequence}`;
788
+ }
789
+
790
+ function dispatchChatEvent(
791
+ type,
792
+ compatibilityDetail={},
793
+ {
794
+ publicDetail={},
795
+ operationId=null,
796
+ bubbles=false,
797
+ composed=false,
798
+ cancelable=false
799
+ }={}
800
+ ){
801
+ if(destroyed){
802
+ return false;
803
+ }
804
+ const publication=events.dispatch(
805
+ type,
806
+ compatibilityDetail,
807
+ {
808
+ publicDetail,
809
+ operationId,
810
+ cancelable
811
+ }
812
+ );
813
+ if(!publication.accepted){
814
+ return false;
815
+ }
816
+ return projectArcaneDOMEvent(
817
+ host,
818
+ publication.occurrence,
819
+ {
820
+ bubbles,
821
+ composed,
822
+ cancelable
823
+ }
824
+ );
825
+ }
826
+
827
+ function publicErrorFields(error,boundaryCode){
828
+ let causeCode='';
829
+ try{
830
+ causeCode=typeof error?.code==='string'?error.code.trim():'';
831
+ }catch{}
832
+ return Object.freeze({
833
+ code:boundaryCode,
834
+ ...(causeCode&&causeCode!==boundaryCode?{causeCode}:{})
835
+ });
836
+ }
837
+
838
+ function isAbortSignal(value){
839
+ return value!==null
840
+ &&typeof value==='object'
841
+ &&typeof value.aborted==='boolean'
842
+ &&typeof value.addEventListener==='function'
843
+ &&typeof value.removeEventListener==='function';
844
+ }
845
+
846
+ function createChatSubmissionAbort(reason,message){
847
+ const error=new Error(message);
848
+ error.name='AbortError';
849
+ error.code=chatErrorCodes.messageSubmissionAborted;
850
+ error.reason=reason;
851
+ return error;
852
+ }
853
+
854
+ function createChatSubmissionOwnership(callerSignal=null){
855
+ if(callerSignal!==null&&callerSignal!==undefined&&!isAbortSignal(callerSignal)){
856
+ throw new TypeError('Chat submission signal must be an AbortSignal.');
857
+ }
858
+ const controller=new AbortController();
859
+ const subscriptions=[];
860
+ let released=false;
861
+
862
+ function releaseChatSubmissionOwnership(){
863
+ if(released){
864
+ return false;
865
+ }
866
+ released=true;
867
+ for(const {signal,listener} of subscriptions.splice(0)){
868
+ signal.removeEventListener('abort',listener);
869
+ }
870
+ activeSubmissionOwnerships.delete(ownership);
871
+ return true;
872
+ }
873
+
874
+ function abortChatSubmission(reason){
875
+ if(!controller.signal.aborted){
876
+ controller.abort(reason);
877
+ }
878
+ releaseChatSubmissionOwnership();
879
+ return true;
880
+ }
881
+
882
+ const ownership=Object.freeze({
883
+ signal:controller.signal,
884
+ abort:abortChatSubmission,
885
+ release:releaseChatSubmissionOwnership
886
+ });
887
+ activeSubmissionOwnerships.add(ownership);
888
+
889
+ function followAbortSignal(signal,fallbackReason){
890
+ if(!signal||controller.signal.aborted){
891
+ return;
892
+ }
893
+ if(signal.aborted){
894
+ abortChatSubmission(signal.reason??fallbackReason);
895
+ return;
896
+ }
897
+ function forwardChatSubmissionAbort(){
898
+ abortChatSubmission(signal.reason??fallbackReason);
899
+ }
900
+ subscriptions.push({signal,listener:forwardChatSubmissionAbort});
901
+ signal.addEventListener('abort',forwardChatSubmissionAbort,{once:true});
902
+ }
903
+
904
+ try{
905
+ followAbortSignal(
906
+ aiRuntimeStateAbortController.signal,
907
+ createChatSubmissionAbort(
908
+ chatReasons.componentDestroyed,
909
+ 'The chat component was destroyed before message submission settled.'
910
+ )
911
+ );
912
+ followAbortSignal(
913
+ callerSignal,
914
+ createChatSubmissionAbort(
915
+ chatReasons.callerSignalAborted,
916
+ 'The caller canceled the chat message submission.'
917
+ )
918
+ );
919
+ }catch(error){
920
+ releaseChatSubmissionOwnership();
921
+ throw error;
922
+ }
923
+ return ownership;
924
+ }
732
925
 
733
926
  host.sendMessage=host.sendMessage||async function(text=''){
734
927
  console.warn('html-import chat.html `host.sendMessage` method not implemented.\n\nThis can also be done by listening for the "chat-send-message" event.\n\nIt is still best practice to implement the `.sendMessage` method atleast with an empty function.\n\n')
@@ -767,7 +960,11 @@
767
960
  signal:aiRuntimeStateAbortController.signal
768
961
  }
769
962
  );
770
- window.addEventListener('pagehide',destroy,{once:true});
963
+ window.addEventListener(
964
+ 'pagehide',
965
+ destroy,
966
+ {once:true,signal:aiRuntimeStateAbortController.signal}
967
+ );
771
968
 
772
969
  function setConversationComplete(complete=true){
773
970
  if(typeof complete!=='boolean'){
@@ -805,7 +1002,7 @@
805
1002
  initialSpeechMuteListenerInstalled=false;
806
1003
  speech.configure?.({initialMuted:requestedInitialSpeechMuted});
807
1004
  },
808
- {once:true}
1005
+ {once:true,signal:aiRuntimeStateAbortController.signal}
809
1006
  );
810
1007
  }
811
1008
 
@@ -944,10 +1141,22 @@
944
1141
  send.setAttribute('aria-label',send.title);
945
1142
  }
946
1143
 
947
- function createAIActivationController({host,panel,title,status,button,EventClass=CustomEvent}){
1144
+ function createAIActivationController({
1145
+ host,
1146
+ panel,
1147
+ title,
1148
+ status,
1149
+ button,
1150
+ publish=dispatchChatEvent,
1151
+ createOperationId=nextChatOperationId,
1152
+ readErrorFields=publicErrorFields,
1153
+ reasons=chatReasons,
1154
+ errorCodes=chatErrorCodes
1155
+ }){
948
1156
  let role=null;
949
1157
  let requestPending=false;
950
1158
  let requestError='';
1159
+ let requestGeneration=0;
951
1160
  let destroyed=false;
952
1161
 
953
1162
  function visibleError(error,fallback){
@@ -981,6 +1190,10 @@
981
1190
  }
982
1191
 
983
1192
  function render(){
1193
+ if(destroyed){
1194
+ panel.hidden=true;
1195
+ return;
1196
+ }
984
1197
  const visible=selected()&&role.state!=='ready';
985
1198
  panel.hidden=!visible;
986
1199
  panel.setAttribute(
@@ -1047,16 +1260,27 @@
1047
1260
  requestError='';
1048
1261
  const intent=Object.freeze({role:'llm',action,reason:'user'});
1049
1262
  const request=Object.freeze({intent,state:role});
1050
- const activationEvent=new EventClass(
1263
+ const operationId=typeof role?.operationId==='string'&&role.operationId
1264
+ ?role.operationId
1265
+ :createOperationId('llm-activation');
1266
+ const generation=++requestGeneration;
1267
+ const accepted=publish(
1051
1268
  'chat-ai-activation-request',
1269
+ request,
1052
1270
  {
1053
1271
  bubbles:true,
1054
1272
  composed:true,
1055
1273
  cancelable:true,
1056
- detail:request
1274
+ operationId,
1275
+ publicDetail:{
1276
+ role:'llm',
1277
+ action,
1278
+ reason:reasons.languageModelActivationRequested,
1279
+ state:role.state
1280
+ }
1057
1281
  }
1058
1282
  );
1059
- if(!host.dispatchEvent(activationEvent)){
1283
+ if(!accepted||destroyed||generation!==requestGeneration){
1060
1284
  render();
1061
1285
  return false;
1062
1286
  }
@@ -1064,8 +1288,14 @@
1064
1288
  render();
1065
1289
  try{
1066
1290
  await host.requestAIActivation(intent);
1291
+ if(destroyed||generation!==requestGeneration){
1292
+ return false;
1293
+ }
1067
1294
  return true;
1068
1295
  }catch(error){
1296
+ if(destroyed||generation!==requestGeneration){
1297
+ return false;
1298
+ }
1069
1299
  if(action==='load'
1070
1300
  &&cancellation(error)
1071
1301
  &&['unloaded','unloading'].includes(role?.state)){
@@ -1076,20 +1306,30 @@
1076
1306
  `The language model ${action} request failed.`
1077
1307
  );
1078
1308
  requestError=message;
1079
- host.dispatchEvent(
1080
- new EventClass(
1081
- 'chat-ai-activation-error',
1082
- {
1083
- bubbles:true,
1084
- composed:true,
1085
- detail:Object.freeze({request,error,message})
1309
+ publish(
1310
+ 'chat-ai-activation-error',
1311
+ Object.freeze({request,error,message}),
1312
+ {
1313
+ bubbles:true,
1314
+ composed:true,
1315
+ operationId,
1316
+ publicDetail:{
1317
+ role:'llm',
1318
+ action,
1319
+ reason:reasons.languageModelActivationRejected,
1320
+ ...readErrorFields(
1321
+ error,
1322
+ errorCodes.languageModelActivationRejected
1323
+ )
1086
1324
  }
1087
- )
1325
+ }
1088
1326
  );
1089
1327
  return false;
1090
1328
  }finally{
1091
- requestPending=false;
1092
- render();
1329
+ if(!destroyed&&generation===requestGeneration){
1330
+ requestPending=false;
1331
+ render();
1332
+ }
1093
1333
  }
1094
1334
  }
1095
1335
 
@@ -1104,7 +1344,9 @@
1104
1344
  }
1105
1345
  if(role?.state!==nextRole.state
1106
1346
  ||role?.providerId!==nextRole.providerId
1107
- ||role?.modelId!==nextRole.modelId){
1347
+ ||role?.modelId!==nextRole.modelId
1348
+ ||role?.operationId!==nextRole.operationId){
1349
+ requestGeneration+=1;
1108
1350
  requestError='';
1109
1351
  requestPending=false;
1110
1352
  }
@@ -1117,6 +1359,8 @@
1117
1359
  return;
1118
1360
  }
1119
1361
  destroyed=true;
1362
+ requestGeneration+=1;
1363
+ requestPending=false;
1120
1364
  button.removeEventListener('click',activateSelectedAI);
1121
1365
  panel.hidden=true;
1122
1366
  }
@@ -1256,6 +1500,10 @@
1256
1500
  }
1257
1501
 
1258
1502
  function queueConversationTimeboxMessage(message,context,guard){
1503
+ const operationId=typeof context?.operationId==='string'&&context.operationId
1504
+ ?context.operationId
1505
+ :nextChatOperationId('conversation-timebox');
1506
+ const deliveryContext=Object.freeze({...context,operationId});
1259
1507
  const prior=programmaticSubmissionQueue.catch(
1260
1508
  function ignorePriorProgrammaticSubmissionFailure(){
1261
1509
  return false;
@@ -1263,6 +1511,12 @@
1263
1511
  );
1264
1512
  const queued=prior.then(
1265
1513
  async function dispatchConversationTimeboxMessage(){
1514
+ if(destroyed||aiRuntimeStateAbortController.signal.aborted){
1515
+ return false;
1516
+ }
1517
+ if(typeof guard==='function'&&!guard()){
1518
+ return false;
1519
+ }
1266
1520
  let lastError;
1267
1521
  let attemptedSubmissionGeneration=null;
1268
1522
  for(
@@ -1271,10 +1525,13 @@
1271
1525
  attempt++
1272
1526
  ){
1273
1527
  const retryDelay=conversationTimeboxRetryDelays[attempt];
1274
- if(retryDelay>0){
1275
- await waitForConversationTimeboxRetry(retryDelay);
1528
+ if(retryDelay>0&&!(await waitForConversationTimeboxRetry(retryDelay))){
1529
+ return false;
1276
1530
  }
1277
1531
  await hostSubmissionBarrier.waitForIdle();
1532
+ if(destroyed||aiRuntimeStateAbortController.signal.aborted){
1533
+ return false;
1534
+ }
1278
1535
  if(typeof guard==='function'&&!guard()){
1279
1536
  return false;
1280
1537
  }
@@ -1288,15 +1545,24 @@
1288
1545
  const delivery=submitMessage(
1289
1546
  message,
1290
1547
  {
1291
- ...context,
1548
+ ...deliveryContext,
1292
1549
  retry:attempt>0,
1293
1550
  reuseVisibleMessage:attempt>0
1294
1551
  }
1295
1552
  );
1296
1553
  attemptedSubmissionGeneration=hostSubmissionGeneration;
1297
1554
  const delivered=await delivery;
1555
+ if(destroyed||aiRuntimeStateAbortController.signal.aborted){
1556
+ return false;
1557
+ }
1558
+ if(typeof guard==='function'&&!guard()){
1559
+ return false;
1560
+ }
1298
1561
  return requireConversationTimeboxDelivery(delivered);
1299
1562
  }catch(error){
1563
+ if(destroyed||aiRuntimeStateAbortController.signal.aborted){
1564
+ return false;
1565
+ }
1300
1566
  lastError=error;
1301
1567
  }
1302
1568
  }
@@ -1305,6 +1571,12 @@
1305
1571
  );
1306
1572
  programmaticSubmissionQueue=queued.then(
1307
1573
  function finishConversationTimeboxSubmission(result){
1574
+ if(destroyed||aiRuntimeStateAbortController.signal.aborted){
1575
+ return false;
1576
+ }
1577
+ if(typeof guard==='function'&&!guard()){
1578
+ return false;
1579
+ }
1308
1580
  if(result!==false){
1309
1581
  clearConversationTimeboxError();
1310
1582
  const current=conversationTimeboxController?.snapshot();
@@ -1315,21 +1587,31 @@
1315
1587
  return result;
1316
1588
  },
1317
1589
  function reportConversationTimeboxSubmissionFailure(error){
1590
+ if(destroyed||aiRuntimeStateAbortController.signal.aborted){
1591
+ return false;
1592
+ }
1318
1593
  if(typeof guard==='function'&&!guard()){
1319
1594
  return false;
1320
1595
  }
1321
1596
  setConversationTimeboxError(
1322
1597
  'The time check could not reach the AI.'
1323
1598
  );
1324
- host.dispatchEvent(
1325
- new CustomEvent(
1326
- 'conversation-timebox-error',
1327
- {
1328
- bubbles:true,
1329
- composed:true,
1330
- detail:{error,context:{...context}}
1599
+ dispatchChatEvent(
1600
+ 'conversation-timebox-error',
1601
+ {error,context:{...deliveryContext}},
1602
+ {
1603
+ bubbles:true,
1604
+ composed:true,
1605
+ operationId,
1606
+ publicDetail:{
1607
+ source:deliveryContext.source||'conversation-timebox',
1608
+ reason:chatReasons.conversationTimeboxDeliveryRejected,
1609
+ ...publicErrorFields(
1610
+ error,
1611
+ chatErrorCodes.conversationTimeboxDeliveryRejected
1612
+ )
1331
1613
  }
1332
- )
1614
+ }
1333
1615
  );
1334
1616
  console.error('The conversation timebox message could not be sent.');
1335
1617
  return false;
@@ -1339,22 +1621,46 @@
1339
1621
  }
1340
1622
 
1341
1623
  function waitForConversationTimeboxRetry(delayMilliseconds){
1624
+ const signal=aiRuntimeStateAbortController.signal;
1625
+ if(signal.aborted){
1626
+ return Promise.resolve(false);
1627
+ }
1342
1628
  return new Promise(function scheduleConversationTimeboxRetry(resolve){
1343
- setTimeout(resolve,delayMilliseconds);
1344
- });
1345
- }
1346
-
1347
- {
1348
- const chatReady=new CustomEvent(
1349
- 'chat-ready', {
1350
- detail: { chat: host }
1629
+ let settled=false;
1630
+ let timer=null;
1631
+ function finishRetryWait(completed){
1632
+ if(settled){
1633
+ return;
1634
+ }
1635
+ settled=true;
1636
+ if(timer!==null){
1637
+ clearTimeout(timer);
1638
+ }
1639
+ signal.removeEventListener('abort',cancelRetryWait);
1640
+ resolve(completed);
1351
1641
  }
1352
- );
1353
-
1354
- host.dispatchEvent(chatReady);
1642
+ function cancelRetryWait(){
1643
+ finishRetryWait(false);
1644
+ }
1645
+ timer=setTimeout(function completeRetryWait(){
1646
+ finishRetryWait(true);
1647
+ },delayMilliseconds);
1648
+ signal.addEventListener('abort',cancelRetryWait,{once:true});
1649
+ if(signal.aborted){
1650
+ cancelRetryWait();
1651
+ }
1652
+ });
1355
1653
  }
1356
1654
 
1357
1655
  host.ready=true;
1656
+ dispatchChatEvent(
1657
+ 'chat-ready',
1658
+ {chat:host},
1659
+ {
1660
+ operationId:nextChatOperationId('ready'),
1661
+ publicDetail:{ready:true,reason:chatReasons.ready}
1662
+ }
1663
+ );
1358
1664
 
1359
1665
  async function streamMessage(text='', id='', isThinking){
1360
1666
  if(!id){
@@ -1429,20 +1735,26 @@
1429
1735
  chatOutput.scrollTop = chatOutput.scrollHeight;
1430
1736
  }
1431
1737
 
1432
- textArea.addEventListener('keyup', resizeTextArea);
1738
+ textArea.addEventListener(
1739
+ 'keyup',
1740
+ resizeTextArea,
1741
+ {signal:aiRuntimeStateAbortController.signal}
1742
+ );
1433
1743
 
1434
1744
  send.addEventListener(
1435
1745
  'click',
1436
1746
  function submitChatMessageFromButton(){
1437
1747
  void submitMessage('',{source:'user'});
1438
- }
1748
+ },
1749
+ {signal:aiRuntimeStateAbortController.signal}
1439
1750
  );
1440
1751
 
1441
1752
  uploadBtn.addEventListener(
1442
1753
  'click',
1443
1754
  function(){
1444
1755
  fileInput.click();
1445
- }
1756
+ },
1757
+ {signal:aiRuntimeStateAbortController.signal}
1446
1758
  );
1447
1759
 
1448
1760
  fileInput.addEventListener(
@@ -1455,23 +1767,51 @@
1455
1767
  for(let i=0;i<fileInput.files.length;i++){
1456
1768
  const file=fileInput.files[i];
1457
1769
  const newFile=new FileEntity(file.name, 'documents');
1458
- newFile.save(file);
1459
-
1460
- const uploadEvent = new CustomEvent(
1461
- 'chat-file-uploaded',
1462
- {
1463
- detail: { file },
1464
- bubbles: true,
1465
- composed: true,
1466
- cancelable: true
1467
- }
1468
- );
1469
-
1470
- host.dispatchEvent(uploadEvent);
1770
+ const operationId=nextChatOperationId('file-upload');
1771
+ try{
1772
+ await newFile.save(file);
1773
+ if(destroyed)return;
1774
+ dispatchChatEvent(
1775
+ 'chat-file-uploaded',
1776
+ {file},
1777
+ {
1778
+ bubbles:true,
1779
+ composed:true,
1780
+ cancelable:true,
1781
+ operationId,
1782
+ publicDetail:{
1783
+ size:Number.isFinite(file.size)?file.size:null,
1784
+ type:String(file.type||''),
1785
+ reason:chatReasons.fileStorageCompleted
1786
+ }
1787
+ }
1788
+ );
1789
+ }catch(error){
1790
+ if(destroyed)return;
1791
+ dispatchChatEvent(
1792
+ 'chat-file-upload-error',
1793
+ {file,error},
1794
+ {
1795
+ bubbles:true,
1796
+ composed:true,
1797
+ operationId,
1798
+ publicDetail:{
1799
+ size:Number.isFinite(file.size)?file.size:null,
1800
+ type:String(file.type||''),
1801
+ reason:chatReasons.fileStorageRejected,
1802
+ ...publicErrorFields(
1803
+ error,
1804
+ chatErrorCodes.fileStorageRejected
1805
+ )
1806
+ }
1807
+ }
1808
+ );
1809
+ }
1471
1810
  }
1472
1811
 
1473
1812
  fileInput.value = '';
1474
- }
1813
+ },
1814
+ {signal:aiRuntimeStateAbortController.signal}
1475
1815
  );
1476
1816
 
1477
1817
  speech.addEventListener(
@@ -1481,29 +1821,62 @@
1481
1821
  event.detail?.text||'',
1482
1822
  {source:'speech'}
1483
1823
  );
1484
- }
1824
+ },
1825
+ {signal:aiRuntimeStateAbortController.signal}
1485
1826
  )
1486
1827
 
1487
1828
  shadowRoot.querySelector('#languages').addEventListener(
1488
1829
  'change',
1489
1830
  function() {
1490
- host.language = this.value;
1491
- console.info("Language changed to:", host.language);
1492
-
1493
- const chatEvent = new CustomEvent(
1831
+ const language=this.value;
1832
+ const operationId=nextChatOperationId('language-change');
1833
+ const accepted=dispatchChatEvent(
1494
1834
  'chat-language-changed',
1835
+ {message:language},
1495
1836
  {
1496
- detail: {
1497
- message:host.language
1498
- },
1499
- bubbles: true,
1500
- cancelable: true
1837
+ bubbles:true,
1838
+ cancelable:true,
1839
+ operationId,
1840
+ publicDetail:{
1841
+ language,
1842
+ reason:chatReasons.languageChangeRequested
1843
+ }
1501
1844
  }
1502
1845
  );
1503
-
1504
- host.dispatchEvent(chatEvent);
1505
- host.languageChanged(host.language);
1506
- }
1846
+ if(!accepted){
1847
+ this.value=host.language;
1848
+ return;
1849
+ }
1850
+ host.language=language;
1851
+ console.info("Language changed to:",host.language);
1852
+ let result;
1853
+ try{result=host.languageChanged(host.language);}
1854
+ catch(error){result=Promise.reject(error);}
1855
+ void Promise.resolve(result).catch(
1856
+ function reportLanguageChangeCallbackFailure(error){
1857
+ if(destroyed)return false;
1858
+ dispatchChatEvent(
1859
+ 'chat-language-change-error',
1860
+ {error,language},
1861
+ {
1862
+ bubbles:true,
1863
+ composed:true,
1864
+ operationId,
1865
+ publicDetail:{
1866
+ language,
1867
+ reason:chatReasons.languageChangeCallbackRejected,
1868
+ ...publicErrorFields(
1869
+ error,
1870
+ chatErrorCodes.languageChangeCallbackRejected
1871
+ )
1872
+ }
1873
+ }
1874
+ );
1875
+ return false;
1876
+ }
1877
+ );
1878
+ },
1879
+ {signal:aiRuntimeStateAbortController.signal}
1507
1880
  );
1508
1881
 
1509
1882
  function getMilTime() {
@@ -1528,27 +1901,50 @@
1528
1901
  textArea.style.height = `${Math.min(scrollHeight, maxHeight)}px`;
1529
1902
  }
1530
1903
 
1531
- function observeHostSubmission(result,context){
1532
- const observed=hostSubmissionBarrier.track(result).catch(
1904
+ function observeHostSubmission(result,context,ownership){
1905
+ const observed=hostSubmissionBarrier.track(result).then(
1906
+ function settleHostSubmission(value){
1907
+ if(destroyed||context.signal.aborted){
1908
+ return false;
1909
+ }
1910
+ return value;
1911
+ },
1533
1912
  function reportHostSubmissionFailure(error){
1534
- host.dispatchEvent(
1535
- new CustomEvent(
1536
- 'chat-send-error',
1537
- {
1538
- bubbles:true,
1539
- composed:true,
1540
- detail:{error,context:{...context}}
1913
+ if(destroyed||context.signal.aborted){
1914
+ return false;
1915
+ }
1916
+ dispatchChatEvent(
1917
+ 'chat-send-error',
1918
+ {error,context:{...context}},
1919
+ {
1920
+ bubbles:true,
1921
+ composed:true,
1922
+ operationId:context.operationId||null,
1923
+ publicDetail:{
1924
+ source:context.source||'user',
1925
+ reason:chatReasons.hostMessageSubmissionRejected,
1926
+ ...publicErrorFields(
1927
+ error,
1928
+ chatErrorCodes.hostMessageSubmissionRejected
1929
+ )
1541
1930
  }
1542
- )
1931
+ }
1543
1932
  );
1544
1933
  console.error('The chat message could not be submitted.');
1545
1934
  return false;
1546
1935
  }
1936
+ ).finally(
1937
+ function releaseHostSubmissionOwnership(){
1938
+ ownership.release();
1939
+ }
1547
1940
  );
1548
1941
  return observed;
1549
1942
  }
1550
1943
 
1551
1944
  async function submitMessage(textOverride='',context={}) {
1945
+ if(destroyed){
1946
+ return false;
1947
+ }
1552
1948
  if(typeof textOverride!=='string'){
1553
1949
  throw new TypeError('Programmatic chat messages must be strings.');
1554
1950
  }
@@ -1571,60 +1967,104 @@
1571
1967
  if(!text.trim()){
1572
1968
  return false;
1573
1969
  }
1574
- const messageTime = getMilTime();
1575
- if(!submissionContext.preserveDraft){
1576
- textArea.value='';
1577
- }
1578
-
1579
- //console.log(host,host.name);
1580
- // console.log(forceText,text,1);
1581
- if(!submissionContext.synthetic&&!submissionContext.reuseVisibleMessage){
1582
- hostSubmissionGeneration++;
1583
- chatOutput.innerHTML += `
1584
- <li class='user'>
1585
- <strong>${host.name} ${messageTime}:</strong>
1586
- <br>
1587
- <br>
1588
- <div class='markdown'>
1589
- ${new MD(text).rendered}
1590
- </div>
1591
- </li>
1592
- `;
1593
- }
1594
- chatOutput.scrollTop = chatOutput.scrollHeight;
1595
-
1596
- const chatEvent = new CustomEvent(
1597
- 'chat-send-message',
1970
+ const operationId=typeof submissionContext.operationId==='string'
1971
+ &&submissionContext.operationId
1972
+ ?submissionContext.operationId
1973
+ :nextChatOperationId('message');
1974
+ const ownership=createChatSubmissionOwnership(submissionContext.signal??null);
1975
+ const eventContext=Object.freeze(
1598
1976
  {
1599
- detail: {
1977
+ ...submissionContext,
1978
+ operationId,
1979
+ signal:ownership.signal
1980
+ }
1981
+ );
1982
+ if(ownership.signal.aborted){
1983
+ ownership.release();
1984
+ return false;
1985
+ }
1986
+ let accepted;
1987
+ try{
1988
+ accepted=dispatchChatEvent(
1989
+ 'chat-send-message',
1990
+ {
1600
1991
  message:text,
1601
- context:submissionContext
1992
+ context:eventContext
1602
1993
  },
1603
- bubbles: true,
1604
- composed:true,
1605
- cancelable: true
1994
+ {
1995
+ bubbles:true,
1996
+ composed:true,
1997
+ cancelable:true,
1998
+ operationId,
1999
+ publicDetail:{
2000
+ messageLength:text.length,
2001
+ source:eventContext.source,
2002
+ synthetic:eventContext.synthetic===true,
2003
+ reason:chatReasons.messageSubmissionRequested
2004
+ }
2005
+ }
2006
+ );
2007
+ }catch(error){
2008
+ ownership.release();
2009
+ throw error;
2010
+ }
2011
+ if(!accepted){
2012
+ ownership.abort(
2013
+ createChatSubmissionAbort(
2014
+ chatReasons.messageSubmissionCancelled,
2015
+ 'The chat message submission was canceled before delivery.'
2016
+ )
2017
+ );
2018
+ return false;
2019
+ }
2020
+ if(destroyed||ownership.signal.aborted){
2021
+ ownership.release();
2022
+ return false;
2023
+ }
2024
+ try{
2025
+ const messageTime = getMilTime();
2026
+ if(!eventContext.preserveDraft){
2027
+ textArea.value='';
1606
2028
  }
1607
- );
1608
-
1609
- if(host.aiAvailability.tts&&speech.muted===false){
1610
- const aiRuntime=globalThis.ai;
1611
- aiRuntime?.stopAudio?.();
1612
- if(typeof aiRuntime?.resumeAudio==='function'){
1613
- void Promise.resolve(aiRuntime.resumeAudio()).catch(
1614
- reportTTSError
1615
- );
2029
+
2030
+ //console.log(host,host.name);
2031
+ // console.log(forceText,text,1);
2032
+ if(!eventContext.synthetic&&!eventContext.reuseVisibleMessage){
2033
+ hostSubmissionGeneration++;
2034
+ chatOutput.innerHTML += `
2035
+ <li class='user'>
2036
+ <strong>${host.name} ${messageTime}:</strong>
2037
+ <br>
2038
+ <br>
2039
+ <div class='markdown'>
2040
+ ${new MD(text).rendered}
2041
+ </div>
2042
+ </li>
2043
+ `;
1616
2044
  }
1617
- }
2045
+ chatOutput.scrollTop = chatOutput.scrollHeight;
1618
2046
 
1619
- host.dispatchEvent(chatEvent);
2047
+ if(host.aiAvailability.tts&&speech.muted===false){
2048
+ const aiRuntime=globalThis.ai;
2049
+ aiRuntime?.stopAudio?.();
2050
+ if(typeof aiRuntime?.resumeAudio==='function'){
2051
+ void Promise.resolve(aiRuntime.resumeAudio()).catch(
2052
+ reportTTSError
2053
+ );
2054
+ }
2055
+ }
1620
2056
 
1621
- let result;
1622
- try{
1623
- result=host.sendMessage(text,submissionContext);
2057
+ let result;
2058
+ try{
2059
+ result=host.sendMessage(text,eventContext);
2060
+ }catch(error){
2061
+ result=Promise.reject(error);
2062
+ }
2063
+ return observeHostSubmission(result,eventContext,ownership);
1624
2064
  }catch(error){
1625
- result=Promise.reject(error);
2065
+ ownership.release();
2066
+ throw error;
1626
2067
  }
1627
- return observeHostSubmission(result,submissionContext);
1628
2068
  }
1629
2069
 
1630
2070
  async function receivedMessage(name = 'Assistant', text = '', id=''){
@@ -1644,32 +2084,62 @@
1644
2084
  }
1645
2085
 
1646
2086
  function reportTTSError(error){
2087
+ if(destroyed){
2088
+ return false;
2089
+ }
1647
2090
  if(typeof speech.reportTTSError==='function'){
1648
- speech.reportTTSError(error);
1649
- return;
2091
+ try{
2092
+ speech.reportTTSError(error);
2093
+ return true;
2094
+ }catch(reportingError){
2095
+ error=reportingError;
2096
+ }
1650
2097
  }
1651
2098
 
1652
2099
  const message=typeof error?.message==='string'&&error.message.trim()
1653
2100
  ?error.message.trim().slice(0,240)
1654
2101
  :'Speech synthesis failed.';
1655
- host.dispatchEvent(
1656
- new CustomEvent(
1657
- 'chat-speech-synthesis-error',
1658
- {
1659
- bubbles:true,
1660
- composed:true,
1661
- detail:{error,message}
2102
+ dispatchChatEvent(
2103
+ 'chat-speech-synthesis-error',
2104
+ {error,message},
2105
+ {
2106
+ bubbles:true,
2107
+ composed:true,
2108
+ operationId:nextChatOperationId('speech-synthesis'),
2109
+ publicDetail:{
2110
+ reason:chatReasons.speechSynthesisRejected,
2111
+ ...publicErrorFields(
2112
+ error,
2113
+ chatErrorCodes.speechSynthesisRejected
2114
+ )
1662
2115
  }
1663
- )
2116
+ }
1664
2117
  );
1665
2118
  console.error('Speech synthesis failed.');
2119
+ return false;
1666
2120
  }
1667
2121
 
1668
2122
  function destroy(){
1669
- aiRuntimeStateAbortController.abort();
2123
+ if(destroyed){
2124
+ return;
2125
+ }
2126
+ destroyed=true;
2127
+ const destroyReason=createChatSubmissionAbort(
2128
+ chatReasons.componentDestroyed,
2129
+ 'The chat component was destroyed before message submission settled.'
2130
+ );
2131
+ for(const ownership of [...activeSubmissionOwnerships]){
2132
+ ownership.abort(destroyReason);
2133
+ }
2134
+ aiRuntimeStateAbortController.abort(destroyReason);
1670
2135
  aiActivationController.destroy();
1671
2136
  window.removeEventListener('pagehide',destroy);
2137
+ conversationTimeboxUnsubscribe?.();
2138
+ conversationTimeboxUnsubscribe=null;
2139
+ conversationTimeboxController=null;
2140
+ initialSpeechMuteListenerInstalled=false;
1672
2141
  speech.destroy?.();
2142
+ events.dispose();
1673
2143
  host.ready=false;
1674
2144
  }
1675
2145