Timeouts
Purpose
Define the timeout budgeting model for Clawperator executions: execution-level timeout, action-level timeout on wait actions, builder inflation rules, and the runtime's best-effort ceiling.
Sources
- Limits:
apps/node/src/contracts/limits.ts - Validation:
apps/node/src/domain/executions/validateExecution.ts - Runtime enforcement:
apps/node/src/domain/executions/runExecution.ts - Wait builders:
apps/node/src/domain/actions/wait.ts,apps/node/src/domain/actions/waitForNav.ts - Snapshot builder:
apps/node/src/domain/observe/snapshot.ts - Sleep builder:
apps/node/src/domain/actions/sleep.ts execvalidation and override path:apps/node/src/cli/commands/execute.tsexec best-effortCLI routing:apps/node/src/cli/registry.ts
Two Timeout Levels
Clawperator currently has two timeout layers that matter to agents:
| Level | Field | Scope |
|---|---|---|
| execution-level | top-level execution.timeoutMs |
whole payload |
| action-level | params.timeoutMs on some actions |
one action's internal wait window |
The key rule is:
- the action-level timeout must fit inside the execution-level timeout
- builder helpers intentionally inflate the execution-level timeout above the action-level wait so Node does not kill the whole run before the wait action has a chance to finish
CLI Timeout Mapping
The --timeout flag does not land in the same place on every command.
Current CLI mappings:
| Command | CLI flag | Where it ends up |
|---|---|---|
clawperator wait --timeout <ms> |
global --timeout parsed into ctx.timeoutMs |
wait_for_node.params.timeoutMs, with buildWaitExecution() inflating top-level execution.timeoutMs |
clawperator wait-for-nav --timeout <ms> |
global --timeout parsed into ctx.timeoutMs |
wait_for_navigation.params.timeoutMs, with buildWaitForNavExecution() inflating top-level execution.timeoutMs |
clawperator exec --timeout <ms> |
cmdExecute() override |
replaces top-level execution.timeoutMs after validation |
Use --validate-only or --dry-run when you need to confirm which layer a CLI flag changed.
Execution-Level Timeout
The execution-level timeout is always the top-level timeoutMs on the execution payload.
Current hard limits:
- minimum:
1000 - maximum:
120000
Those values come from:
LIMITS.MIN_EXECUTION_TIMEOUT_MS = 1000LIMITS.MAX_EXECUTION_TIMEOUT_MS = 120000
If the caller provides a non-finite timeout or a value outside that range, Node returns a top-level validation failure:
{
"code": "EXECUTION_VALIDATION_FAILED",
"message": "timeoutMs must be between 1000 and 120000"
}
At runtime, runExecution() waits for the Android result envelope using:
execution.timeoutMs + 5000
That extra 5000 milliseconds is a buffer for envelope write. It does not change the documented execution budget. Agents should still reason from the top-level execution.timeoutMs.
If the runtime exceeds the budget and Node never receives a valid result envelope, the caller gets a top-level RESULT_ENVELOPE_TIMEOUT error, not a normal success wrapper.
Verification pattern - confirm a timeout value was accepted without dispatching:
clawperator exec --validate-only --execution '{"commandId":"timeout-check","taskId":"timeout-check","source":"docs","expectedFormat":"android-ui-automator","timeoutMs":30000,"actions":[{"id":"snap","type":"snapshot_ui"}]}'
Expected success shape:
{
"ok": true,
"validated": true,
"execution": {
"commandId": "timeout-check",
"taskId": "timeout-check",
"source": "docs",
"expectedFormat": "android-ui-automator",
"timeoutMs": 30000,
"actions": [
{
"id": "snap",
"type": "snapshot_ui"
}
]
}
}
Error cases for execution-level timeout:
- non-finite timeout:
EXECUTION_VALIDATION_FAILEDwithmessage: "timeoutMs must be a finite number" - timeout below
1000or above120000:EXECUTION_VALIDATION_FAILEDwithmessage: "timeoutMs must be between 1000 and 120000" - live execution that runs too long: top-level
RESULT_ENVELOPE_TIMEOUT - malformed terminal envelope during a live run: top-level
RESULT_ENVELOPE_MALFORMED
Action-Level Timeout
Only some actions use params.timeoutMs today. The main public case is wait_for_navigation, and the flat CLI wait builder also carries a wait-specific timeout value in params.timeoutMs for wait_for_node.
Current validated action-level rules:
| Action | Field | Valid values |
|---|---|---|
wait_for_navigation |
params.timeoutMs |
required, > 0, <= 30000 |
wait_for_node via CLI builder |
params.timeoutMs |
optional in raw schema, set by buildWaitExecution() when --timeout is passed |
Important boundary:
validateExecution.tsdoes not currently enforce a numeric range forwait_for_node.params.timeoutMs- the current public timeout contract for
wait_for_nodecomes from the CLI builder inapps/node/src/domain/actions/wait.ts, not from a dedicatedwaitForNode.tsmodule
For wait_for_navigation, invalid values fail validation before dispatch:
{
"code": "EXECUTION_VALIDATION_FAILED",
"message": "wait_for_navigation params.timeoutMs must not exceed 30000",
"details": {
"path": "actions.0.params.timeoutMs",
"actionId": "wait-for-nav",
"actionType": "wait_for_navigation"
}
}
Relationship between the two levels:
- action-level timeout controls how long the wait action should keep waiting for its condition
- execution-level timeout controls how long the full payload is allowed to exist
- when the action timeout is close to or greater than the execution timeout, the execution may fail early at the envelope layer
Verification pattern - confirm wait-for-nav action timeout is encoded:
clawperator wait-for-nav --app com.android.settings --timeout 5000 --validate-only
Expected validated execution shape:
{
"ok": true,
"validated": true,
"execution": {
"timeoutMs": 30000,
"actions": [
{
"id": "wait-for-nav",
"type": "wait_for_navigation",
"params": {
"expectedPackage": "com.android.settings",
"timeoutMs": 5000
}
}
]
}
}
For wait, the equivalent CLI mapping is:
clawperator wait --text "Done" --timeout 5000 --validate-only
Expected shape:
{
"ok": true,
"validated": true,
"execution": {
"timeoutMs": 30000,
"actions": [
{
"id": "wait",
"type": "wait_for_node",
"params": {
"matcher": {
"textEquals": "Done"
},
"timeoutMs": 5000
}
}
]
}
}
Builder Inflation Rules
The flat CLI builders intentionally inflate execution timeouts so the wait action has room to finish.
wait_for_node
buildWaitExecution() uses:
- default execution timeout:
30000 - if
waitTimeoutMsis provided:max(waitTimeoutMs + 5000, 30000)
So:
clawperator wait --text "Done" --timeout 5000becomes execution timeout30000clawperator wait --text "Done" --timeout 45000becomes execution timeout50000
Exact builder literals:
source: "clawperator-action"- action id:
wait - action type:
wait_for_node
wait_for_navigation
buildWaitForNavExecution() uses the same pattern:
- default execution timeout:
30000 - if
navTimeoutMsis provided:max(navTimeoutMs + 5000, 30000)
So:
clawperator wait-for-nav --app com.android.settings --timeout 5000gets execution timeout30000clawperator wait-for-nav --app com.android.settings --timeout 25000gets execution timeout30000clawperator wait-for-nav --app com.android.settings --timeout 30000gets execution timeout35000
Exact builder literals:
source: "clawperator-action"- action id:
wait-for-nav - action type:
wait_for_navigation
sleep
buildSleepExecution() uses:
max(durationMs + 5000, globalTimeoutMs ?? 0, 30000)
This is the same design principle: the whole execution must last longer than the single action's internal work.
Exact builder literals:
source: "clawperator-action"- action id:
sleep - action type:
sleep
snapshot_ui
buildSnapshotExecution() defaults to 30000. It has no separate action-level timeout field.
Exact snapshot builder literals:
source: "clawperator-observe"- action id:
snap - action type:
snapshot_ui mode: "direct"
Best-Effort Runtime Ceiling
LIMITS.MAX_BEST_EFFORT_RUNTIME_MS is 180000.
This constant exists in apps/node/src/contracts/limits.ts, but the current CLI exec best-effort path is still NOT_IMPLEMENTED and returns:
{
"code": "NOT_IMPLEMENTED",
"message": "exec best-effort is Stage 1 limited; use snapshot + agent reasoning for now"
}
What is implemented today:
- normal direct executions validate
execution.timeoutMsinside1000..120000 validateExecution()only acceptsmode: "direct"ormode: "artifact_compiled"MAX_BEST_EFFORT_RUNTIME_MSis a defined constant, not an active public execution contract for the current CLI path
Agent guidance:
- do not plan around
exec best-effortyet - do not assume
timeoutMs > 120000becomes valid because the constant exists
Concrete Budget Examples
Example 1: Simple snapshot
{
"commandId": "snap-1",
"taskId": "snap-1",
"source": "clawperator-observe",
"expectedFormat": "android-ui-automator",
"timeoutMs": 30000,
"actions": [
{ "id": "snap", "type": "snapshot_ui" }
]
}
Good budget because:
- single cheap action
- no action-level timeout
30000is the literal default used bybuildSnapshotExecution()- the runtime still gets its separate
+5000envelope buffer internally
Example 2: Navigation wait
{
"commandId": "nav-1",
"taskId": "nav-1",
"source": "clawperator-action",
"expectedFormat": "android-ui-automator",
"timeoutMs": 35000,
"actions": [
{
"id": "open",
"type": "open_app",
"params": {
"applicationId": "com.android.settings"
}
},
{
"id": "wait",
"type": "wait_for_navigation",
"params": {
"expectedPackage": "com.android.settings",
"timeoutMs": 30000
}
}
]
}
Why 35000 is the right budget here:
- the wait action itself is allowed to wait for up to
30000 - the builder rule adds a
5000cushion - keeping execution timeout equal to
30000would be too tight for open-app dispatch plus navigation wait plus envelope return
Example 3: Multi-step read after click
{
"commandId": "read-after-click",
"taskId": "read-after-click",
"source": "clawperator-action",
"expectedFormat": "android-ui-automator",
"timeoutMs": 45000,
"actions": [
{
"id": "click-1",
"type": "click",
"params": {
"matcher": { "textEquals": "Settings" }
}
},
{
"id": "sleep-1",
"type": "sleep",
"params": {
"durationMs": 1500
}
},
{
"id": "wait-1",
"type": "wait_for_node",
"params": {
"matcher": { "textEquals": "Connected devices" },
"timeoutMs": 10000
}
},
{
"id": "snap-1",
"type": "snapshot_ui"
}
]
}
Reasonable because:
- the action-level wait is
10000 - the click and sleep consume part of the same overall budget
- the final snapshot still needs time for extraction and envelope return
Verification pattern - inspect the plan without dispatching:
clawperator exec --dry-run --execution '{"commandId":"read-after-click","taskId":"read-after-click","source":"docs","expectedFormat":"android-ui-automator","timeoutMs":45000,"actions":[{"id":"click-1","type":"click","params":{"matcher":{"textEquals":"Settings"}}},{"id":"sleep-1","type":"sleep","params":{"durationMs":1500}},{"id":"wait-1","type":"wait_for_node","params":{"matcher":{"textEquals":"Connected devices"},"timeoutMs":10000}},{"id":"snap-1","type":"snapshot_ui"}]}'
Expected success shape:
{
"ok": true,
"dryRun": true,
"plan": {
"commandId": "read-after-click",
"timeoutMs": 45000,
"actionCount": 4,
"actions": [
{
"id": "click-1",
"type": "click",
"params": {
"matcher": {
"textEquals": "Settings"
}
}
},
{
"id": "sleep-1",
"type": "sleep",
"params": {
"durationMs": 1500
}
},
{
"id": "wait-1",
"type": "wait_for_node",
"params": {
"matcher": {
"textEquals": "Connected devices"
},
"timeoutMs": 10000
}
},
{
"id": "snap-1",
"type": "snapshot_ui"
}
]
}
}
If a live run returns RESULT_ENVELOPE_MALFORMED, treat it as a transport-contract failure rather than a normal action timeout. Recovery:
- rerun once to rule out transient logcat noise
- if it repeats, check CLI and APK compatibility with
clawperator version --check-compat --json - rerun the failing command with
--verboseand inspect Android-side logs
Common Mistakes
Execution timeout too close to wait timeout
Bad:
wait_for_navigation.params.timeoutMs = 30000execution.timeoutMs = 30000
Risk:
- the envelope wait can expire before the action has enough time to finish cleanly
Better:
- use the builder pattern:
max(actionTimeout + 5000, 30000)
Forgetting multi-step cost
A click -> sleep -> wait -> snapshot workflow needs more than the wait action's own timeout. Budget for the whole sequence, not just the longest single step.
Treating the +5000 buffer as extra application time
The builder cushion is there to stop the envelope layer from killing the run too early. It is not a guarantee that Android will continue useful work for exactly 5 extra seconds.
Passing out-of-range values
- execution timeout above
120000fails validation - execution timeout below
1000fails validation wait-for-nav --timeoutabove30000fails validationwait-for-navwithout--timeoutfails withMISSING_ARGUMENT- non-finite values fail validation
Practical Rules
- default to
30000for simple one-step or two-step payloads - for
wait_for_navigation, setexecution.timeoutMsto at leastactionTimeout + 5000 - for multi-step flows, add budget for earlier actions and the final envelope
- if you hit
RESULT_ENVELOPE_TIMEOUT, verify health with Doctor before only increasing the timeout - use
--validate-onlyor--dry-runto confirm the final timeout value before you spend a live device run