@hienlh/ppm 0.8.0 → 0.8.1

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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.1] - 2026-03-23
4
+
5
+ ### Fixed
6
+ - **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
7
+ - **SDK error extraction**: Use `errors: string[]` array per SDK spec instead of non-existent singular `error` field — previously swallowed error details
8
+ - **Assistant message error detection**: Handle `SDKAssistantMessage.error` field for `authentication_failed`, `billing_error`, `rate_limit`, `server_error` per SDK spec
9
+ - **Empty success detection**: Detect SDK returning `success` with 0 turns and no content as silent failure, surface guidance to user
10
+ - **Network error hints**: Add WSL-specific hints for ConnectionRefused, auth failures, and connectivity issues
11
+
3
12
  ## [0.8.0] - 2026-03-23
4
13
 
5
14
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -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
- const msg = String(e.error ?? e.error_message ?? e.message ?? "");
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 account = accountSelector.isEnabled() ? accountSelector.next() : null;
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,20 @@ 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
+ };
801
+ const hint = errorHints[assistantError] ?? `API error: ${assistantError}`;
802
+ console.error(`[sdk] session=${sessionId} assistant error: ${assistantError}`);
803
+ yield { type: "error", message: hint };
804
+ }
777
805
  const content = (msg as any).message?.content;
778
806
  if (Array.isArray(content)) {
779
807
  for (const block of content) {
@@ -875,9 +903,11 @@ export class ClaudeAgentSdkProvider implements AIProvider {
875
903
 
876
904
  // Surface non-success subtypes as errors so FE can display them
877
905
  if (subtype && subtype !== "success") {
878
- // Extract error detail from SDK result check multiple possible fields
879
- const sdkError = result.error ?? result.error_message ?? result.message ?? result.reason ?? "";
880
- const sdkDetail = typeof sdkError === "string" ? sdkError : JSON.stringify(sdkError);
906
+ // SDK error results use `errors: string[]` array (not singular `error`)
907
+ const errorsArr = Array.isArray(result.errors) ? result.errors : [];
908
+ const sdkDetail = errorsArr.length > 0
909
+ ? errorsArr.join("\n")
910
+ : (typeof result.error === "string" ? result.error : "");
881
911
  // Log full result for debugging (truncated at 2000 chars)
882
912
  console.error(`[sdk] result error: subtype=${subtype} turns=${result.num_turns ?? 0} detail=${sdkDetail || "(none)"}`);
883
913
  console.error(`[sdk] result full dump: ${JSON.stringify(result).slice(0, 2000)}`);
@@ -887,13 +917,35 @@ export class ClaudeAgentSdkProvider implements AIProvider {
887
917
  error_during_execution: "Agent encountered an error during execution.",
888
918
  };
889
919
  const baseMsg = errorMessages[subtype] ?? `Agent stopped: ${subtype}`;
890
- const fullMsg = sdkDetail ? `${baseMsg}\n${sdkDetail}` : baseMsg;
920
+ // Add specific hints for common network/auth errors
921
+ const detailLower = sdkDetail.toLowerCase();
922
+ let hint = "";
923
+ if (detailLower.includes("connectionrefused") || detailLower.includes("connection refused") || detailLower.includes("econnrefused")) {
924
+ 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).";
925
+ } else if (detailLower.includes("unable to connect")) {
926
+ hint = "\n\nHint: Network connectivity issue. Check your internet connection and firewall/proxy settings.";
927
+ } else if (detailLower.includes("401") || detailLower.includes("unauthorized") || detailLower.includes("invalid api key")) {
928
+ hint = "\n\nHint: Authentication failed. Try re-adding your account in Settings → Accounts.";
929
+ }
930
+ const fullMsg = sdkDetail ? `${baseMsg}\n${sdkDetail}${hint}` : baseMsg;
891
931
  yield {
892
932
  type: "error",
893
933
  message: fullMsg,
894
934
  };
895
935
  }
896
936
 
937
+ // Detect empty/suspicious success — SDK returned "success" but no real assistant content
938
+ if ((!subtype || subtype === "success") && (result.num_turns ?? 0) === 0 && !assistantContent) {
939
+ // SDK success result has `result: string` containing final text
940
+ const resultText = typeof result.result === "string" ? result.result : "";
941
+ console.warn(`[sdk] session=${sessionId} result success but 0 turns, no assistant content, result="${resultText.slice(0, 200)}"`);
942
+ console.warn(`[sdk] result dump: ${JSON.stringify(result).slice(0, 2000)}`);
943
+ const hint = resultText
944
+ ? `Claude returned: "${resultText}"\nThis may indicate a session or connection issue. Try creating a new chat session.`
945
+ : "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.";
946
+ yield { type: "error", message: hint };
947
+ }
948
+
897
949
  // Store subtype and numTurns for the done event
898
950
  resultSubtype = subtype;
899
951
  resultNumTurns = result.num_turns as number | undefined;
@@ -961,7 +1013,8 @@ export class ClaudeAgentSdkProvider implements AIProvider {
961
1013
  if (retryMsg.type === "result") {
962
1014
  const r = retryMsg as any;
963
1015
  if (r.subtype && r.subtype !== "success") {
964
- yield { type: "error", message: r.error ?? `Agent stopped: ${r.subtype}` };
1016
+ const retryErrors = Array.isArray(r.errors) ? r.errors.join("\n") : "";
1017
+ yield { type: "error", message: retryErrors || `Agent stopped: ${r.subtype}` };
965
1018
  }
966
1019
  resultSubtype = r.subtype;
967
1020
  resultNumTurns = r.num_turns;
@@ -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) return null;
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
- return accountService.getWithTokens(pickedId);
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 }