-
-
Notifications
You must be signed in to change notification settings - Fork 32
Description
Problem
When acpx sessions are created in oneshot mode (via OpenClaw ACP mode: "run"), the session record's closed flag is never automatically set to true after the agent process exits. This causes zombie sessions to accumulate in ~/.acpx/sessions/, eventually hitting the maxConcurrentSessions limit and blocking all new ACP spawns.
Root Cause
In withConnectedSession (bundled CLI source ~line 4925):
// After successful run:
record.closed = false; // explicitly NOT closing
// ...
// In finally block:
await client.close();
applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
await writeSessionRecord(record).catch(() => {}); // writes exit info but closed stays falseThe session record correctly records last_agent_exit_at, last_agent_exit_code, and last_agent_disconnect_reason, but the closed flag remains false. The closed flag is only set when acpx <agent> sessions close <name> is explicitly called.
Impact
- 87 stale session files accumulated in
~/.acpx/sessions/over 2 days - 5 zombie sessions (agent exited but
closed: false) counted againstmaxConcurrentSessions: 8 - All new ACP spawns fail with "max concurrent sessions reached (8/8)" despite no actual acpx processes running
- Manual cleanup required: set
closed: trueon zombie records + gateway restart
Suggested Fix
In the finally block of withConnectedSession, when the session mode is oneshot:
// In finally block, after applyLifecycleSnapshotToRecord:
if (record.mode === 'oneshot' || options.mode === 'oneshot') {
record.closed = true;
record.closedAt = isoNow();
}
await writeSessionRecord(record).catch(() => {});This preserves the current reusable-session behavior for persistent sessions while ensuring oneshot sessions are always cleaned up.
Workaround
Periodic cleanup script that closes sessions with last_agent_exit_at set but closed: false (with a 5-minute grace period):
cd ~/.acpx/sessions && for f in *.json; do
node -e "
const fs=require('fs'), d=JSON.parse(fs.readFileSync('$f','utf8'));
if(d.closed!==true && d.last_agent_exit_at) {
const age = Date.now() - new Date(d.last_agent_exit_at).getTime();
if(age > 300000) { d.closed=true; d.closed_at=new Date().toISOString(); fs.writeFileSync('$f',JSON.stringify(d,null,2)); }
}" 2>/dev/null
doneEnvironment
- acpx: 0.1.15
- openclaw: 2026.3.2
- OS: macOS arm64
- Agents: codex, claude