Skip to content

fix(mobile-native-chat): reland glued pending retirement with resolvable baselines - #14928

Closed
brennanb2025 wants to merge 1 commit into
mainfrom
brennanb2025/reland-mobile-glue-retirement
Closed

brennanb2025 wants to merge 1 commit into
mainfrom
brennanb2025/reland-mobile-glue-retirement

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​690 $\color{#cf222e}{\Huge{\mathbf{−}}}$​4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​686
Prod 5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​231 $\color{#cf222e}{\Huge{\mathbf{−}}}$​31 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​200

Relands #14665, which #14819 reverted. Fixes STA-4482 (the reland) and STA-4492 (the hydration defect the reland was asked to carry a fix for).

STA-4492 does not reproduce on main today, and that is expected: glueBaselineTrusted exists nowhere in mobile/ because #14819 deleted it. It returns the moment #14665 relands, which is why it is fixed here rather than filed as won't-fix.

What #14665 was for

Mobile retires an optimistic bubble only when a transcript user turn matches its normalized text at the expected ordinal. When the host glues two rapid sends into ONE row, neither text matches, so both bubbles pin below every later reply for the rest of the session.

What #14819 reverted it for, and what changed

1. A rejected send restored a trimmed composer and dropped draft text

#14665 shadowed the send parameter with const text = rawText.trimEnd() and then handed that trimmed value to restoreRejectedDraft. The trim is a wire concern — trailing whitespace the host writes verbatim onto the agent's input line can glue the next rapid send onto this one — and it has no business touching the draft.

The seam now keeps draftText as the user typed it and trims only the bytes that go out. clearDraftForSend / restoreRejectedDraft get the original; captureSendOrigin / acceptSend / the RPC get the trimmed text.

2. Loading-time glued sends stayed queued forever (STA-4492)

glueBaselineTrusted: !transcriptLoading was stamped once and never revisited. In selectGluedPendingIds an untrusted entry becomes a null segment — a permanent barrier — so its glued row could never retire it, and it split the run so its neighbours could not glue either.

The flag is replaced by baselineResolved, which is transient:

  • A send issued while the transcript is hydrating is captured unresolved. messages is not this session's history then (a read is in flight, or it is still the previously active tab's), so the entry is held out of matching entirely rather than judged against somebody else's rows.
  • On the first authoritative read, rebaseMobileNativeChatPendingBaselines pins it to that read's tail and re-derives its ordinal from that transcript. It is a normal send from then on.

Consequence beyond the reverted defect: a hydration-time send is also no longer retired by history it never saw. On main today, baselineOccurrences captured against an empty/foreign transcript makes a send whose text already appears in history retire the instant that history loads — the mirror of the same bug. There is a test for it.

Tests: RED → GREEN, against a naive re-apply of #14665

Every test below was run against the literal #14665 tree (git checkout 68ca17e46c -- <files>), not against main, so it proves the reland is different from the diff that was reverted.

Regression 1 — use-mobile-native-chat-message-send.test.ts:

FAIL > restores the untrimmed draft when the send is rejected
AssertionError: expected 'first line\n\nsecond line' to be 'first line\n\nsecond line\n\n'

FAIL > restores the untrimmed draft when the launch-draft pre-clear is rejected
AssertionError: expected 'ship it' to be 'ship it   '

Regression 2 — use-mobile-native-chat-drafts-glued-pending.test.ts:

FAIL > sends issued while the transcript is still hydrating
       > retires both bubbles once their glued row lands after hydration
AssertionError: expected [ { id: 'pending-1', …(4) }, …(1) ] to deeply equal []

FAIL > sends issued while the transcript is still hydrating
       > does not retire a hydration-time send against history it never saw
AssertionError: expected [] to deeply equal [ 'run the tests' ]

The other 8 tests in that file pass against #14665 too — the reland keeps its intended behavior. Against origin/main (no glue handling at all) 4 of the 10 fail, including the core retirement.

A separate codex session, given only the #14665 sources and no hint of the answer, independently traced the hang to !item.glueBaselineTrusted in selectGluedPendingIds and concluded "both queued bubbles remain forever".

Full run: 451 files / 3565 tests passed in mobile/. tsc -p mobile/tsconfig.json and oxlint mobile/src/session/ clean (exit 0). No max-lines disable and no .oxlintrc.json bump.

Not covered

Emulator QA is blocked on this host — see the PR discussion. The change is pure reconciliation logic behind the drafts hook and is covered at the hook level (the tests drive the real useMobileNativeChatDrafts through react-test-renderer and assert on the bubbles the list actually renders), but it has not been exercised on a device.

🤖 Generated with Claude Code

…ble baselines

Relands #14665 (reverted by #14819). Mobile retires an optimistic bubble only
when a transcript user turn matches its normalized text at the expected ordinal,
so two rapid sends the host glued into ONE row matched nothing and both bubbles
pinned below every later reply for the rest of the session.

#14819 reverted it for two launch blockers, both fixed here rather than restored:

1. A rejected send restored a TRIMMED composer. The trim belongs to the wire —
   trailing whitespace the host writes verbatim can glue the next rapid send —
   not to the draft. The send seam now keeps `draftText` as the user typed it and
   trims only the bytes that go out, so a rejection puts back the original draft,
   blank lines included.

2. Sends issued while the transcript was still hydrating were stamped
   `glueBaselineTrusted: false` FOREVER, so their glued row could never retire
   them and they barred their neighbours from matching too (STA-4492). A
   hydrating transcript is not this session's history, so the flag is now
   `baselineResolved` and it is transient: such a send is held out of matching
   until the first authoritative read, which rebases it onto that read's tail and
   re-derives its ordinal. It then reconciles like any other send — and, as a
   consequence, also stops being retired by history it never saw.

Fixes STA-4482, STA-4492
Refs #14665, #14819, #14262
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Mobile emulator QA: BLOCKED

Attempted per /orca-mobile-emulator-qa. Not substituted with any other interaction method (no computer-use, no accessibility-API driving, no Expo Web).

What worked:

  • orca emulator attach 'iPhone 17 Pro' --worktree path:<my worktree> → attached: true, backend ios, device 50FD514C-….
  • Metro started from this worktree's mobile/ and was reachable: curl http://100.76.32.125:8081/status → packager-status:running.
  • orca computer get-app-state --app com.apple.iphonesimulator --restore-window returned an AX tree — the simulator is at the iOS home screen.

What blocked it:

  1. orca emulator exec passthrough is broken on this relay build. Every command returns the same error, including help:
    {"code":"emulator_error","message":"error: unknown option '-d'\n"}
    That is the only route from this host to simctl openurl, so the dev-client URL cannot be opened on the simulator. (xcrun does not exist on this Linux host.)
  2. Screenshots are not readable from this host. The simulator and Computer Use provider run on a Mac; result.screenshot.path is a Mac path (/var/folders/…) with dataOmitted: true. There is no command to transfer the bytes here, so the skill's "read the PNG and inspect it visually" step and the gh image attachment cannot be done.
  3. The Orca Mobile dev client was not visible on the simulator's home screen page 1 and its installation is unconfirmed.

Someone on a Mac host can run this in a few minutes: start Metro on this branch, xcrun simctl openurl booted 'exp+orca-mobile://expo-development-client/?url=…', then fire two composer sends in quick succession while the transcript is still loading and confirm both bubbles clear when the glued row lands.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The changes add transcript-baseline rebasing and centralized retirement for mobile native-chat pending messages. The logic handles exact echoes, glued sends, image echoes, occurrence counts, barriers, and hydration state. Pending send origins now record baseline resolution. The send flow right-trims transmitted text while preserving the original draft for clearing and rejection restoration. Tests cover rebasing, retirement, hydration, glued messages, image previews, whitespace handling, and rejected sends.

Merge Risk: ⚪ Minimal · up to df257

This PR restores correct retirement of glued pending messages, preserves rejected draft text, and defers hydration-time matching until baselines resolve. The change is merge-ready after normal checks; no actionable merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the mobile native-chat fix and the relanded glued-pending retirement with baseline resolution.
Description check ✅ Passed The description clearly explains the changes, motivation, linked issues, testing results, and emulator-QA limitation, so it is mostly complete.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
mobile/src/session/use-mobile-native-chat-message-send.test.ts (1)

39-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the send-origin fixture type-safe.

as never hides missing fields in MobileNativeChatSendOrigin. The test can compile after the production origin contract grows, even if this mock remains incomplete. Return a complete typed origin fixture instead.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 202ab1f6-7ac9-4d25-8cb3-1aa9ded529a4

📥 Commits

Reviewing files that changed from the base of the PR and between 516a269 and df25755.

📒 Files selected for processing (10)
  • mobile/src/session/mobile-native-chat-pending-baseline.test.ts
  • mobile/src/session/mobile-native-chat-pending-baseline.ts
  • mobile/src/session/mobile-native-chat-pending-echo.ts
  • mobile/src/session/mobile-native-chat-pending-retirement.test.ts
  • mobile/src/session/mobile-native-chat-pending-retirement.ts
  • mobile/src/session/use-mobile-native-chat-controller.test.ts
  • mobile/src/session/use-mobile-native-chat-drafts-glued-pending.test.ts
  • mobile/src/session/use-mobile-native-chat-drafts.ts
  • mobile/src/session/use-mobile-native-chat-message-send.test.ts
  • mobile/src/session/use-mobile-native-chat-message-send.ts

Included review availability: Your plan includes up to 10 reviews per rolling hour; 4 remain after this review.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Superseded by #14936, which relands #14665 addressing both documented revert causes rather than one.

The reason this matters: #14819's own body says only "launch-blocking regression". The actual causes are in a maintainer comment on #14665 —

  1. a rejected send restored a trimEnd'd composer and dropped draft text (the trim is a wire concern and has no business touching the draft), and
  2. glueBaselineTrusted: false was never cleared, so hydration-time sends stranded forever.

Cause (2) is STA-4492, which provably does not reproduce on current main because #14819 deleted every file and symbol it cites — it is a constraint on the reland, not a standalone bug. #14936 fixes both and covers them with 2/3/14/6 RED across four mutations, one per revert cause plus the feature and its false-positive bound.

Closing this to keep one review surface per ticket.

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