Three cross-layer additions that close longstanding visibility gaps
between the phone, the relay, and the host-side agent:
Agent awareness — unattended/screen/credential-lock state
-----------------------------------------------------------
PhoneSnapshot gains unattendedEnabled, credentialLockDetected, and
screenOn fields. PhoneStatusPromptBuilder.buildBridgeLine() now
appends explicit guidance — e.g. "Unattended access: off — commands
only land when the screen is already on" or "Unattended access: on,
but the device has a credential lock — commands will return
keyguard_blocked and fail."
BridgeStatusReporter.emitTick() emits a parallel `unattended` group
in the bridge.status WSS envelope so the host-side `/bridge/status`
cache (and the `android_phone_status` tool that reads it) sees the
same state for non-phone frontends like Discord. Push triggers fire
on toggle flip via ConnectionViewModel so the host cache updates in
~1s instead of waiting up to 30s for the periodic tick.
android_phone_status tool description updated so the LLM proactively
checks unattended.* fields and warns the user when commands will
hit keyguard_blocked.
Auto-return to Hermes-Relay
---------------------------
android_return_to_hermes is an LLM-called tool that the agent
routinely forgets, leaving the user stranded on Starbucks/Chrome/etc
after the run. Two safety nets:
1. Tightened tool descriptions (REQUIRED FINAL STEP, MANDATORY
CLEANUP framing in android_open_app + android_return_to_hermes).
2. New BridgeRunTracker singleton — coordinates two completion
signals: Chat-tab SSE run.completed (fast, phone-only) and a
12s bridge-idle timer (universal, works for Discord/CLI/web).
Whichever fires first dispatches a local /return_to_hermes via
handleLocalCommand. markReturnedToHermes() prevents double-fires
when the LLM does call return explicitly.
BridgeCommandHandler tracks foreground-shifting paths (/open_app,
/send_intent) at respond() and arms/resets the idle timer accordingly.
Reset hooks at both dispatch start and respond finish so slow-
executing commands (screenshots, big tree reads) don't eat the idle
budget.
Bridge activity log wiring
--------------------------
The Activity Log card on the Bridge tab was scaffolded in Phase 3 but
never wired — recordActivity() existed, the UI rendered the flow, but
no code ever called it. BridgeCommandHandler now emits a
BridgeActivityEntry per dispatched command (Success/Failed/Blocked)
via a new onActivity callback. ConnectionViewModel pipes it through
to BridgePreferencesRepository.appendEntry. High-frequency polls
(/ping, /events, /current_app, /screen_hash) suppressed so the log
shows user-meaningful activity, not noise. Per-route summarizers
produce natural-looking entries: "tap (540, 1200)", "open_app
com.starbucks.mobilecard", etc. resultText surfaces error strings
on Failed/Blocked so users can see WHY without digging through logs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three-layer feature so the agent finally knows what the phone can do, plus
follow-up fixes discovered while smoke-testing the merged Phase 3 build.
Layer 1 — phone-side dynamic system prompt (replaces the static one-liner):
- New PhoneStatusPromptBuilder.kt builds a transparent block from real
bridge/permission state, capped under 100 words, returns null when
everything is off (no empty system messages)
- 4 new sub-toggles in ConnectionViewModel: bridge state, current app,
battery, safety status. Privacy-sensitive ones (current_app, battery)
default OFF
- ChatSettingsScreen gains a live preview Card showing exactly what
will be sent on the next message
- ChatViewModel deletes APP_CONTEXT_PROMPT, calls the builder via a
guarded-reflection capturePhoneSnapshot() helper
Layer 2 — relay backend (GET /bridge/status, loopback only):
- BridgeStatusReporter expanded to push the full nested device/bridge/safety
contract every 30s; new pushNow() method called on master toggle flips
- BridgeHandler caches latest_status + last_seen_at
- New handle_bridge_status route mirrors /pairing/register loopback gate
- 7 new stdlib unittest tests in test_bridge_status.py — all green
Layer 2 — symmetric trio (mirrors the pair feature):
- New plugin/tools/android_phone_status.py — Hermes tool, stdlib only
- New plugin/status.py + hermes-status shim — operator CLI with --json/--port,
three exit codes (0/1/2 for connected/relay-down/no-phone)
- New skills/devops/hermes-relay-status/SKILL.md — slash command
- install.sh + uninstall.sh updated for the second shim
- 19 new stdlib unittest tests — all green
Bridge UI hardening (master gate, MediaProjection, labels):
- HermesAccessibilityService now feeds cachedMasterEnabled itself via a
service-scoped DataStore observer. The previous push-from-outside
pattern was never wired and the cache stayed false forever, 403'ing
every command except /ping and /current_app
- MainActivity registers an ActivityResultLauncher for MediaProjection;
new ScreenCaptureRequester process-singleton bridges non-Activity
callers (BridgeViewModel.requestScreenCapture()). Bridge tab's Screen
Capture row is now tappable instead of inert
- BridgeViewModel.testNotificationListener() + an onTestNotificationListener
lambda through BridgePermissionChecklist for parity with the other rows
- Sideload flavor strings: app_name → "Hermes Dev", a11y_service_label →
"Hermes-Bridge Dev", notification_companion_label → "Hermes Dev …".
googlePlay's a11y_service_label flipped to "Hermes-Bridge" (with hyphen)
for consistency. Disambiguates side-by-side installs in launcher /
recents / Settings → Apps
- BridgeForegroundService: Intent.flags = … → addFlags(…) (the property
setter form fails because Intent.setFlags returns Intent, not void)
- BridgeViewModel: removed deprecated StateFlow.distinctUntilChanged()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>