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

684 lines
19 KiB
TypeScript

/**
* Telegram activity lifecycle normalization and extension dispatch
* Zones: pi agent lifecycle, extension API, operational delivery
* Owns stable handler registration, evidence-based activity/source identity, assistant segment and reasoning normalization, executed-tool and compaction events, isolated non-blocking queues, shutdown fencing, diagnostics, and fresh delivery contexts; excludes Pi hook wiring, visibility policy, Telegram rendering, and consumer-extension behavior
*/
import {
deleteTelegramView,
editTelegramView,
sendTelegramChatAction,
sendTelegramView,
type TelegramDeliveryChatAction,
type TelegramDeliveryHandle,
type TelegramDeliveryResult,
type TelegramDeliveryScope,
type TelegramDeliveryTarget,
type TelegramDeliveryView,
} from "./delivery.ts";
const TELEGRAM_ACTIVITY_REGISTRY_KEY = "__piTelegramActivityRegistry__";
export type TelegramActivitySource =
| "telegram"
| "local"
| "autonomous"
| "unknown";
export type TelegramActivityTarget = Readonly<TelegramDeliveryTarget>;
export interface TelegramActivityEnvelope {
activityId: string;
sequence: number;
source: TelegramActivitySource;
target?: TelegramActivityTarget;
timestamp: number;
}
export type TelegramActivityPayload =
| { type: "agent-start" }
| {
type: "assistant-text-delta";
contentIndex: number;
delta: string;
}
| {
type: "assistant-segment";
contentIndex: number;
text: string;
placement: "intermediate" | "final" | "terminal-partial";
}
| {
type: "reasoning-delta";
contentIndex: number;
delta: string;
}
| {
type: "reasoning-end";
contentIndex: number;
text: string;
}
| {
type: "tool-start";
toolCallId: string;
toolName: string;
args: unknown;
}
| {
type: "tool-update";
toolCallId: string;
toolName: string;
update: unknown;
}
| {
type: "tool-end";
toolCallId: string;
toolName: string;
result: unknown;
isError: boolean;
}
| {
type: "compaction-start";
reason: "manual" | "threshold" | "overflow" | "unknown";
}
| {
type: "compaction-end";
reason: "manual" | "threshold" | "overflow" | "unknown";
}
| { type: "agent-end" }
| { type: "agent-settled" };
export type TelegramActivityEvent = TelegramActivityEnvelope &
TelegramActivityPayload;
export interface TelegramActivityContext {
activityId: string;
sequence: number;
source: TelegramActivitySource;
defaultScope: TelegramDeliveryScope;
send: (
view: TelegramDeliveryView,
options?: {
scope?: TelegramDeliveryScope;
replyToMessageId?: number;
},
) => Promise<TelegramDeliveryResult<TelegramDeliveryHandle>>;
edit: (
handle: TelegramDeliveryHandle,
view: TelegramDeliveryView,
) => Promise<TelegramDeliveryResult<TelegramDeliveryHandle>>;
delete: (
handle: TelegramDeliveryHandle,
) => Promise<TelegramDeliveryResult<void>>;
chatAction: (
action: TelegramDeliveryChatAction,
options?: { scope?: TelegramDeliveryScope },
) => Promise<TelegramDeliveryResult<void>>;
}
export interface TelegramActivityHandlerRegistration {
id: string;
order?: number;
handle: (
event: TelegramActivityEvent,
ctx: TelegramActivityContext,
) => void | Promise<void>;
}
interface RegisteredTelegramActivityHandler
extends TelegramActivityHandlerRegistration {
id: string;
order: number;
}
interface TelegramActivityRegistry {
handlers: Map<string, RegisteredTelegramActivityHandler>;
}
function getOrCreateTelegramActivityRegistry(): TelegramActivityRegistry {
const globals = globalThis as Record<string, unknown>;
const existing = globals[TELEGRAM_ACTIVITY_REGISTRY_KEY];
if (
existing &&
typeof existing === "object" &&
"handlers" in existing &&
existing.handlers instanceof Map
) {
return existing as TelegramActivityRegistry;
}
const registry: TelegramActivityRegistry = { handlers: new Map() };
globals[TELEGRAM_ACTIVITY_REGISTRY_KEY] = registry;
return registry;
}
export function registerTelegramActivityHandler(
registration: TelegramActivityHandlerRegistration,
): () => void {
const id = registration.id.trim();
if (!id) throw new Error("Telegram activity handler id is required.");
const registry = getOrCreateTelegramActivityRegistry();
if (registry.handlers.has(id)) {
throw new Error(`Telegram activity handler is already registered: ${id}`);
}
const handler: RegisteredTelegramActivityHandler = {
...registration,
id,
order: registration.order ?? 0,
};
registry.handlers.set(id, handler);
return () => {
if (registry.handlers.get(id) === handler) registry.handlers.delete(id);
};
}
/** @internal */
export function clearTelegramActivityHandlers(): void {
getOrCreateTelegramActivityRegistry().handlers.clear();
}
function getTelegramActivityHandlers(): RegisteredTelegramActivityHandler[] {
return Array.from(
getOrCreateTelegramActivityRegistry().handlers.values(),
).sort(function (left, right) {
return left.order - right.order || left.id.localeCompare(right.id);
});
}
function cloneActivityTarget(
target: TelegramActivityTarget,
): TelegramActivityTarget {
return Object.freeze(
target.threadId === undefined
? { chatId: target.chatId }
: { chatId: target.chatId, threadId: target.threadId },
);
}
function createTelegramActivityContext(
event: TelegramActivityEvent,
isActive: () => boolean,
): TelegramActivityContext {
const defaultScope: TelegramDeliveryScope = event.target
? { kind: "target", target: cloneActivityTarget(event.target) }
: event.source === "telegram"
? { kind: "active-turn" }
: { kind: "instance" };
const inactive = <T>(): Promise<TelegramDeliveryResult<T>> =>
Promise.resolve({
ok: false,
reason: "runtime-unavailable",
message: "Telegram activity context belongs to an inactive session.",
});
return {
activityId: event.activityId,
sequence: event.sequence,
source: event.source,
defaultScope,
send(view, options) {
if (!isActive()) return inactive();
return sendTelegramView(view, {
scope: options?.scope ?? defaultScope,
replyToMessageId: options?.replyToMessageId,
});
},
edit(handle, view) {
return isActive() ? editTelegramView(handle, view) : inactive();
},
delete(handle) {
return isActive() ? deleteTelegramView(handle) : inactive();
},
chatAction(action, options) {
if (!isActive()) return inactive();
return sendTelegramChatAction(action, {
scope: options?.scope ?? defaultScope,
});
},
};
}
interface TelegramActivityHandlerQueue {
registration: RegisteredTelegramActivityHandler;
events: TelegramActivityEvent[];
running: boolean;
active: boolean;
}
function canCoalesceActivityEvents(
previous: TelegramActivityEvent,
next: TelegramActivityEvent,
): boolean {
if (
previous.activityId !== next.activityId ||
previous.type !== next.type
) {
return false;
}
if (
previous.type === "assistant-text-delta" &&
next.type === "assistant-text-delta"
) {
return previous.contentIndex === next.contentIndex;
}
if (
previous.type === "reasoning-delta" &&
next.type === "reasoning-delta"
) {
return previous.contentIndex === next.contentIndex;
}
if (previous.type === "tool-update" && next.type === "tool-update") {
return previous.toolCallId === next.toolCallId;
}
return false;
}
function coalesceActivityEvents(
previous: TelegramActivityEvent,
next: TelegramActivityEvent,
): TelegramActivityEvent {
if (
previous.type === "assistant-text-delta" &&
next.type === "assistant-text-delta"
) {
return { ...next, delta: previous.delta + next.delta };
}
if (
previous.type === "reasoning-delta" &&
next.type === "reasoning-delta"
) {
return { ...next, delta: previous.delta + next.delta };
}
return next;
}
/** @internal */
export interface TelegramActivityDispatcher {
dispatch: (event: TelegramActivityEvent) => void;
stop: () => void;
}
/** @internal */
export function createTelegramActivityDispatcher(deps: {
recordFailure?: (
handlerId: string,
event: TelegramActivityEvent,
error: unknown,
) => void;
} = {}): TelegramActivityDispatcher {
const queues = new Map<string, TelegramActivityHandlerQueue>();
let stopped = false;
const drain = async (queue: TelegramActivityHandlerQueue): Promise<void> => {
if (queue.running || !queue.active) return;
queue.running = true;
try {
while (queue.active) {
const event = queue.events.shift();
if (!event) break;
if (
getOrCreateTelegramActivityRegistry().handlers.get(
queue.registration.id,
) !== queue.registration
) {
queue.active = false;
queue.events = [];
break;
}
try {
await queue.registration.handle(
event,
createTelegramActivityContext(event, () =>
queue.active &&
!stopped &&
getOrCreateTelegramActivityRegistry().handlers.get(
queue.registration.id,
) === queue.registration,
),
);
} catch (error) {
deps.recordFailure?.(queue.registration.id, event, error);
}
}
} finally {
queue.running = false;
}
};
return {
dispatch(event) {
if (stopped) return;
for (const registration of getTelegramActivityHandlers()) {
let queue = queues.get(registration.id);
if (!queue || queue.registration !== registration) {
queue = {
registration,
events: [],
running: false,
active: true,
};
queues.set(registration.id, queue);
}
const previous = queue.events.at(-1);
if (previous && canCoalesceActivityEvents(previous, event)) {
queue.events[queue.events.length - 1] = coalesceActivityEvents(
previous,
event,
);
} else {
queue.events.push(event);
}
queueMicrotask(function () {
void drain(queue!);
});
}
},
stop() {
stopped = true;
for (const queue of queues.values()) {
queue.active = false;
queue.events = [];
}
queues.clear();
},
};
}
/** @internal */
export function createTelegramActivityBridgeRuntime(deps: {
generation: string;
recordFailure?: (
handlerId: string,
event: TelegramActivityEvent,
error: unknown,
) => void;
now?: () => number;
}): TelegramActivityRuntime {
let generationSequence = 0;
let runtime: TelegramActivityRuntime | undefined;
const getRuntime = (): TelegramActivityRuntime | undefined => runtime;
return {
onSessionStart() {
runtime?.onSessionShutdown();
runtime = createTelegramActivityRuntime({
generation: `${deps.generation}:${++generationSequence}`,
dispatcher: createTelegramActivityDispatcher({
recordFailure: deps.recordFailure,
}),
now: deps.now,
});
},
recordInputSource(source) {
getRuntime()?.recordInputSource(source);
},
onAgentStart(target) {
getRuntime()?.onAgentStart(target);
},
onAssistantEvent(event) {
getRuntime()?.onAssistantEvent(event);
},
onToolStart(event) {
getRuntime()?.onToolStart(event);
},
onToolUpdate(event) {
getRuntime()?.onToolUpdate(event);
},
onToolEnd(event) {
getRuntime()?.onToolEnd(event);
},
onCompactionStart(reason) {
getRuntime()?.onCompactionStart(reason);
},
onCompactionEnd(reason) {
getRuntime()?.onCompactionEnd(reason);
},
onCompactionAbandoned() {
getRuntime()?.onCompactionAbandoned();
},
onAgentEnd() {
getRuntime()?.onAgentEnd();
},
onAgentSettled() {
getRuntime()?.onAgentSettled();
},
onSessionShutdown() {
runtime?.onSessionShutdown();
runtime = undefined;
},
};
}
export type TelegramActivityInputSource =
| "interactive"
| "rpc"
| "extension"
| "unknown";
export type TelegramAssistantStreamEvent =
| { type: "start" }
| { type: "text_start"; contentIndex: number }
| { type: "text_delta"; contentIndex: number; delta: string }
| { type: "text_end"; contentIndex: number; content: string }
| { type: "thinking_delta"; contentIndex: number; delta: string }
| { type: "thinking_end"; contentIndex: number; content: string }
| { type: "thinking_start"; contentIndex: number }
| { type: "toolcall_start"; contentIndex: number }
| { type: "toolcall_delta"; contentIndex: number; delta: string }
| { type: "toolcall_end"; contentIndex: number }
| { type: "done" }
| { type: "error" };
/** @internal */
export interface TelegramActivityRuntime {
onSessionStart?: () => void;
recordInputSource: (source: TelegramActivityInputSource) => void;
onAgentStart: (activeTelegramTarget?: TelegramActivityTarget) => void;
onAssistantEvent: (event: TelegramAssistantStreamEvent) => void;
onToolStart: (event: {
toolCallId: string;
toolName: string;
args: unknown;
}) => void;
onToolUpdate: (event: {
toolCallId: string;
toolName: string;
update: unknown;
}) => void;
onToolEnd: (event: {
toolCallId: string;
toolName: string;
result: unknown;
isError: boolean;
}) => void;
onCompactionStart: (
reason: "manual" | "threshold" | "overflow" | "unknown",
) => void;
onCompactionEnd: (
reason: "manual" | "threshold" | "overflow" | "unknown",
) => void;
onCompactionAbandoned: () => void;
onAgentEnd: () => void;
onAgentSettled: () => void;
onSessionShutdown: () => void;
}
interface PendingAssistantSegment {
contentIndex: number;
text: string;
}
/** @internal */
export function createTelegramActivityRuntime(deps: {
generation: string;
dispatcher: TelegramActivityDispatcher;
now?: () => number;
}): TelegramActivityRuntime {
const now = deps.now ?? Date.now;
let nextActivityNumber = 0;
let activityId: string | undefined;
let activitySource: TelegramActivitySource = "unknown";
let activityTarget: TelegramActivityTarget | undefined;
let sequence = 0;
let pendingInputSource: TelegramActivityInputSource = "unknown";
let pendingAssistantSegment: PendingAssistantSegment | undefined;
let compactionInProgress = false;
let compactionOwnedActivity = false;
const ensureActivity = (
activeTelegramTarget?: TelegramActivityTarget,
): string => {
if (activityId) return activityId;
nextActivityNumber += 1;
activityId = `${deps.generation}:${nextActivityNumber}`;
activitySource = activeTelegramTarget
? "telegram"
: pendingInputSource === "interactive" || pendingInputSource === "rpc"
? "local"
: pendingInputSource === "extension"
? "autonomous"
: "unknown";
activityTarget = activeTelegramTarget
? cloneActivityTarget(activeTelegramTarget)
: undefined;
sequence = 0;
pendingInputSource = "unknown";
return activityId;
};
const emit = (event: TelegramActivityPayload): void => {
const currentActivityId = ensureActivity();
sequence += 1;
deps.dispatcher.dispatch({
...event,
activityId: currentActivityId,
sequence,
source: activitySource,
...(activityTarget ? { target: activityTarget } : {}),
timestamp: now(),
} as TelegramActivityEvent);
};
const flushPendingSegment = (
placement: "intermediate" | "final" | "terminal-partial",
): void => {
const segment = pendingAssistantSegment;
pendingAssistantSegment = undefined;
if (!segment?.text.trim()) return;
emit({
type: "assistant-segment",
contentIndex: segment.contentIndex,
text: segment.text,
placement,
});
};
const clearActivity = (): void => {
activityId = undefined;
activitySource = "unknown";
activityTarget = undefined;
sequence = 0;
pendingAssistantSegment = undefined;
compactionInProgress = false;
compactionOwnedActivity = false;
};
const abandonCompaction = (): void => {
if (!compactionInProgress) return;
const shouldClearActivity = compactionOwnedActivity;
compactionInProgress = false;
compactionOwnedActivity = false;
if (shouldClearActivity) clearActivity();
};
return {
recordInputSource(source) {
pendingInputSource = source;
},
onAgentStart(activeTelegramTarget) {
abandonCompaction();
ensureActivity(activeTelegramTarget);
emit({ type: "agent-start" });
},
onAssistantEvent(event) {
if (event.type === "text_start") {
flushPendingSegment("intermediate");
return;
}
if (event.type === "text_delta") {
if (!event.delta) return;
emit({
type: "assistant-text-delta",
contentIndex: event.contentIndex,
delta: event.delta,
});
return;
}
if (event.type === "text_end") {
pendingAssistantSegment = {
contentIndex: event.contentIndex,
text: event.content,
};
return;
}
if (event.type === "thinking_delta") {
if (!event.delta) return;
emit({
type: "reasoning-delta",
contentIndex: event.contentIndex,
delta: event.delta,
});
return;
}
if (event.type === "thinking_end") {
if (!event.content.trim()) return;
emit({
type: "reasoning-end",
contentIndex: event.contentIndex,
text: event.content,
});
return;
}
if (event.type === "toolcall_start") {
flushPendingSegment("intermediate");
return;
}
if (event.type === "done") {
flushPendingSegment("final");
return;
}
if (event.type === "error") flushPendingSegment("terminal-partial");
},
onToolStart(event) {
emit({ type: "tool-start", ...event });
},
onToolUpdate(event) {
emit({ type: "tool-update", ...event });
},
onToolEnd(event) {
emit({ type: "tool-end", ...event });
},
onCompactionStart(reason) {
abandonCompaction();
compactionOwnedActivity = !activityId;
compactionInProgress = true;
ensureActivity();
emit({ type: "compaction-start", reason });
},
onCompactionEnd(reason) {
if (!compactionInProgress || !activityId) return;
const shouldClearActivity = compactionOwnedActivity;
compactionInProgress = false;
compactionOwnedActivity = false;
emit({ type: "compaction-end", reason });
if (shouldClearActivity) clearActivity();
},
onCompactionAbandoned() {
abandonCompaction();
},
onAgentEnd() {
if (activityId) emit({ type: "agent-end" });
},
onAgentSettled() {
if (!activityId) return;
flushPendingSegment("terminal-partial");
emit({ type: "agent-settled" });
clearActivity();
},
onSessionShutdown() {
pendingInputSource = "unknown";
clearActivity();
deps.dispatcher.stop();
},
};
}