Skip to content

Oneshot sessions not auto-closed after agent exit — causes zombie accumulation #47

@allanjeng

Description

@allanjeng

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 false

The 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 against maxConcurrentSessions: 8
  • All new ACP spawns fail with "max concurrent sessions reached (8/8)" despite no actual acpx processes running
  • Manual cleanup required: set closed: true on 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
done

Environment

  • acpx: 0.1.15
  • openclaw: 2026.3.2
  • OS: macOS arm64
  • Agents: codex, claude

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions