Repository navigation
Removing a session should require it to still be archived #3050
Description
Correction (2026-08-15): the original analysis was wrong about where the atomicity is missing. The Host already decides this in one step; the defect is in the desktop client's retry loop. Rewritten accordingly.
Problem
Deleting a task that is concurrently restored — from a second window or another client — deletes it permanently anyway, and the caller is told it was removed.
Found by @M4n5ter reviewing #2985, reproduced deterministically by restoring the second task after the first removal completed.
What already works
The compare-and-set that should prevent this is present end to end:
session-retirement-coordinator.ts#removecomparestarget.revision !== input.expectedRevisioninside the#withStableFamilyadmission lock and returnsrevision_conflict.sqlite-session-metadata-store.tsremoveVersionedre-checksmetadataVersioninside the same SQL transaction as theDELETE.setLifecycleVersionedwrites throughupdateHeaderSync, so unarchiving bumpsmetadataVersion.
A concurrent restore therefore makes the first remove fail. Runtime Host is not missing a precondition, and session.remove does not need a new one.
Where it breaks
apps/desktop/src/main/runtime-host-client.ts, removeSession:
for (let attempt = 0; attempt < MAX_SESSION_REVISION_ATTEMPTS; attempt += 1) {
const current = await this.#requireSession(sessionId);
const result = await this.request("session.remove", { sessionId, expectedRevision: current.revision });
if (result.kind === "removed") return;
}The conflict is treated as a stale read and the delete is replayed with the fresh revision, up to 8 times. Replay is right for rename and configuration writes — the write means the same thing at any revision. It is wrong for remove: the conflict is precisely the signal that the task was touched after the caller decided to destroy it, and the replay carries out a destruction whose premise no longer holds. The loop also widens the window from "between two calls" to "the whole retry".
Direction
Lifecycle writes always bump metadataVersion, and the version check and the DELETE share one transaction. So "read as archived at revision R" plus "removed with expectedRevision R" implies "still archived when deleted" — the loop only has to re-assert the precondition on each re-read, not carry a new one over the wire.
removeSession should stop replaying blindly: on each attempt, give up if the re-read session is no longer archived, and return an outcome distinguishable from a failure. purgeSessions then reports "restored meanwhile" separately from "failed", counting it as neither removed nor errored.
This stays in apps/desktop, outside the Runtime Host protected area. The earlier proposal — an expected lifecycle on session.remove, validated by the retirement coordinator — is over-built given the CAS already there, and is dropped.
Open question: whether the single-row delete path, which shares removeSessionFamily, wants the same treatment or is adequately covered by its own confirm.
Impact
Permanent data loss, but it needs concurrent activity: the purge page disables its own controls while a sweep runs, so the trigger is a second window or another client. Not blocking anything currently in flight.
Activity
feat(desktop): add an archived tasks page to settings (#2985)
fix(desktop): settle every swept id against the delete, not a snapshot
1625567290 commentedon Aug 15, 2026
I'm working on this. I'll stop
removeSessionfrom replaying a delete after a revision conflict when the re-read session is no longer archived, return that as a distinctrestoredoutcome, and havepurgeSessionscount it separately from a failure.Grok is assisting with the implementation; I will review the change and remain the human contributor of record.
简体中文对照
我来修。
removeSession在 revision conflict 之后若重读发现任务已不再 archived,就不再用新 revision 重放删除;返回可区分的restored,purgeSessions把它和失败分开计数。Grok 协助实现;我会审阅改动,并作为 human contributor of record。
M4n5ter commentedon Aug 15, 2026
@1625567290 Thanks for offering to help. This is already being implemented in #3056, which covers the same
removeSessionconflict handling, distinctrestoredoutcome, andpurgeSessionsreporting. Please coordinate on the existing PR rather than opening a duplicate one.1625567290 commentedon Aug 15, 2026
Got it — standing down here. Closed #3068 in favor of #3056.
fix(desktop): keep a remove from destroying a restored task
fix(desktop): settle every swept id against the delete, not a snapshot
fix(desktop): keep a remove from destroying a restored task
fix(desktop): settle every swept id against the delete, not a snapshot
fix(desktop): keep a remove from destroying a restored task (apache#3056