diff --git a/packages/ai/src/dialect/rendering.ts b/packages/ai/src/dialect/rendering.ts
index 882961f65..94b69b403 100644
--- a/packages/ai/src/dialect/rendering.ts
+++ b/packages/ai/src/dialect/rendering.ts
@@ -161,20 +161,55 @@ function isAsciiWhitespace(code: number): boolean {
return code === 9 || code === 10 || code === 11 || code === 12 || code === 13 || code === 32;
}
-function unwrapDelimitedThinking(open: string, close: string, text: string): string {
- let start = 0;
- let end = text.length;
- let changed = false;
- while (true) {
- while (start < end && isAsciiWhitespace(text.charCodeAt(start))) start++;
- while (end > start && isAsciiWhitespace(text.charCodeAt(end - 1))) end--;
- if (end - start < open.length + close.length) break;
- if (!text.startsWith(open, start) || !text.startsWith(close, end - close.length)) break;
- start += open.length;
- end -= close.length;
- changed = true;
+function trimAsciiStart(text: string, start: number, end: number): number {
+ let cursor = start;
+ while (cursor < end && isAsciiWhitespace(text.charCodeAt(cursor))) cursor++;
+ return cursor;
+}
+
+function trimAsciiEnd(text: string, start: number, end: number): number {
+ let cursor = end;
+ while (cursor > start && isAsciiWhitespace(text.charCodeAt(cursor - 1))) cursor--;
+ return cursor;
+}
+
+function findDelimitedThinkingClose(open: string, close: string, text: string, start: number, end: number): number {
+ let depth = 1;
+ let cursor = start;
+ while (cursor < end) {
+ const nextClose = text.indexOf(close, cursor);
+ if (nextClose < 0 || nextClose >= end) return -1;
+ const nextOpen = text.indexOf(open, cursor);
+ if (nextOpen >= 0 && nextOpen < nextClose) {
+ depth++;
+ cursor = nextOpen + open.length;
+ continue;
+ }
+ depth--;
+ if (depth === 0) return nextClose;
+ cursor = nextClose + close.length;
}
- return changed ? text.slice(start, end) : text;
+ return -1;
+}
+
+function unwrapDelimitedThinking(open: string, close: string, text: string): string {
+ const end = trimAsciiEnd(text, 0, text.length);
+ let cursor = trimAsciiStart(text, 0, end);
+ if (cursor >= end || !text.startsWith(open, cursor)) return text;
+
+ const segments: string[] = [];
+ while (cursor < end) {
+ if (!text.startsWith(open, cursor)) return text;
+ const innerStart = cursor + open.length;
+ const innerEnd = findDelimitedThinkingClose(open, close, text, innerStart, end);
+ if (innerEnd < 0) return text;
+
+ const trimmedInnerEnd = trimAsciiEnd(text, innerStart, innerEnd);
+ const trimmedInnerStart = trimAsciiStart(text, innerStart, trimmedInnerEnd);
+ segments.push(unwrapDelimitedThinking(open, close, text.slice(trimmedInnerStart, trimmedInnerEnd)));
+ cursor = trimAsciiStart(text, innerEnd + close.length, end);
+ }
+ return segments.join("\n");
}
export function renderDelimitedThinking(open: string, close: string, text: string): string {
diff --git a/packages/coding-agent/src/advisor/__tests__/advisor.test.ts b/packages/coding-agent/src/advisor/__tests__/advisor.test.ts
index e707079e8..a1dbf51b0 100644
--- a/packages/coding-agent/src/advisor/__tests__/advisor.test.ts
+++ b/packages/coding-agent/src/advisor/__tests__/advisor.test.ts
@@ -605,5 +605,26 @@ describe("advisor", () => {
expect(md).toContain("Assistant: \nCheck logs before accepting container health.\n");
expect(md).not.toContain("\n");
});
+
+ it("unwraps sibling literal thinking envelopes independently", () => {
+ const md = formatSessionDumpText({
+ messages: [
+ {
+ role: "assistant",
+ content: [
+ { type: "thinking", thinking: "\nfirst\n" },
+ { type: "toolCall", id: "tc-1", name: "read", arguments: { path: "file.ts" } },
+ { type: "thinking", thinking: "\nsecond\n" },
+ ],
+ timestamp: Date.now(),
+ } as AgentMessage,
+ ],
+ tools: [{ name: "read", description: "Read a file", parameters: { type: "object" } }],
+ thinkingLevel: "high",
+ });
+
+ expect(md).toContain("Assistant: \nfirst\nsecond\n");
+ expect(md).not.toContain("first\n\n\nsecond");
+ });
});
});