mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
fix(core): resolve relative directories during V1 migration (#57618)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
94bc5662c4
commit
c3b613cac8
@@ -12,12 +12,15 @@ function isWindowsStoragePath(input: string) {
|
||||
return /^[A-Za-z]:\//.test(input) || input.startsWith("//")
|
||||
}
|
||||
|
||||
function absolute(input: string) {
|
||||
export function storedAbsolutePath(input: string) {
|
||||
const result = storagePath(input)
|
||||
// Persisted projects and sessions can move between operating systems during migration.
|
||||
if (!nodePath.posix.isAbsolute(result) && !isWindowsStoragePath(result)) {
|
||||
throw new Error(`Path is not absolute: ${input}`)
|
||||
}
|
||||
if (nodePath.posix.isAbsolute(result) || isWindowsStoragePath(result)) return result
|
||||
}
|
||||
|
||||
function absolute(input: string) {
|
||||
const result = storedAbsolutePath(input)
|
||||
if (result === undefined) throw new Error(`Path is not absolute: ${input}`)
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ export * as V1Migration from "./v1-migration.js"
|
||||
|
||||
import { Cause, Effect, Layer, Option, Schema, Semaphore } from "effect"
|
||||
import { Database } from "./database.js"
|
||||
import { storedAbsolutePath } from "./path.js"
|
||||
import { SessionMessageTable, SessionTable } from "../session/sql.js"
|
||||
import { SessionV1 } from "@opencode/schema/session-v1"
|
||||
import { SessionMessage } from "../session/message.js"
|
||||
@@ -580,16 +581,19 @@ export function run(options: Options = {}): Effect.Effect<RunResult, never, Data
|
||||
updateProgress({ label: "Migrating sessions", numerator: migrated + completed, denominator })
|
||||
})
|
||||
updateProgress({ label: "Migrating sessions", numerator: migrated + sourceTotal, denominator })
|
||||
const projects = new Set(
|
||||
(yield* db.all<{ id: string }>(sql`SELECT id FROM project`)).map((project) => project.id),
|
||||
const projects = new Map(
|
||||
(yield* db.all<{ id: string; worktree: string }>(sql`SELECT id, worktree FROM project`)).map((project) => [
|
||||
project.id,
|
||||
project.worktree,
|
||||
]),
|
||||
)
|
||||
while (true) {
|
||||
const state = yield* readState(db)
|
||||
const cursorValue = state?.phase === "sessions" ? state.cursor : undefined
|
||||
const nextID = yield* db.get<{ id: string; project_id: string }>(
|
||||
const nextID = yield* db.get<{ id: string; project_id: string; directory: string }>(
|
||||
cursorValue === undefined
|
||||
? sql`SELECT id, project_id FROM session ORDER BY id DESC LIMIT 1`
|
||||
: sql`SELECT id, project_id FROM session WHERE id < ${cursorValue} ORDER BY id DESC LIMIT 1`,
|
||||
? sql`SELECT id, project_id, directory FROM session ORDER BY id DESC LIMIT 1`
|
||||
: sql`SELECT id, project_id, directory FROM session WHERE id < ${cursorValue} ORDER BY id DESC LIMIT 1`,
|
||||
)
|
||||
if (!nextID) break
|
||||
yield* db
|
||||
@@ -609,6 +613,18 @@ export function run(options: Options = {}): Effect.Effect<RunResult, never, Data
|
||||
sessionID: nextID.id,
|
||||
projectID: nextID.project_id,
|
||||
})
|
||||
// Legacy ACP sessions can store a cwd relative to an unknown process directory.
|
||||
// Use the project worktree instead, and leave the original V1 row intact.
|
||||
const directory =
|
||||
!nextID.directory || storedAbsolutePath(nextID.directory)
|
||||
? nextID.directory
|
||||
: (storedAbsolutePath(projects.get(projectID) ?? path.parse(global.data).root) ?? "")
|
||||
if (directory !== nextID.directory)
|
||||
yield* Effect.logWarning("Replaced relative V1 session directory", {
|
||||
sessionID: nextID.id,
|
||||
directory: nextID.directory,
|
||||
resolved: directory,
|
||||
})
|
||||
yield* tx.run(sql`
|
||||
INSERT OR IGNORE INTO session_v2 (
|
||||
id, project_id, workspace_id, parent_id, slug, directory, path, title, version, share_url,
|
||||
@@ -617,7 +633,7 @@ export function run(options: Options = {}): Effect.Effect<RunResult, never, Data
|
||||
revert, permission, agent, model, time_created, time_updated, time_compacting, time_archived
|
||||
)
|
||||
SELECT
|
||||
id, ${projectID}, workspace_id, parent_id, slug, directory, path, title, version, share_url,
|
||||
id, ${projectID}, workspace_id, parent_id, slug, ${directory}, 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, NULL, agent, model, time_created, time_updated, time_compacting, time_archived
|
||||
|
||||
@@ -1120,13 +1120,46 @@ describe("V1Migration database workflow", () => {
|
||||
)
|
||||
})
|
||||
|
||||
test("reassigns V1 sessions whose projects are missing to the global project", async () => {
|
||||
test.each([
|
||||
{ worktree: "/repo", directory: ".", expected: "/repo" },
|
||||
{ worktree: "/repo", directory: "src", expected: "/repo" },
|
||||
{ worktree: "C:/repo", directory: ".", expected: "C:/repo" },
|
||||
{ worktree: "//server/share/repo", directory: ".", expected: "//server/share/repo" },
|
||||
{ worktree: "/repo", directory: "C:/other", expected: "C:/other" },
|
||||
{ worktree: "/repo", directory: "//server/share/other", expected: "//server/share/other" },
|
||||
{ worktree: "/repo", directory: "", expected: "" },
|
||||
{ worktree: "relative", directory: ".", expected: "" },
|
||||
])("migrates $directory in $worktree", async (input) => {
|
||||
await database(
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
yield* db.run(sql`
|
||||
INSERT INTO project (id, worktree, time_created, time_updated, sandboxes)
|
||||
VALUES ('saved-project', ${input.worktree}, 1, 2, '[]')
|
||||
`)
|
||||
yield* db.run(sql`
|
||||
INSERT INTO session (id, project_id, slug, directory, title, version, time_created, time_updated)
|
||||
VALUES ('ses_test', 'saved-project', 'test', ${input.directory}, 'Test', '1', 1, 2)
|
||||
`)
|
||||
|
||||
expect(yield* V1Migration.run()).toEqual({ status: "completed" })
|
||||
expect(yield* db.get(sql`SELECT directory FROM session_v2 WHERE id = 'ses_test'`)).toEqual({
|
||||
directory: input.expected,
|
||||
})
|
||||
expect(yield* db.get(sql`SELECT directory FROM session WHERE id = 'ses_test'`)).toEqual({
|
||||
directory: input.directory,
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test.each(["/tmp/orphan", "."])("reassigns V1 sessions with missing projects and directory %s", async (directory) => {
|
||||
await database(
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
const global = yield* Global.Service
|
||||
yield* db.run(
|
||||
sql`INSERT INTO session (id, project_id, slug, directory, title, version, time_created, time_updated) VALUES ('ses_orphan', 'missing-project', 'orphan', '/tmp/orphan', 'Orphan', '1', 1, 2)`,
|
||||
sql`INSERT INTO session (id, project_id, slug, directory, title, version, time_created, time_updated) VALUES ('ses_orphan', 'missing-project', 'orphan', ${directory}, 'Orphan', '1', 1, 2)`,
|
||||
)
|
||||
|
||||
expect(yield* V1Migration.run()).toEqual({ status: "completed" })
|
||||
@@ -1136,6 +1169,9 @@ describe("V1Migration database workflow", () => {
|
||||
expect(yield* db.get(sql`SELECT worktree FROM project WHERE id = 'global'`)).toEqual({
|
||||
worktree: path.parse(global.data).root,
|
||||
})
|
||||
expect(yield* db.get(sql`SELECT directory FROM session_v2 WHERE id = 'ses_orphan'`)).toEqual({
|
||||
directory: directory === "." ? path.parse(global.data).root.replaceAll("\\", "/") : directory,
|
||||
})
|
||||
expect(yield* db.get(sql`SELECT value FROM kv WHERE key = 'migration.v1-v2'`)).toEqual({
|
||||
value: '{"phase":"completed"}',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user