import type { DaemonInfo } from './registry.js'; import type { DashboardEvent } from '../core/dashboard-events.js'; type Row = { sessionId: string; larkAppId: string; [k: string]: unknown; }; type Sched = { id: string; [k: string]: unknown; }; /** * Aggregates session and schedule state across all online daemons. * Pure state machine — no I/O. The dashboard process feeds it events from * each daemon's SSE stream (via subscribeDaemon below) and from initial * hydration calls (via GET /api/sessions /api/schedules). */ export declare class Aggregator { private sessions; private schedules; private listeners; applyEvent(larkAppId: string, ev: DashboardEvent): void; /** Bulk-load on dashboard start before SSE catches up. Idempotent. */ hydrateSessions(larkAppId: string, rows: Row[]): void; /** * Per-daemon reconcile for schedules. Upserts `rows` and REMOVES schedules * owned by `larkAppId` that are absent from the snapshot: a * `schedule.deleted` missed while the SSE stream was down would otherwise * ghost forever, since upsert alone can't delete. Schedules owned by other * daemons (and ownerless rows) are untouched. Rows missing `larkAppId` * are tagged with the hydrating daemon, which is authoritative for them * (they came from its /api/schedules). */ hydrateSchedules(larkAppId: string, rows: Sched[]): void; getSessions(): Row[]; getSession(sessionId: string): Row | undefined; getSchedules(): Sched[]; /** sessionId → owning daemon's larkAppId (used for write routing). */ ownerOf(sessionId: string): string | undefined; /** sessionId → owning bot daemon's terminal reverse-proxy port. Used by the * dashboard `/s/*` bridge to route a terminal request to the right daemon's * proxy (each bot daemon runs its own terminal proxy on proxyBasePort+idx). * undefined when the session is unknown or its daemon's proxy isn't up. */ terminalProxyPortOf(sessionId: string): number | undefined; /** Whether a session row with this id exists at all in the aggregator, * regardless of `larkAppId` presence. Mirrors `scheduleExists`; lets * the Route B write gate tell apart "legacy row with no owner" from * "unknown id" so the close/resume/locate handler can route legacy rows * to the caller's bot instead of 404'ing them. */ sessionExists(sessionId: string): boolean; scheduleOwnerOf(id: string): string | undefined; /** Whether a schedule row with this id exists at all in the aggregator, * regardless of `larkAppId` presence. Used by the Route B write gate to * distinguish a "legacy row with no owner" from a genuinely "unknown id" * — the former should still proxy somewhere (the caller's bot), the * latter is a 404 (codex 2026-06-10 schedules slice 2a blocker). */ scheduleExists(id: string): boolean; on(fn: (e: DashboardEvent & { larkAppId: string; }) => void): () => void; } /** * Subscribe to one daemon's SSE stream and feed events into the aggregator. * Auto-reconnects on error OR clean EOF with 1s backoff. Returns an abort * function. * * `onConnected` fires after EVERY successful stream establishment — the first * connection included — BEFORE any frame is read, and receives the * subscription's abort signal. While it runs, incoming frames stay queued in * the stream; once it resolves, reading begins and the queued frames are * applied in order. The caller uses it to install an authoritative snapshot * (GET /api/sessions) that is therefore at least as fresh as every frame * applied afterwards: a slow snapshot response can never clobber state that * a faster SSE event already delivered (the reverse race of a naive * post-subscribe hydrate). The same barrier on reconnect recovers events * missed while the stream was down. * * The signal is also the generation arbitration: if this subscription is * aborted (daemon offline, superseded by a newer generation) while the * callback is in flight, the callback MUST discard its result instead of * applying it — otherwise the stale generation's snapshot reverse-clobbers * the newer one. The callback should fetch with * `AbortSignal.any([signal, timeout])` and re-check `signal.aborted` before * every aggregator mutation. If `onConnected` throws, frames are still read * (best-effort). */ export declare function subscribeDaemon(d: DaemonInfo, agg: Aggregator, onError: (e: Error) => void, fetchImpl?: typeof fetch, onConnected?: (signal: AbortSignal) => Promise | void): () => void; export {}; //# sourceMappingURL=aggregator.d.ts.map