Harold Hunt d58dafae88
feat(telegram/acp): Topic Binding, Pin Binding Message, Fix Spawn Param Parsing (#36683)
* fix(acp): normalize unicode flags and Telegram topic binding

* feat(telegram/acp): restore topic-bound ACP and session bindings

* fix(acpx): clarify permission-denied guidance

* feat(telegram/acp): pin spawn bind notice in topics

* docs(telegram): document ACP topic thread binding behavior

* refactor(reply): share Telegram conversation-id resolver

* fix(telegram/acp): preserve bound session routing semantics

* fix(telegram): respect binding persistence and expiry reporting

* refactor(telegram): simplify binding lifecycle persistence

* fix(telegram): bind acp spawns in direct messages

* fix: document telegram ACP topic binding changelog (#36683) (thanks @huntharo)

---------

Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
2026-03-06 02:17:50 +01:00

96 lines
3.1 KiB
TypeScript

import { getSessionBindingService } from "../../../infra/outbound/session-binding-service.js";
import type { CommandHandlerResult } from "../commands-types.js";
import { formatRunLabel, sortSubagentRuns } from "../subagents-utils.js";
import {
type SubagentsCommandContext,
resolveChannelAccountId,
resolveCommandSurfaceChannel,
stopWithText,
} from "./shared.js";
function formatConversationBindingText(params: {
channel: string;
conversationId: string;
}): string {
if (params.channel === "discord") {
return `thread:${params.conversationId}`;
}
if (params.channel === "telegram") {
return `conversation:${params.conversationId}`;
}
return `binding:${params.conversationId}`;
}
export function handleSubagentsAgentsAction(ctx: SubagentsCommandContext): CommandHandlerResult {
const { params, requesterKey, runs } = ctx;
const channel = resolveCommandSurfaceChannel(params);
const accountId = resolveChannelAccountId(params);
const bindingService = getSessionBindingService();
const bindingsBySession = new Map<string, ReturnType<typeof bindingService.listBySession>>();
const resolveSessionBindings = (sessionKey: string) => {
const cached = bindingsBySession.get(sessionKey);
if (cached) {
return cached;
}
const resolved = bindingService
.listBySession(sessionKey)
.filter(
(entry) =>
entry.status === "active" &&
entry.conversation.channel === channel &&
entry.conversation.accountId === accountId,
);
bindingsBySession.set(sessionKey, resolved);
return resolved;
};
const visibleRuns = sortSubagentRuns(runs).filter((entry) => {
if (!entry.endedAt) {
return true;
}
return resolveSessionBindings(entry.childSessionKey).length > 0;
});
const lines = ["agents:", "-----"];
if (visibleRuns.length === 0) {
lines.push("(none)");
} else {
let index = 1;
for (const entry of visibleRuns) {
const binding = resolveSessionBindings(entry.childSessionKey)[0];
const bindingText = binding
? formatConversationBindingText({
channel,
conversationId: binding.conversation.conversationId,
})
: channel === "discord" || channel === "telegram"
? "unbound"
: "bindings available on discord/telegram";
lines.push(`${index}. ${formatRunLabel(entry)} (${bindingText})`);
index += 1;
}
}
const requesterBindings = resolveSessionBindings(requesterKey).filter(
(entry) => entry.targetKind === "session",
);
if (requesterBindings.length > 0) {
lines.push("", "acp/session bindings:", "-----");
for (const binding of requesterBindings) {
const label =
typeof binding.metadata?.label === "string" && binding.metadata.label.trim()
? binding.metadata.label.trim()
: binding.targetSessionKey;
lines.push(
`- ${label} (${formatConversationBindingText({
channel,
conversationId: binding.conversation.conversationId,
})}, session:${binding.targetSessionKey})`,
);
}
}
return stopWithText(lines.join("\n"));
}