Skip to content

Commit 21f9a81

Browse files
committed
fix: make ACP projection truncation UTF-16 safe
1 parent e9a2f78 commit 21f9a81

3 files changed

Lines changed: 33 additions & 74 deletions

File tree

src/auto-reply/reply/acp-projector-truncation.test.ts

Lines changed: 0 additions & 70 deletions
This file was deleted.

src/auto-reply/reply/acp-projector.test.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -761,7 +761,33 @@ describe("createAcpReplyProjector", () => {
761761
]);
762762
});
763763

764-
it("truncates oversized turns once and emits one truncation notice", async () => {
764+
it("truncates status updates without splitting surrogate pairs", async () => {
765+
const { deliveries, projector } = createProjectorHarness(
766+
createLiveCfgOverrides({
767+
coalesceIdleMs: 0,
768+
maxChunkChars: 256,
769+
maxSessionUpdateChars: 64,
770+
tagVisibility: {
771+
memory_summary: true,
772+
},
773+
}),
774+
);
775+
776+
await projector.onEvent({
777+
type: "status",
778+
tag: "memory_summary",
779+
text: `${"a".repeat(62)}🎉${"b".repeat(10)}`,
780+
});
781+
782+
expect(deliveries).toEqual([
783+
{
784+
kind: "tool",
785+
text: prefixSystemMessage(`${"a".repeat(62)}…`),
786+
},
787+
]);
788+
});
789+
790+
it("truncates oversized turns at code-point boundaries and emits one notice", async () => {
765791
const { deliveries, projector } = createProjectorHarness({
766792
acp: {
767793
enabled: true,
@@ -776,7 +802,7 @@ describe("createAcpReplyProjector", () => {
776802

777803
await projector.onEvent({
778804
type: "text_delta",
779-
text: "hello world",
805+
text: "abcd😀 tail",
780806
tag: "agent_message_chunk",
781807
});
782808
await projector.onEvent({
@@ -787,7 +813,7 @@ describe("createAcpReplyProjector", () => {
787813
await projector.flush(true);
788814

789815
expect(deliveries).toEqual([
790-
{ kind: "block", text: "hello" },
816+
{ kind: "block", text: "abcd" },
791817
{
792818
kind: "tool",
793819
text: prefixSystemMessage("output truncated"),

src/auto-reply/reply/acp-projector.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ export function createAcpReplyProjector(params: {
463463
return;
464464
}
465465
const remaining = settings.maxOutputChars - emittedOutputChars;
466-
const accepted = remaining < text.length ? text.slice(0, remaining) : text;
466+
const accepted = remaining < text.length ? truncateUtf16Safe(text, remaining) : text;
467467
if (accepted.length > 0) {
468468
emittedOutputChars += accepted.length;
469469
lastVisibleOutputTail = accepted.slice(-1);
@@ -480,6 +480,9 @@ export function createAcpReplyProjector(params: {
480480
}
481481
}
482482
if (accepted.length < text.length) {
483+
// A split code point can leave the accepted prefix shorter than the remaining budget.
484+
// Exhaust it after any drop so later deltas cannot skip past omitted text.
485+
emittedOutputChars = settings.maxOutputChars;
483486
await emitTruncationNotice();
484487
}
485488
return;

0 commit comments

Comments
 (0)