claude-token-saver 3.22.0 → 3.22.1
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 +1 -1
- package/src/route-scan.js +15 -5
- package/src/subagent-records.js +30 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-token-saver",
|
|
3
|
-
"version": "3.22.
|
|
3
|
+
"version": "3.22.1",
|
|
4
4
|
"description": "Route the easy work your expensive Claude model keeps repeating down to haiku/sonnet — post-hoc session analysis, no realtime router, no extra LLM calls.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/route-scan.js
CHANGED
|
@@ -22,7 +22,9 @@ import { join } from 'node:path';
|
|
|
22
22
|
import { userDataDir } from './paths.js';
|
|
23
23
|
import { discoverSessionFiles } from './parser.js';
|
|
24
24
|
import { collectSessionRecords } from './session-records.js';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
collectSubagentRuns, indexRuns, exactRunsForEpisode, fallbackRunsForEpisode,
|
|
27
|
+
} from './subagent-records.js';
|
|
26
28
|
import { estimateCost, modelRank, isRecognizedModelId, TIER_TARGET_RANK, tierForRank } from './cost.js';
|
|
27
29
|
import { learnProfileMapping, resetModelAliasCache } from './model-alias.js';
|
|
28
30
|
import { agentPhrase, agentPhraseEn } from './agents.js';
|
|
@@ -534,10 +536,18 @@ export async function runRouteScan({ days = 14 } = {}) {
|
|
|
534
536
|
(r.scope === 'global' || r.project === projectDir));
|
|
535
537
|
for (const [sessionPath, index] of runIndexBySession) {
|
|
536
538
|
const used = new Set();
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
539
|
+
const eps = all.filter((x) => x.sessionPath === sessionPath);
|
|
540
|
+
// Two passes over the session, not one per episode: every exact tool_use
|
|
541
|
+
// join is settled first, so the timestamp fallback can only ever claim a
|
|
542
|
+
// run that no episode was able to prove was its own.
|
|
543
|
+
const runsByEp = new Map();
|
|
544
|
+
for (const item of eps) runsByEp.set(item, exactRunsForEpisode(index, item.ep, used));
|
|
545
|
+
for (const item of eps) {
|
|
546
|
+
runsByEp.get(item).push(...fallbackRunsForEpisode(index, item.ep, used));
|
|
547
|
+
}
|
|
548
|
+
for (const item of eps) {
|
|
549
|
+
const { ep, projectDir } = item;
|
|
550
|
+
const runs = runsByEp.get(item);
|
|
541
551
|
if (runs.length === 0) continue;
|
|
542
552
|
const cat = categorize(ep.text, ep.tools);
|
|
543
553
|
if (!cat) continue;
|
package/src/subagent-records.js
CHANGED
|
@@ -150,6 +150,10 @@ export async function collectSubagentRuns(sessionPath) {
|
|
|
150
150
|
* with the un-joinable ones kept aside for the timestamp fallback (a run
|
|
151
151
|
* whose .meta.json is missing or predates toolUseId still happened, and
|
|
152
152
|
* dropping it would silently under-count a rule's real error rate).
|
|
153
|
+
*
|
|
154
|
+
* `unjoined` is kept for callers that want only the no-id runs; the fallback
|
|
155
|
+
* itself works off `all` minus whatever the exact pass claimed, because a run
|
|
156
|
+
* CAN carry an id that no parent episode holds (see fallbackRunsForEpisode).
|
|
153
157
|
*/
|
|
154
158
|
export function indexRuns(runs) {
|
|
155
159
|
const byToolUse = new Map();
|
|
@@ -168,13 +172,38 @@ export function indexRuns(runs) {
|
|
|
168
172
|
* (an episode's span can overlap a neighbouring episode's runs).
|
|
169
173
|
*/
|
|
170
174
|
export function runsForEpisode(index, ep, used) {
|
|
175
|
+
return [...exactRunsForEpisode(index, ep, used), ...fallbackRunsForEpisode(index, ep, used)];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Runs this episode's own Task calls spawned. No guessing. */
|
|
179
|
+
export function exactRunsForEpisode(index, ep, used) {
|
|
171
180
|
const out = [];
|
|
172
181
|
for (const id of ep.delegationToolUseIds || []) {
|
|
173
182
|
const r = index.byToolUse.get(id);
|
|
174
183
|
if (r && !used.has(r.path)) { used.add(r.path); out.push(r); }
|
|
175
184
|
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Runs that no episode's tool_use ids claimed, matched by start time falling
|
|
190
|
+
* inside this episode's span.
|
|
191
|
+
*
|
|
192
|
+
* The pool is every unclaimed run, not just the ones without a toolUseId. A
|
|
193
|
+
* nested delegation — a subagent spawning its own subagent — records the
|
|
194
|
+
* SIBLING's tool_use id, which appears in no parent transcript and therefore
|
|
195
|
+
* matches nothing. Gating the fallback on "has no toolUseId" made having one
|
|
196
|
+
* disqualify the run from the only path that could still attribute it, so
|
|
197
|
+
* every spawnDepth >= 2 run was dropped forever (measured: 3 of 3 over 14
|
|
198
|
+
* days, ~$3.42 of savings and three runs of rule-health evidence).
|
|
199
|
+
*
|
|
200
|
+
* Run session-wide AFTER every episode's exact join, so a timestamp guess
|
|
201
|
+
* cannot take a run that another episode can prove is its own.
|
|
202
|
+
*/
|
|
203
|
+
export function fallbackRunsForEpisode(index, ep, used) {
|
|
204
|
+
const out = [];
|
|
176
205
|
if (ep.startedAt === null || ep.endedAt === null) return out;
|
|
177
|
-
for (const r of index.
|
|
206
|
+
for (const r of index.all) {
|
|
178
207
|
if (used.has(r.path) || r.startedAt === null) continue;
|
|
179
208
|
if (r.startedAt >= ep.startedAt && r.startedAt <= ep.endedAt) {
|
|
180
209
|
used.add(r.path);
|