fix(sessions): correct what the missing-pid rule actually recognises

A second real reboot disproved the mechanism the previous commit was built
on. Typing `/exit` does not persist `pid: null`, and the session was
restored anyway.

The pid a session record carries is its `tmux attach-session` process, not
the agent. `/exit` ends the CLI inside the pane, `remain-on-exit` keeps the
pane, and the attach process stays alive throughout — so Codeman's PTY never
exits, no exit handler runs, and the record keeps both its pid and
`status: 'idle'`. The lifecycle log for the session that came back shows
created, started, stale_cleaned and recovered, with no exit event at all,
which is the proof: Codeman never learned the agent was gone.

So nothing durable distinguishes an exited agent from a session that was
idle when the power went, and this pass restores both. Ark0N/Codeman#446 is
about making Codeman notice the dead pane; contrary to what the previous
commit's message claimed, this genuinely does wait on that. Until a record
can say the agent is gone, the user dismisses or closes those sessions.

The rule itself is kept, because a record with no attach process does
describe a session that never started or whose pane died outright, and
refusing it is right. Only its documentation was wrong. The module header,
the branch comment and the test names now say what it recognises instead of
claiming the case it cannot see.

Refs #411

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Grundberg
2026-09-17 19:49:06 +02:00
co-authored by Claude Opus 5
parent 5108a24bf0
commit 62ceb4e87b
2 changed files with 30 additions and 21 deletions
+6 -5
View File
@@ -138,17 +138,18 @@ describe('which dead sessions may be rebuilt', () => {
});
});
describe('a session whose agent had already exited', () => {
it('is refused, because `/exit` leaves the record reading idle with no pid', () => {
// What the process-exit handler persists: the CLI is gone, the record is not,
// and its status is indistinguishable from a session that was merely idle.
describe('a session with no attach process in its record', () => {
it('is refused, because there was nothing running to bring back', () => {
// A session that never started, or whose pane died outright. NOT a session
// the user ended with `/exit`: that keeps its pid, because the pid is the
// tmux attach process and `remain-on-exit` keeps the pane alive.
const persisted = { exited: persistedSession({ id: 'exited', status: 'idle', pid: null }) };
const plan = planRebootRestore(['exited'], persisted, () => true);
expect(plan.restore).toEqual([]);
expect(plan.skipped).toEqual([{ sessionId: 'exited', reason: 'not-running' }]);
});
it('still restores the session beside it that was running when the power went', () => {
it('still restores the session beside it that was attached when the power went', () => {
const persisted = {
exited: persistedSession({ id: 'exited', pid: null }),
running: persistedSession({ id: 'running', pid: 4242 }),