/** * Telegram daemon controller + owner-scoped control-request helpers. * * Reload is a hybrid: an owner-scoped control-request file records auditable * intent, SIGTERM is the wakeup that aborts the in-flight long poll, and a * fresh daemon is spawned only after the old pid is dead / has exited. This * keeps the single-poller invariant (no Telegram getUpdates 409 overlap) and * never steals a still-live owner. */ import type { Settings } from "../../config/settings"; import type { BuiltInDaemonController, DaemonOperationOptions, DaemonOperationResult, DaemonStatus } from "../../daemon/control-types"; import { type TelegramDaemonDeps, type TelegramDaemonFs } from "./telegram-daemon"; export interface TelegramDaemonControlRequest { version: 1; requestId: string; action: "reload" | "stop"; ownerId: string; pid: number; createdAt: number; } export declare function telegramControlRequestPath(agentDir: string): string; export declare function readTelegramControlRequest(settings: Settings, fsImpl?: TelegramDaemonFs): Promise; export declare function writeTelegramControlRequest(settings: Settings, request: TelegramDaemonControlRequest, fsImpl?: TelegramDaemonFs): Promise; export declare function clearTelegramControlRequest(settings: Settings, requestId?: string, fsImpl?: TelegramDaemonFs): Promise; export interface DaemonProcessReference { incarnation: string; /** Whether the platform delivers the requested signal cooperatively or as a hard termination. */ termination: "cooperative" | "hard"; signalRoot(signal: NodeJS.Signals): void; } export declare function defaultProcessReference(pid: number, platform?: NodeJS.Platform): DaemonProcessReference | undefined; export interface TelegramDaemonControlDeps { fs?: TelegramDaemonFs; now?: () => number; pidAlive?: (pid: number) => boolean; processReference?: (pid: number) => DaemonProcessReference | undefined; pidIncarnation?: (pid: number) => string | undefined; /** Test seam for platform-specific default stable-process authority. */ platform?: NodeJS.Platform; spawn?: TelegramDaemonDeps["spawn"]; execPath?: string; /** * Stable process id encoded into freshly-spawned daemon owner ids. * * The daemon-internal entrypoint rejects numeric owner ids whose process is * already gone. `gjc daemon reload` is a short-lived CLI process, so using its * own pid can race the child startup and make the replacement exit immediately. */ ownerPid?: number; randomId?: () => string; sleep?: (ms: number) => Promise; waitStepMs?: number; /** Bounded startup-readiness timeout; injectable for deterministic controller tests. */ readinessTimeoutMs?: number; } export type TelegramGenerationReloadResult = { outcome: "ready"; operation: DaemonOperationResult; } | { outcome: "failed"; operation: DaemonOperationResult; }; export declare class TelegramDaemonController implements BuiltInDaemonController { #private; private readonly settings; private readonly deps; readonly kind: "telegram"; private readonly fsImpl; private readonly pidAlive; private readonly now; private readonly processReference; private readonly waitStepMs; constructor(settings: Settings, deps?: TelegramDaemonControlDeps); private runtimeInfo; status(): Promise; private spawnDeps; private spawnAndWait; private sleep; /** * Wait until the captured pid is dead. Ownership-file movement is NOT treated * as quiescence here: only actual process death proves the old poller stopped, * which is what the no-409 invariant requires before spawning a fresh poller. */ private waitForPidDeath; private signalCapturedOwner; private result; reload(opts?: DaemonOperationOptions): Promise; reloadForGenerationUpgrade(opts?: DaemonOperationOptions, attestedLegacyUpgrade?: boolean): Promise; stop(opts?: DaemonOperationOptions): Promise; private stopOrReload; /** Clear only our exact control request; a successor request must survive. */ private clearOwnRequest; }