From 5877d1352a37c45857b45156cbcd814f906ec2eb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 31 Oct 2026 10:03:32 +0000 Subject: [PATCH 1/3] test(shared): reproduce #28075 citations with long message IDs Co-Authored-By: Claude Opus 5.5 --- .../shared/src/assistantCitations.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/shared/src/assistantCitations.test.ts b/packages/shared/src/assistantCitations.test.ts index 923ca2555a0be..c565ccdeb9169 100644 --- a/packages/shared/src/assistantCitations.test.ts +++ b/packages/shared/src/assistantCitations.test.ts @@ -154,6 +154,30 @@ describe("assistant citation references", () => { ); }); + it("round-trips citations of provider messages whose IDs nest the thread ID", () => { + const threadId = ThreadId.make( + "thread:project:7a0c2f3e-5b1d-4c8a-9e6f-2d4b8a1c3e5f:0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", + ); + const nest = (outer: string, inner: string) => `${outer}:${encodeURIComponent(inner)}`; + const providerTurnId = nest( + "provider-turn:provider:pi:native-turn", + nest(threadId, `${threadId}:run:1`), + ); + const messageId = MessageId.make( + nest( + "message:provider:pi:native-item", + nest(providerTurnId, `${providerTurnId}:assistant:0`), + ), + ); + const piCitation: AssistantCitation = { ...citation, threadId, messageId }; + const marker = serializeAssistantCitation(piCitation); + + expect(messageId.length).toBeGreaterThan(512); + expect(collectAssistantCitations(marker)).toEqual([ + { citation: piCitation, source: marker, start: 0, end: marker.length }, + ]); + }); + it("edits only the bound comment and trims only its outer whitespace", () => { const original = Object.freeze({ ...citation, comment: "Previous comment" }); const comment = ' \n\tPlease keep "日本語".\n Preserve indentation and `code`.\t '; From 6ae1ef89ea3960b4eb0c80bab06f287efd4efb3e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 31 Oct 2026 10:08:39 +0000 Subject: [PATCH 2/3] fix(shared): cite assistant messages with IDs over 512 characters Pi assistant message IDs nest and re-encode their thread and run IDs, so they reach 533-684 characters. AssistantCitation capped each ID at 512, so the Cite action built links the parser rejected and the composer showed raw markdown. Raise the per-ID cap to ASSISTANT_CITATION_MAX_ID_LENGTH (4,096) and derive the href limit from every bounded field, so any citation that decodes still fits under it. The link format is unchanged. Co-Authored-By: Claude Opus 5.5 --- packages/contracts/src/assistantCitations.ts | 8 +++++--- packages/shared/src/assistantCitations.test.ts | 7 ++++--- packages/shared/src/assistantCitations.ts | 11 +++++++++-- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/contracts/src/assistantCitations.ts b/packages/contracts/src/assistantCitations.ts index 71934d1684ab0..96572a4bfa656 100644 --- a/packages/contracts/src/assistantCitations.ts +++ b/packages/contracts/src/assistantCitations.ts @@ -4,13 +4,15 @@ import { EnvironmentId, MessageId, NonNegativeInt, ThreadId } from "./baseSchema export const ASSISTANT_CITATION_MAX_TEXT_LENGTH = 8_000; export const ASSISTANT_CITATION_MAX_COMMENT_LENGTH = 8_000; export const ASSISTANT_CITATION_CONTEXT_LENGTH = 32; +// Provider message IDs nest and re-encode their thread and turn IDs, so they outgrow a few hundred characters. +export const ASSISTANT_CITATION_MAX_ID_LENGTH = 4_096; /** Positions are UTF-16 offsets, not Markdown offsets. */ export const AssistantCitation = Schema.Struct({ version: Schema.Literal(1), - environmentId: EnvironmentId.check(Schema.isMaxLength(512)), - threadId: ThreadId.check(Schema.isMaxLength(512)), - messageId: MessageId.check(Schema.isMaxLength(512)), + environmentId: EnvironmentId.check(Schema.isMaxLength(ASSISTANT_CITATION_MAX_ID_LENGTH)), + threadId: ThreadId.check(Schema.isMaxLength(ASSISTANT_CITATION_MAX_ID_LENGTH)), + messageId: MessageId.check(Schema.isMaxLength(ASSISTANT_CITATION_MAX_ID_LENGTH)), text: Schema.String.check( Schema.isNonEmpty(), Schema.isMaxLength(ASSISTANT_CITATION_MAX_TEXT_LENGTH), diff --git a/packages/shared/src/assistantCitations.test.ts b/packages/shared/src/assistantCitations.test.ts index c565ccdeb9169..206ca93c5769f 100644 --- a/packages/shared/src/assistantCitations.test.ts +++ b/packages/shared/src/assistantCitations.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vite-plus/test"; import { ASSISTANT_CITATION_CONTEXT_LENGTH, ASSISTANT_CITATION_MAX_COMMENT_LENGTH, + ASSISTANT_CITATION_MAX_ID_LENGTH, ASSISTANT_CITATION_MAX_TEXT_LENGTH, EnvironmentId, MessageId, @@ -125,9 +126,9 @@ describe("assistant citation references", () => { it("accepts complete 8k CJK quotes and comments with maximum-sized source selectors", () => { const largeCitation: AssistantCitation = { ...citation, - environmentId: EnvironmentId.make("環".repeat(512)), - threadId: ThreadId.make("線".repeat(512)), - messageId: MessageId.make("文".repeat(512)), + environmentId: EnvironmentId.make("環".repeat(ASSISTANT_CITATION_MAX_ID_LENGTH)), + threadId: ThreadId.make("線".repeat(ASSISTANT_CITATION_MAX_ID_LENGTH)), + messageId: MessageId.make("文".repeat(ASSISTANT_CITATION_MAX_ID_LENGTH)), text: "引".repeat(ASSISTANT_CITATION_MAX_TEXT_LENGTH), comment: "注".repeat(ASSISTANT_CITATION_MAX_COMMENT_LENGTH), start: Number.MAX_SAFE_INTEGER - ASSISTANT_CITATION_MAX_TEXT_LENGTH, diff --git a/packages/shared/src/assistantCitations.ts b/packages/shared/src/assistantCitations.ts index 9af6a29706724..420708815efcb 100644 --- a/packages/shared/src/assistantCitations.ts +++ b/packages/shared/src/assistantCitations.ts @@ -1,5 +1,7 @@ import { + ASSISTANT_CITATION_CONTEXT_LENGTH, ASSISTANT_CITATION_MAX_COMMENT_LENGTH, + ASSISTANT_CITATION_MAX_ID_LENGTH, ASSISTANT_CITATION_MAX_TEXT_LENGTH, AssistantCitation, } from "@t3tools/contracts"; @@ -8,9 +10,14 @@ import * as Schema from "effect/Schema"; const CITATION_PROTOCOL = "t3-citation:"; const CITATION_HREF_PREFIX = `${CITATION_PROTOCOL}//v1/`; -// Percent encoding needs up to nine characters per UTF-16 code unit; 16k covers selectors. +// Percent encoding needs up to nine characters per UTF-16 code unit; 1k covers keys and offsets. const MAX_CITATION_HREF_LENGTH = - 9 * (ASSISTANT_CITATION_MAX_TEXT_LENGTH + ASSISTANT_CITATION_MAX_COMMENT_LENGTH) + 16_000; + 9 * + (ASSISTANT_CITATION_MAX_TEXT_LENGTH + + ASSISTANT_CITATION_MAX_COMMENT_LENGTH + + 3 * ASSISTANT_CITATION_MAX_ID_LENGTH + + 2 * ASSISTANT_CITATION_CONTEXT_LENGTH) + + 1_000; const CITATION_LINK = new RegExp( String.raw`\[Assistant quote\]\((${CITATION_HREF_PREFIX}[^\s)]{1,${MAX_CITATION_HREF_LENGTH - CITATION_HREF_PREFIX.length}})\)`, "g", From 7e0a9808a311fb49c2320eecdfa9ebb85d335a71 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Nov 2026 23:54:03 +0000 Subject: [PATCH 3/3] test(shared): drop redundant nested provider ID citation case AI-assisted; verified with the tests and tools listed above. --- .../shared/src/assistantCitations.test.ts | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/packages/shared/src/assistantCitations.test.ts b/packages/shared/src/assistantCitations.test.ts index 32ca5f0fa0878..7dbaaf9d8bdeb 100644 --- a/packages/shared/src/assistantCitations.test.ts +++ b/packages/shared/src/assistantCitations.test.ts @@ -156,30 +156,6 @@ describe("assistant citation references", () => { ); }); - it("round-trips citations of provider messages whose IDs nest the thread ID", () => { - const threadId = ThreadId.make( - "thread:project:7a0c2f3e-5b1d-4c8a-9e6f-2d4b8a1c3e5f:0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", - ); - const nest = (outer: string, inner: string) => `${outer}:${encodeURIComponent(inner)}`; - const providerTurnId = nest( - "provider-turn:provider:pi:native-turn", - nest(threadId, `${threadId}:run:1`), - ); - const messageId = MessageId.make( - nest( - "message:provider:pi:native-item", - nest(providerTurnId, `${providerTurnId}:assistant:0`), - ), - ); - const piCitation: AssistantCitation = { ...citation, threadId, messageId }; - const marker = serializeAssistantCitation(piCitation); - - expect(messageId.length).toBeGreaterThan(512); - expect(collectAssistantCitations(marker)).toEqual([ - { citation: piCitation, source: marker, start: 0, end: marker.length }, - ]); - }); - it("edits only the bound comment and trims only its outer whitespace", () => { const original = Object.freeze({ ...citation, comment: "Previous comment" }); const comment = ' \n\tPlease keep "日本語".\n Preserve indentation and `code`.\t ';