import type { AutomationRouteBinding } from '../automation/routes.js'; import type { CreateSharedSessionInput, ParticipantRouteAttachInput, RegisterSharedSessionInput, SharedSessionCloseReason, SharedSessionParticipant, SharedSessionRecord, SharedSessionRegisterResult } from './session-types.js'; export declare const RESERVED_SHARED_SESSION_IDS: Set; /** * Metadata key under which a closed session records WHY it was closed. Kept in * `metadata` (not a first-class field) so it rides the wire without a schema * change and 0.38 readers ignore it. See {@link SharedSessionCloseReason}. */ export declare const SESSION_CLOSE_REASON_METADATA_KEY = "closeReason"; /** Read the close reason off a record's metadata, or undefined when unset/unknown. */ export declare function readSessionCloseReason(session: SharedSessionRecord): SharedSessionCloseReason | undefined; /** * True when the SYSTEM closed this session rather than a user or a surface, * the reaper ('idle-reaped') or the boot orphan sweep ('boot-orphaned'). * * Both mean "nobody asked for this to close, we inferred it", so both reopen * automatically on the next participant heartbeat. That is what makes the boot * sweep safe to run against every still-active record: a surface that outlived * the daemon restart and is genuinely still there re-registers on its next * heartbeat and gets its session back, while a session whose process really is * gone stays closed with the reason on the record. */ export declare function isSystemClosedSession(session: SharedSessionRecord): boolean; /** Stamp a close reason onto a metadata bag (returns a new object). */ export declare function withSessionCloseReason(metadata: Record, reason: SharedSessionCloseReason): Record; /** Strip any close reason from a metadata bag (returns a new object only if needed). */ export declare function withoutSessionCloseReason(metadata: Record): Record; /** * Metadata flag marking a session as SURFACE-MANAGED: a spine-registered surface * (via `sessions.register`) owns turn execution and input collection for it. Set * on every register/heartbeat. Steer/follow-up inputs to such a session (when no * daemon agent is live) route to the surface as a queued input the surface * collects, NOT a daemon executor spawn. Kept in metadata (open record) so it * needs no schema change. Daemon-run origins (companion-task/automation, or a * plain /task session) never call register and stay executor-routed. */ export declare const SESSION_SURFACE_MANAGED_METADATA_KEY = "surfaceManaged"; /** How recently a participant must have been seen to count as a LIVE surface for * steer/follow-up routing. A surface heartbeats via `sessions.register`; if the * newest heartbeat is older than this, the surface is treated as gone and inputs * fall back to the daemon executor path. */ export declare const SURFACE_ROUTE_FRESHNESS_MS: number; export declare function isSurfaceManagedSession(session: SharedSessionRecord): boolean; /** * True when a participant OTHER than `exclude` (the current input's own surface) * was seen within `freshnessMs`. Excluding the sender prevents a steer's own * just-attached participant from making a surfaceless session look live. */ export declare function hasFreshSurfaceParticipant(session: SharedSessionRecord, now: number, freshnessMs: number, exclude?: { readonly surfaceId?: string | undefined; }): boolean; /** Steer/follow-up to this session should route to a live surface (queue-for-surface) * rather than spawn a daemon executor. */ export declare function shouldRouteInputToSurface(session: SharedSessionRecord, now: number, freshnessMs: number, exclude?: { readonly surfaceId?: string | undefined; }): boolean; export interface CreateSharedSessionRecordInput { readonly id?: string | undefined; readonly title?: string | undefined; readonly metadata?: Record | undefined; readonly routeBinding?: AutomationRouteBinding | undefined; readonly participant?: SharedSessionParticipant | undefined; readonly kind?: SharedSessionRecord['kind'] | undefined; readonly project?: string | undefined; } export declare function assertSharedSessionIdAllowed(id: string | undefined): void; export declare function createSharedSessionRecord(input: CreateSharedSessionRecordInput): SharedSessionRecord; /** * Shape a participant triple into the attach-input the broker's * participant/route merge expects. Used by `register` so a heartbeat re-attaches * the participant (advancing `lastSeenAt`) without carrying a message body. */ /** The broker operations {@link registerSharedSession} needs, injected so the * register control-flow lives here instead of bloating the broker class. */ export interface RegisterBrokerOps { getSession(id: string): SharedSessionRecord | null; createSession(input: CreateSharedSessionInput): Promise; reopenSession(id: string): Promise; attachParticipant(session: SharedSessionRecord, attach: ParticipantRouteAttachInput): Promise; } /** * The idempotent register/heartbeat control-flow with HONEST closed semantics: * a brand-new id is created; an existing OPEN id adopts the participant; an * existing CLOSED id records the heartbeat but stays closed (returning a conflict * marker) UNLESS `reopen: true` is passed. A titled session is never renamed by * the heartbeat (that rule lives in {@link attachSharedSessionParticipantAndRoute}). */ export declare function registerSharedSession(ops: RegisterBrokerOps, input: RegisterSharedSessionInput): Promise; export declare function participantToAttachInput(participant: SharedSessionParticipant, title?: string): ParticipantRouteAttachInput; /** Close a session, recording WHY. Defaults to 'user', the explicit close verb * (daemon `sessions.close`) is a deliberate user/surface action, which does NOT * auto-reopen on heartbeat. The reaper passes 'idle-reaped'. */ export declare function closeSharedSessionRecord(session: SharedSessionRecord, reason?: SharedSessionCloseReason): SharedSessionRecord; export declare function reopenSharedSessionRecord(session: SharedSessionRecord): SharedSessionRecord; export declare function bindSharedSessionAgent(session: SharedSessionRecord, agentId: string): SharedSessionRecord; /** * True when a session title is still the auto-generated placeholder (empty or * `Session `) and can be named by an incoming register/attach. A real, * user-supplied title is never overwritten by this path. */ export declare function isPlaceholderSessionTitle(title: string, id: string): boolean; /** * Merge a participant + optional route onto a session. This is the HEARTBEAT * path, it records the participant and advances lastSeenAt, but it must NOT by * itself change lifecycle status (a closed session stays closed; reopening is an * explicit verb) and must NOT overwrite a real title (only names a placeholder). */ export declare function attachSharedSessionParticipantAndRoute(input: { readonly session: SharedSessionRecord; readonly message: ParticipantRouteAttachInput; readonly binding?: AutomationRouteBinding | undefined; }): SharedSessionRecord; /** * The inverse of {@link attachSharedSessionParticipantAndRoute}: remove EVERY * participant bound to `surfaceId` and unbind the route bindings those * participants alone held. This is "detach != close != kill", the session and * all other participants keep running; the detached surface simply stops being a * routing target (and, filtered by domain, stops receiving session updates). * * Route pruning is conservative: a routeId is dropped from the session only when * NO surviving participant still references it, so detaching one surface never * severs another surface's live binding. * * Returns `{ session, changed }`. `changed` is false when no participant matched * `surfaceId` (the caller can treat a no-match detach as an idempotent no-op and * skip persistence/emit). Status, title, and closedAt are preserved untouched, * detach is never a lifecycle transition. */ export declare function detachSharedSessionParticipant(session: SharedSessionRecord, surfaceId: string): { readonly session: SharedSessionRecord; readonly changed: boolean; }; //# sourceMappingURL=session-broker-sessions.d.ts.map