Files
Josh Lehman 65ad180052 feat(knowledge): bundle the Knowledge memory plugin (#180139)
* feat(knowledge): import Knowledge plugin sources

Imported from openclaw/knowledge at 7847e55. Uses the public SDK root()
and the host's typed projectKey.

* style(knowledge): apply repository formatting and lint autofixes

* test(knowledge): port the Knowledge suite to vitest

* docs(knowledge): document the bundled Knowledge plugin

* refactor(knowledge): conform to bundled-plugin lint, types and file-size rules

* fix(knowledge): locate workers and packaged data from the runtime entry

Built output moves shared modules into host chunks, so module-relative
URLs no longer reach the storage and registry workers, the default
ontology or the sample vault. Resolve them from api.runtimeSource, ship
the data as static assets, and read files through @openclaw/fs-safe.

* chore(knowledge): register plugin inventory, config baseline and labels

* refactor(knowledge): drop standalone-host leftovers

Metadata-only registration has no runtime entry; locate packaged files
only when the host supplies one. Remove the older-host panel guard and
references to docs that did not move with the plugin.

* refactor(knowledge): remove unused exports and dead search helpers

* openclaw-ef3.7.3: open Knowledge databases through SDK helpers and stamp schema versions

Replace raw DatabaseSync construction and PRAGMA-based header versioning in the
operational (state.sqlite) and registry databases with openNodeSqliteDatabase,
runSqliteImmediateTransactionSync and a knowledge_schema version table. Stores
written before stamping are dated by their tables and migrated additively;
their native header is left untouched. Identity and registry upserts use Kysely.

* openclaw-ef3.7.2: port Knowledge transactions and conflict inserts to SDK patterns

BEGIN IMMEDIATE blocks become runSqliteImmediateTransactionSync, INSERT OR
IGNORE/REPLACE become Kysely onConflict inserts, and sqlite_master probes use
tableExists. Register Knowledge's worker-only SQL modules in workerModules.

* openclaw-ef3.7.4: move Knowledge JSON projections out of raw SQL

Publication source and receipt lookups use jsonPathValue with Kysely. The recent
projection parses each published plan once in TypeScript instead of json_each/
json_extract, and applies record eligibility and source grouping while streaming
newest-first events. Storage base transactions and upserts use SDK helpers.

* openclaw-ef3.7.6: document type assertion invariants in Knowledge storage modules

Every type assertion in the storage, registry, recent, retrieval, lifecycle,
session-activity and attachment modules now states its invariant, satisfying
the assertion SAFETY ratchet against main.

* openclaw-ef3.7.6: keep the Knowledge schema step type module-private

* openclaw-ef3.7.2: resolve Knowledge worker SQLite SDK modules through the host resolver

* openclaw-ef3.7.4: keep Knowledge SQL out of the control UI bundle

Recent activity SQL moves to recent-activity.ts so recent.ts only carries the
schemas the control UI imports, and session activity records retried calls with
a portable ON CONFLICT DO NOTHING prepared statement instead of the SQLite SDK.

* refactor(knowledge): move scan primitives to a leaf module

Breaks the evidence-files -> publication-files -> vault import cycle.

* openclaw-ef3.7: import digest from scan-primitives to break the lifecycle -> vault -> retrieval cycle

* openclaw-ef3.7.5: classify Knowledge keyword-index FTS5 as engine-private

Knowledge's keyword index is a rebuildable cache in each store's own
state.sqlite, opened by SqliteIdentityStore and never through the selected
Gateway engine. Classify its fts5, temp-schema and rowid constructs in
retrieval.ts as owned engine maintenance; other constructs there stay counted.

* chore(knowledge): justify or remove type assertions

Adds SAFETY invariants for the assertion ratchet and removes casts that
real types or narrowing make unnecessary.

* refactor(knowledge): drop the Publisher alias and unused MAX_ENTRIES re-exports

* fix(knowledge): bound Markdown block runs before link lexing

JavaScriptCore reports regexp matches that exceed its backtracking memory
cap as no match. marked's paragraph rule hits that cap after ~49K lines in
one block, then lexes the rest line by line, so scanning a near-limit
record took hours under Bun. Split runs over 10K non-blank lines before
lexing; links are unchanged.

* fix(knowledge): transfer workspace attachments without model byte copying

knowledge-live-728: reuse durable chunk uploads behind a scoped workspace-file tool and document programmatic source retention.
(cherry picked from commit d196fdc0b1c30c4850a1f97d94a512ae2b27aa74)

* fix(knowledge): declare knowledge_attach_file in the plugin manifest

* feat(knowledge): add knowledge-unlinked-claims skill for post-relink review

* fix(knowledge): resume interrupted store setup and admit state once

- Resume a store whose setup was interrupted: an empty state.sqlite is a new
  database, and writeOnce completes an empty vault.json or registry sentinel.
- Admit each physical state database once per thread: the schema check,
  history indexes and additive operational tables move out of every
  connection and every operation.
- Keep maintenance schedule bookkeeping in registry meta instead of
  maintenance.json.

* feat(knowledge): steer the memory prompt toward entity pages and recall before planning

Describe Knowledge as world knowledge about ontology entities, search named
entities once before planning, keep pages for sustained work through
knowledge-entities and knowledge-capture, record agent findings on pages
instead of skipping them, word remembered claims no more broadly than their
excerpt, and record supersession with knowledge-correct.

* fix(knowledge): keep knowledge_remember a search-free fast path for one-off claims

* fix(knowledge): complete an empty registry sentinel before checking it

A crash after the registry sentinel is created but before its ID is written
left an empty registry.id. The constructor compared it with the registry ID
before writeOnce could complete it, so every registry operation failed until
manual recovery. writeOnce now runs first and reports whether it wrote; the
comparison still refuses a sentinel that names another registry.

* feat(knowledge): append a generated Knowledge index to owner-private sessions

* fix(knowledge): honor operator roles and name the real memory audience denial

Operator browsing of an agent's private knowledge now stays within the caller's role agents and requires operator.admin on a shared Gateway. A missing memory audience no longer blames a parent reset; the error points to the memory_audience_denied Gateway log entry without naming other sessions.

(cherry picked from commit 2745ceeed4)

* test(knowledge): keep the flush-plan test to tool policy and read-only behavior

Prompt phrasing is not a contract; the persistence tool set, NO_REPLY, determinism, and read-only null plan stay asserted.

* chore(knowledge): regenerate config baseline and worker inventory on current main

* test(knowledge): match the missing-audience denial by its new diagnostic

* fix(knowledge): re-admit a state database whose inode was recycled

Admission was cached per device:inode, so a deleted and recreated or restored database on a recycled inode skipped schema creation and failed with missing tables. An admitted inode must now also carry the current schema stamp; otherwise it is admitted again.

* fix(knowledge): check operator role and shared-Gateway policy live before each read

Operator browsing now calls the host's allowsAgent() and isSharedGateway() each time the principal is re-resolved, so a role or roster change during an awaited read refuses it before disclosure instead of trusting dispatch-time values.

* fix(knowledge): pin the rendered memory index for a session until its cutoff moves

Each turn re-rendered current records, and edits keep recorded_at, so a rename between turns changed the system prompt. The rendered text is now pinned per agent, session incarnation, store binding and cutoff, and authority is still checked live before reuse.

* feat(knowledge): subject hint on remember and agent review after maintenance relink

knowledge_remember accepts an optional per-item about hint, stored as
subject_hint; remember and relink resolve it before the claim text. A
configured maintenance.reviewJobId automation is queued after a pass that
leaves private remembered claims unlinked; its agent turn follows the
knowledge-unlinked-claims skill.

* feat(knowledge): count every pageless claim for the maintenance review

Maintenance queued the review only for unlinked knowledge_remember claims,
so claims saved unlinked through knowledge_create_claim were never reviewed.
The pending count and the skill's enumeration now cover every current claim
with no linked page and no source page, via knowledge_relink include: all
(dry run only; relink still links only remembered claims).

* feat(knowledge): list the busiest pages' newest decisions and commitments in the index

* fix(knowledge): recheck operator authority when saving store assignments

A store-assignment change checks operator.admin at admission, then waits for
the registry, store lanes and the config lock before writing. An operator who
disconnected or lost access meanwhile could still persist new membership.

plugins.sessionAction now gives handlers a live client.assertCurrent backed by
the Gateway's request authority. Knowledge passes it to the config writer as
writeOptions.assertCurrent, so publication is refused once the caller's
authority has ended, and accepts assignment changes only from hosts that
provide it.

* fix(knowledge): satisfy the type-assertion and forced-exit boundary guards

- History reads use a typed Kysely query against a declared history table
  instead of casting raw rows.
- The download picker uses an optional Window.showSaveFilePicker declaration
  and the DOM FileSystemWritableFileStream type instead of a window cast.
- The source-ingest scripts run from an async entry function and return from
  the help branch instead of calling process.exit.

* test(knowledge): narrow possibly-missing records in maintenance and index tests

* openclaw-87bf: split shared Knowledge skill guidance into topic references

* openclaw-395: rewrite meeting ingestion skill as a short routed procedure

* openclaw-395: speaker mapping, page-creation rules, writing style and new-store setup for meeting ingestion

* openclaw-14d: slim knowledge-entities onto shared topic references with gbrain parity

* openclaw-14d: carry gbrain meeting-ingestion rules into knowledge-meeting-ingest

* openclaw-c0f openclaw-b6d openclaw-5f3: no reference rereads on handoff, ingestion-scoped web lookups, ontology-led typing

* openclaw-b6d openclaw-5f3 openclaw-395: participant identity lookups, project typing, single-pass transcript digest

* openclaw-395: verify from drafts; no full published-page readback

* openclaw-951: lookup-first identity resolution with a reusable identity map

* openclaw-b22: compact knowledge_grep/lookup hits and a 10-hit default for agent calls

* feat(knowledge): search every named entity, page significant ones now, and state the remember call shape

* fix(knowledge): apply live operator policy to store record counts

stores_snapshot counted every running store's private records with an access
that checked only static operator scopes, so an operator whose role excludes
an agent, or a non-admin on a shared Gateway, could read that agent's private
record count. Counts now resolve through featurePrincipal, pinned for each
storage check, through a member the operator may browse; forbidden counts stay
null.

* fix(knowledge): type withSearchDetail through Type.Object instead of a chained assertion

* test(knowledge): remove unchecked any casts from the Knowledge suite

The repository keeps unchecked `as any` casts at zero; the imported Knowledge
tests had 602. Most were redundant (casting values already typed any). The
rest now use real types: typed fixture returns, contract and record types,
literal tuples, non-null assertions where tests already rely on presence,
small subclasses for storage and Git fakes, and single-step assertions to real
types for deliberately partial inputs. No assertion changed.

The five ingestion-contract @ts-expect-error checks are deliberate negative
type tests and join the inventory's explicit allow-list.

* feat(knowledge): route /learn into knowledge-ingest with source-faithful supersession

* chore(knowledge): align fs-safe with main at 0.26.11

* chore(knowledge): regenerate config baseline and worker inventory on current main

* fix(knowledge): state the invariant behind the search-detail input cast

* docs(learn): describe /learn as saving to skills or memory

* fix(knowledge): break storage and retrieval import cycles and clear lint errors

* chore(knowledge): regenerate config baseline on current main

* chore(knowledge): regenerate plugin inventory on current main

* fix(knowledge): key the pinned memory index by physical store

A binding can move to a different physical store (share the store, then use
own), so a pin keyed by binding, session and cutoff could serve the previous
store's index. The pin key now includes the store's vault ID.

* test(knowledge): walk test trees with listDirectoryTreeStrict

* test(knowledge): consolidate duplicated and low-value tests

* test(knowledge): drop low-stakes single-purpose tests

* ci: raise test-lines ceiling to 6,363,000 for bundled Knowledge

* ci: keep main's test-lines ceiling; regenerate worker inventory after main merge
2026-10-25 10:30:07 -07:00
..
…

OpenClaw Android App

OpenClaw Android is the officially released Google Play app. It connects to an OpenClaw Gateway as a companion node for chat, voice, approvals, screen, and device-aware automation.

App features

  • Pair with a Gateway using a QR code, setup code, or manual connection. Gateway credentials are stored encrypted.
  • Stream chat replies, choose models and reasoning effort, manage session permissions, and expand task progress. The draft has its own full-width row above one control row: +, model, and reasoning on the left; microphone and Talk/send on the right. Session permissions are inside the + menu, with the current access mode shown. The model name opens a compact searchable provider menu above the composer; the configured default is marked on its model row, without settings buttons in the picker. The top-right Chat actions menu includes Context with its live usage ring, which opens context usage, latest tokens, and the cost breakdown. Tap the microphone for dictation. While listening, a Stop icon replaces the microphone; tap it to finish listening. While starting or transcribing, a Close icon cancels that attempt. Long-press for voice messages or Talk, or use the Talk button beside the microphone when the draft is empty. The effort dial opens the live-preview slider and Fast mode. Both controls require Gateway write access; read-only connections cannot change them.
  • Select agents, pin sessions, and browse available native session catalogs from the sidebar. Connecting creates or adopts a dedicated Android session without resetting its history. Native sessions keep their runtime-owned model: Android shows that ownership instead of offering a model change. New session starts independently of the current native thread. Generic child-session forks and new worktrees are unavailable for those sessions; supported message-level forks remain available.
  • The sidebar reads Gateway folders and section order, including empty folders. Show more expands each section; Load more sessions fetches the next roster page without losing previously loaded members. Folder editing and migration of device-local folder names are separate from this display step.
  • Session group renames and deletions stay on the Gateway where they started. Switching Gateways never carries unfinished group changes or their error messages to the replacement Gateway.
  • Search from Overview or Settings to find settings by their displayed name or category, alongside quick actions and recent threads. Local destinations such as Appearance, Profile, and Licenses work without connecting a Gateway. Back from a settings detail returns to the screen that opened search; Desktop appears only when the connected Gateway supports it.
  • Choose a theme family, color mode, accent, and app language in Settings → Appearance. Theme and accent edits sync with a connected writable profile. Read-only or unknown-profile edits, including new edits after restarting offline, stay on the device; choose them again after connecting to sync. Already profile-bound edits wait for that profile to reconnect, without discarding or replacing newer device-local choices.
  • Choose Text size in Settings → Appearance: 90%, 100% (default), 110%, 125%, or 140%. This device-local choice survives restarting the app and does not sync to your profile. It combines with Android system font scaling, including nonlinear scaling, without changing spacing or typefaces.
  • Configure foreground on-device Voice Wake and Gateway-synced wake words in Settings → Voice.
  • Talk setup and startup failures stay visible in a dismissible Chat dialog. If the message reports a missing provider or invalid credentials, configure the Talk provider on the Gateway before trying again.
  • Use Settings → OpenClaw for guided Gateway setup and repair. New replies stay visible at the end of the conversation; scrolling back preserves your reading position until you return or tap Jump to latest.
  • Setup offers optional notifications, microphone, camera, and location access; other permissions are under Additional features. Request all asks for the available runtime permissions without enabling the camera or location features; Android handles consent through its system prompts. Notification access stays separate under Special access. Continue without granting access, or enable phone capabilities individually or later in Settings. Blocked individual setup requests offer Android Settings. Biometric locking, Gateway/chat notifications, and authenticated background presence are supported.
  • SMS is available only in third-party builds on supported telephony devices. Setup distinguishes partial read/send access. Android Settings can recover user denial, but installer or device-policy restrictions may leave SMS unavailable; OpenClaw cannot override them. Returning from Settings rereads the actual grants without restarting Gateway setup.
  • View the phone's memory and disk meters on the Control UI Devices page. Connected Android nodes report host resource stats immediately and every 60 seconds; disk meters require an available storage sample and a Gateway that supports host stats.
  • Manage installed skills and Gateway-verified ClawHub releases, and inspect or edit automations with the required Gateway access.
  • Use the Wear OS companion for sessions, replies, aborts, and realtime Talk through the paired phone without storing Gateway credentials on the watch.

Open in Android Studio

  • Open the folder apps/android.

Gradle sync generates the native localization lookup and resources before IDE indexing. Use the repository's supported Node.js version on your PATH, as for the build commands below; no separate localization generation step is required.

Tool icons

Chat tool activity uses the shared tool display icons with native Android glyphs. Command, read, edit, write, search, and fetch rows take precedence over the tool's configured icon; unknown tools use a puzzle piece. Tool Search calls use the called tool's icon. Failed rows retain their error mark, and progress cards keep their receipt presentation.

Session colors

Long-press a row on the Threads page and choose Color, then select a swatch or Default to clear it. The eight colors are red, blue, green, yellow, purple, orange, pink, and cyan. Colored sessions show a narrow leading stripe in the sidebar and Threads page, plus a colored ring around the agent avatar in the open chat header. Unset colors add no indicator. Colors sync through the Gateway and remain visible in the local session cache while offline.

Session snooze

Long-press an eligible session on Threads and choose Snooze, then In 1 hour, In 3 hours, This evening (18:00 local, when more than one hour away), Tomorrow (09:00 local), or Next week (next Monday at 09:00 local). Next week is omitted on Sundays because it matches Tomorrow. Archived, child, and protected sessions cannot be snoozed.

Snoozed sessions leave Recent, Current, and sidebar recents. Open the Snoozed pill to see their Wakes times, or long-press and choose Wake session to bring one back early. The Gateway saves the wake time across clients. A session wakes at its deadline, when a real message arrives, or when a run completes; pinning or archiving also clears its snooze. Snooze never stops a run, blocks messages, or disables an automation.

Camera attachments

Tap + → Camera to open the viewfinder directly. Switch between Photo and Video inside the camera; there is no separate mode-selection menu. Capture a photo, or start and stop a video recording, to add it to the original draft. Camera access is requested on first use, and video recording requests microphone access when needed. Closing the camera, leaving the app, or rotating the screen cancels capture and preserves the draft. Nothing is sent until you tap Send. Gallery selects both existing images and videos.

Image previews in Chat

Tap an image to open the full image. Pinch to zoom up to 4× and drag to pan; double-tap the image for 2.5× zoom or to reset. The bottom controls zoom out, show the current percentage (tap to reset), and zoom in. Close with X, Android Back, or a stationary tap on empty background. Image taps, drags, and two-finger gestures do not dismiss the preview. Rotation or recreation refits the image rather than restoring offsets from the old viewport.

Photo and voice-note messages keep their attachments after history refresh. Queued photos use their stored attachment bytes for previews. The default Gateway transcript echo appears beneath its transcribed voice note, and Copy, Share, and Reply include the visible transcript. Custom transcript formats remain ordinary message text. Cached history keeps media references and transcription state; loading an uncached photo still requires reconnecting to its Gateway.

Completed work in Chat

In the app's own conversation, agent main sessions, and dashboard conversations, completed commentary and tool activity fold into a Worked or Worked for… row. Tap the row to expand or collapse the details. Prompts, final answers, and attachments remain visible. The summary keeps counts of failed, blocked, and unknown tool outcomes visible even when work is collapsed; these describe individual tools, not the overall answer. Failures without a later answer stay visible; earlier tool failure details remain available in expanded work. Active work stays expanded, including a run continued by a steering message. Channel conversations retain their full transcript.

Message information

Tap the subdued timestamp under an assistant message to see its recorded model, token counts, cache reads/writes, and estimated cost when the Gateway supplies them. These are facts for that message's model call, not totals for the whole run. Missing and zero-valued details are omitted; messages without details keep a plain timestamp. The existing offline transcript cache retains these facts. Android does not infer a historical context percentage from the current session's model limit.

Review changes

When the connected Gateway advertises sessions.diff, open a conversation's three-dot Chat actions menu and choose Review changes. If the action is missing, update the Gateway before reviewing changes on Android. The full-screen native viewer loads the same sessions.diff snapshot as the web Review panel. Close it with Android Back or Close review; pulling down does not dismiss it. Review shows Uncommitted changes only. The scope label is static; branch-base and historical commit comparisons are outside this Android viewer's scope. Tap a file header to collapse it. Line numbers are hidden by default. Start a new rightward swipe of at least 64dp with the code already at its left edge to reveal them; swipe left at least 64dp to hide them, then start a fresh swipe to pan the code. A swipe that starts away from the left edge only pans, even after reaching that edge: lift your finger before swiping again to reveal numbers. Addition/deletion markers remain visible. Long-press a code line, then drag to select more lines. Release to choose To chat (append path:start-end (Before | Uncommitted) or the corresponding After context, followed by all selected text in a fenced code block, to your draft without sending it) or Copy (copy the selected text). A pulse marks the start of selection; a toast confirms either action. Drag any of the four selection handles to refine the range before choosing an action. Selection stays in one hunk and uses the starting line's side: Before for deleted lines, After otherwise. It skips the opposite side and never includes omitted context. Tap outside the actions to cancel. Use Copy patch to copy that file's returned patch. Refresh changes requests a new snapshot; the viewer does not stream updates.

The Gateway owns repository selection and session-start filtering. This is a checkout snapshot, not an exact audit of the assistant's edits. Binary files, truncated patches, stopped workspaces, and unavailable repositories are identified in the viewer. Switching conversation or Gateway closes the review; safe fold layout changes preserve the opening.

Foldable layout

With a full-height vertical separator reported by AndroidX WindowManager, the main app places its sidebar on the reading-direction start plane and the active page on the other plane. Both use the reported bounds, including off-center hinges. The sidebar remains visible after selecting a page or session.

Book panes require at least 280 dp for the sidebar, 320 dp for the active page, and 320 dp of height. Onboarding and layouts without a supported split use the largest rectangular region clear of separating folds and fully occluding hinges. Equal regions prefer the top, then the reading-direction start side. Opening the keyboard does not select a different fallback region for this outer host. Without an intersecting separator, the app keeps its full-window layout and modal sidebar.

In Chat, a full-width horizontal separator can place the conversation header, transcript, and status above the hinge and the same composer below it. Each usable pane must be at least 320 dp wide. The measured space must fit the complete header, a transcript band with a complete text line, and status above, with a complete input line and controls below, accounting for the current font size and padding.

Layout changes retain the draft, cursor, reader position, and existing local capture state owners. If keyboard insets or larger text leave too little space, Chat uses the existing one-region fallback based on the space remaining after insets, then restores the split when it fits.

Command search and gateway trust prompts retain the single-region host.

Gateway trust, QR scan-error, Replace gateway setup, and Forget gateway prompts also stay within one safe region. Their complete contents and actions scroll when space is limited; opening the keyboard does not move them to another region. If the keyboard covers that region entirely, dismiss the keyboard to reach the prompt again.

Chat actions and voice options menus stay in the safe region containing their trigger. These popups remain focusable without becoming keyboard (IME) targets. If folds, insets, or layout changes invalidate an open menu, it closes without choosing an action. Reopen it explicitly when space permits; it does not reopen automatically when the layout recovers. Dismissing the menu does not reset Chat's draft, editor, or reader state.

Chat's + menu opens above the composer as a compact icon list: Camera, Gallery, Files, Location, and Permissions. Permissions shows the current access mode. Camera offers photo and video capture through the in-app camera after Android permission is granted. Confirmed captures are added to the original draft for review, without sending; cancellation keeps the draft unchanged. Temporary captures are removed after import or cancellation. Gallery selects both photos and videos through Android's system picker without whole-library permission. The embedded preview is not used: its Done callback can precede pending URI grants and revocations, so it cannot supply a reliable final selection. Location opens a confirmation view, requests foreground permission, and adds a map link to the draft for review before sending. It respects the app's Precise Location toggle, including changes made while capture is pending, using the same approximate grid as node location responses. Closing a picker preserves the draft; switching conversations retires the opening.

Background tasks and Switch branch sheets initially use the largest safe region with usable sheet space, not the trigger's region. They keep that region while it remains usable. Valid geometry changes retain the same sheet and local state. An invalid opening closes without selecting an option and stays closed until explicitly reopened.

The attachment, model, permissions, context, and effort menus use the same opening and safe-region checks, anchored above the composer when space permits. Search and provider expansion stay inside its bounded viewport; keyboard and fold changes keep it in usable window space.

Background tasks remains an agent-wide, read-only list and detail view. Safe layout changes retain the opening and its reading state. Switching Gateway, agent, or chat closes it; a same-owner disconnect leaves read errors visible with Refresh available.

Switch branch keeps its list and reading position through safe layout changes; the title and rows scroll together in short panes. Changing Gateway, agent, or chat retires the opening. Reading remains available when a run is pending, outbox restoration is incomplete, the current session has outbox items, or a branch switch is already in flight. Mutation rows stay disabled in those states. Closing or retiring the sheet does not cancel an admitted switch, and its completion cannot dismiss a replacement opening.

Other dialogs, sheets, and popup menus are not fold-adapted yet.

Wear OS companion

The wear app is a paired-phone companion with the same application ID and signing identity as the phone app. The watch discovers the phone through Wear OS Data Layer, then uses the phone's existing authenticated operator session. It never receives or stores Gateway tokens, passwords, TLS pins, or device-signing identity.

The watch supports agent and session selection, bounded text-only transcript history, streaming reply state, text and voice replies, abort, realtime Talk within the selected session, paired-phone Gateway controls, local reply notifications, theme and automatic-speech settings, and a launch Tile. Realtime Talk streams watch microphone and playback audio over a temporary Wear OS Data Layer channel; it still uses the phone's authenticated Gateway session and closes when the selected phone or Gateway connection changes. A missing Data Layer event sequence or changed phone-process epoch triggers a fresh history request instead of applying uncertain deltas. Agent and Gateway controls are capability-negotiated so an older paired phone remains usable during staggered updates.

cd apps/android
./gradlew :wear:testDebugUnitTest :wear:assembleDebug :wear:lintDebug :wear:ktlintCheck

Build / Run

Install the repository's Node.js and pnpm dependencies before building. Gradle builds the shared Mermaid renderer automatically and packages its local assets with the app; no CDN or Gateway renderer is needed.

Gradle also generates the Kotlin localization lookup and native_ string resources in the ignored app/build/generated/native-i18n/ directory. Builds, tests, and Android Studio sync share the same task; unchanged inputs skip generation, and clean builds can restore the outputs from Gradle's build cache. Keep manual XML resources in app/src/main/res. Native source strings and translations remain in apps/.i18n/native-source.json and apps/.i18n/native/<locale>.json; the existing pnpm native:i18n:baseline and pnpm native:i18n:sync workflow owns updates. Never edit or commit the generated lookup or native_ resources. pnpm android:i18n:check validates the catalog against those source inputs.

pnpm install
cd apps/android
./gradlew :app:assemblePlayDebug
./gradlew :app:installPlayDebug
./gradlew :app:testPlayDebugUnitTest
cd ../..
pnpm android:release:archive

Third-party debug flavor:

cd apps/android
./gradlew :app:assembleThirdPartyDebug
./gradlew :app:installThirdPartyDebug
./gradlew :app:testThirdPartyDebugUnitTest

Mermaid diagrams

Chat renders completed mermaid code blocks inline. Tap a diagram for a full-screen view with pinch-to-zoom and panning. The corner menu switches to source or retries a temporary failure, and the copy button copies the original Mermaid source. Incomplete streaming blocks remain readable code.

The renderer shares its pinned Mermaid version, sandbox, and SVG sanitizer with the Control UI. Android keeps bounded bitmap previews in memory and retains the sanitized SVG for zooming. Math and diagrams share the render queue and lifecycle owner, with separate lazy WebViews and resource limits. See packages/mermaid-renderer for the shared runtime and build contract.

Repository-backed debug Gradle invocations, including pnpm android:run and pnpm android:screenshots, stamp the full checkout commit and capture one UTC build timestamp shared by every debug variant in that invocation. Release tasks still require explicit openclawBuildCommit and openclawBuildTimestamp properties so signed artifacts remain reproducible.

Android release archives use the pinned version in apps/android/version.json. Run Android Store Release from main with the default release operation, or run pnpm android:release:upload from a clean local main matching origin/main. The pipeline selects unused phone and Wear build numbers from Google Play and generates OpenAI release notes from changes since each form factor's public release. It saves the plan and notes as release artifacts and uploads the selected clean source commit. Tracked version defaults and notes stay unchanged; the flow creates no preparation commits or follow-up PRs.

For daily Google Play Internal testing builds, the same workflow runs at 7:00 AM Pacific using the America/Los_Angeles time zone, including daylight saving changes. Scheduled runs require the repository variable ANDROID_INTERNAL_ENABLED=true and use the android-internal environment. They upload the phone and Wear builds and generated notes to internal and wear:internal, without capturing screenshots or changing the store listing. Production promotion remains manual.

To run this distribution manually, choose operation=internal from main, or run pnpm android:release:upload -- --destination internal from a clean local main matching origin/main. Manual runs work while the schedule is disabled. See daily Internal testing setup for the environment, credentials, and enablement steps.

For local preparation or inspection:

pnpm android:version
pnpm android:version:check
pnpm android:version:pin -- --from-gateway
pnpm android:version:sync

Release-owner signing sync:

pnpm android:release:signing:plan
MATCH_PASSWORD=<signing repo password> pnpm android:release:signing:sync:pull
MATCH_PASSWORD=<signing repo password> pnpm android:release:signing:check

The signing sync pulls encrypted Android upload-key assets from the shared apps-signing repo and materializes decrypted files under apps/android/build/release-signing/. Standalone release APK verification also requires that key's public certificate SHA-256 fingerprint to match Config/ReleaseSigning.json.

Generate phone and Wear OS Google Play screenshots:

pnpm android:screenshots

The screenshot script captures both form factors with retained OpenClaw_Screenshots_API36 (Pixel 2) and OpenClaw_Wear_Screenshots_API34 (Wear OS Large Round) AVDs. It creates a missing AVD, boots it headlessly, waits for Android to finish booting, disables animations, captures the screenshots, then shuts down the emulator it started. Install the API 36 Google APIs and API 34 Wear OS system images in the local Android SDK. Use --form-factor phone|wear with --avd or --device to explicitly capture one form factor from another emulator.

For local branch-switching proof, launch a debug build with the intent extras openclaw.screenshotMode=true and openclaw.screenshotScene=branches. This Chat scene has 12 local branch alternatives and no active run. Switching updates the selected branch and transcript only in fixture memory, never on a live Gateway. Start a fresh app process before choosing a scene; restarting only the Activity reuses the process runtime. Same-scene re-entry retains the selected branch.

For sidebar attention proof, use openclaw.screenshotScene=attention. The native drawer contains inactive sessions with multiple questions and execution, plugin, and Gateway-settings approvals. Tap or keyboard-focus an attention icon to inspect the oldest request. The attention-expiry scene uses successive short deadlines to exercise live removal without another Gateway event. These fixtures use the normal request parsers and lifecycle owners with synthetic in-memory responses; start a fresh app process between scenes.

For session snooze proof, run pnpm android:screenshots --snooze-proof before before adding the feature, then pnpm android:screenshots --snooze-proof after. The synthetic snooze scene supplies one active and one snoozed session through the production list parser. The capture steps open Threads and both row menus, saving PNGs under .artifacts/android-snooze-proof/before/ and after/. This mode requires no existing ADB devices, starts and stops its own phone emulator, and leaves store screenshots unchanged.

For completed-work proof, use openclaw.screenshotScene=completed-work, active-work, or work-boundaries. These scenes use the same Chat screen with synthetic history in a node-owned app conversation. They cover disclosure expansion, active work, attachments, and failed tools without a live Gateway. Start a fresh app process between scenes.

pnpm android:release:archive builds signed release artifacts into apps/android/build/release-artifacts/ and writes .sha256 checksum files:

  • Play build: openclaw-<version>-play-release.aab
  • Wear build: openclaw-<version>-wear-release.aab
  • Third-party build: openclaw-<version>-third-party-release.apk

pnpm android:bundle:release is an alias for the same Fastlane archive lane.

Regular final and correction OpenClaw releases publish the signed third-party APK as OpenClaw-Android.apk with a checksum manifest and GitHub Actions provenance. The Android APK Artifact Publish workflow at .github/workflows/android-release.yml is the only automated GitHub Release upload path. When the tagged Android pin matches the stable release train, OpenClaw Release Publish qualifies Android independently and dispatches it after core npm succeeds. A mismatched pin records an explicit skip. Android does not hold npm or GitHub release finalization, so verified APK assets may attach after the release is public.

The protected android-release environment supplies MATCH_PASSWORD; the repository's read-only GitHub App token checks out encrypted material from openclaw/apps-signing. The workflow builds the exact release tag, refuses to replace different existing bytes, and re-downloads the APK for checksum, certificate, and provenance verification.

pnpm android:release:archive is for local archive validation only. It is not a fallback upload path after pnpm android:release:upload fails.

Agent-driven Google Play uploads must use pnpm android:release:upload as the only release path. If that command fails, stop and fix the failing screenshot, metadata, signing, validation, archive, or upload step and inspect the store outcome before trying again. Keep the saved plan and generated notes for investigation or local archive replay. Do not upload archived artifacts through direct Fastlane lanes, Gradle artifacts, Google Play API commands, or Play Console mutation commands.

The release lane uploads the phone and Wear bundles in one atomic Google Play edit. It publishes the phone bundle to GOOGLE_PLAY_TRACK and maps the Wear bundle to the corresponding form-factor track (wear:<track>), so the default internal channel publishes to internal and wear:internal.

See apps/android/VERSIONING.md and apps/android/fastlane/SETUP.md for the release workflow.

Prefer pnpm android:release:archive, which stamps and validates the full Git commit and one UTC build timestamp before signing. Flavor-specific direct Gradle release tasks must pass the same metadata explicitly:

cd apps/android
commit="$(git -C ../.. rev-parse HEAD)"
built_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
./gradlew -PopenclawBuildCommit="$commit" -PopenclawBuildTimestamp="$built_at" :app:bundlePlayRelease
./gradlew -PopenclawBuildCommit="$commit" -PopenclawBuildTimestamp="$built_at" :wear:bundleRelease
./gradlew -PopenclawBuildCommit="$commit" -PopenclawBuildTimestamp="$built_at" :app:bundleThirdPartyRelease

Kotlin Lint + Format

pnpm android:lint
pnpm android:format

Android framework/resource lint (separate pass):

pnpm android:lint:android

Direct Gradle tasks:

cd apps/android
./gradlew :app:ktlintCheck :benchmark:ktlintCheck :wear:ktlintCheck :wear-shared:ktlintCheck
./gradlew :app:ktlintFormat :benchmark:ktlintFormat :wear:ktlintFormat :wear-shared:ktlintFormat
./gradlew :app:lintPlayDebug :app:lintThirdPartyDebug :wear:lintDebug :wear-shared:lintDebug

Set ANDROID_HOME to your installed Android SDK, or set sdk.dir in the local apps/android/local.properties file. For Homebrew's command-line tools, the SDK may be at /opt/homebrew/share/android-commandlinetools.

Macrobenchmark (Startup + Frame Timing)

cd apps/android
./gradlew :benchmark:connectedDebugAndroidTest

Reports are written under:

  • apps/android/benchmark/build/reports/androidTests/connected/

Perf CLI (low-noise)

Repeatable startup measurement + hotspot extraction with compact CLI output:

cd apps/android
./scripts/perf-startup-benchmark.sh
./scripts/perf-startup-hotspots.sh

Benchmark script behavior:

  • Runs only StartupMacrobenchmark#coldStartup (10 iterations).
  • Prints median/min/max/COV in one line.
  • Writes timestamped snapshot JSON to apps/android/benchmark/results/.
  • Auto-compares with previous local snapshot (or pass explicit baseline: --baseline <old-benchmarkData.json>).

Hotspot script behavior:

  • Ensures debug app installed, captures startup simpleperf data for .MainActivity.
  • Prints top DSOs, top symbols, and key app-path clues (Compose/MainActivity/WebView).
  • Writes raw perf.data path for deeper follow-up if needed.

Run on a Real Android Phone (USB)

  1. On phone, enable Developer options + USB debugging.
  2. Connect by USB and accept the debugging trust prompt on phone.
  3. Verify ADB can see the device:
adb devices -l
  1. Install + launch debug build:
pnpm android:install
pnpm android:run

If adb devices -l shows unauthorized, re-plug and accept the trust prompt again.

USB-only gateway testing (no LAN dependency)

Use adb reverse so Android localhost:18789 tunnels to your laptop localhost:18789.

Terminal A (gateway):

pnpm openclaw gateway --port 18789 --verbose

Terminal B (USB tunnel):

adb reverse tcp:18789 tcp:18789

Then open Settings → Gateway → Manual Gateway (or Set up manually during first-run setup):

  • Host: 127.0.0.1
  • Port: 18789
  • Connection security: Unencrypted

Hot Reload / Fast Iteration

This app is native Kotlin + Jetpack Compose.

  • For Compose UI edits: use Android Studio Live Edit on a debug build (works on physical devices; project minSdk=31 already meets API requirement).
  • For many non-structural code/resource changes: use Android Studio Apply Changes.
  • For structural/native/manifest/Gradle changes: do full reinstall (pnpm android:run).

Connect / Pair

  1. Start the gateway (on your main machine):
pnpm openclaw gateway --port 18789 --verbose
  1. In the Android app:
  • Follow the first-run connection screen, or open Settings → Gateway to change a saved connection.
  • Scan a QR code, paste a setup code, or enter the Gateway manually.

Gateway credentials and setup codes are masked and accept paste. The app requests password input with autocorrection disabled; this does not guarantee how a keyboard stores or learns from input.

  1. Approve pairing (on the gateway machine):
openclaw devices list
openclaw devices approve <requestId>

More details: docs/platforms/android.md.

If the gateway cannot be reached, the app keeps the connection error visible during automatic retries. For an address that may use Tailscale, Set up Tailscale opens the Android installation guide. Open Tailscale and connect to the gateway's tailnet. Check that the gateway computer is online and OpenClaw is running, then retry. This advice does not verify Tailscale's connection state or change certificate trust. If an earlier network request is still stopping, the app waits for it before starting another request.

Permissions

  • Discovery:
    • Android 13+ (API 33+): NEARBY_WIFI_DEVICES
    • Android 12 and below: ACCESS_FINE_LOCATION (required for NSD scanning)
  • Location:
    • Both flavors: ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION for foreground checks.
    • Third-party flavor only: ACCESS_BACKGROUND_LOCATION plus FOREGROUND_SERVICE_LOCATION for user-enabled Always checks.
  • Foreground service notification (Android 13+): POST_NOTIFICATIONS
  • Camera:
    • CAMERA for camera.snap and camera.clip
    • RECORD_AUDIO for camera.clip when includeAudio=true

Google Play Restricted Permissions

As of March 19, 2026, these manifest permissions are the main Google Play policy risk for this app:

  • READ_SMS
  • SEND_SMS
  • READ_CALL_LOG

Why these matter:

  • Google Play treats SMS and Call Log access as highly restricted. In most cases, Play only allows them for the default SMS app, default Phone app, default Assistant, or a narrow policy exception.
  • Review usually involves a Permissions Declaration Form, policy justification, and demo video evidence in Play Console.
  • The Play build removes these behind the play flavor.
  • Photo library access is also removed from the Play build. Use third-party builds for photos.latest.

Current OpenClaw Android implication:

  • APK / sideload build can keep SMS, Call Log, and recent-photo features.
  • Google Play build excludes SMS send/search, Call Log search, and recent-photo access unless the product is intentionally positioned and approved under the relevant policy exception.
  • The repo now ships this split as Android product flavors:
    • play: removes READ_SMS, SEND_SMS, READ_CALL_LOG, READ_MEDIA_IMAGES, READ_MEDIA_VISUAL_USER_SELECTED, READ_EXTERNAL_STORAGE, and background location; hides SMS, Call Log, Photos, and Always location surfaces.
    • Installed-app listing is user controlled. device.apps is advertised only after the user enables Settings > Phone Capabilities > Installed Apps. The command defaults to launcher-visible apps and does not require QUERY_ALL_PACKAGES.
    • thirdParty: keeps the full permission set and the existing SMS / Call Log / Photos functionality, and offers explicit Always location opt-in through Android settings.

Policy links:

Other Play-restricted surfaces to watch if added later:

  • ACCESS_BACKGROUND_LOCATION
  • MANAGE_EXTERNAL_STORAGE
  • QUERY_ALL_PACKAGES
  • REQUEST_INSTALL_PACKAGES
  • AccessibilityService

Reference links:

Release validation

Run from a clean checkout with the Android SDK, Java, and emulator acceleration available:

pnpm android:release:validate -- \
  --target-sha "$(git rev-parse HEAD)" \
  --output /tmp/android-release-e2e-proof.json

The harness builds the Play Debug phone app, Debug Wear app, and their instrumentation APKs once, then uses fresh API 36 phone and API 34 round Wear emulators. It owns their temporary state, processes, and cleanup. Both emulators boot with a 16 KiB VSOCK packet limit to avoid large contiguous kernel allocations that can disconnect ADB during transfers. The harness verifies each guest's limit before installing its app and records it in the qualification proof.

The Android Release E2E workflow runs this command directly, during eligible full manual CI at the workflow's exact source SHA, and before a manual Android Store Release accesses signing credentials. Scheduled and manual internal distributions retain their existing path. Gateway APK publication in android-release.yml remains separate.

The phone completes setup-code onboarding against a pinned published stable Gateway and a local deterministic model fixture, sends messages, relaunches with its saved connection, and opens Overview. Native keyboard and transcript checks verify that replies remain visible above the real keyboard, dismissing the keyboard preserves a multiline draft, and the beginning and end of a long reply remain reachable. Wear checks exercise native input, chat, reply history, relaunch, phone restart, and the round-screen reply reader. A local raw-message transport replaces the Google Data Layer boundary through the existing transport interfaces. Requests still pass through the production wire codec, phone bridge and controller, authenticated Gateway, and deterministic model fixture; responses and streaming events return through the same application owners. The phone executes the requests; the transport does not supply canned successful replies.

This CI gate does not qualify Google's live Wear Data Layer, device pairing, or companion provisioning. CI starts fresh standard SDK emulators and uses no Google account, companion APK, or saved paired-device image. In particular, it excludes real Google capability discovery, WearableListenerService delivery, and Play services package-name/signature enforcement. The APK checks still verify the built debug artifacts' hashes, signatures, matching package names and certificates, version codes, and declared listener services. They do not exercise Google's enforcement or delivery path. Release/R8 behavior, store signing, and upload remain owned by the existing archive/release lane; this native gate tests instrumented Debug APKs. Physical radios, voice, microphone permissions, notifications, Tiles, and battery behavior also remain outside its coverage.

Each run freezes its stable Gateway version and dependency lock in an artifact; reruns reuse that selection. Missing or invalid saved selections fail instead of silently choosing a newer Gateway. Qualification retains sanitized proof and synthetic UI screenshots, with no raw Gateway or instrumentation logs or setup credentials. Failed proof records bounded SDK operation/exit facts and native test source locations, excluding command arguments and assertion text. Skipped selected tests, crashes, timeouts, and incomplete cleanup fail qualification.

Keep a small physical-device smoke separate, using matching Play-signed phone and Wear apps:

  1. Pair the real devices normally, connect the phone to a Gateway, and confirm the watch discovers the phone and loads session history.
  2. Enter a unique text turn through the watch's native input and confirm its reply on the watch. Repeat with the phone app in the background to exercise real listener-service delivery.
  3. Disconnect and reconnect the pair, restart both apps, then reload history and send another turn. Confirm history and replies remain usable.

Record app/package/signing identities, device/Wear OS versions, connection type, and observed results. This smoke supplies real Data Layer discovery, delivery, and reconnect evidence separately; CI has no Google provisioning prerequisite.

Integration Capability Test (Preconditioned)

This suite assumes setup is already done manually. It does not install/run/pair automatically.

Pre-req checklist:

  1. Gateway is running and reachable from the Android app.
  2. Android app is connected to that gateway and openclaw nodes status shows it as paired + connected.
  3. App stays unlocked and in foreground for the whole run.
  4. Grant runtime permissions for capabilities you expect to pass (camera/mic/location/notification listener/location, etc.).
  5. No interactive system dialogs should be pending before test start.
  6. Local operator test client pairing is approved. If first run fails with pairing required, preview the latest pending request, approve the printed request ID, then rerun:
openclaw devices list
openclaw devices approve --latest   # preview only; copy the requestId from output
openclaw devices approve <requestId>

Run:

pnpm android:test:integration

Optional overrides:

  • OPENCLAW_ANDROID_GATEWAY_URL=ws://... (default: from your local OpenClaw config)
  • OPENCLAW_ANDROID_GATEWAY_TOKEN=...
  • OPENCLAW_ANDROID_GATEWAY_PASSWORD=...
  • OPENCLAW_ANDROID_NODE_ID=... or OPENCLAW_ANDROID_NODE_NAME=...

What it does:

  • Reads node.describe command list from the selected Android node.
  • Invokes advertised non-interactive commands.
  • Skips screen.record and talk.ptt.* in this suite because they require interactive capture. Use apps/android/scripts/voice-e2e.sh for microphone and voice-path proof.
  • Asserts command contracts (success or the expected error for safe-invalid calls like sms.send and notifications.actions).

Common failure quick-fixes:

  • pairing required before tests start:
    • list pending requests (openclaw devices list), then approve with the exact ID (openclaw devices approve <requestId>) and rerun.

Contributions

Maintainer: @obviyus. For official OpenClaw support, visit community.openclaw.ai. For reproducible bugs, use the issue chooser. For contribution coordination, reach out on Discord.