troxy-cli 1.30.11 → 1.30.13

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.30.11",
3
+ "version": "1.30.13",
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": {
@@ -74,7 +74,7 @@ const HAIKU_BANDS = [[0.2, 'haiku']];
74
74
  // A Haiku session moves up to Sonnet, once and for good, when a later prompt scores above
75
75
  // this or the context passes the Haiku price break (Haiku 5.5 is 5x dearer past 100k tokens).
76
76
  // Never back down: every switch re-writes the cache.
77
- const HAIKU_ESCALATE_ABOVE = 0.6;
77
+ const HAIKU_ESCALATE_ABOVE = 0.3; // job B's bug and fix prompts scored 0.4 to 0.5 and Sonnet won them 8 to 2; at 0.6 they stayed on Haiku
78
78
  const HAIKU_MAX_TOKENS = 100000;
79
79
  const ROUTE_CONTEXT_FLOOR_TOKENS = 50000;
80
80
  const SCORE_TIMEOUT_MS = 3000;
@@ -107,6 +107,23 @@ function rebuildWithText(toolResponse, text) {
107
107
  return { ...toolResponse, content: text };
108
108
  }
109
109
 
110
+ // Only outputs Claude rarely fetches back get shortened. Measured 2026-10-23 on 1,435 shortened
111
+ // outputs: Grep 0% recalled; wc, ls, head, tail, test runs and logs 0-15%; Read of .js/.html
112
+ // 17-20%. Targeted reads (sed, cat, Bash grep, Read of .py/.md/.txt) came back 30-86%, so
113
+ // shortening them only adds a recall step.
114
+ const SHORTEN_COMMANDS = new Set(['wc', 'ls', 'head', 'tail', 'ps', 'gh', 'aws', 'pytest']);
115
+ const SHORTEN_READ_EXT = /\.(js|html)$/i;
116
+
117
+ export function isShortenable(toolName, toolInput) {
118
+ if (toolName === undefined) return true; // ponytail: payloads without tool_name (old tests); Claude Code always sends it
119
+ if (toolName === 'Grep') return true;
120
+ if (toolName === 'Read') return SHORTEN_READ_EXT.test(String(toolInput?.file_path || ''));
121
+ if (toolName !== 'Bash') return false;
122
+ const cmd = String(toolInput?.command || '').trim().replace(/^(cd\s+\S+\s*(&&|;)\s*)+/, '');
123
+ if (/^python3?\s+-m\s+pytest\b/.test(cmd)) return true;
124
+ return SHORTEN_COMMANDS.has(path.basename(cmd.split(/\s+/)[0] || ''));
125
+ }
126
+
110
127
  export async function buildHookResponse(toolUseBlock, {
111
128
  sandboxFraction = 1,
112
129
  holdoutFraction = 0.2,
@@ -124,7 +141,7 @@ export async function buildHookResponse(toolUseBlock, {
124
141
  return undefined;
125
142
  }
126
143
  const output = extractText(toolResponse);
127
- if (output === null || !shouldSandbox(output)) {
144
+ if (output === null || !shouldSandbox(output) || !isShortenable(toolUseBlock?.tool_name, toolUseBlock?.tool_input)) {
128
145
  writeState(storeDir, 'skipped');
129
146
  return undefined;
130
147
  }
@@ -107,7 +107,8 @@ test('stepModel: a Haiku session moves up to Sonnet on a hard prompt or a long c
107
107
  assert.equal(stepModel({ ...base, cold: true, score: 0.1, warm: null, tokens: 150000 }), MODELS.sonnet); // too long to start on Haiku
108
108
  assert.equal(stepModel({ ...base, cold: false, score: 0.1, warm: MODELS.haiku }), MODELS.haiku); // easy prompt: hold
109
109
  assert.equal(stepModel({ ...base, cold: false, score: null, warm: MODELS.haiku }), MODELS.haiku); // no score: hold
110
- assert.equal(stepModel({ ...base, cold: false, score: 0.7, warm: MODELS.haiku }), MODELS.sonnet); // hard prompt: up
110
+ assert.equal(stepModel({ ...base, cold: false, score: 0.4, warm: MODELS.haiku }), MODELS.sonnet); // harder prompt: up
111
+ assert.equal(stepModel({ ...base, cold: false, score: 0.3, warm: MODELS.haiku }), MODELS.haiku); // 0.3 or lower: hold
111
112
  assert.equal(stepModel({ ...base, cold: false, score: 0.1, warm: MODELS.haiku, tokens: 120000 }), MODELS.sonnet); // long: up
112
113
  assert.equal(stepModel({ ...base, cold: false, score: 0.1, warm: MODELS.sonnet }), MODELS.sonnet); // Sonnet never goes down
113
114
  assert.equal(stepModel({ ...base, haiku: false, cold: false, score: 0.1, warm: MODELS.haiku }), MODELS.sonnet); // flag off: up
@@ -271,3 +271,25 @@ test('posttooluse-hook is a no-op unless TROXY_COMPRESS=1', () => {
271
271
  fs.rmSync(tmpHome, { recursive: true, force: true });
272
272
  }
273
273
  });
274
+
275
+ test('isShortenable: only output kinds Claude rarely fetches back', async () => {
276
+ const { isShortenable } = await import('../sandbox/posttooluse-hook.js');
277
+ assert.equal(isShortenable('Grep', {}), true);
278
+ assert.equal(isShortenable('Read', { file_path: '/a/b/app.js' }), true);
279
+ assert.equal(isShortenable('Read', { file_path: '/a/b/notes.md' }), false);
280
+ assert.equal(isShortenable('Read', { file_path: '/a/b/x.py' }), false);
281
+ assert.equal(isShortenable('Bash', { command: 'cd /repo && ls -la src' }), true);
282
+ assert.equal(isShortenable('Bash', { command: 'python3 -m pytest -q' }), true);
283
+ assert.equal(isShortenable('Bash', { command: 'sed -n 1,200p file.js' }), false);
284
+ assert.equal(isShortenable('Bash', { command: 'cat big.log' }), false);
285
+ assert.equal(isShortenable('mcp__claude_ai_Gmail__get_thread', {}), false);
286
+ });
287
+
288
+ test('a large sed output passes through unshortened', async () => {
289
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'troxy-sandbox-'));
290
+ const response = await buildHookResponse(
291
+ { tool_name: 'Bash', tool_input: { command: 'sed -n 1,400p a.js' }, tool_response: bashResult('x'.repeat(5000)) },
292
+ { sandboxFraction: 1, holdoutFraction: 0, randomFn: () => 0.5, storeDir: dir });
293
+ assert.equal(response, undefined);
294
+ assert.equal(readState(dir).state, 'skipped');
295
+ });