feat(ios): read-only offline cache for chat sessions and transcripts#100219
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 548326ae30
ℹ️ 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".
| // is written through so the next cold open pre-paints current rows. | ||
| self.hasAppliedLiveHistory = true | ||
| self.isShowingCachedTranscript = false | ||
| self.persistTranscriptToCache(sessionKey: request.session.key) |
There was a problem hiding this comment.
Avoid caching optimistic send echoes
When this is reached from refreshHistoryAfterRun(... preservingOptimisticLocalMessages: true), self.messages can still include the pending local user echo inserted before sendMessage and preserved by reconciliation while the gateway history has not durably returned that row yet. Persisting the whole visible transcript here writes non-authoritative UI state into the offline cache; if the run later times out or the app restarts before canonical history catches up, the next cold open ghost-paints a message the gateway never had. Store only canonical live-history rows for cache write-through, not optimistic messages.
Useful? React with 👍 / 👎.
| let transportModeID = self.appModel.chatTransportModeID | ||
| // Includes the cache gateway identity so switching paired gateways | ||
| // rebuilds the view model even while the transport mode stays the same. | ||
| let transportModeID = self.appModel.chatViewModelIdentityID |
There was a problem hiding this comment.
Observe gateway cache identity changes
This identity only takes effect when syncChatViewModel() is called, but the view currently calls that on initial task, chat-session changes, fixture toggles, and isOperatorGatewayConnected becoming true. If the user changes the active/saved gateway while the chat tab is mounted and still offline/connecting, chatViewModelIdentityID changes without rebuilding, so the existing view model and transcript cache remain scoped to the previous gateway until connection succeeds and can show stale cached transcripts under the new gateway. Add an onChange for this identity (or the gateway cache identity) so switches rebuild immediately.
Useful? React with 👍 / 👎.
|
Codex review: needs real behavior proof before merge. Reviewed July 5, 2026, 7:46 PM ET / 23:46 UTC. Summary PR surface: Docs +2, Other +1303. Total +1305 across 21 files. Reproducibility: yes. source-reproducible. Start a sessions.list request from Home, Sessions, or iPad Activity for gateway A, switch to gateway B before the response returns, and the current code applies/stores A's response through the now-current cache identity. Review metrics: 2 noteworthy metrics.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review findings
Review detailsBest possible solution: Land the per-gateway read-only cache only after every session-list browsing surface uses a request-scoped cache identity guard, the release-owned changelog edit is removed, and redacted simulator/device proof shows cold open, offline browsing, reconnect replacement, and purge. Do we have a high-confidence way to reproduce the issue? Yes, source-reproducible. Start a sessions.list request from Home, Sessions, or iPad Activity for gateway A, switch to gateway B before the response returns, and the current code applies/stores A's response through the now-current cache identity. Is this the best way to solve the issue? No, not yet. The cache belongs near the shared chat/session boundary and the view-model path is well covered, but the visible browsing surfaces need the same request-scoped identity guard before this is the safe implementation. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 2ded26a5d6be. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Docs +2, Other +1303. Total +1305 across 21 files. View PR surface stats
Security concerns:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (3 earlier review cycles)
|
165063f to
b1c7f92
Compare
|
@openclaw-mantis visual task: verify iOS chat cold-opens from cached transcript while offline, disables sending, then live gateway history replaces the cache on reconnect. Use current head b1c7f92 and publish the redacted simulator artifact manifest here. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b1c7f922a9
ℹ️ 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".
| // A live sessions response (even an empty one) is authoritative; | ||
| // a slow cache read must never repaint over it. | ||
| guard self.sessions.isEmpty, !self.hasAppliedLiveSessions else { return } | ||
| self.sessions = cached |
There was a problem hiding this comment.
Wire cached sessions into the visible sessions UI
When the app is offline, this only assigns cached session rows to OpenClawChatViewModel.sessions, but the iOS chat tab constructs OpenClawChatView with showsSessionSwitcher: false (apps/ios/Sources/Design/ChatProTab.swift:94), while the visible Recent sessions/Sessions surfaces use CommandCenterTab.refreshSessions() and immediately clear their rows when isCommandSessionListAvailable is false (apps/ios/Sources/Design/CommandCenterTab.swift:760-764). As a result, after a cold offline launch the cached transcript can paint for the current chat, but users still cannot browse cached recent sessions as promised; route those surfaces through the same cache or enable a cache-backed switcher.
Useful? React with 👍 / 👎.
|
The dead-code cleanup in Please drop that duplicate hunk when rebasing this PR. The offline cache work remains otherwise distinct from #100328. |
b1c7f92 to
b6ae8e1
Compare
b6ae8e1 to
4d38e7f
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4d38e7f3b5
ℹ️ 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".
| let transport = self.appModel.makeChatTransport() | ||
| let response = try await transport.listSessions(limit: Self.recentSessionsFetchLimit) | ||
| self.defaultChatSessionEntry = response.sessions.first { | ||
| $0.key == self.appModel.defaultChatSessionKey | ||
| } | ||
| self.recentChatSessions = Self.sessionChoices( | ||
| response.sessions, | ||
| currentSessionKey: self.appModel.chatSessionKey, | ||
| defaultSessionKey: self.appModel.defaultChatSessionKey) | ||
| self.applySessions(response.sessions) | ||
| await self.appModel.storeCachedChatSessions(response.sessions) |
There was a problem hiding this comment.
Keep session cache writes scoped to the request gateway
If this sessions.list call starts against gateway A and the user switches/reconnects to gateway B before it returns, the response is still applied and storeCachedChatSessions resolves the current cache identity, so A's session rows can be written into B's offline cache and later shown while B is offline. The refreshID change cancels the old SwiftUI task, but this non-throwing await path never checks cancellation or that chatViewModelIdentityID is unchanged before applying/storing the result; capture the identity/cache for the request and guard it after the await.
Useful? React with 👍 / 👎.
|
Land-ready rewrite complete on exact head What changed:
Verification:
Known proof gap: no fresh real-device/simulator recording for cold-open/offline navigation/reconnect/purge. The focused cache/view-model regression suite remains in the PR, and exact-head Apple build/test gates are green. |
4d38e7f to
7551c63
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7551c63991
ℹ️ 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".
| self.sessions = response.sessions | ||
| await self.appModel.storeCachedChatSessions(response.sessions) |
There was a problem hiding this comment.
Guard iPad session writes against gateway switches
When the iPad Activity screen starts sessions.list for gateway A and the user switches/forgets to gateway B before the request returns, this path still applies the old response and storeCachedChatSessions resolves the current cache identity. I checked the request path (apps/ios/Sources/Chat/IOSGatewayChatTransport.swift:186-189 → apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift:378-398), and there is no cancellation or route/identity check after the await, so A's sessions can be painted and persisted into B's offline cache. Capture the chatViewModelIdentityID/cache identity before the await and re-check it before assigning or storing.
Useful? React with 👍 / 👎.
Cache-first cold open for the iOS chat tab: the last known transcript and session list render immediately from a local SQLite cache, then live gateway history replaces them wholesale through the existing reconciliation path. When the gateway is unreachable, recent sessions and transcripts stay browsable read-only; sending remains gated by the existing connection state. - New OpenClawChatTranscriptCache protocol seam plus SQLite-backed store in OpenClawChatUI (raw SQLite3, no new dependencies), scoped per gateway stableID so transcripts never leak across paired gateways. - Bounds: 50 sessions, 50 transcripts, 200 messages per session; text rows only (attachment/binary payloads and tool arguments are stripped). - Disposable cache: schema versioned via user_version; any open/schema/decode mismatch drops and rebuilds silently. File protection completeUntilFirstUserAuthentication on iOS. - View model pre-paints from cache only until a live response applies (hasAppliedLiveHistory/hasAppliedLiveSessions guards); write-through is chained and off the render path. - iOS wiring keys the chat view model identity on transport mode plus cache gateway ID so switching paired gateways rebuilds the view model. Part of #100194
7551c63 to
ec1ee2b
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ec1ee2bd5c
ℹ️ 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".
| self.viewModel = OpenClawChatViewModel( | ||
| sessionKey: sessionKey, | ||
| transport: self.appModel.makeChatTransport(), | ||
| transcriptCache: self.appModel.makeChatTranscriptCache(), |
There was a problem hiding this comment.
Bind transcript cache to the transport route
When a user switches from gateway A to gateway B, connectToGateway updates the active config before the old operator GatewayNodeSession is necessarily disconnected (I checked prepareForGatewayConnect: it cancels the old operator task but does not immediately clear the session channel), so this rebuilt chat model can pair makeChatTransport() for A's still-installed channel with makeChatTranscriptCache() scoped from the new active B identity. If ChatView.onAppear loads during that window, A's history can be written into B's per-gateway database and later shown offline under B; create/use the cache only for the actual connected route or make the transport route-checked before assigning the new cache identity.
Useful? React with 👍 / 👎.
|
Merged via squash.
|
…penclaw#100219) * feat(ios): read-only offline cache for chat sessions and transcripts Cache-first cold open for the iOS chat tab: the last known transcript and session list render immediately from a local SQLite cache, then live gateway history replaces them wholesale through the existing reconciliation path. When the gateway is unreachable, recent sessions and transcripts stay browsable read-only; sending remains gated by the existing connection state. - New OpenClawChatTranscriptCache protocol seam plus SQLite-backed store in OpenClawChatUI (raw SQLite3, no new dependencies), scoped per gateway stableID so transcripts never leak across paired gateways. - Bounds: 50 sessions, 50 transcripts, 200 messages per session; text rows only (attachment/binary payloads and tool arguments are stripped). - Disposable cache: schema versioned via user_version; any open/schema/decode mismatch drops and rebuilds silently. File protection completeUntilFirstUserAuthentication on iOS. - View model pre-paints from cache only until a live response applies (hasAppliedLiveHistory/hasAppliedLiveSessions guards); write-through is chained and off the render path. - iOS wiring keys the chat view model identity on transport mode plus cache gateway ID so switching paired gateways rebuilds the view model. Part of openclaw#100194 * fix(ios): harden offline transcript cache * fix(ios): expose offline cached sessions * fix(ios): isolate transcript caches by gateway * chore(i18n): sync native inventory * fix(ios): allow cache extension to replace messages
…penclaw#100219) * feat(ios): read-only offline cache for chat sessions and transcripts Cache-first cold open for the iOS chat tab: the last known transcript and session list render immediately from a local SQLite cache, then live gateway history replaces them wholesale through the existing reconciliation path. When the gateway is unreachable, recent sessions and transcripts stay browsable read-only; sending remains gated by the existing connection state. - New OpenClawChatTranscriptCache protocol seam plus SQLite-backed store in OpenClawChatUI (raw SQLite3, no new dependencies), scoped per gateway stableID so transcripts never leak across paired gateways. - Bounds: 50 sessions, 50 transcripts, 200 messages per session; text rows only (attachment/binary payloads and tool arguments are stripped). - Disposable cache: schema versioned via user_version; any open/schema/decode mismatch drops and rebuilds silently. File protection completeUntilFirstUserAuthentication on iOS. - View model pre-paints from cache only until a live response applies (hasAppliedLiveHistory/hasAppliedLiveSessions guards); write-through is chained and off the render path. - iOS wiring keys the chat view model identity on transport mode plus cache gateway ID so switching paired gateways rebuilds the view model. Part of openclaw#100194 * fix(ios): harden offline transcript cache * fix(ios): expose offline cached sessions * fix(ios): isolate transcript caches by gateway * chore(i18n): sync native inventory * fix(ios): allow cache extension to replace messages
What Problem This Solves
Resolves a problem where opening the chat tab showed a blank transcript until the gateway responded, and showed nothing at all when the gateway was unreachable. All chat state was in memory only, so cold opens and offline periods left users staring at an empty screen even for conversations they had just been reading.
Part of #100194
Why This Change Was Made
Adds a small, disposable, read-only offline cache of recent chat sessions and transcripts, scoped per paired gateway identity. Cold opens paint the last known transcript immediately; authoritative gateway history then replaces it through the existing reconciliation path. Post-send incomplete history cannot cache optimistic echoes or erase the last canonical snapshot.
The store is versioned SQLite with drop-and-rebuild on mismatch, bounded retention, no migrations, and no new dependency. Text rows only; attachment payloads and tool arguments are stripped. Each gateway gets a SHA-256-named database, so bootstrap replacement can retire and physically remove only that gateway's file and sidecars without deleting other paired gateways' caches. iOS opens cache files with Complete file protection and treats locked-device protected-data unavailability as a temporary cache miss.
User Impact
The chat tab renders the last known conversation immediately on launch. Cached sessions remain visible and openable from Home, the full Sessions screen, and iPad Activity while disconnected. Sending stays disabled until the connection is healthy. Switching gateways rebuilds cache consumers immediately; reset/forget physically removes the appropriate cached conversation files.
Evidence
ec1ee2bd5c468874d23314649ef9e4df2c0d1a85; landed as5d8293c1fe404f607b3ecddcb9f12d1753a4de51.28759147624— success, includingmacos-swift,ios-build, QA Smoke, Android, Node, docs, security, and native i18n lanes.28759147630— success.swiftformat --lint --config config/swiftformat <17 touched Swift files>— pass.swiftlint lint --config apps/ios/.swiftlint.yml <touched Swift files>— no new errors; only pre-existing size/complexity warnings.node --import tsx scripts/native-app-i18n.ts check— pass, 2542 entries, no drift.node scripts/ios-write-swift-filelist.mjs— pass; both new shared cache files are included in the iOS lint input.tbx_01kwt78aakecg7xbakwev6mxcn: Linux Swift attempts reached existing Apple-only package blockers (AudioToolbox; then CoreFoundation symbols with Talk disabled). Exact-head Apple CI supplied the compile/test proof.patch is correct, confidence 0.77).Known proof gap: no live simulator/device visual recording for cold-open, offline navigation, reconnect, and purge behavior.