@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 +5 -0
- package/dist/monitor.js +88 -48
- package/package.json +1 -1
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
|
-
//
|
|
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
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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.
|
|
138
|
-
|
|
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);
|