claude-usage-limits 1.40.3 → 1.40.4

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.40.3",
4
+ "version": "1.40.4",
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.40.3",
3
+ "version": "1.40.4",
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/README.md CHANGED
@@ -393,20 +393,20 @@ relayed run either.
393
393
  At the wall with weekly headroom, the line looks like this:
394
394
 
395
395
  ```
396
- If the wall offers it, /low-priority carries this session past the 5-hour limit
397
- at lower priority instead of stopping: it spends the weekly limit, which is at
398
- 45% and so has room, and replies may pause while it waits for spare capacity -
399
- the wait and its ceiling are set by the server per request. It is a toggle: you
400
- type it yourself and run it again to stop, and nothing here can switch it on
401
- for you.
396
+ If the wall offers it, /low-priority can carry this session past the 5-hour
397
+ limit at lower priority instead of stopping: it spends the weekly limit, which
398
+ is at 45% and so has room, and replies may pause while it waits for spare
399
+ capacity - the wait and its ceiling are set by the server per request. It is a
400
+ toggle only the user can type, and type again to stop: you cannot run a slash
401
+ command, and nothing here can switch it on.
402
402
  ```
403
403
 
404
404
  **The recommendation has a number behind it.** It is offered only while the
405
405
  weekly is at or below **80 per cent** and the 5-hour window is the binding wall.
406
406
  Above that the brief actively says not to, and cites the figure: low-priority
407
407
  spends the weekly *and* draws on a weekly allowance whose size is exposed to no
408
- hook and no file, and a real user measured it emptying most of a week in a
409
- couple of hours. No wait time is ever printed, because the retry and the ceiling
408
+ hook and no file, and one user has reported it burning through almost a week of
409
+ usage in a couple of hours (anthropics/claude-code#92544). No wait time is ever printed, because the retry and the ceiling
410
410
  come from `lowPriorityRetryAfterSeconds` and `lowPriorityMaxWaitSeconds` on each
411
411
  response — any fixed "20 seconds, 20 minutes" would be invented.
412
412
 
@@ -442,13 +442,19 @@ wake. So when a resume relay is armed and this is on, the brief says so: the
442
442
  wake is the route for a session that will be **closed** at the reset, and both
443
443
  firing for the same reset would start the work twice and spend the weekly twice.
444
444
 
445
- **`/limit-reset`**, the once-weekly manual session reset, is detected but not
446
- built on: this account holds no grant (`tengu_cedar_ember` absent,
447
- `cachedUsageUtilization.cedar_ember` null), so a feature resting on it would be
448
- untestable. If a grant appears, the brief names it at the wall, says it only
449
- works while you are actually at a limit, and says the work it unlocks still
450
- spends the weekly. It never reports how many are left — `resets_left` comes from
451
- a live endpoint and appears in no file a hook can read.
445
+ **`/limit-reset`** is one command behind two different server flags, and the
446
+ brief words each in its own copy's terms. `tengu_nifty_lemur` is the
447
+ once-a-week reset of the 5-hour session limit ("uses weekly limit · 1/week",
448
+ "your weekly limit still applies"); `tengu_cedar_ember` is a counted grant with
449
+ a use-by date that "refills your limits". The account this was built on carries
450
+ `tengu_nifty_lemur` enabled and no `tengu_cedar_ember`. When the budget is tight
451
+ and the 5-hour window is the wall, the brief names the weekly reset, says the
452
+ work it unlocks still counts toward the weekly, and says only the user can type
453
+ it; at a weekly wall it says nothing, because resetting the 5-hour limit cannot
454
+ help there. It never reports whether this week's reset is used or how many
455
+ grants are left — both come from a live endpoint and appear in no file a hook
456
+ can read. A flag counts only when it is a plain object with `enabled: true`,
457
+ the way the CLI reads it.
452
458
 
453
459
  **Grace is not weekly spend.** The allowance the wall gives you is metered in
454
460
  its own per-window meters (`anthropic-ratelimit-unified-grace-5h-utilization`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-usage-limits",
3
- "version": "1.40.3",
3
+ "version": "1.40.4",
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",
@@ -625,10 +625,11 @@ longer stops at. **Record it only when they said so.** Whether it is running is
625
625
  in the CLI's process memory and readable nowhere, so nothing here may infer it,
626
626
  and `mode --low-priority off` is how it is taken back.
627
627
 
628
- The same applies to `/limit-reset`, the once-weekly manual session reset: the
629
- line names it only if a grant is actually readable, it only works while you are
630
- at a limit, the work it unlocks still spends the weekly, and how many are left is
631
- not knowable from here.
628
+ The same applies to `/limit-reset`, the manual reset: only the user can type
629
+ it. The line names it only when the account's flags say it is set up. The
630
+ once-a-week kind resets the 5-hour limit and its work still counts toward the
631
+ weekly, so it is named only at a 5-hour wall; whether this week's is already
632
+ used, and how many of the counted kind are left, is not knowable from here.
632
633
 
633
634
  And if the budget line says Claude Code is injecting its own wrap-up note at this
634
635
  wall, follow that note: it is more specific than anything here, and two
@@ -1028,53 +1028,99 @@ function briefText(input) {
1028
1028
  // style drops only what reads the same every turn. This does not.
1029
1029
  const lp = parts.lowPriority || null;
1030
1030
  let lowPrioritySentence = null;
1031
- if (lp && lp.state === 'acknowledged') {
1031
+ // The acknowledged sentence describes the SWAP, so it may only be said when
1032
+ // the swap really happened. wallFeatures refuses to swap unless there is a
1033
+ // weekly window with a live percentage and a known reset, and this branch
1034
+ // used to fire on the acknowledgement alone - so on an account with no
1035
+ // readable weekly the brief said "binding window is 5-hour 99% used" in one
1036
+ // clause and "the figures above are the weekly" in the next. A line that
1037
+ // contradicts the numbers printed beside it is worse than no line: it tells
1038
+ // the model to ignore a wall that is still there.
1039
+ if (lp && lp.state === 'acknowledged' && lp.weeklyBinding) {
1032
1040
  lowPrioritySentence = (
1033
- 'You have said /low-priority is on, so the brake is the weekly window and not the 5-hour one: ' +
1041
+ 'The user has said /low-priority is on, so the brake is the weekly window and not the 5-hour one: ' +
1034
1042
  (Number.isFinite(lp.fiveHourPercent)
1035
1043
  ? 'the 5-hour limit is at ' + lp.fiveHourPercent + '% and no longer stops this session, '
1036
1044
  : 'the 5-hour limit no longer stops this session, ') +
1037
1045
  'the figures above are the weekly, and replies may pause while lower priority waits for spare ' +
1038
- 'capacity. Whether it is still on cannot be read from here, so if it has ended say so with ' +
1039
- 'usage-mode --low-priority off and the 5-hour wall counts again. It also draws on a separate ' +
1046
+ 'capacity. Whether it is still on cannot be read from here, so if the user says it has ended, ' +
1047
+ 'record that with usage-mode --low-priority off and the 5-hour wall counts again. It also draws on a separate ' +
1040
1048
  'weekly lower-priority allowance that is exposed to no hook and no file, so nothing here can ' +
1041
1049
  'track how much of that is left.'
1042
1050
  );
1051
+ } else if (lp && lp.state === 'acknowledged') {
1052
+ // Acknowledged, but there is no weekly reading to brake on, so the figures
1053
+ // above are still the window they say they are and the wall is still real.
1054
+ lowPrioritySentence = (
1055
+ 'The user has said /low-priority is on, which spends the weekly limit instead of stopping at the ' +
1056
+ '5-hour one - but there is no usable weekly reading here to brake on, so the figures above are ' +
1057
+ 'the window named beside them and that window is still the one to plan against. /usage (the ' +
1058
+ 'user types it) refreshes the account snapshot; until it has a weekly, treat the limit above as real.'
1059
+ );
1043
1060
  } else if (lp && lp.advise && lp.advise.kind === 'offer') {
1044
1061
  lowPrioritySentence = (
1045
- 'If the wall offers it, /low-priority carries this session past the 5-hour limit at lower ' +
1062
+ 'If the wall offers it, /low-priority can carry this session past the 5-hour limit at lower ' +
1046
1063
  'priority instead of stopping: it spends the weekly limit, which is at ' +
1047
1064
  lp.advise.weeklyPercent + '% and so has room, and replies may pause while it waits for spare ' +
1048
- 'capacity - the wait and its ceiling are set by the server per request. It is a toggle: you ' +
1049
- 'type it yourself and run it again to stop, and nothing here can switch it on for you. Say ' +
1050
- 'in one line that it is there; if it is taken, usage-mode --low-priority on is what makes ' +
1051
- 'this line brake on the weekly instead.'
1065
+ 'capacity - the wait and its ceiling are set by the server per request. It is a toggle only ' +
1066
+ 'the user can type, and type again to stop: you cannot run a slash command, and nothing here ' +
1067
+ 'can switch it on. Tell the user in one line that it is there; if the user says it is taken, ' +
1068
+ 'usage-mode --low-priority on is what makes this line brake on the weekly instead.'
1052
1069
  );
1053
1070
  } else if (lp && lp.advise && lp.advise.kind === 'hold') {
1054
1071
  lowPrioritySentence = (
1055
1072
  'The wall may offer /low-priority here, and it is not worth taking: it spends the weekly limit, ' +
1056
1073
  'which is already at ' + lp.advise.weeklyPercent + '% - past the ' + lp.advise.threshold +
1057
1074
  ' per cent this plugin will recommend it at - and it draws on a weekly lower-priority ' +
1058
- 'allowance as well, which has been measured emptying most of a week in a couple of hours. ' +
1059
- 'Say in one line that the answer is no and why; waiting out the 5-hour reset is the cheaper move.'
1075
+ 'allowance as well, which one user has reported burning through almost a week of usage in a ' +
1076
+ 'couple of hours. The choice is the user\'s, and only the user can type it: say in one line ' +
1077
+ 'that this plugin advises against it and why; waiting out the 5-hour reset is the cheaper move.'
1060
1078
  );
1061
1079
  }
1062
1080
  if (lowPrioritySentence) sentences.push(lowPrioritySentence);
1063
- // A manual session reset, if this account ever gets one.
1081
+ // A manual reset behind /limit-reset, when this account holds one.
1082
+ //
1083
+ // Two server flags sit behind the one command and they are different offers,
1084
+ // so each is worded in its own copy's terms and nothing is borrowed across
1085
+ // (lowpri.sessionReset has the bundle strings):
1086
+ //
1087
+ // 'weekly' (tengu_nifty_lemur, which this account has): resets the 5-hour
1088
+ // session limit, once a week, and the work still counts toward the weekly.
1089
+ // So it only helps when the 5-hour window is the wall - said at a weekly
1090
+ // wall it would be advice that cannot work - and the first build, reading
1091
+ // only the other flag, never said it at all.
1064
1092
  //
1065
- // /limit-reset refills the 5-hour window, works only AT a limit, and is once a
1066
- // week - and the work it unlocks still spends the weekly, which is the part
1067
- // worth saying out loud. This account holds no grant today
1068
- // (tengu_cedar_ember absent, cachedUsageUtilization.cedar_ember null), so the
1069
- // sentence is a detector rather than a feature: nothing is claimed about how
1070
- // many resets are left, because resets_left is served by a live endpoint and
1071
- // appears in no file a hook can read.
1072
- if (lp && lp.resetGrant && (parts.pressure === 'tight' || parts.pressure === 'gone')) {
1093
+ // 'grant' (tengu_cedar_ember): a counted reset with a use-by date that
1094
+ // "refills your limits". Its copy does not say it spends the weekly, and it
1095
+ // has an early-use path, so neither "once a week" nor "only at a limit" is
1096
+ // claimed for it.
1097
+ //
1098
+ // Neither is counted: whether this week's reset is already used, and how
1099
+ // many grants are left, are served by the API and appear in no file. And the
1100
+ // CLI refuses a reset while lower priority runs ("Resets can't be used while
1101
+ // you continue at lower priority"), which is why the weekly variant stays
1102
+ // silent once the brief has swapped to the weekly, and the grant says so.
1103
+ const resetVariant = lp && lp.resetGrant ? lp.resetVariant || 'grant' : null;
1104
+ const resetPressure = parts.pressure === 'tight' || parts.pressure === 'gone';
1105
+ const fiveHourWall = Boolean(parts.binding && parts.binding.key === 'five_hour');
1106
+ // Acknowledged is checked on its own as well as through the binding: with no
1107
+ // weekly reading the brief cannot swap, the 5-hour window stays binding, and
1108
+ // the reset would be offered to a session the CLI will not reset.
1109
+ if (resetVariant === 'weekly' && resetPressure && fiveHourWall && lp.state !== 'acknowledged') {
1110
+ sentences.push(
1111
+ 'This account is set up for a manual session reset (/limit-reset): once a week it resets the ' +
1112
+ '5-hour limit, and the work it unlocks still counts toward the weekly, so it moves the 5-hour ' +
1113
+ 'wall rather than adding budget. The server decides whether it can be used right now, and ' +
1114
+ "whether this week's is already used is not readable from here. Like /low-priority, only the user can type " +
1115
+ 'it; you cannot run a slash command.'
1116
+ );
1117
+ } else if (resetVariant === 'grant' && resetPressure) {
1073
1118
  sentences.push(
1074
- 'A once-weekly manual session reset appears to be available on this account (/limit-reset). It ' +
1075
- 'only works while you are actually AT a limit, and the work it unlocks still spends the weekly, ' +
1076
- 'so it moves the 5-hour wall rather than adding budget. How many are left is not readable from ' +
1077
- 'here - the CLI asks the server for that. It is yours to type, like /low-priority.'
1119
+ 'This account appears to hold a limit reset (/limit-reset), which the CLI describes as refilling ' +
1120
+ 'your limits while the weekly reset day stays put. How many are left and until when is not ' +
1121
+ 'readable from here - the CLI asks the server for that.' +
1122
+ (lp.state === 'acknowledged' ? ' The CLI will not use one while lower priority is on.' : '') +
1123
+ ' Like /low-priority, only the user can type it; you cannot run a slash command.'
1078
1124
  );
1079
1125
  }
1080
1126
  if (parts.session) {
@@ -1524,9 +1570,17 @@ function wallFeatures(now, binding, windows, hostName) {
1524
1570
  if (weekly && Number.isFinite(weekly.percentUsed) && !weekly.stale && Number.isFinite(weekly.resetsAt)) {
1525
1571
  out.binding = weekly;
1526
1572
  out.swapped = true;
1527
- out.lowPriority = Object.assign({}, info, { weeklyBinding: true });
1528
1573
  }
1529
1574
  }
1575
+ // Whether the weekly really is the window the figures describe, which is
1576
+ // what the acknowledged sentence claims. True when the swap just happened
1577
+ // AND when the weekly was already binding on its own - the 5-hour window
1578
+ // reading low is the ordinary case for a session that has been running at
1579
+ // lower priority for a while. False when there was no weekly to brake on,
1580
+ // and then the sentence has to say so instead of claiming otherwise.
1581
+ out.lowPriority = Object.assign({}, info, {
1582
+ weeklyBinding: Boolean(out.binding && out.binding.key === 'seven_day'),
1583
+ });
1530
1584
  } catch (err) {
1531
1585
  // Nothing about these features is worth a failed prompt.
1532
1586
  }
@@ -39,13 +39,14 @@
39
39
  // 30-minute freshness cutoff on cached window readings, nothing to do with
40
40
  // this.)
41
41
  //
42
- // /limit-reset - a once-weekly manual refill of the 5-hour window, usable
43
- // only AT a limit, whose work still spends the weekly. Gated on
44
- // tengu_cedar_ember, which is ABSENT from this account's feature cache, with
45
- // cachedUsageUtilization.utilization.cedar_ember null. So there is nothing to
46
- // spend here and nothing is built on it: only a detector that starts
47
- // reporting if a grant ever appears. resets_left is served by a live
48
- // endpoint, never a file, so the count is never claimed.
42
+ // /limit-reset - a manual reset, behind two different server flags (see
43
+ // sessionReset below). tengu_nifty_lemur is the once-a-week reset of the
44
+ // 5-hour session limit whose work still counts toward the weekly, and THIS
45
+ // ACCOUNT HAS IT (enabled:true). tengu_cedar_ember is a counted grant with a
46
+ // use-by date, absent here, with cachedUsageUtilization.utilization.cedar_ember
47
+ // null. Nothing is spent by the plugin - only the user can type the command -
48
+ // and whether this week's reset is used, or how many grants are left, is
49
+ // served by the API and never a file, so neither is ever claimed.
49
50
  //
50
51
  // The graceful wrap-up note - the CLI injecting "finish up" at the wall. The
51
52
  // mechanism is real and the treatment TEXT is provisioned on this machine
@@ -81,6 +82,7 @@ const atomic = require('./atomic.js');
81
82
 
82
83
  const FLAG = 'tengu_toasty_breeze';
83
84
  const RESET_FLAG = 'tengu_cedar_ember';
85
+ const SESSION_RESET_FLAG = 'tengu_nifty_lemur';
84
86
  const WRAPUP_MODE_FLAG = 'tengu_lantern_wick_mode';
85
87
  const WRAPUP_TEXT_FLAG = 'tengu_lantern_wick_text';
86
88
  const NEAR_WALL_FLAG = 'tengu_vellum_anchor';
@@ -95,7 +97,7 @@ const MAX_COOLOFF_MINUTES = 1440;
95
97
  // The recommendation rule, in one number so it can be argued with.
96
98
  //
97
99
  // /low-priority spends the weekly and draws on a weekly allowance whose size is
98
- // exposed nowhere a hook can read, and a real user measured it burning "almost
100
+ // exposed nowhere a hook can read, and one user has reported it burning "almost
99
101
  // a week of usage in a couple hours"
100
102
  // (github.com/anthropics/claude-code/issues/92544). So it is worth naming only
101
103
  // while the weekly still has real room. At or below this it is offered; above
@@ -106,6 +108,22 @@ const WEEKLY_HEADROOM_MAX = 80;
106
108
  // offers the toggle at all.
107
109
  const WALL_PERCENT = 90;
108
110
 
111
+ // How long an acknowledgement with no readable reset time is allowed to live.
112
+ //
113
+ // The record is meant to lapse at the reset of the window it belongs to. When
114
+ // that reset could not be read - a session with no account snapshot, or a
115
+ // snapshot with no five_hour entry - the record used to be written with
116
+ // resetsAt null, and readAck only lapsed a FINITE resetsAt. So it never lapsed:
117
+ // a record written once went on redirecting the headroom maths at the weekly
118
+ // for ever. Probed on 2026-09-26 with an acknowledgement three days old and the
119
+ // 5-hour window reading 0 per cent, and the brief still said the 5-hour limit
120
+ // "no longer stops this session".
121
+ //
122
+ // The window the toggle is offered at is five hours long, so an
123
+ // acknowledgement made against it cannot honestly outlive five hours from when
124
+ // it was made. That is the ceiling, not a guess at when it really ended.
125
+ const ACK_MAX_MS = 5 * 60 * 60 * 1000;
126
+
109
127
  function configDir() {
110
128
  return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
111
129
  }
@@ -203,16 +221,53 @@ function offer(account) {
203
221
  };
204
222
  }
205
223
 
206
- // The manual session reset. Detected, never spent: this account holds no grant,
207
- // so a feature built on it would be untestable here.
224
+ // A flag object the way the CLI reads one: a plain object whose `enabled` is
225
+ // exactly true. The bundle's own readers are
226
+ // function qae(){let e=x("tengu_nifty_lemur",{});return typeof e==="object"&&e!==null&&!Array.isArray(e)?e:{}}
227
+ // function pme(){return qae().enabled===!0}
228
+ // and the same shape for tengu_cedar_ember ($Z()). Truthiness is not enough:
229
+ // `{ enabled: false }` is a truthy object, and the first build counted it as a
230
+ // reset on offer.
231
+ function flagEnabled(gb, key) {
232
+ if (!gb || !Object.prototype.hasOwnProperty.call(gb, key)) return false;
233
+ const value = gb[key];
234
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value) && value.enabled === true);
235
+ }
236
+
237
+ // The manual reset behind /limit-reset. Detected, never spent.
238
+ //
239
+ // There are TWO server flags behind the one command, and they describe
240
+ // different things, so the brief must not word one in the other's terms:
241
+ //
242
+ // tengu_nifty_lemur - "Reset your session limit now and keep working; once a
243
+ // week, still counts toward your weekly limit" (the command's description
244
+ // when this is the variant). Its notice: "/limit-reset to reset your session
245
+ // limit now · uses weekly limit · 1/week". THIS ACCOUNT HAS IT: enabled:true,
246
+ // version 1, read from ~/.claude.json on 2026-09-25. The first build looked
247
+ // only at tengu_cedar_ember, called the account grant-less, and so never said
248
+ // a word about a reset the account actually holds.
249
+ //
250
+ // tengu_cedar_ember - "Use an available limit reset and keep working". A
251
+ // counted grant: "Refills your {limits} now · your weekly reset day stays
252
+ // {week}", "{resets} left · use by {deadline}", and an early-use path ("You
253
+ // haven't reached a limit yet - use your reset anyway?"). Not once a week,
254
+ // not only at a limit, and whether it spends the weekly is not in its copy.
255
+ //
256
+ // The CLI prefers cedar_ember when both are on (its description reads
257
+ // `$Z()?"Use an available limit reset...":"Reset your session limit now..."`),
258
+ // so this does too. Whether this week's reset is already spent, and how many
259
+ // grants are left, come from the server and appear in no file.
208
260
  function sessionReset(account) {
209
261
  const gb = features(account);
210
262
  const util = utilization(account);
211
- const flagged = Boolean(gb && Object.prototype.hasOwnProperty.call(gb, RESET_FLAG) && gb[RESET_FLAG]);
263
+ const grantFlag = flagEnabled(gb, RESET_FLAG);
264
+ const weeklyFlag = flagEnabled(gb, SESSION_RESET_FLAG);
212
265
  const grant = util && util.cedar_ember ? util.cedar_ember : null;
266
+ const variant = grantFlag || grant ? 'grant' : weeklyFlag ? 'weekly' : null;
213
267
  return {
214
268
  known: Boolean(gb || util),
215
- present: Boolean(flagged || grant),
269
+ present: variant !== null,
270
+ variant,
216
271
  // resets_left is served from /api/organizations/<uuid>/reset_rate_limits,
217
272
  // not from any file a hook can read, so it stays unreported.
218
273
  resetsLeft: null,
@@ -276,9 +331,13 @@ function autoContinue() {
276
331
  // session where the 5-hour wall is real again.
277
332
  // ---------------------------------------------------------------------------
278
333
 
334
+ // An array is an object too, and JSON.stringify drops a named property set on
335
+ // one: a state file holding `[]` made acknowledge() write `[]` straight back,
336
+ // report saved:true, and usage-mode print "Recorded" for a statement that the
337
+ // next read could not find. Only a plain object is a state.
279
338
  function readState() {
280
339
  const parsed = readJson(stateFile());
281
- return parsed && typeof parsed === 'object' ? parsed : {};
340
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
282
341
  }
283
342
 
284
343
  function writeState(state) {
@@ -290,14 +349,20 @@ function writeState(state) {
290
349
  }
291
350
  }
292
351
 
352
+ // `saved` is reported, never assumed. A state path that cannot be written -
353
+ // a directory sitting where the file goes, a read-only home - used to return
354
+ // the record anyway, so usage-mode printed "Recorded: you have switched
355
+ // /low-priority on" and then the very next brief behaved as though nothing had
356
+ // been said. Telling somebody their statement was recorded when it was not is
357
+ // the one thing this module must not do.
293
358
  function acknowledge(input) {
294
359
  const options = input || {};
295
360
  const now = Number.isFinite(options.now) ? options.now : Date.now();
296
361
  if (!options.on) {
297
362
  const state = readState();
298
363
  delete state.ack;
299
- writeState(state);
300
- return { on: false, at: now };
364
+ const saved = writeState(state);
365
+ return { on: false, at: now, saved, file: stateFile() };
301
366
  }
302
367
  const record = {
303
368
  on: true,
@@ -307,18 +372,35 @@ function acknowledge(input) {
307
372
  };
308
373
  const state = readState();
309
374
  state.ack = record;
310
- writeState(state);
311
- return record;
375
+ const saved = writeState(state);
376
+ return Object.assign({}, record, { saved, file: stateFile() });
377
+ }
378
+
379
+ // When a record stops being true: the reset of the window it was stamped
380
+ // against, or - when that could not be read - ACK_MAX_MS after it was made.
381
+ // Both are returned as `expiresAt` so a caller never has to work it out twice
382
+ // and get a different answer.
383
+ function ackExpiry(ack) {
384
+ if (!ack) return null;
385
+ if (Number.isFinite(ack.resetsAt)) return ack.resetsAt;
386
+ if (Number.isFinite(ack.at)) return ack.at + ACK_MAX_MS;
387
+ return null;
312
388
  }
313
389
 
314
390
  function readAck(now) {
315
391
  const at = Number.isFinite(now) ? now : Date.now();
316
392
  const state = readState();
317
393
  const ack = state.ack;
318
- if (!ack || ack.on !== true) return null;
319
- // Past the reset of the window it was recorded against, the fact is spent.
320
- if (Number.isFinite(ack.resetsAt) && at >= ack.resetsAt) return null;
321
- return ack;
394
+ if (!ack || typeof ack !== 'object' || ack.on !== true) return null;
395
+ // A record with no usable timestamp is not a record. It printed as
396
+ // "acknowledged ... at Invalid Date" before this, off a hand-edited or
397
+ // half-written file, which is the plugin quoting garbage back as fact.
398
+ if (!Number.isFinite(ack.at)) return null;
399
+ // Past the reset of the window it was recorded against - or past the length
400
+ // of that window, when no reset was readable - the fact is spent.
401
+ const expiresAt = ackExpiry(ack);
402
+ if (Number.isFinite(expiresAt) && at >= expiresAt) return null;
403
+ return Object.assign({}, ack, { expiresAt, expiryKnown: Number.isFinite(ack.resetsAt) });
322
404
  }
323
405
 
324
406
  // Exactly three states, and activeKnown is false in every one of them.
@@ -388,6 +470,7 @@ function forBrief(input) {
388
470
  weeklyPercent: weekly && Number.isFinite(weekly.percentUsed) ? Math.round(weekly.percentUsed) : null,
389
471
  fiveHourPercent: five && Number.isFinite(five.percentUsed) ? Math.round(five.percentUsed) : null,
390
472
  resetGrant: sessionReset(account).present,
473
+ resetVariant: sessionReset(account).variant,
391
474
  credits: credits(account),
392
475
  autoContinue: autoContinue(),
393
476
  };
@@ -396,6 +479,7 @@ function forBrief(input) {
396
479
  module.exports = {
397
480
  FLAG,
398
481
  RESET_FLAG,
482
+ SESSION_RESET_FLAG,
399
483
  WRAPUP_MODE_FLAG,
400
484
  WRAPUP_TEXT_FLAG,
401
485
  NEAR_WALL_FLAG,
@@ -404,12 +488,15 @@ module.exports = {
404
488
  MAX_COOLOFF_MINUTES,
405
489
  WEEKLY_HEADROOM_MAX,
406
490
  WALL_PERCENT,
491
+ ACK_MAX_MS,
492
+ ackExpiry,
407
493
  configDir,
408
494
  stateFile,
409
495
  accountFiles,
410
496
  snapshot,
411
497
  offer,
412
498
  sessionReset,
499
+ flagEnabled,
413
500
  wrapUp,
414
501
  credits,
415
502
  autoContinue,
@@ -1505,7 +1505,12 @@ function main(argv) {
1505
1505
  const usage = require('./usage.js');
1506
1506
  const windows = usage.snapshotWindows(usage.collect(now), now, null) || [];
1507
1507
  const five = windows.find((w) => w && w.key === 'five_hour');
1508
- if (five && Number.isFinite(five.resetsAt)) {
1508
+ // Only a reset still ahead. A stale snapshot carries the reset of a
1509
+ // window that has already rolled over, and stamping that on the record
1510
+ // printed "Recorded ... lapses at" a time already gone - and the very
1511
+ // next read found it spent, so the statement vanished. The next reset
1512
+ // is unknown then, and the record takes the five-hour ceiling instead.
1513
+ if (five && Number.isFinite(five.resetsAt) && five.resetsAt > now) {
1509
1514
  windowKey = 'five_hour';
1510
1515
  resetsAt = five.resetsAt;
1511
1516
  }
@@ -1513,7 +1518,18 @@ function main(argv) {
1513
1518
  // No reading is not a reason to refuse the user's own statement; the
1514
1519
  // record simply has no expiry to hang on.
1515
1520
  }
1516
- lowpri.acknowledge({ on: true, windowKey, resetsAt, now });
1521
+ const written = lowpri.acknowledge({ on: true, windowKey, resetsAt, now });
1522
+ // A write that did not happen is never reported as one. With a directory
1523
+ // sitting where the state file goes this said "Recorded" and the next
1524
+ // brief behaved as though nothing had been said.
1525
+ if (!written.saved) {
1526
+ return [
1527
+ 'NOT recorded: ' + written.file + ' could not be written, so nothing was saved and the ' +
1528
+ 'brief will go on treating the 5-hour window as a real wall.',
1529
+ '',
1530
+ 'Check that the path is a writable file and not a directory, then run this again.',
1531
+ ].join('\n');
1532
+ }
1517
1533
  logChange({ plane: 'mode', key: 'low-priority', from: 'unknown', to: 'on', by: 'user', reason: null }, now);
1518
1534
  return [
1519
1535
  'Recorded: you have switched /low-priority on.',
@@ -1530,14 +1546,25 @@ function main(argv) {
1530
1546
  '',
1531
1547
  resetsAt
1532
1548
  ? 'This lapses on its own at ' + new Date(resetsAt).toLocaleString() + ', when the 5-hour window resets.'
1533
- : 'No 5-hour reset time was readable, so this has no expiry: clear it by hand when it ends.',
1549
+ : 'No 5-hour reset time was readable, so this lapses at ' +
1550
+ new Date(now + lowpri.ACK_MAX_MS).toLocaleString() + ' instead - five hours, the length of ' +
1551
+ 'the window the toggle belongs to, which is the longest it could honestly still be true. ' +
1552
+ 'Run /usage and set it again if it is still on then.',
1534
1553
  '',
1535
1554
  detail,
1536
1555
  ].join('\n');
1537
1556
  }
1538
1557
  if (asked === 'off' || asked === 'no' || asked === 'false') {
1539
1558
  const had = lowpri.readAck(now);
1540
- lowpri.acknowledge({ on: false, now });
1559
+ const cleared = lowpri.acknowledge({ on: false, now });
1560
+ // The same rule as "on": a clear that did not reach the file is not
1561
+ // reported as one, because the record it failed to remove still steers
1562
+ // the brief at the weekly.
1563
+ if (had && !cleared.saved) {
1564
+ return 'NOT cleared: ' + cleared.file + ' could not be written, so the acknowledgement is still ' +
1565
+ 'there and the brief will go on braking on the weekly until it lapses at ' +
1566
+ new Date(had.expiresAt).toLocaleString() + '. Check that the file is writable, then run this again.';
1567
+ }
1541
1568
  logChange({ plane: 'mode', key: 'low-priority', from: had ? 'on' : 'unknown', to: 'off', by: 'user', reason: null }, now);
1542
1569
  return had
1543
1570
  ? 'Cleared: low-priority is off again, so the 5-hour window counts as a wall from the next prompt.'
@@ -1550,9 +1577,15 @@ function main(argv) {
1550
1577
  const wrap = lowpri.wrapUp(account);
1551
1578
  const auto = lowpri.autoContinue();
1552
1579
  return [
1580
+ // readAck drops a record with no usable timestamp, so there is no longer
1581
+ // a path that prints "at Invalid Date" here, and the expiry is always a
1582
+ // real time: the window's reset, or five hours from the statement.
1553
1583
  ack
1554
1584
  ? 'You have acknowledged low-priority ON, at ' + new Date(ack.at).toLocaleString() +
1555
- (Number.isFinite(ack.resetsAt) ? ', lapsing at ' + new Date(ack.resetsAt).toLocaleString() : ', with no expiry') + '.'
1585
+ (Number.isFinite(ack.expiresAt)
1586
+ ? ', lapsing at ' + new Date(ack.expiresAt).toLocaleString() +
1587
+ (ack.expiryKnown ? ' when the 5-hour window resets' : ' (no reset time was readable, so this is five hours from the statement)')
1588
+ : '') + '.'
1556
1589
  : 'Low-priority has not been acknowledged, so the plugin treats the 5-hour window as a real wall.',
1557
1590
  '',
1558
1591
  'It is a toggle you type yourself. The model cannot run a slash command, and /low-priority is ' +
@@ -1496,11 +1496,30 @@ async function armByHand(rest) {
1496
1496
  });
1497
1497
  if (!result.ok) return 'Could not arm: ' + result.error;
1498
1498
  if (text) saveContinuation(sessionId, text);
1499
+ // armable() refuses a 5-hour wake while low-priority is acknowledged, and
1500
+ // this path never went through it: `relay arm --session <id>` booked the wake
1501
+ // anyway and said nothing, which is the double-start the whole gate exists to
1502
+ // prevent. An explicit command is still honoured - it is what somebody typed
1503
+ // - but it says what `defer reset` says, so the person deciding has the fact.
1504
+ let lowPriorityNote = '';
1505
+ if (binding.key === 'five_hour' && usage.currentHost() === host.CLAUDE) {
1506
+ try {
1507
+ if (lowpri.readAck(now)) {
1508
+ lowPriorityNote =
1509
+ ' Note: you have said /low-priority is on, so this session carries past the 5-hour reset ' +
1510
+ 'on its own and this wake would start the work a second time. Arm against the weekly ' +
1511
+ 'instead, or say usage-mode --low-priority off if it has ended; "relay cancel" calls this one off.';
1512
+ }
1513
+ } catch (err) {
1514
+ // A missing or unreadable record is not a reason to refuse a command.
1515
+ }
1516
+ }
1499
1517
  return (
1500
1518
  'Armed by hand for session ' + sessionId.slice(0, 8) + ': wake at ' +
1501
1519
  new Date(result.record.wakeAt).toLocaleString() + ' via ' + result.record.how + '.' +
1502
1520
  (result.record.preflight && result.record.preflight.length ? ' Pre-answered: ' + result.record.preflight.join(', ') + '.' : '') +
1503
- (text ? ' Continuation saved.' : ' No continuation yet - add one with: relay note "<text>"')
1521
+ (text ? ' Continuation saved.' : ' No continuation yet - add one with: relay note "<text>"') +
1522
+ lowPriorityNote
1504
1523
  );
1505
1524
  }
1506
1525
 
@@ -559,8 +559,17 @@ function mine(held, record) {
559
559
  }
560
560
 
561
561
  async function run(now, argv, overrides) {
562
+ // `reachable` belongs in here with the rest. Left out, the preflight below
563
+ // called the real network on every run of the retry tests, so the whole
564
+ // launch-failure suite passed only on a machine that could reach
565
+ // api.anthropic.com - and went red, in the offline branch, on a laptop with
566
+ // its wifi off or behind a TLS-inspecting proxy. That is the exact condition
567
+ // the relay is built for, so it is the last thing its tests should need.
562
568
  const deps = Object.assign(
563
- { windowReopened, deliverClaude, deliverCodex, toast, userIsPresent, arm: relay.arm, capabilities: relay.capabilities },
569
+ {
570
+ windowReopened, deliverClaude, deliverCodex, toast, userIsPresent,
571
+ arm: relay.arm, capabilities: relay.capabilities, reachable: net.reachable,
572
+ },
564
573
  overrides || null
565
574
  );
566
575
  const id = argOf(argv, '--id');
@@ -618,7 +627,7 @@ async function run(now, argv, overrides) {
618
627
  // So: ask first, before spending a launch on it, and give being offline its
619
628
  // own much longer budget. A machine that cannot reach the API has not failed.
620
629
  // It is waiting, and waiting is free.
621
- const link = await net.reachable({ timeoutMs: 8000 });
630
+ const link = await deps.reachable({ timeoutMs: 8000 });
622
631
  if (!link.online) {
623
632
  const offlineAttempt = (record.offlineAttempt || 0) + 1;
624
633
  if (offlineAttempt <= config.offlineAttempts) {