arcane-os 0.3.4 → 0.3.6

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 (35) 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 +469 -220
  8. package/runtime/arcane/components/speech.html +33 -13
  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 +776 -151
  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 +23 -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/installed-sdk-runtime.mjs +11 -1
@@ -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) {
@@ -1693,6 +1692,26 @@
1693
1692
  keyboardRecording = false;
1694
1693
  }
1695
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
+
1696
1715
  function destroy() {
1697
1716
  if (destroyed) {
1698
1717
  return;
@@ -1701,7 +1720,8 @@
1701
1720
  destroyed = true;
1702
1721
  runtimeStateAbortController.abort();
1703
1722
  sttActivationController.destroy();
1704
- window.removeEventListener('pagehide', destroy);
1723
+ window.removeEventListener('pagehide', handlePageHide);
1724
+ window.removeEventListener('pageshow', handlePageShow);
1705
1725
  microphonePermissionStatus?.removeEventListener?.(
1706
1726
  'change',
1707
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
  }
@@ -7,10 +7,9 @@ import {normalizeMemoryContent} from '../modules/MemoryRecords.js';
7
7
  const is = new Is(false);
8
8
 
9
9
  function plainRecord(value){
10
- return Boolean(value)
11
- &&typeof value==='object'
12
- &&!Array.isArray(value)
13
- &&Object.getPrototypeOf(value)===Object.prototype;
10
+ if(!value||typeof value!=='object'||Array.isArray(value))return false;
11
+ const prototype=Object.getPrototypeOf(value);
12
+ return prototype===Object.prototype||prototype===null;
14
13
  }
15
14
 
16
15
  function coded(error,code){
@@ -18,15 +17,32 @@ function coded(error,code){
18
17
  return error;
19
18
  }
20
19
 
20
+ function copyCompleteValue(value,seen=new Map()){
21
+ if(value===null||typeof value!=='object')return value;
22
+ if(seen.has(value))return seen.get(value);
23
+ if(Array.isArray(value)){
24
+ const result=[];
25
+ seen.set(value,result);
26
+ for(const item of value)result.push(copyCompleteValue(item,seen));
27
+ return result;
28
+ }
29
+ const result={};
30
+ seen.set(value,result);
31
+ for(const [key,descriptor] of Object.entries(
32
+ Object.getOwnPropertyDescriptors(value)
33
+ )){
34
+ if(Object.hasOwn(descriptor,'value')){
35
+ result[key]=copyCompleteValue(descriptor.value,seen);
36
+ }
37
+ }
38
+ return result;
39
+ }
40
+
21
41
  function copyToolCalls(value){
22
42
  if(value===undefined) return null;
23
- if(!Array.isArray(value)||value.length!==1){
24
- const error=new TypeError(
25
- 'assistantMessage.tool_calls must contain exactly one structural tool call.'
26
- );
27
- error.code=Array.isArray(value)&&value.length>1
28
- ?'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED'
29
- :'AI_CHAT_INVALID_TOOL_CALL';
43
+ if(!Array.isArray(value)){
44
+ const error=new TypeError('assistantMessage.tool_calls must be an array.');
45
+ error.code='AI_CHAT_INVALID_TOOL_CALL';
30
46
  throw error;
31
47
  }
32
48
  const ids=new Set();
@@ -78,7 +94,8 @@ function copyToolCalls(value){
78
94
  }
79
95
  ids.add(id);
80
96
  return {
81
- function:{arguments:argumentValue,name},
97
+ ...call,
98
+ function:{...call.function},
82
99
  id,
83
100
  type:'function'
84
101
  };
@@ -103,13 +120,18 @@ function turnMessage(value,label){
103
120
  'AI_CHAT_INVALID_TOOL_MESSAGE'
104
121
  );
105
122
  }
106
- return {content:value.content,role:'tool',tool_call_id:toolCallId};
123
+ return {
124
+ ...value,
125
+ content:value.content,
126
+ role:'tool',
127
+ tool_call_id:toolCallId
128
+ };
107
129
  }
108
- return {content:value.content,role:'user'};
130
+ return {...value,content:value.content,role:'user'};
109
131
  }
110
132
 
111
- function pendingToolCallId(messages){
112
- let pending=null;
133
+ function pendingToolCalls(messages){
134
+ const pending=new Map();
113
135
  for(const [index,message] of messages.entries()){
114
136
  if(!plainRecord(message)||!['assistant','system','tool','user'].includes(message.role)){
115
137
  throw coded(
@@ -129,28 +151,30 @@ function pendingToolCallId(messages){
129
151
  'AI_CHAT_INCOHERENT_PERSISTENCE'
130
152
  );
131
153
  }
132
- if(message.role==='system'&&pending){
154
+ if(message.role==='system'&&pending.size){
133
155
  throw coded(
134
- new TypeError(`Chat message ${index+1} precedes the pending tool result.`),
156
+ new TypeError(`Chat message ${index+1} precedes the pending tool results.`),
135
157
  'AI_CHAT_INCOHERENT_PERSISTENCE'
136
158
  );
137
159
  }
138
- if(message.role==='user'&&pending){
160
+ if(message.role==='user'&&pending.size){
139
161
  throw coded(
140
- new TypeError(`Chat message ${index+1} starts a user turn before the pending tool result.`),
162
+ new TypeError(`Chat message ${index+1} starts a user turn before the pending tool results.`),
141
163
  'AI_CHAT_INCOHERENT_PERSISTENCE'
142
164
  );
143
165
  }
144
166
  if(message.role==='assistant'){
145
- if(pending){
167
+ if(pending.size){
146
168
  throw coded(
147
- new TypeError(`Chat message ${index+1} precedes the pending tool result.`),
169
+ new TypeError(`Chat message ${index+1} precedes the pending tool results.`),
148
170
  'AI_CHAT_INCOHERENT_PERSISTENCE'
149
171
  );
150
172
  }
151
173
  if(message.tool_calls){
152
174
  const calls=copyToolCalls(message.tool_calls);
153
- pending=calls[0].id;
175
+ for(const call of calls){
176
+ pending.set(call.id,call);
177
+ }
154
178
  }
155
179
  }
156
180
  if(message.role==='tool'){
@@ -167,18 +191,37 @@ function pendingToolCallId(messages){
167
191
  'AI_CHAT_INCOHERENT_PERSISTENCE'
168
192
  );
169
193
  }
170
- if(!pending||pending!==toolCallId){
194
+ if(!pending.has(toolCallId)){
171
195
  throw coded(
172
- new TypeError(`Chat message ${index+1} does not match the pending tool call.`),
196
+ new TypeError(`Chat message ${index+1} does not match a pending tool call.`),
173
197
  'AI_CHAT_INCOHERENT_PERSISTENCE'
174
198
  );
175
199
  }
176
- pending=null;
200
+ pending.delete(toolCallId);
177
201
  }
178
202
  }
179
203
  return pending;
180
204
  }
181
205
 
206
+ function turnMessages(requestMessage,requestMessages){
207
+ const hasMessage=requestMessage!==undefined;
208
+ const hasMessages=requestMessages!==undefined;
209
+ if(hasMessage===hasMessages){
210
+ throw new TypeError('Provide exactly one of requestMessage or requestMessages.');
211
+ }
212
+ const values=hasMessages?requestMessages:[requestMessage];
213
+ if(!Array.isArray(values)||values.length===0){
214
+ throw new TypeError('requestMessages must be a nonempty array.');
215
+ }
216
+ const records=values.map((value,index)=>
217
+ turnMessage(value,hasMessages?`requestMessages[${index}]`:'requestMessage')
218
+ );
219
+ if(records.length>1&&records.some(message=>message.role!=='tool')){
220
+ throw new TypeError('A multi-message request may contain only tool results.');
221
+ }
222
+ return records;
223
+ }
224
+
182
225
  /**
183
226
  * Represents a single chat message.
184
227
  *
@@ -191,7 +234,7 @@ function pendingToolCallId(messages){
191
234
  * @property {boolean} [memory_excluded]
192
235
  * Internal persistence marker omitted from model-facing messages.
193
236
  * @property {boolean} [ui_hidden]
194
- * Internal persistence marker for messages intentionally omitted from chat UI.
237
+ * Legacy persistence metadata retained without suppressing the public transcript.
195
238
  * @property {boolean} [persistence_excluded]
196
239
  * Internal marker for session-only messages omitted from durable chat and memory.
197
240
  * @property {Array<Object>} [tool_calls]
@@ -329,16 +372,16 @@ class ChatEntity{
329
372
  * This is intended to be passed directly into
330
373
  * the AI request pipeline.
331
374
  *
332
- * @returns {ChatMessage[]}
375
+ * @returns {Array<*>}
333
376
  */
334
377
  get messages(){
378
+ pendingToolCalls(this.#messages);
335
379
  return this.#messages.map(function publicChatMessage(message){
336
380
  const copy={...message};
337
381
  if(copy.tool_calls){
338
382
  copy.tool_calls=copyToolCalls(copy.tool_calls).map(call=>({
339
- function:{...call.function},
340
- id:call.id,
341
- type:call.type,
383
+ ...call,
384
+ function:{...call.function}
342
385
  }));
343
386
  }
344
387
  delete copy.memory_excluded;
@@ -354,41 +397,28 @@ class ChatEntity{
354
397
  * Model-facing callers should continue to use `messages`, which intentionally
355
398
  * omits display metadata.
356
399
  *
357
- * @returns {ChatMessage[]}
400
+ * @returns {Array<*>}
358
401
  */
359
402
  get transcript(){
360
- return this.#messages
361
- .filter(message=>message.ui_hidden!==true)
362
- .map(function publicTranscriptMessage(message){
363
- const copy={...message};
364
- if(copy.tool_calls){
365
- copy.tool_calls=copyToolCalls(copy.tool_calls).map(call=>({
366
- function:{...call.function},
367
- id:call.id,
368
- type:call.type,
369
- }));
370
- }
371
- delete copy.memory_excluded;
372
- delete copy.persistence_excluded;
373
- delete copy.ui_hidden;
374
- return copy;
375
- });
403
+ return this.#messages.map(function publicTranscriptMessage(message){
404
+ return copyCompleteValue(message);
405
+ });
376
406
  }
377
407
 
378
408
  /**
379
- * Setter exists only to prevent external mutation.
380
- *
381
- * Messages should be modified using the helper
382
- * methods such as `addUserMessage` and `addAIMessage`.
383
- *
384
- * @param {ChatMessage[]} v
385
- * Ignored value.
409
+ * Replaces the current in-memory transcript with complete copied records.
386
410
  *
387
- * @returns {ChatMessage[]}
411
+ * @param {Array<*>} v
412
+ * @returns {Array<*>}
388
413
  */
389
414
  set messages(v){
390
- console.trace('Direct mutation of messages is not allowed. Use addUserMessage or addAIMessage methods.');
391
- return this.messages;
415
+ if(!Array.isArray(v)){
416
+ throw new TypeError('messages must be an array.');
417
+ }
418
+ this.#messages=v.map(message=>copyCompleteValue(message));
419
+ this.#persistedMessageCount=0;
420
+ this.#saved=false;
421
+ return this.transcript;
392
422
  }
393
423
 
394
424
  /**
@@ -401,13 +431,15 @@ class ChatEntity{
401
431
  }
402
432
 
403
433
  /**
404
- * Setter exists only to prevent external mutation.
405
- * Saved status is managed internally.
434
+ * Replaces the current saved-status flag.
406
435
  *
407
436
  * @returns {boolean}
408
437
  */
409
438
  set saved(v){
410
- console.trace('Direct mutation of saved status is not allowed. If you want to save the chat, call the save() method.');
439
+ if(typeof v!=='boolean'){
440
+ throw new TypeError('saved must be a boolean.');
441
+ }
442
+ this.#saved=v;
411
443
  return this.#saved;
412
444
  }
413
445
 
@@ -420,7 +452,8 @@ class ChatEntity{
420
452
  * @param {string} text
421
453
  * Message content from the user.
422
454
  * @param {{hidden?:boolean,persist?:boolean}} options
423
- * Hidden messages remain in the saved/model context but are not user-authored UI turns.
455
+ * The legacy hidden option excludes the message from memory extraction without
456
+ * removing it from the complete saved or UI transcript.
424
457
  */
425
458
  addUserMessage(text='',{hidden=false,persist=true}={}){
426
459
  if(!is.string(text)){
@@ -432,9 +465,9 @@ class ChatEntity{
432
465
  if(!is.boolean(persist)){
433
466
  throw new Error('persist must be boolean');
434
467
  }
435
- if(pendingToolCallId(this.#messages)){
468
+ if(pendingToolCalls(this.#messages).size){
436
469
  throw coded(
437
- new TypeError('The pending structural tool result must be supplied before a new user turn.'),
470
+ new TypeError('The pending structural tool results must be supplied before a new user turn.'),
438
471
  'AI_CHAT_TOOL_RESULT_REQUIRED'
439
472
  );
440
473
  }
@@ -445,7 +478,6 @@ class ChatEntity{
445
478
  timestamp:Date.now()
446
479
  };
447
480
  if(hidden){
448
- message.ui_hidden=true;
449
481
  message.memory_excluded=true;
450
482
  }
451
483
 
@@ -477,9 +509,9 @@ class ChatEntity{
477
509
  if(!is.boolean(persist)){
478
510
  throw new Error('persist must be boolean');
479
511
  }
480
- if(pendingToolCallId(this.#messages)){
512
+ if(pendingToolCalls(this.#messages).size){
481
513
  throw coded(
482
- new TypeError('The pending structural tool result must be supplied before another assistant turn.'),
514
+ new TypeError('The pending structural tool results must be supplied before another assistant turn.'),
483
515
  'AI_CHAT_TOOL_RESULT_REQUIRED'
484
516
  );
485
517
  }
@@ -521,9 +553,9 @@ class ChatEntity{
521
553
  if(!is.boolean(persist)){
522
554
  throw new TypeError('persist must be boolean.');
523
555
  }
524
- if(pendingToolCallId(this.#messages)){
556
+ if(pendingToolCalls(this.#messages).size){
525
557
  throw coded(
526
- new TypeError('The pending structural tool result must be supplied before another tool exchange.'),
558
+ new TypeError('The pending structural tool results must be supplied before another tool exchange.'),
527
559
  'AI_CHAT_TOOL_RESULT_REQUIRED'
528
560
  );
529
561
  }
@@ -578,9 +610,10 @@ class ChatEntity{
578
610
  memoryRequest=messages=>ai.fetch(messages),
579
611
  messagePersist=true,
580
612
  requestMessage,
613
+ requestMessages,
581
614
  responsePersist=messagePersist,
582
615
  }={}){
583
- const request=turnMessage(requestMessage,'requestMessage');
616
+ const requests=turnMessages(requestMessage,requestMessages);
584
617
  if(!plainRecord(assistantMessage)||assistantMessage.role!=='assistant'){
585
618
  throw new TypeError('assistantMessage must be an assistant message.');
586
619
  }
@@ -596,18 +629,32 @@ class ChatEntity{
596
629
  'AI_CHAT_INCOHERENT_PERSISTENCE'
597
630
  );
598
631
  }
599
- const pendingTool=pendingToolCallId(this.#messages);
600
- if(request.role==='user'&&pendingTool){
632
+ const pendingTools=pendingToolCalls(this.#messages);
633
+ const userRequest=requests.find(message=>message.role==='user');
634
+ const toolRequests=requests.filter(message=>message.role==='tool');
635
+ if(userRequest&&pendingTools.size){
601
636
  throw coded(
602
- new TypeError('The pending structural tool result must be supplied before a new user turn.'),
637
+ new TypeError('The pending structural tool results must be supplied before a new user turn.'),
603
638
  'AI_CHAT_TOOL_RESULT_REQUIRED'
604
639
  );
605
640
  }
606
- if(request.role==='tool'&&pendingTool!==request.tool_call_id){
607
- throw coded(
608
- new TypeError('requestMessage does not match the pending structural tool call.'),
609
- 'AI_CHAT_INVALID_TOOL_MESSAGE'
610
- );
641
+ if(toolRequests.length){
642
+ const suppliedIds=new Set();
643
+ for(const request of toolRequests){
644
+ if(suppliedIds.has(request.tool_call_id)||!pendingTools.has(request.tool_call_id)){
645
+ throw coded(
646
+ new TypeError('The request tool results do not match the pending structural tool calls.'),
647
+ 'AI_CHAT_INVALID_TOOL_MESSAGE'
648
+ );
649
+ }
650
+ suppliedIds.add(request.tool_call_id);
651
+ }
652
+ if(suppliedIds.size!==pendingTools.size){
653
+ throw coded(
654
+ new TypeError('Every pending structural tool call must receive one matching result.'),
655
+ 'AI_CHAT_TOOL_RESULT_REQUIRED'
656
+ );
657
+ }
611
658
  }
612
659
  const toolCalls=copyToolCalls(assistantMessage.tool_calls);
613
660
  const assistantContent=assistantMessage.content??'';
@@ -619,7 +666,7 @@ class ChatEntity{
619
666
  !is.union(assistantContent,'string','number')
620
667
  ||(
621
668
  !String(assistantContent)
622
- &&!toolCalls
669
+ &&!toolCalls?.length
623
670
  &&!(typeof assistantReasoning==='string'&&assistantReasoning.length)
624
671
  )
625
672
  ){
@@ -628,21 +675,31 @@ class ChatEntity{
628
675
  );
629
676
  }
630
677
  const timestamp=Date.now();
631
- const requestRecord={...request,timestamp};
678
+ const requestRecords=requests.map(request=>({
679
+ ...request,
680
+ timestamp,
681
+ ...(request.role==='tool'?{memory_excluded:true}:{})
682
+ }));
632
683
  const assistantRecord={
684
+ ...copyCompleteValue(assistantMessage),
633
685
  content:assistantContent,
634
686
  role:'assistant',
635
687
  timestamp,
636
688
  ...(assistantReasoning!==undefined?{reasoning_content:assistantReasoning}:{}),
637
689
  ...(toolCalls?{tool_calls:toolCalls}:{})
638
690
  };
639
- if(request.role==='tool') requestRecord.memory_excluded=true;
640
- if(!extractMemory||toolCalls) assistantRecord.memory_excluded=true;
641
- if(!messagePersist) requestRecord.persistence_excluded=true;
691
+ if(!extractMemory||toolCalls?.length) assistantRecord.memory_excluded=true;
692
+ else delete assistantRecord.memory_excluded;
693
+ if(!messagePersist){
694
+ for(const requestRecord of requestRecords){
695
+ requestRecord.persistence_excluded=true;
696
+ }
697
+ }
642
698
  if(!responsePersist) assistantRecord.persistence_excluded=true;
699
+ else delete assistantRecord.persistence_excluded;
643
700
 
644
- const appended=this.#appendMessages([requestRecord,assistantRecord],{prepared:true});
645
- if(extractMemory&&messagePersist&&responsePersist&&!toolCalls&&this.persist){
701
+ const appended=this.#appendMessages([...requestRecords,assistantRecord],{prepared:true});
702
+ if(extractMemory&&messagePersist&&responsePersist&&!toolCalls?.length&&this.persist){
646
703
  return Promise.resolve(appended).then(result=>{
647
704
  this.#queueMemoryUpdate(memoryRequest);
648
705
  return result;
@@ -657,7 +714,7 @@ class ChatEntity{
657
714
  * If the file exists it replaces the current
658
715
  * in-memory message list.
659
716
  *
660
- * @returns {Promise<ChatMessage[]>}
717
+ * @returns {Promise<Array<*>>}
661
718
  */
662
719
  async load(){
663
720
 
@@ -673,22 +730,31 @@ class ChatEntity{
673
730
  this.#messages=systemMessage?[systemMessage]:[];
674
731
  this.#persistedMessageCount=0;
675
732
  this.#saved=true;
676
- return this.messages;
733
+ return this.transcript;
677
734
  }
678
735
 
679
736
  const loadedMessages=Array.isArray(content)
680
- ?content
737
+ ?content.map(message=>copyCompleteValue(message))
681
738
  :String(content)
682
739
  .split('\n')
683
- .map(row=>row.trim())
684
- .filter(Boolean)
685
- .map(row=>JSON.parse(row));
686
- pendingToolCallId(loadedMessages);
740
+ .filter(row=>row.trim())
741
+ .map((row,index)=>{
742
+ try{
743
+ return JSON.parse(row);
744
+ }catch(error){
745
+ console.error(
746
+ `Arcane saved chat row ${index+1} could not be parsed.`,
747
+ error,
748
+ row
749
+ );
750
+ return row;
751
+ }
752
+ });
687
753
  this.#messages=loadedMessages;
688
754
  this.#persistedMessageCount=this.#durableMessages().length;
689
755
  this.#saved=true;
690
756
 
691
- return this.messages;
757
+ return this.transcript;
692
758
  }
693
759
 
694
760
  async getMemoriesAboutUser({request=messages=>ai.fetch(messages)}={}){
@@ -769,7 +835,7 @@ ${JSON.stringify(transcript)}`
769
835
  return false;
770
836
  }
771
837
 
772
- const snapshot=this.#durableMessages().map(message=>({...message}));
838
+ const snapshot=this.#durableMessages().map(message=>copyCompleteValue(message));
773
839
  this.#saved=false;
774
840
 
775
841
  return this.#queuePersistence(
@@ -778,7 +844,7 @@ ${JSON.stringify(transcript)}`
778
844
  }
779
845
 
780
846
  #queueMemoryUpdate(request){
781
- const snapshot=this.#durableMessages().map(message=>({...message}));
847
+ const snapshot=this.#durableMessages().map(message=>copyCompleteValue(message));
782
848
  const queued=this.#memoryQueue.then(()=>this.#writeMemory(snapshot,request));
783
849
  this.#memoryQueue=queued.catch(()=>{
784
850
  console.warn('Unable to update chat memory.');
@@ -882,7 +948,9 @@ ${JSON.stringify(transcript)}`
882
948
  }
883
949
 
884
950
  #durableMessages(){
885
- return this.#messages.filter(message=>message.persistence_excluded!==true);
951
+ return this.#messages.filter(
952
+ message=>!message||typeof message!=='object'||message.persistence_excluded!==true
953
+ );
886
954
  }
887
955
 
888
956
  async #appendMessages(messages,{persist=true,prepared=false}={}){
@@ -948,7 +1016,7 @@ ${JSON.stringify(transcript)}`
948
1016
  }else{
949
1017
  const snapshot=durableSnapshot
950
1018
  .slice(0,lastDurableIndex+1)
951
- .map(entry=>({...entry}));
1019
+ .map(entry=>copyCompleteValue(entry));
952
1020
  await this.#writeSnapshot(snapshot);
953
1021
  }
954
1022
  this.#saved=this.#persistedMessageCount===this.#durableMessages().length;