dsh-agy-link 0.4.16 → 0.4.18

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.18 (2026-08-25)
4
+
5
+ - **Quota Fallback Never Clobbers Good Data (5h=100%/weekly-missing 根因).**
6
+ - **What happened**: `retrieveUserQuotaSummary` transiently failed (proxy blip) while `fetchAvailableModels` still answered; the per-model fallback then OVERWROTE the stored family entry with a single-window partial shape — weeklyFraction vanished and the 5h row received wrong-window numbers (observed 5h=100%, reset a week out, weekly `—`, while the live API actually reported 5h 84% / weekly 47%).
7
+ - **`mergeFallbackFamilyQuota`**: last-known-good complete family data now always wins over partial fallback; the fallback only fills families with no usable previous entry (first-ever refresh). Verified live: both endpoints answer correctly and a successful summary refresh fully restores the display.
8
+ - Added `scripts/diag-quota.mts` one-shot endpoint probe for future incidents.
9
+
10
+ ## 0.4.17 (2026-08-25)
11
+
12
+ - **Ghost-Cooldown & Quota-Display Fix (额度显示 0% 根因).**
13
+ - **What happened**: the UI showed 5h quota as 0% while `agy` reported 98% — the parsed quota data was CORRECT all along, but (a) any active local cooldown forced the 5h bar to render 0%, and (b) the loose rate-limit classifier kept creating ghost cooldowns: it scanned the ENTIRE stdout (model prose mentioning "rate limit"/"quota", hash fragments containing "429") and matched bare keywords, so an unrelated tool/permission error froze a healthy account out of rotation with a 15-minute+ cooldown (captured real reason: `rate limit reached: declaring permissions: cortex tool write_to_file … invalid tool call error`).
14
+ - **Hard vs soft classification**: new `looksLikeHardRateLimit` (RESOURCE_EXHAUSTED / code·status·HTTP 429 / too many requests / individual quota reached / quota exceeded·reached·exhausted / rate limit exceeded·reached·hit) is the ONLY pattern allowed to put an account into cooldown; soft signals (model overloaded / high traffic) still shape the error message but never cool accounts.
15
+ - **Scan scope narrowed**: error classification reads stderr + the result envelope's error field only — stdout (event JSON + model prose) no longer participates.
16
+ - **Honest quota bars**: a local cooldown no longer overwrites the server-reported fraction with 0%; it now appends a `· 本地冷却中` note next to the reset time instead.
17
+ - Regression tests pin the exact incident text (cortex tool permission error) as a non-rate-limit fixture.
18
+
3
19
  ## 0.4.16 (2026-08-24)
4
20
 
5
21
  - **External Re-Login Sync (换号自动/手动同步).**
package/dist/client.js CHANGED
@@ -963,8 +963,8 @@ body.dark,
963
963
  const info = acc.quotas[familyKey];
964
964
  const cd = acc.cooldowns[familyKey];
965
965
  const inCooldown = cd && cd.cooldownUntil > Date.now();
966
- let pct5h = typeof info?.remainingFraction === "number" && Number.isFinite(info.remainingFraction) ? Math.max(0, Math.min(100, Math.round(info.remainingFraction * 100))) : -1;
967
- if (inCooldown) pct5h = 0;
966
+ const pct5h = typeof info?.remainingFraction === "number" && Number.isFinite(info.remainingFraction) ? Math.max(0, Math.min(100, Math.round(info.remainingFraction * 100))) : -1;
967
+ const cdNote = inCooldown ? " · 本地冷却中" : "";
968
968
  const w5h = formatQuotaWindow(info?.resetTime);
969
969
  const pctWeekly = typeof info?.weeklyFraction === "number" && Number.isFinite(info.weeklyFraction) ? Math.max(0, Math.min(100, Math.round(info.weeklyFraction * 100))) : -1;
970
970
  const wWeekly = formatQuotaWindow(info?.weeklyResetTime);
@@ -1052,7 +1052,7 @@ body.dark,
1052
1052
  fontSize: "12.5px",
1053
1053
  marginBottom: "5px",
1054
1054
  color: "var(--agy-text-primary)"
1055
- } }, brandIcon(FAMILY_BRAND[familyKey], 14), h("span", null, label)), renderLine("5h 额度", pct5h, c5h, w5h.resetText), renderLine("周额度", pctWeekly, cWeekly, wWeekly.resetText));
1055
+ } }, brandIcon(FAMILY_BRAND[familyKey], 14), h("span", null, label)), renderLine("5h 额度", pct5h, c5h, w5h.resetText ? w5h.resetText + cdNote : cdNote.replace(/^ · /, "")), renderLine("周额度", pctWeekly, cWeekly, wWeekly.resetText));
1056
1056
  };
1057
1057
  const renderedAccountCards = accounts.map((acc) => {
1058
1058
  const isPrimary = acc.id === pool?.primaryAccountId;
package/dist/index.js CHANGED
@@ -115,9 +115,25 @@ function extractAuthUrl(text) {
115
115
  if (!m) return void 0;
116
116
  return m[0].replace(/[)\]>.,;\x27\x22]+$/, "");
117
117
  }
118
+ /**
119
+ * HARD, server-issued rate-limit signatures — the ONLY patterns allowed to
120
+ * put an account into cooldown. Deliberately narrow: bare `429` or
121
+ * `rate limit` matched incidental substrings in the wild (hash/UUID
122
+ * fragments, model prose mentioning quotas, unrelated tool/permission
123
+ * errors quoting such words) and produced ghost cooldowns that froze
124
+ * healthy accounts out of rotation.
125
+ */
126
+ function looksLikeHardRateLimit(text) {
127
+ if (!text) return false;
128
+ return /RESOURCE_EXHAUSTED|code[ :]?429\b|status[ :]?429\b|HTTP[ :]?429\b|too many requests|individual quota reached|quota (?:exceeded|reached|exhausted)|rate[ -]?limit(?:ed)? (?:exceeded|reached|hit)|exceeded (?:your |the )?quota/i.test(text);
129
+ }
130
+ /**
131
+ * Soft heuristic adds capacity signals (model overloaded / high traffic).
132
+ * Shapes the user-facing error message only — NEVER cools an account down.
133
+ */
118
134
  function looksLikeRateLimit(text) {
119
135
  if (!text) return false;
120
- return /429|too many requests|resource_exhausted|quota exceeded|quota reached|individual quota reached|rate limit|model overloaded|server.*experiencing high traffic|exceeded.*quota/i.test(text);
136
+ return looksLikeHardRateLimit(text) || /model overloaded|experiencing high traffic/i.test(text);
121
137
  }
122
138
  /**
123
139
  * Parse reset duration in milliseconds from rate-limit / quota-exhausted error strings.
@@ -2364,11 +2380,8 @@ var AgyAdapter = class extends LlmAdapter {
2364
2380
  const conversationId = streamCid ?? diffed;
2365
2381
  const r = rec.getResultEvent();
2366
2382
  const consumable = r !== null && (r.ok || r.response !== "");
2367
- const isRateLimit = looksLikeRateLimit([
2368
- outcome.stderrTail,
2369
- outcome.stdout,
2370
- parser.stats.lastResultError
2371
- ].filter(Boolean).join(" "));
2383
+ const rawErrText = [outcome.stderrTail, parser.stats.lastResultError].filter(Boolean).join(" ");
2384
+ const isRateLimit = looksLikeRateLimit(rawErrText);
2372
2385
  let failure = null;
2373
2386
  if (outcome.aborted) failure = {
2374
2387
  kind: "aborted",
@@ -2423,7 +2436,7 @@ var AgyAdapter = class extends LlmAdapter {
2423
2436
  }
2424
2437
  } else {
2425
2438
  const effectiveRateLimit = isRateLimit || looksLikeRateLimit(failure.message);
2426
- if (account && effectiveRateLimit) this.deps.pool?.recordFailure(account.id, family, failure.message);
2439
+ if (account && looksLikeHardRateLimit(rawErrText)) this.deps.pool?.recordFailure(account.id, family, failure.message);
2427
2440
  if (account && (failure.code === Err.AUTH || /invalid_grant|not signed in|auth/i.test(failure.message))) this.deps.pool?.markAuthRequired(account.id, failure.message);
2428
2441
  if (!isAux && sessionAccountKey !== "") {
2429
2442
  if (failure.code === Err.AUTH || effectiveRateLimit || failure.message && /conversation.*(not found|invalid|not recognized|expired|does not exist)|session.*(expired|invalid)/i.test(failure.message)) this.deps.store.delete(sessionAccountKey);
@@ -27668,6 +27681,18 @@ var PoolAuthFlow = class {
27668
27681
  };
27669
27682
  //#endregion
27670
27683
  //#region src/host/quota.ts
27684
+ /**
27685
+ * When the quota-summary endpoint transiently fails, per-model fallback
27686
+ * data carries a SINGLE window (sometimes the weekly one) and no weekly
27687
+ * fields. Overwriting a previously COMPLETE family entry with that partial
27688
+ * shape dropped weeklyFraction to none and put wrong-window numbers into
27689
+ * the 5h row (observed: 5h=100% / reset a week out / weekly missing).
27690
+ * Rule: last-known-good complete data always wins over partial fallback.
27691
+ */
27692
+ function mergeFallbackFamilyQuota(prev, fallback) {
27693
+ if (prev && typeof prev.remainingFraction === "number") return prev;
27694
+ return fallback;
27695
+ }
27671
27696
  function detectEmailFromAgyLogs(homeDir) {
27672
27697
  const logDir = join(homeDir, ".gemini", "antigravity-cli", "log");
27673
27698
  if (!existsSync(logDir)) return void 0;
@@ -27972,11 +27997,11 @@ var QuotaService = class {
27972
27997
  remainingFraction: remaining,
27973
27998
  resetTime
27974
27999
  });
27975
- if (!familyQuotas[fam]) familyQuotas[fam] = {
28000
+ if (!familyQuotas[fam]) familyQuotas[fam] = mergeFallbackFamilyQuota(account.quotas[fam], {
27976
28001
  remainingFraction: remaining,
27977
28002
  resetTime,
27978
28003
  updatedAt: now
27979
- };
28004
+ });
27980
28005
  else if (familyQuotas[fam].remainingFraction === void 0) {
27981
28006
  const curRemaining = familyQuotas[fam].remainingFraction ?? 1;
27982
28007
  if (remaining < curRemaining) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-agy-link",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
4
4
  "description": "Google Antigravity (agy CLI) models for DeepSeek Harness — stream Gemini/Claude/GPT-OSS subscriptions into DSH with thinking, tool activity, token usage and in-GUI Google OAuth login.",
5
5
  "type": "module",
6
6
  "license": "MIT",