COD-142 retain pin when a pinned session is killed

A killed session was full-deleted from state.json (removeSession),
dropping the COD-139 pinned/pinnedAt fields, so the session vanished
from the session-manager pinned group. cleanupStaleSessions also reaped
any persisted record with no live session on boot, which would have
wiped a preserved pin on the next restart.

Fix (state-store):
- demoteOrRemoveSession(id): on kill, demote a *pinned* record to a
  lightweight stopped record (status=stopped, pid=null, pin retained)
  instead of deleting; unpinned records are removed as before.
- cleanupStaleSessions skips pinned records so the pin survives restart.
- server _doCleanupSession calls demoteOrRemoveSession on the killMux
  path (shutdown path unchanged).

Restoration iterates live mux sessions, not state.json, so a stopped+
pinned record is never auto-revived. Unit-tested on the real StateStore
path (state-store.test.ts +4); session-cleanup/session-pin regress green.

(cherry picked from commit 86f183eacfc3f2f6ac28499fb1ae2d21eef2bbed)
This commit is contained in:
Aamer Akhter
2026-07-17 16:26:36 -04:00
parent 7a86cf87f7
commit 35a0217ccd
3 changed files with 81 additions and 1 deletions
+60
View File
@@ -141,6 +141,66 @@ describe('StateStore', () => {
});
});
describe('demoteOrRemoveSession and pinned cleanup (COD-142)', () => {
it('should preserve a pinned session as a stopped record on kill', () => {
const store = new StateStore(testFilePath);
const pinnedAt = Date.now();
store.setSession('pinned-1', {
...createMockSessionState('pinned-1'),
name: 'My Pinned Session',
workingDir: '/tmp/pinned',
pinned: true,
pinnedAt,
});
const result = store.demoteOrRemoveSession('pinned-1');
expect(result).toBe('preserved');
const preserved = store.getSession('pinned-1');
expect(preserved).not.toBeNull();
expect(preserved?.status).toBe('stopped');
expect(preserved?.pid).toBeNull();
expect(preserved?.pinned).toBe(true);
expect(preserved?.pinnedAt).toBe(pinnedAt);
expect(preserved?.name).toBe('My Pinned Session');
expect(preserved?.workingDir).toBe('/tmp/pinned');
});
it('should fully remove an unpinned session on kill', () => {
const store = new StateStore(testFilePath);
store.setSession('plain-1', createMockSessionState('plain-1'));
const result = store.demoteOrRemoveSession('plain-1');
expect(result).toBe('removed');
expect(store.getSession('plain-1')).toBeNull();
});
it('should report absent for an unknown session id', () => {
const store = new StateStore(testFilePath);
expect(store.demoteOrRemoveSession('does-not-exist')).toBe('absent');
});
it('should keep pinned records during cleanupStaleSessions but reap unpinned ones', () => {
const store = new StateStore(testFilePath);
store.setSession('pinned-1', {
...createMockSessionState('pinned-1'),
pinned: true,
pinnedAt: Date.now(),
});
store.setSession('plain-1', createMockSessionState('plain-1'));
const result = store.cleanupStaleSessions(new Set<string>());
expect(result.count).toBe(1);
expect(result.cleaned.map((c) => c.id)).toEqual(['plain-1']);
expect(result.cleaned.some((c) => c.id === 'pinned-1')).toBe(false);
expect(store.getSession('pinned-1')).not.toBeNull();
expect(store.getSession('plain-1')).toBeNull();
});
});
describe('task operations', () => {
it('should set and get tasks', () => {
const store = new StateStore(testFilePath);