import type { Session } from '../types.js'; /** * The compatibility reader deliberately exposes an empty projection after a * read/parse failure. Destructive callers must use the strict API below so an * unreadable store cannot be mistaken for "there are no durable sessions". */ export declare class SessionStoreUnavailableError extends Error { readonly loadError: Error; readonly name = "SessionStoreUnavailableError"; constructor(loadError: Error); } export declare function stripLegacyPendingCardFields(session: Record): void; /** The active row no longer has the lineage/ownership sampled by the caller. */ export declare class RemoteLineageOwnershipError extends Error { readonly name = "RemoteLineageOwnershipError"; } export type RemoteDurableOwner = { pid: number | null; larkAppId: string | null; backendType: string | null; }; export type ActiveRemoteShutdownSnapshot = { sessionId: string; taskId: string | null; owner: RemoteDurableOwner; }; export type ActiveRemoteLineageBatchUpdate = ActiveRemoteShutdownSnapshot & { targetTaskId: string | null; expectedCurrentTaskIds: readonly (string | null)[]; }; export type RemoteLineageBatchFailureStage = 'prewrite_ownership' | 'prewrite_io' | 'postrename_ambiguity'; export declare class RemoteLineageBatchError extends Error { readonly stage: RemoteLineageBatchFailureStage; readonly sessionIds: readonly string[]; readonly name = "RemoteLineageBatchError"; constructor(stage: RemoteLineageBatchFailureStage, sessionIds: readonly string[], message: string); } export declare function __testOnly_setAfterRemoteBatchRename(hook: (() => void) | undefined): void; /** * Initialise session store for a specific bot (multi-daemon mode). * When appId is set, sessions are stored in `sessions-{appId}.json`. * When unset, uses the legacy `sessions.json`. */ export declare function init(appId?: string): void; export declare function repairMissingChatScope(session: unknown): boolean; /** * Sample every active remote participant from one fresh sessions projection. * Fleet shutdown takes this snapshot before fencing any worker. */ export declare function getActiveRemoteShutdownSnapshotsBatch(sessionIds: readonly string[], options?: { maxWaitMs?: number; }): ActiveRemoteShutdownSnapshot[]; /** * Commit every prepared remote lineage as one compare-and-set transaction. * The published projection is read back under the same lock before workers * are allowed to exit. */ export declare function persistActiveRemoteLineagesExactBatch(updates: readonly ActiveRemoteLineageBatchUpdate[], options?: { maxWaitMs?: number; }): ActiveRemoteShutdownSnapshot[]; export declare function createSession(chatId: string, rootMessageId: string, title: string, chatType?: 'group' | 'p2p', scope?: 'thread' | 'chat'): Session; export declare function getSession(sessionId: string): Session | undefined; export declare function registerSessionBridgeSendMarkerCleanupFence(sessionId: string, fence: Promise): void; /** * Return a row only when it belongs to this process's currently-initialised * bot store. Mutating daemon endpoints must use this instead of getSession(), * whose cross-file fallback is intentionally read-only discovery. */ export declare function getOwnedSession(sessionId: string): Session | undefined; /** Cross-process fresh read ordered after daemon/CLI writes by the shared lock. */ export declare function getSessionFresh(sessionId: string): Session | undefined; export declare function cleanupSessionBridgeSendMarkersNow(sessionId: string): void; export declare function cleanupSessionBridgeSendMarkers(sessionId: string): void; export declare function isValidMojoCloseJournal(value: unknown): value is NonNullable; /** Persist the admission fence before any authoritative Mojo cancel begins. */ export declare function beginMojoCloseJournal(sessionId: string, requestId: string, expectedTaskId?: string): Session; /** Publish irreversible remote-cancel proof before the local close commit. */ export declare function markMojoClosePrepared(sessionId: string, requestId: string, taskId?: string, localResidual?: NonNullable['localResidual']): Session; /** * Finish a failed prepare after worker admission restore. If restore was not * proven, keep a durable uncertain fence; either way retain a newly discovered * pre-init lineage for later reconciliation. */ export declare function finishMojoCloseAbort(sessionId: string, requestId: string, options: { admissionRestored: boolean; taskId?: string; }): Session; /** * Persist a FAILED prepare that must NOT be rolled back, with its exact verdict. * * Such a prepare previously left the journal at `preparing` carrying the * PRE-prepare task id: a restart could not tell "reconcile me" apart from "only * the local commit is left", the lineage the worker actually reported was * dropped, and nothing recorded that write admission was never re-opened. * * `irreversible` is stored as a commit-only `prepared` row on purpose - every * existing recovery path (restore, retry, abort) then treats it as * un-cancellable and finishes only the local close. */ export declare function markMojoCloseUnresolved(sessionId: string, requestId: string, options: { /** * Whether the CLOSE may be retried. `retryable` is a legitimate value here: * a close that keeps writes fenced is not automatically un-retryable, and * forcing it into `uncertain` would forbid the retry that can still succeed. */ recovery: 'retryable' | 'uncertain' | 'irreversible'; taskId?: string; /** Whether a new WRITE may be admitted. Recorded verbatim, never derived. */ admission: 'restorable' | 'fenced'; }): Session; export declare function closeSession(sessionId: string, opts?: { cleanupBridgeMarkers?: boolean; clearRiffParentTaskId?: boolean; /** * Park an uncancellable mojo lineage as PART of this transaction. * * The caller must not pre-write this onto its own Session object: the runtime * object is not always the authoritative row (and when it is, a failed save * would leave a parked id the rollback below does not know about). Merging it * here — against the store's own row, snapshotted and rolled back with * everything else — is what makes "closed + parked" actually atomic. */ parkMojoLineage?: string; /** * Park a LOCAL-subtree residual as PART of this transaction, so an idempotent * re-close of the already-closed row still reports `closed_with_residual`. * The journal (the residual's other home) is wiped on commit below, and a * client that lost the first response and retries would otherwise get a false * all-clear while the containment handle and blocker are still held. */ parkLocalResidual?: 'local_subtree_unprovable_on_platform' | 'local_subtree_boundary_unproven'; }): void; /** * Reactivate one explicitly closed row and discard every queued/setup owner in * the same durable file replacement. The close path has cleared these fields * since 2026-07, but older closed rows can still contain prepared input. A * generic resume is an explicit new lifecycle and must never revive that * abandoned FIFO. * * `previewTarget` is cleared here for the same reason: closeSession() now drops * it, but rows closed by an older build still carry one on disk, and resume * starts a new worker generation that has not registered any port. */ export declare function reactivateClosedSession(sessionId: string): { ok: true; session: Session; } | { ok: false; error: 'not_found' | 'not_closed'; }; export declare function updateSessionPid(sessionId: string, pid: number | null): void; export declare function updateSession(session: Session): void; /** * Persist one exact remote follow-up lineage for an active durable owner. * The process cache changes only after the atomic file replacement succeeds. */ export declare function persistActiveRemoteLineageExact(sessionId: string, taskId: string | null, options?: { expectedCurrentTaskIds?: readonly (string | null)[]; expectedOwner?: RemoteDurableOwner; }): Session; export declare function listSessions(): Session[]; /** * Return the current projection only when its backing file was loaded safely. * Use this for decisions that delete, retire, or reconfigure resources: the * legacy empty-on-error behaviour of listSessions() is unsafe at those gates. * A failed load remains unhealthy until init() explicitly selects/reloads a * store, avoiding a silent mid-transaction recovery against a different view. */ export declare function listSessionsStrict(): Session[]; /** * Cross-file lookup: find every active session attached to a thread, across * all bots. Used when a not-yet-initialized bot is mentioned in a thread that * another bot has already pinned to a working directory — the new bot inherits * the pinned dir instead of re-prompting the user for repo selection. * * Reads other bots' session files directly (best-effort) instead of relying on * any in-memory state, since each daemon process only owns its own bot. */ export declare function findActiveSessionsByRoot(rootMessageId: string): Session[]; /** * Cross-file lookup: find every active chat-scope session for a chat, across * all bots. Mirror of findActiveSessionsByRoot for chat-scope (普通群整群一会话): * lets a not-yet-initialised bot inherit the workingDir from a peer bot that * already has a chat-scope session in the same chat, so a `botmux send * --mention ` in 普通群 can spawn the second bot without bouncing * through the repo-select card. * * Only returns scope='chat' sessions — thread-scope sessions in the same chat * are routed by rootMessageId and not eligible for chat-scope inheritance. */ export declare function findActiveChatScopeSessionsByChat(chatId: string): Session[]; /** * Count active sessions across every bot's on-disk session file. A pure disk * read (no in-memory state) so it's correct at daemon startup regardless of * which bot owns this process — used by the restart-report DM after a restart. */ export declare function countActiveSessionsOnDisk(dataDir?: string): number; /** * Collect every CLI session identity botmux has ever recorded — across ALL bot * store files, ANY status (active or closed). Returns both each session's * botmux `sessionId` (which, for claude-family, IS the on-disk jsonl filename * since botmux spawns with `--session-id `) and its `cliSessionId` (the * CLI-native id after any resume/rotation, e.g. a codex/traex rollout id). * * Used by `/adopt`'s resume-import discovery to hide sessions botmux already * manages — live OR closed — so the picker surfaces only genuinely external * sessions (a CLI the user ran standalone). Closed botmux sessions remain * resumable via their own session-closed cards. */ export declare function collectBotmuxSessionIdentities(dataDir?: string): Set; /** * Read-only snapshot of every session row across the legacy `sessions.json` * and all per-bot `sessions-.json` files. Per-bot rows win duplicate * sessionIds and get `larkAppId` stamped from their filename so a later * offline mutation resolves the owning file. Deliberately lock-free: atomic * tmp+rename publication keeps each file self-consistent, and snapshot * composition must stay a pure reader (an older CLI opportunistically migrated * legacy rows here, which made even `botmux list` a whole-file writer able to * race a daemon save). */ export declare function loadAllSessionsSnapshot(options?: { dataDir?: string; /** Per-bot fallback when the data dir cannot be enumerated (the CLI file * sandbox exposes sessions-.json but NOT a listing of data/). */ fallbackAppId?: string; }): Map; /** * Unlocked point-read of one row straight from disk, bypassing this process's * in-memory cache: the owning per-bot file first, then the legacy * `sessions.json`. Atomic tmp+rename publication keeps each file * self-consistent, so this never blocks on (or throws from) the store lock — * safe on hot paths that only need a freshness hint. */ export declare function readSessionRowFromDisk(sessionId: string, larkAppId?: string, dataDir?: string): Session | undefined; /** * Fail-closed identity scan: every file's copy of one session row across the * legacy and all per-bot files — one entry per file that holds the id. An * unlistable data dir THROWS: a caller proving "this row resolves exactly * once" must not mistake an unreadable store for an empty one. A corrupt * individual file is skipped: an unrelated bot's bad file must neither block * nor impersonate a valid record; the target row still has to resolve from a * readable file. */ export declare function readSessionRowCopiesAcrossStores(sessionId: string, dataDir?: string): Session[]; /** * Locked offline mutation of one exact row in its owning file (per-bot when * the caller-observed row carries `larkAppId`, legacy `sessions.json` * otherwise). Re-reads the row under the SAME lock the owning daemon uses for * every save and hands the FRESH copy to `mutate` — never publishing the * caller's possibly-stale snapshot — then republishes with the daemon's own * tmp+rename / mismatched-key cleanup / legacy-field strip. * * `abortIf` is evaluated under the lock at entry and re-evaluated immediately * before publication; returning true abandons the mutation with `undefined` * (callers pass a daemon-liveness probe so an owning daemon that appears * mid-flight stays authoritative and the file is left untouched). * * Returns the fresh row — mutated when `mutate` returned true, otherwise * unmodified (so `() => false` is a locked fresh read) — or undefined when * the row is absent or `abortIf` aborted. */ export declare function mutateSessionRowOffline(target: { sessionId: string; larkAppId?: string; }, mutate: (current: Session) => boolean, options?: { dataDir?: string; abortIf?: () => boolean; }): Session | undefined; //# sourceMappingURL=session-store.d.ts.map