Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/guides/sidecars.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,5 @@ Claude Code OAuth fingerprint precedent, but should be soak-tested with the
intended account and workload.

See the [Configuration reference](/reference/configuration/server/#sidecars) for every field.

Long search queries and answers are shortened without splitting a UTF-16 surrogate pair, so a single character such as 😀 is never cut in half by the result limits. A sequence of several characters, such as emoji joined with a zero-width joiner, can still be cut between its parts.
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/ko/guides/sidecars.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,5 @@ OpenCodex가 `web_search` 가로채기를 멈추고 Codex 통합이 `~/.codex/co
파일을 직접 고치고 싶다면 이전처럼 `config.json`에서 `enabled`를 `false`로 두면 됩니다. Anthropic OAuth 검색과 이미지 설명은 기존 Claude Code OAuth
fingerprint 선례를 따르지만, 실제 계정과 작업량으로 충분히 soak test하는 편이 좋습니다. 전체
필드는 [설정 레퍼런스](/ko/reference/configuration/server/#sidecars)를 참고하세요.

긴 검색어와 답변은 UTF-16 서로게이트 쌍을 나누지 않는 위치에서 줄이므로, 결과 길이 제한 때문에 😀 같은 한 글자가 반으로 잘리지 않습니다. 다만 제로 폭 결합자(ZWJ)로 이어진 이모지처럼 여러 글자로 이루어진 조합은 중간에서 잘릴 수 있습니다.
1 change: 1 addition & 0 deletions scripts/test-layout/layout-adapters.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"fix-search-unicode-clamp.test.ts": "web-search",
"cursor-native-grep-globs.test.ts": "providers/cursor",
"tool-identity-hash.test.ts": "adapters",
"eventstream-fragmentation.test.ts": "responses",
Expand Down
5 changes: 3 additions & 2 deletions src/web-search/format-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ const MAX_TOTAL_CHARS = 8000;
/** Bound for a batched error status so every query's status line stays small. */
const MAX_BATCH_ERROR_CHARS = 300;

/** Truncate to at most `max` UTF-16 units without splitting a surrogate pair, then mark the cut. */
function clamp(s: string, max: number): string {
return s.length <= max ? s : `${s.slice(0, max)}\n…[truncated]`;
return s.length <= max ? s : `${safePrefix(s, max)}\n…[truncated]`;
}

/** A prefix of at most `max` UTF-16 units that never ends on half of a surrogate pair. */
Expand All @@ -25,7 +26,7 @@ function safePrefix(s: string, max: number): string {

/** Clamp and sanitize a query string for display in the tool result boundary. */
function safeQuery(q: string): string {
const clamped = q.length <= 200 ? q : `${q.slice(0, 200)}…`;
const clamped = q.length <= 200 ? q : `${safePrefix(q, 200)}…`;
// Strip angle brackets so the query can't close/open XML-ish boundary tags.
return clamped.replace(/[<>]/g, "");
}
Expand Down
2 changes: 2 additions & 0 deletions structure/data-planes/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,5 @@ routing. Do not infer an exact account from caller-supplied account headers, pro
selection, connection identity, or other ambient state; concurrent threads could cross-route
credentials. Extending exact routing to those endpoints requires an opaque client correlation that
can be bound server-side to a previously validated selector.

Single-result query and answer truncation in `src/web-search/format-result.ts` uses the same surrogate-safe prefix as batched results. The existing UTF-16 limits and truncation markers remain unchanged.
4 changes: 3 additions & 1 deletion tests/fixtures/test-layout-expected.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{ "provider-input-modalities.test.ts": "providers", "turn-membership.test.ts": "responses", "turn-membership-ingress.test.ts": "responses", "workbuddy-quota.test.ts": "providers", "workbuddy-login.test.ts": "oauth", "workbuddy-adapter.test.ts": "adapters", "workbuddy-provider.test.ts": "providers", "link-tunnel-history.test.ts": "clients", "link-readiness.test.ts": "clients", "link-diagnostics.test.ts": "clients", "combo-description.test.ts": "routing", "combo-description-catalog.test.ts": "codex-integration", "catalog-auto-review-override.test.ts": "codex-integration", "management-auto-review-settings.test.ts": "server", "management-auto-review-auth.test.ts": "server", "prefer-model-alias-ids.test.ts": "server", "responses-mimo-envelope.test.ts": "responses", "responses-call-id-linkage.test.ts": "responses", "responses-phase-inference.test.ts": "responses",
{
"fix-search-unicode-clamp.test.ts": "web-search",
"provider-input-modalities.test.ts": "providers", "turn-membership.test.ts": "responses", "turn-membership-ingress.test.ts": "responses", "workbuddy-quota.test.ts": "providers", "workbuddy-login.test.ts": "oauth", "workbuddy-adapter.test.ts": "adapters", "workbuddy-provider.test.ts": "providers", "link-tunnel-history.test.ts": "clients", "link-readiness.test.ts": "clients", "link-diagnostics.test.ts": "clients", "combo-description.test.ts": "routing", "combo-description-catalog.test.ts": "codex-integration", "catalog-auto-review-override.test.ts": "codex-integration", "management-auto-review-settings.test.ts": "server", "management-auto-review-auth.test.ts": "server", "prefer-model-alias-ids.test.ts": "server", "responses-mimo-envelope.test.ts": "responses", "responses-call-id-linkage.test.ts": "responses", "responses-phase-inference.test.ts": "responses",
"claude-intercept-acl-barrier.test.ts": "claude-integration",
"codex-catalog-heal-reanchor.test.ts": "codex-integration",
"tool-identity-hash.test.ts": "adapters",
Expand Down
27 changes: 27 additions & 0 deletions tests/web-search/fix-search-unicode-clamp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, test } from "bun:test";
import { formatWebSearchResult } from "../../src/web-search/format-result";

describe("single search result Unicode boundaries", () => {
test.each([false, true])("never splits query or answer surrogate pairs (structured=%s)", structured => {
const query = "q".repeat(199) + "😀tail";
const text = "a".repeat(3999) + "😀tail";
const result = formatWebSearchResult(query, { text, sources: [] }, structured);
const shown = structured ? JSON.parse(result.split("\n")[1]!) : { query: result, answer: result };
expect(shown.query).not.toMatch(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/u);
expect(shown.answer).not.toMatch(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/u);
expect(shown.answer).toContain("…[truncated]");
});
test("preserves complete pairs exactly at the limit", () => {
const text = "a".repeat(3998) + "😀";
const result = formatWebSearchResult("q".repeat(198) + "😀", { text, sources: [] }, true);
Comment thread
oocheol marked this conversation as resolved.
Resolved
const data = JSON.parse(result.split("\n")[1]!);
expect(data.answer).toBe(text);
expect(data.query).toEndWith("😀");
});
test("keeps a pair that ends exactly at the limit when truncation occurs", () => {
const result = formatWebSearchResult("q".repeat(198) + "😀tail", { text: "a".repeat(3998) + "😀tail", sources: [] }, true);
const data = JSON.parse(result.split("\n")[1]!);
expect(data.answer).toBe("a".repeat(3998) + "😀\n…[truncated]");
expect(data.query).toBe("q".repeat(198) + "😀…");
});
});
Loading