-
Notifications
You must be signed in to change notification settings - Fork 1
Closed
Description
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?
- agentctl launches Claude Code via
spawn()withdetached: trueandchild.unref() - The launch wrapper (agentctl's own process) gets SIGTERM'd (e.g., OpenClaw exec timeout)
- The daemon calls
onSessionExit()for the wrapper's session - Session is marked
stoppedin state.json - The actual Claude Code process (different PID) is still running and producing work
- Subsequent
agentctl listshows "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:
- The daemon should call
adapter.discover()or check PID liveness when queried session.stop(explicit stop command) should kill the actual process PID, then let the next discover() cycle confirm it's dead- 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
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels