fix: don't discard codex/gemini/antigravity conversations on Resume; fix DELETE ownership dup + missing broadcast

resumeHistorySession() creates the resumed row in its own mode via a
modeConfigKey map (opencode/pi/grok/omp -> continueSession, deepseek ->
resumeSession) and retires the old row afterward. codex, gemini and
antigravity were missing from that map, so resuming one of their rows
started a brand-new session with NO continuation while still deleting
the row it came from -- silent data loss dressed as the duplicate-row
fix. Gate row retirement on continuesSomething (true only for modes that
actually got a continuation config) instead of wiring an unverified
sessionId->native-conversation-id assumption for the three affected CLIs.

DELETE /api/sessions/:id reimplemented the ownership 404 check inline in
two places instead of going through findSessionOrFail, and its
persisted-only-session branch never broadcast session:deleted, so other
open tabs kept the retired row until their next unrelated fetch. Extract
the shared 404 into sessionNotFoundError(), add findPersistedSessionOrFail()
alongside findSessionOrFail() in route-helpers.ts (same ownership
contract, returns a SessionState instead of a live Session), and use both
from the route instead of inline checks. Add the missing broadcast.
This commit is contained in:
timkjr
2026-08-28 14:03:07 -05:00
parent 2ee2eacb4b
commit f18dccace1
5 changed files with 205 additions and 23 deletions
+10 -15
View File
@@ -67,6 +67,7 @@ import {
autoConfigureRalph,
canAccessOwned,
CASES_DIR,
findPersistedSessionOrFail,
findSessionOrFail,
getAuthUser,
isAdmin,
@@ -1191,25 +1192,19 @@ export function registerSessionRoutes(
// rather than 404ing: the caller means "make this row go away", and a
// stale duplicate row is exactly what's left behind otherwise. Pinned
// sessions keep their existing demote-not-delete protection.
const session = ctx.sessions.get(id);
if (!session) {
const persisted = ctx.store.getSession(id);
if (!persisted || !canAccessOwned(getAuthUser(req), persisted.owner)) {
throw Object.assign(new Error(`Session ${id} not found`), {
statusCode: 404,
body: createErrorResponse(ApiErrorCode.NOT_FOUND, `Session ${id} not found`),
});
}
if (!ctx.sessions.has(id)) {
// Called for its existence/ownership 404 side effect only — demoteOrRemoveSession
// below re-looks-up the record by id, so the returned SessionState is unused here.
findPersistedSessionOrFail(ctx.store, id, req);
ctx.store.demoteOrRemoveSession(id);
// Mirrors the broadcast at the tail of the live-session cleanup path
// (_doCleanupSession in server.ts) — without it, other open tabs keep
// showing the retired row until their next unrelated fetch.
ctx.broadcast(SseEvent.SessionDeleted, { id });
return {};
}
if (req && !canAccessOwned(getAuthUser(req), session.owner)) {
throw Object.assign(new Error(`Session ${id} not found`), {
statusCode: 404,
body: createErrorResponse(ApiErrorCode.NOT_FOUND, `Session ${id} not found`),
});
}
const session = findSessionOrFail(ctx, id, req);
await ctx.cleanupSession(session.id, killMux, 'user_delete');
return {};
});