@slkiser/opencode-quota 3.1.4 → 3.3.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 (133) hide show
  1. package/README.md +256 -559
  2. package/dist/lib/anthropic.js +1 -1
  3. package/dist/lib/anthropic.js.map +1 -1
  4. package/dist/lib/cache.d.ts +17 -27
  5. package/dist/lib/cache.d.ts.map +1 -1
  6. package/dist/lib/cache.js +62 -65
  7. package/dist/lib/cache.js.map +1 -1
  8. package/dist/lib/config-file-utils.d.ts +12 -0
  9. package/dist/lib/config-file-utils.d.ts.map +1 -1
  10. package/dist/lib/config-file-utils.js +23 -0
  11. package/dist/lib/config-file-utils.js.map +1 -1
  12. package/dist/lib/config.d.ts +16 -3
  13. package/dist/lib/config.d.ts.map +1 -1
  14. package/dist/lib/config.js +448 -214
  15. package/dist/lib/config.js.map +1 -1
  16. package/dist/lib/copilot.d.ts.map +1 -1
  17. package/dist/lib/copilot.js +3 -2
  18. package/dist/lib/copilot.js.map +1 -1
  19. package/dist/lib/entries.d.ts +6 -2
  20. package/dist/lib/entries.d.ts.map +1 -1
  21. package/dist/lib/format-utils.d.ts.map +1 -1
  22. package/dist/lib/format-utils.js +3 -2
  23. package/dist/lib/format-utils.js.map +1 -1
  24. package/dist/lib/format.d.ts +2 -1
  25. package/dist/lib/format.d.ts.map +1 -1
  26. package/dist/lib/format.js +12 -6
  27. package/dist/lib/format.js.map +1 -1
  28. package/dist/lib/google-gemini-cli-companion.d.ts +29 -0
  29. package/dist/lib/google-gemini-cli-companion.d.ts.map +1 -0
  30. package/dist/lib/google-gemini-cli-companion.js +166 -0
  31. package/dist/lib/google-gemini-cli-companion.js.map +1 -0
  32. package/dist/lib/google-gemini-cli.d.ts +48 -0
  33. package/dist/lib/google-gemini-cli.d.ts.map +1 -0
  34. package/dist/lib/google-gemini-cli.js +404 -0
  35. package/dist/lib/google-gemini-cli.js.map +1 -0
  36. package/dist/lib/init-installer.d.ts +2 -1
  37. package/dist/lib/init-installer.d.ts.map +1 -1
  38. package/dist/lib/init-installer.js +10 -6
  39. package/dist/lib/init-installer.js.map +1 -1
  40. package/dist/lib/opencode-go.js +1 -1
  41. package/dist/lib/opencode-go.js.map +1 -1
  42. package/dist/lib/provider-metadata.d.ts +2 -1
  43. package/dist/lib/provider-metadata.d.ts.map +1 -1
  44. package/dist/lib/provider-metadata.js +27 -0
  45. package/dist/lib/provider-metadata.js.map +1 -1
  46. package/dist/lib/quota-format-style.d.ts +21 -0
  47. package/dist/lib/quota-format-style.d.ts.map +1 -0
  48. package/dist/lib/quota-format-style.js +38 -0
  49. package/dist/lib/quota-format-style.js.map +1 -0
  50. package/dist/lib/quota-render-data.d.ts +4 -12
  51. package/dist/lib/quota-render-data.d.ts.map +1 -1
  52. package/dist/lib/quota-render-data.js +83 -70
  53. package/dist/lib/quota-render-data.js.map +1 -1
  54. package/dist/lib/quota-runtime-context.d.ts +43 -0
  55. package/dist/lib/quota-runtime-context.d.ts.map +1 -0
  56. package/dist/lib/quota-runtime-context.js +61 -0
  57. package/dist/lib/quota-runtime-context.js.map +1 -0
  58. package/dist/lib/quota-state.d.ts +21 -0
  59. package/dist/lib/quota-state.d.ts.map +1 -0
  60. package/dist/lib/quota-state.js +228 -0
  61. package/dist/lib/quota-state.js.map +1 -0
  62. package/dist/lib/quota-status.d.ts +16 -0
  63. package/dist/lib/quota-status.d.ts.map +1 -1
  64. package/dist/lib/quota-status.js +63 -17
  65. package/dist/lib/quota-status.js.map +1 -1
  66. package/dist/lib/toast-format-grouped.d.ts.map +1 -1
  67. package/dist/lib/toast-format-grouped.js +5 -3
  68. package/dist/lib/toast-format-grouped.js.map +1 -1
  69. package/dist/lib/tui-config-diagnostics.d.ts +7 -2
  70. package/dist/lib/tui-config-diagnostics.d.ts.map +1 -1
  71. package/dist/lib/tui-config-diagnostics.js +27 -8
  72. package/dist/lib/tui-config-diagnostics.js.map +1 -1
  73. package/dist/lib/tui-runtime.d.ts +1 -2
  74. package/dist/lib/tui-runtime.d.ts.map +1 -1
  75. package/dist/lib/tui-runtime.js +24 -17
  76. package/dist/lib/tui-runtime.js.map +1 -1
  77. package/dist/lib/tui-sidebar-format.d.ts.map +1 -1
  78. package/dist/lib/tui-sidebar-format.js +2 -10
  79. package/dist/lib/tui-sidebar-format.js.map +1 -1
  80. package/dist/lib/types.d.ts +51 -9
  81. package/dist/lib/types.d.ts.map +1 -1
  82. package/dist/lib/types.js +2 -1
  83. package/dist/lib/types.js.map +1 -1
  84. package/dist/plugin.d.ts.map +1 -1
  85. package/dist/plugin.js +448 -242
  86. package/dist/plugin.js.map +1 -1
  87. package/dist/providers/alibaba-coding-plan.d.ts.map +1 -1
  88. package/dist/providers/alibaba-coding-plan.js +0 -16
  89. package/dist/providers/alibaba-coding-plan.js.map +1 -1
  90. package/dist/providers/anthropic.d.ts.map +1 -1
  91. package/dist/providers/anthropic.js +15 -29
  92. package/dist/providers/anthropic.js.map +1 -1
  93. package/dist/providers/copilot.d.ts.map +1 -1
  94. package/dist/providers/copilot.js +27 -53
  95. package/dist/providers/copilot.js.map +1 -1
  96. package/dist/providers/cursor.d.ts.map +1 -1
  97. package/dist/providers/cursor.js +38 -79
  98. package/dist/providers/cursor.js.map +1 -1
  99. package/dist/providers/google-gemini-cli.d.ts +3 -0
  100. package/dist/providers/google-gemini-cli.d.ts.map +1 -0
  101. package/dist/providers/google-gemini-cli.js +83 -0
  102. package/dist/providers/google-gemini-cli.js.map +1 -0
  103. package/dist/providers/kimi-code.d.ts.map +1 -1
  104. package/dist/providers/kimi-code.js +3 -15
  105. package/dist/providers/kimi-code.js.map +1 -1
  106. package/dist/providers/minimax-coding-plan.d.ts.map +1 -1
  107. package/dist/providers/minimax-coding-plan.js +3 -17
  108. package/dist/providers/minimax-coding-plan.js.map +1 -1
  109. package/dist/providers/nanogpt.d.ts.map +1 -1
  110. package/dist/providers/nanogpt.js +23 -42
  111. package/dist/providers/nanogpt.js.map +1 -1
  112. package/dist/providers/openai.d.ts.map +1 -1
  113. package/dist/providers/openai.js +6 -23
  114. package/dist/providers/openai.js.map +1 -1
  115. package/dist/providers/qwen-code.d.ts.map +1 -1
  116. package/dist/providers/qwen-code.js +7 -23
  117. package/dist/providers/qwen-code.js.map +1 -1
  118. package/dist/providers/registry.d.ts.map +1 -1
  119. package/dist/providers/registry.js +2 -0
  120. package/dist/providers/registry.js.map +1 -1
  121. package/dist/providers/result-helpers.d.ts +2 -2
  122. package/dist/providers/result-helpers.d.ts.map +1 -1
  123. package/dist/providers/result-helpers.js +7 -2
  124. package/dist/providers/result-helpers.js.map +1 -1
  125. package/dist/providers/synthetic.d.ts.map +1 -1
  126. package/dist/providers/synthetic.js +5 -14
  127. package/dist/providers/synthetic.js.map +1 -1
  128. package/dist/providers/zai.d.ts.map +1 -1
  129. package/dist/providers/zai.js +6 -28
  130. package/dist/providers/zai.js.map +1 -1
  131. package/dist/tui.d.ts.map +1 -1
  132. package/dist/tui.tsx +1 -15
  133. package/package.json +2 -1
package/dist/plugin.js CHANGED
@@ -6,8 +6,8 @@
6
6
  * Supports GitHub Copilot and Google (via opencode-antigravity-auth).
7
7
  */
8
8
  import { DEFAULT_CONFIG } from "./lib/types.js";
9
- import { loadConfig, createLoadConfigMeta } from "./lib/config.js";
10
- import { getOrFetchWithCacheControl } from "./lib/cache.js";
9
+ import { createLoadConfigMeta } from "./lib/config.js";
10
+ import { clearCache, getOrFetchWithCacheControl } from "./lib/cache.js";
11
11
  import { formatQuotaRows } from "./lib/format.js";
12
12
  import { formatQuotaCommand } from "./lib/quota-command-format.js";
13
13
  import { getProviders } from "./providers/registry.js";
@@ -26,7 +26,10 @@ import { parseOptionalJsonArgs, parseQuotaBetweenArgs, startOfLocalDayMs, startO
26
26
  import { handled } from "./lib/command-handled.js";
27
27
  import { renderCommandHeading } from "./lib/format-utils.js";
28
28
  import { sanitizeDisplayText } from "./lib/display-sanitize.js";
29
+ import { ALL_WINDOWS_FORMAT_STYLE, SINGLE_WINDOW_PER_PROVIDER_FORMAT_STYLE, resolveQuotaFormatStyle, } from "./lib/quota-format-style.js";
29
30
  import { collectQuotaRenderData, collectQuotaStatusLiveProbes, matchesQuotaProviderCurrentSelection, resolveQuotaRenderSelection, } from "./lib/quota-render-data.js";
31
+ import { createQuotaProviderRuntimeContext, createQuotaRuntimeRequestContext, resolveQuotaRuntimeContext, } from "./lib/quota-runtime-context.js";
32
+ const DEFERRED_QUOTA_REFRESH_DELAYS_MS = [3_000, 15_000, 60_000, 300_000];
30
33
  /** All token report command specifications */
31
34
  const TOKEN_REPORT_COMMANDS = [
32
35
  {
@@ -116,7 +119,6 @@ function isTokenReportCommand(cmd) {
116
119
  // =============================================================================
117
120
  // Plugin Implementation
118
121
  // =============================================================================
119
- const LIVE_LOCAL_USAGE_PROVIDER_IDS = new Set(["qwen-code", "alibaba-coding-plan", "cursor"]);
120
122
  /**
121
123
  * Main plugin export
122
124
  */
@@ -158,50 +160,64 @@ export const QuotaToastPlugin = async ({ client }) => {
158
160
  let configLoaded = false;
159
161
  let configInFlight = null;
160
162
  let configMeta = createLoadConfigMeta();
163
+ let runtimeProviders = getProviders();
161
164
  // Track last session token error for /quota_status diagnostics
162
165
  let lastSessionTokenError;
163
- const providerFetchCache = new Map();
164
- function getQuotaCommandCache() {
165
- const existing = globalThis.__opencodeQuotaCommandCache;
166
- if (existing instanceof Map) {
167
- return existing;
168
- }
169
- const quotaCache = new Map();
170
- globalThis.__opencodeQuotaCommandCache = quotaCache;
171
- return quotaCache;
166
+ const deferredQuotaRefreshes = new Map();
167
+ function getDeferredQuotaRefreshDelayMs(attempts) {
168
+ const index = Math.min(Math.max(0, attempts), DEFERRED_QUOTA_REFRESH_DELAYS_MS.length - 1);
169
+ return DEFERRED_QUOTA_REFRESH_DELAYS_MS[index];
172
170
  }
173
- function clearQuotaCommandCache() {
174
- getQuotaCommandCache().clear();
171
+ function clearDeferredQuotaRefresh(sessionID) {
172
+ const state = deferredQuotaRefreshes.get(sessionID);
173
+ if (state?.timer) {
174
+ clearTimeout(state.timer);
175
+ }
176
+ deferredQuotaRefreshes.delete(sessionID);
175
177
  }
176
- function buildQuotaCommandCacheKey(params) {
177
- const enabledProviders = config.enabledProviders === "auto" ? "auto" : config.enabledProviders.join(",");
178
- const googleModels = config.googleModels.join(",");
179
- const currentModel = config.onlyCurrentModel && params.sessionID ? (params.sessionMeta?.modelID ?? "") : "";
180
- const currentProviderID = config.onlyCurrentModel && params.sessionID ? (params.sessionMeta?.providerID ?? "") : "";
181
- return [
182
- `sessionID=${params.sessionID ?? ""}`,
183
- `showSessionTokens=${config.showSessionTokens ? "yes" : "no"}`,
184
- `onlyCurrentModel=${config.onlyCurrentModel ? "yes" : "no"}`,
185
- `enabledProviders=${enabledProviders}`,
186
- `anthropicBinaryPath=${config.anthropicBinaryPath}`,
187
- `googleModels=${googleModels}`,
188
- `alibabaTier=${config.alibabaCodingPlanTier}`,
189
- `cursorPlan=${config.cursorPlan}`,
190
- `cursorIncludedApiUsd=${config.cursorIncludedApiUsd ?? ""}`,
191
- `cursorBillingCycleStartDay=${config.cursorBillingCycleStartDay ?? ""}`,
192
- `currentModel=${currentModel}`,
193
- `currentProviderID=${currentProviderID}`,
194
- ].join("|");
178
+ function clearDeferredQuotaRefreshTimer(state) {
179
+ if (!state.timer)
180
+ return;
181
+ clearTimeout(state.timer);
182
+ state.timer = null;
195
183
  }
196
- function pruneQuotaCommandCache(ttlMs, nowMs) {
197
- const quotaCache = getQuotaCommandCache();
198
- for (const [cacheKey, entry] of quotaCache.entries()) {
199
- if (entry.inFlight)
200
- continue;
201
- if (entry.timestamp <= 0 || ttlMs <= 0 || nowMs - entry.timestamp >= ttlMs) {
202
- quotaCache.delete(cacheKey);
184
+ function scheduleDeferredQuotaRefresh(params) {
185
+ let state = deferredQuotaRefreshes.get(params.sessionID);
186
+ if (!state) {
187
+ state = {
188
+ sessionID: params.sessionID,
189
+ attempts: 0,
190
+ reason: params.reason,
191
+ queuedAtMs: Date.now(),
192
+ timer: null,
193
+ inFlight: false,
194
+ };
195
+ deferredQuotaRefreshes.set(params.sessionID, state);
196
+ }
197
+ else {
198
+ if (params.incrementAttempts) {
199
+ state.attempts += 1;
203
200
  }
201
+ state.reason = params.reason;
202
+ clearDeferredQuotaRefreshTimer(state);
204
203
  }
204
+ const delayMs = getDeferredQuotaRefreshDelayMs(state.attempts);
205
+ state.timer = setTimeout(() => {
206
+ void runDeferredQuotaRefresh(params.sessionID);
207
+ }, delayMs);
208
+ state.timer.unref?.();
209
+ void log("Deferred quota refresh scheduled", {
210
+ sessionID: params.sessionID,
211
+ reason: params.reason,
212
+ attempts: state.attempts,
213
+ delayMs,
214
+ });
215
+ }
216
+ async function runDeferredQuotaRefresh(sessionID) {
217
+ const state = deferredQuotaRefreshes.get(sessionID);
218
+ if (!state || state.inFlight)
219
+ return;
220
+ await showQuotaToast(sessionID, "deferred.retry", { deferredRetry: true });
205
221
  }
206
222
  function asRecord(value) {
207
223
  return value && typeof value === "object" ? value : null;
@@ -267,13 +283,28 @@ export const QuotaToastPlugin = async ({ client }) => {
267
283
  }
268
284
  return false;
269
285
  }
270
- async function shouldBypassQuotaCommandCache(sessionID, sessionMeta) {
271
- if (config.debug || !sessionID)
272
- return config.debug;
273
- return await shouldBypassToastCacheForLiveLocalUsage({
274
- trigger: "question",
275
- sessionID,
276
- sessionMeta,
286
+ function getPluginRuntimeRootHints() {
287
+ const cwd = process.cwd();
288
+ return {
289
+ workspaceRoot: cwd,
290
+ configRoot: cwd,
291
+ fallbackDirectory: cwd,
292
+ };
293
+ }
294
+ async function resolvePluginRuntimeContext(params = {}) {
295
+ if (!configLoaded) {
296
+ await refreshConfig();
297
+ }
298
+ return resolveQuotaRuntimeContext({
299
+ client: typedClient,
300
+ roots: getPluginRuntimeRootHints(),
301
+ config,
302
+ configMeta,
303
+ providers: runtimeProviders,
304
+ sessionID: params.sessionID,
305
+ sessionMeta: params.sessionMeta,
306
+ resolveSessionMeta: (sessionID) => getSessionModelMeta(sessionID),
307
+ includeSessionMeta: params.includeSessionMeta,
277
308
  });
278
309
  }
279
310
  async function refreshConfig() {
@@ -281,8 +312,13 @@ export const QuotaToastPlugin = async ({ client }) => {
281
312
  return configInFlight;
282
313
  configInFlight = (async () => {
283
314
  try {
284
- configMeta = createLoadConfigMeta();
285
- config = await loadConfig(typedClient, configMeta);
315
+ const runtime = await resolveQuotaRuntimeContext({
316
+ client: typedClient,
317
+ roots: getPluginRuntimeRootHints(),
318
+ });
319
+ configMeta = runtime.configMeta;
320
+ config = runtime.config;
321
+ runtimeProviders = runtime.providers;
286
322
  setPricingSnapshotAutoRefresh(config.pricingSnapshot.autoRefresh);
287
323
  setPricingSnapshotSelection(config.pricingSnapshot.source);
288
324
  configLoaded = true;
@@ -291,6 +327,8 @@ export const QuotaToastPlugin = async ({ client }) => {
291
327
  catch {
292
328
  // Leave configLoaded=false so we can retry on next trigger.
293
329
  config = DEFAULT_CONFIG;
330
+ configMeta = createLoadConfigMeta();
331
+ runtimeProviders = getProviders();
294
332
  setPricingSnapshotAutoRefresh(DEFAULT_CONFIG.pricingSnapshot.autoRefresh);
295
333
  setPricingSnapshotSelection(DEFAULT_CONFIG.pricingSnapshot.source);
296
334
  }
@@ -446,12 +484,11 @@ export const QuotaToastPlugin = async ({ client }) => {
446
484
  }
447
485
  return "current session";
448
486
  }
449
- async function buildQuotaCommandUnavailableMessage(params = {}) {
487
+ async function buildQuotaCommandUnavailableMessage(runtime) {
450
488
  const selection = await resolveQuotaRenderSelection({
451
- client: typedClient,
452
- config,
453
- request: params,
454
- formatStyle: "grouped",
489
+ client: runtime.client,
490
+ config: runtime.config,
491
+ request: createQuotaRuntimeRequestContext(runtime),
455
492
  });
456
493
  if (!selection) {
457
494
  return "Quota unavailable\n\nNo enabled quota providers are configured.\n\nRun /quota_status for diagnostics.";
@@ -487,171 +524,374 @@ export const QuotaToastPlugin = async ({ client }) => {
487
524
  "This may be a temporary API error.\n\n" +
488
525
  "Run /quota_status for diagnostics.");
489
526
  }
490
- async function fetchQuotaMessage(trigger, sessionID) {
527
+ function buildToastCacheKey(params) {
528
+ const formatStyle = resolveQuotaFormatStyle(config.formatStyle);
529
+ const enabledProviders = config.enabledProviders === "auto" ? "auto" : config.enabledProviders.join(",");
530
+ const googleModels = config.googleModels.join(",");
531
+ const currentModel = config.onlyCurrentModel && params.sessionID ? (params.sessionMeta?.modelID ?? "") : "";
532
+ const currentProviderID = config.onlyCurrentModel && params.sessionID ? (params.sessionMeta?.providerID ?? "") : "";
533
+ return [
534
+ `sessionID=${params.sessionID}`,
535
+ `enabledProviders=${enabledProviders}`,
536
+ `formatStyle=${formatStyle}`,
537
+ `percentDisplayMode=${config.percentDisplayMode}`,
538
+ `layout=${JSON.stringify(config.layout)}`,
539
+ `showSessionTokens=${config.showSessionTokens ? "yes" : "no"}`,
540
+ `onlyCurrentModel=${config.onlyCurrentModel ? "yes" : "no"}`,
541
+ `currentModel=${currentModel}`,
542
+ `currentProviderID=${currentProviderID}`,
543
+ `anthropicBinaryPath=${config.anthropicBinaryPath}`,
544
+ `googleModels=${googleModels}`,
545
+ `alibabaTier=${config.alibabaCodingPlanTier}`,
546
+ `cursorPlan=${config.cursorPlan}`,
547
+ `cursorIncludedApiUsd=${config.cursorIncludedApiUsd ?? ""}`,
548
+ `cursorBillingCycleStartDay=${config.cursorBillingCycleStartDay ?? ""}`,
549
+ ].join("|");
550
+ }
551
+ function clearToastCacheForSession(params) {
552
+ clearCache(buildToastCacheKey(params));
553
+ }
554
+ function isProviderFetchFailureOnly(errors) {
555
+ return (errors.length > 0 && errors.every((error) => error.message === "Failed to read quota data"));
556
+ }
557
+ async function fetchQuotaMessageResult(params) {
491
558
  // Ensure we have loaded config at least once. If load fails, we keep trying
492
- // on subsequent triggers.
559
+ // on subsequent triggers and queue a deferred retry for toast paths.
493
560
  if (!configLoaded) {
494
561
  await refreshConfig();
495
562
  }
563
+ if (!configLoaded) {
564
+ return {
565
+ message: config.debug
566
+ ? formatDebugInfo({
567
+ trigger: params.trigger,
568
+ reason: "config load failed",
569
+ enabledProviders: config.enabledProviders,
570
+ })
571
+ : null,
572
+ cacheRenderedMessage: false,
573
+ retryable: true,
574
+ retryReason: "config_load_failed",
575
+ hasQuotaRows: false,
576
+ };
577
+ }
496
578
  if (!config.enabled) {
497
- return config.debug
498
- ? formatDebugInfo({ trigger, reason: "disabled", enabledProviders: [] })
499
- : null;
579
+ return {
580
+ message: config.debug
581
+ ? formatDebugInfo({ trigger: params.trigger, reason: "disabled", enabledProviders: [] })
582
+ : null,
583
+ cacheRenderedMessage: false,
584
+ retryable: false,
585
+ hasQuotaRows: false,
586
+ };
500
587
  }
501
588
  if (config.enabledProviders !== "auto" && config.enabledProviders.length === 0) {
502
- return config.debug
503
- ? formatDebugInfo({ trigger, reason: "enabledProviders empty", enabledProviders: [] })
504
- : null;
589
+ return {
590
+ message: config.debug
591
+ ? formatDebugInfo({
592
+ trigger: params.trigger,
593
+ reason: "enabledProviders empty",
594
+ enabledProviders: [],
595
+ })
596
+ : null,
597
+ cacheRenderedMessage: false,
598
+ retryable: false,
599
+ hasQuotaRows: false,
600
+ };
505
601
  }
506
- const quotaRequestContext = {
507
- sessionID,
508
- sessionMeta: config.onlyCurrentModel && sessionID ? await getSessionModelMeta(sessionID) : undefined,
509
- };
602
+ const runtime = await resolvePluginRuntimeContext({
603
+ sessionID: params.sessionID,
604
+ sessionMeta: params.sessionMeta,
605
+ includeSessionMeta: (config) => config.onlyCurrentModel,
606
+ });
607
+ const runtimeConfig = runtime.config;
608
+ const quotaRequestContext = createQuotaRuntimeRequestContext(runtime);
510
609
  const quotaResult = await collectQuotaRenderData({
511
- client: typedClient,
512
- config,
610
+ client: runtime.client,
611
+ config: runtimeConfig,
513
612
  request: quotaRequestContext,
514
- providerFetchCache,
515
613
  surfaceExplicitProviderIssues: true,
516
- formatStyle: config.formatStyle,
614
+ formatStyle: resolveQuotaFormatStyle(runtimeConfig.formatStyle),
615
+ bypassProviderCache: params.bypassProviderCache,
517
616
  });
518
617
  const { selection, availability, active, attemptedAny, hasExplicitProviderIssues, data } = quotaResult;
519
- if (config.showSessionTokens && sessionID) {
618
+ if (runtimeConfig.showSessionTokens && params.sessionID) {
520
619
  lastSessionTokenError = quotaResult.sessionTokenError;
521
620
  }
522
621
  const currentModel = selection?.currentModel;
523
622
  const errors = data?.errors ?? [];
623
+ const hasProviderQuotaRows = Boolean(data?.entries.length);
624
+ const hasQuotaRows = Boolean(hasProviderQuotaRows || data?.sessionTokens);
625
+ const providerFetchFailureOnly = attemptedAny && isProviderFetchFailureOnly(errors);
626
+ const retryableAvailabilityFailure = active.length === 0 && availability.some((item) => !item.ok && item.error === true);
524
627
  if (active.length === 0 && !(hasExplicitProviderIssues && errors.length > 0)) {
525
- return config.debug
628
+ const message = runtimeConfig.debug
526
629
  ? formatDebugInfo({
527
- trigger,
630
+ trigger: params.trigger,
528
631
  reason: "no enabled providers available",
529
632
  currentModel,
530
- enabledProviders: config.enabledProviders,
633
+ enabledProviders: runtimeConfig.enabledProviders,
531
634
  availability: availability.map((item) => ({
532
635
  id: item.provider.id,
533
636
  ok: item.ok,
534
637
  })),
535
638
  })
536
639
  : null;
640
+ const retryableNoProviders = selection?.isAutoMode === true || retryableAvailabilityFailure;
641
+ return {
642
+ message,
643
+ cacheRenderedMessage: false,
644
+ retryable: retryableNoProviders,
645
+ retryReason: retryableNoProviders ? "no_available_providers" : undefined,
646
+ hasQuotaRows: false,
647
+ };
537
648
  }
538
- if (data?.entries.length) {
649
+ if (hasQuotaRows) {
539
650
  const formatted = formatQuotaRows({
540
651
  version: "1.0.0",
541
- layout: config.layout,
542
- entries: data.entries,
543
- errors: data.errors,
544
- style: config.formatStyle,
545
- percentDisplayMode: config.percentDisplayMode,
546
- sessionTokens: data.sessionTokens,
652
+ layout: runtimeConfig.layout,
653
+ entries: data?.entries ?? [],
654
+ errors: data?.errors ?? [],
655
+ style: resolveQuotaFormatStyle(runtimeConfig.formatStyle),
656
+ percentDisplayMode: runtimeConfig.percentDisplayMode,
657
+ sessionTokens: data?.sessionTokens,
547
658
  });
548
- if (!config.debug)
549
- return formatted;
550
- const debugFooter = `\n\n[debug] src=${configMeta.source} providers=${config.enabledProviders === "auto" ? "(auto)" : config.enabledProviders.join(",") || "(none)"} avail=${availability
659
+ const retryableMaskedProviderFailure = !hasProviderQuotaRows && providerFetchFailureOnly;
660
+ if (!runtimeConfig.debug) {
661
+ return {
662
+ message: formatted,
663
+ cacheRenderedMessage: true,
664
+ retryable: retryableMaskedProviderFailure,
665
+ retryReason: retryableMaskedProviderFailure ? "provider_fetch_failed" : undefined,
666
+ hasQuotaRows: true,
667
+ };
668
+ }
669
+ const debugFooter = `\n\n[debug] src=${configMeta.source} providers=${runtimeConfig.enabledProviders === "auto" ? "(auto)" : runtimeConfig.enabledProviders.join(",") || "(none)"} avail=${availability
551
670
  .map((item) => `${item.provider.id}:${item.ok ? "ok" : "no"}`)
552
671
  .join(" ")}`;
553
- return formatted + debugFooter;
672
+ return {
673
+ message: formatted + debugFooter,
674
+ cacheRenderedMessage: false,
675
+ retryable: retryableMaskedProviderFailure,
676
+ retryReason: retryableMaskedProviderFailure ? "provider_fetch_failed" : undefined,
677
+ hasQuotaRows: true,
678
+ };
554
679
  }
555
680
  // Show errors even without entries when:
556
681
  // 1. showOnBothFail is enabled and at least one provider attempted (existing behavior)
557
682
  // 2. OR we're in explicit mode and have "Not configured"/"Unavailable" errors (new behavior)
558
- if ((config.showOnBothFail && attemptedAny && errors.length > 0) || hasExplicitProviderIssues) {
683
+ if ((runtimeConfig.showOnBothFail && attemptedAny && errors.length > 0) ||
684
+ hasExplicitProviderIssues) {
559
685
  const errorLines = errors.map((error) => `${error.label}: ${error.message}`).join("\n");
560
- if (!config.debug)
561
- return errorLines || "Quota unavailable";
562
- return ((errorLines || "Quota unavailable") +
563
- "\n\n" +
564
- formatDebugInfo({
565
- trigger,
566
- reason: hasExplicitProviderIssues
567
- ? "providers missing/unavailable"
568
- : "all providers failed",
686
+ const retryableFetchFailure = !hasExplicitProviderIssues && providerFetchFailureOnly;
687
+ const retryableFailure = retryableFetchFailure || retryableAvailabilityFailure;
688
+ const retryReason = retryableFetchFailure
689
+ ? "provider_fetch_failed"
690
+ : retryableAvailabilityFailure
691
+ ? "no_available_providers"
692
+ : undefined;
693
+ const message = !runtimeConfig.debug
694
+ ? errorLines || "Quota unavailable"
695
+ : (errorLines || "Quota unavailable") +
696
+ "\n\n" +
697
+ formatDebugInfo({
698
+ trigger: params.trigger,
699
+ reason: hasExplicitProviderIssues
700
+ ? "providers missing/unavailable"
701
+ : "all providers failed",
702
+ currentModel,
703
+ enabledProviders: runtimeConfig.enabledProviders,
704
+ availability: availability.map((item) => ({
705
+ id: item.provider.id,
706
+ ok: item.ok,
707
+ })),
708
+ });
709
+ return {
710
+ message,
711
+ cacheRenderedMessage: false,
712
+ retryable: retryableFailure,
713
+ retryReason,
714
+ hasQuotaRows: false,
715
+ };
716
+ }
717
+ const retryableNoData = providerFetchFailureOnly ||
718
+ (selection?.isAutoMode === true && active.length > 0 && errors.length === 0);
719
+ return {
720
+ message: runtimeConfig.debug
721
+ ? formatDebugInfo({
722
+ trigger: params.trigger,
723
+ reason: "no entries",
569
724
  currentModel,
570
- enabledProviders: config.enabledProviders,
725
+ enabledProviders: runtimeConfig.enabledProviders,
571
726
  availability: availability.map((item) => ({
572
727
  id: item.provider.id,
573
728
  ok: item.ok,
574
729
  })),
575
- }));
730
+ })
731
+ : null,
732
+ cacheRenderedMessage: false,
733
+ retryable: retryableNoData,
734
+ retryReason: providerFetchFailureOnly
735
+ ? "provider_fetch_failed"
736
+ : retryableNoData
737
+ ? "no_reportable_data"
738
+ : undefined,
739
+ hasQuotaRows: false,
740
+ };
741
+ }
742
+ async function fetchQuotaMessage(params) {
743
+ const result = await fetchQuotaMessageResult(params);
744
+ return result.message;
745
+ }
746
+ async function reconcileDeferredQuotaRefresh(params) {
747
+ const existing = deferredQuotaRefreshes.get(params.sessionID);
748
+ if (!params.result.retryable) {
749
+ if (existing) {
750
+ clearDeferredQuotaRefresh(params.sessionID);
751
+ await log("Deferred quota refresh cleared", {
752
+ sessionID: params.sessionID,
753
+ trigger: params.trigger,
754
+ reason: params.result.hasQuotaRows ? "quota_rows_available" : "not_retryable",
755
+ });
756
+ }
757
+ return;
576
758
  }
577
- return config.debug
578
- ? formatDebugInfo({
579
- trigger,
580
- reason: "no entries",
581
- currentModel,
582
- enabledProviders: config.enabledProviders,
583
- availability: availability.map((item) => ({
584
- id: item.provider.id,
585
- ok: item.ok,
586
- })),
587
- })
588
- : null;
759
+ if (!params.result.retryReason) {
760
+ return;
761
+ }
762
+ scheduleDeferredQuotaRefresh({
763
+ sessionID: params.sessionID,
764
+ reason: params.result.retryReason,
765
+ incrementAttempts: params.consumedDeferredRetry,
766
+ });
589
767
  }
590
768
  /**
591
769
  * Show quota toast for a session
592
770
  */
593
- async function showQuotaToast(sessionID, trigger) {
771
+ async function showQuotaToast(sessionID, trigger, options = {}) {
594
772
  if (!configLoaded) {
595
773
  await refreshConfig();
596
774
  }
597
- // Check if subagent session
598
- if (await isSubagentSession(sessionID)) {
599
- await log("Skipping toast for subagent session", { sessionID, trigger });
600
- return;
601
- }
602
- // Get or fetch quota (with caching/throttling)
603
- // If debug is enabled, bypass caching so the toast reflects current state.
604
- function shouldCacheToastMessage(msg) {
605
- // Cache when we have any quota row (which always includes a "NN%" token).
606
- // Do not cache when output is only error rows (rendered as "label: message").
607
- const lines = msg.split("\n");
608
- return lines.some((l) => /\b\d{1,3}%\b/.test(l) && !/:\s/.test(l));
609
- }
610
- const bypassMessageCache = config.debug
611
- ? true
612
- : await shouldBypassToastCacheForLiveLocalUsage({ trigger, sessionID });
613
- const message = bypassMessageCache
614
- ? await fetchQuotaMessage(trigger, sessionID)
615
- : await getOrFetchWithCacheControl(async () => {
616
- const msg = await fetchQuotaMessage(trigger, sessionID);
617
- const cache = msg ? shouldCacheToastMessage(msg) : true;
618
- return { message: msg, cache };
619
- }, config.minIntervalMs);
620
- if (!message) {
621
- await log("No quota message to display", { trigger });
622
- return;
623
- }
624
- if (!config.enableToast) {
625
- await log("Toast disabled (enableToast=false)", { trigger });
626
- return;
775
+ const pendingDeferred = deferredQuotaRefreshes.get(sessionID);
776
+ const consumedDeferredRetry = options.deferredRetry === true || Boolean(pendingDeferred);
777
+ if (pendingDeferred) {
778
+ if (pendingDeferred.inFlight && !options.deferredRetry) {
779
+ await log("Skipping duplicate deferred quota refresh", { sessionID, trigger });
780
+ return;
781
+ }
782
+ pendingDeferred.inFlight = true;
783
+ clearDeferredQuotaRefreshTimer(pendingDeferred);
627
784
  }
628
- // Show toast
629
785
  try {
630
- await typedClient.tui.showToast({
631
- body: {
632
- message: sanitizeDisplayText(message),
633
- variant: "info",
634
- duration: config.toastDurationMs,
635
- },
786
+ // Check if session is a subagent session
787
+ if (await isSubagentSession(sessionID)) {
788
+ if (consumedDeferredRetry) {
789
+ clearDeferredQuotaRefresh(sessionID);
790
+ }
791
+ await log("Skipping toast for subagent session", { sessionID, trigger });
792
+ return;
793
+ }
794
+ // Get or fetch quota (with caching/throttling)
795
+ // If debug is enabled, bypass caching so the toast reflects current state.
796
+ function shouldCacheToastMessage(msg) {
797
+ // Cache when we have any quota row (which always includes a "NN%" token).
798
+ // Do not cache when output is only error rows (rendered as "label: message").
799
+ const lines = msg.split("\n");
800
+ return lines.some((l) => /\b\d+%\b/.test(l) && !/:\s/.test(l));
801
+ }
802
+ const sessionMeta = await getSessionModelMeta(sessionID);
803
+ const bypassForLiveLocalUsage = await shouldBypassToastCacheForLiveLocalUsage({
804
+ trigger,
805
+ sessionID,
806
+ sessionMeta,
636
807
  });
637
- await log("Displayed quota toast", { message, trigger });
638
- }
639
- catch (err) {
640
- await log("Failed to show toast", {
641
- error: err instanceof Error ? err.message : String(err),
808
+ const bypassMessageCache = config.debug || consumedDeferredRetry || bypassForLiveLocalUsage;
809
+ const bypassProviderCache = consumedDeferredRetry || bypassForLiveLocalUsage;
810
+ const toastCacheKey = buildToastCacheKey({ sessionID, sessionMeta });
811
+ let fetchResult;
812
+ const fetchForToast = () => fetchQuotaMessageResult({
813
+ trigger,
814
+ sessionID,
815
+ sessionMeta,
816
+ bypassProviderCache,
642
817
  });
818
+ const message = bypassMessageCache
819
+ ? await (async () => {
820
+ fetchResult = await fetchForToast();
821
+ return fetchResult.message;
822
+ })()
823
+ : await (async () => {
824
+ const fetched = {};
825
+ const cachedMessage = await getOrFetchWithCacheControl(toastCacheKey, async () => {
826
+ const result = await fetchForToast();
827
+ fetched.result = result;
828
+ const cache = result.message
829
+ ? result.cacheRenderedMessage && shouldCacheToastMessage(result.message)
830
+ : result.cacheRenderedMessage;
831
+ return { message: result.message, cache };
832
+ }, config.minIntervalMs);
833
+ fetchResult = fetched.result;
834
+ return cachedMessage;
835
+ })();
836
+ if (fetchResult) {
837
+ await reconcileDeferredQuotaRefresh({
838
+ sessionID,
839
+ result: fetchResult,
840
+ consumedDeferredRetry,
841
+ trigger,
842
+ });
843
+ }
844
+ if (options.deferredRetry && fetchResult && !fetchResult.hasQuotaRows) {
845
+ await log("Deferred quota refresh did not produce reportable data", {
846
+ sessionID,
847
+ trigger,
848
+ retryable: fetchResult.retryable,
849
+ retryReason: fetchResult.retryReason,
850
+ });
851
+ return;
852
+ }
853
+ if (!message) {
854
+ await log("No quota message to display", { trigger });
855
+ return;
856
+ }
857
+ if (!config.enableToast) {
858
+ await log("Toast disabled (enableToast=false)", { trigger });
859
+ return;
860
+ }
861
+ // Show toast
862
+ try {
863
+ await typedClient.tui.showToast({
864
+ body: {
865
+ message: sanitizeDisplayText(message),
866
+ variant: "info",
867
+ duration: config.toastDurationMs,
868
+ },
869
+ });
870
+ await log("Displayed quota toast", { message, trigger });
871
+ }
872
+ catch (err) {
873
+ await log("Failed to show toast", {
874
+ error: err instanceof Error ? err.message : String(err),
875
+ });
876
+ }
877
+ }
878
+ finally {
879
+ const state = deferredQuotaRefreshes.get(sessionID);
880
+ if (state) {
881
+ state.inFlight = false;
882
+ }
643
883
  }
644
884
  }
645
- async function fetchQuotaCommandData(trigger, params = {}) {
885
+ async function fetchQuotaCommandData(runtime) {
886
+ const request = createQuotaRuntimeRequestContext(runtime);
646
887
  const quotaResult = await collectQuotaRenderData({
647
- client: typedClient,
648
- config,
649
- request: params,
650
- providerFetchCache,
888
+ client: runtime.client,
889
+ config: runtime.config,
890
+ request,
651
891
  surfaceExplicitProviderIssues: false,
652
- formatStyle: "grouped",
892
+ formatStyle: ALL_WINDOWS_FORMAT_STYLE,
653
893
  });
654
- if (config.showSessionTokens && params.sessionID) {
894
+ if (runtime.config.showSessionTokens && request.sessionID) {
655
895
  lastSessionTokenError = quotaResult.sessionTokenError;
656
896
  }
657
897
  return quotaResult.data;
@@ -676,11 +916,15 @@ export const QuotaToastPlugin = async ({ client }) => {
676
916
  });
677
917
  }
678
918
  async function buildStatusReport(params) {
679
- await refreshConfig();
680
- if (!config.enabled)
919
+ const runtime = await resolvePluginRuntimeContext({
920
+ sessionID: params.sessionID,
921
+ includeSessionMeta: true,
922
+ });
923
+ const runtimeConfig = runtime.config;
924
+ if (!runtimeConfig.enabled)
681
925
  return null;
682
926
  await kickPricingRefresh({ reason: "status", maxWaitMs: 750 });
683
- const currentSession = await getSessionModelMeta(params.sessionID);
927
+ const currentSession = runtime.session.sessionMeta ?? {};
684
928
  const currentModel = currentSession.modelID;
685
929
  const currentProviderID = currentSession.providerID;
686
930
  const sessionModelLookup = !params.sessionID
@@ -688,24 +932,13 @@ export const QuotaToastPlugin = async ({ client }) => {
688
932
  : currentModel
689
933
  ? "ok"
690
934
  : "not_found";
691
- const isAutoMode = config.enabledProviders === "auto";
692
- const providers = getProviders();
935
+ const isAutoMode = runtimeConfig.enabledProviders === "auto";
936
+ const providers = runtime.providers;
937
+ const providerContext = createQuotaProviderRuntimeContext(runtime);
693
938
  const availability = await Promise.all(providers.map(async (p) => {
694
939
  let ok = false;
695
940
  try {
696
- ok = await p.isAvailable({
697
- client: typedClient,
698
- config: {
699
- googleModels: config.googleModels,
700
- anthropicBinaryPath: config.anthropicBinaryPath,
701
- alibabaCodingPlanTier: config.alibabaCodingPlanTier,
702
- cursorPlan: config.cursorPlan,
703
- cursorIncludedApiUsd: config.cursorIncludedApiUsd,
704
- cursorBillingCycleStartDay: config.cursorBillingCycleStartDay,
705
- currentModel,
706
- currentProviderID,
707
- },
708
- });
941
+ ok = await p.isAvailable(providerContext);
709
942
  }
710
943
  catch {
711
944
  ok = false;
@@ -713,7 +946,7 @@ export const QuotaToastPlugin = async ({ client }) => {
713
946
  return {
714
947
  id: p.id,
715
948
  // In auto mode, a provider is effectively "enabled" if it's available.
716
- enabled: isAutoMode ? ok : config.enabledProviders.includes(p.id),
949
+ enabled: isAutoMode ? ok : runtimeConfig.enabledProviders.includes(p.id),
717
950
  available: ok,
718
951
  matchesCurrentModel: currentModel || isCursorProviderId(currentProviderID)
719
952
  ? matchesQuotaProviderCurrentSelection({
@@ -736,15 +969,11 @@ export const QuotaToastPlugin = async ({ client }) => {
736
969
  if (liveProbeProviders.length > 0) {
737
970
  try {
738
971
  providerLiveProbes = await collectQuotaStatusLiveProbes({
739
- client: typedClient,
740
- config,
741
- request: {
742
- sessionID: params.sessionID,
743
- sessionMeta: currentSession,
744
- },
745
- formatStyle: "classic",
972
+ client: runtime.client,
973
+ config: runtimeConfig,
974
+ request: createQuotaRuntimeRequestContext(runtime),
975
+ formatStyle: SINGLE_WINDOW_PER_PROVIDER_FORMAT_STYLE,
746
976
  providers: liveProbeProviders,
747
- providerFetchCache,
748
977
  });
749
978
  }
750
979
  catch (error) {
@@ -764,20 +993,23 @@ export const QuotaToastPlugin = async ({ client }) => {
764
993
  const refresh = params.refreshGoogleTokens
765
994
  ? await refreshGoogleTokensForAllAccounts({ skewMs: params.skewMs, force: params.force })
766
995
  : null;
767
- const tuiDiagnostics = await inspectTuiConfig();
996
+ const tuiDiagnostics = await inspectTuiConfig({ roots: runtime.roots });
768
997
  return await buildQuotaStatusReport({
769
998
  tuiDiagnostics,
770
- configSource: configMeta.source,
771
- configPaths: configMeta.paths,
772
- networkSettingSources: configMeta.networkSettingSources,
773
- enabledProviders: config.enabledProviders,
774
- anthropicBinaryPath: config.anthropicBinaryPath,
775
- alibabaCodingPlanTier: config.alibabaCodingPlanTier,
776
- cursorPlan: config.cursorPlan,
777
- cursorIncludedApiUsd: config.cursorIncludedApiUsd,
778
- cursorBillingCycleStartDay: config.cursorBillingCycleStartDay,
779
- pricingSnapshotSource: config.pricingSnapshot.source,
780
- onlyCurrentModel: config.onlyCurrentModel,
999
+ configSource: runtime.configMeta.source,
1000
+ configPaths: runtime.configMeta.paths,
1001
+ globalConfigPaths: runtime.configMeta.globalConfigPaths,
1002
+ workspaceConfigPaths: runtime.configMeta.workspaceConfigPaths,
1003
+ settingSources: runtime.configMeta.settingSources,
1004
+ configIssues: runtime.configMeta.configIssues,
1005
+ enabledProviders: runtimeConfig.enabledProviders,
1006
+ anthropicBinaryPath: runtimeConfig.anthropicBinaryPath,
1007
+ alibabaCodingPlanTier: runtimeConfig.alibabaCodingPlanTier,
1008
+ cursorPlan: runtimeConfig.cursorPlan,
1009
+ cursorIncludedApiUsd: runtimeConfig.cursorIncludedApiUsd,
1010
+ cursorBillingCycleStartDay: runtimeConfig.cursorBillingCycleStartDay,
1011
+ pricingSnapshotSource: runtimeConfig.pricingSnapshot.source,
1012
+ onlyCurrentModel: runtimeConfig.onlyCurrentModel,
781
1013
  currentModel,
782
1014
  sessionModelLookup,
783
1015
  providerAvailability: availability,
@@ -791,6 +1023,7 @@ export const QuotaToastPlugin = async ({ client }) => {
791
1023
  }
792
1024
  : { attempted: false },
793
1025
  sessionTokenError: lastSessionTokenError,
1026
+ geminiCliClient: typedClient,
794
1027
  generatedAtMs: params.generatedAtMs,
795
1028
  });
796
1029
  }
@@ -853,52 +1086,21 @@ export const QuotaToastPlugin = async ({ client }) => {
853
1086
  async function handleQuotaSlashCommand(input) {
854
1087
  const sessionID = input.sessionID;
855
1088
  const generatedAtMs = Date.now();
856
- const now = generatedAtMs;
857
- const quotaRequestContext = {
1089
+ const sessionMeta = sessionID ? await getSessionModelMeta(sessionID) : undefined;
1090
+ const runtime = await resolvePluginRuntimeContext({
858
1091
  sessionID,
859
- sessionMeta: sessionID ? await getSessionModelMeta(sessionID) : undefined,
860
- };
861
- const bypassCommandCache = await shouldBypassQuotaCommandCache(sessionID, quotaRequestContext.sessionMeta);
862
- const reportData = bypassCommandCache
863
- ? await fetchQuotaCommandData("command:/quota", quotaRequestContext)
864
- : await (async () => {
865
- const quotaCache = getQuotaCommandCache();
866
- pruneQuotaCommandCache(config.minIntervalMs, now);
867
- const cacheKey = buildQuotaCommandCacheKey(quotaRequestContext);
868
- const cachedEntry = quotaCache.get(cacheKey);
869
- if (cachedEntry?.timestamp && now - cachedEntry.timestamp < config.minIntervalMs) {
870
- return cachedEntry.data ?? null;
871
- }
872
- const cacheEntry = cachedEntry ?? { timestamp: 0 };
873
- if (!cachedEntry) {
874
- quotaCache.set(cacheKey, cacheEntry);
875
- }
876
- return await (cacheEntry.inFlight ??
877
- (cacheEntry.inFlight = (async () => {
878
- try {
879
- const freshData = await fetchQuotaCommandData("command:/quota", quotaRequestContext);
880
- if (freshData) {
881
- cacheEntry.data = freshData;
882
- cacheEntry.timestamp = Date.now();
883
- }
884
- return freshData;
885
- }
886
- finally {
887
- cacheEntry.inFlight = undefined;
888
- if (!cacheEntry.data && cacheEntry.timestamp <= 0) {
889
- quotaCache.delete(cacheKey);
890
- }
891
- }
892
- })()));
893
- })();
1092
+ sessionMeta,
1093
+ includeSessionMeta: (config) => config.onlyCurrentModel,
1094
+ });
1095
+ const reportData = await fetchQuotaCommandData(runtime);
894
1096
  if (!reportData) {
895
1097
  if (!configLoaded) {
896
1098
  return await injectCommandOutputAndHandle(sessionID, "Quota unavailable (config not loaded, try again)");
897
1099
  }
898
- if (!config.enabled) {
1100
+ if (!runtime.config.enabled) {
899
1101
  return await injectCommandOutputAndHandle(sessionID, "Quota disabled in config (enabled: false)");
900
1102
  }
901
- return await injectCommandOutputAndHandle(sessionID, await buildQuotaCommandUnavailableMessage(quotaRequestContext));
1103
+ return await injectCommandOutputAndHandle(sessionID, await buildQuotaCommandUnavailableMessage(runtime));
902
1104
  }
903
1105
  return await injectCommandOutputAndHandle(sessionID, formatQuotaCommand({
904
1106
  ...reportData,
@@ -1141,8 +1343,10 @@ export const QuotaToastPlugin = async ({ client }) => {
1141
1343
  if (!configLoaded) {
1142
1344
  await refreshConfig();
1143
1345
  }
1144
- if (!config.enabled)
1346
+ if (!config.enabled) {
1347
+ clearDeferredQuotaRefresh(sessionID);
1145
1348
  return;
1349
+ }
1146
1350
  if (event.type === "session.idle" && config.showOnIdle) {
1147
1351
  await showQuotaToast(sessionID, "session.idle");
1148
1352
  }
@@ -1157,8 +1361,10 @@ export const QuotaToastPlugin = async ({ client }) => {
1157
1361
  if (!configLoaded) {
1158
1362
  await refreshConfig();
1159
1363
  }
1160
- if (!config.enabled)
1364
+ if (!config.enabled) {
1365
+ clearDeferredQuotaRefresh(input.sessionID);
1161
1366
  return;
1367
+ }
1162
1368
  if (isSuccessfulQuestionExecution(output)) {
1163
1369
  const sessionMeta = await getSessionModelMeta(input.sessionID);
1164
1370
  const model = sessionMeta.modelID;
@@ -1167,7 +1373,7 @@ export const QuotaToastPlugin = async ({ client }) => {
1167
1373
  const plan = await resolveQwenLocalPlanCached();
1168
1374
  if (plan.state === "qwen_free") {
1169
1375
  await recordQwenCompletion();
1170
- clearQuotaCommandCache();
1376
+ clearToastCacheForSession({ sessionID: input.sessionID, sessionMeta });
1171
1377
  }
1172
1378
  }
1173
1379
  else if (isAlibabaModelId(model)) {
@@ -1177,11 +1383,11 @@ export const QuotaToastPlugin = async ({ client }) => {
1177
1383
  });
1178
1384
  if (plan.state === "configured") {
1179
1385
  await recordAlibabaCodingPlanCompletion();
1180
- clearQuotaCommandCache();
1386
+ clearToastCacheForSession({ sessionID: input.sessionID, sessionMeta });
1181
1387
  }
1182
1388
  }
1183
1389
  else if (isCursorProviderId(sessionMeta.providerID) || isCursorModelId(model)) {
1184
- clearQuotaCommandCache();
1390
+ clearToastCacheForSession({ sessionID: input.sessionID, sessionMeta });
1185
1391
  }
1186
1392
  }
1187
1393
  catch (err) {