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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions