mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-28 13:21:11 +00:00
fix(skills): keep history scans UTF-16 safe (#107720)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { sliceUtf16Safe, truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import { filterHeartbeatTranscriptTurns } from "../../auto-reply/heartbeat-transcript-turns.js";
|
||||
import { redactSensitiveText } from "../../logging/redact.js";
|
||||
import { formatSkillExperienceReviewTranscript } from "./experience-review-prompt.js";
|
||||
@@ -20,12 +21,12 @@ function capSessionTranscript(transcript: string, maxChars: number): string {
|
||||
}
|
||||
const omission = "\n\n[older session content omitted]\n\n";
|
||||
if (maxChars <= omission.length) {
|
||||
return transcript.slice(0, maxChars);
|
||||
return truncateUtf16Safe(transcript, maxChars);
|
||||
}
|
||||
const contentBudget = Math.max(0, maxChars - omission.length);
|
||||
const headLength = Math.min(2_000, Math.floor(contentBudget / 2));
|
||||
const head = transcript.slice(0, headLength);
|
||||
const tail = transcript.slice(-(contentBudget - headLength));
|
||||
const head = truncateUtf16Safe(transcript, headLength);
|
||||
const tail = sliceUtf16Safe(transcript, -(contentBudget - head.length));
|
||||
return `${head}${omission}${tail}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ function summary(sessionKey: string, overrides: Partial<SessionEntrySummary["ent
|
||||
>;
|
||||
}
|
||||
|
||||
function hasDanglingSurrogate(value: string): boolean {
|
||||
return /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(value);
|
||||
}
|
||||
|
||||
describe("Skill Workshop history scan", () => {
|
||||
it("keeps interactive sessions and excludes system-owned work", () => {
|
||||
expect(isSkillHistoryScanSessionEligible(summary("agent:main:main"))).toBe(true);
|
||||
@@ -244,6 +248,33 @@ describe("Skill Workshop history scan", () => {
|
||||
expect(transcript).toContain("…redacted…");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "a prefix-only budget",
|
||||
content: "😀tail",
|
||||
maxChars: 8,
|
||||
includesOmission: false,
|
||||
},
|
||||
{
|
||||
name: "the retained head",
|
||||
content: `😀${"a".repeat(80)}`,
|
||||
maxChars: 51,
|
||||
includesOmission: true,
|
||||
},
|
||||
{
|
||||
name: "the retained tail",
|
||||
content: `${"a".repeat(60)}😀${"x".repeat(7)}`,
|
||||
maxChars: 51,
|
||||
includesOmission: true,
|
||||
},
|
||||
])("keeps $name UTF-16 safe", ({ content, maxChars, includesOmission }) => {
|
||||
const transcript = formatSkillHistoryScanTranscript([{ role: "user", content }], maxChars);
|
||||
|
||||
expect(transcript.length).toBeLessThanOrEqual(maxChars);
|
||||
expect(transcript.includes("[older session content omitted]")).toBe(includesOmission);
|
||||
expect(hasDanglingSurrogate(transcript)).toBe(false);
|
||||
});
|
||||
|
||||
it("continues before the oldest cursor and can return to new work", () => {
|
||||
const candidates = [
|
||||
{ instanceId: "new", sessionKey: "main", updatedAtMs: 300, entry: summary("new").entry },
|
||||
|
||||
Reference in New Issue
Block a user