mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-12 18:11:51 +08:00
Merge pull request #1900 from davekim917/fix/discord-truncation-and-session-persist
fix: persist SDK session_id on init + split long outbound messages
This commit is contained in:
@@ -322,6 +322,13 @@ async function processQuery(query: AgentQuery, routing: RoutingContext): Promise
|
||||
|
||||
if (event.type === 'init') {
|
||||
queryContinuation = event.continuation;
|
||||
// Persist immediately so a mid-turn container crash still lets the
|
||||
// next wake resume the conversation. Without this, the session id
|
||||
// was only written after the full stream completed — if the
|
||||
// container died between `init` and `result`, the SDK session was
|
||||
// effectively orphaned and the next message started a blank
|
||||
// Claude session with no prior context.
|
||||
setStoredSessionId(event.continuation);
|
||||
} else if (event.type === 'result' && event.text) {
|
||||
dispatchResultText(event.text, routing);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,40 @@ import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { Adapter } from 'chat';
|
||||
|
||||
import { createChatSdkBridge } from './chat-sdk-bridge.js';
|
||||
import { createChatSdkBridge, splitForLimit } from './chat-sdk-bridge.js';
|
||||
|
||||
function stubAdapter(partial: Partial<Adapter>): Adapter {
|
||||
return { name: 'stub', ...partial } as unknown as Adapter;
|
||||
}
|
||||
|
||||
describe('splitForLimit', () => {
|
||||
it('returns a single chunk when text fits', () => {
|
||||
expect(splitForLimit('short text', 100)).toEqual(['short text']);
|
||||
});
|
||||
|
||||
it('splits on paragraph boundaries when available', () => {
|
||||
const text = 'para one line one\npara one line two\n\npara two line one\npara two line two';
|
||||
const chunks = splitForLimit(text, 40);
|
||||
expect(chunks.length).toBeGreaterThan(1);
|
||||
for (const c of chunks) expect(c.length).toBeLessThanOrEqual(40);
|
||||
});
|
||||
|
||||
it('falls back to line boundaries when no paragraph fits', () => {
|
||||
const text = 'alpha\nbravo\ncharlie\ndelta\necho\nfoxtrot';
|
||||
const chunks = splitForLimit(text, 15);
|
||||
expect(chunks.length).toBeGreaterThan(1);
|
||||
for (const c of chunks) expect(c.length).toBeLessThanOrEqual(15);
|
||||
});
|
||||
|
||||
it('hard-cuts when no whitespace is available', () => {
|
||||
const text = 'a'.repeat(100);
|
||||
const chunks = splitForLimit(text, 30);
|
||||
expect(chunks.length).toBe(Math.ceil(100 / 30));
|
||||
for (const c of chunks) expect(c.length).toBeLessThanOrEqual(30);
|
||||
expect(chunks.join('')).toBe(text);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createChatSdkBridge', () => {
|
||||
// The bridge is now transport-only: forward inbound events, relay outbound
|
||||
// ops. All per-wiring engage / accumulate / drop / subscribe decisions live
|
||||
|
||||
@@ -63,6 +63,38 @@ export interface ChatSdkBridgeConfig {
|
||||
* quirk (e.g. Telegram's legacy Markdown parse mode).
|
||||
*/
|
||||
transformOutboundText?: (text: string) => string;
|
||||
/**
|
||||
* Maximum text length the underlying adapter accepts in a single message.
|
||||
* When set, the bridge splits outbound text longer than this on paragraph
|
||||
* → line → hard-char boundaries and posts multiple messages. Without this,
|
||||
* adapters like Discord (2000) and Telegram (4096) silently truncate
|
||||
* mid-response. The returned id is the first chunk's id so subsequent edits
|
||||
* and reactions still target the head of the reply.
|
||||
*/
|
||||
maxTextLength?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split `text` into chunks no larger than `limit`, preferring paragraph
|
||||
* breaks, then line breaks, then a hard character cut as a last resort.
|
||||
* Preserves code fences only structurally — a fenced block that straddles a
|
||||
* chunk boundary will render as two independent blocks on the receiving
|
||||
* platform, which is the same behavior as manually re-opening a fence.
|
||||
*/
|
||||
export function splitForLimit(text: string, limit: number): string[] {
|
||||
if (text.length <= limit) return [text];
|
||||
const chunks: string[] = [];
|
||||
let remaining = text;
|
||||
while (remaining.length > limit) {
|
||||
let cut = remaining.lastIndexOf('\n\n', limit);
|
||||
if (cut <= 0) cut = remaining.lastIndexOf('\n', limit);
|
||||
if (cut <= 0) cut = remaining.lastIndexOf(' ', limit);
|
||||
if (cut <= 0) cut = limit;
|
||||
chunks.push(remaining.slice(0, cut).trimEnd());
|
||||
remaining = remaining.slice(cut).trimStart();
|
||||
}
|
||||
if (remaining.length > 0) chunks.push(remaining);
|
||||
return chunks;
|
||||
}
|
||||
|
||||
export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter {
|
||||
@@ -338,13 +370,23 @@ export function createChatSdkBridge(config: ChatSdkBridgeConfig): ChannelAdapter
|
||||
data: f.data,
|
||||
filename: f.filename,
|
||||
}));
|
||||
if (fileUploads && fileUploads.length > 0) {
|
||||
const result = await adapter.postMessage(tid, { markdown: text, files: fileUploads });
|
||||
return result?.id;
|
||||
} else {
|
||||
const result = await adapter.postMessage(tid, { markdown: text });
|
||||
return result?.id;
|
||||
// Split if over the adapter's max length. Files ride on the first
|
||||
// chunk so the head of the reply still carries them.
|
||||
const chunks =
|
||||
config.maxTextLength && text.length > config.maxTextLength
|
||||
? splitForLimit(text, config.maxTextLength)
|
||||
: [text];
|
||||
let firstId: string | undefined;
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const chunk = chunks[i];
|
||||
const attachFiles = i === 0 && fileUploads && fileUploads.length > 0;
|
||||
const result = await adapter.postMessage(
|
||||
tid,
|
||||
attachFiles ? { markdown: chunk, files: fileUploads } : { markdown: chunk },
|
||||
);
|
||||
if (i === 0) firstId = result?.id;
|
||||
}
|
||||
return firstId;
|
||||
} else if (message.files && message.files.length > 0) {
|
||||
// Files only, no text
|
||||
const fileUploads = message.files.map((f: { data: Buffer; filename: string }) => ({
|
||||
|
||||
Reference in New Issue
Block a user