@genesislcap/ai-assistant 15.6.2 → 15.7.0-ts7.1

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 (100) hide show
  1. package/dist/ai-assistant.api.json +392 -6
  2. package/dist/ai-assistant.d.ts +616 -24
  3. package/dist/chat-driver.cjs +315 -46
  4. package/dist/chat-driver.cjs.map +3 -3
  5. package/dist/chat-driver.mjs +315 -46
  6. package/dist/chat-driver.mjs.map +3 -3
  7. package/dist/custom-elements.json +254 -10
  8. package/dist/dts/channel/ai-activity-bus.d.ts.map +1 -1
  9. package/dist/dts/channel/ai-activity-channel.d.ts +51 -1
  10. package/dist/dts/channel/ai-activity-channel.d.ts.map +1 -1
  11. package/dist/dts/components/activity-halo/activity-halo.d.ts.map +1 -1
  12. package/dist/dts/components/agent-picker/agent-picker.d.ts.map +1 -1
  13. package/dist/dts/components/agent-picker/agent-picker.template.d.ts.map +1 -1
  14. package/dist/dts/components/chat-bubble/chat-bubble.d.ts.map +1 -1
  15. package/dist/dts/components/chat-driver/chat-driver.d.ts +99 -1
  16. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  17. package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +1 -1
  18. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts.map +1 -1
  19. package/dist/dts/components/chat-markdown/chat-markdown.d.ts.map +1 -1
  20. package/dist/dts/components/flowing-waves-indicator.d.ts.map +1 -1
  21. package/dist/dts/components/halo-overlay.d.ts.map +1 -1
  22. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +2 -0
  23. package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +1 -0
  24. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +14 -0
  25. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  26. package/dist/dts/components/popout-manager/popout-manager.d.ts.map +1 -1
  27. package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
  28. package/dist/dts/components/waves-indicator.d.ts.map +1 -1
  29. package/dist/dts/main/blocked-state.test.d.ts +2 -0
  30. package/dist/dts/main/blocked-state.test.d.ts.map +1 -0
  31. package/dist/dts/main/main.d.ts +394 -6
  32. package/dist/dts/main/main.d.ts.map +1 -1
  33. package/dist/dts/main/main.styles.d.ts.map +1 -1
  34. package/dist/dts/main/main.styles.test.d.ts +2 -0
  35. package/dist/dts/main/main.styles.test.d.ts.map +1 -0
  36. package/dist/dts/main/main.template.d.ts +53 -0
  37. package/dist/dts/main/main.template.d.ts.map +1 -1
  38. package/dist/dts/main/main.types.d.ts +2 -2
  39. package/dist/dts/main/main.types.d.ts.map +1 -1
  40. package/dist/dts/state/ai-assistant-slice.d.ts +163 -7
  41. package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
  42. package/dist/dts/state/debug-event-log.d.ts +6 -1
  43. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  44. package/dist/dts/state/persistence/session-persistence-provider.d.ts.map +1 -1
  45. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  46. package/dist/dts/state/session-store.d.ts +12 -1
  47. package/dist/dts/state/session-store.d.ts.map +1 -1
  48. package/dist/dts/suggestions/chat-suggestions.d.ts.map +1 -1
  49. package/dist/dts/utils/animated-panel-toggle.d.ts.map +1 -1
  50. package/dist/dts/utils/message-partition.d.ts.map +1 -1
  51. package/dist/esm/components/agent-picker/agent-picker.js +2 -1
  52. package/dist/esm/components/agent-picker/agent-picker.template.js +2 -2
  53. package/dist/esm/components/chat-bubble/chat-bubble.js +2 -1
  54. package/dist/esm/components/chat-driver/align-event-globals.js +1 -0
  55. package/dist/esm/components/chat-driver/chat-driver.js +281 -38
  56. package/dist/esm/components/chat-driver/chat-driver.test.js +471 -14
  57. package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +312 -0
  58. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +94 -9
  59. package/dist/esm/components/popout-manager/popout-manager.js +4 -1
  60. package/dist/esm/components/settings-modal/settings-modal.template.js +5 -8
  61. package/dist/esm/main/blocked-state.test.js +969 -0
  62. package/dist/esm/main/main.js +834 -104
  63. package/dist/esm/main/main.styles.js +47 -0
  64. package/dist/esm/main/main.styles.test.js +87 -0
  65. package/dist/esm/main/main.template.js +131 -20
  66. package/dist/esm/state/ai-assistant-slice.js +145 -7
  67. package/dist/esm/state/ai-assistant-slice.test.js +138 -1
  68. package/dist/esm/state/debug-event-log.js +9 -3
  69. package/dist/esm/state/debug-event-log.test.js +49 -1
  70. package/dist/esm/state/persistence/session-persister.js +13 -10
  71. package/dist/esm/state/persistence/session-snapshot.js +3 -2
  72. package/dist/esm/state/persistence/session-snapshot.test.js +18 -0
  73. package/dist/esm/utils/collect-session-models.test.js +2 -1
  74. package/dist/esm/utils/condense-history.js +6 -4
  75. package/dist/esm/utils/condense-history.test.js +15 -21
  76. package/dist/esm/utils/flatten-sub-agent-messages.js +4 -3
  77. package/dist/esm/utils/history-transform.js +2 -1
  78. package/dist/esm/utils/resolve-cost-history-config.js +4 -3
  79. package/dist/esm/utils/resolve-preference-baseline.js +2 -1
  80. package/dist/esm/utils/sum-usage.js +7 -6
  81. package/dist/tsconfig.tsbuildinfo +1 -1
  82. package/docs/migration-GENC-1464.md +562 -0
  83. package/docs/sub_agent.md +20 -3
  84. package/package.json +17 -17
  85. package/src/channel/ai-activity-channel.ts +56 -2
  86. package/src/components/chat-driver/chat-driver.test.ts +549 -0
  87. package/src/components/chat-driver/chat-driver.ts +324 -14
  88. package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +438 -0
  89. package/src/components/orchestrating-driver/orchestrating-driver.ts +101 -6
  90. package/src/main/blocked-state.test.ts +1316 -0
  91. package/src/main/main.styles.test.ts +103 -0
  92. package/src/main/main.styles.ts +47 -0
  93. package/src/main/main.template.ts +131 -4
  94. package/src/main/main.ts +704 -10
  95. package/src/state/ai-assistant-slice.test.ts +215 -0
  96. package/src/state/ai-assistant-slice.ts +218 -8
  97. package/src/state/debug-event-log.test.ts +63 -0
  98. package/src/state/debug-event-log.ts +7 -2
  99. package/src/state/persistence/session-snapshot.test.ts +22 -0
  100. package/tsconfig.json +1 -0
@@ -1,7 +1,7 @@
1
1
  import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
2
  import { aiAssistantSlice, createDefaultSessionState, } from './ai-assistant-slice';
3
3
  // GENC-1351 §5.6 / Phase 1: the new bulk loadSession + resetSession reducers.
4
- const { loadSession, resetSession, setMessages, setState, setSessionMenuOpen, setShowToolCalls, setShowThinkingSteps, setShowNarration, setShowAgentSwitchIndicator, setEnabledAnimations, } = aiAssistantSlice.actions;
4
+ const { loadSession, resetSession, setBlocked, setBlockedReason, setVendorBlocked, setInputValue, setMessages, setState, setSessionMenuOpen, setShowToolCalls, setShowThinkingSteps, setShowNarration, setShowAgentSwitchIndicator, setEnabledAnimations, } = aiAssistantSlice.actions;
5
5
  const reduce = aiAssistantSlice.reducer;
6
6
  const Suite = createLogicSuite('ai-assistant-slice-persistence');
7
7
  Suite('loadSession hydrates the restorable fields and forces idle', () => {
@@ -89,4 +89,141 @@ Suite('setSessionMenuOpen toggles the lifecycle-menu flag; resetSession clears i
89
89
  const cleared = reduce(state, resetSession());
90
90
  assert.is(cleared.sessionMenuOpen, false);
91
91
  });
92
+ // GENC-1464: the backend-blocked latch (exhausted AI budget).
93
+ Suite('setBlocked latches the flag and its reason', () => {
94
+ let state = createDefaultSessionState();
95
+ assert.is(state.blocked, false);
96
+ assert.is(state.blockedReason, null);
97
+ state = reduce(state, setBlocked({ blocked: true }));
98
+ assert.is(state.blocked, true);
99
+ assert.is(state.blockedReason, null, 'no reason given → null, so the element uses its default');
100
+ state = reduce(state, setBlocked({ blocked: true, reason: 'Budget exhausted — contact billing' }));
101
+ assert.is(state.blockedReason, 'Budget exhausted — contact billing');
102
+ });
103
+ Suite('unblocking always clears the reason', () => {
104
+ // A stale explanation must never outlive the condition it described.
105
+ let state = reduce(createDefaultSessionState(), setBlocked({ blocked: true, reason: 'gone' }));
106
+ state = reduce(state, setBlocked({ blocked: false }));
107
+ assert.is(state.blocked, false);
108
+ assert.is(state.blockedReason, null);
109
+ });
110
+ Suite('resetSession preserves the block — "New chat" does not refill the budget', () => {
111
+ // Deliberately unlike sessionMenuOpen/compacting above: this is a backend
112
+ // condition, not conversation state, so Clear must not hand the user a
113
+ // working-looking composer that fails on the next send.
114
+ let state = reduce(createDefaultSessionState(), setBlocked({ blocked: true, reason: 'no funds' }));
115
+ state = reduce(state, setMessages([{ role: 'user', content: 'x' }]));
116
+ const cleared = reduce(state, resetSession());
117
+ assert.is(cleared.messages.length, 0, 'the transcript is still cleared');
118
+ assert.is(cleared.blocked, true, 'the block survives Clear');
119
+ assert.is(cleared.blockedReason, 'no funds', 'and so does its reason');
120
+ });
121
+ Suite('re-latching without a reason preserves the host explanation', () => {
122
+ // The race this fixes is deterministic, not theoretical: `tool-loop-end` is
123
+ // published from the driver's `finally`, i.e. BEFORE `sendMessage()` resolves.
124
+ // So a bus-subscribed host (what the migration guide's Option C recommends)
125
+ // writes its detailed copy first, and the element's own latch — which may have
126
+ // no figures to offer — lands a moment later. Treating an OMITTED reason as
127
+ // "clear it" replaced the host's copy with generic copy every single time.
128
+ let state = reduce(createDefaultSessionState(), setBlocked({ blocked: true, reason: 'contact ops@acme.com' }));
129
+ state = reduce(state, setBlocked({ blocked: true }));
130
+ assert.is(state.blocked, true);
131
+ assert.is(state.blockedReason, 'contact ops@acme.com', 'the host copy survives the re-latch');
132
+ });
133
+ Suite('reason:null clears the explanation while staying blocked', () => {
134
+ // The explicit escape hatch the omitted case gives up: `null` still clears.
135
+ let state = reduce(createDefaultSessionState(), setBlocked({ blocked: true, reason: 'stale copy' }));
136
+ state = reduce(state, setBlocked({ blocked: true, reason: null }));
137
+ assert.is(state.blocked, true);
138
+ assert.is(state.blockedReason, null, 'the banner falls back to the default copy');
139
+ });
140
+ Suite('resetSession still resets everything NOT on the preserved list', () => {
141
+ // Pins the boundary from the other side, so the preserved list cannot quietly
142
+ // grow: these are conversation/UI state and Clear must wipe them.
143
+ let state = reduce(createDefaultSessionState(), setBlocked({ blocked: true, reason: 'r' }));
144
+ state = reduce(state, setSessionMenuOpen(true));
145
+ state = reduce(state, setInputValue('half-typed message'));
146
+ const cleared = reduce(state, resetSession());
147
+ assert.is(cleared.inputValue, '', 'the composer draft is reset');
148
+ assert.is(cleared.sessionMenuOpen, false, 'the panel flag is reset');
149
+ assert.is(cleared.blocked, true, '…while the block is still preserved');
150
+ });
151
+ // ── Per-vendor latches (GENC-1464 per-vendor budgets) ──────────────────────────
152
+ Suite('setVendorBlocked walls one vendor and keeps the wall order', () => {
153
+ let state = reduce(createDefaultSessionState(), setVendorBlocked({ vendor: 'anthropic', blocked: true, reason: 'anthropic copy' }));
154
+ assert.equal(state.blockedVendors, ['anthropic']);
155
+ assert.equal(state.blockedVendorReasons, { anthropic: 'anthropic copy' });
156
+ assert.is(state.blocked, false, 'the vendor-agnostic flag is untouched');
157
+ state = reduce(state, setVendorBlocked({ vendor: 'gemini', blocked: true }));
158
+ assert.equal(state.blockedVendors, ['anthropic', 'gemini'], 'order is the order they were walled');
159
+ });
160
+ Suite('walling an already-walled vendor is idempotent', () => {
161
+ let state = reduce(createDefaultSessionState(), setVendorBlocked({ vendor: 'gemini', blocked: true, reason: 'first' }));
162
+ state = reduce(state, setVendorBlocked({ vendor: 'gemini', blocked: true, reason: 'second' }));
163
+ assert.equal(state.blockedVendors, ['gemini'], 'no duplicate entry');
164
+ assert.is(state.blockedVendorReasons.gemini, 'second');
165
+ });
166
+ Suite('the per-vendor reason honours the same three cases as setBlocked', () => {
167
+ // The same two-writer race (driver latch landing after a bus-subscribed host)
168
+ // applies per vendor, so "omitted keeps" has to hold here too.
169
+ let state = reduce(createDefaultSessionState(), setVendorBlocked({ vendor: 'anthropic', blocked: true, reason: 'host copy' }));
170
+ state = reduce(state, setVendorBlocked({ vendor: 'anthropic', blocked: true }));
171
+ assert.is(state.blockedVendorReasons.anthropic, 'host copy', 'omitted keeps');
172
+ state = reduce(state, setVendorBlocked({ vendor: 'anthropic', blocked: true, reason: null }));
173
+ assert.is(state.blockedVendorReasons.anthropic, undefined, 'null clears');
174
+ });
175
+ Suite('releasing a vendor drops it and its copy', () => {
176
+ let state = reduce(createDefaultSessionState(), setVendorBlocked({ vendor: 'anthropic', blocked: true, reason: 'r' }));
177
+ state = reduce(state, setVendorBlocked({ vendor: 'gemini', blocked: true }));
178
+ state = reduce(state, setVendorBlocked({ vendor: 'anthropic', blocked: false }));
179
+ assert.equal(state.blockedVendors, ['gemini']);
180
+ assert.is(state.blockedVendorReasons.anthropic, undefined);
181
+ });
182
+ Suite('setBlocked(false) also clears every per-vendor latch', () => {
183
+ // THE back-compat rule. A host asserting "the wall is gone" after its own
184
+ // pre-flight (migration-GENC-1464 Option B) must not be overruled by a driver
185
+ // latch it cannot see — without this, per-vendor would silently make the
186
+ // documented unblock stop working.
187
+ let state = reduce(createDefaultSessionState(), setVendorBlocked({ vendor: 'anthropic', blocked: true, reason: 'r' }));
188
+ state = reduce(state, setBlocked({ blocked: true, reason: 'global' }));
189
+ state = reduce(state, setBlocked({ blocked: false }));
190
+ assert.is(state.blocked, false);
191
+ assert.is(state.blockedReason, null);
192
+ assert.equal(state.blockedVendors, []);
193
+ assert.equal(state.blockedVendorReasons, {});
194
+ });
195
+ Suite('setBlockedReason writes the copy without disturbing any latch', () => {
196
+ // It exists precisely so the element's `blockedReason` setter cannot route
197
+ // through `setBlocked(false, …)` — which would now clear the vendor latches.
198
+ let state = reduce(createDefaultSessionState(), setVendorBlocked({ vendor: 'gemini', blocked: true }));
199
+ state = reduce(state, setBlockedReason('host copy'));
200
+ assert.is(state.blockedReason, 'host copy');
201
+ assert.is(state.blocked, false, 'the vendor-agnostic flag is untouched');
202
+ assert.equal(state.blockedVendors, ['gemini'], 'and so are the per-vendor latches');
203
+ });
204
+ Suite('resetSession preserves the per-vendor latches too', () => {
205
+ let state = reduce(createDefaultSessionState(), setVendorBlocked({ vendor: 'anthropic', blocked: true, reason: 'no funds' }));
206
+ state = reduce(state, setMessages([{ role: 'user', content: 'hi' }]));
207
+ const cleared = reduce(state, resetSession());
208
+ assert.equal(cleared.messages, [], 'the conversation is wiped');
209
+ assert.equal(cleared.blockedVendors, ['anthropic'], 'Clear does not refill a vendor budget');
210
+ assert.equal(cleared.blockedVendorReasons, { anthropic: 'no funds' });
211
+ });
212
+ // `resetSession` preserves its keys through `Object.assign`, whose source parameter
213
+ // is structurally permissive: a typo like `blockedResaon` would compile clean,
214
+ // silently stop preserving the real key, AND leak a junk key into serialised state.
215
+ // The reducer therefore annotates its literal `satisfies
216
+ // Partial<AiAssistantSessionState>`, which is a COMPILE-time guarantee — so the
217
+ // durable proof is a type fixture, not a runtime assertion.
218
+ //
219
+ // This mirrors the reducer's clause on the same typo. If `satisfies` ever stopped
220
+ // rejecting excess properties (a TS behaviour change, or someone widening the
221
+ // annotated type), the `@ts-expect-error` below becomes unused and the BUILD fails
222
+ // — which is exactly the signal wanted. The runtime suites above are its
223
+ // behavioural companions: they pin the preserved list from both sides.
224
+ const _typoIsRejectedAtCompileTime = {
225
+ // @ts-expect-error — an excess/misspelled key must be rejected by `satisfies`
226
+ blockedResaon: null,
227
+ };
228
+ void _typoIsRejectedAtCompileTime;
92
229
  Suite.run();
@@ -137,7 +137,8 @@ export function recordTurnRetry(key, reason, detail) {
137
137
  }
138
138
  /** Returns the meta-event timeline for `key`, or an empty array if none recorded. */
139
139
  export function getMetaEvents(key) {
140
- var _a, _b;
140
+ var _a;
141
+ var _b;
141
142
  return (_b = (_a = registry.get(key)) === null || _a === void 0 ? void 0 : _a.events) !== null && _b !== void 0 ? _b : [];
142
143
  }
143
144
  /**
@@ -189,7 +190,12 @@ export function clearSession(key) {
189
190
  * Human/agent-facing guide emitted as the first key of the exported debug log,
190
191
  * so whoever opens the JSON (often an AI agent) knows how to read it without
191
192
  * reverse-engineering the shape. Kept here next to the event catalogue it
192
- * describes so the two stay in sync.
193
+ * describes — but proximity alone demonstrably did NOT keep the two in sync
194
+ * (`refusal` was missing from both reason lists for a whole release), so
195
+ * `debug-event-log.test.ts` now asserts that every `TurnFailureReason` and every
196
+ * `SubAgentFailureReason` is named here, and fails the build when one is added
197
+ * without a matching sentence. Enumerations below read as exhaustive and WILL be
198
+ * trusted by an agent reading the JSON — keep them so.
193
199
  */
194
200
  export const DEBUG_LOG_README = [
195
201
  'This is an exported debug log for the Genesis AI assistant. Read it top-to-bottom.',
@@ -201,7 +207,7 @@ export const DEBUG_LOG_README = [
201
207
  "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.",
202
208
  "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'.",
203
209
  "Each 'event' also has an `importance`: 'high' (failures/limits — turn.error, tool.failed, subagent.failed, file.read-failed, suggestions.failed, context.threshold-crossed), 'normal' (session flow — connects, turns, retries, handoffs, agent/provider changes, interactions, sub-agent start/complete), or 'low' (skippable UI/bookkeeping noise — panel.toggled, attachment.added, driver.wired/unwired, context.updated, context.condensed). To skim, ignore importance:'low'; to triage a failure, filter to importance:'high' then read the nearby messages and turns. A 'high' turn.error is often preceded by one or more 'normal' turn.retry events for the same reason — read them together to see how many attempts were made before bailing. 'message' and 'turn' entries carry no importance — they are the substance, always read them.",
204
- '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.retry (a recoverable in-turn retry — detail.reason plus attempt/maxAttempts; for malformed calls also finishMessage; for empty responses also the provider finishReason + thoughtsTokens + parts breakdown), turn.error (a turn failed or hit a guardrail — detail.reason is one of exception/malformed-function-call/empty-response/unknown-tool-limit/max-iterations/response-truncated, plus reason-specific diagnostics: attempts (for empty-response also finishReason + thoughtsTokens + a parts breakdown, distinguishing a thinking-only STOP from a truly empty turn), finishMessage, for response-truncated the model + maxTokens + outputTokens + tools, unknownTools (split into staleTools — real earlier this activation but retired by the current state or hidden behind an open exclusive fold — and hallucinatedTools — never advertised) + availableTools, iterations + limit, or name + message for exceptions), tool.failed (a tool threw), tool.unresolved (the model called a tool that could not be dispatched — detail.kind is folded/fold-hidden/stale/unknown, plus tool + agent and, for the counted kinds, the consecutive streak; the recurring lead-up to an unknown-tool-limit turn.error), subagent.started/completed/failed (the lifecycle of a `requestSubAgent` delegation — detail.agent names the sub-agent; these bracket the sub-agent turns that appear as kind:turn entries with an N-M `turnIndex`; subagent.failed also carries detail.reason, one of max_iterations/malformed_tool_call/empty_response/unknown_tool_limit/timeout/response_truncated), 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; note that when a sub-agent opens a widget, detail.agent — and the agentName on the interaction message — is the HOST agent that owns the widget, NOT the sub-agent that asked, because widgets render and resolve on the host driver), context.updated/threshold-crossed (token + cost), context.condensed (a stale tool payload was collapsed out of the model-bound history by a `condenseWhen` declaration on the tool — detail.tool + toolCallId, target args|response, trigger (superseded:<key> or age:<n>), stubLen, and an estimated tokensSaved; stored history and this log keep the FULL payload, so the model-visible slice at any point is the full history minus the condensations recorded up to then), panel.toggled, attachment.added, file.read-failed, suggestions.failed.',
210
+ '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.retry (a recoverable in-turn retry — detail.reason plus attempt/maxAttempts; for malformed calls also finishMessage; for empty responses also the provider finishReason + thoughtsTokens + parts breakdown), turn.error (a turn failed or hit a guardrail — detail.reason is one of exception/malformed-function-call/empty-response/unknown-tool-limit/max-iterations/response-truncated/refusal/budget-exhausted, plus reason-specific diagnostics: attempts (for empty-response also finishReason + thoughtsTokens + a parts breakdown, distinguishing a thinking-only STOP from a truly empty turn), finishMessage, for response-truncated the model + maxTokens + outputTokens + tools, unknownTools (split into staleTools — real earlier this activation but retired by the current state or hidden behind an open exclusive fold — and hallucinatedTools — never advertised) + availableTools, iterations + limit, for budget-exhausted the budgetUsd + spentUsd figures reported by the proxy plus the resolved vendor, or name + message for exceptions), tool.failed (a tool threw), tool.unresolved (the model called a tool that could not be dispatched — detail.kind is folded/fold-hidden/stale/unknown, plus tool + agent and, for the counted kinds, the consecutive streak; the recurring lead-up to an unknown-tool-limit turn.error), subagent.started/completed/failed (the lifecycle of a `requestSubAgent` delegation — detail.agent names the sub-agent; these bracket the sub-agent turns that appear as kind:turn entries with an N-M `turnIndex`; subagent.failed also carries detail.reason, one of max_iterations/malformed_tool_call/empty_response/unknown_tool_limit/timeout/response_truncated/refusal/budget_exhausted; budget_exhausted is terminal for the PARENT turn too — the parent stops rather than calling the model again into the same wall), 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; note that when a sub-agent opens a widget, detail.agent — and the agentName on the interaction message — is the HOST agent that owns the widget, NOT the sub-agent that asked, because widgets render and resolve on the host driver), context.updated/threshold-crossed (token + cost), context.condensed (a stale tool payload was collapsed out of the model-bound history by a `condenseWhen` declaration on the tool — detail.tool + toolCallId, target args|response, trigger (superseded:<key> or age:<n>), stubLen, and an estimated tokensSaved; stored history and this log keep the FULL payload, so the model-visible slice at any point is the full history minus the condensations recorded up to then), panel.toggled, attachment.added, file.read-failed, suggestions.failed.',
205
211
  'Sub-agent meta events: a sub-agent\'s own turn.retry/turn.error/tool.failed/tool.unresolved events are merged into this same timeline, tagged with `detail.subAgent` — a `"<parent> › <sub-agent>"` breadcrumb that composes when nested (e.g. `"UI Builder › Planner › Grounding"`) — and interleaved by their original timestamps within the subagent.started→completed/failed bracket. These are the per-attempt/per-failure signals that do NOT appear among the sub-agent\'s (hoisted) messages: a malformed/empty attempt that gets retried produces no message, and the stale-vs-hallucinated split and streak counts live only on the event. A sub-agent\'s high-volume, message-derivable events (turn.start/turn.end, provider.selected, context.updated) are intentionally NOT merged — read its hoisted messages for model/tokens/cost and turn-by-turn activity, and the bracketing subagent.* events for the run\'s span.',
206
212
  "`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.",
207
213
  '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.',
@@ -1,5 +1,5 @@
1
1
  import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
- import { clearMetaEventRegistry, clearSession, getMetaEvents, mergeMetaEvents, recordMetaEvent, } from './debug-event-log';
2
+ import { clearMetaEventRegistry, clearSession, DEBUG_LOG_README, getMetaEvents, mergeMetaEvents, recordMetaEvent, } from './debug-event-log';
3
3
  const event = (overrides = {}) => (Object.assign({ index: 0, timestamp: '2026-06-19T16:00:00.000Z', type: 'turn.retry', importance: 'normal' }, overrides));
4
4
  const suite = createLogicSuite('debug-event-log merge/clear');
5
5
  suite('mergeMetaEvents is a no-op for an empty batch', () => {
@@ -64,4 +64,52 @@ suite('clearSession drops one key without touching others', () => {
64
64
  assert.is(getMetaEvents('child').length, 0);
65
65
  assert.is(getMetaEvents('parent').length, 1);
66
66
  });
67
+ // ── README ↔ taxonomy sync (GENC-1464) ─────────────────────────────────────────
68
+ // DEBUG_LOG_README is emitted as the FIRST key of the exported debug log
69
+ // specifically so an agent reading the JSON does not have to reverse-engineer it.
70
+ // Its reason enumerations therefore read as exhaustive and will be trusted — but
71
+ // they had already drifted twice (`refusal` was missing from both) before anyone
72
+ // noticed, because "the doc lives next to the catalogue" is not enforcement.
73
+ //
74
+ // The `Record<..., true>` below is the enforcement: adding a union member without
75
+ // listing it here is a COMPILE error, and listing it without documenting it is a
76
+ // test failure. So the next added reason fails in the one place the drift keeps
77
+ // happening.
78
+ const ALL_TURN_FAILURE_REASONS = Object.keys({
79
+ exception: true,
80
+ 'malformed-function-call': true,
81
+ 'empty-response': true,
82
+ 'unknown-tool-limit': true,
83
+ 'max-iterations': true,
84
+ 'response-truncated': true,
85
+ refusal: true,
86
+ 'budget-exhausted': true,
87
+ });
88
+ const ALL_SUB_AGENT_FAILURE_REASONS = Object.keys({
89
+ max_iterations: true,
90
+ malformed_tool_call: true,
91
+ empty_response: true,
92
+ unknown_tool_limit: true,
93
+ timeout: true,
94
+ response_truncated: true,
95
+ refusal: true,
96
+ budget_exhausted: true,
97
+ });
98
+ const README_TEXT = DEBUG_LOG_README.join('\n');
99
+ suite('DEBUG_LOG_README enumerates every TurnFailureReason', () => {
100
+ for (const reason of ALL_TURN_FAILURE_REASONS) {
101
+ assert.ok(README_TEXT.includes(reason), `turn.error reason '${reason}' is missing from DEBUG_LOG_README`);
102
+ }
103
+ });
104
+ suite('DEBUG_LOG_README enumerates every SubAgentFailureReason', () => {
105
+ for (const reason of ALL_SUB_AGENT_FAILURE_REASONS) {
106
+ assert.ok(README_TEXT.includes(reason), `subagent.failed reason '${reason}' is missing from DEBUG_LOG_README`);
107
+ }
108
+ });
109
+ suite('DEBUG_LOG_README documents the budget diagnostics that ride turn.error', () => {
110
+ // `recordTurnError('budget-exhausted', …)` attaches these; an agent triaging a
111
+ // support ticket needs to know to look for them.
112
+ assert.ok(README_TEXT.includes('budgetUsd'));
113
+ assert.ok(README_TEXT.includes('spentUsd'));
114
+ });
67
115
  suite.run();
@@ -137,7 +137,8 @@ export class SessionPersister {
137
137
  */
138
138
  persistSession() {
139
139
  return __awaiter(this, void 0, void 0, function* () {
140
- var _a, _b, _c, _d, _e, _f, _g;
140
+ var _a, _b, _c, _d;
141
+ var _e, _f, _g;
141
142
  const { provider, enabled } = this.deps.getPersistence();
142
143
  if (!enabled || !provider)
143
144
  return;
@@ -149,7 +150,7 @@ export class SessionPersister {
149
150
  const s = ref.store.aiAssistant;
150
151
  // Strip the display-only restore notices (restored / couldn't-restore) so they
151
152
  // never persist or accumulate across reload→save→reload cycles.
152
- const messages = ((_b = (_a = driver === null || driver === void 0 ? void 0 : driver.getRawHistory) === null || _a === void 0 ? void 0 : _a.call(driver)) !== null && _b !== void 0 ? _b : s.messages).filter((m) => !isSessionNoticeMarker(m));
153
+ const messages = ((_e = (_a = driver === null || driver === void 0 ? void 0 : driver.getRawHistory) === null || _a === void 0 ? void 0 : _a.call(driver)) !== null && _e !== void 0 ? _e : s.messages).filter((m) => !isSessionNoticeMarker(m));
153
154
  if (!messages.length)
154
155
  return; // nothing worth persisting (e.g. a just-cleared session)
155
156
  // Serialize the live active agent from the DRIVER (the instance whose
@@ -157,7 +158,7 @@ export class SessionPersister {
157
158
  // mints fresh configs (the showcase) would otherwise hand us a never-activated
158
159
  // closure whose `serialize()` returns undefined.
159
160
  const liveSnapshots = {};
160
- const liveActive = (_c = driver === null || driver === void 0 ? void 0 : driver.getActiveAgent) === null || _c === void 0 ? void 0 : _c.call(driver);
161
+ const liveActive = (_b = driver === null || driver === void 0 ? void 0 : driver.getActiveAgent) === null || _b === void 0 ? void 0 : _b.call(driver);
161
162
  if (liveActive === null || liveActive === void 0 ? void 0 : liveActive.serialize) {
162
163
  try {
163
164
  const snap = liveActive.serialize();
@@ -222,7 +223,7 @@ export class SessionPersister {
222
223
  sessionKey: key,
223
224
  host: window.location.host,
224
225
  messages: keepThroughLastUserMessage(messages),
225
- activeAgentName: (_d = s.activeAgent) === null || _d === void 0 ? void 0 : _d.name,
226
+ activeAgentName: (_c = s.activeAgent) === null || _c === void 0 ? void 0 : _c.name,
226
227
  pinnedAgentName: s.pinnedAgentName,
227
228
  flowOwnerAgentName: null,
228
229
  sessionCostUsd: s.sessionUsage.costUsd,
@@ -233,7 +234,7 @@ export class SessionPersister {
233
234
  agentSnapshots: {},
234
235
  });
235
236
  this.logMeta('persistence.suppressed', {
236
- agent: (_e = liveActive === null || liveActive === void 0 ? void 0 : liveActive.name) !== null && _e !== void 0 ? _e : null,
237
+ agent: (_f = liveActive === null || liveActive === void 0 ? void 0 : liveActive.name) !== null && _f !== void 0 ? _f : null,
237
238
  isFlowOwner: s.flowOwnerAgentName === (liveActive === null || liveActive === void 0 ? void 0 : liveActive.name),
238
239
  frozeAtMessages: frozen.messages.length,
239
240
  custom: !!(liveActive === null || liveActive === void 0 ? void 0 : liveActive.notResumableMessage),
@@ -253,7 +254,7 @@ export class SessionPersister {
253
254
  sessionKey: key,
254
255
  host: window.location.host,
255
256
  messages: [...messages],
256
- activeAgentName: (_f = s.activeAgent) === null || _f === void 0 ? void 0 : _f.name,
257
+ activeAgentName: (_d = s.activeAgent) === null || _d === void 0 ? void 0 : _d.name,
257
258
  pinnedAgentName: s.pinnedAgentName,
258
259
  flowOwnerAgentName: s.flowOwnerAgentName,
259
260
  sessionCostUsd: s.sessionUsage.costUsd,
@@ -291,7 +292,8 @@ export class SessionPersister {
291
292
  */
292
293
  restore() {
293
294
  return __awaiter(this, void 0, void 0, function* () {
294
- var _a, _b, _c, _d, _e;
295
+ var _a, _b;
296
+ var _c, _d, _e;
295
297
  const { provider } = this.deps.getPersistence();
296
298
  if (!provider)
297
299
  return;
@@ -351,7 +353,7 @@ export class SessionPersister {
351
353
  // actually came back (meta events reset on reload, so this is the only window
352
354
  // onto the saved→loaded handoff).
353
355
  this.logMeta('persistence.restored', {
354
- agents: Object.keys((_b = snapshot.agentSnapshots) !== null && _b !== void 0 ? _b : {}),
356
+ agents: Object.keys((_c = snapshot.agentSnapshots) !== null && _c !== void 0 ? _c : {}),
355
357
  agentSnapshots: snapshot.agentSnapshots,
356
358
  pinnedAgentName: snapshot.pinnedAgentName,
357
359
  flowOwnerAgentName: snapshot.flowOwnerAgentName,
@@ -419,7 +421,7 @@ export class SessionPersister {
419
421
  }
420
422
  const messages = [...trimmed, ...trailers];
421
423
  ref.actions.aiAssistant.loadSession(toLoadSessionPayload(Object.assign(Object.assign({}, snapshot), { messages })));
422
- (_c = driver === null || driver === void 0 ? void 0 : driver.loadHistory) === null || _c === void 0 ? void 0 : _c.call(driver, [...messages]);
424
+ (_b = driver === null || driver === void 0 ? void 0 : driver.loadHistory) === null || _b === void 0 ? void 0 : _b.call(driver, [...messages]);
423
425
  // Interrupted flow (§5.9): pre-fill the composer with the prompt that started
424
426
  // the flow so "send it again" is one keystroke, and record it for observability.
425
427
  if (snapshot.interrupted) {
@@ -538,7 +540,8 @@ export class SessionPersister {
538
540
  * diagnostics-capable provider, so it's free for other consumers.
539
541
  */
540
542
  scheduleDiagnostics() {
541
- var _a, _b;
543
+ var _a;
544
+ var _b;
542
545
  const { provider } = this.deps.getPersistence();
543
546
  if (!(provider === null || provider === void 0 ? void 0 : provider.appendDiagnostics))
544
547
  return;
@@ -44,10 +44,11 @@ export function createPersistedSession(input) {
44
44
  * @internal
45
45
  */
46
46
  export function resolveNonResumableNotice(input) {
47
- var _a, _b;
47
+ var _a;
48
+ var _b;
48
49
  const custom = input.notResumableMessage;
49
50
  const resumeMessage = custom && custom.trim() ? custom : input.defaultMessage;
50
- const resumePrompt = (_a = input.flowActivationPrompt) !== null && _a !== void 0 ? _a : (_b = input.messages[lastUserMessageIndex(input.messages)]) === null || _b === void 0 ? void 0 : _b.content;
51
+ const resumePrompt = (_b = input.flowActivationPrompt) !== null && _b !== void 0 ? _b : (_a = input.messages[lastUserMessageIndex(input.messages)]) === null || _a === void 0 ? void 0 : _a.content;
51
52
  return { resumeMessage, resumePrompt };
52
53
  }
53
54
  /**
@@ -25,6 +25,24 @@ Suite('createPersistedSession stamps version + savedAt and keeps agentSnapshots'
25
25
  assert.ok(s.savedAt.length > 0);
26
26
  assert.equal(s.agentSnapshots, { A: { machine: { state: 'x', context: {} } } });
27
27
  });
28
+ // GENC-1464 — the blocked latch is deliberately NOT persisted. The snapshot is
29
+ // per-stateKey and long-lived, so a persisted latch would survive an out-of-band
30
+ // budget raise with no in-element way to clear it: the user would return to a
31
+ // permanently dead composer against a budget that has headroom. Re-deriving the
32
+ // state (host pre-flight on mount, or the next 402) is strictly safer than
33
+ // remembering it. This test exists so a future "helpful" addition trips here and
34
+ // has to read that reasoning first — see `FoundationAiAssistant.blocked`'s
35
+ // Lifetime paragraph and docs/migration-GENC-1464.md §Scope.
36
+ Suite('the blocked latch is deliberately not persisted', () => {
37
+ const s = base();
38
+ assert.not.ok('blocked' in s, 'blocked must not enter the persisted snapshot');
39
+ assert.not.ok('blockedReason' in s, 'blockedReason must not enter the persisted snapshot');
40
+ // The per-vendor latches inherit the same lifetime, so they inherit the same
41
+ // rule — a persisted per-vendor wall is exactly as stale as a persisted global
42
+ // one, just harder to notice because only one vendor looks broken.
43
+ assert.not.ok('blockedVendors' in s, 'blockedVendors must not enter the persisted snapshot');
44
+ assert.not.ok('blockedVendorReasons' in s, 'blockedVendorReasons must not enter the persisted snapshot');
45
+ });
28
46
  Suite('createPersistedSession defaults agentSnapshots to {}', () => {
29
47
  const s = createPersistedSession({
30
48
  sessionKey: 'k',
@@ -28,7 +28,8 @@ suite('resolves vendor from providerStatuses when messages lack provider', () =>
28
28
  assert.is((_a = models[0]) === null || _a === void 0 ? void 0 : _a.provider, 'gemini');
29
29
  });
30
30
  suite('does not persist registry tier names as provider', () => {
31
- var _a, _b;
31
+ var _a;
32
+ var _b;
32
33
  const models = collectSessionModels([assistantMsg({ model: 'gemini-3.1-flash-lite', providerName: 'low' })], []);
33
34
  assert.is((_a = models[0]) === null || _a === void 0 ? void 0 : _a.provider, undefined);
34
35
  assert.is('providerName' in ((_b = models[0]) !== null && _b !== void 0 ? _b : {}), false);
@@ -115,7 +115,8 @@ function marksDiscontinuity(kind) {
115
115
  * @param onCondensed - Invoked once per payload at its full→stub transition.
116
116
  */
117
117
  export function applyCondensation(history, policies, ctx, onCondensed) {
118
- var _a, _b;
118
+ var _a;
119
+ var _b;
119
120
  if (policies.size === 0)
120
121
  return history;
121
122
  const triggersOf = (entry) => Array.isArray(entry.policy.on) ? entry.policy.on : [entry.policy.on];
@@ -126,7 +127,7 @@ export function applyCondensation(history, policies, ctx, onCondensed) {
126
127
  // Batch boundary: the collapse set is resolved as of this model-call and stays put
127
128
  // until the next boundary, so the prefix is append-only in between. `batchCalls` of 1
128
129
  // (the default) makes this `ctx.modelCall` — i.e. no batching.
129
- const batchCalls = Math.max(1, Math.floor((_a = ctx.batchCalls) !== null && _a !== void 0 ? _a : 1));
130
+ const batchCalls = Math.max(1, Math.floor((_b = ctx.batchCalls) !== null && _b !== void 0 ? _b : 1));
130
131
  const asOf = batchCalls === 1 ? ctx.modelCall : Math.floor(ctx.modelCall / batchCalls) * batchCalls;
131
132
  // A call made after the boundary is invisible to this pass in BOTH directions: it
132
133
  // cannot supersede an older call (which would collapse the older payload early), and
@@ -137,7 +138,7 @@ export function applyCondensation(history, policies, ctx, onCondensed) {
137
138
  const latestByKey = new Map();
138
139
  const nameById = new Map();
139
140
  for (const msg of history) {
140
- if (!((_b = msg.toolCalls) === null || _b === void 0 ? void 0 : _b.length))
141
+ if (!((_a = msg.toolCalls) === null || _a === void 0 ? void 0 : _a.length))
141
142
  continue;
142
143
  for (const tc of msg.toolCalls) {
143
144
  nameById.set(tc.id, tc.name);
@@ -174,7 +175,8 @@ export function applyCondensation(history, policies, ctx, onCondensed) {
174
175
  // and the event label. `undefined` means the payload stays full.
175
176
  const firstFired = (toolCallId, entry) => triggersOf(entry).find((t) => (marksDiscontinuity(t.kind) || withinBatch(entry)) && triggerFires(toolCallId, entry, t));
176
177
  return history.map((msg) => {
177
- var _a, _b, _c, _d;
178
+ var _a, _b;
179
+ var _c, _d;
178
180
  // Tool-call ARGS live on the assistant message.
179
181
  if ((_a = msg.toolCalls) === null || _a === void 0 ? void 0 : _a.length) {
180
182
  let changed = false;
@@ -9,27 +9,21 @@ const toolMsg = (toolCallId, content) => ({
9
9
  content: '',
10
10
  toolResult: { toolCallId, content },
11
11
  });
12
- const reg = (policy, clocks = {}) => {
13
- var _a, _b, _c, _d;
14
- return ({
15
- policy,
16
- iteration: (_a = clocks.iteration) !== null && _a !== void 0 ? _a : 1,
17
- turn: (_b = clocks.turn) !== null && _b !== void 0 ? _b : 1,
18
- activation: (_c = clocks.activation) !== null && _c !== void 0 ? _c : 1,
19
- phaseEpoch: (_d = clocks.phaseEpoch) !== null && _d !== void 0 ? _d : 1,
20
- });
21
- };
12
+ const reg = (policy, clocks = {}) => { var _a, _b, _c, _d; return ({
13
+ policy,
14
+ iteration: (_a = clocks.iteration) !== null && _a !== void 0 ? _a : 1,
15
+ turn: (_b = clocks.turn) !== null && _b !== void 0 ? _b : 1,
16
+ activation: (_c = clocks.activation) !== null && _c !== void 0 ? _c : 1,
17
+ phaseEpoch: (_d = clocks.phaseEpoch) !== null && _d !== void 0 ? _d : 1,
18
+ }); };
22
19
  /** A CondenseContext for the pure transform. Defaults fire nothing; override per test. */
23
- const ctx = (o = {}) => {
24
- var _a, _b, _c, _d;
25
- return ({
26
- modelCall: (_a = o.modelCall) !== null && _a !== void 0 ? _a : 0,
27
- turn: (_b = o.turn) !== null && _b !== void 0 ? _b : 0,
28
- activationEnded: (_c = o.activationEnded) !== null && _c !== void 0 ? _c : (() => false),
29
- phaseEnded: (_d = o.phaseEnded) !== null && _d !== void 0 ? _d : (() => false),
30
- batchCalls: o.batchCalls,
31
- });
32
- };
20
+ const ctx = (o = {}) => { var _a, _b, _c, _d; return ({
21
+ modelCall: (_a = o.modelCall) !== null && _a !== void 0 ? _a : 0,
22
+ turn: (_b = o.turn) !== null && _b !== void 0 ? _b : 0,
23
+ activationEnded: (_c = o.activationEnded) !== null && _c !== void 0 ? _c : (() => false),
24
+ phaseEnded: (_d = o.phaseEnded) !== null && _d !== void 0 ? _d : (() => false),
25
+ batchCalls: o.batchCalls,
26
+ }); };
33
27
  const sink = () => {
34
28
  const events = [];
35
29
  return { events, on: (d) => events.push(d) };
@@ -353,7 +347,7 @@ const reReadLoop = (n) => {
353
347
  return { history, policies };
354
348
  };
355
349
  /** Result content of every tool message, in order. */
356
- const results = (out) => out.filter((m) => m.role === 'tool').map((m) => { var _a, _b; return (_b = (_a = m.toolResult) === null || _a === void 0 ? void 0 : _a.content) !== null && _b !== void 0 ? _b : ''; });
350
+ const results = (out) => out.filter((m) => m.role === 'tool').map((m) => { var _a; var _b; return (_b = (_a = m.toolResult) === null || _a === void 0 ? void 0 : _a.content) !== null && _b !== void 0 ? _b : ''; });
357
351
  batch('without batching every superseded read collapses immediately', () => {
358
352
  const { history, policies } = reReadLoop(4);
359
353
  const out = results(applyCondensation(history, policies, ctx({ modelCall: 4 }), sink().on));
@@ -30,9 +30,10 @@
30
30
  */
31
31
  export function flattenSubAgentMessages(messages, prefix = '', depth = 0, subAgentOf) {
32
32
  return messages.flatMap((m) => {
33
- var _a, _b, _c;
34
- const breadcrumb = prefix ? `${prefix} › ${(_a = m.agentName) !== null && _a !== void 0 ? _a : '?'}` : m.agentName;
35
- const traceCalls = (_c = (_b = m.toolCalls) === null || _b === void 0 ? void 0 : _b.filter((tc) => { var _a; return (_a = tc.subAgentTrace) === null || _a === void 0 ? void 0 : _a.length; })) !== null && _c !== void 0 ? _c : [];
33
+ var _a;
34
+ var _b, _c;
35
+ const breadcrumb = prefix ? `${prefix} › ${(_b = m.agentName) !== null && _b !== void 0 ? _b : '?'}` : m.agentName;
36
+ const traceCalls = (_c = (_a = m.toolCalls) === null || _a === void 0 ? void 0 : _a.filter((tc) => { var _a; return (_a = tc.subAgentTrace) === null || _a === void 0 ? void 0 : _a.length; })) !== null && _c !== void 0 ? _c : [];
36
37
  const entry = Object.assign(Object.assign(Object.assign({ kind: 'message' }, m), (traceCalls.length > 0 && {
37
38
  toolCalls: m.toolCalls.map((tc) => tc.subAgentTrace ? Object.assign(Object.assign({}, tc), { subAgentTrace: undefined }) : tc),
38
39
  })), (depth > 0 && {
@@ -130,7 +130,8 @@ export function normalizeForProvider(history) {
130
130
  }
131
131
  /** Human-facing divider label for a `compacted-summary` message. */
132
132
  export function compactionDividerLabel(m) {
133
- var _a, _b;
133
+ var _a;
134
+ var _b;
134
135
  const n = (_b = (_a = m.compaction) === null || _a === void 0 ? void 0 : _a.compactedCount) !== null && _b !== void 0 ? _b : 0;
135
136
  return `Compacted ${n} earlier message${n === 1 ? '' : 's'}`;
136
137
  }
@@ -1,6 +1,7 @@
1
1
  /** Resolve `chatConfig.costHistory` with defaults applied. */
2
2
  export function resolveCostHistoryConfig(chatConfig) {
3
- var _a, _b, _c, _d;
3
+ var _a, _b, _c;
4
+ var _d;
4
5
  const cost = chatConfig.costHistory;
5
6
  return {
6
7
  enabled: (cost === null || cost === void 0 ? void 0 : cost.enabled) === true,
@@ -8,7 +9,7 @@ export function resolveCostHistoryConfig(chatConfig) {
8
9
  scope: ((_b = cost === null || cost === void 0 ? void 0 : cost.scope) === null || _b === void 0 ? void 0 : _b.trim()) || undefined,
9
10
  showCost: (cost === null || cost === void 0 ? void 0 : cost.showCost) !== false,
10
11
  showTokens: (cost === null || cost === void 0 ? void 0 : cost.showTokens) !== false,
11
- badgeLabel: (_c = cost === null || cost === void 0 ? void 0 : cost.badgeLabel) !== null && _c !== void 0 ? _c : 'Currently building',
12
- summaryHint: ((_d = cost === null || cost === void 0 ? void 0 : cost.summaryHint) === null || _d === void 0 ? void 0 : _d.trim()) || undefined,
12
+ badgeLabel: (_d = cost === null || cost === void 0 ? void 0 : cost.badgeLabel) !== null && _d !== void 0 ? _d : 'Currently building',
13
+ summaryHint: ((_c = cost === null || cost === void 0 ? void 0 : cost.summaryHint) === null || _c === void 0 ? void 0 : _c.trim()) || undefined,
13
14
  };
14
15
  }
@@ -20,7 +20,8 @@ import { resolveExclusiveLoadingStyle } from './animation-exclusivity';
20
20
  * @internal
21
21
  */
22
22
  export function resolvePreferenceBaseline(ui, stored) {
23
- var _a, _b;
23
+ var _a;
24
+ var _b;
24
25
  const u = ui !== null && ui !== void 0 ? ui : {};
25
26
  // Layer 1 — chatConfig.ui defaults.
26
27
  const baseline = {
@@ -91,7 +91,8 @@ function addInto(into, from) {
91
91
  * per-message allocation is worth avoiding.
92
92
  */
93
93
  function accumulate(messages, into) {
94
- var _a, _b, _c, _d, _e, _f;
94
+ var _a;
95
+ var _b, _c, _d, _e, _f;
95
96
  for (const m of messages) {
96
97
  // Provider/LLM cost plus any non-LLM cost a widget reported for its own
97
98
  // external service calls. Both are real spend on this session.
@@ -99,20 +100,20 @@ function accumulate(messages, into) {
99
100
  into.costUsd += m.cost;
100
101
  if (m.externalCostUsd != null)
101
102
  into.costUsd += m.externalCostUsd;
102
- const cacheRead = (_a = m.cacheReadTokens) !== null && _a !== void 0 ? _a : 0;
103
- const cacheWrite = (_b = m.cacheWriteTokens) !== null && _b !== void 0 ? _b : 0;
103
+ const cacheRead = (_b = m.cacheReadTokens) !== null && _b !== void 0 ? _b : 0;
104
+ const cacheWrite = (_c = m.cacheWriteTokens) !== null && _c !== void 0 ? _c : 0;
104
105
  into.cacheReadTokens += cacheRead;
105
106
  into.cacheWriteTokens += cacheWrite;
106
107
  // The uncached remainder of the prompt. Clamped at zero so malformed or
107
108
  // hand-edited history whose buckets exceed the prompt total can't drive the
108
109
  // session total negative — a wrong-but-plausible number is worse than a
109
110
  // conservative one here, since this feeds a cost display.
110
- into.uncachedInputTokens += Math.max(0, ((_c = m.inputTokens) !== null && _c !== void 0 ? _c : 0) - cacheRead - cacheWrite);
111
- into.outputTokens += (_d = m.outputTokens) !== null && _d !== void 0 ? _d : 0;
111
+ into.uncachedInputTokens += Math.max(0, ((_d = m.inputTokens) !== null && _d !== void 0 ? _d : 0) - cacheRead - cacheWrite);
112
+ into.outputTokens += (_e = m.outputTokens) !== null && _e !== void 0 ? _e : 0;
112
113
  // Spend banked by a compaction on this summary's behalf. NOTE: deliberately
113
114
  // not `compaction.tokensBefore` — that is a one-turn context-window reading,
114
115
  // not cumulative spend, and counting it here would inflate the total.
115
- const rolled = (_e = m.compaction) === null || _e === void 0 ? void 0 : _e.rolledUpUsage;
116
+ const rolled = (_a = m.compaction) === null || _a === void 0 ? void 0 : _a.rolledUpUsage;
116
117
  if (rolled)
117
118
  addInto(into, rolled);
118
119
  for (const tc of (_f = m.toolCalls) !== null && _f !== void 0 ? _f : []) {