Skip to content

fix(chat): a search pick opens a message that changed since it was searched - #2988

Merged
devswha merged 13 commits into
mainfrom
fix/search-pick-changed-turn
Nov 5, 2026
Merged

devswha merged 13 commits into
mainfrom
fix/search-pick-changed-turn

Conversation

@devswha

@devswha devswha commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

Fixes #2958

What changes

A search pick looked for the row named exactly as its turn was when searched (conversationTurnAnchor). An answer that went on, or whose first tool's output arrived after the search, has another name by then, so the pick read every earlier page and said "This message is no longer in the conversation".

Now the pick reads earlier pages until a turn with that name is in, or back to the beginning, as before, and then settles on the rows shown at that moment (a refresh may have changed them meanwhile): it opens the first turn with the match's name, as main does, unless a turn of its time before it changed into the match; otherwise the one turn that changed since (turns without a time go by name only) (pickedTurn and searchedTurnChanged in shared/conversation-search.ts). A changed turn has the match's role and time, goes on from how the searched turn began (its first text grew, or its first tool is the same call up to its output), and still holds the match after the same words (a Markdown mark that streamed in and closed, such as a link, is ignored). When two turns qualify (Codex dates answers the user steered between by one time; an answer that grew while a later one took its old name), none opens and the line says the message is missing: never the wrong answer, which is why #2891's prefix fallback was reverted.

Design reference: the Codex and Claude apps jump to a search hit by the message's own id; our transcripts have none for every agent, so this keeps the content name and adds a strict, unambiguous fallback instead of a per-agent id. The pick reads no more pages than before.

Review round

Codex (Terra, xhigh) on the first head: a refresh during the page reads could make the counted candidate differ from the row opened (HIGH), an exact name could name a later answer after the searched one grew (HIGH, pre-existing), and a closed Markdown link changed the words before the match (MEDIUM). All three are fixed here: the decision moved to the rows shown at the end, an exact name no longer wins over a second qualifying turn, and the comparison ignores link and emphasis marks. omo: CLEAR on the first head.

Second round (omo + Codex on dfc5be6): two answers of one time that both begin with the match counted each other, so neither opened (fixed: a changed turn must go on from the searched turn's opening; test added); a pick whose named row was renamed before it was drawn could stay loading (fixed: the pick settles on the first draw after its pages are in, since loadOlder lays a page before it answers). Codex also reproduced a pre-existing case (main does the same): when the grown answer sits on an older page not loaded yet and a later answer of its time took its old name, the named one opens. Tracked in #2993.

Third round (omo on 0a24b61): turns sharing a name (an untimed prompt sent twice) both counted and neither opened, where main opened the first (fixed: the first named turn opens unless a turn changed since also qualifies; test added). Also fixed: a changed turn's words after the match must go on as they did (a stale view with two same-opening tool answers no longer opens the other), and a pick with no list to show it in fails instead of waiting. Left as LOW: an answer that itself begins with "…" is read as a cut snippet and, once grown, is reported missing. Fourth round (omo on 70523f8): an unchanged answer followed by a longer answer of its time that begins with it was reported missing (fixed: only a changed turn before the named one blocks; test added); an image link's ! is ignored like a link's. Fifth round (omo on b747b5f): answers after the named turn (a task steered twice) and untimed turns that begin with the match blocked an unchanged message main opens (fixed: the named turn decides; only a changed turn of its time before it blocks; a missing time tells no turn apart); a link mark after the match is ignored too. Codex's last MEDIUM (a Codex image prompt searched between its inline and event records reads as missing, as on main) is noted on #2993.

Validation

  • bun test shared/conversation-search.test.ts server/conversation-search.test.ts: 22 pass (with the steered-twice and untimed cases). New tests use the Codex transcript parser (tool output arriving after the search; two steered answers of one time), a grown answer, a closed link, and pickedTurn's none/one/several cases.
  • bun scripts/conversation-search-demo-regression.ts (1280 and 390): pass. New case: the demo serves every match under its old (changed) name, and the pick still opens the answer. Without the fix this case times out with the "no longer in the conversation" line.
  • bun run check fast on the final head with origin/main merged (6193eec, unit 12150 pass, 0 fail): pass (typecheck, build, unit tests, generated files ok).
  • No visual change, so no screenshots. No changelog fragment: the search itself is unreleased (changelog.d/chat-conversation-search.md).

…arched (#2958)

A pick looked for the row named exactly as the turn was when searched (conversationTurnAnchor),
so an answer that went on, or whose first tool's output came, was reported as gone. When no row
has that name back to the conversation's beginning, the pick now takes the one turn of the match's
role and time that still holds the match with the same words before it, and none when two do
(Codex dates answers the user steered between by one time).
@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Explain Change →

Note

CodeRabbit ASCII loading animation

Currently processing new changes in this PR. This may take a few minutes, please wait...

Review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 1d404b8 and f6caa8b.

Files selected for processing: 4

  • scripts/conversation-search-demo-regression.ts
  • shared/conversation-search.test.ts
  • shared/conversation-search.ts
  • src/components/ChatView.tsx

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.

Share

Review round: the pick now settles on the rows shown once its page is in (a refresh may have
changed them while earlier pages were read) and opens a row only when exactly one turn is the
match by its name or changed since (pickedTurn); an answer that grew while a later answer of its
time took its old name opens none. A Markdown mark that streamed in and closed (a link) no longer
hides the words before the match.
… and a match at the start counts only its own answer

Codex re-review: a pick whose named turn changed before its row was drawn stayed loading; it now
settles on the turn changed since too. Words before the match compare exactly unless a snippet was
cut, so an answer that begins with the match no longer counts a later answer of its time.
…, and a pick settles at once

omo review: two answers of one time that both begin with the match counted each other, so neither
opened. A turn changed since the search must now go on from the searched turn's opening (its first
text grew, or its first tool is the same call up to its output). The pick settles on the first draw
after its pages are in, so it never waits on a row that was renamed meanwhile.
…nto the match

omo review: turns sharing a name (an untimed prompt sent twice) both counted, so neither opened,
where main opened the first; the first named turn opens again unless a turn changed since also is
the match. A changed turn's words after the match must go on as they did, and a pick with no list
to show it in fails instead of waiting.
…e match

omo review: an unchanged answer followed by a longer answer of its time that begins with it (Codex
dates both by the task's start) was reported missing. A grown turn can only lose its old name to a
later turn, so only a changed turn before the named one stops the pick. An image link's mark is
ignored like a link's.
@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Review findings and how each was resolved (omo + Codex Terra xhigh, four passes):

Fixed in this PR

  • HIGH (Codex): a refresh during the page reads could make the counted candidate differ from the row opened. The pick now settles on the rows shown once its pages are in.
  • HIGH (Codex): an exact name could name a later answer after the searched one grew. A named turn no longer opens when a turn before it changed into the match.
  • MEDIUM (Codex): a closed Markdown link or image link changed the words before the match. Link and emphasis marks are ignored there.
  • MEDIUM (omo + Codex): two answers of one time that both begin with the match counted each other. A changed turn must go on from the searched turn's opening, and its words after the match must go on as they did.
  • MEDIUM (omo): turns sharing a name (an untimed prompt sent twice) and an unchanged answer followed by a longer answer of its time were reported missing where main opened them. The first named turn opens unless a turn before it changed into the match.
  • MEDIUM/LOW (omo + Codex): a pick could stay loading. It settles on the first draw after its pages are in, and fails when there is no list to show it in.

Left, not blocking

CodeRabbit was rate-limited on these heads, so the reviews above stand in for it.

…y name only

omo review: answers after the named turn (a task steered twice) or untimed turns that begin with
the match blocked an unchanged message that main opens. Only a changed turn of its time before the
named one blocks it, a missing time tells no turn apart, and a link mark after the match is
ignored like one before it.
@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Fifth pass (omo + Codex on b747b5f), resolved in f6caa8b:

  • MEDIUM (omo, reproduced): answers after the named turn (a Codex task steered twice: "Done." / "Done. Lint passes." / "Done. Tests pass.") and untimed turns that begin with the match ("fix it now" / "fix it") blocked an unchanged message that main opens. The named turn now decides; only a changed turn of its time before it blocks; turns without a time go by name only. Tests added.
  • LOW (Codex): a link mark after the match is ignored like one before it. Test added.
  • Not changed: the pre-existing unloaded-page case (Conversation search: a pick can open a later answer that took a grown answer's name on an unloaded page #2993), and the LOW that a shared test imports server/codex.ts (test-only, as shared/relay-mux-engine.test.ts already does; conventions pass).

@devswha

devswha commented Nov 5, 2026

Copy link
Copy Markdown
Owner Author

Local merge gate (owner-approved): bun run check fast passed at this head, omo CLEAR at this head, threads resolved, merges cleanly. Fixes #2958; the older unloaded-page case (present on main before this PR) is #2993. Merged without waiting for GitHub CI.

@devswha
devswha merged commit b01e5e1 into main Nov 5, 2026
15 of 18 checks passed
@devswha
devswha deleted the fix/search-pick-changed-turn branch November 5, 2026 13:58
devswha added a commit that referenced this pull request Nov 5, 2026
…rown answer's name (#3001)

Fixes #2993

## What changes
A conversation search pick stopped reading earlier pages as soon as a
loaded turn had the match's name (`conversationTurnAnchor`). When the
searched answer grew after the search and sits on an older page not
loaded yet, a later answer of the same time that begins with the same
words (Codex dates answers the user steered between by the task's start)
had the old name, and the pick opened it.

- `pickSettled` (`shared/conversation-search.ts`): the pick keeps
reading earlier pages until a turn has the match's name **and** a turn
before that one is of an earlier time than the match (or the beginning
is read). A turn that changed since can lose its name only to a later
turn of its own time, and turns come in time order except answers dated
by an earlier task start, which all follow that start. Then `pickedTurn`
decides on the rows shown, as before: it opens the one turn that is the
match, or none (`several` -> "missing") when an earlier turn grew into
the match and a later one took its name. A time that cannot be parsed
tells nothing (the pick reads on); a match without a time goes by name
only, as before.
- Turns of the match's time that share its name (their first 80
characters agree, or they start with the same tool call) go by which
still has the matched words anywhere in its text, progress notes
included; none opens unless exactly one does. Reading back could
otherwise bring the older one in first and open it, and the searched
answer's words never leave its text, though they can leave the search
(Codex can mark it commentary later), so that case opens none.
- Usual cost: a Claude-style answer has its prompt of an earlier time
right before it, so nothing more is read; a Codex answer reads at most
back to the previous task.
- The issue's second case (comment: a Codex prompt whose inline picture
is paired with its local path after the search) is not changed here: it
says "missing", never opens a wrong turn, and is tracked in #3003. A
rule accepting any picture first was tried and reverted after review (it
could open another prompt with a different picture).

No per-turn transcript id was added: this stays within the client and
the shared helpers, with no wire or reader change.

## Validation
- New tests in `shared/conversation-search.test.ts` (`pickSettled`)
failed on the unchanged code (missing export; the old stop rule settles
as soon as the later answer is read) and pass now: `bun test
shared/conversation-search.test.ts server/conversation-search.test.ts`.
- `bun run check fast`: pass at 095eaa4 (typecheck, build, unit tests
and the rest of the fast set)
- Skipped: `check full` and a browser run (no browser test drives the
pick; the rule is covered at the shared helpers as in #2988).
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.

Chat search: a pick misses a turn that changed after it was searched

1 participant