fix(sessions): recover sessions whose directory disappeared (#3365)
* fix(sessions): keep a shared chat directory until its last session is deleted Deleting a root chat session removed its managed scratch directory even when forks, side threads, or subagents still lived in it; OpenCode then failed every prompt in those sessions with FileSystem.realPath NotFound. The directory is now removed only once no other known session resolves to it. The deleted subtree does not count, because the server cascade- deletes it, and an unloaded global cache keeps the directory instead of guessing. Closes #3312. * fix(sessions): relocate a session whose worktree directory disappeared A worktree removed outside OpenChamber, by the agent or by hand, left its sessions pointed at a path that no longer exists: every terminal create and restart failed with "Invalid working directory" and the tab stayed stuck, while Git, Files, and prompts kept targeting the dead path. The terminal server now names that one rejection (TERMINAL_CWD_MISSING) instead of substituting a directory of its own. The shared UI reuses the archived-restore fallback for live sessions: a server-confirmed missing directory moves the session and its stranded subtree to the project's primary directory through the control-plane move, clears the worktree hint, re-selects the session, and tells the user where it went. It runs from a terminal failure and on activation of any session whose directory is neither a project root nor a managed chat directory; available, unknown, and failed probes leave everything untouched. Closes #3338. * fix(scripts): make oc-dev load again after the changelog cleanup The changelog cleanup referenced fs.existsSync in a module that imports existsSync by name and never binds fs, so every oc-dev invocation failed with "fs is not defined" before reaching its action. * fix(sessions): probe directory availability on disk, not through OpenCode path resolution OpenCode's /path never checks that a directory exists: it echoes the requested path and resolves its project through Git discovery that swallows errors, so a deleted worktree came back as a valid location and every missing-directory fallback (draft recovery, archived restore, session relocation) stayed inert on a real server. The probe now asks OpenChamber's own /api/fs/list, which stats the path and reports not-found and not-directory explicitly; anything else stays unknown. * fix(sidebar): keep a worktree whose directory is gone visible as missing git keeps a worktree registered after its directory is deleted outside git and marks it prunable; the list parser ignored that line, so a deleted worktree looked alive, and nothing in the app asked for a new listing anyway. The server now reports prunable, the UI keeps such a worktree in the topology with worktreeStatus missing and a warning icon on its sidebar group, and relocating a session out of a confirmed- missing directory raises an in-app topology signal the sidebar rediscovers on. Dropping the worktree instead would hide every session that lived there, and a hidden session can never be opened or relocated. No idle polling is added. * fix(sessions): never relocate a session to the filesystem root OpenCode files a directory outside any Git repository under its global project, whose worktree is the filesystem root. A managed chat whose directory vanished would otherwise be moved to /. The relocation now refuses a root destination, and the activation probe recognizes chat directories through the home-based check as well, so it does not depend on the chats root having been resolved yet. * test(sessions): mirror the relocation action in the issue-2039 session-actions mock session-ui-store now imports relocateSessionFromMissingDirectory, and the mocked module in this test listed every other action but not that one, so the file failed on import.
This commit is contained in:
@@ -309,6 +309,7 @@ mock.module("../session-actions", () => ({
|
||||
unrevertSession: mock(async () => undefined),
|
||||
forkFromMessage: mock(async () => undefined),
|
||||
fetchMessagesForSession: mock(async () => undefined),
|
||||
relocateSessionFromMissingDirectory: mock(async () => ({ status: "unchanged" })),
|
||||
getSessionLastAssistantModel: () => null,
|
||||
patchSessionMetadata: mock(async () => undefined),
|
||||
abortCurrentOperation: mock(async () => undefined),
|
||||
|
||||
Reference in New Issue
Block a user