Files
pikasTech-HWLAB/internal/cloud/code-agent-session-registry.mjs
T
2026-05-23 16:04:59 +00:00

304 lines
10 KiB
JavaScript

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",
sessionMode: params.sessionMode ?? params.implementationType ?? "controlled-readonly-session-registry",
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.sessionMode = params.sessionMode ?? session.sessionMode;
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],
sessionMode: "controlled-readonly-session-registry",
recentSessions: [...sessions.values()]
.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))
.slice(0, 10)
.map((session) => publicSession(session)),
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,
sessionMode: session.sessionMode,
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;
}