Skip to content

Show a PR marker once, at its first sighting, not on every re-stamp - #322

Merged
xerhab merged 1 commit into
mainfrom
pr-link-dedupe
Jul 27, 2026
Merged

Show a PR marker once, at its first sighting, not on every re-stamp#322
xerhab merged 1 commit into
mainfrom
pr-link-dedupe

Conversation

@xerhab

@xerhab xerhab commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

The bug

The chat repeated the same ↗ Opened PR #N marker over and over through a session — dozens of times for a single PR.

Cause

The marker (added in #317) treats a pr-link transcript entry as "a PR was opened here". It isn't an event.

Claude Code re-stamps a session's PR links inside the metadata preamble it writes at the top of every user turn — the same block that carries last-prompt, ai-title, mode, permission-mode:

773 last-prompt
774 ai-title
775 mode
776 permission-mode
777 pr-link      <- re-stamped, not a new event
778 bridge-session

So one PR produces ~6 entries differing only in timestamp. Across the corpus that's 1163 pr-link entries for 195 real PRs (6×) over 107 transcripts, with 66% sitting directly after permission-mode.

The existing fold only caught consecutive duplicates. The repeats are separated by whole turns, so nearly all of them slipped through and rendered a marker apiece.

Fix — both render paths

  • _entry_id / entryId key a pr-link on its URL alone, dropping the timestamp. The re-stamps re-record one fact, so sharing an id lets the live tail's id-keyed merge collapse them onto the first, and keeps the redundant copies out of the archive.
  • buildItems dedups by URL across the whole conversation instead of folding neighbours. This is the half that covers the archive / ended-session view, which calls buildItems on stored entries with no merge step at all.

Keeping the first sighting is deliberate and checked: across the corpus the first pr-link for a URL lands 1–6 entries before the gh pr create tool_result that opened it (Claude Code stamps the link the moment it sees the URL), so it marks where the PR genuinely landed in the conversation.

Verification

Corpus differential over the 107 transcripts containing pr-links, one snapshot for both sides:

old new
PR markers rendered 1154 195
distinct PRs 195 195
renders still holding a duplicate URL 17 0
non-PR display items differing 0
  • Marker order matches first-seen order exactly in every transcript.
  • Mirror parity: py vs js entryId over all 1177 pr-link entries — 0 mismatches, and 0 on the pre-existing baseline too (compared as parsed values, not JSON.stringify).
  • Real render: the reported session's own transcript through itemsToHtml140 marks → 17, PR Agent Hub: add Kill button to the live-terminal top bar #80 going from 36 renders to 1.
  • Live path re-checked separately: three beats re-delivering the same PR now merge to one entry and one item.

Markup and CSS for a marker are unchanged from #317, so this only changes how many are emitted.

Suites: 804 Python, 621 turma JS, 64 tunnel JS — all green.

Also

Records the general lesson in the corpus-eval skill: count distinct values and each entry type's predecessor, not just occurrences. A high occurrences-to-facts ratio, or a near-constant preceding type, means the entry is state re-recorded on a schedule rather than an event to render in place — which is exactly what this bug was.

Android renders no PR markers yet (existing PARITY.md gap); its entry there now warns that a per-entry port would reproduce this.

The chat repeated the same "Opened PR #N" marker throughout a session —
often dozens of times for one PR.

The marker (added in #317) read a `pr-link` entry as "a PR was opened
here". It isn't an event. Claude Code re-stamps a session's PR links in
the metadata preamble it writes at the top of EVERY user turn, beside
last-prompt / ai-title / mode / permission-mode. So one PR yields ~6
entries differing only in `timestamp`: 1163 entries for 195 real PRs
across the corpus, 66% of them directly after `permission-mode`. The
existing fold only caught CONSECUTIVE repeats, so the rest — separated
by whole turns — each rendered a marker.

Fixed on both paths:

- `_entry_id`/`entryId` key a pr-link on its URL alone, dropping the
  timestamp. The re-stamps are re-records of one fact, so sharing an id
  lets the live tail's id-keyed merge collapse them to the first, and
  stops the redundant copies reaching the archive.
- `buildItems` dedups by URL across the whole conversation rather than
  folding neighbours. This is the half that covers the archive/ended
  session view, which renders stored entries with no merge step at all.

The first sighting is the right one to keep: measured across the corpus
it lands 1-6 entries BEFORE the `gh pr create` tool_result that opened
the PR (Claude Code stamps the link as soon as it sees the URL), so it
marks where the PR really landed in the conversation.

Verified against the corpus (107 transcripts holding pr-links):
- markers rendered 1154 -> 195, exactly one per distinct PR, in
  first-seen order; no render still holds a duplicate URL.
- zero non-PR display items differ, so nothing else moved.
- py/js mirrors agree on entryId over all 1177 pr-link entries, both
  before and after (baseline 0 mismatches, after 0).
- rendering the reported session's own transcript through itemsToHtml:
  140 marks -> 17, with PR #80 going from 36 renders to 1.

Suites: 804 python, 621 turma JS, 64 tunnel JS, all green.

Also records the underlying lesson in the corpus-eval skill: count
distinct values and each entry type's predecessor, because a high
occurrences-to-facts ratio means state re-recorded on a schedule rather
than an event to render in place.
@xerhab
xerhab merged commit 9fd7c43 into main Jul 27, 2026
7 checks passed
@xerhab
xerhab deleted the pr-link-dedupe branch July 27, 2026 21:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant