@genesislcap/ai-assistant 15.23.0 → 15.23.1-FUI-0.4

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 (33) hide show
  1. package/dist/ai-assistant.api.json +269 -5
  2. package/dist/ai-assistant.d.ts +256 -31
  3. package/dist/chat-driver.cjs +125 -58
  4. package/dist/chat-driver.cjs.map +2 -2
  5. package/dist/chat-driver.mjs +125 -58
  6. package/dist/chat-driver.mjs.map +2 -2
  7. package/dist/custom-elements.json +125 -6
  8. package/dist/dts/components/ai-driver/ai-driver.d.ts +63 -0
  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 +7 -0
  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 -1
  13. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  14. package/dist/dts/config/config.d.ts +53 -17
  15. package/dist/dts/config/config.d.ts.map +1 -1
  16. package/dist/dts/config/define-stateful-agent.d.ts +16 -1
  17. package/dist/dts/config/define-stateful-agent.d.ts.map +1 -1
  18. package/dist/dts/main/main.d.ts +102 -14
  19. package/dist/dts/main/main.d.ts.map +1 -1
  20. package/dist/dts/utils/context-tokens.d.ts +100 -43
  21. package/dist/dts/utils/context-tokens.d.ts.map +1 -1
  22. package/dist/dts/utils/history-transform.d.ts +7 -1
  23. package/dist/dts/utils/history-transform.d.ts.map +1 -1
  24. package/dist/dts/utils/resolve-context-budget.d.ts +8 -5
  25. package/dist/dts/utils/resolve-context-budget.d.ts.map +1 -1
  26. package/dist/esm/components/chat-driver/chat-driver.js +22 -2
  27. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +92 -0
  28. package/dist/esm/config/define-stateful-agent.js +1 -0
  29. package/dist/esm/main/main.js +170 -27
  30. package/dist/esm/utils/context-tokens.js +331 -106
  31. package/dist/esm/utils/history-transform.js +5 -5
  32. package/dist/esm/utils/resolve-context-budget.js +37 -7
  33. package/package.json +16 -16
@@ -46,6 +46,40 @@ function isSpecialist(agent) {
46
46
  function isFallback(agent) {
47
47
  return agent.fallback === true;
48
48
  }
49
+ /**
50
+ * Flatten a `contextReserve` declaration to the two numbers the gate reads
51
+ * (GENC-1582), or `undefined` when the agent declared nothing.
52
+ *
53
+ * A bare number is the peak-only form and leaves the warning to the host's
54
+ * `warnMultiplier`. Non-positive `tokens` is treated as no declaration rather
55
+ * than as "needs no headroom": zero would resolve to a reserve of 0, which sets
56
+ * `blockAt` to the whole window and switches the gate off for every agent the
57
+ * turn could reach — the opposite of what declaring a reserve is for.
58
+ *
59
+ * **Validated at runtime, not just by the type.** `contextReserve` is host-supplied
60
+ * `@beta` config and can arrive from JS, from JSON, or from a dynamic source, so
61
+ * `tokens` may be absent or not a number whatever the signature says. That matters
62
+ * more than a violated return type: `undefined <= 0` is `false`, so a bare
63
+ * comparison passed a malformed reserve straight through as `{ tokens: undefined }`,
64
+ * and the caller's `reserve.tokens > winner.tokens` then compared against
65
+ * `undefined` — always false. One malformed entry on an earlier-listed agent
66
+ * therefore SUPPRESSED every valid reserve behind it, and the gate fell back to the
67
+ * ambient default while still naming the malformed agent. Silent, because
68
+ * `resolveContextBudget` coalesces an undefined reserve to that same default
69
+ * (reviewer catch on #2504).
70
+ */
71
+ function normalizeContextReserve(input) {
72
+ if (input == null)
73
+ return undefined;
74
+ const positive = (v) => typeof v === 'number' && Number.isFinite(v) && v > 0;
75
+ if (typeof input === 'number')
76
+ return positive(input) ? { tokens: input } : undefined;
77
+ if (!positive(input.tokens))
78
+ return undefined;
79
+ return positive(input.warnAheadTokens)
80
+ ? { tokens: input.tokens, warnAheadTokens: input.warnAheadTokens }
81
+ : { tokens: input.tokens };
82
+ }
49
83
  /**
50
84
  * Placeholder a consumer can put in its fallback prompt to position
51
85
  * {@link UNROUTABLE_INSTRUCTION} itself, instead of having it appended.
@@ -293,6 +327,10 @@ export class OrchestratingDriver extends EventTarget {
293
327
  canCompact(options) {
294
328
  return this.chatDriver.canCompact(options);
295
329
  }
330
+ /** {@inheritDoc AiDriver.getPreambleTokens} */
331
+ getPreambleTokens() {
332
+ return this.chatDriver.getPreambleTokens();
333
+ }
296
334
  /** {@inheritDoc AiDriver.getCompactionPlan} */
297
335
  getCompactionPlan(options) {
298
336
  return this.chatDriver.getCompactionPlan(options);
@@ -499,6 +537,60 @@ export class OrchestratingDriver extends EventTarget {
499
537
  primeRestoredAgentState(snapshots) {
500
538
  this.restoredAgentStates = Object.assign({}, snapshots);
501
539
  }
540
+ /**
541
+ * {@inheritDoc AiDriver.getContextReserve}
542
+ *
543
+ * Two cases, because routing has two modes and they need opposite answers.
544
+ */
545
+ getContextReserve() {
546
+ // LOCKED — a user pin or a flow-owner lock. `handleTurn` routes straight to
547
+ // this agent and the handoff branch at the bottom of the routing loop refuses
548
+ // to route away while a lock is held, so no other agent's headroom can come
549
+ // into play until it clears. Its reserve is the whole requirement.
550
+ const locked = this.resolvePinnedAgent();
551
+ if (locked) {
552
+ const reserve = normalizeContextReserve(locked.contextReserve);
553
+ return reserve == null ? undefined : Object.assign(Object.assign({}, reserve), { agentName: locked.name, basis: 'locked' });
554
+ }
555
+ // AUTOMATIC — the classifier picks a specialist, may fall through to the
556
+ // fallback, and may hand off and re-classify more than once inside the one
557
+ // turn. Any of them could be the agent that runs, and the send is allowed or
558
+ // refused before we know which, so the requirement is the largest reserve
559
+ // among them: the turn has to fit whichever way it routes.
560
+ //
561
+ // The MAX, not the sum, even though a handoff chain really does add each leg's
562
+ // growth on top of the last. Summing every reachable agent would reserve room
563
+ // for a journey nobody is taking and strand most of the window on a two-line
564
+ // question; the mid-loop context guard is what bounds a chain that does run
565
+ // long. The max is the smallest reserve that makes the FIRST leg safe, which
566
+ // is the only leg this gate can speak for.
567
+ //
568
+ // `excludeFromClassifier` agents are absent by construction — they are
569
+ // reachable only by pin or by a restored flow-owner lock, and both of those
570
+ // are the locked arm above. The active agent is folded in as a floor: on the
571
+ // turn after a lock clears it is still the agent holding the context, and it
572
+ // need not be one the classifier could have picked.
573
+ const candidates = [...this.specialists];
574
+ if (this.fallback)
575
+ candidates.push(this.fallback);
576
+ if (this.activeAgent)
577
+ candidates.push(this.activeAgent);
578
+ // Compared on `tokens` — the figure that decides whether a turn may start.
579
+ // `warnAheadTokens` is not a candidate for the max in its own right; it rides
580
+ // along with whichever agent wins.
581
+ let winner;
582
+ let agentName = '';
583
+ for (const agent of candidates) {
584
+ const reserve = normalizeContextReserve(agent.contextReserve);
585
+ if (!reserve)
586
+ continue;
587
+ if (!winner || reserve.tokens > winner.tokens) {
588
+ winner = reserve;
589
+ agentName = agent.name;
590
+ }
591
+ }
592
+ return winner ? Object.assign(Object.assign({}, winner), { agentName, basis: 'reachable-max' }) : undefined;
593
+ }
502
594
  /** {@inheritDoc AiDriver.getActiveAgent} */
503
595
  getActiveAgent() {
504
596
  return this.activeAgent;
@@ -264,6 +264,7 @@ export function defineStatefulAgent(opts) {
264
264
  onUnresolvedTool: wrappedOnUnresolvedTool,
265
265
  resumable: wrappedResumable,
266
266
  notResumableMessage: opts.notResumableMessage,
267
+ contextReserve: opts.contextReserve,
267
268
  onActivate: (ctx) => __awaiter(this, void 0, void 0, function* () {
268
269
  state = yield opts.init(ctx);
269
270
  // Sample once with the init state to fail loud on fold misuse at the
@@ -55,7 +55,7 @@ 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
+ import { estimateContextTokens, isContextMeasurable, } from '../utils/context-tokens';
59
59
  import { clearCostSessionHistory, isCostSessionRecord, loadCostSessionHistory, resolveBankedUsage, saveCostSessionHistory, sortRecordsByRecency, upsertRecord, } from '../utils/cost-session-history';
60
60
  import { deriveCostSessionTitleFromMessages, resolveCostSessionTitle, } from '../utils/derive-cost-session-title';
61
61
  import { formatUsd } from '../utils/format-usd';
@@ -607,6 +607,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
607
607
  // immediately, but only if they haven't moved on to something else.
608
608
  // `cancelling` is busy too, so loading → cancelling must NOT settle yet.
609
609
  if (this.isBusyState(prev) && !this.isBusyState(value)) {
610
+ this.rearmContextGate();
610
611
  this.maybeAutoFocusChatInput();
611
612
  // Apply a session re-key deferred by the busy guard first (it rebuilds the
612
613
  // driver against current agents, so the agents-swap below then no-ops).
@@ -622,6 +623,31 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
622
623
  isBusyState(s) {
623
624
  return s === 'loading' || s === 'cancelling';
624
625
  }
626
+ /**
627
+ * Re-arm {@link FoundationAiAssistant.contextGate} against the size the turn
628
+ * just left behind (GENC-1582).
629
+ *
630
+ * The final call of a turn is made AFTER `agentEnd` condensation fires, so by
631
+ * the time a turn settles `contextTokens` is the resting figure rather than the
632
+ * mid-turn peak — which is the number the reserve's thresholds are drawn
633
+ * against.
634
+ *
635
+ * **Called from BOTH settle paths, and it has to be.** A turn does not end
636
+ * through the `state` setter: `send()`'s `finally` writes `setState('idle')`
637
+ * straight to the captured session ref, because the element may have
638
+ * disconnected mid-turn, and that block then replicates the parts of the
639
+ * setter's settle that matter. Arming the gate inline in the setter therefore
640
+ * armed it on the one path a real turn never takes — the gate silently kept
641
+ * falling back to the live figure and the hold did nothing at all. Found by
642
+ * driving a real UI Builder turn and reading `_gateContextTokens`, which was
643
+ * `undefined` from start to finish; no unit test noticed, because the test
644
+ * harness sets `state` through the property.
645
+ *
646
+ * One method rather than two assignments so the two paths cannot drift again.
647
+ */
648
+ rearmContextGate() {
649
+ this._gateContextTokens = this.contextTokens;
650
+ }
625
651
  /**
626
652
  * True while a turn is active — `loading` or `cancelling` (a stop was
627
653
  * requested but the turn is still winding down). The single "is a turn
@@ -3205,6 +3231,12 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3205
3231
  if (isContextMeasurable(this.messages)) {
3206
3232
  this.contextTokens = estimateContextTokens(this.messages);
3207
3233
  }
3234
+ // Keep the gate's figure in step while at rest. Deliberately skipped mid-turn:
3235
+ // that is the spike the gate must not react to (see `_gateContextTokens`). A
3236
+ // compaction reaches this branch — it runs under `compacting`, not `busy` — so
3237
+ // the shrink it produces is picked up immediately rather than waiting for a turn.
3238
+ if (!this.busy)
3239
+ this._gateContextTokens = this.contextTokens;
3208
3240
  // Recompute cost and all four token buckets from the transcript in one walk.
3209
3241
  // Recomputing (rather than incrementing on append) keeps the totals correct under
3210
3242
  // any mutation of the message list — clear-chat, re-render, restore. `sumUsage`
@@ -3503,6 +3535,13 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3503
3535
  var _a;
3504
3536
  if (this.contextGate === 'ok')
3505
3537
  return false;
3538
+ // The `'model'` cause is the exception (GENC-1582). This override exists so a
3539
+ // user told to compact can reach Compact — but when the window cannot host the
3540
+ // agent, Compact and Clear are both futile, and forcing a menu of dead actions
3541
+ // in front of someone is worse than not opening it. The copy points at the
3542
+ // model picker instead.
3543
+ if (this.contextBlockCause === 'model')
3544
+ return false;
3506
3545
  return ((_a = this.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.showSessionMenu) === false || !this.persistence.provider;
3507
3546
  }
3508
3547
  /**
@@ -3535,11 +3574,11 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3535
3574
  * Context-headroom thresholds for the active model (GENC-1567) — the warning
3536
3575
  * and block lines, plus the tail budget compaction aims at.
3537
3576
  *
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.
3577
+ * The per-agent `contextReserve` IS folded in, via the driver (GENC-1582). It
3578
+ * used to be described here as a separate, later check made when an agent took
3579
+ * ownership of a flow — but there is no such check, and by flow entry the send
3580
+ * has already been allowed. Resolving it here, before the send, is what makes it
3581
+ * a gate rather than a label.
3543
3582
  *
3544
3583
  * Overhead is read off `messages` (the Redux mirror) rather than the driver's
3545
3584
  * own history, which the compaction plan uses. That is safe rather than sloppy:
@@ -3548,32 +3587,63 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3548
3587
  * and driver history is only ever at the tail.
3549
3588
  */
3550
3589
  get contextBudget() {
3590
+ // Resolved ONCE. `contextReserve` re-reads live routing state on every access,
3591
+ // so two reads can land on different winners and pair one agent's
3592
+ // `warnAheadTokens` with another's `tokens` — the exact pairing
3593
+ // `ContextReserveResolution` warns against, and the reason this is a local
3594
+ // rather than two property reads (reviewer catch on #2504). It also stops the
3595
+ // orchestrator walking every specialist plus the fallback twice per evaluation.
3596
+ const reserve = this.contextReserve;
3551
3597
  return resolveContextBudget({
3552
3598
  chatConfig: this.chatConfig,
3553
3599
  contextLimit: this.contextLimit,
3554
- agentReserveTokens: this.activeAgentReserveTokens,
3555
- systemOverhead: estimateSystemOverhead(this.messages),
3600
+ agentReserveTokens: reserve === null || reserve === void 0 ? void 0 : reserve.tokens,
3601
+ agentWarnAheadTokens: reserve === null || reserve === void 0 ? void 0 : reserve.warnAheadTokens,
3556
3602
  });
3557
3603
  }
3558
3604
  /**
3559
- * The active agent's declared `contextReserve`, when it is a plain number.
3605
+ * Headroom the next turn needs, resolved against how routing is currently
3606
+ * constrained (GENC-1582), and which agent set it.
3607
+ *
3608
+ * The driver owns the answer because the driver owns routing: whether a pin or a
3609
+ * flow-owner lock is in force, which agents the classifier can reach, and which
3610
+ * one is active. This used to read `getActiveAgent()?.contextReserve` — the agent
3611
+ * that LAST ran — which is a different agent from the one about to run whenever
3612
+ * the orchestrator is doing its job, and the handover turn is precisely the one
3613
+ * that needs the room.
3560
3614
  *
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.
3615
+ * Falls back to the active agent for a host-supplied driver that does not
3616
+ * implement the resolution, so an existing single-agent driver keeps the old
3617
+ * behaviour rather than silently losing its reserve.
3568
3618
  *
3569
3619
  * `contextReserve` is a plain number by design — see `ContextReserveInput` for
3570
3620
  * why a resolver form was dropped rather than accepted and ignored.
3571
3621
  *
3572
3622
  * @internal
3573
3623
  */
3574
- get activeAgentReserveTokens() {
3575
- var _a, _b, _c;
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;
3624
+ get contextReserve() {
3625
+ var _a, _b, _c, _d;
3626
+ const resolved = (_b = (_a = this.driver) === null || _a === void 0 ? void 0 : _a.getContextReserve) === null || _b === void 0 ? void 0 : _b.call(_a);
3627
+ if (resolved)
3628
+ return resolved;
3629
+ const active = (_d = (_c = this.driver) === null || _c === void 0 ? void 0 : _c.getActiveAgent) === null || _d === void 0 ? void 0 : _d.call(_c);
3630
+ const declared = active === null || active === void 0 ? void 0 : active.contextReserve;
3631
+ if (declared == null)
3632
+ return undefined;
3633
+ // Runtime-validated for the same reason `normalizeContextReserve` is: this is
3634
+ // host-supplied config, and `undefined <= 0` is `false`, so a bare comparison
3635
+ // lets a malformed reserve through as `tokens: undefined` (reviewer catch on #2504).
3636
+ const positive = (v) => typeof v === 'number' && Number.isFinite(v) && v > 0;
3637
+ const tokens = typeof declared === 'number' ? declared : declared.tokens;
3638
+ if (!positive(tokens))
3639
+ return undefined;
3640
+ const warnAhead = typeof declared === 'number' ? undefined : declared.warnAheadTokens;
3641
+ return {
3642
+ tokens,
3643
+ warnAheadTokens: positive(warnAhead) ? warnAhead : undefined,
3644
+ agentName: active.name,
3645
+ basis: 'locked',
3646
+ };
3577
3647
  }
3578
3648
  /**
3579
3649
  * What compacting right now would reclaim, or `null` when it would not be
@@ -3585,14 +3655,15 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3585
3655
  * re-evaluation off the Redux mirror.
3586
3656
  */
3587
3657
  get compactionPlan() {
3588
- var _a, _b, _c;
3658
+ var _a, _b, _c, _d, _e;
3589
3659
  if (this.messages.length === 0)
3590
3660
  return null;
3591
3661
  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, {
3662
+ return ((_e = (_b = (_a = this.driver) === null || _a === void 0 ? void 0 : _a.getCompactionPlan) === null || _b === void 0 ? void 0 : _b.call(_a, {
3663
+ preambleTokens: (_d = (_c = this.driver) === null || _c === void 0 ? void 0 : _c.getPreambleTokens) === null || _d === void 0 ? void 0 : _d.call(_c),
3593
3664
  tailTokenBudget: budget.tailTokenBudget,
3594
3665
  minReclaimTokens: budget.minReclaimTokens,
3595
- })) !== null && _c !== void 0 ? _c : null);
3666
+ })) !== null && _e !== void 0 ? _e : null);
3596
3667
  }
3597
3668
  /**
3598
3669
  * Tooltip and hint for the Compact row — why it is or is not available.
@@ -3630,11 +3701,48 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3630
3701
  * and gating on the way out is the lesser harm; the headroom reserve exists so
3631
3702
  * this is rare rather than routine.
3632
3703
  */
3704
+ /**
3705
+ * WHY the composer is blocked, when it is (GENC-1582). `undefined` unless
3706
+ * {@link FoundationAiAssistant.contextGate} is `'blocked'`.
3707
+ *
3708
+ * A cause rather than a fourth gate level: the severity is identical — sends are
3709
+ * refused either way — and every consumer switches on the three levels. What
3710
+ * differs is what the user can do about it, which is worth splitting because the
3711
+ * two answers have no remedy in common.
3712
+ *
3713
+ * - `'transcript'` — the conversation has grown into the reserve. Compacting or
3714
+ * clearing helps, and the copy says so.
3715
+ * - `'model'` — the active agent's reserve meets or exceeds the whole window, so
3716
+ * `blockAt` is 0 and EVERY message is refused, including the first of an empty
3717
+ * chat. Neither compacting nor clearing can help: the constraint is the model,
3718
+ * not the transcript. Before this existed the user was told to clear the chat,
3719
+ * did, and hit the identical block with no way to understand why.
3720
+ *
3721
+ * Keyed off `blockAt === 0`, deliberately NOT off whether a compaction plan
3722
+ * exists. A null plan has two unrelated causes — nothing left worth summarizing,
3723
+ * versus a window that cannot host the agent at all — and conflating them is what
3724
+ * produced the misleading copy.
3725
+ *
3726
+ * Note the block itself is correct in both cases and must stay: an agent that
3727
+ * measurably cannot complete a turn in this window would otherwise start one and
3728
+ * die mid-loop on a provider context error, with partial writes and no clean
3729
+ * exit. Refusing up front is the better failure.
3730
+ *
3731
+ * @internal
3732
+ */
3733
+ get contextBlockCause() {
3734
+ if (this.contextGate !== 'blocked')
3735
+ return undefined;
3736
+ return this.contextBudget.blockAt === 0 ? 'model' : 'transcript';
3737
+ }
3633
3738
  get contextGate() {
3739
+ var _a;
3634
3740
  const budget = this.contextBudget;
3635
3741
  if (!budget.enabled)
3636
3742
  return 'ok';
3637
- const tokens = this.contextTokens;
3743
+ // The figure from the last moment nothing was running, not the live one — see
3744
+ // `_gateContextTokens` for why the gauge and the gate differ here.
3745
+ const tokens = (_a = this._gateContextTokens) !== null && _a !== void 0 ? _a : this.contextTokens;
3638
3746
  if (tokens == null || budget.blockAt == null || budget.warnAt == null)
3639
3747
  return 'ok';
3640
3748
  if (tokens >= budget.blockAt)
@@ -3653,12 +3761,36 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3653
3761
  * @internal
3654
3762
  */
3655
3763
  get contextGateReason() {
3764
+ var _a, _b;
3656
3765
  const level = this.contextGate;
3657
3766
  const canHelp = this.compactionPlan != null;
3767
+ if (level === 'blocked' && this.contextBlockCause === 'model') {
3768
+ // The window cannot host this agent, so neither remedy the other branches
3769
+ // offer can help. Name the agent and both figures instead: the numbers are
3770
+ // what make "switch model" an instruction rather than a shrug, and they are
3771
+ // already to hand.
3772
+ const reserve = this.contextReserve;
3773
+ const need = reserve ? `${reserve.tokens.toLocaleString()} tokens` : 'more headroom';
3774
+ const have = (_a = this.contextLimit) === null || _a === void 0 ? void 0 : _a.toLocaleString();
3775
+ const who = (_b = reserve === null || reserve === void 0 ? void 0 : reserve.agentName) !== null && _b !== void 0 ? _b : 'This task';
3776
+ return (`${who} needs about ${need} of context headroom to complete a turn, and this model has ` +
3777
+ `${have !== null && have !== void 0 ? have : 'less than that'}. Switch to a model with a larger context window, or pick a ` +
3778
+ `different agent — compacting or clearing this conversation will not help.`);
3779
+ }
3658
3780
  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.';
3781
+ // Name the agent whose headroom is binding when routing is automatic
3782
+ // (GENC-1582). The reserve is then the hungriest agent the turn could land
3783
+ // on, so the composer can close while there is plenty of room for the short
3784
+ // question the user was about to type — and a message that only said "not
3785
+ // enough room for more work" would read as a bug rather than a reason.
3786
+ const reserve = this.contextReserve;
3787
+ const cause = (reserve === null || reserve === void 0 ? void 0 : reserve.basis) === 'reachable-max'
3788
+ ? `There is not enough room in the model context to start another task — ${reserve.agentName} needs the most of any I could pick up next.`
3789
+ : 'There is not enough room in the model context for more work.';
3790
+ const remedy = canHelp
3791
+ ? 'Click the error pill and compact the context to continue working.'
3792
+ : 'Compacting the context will not free enough room, click the error pill to clear and start a new conversation.';
3793
+ return `${cause} ${remedy}`;
3662
3794
  }
3663
3795
  if (this.flowOwnerAgentName !== null) {
3664
3796
  return 'This conversation is nearly full. The task in progress will finish, but compact before starting another.';
@@ -3963,6 +4095,10 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
3963
4095
  if (this.driver)
3964
4096
  this.teardownDriver(key);
3965
4097
  (_g = this._sessionRef) === null || _g === void 0 ? void 0 : _g.actions.aiAssistant.resetSession();
4098
+ // `resetSession` zeroes the store-backed `contextTokens`; the gate's held copy
4099
+ // is ours to drop. Leaving it would carry a block into a conversation that no
4100
+ // longer exists — the one thing `contextGate` must never do (GENC-1582).
4101
+ this._gateContextTokens = undefined;
3966
4102
  if (key) {
3967
4103
  this.rebaselineOnClearedTranscript(key, discarded);
3968
4104
  this.rebuildDriver(key);
@@ -5172,6 +5308,7 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
5172
5308
  // setState was dispatched directly via the captured ref (not via the
5173
5309
  // state setter) so the setter's loading→idle cleanup path doesn't fire
5174
5310
  // here. Replicate the parts of it that matter for the post-turn settle.
5311
+ this.rearmContextGate();
5175
5312
  this.maybeAutoFocusChatInput();
5176
5313
  this.applyDeferredSessionSwitch();
5177
5314
  this.applyDeferredAgentsSwap();
@@ -5385,6 +5522,9 @@ __decorate([
5385
5522
  __decorate([
5386
5523
  observable
5387
5524
  ], FoundationAiAssistant.prototype, "_sessionRef", void 0);
5525
+ __decorate([
5526
+ observable
5527
+ ], FoundationAiAssistant.prototype, "_gateContextTokens", void 0);
5388
5528
  __decorate([
5389
5529
  volatile
5390
5530
  ], FoundationAiAssistant.prototype, "busy", null);
@@ -5528,13 +5668,16 @@ __decorate([
5528
5668
  ], FoundationAiAssistant.prototype, "contextBudget", null);
5529
5669
  __decorate([
5530
5670
  volatile
5531
- ], FoundationAiAssistant.prototype, "activeAgentReserveTokens", null);
5671
+ ], FoundationAiAssistant.prototype, "contextReserve", null);
5532
5672
  __decorate([
5533
5673
  volatile
5534
5674
  ], FoundationAiAssistant.prototype, "compactionPlan", null);
5535
5675
  __decorate([
5536
5676
  volatile
5537
5677
  ], FoundationAiAssistant.prototype, "compactHint", null);
5678
+ __decorate([
5679
+ volatile
5680
+ ], FoundationAiAssistant.prototype, "contextBlockCause", null);
5538
5681
  __decorate([
5539
5682
  volatile
5540
5683
  ], FoundationAiAssistant.prototype, "contextGate", null);