Files
ECC/scripts/lib/session-cost-snapshot.js
T

148 lines
4.6 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const { writeFileAtomic } = require('./atomic-write');
const { sanitizeSessionId } = require('./session-bridge');
const COST_SNAPSHOT_SCHEMA_VERSION = 'ecc.cost-snapshot.v1';
const COST_SNAPSHOT_DIRECTORY = 'cost-snapshots';
const COST_LOG_FILENAME = 'costs.jsonl';
function assertSafeSessionId(sessionId) {
if (sanitizeSessionId(sessionId) !== sessionId) {
throw new Error('Cost snapshot requires a safe session ID');
}
}
function getCostSnapshotPath(metricsDir, sessionId) {
assertSafeSessionId(sessionId);
// Prefix the filename so Windows device names such as CON/NUL/COM1 never
// become the basename, even when they are otherwise valid session IDs.
return path.join(metricsDir, COST_SNAPSHOT_DIRECTORY, `session-${sessionId}.json`);
}
function getCostLogSignature(metricsDir) {
const stat = fs.statSync(path.join(metricsDir, COST_LOG_FILENAME));
return {
size_bytes: stat.size,
mtime_ms: stat.mtimeMs
};
}
function signaturesMatch(left, right) {
return left?.size_bytes === right?.size_bytes
&& left?.mtime_ms === right?.mtime_ms;
}
function isValidCostRow(row, sessionId) {
return row?.session_id === sessionId
&& typeof row.estimated_cost_usd === 'number'
&& Number.isFinite(row.estimated_cost_usd)
&& row.estimated_cost_usd >= 0
&& typeof row.input_tokens === 'number'
&& Number.isFinite(row.input_tokens)
&& row.input_tokens >= 0
&& typeof row.output_tokens === 'number'
&& Number.isFinite(row.output_tokens)
&& row.output_tokens >= 0;
}
function assertCostLogSignature(source) {
if (!Number.isSafeInteger(source?.size_bytes) || source.size_bytes < 0) {
throw new Error('Cost snapshot requires a valid source size');
}
if (!Number.isFinite(source?.mtime_ms) || source.mtime_ms < 0) {
throw new Error('Cost snapshot requires a valid source mtime');
}
}
function writeSnapshotForSource(metricsDir, sessionId, row, source) {
assertSafeSessionId(sessionId);
if (!isValidCostRow(row, sessionId)) {
throw new Error('Cost snapshot requires valid non-negative numeric totals for its session');
}
const snapshotPath = getCostSnapshotPath(metricsDir, sessionId);
assertCostLogSignature(source);
return writeFileAtomic(
snapshotPath,
JSON.stringify({
schema_version: COST_SNAPSHOT_SCHEMA_VERSION,
source,
row
}),
{
beforeRename() {
if (!signaturesMatch(source, getCostLogSignature(metricsDir))) {
throw new Error('Cost log changed while publishing its session snapshot');
}
}
}
);
}
function costLogEndsWithRow(metricsDir, row) {
const expected = Buffer.from(`${JSON.stringify(row)}\n`, 'utf8');
const descriptor = fs.openSync(path.join(metricsDir, COST_LOG_FILENAME), 'r');
try {
const stat = fs.fstatSync(descriptor);
if (stat.size < expected.length) return false;
const actual = Buffer.allocUnsafe(expected.length);
const bytesRead = fs.readSync(
descriptor,
actual,
0,
expected.length,
stat.size - expected.length
);
return bytesRead === expected.length && actual.equals(expected);
} finally {
fs.closeSync(descriptor);
}
}
function publishAppendedSessionCostSnapshot(metricsDir, sessionId, row) {
assertSafeSessionId(sessionId);
if (!isValidCostRow(row, sessionId)) {
throw new Error('Cost snapshot requires valid non-negative numeric totals for its session');
}
const sourceBefore = getCostLogSignature(metricsDir);
if (!costLogEndsWithRow(metricsDir, row)) return false;
const sourceAfter = getCostLogSignature(metricsDir);
if (!signaturesMatch(sourceBefore, sourceAfter)) return false;
writeSnapshotForSource(metricsDir, sessionId, row, sourceAfter);
return true;
}
function repairSessionCostSnapshot(metricsDir, sessionId, row, source) {
writeSnapshotForSource(metricsDir, sessionId, row, source);
}
function readSessionCostSnapshot(metricsDir, sessionId) {
try {
const snapshot = JSON.parse(
fs.readFileSync(getCostSnapshotPath(metricsDir, sessionId), 'utf8')
);
if (snapshot?.schema_version !== COST_SNAPSHOT_SCHEMA_VERSION) return null;
if (!isValidCostRow(snapshot.row, sessionId)) return null;
if (!signaturesMatch(snapshot.source, getCostLogSignature(metricsDir))) return null;
return snapshot.row;
} catch {
return null;
}
}
module.exports = {
COST_SNAPSHOT_SCHEMA_VERSION,
COST_SNAPSHOT_DIRECTORY,
COST_LOG_FILENAME,
getCostSnapshotPath,
getCostLogSignature,
signaturesMatch,
isValidCostRow,
publishAppendedSessionCostSnapshot,
repairSessionCostSnapshot,
readSessionCostSnapshot
};