Files
oh-my-pi-telegram/lib/bus.ts
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

1492 lines
45 KiB
TypeScript

/**
* Telegram multi-instance bus protocol and IPC helpers
* Zones: multi-instance bus, local IPC contract, live instance routing
* Owns serializable bus envelopes, socket/auth helpers, local IPC client/server primitives,
* cross-instance forwarding helpers, and the live follower registry model.
*/
import { randomBytes } from "node:crypto";
import {
chmodSync,
existsSync,
lstatSync,
mkdirSync,
readFileSync,
readlinkSync,
renameSync,
symlinkSync,
unlinkSync,
} from "node:fs";
import {
createConnection,
createServer,
type Server,
type Socket,
} from "node:net";
import { platform as getPlatform } from "node:os";
import { basename, dirname, join } from "node:path";
import {
classifyTelegramBusTransportError,
createTelegramBusTransportTimeoutError,
delayTelegramBusTransportRetry,
getTelegramBusEndpointDiagnostics,
getTelegramBusFollowerEndpoint,
getTelegramBusLeaderEndpoint,
getTelegramBusTransportRetryPolicy,
isTelegramBusPipePath,
isRetryableTelegramBusTransportError,
probeTelegramBusEndpoint,
type TelegramBusTransportEventRecorder,
type TelegramBusTransportRetryPolicy,
} from "./bus-transport.ts";
import type { TelegramTarget } from "./target.ts";
import { resolveAgentDir } from "./paths.ts";
export type TelegramBusRole = "leader" | "follower";
export interface TelegramBusProcessRuntime {
instanceId: string;
manualFollowerOwnerId: string;
getLeaderSocketPath: () => string;
getFollowerSocketPath: () => string;
}
export function getTelegramProcessBirthIdentity(
pid: number,
fallbackGeneration: number | string,
): string {
if (pid > 0) {
try {
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
const closeParen = stat.lastIndexOf(")");
const fields = stat
.slice(closeParen + 2)
.trim()
.split(/\s+/u);
const startTicks = fields[19];
if (startTicks) return `${pid}:start:${startTicks}`;
} catch {
/* non-Linux or inaccessible process metadata */
}
}
return `${pid}:generation:${fallbackGeneration}`;
}
export function createTelegramBusProcessRuntime(input: {
getActiveProfileName: () => string | undefined;
pid: number;
parentPid: number;
parentProcessIdentity?: string;
createdAtMs: number;
}): TelegramBusProcessRuntime {
const instanceId = `${input.pid}:${input.createdAtMs}`;
const ownerPid = input.parentPid || input.pid;
const manualFollowerOwnerId =
input.parentProcessIdentity ??
getTelegramProcessBirthIdentity(ownerPid, input.createdAtMs);
return {
instanceId,
manualFollowerOwnerId,
getLeaderSocketPath: () =>
getTelegramBusSocketPath(
undefined,
undefined,
input.getActiveProfileName(),
),
getFollowerSocketPath: () =>
getTelegramBusFollowerSocketPath(
instanceId,
undefined,
undefined,
input.getActiveProfileName(),
),
};
}
export function createTelegramBusAuthSecret(): string {
return randomBytes(32).toString("base64url");
}
export function getTelegramBusSocketPath(
agentDir = resolveAgentDir(),
platform = getPlatform(),
profileName?: string,
): string {
return getTelegramBusLeaderEndpoint({ agentDir, platform, profileName });
}
export function getTelegramBusFollowerSocketPath(
instanceId: string,
agentDir = resolveAgentDir(),
platform = getPlatform(),
profileName?: string,
): string {
return getTelegramBusFollowerEndpoint({
agentDir,
platform,
instanceId,
profileName,
});
}
export interface TelegramBusInstanceRegistration {
instanceId: string;
profileKey?: string;
threadName?: string;
slot?: string;
cwd?: string;
pid?: number;
target?: TelegramTarget;
busSocketPath?: string;
registrationGeneration?: string;
connectedAtMs: number;
}
export interface TelegramBusFollowerView extends TelegramBusInstanceRegistration {
lastHeartbeatMs: number;
}
export function getTelegramFollowerTargetOwnership(input: {
target: TelegramTarget;
followers: readonly TelegramBusFollowerView[];
activeThreadRecords?: readonly {
status?: string;
instanceId?: string;
profileKey?: string;
owner?: { kind?: string };
target: TelegramTarget;
}[];
currentInstanceId?: string;
}): { instanceId: string } | undefined {
const liveFollower = input.followers.find((follower) => {
return (
follower.target?.chatId === input.target.chatId &&
follower.target.threadId === input.target.threadId
);
});
if (liveFollower) return { instanceId: liveFollower.instanceId };
const record = input.activeThreadRecords?.find((candidate) => {
const isFollowerRecord = candidate.owner?.kind
? candidate.owner.kind === "manual-follower"
: candidate.profileKey?.startsWith("manual:") === true;
return (
isFollowerRecord &&
candidate.status === "active" &&
candidate.instanceId &&
candidate.instanceId !== input.currentInstanceId &&
candidate.target.chatId === input.target.chatId &&
candidate.target.threadId === input.target.threadId
);
});
return record?.instanceId ? { instanceId: record.instanceId } : undefined;
}
const TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD = "__piTelegramAggregateDelivery";
export function markTelegramBusAggregateDelivery<
T extends Record<string, unknown>,
>(body: T): T {
return {
...body,
[TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD]: true,
};
}
export function isTelegramBusAggregateDelivery(body: unknown): boolean {
return Boolean(
body &&
typeof body === "object" &&
!Array.isArray(body) &&
(body as Record<string, unknown>)[TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD] ===
true,
);
}
export function stripTelegramBusApiMetadata<T extends Record<string, unknown>>(
body: T,
): T {
if (!(TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD in body)) return body;
const clean = { ...body };
delete clean[TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD];
return clean;
}
export function isTelegramFollowerApiCallAllowed(input: {
follower: TelegramBusFollowerView;
method: string;
args: unknown[];
isMessageOwned?: (chatId: number, messageId: number) => boolean;
}): boolean {
const allowedCallMethods = new Set([
"answerCallbackQuery",
"answerGuestQuery",
"closeForumTopic",
"deleteForumTopic",
"deleteMessage",
"editForumTopic",
"editMessageText",
"sendChatAction",
"sendMessage",
"sendMessageDraft",
"sendRichMessage",
"sendRichMessageDraft",
]);
const allowedMultipartMethods = new Set([
"sendAudio",
"sendDocument",
"sendMediaGroup",
"sendPhoto",
"sendVoice",
]);
const target = input.follower.target;
const matchesId = (value: unknown, expected: number): boolean =>
value === expected || value === String(expected);
const isTargetScoped = (body: unknown): boolean => {
if (!target) return false;
if (!body || typeof body !== "object" || Array.isArray(body)) return false;
const record = body as Record<string, unknown>;
if (!matchesId(record.chat_id, target.chatId)) return false;
if (target.threadId === undefined) return true;
return matchesId(record.message_thread_id, target.threadId);
};
const isTargetChatScoped = (body: unknown): boolean => {
if (!target) return false;
if (!body || typeof body !== "object" || Array.isArray(body)) return false;
const record = body as Record<string, unknown>;
return matchesId(record.chat_id, target.chatId);
};
const isTargetMessageScoped = (body: unknown): boolean => {
if (!isTargetChatScoped(body)) return false;
const messageId = (body as Record<string, unknown>).message_id;
const parsedMessageId =
typeof messageId === "number" ? messageId : Number(messageId);
return (
Number.isInteger(parsedMessageId) && matchesId(messageId, parsedMessageId)
);
};
const isBotCommandRegistration = (body: unknown): boolean => {
if (!body || typeof body !== "object" || Array.isArray(body)) return false;
const commands = (body as Record<string, unknown>).commands;
return (
Array.isArray(commands) &&
commands.every(
(command) =>
command &&
typeof command === "object" &&
!Array.isArray(command) &&
typeof (command as Record<string, unknown>).command === "string" &&
typeof (command as Record<string, unknown>).description === "string",
)
);
};
if (input.method === "downloadFile") return true;
if (input.method === "call") {
const apiMethod = input.args[0];
if (typeof apiMethod !== "string") return false;
if (
apiMethod === "answerCallbackQuery" ||
apiMethod === "answerGuestQuery"
) {
return true;
}
if (apiMethod === "getMe") return true;
if (apiMethod === "setMyCommands")
return isBotCommandRegistration(input.args[1]);
if (apiMethod === "sendChatAction")
return isTargetChatScoped(input.args[1]);
if (
apiMethod === "sendMessage" &&
isTelegramBusAggregateDelivery(input.args[1])
) {
const body = input.args[1] as Record<string, unknown>;
return body.message_thread_id === undefined && isTargetChatScoped(body);
}
if (apiMethod === "deleteMessage" || apiMethod === "editMessageText") {
if (!isTargetMessageScoped(input.args[1])) return false;
const body = input.args[1] as Record<string, unknown>;
const messageId =
typeof body.message_id === "number"
? body.message_id
: Number(body.message_id);
return input.isMessageOwned?.(target!.chatId, messageId) === true;
}
return allowedCallMethods.has(apiMethod) && isTargetScoped(input.args[1]);
}
if (input.method === "callMultipart") {
const apiMethod = input.args[0];
return (
typeof apiMethod === "string" &&
allowedMultipartMethods.has(apiMethod) &&
isTargetScoped(input.args[1])
);
}
return false;
}
export interface TelegramFollowerApiCallAuthorizationInput {
follower: TelegramBusFollowerView;
method: string;
args: unknown[];
}
export function createTelegramFollowerApiCallAuthorizer(deps: {
isMessageOwned(input: {
chatId: number;
messageId: number;
follower: TelegramBusFollowerView;
}): boolean;
}): (input: TelegramFollowerApiCallAuthorizationInput) => boolean {
return (input) =>
isTelegramFollowerApiCallAllowed({
...input,
isMessageOwned(chatId, messageId) {
return deps.isMessageOwned({
chatId,
messageId,
follower: input.follower,
});
},
});
}
export type TelegramBusEnvelope = (
| {
kind: "follower.register";
requestId: string;
registration: TelegramBusInstanceRegistration;
}
| {
kind: "follower.heartbeat";
requestId: string;
instanceId: string;
registrationGeneration?: string;
sentAtMs: number;
}
| {
kind: "leader.forwardCallback";
requestId: string;
recipientInstanceId: string;
recipientRegistrationGeneration?: string;
query: unknown;
sentAtMs: number;
}
| {
kind: "leader.forwardReaction";
requestId: string;
recipientInstanceId: string;
recipientRegistrationGeneration?: string;
reactionUpdate: unknown;
sentAtMs: number;
}
| {
kind: "leader.forwardMessage";
requestId: string;
recipientInstanceId: string;
recipientRegistrationGeneration?: string;
message: unknown;
sentAtMs: number;
}
| {
kind: "leader.forwardEditedMessage";
requestId: string;
recipientInstanceId: string;
recipientRegistrationGeneration?: string;
message: unknown;
sentAtMs: number;
}
| {
kind: "leader.replaceFollowerTarget";
requestId: string;
recipientInstanceId: string;
recipientRegistrationGeneration?: string;
target: TelegramTarget & { threadId: number };
oldTarget?: TelegramTarget & { threadId: number };
reason: "thread-restore";
sentAtMs: number;
}
| {
kind: "follower.callApi";
requestId: string;
instanceId: string;
registrationGeneration?: string;
method: string;
args: unknown[];
sentAtMs: number;
}
| {
kind: "bus.ack";
requestId: string;
ok: boolean;
message?: string;
result?: unknown;
error?: {
code: "commit-unknown" | "request-id-collision" | "ledger-overloaded";
method?: string;
};
}
) & { auth?: string };
export function createTelegramBusRequestId(input: {
instanceId: string;
sequence: number;
}): string {
return `${input.instanceId}:${input.sequence}`;
}
export function createTelegramBusRequestIdFactory(
instanceId: string,
): () => string {
let sequence = 0;
return () => {
sequence += 1;
return createTelegramBusRequestId({ instanceId, sequence });
};
}
export function encodeTelegramBusEnvelope(
envelope: TelegramBusEnvelope,
): string {
return `${JSON.stringify(envelope)}\n`;
}
export function parseTelegramBusEnvelope(
line: string,
): TelegramBusEnvelope | undefined {
let value: unknown;
try {
value = JSON.parse(line);
} catch {
return undefined;
}
if (!isRecord(value)) return undefined;
const kind = value.kind;
const requestId = value.requestId;
if (typeof kind !== "string" || typeof requestId !== "string") {
return undefined;
}
let envelope: TelegramBusEnvelope | undefined;
switch (kind) {
case "follower.register":
envelope = parseRegisterEnvelope(value, requestId);
break;
case "follower.heartbeat":
envelope = parseHeartbeatEnvelope(value, requestId);
break;
case "leader.forwardCallback":
envelope = parseForwardCallbackEnvelope(value, requestId);
break;
case "leader.forwardReaction":
envelope = parseForwardReactionEnvelope(value, requestId);
break;
case "leader.forwardMessage":
envelope = parseForwardMessageEnvelope(
value,
requestId,
"leader.forwardMessage",
);
break;
case "leader.forwardEditedMessage":
envelope = parseForwardMessageEnvelope(
value,
requestId,
"leader.forwardEditedMessage",
);
break;
case "leader.replaceFollowerTarget":
envelope = parseReplaceFollowerTargetEnvelope(value, requestId);
break;
case "follower.callApi":
envelope = parseCallApiEnvelope(value, requestId);
break;
case "bus.ack":
envelope = parseAckEnvelope(value, requestId);
break;
default:
return undefined;
}
const auth = value.auth;
if (envelope && typeof auth === "string") envelope.auth = auth;
return envelope;
}
export interface TelegramBusLocalServer {
start: () => Promise<void>;
stop: () => Promise<void>;
ensureEndpoint: () => Promise<boolean>;
}
export type TelegramBusSocketPathSource = string | (() => string);
export function resolveTelegramBusSocketPath(
source: TelegramBusSocketPathSource,
): string {
return typeof source === "function" ? source() : source;
}
export interface TelegramBusLocalServerDeps {
socketPath: TelegramBusSocketPathSource;
handleEnvelope: (
envelope: TelegramBusEnvelope,
) =>
Promise<TelegramBusEnvelope | undefined> | TelegramBusEnvelope | undefined;
recordTransportEvent?: TelegramBusTransportEventRecorder;
beforeEndpointPublication?: () => Promise<void> | void;
commitEndpointPublication?: (commit: () => void) => boolean;
requestLedgerMaxEntries?: number;
shouldDropResponse?: (
request: TelegramBusEnvelope,
response: TelegramBusEnvelope,
) => boolean;
}
export interface TelegramBusLocalClientOptions {
socketPath: string;
envelope: TelegramBusEnvelope;
timeoutMs?: number;
retry?: TelegramBusTransportRetryPolicy;
recordTransportEvent?: TelegramBusTransportEventRecorder;
}
export interface TelegramBusForeignOwnedForwarderDeps {
socketPath: TelegramBusSocketPathSource;
createRequestId: () => string;
getNowMs?: () => number;
timeoutMs?: number;
getAuthSecret?: () => string | undefined;
}
export function createTelegramBusForeignOwnedUpdateForwarder<
TContext,
TReactionUpdate,
TCallbackQuery,
TMessage = unknown,
>(
deps: TelegramBusForeignOwnedForwarderDeps,
): {
forwardCallback: (input: {
query: TCallbackQuery;
ownership: { instanceId: string; ownerGeneration?: string };
ctx: TContext;
}) => Promise<boolean>;
forwardReaction: (input: {
reactionUpdate: TReactionUpdate;
ownership: { instanceId: string; ownerGeneration?: string };
ctx: TContext;
}) => Promise<boolean>;
forwardMessage: (input: {
message: TMessage;
ownership: { instanceId: string; ownerGeneration?: string };
ctx: TContext;
}) => Promise<boolean>;
forwardEditedMessage: (input: {
message: TMessage;
ownership: { instanceId: string; ownerGeneration?: string };
ctx: TContext;
}) => Promise<boolean>;
} {
const getNowMs = deps.getNowMs ?? Date.now;
const send = async (envelope: TelegramBusEnvelope): Promise<boolean> => {
if (deps.getAuthSecret) envelope.auth = deps.getAuthSecret();
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
const response = await sendTelegramBusLocalEnvelope({
socketPath,
envelope,
timeoutMs: deps.timeoutMs,
retry: getTelegramBusTransportRetryPolicy({
endpoint: socketPath,
operation: "operation",
}),
});
return response?.kind === "bus.ack" && response.ok;
};
return {
forwardCallback: ({ query, ownership }) =>
send({
kind: "leader.forwardCallback",
requestId: deps.createRequestId(),
recipientInstanceId: ownership.instanceId,
...(ownership.ownerGeneration
? { recipientRegistrationGeneration: ownership.ownerGeneration }
: {}),
query,
sentAtMs: getNowMs(),
}),
forwardReaction: ({ reactionUpdate, ownership }) =>
send({
kind: "leader.forwardReaction",
requestId: deps.createRequestId(),
recipientInstanceId: ownership.instanceId,
...(ownership.ownerGeneration
? { recipientRegistrationGeneration: ownership.ownerGeneration }
: {}),
reactionUpdate,
sentAtMs: getNowMs(),
}),
forwardMessage: ({ message, ownership }) =>
send({
kind: "leader.forwardMessage",
requestId: deps.createRequestId(),
recipientInstanceId: ownership.instanceId,
...(ownership.ownerGeneration
? { recipientRegistrationGeneration: ownership.ownerGeneration }
: {}),
message,
sentAtMs: getNowMs(),
}),
forwardEditedMessage: ({ message, ownership }) =>
send({
kind: "leader.forwardEditedMessage",
requestId: deps.createRequestId(),
recipientInstanceId: ownership.instanceId,
...(ownership.ownerGeneration
? { recipientRegistrationGeneration: ownership.ownerGeneration }
: {}),
message,
sentAtMs: getNowMs(),
}),
};
}
export interface TelegramBusFollowerThreadRestoreHandlerDeps {
followerRegistry: Pick<TelegramBusFollowerRegistry, "get" | "register">;
followerTargetController: ReturnType<
typeof createTelegramBusFollowerTargetController
>;
onRestored?: () => void;
}
export function listTelegramBusLiveThreadTargets(input: {
leaderTarget?: TelegramTarget;
followers: readonly TelegramBusFollowerView[];
}): TelegramTarget[] {
const targets: TelegramTarget[] = [];
if (input.leaderTarget?.threadId !== undefined) {
targets.push(input.leaderTarget);
}
for (const follower of input.followers) {
if (follower.target?.threadId !== undefined) targets.push(follower.target);
}
return targets;
}
export function createTelegramBusFollowerTargetController(
deps: TelegramBusForeignOwnedForwarderDeps,
): {
replaceTarget: (input: {
follower: TelegramBusFollowerView;
target: TelegramTarget & { threadId: number };
oldTarget?: TelegramTarget & { threadId: number };
reason: "thread-restore";
}) => Promise<boolean>;
} {
const getNowMs = deps.getNowMs ?? Date.now;
return {
async replaceTarget({ follower, target, oldTarget, reason }) {
if (!follower.busSocketPath) return false;
const envelope: TelegramBusEnvelope = {
kind: "leader.replaceFollowerTarget",
requestId: deps.createRequestId(),
recipientInstanceId: follower.instanceId,
...(follower.registrationGeneration
? {
recipientRegistrationGeneration: follower.registrationGeneration,
}
: {}),
target,
...(oldTarget ? { oldTarget } : {}),
reason,
sentAtMs: getNowMs(),
};
if (deps.getAuthSecret) envelope.auth = deps.getAuthSecret();
const response = await sendTelegramBusLocalEnvelope({
socketPath: follower.busSocketPath,
envelope,
timeoutMs: deps.timeoutMs,
retry: getTelegramBusTransportRetryPolicy({
endpoint: follower.busSocketPath,
operation: "operation",
}),
});
return response?.kind === "bus.ack" && response.ok;
},
};
}
export function createTelegramBusFollowerThreadRestoreHandler(
deps: TelegramBusFollowerThreadRestoreHandlerDeps,
): (input: {
record: { instanceId?: string };
target: TelegramTarget & { threadId: number };
oldTarget?: TelegramTarget & { threadId: number };
}) => Promise<boolean> {
return async ({ record, target, oldTarget }) => {
if (!record.instanceId) return false;
const follower = deps.followerRegistry.get(record.instanceId);
if (!follower) return false;
const replaced = await deps.followerTargetController.replaceTarget({
follower,
target,
oldTarget,
reason: "thread-restore",
});
if (!replaced) return false;
deps.followerRegistry.register({
...follower,
target,
connectedAtMs: follower.connectedAtMs,
});
deps.onRestored?.();
return true;
};
}
export function isTelegramBusEnvelopeAuthorized(
envelope: TelegramBusEnvelope,
secret: string | undefined,
): boolean {
return !secret || envelope.auth === secret;
}
export function createUnauthorizedBusAck(
requestId: string,
): TelegramBusEnvelope {
return {
kind: "bus.ack",
requestId,
ok: false,
message: "Unauthorized Telegram bus envelope.",
};
}
export function createTelegramBusLocalServer(
deps: TelegramBusLocalServerDeps,
): TelegramBusLocalServer {
type LedgerEntry = {
fingerprint: string;
settled: boolean;
result: Promise<TelegramBusEnvelope | undefined>;
};
const requestLedger = new Map<string, LedgerEntry>();
const requestLedgerMaxEntries = Math.max(
1,
deps.requestLedgerMaxEntries ?? 4096,
);
const getRequestLedgerKey = (envelope: TelegramBusEnvelope): string => {
const identity =
envelope.kind === "follower.register"
? envelope.registration.instanceId
: "instanceId" in envelope
? envelope.instanceId
: "recipientInstanceId" in envelope
? envelope.recipientInstanceId
: "ack";
return `${envelope.auth ?? ""}:${identity}:${envelope.requestId}`;
};
const handleEnvelopeOnce = (
envelope: TelegramBusEnvelope,
): Promise<TelegramBusEnvelope | undefined> => {
const key = getRequestLedgerKey(envelope);
const fingerprint = JSON.stringify(envelope);
const existing = requestLedger.get(key);
if (existing) {
if (existing.fingerprint === fingerprint) return existing.result;
return Promise.resolve({
kind: "bus.ack",
requestId: envelope.requestId,
ok: false,
message: "Telegram bus request id was reused with a different payload.",
error: { code: "request-id-collision" },
});
}
if (requestLedger.size >= requestLedgerMaxEntries) {
const settledKey = Array.from(requestLedger.entries()).find(
([, entry]) => entry.settled,
)?.[0];
if (settledKey) requestLedger.delete(settledKey);
}
if (requestLedger.size >= requestLedgerMaxEntries) {
return Promise.resolve({
kind: "bus.ack",
requestId: envelope.requestId,
ok: false,
message: "Telegram bus request ledger is full.",
error: { code: "ledger-overloaded" },
});
}
const entry: LedgerEntry = {
fingerprint,
settled: false,
result: Promise.resolve().then(() => deps.handleEnvelope(envelope)),
};
requestLedger.set(key, entry);
void entry.result.then(
() => {
entry.settled = true;
},
() => {
entry.settled = true;
},
);
return entry.result;
};
let server: Server | undefined;
let activeSocketPath: string | undefined;
let activeListenPath: string | undefined;
let endpointRecovery: Promise<boolean> | undefined;
let stopGeneration = 0;
const sockets = new Set<Socket>();
const closeSocket = (socket: Socket) => {
sockets.delete(socket);
socket.destroy();
};
const runtime: TelegramBusLocalServer = {
start: async () => {
if (server) return;
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
const usesWindowsPipe = isTelegramBusPipePath(socketPath);
const endpointGeneration = randomBytes(8).toString("hex");
const listenPath = usesWindowsPipe
? socketPath
: join(dirname(socketPath), `.pi-telegram-${endpointGeneration}.sock`);
activeSocketPath = socketPath;
activeListenPath = listenPath;
deps.recordTransportEvent?.(
"server-start",
getTelegramBusEndpointDiagnostics(socketPath),
);
if (!usesWindowsPipe) {
const socketDir = dirname(socketPath);
mkdirSync(socketDir, { recursive: true, mode: 0o700 });
chmodSync(socketDir, 0o700);
if (existsSync(listenPath)) unlinkSync(listenPath);
const legacyDeadlineMs = Date.now() + 2000;
while (true) {
let isLegacySocket = false;
try {
isLegacySocket = lstatSync(socketPath).isSocket();
} catch {
/* endpoint does not exist */
}
if (!isLegacySocket) break;
const probe = await probeTelegramBusEndpoint({
endpoint: socketPath,
timeoutMs: 50,
});
if (!probe.reachable) break;
if (Date.now() >= legacyDeadlineMs) {
throw new Error(
`Timed out waiting for legacy Telegram bus endpoint: ${socketPath}`,
);
}
await delayTelegramBusTransportRetry(25);
}
}
server = createServer((socket) => {
sockets.add(socket);
let buffer = "";
socket.setEncoding("utf8");
socket.on("data", (chunk) => {
buffer += chunk;
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
void handleTelegramBusSocketLine(
line,
socket,
handleEnvelopeOnce,
deps.recordTransportEvent,
socketPath,
deps.shouldDropResponse,
);
}
});
socket.on("close", () => sockets.delete(socket));
socket.on("error", (error) => {
deps.recordTransportEvent?.("server-socket-error", {
...getTelegramBusEndpointDiagnostics(socketPath),
...classifyTelegramBusTransportError(error),
});
closeSocket(socket);
});
});
try {
await new Promise<void>((resolve, reject) => {
server?.once("error", reject);
server?.listen(listenPath, resolve);
});
deps.recordTransportEvent?.(
"server-started",
getTelegramBusEndpointDiagnostics(socketPath),
);
} catch (error) {
server = undefined;
activeSocketPath = undefined;
activeListenPath = undefined;
deps.recordTransportEvent?.("server-start-failed", {
...getTelegramBusEndpointDiagnostics(socketPath),
...classifyTelegramBusTransportError(error),
});
throw error;
}
if (!usesWindowsPipe) {
chmodSync(listenPath, 0o600);
const linkPath = `${socketPath}.link.${endpointGeneration}`;
try {
symlinkSync(basename(listenPath), linkPath);
await deps.beforeEndpointPublication?.();
const committed = deps.commitEndpointPublication
? deps.commitEndpointPublication(() =>
renameSync(linkPath, socketPath),
)
: (renameSync(linkPath, socketPath), true);
if (!committed) {
throw new Error(
"Telegram bus endpoint publication lost transport ownership.",
);
}
} catch (error) {
try {
if (lstatSync(linkPath).isSymbolicLink()) unlinkSync(linkPath);
} catch {
/* no unpublished link to remove */
}
const failedServer = server;
server = undefined;
activeSocketPath = undefined;
activeListenPath = undefined;
if (failedServer) {
await new Promise<void>((resolve) =>
failedServer.close(() => resolve()),
);
}
throw error;
}
}
},
stop: async () => {
stopGeneration += 1;
requestLedger.clear();
const activeServer = server;
const socketPath = activeSocketPath;
const listenPath = activeListenPath;
server = undefined;
activeSocketPath = undefined;
activeListenPath = undefined;
for (const socket of sockets) closeSocket(socket);
if (activeServer) {
await new Promise<void>((resolve) =>
activeServer.close(() => resolve()),
);
}
if (socketPath && listenPath && !isTelegramBusPipePath(socketPath)) {
try {
if (
lstatSync(socketPath).isSymbolicLink() &&
readlinkSync(socketPath) === basename(listenPath)
) {
// Leave the generation link in place. Node removes only the unique
// listen path on close; a later generation atomically replaces this
// link, and existsSync() treats the stopped dangling link as missing.
}
} catch {
/* endpoint already moved or removed */
}
}
if (socketPath) {
deps.recordTransportEvent?.(
"server-stopped",
getTelegramBusEndpointDiagnostics(socketPath),
);
}
},
ensureEndpoint: async () => {
const socketPath = activeSocketPath;
if (
!server ||
!socketPath ||
isTelegramBusPipePath(socketPath) ||
existsSync(socketPath)
) {
return false;
}
if (endpointRecovery) return endpointRecovery;
endpointRecovery = (async () => {
deps.recordTransportEvent?.(
"server-endpoint-missing",
getTelegramBusEndpointDiagnostics(socketPath),
);
const recoveryStopGeneration = stopGeneration + 1;
await runtime.stop();
if (stopGeneration !== recoveryStopGeneration) return false;
await runtime.start();
if (stopGeneration !== recoveryStopGeneration) {
await runtime.stop();
return false;
}
deps.recordTransportEvent?.(
"server-endpoint-recovered",
getTelegramBusEndpointDiagnostics(socketPath),
);
return true;
})();
try {
return await endpointRecovery;
} finally {
endpointRecovery = undefined;
}
},
};
return runtime;
}
function getTelegramBusEnvelopeDiagnostics(
envelope: TelegramBusEnvelope,
): Record<string, unknown> {
return {
envelopeKind: envelope.kind,
requestId: envelope.requestId,
};
}
function sendTelegramBusLocalEnvelopeOnce(
options: TelegramBusLocalClientOptions,
): Promise<TelegramBusEnvelope | undefined> {
const timeoutMs = options.timeoutMs ?? 1000;
return new Promise((resolve, reject) => {
const socket = createConnection(options.socketPath);
let settled = false;
let buffer = "";
const settle = (callback: () => void) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
socket.destroy();
callback();
};
const timeout = setTimeout(() => {
settle(() =>
reject(
createTelegramBusTransportTimeoutError(
"Timed out waiting for Telegram bus response",
),
),
);
}, timeoutMs);
timeout.unref?.();
socket.setEncoding("utf8");
socket.once("connect", () => {
socket.write(encodeTelegramBusEnvelope(options.envelope));
});
socket.on("data", (chunk) => {
buffer += chunk;
const newlineIndex = buffer.indexOf("\n");
if (newlineIndex < 0) return;
const line = buffer.slice(0, newlineIndex);
settle(() => resolve(parseTelegramBusEnvelope(line)));
});
socket.once("error", (error) => settle(() => reject(error)));
socket.once("end", () => settle(() => resolve(undefined)));
});
}
export async function sendTelegramBusLocalEnvelope(
options: TelegramBusLocalClientOptions,
): Promise<TelegramBusEnvelope | undefined> {
const attempts = Math.max(1, options.retry?.attempts ?? 1);
const delayMs = Math.max(0, options.retry?.delayMs ?? 0);
for (let attempt = 1; ; attempt += 1) {
try {
return await sendTelegramBusLocalEnvelopeOnce(options);
} catch (error) {
const info = classifyTelegramBusTransportError(error);
options.recordTransportEvent?.("client-failed", {
...getTelegramBusEndpointDiagnostics(options.socketPath),
...getTelegramBusEnvelopeDiagnostics(options.envelope),
attempt,
attempts,
...info,
});
if (attempt >= attempts || !isRetryableTelegramBusTransportError(error)) {
throw error;
}
options.recordTransportEvent?.("client-retry", {
...getTelegramBusEndpointDiagnostics(options.socketPath),
...getTelegramBusEnvelopeDiagnostics(options.envelope),
attempt,
attempts,
delayMs,
...info,
});
await delayTelegramBusTransportRetry(delayMs);
}
}
}
export interface TelegramBusFollowerRegistry {
register: (
registration: TelegramBusInstanceRegistration,
) => TelegramBusFollowerView;
heartbeat: (
instanceId: string,
nowMs: number,
) => TelegramBusFollowerView | undefined;
get: (instanceId: string) => TelegramBusFollowerView | undefined;
getByTarget: (target: TelegramTarget) => TelegramBusFollowerView | undefined;
list: () => TelegramBusFollowerView[];
remove: (instanceId: string) => boolean;
clear: () => void;
pruneStale: (
nowMs: number,
staleAfterMs: number,
) => TelegramBusFollowerView[];
}
export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry {
const followers = new Map<string, TelegramBusFollowerView>();
const clone = (
follower: TelegramBusFollowerView,
): TelegramBusFollowerView => ({
...follower,
target: follower.target ? { ...follower.target } : undefined,
});
return {
register: (registration) => {
const existing = followers.get(registration.instanceId);
for (const [instanceId, follower] of followers.entries()) {
if (instanceId === registration.instanceId) continue;
const sameProfile =
registration.profileKey !== undefined &&
registration.profileKey === follower.profileKey;
const sameTarget =
registration.target !== undefined &&
follower.target?.chatId === registration.target.chatId &&
follower.target.threadId === registration.target.threadId;
if (sameProfile || sameTarget) followers.delete(instanceId);
}
const next: TelegramBusFollowerView = {
...registration,
target: registration.target ? { ...registration.target } : undefined,
lastHeartbeatMs:
existing?.lastHeartbeatMs ?? registration.connectedAtMs,
};
followers.set(registration.instanceId, next);
return clone(next);
},
heartbeat: (instanceId, nowMs) => {
const existing = followers.get(instanceId);
if (!existing) return undefined;
const next = { ...existing, lastHeartbeatMs: nowMs };
followers.set(instanceId, next);
return clone(next);
},
get: (instanceId) => {
const existing = followers.get(instanceId);
return existing ? clone(existing) : undefined;
},
getByTarget: (target) => {
for (const follower of followers.values()) {
if (
follower.target?.chatId === target.chatId &&
follower.target.threadId === target.threadId
) {
return clone(follower);
}
}
return undefined;
},
list: () => [...followers.values()].map(clone),
remove: (instanceId) => followers.delete(instanceId),
clear: () => followers.clear(),
pruneStale: (nowMs, staleAfterMs) => {
const removed: TelegramBusFollowerView[] = [];
for (const [instanceId, follower] of followers.entries()) {
if (nowMs - follower.lastHeartbeatMs <= staleAfterMs) continue;
followers.delete(instanceId);
removed.push(clone(follower));
}
return removed;
},
};
}
async function handleTelegramBusSocketLine(
line: string,
socket: Socket,
handleEnvelope: TelegramBusLocalServerDeps["handleEnvelope"],
recordTransportEvent: TelegramBusTransportEventRecorder | undefined,
socketPath: string,
shouldDropResponse?: (
request: TelegramBusEnvelope,
response: TelegramBusEnvelope,
) => boolean,
): Promise<void> {
const envelope = parseTelegramBusEnvelope(line);
if (!envelope) {
recordTransportEvent?.("server-invalid-envelope", {
...getTelegramBusEndpointDiagnostics(socketPath),
byteLength: Buffer.byteLength(line),
});
socket.write(
encodeTelegramBusEnvelope({
kind: "bus.ack",
requestId: "invalid",
ok: false,
message: "Invalid Telegram bus envelope.",
}),
);
return;
}
try {
const response = await handleEnvelope(envelope);
if (response && !shouldDropResponse?.(envelope, response)) {
socket.write(encodeTelegramBusEnvelope(response));
}
} catch (error) {
recordTransportEvent?.("server-handler-failed", {
...getTelegramBusEndpointDiagnostics(socketPath),
...getTelegramBusEnvelopeDiagnostics(envelope),
...classifyTelegramBusTransportError(error),
});
socket.write(
encodeTelegramBusEnvelope({
kind: "bus.ack",
requestId: envelope.requestId,
ok: false,
message: "Telegram bus handler failed.",
}),
);
}
}
function parseRegisterEnvelope(
value: Record<string, unknown>,
requestId: string,
): TelegramBusEnvelope | undefined {
const registration = parseRegistration(value.registration);
return registration
? { kind: "follower.register", requestId, registration }
: undefined;
}
function parseHeartbeatEnvelope(
value: Record<string, unknown>,
requestId: string,
): TelegramBusEnvelope | undefined {
return typeof value.instanceId === "string" &&
typeof value.sentAtMs === "number"
? {
kind: "follower.heartbeat",
requestId,
instanceId: value.instanceId,
...(typeof value.registrationGeneration === "string"
? { registrationGeneration: value.registrationGeneration }
: {}),
sentAtMs: value.sentAtMs,
}
: undefined;
}
function parseForwardCallbackEnvelope(
value: Record<string, unknown>,
requestId: string,
): TelegramBusEnvelope | undefined {
return typeof value.recipientInstanceId === "string" &&
typeof value.sentAtMs === "number"
? {
kind: "leader.forwardCallback",
requestId,
recipientInstanceId: value.recipientInstanceId,
...(typeof value.recipientRegistrationGeneration === "string"
? {
recipientRegistrationGeneration:
value.recipientRegistrationGeneration,
}
: {}),
query: value.query,
sentAtMs: value.sentAtMs,
}
: undefined;
}
function parseForwardReactionEnvelope(
value: Record<string, unknown>,
requestId: string,
): TelegramBusEnvelope | undefined {
return typeof value.recipientInstanceId === "string" &&
typeof value.sentAtMs === "number"
? {
kind: "leader.forwardReaction",
requestId,
recipientInstanceId: value.recipientInstanceId,
...(typeof value.recipientRegistrationGeneration === "string"
? {
recipientRegistrationGeneration:
value.recipientRegistrationGeneration,
}
: {}),
reactionUpdate: value.reactionUpdate,
sentAtMs: value.sentAtMs,
}
: undefined;
}
function parseForwardMessageEnvelope(
value: Record<string, unknown>,
requestId: string,
kind: "leader.forwardMessage" | "leader.forwardEditedMessage",
): TelegramBusEnvelope | undefined {
return typeof value.recipientInstanceId === "string" &&
typeof value.sentAtMs === "number"
? {
kind,
requestId,
recipientInstanceId: value.recipientInstanceId,
...(typeof value.recipientRegistrationGeneration === "string"
? {
recipientRegistrationGeneration:
value.recipientRegistrationGeneration,
}
: {}),
message: value.message,
sentAtMs: value.sentAtMs,
}
: undefined;
}
function parseReplaceFollowerTargetEnvelope(
value: Record<string, unknown>,
requestId: string,
): TelegramBusEnvelope | undefined {
const target = parseThreadTarget(value.target);
const oldTarget = parseThreadTarget(value.oldTarget);
if (
typeof value.recipientInstanceId !== "string" ||
!target ||
(value.oldTarget !== undefined && !oldTarget) ||
value.reason !== "thread-restore" ||
typeof value.sentAtMs !== "number"
) {
return undefined;
}
return {
kind: "leader.replaceFollowerTarget",
requestId,
recipientInstanceId: value.recipientInstanceId,
...(typeof value.recipientRegistrationGeneration === "string"
? {
recipientRegistrationGeneration:
value.recipientRegistrationGeneration,
}
: {}),
target,
...(oldTarget ? { oldTarget } : {}),
reason: value.reason,
sentAtMs: value.sentAtMs,
};
}
function parseCallApiEnvelope(
value: Record<string, unknown>,
requestId: string,
): TelegramBusEnvelope | undefined {
return typeof value.instanceId === "string" &&
typeof value.method === "string" &&
Array.isArray(value.args) &&
typeof value.sentAtMs === "number"
? {
kind: "follower.callApi",
requestId,
instanceId: value.instanceId,
...(typeof value.registrationGeneration === "string"
? { registrationGeneration: value.registrationGeneration }
: {}),
method: value.method,
args: value.args,
sentAtMs: value.sentAtMs,
}
: undefined;
}
function parseAckEnvelope(
value: Record<string, unknown>,
requestId: string,
): TelegramBusEnvelope | undefined {
if (typeof value.ok !== "boolean") return undefined;
const envelope: TelegramBusEnvelope = {
kind: "bus.ack",
requestId,
ok: value.ok,
message: typeof value.message === "string" ? value.message : undefined,
};
if (Object.hasOwn(value, "result")) envelope.result = value.result;
if (isRecord(value.error)) {
const code = value.error.code;
if (
code === "commit-unknown" ||
code === "request-id-collision" ||
code === "ledger-overloaded"
) {
envelope.error = {
code,
...(typeof value.error.method === "string"
? { method: value.error.method }
: {}),
};
}
}
return envelope;
}
function parseRegistration(
value: unknown,
): TelegramBusInstanceRegistration | undefined {
if (!isRecord(value)) return undefined;
if (typeof value.instanceId !== "string") return undefined;
if (typeof value.connectedAtMs !== "number") return undefined;
const target = parseTarget(value.target);
if (value.target !== undefined && !target) return undefined;
const registration: TelegramBusInstanceRegistration = {
instanceId: value.instanceId,
connectedAtMs: value.connectedAtMs,
};
if (typeof value.profileKey === "string")
registration.profileKey = value.profileKey;
if (typeof value.threadName === "string")
registration.threadName = value.threadName;
if (typeof value.slot === "string" && /^[A-Z]$/.test(value.slot)) {
registration.slot = value.slot;
}
if (typeof value.cwd === "string") registration.cwd = value.cwd;
if (typeof value.pid === "number") registration.pid = value.pid;
if (typeof value.busSocketPath === "string") {
registration.busSocketPath = value.busSocketPath;
}
if (typeof value.registrationGeneration === "string") {
registration.registrationGeneration = value.registrationGeneration;
}
if (target) registration.target = target;
return registration;
}
function parseTarget(value: unknown): TelegramTarget | undefined {
if (value === undefined) return undefined;
if (!isRecord(value) || typeof value.chatId !== "number") return undefined;
return typeof value.threadId === "number"
? { chatId: value.chatId, threadId: value.threadId }
: { chatId: value.chatId };
}
function parseThreadTarget(
value: unknown,
): (TelegramTarget & { threadId: number }) | undefined {
const target = parseTarget(value);
return target && typeof target.threadId === "number"
? { chatId: target.chatId, threadId: target.threadId }
: undefined;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}