@assuredloop/orca-bot-kit 0.17.0 → 0.19.0

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/src/usage.js CHANGED
@@ -39,11 +39,12 @@
39
39
  // the same call can be in more than one of those files, so a call counts
40
40
  // once across all of a conversation's files together (#371).
41
41
 
42
- import { readFileSync, realpathSync } from 'node:fs';
42
+ import { realpathSync } from 'node:fs';
43
43
 
44
44
  import { botDir, botNames, readBot } from './bot.js';
45
45
  import { readBook, sessionIdsIn } from './book.js';
46
- import { claudeSubagentTranscripts, codexRollout, transcriptsIn } from './conversations.js';
46
+ import { claudeSubagentTranscripts, claudeTranscriptAnywhere, codexRollout, transcriptsIn } from './conversations.js';
47
+ import { eachLine } from './lines.js';
47
48
  import { HARNESSES, harnessOf } from './launch.js';
48
49
 
49
50
  /** The kinds a call's tokens are reported in, the same on either harness. */
@@ -123,6 +124,20 @@ function forBot(bots, name, onlySession, window) {
123
124
 
124
125
  const claimed = sessionIdsIn(book);
125
126
 
127
+ // A conversation the book names is its session's wherever the harness filed
128
+ // it: the book says whose it is (ADR 0012). One filed under another
129
+ // folder, or whose record cannot be opened to say which folder it ran in, is
130
+ // found by its id instead, and counted or said to be unreadable (#396).
131
+ for (const id of claimed) {
132
+ if (onRecord.has(id)) continue;
133
+ for (const harness of harnesses) {
134
+ const file = harness === 'codex' ? codexRollout(id) : harness === 'claude' ? claudeTranscriptAnywhere(id) : undefined;
135
+ if (file === undefined) continue;
136
+ onRecord.set(id, { id, file, harness, subagent: false });
137
+ break;
138
+ }
139
+ }
140
+
126
141
  // Two entries can name the same conversation, one resumed after its session
127
142
  // was retired, and one entry can name it twice, now and in its history: it is
128
143
  // counted once, in the first block asked for that names it.
@@ -190,7 +205,7 @@ function counted(one, window) {
190
205
  tally.gaps.unreadable_transcripts += subagents.unreadable;
191
206
  const sources = [one.file, ...subagents.files]
192
207
  .map((file, index) => {
193
- const { entries, unreadable, broken } = transcript(file);
208
+ const { entries, unreadable, broken } = transcript(file, countable);
194
209
  tally.gaps.unreadable_transcripts += unreadable;
195
210
  tally.gaps.broken_lines += broken;
196
211
  return { entries, subagent: index > 0 };
@@ -463,7 +478,7 @@ function originOf(entries, tally) {
463
478
  if (typeof id !== 'string' || id === '') return undefined;
464
479
 
465
480
  const file = codexRollout(id);
466
- const origin = file === undefined ? { unreadable: 1 } : transcript(file);
481
+ const origin = file === undefined ? { unreadable: 1 } : transcript(file, countable);
467
482
  // No `info` is a note about rate limits, not a record of usage.
468
483
  const records = origin.unreadable > 0 ? [] : origin.entries
469
484
  .filter((entry) => entry.type === 'event_msg' && entry.payload?.type === 'token_count')
@@ -552,28 +567,44 @@ function count(tally, used, model, effort, when, calls = 1, subagent = false) {
552
567
  export const lines = (file) => transcript(file).entries;
553
568
 
554
569
  /**
555
- * The lines of a transcript that are JSON records, and how many were not. A
556
- * transcript that cannot be read is said to be, rather than a run that stops.
570
+ * The records the counting here reads: Claude Code's `assistant` and `system`
571
+ * lines, Codex's `session_meta`, `turn_context`, `compacted` and `event_msg`.
572
+ * The rest (what was said, tool calls and their output) is most of a long
573
+ * conversation's record and none of what it cost, so it is not held (#396).
574
+ */
575
+ const COUNTED = new Set(['assistant', 'system', 'session_meta', 'turn_context', 'compacted', 'event_msg']);
576
+ const countable = (entry) => COUNTED.has(entry.type);
577
+
578
+ /**
579
+ * The lines of a transcript that are JSON records, and how many were not; with
580
+ * `keep`, only the records it takes. A transcript that cannot be read is said
581
+ * to be, rather than a run that stops. It is read a line at a time, whatever
582
+ * its size, and a line too long to hold counts as one that is not a record
583
+ * (#396).
557
584
  */
558
- function transcript(file) {
559
- let text;
585
+ function transcript(file, keep = () => true) {
586
+ const entries = [];
587
+ let broken = 0;
560
588
  try {
561
- text = readFileSync(file, 'utf8');
589
+ eachLine(file, (line) => {
590
+ if (line === undefined) {
591
+ broken += 1;
592
+ return;
593
+ }
594
+ if (line.trim() === '') return;
595
+ let entry;
596
+ try {
597
+ entry = JSON.parse(line);
598
+ } catch {
599
+ broken += 1;
600
+ return;
601
+ }
602
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) broken += 1;
603
+ else if (keep(entry)) entries.push(entry);
604
+ });
562
605
  } catch {
563
606
  return { entries: [], unreadable: 1, broken: 0 };
564
607
  }
565
- let broken = 0;
566
- const entries = text.split('\n').flatMap((line) => {
567
- if (line.trim() === '') return [];
568
- try {
569
- const entry = JSON.parse(line);
570
- if (entry !== null && typeof entry === 'object' && !Array.isArray(entry)) return [entry];
571
- } catch {
572
- // Counted below with any other line that is not a record.
573
- }
574
- broken += 1;
575
- return [];
576
- });
577
608
  return { entries, unreadable: 0, broken };
578
609
  }
579
610