Summary
Account-session persistence is fire-and-forget and unsequenced. Multiple saves for the same session, and save/delete pairs, run as independent detached tasks with no revision or ordering token.
An older PUT can therefore complete after a newer PUT and roll the synced transcript backward. A delayed PUT can also race a DELETE and recreate a session the user removed, unless the server supplies an undocumented conflict guarantee.
Root cause
AppSessionSync.save() (apps/ios/Graff/Sources/GraffAccount.swift:293-304) snapshots the session and starts an independent Task.detached PUT. Saves are triggered from turn completion and new-session handoff. Deletion starts a separate detached task in SessionsListView.
There is no:
- per-session client serialization;
- cancellation/coalescing of obsolete saves;
- monotonic client revision;
- conditional request/version in
Gateway.putAppSession;
- ordering between PUT and DELETE.
Reproduction
- Delay the PUT produced after turn 1.
- Complete turn 2 and allow its newer PUT to finish.
- Release the delayed turn-1 PUT.
- Reload history and observe whether the older transcript won.
For the delete variant, delay a PUT, delete the row, then release the PUT and reload history.
Expected behavior
- Define and enforce a per-session ordering/version invariant on both client and gateway.
- Coalesce obsolete saves or reject stale revisions server-side.
- Ensure DELETE tombstones cannot be undone by an older in-flight PUT.
- Add deterministic delayed-response tests for save/save and save/delete ordering.
Summary
Account-session persistence is fire-and-forget and unsequenced. Multiple saves for the same session, and save/delete pairs, run as independent detached tasks with no revision or ordering token.
An older PUT can therefore complete after a newer PUT and roll the synced transcript backward. A delayed PUT can also race a DELETE and recreate a session the user removed, unless the server supplies an undocumented conflict guarantee.
Root cause
AppSessionSync.save()(apps/ios/Graff/Sources/GraffAccount.swift:293-304) snapshots the session and starts an independentTask.detachedPUT. Saves are triggered from turn completion and new-session handoff. Deletion starts a separate detached task inSessionsListView.There is no:
Gateway.putAppSession;Reproduction
For the delete variant, delay a PUT, delete the row, then release the PUT and reload history.
Expected behavior