@dzhechkov/harness-core 0.3.145 → 0.3.147
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 +82 -34
- package/README.md +3 -1
- package/dist/compounding.d.ts +27 -0
- package/dist/compounding.d.ts.map +1 -1
- package/dist/compounding.js +29 -0
- package/dist/compounding.js.map +1 -1
- 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/event-chain.d.ts +302 -0
- package/dist/event-chain.d.ts.map +1 -0
- package/dist/event-chain.js +663 -0
- package/dist/event-chain.js.map +1 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +26 -0
- package/dist/operations.js.map +1 -1
- package/dist/recall-usage.d.ts +52 -4
- package/dist/recall-usage.d.ts.map +1 -1
- package/dist/recall-usage.js +106 -21
- package/dist/recall-usage.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 +3 -3
- package/sbom.json +153 -33
- package/src/compounding.ts +61 -0
- package/src/cost-ledger.ts +1105 -0
- package/src/cost-scoring.ts +17 -0
- package/src/event-chain.ts +870 -0
- package/src/index.ts +86 -0
- package/src/operations.ts +25 -0
- package/src/recall-usage.ts +142 -24
- package/src/usage.ts +74 -12
package/src/index.ts
CHANGED
|
@@ -156,11 +156,13 @@ export {
|
|
|
156
156
|
RECALL_USAGE_LOG_MAX_BYTES,
|
|
157
157
|
RECALL_USAGE_COMPACT_TARGET_BYTES,
|
|
158
158
|
formatRecallUsageRecord,
|
|
159
|
+
buildRecallUsageRecord,
|
|
159
160
|
parseRecallUsageLog,
|
|
160
161
|
aggregateRecallUsage,
|
|
161
162
|
buildRecallUsageReport,
|
|
162
163
|
shouldCompactRecallUsageLogSize,
|
|
163
164
|
compactRecallUsageLog,
|
|
165
|
+
compactRecallUsageLogChecked,
|
|
164
166
|
} from './recall-usage.js';
|
|
165
167
|
export type {
|
|
166
168
|
RecallUsageReadRecord,
|
|
@@ -171,7 +173,53 @@ export type {
|
|
|
171
173
|
RecallPatternUsageRef,
|
|
172
174
|
RecallUsagePatternRow,
|
|
173
175
|
RecallUsageReport,
|
|
176
|
+
RecallUsageRecordInput,
|
|
177
|
+
CompactRecallUsageOptions,
|
|
178
|
+
CompactRecallUsageResult,
|
|
179
|
+
CompactRecallUsageStatus,
|
|
174
180
|
} from './recall-usage.js';
|
|
181
|
+
export {
|
|
182
|
+
EVENT_CHAIN_SCOPE,
|
|
183
|
+
EVENT_CHAIN_GENESIS_HASH,
|
|
184
|
+
EVENT_CHAIN_TAIL_BYTES,
|
|
185
|
+
EVENT_CHAIN_FIELD_OVERHEAD_BYTES,
|
|
186
|
+
EVENT_CHAIN_LEDGER_KIND,
|
|
187
|
+
EVENT_CHAIN_DEFECT_KINDS,
|
|
188
|
+
fnv1a32,
|
|
189
|
+
chainHashOf,
|
|
190
|
+
chainLinesOf,
|
|
191
|
+
lastChainLine,
|
|
192
|
+
readTailInfo,
|
|
193
|
+
appendChainedLines,
|
|
194
|
+
EMPTY_LOG_TAIL,
|
|
195
|
+
nextChainFields,
|
|
196
|
+
withChainFields,
|
|
197
|
+
chainRecordLines,
|
|
198
|
+
chainRewrite,
|
|
199
|
+
defaultEventWeight,
|
|
200
|
+
eventWeightOfText,
|
|
201
|
+
verifyEventChain,
|
|
202
|
+
verifyEventChainText,
|
|
203
|
+
renderEventChainVerification,
|
|
204
|
+
rewriteSnapshot,
|
|
205
|
+
rewriteSnapshotUnchanged,
|
|
206
|
+
guardedRewrite,
|
|
207
|
+
DEFAULT_REWRITE_ATTEMPTS,
|
|
208
|
+
} from './event-chain.js';
|
|
209
|
+
export type {
|
|
210
|
+
ChainFields,
|
|
211
|
+
LogTail,
|
|
212
|
+
EventChainLedger,
|
|
213
|
+
RewriteSnapshot,
|
|
214
|
+
GuardedRewriteIo,
|
|
215
|
+
GuardedRewriteResult,
|
|
216
|
+
GuardedRewriteStatus,
|
|
217
|
+
RewriteProposal,
|
|
218
|
+
EventChainDefect,
|
|
219
|
+
EventChainDefectKind,
|
|
220
|
+
EventChainVerification,
|
|
221
|
+
VerifyEventChainOptions,
|
|
222
|
+
} from './event-chain.js';
|
|
175
223
|
export { decideProvenance, environmentCanMintProvenance, publishArgv, discoverPackages, publishPackages, bumpPatch, compareVersions, findUnpackagedSkills, orderByDependencies, syncReadmeVersion } from './publish.js';
|
|
176
224
|
export { fetchAllDownloads } from './downloads.js';
|
|
177
225
|
export type { PackageDownloads, DownloadsReport } from './downloads.js';
|
|
@@ -215,6 +263,7 @@ export type { RiskScore, RiskThresholds } from './risk-scoring.js';
|
|
|
215
263
|
export {
|
|
216
264
|
MODEL_PRICES,
|
|
217
265
|
pricingFor,
|
|
266
|
+
hasKnownPricing,
|
|
218
267
|
normalizeUsage,
|
|
219
268
|
usageCost,
|
|
220
269
|
invocationCost,
|
|
@@ -294,6 +343,43 @@ export {
|
|
|
294
343
|
readUsageLimits,
|
|
295
344
|
weeklyWindowFor,
|
|
296
345
|
} from './usage.js';
|
|
346
|
+
export { claudeProjectsRoot, rawTokenMixOf, weightedTokensOf } from './usage.js';
|
|
347
|
+
export type { RawTokenMix } from './usage.js';
|
|
348
|
+
// Per-stage cost ledger + reconciliation invariant (feature cost-ledger, ADR-001/002/003).
|
|
349
|
+
export {
|
|
350
|
+
COST_LEDGER_SCOPE,
|
|
351
|
+
COST_LEDGER_DEFECT_KINDS,
|
|
352
|
+
COST_LEDGER_VERDICTS,
|
|
353
|
+
DEFAULT_COST_LEDGER_EPSILON,
|
|
354
|
+
extractCostSamples,
|
|
355
|
+
parseWorkflowRunRecord,
|
|
356
|
+
buildCostLedger,
|
|
357
|
+
verifyCostLedgerReport,
|
|
358
|
+
stageCostAggregates,
|
|
359
|
+
renderCostLedger,
|
|
360
|
+
costLedgerJsonl,
|
|
361
|
+
listCostLedgerRuns,
|
|
362
|
+
deriveCostLedger,
|
|
363
|
+
deriveStageCostAggregates,
|
|
364
|
+
writeCostLedgerJsonl,
|
|
365
|
+
} from './cost-ledger.js';
|
|
366
|
+
export type {
|
|
367
|
+
CostLedgerDefect,
|
|
368
|
+
CostLedgerDefectKind,
|
|
369
|
+
CostLedgerIoOptions,
|
|
370
|
+
CostLedgerReconciliation,
|
|
371
|
+
CostLedgerReport,
|
|
372
|
+
CostLedgerRow,
|
|
373
|
+
CostLedgerRunRef,
|
|
374
|
+
CostLedgerSample,
|
|
375
|
+
CostLedgerVerdict,
|
|
376
|
+
BuildCostLedgerInput,
|
|
377
|
+
DeriveCostLedgerOptions,
|
|
378
|
+
StageCostAggregate,
|
|
379
|
+
StageSampleSet,
|
|
380
|
+
WorkflowRunRecord,
|
|
381
|
+
WorkflowStageEntry,
|
|
382
|
+
} from './cost-ledger.js';
|
|
297
383
|
export type {
|
|
298
384
|
ClaudeUsageModel,
|
|
299
385
|
UsageCalibrationChange,
|
package/src/operations.ts
CHANGED
|
@@ -788,6 +788,31 @@ export async function runDoctor(options: { projectRoot: string }): Promise<Docto
|
|
|
788
788
|
} catch { /* either side absent — covered by other checks */ }
|
|
789
789
|
}
|
|
790
790
|
|
|
791
|
+
// 8b. EVIDENCE-CHAIN INTEGRITY (feature event-chain, ADR-001). `.dz/recall-usage.jsonl` and
|
|
792
|
+
// `.dz/guard-audit.jsonl` are what `dz compounding` and `dz guard promote` decide on; a rewrite
|
|
793
|
+
// that loses or duplicates a record there is a wrong verdict with no symptom. Deliberately OUTSIDE
|
|
794
|
+
// the agentdb-writer branch — the evidence base exists whether or not that writer is deployed.
|
|
795
|
+
// Silent when a log is absent or has never been chained: an unchained file is legal (FR-5), not a
|
|
796
|
+
// fault, and reporting it would train the reader to ignore this line.
|
|
797
|
+
try {
|
|
798
|
+
const { verifyEventChainText, EVENT_CHAIN_SCOPE } = await import('./event-chain.js');
|
|
799
|
+
for (const rel of ['recall-usage.jsonl', 'guard-audit.jsonl']) {
|
|
800
|
+
const p = join(root, '.dz', rel);
|
|
801
|
+
if (!existsSync(p)) continue;
|
|
802
|
+
const v = verifyEventChainText(readFileSync(p, 'utf-8'));
|
|
803
|
+
if (v.chained === 0 || v.ok) continue;
|
|
804
|
+
checks.push({
|
|
805
|
+
name: `evidence chain (.dz/${rel})`,
|
|
806
|
+
ok: false,
|
|
807
|
+
detail:
|
|
808
|
+
`${v.defects.length} defect(s): ${v.defects.slice(0, 3).map((d) => `${d.kind}@L${d.line}`).join(', ')}` +
|
|
809
|
+
` — learning verdicts computed from this log are unsafe. Scope: ${EVENT_CHAIN_SCOPE}`,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
} catch {
|
|
813
|
+
/* doctor never throws on a diagnostic */
|
|
814
|
+
}
|
|
815
|
+
|
|
791
816
|
// 9. Vector-tier mirror divergence (dz-rvf-vector-bridge FR-2/ADR R4). INFORMATIONAL, never an
|
|
792
817
|
// error exit: lexical is the source of truth and `dz consolidate` backfills the mirror. Per
|
|
793
818
|
// Constraint 7 (QR-10) the line reports BOTH counts so a diverged mirror is not misdiagnosed
|
package/src/recall-usage.ts
CHANGED
|
@@ -6,9 +6,22 @@
|
|
|
6
6
|
* into aggregate JSONL rows when it crosses a bounded size. It deliberately knows nothing about the
|
|
7
7
|
* filesystem; callers own reads/writes so the hook and statusline can keep their never-block rules.
|
|
8
8
|
*
|
|
9
|
+
* Compaction RE-CHAINS everything it writes and records what it measured in its input, so that a
|
|
10
|
+
* rewrite which counts an event twice fails `verifyEventChain` instead of producing a well-formed
|
|
11
|
+
* lie — the 2 → 4 → 6 defect below is the reason (see `event-chain.ts`, ADR-002).
|
|
12
|
+
*
|
|
9
13
|
* @packageDocumentation
|
|
10
14
|
*/
|
|
11
15
|
|
|
16
|
+
import {
|
|
17
|
+
EVENT_CHAIN_FIELD_OVERHEAD_BYTES,
|
|
18
|
+
EVENT_CHAIN_LEDGER_KIND,
|
|
19
|
+
chainRewrite,
|
|
20
|
+
defaultEventWeight,
|
|
21
|
+
verifyEventChainText,
|
|
22
|
+
type EventChainDefect,
|
|
23
|
+
} from './event-chain.js';
|
|
24
|
+
|
|
12
25
|
export const RECALL_USAGE_LOG_RELATIVE = '.dz/recall-usage.jsonl';
|
|
13
26
|
export const RECALL_USAGE_LOG_MAX_BYTES = 1_048_576;
|
|
14
27
|
export const RECALL_USAGE_COMPACT_TARGET_BYTES = Math.floor(RECALL_USAGE_LOG_MAX_BYTES * 0.75);
|
|
@@ -104,7 +117,7 @@ interface Acc {
|
|
|
104
117
|
totalScore: number;
|
|
105
118
|
}
|
|
106
119
|
|
|
107
|
-
export
|
|
120
|
+
export interface RecallUsageRecordInput {
|
|
108
121
|
readonly dzId?: unknown;
|
|
109
122
|
readonly score?: unknown;
|
|
110
123
|
readonly ts?: unknown;
|
|
@@ -112,8 +125,18 @@ export function formatRecallUsageRecord(input: {
|
|
|
112
125
|
readonly runId?: unknown;
|
|
113
126
|
readonly eventId?: unknown;
|
|
114
127
|
readonly queryTruncated?: unknown;
|
|
115
|
-
}
|
|
116
|
-
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The normalized RECORD, before serialization — the writer needs the object so it can hang the
|
|
132
|
+
* event-chain fields off it (`seq`/`prevHash`, ADR-001) instead of string-splicing a finished line.
|
|
133
|
+
*/
|
|
134
|
+
export function buildRecallUsageRecord(input: RecallUsageRecordInput): RecallUsageReadRecord | undefined {
|
|
135
|
+
return normalizeReadRecord(input);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function formatRecallUsageRecord(input: RecallUsageRecordInput): string | undefined {
|
|
139
|
+
const rec = buildRecallUsageRecord(input);
|
|
117
140
|
return rec === undefined ? undefined : `${JSON.stringify(rec)}\n`;
|
|
118
141
|
}
|
|
119
142
|
|
|
@@ -125,6 +148,10 @@ export function parseRecallUsageLog(text: string): ParsedRecallUsageLog {
|
|
|
125
148
|
if (trimmed === '') continue;
|
|
126
149
|
try {
|
|
127
150
|
const parsed = JSON.parse(trimmed) as unknown;
|
|
151
|
+
// The compaction ledger (ADR-002) is chain bookkeeping, not a usage record. Counting it as an
|
|
152
|
+
// invalid line would make `invalidLines` — a health number the report prints — lie by one per
|
|
153
|
+
// compaction generation.
|
|
154
|
+
if (isRecord(parsed) && parsed['kind'] === EVENT_CHAIN_LEDGER_KIND) continue;
|
|
128
155
|
const record = normalizeRecord(parsed);
|
|
129
156
|
if (record === undefined) {
|
|
130
157
|
invalidLines += 1;
|
|
@@ -213,10 +240,68 @@ export function shouldCompactRecallUsageLogSize(
|
|
|
213
240
|
return Number.isFinite(sizeBytes) && sizeBytes > validMax(maxBytes);
|
|
214
241
|
}
|
|
215
242
|
|
|
216
|
-
export
|
|
243
|
+
export interface CompactRecallUsageOptions {
|
|
244
|
+
readonly maxBytes?: number;
|
|
245
|
+
readonly targetBytes?: number;
|
|
246
|
+
readonly compactedAt?: string;
|
|
247
|
+
/**
|
|
248
|
+
* Compact even when the input's chain is already defective. OFF by default and never set by any
|
|
249
|
+
* automatic caller — see {@link compactRecallUsageLogChecked} for why.
|
|
250
|
+
*/
|
|
251
|
+
readonly force?: boolean;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export type CompactRecallUsageStatus = 'compacted' | 'refused-dirty' | 'too-large';
|
|
255
|
+
|
|
256
|
+
export interface CompactRecallUsageResult {
|
|
257
|
+
readonly status: CompactRecallUsageStatus;
|
|
258
|
+
/** Empty unless `status === 'compacted'`. */
|
|
259
|
+
readonly text: string;
|
|
260
|
+
/** The input defects that caused a refusal. */
|
|
261
|
+
readonly defects: readonly EventChainDefect[];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Compaction with its verdict attached.
|
|
266
|
+
*
|
|
267
|
+
* AM-2 (Codex QE HIGH-2) — A REWRITER MUST NOT LAUNDER. Compaction parses the input, drops what it
|
|
268
|
+
* cannot read and re-chains from genesis, so a file carrying a `BrokenLink` or a `DoubleCounted`
|
|
269
|
+
* came out the other side verifying `ok: true`. The strongest evidence check in the system was
|
|
270
|
+
* being erased by the routine that runs automatically at a size threshold — corruption converted
|
|
271
|
+
* into a clean chain, with no record that it ever existed.
|
|
272
|
+
*
|
|
273
|
+
* So: the input is VERIFIED FIRST, and a defective chained region REFUSES. The pre-chain prefix is
|
|
274
|
+
* legal and never blocks anything (FR-5); only real defects do.
|
|
275
|
+
*
|
|
276
|
+
* ACCEPTED CONSEQUENCE, stated because it is the cost: a log that stays defective stops being
|
|
277
|
+
* compacted and grows past its cap. That is the right way round — the size cap is a convenience,
|
|
278
|
+
* the evidence is the product — and it is not silent: `dz doctor` and `dz compounding` both report
|
|
279
|
+
* the chain defect, and the caller logs the refusal.
|
|
280
|
+
*/
|
|
281
|
+
export function compactRecallUsageLogChecked(
|
|
217
282
|
text: string,
|
|
218
|
-
opts:
|
|
219
|
-
):
|
|
283
|
+
opts: CompactRecallUsageOptions = {},
|
|
284
|
+
): CompactRecallUsageResult {
|
|
285
|
+
if (opts.force !== true) {
|
|
286
|
+
const v = verifyEventChainText(typeof text === 'string' ? text : '');
|
|
287
|
+
if (!v.ok) return { status: 'refused-dirty', text: '', defects: v.defects };
|
|
288
|
+
}
|
|
289
|
+
const out = compactVerifiedRecallUsageLog(text, opts);
|
|
290
|
+
return out === ''
|
|
291
|
+
? { status: 'too-large', text: '', defects: [] }
|
|
292
|
+
: { status: 'compacted', text: out, defects: [] };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Back-compatible wrapper: the compacted text, or `''` when the rewrite is REFUSED (a defective
|
|
297
|
+
* input) or cannot fit. Callers that need to tell those apart use
|
|
298
|
+
* {@link compactRecallUsageLogChecked}.
|
|
299
|
+
*/
|
|
300
|
+
export function compactRecallUsageLog(text: string, opts: CompactRecallUsageOptions = {}): string {
|
|
301
|
+
return compactRecallUsageLogChecked(text, opts).text;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function compactVerifiedRecallUsageLog(text: string, opts: CompactRecallUsageOptions): string {
|
|
220
305
|
const maxBytes = validMax(opts.maxBytes ?? RECALL_USAGE_LOG_MAX_BYTES);
|
|
221
306
|
const targetBytes = validTarget(opts.targetBytes ?? Math.floor(maxBytes * 0.75), maxBytes);
|
|
222
307
|
const compactedAt = validTs(opts.compactedAt) ? opts.compactedAt : new Date(0).toISOString();
|
|
@@ -233,23 +318,57 @@ export function compactRecallUsageLog(
|
|
|
233
318
|
const toAggregate = parsed.filter((r) => !retained.has(r as RecallUsageReadRecord));
|
|
234
319
|
const stats = aggregateRecallUsage(toAggregate);
|
|
235
320
|
// Replay rows are budgeted FIRST: they are irreplaceable (the corpus), aggregates are re-derivable.
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
let
|
|
242
|
-
for (const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
321
|
+
const candidates: RecallUsageRecord[] = [...replayRows, ...stats.map((s) => aggregateRecord(s, compactedAt))];
|
|
322
|
+
|
|
323
|
+
// The event weight of the INPUT, measured before aggregation. It must come from the input — a
|
|
324
|
+
// total derived from the output could never disagree with it (ADR-002), which is exactly how a
|
|
325
|
+
// double-counting rewrite went unseen until a human re-read the code.
|
|
326
|
+
let sourceEvents = 0;
|
|
327
|
+
for (const rec of parsed) sourceEvents += defaultEventWeight(rec as unknown as Record<string, unknown>);
|
|
328
|
+
|
|
329
|
+
// SELECT, then chain (ADR-002 decision 5): trimming a chain after building it punches holes in it.
|
|
330
|
+
// Each candidate is charged its own bytes plus a fixed allowance for the chain fields it will get.
|
|
331
|
+
let selected = candidates;
|
|
332
|
+
if (byteLength(joinLines(candidates.map(serializeWithChainAllowance))) > maxBytes) {
|
|
333
|
+
const kept: RecallUsageRecord[] = [];
|
|
334
|
+
let used = 0;
|
|
335
|
+
for (const rec of candidates) {
|
|
336
|
+
const cost = byteLength(`${serializeWithChainAllowance(rec)}\n`);
|
|
337
|
+
if (kept.length > 0 && used + cost > targetBytes) continue;
|
|
338
|
+
if (cost > maxBytes) continue;
|
|
339
|
+
if (used + cost <= maxBytes) {
|
|
340
|
+
kept.push(rec);
|
|
341
|
+
used += cost;
|
|
342
|
+
}
|
|
249
343
|
}
|
|
344
|
+
selected = kept;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Final shrink: drop from the TAIL and re-chain, so the survivors are always a valid chain.
|
|
348
|
+
for (;;) {
|
|
349
|
+
const out = joinLines(chainRewrite(selected, { sourceEvents, droppedEvents: droppedEvents(sourceEvents, selected), compactedAt }));
|
|
350
|
+
if (byteLength(out) <= maxBytes) return out;
|
|
351
|
+
if (selected.length === 0) return '';
|
|
352
|
+
selected = selected.slice(0, -1);
|
|
250
353
|
}
|
|
251
|
-
|
|
252
|
-
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* What the byte budget discarded — so a deliberate trim is never mistaken for lost records.
|
|
358
|
+
*
|
|
359
|
+
* The `Math.max(0, …)` is LOAD-BEARING, not defensive tidiness: it is what stops a rewrite that
|
|
360
|
+
* emits MORE events than it read from explaining its own inflation away with a negative "dropped"
|
|
361
|
+
* figure. Over-accounting therefore always reaches {@link verifyEventChain} as `DoubleCounted`.
|
|
362
|
+
*/
|
|
363
|
+
function droppedEvents(sourceEvents: number, selected: readonly RecallUsageRecord[]): number {
|
|
364
|
+
let accounted = 0;
|
|
365
|
+
for (const rec of selected) accounted += defaultEventWeight(rec as unknown as Record<string, unknown>);
|
|
366
|
+
return Math.max(0, sourceEvents - accounted);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** A record's serialized size plus the allowance for the chain fields it will carry. */
|
|
370
|
+
function serializeWithChainAllowance(rec: RecallUsageRecord): string {
|
|
371
|
+
return JSON.stringify(rec) + ' '.repeat(EVENT_CHAIN_FIELD_OVERHEAD_BYTES);
|
|
253
372
|
}
|
|
254
373
|
|
|
255
374
|
function normalizeRecord(value: unknown): RecallUsageRecord | undefined {
|
|
@@ -356,8 +475,8 @@ function mergeAggregate(byId: Map<string, Acc>, rec: RecallUsageAggregateRecord)
|
|
|
356
475
|
}
|
|
357
476
|
}
|
|
358
477
|
|
|
359
|
-
function
|
|
360
|
-
|
|
478
|
+
function aggregateRecord(stat: RecallUsageStat, compactedAt: string): RecallUsageAggregateRecord {
|
|
479
|
+
return {
|
|
361
480
|
kind: 'aggregate',
|
|
362
481
|
dzId: stat.dzId,
|
|
363
482
|
reads: stat.reads,
|
|
@@ -367,7 +486,6 @@ function aggregateLine(stat: RecallUsageStat, compactedAt: string): string {
|
|
|
367
486
|
totalScore: stat.avgScore * stat.reads,
|
|
368
487
|
compactedAt,
|
|
369
488
|
};
|
|
370
|
-
return JSON.stringify(rec);
|
|
371
489
|
}
|
|
372
490
|
|
|
373
491
|
function patternRef(p: RecallPatternUsageRef): RecallPatternUsageRef {
|
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 : '';
|