clementine-agent 1.18.24 → 1.18.25

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.
@@ -5863,6 +5863,21 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
5863
5863
  }
5864
5864
  logger.error({ err, jobName, phase }, `Unleashed task phase ${phase} error`);
5865
5865
  appendProgress({ event: 'phase_error', phase, error: String(err), terminalReason });
5866
+ if (isCreditBalanceError(err)) {
5867
+ markBackgroundCreditBlocked(err);
5868
+ appendProgress({ event: 'aborted', phase, reason: 'account_usage_limit' });
5869
+ writeStatus({
5870
+ jobName,
5871
+ status: 'error',
5872
+ phase,
5873
+ startedAt,
5874
+ finishedAt: new Date().toISOString(),
5875
+ });
5876
+ const message = (`Task "${jobName}" stopped because Claude account usage or billing is blocked: ${String(err).slice(0, 500)}. ` +
5877
+ 'Background jobs have been paused so Clementine does not keep retrying against the same account limit.');
5878
+ logger.error({ jobName, phase }, 'Unleashed task aborted on Claude account usage limit');
5879
+ throw new UnleashedTaskFailedError(message, this._lastTerminalReason);
5880
+ }
5866
5881
  if (terminalReason === 'rapid_refill_breaker' || terminalReason === 'prompt_too_long') {
5867
5882
  appendProgress({ event: 'aborted', phase, reason: terminalReason });
5868
5883
  writeStatus({
@@ -17625,18 +17625,27 @@ async function apiFetch(url, opts) {
17625
17625
  }
17626
17626
  // 401 means the dashboard was restarted and regenerated its token.
17627
17627
  // The page still in your browser has a stale one baked into <meta>,
17628
- // so every API call silently fails. Reload once to pick up the new
17629
- // HTML (and new token). Guard against reload loops with sessionStorage.
17628
+ // so every API call silently fails. Reload to pick up the new HTML
17629
+ // (and new token), but throttle by token so a tab cannot get stuck
17630
+ // forever after one earlier restart.
17630
17631
  if (resp.status === 401) {
17631
- var key = '_dashReloadedOnce';
17632
- if (!sessionStorage.getItem(key)) {
17633
- sessionStorage.setItem(key, String(Date.now()));
17632
+ var key = '_dashReloadedForToken:' + (_dashToken || 'missing').slice(0, 12);
17633
+ var lastReload = Number(sessionStorage.getItem(key) || '0');
17634
+ var now = Date.now();
17635
+ if (!lastReload || now - lastReload > 5000) {
17636
+ sessionStorage.setItem(key, String(now));
17634
17637
  console.warn('Dashboard token expired — reloading to refresh.');
17635
17638
  location.reload();
17636
17639
  // Let the reload kick in; return the 401 so any caller that
17637
17640
  // handles it sees a consistent result until the page unloads.
17638
17641
  return resp;
17639
17642
  }
17643
+ } else if (resp.ok) {
17644
+ try {
17645
+ Object.keys(sessionStorage).forEach(function(k) {
17646
+ if (k.indexOf('_dashReloadedForToken:') === 0) sessionStorage.removeItem(k);
17647
+ });
17648
+ } catch (_) { /* ignore */ }
17640
17649
  }
17641
17650
  return resp;
17642
17651
  }
@@ -5,7 +5,7 @@ const CREDIT_BLOCK_FILE = path.join(BASE_DIR, 'cron', 'credit-block.json');
5
5
  const DEFAULT_BLOCK_MS = 6 * 60 * 60 * 1000;
6
6
  export function isCreditBalanceError(err) {
7
7
  const msg = String(err ?? '');
8
- return /credit balance is too low|credit balance.*too low|insufficient credits?|billing.*credits?|account.*credits?.*low/i.test(msg);
8
+ return /credit balance is too low|credit balance.*too low|insufficient credits?|billing.*credits?|account.*credits?.*low|monthly usage limit|org'?s monthly usage limit|organization'?s monthly usage limit|hit your .*usage limit|usage limit.*(?:reached|exceeded|hit|active)|usage or credit limit|credit limit is active|spending limit|billing limit/i.test(msg);
9
9
  }
10
10
  export function getBackgroundCreditBlock(nowMs = Date.now()) {
11
11
  try {
@@ -41,6 +41,6 @@ export function markBackgroundCreditBlocked(err, nowMs = Date.now()) {
41
41
  return { block, created: true };
42
42
  }
43
43
  export function formatCreditBlock(block) {
44
- return `Claude credit balance is too low. Background jobs are paused until ${block.until}.`;
44
+ return `Claude account usage or credit limit is active. Background jobs are paused until ${block.until}.`;
45
45
  }
46
46
  //# sourceMappingURL=credit-guard.js.map
@@ -16,6 +16,7 @@ import path from 'node:path';
16
16
  import pino from 'pino';
17
17
  import { AGENTS_DIR, BASE_DIR, CRON_FILE } from '../config.js';
18
18
  import { loadPromptOverrides, loadPromptOverridesForJob } from '../agent/prompt-overrides/loader.js';
19
+ import { isCreditBalanceError } from './credit-guard.js';
19
20
  const logger = pino({ name: 'clementine.failure-diagnostics' });
20
21
  const CACHE_FILE = path.join(BASE_DIR, 'cron', 'failure-diagnostics.json');
21
22
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
@@ -297,6 +298,18 @@ export function diagnoseKnownFailurePattern(broken, jobDef, recentRuns, opts) {
297
298
  ...broken.lastErrors,
298
299
  recentRuns,
299
300
  ].join('\n').toLowerCase();
301
+ if (isCreditBalanceError(haystack)) {
302
+ return {
303
+ rootCause: 'Claude is blocked by an org usage or billing limit, so this is not a job-definition failure.',
304
+ confidence: 'high',
305
+ proposedFix: {
306
+ type: 'escalate_to_owner',
307
+ details: 'Keep background jobs paused until the Claude org usage limit resets or billing capacity is restored. Do not retry or auto-apply job fixes for this error.',
308
+ },
309
+ riskLevel: 'low',
310
+ generatedAt: new Date().toISOString(),
311
+ };
312
+ }
300
313
  if (/rapid_refill_breaker|autocompact.*thrash|context refilled|prompt is too long|prompt too long|context.?length|maximum context|input is too long/.test(haystack)) {
301
314
  if (hasContextOverflowPromptOverride(broken.jobName, broken.agentSlug, opts)) {
302
315
  return {
@@ -1,5 +1,5 @@
1
1
  import type { CronRunEntry } from '../types.js';
2
- export type JobHealthKind = 'healthy' | 'recovered' | 'partial' | 'context_overflow' | 'auth' | 'rate_limited' | 'tool_scope' | 'prompt_too_large' | 'failed' | 'unknown';
2
+ export type JobHealthKind = 'healthy' | 'recovered' | 'partial' | 'context_overflow' | 'usage_blocked' | 'auth' | 'rate_limited' | 'tool_scope' | 'prompt_too_large' | 'failed' | 'unknown';
3
3
  export interface JobHealthStatus {
4
4
  status: JobHealthKind;
5
5
  jobName?: string;
@@ -1,3 +1,4 @@
1
+ import { isCreditBalanceError } from './credit-guard.js';
1
2
  function compactEvidence(...items) {
2
3
  const seen = new Set();
3
4
  const result = [];
@@ -21,6 +22,15 @@ export function classifyRunHealth(entry) {
21
22
  lastRunAt: entry.startedAt,
22
23
  terminalReason,
23
24
  };
25
+ if (isCreditBalanceError(blob)) {
26
+ return {
27
+ ...base,
28
+ status: 'usage_blocked',
29
+ evidence: compactEvidence(entry.error, entry.outputPreview, terminalReason ? `terminalReason=${terminalReason}` : undefined),
30
+ recommendedAction: 'Resolve the Claude org usage or billing limit, or switch this job to an available provider/model before retrying.',
31
+ requiresApproval: false,
32
+ };
33
+ }
24
34
  if (terminalReason === 'rapid_refill_breaker' || /rapid_refill_breaker|autocompact|context refilled|maximum context|context.?length/.test(blob)) {
25
35
  return {
26
36
  ...base,
@@ -112,6 +122,6 @@ export function classifyRunHealth(entry) {
112
122
  }
113
123
  export function isRunHealthFailure(entry) {
114
124
  const health = classifyRunHealth(entry);
115
- return !['healthy', 'recovered', 'unknown'].includes(health.status);
125
+ return !['healthy', 'recovered', 'unknown', 'usage_blocked'].includes(health.status);
116
126
  }
117
127
  //# sourceMappingURL=job-health.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.24",
3
+ "version": "1.18.25",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",