Files
oh-my-pi/docs/session-switching-and-recent-listing.md
T
2026-05-31 04:36:14 +02:00

244 lines
11 KiB
Markdown

# Session switching and recent session listing
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`](../packages/coding-agent/src/session/session-manager.ts)
- [`../src/session/agent-session.ts`](../packages/coding-agent/src/session/agent-session.ts)
- [`../src/cli/session-picker.ts`](../packages/coding-agent/src/cli/session-picker.ts)
- [`../src/modes/components/session-selector.ts`](../packages/coding-agent/src/modes/components/session-selector.ts)
- [`../src/modes/controllers/selector-controller.ts`](../packages/coding-agent/src/modes/controllers/selector-controller.ts)
- [`../src/main.ts`](../packages/coding-agent/src/main.ts)
- [`../src/sdk.ts`](../packages/coding-agent/src/sdk.ts)
- [`../src/modes/interactive-mode.ts`](../packages/coding-agent/src/modes/interactive-mode.ts)
- [`../src/modes/utils/ui-helpers.ts`](../packages/coding-agent/src/modes/utils/ui-helpers.ts)
## Recent-session discovery
### Directory scope
`SessionManager` stores sessions under a cwd-scoped directory by default:
- `~/.omp/agent/sessions/--<cwd-encoded>--/*.jsonl`
`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:
1. `getRecentSessions(sessionDir, limit)` (welcome/summary view)
- Reads only a 4KB prefix (`readTextPrefix(..., 4096)` or equivalent direct read for file storage) from each file.
- Parses header + earliest user text preview.
- Returns lightweight `RecentSessionInfo` with lazy `name` and `timeAgo` getters.
- Sorts by file `mtime` descending.
2. `SessionManager.list(...)` / `SessionManager.listAll()` (resume pickers and ID matching)
- Reads the same 4KB prefix per file, not the full JSONL file.
- Builds `SessionInfo` objects (`id`, `cwd`, `title`, `messageCount`, `firstMessage`, `allMessagesText`, timestamps).
- Uses prefix parsing plus marker counting; later messages beyond the prefix may not be present in `allMessagesText`.
- Sorts by `modified` descending.
### Metadata fallback behavior
For recent summaries (`RecentSessionInfo`):
- display name preference: `header.title` -> first user prompt -> `header.id` -> filename
- name is truncated to 40 chars for compact displays
- control characters/newlines are stripped/sanitized from title-derived names
For `SessionInfo` list entries:
- `title` is `header.title` or the last compaction `shortSummary` seen in the 4KB prefix
- `firstMessage` is 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:
1. Read terminal-scoped breadcrumb (`~/.omp/agent/terminal-sessions/<terminal-id>`)
2. Validate breadcrumb:
- current terminal can be identified
- breadcrumb cwd matches current cwd (resolved path compare)
- referenced file still exists
3. If breadcrumb is invalid/missing, fall back to newest file by mtime in the session dir (`findMostRecentSession`)
4. If none found, create a new session
Terminal ID derivation prefers TTY path and falls back to env-based identifiers (`KITTY_WINDOW_ID`, `TMUX_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:
1. Path-like value (contains `/`, `\\`, or ends with `.jsonl`)
- direct `SessionManager.open(sessionArg, parsed.sessionDir)`
2. Resume key value
- `resolveResumableSession(...)` searches local sessions first, then all sessions when `sessionDir` is not forced
- matching is case-insensitive and accepts `id` prefix, 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 matched session cwd differs from current cwd, CLI prompts whether to fork into current project
- yes -> `SessionManager.forkFrom(...)`
- no -> throws error (`Session "..." is in another project (...)`)
No match -> throws error (`Session "..." not found.`).
### `--resume` (no value)
Handled after initial session-manager construction:
1. list local sessions with `SessionManager.list(cwd, parsed.sessionDir)`
2. if empty: print `No sessions found` and exit early
3. open TUI picker (`selectSession`)
4. if canceled: print `No session selected` and exit early
5. if selected: `SessionManager.open(selectedPath)`
### `--continue`
Uses `SessionManager.continueRecent(...)` directly (breadcrumb-first behavior above).
## Picker-based selection internals
## CLI picker (`src/cli/session-picker.ts`)
`selectSession(sessions)` creates a standalone TUI with `SessionSelectorComponent` and resolves exactly once:
- selection -> resolves selected path
- cancel (Esc) -> resolves `null`
- hard exit (Ctrl+C path) -> stops TUI and `process.exit(0)`
## Interactive in-session picker (`SelectorController.showSessionSelector`)
Flow:
1. fetch sessions from current session dir via `SessionManager.list(currentCwd, currentSessionDir)`
2. mount `SessionSelectorComponent` in editor area using `showSelector(...)`
3. callbacks:
- select -> close selector and call `handleResumeSession(sessionPath)`
- cancel -> restore editor and rerender
- exit -> `ctx.shutdown()`
## Session selector component behavior
`SessionList` supports:
- arrow/page navigation
- Enter to select
- Delete to delete after confirmation
- Esc to cancel
- Ctrl+C to exit
- fuzzy search across session id/title/cwd/first message/all messages/path
Empty-list render behavior:
- renders `No sessions in current folder. Press Tab to view all.`
- Enter/Delete on empty do nothing (no callback)
- Esc/Ctrl+C still work
Caveat: the empty-state UI mentions Tab, but this component currently has no Tab handler and current wiring only lists current-scope sessions.
## Runtime switch execution (`AgentSession.switchSession`)
`switchSession(sessionPath)` is the core in-process switch path.
Lifecycle/state transition:
1. capture `previousSessionFile`
2. emit `session_before_switch` hook event (`reason: "resume"`, cancellable)
3. if canceled -> return `false` with no switch
4. disconnect from current agent event stream
5. abort active generation/tool flow
6. clear queued steering/follow-up/next-turn message buffers
7. flush session writer (`sessionManager.flush()`) to persist pending writes
8. `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
9. update `agent.sessionId`
10. rebuild display context via `buildDisplaySessionContext()`
11. restore persisted/discovered MCP tool selections and rebuild active tools/system prompt when discovery is enabled
12. emit `session_switch` hook event (`reason: "resume"`, `previousSessionFile`)
13. replace agent messages with rebuilt context and sync todos
14. close provider sessions when switching to a different session or when same-session reload changed replay messages
15. restore default model from `sessionContext.models.default` if available and present in model registry
16. 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 configured `serviceTier` setting (`"none"` becomes unset)
17. reconnect agent listeners 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(...)`
- clear chat container and rerender from session context (`renderInitialMessages`)
- reload todos from new session artifacts
- show `Resumed session`
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.ts` builds `existingSession = sessionManager.buildSessionContext()`.
- Agent messages are restored once during session creation.
- Model/thinking are selected during creation (including restore/fallback logic).
- Interactive mode then runs `#restoreModeFromSession()` to re-enter persisted mode state (currently plan/plan_paused).
### In-session switch (`/resume`-style selector path)
- Uses `AgentSession.switchSession(...)` on an already-running `AgentSession`.
- Messages/model/thinking are rebuilt immediately in place.
- Hook `session_before_switch`/`session_switch` events are emitted.
- UI chat/todos are refreshed.
- No dedicated post-switch mode restore call is made in selector flow; mode re-entry behavior is not symmetric with startup `#restoreModeFromSession()`.
## Failure and edge-case behavior
### Cancellation paths
- CLI picker cancel -> returns `null`, caller prints `No session selected`, process exits early.
- Interactive picker cancel -> editor restored, no session change.
- Hook cancellation (`session_before_switch`) -> `switchSession()` returns `false`.
### Empty list paths
- CLI `--resume` (no value): empty list prints `No sessions found` and 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 `startsWith` on 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.