@tigorhutasuhut/claude-retry 0.1.5 → 0.1.7

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.
Files changed (2) hide show
  1. package/dist/monitor.js +48 -32
  2. package/package.json +1 -1
package/dist/monitor.js CHANGED
@@ -47,33 +47,29 @@ async function stepState(state, screenText, now, injectContinue, marginSeconds,
47
47
  const limited = match(screenText).limited;
48
48
  const { usage } = await resolveAccountUsage(snapshot, resolvePaneAccount, target);
49
49
  const marginMs = (marginSeconds ?? 60) * 1000;
50
- // 1. Banner gonelimit cleared / claude exited / user already continued / pane reused
50
+ // Banner absent → claude exited / user already continued / pane id reused → nothing to continue.
51
51
  if (!limited) {
52
52
  state.status = 'monitoring';
53
53
  state.waitUntil = 0;
54
54
  logger(`${label} wait abandoned (banner gone)`);
55
55
  return 'monitoring';
56
56
  }
57
- // 2. Account known and NOT limitedstale 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
57
+ // Account still limited with a known reset keep waitUntil aligned to the live reset time.
65
58
  if (usage !== undefined && usage.limited && usage.resetsAtMs !== null) {
66
59
  state.waitUntil = usage.resetsAtMs + marginMs;
67
60
  }
68
- // 4. Timer not elapsed keep waiting
69
- if (now < state.waitUntil) {
70
- return 'rate-limited';
61
+ // The limit is over when the account quota has cleared (early/real reset) OR the timer elapsed.
62
+ const accountCleared = usage !== undefined && !usage.limited;
63
+ const timerElapsed = now >= state.waitUntil;
64
+ if (accountCleared || timerElapsed) {
65
+ await injectContinue();
66
+ state.status = 'monitoring';
67
+ state.waitUntil = 0;
68
+ logger(`${label} reset reached — injected continue`);
69
+ return 'retried';
71
70
  }
72
- // 5. Elapsed + banner still present (+ account limited or unknown)inject
73
- await injectContinue();
74
- state.status = 'monitoring';
75
- state.waitUntil = 0;
76
- return 'retried';
71
+ // Still limited, before resetkeep waiting.
72
+ return 'rate-limited';
77
73
  }
78
74
  // state.status === 'monitoring'
79
75
  const result = match(screenText);
@@ -115,8 +111,7 @@ export async function tick(paneId, state, deps, marginSeconds, fallbackHours) {
115
111
  const screenText = await deps.capture(paneId);
116
112
  return stepState(state, screenText, deps.now(), () => deps.inject(paneId, 'continue'), marginSeconds, fallbackHours);
117
113
  }
118
- async function tickTarget(target, state, deps, marginSeconds, fallbackHours, snapshot) {
119
- const screenText = await deps.capture(target);
114
+ async function tickTarget(target, state, screenText, deps, marginSeconds, fallbackHours, snapshot) {
120
115
  return stepState(state, screenText, deps.now(), () => deps.inject(target, 'continue'), marginSeconds, fallbackHours, snapshot, deps.resolvePaneAccount, target, deps.log);
121
116
  }
122
117
  export async function runMonitor(paneId, deps, pollIntervalMs, marginSeconds, fallbackHours) {
@@ -150,16 +145,6 @@ export async function multiTick(states, deps, marginSeconds, fallbackHours) {
150
145
  log('scan failed: could not list sessions/panes (will retry)');
151
146
  return;
152
147
  }
153
- // Fetch account snapshot once per pass (swallow errors → undefined).
154
- let snapshot;
155
- if (deps.getAccountSnapshot !== undefined) {
156
- try {
157
- snapshot = await deps.getAccountSnapshot();
158
- }
159
- catch {
160
- snapshot = undefined;
161
- }
162
- }
163
148
  // Prune state for panes that no longer exist, using miss counter to tolerate
164
149
  // transient list-panes failures.
165
150
  const live = new Set(targets.map((t) => t.label));
@@ -179,7 +164,38 @@ export async function multiTick(states, deps, marginSeconds, fallbackHours) {
179
164
  log(targets.length === 0
180
165
  ? 'scan: no Claude panes found'
181
166
  : `scan: watching ${targets.length} Claude pane(s) [${targets.map((t) => t.label).join(', ')}]`);
167
+ // Capture each target's screen once, collecting successes into a map.
168
+ // Capture failures are logged and that pane is skipped this round.
169
+ const screens = new Map();
170
+ for (const target of targets) {
171
+ try {
172
+ screens.set(target.label, await deps.capture(target));
173
+ }
174
+ catch {
175
+ log(`${target.label} — capture error (skipped this round)`);
176
+ }
177
+ }
178
+ // Decide whether usage API is needed this pass:
179
+ // - any pane already in 'waiting' state (among current targets), OR
180
+ // - any captured screen has a limit banner.
181
+ const anyWaiting = targets.some((t) => states.get(t.label)?.status === 'waiting');
182
+ const anyBanner = [...screens.values()].some((s) => match(s).limited);
183
+ const needUsage = anyWaiting || anyBanner;
184
+ // Fetch account snapshot only when needed (swallow errors → undefined).
185
+ let snapshot;
186
+ if (needUsage && deps.getAccountSnapshot !== undefined) {
187
+ try {
188
+ snapshot = await deps.getAccountSnapshot();
189
+ }
190
+ catch {
191
+ snapshot = undefined;
192
+ }
193
+ }
182
194
  for (const target of targets) {
195
+ const screenText = screens.get(target.label);
196
+ // Skip panes whose capture failed this round.
197
+ if (screenText === undefined)
198
+ continue;
183
199
  let state = states.get(target.label);
184
200
  if (!state) {
185
201
  state = createState();
@@ -190,12 +206,12 @@ export async function multiTick(states, deps, marginSeconds, fallbackHours) {
190
206
  state.missCount = 0;
191
207
  const before = state.status;
192
208
  try {
193
- const status = await tickTarget(target, state, deps, marginSeconds, fallbackHours, snapshot);
209
+ const status = await tickTarget(target, state, screenText, deps, marginSeconds, fallbackHours, snapshot);
194
210
  logPaneStatus(log, target.label, before, state, status);
195
211
  }
196
212
  catch {
197
- // This pane's capture/inject failed — leave its state, keep going.
198
- log(`${target.label} — capture/inject error (skipped this round)`);
213
+ // This pane's inject failed — leave its state, keep going.
214
+ log(`${target.label} — inject error (skipped this round)`);
199
215
  }
200
216
  }
201
217
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tigorhutasuhut/claude-retry",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Monitor Claude CLI in a zellij pane, auto-inject continue on rate-limit",
5
5
  "type": "module",
6
6
  "license": "MIT",