@hienlh/ppm 0.8.20 → 0.8.22
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,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.22] - 2026-03-24
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **OAuth pre-flight token refresh**: Check token freshness before each SDK query and refresh proactively if expired or expiring within 60s
|
|
7
|
+
- **Auto-refresh on startup**: Run token refresh check immediately on server start instead of waiting 5 minutes for first interval
|
|
8
|
+
- **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
|
|
9
|
+
|
|
10
|
+
## [0.8.21] - 2026-03-24
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **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
|
|
14
|
+
|
|
3
15
|
## [0.8.20] - 2026-03-24
|
|
4
16
|
|
|
5
17
|
### Fixed
|
package/package.json
CHANGED
package/scripts/patch-sdk.mjs
CHANGED
|
@@ -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:
|
|
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){` +
|
|
@@ -545,7 +545,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
545
545
|
// Account-based auth injection (multi-account mode)
|
|
546
546
|
// Fallback to existing env (ANTHROPIC_API_KEY) when no accounts configured.
|
|
547
547
|
const accountsEnabled = accountSelector.isEnabled();
|
|
548
|
-
|
|
548
|
+
let account = accountsEnabled ? accountSelector.next() : null;
|
|
549
549
|
if (accountsEnabled && !account) {
|
|
550
550
|
// All accounts in DB but none usable
|
|
551
551
|
const reason = accountSelector.lastFailReason;
|
|
@@ -557,7 +557,12 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
557
557
|
yield { type: "done" as const, sessionId, resultSubtype: "error_auth" };
|
|
558
558
|
return;
|
|
559
559
|
}
|
|
560
|
+
// Pre-flight: ensure OAuth token is fresh before sending to SDK
|
|
560
561
|
if (account) {
|
|
562
|
+
const fresh = await accountService.ensureFreshToken(account.id);
|
|
563
|
+
if (fresh) {
|
|
564
|
+
account = fresh;
|
|
565
|
+
}
|
|
561
566
|
console.log(`[sdk] Using account ${account.id} (${account.email ?? "no-email"})`);
|
|
562
567
|
yield { type: "account_info" as const, accountId: account.id, accountLabel: account.label ?? account.email ?? "Unknown" };
|
|
563
568
|
}
|
|
@@ -641,8 +646,10 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
641
646
|
|
|
642
647
|
// Retry logic: if SDK returns error_during_execution with 0 turns on first event,
|
|
643
648
|
// it's a transient subprocess failure — retry once before surfacing the error.
|
|
649
|
+
// Also handles authentication_failed by refreshing OAuth token and retrying.
|
|
644
650
|
const MAX_RETRIES = 1;
|
|
645
651
|
let retryCount = 0;
|
|
652
|
+
let authRetried = false;
|
|
646
653
|
|
|
647
654
|
retryLoop: while (true) {
|
|
648
655
|
let sdkEventCount = 0;
|
|
@@ -816,6 +823,52 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
816
823
|
// Dump full SDK message for debugging
|
|
817
824
|
console.error(`[sdk] session=${sessionId} cwd=${effectiveCwd} assistant error: ${assistantError} (isFirst=${isFirstMessage} retry=${retryCount})`);
|
|
818
825
|
console.error(`[sdk] assistant message dump: ${JSON.stringify(msg).slice(0, 2000)}`);
|
|
826
|
+
|
|
827
|
+
// OAuth token expired — refresh and retry once before showing error
|
|
828
|
+
if (assistantError === "authentication_failed" && account && !authRetried) {
|
|
829
|
+
authRetried = true;
|
|
830
|
+
try {
|
|
831
|
+
await accountService.refreshAccessToken(account.id);
|
|
832
|
+
console.log(`[sdk] session=${sessionId} OAuth token refreshed for ${account.id} — retrying`);
|
|
833
|
+
// Re-build env with refreshed token
|
|
834
|
+
const refreshedAccount = accountService.getWithTokens(account.id);
|
|
835
|
+
if (refreshedAccount) {
|
|
836
|
+
const retryEnv = this.buildQueryEnv(meta.projectPath, refreshedAccount);
|
|
837
|
+
const q = query({
|
|
838
|
+
prompt: message,
|
|
839
|
+
options: {
|
|
840
|
+
sessionId: undefined,
|
|
841
|
+
resume: undefined,
|
|
842
|
+
cwd: effectiveCwd,
|
|
843
|
+
systemPrompt: systemPromptOpt,
|
|
844
|
+
settingSources: ["user", "project"],
|
|
845
|
+
env: retryEnv,
|
|
846
|
+
settings: { permissions: { allow: [], deny: [] } },
|
|
847
|
+
allowedTools,
|
|
848
|
+
permissionMode,
|
|
849
|
+
allowDangerouslySkipPermissions: isBypass,
|
|
850
|
+
...(permissionHooks && { hooks: permissionHooks }),
|
|
851
|
+
...(providerConfig.model && { model: providerConfig.model }),
|
|
852
|
+
...(providerConfig.effort && { effort: providerConfig.effort }),
|
|
853
|
+
maxTurns: providerConfig.max_turns ?? 100,
|
|
854
|
+
...(providerConfig.max_budget_usd && { maxBudgetUsd: providerConfig.max_budget_usd }),
|
|
855
|
+
...(providerConfig.thinking_budget_tokens != null && {
|
|
856
|
+
thinkingBudgetTokens: providerConfig.thinking_budget_tokens,
|
|
857
|
+
}),
|
|
858
|
+
canUseTool,
|
|
859
|
+
includePartialMessages: true,
|
|
860
|
+
} as any,
|
|
861
|
+
});
|
|
862
|
+
this.activeQueries.set(sessionId, q);
|
|
863
|
+
eventSource = q;
|
|
864
|
+
continue retryLoop;
|
|
865
|
+
}
|
|
866
|
+
} catch (refreshErr) {
|
|
867
|
+
console.error(`[sdk] session=${sessionId} OAuth refresh failed:`, refreshErr);
|
|
868
|
+
accountSelector.onAuthError(account.id);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
819
872
|
const errorHints: Record<string, string> = {
|
|
820
873
|
authentication_failed: "API authentication failed. Check your account credentials in Settings → Accounts.",
|
|
821
874
|
billing_error: "Billing error on this account. Check your subscription status.",
|
|
@@ -116,6 +116,29 @@ class AccountService {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Ensure the access token for an OAuth account is still fresh.
|
|
121
|
+
* If it's expired or about to expire (within 60s), refresh it proactively.
|
|
122
|
+
* Returns the refreshed account with fresh tokens, or null if refresh failed.
|
|
123
|
+
*/
|
|
124
|
+
async ensureFreshToken(id: string): Promise<AccountWithTokens | null> {
|
|
125
|
+
const acc = this.getWithTokens(id);
|
|
126
|
+
if (!acc) return null;
|
|
127
|
+
// Only OAuth tokens need refresh
|
|
128
|
+
if (!acc.accessToken.startsWith("sk-ant-oat")) return acc;
|
|
129
|
+
if (!acc.expiresAt) return acc;
|
|
130
|
+
const nowS = Math.floor(Date.now() / 1000);
|
|
131
|
+
if (acc.expiresAt - nowS > 60) return acc; // still fresh
|
|
132
|
+
try {
|
|
133
|
+
console.log(`[accounts] Pre-flight refresh for ${acc.email ?? id} (expires in ${acc.expiresAt - nowS}s)`);
|
|
134
|
+
await this.refreshAccessToken(id);
|
|
135
|
+
return this.getWithTokens(id);
|
|
136
|
+
} catch (e) {
|
|
137
|
+
console.error(`[accounts] Pre-flight refresh failed for ${id}:`, e);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
119
142
|
/** Find existing account by email or profile UUID */
|
|
120
143
|
private findDuplicate(email?: string | null, profileData?: OAuthProfileData | null): Account | null {
|
|
121
144
|
if (!email && !profileData?.account?.uuid) return null;
|
|
@@ -586,7 +609,7 @@ class AccountService {
|
|
|
586
609
|
const CHECK_INTERVAL_MS = 5 * 60_000;
|
|
587
610
|
const REFRESH_BUFFER_S = 5 * 60;
|
|
588
611
|
|
|
589
|
-
|
|
612
|
+
const refreshExpiring = async () => {
|
|
590
613
|
const accounts = this.list();
|
|
591
614
|
const nowS = Math.floor(Date.now() / 1000);
|
|
592
615
|
for (const acc of accounts) {
|
|
@@ -600,7 +623,11 @@ class AccountService {
|
|
|
600
623
|
console.error(`[accounts] Auto-refresh failed for ${acc.id}:`, e);
|
|
601
624
|
}
|
|
602
625
|
}
|
|
603
|
-
}
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// Run immediately on startup, then every 5 minutes
|
|
629
|
+
refreshExpiring().catch(() => {});
|
|
630
|
+
this.refreshTimer = setInterval(refreshExpiring, CHECK_INTERVAL_MS);
|
|
604
631
|
|
|
605
632
|
if (typeof this.refreshTimer === "object" && this.refreshTimer !== null && "unref" in this.refreshTimer) {
|
|
606
633
|
(this.refreshTimer as NodeJS.Timeout).unref();
|