This document describes how coding-agent discovers recent sessions, resolves --resume targets, presents session pickers, and switches the active runtime session.
It focuses on current implementation behavior, including fallback paths and caveats.
Implementation files
../src/session/session-manager.ts../src/session/session-listing.ts../src/session/session-paths.ts../src/session/agent-session.ts../src/cli/session-picker.ts../src/modes/components/session-selector.ts../src/modes/controllers/selector-controller.ts../src/main.ts../src/sdk.ts../src/modes/interactive-mode.ts../src/modes/utils/ui-helpers.ts
Recent-session discovery
Directory scope
SessionManager stores sessions under a cwd-scoped directory by default:
~/.omp/agent/sessions/<dir-encoded>/*.jsonl(home-relative-<rel>names,-tmp-<rel>for temp paths, legacy--<abs>--otherwise)
SessionManager.list(cwd, sessionDir?) reads only that directory unless an explicit sessionDir is provided.
Two listing paths with different payloads
There are two different listing pipelines:
-
getRecentSessions(sessionDir, limit)(welcome/summary view)- Reads only a 4KB prefix (
readTextSlices(..., 4096, 0)[0]) from each file. - Parses header + earliest user text preview.
- Returns lightweight
RecentSessionInfo(path,name,timeAgo);nameandtimeAgoare computed eagerly (sessionDisplayName/formatTimeAgo), not lazy getters. - Sorts by file
mtimedescending.
- Reads only a 4KB prefix (
-
SessionManager.list(...)/SessionManager.listAll()(resume pickers and ID matching)- Reads a 4KB prefix plus a bounded 32 KiB tail in one
readTextSlices(...)call per file, not the full JSONL file. - Builds
SessionInfoobjects (id,cwd,title,messageCount,firstMessage,allMessagesText, timestamps, lifecycle status). - Uses prefix parsing plus marker counting for list text, and tail parsing for the final-message lifecycle status; later messages beyond the prefix may not be present in
allMessagesText. - Sorts by
modifieddescending.
- Reads a 4KB prefix plus a bounded 32 KiB tail in one
Metadata fallback behavior
For recent summaries (RecentSessionInfo):
- display name preference (
sessionDisplayName):title-> first user message -> anUntitled · <time>label (the rawidis intentionally never used) - the welcome screen truncates the rendered name to the available column width (no fixed length)
- only the first line is kept and control characters are stripped from title/message-derived names (
sanitizeSessionName)
For SessionInfo list entries:
titleisheader.titleor the last compactionshortSummaryseen in the 4KB prefixfirstMessageis first user message text discoverable from the prefix or"(no messages)"
--continue resolution and terminal breadcrumb preference
SessionManager.continueRecent(cwd, sessionDir?) resolves the target in this order:
- Read terminal-scoped breadcrumb (
~/.omp/agent/terminal-sessions/<terminal-id>) - Validate breadcrumb:
- current terminal can be identified
- referenced file still exists
- If the breadcrumb’s cwd differs from the current cwd, that cwd no longer exists (moved/renamed dir), and the current directory has no sessions of its own, the breadcrumb session is re-rooted into the current directory (
SessionManager.open+moveTo) instead of starting fresh - Otherwise, if the breadcrumb cwd matches the current cwd (resolved path compare), use the breadcrumb session; else fall back to newest file by mtime in the session dir (
findMostRecentSession) - If none found, create a new session
Terminal ID derivation prefers TTY path and falls back to env-based identifiers (ZELLIJ_PANE_ID, TMUX_PANE, CMUX_SURFACE_ID, KITTY_WINDOW_ID, WEZTERM_PANE, TERM_SESSION_ID, WT_SESSION).
Breadcrumb writes are best-effort and non-fatal.
Startup-time resume target resolution (main.ts)
--resume <value>
createSessionManager(...) handles string-valued --resume in two modes:
-
Path-like value (contains
/,\\, or ends with.jsonl)- direct
SessionManager.open(sessionArg, parsed.sessionDir)
- direct
-
Resume key value
resolveResumableSession(...)searches local sessions first, then all sessions whensessionDiris not forced- matching is case-insensitive and accepts
idprefix, full JSONL filename prefix, or the session-id suffix after the timestamp - first match in modified-descending order is used (no ambiguity prompt)
Cross-project match behavior:
- if the matched session’s recorded cwd no longer exists (moved/renamed dir), CLI prompts
Move (re-root) it into the current directory? [Y/n]; yes opens the session andmoveTo(cwd)re-roots it (this also applies to local-scope matches whose recorded cwd is gone) - otherwise, if a global match’s cwd differs from the current cwd, CLI prompts
Fork into current directory? [y/N] - fork accepted ->
SessionManager.forkFrom(...) - either prompt declined -> command cancels (
Resume cancelled: session is in another project.) - non-TTY -> throws
SessionResolutionErrorinstead of prompting
No match -> throws error (Session "..." not found.).
--resume (no value)
Handled after initial session-manager construction:
- list local sessions with
SessionManager.list(cwd, parsed.sessionDir) - if empty: preload
SessionManager.listAll()and open the picker in all-projects scope; printNo sessions foundand exit early only when the global list is also empty - open TUI picker (
selectSession, with optional preloadedallSessions/startInAllScope) - if canceled: print
No session selectedand exit early - if selected: when the session belongs to another project, switch the process into that project’s directory (
setProjectDir, cache resets, settings reload) first; thenSessionManager.open(selected.path)
--continue
Uses SessionManager.continueRecent(...) directly (breadcrumb-first behavior above).
Picker-based selection internals
CLI picker (src/cli/session-picker.ts)
selectSession(sessions, { allSessions?, startInAllScope? }) creates a standalone TUI with SessionSelectorComponent and resolves exactly once:
- selection -> resolves selected
SessionInfo(caller uses.path/.cwd) - cancel (Esc) -> resolves
null - hard exit (Ctrl+C path) -> stops TUI and
process.exit(0) - Tab toggles current-folder / all-projects scope; the all-projects list is loaded lazily via
SessionManager.listAll(or preloaded viaallSessions) - search ranking is augmented with prompt-history matches from
history.db(HistoryStorage.matchingSessionIds) when available
Interactive in-session picker (SelectorController.showSessionSelector)
Flow:
- fetch sessions from current session dir via
SessionManager.list(currentCwd, currentSessionDir); if empty, preloadSessionManager.listAll()and open in all-projects scope - mount
SessionSelectorComponentin editor area usingshowSelector(...), wired withloadAllSessions: () => SessionManager.listAll()and ahistory.dbprompt matcher - callbacks:
- select -> close selector and call
handleResumeSession(sessionPath) - cancel -> restore editor and rerender
- exit ->
ctx.shutdown()
- select -> close selector and call
Session selector component behavior
SessionList supports:
- arrow/page navigation
- Enter to select
- Delete to delete after confirmation
- Esc to cancel
- Ctrl+C to exit
- Tab to toggle current-folder / all-projects scope
- ranked fuzzy search across session id/title/cwd/first message/all messages/path, merged with prompt-history matches from
history.db
Empty-list render behavior:
- current-folder scope renders
No sessions in current folder. Press Tab to view all.; all-projects scope rendersNo sessions found - Enter/Delete on empty do nothing (no callback)
- Esc/Ctrl+C still work
Runtime switch execution (AgentSession.switchSession)
switchSession(sessionPath) is the core in-process switch path.
Lifecycle/state transition:
- capture
previousSessionFile - emit
session_before_switchhook event (reason: "resume", cancellable) - if canceled -> return
falsewith no switch - disconnect from current agent event stream
- abort active generation/tool flow
- flush session writer (
sessionManager.flush()) to persist pending writes, then capture rollback state - clear queued steering/follow-up/next-turn message buffers
sessionManager.setSessionFile(sessionPath)- updates session file pointer
- writes terminal breadcrumb
- loads entries / migrates / blob-resolves / reindexes
- if missing/invalid file data: initializes a new session at that path and rewrites header
- update
agent.sessionId - rebuild display context via
buildDisplaySessionContext() - restore persisted/discovered MCP tool selections and rebuild active tools/system prompt when discovery is enabled
- emit
session_switchhook event (reason: "resume",previousSessionFile) - replace agent messages with rebuilt context and sync todos
- close provider sessions when switching to a different session or when same-session reload changed replay messages
- restore model via
getRestorableSessionModels(sessionContext.models, lastModelChangeRole)— tries the recorded models in fallback order and uses the first one present in the model registry - restore thinking level and service tier:
- thinking uses persisted
thinking_level_change, otherwise the configured default clamped to model capability - service tier uses persisted
service_tier_change, otherwise the configuredserviceTiersetting ("none"becomes unset)
- thinking uses persisted
- reconnect agent listeners, run the registered session-switch reconciler if any (interactive mode re-enters persisted modes; errors logged, not fatal), and return
true
UI state rebuild after interactive switch
SelectorController.handleResumeSession performs UI reset around switchSession:
- stop loading animation
- clear status container
- clear pending-message UI and pending tool map
- reset streaming component/message references
- call
session.switchSession(...) - if the resumed session’s cwd differs from the previous one, re-point the process and cwd-derived caches at it (
applyCwdChange) - clear chat container and rerender from session context (
renderInitialMessages) - reload todos from new session artifacts
- show
Resumed session(orResumed session in <dir>for a cross-project resume)
So visible conversation/todo state is rebuilt from the new session file.
Startup resume vs in-session switch
Startup resume (--continue, --resume, direct open)
- Session file is chosen before
createAgentSession(...). sdk.tsbuildsexistingSession = sessionManager.buildSessionContext().- Agent messages are restored once during session creation.
- Model/thinking are selected during creation (including restore/fallback logic).
- Interactive mode then runs
#reconcileModeFromSession()to re-enter persisted mode state (e.g. plan mode).
In-session switch (/resume-style selector path)
- Uses
AgentSession.switchSession(...)on an already-runningAgentSession. - Messages/model/thinking are rebuilt immediately in place.
- Hook
session_before_switch/session_switchevents are emitted. - UI chat/todos are refreshed.
- Mode re-entry is symmetric with startup: interactive mode registers
#reconcileModeFromSession()as the session-switch reconciler (setSessionSwitchReconciler), andswitchSession()invokes it after reconnecting.
Failure and edge-case behavior
Cancellation paths
- CLI picker cancel -> returns
null, caller printsNo session selected, process exits early. - Interactive picker cancel -> editor restored, no session change.
- Hook cancellation (
session_before_switch) ->switchSession()returnsfalse.
Empty list paths
- CLI
--resume(no value): empty list printsNo sessions foundand exits. - Interactive selector: empty list renders message and remains cancellable.
Missing/invalid target session file
When opening/switching to a specific path (setSessionFile):
- ENOENT -> treated as empty -> new session initialized at that exact path and persisted.
- malformed/invalid header (or effectively unreadable parsed entries) -> treated as empty -> new session initialized and persisted.
This is recovery behavior, not hard failure.
Hard failures
Switch/open can still throw on true I/O failures (permission errors, rewrite failures, etc.), which propagate to callers.
ID prefix matching caveats
- Matching uses
startsWithon the lowercased session id, lowercased JSONL filename, and lowercased id suffix after the filename timestamp. - First match in modified-descending order wins; there is no ambiguity UI if multiple sessions share a prefix.
- Prefix-listing metadata is intentionally lightweight, so search text may not include messages outside the first 4KB of the session file.