PRD: PicoClaw Runtime Support
May 25, 2026 · View on GitHub
Status: Adapter validated against a real captured session Owner: ClawMetry runtime-compat Tracking issue: #956 (supersedes the "shares OpenClaw layout" assumption in PR #1981) Last verified: 2026-05-25 (installed PicoClaw v0.2.9 from source, ran real agent turns against local Ollama, captured + validated)
Verified by running it
We installed PicoClaw v0.2.9 (source build;
go installfails on itsreplacedirective), pointed it at a local Ollama model (llama3, zero cost), and ran realpicoclaw agentturns including a realexectool call. The captured session is committed undertests/fixtures/runtimes/picoclaw/REAL/(seePROVENANCE.md) and the adapter is tested against those exact bytes. Running it for real surfaced two bugs that synthetic fixtures missed (both now fixed):
- Tool calls are OpenAI-nested (
tool_calls[].function.{name,arguments}), not flat. The flat read dropped every tool name + its arguments.- Go trims trailing zeros from fractional seconds (
...39.37008+02:00), which madedatetime.fromisoformat()raise on Python 3.9/3.10 (a CI matrix leg) and silently zero the timestamp. Now padded to 6 digits.Confirmed two ways (Go struct + real bytes): no token/usage/cost field exists on disk for the
agentJSONL path, so the adapter'stotal_tokens=0/cost_usd=None/ no-COST is the correct, honest representation.
1. Summary
Make ClawMetry observe PicoClaw (github.com/sipeed/picoclaw) the way it observes OpenClaw:
zero-config auto-detection plus a real read path for PicoClaw's native session format.
PicoClaw is a tiny single-binary Go runtime (29.1K stars) that runs "anywhere" including
Raspberry-Pi-class hardware. Operators running it are asking whether ClawMetry monitors them.
The headline correction: the premise of issue #956 / PR #1981 was that PicoClaw "shares the OpenClaw on-disk session layout exactly." That is false. PicoClaw writes a different path and a different wire format. The synthetic fixtures in PR #1981 are relabeled OpenClaw v3 records, so the "Verified" badge is not earned. This PRD specifies the real adapter required to earn it.
2. Problem
ClawMetry's file read path (dashboard.py:_get_sessions_from_files / _scan_session_aggregates) only
understands OpenClaw's v3 JSONL envelope ({"type":"message","message":{"usage":{"totalTokens":...}}})
under ~/.<runtime>/agents/main/sessions/. Pointed at a real PicoClaw install it would:
- find zero session files (wrong directory), and
- even with the right directory, parse zero sessions (wrong wire format -> no
typefield, nomessageenvelope -> every line skipped), reporting modelunknownand 0 tokens.
So PicoClaw support is not a "add a candidate path" change. It needs a format-aware adapter.
3. Verified findings (source of truth)
Verified 2026-05-25 via the GitHub API and direct source reads of sipeed/picoclaw (Go, MIT).
3.1 On-disk layout
- Home:
$PICOCLAW_HOMEif set, else~/.picoclaw(pkg/config/envkeys.go,EnvHome="PICOCLAW_HOME"). - Workspace:
$PICOCLAW_HOME/workspace(default~/.picoclaw/workspace). - Sessions:
<workspace>/sessions/<key>.jsonl(append-only) + a<key>.meta.jsonsidecar. NOTagents/main/sessions/. Key sanitization replaces:/\with_(pkg/memory/jsonl.go). - Crons:
<workspace>/cron/jobs.json(a JSON file, not gateway-RPC). Verified real shape: each job hasschedule.{kind, expr}(e.g.{"kind":"cron","expr":"0 9 * * *"}) and apayload.{kind, message}, not theat_seconds/every_seconds/cron_exprthe earlier source-read had guessed. Seetests/fixtures/runtimes/picoclaw/REAL/cron/jobs.json. - Config:
$PICOCLAW_HOME/config.json. Default model inconfig.example.jsonisgpt-5.4(hosted); provider is empty by default (user picks). "PicoClaw == local Ollama" is an assumption; Ollama is opt-in. - Gateway/daemon ports: HTTP gateway 18790, WebUI launcher 18800 (OpenClaw uses 18789).
3.2 Session wire format (pkg/providers/protocoltypes/types.go Message)
Each .jsonl line is a flat providers.Message, not an OpenClaw envelope:
{
"role": "assistant", // user | assistant | tool | system
"content": "Working on it.", // a STRING, not a block array
"model_name": "ollama/llama3.1:8b", // "<protocol>/<model>"; or hosted e.g. "gpt-5.4"
"created_at": "2026-05-12T22:35:31Z",// RFC3339, omitempty
"tool_calls": [{"id":"call_1","name":"bash","arguments":"{\"command\":\"echo hi\"}"}],
"tool_call_id": "call_1", // on tool-result lines
"reasoning_content": "..." // optional
}
<key>.meta.json = SessionMeta: {key, summary, skip, count, created_at, updated_at, scope, aliases}.
Critical: the Message struct has no usage / token / cost field. On-disk PicoClaw JSONL
carries no token counts and no cost. ClawMetry can show PicoClaw transcripts, model, and tool
calls, but token/cost is unavailable from the session files (see Open Question Q1).
3.3 What this breaks vs OpenClaw (each is a real parser failure)
- No
typefield -> ourtype=="message"filter skips every line. - Flat shape, no
messagesub-object ->message.usage.totalTokens/message.modelabsent. contentis a string, not[{type:"text"}]blocks.- Model field is
model_name, notmodel/modelId. - No
usage, nocostobject (OpenClaw writes already-pricedusage.cost.total; PicoClaw never does). - Cost for local models:
providers_pricing.pyhas noollama/local entry, soget_cost()returns 0.0 via the default branch (correct value, wrong reason).
4. Goals / non-goals
Goals (this PRD):
- A
PicoClawAdapterthat reads the native flat-JSONL format into ClawMetry's unifiedSession/Eventshapes (transcripts, model, tool calls). - Zero-config detection of
~/.picoclaw/workspace/sessions(respectingPICOCLAW_HOME). - Honest capability + cost surfacing (tokens/cost shown as unavailable, not fabricated).
- Correct-shape fixtures + CI unit tests that fail if the parser regresses.
- A
providers_pricing.pyollama/local-model entry so cost-0 is intentional.
Non-goals (deferred, tracked below):
- On-disk token/cost (PicoClaw does not persist it; depends on Q1).
- Live gateway ingest on port 18790.
- PicoClaw cron file ingestion into the Crons tab (phase 2; format known).
- Pi hardware metrics (temp/voltage/throttle) — PicoClaw does not emit them; ClawMetry would read
host sysfs/
vcgencmdon the node. Separate roadmap. - Full DuckDB ingest via the sync daemon + cloud snapshot rendering (phase 3).
5. Design
Phase 1 — Read adapter (this PR)
clawmetry/adapters/picoclaw.py — PicoClawAdapter(AgentAdapter) (subclass AgentAdapter directly;
the format differs from OpenClaw, so it does NOT subclass OpenClawAdapter):
sessions_dirfromPICOCLAW_HOME/~/.picoclaw+workspace/sessions, overridable for tests.detect()cheap + never-raises: detected when the sessions dir (or~/.picoclaw) exists.list_sessions()reads each<key>.jsonl+.meta.json, derivesmodelfrom the lastmodel_name(provider prefix kept inextra, stripped for display),message_countfrom metacountor line count, timestamps from meta/file mtime.total_tokens=0,cost_usd=None.list_events()maps each Message -> unified Event (message / tool_call / tool_result / thinking).capabilities()={SESSIONS, EVENTS}only (no COST claim).- Registered in
dashboard.py detect_config()only when~/.picoclawexists (gated, like the family detection pattern) so an absent runtime never clutters the chip bar.
Phase 2 — Auto-detect + crons
- Add
~/.picoclaw/workspace/sessionstodetect_config()SESSIONS_DIR candidates and~/.picoclawto_auto_detect_data_dir()(both dual copies, kept in sync). Note: precedence keeps OpenClaw/ clawdbot first; PicoClaw is selected when those are absent. providers_pricing.py: add explicitollama/ local provider -> 0.0 so attribution buckets correctly instead of falling through the default branch.- Cron reader for
workspace/cron/jobs.jsonmappingat_seconds/every_seconds/cron_exprto the Crons tab shape.
Phase 3 — Daemon ingest + cloud parity
- Sync daemon discovers
~/.picoclaw/workspace/sessions, ingests via a PicoClaw-shaped parser into DuckDB withagent_type="openclaw"(so default UI views show them) anddata._runtime="picoclaw"for labeling. Add a node-levelruntimeInfo.items[]entry{"label":"Runtime","value":"PicoClaw"}so the cloud Runtime popup labels it with no cloud code change (the field already renders).
6. Open questions
- Q1 (RESOLVED): does PicoClaw persist usage tokens on disk? No for the
agentJSONL path (confirmed via the Goproviders.Messagestruct and the real captured session). A separatepkg/seahorseSQLite store has atoken_countcolumn, but theagentCLI writes the JSONL store, which has none. Tokens/cost are surfaced as unavailable, never fabricated. - Q2 (RESOLVED for v0.2.9): sessions are
<workspace>/sessions/<key>.jsonl(key is ask_v1_<hash>); confirm stability across future versions via the pinned fixture + CI. - Q3 (RESOLVED): real
contentis a string; the first line is a real user message (no session header).model_namemay be a bare alias (llama3) orprovider/model; both handled. - Q4: for live data, do we want gateway ingest on 18790? (out of scope now)
7. Verification plan
- Phase 1 (now):
pytest tests/test_picoclaw_adapter.py -vagainst correct-shape fixtures (tests/fixtures/runtimes/picoclaw/workspace/sessions/): detect, model parse, tool-call events,total_tokens==0, never-raise on garbage. In CI (see #956 acceptance criterion (e)). - Phase 2/3 (needs a real capture): install PicoClaw, run a session, point ClawMetry with no flags,
confirm
/api/sessionsshows the PicoClaw session + model and the Runtime popup says "PicoClaw"; decrypt the live cloud snapshot and confirm the runtime label + transcripts; screenshot the tab. Do not stamp "Verified" until a real capture passes a PicoClaw-shaped fixture.
8. Risks
- Marketing-credibility: shipping "Verified" on relabeled OpenClaw fixtures (the current README/docs state) is exactly the risk #956 set out to retire. The badge is downgraded to honest status until a real capture passes.
- Format drift: PicoClaw is young and actively developed; pin the verified commit in fixtures and let CI catch divergence.
Sources
github.com/sipeed/picoclaw:pkg/providers/protocoltypes/types.go,pkg/memory/jsonl.go,pkg/config/envkeys.go,pkg/agent/instance.go,docs/architecture/session-system.md,docs/guides/session-guide.md,docs/reference/cron.md.- ClawMetry:
dashboard.py(_get_sessions_from_files,_scan_session_aggregates,_auto_detect_data_dir,detect_config),clawmetry/local_store.py,clawmetry/providers_pricing.py,clawmetry/adapters/.