mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
fix: preserve emoji at remaining bounded-text edges (#101711)
Some checks are pending
CI / runner-admission (push) Waiting to run
CI / preflight (push) Blocked by required conditions
CI / security-fast (push) Blocked by required conditions
CI / pnpm-store-warmup (push) Blocked by required conditions
CI / build-artifacts (push) Blocked by required conditions
CI / native-i18n (push) Blocked by required conditions
CI / (push) Blocked by required conditions
CI / QA Smoke CI (matrix) (push) Blocked by required conditions
CI / QA Smoke CI (telegram 1/2) (push) Blocked by required conditions
CI / QA Smoke CI (telegram 2/2) (push) Blocked by required conditions
CI / QA Smoke CI (push) Blocked by required conditions
CI / -1 (push) Blocked by required conditions
CI / -2 (push) Blocked by required conditions
CI / checks-node-compat-node22 (push) Blocked by required conditions
CI / -3 (push) Blocked by required conditions
CI / check-bundled-channel-config-metadata (push) Blocked by required conditions
CI / check-dependencies (push) Blocked by required conditions
CI / check-guards (push) Blocked by required conditions
CI / check-lint (push) Blocked by required conditions
CI / check-prod-types (push) Blocked by required conditions
CI / check-shrinkwrap (push) Blocked by required conditions
CI / check-test-types (push) Blocked by required conditions
CI / check-additional-boundaries-a (push) Blocked by required conditions
CI / check-additional-boundaries-bcd (push) Blocked by required conditions
CI / check-additional-extension-bundled (push) Blocked by required conditions
CI / check-additional-extension-channels (push) Blocked by required conditions
CI / check-additional-extension-package-boundary (push) Blocked by required conditions
CI / check-additional-runtime-topology-architecture (push) Blocked by required conditions
CI / check-session-accessor-boundary (push) Blocked by required conditions
CI / check-session-transcript-reader-boundary (push) Blocked by required conditions
CI / check-docs (push) Blocked by required conditions
CI / skills-python (push) Blocked by required conditions
CI / -4 (push) Blocked by required conditions
CI / -5 (push) Blocked by required conditions
CI / macos-swift (push) Blocked by required conditions
CI / ios-build (push) Blocked by required conditions
CI / -6 (push) Blocked by required conditions
CI / ci-timings-summary (push) Blocked by required conditions
ClawSweeper Dispatch / dispatch (push) Waiting to run
CodeQL / Security High (actions) (push) Waiting to run
CodeQL / Security High (channel-runtime-boundary) (push) Waiting to run
CodeQL / Security High (core-auth-secrets) (push) Waiting to run
CodeQL / Security High (mcp-process-tool-boundary) (push) Waiting to run
CodeQL / Security High (network-ssrf-boundary) (push) Waiting to run
CodeQL / Security High (plugin-trust-boundary) (push) Waiting to run
CodeQL / Security High (process-exec-boundary) (push) Waiting to run
Control UI Locale Refresh / plan (push) Waiting to run
Control UI Locale Refresh / Refresh (push) Blocked by required conditions
Control UI Locale Refresh / Commit control UI locale refresh (push) Blocked by required conditions
Docs Sync Publish Repo / sync-publish-repo (push) Waiting to run
Docs / docs (push) Waiting to run
Native App Locale Refresh / Refresh native ar (push) Waiting to run
Native App Locale Refresh / Refresh native de (push) Waiting to run
Native App Locale Refresh / Refresh native es (push) Waiting to run
Native App Locale Refresh / Refresh native fa (push) Waiting to run
Native App Locale Refresh / Refresh native fr (push) Waiting to run
Native App Locale Refresh / Refresh native hi (push) Waiting to run
Native App Locale Refresh / Refresh native id (push) Waiting to run
Native App Locale Refresh / Refresh native it (push) Waiting to run
Native App Locale Refresh / Refresh native ja-JP (push) Waiting to run
Native App Locale Refresh / Refresh native ko (push) Waiting to run
Native App Locale Refresh / Refresh native nl (push) Waiting to run
Native App Locale Refresh / Refresh native pl (push) Waiting to run
Native App Locale Refresh / Refresh native pt-BR (push) Waiting to run
Native App Locale Refresh / Refresh native ru (push) Waiting to run
Native App Locale Refresh / Refresh native sv (push) Waiting to run
Native App Locale Refresh / Refresh native th (push) Waiting to run
Native App Locale Refresh / Refresh native tr (push) Waiting to run
Native App Locale Refresh / Refresh native uk (push) Waiting to run
Native App Locale Refresh / Refresh native vi (push) Waiting to run
Native App Locale Refresh / Refresh native zh-CN (push) Waiting to run
Native App Locale Refresh / Refresh native zh-TW (push) Waiting to run
Native App Locale Refresh / Commit native locale refresh (push) Blocked by required conditions
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Waiting to run
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Blocked by required conditions
Plugin Init Scaffold Validation / Validate provider scaffold (push) Waiting to run
Plugin NPM Release / preview_plugins_npm (push) Waiting to run
Plugin NPM Release / Validate release publish approval (push) Blocked by required conditions
Plugin NPM Release / preview_plugin_pack (push) Blocked by required conditions
Plugin NPM Release / publish_plugins_npm (push) Blocked by required conditions
Plugin NPM Release / verify_plugins_npm (push) Blocked by required conditions
Workflow Sanity / no-tabs (push) Waiting to run
Workflow Sanity / actionlint (push) Waiting to run
Workflow Sanity / generated-doc-baselines (push) Waiting to run
Some checks are pending
CI / runner-admission (push) Waiting to run
CI / preflight (push) Blocked by required conditions
CI / security-fast (push) Blocked by required conditions
CI / pnpm-store-warmup (push) Blocked by required conditions
CI / build-artifacts (push) Blocked by required conditions
CI / native-i18n (push) Blocked by required conditions
CI / (push) Blocked by required conditions
CI / QA Smoke CI (matrix) (push) Blocked by required conditions
CI / QA Smoke CI (telegram 1/2) (push) Blocked by required conditions
CI / QA Smoke CI (telegram 2/2) (push) Blocked by required conditions
CI / QA Smoke CI (push) Blocked by required conditions
CI / -1 (push) Blocked by required conditions
CI / -2 (push) Blocked by required conditions
CI / checks-node-compat-node22 (push) Blocked by required conditions
CI / -3 (push) Blocked by required conditions
CI / check-bundled-channel-config-metadata (push) Blocked by required conditions
CI / check-dependencies (push) Blocked by required conditions
CI / check-guards (push) Blocked by required conditions
CI / check-lint (push) Blocked by required conditions
CI / check-prod-types (push) Blocked by required conditions
CI / check-shrinkwrap (push) Blocked by required conditions
CI / check-test-types (push) Blocked by required conditions
CI / check-additional-boundaries-a (push) Blocked by required conditions
CI / check-additional-boundaries-bcd (push) Blocked by required conditions
CI / check-additional-extension-bundled (push) Blocked by required conditions
CI / check-additional-extension-channels (push) Blocked by required conditions
CI / check-additional-extension-package-boundary (push) Blocked by required conditions
CI / check-additional-runtime-topology-architecture (push) Blocked by required conditions
CI / check-session-accessor-boundary (push) Blocked by required conditions
CI / check-session-transcript-reader-boundary (push) Blocked by required conditions
CI / check-docs (push) Blocked by required conditions
CI / skills-python (push) Blocked by required conditions
CI / -4 (push) Blocked by required conditions
CI / -5 (push) Blocked by required conditions
CI / macos-swift (push) Blocked by required conditions
CI / ios-build (push) Blocked by required conditions
CI / -6 (push) Blocked by required conditions
CI / ci-timings-summary (push) Blocked by required conditions
ClawSweeper Dispatch / dispatch (push) Waiting to run
CodeQL / Security High (actions) (push) Waiting to run
CodeQL / Security High (channel-runtime-boundary) (push) Waiting to run
CodeQL / Security High (core-auth-secrets) (push) Waiting to run
CodeQL / Security High (mcp-process-tool-boundary) (push) Waiting to run
CodeQL / Security High (network-ssrf-boundary) (push) Waiting to run
CodeQL / Security High (plugin-trust-boundary) (push) Waiting to run
CodeQL / Security High (process-exec-boundary) (push) Waiting to run
Control UI Locale Refresh / plan (push) Waiting to run
Control UI Locale Refresh / Refresh (push) Blocked by required conditions
Control UI Locale Refresh / Commit control UI locale refresh (push) Blocked by required conditions
Docs Sync Publish Repo / sync-publish-repo (push) Waiting to run
Docs / docs (push) Waiting to run
Native App Locale Refresh / Refresh native ar (push) Waiting to run
Native App Locale Refresh / Refresh native de (push) Waiting to run
Native App Locale Refresh / Refresh native es (push) Waiting to run
Native App Locale Refresh / Refresh native fa (push) Waiting to run
Native App Locale Refresh / Refresh native fr (push) Waiting to run
Native App Locale Refresh / Refresh native hi (push) Waiting to run
Native App Locale Refresh / Refresh native id (push) Waiting to run
Native App Locale Refresh / Refresh native it (push) Waiting to run
Native App Locale Refresh / Refresh native ja-JP (push) Waiting to run
Native App Locale Refresh / Refresh native ko (push) Waiting to run
Native App Locale Refresh / Refresh native nl (push) Waiting to run
Native App Locale Refresh / Refresh native pl (push) Waiting to run
Native App Locale Refresh / Refresh native pt-BR (push) Waiting to run
Native App Locale Refresh / Refresh native ru (push) Waiting to run
Native App Locale Refresh / Refresh native sv (push) Waiting to run
Native App Locale Refresh / Refresh native th (push) Waiting to run
Native App Locale Refresh / Refresh native tr (push) Waiting to run
Native App Locale Refresh / Refresh native uk (push) Waiting to run
Native App Locale Refresh / Refresh native vi (push) Waiting to run
Native App Locale Refresh / Refresh native zh-CN (push) Waiting to run
Native App Locale Refresh / Refresh native zh-TW (push) Waiting to run
Native App Locale Refresh / Commit native locale refresh (push) Blocked by required conditions
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Waiting to run
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Blocked by required conditions
Plugin Init Scaffold Validation / Validate provider scaffold (push) Waiting to run
Plugin NPM Release / preview_plugins_npm (push) Waiting to run
Plugin NPM Release / Validate release publish approval (push) Blocked by required conditions
Plugin NPM Release / preview_plugin_pack (push) Blocked by required conditions
Plugin NPM Release / publish_plugins_npm (push) Blocked by required conditions
Plugin NPM Release / verify_plugins_npm (push) Blocked by required conditions
Workflow Sanity / no-tabs (push) Waiting to run
Workflow Sanity / actionlint (push) Waiting to run
Workflow Sanity / generated-doc-baselines (push) Waiting to run
* fix: keep remaining text bounds UTF-16 safe Co-authored-by: ben.li <li.yang6@xydigit.com> Co-authored-by: 0668001336 <wang.lizhang@xydigit.com> Co-authored-by: Alix-007 <li.long15@xydigit.com> * docs: credit bounded Unicode fixes * test: update UTF-16 browser import boundary * chore: leave Unicode note to release changelog --------- Co-authored-by: ben.li <li.yang6@xydigit.com> Co-authored-by: 0668001336 <wang.lizhang@xydigit.com> Co-authored-by: Alix-007 <li.long15@xydigit.com>
This commit is contained in:
parent
ed28c57e20
commit
87fe266c5e
15 changed files with 170 additions and 24 deletions
|
|
@ -2175,11 +2175,13 @@ describe("diagnostics-otel service", () => {
|
|||
|
||||
test("bounds plugin-emitted log attributes and omits source paths", async () => {
|
||||
const service = createDiagnosticsOtelService();
|
||||
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { logs: true });
|
||||
const ctx = createOtelContext(OTEL_TEST_ENDPOINT, { logs: true, captureContent: true });
|
||||
await service.start(ctx);
|
||||
|
||||
const boundaryMessage = `${"x".repeat(4095)}🚀tail`;
|
||||
const boundaryAttribute = `${"y".repeat(4095)}🚀tail`;
|
||||
const attributes = Object.create(null) as Record<string, string>;
|
||||
attributes.good = "y".repeat(6000);
|
||||
attributes.good = boundaryAttribute;
|
||||
attributes["bad key"] = "drop-me";
|
||||
attributes[PROTO_KEY] = "pollute";
|
||||
attributes["constructor"] = "pollute";
|
||||
|
|
@ -2189,7 +2191,7 @@ describe("diagnostics-otel service", () => {
|
|||
emitDiagnosticEvent({
|
||||
type: "log.record",
|
||||
level: "INFO",
|
||||
message: "x".repeat(6000),
|
||||
message: boundaryMessage,
|
||||
attributes,
|
||||
code: {
|
||||
filepath: "/Users/alice/openclaw/src/private.ts",
|
||||
|
|
@ -2204,11 +2206,10 @@ describe("diagnostics-otel service", () => {
|
|||
attributes: Record<string, unknown>;
|
||||
body: string;
|
||||
};
|
||||
expect(emitCall.body.length).toBeLessThanOrEqual(4200);
|
||||
expect(String(emitCall.attributes["openclaw.good"])).toMatch(/^y+/);
|
||||
expect(emitCall.body).toBe(`${"x".repeat(4095)}...(truncated)`);
|
||||
expect(emitCall.attributes["openclaw.good"]).toBe(`${"y".repeat(4095)}...(truncated)`);
|
||||
expect(emitCall.attributes["code.lineno"]).toBe(42);
|
||||
expect(emitCall.attributes["code.function"]).toBe("handler");
|
||||
expect(String(emitCall.attributes["openclaw.good"]).length).toBeLessThanOrEqual(4200);
|
||||
expect(Object.hasOwn(emitCall.attributes, `openclaw.${PROTO_KEY}`)).toBe(false);
|
||||
expect(Object.hasOwn(emitCall.attributes, "openclaw.constructor")).toBe(false);
|
||||
expect(Object.hasOwn(emitCall.attributes, "openclaw.prototype")).toBe(false);
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import { waitForDiagnosticEventsDrained } from "openclaw/plugin-sdk/diagnostic-r
|
|||
import { createNodeProxyAgent } from "openclaw/plugin-sdk/fetch-runtime";
|
||||
import { registerUnhandledRejectionHandler } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import type {
|
||||
DiagnosticEventMetadata,
|
||||
DiagnosticEventPayload,
|
||||
|
|
@ -682,7 +683,7 @@ function addUpstreamRequestIdSpanEvent(
|
|||
}
|
||||
|
||||
function clampOtelLogText(value: string, maxChars: number): string {
|
||||
return value.length > maxChars ? `${value.slice(0, maxChars)}...(truncated)` : value;
|
||||
return value.length > maxChars ? `${truncateUtf16Safe(value, maxChars)}...(truncated)` : value;
|
||||
}
|
||||
|
||||
function normalizeOtelLogString(value: string, maxChars: number): string {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
wrapWebContent,
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
|
||||
// Internal-only bounds (the model-facing tool schema declares its own copies).
|
||||
const PARALLEL_MAX_SEARCH_COUNT = 40;
|
||||
|
|
@ -58,7 +59,7 @@ export function normalizeParallelObjective(value: string | undefined): string |
|
|||
}
|
||||
return trimmed.length <= PARALLEL_MAX_OBJECTIVE_CHARS
|
||||
? trimmed
|
||||
: trimmed.slice(0, PARALLEL_MAX_OBJECTIVE_CHARS);
|
||||
: truncateUtf16Safe(trimmed, PARALLEL_MAX_OBJECTIVE_CHARS);
|
||||
}
|
||||
|
||||
export function normalizeParallelClientModel(value: string | undefined): string | undefined {
|
||||
|
|
@ -68,7 +69,7 @@ export function normalizeParallelClientModel(value: string | undefined): string
|
|||
}
|
||||
return trimmed.length <= PARALLEL_CLIENT_MODEL_MAX_LENGTH
|
||||
? trimmed
|
||||
: trimmed.slice(0, PARALLEL_CLIENT_MODEL_MAX_LENGTH);
|
||||
: truncateUtf16Safe(trimmed, PARALLEL_CLIENT_MODEL_MAX_LENGTH);
|
||||
}
|
||||
|
||||
// Parallel's API caps each entry at 200 chars and accepts up to 5 queries. We
|
||||
|
|
@ -90,7 +91,7 @@ export function normalizeParallelSearchQueries(value: unknown): string[] {
|
|||
const capped =
|
||||
trimmed.length <= PARALLEL_MAX_SEARCH_QUERY_CHARS
|
||||
? trimmed
|
||||
: trimmed.slice(0, PARALLEL_MAX_SEARCH_QUERY_CHARS);
|
||||
: truncateUtf16Safe(trimmed, PARALLEL_MAX_SEARCH_QUERY_CHARS);
|
||||
if (seen.has(capped)) {
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,6 +254,7 @@ describe("parallel web search provider", () => {
|
|||
expect(testing.normalizeParallelObjective(undefined)).toBeUndefined();
|
||||
expect(testing.normalizeParallelObjective("")).toBeUndefined();
|
||||
expect((testing.normalizeParallelObjective("x".repeat(6000)) ?? "").length).toBe(5000);
|
||||
expect(testing.normalizeParallelObjective(`${"x".repeat(4999)}🚀tail`)).toBe("x".repeat(4999));
|
||||
});
|
||||
|
||||
it("normalizes search_queries: trim, drop blanks, dedupe, cap length, cap count", () => {
|
||||
|
|
@ -270,6 +271,9 @@ describe("parallel web search provider", () => {
|
|||
expect(testing.normalizeParallelSearchQueries(undefined)).toEqual([]);
|
||||
expect(testing.normalizeParallelSearchQueries("openclaw github")).toEqual([]);
|
||||
expect(testing.normalizeParallelSearchQueries(["x".repeat(250)])).toEqual(["x".repeat(200)]);
|
||||
expect(testing.normalizeParallelSearchQueries([`${"x".repeat(199)}🚀tail`])).toEqual([
|
||||
"x".repeat(199),
|
||||
]);
|
||||
const six = ["a", "b", "c", "d", "e", "f"];
|
||||
expect(testing.normalizeParallelSearchQueries(six)).toEqual(["a", "b", "c", "d", "e"]);
|
||||
});
|
||||
|
|
@ -289,6 +293,7 @@ describe("parallel web search provider", () => {
|
|||
expect(testing.normalizeParallelClientModel(" gpt-5.5 ")).toBe("gpt-5.5");
|
||||
expect(testing.normalizeParallelClientModel(undefined)).toBeUndefined();
|
||||
expect((testing.normalizeParallelClientModel("a".repeat(200)) ?? "").length).toBe(100);
|
||||
expect(testing.normalizeParallelClientModel(`${"m".repeat(99)}🚀tail`)).toBe("m".repeat(99));
|
||||
});
|
||||
|
||||
it("normalizes the Parallel /v1/search response shape", () => {
|
||||
|
|
|
|||
|
|
@ -1938,6 +1938,24 @@ describe("WorkboardStore", () => {
|
|||
await expect(store.buildWorkerContext(card.id)).resolves.toContain("Failure screenshot");
|
||||
});
|
||||
|
||||
it("keeps worker-context text bounds UTF-16 safe", async () => {
|
||||
const store = new WorkboardStore(createMemoryStore());
|
||||
const card = await store.create({
|
||||
title: "Bound context",
|
||||
metadata: {
|
||||
comments: [
|
||||
{
|
||||
id: "comment-1",
|
||||
body: `${"x".repeat(398)}🚀tail`,
|
||||
createdAt: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await expect(store.buildWorkerContext(card.id)).resolves.toContain(`- ${"x".repeat(398)}…`);
|
||||
});
|
||||
|
||||
it("scopes idempotent creates and stats by board", async () => {
|
||||
const store = new WorkboardStore(createMemoryStore());
|
||||
const ops = await store.create({
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
MAX_DATE_TIMESTAMP_MS,
|
||||
resolveExpiresAtMsFromDurationMs,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import type {
|
||||
PersistedWorkboardAttachment,
|
||||
PersistedWorkboardBoard,
|
||||
|
|
@ -2039,7 +2040,7 @@ function capText(value: string | undefined, max: number): string | undefined {
|
|||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
return value.length <= max ? value : `${value.slice(0, Math.max(0, max - 1))}…`;
|
||||
return value.length <= max ? value : `${truncateUtf16Safe(value, Math.max(0, max - 1))}…`;
|
||||
}
|
||||
|
||||
function cardBoardId(card: WorkboardCard): string {
|
||||
|
|
|
|||
|
|
@ -392,6 +392,21 @@ describe("compaction-safeguard tool failures", () => {
|
|||
expect(section).toContain("exec (exitCode=2): failed");
|
||||
});
|
||||
|
||||
it("keeps bounded tool-failure text UTF-16 safe", () => {
|
||||
const failures = collectToolFailures([
|
||||
{
|
||||
role: "toolResult",
|
||||
toolCallId: "call-boundary",
|
||||
toolName: "exec",
|
||||
isError: true,
|
||||
content: [{ type: "text", text: `${"x".repeat(236)}🚀tail` }],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
]);
|
||||
|
||||
expect(failures[0]?.summary).toBe(`${"x".repeat(236)}...`);
|
||||
});
|
||||
|
||||
it("caps the number of failures and adds overflow line", () => {
|
||||
const messages: AgentMessage[] = Array.from({ length: 9 }, (_, idx) => ({
|
||||
role: "toolResult",
|
||||
|
|
@ -454,6 +469,18 @@ describe("compaction-safeguard summary budgets", () => {
|
|||
expect(capped.endsWith(SUMMARY_TRUNCATED_MARKER)).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps compaction summary prefixes UTF-16 safe", () => {
|
||||
const prefixBudget = MAX_COMPACTION_SUMMARY_CHARS - SUMMARY_TRUNCATED_MARKER.length;
|
||||
const oversized = `${"x".repeat(prefixBudget - 1)}🚀${"z".repeat(
|
||||
SUMMARY_TRUNCATED_MARKER.length + 10,
|
||||
)}`;
|
||||
|
||||
expect(capCompactionSummary(oversized)).toBe(
|
||||
`${"x".repeat(prefixBudget - 1)}${SUMMARY_TRUNCATED_MARKER}`,
|
||||
);
|
||||
expect(capCompactionSummary(`${"x".repeat(9)}🚀tail`, 10)).toBe("x".repeat(9));
|
||||
});
|
||||
|
||||
it("preserves workspace critical rules suffix when capping", () => {
|
||||
const suffix =
|
||||
"\n\n<workspace-critical-rules>\n## Session Startup\nRead AGENTS.md\n</workspace-critical-rules>";
|
||||
|
|
@ -522,6 +549,10 @@ describe("compaction-safeguard summary budgets", () => {
|
|||
expect(capped).toContain("<read-files>");
|
||||
expect(capped).toContain("## Session Startup");
|
||||
});
|
||||
|
||||
it("keeps an oversized preserved suffix UTF-16 safe at its leading edge", () => {
|
||||
expect(capCompactionSummaryPreservingSuffix("body", "A🚀tail", 5)).toBe("tail");
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeAdaptiveChunkRatio", () => {
|
||||
|
|
@ -906,6 +937,18 @@ describe("compaction-safeguard recent-turn preservation", () => {
|
|||
expect(section).toContain("[non-text content: image]");
|
||||
});
|
||||
|
||||
it("keeps bounded preserved-turn text UTF-16 safe", () => {
|
||||
const section = formatPreservedTurnsSection([
|
||||
{
|
||||
role: "user",
|
||||
content: `${"x".repeat(599)}🚀tail`,
|
||||
timestamp: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(section).toContain(`- User: ${"x".repeat(599)}...`);
|
||||
});
|
||||
|
||||
it("does not add non-text placeholders for text-only content blocks", () => {
|
||||
const section = formatPreservedTurnsSection([
|
||||
{
|
||||
|
|
@ -2692,6 +2735,14 @@ describe("readWorkspaceContextForSummary", () => {
|
|||
expect(result).not.toContain("Ignore me");
|
||||
});
|
||||
|
||||
it("keeps bounded workspace rules UTF-16 safe", async () => {
|
||||
const heading = "## Session Startup\n";
|
||||
const safePrefix = `${heading}${"x".repeat(1_999 - heading.length)}`;
|
||||
const result = await withWorkspaceSummary(`${safePrefix}🚀tail\n`, ["Session Startup"]);
|
||||
|
||||
expect(result).toContain(`${safePrefix}\n...[truncated]...`);
|
||||
});
|
||||
|
||||
it("reads workspace context from the configured workspace instead of process cwd", async () => {
|
||||
const processRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-compaction-cwd-"));
|
||||
const workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-compaction-workspace-"));
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
/** Extension that safeguards compaction with structured summaries and quality repair. */
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { sliceUtf16Safe, truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import { extractSections } from "../../auto-reply/reply/post-compaction-context.js";
|
||||
import { isAbortError } from "../../infra/abort-signal.js";
|
||||
import { openRootFile } from "../../infra/boundary-file-read.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { isAbortError } from "../../infra/abort-signal.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import {
|
||||
getCompactionProvider,
|
||||
|
|
@ -404,7 +406,7 @@ function truncateFailureText(text: string, maxChars: number): string {
|
|||
if (text.length <= maxChars) {
|
||||
return text;
|
||||
}
|
||||
return `${text.slice(0, Math.max(0, maxChars - 3))}...`;
|
||||
return `${truncateUtf16Safe(text, Math.max(0, maxChars - 3))}...`;
|
||||
}
|
||||
|
||||
function formatToolFailureMeta(details: unknown): string | undefined {
|
||||
|
|
@ -568,9 +570,9 @@ function capCompactionSummary(summary: string, maxChars = MAX_COMPACTION_SUMMARY
|
|||
const budget = Math.max(0, maxChars - marker.length);
|
||||
if (budget <= 0) {
|
||||
// Marker cannot fit; keep body prefix instead of a partial marker fragment.
|
||||
return summary.slice(0, maxChars);
|
||||
return truncateUtf16Safe(summary, maxChars);
|
||||
}
|
||||
return `${summary.slice(0, budget)}${marker}`;
|
||||
return `${truncateUtf16Safe(summary, budget)}${marker}`;
|
||||
}
|
||||
|
||||
function capCompactionSummaryPreservingSuffix(
|
||||
|
|
@ -586,7 +588,7 @@ function capCompactionSummaryPreservingSuffix(
|
|||
}
|
||||
if (suffix.length >= maxChars) {
|
||||
// Preserve tail (workspace rules, diagnostics) over head (preserved turns).
|
||||
return suffix.slice(-maxChars);
|
||||
return sliceUtf16Safe(suffix, -maxChars);
|
||||
}
|
||||
const bodyBudget = Math.max(0, maxChars - suffix.length);
|
||||
const cappedBody = capCompactionSummary(summaryBody, bodyBudget);
|
||||
|
|
@ -790,7 +792,7 @@ function formatContextMessages(messages: AgentMessage[]): string[] {
|
|||
}
|
||||
const trimmed =
|
||||
rendered.length > MAX_RECENT_TURN_TEXT_CHARS
|
||||
? `${rendered.slice(0, MAX_RECENT_TURN_TEXT_CHARS)}...`
|
||||
? `${truncateUtf16Safe(rendered, MAX_RECENT_TURN_TEXT_CHARS)}...`
|
||||
: rendered;
|
||||
return `- ${roleLabel}: ${trimmed}`;
|
||||
})
|
||||
|
|
@ -884,7 +886,7 @@ async function readWorkspaceContextForSummary(
|
|||
const combined = sections.join("\n\n");
|
||||
const safeContent =
|
||||
combined.length > MAX_SUMMARY_CONTEXT_CHARS
|
||||
? combined.slice(0, MAX_SUMMARY_CONTEXT_CHARS) + "\n...[truncated]..."
|
||||
? `${truncateUtf16Safe(combined, MAX_SUMMARY_CONTEXT_CHARS)}\n...[truncated]...`
|
||||
: combined;
|
||||
|
||||
return `\n\n<workspace-critical-rules>\n${safeContent}\n</workspace-critical-rules>`;
|
||||
|
|
|
|||
|
|
@ -114,6 +114,13 @@ describe("system-presence", () => {
|
|||
expect(entry?.text).toBe("Node: relay-host · mode operator");
|
||||
});
|
||||
|
||||
it("keeps fallback text keys UTF-16 safe", () => {
|
||||
const keyPrefix = `presence-${randomUUID()}`.padEnd(63, "x");
|
||||
const update = updateSystemPresence({ text: `${keyPrefix}🚀tail` });
|
||||
|
||||
expect(update.key).toBe(keyPrefix);
|
||||
});
|
||||
|
||||
it("prunes stale non-self entries after TTL", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(Date.now());
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
} from "@openclaw/normalization-core/string-coerce";
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import { resolveRuntimeServiceVersion } from "../version.js";
|
||||
import { pickBestEffortPrimaryLanIPv4 } from "./network-discovery-display.js";
|
||||
|
||||
|
|
@ -201,7 +202,7 @@ export function updateSystemPresence(payload: SystemPresencePayload): SystemPres
|
|||
normalizePresenceKey(parsed.instanceId) ||
|
||||
normalizePresenceKey(parsed.host) ||
|
||||
parsed.ip ||
|
||||
parsed.text.slice(0, 64) ||
|
||||
truncateUtf16Safe(parsed.text, 64) ||
|
||||
normalizeLowercaseStringOrEmpty(os.hostname());
|
||||
const hadExisting = entries.has(key);
|
||||
const existing = entries.get(key) ?? ({} as SystemPresence);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ describe("slash command browser import", () => {
|
|||
);
|
||||
|
||||
expect(importDeclarations(commands)).toEqual([
|
||||
'import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";',
|
||||
'import type { CommandEntry } from "../../../../packages/gateway-protocol/src/index.js";',
|
||||
'import { buildBuiltinChatCommands } from "../../../../src/auto-reply/commands-registry.shared.js";',
|
||||
'import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";',
|
||||
|
|
|
|||
|
|
@ -234,7 +234,8 @@ describe("parseSlashCommand", () => {
|
|||
|
||||
it("caps remote command payload size and long metadata before it reaches UI state", () => {
|
||||
const longName = "x".repeat(260);
|
||||
const longDescription = "d".repeat(2_500);
|
||||
const longDescription = `${"d".repeat(1_999)}🚀tail`;
|
||||
const boundaryArgName = `${"n".repeat(199)}🚀tail`;
|
||||
const oversizedCommand = {
|
||||
name: "plugin-0",
|
||||
textAliases: Array.from({ length: 25 }, (_, aliasIndex) => `/plugin-0-${aliasIndex}`),
|
||||
|
|
@ -243,7 +244,7 @@ describe("parseSlashCommand", () => {
|
|||
scope: "both" as const,
|
||||
acceptsArgs: true,
|
||||
args: Array.from({ length: 25 }, (_, argIndex) => ({
|
||||
name: `${longName}-${argIndex}`,
|
||||
name: argIndex === 0 ? boundaryArgName : `${longName}-${argIndex}`,
|
||||
description: longDescription,
|
||||
type: "string" as const,
|
||||
choices: Array.from({ length: 55 }, (_Local, choiceIndex) => ({
|
||||
|
|
@ -268,8 +269,9 @@ describe("parseSlashCommand", () => {
|
|||
expect(remoteCommands).toHaveLength(500);
|
||||
const first = remoteCommands[0];
|
||||
expect(first.aliases).toHaveLength(19);
|
||||
expect(first.description.length).toBeLessThanOrEqual(2_000);
|
||||
expect(first.description).toBe("d".repeat(1_999));
|
||||
expect(first.args?.split(" ")).toHaveLength(20);
|
||||
expect(first.args?.split(" ")[0]).toBe("[" + "n".repeat(199) + "]");
|
||||
expect(first.argOptions).toHaveLength(50);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
// Control UI chat domain owns pure slash command rules.
|
||||
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import type { CommandEntry } from "../../../../packages/gateway-protocol/src/index.js";
|
||||
import { buildBuiltinChatCommands } from "../../../../src/auto-reply/commands-registry.shared.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../string-coerce.ts";
|
||||
|
|
@ -253,7 +255,7 @@ function normalizeSlashIdentifier(raw: string): string | null {
|
|||
|
||||
function clampText(value: unknown, maxLength: number): string {
|
||||
const text = typeof value === "string" ? value : "";
|
||||
return text.length > maxLength ? text.slice(0, maxLength) : text;
|
||||
return text.length > maxLength ? truncateUtf16Safe(text, maxLength) : text;
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
|
|
|
|||
|
|
@ -562,6 +562,55 @@ describe("renderWorkboard", () => {
|
|||
expect(container.textContent).toContain("Needs proof.");
|
||||
});
|
||||
|
||||
it("keeps bounded diagnostic badges UTF-16 safe", () => {
|
||||
const host = {};
|
||||
const state = getWorkboardState(host);
|
||||
state.loaded = true;
|
||||
state.cards = [
|
||||
{
|
||||
id: "card-boundary",
|
||||
title: "Boundary badge",
|
||||
status: "todo",
|
||||
priority: "normal",
|
||||
labels: [],
|
||||
position: 1000,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
metadata: {
|
||||
diagnostics: [
|
||||
{
|
||||
kind: "protocol_violation",
|
||||
severity: "warning",
|
||||
title: `${"x".repeat(62)}🚀tail`,
|
||||
detail: "Boundary detail.",
|
||||
firstSeenAt: 1,
|
||||
lastSeenAt: 1,
|
||||
count: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
const container = document.createElement("div");
|
||||
|
||||
render(
|
||||
renderWorkboard({
|
||||
host,
|
||||
client: null,
|
||||
connected: true,
|
||||
pluginEnabled: true,
|
||||
agentsList: null,
|
||||
sessions: [],
|
||||
onOpenSession: () => undefined,
|
||||
}),
|
||||
container,
|
||||
);
|
||||
|
||||
expect(container.querySelector(".workboard-card__badge--warning")?.textContent?.trim()).toBe(
|
||||
`${"x".repeat(62)}…`,
|
||||
);
|
||||
});
|
||||
|
||||
it("renders sub-minute heartbeat ages with the duration count interpolation", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-06-13T12:00:00Z"));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
// Control UI view renders workboard screen content.
|
||||
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import { html, nothing, type TemplateResult } from "lit";
|
||||
import { ref } from "lit/directives/ref.js";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
|
|
@ -206,7 +208,9 @@ function formatAge(value: number | undefined): string {
|
|||
|
||||
function truncateBadgeText(value: string, maxLength = 64): string {
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length <= maxLength ? trimmed : `${trimmed.slice(0, maxLength - 1)}…`;
|
||||
return trimmed.length <= maxLength
|
||||
? trimmed
|
||||
: `${truncateUtf16Safe(trimmed, Math.max(0, maxLength - 1))}…`;
|
||||
}
|
||||
|
||||
function canMutate(props: WorkboardProps): boolean {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue