import { randomUUID } from "node:crypto"; export const DEFAULT_CODE_AGENT_SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1000; export const DEFAULT_CODE_AGENT_MAX_SESSIONS = 200; export const CODE_AGENT_SESSION_STATUSES = Object.freeze([ "creating", "ready", "busy", "idle", "interrupted", "expired", "failed" ]); export function createCodeAgentSessionRegistry(options = {}) { const idleTimeoutMs = positiveInteger(options.idleTimeoutMs, DEFAULT_CODE_AGENT_SESSION_IDLE_TIMEOUT_MS); const maxSessions = positiveInteger(options.maxSessions, DEFAULT_CODE_AGENT_MAX_SESSIONS); const idFactory = typeof options.idFactory === "function" ? options.idFactory : () => `ses_${randomUUID()}`; const defaultNow = options.now; const sessions = new Map(); const conversations = new Map(); function acquire(params = {}) { const timestamp = timestampFor(params.now ?? defaultNow); const timestampMs = Date.parse(timestamp); const conversationId = requiredId(params.conversationId, "cnv"); const requestedSessionId = optionalId(params.sessionId); const mappedSessionId = conversations.get(conversationId) ?? null; if (mappedSessionId && requestedSessionId && mappedSessionId !== requestedSessionId) { const mappedSession = sessions.get(mappedSessionId) ?? null; return blockedAcquire({ code: "session_reuse_conflict", summary: `conversationId ${conversationId} is already bound to sessionId ${mappedSessionId}; requested ${requestedSessionId}.`, session: mappedSession, timestamp, traceId: params.traceId }); } const effectiveSessionId = mappedSessionId || requestedSessionId || requiredId(idFactory(), "ses"); let session = sessions.get(effectiveSessionId) ?? null; const reused = Boolean(session); if (session && sessionExpired(session, timestampMs)) { session.status = "expired"; session.updatedAt = timestamp; session.lastTraceId = optionalId(params.traceId); return blockedAcquire({ code: "session_expired", summary: `Code Agent runner session ${effectiveSessionId} expired after ${session.idleTimeoutMs}ms idle timeout.`, session, timestamp, traceId: params.traceId }); } if (session?.status === "busy") { session.updatedAt = timestamp; session.lastTraceId = optionalId(params.traceId) ?? session.lastTraceId; return blockedAcquire({ code: "session_busy", summary: `Code Agent runner session ${effectiveSessionId} is already busy with traceId ${session.currentTraceId ?? "unknown"}.`, session, timestamp, traceId: params.traceId }); } if (!session) { session = { sessionId: effectiveSessionId, conversationIds: new Set(), status: "creating", workspace: params.workspace ?? null, sandbox: params.sandbox ?? "read-only", runnerKind: params.runnerKind ?? "hwlab-readonly-runner", capabilityLevel: params.capabilityLevel ?? "read-only-session-tools", implementationType: params.implementationType ?? "controlled-readonly-session-registry", createdAt: timestamp, updatedAt: timestamp, idleTimeoutMs, expiresAt: plusMs(timestampMs, idleTimeoutMs), lastTraceId: optionalId(params.traceId), currentTraceId: null, turn: 0, durable: false, longLivedSession: false, codexStdio: false, secretMaterialStored: false }; sessions.set(effectiveSessionId, session); } session.conversationIds.add(conversationId); session.workspace = params.workspace ?? session.workspace; session.sandbox = params.sandbox ?? session.sandbox; session.runnerKind = params.runnerKind ?? session.runnerKind; session.capabilityLevel = params.capabilityLevel ?? session.capabilityLevel; session.implementationType = params.implementationType ?? session.implementationType; session.status = "busy"; session.updatedAt = timestamp; session.expiresAt = plusMs(timestampMs, idleTimeoutMs); session.lastTraceId = optionalId(params.traceId); session.currentTraceId = optionalId(params.traceId); session.turn += 1; conversations.set(conversationId, effectiveSessionId); prune(); return { ok: true, reused, session: publicSession(session, { conversationId, reused, timestamp }) }; } function release(sessionId, params = {}) { const session = sessions.get(requiredId(sessionId, "ses")) ?? null; if (!session) return null; const timestamp = timestampFor(params.now ?? defaultNow); const timestampMs = Date.parse(timestamp); session.status = CODE_AGENT_SESSION_STATUSES.includes(params.status) ? params.status : "idle"; session.updatedAt = timestamp; session.expiresAt = plusMs(timestampMs, session.idleTimeoutMs); session.lastTraceId = optionalId(params.traceId) ?? session.lastTraceId; session.currentTraceId = null; session.statusReason = params.statusReason ?? null; return publicSession(session, { conversationId: params.conversationId, reused: params.reused, timestamp }); } function fail(sessionId, params = {}) { return release(sessionId, { ...params, status: "failed" }); } function interrupt(sessionId, params = {}) { return release(sessionId, { ...params, status: "interrupted" }); } function get(sessionId, params = {}) { const session = sessions.get(requiredId(sessionId, "ses")) ?? null; return session ? publicSession(session, params) : null; } function getByConversation(conversationId, params = {}) { const sessionId = conversations.get(requiredId(conversationId, "cnv")); return sessionId ? get(sessionId, { ...params, conversationId }) : null; } function expireIdle(params = {}) { const timestamp = timestampFor(params.now ?? defaultNow); const timestampMs = Date.parse(timestamp); let expired = 0; for (const session of sessions.values()) { if (!sessionExpired(session, timestampMs)) continue; session.status = "expired"; session.updatedAt = timestamp; session.currentTraceId = null; expired += 1; } return expired; } function describe() { return { kind: "code-agent-session-registry", implementationType: "controlled-readonly-session-registry", status: "available", sessionCount: sessions.size, maxSessions, idleTimeoutMs, statuses: [...CODE_AGENT_SESSION_STATUSES], longLivedCodexStdio: false, durable: false, secretMaterialStored: false, valuesRedacted: true }; } function clear() { sessions.clear(); conversations.clear(); } function prune() { if (sessions.size <= maxSessions) return; const sorted = [...sessions.entries()] .sort((a, b) => String(a[1].updatedAt).localeCompare(String(b[1].updatedAt))); for (const [sessionId, session] of sorted.slice(0, sessions.size - maxSessions)) { sessions.delete(sessionId); for (const conversationId of session.conversationIds) { if (conversations.get(conversationId) === sessionId) { conversations.delete(conversationId); } } } } return { acquire, release, fail, interrupt, get, getByConversation, expireIdle, describe, clear }; } function blockedAcquire({ code, summary, session, timestamp, traceId }) { const publicEvidence = session ? publicSession(session, { timestamp, reused: true }) : { status: "failed", updatedAt: timestamp, lastTraceId: optionalId(traceId), secretMaterialStored: false, valuesRedacted: true }; return { ok: false, code, message: summary, session: publicEvidence, blocker: { code, sourceIssue: "pikasTech/HWLAB#317", summary } }; } function publicSession(session, { conversationId = null, reused = null } = {}) { const conversationIds = [...session.conversationIds]; return { sessionId: session.sessionId, conversationId: conversationId ?? conversationIds[0] ?? null, conversationIds, status: session.status, workspace: session.workspace, sandbox: session.sandbox, runnerKind: session.runnerKind, capabilityLevel: session.capabilityLevel, implementationType: session.implementationType, createdAt: session.createdAt, updatedAt: session.updatedAt, idleTimeoutMs: session.idleTimeoutMs, expiresAt: session.expiresAt, lastTraceId: session.lastTraceId, currentTraceId: session.currentTraceId, turn: session.turn, reused: reused === null ? undefined : Boolean(reused), durable: session.durable === true, longLivedSession: session.longLivedSession === true, codexStdio: session.codexStdio === true, secretMaterialStored: false, valuesRedacted: true, ...(session.statusReason ? { statusReason: session.statusReason } : {}) }; } function sessionExpired(session, timestampMs) { if (!Number.isFinite(timestampMs) || session.status === "expired") return session.status === "expired"; return Date.parse(session.expiresAt) <= timestampMs; } function timestampFor(now) { const value = typeof now === "function" ? now() : now; if (typeof value === "string" && !Number.isNaN(Date.parse(value))) return value; if (value instanceof Date && !Number.isNaN(value.valueOf())) return value.toISOString(); return new Date().toISOString(); } function plusMs(timestampMs, offsetMs) { return new Date(timestampMs + offsetMs).toISOString(); } function requiredId(value, fallbackPrefix) { const id = optionalId(value); if (id) return id; return `${fallbackPrefix}_${randomUUID()}`; } function optionalId(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function positiveInteger(value, fallback) { return Number.isInteger(value) && value > 0 ? value : fallback; }