mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 00:03:15 +00:00
fix(native-chat): a retraction forgets what is current, not the tombstones
The delete lost a fence the cursor cannot replace: the version cursor only compares within a lineage, so a delayed frame from an already-superseded epoch had nothing left to stop it putting a chat row back for a worktree the host no longer publishes. Keeping the record intact had the opposite fault — the renderer's own epoch is the history's current, so the next frame under any other epoch retired it. Clearing only current does neither: noteRetiredValue retires nothing when there is nothing current, and the tombstones stay. Each of the four shapes now fails a different test.
This commit is contained in:
@@ -168,6 +168,20 @@ describe('a revealed chat survives the frames the reveal provokes', () => {
|
||||
expect(chatTabIds(state)).toEqual([SESSION_TAB])
|
||||
})
|
||||
|
||||
it('still fences a delayed frame from an epoch that was already superseded', () => {
|
||||
// The retraction keeps its tombstones. The version cursor only fences within a lineage, so
|
||||
// without them a straggler under a long-dead epoch would put a chat row back on screen for a
|
||||
// worktree the host no longer publishes.
|
||||
let state = apply(baseState(), chatFrame('structured:old', 5))
|
||||
state = apply(state, chatFrame(RENDERER_EPOCH, 120)) // retires structured:old
|
||||
state = apply(state, emptyFrame(RENDERER_EPOCH, 121))
|
||||
state = apply(state, { ...emptyFrame('removed:abc', 0), removed: true } as never)
|
||||
|
||||
state = apply(state, chatFrame('structured:old', 6)) // straggler from the dead epoch
|
||||
|
||||
expect(chatTabIds(state)).toEqual([])
|
||||
})
|
||||
|
||||
it('still prunes the mirrored rows when the host retracts the worktree', () => {
|
||||
// The retraction must not merely stop fencing later frames — it has to take the rows with it,
|
||||
// or a worktree the host no longer publishes keeps a chat on screen that nothing backs.
|
||||
|
||||
@@ -114,11 +114,20 @@ export function applyLocalStructuredSessionTabSnapshots<
|
||||
// republication is never gated by it, while dropping it would leave a frame issued before
|
||||
// the close free to land afterwards and strand a row nothing republishes.
|
||||
//
|
||||
// The epoch history goes. Unlike the mainstream path — where the epochs belong to a remote
|
||||
// publisher and the history is a useful tombstone — here the consumer IS the publisher, and
|
||||
// `current` is the renderer's own lifetime epoch. Keep it, and the next frame under any
|
||||
// other epoch retires that one for good, which is this bug again one cycle later.
|
||||
localStructuredSessionEpochHistoryByWorktree.delete(snapshot.worktree)
|
||||
// The history keeps its tombstones but forgets what is current. Unlike the mainstream path —
|
||||
// where the epochs belong to a remote publisher — here the consumer IS the publisher, and
|
||||
// `current` is the renderer's own lifetime epoch: leave it set and the next frame under any
|
||||
// other epoch retires it for good, which is this bug again one cycle later. Clearing the
|
||||
// whole record would instead let a delayed frame from an already-superseded epoch back in,
|
||||
// because the version cursor only fences within a lineage. Dropping `current` alone does
|
||||
// neither: `noteRetiredValue` retires nothing when there is nothing current.
|
||||
const history = localStructuredSessionEpochHistoryByWorktree.get(snapshot.worktree)
|
||||
if (history) {
|
||||
localStructuredSessionEpochHistoryByWorktree.set(snapshot.worktree, {
|
||||
current: null,
|
||||
retired: history.retired
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
localStructuredSessionVersionByWorktree.set(snapshot.worktree, {
|
||||
|
||||
Reference in New Issue
Block a user