MusePi

会话操作:导出、转储、分享、fresh、fork、恢复/继续

English 中文

本文档描述会话导出/分享/派生/恢复操作当前实现中面向操作者的可见行为。

实现文件

操作矩阵

操作 入口路径 会话变更 会话文件创建/切换 输出产物
/dump 交互式斜杠命令 剪贴板文本
/export [path] 交互式斜杠命令 HTML 文件
--export <session.jsonl> [outputPath] CLI 启动快速路径 不改变运行时会话 无活跃会话;读取目标文件 HTML 文件
/share 交互式斜杠命令 加密分享链接(gist 或分享服务器);自定义 handler 时才临时生成 HTML
/fresh 交互式斜杠命令 是(仅面向 provider 的内存 id/状态) 否;保留当前会话文件/header
/fork 交互式斜杠命令 是(活跃会话身份改变) 创建新会话文件并将当前会话切换到它(仅持久化模式) 存在时把 artifact 目录复制到新会话命名空间
--fork <id\|path> CLI 启动 是(创建会话之后) 从所选源创建新的会话派生到当前 cwd/会话目录
/resume 交互式斜杠命令 是(替换活跃内存状态) 切换到所选的既有会话文件
--resume CLI 启动选择器 是(创建会话之后) 打开所选的既有会话文件
--resume <id\|path> CLI 启动 是(创建会话之后) 打开既有会话;全局跨项目匹配时重新定位(目录已移动)或派生到当前项目
--continue CLI 启动 是(创建会话之后) 打开终端面包屑(目录已移动则重新定位)或最近的会话;若无则新建会话

导出与转储

/export [outputPath](交互式)

流程:

  1. 内置斜杠命令注册表(src/slash-commands/builtin-registry.ts)在 TUI 中把 /export... 路由到 CommandController.handleExportCommand
  2. 命令按空白拆分,只用 /export 之后的首个参数作为 outputPath
  3. AgentSession.exportToHtml() 调用 exportSessionToHtml(sessionManager, state, { outputPath, themeName })
  4. 成功后,UI 显示路径并在浏览器中打开该文件。

行为细节:

注意:

--export <inputSessionFile> [outputPath](CLI)

main.ts 中的流程:

  1. 在交互式/会话启动之前尽早处理。
  2. 调用 exportFromFile(inputPath, outputPath?)
  3. SessionManager.open(inputPath) 加载条目,然后生成并写入 HTML。
  4. 进程打印 Exported to: ... 并退出。

行为细节:

/dump(交互式剪贴板导出)

流程:

  1. CommandController.handleDumpCommand() 调用 session.formatSessionAsText()
  2. 若为空字符串,则报告 No messages to dump yet.
  3. 否则通过原生 copyToClipboard 复制到剪贴板。

转储内容包括:

转储不改变任何会话持久化状态。

分享

/share 发布会话的端到端加密快照并打印查看链接。实现:../packages/coding-agent/src/export/share.ts

阶段 1:自定义分享 handler(若存在)

loadCustomShare()~/.musepi/agent 中检查首个存在的候选:

要求:

若存在且有效,则保留旧契约:会话被导出到一个临时 HTML 文件(${os.tmpdir()}/${Snowflake.next()}.html),handler 收到其路径,之后临时文件被删除。 Handler 结果解释:

关键回退行为:

阶段 2:默认加密分享

仅当未找到自定义分享 handler 时(shareSession()):

  1. 构建会话快照(headerentriesleafId,以及来自 agent 状态的当前 systemPrompt 和 tool 描述)。
  2. 若启用了 share.redactSecrets(默认)且配置了 secrets(secrets.*),secret 混淆器会深度遍历快照中的每个字符串,把配置/发现的 secret 替换为占位符。
  3. JSON 被 gzip 压缩并用一个新鲜的 AES-256-GCM 密钥封存([12B IV][ciphertext+tag])。
  4. 上传目标由 share.store 决定:
    • 分享服务器(默认,store: "blob")——向 <share.serverUrl>(默认 https://my.omp.sh/sPOST 原始 blob,上限 1 MB。超大的快照会被裁剪到能放下为止:先内联图片,再长字符串(32 KB → 8 KB → 2 KB → 512 B 上限),最后是最旧的条目。
    • Secret giststore: "gist")——当 gh 已安装且已认证时,封存 blob 以 base64 编码推送到 session.ompshare.txt(封存预算 5 MB;gist 原始抓取上限 10 MB),当 gh 不可用时回退到分享服务器。
  5. 两种情况下链接都是 <share.serverUrl>/<id>#<base64url key>。那里提供的查看页面抓取 blob(十六进制 id 走 GitHub gist API,其它走服务器的 blob 存储)并在客户端解密;密钥只存在于 URL fragment,绝不出现在任何 HTTP 请求中。

UI 会报告分享 URL(以及在适用时的底层 gist URL 和截断说明)。无头 /share 打印相同的行。与 /export 不同,/share 对内存会话(--no-session)也有效:快照从实时条目构建,无需会话文件。

分享中的取消/中止语义:

Fresh

交互式 /fresh 重置当前会话面向 provider 的流状态,而不触碰本地 transcript、会话文件或 header。当 provider 流被卡住或损坏时(过期的 prompt cache、回合中途故障、或服务端会话 id 漂移),用它来恢复,同时保留你能看到的对话。

AgentSession.freshSession()

因为它保留当前会话文件,/fresh/new(启动一个全新空会话)和 /drop(删除当前会话并新建一个)不同:只有 /fresh 在给 provider 一个干净起点时仍保留可见历史。

Fork

交互式 /fork 从当前会话创建一个新会话,并把活跃会话身份切换过去。

前置条件与即时防护

会话级流程

AgentSession.fork()

  1. 发出 session_before_switchreason: "fork"(可取消)。
  2. 刷新待写入内容。
  3. 调用 SessionManager.fork()
  4. 把 artifact 目录从旧会话命名空间复制到新命名空间(尽力而为;非 ENOENT 的复制失败会记录日志,不致命)。
  5. 更新 agent.sessionId,并继承上一个 provider prompt-cache 键,除非已显式固定 prompt-cache 键。
  6. 发出 session_switchreason: "fork"

SessionManager.fork() 行为:

非持久化行为

CLI --fork <id|path>

启动时的 --fork 在正常会话创建之前解析:

  1. --fork--no-session 一起会被拒绝。
  2. 路径样式的值(/\.jsonl)调用 SessionManager.forkFrom(path, cwd, sessionDir)
  3. 其它值通过 resolveResumableSession(...) 解析:先本地会话,当 sessionDir 未被强制时再做全局搜索。匹配接受小写会话 id 前缀、完整 JSONL 文件名前缀以及去时间戳后的文件名 id 后缀。
  4. 派生的文件在当前 cwd/会话目录作用域中创建,并成为启动时的活跃会话管理器。
  5. 全上下文派生会自动从源 header 的继承键填充 providerPromptCacheKey,回退到源会话 id。当 --model--thinking--system-prompt--append-system-prompt--tools--no-tools 改变了 provider 路由或 prompt/tool 形状时,启动会丢弃该自动继承。

--prompt-cache-key <key> 显式且独立地固定 provider prompt-cache 身份,使其与 OMP 会话 id 和 --provider-session-id 都无关。--provider-session-id 继续控制 provider 会话/路由 header 与粘性凭据选择;--prompt-cache-key 在受支持时控制 OpenAI Responses 的 prompt_cache_key 载荷。

恢复与继续

交互式 /resume

流程:

  1. 打开会话选择器,内容由 SessionManager.list(currentCwd, currentSessionDir) 填充。若当前文件夹没有会话,则预加载 SessionManager.listAll(),选择器直接以全项目作用域打开。
  2. 选择后,SelectorController.handleResumeSession(sessionPath) 调用 session.switchSession(sessionPath)
  3. UI 清除/重建聊天和 todos,然后报告 Resumed session(当恢复的会话属于另一个项目时报告 Resumed session in <dir>,此时进程 cwd 与 cwd 派生的缓存通过 applyCwdChange 重新指向)。

备注:

CLI --resume

--resume(无值)

--resume <value>

createSessionManager() 的解析顺序:

  1. 若值看起来像路径(/\.jsonl),直接打开。
  2. 否则 resolveResumableSession(...) 搜索:
    • 当前作用域(SessionManager.list(cwd, sessionDir)
    • 仅当未提供显式 sessionDir 时才搜索全局会话(SessionManager.listAll()
  3. 匹配接受大小写不敏感的会话 id 前缀、完整 JSONL 文件名前缀,以及 <timestamp>_<sessionId>.jsonl 中时间戳后的 id 后缀。

跨项目 id 匹配行为:

CLI --continue

SessionManager.continueRecent(cwd, sessionDir)

  1. 解析当前 cwd 的会话目录。
  2. 读取终端作用域的面包屑。
  3. 若面包屑指向一个记录在不同 cwd 下、其目录已不存在(已移动/重命名)的会话,当前目录没有自己的会话,则通过 moveTo 把该会话重新定位到当前目录,而不是重新开始。
  4. 否则,若面包屑的 cwd 与当前 cwd 匹配,则使用面包屑会话;否则回退到最近修改的会话文件。
  5. 打开找到的会话;若无,则新建一个会话。

这是仅限启动的行为;没有交互式 /continue 斜杠命令。

会话切换实际上如何改变运行时状态

AgentSession.switchSession(sessionPath) 完成 resume 类操作所用的运行时切换:

  1. 发出 session_before_switchreason: "resume"targetSessionFile(可取消)。
  2. 断开 agent 事件订阅并中止进行中的工作。
  3. 刷新当前会话管理器的写入。
  4. 捕获当前会话、agent 消息、排队中的 steering/follow-up/next-turn 消息、model/thinking/service-tier、MCP 选择、tools 和 system prompt 的回滚状态。
  5. 清空排队的 steering/follow-up/next-turn 消息。
  6. sessionManager.setSessionFile(sessionPath) 并更新 agent.sessionId
  7. 从加载的条目构建会话上下文。
  8. 为目标会话恢复 MCP 选择/tools/system prompt。
  9. 发出 session_switchreason: "resume"
  10. 从上下文替换 agent 消息并同步 todos。
  11. 切换文件时关闭 provider 会话,或同文件重载改变了重放消息时也一样。
  12. 恢复 model(若当前 registry 可用)。
  13. 恢复或初始化 thinking 级别与 service tier。
  14. 重新连接 agent 事件订阅。
  15. 若有,则运行已注册的会话切换 reconciler(交互模式通过 setSessionSwitchReconciler 注册 #reconcileModeFromSession(),以便重新进入如 plan 的持久化模式);reconciler 错误会被记录日志,不致命。

若捕获之后的任一步骤失败,switchSession() 会恢复已捕获的状态并重新连接之前的 agent 订阅,然后再重新抛出。

switchSession() 本身不会创建新会话文件。

事件发出与取消点

切换/派生生命周期钩子

对于 newSessionforkswitchSession

ExtensionRunner.emit() 在第一个返回取消的 before 事件结果时提前返回。

自定义 tool onSession 行为

SDK 把扩展会话事件桥接到自定义 tool onSession 回调:

这些回调是观察性的;它们不会取消切换/派生。

与本文档相关的其它取消面

非持久化(内存)会话行为

当会话管理器以 SessionManager.inMemory()--no-session)创建时:

已知实现注意点(截至当前代码)