Repository navigation
Conversation
Every short CLI process re-opened all packaged TS/JSON sources to compute the managed runtime fingerprint. On hosts that scan each open this cost 0.15-0.55 s per heartbeat, quota or status command. On POSIX, record the computed digest in the existing private per-user runtime namespace, keyed by release root, interpreter, schema and the complete source stat snapshot. A later process with the same snapshot reuses it; a key mismatch, symlink, shared namespace or untrusted record falls back to the unchanged full-byte hash and freshness recheck. Sources changed within two seconds are hashed but not recorded, and the memo never creates the namespace. Other platforms keep full reads. The bilingual TypeScript migration RFC records the trade-off and the residual metadata-preserving replacement risk. Signed-off-by: lusendong.6789 <lusendong.6789@bytedance.com> Co-authored-by: TRAE CLI <traecli@bytedance.com>
|
Real-state end-to-end evidence for this head (
Limits: a single machine with a warm filesystem. This is not a cross-platform result and does not cover the failing |
JasonBuildAI
left a comment
There was a problem hiding this comment.
Reviewer: model_agent · GPT-6.1 · OpenAI · self_reported
本次评审固定于 7748@249773887ce8c0903208f8f7308a099914010105。我阅读了 4 个改动文件、完整 base-to-head diff、运行时指纹与 locator 消费路径,复跑了相关测试和文档检查,并用 base/head 对照探针验证缓存失效竞态。结论为 REQUEST_CHANGES:跨进程缓存命中路径没有在返回前再次核对源码快照,可能返回与当前源码不一致的旧指纹。
动机
受影响的是频繁启动短生命周期 LoopX CLI 的操作者和自动化链。heartbeat-prompt、quota should-run、status、refresh-state 和 todo 每一步都会启动新的 Python 进程;这些进程都要为受管 TypeScript runtime 计算源码指纹,而旧实现会在每个新进程中重新打开约 380 个已打包的 .ts/.json 文件。作者报告在 macOS 上单个命令因此增加约 0.15–0.55 秒,读取打开成本高于哈希或 stat 成本。
修复后的可观察结果是在源码元数据未变化时复用已经计算过的摘要,避免后续进程重新打开全部源码;源码变化、release root 变化、解释器或 Python 版本变化、记录不可信时仍必须回到完整字节哈希。缓存只保存指纹,不授予执行、租约、任务完成或 runtime authority,也不改变指纹本身的算法和 locator 协议。本 PR 的边界是“复用同一源码修订的摘要”,不是改写 TypeScript runtime 的状态所有权。
正确性要求不只是“正常情况下源码改变会换 key”,还包括缓存命中路径必须像完整哈希路径一样在返回前观察到并发变化;否则调用方会把旧修订当成当前修订。下面这项 P2 正是该边界没有被保持。
改动思路
入口仍是 _runtime_fingerprint():它先通过既有 _runtime_source_snapshot() 取得有序的 (path, mtime_ns, ctime_ns, size) 元数据,再交给既有的 _runtime_fingerprint_for_snapshot()。完整哈希路径仍按顺序读取每个源码字节、更新同一 SHA-256,并在返回前重新扫描快照;快照变化时由 _RuntimeSourceChanged 和现有两次尝试循环重试。新增逻辑只是在函数入口增加一个持久化 memo,命中时直接返回记录值。
memo 放在现有 runtime 私有目录中,键由 schema、解释器、Python 版本、规范化 release root 和完整快照组成。POSIX 上才启用;读取要求父目录属于当前用户且没有 group/other 权限,记录文件通过 O_NOFOLLOW 打开,同时校验 owner、权限、schema、key 和摘要格式。写入只发生在命名空间已经存在、源码至少稳定两秒且完整哈希已经复核之后,并使用同目录临时文件和原子替换。其他平台继续逐进程完整读取。这复用了现有 runtime 生命周期和文件所有权,没有创建第二个持久化 authority,也没有引入新的 CLI 配置面。
架构判断可接受:缓存是 best-effort memo,私有目录和文件校验把不可信记录降级为完整重读,原有 locator、锁、进程和指纹算法仍是唯一 owner。问题在于 memo 命中分支过早返回,绕过了完整路径末尾的 freshness recheck;它需要留在同一个 fingerprint owner 内修复,而不是由调用方补一次判断。本 PR 的边界是“复用同一源码修订的摘要”,不是改写 TypeScript runtime 的状态所有权。
具体改动
关键代码讲解
loopx/control_plane/effect_runtime.py:89新增摘要 schema、4096 字节上限和两秒 settle 窗口。settle 防止粗粒度 ctime 在短时间重写时保持旧时间戳;窗口外的源码才写入缓存,窗口内仍可计算但不会留下跨进程记录。loopx/control_plane/effect_runtime.py:355的_source_digest_location先按 schema、sys.executable、Python 版本和规范化 root 生成固定记录路径,再把完整源码快照加入 key。这样不同解释器、不同 release root 或不同文件集合不会共享一个摘要。loopx/control_plane/effect_runtime.py:388的_read_source_digest校验父目录和文件的 POSIX 所有权/权限,拒绝 symlink、非普通文件和超过上限的读取,并只接受匹配 schema、key 且符合裸 SHA-256 格式的指纹;任一项不满足都返回None并走完整哈希。loopx/control_plane/effect_runtime.py:414的_write_source_digest不创建 runtime 目录;它只在既有私有目录内用O_EXCL | O_NOFOLLOW写 0600 临时文件并原子替换。写入失败是 best-effort,不影响本次指纹结果。loopx/control_plane/effect_runtime.py:451的_runtime_fingerprint_for_snapshot保留原有lru_cache,并在457–461先尝试持久化记录。这里命中后立即返回;完整哈希分支则在484–486重新扫描并比较快照。缓存命中没有等价的二次校验,是本次 P2 的位置。tests/control_plane/test_runtime_source_read_batching.py:114新增跨进程缓存、源码编辑/增加/删除、root 变化、不可信记录和失败不落盘等回归。现有用例在两次完整调用之间修改源码,能证明下一次快照换 key,但没有覆盖“修改发生在第一次 snapshot 之后、cache read 返回之前”的竞态。- 两份迁移 RFC 同步说明 POSIX 摘要复用、两秒 settle、不可信记录回退和 ctime/symlink 残余风险;英文与中文改动对应。
P2 finding
P2:缓存命中可能在源码变化后返回旧指纹。 触发序列是:进程取得快照 S;另一个安装/写入进程把某个源码改为 S';本进程随后读取为 S 生成的缓存记录并直接返回旧摘要。_runtime_fingerprint_for_snapshot 在缓存命中后没有再次调用 _runtime_source_snapshot,因此不会进入既有的 _RuntimeSourceChanged 重试。完整哈希路径本来会在读取全部字节后复查快照,所以 base 在同样的并发变化下会返回 S' 的摘要。
我使用确定性 base/head 对照探针复现:在首次快照返回后、缓存读取前修改一个 .ts 文件。head 只执行一次快照扫描和一次缓存读取,返回旧摘要;base 在完整哈希后的复查发现变化,重新计算并返回当前源码摘要。该窗口很短,但 release 更新、安装脚本并行运行或编辑器落盘时真实存在;错误指纹会影响 locator/runtime revision 的选择,不能仅当作性能缓存的无害误差。
最小修复是在缓存命中返回前重新扫描当前快照并与 key 对应的 snapshot 比较;不相等时抛出既有 _RuntimeSourceChanged(current),让 _runtime_fingerprint() 用新快照重试。另加一个“第一次 snapshot 后、cache read 前修改源码”的回归,要求缓存记录不得被采用。修复后应至少复跑:
python -m pytest tests/control_plane/test_runtime_source_read_batching.py tests/control_plane/test_effect_runtime_integration.py -q
uv run --extra test ruff check loopx/control_plane/effect_runtime.py tests/control_plane/test_runtime_source_read_batching.py
规格映射
规格依据是 docs/architecture/rfcs/typescript-control-plane-migration-v0.md 中“源码指纹读取已准入字节并复查新鲜度”的约束;本 PR 修改了该段并声明缓存复用边界。
| criterion_id | 要求 | 当前 head |
|---|---|---|
reuse-valid-key |
只有完整 key 匹配时才可复用摘要 | implemented;_source_digest_location 与 _read_source_digest |
freshness-recheck-on-hit |
缓存命中返回前仍须确认源码快照未变化 | not_met;_runtime_fingerprint_for_snapshot:457–461 直接返回 |
private-record |
记录必须位于私有目录,拒绝 symlink/foreign mode/损坏内容 | implemented;_private_directory、_read_source_digest |
stable-source-only |
只有稳定源码才能写入跨进程记录,失败不产生部分记录 | implemented;settle 与 _write_source_digest |
non-posix-parity |
非 POSIX 主机保持原有逐次完整读取 | implemented;_source_digest_location 在非 POSIX 返回 None |
对主干的风险
最强失败形态是缓存把旧源码摘要当作当前修订,导致同一 release 的 runtime identity 与磁盘源码不一致:调用方可能复用旧 runtime,或把本应启动新修订的请求归到旧 locator。这个竞态不会损坏缓存文件,也不是权限提升;但它破坏了本 PR 用来证明优化安全的 freshness invariant,因此属于 correctness P2,而不是只补一个测试。
现有正反例覆盖较完整:正常复用、源码编辑/增加/删除、root 变化、不可信记录、失败不落盘和缺失命名空间都有断言;完整集成模块在 Windows 上为 102 passed、9 skipped。Windows 会跳过 8 个 POSIX 缓存用例,所以本次不能把该平台的测试通过当成缓存路径的独立 qualification;作者报告 macOS 上 388 passed、1 skipped,代码审查和 base/head 探针仍独立暴露了上述竞态。
CI 侧当前有 3 个失败:Windows PowerShell 作业失败在未改动的 delegation revalidation 用例,dashboard-browser (2) 失败在既有 lens-switch 预算,checks 是聚合失败;这些路径不经过本 PR 修改的缓存代码,且 Windows 仍按设计走完整读取。它们在本地 base/head 对照中没有形成本 PR 的差分证据,故作为独立 merge-readiness hold 记录,而不是把红灯说成绿色。作者报告的性能收益和文档改动未发现第二个 blocking finding。
语义与 CI 对齐
这次改动没有新增 actor、任务义务、租约或运行时权限;新增的只是私有缓存 schema 和一个清理前的发布步骤。default_off_isolation 不适用:这不是可选功能开关,而是 POSIX 上默认启用的 memo,Windows 显式保持旧路径;因此不能从“没有 feature flag”推导出隔离性,必须依赖平台分支、失败回退和现有指纹测试。缓存命中修复后,文档中“源码变化仍触发完整哈希”的承诺才与实现一致。
我的整体评价
这是一个有价值的性能切片:它针对真实的高频短进程路径,复用既有 runtime/私有目录/指纹 owner,没有把缓存变成第二套状态系统;key、私有文件校验、settle 和失败回退都与现有协议相称,代码量也主要落在可独立验证的 I/O helper 和回归上。repository_reuse 为 reused,change_proportionality 为 proportionate;不建议扩展成第二个后台缓存服务或改写 locator 协议。
但在当前 exact head,缓存命中破坏了源码指纹最重要的 freshness invariant。先加入返回前快照复查和对应竞态回归,再在同一精确 head 重跑上述命令;在修复前我不会批准。当前 CI 的其他红项仍需单独处理,APPROVE 或缓存修复都不授予 merge authority。
English verdict: REQUEST_CHANGES — 249773887ce8c0903208f8f7308a099914010105 adds the intended private POSIX digest reuse, but a cache hit can return the old fingerprint when a source changes between the initial snapshot and the cache read because the hit path skips the full-hash path's freshness recheck; add the recheck and a deterministic race regression, then rerun the focused suites and static checks. Unrelated current CI failures remain a separate merge-readiness hold.
Delivery brief
heartbeat-prompt,quota should-run,status,refresh-state,todo) starts a fresh Python process for each step, and each step computes the managed TypeScript runtime fingerprint once..ts/.jsonsources in every process. Fresh-process measurement on macOS shows the cost is dominated byopen(), not by hashing orstat(a stat-only pass is ~1 ms). It was 0.15–0.55 s per command, and 100% of the first explore-validation cost on the measured route.loopx/control_plane/effect_runtime.py(_runtime_fingerprint_for_snapshot). This does not change TS authority, the fingerprint value, runtime locators, scheduling, settlement, defaults or output contracts.Change
On POSIX, after a successful full hash plus the existing freshness recheck, the digest is recorded in the existing private per-user runtime namespace as
source-digest-<id>.json, mode0600, written atomically.sys.executable, Python version, normalized release root and the complete(path, mtime_ns, ctime_ns, size)source snapshot. That snapshot is already computed for the in-process cache.O_NOFOLLOW, it is a regular0600-style file owned by the user, and both the key and thesha256digest validate. Otherwise the code falls back to the unchanged full read.runtime-*.json, the*.ts-effect.lock*names andstart-*.lock. Locator retirement, fixture cleanup and lock observation are therefore unaffected.The bilingual TypeScript migration RFC replaces "still reads every admitted byte" with this trade-off and its residual risk. Another file with identical size, mtime and ctime (for example a retargeted symlink), or a filesystem that does not maintain ctime, can hide a change.
Evidence
Base
04c5799b8and head were run alternately in fresh processes against one isolated private namespace, on macOS with a synthetic SQLite fixture and 0.6 s gaps between runs:_runtime_fingerprint()in a fresh process (n=12)heartbeat-prompt --thintotal (n=6)quota should-runtotal (n=6)statustotal (n=6)The fingerprint value is identical on base and head. After the run,
canaryfixture retirement completed normally, and the namespace contained only the digest record and the existing compile cache.Validation at head
249773887:tests/control_plane/test_runtime_source_read_batching.py, checked against an independent serial reference:pytestover the fingerprint, readiness, status-scan, alltest_effect_runtime_*, managed-fixture, telemetry-isolation, performance-diagnosis, local-delegation and goal-portfolio suites: 388 passed, 1 skipped.ruff checkon changed Python: passed.scripts/check_rfc_bilingual.py --base origin/main --head HEAD: passed.loopx canary premerge --from-git-diff --git-diff-base origin/main(standard tier):Not covered
loopx/**, so it needs maintainer review and merge authorization. It is not self-merged.