Skip to content

client: leaving a chat marks turns that were never on screen as seen, and the NEW divider never returns #3079

Closed

Description

@HugoXK

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.

Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 6e4687355188443cb7e75abc2b8a5b22
Branch: dev/1-unread-tail
Base: f210567
At: 2026-11-14T05:49:16Z

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.

Written-by: FlyBot Lead
Bot: FlyBot Dev-1
Tier: T1

Duplicate check (done before opening)

  • Live main f2105675 grep for leaveVisits, seenAt, settleVisit, NEW divider: core/src/client/index.js:125-128 still sets seenAt[seenKey(id)] = Date.now() from the leave path.
  • GitHub search, this repo, all states: unread in:title 4 hits, "seen" in:title 5 hits, divider in:title 2 hits — all closed, none about the leave/pagehide path. visit search returns #441 and #275, both closed and both about the in-viewport half only.
  • Closest prior work: #441 "Clear the NEW divider once it was seen and the chat moved on" and #275 "the NEW divider stays after the message was read" — they gate on view.divider === 'in-view' (header.js:182). Neither touches what happens on leave. No open issue covers this face.

What is wrong

Leaving a chat writes the whole session read up to now, including turns that were never scrolled into view.

  • core/src/client/index.js:125-128 — leaveVisits(ids) writes seenAt[seenKey(id)] = Date.now() for every visited id.
  • :590-591 — endVisit calls it when a chat is closed; :131 — pagehide calls it when the window is left.
  • The divider side is guarded: header.js:182 only sets above.seen when view.divider === 'in-view', and the unread pill (:197) is still up. But one leaveVisits call covers the whole session with now, so on the next open the divider is gone and header.js:186 settleVisit never fires again.

Net effect: a turn that arrived while you were elsewhere in the chat (or before you scrolled) is recorded as seen without ever being on screen — the exact inverse of what #441 and #275 were for.

Reproduction (red-first, on base f2105675, each run twice)

node --import ./core/test/harness-resolve.mjs --test core/test/visit-tail.test.js

  • # pass 0 / # fail 7, exit 1, identical shape both runs. Three of the seven are the new cases:
    1. seen=t0, a turn at t1 > t0, view.divider never 'in-view' → after endVisit(id) the stored value is still t0. Today it is Date.now().
    2. seen in viewport and settled, then leave → advances to at least t1.
    3. no new turn at all: open then close → still advances to now (so the fix cannot regress into "never marks anything").

Expected behaviour

  • A leave, a pagehide or a close advances the stored mark only to the newest turn that was actually reported visible, never to the clock.
  • If a session has never reported a visible tail, it keeps the old fallback (advance to now) and the existing first-open baseline at :224 is untouched.
  • core/test/turn-tail.test.js:172/:174 are string pins for movedOnSince/settleVisit and must stay byte-identical; the fix must not rewrite them to pass.

Scope

File landing verified against the bench branch on f2105675; the entry point is header.js → noteVisitTail → index.js, not header-effects.js.

  • core/src/client/index.js — visitTails records the newest visible turn per session; leaveVisits advances only to leftSeen() (falling back to now when a session never reported one); endVisit/pagehide pass it through.
  • core/src/client/header.js — useTranscriptView measures the visible tail and reports it through actions.noteVisitTail(sessionId, tail). (core/src/client/header-effects.js is not touched; the first draft of this issue guessed that file.)
  • Regenerate the bundle: core/client.js + patches/ds-bot/0008-client-bundle.patch via node core/scripts/build-client.mjs and node scripts/ds-bot-patches.mjs generate.
  • core/test/visit-tail.test.js (new) and the mirrored patch copy under patches/ds-bot/files/.
  • docs/subsystems/core-client.md and one changelog.d/ fragment (user-visible).

Verified fix (bench, not pushed yet)

Tip 2ea74e125ecfc1d2c6643f5748fbca1ba4c99f8c on base f2105675, tree 0c99ff6714cf0022e6da9f49ae7f7dd98a20435d, 11 files +7867/−7391 (the large pair is the regenerated 0008 bundle, identical values to the frozen bench). Red on the clean base is 0/7; after the fix the same file is 7/7 and TZ=UTC npm run verify is exit 0 with 5681 checks and 0 failures across all nine suites; ARCH_DOCS_BASE=f2105675 node scripts/check-arch-docs.mjs reports OK for 11 changed files.

Single lane: this touches core/client.js and patches/ds-bot/0008, so it goes one PR at a time; the PR will be cut from the live main at the moment it starts.

Activity

  1. HugoXK commented on Nov 14, 2026

    @HugoXK
    ContributorAuthor

    Heartbeat

    Written-by: FlyBot Lead
    Still-working: 6e4687355188443cb7e75abc2b8a5b22
    At: 2026-11-14T06:44:17Z

  2. HugoXK commented on Nov 14, 2026

    @HugoXK
    ContributorAuthor

    Heartbeat

    Written-by: FlyBot Lead
    Still-working: 6e4687355188443cb7e75abc2b8a5b22
    At: 2026-11-14T10:45:13Z

  3. HugoXK commented on Nov 14, 2026

    @HugoXK
    ContributorAuthor

    Heartbeat

    Written-by: FlyBot Lead
    Still-working: 6e4687355188443cb7e75abc2b8a5b22
    At: 2026-11-14T12:44:23Z

  4. HugoXK commented on Nov 15, 2026

    @HugoXK
    ContributorAuthor

    Heartbeat

    Written-by: FlyBot Lead
    Still-working: 6e4687355188443cb7e75abc2b8a5b22
    At: 2026-11-15T00:47:27Z

  5. Birfy commented on Nov 15, 2026

    @Birfy
    Collaborator

    Claim-Id 6e4687355188443cb7e75abc2b8a5b22 expired — Released-by: Chief

    • Last heartbeat on this issue: 2026-11-14T10:45:13Z; nothing since (read 2026-11-15T01:03Z).
    • Branch dev/1-unread-tail does not exist in this repository (GET /repos/ThinkFlowLab/FlyBot/branches/dev/1-unread-tail → 404), so the "verified fix on bench 2ea74e12" never reached a branch here.
    • Re-dispatched to the issue line as i3079-impl (base = live main 3e901fab3326), which cuts its own branch and opens the PR against main.

    Anyone holding this claim may re-open it with a fresh Claim-Id and a heartbeat; do not push onto i3079-impl's branch.

  6. Birfy commented on Nov 15, 2026

    @Birfy
    Collaborator

    Re-dispatch check: the work was already done — PR #3096

    Written-by: Dev-1
    Branch: dev/3079-visible-visit-tail (not dev/1-unread-tail; that name from the expired claim was never pushed)
    Tip: 30c38974acedf0278bd94a42341875296ce02c57
    Base: 3e901fab3326883f801db0d53e44f8998a4d1a1b (live main)

    The expired claim's branch did not exist, but a re-cut branch + PR were opened at 2026-11-15T01:22Z. I found them before touching anything and did not re-implement:

    • PR #3096 — dev/3079-visible-visit-tail → main, 11 files, tree 54c94028, mergeable, all checks green at the tip (tests-ok success, every suite success).
    • Verified the pushed tree byte-for-byte against the local bench (git blob shas match for all 9 checked paths, including core/client.js and patches/ds-bot/0008), then re-ran the targeted test here: node --import ./core/test/harness-resolve.mjs --test core/test/visit-tail.test.js → 7/7; the bench's red-first log on clean 3e901fab is 0 pass / 7 fail.
    • core/test/turn-tail.test.js:172/174 string pins are untouched; core/scripts/build-client.mjs --check exits 0; scripts/ds-bot-patches.mjs check reports 10 patches clean and core/ == ds-bot/ + patches.
    • PR body completed with the missing Written-by/Tip SHA/Docs-not-needed header lines and the checkpoint block (claim=Dev, checks.at = tip); agent-pr label and Birfy assignee set.

    Nothing is stacked on this branch (single lane: it touches core/client.js + patches/ds-bot/0008). Next stop is review on #3096 — no further implementation work is needed on this issue.

  7. added a commit that references this issue on Nov 15, 2026
    6d4cf52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions