import { sessionStore } from '@electron/gateway/session-store'; import { getTranscriptFilePath, getTranscriptPathCandidates, } from '@electron/utils/token-usage-writer'; import { buildAgentSessionKey, normalizeAgentSessionKey, parseSessionKey } from '@runtime/lib/models'; import type { HostApiContext } from '../context'; import type { NormalizedHostApiRequest } from '../route-utils'; import { fail, ok, parseJsonBody } from '../route-utils'; function parseSessionIdentity(request: NormalizedHostApiRequest): { agentId: string; sessionId: string; sessionKey: string } | null { const sessionKey = normalizeAgentSessionKey(request.url.searchParams.get('sessionKey')?.trim() || ''); const parsed = parseSessionKey(sessionKey); if (sessionKey && parsed.isAgentSession) { return { agentId: parsed.agentId, sessionId: parsed.sessionId, sessionKey: parsed.sessionKey, }; } const agentId = request.url.searchParams.get('agentId')?.trim() || ''; const sessionId = request.url.searchParams.get('sessionId')?.trim() || ''; if (!agentId || !sessionId) return null; return { agentId: parseSessionKey(buildAgentSessionKey(agentId, sessionId)).agentId, sessionId: parseSessionKey(buildAgentSessionKey(agentId, sessionId)).sessionId, sessionKey: buildAgentSessionKey(agentId, sessionId), }; } export async function handleSessionRoutes( request: NormalizedHostApiRequest, _ctx: HostApiContext, ) { const { pathname, method } = request; if (pathname === '/api/sessions/transcript' && method === 'GET') { const identity = parseSessionIdentity(request); if (!identity) { return fail(400, 'sessionKey or agentId/sessionId is required'); } try { const fsP = await import('node:fs/promises'); let transcriptPath = getTranscriptFilePath(identity.sessionKey); let raw: string | null = null; let lastError: unknown = null; for (const candidatePath of getTranscriptPathCandidates(identity.sessionKey)) { try { raw = await fsP.readFile(candidatePath, 'utf8'); transcriptPath = candidatePath; break; } catch (error) { lastError = error; if ((error as { code?: string } | null)?.code !== 'ENOENT') { throw error; } } } if (raw === null) { const requestedSessionKey = request.url.searchParams.get('sessionKey')?.trim() || ''; if (requestedSessionKey && requestedSessionKey !== identity.sessionKey) { for (const candidatePath of getTranscriptPathCandidates(requestedSessionKey)) { try { raw = await fsP.readFile(candidatePath, 'utf8'); transcriptPath = candidatePath; break; } catch (candidateError) { lastError = candidateError; if ((candidateError as { code?: string } | null)?.code !== 'ENOENT') { throw candidateError; } } } } } if (raw === null) { throw lastError ?? Object.assign(new Error('Transcript not found'), { code: 'ENOENT' }); } const lines = raw.split(/\r?\n/).filter(Boolean); const messages = lines.flatMap((line) => { try { const entry = JSON.parse(line) as { type?: string; message?: unknown; timestamp?: string }; return entry.type === 'message' && entry.message ? [{ ...entry.message, timestamp: entry.timestamp }] : []; } catch { return []; } }); return ok({ agentId: identity.agentId, sessionId: identity.sessionId, sessionKey: identity.sessionKey, transcriptPath, messages, }); } catch (error: any) { if (error?.code === 'ENOENT') { return fail(404, 'Transcript not found'); } return fail(500, error instanceof Error ? error.message : String(error)); } } if (pathname === '/api/sessions/delete' && method === 'POST') { const body = parseJsonBody<{ sessionKey: string }>(request.body); const rawSessionKey = String(body?.sessionKey || '').trim(); const sessionKey = normalizeAgentSessionKey(rawSessionKey); if (!sessionKey) { return fail(400, 'sessionKey is required'); } sessionStore.deleteSession(sessionKey); const fsP = await import('node:fs/promises'); const transcriptPathCandidates = Array.from(new Set([ ...getTranscriptPathCandidates(sessionKey), ...(rawSessionKey && rawSessionKey !== sessionKey ? getTranscriptPathCandidates(rawSessionKey) : []), ])); for (const transcriptPath of transcriptPathCandidates) { try { await fsP.rename(transcriptPath, transcriptPath.replace(/\.jsonl$/, '.deleted.jsonl')); break; } catch { continue; } } return ok({ success: true }); } return null; }