@tigorhutasuhut/claude-retry 0.1.3 → 0.1.5

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/dist/monitor.d.ts CHANGED
@@ -26,6 +26,7 @@ export type MonitorStatus = 'monitoring' | 'rate-limited' | 'retried' | 'exited'
26
26
  export interface MonitorState {
27
27
  status: 'monitoring' | 'waiting';
28
28
  waitUntil: number;
29
+ missCount: number;
29
30
  }
30
31
  export declare function createState(): MonitorState;
31
32
  export declare function tick(paneId: string, state: MonitorState, deps: MonitorDeps, marginSeconds?: number, fallbackHours?: number): Promise<MonitorStatus>;
@@ -38,6 +39,10 @@ export declare function runMonitor(paneId: string, deps: MonitorDeps, pollInterv
38
39
  * target's label (session:paneId), and persists across calls. A failed
39
40
  * discovery or a single pane's capture/inject error is swallowed so one bad
40
41
  * pane never stops the others.
42
+ *
43
+ * Uses a miss counter (MAX_MISSES) so transient list-panes failures don't
44
+ * prune waiting state immediately — a pane must be absent for MAX_MISSES
45
+ * consecutive passes before its state is dropped.
41
46
  */
42
47
  export declare function multiTick(states: PaneStates, deps: MultiMonitorDeps, marginSeconds?: number, fallbackHours?: number): Promise<void>;
43
48
  export declare function runMultiMonitor(deps: MultiMonitorDeps, pollIntervalMs?: number, marginSeconds?: number, fallbackHours?: number): Promise<void>;
package/dist/monitor.js CHANGED
@@ -1,7 +1,36 @@
1
1
  import { match } from "./patterns.js";
2
2
  import { parseResetTime, calculateWaitMs } from "./time-parser.js";
3
+ const MAX_MISSES = 3;
3
4
  export function createState() {
4
- return { status: 'monitoring', waitUntil: 0 };
5
+ return { status: 'monitoring', waitUntil: 0, missCount: 0 };
6
+ }
7
+ /**
8
+ * Resolve which account dir (if any) applies to this pane, and return its usage.
9
+ * Mirrors the account-resolution logic previously inline in the monitoring branch.
10
+ */
11
+ async function resolveAccountUsage(snapshot, resolvePaneAccount, target) {
12
+ if (snapshot === undefined) {
13
+ return { dir: null, usage: undefined };
14
+ }
15
+ let dir = null;
16
+ if (snapshot.byDir.size === 1) {
17
+ dir = [...snapshot.byDir.keys()][0];
18
+ }
19
+ else {
20
+ const limitedDirs = [];
21
+ for (const [d, usage] of snapshot.byDir) {
22
+ if (usage.limited)
23
+ limitedDirs.push(d);
24
+ }
25
+ if (limitedDirs.length === 1) {
26
+ dir = limitedDirs[0];
27
+ }
28
+ else if (target !== undefined && resolvePaneAccount !== undefined) {
29
+ dir = await resolvePaneAccount(target, snapshot);
30
+ }
31
+ }
32
+ const usage = dir !== null ? snapshot.byDir.get(dir) : undefined;
33
+ return { dir, usage };
5
34
  }
6
35
  /**
7
36
  * Core state transition for one pane, given its current screen text.
@@ -12,11 +41,35 @@ export function createState() {
12
41
  * account-aware limit resolution. Single-pane callers omit these — text path only.
13
42
  */
14
43
  async function stepState(state, screenText, now, injectContinue, marginSeconds, fallbackHours, snapshot, resolvePaneAccount, target, log) {
44
+ const logger = log ?? (() => { });
45
+ const label = target?.label ?? 'pane';
15
46
  if (state.status === 'waiting') {
47
+ const limited = match(screenText).limited;
48
+ const { usage } = await resolveAccountUsage(snapshot, resolvePaneAccount, target);
49
+ const marginMs = (marginSeconds ?? 60) * 1000;
50
+ // 1. Banner gone → limit cleared / claude exited / user already continued / pane reused
51
+ if (!limited) {
52
+ state.status = 'monitoring';
53
+ state.waitUntil = 0;
54
+ logger(`${label} wait abandoned (banner gone)`);
55
+ return 'monitoring';
56
+ }
57
+ // 2. Account known and NOT limited → stale banner persisting → drop
58
+ if (usage !== undefined && !usage.limited) {
59
+ state.status = 'monitoring';
60
+ state.waitUntil = 0;
61
+ logger(`${label} wait abandoned (account not limited)`);
62
+ return 'monitoring';
63
+ }
64
+ // 3. Account known, still limited, with a fresh resetsAtMs → refresh waitUntil
65
+ if (usage !== undefined && usage.limited && usage.resetsAtMs !== null) {
66
+ state.waitUntil = usage.resetsAtMs + marginMs;
67
+ }
68
+ // 4. Timer not elapsed → keep waiting
16
69
  if (now < state.waitUntil) {
17
70
  return 'rate-limited';
18
71
  }
19
- // Wait period elapsed inject continue
72
+ // 5. Elapsed + banner still present (+ account limited or unknown) → inject
20
73
  await injectContinue();
21
74
  state.status = 'monitoring';
22
75
  state.waitUntil = 0;
@@ -25,53 +78,26 @@ async function stepState(state, screenText, now, injectContinue, marginSeconds,
25
78
  // state.status === 'monitoring'
26
79
  const result = match(screenText);
27
80
  if (result.limited) {
28
- const label = target?.label ?? 'pane';
29
- const logger = log ?? (() => { });
30
81
  // Tier 1: account-aware resolution when snapshot is available
31
82
  if (snapshot !== undefined) {
32
- let accountDir = null;
33
- if (snapshot.byDir.size === 1) {
34
- // Single account — always attributable, regardless of limited state
35
- // (covers both fresh-limit and stale-banner cases without resolver)
36
- accountDir = [...snapshot.byDir.keys()][0];
37
- }
38
- else {
39
- // Find dirs that are limited in snapshot
40
- const limitedDirs = [];
41
- for (const [dir, usage] of snapshot.byDir) {
42
- if (usage.limited)
43
- limitedDirs.push(dir);
44
- }
45
- if (limitedDirs.length === 1) {
46
- // Exactly one limited account — attribute banner to it
47
- accountDir = limitedDirs[0];
83
+ const { dir: accountDir, usage } = await resolveAccountUsage(snapshot, resolvePaneAccount, target);
84
+ if (accountDir !== null && usage !== undefined) {
85
+ if (!usage.limited) {
86
+ // Staleness gate: account is not limited → banner is stale → ignore
87
+ logger(`${label} stale banner ignored (account not limited)`);
88
+ return 'monitoring';
48
89
  }
49
- else if (target !== undefined && resolvePaneAccount !== undefined) {
50
- // Ambiguous (0 or 2+) try proc bridge (phase 2 stub, returns null)
51
- accountDir = await resolvePaneAccount(target, snapshot);
90
+ // Account confirmed limited use resetsAtMs if available
91
+ const marginMs = (marginSeconds ?? 60) * 1000;
92
+ if (usage.resetsAtMs !== null) {
93
+ state.waitUntil = usage.resetsAtMs + marginMs;
94
+ state.status = 'waiting';
95
+ logger(`${label} account ${accountDir} limited, reset ${new Date(usage.resetsAtMs).toISOString()}`);
96
+ return 'rate-limited';
52
97
  }
53
- }
54
- if (accountDir !== null) {
55
- const usage = snapshot.byDir.get(accountDir);
56
- if (usage !== undefined) {
57
- if (!usage.limited) {
58
- // Staleness gate: account is not limited → banner is stale → ignore
59
- logger(`${label} stale banner ignored (account not limited)`);
60
- return 'monitoring';
61
- }
62
- // Account confirmed limited — use resetsAtMs if available
63
- const marginMs = (marginSeconds ?? 60) * 1000;
64
- if (usage.resetsAtMs !== null) {
65
- state.waitUntil = usage.resetsAtMs + marginMs;
66
- state.status = 'waiting';
67
- logger(`${label} account ${accountDir} limited, reset ${new Date(usage.resetsAtMs).toISOString()}`);
68
- return 'rate-limited';
69
- }
70
- // resetsAtMs null — fall through to text parse for the time, but
71
- // we know account is limited so we don't need to gate on text
72
- // (fall through to tier 3 below)
73
- }
74
- // usage missing for this dir — fall through to tier 3
98
+ // resetsAtMs null — fall through to text parse for the time, but
99
+ // we know account is limited so we don't need to gate on text
100
+ // (fall through to tier 3 below)
75
101
  }
76
102
  // accountDir null or usage missing — fall through to tier 3
77
103
  }
@@ -108,6 +134,10 @@ export async function runMonitor(paneId, deps, pollIntervalMs, marginSeconds, fa
108
134
  * target's label (session:paneId), and persists across calls. A failed
109
135
  * discovery or a single pane's capture/inject error is swallowed so one bad
110
136
  * pane never stops the others.
137
+ *
138
+ * Uses a miss counter (MAX_MISSES) so transient list-panes failures don't
139
+ * prune waiting state immediately — a pane must be absent for MAX_MISSES
140
+ * consecutive passes before its state is dropped.
111
141
  */
112
142
  export async function multiTick(states, deps, marginSeconds, fallbackHours) {
113
143
  const log = deps.log ?? (() => { });
@@ -130,12 +160,20 @@ export async function multiTick(states, deps, marginSeconds, fallbackHours) {
130
160
  snapshot = undefined;
131
161
  }
132
162
  }
133
- // Prune state for panes that no longer exist.
163
+ // Prune state for panes that no longer exist, using miss counter to tolerate
164
+ // transient list-panes failures.
134
165
  const live = new Set(targets.map((t) => t.label));
135
166
  for (const key of [...states.keys()]) {
136
167
  if (!live.has(key)) {
137
- states.delete(key);
138
- log(`${key} gone — dropped from watch`);
168
+ const s = states.get(key);
169
+ s.missCount++;
170
+ if (s.missCount >= MAX_MISSES) {
171
+ states.delete(key);
172
+ log(`${key} gone — dropped after ${MAX_MISSES} misses`);
173
+ }
174
+ else {
175
+ log(`${key} missing (${s.missCount}/${MAX_MISSES}) — keeping state`);
176
+ }
139
177
  }
140
178
  }
141
179
  log(targets.length === 0
@@ -148,6 +186,8 @@ export async function multiTick(states, deps, marginSeconds, fallbackHours) {
148
186
  states.set(target.label, state);
149
187
  log(`${target.label} — new Claude pane, now watching`);
150
188
  }
189
+ // Reset miss counter for panes present this pass
190
+ state.missCount = 0;
151
191
  const before = state.status;
152
192
  try {
153
193
  const status = await tickTarget(target, state, deps, marginSeconds, fallbackHours, snapshot);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tigorhutasuhut/claude-retry",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Monitor Claude CLI in a zellij pane, auto-inject continue on rate-limit",
5
5
  "type": "module",
6
6
  "license": "MIT",