Skip to content

Commit f0ead9b

Browse files
heavygeecursoragent
andcommitted
fix(hub): restore inactive-session guard on /archive except split-brain
Addresses post-rebase bot review Major on tiann#923: dropping requireActive entirely let normal inactive non-archived rows (completed stubs, UI Delete/Reopen targets) fall through to archiveSession, which could stamp archivedBy=hub on sessions that were never active. Restore the 409 for inactive rows unless metadata.lifecycleState is still 'running' (hub-restart split-brain cleanup case from tiann#916). Two route tests cover the guard and the exception. Refs tiann#916. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 1495971 commit f0ead9b

2 files changed

Lines changed: 48 additions & 6 deletions

File tree

hub/src/web/routes/sessions.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,42 @@ describe('sessions routes', () => {
10991099
expect(response.status).toBe(404)
11001100
expect(await response.json()).toEqual({ error: 'Session not found' })
11011101
})
1102+
1103+
it('returns 409 for an inactive non-archived row whose lifecycle is not running', async () => {
1104+
let called = false
1105+
const session = createSession({ active: false })
1106+
const { app } = createApp(session, {
1107+
archiveSession: async () => { called = true }
1108+
})
1109+
1110+
const response = await app.request('/api/sessions/session-1/archive', { method: 'POST' })
1111+
1112+
expect(response.status).toBe(409)
1113+
expect(await response.json()).toEqual({ error: 'Session is inactive' })
1114+
expect(called).toBe(false)
1115+
})
1116+
1117+
it('returns 2xx for an inactive split-brain row still marked lifecycleState=running', async () => {
1118+
const calls: string[] = []
1119+
const session = createSession({
1120+
active: false,
1121+
metadata: {
1122+
path: '/tmp/project',
1123+
host: 'localhost',
1124+
flavor: 'codex',
1125+
lifecycleState: 'running'
1126+
}
1127+
})
1128+
const { app } = createApp(session, {
1129+
archiveSession: async (sessionId: string) => { calls.push(sessionId) }
1130+
})
1131+
1132+
const response = await app.request('/api/sessions/session-1/archive', { method: 'POST' })
1133+
1134+
expect(response.status).toBe(200)
1135+
expect(await response.json()).toEqual({ ok: true })
1136+
expect(calls).toEqual(['session-1'])
1137+
})
11021138
})
11031139

11041140
})

hub/src/web/routes/sessions.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -291,11 +291,12 @@ export function createSessionsRoutes(getSyncEngine: () => SyncEngine | null): Ho
291291
})
292292

293293
app.post('/sessions/:id/archive', async (c) => {
294-
// tiann/hapi#916: drop `requireActive: true` so the endpoint is
295-
// idempotent for already-archived rows AND for live-in-cache /
296-
// dead-on-cli split-brain rows that the operator wants to clean up
297-
// after a hub-restart cascade. We still 404 on unknown ids via
298-
// requireSessionFromParam, and 5xx on real RPC failures.
294+
// tiann/hapi#916: relax the blanket `requireActive: true` guard so
295+
// the endpoint is idempotent for already-archived rows AND can clean
296+
// up split-brain rows after a hub-restart cascade (inactive in cache
297+
// but metadata.lifecycleState still 'running'). Normal inactive rows
298+
// that are not archived (completed stubs, UI Delete/Reopen targets)
299+
// keep the old 409 contract.
299300
const engine = requireSyncEngine(c, getSyncEngine)
300301
if (engine instanceof Response) {
301302
return engine
@@ -306,10 +307,15 @@ export function createSessionsRoutes(getSyncEngine: () => SyncEngine | null): Ho
306307
return sessionResult
307308
}
308309

309-
if (sessionResult.session.metadata?.lifecycleState === 'archived') {
310+
const lifecycleState = sessionResult.session.metadata?.lifecycleState
311+
if (lifecycleState === 'archived') {
310312
return c.json({ ok: true, alreadyArchived: true })
311313
}
312314

315+
if (!sessionResult.session.active && lifecycleState !== 'running') {
316+
return c.json({ error: 'Session is inactive' }, 409)
317+
}
318+
313319
await engine.archiveSession(sessionResult.sessionId)
314320
return c.json({ ok: true })
315321
})

0 commit comments

Comments
 (0)