troxy-cli 1.29.63 → 1.29.64
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.
|
|
3
|
+
"version": "1.29.64",
|
|
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": {
|
|
@@ -369,7 +369,7 @@ async function scorePrompt($, e, next) {
|
|
|
369
369
|
const now = await $.clock.now();
|
|
370
370
|
const { value: shown } = await $.state.get(COMPACTED);
|
|
371
371
|
if (shown && !(now - shown.at < COMPACTED_SHOW_MS)) await $.state.set(COMPACTED, null);
|
|
372
|
-
const ls =
|
|
372
|
+
const ls = await lastSentFor($, sid);
|
|
373
373
|
let idleSince = ls ? ls.at : null;
|
|
374
374
|
try {
|
|
375
375
|
const stored = await $.store.get(`idle_started_${sid}`);
|
|
@@ -398,6 +398,18 @@ async function scorePrompt($, e, next) {
|
|
|
398
398
|
const lastAsked = new Map();
|
|
399
399
|
// The model each session last ran on and when: its cache is warm for about an hour.
|
|
400
400
|
const lastSent = new Map();
|
|
401
|
+
// Kept on disk too: each `claude -p` step and every resume or restart is a new process,
|
|
402
|
+
// and an in-memory-only map made those look cold, so the mod re-picked a model while the
|
|
403
|
+
// cache was still warm (the 2026-10-15/16 A/Bs re-routed at almost every step).
|
|
404
|
+
// ponytail: one small key per session, never pruned; prune with asked: keys if it grows.
|
|
405
|
+
async function lastSentFor($, sid) {
|
|
406
|
+
if (!lastSent.has(sid)) {
|
|
407
|
+
let v = null;
|
|
408
|
+
try { v = (await $.store.get(`sent:${sid}`)) || null; } catch {}
|
|
409
|
+
lastSent.set(sid, v);
|
|
410
|
+
}
|
|
411
|
+
return lastSent.get(sid);
|
|
412
|
+
}
|
|
401
413
|
const WARM_MS = 55 * 60 * 1000;
|
|
402
414
|
// Sessions whose next step is a cold moment because the conversation was just compacted.
|
|
403
415
|
const coldNext = new Set();
|
|
@@ -445,7 +457,7 @@ async function* routeStep($, e, next) {
|
|
|
445
457
|
lastAsked.set(sid, e.model);
|
|
446
458
|
const switched = Boolean(prev && prev !== e.model);
|
|
447
459
|
const want = await askedFor($, sid, e.model, switched);
|
|
448
|
-
const ls =
|
|
460
|
+
const ls = await lastSentFor($, sid);
|
|
449
461
|
const isCold = !ls || now - ls.at >= WARM_MS || coldNext.has(sid);
|
|
450
462
|
const model = (await $.store.get('paused')) ? want : stepModel({
|
|
451
463
|
arm, requested: want, switched,
|
|
@@ -454,6 +466,7 @@ async function* routeStep($, e, next) {
|
|
|
454
466
|
});
|
|
455
467
|
coldNext.delete(sid);
|
|
456
468
|
lastSent.set(sid, { model, at: now });
|
|
469
|
+
try { await $.store.set(`sent:${sid}`, { model, at: now }); } catch {} // a missed write only makes the next process cold
|
|
457
470
|
// The daemon tags every call with the arm, plus from/to when the model changed.
|
|
458
471
|
const file = modRouteFile($.plugin.root, sid);
|
|
459
472
|
const key = `${sid}:${arm}:${want}>${model}`;
|
|
@@ -538,4 +551,4 @@ export function register(on) {
|
|
|
538
551
|
}
|
|
539
552
|
|
|
540
553
|
// Exported for tests only - not part of the mod's runtime contract.
|
|
541
|
-
export const _internal = { askedFor, rememberAsked, asked, armCompact, cancelCompact, toDomainAndFields, guardBash, failClosed, parseScore, asRequest, pickModel, troxyCommand, modRouteFile, estimateTokens, MODELS, savedLine, sessionLine, addTurn, withKept, statusText, compactPlan, savingsText, fetchSavings, scorePrompt, routeStep, stepModel };
|
|
554
|
+
export const _internal = { askedFor, rememberAsked, asked, armCompact, cancelCompact, toDomainAndFields, guardBash, failClosed, parseScore, asRequest, pickModel, troxyCommand, modRouteFile, estimateTokens, MODELS, savedLine, sessionLine, addTurn, withKept, statusText, compactPlan, savingsText, fetchSavings, scorePrompt, routeStep, stepModel, lastSent };
|
|
@@ -188,6 +188,22 @@ test('routeStep: a /model switch wins and becomes the held model', async () => {
|
|
|
188
188
|
assert.equal(await runTurn($, sonnet), sonnet); // warm on sonnet: held, no switch
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
test('routeStep: a new process (claude -p step, resume, restart) keeps the warm model from disk', async () => {
|
|
192
|
+
const { MODELS: M, lastSent } = _internal;
|
|
193
|
+
const disk = new Map();
|
|
194
|
+
let now = 0, score = '0.1';
|
|
195
|
+
const $ = { ...routingApi(),
|
|
196
|
+
store: { get: async (k) => disk.get(k), set: async (k, v) => disk.set(k, v) },
|
|
197
|
+
clock: { now: async () => now },
|
|
198
|
+
model: { complete: async () => ({ isAnswered: true, text: score }) } };
|
|
199
|
+
assert.equal(await runTurn($, M.opus), M.sonnet); // cold start, easy prompt: sonnet
|
|
200
|
+
lastSent.clear(); score = '0.95'; // new process, hard prompt
|
|
201
|
+
now += 10 * 60 * 1000;
|
|
202
|
+
assert.equal(await runTurn($, M.opus), M.sonnet); // cache still warm: held
|
|
203
|
+
lastSent.clear(); now += 56 * 60 * 1000;
|
|
204
|
+
assert.equal(await runTurn($, M.opus), M.opus); // 55+ min idle: cold, re-picked
|
|
205
|
+
});
|
|
206
|
+
|
|
191
207
|
test('stepModel: decide only at cold moments, hold in between, holdout never routes', () => {
|
|
192
208
|
const { stepModel, MODELS: M } = _internal;
|
|
193
209
|
const base = { arm: 'treated', requested: M.opus, switched: false, warm: M.haiku };
|