claude-token-saver 3.4.1 → 3.4.3

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": "claude-token-saver",
3
- "version": "3.4.1",
3
+ "version": "3.4.3",
4
4
  "description": "Save tokens on Claude Code — spike diagnosis, 1M-context detection, TTL countdown, statusline. (formerly claude-cache-monitor)",
5
5
  "type": "module",
6
6
  "bin": {
package/src/route-scan.js CHANGED
@@ -115,8 +115,10 @@ const MIN_BEHAVIOR_CALLS = 2; // fewer calls than this → too little behavior t
115
115
 
116
116
  /**
117
117
  * Narrow the candidate categories from the episode's tool mix.
118
- * Returns an array of category ids (ordered first is the default when
119
- * keywords stay silent), or null when behavior is inconclusive.
118
+ * Returns { ids, fallback } ids are the candidate categories, fallback is
119
+ * the id to use when keywords stay silent (null = keywords are REQUIRED, an
120
+ * id-less episode is not a delegation candidate). Returns null when behavior
121
+ * is inconclusive.
120
122
  */
121
123
  export function behaviorPool(toolCounts) {
122
124
  let run = 0, lookup = 0, write = 0, total = 0;
@@ -127,9 +129,15 @@ export function behaviorPool(toolCounts) {
127
129
  else if (WRITE_TOOLS.has(name)) write += n;
128
130
  }
129
131
  if (total < MIN_BEHAVIOR_CALLS) return null;
130
- if (run > lookup + write) return ['run'];
131
- if (lookup > run + write) return ['explore', 'read', 'check', 'translate'];
132
- return null; // mixed / write-heavy no reliable verdict, keywords decide
132
+ if (run > lookup + write) return { ids: ['run'], fallback: 'run' };
133
+ if (lookup > run + write) return { ids: ['explore', 'read', 'check', 'translate'], fallback: 'explore' };
134
+ // Write-dominant episodes are EDITING work, not delegable lookups without
135
+ // this they leak into read/explore via generic keywords ("설명이 필요해보이는데"
136
+ // + Edit×5 landed in read/T1). Only translate legitimately writes, so it is
137
+ // the sole candidate — and only with explicit translate keywords (no
138
+ // silent fallback: an editing episode is not a delegation candidate).
139
+ if (write > run + lookup) return { ids: ['translate'], fallback: null };
140
+ return null; // mixed — no reliable verdict, keywords decide
133
141
  }
134
142
 
135
143
  /** Weighted keyword score for one category (0 when it has no kw table). */
@@ -176,7 +184,7 @@ export function categorize(text, toolCounts) {
176
184
  if (text.length >= PASTE_MIN_LEN) return CATEGORIES.find((c) => c.id === 'paste');
177
185
  const pool = behaviorPool(toolCounts);
178
186
  const eligible = pool
179
- ? pool.map((id) => CATEGORIES.find((c) => c.id === id))
187
+ ? pool.ids.map((id) => CATEGORIES.find((c) => c.id === id))
180
188
  : CATEGORIES.filter((c) => c.kw);
181
189
  let best = null;
182
190
  let bestScore = 0;
@@ -185,7 +193,8 @@ export function categorize(text, toolCounts) {
185
193
  if (s > bestScore) { best = c; bestScore = s; }
186
194
  }
187
195
  if (best) return best;
188
- return pool ? eligible[0] : null;
196
+ if (pool?.fallback) return CATEGORIES.find((c) => c.id === pool.fallback);
197
+ return null;
189
198
  }
190
199
 
191
200
  function isSkippable(text) {
@@ -32,6 +32,24 @@ function contentText(content) {
32
32
  const MUTATING_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit', 'Bash']);
33
33
  const DELEGATION_TOOLS = new Set(['Task', 'Agent']);
34
34
 
35
+ // Permission rejections and harness policy denials arrive as is_error
36
+ // tool_results, but they encode the user's choice / the permission system's
37
+ // policy, not task difficulty — counting them poisons the rule-health error
38
+ // rate. Measured on a 14-day window: 6 user rejections + ~29 auto-mode
39
+ // classifier denials out of 142 is_error results (~25% of the numerator).
40
+ const REJECTION_RE = /doesn't want to proceed|tool use was rejected|doesn't want to take this action|denied by the claude code auto mode classifier|permission for this action was denied|requires approval/i;
41
+
42
+ function toolResultText(content) {
43
+ if (typeof content === 'string') return content;
44
+ if (!Array.isArray(content)) return '';
45
+ return content.map((b) => (b && typeof b.text === 'string' ? b.text : '')).join(' ');
46
+ }
47
+
48
+ function isRealToolError(block) {
49
+ return block && block.type === 'tool_result' && block.is_error &&
50
+ !REJECTION_RE.test(toolResultText(block.content));
51
+ }
52
+
35
53
  export async function collectSessionRecords(filePath, { includeContent = true } = {}) {
36
54
  const records = new Map();
37
55
  let depth = 0;
@@ -62,7 +80,7 @@ export async function collectSessionRecords(filePath, { includeContent = true }
62
80
  // follows the assistant call — attribute them to that call's record.
63
81
  if (lastRecord && Array.isArray(msg.content)) {
64
82
  for (const b of msg.content) {
65
- if (b && b.type === 'tool_result' && b.is_error) lastRecord.toolErrors += 1;
83
+ if (isRealToolError(b)) lastRecord.toolErrors += 1;
66
84
  }
67
85
  }
68
86
  continue;