Skip to content

feat(android): durable offline command outbox for chat sends#100290

Merged
steipete merged 1 commit into
mainfrom
feat/android-offline-outbox
Jul 5, 2026
Merged

feat(android): durable offline command outbox for chat sends#100290
steipete merged 1 commit into
mainfrom
feat/android-offline-outbox

Conversation

@steipete

@steipete steipete commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Stacked on the Android chat offline cache branch (#100227, feat/android-chat-offline-cache); lands after it.

Part of #46664

What Problem This Solves

Text commands typed while the gateway is unreachable are currently refused with an error and lost. Users routinely open chat while disconnected (transit, flaky Wi-Fi, gateway restart) and expect what they typed to be delivered once the connection returns.

Why This Change Was Made

A durable outbox lets users keep issuing text commands offline and have them delivered in order after reconnect, without inventing a second reachability source or a new store: it extends the existing chat cache database (schema v2, destructive migration per that DB's established contract) and the existing gateway-health transition as the flush trigger. Every row's UUID is its chat.send idempotency key, so delivery is at-least-once with gateway-side dedup — history replaces each queued bubble exactly once. Conservative defaults: 50 queued per gateway, 48h expiry to "failed (expired)" instead of firing stale commands, 3 send attempts with backoff, transport drops keep rows queued without burning attempts, sending rows revert to queued on startup, and pairing/auth resets purge the outbox with the cache. Text-only v1; attachments stay online-only.

User Impact

  • Sending a text message while offline queues it durably: it appears in the transcript immediately with a "Queued — sends when reconnected" state and survives app restarts.
  • On reconnect, queued messages flush strictly in order; the thinking level chosen at enqueue time is preserved.
  • Failures park as visibly failed after 3 attempts with Retry/Delete inline on the bubble; Retry refreshes the timestamp so an expired row can actually resend. Refused sends restore the draft instead of dropping it.

Evidence

  • RoomChatCommandOutboxTest (Robolectric, 10 tests): persistence, FIFO under same-millisecond clock collisions, MAX_QUEUED refusal, 48h boundary expiry, sending→queued recovery, retry-refresh, gateway scoping, purges.
  • ChatControllerOutboxTest (12 tests): enqueue-while-offline across controller recreation, ordered flush with idempotency keys asserted, ack removes row with exactly one history bubble, rejected→failed-after-3, transport-failure requeue, main-alias rows flushed to the canonical main session, purge on session delete.
  • cd apps/android && ./gradlew :app:testPlayDebugUnitTest — full lane green; ktlint clean on touched files; native i18n inventory synced.
  • Codex autoreview: 4 runs across 3 fix cycles; 5 accepted findings fixed (enqueue-time thinking level, expired-retry dead end, main-alias misrouting, transport failures burning attempts, draft loss + purge race); final run clean ("patch is correct").

Follow-up: voice capture (MicCaptureManager) still buffers recognized turns in an in-memory FIFO while offline; its queue is coupled to per-turn reply correlation and TTS playback, so routing it into the durable outbox is deferred to a dedicated change. No on-device screenshot pass yet (unit/Robolectric proof).

Update (rebase onto the reworked cache base): the base branch's cache API was rebuilt upstream around explicit gateway-scope binding (ChatCacheScope); the outbox is now re-expressed on that model — enqueue/publish/flush bind to the captured scope, a pairing/connection switch mid-flush stops the flush and clears presented rows, per-item retries re-check health and scope around each backoff (old-gateway text can never replay into a new gateway), and updateStatus is a claiming write so a row deleted through any path is never resent. Squashed to a single commit on the new base; 24 outbox tests green plus the full Play unit lane. One autoreview finding consciously rejected with inline rationale: destructive migration for the outbox table is the established design contract for this database (losing at most a few unsent commands at a rare schema-bump boundary is the accepted trade-off, documented at the site).

@openclaw-barnacle openclaw-barnacle Bot added app: android App: android size: XL maintainer Maintainer-authored PR labels Jul 5, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 12edbf9c20

ℹ️ 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 on lines +831 to +832
} catch (err: Throwable) {
OutboxSendResult.TransportFailure(err.message ?: "send failed")

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.

P2 Badge Don't classify gateway rejections as transport drops

When a queued send reaches the gateway but is rejected at the RPC layer, GatewaySession.request throws for ok:false responses (apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt:351-359), and chat.send uses that path for cases like archived/deleted sessions (src/gateway/server-methods/chat.ts:3900-3903). This catch treats those application rejections as TransportFailure, so the row is put back in Queued with retryCount unchanged and health forced false, leaving a permanently invalid command to retry forever instead of surfacing as failed/deletable after the configured attempts.

Useful? React with 👍 / 👎.

Comment on lines +281 to +283
// Same monotonic clamp as enqueue: a retried row re-joins the end of the FIFO queue.
val createdAt = maxOf(nowMs, (dao.maxCreatedAt(gateway) ?: Long.MIN_VALUE) + 1)
dao.requeueForRetry(id = id, createdAtMs = createdAt, status = ChatOutboxStatus.Queued.dbValue)

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.

P2 Badge Regenerate the idempotency key when retrying failed rows

For a row that reaches Failed after a terminal ACK, user Retry only refreshes status/timestamp and keeps the same row id. The gateway caches chat.send results by chat:<runId> before accepting new work (src/gateway/server-methods/chat.ts:3841-3846), while the send path uses this row id as the idempotency key, so retrying a terminal timeout/error replays the cached failure instead of starting a fresh turn. Keep the stable key for ambiguous redelivery, but rotate it for explicit user retry after terminal failure.

Useful? React with 👍 / 👎.

Comment on lines +1146 to +1148
commandOutbox?.let {
runCatching { it.deleteForSession(key) }
publishOutbox()

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.

P2 Badge Purge main-alias rows when the canonical session is deleted

Rows queued before the gateway hello are stored with sessionKey == "main", and the flush path later maps them to the canonical appliedMainSessionKey. If the gateway deletes that canonical main session, this exact-key purge misses the pre-hello "main" rows, so they remain visible and can be flushed into the deleted session on the next reconnect. When key is the current canonical main key, purge the "main" alias as well.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed July 5, 2026, 4:41 PM ET / 20:41 UTC.

Summary
Adds an Android Room-backed durable offline text-command outbox with queued/failed chat bubbles, retry/delete controls, reconnect flushing, i18n inventory updates, and Robolectric/controller tests.

PR surface: Other +1581. Total +1581 across 15 files.

Reproducibility: yes. for review findings: the destructive migration, retry-key reuse, RPC-rejection catch, main-alias purge, live-ordering, composition-scope handoff, and timeline anchoring issues are source-reproducible from the PR head and current gateway/controller contracts.

Review metrics: 1 noteworthy metric.

  • Stored user-input model: 1 Room database version bump with 1 new outbox entity. The new table stores unsent user commands, so upgrade behavior matters beyond ordinary cache invalidation.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🦪 silver shellfish
Result: blocked until real behavior proof from a real setup is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Add redacted Android emulator or device proof showing offline enqueue, restart persistence, reconnect flush, and visible failed-row controls.
  • Resolve the outbox migration/data-loss decision with either non-destructive storage or explicit maintainer acceptance.
  • [P1] Fix the idempotency retry, scope, purge, live-ordering, composition-lifetime, and timeline anchoring defects.

Proof guidance:

  • [P1] Needs real behavior proof before merge: The PR body provides unit/Robolectric/CI evidence and explicitly says there is no on-device screenshot pass, so real Android behavior proof is still missing. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Risk before merge

  • [P1] Future schema bumps can erase queued user commands because the PR adds durable outbox rows to a destructively migrated cache database.
  • [P1] Automatic and explicit retries can replay cached terminal chat.send failures because the PR reuses the same row id as the idempotency key.
  • [P1] Queued commands can still be misclassified, misordered, or sent across a gateway-scope switch in source-reproducible paths.
  • [P1] Contributor real behavior proof is only unit/Robolectric/CI evidence, so the visible Android enqueue/reconnect/retry flow has not been shown in a real setup.

Maintainer options:

  1. Make outbox storage non-destructive (recommended)
    Add an explicit Room migration or separate durable owner for queued commands so app upgrades do not delete unsent user input.
  2. Accept the data-loss contract
    A maintainer can intentionally accept destructive migration, but the PR should document that unsent queued commands may be dropped on schema bumps.
  3. Pause for storage ownership
    Close or pause this branch if the durable outbox should wait for a broader mobile storage contract.

Next step before merge

  • [P1] The PR has a protected maintainer label plus product/storage judgment and contributor-proof blockers; automation should not pick the storage contract or merge path.

Maintainer decision needed

  • Question: Should Android offline text commands be treated as durable user state that must survive app schema upgrades, or may maintainers accept destructive migration for unsent queued commands in this feature?
  • Rationale: The PR stores user-entered commands in the cache database and explicitly accepts losing them at schema-bump boundaries, which is a maintainer product/storage decision rather than a mechanical code choice.
  • Likely owner: steipete — Recent merged Android chat/cache history points to steipete as the strongest available routing candidate for this storage-owner decision.
  • Options:
    • Require durable migration (recommended): Keep the feature but move outbox rows to a non-destructive Room migration/storage path before merge.
    • Accept destructive outbox loss: Land only if maintainers explicitly accept that unsent queued commands may be erased at schema-bump upgrade boundaries and want that documented as the product contract.
    • Pause for storage ownership: Pause this PR until a broader mobile durable-command storage contract is approved.

Security
Needs attention: The diff persists private unsent command text, so privacy, retention, deletion, diagnostics redaction, and upgrade behavior need maintainer acceptance before merge.

Review findings

  • [P2] Recheck scope before the first outbox send — apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt:887
  • [P2] Classify RPC rejections separately from drops — apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt:970-971
  • [P2] Rotate keys for explicit failed-row retries — apps/android/app/src/main/java/ai/openclaw/app/chat/ChatCommandOutbox.kt:291-293
Review details

Best possible solution:

Keep the Android text-outbox direction, but require non-destructive durable storage or explicit maintainer acceptance of the data-loss contract, fix the delivery/scroll blockers, and add redacted Android emulator or device proof before merge.

Do we have a high-confidence way to reproduce the issue?

Yes for review findings: the destructive migration, retry-key reuse, RPC-rejection catch, main-alias purge, live-ordering, composition-scope handoff, and timeline anchoring issues are source-reproducible from the PR head and current gateway/controller contracts.

Is this the best way to solve the issue?

No. A Room-backed Android text outbox is a plausible staged design, but this head is not the best mergeable form until delivery semantics, timeline behavior, storage/security decisions, and real Android proof are resolved.

Full review comments:

  • [P2] Recheck scope before the first outbox send — apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt:887
    sendOutboxItem claims the row and then calls attemptOutboxSend without rechecking health or the gateway scope. A disconnect or pairing switch between the loop condition and this call can send old-gateway text through a connection that is being torn down or replaced.
    Confidence: 0.9
  • [P2] Classify RPC rejections separately from drops — apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt:970-971
    GatewaySession.request throws for structured ok:false chat.send responses, including deleted or archived session rejections, and this catch maps every throw to TransportFailure. Invalid rows then stay queued without burning attempts instead of becoming failed and deletable.
    Confidence: 0.92
  • [P2] Rotate keys for explicit failed-row retries — apps/android/app/src/main/java/ai/openclaw/app/chat/ChatCommandOutbox.kt:291-293
    requeueForRetry keeps the row id, but that id is also the chat.send idempotency key. Retrying after a terminal failure can replay the gateway's cached failure instead of starting a fresh turn.
    Confidence: 0.88
  • [P2] Rotate keys across terminal retry attempts — apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt:956-958
    The automatic retry loop sends the same row id as the idempotency key on every attempt. A transient terminal error or timeout ACK can be cached by the gateway and replayed for attempts 2 and 3, burning retries without re-running the turn.
    Confidence: 0.9
  • [P2] Purge main-alias rows on canonical delete — apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt:1315-1317
    Rows queued before gateway hello can have sessionKey == "main", while flush normalizes them to the canonical main session. Deleting the canonical main session only purges the exact key here, so pre-hello rows can remain visible and later target the deleted session.
    Confidence: 0.9
  • [P2] Stop destructively migrating durable outbox rows — apps/android/app/src/main/java/ai/openclaw/app/chat/ChatTranscriptCache.kt:175-178
    Adding OutboxCommandEntity to this destructively migrated database makes queued user commands disposable on a future schema bump. The outbox needs a non-destructive migration path, a separate durable owner, or explicit maintainer acceptance before shipping.
    Confidence: 0.9
  • [P2] Move send handoff out of composition scope — apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatComposer.kt:275-276
    The composer clears the draft before launching the suspending send in a composition-bound coroutine. If the sheet or screen is dismissed, navigated away, or recreated right after tapping Send, cancellation can prevent runtime send/queue and the restore path may never run.
    Confidence: 0.86
  • [P2] Queue live sends behind pending outbox rows — apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt:341-351
    Once health is true, sendMessageAwaitAcceptance starts a fresh live chat.send without checking queued or sending durable rows. A newer command can execute before the offline backlog after cold open or reconnect, violating the claimed per-session FIFO behavior.
    Confidence: 0.84
  • [P2] Treat queued outbox rows as new user turns — apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatTimeline.kt:48
    Outbox rows render as user bubbles, but the latest-user tracking still ignores OutboxCommand. The reader's auto-follow path only treats latest user-message version changes as new user turns, so adding a queued row can leave the user anchored on the previous prompt.
    Confidence: 0.86

Overall correctness: patch is incorrect
Overall confidence: 0.91

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 5e0504aa437e.

Label changes

Label justifications:

  • P2: This is a substantial Android feature with bounded user impact and merge-sensitive storage/delivery semantics.
  • merge-risk: 🚨 compatibility: The PR changes the Room schema and keeps destructive migration for rows that now include user-entered commands.
  • merge-risk: 🚨 message-delivery: The PR changes offline enqueue, retry, reconnect flush, and ordering for chat sends.
  • merge-risk: 🚨 security-boundary: The PR persists command text locally and relies on gateway/session scoping to avoid cross-gateway replay.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🦪 silver shellfish.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The PR body provides unit/Robolectric/CI evidence and explicitly says there is no on-device screenshot pass, so real Android behavior proof is still missing. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Other +1581. Total +1581 across 15 files.

View PR surface stats
Area Files Added Removed Net
Source 0 0 0 0
Tests 0 0 0 0
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 15 1665 84 +1581
Total 15 1665 84 +1581

Security concerns:

  • [medium] Review durable command text storage boundary — apps/android/app/src/main/java/ai/openclaw/app/chat/ChatCommandOutbox.kt:131
    OutboxCommandEntity.text stores user-authored commands locally before they reach the gateway; maintainers should explicitly accept or adjust the privacy, retention, diagnostics, deletion, and upgrade-loss boundary.
    Confidence: 0.88

Acceptance criteria:

  • [P1] Redacted Android emulator/device proof of offline enqueue, restart persistence, reconnect flush, failed-row retry/delete controls, and ordering around pending outbox rows.
  • [P1] Focused Android unit/Robolectric outbox tests after storage and retry fixes, including main-alias purge and timeline follow behavior.
  • [P1] cd apps/android && ./gradlew :app:testPlayDebugUnitTest after the repair branch updates.

What I checked:

Likely related people:

  • steipete: Merged current-main history for the Android chat cache and ChatController path is concentrated in commits authored by Peter Steinberger, including the Android read-only offline cache prerequisite PR. (role: recent Android chat/cache area contributor; confidence: high; commits: 09f9a85145c0, f1641571f119, 85c1e0971f90; files: apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt, apps/android/app/src/main/java/ai/openclaw/app/chat/ChatTranscriptCache.kt, apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatTimeline.kt)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (8 earlier review cycles)
  • reviewed 2026-07-05T11:07:07.243Z sha 12edbf9 :: needs real behavior proof before merge. :: [P2] Classify gateway rejections separately from transport drops | [P2] Rotate the idempotency key on explicit failed-row retry | [P2] Purge main-alias rows when deleting the canonical main session | [P2] Regenerate the native i18n inventory
  • reviewed 2026-07-05T13:52:52.707Z sha 675432d :: needs real behavior proof before merge. :: [P2] Classify gateway RPC rejections separately from drops | [P2] Rotate the key for explicit failed-row retries | [P2] Purge main-alias rows on canonical main delete | [P2] Keep durable outbox rows out of destructive migrations | [P2] Move chat dispatch out of composition scopes
  • reviewed 2026-07-05T16:54:20.055Z sha 75a7446 :: needs real behavior proof before merge. :: [P2] Classify gateway RPC rejections separately from drops | [P2] Rotate the key for explicit failed-row retries | [P2] Purge main-alias rows on canonical main delete | [P2] Keep durable outbox rows out of destructive migrations | [P2] Move chat dispatch out of composition scopes | [P2] Queue live sends behind pending outbox rows
  • reviewed 2026-07-05T17:00:39.258Z sha 75a7446 :: needs real behavior proof before merge. :: [P2] Separate RPC rejections from transport drops | [P2] Rotate failed-row retry idempotency keys | [P2] Rotate keys between terminal retry attempts | [P2] Purge main-alias outbox rows on canonical delete | [P2] Use non-destructive storage for durable outbox rows | [P2] Move queued sends out of composition scope
  • reviewed 2026-07-05T19:41:50.066Z sha 9e4a937 :: needs real behavior proof before merge. :: [P2] Separate RPC rejections from transport drops | [P2] Rotate failed-row retry idempotency keys | [P2] Rotate keys between terminal retry attempts | [P2] Purge main-alias outbox rows on canonical delete | [P2] Use non-destructive storage for durable outbox rows | [P2] Move queued sends out of composition scope
  • reviewed 2026-07-05T20:00:15.391Z sha bc36e19 :: needs real behavior proof before merge. :: [P2] Recheck scope before the first outbox send | [P2] Classify RPC rejections separately from drops | [P2] Rotate keys for failed-row retry | [P2] Rotate keys across terminal retry attempts | [P2] Purge main-alias rows on canonical delete | [P2] Stop destructively migrating durable outbox rows
  • reviewed 2026-07-05T20:10:52.152Z sha 1366861 :: needs real behavior proof before merge. :: [P2] Recheck scope before the first outbox send | [P2] Classify RPC rejections separately from drops | [P2] Rotate keys for failed-row retry | [P2] Rotate keys across terminal retry attempts | [P2] Purge main-alias rows on canonical delete | [P2] Stop destructively migrating durable outbox rows
  • reviewed 2026-07-05T20:25:06.637Z sha 91b5abc :: needs real behavior proof before merge. :: [P2] Recheck scope before the first outbox send | [P2] Classify RPC rejections separately from drops | [P2] Rotate keys for explicit failed-row retries | [P2] Rotate keys across terminal retry attempts | [P2] Purge main-alias rows on canonical delete | [P2] Stop destructively migrating durable outbox rows

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. labels Jul 5, 2026
@steipete
steipete force-pushed the feat/android-chat-offline-cache branch from e0701c7 to a19a3d4 Compare July 5, 2026 11:14
@steipete
steipete force-pushed the feat/android-chat-offline-cache branch from df9eb0e to b658f9b Compare July 5, 2026 12:00
@steipete
steipete force-pushed the feat/android-offline-outbox branch from 12edbf9 to 675432d Compare July 5, 2026 13:39

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 675432df45

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

@Database(
entities = [CachedSessionEntity::class, CachedMessageEntity::class],
version = 1,
entities = [CachedSessionEntity::class, CachedMessageEntity::class, OutboxCommandEntity::class],

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.

P2 Badge Keep durable outbox rows out of destructive migrations

Adding OutboxCommandEntity to this database makes the existing destructive migration apply to durable user input: when a user has queued offline commands and installs a later build that bumps this Room schema, .fallbackToDestructiveMigration(dropAllTables = true) will delete those rows before they can be sent, expired, or manually removed. This store is no longer only disposable transcript cache, so the outbox needs a migrated/non-destructive owner before it ships.

Useful? React with 👍 / 👎.

Comment on lines +275 to +276
sendScope.launch {
val accepted = onSend(message)

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.

P2 Badge Keep chat dispatch out of composition-bound scopes

When the sheet/screen leaves composition right after tapping Send (for example dismissal, navigation, or rotation), rememberCoroutineScope is cancelled; this new coroutine now owns onSend(message)/sendChatAwaitAcceptance after the draft has already been cleared, so the message can be cancelled before the runtime queues or sends it and the restore path may never run. The previous path handed off to runtime scope immediately; keep dispatch in ViewModel/runtime scope and only report the acceptance result back for draft restoration (the same new pattern in ChatScreen needs the same treatment).

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. and removed merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. labels Jul 5, 2026
@steipete
steipete force-pushed the feat/android-chat-offline-cache branch from b658f9b to f2d1528 Compare July 5, 2026 16:47
@steipete
steipete force-pushed the feat/android-offline-outbox branch from 675432d to 75a7446 Compare July 5, 2026 16:48

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75a74464d1

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

var attempts = item.retryCount
while (true) {
val error =
when (val result = attemptOutboxSend(item)) {

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.

P2 Badge Rotate keys for automatic terminal-failure retries

Separate from the already-noted user Retry path, the automatic retry loop also calls attemptOutboxSend(item) with the same row id each time, and that id is sent as the chat.send idempotency key. The gateway caches chat:<id> terminal results before later duplicate checks (src/gateway/server-methods/chat.ts:3841-3846, src/gateway/server-methods/chat.ts:5771-5784), so a transient terminal error/timeout ACK is replayed for attempts 2 and 3 instead of re-running the turn, burning the configured attempts and parking the row as Failed immediately. Rotate an attempt key for terminal retries, or treat terminal ACKs as non-retryable.

Useful? React with 👍 / 👎.

@steipete
steipete force-pushed the feat/android-chat-offline-cache branch from f2d1528 to b9fc80e Compare July 5, 2026 19:23
@steipete
steipete force-pushed the feat/android-offline-outbox branch from 75a7446 to 9e4a937 Compare July 5, 2026 19:24
@steipete
steipete force-pushed the feat/android-chat-offline-cache branch from b9fc80e to 17b5cd4 Compare July 5, 2026 19:43
@steipete
steipete force-pushed the feat/android-offline-outbox branch from 9e4a937 to bc36e19 Compare July 5, 2026 19:46

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc36e19dfe

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

var attempts = item.retryCount
while (true) {
val error =
when (val result = attemptOutboxSend(item)) {

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.

P2 Badge Recheck the gateway scope before sending claimed rows

If the user disconnects or starts pairing a different gateway after the flush loop loads a row but before this call, onGatewayScopeChanging() has already flipped health/scope, yet this path still sends the captured old-gateway row once before the later scope checks run. That can replay a queued command into a connection that is being torn down or has just been replaced; add the same _healthOk/currentCacheScope() == flushScope guard immediately before the first attemptOutboxSend.

Useful? React with 👍 / 👎.

@steipete
steipete force-pushed the feat/android-chat-offline-cache branch from 17b5cd4 to a259a40 Compare July 5, 2026 20:02
@steipete
steipete force-pushed the feat/android-offline-outbox branch from bc36e19 to 1366861 Compare July 5, 2026 20:03

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 136686133a

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

val items =
buildList {
// reverseLayout: index 0 renders bottom-most; queued commands are the newest user input.
outboxItems.asReversed().forEach { item -> add(ChatTimelineItem.OutboxCommand(item)) }

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.

P2 Badge Treat queued outbox rows as new user turns

When a text send is accepted offline, this inserts the queued bubble at index 0, but the later latestUserMessage calculation still only considers persisted Message rows, and ChatReaderState.onTimelineChanged only auto-follows a new user turn when latestUserMessageVersion changes. In a chat that already has a user message and is following the read anchor, adding a queued row changes latestContentVersion but not the latest user version, so the reader scrolls back to the previous prompt instead of the new queued bubble; the user can miss that the offline command was accepted.

Useful? React with 👍 / 👎.

Base automatically changed from feat/android-chat-offline-cache to main July 5, 2026 20:14
@steipete
steipete force-pushed the feat/android-offline-outbox branch from 1366861 to 91b5abc Compare July 5, 2026 20:17
@steipete
steipete force-pushed the feat/android-offline-outbox branch from 91b5abc to fe50540 Compare July 5, 2026 20:30

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fe50540022

ℹ️ 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 on lines +890 to +892
runCatching { outbox.delete(item.id) }
publishOutbox()
return OutboxSendOutcome.Sent

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.

P2 Badge Track outbox runs after gateway acceptance

When an offline row flushes and chat.send returns a non-terminal {runId, status:"in_flight"} ack, this branch deletes the row and reports Sent without registering the returned run in pendingRuns. For a queued message in the currently visible session, subsequent chat deltas are ignored because handleChatEvent only renders run IDs already in pendingRuns, and the composer stays enabled with pendingRunCount == 0 while the gateway is still working. Mirror the live-send path by adopting/arming the accepted run before removing the queued bubble.

Useful? React with 👍 / 👎.

@steipete
steipete merged commit 77575d7 into main Jul 5, 2026
77 checks passed
@steipete
steipete deleted the feat/android-offline-outbox branch July 5, 2026 20:45
@steipete

steipete commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

Merged via squash.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: android App: android maintainer Maintainer-authored PR merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: XL status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant