@hienlh/ppm 0.9.68 → 0.9.69
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,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.69] - 2026-04-09
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Cloud WS replaced loop**: `isConnected()` now returns true during 500ms auth handshake, preventing Cloud monitor from killing valid connections. Fixed `disconnect()` not resetting `reconnecting` flag.
|
|
7
|
+
- **Token refresh buffer**: Increased OAuth token refresh buffer from 60s to 1 hour to prevent 401 errors mid-conversation.
|
|
8
|
+
- **SDK retry stale closures**: Extracted `closeCurrentStream()` helper that reads from session map instead of captured variables, preventing retry paths from closing already-replaced streams. Fixed phantom session entries from retry init events.
|
|
9
|
+
|
|
3
10
|
## [0.9.68] - 2026-04-08
|
|
4
11
|
|
|
5
12
|
### Fixed
|
package/package.json
CHANGED
|
@@ -710,16 +710,17 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
710
710
|
crashRetryLoop: for (;;) {
|
|
711
711
|
try {
|
|
712
712
|
// Streaming input: create message channel and persistent query
|
|
713
|
-
const { generator: streamGen, controller: streamCtrl } = createMessageChannel();
|
|
714
713
|
const firstMsg = {
|
|
715
714
|
type: 'user' as const,
|
|
716
715
|
message: buildMessageParam(message),
|
|
717
716
|
parent_tool_use_id: null,
|
|
718
717
|
session_id: sessionId,
|
|
719
718
|
};
|
|
720
|
-
streamCtrl.push(firstMsg);
|
|
721
719
|
|
|
722
|
-
const
|
|
720
|
+
const { generator: streamGen, controller: initialCtrl } = createMessageChannel();
|
|
721
|
+
initialCtrl.push(firstMsg);
|
|
722
|
+
|
|
723
|
+
const initialQuery = query({
|
|
723
724
|
prompt: streamGen,
|
|
724
725
|
options: {
|
|
725
726
|
...queryOptions,
|
|
@@ -727,9 +728,19 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
727
728
|
canUseTool,
|
|
728
729
|
} as any,
|
|
729
730
|
});
|
|
730
|
-
this.streamingSessions.set(sessionId, { meta, query:
|
|
731
|
-
this.activeQueries.set(sessionId,
|
|
732
|
-
let eventSource: AsyncIterable<any> =
|
|
731
|
+
this.streamingSessions.set(sessionId, { meta, query: initialQuery, controller: initialCtrl });
|
|
732
|
+
this.activeQueries.set(sessionId, initialQuery);
|
|
733
|
+
let eventSource: AsyncIterable<any> = initialQuery;
|
|
734
|
+
|
|
735
|
+
// Helper: close the CURRENT streaming session (not stale closure refs).
|
|
736
|
+
// All retry paths must use this instead of closing captured variables directly.
|
|
737
|
+
const closeCurrentStream = () => {
|
|
738
|
+
const ss = this.streamingSessions.get(sessionId);
|
|
739
|
+
if (ss) {
|
|
740
|
+
ss.controller.done();
|
|
741
|
+
ss.query.close();
|
|
742
|
+
}
|
|
743
|
+
};
|
|
733
744
|
|
|
734
745
|
let lastPartialText = "";
|
|
735
746
|
/** Number of tool_use blocks pending results (top-level tools only, not subagent children) */
|
|
@@ -759,9 +770,8 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
759
770
|
if ((msg as any).type === "result" && (msg as any).subtype === "error_during_execution" && ((msg as any).num_turns ?? 0) === 0 && retryCount < MAX_RETRIES) {
|
|
760
771
|
retryCount++;
|
|
761
772
|
console.warn(`[sdk] transient error on first event — retrying (attempt ${retryCount}/${MAX_RETRIES})`);
|
|
762
|
-
// Close
|
|
763
|
-
|
|
764
|
-
q.close();
|
|
773
|
+
// Close current streaming session (uses streamingSessions, not stale closure refs)
|
|
774
|
+
closeCurrentStream();
|
|
765
775
|
const { generator: retryGen, controller: retryCtrl } = createMessageChannel();
|
|
766
776
|
retryCtrl.push(firstMsg);
|
|
767
777
|
const retryOpts = { ...queryOptions, sessionId: undefined, resume: undefined };
|
|
@@ -809,9 +819,13 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
809
819
|
} else {
|
|
810
820
|
console.log(`[sdk] session=${sessionId} ignoring retry init sdk_id=${initMsg.session_id} (mapping already set)`);
|
|
811
821
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
822
|
+
// Only create activeSessions alias for first-time SDK id mapping.
|
|
823
|
+
// Retry init events create phantom entries that pollute the map.
|
|
824
|
+
if (isFirstMessage) {
|
|
825
|
+
const oldMeta = this.activeSessions.get(sessionId);
|
|
826
|
+
if (oldMeta) {
|
|
827
|
+
this.activeSessions.set(initMsg.session_id, { ...oldMeta, id: initMsg.session_id });
|
|
828
|
+
}
|
|
815
829
|
}
|
|
816
830
|
}
|
|
817
831
|
}
|
|
@@ -849,8 +863,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
849
863
|
console.log(`[sdk] session=${sessionId} intercepted api_retry 401 — refreshing token for ${account.id} (${label})`);
|
|
850
864
|
yield { type: "account_retry" as const, reason: "Token refreshed", accountId: refreshedAccount.id, accountLabel: label };
|
|
851
865
|
const retryEnv = this.buildQueryEnv(meta.projectPath, refreshedAccount);
|
|
852
|
-
|
|
853
|
-
q.close();
|
|
866
|
+
closeCurrentStream();
|
|
854
867
|
const { generator: earlyAuthGen, controller: earlyAuthCtrl } = createMessageChannel();
|
|
855
868
|
const currentSdkId = getSessionMapping(sessionId);
|
|
856
869
|
const canResume = !!currentSdkId;
|
|
@@ -877,8 +890,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
877
890
|
console.log(`[sdk] session=${sessionId} refresh failed — switching to ${nextAcc.id} (${label})`);
|
|
878
891
|
yield { type: "account_retry" as const, reason: "Switching account", accountId: nextAcc.id, accountLabel: label };
|
|
879
892
|
const switchEnv = this.buildQueryEnv(meta.projectPath, nextAcc);
|
|
880
|
-
|
|
881
|
-
q.close();
|
|
893
|
+
closeCurrentStream();
|
|
882
894
|
const { generator: switchGen, controller: switchCtrl } = createMessageChannel();
|
|
883
895
|
const currentSdkId = getSessionMapping(sessionId);
|
|
884
896
|
const canResume = !!currentSdkId;
|
|
@@ -1030,8 +1042,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1030
1042
|
// Re-resolve sdkId: the init event may have mapped ppmId → real SDK session_id
|
|
1031
1043
|
// after sdkId was originally resolved. Using the stale value would try to
|
|
1032
1044
|
// resume a non-existent session, causing the SDK to hang forever.
|
|
1033
|
-
|
|
1034
|
-
q.close();
|
|
1045
|
+
closeCurrentStream();
|
|
1035
1046
|
const { generator: authRetryGen, controller: authRetryCtrl } = createMessageChannel();
|
|
1036
1047
|
const currentSdkId = getSessionMapping(sessionId);
|
|
1037
1048
|
const canResume = !!currentSdkId;
|
|
@@ -1082,10 +1093,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1082
1093
|
}
|
|
1083
1094
|
yield { type: "error", message: `Rate limited. Auto-retrying in ${backoff / 1000}s... (${rateLimitRetryCount}/${MAX_RATE_LIMIT_RETRIES})` };
|
|
1084
1095
|
await new Promise((r) => setTimeout(r, backoff));
|
|
1085
|
-
// Close
|
|
1096
|
+
// Close current streaming session and recreate with (potentially new) account env.
|
|
1086
1097
|
// Re-resolve sdkId to pick up init-event mapping (see auth retry comment).
|
|
1087
|
-
|
|
1088
|
-
q.close();
|
|
1098
|
+
closeCurrentStream();
|
|
1089
1099
|
const rlRetryEnv = this.buildQueryEnv(meta.projectPath, account);
|
|
1090
1100
|
const { generator: rlRetryGen, controller: rlRetryCtrl } = createMessageChannel();
|
|
1091
1101
|
const rlCurrentSdkId = getSessionMapping(sessionId);
|
|
@@ -1183,8 +1193,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1183
1193
|
yield { type: "error", message: `Rate limited. Auto-retrying in ${backoff / 1000}s... (${rateLimitRetryCount}/${MAX_RATE_LIMIT_RETRIES})` };
|
|
1184
1194
|
await new Promise((r) => setTimeout(r, backoff));
|
|
1185
1195
|
// Re-resolve sdkId to pick up init-event mapping (see auth retry comment).
|
|
1186
|
-
|
|
1187
|
-
q.close();
|
|
1196
|
+
closeCurrentStream();
|
|
1188
1197
|
const rlRetryEnv = this.buildQueryEnv(meta.projectPath, account);
|
|
1189
1198
|
const { generator: rlRetryGen, controller: rlRetryCtrl } = createMessageChannel();
|
|
1190
1199
|
const rlCurrentSdkId2 = getSessionMapping(sessionId);
|
|
@@ -1216,8 +1225,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
1216
1225
|
console.log(`[sdk] 401 in result on account ${account.id} (${label}) — token refreshed, retrying`);
|
|
1217
1226
|
yield { type: "account_retry" as const, reason: "Token refreshed", accountId: refreshedAccount.id, accountLabel: label };
|
|
1218
1227
|
// Re-resolve sdkId to pick up init-event mapping (see auth retry comment).
|
|
1219
|
-
|
|
1220
|
-
q.close();
|
|
1228
|
+
closeCurrentStream();
|
|
1221
1229
|
const retryEnv = this.buildQueryEnv(meta.projectPath, refreshedAccount);
|
|
1222
1230
|
const { generator: authRetryGen2, controller: authRetryCtrl2 } = createMessageChannel();
|
|
1223
1231
|
const authCurrentSdkId2 = getSessionMapping(sessionId);
|
|
@@ -125,7 +125,9 @@ class AccountService {
|
|
|
125
125
|
|
|
126
126
|
/**
|
|
127
127
|
* Ensure the access token for an OAuth account is still fresh.
|
|
128
|
-
* If it's expired or about to expire (within
|
|
128
|
+
* If it's expired or about to expire (within 1 hour), refresh it proactively.
|
|
129
|
+
* The generous buffer prevents 401 errors mid-conversation — the SDK subprocess
|
|
130
|
+
* may run for a long time before the token is actually sent to the API.
|
|
129
131
|
* Returns the refreshed account with fresh tokens, or null if refresh failed.
|
|
130
132
|
*/
|
|
131
133
|
async ensureFreshToken(id: string): Promise<AccountWithTokens | null> {
|
|
@@ -135,9 +137,10 @@ class AccountService {
|
|
|
135
137
|
if (!acc.accessToken.startsWith("sk-ant-oat")) return acc;
|
|
136
138
|
if (!acc.expiresAt) return acc;
|
|
137
139
|
const nowS = Math.floor(Date.now() / 1000);
|
|
138
|
-
|
|
140
|
+
const REFRESH_BUFFER_S = 3600; // 1 hour — refresh proactively before expiry
|
|
141
|
+
if (acc.expiresAt - nowS > REFRESH_BUFFER_S) return acc; // still fresh
|
|
139
142
|
try {
|
|
140
|
-
console.log(`[accounts] Pre-flight refresh for ${acc.email ?? id} (expires in ${acc.expiresAt - nowS}s)`);
|
|
143
|
+
console.log(`[accounts] Pre-flight refresh for ${acc.email ?? id} (expires in ${acc.expiresAt - nowS}s, buffer=${REFRESH_BUFFER_S}s)`);
|
|
141
144
|
await this.refreshAccessToken(id, false);
|
|
142
145
|
return this.getWithTokens(id);
|
|
143
146
|
} catch (e) {
|
|
@@ -99,6 +99,7 @@ export function connect(opts: {
|
|
|
99
99
|
|
|
100
100
|
export function disconnect(): void {
|
|
101
101
|
shouldConnect = false;
|
|
102
|
+
reconnecting = false; // prevent stale flag from blocking future doConnect()
|
|
102
103
|
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
103
104
|
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
|
|
104
105
|
if (ws) {
|
|
@@ -122,8 +123,12 @@ export function onCommand(handler: CommandHandler): void {
|
|
|
122
123
|
commandHandler = handler;
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
/** Returns true if WS is authenticated and ready, or still in auth handshake */
|
|
125
127
|
export function isConnected(): boolean {
|
|
126
|
-
|
|
128
|
+
// Also treat "connecting/open but auth pending" as connected to prevent
|
|
129
|
+
// external monitors from killing a valid WS during the 500ms auth delay.
|
|
130
|
+
if (connected) return true;
|
|
131
|
+
return ws !== null && ws.readyState <= WebSocket.OPEN;
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
// ─── Internal ───────────────────────────────────────
|