Continue in chat: hand an analysis summary into a new chat tab - #294
Merged
Merged
Conversation
Spec 013, Story 1. Beside an analysis summary on the website, "Continue in chat" opens the chat in a new tab with that summary already the thread's previous turn, so the reader's first message can be a follow-up. **The handoff travels in the URL fragment and is bound to the tab.** The website mints an ID with `POST /api/handoff` and opens `/chat/guest/#handoff=<id>`. Browsers never send a fragment to a server, so the ID stays out of nginx logs and Referer headers. Chainlit 2.11 gives the app no page URL on connect -- only cookies -- and a cookie is shared by every tab, so two handoffs opened together could seed the wrong one. Instead a block in `public/custom.js` reads the fragment and posts it; Chainlit forwards every window message to `@cl.on_window_message` over that tab's own socket. **The retry is load-bearing, measured.** A single post in the first second after load is lost 15 times out of 15, because Chainlit drops window messages until its socket is up; from two seconds on it is claimed 6 of 6. Posting once on load -- the obvious implementation -- would never have worked. The script retries every 500 ms for up to 20 s and stops on its own acknowledgement, and since a claim can therefore arrive twice, redemption is idempotent per session. **The chat continues the summary the reader saw; it does not regenerate one** (FR-002). Generation here is not reproducible (~0.33 similarity run to run), so a handoff copies the summary text when it is minted. It can only be minted for a summary that already exists in the summary cache at the requested tier -- which is also how it refuses to widen disclosure (FR-003): an identifiers-tier handoff exists only if the reader chose identifiers on the website. **The model gets the data, not just the words.** The seeded turn is `[HumanMessage, AIMessage]`, the shape of every real turn, and the AI turn carries the summary plus the allow-listed analysis data rebuilt at the handoff's tier with the same functions the summary endpoint uses. Recorded as having ended at `postprocess`, the last node of all three profiles, so the next message is an ordinary turn with that history. Verified end to end in a browser against a real analysis on beta: the tab opens on the same text the summary endpoint produced; asked which pathway has the lowest FDR, the chat names the analysis's real top pathway with its exact FDR (4.2e-15). The control -- the same question with no handoff -- answers that the user guide does not cover it. An unknown ID says so rather than opening an empty chat. Adversarial review found: - **A promise the architecture could not keep.** The first endpoint also returned a `personal_path`. The store is in memory and the guest and logged-in chats are separate processes, so that link could only ever open on "couldn't load the summary". It now returns one `path`, for the chat served by the process that minted it; the spec records what Story 3 needs. - `current_release()` and `fetch_not_found()` can both return None, which the first version assumed they could not -- found by mypy, handled explicitly. - A claim arriving before `on_chat_start` would have seeded a thread named "None"; it falls back to the session id. Seeding failures now say so to the reader instead of leaving a chat that silently lacks the context, and claims are logged either side. Sabotage: fetching identifiers regardless of tier fails the aggregate test; looking the summary up at `aggregate` whatever was asked fails the cannot-widen test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Spec 013, Story 1. Beside an analysis summary on the website, "Continue in chat" opens the chat in a new tab with that summary already the thread's previous turn — so the reader's first message can be a follow-up.
How the handoff travels
Referer.What the chat receives
The summary the reader saw, not a new one (FR-002). Generation isn't reproducible here (~0.33 similarity run to run), so the handoff copies the text when it is minted. It can only be minted for a summary already in the cache at the requested tier. That one lookup is also how it refuses to widen disclosure (FR-003): an identifiers-tier handoff exists only if the reader chose identifiers.
The data, not just the words. The seeded turn has the shape of every real turn,
[HumanMessage, AIMessage]. The AI turn carries the summary plus the allow-listed analysis data, rebuilt at the handoff's tier with the same functions the summary endpoint uses.Verified in a browser, against a real analysis on beta
The control is what makes the follow-up check mean something: without the handoff, the chat has no way to know that pathway or that number.
Adversarial review
personal_path. The store is in memory, and the guest and logged-in chats are separate processes, so that link could only ever open on "couldn't load the summary". It now returns onepath, for the chat served by the process that minted it. The spec records what Story 3 (logged-in) needs: a store both processes share.current_release()andfetch_not_found()can both returnNone, which the first version assumed they could not. mypy caught it; both are now handled explicitly.on_chat_startwould have seeded a thread named"None"; it falls back to the session id. Seeding failures now tell the reader instead of leaving a chat that silently lacks the context, and claims are logged either side.Sabotage: fetching identifiers regardless of tier fails the aggregate test; looking the summary up at
aggregatewhatever tier was asked fails the cannot-widen test.Not in this PR
/api/answerkeeps nothing.🤖 Generated with Claude Code