fix(sync): guard archive actions by default

This commit is contained in:
Bohdan Triapitsyn
2026-08-02 16:48:02 +03:00
parent 4748c3362f
commit f95f1ab18f
3 changed files with 12 additions and 13 deletions
+4 -4
View File
@@ -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
+2 -4
View File
@@ -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
+6 -5
View File
@@ -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<boolean> {
export async function archiveSession(sessionId: string, expectedRuntimeKey = getRuntimeKey()): Promise<boolean> {
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)) {