import type { Session, ChatMessage, SubtaskMeta } from './types.js'; declare class SessionManager { private sessions; private cleanupTimer?; /** * Per-key promise chain used to serialize writes that perform a * read-modify-write on the in-memory session + persistent JSONL log. * * Without this, two concurrent {@link addMessage} calls on the same key * could interleave: A reads cached session, B reads cached session, both * push a different message into the same array, then both write — the * later write wins and the earlier message is lost from disk (the * in-memory copy is fine because both pushes happened on the same object * reference, but the JSONL log only reflects the most recent appendFile + * meta save). * * The lock is keyed by `${platform}:${channelId}:${threadId}`; different * threads still proceed in parallel. */ private writeQueues; start(): Promise; stop(): void; /** * Run `fn` while holding a per-key serial lock. Subsequent calls with the * same key wait until prior ones settle (success OR failure). Returns the * value of `fn`. Internal use only — keeps {@link addMessage} race-free * without introducing an external dep like p-queue. */ private withLock; /** * Get or create a session for a conversation * Session key: `${platform}:${channelId}:${threadId}` */ getOrCreateSession(platform: string, channelId: string, threadId: string, agent: string): Promise; /** * Get existing session without creating a new one * Returns undefined if no session exists or it's expired */ getExistingSession(platform: string, channelId: string, threadId: string): Promise; /** * Switch the agent for a session. * * Generates a new session id but preserves thread identity AND every * thread-level field that isn't agent-specific: * - usage (per-thread /stats roll-up) * - subtasks/active (subtask state lives at thread level) * - claudeSessionId (Claude UUID survives /oc → /cc round-trips so the * underlying ~/.claude/projects jsonl keeps continuing * when the user comes back to claude) * * `model` and `variant` are reset because they live in different namespaces * across CLIs (`opencode` model ≠ `claude` model); carrying them across * would just feed the new agent an unrecognized argument. */ switchAgent(platform: string, channelId: string, threadId: string, newAgent: string): Promise; /** * Append a message to the session history. * * Performance: instead of re-serializing the entire session JSON every * turn, the message body is appended to a JSONL log file alongside the * metadata (which gets a tiny atomic update for `lastActivity`). * * Concurrency: the entire read-modify-write is serialized per session key * via {@link withLock} so two concurrent calls on the same thread (e.g. * the IM message landing event AND a tool-result event arriving within * the same ms) cannot lose either message to a lost-update race. */ addMessage(platform: string, channelId: string, threadId: string, message: ChatMessage): Promise; /** Remove the current turn's just-appended user message when cancellation * wins during the async history write. Match-last semantics prevent deleting * an unrelated message if another path advanced the session. */ removeLastMessageIfMatches(platform: string, channelId: string, threadId: string, expected: Pick): Promise; /** * Attach a web-chat activity accordion to the latest assistant message * (optionally matched by content). Rewrites the JSONL so refresh can * restore every turn's tools — not only the in-memory ActiveRun. */ patchLastAssistantActivity(platform: string, channelId: string, threadId: string, activity: NonNullable, matchContent?: string): Promise; /** * Persist `model` / `variant` / arbitrary patchable fields. Used by * `/model`, `/think` etc so the change survives a restart between turns. * Mutates the in-memory session in place AND writes metadata atomically. */ patchSession(platform: string, channelId: string, threadId: string, patch: Partial>): Promise; /** * Persist claude-code resumable session bookkeeping (UUID + primed flag). * Returns the updated session, or undefined if no session exists yet for * this thread. Caller is expected to ensure the session exists first. */ setClaudeSessionId(platform: string, channelId: string, threadId: string, claudeSessionId: string): Promise; markClaudeSessionPrimed(platform: string, channelId: string, threadId: string): Promise; /** * Persist opencode's native session id (`ses_…`) once we've seen it in the * adapter's stream. Idempotent — calling with the same id is a no-op so * the per-event callback can fire as many times as opencode sends events. */ setOpencodeSessionId(platform: string, channelId: string, threadId: string, opencodeSessionId: string): Promise; /** * Persist codex's native thread id (UUID) once we've seen it in the * adapter's `thread.started` event. Idempotent — same id may fire multiple * times per spawn. Mirrors setOpencodeSessionId. */ setCodexSessionId(platform: string, channelId: string, threadId: string, codexSessionId: string): Promise; /** * Persist Antigravity's (`agy`) conversation UUID after the first run * creates `~/.gemini/antigravity-cli/conversations/.pb`. The * adapter detects the file via a before/after readdir snapshot and * calls back via `opts.onAgentSessionId(id)`; this setter persists * it so the next turn passes `--conversation `. Mirrors * setOpencodeSessionId / setCodexSessionId. Cleared by /new. */ setAntigravitySessionId(platform: string, channelId: string, threadId: string, antigravitySessionId: string): Promise; /** * Persist Kimi Code's session id after the first run creates or reports it. * Subsequent turns pass it back via `kimi -p --session `. */ setKimiSessionId(platform: string, channelId: string, threadId: string, kimiSessionId: string): Promise; /** * Persist Qoder's stream-json session id after the first run reports it. * Subsequent turns pass it back via `qodercli --resume `. */ setQoderSessionId(platform: string, channelId: string, threadId: string, qoderSessionId: string): Promise; /** * Persist PI's JSON-mode session id after the first run reports it. * Subsequent turns pass it back via `pi --session `. */ setPiSessionId(platform: string, channelId: string, threadId: string, piSessionId: string): Promise; /** * Persist MiMo Code's JSON-mode session id after the first run reports it. * Subsequent turns pass it back via `mimo run --session `. */ setMimoSessionId(platform: string, channelId: string, threadId: string, mimoSessionId: string): Promise; /** * Persist GitHub Copilot CLI's Agim-allocated session UUID. * Subsequent turns pass it back via `copilot --session-id `. */ setCopilotSessionId(platform: string, channelId: string, threadId: string, copilotSessionId: string): Promise; /** * Persist Cursor's stream-json session_id after system.init reports it. * Subsequent turns pass it back via `cursor-agent --resume `. */ setCursorSessionId(platform: string, channelId: string, threadId: string, cursorSessionId: string): Promise; /** * Increment the per-session usage roll-up after a successful agent * invocation. Used by router.callAgentWithHistory to power /stats. */ recordUsage(platform: string, channelId: string, threadId: string, delta: { costUsd: number; costKnown?: boolean; tokensInput?: number; tokensOutput?: number; promptChars: number; responseChars: number; durationMs: number; }): Promise; /** * Reset conversation history (keep session but clear messages) */ resetConversation(platform: string, channelId: string, threadId: string): Promise; /** * Get session with messages (convenience method) */ getSessionWithHistory(platform: string, channelId: string, threadId: string): Promise<{ session: Session; messages: ChatMessage[]; } | undefined>; /** * Create or get a subtask session (independent from parent). */ getOrCreateSubSession(platform: string, channelId: string, threadId: string, subtaskId: number, agent: string): Promise; /** * Set active subtask id on parent session — subsequent messages route to the subtask. */ setActiveSubtask(platform: string, channelId: string, threadId: string, taskId: number | null): Promise; /** * Get subtask metadata list from parent session. */ getSubtasks(platform: string, channelId: string, threadId: string): Promise; /** * Scan all session files on disk and return every subtask, flattened, with * its parent platform/channelId/threadId/agent attached so the dashboard * can render subtasks across all conversations. * * Session files live as `.json` under SESSIONS_DIR. The * sanitized key is one-way (sha256-prefix per non-alnum char), so we * cannot reverse it — but each session file preserves the original * platform/channelId/threadId fields, which is what we need. */ listAllSubtasks(opts?: { agent?: string; }): Promise>; /** * Update subtask metadata in parent session. */ updateSubtask(platform: string, channelId: string, threadId: string, taskId: number, patch: Partial): Promise; /** * Get next subtask id and persist the increment. * * Previously returned 1 when the parent session didn't exist yet, but * never created one — second call returned 1 again, leading to subtask * id collisions. Now we lazy-create the parent session so the counter * increments durably from the first call. */ nextSubtaskId(platform: string, channelId: string, threadId: string, agent?: string): Promise; /** * Persist the full session (metadata + messages). Used for the legacy * one-file format on resetConversation() and switchAgent() — anywhere * the messages array itself was rewritten. Atomic via tmp+rename. */ private saveSession; /** Persist metadata only (no messages payload), atomically. */ private saveSessionMeta; /** Crash-safe write: tmp file + atomic rename. * * Recovers from ENOENT (parent dir missing) by mkdir-recursive + retry * exactly once. This keeps the manager robust to environments where * start() wasn't called yet — tests in particular tend to import * sessionManager without going through the start() lifecycle, and * saveSessionMeta's outer try/catch would otherwise swallow the ENOENT * and silently produce a no-op write (the bug that caused * session-subtasks.test.ts to fail on a fresh CI runner). */ private atomicWrite; private loadSession; private cleanup; } export declare const sessionManager: SessionManager; export {}; //# sourceMappingURL=session.d.ts.map