shraga 0.1.70 → 0.1.71

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shraga",
3
- "version": "0.1.70",
3
+ "version": "0.1.71",
4
4
  "description": "The teammate you delegate coding to — a self-hostable, multi-user AI coding agent web UI (Claude Code, with a pluggable engine seam).",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -122,11 +122,14 @@ export class ClaudeUsageReader {
122
122
  signal: AbortSignal.timeout(this.options.timeoutMs),
123
123
  });
124
124
  if (res.status === 429) {
125
- this.enterCooldown(res.headers.get('retry-after'));
125
+ // The reason lives in the BODY, not the status: "too many requests" (our own poll rate) and
126
+ // an account/org-level throttle read identically from the outside, and only the first one is
127
+ // ours to fix. Backing off blind once cost a day of a hidden gauge, so surface it.
128
+ this.enterCooldown(res.headers.get('retry-after'), await describeError(res));
126
129
  return null;
127
130
  }
128
131
  if (!res.ok) {
129
- console.warn(`${TAG} usage endpoint returned ${res.status}; hiding widget`);
132
+ console.warn(`${TAG} usage endpoint returned ${res.status}; hiding widget — ${await describeError(res)}`);
130
133
  return null;
131
134
  }
132
135
  const body: any = await res.json();
@@ -145,14 +148,17 @@ export class ClaudeUsageReader {
145
148
 
146
149
  /** Climb the backoff ladder one rung per consecutive 429, capped at the last rung. `Retry-After`
147
150
  * (seconds, or an HTTP-date) only ever EXTENDS the wait — never shortens the rung we earned. */
148
- private enterCooldown(retryAfter: string | null) {
151
+ private enterCooldown(retryAfter: string | null, detail: string) {
149
152
  const ladder = this.options.rateLimitBackoffMs;
150
153
  const rung = ladder[Math.min(this.rateLimitStreak, ladder.length - 1)] ?? 60_000;
151
154
  this.rateLimitStreak++;
152
155
  const hinted = parseRetryAfter(retryAfter);
153
156
  const waitMs = Math.max(rung, hinted ?? 0);
154
157
  this.cooldownUntil = Date.now() + waitMs;
155
- console.warn(`${TAG} usage endpoint returned 429; hiding widget and backing off ${Math.round(waitMs / 1000)}s`);
158
+ console.warn(
159
+ `${TAG} usage endpoint returned 429 (ua=claude-code/${claudeCodeVersion()}, retry-after=${retryAfter ?? 'none'}); ` +
160
+ `hiding widget and backing off ${Math.round(waitMs / 1000)}s — ${detail}`,
161
+ );
156
162
  }
157
163
 
158
164
  /** Re-read per poll from whichever source holds them — the token lives ~8h and Claude Code
@@ -208,6 +214,18 @@ function parseCredentials(raw: string, source: string): { accessToken: string; s
208
214
  return { accessToken: oauth.accessToken, subscriptionType: oauth.subscriptionType ?? null };
209
215
  }
210
216
 
217
+ /** Anthropic answers errors as `{ error: { type, message } }`. Never throws and never returns more
218
+ * than a line: this only ever lands in a log, next to a status we already decided to fail on. */
219
+ async function describeError(res: Response): Promise<string> {
220
+ try {
221
+ const raw = (await res.text()).slice(0, 300);
222
+ const parsed = JSON.parse(raw)?.error;
223
+ return parsed?.message ? `${parsed.type ?? 'error'}: ${parsed.message}` : raw || '(empty body)';
224
+ } catch (err) {
225
+ return `(unreadable body: ${(err as Error).message})`;
226
+ }
227
+ }
228
+
211
229
  /** `Retry-After` is either delta-seconds or an HTTP-date. Anything unparseable => no hint. */
212
230
  function parseRetryAfter(raw: string | null): number | null {
213
231
  if (!raw) return null;