@hienlh/ppm 0.9.21 → 0.9.23

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,9 +1,10 @@
1
1
  # Changelog
2
2
 
3
- ## [0.9.21] - 2026-04-05
3
+ ## [0.9.22] - 2026-04-05
4
4
 
5
5
  ### Fixed
6
- - **SDK internal 401 retry loop stuck**: SDK retries 401 errors 10 times with exponential backoff using the same expired token, leaving user stuck at "Thinking..." for 2+ minutes. Now intercepts `api_retry` system events with 401 on first occurrence, refreshes OAuth token immediately, and restarts the query cutting recovery from ~2 min to ~1 sec.
6
+ - **SDK internal 401 retry loop stuck**: SDK retries 401 errors 10x with exponential backoff using same expired token (~2 min stuck at "Thinking..."). Now intercepts `api_retry` on first 401, refreshes token immediately, and if refresh fails (e.g. temporary account with no refresh token), switches to a different account instantly.
7
+ - **Auto-refresh spam for temporary accounts**: Accounts without refresh token triggered auto-refresh every 5 min, failing every time. Now skips them in the auto-refresh loop.
7
8
 
8
9
  ## [0.9.20] - 2026-04-05
9
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.9.21",
3
+ "version": "0.9.23",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -816,6 +816,32 @@ export class ClaudeAgentSdkProvider implements AIProvider {
816
816
  } catch (refreshErr) {
817
817
  console.error(`[sdk] session=${sessionId} early OAuth refresh failed:`, refreshErr);
818
818
  accountSelector.onAuthError(account.id);
819
+ // Refresh failed (e.g. temporary account with no refresh token).
820
+ // Abort the current query immediately and try switching to a different account.
821
+ const nextAcc = accountSelector.next();
822
+ if (nextAcc && nextAcc.id !== account.id) {
823
+ account = nextAcc;
824
+ const label = nextAcc.label ?? nextAcc.email ?? "Unknown";
825
+ console.log(`[sdk] session=${sessionId} refresh failed — switching to ${nextAcc.id} (${label})`);
826
+ yield { type: "account_retry" as const, reason: "Switching account", accountId: nextAcc.id, accountLabel: label };
827
+ const switchEnv = this.buildQueryEnv(meta.projectPath, nextAcc);
828
+ streamCtrl.done();
829
+ q.close();
830
+ const { generator: switchGen, controller: switchCtrl } = createMessageChannel();
831
+ const currentSdkId = getSessionMapping(sessionId);
832
+ const canResume = currentSdkId && currentSdkId !== sessionId;
833
+ if (!canResume) switchCtrl.push(firstMsg);
834
+ const retryOpts = { ...queryOptions, sessionId: undefined, resume: canResume ? currentSdkId : undefined, env: switchEnv };
835
+ const rq = query({
836
+ prompt: switchGen,
837
+ options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
838
+ });
839
+ this.streamingSessions.set(sessionId, { meta, query: rq, controller: switchCtrl });
840
+ this.activeQueries.set(sessionId, rq);
841
+ eventSource = rq;
842
+ continue retryLoop;
843
+ }
844
+ // No other account available — let SDK continue and eventually emit error
819
845
  }
820
846
  }
821
847
 
@@ -705,6 +705,9 @@ class AccountService {
705
705
  if (acc.status === "disabled") continue;
706
706
  if (!acc.expiresAt) continue;
707
707
  if (acc.expiresAt - nowS > REFRESH_BUFFER_S) continue;
708
+ // Skip temporary accounts (no refresh token) — they can't be refreshed
709
+ const withTokens = this.getWithTokens(acc.id);
710
+ if (!withTokens?.refreshToken) continue;
708
711
  console.log(`[accounts] Auto-refreshing token for ${acc.email ?? acc.id}`);
709
712
  try {
710
713
  await this.refreshAccessToken(acc.id, false);