mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-14 20:16:03 +00:00
* feat(android): durable offline chat with attachments and history-proof retirement Every chat send is journaled to the per-gateway Room outbox before any network attempt, so process death always has one durable recovery owner. Rows survive gateway ACKs as 'accepted' and retire only once the turn is proven in canonical chat.history by idempotency key; ambiguous outcomes (lost ACK, kill mid-send, gateway restart before the transcript write) park as delivery-unconfirmed for explicit retry, per the fail-closed model #103273 landed. An unproven accepted head briefly holds only its own session's queue, and retrying a parked head re-orders still-queued successors behind it. Offline sends now accept picked images and voice notes; attachment bytes persist as chunked BLOBs (512 KB chunks, 8 MB per message, 48 MB per gateway) admitted and retired atomically with their row. Pre-hello 'main' rows pin to the canonical session at first dispatch so later default-agent changes cannot retarget captured input; slash commands are connection-gated by a persisted epoch (legacy queued command rows migrate to a never-matching sentinel) and never auto-replay across reconnects. The Room store moves to schema v4 on top of v3's data-parking migration, adding the epoch column and attachment tables while preserving queued rows. The queued->sending claim is an atomic compare-and-set shared by the direct dispatch and the flush loop, the direct path waits for the startup recovery sweep before claiming, and failed reconnect attempts republish outbox rows so queued sends stay visible on offline cold starts. Proof: 1151 Android unit tests and :app:ktlintCheck green (new migration, storage, and controller coverage for admission, restarts, claims, pinning, gating, ordering, and byte round-trips); a live emulator scenario on the pre-integration build queued text+image offline, survived force-stop plus a device reboot, resent exactly once on reconnect (single <rowId>:user turn in the gateway transcript), and drained the outbox after canonical-history confirmation. Closes #104087. * fix(android): rearm outbox recovery when direct-send state persistence fails * fix(android): keep direct dispatch alive across caller cancellation The direct send's network phase now runs in the controller scope, so a cancelled UI scope (leaving the chat screen mid-send) can no longer strand a claimed row in 'sending' with no user action available; the dispatch completes and settles the row exactly once. Direct-path state persistence failures also re-arm the startup recovery sweep, mirroring the flush path's fail-closed handling. * fix(android): hand claim-persistence failures to the flush lane A direct-send claim that fails to persist no longer reads as a lost race: the admitted row is handed to the flush lane so a healthy connection still delivers it, and the flush path's fail-closed handling owns any repeated storage failure instead of the UI reporting success with no active owner. * fix(android): enforce connection gating and fail-closed parking on every send path The direct dispatch now rechecks a slash command's connection epoch after its durable claim, so a reconnect between admission and dispatch parks the command instead of replaying it on the new connection. Parking a stale gated row only counts once the write persists; a storage failure drops health, re-arms recovery, and halts the flush pass instead of spinning or dispatching the stale row. * fix(android): recognize gateway-acknowledged run ids in outbox ownership checks A chat.send ACK can return a run id that differs from the row's idempotency key (the direct path transfers local run ownership to it). Ownership, in-flight, backlog, and timeout-park checks now consider both ids, so reconciliation can no longer park — and Retry can no longer duplicate — a turn that is still running on the gateway. * fix(android): close remaining ack-ownership and persistence-failure gaps in the outbox Flushed sends acked under a divergent run id now transfer local run ownership like the direct path, so live runs cannot time out and surface spurious errors for delivered turns. A session pin that cannot persist stops the dispatch while the row is still safely queued. Reconcile and timeout parking only report changes their writes actually persisted, failing closed on storage errors. * docs(android): record why acknowledged run ids stay in-memory