@dzhechkov/harness-core 0.3.146 → 0.3.148
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 +66 -18
- package/README.md +2 -0
- package/dist/cost-ledger.d.ts +318 -0
- package/dist/cost-ledger.d.ts.map +1 -0
- package/dist/cost-ledger.js +871 -0
- package/dist/cost-ledger.js.map +1 -0
- package/dist/cost-scoring.d.ts +9 -0
- package/dist/cost-scoring.d.ts.map +1 -1
- package/dist/cost-scoring.js +18 -0
- package/dist/cost-scoring.js.map +1 -1
- package/dist/feature-adr-checkpoints.d.ts +113 -0
- package/dist/feature-adr-checkpoints.d.ts.map +1 -0
- package/dist/feature-adr-checkpoints.js +184 -0
- package/dist/feature-adr-checkpoints.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/usage.d.ts +32 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +59 -10
- package/dist/usage.js.map +1 -1
- package/package.json +4 -4
- package/sbom.json +137 -17
- package/src/cost-ledger.ts +1105 -0
- package/src/cost-scoring.ts +17 -0
- package/src/feature-adr-checkpoints.ts +221 -0
- package/src/index.ts +55 -0
- package/src/usage.ts +74 -12
package/src/cost-scoring.ts
CHANGED
|
@@ -78,6 +78,23 @@ const FALLBACK_PRICING = priced(3 / 1e6, 15 / 1e6);
|
|
|
78
78
|
* Resolve a model id to its pricing by longest-prefix match.
|
|
79
79
|
* `bedrock/anthropic.claude-3-opus...` → strips a leading `provider/` segment.
|
|
80
80
|
*/
|
|
81
|
+
/**
|
|
82
|
+
* Whether {@link pricingFor} will find a REAL entry for this model id, or silently fall back to
|
|
83
|
+
* {@link FALLBACK_PRICING} (sonnet-class).
|
|
84
|
+
*
|
|
85
|
+
* Exists so a cost surface can REPORT the fallback instead of hiding it: `claude-fable-*` has no
|
|
86
|
+
* entry in {@link MODEL_PRICES} and is the default model of every recorded feature-adr run, so its
|
|
87
|
+
* dollar figures are priced at a fallback rate (feature `cost-ledger`, ADR-003).
|
|
88
|
+
*/
|
|
89
|
+
export function hasKnownPricing(modelId: string): boolean {
|
|
90
|
+
if (typeof modelId !== 'string' || modelId.length === 0) return false;
|
|
91
|
+
const id = modelId.toLowerCase().replace(/^[a-z0-9-]+\//, '');
|
|
92
|
+
for (const key of Object.keys(MODEL_PRICES)) {
|
|
93
|
+
if (id.startsWith(key)) return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
81
98
|
export function pricingFor(modelId: string): ModelPricing {
|
|
82
99
|
// Strip a leading `provider/` segment (e.g. `bedrock/`, `us-east-1/`).
|
|
83
100
|
const id = modelId.toLowerCase().replace(/^[a-z0-9-]+\//, '');
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* feature-adr durable checkpoints — the PURE half (backlog 49e4a95b).
|
|
3
|
+
*
|
|
4
|
+
* Problem: .claude/workflows/feature-adr.js restarts an L/XL run from scratch when its session dies
|
|
5
|
+
* (the exact failure that forced usage-adaptive routing — one run cost 623k subagent tokens), and the
|
|
6
|
+
* STANDARD L/XL two-phase flow (stop-after-plan → re-invoke) re-runs router+design+plan wholesale.
|
|
7
|
+
* The Workflow harness's own resumeFromRunId is same-session only, so it cannot cover either case.
|
|
8
|
+
*
|
|
9
|
+
* Design: the heavyweight state is ALREADY durable — the 00–09 artifacts in features/<slug>/. The
|
|
10
|
+
* checkpoint layer is deliberately THIN: after each expensive stage the workflow appends one JSONL
|
|
11
|
+
* line { stage, inputHash, result } to features/<slug>/.fa-state/checkpoints.jsonl (via a cheap
|
|
12
|
+
* effort-low agent — the workflow sandbox has no fs). On the next run with the same slug, a stage is
|
|
13
|
+
* SKIPPED only when its recorded inputHash matches the freshly computed one AND its expected artifact
|
|
14
|
+
* is still on disk. Granularity is per-STAGE, not per-agent-call: a death mid-Step-7 re-runs Step 7,
|
|
15
|
+
* never Steps 0–6.
|
|
16
|
+
*
|
|
17
|
+
* Provenance: concept (checkpoint keyed by input hash + call cache) from ADR-157
|
|
18
|
+
* darwin-checkpoints-durable-execution (status PROPOSED) in agent-harness-generator. Its "~39% resume
|
|
19
|
+
* saving" figure is from a SYNTHETIC deterministic simulation — deliberately NOT quoted as expected
|
|
20
|
+
* field saving anywhere in this feature.
|
|
21
|
+
*
|
|
22
|
+
* Everything here is pure and deterministic (no Date/random — the workflow sandbox forbids them);
|
|
23
|
+
* the workflow script mirrors these functions inline (it is self-contained and cannot import), and
|
|
24
|
+
* the wiring test asserts the mirror stays present.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Stages the workflow checkpoints, in pipeline order. Cheap side-channel agents (usage probes,
|
|
28
|
+
* fa-record, auto-cost selects) are never checkpointed; the opt-in Delivery gate re-runs by design
|
|
29
|
+
* (advisory verdicts should reflect the CURRENT tree). */
|
|
30
|
+
export const CHECKPOINT_STAGES = ['router', 'design', 'plan', 'code', 'qe', 'fleet'] as const;
|
|
31
|
+
export type CheckpointStage = (typeof CHECKPOINT_STAGES)[number];
|
|
32
|
+
|
|
33
|
+
/** The artifact(s) (relative to features/<slug>/) whose PRESENCE a resume additionally requires in
|
|
34
|
+
* 'auto' mode — EVERY listed path must exist. null = result-only stage (hash match suffices).
|
|
35
|
+
* Tier-dependent stages (design) take extra artifacts at the call site via `extraArtifacts` —
|
|
36
|
+
* an M+ design must probe its ADR/ideation/architecture files too, not just requirements
|
|
37
|
+
* (Codex QE #2: a one-file probe accepted a materially incomplete design). */
|
|
38
|
+
export const STAGE_ARTIFACTS: Record<CheckpointStage, string | null> = {
|
|
39
|
+
router: null,
|
|
40
|
+
design: '01_requirements.md',
|
|
41
|
+
plan: '06_implementation_plan.md',
|
|
42
|
+
code: '07_code_changes/change_manifest.md',
|
|
43
|
+
qe: '08_qe_report.md',
|
|
44
|
+
fleet: '09_fleet_qe_assessment.md',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** A checkpoint line as persisted (one JSON object per line). */
|
|
48
|
+
export interface CheckpointEntry {
|
|
49
|
+
stage: string;
|
|
50
|
+
inputHash: string;
|
|
51
|
+
result: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Oversize guard: a result JSON above this is NOT checkpointed (the stage simply re-runs on resume).
|
|
55
|
+
* Keeps the read-back prompt bounded; artifacts on disk carry the heavy state anyway. */
|
|
56
|
+
export const CHECKPOINT_MAX_RESULT_CHARS = 12_000;
|
|
57
|
+
|
|
58
|
+
/** Checkpoint format/logic version — SALTED into every input hash. Bump it whenever the workflow's
|
|
59
|
+
* stage semantics, prompts, or composite result shapes change: every pre-existing checkpoint then
|
|
60
|
+
* hashes stale and re-runs, instead of an old-format entry resuming into new logic (Codex QE #5). */
|
|
61
|
+
export const CKPT_SCHEMA_VERSION = 'fa-ckpt-2';
|
|
62
|
+
|
|
63
|
+
/** FNV-1a 32-bit over UTF-16 code units, hex-encoded (one pass; building block for the 64-bit form). */
|
|
64
|
+
export function fnv1a(str: string): string {
|
|
65
|
+
let h = 0x811c9dc5;
|
|
66
|
+
for (let i = 0; i < str.length; i++) {
|
|
67
|
+
h ^= str.charCodeAt(i);
|
|
68
|
+
h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
|
|
69
|
+
}
|
|
70
|
+
return h.toString(16).padStart(8, '0');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 64 bits from two independent FNV-1a passes (plain + salted). A single 32-bit hash admits
|
|
74
|
+
* findable collisions (Codex QE #9 produced a real pair at `11a08b58`); two passes make the
|
|
75
|
+
* single-pair collision odds ~2^-64 — adequate for one slug's checkpoint file. */
|
|
76
|
+
export function fnv1a64(str: string): string {
|
|
77
|
+
return fnv1a(str) + fnv1a('fa-ckpt-salt' + str);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The stage's input fingerprint: a JSON-tuple (delimiter-ambiguity class — never a separator join)
|
|
81
|
+
* of the schema version + stage name + every input that would change the stage's output, hashed.
|
|
82
|
+
* Upstream stage RESULTS are included as their serialized form, so a stale upstream auto-invalidates
|
|
83
|
+
* downstream. HONEST SCOPE: the hash proves the run INPUTS are unchanged — it does NOT fingerprint
|
|
84
|
+
* the working tree (a crash-resume legitimately sees the dead run's uncommitted writes, so a tree
|
|
85
|
+
* hash would invalidate every real resume). Tree-level staleness is out of the checkpoint contract:
|
|
86
|
+
* use resume:'never' (or delete .fa-state/) after manual edits, and re-QE independently — the ADR
|
|
87
|
+
* names this as the accepted limitation (Codex QE #1). */
|
|
88
|
+
export function checkpointInputHash(stage: string, parts: readonly unknown[]): string {
|
|
89
|
+
return fnv1a64(JSON.stringify([CKPT_SCHEMA_VERSION, stage, ...parts.map((p) => (p === undefined ? null : p))]));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type ResumeMode = 'auto' | 'never' | 'force';
|
|
93
|
+
|
|
94
|
+
/** Normalize args.resume: anything but the two explicit strings means the default 'auto'. */
|
|
95
|
+
export function resumeMode(raw: unknown): ResumeMode {
|
|
96
|
+
return raw === 'never' ? 'never' : raw === 'force' ? 'force' : 'auto';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ResumeDecision {
|
|
100
|
+
resume: boolean;
|
|
101
|
+
reason:
|
|
102
|
+
| 'resumed'
|
|
103
|
+
| 'resumed-force'
|
|
104
|
+
| 'mode-never'
|
|
105
|
+
| 'no-checkpoint'
|
|
106
|
+
| 'stale-input'
|
|
107
|
+
| 'artifact-missing';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The pure resume decision. 'auto' resumes only on (hash match AND every required artifact
|
|
111
|
+
* present); 'force' trusts the hash alone; 'never' always runs live. A STALE-INPUT hash NEVER
|
|
112
|
+
* resumes in any mode — force skips only the artifact probe, never the input check (a checkpoint
|
|
113
|
+
* for different inputs is a different feature). A malformed/null recorded result is treated as
|
|
114
|
+
* no-checkpoint (Codex QE #8 — a null result must not resume as a real one). */
|
|
115
|
+
export function decideCheckpointResume(opts: {
|
|
116
|
+
mode: ResumeMode;
|
|
117
|
+
entry: CheckpointEntry | undefined;
|
|
118
|
+
inputHash: string;
|
|
119
|
+
artifactRel: string | readonly string[] | null;
|
|
120
|
+
listing: ReadonlySet<string>;
|
|
121
|
+
}): ResumeDecision {
|
|
122
|
+
if (opts.mode === 'never') return { resume: false, reason: 'mode-never' };
|
|
123
|
+
if (!opts.entry || opts.entry.result === null || opts.entry.result === undefined) {
|
|
124
|
+
return { resume: false, reason: 'no-checkpoint' };
|
|
125
|
+
}
|
|
126
|
+
if (opts.entry.inputHash !== opts.inputHash) return { resume: false, reason: 'stale-input' };
|
|
127
|
+
if (opts.mode === 'force') return { resume: true, reason: 'resumed-force' };
|
|
128
|
+
const required = opts.artifactRel === null ? [] : (typeof opts.artifactRel === 'string' ? [opts.artifactRel] : opts.artifactRel);
|
|
129
|
+
for (const rel of required) {
|
|
130
|
+
if (!opts.listing.has(rel)) return { resume: false, reason: 'artifact-missing' };
|
|
131
|
+
}
|
|
132
|
+
return { resume: true, reason: 'resumed' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Serialize one checkpoint line, or null when the result is null/oversize/unserializable —
|
|
136
|
+
* the caller logs the skip loudly; a missing checkpoint only costs a re-run, never corrupts.
|
|
137
|
+
* A null result is never persisted (Codex QE #8: it would later parse as a resumable entry). */
|
|
138
|
+
export function serializeCheckpoint(stage: string, inputHash: string, result: unknown): string | null {
|
|
139
|
+
if (result === null || result === undefined) return null;
|
|
140
|
+
let line: string;
|
|
141
|
+
try {
|
|
142
|
+
line = JSON.stringify({ stage, inputHash, result });
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
if (typeof line !== 'string' || line.length > CHECKPOINT_MAX_RESULT_CHARS) return null;
|
|
147
|
+
return line;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface ParsedCheckpointRead {
|
|
151
|
+
entries: Record<string, CheckpointEntry>;
|
|
152
|
+
listing: Set<string>;
|
|
153
|
+
malformedLines: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Sentinel separating the checkpoint file body from the artifact listing in the single read-back
|
|
157
|
+
* command's stdout. */
|
|
158
|
+
export const CHECKPOINT_LS_SENTINEL = '---FA-CKPT-LS---';
|
|
159
|
+
|
|
160
|
+
/** Parse the read-back agent's stdout: JSONL entries (LAST occurrence of a stage wins — a re-run
|
|
161
|
+
* overwrites by append), then the sentinel ON ITS OWN LINE, then one artifact path per line
|
|
162
|
+
* (relative to the feature dir). The sentinel match is LINE-ANCHORED: JSON.stringify never emits
|
|
163
|
+
* literal newlines, so a sentinel string INSIDE a recorded result shares its line with JSON syntax
|
|
164
|
+
* and can never split the stream (Codex QE #10). Malformed JSONL lines are COUNTED, never silently
|
|
165
|
+
* ignored (corruption is named); entries with a null result are malformed, not resumable. */
|
|
166
|
+
export function parseCheckpointRead(text: string): ParsedCheckpointRead {
|
|
167
|
+
const out: ParsedCheckpointRead = { entries: {}, listing: new Set(), malformedLines: 0 };
|
|
168
|
+
const raw = String(text ?? '');
|
|
169
|
+
const lines = raw.split('\n');
|
|
170
|
+
const sentinelAt = lines.findIndex((l) => l.trim() === CHECKPOINT_LS_SENTINEL);
|
|
171
|
+
const body = sentinelAt === -1 ? lines : lines.slice(0, sentinelAt);
|
|
172
|
+
const ls = sentinelAt === -1 ? [] : lines.slice(sentinelAt + 1);
|
|
173
|
+
for (const line of body) {
|
|
174
|
+
const t = line.trim();
|
|
175
|
+
if (t === '') continue;
|
|
176
|
+
try {
|
|
177
|
+
const e = JSON.parse(t) as CheckpointEntry;
|
|
178
|
+
if (e && typeof e === 'object' && typeof e.stage === 'string' && typeof e.inputHash === 'string' && 'result' in e && e.result !== null && e.result !== undefined) {
|
|
179
|
+
out.entries[e.stage] = e;
|
|
180
|
+
} else {
|
|
181
|
+
// last-wins holds for BAD records too: a stage-identifiable null/invalid record ERASES the
|
|
182
|
+
// older entry for that stage instead of silently reactivating it (Codex QE r2 #6).
|
|
183
|
+
if (e && typeof e === 'object' && typeof (e as CheckpointEntry).stage === 'string') delete out.entries[(e as CheckpointEntry).stage];
|
|
184
|
+
out.malformedLines++;
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
out.malformedLines++;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const line of ls) {
|
|
191
|
+
const t = line.trim();
|
|
192
|
+
if (t !== '') out.listing.add(t);
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Single-quote shell escaping (the workflow's shq twin). */
|
|
198
|
+
export function shellQuote(s: string): string {
|
|
199
|
+
return "'" + String(s).replace(/'/g, "'\\''") + "'";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The one Bash command the read-back agent runs: checkpoint file body (absent file = empty),
|
|
203
|
+
* the sentinel, then the artifact listing as feature-dir-relative paths (find prints them with a
|
|
204
|
+
* leading ./ that sed strips). Never fails: every leg is || true. */
|
|
205
|
+
export function checkpointReadCmd(fdirAbs: string): string {
|
|
206
|
+
const q = shellQuote(fdirAbs);
|
|
207
|
+
return (
|
|
208
|
+
'cat ' + q + '/.fa-state/checkpoints.jsonl 2>/dev/null || true; ' +
|
|
209
|
+
"echo '" + CHECKPOINT_LS_SENTINEL + "'; " +
|
|
210
|
+
'cd ' + q + ' 2>/dev/null && find . -maxdepth 2 -type f 2>/dev/null | sed "s|^\\./||" || true'
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The one Bash command the write agent runs: mkdir the state dir, then append ONE line. The line
|
|
215
|
+
* is single-quote-escaped as a whole — JSON.stringify output never contains literal newlines, so
|
|
216
|
+
* printf '%s\n' emits exactly one record. */
|
|
217
|
+
export function checkpointAppendCmd(fdirAbs: string, line: string): string {
|
|
218
|
+
const dir = shellQuote(fdirAbs + '/.fa-state');
|
|
219
|
+
const file = shellQuote(fdirAbs + '/.fa-state/checkpoints.jsonl');
|
|
220
|
+
return 'mkdir -p ' + dir + " && printf '%s\\n' " + shellQuote(line) + ' >> ' + file;
|
|
221
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -138,6 +138,23 @@ export { hookDecision, isFenced, isNewLine, ESCAPE_TEACHING } from './claim-chec
|
|
|
138
138
|
export type { HookDecision, HookDecisionOpts } from './claim-check-hook-policy.js';
|
|
139
139
|
export { step8ClaimGate } from './feature-adr-claim-gate.js';
|
|
140
140
|
export type { Step8ClaimCounts, Step8ClaimGate } from './feature-adr-claim-gate.js';
|
|
141
|
+
export {
|
|
142
|
+
CHECKPOINT_STAGES,
|
|
143
|
+
STAGE_ARTIFACTS,
|
|
144
|
+
CHECKPOINT_MAX_RESULT_CHARS,
|
|
145
|
+
CHECKPOINT_LS_SENTINEL,
|
|
146
|
+
CKPT_SCHEMA_VERSION,
|
|
147
|
+
fnv1a,
|
|
148
|
+
fnv1a64,
|
|
149
|
+
checkpointInputHash,
|
|
150
|
+
resumeMode,
|
|
151
|
+
decideCheckpointResume,
|
|
152
|
+
serializeCheckpoint,
|
|
153
|
+
parseCheckpointRead,
|
|
154
|
+
checkpointReadCmd,
|
|
155
|
+
checkpointAppendCmd,
|
|
156
|
+
} from './feature-adr-checkpoints.js';
|
|
157
|
+
export type { CheckpointStage, CheckpointEntry, ResumeMode, ResumeDecision, ParsedCheckpointRead } from './feature-adr-checkpoints.js';
|
|
141
158
|
export {
|
|
142
159
|
detectQueryLang,
|
|
143
160
|
relevanceFloorFor,
|
|
@@ -263,6 +280,7 @@ export type { RiskScore, RiskThresholds } from './risk-scoring.js';
|
|
|
263
280
|
export {
|
|
264
281
|
MODEL_PRICES,
|
|
265
282
|
pricingFor,
|
|
283
|
+
hasKnownPricing,
|
|
266
284
|
normalizeUsage,
|
|
267
285
|
usageCost,
|
|
268
286
|
invocationCost,
|
|
@@ -342,6 +360,43 @@ export {
|
|
|
342
360
|
readUsageLimits,
|
|
343
361
|
weeklyWindowFor,
|
|
344
362
|
} from './usage.js';
|
|
363
|
+
export { claudeProjectsRoot, rawTokenMixOf, weightedTokensOf } from './usage.js';
|
|
364
|
+
export type { RawTokenMix } from './usage.js';
|
|
365
|
+
// Per-stage cost ledger + reconciliation invariant (feature cost-ledger, ADR-001/002/003).
|
|
366
|
+
export {
|
|
367
|
+
COST_LEDGER_SCOPE,
|
|
368
|
+
COST_LEDGER_DEFECT_KINDS,
|
|
369
|
+
COST_LEDGER_VERDICTS,
|
|
370
|
+
DEFAULT_COST_LEDGER_EPSILON,
|
|
371
|
+
extractCostSamples,
|
|
372
|
+
parseWorkflowRunRecord,
|
|
373
|
+
buildCostLedger,
|
|
374
|
+
verifyCostLedgerReport,
|
|
375
|
+
stageCostAggregates,
|
|
376
|
+
renderCostLedger,
|
|
377
|
+
costLedgerJsonl,
|
|
378
|
+
listCostLedgerRuns,
|
|
379
|
+
deriveCostLedger,
|
|
380
|
+
deriveStageCostAggregates,
|
|
381
|
+
writeCostLedgerJsonl,
|
|
382
|
+
} from './cost-ledger.js';
|
|
383
|
+
export type {
|
|
384
|
+
CostLedgerDefect,
|
|
385
|
+
CostLedgerDefectKind,
|
|
386
|
+
CostLedgerIoOptions,
|
|
387
|
+
CostLedgerReconciliation,
|
|
388
|
+
CostLedgerReport,
|
|
389
|
+
CostLedgerRow,
|
|
390
|
+
CostLedgerRunRef,
|
|
391
|
+
CostLedgerSample,
|
|
392
|
+
CostLedgerVerdict,
|
|
393
|
+
BuildCostLedgerInput,
|
|
394
|
+
DeriveCostLedgerOptions,
|
|
395
|
+
StageCostAggregate,
|
|
396
|
+
StageSampleSet,
|
|
397
|
+
WorkflowRunRecord,
|
|
398
|
+
WorkflowStageEntry,
|
|
399
|
+
} from './cost-ledger.js';
|
|
345
400
|
export type {
|
|
346
401
|
ClaudeUsageModel,
|
|
347
402
|
UsageCalibrationChange,
|
package/src/usage.ts
CHANGED
|
@@ -45,6 +45,77 @@ const MTIME_SLACK_MS = HOUR_MS;
|
|
|
45
45
|
*/
|
|
46
46
|
export const TOKEN_WEIGHTS = { input: 1, cacheWrite: 1.25, cacheWrite1h: 2, cacheRead: 0.1, output: 5 } as const;
|
|
47
47
|
|
|
48
|
+
/** The four raw token buckets of one `message.usage` record, after clamping. */
|
|
49
|
+
export interface RawTokenMix {
|
|
50
|
+
readonly input: number;
|
|
51
|
+
/** Cache-CREATION tokens, already priced at their TTL rate inside {@link weightedTokensOf}. */
|
|
52
|
+
readonly cacheWrite: number;
|
|
53
|
+
readonly cacheRead: number;
|
|
54
|
+
readonly output: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function positiveFinite(v: unknown): number {
|
|
58
|
+
return typeof v === 'number' && isFinite(v) && v > 0 ? v : 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The raw token buckets of an Anthropic `message.usage` object, clamped to finite non-negatives.
|
|
63
|
+
* `cacheWrite` prefers the TTL breakdown (`cache_creation.ephemeral_*`) and falls back to the flat
|
|
64
|
+
* `cache_creation_input_tokens` — reading only the flat field scored a nested-only record as ZERO.
|
|
65
|
+
*/
|
|
66
|
+
export function rawTokenMixOf(usage: unknown): RawTokenMix {
|
|
67
|
+
if (typeof usage !== 'object' || usage === null) {
|
|
68
|
+
return { input: 0, cacheWrite: 0, cacheRead: 0, output: 0 };
|
|
69
|
+
}
|
|
70
|
+
const u = usage as Record<string, unknown>;
|
|
71
|
+
const cc = (typeof u['cache_creation'] === 'object' && u['cache_creation'] !== null
|
|
72
|
+
? (u['cache_creation'] as Record<string, unknown>)
|
|
73
|
+
: {}) as Record<string, unknown>;
|
|
74
|
+
const c5 = positiveFinite(cc['ephemeral_5m_input_tokens']);
|
|
75
|
+
const c1h = positiveFinite(cc['ephemeral_1h_input_tokens']);
|
|
76
|
+
const cacheWrite = c5 + c1h > 0 ? c5 + c1h : positiveFinite(u['cache_creation_input_tokens']);
|
|
77
|
+
return {
|
|
78
|
+
input: positiveFinite(u['input_tokens']),
|
|
79
|
+
cacheWrite,
|
|
80
|
+
cacheRead: positiveFinite(u['cache_read_input_tokens']),
|
|
81
|
+
output: positiveFinite(u['output_tokens']),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* THE estimator — cost-weighted "input-equivalent" tokens for one `message.usage` object.
|
|
87
|
+
*
|
|
88
|
+
* A flat token sum is 89-99.7% `cache_read` on this machine, which tracks CONVERSATION LENGTH
|
|
89
|
+
* rather than work done. {@link TOKEN_WEIGHTS} are the published per-token price ratios relative to
|
|
90
|
+
* base input, so the result tracks consumption instead of context size.
|
|
91
|
+
*
|
|
92
|
+
* Extracted verbatim from `computeUsage`'s per-sample arithmetic so that `dz usage` and the
|
|
93
|
+
* per-stage cost ledger measure the same quantity (feature `cost-ledger`, ADR-002 — the invariant
|
|
94
|
+
* only means something if both sides use ONE estimator). The return value is UNROUNDED; callers
|
|
95
|
+
* that need exact integer identities round once at their own extraction point.
|
|
96
|
+
*/
|
|
97
|
+
export function weightedTokensOf(usage: unknown): number {
|
|
98
|
+
if (typeof usage !== 'object' || usage === null) return 0;
|
|
99
|
+
const mix = rawTokenMixOf(usage);
|
|
100
|
+
const u = usage as Record<string, unknown>;
|
|
101
|
+
const cc =
|
|
102
|
+
typeof u['cache_creation'] === 'object' && u['cache_creation'] !== null
|
|
103
|
+
? (u['cache_creation'] as Record<string, unknown>)
|
|
104
|
+
: {};
|
|
105
|
+
const c5 = positiveFinite(cc['ephemeral_5m_input_tokens']);
|
|
106
|
+
const c1h = positiveFinite(cc['ephemeral_1h_input_tokens']);
|
|
107
|
+
const cacheWriteCost =
|
|
108
|
+
c5 + c1h > 0
|
|
109
|
+
? c5 * TOKEN_WEIGHTS.cacheWrite + c1h * TOKEN_WEIGHTS.cacheWrite1h
|
|
110
|
+
: mix.cacheWrite * TOKEN_WEIGHTS.cacheWrite;
|
|
111
|
+
return (
|
|
112
|
+
mix.input * TOKEN_WEIGHTS.input +
|
|
113
|
+
cacheWriteCost +
|
|
114
|
+
mix.cacheRead * TOKEN_WEIGHTS.cacheRead +
|
|
115
|
+
mix.output * TOKEN_WEIGHTS.output
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
48
119
|
export const CLAUDE_USAGE_MODELS = ['fable', 'opus', 'sonnet', 'haiku'] as const;
|
|
49
120
|
export type ClaudeUsageModel = (typeof CLAUDE_USAGE_MODELS)[number];
|
|
50
121
|
|
|
@@ -230,7 +301,7 @@ export function normalizeClaudeUsageModelKey(raw: unknown): ClaudeUsageModel | n
|
|
|
230
301
|
* The `~/.claude/projects` root (the account-wide transcript store). Overridable via
|
|
231
302
|
* `DZ_CLAUDE_PROJECTS_ROOT` — used by tests to point at a temp tree. Never throws.
|
|
232
303
|
*/
|
|
233
|
-
function claudeProjectsRoot(): string {
|
|
304
|
+
export function claudeProjectsRoot(): string {
|
|
234
305
|
const override = process.env['DZ_CLAUDE_PROJECTS_ROOT'];
|
|
235
306
|
if (typeof override === 'string' && override.length > 0) return override;
|
|
236
307
|
return join(homedir(), '.claude', 'projects');
|
|
@@ -414,17 +485,8 @@ function extractSamples(path: string, scanCutoff: number, into: Sample[], seen:
|
|
|
414
485
|
// tokens": a quantity that tracks consumption instead of context size.
|
|
415
486
|
// Prefer the TTL breakdown when present (5m 1.25x / 1h 2x); fall back to the flat field at the
|
|
416
487
|
// 5m rate. Reading only the flat field scored a nested-only record as ZERO.
|
|
417
|
-
|
|
418
|
-
const
|
|
419
|
-
const cacheWriteCost =
|
|
420
|
-
c5 + c1h > 0
|
|
421
|
-
? c5 * TOKEN_WEIGHTS.cacheWrite + c1h * TOKEN_WEIGHTS.cacheWrite1h
|
|
422
|
-
: n(usage.cache_creation_input_tokens) * TOKEN_WEIGHTS.cacheWrite;
|
|
423
|
-
const tokens =
|
|
424
|
-
n(usage.input_tokens) * TOKEN_WEIGHTS.input +
|
|
425
|
-
cacheWriteCost +
|
|
426
|
-
n(usage.cache_read_input_tokens) * TOKEN_WEIGHTS.cacheRead +
|
|
427
|
-
n(usage.output_tokens) * TOKEN_WEIGHTS.output;
|
|
488
|
+
// ONE estimator, shared with the per-stage cost ledger (feature `cost-ledger`, ADR-002).
|
|
489
|
+
const tokens = weightedTokensOf(usage);
|
|
428
490
|
if (tokens <= 0) continue;
|
|
429
491
|
// Dedup: streamed assistant messages repeat their usage object across chunks.
|
|
430
492
|
const id = typeof rec.message?.id === 'string' ? rec.message.id : '';
|