Phones aren't tiny desktops.
They're a key, a capture rig, a remote.
Android v1.0 is repositioned around three jobs: StrongBox hardware DID wallet, five mobile-capture pieces (voice / camera / location / share / push), and REMOTE invocation of the 139 desktop skills. Mirrors Claude Desktop / Mobile's split — desktop is the workstation, phone is its extension.
v5.0.3.54. Each release tag carries 8 desktop bundles + 4 Android bundles together.
L1 · L2 · L3 — each layer earns its keep.
We stopped competing with desktop on skill count. The phone does the three things it's actually best at: guard the private key, capture from the field, drive the desktop remotely.
StrongBox DID wallet
Hardware key store (Android Keystore + StrongBox HSM) guards W3C DID v2 private keys. BIP-39 mnemonic, biometric (fingerprint / face) unlock, multi-DID switching, automatic migration from any legacy plaintext store.
Field capture, five pieces
All five wired: VoiceMode (continuous SeedASR → LLM → TTS), CameraOCR (photo → OCR → note), LocationTagger (GPS foreground service), ShareReceiver (5 SharePayload kinds → KB), PushNotifier (4 notification channels + FCM scaffold).
REMOTE control
Phone → desktop, 23 REMOTE commands (audit of 795 suspend fns). RemoteSkillRegistry with file + method dual-granularity whitelist · ApprovalUI in 4 categories (Sign / Cowork / Marketplace / SystemCritical) · ProgressViewer for long tasks · §8.3 alias compat window.
Flow B QR pairing (default)
Desktop shows the QR, phone camera scans the desktop screen — same UX as WeChat / Alipay / Discord / WhatsApp Web. Phone-scans-desktop has dramatically better recognition rates than desktop-webcam-scans-tiny-phone-screen. Xiaomi 24115RA8EC E2E walked the full chain: desktop QR → ML Kit scan → signaling pair-ack → CLI writes SQLite → Vue list refreshes.
- · Entry: phone Settings → "Scan desktop QR" (recommended) / desktop V5, V6, web-shell all three
- · Tests: ScanDesktopPairingViewModel 10 · desktop-pair-handlers 19 · ZXing / ML Kit pass-through · adb-reverse E2E proven
- · Advanced path: Flow A (phone-generates / desktop-scans, Signal e2ee) kept as opt-in
Desktop ↔ Android two-way sync
Five resource types — Note / Conversation / DID / Community / Channel — plus tombstones, bidirectional walker. dagger.Lazy untangles 4 Hilt cycles; Room persists SyncRemoteCursor; sync.* JSON-RPC handlers wire transport. Gates 1–4 are strict Ed25519 verify. Private keys stay on phone, passwords never on the wire.
- · M2 → v1.2, 12 commits: desktop side 5 ResourceType walker + 52 tests · Android side SocialSyncWalker fills handlePullRpc + DID auth
- · Entry: desktop Settings → Sync → SyncMobile device manager + manual pairing; mobile Settings → Sync
- · Protocol: CommandRequest.auth nullable · listTombstones SQL filter · Android substitutes P2P session DID for device registry
Remote first: signaling-forward RPC.
Phone as remote — call desktop skills / list / status from the phone, low-bandwidth path first. signaling-relay forwards JSON-RPC 2.0; desktop side runs RelayClient + handlePairAckFromRelay, Android side runs SignalingRpcClient + RemoteOperateScreen. LAN direct first, falls back to public relay; PairedDesktopsStore persists paired peers.
- · Three-path decision: Plan C signal-forward (shipped first) / Plan A WebRTC DC (throughput) / Plan B STUN+TURN (NAT traversal) — three tiers compose
- · Measured ping: 100–400ms (relay region dependent); fine for everyday remote, Plan A migrates the hot path to DC
- · 20 new unit tests · key fixes: runCurrent over advanceUntilIdle for SharedFlow race / org.json over kotlinx-serialization to slim deps
WebRTC forwarding + STUN/TURN fallback.
signaling-relay forwards offer/answer/ice-candidate and injects the from field (from ws.peerId), so LAN and relay paths are interchangeable in mobileBridge.handleSignalingMessage. coturn 4.6 deployed at turn.chainlesschain.com (3478 UDP/TCP + 5349 TLS + 49152-65535 relay UDP), Let's Encrypt acme.sh auto-renew, HMAC-SHA1 use-auth-secret 24h-TTL ephemeral creds.
- · iceServers don't ship in the QR: 650 chars + 280px + error-H stalls 30s in practice. Pushed async after scan — desktop pair-ack matched ⇒ pushIceServersToMobile dual-send LAN + relay
chainlesschain:ice:config - · Android persistence: WebRTCClient.setOnForwardedMessageReceived intercepts ⇒ PairedDesktopsStore.iceServersJson · SignalingRpcClient mirror race-tolerant
- · No hardcoded secrets: CC_TURN_SECRET strictly required · Alibaba Cloud SG must open UDP/TCP 3478 / TCP 5349 / UDP 49152-65535 to 0.0.0.0/0
Same day, upgraded: 4-hop signaling collapsed to a 1-hop DataChannel.
v5.0.3.52 Plan A real-device validation (Xiaomi 24115RA8EC × Win desktop dev) surfaced one architectural issue: a 4-hop signaling chain (phone → router → public relay → desktop RelayClient) is fragile under NAT idle / cellular carrier-side TCP RST — any hop down kills the whole chain. Plan A.1 moves high-frequency / high-throughput terminal traffic onto a WebRTC DataChannel direct connection, bypassing every middle hop; signaling stays as a fallback.
- · Phase 1 Trap 1 fix: SignalClient.forwardedMessages migrated to multi-subscribe SharedFlow replacing the single-listener setOnForwardedMessageReceived → the ice:config interceptor was no longer silently overwritten when TerminalRpcClient.start() ran, fixing the bug where iceServers expired in 24h and cross-NAT became unreachable; + new WebRTCClient.dataChannelReady StateFlow (READY truly means DC OPEN)
- · Phase 2 DC fast path: SignalingRpcClient.invoke embeds a transport selector — connectionState==READY → send via DC; throws or not-ready → fallback signaling. Two listeners consume both streams; same requestId → same CompletableDeferred (second complete is a no-op, dual delivery is safe without explicit dedup)
- · Phase 3 handshake trigger + UI indicator: TerminalListViewModel async-triggers RemoteConnectionManager.connect on entry; chip shows "P2P direct" (green) vs "Relay path" (yellow)
- · Phase 4 bidirectional LRU dedup: Android dedups stdout by (sessionId|seq) with 256-LRU + exit by sessionId with 64-LRU on dual streams; desktop mobile-bridge.bridgeToLibp2p gets a 128-LRU / 30s-TTL keyed by payload.id for mobile→desktop command requests, guarding against duplicate PtyManager side effects
- · Phase 5 zero new code: DC failure fallback / auto-reconnect / recovery auto-switch / live UI mapping all fall out of Phase 2 + P2PClient's existing wiring for free
- · Perf: RTT p50 200-500ms → 30-80ms LAN / 50-200ms TURN · p99 1.5-30s with timeouts → 200-800ms · stability 20s-2min outages → hours-long sustained
- · Tests: Android +8 (TerminalRpcClient 3 dedup / SignalingRpcClient 4 transport / WebRTCClient 1 Trap 1) + desktop +14 (LRU dedup 5 + sendToMobile 5 + guard rails 2) · all 3 suites green (11 + 15 + 21 Android + 14 desktop) · 12-test regression fix (mockk relaxed StateFlow generic erasure)
Design doc: Android Remote Terminal Plan A.1 → · Real-device E2E §5.3 5-scenario matrix (LAN / cellular / double-NAT / DC forced-down / DC recovered) is on the user.
See your desktop terminals on your phone.
User pain: "I have many terminals open on the PC, can I see their output and type commands from Android?". Hard constraint — Windows externally-running terminals can't be attached by another process (OS handles are private). Solution: ChainlessChain desktop spawns new terminals via node-pty and streams stdin/stdout through signaling-relay into an Android xterm.js WebView.
- · Desktop Phase 1: PtyManager (lazy node-pty + 256 KB ring buffer + 24h idle kill + shell allowlist pwsh/cmd/bash/wsl + 8-session cap) · 8 WS topics (create/list/stdin/resize/close/history + server-push stdout/exit) · dangerous-keyword Electron messageBox confirm + permanent trust per-cmd cache
- · Phase 1.5 three shells: V5/V6 web-shell + V6 plugin widget + cc ui mirror (agent-runtime.startUiServer reuses dispatcher) · /terminal route + sidebar menu + slash command
- · Android Phase 3: TerminalRpcClient reuses envelope · WebView ↔ Kotlin JS bridge · xterm.js vendored into assets/terminal/ · softkey toolbar Ctrl/Tab/Esc/arrows/Ctrl+C/D
- · Phase 4 resilience: paired_devices/permission-gate · mobile-bridge stdout/exit per-peer subscription fan-out · reconnect terminal.history backfill
- · 162 new unit tests, all green · Desktop 61 / CLI 21 / Web Panel 17 + 3 real e2e (cc ui subprocess + real WS + shell stdin/stdout round-trip) / Android 10 · including real cmd.exe spawn streaming probe stdout back
Design doc: Android Remote Terminal Plan A → · User guide: Remote terminal →
Social goes from demo to production.
The 14-screen + 9-ViewModel + 4-Repository social scaffold (≈10K LOC) was built long ago,
but only MyQRCode / QRCodeScanner were actually wired; the other seven routes were
registerPlaceholder("temporarily simplified"),
and the SocialScreen Friends/Timeline tabs were hardcoded placeholder strings. Closed in one
pass: all routes bound to real composables, two new routes for NotificationCenter / BlockedUsers,
PostReportDao landed (the entity was in schema, DAO was missing), and PROFILE_QUERY/RESPONSE
protocol added for remote DID profile lookup.
- · NavGraph: 7 placeholders → real screens + 2 new routes · PublishPost / PostDetail / FriendDetail / UserProfile / AddFriend / CommentDetail / EditPost / NotificationCenter / BlockedUsers · spinner shown while the DID document loads
- · SocialScreen three tabs upgraded · Friends → FriendListScreen · Timeline → TimelineScreen (myDid via DIDViewModel) · Notifications → NotificationCenterScreen (filter / mark-all-read / cleanup menus)
- · PostReportDao · reportPost was building entities without inserting + getUserReports returned a hardcoded empty list. New DAO adds idempotent dedupe + status transitions + 8 in-memory Room tests
- · PROFILE_QUERY/RESPONSE protocol · RealtimeEventManager.queryProfile with 5s timeout · onSubscription clears the SharedFlow replay=0 subscribe-race · DefaultSelfProfileProvider wired in Application.delayedInit
- · 39 new tests, all green · 6 core-p2p / 14 feature-p2p / 8 core-database Room / 11 NavGraph + tab structural regressions · no emulator required
383+ unit tests · GA grade.
M1–M5 JVM-side fully green. Real-device M3 / M4 D2 / M6 perf / FCM credentials / docs sync — the five user-driven items — are tracked in the v1.0 GA checklist.
Stack: Jetpack Compose · Kotlin · Hilt DI · Room · MockK + Robolectric · Play Services (FusedLocationProvider / FCM scaffold) · StrongBox Keystore · ML Kit / ZXing scanner · Volcengine SeedASR 16 kHz streaming.
What v1.0 doesn't ship.
- · FCM in mainland China: unreliable behind the GFW · v1.1 will unify OPPO / Xiaomi / Huawei push channels
- · Single-peer pairing: v1.0 pairs one desktop · multi-device N-end in v1.1
- · Offline message queue: REMOTE requests aren't queued while desktop is offline · v1.1 adds replay
- · Real-device M3 / M4 D2, M6 perf, FCM creds, docs sync: 5 user-driven steps tracked in the GA checklist
Pick the right package.
Most devices since 2019 want arm64-v8a. If unsure or older, go universal. Google Play uploads use the AAB (developer-only). Every artifact is signed by GitHub Actions release.yml.
app-arm64-v8a-release.apk
app-universal-release.apk
app-armeabi-v7a-release.apk
app-release.aab
Settings → Check for updates — it walks
GitHub Releases v5.0.3.54 → DownloadManager →
REQUEST_INSTALL_PACKAGES. iOS coming.
Want the full notes? See GitHub Release v5.0.3.54 ↗ or the Android CHANGELOG ↗