import { randomUUID } from 'node:crypto' import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises' import path from 'node:path' import { appConfigFilePath } from './vault-path.js' const MAX_SESSION_ID_BYTES = 128 const MAX_EVENT_KIND_BYTES = 64 const MAX_SUMMARY_BYTES = 12_000 const MAX_DETAIL_COUNT = 64 const MAX_DETAIL_BYTES = 12_000 const MAX_CHECKPOINT_BYTES = 1_000_000 function runtimeBasePath() { return appConfigFilePath('hldp-runtime') } function validatedSessionId(value) { const sessionId = typeof value === 'string' ? value.trim() : '' if ( !sessionId || Buffer.byteLength(sessionId) > MAX_SESSION_ID_BYTES || !/^[A-Za-z0-9._-]+$/.test(sessionId) ) { throw new Error('HLDP session id must use only letters, numbers, dot, dash, or underscore.') } return sessionId } function validatedText(label, value, maximumBytes) { const text = typeof value === 'string' ? value.trim() : '' if (!text) throw new Error(`${label} must not be empty.`) if (Buffer.byteLength(text) > maximumBytes) { throw new Error(`${label} exceeds the ${maximumBytes}-byte limit.`) } return text } function journalPath(basePath, sessionId) { return path.join(basePath, 'sessions', `${sessionId}.hdlp.jsonl`) } function checkpointPath(basePath, sessionId) { return path.join(basePath, 'checkpoints', `${sessionId}.hdlp`) } export async function recordHldpHeartbeat(args = {}, { basePath = runtimeBasePath() } = {}) { const sessionId = validatedSessionId(args.session_id) const eventKind = validatedText('HLDP event kind', args.event_kind, MAX_EVENT_KIND_BYTES) const summary = validatedText('HLDP summary', args.summary, MAX_SUMMARY_BYTES) const details = Array.isArray(args.details) ? args.details : [] if (details.length > MAX_DETAIL_COUNT) { throw new Error(`HLDP heartbeat accepts at most ${MAX_DETAIL_COUNT} detail entries.`) } const normalizedDetails = details.map(detail => ( validatedText('HLDP detail', detail, MAX_DETAIL_BYTES) )) const timestamp = new Date().toISOString() const receiptId = randomUUID() const targetPath = journalPath(basePath, sessionId) await mkdir(path.dirname(targetPath), { recursive: true }) await appendFile(targetPath, `${JSON.stringify({ schema: 'hldp-heartbeat/v0.1', session_id: sessionId, receipt_id: receiptId, timestamp, event_kind: eventKind, summary, details: normalizedDetails, source: 'persona', })}\n`, 'utf8') return { sessionId, receiptId, relativePath: path.relative(basePath, targetPath), timestamp, } } export async function readHldpHeartbeat(args = {}, { basePath = runtimeBasePath() } = {}) { const sessionId = validatedSessionId(args.session_id) const requestedLimit = Number.isInteger(args.limit) ? args.limit : 200 const limit = Math.min(Math.max(requestedLimit, 1), 2_000) try { const content = await readFile(journalPath(basePath, sessionId), 'utf8') return content .trim() .split('\n') .filter(Boolean) .slice(-limit) .map(line => JSON.parse(line)) } catch (error) { if (error?.code === 'ENOENT') return [] throw error } } export async function finalizeHldpCheckpoint(args = {}, { basePath = runtimeBasePath() } = {}) { const sessionId = validatedSessionId(args.session_id) const checkpoint = validatedText( 'HLDP checkpoint', args.checkpoint, MAX_CHECKPOINT_BYTES, ) const targetPath = checkpointPath(basePath, sessionId) await mkdir(path.dirname(targetPath), { recursive: true }) await writeFile(targetPath, `${checkpoint}\n`, { encoding: 'utf8', flag: 'wx' }) return { sessionId, receiptId: randomUUID(), relativePath: path.relative(basePath, targetPath), timestamp: new Date().toISOString(), } }