mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 16:09:43 +02:00
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:
@@ -12,7 +12,7 @@ import { homedir } from 'node:os';
|
||||
import type { z } from 'zod';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { Session } from '../session.js';
|
||||
import { ApiErrorCode, createErrorResponse, type AuthUser } from '../types.js';
|
||||
import { ApiErrorCode, createErrorResponse, type AuthUser, type SessionState } from '../types.js';
|
||||
import { MAX_CONCURRENT_SESSIONS } from '../config/map-limits.js';
|
||||
import { parseRalphLoopConfig, extractCompletionPhrase } from '../ralph-config.js';
|
||||
import { SseEvent } from './sse-events.js';
|
||||
@@ -264,6 +264,18 @@ export function revokeUserSessions(
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* The 404 both session-lookup helpers below throw. A missing session and one
|
||||
* the caller isn't allowed to see get the IDENTICAL error (never 403), so
|
||||
* existence of another user's session is never leaked.
|
||||
*/
|
||||
function sessionNotFoundError(sessionId: string): Error & { statusCode: number; body: unknown } {
|
||||
return Object.assign(new Error(`Session ${sessionId} not found`), {
|
||||
statusCode: 404,
|
||||
body: createErrorResponse(ApiErrorCode.NOT_FOUND, `Session ${sessionId} not found`),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a session by ID or throw a structured error.
|
||||
* Replaces the pattern: `const session = sessions.get(id); if (!session) return createErrorResponse(...)`.
|
||||
@@ -274,15 +286,31 @@ export function revokeUserSessions(
|
||||
*/
|
||||
export function findSessionOrFail(ctx: SessionPort, sessionId: string, req?: FastifyRequest): Session {
|
||||
const session = ctx.sessions.get(sessionId);
|
||||
if (!session || (req && !canAccessOwned(getAuthUser(req), session.owner))) {
|
||||
throw Object.assign(new Error(`Session ${sessionId} not found`), {
|
||||
statusCode: 404,
|
||||
body: createErrorResponse(ApiErrorCode.NOT_FOUND, `Session ${sessionId} not found`),
|
||||
});
|
||||
}
|
||||
if (!session) throw sessionNotFoundError(sessionId);
|
||||
if (req && !canAccessOwned(getAuthUser(req), session.owner)) throw sessionNotFoundError(sessionId);
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link findSessionOrFail}, for a session that exists ONLY in persisted
|
||||
* state — a resumed-but-never-reattached row (e.g. a non-claude "Resume" that
|
||||
* relaunched into a new session and wants to retire the row it can no longer
|
||||
* reattach to) has no live `Session` instance for `findSessionOrFail` to
|
||||
* return, so this returns the persisted record instead. Same ownership
|
||||
* enforcement, same 404-not-403 leak protection — this is that function's
|
||||
* missing other half, not a separate check reimplemented inline.
|
||||
*/
|
||||
export function findPersistedSessionOrFail(
|
||||
store: { getSession(id: string): SessionState | null },
|
||||
sessionId: string,
|
||||
req?: FastifyRequest
|
||||
): SessionState {
|
||||
const persisted = store.getSession(sessionId);
|
||||
if (!persisted) throw sessionNotFoundError(sessionId);
|
||||
if (req && !canAccessOwned(getAuthUser(req), persisted.owner)) throw sessionNotFoundError(sessionId);
|
||||
return persisted;
|
||||
}
|
||||
|
||||
/** Shortest prefix accepted for a parent session id (see resolveParentSessionId). */
|
||||
const PARENT_SESSION_ID_MIN_PREFIX = 8;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user