Skip to content

Commit d691df7

Browse files
authored
Merge pull request #109 from odefun/feat/chunk-long-result-messages-366539
fix: split oversized final results into indexed chunks
2 parents 9571fe3 + e34f019 commit d691df7

File tree

3 files changed

+109
-5
lines changed

3 files changed

+109
-5
lines changed

packages/core/runtime.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { recoverPendingRequests as recoverPendingRequestsInternal } from "@/core
2525
import { prepareRuntimeSession } from "@/core/runtime/session-bootstrap";
2626
import { runOpenRequest } from "@/core/runtime/open-request";
2727
import { buildMessageOptions } from "@/core/runtime/message-options";
28+
import { splitResultMessage } from "@/core/runtime/result-message";
2829
import type { OpenCodeOptions } from "@/agents";
2930

3031
type RuntimeDeps = {
@@ -160,19 +161,34 @@ export function createCoreRuntime(deps: RuntimeDeps) {
160161
text: string;
161162
}): Promise<void> {
162163
const { channelId, threadId, statusTs, text } = params;
163-
if (resolveStatusMessageFormat() === "aggressive") {
164-
await deps.im.sendMessage(channelId, threadId, text, true);
164+
const statusFormat = resolveStatusMessageFormat();
165+
const finalChunks = splitResultMessage(text);
166+
const singleChunk = finalChunks[0] ?? text;
167+
168+
if (finalChunks.length > 1) {
169+
if (statusFormat !== "aggressive") {
170+
await deps.im.updateMessage(channelId, statusTs, "Final result posted below in multiple messages.", false);
171+
}
172+
173+
for (const chunk of finalChunks) {
174+
await deps.im.sendMessage(channelId, threadId, chunk, true);
175+
}
176+
return;
177+
}
178+
179+
if (statusFormat === "aggressive") {
180+
await deps.im.sendMessage(channelId, threadId, singleChunk, true);
165181
return;
166182
}
167183

168184
const maxEditableMessageChars = deps.im.maxEditableMessageChars;
169185
if (typeof maxEditableMessageChars === "number" && text.length > maxEditableMessageChars) {
170-
await deps.im.updateMessage(channelId, statusTs, "_Final result posted below._", false);
171-
await deps.im.sendMessage(channelId, threadId, text, true);
186+
await deps.im.updateMessage(channelId, statusTs, "Final result posted below.", false);
187+
await deps.im.sendMessage(channelId, threadId, singleChunk, true);
172188
return;
173189
}
174190

175-
await deps.im.updateMessage(channelId, statusTs, text, true);
191+
await deps.im.updateMessage(channelId, statusTs, singleChunk, true);
176192
}
177193

178194
async function handleUserMessageInternal(context: CoreMessageContext, text: string): Promise<void> {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const DEFAULT_RESULT_MESSAGE_LIMIT = 3_000;
2+
3+
function splitTextByLimit(text: string, maxChars: number): string[] {
4+
const chunks: string[] = [];
5+
let cursor = 0;
6+
7+
while (cursor < text.length) {
8+
const remaining = text.length - cursor;
9+
if (remaining <= maxChars) {
10+
chunks.push(text.slice(cursor));
11+
break;
12+
}
13+
14+
const slice = text.slice(cursor, cursor + maxChars);
15+
let splitAt = slice.lastIndexOf("\n");
16+
if (splitAt <= 0) {
17+
splitAt = slice.lastIndexOf(" ");
18+
}
19+
if (splitAt <= 0) {
20+
splitAt = maxChars;
21+
}
22+
23+
const nextChunk = text.slice(cursor, cursor + splitAt);
24+
if (nextChunk.length > 0) {
25+
chunks.push(nextChunk);
26+
}
27+
28+
cursor += splitAt;
29+
while (cursor < text.length && /\s/.test(text[cursor] || "")) {
30+
cursor += 1;
31+
}
32+
}
33+
34+
return chunks.length > 0 ? chunks : [text];
35+
}
36+
37+
export function splitResultMessage(text: string, maxChars = DEFAULT_RESULT_MESSAGE_LIMIT): string[] {
38+
if (text.length <= maxChars) {
39+
return [text];
40+
}
41+
42+
let estimatedCount = 1;
43+
let contentChunks: string[] = [];
44+
45+
while (true) {
46+
const prefixLength = `(${estimatedCount}/${estimatedCount}) `.length;
47+
const payloadLimit = Math.max(1, maxChars - prefixLength);
48+
contentChunks = splitTextByLimit(text, payloadLimit);
49+
50+
if (contentChunks.length === estimatedCount) {
51+
break;
52+
}
53+
54+
estimatedCount = contentChunks.length;
55+
}
56+
57+
const total = contentChunks.length;
58+
return contentChunks.map((chunk, index) => `(${index + 1}/${total}) ${chunk}`);
59+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { describe, expect, it } from "bun:test";
2+
import { splitResultMessage } from "../runtime/result-message";
3+
4+
describe("splitResultMessage", () => {
5+
it("keeps short messages as one chunk", () => {
6+
const chunks = splitResultMessage("hello", 3000);
7+
expect(chunks).toEqual(["hello"]);
8+
});
9+
10+
it("splits long messages with indexed prefixes", () => {
11+
const text = "a".repeat(6200);
12+
const chunks = splitResultMessage(text, 3000);
13+
14+
expect(chunks.length).toBeGreaterThan(1);
15+
expect(chunks[0]?.startsWith("(1/")).toBe(true);
16+
expect(chunks[chunks.length - 1]?.startsWith(`(${chunks.length}/${chunks.length}) `)).toBe(true);
17+
for (const chunk of chunks) {
18+
expect(chunk.length).toBeLessThanOrEqual(3000);
19+
}
20+
});
21+
22+
it("prefers newline boundaries when available", () => {
23+
const text = `${"a".repeat(2990)}\n${"b".repeat(2990)}`;
24+
const chunks = splitResultMessage(text, 3000);
25+
26+
expect(chunks.length).toBe(2);
27+
expect(chunks[0]?.includes("\n")).toBe(false);
28+
});
29+
});

0 commit comments

Comments
 (0)