Skip to content

Unread dot comes back after every open when its saved watermark is newer than any saved row #19688

Description

@unstablebrainiac

Existing issues

  • I searched open issues and this is not already reported.

KiroCrew version

Found on main at de2d143. Seen on a 0.8.0-rc.1 build.

Release channel

Insider (prerelease)

How is it installed?

From source

Platform

Not platform-specific

What happened

One session's unread dot cannot be cleared. Opening the session clears it, and it lights again a moment later. Mark as read does the same. Every reload brings it back.

The dot's shared unread record (mc-unread-shared) holds a watermark newer than the session's last_ts. markSlotRead clears this window's badge but keeps the shared record, because last_ts does not cover it (dashboardSlice.ts L1346). The read is relayed at last_ts, and the gateway sends it back to every owner window, the sender included. In this window remoteSlotRead finds the shared watermark uncovered and lights the badge again (L1365-L1370). On reload, restoreUnreadBadges restores it from the same record (L288).

Builds before #15014 recorded such a watermark when a turn finished: last_ts was then the turn's unsaved done row, which a gateway restart drops. #15014 stops new ones, but a record saved before it stays in the browser's localStorage, and no read on main can cover it.

Expected: opening, or marking as read, a session with nothing in flight clears its dot for good.

Steps to reproduce

Saved under website/src/test/, this fails on main with expected [ 's1' ] to not include 's1':

import { it, expect } from 'vitest'
import { createTestStore } from './helpers'
import dashboardReducer, { sseConnected, sseSlots, markSlotRead, remoteSlotRead, restoreUnreadSince, restoreUnreadBadges } from '../store/dashboardSlice'
import type { RootState } from '../store'

it('a read of an idle session keeps its dot cleared', () => {
  // The record a build before #15014 left: the watermark is the turn's
  // unsaved `done` row, a few ms after the transcript tail.
  localStorage.setItem('mc-unread-shared', JSON.stringify({ s1: '2026-10-09T22:33:35.611204+00:00' }))
  const since = restoreUnreadSince()
  const store = createTestStore({ dashboard: {
    ...dashboardReducer(undefined, { type: '@@test/init' }),
    unreadSince: since, unreadSlots: restoreUnreadBadges(since),
  } as RootState['dashboard'] })
  store.dispatch(sseConnected())
  store.dispatch(sseSlots([{ key: 's1', messages: 166, running: false, last_ts: '2026-10-09T22:33:35.542839+00:00' }]))
  // What opening the session or Mark as read does: the local read, then the
  // gateway's rebroadcast of the relayed read reaching this window too.
  store.dispatch(markSlotRead('s1'))
  store.dispatch(remoteSlotRead({ slot: 's1', readTs: '2026-10-09T22:33:35.542839+00:00' }))
  expect(store.getState().dashboard.unreadSlots).not.toContain('s1')
})

In a browser: open a session whose dot came from a build before #15014 and has not cleared since the upgrade. The dot goes out, then comes back.

Activity

  1. added
    channel: insiderReported from an insider prerelease build — candidate release blocker
    on Oct 13, 2026
  2. akushonkamen commented on Oct 13, 2026

    @akushonkamen
    Contributor

    Confirmed the root cause and picking this up.

    In website/src/store/dashboardSlice.ts, markSlotRead (~L1345) only feeds slot.last_ts into clearSharedUnreadIfCovered, so a stale mc-unread-shared watermark written before #15014 (pointing at a row that was dropped on gateway restart) can never be covered by any read. remoteSlotRead (~L1365-1371) then adopts the surviving watermark back into unreadSince and re-lights the badge (echo fan-in), and restoreUnreadBadges (~L288-295) restores it from localStorage on every refresh — the dot never retires.

    Plan: retire the orphan shared watermark when the local slot is fully caught up (running: false), while preserving the lag-window protection, the MANUAL_UNREAD sentinel, and the newest-ts-wins persist semantics. Repro test first, then fix.

    Note: this investigation is assisted by an AI coding agent (Claude), supervised and verified by me.

  3. added
    tier:T1Bug: behaviour does not match intent; fix keeps the design
    on Oct 13, 2026
  4. KiroCrewCaptain commented on Oct 13, 2026

    @KiroCrewCaptain
    Collaborator

    Thanks for the report. This issue has been triaged by Crew Captain and will be assigned to a maintainer, who will follow up here.

  5. akushonkamen commented on Oct 13, 2026

    @akushonkamen
    Contributor

    PR is up: #19782 (rebased onto current main, fec23af). The issue's repro is red on main (2 failed | 1 passed, expected [ 's1' ] to not include 's1') and green with the fix (3 passed (3)); the running-slot lag-window protection case is included and stays green.

  6. added 3 commits that reference this issue on Oct 19, 2026
    469bfda
    bd2d0fd
    d41daa9
  7. dodgeblaster commented on Oct 20, 2026

    @dodgeblaster
    Contributor

    Triage (T1/T2 sweep). Confirmed this is a live T1 bug on current main (355dd9027f): markSlotRead never retires an uncovered orphan mc-unread-shared watermark, so a pre-#15014 record newer than every saved row re-lights the dot on every open and reload. Two open fork PRs fix it; recommending one.

    Recommend #19782 (akushonkamen). Surgical 2-file, +134/-4 change that matches this issue's own diagnosis and ships the issue's exact repro. It retires the orphan only on an idle slot with a proven live view (!running && connected && slotsLive), and slotsLive is cleared by connect/disconnect/HTTP-fetch so a reconnect gap or stale HTTP reply can't erase a sibling's newer watermark. Design/First-Principles/UX/Opus lanes PASS. Its only blocker is one GPT 6.1 BLOCKING residual (connected-but-lagging-tab race, security/data-loss class, worst case one lost badge) that needs a maintainer to adjudicate — the writer-side override path doesn't apply to a security-class finding.

    Not #19779 (unstablebrainiac) for this fix: it's a much larger 9-file, +1296/-38 change that introduces a permanent new browser protocol (mc-unread-orphaned / mc-unread-arrival). Its PR Readiness is green, but it carries a CHANGES_REQUESTED and is explicitly held for a maintainer product decision (keep the permanent compat layer vs a one-time versioned migration). That's a design call, not a defect — appropriate to decide separately, but heavier than this bug needs.

    (#20020 is a different bug — unread dots dropped when a restart serves a partial slot list — not this issue.)

    Both are cross-repo forks, so neither can be merged by the sweep (fork merge needs the author + a maintainer; no admin bypass). Next step is a maintainer: adjudicate the GPT residual on #19782 (preferred), or choose the #19779 protocol direction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

channel: insiderReported from an insider prerelease build — candidate release blockertier:T1Bug: behaviour does not match intent; fix keeps the design

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions