Skip to content

Commit 844d818

Browse files
committed
fix(agents): keep prompt template descriptions UTF-16 safe
1 parent 60f0749 commit 844d818

2 files changed

Lines changed: 58 additions & 1 deletion

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Prompt template tests cover markdown discovery and fallback metadata.
2+
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { afterEach, describe, expect, it } from "vitest";
6+
import { loadPromptTemplates } from "./prompt-templates.js";
7+
8+
const tempDirs: string[] = [];
9+
10+
async function makeTempDir(prefix: string): Promise<string> {
11+
const dir = await mkdtemp(join(tmpdir(), prefix));
12+
tempDirs.push(dir);
13+
return dir;
14+
}
15+
16+
afterEach(async () => {
17+
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
18+
});
19+
20+
function hasLoneSurrogate(value: string): boolean {
21+
for (let index = 0; index < value.length; index += 1) {
22+
const code = value.charCodeAt(index);
23+
if (code >= 0xd800 && code <= 0xdbff) {
24+
const next = value.charCodeAt(index + 1);
25+
if (!(next >= 0xdc00 && next <= 0xdfff)) {
26+
return true;
27+
}
28+
index += 1;
29+
continue;
30+
}
31+
if (code >= 0xdc00 && code <= 0xdfff) {
32+
return true;
33+
}
34+
}
35+
return false;
36+
}
37+
38+
describe("loadPromptTemplates", () => {
39+
it("keeps fallback descriptions on a UTF-16 boundary", async () => {
40+
const root = await makeTempDir("openclaw-prompt-templates-");
41+
const promptsDir = join(root, "prompts");
42+
await mkdir(promptsDir, { recursive: true });
43+
await writeFile(join(promptsDir, "emoji.md"), `${"a".repeat(59)}\u{1f63e}tail\n`, "utf-8");
44+
45+
const templates = loadPromptTemplates({
46+
cwd: root,
47+
agentDir: join(root, "agent"),
48+
promptPaths: [promptsDir],
49+
includeDefaults: false,
50+
});
51+
52+
expect(templates).toHaveLength(1);
53+
expect(templates[0]?.description).toBe(`${"a".repeat(59)}...`);
54+
expect(hasLoneSurrogate(templates[0]?.description ?? "")).toBe(false);
55+
});
56+
});

src/agents/sessions/prompt-templates.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
22
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
3+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
34
/**
45
* Prompt template discovery and loading.
56
*
@@ -43,7 +44,7 @@ function loadTemplateFromFile(filePath: string, sourceInfo: SourceInfo): PromptT
4344
const firstLine = body.split("\n").find((line) => line.trim());
4445
if (firstLine) {
4546
// Truncate if too long
46-
description = firstLine.slice(0, 60);
47+
description = truncateUtf16Safe(firstLine, 60);
4748
if (firstLine.length > 60) {
4849
description += "...";
4950
}

0 commit comments

Comments
 (0)