@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.
package/src/main/main.ts CHANGED
@@ -49,7 +49,13 @@ import { AiChatMarkdown } from '../components/chat-markdown/chat-markdown';
49
49
  import { AiHaloOverlay } from '../components/halo-overlay';
50
50
  import { OrchestratingDriver } from '../components/orchestrating-driver/orchestrating-driver';
51
51
  import type { AgentConfig } from '../config/config';
52
- import { getOrCreateDriver, deleteDriver } from '../state/driver-registry';
52
+ import {
53
+ recordMetaEvent,
54
+ getMetaEvents,
55
+ DEBUG_LOG_README,
56
+ type MetaEventType,
57
+ } from '../state/debug-event-log';
58
+ import { getOrCreateDriver, getDriver, deleteDriver } from '../state/driver-registry';
53
59
  import { getSessionStore, hasSessionStore, type SessionStoreReturn } from '../state/session-store';
54
60
  import {
55
61
  AI_COLOUR_AMBER,
@@ -228,6 +234,11 @@ export class FoundationAiAssistant extends GenesisElement {
228
234
  set state(value: AiAssistantState) {
229
235
  const prev = this._sessionRef?.store.aiAssistant.state;
230
236
  this._sessionRef?.actions.aiAssistant.setState(value);
237
+ // Only record a real transition against a live session — a missing
238
+ // _sessionRef means setState no-op'd, so no transition actually happened.
239
+ if (this._sessionRef && prev !== value) {
240
+ this.logMeta('state.changed', { from: prev, to: value });
241
+ }
231
242
  this.syncShowHalo();
232
243
  // When the agent finishes (loading → !loading) the input row reappears (or
233
244
  // becomes enabled) — refocus it so the user can type immediately, but only
@@ -354,6 +365,7 @@ export class FoundationAiAssistant extends GenesisElement {
354
365
  // Suggestions are scoped to the active routing — resetting forces a fresh
355
366
  // fetch when the user pins/unpins so the prompts match the new agent.
356
367
  if (previous !== value) {
368
+ this.logMeta(value ? 'agent.pinned' : 'agent.unpinned', { agent: value ?? previous });
357
369
  this.suggestionsState = { status: 'idle' };
358
370
  this.fetchSuggestions();
359
371
  }
@@ -508,6 +520,12 @@ export class FoundationAiAssistant extends GenesisElement {
508
520
  private _userScrolledAway = false;
509
521
  private _scrollListener?: () => void;
510
522
  private static readonly SCROLL_BOTTOM_THRESHOLD_PX = 50;
523
+ /** Context-usage percentage that fires the one-shot `context.threshold-crossed` event. */
524
+ private static readonly CONTEXT_USAGE_WARN_PERCENT = 80;
525
+ /** Last `contextTokens` logged to the timeline — dedupes the `context.updated` event. */
526
+ private _lastLoggedContextTokens?: number;
527
+ /** Whether the one-shot ≥80% `context.threshold-crossed` event has fired this session. */
528
+ private _contextThresholdCrossed = false;
511
529
  /**
512
530
  * Interaction widgets that grow mid-lifecycle (e.g. expanding a "More info"
513
531
  * panel, appending an SSE status row, revealing generated code) bubble a
@@ -648,6 +666,10 @@ export class FoundationAiAssistant extends GenesisElement {
648
666
  this.wireDriver();
649
667
  if (history.length) this.driver.loadHistory([...history]);
650
668
  this._driverAgentsKey = newKey;
669
+ this.logMeta('driver.created', {
670
+ reason: 'agents-changed',
671
+ driverKind: this.driver instanceof OrchestratingDriver ? 'orchestrating' : 'chat',
672
+ });
651
673
  }
652
674
 
653
675
  /** Returns a stable fingerprint for an agents array based on agent names and tool handler keys. */
@@ -741,6 +763,7 @@ export class FoundationAiAssistant extends GenesisElement {
741
763
  agent.maxToolIterations,
742
764
  agent.maxFoldOperations,
743
765
  agent.maxTurnSnapshots,
766
+ this.getStateKey() ?? '',
744
767
  );
745
768
  }
746
769
 
@@ -930,6 +953,7 @@ export class FoundationAiAssistant extends GenesisElement {
930
953
  }
931
954
  }
932
955
 
956
+ const driverExisted = !!getDriver(key);
933
957
  this.driver = getOrCreateDriver(key, () => this.createDriver());
934
958
  this._driverAgentsKey = this.getAgentsKey(this.agents);
935
959
  this.wireDriver();
@@ -939,9 +963,11 @@ export class FoundationAiAssistant extends GenesisElement {
939
963
  // panel, a collapse-mode element connects and wires to the same shared
940
964
  // driver. Unwire this (hidden) instance on popout; re-wire on popin.
941
965
  const unsubPopout = agenticActivityBus.subscribe('chat-popout', () => {
966
+ this.logMeta('driver.unwired', { reason: 'popout' });
942
967
  this.unwireDriver();
943
968
  });
944
969
  const unsubPopin = agenticActivityBus.subscribe('chat-popin', () => {
970
+ this.logMeta('driver.wired', { reason: 'popin' });
945
971
  this.wireDriver();
946
972
  });
947
973
  this.unsubBus = () => {
@@ -994,6 +1020,13 @@ export class FoundationAiAssistant extends GenesisElement {
994
1020
  // where state is loading and the input may be hidden or disabled).
995
1021
  if (this.state !== 'loading') this.maybeAutoFocusChatInput();
996
1022
  logger.debug('FoundationAiAssistant connected');
1023
+ this.logMeta('assistant.connected', {
1024
+ store: isNewStore ? 'created' : 'restored',
1025
+ restoredMessages: this.messages.length,
1026
+ driver: driverExisted ? 'reused' : 'created',
1027
+ driverKind: this.driver instanceof OrchestratingDriver ? 'orchestrating' : 'chat',
1028
+ driverBusy: this.driver?.isBusy() ?? false,
1029
+ });
997
1030
  }
998
1031
 
999
1032
  disconnectedCallback() {
@@ -1018,6 +1051,8 @@ export class FoundationAiAssistant extends GenesisElement {
1018
1051
  // the panel re-mounts open.
1019
1052
  this._agentPickerToggle.finalize();
1020
1053
  this._settingsToggle.finalize();
1054
+ // Capture before clearing — `wasBusy` reads the driver, which is dropped below.
1055
+ this.logMeta('assistant.disconnected', { wasBusy: this.driver?.isBusy() ?? false });
1021
1056
  // Clear local references only — driver and store stay in their registries.
1022
1057
  this.driver = undefined;
1023
1058
  this._sessionRef = undefined;
@@ -1101,6 +1136,34 @@ export class FoundationAiAssistant extends GenesisElement {
1101
1136
  if (runningCost !== this.sessionCostUsd) {
1102
1137
  this.sessionCostUsd = runningCost;
1103
1138
  }
1139
+ // Record a context.updated meta event when the token count changes (≈once
1140
+ // per LLM call, as a new usage-bearing message arrives), plus a one-shot
1141
+ // threshold-crossed event the first time usage passes 80%.
1142
+ if (this.contextTokens != null && this.contextTokens !== this._lastLoggedContextTokens) {
1143
+ this._lastLoggedContextTokens = this.contextTokens;
1144
+ const usagePercent =
1145
+ this.contextLimit != null && this.contextLimit > 0
1146
+ ? Math.round((this.contextTokens / this.contextLimit) * 100)
1147
+ : undefined;
1148
+ this.logMeta('context.updated', {
1149
+ tokens: this.contextTokens,
1150
+ limit: this.contextLimit,
1151
+ usagePercent,
1152
+ costUsd: this.sessionCostUsd,
1153
+ });
1154
+ if (
1155
+ usagePercent != null &&
1156
+ usagePercent >= FoundationAiAssistant.CONTEXT_USAGE_WARN_PERCENT &&
1157
+ !this._contextThresholdCrossed
1158
+ ) {
1159
+ this._contextThresholdCrossed = true;
1160
+ this.logMeta('context.threshold-crossed', {
1161
+ usagePercent,
1162
+ tokens: this.contextTokens,
1163
+ limit: this.contextLimit,
1164
+ });
1165
+ }
1166
+ }
1104
1167
  // Publish halo-start whenever a new toolCalls message arrives.
1105
1168
  if (this.showHalo !== 'no' && this.enabledAnimations?.includes('halo')) {
1106
1169
  const last = this.messages[this.messages.length - 1];
@@ -1166,8 +1229,10 @@ export class FoundationAiAssistant extends GenesisElement {
1166
1229
  /** Called when the user clicks the popout button. */
1167
1230
  handlePopout(): void {
1168
1231
  if (this.popoutMode === 'expand') {
1232
+ this.logMeta('assistant.popout');
1169
1233
  agenticActivityBus.publish('chat-popout', undefined);
1170
1234
  } else if (this.popoutMode === 'collapse') {
1235
+ this.logMeta('assistant.popin');
1171
1236
  agenticActivityBus.publish('chat-popin', undefined);
1172
1237
  }
1173
1238
  }
@@ -1178,6 +1243,25 @@ export class FoundationAiAssistant extends GenesisElement {
1178
1243
  return parts.length ? parts.join('::') : undefined;
1179
1244
  }
1180
1245
 
1246
+ /**
1247
+ * Record a meta/lifecycle event onto this session's debug-log timeline
1248
+ * (exported via {@link FoundationAiAssistant.getDebugLog}). Every event
1249
+ * auto-carries `placement` so a session emitted from both the bubble and the
1250
+ * layout panel stays disambiguated. Keyed by `stateKey` so the timeline is
1251
+ * shared across pop-in/pop-out and survives driver rebuilds.
1252
+ */
1253
+ private logMeta(type: MetaEventType, detail?: Record<string, unknown>): void {
1254
+ const key = this.getStateKey();
1255
+ if (!key) return;
1256
+ const placement =
1257
+ this.popoutMode === 'expand'
1258
+ ? 'bubble'
1259
+ : this.popoutMode === 'collapse'
1260
+ ? 'panel'
1261
+ : 'standalone';
1262
+ recordMetaEvent(key, type, { placement, ...detail });
1263
+ }
1264
+
1181
1265
  private readonly _settingsToggle = new AnimatedPanelToggle(
1182
1266
  this,
1183
1267
  '.settings-panel',
@@ -1189,6 +1273,7 @@ export class FoundationAiAssistant extends GenesisElement {
1189
1273
  );
1190
1274
 
1191
1275
  toggleSettings() {
1276
+ this.logMeta('panel.toggled', { panel: 'settings', open: !this.settingsOpen });
1192
1277
  this._settingsToggle.toggle();
1193
1278
  }
1194
1279
 
@@ -1203,6 +1288,7 @@ export class FoundationAiAssistant extends GenesisElement {
1203
1288
  );
1204
1289
 
1205
1290
  toggleAgentPicker() {
1291
+ this.logMeta('panel.toggled', { panel: 'agent-picker', open: !this.agentPickerOpen });
1206
1292
  this._agentPickerToggle.toggle();
1207
1293
  }
1208
1294
 
@@ -1337,8 +1423,60 @@ export class FoundationAiAssistant extends GenesisElement {
1337
1423
  this.contextTokens != null && this.contextLimit != null && this.contextLimit > 0
1338
1424
  ? Math.round((this.contextTokens / this.contextLimit) * 100)
1339
1425
  : undefined;
1426
+ const stateKey = this.getStateKey();
1427
+
1428
+ // Collapse repeated system prompts across consecutive turns. A stateful
1429
+ // agent's prompt changes between turns, but a stable agent repeats the same
1430
+ // (often multi-KB) prompt every turn — so replace a turn's `systemPrompt`
1431
+ // with a sentinel when it's byte-identical to the previous turn's. The
1432
+ // prompt is still shown in full whenever it changes, so prompt evolution
1433
+ // stays visible.
1434
+ let lastFullPrompt: string | undefined;
1435
+ let lastFullIndex = -1;
1436
+ const turns = (this.driver?.getTurnSnapshots?.() ?? []).map((t) => {
1437
+ let { systemPrompt } = t;
1438
+ if (systemPrompt != null && systemPrompt === lastFullPrompt) {
1439
+ systemPrompt = `<repeated — identical to turn ${lastFullIndex}>`;
1440
+ } else if (systemPrompt != null) {
1441
+ lastFullPrompt = systemPrompt;
1442
+ lastFullIndex = t.turnIndex;
1443
+ }
1444
+ return { kind: 'turn' as const, ...t, systemPrompt };
1445
+ });
1446
+
1447
+ // Single chronological timeline. The conversation messages, per-LLM-call
1448
+ // turn snapshots, and lifecycle/structural meta events are stored separately
1449
+ // in memory (history on the driver, turn buffer on the driver, meta buffer
1450
+ // keyed by stateKey) but interleaved here so the exported log reads
1451
+ // top-to-bottom as one sequence — no cross-referencing parallel arrays.
1452
+ // Every entry is tagged `kind`. All three streams stamp ISO timestamps in
1453
+ // the same format, so a string compare orders them chronologically; ties
1454
+ // break by `kind` (event → turn → message: the cause, then the call it
1455
+ // triggered, then the output) and are otherwise stable within a stream.
1456
+ const KIND_RANK: Record<'event' | 'turn' | 'message', number> = {
1457
+ event: 0,
1458
+ turn: 1,
1459
+ message: 2,
1460
+ };
1461
+ const messages = this.driver?.getRawHistory?.() ?? this.messages;
1462
+ const timeline = [
1463
+ ...messages.map((m) => ({ kind: 'message' as const, ...m })),
1464
+ ...turns,
1465
+ ...(stateKey ? getMetaEvents(stateKey) : []).map((e) => ({ kind: 'event' as const, ...e })),
1466
+ ].sort((a, b) => {
1467
+ const ta = a.timestamp ?? '';
1468
+ const tb = b.timestamp ?? '';
1469
+ if (ta < tb) return -1;
1470
+ if (ta > tb) return 1;
1471
+ return KIND_RANK[a.kind] - KIND_RANK[b.kind];
1472
+ });
1340
1473
  return {
1341
- messages: this.driver?.getRawHistory?.() ?? this.messages,
1474
+ // First key so it's at the top of the file — tells whoever opens the JSON
1475
+ // (often an AI agent) how to read the rest. See debug-event-log.ts.
1476
+ readme: DEBUG_LOG_README,
1477
+ // The whole session as one readable sequence. The conversation transcript
1478
+ // lives here as `kind: 'message'` entries rather than in a parallel block.
1479
+ timeline,
1342
1480
  meta: {
1343
1481
  timestamp,
1344
1482
  host: window.location.host,
@@ -1380,9 +1518,6 @@ export class FoundationAiAssistant extends GenesisElement {
1380
1518
  // Snapshot captured fresh at log-export time — reflects state NOW, which
1381
1519
  // may have transitioned since the last LLM call.
1382
1520
  activeDebugSnapshot,
1383
- // Per-LLM-call timeline — pairs each turn with the prompt + tool surface
1384
- // + agent state that drove it. Capped to the most recent N entries.
1385
- turnSnapshots: this.driver?.getTurnSnapshots?.() ?? [],
1386
1521
  debug: this.debugStateFactory?.(),
1387
1522
  },
1388
1523
  };
@@ -1466,6 +1601,10 @@ export class FoundationAiAssistant extends GenesisElement {
1466
1601
  };
1467
1602
  } catch (readError) {
1468
1603
  logger.error('Failed to read file:', readError);
1604
+ this.logMeta('file.read-failed', {
1605
+ file: file.name,
1606
+ message: readError instanceof Error ? readError.message : String(readError),
1607
+ });
1469
1608
  return { ok: false, message: `Failed to read "${file.name}".` };
1470
1609
  }
1471
1610
  }),
@@ -1486,7 +1625,13 @@ export class FoundationAiAssistant extends GenesisElement {
1486
1625
  const files = Array.from(input.files ?? []);
1487
1626
  input.value = '';
1488
1627
  const { attachments, errors } = await this.processFiles(files);
1489
- if (attachments.length) this.attachments = [...this.attachments, ...attachments];
1628
+ if (attachments.length) {
1629
+ this.attachments = [...this.attachments, ...attachments];
1630
+ this.logMeta('attachment.added', {
1631
+ count: attachments.length,
1632
+ names: attachments.map((a) => a.name),
1633
+ });
1634
+ }
1490
1635
  if (errors.length) this.attachmentErrors = [...this.attachmentErrors, ...errors];
1491
1636
  }
1492
1637
 
@@ -1619,6 +1764,7 @@ export class FoundationAiAssistant extends GenesisElement {
1619
1764
  if (generation !== this._suggestionsGeneration) return;
1620
1765
  this.suggestionsState = { status: 'error', message: (e as Error).message };
1621
1766
  logger.error('Failed to fetch suggestions:', e);
1767
+ this.logMeta('suggestions.failed', { message: (e as Error).message });
1622
1768
  }
1623
1769
  }
1624
1770
 
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Module-level append-only timeline of meta/lifecycle events, keyed by session
3
+ * identity (the same `stateKey` space used by the driver registry and the
4
+ * session store).
5
+ *
6
+ * Why a module registry rather than the redux store or the driver:
7
+ * - **Survives the pop-in/pop-out element churn.** A single session can be
8
+ * emitted from up to two element instances (an `expand` bubble + a `collapse`
9
+ * panel) sharing one driver; keying by `stateKey` unifies them into one
10
+ * timeline.
11
+ * - **Survives driver rebuilds.** The driver is torn down and recreated when the
12
+ * host swaps the agents array (see `agentsChanged` / `deleteDriver`), so a
13
+ * driver-owned buffer would lose the timeline mid-session. The `stateKey`
14
+ * outlives the driver.
15
+ * - **Stays out of redux reactivity.** These appends never drive the UI, so
16
+ * there's no reason to pay immutable-reducer cost or flood the Redux DevTools
17
+ * action log with debug noise.
18
+ *
19
+ * Surfaced under `getDebugLog().meta.events`. Ring-buffered so a long-lived
20
+ * session can't grow the timeline unbounded.
21
+ *
22
+ * @internal
23
+ */
24
+
25
+ /**
26
+ * Catalogue of meta event names. This is the documented surface — extend it as
27
+ * new events are wired in (Tier 2/3 lifecycle, interaction, provider events).
28
+ */
29
+ export type MetaEventType =
30
+ // Element lifecycle + window placement
31
+ | 'assistant.connected'
32
+ | 'assistant.disconnected'
33
+ | 'assistant.popout'
34
+ | 'assistant.popin'
35
+ // Driver lifecycle / wiring
36
+ | 'driver.created'
37
+ | 'driver.wired'
38
+ | 'driver.unwired'
39
+ // Turn lifecycle
40
+ | 'state.changed'
41
+ | 'turn.start'
42
+ | 'turn.end'
43
+ | 'turn.error'
44
+ | 'tool.failed'
45
+ // Routing / providers
46
+ | 'agent.handoff'
47
+ | 'agent.pinned'
48
+ | 'agent.unpinned'
49
+ | 'provider.selected'
50
+ // Blocking interactions
51
+ | 'interaction.requested'
52
+ | 'interaction.resolved'
53
+ // Context / cost
54
+ | 'context.updated'
55
+ | 'context.threshold-crossed'
56
+ // UI + input
57
+ | 'panel.toggled'
58
+ | 'attachment.added'
59
+ | 'file.read-failed'
60
+ | 'suggestions.failed';
61
+
62
+ /**
63
+ * How much a reader should care about an event — lets a consumer (or an AI
64
+ * agent) filter the timeline: skip `low` UI/bookkeeping noise, skim `normal`
65
+ * session flow, or jump straight to `high` problem/limit signals when triaging.
66
+ */
67
+ export type MetaEventImportance = 'high' | 'normal' | 'low';
68
+
69
+ /**
70
+ * Importance is intrinsic to the event *type*, not the instance, so it's
71
+ * defined once here and stamped automatically by {@link recordMetaEvent} — call
72
+ * sites never pass it. The `Record` is exhaustive: adding a `MetaEventType`
73
+ * without a level here is a compile error.
74
+ *
75
+ * - `high` — failures and hard limits you almost always want to see.
76
+ * - `normal` — meaningful session flow you read to follow what happened.
77
+ * - `low` — frequent UI / bookkeeping noise, usually safe to skip.
78
+ */
79
+ export const META_EVENT_IMPORTANCE: Record<MetaEventType, MetaEventImportance> = {
80
+ 'turn.error': 'high',
81
+ 'tool.failed': 'high',
82
+ 'file.read-failed': 'high',
83
+ 'suggestions.failed': 'high',
84
+ 'context.threshold-crossed': 'high',
85
+
86
+ 'assistant.connected': 'normal',
87
+ 'assistant.disconnected': 'normal',
88
+ 'assistant.popout': 'normal',
89
+ 'assistant.popin': 'normal',
90
+ 'driver.created': 'normal',
91
+ 'state.changed': 'normal',
92
+ 'turn.start': 'normal',
93
+ 'turn.end': 'normal',
94
+ 'agent.handoff': 'normal',
95
+ 'agent.pinned': 'normal',
96
+ 'agent.unpinned': 'normal',
97
+ 'provider.selected': 'normal',
98
+ 'interaction.requested': 'normal',
99
+ 'interaction.resolved': 'normal',
100
+
101
+ 'driver.wired': 'low',
102
+ 'driver.unwired': 'low',
103
+ 'context.updated': 'low',
104
+ 'panel.toggled': 'low',
105
+ 'attachment.added': 'low',
106
+ };
107
+
108
+ /** One entry in the meta-event timeline. */
109
+ export interface MetaEvent {
110
+ /** Monotonic counter across the session's lifetime (does not reset on driver rebuild). */
111
+ index: number;
112
+ /** ISO timestamp, matching the format used elsewhere in the debug log. */
113
+ timestamp: string;
114
+ /** Event name — see {@link MetaEventType}. */
115
+ type: MetaEventType;
116
+ /** How much to care about this event — see {@link META_EVENT_IMPORTANCE}. */
117
+ importance: MetaEventImportance;
118
+ /** Optional structured payload. */
119
+ detail?: Record<string, unknown>;
120
+ }
121
+
122
+ /** Default ring-buffer cap. ~5× the turn-snapshot cap — entries are cheap. */
123
+ const DEFAULT_MAX_META_EVENTS = 200;
124
+
125
+ interface MetaEventBuffer {
126
+ events: MetaEvent[];
127
+ /** Next index to assign — kept separate from `events.length` so it stays monotonic after eviction. */
128
+ next: number;
129
+ }
130
+
131
+ const registry = new Map<string, MetaEventBuffer>();
132
+
133
+ /**
134
+ * Append a meta event to the timeline for `key`. Evicts the oldest entry once
135
+ * the buffer exceeds {@link DEFAULT_MAX_META_EVENTS}. An empty `key` is bucketed
136
+ * under `''`, matching how drivers/stores handle an absent session identity, so
137
+ * callers never need to guard.
138
+ */
139
+ export function recordMetaEvent(
140
+ key: string,
141
+ type: MetaEventType,
142
+ detail?: Record<string, unknown>,
143
+ ): void {
144
+ let buffer = registry.get(key);
145
+ if (!buffer) {
146
+ buffer = { events: [], next: 0 };
147
+ registry.set(key, buffer);
148
+ }
149
+ buffer.events.push({
150
+ index: buffer.next,
151
+ timestamp: new Date().toISOString(),
152
+ type,
153
+ importance: META_EVENT_IMPORTANCE[type],
154
+ detail,
155
+ });
156
+ buffer.next += 1;
157
+ if (buffer.events.length > DEFAULT_MAX_META_EVENTS) {
158
+ buffer.events.shift();
159
+ }
160
+ }
161
+
162
+ /** Returns the meta-event timeline for `key`, or an empty array if none recorded. */
163
+ export function getMetaEvents(key: string): ReadonlyArray<MetaEvent> {
164
+ return registry.get(key)?.events ?? [];
165
+ }
166
+
167
+ /**
168
+ * Human/agent-facing guide emitted as the first key of the exported debug log,
169
+ * so whoever opens the JSON (often an AI agent) knows how to read it without
170
+ * reverse-engineering the shape. Kept here next to the event catalogue it
171
+ * describes so the two stay in sync.
172
+ */
173
+ export const DEBUG_LOG_README: readonly string[] = [
174
+ 'This is an exported debug log for the Genesis AI assistant. Read it top-to-bottom.',
175
+ '`timeline` is the entire session as one array, already sorted chronologically by `timestamp` (ISO 8601). Every entry has a `kind`.',
176
+ "kind:'message' — the conversation. `role` is user/assistant/tool/system-event; `agentName` says which agent produced it; `toolCalls`/`toolResult`/`interaction` carry tool and widget activity; `inputTokens`/`outputTokens`/`cost` are per-message usage.",
177
+ "kind:'turn' — one LLM call. `systemPrompt` and `toolNames` are what the model saw. A systemPrompt of '<repeated — identical to turn N>' was byte-identical to turn N and de-duplicated; the full prompt is shown whenever it changes (often because a stateful agent advanced), so prompt evolution is visible.",
178
+ "kind:'turn'.`agentSnapshot` — the active agent's own view of its internal state, captured at that turn. An agent opts into this by exposing a `getDebugSnapshot()` that returns JSON-serializable per-state info; stateful/flow agents wire it automatically, so you can watch a flow advance turn-by-turn (e.g. current step, cursor, collected fields, pending changes). Absent for agents that don't expose one.",
179
+ "kind:'event' — a meta/lifecycle event. `type` names it (see below); `detail` carries structured data. `detail.placement` is the emitting UI instance: 'bubble' (collapsed), 'panel' (popped-out), or 'standalone'.",
180
+ "Each 'event' also has an `importance`: 'high' (failures/limits — turn.error, tool.failed, file.read-failed, suggestions.failed, context.threshold-crossed), 'normal' (session flow — connects, turns, handoffs, agent/provider changes, interactions), or 'low' (skippable UI/bookkeeping noise — panel.toggled, attachment.added, driver.wired/unwired, context.updated). To skim, ignore importance:'low'; to triage a failure, filter to importance:'high' then read the nearby messages and turns. 'message' and 'turn' entries carry no importance — they are the substance, always read them.",
181
+ 'Event types: assistant.connected/disconnected (mount + placement + whether the session was created or restored), assistant.popout/popin (window placement), driver.created/wired/unwired (which driver is live and why it stops/starts responding across a popout), state.changed (idle↔loading), turn.start/turn.end (turn boundary; turn.end carries durationMs), turn.error (a turn failed or hit a guardrail — see detail.reason), tool.failed (a tool threw), agent.handoff (routing; from=null is the initial activation), agent.pinned/unpinned (forced routing), provider.selected (model/provider for the upcoming turns), interaction.requested/resolved (blocking user widgets — explain quiet gaps), context.updated/threshold-crossed (token + cost), panel.toggled, attachment.added, file.read-failed, suggestions.failed.',
182
+ "`meta` holds context captured at export time: agentSummary (full agent configs), context (active model, token usage, session cost), activeDebugSnapshot (the active agent's `getDebugSnapshot()` taken fresh at export — reflects state NOW, which may have advanced beyond the last turn's agentSnapshot), debug (optional host-supplied debug state), host, and the export timestamp.",
183
+ 'To debug a failure: find the last turn.error or tool.failed, then read upward for the user message, the turn(s), and the agent/provider/state events that led into it.',
184
+ ];
185
+
186
+ /**
187
+ * Removes all entries. Exposed for test isolation only — not part of the
188
+ * public API.
189
+ *
190
+ * @internal
191
+ */
192
+ export function clearMetaEventRegistry(): void {
193
+ registry.clear();
194
+ }