fix: transcript pricing never throws from a PTY ledger writer

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-29 04:59:06 +00:00
1 parent 55bd791f32
commit 876cf1a8d3
2 files changed
+22 -21

No files matched your search

@@ -45,6 +45,7 @@ describe('ptyTranscriptCostUsd', () => {
const unpriced = fixture(project => fs.writeFileSync(path.join(project, `${SID}.jsonl`),
assistant('msg_a', 'claude-opus-5-5', { output_tokens: 1 }) + '\n' + assistant('msg_b', 'some-unpriced-model', { output_tokens: 1 }) + '\n'));
const empty = fixture(project => fs.writeFileSync(path.join(project, `${SID}.jsonl`), JSON.stringify({ type: 'user', sessionId: SID }) + '\n'));
const unreadable = fixture(project => fs.mkdirSync(path.join(project, `${SID}.jsonl`)));
try {
expect(ptyTranscriptCostUsd(unpriced, SID)).toBeUndefined();
expect(ptyTranscriptCostUsd(empty, SID)).toBeUndefined();
@@ -53,6 +54,7 @@ describe('ptyTranscriptCostUsd', () => {
expect(ptyTranscriptCostUsd(null, SID)).toBeUndefined();
expect(ptyTranscriptCostUsd(path.join(empty, 'missing'), SID)).toBeUndefined();
expect(pricedTranscript(undefined)).toEqual({});
} finally { for (const dir of [unpriced, empty]) fs.rmSync(dir, { recursive: true, force: true }); }
expect(ptyTranscriptCostUsd(unreadable, SID)).toBeUndefined();
} finally { for (const dir of [unpriced, empty, unreadable]) fs.rmSync(dir, { recursive: true, force: true }); }
});
});
+19 -20
View File
@@ -12,31 +12,30 @@ export function pricedTranscript(session: { hermeticConfigDir?: string | null; s
* Price one PTY session from the transcripts Claude Code wrote under its owned config dir:
* `projects/<dir>/<sessionId>.jsonl` (inline sidechains included) and `projects/<dir>/<sessionId>/subagents/*.jsonl`,
* rows deduplicated by message id. Undefined (cost-unknown, never $0) without a config dir, session id, transcript or
* usage, or when a message's model has no price row.
* usage, when a message's model has no price row, or when any read fails: it runs in ledger writers before close and
* cleanup, so it never throws.
*/
export function ptyTranscriptCostUsd(configDir: string | null | undefined, sessionId: string | undefined): number | undefined {
if (!configDir || !sessionId) return undefined;
const projects = path.join(configDir, 'projects');
let dirs: fs.Dirent[];
try {
dirs = fs.readdirSync(projects, { withFileTypes: true });
const projects = path.join(configDir, 'projects');
const files: string[] = [];
for (const dir of fs.readdirSync(projects, { withFileTypes: true })) {
if (!dir.isDirectory()) continue;
const main = path.join(projects, dir.name, `${sessionId}.jsonl`);
if (fs.existsSync(main)) files.push(main);
const subagents = path.join(projects, dir.name, sessionId, 'subagents');
if (fs.existsSync(subagents)) files.push(...fs.readdirSync(subagents).filter(f => f.endsWith('.jsonl')).map(f => path.join(subagents, f)));
}
const rows = files.flatMap(file => fs.readFileSync(file, 'utf8').split('\n').flatMap(line => {
try {
return [JSON.parse(line)];
} catch {
return [];
}
}));
return streamedUsageCostUsd(rows);
} catch {
return undefined;
}
const files: string[] = [];
for (const dir of dirs) {
if (!dir.isDirectory()) continue;
const main = path.join(projects, dir.name, `${sessionId}.jsonl`);
if (fs.existsSync(main)) files.push(main);
const subagents = path.join(projects, dir.name, sessionId, 'subagents');
if (fs.existsSync(subagents)) files.push(...fs.readdirSync(subagents).filter(f => f.endsWith('.jsonl')).map(f => path.join(subagents, f)));
}
const rows = files.flatMap(file => fs.readFileSync(file, 'utf8').split('\n').flatMap(line => {
try {
return [JSON.parse(line)];
} catch {
return [];
}
}));
return streamedUsageCostUsd(rows);
}