@genesislcap/ai-assistant 14.450.0 → 14.451.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,7 @@ import { __awaiter } from "tslib";
2
2
  import { MalformedFunctionCallError } from '@genesislcap/foundation-ai';
3
3
  import { agenticActivityBus } from '../../channel/ai-activity-bus';
4
4
  import { resolveChatProvider } from '../../config/validate-providers';
5
+ import { recordMetaEvent } from '../../state/debug-event-log';
5
6
  import { applyHistoryCap } from '../../utils/history-transform';
6
7
  import { logger } from '../../utils/logger';
7
8
  import { TOOL_FOLD_SYMBOL } from '../../utils/tool-fold';
@@ -27,12 +28,17 @@ const HANDOFF_TOOL_RESULT_PLACEHOLDER = 'Handoff to another specialist — routi
27
28
  * @beta
28
29
  */
29
30
  export class ChatDriver extends EventTarget {
30
- constructor(providerRegistry, toolHandlers = {}, toolDefinitions = [], systemPrompt, primerHistory, maxToolIterations = DEFAULT_MAX_TOOL_ITERATIONS, maxFoldOperations = DEFAULT_MAX_FOLD_OPERATIONS, maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS) {
31
+ constructor(providerRegistry, toolHandlers = {}, toolDefinitions = [], systemPrompt, primerHistory, maxToolIterations = DEFAULT_MAX_TOOL_ITERATIONS, maxFoldOperations = DEFAULT_MAX_FOLD_OPERATIONS, maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS,
32
+ /** Session identity used to file meta events onto the shared debug-log timeline. */
33
+ sessionKey = '') {
31
34
  super();
32
35
  this.providerRegistry = providerRegistry;
33
36
  this.maxToolIterations = maxToolIterations;
37
+ this.sessionKey = sessionKey;
34
38
  this.history = [];
35
39
  this.busy = false;
40
+ /** Epoch ms when the current turn loop began — drives the `turn.end` duration. */
41
+ this.turnStartedAt = 0;
36
42
  this.pendingInteractions = new Map();
37
43
  /** Stack of fold frames — grows when a fold opens, shrinks when it closes. */
38
44
  this.foldStack = [];
@@ -184,6 +190,10 @@ export class ChatDriver extends EventTarget {
184
190
  this.lastResolvedProviderName = resolvedName;
185
191
  if (resolvedName !== this.lastDispatchedProviderName) {
186
192
  this.lastDispatchedProviderName = resolvedName;
193
+ recordMetaEvent(this.sessionKey, 'provider.selected', {
194
+ provider: resolvedName,
195
+ agent: this.activeAgentName,
196
+ });
187
197
  this.dispatchEvent(new CustomEvent('provider-changed', { detail: { name: resolvedName } }));
188
198
  }
189
199
  return provider;
@@ -412,6 +422,11 @@ export class ChatDriver extends EventTarget {
412
422
  reject,
413
423
  overrideId: chatInputDuringExecution ? interactionId : undefined,
414
424
  });
425
+ recordMetaEvent(this.sessionKey, 'interaction.requested', {
426
+ interactionId,
427
+ component: componentName,
428
+ agent: this.activeAgentName,
429
+ });
415
430
  if (chatInputDuringExecution) {
416
431
  this.dispatchEvent(new CustomEvent('interaction-start', {
417
432
  detail: { interactionId, chatInputDuringExecution },
@@ -442,6 +457,7 @@ export class ChatDriver extends EventTarget {
442
457
  if (interaction.overrideId) {
443
458
  this.dispatchEvent(new CustomEvent('interaction-stop', { detail: { interactionId } }));
444
459
  }
460
+ recordMetaEvent(this.sessionKey, 'interaction.resolved', { interactionId });
445
461
  interaction.resolve(result);
446
462
  this.pendingInteractions.delete(interactionId);
447
463
  }
@@ -467,16 +483,32 @@ export class ChatDriver extends EventTarget {
467
483
  this.subAgentCompletion = undefined;
468
484
  this.agentReleaseRequested = false;
469
485
  this.appendToHistory({ role: 'user', content: userInput, attachments });
486
+ this.turnStartedAt = Date.now();
487
+ recordMetaEvent(this.sessionKey, 'turn.start', {
488
+ phase: 'sendMessage',
489
+ agent: this.activeAgentName,
490
+ });
470
491
  agenticActivityBus.publish('tool-loop-start', undefined);
471
492
  try {
472
493
  return yield this.runToolLoop(userInput, attachments);
473
494
  }
474
495
  catch (e) {
475
496
  logger.error('ChatDriver error:', e);
497
+ recordMetaEvent(this.sessionKey, 'turn.error', {
498
+ phase: 'sendMessage',
499
+ agent: this.activeAgentName,
500
+ provider: this.lastResolvedProviderName,
501
+ message: e instanceof Error ? e.message : String(e),
502
+ });
476
503
  this.appendToHistory({ role: 'assistant', content: 'Sorry, something went wrong.' });
477
504
  return { reason: 'done' };
478
505
  }
479
506
  finally {
507
+ recordMetaEvent(this.sessionKey, 'turn.end', {
508
+ phase: 'sendMessage',
509
+ agent: this.activeAgentName,
510
+ durationMs: Date.now() - this.turnStartedAt,
511
+ });
480
512
  this.busy = false;
481
513
  agenticActivityBus.publish('tool-loop-end', undefined);
482
514
  }
@@ -601,16 +633,32 @@ export class ChatDriver extends EventTarget {
601
633
  return { reason: 'done' };
602
634
  this.busy = true;
603
635
  this.subAgentCompletion = undefined;
636
+ this.turnStartedAt = Date.now();
637
+ recordMetaEvent(this.sessionKey, 'turn.start', {
638
+ phase: 'continueFromHistory',
639
+ agent: this.activeAgentName,
640
+ });
604
641
  agenticActivityBus.publish('tool-loop-start', undefined);
605
642
  try {
606
643
  return yield this.runToolLoop('', undefined, transientPrimer);
607
644
  }
608
645
  catch (e) {
609
646
  logger.error('ChatDriver error:', e);
647
+ recordMetaEvent(this.sessionKey, 'turn.error', {
648
+ phase: 'continueFromHistory',
649
+ agent: this.activeAgentName,
650
+ provider: this.lastResolvedProviderName,
651
+ message: e instanceof Error ? e.message : String(e),
652
+ });
610
653
  this.appendToHistory({ role: 'assistant', content: 'Sorry, something went wrong.' });
611
654
  return { reason: 'done' };
612
655
  }
613
656
  finally {
657
+ recordMetaEvent(this.sessionKey, 'turn.end', {
658
+ phase: 'continueFromHistory',
659
+ agent: this.activeAgentName,
660
+ durationMs: Date.now() - this.turnStartedAt,
661
+ });
614
662
  this.busy = false;
615
663
  agenticActivityBus.publish('tool-loop-end', undefined);
616
664
  }
@@ -854,6 +902,11 @@ export class ChatDriver extends EventTarget {
854
902
  continue;
855
903
  }
856
904
  logger.error('ChatDriver: MALFORMED_FUNCTION_CALL, max retries reached');
905
+ recordMetaEvent(this.sessionKey, 'turn.error', {
906
+ reason: 'malformed-function-call',
907
+ agent: this.activeAgentName,
908
+ provider: this.lastResolvedProviderName,
909
+ });
857
910
  this.appendToHistory({
858
911
  role: 'assistant',
859
912
  content: "I'm sorry, I wasn't able to complete that request. Please try rephrasing or breaking it into smaller steps.",
@@ -872,6 +925,11 @@ export class ChatDriver extends EventTarget {
872
925
  continue;
873
926
  }
874
927
  logger.error('ChatDriver: empty model response after all retries');
928
+ recordMetaEvent(this.sessionKey, 'turn.error', {
929
+ reason: 'empty-response',
930
+ agent: this.activeAgentName,
931
+ provider: this.lastResolvedProviderName,
932
+ });
875
933
  this.appendToHistory({
876
934
  role: 'assistant',
877
935
  content: 'Remote agent returned no response.',
@@ -976,6 +1034,11 @@ export class ChatDriver extends EventTarget {
976
1034
  }
977
1035
  catch (e) {
978
1036
  logger.error(`ChatDriver tool "${tc.name}" failed:`, e);
1037
+ recordMetaEvent(this.sessionKey, 'tool.failed', {
1038
+ tool: tc.name,
1039
+ agent: this.activeAgentName,
1040
+ message: e instanceof Error ? e.message : String(e),
1041
+ });
979
1042
  executedById.set(tc.id, {
980
1043
  toolCallId: tc.id,
981
1044
  content: `Tool error: ${e.message}`,
@@ -1042,6 +1105,11 @@ export class ChatDriver extends EventTarget {
1042
1105
  }
1043
1106
  if (hitUnknownToolLimit) {
1044
1107
  logger.error(`ChatDriver: unknown-tool limit (${DEFAULT_MAX_UNKNOWN_TOOL_CALLS}) reached — stopping`);
1108
+ recordMetaEvent(this.sessionKey, 'turn.error', {
1109
+ reason: 'unknown-tool-limit',
1110
+ agent: this.activeAgentName,
1111
+ provider: this.lastResolvedProviderName,
1112
+ });
1045
1113
  this.appendToHistory({
1046
1114
  role: 'assistant',
1047
1115
  content: "I'm sorry, I repeatedly tried to use tools that don't exist. Please check your agent configuration or try rephrasing your request.",
@@ -1061,6 +1129,11 @@ export class ChatDriver extends EventTarget {
1061
1129
  }
1062
1130
  if (iterations >= this.maxToolIterations) {
1063
1131
  logger.warn('ChatDriver: reached max tool iterations, stopping');
1132
+ recordMetaEvent(this.sessionKey, 'turn.error', {
1133
+ reason: 'max-iterations',
1134
+ agent: this.activeAgentName,
1135
+ provider: this.lastResolvedProviderName,
1136
+ });
1064
1137
  this.appendToHistory({
1065
1138
  role: 'assistant',
1066
1139
  content: "I've reached my limit for this response. You can ask me to continue and I'll pick up where I left off.",
@@ -1070,10 +1143,16 @@ export class ChatDriver extends EventTarget {
1070
1143
  });
1071
1144
  }
1072
1145
  appendToHistory(message) {
1073
- const tagged = this.activeAgentName
1074
- ? Object.assign(Object.assign({}, message), { agentName: this.activeAgentName,
1146
+ var _a;
1147
+ const tagged = Object.assign(Object.assign(Object.assign({}, message), {
1148
+ // Stamp on first append; preserve any caller-supplied timestamp.
1149
+ timestamp: (_a = message.timestamp) !== null && _a !== void 0 ? _a : new Date().toISOString() }), (this.activeAgentName
1150
+ ? {
1151
+ agentName: this.activeAgentName,
1075
1152
  // Display-only — falls back to agentName in renderers when unset.
1076
- agentLabel: this.activeAgentLabel }) : message;
1153
+ agentLabel: this.activeAgentLabel,
1154
+ }
1155
+ : {}));
1077
1156
  this.history = [...this.history, tagged];
1078
1157
  this.dispatchEvent(new CustomEvent('history-updated', {
1079
1158
  detail: this.history,
@@ -1,5 +1,6 @@
1
1
  import { __awaiter } from "tslib";
2
2
  import { validateStaticAgentProviders } from '../../config/validate-providers';
3
+ import { recordMetaEvent } from '../../state/debug-event-log';
3
4
  import { transformHistoryForAgent } from '../../utils/history-transform';
4
5
  import { logger } from '../../utils/logger';
5
6
  import { ChatDriver, REQUEST_CONTINUATION_TOOL, } from '../chat-driver/chat-driver';
@@ -95,7 +96,7 @@ export class OrchestratingDriver extends EventTarget {
95
96
  const rawFallback = fallbacks[0];
96
97
  this.fallback = rawFallback
97
98
  ? Object.assign(Object.assign({}, rawFallback), { systemPrompt: buildFallbackSystemPrompt(rawFallback, this.specialists) }) : undefined;
98
- this.chatDriver = new ChatDriver(providerRegistry, {}, [], undefined, undefined, options.maxToolIterations, options.maxFoldOperations, options.maxTurnSnapshots);
99
+ this.chatDriver = new ChatDriver(providerRegistry, {}, [], undefined, undefined, options.maxToolIterations, options.maxFoldOperations, options.maxTurnSnapshots, this.sessionKey);
99
100
  // Proxy events from the shared driver
100
101
  this.chatDriver.addEventListener('history-updated', (e) => {
101
102
  this.dispatchEvent(new CustomEvent('history-updated', { detail: e.detail }));
@@ -242,8 +243,17 @@ export class OrchestratingDriver extends EventTarget {
242
243
  }
243
244
  applyAgent(agent) {
244
245
  return __awaiter(this, void 0, void 0, function* () {
246
+ var _a;
245
247
  const previousAgent = this.activeAgent;
246
248
  const isSwitch = !previousAgent || previousAgent.name !== agent.name;
249
+ // Record the agent transition on the debug-log timeline. A `null` `from`
250
+ // marks the initial activation; a named `from` is a handoff between agents.
251
+ if (isSwitch) {
252
+ recordMetaEvent(this.sessionKey, 'agent.handoff', {
253
+ from: (_a = previousAgent === null || previousAgent === void 0 ? void 0 : previousAgent.name) !== null && _a !== void 0 ? _a : null,
254
+ to: agent.name,
255
+ });
256
+ }
247
257
  // Fire lifecycle hooks around the swap — outgoing first, then incoming.
248
258
  // Both are awaited so a heavy `onActivate` (e.g. machine restore) completes
249
259
  // before the agent's first turn runs.
@@ -311,7 +321,10 @@ export class OrchestratingDriver extends EventTarget {
311
321
  }
312
322
  if (previousAgent && previousAgent.name !== agent.name) {
313
323
  const rawHistory = this.chatDriver.getHistory();
314
- this.chatDriver.loadHistory([...rawHistory, { role: 'system-event', content: agent.name }]);
324
+ this.chatDriver.loadHistory([
325
+ ...rawHistory,
326
+ { role: 'system-event', content: agent.name, timestamp: new Date().toISOString() },
327
+ ]);
315
328
  }
316
329
  this.chatDriver.setProviderHistoryTransform((h) => transformHistoryForAgent(h, agent.name));
317
330
  this.chatDriver.applyAgent(agentToApply);
@@ -493,7 +506,10 @@ export class OrchestratingDriver extends EventTarget {
493
506
  }
494
507
  appendInlineMessage(content) {
495
508
  const history = this.chatDriver.getHistory();
496
- this.chatDriver.loadHistory([...history, { role: 'assistant', content }]);
509
+ this.chatDriver.loadHistory([
510
+ ...history,
511
+ { role: 'assistant', content, timestamp: new Date().toISOString() },
512
+ ]);
497
513
  this.dispatchEvent(new CustomEvent('history-updated', { detail: this.chatDriver.getHistory() }));
498
514
  }
499
515
  }
@@ -34,7 +34,8 @@ import { AiChatInteractionWrapper } from '../components/chat-interaction-wrapper
34
34
  import { AiChatMarkdown } from '../components/chat-markdown/chat-markdown';
35
35
  import { AiHaloOverlay } from '../components/halo-overlay';
36
36
  import { OrchestratingDriver } from '../components/orchestrating-driver/orchestrating-driver';
37
- import { getOrCreateDriver, deleteDriver } from '../state/driver-registry';
37
+ import { recordMetaEvent, getMetaEvents, DEBUG_LOG_README, } from '../state/debug-event-log';
38
+ import { getOrCreateDriver, getDriver, deleteDriver } from '../state/driver-registry';
38
39
  import { getSessionStore, hasSessionStore } from '../state/session-store';
39
40
  import { AI_COLOUR_AMBER, AI_COLOUR_CYAN, AI_COLOUR_PINK, AI_COLOUR_VIOLET, } from '../styles/ai-colours';
40
41
  import { ChatSuggestions } from '../suggestions/chat-suggestions';
@@ -130,6 +131,8 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
130
131
  };
131
132
  /** True when the user has intentionally scrolled away from the bottom — suppresses auto-scroll. */
132
133
  this._userScrolledAway = false;
134
+ /** Whether the one-shot ≥80% `context.threshold-crossed` event has fired this session. */
135
+ this._contextThresholdCrossed = false;
133
136
  /**
134
137
  * Interaction widgets that grow mid-lifecycle (e.g. expanding a "More info"
135
138
  * panel, appending an SSE status row, revealing generated code) bubble a
@@ -190,6 +193,11 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
190
193
  var _a, _b;
191
194
  const prev = (_a = this._sessionRef) === null || _a === void 0 ? void 0 : _a.store.aiAssistant.state;
192
195
  (_b = this._sessionRef) === null || _b === void 0 ? void 0 : _b.actions.aiAssistant.setState(value);
196
+ // Only record a real transition against a live session — a missing
197
+ // _sessionRef means setState no-op'd, so no transition actually happened.
198
+ if (this._sessionRef && prev !== value) {
199
+ this.logMeta('state.changed', { from: prev, to: value });
200
+ }
193
201
  this.syncShowHalo();
194
202
  // When the agent finishes (loading → !loading) the input row reappears (or
195
203
  // becomes enabled) — refocus it so the user can type immediately, but only
@@ -327,6 +335,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
327
335
  // Suggestions are scoped to the active routing — resetting forces a fresh
328
336
  // fetch when the user pins/unpins so the prompts match the new agent.
329
337
  if (previous !== value) {
338
+ this.logMeta(value ? 'agent.pinned' : 'agent.unpinned', { agent: value !== null && value !== void 0 ? value : previous });
330
339
  this.suggestionsState = { status: 'idle' };
331
340
  this.fetchSuggestions();
332
341
  }
@@ -576,6 +585,10 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
576
585
  if (history.length)
577
586
  this.driver.loadHistory([...history]);
578
587
  this._driverAgentsKey = newKey;
588
+ this.logMeta('driver.created', {
589
+ reason: 'agents-changed',
590
+ driverKind: this.driver instanceof OrchestratingDriver ? 'orchestrating' : 'chat',
591
+ });
579
592
  }
580
593
  /** Returns a stable fingerprint for an agents array based on agent names and tool handler keys. */
581
594
  getAgentsKey(agents) {
@@ -631,7 +644,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
631
644
  }
632
645
  }
633
646
  createDriver() {
634
- var _a, _b;
647
+ var _a, _b, _c;
635
648
  this.warnUnreachableStatefulAgents();
636
649
  const agent = (_a = this.chatConfig.agent) !== null && _a !== void 0 ? _a : {};
637
650
  const { agents } = this;
@@ -659,7 +672,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
659
672
  maxTurnSnapshots: agent.maxTurnSnapshots,
660
673
  });
661
674
  }
662
- return new ChatDriver(this.providerRegistry, {}, [], undefined, undefined, agent.maxToolIterations, agent.maxFoldOperations, agent.maxTurnSnapshots);
675
+ return new ChatDriver(this.providerRegistry, {}, [], undefined, undefined, agent.maxToolIterations, agent.maxFoldOperations, agent.maxTurnSnapshots, (_c = this.getStateKey()) !== null && _c !== void 0 ? _c : '');
663
676
  }
664
677
  /**
665
678
  * Attaches event listeners to the current driver. Stores a cleanup function
@@ -811,7 +824,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
811
824
  this.driverCleanup = undefined;
812
825
  }
813
826
  connectedCallback() {
814
- var _a, _b, _c, _d, _e, _f, _j;
827
+ var _a, _b, _c, _d, _e, _f, _j, _k, _l;
815
828
  // Initialise the store reference BEFORE super.connectedCallback() so that
816
829
  // the first FAST render has access to the store. The store Proxy calls
817
830
  // Observable.track(observableStore, sliceName) whenever a slice is read,
@@ -835,6 +848,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
835
848
  this.pinnedAgentName = defaultAgent;
836
849
  }
837
850
  }
851
+ const driverExisted = !!getDriver(key);
838
852
  this.driver = getOrCreateDriver(key, () => this.createDriver());
839
853
  this._driverAgentsKey = this.getAgentsKey(this.agents);
840
854
  this.wireDriver();
@@ -843,9 +857,11 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
843
857
  // panel, a collapse-mode element connects and wires to the same shared
844
858
  // driver. Unwire this (hidden) instance on popout; re-wire on popin.
845
859
  const unsubPopout = agenticActivityBus.subscribe('chat-popout', () => {
860
+ this.logMeta('driver.unwired', { reason: 'popout' });
846
861
  this.unwireDriver();
847
862
  });
848
863
  const unsubPopin = agenticActivityBus.subscribe('chat-popin', () => {
864
+ this.logMeta('driver.wired', { reason: 'popin' });
849
865
  this.wireDriver();
850
866
  });
851
867
  this.unsubBus = () => {
@@ -896,9 +912,16 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
896
912
  if (this.state !== 'loading')
897
913
  this.maybeAutoFocusChatInput();
898
914
  logger.debug('FoundationAiAssistant connected');
915
+ this.logMeta('assistant.connected', {
916
+ store: isNewStore ? 'created' : 'restored',
917
+ restoredMessages: this.messages.length,
918
+ driver: driverExisted ? 'reused' : 'created',
919
+ driverKind: this.driver instanceof OrchestratingDriver ? 'orchestrating' : 'chat',
920
+ driverBusy: (_l = (_k = this.driver) === null || _k === void 0 ? void 0 : _k.isBusy()) !== null && _l !== void 0 ? _l : false,
921
+ });
899
922
  }
900
923
  disconnectedCallback() {
901
- var _a, _b;
924
+ var _a, _b, _c, _d;
902
925
  super.disconnectedCallback();
903
926
  this.stopLoadingTimer();
904
927
  this.state = 'idle';
@@ -920,6 +943,8 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
920
943
  // the panel re-mounts open.
921
944
  this._agentPickerToggle.finalize();
922
945
  this._settingsToggle.finalize();
946
+ // Capture before clearing — `wasBusy` reads the driver, which is dropped below.
947
+ this.logMeta('assistant.disconnected', { wasBusy: (_d = (_c = this.driver) === null || _c === void 0 ? void 0 : _c.isBusy()) !== null && _d !== void 0 ? _d : false });
923
948
  // Clear local references only — driver and store stay in their registries.
924
949
  this.driver = undefined;
925
950
  this._sessionRef = undefined;
@@ -1008,6 +1033,31 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1008
1033
  if (runningCost !== this.sessionCostUsd) {
1009
1034
  this.sessionCostUsd = runningCost;
1010
1035
  }
1036
+ // Record a context.updated meta event when the token count changes (≈once
1037
+ // per LLM call, as a new usage-bearing message arrives), plus a one-shot
1038
+ // threshold-crossed event the first time usage passes 80%.
1039
+ if (this.contextTokens != null && this.contextTokens !== this._lastLoggedContextTokens) {
1040
+ this._lastLoggedContextTokens = this.contextTokens;
1041
+ const usagePercent = this.contextLimit != null && this.contextLimit > 0
1042
+ ? Math.round((this.contextTokens / this.contextLimit) * 100)
1043
+ : undefined;
1044
+ this.logMeta('context.updated', {
1045
+ tokens: this.contextTokens,
1046
+ limit: this.contextLimit,
1047
+ usagePercent,
1048
+ costUsd: this.sessionCostUsd,
1049
+ });
1050
+ if (usagePercent != null &&
1051
+ usagePercent >= FoundationAiAssistant_1.CONTEXT_USAGE_WARN_PERCENT &&
1052
+ !this._contextThresholdCrossed) {
1053
+ this._contextThresholdCrossed = true;
1054
+ this.logMeta('context.threshold-crossed', {
1055
+ usagePercent,
1056
+ tokens: this.contextTokens,
1057
+ limit: this.contextLimit,
1058
+ });
1059
+ }
1060
+ }
1011
1061
  // Publish halo-start whenever a new toolCalls message arrives.
1012
1062
  if (this.showHalo !== 'no' && ((_a = this.enabledAnimations) === null || _a === void 0 ? void 0 : _a.includes('halo'))) {
1013
1063
  const last = this.messages[this.messages.length - 1];
@@ -1067,9 +1117,11 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1067
1117
  /** Called when the user clicks the popout button. */
1068
1118
  handlePopout() {
1069
1119
  if (this.popoutMode === 'expand') {
1120
+ this.logMeta('assistant.popout');
1070
1121
  agenticActivityBus.publish('chat-popout', undefined);
1071
1122
  }
1072
1123
  else if (this.popoutMode === 'collapse') {
1124
+ this.logMeta('assistant.popin');
1073
1125
  agenticActivityBus.publish('chat-popin', undefined);
1074
1126
  }
1075
1127
  }
@@ -1078,10 +1130,30 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1078
1130
  const parts = [this.id, this.headerTitle].filter(Boolean);
1079
1131
  return parts.length ? parts.join('::') : undefined;
1080
1132
  }
1133
+ /**
1134
+ * Record a meta/lifecycle event onto this session's debug-log timeline
1135
+ * (exported via {@link FoundationAiAssistant.getDebugLog}). Every event
1136
+ * auto-carries `placement` so a session emitted from both the bubble and the
1137
+ * layout panel stays disambiguated. Keyed by `stateKey` so the timeline is
1138
+ * shared across pop-in/pop-out and survives driver rebuilds.
1139
+ */
1140
+ logMeta(type, detail) {
1141
+ const key = this.getStateKey();
1142
+ if (!key)
1143
+ return;
1144
+ const placement = this.popoutMode === 'expand'
1145
+ ? 'bubble'
1146
+ : this.popoutMode === 'collapse'
1147
+ ? 'panel'
1148
+ : 'standalone';
1149
+ recordMetaEvent(key, type, Object.assign({ placement }, detail));
1150
+ }
1081
1151
  toggleSettings() {
1152
+ this.logMeta('panel.toggled', { panel: 'settings', open: !this.settingsOpen });
1082
1153
  this._settingsToggle.toggle();
1083
1154
  }
1084
1155
  toggleAgentPicker() {
1156
+ this.logMeta('panel.toggled', { panel: 'agent-picker', open: !this.agentPickerOpen });
1085
1157
  this._agentPickerToggle.toggle();
1086
1158
  }
1087
1159
  /**
@@ -1213,12 +1285,66 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1213
1285
  const contextUsagePercent = this.contextTokens != null && this.contextLimit != null && this.contextLimit > 0
1214
1286
  ? Math.round((this.contextTokens / this.contextLimit) * 100)
1215
1287
  : undefined;
1288
+ const stateKey = this.getStateKey();
1289
+ // Collapse repeated system prompts across consecutive turns. A stateful
1290
+ // agent's prompt changes between turns, but a stable agent repeats the same
1291
+ // (often multi-KB) prompt every turn — so replace a turn's `systemPrompt`
1292
+ // with a sentinel when it's byte-identical to the previous turn's. The
1293
+ // prompt is still shown in full whenever it changes, so prompt evolution
1294
+ // stays visible.
1295
+ let lastFullPrompt;
1296
+ let lastFullIndex = -1;
1297
+ const turns = ((_e = (_d = (_c = this.driver) === null || _c === void 0 ? void 0 : _c.getTurnSnapshots) === null || _d === void 0 ? void 0 : _d.call(_c)) !== null && _e !== void 0 ? _e : []).map((t) => {
1298
+ let { systemPrompt } = t;
1299
+ if (systemPrompt != null && systemPrompt === lastFullPrompt) {
1300
+ systemPrompt = `<repeated — identical to turn ${lastFullIndex}>`;
1301
+ }
1302
+ else if (systemPrompt != null) {
1303
+ lastFullPrompt = systemPrompt;
1304
+ lastFullIndex = t.turnIndex;
1305
+ }
1306
+ return Object.assign(Object.assign({ kind: 'turn' }, t), { systemPrompt });
1307
+ });
1308
+ // Single chronological timeline. The conversation messages, per-LLM-call
1309
+ // turn snapshots, and lifecycle/structural meta events are stored separately
1310
+ // in memory (history on the driver, turn buffer on the driver, meta buffer
1311
+ // keyed by stateKey) but interleaved here so the exported log reads
1312
+ // top-to-bottom as one sequence — no cross-referencing parallel arrays.
1313
+ // Every entry is tagged `kind`. All three streams stamp ISO timestamps in
1314
+ // the same format, so a string compare orders them chronologically; ties
1315
+ // break by `kind` (event → turn → message: the cause, then the call it
1316
+ // triggered, then the output) and are otherwise stable within a stream.
1317
+ const KIND_RANK = {
1318
+ event: 0,
1319
+ turn: 1,
1320
+ message: 2,
1321
+ };
1322
+ const messages = (_k = (_j = (_f = this.driver) === null || _f === void 0 ? void 0 : _f.getRawHistory) === null || _j === void 0 ? void 0 : _j.call(_f)) !== null && _k !== void 0 ? _k : this.messages;
1323
+ const timeline = [
1324
+ ...messages.map((m) => (Object.assign({ kind: 'message' }, m))),
1325
+ ...turns,
1326
+ ...(stateKey ? getMetaEvents(stateKey) : []).map((e) => (Object.assign({ kind: 'event' }, e))),
1327
+ ].sort((a, b) => {
1328
+ var _a, _b;
1329
+ const ta = (_a = a.timestamp) !== null && _a !== void 0 ? _a : '';
1330
+ const tb = (_b = b.timestamp) !== null && _b !== void 0 ? _b : '';
1331
+ if (ta < tb)
1332
+ return -1;
1333
+ if (ta > tb)
1334
+ return 1;
1335
+ return KIND_RANK[a.kind] - KIND_RANK[b.kind];
1336
+ });
1216
1337
  return {
1217
- messages: (_e = (_d = (_c = this.driver) === null || _c === void 0 ? void 0 : _c.getRawHistory) === null || _d === void 0 ? void 0 : _d.call(_c)) !== null && _e !== void 0 ? _e : this.messages,
1338
+ // First key so it's at the top of the file tells whoever opens the JSON
1339
+ // (often an AI agent) how to read the rest. See debug-event-log.ts.
1340
+ readme: DEBUG_LOG_README,
1341
+ // The whole session as one readable sequence. The conversation transcript
1342
+ // lives here as `kind: 'message'` entries rather than in a parallel block.
1343
+ timeline,
1218
1344
  meta: {
1219
1345
  timestamp,
1220
1346
  host: window.location.host,
1221
- agentSummary: (_f = this.agents) === null || _f === void 0 ? void 0 : _f.map((a) => {
1347
+ agentSummary: (_l = this.agents) === null || _l === void 0 ? void 0 : _l.map((a) => {
1222
1348
  var _a;
1223
1349
  return (Object.assign(Object.assign({}, a), { toolDefinitions: Array.isArray(a.toolDefinitions)
1224
1350
  ? typeof a.toolHandlers === 'function'
@@ -1230,10 +1356,10 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1230
1356
  ? '<dynamic — resolved per turn>'
1231
1357
  : [], toolHandlers: undefined, onActivate: undefined, onDeactivate: undefined, getDebugSnapshot: undefined }));
1232
1358
  }),
1233
- activeSystemPrompt: typeof ((_j = this.activeAgent) === null || _j === void 0 ? void 0 : _j.systemPrompt) === 'function'
1359
+ activeSystemPrompt: typeof ((_m = this.activeAgent) === null || _m === void 0 ? void 0 : _m.systemPrompt) === 'function'
1234
1360
  ? '<dynamic — resolved per turn>'
1235
- : (_k = this.activeAgent) === null || _k === void 0 ? void 0 : _k.systemPrompt,
1236
- activePrimerHistory: (_l = this.activeAgent) === null || _l === void 0 ? void 0 : _l.primerHistory,
1361
+ : (_o = this.activeAgent) === null || _o === void 0 ? void 0 : _o.systemPrompt,
1362
+ activePrimerHistory: (_p = this.activeAgent) === null || _p === void 0 ? void 0 : _p.primerHistory,
1237
1363
  activeFoldStack: this.driver instanceof ChatDriver ? this.driver.getActiveFoldNames() : undefined,
1238
1364
  // Context window + cost snapshot. `sessionCostUsd` is the chat-scoped
1239
1365
  // total (per-message `cost` summed); the transport's lifetime cost is
@@ -1250,9 +1376,6 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1250
1376
  // Snapshot captured fresh at log-export time — reflects state NOW, which
1251
1377
  // may have transitioned since the last LLM call.
1252
1378
  activeDebugSnapshot,
1253
- // Per-LLM-call timeline — pairs each turn with the prompt + tool surface
1254
- // + agent state that drove it. Capped to the most recent N entries.
1255
- turnSnapshots: (_p = (_o = (_m = this.driver) === null || _m === void 0 ? void 0 : _m.getTurnSnapshots) === null || _o === void 0 ? void 0 : _o.call(_m)) !== null && _p !== void 0 ? _p : [],
1256
1379
  debug: (_q = this.debugStateFactory) === null || _q === void 0 ? void 0 : _q.call(this),
1257
1380
  },
1258
1381
  };
@@ -1325,6 +1448,10 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1325
1448
  }
1326
1449
  catch (readError) {
1327
1450
  logger.error('Failed to read file:', readError);
1451
+ this.logMeta('file.read-failed', {
1452
+ file: file.name,
1453
+ message: readError instanceof Error ? readError.message : String(readError),
1454
+ });
1328
1455
  return { ok: false, message: `Failed to read "${file.name}".` };
1329
1456
  }
1330
1457
  })));
@@ -1345,8 +1472,13 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1345
1472
  const files = Array.from((_a = input.files) !== null && _a !== void 0 ? _a : []);
1346
1473
  input.value = '';
1347
1474
  const { attachments, errors } = yield this.processFiles(files);
1348
- if (attachments.length)
1475
+ if (attachments.length) {
1349
1476
  this.attachments = [...this.attachments, ...attachments];
1477
+ this.logMeta('attachment.added', {
1478
+ count: attachments.length,
1479
+ names: attachments.map((a) => a.name),
1480
+ });
1481
+ }
1350
1482
  if (errors.length)
1351
1483
  this.attachmentErrors = [...this.attachmentErrors, ...errors];
1352
1484
  });
@@ -1474,6 +1606,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1474
1606
  return;
1475
1607
  this.suggestionsState = { status: 'error', message: e.message };
1476
1608
  logger.error('Failed to fetch suggestions:', e);
1609
+ this.logMeta('suggestions.failed', { message: e.message });
1477
1610
  }
1478
1611
  });
1479
1612
  }
@@ -1544,6 +1677,8 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
1544
1677
  }
1545
1678
  };
1546
1679
  FoundationAiAssistant.SCROLL_BOTTOM_THRESHOLD_PX = 50;
1680
+ /** Context-usage percentage that fires the one-shot `context.threshold-crossed` event. */
1681
+ FoundationAiAssistant.CONTEXT_USAGE_WARN_PERCENT = 80;
1547
1682
  FoundationAiAssistant.DEFAULT_LOADING_DELAY_S = 5;
1548
1683
  FoundationAiAssistant.DEFAULT_SUGGESTION_COUNT = 3;
1549
1684
  FoundationAiAssistant.MS_PER_SECOND = 1000;