@genesislcap/ai-assistant 15.19.6 → 15.20.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.
Files changed (42) hide show
  1. package/dist/ai-assistant.api.json +605 -72
  2. package/dist/ai-assistant.d.ts +404 -25
  3. package/dist/chat-driver.cjs +341 -28
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +341 -28
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +630 -20
  8. package/dist/dts/components/ai-driver/ai-driver.d.ts +33 -7
  9. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  10. package/dist/dts/components/chat-driver/chat-driver.d.ts +63 -2
  11. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  12. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +9 -3
  13. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  14. package/dist/dts/config/config.d.ts +44 -0
  15. package/dist/dts/config/config.d.ts.map +1 -1
  16. package/dist/dts/main/main.d.ts +187 -5
  17. package/dist/dts/main/main.d.ts.map +1 -1
  18. package/dist/dts/main/main.styles.d.ts.map +1 -1
  19. package/dist/dts/main/main.template.d.ts.map +1 -1
  20. package/dist/dts/utils/condense-history.d.ts.map +1 -1
  21. package/dist/dts/utils/context-tokens.d.ts +156 -0
  22. package/dist/dts/utils/context-tokens.d.ts.map +1 -0
  23. package/dist/dts/utils/history-transform.d.ts +76 -14
  24. package/dist/dts/utils/history-transform.d.ts.map +1 -1
  25. package/dist/dts/utils/resolve-context-budget.d.ts +98 -0
  26. package/dist/dts/utils/resolve-context-budget.d.ts.map +1 -0
  27. package/dist/esm/components/chat-driver/chat-driver.js +179 -34
  28. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +12 -4
  29. package/dist/esm/main/main.js +391 -21
  30. package/dist/esm/main/main.styles.js +128 -0
  31. package/dist/esm/main/main.template.js +64 -29
  32. package/dist/esm/state/debug-event-log.js +1 -1
  33. package/dist/esm/utils/condense-history.js +1 -5
  34. package/dist/esm/utils/context-tokens.js +339 -0
  35. package/dist/esm/utils/history-transform.js +101 -19
  36. package/dist/esm/utils/resolve-context-budget.js +84 -0
  37. package/package.json +16 -16
  38. package/sandbox/README.md +93 -4
  39. package/sandbox/controls.ts +77 -10
  40. package/sandbox/fixtures.ts +163 -6
  41. package/sandbox/sandbox.css +54 -1
  42. package/sandbox/sandbox.ts +384 -7
@@ -55,11 +55,13 @@ import { AnimatedPanelToggle } from '../utils/animated-panel-toggle';
55
55
  import { resolveExclusiveLoadingStyle } from '../utils/animation-exclusivity';
56
56
  import { deleteBankedBaseline, getBankedBaseline, setBankedBaseline, } from '../utils/banked-usage-baselines';
57
57
  import { collectSessionModels } from '../utils/collect-session-models';
58
+ import { estimateContextTokens, estimateSystemOverhead, isContextMeasurable, } from '../utils/context-tokens';
58
59
  import { clearCostSessionHistory, isCostSessionRecord, loadCostSessionHistory, resolveBankedUsage, saveCostSessionHistory, sortRecordsByRecency, upsertRecord, } from '../utils/cost-session-history';
59
60
  import { deriveCostSessionTitleFromMessages, resolveCostSessionTitle, } from '../utils/derive-cost-session-title';
60
61
  import { formatUsd } from '../utils/format-usd';
61
62
  import { logger } from '../utils/logger';
62
63
  import { filterVisibleMessages, trailingInteractionRow } from '../utils/message-partition';
64
+ import { CONTEXT_ABORT_MARGIN_TOKENS, resolveContextBudget, } from '../utils/resolve-context-budget';
63
65
  import { resolveCostHistoryConfig, } from '../utils/resolve-cost-history-config';
64
66
  import { resolvePreferenceBaseline } from '../utils/resolve-preference-baseline';
65
67
  import { stripAgentHandlers } from '../utils/strip-agent-handlers';
@@ -641,7 +643,11 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
641
643
  * restored/compacted history lands (GENC-1351 §6).
642
644
  */
643
645
  get sendBlocked() {
644
- return this.busy || this.restoring || this.compacting || this.blocked;
646
+ return (this.busy ||
647
+ this.restoring ||
648
+ this.compacting ||
649
+ this.blocked ||
650
+ this.contextGate === 'blocked');
645
651
  }
646
652
  /**
647
653
  * Why a programmatic send was refused, for `submitMessage`'s `errors`. A
@@ -650,7 +656,14 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
650
656
  * looping on "Assistant is busy" would spin forever.
651
657
  */
652
658
  sendRefusalReason() {
653
- return this.blocked ? this.effectiveBlockedReason : 'Assistant is busy';
659
+ if (this.blocked)
660
+ return this.effectiveBlockedReason;
661
+ // Also a standing condition rather than a transient one — a caller looping on
662
+ // "Assistant is busy" would spin forever — but unlike a budget wall the user
663
+ // CAN clear this one, so the reason says how.
664
+ if (this.contextGate === 'blocked')
665
+ return this.contextGateReason;
666
+ return 'Assistant is busy';
654
667
  }
655
668
  /**
656
669
  * Re-runs `agentsChanged` if the live `agents` array no longer matches the
@@ -2602,6 +2615,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
2602
2615
  // reassignment preserve both.
2603
2616
  driver.setPinnedAgent(this.pinnedAgentName);
2604
2617
  driver.setFlowOwner(this.flowOwnerAgentName);
2618
+ driver.setContextGuard(this.contextGuardPolicy);
2605
2619
  const onOrchStart = () => {
2606
2620
  this.showHalo = 'orchestrating';
2607
2621
  };
@@ -3009,12 +3023,45 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3009
3023
  : yield this.providerRegistry.getStatus();
3010
3024
  this.contextLimit = status === null || status === void 0 ? void 0 : status.contextLimit;
3011
3025
  this.activeModel = status === null || status === void 0 ? void 0 : status.model;
3026
+ this.syncContextGuard();
3012
3027
  }
3013
3028
  catch (_b) {
3014
3029
  // Non-fatal — context limit / model display simply won't show
3015
3030
  }
3016
3031
  });
3017
3032
  }
3033
+ /**
3034
+ * Push the mid-loop context guard down to the driver (GENC-1567).
3035
+ *
3036
+ * Called wherever the inputs can change — the resolved provider status, and a
3037
+ * freshly built driver. A driver that never receives one simply runs without
3038
+ * the guard, which is the right default: it fires only when a real context
3039
+ * window is known.
3040
+ */
3041
+ syncContextGuard() {
3042
+ var _a, _b;
3043
+ (_b = (_a = this.driver) === null || _a === void 0 ? void 0 : _a.setContextGuard) === null || _b === void 0 ? void 0 : _b.call(_a, this.contextGuardPolicy);
3044
+ }
3045
+ /**
3046
+ * Guard policy pushed to the driver, or `undefined` when the host has switched
3047
+ * the gate off — so `enabled: false` really does mean no local blocking of any
3048
+ * kind, UI or mid-loop.
3049
+ *
3050
+ * A MARGIN, not a threshold: the driver resolves the window of the provider
3051
+ * each call actually goes to. The fallback window rides along because the
3052
+ * driver cannot learn it any other way — without it, a host that knows its
3053
+ * window while its provider does not report one had `fallbackContextLimit`
3054
+ * protecting the composer and nothing else.
3055
+ */
3056
+ get contextGuardPolicy() {
3057
+ var _a;
3058
+ if (!this.contextBudget.enabled)
3059
+ return undefined;
3060
+ return {
3061
+ marginTokens: CONTEXT_ABORT_MARGIN_TOKENS,
3062
+ fallbackLimit: (_a = this.chatConfig.context) === null || _a === void 0 ? void 0 : _a.fallbackContextLimit,
3063
+ };
3064
+ }
3018
3065
  loadProviderStatuses() {
3019
3066
  return __awaiter(this, void 0, void 0, function* () {
3020
3067
  try {
@@ -3033,6 +3080,10 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3033
3080
  this.syncShowingSplash();
3034
3081
  this.syncActiveCostSessionTitle();
3035
3082
  this.reloadCostSessionHistory();
3083
+ // `chatConfig.context` decides whether the mid-loop guard runs at all, so a
3084
+ // late or changed binding has to reach the driver — otherwise switching the
3085
+ // gate off leaves the guard armed from whatever the config said at connect.
3086
+ this.syncContextGuard();
3036
3087
  }
3037
3088
  /**
3038
3089
  * `persistence` (like `chatConfig`) can bind after `connectedCallback` — a host may
@@ -3128,12 +3179,31 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3128
3179
  }
3129
3180
  this.syncShowHalo();
3130
3181
  this.syncShowingSplash();
3131
- // Update context token count from the most recent message that carries usage data.
3132
- for (let i = this.messages.length - 1; i >= 0; i -= 1) {
3133
- if (this.messages[i].inputTokens != null) {
3134
- this.contextTokens = this.messages[i].inputTokens;
3135
- break;
3136
- }
3182
+ // Context size, from the estimator rather than from the last raw measurement.
3183
+ //
3184
+ // `inputTokens` on a response measured the prompt that PRODUCED it, so it is
3185
+ // always at least one message out of date — it cannot see the response's own
3186
+ // content, nor any tool results appended since, all of which ride the next
3187
+ // prompt. As a display figure that lag was invisible; once it gates sending,
3188
+ // a single large final response could carry the transcript past the block
3189
+ // while the composer stayed live (GENC-1567).
3190
+ //
3191
+ // `estimateContextTokens` is that measurement rebased: calibrated against
3192
+ // every usable anchor, with whatever followed the last one added on. It also
3193
+ // discards anchors invalidated by a compaction — the case where trusting the
3194
+ // raw figure was not a lag but a dead end, leaving the gate blocked on a
3195
+ // conversation that had already shrunk, with sending (the only way to produce
3196
+ // a fresh measurement) being the thing blocked.
3197
+ //
3198
+ // Gated on the transcript being MEASURABLE AT ALL rather than on a currently
3199
+ // usable anchor. Straight after a compaction every retained measurement is
3200
+ // invalidated, so the narrower test skipped the assignment and left a blocked
3201
+ // figure latched — reintroducing the dead end from the other direction. The
3202
+ // estimator handles the invalidated anchors; this only decides whether the
3203
+ // gate gets an opinion, and a provider that has never reported usage still
3204
+ // leaves `contextTokens` unset and the gate inert.
3205
+ if (isContextMeasurable(this.messages)) {
3206
+ this.contextTokens = estimateContextTokens(this.messages);
3137
3207
  }
3138
3208
  // Recompute cost and all four token buckets from the transcript in one walk.
3139
3209
  // Recomputing (rather than incrementing on append) keeps the totals correct under
@@ -3414,8 +3484,45 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3414
3484
  */
3415
3485
  get sessionMenuEnabled() {
3416
3486
  var _a;
3487
+ // The context gate forces the menu open, whatever hid it (GENC-1567). Both
3488
+ // reasons it can be hidden produce the same dead end — a disabled composer
3489
+ // telling the user to compact, with no control anywhere on screen that
3490
+ // compacts. Note that `showSessionMenu: false` is documented for exactly the
3491
+ // guided-onboarding case this gate is most likely to fire in, so the override
3492
+ // is not an edge case; the flow-entry reserve is what should keep it rare.
3493
+ if (this.sessionMenuForcedByContext)
3494
+ return true;
3417
3495
  return ((_a = this.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showSessionMenu) !== false && !!this.persistence.provider;
3418
3496
  }
3497
+ /**
3498
+ * Whether the menu is on screen ONLY because the context gate put it there.
3499
+ *
3500
+ * @internal
3501
+ */
3502
+ get sessionMenuForcedByContext() {
3503
+ var _a;
3504
+ if (this.contextGate === 'ok')
3505
+ return false;
3506
+ return ((_a = this.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showSessionMenu) === false || !this.persistence.provider;
3507
+ }
3508
+ /**
3509
+ * Whether to withhold Clear from a menu the context gate forced open.
3510
+ *
3511
+ * The hosts who hid this menu did so to protect a flow from being cleared
3512
+ * mid-journey, and handing someone a Clear button at the moment they are stuck
3513
+ * hunting for a way out is how a conversation gets destroyed by accident.
3514
+ *
3515
+ * But only **while Compact is a real escape**. When the plan says compacting
3516
+ * would not free enough, starting a new conversation is the only way out — and
3517
+ * the blocked copy says exactly that, pointing the user at this menu to do it.
3518
+ * Withholding Clear there would send them to a menu that does not contain the
3519
+ * action they were just told to take.
3520
+ *
3521
+ * @internal
3522
+ */
3523
+ get clearWithheldByContext() {
3524
+ return this.sessionMenuForcedByContext && this.compactable;
3525
+ }
3419
3526
  /** The persistence toggle row renders only when a provider is configured. */
3420
3527
  get persistenceToggleable() {
3421
3528
  return !!this.persistence.provider;
@@ -3425,15 +3532,218 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3425
3532
  return !!this.persistence.enabled && !!this.persistence.provider;
3426
3533
  }
3427
3534
  /**
3428
- * Whether the menu's "Compact" action would do anything (GENC-1351 §5.7).
3429
- * Delegates to `driver.canCompact()` so the gate uses the exact same history
3430
- * `compact()` acts on — one source of truth. Also reads `messages`, purely to
3431
- * stay reactive as the conversation grows: the driver's history isn't a tracked
3432
- * observable, so the button keys its re-evaluation off the Redux mirror.
3535
+ * Context-headroom thresholds for the active model (GENC-1567) — the warning
3536
+ * and block lines, plus the tail budget compaction aims at.
3537
+ *
3538
+ * The agent's own `contextReserve` is deliberately NOT folded in here. This is
3539
+ * the AMBIENT budget, which protects any turn from any agent; a per-agent
3540
+ * reserve is an additional, larger requirement checked when that agent takes
3541
+ * ownership of a flow, where there is a turn context to resolve its function
3542
+ * form against.
3543
+ *
3544
+ * Overhead is read off `messages` (the Redux mirror) rather than the driver's
3545
+ * own history, which the compaction plan uses. That is safe rather than sloppy:
3546
+ * `estimateSystemOverhead` only looks at messages BEFORE the first usage-bearing
3547
+ * one, and the two lists agree completely that early — the drift between mirror
3548
+ * and driver history is only ever at the tail.
3433
3549
  */
3434
- get compactable() {
3550
+ get contextBudget() {
3551
+ return resolveContextBudget({
3552
+ chatConfig: this.chatConfig,
3553
+ contextLimit: this.contextLimit,
3554
+ agentReserveTokens: this.activeAgentReserveTokens,
3555
+ systemOverhead: estimateSystemOverhead(this.messages),
3556
+ });
3557
+ }
3558
+ /**
3559
+ * The active agent's declared `contextReserve`, when it is a plain number.
3560
+ *
3561
+ * Read off the LIVE active agent rather than checked at flow entry, which is
3562
+ * where the design first put it. `flow-owner-changed` turned out to fire while
3563
+ * the driver is already routing the turn — after the send was allowed — so it
3564
+ * is not a pre-flight hook, and gating there would mean aborting a turn in
3565
+ * flight. Reading the active agent instead means a hungry agent's reserve
3566
+ * governs the gate from its next send onwards: one turn later than a true
3567
+ * pre-flight, but synchronous, and it never has to interrupt anything.
3568
+ *
3569
+ * `contextReserve` is a plain number by design — see `ContextReserveInput` for
3570
+ * why a resolver form was dropped rather than accepted and ignored.
3571
+ *
3572
+ * @internal
3573
+ */
3574
+ get activeAgentReserveTokens() {
3435
3575
  var _a, _b, _c;
3436
- return this.messages.length > 0 && ((_c = (_b = (_a = this.driver) === null || _a === void 0 ? void 0 : _a.canCompact) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : false);
3576
+ return (_c = (_b = (_a = this.driver) === null || _a === void 0 ? void 0 : _a.getActiveAgent) === null || _b === void 0 ? void 0 : _b.call(_a)) === null || _c === void 0 ? void 0 : _c.contextReserve;
3577
+ }
3578
+ /**
3579
+ * What compacting right now would reclaim, or `null` when it would not be
3580
+ * worth it (GENC-1567). Sized against {@link FoundationAiAssistant.contextBudget},
3581
+ * so the plan aims at this model's window rather than a fixed default.
3582
+ *
3583
+ * Reads `messages` to stay reactive as the conversation grows: the driver's
3584
+ * history is not a tracked observable, so every consumer of this keys its
3585
+ * re-evaluation off the Redux mirror.
3586
+ */
3587
+ get compactionPlan() {
3588
+ var _a, _b, _c;
3589
+ if (this.messages.length === 0)
3590
+ return null;
3591
+ const budget = this.contextBudget;
3592
+ return ((_c = (_b = (_a = this.driver) === null || _a === void 0 ? void 0 : _a.getCompactionPlan) === null || _b === void 0 ? void 0 : _b.call(_a, {
3593
+ tailTokenBudget: budget.tailTokenBudget,
3594
+ minReclaimTokens: budget.minReclaimTokens,
3595
+ })) !== null && _c !== void 0 ? _c : null);
3596
+ }
3597
+ /**
3598
+ * Tooltip and hint for the Compact row — why it is or is not available.
3599
+ *
3600
+ * Replaces a fixed "Not enough conversation yet to be worth compacting", which
3601
+ * since GENC-1567 could be flatly wrong: a long conversation whose tail is one
3602
+ * indivisible tool result has plenty of conversation and still cannot be
3603
+ * compacted. Reading the plan means the hint states the actual reason.
3604
+ *
3605
+ * @internal
3606
+ */
3607
+ get compactHint() {
3608
+ const plan = this.compactionPlan;
3609
+ if (plan)
3610
+ return `Summarize older messages to free up context`;
3611
+ return this.messages.length === 0
3612
+ ? 'Nothing to compact yet'
3613
+ : 'There is nothing left to summarize here — the recent messages are too large to shrink further';
3614
+ }
3615
+ /**
3616
+ * How close this conversation is to the model's context window (GENC-1567).
3617
+ *
3618
+ * **Derived, never latched** — the opposite of {@link FoundationAiAssistant.blocked}.
3619
+ * A budget wall stays set because nothing the user does fixes it; this clears
3620
+ * itself the moment the conversation shrinks, which is precisely what the user
3621
+ * is being asked to do. The two must not be conflated: `blocked` is also
3622
+ * deliberately preserved across `resetSession`, and a context gate obviously
3623
+ * must not survive starting a new chat.
3624
+ *
3625
+ * **A flow in progress is never blocked, only warned.** Machine state lives
3626
+ * outside the transcript, so a stateful flow is not desynced by compaction —
3627
+ * but the summary can still drop detail the flow's own prompt leans on, and
3628
+ * blocking mid-journey would strand the user between a flow they cannot finish
3629
+ * and a compaction that is a poor idea right there. Letting the flow complete
3630
+ * and gating on the way out is the lesser harm; the headroom reserve exists so
3631
+ * this is rare rather than routine.
3632
+ */
3633
+ get contextGate() {
3634
+ const budget = this.contextBudget;
3635
+ if (!budget.enabled)
3636
+ return 'ok';
3637
+ const tokens = this.contextTokens;
3638
+ if (tokens == null || budget.blockAt == null || budget.warnAt == null)
3639
+ return 'ok';
3640
+ if (tokens >= budget.blockAt)
3641
+ return this.flowOwnerAgentName !== null ? 'warn' : 'blocked';
3642
+ return tokens >= budget.warnAt ? 'warn' : 'ok';
3643
+ }
3644
+ /**
3645
+ * What the composer notice says about the context gate.
3646
+ *
3647
+ * The copy is a function of whether compaction can actually clear the gate,
3648
+ * which is why it reads {@link FoundationAiAssistant.compactionPlan} rather
3649
+ * than assuming. Telling someone to compact when the projection says it would
3650
+ * not free enough — a tail that is one indivisible tool result, say — sends
3651
+ * them into a summarizer call that leaves them exactly where they were.
3652
+ *
3653
+ * @internal
3654
+ */
3655
+ get contextGateReason() {
3656
+ const level = this.contextGate;
3657
+ const canHelp = this.compactionPlan != null;
3658
+ if (level === 'blocked') {
3659
+ return canHelp
3660
+ ? 'There is not enough room in the model context for more work. Click the error pill and compact the context to continue working.'
3661
+ : 'There is not enough room in the model context for more work. Compacting the context will not free enough room, click the error pill to clear and start a new conversation.';
3662
+ }
3663
+ if (this.flowOwnerAgentName !== null) {
3664
+ return 'This conversation is nearly full. The task in progress will finish, but compact before starting another.';
3665
+ }
3666
+ return canHelp
3667
+ ? 'This conversation is getting long. Compact it to free up room.'
3668
+ : 'This conversation is getting long and is close to the limit.';
3669
+ }
3670
+ /**
3671
+ * Short label the session-menu pill wears while the gate is active — the
3672
+ * primary visible signal, in place of a banner.
3673
+ *
3674
+ * Borrowed from the agent pin, which is the established way this composer says
3675
+ * "a mode is in force": a pill that grows a label rather than a strip of prose
3676
+ * above the input. It states the CONDITION only; the remedy lives in the
3677
+ * placeholder once the composer is actually blocked, which is the moment the
3678
+ * user needs to be told what to do rather than merely warned.
3679
+ *
3680
+ * @internal
3681
+ */
3682
+ get contextGateLabel() {
3683
+ switch (this.contextGate) {
3684
+ case 'blocked':
3685
+ return 'At context limit';
3686
+ case 'warn':
3687
+ return 'Approaching context limit';
3688
+ default:
3689
+ return '';
3690
+ }
3691
+ }
3692
+ /**
3693
+ * Whether the composer notice has anything to show — a budget wall or the
3694
+ * context gate. One region for both, so the live region stays single and
3695
+ * registered (see the banner template's accessibility note).
3696
+ *
3697
+ * @internal
3698
+ */
3699
+ get composerNoticeVisible() {
3700
+ return this.bannerVisible || this.contextGate !== 'ok';
3701
+ }
3702
+ /**
3703
+ * Text for that region. A budget wall outranks the context gate: it is the
3704
+ * condition the user cannot clear, so it must not be hidden behind advice
3705
+ * about compacting that would not restore sending anyway.
3706
+ *
3707
+ * For the context gate the region is present and announced but not drawn — see
3708
+ * {@link FoundationAiAssistant.composerNoticeSrOnly}.
3709
+ *
3710
+ * @internal
3711
+ */
3712
+ get composerNoticeText() {
3713
+ if (this.bannerVisible)
3714
+ return this.effectiveBlockedReason;
3715
+ return this.contextGate === 'ok' ? '' : this.contextGateReason;
3716
+ }
3717
+ /**
3718
+ * Whether the notice region should be announced but not drawn.
3719
+ *
3720
+ * The context gate shows itself visually through the pill's label, the
3721
+ * composer outline and the placeholder — a strip of prose on top of those
3722
+ * would be a fourth statement of the same thing. But dropping the region
3723
+ * outright would be an accessibility regression, and a documented one: a
3724
+ * `role="status"` region announces MUTATIONS to a region already being
3725
+ * observed, and it is the only announcement available here, because the
3726
+ * composer flips to `disabled` at the same moment and a disabled control is out
3727
+ * of the tab order — so neither the placeholder nor an `aria-label` on it is
3728
+ * ever voiced.
3729
+ *
3730
+ * So the region stays mounted, keeps its text and keeps announcing; only the
3731
+ * visual treatment is withheld. A budget wall still draws its banner, because
3732
+ * that condition has no other visible expression.
3733
+ *
3734
+ * @internal
3735
+ */
3736
+ get composerNoticeSrOnly() {
3737
+ return !this.bannerVisible && this.contextGate !== 'ok';
3738
+ }
3739
+ /**
3740
+ * Whether the menu's "Compact" action would do anything (GENC-1351 §5.7,
3741
+ * GENC-1567). Delegates to the driver's plan so the gate uses the exact same
3742
+ * history `compact()` acts on, judged against the same budget — one source of
3743
+ * truth for both whether a cut is legal and whether it reclaims enough.
3744
+ */
3745
+ get compactable() {
3746
+ return this.compactionPlan != null;
3437
3747
  }
3438
3748
  toggleSessionMenu() {
3439
3749
  // Mutually exclusive with the agent picker; always reopen showing the menu
@@ -3562,7 +3872,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3562
3872
  */
3563
3873
  compactSession() {
3564
3874
  return __awaiter(this, void 0, void 0, function* () {
3565
- var _a, _b, _c, _d;
3875
+ var _a, _b, _c, _d, _e, _f;
3566
3876
  const driver = this.driver;
3567
3877
  if (!(driver === null || driver === void 0 ? void 0 : driver.compact) || driver.isBusy() || this.compacting)
3568
3878
  return;
@@ -3579,16 +3889,32 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3579
3889
  // false` in the `finally` would then clear the SUCCESSOR session's flag. A write
3580
3890
  // through the captured ref is a no-op on a detached store.
3581
3891
  const ref = this._sessionRef;
3892
+ // Capture the plan the affordance was gated on and hand the SAME budget to
3893
+ // the action, so the compaction that runs is the one the user was shown
3894
+ // (GENC-1567) — and so the projected reclaim can be logged next to what the
3895
+ // compaction actually did.
3896
+ const budget = this.contextBudget;
3897
+ const planOptions = {
3898
+ tailTokenBudget: budget.tailTokenBudget,
3899
+ minReclaimTokens: budget.minReclaimTokens,
3900
+ };
3901
+ const plan = (_b = (_a = driver.getCompactionPlan) === null || _a === void 0 ? void 0 : _a.call(driver, planOptions)) !== null && _b !== void 0 ? _b : null;
3582
3902
  ref === null || ref === void 0 ? void 0 : ref.actions.aiAssistant.setCompacting(true);
3583
3903
  try {
3584
- const summary = yield driver.compact();
3904
+ const summary = yield driver.compact(planOptions);
3585
3905
  if (summary) {
3586
3906
  this.logMeta('context.compacted', {
3587
- compactedCount: (_a = summary.compaction) === null || _a === void 0 ? void 0 : _a.compactedCount,
3588
- coveredThroughTimestamp: (_b = summary.compaction) === null || _b === void 0 ? void 0 : _b.coveredThroughTimestamp,
3589
- model: (_c = summary.compaction) === null || _c === void 0 ? void 0 : _c.model,
3907
+ compactedCount: (_c = summary.compaction) === null || _c === void 0 ? void 0 : _c.compactedCount,
3908
+ coveredThroughTimestamp: (_d = summary.compaction) === null || _d === void 0 ? void 0 : _d.coveredThroughTimestamp,
3909
+ model: (_e = summary.compaction) === null || _e === void 0 ? void 0 : _e.model,
3590
3910
  trigger: 'manual',
3591
- tokensBefore: (_d = summary.compaction) === null || _d === void 0 ? void 0 : _d.tokensBefore,
3911
+ tokensBefore: (_f = summary.compaction) === null || _f === void 0 ? void 0 : _f.tokensBefore,
3912
+ // The projection the gate promised. Logged alongside the real figures
3913
+ // so a drifting estimator shows up in the diagnostics rather than as an
3914
+ // unexplained "I compacted and it did not help".
3915
+ projectedTokensAfter: plan === null || plan === void 0 ? void 0 : plan.tokensAfter,
3916
+ projectedReclaim: plan === null || plan === void 0 ? void 0 : plan.reclaimed,
3917
+ contextLimit: budget.limit,
3592
3918
  summaryText: summary.content,
3593
3919
  });
3594
3920
  }
@@ -4083,6 +4409,11 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
4083
4409
  // anyway, so naming the pinned agent would only imply a send is possible.
4084
4410
  if (this.blocked)
4085
4411
  return BLOCKED_PLACEHOLDER;
4412
+ // The full explanation, not a stub. With no banner above it this is the only
4413
+ // place the remedy is stated, and the composer it sits in is the control the
4414
+ // remedy is about — so the sentence is where the user is already looking.
4415
+ if (this.contextGate === 'blocked')
4416
+ return this.contextGateReason;
4086
4417
  if (this.pinnedAgentName)
4087
4418
  return `Message ${this.pinnedAgentName}...`;
4088
4419
  return this.placeholder;
@@ -5162,6 +5493,9 @@ __decorate([
5162
5493
  __decorate([
5163
5494
  volatile
5164
5495
  ], FoundationAiAssistant.prototype, "visibleMessages", null);
5496
+ __decorate([
5497
+ volatile
5498
+ ], FoundationAiAssistant.prototype, "contextGuardPolicy", null);
5165
5499
  __decorate([
5166
5500
  observable
5167
5501
  ], FoundationAiAssistant.prototype, "confirmingClear", void 0);
@@ -5171,12 +5505,48 @@ __decorate([
5171
5505
  __decorate([
5172
5506
  volatile
5173
5507
  ], FoundationAiAssistant.prototype, "sessionMenuEnabled", null);
5508
+ __decorate([
5509
+ volatile
5510
+ ], FoundationAiAssistant.prototype, "sessionMenuForcedByContext", null);
5511
+ __decorate([
5512
+ volatile
5513
+ ], FoundationAiAssistant.prototype, "clearWithheldByContext", null);
5174
5514
  __decorate([
5175
5515
  volatile
5176
5516
  ], FoundationAiAssistant.prototype, "persistenceToggleable", null);
5177
5517
  __decorate([
5178
5518
  volatile
5179
5519
  ], FoundationAiAssistant.prototype, "persistenceActive", null);
5520
+ __decorate([
5521
+ volatile
5522
+ ], FoundationAiAssistant.prototype, "contextBudget", null);
5523
+ __decorate([
5524
+ volatile
5525
+ ], FoundationAiAssistant.prototype, "activeAgentReserveTokens", null);
5526
+ __decorate([
5527
+ volatile
5528
+ ], FoundationAiAssistant.prototype, "compactionPlan", null);
5529
+ __decorate([
5530
+ volatile
5531
+ ], FoundationAiAssistant.prototype, "compactHint", null);
5532
+ __decorate([
5533
+ volatile
5534
+ ], FoundationAiAssistant.prototype, "contextGate", null);
5535
+ __decorate([
5536
+ volatile
5537
+ ], FoundationAiAssistant.prototype, "contextGateReason", null);
5538
+ __decorate([
5539
+ volatile
5540
+ ], FoundationAiAssistant.prototype, "contextGateLabel", null);
5541
+ __decorate([
5542
+ volatile
5543
+ ], FoundationAiAssistant.prototype, "composerNoticeVisible", null);
5544
+ __decorate([
5545
+ volatile
5546
+ ], FoundationAiAssistant.prototype, "composerNoticeText", null);
5547
+ __decorate([
5548
+ volatile
5549
+ ], FoundationAiAssistant.prototype, "composerNoticeSrOnly", null);
5180
5550
  __decorate([
5181
5551
  volatile
5182
5552
  ], FoundationAiAssistant.prototype, "compactable", null);