第 26 章 Teleport——远程协作

26.1 Teleport 解决什么问题

想象这个场景:你在公司的开发机上启动了一个 Claude Code 会话,花了 30 分钟让 Agent 理解了你的项目架构、建立了上下文。然后你需要离开办公室,在回家的笔记本上继续这个对话。你不想从头再来——30 分钟的上下文建立过程太昂贵了。

这就是 Teleport 要解决的问题:在不同机器之间无缝迁移 Claude Code 会话

Teleport 提供两个核心能力:

  1. Teleport To Remote:将本地会话"上传"到 Claude.ai 云端,生成一个可在任何地方访问的远程会话。
  2. Teleport Resume:从另一台机器"下载"之前的会话,恢复完整的对话上下文和代码状态。
graph LR subgraph "机器 A: 开发机" A1[Claude Code
本地会话] -->|Teleport To Remote| Cloud[Claude.ai 云端
Session Store] end subgraph "机器 B: 笔记本" Cloud -->|Teleport Resume| B1[Claude Code
恢复的会话] end subgraph "CI/CD 环境" Cloud -->|Teleport Resume| C1[Claude Code
CI Agent] end style Cloud fill:#9B59B6,color:#fff style A1 fill:#4A90D9,color:#fff style B1 fill:#7BC67E,color:#fff style C1 fill:#E8A838,color:#fff

这里最值得强调的一点是:Teleport 迁移的不是一个进程,而是一个可重新物化的工作上下文。这与远程桌面、tmux attach 或 SSH 代理完全不同。那些技术迁移的是"同一个正在运行的会话";Teleport 迁移的是"让另一台机器足以继续这个会话所需的最小充分条件"。这是一种更贴近 Agent 本质的设计,因为 Agent 的核心不是进程 ID,而是上下文连续性。

26.2 Teleport To Remote:上传会话

当你执行 --teleport/teleport 命令时,Claude Code 会将当前会话打包上传到云端。

源码位置:utils/teleport.tsx 中的 teleportToRemote 函数。

整个过程分为几个阶段:

阶段一:前置检查

在 Teleport 之前,系统会进行一系列前置检查:

// utils/teleport.tsx
// 前置错误检查由 getTeleportErrors() 执行
// 包括:Git 是否干净、是否有未提交的变更、是否已安装 GitHub App 等

getTeleportErrors() 返回一组错误类型,如:

  • needsGitStash:工作目录有未提交的变更,需要先 stash 或 commit
  • needsGitHubApp:需要安装 GitHub App 才能使用远程功能
  • 其他错误:认证失败、权限不足等

这些错误通过 TeleportError React 组件展示给用户,用户可以在 UI 中直接解决(比如一键 stash 变更)。

阶段二:生成会话元数据

Teleport 需要为远程会话生成一个有意义的标题和分支名。这里用了一个小巧的设计:调用 Claude Haiku(一个快速的小模型)来生成标题和分支名。

// utils/teleport.tsx
async function generateTitleAndBranch(description: string, signal: AbortSignal) {
  // 使用 Haiku 模型生成 JSON 格式的 {title, branch}
  const response = await queryHaiku({
    systemPrompt: asSystemPrompt([]),
    userPrompt: SESSION_TITLE_AND_BRANCH_PROMPT.replace('{description}', description),
    outputFormat: {
      type: 'json_schema',
      schema: { type: 'object', properties: { title: { type: 'string' }, branch: { type: 'string' } } }
    }
  })
}

分支名以 claude/ 为前缀(如 claude/fix-auth-bug),标题则是一个简洁的人类可读描述。这确保远程会话在 Claude.ai 的 UI 中一目了然。

阶段三:Git Bundle 上传

Teleport 的核心数据传输使用了 git bundle——这是 Git 原生的打包格式,可以将仓库的完整历史(或指定范围)打包成单个文件。

// utils/teleport/gitBundle.ts
export async function createAndUploadGitBundle(
  config: FilesApiConfig,
  opts?: { cwd?: string; signal?: AbortSignal },
): Promise<BundleUploadResult>

Git Bundle 的创建有一个精心设计的三级回退链,处理不同大小的仓库:

graph TD Start["git bundle create --all"] Start --> CheckAll{"--all bundle
<= 100MB?"} CheckAll -->|是| Done1["scope: all
包含完整历史"] CheckAll -->|否| TryHead["git bundle create HEAD"] TryHead --> CheckHead{"HEAD bundle
<= 100MB?"} CheckHead -->|是| Done2["scope: head
当前分支历史"] CheckHead -->|否| TrySquash["git commit-tree + bundle
创建单提交快照"] TrySquash --> CheckSquash{"squash bundle
<= 100MB?"} CheckSquash -->|是| Done3["scope: squashed
无历史,仅快照"] CheckSquash -->|否| Fail["报错:仓库过大"] style Done1 fill:#7BC67E,color:#fff style Done2 fill:#E8A838,color:#fff style Done3 fill:#4A90D9,color:#fff style Fail fill:#D94949,color:#fff

这个回退链的每一级都做了明确的取舍:

  • --all 包含所有分支和标签,最适合但可能很大
  • HEAD 只打包当前分支,减少体积但丢失其他分支
  • squashed-root 通过 git commit-tree 创建一个无历史的单提交快照,最小但完全没有 Git 历史

此外,Bundle 创建还会捕获工作区的未提交变更(通过 git stash create),将其保存为 refs/seed/stash 引用,并在上传后清理这些临时引用——不会污染用户的 git stash 栈。

为什么选择 git bundle 而不是直接 push?

  1. 离线友好:bundle 创建不依赖远程连接,可以在任何环境下生成
  2. 完整性:bundle 包含完整的提交历史和对象,恢复时不需要网络
  3. 原子性:bundle 是单个文件,上传要么成功要么失败,不存在半完成状态
  4. 体积自适应:三级回退链确保即使在超大仓库中也能找到可用的打包方案

更深一层看,Teleport 在这里做的是语义保真,而不是形式保真。它不要求远端拥有与你本地完全相同的仓库形态、stash 栈和临时状态;它要求的是,远端恢复后依然能对"我正在做什么"给出一致理解。这也是为什么可以接受从完整历史逐步降级到 squash 快照:Git 历史是重要信息,但在"继续当前工作"这个目标下,它并不是绝对不可妥协的全部。

阶段四:会话创建

最后,Teleport 调用 Claude.ai 的 API 创建远程会话:

// 伪代码
const response = await createRemoteSession({
  title: generatedTitle,
  branch: generatedBranch,
  gitBundle: uploadedBundleUrl,
  sessionLogs: serializedConversationMessages,
})

返回的 TeleportToRemoteResponse 包含会话 ID 和标题,用户可以在 Claude.ai 上访问这个会话。

26.3 Teleport Resume:恢复会话

Teleport Resume 是 Teleport To Remote 的逆过程——从云端下载会话数据,在本地恢复对话上下文。

源码位置:utils/teleport.tsx 中的 teleportResumeCodeSession 函数。

仓库验证:安全的第一道门

Resume 的第一步不是下载消息,而是验证仓库匹配。这是一个关键的安全设计:

// utils/teleport.tsx
async function validateSessionRepository(sessionData: SessionResource): Promise<RepoValidationResult> {
  const currentParsed = await detectCurrentRepositoryWithHost()
  const currentRepo = currentParsed ? `${currentParsed.owner}/${currentRepo}` : null

  // 从会话数据中提取 Git 仓库信息
  const gitSource = sessionData.session_context.sources.find(s => s.type === 'git_repository')

  // 比较主机和仓库路径
  if (currentRepo !== sessionRepo) {
    return { status: 'mismatch', sessionRepo, currentRepo }
  }
  return { status: 'match' }
}

验证有四种可能的结果:

状态 含义 处理
match 当前仓库与会话仓库一致 继续恢复
mismatch 仓库不匹配 报错,要求用户切换到正确的仓库
not_in_repo 当前不在任何 Git 仓库中 报错,要求用户先 checkout 仓库
no_repo_required 原会话不涉及 Git 直接继续

注意这里还考虑了主机匹配(GitHub.com vs GitHub Enterprise),避免将 GitHub.com 的会话恢复到 GHE 的仓库上。

这个校验看似保守,实际上是在维护一个关键的不变量:会话里的语义必须绑定到正确的现实对象。如果把同样的对话历史恢复到错误的仓库,模型会继续"说得很像对的",但它理解的文件、分支和代码身份都已经错位。Teleport 的难点从来不只是传输成功,而是防止这种"貌似连续、实则漂移"的恢复。

消息恢复与处理

消息恢复的核心挑战是不完整工具调用的处理。如果原始会话在一个工具调用中途被打断,恢复时需要清理这些不完整的消息:

// utils/teleport.tsx
export function processMessagesForTeleportResume(messages: Message[], error: Error | null): Message[] {
  // 1. 反序列化消息(处理特殊格式)
  const deserializedMessages = deserializeMessages(messages)

  // 2. 添加 Teleport 恢复通知
  return [
    ...deserializedMessages,
    createTeleportResumeUserMessage(),   // 告知模型会话从另一台机器恢复
    createTeleportResumeSystemMessage(error)  // 系统级通知
  ]
}

createTeleportResumeUserMessage() 生成一条特殊的用户消息:"This session is being continued from another machine. Application state may have changed."。这条消息确保模型知道环境可能已经变化,不会盲目依赖之前的文件系统状态。

分支检出

如果原会话有关联的 Git 分支,Teleport Resume 会自动检出该分支:

async function checkoutBranch(branchName: string): Promise<void> {
  // 先尝试本地检出
  let { code } = await execFileNoThrow(gitExe(), ['checkout', branchName])

  // 如果本地不存在,从 origin 检出
  if (code !== 0) {
    await execFileNoThrow(gitExe(), ['checkout', '-b', branchName, '--track', `origin/${branchName}`])
  }

  // 确保上游分支已设置
  await ensureUpstreamIsSet(branchName)
}

这里的多重回退策略(本地检出 -> 从 origin 创建 -> 直接跟踪远程分支)展示了处理 Git 操作时的鲁棒性思维。

所以,Teleport 的真正价值不是让 Agent 去云上跑,而是把工作连续性从单机绑定里解放出来。对一个现代 Agent 系统来说,连续性应该绑定在会话语义、代码快照和恢复协议上,而不是绑定在"原来那台机器还活着"这个偶然条件上。

26.4 Teleport 的架构全景

sequenceDiagram participant User as 用户 participant CLI as Claude CLI participant Teleport as Teleport Module participant API as Claude.ai API participant Git as Git Remote rect rgb(240, 248, 255) Note over User,Git: Teleport To Remote User->>CLI: claude --teleport CLI->>Teleport: teleportToRemote() Teleport->>Teleport: generateTitleAndBranch()
调用 Haiku Teleport->>Teleport: createAndUploadGitBundle()
打包 Git 历史 Teleport->>API: 创建远程会话 API-->>Teleport: {id, title} Teleport-->>CLI: 远程会话已创建 CLI-->>User: 显示会话 URL end rect rgb(240, 255, 240) Note over User,Git: Teleport Resume(另一台机器) User->>CLI: claude --teleport CLI->>Teleport: teleportResumeCodeSession() Teleport->>API: fetchSession(sessionId) API-->>Teleport: SessionResource Teleport->>Teleport: validateSessionRepository() alt 仓库不匹配 Teleport-->>User: 报错,需要 checkout 正确的仓库 else 仓库匹配 Teleport->>API: getTeleportEvents() / getSessionLogs() API-->>Teleport: 会话消息 Teleport->>Teleport: processMessagesForTeleportResume() Teleport->>Git: fetchFromOrigin() + checkoutBranch() Git-->>Teleport: 分支已检出 Teleport-->>CLI: 恢复的会话 + 消息 CLI-->>User: 继续对话 end end

26.5 会话数据的双重获取

Teleport Resume 的数据获取有一个渐进式的回退策略:

// 先尝试 CCR v2 端点(Spanner/Threadstore)
let logs = await getTeleportEvents(sessionId, accessToken, orgUUID)

// v2 不可用时回退到 session-ingress API
if (logs === null) {
  logs = await getSessionLogsViaOAuth(sessionId, accessToken, orgUUID)
}

这个双重获取策略反映了实际系统演进中的常见模式:新 API 逐步替代旧 API,但需要保持向后兼容。getTeleportEvents 是新一代端点,getSessionLogsViaOAuth 是遗留端点。当新端点返回 null(未部署或暂时不可用)时,自动回退到旧端点。

API 请求的弹性设计

Teleport 的所有 API 请求都通过 axiosGetWithRetry 封装,实现了自动重试和指数退避:

// utils/teleport/api.ts
const TELEPORT_RETRY_DELAYS = [2000, 4000, 8000, 16000] // 4 次重试,指数退避

重试策略的判断逻辑是:只有瞬态错误才重试(网络断开、5xx 服务器错误),4xx 客户端错误不重试。这意味着如果请求因为认证失败被拒绝,不会浪费 30 秒在无意义的重试上。

这个设计体现了 Teleport 对网络环境的务实态度:用户可能在咖啡厅的 WiFi 上操作,网络可能不稳定,但认证错误不是重试能解决的。

获取到的日志还需要过滤:

const messages = logs
  .filter(entry => isTranscriptMessage(entry) && !entry.isSidechain) as Message[]

isTranscriptMessage 确保只保留对话消息(排除系统事件),!entry.isSidechain 排除侧链消息(如 Agent 内部的子查询)。这确保恢复的上下文是用户可见的"主对话"。

26.6 环境选择与远程执行

Teleport 不仅支持恢复到本地机器,还支持在远程环境中执行。utils/teleport/environments.ts 提供了环境发现和选择能力。

// utils/teleport/environments.ts
export async function fetchEnvironments(): Promise<Environment[]>

这允许用户将 Claude Code 会话 Teleport 到:

  • 本地机器:最常见,从开发机到笔记本的切换
  • 远程开发环境:如 GitHub Codespaces、云 IDE
  • CI/CD 环境:在持续集成中运行 Claude Code 任务

环境选择会检查 preconditions(前置条件),如 GitHub App 是否已安装、认证是否有效等(见 utils/background/remote/preconditions.ts)。

26.7 能学到什么

  1. 会话的可移植性设计:将 AI Agent 的会话设计为可移植的——不绑定到特定机器或进程——是一个强大的架构决策。它要求会话状态是自包含的,可以在任何环境中重建。这需要序列化对话消息、分离环境相关的状态(如当前目录、Git 状态)、并在恢复时重新绑定环境。
  1. 渐进式回退:双重 API 获取策略展示了如何在不牺牲可靠性的前提下迁移到新系统。新 API 优先,旧 API 兜底。当新 API 完全可用后,旧 API 的调用路径自然变为死代码,可以安全移除。
  1. 安全验证的纵深防御:Teleport Resume 不是简单地下载并恢复,而是先验证仓库匹配、检查主机一致性、处理不完整消息。每一层验证都是一道安全门,防止在错误的环境中恢复会话。
  1. 用小模型做元数据生成:用 Haiku 生成标题和分支名是一个经典的"用合适的工具做合适的事"的案例。这类简单的结构化生成任务不需要最强的模型,用快速的小模型既省钱又省时。
  1. Git Bundle 作为传输格式git bundle 是 Git 生态中一个被低估的能力。它提供了一种将完整仓库状态打包为单个文件的方法,非常适合"离线创建、在线传输、远程恢复"的场景。三级回退链(--all -> HEAD -> squashed-root)是一个优雅的"最佳努力"模式:优先尝试最好的方案,失败后逐步降级,每级都有明确的取舍。
  1. 瞬态错误与永久错误的区分:重试机制只对瞬态错误(网络断开、5xx)生效,对永久错误(4xx 认证失败)立即放弃。这种区分避免了在不可恢复的场景下浪费时间重试,同时在可恢复的场景下提供弹性。