feat: make streaming edits toggleable per-channel, disabled by default (#436)

This commit is contained in:
Cameron
2026-02-27 16:08:51 -08:00
committed by GitHub
parent 2174736de0
commit 6a2493da5c
7 changed files with 23 additions and 2 deletions

View File

@@ -65,17 +65,20 @@ channels:
enabled: true enabled: true
token: "123456:ABC-DEF..." token: "123456:ABC-DEF..."
dmPolicy: pairing dmPolicy: pairing
# streaming: true # Opt-in: progressively edit messages as tokens arrive
slack: slack:
enabled: true enabled: true
botToken: xoxb-... botToken: xoxb-...
appToken: xapp-... appToken: xapp-...
dmPolicy: pairing dmPolicy: pairing
# streaming: true
discord: discord:
enabled: true enabled: true
token: "..." token: "..."
dmPolicy: pairing dmPolicy: pairing
# streaming: true
whatsapp: whatsapp:
enabled: true enabled: true
@@ -368,6 +371,7 @@ All channels share these common options:
| `instantGroups` | string[] | Group/channel IDs that bypass debounce entirely (legacy) | | `instantGroups` | string[] | Group/channel IDs that bypass debounce entirely (legacy) |
| `groups` | object | Per-group configuration map (use `*` as default) | | `groups` | object | Per-group configuration map (use `*` as default) |
| `mentionPatterns` | string[] | Extra regex patterns for mention detection (Telegram/WhatsApp/Signal) | | `mentionPatterns` | string[] | Extra regex patterns for mention detection (Telegram/WhatsApp/Signal) |
| `streaming` | boolean | Stream responses via progressive message edits (default: false; Telegram/Discord/Slack only) |
### Group Message Debouncing ### Group Message Debouncing

View File

@@ -26,6 +26,7 @@ export interface DiscordConfig {
token: string; token: string;
dmPolicy?: DmPolicy; // 'pairing' (default), 'allowlist', or 'open' dmPolicy?: DmPolicy; // 'pairing' (default), 'allowlist', or 'open'
allowedUsers?: string[]; // Discord user IDs allowedUsers?: string[]; // Discord user IDs
streaming?: boolean; // Stream responses via progressive message edits (default: false)
attachmentsDir?: string; attachmentsDir?: string;
attachmentsMaxBytes?: number; attachmentsMaxBytes?: number;
groups?: Record<string, GroupModeConfig>; // Per-guild/channel settings groups?: Record<string, GroupModeConfig>; // Per-guild/channel settings
@@ -414,7 +415,7 @@ Ask the bot owner to approve with:
} }
supportsEditing(): boolean { supportsEditing(): boolean {
return true; return this.config.streaming ?? false;
} }
private async handleReactionEvent( private async handleReactionEvent(

View File

@@ -24,6 +24,7 @@ export interface SlackConfig {
appToken: string; // xapp-... (for Socket Mode) appToken: string; // xapp-... (for Socket Mode)
dmPolicy?: 'pairing' | 'allowlist' | 'open'; dmPolicy?: 'pairing' | 'allowlist' | 'open';
allowedUsers?: string[]; // Slack user IDs (e.g., U01234567) allowedUsers?: string[]; // Slack user IDs (e.g., U01234567)
streaming?: boolean; // Stream responses via progressive message edits (default: false)
attachmentsDir?: string; attachmentsDir?: string;
attachmentsMaxBytes?: number; attachmentsMaxBytes?: number;
groups?: Record<string, GroupModeConfig>; // Per-channel settings groups?: Record<string, GroupModeConfig>; // Per-channel settings
@@ -326,6 +327,10 @@ export class SlackAdapter implements ChannelAdapter {
return { messageId: ts }; return { messageId: ts };
} }
supportsEditing(): boolean {
return this.config.streaming ?? false;
}
async editMessage(chatId: string, messageId: string, text: string): Promise<void> { async editMessage(chatId: string, messageId: string, text: string): Promise<void> {
if (!this.app) throw new Error('Slack not started'); if (!this.app) throw new Error('Slack not started');

View File

@@ -27,6 +27,7 @@ export interface TelegramConfig {
token: string; token: string;
dmPolicy?: DmPolicy; // 'pairing' (default), 'allowlist', or 'open' dmPolicy?: DmPolicy; // 'pairing' (default), 'allowlist', or 'open'
allowedUsers?: number[]; // Telegram user IDs (config allowlist) allowedUsers?: number[]; // Telegram user IDs (config allowlist)
streaming?: boolean; // Stream responses via progressive message edits (default: false)
attachmentsDir?: string; attachmentsDir?: string;
attachmentsMaxBytes?: number; attachmentsMaxBytes?: number;
mentionPatterns?: string[]; // Regex patterns for mention detection mentionPatterns?: string[]; // Regex patterns for mention detection
@@ -600,6 +601,10 @@ export class TelegramAdapter implements ChannelAdapter {
return { messageId: String(result.message_id) }; return { messageId: String(result.message_id) };
} }
supportsEditing(): boolean {
return this.config.streaming ?? false;
}
async editMessage(chatId: string, messageId: string, text: string): Promise<void> { async editMessage(chatId: string, messageId: string, text: string): Promise<void> {
const { markdownToTelegramV2 } = await import('./telegram-format.js'); const { markdownToTelegramV2 } = await import('./telegram-format.js');
try { try {

View File

@@ -273,6 +273,7 @@ export interface TelegramConfig {
token?: string; token?: string;
dmPolicy?: 'pairing' | 'allowlist' | 'open'; dmPolicy?: 'pairing' | 'allowlist' | 'open';
allowedUsers?: string[]; allowedUsers?: string[];
streaming?: boolean; // Stream responses via progressive message edits (default: false)
groupDebounceSec?: number; // Debounce interval in seconds (default: 5, 0 = immediate) groupDebounceSec?: number; // Debounce interval in seconds (default: 5, 0 = immediate)
groupPollIntervalMin?: number; // @deprecated Use groupDebounceSec instead groupPollIntervalMin?: number; // @deprecated Use groupDebounceSec instead
instantGroups?: string[]; // Group chat IDs that bypass batching instantGroups?: string[]; // Group chat IDs that bypass batching
@@ -299,6 +300,7 @@ export interface SlackConfig {
botToken?: string; botToken?: string;
dmPolicy?: 'pairing' | 'allowlist' | 'open'; dmPolicy?: 'pairing' | 'allowlist' | 'open';
allowedUsers?: string[]; allowedUsers?: string[];
streaming?: boolean; // Stream responses via progressive message edits (default: false)
groupDebounceSec?: number; // Debounce interval in seconds (default: 5, 0 = immediate) groupDebounceSec?: number; // Debounce interval in seconds (default: 5, 0 = immediate)
groupPollIntervalMin?: number; // @deprecated Use groupDebounceSec instead groupPollIntervalMin?: number; // @deprecated Use groupDebounceSec instead
instantGroups?: string[]; // Channel IDs that bypass batching instantGroups?: string[]; // Channel IDs that bypass batching
@@ -345,6 +347,7 @@ export interface DiscordConfig {
token?: string; token?: string;
dmPolicy?: 'pairing' | 'allowlist' | 'open'; dmPolicy?: 'pairing' | 'allowlist' | 'open';
allowedUsers?: string[]; allowedUsers?: string[];
streaming?: boolean; // Stream responses via progressive message edits (default: false)
groupDebounceSec?: number; // Debounce interval in seconds (default: 5, 0 = immediate) groupDebounceSec?: number; // Debounce interval in seconds (default: 5, 0 = immediate)
groupPollIntervalMin?: number; // @deprecated Use groupDebounceSec instead groupPollIntervalMin?: number; // @deprecated Use groupDebounceSec instead
instantGroups?: string[]; // Guild/server IDs or channel IDs that bypass batching instantGroups?: string[]; // Guild/server IDs or channel IDs that bypass batching

View File

@@ -2082,7 +2082,7 @@ export class LettaBot implements AgentSession {
// Live-edit streaming for channels that support it // Live-edit streaming for channels that support it
// Hold back streaming edits while response could still be <no-reply/> or <actions> block // Hold back streaming edits while response could still be <no-reply/> or <actions> block
const canEdit = adapter.supportsEditing?.() ?? true; const canEdit = adapter.supportsEditing?.() ?? false;
const trimmed = response.trim(); const trimmed = response.trim();
const mayBeHidden = '<no-reply/>'.startsWith(trimmed) const mayBeHidden = '<no-reply/>'.startsWith(trimmed)
|| '<actions>'.startsWith(trimmed) || '<actions>'.startsWith(trimmed)

View File

@@ -365,6 +365,7 @@ function createChannelsForAgent(
allowedUsers: agentConfig.channels.telegram!.allowedUsers && agentConfig.channels.telegram!.allowedUsers.length > 0 allowedUsers: agentConfig.channels.telegram!.allowedUsers && agentConfig.channels.telegram!.allowedUsers.length > 0
? agentConfig.channels.telegram!.allowedUsers.map(u => typeof u === 'string' ? parseInt(u, 10) : u) ? agentConfig.channels.telegram!.allowedUsers.map(u => typeof u === 'string' ? parseInt(u, 10) : u)
: undefined, : undefined,
streaming: agentConfig.channels.telegram!.streaming,
attachmentsDir, attachmentsDir,
attachmentsMaxBytes, attachmentsMaxBytes,
groups: agentConfig.channels.telegram!.groups, groups: agentConfig.channels.telegram!.groups,
@@ -396,6 +397,7 @@ function createChannelsForAgent(
allowedUsers: agentConfig.channels.slack.allowedUsers && agentConfig.channels.slack.allowedUsers.length > 0 allowedUsers: agentConfig.channels.slack.allowedUsers && agentConfig.channels.slack.allowedUsers.length > 0
? agentConfig.channels.slack.allowedUsers ? agentConfig.channels.slack.allowedUsers
: undefined, : undefined,
streaming: agentConfig.channels.slack.streaming,
attachmentsDir, attachmentsDir,
attachmentsMaxBytes, attachmentsMaxBytes,
groups: agentConfig.channels.slack.groups, groups: agentConfig.channels.slack.groups,
@@ -452,6 +454,7 @@ function createChannelsForAgent(
allowedUsers: agentConfig.channels.discord.allowedUsers && agentConfig.channels.discord.allowedUsers.length > 0 allowedUsers: agentConfig.channels.discord.allowedUsers && agentConfig.channels.discord.allowedUsers.length > 0
? agentConfig.channels.discord.allowedUsers ? agentConfig.channels.discord.allowedUsers
: undefined, : undefined,
streaming: agentConfig.channels.discord.streaming,
attachmentsDir, attachmentsDir,
attachmentsMaxBytes, attachmentsMaxBytes,
groups: agentConfig.channels.discord.groups, groups: agentConfig.channels.discord.groups,