fix: telegram draft chunking defaults (#667) (thanks @rubyrunsstuff)
This commit is contained in:
@@ -26,6 +26,8 @@
|
||||
- Agents: avoid duplicate replies when the message tool sends. (#659) — thanks @mickahouan.
|
||||
- Agents: harden Cloud Code Assist tool ID sanitization (toolUse/toolCall/toolResult) and scrub extra JSON Schema constraints. (#665) — thanks @sebslight.
|
||||
- Agents/Tools: resolve workspace-relative Read/Write/Edit paths; align bash default cwd. (#642) — thanks @mukhtharcm.
|
||||
- Discord: include forwarded message snapshots in agent session context. (#667) — thanks @rubyrunsstuff.
|
||||
- Telegram: add `telegram.draftChunk` to tune draft streaming chunking for `streamMode: "block"`. (#667) — thanks @rubyrunsstuff.
|
||||
- Tests/Agents: add regression coverage for workspace tool path resolution and bash cwd defaults.
|
||||
- iOS/Android: enable stricter concurrency/lint checks; fix Swift 6 strict concurrency issues + Android lint errors (ExifInterface, obsolete SDK check). (#662) — thanks @KristijanJovanovski.
|
||||
- iOS/macOS: share `AsyncTimeout`, require explicit `bridgeStableID` on connect, and harden tool display defaults (avoids missing-resource label fallbacks).
|
||||
|
||||
@@ -100,6 +100,7 @@ Telegram is the only provider with draft streaming:
|
||||
- `partial`: draft updates with the latest stream text.
|
||||
- `block`: draft updates in chunked blocks (same chunker rules).
|
||||
- `off`: no draft streaming.
|
||||
- Draft chunk config (only for `streamMode: "block"`): `telegram.draftChunk` (defaults: `minChars: 200`, `maxChars: 800`).
|
||||
- Draft streaming is separate from block streaming; block replies are off by default and only enabled by `*.blockStreaming: true` on non-Telegram providers.
|
||||
- Final reply is still a normal message.
|
||||
- `/reasoning stream` writes reasoning into the draft bubble (Telegram only).
|
||||
|
||||
@@ -676,6 +676,11 @@ Multi-account support lives under `telegram.accounts` (see the multi-account sec
|
||||
},
|
||||
replyToMode: "first", // off | first | all
|
||||
streamMode: "partial", // off | partial | block (draft streaming; separate from block streaming)
|
||||
draftChunk: { // optional; only for streamMode=block
|
||||
minChars: 200,
|
||||
maxChars: 800,
|
||||
breakPreference: "paragraph" // paragraph | newline | sentence
|
||||
},
|
||||
actions: { reactions: true, sendMessage: true }, // tool action gates (false disables)
|
||||
mediaMaxMb: 5,
|
||||
retry: { // outbound retry policy
|
||||
|
||||
@@ -209,6 +209,9 @@ Config:
|
||||
- `partial`: update the draft bubble with the latest streaming text.
|
||||
- `block`: update the draft bubble in larger blocks (chunked).
|
||||
- `off`: disable draft streaming.
|
||||
- Optional (only for `streamMode: "block"`):
|
||||
- `telegram.draftChunk: { minChars?, maxChars?, breakPreference? }`
|
||||
- defaults: `minChars: 200`, `maxChars: 800`, `breakPreference: "paragraph"` (clamped to `telegram.textChunkLimit`).
|
||||
|
||||
Note: draft streaming is separate from **block streaming** (provider messages).
|
||||
Block streaming is off by default and requires `telegram.blockStreaming: true`
|
||||
|
||||
54
src/auto-reply/reply/block-streaming.test.ts
Normal file
54
src/auto-reply/reply/block-streaming.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import { resolveTelegramDraftStreamingChunking } from "./block-streaming.js";
|
||||
|
||||
describe("resolveTelegramDraftStreamingChunking", () => {
|
||||
it("uses smaller defaults than block streaming", () => {
|
||||
const chunking = resolveTelegramDraftStreamingChunking(
|
||||
undefined,
|
||||
"default",
|
||||
);
|
||||
expect(chunking).toEqual({
|
||||
minChars: 200,
|
||||
maxChars: 800,
|
||||
breakPreference: "paragraph",
|
||||
});
|
||||
});
|
||||
|
||||
it("clamps to telegram.textChunkLimit", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
telegram: { allowFrom: ["*"], textChunkLimit: 150 },
|
||||
};
|
||||
const chunking = resolveTelegramDraftStreamingChunking(cfg, "default");
|
||||
expect(chunking).toEqual({
|
||||
minChars: 150,
|
||||
maxChars: 150,
|
||||
breakPreference: "paragraph",
|
||||
});
|
||||
});
|
||||
|
||||
it("supports per-account overrides", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
telegram: {
|
||||
allowFrom: ["*"],
|
||||
accounts: {
|
||||
default: {
|
||||
allowFrom: ["*"],
|
||||
draftChunk: {
|
||||
minChars: 10,
|
||||
maxChars: 20,
|
||||
breakPreference: "sentence",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const chunking = resolveTelegramDraftStreamingChunking(cfg, "default");
|
||||
expect(chunking).toEqual({
|
||||
minChars: 10,
|
||||
maxChars: 20,
|
||||
breakPreference: "sentence",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,8 @@ import { resolveTextChunkLimit, type TextChunkProvider } from "../chunk.js";
|
||||
const DEFAULT_BLOCK_STREAM_MIN = 800;
|
||||
const DEFAULT_BLOCK_STREAM_MAX = 1200;
|
||||
const DEFAULT_BLOCK_STREAM_COALESCE_IDLE_MS = 1000;
|
||||
const DEFAULT_TELEGRAM_DRAFT_STREAM_MIN = 200;
|
||||
const DEFAULT_TELEGRAM_DRAFT_STREAM_MAX = 800;
|
||||
const PROVIDER_COALESCE_DEFAULTS: Partial<
|
||||
Record<TextChunkProvider, { minChars: number; idleMs: number }>
|
||||
> = {
|
||||
@@ -72,6 +74,39 @@ export function resolveBlockStreamingChunking(
|
||||
return { minChars, maxChars, breakPreference };
|
||||
}
|
||||
|
||||
export function resolveTelegramDraftStreamingChunking(
|
||||
cfg: ClawdbotConfig | undefined,
|
||||
accountId?: string | null,
|
||||
): {
|
||||
minChars: number;
|
||||
maxChars: number;
|
||||
breakPreference: "paragraph" | "newline" | "sentence";
|
||||
} {
|
||||
const providerKey: TextChunkProvider = "telegram";
|
||||
const textLimit = resolveTextChunkLimit(cfg, providerKey, accountId);
|
||||
const normalizedAccountId = normalizeAccountId(accountId);
|
||||
const draftCfg =
|
||||
cfg?.telegram?.accounts?.[normalizedAccountId]?.draftChunk ??
|
||||
cfg?.telegram?.draftChunk;
|
||||
|
||||
const maxRequested = Math.max(
|
||||
1,
|
||||
Math.floor(draftCfg?.maxChars ?? DEFAULT_TELEGRAM_DRAFT_STREAM_MAX),
|
||||
);
|
||||
const maxChars = Math.max(1, Math.min(maxRequested, textLimit));
|
||||
const minRequested = Math.max(
|
||||
1,
|
||||
Math.floor(draftCfg?.minChars ?? DEFAULT_TELEGRAM_DRAFT_STREAM_MIN),
|
||||
);
|
||||
const minChars = Math.min(minRequested, maxChars);
|
||||
const breakPreference =
|
||||
draftCfg?.breakPreference === "newline" ||
|
||||
draftCfg?.breakPreference === "sentence"
|
||||
? draftCfg.breakPreference
|
||||
: "paragraph";
|
||||
return { minChars, maxChars, breakPreference };
|
||||
}
|
||||
|
||||
export function resolveBlockStreamingCoalescing(
|
||||
cfg: ClawdbotConfig | undefined,
|
||||
provider?: string,
|
||||
|
||||
@@ -120,6 +120,10 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"telegram.botToken": "Telegram Bot Token",
|
||||
"telegram.dmPolicy": "Telegram DM Policy",
|
||||
"telegram.streamMode": "Telegram Draft Stream Mode",
|
||||
"telegram.draftChunk.minChars": "Telegram Draft Chunk Min Chars",
|
||||
"telegram.draftChunk.maxChars": "Telegram Draft Chunk Max Chars",
|
||||
"telegram.draftChunk.breakPreference":
|
||||
"Telegram Draft Chunk Break Preference",
|
||||
"telegram.retry.attempts": "Telegram Retry Attempts",
|
||||
"telegram.retry.minDelayMs": "Telegram Retry Min Delay (ms)",
|
||||
"telegram.retry.maxDelayMs": "Telegram Retry Max Delay (ms)",
|
||||
@@ -203,6 +207,12 @@ const FIELD_HELP: Record<string, string> = {
|
||||
'Direct message access control ("pairing" recommended). "open" requires telegram.allowFrom=["*"].',
|
||||
"telegram.streamMode":
|
||||
"Draft streaming mode for Telegram replies (off | partial | block). Separate from block streaming; requires private topics + sendMessageDraft.",
|
||||
"telegram.draftChunk.minChars":
|
||||
'Minimum chars before emitting a Telegram draft update when telegram.streamMode="block" (default: 200).',
|
||||
"telegram.draftChunk.maxChars":
|
||||
'Target max size for a Telegram draft update chunk when telegram.streamMode="block" (default: 800; clamped to telegram.textChunkLimit).',
|
||||
"telegram.draftChunk.breakPreference":
|
||||
"Preferred breakpoints for Telegram draft chunks (paragraph | newline | sentence). Default: paragraph.",
|
||||
"telegram.retry.attempts":
|
||||
"Max retry attempts for outbound Telegram API calls (default: 3).",
|
||||
"telegram.retry.minDelayMs":
|
||||
|
||||
@@ -22,6 +22,12 @@ export type BlockStreamingCoalesceConfig = {
|
||||
idleMs?: number;
|
||||
};
|
||||
|
||||
export type BlockStreamingChunkConfig = {
|
||||
minChars?: number;
|
||||
maxChars?: number;
|
||||
breakPreference?: "paragraph" | "newline" | "sentence";
|
||||
};
|
||||
|
||||
export type HumanDelayConfig = {
|
||||
/** Delay style for block replies (off|natural|custom). */
|
||||
mode?: "off" | "natural" | "custom";
|
||||
@@ -345,6 +351,8 @@ export type TelegramAccountConfig = {
|
||||
textChunkLimit?: number;
|
||||
/** Disable block streaming for this account. */
|
||||
blockStreaming?: boolean;
|
||||
/** Chunking config for draft streaming in `streamMode: "block"`. */
|
||||
draftChunk?: BlockStreamingChunkConfig;
|
||||
/** Merge streamed block replies before sending. */
|
||||
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
||||
/** Draft streaming mode for Telegram (off|partial|block). Default: partial. */
|
||||
@@ -1318,11 +1326,7 @@ export type AgentDefaultsConfig = {
|
||||
*/
|
||||
blockStreamingBreak?: "text_end" | "message_end";
|
||||
/** Soft block chunking for streamed replies (min/max chars, prefer paragraph/newline). */
|
||||
blockStreamingChunk?: {
|
||||
minChars?: number;
|
||||
maxChars?: number;
|
||||
breakPreference?: "paragraph" | "newline" | "sentence";
|
||||
};
|
||||
blockStreamingChunk?: BlockStreamingChunkConfig;
|
||||
/**
|
||||
* Block reply coalescing (merge streamed chunks before send).
|
||||
* idleMs: wait time before flushing when idle.
|
||||
|
||||
@@ -103,6 +103,18 @@ const BlockStreamingCoalesceSchema = z.object({
|
||||
idleMs: z.number().int().nonnegative().optional(),
|
||||
});
|
||||
|
||||
const BlockStreamingChunkSchema = z.object({
|
||||
minChars: z.number().int().positive().optional(),
|
||||
maxChars: z.number().int().positive().optional(),
|
||||
breakPreference: z
|
||||
.union([
|
||||
z.literal("paragraph"),
|
||||
z.literal("newline"),
|
||||
z.literal("sentence"),
|
||||
])
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const HumanDelaySchema = z.object({
|
||||
mode: z
|
||||
.union([z.literal("off"), z.literal("natural"), z.literal("custom")])
|
||||
@@ -207,6 +219,7 @@ const TelegramAccountSchemaBase = z.object({
|
||||
groupPolicy: GroupPolicySchema.optional().default("open"),
|
||||
textChunkLimit: z.number().int().positive().optional(),
|
||||
blockStreaming: z.boolean().optional(),
|
||||
draftChunk: BlockStreamingChunkSchema.optional(),
|
||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||
streamMode: z.enum(["off", "partial", "block"]).optional().default("partial"),
|
||||
mediaMaxMb: z.number().positive().optional(),
|
||||
@@ -1038,19 +1051,7 @@ const AgentDefaultsSchema = z
|
||||
blockStreamingBreak: z
|
||||
.union([z.literal("text_end"), z.literal("message_end")])
|
||||
.optional(),
|
||||
blockStreamingChunk: z
|
||||
.object({
|
||||
minChars: z.number().int().positive().optional(),
|
||||
maxChars: z.number().int().positive().optional(),
|
||||
breakPreference: z
|
||||
.union([
|
||||
z.literal("paragraph"),
|
||||
z.literal("newline"),
|
||||
z.literal("sentence"),
|
||||
])
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
blockStreamingChunk: BlockStreamingChunkSchema.optional(),
|
||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||
humanDelay: HumanDelaySchema.optional(),
|
||||
timeoutSeconds: z.number().int().positive().optional(),
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
listNativeCommandSpecs,
|
||||
} from "../auto-reply/commands-registry.js";
|
||||
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
||||
import { resolveBlockStreamingChunking } from "../auto-reply/reply/block-streaming.js";
|
||||
import { resolveTelegramDraftStreamingChunking } from "../auto-reply/reply/block-streaming.js";
|
||||
import {
|
||||
buildMentionRegexes,
|
||||
matchesMentionPatterns,
|
||||
@@ -749,7 +749,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
||||
: undefined;
|
||||
const draftChunking =
|
||||
draftStream && streamMode === "block"
|
||||
? resolveBlockStreamingChunking(cfg, "telegram", route.accountId)
|
||||
? resolveTelegramDraftStreamingChunking(cfg, route.accountId)
|
||||
: undefined;
|
||||
const draftChunker = draftChunking
|
||||
? new EmbeddedBlockChunker(draftChunking)
|
||||
|
||||
Reference in New Issue
Block a user