/** * SessionRegistry — cross-process session and agent tracker. * * Each WrongStack process registers its session on start and updates its * status periodically. The registry is a single JSON file at * `~/.wrongstack/session-registry.json`. Entries are keyed by session ID. * * Because multiple processes may write concurrently, every write is an * atomic read-modify-write protected by a per-file advisory lock (flock on * Unix, exclusive open on Windows). Stale entries (process no longer alive) * are pruned on every read. * * @module session-registry */ /** Live status of a single agent within a session. */ export type AgentLiveStatus = 'idle' | 'running' | 'streaming' | 'waiting_user' | 'error'; /** A bounded, display-safe tool receipt shared with cross-process observers. */ export interface AgentRecentTool { id: string; name: string; startedAt: number; completedAt: number; durationMs: number; ok: boolean; input?: unknown | undefined; output?: string | undefined; inputLines?: number | undefined; oldLines?: number | undefined; newLines?: number | undefined; addedLines?: number | undefined; removedLines?: number | undefined; outputLines?: number | undefined; outputBytes?: number | undefined; outputTokens?: number | undefined; } export interface AgentRecentMail { id: string; direction: 'incoming' | 'outgoing'; from: string; to: string; type: string; subject: string; at: number; } /** A compact todo mirrored with the leader so every Office can show its live worklist. */ export interface AgentTodoItem { id: string; content: string; status: 'pending' | 'in_progress' | 'completed'; activeForm?: string | undefined; } /** Session-long aggregate used by project Office dashboards. */ export interface AgentActivityTotals { filesTouched: string[]; reads: number; writes: number; edits: number; terminalCalls: number; webCalls: number; searches: number; otherCalls: number; mailReceived: number; mailSent: number; linesRead: number; linesWritten: number; linesAdded: number; linesRemoved: number; } export interface AgentEntry { /** Unique agent id (ULID or UUID). */ id: string; /** Human-readable label (e.g. "leader", "bug-hunter #1"). */ name: string; /** UTC ISO timestamp when this agent/run started, when known. */ startedAt?: string | undefined; status: AgentLiveStatus; /** Current tool name if running, undefined otherwise. */ currentTool?: string | undefined; /** Human-readable task currently assigned to this agent. */ currentTask?: string | undefined; /** Stable coordinator task id when this is a delegated/subagent task. */ taskId?: string | undefined; /** Iteration count so far. */ iterations: number; /** Tool calls so far. */ toolCalls: number; /** Cumulative cost in USD for this agent, when known. */ costUsd?: number | undefined; /** Cumulative input tokens, when known. */ tokensIn?: number | undefined; /** Cumulative output tokens, when known. */ tokensOut?: number | undefined; /** Context window fill 0–100 (may exceed 100 when over limit), when known. */ ctxPct?: number | undefined; /** Model id this agent is running on, when known. */ model?: string | undefined; /** * Tail of the assistant text currently being streamed (capped, throttled). * Lets a cross-process watcher see the response form in near-real-time * instead of waiting for the completed turn to land in the session log. */ partialText?: string | undefined; /** Recent completed tools, newest first. Bounded by AgentStatusTracker. */ recentTools?: AgentRecentTool[] | undefined; recentMail?: AgentRecentMail[] | undefined; /** Session worklist. Populated on the leader entry only. */ todos?: AgentTodoItem[] | undefined; /** Most recent operator prompt. Populated on the leader entry only. */ latestPrompt?: string | undefined; latestPromptAt?: number | undefined; /** Cumulative activity for this live session, reset when the session ends. */ activity?: AgentActivityTotals | undefined; /** UTC ISO timestamp of last activity. */ lastActivityAt: string; } export type SessionLiveStatus = 'active' | 'idle' | 'closing' | 'stale' | 'lost'; export interface SessionRegistryEntry { sessionId: string; projectSlug: string; projectRoot: string; projectName: string; workingDir: string; /** * Which surface owns this session — `'tui'` / `'webui'` / `'cli'` (one-shot or * REPL). Lets cross-process consumers (e.g. the WebUI Fleet HQ office map) label * each live session by client kind. Optional for back-compat with older entries. */ clientType?: 'tui' | 'webui' | 'cli' | 'repl' | string | undefined; /** Current git branch, if the project is a git repo. Detected at registration. */ gitBranch?: string | undefined; status: SessionLiveStatus; pid: number; /** UTC ISO */ startedAt: string; /** UTC ISO — updated on every heartbeat */ lastHeartbeatAt: string; /** Count of tracked agents */ agentCount: number; agents: AgentEntry[]; } export declare class SessionRegistry { private readonly filePath; private heartbeatTimer; private currentSessionId; private lastTempPruneAt; private tempPrunePromise; /** Latest agent snapshot not yet written; superseded by newer calls. */ private pendingAgents; /** Shared settle promise for all updateAgents calls coalesced into one trailing write. */ private pendingAgentsFlush; private pendingAgentsResolve; private agentsFlushTimer; private lastAgentsWriteAt; /** * Last full entry this process registered. Kept so the heartbeat can * re-create our entry if it ever goes missing — e.g. our initial register() * write was dropped (a wedged lock), the file was reset, or we were pruned. */ private lastEntry; constructor(globalRoot: string); /** * Register the current session. Call once on session start. * Starts the heartbeat timer. */ register(entry: Omit & { agents?: AgentEntry[] | undefined; }): Promise; /** * Update agent status for the current session. Call on every * significant status change (agent start, tool start, user wait, error). * * Writes are coalesced: the first call in a quiet window writes * immediately; calls arriving within {@link AGENTS_WRITE_THROTTLE_MS} of * the last write collapse into one trailing write carrying the newest * snapshot. The in-memory cache ({@link lastEntry}) is always updated * synchronously, so heartbeat re-inserts never carry stale agents. */ updateAgents(agents: AgentEntry[]): Promise; /** Write the newest pending agent snapshot to the registry file. */ private writeAgentsSnapshot; /** * Cancel a scheduled trailing agents write (releasing any waiters). Used on * shutdown so a late timer can't resurrect an entry that markClosing() / * unregister() is about to finalize. Returns the snapshot that was pending. */ private cancelPendingAgentsFlush; /** * Mark the session as closing. Called during shutdown. * Stops the heartbeat timer. */ markClosing(): Promise; /** * Remove the current session from the registry. Call on clean exit. */ unregister(): Promise; /** * List all non-stale sessions. Prunes stale entries automatically. */ list(): Promise; /** * Get a single session entry by ID. Returns undefined if not found or stale. */ get(sessionId: string): Promise; /** * List all sessions for a specific project (by slug). */ listByProject(projectSlug: string): Promise; /** * Return the registry file path. Useful for WebUI to watch/read. */ get registryPath(): string; private heartbeat; private readAndPrune; private atomicUpdate; /** * Break a contended lock if it is stale: the recorded owner pid is no longer * alive, or the lock is older than {@link STALE_LOCK_MS}. Returns true when the * lock was removed (caller should retry acquisition). Best-effort and * race-tolerant — a fresh lock (age ~0, live owner) is never broken, so the * common concurrent case self-heals on the next heartbeat. */ private breakStaleLock; private writeAtomicLocked; /** Legacy write without lock — used by heartbeat for performance. */ private writeAtomic; private maybePruneStaleTempFiles; private writeAtomicFile; private pruneStaleTempFiles; } export declare function getSessionRegistry(globalRoot?: string): SessionRegistry; export declare function hasSessionRegistry(): boolean; //# sourceMappingURL=session-registry.d.ts.map