diff --git a/packages/ui/src/sync/DOCUMENTATION.md b/packages/ui/src/sync/DOCUMENTATION.md index 92399850..c8056edc 100644 --- a/packages/ui/src/sync/DOCUMENTATION.md +++ b/packages/ui/src/sync/DOCUMENTATION.md @@ -217,15 +217,15 @@ Examples of global-store updates performed in `session-actions.ts`: - `deleteSession()` -> waits for server confirmation or `404`, then removes the session and its persisted state - `moveSessionToDirectory()` -> move the session between directory stores and update the global directory index -Archive callers whose confirmation spans asynchronous SDK calls may capture the -runtime key at operation start and pass it as `expectedRuntimeKey`. The guarded -action rechecks that key before every store reconciliation, so a response +Archive actions capture the active runtime key when they start and recheck it +before every store reconciliation, so a response produced by the previous runtime is rejected instead of mutating the current runtime's live or global session state. A guarded batch stops at the first observed runtime change: sessions the server already confirmed remain archived and stay in `archivedIds`, while every ID not confirmed on the captured runtime is returned in `failedIds` so existing partial-failure feedback stays truthful. -Callers that pass no runtime key keep the previous unguarded behavior. +Callers whose confirmation can span a runtime switch may pass an +`expectedRuntimeKey` captured earlier; ordinary callers are guarded by default. ## The golden rule diff --git a/packages/ui/src/sync/session-actions.test.ts b/packages/ui/src/sync/session-actions.test.ts index 0e2bda49..b23f6e19 100644 --- a/packages/ui/src/sync/session-actions.test.ts +++ b/packages/ui/src/sync/session-actions.test.ts @@ -448,7 +448,7 @@ describe("confirmed session removal", () => { const { archiveSession, setActionRefs } = await import("./session-actions") setActionRefs(mockSdk as unknown as OpencodeClient, createChildStores([["/test/project", source]]), () => "/test/project") - expect(await archiveSession("session-a", "archive-runtime-a")).toBe(false) + expect(await archiveSession("session-a")).toBe(false) expect(getRuntimeKey()).toBe("archive-runtime-b") // The stale response must not reconcile the runtime the user switched to. expect(source.getState().session.map((item) => item.id)).toEqual(["session-a"]) @@ -476,9 +476,7 @@ describe("confirmed session removal", () => { const { archiveSessions, setActionRefs } = await import("./session-actions") setActionRefs(mockSdk as unknown as OpencodeClient, createChildStores([["/test/project", source]]), () => "/test/project") - const result = await archiveSessions(["session-a", "session-b", "session-c"], { - expectedRuntimeKey: "archive-batch-a", - }) + const result = await archiveSessions(["session-a", "session-b", "session-c"]) // session-a was confirmed before the switch and stays archived; session-b's // response is stale and session-c is never attempted, so both are reported diff --git a/packages/ui/src/sync/session-actions.ts b/packages/ui/src/sync/session-actions.ts index 43e08171..c554b9c6 100644 --- a/packages/ui/src/sync/session-actions.ts +++ b/packages/ui/src/sync/session-actions.ts @@ -827,15 +827,16 @@ export async function deleteSessionInDirectory(sessionId: string, directory: str /** * Archive one session. * - * `expectedRuntimeKey` is the runtime key the caller captured when the user - * confirmed the operation. When it is supplied and the runtime changes, the - * action stops and returns `false` without reconciling any store, so a response + * `expectedRuntimeKey` defaults to the active runtime when the action starts. + * Callers may supply a key captured earlier when confirmation spans a runtime + * switch. When the runtime changes, the action stops and returns `false` + * without reconciling any store, so a response * produced by the previous runtime cannot mutate the current runtime's live or * global session state. A session the server already archived before the switch * stays archived on that runtime and is re-read from the server the next time * the runtime is loaded. */ -export async function archiveSession(sessionId: string, expectedRuntimeKey?: string): Promise { +export async function archiveSession(sessionId: string, expectedRuntimeKey = getRuntimeKey()): Promise { if (isStaleRuntime(expectedRuntimeKey)) return false const sessionDirectory = getSessionDirectory(sessionId) const archivedAt = Date.now() @@ -884,7 +885,7 @@ export async function archiveSessions( ): Promise<{ archivedIds: string[]; failedIds: string[] }> { const archivedIds: string[] = [] const failedIds: string[] = [] - const expectedRuntimeKey = options?.expectedRuntimeKey + const expectedRuntimeKey = options?.expectedRuntimeKey ?? getRuntimeKey() for (const [index, id] of ids.entries()) { if (isStaleRuntime(expectedRuntimeKey)) {