feat(FN-4398): complete Step 1 — add retry counter schema columns

Fusion-Task-Id: FN-4398
Fusion-Task-Lineage: 8b898b2e-3468-4fa7-8546-b8f6ba52cf46
This commit is contained in:
Fusion
2026-05-14 14:43:24 -07:00
committed by gsxdsm
parent 990fea9f02
commit 72f921a486
9 changed files with 127 additions and 33 deletions

View File

@@ -717,7 +717,57 @@ describe("schema migration", () => {
{ id: "WS-001", mode: "prompt", gateMode: "advisory" }, { id: "WS-001", mode: "prompt", gateMode: "advisory" },
{ id: "WS-002", mode: "script", gateMode: "advisory" }, { id: "WS-002", mode: "script", gateMode: "advisory" },
]); ]);
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
db.close();
});
it("adds retry-burned task counters when migrating from schema version 77", () => {
const db = new Database(fusionDir);
db.exec("CREATE TABLE IF NOT EXISTS __meta (key TEXT PRIMARY KEY, value TEXT)");
db.exec(`
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
description TEXT NOT NULL,
"column" TEXT NOT NULL,
currentStep INTEGER NOT NULL DEFAULT 0,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
)
`);
db.exec("INSERT INTO __meta (key, value) VALUES ('schemaVersion', '77')");
db.exec("INSERT INTO __meta (key, value) VALUES ('lastModified', '1000')");
db.exec(`
INSERT INTO tasks (
id, description, "column", currentStep, createdAt, updatedAt
) VALUES (
'FN-0001', 'legacy row', 'todo', 0, '2025-01-01T00:00:00.000Z', '2025-01-01T00:00:00.000Z'
)
`);
db.init();
const columns = db
.prepare("PRAGMA table_info(tasks)")
.all() as Array<{ name: string }>;
const names = new Set(columns.map((col) => col.name));
expect(names.has("branchConflictRecoveryCount")).toBe(true);
expect(names.has("reviewerContextRetryCount")).toBe(true);
expect(names.has("reviewerFallbackRetryCount")).toBe(true);
const counts = db
.prepare("SELECT branchConflictRecoveryCount, reviewerContextRetryCount, reviewerFallbackRetryCount FROM tasks WHERE id = ?")
.get("FN-0001") as {
branchConflictRecoveryCount: number;
reviewerContextRetryCount: number;
reviewerFallbackRetryCount: number;
};
expect(counts).toEqual({
branchConflictRecoveryCount: 0,
reviewerContextRetryCount: 0,
reviewerFallbackRetryCount: 0,
});
expect(db.getSchemaVersion()).toBe(78);
db.close(); db.close();
}); });
@@ -752,7 +802,7 @@ describe("schema migration", () => {
{ id: "WS-002", mode: "script", enabled: 1, gateMode: "advisory" }, { id: "WS-002", mode: "script", enabled: 1, gateMode: "advisory" },
{ id: "WS-003", mode: "prompt", enabled: 0, gateMode: "advisory" }, { id: "WS-003", mode: "prompt", enabled: 0, gateMode: "advisory" },
]); ]);
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
db.close(); db.close();
}); });

View File

@@ -290,7 +290,7 @@ describe("Database", () => {
}); });
it("seeds schema version", () => { it("seeds schema version", () => {
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
}); });
it("includes tokenUsageCacheWriteTokens on freshly initialized tasks table", () => { it("includes tokenUsageCacheWriteTokens on freshly initialized tasks table", () => {
@@ -318,7 +318,7 @@ describe("Database", () => {
it("is idempotent - calling init() twice does not fail", () => { it("is idempotent - calling init() twice does not fail", () => {
expect(() => db.init()).not.toThrow(); expect(() => db.init()).not.toThrow();
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
}); });
it("does not overwrite existing config on re-init", () => { it("does not overwrite existing config on re-init", () => {
// Update the config // Update the config
@@ -1383,7 +1383,7 @@ describe("schema migrations", () => {
db.init(); db.init();
// Verify version bumped to 29 (includes v1→v2 through v26→v29) // Verify version bumped to 29 (includes v1→v2 through v26→v29)
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
// Verify new columns exist and existing data is intact // Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>; const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -1408,11 +1408,11 @@ describe("schema migrations", () => {
const db = new Database(fusionDir); const db = new Database(fusionDir);
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
// Re-init should not fail // Re-init should not fail
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
db.close(); db.close();
}); });
@@ -1447,7 +1447,7 @@ describe("schema migrations", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>; const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
expect(cols.map((col) => col.name)).toContain("priority"); expect(cols.map((col) => col.name)).toContain("priority");
@@ -1488,7 +1488,7 @@ describe("schema migrations", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>; const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const colNames = cols.map((col) => col.name); const colNames = cols.map((col) => col.name);
@@ -1560,7 +1560,7 @@ describe("schema migrations", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>; const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const colNames = cols.map((col) => col.name); const colNames = cols.map((col) => col.name);
@@ -1800,7 +1800,7 @@ describe("schema migrations", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
const cols = db.prepare("PRAGMA table_info(chat_messages)").all() as Array<{ name: string }>; const cols = db.prepare("PRAGMA table_info(chat_messages)").all() as Array<{ name: string }>;
expect(cols.map((col) => col.name)).toContain("attachments"); expect(cols.map((col) => col.name)).toContain("attachments");
@@ -1874,7 +1874,7 @@ describe("schema migrations", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>; const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "agentRatings" }]); expect(tables).toEqual([{ name: "agentRatings" }]);
@@ -1898,7 +1898,7 @@ describe("schema migrations", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>; const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "mission_events" }]); expect(tables).toEqual([{ name: "mission_events" }]);
@@ -2002,7 +2002,7 @@ describe("schema migrations", () => {
db.init(); db.init();
// Verify version bumped to 29 // Verify version bumped to 29
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
// Verify new columns exist and existing data is intact // Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>; const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -2221,7 +2221,7 @@ describe("schema migrations", () => {
localDb.init(); localDb.init();
expect(localDb.getSchemaVersion()).toBe(77); expect(localDb.getSchemaVersion()).toBe(78);
const columns = localDb.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>; const columns = localDb.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
expect(columns.map((column) => column.name)).toContain("tokenUsageCacheWriteTokens"); expect(columns.map((column) => column.name)).toContain("tokenUsageCacheWriteTokens");
@@ -2532,7 +2532,7 @@ describe("createDatabase factory", () => {
const db = createDatabase(fusionDir); const db = createDatabase(fusionDir);
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
expect(db.getLastModified()).toBeGreaterThan(0); expect(db.getLastModified()).toBeGreaterThan(0);
db.close(); db.close();
@@ -2688,7 +2688,7 @@ describe("migration v77 task token budget columns", () => {
migrated = new Database(fusion); migrated = new Database(fusion);
migrated.init(); migrated.init();
expect(migrated.getSchemaVersion()).toBe(77); expect(migrated.getSchemaVersion()).toBe(78);
const rows = migrated.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>; const rows = migrated.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const names = new Set(rows.map((row) => row.name)); const names = new Set(rows.map((row) => row.name));
expect(names.has("tokenBudgetSoftAlertedAt")).toBe(true); expect(names.has("tokenBudgetSoftAlertedAt")).toBe(true);
@@ -2734,7 +2734,7 @@ describe("migration v67 drops orphan project auth tables", () => {
migrated = new Database(fusion); migrated = new Database(fusion);
migrated.init(); migrated.init();
expect(migrated.getSchemaVersion()).toBe(77); expect(migrated.getSchemaVersion()).toBe(78);
const tables = migrated const tables = migrated
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'project_auth_%'") .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'project_auth_%'")
.all() as Array<{ name: string }>; .all() as Array<{ name: string }>;
@@ -2761,7 +2761,7 @@ describe("migration v67 drops orphan project auth tables", () => {
try { try {
fresh.init(); fresh.init();
expect(fresh.getSchemaVersion()).toBe(77); expect(fresh.getSchemaVersion()).toBe(78);
const tables = fresh const tables = fresh
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'project_auth_%'") .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'project_auth_%'")
.all() as Array<{ name: string }>; .all() as Array<{ name: string }>;

View File

@@ -886,7 +886,7 @@ describe("Migration: pre-33 DB upgrade", () => {
// Step 1: Create a fresh database at v33 (runs all migrations up to 33) // Step 1: Create a fresh database at v33 (runs all migrations up to 33)
const db1 = createDatabase(legacyDir); const db1 = createDatabase(legacyDir);
db1.init(); db1.init();
expect(db1.getSchemaVersion()).toBe(77); expect(db1.getSchemaVersion()).toBe(78);
db1.close(); db1.close();
// Step 2: Manually downgrade to version 32 and drop insight tables // Step 2: Manually downgrade to version 32 and drop insight tables
@@ -921,7 +921,7 @@ describe("Migration: pre-33 DB upgrade", () => {
expect(tableNamesBefore).not.toContain("project_insight_runs"); expect(tableNamesBefore).not.toContain("project_insight_runs");
// Now run init — this triggers the v32→v33 migration // Now run init — this triggers the v32→v33 migration
db3.init(); db3.init();
expect(db3.getSchemaVersion()).toBe(77); expect(db3.getSchemaVersion()).toBe(78);
// Step 4: Verify insight tables exist after migration // Step 4: Verify insight tables exist after migration
const tablesAfter = db3.prepare( const tablesAfter = db3.prepare(
@@ -952,12 +952,12 @@ describe("Migration: pre-33 DB upgrade", () => {
try { try {
const db1 = createDatabase(testDir); const db1 = createDatabase(testDir);
db1.init(); db1.init();
expect(db1.getSchemaVersion()).toBe(77); expect(db1.getSchemaVersion()).toBe(78);
db1.close(); db1.close();
const db2 = createDatabase(testDir); const db2 = createDatabase(testDir);
expect(() => db2.init()).not.toThrow(); expect(() => db2.init()).not.toThrow();
expect(db2.getSchemaVersion()).toBe(77); expect(db2.getSchemaVersion()).toBe(78);
db2.close(); db2.close();
} finally { } finally {
rmSync(testDir, { recursive: true, force: true }); rmSync(testDir, { recursive: true, force: true });
@@ -971,7 +971,7 @@ describe("Migration: pre-33 DB upgrade", () => {
// Step 1: Create a fresh DB and run migrations // Step 1: Create a fresh DB and run migrations
const db1 = createDatabase(compatDir); const db1 = createDatabase(compatDir);
db1.init(); db1.init();
expect(db1.getSchemaVersion()).toBe(77); expect(db1.getSchemaVersion()).toBe(78);
// Step 2: Strip lifecycle and cancelledAt columns by recreating the // Step 2: Strip lifecycle and cancelledAt columns by recreating the
// table without them. This simulates a DB that was created before the // table without them. This simulates a DB that was created before the

View File

@@ -2639,7 +2639,7 @@ describe("MissionStore", () => {
describe("Loop State & Validator Run Schema (v31)", () => { describe("Loop State & Validator Run Schema (v31)", () => {
it("schema version is 40 after migration", () => { it("schema version is 40 after migration", () => {
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
}); });
it("mission_features table has loop state columns", () => { it("mission_features table has loop state columns", () => {

View File

@@ -584,7 +584,7 @@ describe("Run Audit", () => {
}); });
it("schema version is bumped to 40", () => { it("schema version is bumped to 40", () => {
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
}); });
}); });
}); });

View File

@@ -51,7 +51,7 @@ describe("TaskStore task documents", () => {
expect(tableNames.has("task_documents")).toBe(true); expect(tableNames.has("task_documents")).toBe(true);
expect(tableNames.has("task_document_revisions")).toBe(true); expect(tableNames.has("task_document_revisions")).toBe(true);
expect(db.getSchemaVersion()).toBe(77); expect(db.getSchemaVersion()).toBe(78);
const index = db const index = db
.prepare( .prepare(

View File

@@ -119,7 +119,7 @@ export function probeFts5(db: DatabaseSync): boolean {
// ── Schema Definition ──────────────────────────────────────────────── // ── Schema Definition ────────────────────────────────────────────────
const SCHEMA_VERSION = 77; const SCHEMA_VERSION = 78;
function normalizeTaskComments( function normalizeTaskComments(
steeringComments: SteeringComment[] | undefined, steeringComments: SteeringComment[] | undefined,
@@ -3276,6 +3276,14 @@ export class Database {
}); });
} }
if (version < 78) {
this.applyMigration(78, () => {
this.addColumnIfMissing("tasks", "branchConflictRecoveryCount", "INTEGER DEFAULT 0");
this.addColumnIfMissing("tasks", "reviewerContextRetryCount", "INTEGER DEFAULT 0");
this.addColumnIfMissing("tasks", "reviewerFallbackRetryCount", "INTEGER DEFAULT 0");
});
}
} }
/** /**

View File

@@ -81,6 +81,9 @@ interface TaskRow {
verificationFailureCount: number | null; verificationFailureCount: number | null;
mergeConflictBounceCount: number | null; mergeConflictBounceCount: number | null;
mergeAuditBounceCount: number | null; mergeAuditBounceCount: number | null;
branchConflictRecoveryCount: number | null;
reviewerContextRetryCount: number | null;
reviewerFallbackRetryCount: number | null;
nextRecoveryAt: string | null; nextRecoveryAt: string | null;
error: string | null; error: string | null;
summary: string | null; summary: string | null;
@@ -1008,6 +1011,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
verificationFailureCount: row.verificationFailureCount ?? undefined, verificationFailureCount: row.verificationFailureCount ?? undefined,
mergeConflictBounceCount: row.mergeConflictBounceCount ?? undefined, mergeConflictBounceCount: row.mergeConflictBounceCount ?? undefined,
mergeAuditBounceCount: row.mergeAuditBounceCount ?? undefined, mergeAuditBounceCount: row.mergeAuditBounceCount ?? undefined,
branchConflictRecoveryCount: row.branchConflictRecoveryCount ?? undefined,
reviewerContextRetryCount: row.reviewerContextRetryCount ?? undefined,
reviewerFallbackRetryCount: row.reviewerFallbackRetryCount ?? undefined,
nextRecoveryAt: row.nextRecoveryAt || undefined, nextRecoveryAt: row.nextRecoveryAt || undefined,
error: row.error || undefined, error: row.error || undefined,
summary: row.summary || undefined, summary: row.summary || undefined,
@@ -1347,7 +1353,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
"modelPresetId", "modelProvider", "modelId", "modelPresetId", "modelProvider", "modelId",
"validatorModelProvider", "validatorModelId", "validatorModelProvider", "validatorModelId",
"planningModelProvider", "planningModelId", "planningModelProvider", "planningModelId",
"mergeRetries", "workflowStepRetries", "stuckKillCount", "postReviewFixCount", "recoveryRetryCount", "taskDoneRetryCount", "verificationFailureCount", "mergeConflictBounceCount", "mergeAuditBounceCount", "nextRecoveryAt", "mergeRetries", "workflowStepRetries", "stuckKillCount", "postReviewFixCount", "recoveryRetryCount", "taskDoneRetryCount", "verificationFailureCount", "mergeConflictBounceCount", "mergeAuditBounceCount", "branchConflictRecoveryCount", "reviewerContextRetryCount", "reviewerFallbackRetryCount", "nextRecoveryAt",
"error", "summary", "thinkingLevel", "executionMode", "error", "summary", "thinkingLevel", "executionMode",
"tokenUsageInputTokens", "tokenUsageOutputTokens", "tokenUsageCachedTokens", "tokenUsageCacheWriteTokens", "tokenUsageTotalTokens", "tokenUsageFirstUsedAt", "tokenUsageLastUsedAt", "tokenBudgetSoftAlertedAt", "tokenBudgetHardAlertedAt", "tokenBudgetOverride", "tokenUsageInputTokens", "tokenUsageOutputTokens", "tokenUsageCachedTokens", "tokenUsageCacheWriteTokens", "tokenUsageTotalTokens", "tokenUsageFirstUsedAt", "tokenUsageLastUsedAt", "tokenBudgetSoftAlertedAt", "tokenBudgetHardAlertedAt", "tokenBudgetOverride",
"createdAt", "updatedAt", "columnMovedAt", "executionStartedAt", "executionCompletedAt", "createdAt", "updatedAt", "columnMovedAt", "executionStartedAt", "executionCompletedAt",
@@ -1396,7 +1402,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
"modelPresetId", "modelProvider", "modelId", "modelPresetId", "modelProvider", "modelId",
"validatorModelProvider", "validatorModelId", "validatorModelProvider", "validatorModelId",
"planningModelProvider", "planningModelId", "planningModelProvider", "planningModelId",
"mergeRetries", "workflowStepRetries", "stuckKillCount", "postReviewFixCount", "recoveryRetryCount", "taskDoneRetryCount", "verificationFailureCount", "mergeConflictBounceCount", "mergeAuditBounceCount", "nextRecoveryAt", "mergeRetries", "workflowStepRetries", "stuckKillCount", "postReviewFixCount", "recoveryRetryCount", "taskDoneRetryCount", "verificationFailureCount", "mergeConflictBounceCount", "mergeAuditBounceCount", "branchConflictRecoveryCount", "reviewerContextRetryCount", "reviewerFallbackRetryCount", "nextRecoveryAt",
"error", "summary", "thinkingLevel", "executionMode", "error", "summary", "thinkingLevel", "executionMode",
"tokenUsageInputTokens", "tokenUsageOutputTokens", "tokenUsageCachedTokens", "tokenUsageCacheWriteTokens", "tokenUsageTotalTokens", "tokenUsageFirstUsedAt", "tokenUsageLastUsedAt", "tokenBudgetSoftAlertedAt", "tokenBudgetHardAlertedAt", "tokenBudgetOverride", "tokenUsageInputTokens", "tokenUsageOutputTokens", "tokenUsageCachedTokens", "tokenUsageCacheWriteTokens", "tokenUsageTotalTokens", "tokenUsageFirstUsedAt", "tokenUsageLastUsedAt", "tokenBudgetSoftAlertedAt", "tokenBudgetHardAlertedAt", "tokenBudgetOverride",
"createdAt", "updatedAt", "columnMovedAt", "executionStartedAt", "executionCompletedAt", "createdAt", "updatedAt", "columnMovedAt", "executionStartedAt", "executionCompletedAt",
@@ -1466,6 +1472,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
task.verificationFailureCount ?? 0, task.verificationFailureCount ?? 0,
task.mergeConflictBounceCount ?? 0, task.mergeConflictBounceCount ?? 0,
task.mergeAuditBounceCount ?? 0, task.mergeAuditBounceCount ?? 0,
task.branchConflictRecoveryCount ?? 0,
task.reviewerContextRetryCount ?? 0,
task.reviewerFallbackRetryCount ?? 0,
task.nextRecoveryAt ?? null, task.nextRecoveryAt ?? null,
task.error ?? null, task.error ?? null,
task.summary ?? null, task.summary ?? null,
@@ -1546,7 +1555,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
id, lineageId, title, description, priority, "column", status, size, reviewLevel, currentStep, id, lineageId, title, description, priority, "column", status, size, reviewLevel, currentStep,
worktree, blockedBy, paused, userPaused, baseBranch, branch, executionStartBranch, baseCommitSha, modelPresetId, modelProvider, worktree, blockedBy, paused, userPaused, baseBranch, branch, executionStartBranch, baseCommitSha, modelPresetId, modelProvider,
modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries, modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries,
workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, mergeConflictBounceCount, mergeAuditBounceCount, nextRecoveryAt, error, workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, mergeConflictBounceCount, mergeAuditBounceCount, branchConflictRecoveryCount, reviewerContextRetryCount, reviewerFallbackRetryCount, nextRecoveryAt, error,
summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens, summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
tokenUsageCacheWriteTokens, tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, tokenBudgetSoftAlertedAt, tokenBudgetHardAlertedAt, tokenBudgetOverride, createdAt, updatedAt, columnMovedAt, tokenUsageCacheWriteTokens, tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, tokenBudgetSoftAlertedAt, tokenBudgetHardAlertedAt, tokenBudgetOverride, createdAt, updatedAt, columnMovedAt,
executionStartedAt, executionCompletedAt, executionStartedAt, executionCompletedAt,
@@ -1571,7 +1580,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
id, lineageId, title, description, priority, "column", status, size, reviewLevel, currentStep, id, lineageId, title, description, priority, "column", status, size, reviewLevel, currentStep,
worktree, blockedBy, paused, userPaused, baseBranch, branch, executionStartBranch, baseCommitSha, modelPresetId, modelProvider, worktree, blockedBy, paused, userPaused, baseBranch, branch, executionStartBranch, baseCommitSha, modelPresetId, modelProvider,
modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries, modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries,
workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, mergeConflictBounceCount, mergeAuditBounceCount, nextRecoveryAt, error, workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, mergeConflictBounceCount, mergeAuditBounceCount, branchConflictRecoveryCount, reviewerContextRetryCount, reviewerFallbackRetryCount, nextRecoveryAt, error,
summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens, summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
tokenUsageCacheWriteTokens, tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, tokenBudgetSoftAlertedAt, tokenBudgetHardAlertedAt, tokenBudgetOverride, createdAt, updatedAt, columnMovedAt, tokenUsageCacheWriteTokens, tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, tokenBudgetSoftAlertedAt, tokenBudgetHardAlertedAt, tokenBudgetOverride, createdAt, updatedAt, columnMovedAt,
executionStartedAt, executionCompletedAt, executionStartedAt, executionCompletedAt,
@@ -1614,6 +1623,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
verificationFailureCount = excluded.verificationFailureCount, verificationFailureCount = excluded.verificationFailureCount,
mergeConflictBounceCount = excluded.mergeConflictBounceCount, mergeConflictBounceCount = excluded.mergeConflictBounceCount,
mergeAuditBounceCount = excluded.mergeAuditBounceCount, mergeAuditBounceCount = excluded.mergeAuditBounceCount,
branchConflictRecoveryCount = excluded.branchConflictRecoveryCount,
reviewerContextRetryCount = excluded.reviewerContextRetryCount,
reviewerFallbackRetryCount = excluded.reviewerFallbackRetryCount,
nextRecoveryAt = excluded.nextRecoveryAt, nextRecoveryAt = excluded.nextRecoveryAt,
error = excluded.error, error = excluded.error,
summary = excluded.summary, summary = excluded.summary,
@@ -3928,7 +3940,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
async updateTask( async updateTask(
id: string, id: string,
updates: { title?: string; description?: string; priority?: TaskPriority | null; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; currentStep?: number; blockedBy?: string | null; assignedAgentId?: string | null; pausedByAgentId?: string | null; pausedReason?: string | null; tokenBudgetSoftAlertedAt?: string | null; tokenBudgetHardAlertedAt?: string | null; tokenBudgetOverride?: import("./types.js").TaskTokenBudgetOverride | null; dispatchStormCount?: number | null; lastDispatchAt?: string | null; assigneeUserId?: string | null; scopeOverride?: boolean | null; scopeOverrideReason?: string | null; nodeId?: string | null; effectiveNodeId?: string | null; effectiveNodeSource?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; checkoutNodeId?: string | null; checkoutRunId?: string | null; checkoutLeaseRenewedAt?: string | null; checkoutLeaseEpoch?: number | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; executionStartBranch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("./types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; postReviewFixCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; verificationFailureCount?: number | null; mergeConflictBounceCount?: number | null; mergeAuditBounceCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; noCommitsExpected?: boolean | null; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; executionStartedAt?: string | null; executionCompletedAt?: string | null; review?: import("./types.js").TaskReview | null; reviewState?: import("./types.js").TaskReviewState | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; sourceIssue?: import("./types.js").TaskSourceIssue | null; githubTracking?: import("./types.js").TaskGithubTracking | null; tokenUsage?: import("./types.js").TaskTokenUsage | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null }, updates: { title?: string; description?: string; priority?: TaskPriority | null; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; currentStep?: number; blockedBy?: string | null; assignedAgentId?: string | null; pausedByAgentId?: string | null; pausedReason?: string | null; tokenBudgetSoftAlertedAt?: string | null; tokenBudgetHardAlertedAt?: string | null; tokenBudgetOverride?: import("./types.js").TaskTokenBudgetOverride | null; dispatchStormCount?: number | null; lastDispatchAt?: string | null; assigneeUserId?: string | null; scopeOverride?: boolean | null; scopeOverrideReason?: string | null; nodeId?: string | null; effectiveNodeId?: string | null; effectiveNodeSource?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; checkoutNodeId?: string | null; checkoutRunId?: string | null; checkoutLeaseRenewedAt?: string | null; checkoutLeaseEpoch?: number | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; executionStartBranch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("./types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; postReviewFixCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; verificationFailureCount?: number | null; mergeConflictBounceCount?: number | null; mergeAuditBounceCount?: number | null; branchConflictRecoveryCount?: number | null; reviewerContextRetryCount?: number | null; reviewerFallbackRetryCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; noCommitsExpected?: boolean | null; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; executionStartedAt?: string | null; executionCompletedAt?: string | null; review?: import("./types.js").TaskReview | null; reviewState?: import("./types.js").TaskReviewState | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; sourceIssue?: import("./types.js").TaskSourceIssue | null; githubTracking?: import("./types.js").TaskGithubTracking | null; tokenUsage?: import("./types.js").TaskTokenUsage | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
runContext?: RunMutationContext, runContext?: RunMutationContext,
): Promise<Task> { ): Promise<Task> {
return this.withTaskLock(id, async () => { return this.withTaskLock(id, async () => {
@@ -4209,6 +4221,21 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
} else if (updates.mergeAuditBounceCount !== undefined) { } else if (updates.mergeAuditBounceCount !== undefined) {
task.mergeAuditBounceCount = updates.mergeAuditBounceCount; task.mergeAuditBounceCount = updates.mergeAuditBounceCount;
} }
if (updates.branchConflictRecoveryCount === null) {
task.branchConflictRecoveryCount = undefined;
} else if (updates.branchConflictRecoveryCount !== undefined) {
task.branchConflictRecoveryCount = updates.branchConflictRecoveryCount;
}
if (updates.reviewerContextRetryCount === null) {
task.reviewerContextRetryCount = undefined;
} else if (updates.reviewerContextRetryCount !== undefined) {
task.reviewerContextRetryCount = updates.reviewerContextRetryCount;
}
if (updates.reviewerFallbackRetryCount === null) {
task.reviewerFallbackRetryCount = undefined;
} else if (updates.reviewerFallbackRetryCount !== undefined) {
task.reviewerFallbackRetryCount = updates.reviewerFallbackRetryCount;
}
if (updates.nextRecoveryAt === null) { if (updates.nextRecoveryAt === null) {
task.nextRecoveryAt = undefined; task.nextRecoveryAt = undefined;
} else if (updates.nextRecoveryAt !== undefined) { } else if (updates.nextRecoveryAt !== undefined) {

View File

@@ -1417,6 +1417,15 @@ export interface Task {
* `MAX_MERGE_AUDIT_BOUNCES`, the task is parked with `status="failed"` and a * `MAX_MERGE_AUDIT_BOUNCES`, the task is parked with `status="failed"` and a
* recovery follow-up task is created. */ * recovery follow-up task is created. */
mergeAuditBounceCount?: number; mergeAuditBounceCount?: number;
/** Number of branch-conflict recovery attempts consumed by executor branch
* conflict auto-recovery loops. Incremented once per recovery retry attempt. */
branchConflictRecoveryCount?: number;
/** Number of reviewer context-limit retries consumed by FN-4082 compact
* reviewer-request fallback handling. */
reviewerContextRetryCount?: number;
/** Number of reviewer fallback retries consumed by FN-4092 fallback-model
* and same-model strict-prompt retry paths. */
reviewerFallbackRetryCount?: number;
/** ISO-8601 timestamp indicating when the task becomes eligible for the next /** ISO-8601 timestamp indicating when the task becomes eligible for the next
* recovery retry. Scheduler and triage processor skip tasks whose * recovery retry. Scheduler and triage processor skip tasks whose
* `nextRecoveryAt` is still in the future. Cleared alongside `recoveryRetryCount`. */ * `nextRecoveryAt` is still in the future. Cleared alongside `recoveryRetryCount`. */