feat(FN-4396): complete Step 3 — migrate and persist token budget fields

Fusion-Task-Id: FN-4396
Fusion-Task-Lineage: 3839210b-b443-410d-a476-aa81128c739a
This commit is contained in:
Fusion
2026-05-14 07:42:39 -07:00
committed by gsxdsm
parent b233cb391b
commit c857bea62c
9 changed files with 159 additions and 40 deletions

View File

@@ -717,7 +717,7 @@ describe("schema migration", () => {
{ id: "WS-001", mode: "prompt", gateMode: "advisory" }, { id: "WS-001", mode: "prompt", gateMode: "advisory" },
{ id: "WS-002", mode: "script", gateMode: "gate" }, { id: "WS-002", mode: "script", gateMode: "gate" },
]); ]);
expect(db.getSchemaVersion()).toBe(76); expect(db.getSchemaVersion()).toBe(77);
db.close(); db.close();
}); });

View File

@@ -290,7 +290,7 @@ describe("Database", () => {
}); });
it("seeds schema version", () => { it("seeds schema version", () => {
expect(db.getSchemaVersion()).toBe(76); expect(db.getSchemaVersion()).toBe(77);
}); });
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(76); expect(db.getSchemaVersion()).toBe(77);
}); });
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(76); expect(db.getSchemaVersion()).toBe(77);
// 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(76); expect(db.getSchemaVersion()).toBe(77);
// Re-init should not fail // Re-init should not fail
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(76); expect(db.getSchemaVersion()).toBe(77);
db.close(); db.close();
}); });
@@ -1447,7 +1447,7 @@ describe("schema migrations", () => {
db.init(); db.init();
expect(db.getSchemaVersion()).toBe(76); expect(db.getSchemaVersion()).toBe(77);
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(76); expect(db.getSchemaVersion()).toBe(77);
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(76); expect(db.getSchemaVersion()).toBe(77);
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(76); expect(db.getSchemaVersion()).toBe(77);
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(76); expect(db.getSchemaVersion()).toBe(77);
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(76); expect(db.getSchemaVersion()).toBe(77);
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(76); expect(db.getSchemaVersion()).toBe(77);
// 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(76); expect(localDb.getSchemaVersion()).toBe(77);
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(76); expect(db.getSchemaVersion()).toBe(77);
expect(db.getLastModified()).toBeGreaterThan(0); expect(db.getLastModified()).toBeGreaterThan(0);
db.close(); db.close();
@@ -2646,6 +2646,72 @@ describe("TaskStore — verification cache", () => {
}); });
}); });
describe("migration v77 task token budget columns", () => {
it("includes task token budget columns on fresh init", () => {
const temp = makeTmpDir();
const fusion = join(temp, ".fusion");
const fresh = new Database(fusion);
try {
fresh.init();
const rows = fresh
.prepare("PRAGMA table_info(tasks)")
.all() as Array<{ name: string }>;
const names = new Set(rows.map((row) => row.name));
expect(names.has("tokenBudgetSoftAlertedAt")).toBe(true);
expect(names.has("tokenBudgetHardAlertedAt")).toBe(true);
expect(names.has("tokenBudgetOverride")).toBe(true);
} finally {
try {
fresh.close();
} catch {
// already closed
}
removeTrackedTmpDirSync(temp);
}
});
it("adds task token budget columns during migration without dropping existing rows", () => {
const temp = makeTmpDir();
const fusion = join(temp, ".fusion");
const localDb = new Database(fusion);
let migrated: Database | undefined;
try {
localDb.init();
localDb.prepare("INSERT INTO tasks (id, description, \"column\", createdAt, updatedAt) VALUES (?, ?, ?, ?, ?)")
.run("FN-MIGRATE", "migration row", "todo", "2026-01-01T00:00:00.000Z", "2026-01-01T00:00:00.000Z");
localDb.prepare("UPDATE __meta SET value = '76' WHERE key = 'schemaVersion'").run();
localDb.exec("ALTER TABLE tasks DROP COLUMN tokenBudgetSoftAlertedAt");
localDb.exec("ALTER TABLE tasks DROP COLUMN tokenBudgetHardAlertedAt");
localDb.exec("ALTER TABLE tasks DROP COLUMN tokenBudgetOverride");
localDb.close();
migrated = new Database(fusion);
migrated.init();
expect(migrated.getSchemaVersion()).toBe(77);
const rows = migrated.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
const names = new Set(rows.map((row) => row.name));
expect(names.has("tokenBudgetSoftAlertedAt")).toBe(true);
expect(names.has("tokenBudgetHardAlertedAt")).toBe(true);
expect(names.has("tokenBudgetOverride")).toBe(true);
const task = migrated.prepare("SELECT id FROM tasks WHERE id = ?").get("FN-MIGRATE") as { id: string } | undefined;
expect(task?.id).toBe("FN-MIGRATE");
} finally {
try {
migrated?.close();
} catch {
// already closed
}
try {
localDb.close();
} catch {
// already closed
}
removeTrackedTmpDirSync(temp);
}
});
});
describe("migration v67 drops orphan project auth tables", () => { describe("migration v67 drops orphan project auth tables", () => {
it("drops project_auth_* tables left over from the removed pluggable auth feature", () => { it("drops project_auth_* tables left over from the removed pluggable auth feature", () => {
const temp = makeTmpDir(); const temp = makeTmpDir();
@@ -2668,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(76); expect(migrated.getSchemaVersion()).toBe(77);
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 }>;
@@ -2695,7 +2761,7 @@ describe("migration v67 drops orphan project auth tables", () => {
try { try {
fresh.init(); fresh.init();
expect(fresh.getSchemaVersion()).toBe(76); expect(fresh.getSchemaVersion()).toBe(77);
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(76); expect(db1.getSchemaVersion()).toBe(77);
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(76); expect(db3.getSchemaVersion()).toBe(77);
// 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(76); expect(db1.getSchemaVersion()).toBe(77);
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(76); expect(db2.getSchemaVersion()).toBe(77);
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(76); expect(db1.getSchemaVersion()).toBe(77);
// 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(76); expect(db.getSchemaVersion()).toBe(77);
}); });
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(76); expect(db.getSchemaVersion()).toBe(77);
}); });
}); });
}); });

View File

@@ -141,6 +141,39 @@ describe("TaskStore", () => {
expect(detail.tokenUsage).toEqual(tokenUsage); expect(detail.tokenUsage).toEqual(tokenUsage);
}); });
it("round-trips token budget alert sentinels and overrides", async () => {
const created = await harness.store().createTask({
description: "Token budget fields",
});
(harness.store() as any).db.prepare(`
UPDATE tasks
SET tokenBudgetSoftAlertedAt = ?,
tokenBudgetHardAlertedAt = ?,
tokenBudgetOverride = ?
WHERE id = ?
`).run(
"2026-05-14T01:00:00.000Z",
"2026-05-14T01:05:00.000Z",
JSON.stringify({
soft: 1_000_000,
hard: 2_000_000,
raisedAt: "2026-05-14T01:06:00.000Z",
reason: "manual override",
}),
created.id,
);
const reloaded = await harness.store().getTask(created.id);
expect(reloaded.tokenBudgetSoftAlertedAt).toBe("2026-05-14T01:00:00.000Z");
expect(reloaded.tokenBudgetHardAlertedAt).toBe("2026-05-14T01:05:00.000Z");
expect(reloaded.tokenBudgetOverride).toEqual({
soft: 1_000_000,
hard: 2_000_000,
raisedAt: "2026-05-14T01:06:00.000Z",
reason: "manual override",
});
});
it("clears token usage via null update and keeps it absent after reload", async () => { it("clears token usage via null update and keeps it absent after reload", async () => {
// Cross-instance persistence test — see counterpart above. // Cross-instance persistence test — see counterpart above.
harness.store().close(); harness.store().close();

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(76); expect(db.getSchemaVersion()).toBe(77);
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 = 76; const SCHEMA_VERSION = 77;
function normalizeTaskComments( function normalizeTaskComments(
steeringComments: SteeringComment[] | undefined, steeringComments: SteeringComment[] | undefined,
@@ -224,6 +224,9 @@ CREATE TABLE IF NOT EXISTS tasks (
tokenUsageTotalTokens INTEGER, tokenUsageTotalTokens INTEGER,
tokenUsageFirstUsedAt TEXT, tokenUsageFirstUsedAt TEXT,
tokenUsageLastUsedAt TEXT, tokenUsageLastUsedAt TEXT,
tokenBudgetSoftAlertedAt TEXT,
tokenBudgetHardAlertedAt TEXT,
tokenBudgetOverride TEXT,
createdAt TEXT NOT NULL, createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL, updatedAt TEXT NOT NULL,
columnMovedAt TEXT, columnMovedAt TEXT,
@@ -323,7 +326,6 @@ CREATE TABLE IF NOT EXISTS workflow_steps (
description TEXT NOT NULL, description TEXT NOT NULL,
mode TEXT NOT NULL DEFAULT 'prompt', mode TEXT NOT NULL DEFAULT 'prompt',
phase TEXT NOT NULL DEFAULT 'pre-merge', phase TEXT NOT NULL DEFAULT 'pre-merge',
gateMode TEXT NOT NULL DEFAULT 'gate',
prompt TEXT NOT NULL DEFAULT '', prompt TEXT NOT NULL DEFAULT '',
gateMode TEXT NOT NULL DEFAULT 'advisory', gateMode TEXT NOT NULL DEFAULT 'advisory',
toolMode TEXT, toolMode TEXT,
@@ -1770,7 +1772,6 @@ export class Database {
description TEXT NOT NULL, description TEXT NOT NULL,
mode TEXT NOT NULL DEFAULT 'prompt', mode TEXT NOT NULL DEFAULT 'prompt',
phase TEXT NOT NULL DEFAULT 'pre-merge', phase TEXT NOT NULL DEFAULT 'pre-merge',
gateMode TEXT NOT NULL DEFAULT 'gate',
prompt TEXT NOT NULL DEFAULT '', prompt TEXT NOT NULL DEFAULT '',
gateMode TEXT NOT NULL DEFAULT 'advisory', gateMode TEXT NOT NULL DEFAULT 'advisory',
toolMode TEXT, toolMode TEXT,
@@ -1801,7 +1802,6 @@ export class Database {
description, description,
mode, mode,
phase, phase,
gateMode,
prompt, prompt,
gateMode, gateMode,
toolMode, toolMode,
@@ -3262,6 +3262,14 @@ export class Database {
}); });
} }
if (version < 77) {
this.applyMigration(77, () => {
this.addColumnIfMissing("tasks", "tokenBudgetSoftAlertedAt", "TEXT");
this.addColumnIfMissing("tasks", "tokenBudgetHardAlertedAt", "TEXT");
this.addColumnIfMissing("tasks", "tokenBudgetOverride", "TEXT");
});
}
} }
/** /**

View File

@@ -92,6 +92,9 @@ interface TaskRow {
tokenUsageTotalTokens: number | null; tokenUsageTotalTokens: number | null;
tokenUsageFirstUsedAt: string | null; tokenUsageFirstUsedAt: string | null;
tokenUsageLastUsedAt: string | null; tokenUsageLastUsedAt: string | null;
tokenBudgetSoftAlertedAt: string | null;
tokenBudgetHardAlertedAt: string | null;
tokenBudgetOverride: string | null;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
columnMovedAt: string | null; columnMovedAt: string | null;
@@ -1015,6 +1018,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
dependencies: fromJson<string[]>(row.dependencies) || [], dependencies: fromJson<string[]>(row.dependencies) || [],
steps: fromJson<import("./types.js").TaskStep[]>(row.steps) || [], steps: fromJson<import("./types.js").TaskStep[]>(row.steps) || [],
log: fromJson<import("./types.js").TaskLogEntry[]>(row.log) || [], log: fromJson<import("./types.js").TaskLogEntry[]>(row.log) || [],
tokenBudgetSoftAlertedAt: row.tokenBudgetSoftAlertedAt || undefined,
tokenBudgetHardAlertedAt: row.tokenBudgetHardAlertedAt || undefined,
tokenBudgetOverride: fromJson<import("./types.js").TaskTokenBudgetOverride>(row.tokenBudgetOverride) ?? undefined,
tokenUsage: (() => { tokenUsage: (() => {
if ( if (
row.tokenUsageInputTokens === null row.tokenUsageInputTokens === null
@@ -1337,7 +1343,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
"planningModelProvider", "planningModelId", "planningModelProvider", "planningModelId",
"mergeRetries", "workflowStepRetries", "stuckKillCount", "postReviewFixCount", "recoveryRetryCount", "taskDoneRetryCount", "verificationFailureCount", "mergeConflictBounceCount", "mergeAuditBounceCount", "nextRecoveryAt", "mergeRetries", "workflowStepRetries", "stuckKillCount", "postReviewFixCount", "recoveryRetryCount", "taskDoneRetryCount", "verificationFailureCount", "mergeConflictBounceCount", "mergeAuditBounceCount", "nextRecoveryAt",
"error", "summary", "thinkingLevel", "executionMode", "error", "summary", "thinkingLevel", "executionMode",
"tokenUsageInputTokens", "tokenUsageOutputTokens", "tokenUsageCachedTokens", "tokenUsageCacheWriteTokens", "tokenUsageTotalTokens", "tokenUsageFirstUsedAt", "tokenUsageLastUsedAt", "tokenUsageInputTokens", "tokenUsageOutputTokens", "tokenUsageCachedTokens", "tokenUsageCacheWriteTokens", "tokenUsageTotalTokens", "tokenUsageFirstUsedAt", "tokenUsageLastUsedAt", "tokenBudgetSoftAlertedAt", "tokenBudgetHardAlertedAt", "tokenBudgetOverride",
"createdAt", "updatedAt", "columnMovedAt", "executionStartedAt", "executionCompletedAt", "createdAt", "updatedAt", "columnMovedAt", "executionStartedAt", "executionCompletedAt",
"dependencies", "steps", "comments", "review", "reviewState", "workflowStepResults", "steeringComments", "dependencies", "steps", "comments", "review", "reviewState", "workflowStepResults", "steeringComments",
"attachments", "prInfo", "issueInfo", "githubTracking", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails", "attachments", "prInfo", "issueInfo", "githubTracking", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails",
@@ -1386,7 +1392,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
"planningModelProvider", "planningModelId", "planningModelProvider", "planningModelId",
"mergeRetries", "workflowStepRetries", "stuckKillCount", "postReviewFixCount", "recoveryRetryCount", "taskDoneRetryCount", "verificationFailureCount", "mergeConflictBounceCount", "mergeAuditBounceCount", "nextRecoveryAt", "mergeRetries", "workflowStepRetries", "stuckKillCount", "postReviewFixCount", "recoveryRetryCount", "taskDoneRetryCount", "verificationFailureCount", "mergeConflictBounceCount", "mergeAuditBounceCount", "nextRecoveryAt",
"error", "summary", "thinkingLevel", "executionMode", "error", "summary", "thinkingLevel", "executionMode",
"tokenUsageInputTokens", "tokenUsageOutputTokens", "tokenUsageCachedTokens", "tokenUsageCacheWriteTokens", "tokenUsageTotalTokens", "tokenUsageFirstUsedAt", "tokenUsageLastUsedAt", "tokenUsageInputTokens", "tokenUsageOutputTokens", "tokenUsageCachedTokens", "tokenUsageCacheWriteTokens", "tokenUsageTotalTokens", "tokenUsageFirstUsedAt", "tokenUsageLastUsedAt", "tokenBudgetSoftAlertedAt", "tokenBudgetHardAlertedAt", "tokenBudgetOverride",
"createdAt", "updatedAt", "columnMovedAt", "executionStartedAt", "executionCompletedAt", "createdAt", "updatedAt", "columnMovedAt", "executionStartedAt", "executionCompletedAt",
"dependencies", "steps", "attachments", "steeringComments", "dependencies", "steps", "attachments", "steeringComments",
"comments", "review", "reviewState", "workflowStepResults", "prInfo", "issueInfo", "githubTracking", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails", "comments", "review", "reviewState", "workflowStepResults", "prInfo", "issueInfo", "githubTracking", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails",
@@ -1465,6 +1471,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
task.tokenUsage?.totalTokens ?? null, task.tokenUsage?.totalTokens ?? null,
task.tokenUsage?.firstUsedAt ?? null, task.tokenUsage?.firstUsedAt ?? null,
task.tokenUsage?.lastUsedAt ?? null, task.tokenUsage?.lastUsedAt ?? null,
task.tokenBudgetSoftAlertedAt ?? null,
task.tokenBudgetHardAlertedAt ?? null,
toJsonNullable(task.tokenBudgetOverride),
task.createdAt, task.createdAt,
task.updatedAt, task.updatedAt,
task.columnMovedAt ?? null, task.columnMovedAt ?? null,
@@ -1522,6 +1531,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
* duplicate IDs instead of silently rewriting the existing row. * duplicate IDs instead of silently rewriting the existing row.
*/ */
private insertTask(task: Task): void { private insertTask(task: Task): void {
const values = this.getTaskPersistValues(task);
const placeholders = values.map(() => "?").join(", ");
this.db.prepare(` this.db.prepare(`
INSERT INTO tasks ( INSERT INTO tasks (
id, lineageId, title, description, priority, "column", status, size, reviewLevel, currentStep, id, lineageId, title, description, priority, "column", status, size, reviewLevel, currentStep,
@@ -1529,16 +1540,14 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
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, nextRecoveryAt, error,
summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens, summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
tokenUsageCacheWriteTokens, tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, createdAt, updatedAt, columnMovedAt, tokenUsageCacheWriteTokens, tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, tokenBudgetSoftAlertedAt, tokenBudgetHardAlertedAt, tokenBudgetOverride, createdAt, updatedAt, columnMovedAt,
executionStartedAt, executionCompletedAt, executionStartedAt, executionCompletedAt,
dependencies, steps, log, attachments, steeringComments, dependencies, steps, log, attachments, steeringComments,
comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking, comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking,
sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl, sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, scopeOverride, scopeOverrideReason, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt, checkoutNodeId, checkoutRunId, checkoutLeaseRenewedAt, checkoutLeaseEpoch mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, scopeOverride, scopeOverrideReason, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt, checkoutNodeId, checkoutRunId, checkoutLeaseRenewedAt, checkoutLeaseEpoch
) VALUES ( ) VALUES (${placeholders})
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? `).run(...values);
)
`).run(...this.getTaskPersistValues(task));
this.db.bumpLastModified(); this.db.bumpLastModified();
} }
@@ -1547,6 +1556,8 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
* semantics; create paths must use insertTask() instead. * semantics; create paths must use insertTask() instead.
*/ */
private upsertTask(task: Task): void { private upsertTask(task: Task): void {
const values = this.getTaskPersistValues(task);
const placeholders = values.map(() => "?").join(", ");
this.db.prepare(` this.db.prepare(`
INSERT INTO tasks ( INSERT INTO tasks (
id, lineageId, title, description, priority, "column", status, size, reviewLevel, currentStep, id, lineageId, title, description, priority, "column", status, size, reviewLevel, currentStep,
@@ -1554,15 +1565,13 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
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, nextRecoveryAt, error,
summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens, summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
tokenUsageCacheWriteTokens, tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, createdAt, updatedAt, columnMovedAt, tokenUsageCacheWriteTokens, tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, tokenBudgetSoftAlertedAt, tokenBudgetHardAlertedAt, tokenBudgetOverride, createdAt, updatedAt, columnMovedAt,
executionStartedAt, executionCompletedAt, executionStartedAt, executionCompletedAt,
dependencies, steps, log, attachments, steeringComments, dependencies, steps, log, attachments, steeringComments,
comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking, comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking,
sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl, sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, scopeOverride, scopeOverrideReason, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt, checkoutNodeId, checkoutRunId, checkoutLeaseRenewedAt, checkoutLeaseEpoch mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, scopeOverride, scopeOverrideReason, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt, checkoutNodeId, checkoutRunId, checkoutLeaseRenewedAt, checkoutLeaseEpoch
) VALUES ( ) VALUES (${placeholders})
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
)
ON CONFLICT(id) DO UPDATE SET ON CONFLICT(id) DO UPDATE SET
lineageId = excluded.lineageId, lineageId = excluded.lineageId,
title = excluded.title, title = excluded.title,
@@ -1608,6 +1617,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
tokenUsageTotalTokens = excluded.tokenUsageTotalTokens, tokenUsageTotalTokens = excluded.tokenUsageTotalTokens,
tokenUsageFirstUsedAt = excluded.tokenUsageFirstUsedAt, tokenUsageFirstUsedAt = excluded.tokenUsageFirstUsedAt,
tokenUsageLastUsedAt = excluded.tokenUsageLastUsedAt, tokenUsageLastUsedAt = excluded.tokenUsageLastUsedAt,
tokenBudgetSoftAlertedAt = excluded.tokenBudgetSoftAlertedAt,
tokenBudgetHardAlertedAt = excluded.tokenBudgetHardAlertedAt,
tokenBudgetOverride = excluded.tokenBudgetOverride,
createdAt = excluded.createdAt, createdAt = excluded.createdAt,
updatedAt = excluded.updatedAt, updatedAt = excluded.updatedAt,
columnMovedAt = excluded.columnMovedAt, columnMovedAt = excluded.columnMovedAt,