computer-tool and mobile-ui-tool hand-rolled the same node-resolution guard
(exact node-id match against the full list → reject an ineligible exact match →
display-name resolution only among eligible nodes → reject ambiguity). Extract
it into resolveEligibleNodeFromList in nodes-utils, so the wrong-device footgun
lives once at the owner boundary: any capability-gated tool now resolves through
the shared helper instead of handing a pre-filtered list to resolveNodeIdFromList.
Each tool keeps its own eligibility predicate and error wording via an
EligibleNodeMessages config; error strings are unchanged, so existing node-
resolution tests pass as-is. bash-tools already resolves against the full list
and is left untouched.
* fix(ui): unify tooltip arrows and rework sidebar footer hover cards
* fix(ui): keep rich tooltips open while trigger or card retains hover or focus
* fix(ui): type trigger slot query and drop unused build-card export
* test(ui): type usage hint button query for test-types lane
* feat(agents): mobile_ui agent tool for Android UI control (PR 3/3)
Add a dedicated model-facing tool that drives another Android app through the
PR2 mobile.ui.observe/act node commands, completing the vertical slice
(agent -> tool -> node.invoke -> AccessibilityService).
- Mirrors the desktop computer tool's safety model: owner-only + HTTP-denied
(dangerous-tools), raw node.invoke of mobile.ui.* redirected to this tool so
the generic nodes tool cannot bypass it, run/tool-call idempotency, and the
phone-arm workflow (mobile.ui.* must be explicitly armed).
- One call is observe or one act; every act automatically re-observes for
postcondition verification and preserves the landed outcome if re-observe fails.
- Fail-closed confirmation: all state-changing acts (activate, set_text, tap,
swipe) require confirmed=true after the model reviews the proposed effect;
observe, scroll, wait, and navigation global_actions do not. The keyword list
only enriches the confirmation message and is never the sole gate (a11y labels
are localized/iconographic/coordinate-blind).
- Node selection resolves an explicit id against the full device set first
(case-insensitively) and rejects an ineligible or ambiguous match, so an
explicit selection can never be silently redirected to the wrong phone.
- All observed UI text is treated as untrusted; the tool description forbids
following instructions found in app UI.
Additive; no protocol bump. mobile-ui-tool + registration + policy tests pass;
core tsgo and lint verified on Testbox. On-device drive is exercised via the
PR1/PR2 emulator proof; this PR is the agent-side tool + tests.
Follow-up: computer-tool.ts has the same latent node-resolution ordering bug
(explicit id searched only among eligible nodes); tracked separately.
* fix(agents,phone-control): migrate mobile_ui arming hints/tests to gateway.nodes.commands.allow
Match main's node-command arming rename (gateway.nodes.allowCommands ->
gateway.nodes.commands.allow / commands.deny): update the mobile_ui tool arm-hint
matchers to the gateway's current rejection strings and fix the phone-control /
tool tests to the current config shape. Production write path was already correct.
* fix(agents): register mobile_ui in owner-only denylist; trim phone-control under max-lines
- tool-resolution.exclude.test.ts: mobile_ui joins the owner-only core
tools, so add it to the expected plugin/inherited denylists.
- phone-control: derive ArmGroup from a single ARM_GROUPS const and
collapse parseGroup's ||-chain, dropping index.ts back under 700 lines.
* fix(agents): centralize mobile_ui global-action names; regenerate tool-display snapshot
- Extract GLOBAL_ACTION_NAMES const so the schema, action type, and
validator share one source. This also removes the bare `name: "back"`
type-annotation literal that scripts/tool-display.ts's name-regex was
misreading as a phantom runtime tool.
- Regenerate apps/.../tool-display.json to add the mobile_ui display entry
(was missing from the Swift snapshot).
* feat(sessions): first-class archived-session handling
Archived sessions are now exempt from every automatic maintenance path
(age prune, entry cap, model-run prune, disk budget, cleanup repairs);
only explicit sessions.delete removes them. sessions.list gains an
additive tri-state archived filter (true | false | "all"). The sidebar
gets a persisted Active/Archived/All status filter with inline dimmed
archived rows, replacing the nav-away View-archived button; the Sessions
page toggle becomes the same tri-state with dimmed+badged rows in All
mode; the chat composer's archived notice becomes a full-width banner
with an Unarchive action.
* chore(protocol): regenerate Swift gateway models for tri-state archived filter
* refactor(channels): trim deprecated setup-input fields and legacy promotion keys to published readers
* ci: retrigger after dropped ready event
* fix(channels): update setup-input guardrails and dead code for the trimmed tier
Media-facts program PR 5 — the planned additive bridge. Structured
facts travel alongside existing prompt text through prompt prelude,
GetReplyOptions, FollowupRun, queue collect/defer/retry, active
steering, embedded/CLI runs, Gateway offloads (offloadedRefs no longer
discarded; AgentContentPhaseResult carries media), Tlon, and late-media
projection — with prompt bytes golden-equal everywhere. Facts follow
the same adoption/idempotency/late-append lifecycle as prompt text.
Intentionally +175 prod LOC per the audit; the facts-first
hydration/prune deletion PR consumes this carrier next.
* feat(android,gateway): expose mobile.ui.observe/act as node commands (PR 2/3)
Wire the PR1 AccessibilityService executor over the existing node.invoke
transport. Additive: no gateway protocol version bump.
- New commands mobile.ui.observe / mobile.ui.act (capability mobileUI),
generated into the protocol constants.
- thirdParty MobileUiHandler owns one mutex-serialized AccessibilityActionExecutor
and bridges JSON <-> the PR1 snapshot/action model; play flavor ships a
permanently-unavailable no-op stub (Play APK stays accessibility-free).
- Commands advertised only when accessibilityControlEnabled and the service is
connected; NodeRuntime refreshes the advertised surface on connect/disconnect.
- Classified dangerous in node-command-policy (declarable-but-armed, mirroring
computer.act): the transport cannot invoke them until gateway.nodes.allowCommands
explicitly arms them. Screen reads are treated as dangerous too, so observe is
gated as well as act.
Both flavors assemble; thirdParty/play unit tests, ktlint, android lint, and the
gateway node-command-policy tests pass; protocol generator is idempotent; Play
APK verified to contain zero accessibility classes. Emulator: no regression to
PR1 dev-screen observe.
* fix(gateway): migrate mobile.ui arming test/comment to gateway.nodes.commands.allow
Main renamed the node-command arming config from gateway.nodes.allowCommands to
gateway.nodes.commands.allow; update the mobile.ui policy test config shape and
the comment to the current contract. Production resolution already reads the new
path.
* chore(android): update native i18n baseline for PR2 line-number shifts
* feat(apple): add rare working claw stances
Add zen, drummer, and peekaboo animations to the shared iOS and macOS working indicator, with deterministic stance and pose coverage.
* chore(i18n): refresh native source anchors
Gateway restarts (rebuilds, auto-updates) frequently killed main sessions
mid-turn and recovery gave up with the user-facing "couldn't safely resume"
notice for the two most common interruption shapes:
- partial streamed assistant output persisted by a provider abort event
- a tool call interrupted mid-execution (no result persisted)
Both now resume through the existing recovery continuation. Dangling tool
calls are classified against the audited replay-safe allowlist: replay-safe
calls resume with full tools, side-effecting calls keep the restart-safe
tool restriction so an ambiguous side effect is never silently re-executed.
The gateway's own restart abort reason ("agent run aborted for restart")
is now also recognized as a restart abort artifact.
Fail-closed behavior is unchanged for unknown terminal delivery receipts,
before_agent_reply hook states, non-replay-safe Code Mode waits, stale
approval-pending tails, and completed-but-undelivered output.
* feat(android): AccessibilityService UI executor (thirdParty, PR 1/3)
Add a thirdParty-flavor-only AccessibilityService that observes the active
app's UI as a bounded semantic snapshot and performs typed actions, exercised
via a local developer screen. No gateway/agent wiring yet (PR 2 adds
node.invoke commands, PR 3 the agent tool + policy gates).
- Play APK stays accessibility-free: service, config, executor, and UI live
entirely under src/thirdParty; SensitiveFeatureConfig.accessibilityControlEnabled
gates it. Verified: 0 accessibility refs in every merged Play manifest and
0 accessibility classes in the Play dex.
- Semantic-first executor: observe() returns a bounded snapshot (node/depth/text
caps, deterministic order, password + sensitive-field redaction, stable action
vocabulary, generation-scoped refs); act() performs one typed action with a
closed ActionOutcomeCode result set.
- Safety model: coordinate gestures gated by package-match (fail-closed) + a UI
epoch advanced on window/content/scroll/text mutation events; node actions
gated by package-match + per-node refresh(); global actions ungated. Capture
runs off the main thread. Dev UI honestly disables cross-app node controls
(only reachable while the target is foreground; validated via the remote path).
Built and emulator-tested on API 36: live connection status, immediate observe,
a 103-node cross-app Settings capture, and global Home. Both flavors assemble;
thirdParty unit tests, ktlint, and android lint pass.
* feat(android): gate accessibility control behind an off-by-default opt-in
Make landing the accessibility feature a no-op for existing thirdParty users:
the service is invisible and inert until the user explicitly opts in.
- The AccessibilityService and its dev activity are declared
android:enabled="false", so a fresh install exposes NO new accessibility
service in system settings and nothing can bind it (verified on device:
absent from the installed-services list and unbindable; shell cannot enable
it either — only the app can).
- New thirdParty-only "Control other apps" toggle (persisted in the existing
openclaw.node prefs, default OFF). Turning it on enables both components via
PackageManager.setComponentEnabledSetting and deep-links to Accessibility
settings so the user can grant it; turning it off disables them again
(DONT_KILL_APP). A disclosure describes what enabling does.
- Play flavor is a no-op (FlavorPhoneCapabilitiesSettings = Unit); no
accessibility component/controller/toggle references reach the Play APK.
- No new permissions or dependencies. The compile-time flavor gate and the
gateway dangerous-command arming remain as additional layers.
* chore(android): update native i18n baseline for accessibility control strings
* fix(memory): keep canonical rows when same-file legacy memory tables diverge
* fix(memory): keep canonical-owned sources' chunk sets coherent during legacy import
* fix(memory): import legacy chunks when canonical source has no chunks
The same-file legacy migration excluded a legacy chunk from import whenever the
canonical index already had a source row for its (path, source). That stranded a
file whose canonical source was registered but had no chunks yet (indexing
interrupted before chunks were written, or embedding pending/failed): the legacy
chunks were its only searchable content, and the matching source hash stops sync
from re-indexing, so the file went silently unsearchable.
Re-key the chunk-coherence exclusion on canonical chunk ownership instead:
snapshot the (path, source) pairs that already have canonical chunks before the
import and skip legacy chunks only for those. A source with a canonical row but
no chunks now imports its legacy chunks. The snapshot is taken pre-insert because
the exclusion predicate reads the chunks table the import writes to.
Add regressions: legacy chunks import for a chunk-less canonical source while a
chunk-owning source still drops its stale legacy chunk; and restore abort
coverage for the meta and chunks copy assertions (previously only files was
exercised).
* fix(memory): harden same-file legacy conflict recovery
* fix(memory): rebuild ambiguous partial legacy sources
* fix(memory): reconcile migrated derived indexes
* fix(memory): close migrated index ownership gaps
* test(memory): align migration expectations
* test(tooling): match routed test order
* test(memory): exercise vector reload cleanup
* test(memory): prove real vector reload cleanup
* fix(memory): make migrated indexes converge
---------
Co-authored-by: Serhii Leniv <leniv.tech@gmail.com>
Co-authored-by: VACInc <3279061+VACInc@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(scripts): reference update script for source-checkout gateway servers
Teams running a gateway from a git checkout kept re-deriving the same
update ritual by hand: restore the tracked bundle pnpm build rewrites,
fail closed on real local changes, fast-forward main or rebase a local
server branch, install deps, build clean (incremental builds have
shipped stale hashed chunks), restart the service. update-gateway.sh
encodes that as the documented reference; simple installs keep using
openclaw update --channel dev.
* fix(scripts): harden update-gateway guards and refresh docs map
Review findings: never run over a pre-existing rebase/merge/cherry-pick
(the abort path could discard operator progress), and treat untracked
files honestly — fail closed only where the clean build would delete
them, warn-list elsewhere. Regenerate docs_map for the new updating.md
section.
* fix(scripts): pipefail-safe warnings and honest disposable-dir policy
Review findings: head-truncating a long warning list through a pipe
dies under pipefail (SIGPIPE) in the warning path, so truncate via
here-strings; the untracked-under-build-dirs guard was theater since
ignored files bypassed it — declare dist/dist-runtime/.artifacts as
wholly disposable instead and say so; note the rm targets are
symlink-safe (no trailing slashes).
* fix(scripts): order guards before mutations and refuse symlinked build dirs
Review round: the in-progress-operation guard now precedes the bundle
restore (the script's first mutation), symlinked build dirs are refused
before the recursive clean (intermediate symlink resolution would
redirect the delete outside the checkout), and the untracked-files note
no longer overpromises — kept files can affect the build and the
message says so.
* fix(scripts): preserve merge commits when rebasing the server branch
Plain git rebase flattens merges, silently dropping merge-only conflict
resolutions from a local server branch; --rebase-merges keeps them.
The core-runtime-media-ui shard runs its ui config non-isolated for speed, but the
full chat-pane lifecycle tests instantiate the pane component, which relies on
chat-thread/chat-message module-level singletons (thread-state map, confirmation
dismisser WeakMap, module-scoped document context-menu listeners) and spies on those
modules. Under the shared non-isolated graph a stateful predecessor file can leave
those modules duplicated, so the pane binds to a different instance than the test's
spy/registry -- producing order-dependent flakes: `removeEventListener`-not-called
teardown assertions or 120s session-lifecycle hangs. Reproduced deterministically on
a Linux Node 24 Testbox (MAX_WORKERS=1); isolating a single file only shifted the
failure to a sibling pane test, so the whole full-pane family shares the fragility.
Route the 7 full-pane lifecycle test files through a new isolated jsdom lane
(vitest.ui-isolated.config.ts, isolate: true) for a fresh module graph; the other
~370 ui tests stay fast and non-isolated. Registered in both shard registries
(ci-node-test-plan.mjs, vitest.test-shards.mjs) and excluded from vitest.ui.config.ts.
Verified on Testbox: the deterministic single-worker media-ui shard goes from failing
to 5182 passed / 0 failed. Test-infrastructure only; no product code changes.
Zen (2%) breathes through a 6s cycle with one deliberate late snip,
drummer (1%) rocks a two-beat rhythm with jaw hits on the tilts, and
peekaboo (1%) ducks down small and pops back wide open. All three stay
in place (scale/rotate/vertical only); weights rebalance from the
default and southpaw stances.
The initials fallback span inside openclaw-viewer-avatar was hidden with a
single-class selector (.viewer-avatar__fallback, 0-1-0) that the later
.viewer-avatar > span display rule (0-1-1) outranks, so image avatars rendered
the photo and the colored initials side by side in the who's-online roster,
footer facepile, and session facepiles. Bump the hiding selector to
.viewer-avatar > .viewer-avatar__fallback so it wins again; the
.is-fallback swap (0-3-0) still shows initials when the image fails to load.
Regressed in #111421.
* refactor(gateway): declare spawn lineage explicitly at sessions.create
Spawn depth is now a declared fact, never inferred from parentSessionKey.
sessions.create accepts an optional spawnDepth (requires parentSessionKey) that
spawn-owned creations pass; every other fresh session persists spawnDepth 0,
making operator chats and UI forks spawn-capable roots. The visible spawn tool
declares callerDepth + 1, and subagent depth recovery no longer walks
parentSessionKey, which is UI threading only.
Accepted tradeoff (documented inline): pre-upgrade visible children whose only
lineage was parentSessionKey resolve as roots; the transient population may
spawn one extra generation, still capped by maxChildrenPerAgent.
* chore(protocol): regenerate Swift gateway models for sessions.create spawnDepth
* fix(channels): restore transcript context after restart
Merge bounded active-branch session transcript turns at the shared prepared-turn seam so message channels retain assistant replies after restart or history eviction. Migrate Telegram's one-off merge while preserving exact projection and legacy dedupe behavior.\n\nCloses #112520. Slack case reported by Joe Tam (@joetam) in #102594.
* style(channels): avoid spread in transcript mapping
* refactor(telegram): drop obsolete transcript exports
Media-facts program PR 4: legacy Media* fields are emitted only by the
user-turn transcript writer (persisted rows byte-identical, no
migration); reads normalize into ordered facts; documented singular
template variables project per attachment with per-attachment CLI
execution. Review fix: normalized attachments carry their original fact
index so sparse aligned slots cannot shift CLI url/type projections.
Ensure doctor runtime tool schema checks and wiki CLI resolve agent-scoped vaults from canonical agents.entries while retaining the internal list projection fallback.
Codex readers only reliably see the root AGENTS.md, so the .d.mts
declaration contract, the wrapper fail-loud convention, and the
stale-node_modules crash signature move from tribal knowledge into the
root file's Commands section.
Auto-parenting operator dashboard sessions to the agent main session (#110913)
collided with subagent depth recovery, which walks parentSessionKey as spawn
lineage for visible spawn children. Every WebChat dashboard session resolved to
spawn depth 1 and lost all sessions_spawn rights at the default maxSpawnDepth
of 1, breaking swarm collectors and the swarm dashboard from the web UI.
Record an explicit spawnDepth 0 on auto-parented dashboard creations so depth
recovery classifies them as roots; explicitly parented creations (visible
spawn children, forks) keep deriving depth from lineage.
* feat(ui): show live run output tokens
* fix(ui): map live usage to active runs
* test(ui): split live usage coverage
* fix(agents): scope live usage cleanup
* fix(agents): reject stale live usage events
* fix(agents): keep live usage type local
* refactor(ui): keep chat view within line budget
* fix(agents): require lifecycle for live usage
* refactor(ui): dedupe chat reply target type
Replace the onboarding security disclaimer's "hobby project ... sharp
edges" line with a friendlier attribution to the OpenClaw Foundation (a
non-profit), matching the LICENSE copyright holder, and drop the beta
claim. Rename the now-misnamed wizard.security.beta key to
wizard.security.attribution across en/zh-CN/zh-TW and its consumer.
README intro gains the same attribution plus "learns and grows with
you".
* chore(scripts): end failing oxlint runs with a stable status line
A crashed run-oxlint wrapper printed only a stack trace, and a lint
invocation whose output was truncated (cmd | tail -N) read as success —
which recently let a wrapper crash (stale node_modules after a dep-adding
merge) masquerade as a clean lint. Route the CLI entry through a small
wrapper that converts crashes into exit 1 and ends every failing run
with '[oxlint] FAILED (exit N)' as the final line.
* chore(scripts): declare runOxlintCliEntry in the script declaration contract
check-guards verifies .d.mts contracts against .mjs exports and
check-test-types consumes them; the new entry export needed both the
declaration and explicit log-parameter annotations in the test.