From 76d943e625675d5fbcde457f3ca7e24df49bcda5 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 29 Sep 2026 11:44:35 -0500 Subject: [PATCH] Bound Cursor child summaries without splitting Unicode characters Co-Authored-By: Paperclip --- .../src/drivers/acpx/cursor-extensions.test.ts | 15 +++++++++++++++ .../src/drivers/acpx/cursor-extensions.ts | 14 +++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.test.ts b/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.test.ts index 3aae9725bc..b5b3560ae5 100644 --- a/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.test.ts @@ -2,6 +2,7 @@ import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; +import { validateAcpxRichEvent } from "./profile-extensions.js"; import { PAPERCLIP_QUESTION_RESPONSE_SCHEMA } from "../../contracts/question-set.js"; import { createCursorNotificationNormalizer, createCursorProfileExtensionAdapter, createCursorSubagentNormalizer, cursorWorkspaceArtifactReference, @@ -144,6 +145,20 @@ describe("Cursor active-turn extension adapter", () => { await expect(createCursorProfileExtensionAdapter({ ...context, turnId: "next" }).notification("cursor/subagent_update", { sessionId: "parent", update: state })).rejects.toThrow("no spawn"); }); + it.each(["😀".repeat(2_500), "😀".repeat(5_000), "漢😀".repeat(2_000)])("bounds Unicode child activity for UTF-16 consumers without splitting code points (%#)", async message => { + const adapter = createCursorProfileExtensionAdapter({ workspacePath: await workspace(), sessionId: "parent", turnId: "turn" }); + await adapter.notification("cursor/subagent_update", { sessionId: "parent", update: { sessionUpdate: "subagent_spawned", subagentSessionId: "child", name: "Explore", task: "", _meta: { cursor: { toolCallId: "task", agentId: "agent" } } } }); + const events = await adapter.notification("cursor/subagent_update", { sessionId: "parent", childSessionId: "child", update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: message } } }); + const event = events[0]!; + expect(() => validateAcpxRichEvent(event)).not.toThrow(); + const summary = (event.payload.children as { activitySummary: string }[])[0]!.activitySummary; + expect(summary.length).toBeLessThanOrEqual(4_000); + expect(summary.isWellFormed()).toBe(true); + expect(summary).toMatch(/^\[Earlier child activity omitted\]\n/); + const tail = summary.slice("[Earlier child activity omitted]\n".length); + expect(message.endsWith(tail)).toBe(true); + }); + it("attributes nested child transcripts, reports partial tool detail, and settles disconnected children honestly", async () => { const adapter = createCursorProfileExtensionAdapter({ workspacePath: await workspace(), sessionId: "parent", turnId: "turn" }); const child = { sessionUpdate: "subagent_spawned", subagentSessionId: "child", name: "Explore", task: "", _meta: { cursor: { toolCallId: "task", agentId: "agent" } } }; diff --git a/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.ts b/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.ts index d28ad4168f..8516cb28b6 100644 --- a/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.ts +++ b/packages/paperclip-runner/src/drivers/acpx/cursor-extensions.ts @@ -369,8 +369,20 @@ function cursorChildActivitySummary(update: Record): { text: st } function boundedChildActivity(text: string): string { + if (text.length <= 4_000) return text; + const prefix = "[Earlier child activity omitted]\n"; const points = Array.from(text); - return points.length <= 4_000 ? text : `[Earlier child activity omitted]\n${points.slice(-3_960).join("")}`; + const tail: string[] = []; + let length = prefix.length; + // Bound both UTF-16 consumers and the schema's Unicode code-point count. + // Keep each complete code point when retaining the most recent activity. + for (let index = points.length - 1; index >= 0; index -= 1) { + const point = points[index]!; + if (length + point.length > 4_000) break; + tail.push(point); + length += point.length; + } + return prefix + tail.reverse().join(""); } /** Preserve identity across delta state updates; use a fresh reducer per turn. */