agent-relay-runner 0.91.0 → 0.91.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
- "version": "0.91.0",
3
+ "version": "0.91.2",
4
4
  "description": "Unified provider lifecycle runner for Agent Relay",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.91.0",
4
+ "version": "0.91.2",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -10,15 +10,27 @@ error="$(relay_json_string_field error_type "$payload")"
10
10
  [ -z "$error" ] && error="$(relay_json_string_field error "$payload")"
11
11
  reset_time="$(relay_json_number_field reset_time "$payload")"
12
12
  error_message="$(relay_json_string_field error_message "$payload")"
13
+ [ -z "$error_message" ] && error_message="$(relay_json_string_field error_details "$payload")"
14
+ [ -z "$error_message" ] && error_message="$(relay_json_string_field last_assistant_message "$payload")"
13
15
 
14
16
  # #286: a usage/rate-limit stall is transient and time-bounded — hold the agent
15
17
  # (providerState blocked) and auto-resume at reset, instead of silently going idle.
16
18
  # The disambiguator for billing_error is reset_time: a subscription rolling-window
17
19
  # limit carries one (and is auto-resumable); true credit exhaustion does not (fatal).
18
20
  case "$error" in
19
- rate_limit|overloaded)
21
+ rate_limit|overloaded|server_error|api_error)
20
22
  relay_post_rate_limit "$error" "$reset_time" "$error_message"
21
23
  ;;
24
+ unknown)
25
+ case "$error_message" in
26
+ *"529"*|*"Overloaded"*|*"overloaded"*|*"temporar"*|*"server-side"*|*"server error"*)
27
+ relay_post_rate_limit "$error" "$reset_time" "$error_message"
28
+ ;;
29
+ *)
30
+ relay_post_status_clearing_subagents idle
31
+ ;;
32
+ esac
33
+ ;;
22
34
  billing_error)
23
35
  case "$reset_time" in
24
36
  ''|*[!0-9]*) relay_post_status_clearing_subagents error ;;
@@ -559,7 +559,7 @@ export function claudeRateLimitStatus(text: string, sessionName?: string): Provi
559
559
  status: "idle",
560
560
  clear: ["subagent"],
561
561
  providerState: buildRateLimitProviderState({
562
- errorType: "session_limit",
562
+ errorType: parsed.errorType ?? "session_limit", kind: parsed.kind ?? "usage_limit",
563
563
  ...(parsed.resetAt ? { resetAt: parsed.resetAt } : {}),
564
564
  message: parsed.message,
565
565
  source: sessionName ? `claude-pane:${sessionName}` : "claude-pane",
package/src/rate-limit.ts CHANGED
@@ -17,6 +17,7 @@ const MAX_RESET_AHEAD_MS = 7 * 24 * 60 * 60 * 1000;
17
17
 
18
18
  interface RateLimitHoldInput {
19
19
  errorType?: string;
20
+ kind?: "usage_limit" | "transient_overload";
20
21
  /** Unix ms the limit window resets, when known. */
21
22
  resetAt?: number;
22
23
  message?: string;
@@ -27,12 +28,18 @@ interface RateLimitHoldInput {
27
28
  export function buildRateLimitProviderState(input: RateLimitHoldInput): Record<string, unknown> {
28
29
  const now = Date.now();
29
30
  const resetAt = typeof input.resetAt === "number" && Number.isFinite(input.resetAt) ? input.resetAt : undefined;
30
- const label = resetAt ? `usage limit · resets ${formatResetClock(resetAt)}` : "usage limit reached";
31
+ const kind = input.kind ?? rateLimitKindForError(input.errorType);
32
+ const label = kind === "transient_overload"
33
+ ? "provider overloaded"
34
+ : resetAt ? `usage limit · resets ${formatResetClock(resetAt)}` : "usage limit reached";
31
35
  return {
32
36
  state: "blocked",
33
37
  reason: RATE_LIMIT_BLOCK_REASON,
34
38
  label,
35
- recommendedAction: "Holding until the usage window resets; agent-relay auto-resumes the agent.",
39
+ recommendedAction: kind === "transient_overload"
40
+ ? "Holding briefly for provider overload; agent-relay will retry with bounded backoff."
41
+ : "Holding until the usage window resets; agent-relay auto-resumes the agent.",
42
+ kind,
36
43
  source: input.source ?? "claude",
37
44
  errorType: input.errorType ?? RATE_LIMIT_BLOCK_REASON,
38
45
  enteredAt: now,
@@ -42,6 +49,10 @@ export function buildRateLimitProviderState(input: RateLimitHoldInput): Record<s
42
49
  };
43
50
  }
44
51
 
52
+ function rateLimitKindForError(errorType: string | undefined): "usage_limit" | "transient_overload" {
53
+ return /^(overloaded|server_error|api_error|unknown|529|5\d\d)$/i.test(errorType ?? "") ? "transient_overload" : "usage_limit";
54
+ }
55
+
45
56
  /** Host-local HH:MM for the chat notice / badge label (matches how the CLI shows "resets 3:45pm"). */
46
57
  function formatResetClock(resetAtMs: number): string {
47
58
  try {
@@ -64,6 +75,9 @@ interface PaneRateLimit {
64
75
  message: string;
65
76
  /** Unix ms reset, when parseable from the pane (else undefined → resume falls back to poll). */
66
77
  resetAt?: number;
78
+ /** Usage limits wait for reset; transient provider overloads retry quickly. */
79
+ kind?: "usage_limit" | "transient_overload";
80
+ errorType?: string;
67
81
  }
68
82
 
69
83
  /**
@@ -75,6 +89,8 @@ interface PaneRateLimit {
75
89
  */
76
90
  export function parseClaudeRateLimitPane(text: string, nowMs: number = Date.now()): PaneRateLimit | null {
77
91
  if (!text) return null;
92
+ const overload = parseClaudeTransientOverloadPane(text);
93
+ if (overload) return overload;
78
94
  const limit = LIMIT_PHRASE_RE.exec(text);
79
95
  if (!limit) return null;
80
96
  const message = limitLineAt(text, limit.index);
@@ -95,6 +111,21 @@ export function parseClaudeRateLimitPane(text: string, nowMs: number = Date.now(
95
111
  return null;
96
112
  }
97
113
 
114
+ const TRANSIENT_API_ERROR_RE = /\bAPI Error:\s*((?:5\d\d)|529)\b[^\n]*(?:overload|server-side|temporar|try again|status\.claude\.com)?[^\n]*/i;
115
+ const TRANSIENT_5XX_RE = /\b(?:5(?:00|02|03|04|20|29)|529)\b[^\n]*(?:overload|server-side|temporar|try again|status\.claude\.com)\b[^\n]*/i;
116
+
117
+ function parseClaudeTransientOverloadPane(text: string): PaneRateLimit | null {
118
+ const match = TRANSIENT_API_ERROR_RE.exec(text) ?? TRANSIENT_5XX_RE.exec(text);
119
+ if (!match) return null;
120
+ const message = limitLineAt(text, match.index);
121
+ const code = match[1] ?? /\b(5\d\d|529)\b/.exec(match[0])?.[1];
122
+ return {
123
+ message: message || match[0].trim(),
124
+ kind: "transient_overload",
125
+ errorType: code === "529" ? "overloaded" : "server_error",
126
+ };
127
+ }
128
+
98
129
  export interface ClaudeRateLimitPaneGateState {
99
130
  consecutiveDetections: number;
100
131
  reported: boolean;
@@ -116,7 +147,8 @@ export function gatedClaudeRateLimitStatus(
116
147
  status: "idle",
117
148
  clear: ["subagent"],
118
149
  providerState: buildRateLimitProviderState({
119
- errorType: "session_limit",
150
+ errorType: parsed.errorType ?? "session_limit",
151
+ kind: parsed.kind ?? "usage_limit",
120
152
  ...(parsed.resetAt ? { resetAt: parsed.resetAt } : {}),
121
153
  message: parsed.message,
122
154
  source: options.sessionName ? `claude-pane:${options.sessionName}` : "claude-pane",