Skip to content

🔑 feat: Expose the Messages Key on the Chat Contract - #16395

Merged
berry-13 merged 8 commits into
canaryfrom
berry-13/chat-contract
Sep 27, 2026
Merged

berry-13 merged 8 commits into
canaryfrom
berry-13/chat-contract

Conversation

@berry-13

@berry-13 berry-13 commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The chat contract hands consumers getMessages() and conversation, but not which conversation those messages come from. getMessages() reads the route's id, which runs ahead of the conversation atom while navigation settles, so a consumer that reports conversation.conversationId beside those messages names the previous chat for that window.

This adds messagesKey, the key getMessages() reads when called without one: the route id, else the conversation id, else '' before the pane has either. useChatHelpers serves it from the queryParam it already computes; no consumer changes.

Targets canary, where #16374 landed the parts mapping. #16375, the AI SDK-shaped useChat facade, is stacked on this and reads messagesKey as its chat id and its message query key.

Type of change

  • Feature

Testing

Tested environments/configuration:

Unit level; no component reads the new member yet.

Automated tests:

  • Added client/src/hooks/Chat/__tests__/contract.spec.tsx: the key follows the route id before the conversation catches up, falls back to the conversation id without one, and is empty before either exists
  • cd client && npx jest --findRelatedTests src/hooks/Chat/useChatHelpers.ts src/hooks/Chat/contract.ts: 5624 passed
  • cd client && npx tsc --noEmit: clean, including contract.check.ts
  • reviewctl precheck against canary: static checks and related jest pass

Screenshots / recordings

No user-facing change.

Risk / compatibility

None. The member is read-only and derived from a value the host already computed.

Checklist

  • I reviewed my own changes
  • Relevant tests have been added or updated
  • Existing relevant tests pass
  • The change does not introduce new warnings or errors
  • User-facing or complex behavior is documented where necessary
  • Required documentation PR: N/A

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-26T21:43:50.975691Z d39751d New commits
🔒 Security Review ✅ Completed 2026-09-26T14:05:03.207418Z 1d23969 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1d23969433

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread client/src/hooks/Chat/useChatHelpers.ts Outdated
Comment on lines +153 to +154
const [submission, setSubmission] = useRecoilState(store.submissionByIndex(index));
const initialResponse = submission?.initialResponse;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Convert the touched submission atom as a unit

Reading submissionByIndex here adds a new Recoil consumer for feature-owned submission state. The repository requires an existing atom and all of its readers/writers to be converted together when that state is touched; exposing another reader through the public chat contract instead expands the migration boundary. Convert this atom and its consumers to Jotai before exposing the value.

AGENTS.md reference: AGENTS.md:L104-L107

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4288051 (head b503216). The host no longer reads submissionByIndex: it keeps the response ask submits in its own state, beside the setter it already hands useChatFunctions, so the Recoil atom gains no reader and needs no migration here. useChatFunctions now takes that setter as a plain value setter, which is the only way it calls it.

Comment thread client/src/hooks/Chat/useChatHelpers.ts Outdated

const setSubmission = useSetRecoilState(store.submissionByIndex(index));
const [submission, setSubmission] = useRecoilState(store.submissionByIndex(index));
const initialResponse = submission?.initialResponse;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve pre-stream semantics when restoring a run

When an active generation is restored after reload or cross-device navigation, useResumeOnLoad stores a reconstructed submission whose initialResponse.content is already-populated resumeState.aggregatedContent (useResumeOnLoad.ts, lines 217-257 and 1052-1060). Returning that value here violates the new contract's pre-stream/object-identity guarantee, so an adapter cannot reliably distinguish retained seed parts from output that streamed before reconnection. Preserve a separate original seed, or explicitly represent that no pre-stream response is available for restored runs.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4288051. A restored run installs its submission through useResumeOnLoad, not ask, so the host records no pre-stream response for it and initialResponse is undefined, as the contract now states. Regression: 'has no submitted response for a run restored outside ask' in contract.spec.tsx.

Comment thread client/src/hooks/Chat/contract.ts Outdated
Comment on lines +75 to +78
* The response the in-flight turn was submitted with, before any streamed output. Parts it was
* seeded with (a retained edit prefix, lane placeholders) stay these same objects in the cache
* until the stream replaces them, so a part that is not one of them has streamed. `undefined`
* when no turn has been submitted from this pane.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear the submitted response when the turn settles

On the normal FINAL path, finalHandler sets isSubmitting to false but does not reset submissionByIndex; useResumeOnLoad.ts lines 485-487 explicitly note that a completed submission remains installed. Consequently this property continues returning the previous turn's response throughout the ready/error state, despite being specified as the response for the in-flight turn, so consumers can retain stale seed identities until navigation or the next send. Gate the exposed value on a live-turn signal or clear/extract the snapshot during terminal teardown.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4288051: initialResponse is exposed only while isSubmitting, so it reads undefined once the turn settles even though the submission stays installed. Regression: 'drops the submitted response once the turn settles' (submit, then settle).

@berry-13
berry-13 changed the base branch from canary to main September 26, 2026 14:17
@berry-13
berry-13 added this pull request to stack #16397 September 26, 2026 14:17
@berry-13
berry-13 removed this pull request from stack #16397 September 26, 2026 14:17
@berry-13
berry-13 changed the base branch from main to canary September 26, 2026 14:17
@berry-13
berry-13 added this pull request to stack #16398 September 26, 2026 14:17

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b503216cb5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread client/src/hooks/Chat/useChatHelpers.ts Outdated
},
[setStoredSubmission],
);
const initialResponse = isSubmitting ? submittedResponse : undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear the submitted response when changing conversations

When this ChatView instance navigates between routes, submittedResponse survives because the hook remains mounted. If conversation A has populated it and conversation B is then restored with an active generation, useResumeOnLoad installs B's submission outside this callback and sets isSubmitting true, causing the contract to expose A's response for B instead of undefined. The replacement local-state implementation is fresh evidence beyond the earlier restored-run finding; reset or key this snapshot by queryParam/conversation identity.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4c732a3 (head 9db0930). The host keeps the submitted response under the messages key it was sent from and exposes it only while that key is current, so a run resumed in another chat reports none. Regression: 'hides the submitted response once the pane moves to another chat'.

Comment thread client/src/hooks/Chat/contract.ts Outdated
Comment on lines +77 to +79
* seeded with (a retained edit prefix, lane placeholders) stay these same objects in the cache
* until the stream replaces them, so a part that is not one of them has streamed. `undefined`
* while no turn is in flight, and for a run restored after a reload, whose response already

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve seeded-part identity in the message cache

The stated object-identity guarantee does not hold for edited responses. ask clones the retained prefix and then writes it through React Query's setQueryData; with default structural sharing, unchanged nested parts can be replaced by the equal objects already in the cached response. A consumer comparing cached parts with initialResponse.content will therefore classify unchanged retained parts as streamed. Either force the optimistic cache entry to retain the submitted objects or expose an explicit prefix/seed marker instead of promising identity.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the identity promise does not survive setQueryData's structural sharing. Fixed in 4c732a3 by changing what the contract promises: seeded parts hold their indices, the stream appends after them or fills an empty one and never rewrites a seeded part with content, so consumers compare by position. The facade in #16375 now reads it that way.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9db09300e7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread client/src/hooks/Chat/useChatHelpers.ts Outdated
Comment on lines +164 to +165
const initialResponse =
isSubmitting && submitted?.key === queryParam ? submitted.response : undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Tie submitted snapshots to the active run

When a locally submitted turn in conversation A settles, submitted remains cached; if a later run for A is started on another client and useResumeOnLoad restores it, that hook writes directly to submissionByIndex and sets the pane submitting again without replacing this snapshot. Because the key still equals A, this condition exposes the old turn's response instead of undefined for the restored run. Fresh evidence beyond the earlier navigation fix is that re-entering the same conversation reuses the key while useResumeOnLoad.ts:1052-1060 installs a different generation outside this callback, so the snapshot needs a run identity or terminal clearing rather than only a conversation key.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1b364a0: the host drops the submitted response when the turn settles (render-time transition check on isSubmitting), so a later run restored into the same chat outside ask reports none. Regression: 'forgets a settled turn when a later run in the same chat is restored'.

Comment thread client/src/hooks/Chat/contract.ts Outdated
Comment on lines +77 to +79
* holds the parts the response was seeded with (a retained edit prefix, empty lane placeholders)
* at their indices: the stream appends after them or fills an empty one, and never rewrites a
* seeded part that has content. Compare by position, not identity, since the cache may hold

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Account for streamed output folded into edit prefixes

For an edited response whose retained prefix ends in an unphased TEXT or matching THINK part, the stream does rewrite a populated seed position: calculateContentIndex maps server index 0 to editPrefixOffset - 1, and updateContent concatenates the incoming delta there. The existing steps.spec.ts case explicitly turns "kept tail" into "kept tail!". Consequently an adapter following this contract can classify newly streamed output as seed content; expose the folding boundary/length or document that the last seeded part may contain appended output. This is fresh evidence beyond the earlier object-identity discussion because the contradiction is in the stream's index-folding behavior itself.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, calculateContentIndex folds server index 0 into the last prefix part. Fixed in 1b364a0: the contract now says the stream may continue the last seeded part when it is text or reasoning of the same kind, and to compare by position and content. The facade in #16375 treats that part as streamed once its text differs from the seed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1b364a0bb6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread client/src/hooks/Chat/useChatHelpers.ts Outdated

const setSubmission = useSetRecoilState(store.submissionByIndex(index));
const setStoredSubmission = useSetRecoilState(store.submissionByIndex(index));
const [submitted, setSubmitted] = useState<{ key: string; response?: TMessage }>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Store the new submission snapshots in Jotai

The new submitted snapshot—and the accompanying wasSubmitting transition state—introduces feature-owned client state through React useState. Repository policy requires all new client state to use Jotai and to keep feature-owned atoms within the feature, so these snapshots should be represented by feature-local Jotai state rather than adding another state mechanism that must be migrated later.

AGENTS.md reference: AGENTS.md:L104-L109

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applying this one. The client-state rule is about which store holds shared state while the app moves off Recoil ("the unit of conversion is one atom plus every file that reads or writes it"). This value is local to the hook instance that calls ask, exactly like files, filesLoading and showPopover, which useChatHelpers already keeps in useState. An atom would make per-pane local state globally readable store state, which is the opposite of what the rule protects.

@berry-13
berry-13 force-pushed the berry-13/chat-contract branch from 1b364a0 to 4e4e065 Compare September 26, 2026 16:52

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4e4e0656f4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread client/src/hooks/Chat/useChatHelpers.ts Outdated
Comment on lines +166 to +170
if (wasSubmitting !== isSubmitting) {
setWasSubmitting(isSubmitting);
if (!isSubmitting) {
setSubmitted(undefined);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear the snapshot when handing off to a replacement run

When the server reports that another generation has replaced the current one in the same conversation, useResumableSSE.ts lines 2709-2711 and 4416-4418 deliberately keep isSubmitting true while clearing the submission atom directly. This false-only cleanup therefore never runs, and because queryParam is unchanged, the contract exposes the superseded run's initialResponse for the replacement generation instead of undefined. Fresh evidence beyond the earlier same-chat restored-run comment is that the replacement-handoff paths explicitly preserve the true submitting state, so key or clear this snapshot using the generation identity rather than only a true-to-false render.

AGENTS.md reference: AGENTS.md:L31-L34

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by removing the member in 5be1d4a. Replacement, restore and navigation all install or tear down a run outside ask, so a snapshot the host keeps beside ask cannot know when its run ended; the only true run identity is the submission atom, and reading it adds a Recoil reader. The contract now carries messagesKey only, and the facade in #16375 reads streaming status from content alone.

@berry-13 berry-13 changed the title 🔑 feat: Expose the Messages Key and Submitted Response on the Chat Contract 🔑 feat: Expose the Messages Key on the Chat Contract Sep 26, 2026
berry-13 and others added 8 commits September 26, 2026 23:36
…the submission atom

The contract's initialResponse is now the response ask submitted, held beside
the host's own state and exposed only while the turn is in flight, so a settled
turn and a run restored after a reload report none, and the submission atom
gains no reader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw
… messages key

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw
…tions, not identity

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw
…m can continue the last seeded part

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw
A run can be restored, replaced or torn down outside ask, so a copy of the
submitted response kept in the host describes the wrong run on those paths. The
contract keeps messagesKey only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQdLgSaTheiBFHYnUDgobw
@berry-13
berry-13 force-pushed the berry-13/chat-contract branch from 5be1d4a to d39751d Compare September 26, 2026 21:41
@berry-13
berry-13 merged commit 77a1cfa into canary Sep 27, 2026
26 checks passed
@berry-13
berry-13 deleted the berry-13/chat-contract branch September 27, 2026 00:35
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