mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor: route transcript readers through facade
This commit is contained in:
@@ -1365,6 +1365,8 @@ jobs:
|
||||
boundary_shard: 2/4,3/4,4/4
|
||||
- check_name: check-session-accessor-boundary
|
||||
group: session-accessor-boundary
|
||||
- check_name: check-session-transcript-reader-boundary
|
||||
group: session-transcript-reader-boundary
|
||||
- check_name: check-additional-extension-channels
|
||||
group: extension-channels
|
||||
- check_name: check-additional-extension-bundled
|
||||
@@ -1520,6 +1522,9 @@ jobs:
|
||||
run_check "lint:tmp:session-accessor-boundary" pnpm run lint:tmp:session-accessor-boundary
|
||||
fi
|
||||
;;
|
||||
session-transcript-reader-boundary)
|
||||
run_check "lint:tmp:session-transcript-reader-boundary" pnpm run lint:tmp:session-transcript-reader-boundary
|
||||
;;
|
||||
extension-channels)
|
||||
run_check "lint:extensions:channels" pnpm run lint:extensions:channels
|
||||
;;
|
||||
|
||||
@@ -1598,6 +1598,7 @@
|
||||
"lint:tmp:no-raw-channel-fetch": "node scripts/check-no-raw-channel-fetch.mjs",
|
||||
"lint:tmp:no-raw-http2-imports": "node scripts/check-no-raw-http2-imports.mjs",
|
||||
"lint:tmp:session-accessor-boundary": "node scripts/check-session-accessor-boundary.mjs",
|
||||
"lint:tmp:session-transcript-reader-boundary": "node scripts/check-session-transcript-reader-boundary.mjs",
|
||||
"lint:tmp:tsgo-core-boundary": "node scripts/check-tsgo-core-boundary.mjs",
|
||||
"lint:ui:no-raw-window-open": "node scripts/check-no-raw-window-open.mjs",
|
||||
"lint:web-fetch-provider-boundaries": "node scripts/check-web-fetch-provider-boundaries.mjs",
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
#!/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 legacyTranscriptReaderModules = new Set([
|
||||
"../gateway/session-utils.js",
|
||||
"../gateway/session-utils.fs.js",
|
||||
"../../gateway/session-utils.js",
|
||||
"../../gateway/session-utils.fs.js",
|
||||
"./session-utils.js",
|
||||
"./session-utils.fs.js",
|
||||
"../session-utils.js",
|
||||
"../session-utils.fs.js",
|
||||
]);
|
||||
|
||||
const transcriptReaderNames = new Set([
|
||||
"attachOpenClawTranscriptMeta",
|
||||
"capArrayByJsonBytes",
|
||||
"readFirstUserMessageFromTranscript",
|
||||
"readLatestRecentSessionUsageFromTranscriptAsync",
|
||||
"readLatestSessionUsageFromTranscript",
|
||||
"readLatestSessionUsageFromTranscriptAsync",
|
||||
"readRecentSessionMessages",
|
||||
"readRecentSessionMessagesAsync",
|
||||
"readRecentSessionMessagesWithStats",
|
||||
"readRecentSessionMessagesWithStatsAsync",
|
||||
"readRecentSessionTranscriptLines",
|
||||
"readRecentSessionUsageFromTranscript",
|
||||
"readRecentSessionUsageFromTranscriptAsync",
|
||||
"readSessionMessageByIdAsync",
|
||||
"readSessionMessageCount",
|
||||
"readSessionMessageCountAsync",
|
||||
"readSessionMessages",
|
||||
"readSessionMessagesAsync",
|
||||
"readSessionMessagesWithSourceAsync",
|
||||
"readSessionPreviewItemsFromTranscript",
|
||||
"readSessionTitleFieldsFromTranscript",
|
||||
"readSessionTitleFieldsFromTranscriptAsync",
|
||||
"visitSessionMessages",
|
||||
"visitSessionMessagesAsync",
|
||||
]);
|
||||
|
||||
export const migratedSessionTranscriptReaderFiles = new Set([
|
||||
"src/agents/main-session-restart-recovery.ts",
|
||||
"src/agents/subagent-announce-output.test.ts",
|
||||
"src/agents/subagent-announce-output.ts",
|
||||
"src/agents/subagent-announce.runtime.ts",
|
||||
"src/agents/subagent-orphan-recovery.test.ts",
|
||||
"src/agents/subagent-orphan-recovery.ts",
|
||||
"src/agents/tools/embedded-gateway-stub.runtime.ts",
|
||||
"src/agents/tools/embedded-gateway-stub.test.ts",
|
||||
"src/agents/tools/embedded-gateway-stub.ts",
|
||||
"src/agents/tools/sessions-history-tool.ts",
|
||||
"src/agents/tools/sessions-list-tool.ts",
|
||||
"src/gateway/cli-session-history.claude.ts",
|
||||
"src/gateway/gateway-models.profiles.live.test.ts",
|
||||
"src/gateway/managed-image-attachments.test.ts",
|
||||
"src/gateway/managed-image-attachments.ts",
|
||||
"src/gateway/server-methods/artifacts.test.ts",
|
||||
"src/gateway/server-methods/artifacts.ts",
|
||||
"src/gateway/server-methods/chat.ts",
|
||||
"src/gateway/server-methods/sessions-files.test.ts",
|
||||
"src/gateway/server-methods/sessions-files.ts",
|
||||
"src/gateway/server-methods/sessions.ts",
|
||||
"src/gateway/server-session-events.ts",
|
||||
"src/gateway/session-history-state.test.ts",
|
||||
"src/gateway/session-history-state.ts",
|
||||
"src/gateway/session-reset-service.ts",
|
||||
"src/gateway/session-utils.ts",
|
||||
"src/gateway/sessions-history-http.revocation.test.ts",
|
||||
"src/gateway/sessions-history-http.ts",
|
||||
"src/status/status-message.ts",
|
||||
"src/tui/embedded-backend.test.ts",
|
||||
"src/tui/embedded-backend.ts",
|
||||
]);
|
||||
|
||||
function normalizeRelativePath(filePath) {
|
||||
return filePath.replaceAll(path.sep, "/");
|
||||
}
|
||||
|
||||
function importedModuleName(node) {
|
||||
return node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)
|
||||
? node.moduleSpecifier.text
|
||||
: 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 destructuresLegacyNamespace(node, legacyNamespaces) {
|
||||
const pattern = node.parent;
|
||||
const declaration = pattern?.parent;
|
||||
if (
|
||||
!pattern ||
|
||||
!ts.isObjectBindingPattern(pattern) ||
|
||||
!declaration ||
|
||||
!ts.isVariableDeclaration(declaration) ||
|
||||
!declaration.initializer
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const initializer = unwrapExpression(declaration.initializer);
|
||||
return ts.isIdentifier(initializer) && legacyNamespaces.has(initializer.text);
|
||||
}
|
||||
|
||||
export function findSessionTranscriptReaderBoundaryViolations(content, fileName = "source.ts") {
|
||||
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true);
|
||||
const violations = [];
|
||||
const legacyNamespaces = new Set();
|
||||
|
||||
const visit = (node) => {
|
||||
if (ts.isImportDeclaration(node)) {
|
||||
const moduleName = importedModuleName(node);
|
||||
const namedBindings = node.importClause?.namedBindings;
|
||||
if (moduleName && legacyTranscriptReaderModules.has(moduleName) && namedBindings) {
|
||||
if (ts.isNamedImports(namedBindings)) {
|
||||
for (const specifier of namedBindings.elements) {
|
||||
const importedName = specifier.propertyName?.text ?? specifier.name.text;
|
||||
if (transcriptReaderNames.has(importedName)) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, specifier),
|
||||
reason: `imports transcript reader "${importedName}" from legacy module "${moduleName}"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (ts.isNamespaceImport(namedBindings)) {
|
||||
legacyNamespaces.add(namedBindings.name.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ts.isExportDeclaration(node)) {
|
||||
const moduleName = importedModuleName(node);
|
||||
if (moduleName && legacyTranscriptReaderModules.has(moduleName)) {
|
||||
const exportClause = node.exportClause;
|
||||
if (!exportClause) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, node),
|
||||
reason: `re-exports transcript readers from legacy module "${moduleName}"`,
|
||||
});
|
||||
} else if (ts.isNamedExports(exportClause)) {
|
||||
for (const specifier of exportClause.elements) {
|
||||
const exportedName = specifier.propertyName?.text ?? specifier.name.text;
|
||||
if (transcriptReaderNames.has(exportedName)) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, specifier),
|
||||
reason: `re-exports transcript reader "${exportedName}" from legacy module "${moduleName}"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (ts.isNamespaceExport(exportClause)) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, exportClause),
|
||||
reason: `re-exports transcript reader namespace from legacy module "${moduleName}"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ts.isBindingElement(node)) {
|
||||
const name = bindingName(node);
|
||||
if (
|
||||
name &&
|
||||
transcriptReaderNames.has(name) &&
|
||||
destructuresLegacyNamespace(node, legacyNamespaces)
|
||||
) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, node),
|
||||
reason: `aliases legacy transcript reader "${name}"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (ts.isPropertyAccessExpression(node)) {
|
||||
const receiver = unwrapExpression(node.expression);
|
||||
if (
|
||||
ts.isIdentifier(receiver) &&
|
||||
legacyNamespaces.has(receiver.text) &&
|
||||
transcriptReaderNames.has(node.name.text)
|
||||
) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, node.name),
|
||||
reason: `references legacy transcript reader "${node.name.text}"`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
ts.isElementAccessExpression(node) &&
|
||||
ts.isIdentifier(unwrapExpression(node.expression)) &&
|
||||
legacyNamespaces.has(unwrapExpression(node.expression).text) &&
|
||||
ts.isStringLiteral(node.argumentExpression) &&
|
||||
transcriptReaderNames.has(node.argumentExpression.text)
|
||||
) {
|
||||
violations.push({
|
||||
line: toLine(sourceFile, node.argumentExpression),
|
||||
reason: `references legacy transcript reader "${node.argumentExpression.text}"`,
|
||||
});
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
|
||||
visit(sourceFile);
|
||||
return violations;
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
const repoRoot = resolveRepoRoot(import.meta.url);
|
||||
const sourceRoots = resolveSourceRoots(repoRoot, [
|
||||
"src/agents",
|
||||
"src/gateway",
|
||||
"src/status",
|
||||
"src/tui",
|
||||
]);
|
||||
const violations = await collectFileViolations({
|
||||
repoRoot,
|
||||
sourceRoots,
|
||||
includeTests: true,
|
||||
skipFile: (filePath) =>
|
||||
!migratedSessionTranscriptReaderFiles.has(
|
||||
normalizeRelativePath(path.relative(repoRoot, filePath)),
|
||||
),
|
||||
findViolations: findSessionTranscriptReaderBoundaryViolations,
|
||||
});
|
||||
|
||||
if (violations.length === 0) {
|
||||
console.log("session transcript reader boundary guard passed.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("Found legacy transcript reader usage in migrated files:");
|
||||
for (const violation of violations) {
|
||||
console.error(`- ${violation.path}:${violation.line}: ${violation.reason}`);
|
||||
}
|
||||
console.error(
|
||||
"Use src/gateway/session-transcript-readers.ts for migrated transcript reader paths. Expand this ratchet only after a slice migrates more files.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
runAsScript(import.meta.url, main);
|
||||
@@ -124,6 +124,7 @@ export async function collectTypeScriptFilesFromRoots(sourceRoots, options = {})
|
||||
*/
|
||||
export async function collectFileViolations(params) {
|
||||
const files = await collectTypeScriptFilesFromRoots(params.sourceRoots, {
|
||||
includeTests: params.includeTests,
|
||||
extraTestSuffixes: params.extraTestSuffixes,
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from "../config/sessions.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import { readSessionMessagesAsync } from "../gateway/session-utils.fs.js";
|
||||
import { readSessionMessagesAsync } from "../gateway/session-transcript-readers.js";
|
||||
import { resolveGatewaySessionStoreTarget } from "../gateway/session-utils.js";
|
||||
import {
|
||||
getAgentEventLifecycleGeneration,
|
||||
@@ -27,7 +27,12 @@ import {
|
||||
} from "../infra/agent-events.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { CommandLane } from "../process/lanes.js";
|
||||
import { isAcpSessionKey, isCronSessionKey, isSubagentSessionKey } from "../routing/session-key.js";
|
||||
import {
|
||||
isAcpSessionKey,
|
||||
isCronSessionKey,
|
||||
isSubagentSessionKey,
|
||||
resolveAgentIdFromSessionKey,
|
||||
} from "../routing/session-key.js";
|
||||
import { resolveSendPolicy } from "../sessions/send-policy.js";
|
||||
import {
|
||||
deliveryContextFromSession,
|
||||
@@ -785,9 +790,12 @@ async function recoverStore(params: {
|
||||
let messages: unknown[];
|
||||
try {
|
||||
messages = await readSessionMessagesAsync(
|
||||
entry.sessionId,
|
||||
params.storePath,
|
||||
entry.sessionFile,
|
||||
{
|
||||
agentId: resolveAgentIdFromSessionKey(sessionKey),
|
||||
sessionFile: entry.sessionFile,
|
||||
sessionId: entry.sessionId,
|
||||
storePath: params.storePath,
|
||||
},
|
||||
{
|
||||
mode: "recent",
|
||||
maxMessages: 20,
|
||||
|
||||
@@ -216,9 +216,10 @@ describe("readSubagentOutput", () => {
|
||||
}),
|
||||
).resolves.toBe("fresh recovered output");
|
||||
expect(deps.readSessionMessagesAsync).toHaveBeenCalledWith(
|
||||
"agent:main:subagent:child",
|
||||
undefined,
|
||||
"/tmp/openclaw-internal-run.jsonl",
|
||||
{
|
||||
sessionFile: "/tmp/openclaw-internal-run.jsonl",
|
||||
sessionId: "agent:main:subagent:child",
|
||||
},
|
||||
{ mode: "recent", maxMessages: 100, maxBytes: 1024 * 1024 },
|
||||
);
|
||||
expect(deps.callGateway).not.toHaveBeenCalled();
|
||||
|
||||
@@ -207,9 +207,10 @@ export async function readSubagentOutput(
|
||||
let messages: unknown[] | undefined;
|
||||
if (options?.sessionFile) {
|
||||
const transcriptMessages = await subagentAnnounceOutputDeps.readSessionMessagesAsync(
|
||||
sessionKey,
|
||||
undefined,
|
||||
options.sessionFile,
|
||||
{
|
||||
sessionFile: options.sessionFile,
|
||||
sessionId: sessionKey,
|
||||
},
|
||||
{
|
||||
mode: "recent",
|
||||
maxMessages: 100,
|
||||
|
||||
@@ -12,7 +12,7 @@ export {
|
||||
resolveStorePath,
|
||||
} from "../config/sessions.js";
|
||||
export { callGateway } from "../gateway/call.js";
|
||||
export { readSessionMessagesAsync } from "../gateway/session-utils.fs.js";
|
||||
export { readSessionMessagesAsync } from "../gateway/session-transcript-readers.js";
|
||||
export { dispatchGatewayMethodInProcess } from "../gateway/server-plugins.js";
|
||||
export {
|
||||
isEmbeddedAgentRunActive,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as sessions from "../config/sessions.js";
|
||||
import * as gateway from "../gateway/call.js";
|
||||
import * as sessionUtils from "../gateway/session-utils.fs.js";
|
||||
import * as sessionUtils from "../gateway/session-transcript-readers.js";
|
||||
import { resolveInternalSessionEffectsTranscriptPath } from "./internal-session-effects.js";
|
||||
import * as announceDelivery from "./subagent-announce-delivery.js";
|
||||
import {
|
||||
@@ -32,7 +32,7 @@ vi.mock("../gateway/call.js", () => ({
|
||||
callGateway: vi.fn(async () => ({ runId: "test-run-id" })),
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/session-utils.fs.js", () => ({
|
||||
vi.mock("../gateway/session-transcript-readers.js", () => ({
|
||||
readSessionMessagesAsync: vi.fn(async () => []),
|
||||
}));
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
type SessionEntry,
|
||||
} from "../config/sessions.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
import { readSessionMessagesAsync } from "../gateway/session-utils.fs.js";
|
||||
import { readSessionMessagesAsync } from "../gateway/session-transcript-readers.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { resolveInternalSessionEffectsTranscriptPath } from "./internal-session-effects.js";
|
||||
@@ -294,9 +294,12 @@ export async function recoverOrphanedSubagentSessions(params: {
|
||||
log.info(`found orphaned subagent session: ${childSessionKey} (run=${runId})`);
|
||||
|
||||
const messages = await readSessionMessagesAsync(
|
||||
entry.sessionId,
|
||||
storePath,
|
||||
entry.sessionFile,
|
||||
{
|
||||
agentId: resolveAgentIdFromSessionKey(childSessionKey),
|
||||
sessionFile: entry.sessionFile,
|
||||
sessionId: entry.sessionId,
|
||||
storePath,
|
||||
},
|
||||
{
|
||||
mode: "recent",
|
||||
maxMessages: 200,
|
||||
|
||||
@@ -18,12 +18,14 @@ export {
|
||||
enforceChatHistoryFinalBudget,
|
||||
replaceOversizedChatHistoryMessages,
|
||||
} from "../../gateway/server-methods/chat.js";
|
||||
export { capArrayByJsonBytes } from "../../gateway/session-utils.fs.js";
|
||||
export {
|
||||
capArrayByJsonBytes,
|
||||
readSessionMessagesAsync,
|
||||
} from "../../gateway/session-transcript-readers.js";
|
||||
export {
|
||||
listSessionsFromStoreAsync,
|
||||
loadCombinedSessionStoreForGateway,
|
||||
loadSessionEntry,
|
||||
readSessionMessagesAsync,
|
||||
resolveSessionModelRef,
|
||||
} from "../../gateway/session-utils.js";
|
||||
export { resolveSessionKeyFromResolveParams } from "../../gateway/sessions-resolve.js";
|
||||
|
||||
@@ -124,9 +124,12 @@ describe("embedded gateway stub", () => {
|
||||
maxMessages: 200,
|
||||
});
|
||||
expect(runtime.readSessionMessagesAsync).toHaveBeenCalledWith(
|
||||
"sess-main",
|
||||
"/tmp/openclaw-sessions.json",
|
||||
undefined,
|
||||
{
|
||||
agentId: "main",
|
||||
sessionFile: undefined,
|
||||
sessionId: "sess-main",
|
||||
storePath: "/tmp/openclaw-sessions.json",
|
||||
},
|
||||
{
|
||||
mode: "recent",
|
||||
maxMessages: 200,
|
||||
@@ -187,9 +190,12 @@ describe("embedded gateway stub", () => {
|
||||
maxMessages: 1,
|
||||
});
|
||||
expect(runtime.readSessionMessagesAsync).toHaveBeenCalledWith(
|
||||
"sess-main",
|
||||
"/tmp/openclaw-sessions.json",
|
||||
undefined,
|
||||
{
|
||||
agentId: "main",
|
||||
sessionFile: undefined,
|
||||
sessionId: "sess-main",
|
||||
storePath: "/tmp/openclaw-sessions.json",
|
||||
},
|
||||
{
|
||||
mode: "recent",
|
||||
maxMessages: 1,
|
||||
@@ -217,9 +223,12 @@ describe("embedded gateway stub", () => {
|
||||
maxMessages: 2,
|
||||
});
|
||||
expect(runtime.readSessionMessagesAsync).toHaveBeenCalledWith(
|
||||
"sess-main",
|
||||
"/tmp/openclaw-sessions.json",
|
||||
undefined,
|
||||
{
|
||||
agentId: "main",
|
||||
sessionFile: undefined,
|
||||
sessionId: "sess-main",
|
||||
storePath: "/tmp/openclaw-sessions.json",
|
||||
},
|
||||
{
|
||||
mode: "recent",
|
||||
maxMessages: 2,
|
||||
|
||||
@@ -9,7 +9,10 @@ import type {
|
||||
} from "../../../packages/gateway-protocol/src/index.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { CallGatewayOptions } from "../../gateway/call.js";
|
||||
import type { ReadSessionMessagesAsyncOptions } from "../../gateway/session-utils.fs.js";
|
||||
import type {
|
||||
ReadSessionMessagesAsyncOptions,
|
||||
SessionTranscriptReadScope,
|
||||
} from "../../gateway/session-transcript-readers.js";
|
||||
import type { SessionsListResult } from "../../gateway/session-utils.types.js";
|
||||
import type { SessionsResolveResult } from "../../gateway/sessions-resolve.js";
|
||||
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
||||
@@ -71,9 +74,7 @@ interface EmbeddedGatewayRuntime {
|
||||
entry: Record<string, unknown> | undefined;
|
||||
};
|
||||
readSessionMessagesAsync: (
|
||||
sessionId: string,
|
||||
storePath: string,
|
||||
sessionFile: string | undefined,
|
||||
scope: SessionTranscriptReadScope,
|
||||
opts: ReadSessionMessagesAsyncOptions,
|
||||
) => Promise<unknown[]>;
|
||||
resolveSessionModelRef: (
|
||||
@@ -158,9 +159,12 @@ async function handleChatHistory(params: Record<string, unknown>): Promise<{
|
||||
const localMessages =
|
||||
sessionId && storePath
|
||||
? await rt.readSessionMessagesAsync(
|
||||
sessionId,
|
||||
storePath,
|
||||
entry?.sessionFile as string | undefined,
|
||||
{
|
||||
agentId: sessionAgentId,
|
||||
sessionFile: entry?.sessionFile as string | undefined,
|
||||
sessionId,
|
||||
storePath,
|
||||
},
|
||||
{
|
||||
mode: "recent",
|
||||
maxMessages: max,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Type } from "typebox";
|
||||
import { getRuntimeConfig } from "../../config/config.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { capArrayByJsonBytes } from "../../gateway/session-utils.fs.js";
|
||||
import { capArrayByJsonBytes } from "../../gateway/session-transcript-readers.js";
|
||||
import { jsonUtf8Bytes } from "../../infra/json-utf8-bytes.js";
|
||||
import { redactToolPayloadText } from "../../logging/redact.js";
|
||||
import { truncateUtf16Safe } from "../../utils.js";
|
||||
|
||||
@@ -18,10 +18,8 @@ import {
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import {
|
||||
deriveSessionTitle,
|
||||
readSessionTitleFieldsFromTranscriptAsync,
|
||||
} from "../../gateway/session-utils.js";
|
||||
import { readSessionTitleFieldsFromTranscriptAsync } from "../../gateway/session-transcript-readers.js";
|
||||
import { deriveSessionTitle } from "../../gateway/session-utils.js";
|
||||
import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
|
||||
import { deliveryContextFromSession } from "../../utils/delivery-context.shared.js";
|
||||
import {
|
||||
@@ -385,12 +383,12 @@ export function createSessionsListTool(opts?: {
|
||||
return;
|
||||
}
|
||||
const target = titleTargets[next];
|
||||
const fields = await readSessionTitleFieldsFromTranscriptAsync(
|
||||
target.sessionId,
|
||||
const fields = await readSessionTitleFieldsFromTranscriptAsync({
|
||||
agentId: target.agentId,
|
||||
sessionFile: target.sessionFile,
|
||||
sessionId: target.sessionId,
|
||||
storePath,
|
||||
target.sessionFile,
|
||||
target.agentId,
|
||||
);
|
||||
});
|
||||
if (includeDerivedTitles && !target.row.derivedTitle) {
|
||||
target.row.derivedTitle = deriveSessionTitle(
|
||||
target.titleEntry,
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
type ToolContentBlock,
|
||||
} from "../chat/tool-content.js";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
import { attachOpenClawTranscriptMeta } from "./session-utils.fs.js";
|
||||
import { attachOpenClawTranscriptMeta } from "./session-transcript-readers.js";
|
||||
|
||||
export const CLAUDE_CLI_PROVIDER = "claude-cli";
|
||||
const CLAUDE_PROJECTS_RELATIVE_DIR = path.join(".claude", "projects");
|
||||
|
||||
@@ -69,7 +69,8 @@ import {
|
||||
shouldRetryToolReadProbe,
|
||||
} from "./live-tool-probe-utils.js";
|
||||
import { startGatewayServer } from "./server.impl.js";
|
||||
import { loadSessionEntry, readSessionMessagesAsync } from "./session-utils.js";
|
||||
import { readSessionMessagesAsync } from "./session-transcript-readers.js";
|
||||
import { loadSessionEntry } from "./session-utils.js";
|
||||
|
||||
const ZAI_FALLBACK = isTruthyEnvValue(process.env.OPENCLAW_LIVE_GATEWAY_ZAI_FALLBACK);
|
||||
const REQUIRE_PROFILE_KEYS = isLiveProfileKeyModeEnabled();
|
||||
@@ -2124,10 +2125,17 @@ async function readSessionAssistantTexts(sessionKey: string, modelKey?: string):
|
||||
if (!entry?.sessionId) {
|
||||
return [];
|
||||
}
|
||||
const messages = await readSessionMessagesAsync(entry.sessionId, storePath, entry.sessionFile, {
|
||||
mode: "full",
|
||||
reason: "live model assistant text verification",
|
||||
});
|
||||
const messages = await readSessionMessagesAsync(
|
||||
{
|
||||
sessionFile: entry.sessionFile,
|
||||
sessionId: entry.sessionId,
|
||||
storePath,
|
||||
},
|
||||
{
|
||||
mode: "full",
|
||||
reason: "live model assistant text verification",
|
||||
},
|
||||
);
|
||||
const assistantTexts: string[] = [];
|
||||
for (const message of messages) {
|
||||
if (!message || typeof message !== "object") {
|
||||
|
||||
@@ -34,12 +34,15 @@ vi.mock("./http-utils.js", () => ({
|
||||
|
||||
vi.mock("./session-utils.js", () => ({
|
||||
loadSessionEntry: loadSessionEntryMock,
|
||||
resolveSessionHistoryTranscriptPathAsync: resolveSessionHistoryTranscriptPathMock,
|
||||
}));
|
||||
|
||||
vi.mock("./session-transcript-readers.js", () => ({
|
||||
readSessionMessagesAsync: readSessionMessagesMock,
|
||||
readSessionMessagesWithSourceAsync: async (...args: unknown[]) => ({
|
||||
messages: await readSessionMessagesMock(...args),
|
||||
transcriptPath: await resolveSessionHistoryTranscriptPathMock(...args),
|
||||
}),
|
||||
resolveSessionHistoryTranscriptPathAsync: resolveSessionHistoryTranscriptPathMock,
|
||||
}));
|
||||
|
||||
const {
|
||||
@@ -287,9 +290,12 @@ describe("handleManagedOutgoingImageHttpRequest", () => {
|
||||
expect(result.headers["content-disposition"]).toContain("inline");
|
||||
expect(result.body.toString("utf-8")).toBe("original-image");
|
||||
expect(readSessionMessagesMock).toHaveBeenCalledWith(
|
||||
"sess-1",
|
||||
path.join(stateDir, "gateway-sessions.json"),
|
||||
"session.jsonl",
|
||||
{
|
||||
agentId: undefined,
|
||||
sessionFile: "session.jsonl",
|
||||
sessionId: "sess-1",
|
||||
storePath: path.join(stateDir, "gateway-sessions.json"),
|
||||
},
|
||||
expect.objectContaining({ allowResetArchiveFallback: true }),
|
||||
);
|
||||
});
|
||||
@@ -1056,9 +1062,12 @@ describe("cleanupManagedOutgoingImageRecords", () => {
|
||||
expect(result.retainedCount).toBe(1);
|
||||
await expect(fs.access(fixture.originalPath)).resolves.toBeUndefined();
|
||||
expect(readSessionMessagesMock).toHaveBeenCalledWith(
|
||||
"sess-main",
|
||||
path.join(stateDir, "gateway-sessions.json"),
|
||||
"/tmp/sess-main.jsonl",
|
||||
{
|
||||
agentId: undefined,
|
||||
sessionFile: "/tmp/sess-main.jsonl",
|
||||
sessionId: "sess-main",
|
||||
storePath: path.join(stateDir, "gateway-sessions.json"),
|
||||
},
|
||||
expect.objectContaining({ allowResetArchiveFallback: true }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -28,11 +28,8 @@ import {
|
||||
resolveOpenAiCompatibleHttpSenderIsOwner,
|
||||
} from "./http-utils.js";
|
||||
import { authorizeOperatorScopesForMethod } from "./method-scopes.js";
|
||||
import {
|
||||
loadSessionEntry,
|
||||
readSessionMessagesWithSourceAsync,
|
||||
resolveSessionHistoryTranscriptPathAsync,
|
||||
} from "./session-utils.js";
|
||||
import { readSessionMessagesWithSourceAsync } from "./session-transcript-readers.js";
|
||||
import { loadSessionEntry, resolveSessionHistoryTranscriptPathAsync } from "./session-utils.js";
|
||||
|
||||
const OUTGOING_IMAGE_ROUTE_PREFIX = "/api/chat/media/outgoing";
|
||||
const DEFAULT_TRANSIENT_OUTGOING_IMAGE_TTL_MS = 15 * 60 * 1000;
|
||||
@@ -732,9 +729,12 @@ async function getSessionManagedOutgoingAttachmentIndex(
|
||||
}
|
||||
|
||||
const readResult = await readSessionMessagesWithSourceAsync(
|
||||
sessionId,
|
||||
storePath,
|
||||
entry.sessionFile,
|
||||
{
|
||||
agentId,
|
||||
sessionFile: entry.sessionFile,
|
||||
sessionId,
|
||||
storePath,
|
||||
},
|
||||
{
|
||||
mode: "full",
|
||||
reason: "managed outgoing attachment index",
|
||||
|
||||
@@ -20,6 +20,15 @@ vi.mock("../session-utils.js", async () => {
|
||||
return {
|
||||
...actual,
|
||||
loadSessionEntry: hoisted.loadSessionEntry,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../session-transcript-readers.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../session-transcript-readers.js")>(
|
||||
"../session-transcript-readers.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
visitSessionMessagesAsync: hoisted.visitSessionMessagesAsync,
|
||||
};
|
||||
});
|
||||
@@ -207,12 +216,10 @@ describe("artifacts RPC handlers", () => {
|
||||
});
|
||||
|
||||
function mockedMessages(messages: unknown[]) {
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
messages.forEach((message, index) => visit(message, index + 1));
|
||||
return messages.length;
|
||||
},
|
||||
);
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(async (_scope, visit) => {
|
||||
messages.forEach((message, index) => visit(message, index + 1));
|
||||
return messages.length;
|
||||
});
|
||||
}
|
||||
|
||||
it("lists stable transcript artifact summaries by sessionKey", async () => {
|
||||
@@ -235,9 +242,12 @@ describe("artifacts RPC handlers", () => {
|
||||
expect(artifact?.id).toMatch(/^artifact_/);
|
||||
expect(artifact).not.toHaveProperty("data");
|
||||
expect(hoisted.visitSessionMessagesAsync).toHaveBeenCalledWith(
|
||||
"sess-main",
|
||||
"/tmp/sessions.json",
|
||||
"/tmp/sess-main.jsonl",
|
||||
{
|
||||
agentId: "main",
|
||||
sessionFile: "/tmp/sess-main.jsonl",
|
||||
sessionId: "sess-main",
|
||||
storePath: "/tmp/sessions.json",
|
||||
},
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ cache: "skip" }),
|
||||
);
|
||||
|
||||
@@ -27,7 +27,8 @@ import {
|
||||
resolveSessionStoreKey,
|
||||
resolveStoredSessionKeyForAgentStore,
|
||||
} from "../session-store-key.js";
|
||||
import { loadSessionEntry, visitSessionMessagesAsync } from "../session-utils.js";
|
||||
import { visitSessionMessagesAsync } from "../session-transcript-readers.js";
|
||||
import { loadSessionEntry } from "../session-utils.js";
|
||||
import type { GatewayRequestHandlers, RespondFn } from "./types.js";
|
||||
import { assertValidParams } from "./validation.js";
|
||||
|
||||
@@ -481,9 +482,12 @@ async function loadArtifacts(
|
||||
}
|
||||
const artifacts: ArtifactRecord[] = [];
|
||||
await visitSessionMessagesAsync(
|
||||
sessionId,
|
||||
storePath,
|
||||
entry?.sessionFile,
|
||||
{
|
||||
agentId: resolved.agentId ?? resolveAgentIdFromSessionKey(sessionKey),
|
||||
sessionFile: entry?.sessionFile,
|
||||
sessionId,
|
||||
storePath,
|
||||
},
|
||||
(message, seq) => {
|
||||
collectArtifactsFromMessage({
|
||||
message,
|
||||
|
||||
@@ -153,15 +153,17 @@ import { resolveSessionHistoryTailReadOptions } from "../session-history-state.j
|
||||
import { readSessionTranscriptIndex } from "../session-transcript-index.fs.js";
|
||||
import {
|
||||
capArrayByJsonBytes,
|
||||
readSessionMessageByIdAsync,
|
||||
readRecentSessionMessagesAsync,
|
||||
readSessionMessagesAsync,
|
||||
} from "../session-transcript-readers.js";
|
||||
import {
|
||||
buildGatewaySessionInfo,
|
||||
getSessionDefaults,
|
||||
loadSessionEntry,
|
||||
listAgentsForGateway,
|
||||
readSessionMessageByIdAsync,
|
||||
readSessionMessagesAsync,
|
||||
resolveGatewayModelSupportsImages,
|
||||
resolveDeletedAgentIdFromSessionKey,
|
||||
readRecentSessionMessagesAsync,
|
||||
resolveSessionModelRef,
|
||||
resolveSessionStoreKey,
|
||||
} from "../session-utils.js";
|
||||
@@ -2489,6 +2491,7 @@ async function isChatMessageIdVisibleAfterHistoryFilters(params: {
|
||||
sessionId: string;
|
||||
storePath: string | undefined;
|
||||
sessionFile: string | undefined;
|
||||
agentId?: string;
|
||||
messageId: string;
|
||||
sessionStartedAt?: number;
|
||||
allowResetArchiveFallback?: boolean;
|
||||
@@ -2497,9 +2500,12 @@ async function isChatMessageIdVisibleAfterHistoryFilters(params: {
|
||||
return true;
|
||||
}
|
||||
const messages = await readSessionMessagesAsync(
|
||||
params.sessionId,
|
||||
params.storePath,
|
||||
params.sessionFile,
|
||||
{
|
||||
agentId: params.agentId,
|
||||
sessionFile: params.sessionFile,
|
||||
sessionId: params.sessionId,
|
||||
storePath: params.storePath,
|
||||
},
|
||||
{
|
||||
mode: "full",
|
||||
reason: "chat.message.get visibility",
|
||||
@@ -2613,11 +2619,19 @@ async function handleChatHistoryRequest({
|
||||
};
|
||||
const localMessages =
|
||||
sessionId && storePath
|
||||
? await readRecentSessionMessagesAsync(sessionId, storePath, entry?.sessionFile, {
|
||||
...localHistoryReadOptions,
|
||||
maxBytes: Math.max(maxHistoryBytes * 2, 1024 * 1024),
|
||||
allowResetArchiveFallback: true,
|
||||
})
|
||||
? await readRecentSessionMessagesAsync(
|
||||
{
|
||||
agentId: sessionAgentId,
|
||||
sessionFile: entry?.sessionFile,
|
||||
sessionId,
|
||||
storePath,
|
||||
},
|
||||
{
|
||||
...localHistoryReadOptions,
|
||||
maxBytes: Math.max(maxHistoryBytes * 2, 1024 * 1024),
|
||||
allowResetArchiveFallback: true,
|
||||
},
|
||||
)
|
||||
: [];
|
||||
const overreadContextMessage =
|
||||
localMessages.length > rawHistoryWindow.maxMessages ? localMessages[0] : undefined;
|
||||
@@ -2793,10 +2807,18 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionAgentId = resolveSessionAgentId({
|
||||
sessionKey,
|
||||
config: cfg,
|
||||
agentId: selectedAgent.agentId,
|
||||
});
|
||||
const resolved = await readSessionMessageByIdAsync(
|
||||
sessionId,
|
||||
storePath,
|
||||
entry?.sessionFile,
|
||||
{
|
||||
agentId: sessionAgentId,
|
||||
sessionFile: entry?.sessionFile,
|
||||
sessionId,
|
||||
storePath,
|
||||
},
|
||||
messageId,
|
||||
{ allowResetArchiveFallback: true },
|
||||
);
|
||||
@@ -2808,6 +2830,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
||||
sessionId,
|
||||
storePath,
|
||||
sessionFile: entry?.sessionFile,
|
||||
agentId: sessionAgentId,
|
||||
messageId,
|
||||
sessionStartedAt:
|
||||
typeof entry?.sessionStartedAt === "number" ? entry.sessionStartedAt : undefined,
|
||||
|
||||
@@ -22,6 +22,15 @@ vi.mock("../session-utils.js", async () => {
|
||||
return {
|
||||
...actual,
|
||||
loadSessionEntry: hoisted.loadSessionEntry,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../session-transcript-readers.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../session-transcript-readers.js")>(
|
||||
"../session-transcript-readers.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
visitSessionMessagesAsync: hoisted.visitSessionMessagesAsync,
|
||||
};
|
||||
});
|
||||
@@ -108,18 +117,16 @@ describe("sessions.files RPC handlers", () => {
|
||||
spawnedCwd: workspaceRoot,
|
||||
},
|
||||
});
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
[
|
||||
assistantToolCall("edit", { path: "ui/chat.ts" }),
|
||||
assistantToolCall("read", { path: "src/readme.md" }),
|
||||
assistantToolCall("apply_patch", {
|
||||
input: "*** Begin Patch\n*** Update File: package.json\n*** End Patch\n",
|
||||
}),
|
||||
].forEach((message, index) => visit(message, index + 1));
|
||||
return 3;
|
||||
},
|
||||
);
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(async (_scope, visit) => {
|
||||
[
|
||||
assistantToolCall("edit", { path: "ui/chat.ts" }),
|
||||
assistantToolCall("read", { path: "src/readme.md" }),
|
||||
assistantToolCall("apply_patch", {
|
||||
input: "*** Begin Patch\n*** Update File: package.json\n*** End Patch\n",
|
||||
}),
|
||||
].forEach((message, index) => visit(message, index + 1));
|
||||
return 3;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -154,29 +161,27 @@ describe("sessions.files RPC handlers", () => {
|
||||
});
|
||||
|
||||
it("collects touched files from existing transcript tool-call spellings", async () => {
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
visit(
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool_use", name: "read", input: { path: "src/readme.md" } },
|
||||
{ type: "toolcall", name: "edit", arguments: { path: "ui/vite.config.ts" } },
|
||||
{ type: "tool_use", name: "read", args: { path: "ui/chat.ts" } },
|
||||
{
|
||||
type: "tool_call",
|
||||
name: "apply_patch",
|
||||
input: {
|
||||
input: "*** Begin Patch\n*** Update File: package.json\n*** End Patch\n",
|
||||
},
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(async (_scope, visit) => {
|
||||
visit(
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool_use", name: "read", input: { path: "src/readme.md" } },
|
||||
{ type: "toolcall", name: "edit", arguments: { path: "ui/vite.config.ts" } },
|
||||
{ type: "tool_use", name: "read", args: { path: "ui/chat.ts" } },
|
||||
{
|
||||
type: "tool_call",
|
||||
name: "apply_patch",
|
||||
input: {
|
||||
input: "*** Begin Patch\n*** Update File: package.json\n*** End Patch\n",
|
||||
},
|
||||
],
|
||||
},
|
||||
1,
|
||||
);
|
||||
return 1;
|
||||
},
|
||||
);
|
||||
},
|
||||
],
|
||||
},
|
||||
1,
|
||||
);
|
||||
return 1;
|
||||
});
|
||||
|
||||
const payload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
@@ -193,21 +198,19 @@ describe("sessions.files RPC handlers", () => {
|
||||
});
|
||||
|
||||
it("collects changed files from structured apply_patch changes", async () => {
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
visit(
|
||||
assistantToolCall("apply_patch", {
|
||||
changes: [
|
||||
{ path: "ui/chat.ts", kind: "update" },
|
||||
{ path: "src/readme.md", kind: "delete" },
|
||||
{ path: "old-name.md", kind: { type: "update", move_path: "package.json" } },
|
||||
],
|
||||
}),
|
||||
1,
|
||||
);
|
||||
return 1;
|
||||
},
|
||||
);
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(async (_scope, visit) => {
|
||||
visit(
|
||||
assistantToolCall("apply_patch", {
|
||||
changes: [
|
||||
{ path: "ui/chat.ts", kind: "update" },
|
||||
{ path: "src/readme.md", kind: "delete" },
|
||||
{ path: "old-name.md", kind: { type: "update", move_path: "package.json" } },
|
||||
],
|
||||
}),
|
||||
1,
|
||||
);
|
||||
return 1;
|
||||
});
|
||||
|
||||
const payload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
@@ -239,13 +242,11 @@ describe("sessions.files RPC handlers", () => {
|
||||
spawnedWorkspaceDir: workspaceRoot,
|
||||
},
|
||||
});
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
visit(assistantToolCall("read", { path: "src/readme.md" }), 1);
|
||||
visit(assistantToolCall("read", { path: "../shared/config.ts" }), 2);
|
||||
return 2;
|
||||
},
|
||||
);
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(async (_scope, visit) => {
|
||||
visit(assistantToolCall("read", { path: "src/readme.md" }), 1);
|
||||
visit(assistantToolCall("read", { path: "../shared/config.ts" }), 2);
|
||||
return 2;
|
||||
});
|
||||
|
||||
const payload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
@@ -320,12 +321,10 @@ describe("sessions.files RPC handlers", () => {
|
||||
sessionFile: "sess-main.jsonl",
|
||||
},
|
||||
});
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
visit(assistantToolCall("read", { path: "src/readme.md" }), 1);
|
||||
return 1;
|
||||
},
|
||||
);
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(async (_scope, visit) => {
|
||||
visit(assistantToolCall("read", { path: "src/readme.md" }), 1);
|
||||
return 1;
|
||||
});
|
||||
|
||||
const payload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
@@ -354,12 +353,10 @@ describe("sessions.files RPC handlers", () => {
|
||||
sessionFile: "sess-main.jsonl",
|
||||
},
|
||||
});
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
visit(assistantToolCall("read", { path: "src/readme.md" }), 1);
|
||||
return 1;
|
||||
},
|
||||
);
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(async (_scope, visit) => {
|
||||
visit(assistantToolCall("read", { path: "src/readme.md" }), 1);
|
||||
return 1;
|
||||
});
|
||||
|
||||
const payload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
@@ -454,12 +451,10 @@ describe("sessions.files RPC handlers", () => {
|
||||
sessionFile: "missing-session.jsonl",
|
||||
},
|
||||
});
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
visit(assistantToolCall("read", { path: outsidePath }), 1);
|
||||
return 1;
|
||||
},
|
||||
);
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(async (_scope, visit) => {
|
||||
visit(assistantToolCall("read", { path: outsidePath }), 1);
|
||||
return 1;
|
||||
});
|
||||
|
||||
try {
|
||||
const error = expectError(
|
||||
@@ -570,12 +565,10 @@ describe("sessions.files RPC handlers", () => {
|
||||
sessionFile: "sess-main.jsonl",
|
||||
},
|
||||
});
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
visit(assistantToolCall("read", { path: "secret.txt" }), 1);
|
||||
return 1;
|
||||
},
|
||||
);
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(async (_scope, visit) => {
|
||||
visit(assistantToolCall("read", { path: "secret.txt" }), 1);
|
||||
return 1;
|
||||
});
|
||||
try {
|
||||
const listPayload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
@@ -609,12 +602,10 @@ describe("sessions.files RPC handlers", () => {
|
||||
|
||||
it("reports oversized existing files without marking them missing", async () => {
|
||||
writeWorkspaceFile(workspaceRoot, "large.log", "x".repeat(260 * 1024));
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
visit(assistantToolCall("read", { path: "large.log" }), 1);
|
||||
return 1;
|
||||
},
|
||||
);
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(async (_scope, visit) => {
|
||||
visit(assistantToolCall("read", { path: "large.log" }), 1);
|
||||
return 1;
|
||||
});
|
||||
|
||||
const error = expectError(
|
||||
await invokeSessionFilesHandler("sessions.files.get", {
|
||||
|
||||
@@ -15,7 +15,8 @@ import {
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||
import { root as fsSafeRoot, FsSafeError, type ReadResult } from "../../infra/fs-safe.js";
|
||||
import { normalizeAgentId, parseAgentSessionKey } from "../../routing/session-key.js";
|
||||
import { loadSessionEntry, visitSessionMessagesAsync } from "../session-utils.js";
|
||||
import { visitSessionMessagesAsync } from "../session-transcript-readers.js";
|
||||
import { loadSessionEntry } from "../session-utils.js";
|
||||
import type { GatewayRequestHandlers, RespondFn } from "./types.js";
|
||||
import { assertValidParams } from "./validation.js";
|
||||
|
||||
@@ -601,9 +602,12 @@ async function loadSessionFiles(params: {
|
||||
const fileRoot = resolveFileRoot({ root, spawnedCwd });
|
||||
const files = new Map<string, TouchedFile>();
|
||||
await visitSessionMessagesAsync(
|
||||
entry.sessionId,
|
||||
storePath,
|
||||
entry.sessionFile,
|
||||
{
|
||||
agentId,
|
||||
sessionFile: entry.sessionFile,
|
||||
sessionId: entry.sessionId,
|
||||
storePath,
|
||||
},
|
||||
(message) => collectTouchedFilesFromMessage(message, files),
|
||||
{
|
||||
mode: "full",
|
||||
|
||||
@@ -94,6 +94,12 @@ import {
|
||||
resolveStoredSessionOwnerAgentId,
|
||||
} from "../session-store-key.js";
|
||||
import { reactivateCompletedSubagentSession } from "../session-subagent-reactivation.js";
|
||||
import {
|
||||
readRecentSessionMessagesWithStatsAsync,
|
||||
readRecentSessionTranscriptLines,
|
||||
readSessionMessageCountAsync,
|
||||
readSessionPreviewItemsFromTranscript,
|
||||
} from "../session-transcript-readers.js";
|
||||
import {
|
||||
archiveFileOnDisk,
|
||||
buildGatewaySessionRow,
|
||||
@@ -101,10 +107,6 @@ import {
|
||||
loadCombinedSessionStoreForGateway,
|
||||
loadSessionEntry,
|
||||
migrateAndPruneGatewaySessionStoreKey,
|
||||
readRecentSessionMessagesWithStatsAsync,
|
||||
readRecentSessionTranscriptLines,
|
||||
readSessionMessageCountAsync,
|
||||
readSessionPreviewItemsFromTranscript,
|
||||
resolveDeletedAgentIdFromSessionKey,
|
||||
resolveFreshestSessionEntryFromStoreKeys,
|
||||
resolveGatewaySessionStoreTarget,
|
||||
@@ -863,7 +865,12 @@ async function handleSessionSend(params: {
|
||||
}
|
||||
|
||||
const messageSeq =
|
||||
(await readSessionMessageCountAsync(entry.sessionId, storePath, entry.sessionFile)) + 1;
|
||||
(await readSessionMessageCountAsync({
|
||||
agentId: requestedAgentId,
|
||||
sessionFile: entry.sessionFile,
|
||||
sessionId: entry.sessionId,
|
||||
storePath,
|
||||
})) + 1;
|
||||
let sendAcked = false;
|
||||
let sendPayload: unknown;
|
||||
let sendCached = false;
|
||||
@@ -1201,10 +1208,12 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
||||
continue;
|
||||
}
|
||||
const items = readSessionPreviewItemsFromTranscript(
|
||||
entry.sessionId,
|
||||
target.storePath,
|
||||
entry.sessionFile,
|
||||
target.agentId,
|
||||
{
|
||||
agentId: target.agentId,
|
||||
sessionFile: entry.sessionFile,
|
||||
sessionId: entry.sessionId,
|
||||
storePath: target.storePath,
|
||||
},
|
||||
limit,
|
||||
maxChars,
|
||||
);
|
||||
@@ -1578,11 +1587,12 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
||||
let runError: unknown;
|
||||
let runMeta: Record<string, unknown> | undefined;
|
||||
const messageSeq = initialMessage
|
||||
? (await readSessionMessageCountAsync(
|
||||
createdEntry.sessionId,
|
||||
target.storePath,
|
||||
createdEntry.sessionFile,
|
||||
)) + 1
|
||||
? (await readSessionMessageCountAsync({
|
||||
agentId: target.agentId,
|
||||
sessionFile: createdEntry.sessionFile,
|
||||
sessionId: createdEntry.sessionId,
|
||||
storePath: target.storePath,
|
||||
})) + 1
|
||||
: undefined;
|
||||
|
||||
if (initialMessage) {
|
||||
@@ -2447,9 +2457,12 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
||||
return;
|
||||
}
|
||||
const { messages } = await readRecentSessionMessagesWithStatsAsync(
|
||||
entry.sessionId,
|
||||
storePath,
|
||||
entry.sessionFile,
|
||||
{
|
||||
agentId: requestedAgent.agentId,
|
||||
sessionFile: entry.sessionFile,
|
||||
sessionId: entry.sessionId,
|
||||
storePath,
|
||||
},
|
||||
{
|
||||
maxMessages: limit,
|
||||
maxLines: limit * 20 + 20,
|
||||
|
||||
@@ -18,9 +18,11 @@ import { hasTrackedActiveSessionRun } from "./server-methods/session-active-runs
|
||||
import { resolveSessionKeyForTranscriptFile } from "./session-transcript-key.js";
|
||||
import {
|
||||
attachOpenClawTranscriptMeta,
|
||||
readSessionMessageCountAsync,
|
||||
} from "./session-transcript-readers.js";
|
||||
import {
|
||||
loadGatewaySessionRow,
|
||||
loadSessionEntry,
|
||||
readSessionMessageCountAsync,
|
||||
type GatewaySessionRow,
|
||||
} from "./session-utils.js";
|
||||
|
||||
@@ -181,7 +183,12 @@ async function handleTranscriptUpdateBroadcast(
|
||||
const { entry, storePath } = loadSessionEntry(sessionKey, { agentId: visibleAgentId });
|
||||
messageSeq = entry?.sessionId
|
||||
? asPositiveSafeInteger(
|
||||
await readSessionMessageCountAsync(entry.sessionId, storePath, entry.sessionFile),
|
||||
await readSessionMessageCountAsync({
|
||||
agentId: visibleAgentId,
|
||||
sessionFile: entry.sessionFile,
|
||||
sessionId: entry.sessionId,
|
||||
storePath,
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createHash } from "node:crypto";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { HEARTBEAT_PROMPT } from "../auto-reply/heartbeat.js";
|
||||
import { buildSessionHistorySnapshot, SessionHistorySseState } from "./session-history-state.js";
|
||||
import * as sessionUtils from "./session-utils.js";
|
||||
import * as sessionTranscriptReaders from "./session-transcript-readers.js";
|
||||
|
||||
type HistorySnapshot = ReturnType<typeof buildSessionHistorySnapshot>;
|
||||
type RawStateOptions = Omit<
|
||||
@@ -90,7 +90,7 @@ function appendAssistantText(state: SessionHistorySseState, text: string, messag
|
||||
describe("SessionHistorySseState", () => {
|
||||
test("uses the initial raw snapshot for both first history and seq seeding", () => {
|
||||
const readSpy = vi
|
||||
.spyOn(sessionUtils, "readSessionMessagesAsync")
|
||||
.spyOn(sessionTranscriptReaders, "readSessionMessagesAsync")
|
||||
.mockResolvedValue([assistantTextMessage("stale disk message", 1)]);
|
||||
try {
|
||||
const state = newState([assistantTextMessage("fresh snapshot message", 2)]);
|
||||
@@ -407,9 +407,11 @@ describe("SessionHistorySseState", () => {
|
||||
});
|
||||
|
||||
test("refreshes limited SSE history from bounded async tail reads", async () => {
|
||||
const fullReadSpy = vi.spyOn(sessionUtils, "readSessionMessagesAsync").mockResolvedValue([]);
|
||||
const fullReadSpy = vi
|
||||
.spyOn(sessionTranscriptReaders, "readSessionMessagesAsync")
|
||||
.mockResolvedValue([]);
|
||||
const tailReadSpy = vi
|
||||
.spyOn(sessionUtils, "readRecentSessionMessagesWithStatsAsync")
|
||||
.spyOn(sessionTranscriptReaders, "readRecentSessionMessagesWithStatsAsync")
|
||||
.mockResolvedValueOnce({
|
||||
messages: [assistantTextMessage("tail two", 8)],
|
||||
totalMessages: 8,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
attachOpenClawTranscriptMeta,
|
||||
readRecentSessionMessagesWithStatsAsync,
|
||||
readSessionMessagesWithSourceAsync,
|
||||
} from "./session-utils.js";
|
||||
} from "./session-transcript-readers.js";
|
||||
|
||||
// Session history state owns the SSE-friendly projection of transcript JSONL:
|
||||
// raw messages are projected for display, paginated by transcript seq, then
|
||||
@@ -42,6 +42,7 @@ type InlineSessionHistoryAppend = {
|
||||
};
|
||||
|
||||
type SessionHistoryTranscriptTarget = {
|
||||
agentId?: string;
|
||||
sessionId: string;
|
||||
storePath?: string;
|
||||
sessionFile?: string;
|
||||
@@ -361,9 +362,12 @@ export class SessionHistorySseState {
|
||||
private async readRawSnapshotAsync(): Promise<SessionHistoryRawSnapshot> {
|
||||
if (this.cursor === undefined && typeof this.limit === "number") {
|
||||
const snapshot = await readRecentSessionMessagesWithStatsAsync(
|
||||
this.target.sessionId,
|
||||
this.target.storePath,
|
||||
this.target.sessionFile,
|
||||
{
|
||||
agentId: this.target.agentId,
|
||||
sessionFile: this.target.sessionFile,
|
||||
sessionId: this.target.sessionId,
|
||||
storePath: this.target.storePath,
|
||||
},
|
||||
{
|
||||
...resolveSessionHistoryTailReadOptions(this.limit),
|
||||
allowResetArchiveFallback: true,
|
||||
@@ -377,9 +381,12 @@ export class SessionHistorySseState {
|
||||
};
|
||||
}
|
||||
const snapshot = await readSessionMessagesWithSourceAsync(
|
||||
this.target.sessionId,
|
||||
this.target.storePath,
|
||||
this.target.sessionFile,
|
||||
{
|
||||
agentId: this.target.agentId,
|
||||
sessionFile: this.target.sessionFile,
|
||||
sessionId: this.target.sessionId,
|
||||
storePath: this.target.storePath,
|
||||
},
|
||||
{
|
||||
mode: "full",
|
||||
reason: "session history cursor pagination",
|
||||
|
||||
@@ -64,10 +64,10 @@ import {
|
||||
resolveStableSessionEndTranscript,
|
||||
type ArchivedSessionTranscript,
|
||||
} from "./session-transcript-files.fs.js";
|
||||
import { readSessionMessagesAsync } from "./session-transcript-readers.js";
|
||||
import {
|
||||
loadSessionEntry,
|
||||
migrateAndPruneGatewaySessionStoreKey,
|
||||
readSessionMessagesAsync,
|
||||
resolveGatewaySessionStoreTarget,
|
||||
resolveSessionStoreKey,
|
||||
resolveSessionModelRef,
|
||||
@@ -806,10 +806,18 @@ export async function emitGatewayBeforeResetPluginHook(params: {
|
||||
let messages: unknown[] = [];
|
||||
try {
|
||||
if (typeof sessionId === "string" && sessionId.trim().length > 0) {
|
||||
messages = await readSessionMessagesAsync(sessionId, params.storePath, sessionFile, {
|
||||
mode: "full",
|
||||
reason: "before_reset hook payload",
|
||||
});
|
||||
messages = await readSessionMessagesAsync(
|
||||
{
|
||||
agentId,
|
||||
sessionFile,
|
||||
sessionId,
|
||||
storePath: params.storePath,
|
||||
},
|
||||
{
|
||||
mode: "full",
|
||||
reason: "before_reset hook payload",
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logVerbose(
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
readSessionMessageCountAsync as readSessionMessageCountAsyncFile,
|
||||
readSessionMessages as readSessionMessagesFile,
|
||||
readSessionMessagesAsync as readSessionMessagesAsyncFile,
|
||||
readSessionMessagesWithSourceAsync as readSessionMessagesWithSourceAsyncFile,
|
||||
readSessionPreviewItemsFromTranscript as readSessionPreviewItemsFromTranscriptFile,
|
||||
readSessionTitleFieldsFromTranscript as readSessionTitleFieldsFromTranscriptFile,
|
||||
readSessionTitleFieldsFromTranscriptAsync as readSessionTitleFieldsFromTranscriptAsyncFile,
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
} from "./session-utils.fs.js";
|
||||
|
||||
export type { ReadRecentSessionMessagesOptions, ReadSessionMessagesAsyncOptions };
|
||||
export { attachOpenClawTranscriptMeta, capArrayByJsonBytes } from "./session-utils.fs.js";
|
||||
|
||||
export type SessionTranscriptReadScope = {
|
||||
agentId?: string;
|
||||
@@ -42,9 +44,15 @@ type SessionTitleFields = {
|
||||
|
||||
type ReadRecentSessionMessagesResult = {
|
||||
messages: unknown[];
|
||||
transcriptPath?: string;
|
||||
totalMessages: number;
|
||||
};
|
||||
|
||||
type ReadSessionMessagesResult = {
|
||||
messages: unknown[];
|
||||
transcriptPath?: string;
|
||||
};
|
||||
|
||||
type ReadSessionMessageByIdResult = {
|
||||
message?: unknown;
|
||||
seq?: number;
|
||||
@@ -126,6 +134,20 @@ export async function readSessionMessagesAsync(
|
||||
);
|
||||
}
|
||||
|
||||
/** Reads display messages with source metadata through the reader seam. */
|
||||
export async function readSessionMessagesWithSourceAsync(
|
||||
scope: SessionTranscriptReadScope,
|
||||
opts: ReadSessionMessagesAsyncOptions,
|
||||
): Promise<ReadSessionMessagesResult> {
|
||||
return await readSessionMessagesWithSourceAsyncFile(
|
||||
scope.sessionId,
|
||||
scope.storePath,
|
||||
scope.sessionFile,
|
||||
opts,
|
||||
scope.agentId,
|
||||
);
|
||||
}
|
||||
|
||||
/** Reads recent display messages asynchronously through the reader seam. */
|
||||
export async function readRecentSessionMessagesAsync(
|
||||
scope: SessionTranscriptReadScope,
|
||||
@@ -144,13 +166,14 @@ export async function readRecentSessionMessagesAsync(
|
||||
export async function readSessionMessageByIdAsync(
|
||||
scope: SessionTranscriptReadScope,
|
||||
messageId: string,
|
||||
opts?: { allowResetArchiveFallback?: boolean },
|
||||
): Promise<ReadSessionMessageByIdResult> {
|
||||
return await readSessionMessageByIdAsyncFile(
|
||||
scope.sessionId,
|
||||
scope.storePath,
|
||||
scope.sessionFile,
|
||||
messageId,
|
||||
{ agentId: scope.agentId },
|
||||
{ ...opts, agentId: scope.agentId },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,10 @@ import type {
|
||||
export {
|
||||
archiveFileOnDisk,
|
||||
archiveSessionTranscripts,
|
||||
resolveSessionHistoryTranscriptPathAsync,
|
||||
resolveSessionTranscriptCandidates,
|
||||
} from "./session-utils.fs.js";
|
||||
export {
|
||||
attachOpenClawTranscriptMeta,
|
||||
capArrayByJsonBytes,
|
||||
readFirstUserMessageFromTranscript,
|
||||
@@ -130,12 +134,12 @@ export {
|
||||
readSessionPreviewItemsFromTranscript,
|
||||
readSessionMessagesAsync,
|
||||
readSessionMessagesWithSourceAsync,
|
||||
resolveSessionHistoryTranscriptPathAsync,
|
||||
visitSessionMessagesAsync,
|
||||
resolveSessionTranscriptCandidates,
|
||||
} from "./session-utils.fs.js";
|
||||
export type { ReadSessionMessagesAsyncOptions } from "./session-utils.fs.js";
|
||||
export type { SessionTranscriptReadScope } from "./session-transcript-readers.js";
|
||||
} from "./session-transcript-readers.js";
|
||||
export type {
|
||||
ReadSessionMessagesAsyncOptions,
|
||||
SessionTranscriptReadScope,
|
||||
} from "./session-transcript-readers.js";
|
||||
export { canonicalizeSpawnedByForAgent, resolveSessionStoreKey } from "./session-store-key.js";
|
||||
export type {
|
||||
GatewayAgentRow,
|
||||
|
||||
@@ -93,9 +93,13 @@ vi.mock("./session-utils.js", () => ({
|
||||
sessionId: "session-1",
|
||||
sessionFile: "/tmp/session-1.jsonl",
|
||||
}),
|
||||
resolveSessionTranscriptCandidates: () => ["/tmp/session-1.jsonl"],
|
||||
}));
|
||||
|
||||
vi.mock("./session-transcript-readers.js", () => ({
|
||||
readRecentSessionMessagesWithStatsAsync: async () => ({ messages: [], totalMessages: 0 }),
|
||||
readSessionMessagesAsync: async () => [],
|
||||
readSessionMessagesWithSourceAsync: async () => ({ messages: [] }),
|
||||
resolveSessionTranscriptCandidates: () => ["/tmp/session-1.jsonl"],
|
||||
}));
|
||||
|
||||
vi.mock("./session-history-state.js", () => ({
|
||||
|
||||
@@ -34,6 +34,8 @@ import { resolveTranscriptPathForComparison } from "./session-transcript-path.js
|
||||
import {
|
||||
readRecentSessionMessagesWithStatsAsync,
|
||||
readSessionMessagesWithSourceAsync,
|
||||
} from "./session-transcript-readers.js";
|
||||
import {
|
||||
resolveFreshestSessionEntryFromStoreKeys,
|
||||
resolveGatewaySessionStoreTarget,
|
||||
resolveSessionTranscriptCandidates,
|
||||
@@ -145,9 +147,12 @@ export async function handleSessionHistoryHttpRequest(
|
||||
const boundedSnapshot =
|
||||
cursor === undefined && typeof limit === "number"
|
||||
? await readRecentSessionMessagesWithStatsAsync(
|
||||
entry.sessionId,
|
||||
target.storePath,
|
||||
entry.sessionFile,
|
||||
{
|
||||
agentId: target.agentId,
|
||||
sessionFile: entry.sessionFile,
|
||||
sessionId: entry.sessionId,
|
||||
storePath: target.storePath,
|
||||
},
|
||||
{
|
||||
...resolveSessionHistoryTailReadOptions(limit),
|
||||
allowResetArchiveFallback: true,
|
||||
@@ -159,9 +164,12 @@ export async function handleSessionHistoryHttpRequest(
|
||||
const fullSnapshot =
|
||||
boundedSnapshot === undefined && entry?.sessionId
|
||||
? await readSessionMessagesWithSourceAsync(
|
||||
entry.sessionId,
|
||||
target.storePath,
|
||||
entry.sessionFile,
|
||||
{
|
||||
agentId: target.agentId,
|
||||
sessionFile: entry.sessionFile,
|
||||
sessionId: entry.sessionId,
|
||||
storePath: target.storePath,
|
||||
},
|
||||
{
|
||||
mode: "full",
|
||||
reason: "session history cursor pagination",
|
||||
@@ -204,6 +212,7 @@ export async function handleSessionHistoryHttpRequest(
|
||||
let sentHistory = history;
|
||||
const sseState = SessionHistorySseState.fromRawSnapshot({
|
||||
target: {
|
||||
agentId: target.agentId,
|
||||
sessionId: entry.sessionId,
|
||||
storePath: target.storePath,
|
||||
sessionFile: entry.sessionFile,
|
||||
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
} from "../config/sessions.js";
|
||||
import { hasSessionAutoModelFallbackProvenance } from "../config/sessions/model-override-provenance.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { readRecentSessionUsageFromTranscript } from "../gateway/session-utils.fs.js";
|
||||
import { readRecentSessionUsageFromTranscript } from "../gateway/session-transcript-readers.js";
|
||||
import { formatTimeAgo } from "../infra/format-time/format-relative.ts";
|
||||
import { resolveCommitHash } from "../infra/git-commit.js";
|
||||
import {
|
||||
@@ -320,10 +320,12 @@ const readUsageFromSessionLog = (
|
||||
|
||||
try {
|
||||
const snapshot = readRecentSessionUsageFromTranscript(
|
||||
sessionId,
|
||||
storePath,
|
||||
sessionEntry?.sessionFile,
|
||||
agentId ?? (sessionKey ? resolveAgentIdFromSessionKey(sessionKey) : undefined),
|
||||
{
|
||||
agentId: agentId ?? (sessionKey ? resolveAgentIdFromSessionKey(sessionKey) : undefined),
|
||||
sessionFile: sessionEntry?.sessionFile,
|
||||
sessionId,
|
||||
storePath,
|
||||
},
|
||||
256 * 1024,
|
||||
);
|
||||
if (!snapshot) {
|
||||
|
||||
@@ -176,8 +176,6 @@ vi.mock("../gateway/session-utils.js", () => ({
|
||||
loadSessionEntry: (sessionKey: string, opts?: { agentId?: string }) =>
|
||||
loadSessionEntryMock(sessionKey, opts),
|
||||
migrateAndPruneGatewaySessionStoreKey: ({ key }: { key: string }) => ({ primaryKey: key }),
|
||||
readSessionMessagesAsync: (...args: Parameters<typeof readSessionMessagesAsyncMock>) =>
|
||||
readSessionMessagesAsyncMock(...args),
|
||||
resolveGatewaySessionStoreTarget: ({ key }: { key: string }) => ({
|
||||
canonicalKey: key,
|
||||
storePath: "/tmp/openclaw-sessions.json",
|
||||
@@ -193,8 +191,10 @@ vi.mock("../gateway/session-reset-service.js", () => ({
|
||||
performGatewaySessionReset: () => ({ ok: true, key: "agent:main:main", entry: {} }),
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/session-utils.fs.js", () => ({
|
||||
vi.mock("../gateway/session-transcript-readers.js", () => ({
|
||||
capArrayByJsonBytes: (items: unknown[]) => ({ items }),
|
||||
readSessionMessagesAsync: (...args: Parameters<typeof readSessionMessagesAsyncMock>) =>
|
||||
readSessionMessagesAsyncMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/sessions-patch.js", () => ({
|
||||
@@ -650,9 +650,12 @@ describe("EmbeddedTuiBackend", () => {
|
||||
await backend.loadHistory({ sessionKey: "agent:main:main" });
|
||||
|
||||
expect(readSessionMessagesAsyncMock).toHaveBeenCalledWith(
|
||||
"sess-main",
|
||||
"/tmp/openclaw-sessions.json",
|
||||
undefined,
|
||||
{
|
||||
agentId: "main",
|
||||
sessionFile: undefined,
|
||||
sessionId: "sess-main",
|
||||
storePath: "/tmp/openclaw-sessions.json",
|
||||
},
|
||||
{
|
||||
mode: "recent",
|
||||
maxMessages: 200,
|
||||
|
||||
@@ -49,7 +49,10 @@ import {
|
||||
} from "../gateway/server-methods/chat.js";
|
||||
import { loadGatewayModelCatalog } from "../gateway/server-model-catalog.js";
|
||||
import { performGatewaySessionReset } from "../gateway/session-reset-service.js";
|
||||
import { capArrayByJsonBytes } from "../gateway/session-utils.fs.js";
|
||||
import {
|
||||
capArrayByJsonBytes,
|
||||
readSessionMessagesAsync,
|
||||
} from "../gateway/session-transcript-readers.js";
|
||||
import {
|
||||
buildGatewaySessionInfo,
|
||||
getSessionDefaults,
|
||||
@@ -60,7 +63,6 @@ import {
|
||||
migrateAndPruneGatewaySessionStoreKey,
|
||||
resolveGatewaySessionStoreTarget,
|
||||
resolveSessionModelRef,
|
||||
readSessionMessagesAsync,
|
||||
} from "../gateway/session-utils.js";
|
||||
import { applySessionsPatchToStore } from "../gateway/sessions-patch.js";
|
||||
import { type AgentEventPayload, onAgentEvent } from "../infra/agent-events.js";
|
||||
@@ -445,12 +447,20 @@ export class EmbeddedTuiBackend implements TuiBackend {
|
||||
const maxHistoryBytes = getMaxChatHistoryMessagesBytes();
|
||||
const localMessages =
|
||||
sessionId && storePath
|
||||
? await readSessionMessagesAsync(sessionId, storePath, entry?.sessionFile, {
|
||||
mode: "recent",
|
||||
maxMessages: max,
|
||||
maxBytes: Math.max(maxHistoryBytes * 2, 1024 * 1024),
|
||||
allowResetArchiveFallback: true,
|
||||
})
|
||||
? await readSessionMessagesAsync(
|
||||
{
|
||||
agentId: sessionAgentId,
|
||||
sessionFile: entry?.sessionFile,
|
||||
sessionId,
|
||||
storePath,
|
||||
},
|
||||
{
|
||||
mode: "recent",
|
||||
maxMessages: max,
|
||||
maxBytes: Math.max(maxHistoryBytes * 2, 1024 * 1024),
|
||||
allowResetArchiveFallback: true,
|
||||
},
|
||||
)
|
||||
: [];
|
||||
const rawMessages = augmentChatHistoryWithCliSessionImports({
|
||||
entry,
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
findSessionTranscriptReaderBoundaryViolations,
|
||||
migratedSessionTranscriptReaderFiles,
|
||||
} from "../../scripts/check-session-transcript-reader-boundary.mjs";
|
||||
|
||||
describe("session transcript reader boundary guard", () => {
|
||||
it("ratchets only the files migrated by the transcript reader slice", () => {
|
||||
expect(migratedSessionTranscriptReaderFiles).toEqual(
|
||||
new Set([
|
||||
"src/agents/main-session-restart-recovery.ts",
|
||||
"src/agents/subagent-announce-output.test.ts",
|
||||
"src/agents/subagent-announce-output.ts",
|
||||
"src/agents/subagent-announce.runtime.ts",
|
||||
"src/agents/subagent-orphan-recovery.test.ts",
|
||||
"src/agents/subagent-orphan-recovery.ts",
|
||||
"src/agents/tools/embedded-gateway-stub.runtime.ts",
|
||||
"src/agents/tools/embedded-gateway-stub.test.ts",
|
||||
"src/agents/tools/embedded-gateway-stub.ts",
|
||||
"src/agents/tools/sessions-history-tool.ts",
|
||||
"src/agents/tools/sessions-list-tool.ts",
|
||||
"src/gateway/cli-session-history.claude.ts",
|
||||
"src/gateway/gateway-models.profiles.live.test.ts",
|
||||
"src/gateway/managed-image-attachments.test.ts",
|
||||
"src/gateway/managed-image-attachments.ts",
|
||||
"src/gateway/server-methods/artifacts.test.ts",
|
||||
"src/gateway/server-methods/artifacts.ts",
|
||||
"src/gateway/server-methods/chat.ts",
|
||||
"src/gateway/server-methods/sessions-files.test.ts",
|
||||
"src/gateway/server-methods/sessions-files.ts",
|
||||
"src/gateway/server-methods/sessions.ts",
|
||||
"src/gateway/server-session-events.ts",
|
||||
"src/gateway/session-history-state.test.ts",
|
||||
"src/gateway/session-history-state.ts",
|
||||
"src/gateway/session-reset-service.ts",
|
||||
"src/gateway/session-utils.ts",
|
||||
"src/gateway/sessions-history-http.revocation.test.ts",
|
||||
"src/gateway/sessions-history-http.ts",
|
||||
"src/status/status-message.ts",
|
||||
"src/tui/embedded-backend.test.ts",
|
||||
"src/tui/embedded-backend.ts",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("flags legacy transcript reader imports", () => {
|
||||
expect(
|
||||
findSessionTranscriptReaderBoundaryViolations(`
|
||||
import { readSessionMessagesAsync, loadSessionEntry } from "./session-utils.js";
|
||||
import { readRecentSessionMessages as readRecent } from "./session-utils.fs.js";
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
line: 2,
|
||||
reason:
|
||||
'imports transcript reader "readSessionMessagesAsync" from legacy module "./session-utils.js"',
|
||||
},
|
||||
{
|
||||
line: 3,
|
||||
reason:
|
||||
'imports transcript reader "readRecentSessionMessages" from legacy module "./session-utils.fs.js"',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("flags namespace legacy transcript reader references", () => {
|
||||
expect(
|
||||
findSessionTranscriptReaderBoundaryViolations(`
|
||||
import * as sessionUtils from "./session-utils.js";
|
||||
sessionUtils.readSessionMessagesAsync();
|
||||
sessionUtils["readRecentSessionMessages"]();
|
||||
const { readSessionMessages } = sessionUtils;
|
||||
`),
|
||||
).toEqual([
|
||||
{ line: 3, reason: 'references legacy transcript reader "readSessionMessagesAsync"' },
|
||||
{ line: 4, reason: 'references legacy transcript reader "readRecentSessionMessages"' },
|
||||
{ line: 5, reason: 'aliases legacy transcript reader "readSessionMessages"' },
|
||||
]);
|
||||
});
|
||||
|
||||
it("flags legacy transcript reader re-exports", () => {
|
||||
expect(
|
||||
findSessionTranscriptReaderBoundaryViolations(`
|
||||
export { readSessionMessagesAsync } from "./session-utils.js";
|
||||
export { readRecentSessionMessages as readRecent } from "./session-utils.fs.js";
|
||||
export * as sessionUtils from "./session-utils.js";
|
||||
export * from "./session-utils.fs.js";
|
||||
`),
|
||||
).toEqual([
|
||||
{
|
||||
line: 2,
|
||||
reason:
|
||||
're-exports transcript reader "readSessionMessagesAsync" from legacy module "./session-utils.js"',
|
||||
},
|
||||
{
|
||||
line: 3,
|
||||
reason:
|
||||
're-exports transcript reader "readRecentSessionMessages" from legacy module "./session-utils.fs.js"',
|
||||
},
|
||||
{
|
||||
line: 4,
|
||||
reason: 're-exports transcript reader namespace from legacy module "./session-utils.js"',
|
||||
},
|
||||
{
|
||||
line: 5,
|
||||
reason: 're-exports transcript readers from legacy module "./session-utils.fs.js"',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("allows migrated reader facade imports and non-reader session utilities", () => {
|
||||
expect(
|
||||
findSessionTranscriptReaderBoundaryViolations(`
|
||||
import { readSessionMessagesAsync } from "./session-transcript-readers.js";
|
||||
import { loadSessionEntry } from "./session-utils.js";
|
||||
export { readSessionMessagesAsync };
|
||||
await readSessionMessagesAsync(scope, opts);
|
||||
loadSessionEntry("agent:main");
|
||||
`),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("allows reader-named destructuring from non-legacy objects", () => {
|
||||
expect(
|
||||
findSessionTranscriptReaderBoundaryViolations(`
|
||||
const { readSessionMessagesAsync } = deps;
|
||||
const { readSessionMessages: readMessages } = mockReaders;
|
||||
`),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -32,6 +32,22 @@ describe("ci workflow guards", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("runs the transcript reader ratchet as a visible additional check", () => {
|
||||
const workflow = readCiWorkflow();
|
||||
const additionalJob = workflow.jobs["check-additional-shard"];
|
||||
const matrixRows = additionalJob.strategy.matrix.include;
|
||||
expect(matrixRows).toContainEqual({
|
||||
check_name: "check-session-transcript-reader-boundary",
|
||||
group: "session-transcript-reader-boundary",
|
||||
});
|
||||
|
||||
const runStep = additionalJob.steps.find((step) => step.name === "Run additional check shard");
|
||||
expect(runStep.run).toContain("session-transcript-reader-boundary)");
|
||||
expect(runStep.run).toContain(
|
||||
'run_check "lint:tmp:session-transcript-reader-boundary" pnpm run lint:tmp:session-transcript-reader-boundary',
|
||||
);
|
||||
});
|
||||
|
||||
it("kills timed manual checkout fetches after the grace period", () => {
|
||||
const workflowPaths = [
|
||||
".github/workflows/ci.yml",
|
||||
|
||||
Reference in New Issue
Block a user