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
@@ -5,31 +5,34 @@ import {
5
5
  } from 'arcane-os/event-manager';
6
6
 
7
7
  const DEFAULT_DELAY_MS=2_000;
8
- const DEFAULT_MAX_REPORTS_PER_SESSION=10;
9
- const DEFAULT_MAX_REPORTS_PER_WINDOW=3;
10
- const DEFAULT_MAX_PENDING_INCIDENTS=25;
11
- const DEFAULT_RATE_WINDOW_MS=60_000;
12
8
  const LEDGER_STORAGE_KEY='arcane-global-errors-v1';
13
- const MAX_DETAIL_LENGTH=8_000;
14
9
  const HANDLER_MARKER=Symbol.for('arcane.global-errors.handler');
15
10
  const DEVELOPER_MODAL_HREF=new URL('../components/modal.html?v=13',import.meta.url).href;
16
-
17
- export const GLOBAL_ERROR_EVENT_TYPES=Object.freeze({
11
+ const ERROR_EVENT_TYPES={
18
12
  browserErrorCaptured:'arcane-error-captured',
19
13
  unhandledRejectionCaptured:'arcane-unhandled-rejection-captured'
20
- });
21
- export const GLOBAL_ERROR_EVENT_CODES=Object.freeze({
14
+ };
15
+ const ERROR_EVENT_CODES={
22
16
  browserErrorCaptured:'ARCANE_BROWSER_ERROR_CAPTURED',
23
17
  unhandledRejectionCaptured:'ARCANE_UNHANDLED_REJECTION_CAPTURED'
24
- });
25
- export const GLOBAL_ERROR_REASONS=Object.freeze({
18
+ };
19
+ const ERROR_REASONS={
26
20
  browserErrorCaptured:'browser-error-captured',
27
21
  unhandledRejectionCaptured:'unhandled-promise-rejection-captured'
28
- });
22
+ };
23
+ const RUNTIME_OCCURRENCE_PREFIX=(
24
+ typeof globalThis.crypto?.randomUUID==='function'
25
+ ? globalThis.crypto.randomUUID()
26
+ : `${Date.now().toString(36)}-${Math.random().toString(36).replace(/^0\./,'')}`
27
+ );
28
+ let runtimeOccurrenceSequence=0;
29
+
30
+ export const GLOBAL_ERROR_EVENT_TYPES={...ERROR_EVENT_TYPES};
31
+ export const GLOBAL_ERROR_EVENT_CODES={...ERROR_EVENT_CODES};
32
+ export const GLOBAL_ERROR_REASONS={...ERROR_REASONS};
29
33
 
30
34
  const MESSAGE_STYLE=[
31
35
  'Write a simple plain-text email showing the error and, when the available details support it, a possible solution.',
32
- 'If loop_detected or error_storm_detected is true, put that warning first and clearly state that further notifications have been suppressed.',
33
36
  'Do not add facts that are not present in the report data.',
34
37
  ].join(' ');
35
38
 
@@ -40,7 +43,7 @@ function safeText(value,fallback=''){
40
43
 
41
44
  try{
42
45
  const text=typeof value==='string' ? value:String(value);
43
- return text.slice(0,MAX_DETAIL_LENGTH)||fallback;
46
+ return text||fallback;
44
47
  }catch{
45
48
  return fallback;
46
49
  }
@@ -126,92 +129,17 @@ export function normalizeRejectionEvent(event={},target=globalThis.window){
126
129
  };
127
130
  }
128
131
 
129
- function normalizeFingerprintPart(value){
130
- return safeText(value)
131
- .trim()
132
- .replace(/\s+/g,' ')
133
- .slice(0,2_000);
134
- }
135
-
136
- function normalizeVolatileFingerprintPart(value){
137
- return normalizeFingerprintPart(value)
138
- .replace(/[?#][^\s)]+/g,'')
139
- .replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi,'<uuid>')
140
- .replace(/\b[0-9a-f]{16,}\b/gi,'<hex>')
141
- .replace(/\b\d+(?:\.\d+)?\b/g,'<number>');
142
- }
143
-
144
- function normalizeStackFingerprint(value,name,message){
145
- const stack=safeText(value).trim();
146
- if(!stack){
147
- return '';
148
- }
149
-
150
- const lines=stack.split(/\r?\n/);
151
- const firstLine=lines[0].trim();
152
- const errorName=safeText(name).trim();
153
- const errorMessage=safeText(message).trim();
154
- const hasMessageHeader=lines.length>1&&(
155
- firstLine===errorMessage
156
- || firstLine===`${errorName}: ${errorMessage}`
157
- || (errorName&&firstLine.startsWith(`${errorName}:`))
158
- || /^(?:Error|[A-Za-z_$][\w.$]*(?:Error|Exception)):(?:\s|$)/.test(firstLine)
159
- );
160
- const frames=hasMessageHeader ? lines.slice(1):lines;
161
-
162
- return normalizeFingerprintPart(frames.join('\n'))
163
- .replace(/[?#][^\s)]+/g,'')
164
- .replace(/\b[0-9a-f]{8}-[0-9a-f-]{27,}\b/gi,'<uuid>')
165
- .replace(/\b[0-9a-f]{16,}\b/gi,'<hex>');
166
- }
167
-
168
- function hashText(value){
169
- let first=0x811c9dc5;
170
- let second=0x9e3779b9;
171
-
172
- for(let index=0;index<value.length;index++){
173
- const code=value.charCodeAt(index);
174
- first=Math.imul(first^code,0x01000193);
175
- second=Math.imul(second^code,0x85ebca6b);
176
- }
177
-
178
- return [first,second]
179
- .map(hash => (hash>>>0).toString(16).padStart(8,'0'))
180
- .join('');
181
- }
182
-
183
132
  /**
184
- * Build a stable call-site signature. When a precise source location is
185
- * available, the human-readable message is deliberately excluded so loops
186
- * with changing counters, names, or timestamps still produce one report.
133
+ * Return a fresh opaque occurrence identifier for legacy callers of the former
134
+ * fingerprint helper. The incident is intentionally not read: identifiers do
135
+ * not group, admit, or derive identity from error content.
187
136
  *
188
137
  * @param {Object} incident
189
138
  * @returns {string}
190
139
  */
191
- export function fingerprintIncident(incident){
192
- const stackFingerprint=normalizeStackFingerprint(
193
- incident?.stack,
194
- incident?.name,
195
- incident?.message
196
- );
197
- const hasPreciseSource=(
198
- Number.isFinite(incident?.lineno)
199
- || Number.isFinite(incident?.colno)
200
- || Boolean(stackFingerprint)
201
- );
202
- const signature=[
203
- normalizeFingerprintPart(incident?.type),
204
- normalizeFingerprintPart(incident?.name),
205
- hasPreciseSource
206
- ? ''
207
- : normalizeVolatileFingerprintPart(incident?.message),
208
- normalizeFingerprintPart(incident?.filename).replace(/[?#].*$/,''),
209
- normalizeFingerprintPart(incident?.lineno),
210
- normalizeFingerprintPart(incident?.colno),
211
- stackFingerprint,
212
- ].join('|');
213
-
214
- return `error-${hashText(signature)}`;
140
+ export function fingerprintIncident(_incident){
141
+ runtimeOccurrenceSequence+=1;
142
+ return `error-${RUNTIME_OCCURRENCE_PREFIX}-${runtimeOccurrenceSequence.toString(36)}`;
215
143
  }
216
144
 
217
145
  function defaultStorage(target){
@@ -262,7 +190,7 @@ function appendDeveloperDetail(document,list,label,value){
262
190
  list.append(term,description);
263
191
  }
264
192
 
265
- function buildDeveloperIncidentContent(document,incident,fingerprint){
193
+ function buildDeveloperIncidentContent(document,incident,occurrenceId){
266
194
  const content=document.createElement('section');
267
195
  const heading=document.createElement('h2');
268
196
  const introduction=document.createElement('p');
@@ -282,7 +210,7 @@ function buildDeveloperIncidentContent(document,incident,fingerprint){
282
210
  appendDeveloperDetail(document,details,'Name',incident?.name);
283
211
  appendDeveloperDetail(document,details,'Message',incident?.message);
284
212
  appendDeveloperDetail(document,details,'Source',source);
285
- appendDeveloperDetail(document,details,'Fingerprint',fingerprint);
213
+ appendDeveloperDetail(document,details,'Occurrence',occurrenceId);
286
214
 
287
215
  content.append(heading,introduction,details);
288
216
 
@@ -324,7 +252,7 @@ async function ensureHTMLImport(target){
324
252
  }
325
253
  }
326
254
 
327
- async function presentDeveloperIncidentModal(target,incident,fingerprint){
255
+ async function presentDeveloperIncidentModal(target,incident,occurrenceId){
328
256
  const document=target?.document;
329
257
  const container=document?.body||document?.documentElement;
330
258
 
@@ -335,11 +263,11 @@ async function presentDeveloperIncidentModal(target,incident,fingerprint){
335
263
  await ensureHTMLImport(target);
336
264
 
337
265
  const modal=document.createElement('html-import');
338
- const content=buildDeveloperIncidentContent(document,incident,fingerprint);
266
+ const content=buildDeveloperIncidentContent(document,incident,occurrenceId);
339
267
 
340
268
  modal.className='modal developer-error-modal';
341
269
  modal.setAttribute('aria-label','Application error');
342
- modal.setAttribute('data-global-error-modal',fingerprint);
270
+ modal.setAttribute('data-global-error-modal',occurrenceId);
343
271
  modal.setAttribute('data-once','');
344
272
  modal.setAttribute('href',DEVELOPER_MODAL_HREF);
345
273
 
@@ -409,26 +337,14 @@ class Errors {
409
337
 
410
338
  this.#events=createArcaneEventSource(this,{
411
339
  source:'global-error-handler',
412
- eventTypes:Object.freeze([
413
- GLOBAL_ERROR_EVENT_TYPES.browserErrorCaptured,
414
- GLOBAL_ERROR_EVENT_TYPES.unhandledRejectionCaptured
415
- ])
340
+ eventTypes:[
341
+ ERROR_EVENT_TYPES.browserErrorCaptured,
342
+ ERROR_EVENT_TYPES.unhandledRejectionCaptured
343
+ ]
416
344
  });
417
345
  this[HANDLER_MARKER]=true;
418
346
  this.target=target;
419
347
  this.delayMs=options.delayMs??DEFAULT_DELAY_MS;
420
- this.deliveryTimeoutMs=options.deliveryTimeoutMs??45_000;
421
- this.deliverySchedule=options.deliverySchedule
422
- || globalThis.setTimeout.bind(globalThis);
423
- this.deliveryCancel=options.deliveryCancel
424
- || globalThis.clearTimeout.bind(globalThis);
425
- this.maxPendingIncidents=options.maxPendingIncidents
426
- ?? DEFAULT_MAX_PENDING_INCIDENTS;
427
- this.maxReportsPerSession=options.maxReportsPerSession
428
- ?? DEFAULT_MAX_REPORTS_PER_SESSION;
429
- this.maxReportsPerWindow=options.maxReportsPerWindow
430
- ?? DEFAULT_MAX_REPORTS_PER_WINDOW;
431
- this.rateWindowMs=options.rateWindowMs??DEFAULT_RATE_WINDOW_MS;
432
348
  this.now=options.now||Date.now;
433
349
  this.schedule=options.schedule||globalThis.setTimeout.bind(globalThis);
434
350
  this.cancel=options.cancel||globalThis.clearTimeout.bind(globalThis);
@@ -452,21 +368,15 @@ class Errors {
452
368
  this.pending=new Map();
453
369
  this.deliveryQueue=Promise.resolve();
454
370
  this.destroyed=false;
455
- this.invokingMail=false;
456
- this.deferredDeveloperIncident=null;
457
- this.developerModalActive=false;
458
- this.developerUiDisabled=false;
459
- this.developerShownFingerprints=new Set();
371
+ this.developerIncidentQueue=[];
372
+ this.developerPresentationActive=false;
460
373
  this.waitingForUser=false;
461
374
 
462
375
  const ledger=this.loadLedger();
463
- this.reported=new Set(ledger.fingerprints);
464
- this.reportTimestamps=ledger.reportTimestamps;
465
- this.circuitOpen=ledger.circuitOpen||!this.storageHealthy;
466
376
 
467
377
  if(!this.storageHealthy){
468
378
  this.warn(
469
- 'Persistent suppression storage is unavailable; error email notifications are disabled to prevent reload loops.'
379
+ 'Pending error delivery storage is unavailable; delivery will continue in memory.'
470
380
  );
471
381
  }
472
382
 
@@ -498,12 +408,7 @@ class Errors {
498
408
  }
499
409
 
500
410
  loadLedger(){
501
- const empty={
502
- fingerprints:[],
503
- pending:[],
504
- reportTimestamps:[],
505
- circuitOpen:false,
506
- };
411
+ const empty={ pending:[] };
507
412
 
508
413
  if(!this.storageHealthy){
509
414
  return empty;
@@ -516,25 +421,12 @@ class Errors {
516
421
  }
517
422
 
518
423
  return {
519
- fingerprints:Array.isArray(value.fingerprints)
520
- ? value.fingerprints.filter(item => typeof item==='string')
521
- : [],
522
424
  pending:Array.isArray(value.pending)
523
- ? value.pending.filter(record => (
524
- record
525
- && typeof record==='object'
526
- && typeof record.fingerprint==='string'
527
- && record.incident
528
- && typeof record.incident==='object'
529
- ))
530
- : [],
531
- reportTimestamps:Array.isArray(value.reportTimestamps)
532
- ? value.reportTimestamps.filter(Number.isFinite)
533
- : [],
534
- circuitOpen:value.circuitOpen===true,
425
+ ? value.pending
426
+ : []
535
427
  };
536
- }catch{
537
- this.storageHealthy=false;
428
+ }catch(error){
429
+ this.warn('Unable to restore pending error deliveries.',error);
538
430
  return empty;
539
431
  }
540
432
  }
@@ -545,50 +437,22 @@ class Errors {
545
437
  }
546
438
 
547
439
  try{
548
- const serialized=JSON.stringify({
549
- fingerprints:[...this.reported],
440
+ this.storage.setItem(LEDGER_STORAGE_KEY,JSON.stringify({
550
441
  pending:[...this.pending.values()].map(record => ({
551
- count:record.count,
442
+ capturedAt:record.capturedAt,
552
443
  dueAt:record.dueAt,
553
- errorStormDetected:record.errorStormDetected,
554
- fingerprint:record.fingerprint,
555
- firstSeen:record.firstSeen,
556
444
  incident:record.incident,
557
- lastSeen:record.lastSeen,
558
- uniqueIncidentCount:record.uniqueIncidentCount,
559
- })),
560
- reportTimestamps:this.reportTimestamps,
561
- circuitOpen:this.circuitOpen,
562
- });
563
- this.storage.setItem(LEDGER_STORAGE_KEY,serialized);
564
- if(this.storage.getItem(LEDGER_STORAGE_KEY)!==serialized){
565
- throw new Error('Suppression ledger verification failed');
566
- }
445
+ occurrenceId:record.occurrenceId,
446
+ retryRequired:record.retryRequired===true
447
+ }))
448
+ }));
567
449
  return true;
568
450
  }catch(error){
569
- this.disableForStorageFailure(error);
451
+ this.warn('Unable to persist pending error deliveries.',error);
570
452
  return false;
571
453
  }
572
454
  }
573
455
 
574
- disableForStorageFailure(error){
575
- const shouldWarn=this.storageHealthy;
576
- this.storageHealthy=false;
577
- this.circuitOpen=true;
578
-
579
- for(const record of this.pending.values()){
580
- this.cancelTimer(record);
581
- }
582
- this.pending.clear();
583
-
584
- if(shouldWarn){
585
- this.warn(
586
- 'Suppression storage failed; error email notifications are disabled to prevent reload loops.',
587
- error
588
- );
589
- }
590
- }
591
-
592
456
  cancelTimer(record){
593
457
  if(record?.timer===null||record?.timer===undefined){
594
458
  return;
@@ -608,7 +472,7 @@ class Errors {
608
472
  record.timer=this.schedule(
609
473
  () => {
610
474
  try{
611
- this.flushFingerprint(record.fingerprint);
475
+ this.flushOccurrence(record.occurrenceId);
612
476
  }catch(error){
613
477
  this.warn('Unable to flush a scheduled error notification.',error);
614
478
  }
@@ -617,45 +481,61 @@ class Errors {
617
481
  );
618
482
  return true;
619
483
  }catch(error){
620
- this.pending.delete(record.fingerprint);
621
484
  this.warn('Unable to schedule an error notification.',error);
485
+ record.timer=null;
486
+ this.flushOccurrence(record.occurrenceId);
622
487
  return false;
623
488
  }
624
489
  }
625
490
 
626
491
  restorePending(records){
627
- if(this.circuitOpen||!Array.isArray(records)){
492
+ if(!Array.isArray(records)){
628
493
  return;
629
494
  }
630
495
 
631
496
  const timestamp=this.now();
632
- for(const storedRecord of records.slice(0,this.maxPendingIncidents)){
633
- if(this.reported.has(storedRecord.fingerprint)){
497
+ for(const storedRecord of records){
498
+ if(
499
+ !storedRecord
500
+ || typeof storedRecord!=='object'
501
+ || !storedRecord.incident
502
+ || typeof storedRecord.incident!=='object'
503
+ ){
504
+ this.warn('A persisted error delivery could not be restored.');
634
505
  continue;
635
506
  }
636
507
 
637
- const firstSeen=Number.isFinite(storedRecord.firstSeen)
638
- ? storedRecord.firstSeen:timestamp;
508
+ const capturedAt=Number.isFinite(storedRecord.capturedAt)
509
+ ? storedRecord.capturedAt
510
+ : Number.isFinite(storedRecord.firstSeen)
511
+ ? storedRecord.firstSeen
512
+ : timestamp;
639
513
  const dueAt=Number.isFinite(storedRecord.dueAt)
640
- ? storedRecord.dueAt:firstSeen+this.delayMs;
514
+ ? storedRecord.dueAt:capturedAt+this.delayMs;
515
+ let occurrenceId=(
516
+ typeof storedRecord.occurrenceId==='string'
517
+ && storedRecord.occurrenceId.trim()
518
+ )
519
+ ? storedRecord.occurrenceId
520
+ : fingerprintIncident(storedRecord.incident);
521
+ while(this.pending.has(occurrenceId)){
522
+ occurrenceId=fingerprintIncident(storedRecord.incident);
523
+ }
641
524
  const record={
642
- count:Number.isFinite(storedRecord.count)
643
- ? Math.max(1,storedRecord.count):1,
525
+ capturedAt,
526
+ delivering:false,
644
527
  dueAt,
645
- errorStormDetected:storedRecord.errorStormDetected===true,
646
- fingerprint:storedRecord.fingerprint,
647
- firstSeen,
648
528
  incident:storedRecord.incident,
649
- lastSeen:Number.isFinite(storedRecord.lastSeen)
650
- ? storedRecord.lastSeen:firstSeen,
651
- timer:null,
652
- uniqueIncidentCount:Number.isFinite(storedRecord.uniqueIncidentCount)
653
- ? Math.max(1,storedRecord.uniqueIncidentCount):1,
529
+ occurrenceId,
530
+ retryRequired:storedRecord.retryRequired===true,
531
+ timer:null
654
532
  };
655
533
 
656
- this.pending.set(record.fingerprint,record);
657
- this.scheduleRecord(record,dueAt-timestamp);
658
- this.offerDeveloperIncident(record.incident,record.fingerprint);
534
+ this.pending.set(record.occurrenceId,record);
535
+ if(!record.retryRequired){
536
+ this.scheduleRecord(record,dueAt-timestamp);
537
+ }
538
+ this.offerDeveloperIncident(record.incident,record.occurrenceId);
659
539
  }
660
540
 
661
541
  this.persistLedger();
@@ -690,15 +570,11 @@ class Errors {
690
570
  return this.isDeveloperMode();
691
571
  }catch(error){
692
572
  this.warn('Unable to read the developer-mode preference.',error);
693
- return false;
573
+ return null;
694
574
  }
695
575
  }
696
576
 
697
- deferDeveloperIncident(incident,fingerprint){
698
- if(!this.deferredDeveloperIncident){
699
- this.deferredDeveloperIncident={ fingerprint,incident };
700
- }
701
-
577
+ waitForDeveloperPreference(){
702
578
  if(this.waitingForUser){
703
579
  return;
704
580
  }
@@ -714,306 +590,191 @@ class Errors {
714
590
  onUserLoaded(){
715
591
  this.#stopUserLoaded=null;
716
592
  this.waitingForUser=false;
717
-
718
- const deferred=this.deferredDeveloperIncident;
719
- this.deferredDeveloperIncident=null;
720
-
721
- if(deferred){
722
- this.offerDeveloperIncident(
723
- deferred.incident,
724
- deferred.fingerprint,
725
- false
726
- );
727
- }
593
+ this.drainDeveloperIncidents();
728
594
  }
729
595
 
730
- offerDeveloperIncident(incident,fingerprint,allowDefer=true){
596
+ drainDeveloperIncidents(){
731
597
  if(
732
598
  this.destroyed
733
- || this.developerUiDisabled
734
- || this.developerModalActive
735
- || this.developerShownFingerprints.has(fingerprint)
599
+ || this.developerPresentationActive
600
+ || this.developerIncidentQueue.length===0
736
601
  ){
737
602
  return false;
738
603
  }
739
604
 
740
605
  const developerMode=this.readDeveloperMode();
606
+ if(developerMode===null||developerMode===undefined){
607
+ this.waitForDeveloperPreference();
608
+ return false;
609
+ }
610
+
741
611
  if(developerMode!==true){
742
- if(allowDefer&&(developerMode===null||developerMode===undefined)){
743
- this.deferDeveloperIncident(incident,fingerprint);
744
- }
612
+ this.developerIncidentQueue.length=0;
745
613
  return false;
746
614
  }
747
615
 
748
- this.developerShownFingerprints.add(fingerprint);
749
- this.developerModalActive=true;
616
+ const next=this.developerIncidentQueue.shift();
617
+ this.developerPresentationActive=true;
618
+ Promise.resolve()
619
+ .then(() => this.presentDeveloperIncident(
620
+ next.incident,
621
+ next.occurrenceId
622
+ ))
623
+ .catch(error => {
624
+ this.warn('Developer error display failed.',error);
625
+ })
626
+ .finally(() => {
627
+ this.developerPresentationActive=false;
628
+ this.drainDeveloperIncidents();
629
+ });
750
630
 
751
- let presentation;
752
- try{
753
- presentation=this.presentDeveloperIncident(incident,fingerprint);
754
- }catch(error){
755
- this.developerModalActive=false;
756
- this.developerUiDisabled=true;
757
- this.warn('Developer error display failed and has been disabled for this session.',error);
631
+ return true;
632
+ }
633
+
634
+ offerDeveloperIncident(incident,occurrenceId){
635
+ if(this.destroyed){
758
636
  return false;
759
637
  }
760
638
 
761
- Promise.resolve(presentation).then(
762
- () => {
763
- this.developerModalActive=false;
764
- },
765
- error => {
766
- this.developerModalActive=false;
767
- this.developerUiDisabled=true;
768
- this.warn('Developer error display failed and has been disabled for this session.',error);
769
- }
770
- );
639
+ const developerMode=this.readDeveloperMode();
640
+ if(developerMode===false){
641
+ return false;
642
+ }
771
643
 
644
+ this.developerIncidentQueue.push({ incident,occurrenceId });
645
+ if(developerMode===null||developerMode===undefined){
646
+ this.waitForDeveloperPreference();
647
+ return true;
648
+ }
649
+
650
+ this.drainDeveloperIncidents();
772
651
  return true;
773
652
  }
774
653
 
775
654
  capture(incident){
776
- if(this.destroyed||this.invokingMail){
655
+ if(this.destroyed){
777
656
  return false;
778
657
  }
779
658
 
780
- const fingerprint=fingerprintIncident(incident);
781
- if(this.reported.has(fingerprint)){
782
- return false;
659
+ let occurrenceId=fingerprintIncident(incident);
660
+ while(this.pending.has(occurrenceId)){
661
+ occurrenceId=fingerprintIncident(incident);
783
662
  }
784
-
785
663
  const incidentKind=incident?.type==='unhandledrejection'
786
664
  ?'unhandled-promise-rejection'
787
665
  :'browser-error';
788
666
  const eventType=incidentKind==='unhandled-promise-rejection'
789
- ?GLOBAL_ERROR_EVENT_TYPES.unhandledRejectionCaptured
790
- :GLOBAL_ERROR_EVENT_TYPES.browserErrorCaptured;
667
+ ?ERROR_EVENT_TYPES.unhandledRejectionCaptured
668
+ :ERROR_EVENT_TYPES.browserErrorCaptured;
791
669
  const code=incidentKind==='unhandled-promise-rejection'
792
- ?GLOBAL_ERROR_EVENT_CODES.unhandledRejectionCaptured
793
- :GLOBAL_ERROR_EVENT_CODES.browserErrorCaptured;
670
+ ?ERROR_EVENT_CODES.unhandledRejectionCaptured
671
+ :ERROR_EVENT_CODES.browserErrorCaptured;
794
672
  const reason=incidentKind==='unhandled-promise-rejection'
795
- ?GLOBAL_ERROR_REASONS.unhandledRejectionCaptured
796
- :GLOBAL_ERROR_REASONS.browserErrorCaptured;
673
+ ?ERROR_REASONS.unhandledRejectionCaptured
674
+ :ERROR_REASONS.browserErrorCaptured;
797
675
  this.#operationSequence+=1;
798
676
  this.#events.dispatch(
799
677
  eventType,
800
- Object.freeze({...incident,code,fingerprint,reason}),
678
+ {id:occurrenceId,fingerprint:occurrenceId,code,kind:incidentKind,reason},
801
679
  {
802
680
  operationId:`global-error-handler-${this.#events.instanceId}-${this.#operationSequence.toString(36)}`,
803
- publicDetail:Object.freeze({
804
- id:fingerprint,
681
+ publicDetail:{
682
+ id:occurrenceId,
683
+ fingerprint:occurrenceId,
805
684
  code,
806
685
  kind:incidentKind,
807
686
  reason
808
- })
687
+ }
809
688
  }
810
689
  );
811
690
 
812
- this.offerDeveloperIncident(incident,fingerprint);
813
-
814
- if(this.circuitOpen){
815
- return false;
816
- }
691
+ this.offerDeveloperIncident(incident,occurrenceId);
817
692
 
818
693
  const timestamp=this.now();
819
- const existing=this.pending.get(fingerprint);
820
- if(existing){
821
- existing.count++;
822
- existing.lastSeen=timestamp;
823
- if(existing.count===2){
824
- return this.persistLedger();
825
- }
826
- return true;
827
- }
828
-
829
- if(this.pending.size>=this.maxPendingIncidents){
830
- const stormRecord=this.pending.values().next().value;
831
- if(stormRecord){
832
- const stormAlreadyDetected=stormRecord.errorStormDetected;
833
- stormRecord.errorStormDetected=true;
834
- stormRecord.lastSeen=timestamp;
835
- stormRecord.uniqueIncidentCount=stormAlreadyDetected
836
- ? stormRecord.uniqueIncidentCount+1
837
- : this.pending.size+1;
838
- this.persistLedger();
839
- }
840
- return false;
841
- }
842
-
843
694
  const record={
844
- count:1,
695
+ capturedAt:timestamp,
696
+ delivering:false,
845
697
  dueAt:timestamp+this.delayMs,
846
- errorStormDetected:false,
847
- fingerprint,
848
- firstSeen:timestamp,
849
698
  incident,
850
- lastSeen:timestamp,
851
- timer:null,
852
- uniqueIncidentCount:1,
699
+ occurrenceId,
700
+ retryRequired:false,
701
+ timer:null
853
702
  };
854
703
 
855
- this.pending.set(fingerprint,record);
856
- // This is intentionally a fixed window from the first occurrence.
857
- // Resetting the timer on every repeat could postpone a true loop forever.
858
- if(!this.scheduleRecord(record,this.delayMs)){
859
- return false;
860
- }
861
-
862
- return this.persistLedger();
863
- }
864
-
865
- reserveDelivery(timestamp){
866
- const cutoff=timestamp-this.rateWindowMs;
867
- const recentReportTimestamps=this.reportTimestamps.filter(
868
- reportTimestamp => reportTimestamp>=cutoff
869
- );
870
-
871
- if(
872
- this.reportTimestamps.length>=this.maxReportsPerSession
873
- || recentReportTimestamps.length>=this.maxReportsPerWindow
874
- ){
875
- this.openCircuit();
876
- return { allowed:false,circuitOpened:true };
877
- }
878
-
879
- this.reportTimestamps.push(timestamp);
880
-
881
- const circuitOpened=(
882
- this.reportTimestamps.length>=this.maxReportsPerSession
883
- || recentReportTimestamps.length+1>=this.maxReportsPerWindow
884
- );
885
-
886
- if(circuitOpened){
887
- this.openCircuit();
888
- }
889
-
890
- return { allowed:true,circuitOpened };
891
- }
892
-
893
- openCircuit(){
894
- this.circuitOpen=true;
895
-
896
- for(const record of this.pending.values()){
897
- this.cancelTimer(record);
898
- }
899
-
900
- this.pending.clear();
704
+ this.pending.set(occurrenceId,record);
901
705
  this.persistLedger();
706
+ this.scheduleRecord(record,this.delayMs);
707
+ return true;
902
708
  }
903
709
 
904
- flushFingerprint(fingerprint){
905
- const record=this.pending.get(fingerprint);
906
- if(!record){
907
- return;
710
+ flushOccurrence(occurrenceId){
711
+ const record=this.pending.get(occurrenceId);
712
+ if(!record||record.delivering){
713
+ return false;
908
714
  }
909
715
 
910
- this.pending.delete(fingerprint);
911
716
  this.cancelTimer(record);
912
-
913
- if(this.destroyed||this.circuitOpen||this.reported.has(fingerprint)){
717
+ if(this.destroyed){
914
718
  this.persistLedger();
915
- return;
916
- }
917
-
918
- // Suppress before performing any fallible notification work. A failed
919
- // mail request is an attempted report and must never be retried recursively.
920
- this.reported.add(fingerprint);
921
- const reservation=this.reserveDelivery(this.now());
922
- if(record.errorStormDetected&&reservation.allowed&&!reservation.circuitOpened){
923
- reservation.circuitOpened=true;
924
- this.openCircuit();
925
- }
926
- const persisted=this.persistLedger();
927
-
928
- if(!reservation.allowed||!persisted){
929
- return;
719
+ return false;
930
720
  }
931
721
 
722
+ record.delivering=true;
932
723
  this.deliveryQueue=this.deliveryQueue
933
- .then(() => this.deliver(record,reservation))
724
+ .then(async () => {
725
+ if(this.destroyed){
726
+ record.retryRequired=true;
727
+ return false;
728
+ }
729
+ await this.deliver(record);
730
+ return true;
731
+ })
732
+ .then(delivered => {
733
+ if(!delivered){
734
+ record.delivering=false;
735
+ this.persistLedger();
736
+ return;
737
+ }
738
+ if(this.pending.get(occurrenceId)===record){
739
+ this.pending.delete(occurrenceId);
740
+ }
741
+ record.delivering=false;
742
+ this.persistLedger();
743
+ })
934
744
  .catch(error => {
935
- this.warn('Unexpected notification-queue failure; no retry will be attempted.',error);
745
+ record.delivering=false;
746
+ record.retryRequired=true;
747
+ this.warn('Error notification failed; delivery remains pending for retry.',error);
748
+ this.persistLedger();
936
749
  });
750
+ return true;
937
751
  }
938
752
 
939
- buildNotification(record,reservation){
940
- const loopDetected=record.count>1;
941
- const errorStormDetected=record.errorStormDetected
942
- || reservation.circuitOpened;
943
- const baseSubject=record.incident.type==='unhandledrejection'
753
+ flushFingerprint(fingerprint){
754
+ return this.flushOccurrence(fingerprint);
755
+ }
756
+
757
+ buildNotification(record){
758
+ const subject=record.incident?.type==='unhandledrejection'
944
759
  ? 'ARCANE JS UNHANDLED REJECTION'
945
760
  : 'ARCANE JS ERROR';
946
-
947
- let subject=baseSubject;
948
- if(errorStormDetected){
949
- subject=`${baseSubject} - ERROR STORM DETECTED`;
950
- }else if(loopDetected){
951
- subject=`${baseSubject} - LOOP DETECTED`;
952
- }
953
-
954
761
  const payload={
955
762
  ...record.incident,
956
- fingerprint:record.fingerprint,
957
- occurrence_count:record.count,
958
- first_seen_at:safeIso(record.firstSeen),
959
- last_seen_at:safeIso(record.lastSeen),
960
- observation_window_ms:this.delayMs,
961
- loop_detected:loopDetected,
962
- error_storm_detected:errorStormDetected,
963
- unique_incident_count:record.uniqueIncidentCount,
964
- matching_notifications_suppressed:true,
965
- loop_notice:loopDetected
966
- ? `This error repeated ${record.count} times during the ${this.delayMs}ms observation window. Further matching notifications are suppressed for this browser session.`
967
- : 'Further occurrences of this same error are suppressed for this browser session.',
968
- circuit_breaker_notice:errorStormDetected
969
- ? 'The global notification limit was reached. Further error emails are suppressed for this browser session to stop a possible varying error loop.'
970
- : null,
763
+ captured_at:safeIso(record.capturedAt),
764
+ occurrence_id:record.occurrenceId
971
765
  };
972
766
 
973
767
  return { payload,subject };
974
768
  }
975
769
 
976
- async deliver(record,reservation){
977
- const { payload,subject }=this.buildNotification(record,reservation);
978
- let timeout=null;
979
- const delivery=Promise.resolve().then(() => {
980
- // Only suppress events emitted synchronously by the adapter call.
981
- // Its returned promise is observed below, so unrelated errors that
982
- // occur while network delivery is pending must still be captured.
983
- this.invokingMail=true;
984
- try{
985
- return this.sendMail([],subject,payload,MESSAGE_STYLE,'error');
986
- }finally{
987
- this.invokingMail=false;
988
- }
989
- });
990
-
991
- try{
992
- await Promise.race([
993
- delivery,
994
- new Promise((resolve,reject) => {
995
- timeout=this.deliverySchedule(
996
- () => reject(new Error('Error notification timed out')),
997
- this.deliveryTimeoutMs
998
- );
999
- }),
1000
- ]);
1001
- }catch(error){
1002
- this.warn('Error notification failed; no retry will be attempted.',error);
1003
- }finally{
1004
- if(timeout!==null){
1005
- try{
1006
- this.deliveryCancel(timeout);
1007
- }catch(error){
1008
- this.warn('Unable to cancel the notification timeout.',error);
1009
- }
1010
- }
1011
- }
770
+ async deliver(record){
771
+ const { payload,subject }=this.buildNotification(record);
772
+ await this.sendMail([],subject,payload,MESSAGE_STYLE,'error');
1012
773
  }
1013
774
 
1014
775
  async flush(){
1015
- for(const fingerprint of [...this.pending.keys()]){
1016
- this.flushFingerprint(fingerprint);
776
+ for(const occurrenceId of [...this.pending.keys()]){
777
+ this.flushOccurrence(occurrenceId);
1017
778
  }
1018
779
 
1019
780
  await this.whenIdle();
@@ -1034,7 +795,7 @@ class Errors {
1034
795
  this.#stopUserLoaded?.();
1035
796
  this.#stopUserLoaded=null;
1036
797
  this.waitingForUser=false;
1037
- this.deferredDeveloperIncident=null;
798
+ this.developerIncidentQueue.length=0;
1038
799
 
1039
800
  try{
1040
801
  const modal=this.target.document?.querySelector?.(
@@ -1061,7 +822,6 @@ class Errors {
1061
822
  this.cancelTimer(record);
1062
823
  }
1063
824
 
1064
- this.pending.clear();
1065
825
  this.persistLedger();
1066
826
 
1067
827
  if(this.target.errors===this){