@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.
@@ -0,0 +1,118 @@
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
+ * Importance is intrinsic to the event *type*, not the instance, so it's
26
+ * defined once here and stamped automatically by {@link recordMetaEvent} — call
27
+ * sites never pass it. The `Record` is exhaustive: adding a `MetaEventType`
28
+ * without a level here is a compile error.
29
+ *
30
+ * - `high` — failures and hard limits you almost always want to see.
31
+ * - `normal` — meaningful session flow you read to follow what happened.
32
+ * - `low` — frequent UI / bookkeeping noise, usually safe to skip.
33
+ */
34
+ export const META_EVENT_IMPORTANCE = {
35
+ 'turn.error': 'high',
36
+ 'tool.failed': 'high',
37
+ 'file.read-failed': 'high',
38
+ 'suggestions.failed': 'high',
39
+ 'context.threshold-crossed': 'high',
40
+ 'assistant.connected': 'normal',
41
+ 'assistant.disconnected': 'normal',
42
+ 'assistant.popout': 'normal',
43
+ 'assistant.popin': 'normal',
44
+ 'driver.created': 'normal',
45
+ 'state.changed': 'normal',
46
+ 'turn.start': 'normal',
47
+ 'turn.end': 'normal',
48
+ 'agent.handoff': 'normal',
49
+ 'agent.pinned': 'normal',
50
+ 'agent.unpinned': 'normal',
51
+ 'provider.selected': 'normal',
52
+ 'interaction.requested': 'normal',
53
+ 'interaction.resolved': 'normal',
54
+ 'driver.wired': 'low',
55
+ 'driver.unwired': 'low',
56
+ 'context.updated': 'low',
57
+ 'panel.toggled': 'low',
58
+ 'attachment.added': 'low',
59
+ };
60
+ /** Default ring-buffer cap. ~5× the turn-snapshot cap — entries are cheap. */
61
+ const DEFAULT_MAX_META_EVENTS = 200;
62
+ const registry = new Map();
63
+ /**
64
+ * Append a meta event to the timeline for `key`. Evicts the oldest entry once
65
+ * the buffer exceeds {@link DEFAULT_MAX_META_EVENTS}. An empty `key` is bucketed
66
+ * under `''`, matching how drivers/stores handle an absent session identity, so
67
+ * callers never need to guard.
68
+ */
69
+ export function recordMetaEvent(key, type, detail) {
70
+ let buffer = registry.get(key);
71
+ if (!buffer) {
72
+ buffer = { events: [], next: 0 };
73
+ registry.set(key, buffer);
74
+ }
75
+ buffer.events.push({
76
+ index: buffer.next,
77
+ timestamp: new Date().toISOString(),
78
+ type,
79
+ importance: META_EVENT_IMPORTANCE[type],
80
+ detail,
81
+ });
82
+ buffer.next += 1;
83
+ if (buffer.events.length > DEFAULT_MAX_META_EVENTS) {
84
+ buffer.events.shift();
85
+ }
86
+ }
87
+ /** Returns the meta-event timeline for `key`, or an empty array if none recorded. */
88
+ export function getMetaEvents(key) {
89
+ var _a, _b;
90
+ return (_b = (_a = registry.get(key)) === null || _a === void 0 ? void 0 : _a.events) !== null && _b !== void 0 ? _b : [];
91
+ }
92
+ /**
93
+ * Human/agent-facing guide emitted as the first key of the exported debug log,
94
+ * so whoever opens the JSON (often an AI agent) knows how to read it without
95
+ * reverse-engineering the shape. Kept here next to the event catalogue it
96
+ * describes so the two stay in sync.
97
+ */
98
+ export const DEBUG_LOG_README = [
99
+ 'This is an exported debug log for the Genesis AI assistant. Read it top-to-bottom.',
100
+ '`timeline` is the entire session as one array, already sorted chronologically by `timestamp` (ISO 8601). Every entry has a `kind`.',
101
+ "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.",
102
+ "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.",
103
+ "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.",
104
+ "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'.",
105
+ "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.",
106
+ '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.',
107
+ "`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.",
108
+ '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.',
109
+ ];
110
+ /**
111
+ * Removes all entries. Exposed for test isolation only — not part of the
112
+ * public API.
113
+ *
114
+ * @internal
115
+ */
116
+ export function clearMetaEventRegistry() {
117
+ registry.clear();
118
+ }
@@ -1 +1 @@
1
- {"root":["../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/halo-overlay.ts","../src/components/activity-halo/activity-halo.ts","../src/components/agent-picker/agent-picker.constants.ts","../src/components/agent-picker/agent-picker.styles.ts","../src/components/agent-picker/agent-picker.template.ts","../src/components/agent-picker/agent-picker.ts","../src/components/agent-picker/index.ts","../src/components/ai-driver/ai-driver.ts","../src/components/ai-driver/index.ts","../src/components/chat-bubble/chat-bubble.styles.ts","../src/components/chat-bubble/chat-bubble.template.ts","../src/components/chat-bubble/chat-bubble.ts","../src/components/chat-bubble/index.ts","../src/components/chat-driver/chat-driver.ts","../src/components/chat-driver/index.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts","../src/components/chat-interaction-wrapper/index.ts","../src/components/chat-markdown/chat-markdown.ts","../src/components/chat-markdown/index.ts","../src/components/orchestrating-driver/index.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/config/config.ts","../src/config/define-stateful-agent.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/config/validate-providers.test.ts","../src/config/validate-providers.ts","../src/main/index.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/state/ai-assistant-slice.ts","../src/state/driver-registry.ts","../src/state/session-store.ts","../src/styles/ai-colours.ts","../src/styles/index.ts","../src/styles/styles.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/utils/animated-panel-toggle.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/sum-costs.test.ts","../src/utils/sum-costs.ts","../src/utils/tool-fold.ts"],"version":"5.9.2"}
1
+ {"root":["../src/index.ts","../src/channel/ai-activity-bus.ts","../src/channel/ai-activity-channel.ts","../src/components/halo-overlay.ts","../src/components/activity-halo/activity-halo.ts","../src/components/agent-picker/agent-picker.constants.ts","../src/components/agent-picker/agent-picker.styles.ts","../src/components/agent-picker/agent-picker.template.ts","../src/components/agent-picker/agent-picker.ts","../src/components/agent-picker/index.ts","../src/components/ai-driver/ai-driver.ts","../src/components/ai-driver/index.ts","../src/components/chat-bubble/chat-bubble.styles.ts","../src/components/chat-bubble/chat-bubble.template.ts","../src/components/chat-bubble/chat-bubble.ts","../src/components/chat-bubble/index.ts","../src/components/chat-driver/chat-driver.ts","../src/components/chat-driver/index.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts","../src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts","../src/components/chat-interaction-wrapper/index.ts","../src/components/chat-markdown/chat-markdown.ts","../src/components/chat-markdown/index.ts","../src/components/orchestrating-driver/index.ts","../src/components/orchestrating-driver/orchestrating-driver.ts","../src/components/popout-manager/index.ts","../src/components/popout-manager/popout-manager.ts","../src/config/config.ts","../src/config/define-stateful-agent.ts","../src/config/fallback-agents.ts","../src/config/index.ts","../src/config/validate-providers.test.ts","../src/config/validate-providers.ts","../src/main/index.ts","../src/main/main.styles.ts","../src/main/main.template.ts","../src/main/main.ts","../src/main/main.types.ts","../src/state/ai-assistant-slice.ts","../src/state/debug-event-log.ts","../src/state/driver-registry.ts","../src/state/session-store.ts","../src/styles/ai-colours.ts","../src/styles/index.ts","../src/styles/styles.ts","../src/suggestions/chat-suggestions.ts","../src/tags/index.ts","../src/types/ai-chat-widget.ts","../src/utils/animated-panel-toggle.ts","../src/utils/history-transform.ts","../src/utils/index.ts","../src/utils/logger.ts","../src/utils/sum-costs.test.ts","../src/utils/sum-costs.ts","../src/utils/tool-fold.ts"],"version":"5.9.2"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/ai-assistant",
3
3
  "description": "Genesis AI Assistant micro-frontend",
4
- "version": "14.450.0",
4
+ "version": "14.451.0",
5
5
  "license": "SEE LICENSE IN license.txt",
6
6
  "main": "dist/esm/index.js",
7
7
  "types": "dist/ai-assistant.d.ts",
@@ -64,24 +64,24 @@
64
64
  }
65
65
  },
66
66
  "devDependencies": {
67
- "@genesislcap/foundation-testing": "14.450.0",
68
- "@genesislcap/genx": "14.450.0",
69
- "@genesislcap/rollup-builder": "14.450.0",
70
- "@genesislcap/ts-builder": "14.450.0",
71
- "@genesislcap/uvu-playwright-builder": "14.450.0",
72
- "@genesislcap/vite-builder": "14.450.0",
73
- "@genesislcap/webpack-builder": "14.450.0",
67
+ "@genesislcap/foundation-testing": "14.451.0",
68
+ "@genesislcap/genx": "14.451.0",
69
+ "@genesislcap/rollup-builder": "14.451.0",
70
+ "@genesislcap/ts-builder": "14.451.0",
71
+ "@genesislcap/uvu-playwright-builder": "14.451.0",
72
+ "@genesislcap/vite-builder": "14.451.0",
73
+ "@genesislcap/webpack-builder": "14.451.0",
74
74
  "@types/dompurify": "^3.0.5",
75
75
  "@types/marked": "^5.0.2"
76
76
  },
77
77
  "dependencies": {
78
- "@genesislcap/foundation-ai": "14.450.0",
79
- "@genesislcap/foundation-logger": "14.450.0",
80
- "@genesislcap/foundation-redux": "14.450.0",
81
- "@genesislcap/foundation-ui": "14.450.0",
82
- "@genesislcap/foundation-utils": "14.450.0",
83
- "@genesislcap/rapid-design-system": "14.450.0",
84
- "@genesislcap/web-core": "14.450.0",
78
+ "@genesislcap/foundation-ai": "14.451.0",
79
+ "@genesislcap/foundation-logger": "14.451.0",
80
+ "@genesislcap/foundation-redux": "14.451.0",
81
+ "@genesislcap/foundation-ui": "14.451.0",
82
+ "@genesislcap/foundation-utils": "14.451.0",
83
+ "@genesislcap/rapid-design-system": "14.451.0",
84
+ "@genesislcap/web-core": "14.451.0",
85
85
  "dompurify": "^3.3.1",
86
86
  "marked": "^17.0.3"
87
87
  },
@@ -93,5 +93,5 @@
93
93
  "publishConfig": {
94
94
  "access": "public"
95
95
  },
96
- "gitHead": "b9824253dfa64fe2dfe72cad8268b7caacebf0b0"
96
+ "gitHead": "0e6d8695edfcba1901f3f42f12e9387bee747c60"
97
97
  }
@@ -22,6 +22,7 @@ import type {
22
22
  ToolHandlersInput,
23
23
  } from '../../config/config';
24
24
  import { resolveChatProvider } from '../../config/validate-providers';
25
+ import { recordMetaEvent } from '../../state/debug-event-log';
25
26
  import { applyHistoryCap } from '../../utils/history-transform';
26
27
  import { logger } from '../../utils/logger';
27
28
  import { TOOL_FOLD_SYMBOL, type ToolFold } from '../../utils/tool-fold';
@@ -99,6 +100,8 @@ interface FoldStackFrame {
99
100
  export class ChatDriver extends EventTarget implements AiDriver {
100
101
  private history: ChatMessage[] = [];
101
102
  private busy = false;
103
+ /** Epoch ms when the current turn loop began — drives the `turn.end` duration. */
104
+ private turnStartedAt = 0;
102
105
  private pendingInteractions = new Map<
103
106
  string,
104
107
  {
@@ -233,6 +236,8 @@ export class ChatDriver extends EventTarget implements AiDriver {
233
236
  private readonly maxToolIterations: number = DEFAULT_MAX_TOOL_ITERATIONS,
234
237
  maxFoldOperations: number = DEFAULT_MAX_FOLD_OPERATIONS,
235
238
  maxTurnSnapshots: number = DEFAULT_MAX_TURN_SNAPSHOTS,
239
+ /** Session identity used to file meta events onto the shared debug-log timeline. */
240
+ private readonly sessionKey: string = '',
236
241
  ) {
237
242
  super();
238
243
  if (typeof toolHandlers === 'function') {
@@ -348,6 +353,10 @@ export class ChatDriver extends EventTarget implements AiDriver {
348
353
  this.lastResolvedProviderName = resolvedName;
349
354
  if (resolvedName !== this.lastDispatchedProviderName) {
350
355
  this.lastDispatchedProviderName = resolvedName;
356
+ recordMetaEvent(this.sessionKey, 'provider.selected', {
357
+ provider: resolvedName,
358
+ agent: this.activeAgentName,
359
+ });
351
360
  this.dispatchEvent(
352
361
  new CustomEvent<{ name: string }>('provider-changed', { detail: { name: resolvedName } }),
353
362
  );
@@ -610,6 +619,11 @@ export class ChatDriver extends EventTarget implements AiDriver {
610
619
  reject,
611
620
  overrideId: chatInputDuringExecution ? interactionId : undefined,
612
621
  });
622
+ recordMetaEvent(this.sessionKey, 'interaction.requested', {
623
+ interactionId,
624
+ component: componentName,
625
+ agent: this.activeAgentName,
626
+ });
613
627
  if (chatInputDuringExecution) {
614
628
  this.dispatchEvent(
615
629
  new CustomEvent('interaction-start', {
@@ -647,6 +661,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
647
661
  if (interaction.overrideId) {
648
662
  this.dispatchEvent(new CustomEvent('interaction-stop', { detail: { interactionId } }));
649
663
  }
664
+ recordMetaEvent(this.sessionKey, 'interaction.resolved', { interactionId });
650
665
  interaction.resolve(result);
651
666
  this.pendingInteractions.delete(interactionId);
652
667
  } else {
@@ -674,15 +689,31 @@ export class ChatDriver extends EventTarget implements AiDriver {
674
689
  this.subAgentCompletion = undefined;
675
690
  this.agentReleaseRequested = false;
676
691
  this.appendToHistory({ role: 'user', content: userInput, attachments });
692
+ this.turnStartedAt = Date.now();
693
+ recordMetaEvent(this.sessionKey, 'turn.start', {
694
+ phase: 'sendMessage',
695
+ agent: this.activeAgentName,
696
+ });
677
697
  agenticActivityBus.publish('tool-loop-start', undefined);
678
698
 
679
699
  try {
680
700
  return await this.runToolLoop(userInput, attachments);
681
701
  } catch (e) {
682
702
  logger.error('ChatDriver error:', e);
703
+ recordMetaEvent(this.sessionKey, 'turn.error', {
704
+ phase: 'sendMessage',
705
+ agent: this.activeAgentName,
706
+ provider: this.lastResolvedProviderName,
707
+ message: e instanceof Error ? e.message : String(e),
708
+ });
683
709
  this.appendToHistory({ role: 'assistant', content: 'Sorry, something went wrong.' });
684
710
  return { reason: 'done' };
685
711
  } finally {
712
+ recordMetaEvent(this.sessionKey, 'turn.end', {
713
+ phase: 'sendMessage',
714
+ agent: this.activeAgentName,
715
+ durationMs: Date.now() - this.turnStartedAt,
716
+ });
686
717
  this.busy = false;
687
718
  agenticActivityBus.publish('tool-loop-end', undefined);
688
719
  }
@@ -844,14 +875,30 @@ export class ChatDriver extends EventTarget implements AiDriver {
844
875
 
845
876
  this.busy = true;
846
877
  this.subAgentCompletion = undefined;
878
+ this.turnStartedAt = Date.now();
879
+ recordMetaEvent(this.sessionKey, 'turn.start', {
880
+ phase: 'continueFromHistory',
881
+ agent: this.activeAgentName,
882
+ });
847
883
  agenticActivityBus.publish('tool-loop-start', undefined);
848
884
  try {
849
885
  return await this.runToolLoop('', undefined, transientPrimer);
850
886
  } catch (e) {
851
887
  logger.error('ChatDriver error:', e);
888
+ recordMetaEvent(this.sessionKey, 'turn.error', {
889
+ phase: 'continueFromHistory',
890
+ agent: this.activeAgentName,
891
+ provider: this.lastResolvedProviderName,
892
+ message: e instanceof Error ? e.message : String(e),
893
+ });
852
894
  this.appendToHistory({ role: 'assistant', content: 'Sorry, something went wrong.' });
853
895
  return { reason: 'done' };
854
896
  } finally {
897
+ recordMetaEvent(this.sessionKey, 'turn.end', {
898
+ phase: 'continueFromHistory',
899
+ agent: this.activeAgentName,
900
+ durationMs: Date.now() - this.turnStartedAt,
901
+ });
855
902
  this.busy = false;
856
903
  agenticActivityBus.publish('tool-loop-end', undefined);
857
904
  }
@@ -1140,6 +1187,11 @@ export class ChatDriver extends EventTarget implements AiDriver {
1140
1187
  continue;
1141
1188
  }
1142
1189
  logger.error('ChatDriver: MALFORMED_FUNCTION_CALL, max retries reached');
1190
+ recordMetaEvent(this.sessionKey, 'turn.error', {
1191
+ reason: 'malformed-function-call',
1192
+ agent: this.activeAgentName,
1193
+ provider: this.lastResolvedProviderName,
1194
+ });
1143
1195
  this.appendToHistory({
1144
1196
  role: 'assistant',
1145
1197
  content:
@@ -1163,6 +1215,11 @@ export class ChatDriver extends EventTarget implements AiDriver {
1163
1215
  continue;
1164
1216
  }
1165
1217
  logger.error('ChatDriver: empty model response after all retries');
1218
+ recordMetaEvent(this.sessionKey, 'turn.error', {
1219
+ reason: 'empty-response',
1220
+ agent: this.activeAgentName,
1221
+ provider: this.lastResolvedProviderName,
1222
+ });
1166
1223
  this.appendToHistory({
1167
1224
  role: 'assistant',
1168
1225
  content: 'Remote agent returned no response.',
@@ -1281,6 +1338,11 @@ export class ChatDriver extends EventTarget implements AiDriver {
1281
1338
  anyRealToolExecuted = true;
1282
1339
  } catch (e) {
1283
1340
  logger.error(`ChatDriver tool "${tc.name}" failed:`, e);
1341
+ recordMetaEvent(this.sessionKey, 'tool.failed', {
1342
+ tool: tc.name,
1343
+ agent: this.activeAgentName,
1344
+ message: e instanceof Error ? e.message : String(e),
1345
+ });
1284
1346
  executedById.set(tc.id, {
1285
1347
  toolCallId: tc.id,
1286
1348
  content: `Tool error: ${(e as Error).message}`,
@@ -1361,6 +1423,11 @@ export class ChatDriver extends EventTarget implements AiDriver {
1361
1423
  logger.error(
1362
1424
  `ChatDriver: unknown-tool limit (${DEFAULT_MAX_UNKNOWN_TOOL_CALLS}) reached — stopping`,
1363
1425
  );
1426
+ recordMetaEvent(this.sessionKey, 'turn.error', {
1427
+ reason: 'unknown-tool-limit',
1428
+ agent: this.activeAgentName,
1429
+ provider: this.lastResolvedProviderName,
1430
+ });
1364
1431
  this.appendToHistory({
1365
1432
  role: 'assistant',
1366
1433
  content:
@@ -1387,6 +1454,11 @@ export class ChatDriver extends EventTarget implements AiDriver {
1387
1454
 
1388
1455
  if (iterations >= this.maxToolIterations) {
1389
1456
  logger.warn('ChatDriver: reached max tool iterations, stopping');
1457
+ recordMetaEvent(this.sessionKey, 'turn.error', {
1458
+ reason: 'max-iterations',
1459
+ agent: this.activeAgentName,
1460
+ provider: this.lastResolvedProviderName,
1461
+ });
1390
1462
  this.appendToHistory({
1391
1463
  role: 'assistant',
1392
1464
  content:
@@ -1398,14 +1470,18 @@ export class ChatDriver extends EventTarget implements AiDriver {
1398
1470
  }
1399
1471
 
1400
1472
  private appendToHistory(message: ChatMessage): void {
1401
- const tagged: ChatMessage = this.activeAgentName
1402
- ? {
1403
- ...message,
1404
- agentName: this.activeAgentName,
1405
- // Display-only — falls back to agentName in renderers when unset.
1406
- agentLabel: this.activeAgentLabel,
1407
- }
1408
- : message;
1473
+ const tagged: ChatMessage = {
1474
+ ...message,
1475
+ // Stamp on first append; preserve any caller-supplied timestamp.
1476
+ timestamp: message.timestamp ?? new Date().toISOString(),
1477
+ ...(this.activeAgentName
1478
+ ? {
1479
+ agentName: this.activeAgentName,
1480
+ // Display-only — falls back to agentName in renderers when unset.
1481
+ agentLabel: this.activeAgentLabel,
1482
+ }
1483
+ : {}),
1484
+ };
1409
1485
  this.history = [...this.history, tagged];
1410
1486
  this.dispatchEvent(
1411
1487
  new CustomEvent<ReadonlyArray<ChatMessage>>('history-updated', {
@@ -13,6 +13,7 @@ import type {
13
13
  SystemPromptInput,
14
14
  } from '../../config/config';
15
15
  import { validateStaticAgentProviders } from '../../config/validate-providers';
16
+ import { recordMetaEvent } from '../../state/debug-event-log';
16
17
  import { transformHistoryForAgent } from '../../utils/history-transform';
17
18
  import { logger } from '../../utils/logger';
18
19
  import type { AiDriver, AllAgentSummary } from '../ai-driver/ai-driver';
@@ -159,6 +160,7 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
159
160
  options.maxToolIterations,
160
161
  options.maxFoldOperations,
161
162
  options.maxTurnSnapshots,
163
+ this.sessionKey,
162
164
  );
163
165
 
164
166
  // Proxy events from the shared driver
@@ -340,6 +342,15 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
340
342
  const previousAgent = this.activeAgent;
341
343
  const isSwitch = !previousAgent || previousAgent.name !== agent.name;
342
344
 
345
+ // Record the agent transition on the debug-log timeline. A `null` `from`
346
+ // marks the initial activation; a named `from` is a handoff between agents.
347
+ if (isSwitch) {
348
+ recordMetaEvent(this.sessionKey, 'agent.handoff', {
349
+ from: previousAgent?.name ?? null,
350
+ to: agent.name,
351
+ });
352
+ }
353
+
343
354
  // Fire lifecycle hooks around the swap — outgoing first, then incoming.
344
355
  // Both are awaited so a heavy `onActivate` (e.g. machine restore) completes
345
356
  // before the agent's first turn runs.
@@ -413,7 +424,10 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
413
424
 
414
425
  if (previousAgent && previousAgent.name !== agent.name) {
415
426
  const rawHistory = this.chatDriver.getHistory() as ChatMessage[];
416
- this.chatDriver.loadHistory([...rawHistory, { role: 'system-event', content: agent.name }]);
427
+ this.chatDriver.loadHistory([
428
+ ...rawHistory,
429
+ { role: 'system-event', content: agent.name, timestamp: new Date().toISOString() },
430
+ ]);
417
431
  }
418
432
 
419
433
  this.chatDriver.setProviderHistoryTransform((h) => transformHistoryForAgent(h, agent.name));
@@ -606,7 +620,10 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
606
620
 
607
621
  private appendInlineMessage(content: string): void {
608
622
  const history = this.chatDriver.getHistory() as ChatMessage[];
609
- this.chatDriver.loadHistory([...history, { role: 'assistant', content }]);
623
+ this.chatDriver.loadHistory([
624
+ ...history,
625
+ { role: 'assistant', content, timestamp: new Date().toISOString() },
626
+ ]);
610
627
  this.dispatchEvent(
611
628
  new CustomEvent('history-updated', { detail: this.chatDriver.getHistory() }),
612
629
  );