Files
openclaw/scripts/check-session-accessor-boundary.mjs

426 lines
14 KiB
JavaScript

#!/usr/bin/env node
import path from "node:path";
import ts from "typescript";
import {
collectFileViolations,
resolveRepoRoot,
resolveSourceRoots,
runAsScript,
toLine,
unwrapExpression,
} from "./lib/ts-guard-utils.mjs";
const legacyReaderNames = new Set([
"loadSessionStore",
"readSessionEntries",
"readSessionEntry",
"readSessionStoreReadOnly",
"resolveSessionStoreEntry",
]);
const legacyWholeStoreAccessNames = new Set([
...legacyReaderNames,
"saveSessionStore",
"updateSessionStore",
]);
const legacyWriterNames = new Set([
"applySessionStoreEntryPatch",
"saveSessionStore",
"updateSessionStore",
"updateSessionStoreEntry",
]);
const legacyTranscriptWriterNames = new Set([
"appendSessionTranscriptMessage",
"emitSessionTranscriptUpdate",
"rewriteTranscriptEntriesInSessionFile",
]);
const sessionCreateLifecycleWriterNames = new Set([
"applySessionStoreEntryPatch",
"saveSessionStore",
"updateSessionStore",
"updateSessionStoreEntry",
"ensureSessionTranscriptFile",
]);
const legacyManualCompactTrimNames = new Set([
"archiveFileOnDisk",
"readRecentSessionTranscriptLines",
]);
const legacyLifecycleCleanupNames = new Set([
"archiveRemovedSessionTranscripts",
"cleanupArchivedSessionTranscripts",
]);
export const migratedSessionAccessorFiles = new Set([
"src/agents/embedded-agent-runner/compaction-successor-transcript.ts",
"src/agents/embedded-agent-runner/run/attempt.ts",
"src/agents/embedded-agent-runner/tool-result-truncation.ts",
"src/agents/embedded-agent-runner/transcript-rewrite.ts",
"src/agents/embedded-agent-runner/transcript-runtime-state.ts",
"src/auto-reply/reply/agent-runner-helpers.ts",
"src/auto-reply/reply/agent-runner.ts",
"src/auto-reply/reply/commands-subagents/action-info.ts",
"src/auto-reply/reply/followup-runner.ts",
"src/auto-reply/reply/queue/drain.ts",
"src/commands/export-trajectory.ts",
"src/commands/health.ts",
"src/commands/sandbox-explain.ts",
"src/commands/sessions-tail.ts",
"src/commands/sessions.ts",
"src/commands/status.agent-local.ts",
"src/commands/status.summary.ts",
"src/config/sessions/combined-store-gateway.ts",
"src/cron/isolated-agent/delivery-target.ts",
"src/cron/service/timer.ts",
"src/gateway/session-compaction-checkpoints.ts",
"src/gateway/session-utils.ts",
"src/gateway/sessions-resolve.ts",
"src/gateway/server-methods/sessions.ts",
"src/infra/outbound/message-action-tts.ts",
"src/tui/embedded-backend.ts",
]);
export const migratedBundledPluginSessionAccessorFiles = new Set([
"extensions/discord/src/monitor/native-command-model-picker-apply.ts",
"extensions/discord/src/monitor/thread-session-close.ts",
"extensions/telegram/src/bot-handlers.runtime.ts",
]);
export const migratedSessionAccessorWriteFiles = new Set([
"src/agents/command/attempt-execution.shared.ts",
"src/agents/command/session-store.ts",
"src/agents/embedded-agent-runner/run.ts",
"src/agents/embedded-agent-runner/run/attempt.ts",
"src/auto-reply/reply/abort-cutoff.runtime.ts",
"src/auto-reply/reply/agent-runner-cli-dispatch.ts",
"src/auto-reply/reply/agent-runner-execution.ts",
"src/auto-reply/reply/agent-runner-memory.ts",
"src/auto-reply/reply/agent-runner.ts",
"src/auto-reply/reply/body.ts",
"src/auto-reply/reply/commands-acp/lifecycle.ts",
"src/auto-reply/reply/commands-reset.ts",
"src/auto-reply/reply/directive-handling.impl.ts",
"src/auto-reply/reply/directive-handling.persist.ts",
"src/auto-reply/reply/dispatch-from-config.runtime.ts",
"src/auto-reply/reply/followup-runner.ts",
"src/auto-reply/reply/get-reply.ts",
"src/auto-reply/reply/model-selection.ts",
"src/auto-reply/reply/session-reset-model.ts",
"src/auto-reply/reply/session-updates.ts",
"src/auto-reply/reply/session-usage.ts",
"src/tui/embedded-backend.ts",
"src/config/sessions/cleanup-service.ts",
]);
export const migratedTranscriptWriterFiles = new Set([
"src/agents/command/attempt-execution.ts",
"src/agents/embedded-agent-runner/context-engine-maintenance.ts",
"src/config/sessions/transcript.ts",
"src/gateway/server-methods/chat.ts",
"src/gateway/server-methods/chat-transcript-inject.ts",
"src/sessions/user-turn-transcript.ts",
]);
export const migratedSessionCompactManualTrimFiles = new Set([
"src/gateway/server-methods/sessions.ts",
]);
export const migratedSessionLifecycleCleanupFiles = new Set([
"src/config/sessions/cleanup-service.ts",
"src/cron/session-reaper.ts",
"src/infra/heartbeat-runner.ts",
]);
function normalizeRelativePath(filePath) {
return filePath.replaceAll(path.sep, "/");
}
function legacyNamesForFile(fileName) {
const normalized = normalizeRelativePath(fileName);
if (
fileName === "source.ts" ||
[...migratedBundledPluginSessionAccessorFiles].some((filePath) => normalized.endsWith(filePath))
) {
return legacyWholeStoreAccessNames;
}
return legacyReaderNames;
}
function propertyAccessName(expression) {
const unwrapped = unwrapExpression(expression);
if (ts.isIdentifier(unwrapped)) {
return unwrapped.text;
}
if (ts.isPropertyAccessExpression(unwrapped)) {
return unwrapped.name.text;
}
if (ts.isElementAccessExpression(unwrapped) && ts.isStringLiteral(unwrapped.argumentExpression)) {
return unwrapped.argumentExpression.text;
}
return null;
}
function bindingName(node) {
if (node.propertyName && ts.isIdentifier(node.propertyName)) {
return node.propertyName.text;
}
if (ts.isIdentifier(node.name)) {
return node.name.text;
}
return null;
}
function findNamedBoundaryViolations(content, fileName, legacyNames, subject) {
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
const violations = [];
const visit = (node) => {
if (ts.isImportDeclaration(node)) {
const namedBindings = node.importClause?.namedBindings;
if (namedBindings && ts.isNamedImports(namedBindings)) {
for (const specifier of namedBindings.elements) {
const importedName = specifier.propertyName?.text ?? specifier.name.text;
if (legacyNames.has(importedName)) {
violations.push({
line: toLine(sourceFile, specifier),
reason: `imports ${subject} "${importedName}"`,
});
}
}
}
}
if (ts.isBindingElement(node)) {
const name = bindingName(node);
if (name && legacyNames.has(name)) {
violations.push({
line: toLine(sourceFile, node),
reason: `aliases ${subject} "${name}"`,
});
}
}
if (ts.isPropertyAccessExpression(node) && legacyNames.has(node.name.text)) {
violations.push({
line: toLine(sourceFile, node.name),
reason: `references ${subject} "${node.name.text}"`,
});
}
if (
ts.isElementAccessExpression(node) &&
ts.isStringLiteral(node.argumentExpression) &&
legacyNames.has(node.argumentExpression.text)
) {
violations.push({
line: toLine(sourceFile, node.argumentExpression),
reason: `references ${subject} "${node.argumentExpression.text}"`,
});
}
if (ts.isCallExpression(node)) {
const calleeName = propertyAccessName(node.expression);
if (
calleeName &&
legacyNames.has(calleeName) &&
ts.isIdentifier(unwrapExpression(node.expression))
) {
violations.push({
line: toLine(sourceFile, node.expression),
reason: `calls ${subject} "${calleeName}"`,
});
}
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
return violations;
}
function findNamedSessionStoreViolations(content, fileName, legacyNames, legacyKind) {
return findNamedBoundaryViolations(
content,
fileName,
legacyNames,
`legacy session store ${legacyKind}`,
);
}
export function findSessionAccessorBoundaryViolations(content, fileName = "source.ts") {
const legacyNames = legacyNamesForFile(fileName);
const legacyKind = legacyNames === legacyWholeStoreAccessNames ? "access" : "reader";
return findNamedSessionStoreViolations(content, fileName, legacyNames, legacyKind);
}
export function findSessionAccessorWriteBoundaryViolations(content, fileName = "source.ts") {
return findNamedSessionStoreViolations(content, fileName, legacyWriterNames, "writer");
}
export function findTranscriptWriterBoundaryViolations(content, fileName = "source.ts") {
return findNamedBoundaryViolations(
content,
fileName,
legacyTranscriptWriterNames,
"legacy transcript writer",
);
}
export function findGatewaySessionCreateLifecycleViolations(content, fileName = "source.ts") {
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
const violations = [];
const visitCreateHandler = (node) => {
if (ts.isCallExpression(node)) {
const calleeName = propertyAccessName(node.expression);
if (calleeName && sessionCreateLifecycleWriterNames.has(calleeName)) {
violations.push({
line: toLine(sourceFile, node.expression),
reason: `calls legacy sessions.create lifecycle writer "${calleeName}"`,
});
}
}
ts.forEachChild(node, visitCreateHandler);
};
const visit = (node) => {
if (
ts.isPropertyAssignment(node) &&
ts.isStringLiteralLike(node.name) &&
node.name.text === "sessions.create"
) {
visitCreateHandler(node.initializer);
return;
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
return violations;
}
export function findSessionCompactManualTrimBoundaryViolations(content, fileName = "source.ts") {
return findNamedSessionStoreViolations(
content,
fileName,
legacyManualCompactTrimNames,
"manual compact trim",
);
}
export function findSessionLifecycleCleanupBoundaryViolations(content, fileName = "source.ts") {
return findNamedSessionStoreViolations(
content,
fileName,
legacyLifecycleCleanupNames,
"lifecycle cleanup",
);
}
export async function main() {
const repoRoot = resolveRepoRoot(import.meta.url);
const readSourceRoots = resolveSourceRoots(repoRoot, [
"extensions/discord/src/monitor",
"extensions/telegram/src",
"src/agents",
"src/auto-reply",
"src/commands",
"src/config/sessions",
"src/cron",
"src/gateway",
"src/infra",
"src/tui",
]);
const writeSourceRoots = resolveSourceRoots(repoRoot, [
"src/agents",
"src/auto-reply",
"src/config/sessions",
"src/tui",
]);
const transcriptWriterSourceRoots = resolveSourceRoots(repoRoot, [
"src/agents/command",
"src/agents/embedded-agent-runner",
"src/config/sessions",
"src/gateway/server-methods",
"src/sessions",
]);
const readViolations = await collectFileViolations({
repoRoot,
sourceRoots: readSourceRoots,
skipFile: (filePath) => {
const relativePath = normalizeRelativePath(path.relative(repoRoot, filePath));
return (
!migratedSessionAccessorFiles.has(relativePath) &&
!migratedBundledPluginSessionAccessorFiles.has(relativePath)
);
},
findViolations: findSessionAccessorBoundaryViolations,
});
const writeViolations = await collectFileViolations({
repoRoot,
sourceRoots: writeSourceRoots,
skipFile: (filePath) =>
!migratedSessionAccessorWriteFiles.has(
normalizeRelativePath(path.relative(repoRoot, filePath)),
),
findViolations: findSessionAccessorWriteBoundaryViolations,
});
const transcriptWriterViolations = await collectFileViolations({
repoRoot,
sourceRoots: transcriptWriterSourceRoots,
skipFile: (filePath) =>
!migratedTranscriptWriterFiles.has(normalizeRelativePath(path.relative(repoRoot, filePath))),
findViolations: findTranscriptWriterBoundaryViolations,
});
const sessionCreateLifecycleViolations = await collectFileViolations({
repoRoot,
sourceRoots: resolveSourceRoots(repoRoot, ["src/gateway/server-methods"]),
skipFile: (filePath) =>
normalizeRelativePath(path.relative(repoRoot, filePath)) !==
"src/gateway/server-methods/sessions.ts",
findViolations: findGatewaySessionCreateLifecycleViolations,
});
const manualCompactTrimViolations = await collectFileViolations({
repoRoot,
sourceRoots: resolveSourceRoots(repoRoot, ["src/gateway/server-methods"]),
skipFile: (filePath) =>
!migratedSessionCompactManualTrimFiles.has(
normalizeRelativePath(path.relative(repoRoot, filePath)),
),
findViolations: findSessionCompactManualTrimBoundaryViolations,
});
const lifecycleCleanupViolations = await collectFileViolations({
repoRoot,
sourceRoots: readSourceRoots,
skipFile: (filePath) =>
!migratedSessionLifecycleCleanupFiles.has(
normalizeRelativePath(path.relative(repoRoot, filePath)),
),
findViolations: findSessionLifecycleCleanupBoundaryViolations,
});
const violations = [
...readViolations,
...writeViolations,
...transcriptWriterViolations,
...sessionCreateLifecycleViolations,
...manualCompactTrimViolations,
...lifecycleCleanupViolations,
];
if (violations.length === 0) {
console.log("session accessor boundary guard passed.");
return;
}
console.error("Found legacy session store usage in session-accessor migrated files:");
for (const violation of violations) {
console.error(`- ${violation.path}:${violation.line}: ${violation.reason}`);
}
console.error(
"Use src/config/sessions/session-accessor.ts helpers for migrated read/write and transcript-writer paths. Expand this ratchet only after a slice migrates more files.",
);
process.exit(1);
}
runAsScript(import.meta.url, main);