@hienlh/ppm 0.8.48 → 0.8.49

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.49] - 2026-03-25
4
+
5
+ ### Fixed
6
+ - **Auth error backoff**: `onAuthError` now uses 5-minute base cooldown (exponential up to 30min) instead of 1s rate-limit backoff — prevents rapid retry loops on dead/rejected accounts
7
+ - **No permanent disable**: Auth errors never permanently disable accounts — cooldown allows recovery from transient issues (subscription lapse, org changes, API hiccups)
8
+
3
9
  ## [0.8.48] - 2026-03-25
4
10
 
5
11
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.8.48",
3
+ "version": "0.8.49",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -628,9 +628,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
628
628
  }
629
629
  } catch (refreshErr) {
630
630
  console.error(`[sdk] session=${sessionId} OAuth refresh failed:`, refreshErr);
631
- // Cooldown instead of permanent disable — auth issues can be transient
632
- // (subscription lapse, org changes, API hiccups)
633
- accountSelector.onRateLimit(account.id);
631
+ accountSelector.onAuthError(account.id);
634
632
  }
635
633
  }
636
634
 
@@ -704,8 +702,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
704
702
  await accountService.refreshAccessToken(account.id, false);
705
703
  console.log(`[sdk] 401 on account ${account.id} — token refreshed`);
706
704
  } catch {
707
- // Cooldown instead of permanent disable — auth issues can be transient
708
- accountSelector.onRateLimit(account.id);
705
+ accountSelector.onAuthError(account.id);
709
706
  }
710
707
  } else {
711
708
  accountSelector.onSuccess(account.id);
@@ -8,6 +8,7 @@ const MAX_RETRY_CONFIG_KEY = "account_max_retry";
8
8
 
9
9
  const BACKOFF_BASE_MS = 1_000;
10
10
  const BACKOFF_MAX_MS = 30 * 60_000;
11
+ const AUTH_BACKOFF_BASE_MS = 5 * 60_000; // 5min base for auth errors (longer than rate limits)
11
12
 
12
13
  class AccountSelectorService {
13
14
  private cursor = 0;
@@ -125,11 +126,13 @@ class AccountSelectorService {
125
126
  console.log(`[accounts] ${accountId} rate limited — cooldown ${Math.round(backoffMs / 1000)}s (retry #${retries})`);
126
127
  }
127
128
 
128
- /** Called when 401 Unauthorizeddisable account */
129
+ /** Called when auth error (401 / authentication_failed) cooldown with longer backoff */
129
130
  onAuthError(accountId: string): void {
130
- console.log(`[accounts] ${accountId} auth error disabling account`);
131
- accountService.setDisabled(accountId);
132
- this.retryCounts.delete(accountId);
131
+ const retries = (this.retryCounts.get(accountId) ?? 0) + 1;
132
+ this.retryCounts.set(accountId, retries);
133
+ const backoffMs = Math.min(AUTH_BACKOFF_BASE_MS * Math.pow(2, retries - 1), BACKOFF_MAX_MS);
134
+ accountService.setCooldown(accountId, Date.now() + backoffMs);
135
+ console.log(`[accounts] ${accountId} auth error — cooldown ${Math.round(backoffMs / 1000)}s (retry #${retries})`);
133
136
  }
134
137
 
135
138
  /** Called on successful request — reset retry count + track usage */