feat(db): trim stored hook payloads; price the session list like a session

Analytics parity, phase 1 (data hygiene), from the analytics-parity session.

events.data is now stored through the SQL function trim_event_data(?)
(server/lib/event-payload.js, registered in server/db.js) on every event
INSERT and in the duplicate guard: Edit/MultiEdit/NotebookEdit/Write drop
the whole-file mirrors (originalFile, oldString/newString, Write content),
Read drops file.content/base64, and any string in tool_input/tool_response
over 8 KB keeps a 6 KB head + 2 KB tail. structuredPatch and every field
outside those two stay whole; _trimmed names what was dropped. The full
text stays in the transcript and its snapshot. Rows stored before this are
trimmed once in the background (sweepEventPayloads, 200 rows per write
transaction, user_version 4). No VACUUM, no row pruned.

The session list now prices every token column /api/pricing/cost/:id does
(speed, geo, tier, the 1h cache write, server-tool fees) through one
tokensForSessions helper, so a row's cost equals the session's own.
This commit is contained in:
2026-10-05 13:13:02 +07:00
parent 138e706dd1
commit 215cae7393
9 changed files with 368 additions and 46 deletions
+26 -35
View File
@@ -121,6 +121,30 @@ async function readFirstLine(filePath) {
return null;
}
// Every priced column, as `stmts.getTokensBySession` reads one session — so a
// list row's cost equals the session's own `/api/pricing/cost/:id` (speed, geo,
// tier, the 1h cache write and server-tool fees included). ≤ 900 ids per call.
function tokensForSessions(ids) {
const out = {};
if (ids.length === 0) return out;
const rows = db
.prepare(
`SELECT session_id, model, speed, inference_geo, service_tier,
input_tokens + baseline_input as input_tokens,
output_tokens + baseline_output as output_tokens,
cache_read_tokens + baseline_cache_read as cache_read_tokens,
cache_write_tokens + baseline_cache_write as cache_write_tokens,
cache_write_1h_tokens + baseline_cache_write_1h as cache_write_1h_tokens,
web_search_requests + baseline_web_search as web_search_requests,
web_fetch_requests + baseline_web_fetch as web_fetch_requests,
code_execution_requests + baseline_code_execution as code_execution_requests
FROM token_usage WHERE session_id IN (${ids.map(() => "?").join(",")})`
)
.all(...ids);
for (const t of rows) (out[t.session_id] ||= []).push(t);
return out;
}
router.get("/", (req, res) => {
const limit = Math.min(parseInt(req.query.limit) || 50, 10000);
const offset = parseInt(req.query.offset) || 0;
@@ -201,23 +225,7 @@ router.get("/", (req, res) => {
for (let i = 0; i < allRows.length; i += 900) {
const chunk = allRows.slice(i, i + 900);
const ids = chunk.map((r) => r.id);
const placeholders = ids.map(() => "?").join(",");
const chunkTokens = db
.prepare(
`SELECT session_id, model,
input_tokens + baseline_input as input_tokens,
output_tokens + baseline_output as output_tokens,
cache_read_tokens + baseline_cache_read as cache_read_tokens,
cache_write_tokens + baseline_cache_write as cache_write_tokens
FROM token_usage WHERE session_id IN (${placeholders})`
)
.all(...ids);
const tokensBySession = {};
for (const t of chunkTokens) {
if (!tokensBySession[t.session_id]) tokensBySession[t.session_id] = [];
tokensBySession[t.session_id].push(t);
}
const tokensBySession = tokensForSessions(ids);
for (const row of chunk) {
const sessionTokens = tokensBySession[row.id];
@@ -250,25 +258,8 @@ router.get("/", (req, res) => {
.all(...params, limit, offset);
if (rows.length > 0) {
const ids = rows.map((r) => r.id);
const placeholders = ids.map(() => "?").join(",");
const allTokens = db
.prepare(
`SELECT session_id, model,
input_tokens + baseline_input as input_tokens,
output_tokens + baseline_output as output_tokens,
cache_read_tokens + baseline_cache_read as cache_read_tokens,
cache_write_tokens + baseline_cache_write as cache_write_tokens
FROM token_usage WHERE session_id IN (${placeholders})`
)
.all(...ids);
const rules = stmts.listPricing.all();
const tokensBySession = {};
for (const t of allTokens) {
if (!tokensBySession[t.session_id]) tokensBySession[t.session_id] = [];
tokensBySession[t.session_id].push(t);
}
const tokensBySession = tokensForSessions(rows.map((r) => r.id));
for (const row of rows) {
const sessionTokens = tokensBySession[row.id];