Skip to content

feat: restore production chat reads, honest Settings GET, and granted chat conversation composition - #12914

Merged
undivisible merged 6 commits into
v5from
cursor/chat-read-persistence-2ab3
Sep 7, 2026
Merged

feat: restore production chat reads, honest Settings GET, and granted chat conversation composition#12914
undivisible merged 6 commits into
v5from
cursor/chat-read-persistence-2ab3

Conversation

@undivisible

@undivisible undivisible commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Production New-backend now serves GET /v1/chat-messages from PostgreSQL under an explicit chat.read grant, mounts GET /v1/settings without inventing a signed-in profile, and composes granted main-chat sessions onto the first GET /v1/conversations page. Opening chat:chat-main loads that persisted history instead of a title-only pane. Missing grants and missing producers stay visible failures. Empty successful transcripts, token-claim identities, and empty chat:chat-main rows are not used to satisfy the screens.

  • Missing, revoked, or non-chat grants return 403, not an empty 200 page.
  • A granted account with no rows returns an honest empty page plus the existing attachment capabilities.
  • Assistant rows require a unique terminal generation event. Orphans and mismatched terminals are 503, not completed answers.
  • POST /v1/chat-messages and generation SSE stay unmounted (404 {error:"not_found"}).
  • Migration 0055 adds chat_messages and chat_generation_events. Migration 0056 adds read_chat_conversation_sessions(). Both update the checksummed manifest.
  • Signed-out GET /v1/settings stays {identity:null,entitlement:null}. A verified Firebase identity without an owner-backed producer is 503, not a 200 profile minted from token claims.
  • First-page conversation reads include chat:chat-main only when the same credential holds chat.read and main-session messages exist. Later cursor pages keep the Listen sequence and do not repeat that row.
  • Conversation detail for chat:chat-main reads GET /v1/chat-messages. Other chat: session ids stay unavailable rather than showing the main transcript. Grant denial keeps typed copy.
  • Mobile chat, conversations, memories, and tasks show typed grant-denied copy instead of an empty library.
  • Real-PG chat fixtures pass generation frames as $n::text::jsonb so postgres.js does not double-encode terminals into JSON strings (which correctly fail closed as 503).

Why this boundary

Chat history ownership is the chat.read grant, not Firebase identity and not memories.read. Writes still need a source-owned entitlement producer; this PR does not invent quotas or mount admission. Settings identity and entitlements still need an owner-backed producer; this PR does not project Firebase claims as a profile. Conversation composition uses a second chat.read authorization rather than reading chat tables under conversations.read.

Verification

From backends/example-platform:

bun run check:deployed
# 293 pass, 1 skip (real PostgreSQL 18.4), 0 fail
# production import closure: 130 runtime-linked modules, no sqlite/qa

GitHub Validate backend worker passed on ec68aaa925, including real PostgreSQL 18.4 (test:postgres) for the jsonb fixture and chat:chat-main composition.

React Native:

bun run --cwd react-native test src/chatConversationHistory.test.tsx src/recordingTranscript.test.tsx src/pages/Conversations.test.tsx src/pages/pages.test.tsx __tests__/chatClient.test.ts
# 5 suites, 56 passed

The skipped local tests are chat-messages.real.test.ts and the conversation-chat composition case in conversations.real.test.ts, which need Docker/OMI_TEST_POSTGRES_URL. This Cloud VM has no Docker. CI test:postgres is the real-PG proof.

Not verified here: applying migrations 55-56 to based-hardware-dev, Cloud Run deploy, live Firebase grants, Apple Debug builds, ScreenCaptureKit, physical BLE/iPad.

Do not deploy this entry against a database that still has only migrations 1–54: readiness compares the full checksummed manifest and will stay unready.

Remaining (not this PR)

  • Chat POST/SSE/cancellation and a real entitlement producer
  • Settings identity/entitlement producers and attachments
  • Unified Listen+chat pagination beyond first-page composition
  • DEV IAM / Cloud Run (no gcloud in this VM)
  • main/prod are untouched
Open in Web Open in Cursor 

Review in cubic

Mount GET /v1/chat-messages on the production Firebase/PostgreSQL entry
behind an explicit chat.read grant. Missing or revoked grants stay 403.
Granted empty history is an honest empty page with existing attachment
capabilities. Assistant rows without a unique terminal generation event
are 503. POST and SSE stay unmounted.

Verification: backends/example-platform `bun run check:deployed` passed
(282 tests, 1 skipped real PostgreSQL without Docker). Migration 55 is
not applied to DEV from this environment.
Missing chat.read and conversations/memories/tasks grants now surface
typed account-unavailable copy instead of a generic load failure or an
empty successful list.
Verified Firebase credentials no longer 404 on GET /v1/settings. Signed-out
stays a null envelope; signed-in stays 503 until an owner-backed profile
and entitlement producer exists.
@cursor cursor Bot changed the title feat: persist PostgreSQL chat history under chat.read feat: restore production chat history reads and honest Settings GET Sep 7, 2026
The production unique-terminal projector fail-closes to 503 when
frame_json is a JSON string. Pass the serialized frame as text so
postgres.js does not double-encode the jsonb fixture.

Failure-Class: none
First-page GET /v1/conversations includes chat:chat-main when the same
credential holds chat.read and main-session messages exist. Missing
grants and empty history never invent that row. Later cursor pages keep
the Listen sequence.

Failure-Class: none
@cursor cursor Bot changed the title feat: restore production chat history reads and honest Settings GET feat: restore production chat reads, honest Settings GET, and granted chat conversation composition Sep 7, 2026
Opening chat:chat-main reads GET /v1/chat-messages instead of a
title-only pane. Other chat session ids stay unavailable rather than
showing the main transcript. Grant denial keeps typed copy.

Failure-Class: none
@undivisible
undivisible marked this pull request as ready for review September 7, 2026 09:21
@undivisible
undivisible merged commit fc54d06 into v5 Sep 7, 2026
3 checks passed
@undivisible
undivisible deleted the cursor/chat-read-persistence-2ab3 branch September 7, 2026 09:21
@cursor

cursor Bot commented Sep 7, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_b9c0e58b-013a-4b71-85c7-68baed3e6b49)

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 7, 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-07T09:35:18.342742Z 59fc4c4 Draft marked ready
ℹ️ 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: 59fc4c4d12

ℹ️ 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".

@@ -0,0 +1,273 @@
CREATE TABLE omi_memory.chat_messages (

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 Mark unresolved chat schema names for mechanical renaming

Add SQL-compatible domain-pending(...) annotations to migrations 0055 and 0056. This migration introduces the unresolved sender, type, session, revision, hash, and source vocabulary throughout the durable schema, but the markers currently exist only in the TypeScript adapters; that leaves these SQL identifiers invisible to the required mechanical rename process.

AGENTS.md reference: backends/example-platform/AGENTS.md:L10-L13

Useful? React with 👍 / 👎.

if (omiBackend == null) {
throw new Error('Native transport unavailable');
}
const messages = await loadChatHistory(omiBackend);

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 pagination when loading conversation chat history

When the main chat contains more than 50 messages, loadChatHistory fetches only the newest page and returns just its message array, so this hook discards olderCursor and hasOlder. ChatConversationHistory consequently provides no way to load the omitted messages, making all history beyond the newest 50 entries inaccessible from the newly added conversation detail.

Useful? React with 👍 / 👎.

Comment on lines +190 to +193
if (!chatAuthorization.authorized) {
return chatAuthorization.outcome === "authorization"
? listen
: failed(503, "unavailable");

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 Return unauthorized when the second identity check fails

If a first-page conversation read crosses the Firebase token's expiry boundary, the initial conversations.read authorization can succeed while this subsequent chat.read authorization returns authentication. This branch maps every result except missing authorization to 503, so the client treats an expired session as a transient service outage and does not trigger its 401 reauthentication flow; preserve the established 401 unauthorized response for this outcome.

Useful? React with 👍 / 👎.

if (!sessionIds.has(row.id)) items.push(row);
}
items.push(...sessions);
items.sort(compareItems);

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 the listen cursor order when adding chat

Do not re-sort the returned listen page independently of its cursor. PostgreSQL pages recordings by ascending durable conversation_sequence, and the runtime saves the next cursor against that original sequence, but this line reorders only the first page by descending updatedAt; after the client appends later pages, recordings can appear in a different order from the cursor chain, with an old injected chat row also preceding newer recordings that have not yet been fetched. Either preserve the listen rows' order when inserting the chat item or paginate the combined ordering.

Useful? React with 👍 / 👎.

)}
{selected.source === 'chat' &&
selected.id === MAIN_CHAT_CONVERSATION_ID && (
<ChatConversationHistory key={selected.id} />

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 Reload the selected chat when its conversation changes

When a conversations refresh returns chat:chat-main with a newer updatedAt, React preserves this child because its key remains the conversation ID, while useChatConversationHistory reloads only when active or its private retry counter changes. The list can therefore show new chat activity while the open detail continues displaying the old message snapshot, and the loaded state offers no manual reload action; pass the conversation revision/timestamp into the hook or include it in the component key, as the recording transcript path already does.

Useful? React with 👍 / 👎.

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.

2 participants