652 lines
26 KiB
TypeScript
652 lines
26 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
|
|
import {
|
|
CODE_AGENT_SESSION_LIFECYCLE_STATUSES,
|
|
CODE_AGENT_SESSION_STATUS_ALIASES,
|
|
codeAgentSessionLifecycleSummary,
|
|
decorateCodeAgentSession
|
|
} from "./code-agent-session-lifecycle.ts";
|
|
|
|
export const DEFAULT_CODE_AGENT_SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
export const DEFAULT_CODE_AGENT_MAX_SESSIONS = 200;
|
|
export const DEFAULT_CODE_AGENT_MAX_CONVERSATION_FACTS = 12;
|
|
export const DEFAULT_CODE_AGENT_MAX_FACT_TOOL_CALLS = 8;
|
|
export const DEFAULT_CODE_AGENT_MAX_FACT_SKILLS = 24;
|
|
export const CODE_AGENT_SESSION_STATUSES = Object.freeze([
|
|
"creating",
|
|
"ready",
|
|
"busy",
|
|
"idle",
|
|
"timeout",
|
|
"error",
|
|
"canceled",
|
|
"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 maxConversationFacts = positiveInteger(options.maxConversationFacts, DEFAULT_CODE_AGENT_MAX_CONVERSATION_FACTS);
|
|
const maxFactToolCalls = positiveInteger(options.maxFactToolCalls, DEFAULT_CODE_AGENT_MAX_FACT_TOOL_CALLS);
|
|
const maxFactSkills = positiveInteger(options.maxFactSkills, DEFAULT_CODE_AGENT_MAX_FACT_SKILLS);
|
|
const idFactory = typeof options.idFactory === "function" ? options.idFactory : () => `ses_${randomUUID()}`;
|
|
const defaultNow = options.now;
|
|
const sessions = new Map();
|
|
const conversations = new Map();
|
|
const conversationFacts = 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 && ["expired"].includes(session.status)) {
|
|
session.updatedAt = timestamp;
|
|
session.lastTraceId = optionalId(params.traceId) ?? session.lastTraceId;
|
|
return blockedAcquire({
|
|
code: `session_${session.status}`,
|
|
summary: `Code Agent runner session ${effectiveSessionId} is ${session.status}; create a new session before retrying.`,
|
|
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: true,
|
|
codexStdio: false,
|
|
readOnly: true,
|
|
writeCapable: 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 findSession(params = {}) {
|
|
const conversationId = optionalId(params.conversationId);
|
|
const sessionId = optionalId(params.sessionId);
|
|
const traceId = optionalId(params.traceId);
|
|
const threadId = optionalId(params.threadId);
|
|
if (sessionId) {
|
|
const session = get(sessionId, { conversationId });
|
|
if (session) return session;
|
|
}
|
|
if (conversationId) {
|
|
const session = getByConversation(conversationId, { conversationId });
|
|
if (session) return session;
|
|
}
|
|
for (const session of sessions.values()) {
|
|
const conversationIds = [...session.conversationIds];
|
|
if (conversationId && conversationIds.includes(conversationId)) return publicSession(session, { conversationId });
|
|
if (traceId && (session.lastTraceId === traceId || session.currentTraceId === traceId)) return publicSession(session, { conversationId: conversationIds[0] ?? null });
|
|
if (threadId && session.threadId === threadId) return publicSession(session, { conversationId: conversationIds[0] ?? null });
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function inspect(params = {}) {
|
|
const session = findSession(params);
|
|
const conversationId = optionalId(params.conversationId) ?? session?.conversationId ?? session?.conversationIds?.[0] ?? null;
|
|
const facts = conversationId ? getConversationFacts(conversationId) : null;
|
|
const factsFound = hasConversationFacts(facts);
|
|
const requestedTraceId = optionalId(params.traceId);
|
|
const traceIds = uniqueStrings([
|
|
facts?.latestTraceId,
|
|
session?.currentTraceId,
|
|
session?.lastTraceId,
|
|
...(Array.isArray(facts?.traceIds) ? facts.traceIds : [])
|
|
]);
|
|
const found = Boolean(session || factsFound || traceIds.length > 0);
|
|
return {
|
|
ok: found,
|
|
action: "code-agent.session.inspect",
|
|
status: found ? "found" : "not_found",
|
|
query: {
|
|
conversationId: optionalId(params.conversationId),
|
|
sessionId: optionalId(params.sessionId),
|
|
threadId: optionalId(params.threadId),
|
|
traceId: requestedTraceId
|
|
},
|
|
session,
|
|
conversationFacts: facts,
|
|
traceIds,
|
|
latestTraceId: traceIds[0] ?? null,
|
|
valuesRedacted: true,
|
|
secretMaterialStored: false
|
|
};
|
|
}
|
|
|
|
function recordFact(conversationIdValue, fact, params = {}) {
|
|
const conversationId = requiredId(conversationIdValue, "cnv");
|
|
const timestamp = timestampFor(params.now ?? defaultNow);
|
|
const sanitized = sanitizeConversationFact(fact, {
|
|
conversationId,
|
|
timestamp,
|
|
maxFactToolCalls,
|
|
maxFactSkills
|
|
});
|
|
if (!sanitized) return getConversationFacts(conversationId);
|
|
const existing = conversationFacts.get(conversationId) ?? {
|
|
conversationId,
|
|
facts: []
|
|
};
|
|
existing.facts.push(sanitized);
|
|
existing.facts = existing.facts.slice(-maxConversationFacts);
|
|
conversationFacts.set(conversationId, existing);
|
|
return getConversationFacts(conversationId);
|
|
}
|
|
|
|
function getConversationFacts(conversationIdValue, params = {}) {
|
|
const conversationId = requiredId(conversationIdValue, "cnv");
|
|
const stored = conversationFacts.get(conversationId);
|
|
const facts = stored?.facts ?? [];
|
|
const session = getByConversation(conversationId, params);
|
|
return publicConversationFacts({
|
|
conversationId,
|
|
session,
|
|
facts,
|
|
maxFactToolCalls,
|
|
maxConversationFacts
|
|
});
|
|
}
|
|
|
|
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,
|
|
conversationFactCount: conversationFacts.size,
|
|
maxSessions,
|
|
maxConversationFacts,
|
|
idleTimeoutMs,
|
|
statuses: [...CODE_AGENT_SESSION_STATUSES],
|
|
lifecycleStatuses: [...CODE_AGENT_SESSION_LIFECYCLE_STATUSES],
|
|
statusAliases: { ...CODE_AGENT_SESSION_STATUS_ALIASES },
|
|
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,
|
|
longLivedSession: true,
|
|
durable: false,
|
|
secretMaterialStored: false,
|
|
valuesRedacted: true
|
|
};
|
|
}
|
|
|
|
function clear() {
|
|
sessions.clear();
|
|
conversations.clear();
|
|
conversationFacts.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);
|
|
}
|
|
conversationFacts.delete(conversationId);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
acquire,
|
|
release,
|
|
fail,
|
|
interrupt,
|
|
get,
|
|
getByConversation,
|
|
findSession,
|
|
inspect,
|
|
recordFact,
|
|
getConversationFacts,
|
|
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 decorateCodeAgentSession({
|
|
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,
|
|
durableSession: session.durable === true,
|
|
longLivedSession: session.longLivedSession === true,
|
|
codexStdio: session.codexStdio === true,
|
|
readOnly: session.readOnly === true,
|
|
writeCapable: session.writeCapable === true,
|
|
secretMaterialStored: false,
|
|
valuesRedacted: true,
|
|
...(session.statusReason ? { statusReason: session.statusReason } : {})
|
|
}, { reused });
|
|
}
|
|
|
|
function publicConversationFacts({ conversationId, session, facts, maxFactToolCalls, maxConversationFacts }) {
|
|
const latest = facts.at(-1) ?? null;
|
|
const latestOperational = [...facts].reverse().find((fact) => fact.kind !== "session_context") ?? latest;
|
|
const latestSessionFact = [...facts].reverse().find((fact) => fact.sessionId && fact.sessionMode !== "provider-text-request") ?? latestOperational;
|
|
const latestSkills = [...facts].reverse().find((fact) => fact.skills?.status && fact.skills.status !== "not_requested")?.skills ?? null;
|
|
const latestWorkspace = [...facts].reverse().find((fact) => fact.kind !== "session_context" && fact.workspace)?.workspace ??
|
|
[...facts].reverse().find((fact) => fact.workspace)?.workspace ??
|
|
session?.workspace ??
|
|
null;
|
|
const traceIds = [...new Set(facts.map((fact) => fact.traceId).filter(Boolean))].slice(-6);
|
|
const recentToolCalls = facts
|
|
.flatMap((fact) => fact.toolCalls?.items ?? [])
|
|
.slice(-maxFactToolCalls);
|
|
return {
|
|
conversationId,
|
|
sessionId: session?.sessionId ?? latestSessionFact?.sessionId ?? latest?.sessionId ?? null,
|
|
sessionStatus: session?.status ?? latestSessionFact?.sessionStatus ?? latest?.sessionStatus ?? null,
|
|
sessionLifecycleStatus: session?.lifecycleStatus ??
|
|
normalizeFactLifecycleStatus(latestSessionFact?.sessionLifecycleStatus ?? latest?.sessionLifecycleStatus ?? latestSessionFact?.sessionStatus ?? latest?.sessionStatus),
|
|
sessionMode: session?.sessionMode ?? latestSessionFact?.sessionMode ?? latest?.sessionMode ?? null,
|
|
capabilityLevel: latestOperational?.capabilityLevel ?? session?.capabilityLevel ?? latest?.capabilityLevel ?? null,
|
|
runnerKind: latestOperational?.runnerKind ?? session?.runnerKind ?? latest?.runnerKind ?? null,
|
|
latestProvider: latestOperational?.provider ?? latest?.provider ?? null,
|
|
latestBackend: latestOperational?.backend ?? latest?.backend ?? null,
|
|
workspace: latestWorkspace,
|
|
sandbox: latestOperational?.sandbox ?? session?.sandbox ?? latest?.sandbox ?? null,
|
|
turnCount: facts.length,
|
|
maxFacts: maxConversationFacts,
|
|
traceIds,
|
|
latestTraceId: traceIds.at(-1) ?? session?.lastTraceId ?? null,
|
|
latestSkills,
|
|
recentToolCalls,
|
|
facts: facts.map((fact) => ({ ...fact })),
|
|
valuesRedacted: true,
|
|
secretMaterialStored: false,
|
|
summary: conversationFactsSummary({ facts, session, latestSkills, latestWorkspace, traceIds, recentToolCalls })
|
|
};
|
|
}
|
|
|
|
function sanitizeConversationFact(fact, { conversationId, timestamp, maxFactToolCalls, maxFactSkills }) {
|
|
if (!fact || typeof fact !== "object") return null;
|
|
const session = fact.session && typeof fact.session === "object" ? fact.session : null;
|
|
const runner = fact.runner && typeof fact.runner === "object" ? fact.runner : null;
|
|
const runnerTrace = fact.runnerTrace && typeof fact.runnerTrace === "object" ? fact.runnerTrace : null;
|
|
const toolCalls = sanitizeToolCalls(fact.toolCalls, maxFactToolCalls);
|
|
const skills = sanitizeSkillsFact(fact.skills, maxFactSkills);
|
|
return {
|
|
kind: safeFactText(fact.kind ?? "runner_turn", 80),
|
|
conversationId,
|
|
sessionId: safeFactText(session?.sessionId ?? fact.sessionId ?? runner?.sessionId, 120),
|
|
sessionStatus: safeFactText(session?.status ?? fact.sessionStatus, 40),
|
|
sessionLifecycleStatus: safeFactText(
|
|
session?.lifecycleStatus ??
|
|
runnerTrace?.sessionLifecycleStatus ??
|
|
fact.sessionLifecycleStatus ??
|
|
normalizeFactLifecycleStatus(session?.status ?? fact.sessionStatus),
|
|
40
|
|
),
|
|
sessionMode: safeFactText(fact.sessionMode ?? session?.sessionMode ?? runner?.sessionMode ?? runnerTrace?.sessionMode, 120),
|
|
sessionReused: fact.sessionReuse?.reused === true || session?.reused === true || runner?.sessionReused === true,
|
|
turn: numberOrNull(session?.turn ?? fact.sessionReuse?.turn ?? runnerTrace?.turn ?? runner?.turn),
|
|
recordedAt: timestamp,
|
|
traceId: safeFactText(fact.traceId ?? runnerTrace?.traceId, 120),
|
|
provider: safeFactText(fact.provider ?? runner?.provider, 120),
|
|
backend: safeFactText(fact.backend ?? runner?.backend, 160),
|
|
runnerKind: safeFactText(runner?.kind ?? runnerTrace?.runnerKind ?? fact.runnerKind, 120),
|
|
workspace: safeFactText(fact.workspace ?? runner?.workspace ?? runnerTrace?.workspace ?? session?.workspace, 240),
|
|
sandbox: safeFactText(fact.sandbox ?? runner?.sandbox ?? runnerTrace?.sandbox ?? session?.sandbox, 120),
|
|
capabilityLevel: safeFactText(fact.capabilityLevel ?? runner?.capabilityLevel ?? session?.capabilityLevel, 120),
|
|
toolCalls,
|
|
skills,
|
|
runnerTrace: sanitizeRunnerTrace(runnerTrace),
|
|
partialContext: sanitizePartialContext(fact.partialContext ?? fact.partialConversationContext ?? runnerTrace?.partialContext),
|
|
valuesRedacted: true,
|
|
secretMaterialStored: false
|
|
};
|
|
}
|
|
|
|
function sanitizePartialContext(value) {
|
|
if (!value || typeof value !== "object") return null;
|
|
const assistantMessages = Array.isArray(value.assistantMessages)
|
|
? value.assistantMessages.map((item) => safeFactText(item, 240)).filter(Boolean).slice(-4)
|
|
: [];
|
|
const toolEvidence = Array.isArray(value.toolEvidence)
|
|
? value.toolEvidence.map((item) => safeFactText(item, 200)).filter(Boolean).slice(-6)
|
|
: [];
|
|
const summary = safeFactText(value.summary, 400);
|
|
if (!summary && assistantMessages.length === 0 && toolEvidence.length === 0) return null;
|
|
return {
|
|
status: safeFactText(value.status ?? "partial", 40) ?? "partial",
|
|
summary,
|
|
assistantMessages,
|
|
toolEvidence,
|
|
valuesRedacted: true
|
|
};
|
|
}
|
|
|
|
function sanitizeToolCalls(toolCalls, limit) {
|
|
const items = Array.isArray(toolCalls)
|
|
? toolCalls.slice(0, limit).map((toolCall) => ({
|
|
name: safeFactText(toolCall?.name, 120),
|
|
type: safeFactText(toolCall?.type, 80),
|
|
status: safeFactText(toolCall?.status, 40),
|
|
route: safeFactText(toolCall?.route ?? toolCall?.hwlabApi?.route, 120),
|
|
method: safeFactText(toolCall?.method, 16),
|
|
traceId: safeFactText(toolCall?.traceId, 120),
|
|
outputTruncated: toolCall?.outputTruncated === true
|
|
})).filter((toolCall) => toolCall.name || toolCall.status || toolCall.route)
|
|
: [];
|
|
return {
|
|
count: Array.isArray(toolCalls) ? toolCalls.length : 0,
|
|
items,
|
|
truncated: Array.isArray(toolCalls) && toolCalls.length > items.length
|
|
};
|
|
}
|
|
|
|
function sanitizeSkillsFact(skills, limit) {
|
|
if (!skills || typeof skills !== "object") {
|
|
return {
|
|
status: "not_requested",
|
|
count: 0,
|
|
totalCount: 0,
|
|
names: [],
|
|
blockers: [],
|
|
truncated: false
|
|
};
|
|
}
|
|
const items = Array.isArray(skills.items) ? skills.items : [];
|
|
const names = items
|
|
.map((item) => safeFactText(item?.name, 120))
|
|
.filter(Boolean)
|
|
.slice(0, limit);
|
|
return {
|
|
status: safeFactText(skills.status ?? "unknown", 40),
|
|
count: numberOrNull(skills.count ?? items.length) ?? items.length,
|
|
totalCount: numberOrNull(skills.totalCount ?? skills.count ?? items.length) ?? items.length,
|
|
names,
|
|
blockers: Array.isArray(skills.blockers)
|
|
? skills.blockers.map((blocker) => safeFactText(blocker?.code, 120)).filter(Boolean).slice(0, 6)
|
|
: [],
|
|
truncated: skills.truncated === true || items.length > names.length
|
|
};
|
|
}
|
|
|
|
function sanitizeRunnerTrace(runnerTrace) {
|
|
if (!runnerTrace || typeof runnerTrace !== "object") return null;
|
|
return {
|
|
traceId: safeFactText(runnerTrace.traceId, 120),
|
|
runnerKind: safeFactText(runnerTrace.runnerKind, 120),
|
|
sessionMode: safeFactText(runnerTrace.sessionMode, 120),
|
|
sessionId: safeFactText(runnerTrace.sessionId, 120),
|
|
sessionStatus: safeFactText(runnerTrace.sessionStatus, 80),
|
|
sessionLifecycleStatus: safeFactText(runnerTrace.sessionLifecycleStatus ?? normalizeFactLifecycleStatus(runnerTrace.sessionStatus), 80),
|
|
turn: numberOrNull(runnerTrace.turn),
|
|
events: Array.isArray(runnerTrace.events)
|
|
? runnerTrace.events.map((event) => safeFactText(event, 120)).filter(Boolean).slice(-8)
|
|
: [],
|
|
route: safeFactText(runnerTrace.route, 120),
|
|
status: safeFactText(runnerTrace.status, 80),
|
|
partialContext: sanitizePartialContext(runnerTrace.partialContext),
|
|
outputTruncated: runnerTrace.outputTruncated === true,
|
|
valuesPrinted: false
|
|
};
|
|
}
|
|
|
|
function normalizeFactLifecycleStatus(status) {
|
|
return codeAgentSessionLifecycleSummary({ sessionStatus: status }).status;
|
|
}
|
|
|
|
function conversationFactsSummary({ facts, session, latestSkills, latestWorkspace, traceIds, recentToolCalls }) {
|
|
const skillNames = latestSkills?.names?.slice(0, 5) ?? [];
|
|
const toolNames = recentToolCalls.map((toolCall) => [toolCall.name, toolCall.status].filter(Boolean).join(":")).filter(Boolean).slice(-5);
|
|
return [
|
|
`turns=${facts.length}`,
|
|
`sessionId=${session?.sessionId ?? facts.at(-1)?.sessionId ?? "unknown"}`,
|
|
`sessionStatus=${session?.status ?? facts.at(-1)?.sessionStatus ?? "unknown"}`,
|
|
latestWorkspace ? `workspace=${latestWorkspace}` : null,
|
|
latestSkills ? `skills=${latestSkills.totalCount ?? latestSkills.count ?? skillNames.length}${skillNames.length ? ` (${skillNames.join(",")})` : ""}` : "skills=not_requested",
|
|
traceIds.length ? `traceIds=${traceIds.join(",")}` : null,
|
|
toolNames.length ? `tools=${toolNames.join(",")}` : null
|
|
].filter(Boolean).join(" / ");
|
|
}
|
|
|
|
function safeFactText(value, maxLength) {
|
|
if (value === undefined || value === null || value === "") return null;
|
|
const normalized = String(value)
|
|
.replace(/\s+/gu, " ")
|
|
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gu, "Bearer ***")
|
|
.replace(/sk-[A-Za-z0-9._-]+/gu, "sk-***")
|
|
.replace(/([A-Za-z0-9_]*(?:TOKEN|KEY|SECRET|PASSWORD)[A-Za-z0-9_]*=)[^\s]+/giu, "$1***")
|
|
.replace(/\/\/[^/@\s]+@/gu, "//***@")
|
|
.trim();
|
|
if (!normalized) return null;
|
|
return normalized.length > maxLength ? `${normalized.slice(0, maxLength - 12)}...[redacted]` : normalized;
|
|
}
|
|
|
|
function numberOrNull(value) {
|
|
return Number.isFinite(value) ? value : null;
|
|
}
|
|
|
|
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 uniqueStrings(values) {
|
|
return [...new Set(values.map((value) => typeof value === "string" ? value.trim() : "").filter(Boolean))];
|
|
}
|
|
|
|
function hasConversationFacts(facts) {
|
|
return Boolean(facts && (
|
|
facts.sessionId ||
|
|
facts.sessionStatus ||
|
|
facts.latestTraceId ||
|
|
Number(facts.turnCount) > 0 ||
|
|
(Array.isArray(facts.traceIds) && facts.traceIds.length > 0) ||
|
|
(Array.isArray(facts.facts) && facts.facts.length > 0)
|
|
));
|
|
}
|
|
|
|
function positiveInteger(value, fallback) {
|
|
return Number.isInteger(value) && value > 0 ? value : fallback;
|
|
}
|