LoopX 2.0 发布了。借这个节点,我想把 TypeScript 重构的历史和技术细节整理出来。
正式迁移 RFC 在 8 月 15 日合入。到 10 月中旬集中清理普通旧 writer,这项工程已经跨了差不多两个月。它有多条并行线:事务规则迁移、调用方收敛、共享权威、存储与历史成本、Semantics,以及后续兼容与退役。
最后 3 位开发者与各自 Agent 接力的 #6396,是这些工作累积后的一次集中收尾。前面的选择和转折,决定了最后能删什么,以及删掉以后还能恢复什么。
其中,新 maintainer Song(@songoow) 从 9 月中旬持续推进 Semantics RFC、词表收敛、生产证据和检查器修复。这部分也值得与事务、存储放在一起讲。
1. 先还原历史:迁移方案本身也在变化
LoopX 保存长程 Agent 的目标、任务、证据与执行边界。模型可以提出新路线,控制面要保护版本、执行权和状态提交,并在中断后找到正确的恢复位置。
最初的大量实现是 Python,TS 的宿主、Pi 集成与 dashboard 已经存在。通过一次 Python 调用来做决策,再把结果接回 TS,是自然的起点。随着规则增多,默认值、状态解释、调用顺序和持久化也可能分散在两边。
我们希望这些控制面规则有清楚的 typed owner,同时让 CLI、宿主和 App 共享合同。实际过程可以分成以下几个互相重叠的阶段。
| 时间 | 主要变化 | 代表证据 |
|---|---|---|
| 8 月 15 日 | 正式提出进程边界上的渐进迁移,初稿偏向先做 read/projection | #3226 |
| 8 月 21–22 日 | 先切 Effect Program,再从叶子规则试点转向完整事务 | #3416、#3447 |
| 8 月下旬–9 月上旬 | settlement、Todo、quota、lease、Vision、monitor 等事务逐步切换;共享权威并行发展 | #3464、#3798 |
| 9 月上中旬 | 接真实调用方,收敛规划、读取与 provider 打开规则;接入可选 SQLite | #4178、#4317、#4121 |
| 9 月中旬起 | Song 推进 Semantics;存储线处理 checkpoint、delta、写入与 replay 成本 | #4433、#4408 |
| 9 月下旬 | 退役未使用的 Python authority prototype 和实验性 Todo event overlay;保留真实兼容边界 | #5105、#5054 |
| 10 月上中旬 | 新建默认值更新,普通旧 writer 隔离,实际 consumer 路由收敛 | #5805、#6104、#6280 |
| 10 月中旬 | 集中退役普通旧写入路径,继续修恢复、producer 与集成反例 | #6396 |
8 月原始提案考虑过 read-first 和分阶段 dual-read/dual-write。后来接受的方案改为 Effect Program 先行,再按完整事务替换、删除旧规则,生产路径只有一个语义 owner。最初提案中的阶段设想,不能直接当成 2.0 的运行时设计。
这种变化很正常。重要的是把为什么改路线、已经切到哪里、哪些仍是 reference 或候选写清楚。当前维护中的 TS RFC 记录了这些检查点。
2. 只统计核心 control plane:研发规模与代码变化
为了把整段工程的规模说清楚,这里固定一个可复算的窄范围:loopx/control_plane/**。其他目录的 App、benchmark、文档、测试套件不计入核心代码量。这个目录自身的测试支持工具仍在目录口径内。
基线是正式 TS RFC 合入前的 d1fe059,终点是 10 月 16 日的 488e443。从正式 RFC 到终点约 62 天,跨 63 个日历日期;其中 62 个日期有核心目录的主干提交。起止源码对照可以复查版本。
整个周期的研发活动
| 指标 | 核心目录口径 |
|---|---|
| 触及核心目录的主干落地提交,按第一父链去重 | 1,144 |
| 可核验关联的已合并 PR,按 PR 编号去重 | 1,121 |
| 这些 PR 的作者账号 | 56 |
| 本次 API 快照中的 review 记录 | 2,329 |
| 本次 API 快照中的会话评论,不含行内 review comments | 1,631 |
| 有 review 记录的关联 PR | 1,031 / 1,121 |
| PR 创建至合并的中位历时 | 5.0 小时 |
| PR 创建至合并的 P90 / P95 历时 | 25.4 / 45.0 小时 |
这组范围包含同期的迁移、新能力、修复与维护。56 是 GitHub 账号数,其中有自动化与共享账号;它不等于 56 个人或 56 个 Agent。review 数也包含自审、重复审查等记录,部分讨论发生在合并后。合并历时包含等待,不能换算成有效研发工时。
新 maintainer Song 在这个窄目录口径下关联 53 个 PR。他在 loopx/semantics/ 等目录的贡献没有全部进入这个数,具体技术归属仍以本文的贡献说明和原 PR 为准。
代码结构与累计变化量
下表按物理行统计,包含注释和空行;列出的文件量只计相应语言源码。
| 核心目录源码 | 基线 | 取证终点 | 期间累计新增 / 删除 |
|---|---|---|---|
| Python | 256 个文件;80,473 行 | 471 个文件;142,371 行 | +120,899 / −59,001 行 |
| TypeScript | 0 个文件;0 行 | 323 个文件;87,268 行 | +104,769 / −17,501 行 |
| Python + TypeScript | 80,473 行 | 229,639 行 | +225,668 / −76,502 行 |
TS 占终点这两种语言源码物理行数的 38.0%。这是目录里的语言结构,不能当作“38% 的控制面已完成迁移”:一部分旧入口本来就在目录外,迁移中也有模块归位、新能力与测试支持代码进入。
累计新增和删除按主干每次落地差分相加,同一行反复修改会重复计数;两端存量只看各自快照。本次将累计净变化与端点差分、源码存量变化逐项对齐。这里能看到约 30.2 万行次的 Python/TS 改动,不能据此计算独立产出、质量或模型生产率。
节奏:这段工程怎样持续推进
| 北京日期,按周分桶 | 核心主干提交 | 关联合并 PR | 当周 PR 作者账号 |
|---|---|---|---|
| 08-15—08-21 | 52 | 48 | 10 |
| 08-22—08-28 | 87 | 86 | 14 |
| 08-29—09-04 | 61 | 61 | 16 |
| 09-05—09-11 | 92 | 92 | 14 |
| 09-12—09-18 | 133 | 133 | 14 |
| 09-19—09-25 | 150 | 147 | 10 |
| 09-26—10-02 | 161 | 161 | 17 |
| 10-03—10-09 | 216 | 214 | 12 |
| 10-10—10-16 | 192 | 179 | 14 |
首末周只覆盖统计起止时间以内的部分。周内按合并时间计 PR、按提交时间计主干提交,两种时间可能不同,因此不要把两个数逐行当成一一对应。
再按标题前缀观察,fix 为 658,feat 为 208,refactor 为 137,perf 为 28,其他为验证、文档或未按这些前缀命名的工作。它们是标题分类,不能把其中某一类当作全部迁移工作。
这些数据让我更愿意把后面的机制按阶段讲:规则搬迁之后,还有调用方接入、语义修复、恢复验证和旧路径退役。最后的 #6396 是整个过程中的一个集中切片。
统计如何复查
公开安全的PR 清单、核心主干提交清单和指标 JSON随稿提供。PR 集合来自触及该目录的主干提交与 GitHub 合并元数据的对应关系;同一 PR 只计一次。2 次主干提交没有可核验的合并 PR 对应,仍计入提交和代码变化量。
取证中先枚举公开 PR,再用 Git 主干反查;直接读取补齐了搜索索引漏掉的 14 个 PR 元数据。源数据与处理脚本保留在本次调研材料中。没有从这份盘点推算 token、模型费用、CI 算力、研发工时或 benchmark 收益。
3. Effect Program 先行:先统一“做过什么”
第一次有界切换发生在 8 月 21 日的 #3416。TS 接下 Effect Program、settlement algebra、Turn journal 的解释,以及持久 checkpoint effect。
这组边界之所以连在一起,是因为恢复要回答同一个问题:这笔工作有哪些 effect,哪些已经发生,下一个合法动作是什么?
一个 checkpoint 不能只是“把 JSON 写回文件”。操作身份、同键互斥、旧内容匹配、原子替换与持久化顺序,都影响重试能否识别原结果。把这些规则拆在多个运行时,下一次修恢复就要同时追几份实现。
因此这次切换把内部 effect 及其规则交给 TS。Python 保留兼容调用和 callback/provider 边界。托管的本地 runtime 按需启动、空闲退出,使用私有 token 与源码指纹处理生命周期;调用者不必手工管理一套 daemon。
这项设计也有成本。PR 中同一历史 checkpoint 工作负载的测量如下:
| 路径 | p50 | p95 |
|---|---|---|
| 旧 checkpoint | 0.755 ms | 2.020 ms |
| 新的持久 checkpoint 路径 | 12.597 ms | 18.610 ms |
新路径增加了 RPC 与 durability 工作。这个比较说明第一次切换付出了什么,不能推导“TS 比 Python 慢”或今天整个 CLI 的延迟。先承认成本,才有条件检查后续完整事务是否消除了更多往返和重复判断。
4. 迁移完整事务:一次少到两次,也可能是正确答案
8 月 22 日的 #3447 明确停止继续平铺叶子 helper。原因很实际:每搬一个函数都加 facade、DTO 和 parity 脚手架,Python 仍决定组合顺序,生产链反而变长。
新的单位是一笔可观察事务。比如 Turn settlement,需要把输入解码、身份、journal、待执行 effect、结果归约和 closeout 放到一个 owner 下。Todo completion 同样牵动准入、状态、lease、continuation 和提交回执。
#3464 把 settlement 的 7 次叶子 runtime 调用收敛成粗粒度 turn.settlement.reduce。已交付切片中的路径是:
新 provider 工作:TS preflight → Python provider / checkpoint → TS final reduction
原结果重放: TS reduction / 原结果恢复
两次与一次,分别对应有新 provider 工作和重放。图中的职责也不代表整个 CLI 只会发生这些调用。
评审曾发现一个有诱惑力的错误优化:只保留最后一次 reduction。这样调用更少,但畸形身份或 journal 可能直到外部 provider 执行之后才被拒绝。最终保留 preflight,由 TS 先授权准确的待执行 effect,Python 只执行这些 effect,再返回最终归约。
这个负例让我更看重“检查放在什么位置”。一个正确判断,如果发生在不可逆 effect 之后,也可能保护不了用户。
后续 task lease、Vision、monitor-poll、spend/void 等切片也沿完整事务推进。迁移账本需要记录:旧规则删了什么;新 bridge 加了什么;happy/recovery path 跨几次运行时;哪些脚手架可以退役;完整入口的正确性和延迟怎样。代码净增可以有理由,但要说清楚多出的代码承担什么。
5. 共享权威:领域规则和存储原语分别归属
事务迁移时,共享权威也在发展。从 wchwawa 推进的协调核心、aggregate/provider 与可恢复 reference,到 #3798 的 TS AuthorityStore,逐渐收敛出一个 provider-neutral 的持久化合同。
这里的关键是把两类问题分开:
- 领域 owner 判断一次状态迁移是否合法、谁有执行权。
- store 保证条件提交、持久化结果与证明的读取。
当前 AuthorityStore 提供 storeIdentity、loadAuthority、commitAuthority、readReceipt、scanCommitted 等操作。store 原子地保护 head、事件和回执;领域规则不能悄悄移进某个数据库 adapter。
几种版本必须分别解释:
| 标识 | 保护的边界 |
|---|---|
provider_revision |
provider 上的一次条件提交;其表示对领域调用方可以不透明 |
authority_revision |
领域权威状态的版本 |
lease_epoch |
一轮执行权的代际,防止旧 executor 继续写 |
| store identity / lineage | 当前读取的是否仍是同一条权威历史 |
| operation identity / receipt | 是否为同一笔已生效操作,原结果是什么 |
例如 PostgreSQL 在 COMMIT 后断连,结果可能已经持久化。provider 必须区分 missing、unavailable、failed 与 ambiguous durability。不能因为客户端没有收到成功,就当成没有提交。后续按原操作身份查 receipt,才有机会安全恢复。PostgreSQL candidate 的边界 也明确保留了部署与采用资格。
早期 Python executor 有 reference/coverage 作用,后来未使用的 prototype 被删除;实际 native File/SQLite 路径成为对应资格验证对象。保留过渡实现有期限,不能让 reference 悄悄变成第二套生产权威。
6. SQLite 之后:长程历史也要有成本模型
LIHUA919 的 #4121 把可选 SQLite provider 接到既有 TS authority 合同。一次事务原子提交 head、receipts、events 和 outbox;claim/lease 的领域语义仍从原 owner 来。
接入数据库以后,还有三个不同的工程问题。
第一,当前 head 的成本不能跟总历史一起长。 #4408 把每次保留完整 projection 改为经过验证的周期 checkpoint 与已提交 delta,收敛 codec owner,提供事务性、幂等的旧格式迁移。当前读取、恢复连续性和完整审计有不同的读取范围。
第二,写入量要测对。 #4778 分别观察 logical write volume、窗口内累计 WAL 流量和应用观察到的锁等待。最终数据库文件多大,不能替代写放大测量;一次 rehearsal 也不能直接叫容量或持续运行验收。
第三,replay 的对象所有权会影响正确性。 #4931 减少未改变子树的复制、排序、编码,以及 receipt lookup 中无用的完整 projection 构造。同时保证返回给调用方的对象被修改后,不会污染下一次 replay 的内部基础状态;持久证明字节也保持不变。
这轮优化在一些路径上有明显收益,但大状态、扫描延迟和内存成本仍有缺口,PR 保留了未达到的目标。把这些差异留下,比给所有路径写一个统一“性能提升”更有用。
10 月的 #5805 又更新了未配置的新建 Goal 默认值,选择 canonical SQLite 与 hard lease。已有 Goal、显式选择和旧 operation retry 继续遵守各自合同。新建默认值、旧数据迁移、旧 writer 退役,是三件发生在不同阶段的事。
7. Song 的 Semantics:定义、生产、解释要接起来
新 maintainer Song(@songoow) 从 9 月 15 日的 #4433 发起并实现 Semantics RFC、registry 和提交路径检查,之后持续推进收敛。这条线让我们开始系统约束“概念怎样增长”。
跨语言迁移很容易产生重复知识:同名状态在 Python Enum、Literal、TS as const、展示层和判断分支中各出现一次。即使今天字符串一样,明天也可能有一处多加了值或换了解释。
Semantics RFC 将检查拆为可追溯关系:定义 owner、值域、生成绑定、producer、interpreter/projection。运行时从类型化 owner 取规则,登记表做约束与检查,不成为线上规则的另一个来源。
一个字段混入四种东西
#4494 处理过 effective_action 的混用:quota 决策、frontier 动作、replay 判决和诊断错误码放在同一字段,调用方难以知道该按哪套域解释。
收敛后,判决放 decision,诊断放 error_code,根动作限定为已登记且不相交的 quota/frontier 并集。跨运行时绑定从 owner 生成;旧 v0 签名计划保留原字节语义,避免改写历史导致原身份核验失败。
同一改动还抓到 skip/quota_skip:消费分支存在,不能证明 producer 会输出那个值。需要追到支持位点上的真实赋值与返回,而不是把比较字符串也算作生产证据。
检查器也需要能推翻自己的反例
#4687 保留了评审中有价值的反例,修复三种“自信地答错”:全局重绑定被漏掉,字典 spread 后修改仍被当成旧初始化值,遮蔽的 String 被误认成内建转换。
这里误报与漏报的代价不同。保守扩大候选值可能多报一个问题;漏掉真实生产值,会让未登记输出通过检查。没有把握的动态形式应留下 unknown。
检查还要有扫描覆盖下限,不能靠少扫文件过线。后来删除持续膨胀的已提交 inventory,改成按需计算全树清单,也没有取消这个义务。当前支持形式之外仍有缺口,跟踪 issue 继续保留。
这项工作让我很认同一个实践:自动生成和自动检查,可以减少同步错误;它们自己的前提、覆盖和错误方向,也需要被审查。
8. 调用方和默认值,决定迁移能否到用户手上
完整事务需要真实 producer 接入。9 月的 #4178 将非终态规划更新收敛到一个权威快照与 TS transaction,删除 Python 合成 Markdown、再解析和事务前 target 读取的绕路。
省略、null、空值、false 在这一层都有含义。dry-run 要准确模拟,不能因为选定 provider 不可用就回退别处。随后 #4279 也收敛了读取分类与投影交付语义;部分 Python read owner 被有意保留。一个 owner 是否清楚,比为了语言纯度再造一份解释更重要。
新建默认值是另一个例子。当前 priority 回归保护以下区别:
| 输入或操作 | 预期行为 |
|---|---|
| 新建任务,省略 priority | 使用新建默认 P1 |
| 读取旧任务,历史上没有 priority | 保留历史含义 |
| 重放原创建 operation | 返回原结果 |
| 只修改正文 | 保留已有 priority |
| 显式清空 | 确实清空 |
| 结构化声明与旧正文前缀冲突 | 拒绝并给出修正上下文 |
这些区别由 File/SQLite 回归 保护。把“新建默认值”放在原回执恢复之后,才能避免升级替历史做决定。
最后一轮评审还发现:TS 核心的 metadata-only completion repair 可以通过直接测试,普通 Python producer 却会自动带上 completion policy 和 auto-acquire 等默认参数,被核心视为混入了新工作,导致 File/SQLite、dry-run/执行的实际入口都失败。公开反例 记录了这一点。
这个恢复缺口在基线也存在,不能全部归因给最后的贡献者。修复要走到普通调用方,同时继续拒绝真正的新正文、continuation、所有权或验证修改。直接构造正确 typed request,仅证明内层会处理它。
9. 恢复原结果,同时保护当前执行权
恢复最容易混淆两个时间点:原操作当时合法,现在的执行者是否还能继续改状态。
设想一次 team-plan 已经提交,响应丢了。升级后,调用者拿原 operation ID 重试,新准入规则却变严了。若先按新操作拒绝,就会把原成功结果挡住。最后的 恢复评审 抓到了这个问题。
当前路径先解码并定位正确权威,核验原身份、意图与回执。确属同一笔已生效操作,就恢复原结果;无匹配原结果的工作,再走当前准入。选定 provider 的可用性与证明仍需要成立,不能在故障后偷偷换到 Markdown。
返回原结果不会续租,不会覆盖后来提交,也不会复活旧 writer。租约转交、epoch 改变和后续状态,都有自己的保护。
类型也表达这种区别:历史解码仍认识 legacy,当前新执行的 handoff 配置只接受 soft_claim 与 hard_lease。旧记录可读、原操作可恢复、当前执行获准,各自有独立条件。
内部回执保护的范围也有限。一次外部远端 effect 若没有可核验的幂等身份与回读,不能只凭控制面只结算一次,就宣称远端恰好执行一次。unknown 应在它实际发生的边界上保留。
10. CLI 输出约束:压缩前先保护决策语义
对 Agent 来说,CLI 的文字会参与决策。这项工程约束早于最后的 writer 退役,在迁移中继续作为验证边界。
接口预算合同 关注:
- 实际打印的 stdout,而不只是内存里的 payload;
- 字符、UTF-8 字节、行数和 JSON 结构的不同成本;
- 同一 fixture 的 base/head,以及 Todo、history 增长时的斜率;
- 必需字段、action signature、语义锚点和明确的 detail 路径。
展示压缩应在完整规划之后发生。少展示了什么,要与根本没读到什么区分开。projection envelope 也分别表达观测时间、服务时间、来源覆盖和完整性;缓存重新返回,不能给旧事实刷新时间戳。
自然语言还需要逐条看:义务、顺序、证据来源、权限限定、适用范围、继续和停止条件。删除一句“仅在原证明成立时”可能比改一个字段更影响行为。
一个有界实例是去掉 JSON 缩进与分隔空白。在同一历史 fixture 的 36 个 Todo、12 条 run 上,输出从 48,418 降到 39,726 字符,完整 payload 解码保持一致,字符数减少约 18%。它证明展示体积变化;token、费用和 Agent 效果需要另测。
预算保护的是消费者能用的接口。新增指导确实有用,可以带同条件证据提高预算;为了变绿而删掉决定所需的条件,反而损坏接口。
11. 最后退役:保留贡献,核对删除的究竟是什么
9 月下旬已经删除了未使用的 authority prototype,以及实验性 Todo event overlay、dual-writer 和 backfill;其中有数据的旧来源有显式保留或拒绝处理。这里没有删除所有 event logging,也没有取消新 authority 的原子事件与回执。
10 月继续隔离普通 Markdown writer,验证模块不在时真实后续写入仍走正确 owner。等 consumer 路由与替代路径接上,才具备集中删除的条件。
#6396 由 Duang777 主导,Song 和维护侧 Agent 接力补恢复、producer 与集成,沿原分支保留贡献历史。这一轮 3 位开发者与 Agent 的接力,是整个两个月工程的收尾阶段。
最终合并提交相对首个父提交的 diff 如下,只统计这个 PR,包含实现与测试:
| 文件范围 | 新增 | 删除 | 净变化 |
|---|---|---|---|
| Python | 10,938 | 25,235 | −14,297 |
| TypeScript | 2,547 | 5,116 | −2,569 |
| 其他 | 1,944 | 3,013 | −1,069 |
| 合计 | 15,429 | 33,364 | −17,935 |
大部分删除也包括旧测试。不能把被删测试行当成业务能力;应继续核对原来保护的 invariant 是否由现有入口和独立反例接住。这里的数字用于说明收尾范围,不能替代整个迁移的结果验证。
12. 社区贡献与下一步
这项工程有多位贡献者逐步推进。下面按具体边界列出代表工作,便于继续读历史。
| 贡献者 | 代表贡献 |
|---|---|
| 新 maintainer Song / @songoow | Semantics RFC 与检查、动作域与 producer、扫描反例修复,以及后续语义和恢复收敛 |
| @wchwawa | 协调核心、aggregate/provider 基础、可恢复 reference |
| @zigzagcai | task-lease settlement transaction |
| @Alicecooo | Turn outcome projection 与后续 facade 清理 |
| @hhyykk | native lease acquire、Vision transaction、scheduler、monitor 与后续事务组合 |
| @yuefengw | 本地 lease lifecycle、fence 与锁/恢复 |
| @LIHUA919 | SQLite provider、写入测量、owned-state replay |
| @Duang777 | 早期验证贡献,以及 普通旧 writer 集中退役 |
这些只是相关主线的代表工作。Semantics 的后续收敛、diff advisory 等也有其他社区贡献,不能全部归给同一个人。
回看这次过程,我更在意的验收是:一次规则修改能否定位到清楚的 owner;实际调用者能否表达正确意图;副作用前是否检查;恢复能否保留原结果和后来状态;旧规则是否退出生产;成本是否在真实入口上可测。
2.0 也没有把所有 RFC 目标画上句号。保留的 Python transport 与专业 provider 有明确用途,更广的 native CLI、持续运行资格继续按各自路径验证。工程闭环更清楚以后,我们还要在 EdgeBench、LHTB 这类通用长程 benchmark 上做能力与效率对照,尤其是 single session。
我希望这次重构留下的,是下一轮修改更容易定位、验证和回滚的系统,以及遇到反例时愿意继续修正的工作方式。