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:
7
.changeset/usage-events-retention.md
Normal file
7
.changeset/usage-events-retention.md
Normal 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.
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user