Recording Format
Purpose
Document the current recording workflow, the raw NDJSON schema written by the Operator app, the parsed step-log format produced by clawperator record parse, the agent-context export produced by clawperator recording export, and the compare workflow exposed by clawperator recording compare.
Sources
- Raw event schema:
apps/node/src/domain/recording/recordingEventTypes.ts - Parser behavior:
apps/node/src/domain/recording/parseRecording.ts - Pull behavior:
apps/node/src/domain/recording/pullRecording.ts - CLI commands:
apps/node/src/cli/commands/record.ts,apps/node/src/cli/registry.ts - Export builder:
apps/node/src/domain/recording/exportRecording.ts - Compare builder:
apps/node/src/domain/recording/compareRecording.ts - Shared validation:
apps/node/src/domain/recording/recordingValidation.ts - Public error codes:
apps/node/src/contracts/errors.ts
Recording As Evidence
Recordings are evidence for later authoring, debugging, and compare work. They are not executable skills.
Current durable rules:
- retain the pulled NDJSON as the raw capture
- retain
clawperator recording exportoutput as the canonical structured artifact for authoring and compare - use
clawperator record parseas lossy human inspection only - do not treat a recording export or parsed step log as a reusable skill with only light cleanup
What the retained export gives you:
- the raw event timeline
- package-transition evidence
- candidate selectors, text, and timing
- optional preserved XML snapshots when
--snapshots includeis used
What it does not give you:
- the final reusable control flow for a skill
- the final selector strategy for every tap
- terminal verification policy
- a guarantee that the recorded visible label was the real clickable node
Practical evidence quality rules:
- start the recording from a fresh app state whenever possible
- if the author already explored the target app manually, close the target app or apps before recording so the capture reflects a reusable path rather than a half-completed mid-flow state
- a single recording is the minimum evidence, not always the ideal evidence
- when the first recording looks exploratory, sparse, or branch-dependent, capture another pass rather than pretending one shaky run is authoritative
Recording Lifecycle
The current flow is:
clawperator record start [--session-id <id>]- interact with the device
clawperator record stop [--session-id <id>]clawperator record pull [--session-id <id>] [--out <dir>]clawperator recording export --input <file|directory> [--out <file>] [--snapshots <omit|include>]clawperator record parse --input <file> [--out <file>]clawperator recording compare --baseline <export.json> --result <skills-run.json> [--mode <auto|literal|semantic>]
Notes:
recordis a top-level alias forrecordingpulldefaults to./recordings/when--outis omitted- the practical authoring order is
pull, thenexport, then optionalparseor other human inspection - do not treat
parseas the only retained baseline artifact; it intentionally drops event detail thatexportpreserves - do not run
exportorparseagainst a recording directory untilpullhas finished writing the local NDJSON file you plan to keep parsewrites<input without .ndjson>.steps.jsonwhen the input ends with.ndjson, otherwise<input>.steps.jsonrecord startbuilder timeout is10000record stopbuilder timeout is15000recording exportdefaults to--snapshots omitrecording comparedefaults to--mode auto- if
recording export --inputpoints at a file and--outis omitted, the output path is<input without .ndjson>.export.jsonwhen the input ends with.ndjson, otherwise<input>.export.json - if
recording export --inputpoints at a directory, the command picks the newest*.ndjsonfile in that directory and derives the default export path from that resolved file recording comparereads a savedclawperator skills run --jsonwrapper file and extracts its top-levelskillResult- for authored skills, the durable retained baseline for compare should live under a reference-style path such as
skills/<skill_id>/references/compare-baseline.export.json - that retained baseline is authoring and maintenance evidence, not a runtime artifact consumed by
skills run - a common authoring workflow is:
recording stoprecording pull --out <dir>recording export --input <same dir>skills run <skill_id> --json > <run>.skills-run.json- copy the retained export to
skills/<skill_id>/references/compare-baseline.export.json recording compare --baseline skills/<skill_id>/references/compare-baseline.export.json --result <run>.skills-run.json
Recommended pre-recording reset:
- ask which target app or apps the user intends to record
- close those apps through Clawperator before
recording start - do not rely on the user manually swiping apps away unless the workflow makes that explicit
- for a single app reset, prefer the flat CLI:
clawperator close --app <application_id> --device <device_id> --operator-package <operator_package> --json - for multiple app resets, use a small
clawperator execwith one or moreclose_appactions before you start recording - the underlying API action is
close_app, which Node executes as an adbam force-stoppre-flight and normalizes to a successful close only when that force-stop actually succeeded
Recommended recording-count stance:
- default to one recording for a first pass
- recommend a second recording when the first pass looks exploratory, state-dependent, or unusually sparse
- use a third recording only when the flow is especially flaky or divergent
- do not silently average multiple recordings; explain what the extra pass is meant to confirm
CLI Commands
Start
clawperator record start [--session-id <id>] [--device <serial>] [--operator-package <pkg>]
Current builder payload:
{
"commandId": "start_recording_1700000000000",
"taskId": "cli-record-start",
"source": "clawperator-cli",
"timeoutMs": 10000,
"expectedFormat": "android-ui-automator",
"actions": [
{
"id": "a1",
"type": "start_recording",
"params": {
"sessionId": "optional-session-id"
}
}
]
}
Exact builder literals:
taskId: "cli-record-start"source: "clawperator-cli"timeoutMs: 10000- action id:
a1
Verification:
clawperator record start --session-id demo-session --device <device_serial> --json
Expected success wrapper shape:
{
"envelope": {
"status": "success",
"stepResults": [
{
"actionType": "start_recording",
"success": true,
"data": {
"sessionId": "demo-session",
"filePath": "/sdcard/Android/data/com.clawperator.operator.dev/files/recordings/demo-session.ndjson"
}
}
]
},
"deviceId": "<device_serial>",
"terminalSource": "clawperator_result",
"isCanonicalTerminal": true
}
Stop
clawperator record stop [--session-id <id>] [--device <serial>] [--operator-package <pkg>]
Current builder payload:
{
"commandId": "stop_recording_1700000000000",
"taskId": "cli-record-stop",
"source": "clawperator-cli",
"timeoutMs": 15000,
"expectedFormat": "android-ui-automator",
"actions": [
{
"id": "a1",
"type": "stop_recording",
"params": {
"sessionId": "optional-session-id"
}
}
]
}
Exact builder literals:
taskId: "cli-record-stop"source: "clawperator-cli"timeoutMs: 15000- action id:
a1
Verification:
clawperator record stop --session-id demo-session --device <device_serial> --json
Expected success wrapper shape:
{
"envelope": {
"status": "success",
"stepResults": [
{
"actionType": "stop_recording",
"success": true,
"data": {
"sessionId": "demo-session",
"filePath": "/sdcard/Android/data/com.clawperator.operator.dev/files/recordings/demo-session.ndjson",
"eventCount": "17"
}
}
]
}
}
Pull
clawperator record pull [--session-id <id>] [--out <dir>] [--device <serial>] [--operator-package <pkg>]
Successful response shape:
{
"ok": true,
"localPath": "recordings/demo-session.ndjson",
"sessionId": "demo-session"
}
Exact default:
- if
--outis omitted,registry.tssetsoutputDirto./recordings/
Verification:
clawperator record pull --session-id demo-session --device <device_serial> --json
Check:
ok == truesessionId == "demo-session"localPathends with/demo-session.ndjson
Parse
clawperator record parse --input <file> [--out <file>]
Successful response shape:
{
"ok": true,
"outputFile": "./recordings/demo-session.steps.json",
"stepCount": 2,
"warnings": [
"seq 3: scroll event dropped (not extracted in v1)"
]
}
Exact default output-file rule from cmdRecordParse():
- if input ends with
.ndjson, output is<input without .ndjson>.steps.json - otherwise output is
<input>.steps.json
Verification:
clawperator record parse --input ./recordings/demo-session.ndjson --json
Check:
ok == trueoutputFile == "./recordings/demo-session.steps.json"stepCountmatches the parsedsteps.lengthstdoutcontains the JSON result, whilestderralso receives a human-readable step summary fromprintStepSummary()
Export
clawperator recording export --input <file|directory> [--out <file>] [--snapshots <omit|include>] [--output <json|pretty>]
clawperator record export --input <file|directory> [--out <file>] [--snapshots <omit|include>] [--output <json|pretty>]
What the command does:
- reads a local NDJSON recording file
- validates it with the shared recording validator
- preserves every supported raw event type in a richer JSON artifact
- writes the export to disk
- returns a compact success wrapper with the export path and summary counts
- does not derive skills, selectors, parameters, or plans
Snapshot mode:
omitis the defaultincludepreserves the raw XML string insnapshot.xml- both modes always preserve
snapshot.present omitis usually the right authoring default because it preserves event evidence without embedding the raw XML snapshotsincludeis better when an external agent or human needs the full hierarchy snapshots for detailed manual review
Output-path rule:
- if
--outis omitted and the input is a file ending with.ndjson, the output path is<input without .ndjson>.export.json - if
--outis omitted and the input is a file without a trailing.ndjson, the output path is<input>.export.json - if
--outis omitted and the input is a directory, the command first resolves the newest*.ndjsonfile in that directory and then applies the same default-path rule to that resolved file - if
--outis provided, that path is used as-is
Success wrapper shape:
{
"ok": true,
"outputFile": "./recordings/demo-session.export.json",
"sessionId": "demo-session",
"eventCount": 5,
"packageTransitionCount": 2,
"byType": {
"window_change": 1,
"click": 1,
"scroll": 1,
"press_key": 1,
"text_change": 1
}
}
Verification:
clawperator recording export --input ./recordings/export-demo.ndjson --json
Check:
ok == trueoutputFileends with.export.jsoneventCountmatchescounts.totalEventsinside the written filepackageTransitionCountmatches the number of computed package transitionsbyTypematches the event-type counts inside the written file
Practical size tradeoff:
- on a real small emulator capture, an export written with
--snapshots omitwas about2 KB - the same recording written with
--snapshots includewas about165 KB - the raw pulled NDJSON was about
164 KB
This is why omit is the default for agent-context export.
Export file contract:
{
"exportVersion": 1,
"session": {
"sessionId": "demo-session",
"schemaVersion": 1,
"startedAt": 1710000000000,
"operatorPackage": "com.clawperator.operator.dev"
},
"snapshotMode": "omit",
"events": [
{
"seq": 0,
"ts": 1710000000000,
"deltaMsSincePrevious": null,
"type": "window_change",
"packageName": "com.android.settings",
"className": "com.android.settings.Settings",
"title": "Settings",
"snapshot": {
"present": true,
"xml": null
}
}
],
"counts": {
"totalEvents": 1,
"byType": {
"window_change": 1
}
},
"packageTransitions": [],
"timeline": {
"firstEventTs": 1710000000000,
"lastEventTs": 1710000000000,
"durationMs": 0
}
}
Exported event types:
| Type | Preserved fields |
|---|---|
window_change |
seq, ts, deltaMsSincePrevious, type, packageName, className, title, snapshot |
click |
seq, ts, deltaMsSincePrevious, type, packageName, resourceId, text, contentDesc, bounds, snapshot |
scroll |
seq, ts, deltaMsSincePrevious, type, packageName, resourceId, scrollX, scrollY, maxScrollX, maxScrollY, snapshot |
press_key |
seq, ts, deltaMsSincePrevious, type, key, snapshot |
text_change |
seq, ts, deltaMsSincePrevious, type, packageName, resourceId, text, snapshot |
Compare
clawperator recording compare --baseline <export.json> --result <skills-run.json> [--mode <auto|literal|semantic>] [--output <json|pretty>]
clawperator record compare --baseline <export.json> --result <skills-run.json> [--mode <auto|literal|semantic>] [--output <json|pretty>]
What the command does:
- reads a recording export artifact from
--baseline - reads a saved
clawperator skills run --jsonwrapper from--result - extracts the wrapper's top-level
skillResult - normalizes the export into a checkpoint baseline
- compares that baseline against
skillResult.checkpointsplusskillResult.terminalVerification - returns a typed compare report
What compare treats as authoritative:
- the recording export is baseline evidence, not a ready-made checkpoint list
- compare derives a smaller checkpoint baseline from the export's structural facts
skillResult.checkpointsprovide the path evidence for the current runskillResult.terminalVerificationis the final-state proof channel- compare ignores the duplicated
terminal_state_verifiedcheckpoint id during path matching and usesterminalVerificationinstead
Normalization scope:
- v1 baseline normalization uses Solax-specific heuristics to extract four structural checkpoints from the recording export:
app_opened(first in-appwindow_change),discharge_to_row_focused(first click matchingdischarge),target_text_entered(lasttext_changewith non-empty text), andsave_completed(last click matchingsaveorconfirm) - compare requires all four checkpoints to be extractable from the baseline export; if normalization produces fewer, compare returns
normalization_insufficientinstead of proceeding with a partial baseline - recording exports from other app flows will produce
normalization_insufficientuntil per-skill declared checkpoint baselines are supported in a future release - every compare report includes
normalizationStrategy: "solax_heuristic"so consumers know which normalization path was used - this closeout makes the Solax heuristic path honest and fail-closed; it does not make compare generic
Mode selection:
autois the defaultautoselectssemanticwhenskillResult.source.kind == "agent"autoselectsliteralwhenskillResult.source.kind == "script"--mode literaland--mode semanticoverride the auto-selected mode
Current v1 compare outcomes:
literal_matchsemantic_matchoutcome_matches_path_differsbaseline_driftverification_failedverification_indeterminateupstream_failureruntime_poisonedruntime_unavailablenormalization_insufficientbaseline_uncoveredbaseline_weakly_covered
Current interpretation rules:
literal_matchis the success case for replay-style or other script-driven runs whose checkpoint path matches the retained baselinesemantic_matchis the success case for agent-driven runs whose checkpoint path still matches the retained baselineoutcome_matches_path_differsis also a success case, used when an agent-driven run proves the same terminal outcome through a different valid checkpoint pathbaseline_driftis the path-divergence failure class for runs that should still be path-sensitiveverification_failedmeans the path matched but the proved final state did notverification_indeterminatemeans the run did not prove the declared final state at allupstream_failure,runtime_poisoned, andruntime_unavailablereport the skill's own failure state instead of inventing later divergencenormalization_insufficientmeans the baseline export did not produce the required checkpoint set through heuristic normalization; compare cannot proceed and does not attempt path or terminal comparisonbaseline_uncoveredmeans terminal verification passed for an agent-driven run, but no baseline checkpoint IDs appeared in the actual run at all; this is suspicious because the baseline is effectively irrelevant to the path the skill tookbaseline_weakly_coveredmeans terminal verification passed for an agent-driven run, but the overlap with the baseline was below the minimum trusted threshold for the Solax heuristic path
Exit-code contract:
- exit
0forliteral_match - exit
0forsemantic_match - exit
0foroutcome_matches_path_differs - exit non-zero for
normalization_insufficient - exit non-zero for
baseline_uncovered - exit non-zero for
baseline_weakly_covered - exit non-zero for every other compare outcome
- exit non-zero for input or parse errors
Result-wrapper requirement:
--resultmust be a savedskills run --jsonwrapper object- v1 compare does not accept a bare
SkillResultdocument - the wrapper must contain a top-level non-null
skillResult - when you want durable compare evidence, save the full wrapper and keep it as the compare input rather than copying only the embedded
skillResult
Successful semantic compare example:
{
"compareMode": "semantic",
"outcome": "outcome_matches_path_differs",
"summary": "terminal verification matched even though the runtime path differed from the recording baseline",
"pathMatches": false,
"terminalVerificationStatus": "verified",
"baseline": {
"appPackage": "com.solaxcloud.starter",
"checkpointIds": [
"app_opened",
"discharge_to_row_focused",
"target_text_entered",
"save_completed"
]
},
"actual": {
"skillId": "com.solaxcloud.starter.set-discharge-to-limit-orchestrated",
"sourceKind": "agent",
"status": "success",
"runtimeState": "healthy",
"checkpointIds": [
"app_opened",
"device_discharging_card_opened",
"discharge_to_row_focused",
"target_text_entered",
"save_completed"
]
},
"baselineCoverage": {
"declared": 4,
"covered": 4
},
"normalizationStrategy": "solax_heuristic",
"minimumSemanticCoverage": 2,
"firstDivergence": {
"index": 1,
"baselineCheckpoint": "discharge_to_row_focused",
"actualCheckpoint": "device_discharging_card_opened",
"baselineStatus": "ok",
"actualStatus": "ok",
"baselineSummary": "click:com.solaxcloud.starter:discharge to"
}
}
Every compare report includes baselineCoverage and normalizationStrategy:
baselineCoverage.declaredis the number of baseline checkpoint IDsbaselineCoverage.coveredis how many of those IDs appeared in the actual runnormalizationStrategyis"solax_heuristic"in v1minimumSemanticCoverageis2in v1 for the Solax heuristic path- the current trust bar is enforced by fixture-backed regression tests for the Solax proving flow, not by a generic per-skill compare contract
Divergence example:
{
"compareMode": "semantic",
"outcome": "verification_failed",
"summary": "checkpoint sequence matched the recording baseline but terminal verification did not match the requested outcome",
"pathMatches": true,
"terminalVerificationStatus": "failed"
}
Verification:
clawperator recording compare \
--baseline ./skills/com.solaxcloud.starter.set-discharge-to-limit-orchestrated/references/compare-baseline.export.json \
--result ./runs/demo.skills-run.json \
--json
Check:
compareModematches the requested mode or theskillResult.source.kindauto-selection ruleoutcomeis one of the v1 outcome enums abovepathMatchesisfalseonly when compare found a first checkpoint divergencefirstDivergenceis present whenpathMatches == false
Deterministic derived fields:
- events are sorted by
seqbefore export deltaMsSincePreviousisnullfor the first event, thencurrent.ts - previous.ts- package transitions are computed from adjacent package-bearing events only
press_keyevents are skipped for package-transition comparisontimeline.durationMsislastEventTs - firstEventTs
Observed-runtime caveat:
- the export preserves every supported raw event type when those events are present in the NDJSON
- recording still reflects what the runtime actually observed, not a one-to-one replay of CLI commands
- in a real emulator run, a
backCLI action produced downstreamwindow_changeevents but no rawpress_keyevent in the recording - do not assume every device action will always appear as a distinct raw event type
Failure modes:
- malformed header or event data:
RECORDING_PARSE_FAILED - unsupported schema version:
RECORDING_SCHEMA_VERSION_UNSUPPORTED - input path inspection or read failure:
RECORDING_EXPORT_FAILED - output write failure:
RECORDING_EXPORT_FAILED
Recovery:
RECORDING_EXPORT_FAILED: confirm the--inputpath exists and is readable, or fix the--outpath and parent-directory permissions
The export is evidence for an external authoring agent or human. It is not an automatic skill generator.
NDJSON Format
A recording file is newline-delimited JSON with:
- one header line
- zero or more event lines
The first non-empty line must be a recording_header.
Verification pattern - minimum valid file skeleton:
{"type":"recording_header","schemaVersion":1,"sessionId":"demo-session","startedAt":1710000000000,"operatorPackage":"com.clawperator.operator.dev"}
{"ts":1710000000001,"seq":0,"type":"window_change","packageName":"com.android.settings","className":"com.android.settings.Settings","title":"Settings","snapshot":"<hierarchy .../>"}
Header Line
Current header schema:
| Field | Type | Meaning |
|---|---|---|
type |
"recording_header" |
fixed discriminator |
schemaVersion |
number |
current parser supports only 1 |
sessionId |
string |
recording id |
startedAt |
number |
epoch milliseconds |
operatorPackage |
string |
Operator package that produced the recording |
Example:
{"type":"recording_header","schemaVersion":1,"sessionId":"demo-session","startedAt":1710000000000,"operatorPackage":"com.clawperator.operator.dev"}
Event Types
Current raw event union:
window_changeclickscrollpress_keytext_change
Every event must include:
tsseqtype
window_change
| Field | Type |
|---|---|
ts |
number |
seq |
number |
type |
"window_change" |
packageName |
string |
className |
string \| null |
title |
string \| null |
snapshot |
string \| null \| undefined |
Example:
{"ts":1710000000000,"seq":0,"type":"window_change","packageName":"com.android.settings","className":"com.android.settings.Settings","title":"Settings","snapshot":"<hierarchy .../>"}
click
| Field | Type |
|---|---|
ts |
number |
seq |
number |
type |
"click" |
packageName |
string |
resourceId |
string \| null |
text |
string \| null |
contentDesc |
string \| null |
bounds.left |
number |
bounds.top |
number |
bounds.right |
number |
bounds.bottom |
number |
snapshot |
string \| null \| undefined |
Example:
{"ts":1710000000800,"seq":1,"type":"click","packageName":"com.android.settings","resourceId":"android:id/title","text":"Connected devices","contentDesc":null,"bounds":{"left":216,"top":1503,"right":661,"bottom":1573},"snapshot":"<hierarchy .../>"}
scroll
| Field | Type |
|---|---|
ts |
number |
seq |
number |
type |
"scroll" |
packageName |
string |
resourceId |
string \| null |
scrollX |
number |
scrollY |
number |
maxScrollX |
number |
maxScrollY |
number |
snapshot |
string \| null \| undefined |
press_key
| Field | Type |
|---|---|
ts |
number |
seq |
number |
type |
"press_key" |
key |
"back" |
snapshot |
string \| null \| undefined |
Important:
- the current schema only allows
key: "back" - any other
keyvalue causesRECORDING_PARSE_FAILED
text_change
| Field | Type |
|---|---|
ts |
number |
seq |
number |
type |
"text_change" |
packageName |
string |
resourceId |
string \| null |
text |
string |
snapshot |
string \| null \| undefined |
Parse Output Shape
record parse does not replay the whole NDJSON one-to-one. It normalizes it into a smaller step log:
{
"sessionId": "demo-session",
"schemaVersion": 1,
"steps": [
{
"seq": 0,
"type": "open_app",
"packageName": "com.android.settings",
"uiStateBefore": "<hierarchy .../>"
},
{
"seq": 1,
"type": "click",
"packageName": "com.android.settings",
"resourceId": "android:id/title",
"text": "Connected devices",
"contentDesc": null,
"bounds": {
"left": 216,
"top": 1503,
"right": 661,
"bottom": 1573
},
"uiStateBefore": "<hierarchy .../>"
}
],
"_warnings": [
"seq 3: scroll event dropped (not extracted in v1)"
]
}
Important:
parseis intentionally lossy and step-orientedexportis intentionally evidence-preserving and event-oriented- the same recording can produce a small parsed step log while the export still contains multiple raw events such as
scroll,window_change, ortext_change
Current parsed step types:
open_appclick
Current normalization rules in parseRecording.ts:
- the first
window_changebecomes oneopen_appstep - every
clickbecomes oneclickstep scrollevents are dropped and produce warningstext_changeevents are dropped silentlypress_keyevents are dropped silently, but they do affect subsequentwindow_changehandling
Verification:
clawperator record parse --input ./recordings/demo-session.ndjson --json
Then open the written .steps.json file and confirm:
schemaVersion == 1steps[0].type == "open_app"when the first raw event waswindow_change_warningsis present only when parser warnings were generated
Parser Warnings
The parser currently emits warnings for:
window_changeorclickevents missingsnapshot- dropped
scrollevents
Warnings are written into _warnings in the parsed step log and also surfaced by record parse in its success wrapper when present.
This is an exact optional-field rule:
- if there are no warnings,
_warningsis omitted from the parsed JSON - if there are warnings,
_warningsis present andrecord parsealso copies them into the top-levelwarningsarray of its success wrapper
Pull Semantics
pullRecording() determines the session id like this:
- use
--session-idif provided - otherwise read
/sdcard/Android/data/<operatorPackage>/files/recordings/latest
Session ids are accepted only if they match:
^[a-zA-Z0-9_-]+$
This is the exact safe session-id pattern from pullRecording.ts.
The pulled file path is:
/sdcard/Android/data/<operatorPackage>/files/recordings/<sessionId>.ndjson
Error cases:
- invalid
--session-idformat:RECORDING_SESSION_NOT_FOUND - no
latestpointer file or empty pointer file:RECORDING_SESSION_NOT_FOUND - adb pull failure:
RECORDING_PULL_FAILED
Error Codes
Only document codes that exist in apps/node/src/contracts/errors.ts.
| Code | When it happens |
|---|---|
RECORDING_ALREADY_IN_PROGRESS |
runtime rejected record start because a session is already active |
RECORDING_NOT_IN_PROGRESS |
runtime rejected record stop because no session is active |
RECORDING_SESSION_NOT_FOUND |
record pull could not resolve a session id or the provided id was invalid |
RECORDING_PULL_FAILED |
adb pull failed |
RECORDING_PARSE_FAILED |
malformed file, invalid header, bad event fields, bad NDJSON, or unknown event type |
RECORDING_EXPORT_FAILED |
recording export input could not be inspected/read, or the output file could not be written |
RECORDING_COMPARE_FAILED |
compare could not read or parse the baseline export, result wrapper, or embedded skillResult |
RECORDING_SCHEMA_VERSION_UNSUPPORTED |
header schema version was not 1 |
Related CLI usage error:
record parsewithout--inputreturns a top-levelUSAGEobject fromregistry.ts, not aRECORDING_PARSE_FAILEDerror code
Common Failure Modes
RECORDING_SESSION_NOT_FOUND
Typical causes:
- no
latestfile on device - invalid
--session-idcharacters - trying to pull before a recording was started
Typical failure shape:
{
"code": "RECORDING_SESSION_NOT_FOUND",
"message": "No recording session found on device. Start a recording first."
}
Recovery:
- run
clawperator record start --session-id <id>before trying to pull - if you expected the latest pointer to exist, stop the active recording first so the device writes the finished session metadata
- if you passed
--session-id, confirm it matches^[a-zA-Z0-9_-]+$
RECORDING_ALREADY_IN_PROGRESS
Typical cause:
record startwas called while the Operator runtime already had an active recording session
Typical failure shape:
{
"code": "RECORDING_ALREADY_IN_PROGRESS",
"message": "Recording is already in progress",
"sessionId": "record-123",
"filePath": "/storage/emulated/0/Android/data/com.clawperator.operator.dev/files/recordings/record-123.ndjson",
"hint": "Run 'clawperator recording stop --session-id record-123 --device <device_serial> --operator-package <package> --json' before starting a new recording."
}
Recovery:
- run
clawperator recording stop --session-id <active_session_id> --device <device_serial> --operator-package <package> --json - then pull or parse the finished session before starting a new one
- if your workflow uses explicit session ids, reuse the active session id instead of starting a second overlapping recording
- use the
sessionIdandfilePathfields in the error payload to target the exact session that is still active - the CLI also surfaces the same hint inside the failed
start_recordingstep and the top-level envelope for easy copy/paste
Verification pattern:
clawperator record stop --device <device_serial> --json
clawperator record pull --device <device_serial> --json
RECORDING_NOT_IN_PROGRESS
Typical cause:
record stopwas called when no recording session was active on the device
Typical failure shape:
{
"code": "RECORDING_NOT_IN_PROGRESS",
"message": "Recording is not in progress"
}
Recovery:
- start a recording first with
clawperator record start --session-id <id> --device <device_serial> - only call
record stopafter the session has actually started
Verification pattern:
clawperator record start --session-id demo-session --device <device_serial> --json
clawperator record stop --session-id demo-session --device <device_serial> --json
RECORDING_PULL_FAILED
Typical causes:
- adb transport failure during
adb pull - disconnected device
- remote recording file missing even though the session id resolved
Typical failure shape:
{
"code": "RECORDING_PULL_FAILED",
"message": "Failed to pull recording from device: adb: error: failed to stat remote object '/sdcard/Android/data/com.clawperator.operator.dev/files/recordings/demo-session.ndjson': No such file or directory"
}
Recovery:
- confirm the device is still visible in
clawperator devices --json - rerun
clawperator record stop --session-id <id>if the session may still be open - retry
record pullwith the exact--session-idyou just stopped - if adb itself is failing, fix the transport problem before retrying
Verification pattern:
clawperator devices --json
clawperator record pull --session-id demo-session --device <device_serial> --json
Runtime Step Errors Outside The Public Node Error Enum
record start and record stop can also surface Android runtime step errors that are not part of apps/node/src/contracts/errors.ts.
Current examples from RecordingManager.kt and UiActionEngine.kt:
RECORDING_STORAGE_UNAVAILABLERECORDING_START_FAILEDRECORDING_STOP_FAILED
These values appear inside envelope.stepResults[].data.error, not as top-level Node code values.
Branching rule:
- top-level
codeis the public Node error contract documented incontracts/errors.ts stepResults[].data.errormay also contain Android runtime-specific strings that are useful for recovery but are not part of the public top-level enum
Typical recovery:
RECORDING_STORAGE_UNAVAILABLE: verify the Operator app can access its external files directory, then retryrecord startRECORDING_START_FAILED: check the suppliedsessionIdand retry with a safe id matching^[a-zA-Z0-9_-]+$RECORDING_STOP_FAILED: retryrecord stop, then inspect device state if finalization keeps timing out
RECORDING_PARSE_FAILED
Typical causes:
- empty file
- missing header
- malformed JSON on any line
- event object missing required fields
- unsupported event
type
Typical failure shape:
{
"code": "RECORDING_PARSE_FAILED",
"message": "Malformed NDJSON at line 3"
}
RECORDING_SCHEMA_VERSION_UNSUPPORTED
The parser is strict:
- only
schemaVersion: 1is accepted
Agents should branch on this code and stop rather than trying to guess how to parse a newer schema.
Typical failure shape:
{
"code": "RECORDING_SCHEMA_VERSION_UNSUPPORTED",
"message": "Unsupported recording schema version: 2"
}
What Agents Should Rely On
- raw recording files are NDJSON, header first
record parsecurrently extracts onlyopen_appandclicksteps- warnings are significant because they explain dropped or degraded data
- use parsed output as a deterministic summary, not as a promise that every raw event was preserved
- verify recording state with the returned JSON wrappers instead of assuming
record startorrecord stopworked from exit code alone