feat: human error states, operator toasts and a queue/origin view in the omp UI

- classify transport and API failures into a short state with the next step
- count undelivered messages and show them next to the sent counter
- toast on owner paired, connection recovered and reply not delivered (throttled)
- second status line previews the next queued turns
- host spinner names the Telegram prompt while its turn runs
- sanitize every Telegram-controlled string that reaches the widget
This commit is contained in:
evandrodevbr committed 2026-10-01 17:33:37 -03:00
1 parent 9435ff709b
commit 057281a3bb
19 files changed
+862 -22

No files matched your search

+14
View File
@@ -329,6 +329,8 @@ export function registerTelegramLifecycleRuntimeHooks({
updateStatus,
recordRuntimeEvent,
}: TelegramLifecycleBindingDeps): void {
// Whether this bridge set the host spinner text, so it only clears its own.
let telegramWorkingMessageShown = false;
const agentEndResetter = Runtime.createTelegramAgentEndResetter({
abort,
typing,
@@ -589,6 +591,14 @@ export function registerTelegramLifecycleRuntimeHooks({
await agentStartWithDedupReset(event, ctx);
activityRuntime.onAgentStart(activeTurnRuntime.get()?.target);
startAgentActivityTypingLoop(ctx);
const telegramTurn = activeTurnRuntime.get();
if (telegramTurn) {
Pi.setExtensionWorkingMessage(
ctx,
Status.formatTelegramWorkingMessage(telegramTurn.statusSummary),
);
telegramWorkingMessageShown = true;
}
},
async onToolExecutionStart(event, ctx) {
if (!isSessionContextActive(ctx)) return;
@@ -638,6 +648,10 @@ export function registerTelegramLifecycleRuntimeHooks({
onAgentSettled(_event, ctx) {
if (!isSessionContextActive(ctx)) return;
activityRuntime.onAgentSettled();
if (telegramWorkingMessageShown) {
telegramWorkingMessageShown = false;
Pi.setExtensionWorkingMessage(ctx, undefined);
}
},
settleOnTerminalAgentEnd: Pi.isOmpHost(pi),
onBeforeAgentStart: Prompts.createTelegramProactiveBeforeAgentStartHook({
+8 -3
View File
@@ -777,6 +777,8 @@ export interface TelegramUserPairingRuntimeDeps<TContext> {
setAllowedUserId: (userId: number) => void;
persistConfig: () => Promise<void>;
updateStatus: (ctx: TContext) => void;
/** Called once the user was accepted as owner and the config persisted. */
onPaired?: (userId: number, ctx: TContext) => void;
}
export interface TelegramUserPairingRuntime<TContext> {
@@ -827,13 +829,16 @@ export function createTelegramUserPairingRuntime<TContext>(
deps: TelegramUserPairingRuntimeDeps<TContext>,
): TelegramUserPairingRuntime<TContext> {
return {
pairIfNeeded: (userId, ctx) =>
pairTelegramUserIfNeeded(userId, {
pairIfNeeded: async (userId, ctx) => {
const paired = await pairTelegramUserIfNeeded(userId, {
allowedUserId: deps.getAllowedUserId(),
ctx,
setAllowedUserId: deps.setAllowedUserId,
persistConfig: deps.persistConfig,
updateStatus: deps.updateStatus,
}),
});
if (paired) deps.onPaired?.(userId, ctx);
return paired;
},
};
}
+100
View File
@@ -0,0 +1,100 @@
/**
* Telegram operator notices
* Zones: telegram ui, tui
* Owns the one-off host toasts (owner paired, connection recovered, reply not delivered) and their throttling
*/
import {
sanitizeTelegramStatusText,
summarizeTelegramStatusError,
} from "./status.ts";
export type TelegramNoticeLevel = "info" | "warning" | "error";
/** A failed delivery raises at most one notice per this window. */
export const TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS = 30_000;
export interface TelegramNoticeRuntimeDeps<TContext> {
/** Current session context, when the bridge has one. */
getContext: () => TContext | undefined;
notify: (ctx: TContext, message: string, level: TelegramNoticeLevel) => void;
recordRuntimeEvent: (
category: string,
error: unknown,
details?: Record<string, unknown>,
) => void;
now?: () => number;
}
export interface TelegramNoticeRuntime<TContext> {
/** The first Telegram user was accepted as the bridge owner. */
paired: (userId: number, ctx: TContext) => void;
/** Polling works again after at least one failed attempt. */
recovered: (ctx: TContext) => void;
/** A message could not be delivered; throttled, uses the current context. */
deliveryFailed: (error: unknown) => void;
}
function describeTelegramDeliveryFailure(error: unknown): {
reason: string;
level: TelegramNoticeLevel;
} {
const message = error instanceof Error ? error.message : String(error);
const summary = summarizeTelegramStatusError(message);
if (summary) {
return {
reason: summary.label,
level: summary.kind === "fatal" ? "error" : "warning",
};
}
const raw = sanitizeTelegramStatusText(message);
return {
reason: raw.length > 80 ? `${raw.slice(0, 79)}…` : raw,
level: "error",
};
}
/**
* Toasts never propagate failures into the path that raised them: a stale
* session context only produces a recorded runtime event.
*/
export function createTelegramNoticeRuntime<TContext>(
deps: TelegramNoticeRuntimeDeps<TContext>,
): TelegramNoticeRuntime<TContext> {
const now = deps.now ?? Date.now;
let lastDeliveryNoticeAtMs: number | undefined;
const show = (
ctx: TContext,
message: string,
level: TelegramNoticeLevel,
): void => {
try {
deps.notify(ctx, message, level);
} catch (error) {
deps.recordRuntimeEvent("status", error, { phase: "notice" });
}
};
return {
paired(userId, ctx) {
show(ctx, `Telegram paired with user ${userId}`, "info");
},
recovered(ctx) {
show(ctx, "Telegram reconnected", "info");
},
deliveryFailed(error) {
const ctx = deps.getContext();
if (ctx === undefined) return;
const nowMs = now();
if (
lastDeliveryNoticeAtMs !== undefined &&
nowMs - lastDeliveryNoticeAtMs <
TELEGRAM_DELIVERY_FAILED_NOTICE_COOLDOWN_MS
) {
return;
}
lastDeliveryNoticeAtMs = nowMs;
const { reason, level } = describeTelegramDeliveryFailure(error);
show(ctx, `Telegram: reply not delivered (${reason})`, level);
},
};
}
+21
View File
@@ -260,6 +260,27 @@ export function isTerminalAgentEnd(event: AgentEndEvent): boolean {
return !("willContinue" in event && event.willContinue === true);
}
/** Show a one-off toast in the host UI; a no-op where the host has no UI. */
export function notifyExtensionContext(
ctx: ExtensionContext,
message: string,
level: "info" | "warning" | "error",
): void {
ctx.ui.notify(message, level);
}
/**
* Replace the host spinner text, or restore its default with `undefined`.
* Pi-compatible hosts do not all expose `setWorkingMessage`; the spinner text
* is cosmetic, so a host without it is skipped instead of failing the turn.
*/
export function setExtensionWorkingMessage(
ctx: ExtensionContext,
message?: string,
): void {
ctx.ui.setWorkingMessage?.(message);
}
export function isExtensionContextIdle(ctx: ExtensionContext): boolean {
return ctx.isIdle();
}
+20
View File
@@ -163,6 +163,7 @@ export function createTelegramPollingControllerRuntime<
updateStatus: deps.updateStatus,
sleep: deps.sleep,
maxUpdateFailures: deps.maxUpdateFailures,
onRecovered: deps.onRecovered,
recordRuntimeEvent: deps.recordRuntimeEvent,
}),
updateStatus: deps.updateStatus,
@@ -913,6 +914,8 @@ export interface TelegramPollLoopDeps<
onStatusReset: () => void;
sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
maxUpdateFailures?: number;
/** Called on the first successful poll after a failed attempt. */
onRecovered?: () => void;
}
export interface TelegramPollLoopRunnerDeps<
@@ -930,6 +933,8 @@ export interface TelegramPollLoopRunnerDeps<
updateStatus: (ctx: TContext, message?: string) => void;
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
maxUpdateFailures?: number;
/** Called on the first successful poll after a failed attempt. */
onRecovered?: (ctx: TContext) => void;
}
export function sleepTelegramPollingRetry(
@@ -994,6 +999,15 @@ export function createTelegramPollLoopRunner<
},
sleep,
maxUpdateFailures: deps.maxUpdateFailures,
onRecovered: () => {
try {
deps.onRecovered?.(ctx);
} catch (error) {
deps.recordRuntimeEvent?.("polling", error, {
phase: "recovered-notice",
});
}
},
recordRuntimeEvent: deps.recordRuntimeEvent,
});
}
@@ -1041,6 +1055,7 @@ export async function runTelegramPollLoop<
const admittedUpdates = new Set<number>();
let handledUpdateFailureRethrown = false;
let consecutiveGetUpdatesConflicts = 0;
let failing = false;
while (!deps.signal.aborted) {
try {
const updates = await deps.getUpdates(
@@ -1048,6 +1063,10 @@ export async function runTelegramPollLoop<
deps.signal,
);
consecutiveGetUpdatesConflicts = 0;
if (failing) {
failing = false;
deps.onRecovered?.();
}
for (const update of updates) {
if (admittedUpdates.has(update.update_id)) {
deps.config.lastUpdateId = update.update_id;
@@ -1105,6 +1124,7 @@ export async function runTelegramPollLoop<
continue;
}
consecutiveGetUpdatesConflicts = 0;
failing = true;
deps.onErrorStatus(getTelegramPollingErrorMessage(error));
await deps.sleep(TELEGRAM_POLLING_RETRY_MS, deps.signal);
if (deps.signal.aborted) return;
+19
View File
@@ -499,6 +499,25 @@ export function formatQueuedTelegramItemsStatus<TContext = unknown>(
return items.length === 0 ? "" : ` +${items.length}`;
}
/**
* Summaries of the waiting items in dispatch order, for the status preview.
* Display must never break the status update, so a malformed item only
* costs the ordering.
*/
export function formatQueuedTelegramItemsPreview<TContext = unknown>(
items: TelegramQueueItem<TContext>[],
): string[] {
let ordered = items;
try {
ordered = [...items].sort(compareTelegramQueueItems);
} catch {
// Keep the store order.
}
return ordered
.map((item) => item.statusSummary.trim())
.filter((summary) => summary.length > 0);
}
export function truncateTelegramQueueSummary(
text: string,
maxWords = 5,
+3
View File
@@ -556,6 +556,8 @@ export interface TelegramInboundRouteRuntimeDeps<
) => Promise<TResponse>;
/** Called for every message from the paired owner, before it is dispatched. */
onAuthorizedMessage?: (ctx: TContext) => void;
/** Called when the first Telegram user is accepted as the bridge owner. */
onPaired?: (userId: number, ctx: TContext) => void;
getCurrentInstanceId?: () => string | undefined;
getMessageOwnership?: Updates.TelegramMessageOwnershipLookup;
getTargetOwnership?: Updates.TelegramTargetOwnershipLookup;
@@ -2076,6 +2078,7 @@ export function createTelegramInboundRouteRuntime<
setAllowedUserId: deps.configStore.setAllowedUserId,
persistConfig: deps.configStore.persist,
updateStatus: deps.updateStatus,
onPaired: deps.onPaired,
removePendingMediaGroupMessages: deps.mediaGroupRuntime.removeMessages,
removeQueuedTelegramTurnsByMessageIds:
deps.queueMutationRuntime.removeByMessageIds,
+158 -9
View File
@@ -8,6 +8,8 @@
export interface TelegramStatusTraffic {
received: number;
sent: number;
/** Deliveries that failed after the transport retries; absent means none. */
failed?: number;
lastAtMs?: number;
}
@@ -253,6 +255,8 @@ export interface TelegramStatusBarState {
processing: boolean;
processingStatus?: string;
queuedStatus: string;
/** Summaries of the queued turns, in dispatch order. */
queuedPreview?: string[];
error?: string;
}
@@ -303,6 +307,7 @@ export interface TelegramBridgeStatusRuntimeDeps<
hasPendingModelSwitch: () => boolean;
getQueuedItems: () => TQueueItem[];
formatQueuedStatus: (items: TQueueItem[]) => string;
getQueuedPreview?: (items: TQueueItem[]) => string[];
getRecentRuntimeEvents: () => TelegramRuntimeEvent[];
getRuntimeLockState?: () => string;
getBusRole?: () => TelegramBridgeBusRole | undefined;
@@ -597,7 +602,7 @@ export function createTelegramStatusRuntime<
const statusKey = deps.statusKey ?? "telegram";
return {
updateStatus: (ctx, error) => {
const text = buildTelegramStatusBarText(
const lines = buildTelegramStatusBarLines(
ctx.ui.theme,
deps.getStatusBarState(ctx, error),
);
@@ -605,11 +610,11 @@ export function createTelegramStatusRuntime<
// The host strips ANSI from status text, so setStatus would drop the
// theme colors. String widgets keep them; clear the plain footer entry
// so the state is not shown twice.
ctx.ui.setWidget(statusKey, [text], { placement: "belowEditor" });
ctx.ui.setWidget(statusKey, lines, { placement: "belowEditor" });
ctx.ui.setStatus(statusKey, undefined);
return;
}
ctx.ui.setStatus(statusKey, text);
ctx.ui.setStatus(statusKey, lines[0]);
},
getStatusLines: (options) =>
buildTelegramBridgeStatusLines(deps.getBridgeStatusLineState(), options),
@@ -658,6 +663,7 @@ export function createTelegramBridgeStatusRuntime<
queuedItems: queuedItems.length,
}),
queuedStatus: deps.formatQueuedStatus(queuedItems),
queuedPreview: deps.getQueuedPreview?.(queuedItems),
error,
};
},
@@ -846,10 +852,15 @@ function buildTelegramStatusBarDetails(
state: TelegramStatusBarState,
): string {
const parts: string[] = [];
if (state.botUsername) parts.push(`@${state.botUsername}`);
if (state.botUsername) {
parts.push(`@${sanitizeTelegramStatusText(state.botUsername)}`);
}
if (state.traffic) {
const failed = state.traffic.failed
? ` ${glyphs.error}${state.traffic.failed}`
: "";
parts.push(
`${glyphs.input}${state.traffic.received} ${glyphs.output}${state.traffic.sent}`,
`${glyphs.input}${state.traffic.received} ${glyphs.output}${state.traffic.sent}${failed}`,
);
if (state.traffic.lastAtMs !== undefined) {
parts.push(
@@ -871,28 +882,126 @@ function buildTelegramTrafficLines(
? ""
: `, last ${formatTelegramStatusBarClock(state.traffic.lastAtMs)}`;
return [
`- messages: ${state.traffic.received} received, ${state.traffic.sent} sent${last}`,
`- messages: ${state.traffic.received} received, ${state.traffic.sent} sent${state.traffic.failed ? `, ${state.traffic.failed} failed` : ""}${last}`,
];
}
/**
* Strip control characters and bidi overrides. Widget lines bypass the host's
* status sanitizer, and thread names, bot names, queue summaries and error text
* can all originate from Telegram, so none may carry terminal escapes.
*/
export function sanitizeTelegramStatusText(text: string): string {
return text
.replace(
/[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g,
" ",
)
.replace(/ +/g, " ")
.trim();
}
export interface TelegramStatusErrorSummary {
/** Short state text such as "invalid token". */
label: string;
/** What the operator can do next, or "retrying" while the bridge retries. */
hint?: string;
/** fatal needs the operator, attention needs a look, retrying heals itself. */
kind: "fatal" | "attention" | "retrying";
}
/**
* Turn a raw transport or API failure into a short state and next step.
* Returns undefined for anything unrecognized so the raw text stays visible.
*/
export function summarizeTelegramStatusError(
message: string,
): TelegramStatusErrorSummary | undefined {
if (/HTTP 401\b|\bUnauthorized\b/i.test(message)) {
return { kind: "fatal", label: "invalid token", hint: "/telegram-setup" };
}
if (/HTTP 429\b|Too Many Requests|retry[ _]after/i.test(message)) {
const seconds = /retry[ _]after[:= ]+(\d+)/i.exec(message)?.[1];
return {
kind: "retrying",
label: seconds ? `rate limited ${seconds}s` : "rate limited",
hint: "retrying",
};
}
if (
/HTTP 409\b|\bConflict\b|terminated by other getUpdates/i.test(message)
) {
return {
kind: "attention",
label: "another poller is active",
hint: "/telegram-status",
};
}
if (
/HTTP 5\d\d\b|Bad Gateway|Service Unavailable|Internal Server Error/i.test(
message,
)
) {
return { kind: "retrying", label: "Telegram unavailable", hint: "retrying" };
}
if (
/Unable to connect|ECONNREFUSED|ECONNRESET|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|fetch failed|network|timed? out|socket hang up/i.test(
message,
)
) {
return { kind: "retrying", label: "offline", hint: "retrying" };
}
return undefined;
}
function formatTelegramStatusBarError(
theme: TelegramStatusBarTheme,
glyphs: TelegramStatusBarGlyphs,
label: string,
error: string,
): string {
const summary = summarizeTelegramStatusError(error);
if (!summary) {
const raw = sanitizeTelegramStatusText(error);
const shown = raw.length > 100 ? `${raw.slice(0, 99)}…` : raw;
return `${label} ${theme.fg("error", `${glyphs.error} error`)} ${theme.fg("muted", shown)}`;
}
const token = summary.kind === "fatal" ? "error" : "warning";
const glyph =
summary.kind === "fatal"
? glyphs.error
: summary.kind === "retrying"
? glyphs.running
: glyphs.warning;
const hint = summary.hint
? theme.fg("muted", `${glyphs.dot}${summary.hint}`)
: "";
return `${label} ${theme.fg(token, `${glyph} ${summary.label}`)}${hint}`;
}
export function buildTelegramStatusBarText(
theme: TelegramStatusBarTheme,
state: TelegramStatusBarState,
): string {
const glyphs = resolveTelegramStatusBarGlyphs(theme);
const label = theme.fg("accent", getTelegramStatusBarLabel(state));
const label = theme.fg(
"accent",
sanitizeTelegramStatusText(getTelegramStatusBarLabel(state)),
);
const hint = (text: string) => theme.fg("muted", `${glyphs.dot}${text}`);
const queued = state.queuedStatus
? theme.fg("success", state.queuedStatus)
: "";
if (state.error) {
return `${label} ${theme.fg("error", `${glyphs.error} error`)} ${theme.fg("muted", state.error)}`;
return formatTelegramStatusBarError(theme, glyphs, label, state.error);
}
if (!state.hasBotToken) {
return `${label} ${theme.fg("muted", `${glyphs.disabled} not configured`)}${queued}${hint("/telegram-setup")}`;
}
if (!state.paired) {
const bot = state.botUsername ? `@${state.botUsername}` : "the bot";
const bot = state.botUsername
? `@${sanitizeTelegramStatusText(state.botUsername)}`
: "the bot";
return `${label} ${theme.fg("warning", `${glyphs.warning} awaiting pairing`)}${queued}${hint(`send /start to ${bot}`)}`;
}
if (state.busLifecyclePhase === "electing") {
@@ -914,6 +1023,46 @@ export function buildTelegramStatusBarText(
return `${label} ${theme.fg("success", `${glyphs.enabled} ${role}`)}${queued}${details}`;
}
/** Up to two queued turns and a remainder count, only while the bridge is up. */
function buildTelegramStatusBarQueueLine(
theme: TelegramStatusBarTheme,
state: TelegramStatusBarState,
): string | undefined {
const operational =
state.hasBotToken &&
state.paired &&
!state.error &&
state.busLifecyclePhase !== "electing" &&
(state.pollingActive || state.busRole === "follower");
const preview = (state.queuedPreview ?? [])
.map(sanitizeTelegramStatusText)
.filter((summary) => summary.length > 0);
if (!operational || preview.length === 0) return undefined;
const glyphs = resolveTelegramStatusBarGlyphs(theme);
const shown = preview.slice(0, 2).map((summary) => `"${summary}"`);
const more =
preview.length > 2 ? `${glyphs.dot}+${preview.length - 2} more` : "";
return theme.fg("muted", ` next: ${shown.join(glyphs.dot)}${more}`);
}
/** The main status line, plus a queue preview line when turns are waiting. */
export function buildTelegramStatusBarLines(
theme: TelegramStatusBarTheme,
state: TelegramStatusBarState,
): string[] {
const queueLine = buildTelegramStatusBarQueueLine(theme, state);
return [
buildTelegramStatusBarText(theme, state),
...(queueLine ? [queueLine] : []),
];
}
/** Host spinner text while a turn that arrived from Telegram is running. */
export function formatTelegramWorkingMessage(summary: string): string {
const text = sanitizeTelegramStatusText(summary);
return text ? `Answering Telegram · "${text}"` : "Answering Telegram";
}
function formatTelegramBridgeBotStatus(
state: Pick<TelegramBridgeStatusLineState, "hasBotToken" | "botUsername">,
): string {
+6
View File
@@ -377,6 +377,8 @@ export interface TelegramBridgeApiRuntimeDeps {
) => void;
/** Called after a Bot API call or multipart upload succeeded. */
onCallSucceeded?: (method: string) => void;
/** Called after a Bot API call or multipart upload failed for good. */
onCallFailed?: (method: string, error: unknown) => void;
}
export interface TelegramBridgeApiRuntime {
@@ -1249,6 +1251,7 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
getBotToken: () => string | undefined;
recordRuntimeEvent: TelegramBridgeApiRuntimeDeps["recordRuntimeEvent"];
onCallSucceeded?: TelegramBridgeApiRuntimeDeps["onCallSucceeded"];
onCallFailed?: TelegramBridgeApiRuntimeDeps["onCallFailed"];
}): TelegramBridgeApiRuntime {
return createTelegramBridgeApiRuntime({
client: createTelegramApiClient(deps.getBotToken, {
@@ -1259,6 +1262,7 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
tempFileMaxAgeMs: TELEGRAM_TEMP_FILE_MAX_AGE_MS,
recordRuntimeEvent: deps.recordRuntimeEvent,
onCallSucceeded: deps.onCallSucceeded,
onCallFailed: deps.onCallFailed,
});
}
@@ -1279,6 +1283,7 @@ export function createTelegramBridgeApiRuntime(
error,
withTelegramTransportDiagnostics(error, { method }),
);
deps.onCallFailed?.(method, error);
throw error;
}
deps.onCallSucceeded?.(method);
@@ -1316,6 +1321,7 @@ export function createTelegramBridgeApiRuntime(
error,
withTelegramTransportDiagnostics(error, { method, fileName }),
);
deps.onCallFailed?.(method, error);
throw error;
}
deps.onCallSucceeded?.(method);
+19
View File
@@ -11,6 +11,8 @@ export interface TelegramTrafficSnapshot {
received: number;
/** Messages the bridge delivered to Telegram since the bridge loaded. */
sent: number;
/** Messages that could not be delivered after the transport retries. */
failed: number;
/** Time of the latest received or sent message, when there was one. */
lastAtMs?: number;
}
@@ -18,6 +20,7 @@ export interface TelegramTrafficSnapshot {
export interface TelegramTrafficCounters {
recordReceived: () => void;
recordSent: () => void;
recordFailed: () => void;
snapshot: () => TelegramTrafficSnapshot;
}
@@ -48,6 +51,7 @@ export function createTelegramTrafficCounters(
): TelegramTrafficCounters {
let received = 0;
let sent = 0;
let failed = 0;
let lastAtMs: number | undefined;
return {
recordReceived() {
@@ -58,9 +62,13 @@ export function createTelegramTrafficCounters(
sent += 1;
lastAtMs = now();
},
recordFailed() {
failed += 1;
},
snapshot: () => ({
received,
sent,
failed,
...(lastAtMs === undefined ? {} : { lastAtMs }),
}),
};
@@ -76,11 +84,15 @@ export interface TelegramTrafficStatusBridgeDeps<TContext> {
error: unknown,
details?: Record<string, unknown>,
) => void;
/** Told about every message delivery that failed, e.g. to raise a notice. */
onDeliveryFailed?: (error: unknown) => void;
}
export interface TelegramTrafficStatusBridge<TContext> {
/** Observer for successful Bot API calls; counts delivered messages. */
onCallSucceeded: (method: string) => void;
/** Observer for failed Bot API calls; counts undelivered messages. */
onCallFailed: (method: string, error: unknown) => void;
/** Observer for messages accepted from the paired owner. */
onAuthorizedMessage: (ctx: TContext) => void;
}
@@ -107,6 +119,13 @@ export function createTelegramTrafficStatusBridge<TContext>(
const ctx = deps.getContext();
if (ctx !== undefined) refreshStatus(ctx);
},
onCallFailed(method, error) {
if (!isTelegramOutboundMessageMethod(method)) return;
deps.counters.recordFailed();
deps.onDeliveryFailed?.(error);
const ctx = deps.getContext();
if (ctx !== undefined) refreshStatus(ctx);
},
onAuthorizedMessage(ctx) {
deps.counters.recordReceived();
refreshStatus(ctx);
+1
View File
@@ -850,6 +850,7 @@ export function createTelegramPairedUpdateRuntime<
setAllowedUserId: deps.setAllowedUserId,
persistConfig: deps.persistConfig,
updateStatus: deps.updateStatus,
onPaired: deps.onPaired,
}).pairIfNeeded,
answerCallbackQuery: deps.answerCallbackQuery,
answerGuestQuery: deps.answerGuestQuery,