From a6ab85988ea42a3c25e8f22db381188c243cddc2 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Sun, 2 Aug 2026 22:47:06 -0700 Subject: [PATCH] feat(channels): support routed conversation compaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose /compact through channel command routing so operators can recover oversized or poisoned conversations without leaving the chat, while preserving route scoping and command authorization. 👾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta Code --- scripts/source-file-size-baseline.json | 2 +- src/channels/README.md | 9 +- src/channels/access-control.test.ts | 5 +- src/channels/commands.test.ts | 18 +-- src/channels/commands.ts | 72 +++++------ src/channels/registry-command-routing.test.ts | 4 +- src/channels/registry-commands.ts | 33 +++++ src/channels/registry-compact-routing.test.ts | 115 ++++++++++++++++++ src/channels/registry-handlers.ts | 5 + src/channels/registry-inbound.ts | 2 + src/channels/registry.ts | 7 ++ .../listener/channel-runtime-commands.ts | 54 ++++++++ src/websocket/listener/commands.ts | 5 +- src/websocket/listener/lifecycle.ts | 24 +--- 14 files changed, 283 insertions(+), 72 deletions(-) create mode 100644 src/channels/registry-compact-routing.test.ts create mode 100644 src/websocket/listener/channel-runtime-commands.ts diff --git a/scripts/source-file-size-baseline.json b/scripts/source-file-size-baseline.json index 70caf1161f..7ef2ffd731 100644 --- a/scripts/source-file-size-baseline.json +++ b/scripts/source-file-size-baseline.json @@ -47,7 +47,7 @@ "src/websocket/listener/commands/channels.ts": 1315, "src/websocket/listener/commands/memory.ts": 1114, "src/websocket/listener/file-commands.ts": 1030, - "src/websocket/listener/lifecycle.ts": 1617, + "src/websocket/listener/lifecycle.ts": 1597, "src/websocket/listener/protocol-inbound.ts": 2332, "src/websocket/listener/protocol-outbound.ts": 1100 } diff --git a/src/channels/README.md b/src/channels/README.md index 784f7dbf5d..44d14307ce 100644 --- a/src/channels/README.md +++ b/src/channels/README.md @@ -176,14 +176,21 @@ Typed slash commands are handled before normal channel ingress so operational commands do not get delivered to the agent as regular user messages. The shared channel command set is: +- `/help` — show channel usage and available commands. - `/status` — show account, listener, route, agent, and conversation state. +- `/whoami` — show the sender's access scope, tier, and runnable commands. - `/pause` — disable agent replies for the current routed chat. - `/resume` — re-enable agent replies for the current routed chat. - `/cancel` — abort the in-progress agent turn for the current routed chat. +- `/compact [mode]` — summarize the current route's active conversation + history; accepts the same modes as the CLI command. - `/chat` — show the Letta web chat link for the current route. -- `/whoami` — show the sender's access scope, tier, and runnable commands. +- `/feedback ` — send product feedback with route context. +- `/model [list|handle]` — show, list, or switch the routed conversation's + model. - `/reflection` — start a memory reflection pass for the current route's agent conversation when MemFS is enabled. +- `/reload` — reload settings, local mods, and agent secrets. Slack-native slash command payloads currently exist only for `/cancel`; the rest are expected to be sent as normal channel messages in the relevant chat/thread. diff --git a/src/channels/access-control.test.ts b/src/channels/access-control.test.ts index aa17817c8e..e0076cf76e 100644 --- a/src/channels/access-control.test.ts +++ b/src/channels/access-control.test.ts @@ -228,13 +228,14 @@ describe("channel command tiers", () => { expect(canRunChannelCommand(userGate, "whoami")).toBe(true); expect(canRunChannelCommand(userGate, "pause")).toBe(false); expect(canRunChannelCommand(userGate, "model")).toBe(false); + expect(canRunChannelCommand(userGate, "compact")).toBe(false); expect(canRunChannelCommand(userGate, "reload")).toBe(false); }); test("userAllowedCommands extends the floor and normalizes names", () => { const account = makeAccount({ adminUsers: ["admin-1"], - userAllowedCommands: ["/Model", "cancel", "reload"], + userAllowedCommands: ["/Model", "cancel", "compact", "reload"], }); const gate = resolveChannelCommandGate({ account, @@ -243,6 +244,7 @@ describe("channel command tiers", () => { }); expect(canRunChannelCommand(gate, "model")).toBe(true); expect(canRunChannelCommand(gate, "cancel")).toBe(true); + expect(canRunChannelCommand(gate, "compact")).toBe(true); expect(canRunChannelCommand(gate, "reload")).toBe(true); expect(canRunChannelCommand(gate, "pause")).toBe(false); }); @@ -308,6 +310,7 @@ describe("channel command tiers", () => { expect(canRunChannelCommand(gate, "whoami")).toBe(true); expect(canRunChannelCommand(gate, "cancel")).toBe(false); expect(canRunChannelCommand(gate, "model")).toBe(false); + expect(canRunChannelCommand(gate, "compact")).toBe(false); expect(canRunChannelCommand(gate, "pause")).toBe(false); expect(canRunChannelCommand(gate, "reload")).toBe(false); diff --git a/src/channels/commands.test.ts b/src/channels/commands.test.ts index 8ff912fcda..d0449c6d9c 100644 --- a/src/channels/commands.test.ts +++ b/src/channels/commands.test.ts @@ -250,6 +250,7 @@ describe("channel slash commands", () => { "pause", "resume", "cancel", + "compact", "chat", "feedback", "model", @@ -265,7 +266,7 @@ describe("channel slash commands", () => { expect(text).toContain("Telegram is connected to Letta Code."); expect(text).not.toContain("MessageChannel"); expect(text).toContain( - "Supported slash commands here: /help, /status, /whoami, /pause, /resume, /cancel, /chat, /feedback, /model, /reflection, /reload.", + "Supported slash commands here: /help, /status, /whoami, /pause, /resume, /cancel, /compact, /chat, /feedback, /model, /reflection, /reload.", ); const slackText = buildChannelHelpMessage("slack"); @@ -284,12 +285,15 @@ describe("channel slash commands", () => { "@agent /model - switch this thread's model", ); expect(slackText).toContain("@agent /feedback "); + expect(slackText).toContain( + "@agent /compact - summarize this thread's active conversation history", + ); expect(slackText).toContain("@agent /detach"); expect(slackText).toContain( "@agent /reload - reload settings, local mods, and agent secrets", ); expect(slackText).toContain( - "Legacy bang aliases still work after a mention: !help, !detach, !model, !new, !reload.", + "Legacy bang aliases still work after a mention: !help, !compact, !detach, !model, !new, !reload.", ); }); @@ -834,17 +838,17 @@ describe("channel slash commands", () => { }); test("builds a useful unsupported-command response", () => { - const command = parseChannelSlashCommand("/compact now"); + const command = parseChannelSlashCommand("/doctor now"); expect(command).not.toBeNull(); if (!command) { - throw new Error("Expected /compact to parse as a channel slash command"); + throw new Error("Expected /doctor to parse as a channel slash command"); } const text = buildUnsupportedChannelCommandMessage("telegram", command); - expect(text).toContain("Telegram received /compact now"); + expect(text).toContain("Telegram received /doctor now"); expect(text).toContain("not supported in channels yet"); expect(text).toContain( - "Supported slash commands: /help, /status, /whoami, /pause, /resume, /cancel, /chat, /feedback, /model, /reflection, /reload.", + "Supported slash commands: /help, /status, /whoami, /pause, /resume, /cancel, /compact, /chat, /feedback, /model, /reflection, /reload.", ); expect(text).toContain("without a leading slash"); @@ -867,7 +871,7 @@ describe("channel slash commands", () => { ); expect(bangText).toContain("Slack received !pause"); expect(bangText).toContain( - "Supported bang commands: !help, !detach, !model, !new, !reload.", + "Supported bang commands: !help, !compact, !detach, !model, !new, !reload.", ); }); }); diff --git a/src/channels/commands.ts b/src/channels/commands.ts index 416243f945..91b642ce4c 100644 --- a/src/channels/commands.ts +++ b/src/channels/commands.ts @@ -37,48 +37,27 @@ export type ChannelSlashCommandHandlerResult = { modelPicker?: ChannelModelPickerData; }; +type ChannelSlashCommandHandler = ( + command: ParsedChannelSlashCommand, + msg: InboundChannelMessage, +) => Promise; + type ChannelDirectReplyPayload = { text: string; modelPicker?: ChannelModelPickerData; }; export type ChannelSlashCommandHandlers = { - cancel?: ( - command: ParsedChannelSlashCommand, - msg: InboundChannelMessage, - ) => Promise; - chat?: ( - command: ParsedChannelSlashCommand, - msg: InboundChannelMessage, - ) => Promise; - detach?: ( - command: ParsedChannelSlashCommand, - msg: InboundChannelMessage, - ) => Promise; - model?: ( - command: ParsedChannelSlashCommand, - msg: InboundChannelMessage, - ) => Promise; - newConversation?: ( - command: ParsedChannelSlashCommand, - msg: InboundChannelMessage, - ) => Promise; - pause?: ( - command: ParsedChannelSlashCommand, - msg: InboundChannelMessage, - ) => Promise; - reflection?: ( - command: ParsedChannelSlashCommand, - msg: InboundChannelMessage, - ) => Promise; - reload?: ( - command: ParsedChannelSlashCommand, - msg: InboundChannelMessage, - ) => Promise; - resume?: ( - command: ParsedChannelSlashCommand, - msg: InboundChannelMessage, - ) => Promise; + cancel?: ChannelSlashCommandHandler; + compact?: ChannelSlashCommandHandler; + chat?: ChannelSlashCommandHandler; + detach?: ChannelSlashCommandHandler; + model?: ChannelSlashCommandHandler; + newConversation?: ChannelSlashCommandHandler; + pause?: ChannelSlashCommandHandler; + reflection?: ChannelSlashCommandHandler; + reload?: ChannelSlashCommandHandler; + resume?: ChannelSlashCommandHandler; }; export type ChannelStatusContext = { @@ -127,6 +106,11 @@ const CHANNEL_SLASH_COMMANDS: ChannelSlashCommandDefinition[] = [ kind: "agent-scoped", summary: "Cancel the in-progress agent turn for this chat.", }, + { + name: "compact", + kind: "agent-scoped", + summary: "Summarize this conversation's active history.", + }, { name: "chat", kind: "direct", @@ -158,6 +142,7 @@ const CHANNEL_SLASH_COMMANDS: ChannelSlashCommandDefinition[] = [ const SLACK_MENTION_COMMAND_NAMES = [ "help", + "compact", "detach", "model", "new", @@ -282,6 +267,7 @@ const SLACK_MENTION_SLASH_COMMAND_EXAMPLES = [ "@agent /model list", "@agent /model ", "@agent /cancel", + "@agent /compact", "@agent /chat", "@agent /feedback ", "@agent /reflection", @@ -337,6 +323,7 @@ export function buildChannelHelpMessage(channelId: string): string { "@agent /model - switch this thread's model", "@agent /status - show route and listener status", "@agent /cancel - cancel the current turn", + "@agent /compact - summarize this thread's active conversation history", "@agent /chat - show the web chat link", "@agent /feedback - send feedback to the Letta team from this routed thread", "@agent /reflection - start a memory reflection pass", @@ -801,6 +788,13 @@ export function buildChannelReflectionUnavailableMessage( return `${displayName} cannot start reflection for this chat because the listener is not ready yet. Try again in a moment.`; } +export function buildChannelCompactUnavailableMessage( + channelId: string, +): string { + const displayName = channelDisplayName(channelId); + return `${displayName} cannot compact this chat's routed conversation because the listener is not ready yet. Try again in a moment.`; +} + export function buildChannelReloadUnavailableMessage( channelId: string, ): string { @@ -920,6 +914,12 @@ export async function tryHandleChannelSlashCommand( handler: options.handlers?.cancel, defaultText: buildChannelCancelAcceptedMessage(msg.channel), }); + case "compact": + return handleScopedCommand({ + msg, + command, + handler: options.handlers?.compact, + }); case "chat": return handleScopedCommand({ msg, diff --git a/src/channels/registry-command-routing.test.ts b/src/channels/registry-command-routing.test.ts index f722604b2c..6bb1d1734f 100644 --- a/src/channels/registry-command-routing.test.ts +++ b/src/channels/registry-command-routing.test.ts @@ -216,7 +216,7 @@ describe("ChannelRegistry command routing", () => { chatId: "123", senderId: "456", senderName: "Alice", - text: "/compact now", + text: "/doctor now", timestamp: Date.now(), messageId: "77", chatType: "direct", @@ -229,7 +229,7 @@ describe("ChannelRegistry command routing", () => { replyToMessageId: "77", }); expect(replies[0]?.text).toContain( - "Telegram received /compact now, but that slash command is not supported in channels yet.", + "Telegram received /doctor now, but that slash command is not supported in channels yet.", ); }); diff --git a/src/channels/registry-commands.ts b/src/channels/registry-commands.ts index 8aa142293b..c21adb641d 100644 --- a/src/channels/registry-commands.ts +++ b/src/channels/registry-commands.ts @@ -8,6 +8,7 @@ import { buildChannelCancelUnavailableMessage, buildChannelChatLinkMessage, buildChannelChatUnavailableMessage, + buildChannelCompactUnavailableMessage, buildChannelDetachedMessage, buildChannelDetachUnsupportedMessage, buildChannelModelUnavailableMessage, @@ -22,6 +23,7 @@ import { import type { ChannelRegistryEvent } from "./registry-events"; import type { ChannelCancelHandler, + ChannelCompactHandler, ChannelModelHandler, ChannelReflectionHandler, ChannelReloadHandler, @@ -53,6 +55,7 @@ export function createChannelCommandRouter(deps: { threadId?: string | null, ) => ChannelRoute | null; getCancelHandler: () => ChannelCancelHandler | null; + getCompactHandler: () => ChannelCompactHandler | null; getReflectionHandler: () => ChannelReflectionHandler | null; getReloadHandler: () => ChannelReloadHandler | null; getModelHandler: () => ChannelModelHandler | null; @@ -143,6 +146,35 @@ export function createChannelCommandRouter(deps: { return { handled: true }; } + async function handleCompactSlashCommand( + command: { args: string }, + msg: InboundChannelMessage, + ): Promise<{ handled: boolean; text?: string }> { + const route = loadAndFindRawRouteForMessage(msg); + if (!route?.enabled) { + return { + handled: true, + text: buildChannelNoRouteMessage(msg.channel), + }; + } + + const compactHandler = deps.getCompactHandler(); + if (!compactHandler) { + return { + handled: true, + text: buildChannelCompactUnavailableMessage(msg.channel), + }; + } + + return compactHandler({ + runtime: { + agent_id: route.agentId, + conversation_id: route.conversationId, + }, + ...(command.args ? { args: command.args } : {}), + }); + } + async function handleChatSlashCommand( msg: InboundChannelMessage, ): Promise<{ handled: boolean; text?: string }> { @@ -448,6 +480,7 @@ export function createChannelCommandRouter(deps: { return { handleCancelSlashCommand, handleChatSlashCommand, + handleCompactSlashCommand, handleDetachSlashCommand, handleModelSlashCommand, handleNewConversationSlashCommand, diff --git a/src/channels/registry-compact-routing.test.ts b/src/channels/registry-compact-routing.test.ts new file mode 100644 index 0000000000..d525e64376 --- /dev/null +++ b/src/channels/registry-compact-routing.test.ts @@ -0,0 +1,115 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { + __testOverrideLoadChannelAccounts, + __testOverrideSaveChannelAccounts, + clearChannelAccountStores, +} from "@/channels/accounts"; +import { ChannelRegistry, getChannelRegistry } from "@/channels/registry"; +import { + __testOverrideLoadRoutes, + __testOverrideSaveRoutes, + addRoute, + clearAllRoutes, +} from "@/channels/routing"; + +beforeEach(() => { + __testOverrideLoadChannelAccounts(() => []); + __testOverrideSaveChannelAccounts(() => {}); + __testOverrideLoadRoutes(() => null); + __testOverrideSaveRoutes(() => {}); +}); + +afterEach(async () => { + const registry = getChannelRegistry(); + if (registry) { + await registry.stopAll(); + } + clearAllRoutes(); + clearChannelAccountStores(); + __testOverrideLoadChannelAccounts(null); + __testOverrideSaveChannelAccounts(null); + __testOverrideLoadRoutes(null); + __testOverrideSaveRoutes(null); +}); + +describe("ChannelRegistry compact command routing", () => { + test("/compact invokes the handler for the exact routed conversation", async () => { + const replies: Array<{ + chatId: string; + text: string; + replyToMessageId?: string; + }> = []; + const compactions: unknown[] = []; + const registry = new ChannelRegistry(); + const delivered: unknown[] = []; + + registry.setMessageHandler((delivery) => delivered.push(delivery)); + registry.setCompactHandler(async (params) => { + compactions.push(params); + return { + handled: true, + text: "Compaction completed. Message buffer length reduced from 72 to 2.", + }; + }); + registry.setReady(); + registry.registerAdapter({ + id: "telegram:acct-telegram", + channelId: "telegram", + accountId: "acct-telegram", + name: "Telegram", + start: async () => {}, + stop: async () => {}, + isRunning: () => true, + sendMessage: async () => ({ messageId: "msg-1" }), + sendDirectReply: async (chatId, text, options) => { + replies.push({ + chatId, + text, + replyToMessageId: options?.replyToMessageId, + }); + }, + onMessage: undefined, + }); + addRoute("telegram", { + accountId: "acct-telegram", + chatId: "123", + chatType: "direct", + threadId: null, + agentId: "agent-1", + conversationId: "conv-1", + enabled: true, + createdAt: "2026-08-03T00:00:00.000Z", + }); + + const adapter = registry.getAdapter("telegram", "acct-telegram"); + await adapter?.onMessage?.({ + channel: "telegram", + accountId: "acct-telegram", + chatId: "123", + senderId: "456", + senderName: "Alice", + text: "/compact all", + timestamp: Date.now(), + messageId: "77", + chatType: "direct", + }); + + expect(delivered).toHaveLength(0); + expect(compactions).toEqual([ + { + runtime: { + agent_id: "agent-1", + conversation_id: "conv-1", + }, + args: "all", + }, + ]); + expect(replies).toEqual([ + { + chatId: "123", + text: "Compaction completed. Message buffer length reduced from 72 to 2.", + replyToMessageId: "77", + }, + ]); + }); +}); diff --git a/src/channels/registry-handlers.ts b/src/channels/registry-handlers.ts index 5ebcd97f5a..124686eb2c 100644 --- a/src/channels/registry-handlers.ts +++ b/src/channels/registry-handlers.ts @@ -19,6 +19,11 @@ export type ChannelCancelHandler = (params: { runtime: { agent_id: string; conversation_id: string }; }) => Promise; +export type ChannelCompactHandler = (params: { + runtime: { agent_id: string; conversation_id: string }; + args?: string; +}) => Promise<{ handled: boolean; text?: string }>; + export type ChannelReflectionHandler = (params: { runtime: { agent_id: string; conversation_id: string }; }) => Promise<{ handled: boolean; text?: string }>; diff --git a/src/channels/registry-inbound.ts b/src/channels/registry-inbound.ts index c81cf9b463..e45ccf9bab 100644 --- a/src/channels/registry-inbound.ts +++ b/src/channels/registry-inbound.ts @@ -111,6 +111,8 @@ export function createChannelInboundRouter(deps: { handlers: { cancel: async (_command, commandMsg) => deps.commands.handleCancelSlashCommand(commandMsg), + compact: async (command, commandMsg) => + deps.commands.handleCompactSlashCommand(command, commandMsg), chat: async (_command, commandMsg) => deps.commands.handleChatSlashCommand(commandMsg), detach: async (_command, commandMsg) => diff --git a/src/channels/registry.ts b/src/channels/registry.ts index 56ff1fd9d4..62c6324fda 100644 --- a/src/channels/registry.ts +++ b/src/channels/registry.ts @@ -35,6 +35,7 @@ import { import type { ChannelRegistryEvent } from "./registry-events"; import type { ChannelCancelHandler, + ChannelCompactHandler, ChannelInboundDelivery, ChannelMessageHandler, ChannelModelHandler, @@ -164,6 +165,7 @@ export class ChannelRegistry { private eventHandler: ((event: ChannelRegistryEvent) => void) | null = null; private approvalResponseHandler: ChannelApprovalResponseHandler | null = null; private cancelHandler: ChannelCancelHandler | null = null; + private compactHandler: ChannelCompactHandler | null = null; private reflectionHandler: ChannelReflectionHandler | null = null; private modelHandler: ChannelModelHandler | null = null; private reloadHandler: ChannelReloadHandler | null = null; @@ -195,6 +197,7 @@ export class ChannelRegistry { getRoute: (channel, chatId, accountId, threadId) => this.getRoute(channel, chatId, accountId, threadId), getCancelHandler: () => this.cancelHandler, + getCompactHandler: () => this.compactHandler, getReflectionHandler: () => this.reflectionHandler, getReloadHandler: () => this.reloadHandler, getModelHandler: () => this.modelHandler, @@ -431,6 +434,10 @@ export class ChannelRegistry { this.cancelHandler = handler; } + setCompactHandler(handler: ChannelCompactHandler | null): void { + this.compactHandler = handler; + } + setReflectionHandler(handler: ChannelReflectionHandler | null): void { this.reflectionHandler = handler; } diff --git a/src/websocket/listener/channel-runtime-commands.ts b/src/websocket/listener/channel-runtime-commands.ts new file mode 100644 index 0000000000..53cfed972a --- /dev/null +++ b/src/websocket/listener/channel-runtime-commands.ts @@ -0,0 +1,54 @@ +import type { ChannelRegistry } from "@/channels/registry"; +import { handleCompactCommand, handleReloadCommand } from "./commands"; +import { getOrCreateScopedRuntime } from "./conversation-runtime"; +import { emitDeviceStatusUpdate } from "./protocol-outbound"; +import type { ListenerTransport } from "./transport"; +import type { ListenerRuntime } from "./types"; + +export function wireChannelRuntimeCommands(params: { + registry: ChannelRegistry; + listener: ListenerRuntime; + socket: ListenerTransport; +}): void { + const { registry, listener, socket } = params; + + registry.setCompactHandler(async ({ runtime, args }) => { + const scopedRuntime = getOrCreateScopedRuntime( + listener, + runtime.agent_id, + runtime.conversation_id, + ); + try { + return { + handled: true, + text: await handleCompactCommand(socket, scopedRuntime, args), + }; + } catch (error) { + return { + handled: true, + text: `Failed to compact this conversation: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }); + + registry.setReloadHandler(async ({ runtime }) => { + const scopedRuntime = getOrCreateScopedRuntime( + listener, + runtime.agent_id, + runtime.conversation_id, + ); + try { + const output = await handleReloadCommand(scopedRuntime); + emitDeviceStatusUpdate(socket, scopedRuntime, runtime); + return { + handled: true, + text: output, + }; + } catch (error) { + return { + handled: true, + text: `Failed to reload listener settings: ${error instanceof Error ? error.message : String(error)}`, + }; + } + }); +} diff --git a/src/websocket/listener/commands.ts b/src/websocket/listener/commands.ts index e1d6b7cfe9..155ce32ba5 100644 --- a/src/websocket/listener/commands.ts +++ b/src/websocket/listener/commands.ts @@ -57,6 +57,7 @@ import { ensureSecretsHydratedForAgent, invalidateSecretsCacheForAgent, } from "./secrets-sync"; +import type { ListenerTransport } from "./transport"; import { handleIncomingMessage } from "./turn"; import { buildMaybeLaunchReflectionSubagent } from "./turn-events"; import type { ConversationRuntime, StartListenerOptions } from "./types"; @@ -454,8 +455,8 @@ function compactHelpOutput(): string { } /** /compact — Summarize conversation history through the active Backend. */ -async function handleCompactCommand( - socket: WebSocket, +export async function handleCompactCommand( + socket: ListenerTransport, conversationRuntime: ConversationRuntime, args: string | undefined, ): Promise { diff --git a/src/websocket/listener/lifecycle.ts b/src/websocket/listener/lifecycle.ts index d92831de30..b0163e52f7 100644 --- a/src/websocket/listener/lifecycle.ts +++ b/src/websocket/listener/lifecycle.ts @@ -33,11 +33,11 @@ import { replayPendingApprovalRequestsToConnection, } from "./approval"; import { resolveListenerReconnectAuth } from "./auth"; +import { wireChannelRuntimeCommands } from "./channel-runtime-commands"; import { recoverActiveChannelTurn, uniqueChannelTurnSources, } from "./channel-turn-session"; -import { handleReloadCommand } from "./commands"; import { handleChannelRegistryEvent } from "./commands/channel-registry-events"; import { applyModelUpdateForRuntime, @@ -517,6 +517,7 @@ export async function wireChannelIngress( processQueuedTurn, }), ); + wireChannelRuntimeCommands({ registry, listener, socket }); registry.setModelHandler(async ({ channelId, runtime, modelIdentifier }) => { if (!modelIdentifier) { @@ -658,27 +659,6 @@ export async function wireChannelIngress( } }); - registry.setReloadHandler(async ({ runtime }) => { - const scopedRuntime = getOrCreateScopedRuntime( - listener, - runtime.agent_id, - runtime.conversation_id, - ); - try { - const output = await handleReloadCommand(scopedRuntime); - emitDeviceStatusUpdate(socket, scopedRuntime, runtime); - return { - handled: true, - text: output, - }; - } catch (error) { - return { - handled: true, - text: `Failed to reload listener settings: ${error instanceof Error ? error.message : String(error)}`, - }; - } - }); - registry.setReflectionHandler(async ({ runtime }) => { const agentId = runtime.agent_id; const conversationId = runtime.conversation_id;