/** * Session protocol vocabulary (DEC-050). * * A SESSION is the live attachment of an INSTANCE to the protocol over a * given transport. The INSTANCE (`claude:session-42`) is identity; the * SESSION is the heartbeat-bounded liveness of one process holding that * identity. An instance MAY have several concurrent sessions; the heartbeat * is what distinguishes a live attachment from a stale registration. * * V1 ships this as a declarative vocabulary + presence guard. The actual * heartbeat producer, presence-file format on disk, and MCP notification * dispatch are implemented in `@sentropic/h2a` (DEC-051 / DEC-052). */ import type { H2AWorkspaceRef } from "./identity.js"; export declare const H2A_SESSION_STATES: readonly ["opening", "live", "draining", "closed", "expired"]; export declare const H2A_SESSION_NOTIFICATION_TOPICS: readonly ["presence.peer_joined", "presence.peer_left", "inbox.envelope_arrived", "negotiation.event_appended", "peer.blocked", "peer.unblocked"]; export declare const H2A_SESSION_DEFAULT_HEARTBEAT_INTERVAL_MS = 5000; export declare const H2A_SESSION_DEFAULT_EXPIRY_MS = 90000; /** * WP-F (presence-honesty): default window (ms) within which a session's * `lastMcpActivityAt` must fall for its connection to read as "active". Beyond * it — while the heartbeat is still fresh — the connection is "idle-uncertain": * the process lives but the MCP channel has carried no traffic, so it is either * genuinely idle OR silently disconnected (the false-live case). Override at the * CLI boundary via `H2A_ACTIVITY_WINDOW_MS`. Advisory only — never a routing gate. */ export declare const H2A_ACTIVITY_WINDOW_DEFAULT_MS = 600000; export type H2ASessionState = (typeof H2A_SESSION_STATES)[number]; export type H2ASessionNotificationTopic = (typeof H2A_SESSION_NOTIFICATION_TOPICS)[number]; export declare const H2A_WORK_STATUSES: readonly ["working", "paused", "done", "blocked", "out-of-tokens"]; export type H2AWorkStatus = (typeof H2A_WORK_STATUSES)[number]; /** * Where/how an agent session was launched — captured AT LAUNCH (DEC-084). It * cannot be inferred later (a shell that did not launch the session cannot * find its tmux pane), so the host plugin records it at start for the * relauncher to use. */ export interface H2ALaunchContext { readonly cwd: string; /** The command that launched the session. */ readonly command: string; /** How to resume it (e.g. `codex resume`, `claude -r `). */ readonly resumeCommand?: string; readonly tty?: string; readonly tmux?: { readonly session: string; readonly window?: string; readonly pane: string; }; } export interface H2ASessionInterests { /** Scopes the session wants to observe for presence and negotiation events. */ readonly scopes: readonly string[]; /** Specific negotiation ids the session wants to follow. */ readonly negotiations: readonly string[]; } export interface H2ASession { readonly sessionId: string; readonly instance: string; /** Optional host CLI hint (e.g. "claude", "codex", "gemini"). */ readonly host?: string; /** Optional PID of the process holding the session (for same-machine staleness checks). */ readonly pid?: number; /** ISO timestamp when the session was opened. */ readonly startedAt: string; /** ISO timestamp of the last heartbeat. Drives expiry. */ readonly heartbeatAt: string; readonly state: H2ASessionState; readonly interests: H2ASessionInterests; readonly subscribedTopics: readonly H2ASessionNotificationTopic[]; /** Drumbeat (DEC-084): the agent's self-reported work status, if any. */ readonly workStatus?: H2AWorkStatus; /** Drumbeat (DEC-084): launch context captured at session start, for relance. */ readonly launchContext?: H2ALaunchContext; /** * DEC-114 (agent-identity fix): first-class WORKSPACE the session is attached * to (a traced place, not an actor). Additive — old presence records without * it stay valid; the guard validates it only when present. */ readonly workspace?: H2AWorkspaceRef; /** * DEC-114: the perennial agent's mutable display name (e.g. set via `--name` * or a `/rename`). UX only — never a routing key (the `instance` handle is). */ readonly name?: string; /** * Deployed-version stamp captured at session open, so drift is visible in * `/h2a discover` / `h2a doctor` without polling each host: `cli` = the running * `@sentropic/h2a`, `skill` = the installed h2a skill for this host (the * binary auto-upgrades but the skill does not). Additive; absent on old records. */ readonly version?: H2AAgentVersion; /** * ISO timestamp set by the mirror ingester when this session's presence was * ingested from a remote/sidecar (absent for a directly-connected local session). */ readonly mirroredAt?: string; /** * WP-F (presence-honesty): ISO timestamp of the last inbound MCP JSON-RPC * line this session's mcp-serve actually received — proof the host→server * channel carried traffic, NOT merely that the process lives (the blind * heartbeat). Absent on legacy/mirrored records → "unknown" confidence, never * "stale". Only the owning mcp-serve writes it; the external keepalive prober * (which refreshes `heartbeatAt` from tmux-pane liveness) cannot fake it. */ readonly lastMcpActivityAt?: string; } /** Deployed-version stamp for an agent session (see {@link H2ASession.version}). */ export interface H2AAgentVersion { /** Running `@sentropic/h2a` version (its package.json). */ readonly cli?: string; /** Installed h2a skill version for the host (frontmatter `version:`), if resolvable. */ readonly skill?: string; } export declare function isH2ASession(value: unknown): value is H2ASession; export interface H2ASessionExpiryOptions { /** Reference instant; defaults to Date.now(). */ readonly now?: number; /** Expiry window in ms; defaults to H2A_SESSION_DEFAULT_EXPIRY_MS. */ readonly expiryMs?: number; } /** * A session is expired iff its state is already `expired` or `closed`, or if * its last heartbeat is older than `expiryMs` from `now`. The `opening` and * `draining` states stay fresh as long as the heartbeat is recent — they * describe lifecycle, not absence. */ export declare function isSessionExpired(session: H2ASession, options?: H2ASessionExpiryOptions): boolean; /** * Filter a list of sessions to those whose heartbeat is still within * `expiryMs` of `now` and whose state is not `closed`/`expired`. Pure helper, * no I/O. */ export declare function pickFreshSessions(sessions: readonly H2ASession[], options?: H2ASessionExpiryOptions): H2ASession[]; /** Connection-channel confidence derived from MCP-traffic recency (WP-F). */ export type H2AConnectionConfidence = "active" | "idle-uncertain" | "unknown"; export interface H2AConnectionConfidenceOptions { /** Reference instant; defaults to Date.now(). */ readonly now?: number; /** Activity window in ms; defaults to H2A_ACTIVITY_WINDOW_DEFAULT_MS. */ readonly activityWindowMs?: number; /** * Clock-skew margin (ms) added to the window when the timestamp may come from * another machine (a mirrored session). Defaults to 0. */ readonly skewMarginMs?: number; } /** * Derive the connection-channel confidence for a session — distinct from * heartbeat-based expiry, which only proves the PROCESS is alive. Pure, no I/O. * * - "unknown": no parseable `lastMcpActivityAt` (legacy/mirrored record) — never * infer staleness from absence. * - "active": last MCP traffic within `activityWindowMs` (+ skew margin). * - "idle-uncertain": the MCP channel has been silent past the window — the * process may be a genuinely-idle agent OR a silently-disconnected false-live. * * NEVER drops a session and is ADVISORY only: callers surface it; routing is not * gated on it (that change is parked for an explicit decision). */ export declare function deriveConnectionConfidence(session: H2ASession, options?: H2AConnectionConfidenceOptions): H2AConnectionConfidence; /** Default idle window before a still-"working" session is treated as stalled. */ export declare const H2A_DEFAULT_STALL_IDLE_MS = 120000; export type H2AStallReason = "out-of-tokens" | "idle" | "idle-heuristic"; export interface H2AStallOptions { /** Reference instant; defaults to Date.now(). */ readonly now?: number; /** Idle window (ms) before a working/paused session counts as stalled. */ readonly idleMs?: number; } export interface H2AStallVerdict { readonly stalled: boolean; readonly reason?: H2AStallReason; } /** * Pure stall detection for the drumbeat (DEC-084): does this session look like * an agent that stopped without finishing? It is session-level only — the * daemon enriches it with engagement/journal progress. * * Rules: * - `closed`/`expired` lifecycle, or `workStatus: "done"` → not a stall. * - `workStatus: "blocked"` → not a drumbeat stall (the explicit-blockage * feedback loop, EVO-3, owns that). * - `workStatus: "out-of-tokens"` → stalled, regardless of heartbeat. * - otherwise, a heartbeat older than `idleMs` is a stall: reason `idle` when * the agent said it was `working`/`paused`, `idle-heuristic` when it never * reported a status. A fresh or unparseable heartbeat → not a stall. */ export declare function inferStall(session: H2ASession, options?: H2AStallOptions): H2AStallVerdict; //# sourceMappingURL=session.d.ts.map