diff --git a/packages/opencode/src/file/time.ts b/packages/opencode/src/file/time.ts index 770427abe96..144d53ab6f2 100644 --- a/packages/opencode/src/file/time.ts +++ b/packages/opencode/src/file/time.ts @@ -7,18 +7,29 @@ export namespace FileTime { // All tools that overwrite existing files should run their // assert/read/write/update sequence inside withLock(filepath, ...) // so concurrent writes to the same file are serialized. - export const state = Instance.state(() => { - const read: { - [sessionID: string]: { - [path: string]: Date | undefined + export const state = Instance.state( + () => { + const read: { + [sessionID: string]: { + [path: string]: Date | undefined + } + } = {} + const locks = new Map>() + return { + read, + locks, } - } = {} - const locks = new Map>() - return { - read, - locks, - } - }) + }, + // Explicit cleanup on instance disposal. While the state object is GC'd + // when the instance is disposed, this makes the lifecycle explicit for + // long-running sessions with many subtasks. + async (current) => { + for (const key of Object.keys(current.read)) { + delete current.read[key] + } + current.locks.clear() + }, + ) export function read(sessionID: string, file: string) { log.info("read", { sessionID, file }) @@ -61,4 +72,10 @@ export namespace FileTime { ) } } + + // Clears read timestamps for a deleted session. Called by Session.remove() + // to prevent orphaned entries from accumulating when sessions/subtasks are deleted. + export function clearSession(sessionID: string) { + delete state().read[sessionID] + } } diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 0776590d6a9..7cfebe8d149 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -16,6 +16,7 @@ import { SessionPrompt } from "./prompt" import { fn } from "@/util/fn" import { Command } from "../command" import { Snapshot } from "@/snapshot" +import { FileTime } from "@/file/time" import type { Provider } from "@/provider/provider" import { PermissionNext } from "@/permission/next" @@ -319,6 +320,8 @@ export namespace Session { await Storage.remove(msg) } await Storage.remove(["session", project.id, sessionID]) + // Clear file read timestamps - session is gone, timestamps are orphaned + FileTime.clearSession(sessionID) Bus.publish(Event.Deleted, { info: session, })