From 3949d025f582d79b2bc9fad0a67e50705af920e8 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Mon, 2 Nov 2026 05:11:27 -0500 Subject: [PATCH 1/2] syncsetup: an ownership check that finds another holder retires the send receipt first When CheckLease finds another computer holding the chat, Batcher.Displaced marks the batcher stale, and that mark already wakes renewal readers through endRenewals. The send receipt was only retired later, in becomeViewer, so a reader woken by that first notice still found a receipt for a hold another computer has (contract K2). The another-holder branch now retires the receipt it read before the check, after in-flight seals and before Displaced, in a small helper so CheckLease does not grow. --- internal/syncsetup/drive.go | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/internal/syncsetup/drive.go b/internal/syncsetup/drive.go index 749ebd9717..be0588dcd6 100644 --- a/internal/syncsetup/drive.go +++ b/internal/syncsetup/drive.go @@ -613,17 +613,7 @@ func (d *Drive) CheckLease(ctx context.Context) error { } if err == nil && (v.Cell.Lease.Expires <= v.Now || v.Cell.Lease.Device != d.sync.Device.ID() || v.Cell.Lease.Fence != d.batcher.Fence()) { if v.Cell.Lease.Device != d.sync.Device.ID() { - // Another computer holds the chat. Retire the hold at once so no - // new seal starts, wait for any seal already past Gate, and let the - // batcher keep unsent turns as a branch before the window names the - // new holder (#2091). The hold is retired quietly: becomeViewer - // announces the change once, with the viewer line already set. - d.mu.Lock() - d.lost = true - d.mu.Unlock() - d.sealMu.Lock() - d.sealMu.Unlock() - d.batcher.Displaced(ctx) + d.retireForAnotherHolder(ctx, held) } d.becomeViewer(cellsync.Superseded{By: v.Cell.Lease.Device}) } else if err == nil { @@ -632,6 +622,26 @@ func (d *Drive) CheckLease(ctx context.Context) error { return nil } +// retireForAnotherHolder retires this writer's hold when CheckLease finds +// another computer holding the chat. It retires the hold at once so no new +// seal starts, waits for any seal already past Gate, and lets the batcher keep +// unsent turns as a branch before the window names the new holder (#2091). +// The send receipt goes before the +// batcher marks itself stale, because that mark already wakes renewal readers +// and none of them may still find a receipt for a hold another computer has. +// The hold is retired quietly: becomeViewer announces the change once. +func (d *Drive) retireForAnotherHolder(ctx context.Context, held sendLease) { + d.mu.Lock() + d.lost = true + d.mu.Unlock() + d.sealMu.Lock() + d.sealMu.Unlock() + if err := d.sync.forgetSendLease(d.cell.ID, held); err != nil { + d.notice(err.Error()) + } + d.batcher.Displaced(ctx) +} + // sendLeaseSnapshot binds retirement to the receipt held before a relay reading, // so an older reading cannot retire a receipt recorded by a later reacquisition. func (d *Drive) sendLeaseSnapshot() sendLease { From 81faebc57bb78444accf338a0b928f65e6b67c33 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Mon, 2 Nov 2026 05:12:29 -0500 Subject: [PATCH 2/2] docs: change entry for this pull request --- docs/changes/unreleased/2180-watch-receipt.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/changes/unreleased/2180-watch-receipt.md diff --git a/docs/changes/unreleased/2180-watch-receipt.md b/docs/changes/unreleased/2180-watch-receipt.md new file mode 100644 index 0000000000..46443683dc --- /dev/null +++ b/docs/changes/unreleased/2180-watch-receipt.md @@ -0,0 +1,6 @@ +--- +kind: fixed +title: Another computer taking a chat retires this window's send receipt before any reader is told +pr: 2180 +surface: [chat, app] +---