日期:2026-11-01 | GitHub Trending 日榜第 4 名
手机断网了,开发机上的代理还在修改文件;浏览器重新打开,聊天记录能恢复,刚才那个等待批准的命令却未必还能继续。这些情况比"支持几个模型"更能说明一个代理开发工具的设计质量。
T3 Code 把自己称为 agent harness control surface,即代理运行环境的控制界面。它管理会话、账户、命令确认和工作区回退,却不重新实现 Codex 或 Claude Code,而是在这些运行时之间建立状态边界。
📋 项目概览
| 项目 | 内容 |
|---|---|
| 主仓库 | pingdotgg/t3code |
| 定位 | 在桌面、Web、移动客户端中控制开发机上的 coding agent |
| 执行归属 | 持有代码目录、Git、终端和 provider 凭据的环境服务器 |
| 主要实现 | TypeScript;服务编排使用 Effect,桌面壳使用 Electron |
| 持久状态 | SQLite 事件日志、读模型、命令收据与副作用 outbox;Git 工作区检查点 |
| 典型接入 | Codex、Claude Code、Cursor、Grok Build、OpenCode、Antigravity、fx |
| 许可证 | MIT,保留版权与许可声明 |
🔥 为什么值得关注
代理写代码时,至少有两种进度在同时变化:模型正在做什么,以及工作目录已经变成什么样。用户看到回复结束,不代表差异计算和检查点保存也已经结束;服务器接受了"发送消息",也不代表外部代理已经收到消息。
T3 Code 把这些分歧写进了实现。orchestration-v2/EventSink.ts 将事件、投影、命令收据和副作用意图放进同一个数据库事务;EffectWorker.ts 在事务之后执行外部操作。确认只代表意图提交,而非文件已改完;超时和重试也因此有了明确语义。
另一个值得注意的选择是执行位置。手机和浏览器只是客户端,Git、终端和代理进程仍在服务器所在的环境。连接远程开发机时,不能因为客户端本地存在一个同名目录,就去读客户端的文件;也不能悄悄使用客户端的代理账户替代远端账户。桌面应用虽然内置服务器,其 renderer 仍沿用这一边界。
🏗️ 核心特性
会话历史独立于代理进程
项目中的 thread 是持久的对话与工作历史;provider session 是当前连接的代理运行时。关闭 session 不等于删除 thread,恢复 thread 也不等于原来那个进程仍活着。Glossary 还区分 driver 和 provider instance:前者表示某种集成,后者表示带有配置、账户和生命周期的一份实例。同一种 driver 下的两个账户,不该共享可变会话状态。
统一权限入口,保留底层差异
权限模式位于消息编辑器,线程可以选择 Supervised、Auto-accept edits、Auto、Full access。初始默认是 Full access,第一次体验前应主动检查,而不是把"有审批 UI"等同于默认受限。
相同名称不意味着相同实现。Codex 的监督模式默认映射到 untrusted 审批和 readOnly sandbox;Auto-accept edits 映射到 on-request 与 workspaceWrite;Auto 还使用 auto_review reviewer;Full access 则是 never 和 dangerFullAccess。这些映射在 CodexAdapterV2.ts 中明确列出。
Claude 侧通过 SDK 的 permissionMode、工具集合和 canUseTool 回调协作。计划模式会强制安装权限回调;如果本机 Claude 设置禁止 bypass,T3 Code 会降级,而不是仅凭 UI 上的 Full access 字样强行绕过。对开发者来说,权限模式是代理能力协商的一部分,不是一层独立、通用的操作系统沙箱。
工作区检查点与原生对话回退相互配合
T3 Code 用隐藏 Git ref 保存工作区状态,不把每次代理修改都提交到用户分支。检查点可以支撑轮次差异和文件恢复,但"文件回去了"和"代理忘记了后面的对话"是两个不同操作。
回退必须考虑两者:不支持原生对话回退的 provider,不能先改文件再报错;共享工作区也不能随意恢复整份 checkout。
多端读同一份事实,不要求一直开着页面
客户端共享 packages/client-runtime 的连接与领域状态逻辑,平台只提供存储、凭据、网络和生命周期信号。线程详情订阅与缓存生命周期分开;断线后缓存还能显示,但不会因此被当作实时状态。
🔬 技术架构深度解析
1. 从一次发送消息看提交边界
下面的图围绕 T3 Code 的真实模块展开。虚线表示提交后的工作流,不表示一次跨系统事务。
ThreadCommandExecutor 使用 keyed lock。普通命令按 thread 串行,父线程删除和 side chat 分叉等路径还要处理锁顺序;这不是把整个服务器所有线程都塞进一个全局执行队列。
原生事件经 ProviderEventIngestor 归一化后写入 EventSink,不必绕经命令决策。命令输出增量则走 CommandOutputHub,避免输出洪水阻塞领域事件;并非每帧都落进事件日志。
命令决策形成事件与 effects 后,EventSink.commitCommand 先用 commandId 预留 receipt。如果已经存在,就读回原收据与原命令产生的事件,不再追加一套新意图。新命令则追加事件、更新投影、入队 effects,最后把结果序号写进收据。
这解决的是"相同命令标识的重试"。如果调用方每次重试都生成不同的标识,服务端无法凭自然语言相同就认定它们是同一个动作;幂等性也不等于代理执行具有全局 exactly-once 保证。
EventSink 还对事务提交与后续发布建立顺序约束。原因很具体:客户端依赖递增序号处理变化,如果较大的序号先被发布,后来的较小序号可能被误当成旧数据丢弃。数据库提交顺序和订阅者观察顺序必须配合,不能只说"用了事件总线"就算完成设计。
2. outbox 保证有意图可查,不保证所有动作都能重放
EffectOutbox 将副作用分成可在进程丢失后重放的类型,以及绑定原进程的类型。启动、打断、steer、restart provider turn 和回复运行时请求属于后者;检查点捕获、清理等则位于可重放集合。
进程恢复时,仍为 pending 或 running 的 process-bound effects 被取消。对于可重放类型,原来 running 的记录会退回 pending,并清除 lease owner。这个选择避免了一个危险承诺:服务端重启后把旧的 start 或旧审批响应原样再发一次,就可能在新运行时里产生不属于原先语境的动作。
活着的 worker 则按持久状态认领 effect,增加尝试次数、标记执行者。普通失败默认最多尝试五次,重试延迟采用指数退避并设上限;部分已经由领域状态处理的不可重试 provider 控制失败有单独结算路径,不能一概描述为无限重试。
同一线程的普通 effects 还受前序 pending/running 记录约束。即使前一个 rollback 正在退避,后一个 start 也不能越过它。标题生成有独立的顺序类别,避免一次慢标题阻塞实际代理操作。这是数据库认领条件表达出的次序,不只是内存队列碰巧保持了 FIFO。
3. 重启后的问题与审批不能混为一谈
ProviderRuntimeRecoveryService 会整理未完成 run、session、执行节点和请求,但不会粗暴地清空全部问题。可以通过新消息回答的 responseCapability: message 请求能够保留;某些原本依赖 live 回调、且问题内容完整的用户提问,也可转换到新消息路径。原生子代理线程等情况有额外限制。
相反,依赖旧进程即时回调的审批,不能拿一个"仍显示待处理"的数据库行就继续回复。服务需要关闭这些失效请求、取消旧 effect,再决定是否允许后续工作。
Codex 异步问题尤其容易被误解。它可以通过通知送来问题,回答方式是发一条新的用户消息,不一定存在一个等待返回值的 RPC。T3 Code 把这种问题保存在 turn item 与 runtime request 中,因此 provider turn 结束不自动意味着问题失效。回答命令同时提交请求解决状态与用户消息,重复提交同一命令不会重复发答案。
4. 应用 run 不必等于一个原生 turn
T3 Code 的应用线程、provider thread、session、run 和 attempt 都有自己的身份。用 Codex /goal 时,原生运行时可能在一个 turn 完成后立即自行启动下一个 turn;适配器会把它纳入同一个应用 run。若简单假设一次用户消息必然只对应一个原生 turn,统计时长、取消和回退目标都会产生偏差。
Codex 适配器通过 app-server 控制原生线程,恢复时调用 thread/resume,并以 excludeTurns: true 取得恢复所需元数据,而不是重复搬运整个原生历史。Claude 适配器构造 SDK query options,将 cwd、resume、MCP、权限回调传入;即使空闲释放丢掉了内存中的"打开过"标记,持久 turn 记录仍可提示下一次必须 resume。
当恢复失败或上下文预算不适合继续复用时,ProviderTurnStartService 有创建新原生线程、携带恢复交接上下文的路径。不过这不是逐字迁移原生状态:未完成后台工作等条件会阻止轻率替换,摘要交接也不等于保留原生运行时的全部内部信息。统一会话界面不该被理解成不同 provider 的内存可以无损互换。
5. cwd 在执行环境解析,worktree 不是自动沙箱
RuntimePolicy.layerFromProjectStore 优先使用 thread 的 worktreePath,没有时才读取项目的 workspaceRoot;它还根据实例支持的模式处理权限降级。后续适配器收到的是服务器解析后的策略与路径,而不是任意客户端当前目录。
受 T3 管理的 worktree 若因存储清理而消失,启动服务可以根据记录尝试重建。它先检查分支和可用基点;如果仓库没有可用提交,并不存在一个可以凭空 checkout 的基础。这类失败应保留为启动错误,而非悄悄切到另一个目录执行。
worktree 提供 Git checkout 隔离,不提供进程、网络或密钥隔离。environment-auth.md 明确把项目定义为组织边界,而非文件系统沙箱。文件读取能力可以访问服务器账户可读的绝对路径;WorkspaceFileSystem 对工作区相对路径会检查真实路径与 symlink,但绝对本地路径读取有不同语义。这些规则不能替代 provider 自己的 sandbox 和主机账户权限。
6. 检查点的"隐藏"不代表没有 Git 对象
实际捕获路径经 CheckpointStore 分派到 VCS driver;Git 实现位于 vcs/GitVcsDriver.ts。它建立临时 GIT_INDEX_FILE,在那份 index 中组织待保存内容,调用 write-tree、commit-tree,最后 update-ref。有 HEAD 时,检查点 commit 以 HEAD 为父节点,但用户分支指针不前进。
因此,检查点是真正的 Git 对象,不是把整个目录复制进 SQLite。临时 index 让捕获过程不必改写用户的暂存区;源码还处理 split index、sparse checkout 和尚无提交的嵌套仓库等情况。忽略文件的处理沿用 Git 语义,不能把它当作任意文件的完整备份服务。
恢复的影响比捕获大。Git 路径使用 restore --worktree --staged,随后清理工作区中的未跟踪内容,再在存在 HEAD 时重置相应 index。它恢复的是 checkout 内容,不承诺保留用户原来精细划分的 staged/unstaged 组合。忽略文件通常又不在普通 clean -fd 的处理范围内,项目目录之外的副作用更不会因此撤销。
CheckpointRestoreSafety 因而要求文件恢复使用隔离 worktree,并检查其他未删除线程、检查点 cwd 和相关 session 路径是否重叠;单纯字符串前缀不足以判断,代码使用真实路径和祖先/子目录关系。一个只问问题、没有工具动作且已经空闲的 side chat 有特定豁免,不能把所有 side chat 一概视为独占目录。
即使检查通过,原生对话与文件也不是一个分布式事务。CheckpointRollbackService 在需要回退时先调用 provider rollback,再恢复文件,最后更新持久状态。如果 Git 恢复阶段失败,原生对话可能已经回退。界面报错后应检查实际目录与日志,不能认为"整个操作自动撤销"。这也是检查点不等于事务式文件系统的原因。
7. SQLite 的持久与供电故障下的持久有区别
服务数据库位于状态目录的 statev2.sqlite。默认路径布局区分 userdata 与开发用途的 dev;附件、日志和 worktrees 也各有位置,不都塞在项目仓库里。
persistence/Sqlite.ts 启用 WAL,设置 busy timeout 和外键,并采用 synchronous=NORMAL。该配置降低每次事件提交的同步写开销,checkpoint worker 把相关刷盘工作移出事件循环。代价也写得很明白:应用进程崩溃与操作系统崩溃、突然断电不能混为一谈;后两者可能让最近一次 WAL checkpoint 之后的提交回退。
所以,"收到 accepted 就永远不会丢"仍然过强。更准确的承诺是事件、投影、收据和 outbox 的事务一致性;供电故障耐久性还受 SQLite 配置和存储系统影响。
8. 重连先补事实,历史过大则换快照
EventSink.stream 先订阅实时流,再取 high-water 序号,回放到该位置,随后只接受更晚的 live events。这样可以避免"读完历史才开始订阅"留下的一小段空窗。
线程恢复还限制重放成本。ThreadStream.decideThreadResume 对事件数和编码字节数设门槛:超过 128 个事件或 1 MiB 编码数据,或者客户端游标已经领先服务器,就改走快照;恰好等于门槛仍可重放。原始 payload 的安全预算另有检查,不应将传输体积预算等同于解码后的内存上限。
客户端缓存数据和游标必须一起完成更新,旧订阅取消后不能覆盖新订阅留下的缓存。连接 supervisor 管传输重试,线程和 shell 的同步失败则有自己的状态。健康 WebSocket 上某一个订阅失败,不需要假装整台环境正在重连;反过来,读到离线缓存也不证明服务器已经恢复。
这些机制控制重连工作量,实际成本仍取决于事件密度、工具输出大小、磁盘和网络。
📖 README 核心内容摘要
README 首先要求在执行机器上安装并认证至少一个 provider。T3 Code 管理现有代理,不替用户提供一个免费的通用模型账户;手机完成 T3 的连接授权,也不代表远端 Codex 或 Claude 已经登录。
项目提供命令行服务器、Electron 桌面客户端以及移动与 Web 入口。桌面端带有本地服务器,但连接远程环境时,凭据和文件仍归远端。README 也明确提醒项目仍早期,可能有 bug;贡献政策主要考虑小修复,并不承诺接收大型功能 PR。
开发入口采用 Vite+ 的 vp。仓库根的 Node 固定值是 26.12.1,package manager 字段为 pnpm@12.14.0;服务器包本身声明了更宽的 Node 兼容范围。构建整个仓库时,应服从根目录的工具链,不要只看服务器能运行的最低版本。
另一个容易忽略的文档要求是配对 URL。开发服务器输出的完整配对地址负责让新浏览器获得授权,直接手输一个裸 origin 并不会自动登录。配对链接属于凭据,截图、日志或工单里都不该随手公开。
🚀 快速上手
先用源码开发入口,分清代码与测试目录
先按 README 安装 Vite+,使 vp 可用,再获取源码。从仓库根目录执行:
bash
git clone https://github.com/pingdotgg/t3code.git
cd t3code
vp i
vp run dev
vp run dev 经根脚本进入 scripts/dev-runner.ts,负责协调开发服务器和 Web 客户端。首次打开应使用终端打印的配对 URL。工具链及功能会随分支变化,安装时应同时查看所检出版本的 package.json,不要把服务端包版本当成整个仓库工具链版本。
如果已经安装好与此接口一致的 t3 命令,可用独立数据目录启动本地体验服务器。以下命令不安装后台服务,也不进行 provider 登录:
bash
mkdir -p "$HOME/t3-demo-workspace"
cd "$HOME/t3-demo-workspace"
t3 serve --host 127.0.0.1 --base-dir "$PWD/.t3-demo-state"
serve 不自动打开浏览器,并打印配对信息;它也不会强制把当前目录创建成项目。连接后在 UI 中明确添加这个测试目录。将数据目录放在此处只是为了容易辨认,真实项目应把 T3 状态目录放到源码目录之外。
这份独立数据目录隔离的是 T3 状态,不是主机权限,也不会隔离已有 provider 账户。第一次发送任务前,在编辑器选择 Supervised,确认执行环境和工作目录,且只放入可以丢弃的测试文件。不要用生产仓库验证 Full access。
一个合适的起始任务是:"读取 note.txt,说明它的内容;不要修改文件、运行 shell 或安装依赖。"接着再单独授权一次小改动,观察请求、文件变化和轮次差异。模型是否始终遵守文字要求并没有软件级保证,因此模式选择与实际审批仍然重要。
用 Git 小实验理解隐藏检查点
下面是检查点基本原理的简化实验,不是 T3 的内部 CLI,也没有涵盖 sparse checkout、锁处理或恢复流程。仅在一个新建、可丢弃的空目录中执行;脚本创建 checkpoint-demo,若它已存在就停止。
bash
set -euo pipefail
mkdir checkpoint-demo
cd checkpoint-demo
git init -q
git config user.name Demo
git config user.email demo@example.invalid
printf 'before\n' > note.txt
git add note.txt
git commit -qm initial
head_before=$(git rev-parse HEAD)
printf 'after\n' > note.txt
printf 'temporary\n' > extra.txt
printf 'secret.txt\n' > .gitignore
printf 'not captured\n' > secret.txt
index_before=$(git hash-object .git/index)
export GIT_INDEX_FILE="$PWD/.git/demo-index"
git read-tree HEAD
git add -A -- .
tree=$(git write-tree)
checkpoint=$(git commit-tree "$tree" -p HEAD -m demo-checkpoint)
git update-ref refs/demo/checkpoint "$checkpoint"
unset GIT_INDEX_FILE
test "$head_before" = "$(git rev-parse HEAD)"
test "$index_before" = "$(git hash-object .git/index)"
test "$(git show refs/demo/checkpoint:note.txt)" = after
test "$(git show HEAD:note.txt)" = before
test "$(git ls-tree --name-only refs/demo/checkpoint -- secret.txt)" = ''
git diff HEAD refs/demo/checkpoint -- note.txt
这里能同时看到 HEAD 中的 before 与检查点中的 after,用户分支及正式 index 均未改变,忽略文件也未进入检查点。实验只展示差异,不恢复或清理目录。
对照 T3 Code 的实现,临时 index 负责"怎么捕获",隐藏 ref 负责"怎么找到",而能否安全恢复仍要由线程所有权、原生会话能力和工作区隔离共同决定。
遇到问题先看它属于哪一层
| 现象 | 语义与处理方向 |
|---|---|
| 已收到发送确认,却没有代理输出 | 确认可能只说明数据库提交;继续查看 run、session 和 effect 的状态 |
| 手机断线,任务仍在运行 | 客户端断线不等于服务器或 provider 停止;恢复连接后再确认最新事实 |
| 重启后旧审批失效 | 原进程回调已经丢失,不能靠再次点击恢复;应等待恢复整理或重新发起工作 |
| 原生 turn 已结束,但差异还在生成 | provider 工作与检查点收尾是不同里程碑,不宜把后者算成模型仍在思考 |
| 文件恢复提示共享工作区 | 整份 checkout 可能含别的线程修改;优先考虑只回退对话或改用独立 worktree |
| provider 回退后 Git 恢复失败 | 检查原生对话与实际文件,不要假定跨系统自动回滚;Git index 锁有专门错误解释 |
📊 增长速度与社区热度
11 月 1 日的日榜中,T3 Code 位列第 4。统一的任务、审批与差异界面,以及事件和工作区的分离,是它区别于普通聊天壳的地方。
榜单名次不能用来计算 Star 增速或证明生产可靠性。对高权限工具,更应关注权限映射、重启语义与持久事件兼容;早期项目的贡献限制,也意味着定制团队要考虑维护 fork 的成本。
当日日榜如下:
| 排名 | 仓库 |
|---|---|
| 1 | thedotmack/claude-mem |
| 2 | mattpocock/skills |
| 3 | Donchitos/Claude-Code-Game-Studios |
| 4 | pingdotgg/t3code |
| 5 | Renhuai123/ziwei-doushu |
| 6 | milind-soni/OpenMausBot |
| 7 | pbakaus/impeccable |
| 8 | obra/superpowers |
| 9 | lightpanda-io/browser |
| 10 | HKUDS/DeepTutor |
| 11 | Orkas-AI/Orkas |
| 12 | THU-MAIC/OpenMAIC |
| 13 | bregman-arie/devops-exercises |
| 14 | vorssaint/vorssaint-utils |
🎯 适用场景
如果代理主要在一台开发机或远端工作站上运行,而人需要在桌面、浏览器与手机之间切换,T3 Code 的环境归属模型很合适。代码不用跟着 UI 漂移,远程查看也不会偷偷改变代理运行位置。
同时使用 Codex 和 Claude Code 时,共同的审批和差异入口能减少切换,但权限降级、问题回答与原生恢复能力仍须分别了解。
对于多条并行修改,独立 worktree 比共享一个 checkout 更容易建立恢复边界。若所有线程都在主目录里写文件,就不应期待检查点自动判断每一行究竟属于谁,也不应指望一次 revert 安全地替所有任务协调冲突。
它不适合被直接当成开放给陌生人的多租户执行平台。终端权限相当于服务器用户的 shell,能启动 Full access agent 的权限同样很大;scope 控制和项目目录组织并不提供完整租户隔离。若要承载不可信任务,还需要单独的运行账户、容器或虚拟机、网络与密钥策略。
检查点也不能承担完整灾备:数据库、Git 对象、忽略文件和 provider 原生历史分属不同系统,仍需单独制定备份策略。
💡 总结
T3 Code 最有价值的地方,是把代理控制中的几个容易混淆的事实拆开:意图提交、原生执行、文件变化和客户端观察,都有独立的生命周期。SQLite 事务与 outbox 让"收到命令"有据可查,provider 适配器保留真实协议差异,隐藏 Git ref 则让工作区变化可以比较和有条件地恢复。
这种设计不会让所有失败消失。它能做的是让失败有边界:丢失的回调不冒充可恢复任务,共享 checkout 不冒充独占工作区,收到确认不冒充执行完成。把这些边界看清楚,比多一个模型选项更能帮助开发者决定怎样放心地把代理接入日常代码。