@hienlh/ppm 0.8.20 → 0.8.21

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.21] - 2026-03-24
4
+
5
+ ### Fixed
6
+ - **OAuth auto-refresh on auth failure**: Automatically refresh expired OAuth token and retry when SDK returns `authentication_failed`, instead of just showing error to user
7
+
3
8
  ## [0.8.20] - 2026-03-24
4
9
 
5
10
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.8.20",
3
+ "version": "0.8.21",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -46,11 +46,14 @@ if (content.includes("waiting for drain")) {
46
46
  const arg = drainMatch[1];
47
47
  const logger = drainMatch[2];
48
48
 
49
- // Replace backpressure line: await drain instead of just logging
49
+ // Replace backpressure line:
50
+ // - Non-Windows: await drain event (pipe buffers are large, drain fires reliably)
51
+ // - Windows: skip drain — Bun+Windows pipe drain event is unreliable (may never fire
52
+ // in PowerShell). OS still buffers data; subprocess reads when ready.
50
53
  const newLine =
51
54
  `if(!this.processStdin.write(${arg})){` +
52
- `${logger}("[ProcessTransport] Write buffer full, waiting for drain");` +
53
- `await new Promise(_dr=>this.processStdin.once("drain",_dr))}`;
55
+ `${logger}("[ProcessTransport] Write buffer full, "+(process.platform==="win32"?"skipping drain (Windows)":"waiting for drain"));` +
56
+ `if(process.platform!=="win32")await new Promise(_dr=>this.processStdin.once("drain",_dr))}`;
54
57
 
55
58
  content = content.replace(oldLine, newLine);
56
59
 
@@ -162,6 +165,9 @@ if (content.includes("__ppm_manual_readline__")) {
162
165
  `_buf="";_done=true;_notify()` +
163
166
  `});` +
164
167
  `this.processStdout.on("error",(e)=>{_err=e;_done=true;_notify()});` +
168
+ // Bun on Windows may not auto-switch to flowing mode when "data" listener is added.
169
+ // Explicit resume() ensures data events fire.
170
+ `this.processStdout.resume();` +
165
171
  `try{` +
166
172
  `while(true){` +
167
173
  `while(_lines.length>0){` +
@@ -641,8 +641,10 @@ export class ClaudeAgentSdkProvider implements AIProvider {
641
641
 
642
642
  // Retry logic: if SDK returns error_during_execution with 0 turns on first event,
643
643
  // it's a transient subprocess failure — retry once before surfacing the error.
644
+ // Also handles authentication_failed by refreshing OAuth token and retrying.
644
645
  const MAX_RETRIES = 1;
645
646
  let retryCount = 0;
647
+ let authRetried = false;
646
648
 
647
649
  retryLoop: while (true) {
648
650
  let sdkEventCount = 0;
@@ -816,6 +818,52 @@ export class ClaudeAgentSdkProvider implements AIProvider {
816
818
  // Dump full SDK message for debugging
817
819
  console.error(`[sdk] session=${sessionId} cwd=${effectiveCwd} assistant error: ${assistantError} (isFirst=${isFirstMessage} retry=${retryCount})`);
818
820
  console.error(`[sdk] assistant message dump: ${JSON.stringify(msg).slice(0, 2000)}`);
821
+
822
+ // OAuth token expired — refresh and retry once before showing error
823
+ if (assistantError === "authentication_failed" && account && !authRetried) {
824
+ authRetried = true;
825
+ try {
826
+ await accountService.refreshAccessToken(account.id);
827
+ console.log(`[sdk] session=${sessionId} OAuth token refreshed for ${account.id} — retrying`);
828
+ // Re-build env with refreshed token
829
+ const refreshedAccount = accountService.getWithTokens(account.id);
830
+ if (refreshedAccount) {
831
+ const retryEnv = this.buildQueryEnv(meta.projectPath, refreshedAccount);
832
+ const q = query({
833
+ prompt: message,
834
+ options: {
835
+ sessionId: undefined,
836
+ resume: undefined,
837
+ cwd: effectiveCwd,
838
+ systemPrompt: systemPromptOpt,
839
+ settingSources: ["user", "project"],
840
+ env: retryEnv,
841
+ settings: { permissions: { allow: [], deny: [] } },
842
+ allowedTools,
843
+ permissionMode,
844
+ allowDangerouslySkipPermissions: isBypass,
845
+ ...(permissionHooks && { hooks: permissionHooks }),
846
+ ...(providerConfig.model && { model: providerConfig.model }),
847
+ ...(providerConfig.effort && { effort: providerConfig.effort }),
848
+ maxTurns: providerConfig.max_turns ?? 100,
849
+ ...(providerConfig.max_budget_usd && { maxBudgetUsd: providerConfig.max_budget_usd }),
850
+ ...(providerConfig.thinking_budget_tokens != null && {
851
+ thinkingBudgetTokens: providerConfig.thinking_budget_tokens,
852
+ }),
853
+ canUseTool,
854
+ includePartialMessages: true,
855
+ } as any,
856
+ });
857
+ this.activeQueries.set(sessionId, q);
858
+ eventSource = q;
859
+ continue retryLoop;
860
+ }
861
+ } catch (refreshErr) {
862
+ console.error(`[sdk] session=${sessionId} OAuth refresh failed:`, refreshErr);
863
+ accountSelector.onAuthError(account.id);
864
+ }
865
+ }
866
+
819
867
  const errorHints: Record<string, string> = {
820
868
  authentication_failed: "API authentication failed. Check your account credentials in Settings → Accounts.",
821
869
  billing_error: "Billing error on this account. Check your subscription status.",