tiny
Agent tools — the agent reaches the ring·
How the agent reaches the ring: the ring_commands queue the phone gateway claims, the nine verbs, and the health_* tools.
In 10 seconds
"tiny, find my ring" cannot open a socket to the ring — it enqueues a ring command in the worker, the phone
gateway claims it over its BLE link, executes, posts the result; the ring_command tool polls ≤ 90 s.
Nine verbs: find · battery · status · measure_hr · measure_spo2 · set_auto_hr · sync · memo_pull · record.
First metal proof 2026-09-19 20:35Z: iPhone claimed memo_pull in 6 s.
agent / web ──POST /api/health/ring/commands──▶ worker (pending) ◀──POST …/claim (device token)── phone gateway
agent ◀── GET …/commands/:id (poll ≤ 90 s) ──── worker (done) ◀──POST …/:id/result ──────── phone gateway ⇄ ring (BLE)
The nine verbs — args, BLE sequence, result shape (the contract a gateway implements)
| cmd | args (validated server-side) | BLE sequence (decimal cmds) | result the gateway posts |
|---|---|---|---|
find |
mode 1..3 (default 1) |
4 vibrateLED(mode) → echo |
{acked:true} |
battery |
– | 6 → pct, charging |
{battery, charging} |
status |
– | 37, 6, 0x2F audio state, 0x3D file count, 55 features | {firmware, mac, battery, charging, audio_state, memo_count, features} |
measure_hr |
seconds 5..120 (default 30) |
7 open → collect 11 ticks for seconds → 8 close (also ingest samples) |
{hr:[{ts,bpm}], avg, n} — n:0 ⇒ ring off finger |
measure_spo2 |
– | 23 01 → wait 24 (or 0x19 ended-without-value) → 23 00 |
{spo2} or {spo2:null, reason:"no reading"} |
set_auto_hr |
enabled bool, interval_min 1..60 (default 5) |
30 00 00 17 3B <en> <min> → echo |
{enabled, interval_min} |
sync |
days 1..7 (default 2) |
33/34/36 per day → 42/43/45; ingest via /api/health/ingest | {samples, days} |
memo_pull |
– | 0x2F (skip if recording) → 0x3D count → 0x40, 1 s, 0x34 stream → 0x36 echo; POST /api/health/media | {memos:[{id, duration_s}], remaining} |
record |
seconds (default 30) |
0x30 mode 1 → 0x32/0x33 live stream (on the charger the ring records to flash instead → memo pull) | {media_id, duration_s, transcript} |
Never queued, never sent: 39 reboot / factory reset, 67 unbind, SUOTA FEF5 (ring-cli raw refuses 39|67 too).
Expiry: anything not done 10 minutes after creation flips to expired — a stale vibrate never fires an hour later.
Tools the agent has·
| tool | does |
|---|---|
ring_command {device_id, cmd, args?, wait_s ≤ 90} |
enqueue + poll; note explains a pending/expired command so the model says "the phone hasn't reached the ring yet" instead of inventing a result; action: get | list |
health_summary · health_series · health_query |
the day, a metric series, ad-hoc questions — cradle readings excluded |
health_devices · health_memos · health_notes |
rings with labels/battery/last sync · memos + transcripts (list/transcribe/delete) · notes |
health_alert_rules {memo_agent, button_prompt, auto_hr_min, …} |
the owner's switches: memo → agent, tap → hotkey, auto-HR interval (rides on every claim reply) |
Prompt routing: "find my ring" → find · "measure my heart rate now" → measure_hr · "how much battery" → battery ·
"pull my memos" → memo_pull · "record a 30 second voice note" → record.
Doors (HTTP) — owner session and gateway token
Owner door (session; the app proxies with ?userId + X-Internal-Key):
POST /api/health/ring/commands {device_id, cmd, args?, requested_by?}→201 {command}.device_id=health_devices.id(hd_…). Max 8 waiting per ring (429over_limit). Unknown cmd / bad args → 400 with the reason sentence.GET /api/health/ring/commands?device_id=&status=&limit=→{commands}newest first.GET /api/health/ring/commands/:id→{command}; statuspending | claimed | done | failed | expired.
Gateway door (device token — the fleet device's {deviceId, token}, same no-oracle rule as /api/health/ingest):
POST /api/health/ring/commands/claim {deviceId, token, device_ids?: ["hd_…"]}→{commands, poll_after_ms}— everypendingcommand for the user's rings (optionally only the rings this phone holds) becomesclaimedby this device; re-claiming returns the same rows (idempotent). Poll everypoll_after_ms(2 s while busy, 15 s idle); on iOS piggy-back on the background BLE wake-ups.POST /api/health/ring/commands/:id/result {deviceId, token, ok, result?, error?}→{command}; a second post is a no-opduplicate:true. Emits eventring_commandfor the owner's feed.
Gateway status (2026-09-19)·
| gateway | claim loop | find | battery/status | measure_hr/spo2 | set_auto_hr | sync | memo_pull | record |
|---|---|---|---|---|---|---|---|---|
| iOS RingGateway OTA 106 | ✅ 15 s / 2 s busy | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ on metal | 🟡 stream off-charger unseen |
| Android RingGateway | ✅ built | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ — all pending install on the Pixel |
Mac ring-cli (reference) |
– | ✅ find |
✅ | ✅ | ✅ | ✅ | ✅ audio-pull |
✅ audio-record |