oh-my-pi 会话切换与最近会话列选完全指南:从 --resume 解析到运行时 switchSession 的底层机制
2026/9/14 9:48:47 网站建设 项目流程

oh-my-pi 会话切换与最近会话列选完全指南:从 --resume 解析到运行时 switchSession 的底层机制

【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi

本文是 oh-my-pi(Coding agent with the IDE wired in)会话子系统的一线技术指南,围绕 docs/session-switching-and-recent-listing.md 展开,系统讲解最近会话发现、--resume/--continue目标解析、会话选择器(TUI)以及运行时会话切换的完整链路。读完你将掌握:会话文件如何按 cwd 分桶存储、两类列表管线(轻量欢迎视图 vs 完整恢复列表)的区别、终端面包屑(breadcrumb)如何决定--continue的目标、启动期 resume 与进程内 switch 两条路径的差异,以及各种失败与边界场景的行为约定。文中所有结论均可在 packages/coding-agent/src/session 下的源码与测试中验证。

一、实现全景:本主题涉及的源码文件

会话切换与最近会话列选不是一个孤立的函数,而是一条横跨存储、列表、CLI、TUI 组件与运行时控制器的完整调用链。以下文件按职责分类,全文将反复引用:

职责文件
会话存储与导航核心(SessionManagerpackages/coding-agent/src/session/session-manager.ts
会话列表扫描与轻量元数据(SessionInfo/RecentSessionInfopackages/coding-agent/src/session/session-listing.ts
磁盘布局、目录编码与终端面包屑packages/coding-agent/src/session/session-paths.ts
单个会话文件的加载 / 迁移 / blob 解析packages/coding-agent/src/session/session-loader.ts
CLI 全屏选择器(selectSessionpackages/coding-agent/src/cli/session-picker.ts
TUI 会话列表组件(SessionListpackages/coding-agent/src/modes/components/session-selector.ts
交互式选择器控制器(SelectorControllerpackages/coding-agent/src/modes/controllers/selector-controller.ts
启动参数解析与 resume 编排packages/coding-agent/src/main.ts
运行时会话切换核心(AgentSession.switchSessionpackages/coding-agent/src/session/agent-session.ts

关于磁盘布局、文件格式与条目(entry)类型的基础知识,请先阅读姊妹文档 docs/session.md,本文默认你已经了解"会话 = JSONL 文件,一行一个条目"这一前提。

二、最近会话的发现机制

2.1 目录作用域:按规范化 cwd 分桶

SessionManager默认把文件会话存放在"规范化 cwd"对应的桶目录下:

~/.omp/agent/sessions/<encoded-cwd>/<timestamp>_<sessionId>.jsonl

其中<encoded-cwd>是对规范化 cwd 做路径编码后的目录名(详见 docs/session.md):

  • 家目录下的相对路径 →-<relative>(如-work-proj);
  • 临时根目录下的相对路径 →-tmp-<relative>
  • 其余绝对路径 →--<encoded-absolute>--

编码前会先做路径等价解析(resolveEquivalentPath),因此 symlink 别名与真实路径会共享同一个桶——这保证了"同一个项目不管从哪个别名进入,都能看到同一批会话"。

在 session-paths.ts 的computeDefaultSessionDir中,每次解析目录还会执行两类最佳努力迁移

  1. 17.2.5–17.2.8 短暂使用的哈希桶方案(<scope>-<project-basename>-<sha256(canonical-cwd)>,已于 17.2.9 回滚)被迁移回路径编码名(修复 issue #7677);
  2. 更早的--<home-encoded>-*--拼写被合并进新的-*格式。

SessionManager.list(cwd, sessionDir?)(session-manager.ts)只读取解析出的那个桶,除非显式传入sessionDir覆盖。

2.2 两条负载不同的列表管线

代码中并存着两套列表实现,它们读取的数据量和产出完全不同:

管线一:getRecentSessions(sessionDir, limit)—— 欢迎页 / 摘要视图

定义在 session-listing.ts:

  • 每个文件只读取4 KiB 前缀
  • 同时理解当前"定宽标题槽(title slot)"文件和旧式"头部优先(header-first)"文件两种格式;
  • 解析出 header + 最早的用户文本预览;
  • 返回轻量RecentSessionInfopathnametimeAgo三个字段);
  • 按文件mtime降序排列。

它刻意不做全目录内容扫描(避免数千个会话时耗时数百毫秒):先列文件、按 mtime 排序,然后仅对最新的limit个文件解析名字。名字优先查history.db标题索引,未索引的旧文件(分支/复刻副本、legacy 会话)才回退做逐文件 header 扫描,且扫描出的标题会回填索引,下次启动即可跳过读取。

管线二:SessionManager.list(...)/listAll()—— resume 选择器与 ID 匹配

  • 每个文件读取4 KiB 前缀 + 有界的 32 KiB 尾部(不读完整 JSONL 主体);
  • 构造完整SessionInfopathidcwd、title/parent 元数据、创建/修改时间、大小、消息预览与计数、生命周期状态);
  • 前缀解析 + 消息标记计数用于列表文本,尾部解析用于推导最终消息的生命周期状态;超出前缀窗口的后续消息不会出现在allMessagesText中;
  • 状态枚举SessionStatuscomplete/interrupted/aborted/error/pending/unknown(session-listing.ts);
  • modified降序排列;SessionManager.listlistAll还会把置顶(pinned)会话排在最前(sortPinnedFirst)。

这两个常量在 session-listing.ts 中定义:SESSION_LIST_PREFIX_BYTES = 4096SESSION_LIST_SUFFIX_BYTES = 32_768。并行度控制同样在此:文件数 ≤ 64 时单线程扫描,超过后按min(16, 可用并行度, ceil(文件数/64))启动有界并行 worker。

性能上还有一个值得注意的细节:扫描结果按stat 身份(mtimeMs + size)键控缓存在 4096 条目的 LRU 中。每次扫描仍会执行一次statSync(它就是失效检查),命中缓存则跳过打开 + 读取 + 解析。两个失效路径都被覆盖:流式追加会增大sizeupdateSessionTitle通过writeSync原地改写定宽标题槽,size不变但mtimeMs更新。不可解析文件的负结果同样会被缓存。

2.3 孤儿备份恢复与只读变体

正常按目录扫描前会先调用recoverOrphanedBackups(session-listing.ts):当主.jsonl文件缺失时,把 EPERM 原子改写回退路径产生的<basename>.jsonl.<snowflake>.bak中最新的一份恢复到主路径,防止崩溃于两次 rename 之间时把用户最后的好状态遗留在加载器视野之外。listSessionsReadOnly则是非变异变体,跳过这一修复步骤。

2.4 元数据回退行为

RecentSessionInfo(最近摘要)的显示名,由sessionDisplayName(session-listing.ts)按优先级决定:

  1. title(显式标题);
  2. 第一条用户消息;
  3. 兜底标签Untitled · <time>

原始id刻意永不使用——它是 UUID,对用户不友好且与相邻会话无法区分。欢迎屏按可用列宽截断渲染名(不设固定长度)。无论标题还是消息派生的名字,都只保留第一行,并剥离控制字符(sanitizeSessionName,session-listing.ts)。

SessionInfo列表条目的字段回退:

  • title:优先定宽标题槽值,否则header.title,否则前缀中能看到的最后一次压缩shortSummary
  • firstMessage:前缀中能发现的第一条用户消息,否则"(no messages)"
  • 选择器还展示:修改时间、文件大小、生命周期状态(unknown除外)、fork 标记,以及 all-projects 作用域下的 cwd。

三、--continue的目标解析与终端面包屑

3.1 终端面包屑的写入与读取

面包屑文件位于:

~/.omp/agent/terminal-sessions/<terminal-id>

内容为两行(原始 cwd + 会话文件路径),可选第三行freshwriteTerminalBreadcrumb(session-paths.ts)是同步 + 最佳努力的:写入频率低(仅在会话创建/切换/重置时,绝非每次 append),且顺序很重要——懒创建的 fresh 会话一旦物化必须立刻被重打为非 fresh,异步乱序写入会导致已物化会话被误标为 fresh。readTerminalBreadcrumbEntry(session-paths.ts)返回TerminalBreadcrumbcwdsessionFileexistsfresh);目标文件缺失时返回null除非第三行是fresh——那是"JSONL 尚未落盘的懒会话边界",此时以exists:false返回,让调用方与真正失效的旧面包屑区分开。

3.2continueRecent的 7 步决议

SessionManager.continueRecent(cwd, sessionDir?)(session-manager.ts)按下述顺序解析目标:

  1. 读终端面包屑~/.omp/agent/terminal-sessions/<terminal-id>);
  2. 验证面包屑:已物化目标可用;目标缺失仅在第三行是fresh(懒创建的/new边界)时可用;
  3. 缺失的 fresh 目标 → 开新会话,而不是回退到复活旧 transcript——这防止了"创建后未产出任何内容就退出"时,重启反而被拉回更早的会话;
  4. 解析过期的 subagent 面包屑到其交互式父会话:resolveBreadcrumbToInteractiveRoot(session-manager.ts)会沿<dir>.jsonl存在的方向向上最多走 8 层,因为 subagent 会话位于父会话的 artifacts 目录(<parent>/<agentId>.jsonl),修复前的面包屑可能指向这样的子会话;
  5. cwd 不匹配且旧 cwd 已消失、且当前位置又没有自己的会话时,把面包屑会话重新扎根到当前 cwd(open+moveTo)——这是 worktree 移动/重命名后的恢复路径;
  6. 否则使用 cwd 匹配当前目录的面包屑;cwd 不匹配时用当前桶中最新会话;
  7. 没有可用面包屑时,按 mtime 选最新文件;没有文件则新建会话。

面包屑写入失败非致命(best-effort)。

3.3-c <value>的归一化

-c唯一位置参数符合会话 id 形状时,它被归一化为显式 resume 目标;否则该位置文本继续作为--continue的初始提示词。

四、启动期 resume 目标解析(main.ts)

4.1--resume <value>:两种模式

createSessionManager(...)处理字符串型--resume

模式一:路径型值(含/\,或.jsonl结尾) 直接SessionManager.open(sessionArg, parsed.sessionDir)

模式二:resume key 值resolveResumableSession(...)(session-listing.ts):

  • 先搜本地(当前 cwd 桶)会话,未命中再搜全局所有会话——除非自定义sessionDir禁用了全局回退;
  • 匹配不区分大小写,接受:id前缀、完整 JSONL 文件名前缀、文件名时间戳之后 id 后缀(sessionMatchesResumeArg,session-listing.ts);
  • 按 modified 降序取第一个匹配,没有歧义提示(多个会话共享前缀时取最新)。

cwd 失效处理:若匹配到的会话记录 cwd 已不存在,CLI 提示Move (re-root) it into the current directory? [Y/n]。接受则openmoveTo(cwd)迁移;拒绝则干净退出;非 TTY 无法应答,抛出SessionResolutionError(main.ts)。

跨项目恢复不 fork:否则会话在其记录的项目中打开——即使匹配来自全局,启动进程也会切换 cwd、重载项目级设置/插件、重新解析启用的模型,然后才构造 agent。它不会因为跨项目匹配就自动 fork 出一个新会话。switchToResumedProject(main.ts)负责这一"进程 cwd 提交 + 插件缓存重置 + 设置重载 + 模型重解析"的过程;其中任何一步失败都会尝试完整回滚到启动目录,回滚也失败则抛SessionResolutionError

无匹配:抛出Session "..." not found.,并提示可用omp --resume(无参)从最近会话中选择,或直接omp新建。

4.2--resume(无值):选择器流程

无值--resume在初始 session-manager 构造完成后处理:

  1. SessionManager.list(cwd, parsed.sessionDir)列当前目录会话;
  2. 若为空,探测SessionManager.listAll()——用于区分"全局全空"状态并预加载 Tab 作用域数据;选择器本身从不自动切到 all-projects 作用域(issue #3099);
  3. 两个列表都空 → 打印No sessions found并退出;
  4. 打开全屏 TUI 选择器(selectSession);
  5. 取消 → 打印No session selected并退出;
  6. 选中 →SessionManager.open(selected.path),然后switchToResumedProject把进程/项目级状态切到会话 cwd(setProjectDir、插件缓存重置、设置重载),并重新解析作用域模型。

4.3--continue

直接使用SessionManager.continueRecent(...),即第三节的面包屑优先行为。

五、选择器内部机制

5.1 CLI 选择器(src/cli/session-picker.ts

selectSession(sessions, options)(session-picker.ts)创建全屏备选屏(alternate-screen)TUI,挂载SessionSelectorComponent,且恰好 resolve 一次

  • 选中 → resolve 所选SessionInfo
  • 取消(Esc)→ resolvenull
  • 硬退出(Ctrl+C 路径)→ 停止 TUI 并退出进程;
  • Tab切换当前目录 / 全项目作用域;all-projects 列表懒加载或由调用方预加载;
  • 搜索:会话元数据/前缀文本 + 短暂 debounce 后的history.db提示历史匹配(historyMatcherHistoryStorage.open()构建,缺失/锁定时降级为纯会话内搜索,不阻塞选择器);
  • 鼠标滚轮切换选中行,左键点击直接选中;
  • Delete(或空搜索时 Backspace)弹出确认后删除 JSONL 及会话 artifacts(deleteSessionWithArtifacts)。

SessionPickerOptions提供allSessionstitlescopeLabelshowCwdallowDeleteallowGlobalScopehistorySearchpinnedIds等控制项;外来会话导入选择器会关闭删除、历史增强与全项目作用域。

5.2 进程内交互选择器(SelectorController.showSessionSelector

流程(selector-controller.ts):

  1. SessionManager.list(currentCwd, currentSessionDir)取当前目录会话;即便目录为空,all-projects 列表仍保持懒加载;
  2. 以全屏备选屏 overlay 形式展示SessionSelectorComponent(通过ctx.ui.showOverlay,左上角锚定、全尺寸,底层 transcript 不被改动),并接入懒加载loadAllSessionshistory.db提示匹配、删除、置顶标记;
  3. 回调:
    • select→ 锁定选择器输入,调用handleResumeSession(sessionPath);成功后隐藏 overlay、恢复编辑器焦点;可恢复的切换前失败会解锁选择器并保持打开;
    • cancel→ 隐藏 overlay、恢复编辑器焦点、重渲染;
    • exit→ 隐藏 overlay 后ctx.shutdown()

/resume <id-prefix>命令先本地后全局解析并直接切换;而/resume @claude/resume @codex打开的是只读源导入选择器:选中的外来 transcript 先持久化为 OMP 会话再切换,且这些选择器不提供删除、历史增强与全项目作用域。

5.3 会话列表组件行为(SessionList

支持:

  • Up/Down 与 Page Up/Page Down 导航(钳制边界、不循环);
  • Enter 选中;
  • 空搜索时 Delete 或 Backspace 触发确认删除;
  • Esc 取消;Ctrl+C 退出;
  • Tab 切换当前目录 / 全项目作用域;
  • 全屏选择器中鼠标滚轮 / 点击;
  • 多 token 搜索:覆盖 id/title/cwd/首条消息/前缀消息文本/路径——字面匹配按新近度优先,其次足够强的模糊匹配;history.db的提示历史匹配在输入停顿后可能被提升到前面。

空列表渲染:

  • 当前目录作用域 →No sessions in current folder. Press Tab to view all.
  • 全项目作用域 →No sessions found
  • 空列表下 Enter/Delete/Backspace 无操作,Esc/Ctrl+C 仍然有效。

六、运行时切换核心:AgentSession.switchSession

switchSession(sessionPath)是进程内切换的主路径。生命周期 / 状态迁移(13 步):

  1. 捕获前一文件,发出可取消的session_before_switchreason: "resume",含目标文件);
  2. 断开 agent 监听器、中止活动工作、运行切换前 reconciler、flush 挂起的 bash/会话写入;
  3. 快照回滚状态(manager、队列、消息、模型/thinking/tier、工具/prompts、provider-cache 身份、checkpoint/rewind 状态),随后清空消息队列;
  4. 切换不同会话时,排干/分离 advisor 记录器;
  5. sessionManager.setSessionFile(sessionPath):更新面包屑、加载/迁移/blob 解析/建索引、在 cwd 策略允许时采纳已记录的 cwd(session-manager.ts);
  6. 同步会话 id、memory key、继承的 provider-cache key、显示上下文与 checkpoint/rewind 状态;
  7. 发出session_switch、替换消息、重置 advisor 会话状态、同步 todos;
  8. 切换不同会话时关闭旧 provider 会话;同会话重载但 replay 发生变化时同样处理;
  9. 按 role/default 回退顺序恢复第一个可用的已记录模型;
  10. 若加载的分支以被打断的工具流结尾,追加一条合成的 abort 消息并重建显示上下文;
  11. 恢复配置的 thinking(auto保持为 auto)与各 family 的 service tier,无对应记录时回退到当前设置;
  12. 按需重置 memory/工具会话状态、重连监听器、运行模式 reconciler、刷新 workspace 感知的基础系统提示词;
  13. 切换不同会话时恢复 advisor 成本、完成 bash 过渡、通知会话变更回调,成功返回true

返回false的场景:before-switch hook 取消,或 cwd 策略拒绝。跨项目切换时若没有 cwd 变更回调则直接拒绝——绝不静默采纳目标 cwd;回调拒绝同样视为取消。

失败恢复:快照之后的任何失败都会恢复之前的 manager 与运行时状态、重连并重新 reconcile、把 bash 过渡标记为失败,然后重新抛出。

七、交互式切换后的 UI 状态重建

SelectorController.handleResumeSession(selector-controller.ts)先调用switchSession

  • 若返回false,选择器在任何新会话 UI 更新之前停止,保持现有会话与 UI 不变(可恢复的切换前失败会解锁选择器并保持打开);
  • 切换成功后才执行 UI 刷新:
    • 停止加载动画;
    • 清空状态容器;
    • 清空 pending 消息 UI 与 pending 工具映射;
    • 重置流式组件/消息引用;
    • 若恢复会话的 cwd 与之前不同,把进程与 cwd 派生的缓存重新指向它(applyCwdChange);
    • 清空聊天容器并从会话上下文重渲染(renderInitialMessages);
    • 从新会话 artifacts 重载 todos;
    • 显示Resumed session(跨项目恢复时显示Resumed session in <dir>)。

也就是说,可见的对话与 todo 状态完全从新的会话文件重建。

八、启动期恢复 vs 进程内切换

8.1 启动期恢复(--continue/--resume/ 直接打开)

  • 会话文件在createAgentSession(...)之前选定;
  • sdk.ts在创建期间构建既有会话上下文;
  • agent 消息与 replay 状态在构造期一次性恢复;
  • 模型/thinking/service tier 使用持久化状态 + 当前配置回退;
  • 交互模式随后 reconcile 持久化的模式状态。

8.2 进程内切换(/resume风格选择器路径)

  • 使用已运行会话上的AgentSession.switchSession(...)
  • 消息/模型/thinking/tier 与会话级运行时状态原地重建
  • 发出session_before_switch/session_switch钩子(扩展与钩子系统分别在 packages/coding-agent/src/extensibility/extensions/types.ts 与 packages/coding-agent/src/extensibility/hooks/types.ts 注册这两个事件);
  • UI 聊天/todos 刷新;
  • 交互模式 reconciliation 通过注册的会话切换 reconciler 运行。

九、失败与边界行为

9.1 取消路径

  • CLI 选择器取消 → 返回null,调用方打印No session selected,进程退出;
  • 交互式选择器取消 → 关闭 overlay,会话不变;
  • 核心钩子或 cwd 策略取消 →switchSession()返回false;交互式选择器在其 UI 刷新/状态路径之前停止,保留旧会话与旧 UI;无回调的跨项目切换被拒绝而非静默采纳目标 cwd。

9.2 空列表路径

  • CLI--resume(无值):只有当前目录全局列表都空时才打印No sessions found并退出;否则空目录选择器会引导按 Tab;
  • 交互式选择器:空目录作用域渲染 Tab 提示且保持可取消。

9.3 目标会话文件缺失 / 无效

当打开或切换到具体路径(setSessionFile)时:

  • ENOENT→ 视为空 → 在该精确路径初始化新会话并持久化;
  • header 损坏/无效(或解析出的条目不可读)→ 视为空 → 初始化并持久化新会话。

这是恢复行为而非硬失败(session-manager.ts 中明确的显式空路径分支会立即物化 header)。注意区分:SessionManager.open损坏 header会在#setSessionFile内抛错,而列表管线中的不可解析文件只是被跳过并缓存负结果。

9.4 硬失败

真正的 I/O 失败(权限错误、改写失败等)仍会抛出并传播给调用方。

9.5 ID 前缀匹配的注意点

  • 匹配用小写化后的会话 id、小写化 JSONL 文件名、文件名时间戳之后的小写化 id 后缀做startsWith
  • modified 降序的第一个匹配胜出;多个会话共享前缀时没有歧义 UI
  • 前缀列表元数据刻意保持轻量,因此搜索文本可能不包含会话文件前 4KB 之外的消息——这正是 CLI 选择器额外叠加history.db提示历史匹配的原因(session-picker.ts 的注释明确指出:这用来恢复 4KB 会话列表前缀永远看不到的提示词)。

十、实战速查

场景推荐命令目标解析路径
回到本终端上次的会话omp --continue终端面包屑 → 最老新鲜边界 → 最新文件 → 新建
按 ID/文件名前缀恢复omp --resume <key>本地桶 → 全局桶(大小写不敏感前缀匹配)
恢复指定文件omp --resume <path>直接open(path)
交互选择最近会话omp --resumeTUI 选择器(当前目录,Tab 切全项目)
会话内切换/resume <id-prefix>switchSession原地重建
导入 Claude/Codex 会话/resume @claude//resume @codex只读导入 → 持久化为 OMP 会话 → 切换

关键常量速记:会话列表前缀 4 KiB、状态尾窗 32 KiB、并行阈值 64 文件、最大 16 worker、扫描缓存 4096 条;session_before_switch可取消、switchSession返回false表示被拒;非 TTY 下 cwd 失效的 resume 抛SessionResolutionError

本文所有机制均以当前仓库实现为准:列表管线细节见 packages/coding-agent/src/session/session-listing.ts,决议逻辑见 packages/coding-agent/src/session/session-manager.ts 与 packages/coding-agent/src/session/session-paths.ts,启动编排见 packages/coding-agent/src/main.ts。若你需要在多项目、多终端并发场景下调试"为什么--continue没回到我想的会话",优先检查~/.omp/agent/terminal-sessions/<terminal-id>面包屑内容与~/.omp/agent/sessions/下的桶目录归属——这两处几乎能解释全部 resume 行为。

【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询