Storage & Context Building
The CLI writes the three kinds of data pushed from the phone — notifications, recordings (including transcripts / summaries), and images — plus captured web pages from a browser extension and meeting recordings from the desktop "YoooClaw Capture" app, all to local files, then uses a set of commands to turn them into context an Agent can consume directly.
This chapter has two halves: the first covers how each of the three data types gets written to disk, and the second covers how the CLI turns that on-disk data into context. For where the storage directory sits within a profile, see Architecture & Implementation · Profiles; for how clientLabel tagging works across multiple accounts, see Multi API-Key Design.
Design philosophy: disk writes are the source of truth, reads are context
All three data types share the same underlying approach:
- Everything is written to a local file first — notifications are per-day JSON, recordings / images each have an
index.jsonplus entity files, and status is driven by a state machine. The files are the source of truth; whether the daemon is online has no effect on "reading." - Read and write control are separated — query commands (🟢) do pure disk reads, so they work right after a cold start, no daemon required.
- A consistent trio across data types — each type has: a metadata index, a sync / transcription state, a
clientLabelsource tag, and a resolvable local path. An Agent either gets a complete result or a clear "not ready yet" error — it never gets a partial result.
profiles/<name>/
├── notifications/ # per-day JSON + dedup indexes
│ ├── 2026-05-25.json
│ ├── .ids/ # dedup by id
│ └── .keys/ # dedup by content fingerprint
├── recordings/ # smart_hardware source (phone-side recording hardware)
│ ├── audio/ # raw audio (.ogg)
│ ├── transcript-data/ # transcript JSON (primary storage)
│ ├── transcripts/ # transcript text .md (derived, filename <YYYYMMDDHH>_<title>_<id>.md)
│ ├── summaries/ # summary .md (derived, same filename convention)
│ ├── index.json # metadata + state index
│ ├── asr-config.json # local ASR config
│ └── state/events.jsonl # state event stream
├── voice/ # capture_app source (desktop YoooClaw Capture)
│ ├── audio-jsonl/ # usage records, sharded by day YYYY-MM-DD.jsonl
│ └── recordings-jsonl/ # meeting recording metadata, sharded by day
├── images/
│ ├── index.json # metadata + sync state
│ └── files/ # downloaded image files
└── web-pages/
├── index.json # metadata (deduped by URL hash)
└── files/ # captured body .md (optional .html archive)recording * commands merge results across both recordings/ (smart_hardware) and voice/recordings-jsonl/ (capture_app) when queried, filterable to just one with --source. voice/audio-jsonl/ is a different thing entirely — it's the recording app's own usage history (which app was recorded, when, for how long), queryable only via voice * — not the same dataset as recording *.
Notifications: per-day JSON with double deduplication
Notifications are written per day to notifications/YYYY-MM-DD.json (a JSON array, append-only). Each StoredNotification looks like:
{
"clientLabel": "phone-a",
"appName": "com.tencent.xin",
"appDisplayName": "WeChat",
"title": "Mr. Wang",
"content": "The 3pm meeting moved to 4pm",
"timestamp": "2026-05-25T14:02:11+08:00",
"senderName": "Mr. Wang",
"conversationType": "private"
}A few key points:
appDisplayNameis filled in by the backend's app-name-map — the package namecom.tencent.xinis resolved to "WeChat" at write time, making the context more readable for both humans and models.- IM apps like Lark get structured splitting — title / subtitle are normalized into
senderName/conversationType/conversationName, so group chats and DMs can be distinguished. - Double deduplication:
.ids/deduplicates by the notification'sid, and.keys/deduplicates by a SHA-256 content fingerprint ofclientLabel+app+title+content+timestamp. The same message being re-pushed by the phone, or delivered twice under multiple keys, won't land twice. Dedup happens perclientLabel— the same content received by different accounts doesn't shadow each other. - Retention: data files and both indexes are automatically cleaned up once
retentionDaysexpires.
Recordings: audio → transcript JSON → summary
Recordings have the longest pipeline, written across a four-level directory structure strung together by index.json. Recording results are written by the app / cloud via recordings.result.write:
recordings.result.write (transcript/summary, optional ossUrl)
↓ entry written to index.json (status=synced on creation)
transcript-data/<id>.json ← primary transcript storage (structured: title/summary/text/segments)
transcripts/<id>_<title>.md ← body text (derived)
summaries/<id>.md ← summary (derived)
↓ optional: if ossUrl is present, download the audio in the background → audio/<id>.ogg
↓ status=transcribedWhen you need to re-transcribe locally on the daemon's machine, recordings.retranscribe triggers it using local / request-level ASR config: status=transcribing → ASR (api/model-proxy, falls back to the account ock- key by default) → transcribed.
Key design points:
- The JSON in
transcript-data/is the primary storage; the text intranscripts/and the summary insummaries/are both derived from it.readTranscript/readSummaryprefer reading the JSON, falling back to Markdown only for legacy data — keeping old and new data on the same footing. title/summarybuilt in: finishing a transcription also produces a shorttitleand a summary, written into the index andsummaries/. When an Agent wants a quick sense of "what's this recording about," reading the summary is enough — no need to pull the full text.- A strictly validated state machine:
transfer_statustransitionssynced → transcribing → transcribed(ortranscribe_failed) via a state machine, and illegal transitions are rejected outright. Writing a result viaresult.writeis an out-of-band disk write, and goes straight totranscribed. Atranscribingstate left over from a daemon restart is treated as interrupted and set totranscribe_failed. - Event stream: every state change is appended to
state/events.jsonl;yc recording events --id <id> --watchcan follow it liketail -f, and the same event is also pushed back to the phone via Relay asrecording.status. - Recording time follows the actual capture time in the result, not transcript-generation time; timestamps arriving in different timezones/formats are parsed into real instants before sorting, so "latest" never picks the wrong one.
duration_secis a whole number of seconds, with a matching human-readableduration_display(16s/1m 16s/1h 1m 1s); once the audio finishes downloading,file_size_displayreports size in an Android-compatible short format, and the oldfile_size_bytesfield has been retired. - Reads and writes are scoped by clientLabel: recordings ingested via
/gateway/recordings.*are tagged with theclientLabelof the api-key that wrote them; the read side is scoped the same way — a client connected over a Relay tunnel or a specific api-key only sees/mutates its own recordings, loopback / gateway-token requests are unrestricted, and label-less or legacy entries stay visible to every client.
ASR config is written to recordings/asr-config.json (generated by yc recording setup-asr); when mode=api is missing a key, it falls back to the account-level ock- key. The current Go beta only supports api / model-proxy; the local mode is kept in the schema for compatibility with older requests, but is rejected during validation.
The capture_app source under voice/recordings-jsonl/ (meeting recordings from the desktop "YoooClaw Capture" app) goes through the same recording * query commands (--source capture_app), but its transcript shape is sentence-level — transcripts[].sentences[] (with begin_time/end_time/speaker_id) — and it has no equivalent to recordings/state/events.jsonl.
Images: a download pipeline mirroring recordings
Images arrive via POST /images, are written to images/index.json, and then downloaded from OSS as a background stream to images/files/<id>.<ext>:
POST /images → entry written to index.json, status=syncing
↓ background fetch(ossUrl) streamed to files/<id>.<ext>
status=synced (failure → sync_failed, with lastError recorded)image path <id> returns YOOOCLAW_IMAGE_NOT_READY rather than a partial file while the download is still in progress — so when an Agent feeds the local path to a multimodal model, it either gets the complete image or a clear "not ready yet." index.json also carries metadata like clientLabel, source_app, and caption.
Web pages: captured by a browser extension
Captured web pages are the one data source that doesn't come from the phone at all — a browser extension delivers it over Relay instead. The extension converts the current page into Markdown and POSTs it to /web-pages; the daemon writes an entry to web-pages/index.json and saves the body to a file. Purely local, no Internet access involved:
browser extension → POST /web-pages (Markdown body + metadata)
↓ deduplicated by URL hash
already captured: captureCount++, firstCapturedAt unchanged, capturedAt refreshed to now
not yet captured: new entry, files/<hash>.md written (optionally archiving the raw HTML)Each index.json entry records urlHash, title, siteName, canonicalUrl, capturedAt, firstCapturedAt, captureCount, bytes, relativePath, and clientLabel. The daemon also exposes two matching read-only endpoints for the extension: GET /web-pages/status?h=…, so the popup can check whether a URL is already captured when it opens, and GET /web-pages/index, so the extension can backfill local capture state in one shot after sign-in.
yoooclaw synced-web-page search only searches the Markdown body plus metadata like title/URL — never the raw HTML archive, and never the live Internet. What it returns is a locally captured historical copy, not proof of the page's current content; when freshness matters, verify separately with a live web tool.
How the CLI helps you build context
The disk writes are just raw material — the CLI's command layer turns them into context an Agent can use easily. There are six core mechanisms:
1. Presets for high-frequency scenarios (shortcuts)
The + prefix hardcodes the most common queries into a single command, so an Agent doesn't have to assemble a pile of flags:
yc notification +today # today's notification summary
yc notification +recent # the last hour
yc recording +latest # the most recent recording (with title/summary/transcript)
yc image +latest # details for the most recent image2. Aggregation commands built for "summarize this"
notification summary doesn't just spit out notifications one by one — it returns aggregate statistics + recent samples together, exactly the shape an Agent needs to write a summary; notification stats supports multi-dimensional aggregation by date|app|sender|hour|client:
yc notification summary --from 2026-05-25T00:00:00+08:00 --sample 30 --top 10
yc notification stats --dim app --from 2026-05-203. Incremental cursors for feeding a memory system
The sync service (scan / fetch / commit) provides a cursor scheme designed for "continuously pipe notifications into a memory system": scan finds how much is pending per date, fetch gets a given date's details and returns endIndex, and commit marks that batch processed using that endIndex. Re-running doesn't re-ingest, and it can resume from where it left off.
4. Recordings read "summary-first"
recording status <id> / +latest return title + summary + transcript, so an Agent can look at the summary first and decide whether to expand the full text, saving tokens:
yc recording +latest # the latest recording's title + summary + text
yc recording list --status transcribed --client phone-a5. Images provide a "local absolute path" for multimodal input
image +latest / image path <id> return the local file's absolute path directly, for an Agent to feed to a multimodal model; if the download hasn't finished, they return YOOOCLAW_IMAGE_NOT_READY instead of a partial file.
6. An Agent-facing output contract
All commands support a unified --format json|pretty|table|ndjson:
ndjsonputs one JSON result per line, ideal for an Agent to stream and consume with backpressure;- Success
{ "ok": true, ... }/ failure{ "ok": false, "error": { "code": "YOOOCLAW_*", ... } }share stdout; local CLI errors additionally set a non-zero exit code, and raw HTTP results should also be checked forok/ HTTP status; --client <label>splits context by account;--client allmeans no filter.
Putting these together, building context for "summarize what happened on my phone today" looks like:
yc notification summary +today → today's aggregated notifications + samples
yc recording list --status transcribed
└─ yc recording status <id> → read the summary for the recordings picked out
yc image +latest → the latest image's local path, for a multimodal model
↓ all with --format ndjson / json
Agent assembles context → answersNext steps
- Command Reference — every subcommand and flag for
notification/recording/voice/image/sync. - Agent Skill — how an Agent drives these commands automatically.
- Architecture & Implementation — the full picture of the daemon, ingest, and the recording state machine.
- Multi API-Key Design — where
clientLabelcomes from, and how to switch between accounts.