@codizelabs/agent-rewake 0.4.0 → 0.6.0
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.
|
@@ -76,6 +76,104 @@ export function blockingKind(rateLimits, now) {
|
|
|
76
76
|
)?.kind;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// ---- Warnings before a limit: Claude Code's own usage figures, no network, no sign-in.
|
|
80
|
+
|
|
81
|
+
/** Percent used at which a window is warned about, lowest first. */
|
|
82
|
+
const WARN_AT = [80, 95];
|
|
83
|
+
/** Samples kept per window, and the least time they must span before a pace is trusted. */
|
|
84
|
+
const SAMPLE_CAP = 12;
|
|
85
|
+
const MIN_SPAN_MS = 10 * MINUTE;
|
|
86
|
+
/** Only readings this recent give the pace. */
|
|
87
|
+
const RECENT_MS = HOUR;
|
|
88
|
+
/** A pace warning comes when the projected limit is this near. */
|
|
89
|
+
const PACE_HORIZON_MS = 45 * MINUTE;
|
|
90
|
+
/** Two reset times closer than this are the same window (the figures can differ by a moment). */
|
|
91
|
+
const SAME_WINDOW_MS = 30 * MINUTE;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Milliseconds until the window reaches 100, from its samples ({ t, p }: time, percent used), or
|
|
95
|
+
* undefined when the pace is unknown: the figure has not moved since the newest sample (so idle
|
|
96
|
+
* time never counts as a pace), fewer than two samples from the last hour, those less than 10
|
|
97
|
+
* minutes apart, or no rise.
|
|
98
|
+
*/
|
|
99
|
+
export function timeToLimit(samples, now) {
|
|
100
|
+
const recent = samples.filter((s) => s.t >= now - RECENT_MS);
|
|
101
|
+
const first = recent[0];
|
|
102
|
+
const last = recent.at(-1);
|
|
103
|
+
if (!first || !last || last.t !== now || last.t - first.t < MIN_SPAN_MS) return undefined;
|
|
104
|
+
const rate = (last.p - first.p) / (last.t - first.t);
|
|
105
|
+
return rate > 0 ? (100 - last.p) / rate : undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The estimate as a range of whole minutes in fives, about 0.7x to 1.4x: [20, 40]. */
|
|
109
|
+
export function paceRange(ms) {
|
|
110
|
+
const five = (x) => Math.max(5, Math.round(x / (5 * MINUTE)) * 5);
|
|
111
|
+
const lo = five(ms * 0.7);
|
|
112
|
+
return [lo, Math.max(lo + 5, five(ms * 1.4))];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* One window's reading, given what was kept for it (`prev`: { resetsAt, samples, high, pace },
|
|
117
|
+
* or undefined). Returns undefined when there is nothing to track (a spending cap, no reset ahead,
|
|
118
|
+
* a full window: the limit flow takes over), else { next, level?, pace? }: the state to keep, the
|
|
119
|
+
* threshold newly crossed (only the highest if it jumped past both) and the pace range to warn
|
|
120
|
+
* with. A threshold is warned once per window, the pace once per window and never together with
|
|
121
|
+
* a threshold (it waits for the next reading).
|
|
122
|
+
*/
|
|
123
|
+
export function usageStep(prev, w, now) {
|
|
124
|
+
const reset = typeof w.resetsAt === "string" ? Date.parse(w.resetsAt) : Number.NaN;
|
|
125
|
+
const p = w.percentUsed;
|
|
126
|
+
if (BILLING_KINDS.has(w.kind) || !Number.isFinite(reset) || reset <= now) return undefined;
|
|
127
|
+
if (!(p >= 0 && p < 100)) return undefined;
|
|
128
|
+
const same = prev && Math.abs(prev.resetsAt - reset) < SAME_WINDOW_MS;
|
|
129
|
+
const base = same ? prev : { resetsAt: reset, samples: [], high: 0, pace: false };
|
|
130
|
+
const last = base.samples.at(-1);
|
|
131
|
+
const samples =
|
|
132
|
+
last?.p === p ? base.samples : [...(last && p < last.p ? [] : base.samples), { t: now, p }];
|
|
133
|
+
const kept = samples.slice(-SAMPLE_CAP);
|
|
134
|
+
const level = WARN_AT.filter((l) => p >= l && l > base.high).pop();
|
|
135
|
+
const ms = level === undefined && !base.pace ? timeToLimit(kept, now) : undefined;
|
|
136
|
+
const pace =
|
|
137
|
+
ms !== undefined && ms <= PACE_HORIZON_MS && now + ms < reset ? paceRange(ms) : undefined;
|
|
138
|
+
return {
|
|
139
|
+
next: {
|
|
140
|
+
resetsAt: reset, // the latest, so a slowly drifting time can't make the window look new
|
|
141
|
+
samples: kept,
|
|
142
|
+
high: level ?? base.high,
|
|
143
|
+
pace: base.pace || pace !== undefined,
|
|
144
|
+
},
|
|
145
|
+
level,
|
|
146
|
+
pace,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** "5-hour limit", "7-day limit", "7-day opus limit", "some kind limit". */
|
|
151
|
+
export function limitName(kind) {
|
|
152
|
+
const m = /^seven_day_(.+)$/.exec(kind);
|
|
153
|
+
const name =
|
|
154
|
+
kind === "five_hour"
|
|
155
|
+
? "5-hour"
|
|
156
|
+
: kind === "seven_day"
|
|
157
|
+
? "7-day"
|
|
158
|
+
: m
|
|
159
|
+
? `7-day ${m[1].replace(/_/g, " ")}`
|
|
160
|
+
: String(kind).replace(/_/g, " ");
|
|
161
|
+
return `${name} limit`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** `reset` is the reset time as words: "at 3:00 AM today", "on Friday at 9:00 AM". */
|
|
165
|
+
export function levelText(kind, percent, reset) {
|
|
166
|
+
const head = `${limitName(kind)}: ${Math.floor(percent)}% used, as of Claude Code's last reply. It resets ${reset}.`;
|
|
167
|
+
// Compacting is a model call too, so it is only hinted at below 95%.
|
|
168
|
+
return percent >= 95
|
|
169
|
+
? `${head} A good moment to wrap up or save what you are doing.`
|
|
170
|
+
: `${head} If you are in the middle of a task, /compact now can make the rest last longer (compacting also uses some).`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function paceText(kind, [lo, hi], reset) {
|
|
174
|
+
return `At this pace you reach the ${limitName(kind)} in about ${lo} to ${hi} minutes, before it resets ${reset}.`;
|
|
175
|
+
}
|
|
176
|
+
|
|
79
177
|
/**
|
|
80
178
|
* Claude Code's own panels, by `CLAUDE_CODE_ENTRYPOINT`: the VS Code extension (also in Cursor and
|
|
81
179
|
* other VS Code builds) and the desktop app. They run Claude Code through the Agent SDK, so the
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
// offered (not answered) | armed | sent
|
|
14
14
|
// claim: { by, at }, the process sending the armed continue right now
|
|
15
15
|
// sched:<id> [{ at, text }]
|
|
16
|
+
// usage:<dir> { <kind>: { resetsAt, samples: [{ t, p }], high, pace } }, what the warnings
|
|
17
|
+
// before a limit have said. The figures are the account's, not the session's, so
|
|
18
|
+
// the key is the config dir (CLAUDE_CONFIG_DIR, which is one sign-in): a restart or
|
|
19
|
+
// another session of the same account doesn't repeat a warning.
|
|
16
20
|
// A copy of each limit record (no message text) goes to Rewake's shared state folder, so
|
|
17
21
|
// `agent-rewake doctor` and the schedules page can show it, and so Rewake can continue the session
|
|
18
22
|
// headless if Claude Code closes before the limit is answered (it adds `open`, the Claude Code
|
|
@@ -34,6 +38,7 @@ import {
|
|
|
34
38
|
helpText,
|
|
35
39
|
holdArgv,
|
|
36
40
|
LINUX_MAINS,
|
|
41
|
+
levelText,
|
|
37
42
|
limitOf,
|
|
38
43
|
MAX_REARMS,
|
|
39
44
|
MAX_REHITS,
|
|
@@ -42,6 +47,7 @@ import {
|
|
|
42
47
|
mustAsk,
|
|
43
48
|
nativeLikely,
|
|
44
49
|
notHere,
|
|
50
|
+
paceText,
|
|
45
51
|
parseCommand,
|
|
46
52
|
parseProcess,
|
|
47
53
|
parseProgram,
|
|
@@ -54,6 +60,7 @@ import {
|
|
|
54
60
|
sessionVars,
|
|
55
61
|
splitWhen,
|
|
56
62
|
surfaceOf,
|
|
63
|
+
usageStep,
|
|
57
64
|
WAITING_EXPIRES_MS,
|
|
58
65
|
WAKE_HORIZON_MS,
|
|
59
66
|
WAKE_PROBE,
|
|
@@ -106,6 +113,10 @@ const ME = Math.random().toString(36).slice(2);
|
|
|
106
113
|
const CLAIM_MS = 5 * MINUTE;
|
|
107
114
|
/** Rewake's keep-awake setting: "plugged-in" (default), "always" or "never". */
|
|
108
115
|
let keepAwake = "plugged-in";
|
|
116
|
+
/** Rewake's usage-warnings setting: "on" (default) or "off". */
|
|
117
|
+
let usageWarnings = "on";
|
|
118
|
+
/** CLAUDE_CONFIG_DIR, which names the account's warnings in $.store ("" for the default). */
|
|
119
|
+
let configDir = "";
|
|
109
120
|
/** { kind, pid, program }: Claude Code's PID and the hold program (macOS, Linux with systemd-inhibit, Windows); null where there is none. */
|
|
110
121
|
let wakeTarget;
|
|
111
122
|
/** The running hold: { key, stream }. */
|
|
@@ -131,6 +142,8 @@ async function loadConfig($) {
|
|
|
131
142
|
if (["on", "off", "ask"].includes(settings.newThreads)) sharedAuto = settings.newThreads;
|
|
132
143
|
if (["plugged-in", "always", "never"].includes(settings.keepAwake))
|
|
133
144
|
keepAwake = settings.keepAwake;
|
|
145
|
+
if (settings.usageWarnings === "on" || settings.usageWarnings === "off")
|
|
146
|
+
usageWarnings = settings.usageWarnings;
|
|
134
147
|
} catch {
|
|
135
148
|
// No settings saved yet: the default clock.
|
|
136
149
|
}
|
|
@@ -146,7 +159,7 @@ async function loadSessionInfo($) {
|
|
|
146
159
|
// SETTINGS_VARS and KEY_VARS; a test in Rewake's repository compares them with these).
|
|
147
160
|
const read = (get) => get.catch(() => undefined);
|
|
148
161
|
const values = {
|
|
149
|
-
CLAUDE_CONFIG_DIR:
|
|
162
|
+
CLAUDE_CONFIG_DIR: configDir, // read once, at session start
|
|
150
163
|
CLAUDE_CODE_PROJECT_DIR_NAME: await read($.env.get("CLAUDE_CODE_PROJECT_DIR_NAME")),
|
|
151
164
|
ANTHROPIC_BASE_URL: await read($.env.get("ANTHROPIC_BASE_URL")),
|
|
152
165
|
ANTHROPIC_MODEL: await read($.env.get("ANTHROPIC_MODEL")),
|
|
@@ -494,6 +507,26 @@ async function onMeasure($, rateLimits) {
|
|
|
494
507
|
);
|
|
495
508
|
}
|
|
496
509
|
|
|
510
|
+
/** Toasts at 80% and 95% of a usage window, and when the pace would reach the limit soon. */
|
|
511
|
+
async function warnUsage($, rateLimits) {
|
|
512
|
+
if (usageWarnings === "off" || !personAtPrompt({ isInteractive, entrypoint })) return;
|
|
513
|
+
const now = await $.clock.now();
|
|
514
|
+
const key = `usage:${configDir}`;
|
|
515
|
+
const state = (await $.store.get(key)) ?? {};
|
|
516
|
+
const toasts = [];
|
|
517
|
+
for (const w of rateLimits ?? []) {
|
|
518
|
+
const step = w && usageStep(state[w.kind], w, now);
|
|
519
|
+
if (!step) continue;
|
|
520
|
+
state[w.kind] = step.next;
|
|
521
|
+
const reset = atWhen(step.next.resetsAt, now);
|
|
522
|
+
if (step.level !== undefined) toasts.push(levelText(w.kind, w.percentUsed, reset));
|
|
523
|
+
else if (step.pace) toasts.push(paceText(w.kind, step.pace, reset));
|
|
524
|
+
}
|
|
525
|
+
// Saved before showing: a failure while showing must not make the same warning come back.
|
|
526
|
+
await $.store.set(key, state);
|
|
527
|
+
for (const text of toasts) $.ui.toast(text);
|
|
528
|
+
}
|
|
529
|
+
|
|
497
530
|
async function onNotification($, e) {
|
|
498
531
|
const id = e.session_id;
|
|
499
532
|
const ep = await $.store.get(limitKey(id));
|
|
@@ -859,6 +892,7 @@ export function register(on) {
|
|
|
859
892
|
isInteractive = e.isInteractive;
|
|
860
893
|
sessionCwd = e.cwd;
|
|
861
894
|
entrypoint = await $.env.get("CLAUDE_CODE_ENTRYPOINT").catch(() => undefined);
|
|
895
|
+
configDir = (await $.env.get("CLAUDE_CONFIG_DIR").catch(() => undefined)) ?? "";
|
|
862
896
|
inert = Boolean(await $.env.get("AGENT_REWAKE_FIRE").catch(() => undefined));
|
|
863
897
|
if (inert) return next(e);
|
|
864
898
|
surface = surfaceOf({ surface: e.surface, entrypoint });
|
|
@@ -905,7 +939,10 @@ export function register(on) {
|
|
|
905
939
|
}).catch(passOn);
|
|
906
940
|
|
|
907
941
|
on("session.measure", ($, e, next) => {
|
|
908
|
-
if (!inert && e.changed.includes("rateLimits"))
|
|
942
|
+
if (!inert && e.changed.includes("rateLimits")) {
|
|
943
|
+
void onMeasure($, e.rateLimits);
|
|
944
|
+
void warnUsage($, e.rateLimits).catch(() => undefined);
|
|
945
|
+
}
|
|
909
946
|
return next(e);
|
|
910
947
|
}).catch(passOn);
|
|
911
948
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codizelabs/agent-rewake",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Auto-resume Claude Code, Codex, Gemini CLI, GitHub Copilot, Grok or any Zed agent when its usage limit resets, in the same session, and schedule messages into your agent threads.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"zed",
|