@dzhechkov/harness-core 0.3.131 → 0.3.133
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/.dz-manifest.json +19 -19
- package/README.md +2 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +11 -3
- package/dist/operations.js.map +1 -1
- package/dist/skills-verify.d.ts +13 -0
- package/dist/skills-verify.d.ts.map +1 -1
- package/dist/skills-verify.js +161 -29
- package/dist/skills-verify.js.map +1 -1
- package/dist/usage.d.ts +12 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +70 -18
- package/dist/usage.js.map +1 -1
- package/package.json +5 -5
- package/sbom.json +18 -18
- package/src/operations.ts +12 -3
- package/src/skills-verify.ts +188 -32
- package/src/usage.ts +69 -16
package/src/usage.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* @packageDocumentation
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
31
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
32
32
|
import { homedir } from 'node:os';
|
|
33
33
|
import { join } from 'node:path';
|
|
34
34
|
|
|
@@ -38,6 +38,13 @@ const DEFAULT_WEEKLY_RESET_ANCHOR = 'Wed 08:59';
|
|
|
38
38
|
// mtime prefilter slack (+1h) — guards against clock skew between the writer and this reader.
|
|
39
39
|
const MTIME_SLACK_MS = HOUR_MS;
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Per-token price ratios relative to base input, used to turn a raw token mix into INPUT-EQUIVALENT
|
|
43
|
+
* tokens. Without this the metric is ~90-99% cache-read and measures context size, not work.
|
|
44
|
+
* (Anthropic list pricing: 5m cache write 1.25x input, cache read 0.1x input, output 5x input.)
|
|
45
|
+
*/
|
|
46
|
+
export const TOKEN_WEIGHTS = { input: 1, cacheWrite: 1.25, cacheWrite1h: 2, cacheRead: 0.1, output: 5 } as const;
|
|
47
|
+
|
|
41
48
|
export const CLAUDE_USAGE_MODELS = ['fable', 'opus', 'sonnet', 'haiku'] as const;
|
|
42
49
|
export type ClaudeUsageModel = (typeof CLAUDE_USAGE_MODELS)[number];
|
|
43
50
|
|
|
@@ -287,6 +294,20 @@ interface Sample {
|
|
|
287
294
|
* List every `*.jsonl` under `~/.claude/projects/<dir>/`, best-effort. Never throws — an
|
|
288
295
|
* unreadable dir/file is skipped. Returns absolute paths + their `mtimeMs` (the prefilter lever).
|
|
289
296
|
*/
|
|
297
|
+
/** A hard bound: a pathological tree must degrade to a partial estimate, never to a hang or an OOM. */
|
|
298
|
+
const MAX_TRANSCRIPT_FILES = 20_000;
|
|
299
|
+
|
|
300
|
+
/** Only a REGULAR file is readable transcript data. lstat (not stat) so a symlink is never followed —
|
|
301
|
+
* a FIFO blocks readFileSync forever and a symlink to a huge file explodes memory. */
|
|
302
|
+
function regularFileMtime(p: string): number | null {
|
|
303
|
+
try {
|
|
304
|
+
const st = lstatSync(p);
|
|
305
|
+
return st.isFile() ? st.mtimeMs : null;
|
|
306
|
+
} catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
290
311
|
function listTranscriptFiles(root: string): Array<{ path: string; mtimeMs: number }> {
|
|
291
312
|
const out: Array<{ path: string; mtimeMs: number }> = [];
|
|
292
313
|
let dirs: string[];
|
|
@@ -307,13 +328,29 @@ function listTranscriptFiles(root: string): Array<{ path: string; mtimeMs: numbe
|
|
|
307
328
|
continue;
|
|
308
329
|
}
|
|
309
330
|
for (const f of files) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
331
|
+
// A session's SUBAGENT transcripts live one level deeper — `<session>/subagents/*.jsonl` — and
|
|
332
|
+
// carry real, non-duplicated usage that was silently excluded (MEASURED: 27 such files on this
|
|
333
|
+
// machine). Their tokens are spent exactly like the main loop's.
|
|
334
|
+
if (!f.endsWith('.jsonl')) {
|
|
335
|
+
const nested = join(projDir, f, 'subagents');
|
|
336
|
+
try {
|
|
337
|
+
if (!statSync(nested).isDirectory()) continue;
|
|
338
|
+
for (const sf of readdirSync(nested)) {
|
|
339
|
+
if (!sf.endsWith('.jsonl')) continue;
|
|
340
|
+
if (out.length >= MAX_TRANSCRIPT_FILES) break;
|
|
341
|
+
const sp = join(nested, sf);
|
|
342
|
+
const m = regularFileMtime(sp);
|
|
343
|
+
if (m !== null) out.push({ path: sp, mtimeMs: m });
|
|
344
|
+
}
|
|
345
|
+
} catch {
|
|
346
|
+
/* not a session dir — skip */
|
|
347
|
+
}
|
|
348
|
+
continue;
|
|
316
349
|
}
|
|
350
|
+
if (out.length >= MAX_TRANSCRIPT_FILES) break;
|
|
351
|
+
const p = join(projDir, f);
|
|
352
|
+
const mt = regularFileMtime(p);
|
|
353
|
+
if (mt !== null) out.push({ path: p, mtimeMs: mt });
|
|
317
354
|
}
|
|
318
355
|
}
|
|
319
356
|
return out;
|
|
@@ -344,6 +381,9 @@ function extractSamples(path: string, scanCutoff: number, into: Sample[], seen:
|
|
|
344
381
|
usage?: {
|
|
345
382
|
input_tokens?: unknown;
|
|
346
383
|
cache_creation_input_tokens?: unknown;
|
|
384
|
+
/** TTL breakdown: 5m writes price at 1.25x, 1h writes at 2x. A nested-only record used to
|
|
385
|
+
* count as ZERO because only the flat field was read. */
|
|
386
|
+
cache_creation?: { ephemeral_5m_input_tokens?: unknown; ephemeral_1h_input_tokens?: unknown };
|
|
347
387
|
cache_read_input_tokens?: unknown;
|
|
348
388
|
output_tokens?: unknown;
|
|
349
389
|
};
|
|
@@ -362,20 +402,33 @@ function extractSamples(path: string, scanCutoff: number, into: Sample[], seen:
|
|
|
362
402
|
if (!isFinite(ts)) continue;
|
|
363
403
|
if (ts < scanCutoff) continue;
|
|
364
404
|
const n = (v: unknown): number => (typeof v === 'number' && isFinite(v) && v > 0 ? v : 0);
|
|
405
|
+
// COST-WEIGHTED, not a flat sum. A flat sum is 89-99.7% `cache_read` (MEASURED on this machine),
|
|
406
|
+
// which grows with CONVERSATION LENGTH rather than with work done — two sessions doing identical
|
|
407
|
+
// work differ by orders of magnitude, so no threshold over it can mean anything. These weights are
|
|
408
|
+
// the published per-token price ratios relative to base input, so the total is "input-equivalent
|
|
409
|
+
// tokens": a quantity that tracks consumption instead of context size.
|
|
410
|
+
// Prefer the TTL breakdown when present (5m 1.25x / 1h 2x); fall back to the flat field at the
|
|
411
|
+
// 5m rate. Reading only the flat field scored a nested-only record as ZERO.
|
|
412
|
+
const c5 = n(usage.cache_creation?.ephemeral_5m_input_tokens);
|
|
413
|
+
const c1h = n(usage.cache_creation?.ephemeral_1h_input_tokens);
|
|
414
|
+
const cacheWriteCost =
|
|
415
|
+
c5 + c1h > 0
|
|
416
|
+
? c5 * TOKEN_WEIGHTS.cacheWrite + c1h * TOKEN_WEIGHTS.cacheWrite1h
|
|
417
|
+
: n(usage.cache_creation_input_tokens) * TOKEN_WEIGHTS.cacheWrite;
|
|
365
418
|
const tokens =
|
|
366
|
-
n(usage.input_tokens) +
|
|
367
|
-
|
|
368
|
-
n(usage.cache_read_input_tokens) +
|
|
369
|
-
n(usage.output_tokens);
|
|
419
|
+
n(usage.input_tokens) * TOKEN_WEIGHTS.input +
|
|
420
|
+
cacheWriteCost +
|
|
421
|
+
n(usage.cache_read_input_tokens) * TOKEN_WEIGHTS.cacheRead +
|
|
422
|
+
n(usage.output_tokens) * TOKEN_WEIGHTS.output;
|
|
370
423
|
if (tokens <= 0) continue;
|
|
371
424
|
// Dedup: streamed assistant messages repeat their usage object across chunks.
|
|
372
425
|
const id = typeof rec.message?.id === 'string' ? rec.message.id : '';
|
|
373
426
|
const reqId = typeof rec.requestId === 'string' ? rec.requestId : '';
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
427
|
+
// With no ids, fall back to a CONTENT key (timestamp + weighted total): the same record copied
|
|
428
|
+
// into both a main and a subagent transcript would otherwise be counted twice.
|
|
429
|
+
const key = id !== '' || reqId !== '' ? id + ':' + reqId : `anon:${ts}:${tokens}`;
|
|
430
|
+
if (seen.has(key)) continue;
|
|
431
|
+
seen.add(key);
|
|
379
432
|
into.push({ ts, tokens, key, model: normalizeClaudeUsageModel(rec.message?.model ?? rec.model) });
|
|
380
433
|
}
|
|
381
434
|
}
|