Files
Evandro Fonseca Junior d1d04001d0
Release / release (push) Failing after 5s
Validate / validate (push) Failing after 5m21s
chore: fork @llblab/[email protected] as @evandrodevbr/oh-my-pi-telegram
Fork of upstream llblab/[email protected].0 (SHA afe09c5) repackaged as a
drop-in extension for can1357/oh-my-pi (omp >= 16.5.2).

Changes vs upstream:
- Rename package: @llblab/pi-telegram -> @evandrodevbr/oh-my-pi-telegram
- Bump version: 0.22.0 -> 0.22.0-evandro.1
- Update repository/homepage/bugs URLs to evandrodevbr
- Add omp field in package.json (displayName + compatibleWith)
- README: prepend 'Oh My Pi (omp) Compatibility' section with install
  commands, verified-components table, upstream lineage, and rationale
- CHANGELOG: add '0.22.0-evandro.1' entry under Unreleased
- docs/: rename @llblab/pi-telegram imports to @evandrodevbr/oh-my-pi-telegram
- tests/public-api.test.ts: rename package import paths

Gates re-run on the fork:
- npm run typecheck  -> tsc --noEmit (success)
- npm test           -> 1229 pass / 0 fail / 1 platform-only skip
- npm run audit      -> found 0 vulnerabilities
- npm run pack:check -> evandrodevbr-oh-my-pi-telegram-0.22.0-evandro.1.tgz

Verified:
- omp install --link ./pi-telegram -> Linked OK
- omp plugin list -> @evandrodevbr/[email protected].0-evandro.1

Upstream lineage: badlogic/pi-telegram -> llblab/pi-telegram (0.22.0) -> evandrodevbr/oh-my-pi-telegram (this fork)
2026-07-15 14:18:16 -03:00

1298 lines
43 KiB
TypeScript

/**
* Telegram status rendering helpers
* Zones: telegram ui, pi agent diagnostics, tui
* Builds usage, cost, and context summaries for the interactive Telegram status view
*/
export type TelegramStatusQueueLane = "control" | "priority" | "default";
export interface TelegramUsageStats {
totalInput: number;
totalOutput: number;
totalCacheRead: number;
totalCacheWrite: number;
totalCost: number;
}
interface TelegramUsageMessage {
role: string;
usage?: {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
cost: { total: number };
};
}
interface TelegramStatusSessionEntry {
type: string;
message?: TelegramUsageMessage;
}
interface TelegramContextUsage {
contextWindow?: number;
percent: number | null;
}
export interface TelegramStatusActiveModel {
provider?: string;
id?: string;
contextWindow?: number;
}
export interface TelegramStatusLineProviderContext {
activeModel: TelegramStatusActiveModel | undefined;
}
export interface TelegramStatusLineProviderResult {
label: string;
value: string;
}
export type TelegramStatusLineProvider = (
ctx: TelegramStatusLineProviderContext,
) => TelegramStatusLineProviderResult | undefined;
export interface TelegramStatusContext {
sessionManager: { getEntries(): TelegramStatusSessionEntry[] };
getContextUsage(): TelegramContextUsage | undefined;
isIdle?: () => boolean;
hasPendingMessages?: () => boolean;
isCompactionInProgress?: () => boolean;
modelRegistry: {
isUsingOAuth(model: TelegramStatusActiveModel): boolean;
};
}
export type TelegramRuntimeEventDetailValue = string | number | boolean | null;
const TELEGRAM_STATUS_LINE_PROVIDER_REGISTRY_KEY =
"__piTelegramStatusLineProviders__";
const MAX_RECENT_TELEGRAM_RUNTIME_EVENTS = 10;
const MAX_TELEGRAM_RUNTIME_EVENT_MESSAGE_LENGTH = 1000;
const MAX_TELEGRAM_RUNTIME_EVENT_DETAIL_LENGTH = 1000;
export interface TelegramRuntimeEvent {
at: number;
category: string;
message: string;
details?: Record<string, TelegramRuntimeEventDetailValue>;
}
export interface TelegramRuntimeEventInput {
category: string;
error?: unknown;
message?: string;
details?: Record<string, unknown>;
}
export interface TelegramRuntimeEventRecorder {
record: (
category: string,
error: unknown,
details?: Record<string, unknown>,
) => void;
getEvents: () => TelegramRuntimeEvent[];
clear: () => void;
}
export interface TelegramRuntimeEventRecorderOptions {
getBotToken: () => string | undefined;
maxEvents?: number;
now?: () => number;
}
export interface TelegramBridgeStatusBusFollower {
instanceId: string;
cwd?: string;
lastHeartbeatMs: number;
target?: { chatId: number; threadId?: number };
slot?: string;
threadName?: string;
status?: string;
}
export interface TelegramBridgeStatusLocalBus {
leaderSocketPath?: string;
leaderTransport?: "pipe" | "socket";
followerSocketPath?: string;
followerTransport?: "pipe" | "socket";
followerRegistered?: boolean;
followerTarget?: { chatId: number; threadId?: number };
followerSlot?: string;
followerThreadName?: string;
}
export interface TelegramBridgeStatusTopicTarget {
instanceId?: string;
status?: string;
target?: { chatId: number; threadId?: number };
slot?: string;
threadName?: string;
syncStatus?: string;
lastSyncObservedAtMs?: number;
lastSyncProbeAtMs?: number;
lastSyncError?: string;
lastReconcileAction?: string;
}
export interface TelegramBridgeStatusThreadReservation {
target?: { chatId: number; threadId?: number };
slot?: string;
reason?: string;
instanceId?: string;
expiresAtMs?: number;
lastReconcileAction?: string;
}
export interface TelegramBridgeStatusSyncObservation {
target?: { chatId: number; threadId?: number };
syncStatus: string;
observedAtMs: number;
instanceId?: string;
slot?: string;
lastSyncError?: string;
lastReconcileAction?: string;
}
export interface TelegramBridgeStatusSyncSlice {
status: string;
updatedAtMs?: number;
suspectAtMs?: number;
reason?: string;
lastReconcileAction?: string;
}
export interface TelegramBridgeThreadReconciliationState {
phase: string;
event: string;
atMs: number;
leaderEpoch?: number | string;
pendingProvisionCount: number;
syncActionCount: number;
cleanupActionCount: number;
}
export type TelegramBridgeBusRole = "leader" | "follower";
export type TelegramBridgeBusLifecyclePhase = "electing";
export interface TelegramBridgeStatusLineState {
hasBotToken?: boolean;
botUsername?: string;
activeProfileName?: string;
diagnosticPaths?: { state: string; logs: string };
allowedUserId?: number;
botThreadMode?: "unknown" | "enabled" | "disabled";
botThreadModeUpdatedAtMs?: number;
botThreadModeAction?: string;
busRole?: TelegramBridgeBusRole;
busLifecyclePhase?: TelegramBridgeBusLifecyclePhase;
instanceSlot?: string;
instanceThreadName?: string;
lockState?: string;
pollingActive: boolean;
lastUpdateId?: number;
activeSourceMessageIds?: number[];
pendingDispatch: boolean;
compactionInProgress: boolean;
activeToolExecutions: number;
pendingModelSwitch: boolean;
queuedItems: Array<{ queueLane: TelegramStatusQueueLane }>;
busFollowers?: TelegramBridgeStatusBusFollower[];
localBus?: TelegramBridgeStatusLocalBus;
topicTargets?: TelegramBridgeStatusTopicTarget[];
threadReservations?: TelegramBridgeStatusThreadReservation[];
topicSyncObservations?: TelegramBridgeStatusSyncObservation[];
syncState?: Record<string, TelegramBridgeStatusSyncSlice | undefined>;
threadReconciliation?: TelegramBridgeThreadReconciliationState;
busNowMs?: number;
recentRuntimeEvents: TelegramRuntimeEvent[];
}
export interface TelegramStatusBarTheme {
fg: (
token: "accent" | "error" | "muted" | "warning" | "success",
text: string,
) => string;
}
export interface TelegramStatusBarState {
hasBotToken: boolean;
pollingActive: boolean;
paired: boolean;
busRole?: TelegramBridgeBusRole;
busLifecyclePhase?: TelegramBridgeBusLifecyclePhase;
instanceSlot?: string;
instanceThreadName?: string;
compactionInProgress: boolean;
processing: boolean;
processingStatus?: string;
queuedStatus: string;
error?: string;
}
export interface TelegramStatusRuntimeContext {
ui: {
theme: TelegramStatusBarTheme;
setStatus: (key: string, text: string) => void;
};
}
export interface TelegramStatusRuntimeDeps<
TContext extends TelegramStatusRuntimeContext,
> {
statusKey?: string;
getStatusBarState: (ctx: TContext, error?: string) => TelegramStatusBarState;
getBridgeStatusLineState: () => TelegramBridgeStatusLineState;
}
export interface TelegramBridgeStatusConfig {
botToken?: string;
botUsername?: string;
allowedUserId?: number;
lastUpdateId?: number;
}
export interface TelegramBridgeStatusRuntimeDeps<
TQueueItem extends { queueLane: TelegramStatusQueueLane },
> {
statusKey?: string;
getConfig: () => TelegramBridgeStatusConfig;
getActiveProfileName?: () => string | undefined;
getDiagnosticPaths?: (
profileName?: string,
) => { state: string; logs: string };
isPollingActive: () => boolean;
getActiveSourceMessageIds: () => number[] | undefined;
hasActiveTurn: () => boolean;
hasDispatchPending: () => boolean;
isCompactionInProgress: () => boolean;
getActiveToolExecutions: () => number;
hasPendingModelSwitch: () => boolean;
getQueuedItems: () => TQueueItem[];
formatQueuedStatus: (items: TQueueItem[]) => string;
getRecentRuntimeEvents: () => TelegramRuntimeEvent[];
getRuntimeLockState?: () => string;
getBusRole?: () => TelegramBridgeBusRole | undefined;
getBusLifecyclePhase?: () => TelegramBridgeBusLifecyclePhase | undefined;
getBotThreadMode?: () =>
| {
threadMode: "unknown" | "enabled" | "disabled";
updatedAtMs?: number;
lastReconcileAction?: string;
}
| undefined;
getBusFollowers?: () => TelegramBridgeStatusBusFollower[];
getLocalBus?: () => TelegramBridgeStatusLocalBus | undefined;
getTopicTargets?: () => TelegramBridgeStatusTopicTarget[];
getThreadReservations?: () => TelegramBridgeStatusThreadReservation[];
getTopicSyncObservations?: () => TelegramBridgeStatusSyncObservation[];
getSyncState?: () => Record<
string,
TelegramBridgeStatusSyncSlice | undefined
>;
getThreadReconciliationState?: () =>
| TelegramBridgeThreadReconciliationState
| undefined;
getInstanceSlot?: () => string | undefined;
getInstanceThreadName?: () => string | undefined;
getNowMs?: () => number;
}
export interface TelegramBridgeStatusLineOptions {
verbose?: boolean;
}
export interface TelegramStatusRuntime<
TContext extends TelegramStatusRuntimeContext,
> {
updateStatus: (ctx: TContext, error?: string) => void;
getStatusLines: (options?: TelegramBridgeStatusLineOptions) => string[];
getStatusState: () => TelegramBridgeStatusLineState;
}
function truncateTelegramRuntimeEventText(
text: string,
maxLength: number,
): string {
if (text.length <= maxLength) return text;
return `${text.slice(0, maxLength).trimEnd()}… [truncated ${text.length - maxLength} chars]`;
}
export function redactTelegramRuntimeMessage(
message: string,
botToken: string | undefined,
): string {
const redacted = botToken
? message.split(botToken).join("<redacted-token>")
: message;
return truncateTelegramRuntimeEventText(
redacted,
MAX_TELEGRAM_RUNTIME_EVENT_MESSAGE_LENGTH,
);
}
function redactTelegramRuntimeDetail(
message: string,
botToken: string | undefined,
): string {
const redacted = botToken
? message.split(botToken).join("<redacted-token>")
: message;
return truncateTelegramRuntimeEventText(
redacted,
MAX_TELEGRAM_RUNTIME_EVENT_DETAIL_LENGTH,
);
}
function normalizeTelegramRuntimeEventDetails(
details: Record<string, unknown> | undefined,
botToken: string | undefined,
): Record<string, TelegramRuntimeEventDetailValue> | undefined {
if (!details) return undefined;
const normalized: Record<string, TelegramRuntimeEventDetailValue> = {};
for (const [key, value] of Object.entries(details)) {
if (value === undefined) continue;
if (typeof value === "string") {
normalized[key] = redactTelegramRuntimeDetail(value, botToken);
continue;
}
if (typeof value === "number" || typeof value === "boolean") {
normalized[key] = value;
continue;
}
if (value === null) {
normalized[key] = null;
continue;
}
normalized[key] = redactTelegramRuntimeDetail(String(value), botToken);
}
return Object.keys(normalized).length > 0 ? normalized : undefined;
}
function getTelegramRuntimeEventMessage(
input: TelegramRuntimeEventInput,
): string {
if (input.message !== undefined) return input.message;
if (input.error instanceof Error) return input.error.message;
return String(input.error);
}
export function recordStructuredTelegramRuntimeEvent(
events: TelegramRuntimeEvent[],
input: TelegramRuntimeEventInput,
options: { botToken?: string; maxEvents: number; now?: number },
): void {
const details = normalizeTelegramRuntimeEventDetails(
input.details,
options.botToken,
);
events.push({
at: options.now ?? Date.now(),
category: input.category,
message: redactTelegramRuntimeMessage(
getTelegramRuntimeEventMessage(input),
options.botToken,
),
...(details ? { details } : {}),
});
while (events.length > options.maxEvents) {
events.shift();
}
}
export function recordTelegramRuntimeEvent(
events: TelegramRuntimeEvent[],
category: string,
error: unknown,
options: { botToken?: string; maxEvents: number; now?: number },
): void {
recordStructuredTelegramRuntimeEvent(events, { category, error }, options);
}
function getOrCreateTelegramStatusLineProviderRegistry(): Map<
string,
TelegramStatusLineProvider
> {
const existing = (globalThis as Record<string, unknown>)[
TELEGRAM_STATUS_LINE_PROVIDER_REGISTRY_KEY
];
if (existing instanceof Map)
return existing as Map<string, TelegramStatusLineProvider>;
const registry = new Map<string, TelegramStatusLineProvider>();
(globalThis as Record<string, unknown>)[
TELEGRAM_STATUS_LINE_PROVIDER_REGISTRY_KEY
] = registry;
return registry;
}
/**
* Register a compact extension-provided line for the Telegram status menu.
*
* Providers are synchronous and should return undefined when their line is not
* relevant for the active model. Errors are isolated so optional extension
* status cannot break the core Telegram menu.
*/
export function registerTelegramStatusLineProvider(
provider: TelegramStatusLineProvider,
options: { id: string },
): () => void {
const registry = getOrCreateTelegramStatusLineProviderRegistry();
registry.set(options.id, provider);
return () => {
if (registry.get(options.id) === provider) registry.delete(options.id);
};
}
export function getTelegramStatusLineProviderResults(
ctx: TelegramStatusLineProviderContext,
): TelegramStatusLineProviderResult[] {
const results: TelegramStatusLineProviderResult[] = [];
const registry = getOrCreateTelegramStatusLineProviderRegistry();
for (const provider of registry.values()) {
try {
const result = provider(ctx);
if (!result?.label || !result.value) continue;
results.push(result);
} catch {
continue;
}
}
return results;
}
export function clearTelegramStatusLineProviders(): void {
getOrCreateTelegramStatusLineProviderRegistry().clear();
}
export function createTelegramRuntimeEventRecorder(
options: TelegramRuntimeEventRecorderOptions,
): TelegramRuntimeEventRecorder {
const events: TelegramRuntimeEvent[] = [];
return {
record: (category, error, details) => {
recordStructuredTelegramRuntimeEvent(
events,
{ category, error, details },
{
botToken: options.getBotToken(),
maxEvents: options.maxEvents ?? MAX_RECENT_TELEGRAM_RUNTIME_EVENTS,
now: options.now?.(),
},
);
},
getEvents: () => events,
clear: () => {
events.length = 0;
},
};
}
function formatTelegramRuntimeEventCategory(
event: TelegramRuntimeEvent,
): string {
const method = event.details?.method;
return typeof method === "string"
? `${event.category}:${method}`
: event.category;
}
function formatTelegramRuntimeEventDetails(
event: TelegramRuntimeEvent,
): string {
if (!event.details) return "";
const details = Object.entries(event.details)
.filter(([key]) => key !== "method")
.map(([key, value]) => `${key}=${JSON.stringify(value)}`);
return details.length > 0 ? ` (${details.join(", ")})` : "";
}
function formatTelegramRuntimeEventSummary(
event: TelegramRuntimeEvent,
): string {
return `${formatTelegramRuntimeEventCategory(event)}: ${event.message}${formatTelegramRuntimeEventDetails(event)}`;
}
function formatTelegramRuntimeEvent(event: TelegramRuntimeEvent): string {
return `${new Date(event.at).toISOString()} ${formatTelegramRuntimeEventSummary(event)}`;
}
function buildTelegramRuntimeEventSummary(
events: TelegramRuntimeEvent[],
): string {
const counts = new Map<string, number>();
for (const event of events) {
const category = formatTelegramRuntimeEventCategory(event);
counts.set(category, (counts.get(category) ?? 0) + 1);
}
return Array.from(counts.entries())
.sort(([left], [right]) => left.localeCompare(right))
.map(([category, count]) => `${category}=${count}`)
.join(", ");
}
export function buildTelegramRuntimeEventLines(
events: TelegramRuntimeEvent[],
): string[] {
if (events.length === 0) return ["recent runtime events: none"];
return [
"recent runtime events:",
`- summary: ${buildTelegramRuntimeEventSummary(events)}`,
...events
.slice()
.reverse()
.map((event) => `- ${formatTelegramRuntimeEvent(event)}`),
];
}
export function createTelegramStatusHtmlBuilder<TContext>(deps: {
getActiveModel: (ctx: TContext) => TelegramStatusActiveModel | undefined;
isCompactionInProgress?: () => boolean;
getBridgeStatusLineState?: () => TelegramBridgeStatusLineState;
}): (ctx: TContext & TelegramStatusContext) => string {
return (ctx) =>
buildStatusHtml(
{ ...ctx, isCompactionInProgress: deps.isCompactionInProgress },
deps.getActiveModel(ctx),
deps.getBridgeStatusLineState?.(),
);
}
export function createTelegramStatusRuntime<
TContext extends TelegramStatusRuntimeContext,
>(deps: TelegramStatusRuntimeDeps<TContext>): TelegramStatusRuntime<TContext> {
const statusKey = deps.statusKey ?? "telegram";
return {
updateStatus: (ctx, error) => {
ctx.ui.setStatus(
statusKey,
buildTelegramStatusBarText(
ctx.ui.theme,
deps.getStatusBarState(ctx, error),
),
);
},
getStatusLines: (options) =>
buildTelegramBridgeStatusLines(deps.getBridgeStatusLineState(), options),
getStatusState: deps.getBridgeStatusLineState,
};
}
export function createTelegramBridgeStatusRuntime<
TContext extends TelegramStatusRuntimeContext,
TQueueItem extends { queueLane: TelegramStatusQueueLane },
>(
deps: TelegramBridgeStatusRuntimeDeps<TQueueItem>,
): TelegramStatusRuntime<TContext> {
return createTelegramStatusRuntime({
statusKey: deps.statusKey,
getStatusBarState: (_ctx, error) => {
const config = deps.getConfig();
const queuedItems = deps.getQueuedItems();
const hasActiveTurn = deps.hasActiveTurn();
const hasPendingDispatch = deps.hasDispatchPending();
const hasPendingModelSwitch = deps.hasPendingModelSwitch();
const activeToolExecutions = deps.getActiveToolExecutions();
const compactionInProgress = deps.isCompactionInProgress();
return {
hasBotToken: !!config.botToken,
pollingActive: deps.isPollingActive(),
paired: !!config.allowedUserId,
busRole: deps.getBusRole?.(),
busLifecyclePhase: deps.getBusLifecyclePhase?.(),
instanceSlot: deps.getInstanceSlot?.(),
instanceThreadName: deps.getInstanceThreadName?.(),
compactionInProgress,
processing:
hasActiveTurn ||
hasPendingDispatch ||
hasPendingModelSwitch ||
activeToolExecutions > 0 ||
queuedItems.length > 0,
processingStatus: getTelegramStatusBarProcessingStatus({
hasActiveTurn,
hasPendingDispatch,
hasPendingModelSwitch,
activeToolExecutions,
queuedItems: queuedItems.length,
}),
queuedStatus: deps.formatQueuedStatus(queuedItems),
error,
};
},
getBridgeStatusLineState: () => {
const config = deps.getConfig();
const botThreadMode = deps.getBotThreadMode?.();
const activeProfileName = deps.getActiveProfileName?.();
return {
hasBotToken: Boolean(config.botToken),
botUsername: config.botUsername,
activeProfileName,
diagnosticPaths: deps.getDiagnosticPaths?.(activeProfileName),
allowedUserId: config.allowedUserId,
botThreadMode: botThreadMode?.threadMode,
botThreadModeUpdatedAtMs: botThreadMode?.updatedAtMs,
botThreadModeAction: botThreadMode?.lastReconcileAction,
busRole: deps.getBusRole?.(),
busLifecyclePhase: deps.getBusLifecyclePhase?.(),
instanceSlot: deps.getInstanceSlot?.(),
instanceThreadName: deps.getInstanceThreadName?.(),
lockState: deps.getRuntimeLockState?.(),
pollingActive: deps.isPollingActive(),
lastUpdateId: config.lastUpdateId,
activeSourceMessageIds: deps.getActiveSourceMessageIds(),
pendingDispatch: deps.hasDispatchPending(),
compactionInProgress: deps.isCompactionInProgress(),
activeToolExecutions: deps.getActiveToolExecutions(),
pendingModelSwitch: deps.hasPendingModelSwitch(),
queuedItems: deps.getQueuedItems(),
busFollowers: deps.getBusFollowers?.(),
localBus: deps.getLocalBus?.(),
topicTargets: deps.getTopicTargets?.(),
threadReservations: deps.getThreadReservations?.(),
topicSyncObservations: deps.getTopicSyncObservations?.(),
syncState: deps.getSyncState?.(),
threadReconciliation: deps.getThreadReconciliationState?.(),
busNowMs: deps.getNowMs?.(),
recentRuntimeEvents: deps.getRecentRuntimeEvents(),
};
},
});
}
export interface TelegramRuntimeLogScope extends Record<string, unknown> {
instanceId: string;
role: string;
slot?: string;
threadName?: string;
lockState?: string;
}
export function createTelegramRuntimeLogScope(input: {
state: TelegramBridgeStatusLineState;
instanceId: string;
}): TelegramRuntimeLogScope {
return {
instanceId: input.instanceId,
role: input.state.busRole ?? "classic-or-disconnected",
slot: input.state.instanceSlot,
threadName: input.state.instanceThreadName,
lockState: input.state.lockState,
};
}
export function createTelegramStatusSnapshot(
state: TelegramBridgeStatusLineState,
): {
runtime: Record<string, unknown>;
liveRoster: Record<string, unknown>;
diagnostics: Record<string, unknown>;
} {
return {
runtime: {
busRole: state.busRole,
...(state.busLifecyclePhase
? { busLifecyclePhase: state.busLifecyclePhase }
: {}),
botThreadMode: state.botThreadMode,
botThreadModeUpdatedAtMs: state.botThreadModeUpdatedAtMs,
botThreadModeAction: state.botThreadModeAction,
instanceSlot: state.instanceSlot,
instanceThreadName: state.instanceThreadName,
pollingActive: state.pollingActive,
lockState: state.lockState,
},
liveRoster: {
busFollowers: state.busFollowers ?? [],
...(state.localBus ? { localBus: state.localBus } : {}),
topicTargets: state.topicTargets ?? [],
reservations: state.threadReservations ?? [],
syncObservations: state.topicSyncObservations ?? [],
},
diagnostics: {
pendingDispatch: state.pendingDispatch,
compactionInProgress: state.compactionInProgress,
activeToolExecutions: state.activeToolExecutions,
pendingModelSwitch: state.pendingModelSwitch,
syncState: state.syncState,
threadReconciliation: state.threadReconciliation,
recentRuntimeEvents: state.recentRuntimeEvents,
},
};
}
export function createTelegramRuntimeDiagnosticsSnapshotScheduler(deps: {
persistSnapshot: () => Promise<void>;
recordError: (error: unknown) => void;
setTimer?: (callback: () => void, ms: number) => { unref?: () => void };
}): () => void {
const setTimer = deps.setTimer ?? setTimeout;
let timer: { unref?: () => void } | number | undefined;
return () => {
if (timer) return;
timer = setTimer(() => {
timer = undefined;
void deps.persistSnapshot().catch(deps.recordError);
}, 0);
if (typeof timer !== "number") timer?.unref?.();
};
}
export function getTelegramStatusBarProcessingStatus(state: {
hasActiveTurn: boolean;
hasPendingDispatch: boolean;
hasPendingModelSwitch: boolean;
activeToolExecutions: number;
queuedItems: number;
}): string | undefined {
if (state.hasPendingModelSwitch) return "model";
if (state.hasActiveTurn || state.activeToolExecutions > 0) return "active";
if (state.hasPendingDispatch) return "dispatching";
if (state.queuedItems > 0) return "queued";
return undefined;
}
function getTelegramStatusBarLabel(state: TelegramStatusBarState): string {
const threadName = state.instanceThreadName?.trim();
if (!threadName) return "telegram";
const genericLabels = new Set(["telegram", "leader", "follower"]);
if (genericLabels.has(threadName.toLowerCase())) return "telegram";
return threadName;
}
export function buildTelegramStatusBarText(
theme: TelegramStatusBarTheme,
state: TelegramStatusBarState,
): string {
const label = theme.fg("accent", getTelegramStatusBarLabel(state));
if (state.error) {
return `${label} ${theme.fg("error", "error")} ${theme.fg("muted", state.error)}`;
}
const queued = state.queuedStatus
? theme.fg("success", state.queuedStatus)
: "";
if (!state.hasBotToken)
return `${label} ${theme.fg("muted", "not configured")}${queued}`;
if (!state.paired)
return `${label} ${theme.fg("warning", "awaiting pairing")}${queued}`;
if (state.busLifecyclePhase === "electing")
return `${label} ${theme.fg("warning", "electing")}${queued}`;
if (!state.pollingActive && state.busRole !== "follower")
return `${theme.fg("accent", "telegram")} ${theme.fg("muted", "disconnected")}${queued}`;
if (state.processing) {
const processingStatus = state.queuedStatus
? "active"
: (state.processingStatus ?? "processing");
const processingToken =
processingStatus === "active" ? "warning" : "accent";
return `${label} ${theme.fg(processingToken, processingStatus)}${queued}`;
}
if (state.busRole === "follower")
return `${label} ${theme.fg("success", "follower")}${queued}`;
if (state.busRole === "leader")
return `${label} ${theme.fg("success", "leader")}`;
return `${label} ${theme.fg("success", "connected")}`;
}
function formatTelegramBridgeBotStatus(
state: Pick<TelegramBridgeStatusLineState, "hasBotToken" | "botUsername">,
): string {
if (state.botUsername) return `@${state.botUsername}`;
return state.hasBotToken ? "unknown" : "not configured";
}
function formatTelegramStatusTarget(
target: { chatId: number; threadId?: number } | undefined,
): string {
if (!target) return "";
return target.threadId === undefined
? ` target ${target.chatId}`
: ` target ${target.chatId}:${target.threadId}`;
}
function formatTelegramThreadStatusLabel(input: {
threadName?: string;
slot?: string;
}): string {
const threadName = input.threadName?.trim();
if (threadName) return threadName;
return input.slot ? `[${input.slot}]` : "";
}
function buildTelegramBusFollowerLines(
state: Pick<TelegramBridgeStatusLineState, "busFollowers" | "busNowMs">,
): string[] {
const followers = state.busFollowers ?? [];
if (followers.length === 0) return [];
const nowMs = state.busNowMs ?? Date.now();
return [
"",
"bus:",
`- followers: ${followers.length}`,
...followers.map((follower) => {
const ageSeconds = Math.max(
0,
Math.round((nowMs - follower.lastHeartbeatMs) / 1000),
);
const label = formatTelegramThreadStatusLabel(follower);
const labelSuffix = label ? ` ${label}` : "";
const statusLabel = follower.status ? ` (${follower.status})` : "";
const cwd = follower.cwd ? ` ${follower.cwd}` : "";
const target = formatTelegramStatusTarget(follower.target);
return `- ${follower.instanceId}:${labelSuffix} heartbeat ${ageSeconds}s ago${statusLabel}${target}${cwd}`;
}),
];
}
function buildTelegramLocalBusLines(
state: Pick<TelegramBridgeStatusLineState, "localBus">,
options: { verbose?: boolean } = {},
): string[] {
const localBus = state.localBus;
if (!localBus) return [];
const target = formatTelegramStatusTarget(localBus.followerTarget);
const label = formatTelegramThreadStatusLabel({
slot: localBus.followerSlot,
threadName: localBus.followerThreadName,
});
const followerLine = `- follower registered: ${localBus.followerRegistered ? "yes" : "no"}${label ? ` ${label}` : ""}${target}`;
const lines = ["", "local bus:", followerLine];
if (options.verbose) {
if (localBus.leaderSocketPath) {
const transport = localBus.leaderTransport
? ` [${localBus.leaderTransport}]`
: "";
lines.push(`- leader endpoint${transport}: ${localBus.leaderSocketPath}`);
}
if (localBus.followerSocketPath) {
const transport = localBus.followerTransport
? ` [${localBus.followerTransport}]`
: "";
lines.push(
`- follower endpoint${transport}: ${localBus.followerSocketPath}`,
);
}
}
return lines;
}
function buildTelegramSyncSliceLines(
state: Pick<TelegramBridgeStatusLineState, "syncState">,
): string[] {
const syncState = state.syncState;
if (!syncState || Object.keys(syncState).length === 0) return [];
return [
"sync:",
...Object.entries(syncState).map(([slice, value]) => {
const status = value?.status ?? "unknown";
const action = value?.lastReconcileAction
? ` reconcile=${value.lastReconcileAction}`
: "";
const reason = value?.reason ? ` reason=${value.reason}` : "";
return `- ${slice}: ${status}${action}${reason}`;
}),
];
}
function buildTelegramThreadReconciliationLines(
state: Pick<TelegramBridgeStatusLineState, "threadReconciliation">,
): string[] {
const reconciliation = state.threadReconciliation;
if (!reconciliation) return [];
const epoch =
reconciliation.leaderEpoch !== undefined
? ` epoch=${reconciliation.leaderEpoch}`
: "";
return [
"reconciliation:",
`- phase: ${reconciliation.phase} event=${reconciliation.event}${epoch}`,
`- counts: pending=${reconciliation.pendingProvisionCount}, sync=${reconciliation.syncActionCount}, cleanup=${reconciliation.cleanupActionCount}`,
];
}
function buildTelegramTopicTargetDiagnosticLines(
state: Pick<
TelegramBridgeStatusLineState,
"topicTargets" | "threadReservations" | "topicSyncObservations"
>,
): string[] {
const activeTargets = (state.topicTargets ?? []).filter(
(record) =>
!!record.instanceId &&
(record.status === "active" || record.status === "starting"),
);
const reservations = state.threadReservations ?? [];
const observations = state.topicSyncObservations ?? [];
if (
activeTargets.length === 0 &&
reservations.length === 0 &&
observations.length === 0
)
return [];
const byInstance = new Map<string, TelegramBridgeStatusTopicTarget[]>();
for (const record of activeTargets) {
const key = record.instanceId;
if (!key) continue;
const records = byInstance.get(key) ?? [];
records.push(record);
byInstance.set(key, records);
}
const duplicateLines = Array.from(byInstance.entries())
.filter(([, records]) => records.length > 1)
.map(([instanceId, records]) => {
const targets = records
.map((record) => {
const label = formatTelegramThreadStatusLabel(record);
return `${label}${formatTelegramStatusTarget(record.target) || " unknown"}`.trim();
})
.join(", ");
return `- duplicate ${instanceId}: ${records.length} active threads ${targets}`;
});
const twinLines = activeTargets.map((record) => {
const label = formatTelegramThreadStatusLabel(record);
const target = formatTelegramStatusTarget(record.target) || " unknown";
const sync = record.syncStatus ? ` sync=${record.syncStatus}` : "";
const observed = record.lastSyncObservedAtMs
? ` observed=${new Date(record.lastSyncObservedAtMs).toISOString()}`
: "";
const probe = record.lastSyncProbeAtMs
? ` probed=${new Date(record.lastSyncProbeAtMs).toISOString()}`
: "";
const error = record.lastSyncError
? ` syncError=${record.lastSyncError}`
: "";
const action = record.lastReconcileAction
? ` reconcile=${record.lastReconcileAction}`
: "";
return `- ${label}${target}${sync}${observed}${probe}${error}${action}`.trim();
});
const reservationLines = reservations.map((reservation) => {
const slot = reservation.slot ? `[${reservation.slot}]` : "";
const target = formatTelegramStatusTarget(reservation.target) || " unknown";
const reason = reservation.reason ? ` reason=${reservation.reason}` : "";
const instance = reservation.instanceId
? ` instance=${reservation.instanceId}`
: "";
const action = reservation.lastReconcileAction
? ` reconcile=${reservation.lastReconcileAction}`
: "";
return `- reservation ${slot}${target}${reason}${instance}${action}`.trim();
});
const observationLines = observations.map((observation) => {
const slot = observation.slot ? `[${observation.slot}]` : "";
const target = formatTelegramStatusTarget(observation.target) || " unknown";
const observed = ` observed=${new Date(observation.observedAtMs).toISOString()}`;
const instance = observation.instanceId
? ` instance=${observation.instanceId}`
: "";
const error = observation.lastSyncError
? ` syncError=${observation.lastSyncError}`
: "";
const action = observation.lastReconcileAction
? ` reconcile=${observation.lastReconcileAction}`
: "";
return `- sync ${slot}${target} sync=${observation.syncStatus}${observed}${instance}${error}${action}`.trim();
});
return [
"topics:",
`- active bindings: instances=${byInstance.size}, targets=${activeTargets.length}`,
...duplicateLines,
...twinLines,
...reservationLines,
...observationLines,
];
}
function buildTelegramBridgeCompactThreadLines(
state: Pick<
TelegramBridgeStatusLineState,
| "busFollowers"
| "topicTargets"
| "threadReservations"
| "topicSyncObservations"
>,
): string[] {
const activeTargets = (state.topicTargets ?? []).filter(
(record) =>
!!record.instanceId &&
(record.status === "active" || record.status === "starting"),
);
const activeLabels = activeTargets
.map(formatTelegramThreadStatusLabel)
.filter((label) => label.length > 0);
const followers = state.busFollowers ?? [];
const reservations = state.threadReservations ?? [];
const observations = state.topicSyncObservations ?? [];
if (
activeLabels.length === 0 &&
followers.length === 0 &&
reservations.length === 0 &&
observations.length === 0
) {
return [];
}
const lines = ["threads:"];
if (activeLabels.length > 0)
lines.push(`- active: ${activeLabels.join(", ")}`);
if (followers.length > 0) lines.push(`- followers: ${followers.length}`);
if (reservations.length > 0) lines.push(`- reserved: ${reservations.length}`);
const syncIssueCount = observations.filter(
(observation) => observation.syncStatus !== "open",
).length;
if (syncIssueCount > 0) lines.push(`- sync issues: ${syncIssueCount}`);
return lines;
}
function buildTelegramBridgeCompactStatusLines(
state: TelegramBridgeStatusLineState,
): string[] {
const controlQueueCount = state.queuedItems.filter(
(item) => item.queueLane === "control",
).length;
const priorityQueueCount = state.queuedItems.filter(
(item) => item.queueLane === "priority",
).length;
const defaultQueueCount = state.queuedItems.filter(
(item) => item.queueLane === "default",
).length;
const queueLine = `- queued turns: ${state.queuedItems.length}${
state.queuedItems.length > 0
? ` (control=${controlQueueCount}, priority=${priorityQueueCount}, default=${defaultQueueCount})`
: ""
}`;
const executionState = state.pendingDispatch
? "pending dispatch"
: state.activeSourceMessageIds?.length
? "active"
: "idle";
const profileSuffix = state.activeProfileName
? `.${state.activeProfileName.replace(/[^a-zA-Z0-9._-]+/g, "_")}`
: "";
const profileSlug = profileSuffix.slice(1);
const diagnosticsPaths = state.diagnosticPaths ?? {
state: `~/.pi/agent/tmp/telegram/state${profileSuffix}.json`,
logs: `~/.pi/agent/tmp/telegram/logs${profileSlug ? `.${profileSlug}` : ""}.jsonl`,
};
return [
"connection:",
`- bot: ${formatTelegramBridgeBotStatus(state)}`,
...(state.activeProfileName ? [`- profile: ${state.activeProfileName}`] : []),
`- user: ${state.allowedUserId ?? "not paired"}`,
...(state.botThreadMode ? [`- thread mode: ${state.botThreadMode}`] : []),
...(state.busRole ? [`- role: ${state.busRole}`] : []),
...(state.busLifecyclePhase
? [`- lifecycle: ${state.busLifecyclePhase}`]
: []),
...(state.instanceThreadName || state.instanceSlot
? [`- instance: ${state.instanceThreadName ?? state.instanceSlot}`]
: []),
...(state.lockState ? [`- owner: ${state.lockState}`] : []),
"",
"health:",
`- polling: ${state.pollingActive ? "running" : "stopped"}`,
`- state: ${executionState}`,
queueLine,
...(state.activeToolExecutions > 0
? [`- active tools: ${state.activeToolExecutions}`]
: []),
...(state.pendingModelSwitch ? ["- pending model switch: yes"] : []),
...buildTelegramBridgeCompactThreadLines(state),
...buildTelegramBusFollowerLines(state),
...buildTelegramLocalBusLines(state),
...buildTelegramThreadReconciliationLines(state),
"",
"diagnostics:",
`- state: ${diagnosticsPaths.state}`,
`- logs: ${diagnosticsPaths.logs}`,
"- full dump: /telegram-status --debug",
];
}
export function buildTelegramBridgeStatusLines(
state: TelegramBridgeStatusLineState,
options: TelegramBridgeStatusLineOptions = {},
): string[] {
if (options.verbose) return buildTelegramBridgeDiagnosticStatusLines(state);
return buildTelegramBridgeCompactStatusLines(state);
}
export function buildTelegramBridgeDiagnosticStatusLines(
state: TelegramBridgeStatusLineState,
): string[] {
const controlQueueCount = state.queuedItems.filter(
(item) => item.queueLane === "control",
).length;
const priorityQueueCount = state.queuedItems.filter(
(item) => item.queueLane === "priority",
).length;
const defaultQueueCount = state.queuedItems.filter(
(item) => item.queueLane === "default",
).length;
return [
"connection:",
`- bot: ${formatTelegramBridgeBotStatus(state)}`,
...(state.activeProfileName ? [`- profile: ${state.activeProfileName}`] : []),
`- allowed user: ${state.allowedUserId ?? "not paired"}`,
...(state.botThreadMode
? [
`- thread mode: ${state.botThreadMode}${state.botThreadModeAction ? ` reconcile=${state.botThreadModeAction}` : ""}`,
]
: []),
...(state.busRole ? [`- bus role: ${state.busRole}`] : []),
...(state.busLifecyclePhase
? [`- bus lifecycle: ${state.busLifecyclePhase}`]
: []),
...(state.instanceThreadName || state.instanceSlot
? [`- instance: ${state.instanceThreadName ?? state.instanceSlot}`]
: []),
...(state.lockState ? [`- owner: ${state.lockState}`] : []),
"",
"polling:",
`- state: ${state.pollingActive ? "running" : "stopped"}`,
`- last update id: ${state.lastUpdateId ?? "none"}`,
"",
"execution:",
`- active turn: ${state.activeSourceMessageIds?.join(",") || "no"}`,
`- pending dispatch: ${state.pendingDispatch ? "yes" : "no"}`,
`- compaction: ${state.compactionInProgress ? "running" : "idle"}`,
`- active tools: ${state.activeToolExecutions}`,
`- pending model switch: ${state.pendingModelSwitch ? "yes" : "no"}`,
"",
"queue:",
`- queued turns: ${state.queuedItems.length}`,
`- lanes: control=${controlQueueCount}, priority=${priorityQueueCount}, default=${defaultQueueCount}`,
...buildTelegramBusFollowerLines(state),
...buildTelegramLocalBusLines(state, { verbose: true }),
...buildTelegramTopicTargetDiagnosticLines(state),
...buildTelegramThreadReconciliationLines(state),
...buildTelegramSyncSliceLines(state),
"",
...buildTelegramRuntimeEventLines(state.recentRuntimeEvents),
];
}
function escapeHtml(text: string): string {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
function formatTokens(count: number): string {
if (count < 1000) return count.toString();
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
if (count < 1000000) return `${Math.round(count / 1000)}k`;
if (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;
return `${Math.round(count / 1000000)}M`;
}
function collectUsageStats(ctx: TelegramStatusContext): TelegramUsageStats {
const stats: TelegramUsageStats = {
totalInput: 0,
totalOutput: 0,
totalCacheRead: 0,
totalCacheWrite: 0,
totalCost: 0,
};
for (const entry of ctx.sessionManager.getEntries()) {
const usage = entry.message?.usage;
if (
entry.type !== "message" ||
entry.message?.role !== "assistant" ||
!usage
) {
continue;
}
stats.totalInput += usage.input;
stats.totalOutput += usage.output;
stats.totalCacheRead += usage.cacheRead;
stats.totalCacheWrite += usage.cacheWrite;
stats.totalCost += usage.cost.total;
}
return stats;
}
function formatStatusRowLabel(label: string): string {
if (!label) return label;
return `${label[0]?.toUpperCase() ?? ""}${label.slice(1)}`;
}
function buildStatusRow(label: string, value: string): string {
return `<b>${escapeHtml(formatStatusRowLabel(label))}:</b> <code>${escapeHtml(value)}</code>`;
}
function buildUsageSummary(stats: TelegramUsageStats): string | undefined {
const tokenParts: string[] = [];
if (stats.totalInput) tokenParts.push(`↑${formatTokens(stats.totalInput)}`);
if (stats.totalOutput) tokenParts.push(`↓${formatTokens(stats.totalOutput)}`);
if (stats.totalCacheRead)
tokenParts.push(`R${formatTokens(stats.totalCacheRead)}`);
if (stats.totalCacheWrite)
tokenParts.push(`W${formatTokens(stats.totalCacheWrite)}`);
return tokenParts.length > 0 ? tokenParts.join(" ") : undefined;
}
function buildCostSummary(
stats: TelegramUsageStats,
usesSubscription: boolean,
): string | undefined {
if (!stats.totalCost && !usesSubscription) return undefined;
return `$${stats.totalCost.toFixed(3)}${usesSubscription ? " (sub)" : ""}`;
}
function buildContextSummary(
ctx: TelegramStatusContext,
activeModel: TelegramStatusActiveModel | undefined,
): string {
const usage = ctx.getContextUsage();
if (!usage) return "unknown";
const contextWindow = usage.contextWindow ?? activeModel?.contextWindow ?? 0;
const percent = usage.percent !== null ? `${usage.percent.toFixed(1)}%` : "?";
return `${percent}/${formatTokens(contextWindow)}`;
}
function buildStatusSummary(ctx: TelegramStatusContext): string {
if (ctx.hasPendingMessages?.()) return "pending";
if (ctx.isIdle?.() === false) return "active";
if (ctx.isIdle?.() === true) return "idle";
return "unknown";
}
function buildTelegramStatusRoleSuffix(
state: TelegramBridgeStatusLineState | undefined,
): string {
if (state?.botThreadMode !== "enabled" || !state.busRole) return "";
return ` @${state.busRole}`;
}
export function buildStatusHtml(
ctx: TelegramStatusContext,
activeModel: TelegramStatusActiveModel | undefined,
bridgeStatus?: TelegramBridgeStatusLineState,
): string {
const stats = collectUsageStats(ctx);
const usesSubscription = activeModel
? ctx.modelRegistry.isUsingOAuth(activeModel)
: false;
const lines: string[] = [
buildStatusRow(
"Status",
`${buildStatusSummary(ctx)}${buildTelegramStatusRoleSuffix(bridgeStatus)}`,
),
];
const usageSummary = buildUsageSummary(stats);
const costSummary = buildCostSummary(stats, usesSubscription);
if (usageSummary) {
lines.push(buildStatusRow("Usage", usageSummary));
}
if (costSummary) {
lines.push(buildStatusRow("Cost", costSummary));
}
lines.push(buildStatusRow("Context", buildContextSummary(ctx, activeModel)));
for (const row of getTelegramStatusLineProviderResults({ activeModel })) {
lines.push(buildStatusRow(row.label, row.value));
}
return lines.join("\n");
}