ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/9/12 13:20:57 拓冰建站 浏览量
oh-my-pi 会话切换与最近会话列选完全指南:从 --resume 解析到运行时 switchSession 的底层机制 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-piCoding 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.tsCLI 全屏选择器selectSessionpackages/coding-agent/src/cli/session-picker.tsTUI 会话列表组件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中每次解析目录还会执行两类最佳努力迁移17.2.5–17.2.8 短暂使用的哈希桶方案scope-project-basename-sha256(canonical-cwd)已于 17.2.9 回滚被迁移回路径编码名修复 issue #7677更早的--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 最早的用户文本预览返回轻量RecentSessionInfopath、name、timeAgo三个字段按文件mtime降序排列。它刻意不做全目录内容扫描避免数千个会话时耗时数百毫秒先列文件、按 mtime 排序然后仅对最新的limit个文件解析名字。名字优先查history.db标题索引未索引的旧文件分支/复刻副本、legacy 会话才回退做逐文件 header 扫描且扫描出的标题会回填索引下次启动即可跳过读取。管线二SessionManager.list(...)/listAll()—— resume 选择器与 ID 匹配每个文件读取4 KiB 前缀 有界的 32 KiB 尾部不读完整 JSONL 主体构造完整SessionInfopath、id、cwd、title/parent 元数据、创建/修改时间、大小、消息预览与计数、生命周期状态前缀解析 消息标记计数用于列表文本尾部解析用于推导最终消息的生命周期状态超出前缀窗口的后续消息不会出现在allMessagesText中状态枚举SessionStatus为complete/interrupted/aborted/error/pending/unknownsession-listing.ts按modified降序排列SessionManager.list与listAll还会把置顶pinned会话排在最前sortPinnedFirst。这两个常量在 session-listing.ts 中定义SESSION_LIST_PREFIX_BYTES 4096、SESSION_LIST_SUFFIX_BYTES 32_768。并行度控制同样在此文件数 ≤ 64 时单线程扫描超过后按min(16, 可用并行度, ceil(文件数/64))启动有界并行 worker。性能上还有一个值得注意的细节扫描结果按stat 身份mtimeMs size键控缓存在 4096 条目的 LRU 中。每次扫描仍会执行一次statSync它就是失效检查命中缓存则跳过打开 读取 解析。两个失效路径都被覆盖流式追加会增大sizeupdateSessionTitle通过writeSync原地改写定宽标题槽size不变但mtimeMs更新。不可解析文件的负结果同样会被缓存。2.3 孤儿备份恢复与只读变体正常按目录扫描前会先调用recoverOrphanedBackupssession-listing.ts当主.jsonl文件缺失时把 EPERM 原子改写回退路径产生的basename.jsonl.snowflake.bak中最新的一份恢复到主路径防止崩溃于两次 rename 之间时把用户最后的好状态遗留在加载器视野之外。listSessionsReadOnly则是非变异变体跳过这一修复步骤。2.4 元数据回退行为RecentSessionInfo最近摘要的显示名由sessionDisplayNamesession-listing.ts按优先级决定title显式标题第一条用户消息兜底标签Untitled · time。原始id被刻意永不使用——它是 UUID对用户不友好且与相邻会话无法区分。欢迎屏按可用列宽截断渲染名不设固定长度。无论标题还是消息派生的名字都只保留第一行并剥离控制字符sanitizeSessionNamesession-listing.ts。SessionInfo列表条目的字段回退title优先定宽标题槽值否则header.title否则前缀中能看到的最后一次压缩shortSummaryfirstMessage前缀中能发现的第一条用户消息否则(no messages)选择器还展示修改时间、文件大小、生命周期状态unknown除外、fork 标记以及 all-projects 作用域下的 cwd。三、--continue的目标解析与终端面包屑3.1 终端面包屑的写入与读取面包屑文件位于~/.omp/agent/terminal-sessions/terminal-id内容为两行原始 cwd 会话文件路径可选第三行fresh。writeTerminalBreadcrumbsession-paths.ts是同步 最佳努力的写入频率低仅在会话创建/切换/重置时绝非每次 append且顺序很重要——懒创建的 fresh 会话一旦物化必须立刻被重打为非 fresh异步乱序写入会导致已物化会话被误标为 fresh。readTerminalBreadcrumbEntrysession-paths.ts返回TerminalBreadcrumbcwd、sessionFile、exists、fresh目标文件缺失时返回null除非第三行是fresh——那是JSONL 尚未落盘的懒会话边界此时以exists:false返回让调用方与真正失效的旧面包屑区分开。3.2continueRecent的 7 步决议SessionManager.continueRecent(cwd, sessionDir?)session-manager.ts按下述顺序解析目标读终端面包屑~/.omp/agent/terminal-sessions/terminal-id验证面包屑已物化目标可用目标缺失仅在第三行是fresh懒创建的/new边界时可用缺失的 fresh 目标 → 开新会话而不是回退到复活旧 transcript——这防止了创建后未产出任何内容就退出时重启反而被拉回更早的会话解析过期的 subagent 面包屑到其交互式父会话resolveBreadcrumbToInteractiveRootsession-manager.ts会沿dir.jsonl存在的方向向上最多走 8 层因为 subagent 会话位于父会话的 artifacts 目录parent/agentId.jsonl修复前的面包屑可能指向这样的子会话cwd 不匹配且旧 cwd 已消失、且当前位置又没有自己的会话时把面包屑会话重新扎根到当前 cwdopenmoveTo——这是 worktree 移动/重命名后的恢复路径否则使用 cwd 匹配当前目录的面包屑cwd 不匹配时用当前桶中最新会话没有可用面包屑时按 mtime 选最新文件没有文件则新建会话。面包屑写入失败非致命best-effort。3.3-c value的归一化当-c的唯一位置参数符合会话 id 形状时它被归一化为显式 resume 目标否则该位置文本继续作为--continue的初始提示词。四、启动期 resume 目标解析main.ts4.1--resume value两种模式createSessionManager(...)处理字符串型--resume模式一路径型值含/、\或.jsonl结尾 直接SessionManager.open(sessionArg, parsed.sessionDir)。模式二resume key 值走resolveResumableSession(...)session-listing.ts先搜本地当前 cwd 桶会话未命中再搜全局所有会话——除非自定义sessionDir禁用了全局回退匹配不区分大小写接受id前缀、完整 JSONL 文件名前缀、文件名时间戳之后 id 后缀sessionMatchesResumeArgsession-listing.ts按 modified 降序取第一个匹配没有歧义提示多个会话共享前缀时取最新。cwd 失效处理若匹配到的会话记录 cwd 已不存在CLI 提示Move (re-root) it into the current directory? [Y/n]。接受则open后moveTo(cwd)迁移拒绝则干净退出非 TTY 无法应答抛出SessionResolutionErrormain.ts。跨项目恢复不 fork否则会话在其记录的项目中打开——即使匹配来自全局启动进程也会切换 cwd、重载项目级设置/插件、重新解析启用的模型然后才构造 agent。它不会因为跨项目匹配就自动 fork 出一个新会话。switchToResumedProjectmain.ts负责这一进程 cwd 提交 插件缓存重置 设置重载 模型重解析的过程其中任何一步失败都会尝试完整回滚到启动目录回滚也失败则抛SessionResolutionError。无匹配抛出Session ... not found.并提示可用omp --resume无参从最近会话中选择或直接omp新建。4.2--resume无值选择器流程无值--resume在初始 session-manager 构造完成后处理用SessionManager.list(cwd, parsed.sessionDir)列当前目录会话若为空探测SessionManager.listAll()——仅用于区分全局全空状态并预加载 Tab 作用域数据选择器本身从不自动切到 all-projects 作用域issue #3099两个列表都空 → 打印No sessions found并退出打开全屏 TUI 选择器selectSession取消 → 打印No session selected并退出选中 →SessionManager.open(selected.path)然后switchToResumedProject把进程/项目级状态切到会话 cwdsetProjectDir、插件缓存重置、设置重载并重新解析作用域模型。4.3--continue直接使用SessionManager.continueRecent(...)即第三节的面包屑优先行为。五、选择器内部机制5.1 CLI 选择器src/cli/session-picker.tsselectSession(sessions, options)session-picker.ts创建全屏备选屏alternate-screenTUI挂载SessionSelectorComponent且恰好 resolve 一次选中 → resolve 所选SessionInfo取消Esc→ resolvenull硬退出CtrlC 路径→ 停止 TUI 并退出进程Tab切换当前目录 / 全项目作用域all-projects 列表懒加载或由调用方预加载搜索会话元数据/前缀文本 短暂 debounce 后的history.db提示历史匹配historyMatcher从HistoryStorage.open()构建缺失/锁定时降级为纯会话内搜索不阻塞选择器鼠标滚轮切换选中行左键点击直接选中Delete或空搜索时 Backspace弹出确认后删除 JSONL 及会话 artifactsdeleteSessionWithArtifacts。SessionPickerOptions提供allSessions、title、scopeLabel、showCwd、allowDelete、allowGlobalScope、historySearch、pinnedIds等控制项外来会话导入选择器会关闭删除、历史增强与全项目作用域。5.2 进程内交互选择器SelectorController.showSessionSelector流程selector-controller.ts用SessionManager.list(currentCwd, currentSessionDir)取当前目录会话即便目录为空all-projects 列表仍保持懒加载以全屏备选屏 overlay 形式展示SessionSelectorComponent通过ctx.ui.showOverlay左上角锚定、全尺寸底层 transcript 不被改动并接入懒加载loadAllSessions、history.db提示匹配、删除、置顶标记回调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 取消CtrlC 退出Tab 切换当前目录 / 全项目作用域全屏选择器中鼠标滚轮 / 点击多 token 搜索覆盖 id/title/cwd/首条消息/前缀消息文本/路径——字面匹配按新近度优先其次足够强的模糊匹配history.db的提示历史匹配在输入停顿后可能被提升到前面。空列表渲染当前目录作用域 →No sessions in current folder. Press Tab to view all.全项目作用域 →No sessions found空列表下 Enter/Delete/Backspace 无操作Esc/CtrlC 仍然有效。六、运行时切换核心AgentSession.switchSessionswitchSession(sessionPath)是进程内切换的主路径。生命周期 / 状态迁移13 步捕获前一文件发出可取消的session_before_switchreason: resume含目标文件断开 agent 监听器、中止活动工作、运行切换前 reconciler、flush 挂起的 bash/会话写入快照回滚状态manager、队列、消息、模型/thinking/tier、工具/prompts、provider-cache 身份、checkpoint/rewind 状态随后清空消息队列切换不同会话时排干/分离 advisor 记录器sessionManager.setSessionFile(sessionPath)更新面包屑、加载/迁移/blob 解析/建索引、在 cwd 策略允许时采纳已记录的 cwdsession-manager.ts同步会话 id、memory key、继承的 provider-cache key、显示上下文与 checkpoint/rewind 状态发出session_switch、替换消息、重置 advisor 会话状态、同步 todos切换不同会话时关闭旧 provider 会话同会话重载但 replay 发生变化时同样处理按 role/default 回退顺序恢复第一个可用的已记录模型若加载的分支以被打断的工具流结尾追加一条合成的 abort 消息并重建显示上下文恢复配置的 thinkingauto保持为 auto与各 family 的 service tier无对应记录时回退到当前设置按需重置 memory/工具会话状态、重连监听器、运行模式 reconciler、刷新 workspace 感知的基础系统提示词切换不同会话时恢复 advisor 成本、完成 bash 过渡、通知会话变更回调成功返回true。返回false的场景before-switch hook 取消或 cwd 策略拒绝。跨项目切换时若没有 cwd 变更回调则直接拒绝——绝不静默采纳目标 cwd回调拒绝同样视为取消。失败恢复快照之后的任何失败都会恢复之前的 manager 与运行时状态、重连并重新 reconcile、把 bash 过渡标记为失败然后重新抛出。七、交互式切换后的 UI 状态重建SelectorController.handleResumeSessionselector-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 后缀做startsWithmodified 降序的第一个匹配胜出多个会话共享前缀时没有歧义 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-prefixswitchSession原地重建导入 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),仅供参考