fix(core): add backlog cap for agent log buffer and close DB on init failure

Two fixes from CodeRabbit review:
- Add MAX_AGENT_LOG_BACKLOG cap (5000 entries) to prevent unbounded memory
  growth during prolonged SQLite outages. Oldest entries are dropped when
  cap is exceeded.
- Only assign this._db/this._archiveDb after init() succeeds. On failure,
  close the DB handle before throwing so callers never get a partially
  initialized instance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Timothy Laurent
2026-05-04 11:56:57 -07:00
parent 8d3f074492
commit eb675749e3

View File

@@ -526,6 +526,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
private static readonly AGENT_LOG_BUFFER_SIZE = 50;
/** Flush interval in milliseconds. */
private static readonly AGENT_LOG_FLUSH_MS = 2000;
/** Absolute backlog cap — oldest entries are dropped when flushes keep failing. */
private static readonly MAX_AGENT_LOG_BACKLOG = 5_000;
// Test-only: when true, both fusion.db and archive.db open as `:memory:`
// SQLite connections instead of disk-backed files. Production code never
@@ -557,8 +559,14 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
*/
private get db(): Database {
if (!this._db) {
this._db = new Database(this.fusionDir, { inMemory: this.inMemoryDb });
this._db.init();
const db = new Database(this.fusionDir, { inMemory: this.inMemoryDb });
try {
db.init();
} catch (error) {
db.close();
throw error;
}
this._db = db;
// Auto-migrate legacy data if needed
if (detectLegacyData(this.fusionDir)) {
// Note: migrateFromLegacy is async but we need sync access.
@@ -571,8 +579,14 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
private get archiveDb(): ArchiveDatabase {
if (!this._archiveDb) {
this._archiveDb = new ArchiveDatabase(this.fusionDir, { inMemory: this.inMemoryDb });
this._archiveDb.init();
const db = new ArchiveDatabase(this.fusionDir, { inMemory: this.inMemoryDb });
try {
db.init();
} catch (error) {
db.close();
throw error;
}
this._archiveDb = db;
this.migrateLegacyArchiveEntriesToArchiveDb();
}
return this._archiveDb;
@@ -583,8 +597,14 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
// Initialize SQLite database
if (!this._db) {
this._db = new Database(this.fusionDir, { inMemory: this.inMemoryDb });
this._db.init();
const db = new Database(this.fusionDir, { inMemory: this.inMemoryDb });
try {
db.init();
} catch (error) {
db.close();
throw error;
}
this._db = db;
}
// Auto-migrate from legacy file-based storage
@@ -4743,6 +4763,14 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
};
// Buffer the entry for batched insertion to reduce WAL pressure.
// Drop oldest entries if backlog exceeds hard cap (prolonged outage).
if (this.agentLogBuffer.length >= TaskStore.MAX_AGENT_LOG_BACKLOG) {
const dropCount = this.agentLogBuffer.length - TaskStore.MAX_AGENT_LOG_BACKLOG + 1;
this.agentLogBuffer.splice(0, dropCount);
console.warn(
`[fusion] Dropped ${dropCount} buffered agent log entries — backlog cap reached (${this.db.path})`,
);
}
this.agentLogBuffer.push({
taskId,
timestamp,