fix: add usage_events to operational-log retention

usage_events was absent from Database.pruneOperationalLogs, so the
per-tool telemetry log grew unbounded (~187k rows / ~28MB observed) and
became a dominant driver of .fusion DB bloat once runAuditEvents was
already 30-day capped. Prune it on the same operationalLogRetentionDays
cadence, keyed off its `ts` column (not `timestamp`), alongside the other
column-name exceptions. Adds a regression test and changeset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-07 22:58:11 -07:00
parent ce4f173d8f
commit ac719d1203
3 changed files with 48 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Stop the usage telemetry log from growing without bound and bloating the Fusion database.
category: fix
dev: usage_events was absent from operational-log retention, so it grew unbounded (observed ~187k rows / ~28MB with nothing ever aged out). pruneOperationalLogs now prunes usage_events on the same operationalLogRetentionDays cadence, keyed off its `ts` column (not `timestamp`). Existing rows still require a one-time VACUUM to reclaim on-disk space.

View File

@@ -3605,6 +3605,11 @@ describe("Database operational-log retention and recovery-table cleanup", () =>
).run(id, agentId, createdAt);
}
function insertUsageEvent(ts: string): void {
// usage_events.id is INTEGER PRIMARY KEY AUTOINCREMENT — let it auto-assign.
db.prepare("INSERT INTO usage_events (ts, kind) VALUES (?, 'tool_call')").run(ts);
}
it("pruneOperationalLogs deletes rows older than the retention window", () => {
const old = new Date(Date.now() - 200 * 86_400_000).toISOString();
const recent = new Date(Date.now() - 1 * 86_400_000).toISOString();
@@ -3675,8 +3680,27 @@ describe("Database operational-log retention and recovery-table cleanup", () =>
expect(remaining.map((row) => row.id)).toEqual(["agent-1-recent", "agent-2-old-2"]);
});
it("pruneOperationalLogs deletes old usage_events by their `ts` column but keeps recent ones", () => {
// FNXC:TelemetryRetention 2026-07-08-00:00:
// Regression guard for the unbounded-growth fix: usage_events must age out on
// the same retention cadence as the other operational logs, keyed off its `ts`
// column (not `timestamp`), which is why it needs its own delete path.
const old = new Date(Date.now() - 200 * 86_400_000).toISOString();
const recent = new Date(Date.now() - 1 * 86_400_000).toISOString();
insertUsageEvent(old);
insertUsageEvent(old);
insertUsageEvent(recent);
const result = db.pruneOperationalLogs(90 * 86_400_000);
expect(result.deletedByTable.usage_events).toBe(2);
const remaining = db.prepare("SELECT ts FROM usage_events").all() as Array<{ ts: string }>;
expect(remaining).toEqual([{ ts: recent }]);
});
it("pruneOperationalLogs is a no-op when retention is disabled (<= 0)", () => {
insertActivity("old-1", new Date(Date.now() - 200 * 86_400_000).toISOString());
insertUsageEvent(new Date(Date.now() - 200 * 86_400_000).toISOString());
insertAgent("agent-1");
insertAgentRun({
id: "run-old-completed",
@@ -3694,6 +3718,7 @@ describe("Database operational-log retention and recovery-table cleanup", () =>
const result = db.pruneOperationalLogs(0);
expect(result.deletedTotal).toBe(0);
expect(db.prepare("SELECT count(*) AS c FROM activityLog").get()).toMatchObject({ c: 1 });
expect(db.prepare("SELECT count(*) AS c FROM usage_events").get()).toMatchObject({ c: 1 });
expect(db.prepare("SELECT count(*) AS c FROM agentRuns").get()).toMatchObject({ c: 1 });
expect(db.prepare("SELECT count(*) AS c FROM agentConfigRevisions").get()).toMatchObject({ c: 1 });
});

View File

@@ -2581,6 +2581,22 @@ export class Database {
}
}
/*
* FNXC:TelemetryRetention 2026-07-08-00:00:
* usage_events is an append-only per-tool telemetry log. Unlike the OPERATIONAL_LOG_TABLES set it originally had NO retention, so it grew unbounded and became the dominant driver of .fusion DB bloat once runAuditEvents was already 30-day capped (observed ~187k rows / ~28MB with no aged-out rows because nothing ever deleted them).
* Prune it on the same operationalLogRetentionDays cadence as the other operational logs. Its timestamp column is `ts` (not `timestamp`), so it cannot join the generic OPERATIONAL_LOG_TABLES loop above and gets its own delete here alongside the other column-name exceptions (agentRuns.endedAt, agentConfigRevisions.createdAt).
*/
if (this.tableExists("usage_events")) {
try {
recordChanges(
"usage_events",
this.db.prepare("DELETE FROM usage_events WHERE ts < ?").run(cutoffIso),
);
} catch (error) {
console.warn("[fusion:db] Failed to prune operational log table usage_events", error);
}
}
return { deletedByTable, deletedTotal };
}