@codizelabs/agent-rewake 0.3.0 → 0.4.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.
- package/README.md +33 -24
- package/dist/agent-rewake.js +12407 -6694
- package/dist/hosts/claude-code/.claude-plugin/plugin.json +1 -1
- package/dist/hosts/claude-code/hooks/logic.js +170 -0
- package/dist/hosts/claude-code/hooks/register.js +169 -36
- package/package.json +1 -1
|
@@ -22,6 +22,15 @@ export const MAX_REARMS = 4;
|
|
|
22
22
|
export const REHIT_WINDOW_MS = 15 * MINUTE;
|
|
23
23
|
/** Re-hits in a row before Rewake stops continuing this session (Claude Code's own cap is 2). */
|
|
24
24
|
export const MAX_REHITS = 2;
|
|
25
|
+
/** Waits when the limit is back and no new reset time is known: 2, 5, 10, then 20 minutes. */
|
|
26
|
+
const BACKOFF_MS = [2, 5, 10, 20].map((m) => m * MINUTE);
|
|
27
|
+
|
|
28
|
+
/** How long to wait before trying again after `rearms` tries (Rewake's backoffMs, src/core/resume.ts). */
|
|
29
|
+
export function backoffMs(rearms) {
|
|
30
|
+
const i = Math.max(0, Math.min(BACKOFF_MS.length - 1, Math.floor(rearms)));
|
|
31
|
+
return BACKOFF_MS[i];
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
/**
|
|
26
35
|
* Sent as the person's message, so it says who sent it (Rewake's AUTO_LABEL, src/addon.ts):
|
|
27
36
|
* automation never speaks as the person (zed-launch A-5).
|
|
@@ -129,6 +138,167 @@ export function expired(ep, now) {
|
|
|
129
138
|
return false;
|
|
130
139
|
}
|
|
131
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Set by Rewake's `fire` on the Claude Code it starts to continue a closed session: that run is
|
|
143
|
+
* Rewake's own, so the plugin stays out of it (it is not a person at a prompt, and it must not
|
|
144
|
+
* touch the session's record while it works).
|
|
145
|
+
*/
|
|
146
|
+
export const FIRE_ENV = "AGENT_REWAKE_FIRE";
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The settings variables a continue of a closed session needs as the session had them, and the key
|
|
150
|
+
* variables of which only the names that were set are copied. Kept in step with Rewake's lists
|
|
151
|
+
* (src/hosts/claude-code/host.ts); a test in Rewake's repository fails if they drift apart.
|
|
152
|
+
* A name that looks secret is never copied, whatever this list says.
|
|
153
|
+
*/
|
|
154
|
+
export const SETTINGS_VARS = [
|
|
155
|
+
"CLAUDE_CONFIG_DIR",
|
|
156
|
+
"CLAUDE_CODE_PROJECT_DIR_NAME",
|
|
157
|
+
"ANTHROPIC_BASE_URL",
|
|
158
|
+
"ANTHROPIC_MODEL",
|
|
159
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
160
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
161
|
+
"CLAUDE_CODE_USE_FOUNDRY",
|
|
162
|
+
];
|
|
163
|
+
export const KEY_VARS = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN"];
|
|
164
|
+
const SECRET_NAME = /KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL/i;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* What to copy from the session's environment, read with `get(name)`: `env` (a name to its value)
|
|
168
|
+
* and `keysSet` (the key variables that were set, by name only).
|
|
169
|
+
*/
|
|
170
|
+
export function sessionVars(get) {
|
|
171
|
+
const env = {};
|
|
172
|
+
for (const name of SETTINGS_VARS) {
|
|
173
|
+
const v = get(name);
|
|
174
|
+
if (typeof v === "string" && v !== "" && !SECRET_NAME.test(name)) env[name] = v;
|
|
175
|
+
}
|
|
176
|
+
const keysSet = KEY_VARS.filter((k) => {
|
|
177
|
+
const v = get(k);
|
|
178
|
+
return typeof v === "string" && v !== "";
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
...(Object.keys(env).length > 0 && { env }),
|
|
182
|
+
...(keysSet.length > 0 && { keysSet }),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** States in which this plugin hasn't answered the limit: if Claude Code closes, it stays so. */
|
|
187
|
+
const UNANSWERED = new Set(["armed", "offered", "native"]);
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The limit as Rewake's session records keep it (src/hosts/sessions.ts `SessionLimit`), for the
|
|
191
|
+
* states nobody has answered and with a reset time. Undefined once continued, declined, cancelled,
|
|
192
|
+
* or answered by the person typing: then nothing is left for Rewake to continue.
|
|
193
|
+
*/
|
|
194
|
+
export function limitOf(ep) {
|
|
195
|
+
if (!ep || !UNANSWERED.has(ep.state)) return undefined;
|
|
196
|
+
if (typeof ep.resetAt !== "number" || typeof ep.createdAt !== "number") return undefined;
|
|
197
|
+
const kind = ep.kind === "five_hour" ? "session" : isWeekly(ep.kind) ? "weekly" : "other";
|
|
198
|
+
return {
|
|
199
|
+
kind,
|
|
200
|
+
billing: false,
|
|
201
|
+
resetsAt: ep.resetAt,
|
|
202
|
+
seenAt: ep.createdAt,
|
|
203
|
+
confidence: "structured",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const isWeekly = (kind) => typeof kind === "string" && kind.startsWith("seven_day");
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The Claude Code process, from the output of
|
|
211
|
+
* `echo "$PPID"; ps -o comm= -p "$PPID"; command -v claude` run in a shell Claude Code started:
|
|
212
|
+
* its id and program name as Rewake's own check reads them (src/util/proc.ts). Undefined when
|
|
213
|
+
* either is missing.
|
|
214
|
+
*/
|
|
215
|
+
export function parseProcess(stdout) {
|
|
216
|
+
const [first, comm = ""] = String(stdout).split("\n");
|
|
217
|
+
const pid = Number(first?.trim());
|
|
218
|
+
if (!Number.isInteger(pid) || pid <= 1 || comm.trim() === "") return undefined;
|
|
219
|
+
const name = (comm.trim().split(/[\\/]/).pop() ?? "").replace(/^-/, "");
|
|
220
|
+
return name === "" ? undefined : { pid, name };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Where `claude` is on the session's own PATH (a timer's PATH is minimal): the third line of the
|
|
225
|
+
* output above. Undefined unless it is an absolute path.
|
|
226
|
+
*/
|
|
227
|
+
export function parseProgram(stdout) {
|
|
228
|
+
const line = String(stdout).split("\n")[2]?.trim() ?? "";
|
|
229
|
+
return line.startsWith("/") ? line : undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Keeping the computer awake: the same commands as Rewake's src/util/keep-awake.ts (holdCommand,
|
|
233
|
+
// onMains), copied because the plugin is plain files that import nothing from Rewake.
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Run through the shell (macOS, Linux): Claude Code's PID (the shell's parent), the system name
|
|
237
|
+
* and the hold program, when the computer has one.
|
|
238
|
+
*/
|
|
239
|
+
export const WAKE_PROBE =
|
|
240
|
+
'echo "$PPID"; uname -s; for p in /usr/bin/caffeinate /usr/bin/systemd-inhibit /bin/systemd-inhibit; do [ -x "$p" ] && { echo "$p"; break; }; done';
|
|
241
|
+
|
|
242
|
+
/** Windows has no shell here: PowerShell, whose parent is Claude Code. */
|
|
243
|
+
export const WINDOWS_POWERSHELL = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
|
|
244
|
+
export const WINDOWS_PROBE =
|
|
245
|
+
'(Get-CimInstance Win32_Process -Filter "ProcessId=$PID").ParentProcessId';
|
|
246
|
+
|
|
247
|
+
/** Linux: "mains" unless there is a battery and no mains supply online (keep-awake.ts onMains). */
|
|
248
|
+
export const LINUX_MAINS =
|
|
249
|
+
'b=0; m=0; for d in /sys/class/power_supply/*; do t=$(cat "$d/type" 2>/dev/null); [ "$t" = Battery ] && b=1; [ "$t" = Mains ] && [ "$(cat "$d/online" 2>/dev/null)" = 1 ] && m=1; done; if [ $b = 0 ] || [ $m = 1 ]; then echo mains; else echo battery; fi';
|
|
250
|
+
|
|
251
|
+
/** Windows: Win32_Battery.BatteryStatus, or "none" for a desktop (the query in keep-awake.ts). */
|
|
252
|
+
export const WINDOWS_MAINS =
|
|
253
|
+
"$b = Get-CimInstance Win32_Battery; if ($b) { $b.BatteryStatus } else { 'none' }";
|
|
254
|
+
|
|
255
|
+
/** The shell probe's answer: { kind: "darwin" | "linux", pid, program }, or undefined. */
|
|
256
|
+
export function parseWake(stdout) {
|
|
257
|
+
const [pid = "", system, program = ""] = String(stdout)
|
|
258
|
+
.split("\n")
|
|
259
|
+
.map((l) => l.trim());
|
|
260
|
+
if (!/^\d+$/.test(pid)) return undefined;
|
|
261
|
+
if (system === "Darwin" && program === "/usr/bin/caffeinate")
|
|
262
|
+
return { kind: "darwin", pid, program };
|
|
263
|
+
if (system === "Linux" && program.endsWith("/systemd-inhibit"))
|
|
264
|
+
return { kind: "linux", pid, program };
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Whether a mains check's answer means mains power; unknown counts as mains, as in keep-awake.ts. */
|
|
269
|
+
export function mainsFrom(kind, stdout) {
|
|
270
|
+
const text = String(stdout).trim();
|
|
271
|
+
if (kind === "linux") return text !== "battery";
|
|
272
|
+
if (text === "" || text === "none") return true;
|
|
273
|
+
return text.split(/\s+/).some((n) => ["2", "6", "7", "8", "9"].includes(n));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** The program and arguments that hold the computer awake until process `pid` ends. */
|
|
277
|
+
export function holdArgv(kind, mode, pid, program) {
|
|
278
|
+
if (kind === "darwin") return [program, mode === "always" ? "-i" : "-s", "-w", pid];
|
|
279
|
+
if (kind === "linux")
|
|
280
|
+
return [
|
|
281
|
+
program,
|
|
282
|
+
"--what=idle",
|
|
283
|
+
"--who=Agent Rewake",
|
|
284
|
+
"--why=A planned resume is due",
|
|
285
|
+
"--mode=block",
|
|
286
|
+
"sh",
|
|
287
|
+
"-c",
|
|
288
|
+
'while kill -0 "$0" 2>/dev/null; do sleep 5; done',
|
|
289
|
+
pid,
|
|
290
|
+
];
|
|
291
|
+
return [
|
|
292
|
+
WINDOWS_POWERSHELL,
|
|
293
|
+
"-NoProfile",
|
|
294
|
+
"-NonInteractive",
|
|
295
|
+
"-WindowStyle",
|
|
296
|
+
"Hidden",
|
|
297
|
+
"-Command",
|
|
298
|
+
`$t = Add-Type -MemberDefinition '[DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint f);' -Name W -Namespace R -PassThru; $null = $t::SetThreadExecutionState(0x80000001); while (Get-Process -Id ${pid} -ErrorAction SilentlyContinue) { Start-Sleep -Seconds 5 }`,
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
|
|
132
302
|
/** "3:05 PM" or "15:05", in the person's clock ("12h" by default, as in Rewake's settings). */
|
|
133
303
|
function clockTime(d, clock) {
|
|
134
304
|
const h = d.getHours();
|
|
@@ -14,11 +14,15 @@
|
|
|
14
14
|
// claim: { by, at }, the process sending the armed continue right now
|
|
15
15
|
// sched:<id> [{ at, text }]
|
|
16
16
|
// A copy of each limit record (no message text) goes to Rewake's shared state folder, so
|
|
17
|
-
// `agent-rewake doctor` and the schedules page can show it
|
|
18
|
-
//
|
|
17
|
+
// `agent-rewake doctor` and the schedules page can show it, and so Rewake can continue the session
|
|
18
|
+
// headless if Claude Code closes before the limit is answered (it adds `open`, the Claude Code
|
|
19
|
+
// process, the limit and the session's settings variables: src/hosts/sessions.ts SessionRecord).
|
|
20
|
+
// Installed by `agent-rewake install`, which writes that folder's path into rewake.json beside this
|
|
21
|
+
// plugin.
|
|
19
22
|
|
|
20
23
|
import {
|
|
21
24
|
AFTER_RESET_MS,
|
|
25
|
+
backoffMs,
|
|
22
26
|
blockedUntil,
|
|
23
27
|
blockingKind,
|
|
24
28
|
CONTINUE_TEXT,
|
|
@@ -28,22 +32,34 @@ import {
|
|
|
28
32
|
featureOf,
|
|
29
33
|
HOUR,
|
|
30
34
|
helpText,
|
|
35
|
+
holdArgv,
|
|
36
|
+
LINUX_MAINS,
|
|
37
|
+
limitOf,
|
|
31
38
|
MAX_REARMS,
|
|
32
39
|
MAX_REHITS,
|
|
33
40
|
MINUTE,
|
|
41
|
+
mainsFrom,
|
|
34
42
|
mustAsk,
|
|
35
43
|
nativeLikely,
|
|
36
44
|
notHere,
|
|
37
45
|
parseCommand,
|
|
46
|
+
parseProcess,
|
|
47
|
+
parseProgram,
|
|
48
|
+
parseWake,
|
|
38
49
|
parseWhen,
|
|
39
50
|
personAtPrompt,
|
|
40
51
|
REHIT_WINDOW_MS,
|
|
41
52
|
STALE_MS,
|
|
42
53
|
safeId,
|
|
54
|
+
sessionVars,
|
|
43
55
|
splitWhen,
|
|
44
56
|
surfaceOf,
|
|
45
57
|
WAITING_EXPIRES_MS,
|
|
46
58
|
WAKE_HORIZON_MS,
|
|
59
|
+
WAKE_PROBE,
|
|
60
|
+
WINDOWS_MAINS,
|
|
61
|
+
WINDOWS_POWERSHELL,
|
|
62
|
+
WINDOWS_PROBE,
|
|
47
63
|
when,
|
|
48
64
|
} from "./logic.js";
|
|
49
65
|
|
|
@@ -78,15 +94,21 @@ let mirrorDir;
|
|
|
78
94
|
let sharedAuto;
|
|
79
95
|
/** The session's folder, for where Rewake's CLI names it ("in the shop folder"). */
|
|
80
96
|
let sessionCwd;
|
|
97
|
+
/** This Claude Code process ({ pid, name }) and the session's settings variables, for the copy. */
|
|
98
|
+
let claudeProcess;
|
|
99
|
+
let claudeProgram;
|
|
100
|
+
let claudeVars = {};
|
|
101
|
+
/** Rewake's own headless continue (AGENT_REWAKE_FIRE): the plugin does nothing in it. */
|
|
102
|
+
let inert = false;
|
|
81
103
|
/** This process, in a send's claim: two Claude Code processes can have one session open. */
|
|
82
104
|
const ME = Math.random().toString(36).slice(2);
|
|
83
105
|
/** A claim older than this was left by a process that stopped mid-send. */
|
|
84
106
|
const CLAIM_MS = 5 * MINUTE;
|
|
85
107
|
/** Rewake's keep-awake setting: "plugged-in" (default), "always" or "never". */
|
|
86
108
|
let keepAwake = "plugged-in";
|
|
87
|
-
/** Claude Code's PID
|
|
88
|
-
let
|
|
89
|
-
/** The running hold: {
|
|
109
|
+
/** { kind, pid, program }: Claude Code's PID and the hold program (macOS, Linux with systemd-inhibit, Windows); null where there is none. */
|
|
110
|
+
let wakeTarget;
|
|
111
|
+
/** The running hold: { key, stream }. */
|
|
90
112
|
let wakeHold;
|
|
91
113
|
|
|
92
114
|
const limitKey = (id) => `limit:${id}`;
|
|
@@ -117,17 +139,59 @@ async function loadConfig($) {
|
|
|
117
139
|
}
|
|
118
140
|
}
|
|
119
141
|
|
|
142
|
+
/** This process and the session's settings variables, for Rewake's copy of the record. */
|
|
143
|
+
async function loadSessionInfo($) {
|
|
144
|
+
if (!mirrorDir) return;
|
|
145
|
+
// $.env.get takes a literal name, so every variable the plugin reads can be listed (logic.js
|
|
146
|
+
// SETTINGS_VARS and KEY_VARS; a test in Rewake's repository compares them with these).
|
|
147
|
+
const read = (get) => get.catch(() => undefined);
|
|
148
|
+
const values = {
|
|
149
|
+
CLAUDE_CONFIG_DIR: await read($.env.get("CLAUDE_CONFIG_DIR")),
|
|
150
|
+
CLAUDE_CODE_PROJECT_DIR_NAME: await read($.env.get("CLAUDE_CODE_PROJECT_DIR_NAME")),
|
|
151
|
+
ANTHROPIC_BASE_URL: await read($.env.get("ANTHROPIC_BASE_URL")),
|
|
152
|
+
ANTHROPIC_MODEL: await read($.env.get("ANTHROPIC_MODEL")),
|
|
153
|
+
CLAUDE_CODE_USE_BEDROCK: await read($.env.get("CLAUDE_CODE_USE_BEDROCK")),
|
|
154
|
+
CLAUDE_CODE_USE_VERTEX: await read($.env.get("CLAUDE_CODE_USE_VERTEX")),
|
|
155
|
+
CLAUDE_CODE_USE_FOUNDRY: await read($.env.get("CLAUDE_CODE_USE_FOUNDRY")),
|
|
156
|
+
ANTHROPIC_API_KEY: await read($.env.get("ANTHROPIC_API_KEY")),
|
|
157
|
+
ANTHROPIC_AUTH_TOKEN: await read($.env.get("ANTHROPIC_AUTH_TOKEN")),
|
|
158
|
+
CLAUDE_CODE_OAUTH_TOKEN: await read($.env.get("CLAUDE_CODE_OAUTH_TOKEN")),
|
|
159
|
+
};
|
|
160
|
+
claudeVars = sessionVars((name) => values[name]);
|
|
161
|
+
try {
|
|
162
|
+
// The shell's parent is Claude Code; no /bin/sh or ps (Windows) means no process to watch.
|
|
163
|
+
const r = await $.process.run([
|
|
164
|
+
"/bin/sh",
|
|
165
|
+
"-c",
|
|
166
|
+
'echo "$PPID"; ps -o comm= -p "$PPID"; command -v claude',
|
|
167
|
+
]);
|
|
168
|
+
claudeProcess = r.exitCode === 0 ? parseProcess(r.stdout) : undefined;
|
|
169
|
+
claudeProgram = r.exitCode === 0 ? parseProgram(r.stdout) : undefined;
|
|
170
|
+
} catch {
|
|
171
|
+
claudeProcess = undefined;
|
|
172
|
+
claudeProgram = undefined;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
120
176
|
/** Write the record's metadata (never text) where Rewake's CLI reads it. Best effort. */
|
|
121
|
-
async function mirror($, id, ep) {
|
|
177
|
+
async function mirror($, id, ep, lastPromptAt) {
|
|
122
178
|
if (!mirrorDir || !safeId(id)) return;
|
|
123
179
|
const { state, kind, resetAt, fireAt, createdAt, sentAt, rehits, attempts } = ep ?? {
|
|
124
180
|
state: "none",
|
|
125
181
|
};
|
|
182
|
+
const limit = limitOf(ep);
|
|
126
183
|
const record = {
|
|
127
184
|
schemaVersion: 1,
|
|
128
185
|
host: "claude-code",
|
|
129
186
|
sessionId: id,
|
|
130
|
-
|
|
187
|
+
cwd: typeof sessionCwd === "string" ? sessionCwd : "",
|
|
188
|
+
// Rewake's SessionRecord: open while this process runs (it finds out when the process is gone).
|
|
189
|
+
open: true,
|
|
190
|
+
...(claudeProcess && { agents: [claudeProcess] }),
|
|
191
|
+
...(claudeProgram && { program: claudeProgram }),
|
|
192
|
+
...(limit && { limit }),
|
|
193
|
+
...(lastPromptAt !== undefined && { lastPromptAt }),
|
|
194
|
+
...claudeVars,
|
|
131
195
|
state,
|
|
132
196
|
kind,
|
|
133
197
|
resetAt,
|
|
@@ -150,10 +214,10 @@ async function save($, id, ep) {
|
|
|
150
214
|
await mirror($, id, ep);
|
|
151
215
|
}
|
|
152
216
|
|
|
153
|
-
async function drop($, id) {
|
|
217
|
+
async function drop($, id, lastPromptAt) {
|
|
154
218
|
disarm(id);
|
|
155
219
|
await $.store.delete(limitKey(id));
|
|
156
|
-
await mirror($, id, undefined);
|
|
220
|
+
await mirror($, id, undefined, lastPromptAt);
|
|
157
221
|
}
|
|
158
222
|
|
|
159
223
|
function disarm(id) {
|
|
@@ -171,9 +235,53 @@ async function arm($, id, ep) {
|
|
|
171
235
|
}
|
|
172
236
|
|
|
173
237
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
238
|
+
* Finds Claude Code's PID (the parent of the shell, or of PowerShell on Windows) and the hold
|
|
239
|
+
* program: caffeinate on macOS, systemd-inhibit on Linux, PowerShell on Windows. Null where the
|
|
240
|
+
* computer has none.
|
|
241
|
+
*/
|
|
242
|
+
async function findWake($) {
|
|
243
|
+
try {
|
|
244
|
+
const r = await $.process.run(["/bin/sh", "-c", WAKE_PROBE]);
|
|
245
|
+
if (r.exitCode === 0) return parseWake(r.stdout) ?? null;
|
|
246
|
+
} catch {
|
|
247
|
+
// No /bin/sh: Windows.
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const r = await $.process.run([
|
|
251
|
+
WINDOWS_POWERSHELL,
|
|
252
|
+
"-NoProfile",
|
|
253
|
+
"-NonInteractive",
|
|
254
|
+
"-Command",
|
|
255
|
+
WINDOWS_PROBE,
|
|
256
|
+
]);
|
|
257
|
+
const pid = r.stdout.trim();
|
|
258
|
+
return r.exitCode === 0 && /^\d+$/.test(pid)
|
|
259
|
+
? { kind: "win32", pid, program: WINDOWS_POWERSHELL }
|
|
260
|
+
: null;
|
|
261
|
+
} catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Whether a Linux or Windows computer is on mains power; unknown counts as mains. */
|
|
267
|
+
async function onMains($, kind) {
|
|
268
|
+
try {
|
|
269
|
+
const r = await $.process.run(
|
|
270
|
+
kind === "linux"
|
|
271
|
+
? ["/bin/sh", "-c", LINUX_MAINS]
|
|
272
|
+
: [WINDOWS_POWERSHELL, "-NoProfile", "-NonInteractive", "-Command", WINDOWS_MAINS],
|
|
273
|
+
);
|
|
274
|
+
return mainsFrom(kind, r.stdout);
|
|
275
|
+
} catch {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Keep this computer from idling to sleep while a continue is due within a few hours: Rewake's own,
|
|
282
|
+
* or Claude Code's (its wait holds nothing while the session sits idle). The hold is tied to Claude
|
|
283
|
+
* Code's process, so it ends with it even after a crash. "plugged-in" holds only on mains power
|
|
284
|
+
* (macOS's `caffeinate -s` does that itself). Returns whether a hold is running.
|
|
177
285
|
*/
|
|
178
286
|
async function updateWake($, ep, now) {
|
|
179
287
|
const due = ep?.state === "armed" ? ep.fireAt : ep?.state === "native" ? ep.resetAt : undefined;
|
|
@@ -182,34 +290,30 @@ async function updateWake($, ep, now) {
|
|
|
182
290
|
due !== undefined &&
|
|
183
291
|
due > now - STALE_MS &&
|
|
184
292
|
due - now <= WAKE_HORIZON_MS;
|
|
185
|
-
|
|
186
|
-
|
|
293
|
+
if (want && wakeTarget === undefined) wakeTarget = await findWake($);
|
|
294
|
+
const t = want ? wakeTarget : undefined;
|
|
295
|
+
const hold =
|
|
296
|
+
t && (keepAwake === "always" || t.kind === "darwin" || (await onMains($, t.kind)))
|
|
297
|
+
? holdArgv(t.kind, keepAwake, t.pid, t.program)
|
|
298
|
+
: undefined;
|
|
299
|
+
const key = hold?.join("\n");
|
|
300
|
+
if (wakeHold && wakeHold.key !== key) {
|
|
187
301
|
void wakeHold.stream.return?.();
|
|
188
302
|
wakeHold = undefined;
|
|
189
303
|
}
|
|
190
|
-
if (!
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const r = await $.process.run(["/bin/sh", "-c", "test -x /usr/bin/caffeinate && echo $PPID"]);
|
|
195
|
-
wakePid = r.exitCode === 0 && /^\d+$/.test(r.stdout.trim()) ? r.stdout.trim() : null;
|
|
196
|
-
} catch {
|
|
197
|
-
wakePid = null;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
if (wakePid === null) return false;
|
|
201
|
-
const stream = $.process.spawn({ argv: ["/usr/bin/caffeinate", mode, "-w", wakePid] });
|
|
202
|
-
const hold = { mode, stream };
|
|
203
|
-
wakeHold = hold;
|
|
304
|
+
if (!hold || wakeHold) return wakeHold !== undefined;
|
|
305
|
+
const stream = $.process.spawn({ argv: hold });
|
|
306
|
+
const running = { key, stream };
|
|
307
|
+
wakeHold = running;
|
|
204
308
|
void (async () => {
|
|
205
309
|
try {
|
|
206
310
|
for await (const _ of stream) {
|
|
207
|
-
//
|
|
311
|
+
// The hold writes nothing; the loop is the hold's life.
|
|
208
312
|
}
|
|
209
313
|
} catch {
|
|
210
314
|
// It couldn't start: no hold.
|
|
211
315
|
}
|
|
212
|
-
if (wakeHold ===
|
|
316
|
+
if (wakeHold === running) wakeHold = undefined;
|
|
213
317
|
})();
|
|
214
318
|
return true;
|
|
215
319
|
}
|
|
@@ -217,7 +321,10 @@ async function updateWake($, ep, now) {
|
|
|
217
321
|
async function refreshStatus($, id, now) {
|
|
218
322
|
const ep = await $.store.get(limitKey(id));
|
|
219
323
|
const sched = (await $.store.get(schedKey(id))) ?? [];
|
|
220
|
-
const
|
|
324
|
+
const holding = await updateWake($, ep, now);
|
|
325
|
+
const awake = holding
|
|
326
|
+
? ` · keeping this ${wakeTarget?.kind === "darwin" ? "Mac" : "computer"} awake`
|
|
327
|
+
: "";
|
|
221
328
|
if (ep?.state === "armed" && ep.fireAt !== undefined) {
|
|
222
329
|
$.ui.status(`Continues at ${at(ep.fireAt, now)}${awake} · /rewake cancel`);
|
|
223
330
|
} else if (ep?.state === "offered") {
|
|
@@ -340,6 +447,22 @@ async function onLimitOnce($, id) {
|
|
|
340
447
|
await refreshStatus($, id, now);
|
|
341
448
|
return;
|
|
342
449
|
}
|
|
450
|
+
if (resetAt === undefined && rehits > 0) {
|
|
451
|
+
// The limit came back right after Rewake continued, with no new reset time: the first request
|
|
452
|
+
// after the reset was refused (seen at reset + 61 s). The person already said yes, so try
|
|
453
|
+
// again after the usual wait; the re-hit cap above bounds the tries.
|
|
454
|
+
const next = {
|
|
455
|
+
state: "armed",
|
|
456
|
+
createdAt: now,
|
|
457
|
+
rehits,
|
|
458
|
+
attempts: 0,
|
|
459
|
+
fireAt: now + backoffMs(rehits - 1),
|
|
460
|
+
};
|
|
461
|
+
await save($, id, next);
|
|
462
|
+
await arm($, id, next);
|
|
463
|
+
await refreshStatus($, id, now);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
343
466
|
const ep = { state: "waiting", createdAt: now, rehits, attempts: 0 };
|
|
344
467
|
if (resetAt === undefined) {
|
|
345
468
|
// No reset time yet, or a limit that a wait doesn't fix (credits, a spending cap, an API
|
|
@@ -391,7 +514,9 @@ async function onNotification($, e) {
|
|
|
391
514
|
async function standDown($) {
|
|
392
515
|
const id = await $.session.id();
|
|
393
516
|
const ep = await $.store.get(limitKey(id));
|
|
394
|
-
|
|
517
|
+
// The person (or Claude Code itself) moved on: also tells Rewake, in case it planned to continue
|
|
518
|
+
// this session after Claude Code was closed.
|
|
519
|
+
if (ep && ep.state !== "sent") await drop($, id, await $.clock.now());
|
|
395
520
|
await refreshStatus($, id, await $.clock.now());
|
|
396
521
|
}
|
|
397
522
|
|
|
@@ -511,6 +636,9 @@ async function reopen($) {
|
|
|
511
636
|
return;
|
|
512
637
|
}
|
|
513
638
|
}
|
|
639
|
+
// This process has the session open now: Rewake must not continue it headless meanwhile.
|
|
640
|
+
const open = await $.store.get(limitKey(id));
|
|
641
|
+
if (open) await mirror($, id, open);
|
|
514
642
|
await refreshStatus($, id, now);
|
|
515
643
|
}
|
|
516
644
|
|
|
@@ -731,6 +859,8 @@ export function register(on) {
|
|
|
731
859
|
isInteractive = e.isInteractive;
|
|
732
860
|
sessionCwd = e.cwd;
|
|
733
861
|
entrypoint = await $.env.get("CLAUDE_CODE_ENTRYPOINT").catch(() => undefined);
|
|
862
|
+
inert = Boolean(await $.env.get("AGENT_REWAKE_FIRE").catch(() => undefined));
|
|
863
|
+
if (inert) return next(e);
|
|
734
864
|
surface = surfaceOf({ surface: e.surface, entrypoint });
|
|
735
865
|
try {
|
|
736
866
|
nativeSetting = { value: (await $.settings.read()).autoContinueAtUsageLimit };
|
|
@@ -738,6 +868,7 @@ export function register(on) {
|
|
|
738
868
|
nativeSetting = undefined;
|
|
739
869
|
}
|
|
740
870
|
await loadConfig($);
|
|
871
|
+
await loadSessionInfo($);
|
|
741
872
|
$.clock.every(TICK_MS, () => void tick($));
|
|
742
873
|
void reopen($);
|
|
743
874
|
for (const entry of [
|
|
@@ -757,6 +888,7 @@ export function register(on) {
|
|
|
757
888
|
});
|
|
758
889
|
|
|
759
890
|
on("classic.SessionStart", ($, e, next) => {
|
|
891
|
+
if (inert) return next(e);
|
|
760
892
|
// An in-process switch (/resume, /clear, /branch): the armed timer belongs to the old one.
|
|
761
893
|
if (e.source === "resume" || e.source === "clear" || e.source === "fork") {
|
|
762
894
|
disarm();
|
|
@@ -767,23 +899,24 @@ export function register(on) {
|
|
|
767
899
|
|
|
768
900
|
on("classic.StopFailure", ($, e, next) => {
|
|
769
901
|
// Main conversation only: helper and subagent failures carry agent_id.
|
|
770
|
-
if (e.error === "rate_limit" && e.agent_id === undefined)
|
|
902
|
+
if (!inert && e.error === "rate_limit" && e.agent_id === undefined)
|
|
903
|
+
void onLimit($, e.session_id);
|
|
771
904
|
return next(e);
|
|
772
905
|
}).catch(passOn);
|
|
773
906
|
|
|
774
907
|
on("session.measure", ($, e, next) => {
|
|
775
|
-
if (e.changed.includes("rateLimits")) void onMeasure($, e.rateLimits);
|
|
908
|
+
if (!inert && e.changed.includes("rateLimits")) void onMeasure($, e.rateLimits);
|
|
776
909
|
return next(e);
|
|
777
910
|
}).catch(passOn);
|
|
778
911
|
|
|
779
912
|
on("classic.Notification", ($, e, next) => {
|
|
780
|
-
if (e.notification_type.startsWith("quota_auto_resume_")) void onNotification($, e);
|
|
913
|
+
if (!inert && e.notification_type.startsWith("quota_auto_resume_")) void onNotification($, e);
|
|
781
914
|
return next(e);
|
|
782
915
|
}).catch(passOn);
|
|
783
916
|
|
|
784
917
|
on("prompt.submit", ($, e, next) => {
|
|
785
918
|
// Observe only: the person typed, or Claude Code's own auto-continue fired.
|
|
786
|
-
if (MOVED_ON.has(e.origin.kind)) void standDown($);
|
|
919
|
+
if (!inert && MOVED_ON.has(e.origin.kind)) void standDown($);
|
|
787
920
|
return next(e);
|
|
788
921
|
}).catch(passOn);
|
|
789
922
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codizelabs/agent-rewake",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|