fix(sync): read session ownership from the record, not store membership
A session created in a git worktree while the client was already running did not render: the message list stayed empty while the prompt and the assistant reply were both present in the session, visible on any fresh load. Reported as prompting in a worktree sometimes not working. Ownership was read from which child store holds the session. That is containment, not ownership. A project's session list includes the sessions of its worktrees so the sidebar can group them, so the parent repository holds worktree sessions too, and whichever store bootstrapped first won. Captured mid-failure, the two signals disagreed outright: owningDirectory /repo <- parent, merely holds it recordDirectory /repo/.worktrees/feature <- the session's own directory The parent won, so every fetch was addressed to a directory that does not own the session, the session id resolved to undefined there, and the requests failed as /api/session/undefined in a retry loop. The session's own record is now believed; store membership remains the fallback for a record that carries no directory. This also explains why the previous commit alone was not enough: settling the guessed directory adopted this same wrong value and then cleared the guess, which prevented any later correction. Verified against the reproduction rather than by reasoning. Before: three of four runs never rendered. After, on a clean build with the instrumentation removed: three of three rendered the reply live, each routed to its own worktree. Tests cover ownership disagreeing with containment, plus both directions of the guess promotion.
This commit is contained in:
@@ -19,10 +19,14 @@ const PARENT = "/repo"
|
||||
const WORKTREE = "/repo/.worktrees/feature"
|
||||
const SESSION_ID = "ses_directory_adoption"
|
||||
|
||||
const indexSessionIn = (manager: ChildStoreManager, directory: string): void => {
|
||||
const indexSessionIn = (
|
||||
manager: ChildStoreManager,
|
||||
directory: string,
|
||||
recordDirectory: string = directory,
|
||||
): void => {
|
||||
const store = manager.ensureChild(directory, { bootstrap: false })
|
||||
store.setState({
|
||||
session: [{ id: SESSION_ID, directory, title: "test" } as never],
|
||||
session: [{ id: SESSION_ID, directory: recordDirectory, title: "test" } as never],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -45,6 +49,18 @@ describe("adoptAuthoritativeSessionDirectory", () => {
|
||||
expect(useSessionUIStore.getState().currentSessionDirectory).toBe(WORKTREE)
|
||||
})
|
||||
|
||||
test("believes the session record over the store that merely holds it", () => {
|
||||
// A project's session list includes the sessions of its worktrees so the
|
||||
// sidebar can group them, so the parent store holds this session while the
|
||||
// session itself reports the worktree. Ownership comes from the record.
|
||||
useSessionUIStore.getState().setCurrentSession(SESSION_ID)
|
||||
indexSessionIn(manager, PARENT, WORKTREE)
|
||||
|
||||
useSessionUIStore.getState().adoptAuthoritativeSessionDirectory()
|
||||
|
||||
expect(useSessionUIStore.getState().currentSessionDirectory).toBe(WORKTREE)
|
||||
})
|
||||
|
||||
test("does nothing while the owning directory is still unknown", () => {
|
||||
useSessionUIStore.getState().setCurrentSession(SESSION_ID)
|
||||
const before = useSessionUIStore.getState().currentSessionDirectory
|
||||
|
||||
@@ -411,11 +411,24 @@ const getAttachmentForSession = (sessionId: string | null | undefined): SessionW
|
||||
* only then the session record's own fields. `null` means "not indexed yet",
|
||||
* never "no directory" — callers must fall back rather than treat it as empty.
|
||||
*/
|
||||
/**
|
||||
* The directory that owns a session, from the two server-backed signals.
|
||||
*
|
||||
* The session's own record wins. Holding a session in a child store proves
|
||||
* containment, not ownership: a project's session list legitimately includes
|
||||
* the sessions of its worktrees so the sidebar can group them, so the parent
|
||||
* repository holds worktree sessions too. Reading ownership from store
|
||||
* membership therefore reports the parent for a session that lives in a
|
||||
* worktree, and every fetch is then addressed to a directory that does not own
|
||||
* it. Store membership remains the fallback for a session whose record carries
|
||||
* no directory.
|
||||
*/
|
||||
const getAuthoritativeSessionDirectory = (sessionId: string): string | null => {
|
||||
const owningDirectory = getSyncSessionDirectory(sessionId)
|
||||
if (owningDirectory) return normalizePath(owningDirectory)
|
||||
const target = getAllSyncSessions().find((s) => s.id === sessionId)
|
||||
return target ? resolveDirectoryKey(target) : null
|
||||
const recordDirectory = target ? resolveDirectoryKey(target) : null
|
||||
if (recordDirectory) return normalizePath(recordDirectory)
|
||||
const owningDirectory = getSyncSessionDirectory(sessionId)
|
||||
return owningDirectory ? normalizePath(owningDirectory) : null
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user