mirror of
https://github.com/bdnugget/rs-sdk.git
synced 2026-11-07 09:07:30 +00:00
The chat CLI opened a fresh observer WebSocket per invocation. Over a TLS tunnel (split-topology benchmark runs) that handshake is flaky, and a send against a dead game-client relay burned the SDK's 60s actionTimeout per 80-char chunk - multi-chunk sends stalled 120s+ with zero output. - gateway: GET /chat/:bot serves recent chat from a per-bot accumulated ChatHistory (fed from state snapshots); POST /say/:bot relays through the bot's game client with a hard per-chunk deadline (default 10s) and aborts remaining chunks on first failure. Same authenticateSDK gate as sdk_connect. - sdk/chat.ts: HTTP-first with per-attempt timeouts + one retry on network failure, a 90s force-exit backstop on every non-watch path, and clear non-zero exits. Falls back to the observer WebSocket (now with bounded connectTimeout/actionTimeout) for gateways without the endpoints; --watch keeps the long-lived observer connection. - chunkMessage moved to sdk/chunking.ts so the gateway can share it without importing the full SDK; re-exported from sdk/index. - tests: sdk/test/gateway-http-chat.test.ts runs the real gateway subprocess with a scripted bot client, covering read/send/chunking/409/timeout paths and the CLI end-to-end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
59 lines
2.2 KiB
TypeScript
59 lines
2.2 KiB
TypeScript
/**
|
|
* Chat message chunking, shared by the SDK's say() and the gateway's HTTP say
|
|
* endpoint. Lives in its own module so the gateway can import it without
|
|
* pulling in the full SDK (pathfinding, collision data, puppeteer glue).
|
|
*/
|
|
|
|
/**
|
|
* Nibble-cost mirror of the client's WordPack encoding: the 13 most common
|
|
* chars pack to 1 nibble, everything else (digits, punctuation, rarer letters)
|
|
* to 2. Chat frames carry a 1-byte length, so packed text is limited to ~240
|
|
* bytes (480 nibbles) regardless of the char cap - digit-heavy text hits it
|
|
* well before 400 chars.
|
|
*/
|
|
const WORDPACK_COMMON_CHARS = ' etaoihnsrdlu';
|
|
const MAX_PACKED_NIBBLES = 240 * 2;
|
|
|
|
function packedNibbles(text: string): number {
|
|
let nibbles = 0;
|
|
for (const ch of text.toLowerCase()) {
|
|
nibbles += WORDPACK_COMMON_CHARS.includes(ch) ? 1 : 2;
|
|
}
|
|
return nibbles;
|
|
}
|
|
|
|
/**
|
|
* Split text into chunks on word boundaries such that each chunk fits both the
|
|
* char cap and the wire's packed-byte budget (so the client never silently
|
|
* truncates a chunk). Words too big for one chunk are hard-split so nothing is
|
|
* dropped. Exported for tests.
|
|
*/
|
|
export function chunkMessage(text: string, maxLen: number): string[] {
|
|
const fits = (s: string) => s.length <= maxLen && packedNibbles(s) <= MAX_PACKED_NIBBLES;
|
|
const chunks: string[] = [];
|
|
let current = '';
|
|
for (const word of text.trim().split(/\s+/)) {
|
|
// A single oversized word: flush, then hard-split it greedily.
|
|
if (!fits(word)) {
|
|
if (current) { chunks.push(current); current = ''; }
|
|
let rest = word;
|
|
while (rest.length > 0) {
|
|
let take = Math.min(rest.length, maxLen);
|
|
while (take > 1 && !fits(rest.slice(0, take))) take--;
|
|
chunks.push(rest.slice(0, take));
|
|
rest = rest.slice(take);
|
|
}
|
|
continue;
|
|
}
|
|
const candidate = current ? `${current} ${word}` : word;
|
|
if (fits(candidate)) {
|
|
current = candidate;
|
|
} else {
|
|
if (current) chunks.push(current);
|
|
current = word;
|
|
}
|
|
}
|
|
if (current) chunks.push(current);
|
|
return chunks;
|
|
}
|