Repository navigation
fix: stop notification loss, credentialed cache reuse, and clipboard clobber - #11230
Conversation
📝 WalkthroughWalkthroughThe changes add quarantine-aware notification catch-up bookkeeping, including clamped watermark requests, replay deduplication, failure handling, and tests for retries, rollback, delivery failures, and interrupted draining. Attachment image caching now uses global and per-site epochs to prevent cleared in-flight loads from repopulating the cache. Clipboard fallback copying now stops immediate propagation, with updated event mocks and tests covering terminal and window-level copy handlers. 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
mobile/src/notifications/mobile-notifications.ts (1)
172-197: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winA throwing replay rejects
fetchMissedinto an uncaught promise.
deliverMissedEventcan reject (a failingshowLocalNotification). Thefinallyquarantines correctly, but the rejection propagates out of the queued task and throughawait enqueueHostDelivery(...), sofetchMissed()rejects. Its only caller is the un-caughtvoid (async () => { ... await fetchMissed() })()at Line 240, producing an unhandled rejection on every failed replay.🛡️ Proposed fix: contain the replay failure at the queue boundary
- await enqueueHostDelivery(session, async () => { + await enqueueHostDelivery(session, async () => { // Advances only past events this batch settled, so a teardown or a failing show // quarantines the true contiguous point instead of the range it never reached. let contiguousSeq = askFrom let drained = false try { for (const raw of missed) { // Re-checked per event: the batch can start before a teardown and still be // draining after it, and a torn-down host must stop pushing. if (disposed) { return } const event = raw as NotificationEvent | DismissNotificationEvent await deliverMissedEvent(event) contiguousSeq = event.notificationSeq ?? contiguousSeq } drained = true } finally { if (drained) { resolveCatchUpQuarantine(session, hostId) } else { quarantineCatchUpWatermark(session, hostId, contiguousSeq) } } - }) + }).catch(() => { + // Quarantine already recorded the contiguous point; the failure must not escape + // into the un-awaited 'ready' continuation. + })
🧹 Nitpick comments (4)
src/main/jira/attachment-image-cache.ts (1)
18-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAvoid repeating the epoch walkthrough in comments.
src/main/jira/attachment-image-cache.ts#L18-L20: reduce this to a one-line rationale for the monotonic ticker.src/main/jira/attachment-image-cache.test.ts#L63-L66: remove the detailed collision narrative; the test name and setup already communicate the regression.As per coding guidelines, “Comments must be concise, limited to non-obvious information, and preferably one line; do not explain obvious code or walk through implementation details.”
Source: Coding guidelines
src/renderer/src/web/web-clipboard-copy-fallback.ts (1)
14-17: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCondense the propagation comments. Keep only the non-obvious rationale in a short single-line comment.
src/renderer/src/web/web-clipboard-copy-fallback.ts#L14-L17: reduce to one concise explanation of the required bubble-phase ordering.src/renderer/src/web/web-clipboard-copy-terminal-selection.test.ts#L5-L6: shorten the real-DOM rationale.src/renderer/src/web/web-clipboard-copy-terminal-selection.test.ts#L25-L28: replace the multi-line JSDoc with a concise helper description.src/renderer/src/web/web-clipboard-copy-terminal-selection.test.ts#L40-L43: replace the multi-line JSDoc with a concise helper description.src/renderer/src/web/web-clipboard-copy-terminal-selection.test.ts#L64-L68: reduce the historical implementation walkthrough to the test’s essential setup rationale.src/renderer/src/web/web-clipboard-copy-terminal-selection.test.ts#L78-L79: shorten the propagation-order explanation.Source: Coding guidelines
mobile/src/notifications/notification-catchup-failure-quarantine.test.ts (2)
159-163: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider an exact assertion on the replayed titles.
arrayContainingalso passes ifm6/m7are shown twice, which is the adjacent double-push regression theseenguard exists to prevent.toEqual(['m6', 'm7'])pins both presence and dedup.♻️ Tighter assertion
- expect(titles).toEqual(expect.arrayContaining(['m6', 'm7'])) + expect(titles).toEqual(['m6', 'm7'])
222-236: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for a rejecting show, and assert the recovered titles.
The teardown case aborts before
deliverMissedEvent(7)runs, so seqs 7-8 are never marked seen and replay cleanly. The sibling case —showLocalNotificationrejecting mid-batch — takes a different path through theseenbookkeeping flagged inmobile-notifications.ts(Line 113-129) and is untested. Also assertm7/m8were actually shown here, not just that the watermark reached 20.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 47a8b82e-fad7-4e4d-9b0a-187de3278f06
📥 Commits
Reviewing files that changed from the base of the PR and between 930ff96 and 3dc5149cbfd236bca78a58d699fc4028d5039d05.
📒 Files selected for processing (9)
mobile/src/notifications/mobile-notifications.tsmobile/src/notifications/notification-catchup-failure-quarantine.test.tsmobile/src/notifications/notification-reconnect-catchup.tssrc/main/jira/attachment-image-cache.test.tssrc/main/jira/attachment-image-cache.tssrc/renderer/src/web/web-clipboard-copy-fallback.test.tssrc/renderer/src/web/web-clipboard-copy-fallback.tssrc/renderer/src/web/web-clipboard-copy-terminal-selection.test.tssrc/renderer/src/web/web-preload-api.test.ts
Greptile SummaryThis PR fixes three independent data-loss and data-leak defects introduced by earlier patches: mobile notification loss when a catch-up RPC fails, Jira attachment cache re-population with post-revocation credentials, and the web clipboard copy fallback being overwritten by the xterm terminal selection.
Confidence Score: 5/5All three changes are narrowly scoped to the defective paths, backed by regression tests that fail on the original code, and leave the surrounding serialization and dedup logic intact. Each fix addresses a clearly diagnosed, well-tested data-loss or data-leak path. The quarantine logic in the mobile notification path is the most involved change, but it is well-commented, all edge cases are covered by the new test suite, and the known gaps are plainly documented. Files Needing Attention: No files require special attention; the most complex logic is in mobile-notifications.ts and notification-reconnect-catchup.ts, both of which are thoroughly tested by the new quarantine test file.
|
| Filename | Overview |
|---|---|
| mobile/src/notifications/mobile-notifications.ts | Core notification delivery reworked: deliverMissedEvent extracted, seen now marked after show succeeds, batch loop tracks contiguousSeq for fine-grained quarantine, and fetchMissed returns null on failure instead of silently returning an empty array |
| mobile/src/notifications/notification-reconnect-catchup.ts | Adds catchUpQuarantineSeq to HostNotificationSession plus three new functions: quarantineCatchUpWatermark, resolveCatchUpQuarantine, and catchUpWatermarkSeq; adoptNotificationEpoch also resets the quarantine when the desktop counter changes |
| src/main/jira/attachment-image-cache.ts | Replaces the summed siteEpoch + cacheEpoch staleness guard with a single monotonic ticker via Math.max, preventing the sum-collision where a per-site clear plus a global clear produced the same integer |
| src/renderer/src/web/web-clipboard-copy-fallback.ts | Switches onCopy from capture to bubble phase and adds stopImmediatePropagation(), so the document-level handler wins over xterm's bubble handler and any subsequent window-level listeners |
| mobile/src/notifications/notification-catchup-failure-quarantine.test.ts | New test file with 5 cases covering quarantine across multiple failed catch-ups, watermark rollback after an in-flight live write, teardown-induced partial quarantine, and mark-after-success for both replay and live paths |
| src/renderer/src/web/web-clipboard-copy-terminal-selection.test.ts | New test file using a real happy-dom environment with genuine capture/bubble propagation; covers the xterm overwrite scenario and the stopImmediatePropagation window-handler scenario |
| src/main/jira/attachment-image-cache.test.ts | Two new test cases: the sum-collision defect and a regression guard ensuring loads with no intervening clear are still cached |
| src/renderer/src/web/web-clipboard-copy-fallback.test.ts | Adds stopImmediatePropagation stub to the fake ClipboardEvent so existing tests do not throw; no assertions changed |
| src/renderer/src/web/web-preload-api.test.ts | Same stopImmediatePropagation stub added to the shared document-helper fake event; minimal change, no assertions removed |
Sequence Diagram
sequenceDiagram
participant Desktop
participant Session
participant Queue
participant Storage
Note over Session: Quarantine: null, lastDeliveredSeq: 5
Desktop->>Session: ready (epoch-1)
Session->>Desktop: getMissedSince(lastSeenSeq:5)
Desktop-->>Session: FAIL (socket closed)
Session->>Session: "quarantineCatchUpWatermark(askFrom=5)"
Note over Session: Quarantine: 5
Session->>Storage: saveWatermark(seq:5) rollback
Desktop->>Queue: live event seq:11
Queue->>Storage: saveWatermark(seq:5) clamped by quarantine
Desktop->>Session: ready (epoch-1)
Session->>Desktop: getMissedSince(lastSeenSeq:5)
Desktop-->>Session: [seq:6, seq:7]
Session->>Queue: enqueueHostDelivery(batch)
Queue->>Queue: deliverMissedEvent(seq:6) show succeeds, seen.add
Queue->>Queue: deliverMissedEvent(seq:7) show succeeds, seen.add
Queue->>Session: resolveCatchUpQuarantine()
Note over Session: Quarantine: null
Session->>Storage: saveWatermark(seq:11) full watermark restored
Reviews (3): Last reviewed commit: "test(web-clipboard): pin stopImmediatePr..." | Re-trigger Greptile
…clobber Mobile catch-up (stablyai#8591): fetchMissed swallowed the RPC failure while deliverLive kept advancing and persisting lastDeliveredSeq, so the next successful catch-up asked from above the abandoned range and the desktop cut it. Sessions are module-scope, so an unchanged epoch never resets it. Quarantine the watermark at the last contiguously-delivered seq and hold it there until some later catch-up actually drains — not just one retry. A batch cut short by a teardown quarantines at the last event it settled. Jira attachment cache: currentEpoch summed two independent counters, so a site at siteEpoch 1 read the same value before and after a global clear. The mid-flight guard passed and re-inserted credentialed image bytes that "disconnect all" had just purged — resident for the process lifetime since pruneExpired has no timer. One monotonic ticker, compared by max. Web copy fallback: the handler registered in the capture phase, so xterm's bubble-phase listener overwrote text/plain with the terminal selection afterwards; served was already true, so the copy reported success. Every Orca copy affordance over plain HTTP (Copy Pane ID, Copy Path, commit SHA, PR URL) pasted the terminal selection. Bubble phase with stopImmediatePropagation. Covers the secure-context retry branch too, which shares the same helper.
…pImmediatePropagation Adversarial review of a98d7f4d5d found two gaps. 1. The quarantine clamped only writes made AFTER the failure. getMissedSince waits up to 30s, so a live event routinely persists a higher seq while the request is still outstanding; that value stayed on disk, and the next launch read it back and resumed past the abandoned range -- the original bug, reached through a restart. quarantineCatchUpWatermark now re-persists the clamped seq, so the stored value never outlives the gap it guards. 2. web-clipboard-copy-terminal-selection's second test registered its "late" document handler BEFORE the fallback's, so it lost on registration order alone and stopImmediatePropagation was never exercised -- the test passed with that line deleted. Bubbling reaches the document before the window, so a window-level listener is what actually requires it.
A pre-marked seen key made a rejected show unrecoverable: the next catch-up re-fetched the seq and the dedup guard dropped it, and the first later event to drain the batch lifted the quarantine past it. Also contains the rejection so it does not escape the un-awaited 'ready'/live handlers as an unhandled rejection. Co-authored-by: Orca <help@stably.ai>
3dc5149 to
5b33a6c
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
mobile/src/notifications/notification-catchup-failure-quarantine.test.ts (1)
21-22: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueImport the watermark key builder instead of duplicating the key.
WATERMARK_STORAGE_KEY_PREFIXandwatermarkStorageKey()live innotification-reconnect-catchup.ts; exporting the builder from there would avoid this test hard-coding an internal key format.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 7f13a6d8-cf52-4477-bd1c-942bd852290d
📥 Commits
Reviewing files that changed from the base of the PR and between 3dc5149cbfd236bca78a58d699fc4028d5039d05 and 5b33a6c.
📒 Files selected for processing (9)
mobile/src/notifications/mobile-notifications.tsmobile/src/notifications/notification-catchup-failure-quarantine.test.tsmobile/src/notifications/notification-reconnect-catchup.tssrc/main/jira/attachment-image-cache.test.tssrc/main/jira/attachment-image-cache.tssrc/renderer/src/web/web-clipboard-copy-fallback.test.tssrc/renderer/src/web/web-clipboard-copy-fallback.tssrc/renderer/src/web/web-clipboard-copy-terminal-selection.test.tssrc/renderer/src/web/web-preload-api.test.ts
🚧 Files skipped from review as they are similar to previous changes (7)
- src/renderer/src/web/web-clipboard-copy-fallback.test.ts
- src/renderer/src/web/web-clipboard-copy-fallback.ts
- src/main/jira/attachment-image-cache.test.ts
- mobile/src/notifications/notification-reconnect-catchup.ts
- src/main/jira/attachment-image-cache.ts
- src/renderer/src/web/web-preload-api.test.ts
- mobile/src/notifications/mobile-notifications.ts
…handler Both existing cases passed with plain stopPropagation, and with the listener back in the capture phase — neither half of the fix was actually pinned. The window-level clobber is on a different target, so stopPropagation suppresses it too. Registering the clobber on the document, ordered after the fallback's own listener, is the only shape stopPropagation cannot cover. Addresses the review comment posted after the last commit. Co-authored-by: Orca <help@stably.ai>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/renderer/src/web/web-clipboard-copy-terminal-selection.test.ts (1)
43-61: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winRestore the
document.execCommandstub after each test.
stubExecCommandreplaces the global method, but the current cleanup only clearsdocument.body. Restore the original implementation to prevent order-dependent tests.Suggested cleanup
+const originalExecCommand = document.execCommand + afterEach(() => { document.body.innerHTML = '' + document.execCommand = originalExecCommand })
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 421036d0-b77e-425d-bcda-9a722eab9dac
📒 Files selected for processing (1)
src/renderer/src/web/web-clipboard-copy-terminal-selection.test.ts
…clobber (stablyai#11230) * fix: stop notification loss, credentialed cache reuse, and clipboard clobber Mobile catch-up (stablyai#8591): fetchMissed swallowed the RPC failure while deliverLive kept advancing and persisting lastDeliveredSeq, so the next successful catch-up asked from above the abandoned range and the desktop cut it. Sessions are module-scope, so an unchanged epoch never resets it. Quarantine the watermark at the last contiguously-delivered seq and hold it there until some later catch-up actually drains — not just one retry. A batch cut short by a teardown quarantines at the last event it settled. Jira attachment cache: currentEpoch summed two independent counters, so a site at siteEpoch 1 read the same value before and after a global clear. The mid-flight guard passed and re-inserted credentialed image bytes that "disconnect all" had just purged — resident for the process lifetime since pruneExpired has no timer. One monotonic ticker, compared by max. Web copy fallback: the handler registered in the capture phase, so xterm's bubble-phase listener overwrote text/plain with the terminal selection afterwards; served was already true, so the copy reported success. Every Orca copy affordance over plain HTTP (Copy Pane ID, Copy Path, commit SHA, PR URL) pasted the terminal selection. Bubble phase with stopImmediatePropagation. Covers the secure-context retry branch too, which shares the same helper. * fix: roll back the persisted watermark on catch-up failure; cover stopImmediatePropagation Adversarial review of a98d7f4d5d found two gaps. 1. The quarantine clamped only writes made AFTER the failure. getMissedSince waits up to 30s, so a live event routinely persists a higher seq while the request is still outstanding; that value stayed on disk, and the next launch read it back and resumed past the abandoned range -- the original bug, reached through a restart. quarantineCatchUpWatermark now re-persists the clamped seq, so the stored value never outlives the gap it guards. 2. web-clipboard-copy-terminal-selection's second test registered its "late" document handler BEFORE the fallback's, so it lost on registration order alone and stopImmediatePropagation was never exercised -- the test passed with that line deleted. Bubbling reaches the document before the window, so a window-level listener is what actually requires it. * fix(mobile): mark a notification seen only once its show lands A pre-marked seen key made a rejected show unrecoverable: the next catch-up re-fetched the seq and the dedup guard dropped it, and the first later event to drain the batch lifted the quarantine past it. Also contains the rejection so it does not escape the un-awaited 'ready'/live handlers as an unhandled rejection. Co-authored-by: Orca <help@stably.ai> * test(web-clipboard): pin stopImmediatePropagation with a same-target handler Both existing cases passed with plain stopPropagation, and with the listener back in the capture phase — neither half of the fix was actually pinned. The window-level clobber is on a different target, so stopPropagation suppresses it too. Registering the clobber on the document, ordered after the fallback's own listener, is the only shape stopPropagation cannot cover. Addresses the review comment posted after the last commit. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
What
Three independent data-loss/data-leak defects, each in code added by an earlier fix:
mobile/src/notifications/mobile-notifications.ts:123onmain, from fix(mobile): deliver the notifications a reconnect missed, and never persist a watermark past them #10816).fetchMissedcollapsed an RPC failure into an empty batch (.catch(() => [])), so an abandoned catch-up looked identical to "nothing missed". Live delivery kept advancing and persistinglastDeliveredSeq, and because the desktop cuts byseq > lastSeenSeq, the next catch-up asked from above the abandoned range — those notifications were never replayed. A per-session quarantine now freezes the persisted watermark at the seq the failed catch-up asked from until some later catch-up actually drains. Review then surfaced a second half of the same loss:seenwas marked before delivery, so a rejectedshowLocalNotificationleft the key behind and every later replay of that event was dropped as a duplicate — the quarantine held the range, but the notification inside it could never be re-shown.seenis now marked after the show lands, on the same rule as the watermark.src/main/jira/attachment-image-cache.ts:26onmain, from fix(jira): render issue description/comment images with lightbox #8938). The staleness guard comparedsiteEpoch + cacheEpoch, so distinct clear states could sum to the same number (site at 1 before a global clear equals global 1 after it). An in-flight download that spanned a per-site clear followed by "Disconnect all" passed the guard and re-inserted image bytes fetched with a now-revoked token. Replaced with one monotonic ticker read viaMath.max, so distinct clear states can never be equal.src/renderer/src/web/web-clipboard-copy-fallback.ts:18onmain, from fix(terminal): make copy work in the HTTP web client #10534). TheexecCommandfallback registered itscopylistener in the capture phase; xterm's own handler is bubble-phase onterminal.elementand overwrotetext/plainwith the terminal selection afterwards (preventDefaultdoes not stop propagation). Switched to bubble phase plusstopImmediatePropagation().ELI5
Three separate things could quietly hand you the wrong data. On your phone, if the app lost its connection at the exact moment it was asking the desktop "what did I miss?", it would give up on that batch but still move its bookmark forward — so those notifications were skipped for good and you never learned about them. In the Jira panel, an image that was still downloading when you disconnected your account could land in the cache anyway, meaning pictures fetched with credentials you had just revoked stayed in memory and could be shown again. And in the browser version of Orca, clicking something like "Copy Path" while text was highlighted in a terminal would copy the highlighted terminal text instead — the app said "Copied!", but pasting gave you whatever was selected in the terminal. There was also a subtler version of the phone problem: the app ticked a notification off its "already handled" list the instant it started showing it, so if showing it then failed, the app spent forever re-asking for a notification it would keep throwing away as a repeat. Now the phone keeps asking from the point it actually stopped at until it truly catches up, only ticks a notification off once it has really appeared, the disconnected account's images are dropped, and copy copies what you asked for.
Proof of fix
1. Mobile notification loss (
mobile/, vitest)Source files reverted to
origin/main, new test kept at HEAD:The two
Errorsare the unhandled rejections the escaping show produced (vitest exits 1 on them);both queue boundaries now contain it.
With the fix:
2 & 3. Jira cache and web clipboard (desktop vitest)
Source files reverted to
origin/main, new tests kept at HEAD:With the fix:
Clipboard: visual before/after
Read these two shots with the caveat below. They are real captures, but of a purpose-built page, not the shipped Orca web client. What is genuine: real Chromium driven by Playwright, real
@xterm/xtermfrom this repo'snode_modules, a genuinely insecure origin (served over plain HTTP on a LAN IP, withnavigator.clipboard === undefinedrendered on the page —127.0.0.1would be a secure context and would not exercise this path), a real mouse-drag selection via CDP, the unmodifiedcopyClipboardTextViaExecCommandimported fromsrc/, and a realexecCommand('copy')followed by a real paste. The two runs differ only bygit stash/git stash popofweb-clipboard-copy-fallback.ts: same viewport, same URL, same drag coordinates, same click sequence, one shared capture script. The diagnostics block is identical in both shots (xterm hasSelection: true,dom selection anchor: xterm-helpers,dom selection inside terminal: true,copy reported: true), so the only delta is the pasted text. The harness was deleted before committing and is not in this diff.Proof of no regression
New regression tests
mobile/src/notifications/notification-catchup-failure-quarantine.test.ts— 5 cases. Fails on parent (5 failed + 2 unhandled rejections), passes with the fix (5 passed, 0 errors); output above. The last two are the review finding: one drives a rejecting show inside a replay batch, the other a rejecting show on the pure live path with no catch-up failure at all. It drives the realsubscribeToDesktopNotificationsacross two subscription lifetimes (matching theapp/index.tsxteardown/resubscribe cycle) with a mockedRpcClientand AsyncStorage, and asserts the persisted watermark directly.src/main/jira/attachment-image-cache.test.ts— addeddoes not repopulate after "disconnect all" when the site was cleared before(the defect) plusstill caches a load that spans no clear at all, which guards against the degenerate "never cache anything" fix.src/renderer/src/web/web-clipboard-copy-terminal-selection.test.ts— 3 cases. Corrected at the merge gate: the original 2 cases did not pin either half of the fix as this section previously claimed. Re-measured against all four phase/stop combinations, both passed with the listener still in the capture phase, and both passed with plainstopPropagation— the window-level clobber sits on a different target, whichstopPropagationalso suppresses. Addedwins over a later document-level copy handler, which registers the clobber on the same target (document) ordered after the fallback's own listener: it fails withstopPropagation(expected 'later document handler' to be 'pane-42') and passes only withstopImmediatePropagation. All 3 fail against the unfixedorigin/mainsource. The bubble-phase change itself remains unpinned by any test — it is load-bearing in the real DOM (xterm's handler is onterminal.element, a descendant the capture phase reaches before), but the happy-dom harness dispatches from that same element, so capture and bubble are indistinguishable there. Stated rather than claimed as covered.Existing tests over the touched area (real commands, real results, at HEAD):
src/transportis included becausehost-removal-lifecycle.test.tsasserts onHostNotificationSession.lastDeliveredSeq.Typecheck and lint
Deliberately not changed
pruneExpiredin the Jira cache still has no TTL sweep timer, so entries only expire when Jira activity touches the cache. That is why the leak was permanent, but it is not the defect this PR fixes; adding a timer is a separate change. The "no TTL sweep without activity" property is unchanged.enqueueHostDelivery. Moving it in would serialize a request that waits up to 30s ahead of live delivery, and it would not fix the watermark problem — the bug is which seq gets persisted, not ordering.session.lastDeliveredSeqstill advances on live delivery; only the persisted value is clamped. The quarantine changes what reaches AsyncStorage, not in-memory dedup.seenkey. Rollback needs a "did I add this key or was it already here" distinction to avoid deleting a key an earlier successful delivery owns, and it leaves the key present during the await — the exact window a concurrent replay of the same id consults. Marking after the await gives one rule that already governs the watermark two lines below: both advance only once the event reached the user. In-batch pre-marking is not needed, since the batch is a single queue entry and no second replay of a key can interleave within it.deliverLivetail.dismissLocalNotificationswallows its own OS error, so a rejecting dismiss is not reachable through a path a test can drive — that branch is fixed by construction rather than by a dedicated case.web-clipboard-copy-fallback.test.tsand the shared document helper inweb-preload-api.test.ts) gained astopImmediatePropagationstub — a property every realClipboardEventhas. No original assertion was removed or weakened.Known gaps, stated plainly
saveWatermarkcomment already documents, and it is preferred over silent loss — but it is a real behavior change and there is no test pinning that duplicate-on-restart case.contiguousSeqadvances after each replayed event returns, including events skipped as already-seen or already-queued. If a skipped event preceded a later failing one in the same batch, the quarantine point could sit above it. Now thatseenis only marked after a show lands, an already-seen skip provably means the event reached the user — but the already-queued skip still rests on the claim being released only after its own delivery settles, and that composition is not pinned by a test.seenafter the show makes the failed event re-deliverable, but nothing re-fetches on its own — recovery still waits for the nextready. On a stable connection that may be a long time. Reducing that latency (an explicit retry) is deliberately out of scope.copylisteners insrc/were checked (there are none besides this one), and Monaco's sole copy listener is scoped to its own textarea and does not stop propagation. Libraries beyond xterm and Monaco were not swept for document-level copy listeners thatstopImmediatePropagationmight now pre-empt.ab2e89be3c; therolls back a watermark a live event stored while the catch-up was in flighttest covers it.windowand would also pass understopPropagation. It landed after the previous head commit; verified correct and addressed inc075397c47.seenwas marked before delivery, so a rejected show could never be re-shown. Confirmed and fixed in5b33a6cd1c. The suggested diff (drop the pre-mark indeliverMissedEvent) was not sufficient on its own:deliverLivemarked the key itself before its await, so the identical hole existed on the live path with no catch-up involved. Both are now covered. The same commit contains the escaping rejection at both queue boundaries — the catch-up batch CodeRabbit flagged out-of-band, andqueueDelivery, where the live-path rejection escapes identically (pre-existing onmain).Rebase note
Rebased onto
747b241145(was branched atb41e813cb5). No conflicts, and no hunk was dropped:git log b41e813cb5..origin/main --overmobile/src/notifications/,src/main/jira/attachment-image-cache.tsandsrc/renderer/src/web/web-clipboard-copy-fallback.tsis empty, so no upstream commit in that range touches any file this PR changes and none of these three defects was fixed independently.Made with Orca 🐋