fix(sync): guard archive actions by default
This commit is contained in:
@@ -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
|
- `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
|
- `moveSessionToDirectory()` -> move the session between directory stores and update the global directory index
|
||||||
|
|
||||||
Archive callers whose confirmation spans asynchronous SDK calls may capture the
|
Archive actions capture the active runtime key when they start and recheck it
|
||||||
runtime key at operation start and pass it as `expectedRuntimeKey`. The guarded
|
before every store reconciliation, so a response
|
||||||
action rechecks that key before every store reconciliation, so a response
|
|
||||||
produced by the previous runtime is rejected instead of mutating the current
|
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
|
runtime's live or global session state. A guarded batch stops at the first
|
||||||
observed runtime change: sessions the server already confirmed remain archived
|
observed runtime change: sessions the server already confirmed remain archived
|
||||||
and stay in `archivedIds`, while every ID not confirmed on the captured runtime
|
and stay in `archivedIds`, while every ID not confirmed on the captured runtime
|
||||||
is returned in `failedIds` so existing partial-failure feedback stays truthful.
|
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
|
## The golden rule
|
||||||
|
|
||||||
|
|||||||
@@ -448,7 +448,7 @@ describe("confirmed session removal", () => {
|
|||||||
const { archiveSession, setActionRefs } = await import("./session-actions")
|
const { archiveSession, setActionRefs } = await import("./session-actions")
|
||||||
setActionRefs(mockSdk as unknown as OpencodeClient, createChildStores([["/test/project", source]]), () => "/test/project")
|
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")
|
expect(getRuntimeKey()).toBe("archive-runtime-b")
|
||||||
// The stale response must not reconcile the runtime the user switched to.
|
// The stale response must not reconcile the runtime the user switched to.
|
||||||
expect(source.getState().session.map((item) => item.id)).toEqual(["session-a"])
|
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")
|
const { archiveSessions, setActionRefs } = await import("./session-actions")
|
||||||
setActionRefs(mockSdk as unknown as OpencodeClient, createChildStores([["/test/project", source]]), () => "/test/project")
|
setActionRefs(mockSdk as unknown as OpencodeClient, createChildStores([["/test/project", source]]), () => "/test/project")
|
||||||
|
|
||||||
const result = await archiveSessions(["session-a", "session-b", "session-c"], {
|
const result = await archiveSessions(["session-a", "session-b", "session-c"])
|
||||||
expectedRuntimeKey: "archive-batch-a",
|
|
||||||
})
|
|
||||||
|
|
||||||
// session-a was confirmed before the switch and stays archived; session-b's
|
// 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
|
// response is stale and session-c is never attempted, so both are reported
|
||||||
|
|||||||
@@ -827,15 +827,16 @@ export async function deleteSessionInDirectory(sessionId: string, directory: str
|
|||||||
/**
|
/**
|
||||||
* Archive one session.
|
* Archive one session.
|
||||||
*
|
*
|
||||||
* `expectedRuntimeKey` is the runtime key the caller captured when the user
|
* `expectedRuntimeKey` defaults to the active runtime when the action starts.
|
||||||
* confirmed the operation. When it is supplied and the runtime changes, the
|
* Callers may supply a key captured earlier when confirmation spans a runtime
|
||||||
* action stops and returns `false` without reconciling any store, so a response
|
* 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
|
* 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
|
* 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
|
* stays archived on that runtime and is re-read from the server the next time
|
||||||
* the runtime is loaded.
|
* 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
|
if (isStaleRuntime(expectedRuntimeKey)) return false
|
||||||
const sessionDirectory = getSessionDirectory(sessionId)
|
const sessionDirectory = getSessionDirectory(sessionId)
|
||||||
const archivedAt = Date.now()
|
const archivedAt = Date.now()
|
||||||
@@ -884,7 +885,7 @@ export async function archiveSessions(
|
|||||||
): Promise<{ archivedIds: string[]; failedIds: string[] }> {
|
): Promise<{ archivedIds: string[]; failedIds: string[] }> {
|
||||||
const archivedIds: string[] = []
|
const archivedIds: string[] = []
|
||||||
const failedIds: string[] = []
|
const failedIds: string[] = []
|
||||||
const expectedRuntimeKey = options?.expectedRuntimeKey
|
const expectedRuntimeKey = options?.expectedRuntimeKey ?? getRuntimeKey()
|
||||||
|
|
||||||
for (const [index, id] of ids.entries()) {
|
for (const [index, id] of ids.entries()) {
|
||||||
if (isStaleRuntime(expectedRuntimeKey)) {
|
if (isStaleRuntime(expectedRuntimeKey)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user