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
@@ -159,7 +159,7 @@
159
159
  margin: 0;
160
160
  padding: clamp(1rem, 2vw, 1.5rem);
161
161
  list-style: none;
162
- overflow-x: hidden;
162
+ overflow-x: auto;
163
163
  overflow-y: auto;
164
164
  overscroll-behavior: contain;
165
165
  scrollbar-gutter: stable;
@@ -181,7 +181,7 @@
181
181
  clear: both;
182
182
  border: 1px solid var(--arcane-border, var(--border-color));
183
183
  border-radius: calc(var(--arcane-chat-radius) - .1rem);
184
- overflow-x: hidden;
184
+ overflow-x: auto;
185
185
  overflow-wrap: anywhere;
186
186
  }
187
187
 
@@ -238,7 +238,7 @@
238
238
  padding:.65rem;
239
239
  border:1px solid currentColor;
240
240
  border-radius:var(--arcane-radius-small,.3rem);
241
- overflow-x:hidden;
241
+ overflow-x:auto;
242
242
  overflow-wrap:anywhere;
243
243
  }
244
244
 
@@ -256,7 +256,7 @@
256
256
  max-inline-size:100%;
257
257
  max-width:100%;
258
258
  margin-top:.55rem;
259
- overflow-x:hidden;
259
+ overflow-x:auto;
260
260
  }
261
261
 
262
262
  .message_tool_details summary{
@@ -930,8 +930,9 @@
930
930
  let activeSessionMessageToken=null;
931
931
  let sessionBindingGeneration=0;
932
932
  let sessionMessageSequence=0;
933
- let pendingStructuralToolCall=null;
933
+ let pendingStructuralToolCalls=[];
934
934
  let pendingStructuralToolMessage='';
935
+ let sessionHistoryRecoveryMessage='';
935
936
  const activeSubmissionOwnerships=new Set();
936
937
  const conversationTimeboxRetryDelays=[0,500,1_500];
937
938
  const chatReasons={
@@ -1032,10 +1033,9 @@
1032
1033
  }
1033
1034
 
1034
1035
  function isPlainRecord(value){
1035
- return Boolean(value)
1036
- &&typeof value==='object'
1037
- &&!Array.isArray(value)
1038
- &&Object.getPrototypeOf(value)===Object.prototype;
1036
+ if(!value||typeof value!=='object'||Array.isArray(value))return false;
1037
+ const prototype=Object.getPrototypeOf(value);
1038
+ return prototype===Object.prototype||prototype===null;
1039
1039
  }
1040
1040
 
1041
1041
  function visibleErrorMessage(error,fallback){
@@ -1122,6 +1122,7 @@
1122
1122
  failure.code='AI_CHAT_INVALID_TOOL_CALL';
1123
1123
  throw failure;
1124
1124
  }
1125
+ const ids=new Set();
1125
1126
  return value.map((call,index)=>{
1126
1127
  if(
1127
1128
  !call
@@ -1133,6 +1134,7 @@
1133
1134
  ||Array.isArray(call.function)
1134
1135
  ||typeof call.id!=='string'
1135
1136
  ||!call.id.trim()
1137
+ ||ids.has(call.id)
1136
1138
  ||typeof call.function.name!=='string'
1137
1139
  ||!call.function.name.trim()
1138
1140
  ||typeof call.function.arguments!=='string'
@@ -1142,10 +1144,57 @@
1142
1144
  throw failure;
1143
1145
  }
1144
1146
  structuralToolMessage(call);
1145
- return call;
1147
+ ids.add(call.id);
1148
+ return {
1149
+ ...call,
1150
+ function:{...call.function},
1151
+ id:call.id,
1152
+ type:'function'
1153
+ };
1146
1154
  });
1147
1155
  }
1148
1156
 
1157
+ function copyCompleteChatData(value,seen=new Map()){
1158
+ if(value===null||typeof value!=='object') return value;
1159
+ if(seen.has(value)) return seen.get(value);
1160
+ if(Array.isArray(value)){
1161
+ const result=[];
1162
+ seen.set(value,result);
1163
+ for(const item of value) result.push(copyCompleteChatData(item,seen));
1164
+ return result;
1165
+ }
1166
+ const result={};
1167
+ seen.set(value,result);
1168
+ for(const [key,descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))){
1169
+ if(Object.hasOwn(descriptor,'value')){
1170
+ result[key]=copyCompleteChatData(descriptor.value,seen);
1171
+ }
1172
+ }
1173
+ return result;
1174
+ }
1175
+
1176
+ function sameCompleteChatData(left,right,seen=new Map()){
1177
+ if(Object.is(left,right)) return true;
1178
+ if(!left||!right||typeof left!=='object'||typeof right!=='object') return false;
1179
+ if(Array.isArray(left)!==Array.isArray(right)) return false;
1180
+ const matched=seen.get(left);
1181
+ if(matched!==undefined) return matched===right;
1182
+ seen.set(left,right);
1183
+ if(Array.isArray(left)){
1184
+ return left.length===right.length
1185
+ &&left.every((value,index)=>sameCompleteChatData(value,right[index],seen));
1186
+ }
1187
+ const leftKeys=Reflect.ownKeys(left).filter(
1188
+ key=>Object.prototype.propertyIsEnumerable.call(left,key)
1189
+ );
1190
+ const rightKeys=Reflect.ownKeys(right).filter(
1191
+ key=>Object.prototype.propertyIsEnumerable.call(right,key)
1192
+ );
1193
+ return leftKeys.length===rightKeys.length
1194
+ &&leftKeys.every(key=>Object.prototype.propertyIsEnumerable.call(right,key)
1195
+ &&sameCompleteChatData(left[key],right[key],seen));
1196
+ }
1197
+
1149
1198
  function structuralToolMessage(call){
1150
1199
  let argumentsRecord;
1151
1200
  try{
@@ -1175,40 +1224,50 @@
1175
1224
  return argumentsRecord.message;
1176
1225
  }
1177
1226
 
1178
- function setPendingStructuralToolCall(call=null){
1179
- if(call===null){
1180
- pendingStructuralToolCall=null;
1227
+ function setPendingStructuralToolCalls(calls=[]){
1228
+ if(!Array.isArray(calls)||calls.length===0){
1229
+ pendingStructuralToolCalls=[];
1181
1230
  pendingStructuralToolMessage='';
1182
- return null;
1231
+ return [];
1183
1232
  }
1184
- pendingStructuralToolMessage=structuralToolMessage(call);
1185
- pendingStructuralToolCall={
1186
- id:call.id,
1187
- type:'function',
1188
- function:{
1189
- name:call.function.name,
1190
- arguments:call.function.arguments
1191
- }
1192
- };
1193
- return pendingStructuralToolCall;
1233
+ pendingStructuralToolCalls=normalizeVisibleToolCalls(calls);
1234
+ pendingStructuralToolMessage=pendingStructuralToolCalls
1235
+ .map(structuralToolMessage)
1236
+ .join(' · ');
1237
+ return pendingStructuralToolCalls;
1194
1238
  }
1195
1239
 
1196
1240
  function sameStructuralToolCall(left,right){
1197
- return Boolean(left&&right)
1198
- &&left.id===right.id
1199
- &&left.type===right.type
1200
- &&left.function?.name===right.function?.name
1201
- &&left.function?.arguments===right.function?.arguments;
1241
+ return sameCompleteChatData(left,right);
1242
+ }
1243
+
1244
+ function sameStructuralToolCalls(left,right){
1245
+ return Array.isArray(left)
1246
+ &&Array.isArray(right)
1247
+ &&left.length===right.length
1248
+ &&left.every((call,index)=>sameStructuralToolCall(call,right[index]));
1249
+ }
1250
+
1251
+ function pendingStructuralToolSummaries(){
1252
+ return pendingStructuralToolCalls.map(call=>({
1253
+ id:call.id,
1254
+ name:call.function.name,
1255
+ message:structuralToolMessage(call)
1256
+ }));
1202
1257
  }
1203
1258
 
1204
1259
  function pendingStructuralToolSummary(){
1205
- return pendingStructuralToolCall
1206
- ?{
1207
- id:pendingStructuralToolCall.id,
1208
- name:pendingStructuralToolCall.function.name,
1209
- message:pendingStructuralToolMessage
1210
- }
1211
- :null;
1260
+ const summaries=pendingStructuralToolSummaries();
1261
+ return summaries.length===1?summaries[0]:null;
1262
+ }
1263
+
1264
+ function pendingStructuralToolCallsComplete(){
1265
+ return pendingStructuralToolCalls.map(call=>copyCompleteChatData(call));
1266
+ }
1267
+
1268
+ function pendingStructuralToolCallComplete(){
1269
+ const calls=pendingStructuralToolCallsComplete();
1270
+ return calls.length===1?calls[0]:null;
1212
1271
  }
1213
1272
 
1214
1273
  function appendVisibleToolCall(item,call){
@@ -1237,7 +1296,7 @@
1237
1296
  summary.textContent='Tool call details';
1238
1297
  const argumentsBlock=item.ownerDocument.createElement('pre');
1239
1298
  const code=item.ownerDocument.createElement('code');
1240
- code.textContent=call.function.arguments;
1299
+ code.textContent=completeVisibleValue(call);
1241
1300
  argumentsBlock.append(code);
1242
1301
  details.append(summary,argumentsBlock);
1243
1302
  entry.append(heading,userMessage,details);
@@ -1273,23 +1332,27 @@
1273
1332
  return true;
1274
1333
  }
1275
1334
 
1276
- function latestCommittedTranscriptTurn(transcript,requestMessage){
1335
+ function sameTranscriptRequest(actual,expected){
1336
+ return actual?.role===expected?.role
1337
+ &&actual.content===expected.content
1338
+ &&(
1339
+ expected.role!=='tool'
1340
+ ||actual.tool_call_id===expected.tool_call_id
1341
+ );
1342
+ }
1343
+
1344
+ function latestCommittedTranscriptTurn(transcript,requestMessages){
1277
1345
  if(!Array.isArray(transcript)) return null;
1278
- const requestRole=requestMessage?.role??'user';
1279
- const requestContent=requestMessage?.content;
1280
- for(let index=transcript.length-1;index>0;index--){
1346
+ const expected=Array.isArray(requestMessages)
1347
+ ?requestMessages
1348
+ :[requestMessages];
1349
+ if(!expected.length) return null;
1350
+ for(let index=transcript.length-1;index>=expected.length;index--){
1281
1351
  const response=transcript[index];
1282
- const request=transcript[index-1];
1283
- if(
1284
- response?.role==='assistant'
1285
- &&request?.role===requestRole
1286
- &&request.content===requestContent
1287
- &&(
1288
- requestRole!=='tool'
1289
- ||request.tool_call_id===requestMessage.tool_call_id
1290
- )
1291
- ){
1292
- return {request,response};
1352
+ if(response?.role!=='assistant') continue;
1353
+ const requests=transcript.slice(index-expected.length,index);
1354
+ if(requests.every((request,offset)=>sameTranscriptRequest(request,expected[offset]))){
1355
+ return {request:requests.at(-1),requests,response};
1293
1356
  }
1294
1357
  }
1295
1358
  return null;
@@ -1325,87 +1388,131 @@
1325
1388
  return item;
1326
1389
  }
1327
1390
 
1391
+ function completeVisibleValue(value){
1392
+ try{
1393
+ const serialized=JSON.stringify(value,null,2);
1394
+ if(serialized!==undefined) return serialized;
1395
+ }catch(error){
1396
+ console.error('Arcane saved chat record could not be serialized as JSON.',error);
1397
+ }
1398
+ return String(value);
1399
+ }
1400
+
1401
+ function createSavedRecordFallback(message,index){
1402
+ const content=isPlainRecord(message)&&typeof message.content==='string'
1403
+ ?message.content
1404
+ :'Saved record content is available in details.';
1405
+ const item=createTranscriptMessage(
1406
+ 'assistant',
1407
+ content,
1408
+ `Saved record ${index+1}`,
1409
+ {timestamp:isPlainRecord(message)?message.timestamp:undefined}
1410
+ );
1411
+ const details=item.ownerDocument.createElement('details');
1412
+ details.className='message_tool_details';
1413
+ const summary=item.ownerDocument.createElement('summary');
1414
+ summary.textContent='Saved record details';
1415
+ const block=item.ownerDocument.createElement('pre');
1416
+ const code=item.ownerDocument.createElement('code');
1417
+ code.textContent=completeVisibleValue(message);
1418
+ block.append(code);
1419
+ details.append(summary,block);
1420
+ item.insertBefore(details,item.querySelector('.message_timestamp'));
1421
+ return item;
1422
+ }
1423
+
1328
1424
  function renderSessionHistory(history){
1329
1425
  if(!Array.isArray(history)){
1330
1426
  throw new TypeError('Chat session history must be an array.');
1331
1427
  }
1332
1428
  const fragment=chatOutput.ownerDocument.createDocumentFragment();
1333
1429
  const rendered=[];
1334
- let restoredPendingToolCall=null;
1335
- for(const message of history){
1336
- if(!message||typeof message!=='object'||Array.isArray(message)){
1337
- throw new TypeError('Chat session history contains an invalid message.');
1338
- }
1339
- if(message.role==='system'){
1340
- if(restoredPendingToolCall){
1430
+ const restoredPendingToolCalls=new Map();
1431
+ let actionable=true;
1432
+ let recoveryNeeded=false;
1433
+ for(const [index,message] of history.entries()){
1434
+ let item;
1435
+ try{
1436
+ if(!isPlainRecord(message)||!['assistant','system','tool','user'].includes(message.role)){
1341
1437
  throw chatError(
1342
- 'Saved chat contains a system record before its pending structural tool result.',
1438
+ 'Saved chat contains an unsupported record.',
1343
1439
  'AI_CHAT_INCOHERENT_PERSISTENCE'
1344
1440
  );
1345
1441
  }
1346
- continue;
1347
- }
1348
- if(!['assistant','tool','user'].includes(message.role)){
1349
- throw new TypeError(`Chat session history contains an unsupported role: ${message.role}.`);
1350
- }
1351
- if(typeof message.content!=='string'){
1352
- throw new TypeError('Chat session history message content must be a string.');
1353
- }
1354
- if(message.role==='tool'&&!message.content.trim()){
1355
- throw chatError(
1356
- 'Saved chat contains a blank structural tool result.',
1357
- 'AI_CHAT_INVALID_TOOL_MESSAGE'
1358
- );
1359
- }
1360
- const calls=normalizeVisibleToolCalls(message.tool_calls);
1361
- if(calls.length>1){
1362
- throw chatError(
1363
- 'Saved chat contains parallel structural tool calls.',
1364
- 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED'
1365
- );
1366
- }
1367
- if(message.role!=='assistant'&&calls.length){
1368
- throw chatError(
1369
- 'Only assistant records may contain structural tool calls.',
1370
- 'AI_CHAT_INVALID_TOOL_CALL'
1371
- );
1372
- }
1373
- if(message.role==='tool'){
1374
- if(
1375
- !restoredPendingToolCall
1376
- ||typeof message.tool_call_id!=='string'
1377
- ||message.tool_call_id!==restoredPendingToolCall.id
1378
- ){
1442
+ if(typeof message.content!=='string'){
1379
1443
  throw chatError(
1380
- 'Saved chat contains a tool result that does not match its pending structural tool call.',
1381
- 'AI_CHAT_INVALID_TOOL_MESSAGE'
1444
+ 'Saved chat contains a record without text content.',
1445
+ 'AI_CHAT_INCOHERENT_PERSISTENCE'
1382
1446
  );
1383
1447
  }
1384
- restoredPendingToolCall=null;
1385
- }else{
1386
- if(restoredPendingToolCall){
1448
+ const calls=normalizeVisibleToolCalls(message.tool_calls);
1449
+ if(message.role!=='assistant'&&calls.length){
1450
+ throw chatError(
1451
+ 'Only assistant records may contain structural tool calls.',
1452
+ 'AI_CHAT_INVALID_TOOL_CALL'
1453
+ );
1454
+ }
1455
+ if(message.role!=='tool'&&message.tool_call_id!==undefined){
1387
1456
  throw chatError(
1388
- 'Saved chat continues before its pending structural tool result.',
1389
- 'AI_CHAT_TOOL_RESULT_REQUIRED'
1457
+ 'Only tool records may contain a structural tool-call ID.',
1458
+ 'AI_CHAT_INVALID_TOOL_MESSAGE'
1390
1459
  );
1391
1460
  }
1392
- if(calls.length){
1393
- restoredPendingToolCall=calls[0];
1461
+ if(actionable){
1462
+ if(message.role==='tool'){
1463
+ if(
1464
+ typeof message.tool_call_id!=='string'
1465
+ ||!message.tool_call_id.trim()
1466
+ ||!message.content.trim()
1467
+ ||!restoredPendingToolCalls.has(message.tool_call_id)
1468
+ ){
1469
+ throw chatError(
1470
+ 'Saved chat contains a tool result that does not match a pending structural tool call.',
1471
+ 'AI_CHAT_INVALID_TOOL_MESSAGE'
1472
+ );
1473
+ }
1474
+ restoredPendingToolCalls.delete(message.tool_call_id);
1475
+ }else{
1476
+ if(restoredPendingToolCalls.size){
1477
+ throw chatError(
1478
+ 'Saved chat continues before all pending structural tool results.',
1479
+ 'AI_CHAT_TOOL_RESULT_REQUIRED'
1480
+ );
1481
+ }
1482
+ if(calls.length){
1483
+ for(const call of calls){
1484
+ restoredPendingToolCalls.set(call.id,call);
1485
+ }
1486
+ }
1487
+ }
1394
1488
  }
1489
+ const name=message.role==='user'
1490
+ ?host.name
1491
+ :message.role==='tool'
1492
+ ?'Tool'
1493
+ :message.role==='system'
1494
+ ?'System'
1495
+ :host.aiName;
1496
+ item=createTranscriptMessage(message.role,message.content,name,{
1497
+ timestamp:message.timestamp,
1498
+ toolCalls:calls,
1499
+ });
1500
+ }catch(error){
1501
+ recoveryNeeded=true;
1502
+ actionable=false;
1503
+ restoredPendingToolCalls.clear();
1504
+ console.error(`Arcane saved chat record ${index+1} is not actionable.`,error);
1505
+ item=createSavedRecordFallback(message,index);
1395
1506
  }
1396
- const name=message.role==='user'
1397
- ?host.name
1398
- :message.role==='tool'
1399
- ?'Tool'
1400
- :host.aiName;
1401
- const item=createTranscriptMessage(message.role,message.content,name,{
1402
- timestamp:message.timestamp,
1403
- toolCalls:calls,
1404
- });
1405
1507
  fragment.append(item);
1406
1508
  rendered.push(item);
1407
1509
  }
1408
- setPendingStructuralToolCall(restoredPendingToolCall);
1510
+ sessionHistoryRecoveryMessage=recoveryNeeded
1511
+ ?'This saved chat is readable, but some saved actions cannot be resumed.'
1512
+ :'';
1513
+ setPendingStructuralToolCalls(
1514
+ actionable?[...restoredPendingToolCalls.values()]:[]
1515
+ );
1409
1516
  chatOutput.replaceChildren(fragment);
1410
1517
  scrollTranscriptToBottom();
1411
1518
  return rendered;
@@ -1474,7 +1581,7 @@
1474
1581
  host.session=session;
1475
1582
  setSessionStatus(
1476
1583
  pendingStructuralToolMessage?'tool':'ready',
1477
- pendingStructuralToolMessage||'Chat ready.'
1584
+ pendingStructuralToolMessage||sessionHistoryRecoveryMessage||'Chat ready.'
1478
1585
  );
1479
1586
  applyAIAvailability(host.aiAvailability);
1480
1587
  sessionBindingPending=false;
@@ -1483,7 +1590,10 @@
1483
1590
  session,
1484
1591
  ai:boundChatAI,
1485
1592
  history,
1486
- pendingTool:pendingStructuralToolSummary()
1593
+ pendingTool:pendingStructuralToolSummary(),
1594
+ pendingTools:pendingStructuralToolSummaries(),
1595
+ pendingToolCall:pendingStructuralToolCallComplete(),
1596
+ pendingToolCalls:pendingStructuralToolCallsComplete()
1487
1597
  };
1488
1598
  dispatchChatEvent(
1489
1599
  'chat-session-bound',
@@ -1652,11 +1762,27 @@
1652
1762
  host.bindSession=bindSession;
1653
1763
  host.submitMessage=submitMessage;
1654
1764
  host.submitToolResult=submitToolResult;
1765
+ host.submitToolResults=submitToolResults;
1655
1766
  Object.defineProperty(host,'pendingTool',{
1656
1767
  configurable:true,
1657
1768
  enumerable:true,
1658
1769
  get:pendingStructuralToolSummary
1659
1770
  });
1771
+ Object.defineProperty(host,'pendingTools',{
1772
+ configurable:true,
1773
+ enumerable:true,
1774
+ get:pendingStructuralToolSummaries
1775
+ });
1776
+ Object.defineProperty(host,'pendingToolCall',{
1777
+ configurable:true,
1778
+ enumerable:true,
1779
+ get:pendingStructuralToolCallComplete
1780
+ });
1781
+ Object.defineProperty(host,'pendingToolCalls',{
1782
+ configurable:true,
1783
+ enumerable:true,
1784
+ get:pendingStructuralToolCallsComplete
1785
+ });
1660
1786
  host.destroy=destroy;
1661
1787
  host.session=null;
1662
1788
  host.sessionStatus={state:'idle',message:'Chat session is not connected.'};
@@ -1672,8 +1798,13 @@
1672
1798
  );
1673
1799
  window.addEventListener(
1674
1800
  'pagehide',
1675
- destroy,
1676
- {once:true,signal:aiRuntimeStateAbortController.signal}
1801
+ handlePageHide,
1802
+ {signal:aiRuntimeStateAbortController.signal}
1803
+ );
1804
+ window.addEventListener(
1805
+ 'pageshow',
1806
+ handlePageShow,
1807
+ {signal:aiRuntimeStateAbortController.signal}
1677
1808
  );
1678
1809
  window.addEventListener(
1679
1810
  AI_TTS_FAILURE_EVENT,
@@ -1863,9 +1994,12 @@
1863
1994
  send.disabled=host.conversationComplete
1864
1995
  ||!availability.llm
1865
1996
  ||sessionMessagePending
1866
- ||Boolean(pendingStructuralToolMessage);
1997
+ ||Boolean(pendingStructuralToolMessage)
1998
+ ||Boolean(sessionHistoryRecoveryMessage);
1867
1999
  send.title=pendingStructuralToolMessage
1868
2000
  ?pendingStructuralToolMessage
2001
+ :sessionHistoryRecoveryMessage
2002
+ ?'This saved chat is readable but cannot continue.'
1869
2003
  :availability.llm
1870
2004
  ?'Send message'
1871
2005
  :'The selected language model service is unavailable.';
@@ -1910,16 +2044,6 @@
1910
2044
  return configured||role?.modelId||'the selected language model';
1911
2045
  }
1912
2046
 
1913
- function byteProgressUnit(value){
1914
- const unit=String(value??'')
1915
- .trim()
1916
- .toLowerCase()
1917
- .replace(/[\s_-]+/g,'');
1918
- return unit.includes('byte')
1919
- ||unit.includes('octet')
1920
- ||/^(?:[kmgtpe]?i?b)(?:(?:\/|per)?(?:s|sec|second))?$/u.test(unit);
1921
- }
1922
-
1923
2047
  function determinateProgress(){
1924
2048
  const total=Number(role?.progress?.total);
1925
2049
  const completed=Number(role?.progress?.completed);
@@ -1927,15 +2051,14 @@
1927
2051
  ?role.progress.unit
1928
2052
  :'items';
1929
2053
  if(
1930
- byteProgressUnit(unit)
1931
- ||!Number.isFinite(total)
2054
+ !Number.isFinite(total)
1932
2055
  ||total<=0
1933
2056
  ||!Number.isFinite(completed)
1934
2057
  ){
1935
2058
  return null;
1936
2059
  }
1937
2060
  return {
1938
- completed:Math.max(0,completed),
2061
+ completed,
1939
2062
  total,
1940
2063
  unit
1941
2064
  };
@@ -1972,7 +2095,7 @@
1972
2095
  return;
1973
2096
  }
1974
2097
  progress.max=measured.total;
1975
- progress.value=Math.min(measured.completed,measured.total);
2098
+ progress.value=measured.completed;
1976
2099
  }
1977
2100
 
1978
2101
  function render(){
@@ -2727,6 +2850,7 @@
2727
2850
  'AI_CHAT_STREAM_TOOL_CALL_MISMATCH',
2728
2851
  'AI_CHAT_TOOL_MESSAGE_REQUIRED',
2729
2852
  'AI_CHAT_TOOL_RESULT_NOT_PENDING',
2853
+ 'AI_CHAT_TOOL_RESULT_BATCH_REQUIRED',
2730
2854
  'AI_CHAT_TOOL_RESULT_REQUIRED',
2731
2855
  'AI_CHAT_TRANSACTION_SETTLED'
2732
2856
  ].includes(error?.code);
@@ -2756,7 +2880,7 @@
2756
2880
  async function sendMessageThroughBoundSession(
2757
2881
  text,
2758
2882
  context,
2759
- sessionRequestMessage={content:text,role:'user'},
2883
+ sessionRequest={content:text,role:'user'},
2760
2884
  perTurnRequest=null
2761
2885
  ){
2762
2886
  const session=boundChatSession;
@@ -2774,13 +2898,21 @@
2774
2898
  );
2775
2899
  }
2776
2900
 
2901
+ const sessionRequestMessages=Array.isArray(sessionRequest)
2902
+ ?sessionRequest
2903
+ :[sessionRequest];
2904
+ if(!sessionRequestMessages.length){
2905
+ throw new TypeError('A chat session request must contain at least one message.');
2906
+ }
2777
2907
  const request={
2778
- message:sessionRequestMessage,
2908
+ ...(sessionRequestMessages.length===1
2909
+ ?{message:sessionRequestMessages[0]}
2910
+ :{messages:sessionRequestMessages}),
2779
2911
  signal:context.signal,
2780
2912
  ...(perTurnRequest===null?{}:{request:perTurnRequest})
2781
2913
  };
2782
- const previousPendingToolCall=pendingStructuralToolCall;
2783
- let streamedStructuralToolCall=null;
2914
+ const previousPendingToolCalls=[...pendingStructuralToolCalls];
2915
+ const streamedStructuralToolCalls=[];
2784
2916
  const messageId=`session-${++sessionMessageSequence}`;
2785
2917
  const sessionMessageToken=Symbol(messageId);
2786
2918
  activeSessionMessageToken=sessionMessageToken;
@@ -2821,20 +2953,20 @@
2821
2953
  )||null;
2822
2954
  if(!message) return false;
2823
2955
  const normalized=normalizeVisibleToolCalls([call])[0];
2824
- if(
2825
- streamedStructuralToolCall
2826
- &&!sameStructuralToolCall(streamedStructuralToolCall,normalized)
2827
- ){
2956
+ const existing=streamedStructuralToolCalls.find(
2957
+ candidate=>candidate.id===normalized.id
2958
+ );
2959
+ if(existing&&!sameStructuralToolCall(existing,normalized)){
2828
2960
  throw chatError(
2829
2961
  'The streamed structural tool call changed before completion.',
2830
2962
  'AI_CHAT_STREAM_TOOL_CALL_MISMATCH'
2831
2963
  );
2832
2964
  }
2833
- if(streamedStructuralToolCall){
2965
+ if(existing){
2834
2966
  return true;
2835
2967
  }
2836
- streamedStructuralToolCall=normalized;
2837
- setPendingStructuralToolCall(normalized);
2968
+ streamedStructuralToolCalls.push(normalized);
2969
+ setPendingStructuralToolCalls(streamedStructuralToolCalls);
2838
2970
  appendVisibleToolCall(message,normalized);
2839
2971
  setSessionStatus('tool',pendingStructuralToolMessage);
2840
2972
  return true;
@@ -2858,7 +2990,7 @@
2858
2990
  const committedTurn=typeof session.transcript==='function'
2859
2991
  ?latestCommittedTranscriptTurn(
2860
2992
  await session.transcript(),
2861
- sessionRequestMessage
2993
+ sessionRequestMessages
2862
2994
  )
2863
2995
  :null;
2864
2996
  const message=[...chatOutput.children].find(
@@ -2872,41 +3004,32 @@
2872
3004
  result.message.content,
2873
3005
  committedTurn?.response?.timestamp??result.message.timestamp
2874
3006
  );
2875
- const requestMessage=[...chatOutput.children].find(
3007
+ const requestMessages=[...chatOutput.children].filter(
2876
3008
  candidate=>candidate.dataset.operationId===context.operationId
2877
- )||null;
2878
- setTranscriptMessageTimestamp(
2879
- requestMessage,
2880
- committedTurn?.request?.timestamp
2881
3009
  );
2882
- const terminalToolCalls=normalizeVisibleToolCalls(result.message.tool_calls);
2883
- if(terminalToolCalls.length>1){
2884
- throw chatError(
2885
- 'The chat session returned parallel structural tool calls.',
2886
- 'AI_CHAT_PARALLEL_TOOLS_UNSUPPORTED'
3010
+ for(const [index,requestMessage] of requestMessages.entries()){
3011
+ setTranscriptMessageTimestamp(
3012
+ requestMessage,
3013
+ committedTurn?.requests?.[index]?.timestamp
3014
+ ??committedTurn?.request?.timestamp
2887
3015
  );
2888
3016
  }
3017
+ const terminalToolCalls=normalizeVisibleToolCalls(result.message.tool_calls);
2889
3018
  if(
2890
- streamedStructuralToolCall
2891
- &&(
2892
- terminalToolCalls.length!==1
2893
- ||!sameStructuralToolCall(
2894
- streamedStructuralToolCall,
2895
- terminalToolCalls[0]
2896
- )
2897
- )
3019
+ streamedStructuralToolCalls.length
3020
+ &&!sameStructuralToolCalls(streamedStructuralToolCalls,terminalToolCalls)
2898
3021
  ){
2899
3022
  throw chatError(
2900
- 'The terminal structural tool call does not match the streamed call.',
3023
+ 'The terminal structural tool calls do not match the streamed calls.',
2901
3024
  'AI_CHAT_STREAM_TOOL_CALL_MISMATCH'
2902
3025
  );
2903
3026
  }
2904
- if(!streamedStructuralToolCall&&terminalToolCalls.length){
2905
- appendVisibleToolCall(message,terminalToolCalls[0]);
3027
+ if(!streamedStructuralToolCalls.length){
3028
+ for(const call of terminalToolCalls){
3029
+ appendVisibleToolCall(message,call);
3030
+ }
2906
3031
  }
2907
- setPendingStructuralToolCall(
2908
- terminalToolCalls.length?terminalToolCalls.at(-1):null
2909
- );
3032
+ setPendingStructuralToolCalls(terminalToolCalls);
2910
3033
  setSessionStatus(
2911
3034
  pendingStructuralToolMessage?'tool':'ready',
2912
3035
  pendingStructuralToolMessage||'Chat ready.'
@@ -2916,7 +3039,11 @@
2916
3039
  request,
2917
3040
  result,
2918
3041
  message:result.message,
2919
- context
3042
+ context,
3043
+ pendingTool:pendingStructuralToolSummary(),
3044
+ pendingTools:pendingStructuralToolSummaries(),
3045
+ pendingToolCall:pendingStructuralToolCallComplete(),
3046
+ pendingToolCalls:pendingStructuralToolCallsComplete()
2920
3047
  };
2921
3048
  if(activeSessionMessageToken===sessionMessageToken){
2922
3049
  activeSessionMessageToken=null;
@@ -2939,38 +3066,42 @@
2939
3066
  return result;
2940
3067
  }catch(error){
2941
3068
  if(!destroyed&&bindingGeneration===sessionBindingGeneration){
2942
- setPendingStructuralToolCall(previousPendingToolCall);
3069
+ setPendingStructuralToolCalls(previousPendingToolCalls);
2943
3070
  }
2944
3071
  if(
2945
3072
  !destroyed
2946
3073
  &&bindingGeneration===sessionBindingGeneration
2947
3074
  &&internalStructuralToolFailure(error)
2948
3075
  ){
2949
- if(sessionRequestMessage.role==='tool'){
3076
+ if(sessionRequestMessages.every(message=>message.role==='tool')){
2950
3077
  const response=[...chatOutput.children].find(
2951
3078
  candidate=>candidate.id===`message-${messageId}`
2952
3079
  )||null;
2953
- const requestMessage=[...chatOutput.children].find(
3080
+ const requestMessages=[...chatOutput.children].filter(
2954
3081
  candidate=>candidate.dataset.operationId===context.operationId
2955
- )||null;
3082
+ );
2956
3083
  response?.remove();
2957
- requestMessage?.remove();
3084
+ for(const requestMessage of requestMessages){
3085
+ requestMessage.remove();
3086
+ }
2958
3087
  scrollTranscriptToBottom();
2959
3088
  }else{
2960
3089
  restoreRejectedStructuralDraft(messageId,context.operationId,text);
2961
3090
  }
2962
3091
  console.error('Arcane structural tool protocol failure.',error);
2963
3092
  setSessionStatus(
2964
- pendingStructuralToolCall?'tool':'ready',
3093
+ pendingStructuralToolCalls.length?'tool':'ready',
2965
3094
  pendingStructuralToolMessage||'Chat ready.'
2966
3095
  );
2967
3096
  }else if(!destroyed&&bindingGeneration===sessionBindingGeneration){
2968
3097
  console.error('Arcane chat request failed.',error);
2969
- if(sessionRequestMessage.role==='tool'){
2970
- const requestMessage=[...chatOutput.children].find(
3098
+ if(sessionRequestMessages.every(message=>message.role==='tool')){
3099
+ const requestMessages=[...chatOutput.children].filter(
2971
3100
  candidate=>candidate.dataset.operationId===context.operationId
2972
- )||null;
2973
- requestMessage?.remove();
3101
+ );
3102
+ for(const requestMessage of requestMessages){
3103
+ requestMessage.remove();
3104
+ }
2974
3105
  }
2975
3106
  renderSessionMessageFailure(messageId,error);
2976
3107
  setSessionStatus(
@@ -3010,18 +3141,18 @@
3010
3141
  }
3011
3142
  }
3012
3143
 
3013
- async function submitToolResult(options={},context={}){
3144
+ async function submitToolResults(options={},context={}){
3014
3145
  if(destroyed||host.conversationComplete){
3015
3146
  return false;
3016
3147
  }
3017
3148
  if(!isPlainRecord(options)||!isPlainRecord(context)){
3018
- throw new TypeError('Tool-result options and context must be plain objects.');
3149
+ throw new TypeError('Tool-results options and context must be plain objects.');
3019
3150
  }
3020
3151
  const unsupportedOption=Object.keys(options).find(
3021
- key=>!['disposition','message','persist','request','toolCallId'].includes(key)
3152
+ key=>!['request','results'].includes(key)
3022
3153
  );
3023
3154
  if(unsupportedOption){
3024
- throw new TypeError(`Unsupported tool-result option: ${unsupportedOption}.`);
3155
+ throw new TypeError(`Unsupported tool-results option: ${unsupportedOption}.`);
3025
3156
  }
3026
3157
  const unsupportedContext=Object.keys(context).find(
3027
3158
  key=>!['operationId','signal'].includes(key)
@@ -3035,53 +3166,102 @@
3035
3166
  if(!host.aiAvailability.llm){
3036
3167
  return false;
3037
3168
  }
3038
- if(!pendingStructuralToolCall){
3039
- const error=new TypeError('There is no pending structural tool call to settle.');
3169
+ if(!pendingStructuralToolCalls.length){
3170
+ const error=new TypeError('There are no pending structural tool calls to settle.');
3040
3171
  error.code='AI_CHAT_TOOL_RESULT_NOT_PENDING';
3041
3172
  throw error;
3042
3173
  }
3043
- const disposition=options.disposition;
3174
+ if(!Array.isArray(options.results)||options.results.length===0){
3175
+ throw new TypeError('Tool results must be a nonempty array.');
3176
+ }
3044
3177
  const dispositions=new Map([
3045
3178
  ['executed','Executed'],
3046
3179
  ['declined','Declined'],
3047
3180
  ['cancelled','Cancelled'],
3048
3181
  ['not-executed','Not executed']
3049
3182
  ]);
3050
- if(!dispositions.has(disposition)){
3051
- throw new TypeError(
3052
- 'Tool-result disposition must be executed, declined, cancelled, or not-executed.'
3053
- );
3054
- }
3055
- if(typeof options.message!=='string'||!options.message.trim()){
3056
- throw new TypeError('Tool-result message must contain user-facing text.');
3057
- }
3058
3183
  const perTurnRequest=options.request??null;
3059
3184
  if(perTurnRequest!==null&&!isPlainRecord(perTurnRequest)){
3060
3185
  throw new TypeError('Tool-result request options must be a plain object.');
3061
3186
  }
3062
3187
  const managedRequestField=perTurnRequest&&Object.keys(perTurnRequest).find(
3063
- key=>['messages','onChunk','onResponse','onToolCall','signal','stream'].includes(key)
3188
+ key=>[
3189
+ 'message','messages','onChunk','onComplete','onDataChunk','onDataResult',
3190
+ 'onResponse','onToolCall','signal','stream'
3191
+ ].includes(key)
3064
3192
  );
3065
3193
  if(managedRequestField){
3066
3194
  throw new TypeError(
3067
3195
  `Tool-result request.${managedRequestField} is managed by the chat session.`
3068
3196
  );
3069
3197
  }
3070
- const toolCallId=options.toolCallId??pendingStructuralToolCall.id;
3071
- if(typeof toolCallId!=='string'||toolCallId!==pendingStructuralToolCall.id){
3072
- const error=new TypeError('Tool-result ID does not match the pending structural tool call.');
3073
- error.code='AI_CHAT_INVALID_TOOL_MESSAGE';
3198
+ const pendingById=new Map(
3199
+ pendingStructuralToolCalls.map(call=>[call.id,call])
3200
+ );
3201
+ const resultsById=new Map();
3202
+ for(const [index,result] of options.results.entries()){
3203
+ if(!isPlainRecord(result)){
3204
+ throw new TypeError(`Tool result ${index+1} must be a plain object.`);
3205
+ }
3206
+ const unsupportedResultField=Object.keys(result).find(
3207
+ key=>!['disposition','message','persist','toolCallId'].includes(key)
3208
+ );
3209
+ if(unsupportedResultField){
3210
+ throw new TypeError(
3211
+ `Unsupported tool result ${index+1} field: ${unsupportedResultField}.`
3212
+ );
3213
+ }
3214
+ const toolCallId=result.toolCallId;
3215
+ if(
3216
+ typeof toolCallId!=='string'
3217
+ ||!pendingById.has(toolCallId)
3218
+ ||resultsById.has(toolCallId)
3219
+ ){
3220
+ const error=new TypeError(
3221
+ `Tool result ${index+1} does not match a pending structural tool call.`
3222
+ );
3223
+ error.code='AI_CHAT_INVALID_TOOL_MESSAGE';
3224
+ throw error;
3225
+ }
3226
+ if(!dispositions.has(result.disposition)){
3227
+ throw new TypeError(
3228
+ 'Tool-result disposition must be executed, declined, cancelled, or not-executed.'
3229
+ );
3230
+ }
3231
+ if(typeof result.message!=='string'||!result.message.trim()){
3232
+ throw new TypeError('Tool-result message must contain user-facing text.');
3233
+ }
3234
+ const persist=result.persist??true;
3235
+ if(typeof persist!=='boolean'){
3236
+ throw new TypeError('Tool-result persist must be boolean.');
3237
+ }
3238
+ resultsById.set(toolCallId,{
3239
+ content:`${dispositions.get(result.disposition)} — ${result.message}`,
3240
+ persist,
3241
+ role:'tool',
3242
+ tool_call_id:toolCallId
3243
+ });
3244
+ }
3245
+ if(resultsById.size!==pendingById.size){
3246
+ const error=new TypeError(
3247
+ 'Every pending structural tool call must receive one matching result.'
3248
+ );
3249
+ error.code='AI_CHAT_TOOL_RESULT_REQUIRED';
3074
3250
  throw error;
3075
3251
  }
3076
- const persist=options.persist??true;
3077
- if(typeof persist!=='boolean'){
3078
- throw new TypeError('Tool-result persist must be boolean.');
3252
+ const orderedResults=pendingStructuralToolCalls.map(
3253
+ call=>resultsById.get(call.id)
3254
+ );
3255
+ if(new Set(orderedResults.map(result=>result.persist)).size>1){
3256
+ const error=new TypeError(
3257
+ 'All tool results in one atomic settlement must use the same persistence choice.'
3258
+ );
3259
+ error.code='AI_CHAT_INCOHERENT_PERSISTENCE';
3260
+ throw error;
3079
3261
  }
3080
-
3081
- const text=`${dispositions.get(disposition)} — ${options.message}`;
3082
3262
  const operationId=typeof context.operationId==='string'&&context.operationId
3083
3263
  ?context.operationId
3084
- :nextChatOperationId('tool-result');
3264
+ :nextChatOperationId('tool-results');
3085
3265
  const ownership=createChatSubmissionOwnership(context.signal??null);
3086
3266
  const eventContext={
3087
3267
  source:'tool',
@@ -3094,17 +3274,15 @@
3094
3274
  return false;
3095
3275
  }
3096
3276
  try{
3097
- const visibleMessage=appendTranscriptMessage('tool',text,'Tool');
3098
- visibleMessage.dataset.operationId=operationId;
3277
+ for(const result of orderedResults){
3278
+ const visibleMessage=appendTranscriptMessage('tool',result.content,'Tool');
3279
+ visibleMessage.dataset.operationId=operationId;
3280
+ visibleMessage.dataset.toolCallId=result.tool_call_id;
3281
+ }
3099
3282
  const result=sendMessageThroughBoundSession(
3100
- text,
3283
+ orderedResults.map(message=>message.content).join('\n'),
3101
3284
  eventContext,
3102
- {
3103
- content:text,
3104
- persist,
3105
- role:'tool',
3106
- tool_call_id:toolCallId
3107
- },
3285
+ orderedResults,
3108
3286
  perTurnRequest
3109
3287
  );
3110
3288
  return observeHostSubmission(result,eventContext,ownership);
@@ -3114,6 +3292,50 @@
3114
3292
  }
3115
3293
  }
3116
3294
 
3295
+ async function submitToolResult(options={},context={}){
3296
+ if(
3297
+ destroyed
3298
+ ||host.conversationComplete
3299
+ ||!boundChatSession
3300
+ ||sessionBindingPending
3301
+ ||sessionMessagePending
3302
+ ||!host.aiAvailability.llm
3303
+ ){
3304
+ return false;
3305
+ }
3306
+ if(!isPlainRecord(options)){
3307
+ throw new TypeError('Tool-result options must be a plain object.');
3308
+ }
3309
+ const unsupportedOption=Object.keys(options).find(
3310
+ key=>!['disposition','message','persist','request','toolCallId'].includes(key)
3311
+ );
3312
+ if(unsupportedOption){
3313
+ throw new TypeError(`Unsupported tool-result option: ${unsupportedOption}.`);
3314
+ }
3315
+ if(pendingStructuralToolCalls.length!==1){
3316
+ const error=new TypeError(
3317
+ pendingStructuralToolCalls.length
3318
+ ?'Parallel structural tool calls must be settled together with submitToolResults().'
3319
+ :'There is no pending structural tool call to settle.'
3320
+ );
3321
+ error.code=pendingStructuralToolCalls.length
3322
+ ?'AI_CHAT_TOOL_RESULT_BATCH_REQUIRED'
3323
+ :'AI_CHAT_TOOL_RESULT_NOT_PENDING';
3324
+ throw error;
3325
+ }
3326
+ const {request,...result}=options;
3327
+ return submitToolResults(
3328
+ {
3329
+ results:[{
3330
+ ...result,
3331
+ toolCallId:result.toolCallId??pendingStructuralToolCalls[0].id
3332
+ }],
3333
+ ...(request===undefined?{}:{request})
3334
+ },
3335
+ context
3336
+ );
3337
+ }
3338
+
3117
3339
  function observeHostSubmission(result,context,ownership){
3118
3340
  const observed=hostSubmissionBarrier.track(result).then(
3119
3341
  function settleHostSubmission(value){
@@ -3126,9 +3348,12 @@
3126
3348
  if(destroyed||context.signal.aborted){
3127
3349
  return false;
3128
3350
  }
3129
- if(internalStructuralToolFailure(error)){
3130
- return false;
3131
- }
3351
+ console.error(
3352
+ internalStructuralToolFailure(error)
3353
+ ?'Arcane structural tool protocol failure.'
3354
+ :'The chat message could not be submitted.',
3355
+ error
3356
+ );
3132
3357
  dispatchChatEvent(
3133
3358
  'chat-send-error',
3134
3359
  {error,context:{...context}},
@@ -3146,7 +3371,6 @@
3146
3371
  }
3147
3372
  }
3148
3373
  );
3149
- console.error('The chat message could not be submitted.',error);
3150
3374
  return false;
3151
3375
  }
3152
3376
  ).finally(
@@ -3173,7 +3397,12 @@
3173
3397
  if(!host.aiAvailability.llm){
3174
3398
  return false;
3175
3399
  }
3176
- if(sessionBindingPending||sessionMessagePending||pendingStructuralToolMessage){
3400
+ if(
3401
+ sessionBindingPending
3402
+ ||sessionMessagePending
3403
+ ||pendingStructuralToolMessage
3404
+ ||sessionHistoryRecoveryMessage
3405
+ ){
3177
3406
  return false;
3178
3407
  }
3179
3408
  const submissionContext={
@@ -3333,6 +3562,25 @@
3333
3562
  return false;
3334
3563
  }
3335
3564
 
3565
+ function handlePageHide(event){
3566
+ if(event.persisted===true){
3567
+ return;
3568
+ }
3569
+ destroy();
3570
+ }
3571
+
3572
+ function handlePageShow(event){
3573
+ if(event.persisted!==true||destroyed){
3574
+ return;
3575
+ }
3576
+ restoreFromPageCache();
3577
+ }
3578
+
3579
+ function restoreFromPageCache(){
3580
+ setAIAvailability();
3581
+ scrollTranscriptToBottom();
3582
+ }
3583
+
3336
3584
  function destroy(){
3337
3585
  if(destroyed){
3338
3586
  return false;
@@ -3349,7 +3597,8 @@
3349
3597
  }
3350
3598
  aiRuntimeStateAbortController.abort(destroyReason);
3351
3599
  aiActivationController.destroy();
3352
- window.removeEventListener('pagehide',destroy);
3600
+ window.removeEventListener('pagehide',handlePageHide);
3601
+ window.removeEventListener('pageshow',handlePageShow);
3353
3602
  conversationTimeboxUnsubscribe?.();
3354
3603
  conversationTimeboxUnsubscribe=null;
3355
3604
  conversationTimeboxController=null;
@@ -3357,7 +3606,7 @@
3357
3606
  sessionBindingPending=false;
3358
3607
  sessionMessagePending=false;
3359
3608
  activeSessionMessageToken=null;
3360
- setPendingStructuralToolCall();
3609
+ setPendingStructuralToolCalls();
3361
3610
  boundChatSession=null;
3362
3611
  boundChatAI=null;
3363
3612
  host.session=null;