claude-usage-limits 1.24.0 → 1.25.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "usage-limits",
3
3
  "displayName": "Usage Limits",
4
- "version": "1.24.0",
4
+ "version": "1.25.0",
5
5
  "description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.",
6
6
  "author": {
7
7
  "name": "Ridelink",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usage-limits",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "description": "Reports how much of your Codex usage limit is left as turns of work rather than a percentage, prices a job before you start it, and counts the other agents sharing the same budget.",
5
5
  "author": {
6
6
  "name": "Ridelink",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-usage-limits",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.",
5
5
  "keywords": [
6
6
  "claude",
@@ -643,7 +643,11 @@ function schedulePosix(when, argv, name, cwd) {
643
643
  encoding: 'utf8',
644
644
  timeout: 20000,
645
645
  });
646
- if (run.status === 0) return { ok: true, how: 'at' };
646
+ // macOS ships `at` but launchd leaves atrun DISABLED by default, so the
647
+ // command succeeds, prints a job id, and the wake never fires. Reporting
648
+ // ok for that is worse than not having it: it is a silent no-op that looks
649
+ // like a scheduled wake. Fall through to the sleeper, which at least runs.
650
+ if (run.status === 0 && process.platform !== 'darwin') return { ok: true, how: 'at' };
647
651
  }
648
652
  try {
649
653
  const child = spawn('sh', ['-c', 'sleep ' + seconds + ' && ' + command], {
@@ -767,16 +771,41 @@ function arm(input) {
767
771
  }
768
772
  : null,
769
773
  };
770
- if (state.armed && state.armed.id !== id && state.armed.task) cancelSchedule(state.armed.task);
774
+ // Displacing somebody else's live relay is legitimate - one machine, one
775
+ // relay - but it must not be silent. A wake still in the future belonged to
776
+ // work that somebody expected to be picked up.
777
+ if (state.armed && state.armed.id !== id) {
778
+ if (Number.isFinite(state.armed.wakeAt) && state.armed.wakeAt > now) {
779
+ note('displacing the relay armed for ' + state.armed.id + ' (was due ' + new Date(state.armed.wakeAt).toISOString() + ')', now);
780
+ }
781
+ if (state.armed.task) cancelSchedule(state.armed.task);
782
+ }
771
783
  state.armed = record;
772
784
  write(state);
773
785
  note('armed ' + id + ' for ' + new Date(when).toISOString() + ' via ' + scheduled.how, now);
774
786
  return { ok: true, record };
775
787
  }
776
788
 
777
- function disarm(reason, now) {
789
+ // `id` is optional, and when it is given it is a guard rather than a lookup:
790
+ // clear the relay only if the thing armed is the thing the caller means.
791
+ //
792
+ // Without it, disarm clears whatever happens to be armed, and that is not
793
+ // hypothetical. Measured 2026-09-14: a second process armed a throwaway
794
+ // session at 04:57:04 and cleaned it up four seconds later, and the cleanup
795
+ // took a live relay for an unrelated session - armed seven seconds earlier,
796
+ // due to wake five hours later - with it. Nothing reported that, because from
797
+ // disarm's point of view it did exactly what it was asked.
798
+ //
799
+ // A person typing `relay cancel` means "whatever is armed", so the CLI passes
800
+ // no id and the old behaviour stands. Anything that knows which session it is
801
+ // tidying up should say so.
802
+ function disarm(reason, now, id) {
778
803
  const state = read();
779
804
  if (!state.armed) return { ok: true, changed: false };
805
+ if (id && state.armed.id !== id) {
806
+ note('refused to disarm ' + state.armed.id + ' on behalf of ' + id, Number.isFinite(now) ? now : Date.now());
807
+ return { ok: true, changed: false, refused: true, armed: state.armed.id };
808
+ }
780
809
  const record = state.armed;
781
810
  if (record.task) cancelSchedule(record.task);
782
811
  state.history.push(Object.assign({}, record, { endedAt: Number.isFinite(now) ? now : Date.now(), outcome: reason || 'cancelled' }));
@@ -146,6 +146,10 @@ function userIsPresent(cli) {
146
146
  function claudeArgs(record, prompt, config, fallback) {
147
147
  const args = fallback ? ['--continue', '-p', prompt] : ['--resume', record.id, '-p', prompt];
148
148
  if (config.permissionMode) args.push('--permission-mode', config.permissionMode);
149
+ // Nobody is awake to answer a prompt. Deny it rather than stall on it: a
150
+ // resumed run that sits waiting for a keystroke burns its whole timeout and
151
+ // reports nothing.
152
+ args.push('--permission-prompts', 'none');
149
153
  if (!fallback && config.model) args.push('--model', config.model);
150
154
  return args;
151
155
  }
@@ -186,12 +190,29 @@ function deliverClaude(record, prompt, config, cli, runs) {
186
190
  const run = appendRun(runs, 'claude --resume', spawnSync(cli, args, options));
187
191
  if (run.status === 0) return { ok: true, how: 'claude --resume' };
188
192
  const detail = ((run.stderr || run.stdout || '') + '').trim().split('\n')[0];
189
- // A session id that no longer resolves is the one failure worth a second
190
- // attempt: the work still needs doing, only the thread is gone.
193
+ // A session id that no longer resolves used to fall back to `--continue`,
194
+ // on the reasoning that the work still needs doing and only the thread is
195
+ // gone. That fallback is removed, because of what `--continue` actually
196
+ // selects.
197
+ //
198
+ // `--continue` normally SKIPS sessions created by `claude -p`, the SDK and
199
+ // /loop - but `claude -p --continue`, which is exactly what this ran,
200
+ // INCLUDES them. So the most recent session it could land on is a previous
201
+ // relay's own headless run, not the user's work. Resuming that, unattended,
202
+ // at four in the morning, with permissionMode bypassPermissions, means an
203
+ // agent continuing a conversation nobody chose, in a directory it was not
204
+ // asked about.
205
+ //
206
+ // The plan is on disk either way. A wake that stops and says the thread is
207
+ // gone loses nothing; a wake that resumes the wrong conversation can.
191
208
  if (/No conversation found/i.test(detail)) {
192
- const second = appendRun(runs, 'claude --continue', spawnSync(cli, claudeArgs(record, prompt, config, true), options));
193
- if (second.status === 0) return { ok: true, how: 'claude --continue' };
194
- return { ok: false, error: ((second.stderr || second.stdout || '') + '').trim().split('\n')[0] || detail };
209
+ return {
210
+ ok: false,
211
+ error:
212
+ 'the session ' + String(record.id).slice(0, 8) + ' no longer exists, so there was nothing to resume. ' +
213
+ 'The plan is still on disk: run "claude" in ' + record.cwd + ' and paste it in.',
214
+ permanent: true,
215
+ };
195
216
  }
196
217
  return { ok: false, error: detail || 'claude exited ' + run.status };
197
218
  }