@hienlh/ppm 0.8.0 → 0.8.2
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,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.2] - 2026-03-23
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Heartbeat during SDK connection**: Keep "Connecting... (Xs)" indicator alive until real content arrives — previously stopped by `account_info` event, causing misleading "thinking" state during 3-minute SDK failures
|
|
7
|
+
- **Unknown API error hint**: "API error: unknown" now shows actionable guidance (check connectivity, re-add account, new session)
|
|
8
|
+
|
|
9
|
+
## [0.8.1] - 2026-03-23
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- **Silent timeout on account decrypt failure**: When all account tokens fail decryption (e.g. different machine key in WSL), now shows actionable error instead of 120s silent timeout
|
|
13
|
+
- **SDK error extraction**: Use `errors: string[]` array per SDK spec instead of non-existent singular `error` field — previously swallowed error details
|
|
14
|
+
- **Assistant message error detection**: Handle `SDKAssistantMessage.error` field for `authentication_failed`, `billing_error`, `rate_limit`, `server_error` per SDK spec
|
|
15
|
+
- **Empty success detection**: Detect SDK returning `success` with 0 turns and no content as silent failure, surface guidance to user
|
|
16
|
+
- **Network error hints**: Add WSL-specific hints for ConnectionRefused, auth failures, and connectivity issues
|
|
17
|
+
|
|
3
18
|
## [0.8.0] - 2026-03-23
|
|
4
19
|
|
|
5
20
|
### Added
|
package/package.json
CHANGED
|
@@ -106,7 +106,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
106
106
|
if (!event || typeof event !== "object") return null;
|
|
107
107
|
const e = event as Record<string, unknown>;
|
|
108
108
|
if (e.type === "result" && e.subtype === "error_during_execution") {
|
|
109
|
-
|
|
109
|
+
// SDK uses `errors: string[]` array for error details
|
|
110
|
+
const errorsArr = Array.isArray(e.errors) ? (e.errors as string[]).join(" ") : "";
|
|
111
|
+
const msg = errorsArr || String(e.error ?? "");
|
|
110
112
|
if (msg.includes("429") || msg.toLowerCase().includes("rate limit") || msg.toLowerCase().includes("overloaded")) return 429;
|
|
111
113
|
if (msg.includes("401") || msg.toLowerCase().includes("unauthorized") || msg.toLowerCase().includes("invalid api key")) return 401;
|
|
112
114
|
}
|
|
@@ -533,7 +535,19 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
533
535
|
|
|
534
536
|
// Account-based auth injection (multi-account mode)
|
|
535
537
|
// Fallback to existing env (ANTHROPIC_API_KEY) when no accounts configured.
|
|
536
|
-
const
|
|
538
|
+
const accountsEnabled = accountSelector.isEnabled();
|
|
539
|
+
const account = accountsEnabled ? accountSelector.next() : null;
|
|
540
|
+
if (accountsEnabled && !account) {
|
|
541
|
+
// All accounts in DB but none usable
|
|
542
|
+
const reason = accountSelector.lastFailReason;
|
|
543
|
+
const hint = reason === "all_decrypt_failed"
|
|
544
|
+
? "Account tokens were encrypted with a different machine key. Re-add your accounts in Settings, or copy ~/.ppm/account.key from the original machine."
|
|
545
|
+
: "All accounts are disabled or in cooldown. Check Settings → Accounts.";
|
|
546
|
+
console.error(`[sdk] session=${sessionId} account auth failed (${reason}): ${hint}`);
|
|
547
|
+
yield { type: "error" as const, message: `Authentication failed: ${hint}` };
|
|
548
|
+
yield { type: "done" as const, sessionId, resultSubtype: "error_auth" };
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
537
551
|
if (account) {
|
|
538
552
|
console.log(`[sdk] Using account ${account.id} (${account.email ?? "no-email"})`);
|
|
539
553
|
yield { type: "account_info" as const, accountId: account.id, accountLabel: account.label ?? account.email ?? "Unknown" };
|
|
@@ -774,6 +788,21 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
774
788
|
|
|
775
789
|
// Full assistant message
|
|
776
790
|
if (msg.type === "assistant") {
|
|
791
|
+
// SDK assistant messages can carry an error field for auth/billing/rate-limit failures
|
|
792
|
+
const assistantError = (msg as any).error as string | undefined;
|
|
793
|
+
if (assistantError) {
|
|
794
|
+
const errorHints: Record<string, string> = {
|
|
795
|
+
authentication_failed: "API authentication failed. Check your account credentials in Settings → Accounts.",
|
|
796
|
+
billing_error: "Billing error on this account. Check your subscription status.",
|
|
797
|
+
rate_limit: "Rate limited by the API. Please wait and try again.",
|
|
798
|
+
invalid_request: "Invalid request sent to the API.",
|
|
799
|
+
server_error: "Anthropic API server error. Try again shortly.",
|
|
800
|
+
unknown: "API connection failed. Possible causes: network unreachable, expired OAuth token, or API outage. Try: 1) Check connectivity (`curl -s https://api.anthropic.com`), 2) Re-add account in Settings, 3) Create a new chat session.",
|
|
801
|
+
};
|
|
802
|
+
const hint = errorHints[assistantError] ?? `API error: ${assistantError}`;
|
|
803
|
+
console.error(`[sdk] session=${sessionId} assistant error: ${assistantError}`);
|
|
804
|
+
yield { type: "error", message: hint };
|
|
805
|
+
}
|
|
777
806
|
const content = (msg as any).message?.content;
|
|
778
807
|
if (Array.isArray(content)) {
|
|
779
808
|
for (const block of content) {
|
|
@@ -875,9 +904,11 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
875
904
|
|
|
876
905
|
// Surface non-success subtypes as errors so FE can display them
|
|
877
906
|
if (subtype && subtype !== "success") {
|
|
878
|
-
//
|
|
879
|
-
const
|
|
880
|
-
const sdkDetail =
|
|
907
|
+
// SDK error results use `errors: string[]` array (not singular `error`)
|
|
908
|
+
const errorsArr = Array.isArray(result.errors) ? result.errors : [];
|
|
909
|
+
const sdkDetail = errorsArr.length > 0
|
|
910
|
+
? errorsArr.join("\n")
|
|
911
|
+
: (typeof result.error === "string" ? result.error : "");
|
|
881
912
|
// Log full result for debugging (truncated at 2000 chars)
|
|
882
913
|
console.error(`[sdk] result error: subtype=${subtype} turns=${result.num_turns ?? 0} detail=${sdkDetail || "(none)"}`);
|
|
883
914
|
console.error(`[sdk] result full dump: ${JSON.stringify(result).slice(0, 2000)}`);
|
|
@@ -887,13 +918,35 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
887
918
|
error_during_execution: "Agent encountered an error during execution.",
|
|
888
919
|
};
|
|
889
920
|
const baseMsg = errorMessages[subtype] ?? `Agent stopped: ${subtype}`;
|
|
890
|
-
|
|
921
|
+
// Add specific hints for common network/auth errors
|
|
922
|
+
const detailLower = sdkDetail.toLowerCase();
|
|
923
|
+
let hint = "";
|
|
924
|
+
if (detailLower.includes("connectionrefused") || detailLower.includes("connection refused") || detailLower.includes("econnrefused")) {
|
|
925
|
+
hint = "\n\nHint: Cannot reach Anthropic API. If running in WSL, check DNS/proxy settings (e.g. `curl -s https://api.anthropic.com` from WSL terminal).";
|
|
926
|
+
} else if (detailLower.includes("unable to connect")) {
|
|
927
|
+
hint = "\n\nHint: Network connectivity issue. Check your internet connection and firewall/proxy settings.";
|
|
928
|
+
} else if (detailLower.includes("401") || detailLower.includes("unauthorized") || detailLower.includes("invalid api key")) {
|
|
929
|
+
hint = "\n\nHint: Authentication failed. Try re-adding your account in Settings → Accounts.";
|
|
930
|
+
}
|
|
931
|
+
const fullMsg = sdkDetail ? `${baseMsg}\n${sdkDetail}${hint}` : baseMsg;
|
|
891
932
|
yield {
|
|
892
933
|
type: "error",
|
|
893
934
|
message: fullMsg,
|
|
894
935
|
};
|
|
895
936
|
}
|
|
896
937
|
|
|
938
|
+
// Detect empty/suspicious success — SDK returned "success" but no real assistant content
|
|
939
|
+
if ((!subtype || subtype === "success") && (result.num_turns ?? 0) === 0 && !assistantContent) {
|
|
940
|
+
// SDK success result has `result: string` containing final text
|
|
941
|
+
const resultText = typeof result.result === "string" ? result.result : "";
|
|
942
|
+
console.warn(`[sdk] session=${sessionId} result success but 0 turns, no assistant content, result="${resultText.slice(0, 200)}"`);
|
|
943
|
+
console.warn(`[sdk] result dump: ${JSON.stringify(result).slice(0, 2000)}`);
|
|
944
|
+
const hint = resultText
|
|
945
|
+
? `Claude returned: "${resultText}"\nThis may indicate a session or connection issue. Try creating a new chat session.`
|
|
946
|
+
: "Claude returned no response (0 turns). This usually means the API connection failed silently. Check that `claude` CLI works in your terminal, or try creating a new chat session.";
|
|
947
|
+
yield { type: "error", message: hint };
|
|
948
|
+
}
|
|
949
|
+
|
|
897
950
|
// Store subtype and numTurns for the done event
|
|
898
951
|
resultSubtype = subtype;
|
|
899
952
|
resultNumTurns = result.num_turns as number | undefined;
|
|
@@ -961,7 +1014,8 @@ export class ClaudeAgentSdkProvider implements AIProvider {
|
|
|
961
1014
|
if (retryMsg.type === "result") {
|
|
962
1015
|
const r = retryMsg as any;
|
|
963
1016
|
if (r.subtype && r.subtype !== "success") {
|
|
964
|
-
|
|
1017
|
+
const retryErrors = Array.isArray(r.errors) ? r.errors.join("\n") : "";
|
|
1018
|
+
yield { type: "error", message: retryErrors || `Agent stopped: ${r.subtype}` };
|
|
965
1019
|
}
|
|
966
1020
|
resultSubtype = r.subtype;
|
|
967
1021
|
resultNumTurns = r.num_turns;
|
package/src/server/ws/chat.ts
CHANGED
|
@@ -146,8 +146,12 @@ async function runStreamLoop(sessionId: string, providerId: string, content: str
|
|
|
146
146
|
const ev = event as any;
|
|
147
147
|
const evType = ev.type ?? "unknown";
|
|
148
148
|
|
|
149
|
-
// First event
|
|
150
|
-
|
|
149
|
+
// First content event — stop heartbeat, switch to streaming status.
|
|
150
|
+
// Skip metadata events (account_info, streaming_status) that arrive before
|
|
151
|
+
// the SDK subprocess actually produces output — keeps heartbeat + "connecting"
|
|
152
|
+
// indicator alive until real content flows.
|
|
153
|
+
const isMetadataEvent = evType === "account_info" || evType === "streaming_status";
|
|
154
|
+
if (!firstEventReceived && !isMetadataEvent) {
|
|
151
155
|
firstEventReceived = true;
|
|
152
156
|
const waitMs = Date.now() - startTime;
|
|
153
157
|
console.log(`[chat] session=${sessionId} first SDK event after ${waitMs}ms: type=${evType}`);
|
|
@@ -36,11 +36,20 @@ class AccountSelectorService {
|
|
|
36
36
|
setConfigValue(MAX_RETRY_CONFIG_KEY, String(n));
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/** Reason for the last null return from next() */
|
|
40
|
+
private _lastFailReason: "none" | "no_active" | "all_decrypt_failed" = "none";
|
|
41
|
+
|
|
42
|
+
/** Why the last next() call returned null */
|
|
43
|
+
get lastFailReason(): "none" | "no_active" | "all_decrypt_failed" {
|
|
44
|
+
return this._lastFailReason;
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
/**
|
|
40
48
|
* Pick next available account (skips cooldown/disabled).
|
|
41
49
|
* Returns null if no active accounts available.
|
|
42
50
|
*/
|
|
43
51
|
next(): AccountWithTokens | null {
|
|
52
|
+
this._lastFailReason = "none";
|
|
44
53
|
const now = Math.floor(Date.now() / 1000);
|
|
45
54
|
const allAccounts = accountService.list();
|
|
46
55
|
|
|
@@ -53,7 +62,10 @@ class AccountSelectorService {
|
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
const active = accountService.list().filter((a) => a.status === "active");
|
|
56
|
-
if (active.length === 0)
|
|
65
|
+
if (active.length === 0) {
|
|
66
|
+
this._lastFailReason = "no_active";
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
57
69
|
|
|
58
70
|
let pickedId: string;
|
|
59
71
|
if (this.getStrategy() === "fill-first") {
|
|
@@ -66,7 +78,11 @@ class AccountSelectorService {
|
|
|
66
78
|
this.cursor = (this.cursor + 1) % active.length;
|
|
67
79
|
}
|
|
68
80
|
this._lastPickedId = pickedId;
|
|
69
|
-
|
|
81
|
+
const result = accountService.getWithTokens(pickedId);
|
|
82
|
+
if (!result) {
|
|
83
|
+
this._lastFailReason = "all_decrypt_failed";
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
70
86
|
}
|
|
71
87
|
|
|
72
88
|
/** Called when account receives 429 — apply exponential backoff */
|
package/src/types/chat.ts
CHANGED
|
@@ -79,7 +79,8 @@ export type ResultSubtype =
|
|
|
79
79
|
| "success"
|
|
80
80
|
| "error_max_turns"
|
|
81
81
|
| "error_max_budget_usd"
|
|
82
|
-
| "error_during_execution"
|
|
82
|
+
| "error_during_execution"
|
|
83
|
+
| "error_auth";
|
|
83
84
|
|
|
84
85
|
export type ChatEvent =
|
|
85
86
|
| { type: "text"; content: string; parentToolUseId?: string }
|