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)
332 lines
9.6 KiB
TypeScript
332 lines
9.6 KiB
TypeScript
/**
|
|
* Telegram diagnostics logs
|
|
* Zones: telegram diagnostics, filesystem, session observability
|
|
* Owns bounded JSONL runtime evidence files, previous-log preservation, and profile-aware log paths without becoming routing state
|
|
*/
|
|
|
|
import {
|
|
copyFileSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
statSync,
|
|
writeFileSync,
|
|
appendFileSync,
|
|
} from "node:fs";
|
|
import { dirname } from "node:path";
|
|
import {
|
|
resolveAgentDir,
|
|
resolveTelegramProfileTempFilePath,
|
|
} from "./paths.ts";
|
|
import { withTelegramFileTransaction } from "./locks.ts";
|
|
import * as Status from "./status.ts";
|
|
|
|
export type TelegramLogPathInput = string | (() => string);
|
|
|
|
export interface TelegramRuntimeJsonlEvent {
|
|
at: number;
|
|
category: string;
|
|
message: string;
|
|
details?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface TelegramRuntimeJsonlLogOptions {
|
|
path?: TelegramLogPathInput;
|
|
previousPath?: TelegramLogPathInput;
|
|
maxBytes?: number;
|
|
getNowMs?: () => number;
|
|
canReset?: () => boolean;
|
|
commitReset?: (commit: () => void) => boolean;
|
|
}
|
|
|
|
export interface TelegramRuntimeJsonlLog {
|
|
getPath: () => string;
|
|
reset: (reason: string, scope?: Record<string, unknown>) => void;
|
|
resetIfScopeChanged: (
|
|
scopeKey: string,
|
|
reason: string,
|
|
scope?: Record<string, unknown>,
|
|
) => void;
|
|
record: (event: TelegramRuntimeJsonlEvent) => void;
|
|
}
|
|
|
|
const DEFAULT_MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
|
|
export function getTelegramRuntimeLogPath(
|
|
agentDir = resolveAgentDir(),
|
|
profileName?: string,
|
|
): string {
|
|
return resolveTelegramProfileTempFilePath(
|
|
"logs",
|
|
"jsonl",
|
|
agentDir,
|
|
profileName,
|
|
);
|
|
}
|
|
|
|
export function getTelegramPreviousRuntimeLogPath(
|
|
agentDir = resolveAgentDir(),
|
|
profileName?: string,
|
|
): string {
|
|
return resolveTelegramProfileTempFilePath(
|
|
"logs",
|
|
"_prev.jsonl",
|
|
agentDir,
|
|
profileName,
|
|
);
|
|
}
|
|
|
|
function safeJsonLine(value: unknown): string {
|
|
return JSON.stringify(value, (_key, item) => {
|
|
if (item instanceof Error) return item.message;
|
|
if (typeof item === "bigint") return item.toString();
|
|
if (typeof item === "function" || typeof item === "symbol")
|
|
return undefined;
|
|
return item;
|
|
});
|
|
}
|
|
|
|
export function createTelegramRuntimeJsonlLog(
|
|
options: TelegramRuntimeJsonlLogOptions = {},
|
|
): TelegramRuntimeJsonlLog {
|
|
const resolvePath = () =>
|
|
typeof options.path === "function"
|
|
? options.path()
|
|
: (options.path ?? getTelegramRuntimeLogPath());
|
|
const resolvePreviousPath = () => {
|
|
if (typeof options.previousPath === "function")
|
|
return options.previousPath();
|
|
if (options.previousPath) return options.previousPath;
|
|
return resolvePath().replace(/\.jsonl$/u, "._prev.jsonl");
|
|
};
|
|
const maxBytes = options.maxBytes ?? DEFAULT_MAX_LOG_BYTES;
|
|
const getNowMs = options.getNowMs ?? Date.now;
|
|
const scopeKeys = new Map<string, string | undefined>();
|
|
let pending: Promise<void> = Promise.resolve();
|
|
|
|
const ensureParent = (path: string) => {
|
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
};
|
|
|
|
const preserveCurrentLog = (path: string, previousPath: string) => {
|
|
if (!existsSync(path)) return;
|
|
mkdirSync(dirname(previousPath), { recursive: true, mode: 0o700 });
|
|
copyFileSync(path, previousPath);
|
|
};
|
|
|
|
const writeResetLocked = (
|
|
path: string,
|
|
previousPath: string,
|
|
reason: string,
|
|
scope?: Record<string, unknown>,
|
|
) => {
|
|
ensureParent(path);
|
|
preserveCurrentLog(path, previousPath);
|
|
writeFileSync(
|
|
path,
|
|
safeJsonLine({
|
|
at: getNowMs(),
|
|
kind: "reset",
|
|
reason,
|
|
scope,
|
|
previousPath,
|
|
}) + "\n",
|
|
{ mode: 0o600 },
|
|
);
|
|
};
|
|
|
|
const writeReset = (
|
|
reason: string,
|
|
scope?: Record<string, unknown>,
|
|
): boolean => {
|
|
if (options.canReset && !options.canReset()) return false;
|
|
const path = resolvePath();
|
|
let didReset = false;
|
|
withTelegramFileTransaction(`${path}.transaction`, () => {
|
|
const commit = () => {
|
|
writeResetLocked(path, resolvePreviousPath(), reason, scope);
|
|
didReset = true;
|
|
};
|
|
if (options.commitReset) {
|
|
options.commitReset(commit);
|
|
} else {
|
|
commit();
|
|
}
|
|
});
|
|
return didReset;
|
|
};
|
|
|
|
const appendLine = (line: string) => {
|
|
const path = resolvePath();
|
|
const previousPath = resolvePreviousPath();
|
|
pending = pending
|
|
.catch(() => undefined)
|
|
.then(() => {
|
|
ensureParent(path);
|
|
withTelegramFileTransaction(`${path}.transaction`, () => {
|
|
if (
|
|
existsSync(path) &&
|
|
statSync(path).size > maxBytes &&
|
|
(!options.canReset || options.canReset())
|
|
) {
|
|
const rotate = () =>
|
|
writeResetLocked(path, previousPath, "max-bytes", { maxBytes });
|
|
if (options.commitReset) {
|
|
options.commitReset(rotate);
|
|
} else {
|
|
rotate();
|
|
}
|
|
}
|
|
appendFileSync(path, line, { mode: 0o600 });
|
|
});
|
|
});
|
|
};
|
|
|
|
return {
|
|
getPath: resolvePath,
|
|
reset(reason, scope) {
|
|
const path = resolvePath();
|
|
try {
|
|
if (writeReset(reason, scope)) {
|
|
scopeKeys.set(path, scope ? safeJsonLine(scope) : undefined);
|
|
}
|
|
} catch {
|
|
// Diagnostics must never break Telegram runtime behavior.
|
|
}
|
|
},
|
|
resetIfScopeChanged(nextScopeKey, reason, scope) {
|
|
const path = resolvePath();
|
|
if (scopeKeys.get(path) === nextScopeKey) return;
|
|
try {
|
|
if (writeReset(reason, scope)) scopeKeys.set(path, nextScopeKey);
|
|
} catch {
|
|
// Diagnostics must never break Telegram runtime behavior.
|
|
}
|
|
},
|
|
record(event) {
|
|
appendLine(safeJsonLine({ kind: "event", ...event }) + "\n");
|
|
},
|
|
};
|
|
}
|
|
|
|
export interface TelegramRuntimeDiagnosticsRuntime<TContext> {
|
|
events: Status.TelegramRuntimeEventRecorder;
|
|
recordRuntimeEvent(
|
|
category: string,
|
|
error: unknown,
|
|
details?: Record<string, unknown>,
|
|
): void;
|
|
bindStorage(ports: {
|
|
getBotToken(): string | undefined;
|
|
getProfileName(): string | undefined;
|
|
canReset(): boolean;
|
|
commitReset(commit: () => void): boolean;
|
|
}): void;
|
|
bindStatus(ports: {
|
|
instanceId: string;
|
|
updateStatus(ctx: TContext, error?: string): void;
|
|
getStatusState(): Status.TelegramBridgeStatusLineState;
|
|
persistSnapshot(
|
|
snapshot: ReturnType<typeof Status.createTelegramStatusSnapshot>,
|
|
): Promise<void>;
|
|
}): void;
|
|
updateStatus(ctx: TContext, error?: string): void;
|
|
getStatusLines(options?: Status.TelegramBridgeStatusLineOptions): string[];
|
|
scheduleSnapshotPersist(): void;
|
|
}
|
|
|
|
export function createTelegramRuntimeDiagnosticsRuntime<
|
|
TContext,
|
|
>(): TelegramRuntimeDiagnosticsRuntime<TContext> {
|
|
let getBotToken = (): string | undefined => undefined;
|
|
let getProfileName = (): string | undefined => undefined;
|
|
let canReset = (): boolean => false;
|
|
let commitReset = (_commit: () => void): boolean => false;
|
|
let statusPorts:
|
|
| {
|
|
instanceId: string;
|
|
updateStatus(ctx: TContext, error?: string): void;
|
|
getStatusState(): Status.TelegramBridgeStatusLineState;
|
|
persistSnapshot(
|
|
snapshot: ReturnType<typeof Status.createTelegramStatusSnapshot>,
|
|
): Promise<void>;
|
|
}
|
|
| undefined;
|
|
let requestSnapshotPersist = (): void => {};
|
|
const events = Status.createTelegramRuntimeEventRecorder({
|
|
getBotToken: () => getBotToken(),
|
|
});
|
|
const jsonl = createTelegramRuntimeJsonlLog({
|
|
path: () => getTelegramRuntimeLogPath(undefined, getProfileName()),
|
|
previousPath: () =>
|
|
getTelegramPreviousRuntimeLogPath(undefined, getProfileName()),
|
|
canReset: () => canReset(),
|
|
commitReset: (commit) => commitReset(commit),
|
|
});
|
|
const recordRuntimeEvent = function (
|
|
category: string,
|
|
error: unknown,
|
|
details?: Record<string, unknown>,
|
|
): void {
|
|
events.record(category, error, details);
|
|
const latestEvent = events.getEvents().at(-1);
|
|
if (latestEvent) jsonl.record(latestEvent);
|
|
requestSnapshotPersist();
|
|
};
|
|
const persistCurrentSnapshot = async (): Promise<void> => {
|
|
if (!statusPorts) return;
|
|
await statusPorts.persistSnapshot(
|
|
Status.createTelegramStatusSnapshot(statusPorts.getStatusState()),
|
|
);
|
|
};
|
|
const updateRuntimeLogScope = function (reason: string): void {
|
|
if (!statusPorts) return;
|
|
const scope = Status.createTelegramRuntimeLogScope({
|
|
state: statusPorts.getStatusState(),
|
|
instanceId: statusPorts.instanceId,
|
|
});
|
|
jsonl.resetIfScopeChanged(JSON.stringify(scope), reason, scope);
|
|
};
|
|
return {
|
|
events,
|
|
recordRuntimeEvent,
|
|
bindStorage(ports) {
|
|
getBotToken = ports.getBotToken;
|
|
getProfileName = ports.getProfileName;
|
|
canReset = ports.canReset;
|
|
commitReset = ports.commitReset;
|
|
},
|
|
bindStatus(ports) {
|
|
statusPorts = ports;
|
|
requestSnapshotPersist =
|
|
Status.createTelegramRuntimeDiagnosticsSnapshotScheduler({
|
|
persistSnapshot: persistCurrentSnapshot,
|
|
recordError(error) {
|
|
events.record("telegram", error, {
|
|
phase: "runtime-diagnostics-snapshot-persist",
|
|
});
|
|
},
|
|
});
|
|
},
|
|
updateStatus(ctx, error) {
|
|
if (!statusPorts) return;
|
|
statusPorts.updateStatus(ctx, error);
|
|
updateRuntimeLogScope("status-scope-change");
|
|
},
|
|
getStatusLines(options) {
|
|
if (!statusPorts) return [];
|
|
void persistCurrentSnapshot().catch((error) => {
|
|
recordRuntimeEvent("telegram", error, {
|
|
phase: "status-snapshot-persist",
|
|
});
|
|
});
|
|
return Status.buildTelegramBridgeStatusLines(
|
|
statusPorts.getStatusState(),
|
|
options,
|
|
);
|
|
},
|
|
scheduleSnapshotPersist() {
|
|
requestSnapshotPersist();
|
|
},
|
|
};
|
|
}
|