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

632 lines
19 KiB
TypeScript

/**
* Telegram bridge runtime-state helpers
* Zones: pi agent runtime state, telegram session, shared coordination
* Owns small session-local runtime primitives that are shared by orchestration but are not specific to queueing, rendering, polling, or Telegram transport
*/
const TELEGRAM_TYPING_ACTION_INTERVAL_MS = 2500;
const TELEGRAM_TYPING_IDLE_DRAIN_MAX_MS = 250;
export interface TelegramRuntimeQueueCounters {
nextQueuedTelegramItemOrder: number;
nextQueuedTelegramControlOrder: number;
nextPriorityReactionOrder: number;
}
export interface TelegramRuntimeLifecycleFlags {
activeTelegramToolExecutions: number;
telegramTurnDispatchPending: boolean;
compactionInProgress: boolean;
foldQueuedPromptsIntoHistory: boolean;
setupInProgress: boolean;
}
export interface TelegramBridgeRuntimeState
extends TelegramRuntimeQueueCounters, TelegramRuntimeLifecycleFlags {
abortHandler?: () => void;
typingInterval?: ReturnType<typeof setInterval>;
typingInFlight?: Promise<void>;
typingLoopDeps?: TelegramTypingLoopDeps;
typingLoopKey?: string;
}
export interface TelegramRuntimeQueuePort {
syncCounters: (counters: Partial<TelegramRuntimeQueueCounters>) => void;
allocateItemOrder: () => number;
allocateControlOrder: () => number;
getNextPriorityReactionOrder: () => number;
incrementNextPriorityReactionOrder: () => void;
}
export interface TelegramRuntimeLifecyclePort {
syncFlags: (flags: Partial<TelegramRuntimeLifecycleFlags>) => void;
getActiveToolExecutions: () => number;
setActiveToolExecutions: (count: number) => void;
resetActiveToolExecutions: () => void;
hasDispatchPending: () => boolean;
setDispatchPending: (pending: boolean) => void;
clearDispatchPending: () => void;
isCompactionInProgress: () => boolean;
setCompactionInProgress: (inProgress: boolean) => void;
shouldFoldQueuedPromptsIntoHistory: () => boolean;
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
}
export interface TelegramRuntimeSetupPort {
isInProgress: () => boolean;
start: () => boolean;
finish: () => void;
}
export interface TelegramRuntimeAbortPort {
hasHandler: () => boolean;
setHandler: (abortHandler: () => void) => void;
clearHandler: () => void;
getHandler: () => (() => void) | undefined;
abortTurn: () => boolean;
}
export interface TelegramRuntimeTypingPort {
start: (deps: TelegramTypingLoopDeps) => boolean;
stop: () => boolean;
waitForIdle: () => Promise<void>;
}
export interface TelegramBridgeRuntime {
state: TelegramBridgeRuntimeState;
queue: TelegramRuntimeQueuePort;
lifecycle: TelegramRuntimeLifecyclePort;
setup: TelegramRuntimeSetupPort;
abort: TelegramRuntimeAbortPort;
typing: TelegramRuntimeTypingPort;
}
export function createTelegramBridgeRuntimeState(): TelegramBridgeRuntimeState {
return {
nextQueuedTelegramItemOrder: 0,
nextQueuedTelegramControlOrder: 0,
nextPriorityReactionOrder: 0,
activeTelegramToolExecutions: 0,
telegramTurnDispatchPending: false,
compactionInProgress: false,
foldQueuedPromptsIntoHistory: false,
setupInProgress: false,
};
}
export function createTelegramBridgeRuntime(
state = createTelegramBridgeRuntimeState(),
): TelegramBridgeRuntime {
return {
state,
queue: {
syncCounters: (counters) =>
syncTelegramQueueRuntimeCounters(state, counters),
allocateItemOrder: () => allocateTelegramQueueItemOrder(state),
allocateControlOrder: () => allocateTelegramQueueControlOrder(state),
getNextPriorityReactionOrder: () =>
getNextTelegramPriorityReactionOrder(state),
incrementNextPriorityReactionOrder: () =>
incrementNextTelegramPriorityReactionOrder(state),
},
lifecycle: {
syncFlags: (flags) => syncTelegramLifecycleRuntimeFlags(state, flags),
getActiveToolExecutions: () => getActiveTelegramToolExecutions(state),
setActiveToolExecutions: (count) =>
setActiveTelegramToolExecutions(state, count),
resetActiveToolExecutions: () => resetActiveTelegramToolExecutions(state),
hasDispatchPending: () => hasTelegramDispatchPending(state),
setDispatchPending: (pending) =>
setTelegramDispatchPending(state, pending),
clearDispatchPending: () => clearTelegramDispatchPending(state),
isCompactionInProgress: () => isTelegramCompactionInProgress(state),
setCompactionInProgress: (inProgress) =>
setTelegramCompactionInProgress(state, inProgress),
shouldFoldQueuedPromptsIntoHistory: () =>
shouldFoldQueuedPromptsIntoHistory(state),
setFoldQueuedPromptsIntoHistory: (fold) =>
setFoldQueuedPromptsIntoHistory(state, fold),
},
setup: {
isInProgress: () => isTelegramSetupInProgress(state),
start: () => startTelegramSetup(state),
finish: () => finishTelegramSetup(state),
},
abort: {
hasHandler: () => hasTelegramAbortHandler(state),
setHandler: (abortHandler) =>
setTelegramAbortHandler(state, abortHandler),
clearHandler: () => clearTelegramAbortHandler(state),
getHandler: () => getTelegramAbortHandler(state),
abortTurn: () => abortTelegramTurn(state),
},
typing: {
start: (deps) => startTelegramTypingLoop(state, deps),
stop: () => stopTelegramTypingLoop(state),
waitForIdle: () => waitForTelegramTypingLoopIdle(state),
},
};
}
export function syncTelegramQueueRuntimeCounters(
state: TelegramBridgeRuntimeState,
counters: Partial<TelegramRuntimeQueueCounters>,
): void {
if (counters.nextQueuedTelegramItemOrder !== undefined) {
state.nextQueuedTelegramItemOrder = counters.nextQueuedTelegramItemOrder;
}
if (counters.nextQueuedTelegramControlOrder !== undefined) {
state.nextQueuedTelegramControlOrder =
counters.nextQueuedTelegramControlOrder;
}
if (counters.nextPriorityReactionOrder !== undefined) {
state.nextPriorityReactionOrder = counters.nextPriorityReactionOrder;
}
}
export function allocateTelegramQueueItemOrder(
state: TelegramBridgeRuntimeState,
): number {
return state.nextQueuedTelegramItemOrder++;
}
export function allocateTelegramQueueControlOrder(
state: TelegramBridgeRuntimeState,
): number {
return state.nextQueuedTelegramControlOrder++;
}
export function getNextTelegramPriorityReactionOrder(
state: TelegramBridgeRuntimeState,
): number {
return state.nextPriorityReactionOrder;
}
export function incrementNextTelegramPriorityReactionOrder(
state: TelegramBridgeRuntimeState,
): void {
state.nextPriorityReactionOrder += 1;
}
export function syncTelegramLifecycleRuntimeFlags(
state: TelegramBridgeRuntimeState,
flags: Partial<TelegramRuntimeLifecycleFlags>,
): void {
if (flags.activeTelegramToolExecutions !== undefined) {
state.activeTelegramToolExecutions = flags.activeTelegramToolExecutions;
}
if (flags.telegramTurnDispatchPending !== undefined) {
state.telegramTurnDispatchPending = flags.telegramTurnDispatchPending;
}
if (flags.compactionInProgress !== undefined) {
state.compactionInProgress = flags.compactionInProgress;
}
if (flags.foldQueuedPromptsIntoHistory !== undefined) {
state.foldQueuedPromptsIntoHistory = flags.foldQueuedPromptsIntoHistory;
}
if (flags.setupInProgress !== undefined) {
state.setupInProgress = flags.setupInProgress;
}
}
export function getActiveTelegramToolExecutions(
state: TelegramBridgeRuntimeState,
): number {
return state.activeTelegramToolExecutions;
}
export function setActiveTelegramToolExecutions(
state: TelegramBridgeRuntimeState,
count: number,
): void {
state.activeTelegramToolExecutions = count;
}
export function resetActiveTelegramToolExecutions(
state: TelegramBridgeRuntimeState,
): void {
state.activeTelegramToolExecutions = 0;
}
export function hasTelegramDispatchPending(
state: TelegramBridgeRuntimeState,
): boolean {
return state.telegramTurnDispatchPending;
}
function setTelegramDispatchPending(
state: TelegramBridgeRuntimeState,
pending: boolean,
): void {
state.telegramTurnDispatchPending = pending;
}
export function clearTelegramDispatchPending(
state: TelegramBridgeRuntimeState,
): void {
state.telegramTurnDispatchPending = false;
}
export function isTelegramCompactionInProgress(
state: TelegramBridgeRuntimeState,
): boolean {
return state.compactionInProgress;
}
export function setTelegramCompactionInProgress(
state: TelegramBridgeRuntimeState,
inProgress: boolean,
): void {
state.compactionInProgress = inProgress;
}
export function shouldFoldQueuedPromptsIntoHistory(
state: TelegramBridgeRuntimeState,
): boolean {
return state.foldQueuedPromptsIntoHistory;
}
export function setFoldQueuedPromptsIntoHistory(
state: TelegramBridgeRuntimeState,
fold: boolean,
): void {
state.foldQueuedPromptsIntoHistory = fold;
}
export function isTelegramSetupInProgress(
state: TelegramBridgeRuntimeState,
): boolean {
return state.setupInProgress;
}
export function startTelegramSetup(state: TelegramBridgeRuntimeState): boolean {
if (state.setupInProgress) return false;
state.setupInProgress = true;
return true;
}
export function finishTelegramSetup(state: TelegramBridgeRuntimeState): void {
state.setupInProgress = false;
}
export function hasTelegramAbortHandler(
state: TelegramBridgeRuntimeState,
): boolean {
return !!state.abortHandler;
}
export function setTelegramAbortHandler(
state: TelegramBridgeRuntimeState,
abortHandler: () => void,
): void {
state.abortHandler = abortHandler;
}
export function clearTelegramAbortHandler(
state: TelegramBridgeRuntimeState,
): void {
state.abortHandler = undefined;
}
export function getTelegramAbortHandler(
state: TelegramBridgeRuntimeState,
): (() => void) | undefined {
return state.abortHandler;
}
export function abortTelegramTurn(state: TelegramBridgeRuntimeState): boolean {
if (!state.abortHandler) return false;
state.abortHandler();
return true;
}
export interface TelegramTypingLoopTarget {
chatId: number;
threadId?: number;
}
function getTelegramTypingLoopThreadParams(
target: TelegramTypingLoopTarget | undefined,
): { message_thread_id?: number } | undefined {
const threadId = target?.threadId;
return Number.isInteger(threadId)
? { message_thread_id: threadId }
: undefined;
}
export interface TelegramTypingLoopDeps {
chatId: number | undefined;
target?: TelegramTypingLoopTarget;
intervalMs: number;
sendTypingAction: (
chatId: number,
options?: { message_thread_id?: number },
) => Promise<unknown>;
sendAggregateTypingAction?: (chatId: number) => Promise<unknown>;
}
export interface TelegramRuntimeEventRecorderPort {
recordRuntimeEvent?: (
category: string,
error: unknown,
details?: Record<string, unknown>,
) => void;
}
function updateTelegramRuntimeStatusSafely<TContext>(
updateStatus: (ctx: TContext, error?: string) => void,
ctx: TContext,
options: {
error?: string;
category: string;
phase: string;
recordRuntimeEvent?: TelegramRuntimeEventRecorderPort["recordRuntimeEvent"];
},
): void {
try {
updateStatus(ctx, options.error);
} catch (statusError) {
options.recordRuntimeEvent?.(options.category, statusError, {
phase: options.phase,
});
}
}
export interface TelegramTypingLoopStarterDeps<
TContext,
> extends TelegramRuntimeEventRecorderPort {
typing: TelegramRuntimeTypingPort;
getDefaultChatId: () => number | undefined;
sendTypingAction: (
chatId: number,
options?: { message_thread_id?: number },
) => Promise<unknown>;
sendAggregateTypingAction?: (chatId: number) => Promise<unknown>;
updateStatus: (ctx: TContext, error?: string) => void;
intervalMs?: number;
}
export function createTelegramTypingLoopStarter<TContext>(
deps: TelegramTypingLoopStarterDeps<TContext>,
): (
ctx: TContext,
chatId?: number,
options?: { target?: TelegramTypingLoopTarget },
) => void {
return (ctx, chatId, options) => {
deps.typing.start({
chatId: chatId ?? deps.getDefaultChatId(),
target: options?.target,
intervalMs: deps.intervalMs ?? TELEGRAM_TYPING_ACTION_INTERVAL_MS,
sendTypingAction: async (targetChatId, actionOptions) => {
try {
await deps.sendTypingAction(targetChatId, actionOptions);
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
updateTelegramRuntimeStatusSafely(deps.updateStatus, ctx, {
error: message,
category: "typing",
phase: "status-update",
recordRuntimeEvent: deps.recordRuntimeEvent,
});
deps.recordRuntimeEvent?.("typing", error, {
chatId: targetChatId,
});
}
},
sendAggregateTypingAction: deps.sendAggregateTypingAction
? async (targetChatId) => {
try {
await deps.sendAggregateTypingAction?.(targetChatId);
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
updateTelegramRuntimeStatusSafely(deps.updateStatus, ctx, {
error: message,
category: "typing",
phase: "status-update",
recordRuntimeEvent: deps.recordRuntimeEvent,
});
deps.recordRuntimeEvent?.("typing", error, {
chatId: targetChatId,
aggregate: true,
});
}
}
: undefined,
});
};
}
function getTelegramTypingLoopKey(deps: TelegramTypingLoopDeps): string {
const threadId = deps.target?.threadId;
return `${deps.chatId ?? 0}:${Number.isInteger(threadId) ? threadId : "all"}`;
}
export function startTelegramTypingLoop(
state: TelegramBridgeRuntimeState,
deps: TelegramTypingLoopDeps,
): boolean {
if (deps.chatId === undefined || deps.chatId === 0) return false;
const previousKey = state.typingLoopKey;
const nextKey = getTelegramTypingLoopKey(deps);
state.typingLoopDeps = deps;
state.typingLoopKey = nextKey;
const sendTyping = (): void => {
const activeDeps = state.typingLoopDeps;
if (!activeDeps || activeDeps.chatId === undefined || activeDeps.chatId === 0)
return;
const targetChatId = activeDeps.chatId;
const threadParams = getTelegramTypingLoopThreadParams(activeDeps.target);
const typing = Promise.resolve()
.then(async () => {
await activeDeps.sendTypingAction(targetChatId, threadParams);
if (threadParams?.message_thread_id !== undefined) {
await activeDeps.sendAggregateTypingAction?.(targetChatId);
}
})
.then(() => undefined)
.catch(() => undefined);
state.typingInFlight = typing;
void typing.finally(() => {
if (state.typingInFlight === typing) state.typingInFlight = undefined;
});
};
if (state.typingInterval) {
if (previousKey === nextKey) return false;
sendTyping();
return true;
}
sendTyping();
state.typingInterval = setInterval(sendTyping, deps.intervalMs);
state.typingInterval.unref?.();
return true;
}
export function stopTelegramTypingLoop(
state: TelegramBridgeRuntimeState,
): boolean {
if (!state.typingInterval) return false;
clearInterval(state.typingInterval);
state.typingInterval = undefined;
state.typingLoopDeps = undefined;
state.typingLoopKey = undefined;
return true;
}
export async function waitForTelegramTypingLoopIdle(
state: TelegramBridgeRuntimeState,
timeoutMs = TELEGRAM_TYPING_IDLE_DRAIN_MAX_MS,
): Promise<void> {
const inFlight = state.typingInFlight;
if (!inFlight) return;
if (timeoutMs <= 0) {
await Promise.race([inFlight, Promise.resolve()]);
return;
}
await Promise.race([
inFlight,
new Promise<void>((resolve) => {
const timer = setTimeout(resolve, timeoutMs);
timer.unref?.();
}),
]);
}
export function createTelegramContextAbortHandlerSetter<
TContext extends { abort: () => void },
>(
abort: Pick<TelegramRuntimeAbortPort, "setHandler">,
): (ctx: TContext) => void {
return (ctx) => {
abort.setHandler(() => ctx.abort());
};
}
export interface TelegramAgentEndResetDeps {
abort: Pick<TelegramRuntimeAbortPort, "clearHandler">;
typing: Pick<TelegramRuntimeTypingPort, "stop">;
clearActiveTurn: () => void;
resetToolExecutions: () => void;
clearPendingModelSwitch: () => void;
clearDispatchPending: () => void;
}
export function createTelegramAgentEndResetter(
deps: TelegramAgentEndResetDeps,
): () => void {
return () => {
deps.abort.clearHandler();
deps.typing.stop();
deps.clearActiveTurn();
deps.resetToolExecutions();
deps.clearPendingModelSwitch();
deps.clearDispatchPending();
};
}
export interface TelegramPromptDispatchLifecycleDeps<
TContext,
> extends TelegramRuntimeEventRecorderPort {
lifecycle: Pick<
TelegramRuntimeLifecyclePort,
"setDispatchPending" | "clearDispatchPending"
>;
typing: Pick<TelegramRuntimeTypingPort, "stop">;
startTypingLoop: (
ctx: TContext,
chatId?: number,
options?: { target?: TelegramTypingLoopTarget },
) => void;
updateStatus: (ctx: TContext, error?: string) => void;
}
export interface TelegramPromptDispatchRuntimeDeps<
TContext,
> extends TelegramRuntimeEventRecorderPort {
lifecycle: TelegramPromptDispatchLifecycleDeps<TContext>["lifecycle"];
typing: TelegramRuntimeTypingPort;
getDefaultChatId: () => number | undefined;
sendTypingAction: (
chatId: number,
options?: { message_thread_id?: number },
) => Promise<unknown>;
sendAggregateTypingAction?: (chatId: number) => Promise<unknown>;
updateStatus: (ctx: TContext, error?: string) => void;
intervalMs?: number;
}
export interface TelegramPromptDispatchRuntime<TContext> {
startTypingLoop: (
ctx: TContext,
chatId?: number,
options?: { target?: TelegramTypingLoopTarget },
) => void;
onPromptDispatchStart: (ctx: TContext, chatId?: number) => void;
onPromptDispatchFailure: (ctx: TContext, message: string) => void;
}
export function createTelegramPromptDispatchRuntime<TContext>(
deps: TelegramPromptDispatchRuntimeDeps<TContext>,
): TelegramPromptDispatchRuntime<TContext> {
const startTypingLoop = createTelegramTypingLoopStarter(deps);
return {
startTypingLoop,
...createTelegramPromptDispatchLifecycle({
lifecycle: deps.lifecycle,
typing: deps.typing,
startTypingLoop,
updateStatus: deps.updateStatus,
recordRuntimeEvent: deps.recordRuntimeEvent,
}),
};
}
export function createTelegramPromptDispatchLifecycle<TContext>(
deps: TelegramPromptDispatchLifecycleDeps<TContext>,
) {
return {
onPromptDispatchStart: (ctx: TContext, chatId?: number): void => {
deps.lifecycle.setDispatchPending(true);
deps.startTypingLoop(ctx, chatId);
updateTelegramRuntimeStatusSafely(deps.updateStatus, ctx, {
category: "dispatch",
phase: "status-update",
recordRuntimeEvent: deps.recordRuntimeEvent,
});
},
onPromptDispatchFailure: (ctx: TContext, message: string): void => {
deps.lifecycle.clearDispatchPending();
deps.typing.stop();
deps.recordRuntimeEvent?.("dispatch", new Error(message));
updateTelegramRuntimeStatusSafely(deps.updateStatus, ctx, {
error: `dispatch failed: ${message}`,
category: "dispatch",
phase: "status-update",
recordRuntimeEvent: deps.recordRuntimeEvent,
});
},
};
}