@codizelabs/agent-rewake 0.4.0 → 0.5.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 CHANGED
@@ -37,6 +37,8 @@ Coding agents stop at their usage limit: Claude's 5-hour session limit, Codex's
37
37
  - **Automatic, if you want.** Choose *from now on* when Rewake asks (Claude Code), turn on auto-resume in Zed's **Rewake** menu, or run `agent-rewake continue --always` (Copilot CLI, Gemini CLI, Grok Build, Qwen Code, OpenCode, Antigravity CLI), and Rewake resumes without asking. It asks first when a limit resets more than a day away, never resumes credit or billing limits (and says so), and never approves permission prompts for you.
38
38
  - **Sends messages later.** `/rewake in 1h Run the tests` schedules a message into the session (Zed and Claude Code; in Zed also on a repeat, such as every weekday at 9).
39
39
  - **Keeps the computer awake, if it can.** On macOS, and on Linux (with systemd) and Windows, while a resume is due within six hours, Rewake stops the computer from idling to sleep (while plugged in, by default). Only macOS has been tried by hand. Closing the lid still puts it to sleep. `agent-rewake doctor` tells you if your computer's own sleep settings would let it sleep, and [which settings to change](https://codizelabs.github.io/agent-rewake/docs/#keep-your-computer-awake).
40
+ - **Warns you before a usage limit (Claude Code).** The plugin tells you when a usage window passes 80% and 95%, and when your pace over the last hour would reach the limit within about 45 minutes (as a range, "in about 20 to 40 minutes"). It uses only the usage figures Claude Code already gives it: no network, no sign-in. It only tells you; at 80% it mentions `/compact` (which uses some quota itself), at 95% it says it is a good moment to wrap up. It never types or compacts for you. Turn it off with `agent-rewake settings usage-warnings off`. Not built for Codex and the other agents.
41
+ - **Tells you when the project changed while it waited.** Outside Zed, if the session's folder is a git folder and its latest commit or tracked files differ by the time Rewake sends, it tells you, and sends the message as it was. You can instead have it add one fixed line asking the agent to re-read files (`agent-rewake settings workspace-changed note`), or send nothing and ask you first (`hold`). It reads only the commit id and the names-and-status summary from `git status --porcelain --untracked-files=no`, never file contents, and only on your computer.
40
42
  - **Same agent, same session.** No new agent to pick: Rewake works inside or in front of the agents you already use, so your sessions and threads keep going where they stopped.
41
43
 
42
44
  ## Where you work
@@ -88,7 +90,7 @@ npx @codizelabs/agent-rewake doctor
88
90
 
89
91
  ### Updating
90
92
 
91
- From any version: run `npx @codizelabs/agent-rewake@latest install`. Previews use the new version from their next session; in Zed, quit Zed completely and open it again. Your sessions, scheduled messages and settings stay. Details, and any version-specific steps (none so far), are in the [docs](https://codizelabs.github.io/agent-rewake/docs/#update). To see which version you run: `agent-rewake --version` (in Zed also the **Rewake** menu).
93
+ From any version: run `npx @codizelabs/agent-rewake@latest install`. Previews use the new version from their next session; in Zed, choose **Reload Agent** in the Agent Panel's menu in each open thread (Zed 1.18 or newer), or quit Zed completely and open it again. Your sessions, scheduled messages and settings stay. Details, and any version-specific steps (none so far), are in the [docs](https://codizelabs.github.io/agent-rewake/docs/#update). To see which version you run: `agent-rewake --version` (in Zed also the **Rewake** menu).
92
94
 
93
95
  ### Requirements
94
96
 
@@ -120,6 +122,7 @@ In Zed, the **Rewake** menu under the message box does everything; elsewhere, ty
120
122
  | See or change messages | **Rewake → Schedules**, **Change a scheduled message…** | `/rewake list` and `/rewake cancel` (Claude Code, Gemini CLI, Grok Build, Qwen Code, Cursor; in Codex without the slash). `/rewake cancel N` deletes one message: Claude Code only |
121
123
  | See what's planned | The Zed task **Agent Rewake: schedules** | `/rewake list` in the session; `agent-rewake schedules` lists every one, marking the resumes outside Zed; `agent-rewake doctor` shows the next planned resume of a closed session |
122
124
  | Cancel a planned resume | **Rewake → Change a scheduled message…**, or the schedules page | `/rewake cancel` in the session where it's typed, or `agent-rewake continue --cancel` to pick one or all; `agent-rewake continue --cancel <id>` cancels one by the id in square brackets in `agent-rewake schedules` |
125
+ | Resume now, without waiting for the reset | **Rewake → Change a scheduled message…** | `agent-rewake continue --now` picks a planned resume outside Zed and asks first; `agent-rewake continue --now <id>` names one (`--yes` without a terminal). It makes the same checks as at the reset; if the limit is still on, the session is open or changed, you typed after the limit, the agent continued, or the message can't reach the agent (signed out, session deleted or archived, folder gone), nothing is sent and the resume stays planned for its time; Ctrl+C during a run stops it and cancels the resume |
123
126
  | Change settings | **Rewake → Settings…** | `agent-rewake settings` lists them with what each does; `agent-rewake settings change` lets you pick one, or `agent-rewake settings <name> <value>` changes it (for example `agent-rewake settings clock 24h`) |
124
127
 
125
128
  **One command everywhere:** `/rewake` is the same in Zed, Claude Code, Gemini CLI, Grok Build, Qwen Code and Cursor's own agent (in Codex, type `rewake` without the slash). At a usage limit it continues after the reset; `/rewake <time>` continues then, `/rewake cancel` cancels it, and `/rewake help` lists what else works where you are. Rewake answers it itself, without using the model. `npx`/`agent-rewake` on the command line is only for installing, uninstalling and the two agents with no typed command (GitHub Copilot CLI, Antigravity CLI).
@@ -135,7 +138,7 @@ In Zed, Rewake works the same way in front of every external agent: **Claude Age
135
138
  The whole picture, with what Rewake never does and how to undo it, is on the [privacy and security page](https://codizelabs.github.io/agent-rewake/docs/privacy-and-security/). In short:
136
139
 
137
140
  - Runs locally. Rewake never sees your credentials: sign-in goes through each agent's own flow. Besides fetching an agent's own program (a download, or `npm install`) when Zed hasn't already done it, its only network use is error reports, which are off unless you turn them on (`agent-rewake errors on`; see the docs' [Error reports](https://codizelabs.github.io/agent-rewake/docs/#error-reports)).
138
- - Sends only what you scheduled, approved, or turned on.
141
+ - Sends only what you scheduled, approved, or turned on. (One fixed line, saying the project changed while it waited, is added to a resume only if you choose `note`.)
139
142
  - Stores scheduled messages and per-thread settings in a private folder on your machine; logs hold metadata only (events, codes and program names), never message text.
140
143
  - Keeps your message off the command line, with exceptions. When Rewake continues a closed Copilot CLI, Gemini CLI, Qwen Code or OpenCode session, it hands the message to the agent on its standard input; for Grok Build, in a file only you can read. Three cases put it on a command line, where another program on your computer can read it from the process list for as long as the command runs: **Codex** (`codex queue --message <text>`), **Antigravity CLI** (`-p <text>`), and **Gemini CLI** or **Qwen Code** when you've turned its sandbox on (they then start themselves again with the text as an argument; for Qwen Code that is read from its source, not tried). Claude Code, Cursor and Zed don't use a command line for it.
141
144
 
@@ -149,7 +152,7 @@ This takes Rewake out of every place it's set up in, after showing every change
149
152
 
150
153
  ## Status
151
154
 
152
- Version 0.4. Rewake's automated tests run on macOS, Linux and Windows with Node.js 22 and 24, and the author uses it daily in Zed. The previews outside Zed are new: the docs say [what's been tried for each](https://codizelabs.github.io/agent-rewake/docs/#outside-zed-previews), and what hasn't. What changed in each release is in the [changelog](https://codizelabs.github.io/agent-rewake/changelog/). Feedback and bug reports are very welcome: [open an issue](https://github.com/codizelabs/agent-rewake/issues/new/choose). If Rewake missed a usage limit, [say how](https://codizelabs.github.io/agent-rewake/docs/#report-a-limit-rewake-missed).
155
+ Version 0.5. Rewake's automated tests run on macOS, Linux and Windows with Node.js 22 and 24, and the author uses it daily in Zed. The previews outside Zed are new: the docs say [what's been tried for each](https://codizelabs.github.io/agent-rewake/docs/#outside-zed-previews), and what hasn't. What changed in each release is in the [changelog](https://codizelabs.github.io/agent-rewake/changelog/). Feedback and bug reports are very welcome: [open an issue](https://github.com/codizelabs/agent-rewake/issues/new/choose). If Rewake missed a usage limit, [say how](https://codizelabs.github.io/agent-rewake/docs/#report-a-limit-rewake-missed).
153
156
 
154
157
  ## Contributing
155
158
 
@@ -2366,6 +2366,68 @@ var init_options_bridge = __esm({
2366
2366
  }
2367
2367
  });
2368
2368
 
2369
+ // src/util/workspace.ts
2370
+ import { execFileSync } from "node:child_process";
2371
+ import { createHash as createHash2 } from "node:crypto";
2372
+ function gitEnv() {
2373
+ const { GIT_DIR: _d, GIT_WORK_TREE: _w, GIT_INDEX_FILE: _i, ...rest } = process.env;
2374
+ return rest;
2375
+ }
2376
+ function git(cwd, args, deadline) {
2377
+ const left = deadline - Date.now();
2378
+ if (left <= 0) return void 0;
2379
+ try {
2380
+ return execFileSync(
2381
+ "git",
2382
+ ["--no-optional-locks", "-c", "core.fsmonitor=false", "-C", cwd, ...args],
2383
+ {
2384
+ encoding: "utf8",
2385
+ timeout: left,
2386
+ maxBuffer: MAX_OUTPUT_BYTES,
2387
+ stdio: ["ignore", "pipe", "ignore"],
2388
+ windowsHide: true,
2389
+ env: gitEnv()
2390
+ }
2391
+ );
2392
+ } catch {
2393
+ return void 0;
2394
+ }
2395
+ }
2396
+ function fingerprintWorkspace(cwd, budgetMs = FIRE_BUDGET_MS) {
2397
+ if (!cwd) return void 0;
2398
+ const deadline = Date.now() + budgetMs;
2399
+ const commit = git(cwd, ["rev-parse", "HEAD"], deadline)?.trim();
2400
+ if (!commit || !/^[0-9a-f]{40,64}$/.test(commit)) return void 0;
2401
+ const status = git(cwd, ["status", "--porcelain", "--untracked-files=no"], deadline);
2402
+ if (status === void 0) return void 0;
2403
+ const lines = status.split("\n").filter(Boolean).sort();
2404
+ return {
2405
+ commit,
2406
+ hash: createHash2("sha256").update(lines.join("\n")).digest("hex"),
2407
+ dirty: lines.length > 0
2408
+ };
2409
+ }
2410
+ function workspaceChange(before, after) {
2411
+ const commit = before.commit !== after.commit;
2412
+ const changes = before.hash !== after.hash;
2413
+ return commit || changes ? { commit, changes } : void 0;
2414
+ }
2415
+ function validFingerprint(w) {
2416
+ if (typeof w !== "object" || w === null) return false;
2417
+ const x = w;
2418
+ return typeof x.commit === "string" && x.commit.length <= 64 && typeof x.hash === "string" && x.hash.length <= 64 && typeof x.dirty === "boolean";
2419
+ }
2420
+ var ARM_BUDGET_MS, FIRE_BUDGET_MS, MAX_OUTPUT_BYTES, WORKSPACE_NOTE;
2421
+ var init_workspace = __esm({
2422
+ "src/util/workspace.ts"() {
2423
+ "use strict";
2424
+ ARM_BUDGET_MS = 1e3;
2425
+ FIRE_BUDGET_MS = 2e3;
2426
+ MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
2427
+ WORKSPACE_NOTE = "Note from Rewake: the project changed while this session waited for the usage limit to reset. Re-read any files before editing them.";
2428
+ }
2429
+ });
2430
+
2369
2431
  // src/core/store.ts
2370
2432
  import { randomUUID } from "node:crypto";
2371
2433
  import {
@@ -2399,7 +2461,7 @@ function validFollowUps(t) {
2399
2461
  function validateSchedule(value) {
2400
2462
  if (value === null || typeof value !== "object") return void 0;
2401
2463
  const s = value;
2402
- const ok = s.schemaVersion === 1 && typeof s.scheduleId === "string" && /^[0-9a-f-]{36}$/.test(s.scheduleId) && typeof s.sessionId === "string" && s.sessionId.length > 0 && typeof s.cwd === "string" && (s.kind === "user" || s.kind === "limit_resume" || s.kind === "auto_limit_resume") && typeof s.text === "string" && s.text.trim().length > 0 && Buffer.byteLength(s.text) <= MAX_TEXT_BYTES && typeof s.dueAt === "number" && Number.isFinite(s.dueAt) && typeof s.status === "string" && STATUSES.has(s.status) && Array.isArray(s.attempts) && typeof s.createdAt === "number" && typeof s.updatedAt === "number" && (s.repeat === void 0 || validRepeat(s.repeat)) && (s.followUps === void 0 || validFollowUps(s.followUps)) && (s.host === void 0 || typeof s.host === "string" && KNOWN_HOSTS.has(s.host)) && (s.sessionRef === void 0 || validSessionRef(s.sessionRef)) && (s.rearms === void 0 || typeof s.rearms === "number" && Number.isInteger(s.rearms) && s.rearms >= 0) && (s.earlyRearms === void 0 || typeof s.earlyRearms === "number" && Number.isInteger(s.earlyRearms) && s.earlyRearms >= 0);
2464
+ const ok = s.schemaVersion === 1 && typeof s.scheduleId === "string" && /^[0-9a-f-]{36}$/.test(s.scheduleId) && typeof s.sessionId === "string" && s.sessionId.length > 0 && typeof s.cwd === "string" && (s.kind === "user" || s.kind === "limit_resume" || s.kind === "auto_limit_resume") && typeof s.text === "string" && s.text.trim().length > 0 && Buffer.byteLength(s.text) <= MAX_TEXT_BYTES && typeof s.dueAt === "number" && Number.isFinite(s.dueAt) && typeof s.status === "string" && STATUSES.has(s.status) && Array.isArray(s.attempts) && typeof s.createdAt === "number" && typeof s.updatedAt === "number" && (s.repeat === void 0 || validRepeat(s.repeat)) && (s.followUps === void 0 || validFollowUps(s.followUps)) && (s.host === void 0 || typeof s.host === "string" && KNOWN_HOSTS.has(s.host)) && (s.sessionRef === void 0 || validSessionRef(s.sessionRef)) && (s.rearms === void 0 || typeof s.rearms === "number" && Number.isInteger(s.rearms) && s.rearms >= 0) && (s.earlyRearms === void 0 || typeof s.earlyRearms === "number" && Number.isInteger(s.earlyRearms) && s.earlyRearms >= 0) && (s.workspace === void 0 || validFingerprint(s.workspace));
2403
2465
  return ok ? value : void 0;
2404
2466
  }
2405
2467
  function writeFileAtomic(dir, name, data) {
@@ -2420,6 +2482,7 @@ var init_store = __esm({
2420
2482
  "use strict";
2421
2483
  init_fs();
2422
2484
  init_paths();
2485
+ init_workspace();
2423
2486
  init_cron();
2424
2487
  init_lock();
2425
2488
  PAUSE = new Int32Array(new SharedArrayBuffer(4));
@@ -2719,6 +2782,8 @@ function loadSettings(stateDir2) {
2719
2782
  const autoWhenPromptsSkipped = raw.autoWhenPromptsSkipped !== false;
2720
2783
  const keepAwake = KEEP_AWAKE_VALUES.find((v) => v === raw.keepAwake) ?? DEFAULT_SETTINGS.keepAwake;
2721
2784
  const errorReports = raw.errorReports === "on" ? "on" : DEFAULT_SETTINGS.errorReports;
2785
+ const workspaceChanged = WORKSPACE_CHANGED_VALUES.find((v) => v === raw.workspaceChanged) ?? DEFAULT_SETTINGS.workspaceChanged;
2786
+ const usageWarnings = raw.usageWarnings === "off" ? "off" : DEFAULT_SETTINGS.usageWarnings;
2722
2787
  const errorReportsAsked = raw.errorReportsAsked === true;
2723
2788
  return {
2724
2789
  clock: clock2,
@@ -2726,6 +2791,8 @@ function loadSettings(stateDir2) {
2726
2791
  autoWhenPromptsSkipped,
2727
2792
  keepAwake,
2728
2793
  errorReports,
2794
+ workspaceChanged,
2795
+ usageWarnings,
2729
2796
  ...resumePrompt && { resumePrompt },
2730
2797
  ...errorReportsAsked && { errorReportsAsked }
2731
2798
  };
@@ -2746,7 +2813,7 @@ function applySettings(stateDir2) {
2746
2813
  setClock(s.clock);
2747
2814
  return s;
2748
2815
  }
2749
- var CLOCKS, NEW_THREADS_VALUES, KEEP_AWAKE_VALUES, MAX_RESUME_PROMPT, KEEP_AWAKE_SAVED, DEFAULT_SETTINGS;
2816
+ var CLOCKS, NEW_THREADS_VALUES, KEEP_AWAKE_VALUES, WORKSPACE_CHANGED_VALUES, USAGE_WARNINGS_VALUES, MAX_RESUME_PROMPT, KEEP_AWAKE_SAVED, DEFAULT_SETTINGS;
2750
2817
  var init_settings = __esm({
2751
2818
  "src/core/settings.ts"() {
2752
2819
  "use strict";
@@ -2756,6 +2823,13 @@ var init_settings = __esm({
2756
2823
  CLOCKS = ["12h", "24h"];
2757
2824
  NEW_THREADS_VALUES = ["ask", "on", "off"];
2758
2825
  KEEP_AWAKE_VALUES = ["plugged-in", "always", "never"];
2826
+ WORKSPACE_CHANGED_VALUES = [
2827
+ "tell",
2828
+ "note",
2829
+ "hold",
2830
+ "off"
2831
+ ];
2832
+ USAGE_WARNINGS_VALUES = ["on", "off"];
2759
2833
  MAX_RESUME_PROMPT = 16384;
2760
2834
  KEEP_AWAKE_SAVED = {
2761
2835
  "plugged-in": "Rewake keeps this computer awake for resumes and scheduled messages while it's plugged in.",
@@ -2767,13 +2841,15 @@ var init_settings = __esm({
2767
2841
  newThreads: "ask",
2768
2842
  autoWhenPromptsSkipped: true,
2769
2843
  keepAwake: "plugged-in",
2770
- errorReports: "off"
2844
+ errorReports: "off",
2845
+ workspaceChanged: "tell",
2846
+ usageWarnings: "on"
2771
2847
  };
2772
2848
  }
2773
2849
  });
2774
2850
 
2775
2851
  // src/core/threads.ts
2776
- import { createHash as createHash2 } from "node:crypto";
2852
+ import { createHash as createHash3 } from "node:crypto";
2777
2853
  import { readdirSync as readdirSync4, readFileSync as readFileSync7, rmSync as rmSync5 } from "node:fs";
2778
2854
  import { join as join8 } from "node:path";
2779
2855
  var DEFAULT_RESUME_PROMPT, ThreadStore;
@@ -2789,7 +2865,7 @@ var init_threads = __esm({
2789
2865
  this.dir = join8(stateDir2, "threads");
2790
2866
  }
2791
2867
  file(sessionId) {
2792
- return `${createHash2("sha256").update(sessionId).digest("hex").slice(0, 32)}.json`;
2868
+ return `${createHash3("sha256").update(sessionId).digest("hex").slice(0, 32)}.json`;
2793
2869
  }
2794
2870
  get(sessionId) {
2795
2871
  try {
@@ -4479,20 +4555,20 @@ function killTree(child, p = osPlatform2(), signal = "SIGTERM", run3 = taskkill)
4479
4555
  }
4480
4556
  child.kill(signal);
4481
4557
  }
4482
- function killPidTree(pid, p = osPlatform2(), run3 = taskkill) {
4558
+ function killPidTree(pid, p = osPlatform2(), run3 = taskkill, signal = "SIGTERM") {
4483
4559
  killTree(
4484
4560
  {
4485
4561
  pid,
4486
- kill: (signal) => {
4562
+ kill: (signal2) => {
4487
4563
  try {
4488
- return process.kill(pid, signal);
4564
+ return process.kill(pid, signal2);
4489
4565
  } catch {
4490
4566
  return false;
4491
4567
  }
4492
4568
  }
4493
4569
  },
4494
4570
  p,
4495
- "SIGTERM",
4571
+ signal,
4496
4572
  run3
4497
4573
  );
4498
4574
  }
@@ -4892,7 +4968,7 @@ var init_version = __esm({
4892
4968
  return {};
4893
4969
  }
4894
4970
  })();
4895
- VERSION2 = pkg.version ?? (true ? "0.4.0" : "0.0.0");
4971
+ VERSION2 = pkg.version ?? (true ? "0.5.0" : "0.0.0");
4896
4972
  PACKAGE_NAME = pkg.name ?? "@codizelabs/agent-rewake";
4897
4973
  FROM_SOURCE = false;
4898
4974
  }
@@ -5498,6 +5574,11 @@ function armedText(host, cwd, at, now) {
5498
5574
  const typing = host.keepOpen ? `Typing in the ${host.noun ?? "session"} before then cancels this continue.` : `Reopening that ${host.noun ?? "session"} and typing in it before then cancels this resume.`;
5499
5575
  return `Rewake will continue ${placeOf(host, cwd)} ${formatAt(at, now)}. Keep this computer on and awake until then.${keep} ${typing} To cancel all planned resumes: ${rewake("continue --cancel")}`;
5500
5576
  }
5577
+ function withWorkspace(store, s, now) {
5578
+ const workspace = fingerprintWorkspace(s.cwd, ARM_BUDGET_MS);
5579
+ if (!workspace) return s;
5580
+ return store.update(s.scheduleId, (x) => ({ ...x, workspace }), now) ?? s;
5581
+ }
5501
5582
  function armClosed(host, r, at, d) {
5502
5583
  const store = new ScheduleStore(d.stateDir);
5503
5584
  const settings = loadSettings(d.stateDir);
@@ -5517,7 +5598,7 @@ function armClosed(host, r, at, d) {
5517
5598
  };
5518
5599
  store.put(resume);
5519
5600
  d.arm(resume.scheduleId, at + (host.fallbackDelayMs ?? 0));
5520
- return resume;
5601
+ return withWorkspace(store, resume, d.now);
5521
5602
  }
5522
5603
  function onSessionStart(host, sessionId, cwd, d, program) {
5523
5604
  if (d.env[FIRE_ENV]) return;
@@ -5692,6 +5773,7 @@ var init_closed = __esm({
5692
5773
  init_command2();
5693
5774
  init_proc();
5694
5775
  init_secrets();
5776
+ init_workspace();
5695
5777
  init_sessions();
5696
5778
  FIRE_ENV = "AGENT_REWAKE_FIRE";
5697
5779
  COMMON_VARS = ["XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME"];
@@ -8205,7 +8287,7 @@ var init_install6 = __esm({
8205
8287
 
8206
8288
  // src/wrap.ts
8207
8289
  import { spawnSync as spawnSync8 } from "node:child_process";
8208
- import { createHash as createHash3 } from "node:crypto";
8290
+ import { createHash as createHash4 } from "node:crypto";
8209
8291
  import {
8210
8292
  chmodSync as chmodSync2,
8211
8293
  existsSync as existsSync14,
@@ -8359,7 +8441,7 @@ function ensureNpxAgent(id, npx, stateDir2, env, logFile) {
8359
8441
  );
8360
8442
  }
8361
8443
  function zedBinaryDir(id, version, target, env) {
8362
- const sha = (s) => createHash3("sha256").update(s).digest("hex").slice(0, 16);
8444
+ const sha = (s) => createHash4("sha256").update(s).digest("hex").slice(0, 16);
8363
8445
  const archiveKey = target.sha256 ? `${target.archive}\0sha256:${target.sha256.toLowerCase()}` : target.archive;
8364
8446
  return join24(
8365
8447
  zedDataDir(env),
@@ -8430,7 +8512,7 @@ async function downloadBinaryAgent(id, version, target, stateDir2, fetchImpl = f
8430
8512
  if (!res.ok) throw new Error(`download failed (HTTP ${res.status})`);
8431
8513
  const bytes = Buffer.from(await res.arrayBuffer());
8432
8514
  if (target.sha256) {
8433
- const got = createHash3("sha256").update(bytes).digest("hex");
8515
+ const got = createHash4("sha256").update(bytes).digest("hex");
8434
8516
  if (got !== target.sha256.toLowerCase())
8435
8517
  throw new Error("the download doesn't match the registry's checksum");
8436
8518
  }
@@ -9803,6 +9885,9 @@ function quitZed(p = platform3()) {
9803
9885
  if (p === "win32") return "close every Zed window so Zed exits";
9804
9886
  return "quit Zed completely (Ctrl+Q)";
9805
9887
  }
9888
+ function reloadAgent(p = platform3()) {
9889
+ return `choose "Reload Agent" in the Agent Panel's menu in each open thread (Zed 1.18 or newer), or ${quitZed(p)} and open it again`;
9890
+ }
9806
9891
  function nextSteps(reachShown = false) {
9807
9892
  return [
9808
9893
  "",
@@ -10011,7 +10096,8 @@ function explainResume(r, now, locale) {
10011
10096
  lines.push(
10012
10097
  ` When: ${when2}`,
10013
10098
  ` What Rewake will do: send your message "${oneLine(r.text)}" into the ${r.noun} once.`,
10014
- ` Nothing is sent now: this view only reads. ${r.cancel}`
10099
+ ` Nothing is sent now: this view only reads. ${r.cancel}`,
10100
+ ...r.now ? [` ${r.now}`] : []
10015
10101
  );
10016
10102
  return lines;
10017
10103
  }
@@ -10029,6 +10115,10 @@ function explainResume(r, now, locale) {
10029
10115
  lines.push(
10030
10116
  " Its time has already passed by too long, so Rewake won't send it by itself. It will tell you."
10031
10117
  );
10118
+ if (r.workspaceChanged === "tell" || r.workspaceChanged === "note")
10119
+ lines.push(
10120
+ ` Before sending, Rewake checks whether the project's latest git commit or tracked files differ from when this was planned (only the commit id and a summary of file names and status, never contents). If they do, ${r.workspaceChanged === "note" ? "it adds a line to the message asking the agent to re-read files before editing, and tells you" : "it tells you and sends the message as it is"}.`
10121
+ );
10032
10122
  lines.push(" What could stop it:");
10033
10123
  lines.push(
10034
10124
  ` - You type in the ${r.noun} before then: Rewake sends nothing.`,
@@ -10038,12 +10128,17 @@ function explainResume(r, now, locale) {
10038
10128
  lines.push(
10039
10129
  ` - The ${r.noun} is open in a window or terminal then: Rewake sends nothing and tells you.`
10040
10130
  );
10131
+ if (r.workspaceChanged === "hold")
10132
+ lines.push(
10133
+ " - The project's latest git commit or tracked files differ from when this was planned (Rewake reads only the commit id and a summary of file names and status, never contents): Rewake sends nothing and tells you."
10134
+ );
10041
10135
  lines.push(
10042
10136
  " - The agent is still at its usage limit: Rewake waits for the new reset and tries again, or tells you if it can't.",
10043
10137
  " - The computer is off or asleep then: Rewake sends nothing if it runs much later, and tells you.",
10044
10138
  " - A credit or billing limit: Rewake never resumes it, because waiting doesn't fix it.",
10045
10139
  ` Nothing is sent now: this view only reads. ${r.cancel}`
10046
10140
  );
10141
+ if (r.now) lines.push(` ${r.now}`);
10047
10142
  return lines;
10048
10143
  }
10049
10144
  var PENDING, oneLine;
@@ -10268,6 +10363,7 @@ function explainSchedule(stateDir2, idPrefix, now, hostOf, locale) {
10268
10363
  const folder = s.cwd ? basename7(s.cwd) : "";
10269
10364
  const title = new ThreadStore(stateDir2).get(s.sessionId)?.title;
10270
10365
  const where2 = host ? folder ? `${host.name} in the "${folder}" folder` : host.name : title ? `The Zed thread "${title}"` : `The Zed thread ${s.sessionId.slice(0, 8)}`;
10366
+ const workspaceSetting = loadSettings(stateDir2).workspaceChanged;
10271
10367
  const lines = explainResume(
10272
10368
  {
10273
10369
  dueAt: s.dueAt,
@@ -10279,7 +10375,11 @@ function explainSchedule(stateDir2, idPrefix, now, hostOf, locale) {
10279
10375
  noun: host?.noun ?? "thread",
10280
10376
  outsideZed: host !== void 0,
10281
10377
  ...host === void 0 && { lateMs: ZED_LATE_MS },
10282
- cancel: host ? `To cancel it: ${rewake(`continue --cancel ${s.scheduleId.slice(0, 8)}`)}` : `To cancel it, use the schedules page: ${rewake("ui")}`
10378
+ ...host !== void 0 && s.workspace && workspaceSetting !== "off" && { workspaceChanged: workspaceSetting },
10379
+ cancel: host ? `To cancel it: ${rewake(`continue --cancel ${s.scheduleId.slice(0, 8)}`)}` : `To cancel it, use the schedules page: ${rewake("ui")}`,
10380
+ ...host && {
10381
+ now: `To continue it now instead of waiting for the reset: ${rewake(`continue --now ${s.scheduleId.slice(0, 8)}`)}`
10382
+ }
10283
10383
  },
10284
10384
  now,
10285
10385
  locale
@@ -10321,6 +10421,7 @@ var init_overview = __esm({
10321
10421
  "src/ui/overview.ts"() {
10322
10422
  "use strict";
10323
10423
  init_explain();
10424
+ init_settings();
10324
10425
  init_store();
10325
10426
  init_threads();
10326
10427
  init_time();
@@ -13681,6 +13782,7 @@ ${pending.map((s, i) => ` ${i + 1}. ${about(o, s)} [${s.scheduleId.slice(0, 8)
13681
13782
  for (const s of chosen3) await cancel(o, store, s);
13682
13783
  return 0;
13683
13784
  }
13785
+ if (o.mode === "now") return runNow(o);
13684
13786
  const list2 = waiting(o);
13685
13787
  if (list2.length === 0) {
13686
13788
  o.out("Nothing to continue: no closed session is stopped at a usage limit.\n");
@@ -13835,7 +13937,7 @@ function about(o, s) {
13835
13937
  const host = hostFor(o, s.host);
13836
13938
  return `${host ? `${placeOf(host, s.cwd)} ` : s.host === void 0 ? "Zed thread " : ""}${formatAt(s.dueAt, o.deps.now)}`;
13837
13939
  }
13838
- async function cancelOne(o, store, id) {
13940
+ function findOne(o, store, id) {
13839
13941
  const wanted = id.trim().toLowerCase();
13840
13942
  const found = wanted ? store.list().filter((s2) => s2.scheduleId.toLowerCase().startsWith(wanted)) : [];
13841
13943
  const fail2 = (text2) => {
@@ -13857,6 +13959,11 @@ ${found.map((s2) => ` ${s2.scheduleId.slice(0, 8)} ${about(o, s2)}`).join("\n"
13857
13959
  return fail2(
13858
13960
  `That's a message in a Zed thread: change or delete it on the schedules page (${rewake("ui")}), or in the thread's Rewake menu.`
13859
13961
  );
13962
+ return s;
13963
+ }
13964
+ async function cancelOne(o, store, id) {
13965
+ const s = findOne(o, store, id);
13966
+ if (typeof s === "number") return s;
13860
13967
  if (TERMINAL_STATUSES.has(s.status)) {
13861
13968
  const host = hostFor(o, s.host);
13862
13969
  o.out(
@@ -13867,6 +13974,150 @@ ${found.map((s2) => ` ${s2.scheduleId.slice(0, 8)} ${about(o, s2)}`).join("\n"
13867
13974
  }
13868
13975
  return await cancel(o, store, s) ? 0 : 1;
13869
13976
  }
13977
+ async function runNow(o) {
13978
+ const now = o.deps.now;
13979
+ const store = new ScheduleStore(o.deps.stateDir);
13980
+ if (!o.interactive && (o.nowId === void 0 || o.yes !== true)) {
13981
+ (o.err ?? o.out)(
13982
+ `agent-rewake: There is no terminal to ask in, so nothing was sent. Give the id and --yes: ${rewake("continue --now <id> --yes")}. The ids are in ${rewake("schedules")}, in square brackets.
13983
+ `
13984
+ );
13985
+ return 1;
13986
+ }
13987
+ let s;
13988
+ if (o.nowId !== void 0) {
13989
+ const found = findOne(o, store, o.nowId);
13990
+ if (typeof found === "number") return found;
13991
+ s = found;
13992
+ } else {
13993
+ const windowOf = (x) => o.hosts.find((h) => h.id === x.host && h.maxWaitAfterLimitMs !== void 0);
13994
+ const waitingHere = store.list().filter((x) => x.host !== void 0 && x.host !== "acp" && WAITING.has(x.status));
13995
+ const pending = waitingHere.filter((x) => !windowOf(x));
13996
+ if (pending.length === 0) {
13997
+ const blocked = waitingHere.map(windowOf).find((h) => h !== void 0);
13998
+ o.out(
13999
+ blocked ? `Nothing here can be continued now: ${windowReason(blocked)}.
14000
+ ` : "Nothing to resume now: Rewake isn't set to continue any session.\n"
14001
+ );
14002
+ return 0;
14003
+ }
14004
+ s = pending[0];
14005
+ if (pending.length > 1) {
14006
+ const placeIn = (x) => {
14007
+ const h = hostFor(o, x.host);
14008
+ return h ? placeOf(h, x.cwd) : "";
14009
+ };
14010
+ const twin = (x) => pending.filter((y) => placeIn(y) === placeIn(x)).length > 1;
14011
+ o.out(
14012
+ `Planned resumes:
14013
+ ${pending.map((x, i) => ` ${i + 1}. ${about(o, x)}${twin(x) ? ` (${printable(x.cwd)})` : ""} [${x.scheduleId.slice(0, 8)}]`).join("\n")}
14014
+ `
14015
+ );
14016
+ const answer = await o.ask(
14017
+ `Which one should Rewake continue now? (1-${pending.length}, or Enter to keep waiting) `
14018
+ );
14019
+ const one = pending[Number(answer.trim()) - 1];
14020
+ if (!one) {
14021
+ o.out("Nothing was changed.\n");
14022
+ return 1;
14023
+ }
14024
+ s = one;
14025
+ }
14026
+ }
14027
+ const host = hostFor(o, s.host);
14028
+ const noun = host?.noun ?? "session";
14029
+ const place = host ? placeOf(host, s.cwd) : "that session";
14030
+ const reopen = host?.reopen ?? `open the ${noun}`;
14031
+ if (TERMINAL_STATUSES.has(s.status)) {
14032
+ o.out(
14033
+ `Nothing to continue: the resume of ${place} ${s.status === "sent" ? "was already sent" : s.status === "cancelled" ? "was cancelled" : "has already ended"}.
14034
+ `
14035
+ );
14036
+ return 0;
14037
+ }
14038
+ if (s.status === "sending") {
14039
+ o.out(`Rewake is already continuing ${place}. It finishes on its own.
14040
+ `);
14041
+ return 1;
14042
+ }
14043
+ if (!WAITING.has(s.status)) {
14044
+ o.out(
14045
+ `The resume of ${place} isn't waiting for the reset any more (${s.status.replace("_", " ")}), so there is nothing to run now. ${capital2(reopen)} to continue it yourself.
14046
+ `
14047
+ );
14048
+ return 1;
14049
+ }
14050
+ const window = o.hosts.find((h) => h.id === s.host && h.maxWaitAfterLimitMs !== void 0);
14051
+ if (window) {
14052
+ o.out(
14053
+ `Rewake can't continue ${place} from here: ${windowReason(window)}. Type /rewake in the ${noun}, or ${reopen} to continue it yourself.
14054
+ `
14055
+ );
14056
+ return 1;
14057
+ }
14058
+ o.out(
14059
+ `Rewake will continue ${place} now, without waiting for the reset (planned ${formatAt(s.dueAt, now)}).
14060
+ Message, sent once as if you had typed it: "${oneLine2(s.text)}"
14061
+ Nothing is sent, and your resume stays planned for its time, if you typed in the ${noun} since the limit, if it is open or has changed, if the agent has already continued, if the usage limit is still on, or if the message can't reach the agent (signed out, session deleted or archived, folder gone).
14062
+ This window stays busy until the agent finishes (up to 3 hours); waiting for a free place to run can take a few minutes. Ctrl+C stops it and cancels this planned resume.
14063
+ `
14064
+ );
14065
+ if (o.yes !== true && !/^y(es)?$/i.test((await o.ask("Continue it now? [y/N] ")).trim())) {
14066
+ o.out("Nothing was changed.\n");
14067
+ return 1;
14068
+ }
14069
+ const outcome2 = await o.fireNow?.(s.scheduleId) ?? "not-ours";
14070
+ const after = store.get(s.scheduleId);
14071
+ switch (outcome2) {
14072
+ case "sent":
14073
+ o.out(`Rewake continued ${place}. ${capital2(reopen)} to see what it did.
14074
+ `);
14075
+ return 0;
14076
+ case "limited":
14077
+ o.out(
14078
+ `The usage limit is still on, so nothing was sent. Your resume is still planned for ${formatAt(s.dueAt, now)}.
14079
+ `
14080
+ );
14081
+ return 0;
14082
+ case "open":
14083
+ o.out(
14084
+ `The ${noun} is open, so nothing was sent. Close it and run this again, or it still resumes at ${formatAt(s.dueAt, now)}.
14085
+ `
14086
+ );
14087
+ return 1;
14088
+ case "changed":
14089
+ o.out(
14090
+ `The ${noun} has changed since it stopped, so nothing was sent. Your resume is still planned for ${formatAt(s.dueAt, now)}, and will send nothing then either. ${capital2(reopen)} to continue it yourself, or cancel the plan: ${rewake(`continue --cancel ${s.scheduleId.slice(0, 8)}`)}
14091
+ `
14092
+ );
14093
+ return 1;
14094
+ case "typed":
14095
+ case "native":
14096
+ o.out(
14097
+ `${outcome2 === "typed" ? `You typed in the ${noun} after the limit` : "The agent has already continued by itself"}, so nothing was sent. Your resume is still planned for ${formatAt(s.dueAt, now)}, and will send nothing then either. To remove it: ${rewake(`continue --cancel ${s.scheduleId.slice(0, 8)}`)}
14098
+ `
14099
+ );
14100
+ return 1;
14101
+ case "skipped":
14102
+ if (after?.failureReason !== "you") o.out("Rewake sent nothing.\n");
14103
+ return 1;
14104
+ case "busy":
14105
+ o.out(`Rewake is already working on ${place}. Try again in a minute.
14106
+ `);
14107
+ return 1;
14108
+ case "notified":
14109
+ case "failed":
14110
+ if (after && WAITING.has(after.status))
14111
+ o.out(`Your resume is still planned for ${formatAt(after.dueAt, now)}.
14112
+ `);
14113
+ return 1;
14114
+ default:
14115
+ o.out(`Nothing was sent: the resume of ${place} had changed or ended.
14116
+ `);
14117
+ return 1;
14118
+ }
14119
+ }
14120
+ var windowReason, WAITING, capital2;
13870
14121
  var init_continue = __esm({
13871
14122
  "src/continue.ts"() {
13872
14123
  "use strict";
@@ -13876,9 +14127,13 @@ var init_continue = __esm({
13876
14127
  init_time();
13877
14128
  init_closed();
13878
14129
  init_sessions();
14130
+ init_overview();
13879
14131
  init_command2();
13880
14132
  init_printable();
13881
14133
  init_sleep_settings();
14134
+ windowReason = (h) => `${h.name} can only be continued from the window that is waiting for it, within 4 hours of its usage limit`;
14135
+ WAITING = /* @__PURE__ */ new Set(["scheduled", "waiting_for_limit"]);
14136
+ capital2 = (text2) => `${text2.charAt(0).toUpperCase()}${text2.slice(1)}`;
13882
14137
  }
13883
14138
  });
13884
14139
 
@@ -14364,7 +14619,7 @@ function diagnose(ctx) {
14364
14619
  area: "Rewake",
14365
14620
  level: "todo",
14366
14621
  text: "Zed starts Rewake through npm or one Node.js folder. An upgrade of that Node.js, or no network, can stop those agents from starting.",
14367
- fix: `Run the install command again, then quit Zed completely and open it again: ${update}`
14622
+ fix: `Run the install command again: ${update} Then ${reloadAgent(platform6)}.`
14368
14623
  });
14369
14624
  const pins = /* @__PURE__ */ new Set();
14370
14625
  for (const entry of wrappedEntries) {
@@ -14377,7 +14632,7 @@ function diagnose(ctx) {
14377
14632
  area: "Rewake",
14378
14633
  level: "todo",
14379
14634
  text: `Zed starts Rewake ${pin}; this is Rewake ${ctx.version}.`,
14380
- fix: `To update, run: ${update} Then quit Zed completely and open it again.`
14635
+ fix: `To update, run: ${update} Then ${reloadAgent(platform6)}.`
14381
14636
  });
14382
14637
  else if (compareVersions(pin, ctx.version) > 0)
14383
14638
  add({
@@ -14846,7 +15101,7 @@ var init_doctor = __esm({
14846
15101
  });
14847
15102
 
14848
15103
  // src/doctor-report.ts
14849
- import { createHash as createHash4 } from "node:crypto";
15104
+ import { createHash as createHash5 } from "node:crypto";
14850
15105
  import { join as join37 } from "node:path";
14851
15106
  function redactor(home, ids) {
14852
15107
  const homes = [.../* @__PURE__ */ new Set([home, home.replace(/\\/g, "/")])].filter((h) => h.length > 1);
@@ -14949,7 +15204,7 @@ var init_doctor_report = __esm({
14949
15204
  LEFT_OUT = /* @__PURE__ */ new Set(["pid", "text", "prompt", "body", "content", "title", "cwd", "folder"]);
14950
15205
  FIRE_EVENTS = /^(fire\.|hook$|hook\.|limit\.|schedule\.settled|schedule\.delivering)/;
14951
15206
  short2 = (s) => s.slice(0, 8);
14952
- hash = (id) => createHash4("sha256").update(id).digest("hex");
15207
+ hash = (id) => createHash5("sha256").update(id).digest("hex");
14953
15208
  iso = (t) => new Date(t).toISOString();
14954
15209
  }
14955
15210
  });
@@ -15411,7 +15666,9 @@ var init_settings_command = __esm({
15411
15666
  "auto-when-prompts-skipped",
15412
15667
  "clock",
15413
15668
  "keep-awake",
15414
- "resume-prompt"
15669
+ "resume-prompt",
15670
+ "workspace-changed",
15671
+ "usage-warnings"
15415
15672
  ];
15416
15673
  SETTINGS = [
15417
15674
  {
@@ -15478,6 +15735,40 @@ var init_settings_command = __esm({
15478
15735
  return v && v !== DEFAULT_RESUME_PROMPT ? { ...rest, resumePrompt: v } : rest;
15479
15736
  },
15480
15737
  saved: (v) => v && v !== DEFAULT_RESUME_PROMPT ? "Resumes send your message from now on." : "Resumes send Rewake's own message again."
15738
+ },
15739
+ {
15740
+ name: "workspace-changed",
15741
+ title: "Project changed while waiting",
15742
+ short: "What Rewake does when the project changed while a resume waited, outside Zed.",
15743
+ more: "It compares the project's latest git commit and its tracked files (their names and status, never their contents, only on this computer).",
15744
+ choices: () => [
15745
+ ["tell", "Only tell you, and send the message as it is"],
15746
+ ["note", "Also add a line asking the agent to re-read files before editing"],
15747
+ ["hold", "Send nothing and ask you first"],
15748
+ ["off", "Don't check"]
15749
+ ],
15750
+ get: (s) => s.workspaceChanged,
15751
+ set: (s, v) => ({
15752
+ ...s,
15753
+ workspaceChanged: WORKSPACE_CHANGED_VALUES.find((x) => x === v) ?? s.workspaceChanged
15754
+ }),
15755
+ saved: (v) => v === "hold" ? "When the project changed while a resume waited, Rewake sends nothing and tells you." : v === "off" ? "Rewake won't check whether the project changed while a resume waited." : v === "note" ? "When the project changed while a resume waited, Rewake tells you and adds a line asking the agent to re-read files." : "When the project changed while a resume waited, Rewake tells you and sends the message as it is."
15756
+ },
15757
+ {
15758
+ name: "usage-warnings",
15759
+ title: "Warnings before a usage limit",
15760
+ short: "Whether Claude Code warns you when a usage window is nearly used up.",
15761
+ more: "Claude Code only. It uses Claude Code's own usage figures, with no network and no sign-in, and never types anything for you.",
15762
+ choices: () => [
15763
+ ["on", "Warn at 80% and 95% used, and when the pace would reach the limit within 45 minutes"],
15764
+ ["off", "Don't warn"]
15765
+ ],
15766
+ get: (s) => s.usageWarnings,
15767
+ set: (s, v) => ({
15768
+ ...s,
15769
+ usageWarnings: USAGE_WARNINGS_VALUES.find((x) => x === v) ?? s.usageWarnings
15770
+ }),
15771
+ saved: (v) => v === "off" ? "Claude Code won't warn you before a usage limit." : "Claude Code warns you at 80% and 95% of a usage limit, and when your pace would reach it within 45 minutes."
15481
15772
  }
15482
15773
  ];
15483
15774
  orList2 = (v) => v.length < 2 ? v[0] ?? "" : `${v.slice(0, -1).join(", ")} or ${v.at(-1)}`;
@@ -15781,10 +16072,12 @@ var init_help = __esm({
15781
16072
  {
15782
16073
  name: "continue",
15783
16074
  summary: "Continue a closed agent session after its usage limit",
15784
- usage: ["continue [--always | --ask | --cancel [<id>]]"],
16075
+ usage: ["continue [--always | --ask | --cancel [<id>] | --now [<id>] [--yes]]"],
15785
16076
  about: [
15786
16077
  "Continues a session you closed at its usage limit, when the limit resets. Run alone, it asks",
15787
- "which session and when."
16078
+ "which session and when.",
16079
+ "--now is for a planned resume outside Zed that shouldn't wait for the reset (you upgraded",
16080
+ "your plan, or the limit lifted early). In Zed, use Rewake \u2192 Change a scheduled message\u2026."
15788
16081
  ],
15789
16082
  options: [
15790
16083
  { flag: "--always", text: "Continue sessions by itself from now on" },
@@ -15792,11 +16085,27 @@ var init_help = __esm({
15792
16085
  {
15793
16086
  flag: "--cancel",
15794
16087
  text: "Cancel a planned resume: pick it, or give its id"
16088
+ },
16089
+ {
16090
+ flag: "--now",
16091
+ arg: "[<id>]",
16092
+ text: "Continue a planned resume now, without waiting for the reset (outside Zed)"
16093
+ },
16094
+ {
16095
+ flag: "--yes",
16096
+ alias: "-y",
16097
+ text: "With --now: don't ask first (needed without a terminal)"
15795
16098
  }
15796
16099
  ],
15797
16100
  footer: [
15798
- "With several planned and a terminal, --cancel lists them to pick one, or all. The ids are",
15799
- "in square brackets in `agent-rewake schedules`."
16101
+ "With several planned and a terminal, --cancel and --now list them to pick one (--cancel also",
16102
+ "all). --now shows what it will send and asks first; without a terminal it needs the id and",
16103
+ "--yes. It stays busy until the agent finishes; Ctrl+C stops it and cancels that resume. If the",
16104
+ "limit is still on, the session is open or changed, you typed after the limit, the agent",
16105
+ "continued, or the message can't reach the agent (signed out, session deleted or archived,",
16106
+ "folder gone), nothing is sent and the resume stays planned. Other errors end it as failed.",
16107
+ "The ids are in square",
16108
+ "brackets in `agent-rewake schedules`."
15800
16109
  ]
15801
16110
  },
15802
16111
  {
@@ -16916,7 +17225,10 @@ function codexAdapter(deps) {
16916
17225
  try {
16917
17226
  const started = s.attempts.length > 0 ? Math.min(...s.attempts.map((a) => a.startedAt)) : void 0;
16918
17227
  if (started === void 0) return void 0;
16919
- return userMessages(readTail2(transcript, 512 * 1024), started).includes(s.text) || void 0;
17228
+ const sent = [s.text, `${s.text}
17229
+
17230
+ ${WORKSPACE_NOTE}`];
17231
+ return userMessages(readTail2(transcript, 512 * 1024), started).some((m) => sent.includes(m)) || void 0;
16920
17232
  } catch {
16921
17233
  return void 0;
16922
17234
  }
@@ -16943,6 +17255,7 @@ var init_adapter = __esm({
16943
17255
  "src/hosts/codex/adapter.ts"() {
16944
17256
  "use strict";
16945
17257
  init_resume();
17258
+ init_workspace();
16946
17259
  init_cli();
16947
17260
  init_rollout();
16948
17261
  }
@@ -16994,7 +17307,10 @@ function codexHooks(deps) {
16994
17307
  }
16995
17308
  };
16996
17309
  store.put(resume);
16997
- if (deps.arm(resume.scheduleId, at) !== false) return true;
17310
+ if (deps.arm(resume.scheduleId, at) !== false) {
17311
+ withWorkspace(store, resume, ctx.now);
17312
+ return true;
17313
+ }
16998
17314
  store.cancel(resume.scheduleId, ctx.now);
16999
17315
  return false;
17000
17316
  };
@@ -18816,9 +19132,17 @@ import { basename as basename10 } from "node:path";
18816
19132
  function runningText(at, noun = "session") {
18817
19133
  return `${at}: Rewake is continuing the ${noun} now. You'll get a notification when it's done. To stop it: "${rewake("continue --cancel")}".`;
18818
19134
  }
19135
+ function foregroundText(at, noun = "session") {
19136
+ return `${at}: Rewake is continuing the ${noun} now. Press Ctrl+C to stop it.`;
19137
+ }
18819
19138
  function doneText(at, noun = "session", now = Date.now()) {
18820
19139
  return `${at}: Rewake's run ended normally ${formatAt(now, now)}. Open the ${noun} to see what it did.`;
18821
19140
  }
19141
+ function workspaceNoteText(noun, added) {
19142
+ const what = `The project's tracked files or latest commit changed while the ${noun} waited`;
19143
+ const how = added ? "so Rewake added a line to its message asking the agent to re-read files before editing." : "Rewake sent its message as it was. To have it add a line asking the agent to re-read files, choose note; to have it ask you first, choose hold.";
19144
+ return `${what}${added ? "," : "."} ${how} Change this with: ${rewake("settings workspace-changed")} (tell, note, hold or off).`;
19145
+ }
18822
19146
  function where(host, s) {
18823
19147
  const folder = s.cwd ? basename10(s.cwd) : "";
18824
19148
  return folder ? `${host.name} in the "${folder}" folder` : host.name;
@@ -18832,6 +19156,13 @@ function notice(why, agent, now, f) {
18832
19156
  return `${agent}: the usage limit has reset. The ${n} is open, so Rewake didn't send anything. Continue it there.`;
18833
19157
  case "changed":
18834
19158
  return `${agent}: the usage limit has reset, but the ${n} has changed since it stopped, so Rewake didn't send anything. ${cap(reopen)} to see where it stands and continue.`;
19159
+ case "workspace": {
19160
+ const what = [
19161
+ ...f.workspace?.commit ? ["its latest commit moved"] : [],
19162
+ ...f.workspace?.changes ? ["its tracked files differ"] : []
19163
+ ].join(" and ");
19164
+ return `${agent}: the usage limit has reset, but the project changed while the ${n} waited (${what}), so Rewake didn't send anything. ${f.again ? f.again(void 0) : `To continue anyway, run "${rewake("continue")}", or ${reopen}.`} Change this check with: ${rewake("settings workspace-changed")} (tell, note, hold or off).`;
19165
+ }
18835
19166
  case "late":
18836
19167
  return `${agent}: Rewake was due to continue the ${n}${f.dueAt ? ` ${formatAt(f.dueAt, now)}` : ""}, but couldn't run then (the computer may have been off or asleep). ${cap(reopen)} to continue.`;
18837
19168
  case "far-reset":
@@ -18880,6 +19211,14 @@ function rearmEarly(id, store, deps, now) {
18880
19211
  deps.log?.("fire.early", { armed: armed.ok });
18881
19212
  return "early";
18882
19213
  }
19214
+ function keptOutcome(d) {
19215
+ if (d.action === "notify") return d.why === "open" || d.why === "changed" ? d.why : "limited";
19216
+ if (d.action === "skip") {
19217
+ if (d.why === "typed" || d.why === "native") return d.why;
19218
+ return d.why === "expired" ? "limited" : "gone";
19219
+ }
19220
+ return "limited";
19221
+ }
18883
19222
  async function fire(id, deps) {
18884
19223
  const store = new ScheduleStore(deps.stateDir);
18885
19224
  const log = deps.log ?? (() => {
@@ -18905,7 +19244,7 @@ async function fire(id, deps) {
18905
19244
  }
18906
19245
  const lock = new SessionLock(deps.stateDir);
18907
19246
  const lockKey = `fire:${id}`;
18908
- if (first.status !== "cancelled" && now < first.dueAt - EARLY_MS) {
19247
+ if (!deps.immediate && first.status !== "cancelled" && now < first.dueAt - EARLY_MS) {
18909
19248
  if (!lock.acquire(lockKey)) return "early";
18910
19249
  try {
18911
19250
  return rearmEarly(id, store, deps, now);
@@ -18931,7 +19270,7 @@ async function fire(id, deps) {
18931
19270
  removeTimer();
18932
19271
  return "gone";
18933
19272
  }
18934
- if (s.status !== "cancelled" && now < s.dueAt - EARLY_MS)
19273
+ if (!deps.immediate && s.status !== "cancelled" && now < s.dueAt - EARLY_MS)
18935
19274
  return rearmEarly(id, store, deps, now);
18936
19275
  const wait2 = (until, why, count = true) => {
18937
19276
  const next = Math.max(until, now + MIN_ARM_MS);
@@ -18964,7 +19303,10 @@ async function fire(id, deps) {
18964
19303
  const place = await takeSlot({ stateDir: deps.stateDir, ...deps.slots }).catch(
18965
19304
  () => "error"
18966
19305
  );
18967
- if (place === void 0) return wait2(now + MIN_ARM_MS, "no-free-place", false);
19306
+ if (place === void 0) {
19307
+ if (deps.immediate) return "busy";
19308
+ return wait2(now + MIN_ARM_MS, "no-free-place", false);
19309
+ }
18968
19310
  if (place !== "error") slot.release = place;
18969
19311
  }
18970
19312
  const settings = loadSettings(deps.stateDir);
@@ -18977,7 +19319,7 @@ async function fire(id, deps) {
18977
19319
  }
18978
19320
  const decision = decideFire({
18979
19321
  resume: {
18980
- dueAt: s.dueAt,
19322
+ dueAt: deps.immediate ? now : s.dueAt,
18981
19323
  status: s.status,
18982
19324
  ...s.rearms !== void 0 && { rearms: s.rearms }
18983
19325
  },
@@ -18997,7 +19339,7 @@ async function fire(id, deps) {
18997
19339
  }),
18998
19340
  now
18999
19341
  );
19000
- const tell = (why, status, cause, message) => {
19342
+ const tell = (why, status, cause, message, workspace) => {
19001
19343
  settle(status, cause ?? why, message);
19002
19344
  if (facts.newResetsAt !== void 0 && (why === "far-reset" || why === "expired"))
19003
19345
  store.update(id, (x) => ({ ...x, dueAt: (facts.newResetsAt ?? 0) + RESET_MARGIN_MS }), now);
@@ -19010,12 +19352,17 @@ async function fire(id, deps) {
19010
19352
  ...host.again !== void 0 && { again: host.again },
19011
19353
  dueAt: s.dueAt,
19012
19354
  ...facts.newResetsAt !== void 0 && { resetsAt: facts.newResetsAt },
19013
- ...cause && { cause }
19355
+ ...cause && { cause },
19356
+ ...workspace && { workspace }
19014
19357
  })
19015
19358
  );
19016
19359
  removeTimer();
19017
19360
  return why === "failed" || why === "expired" ? "failed" : "notified";
19018
19361
  };
19362
+ if (deps.immediate && decision.action !== "send") {
19363
+ log("fire.kept", { host: host.id, action: decision.action });
19364
+ return keptOutcome(decision);
19365
+ }
19019
19366
  log("fire.decide", {
19020
19367
  host: host.id,
19021
19368
  action: decision.action,
@@ -19023,6 +19370,19 @@ async function fire(id, deps) {
19023
19370
  });
19024
19371
  switch (decision.action) {
19025
19372
  case "send": {
19373
+ let noted = false;
19374
+ let changed = false;
19375
+ if (s.workspace && settings.workspaceChanged !== "off") {
19376
+ const here = fingerprintWorkspace(s.cwd, FIRE_BUDGET_MS);
19377
+ const differs = here && workspaceChange(s.workspace, here);
19378
+ if (differs) {
19379
+ log("fire.workspace", { action: settings.workspaceChanged });
19380
+ if (settings.workspaceChanged === "hold")
19381
+ return tell("workspace", "needs_attention", void 0, void 0, differs);
19382
+ changed = true;
19383
+ noted = settings.workspaceChanged === "note";
19384
+ }
19385
+ }
19026
19386
  const claimed = store.update(
19027
19387
  id,
19028
19388
  (x) => x.status !== s.status || x.dueAt !== s.dueAt ? void 0 : {
@@ -19050,6 +19410,7 @@ async function fire(id, deps) {
19050
19410
  const wake = deps.wake ?? new Wakefulness();
19051
19411
  wake.set(true, settings.keepAwake);
19052
19412
  let watch2;
19413
+ let told = false;
19053
19414
  onResumeRun((pid) => {
19054
19415
  store.update(
19055
19416
  id,
@@ -19059,7 +19420,12 @@ async function fire(id, deps) {
19059
19420
  }),
19060
19421
  now
19061
19422
  );
19062
- deps.notify("Agent Rewake", runningText(at, host.noun));
19423
+ told = changed;
19424
+ const running = deps.immediate ? foregroundText(at, host.noun) : runningText(at, host.noun);
19425
+ deps.notify(
19426
+ "Agent Rewake",
19427
+ changed ? `${running} ${workspaceNoteText(host.noun, noted)}` : running
19428
+ );
19063
19429
  watch2 = setInterval(() => {
19064
19430
  if (!store.get(id)?.attempts.find((a) => a.idempotencyKey === key2)?.stopped) return;
19065
19431
  clearInterval(watch2);
@@ -19067,7 +19433,12 @@ async function fire(id, deps) {
19067
19433
  }, deps.stopPollMs ?? STOP_POLL_MS);
19068
19434
  });
19069
19435
  try {
19070
- result = await host.send(s, key2);
19436
+ result = await host.send(
19437
+ noted ? { ...s, text: `${s.text}
19438
+
19439
+ ${WORKSPACE_NOTE}` } : s,
19440
+ key2
19441
+ );
19071
19442
  } catch (err) {
19072
19443
  result = { ok: false, reason: "failed", detail: err.message };
19073
19444
  } finally {
@@ -19075,7 +19446,7 @@ async function fire(id, deps) {
19075
19446
  if (watch2) clearInterval(watch2);
19076
19447
  wake.release();
19077
19448
  }
19078
- if (store.get(id)?.attempts.find((a) => a.idempotencyKey === key2)?.stopped) {
19449
+ if (store.get(id)?.attempts.find((a) => a.idempotencyKey === key2)?.stopped && !(deps.immediate && result.ok)) {
19079
19450
  settle("stopped", "you");
19080
19451
  removeTimer();
19081
19452
  log("fire.stopped", {});
@@ -19093,10 +19464,25 @@ async function fire(id, deps) {
19093
19464
  if (result.ok) {
19094
19465
  settle("sent");
19095
19466
  removeTimer();
19096
- if (store.get(id)?.attempts.find((a) => a.idempotencyKey === key2)?.pid !== void 0)
19467
+ if (changed && !told)
19468
+ deps.notify("Agent Rewake", `${at}: ${workspaceNoteText(host.noun, noted)}`);
19469
+ if (!deps.immediate && store.get(id)?.attempts.find((a) => a.idempotencyKey === key2)?.pid !== void 0)
19097
19470
  deps.notify("Agent Rewake", doneText(at, host.noun, deps.now()));
19098
19471
  return "sent";
19099
19472
  }
19473
+ if (deps.immediate && (result.reason === "limited" || result.reason === "busy")) {
19474
+ store.update(
19475
+ id,
19476
+ (x) => ({
19477
+ ...x,
19478
+ status: s.status,
19479
+ attempts: x.attempts.filter((a) => a.idempotencyKey !== key2)
19480
+ }),
19481
+ now
19482
+ );
19483
+ log("fire.still-limited", { host: host.id, now: true });
19484
+ return result.reason === "limited" ? "limited" : "busy";
19485
+ }
19100
19486
  if (result.reason === "limited" || result.reason === "busy") {
19101
19487
  const rearms = s.rearms ?? 0;
19102
19488
  const reset = result.reason === "limited" ? result.resetsAt : void 0;
@@ -19118,6 +19504,27 @@ async function fire(id, deps) {
19118
19504
  "needs-approval",
19119
19505
  "folder-gone"
19120
19506
  ].includes(result.detail ?? "") ? result.detail : void 0;
19507
+ if (deps.immediate && cause !== void 0 && !["timeout", "needs-approval"].includes(cause)) {
19508
+ store.update(
19509
+ id,
19510
+ (x) => ({
19511
+ ...x,
19512
+ status: s.status,
19513
+ attempts: x.attempts.filter((a) => a.idempotencyKey !== key2)
19514
+ }),
19515
+ now
19516
+ );
19517
+ deps.notify(
19518
+ "Agent Rewake",
19519
+ notice("failed", at, now, {
19520
+ noun: host.noun,
19521
+ agentName: host.name,
19522
+ ...host.reopen !== void 0 && { reopen: host.reopen },
19523
+ cause
19524
+ })
19525
+ );
19526
+ return "failed";
19527
+ }
19121
19528
  return tell("failed", "failed", cause, result.message);
19122
19529
  }
19123
19530
  case "wait":
@@ -19193,6 +19600,7 @@ var init_fire = __esm({
19193
19600
  init_command2();
19194
19601
  init_keep_awake();
19195
19602
  init_spawn2();
19603
+ init_workspace();
19196
19604
  init_slots();
19197
19605
  init_timers();
19198
19606
  EARLY_MS = 3e4;
@@ -21277,14 +21685,23 @@ ${loginItemPlanText(process.platform)}`);
21277
21685
  return code3;
21278
21686
  }
21279
21687
  if (first === "continue") {
21688
+ if (argv.includes("--now")) {
21689
+ const rest = argv.slice(1).filter((a) => a !== "--now");
21690
+ const yes = rest.some((a) => a === "--yes" || a === "-y");
21691
+ const names = rest.filter((a) => a !== "--yes" && a !== "-y");
21692
+ const nowId = names[0];
21693
+ if (names.length > 1 || nowId?.startsWith("-")) {
21694
+ process.stderr.write(CONTINUE_USAGE);
21695
+ return 2;
21696
+ }
21697
+ return runContinueCommand(env, "now", void 0, nowId, yes);
21698
+ }
21280
21699
  const flag = argv[1];
21281
21700
  const mode = flag === "--always" ? "always" : flag === "--ask" ? "ask" : flag === "--cancel" ? "cancel" : void 0;
21282
21701
  const cancelId = mode === "cancel" ? argv[2] : void 0;
21283
21702
  const extra = argv.length > (cancelId === void 0 ? 2 : 3);
21284
21703
  if (flag !== void 0 && mode === void 0 || extra || cancelId?.startsWith("-")) {
21285
- process.stderr.write(
21286
- "agent-rewake: usage: agent-rewake continue [--always | --ask | --cancel [<id>]]\n"
21287
- );
21704
+ process.stderr.write(CONTINUE_USAGE);
21288
21705
  return 2;
21289
21706
  }
21290
21707
  return runContinueCommand(env, mode, cancelId);
@@ -22014,14 +22431,70 @@ async function prompt(question) {
22014
22431
  rl.close();
22015
22432
  }
22016
22433
  }
22017
- async function runContinueCommand(env, mode, cancelId) {
22434
+ async function runContinueCommand(env, mode, cancelId, nowId, yes = false) {
22018
22435
  const { state, timers, sweepDeps, node } = timerDeps(env);
22019
22436
  sweepQuietly(env);
22020
22437
  const adapters = hostAdapters(env, node, state);
22021
- return runContinue({
22438
+ const log = new Logger(env);
22439
+ let interrupted = false;
22440
+ let sentAnyway = false;
22441
+ const code2 = await runContinue({
22022
22442
  sleepSettings: () => readSleepSettings({ env }),
22023
22443
  ...mode && { mode },
22024
22444
  ...cancelId !== void 0 && { cancelId },
22445
+ ...nowId !== void 0 && { nowId },
22446
+ yes,
22447
+ // The resume's own timer run, at once; what it would tell by notification is said here.
22448
+ fireNow: async (id) => {
22449
+ const store = new ScheduleStore(state);
22450
+ const onInterrupt = () => {
22451
+ if (interrupted) {
22452
+ const pid = store.get(id)?.attempts.at(-1)?.pid;
22453
+ if (pid !== void 0) {
22454
+ killPidTree(pid, void 0, void 0, "SIGKILL");
22455
+ process.stdout.write("Ended the agent now.\n");
22456
+ }
22457
+ return;
22458
+ }
22459
+ interrupted = true;
22460
+ const s = store.get(id);
22461
+ if (s !== void 0 && s.status !== "sending" && !WAITING_STATES.has(s.status)) return;
22462
+ if (s?.status !== "sending") {
22463
+ process.stdout.write("Stopped. Nothing was sent, and your resume is still planned.\n");
22464
+ process.exit(130);
22465
+ }
22466
+ store.update(
22467
+ id,
22468
+ (x) => ({
22469
+ ...x,
22470
+ attempts: x.attempts.map(
22471
+ (a, i) => i === x.attempts.length - 1 ? { ...a, stopped: true } : a
22472
+ )
22473
+ }),
22474
+ Date.now()
22475
+ );
22476
+ process.stdout.write(
22477
+ "Stopping. The agent ends within a few seconds; what it has done so far stays. The planned resume is cancelled too.\n"
22478
+ );
22479
+ };
22480
+ process.on("SIGINT", onInterrupt);
22481
+ try {
22482
+ const outcome2 = await fire(id, {
22483
+ stateDir: state,
22484
+ now: Date.now,
22485
+ hosts: adapters,
22486
+ timers,
22487
+ notify: (_title, body) => process.stdout.write(`${body}
22488
+ `),
22489
+ log: (event, fields2) => log.info(event, fields2),
22490
+ immediate: true
22491
+ });
22492
+ sentAnyway = outcome2 === "sent";
22493
+ return outcome2;
22494
+ } finally {
22495
+ process.off("SIGINT", onInterrupt);
22496
+ }
22497
+ },
22025
22498
  // Codex's resumes too, which `continue` doesn't arm itself.
22026
22499
  hostOf: (id) => adapters.get(id),
22027
22500
  hosts: CLOSED_HOSTS,
@@ -22041,11 +22514,12 @@ async function runContinueCommand(env, mode, cancelId) {
22041
22514
  err: (t) => process.stderr.write(t),
22042
22515
  ask: prompt
22043
22516
  });
22517
+ return interrupted && !sentAnyway ? 130 : code2;
22044
22518
  }
22045
22519
  function previewNames(env) {
22046
22520
  return installedPreviews(env, homedir23(), stateDir(env)).map((id) => PREVIEW_NAMES[id] ?? id);
22047
22521
  }
22048
- var INSTALL_PLACES, KNOWN_OPTIONS, TIMER_PLACES;
22522
+ var INSTALL_PLACES, KNOWN_OPTIONS, WAITING_STATES, CONTINUE_USAGE, TIMER_PLACES;
22049
22523
  var init_cli2 = __esm({
22050
22524
  "src/cli.ts"() {
22051
22525
  "use strict";
@@ -22120,6 +22594,8 @@ var init_cli2 = __esm({
22120
22594
  schedules: ["--all", "--json", "--explain"],
22121
22595
  history: ["--days"]
22122
22596
  };
22597
+ WAITING_STATES = /* @__PURE__ */ new Set(["scheduled", "waiting_for_limit"]);
22598
+ CONTINUE_USAGE = "agent-rewake: usage: agent-rewake continue [--always | --ask | --cancel [<id>] | --now [<id>] [--yes]]\n";
22123
22599
  TIMER_PLACES = /* @__PURE__ */ new Set([
22124
22600
  "codex",
22125
22601
  "copilot-cli",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rewake",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Continues this session when your usage limit resets, and sends messages you schedule into it.",
5
5
  "author": {
6
6
  "name": "Codize Labs",
@@ -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: await read($.env.get("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")) void onMeasure($, e.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.4.0",
3
+ "version": "0.5.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",