fix(#680): session sort corruption with mtime fallback for corrupted JSON
When a session JSON file is corrupted, getFileMainSessions() previously
silently skipped it with 'catch { continue }', causing the session to
disappear from the sorted list entirely.
Now, when JSON.parse fails:
- Use the file's mtime as fallback for both created and updated timestamps
- Set load_error field with the parse error message
- Include the session in sort results so it remains visible
This prevents session sort corruption where corrupted sessions vanish
from the list, breaking chronological ordering and confusing users.
Closes #680
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { existsSync } from "node:fs"
|
||||
import { readdir, readFile } from "node:fs/promises"
|
||||
import { readdir, readFile, stat } from "node:fs/promises"
|
||||
import { join } from "node:path"
|
||||
import { MESSAGE_STORAGE, PART_STORAGE, SESSION_STORAGE, TODO_DIR, TRANSCRIPT_DIR } from "./constants"
|
||||
import { getMessageDir } from "../../shared/opencode-message-dir"
|
||||
@@ -20,14 +20,31 @@ export async function getFileMainSessions(directory?: string): Promise<SessionMe
|
||||
for (const file of sessionFiles) {
|
||||
if (!file.endsWith(".json")) continue
|
||||
|
||||
const filePath = join(projectPath, file)
|
||||
try {
|
||||
const content = await readFile(join(projectPath, file), "utf-8")
|
||||
const content = await readFile(filePath, "utf-8")
|
||||
const meta = JSON.parse(content) as SessionMetadata
|
||||
if (meta.parentID) continue
|
||||
if (directory && meta.directory !== directory) continue
|
||||
sessions.push(meta)
|
||||
} catch {
|
||||
continue
|
||||
} catch (err) {
|
||||
const sessionID = file.replace(/\.json$/, "")
|
||||
try {
|
||||
const stats = await stat(filePath)
|
||||
const mtime = Math.floor(stats.mtimeMs)
|
||||
sessions.push({
|
||||
id: sessionID,
|
||||
projectID: projectDir.name,
|
||||
directory: directory || "",
|
||||
time: {
|
||||
created: mtime,
|
||||
updated: mtime,
|
||||
},
|
||||
load_error: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,6 +397,62 @@ describe("session-manager storage - getMainSessions", () => {
|
||||
expect(sessionsB[0].id).toBe("ses_projB")
|
||||
})
|
||||
|
||||
test("getMainSessions includes corrupted JSON files with mtime fallback and load_error", async () => {
|
||||
//#given
|
||||
const projectID = "proj_corrupt"
|
||||
const now = Date.now()
|
||||
|
||||
// Create a valid session
|
||||
createSessionMetadata(projectID, "ses_valid", { directory: "/test/path", updated: now - 1000 })
|
||||
|
||||
// Create a corrupted session file
|
||||
const projectDir = join(TEST_SESSION_STORAGE, projectID)
|
||||
mkdirSync(projectDir, { recursive: true })
|
||||
const corruptFile = join(projectDir, "ses_corrupt.json")
|
||||
writeFileSync(corruptFile, "{ invalid json", "utf-8")
|
||||
|
||||
// Manually set the mtime to be newer than the valid session
|
||||
// Note: we can't easily control mtime in tests, so we'll verify it's included with load_error
|
||||
|
||||
//#when
|
||||
const sessions = await storage.getMainSessions({ directory: "/test/path" })
|
||||
|
||||
//#then
|
||||
expect(sessions.length).toBe(2)
|
||||
const corruptSession = sessions.find((s) => s.id === "ses_corrupt")
|
||||
expect(corruptSession).toBeDefined()
|
||||
expect(corruptSession?.load_error).toBeTruthy()
|
||||
expect(corruptSession?.time.updated).toBeGreaterThan(0)
|
||||
})
|
||||
test("getMainSessions sorts corrupted JSON files alongside valid sessions using mtime", async () => {
|
||||
//#given
|
||||
const projectID = "proj_sort"
|
||||
const now = Date.now()
|
||||
|
||||
createSessionMetadata(projectID, "ses_old", { directory: "/test/path", updated: now - 5000 })
|
||||
createSessionMetadata(projectID, "ses_new", { directory: "/test/path", updated: now })
|
||||
|
||||
// Create a corrupted file with mtime in between
|
||||
const projectDir = join(TEST_SESSION_STORAGE, projectID)
|
||||
mkdirSync(projectDir, { recursive: true })
|
||||
const corruptFile = join(projectDir, "ses_mid.json")
|
||||
writeFileSync(corruptFile, "{ invalid json", "utf-8")
|
||||
|
||||
// Touch the file to set mtime between old and new
|
||||
// We use utimesSync to set a specific mtime
|
||||
const { utimesSync } = require("node:fs")
|
||||
utimesSync(corruptFile, new Date(now - 2500), new Date(now - 2500))
|
||||
|
||||
//#when
|
||||
const sessions = await storage.getMainSessions({ directory: "/test/path" })
|
||||
|
||||
//#then
|
||||
expect(sessions.length).toBe(3)
|
||||
expect(sessions[0].id).toBe("ses_new")
|
||||
expect(sessions[1].id).toBe("ses_mid")
|
||||
expect(sessions[2].id).toBe("ses_old")
|
||||
})
|
||||
|
||||
test("getMainSessions returns all main sessions when directory is not specified", async () => {
|
||||
// given
|
||||
const projectA = "proj_aaa"
|
||||
|
||||
@@ -49,7 +49,22 @@ export interface SearchResult {
|
||||
timestamp?: number
|
||||
}
|
||||
|
||||
export interface SessionMetadata {
|
||||
id: string
|
||||
version?: string
|
||||
projectID: string
|
||||
directory: string
|
||||
title?: string
|
||||
parentID?: string
|
||||
time: {
|
||||
created: number
|
||||
updated: number
|
||||
}
|
||||
summary?: {
|
||||
additions: number
|
||||
deletions: number
|
||||
files: number
|
||||
}
|
||||
load_error?: string
|
||||
id: string
|
||||
version?: string
|
||||
projectID: string
|
||||
|
||||
Reference in New Issue
Block a user