diff --git a/packages/core/schema.json b/packages/core/schema.json index de4e2ebf9d28..968f5d791916 100644 --- a/packages/core/schema.json +++ b/packages/core/schema.json @@ -1,8 +1,8 @@ { "version": "7", "dialect": "sqlite", - "id": "be60f352-8da1-40e1-8d70-dc41121cfbc5", - "prevIds": ["3fb67508-0196-4bae-b2bd-c08ece7583fd"], + "id": "7f833e4e-de8a-4792-a726-e70daa8b5a4e", + "prevIds": ["be60f352-8da1-40e1-8d70-dc41121cfbc5"], "ddl": [ { "name": "account_state", @@ -1406,7 +1406,7 @@ "autoincrement": false, "default": null, "generated": null, - "name": "time_suspended", + "name": "execution_claimed_at", "entityType": "columns", "table": "session_v2" }, @@ -2041,17 +2041,17 @@ { "columns": [ { - "value": "time_suspended", + "value": "execution_claimed_at", "isExpression": false } ], "isUnique": false, - "where": "\"session_v2\".\"time_suspended\" is not null", + "where": "\"session_v2\".\"execution_claimed_at\" is not null", "origin": "manual", - "name": "session_v2_time_suspended_idx", + "name": "session_v2_execution_claimed_at_idx", "entityType": "indexes", "table": "session_v2" } ], - "renames": [] + "renames": ["session_v2.time_suspended->session_v2.execution_claimed_at"] } diff --git a/packages/core/src/database/migration.gen.ts b/packages/core/src/database/migration.gen.ts index 382849b4ce5b..b7466b9a9105 100644 --- a/packages/core/src/database/migration.gen.ts +++ b/packages/core/src/database/migration.gen.ts @@ -45,6 +45,7 @@ import m42 from "./migration/20260812181746_session_inbox.js" import m43 from "./migration/20260812213948_worktree.js" import m44 from "./migration/20260819222447_session_viewed_state.js" import m45 from "./migration/20260823191254_nullable_workspace_binding.js" +import m46 from "./migration/20260903010538_execution_claimed_at.js" export const migrations = [ m00, @@ -93,4 +94,5 @@ export const migrations = [ m43, m44, m45, + m46, ] satisfies DatabaseMigration.Migration[] diff --git a/packages/core/src/database/migration/20260903010538_execution_claimed_at.ts b/packages/core/src/database/migration/20260903010538_execution_claimed_at.ts new file mode 100644 index 000000000000..65d32b2d3a82 --- /dev/null +++ b/packages/core/src/database/migration/20260903010538_execution_claimed_at.ts @@ -0,0 +1,17 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration.js" + +const migration: DatabaseMigration.Migration = { + id: "20260903010538_execution_claimed_at", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`session_v2\` RENAME COLUMN \`time_suspended\` TO \`execution_claimed_at\`;`) + yield* tx.run(`DROP INDEX IF EXISTS \`session_v2_time_suspended_idx\`;`) + yield* tx.run( + `CREATE INDEX \`session_v2_execution_claimed_at_idx\` ON \`session_v2\` (\`execution_claimed_at\`) WHERE "session_v2"."execution_claimed_at" is not null;`, + ) + }) + }, +} + +export default migration diff --git a/packages/core/src/database/schema.gen.ts b/packages/core/src/database/schema.gen.ts index e30323916074..d73259495bfe 100644 --- a/packages/core/src/database/schema.gen.ts +++ b/packages/core/src/database/schema.gen.ts @@ -214,7 +214,7 @@ const schema: Omit = { \`idle_outcome\` text, \`time_compacting\` integer, \`time_archived\` integer, - \`time_suspended\` integer, + \`execution_claimed_at\` integer, \`resume_attempts\` integer DEFAULT 0 NOT NULL, CONSTRAINT \`fk_session_v2_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE ); @@ -272,7 +272,7 @@ const schema: Omit = { yield* tx.run(`CREATE INDEX \`session_v2_workspace_idx\` ON \`session_v2\` (\`workspace_id\`);`) yield* tx.run(`CREATE INDEX \`session_v2_parent_idx\` ON \`session_v2\` (\`parent_id\`);`) yield* tx.run( - `CREATE INDEX \`session_v2_time_suspended_idx\` ON \`session_v2\` (\`time_suspended\`) WHERE "session_v2"."time_suspended" is not null;`, + `CREATE INDEX \`session_v2_execution_claimed_at_idx\` ON \`session_v2\` (\`execution_claimed_at\`) WHERE "session_v2"."execution_claimed_at" is not null;`, ) }) }, diff --git a/packages/core/src/database/v1-migration.bun.ts b/packages/core/src/database/v1-migration.bun.ts index 193d454831dc..ee3e2562febc 100644 --- a/packages/core/src/database/v1-migration.bun.ts +++ b/packages/core/src/database/v1-migration.bun.ts @@ -772,7 +772,7 @@ function importNextDatabase( path, title, version, share_url, summary_additions, summary_deletions, summary_files, summary_diffs, metadata, cost, tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write, revert, permission, agent, model, time_created, time_updated, time_compacting, - time_archived, time_suspended + time_archived, execution_claimed_at ) VALUES ( ${session.id}, ${projectID}, ${session.workspace_id}, ${session.parent_id}, ${session.fork_session_id}, ${session.fork_boundary}, ${session.slug}, ${session.directory}, diff --git a/packages/core/src/session/sql.ts b/packages/core/src/session/sql.ts index facad140a46a..b13b9cc6a073 100644 --- a/packages/core/src/session/sql.ts +++ b/packages/core/src/session/sql.ts @@ -62,17 +62,17 @@ export const SessionTable = sqliteTable( idle_outcome: text().$type>(), time_compacting: integer(), time_archived: integer(), - /** The execution claim timestamp (historical column name; see SessionStore.claim). */ - time_suspended: integer(), + /** Write-ahead recovery marker; see SessionStore.claim. */ + execution_claimed_at: integer(), resume_attempts: integer().notNull().default(0), }, (table) => [ index("session_v2_project_idx").on(table.project_id), index("session_v2_workspace_idx").on(table.workspace_id), index("session_v2_parent_idx").on(table.parent_id), - index("session_v2_time_suspended_idx") - .on(table.time_suspended) - .where(sql`${table.time_suspended} is not null`), + index("session_v2_execution_claimed_at_idx") + .on(table.execution_claimed_at) + .where(sql`${table.execution_claimed_at} is not null`), ], ) diff --git a/packages/core/src/session/store.ts b/packages/core/src/session/store.ts index e10c16e47e5d..a5c01ad70d69 100644 --- a/packages/core/src/session/store.ts +++ b/packages/core/src/session/store.ts @@ -191,7 +191,7 @@ const layer = Layer.effect( return yield* db .select({ sessionID: SessionTable.id }) .from(SessionTable) - .where(and(isNotNull(SessionTable.time_suspended), isNull(SessionTable.parent_id))) + .where(and(isNotNull(SessionTable.execution_claimed_at), isNull(SessionTable.parent_id))) .all() .pipe( Effect.orDie, @@ -205,15 +205,15 @@ const layer = Layer.effect( // pinned so session ordering only moves on real changes. yield* db .update(SessionTable) - .set({ time_suspended: Date.now(), time_updated: sql`${SessionTable.time_updated}` }) - .where(and(eq(SessionTable.id, sessionID), isNull(SessionTable.time_suspended))) + .set({ execution_claimed_at: Date.now(), time_updated: sql`${SessionTable.time_updated}` }) + .where(and(eq(SessionTable.id, sessionID), isNull(SessionTable.execution_claimed_at))) .run() .pipe(Effect.orDie) }), release: Effect.fn("SessionStore.release")(function* (sessionID) { yield* db .update(SessionTable) - .set({ time_suspended: null, resume_attempts: 0, time_updated: sql`${SessionTable.time_updated}` }) + .set({ execution_claimed_at: null, resume_attempts: 0, time_updated: sql`${SessionTable.time_updated}` }) .where(eq(SessionTable.id, sessionID)) .run() .pipe(Effect.orDie) @@ -221,10 +221,10 @@ const layer = Layer.effect( releaseChildClaims: Effect.fn("SessionStore.releaseChildClaims")((recoverable) => db .update(SessionTable) - .set({ time_suspended: null, resume_attempts: 0, time_updated: sql`${SessionTable.time_updated}` }) + .set({ execution_claimed_at: null, resume_attempts: 0, time_updated: sql`${SessionTable.time_updated}` }) .where( and( - isNotNull(SessionTable.time_suspended), + isNotNull(SessionTable.execution_claimed_at), isNotNull(SessionTable.parent_id), recoverable.length > 0 ? notInArray(SessionTable.id, Array.from(recoverable)) : undefined, ), diff --git a/packages/core/test/execution-claim-migration.test.ts b/packages/core/test/execution-claim-migration.test.ts new file mode 100644 index 000000000000..7cf5dc99d129 --- /dev/null +++ b/packages/core/test/execution-claim-migration.test.ts @@ -0,0 +1,145 @@ +import { describe, expect } from "bun:test" +import { SqliteClient } from "@effect/sql-sqlite-bun" +import { EffectDrizzleSqlite } from "@opencode-ai/core/database/drizzle" +import { DatabaseMigration } from "@opencode-ai/core/database/migration" +import { migrations } from "@opencode-ai/core/database/migration.gen" +import executionClaimMigration from "@opencode-ai/core/database/migration/20260903010538_execution_claimed_at" +import { Global } from "@opencode-ai/util/global" +import { sql } from "drizzle-orm" +import { Effect, Layer } from "effect" +import { testEffect } from "./lib/effect" + +const it = testEffect( + Layer.mergeAll( + SqliteClient.layer({ filename: ":memory:", disableWAL: true }), + Layer.succeed(Global.Service, Global.make()), + ), +) + +describe("execution claim column", () => { + it.live("bootstraps the renamed column with an unclaimed default and its partial index", () => + Effect.gen(function* () { + const db = yield* EffectDrizzleSqlite.makeWithDefaults() + yield* DatabaseMigration.apply(db) + yield* db.run(sql` + INSERT INTO project (id, worktree, time_created, time_updated, sandboxes) + VALUES ('project', '/repo', 1, 2, '[]') + `) + yield* db.run(sql` + INSERT INTO session_v2 (id, project_id, slug, directory, version, time_created, time_updated) + VALUES ('session', 'project', 'session', '/repo', '2', 1, 2) + `) + + expect(yield* db.get(sql`SELECT execution_claimed_at, resume_attempts FROM session_v2`)).toEqual({ + execution_claimed_at: null, + resume_attempts: 0, + }) + expect(yield* db.get(sql`SELECT id FROM migration WHERE id = ${executionClaimMigration.id}`)).toEqual({ + id: executionClaimMigration.id, + }) + expect( + yield* db.all(sql`SELECT name FROM pragma_table_info('session_v2') WHERE name = 'time_suspended'`), + ).toEqual([]) + expect( + yield* db.get(sql` + SELECT name, partial FROM pragma_index_list('session_v2') + WHERE name = 'session_v2_execution_claimed_at_idx' + `), + ).toEqual({ name: "session_v2_execution_claimed_at_idx", partial: 1 }) + expect(yield* db.all(sql`SELECT name FROM pragma_index_info('session_v2_execution_claimed_at_idx')`)).toEqual([ + { name: "execution_claimed_at" }, + ]) + yield* DatabaseMigration.apply(db) + }), + ) + + it.live("migrates existing claims without resetting attempts, activity, or dependent history", () => + Effect.gen(function* () { + const db = yield* EffectDrizzleSqlite.makeWithDefaults() + yield* db.run(sql`PRAGMA foreign_keys = ON`) + yield* db.run(sql` + CREATE TABLE session_v2 ( + id text PRIMARY KEY, + parent_id text, + title text, + time_created integer NOT NULL, + time_updated integer NOT NULL, + time_suspended integer, + resume_attempts integer DEFAULT 0 NOT NULL + ) + `) + yield* db.run(sql` + CREATE INDEX session_v2_time_suspended_idx ON session_v2 (time_suspended) + WHERE "session_v2"."time_suspended" IS NOT NULL + `) + yield* db.run(sql` + CREATE TABLE session_message ( + id text PRIMARY KEY, + session_id text NOT NULL REFERENCES session_v2(id) ON DELETE CASCADE, + data text NOT NULL + ) + `) + yield* db.run(sql` + INSERT INTO session_v2 VALUES + ('claimed', NULL, 'Claimed', 1, 2, 1234, 2), + ('child', 'claimed', 'Child', 3, 4, 5678, 1), + ('idle', NULL, 'Idle', 5, 6, NULL, 0) + `) + yield* db.run(sql`INSERT INTO session_message VALUES ('message', 'claimed', '{"text":"preserved"}')`) + yield* db.run(sql`CREATE TABLE migration (id text PRIMARY KEY, time_completed integer NOT NULL)`) + yield* Effect.forEach( + migrations.filter((migration) => migration.id !== executionClaimMigration.id), + (migration) => db.run(sql`INSERT INTO migration VALUES (${migration.id}, 1)`), + ) + + yield* DatabaseMigration.apply(db) + yield* DatabaseMigration.apply(db) + + expect(yield* db.all(sql`SELECT * FROM session_v2 ORDER BY id`)).toEqual([ + { + id: "child", + parent_id: "claimed", + title: "Child", + time_created: 3, + time_updated: 4, + execution_claimed_at: 5678, + resume_attempts: 1, + }, + { + id: "claimed", + parent_id: null, + title: "Claimed", + time_created: 1, + time_updated: 2, + execution_claimed_at: 1234, + resume_attempts: 2, + }, + { + id: "idle", + parent_id: null, + title: "Idle", + time_created: 5, + time_updated: 6, + execution_claimed_at: null, + resume_attempts: 0, + }, + ]) + expect(yield* db.all(sql`SELECT * FROM session_message`)).toEqual([ + { id: "message", session_id: "claimed", data: '{"text":"preserved"}' }, + ]) + expect(yield* db.all(sql`PRAGMA foreign_key_check`)).toEqual([]) + expect(yield* db.get(sql`SELECT count(*) AS count FROM migration`)).toEqual({ count: migrations.length }) + expect( + yield* db.all(sql` + SELECT name FROM pragma_index_list('session_v2') WHERE name = 'session_v2_time_suspended_idx' + `), + ).toEqual([]) + expect( + yield* db.all(sql` + SELECT id FROM session_v2 INDEXED BY session_v2_execution_claimed_at_idx + WHERE execution_claimed_at IS NOT NULL ORDER BY id + `), + ).toEqual([{ id: "child" }, { id: "claimed" }]) + }), + ) +}) diff --git a/packages/core/test/session-create.test.ts b/packages/core/test/session-create.test.ts index ab49cbfae70a..0e1d695e6d42 100644 --- a/packages/core/test/session-create.test.ts +++ b/packages/core/test/session-create.test.ts @@ -232,7 +232,7 @@ describe("Session.create", () => { ).toMatchObject({ time_created: before.time_created, time_updated: before.time_updated, - time_suspended: before.time_suspended, + execution_claimed_at: before.execution_claimed_at, resume_attempts: before.resume_attempts, }) // Repeated resolution announces the directory's identity exactly once. diff --git a/packages/core/test/session-execution.test.ts b/packages/core/test/session-execution.test.ts index 3d34e97f2c08..e5bb95ba61b7 100644 --- a/packages/core/test/session-execution.test.ts +++ b/packages/core/test/session-execution.test.ts @@ -63,10 +63,10 @@ describe("SessionExecution lifecycle", () => { const parent = Session.ID.make("ses_recover_parent") const child = Session.ID.make("ses_recover_child") const idle = Session.ID.make("ses_recover_idle") - yield* seedSessions(database, [parent], { time_suspended: Date.now() }) + yield* seedSessions(database, [parent], { execution_claimed_at: Date.now() }) yield* seedSessions(database, [idle]) // Children recover through background Job records, never through the root claim sweep. - yield* seedSessions(database, [child], { time_suspended: Date.now(), parent_id: parent }) + yield* seedSessions(database, [child], { execution_claimed_at: Date.now(), parent_id: parent }) expect(yield* store.listSuspended()).toEqual([parent]) @@ -224,7 +224,7 @@ describe("SessionExecution lifecycle", () => { Effect.gen(function* () { const database = yield* Database.Service const sessionIDs = Array.from({ length: 5 }, (_, index) => Session.ID.make(`ses_resume_concurrent_${index}`)) - yield* seedSessions(database, sessionIDs, { time_suspended: Date.now() }) + yield* seedSessions(database, sessionIDs, { execution_claimed_at: Date.now() }) const fourStarted = yield* Deferred.make() const started: Session.ID[] = [] @@ -251,7 +251,7 @@ describe("SessionExecution lifecycle", () => { const bus = yield* Bus.Service const first = Session.ID.make("ses_resume_first") const second = Session.ID.make("ses_resume_second") - yield* seedSessions(database, [first, second], { time_suspended: Date.now() }) + yield* seedSessions(database, [first, second], { execution_claimed_at: Date.now() }) const drained: string[] = [] const bothDraining = yield* Deferred.make() @@ -297,7 +297,7 @@ describe("SessionExecution lifecycle", () => { const bus = yield* Bus.Service const sessionID = Session.ID.make("ses_resume_exhausted") // A claim from a dead process, already resumed twice without completing. - yield* seedSessions(database, [sessionID], { time_suspended: Date.now(), resume_attempts: 2 }) + yield* seedSessions(database, [sessionID], { execution_claimed_at: Date.now(), resume_attempts: 2 }) const drained: string[] = [] const failures: SessionEvent.Execution.Failed[] = [] @@ -322,7 +322,7 @@ describe("SessionExecution lifecycle", () => { Effect.gen(function* () { const database = yield* Database.Service const sessionID = Session.ID.make("ses_resume_counted") - yield* seedSessions(database, [sessionID], { time_suspended: Date.now() }) + yield* seedSessions(database, [sessionID], { execution_claimed_at: Date.now() }) const draining = yield* Deferred.make() const scope = yield* Scope.make() @@ -388,7 +388,7 @@ describe("SessionRestart background recovery", () => { const parent = Session.ID.make("ses_background_recovery_parent") const child = Session.ID.make("ses_background_recovery_child") yield* seedSessions(database, [parent]) - yield* seedSessions(database, [child], { parent_id: parent, time_suspended: Date.now() }) + yield* seedSessions(database, [child], { parent_id: parent, execution_claimed_at: Date.now() }) yield* seedBackground(jobs, parent, [ { id: "sh_background_orphan", shellID: "sh_background_orphan", command: "sleep 60" }, ]) @@ -600,7 +600,7 @@ describe("SessionRestart background recovery", () => { const store = yield* SessionStore.Service const bus = yield* Bus.Service const parent = Session.ID.make("ses_background_claimed_parent") - yield* seedSessions(database, [parent], { time_suspended: Date.now() }) + yield* seedSessions(database, [parent], { execution_claimed_at: Date.now() }) yield* seedBackground(jobs, parent, [{ id: "call-claimed-shell", shellID: "sh_claimed", command: "sleep 60" }]) const observed = yield* Deferred.make() @@ -641,7 +641,7 @@ describe("SessionRestart background recovery", () => { const database = yield* Database.Service const jobs = yield* Job.Service const sessionID = Session.ID.make("ses_shell_recovery_exhausted") - yield* seedSessions(database, [sessionID], { time_suspended: Date.now(), resume_attempts: 2 }) + yield* seedSessions(database, [sessionID], { execution_claimed_at: Date.now(), resume_attempts: 2 }) yield* seedBackground(jobs, sessionID, [ { id: "call-exhausted-shell", shellID: "sh_exhausted", command: "sleep 60" }, ]) @@ -688,7 +688,7 @@ describe("SessionRestart background recovery", () => { yield* seedSessions(database, [parent]) yield* seedSessions(database, [child], { parent_id: parent, - time_suspended: Date.now(), + execution_claimed_at: Date.now(), resume_attempts: resumeAttempts, }) const shell = seedBackground(jobs, child, [ @@ -782,8 +782,8 @@ describe("SessionRestart background recovery", () => { const parent = Session.ID.make("ses_subagent_recovery_parent") const child = Session.ID.make("ses_subagent_recovery_child") const unrelated = Session.ID.make("ses_subagent_unrelated_child") - yield* seedSessions(database, [parent], { time_suspended: Date.now(), resume_attempts: 1 }) - yield* seedSessions(database, [child, unrelated], { parent_id: parent, time_suspended: Date.now() }) + yield* seedSessions(database, [parent], { execution_claimed_at: Date.now(), resume_attempts: 1 }) + yield* seedSessions(database, [child, unrelated], { parent_id: parent, execution_claimed_at: Date.now() }) yield* jobs.start({ id: child, type: "subagent", @@ -959,7 +959,7 @@ describe("SessionRestart background recovery", () => { Session.ID.make("ses_subagent_budget_child_1"), Session.ID.make("ses_subagent_budget_child_2"), ] - yield* seedSessions(database, [parent], { time_suspended: Date.now(), resume_attempts: resumeAttempts }) + yield* seedSessions(database, [parent], { execution_claimed_at: Date.now(), resume_attempts: resumeAttempts }) yield* seedSessions(database, children, { parent_id: parent }) const complete = yield* Deferred.make() for (const child of children) { @@ -1033,7 +1033,11 @@ describe("SessionRestart background recovery", () => { const parent = Session.ID.make("ses_subagent_exhausted_parent") const child = Session.ID.make("ses_subagent_exhausted_child") yield* seedSessions(database, [parent]) - yield* seedSessions(database, [child], { parent_id: parent, time_suspended: Date.now(), resume_attempts: 2 }) + yield* seedSessions(database, [child], { + parent_id: parent, + execution_claimed_at: Date.now(), + resume_attempts: 2, + }) yield* jobs.start({ id: child, type: "subagent", @@ -1273,7 +1277,9 @@ function seedInbox( function seedSessions( database: Database.Service["Service"], sessionIDs: ReadonlyArray, - values: Partial> = {}, + values: Partial< + Pick + > = {}, ) { return Effect.gen(function* () { yield* database.db @@ -1302,7 +1308,7 @@ function seedSessions( function claims(database: Database.Service["Service"]) { return database.db - .select({ id: SessionTable.id, claimed: SessionTable.time_suspended }) + .select({ id: SessionTable.id, claimed: SessionTable.execution_claimed_at }) .from(SessionTable) .all() .pipe( diff --git a/packages/core/test/session-projector.test.ts b/packages/core/test/session-projector.test.ts index 0043dffdf4df..0555414d7a00 100644 --- a/packages/core/test/session-projector.test.ts +++ b/packages/core/test/session-projector.test.ts @@ -549,19 +549,19 @@ describe("SessionProjector", () => { Effect.gen(function* () { const db = yield* seedSession() const bus = yield* Bus.Service - const suspended = () => + const claim = () => db - .select({ timeSuspended: SessionTable.time_suspended }) + .select({ executionClaimedAt: SessionTable.execution_claimed_at }) .from(SessionTable) .where(eq(SessionTable.id, sessionID)) .get() .pipe(Effect.orDie) yield* bus.publish(SessionEvent.Execution.Interrupted, { sessionID, reason: "shutdown" }) - expect((yield* suspended())?.timeSuspended).toBeNull() + expect((yield* claim())?.executionClaimedAt).toBeNull() yield* bus.publish(SessionEvent.Execution.Started, { sessionID }) - expect((yield* suspended())?.timeSuspended).toBeNull() + expect((yield* claim())?.executionClaimedAt).toBeNull() }), ) diff --git a/packages/core/test/v1-migration.test.ts b/packages/core/test/v1-migration.test.ts index b8ca1879f087..27a9f649f22e 100644 --- a/packages/core/test/v1-migration.test.ts +++ b/packages/core/test/v1-migration.test.ts @@ -65,7 +65,7 @@ const session = ( idle_outcome: null, time_compacting: 3, time_archived: null, - time_suspended: null, + execution_claimed_at: null, resume_attempts: 0, ...overrides, }) @@ -870,6 +870,7 @@ describe("V1Migration database workflow", () => { ('msg_next', 'ses_next', 'user', 4, 12, 13, '{"text":"from next''s history","time":{"created":12}}'), ('msg_source_existing', 'ses_existing', 'user', 2, 12, 13, '{"text":"source","time":{"created":12}}'), ('msg_orphan', 'ses_orphan', 'user', 0, 12, 13, '{"text":"orphan","time":{"created":12}}'); + UPDATE session SET time_suspended = 1234 WHERE id = 'ses_next'; `) source.close() @@ -901,6 +902,9 @@ describe("V1Migration database workflow", () => { agent: "build", model: '{"id":"model","providerID":"provider"}', }) + expect( + yield* db.get(sql`SELECT execution_claimed_at, resume_attempts FROM session_v2 WHERE id = 'ses_next'`), + ).toEqual({ execution_claimed_at: 1234, resume_attempts: 0 }) expect( yield* db .select({ directory: SessionTable.directory }) @@ -988,8 +992,8 @@ describe("V1Migration database workflow", () => { const database = yield* Database.Service expect(yield* V1Migration.run({ nextDatabasePath: filename })).toEqual({ status: "completed" }) expect( - yield* database.db.get(sql`SELECT fork_boundary, time_suspended FROM session_v2 WHERE id = 'ses_next'`), - ).toEqual({ fork_boundary: null, time_suspended: null }) + yield* database.db.get(sql`SELECT fork_boundary, execution_claimed_at FROM session_v2 WHERE id = 'ses_next'`), + ).toEqual({ fork_boundary: null, execution_claimed_at: null }) expect( yield* database.db.get( sql`SELECT icon_url, icon_url_override, icon_color, commands FROM project WHERE id = 'next-project'`,