import type { AgentMessage } from "@oh-my-pi/pi-agent-core"; import type { ImageContent, Message, MessageAttribution, ServiceTier, TextContent } from "@oh-my-pi/pi-ai"; import { ArtifactManager } from "./artifacts"; import { type BlobPutResult } from "./blob-store"; import { type BashExecutionMessage, type CustomMessage, type FileMentionMessage, type HookMessage, type PythonExecutionMessage } from "./messages"; import type { SessionStorage } from "./session-storage"; export declare const CURRENT_SESSION_VERSION = 3; export interface SessionHeader { type: "session"; version?: number; id: string; title?: string; titleSource?: "auto" | "user"; timestamp: string; cwd: string; parentSession?: string; } export interface NewSessionOptions { parentSession?: string; /** Skip flushing the current session and delete it instead of saving. */ drop?: boolean; } export interface SessionEntryBase { type: string; id: string; parentId: string | null; timestamp: string; } export interface SessionMessageEntry extends SessionEntryBase { type: "message"; message: AgentMessage; } export interface ThinkingLevelChangeEntry extends SessionEntryBase { type: "thinking_level_change"; thinkingLevel?: string | null; } export interface ModelChangeEntry extends SessionEntryBase { type: "model_change"; /** Model in "provider/modelId" format */ model: string; /** Role: "default", "smol", "slow", etc. Undefined treated as "default" */ role?: string; } export interface ServiceTierChangeEntry extends SessionEntryBase { type: "service_tier_change"; serviceTier: ServiceTier | null; } export interface CompactionEntry extends SessionEntryBase { type: "compaction"; summary: string; shortSummary?: string; firstKeptEntryId: string; tokensBefore: number; /** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */ details?: T; /** Hook-provided data to persist across compaction */ preserveData?: Record; /** True if generated by an extension, undefined/false if pi-generated (backward compatible) */ fromExtension?: boolean; } export interface BranchSummaryEntry extends SessionEntryBase { type: "branch_summary"; fromId: string; summary: string; /** Extension-specific data (not sent to LLM) */ details?: T; /** True if generated by an extension, false if pi-generated */ fromExtension?: boolean; } /** * Custom entry for extensions to store extension-specific data in the session. * Use customType to identify your extension's entries. * * Purpose: Persist extension state across session reloads. On reload, extensions can * scan entries for their customType and reconstruct internal state. * * Does NOT participate in LLM context (ignored by buildSessionContext). * For injecting content into context, see CustomMessageEntry. */ export interface CustomEntry extends SessionEntryBase { type: "custom"; customType: string; data?: T; } /** Label entry for user-defined bookmarks/markers on entries. */ export interface LabelEntry extends SessionEntryBase { type: "label"; targetId: string; label: string | undefined; } /** TTSR injection entry - tracks which time-traveling rules have been injected this session. */ export interface TtsrInjectionEntry extends SessionEntryBase { type: "ttsr_injection"; /** Names of rules that were injected */ injectedRules: string[]; } /** Persisted MCP discovery selection state for a session branch. */ export interface MCPToolSelectionEntry extends SessionEntryBase { type: "mcp_tool_selection"; /** MCP tool names selected for visibility in discovery mode. */ selectedToolNames: string[]; } /** Session init entry - captures initial context for subagent sessions (debugging/replay). */ export interface SessionInitEntry extends SessionEntryBase { type: "session_init"; /** Full system prompt sent to the model */ systemPrompt: string; /** Initial task/user message */ task: string; /** Tools available to the agent */ tools: string[]; /** Output schema if structured output was requested */ outputSchema?: unknown; } /** Mode change entry - tracks agent mode transitions (e.g. plan mode). */ export interface ModeChangeEntry extends SessionEntryBase { type: "mode_change"; /** Current mode name, or "none" when exiting a mode */ mode: string; /** Optional mode-specific data (e.g. plan file path) */ data?: Record; } /** * Custom message entry for extensions to inject messages into LLM context. * Use customType to identify your extension's entries. * * Unlike CustomEntry, this DOES participate in LLM context. * The content participates in LLM context through convertToLlm(). * Use details for extension-specific metadata (not sent to LLM). * * display controls TUI rendering: * - false: hidden entirely * - true: rendered with distinct styling (different from user messages) */ export interface CustomMessageEntry extends SessionEntryBase { type: "custom_message"; customType: string; content: string | (TextContent | ImageContent)[]; details?: T; display: boolean; /** Who initiated this message for billing/attribution semantics. */ attribution?: MessageAttribution; } /** Session entry - has id/parentId for tree structure (returned by "read" methods in SessionManager) */ export type SessionEntry = SessionMessageEntry | ThinkingLevelChangeEntry | ModelChangeEntry | ServiceTierChangeEntry | CompactionEntry | BranchSummaryEntry | CustomEntry | CustomMessageEntry | LabelEntry | TtsrInjectionEntry | MCPToolSelectionEntry | SessionInitEntry | ModeChangeEntry; /** Raw file entry (includes header) */ export type FileEntry = SessionHeader | SessionEntry; /** Tree node for getTree() - defensive copy of session structure */ export interface SessionTreeNode { entry: SessionEntry; children: SessionTreeNode[]; /** Resolved label for this entry, if any */ label?: string; } export interface SessionContext { messages: AgentMessage[]; thinkingLevel?: string; serviceTier?: ServiceTier; /** Model roles: { default: "provider/modelId", small: "provider/modelId", ... } */ models: Record; /** Names of TTSR rules that have been injected this session */ injectedTtsrRules: string[]; /** MCP tool names selected through discovery for this session branch. */ selectedMCPToolNames: string[]; /** Whether this branch contains an explicit persisted MCP selection entry. */ hasPersistedMCPToolSelection: boolean; /** Active mode (e.g. "plan") or "none" if no special mode is active */ mode: string; /** Mode-specific data from the last mode_change entry */ modeData?: Record; } export interface SessionInfo { path: string; id: string; /** Working directory where the session was started. Empty string for old sessions. */ cwd: string; title?: string; /** Path to the parent session (if this session was forked). */ parentSessionPath?: string; created: Date; modified: Date; messageCount: number; /** File size in bytes on disk; used for compact list rendering. */ size: number; firstMessage: string; allMessagesText: string; } export type ReadonlySessionManager = Pick; /** Exported for testing */ export declare function migrateSessionEntries(entries: FileEntry[]): void; /** Exported for compaction.test.ts */ export declare function parseSessionEntries(content: string): FileEntry[]; export declare function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null; /** * Build the session context from entries using tree traversal. * If leafId is provided, walks from that entry to root. * Handles compaction and branch summaries along the path. */ export declare function buildSessionContext(entries: SessionEntry[], leafId?: string | null, byId?: Map): SessionContext; /** Exported for testing */ export declare function loadEntriesFromFile(filePath: string, storage?: SessionStorage): Promise; declare class RecentSessionInfo { #private; readonly path: string; readonly mtime: number; constructor(path: string, mtime: number, header: Record, firstPrompt?: string); /** Display name. Falls back to a timestamp-based label, never the raw UUID. */ get fullName(): string; /** * Display name without an arbitrary length cap. The renderer is responsible for * width-aware truncation so adjacent fields (e.g. the relative time) stay visible. */ get name(): string; /** Human-readable relative time (e.g., "2 hours ago") */ get timeAgo(): string; } /** Exported for testing */ export declare function findMostRecentSession(sessionDir: string, storage?: SessionStorage): Promise; /** Get recent sessions for display in welcome screen */ export declare function getRecentSessions(sessionDir: string, limit?: number, storage?: SessionStorage): Promise; /** * Manages conversation sessions as append-only trees stored in JSONL files. * * Each session entry has an id and parentId forming a tree structure. The "leaf" * pointer tracks the current position. Appending creates a child of the current leaf. * Branching moves the leaf to an earlier entry, allowing new branches without * modifying history. * * Use buildSessionContext() to get the resolved message list for the LLM, which * handles compaction summaries and follows the path from root to current leaf. */ export interface UsageStatistics { input: number; output: number; cacheRead: number; cacheWrite: number; premiumRequests: number; cost: number; } export interface ResolvedSessionMatch { session: SessionInfo; scope: "local" | "global"; } export declare function resolveResumableSession(sessionArg: string, cwd: string, sessionDir?: string, storage?: SessionStorage): Promise; interface SessionManagerStateSnapshot { sessionId: string; sessionName: string | undefined; titleSource: "auto" | "user" | undefined; sessionFile: string | undefined; flushed: boolean; needsFullRewriteOnNextPersist: boolean; fileEntries: FileEntry[]; } export declare class SessionManager { #private; private cwd; private sessionDir; private readonly persist; private readonly storage; private constructor(); /** Puts a binary blob into the blob store and returns the blob reference */ putBlob(data: Buffer): Promise; captureState(): SessionManagerStateSnapshot; restoreState(snapshot: SessionManagerStateSnapshot): void; /** Switch to a different session file (used for resume and branching) */ setSessionFile(sessionFile: string): Promise; /** Start a new session. Closes any existing writer first. */ newSession(options?: NewSessionOptions): Promise; /** Delete a session file and its artifacts. Drains the persist writer first to avoid EPERM on Windows. ENOENT is treated as success. */ dropSession(sessionPath: string): Promise; /** * Fork the current session, creating a new session file with the same entries. * Returns both the old and new session file paths for artifact copying. * @returns { oldSessionFile, newSessionFile } or undefined if not persisting */ fork(): Promise<{ oldSessionFile: string; newSessionFile: string; } | undefined>; /** * Move the session to a new working directory. * Moves session files and artifacts on disk, updates all internal references, * and rewrites the session header with the new cwd. */ moveTo(newCwd: string): Promise; isPersisted(): boolean; /** * Force-persist all current entries to disk, even when no assistant message exists yet. * Used by ACP mode where session/new must create a discoverable session immediately. */ ensureOnDisk(): Promise; /** Flush pending writes to disk. Call before switching sessions or on shutdown. */ flush(): Promise; /** Close the persistent writer after flushing all pending data. */ close(): Promise; getCwd(): string; /** Get usage statistics across all assistant messages in the session. */ getUsageStatistics(): UsageStatistics; getSessionDir(): string; getSessionId(): string; getSessionFile(): string | undefined; /** * Returns the session artifacts directory path (session file path without .jsonl). * Returns null when the session is not persisted to a file. * When this session has adopted an external ArtifactManager (subagent case), * returns that manager's directory so reads/writes land in the shared parent * dir instead of a private (non-existent) subdir. */ getArtifactsDir(): string | null; /** * Adopt an externally-owned ArtifactManager. Used by subagents to share * the parent session's artifact directory and ID counter. */ adoptArtifactManager(manager: ArtifactManager): void; /** * Returns the ArtifactManager this session writes through. Lazily creates * one bound to the current session file unless an external manager was * adopted via `adoptArtifactManager`. Returns null only for non-persistent * sessions with no adopted manager. */ getArtifactManager(): ArtifactManager | null; /** * Allocate a new artifact path and ID for the current session. * Returns an empty object when the session is not persisted. */ allocateArtifactPath(toolType: string): Promise<{ id?: string; path?: string; }>; /** * Save artifact content under the current session and return artifact ID. * Returns an artifact ID for all sessions (file-backed for persistent, in-memory fallback otherwise). */ saveArtifact(content: string, toolType: string): Promise; /** * Resolve an artifact ID to an on-disk path for the current session. * Returns null when missing or when the session is not persisted. */ getArtifactPath(id: string): Promise; /** * Persist (or clear) the current editor draft so the next resume of this * session can restore it. Empty text deletes any stale draft. No-op when the * session is not persisted. */ saveDraft(text: string): Promise; /** * Read and remove the saved draft. Returns the previously-saved text, or * null when no draft is pending. Single-shot: a successful read removes the * sidecar so a subsequent resume does not re-restore the same text. */ consumeDraft(): Promise; /** The source that set the session name: "user" (manual /rename or RPC) or "auto" (generated title). */ get titleSource(): "auto" | "user" | undefined; getSessionName(): string | undefined; /** * Set the session display name. * @param source - "user" for explicit renames (/rename command, RPC); "auto" for generated titles. * Auto-generated titles are silently ignored when the user has already set a name. */ setSessionName(name: string, source?: "auto" | "user"): Promise; _persist(entry: SessionEntry): void; /** Append a message as child of current leaf, then advance leaf. Returns entry id. * Does not allow writing CompactionSummaryMessage and BranchSummaryMessage directly. * Reason: we want these to be top-level entries in the session, not message session entries, * so it is easier to find them. * These need to be appended via appendCompaction() and appendBranchSummary() methods. */ appendMessage(message: Message | CustomMessage | HookMessage | BashExecutionMessage | PythonExecutionMessage | FileMentionMessage): string; /** Append a thinking level change as child of current leaf, then advance leaf. Returns entry id. */ appendThinkingLevelChange(thinkingLevel?: string): string; appendServiceTierChange(serviceTier: ServiceTier | null): string; /** Append a mode change as child of current leaf, then advance leaf. Returns entry id. */ appendModeChange(mode: string, data?: Record): string; /** * Append a model change as child of current leaf, then advance leaf. Returns entry id. * @param model Model in "provider/modelId" format * @param role Optional role (default: "default") */ appendModelChange(model: string, role?: string): string; /** Append session init metadata (for subagent debugging/replay). Returns entry id. */ appendSessionInit(init: { systemPrompt: string; task: string; tools: string[]; outputSchema?: unknown; }): string; /** Append a compaction summary as child of current leaf, then advance leaf. Returns entry id. */ appendCompaction(summary: string, shortSummary: string | undefined, firstKeptEntryId: string, tokensBefore: number, details?: T, fromExtension?: boolean, preserveData?: Record): string; /** Append a custom entry (for extensions) as child of current leaf, then advance leaf. Returns entry id. */ appendCustomEntry(customType: string, data?: unknown): string; /** * Rewrite the session file after in-place entry updates. * Use sparingly (e.g., pruning old tool outputs). */ rewriteEntries(): Promise; /** * Append a custom message entry (for extensions) that participates in LLM context. * @param customType Hook identifier for filtering on reload * @param content Message content (string or TextContent/ImageContent array) * @param display Whether to show in TUI (true = styled display, false = hidden) * @param details Optional extension-specific metadata (not sent to LLM) * @param attribution Who initiated this message for billing/attribution semantics * @returns Entry id */ appendCustomMessageEntry(customType: string, content: string | (TextContent | ImageContent)[], display: boolean, details?: T, attribution?: MessageAttribution): string; /** * Append an MCP tool selection entry recording the discovery-selected MCP tools. * @param selectedToolNames MCP tool names selected for this branch * @returns Entry id */ appendMCPToolSelection(selectedToolNames: string[]): string; /** * Append a TTSR injection entry recording which rules were injected. * @param ruleNames Names of rules that were injected * @returns Entry id */ appendTtsrInjection(ruleNames: string[]): string; /** * Get all unique TTSR rule names that have been injected in the current branch. * Scans from root to current leaf for ttsr_injection entries. */ getInjectedTtsrRules(): string[]; getLeafId(): string | null; getLeafEntry(): SessionEntry | undefined; /** * Get the most recent model role from the current session path. * Returns undefined if no model change has been recorded. */ getLastModelChangeRole(): string | undefined; getEntry(id: string): SessionEntry | undefined; /** * Get all direct children of an entry. */ getChildren(parentId: string): SessionEntry[]; /** * Get the label for an entry, if any. */ getLabel(id: string): string | undefined; /** * Set or clear a label on an entry. * Labels are user-defined markers for bookmarking/navigation. * Pass undefined or empty string to clear the label. */ appendLabelChange(targetId: string, label: string | undefined): string; /** * Walk from entry to root, returning all entries in path order. * Includes all entry types (messages, compaction, model changes, etc.). * Use buildSessionContext() to get the resolved messages for the LLM. */ getBranch(fromId?: string): SessionEntry[]; /** * Build the session context (what gets sent to the LLM). * Uses tree traversal from current leaf. */ buildSessionContext(): SessionContext; /** Strip stale OpenAI Responses assistant replay metadata from loaded in-memory entries. */ sanitizeLoadedOpenAIResponsesReplayMetadata(): boolean; /** * Get session header. */ getHeader(): SessionHeader | null; /** * Get all session entries (excludes header). Returns a shallow copy. * The session is append-only: use appendXXX() to add entries, branch() to * change the leaf pointer. Entries cannot be modified or deleted. */ getEntries(): SessionEntry[]; /** * Get the session as a tree structure. Returns a shallow defensive copy of all entries. * A well-formed session has exactly one root (first entry with parentId === null). * Orphaned entries (broken parent chain) are also returned as roots. */ getTree(): SessionTreeNode[]; /** * Start a new branch from an earlier entry. * Moves the leaf pointer to the specified entry. The next appendXXX() call * will create a child of that entry, forming a new branch. Existing entries * are not modified or deleted. */ branch(branchFromId: string): void; /** * Reset the leaf pointer to null (before any entries). * The next appendXXX() call will create a new root entry (parentId = null). * Use this when navigating to re-edit the first user message. */ resetLeaf(): void; /** * Start a new branch with a summary of the abandoned path. * Same as branch(), but also appends a branch_summary entry that captures * context from the abandoned conversation path. */ branchWithSummary(branchFromId: string | null, summary: string, details?: unknown, fromExtension?: boolean): string; /** * Create a new session file containing only the path from root to the specified leaf. * Useful for extracting a single conversation path from a branched session. * Returns the new session file path, or undefined if not persisting. */ createBranchedSession(leafId: string): string | undefined; /** * Resolve the canonical default session directory for a cwd. */ static getDefaultSessionDir(cwd: string, agentDir?: string, storage?: SessionStorage): string; /** * Create a new session. * @param cwd Working directory (stored in session header) * @param sessionDir Optional session directory. If omitted, uses default (~/.omp/agent/sessions//). */ static create(cwd: string, sessionDir?: string, storage?: SessionStorage): SessionManager; /** * Fork a session into the current project directory. * Copies history from another session file while creating a new session file in the current sessionDir. */ static forkFrom(sourcePath: string, cwd: string, sessionDir?: string, storage?: SessionStorage): Promise; /** * Open a specific session file. * @param path Path to session file * @param sessionDir Optional session directory for /new or /branch. If omitted, derives from file's parent. */ static open(filePath: string, sessionDir?: string, storage?: SessionStorage): Promise; /** * Continue the most recent session, or create new if none. * @param cwd Working directory * @param sessionDir Optional session directory. If omitted, uses default (~/.omp/agent/sessions//). */ static continueRecent(cwd: string, sessionDir?: string, storage?: SessionStorage): Promise; /** Create an in-memory session (no file persistence) */ static inMemory(cwd?: string, storage?: SessionStorage): SessionManager; /** * List all sessions. * @param cwd Working directory (used to compute default session directory) * @param sessionDir Optional session directory. If omitted, uses default (~/.omp/agent/sessions//). */ static list(cwd: string, sessionDir?: string, storage?: SessionStorage): Promise; /** * List all sessions across all project directories. */ static listAll(storage?: SessionStorage): Promise; } export {};