Control OpenCode directly via the Agent Client Protocol (ACP). Start sessions, send prompts, resume conversations, and manage OpenCode updates. Includes auto...
Control OpenCode directly via the Agent Client Protocol (ACP) with automatic recovery and stuck detection.
| Feature | Description |
|---|---|
| Auto-retry | Automatically retries on failure (max 3 attempts) |
| Stuck detection | Detects when OpenCode is not responding |
| Lock cleanup | Automatically removes stale lock files |
| Adaptive polling | Polls faster at start, slower when stable |
| Health checks | Periodic checks that OpenCode is alive |
| Configurable timeouts | Shorter timeouts with escalation |
| Session recovery | Can recover from crashes mid-task |
| Action | How |
|---|---|
| Start OpenCode | exec(command: "opencode acp --cwd /path", background: true) |
| Send message | process.write(sessionId, data: "<json-rpc>\n") |
| Read response | process.poll(sessionId) - adaptive polling |
| Health check | process.poll(sessionId, timeout: 5000) - only when no output >60s |
| Stop OpenCode | process.kill(sessionId) + cleanup locks |
| Clean locks | exec(command: "rm -f ~/.openclaw/agents/*/sessions/*.lock") |
| List sessions | exec(command: "opencode session list", workdir: "...") |
| Resume session | List sessions → session/load |
For most use cases, use this simple workflow:
1. exec(command: "opencode acp --cwd /path/to/project", background: true)
-> sessionId: "bg_42"
2. process.write(sessionId: "bg_42", data: initialize_json + "\n")
process.poll(sessionId: "bg_42", timeout: 10000)
3. process.write(sessionId: "bg_42", data: session_new_json + "\n")
process.poll(sessionId: "bg_42", timeout: 10000)
-> opencodeSessionId: "sess_xyz"
4. process.write(sessionId: "bg_42", data: prompt_json + "\n")
adaptivePoll(sessionId: "bg_42", maxWaitMs: 120000)
5. When done: process.kill(sessionId: "bg_42")
cleanupLocks()
| File | Purpose |
|---|---|
SKILL.md | This file - main documentation |
config.default.json | Default configuration (copy to config.json to customize) |
templates.md | Prompt templates for common tasks |
All defaults are built-in. No config file needed.
Copy config.default.json to config.json in the same folder and modify:
cp config.default.json config.json
# Edit config.json with your preferences
Config structure:
{
"timeouts": { "initialize": 10000, "prompt": {...} },
"retry": { "maxAttempts": 3, "initialDelay": 2000 },
"polling": { "initial": 1000, "active": 2000 },
"healthCheck": { "noOutputThreshold": 60000 },
"recovery": { "autoRecover": true },
"mcpServers": { "default": [], "supabase": ["supabase"] }
}
| Operation | Default | Max | When to increase |
|---|---|---|---|
| Initialize | 10s | 30s | Slow machine |
| Session new | 10s | 30s | Large project |
| Prompt (simple) | 60s | 120s | Complex query |
| Prompt (complex) | 120s | 300s | Refactor, code gen |
| Health check | 5s | 10s | Network issues |
| Setting | Default | Description |
|---|---|---|
| Max retries | 3 | How many times to retry on failure |
| Retry delay | 2s | Initial delay between retries |
| Backoff multiplier | 2 | Delay doubles each retry |
| Max retry delay | 10s | Maximum delay between retries |
| Phase | Interval | Duration |
|---|---|---|
| Initial | 1s | First 10s |
| Active | 2s | 10s - 60s |
| Stable | 3s | 60s - 120s |
| Slow | 5s | 120s+ |
OpenCode is considered "stuck" when:
When stuck is detected:
1. Cancel current operation: session/cancel
2. Wait 2 seconds
3. If still stuck: process.kill
4. Clean up locks
5. Restart OpenCode
6. Resume from last known session (if available)
Lock files can become stale when OpenCode crashes. Always clean up:
# Before starting a new session
exec(command: "find ~/.openclaw/agents -name '*.lock' -mmin +30 -delete")
# After killing a stuck process
exec(command: "rm -f ~/.openclaw/agents/*/sessions/*.lock")
Before starting, verify environment:
# Check OpenCode is installed
exec(command: "opencode --version")
# Clean stale locks (older than 30 minutes)
exec(command: "find ~/.openclaw/agents -name '*.lock' -mmin +30 -delete")
exec(
command: "opencode acp --cwd /path/to/project",
background: true,
workdir: "/path/to/project"
)
# Save sessionId for all subsequent operations
// Send initialize
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{"fs":{"readTextFile":true,"writeTextFile":true},"terminal":true},"clientInfo":{"name":"clawdbot","title":"Clawdbot","version":"2.0.0"}}}
Retry logic:
for attempt in 1..3:
process.write(sessionId, initialize_json + "\n")
response = process.poll(sessionId, timeout: 10000)
if response contains protocolVersion:
break # Success
if attempt < 3:
sleep(2 * attempt) # Backoff: 2s, 4s
else:
# Recovery mode
process.kill(sessionId)
cleanupLocks()
restart from Step 2
{"jsonrpc":"2.0","id":1,"method":"session/new","params":{"cwd":"/path/to/project","mcpServers":[]}}
Same retry logic as initialize.
{"jsonrpc":"2.0","id":2,"method":"session/prompt","params":{"sessionId":"sess_xyz","prompt":[{"type":"text","text":"Your question here"}]}}
Adaptive polling:
elapsed = 0
interval = 1000 # Start at 1s
maxWait = 120000 # 2 minutes for complex tasks
while elapsed < maxWait:
response = process.poll(sessionId, timeout: interval)
if response contains stopReason:
return response # Done
if response contains error:
handle_error(response)
break
# Adaptive interval
elapsed += interval
if elapsed < 10000:
interval = 1000 # First 10s: poll every 1s
elif elapsed < 60000:
interval = 2000 # 10-60s: poll every 2s
elif elapsed < 120000:
interval = 3000 # 60-120s: poll every 3s
else:
interval = 5000 # 120s+: poll every 5s
# Timeout reached - check if stuck
if is_stuck(sessionId):
recover(sessionId)
⚠️ Don't poll constantly - wastes tokens!
Only do health checks when:
DO NOT health check when:
# Smart health check - only when needed
if time_since_last_output > 60000: # 60s
response = process.poll(sessionId, timeout: 5000)
if response contains new data:
last_output_time = now()
continue # Not stuck, just slow
if response is "Process still running" with no data:
# Still alive, just thinking - wait more
continue
if response is "No active session":
# Process died - recover
recover(sessionId)
When done, always clean up:
process.kill(sessionId)
exec(command: "rm -f ~/.openclaw/agents/*/sessions/*.lock")
| Error | Detection | Solution |
|---|---|---|
| Process died | process.poll returns "No active session" | Restart OpenCode, resume session |
| Stuck (no response) | No response after 2x timeout | Cancel, kill, clean locks, restart |
| Lock file exists | .lock file from previous run | Remove stale locks (>30min old) |
| JSON parse error | Malformed response | Skip line, continue polling |
| Timeout | elapsed >= maxWait | Check if stuck, retry or escalate |
| Rate limited | HTTP 429 from OpenCode | Exponential backoff, max 10s |
function recover(sessionId, opencodeSessionId):
# Step 1: Try to cancel gracefully
process.write(sessionId, cancel_json + "\n")
sleep(2000)
# Step 2: Check if recovered
response = process.poll(sessionId, timeout: 5000)
if response is valid:
return # Recovered!
# Step 3: Force kill
process.kill(sessionId)
# Step 4: Clean up
exec("rm -f ~/.openclaw/agents/*/sessions/*.lock")
# Step 5: Restart
newSessionId = startOpenCode()
initialize(newSessionId)
# Step 6: Resume if we had a session
if opencodeSessionId:
session_load(newSessionId, opencodeSessionId)
return newSessionId
OpenCode supports multiple concurrent sessions. Track them:
{
"sessions": {
"process_42": {
"processSessionId": "bg_42",
"opencodeSessionId": "sess_abc",
"project": "/path/to/project1",
"lastActivity": "2026-03-05T10:00:00Z",
"status": "active"
},
"process_43": {
"processSessionId": "bg_43",
"opencodeSessionId": "sess_def",
"project": "/path/to/project2",
"lastActivity": "2026-03-05T09:30:00Z",
"status": "idle"
}
}
}
If OpenCode crashes mid-task:
1. Find the last opencodeSessionId
2. Start new OpenCode process
3. Initialize
4. session/load with the old sessionId
5. Continue from where it left off
Track these to detect problems early:
| Metric | Healthy | Warning | Critical |
|---|---|---|---|
| Response time | <5s | 5-15s | >15s |
| Polls without data | <10 | 10-30 | >30 |
| Lock file age | <5min | 5-30min | >30min |
| Consecutive errors | 0 | 1-2 | ≥3 |
Log important events for debugging:
[2026-03-05 10:00:00] Started OpenCode, sessionId=bg_42
[2026-03-05 10:00:02] Initialized successfully
[2026-03-05 10:00:03] Created session sess_abc
[2026-03-05 10:00:05] Sent prompt: "Refactor auth module"
[2026-03-05 10:00:15] Received update (thinking)
[2026-03-05 10:00:45] Received update (tool use)
[2026-03-05 10:01:30] Completed, stopReason=end_turn
# State tracking
state = {
processSessionId: null,
opencodeSessionId: null,
messageId: 0,
retries: 0,
lastActivity: null
}
# Start with cleanup
cleanupStaleLocks()
# Start OpenCode
state.processSessionId = exec("opencode acp --cwd /path", background: true)
# Initialize with retry
for attempt in 1..3:
process.write(state.processSessionId, initialize())
response = process.poll(state.processSessionId, timeout: 10000)
if is_valid(response):
break
if attempt == 3:
throw Error("Failed to initialize after 3 attempts")
# Create session with retry
for attempt in 1..3:
process.write(state.processSessionId, session_new())
response = process.poll(state.processSessionId, timeout: 10000)
if is_valid(response):
state.opencodeSessionId = response.result.sessionId
break
if attempt == 3:
throw Error("Failed to create session after 3 attempts")
# Send prompt with adaptive polling
process.write(state.processSessionId, prompt(state.messageId, "Your task"))
elapsed = 0
interval = 1000
maxWait = 120000
while elapsed < maxWait:
response = process.poll(state.processSessionId, timeout: interval)
state.lastActivity = now()
if contains_stop_reason(response):
return parse_response(response)
elapsed += interval
interval = get_adaptive_interval(elapsed)
# Smart health check - only if no output for >60s
if elapsed > 60000 && no_recent_output:
if is_stuck(state.processSessionId):
state = recover(state)
# Re-send prompt
process.write(state.processSessionId, prompt(state.messageId, "Your task"))
elapsed = 0
# Timeout
throw Error("Operation timed out after ${maxWait}ms")
# Remove locks older than 30 minutes
find ~/.openclaw/agents -name '*.lock' -mmin +30 -delete
# Check if process is alive but not responding
response = process.poll(sessionId, timeout: 5000)
return response == "Process still running" && no_data_for_60s
if elapsedMs < 10000: return 1000
if elapsedMs < 60000: return 2000
if elapsedMs < 120000: return 3000
return 5000
See templates.md for pre-built prompts for common tasks:
| Category | Templates |
|---|---|
| Refactoring | Extract function, convert to TS, improve readability |
| Features | Add endpoint, add component, implement feature |
| Bug fixes | Debug and fix, TypeScript errors |
| Testing | Unit tests, integration tests |
| Documentation | JSDoc/TSDoc, README updates |
| Database | Migrations, RLS policies |
| Performance | Query optimization, bundle size |
| Security | Security audit |
Usage:
1. Read templates.md
2. Find appropriate template
3. Replace placeholders with your specifics
4. Send as prompt
| Metric | How to track | Healthy | Warning | Critical |
|---|---|---|---|---|
| Avg response time | Log timestamps | <30s | 30-60s | >60s |
| Polls per request | Counter | <20 | 20-50 | >50 |
| Retry rate | Retries/requests | <5% | 5-15% | >15% |
| Stuck rate | Stucks/sessions | <1% | 1-5% | >5% |
| Success rate | Completed/started | >95% | 85-95% | <85% |
[2026-03-05 10:00:00] INFO: Started OpenCode, sessionId=bg_42
[2026-03-05 10:00:02] INFO: Initialized successfully
[2026-03-05 10:00:03] INFO: Created session sess_abc
[2026-03-05 10:00:05] INFO: Sent prompt (refactor)
[2026-03-05 10:00:15] DEBUG: Received update (thinking)
[2026-03-05 10:00:45] DEBUG: Received update (tool use)
[2026-03-05 10:01:30] INFO: Completed, stopReason=end_turn
[2026-03-05 10:01:31] INFO: Metrics: duration=91s, polls=15, retries=0
ERROR - Failures, exceptions, stuck detectedWARN - Retries, slow responses, near-timeoutINFO - Start, complete, cleanupDEBUG - Detailed output, updates receivedopencode-acp-control-3/
├── SKILL.md # This file
├── opencode-session.sh # Helper script (executable)
├── config.default.json # Default configuration
├── templates.md # Prompt templates
├── _meta.json # Skill metadata
└── .clawhub/ # ClawHub metadata
Use opencode-session.sh for automatic workflow:
# Simple usage
~/.openclaw/workspace/skills/opencode-acp-control-3/opencode-session.sh \
--project /path/to/project \
--prompt "Add error handling to the API"
# With template
opencode-session.sh \
--project ~/myapp \
--template "Add API endpoint" \
--prompt "POST /api/users with validation"
# Complex task
opencode-session.sh \
--project ~/myapp \
--timeout complex \
--mcp '["supabase"]' \
--prompt "Create migration for users table"
| Option | Description |
|---|---|
--project PATH | Project directory (required) |
--prompt "TEXT" | Prompt to send (required if no template) |
--template NAME | Use template from templates.md |
--timeout TYPE | simple (60s) | medium (120s) | complex (300s) |
--mcp SERVERS | MCP servers as JSON array |
--verbose | Enable debug logging |
--dry-run | Show JSON-RPC without executing |
--help | Show help |
The script outputs a complete workflow with:
CYPHER should:
Version 2.2.0 - Released 2026-03-05 Changes: Added opencode-session.sh helper script
ZIP package — ready to use