Skip to content

Arch violation: onSessionExit() marks status based on wrapper lifecycle, not process liveness #111

@c-h-

Description

@c-h-

What state is duplicated

SessionTracker.onSessionExit(sessionId) immediately marks a session as stopped in daemon state when called. This is triggered by the launch wrapper's exit — NOT by checking whether the actual coding agent process is still alive.

File: src/daemon/session-tracker.ts lines ~190-198

onSessionExit(sessionId: string): SessionRecord | undefined {
  const session = this.state.getSession(sessionId);
  if (session) {
    session.status = 'stopped';
    session.stoppedAt = new Date().toISOString();
    this.state.setSession(sessionId, session);
  }
  return session;
}

Where is the ground truth?

The actual process PID. The adapters spawn detached child processes (child.unref()) that survive wrapper exit by design. The ground truth is kill(pid, 0) — is the process alive?

How does it desync?

  1. agentctl launches Claude Code via spawn() with detached: true and child.unref()
  2. The launch wrapper (agentctl's own process) gets SIGTERM'd (e.g., OpenClaw exec timeout)
  3. The daemon calls onSessionExit() for the wrapper's session
  4. Session is marked stopped in state.json
  5. The actual Claude Code process (different PID) is still running and producing work
  6. Subsequent agentctl list shows "stopped" because the daemon trusts its own state over adapter discovery

User-visible symptom

This is the exact mechanism behind #109. The wrapper dying ≠ the agent dying, but agentctl treats them as the same event.

Proposed fix

Remove onSessionExit() entirely. Session status should never be determined by daemon-side lifecycle events. Instead:

  1. The daemon should call adapter.discover() or check PID liveness when queried
  2. session.stop (explicit stop command) should kill the actual process PID, then let the next discover() cycle confirm it's dead
  3. If the daemon needs to track "this session was stopped by us" for lock cleanup, use a separate flag that doesn't override adapter truth

Related: #109, #110

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