* feat(gateway): propagate approvals to ancestor sessions with replay
Squash-rebased #103921 segment onto the native-clients tip on current main.
Session-scoped approval events publish sanitized pending/terminal
transitions to opted-in session audiences, with authoritative pending
replay on stream subscribe and durable-expiry reconciliation through the
owning manager. SessionApprovalEvent/Replay wire types export from the
approvals owner module; Swift models regenerated.
(cherry picked from commit 2d1dcf9747044710111d0c730fc46ba6013a165c)
(cherry picked from commit ccf88efd56b513d07599ffcee997bf0cddaf9adc)
(cherry picked from commit 8eb33f59f00ec9ee4625259b76bd7ae60d2cd480)
(cherry picked from commit c297cbc93c8401e9c79c20f242245a194f5dd236)
(cherry picked from commit 93d68f28601ff37d863da4db009504993a22533a)
(cherry picked from commit d285ccd8b2f212bb57e814107e7709c896e96b7d)
(cherry picked from commit 5c536982231402b1b58d1683794ea9948b32d077)
(cherry picked from commit 1646e5b6dc4dd20de54e10f110a5909be18d7d50)
(cherry picked from commit 35cf1b705b247fb282f4d56ffe74bd2fb238221f)
(cherry picked from commit 4f8ef3699d)
(cherry picked from commit 74fbdcc99d5fff062d67760a51dba4ee9e84479b)
(cherry picked from commit 222b285502681d2be14b1819798e9ee5555f431e)
(cherry picked from commit b9e744951f676b600e7c5322bfd04b0e99797c07)
(cherry picked from commit 9e904efde5)
(cherry picked from commit 678f2384fa)
(cherry picked from commit 72842e5cf6)
(cherry picked from commit be74c25e80e84a1f065053766f23efb84822e811)
(cherry picked from commit 93ba8c4b09)
(cherry picked from commit 8f23761372)
* feat(gateway): fail-closed plugin and tool approval gates
Squash-rebased #103932 segment onto the ancestor-propagation tip on
current main. Plugin node.invoke approvals claim a one-shot allow-once
decision before handing execution authority to the policy, so observation
or retry cannot replay a consumed approval; sibling tool gates bind
approval ids to their originating reviewer identity.
(cherry picked from commit 122df0d75281f572c012b6484ed5daf085d1d577)
(cherry picked from commit f23d8ac240a8dcf2a42c4daaae962263975a0ae8)
(cherry picked from commit 8632fb6436a224dac7a9a9ef0216884530de2c24)
(cherry picked from commit d9fe2dd5c53665e5732f5f3da5ed1907a686ade8)
(cherry picked from commit bb139f2c8aa36ddad70413e1ef79ff491a6f2ecd)
(cherry picked from commit 9b3e056b68ea515c7d4baef269289b6670570480)
(cherry picked from commit b11e66b59a7af1f3b08712de06864aed64d349e4)
(cherry picked from commit a12916ee592d03dfa35e86a2aaede4476a5d4e5d)
(cherry picked from commit d699de840fc8346892b9ae244fe3c3e8c4413032)
(cherry picked from commit 9b7e5a9608)
(cherry picked from commit 4b507593f1b2f10b4496984c84a2803b853e5c5e)
(cherry picked from commit 56408a186733c4eeb5aa76bd5907cde7b0cd3d5b)
(cherry picked from commit a4051d6d8353d39d6fa0b5d69c36e06c3cd3e796)
(cherry picked from commit 76829805a7)
(cherry picked from commit a8b493f934)
(cherry picked from commit aceb990597)
(cherry picked from commit a29b0e75482470c53a9fb4fe3f204e14cf71868f)
(cherry picked from commit 2e3be08653)
(cherry picked from commit c9ae3d7202)
* fix: main-gate repairs for the durable-approvals stack
- android: capture createdAtMs on the pending exec-approval write at
registration; canonical readback after a refresh that already replaced
the visible rows was dropping the approval instead of surfacing the
still-pending reconciliation message (#104913 merged without this)
- android: generous CI timeout ceilings in GatewayExecApprovalRuntimeTest
- agents: hermetic model-discovery test via the plugins/provider-runtime
boundary (lazy plugin-runtime resolution hangs vitest workers since #104770)
- cli: usage-cost settle-budget test asserts the budget bound on every call
instead of pinning the poll count (fast hosts fit a second poll in 50ms)
- protocol coverage: allowlist session.approval for ios/android (native
review rides exec.approval push/nudge delivery)
- docs map, native i18n inventory, plugin-sdk api baseline regenerated
* 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
* refactor(android): align talk levels and thinking state with iOS/macOS
TalkAudioLevel.kt ports OpenClawKit's level scale exactly: PCM16 RMS
through the shared 50 dB window replaces mean-abs (mic/talk/playback)
and linear peak (voice notes), so Android waveforms read identically to
iOS/macOS at the same loudness. The waveform's Thinking phase now comes
from a typed awaitingAgent StateFlow set alongside every status change
in TalkModeManager instead of the UI matching status string literals;
forgetting the flag at a new status site fails safe to Listening/Idle.
* fix(android): keep awaiting-agent flag intact on no-op status republishes
* chore(i18n): resync native inventory
* feat(voice): universal talk waveform driven by real audio levels
One shared Siri-style talk animation across iOS, watchOS, macOS, and
Android (TalkWaveformView + exact Compose port), replacing per-platform
fakes: synthetic speaking pulses, constant speech-detected power, static
Android bar rows, flat realtime listening. Listening/recording follow
live mic levels on every route; agent speech follows the real playback
envelope (AVAudioPlayer metering + playback-aligned PCM envelope +
WebRTC stats); voice-note recording shows a live capture wave.
* fix(android): order waveform imports and sync native i18n inventory
* chore(i18n): resync native inventory after rebase
* fix(android): keep cold-start gateway auto-connect from overriding explicit intents
NodeRuntime's init-time auto-connect now atomically claims the first gateway
lifecycle intent (CAS 0->1) and stands down permanently when any explicit
connect/disconnect/switch intent already exists, so a late discovery emission
can no longer override a user decision. This was also the root cause of the
CI-only flakes in GatewayBootstrapAuthTest (refreshGatewayConnection_* at
:616/:627 and switchToUndiscoveredGateway* on PRs #101002/#101387/#101396).
GatewaySession.Connection.sendRequestFrame now starts its response watcher
with CoroutineStart.ATOMIC: disconnect teardown cancels connectionJob right
after failPending(), and a DEFAULT-start watcher still queued for dispatch was
cancelled without running, silently dropping the terminal UNAVAILABLE onError
owed to fire-and-forget callers (root cause of the flaky
GatewaySessionInvokeTest.disconnectReportsUnknownOutcomeForFireAndForgetRpc).
Tests neutralize runtime background work (cancel+join the runtime scope)
before arming the registry and scripting lifecycle steps, and add regression
coverage for the auto-connect intent claim.
Proof: refresh flake reproduced at iteration 1 under taskset -c 0,1 on a
Blacksmith Linux box; post-fix 3 pinned stress rounds (120/150 looped
iterations) plus full :app:testPlayDebugUnitTest (109 classes, 0 failures)
and :app:ktlintCheck green on the same box.
* chore(i18n): resync native inventory line positions after NodeRuntime edits
* feat(android): render LaTeX display math in chat via bundled KaTeX
* fix(android): preserve fallback for invalid math
* docs(changelog): note Android display math
* Revert "docs(changelog): note Android display math"
This reverts commit 40356286a3.