Managing Coding Agents in Threads
Managing agent sessions across different contexts is a headache. I often find myself starting a task in one place, losing the state in another, and ending up with fragmented outputs or duplicate runs. It is frustrating when you cannot keep a conversation locked to a specific thread where the context lives.
I want to show you how we handle ACP (Agent Control Plane) coding agents in thread-capable channels. This approach ensures that your agent stays bound to a specific thread, making the lifecycle and recovery much easier to manage.
What You’ll Need
Section titled “What You’ll Need”- OpenClaw core (handles the control plane and routing)
- Discord channel (for the thread-capable interface)
acpx-backed plugin service (the initial runtime backend)- SQLite store (for state persistence)
Quick Start
Section titled “Quick Start”I recommend following these steps to get an ACP session running in a thread. The goal is to move from a generic session to a bound thread persona in under five minutes.
- Spawn or Focus: Use the
/focuscommand to target an ACP session into a specific thread. - Route Messages: Once bound, any message you send in that thread routes directly to the bound ACP session.
- Stream Output: The agent output streams back to the same thread persona, keeping the history in one place.
- Manage Lifecycle: Use explicit cleanup controls or the
/unfocuscommand when you finish the session.
I prefer this hybrid architecture because OpenClaw core owns the session identity and routing decisions, while the pluggable runtime handles the heavy lifting of transport and queueing.
How it Works
Section titled “How it Works”I believe the best way to maintain reliability is through explicit states. Every ACP session in this system follows a specific lifecycle:
creatingidlerunningcancellingclosederror
By using a SQLite store in WAL mode, the system keeps a record of every event. If the system crashes, we can use these append-only logs for replay and recovery. This ensures that delivery remains deterministic and you do not lose your work.
Troubleshooting
Section titled “Troubleshooting”If you run into issues, check these common scenarios based on the current implementation:
- Metadata Gaps: If messages are not routing correctly, I recommend checking the core ACP branch. We fixed metadata-gap bugs by ensuring all ACP turns go through a single core routing path.
- Duplicate Outputs: If you see double messages in Discord, ensure your idempotency keys are active. We use these keys and fail-closed routing checks to prevent duplicate runs.
- Routing Failures: Remember that inbound routing from a thread binding must resolve to a session key in the core dispatch first. Plugin hooks alone cannot replace this core dispatch flow.
If you need help with your specific configuration, check the AI Setup Assistant.
What’s Next
Section titled “What’s Next”I’ve spent a lot of time dealing with the headache of managing long-running processes in bot architectures. It is frustrating when a simple server restart wipes out active sessions or when you end up with duplicate messages because a process didn’t know it was already running. I wanted to walk you through how we handle this in OpenClaw using the Agent Control Plane (ACP).
What You’ll Need
Section titled “What You’ll Need”- OpenClaw core
acpxplugin backend- SQLite (with WAL mode enabled)
Quick Start
Section titled “Quick Start”To get moving with ACP, you need to understand the boundary between the core and the plugins. The core handles the heavy lifting of session-mode dispatch and delivery arbitration, while the acpx plugin handles the actual process execution.
The Runtime Contract
Section titled “The Runtime Contract”We use a core ACP runtime contract so that our routing code doesn’t care about CLI details. This allows us to switch backends without touching the dispatch logic. Here is the interface I use:
export type AcpRuntimePromptMode = "prompt" | "steer";
export type AcpRuntimeHandle = { sessionKey: string; backend: string; runtimeSessionName: string;};
export type AcpRuntimeEvent = | { type: "text_delta"; stream: "output" | "thought"; text: string } | { type: "tool_call"; name: string; argumentsText: string } | { type: "done"; usage?: Record<string, number> } | { type: "error"; code: string; message: string; retryable?: boolean };
export interface AcpRuntime { ensureSession(input: { sessionKey: string; agent: string; mode: "persistent" | "oneshot"; cwd?: string; env?: Record<string, string>; idempotencyKey: string; }): Promise<AcpRuntimeHandle>;
submit(input: { handle: AcpRuntimeHandle; text: string; mode: AcpRuntimePromptMode; idempotencyKey: string; }): Promise<{ runtimeRunId: string }>;
stream(input: { handle: AcpRuntimeHandle; runtimeRunId: string; onEvent: (event: AcpRuntimeEvent) => Promise<void> | void; signal?: AbortSignal; }): Promise<void>;
cancel(input: { handle: AcpRuntimeHandle; runtimeRunId?: string; reason?: string; idempotencyKey: string; }): Promise<void>;
close(input: { handle: AcpRuntimeHandle; reason: string; idempotencyKey: string }): Promise<void>;
health?(): Promise<{ ok: boolean; details?: string }>;}Data Model and Persistence
Section titled “Data Model and Persistence”I rely on a dedicated ACP SQLite database in WAL mode as the source of truth. This makes the system crash-safe. If the gateway dies, it rehydrates from this DB and resumes from checkpoints.
The core metadata looks like this:
export type AcpSessionMeta = { backend: string; agent: string; runtimeSessionName: string; mode: "persistent" | "oneshot"; cwd?: string; state: "idle" | "running" | "error"; lastActivityAt: number; lastError?: string;};The database tracks sessions, runs, bindings, events, and delivery checkpoints. I make sure process IDs and sockets stay in memory only, while the durable lifecycle lives in the DB.
Routing and State Machines
Section titled “Routing and State Machines”When a message comes in, I look up the thread binding. If it’s an ACP session, it goes to the ACP runtime branch. If you use /acp steer, it triggers the steer mode.
Session State Machine
Section titled “Session State Machine”creating -> idle -> running -> idlerunning -> cancelling -> idle | erroridle -> closederror -> idle | closed
Run State Machine
Section titled “Run State Machine”queued -> running -> completedrunning -> failed | cancelledqueued -> cancelled
I use an actor model via AcpSessionManager. Each session key gets one actor that serializes all side effects like submit or cancel. This prevents out-of-order output and race conditions.
Idempotency and Delivery
Section titled “Idempotency and Delivery”Every external action requires an idempotency key. This covers spawning, prompting, cancelling, and closing. Discord messages are derived from the acp_events table and the acp_delivery_checkpoint. If a connection drops, retries resume from the last checkpoint so I don’t send duplicate chunks.
Troubleshooting
Section titled “Troubleshooting”If things go wrong, here are the standard behaviors and solutions:
- Stale Bindings: If a binding exists but the ACP session is missing, the system fails closed. You will see an explicit stale-binding message. I usually validate these before auto-unbinding.
- Gateway Restarts: On start, the system loads all non-terminal sessions. It reconciles runs that are marked as
runningbut are missing heartbeats, marking them asfailedor recovering them. - Runtime Crashes: I use bounded restart backoffs to prevent infinite crash loops.
Lifecycle and Safety
Section titled “Lifecycle and Safety”You have a few ways to manage active sessions:
- Cancel a run with
/acp cancel. - Unfocus a thread with
/unfocus. - Close a session with
/acp close. - Let the auto-close TTL handle idle sessions.
I recommend using the allowlist for ACP agents and restricting workspace roots to keep your environment safe.
For more help, check out the AI Setup Assistant.
What’s Next
Section titled “What’s Next”- ACP Plugin Development
- SQLite Configuration Guide
- Discord Thread Bindings
- Idempotency Implementation
I have spent many late nights staring at configuration files, trying to figure out which toggle actually fixes the issue. It is often the case that you just want a clear map of the available knobs and dials so you can get back to building.
Configuring the ACP surface does not have to be a guessing game. I prefer to keep things simple by knowing exactly which keys control the routing, the backend, and the recovery logic.
What You’ll Need
Section titled “What You’ll Need”- Access to your main configuration file.
- A list of the agent names you plan to use.
Quick Start
Section titled “Quick Start”If you want to get up and running in a few minutes, follow these four steps:
- Set
acp.enabledtotrueto turn on the system. - Specify your
acp.backend. I recommend using the defaultacpx. - Add your authorized agents to the
acp.allowedAgents[]list. - Define a
acp.defaultAgentto handle incoming sessions.
Core Configuration Keys
Section titled “Core Configuration Keys”These are the primary keys you will use to manage the ACP lifecycle and behavior.
System Toggles
Section titled “System Toggles”acp.enabled: The main switch for the ACP system.acp.dispatch.enabled: This is an independent routing kill switch. Use this if you need to stop traffic without shutting down the entire ACP.
Backend and Agents
Section titled “Backend and Agents”acp.backend: The backend type. This defaults toacpx.acp.defaultAgent: The agent used when no specific agent is requested.acp.allowedAgents[]: An array of agents permitted to run in this environment.
Session Management
Section titled “Session Management”acp.maxConcurrentSessions: Limits how many sessions can run at once.acp.stream.coalesceIdleMs: Time to wait for idle streams to group data.acp.stream.maxChunkChars: The maximum character count for a single stream chunk.acp.runtime.ttlMinutes: How long the runtime stays active before timing out.acp.idempotency.ttlHours: How long idempotency keys are stored.
Control Plane and Recovery
Section titled “Control Plane and Recovery”acp.controlPlane.store: The storage backend, which defaults tosqlite.acp.controlPlane.storePath: The file path for your storage database.acp.controlPlane.recovery.eagerActors: Controls how aggressively the system recovers actors.acp.controlPlane.recovery.reconcileRunningAfterMs: The delay before reconciling running processes during recovery.acp.controlPlane.checkpoint.flushEveryEvents: Flush checkpoints based on event count.acp.controlPlane.checkpoint.flushEveryMs: Flush checkpoints based on a time interval.
Integrations
Section titled “Integrations”channels.discord.threadBindings.spawnAcpSessions: Specific setting for spawning sessions within Discord threads.
Plugin and Backend Keys (acpx)
Section titled “Plugin and Backend Keys (acpx)”When you use the acpx plugin, you have access to specific backend overrides. These allow you to fine-tune the execution environment:
- Command/Path Overrides: Change the default backend binary or script paths.
- Env Allowlist: Define which environment variables are passed to the backend.
- Per-agent Presets: Create specific configurations for different agents.
- Startup/Stop Timeouts: Set how long the system waits for a backend to start or exit.
- Max Inflight Runs per Session: Limit concurrent executions within a single session.
Troubleshooting
Section titled “Troubleshooting”If you run into trouble, check these two areas first:
- Routing Issues: If sessions aren’t being directed correctly, check
acp.dispatch.enabled. If this is false, all routing stops even if the ACP is enabled. - Persistence Failures: If your state isn’t saving, verify the
acp.controlPlane.storePath. Since the defaultacp.controlPlane.storeissqlite, the process needs write access to that path.
If you need more help with your specific config, ask the AI Setup Assistant.
What’s Next
Section titled “What’s Next”We’ve all been there: you’re trying to manage long-running agent sessions, but the state is scattered across three different databases, and if a process restarts, everything just vanishes. It’s frustrating when you can’t tell if a session is actually running or if it just died quietly in the background without leaving a log.
I’ve been working through the implementation spec for our new Agent Control Plane (ACP), and it’s designed specifically to fix that. We’re moving away from ad-hoc metadata writes and toward a centralized, durable control plane that treats agent sessions as first-class citizens with strict invariants.
What You’ll Need
Section titled “What You’ll Need”- Access to the
corerepository (specificallysrc/acp/) - The
acpxruntime plugin installed and enabled acp.enabledset totruein your global configacp.dispatch.enabledset totrue(if you’re ready for the dispatch path)
Quick Start
Section titled “Quick Start”If you want to get a backend up and running in 5 minutes, here is the path. First, you need to register your backend with the core registry:
import { registerAcpRuntimeBackend } from 'src/acp/runtime/registry';
registerAcpRuntimeBackend({ id: 'acpx', runtime: myRuntimeInstance, healthy: () => true});Next, ensure your configuration enables the dispatch gate. In your config file, set acp.dispatch.enabled: true. This allows the AcpSessionManager to start routing commands through the new control plane instead of falling back to legacy paths.
The Architecture
Section titled “The Architecture”I’ve organized the control plane into several dedicated modules in src/acp/control-plane/ to keep concerns separated:
manager.ts: This is the brain. It owns ACP actors, lifecycle transitions, and command serialization.store.ts: Handles SQLite schema management and transactions.events.ts: Contains typed event definitions.checkpoint.ts: Manages durable delivery checkpoints so we don’t lose messages.idempotency.ts: Uses keys to prevent duplicate command execution.recovery.ts: Handles boot-time reconciliation to rehydrate actors after a crash.
We also have a compatibility bridge in src/acp/runtime/session-meta.ts. It stays for now to project data into SessionEntry.acp, but it won’t be the source of truth once we finish the migration.
Required Invariants
Section titled “Required Invariants”To keep the system stable, I’ve baked these rules directly into the code:
- ACP session creation and thread binding are atomic (one single transaction).
- Only one active run per ACP session actor is allowed at a time.
- Event
seqnumbers must strictly increase per run. - Delivery checkpoints never advance past the last committed event.
The Registry API
Section titled “The Registry API”The core registry module at src/acp/runtime/registry.ts is how plugins talk to the control plane. Here is the API you’ll be using:
export type AcpRuntimeBackend = { id: string; runtime: AcpRuntime; healthy?: () => boolean;};
export function registerAcpRuntimeBackend(backend: AcpRuntimeBackend): void;export function unregisterAcpRuntimeBackend(id: string): void;export function getAcpRuntimeBackend(id?: string): AcpRuntimeBackend | null;export function requireAcpRuntimeBackend(id?: string): AcpRuntimeBackend;If you call requireAcpRuntimeBackend and the backend isn’t there, it throws a typed error. Plugins should register on start and unregister on stop.
The acpx Runtime Contract
Section titled “The acpx Runtime Contract”For the acpx backend, we use a strict CLI command contract. The runtime handle is encoded as runtimeSessionName = acpx:v1:<base64url(json)>.
When the manager needs to talk to the backend, it maps commands like this:
- Ensure Session:
acpx --format json --json-strict --cwd <cwd> <agent> sessions ensure --name <name> - Prompt Turn:
acpx --format json --json-strict --cwd <cwd> <agent> prompt --session <name> --file - - Cancel:
acpx --format json --json-strict --cwd <cwd> <agent> cancel --session <name> - Close:
acpx --format json --json-strict --cwd <cwd> <agent> sessions close <name>
We consume ndjson events from the stdout. For example, thought maps to text_delta/thought, and done signals the end of the stream.
Session Schema & Migration
Section titled “Session Schema & Migration”I’ve patched SessionEntry in src/config/sessions/types.ts to include the new metadata:
type SessionAcpMeta = { backend: string; agent: string; runtimeSessionName: string; mode: "persistent" | "oneshot"; cwd?: string; state: "idle" | "running" | "error"; lastActivityAt: number; lastError?: string;};We are rolling this out in four phases. We start with dual-writing to both the legacy acp projection and the new SQLite source-of-truth. Eventually, we’ll move to reading primarily from SQLite and backfilling missing rows from legacy entries.
Delivery & Duplicate Arbitration
Section titled “Delivery & Duplicate Arbitration”We have a strict rule for routing: if an active thread binding exists for an ACP session, we deliver only to that thread. We don’t double-post to the parent channel. If the destination is ambiguous, the system fails closed with an error rather than guessing where to send the message.
Troubleshooting
Section titled “Troubleshooting”If things go wrong, look for these specific error codes in your logs:
ACP_BACKEND_MISSING: The runtime backend isn’t configured. You probably need to enable theacpxplugin.ACP_BACKEND_UNAVAILABLE: The backend is registered but currently failing health checks.ACP_SESSION_INIT_FAILED: We couldn’t initialize the session runtime.ACP_TURN_FAILED: The turn failed before it could complete.
You can also run /acp doctor to validate your store health and backend registration, or /acp sessions to see the state of active runs and errors.
Rollout Guard
Section titled “Rollout Guard”I’ve added a kill switch: acp.dispatch.enabled. It defaults to false for the first release. While it’s off, you can still bind sessions using focus commands, but the main dispatch path won’t activate. This lets us validate the logic before we let it handle all traffic.
Need help getting your backend registered? Check out the AI Setup Assistant.
What’s Next
Section titled “What’s Next”- ACP Event Specification
- Runtime Plugin SDK Guide
- Discord Thread Binding Logic
- Observability Metrics Reference
I have spent too much time chasing rogue agent processes or wondering why a session didn’t resume after a system restart. It is a common headache when building complex agentic systems: you want the agent to stay focused, but the state management usually feels like a house of cards.
I want to walk you through our new Command and UX plan for the Agent Control Plane (ACP). We are moving toward a more predictable, transactional way to handle agent sessions so you can stop worrying about state drift and start building.
What You’ll Need
Section titled “What You’ll Need”- An active Discord integration (our first supported channel for event-driven projection).
- The
acpxbackend adapter configured in your runtime. - Access to the ACP SQLite store for session persistence.
Quick Start
Section titled “Quick Start”You can get an agent up and running in under five minutes using the new command suite. Here is the path to a minimal setup:
- Spawn the agent: Use the spawn command to start a session.
Terminal window /acp spawn <agent-id> [--mode persistent|oneshot] [--thread auto|here|off] - Interact and Steer: If the agent needs a nudge or a change in direction, use steer.
Terminal window /acp steer <instruction> - Monitor Sessions: Check what is currently active.
Terminal window /acp sessions - Clean Up: Close the session when you are finished.
Terminal window /acp close [session]
If you prefer the existing workflow, /focus <sessionKey> still supports ACP targets, and /unfocus keeps its current behavior. We have also replaced the old TTL overrides with /session idle and /session max-age.
The Phased Rollout
Section titled “The Phased Rollout”I am following a strict six-phase plan to ensure the system is stable before we flip the switch on production traffic.
Phase 0 & 1: Foundation
Section titled “Phase 0 & 1: Foundation”We started by freezing the DB schema. This includes acp_sessions, acp_runs, and acp_bindings. I implemented the AcpSessionManager and a per-session actor runtime to handle the core logic. Everything here is backed by SQLite with transactional guarantees.
Phase 2 & 3: Routing and Adapters
Section titled “Phase 2 & 3: Routing and Adapters”We route thread-bound ACP turns through a dispatch pipeline. If a binding or session invariant fails, we enforce fail-closed routing. The acpx adapter handles the runtime contract:
ensureSessionsubmitstreamcancelclose
Phase 4 & 5: UX and Migration
Section titled “Phase 4 & 5: UX and Migration”We are implementing event-driven channel projection (starting with Discord). This ensures we can resume from checkpoints and provide exactly-once delivery for final messages. We use a dual-write approach during migration, writing to both the legacy projection and the new ACP SQLite source-of-truth.
Phase 6: Hardening
Section titled “Phase 6: Hardening”The final step involves enforcing concurrency limits and scale budgets. I am adding full telemetry and crash recovery testing to make sure the system handles backend outages gracefully.
Internal Refactors
Section titled “Internal Refactors”To keep the codebase maintainable, I have already completed several targeted refactors:
- Policy Centralization: Shared helpers now live in
src/acp/policy.ts. - Command Splitting:
src/auto-reply/reply/commands-acp.tsis now a thin router, with logic split intolifecycle.ts,runtime-options.ts, anddiagnostics.ts. - Manager Responsibility: The session manager is split into
manager.core.ts,manager.types.ts, andmanager.utils.ts.
Troubleshooting
Section titled “Troubleshooting”Here are some common issues you might run into and how we handle them in the ACP:
- Duplicate Deliveries: We use a single destination resolver and an idempotent event checkpoint to prevent double-posting.
- Missing Backend: If a plugin is misconfigured, the system provides an explicit operator-facing error and fails closed rather than falling back to a normal session.
- Stale Bindings: The system includes a detector for stale bindings. You will see an explicit error, and you can enable an optional auto-clean policy.
- Process Churn: To handle load, we use long-lived per-session owners combined with concurrency caps and backoff policies.
If you hit a wall, you can always use AI Setup Assistant for real-time help.
What’s Next
Section titled “What’s Next”OpenClaw Expert
Still stuck?
If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.