mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:40:43 +00:00
fix(gateway): preserve oversized transcript placeholders
This commit is contained in:
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/sessions: keep async `sessions.list` title and preview hydration bounded to transcript head/tail reads so Control UI polling cannot full-scan large session transcripts every refresh. Thanks @vincentkoc.
|
||||
- CLI/plugins: reject missing plugin ids before config writes in `plugins enable` and `plugins disable` so a typo no longer persists a stale config entry. (#73554) Thanks @ai-hpc.
|
||||
- Agents/sessions: preserve delivered trailing assistant replies during session-file repair so Telegram/WebChat history is not rewritten to drop already-delivered responses. Fixes #76329. Thanks @obviyus.
|
||||
- Gateway/chat history: preserve oversized transcript turns as explicit omitted-message placeholders while avoiding large JSONL parse stalls. Thanks @vincentkoc.
|
||||
- Gateway: preserve stack diagnostics when `chat.send` or agent attachment parsing/staging fails, improving image-send failure triage. Refs #63432. (#75135) Thanks @keen0206.
|
||||
- Maintainer workflow: push prepared PR heads through GitHub's verified commit API by default and require an explicit override before git-protocol pushes can publish unsigned commits. Thanks @BunsDev.
|
||||
- Feishu: resolve setup/status probes through the selected/default account so multi-account configs with account-scoped app credentials show as configured and probeable. Fixes #72930. Thanks @brokemac79.
|
||||
|
||||
@@ -1672,3 +1672,91 @@ describe("archiveSessionTranscripts", () => {
|
||||
expect(fs.existsSync(transcriptPath)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("oversized transcript line guards", () => {
|
||||
let tmpDir: string;
|
||||
let storePath: string;
|
||||
|
||||
registerTempSessionStore("openclaw-session-fs-oversized-", (nextTmpDir, nextStorePath) => {
|
||||
tmpDir = nextTmpDir;
|
||||
storePath = nextStorePath;
|
||||
});
|
||||
|
||||
test("readRecentSessionMessagesAsync replaces oversized JSONL lines with placeholders", async () => {
|
||||
const sessionId = "test-oversized-recent";
|
||||
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
||||
const oversizedContent = "x".repeat(300 * 1024);
|
||||
const lines = [
|
||||
JSON.stringify({ type: "session", version: 1, id: sessionId }),
|
||||
JSON.stringify({ message: { role: "user", content: "start" } }),
|
||||
JSON.stringify({ message: { role: "assistant", content: oversizedContent } }),
|
||||
JSON.stringify({ message: { role: "user", content: "after oversized" } }),
|
||||
];
|
||||
fs.writeFileSync(transcriptPath, `${lines.join("\n")}\n`, "utf-8");
|
||||
|
||||
const out = await readRecentSessionMessagesAsync(sessionId, storePath, undefined, {
|
||||
maxMessages: 10,
|
||||
});
|
||||
|
||||
const serialized = JSON.stringify(out);
|
||||
expect(serialized).not.toContain(oversizedContent);
|
||||
expect(serialized).toContain("[chat.history omitted: message too large]");
|
||||
expect(serialized).toContain("after oversized");
|
||||
});
|
||||
|
||||
test("readRecentSessionUsageFromTranscriptAsync skips oversized lines", async () => {
|
||||
const sessionId = "test-oversized-usage";
|
||||
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
||||
const oversizedContent = "y".repeat(300 * 1024);
|
||||
const lines = [
|
||||
JSON.stringify({ type: "session", version: 1, id: sessionId }),
|
||||
JSON.stringify({
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: oversizedContent,
|
||||
usage: { input: 9999, output: 9999 },
|
||||
provider: "oversized-provider",
|
||||
model: "oversized-model",
|
||||
},
|
||||
}),
|
||||
JSON.stringify({
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: "normal",
|
||||
usage: { input: 100, output: 50 },
|
||||
provider: "test-provider",
|
||||
model: "test-model",
|
||||
},
|
||||
}),
|
||||
];
|
||||
fs.writeFileSync(transcriptPath, `${lines.join("\n")}\n`, "utf-8");
|
||||
|
||||
const usage = await readRecentSessionUsageFromTranscriptAsync(
|
||||
sessionId,
|
||||
storePath,
|
||||
undefined,
|
||||
undefined,
|
||||
512 * 1024,
|
||||
);
|
||||
|
||||
expect(usage).not.toBeNull();
|
||||
expect(usage?.modelProvider).not.toBe("oversized-provider");
|
||||
expect(usage?.modelProvider).toBe("test-provider");
|
||||
});
|
||||
|
||||
test("readSessionTitleFieldsFromTranscriptAsync delegates to bounded sync reader", async () => {
|
||||
const sessionId = "test-async-title-bounded";
|
||||
writeTranscript(
|
||||
tmpDir,
|
||||
sessionId,
|
||||
buildBasicSessionTranscript(sessionId, "User says hi", "Bot says hello"),
|
||||
);
|
||||
|
||||
const syncResult = readSessionTitleFieldsFromTranscript(sessionId, storePath);
|
||||
const asyncResult = await readSessionTitleFieldsFromTranscriptAsync(sessionId, storePath);
|
||||
|
||||
expect(asyncResult).toEqual(syncResult);
|
||||
expect(asyncResult.firstUserMessage).toBe("User says hi");
|
||||
expect(asyncResult.lastMessagePreview).toBe("Bot says hello");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -266,11 +266,33 @@ async function readRecentTranscriptTailLinesAsync(
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_TRANSCRIPT_PARSE_LINE_BYTES = 256 * 1024;
|
||||
const TRANSCRIPT_OVERSIZED_MESSAGE_PLACEHOLDER = "[chat.history omitted: message too large]";
|
||||
|
||||
function isOversizedTranscriptLine(line: string): boolean {
|
||||
return Buffer.byteLength(line, "utf8") > MAX_TRANSCRIPT_PARSE_LINE_BYTES;
|
||||
}
|
||||
|
||||
function buildOversizedTranscriptRecord(): TailTranscriptRecord {
|
||||
return {
|
||||
record: {
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: TRANSCRIPT_OVERSIZED_MESSAGE_PLACEHOLDER }],
|
||||
__openclaw: { truncated: true, reason: "oversized" },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTailEntryString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function parseTailTranscriptRecord(line: string): TailTranscriptRecord | null {
|
||||
if (isOversizedTranscriptLine(line)) {
|
||||
return buildOversizedTranscriptRecord();
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(line) as unknown;
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
@@ -1097,6 +1119,9 @@ function resolvePositiveUsageNumber(value: unknown): number | undefined {
|
||||
function extractUsageSnapshotFromTranscriptLine(
|
||||
line: string,
|
||||
): SessionTranscriptUsageSnapshot | null {
|
||||
if (isOversizedTranscriptLine(line)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(line) as Record<string, unknown>;
|
||||
const message =
|
||||
|
||||
Reference in New Issue
Block a user