ccdeck 3.38.3 → 3.38.4

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.
@@ -233,13 +233,36 @@ const USAGE_TOTAL_FIELDS = [...USAGE_BLOCK_FIELDS, ...CACHE_SPLIT_FIELDS];
233
233
  * bucket and not an addition to it. What was charged is added to `charged`,
234
234
  * so a request's bill can be taken back when its final record replaces it. */
235
235
  function chargeUsage(state, bucket, blob, fields, speedBucket, charged) {
236
- for (const k of fields) {
237
- const n = grabUsageField(blob, k);
238
- state.usage[k] += n;
239
- if (bucket) bucket[k] += n;
240
- if (speedBucket) speedBucket[k] += n;
241
- charged[k] += n;
242
- }
236
+ for (const k of fields) chargeTokens(state, bucket, speedBucket, charged, k, grabUsageField(blob, k));
237
+ }
238
+
239
+ /** Charge a request's record from its parsed `message.usage`, the same way.
240
+ *
241
+ * Not off the text, as chargeUsage reads a line: USAGE_BLOCK_RE's `[^}]+`
242
+ * stops at the first `}`, and a subagent's streaming snapshot writes its
243
+ * `cache_creation` sub-object before `output_tokens` (all 49,046 snapshots on
244
+ * one machine), so the output a snapshot had streamed read as 0 until the
245
+ * final record came, and stayed 0 for a request that never finished. A
246
+ * request's record names its model, so the line is parsed already; the
247
+ * Projects report reads the same object (countersFrom in account-projects.mjs). */
248
+ function chargeRequestUsage(state, bucket, usage, speedBucket, charged) {
249
+ const split = usage?.cache_creation;
250
+ for (const k of USAGE_BLOCK_FIELDS) chargeTokens(state, bucket, speedBucket, charged, k, tokenCount(usage?.[k]));
251
+ for (const k of CACHE_SPLIT_FIELDS) chargeTokens(state, bucket, speedBucket, charged, k, tokenCount(split?.[k]));
252
+ }
253
+
254
+ /** Add `n` tokens of counter `k` to every place one charge goes. */
255
+ function chargeTokens(state, bucket, speedBucket, charged, k, n) {
256
+ state.usage[k] += n;
257
+ if (bucket) bucket[k] += n;
258
+ if (speedBucket) speedBucket[k] += n;
259
+ charged[k] += n;
260
+ }
261
+
262
+ /** A usage counter as a token count: a whole number of tokens, or 0 for
263
+ * anything else, as USAGE_FIELD_RE reads one off the text. */
264
+ function tokenCount(v) {
265
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.trunc(v) : 0;
243
266
  }
244
267
 
245
268
  /** Add one bucket's counters, and its speed shares, into `dst`. */
@@ -329,9 +352,10 @@ function newTranscriptState() {
329
352
  // mirror of it. Costs no extra read: `message.model` and the `"usage"` block
330
353
  // are on the same line, and this pass already parses both.
331
354
  usageByModel: {},
332
- // The request billed from a snapshot whose final record has not been read
333
- // yet, and what it was charged — see requestBilling. Null between requests.
334
- streamingRequest: null,
355
+ // What this file has read of its requests: the newest request record, and
356
+ // the request billed from a snapshot whose final record has not been read
357
+ // yet, with what it was charged — see requestBilling.
358
+ requests: newRequestLedger(),
335
359
  ctx: newContextBreakdown(),
336
360
  };
337
361
  }
@@ -469,24 +493,82 @@ const SKIP_LINE = "skip";
469
493
  * the request it is waiting on, so the two can arrive in different passes,
470
494
  * which for a live subagent is the usual case.
471
495
  *
496
+ * A resumed subagent writes its history into its file again before the new
497
+ * turn: the same records, with the same `uuid`, `requestId` and `timestamp`,
498
+ * usually byte for byte. Billed as they came, every request in that history was
499
+ * charged once more per replay — on one machine 98 requests in two files, 821
500
+ * times over. A restated record is told apart by its timestamp, which is older
501
+ * than the newest request record the file has already read: nothing Claude
502
+ * Code writes fresh is, none of 162,790 request records on that machine. Telling
503
+ * them apart by request id would need every id a file has billed, kept for as
504
+ * long as the file can grow and persisted with the Projects tally; the newest
505
+ * timestamp is one number. See restatesEarlierRecord.
506
+ *
507
+ * A file that starts in the middle of a request — a fork's file opens with its
508
+ * parent's turn, a resumed agent's with the one it stopped in — has no block 0
509
+ * for it, and that request is not billed here. It is not this file's: on that
510
+ * machine 36 of 38 such openings were requests the parent's or the earlier
511
+ * agent's transcript bills in full, block 0 and final record. The other two
512
+ * were workflow agents whose transcript continued in a second file mid-request,
513
+ * the snapshot billed at the end of the first and the final record opening the
514
+ * second; billing that record here would need to know what the other file
515
+ * charged for it.
516
+ *
472
517
  * BILL_LINE a record from before Claude Code marked its blocks, billed
473
518
  * line by line as it always was
474
519
  * BILL_REQUEST the first record of a request
475
520
  * REBILL_REQUEST a later record of the request still streaming: it replaces
476
521
  * that request's bill
477
522
  * SKIP_LINE a later block of a request already billed from its final
478
- * record, which restates it
523
+ * record, which restates it, or a record restated by a replay
479
524
  *
480
- * The Projects report (account-projects.mjs) bills its own pass of the same
481
- * lines by this rule.
525
+ * `ledger` is what the file has read of its requests so far (newRequestLedger),
526
+ * or null for none; noteRequestRecord adds each record to it. The Projects
527
+ * report (account-projects.mjs) bills its own pass of the same lines by this
528
+ * rule.
482
529
  */
483
- function requestBilling(record, streamingId) {
530
+ function requestBilling(record, ledger) {
484
531
  if (record?.apiBlockIndex === undefined) return BILL_LINE;
532
+ if (restatesEarlierRecord(record, ledger)) return SKIP_LINE;
485
533
  const id = requestIdOf(record);
486
- if (id !== null && id === streamingId && isUsageRecord(record)) return REBILL_REQUEST;
534
+ if (id !== null && id === ledger?.streaming?.id && isUsageRecord(record)) return REBILL_REQUEST;
487
535
  return record.apiBlockIndex === 0 ? BILL_REQUEST : SKIP_LINE;
488
536
  }
489
537
 
538
+ /** What one file has read of its requests, for requestBilling: when the newest
539
+ * request record was written and which request it was, and the request still
540
+ * streaming — `{ id, … }` with whatever its caller needs to take its charge
541
+ * back — or null. Plain data, so the Projects report can persist it. */
542
+ function newRequestLedger() {
543
+ return { newestAt: null, newestId: null, streaming: null };
544
+ }
545
+
546
+ /** Is `record` a copy of one the file has already read, written again by a
547
+ * resumed subagent restating its history? The copy keeps its original
548
+ * timestamp, so it is older than the newest request record read — or, written
549
+ * at that same moment, it is block 0 of the very request that record belongs
550
+ * to, which a request has only one of. A later block at that moment is not a
551
+ * copy: Claude Code writes two blocks of one request within a millisecond
552
+ * often enough (38 times on that machine). A record without a timestamp is
553
+ * never taken for a copy. */
554
+ function restatesEarlierRecord(record, ledger) {
555
+ const at = Date.parse(record.timestamp);
556
+ const newest = ledger?.newestAt ?? null;
557
+ if (newest === null || !Number.isFinite(at)) return false;
558
+ if (at !== newest) return at < newest;
559
+ const id = requestIdOf(record);
560
+ return record.apiBlockIndex === 0 && id !== null && id === ledger.newestId;
561
+ }
562
+
563
+ /** Add a request record to the file's ledger, once it has been billed by
564
+ * requestBilling — skipped or not, it is a record the file has read. */
565
+ function noteRequestRecord(ledger, record) {
566
+ const at = Date.parse(record.timestamp);
567
+ if (!Number.isFinite(at) || (ledger.newestAt !== null && at < ledger.newestAt)) return;
568
+ ledger.newestAt = at;
569
+ ledger.newestId = requestIdOf(record);
570
+ }
571
+
490
572
  /** The request a record belongs to while its final record has yet to come —
491
573
  * the id requestBilling is next asked with — or null once it is final. */
492
574
  function streamingRequestId(record) {
@@ -509,10 +591,11 @@ function isUsageRecord(record) {
509
591
  * or null when it names no model. */
510
592
  function foldUsageLine(state, line, record) {
511
593
  // A request is billed once, from its final record — see requestBilling.
512
- const streaming = state.streamingRequest;
513
- const billing = requestBilling(record, streaming?.id ?? null);
594
+ const ledger = state.requests;
595
+ const billing = requestBilling(record, ledger);
596
+ if (billing !== BILL_LINE) noteRequestRecord(ledger, record);
514
597
  if (billing === SKIP_LINE) return;
515
- if (billing === REBILL_REQUEST) refundUsage(state, streaming);
598
+ if (billing === REBILL_REQUEST) refundUsage(state, ledger.streaming);
516
599
  // Usage totals sum every block in the file, resets included — every block the
517
600
  // model was actually billed for, which is why the `toolUseResult` tail is cut
518
601
  // off first (see billedUsageText) — and are summed a second time into the
@@ -532,21 +615,24 @@ function foldUsageLine(state, line, record) {
532
615
  // text: a split that read a wider stretch of the line than the total it splits
533
616
  // would re-introduce #685's double count on one side of the arithmetic only,
534
617
  // and the two would stop summing to each other.
535
- const billed = billedUsageText(line);
536
618
  const bucket = usageBucketFor(state, state.lastModel);
537
619
  const speedBucket = speedBucketFor(bucket, billedSpeed(record));
538
620
  const charged = newUsageTotals();
539
- for (const m of billed.matchAll(USAGE_BLOCK_RE)) {
540
- chargeUsage(state, bucket, m[1], USAGE_BLOCK_FIELDS, speedBucket, charged);
541
- }
542
- for (const m of billed.replace(ITERATIONS_ARRAY_RE, "").matchAll(CACHE_CREATION_BLOCK_RE)) {
543
- chargeUsage(state, bucket, m[1], CACHE_SPLIT_FIELDS, speedBucket, charged);
621
+ if (billing === BILL_LINE) {
622
+ const billed = billedUsageText(line);
623
+ for (const m of billed.matchAll(USAGE_BLOCK_RE)) {
624
+ chargeUsage(state, bucket, m[1], USAGE_BLOCK_FIELDS, speedBucket, charged);
625
+ }
626
+ for (const m of billed.replace(ITERATIONS_ARRAY_RE, "").matchAll(CACHE_CREATION_BLOCK_RE)) {
627
+ chargeUsage(state, bucket, m[1], CACHE_SPLIT_FIELDS, speedBucket, charged);
628
+ }
629
+ return;
544
630
  }
545
- if (billing === BILL_LINE) return;
631
+ chargeRequestUsage(state, bucket, record.message?.usage, speedBucket, charged);
546
632
  // What this request was charged, and where, kept until its final record
547
633
  // replaces it.
548
634
  const id = streamingRequestId(record);
549
- state.streamingRequest = id === null ? null : { id, bucket, speedBucket, charged };
635
+ ledger.streaming = id === null ? null : { id, bucket, speedBucket, charged };
550
636
  }
551
637
 
552
638
  /** Take back what a request was charged, from every counter it was charged
@@ -741,6 +827,6 @@ function scanTranscript(path) {
741
827
  // above reads exactly as it did where it came from.
742
828
  export {
743
829
  scanTranscript, newUsageTotals, hasSpend, mergeUsageByModel, copyUsageBucket,
744
- requestBilling, streamingRequestId, BILL_LINE, REBILL_REQUEST, SKIP_LINE,
745
- billedSpeed, MAX_SPEEDS_PER_MODEL, UNRECOGNISED_SPEED,
830
+ requestBilling, newRequestLedger, noteRequestRecord, streamingRequestId,
831
+ BILL_LINE, REBILL_REQUEST, SKIP_LINE, billedSpeed, MAX_SPEEDS_PER_MODEL, UNRECOGNISED_SPEED,
746
832
  };