diff --git a/package.json b/package.json index 265a060..c3f8af1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "agentblame", "displayName": "Agent Blame", "description": "Track AI-generated vs human-written code. Provides git notes storage, CLI, and GitHub PR attribution.", - "version": "0.2.6", + "version": "0.2.10", "private": true, "license": "Apache-2.0", "repository": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 5a5c25c..bec3adc 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@mesadev/agentblame", - "version": "0.2.9", + "version": "0.2.10", "description": "CLI to track AI-generated vs human-written code", "license": "Apache-2.0", "repository": { diff --git a/packages/cli/src/capture.ts b/packages/cli/src/capture.ts index 420ddc8..04a1f55 100644 --- a/packages/cli/src/capture.ts +++ b/packages/cli/src/capture.ts @@ -510,6 +510,11 @@ async function processClaudePayload(payload: ClaudePayload): Promise { } // Save all edits to SQLite database + if (process.env.AGENTBLAME_DEBUG && edits.length === 0) { + console.error(`[agentblame] No edits extracted from ${provider} payload`); + } + for (const edit of edits) { // Find the agentblame directory for this file const agentblameDir = findAgentBlameDir(edit.filePath); if (!agentblameDir) { // File is not in an initialized repo, skip silently + if (process.env.AGENTBLAME_DEBUG) { + console.error(`[agentblame] No agentblame dir found for ${edit.filePath}`); + } continue; } // Set the database directory and save setAgentBlameDir(agentblameDir); - saveEdit(edit); + try { + saveEdit(edit); + if (process.env.AGENTBLAME_DEBUG) { + console.error(`[agentblame] Saved edit for ${edit.filePath}: ${edit.lines.length} lines`); + } + } catch (saveErr) { + // Log database errors even without debug mode since they indicate lost data + console.error(`[agentblame] Failed to save edit for ${edit.filePath}:`, saveErr); + } } process.exit(0); diff --git a/packages/cli/src/lib/database.ts b/packages/cli/src/lib/database.ts index eaa639f..e9b5c20 100644 --- a/packages/cli/src/lib/database.ts +++ b/packages/cli/src/lib/database.ts @@ -156,6 +156,9 @@ export function getDatabase(): Database { // Enable foreign keys and WAL mode for better performance dbInstance.exec("PRAGMA foreign_keys = ON"); dbInstance.exec("PRAGMA journal_mode = WAL"); + // Set busy timeout to handle concurrent writes from async hooks + // Without this, concurrent capture processes get SQLITE_BUSY and fail silently + dbInstance.exec("PRAGMA busy_timeout = 5000"); // Create tables and indexes dbInstance.exec(SCHEMA); @@ -204,7 +207,10 @@ export interface InsertEditParams { } /** - * Insert a new AI edit into the database + * Insert a new AI edit into the database. + * Uses an explicit transaction to ensure atomicity - either all data + * is written (edit + lines) or none. This is especially important + * when running async hooks where the process could be interrupted. */ export function insertEdit(params: InsertEditParams): number { const db = getDatabase(); @@ -217,41 +223,49 @@ export function insertEdit(params: InsertEditParams): number { ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); - const result = editStmt.run( - params.timestamp, - params.provider, - params.filePath, - params.model, - params.content, - params.contentHash, - params.contentHashNormalized, - params.editType, - params.oldContent || null, - params.sessionId || null, - params.toolUseId || null - ); - - const editId = Number(result.lastInsertRowid); - - // Insert lines with line numbers and context const lineStmt = db.prepare(` INSERT INTO lines (edit_id, content, hash, hash_normalized, line_number, context_before, context_after) VALUES (?, ?, ?, ?, ?, ?, ?) `); - for (const line of params.lines) { - lineStmt.run( - editId, - line.content, - line.hash, - line.hashNormalized, - line.lineNumber || null, - line.contextBefore || null, - line.contextAfter || null + // Wrap in transaction for atomicity + db.exec("BEGIN TRANSACTION"); + try { + const result = editStmt.run( + params.timestamp, + params.provider, + params.filePath, + params.model, + params.content, + params.contentHash, + params.contentHashNormalized, + params.editType, + params.oldContent || null, + params.sessionId || null, + params.toolUseId || null ); - } - return editId; + const editId = Number(result.lastInsertRowid); + + // Insert lines with line numbers and context + for (const line of params.lines) { + lineStmt.run( + editId, + line.content, + line.hash, + line.hashNormalized, + line.lineNumber || null, + line.contextBefore || null, + line.contextAfter || null + ); + } + + db.exec("COMMIT"); + return editId; + } catch (err) { + db.exec("ROLLBACK"); + throw err; + } } // ============================================================================= diff --git a/packages/cli/src/lib/hooks.ts b/packages/cli/src/lib/hooks.ts index ea1390a..e51ee2a 100644 --- a/packages/cli/src/lib/hooks.ts +++ b/packages/cli/src/lib/hooks.ts @@ -232,10 +232,10 @@ export async function installClaudeHooks(repoRoot: string): Promise { ) ); - // Add the new hook + // Add the new hook with async: true for non-blocking execution config.hooks.PostToolUse.push({ matcher: "Edit|Write|MultiEdit", - hooks: [{ type: "command", command: hookCommand }], + hooks: [{ type: "command", command: hookCommand, async: true }], }); await fs.promises.writeFile(