nyk
feat(refactor): ready for manual QA after main sync (#274)
b6ecafa unverified
import { NextRequest, NextResponse } from 'next/server'
import { existsSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { requireRole } from '@/lib/auth'
import { config } from '@/lib/config'
import { logger } from '@/lib/logger'
import { parseGatewayHistoryTranscript, parseJsonlTranscript } from '@/lib/transcript-parser'
import { callOpenClawGateway } from '@/lib/openclaw-gateway'
/**
* GET /api/sessions/transcript/gateway?key=<session-key>&limit=50
*
* Reads the JSONL transcript file for a gateway session directly from disk.
* OpenClaw stores session transcripts at:
* {OPENCLAW_STATE_DIR}/agents/{agent}/sessions/{sessionId}.jsonl
*
* The session key (e.g. "agent:jarv:cron:task-name") is used to look up
* the sessionId from the agent's sessions.json, then the JSONL file is read.
*/
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'viewer')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const { searchParams } = new URL(request.url)
const sessionKey = searchParams.get('key') || ''
const limit = Math.min(parseInt(searchParams.get('limit') || '50', 10), 200)
if (!sessionKey) {
return NextResponse.json({ error: 'key is required' }, { status: 400 })
}
const stateDir = config.openclawStateDir
if (!stateDir) {
return NextResponse.json({ messages: [], source: 'gateway', error: 'OPENCLAW_STATE_DIR not configured' })
}
try {
try {
const history = await callOpenClawGateway<{ messages?: unknown[] }>(
'chat.history',
{ sessionKey, limit },
15000,
)
const liveMessages = parseGatewayHistoryTranscript(Array.isArray(history?.messages) ? history.messages : [], limit)
if (liveMessages.length > 0) {
return NextResponse.json({ messages: liveMessages, source: 'gateway-rpc' })
}
} catch (rpcErr) {
logger.warn({ err: rpcErr, sessionKey }, 'Gateway chat.history failed, falling back to disk transcript')
}
// Extract agent name from session key (e.g. "agent:jarv:main" -> "jarv")
const agentName = extractAgentName(sessionKey)
if (!agentName) {
return NextResponse.json({ messages: [], source: 'gateway', error: 'Could not determine agent from session key' })
}
// Look up the sessionId from the agent's sessions.json
const sessionsFile = path.join(stateDir, 'agents', agentName, 'sessions', 'sessions.json')
if (!existsSync(sessionsFile)) {
return NextResponse.json({ messages: [], source: 'gateway', error: 'Agent sessions file not found' })
}
let sessionsData: Record<string, any>
try {
sessionsData = JSON.parse(readFileSync(sessionsFile, 'utf-8'))
} catch {
return NextResponse.json({ messages: [], source: 'gateway', error: 'Could not parse sessions.json' })
}
const sessionEntry = sessionsData[sessionKey]
if (!sessionEntry?.sessionId) {
return NextResponse.json({ messages: [], source: 'gateway', error: 'Session not found in sessions.json' })
}
const sessionId = sessionEntry.sessionId
const jsonlPath = path.join(stateDir, 'agents', agentName, 'sessions', `${sessionId}.jsonl`)
if (!existsSync(jsonlPath)) {
return NextResponse.json({ messages: [], source: 'gateway', error: 'Session JSONL file not found' })
}
// Read and parse the JSONL file
const raw = readFileSync(jsonlPath, 'utf-8')
const messages = parseJsonlTranscript(raw, limit)
return NextResponse.json({ messages, source: 'gateway' })
} catch (err: any) {
logger.warn({ err, sessionKey }, 'Gateway session transcript read failed')
return NextResponse.json({ messages: [], source: 'gateway', error: 'Failed to read session transcript' })
}
}
function extractAgentName(sessionKey: string): string | null {
const parts = sessionKey.split(':')
if (parts.length >= 2 && parts[0] === 'agent') {
return parts[1]
}
return null
}
export const dynamic = 'force-dynamic'