import type { Settings } from "../../config/settings"; import type { BuiltInDaemonController, DaemonOperationOptions, DaemonOperationResult, DaemonRuntimeInfo, DaemonStatus } from "../../daemon/control-types"; export type ChatDaemonKind = "discord" | "slack"; export type ChatDaemonAction = "stop" | "reload"; /** * Operational generations of the Discord/Slack daemon lifecycle contracts. * These are intentionally separate from per-session endpoint generations. * Generation 6 carries the retained managed filesystem authority boundary. * Generation 7 restores macOS daemon signaling (kill(2) with a start-time * incarnation recheck) so a live/hung owner can be replaced without an external * `kill -9`. Generation 8 adopts Windows expected-identity ACL verification and * repair for shared native authority. Generation 9 refreshes the shared native * authority declaration contract with bounded frame-delivery acknowledgement. * Generation 11 accepts typed retained exact-unlink cleanup authority — a * concrete detached quarantine plus a proven-absent canonical lock pathname — * when deleting an observed owner-lock lease. Generation 12 refreshes retained * native path and process authority semantics. Generation 13 rejects special * files before retained native authority opens. Generation 14 reloads shared * chat daemons after notification configuration parsing changes. Generation 15 * applies the current notification configuration directly when starting chat * daemon transports. Generation 16 applies Telegram sound-policy configuration * through shared notification parsing. Generation 17 bound managed-session * replacement to exact native filesystem authority; generation 18 retired that * binding, and generation 19 binds exact cleanup to parent/link-count authority * while also adding durable provider-intent admission without changing lifecycle * behavior. Generation 20 discovers isolated chat-only session endpoints when * Telegram identity ownership is blocked. Discord generation 21 applies rustfmt * and clippy-equivalent cleanup to the pi-shell process-tree authority (#3682). * Discord generation 22 / slack generation 21 refreshes retained cleanup * semantics; discord generation 23 / slack generation 22 hardens exact Bash * process-tree ownership shared by chat daemon cleanup. * Discord generation 24 / slack generation 23 apply provider-completeness and * effective-enable admission to chat daemon lifecycle controls. Discord * generation 25 / slack generation 24 apply identity-bound exact replacement * cleanup shared by managed-session and daemon filesystem authority. Discord * generation 26 / slack generation 25 add the in-place operator command channel: * an owner serves per-request commands inside its own serving loop and answers * them against an exact owner tuple, so an owner at an earlier generation may * not serve or answer a request captured against this contract. Discord * generation 27 / slack generation 26 move shared exact unlink and process- * incarnation authority behind lazy native bindings. Discord generation 28 / * slack generation 27 dial attached-session clients on the long-lived session * reconnect budget, so an owner at an earlier generation gives up reconnecting * before the host heartbeat TTL expires and permanently loses its attachment. Discord * generation 29 / slack generation 28 advance the replay cursor only after a frame is * published, so an owner at an earlier generation acknowledges an event before delivering * it and loses that event for good the first time a surface refuses it. Discord generation 30 / * slack generation 29 move lifecycle and attachment authority into SDK core; generation 30 also * isolates per-session Router attachment failures so one stale endpoint cannot block healthy sessions. * Discord generation 31 bounds one REST operation across response parsing and multi-request flows. * Slack generation 30 bounds shutdown, tracks outbound work, and fences late effect commits. * Slack generation 31 fences late post admission and tracks close-marker shutdown work. * Slack generation 32 bounds provider teardown and preserves close markers after Router revocation. * Slack generation 33 bounds lifecycle predecessors under the shutdown deadline. * Slack generation 34 CAS-fences cleanup mapping commits against successors. * Slack generation 35 identity-fences detached startup cleanup from restarted daemon state. * Slack generation 36 retains stop ownership for every detached lifecycle generation. * Discord generation 32 / Slack generation 37 bind cleanup to the removed attachment generation. * Discord generation 33 / Slack generation 38 identity-fence cleanup callbacks on exact attachments. * Discord generation 34 / Slack generation 39 clear stale attachment identity before provider reassignment. * Discord generation 35 / Slack generation 40 capture provider ownership before cleanup awaits. * Discord generation 36 / Slack generation 41 drain prior cleanup before provider restart. * Discord generation 37 / Slack generation 42 serialize successor attachment visibility after cleanup. * Discord generation 38 / Slack generation 43 hold successor frames behind cleanup settlement. * Discord generation 39 / Slack generation 44 recover durable cleanup before attachment publication. * Slack generation 45 persists exact pending cleanup intent through recovery. * Discord generation 40 bounds provider lifecycle joins before Router revocation. * Discord generation 41 / Slack generation 46 revoke Router authority despite provider shutdown failure. * Discord generation 42 / Slack generation 47 retain daemon objects across restart to preserve detached lifecycle fences. * Discord generation 43 / Slack generation 48 fence Router attachment publication and Broker-adopted endpoint authority. * Discord generation 44 / Slack generation 49 retain provider shutdown tails and rejected lifecycle errors so no successor transport starts before ownership settles. * Discord generation 45 / Slack generation 50 clean predecessor presentation authority before exact Router replacement and reject post-stop Discord callbacks. * Discord generation 46 / Slack generation 51 establish replay barriers before reconnect awaits, distinguish replacement from terminal cleanup, and await lifecycle-fenced Discord inbound work. * Discord generation 47 / Slack generation 52 preserve presentation continuity across replacement while exact opaque authority fences stale work. * Discord generation 48 / Slack generation 53 allow exact publication-time requests without reconciliation deadlock. * Discord generation 49 / Slack generation 54 revalidate endpoint authority before exact publication-time requests. * Discord generation 50 / Slack generation 55 terminalize predecessor routes only for a changed same-generation endpoint incarnation. * Slack generation 56 clears predecessor inbound receipts before same-generation successor publication. * Discord generation 51 / Slack generation 57 persist restart-stable Router endpoint-incarnation authority in provider mappings. * Discord generation 52 / Slack generation 58 classify reconnect-time endpoint changes before provider retirement. * Discord generation 53 / Slack generation 59 serialize successor attach behind predecessor provider retirement. * Discord generation 54 / Slack generation 60 version-fence attaches already in flight when retirement begins. * Discord generation 55 / Slack generation 61 fence durable inbound work by exact attachment identity. * Discord generation 56 / Slack generation 62 fence durable provider-post and thread-effect recovery by attachment identity. * Discord generation 57 removes missing-authority wildcard behavior from durable binding checks. * Discord generation 58 preserves exact authority through unarchive replacement fallback. * Discord generation 59 / Slack generation 63 derive attachment authority ids from one Router function so persisted provider bindings and live attachments cannot drift apart. * Discord generation 62 / Slack generation 65 fence the Windows process-incarnation * authority change (#4362): the native binding fallback no longer spawns powershell.exe. * Discord generation 63 / Slack generation 66 retain the shared ownership fence * unless a zero-signal process probe returns ESRCH. EPERM and unknown failures are * indeterminate, so earlier owners must not reclaim, replace, or spawn through them. * Discord generation 64 / Slack generation 67 contain synchronous provider- * subscription admission and ready-hook failures without revoking shared Router * attachment authority. * Discord generation 65 / Slack generation 68 fence the off-reconcile-tail * initial attachment replay introduced by #4542 so pre-upgrade daemon owners * cannot retain the earlier SessionRouter attachment contract. * The Discord 67 / Slack 70 fence covers the SessionRouter idle-poll/change-stamp * rollout (#4689). Both daemons construct a * SessionRouter whose idle tick no longer re-acquires the machine-global * session-index lock every 2s; staleness retirement moved to a 30s sweep and * lease heartbeats no longer force an authority reconcile. A pre-upgrade owner * would retain the old hot polling loop, so replacement is required. */ export declare const CHAT_DAEMON_GENERATIONS: Readonly>; export declare function chatDaemonGeneration(kind: ChatDaemonKind): number; export interface ChatDaemonState { version: 1; kind: ChatDaemonKind; pid: number; ownerId: string; identity: string; incarnation: string; startedAt: number; heartbeatAt: number; transportHealthy: boolean; generation: number; stoppedAt?: number; } /** * State files are untrusted persisted input. A record must be completely valid * before its PID can be treated as an owner, stopped, or safe to replace. */ /** A legacy owner is recognized only when the sole missing field is generation. */ export declare function isRecognizedLegacyGeneration(value: unknown): value is undefined; export declare function hasSafeChatDaemonStateShape(value: unknown): value is ChatDaemonState; export interface ChatDaemonControlRequest { version: 1; requestId: string; action: ChatDaemonAction; ownerId: string; pid: number; createdAt: number; incarnation: string; } export interface ChatDaemonProcessReference { incarnation: string; signalRoot(signal: NodeJS.Signals): void; } export interface ChatDaemonControlDeps { pidAlive?: (pid: number) => boolean; processReference?: (pid: number) => ChatDaemonProcessReference | undefined; spawn?: (command: string, args: string[], opts: { detached: boolean; stdio: "ignore"; }) => { unref?: () => void; }; execPath?: string; ownerPid?: number; randomId?: () => string; pidIncarnation?: (pid: number) => string | undefined; /** Test seam for platform-specific default stable-process authority. */ platform?: NodeJS.Platform; sleep?: (ms: number) => Promise; spawnReadyTimeoutMs?: number; } export declare function chatDaemonPaths(agentDir: string, kind: ChatDaemonKind): { dir: string; lock: string; state: string; control: string; }; /** * Configuration fingerprint that identifies which settings a daemon owner was * started for. `undefined` means the current settings cannot configure that * transport at all, so no owner can be authorized against them. */ export declare function chatDaemonIdentity(settings: Settings, kind: ChatDaemonKind): string | undefined; export declare function readChatDaemonState(agentDir: string, kind: ChatDaemonKind): Promise; export declare function readChatDaemonControlRequest(agentDir: string, kind: ChatDaemonKind): Promise; export declare function writeChatDaemonControlRequest(agentDir: string, kind: ChatDaemonKind, request: ChatDaemonControlRequest): Promise; export declare function clearChatDaemonControlRequest(agentDir: string, kind: ChatDaemonKind, requestId?: string): Promise; export declare function buildChatDaemonSpawnArgs(input: { kind: ChatDaemonKind; ownerId: string; agentDir: string; execPath?: string; }): { command: string; args: string[]; runtime: DaemonRuntimeInfo; }; export declare class ChatDaemonController implements BuiltInDaemonController { private readonly settings; private readonly deps; readonly kind: ChatDaemonKind; constructor(settings: Settings, kind: ChatDaemonKind, deps?: ChatDaemonControlDeps); private identity; private effectivelyEnabled; private alive; status(): Promise; stop(opts?: DaemonOperationOptions): Promise; reload(opts?: DaemonOperationOptions): Promise; ensure(): Promise; private operate; private incarnation; private processReference; private isDefinitelyStoppedState; private stateHealth; private isSignalableMatchingOwner; /** A live PID with an invalid ownership record is never safe to overwrite. */ private isAmbiguouslyLiveState; private isHealthyFreshState; private classify; private isCurrentCompatibleState; private stopForReplacement; private ownsCapturedState; private signalIfOwner; private ownerChanged; private result; private waitForDeath; private sleep; private spawn; private waitForOwnership; } export type EnsureChatDaemonResult = "disabled" | "owner_spawned" | "attached"; export declare function ensureDiscordDaemon(settings: Settings, deps?: ChatDaemonControlDeps): Promise; export declare function ensureSlackDaemon(settings: Settings, deps?: ChatDaemonControlDeps): Promise; export declare function acquireChatDaemonOwnership(input: { agentDir: string; kind: ChatDaemonKind; ownerId: string; pid?: number; identity: string; incarnation?: string; pidAlive?: (pid: number) => boolean; pidIncarnation?: (pid: number) => string | undefined; }): Promise; export declare function renewChatDaemonHeartbeat(input: { agentDir: string; kind: ChatDaemonKind; ownerId: string; pid?: number; incarnation?: string; transportHealthy: boolean; pidAlive?: (pid: number) => boolean; pidIncarnation?: (pid: number) => string | undefined; }): Promise; export declare function releaseChatDaemonOwnership(input: { agentDir: string; kind: ChatDaemonKind; ownerId: string; pid: number; incarnation: string; pidAlive?: (pid: number) => boolean; pidIncarnation?: (pid: number) => string | undefined; }): Promise;