clawmoney 0.15.68 → 0.15.69

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.
@@ -167,6 +167,83 @@ function extractMessageText(content) {
167
167
  function messagesToPrompt(messages) {
168
168
  return messages.map((m) => extractMessageText(m.content)).join("\n");
169
169
  }
170
+ // ── OAuth auto-pause (per-cli_type) ────────────────────────────────────
171
+ //
172
+ // When upstream keeps rejecting our OAuth token (Anthropic 403
173
+ // permission_error, ChatGPT auth failures, etc.), continuing to hammer
174
+ // it wastes buyer requests, surfaces errors the Hub has to failover
175
+ // around, and thrashes the Hub's 5xx ban / unban cycle every time the
176
+ // daemon reconnects. Track consecutive auth-broken errors per cli_type
177
+ // — after AUTH_ERROR_THRESHOLD hits in a row, stop accepting new
178
+ // requests for THAT cli_type until daemon restart. Every successful
179
+ // upstream response resets the counter.
180
+ //
181
+ // Key properties:
182
+ // - Per cli_type: a broken Claude OAuth doesn't take down Codex or
183
+ // Gemini on the same daemon, because each has its own counter and
184
+ // its own disable flag.
185
+ // - In-memory only: state resets on daemon restart. If the operator
186
+ // re-authed between restarts, the next request proves the token
187
+ // works and nothing happens; if they didn't, the counter fills
188
+ // back up within AUTH_ERROR_THRESHOLD requests and re-disables.
189
+ // - No WS lifecycle touched: the daemon stays connected to the Hub
190
+ // so other cli_types still serve. We just refuse to call upstream
191
+ // for the disabled one, returning a clean error the Hub can use
192
+ // to ban this provider row (its existing _is_auth_broken_error
193
+ // pattern catches our "OAuth authentication broken" message).
194
+ //
195
+ // Operator recovery: run `clawmoney login <cli>` (or re-auth the
196
+ // relevant CLI directly — `claude login`, `codex login`, etc.), then
197
+ // `clawmoney relay restart` to reset the counter.
198
+ const AUTH_ERROR_THRESHOLD = 3;
199
+ const consecutiveAuthErrorsByCli = new Map();
200
+ const cliAuthDisabled = new Set();
201
+ const AUTH_BROKEN_PATTERNS = [
202
+ // Anthropic 403: OAuth authentication is currently not allowed for
203
+ // this organization. The new prod signal from 2026-04-15 incident.
204
+ "permission_error",
205
+ "not allowed for this organization",
206
+ // Legacy Claude / Anthropic auth failures (also matched by Hub's
207
+ // _AUTH_BROKEN_PATTERNS, so the two sides agree on classification).
208
+ "token refresh failed",
209
+ "invalid_grant",
210
+ "request not allowed",
211
+ "oauth refresh",
212
+ // Generic OAuth HTTP signatures. Catches the one-off 401/403
213
+ // responses from codex / gemini / antigravity that carry the same
214
+ // meaning even when the upstream-specific message format differs.
215
+ "unauthorized",
216
+ ];
217
+ function isAuthBrokenError(errMsg) {
218
+ const lower = errMsg.toLowerCase();
219
+ return AUTH_BROKEN_PATTERNS.some((p) => lower.includes(p));
220
+ }
221
+ function noteUpstreamAuthError(cliType) {
222
+ const next = (consecutiveAuthErrorsByCli.get(cliType) ?? 0) + 1;
223
+ consecutiveAuthErrorsByCli.set(cliType, next);
224
+ if (next >= AUTH_ERROR_THRESHOLD && !cliAuthDisabled.has(cliType)) {
225
+ cliAuthDisabled.add(cliType);
226
+ logger.error("");
227
+ logger.error(` ╔══════════════════════════════════════════════════════════════`);
228
+ logger.error(` ║ OAuth broken for cli_type='${cliType}' — ${next} consecutive`);
229
+ logger.error(` ║ auth-broken responses from upstream. Pausing relay for this`);
230
+ logger.error(` ║ cli_type to stop thrashing buyer requests + Hub ban state.`);
231
+ logger.error(` ║`);
232
+ logger.error(` ║ TO RESUME: re-authenticate your ${cliType} CLI locally, then`);
233
+ logger.error(` ║ run 'clawmoney relay restart'.`);
234
+ logger.error(` ║`);
235
+ logger.error(` ║ Other cli_types on this daemon continue to serve normally.`);
236
+ logger.error(` ╚══════════════════════════════════════════════════════════════`);
237
+ logger.error("");
238
+ }
239
+ }
240
+ function noteUpstreamSuccess(cliType) {
241
+ // Successful request → reset the consecutive counter. The disabled
242
+ // flag is sticky until daemon restart on purpose — we never want to
243
+ // "heal" mid-run based on a single lucky response, which could be
244
+ // an upstream glitch rather than a real token refresh.
245
+ consecutiveAuthErrorsByCli.delete(cliType);
246
+ }
170
247
  async function executeRelayRequest(request, config, sendChunk) {
171
248
  const { request_id, max_budget_usd } = request;
172
249
  const cliType = request.cli_type ?? config.relay.cli_type;
@@ -190,6 +267,21 @@ async function executeRelayRequest(request, config, sendChunk) {
190
267
  logger.info(` │ CLI: ${cliType} / ${model} (${modeLabel})`);
191
268
  logger.info(` │ Turns: ${turns}`);
192
269
  logger.info(` │ Prompt: ${String(lastUserMsg).slice(0, 80)}`);
270
+ // Fast-fail if this cli_type was auto-paused by a run of auth-broken
271
+ // responses earlier in the session. Returning the error here instead
272
+ // of calling upstream saves the round-trip and keeps the Hub's ban
273
+ // pattern triggering (it matches "OAuth authentication" / "auth
274
+ // broken" in _is_auth_broken_error) so buyer requests go straight to
275
+ // a healthy provider.
276
+ if (cliAuthDisabled.has(cliType)) {
277
+ logger.warn(` └─ REFUSED: ${cliType} auth paused (restart relay after re-auth)`);
278
+ return {
279
+ event: "relay_response",
280
+ request_id,
281
+ content: "",
282
+ error: `OAuth authentication broken for cli_type='${cliType}'. Provider needs to re-authenticate locally and restart the daemon. (permission_error)`,
283
+ };
284
+ }
193
285
  try {
194
286
  const startMs = Date.now();
195
287
  let parsed;
@@ -300,6 +392,9 @@ async function executeRelayRequest(request, config, sendChunk) {
300
392
  if (fakeModelUsed) {
301
393
  logger.warn(` ! CLAWMONEY_FAKE_MODEL_USED=${fakeModelUsed} — reporting fake model to Hub (test mode)`);
302
394
  }
395
+ // Successful upstream round-trip — reset the auth-error counter for
396
+ // this cli_type. One good response means the token currently works.
397
+ noteUpstreamSuccess(cliType);
303
398
  return {
304
399
  event: "relay_response",
305
400
  request_id,
@@ -312,12 +407,22 @@ async function executeRelayRequest(request, config, sendChunk) {
312
407
  };
313
408
  }
314
409
  catch (err) {
315
- logger.error(` └─ ERROR: ${err instanceof Error ? err.message : err}`);
410
+ const errMsg = err instanceof Error ? err.message : String(err);
411
+ logger.error(` └─ ERROR: ${errMsg}`);
412
+ // If the upstream error looks like a persistent auth failure
413
+ // (OAuth rejected, token broken, permission_error, etc.), bump
414
+ // this cli_type's consecutive-auth-error counter. After
415
+ // AUTH_ERROR_THRESHOLD in a row, future requests for this
416
+ // cli_type short-circuit at the top of executeRelayRequest until
417
+ // daemon restart.
418
+ if (isAuthBrokenError(errMsg)) {
419
+ noteUpstreamAuthError(cliType);
420
+ }
316
421
  return {
317
422
  event: "relay_response",
318
423
  request_id,
319
424
  content: "",
320
- error: err instanceof Error ? err.message : "Unknown execution error",
425
+ error: errMsg || "Unknown execution error",
321
426
  };
322
427
  }
323
428
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.15.68",
3
+ "version": "0.15.69",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {