troxy-cli 1.29.50 → 1.29.51

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "troxy-cli",
3
- "version": "1.29.50",
3
+ "version": "1.29.51",
4
4
  "description": "A secure control layer for AI agents: policies across payments, messages, logins, destructive actions, model usage, and secrets, all enforceable from the CLI",
5
5
  "homepage": "https://troxy.io",
6
6
  "bugs": {
@@ -280,9 +280,14 @@ function cancelCompact() {
280
280
  // stayAwake: hold off system sleep (the screen still sleeps) until the timer fires, so a
281
281
  // big session compacts on time instead of when the user is back. macOS caffeinate; where
282
282
  // it is missing the spawn fails quietly and compact-on-return in scorePrompt covers it.
283
- function armCompact($, ms, { stayAwake = false } = {}) {
283
+ function armCompact($, ms, { stayAwake = false, persist = false } = {}) {
284
284
  cancelCompact();
285
285
  if (stayAwake) keepAwake($, ms + 2 * 60 * 1000);
286
+ // Persisted so a restart (which loses lastSent and the timer) still finds the idle start.
287
+ if (persist) try {
288
+ Promise.all([$.session.id(), $.clock.now()])
289
+ .then(([sid, at]) => $.store.set(`idle_started_${sid}`, at)).catch(() => {});
290
+ } catch {}
286
291
  compactTimer = $.clock.after(ms, () => {
287
292
  compactTimer = null;
288
293
  idleCompact($).catch(() => {}).finally(stopAwake);
@@ -346,10 +351,16 @@ async function scorePrompt($, e, next) {
346
351
  cancelCompact();
347
352
  if ((await $.state.get(COMPACTED)).value) await $.state.set(COMPACTED, null);
348
353
  const sid = await $.session.id();
349
- // Idle past the timer by the wall clock: a sleeping Mac freezes the timer, so the
350
- // compaction it missed runs here, before this prompt goes out on the big session.
354
+ // Idle past the timer by the wall clock: a sleeping Mac freezes the timer and a restart
355
+ // loses it, so the compaction it missed runs here, before this prompt goes out.
356
+ const now = await $.clock.now();
351
357
  const ls = lastSent.get(sid);
352
- if (ls && (await $.clock.now()) - ls.at >= COMPACT_IDLE_MS) {
358
+ let idleSince = ls ? ls.at : null;
359
+ try {
360
+ const stored = await $.store.get(`idle_started_${sid}`);
361
+ if (typeof stored === 'number' && (idleSince == null || stored > idleSince)) idleSince = stored;
362
+ } catch {}
363
+ if (idleSince != null && now - idleSince >= COMPACT_IDLE_MS) {
353
364
  try { await idleCompact($, { late: true }); } catch {} // fail open: send the prompt as-is
354
365
  }
355
366
  turn = { start: await snapshot($), ratio: 1 };
@@ -502,7 +513,7 @@ export function register(on) {
502
513
  } catch {} // a missed fetch only leaves the band as it was
503
514
  let stayAwake = false;
504
515
  try { stayAwake = await bigSession($); } catch {}
505
- armCompact($, COMPACT_IDLE_MS, { stayAwake });
516
+ armCompact($, COMPACT_IDLE_MS, { stayAwake, persist: true });
506
517
  return r;
507
518
  });
508
519
  }
@@ -233,6 +233,28 @@ test('scorePrompt: back after 55+ min by the wall clock (Mac slept through the t
233
233
  assert.ok(await runTurn($, opus)); // a failed compaction never blocks the prompt
234
234
  });
235
235
 
236
+ test('scorePrompt: idle 55+ min across a Claude Code restart still compacts, from the saved idle start', async () => {
237
+ const { armCompact, cancelCompact } = _internal;
238
+ const $ = routingApi();
239
+ const db = new Map();
240
+ let now = 1000, compacts = 0;
241
+ $.store = { get: async k => db.get(k), set: async (k, v) => { db.set(k, v); } };
242
+ $.clock.now = async () => now;
243
+ $.clock.after = () => ({ cancel() {} });
244
+ $.session.usage = async () => ({ context: { tokens: 174000 }, rateLimits: [] });
245
+ $.session.compact = async () => { compacts++; return { tokensBefore: 174000, tokensAfter: 20000 }; };
246
+ armCompact($, 55 * 60 * 1000, { persist: true }); // end of the last turn before the restart
247
+ await new Promise(r => setImmediate(r));
248
+ assert.equal(typeof [...db.values()][0], 'number');
249
+ now += 40 * 60 * 1000;
250
+ armCompact($, 55 * 60 * 1000); // session.start after the restart: must not reset it
251
+ await new Promise(r => setImmediate(r));
252
+ now += 16 * 60 * 1000;
253
+ await runTurn($, _internal.MODELS.opus); // fresh process: no lastSent, only the store
254
+ cancelCompact();
255
+ assert.equal(compacts, 1);
256
+ });
257
+
236
258
  test('armCompact: a big session keeps the Mac awake until the timer fires, then lets it sleep', async () => {
237
259
  const { armCompact, cancelCompact } = _internal;
238
260
  const spawned = [], stopped = [];