mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-06 00:46:26 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a366c3cab |
@@ -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"]
|
||||
}
|
||||
|
||||
+2
@@ -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[]
|
||||
|
||||
@@ -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
|
||||
@@ -214,7 +214,7 @@ const schema: Omit<DatabaseMigration.Migration, "id"> = {
|
||||
\`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<DatabaseMigration.Migration, "id"> = {
|
||||
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;`,
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -62,17 +62,17 @@ export const SessionTable = sqliteTable(
|
||||
idle_outcome: text().$type<NonNullable<Session.Info["outcome"]>>(),
|
||||
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`),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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" }])
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -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.
|
||||
|
||||
@@ -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<void>()
|
||||
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<void>()
|
||||
@@ -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<void>()
|
||||
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<string[]>()
|
||||
@@ -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<string>()
|
||||
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<Session.ID>,
|
||||
values: Partial<Pick<typeof SessionTable.$inferInsert, "time_suspended" | "resume_attempts" | "parent_id">> = {},
|
||||
values: Partial<
|
||||
Pick<typeof SessionTable.$inferInsert, "execution_claimed_at" | "resume_attempts" | "parent_id">
|
||||
> = {},
|
||||
) {
|
||||
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(
|
||||
|
||||
@@ -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()
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -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'`,
|
||||
|
||||
Reference in New Issue
Block a user