Repository navigation
fix(contract): bound the Git ownership probes in a public-boundary scan - #8152
Conversation
`doctor` inspects a checkout the operator may not control, and none of its six Git reads had a budget: `rev-parse` twice, the metadata runner behind `git_metadata_for_root`, `merge-base --is-ancestor`, `remote`, `remote get-url` and `rev-parse --verify`. A repository blocked on a stale index lock, a network mount or a very large history therefore held the diagnostic open with no output at all. Reproduced with a fake `git` on PATH that only sleeps: `_is_git_worktree_root` was still running after 5s. All six reads now go through `_run_git`, bounded by `GIT_PROBE_TIMEOUT_SECONDS`. The helper answers `None` when the budget runs out, which is the answer every caller already treats as "unknown": root detection fails closed, metadata fields stay `None`, revision relation stays `UNKNOWN`, and the trusted release ref is refused. No new failure mode, and the diagnostic still returns instead of raising. The same class in `loopx/contract.py` was bounded in loopx-project#8152. Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent; model=gpt-6.1-sol; provider=OpenAI; declaration_source=runtime_reported; reasoning_effort=max. 身份来自本次 Host 记录,不是后端权重或独立验收证明。
动机 / Motivation
执行公开提交前检查的维护者会遇到这个问题:扫描需要用 Git 索引判断哪些私有状态文件已经被跟踪。例如,仓库已跟踪 ACTIVE_GOAL_STATE.md;原实现等待跟踪查询完成后拒绝通过,本改动希望在查询卡住时及时结束,避免发布检查无限等待。目标是让扫描在 Git 查询异常时有限时退出,并继续准确阻断私有文件;当前只验证了查询主进程的限时返回,安全结论与子进程回收未满足。本次范围是现有公开边界检查中的 Git 探测,不需要新增配置、用户界面或改造其他 doctor 命令。
保留无上限的 Git 查询确实会延迟后续工作,但及时返回与安全结论必须同时成立。这里的“未跟踪”表示 Git 已确认文件不在索引中,“未知”表示根本没有得到答案;二者会让发布检查走向不同结果。
改动思路 / Approach
路径是 CLI check --scan-root → check_contract → scan_public_boundary,扫描器结合文件树与 Git 索引收集候选,再区分公开文件和私有状态。PR 给三个 Git 启动位置加入 5 秒超时,并提取 _git_query_succeeds。正常查询仍走原有规则;异常被转换为 False、None 或空列表。
Git 超时属于强制安全检查的输入不可用,不能折叠成否定事实;进程回收应复用现有 process_runtime 传输能力。本 PR 只需在现有扫描所有者中保留未知状态、让不完整扫描明确失败,并为这几个 Git 调用补齐进程树回收;其他 Git 调用审计可保留原有范围。
对照了最新 main 上已合并的 #8115:registry.py 的有界 Git 探测已经使用隔离进程组、terminate_process_tree 和有界管道释放。它在本 PR 的较早分支基点之后合入;这不是声称当前 PR 已包含该修复,也不是要求跨模块调用私有 helper。可以直接复用已有 OS 生命周期能力,把“探测不可用”留给扫描所有者处理,无需创建另一套领域规则。
下面是相同真实 Git 索引、生产默认 5 秒超时的对照;只在指定查询执行前加入 6 秒延迟,文件内容和跟踪状态不变。时间仅辅助说明,未做性能资格比较。
| 输入 | 固定 main 基线 | 本次 head |
|---|---|---|
| 正常查询已跟踪私有状态 | 阻断 | 阻断 |
--error-unmatch 查询延迟,真实 CLI |
退出 1,保留私有文件错误 | 退出 0,ok=true,报告扫描 clean |
ls-files -z 延迟,已跟踪 .local/private.md |
枚举并阻断该文件 | 从候选中消失,ok=true |
| 真实 Git fsmonitor hook 睡眠 30 秒 | 本项用于检验新增超时承诺,未作基线耗时比较 | 约 5 秒返回后,hook 仍存活 |
具体改动 / Changes
完整 PR diff 是 2 个文件,115 行增加、29 行删除:生产代码增加 47 行、删除 29 行,新增测试 68 行。没有公开协议字段、持久状态或依赖变更。按 GitHub 的三点 diff 审查;当前 main 为 faab5f5c291c5359216a0f944fe63fcfbaacdedc,分支 merge base 为 68318d14d2db02bd4855fa03a9af46ebc64be2bc,没有把 main 上其他提交的缺失算成本 PR 的改动。
独立规范是 docs/public-private-boundary.md 与 docs/reference/public-boundary-read-cost.md,(spec_ref=docs/public-private-boundary.md; docs/reference/public-boundary-read-cost.md),固定 spec_revision=faab5f5c291c5359216a0f944fe63fcfbaacdedc;这两份文件在本 head 与该基线一致。按规范已有章节映射:
- Active State:已跟踪的本地私有状态必须阻断。not_met:跟踪查询超时后会跳过它,见 P1。
- Contract / 契约:完整枚举所选树,包括位于默认剪枝目录内的 Git 跟踪文件,并保持未跟踪私有状态的读前排除。not_met:枚举超时返回空列表会遗漏已跟踪
.local文件;修复也必须保留未跟踪文件的排除,不能直接读取所有私有文件。 - User paths / 用户入口:显式公开检查要给出真实安全结论,普通 status/quota/launch 继续按既有契约延后内容扫描。显式检查 not_met;普通入口在本 PR 未新增扫描调用,但扩展回归存在下述未完全归因失败,不能宣称整条 Host 路径通过。
- Qualification / 验证:真实 CLI、负例和回归证据必须支持结论。not_met:新增测试通过,但其超时预期恰好接受错误的 clean 结果,也没有验证后代进程回收。
关键代码讲解
_git_query_succeeds,L328:跟踪查询与 ignore 查询共用subprocess.run(timeout=5)。L346–347 将TimeoutExpired一并转换成False,下游因而无法区分“不在索引”与“未取得索引答案”。_git_probe,L369:把 helper 的布尔值交给私有文件分类。随后scan_public_boundary.public_files()的if not tracked分支将文件计入跳过,而不是阻断,错误最终进入 CLI 的public boundary scan clean。_tracked_scan_files,L957:ls-files -z补回普通遍历会剪枝的 Git 所有文件;L979–980 在超时后返回[],让“没有候选”掩盖“候选不完整”。同一启动点也是实际 fsmonitor 泄漏的复现入口。scan_public_boundary,L1129:它持有安全结论,仍必须区分成功扫描、私有内容阻断和输入不可用;不能让 provider I/O 的失败自行发出通过信号。显式选中私有路径的拒绝分支仍存在,本次反例针对目录扫描,不混淆两条路径。
对主干的风险 / Risks
[P1] Git 所有权超时会把已跟踪私有状态判为安全。 在真实索引中加入 ACTIVE_GOAL_STATE.md,只延迟 ls-files --error-unmatch,再运行隔离 fixture 的 loopx check --scan-root <repo> --limit 0:基线保持退出 1、ok=false;head 退出 0、ok=true,public_boundary_scan 为 completed/clean,只扫描公开文件并将私有文件跳过。原错误只剩一条应 gitignore 的警告。另一个仅延迟枚举的反例会让已跟踪 .local/private.md 消失。新增布尔 helper 的“conservative”说明与实际消费方式相反。最低修复是在现有扫描结果中保留 unknown/unavailable,缺失跟踪答案或枚举不完整时让显式检查非零退出,同时继续在读取前排除确知未跟踪的私有状态。补真实 CLI 的两种超时负例及正常排除对照。
[P2] 超时只结束 Git 主进程,没有回收它启动的 fsmonitor。 在一次性真实 Git 仓库里将 core.fsmonitor 指向记录自身 PID 后睡眠 30 秒的 hook,再调用 _tracked_scan_files。默认 5 秒后方法返回空列表,记录的 hook 仍是活进程;已清理该测试拥有的 PID。Git 的本地 ls-files 可以启动 fsmonitor,因此 L324 的“never fan out into helpers”假设不成立。这个 POSIX 复现证明的是遗留子进程,没有声称调用者仍挂住,也没有推断 Windows 已通过。最低修复是隔离并回收该调用拥有的进程树,限制异常后的管道释放;测试必须断言真实子进程不再存活。
验证结论:新增测试、公开边界与文件读写核心用例包含在 240 PASS / 12 FAIL 的本地扩展运行中。12 个失败属于 operational admission/checkpoint continuation;其中 4 个代表用例在固定 main 上同样失败(上下文导航包装与原始 receipt 预期不匹配),其余 8 个未逐项做基线对照,保留 UNVERIFIED 归因。首次将 Gitless fixture 放入仓库内的运行已因环境不符合仓库测试契约而中止;上面数字来自仓库之外的中性临时目录,未拿早期环境失败充当回归。
Ruff 与 diff/编译/双语 RFC 直接检查 PASS。本地 premerge FAIL:maintainability ratchet 与 first-connect smoke 在本 head、固定 main 上有相同失败身份和细节;其余 4 项 catalog 检查通过。保持这些门的原始失败,不要求本 PR 顺带修无关路径,不宣称 merge readiness。没有抓取或等待远端 CI。
语义与 CI 对齐
本改动复用现有扫描词汇,没有新增注册词表。问题在既有 tracked/空集合的含义发生漂移:provider 输入缺失被当作否定事实,导致强制阻断义务消失。应恢复规范的 fail-closed 结论与真实进程生命周期,再重跑上述两类实际反例及原有公开边界回归。这里不是建议性 warning,也没有 default-off 开关隔离这个变化;它默认影响每次显式扫描。
我的整体评价 / Verdict
REQUEST_CHANGES,评审针对 f52517af6e9bba7e96fc3f81245123a0f144f883。这是一项合理且范围有限的可靠性修复,但当前交付的安全结论和进程回收不足以兑现目标。long_horizon=regression:重复扫描可积累 Git helper;user_experience=regression:维护者会在应阻断时拿到 clean。所需修复限于上面两项,不要求更换语言、增加配置或扩大到所有 Git 调用。
检查了 contract.py 到 CLI check 的入口;没有页面、布局或其他呈现层改动,因此视觉验收不适用。未验证 Windows 进程树及已安装应用行为。尚未证明强模型采用、已安装 Host 行为或长期性能收益;这些不是本次局部实现已交付的事实。代码量与既有所有者匹配,最高价值的简化是复用现有进程树能力并保留明确错误状态。重新评审需要安全拒绝、私有读前排除与真实后代回收共同通过,验证中的无关/未归因失败继续单独保留。
English verdict: REQUEST_CHANGES. Git timeouts currently turn tracked private state into a clean scan and leave a real fsmonitor child alive; preserve unknown/error results and reclaim owned descendants before approval.
| check=False, | ||
| timeout=GIT_PROBE_TIMEOUT_SECONDS, | ||
| ) | ||
| except (OSError, subprocess.SubprocessError): |
There was a problem hiding this comment.
[P1] 超时不能作为“未跟踪”的证据。真实索引已跟踪 ACTIVE_GOAL_STATE.md 时,只将 --error-unmatch 查询延迟6秒:固定 main 的真实 CLI 仍退出1并阻断;这里超时返回False后,public_files 将该文件跳过,check --scan-root 最终退出0、ok=true并报告clean。_tracked_scan_files 的空列表回退同样会丢失剪枝目录内的已跟踪文件。请保留unknown/unavailable,使显式检查在所有权或枚举不完整时失败,同时保留已知未跟踪私有文件的读前排除。补这两种超时的实际CLI负例。
There was a problem hiding this comment.
已修,新 head 1238db835。这一条我接受:把超时折成 False 等于把「没有答案」当成「不在索引」,而那正好是放行私有文件的依据。
三种答案现在是三种
_git_worktree_root:Git 明确回答「不是仓库」时仍返回None;Git 根本没答上来时抛GitProbeUnavailable。_git_probe:tracked/ignored变成True/False/None,None表示没答上来。_tracked_scan_files:枚举超时不再返回空列表,改为抛GitProbeUnavailable。
scan_public_boundary 把每一次没答上来的问题记进新字段 unavailable_git_probes,并把它纳入 ok;check_contract 再把它变成 public_boundary_scan_git_unavailable 这条 error,所以 check --scan-root 现在会非零退出。读前排除没有动:确知未跟踪的私有文件仍然在打开之前就被跳过,只是「未知」不再享受同一条快速路径——私有文件同样不读,但扫描不再自称 clean。
第一条问不出来就停止继续问,避免同一处卡顿再花一个预算。
两种反例
真实索引里跟踪 ACTIVE_GOAL_STATE.md,只用包装 git 延迟指定查询,其余命令照常执行真实 Git:
| 条件 | 改前 head | 本 head |
|---|---|---|
--error-unmatch 阻塞 |
ok=true、clean、私有文件被跳过 |
ok=false,unavailable_git_probes 记录该文件,check_contract 带 public_boundary_scan_git_unavailable |
ls-files -z 阻塞 |
枚举返回空列表,剪枝目录内的已跟踪文件消失 | ok=false,根被记为不可用 |
两个反例在保持 loopx/ 不变、只留新测试时会失败;正常 Git、非 Git 目录与既有拒绝路径的对照仍在同一文件里。
| errors="replace", | ||
| capture_output=True, | ||
| check=False, | ||
| timeout=GIT_PROBE_TIMEOUT_SECONDS, |
There was a problem hiding this comment.
[P2] 这个 timeout 只回收 Git 主进程。真实 Git 仓库配置 core.fsmonitor 为记录PID后睡眠30秒的 hook 时,_tracked_scan_files 在默认5秒后返回,但 hook 仍存活;测试已仅清理自身记录的PID。因此“local reads never fan out into helpers”的假设不成立,重复扫描会遗留子进程。请复用已有隔离进程组/terminate_process_tree与有界管道释放,并用实际fsmonitor断言后代在超时后退出。这里证明的是POSIX子进程泄漏,没有宣称调用者仍挂住或Windows已验证。
There was a problem hiding this comment.
已修,新 head 1238db835。
subprocess.run(timeout=...) 只结束 Git 主进程,而这个 PR 原本还写着「local reads never fan out into helpers」——ls-files 会启动 core.fsmonitor,那句话不成立。
现在探针以独立会话启动(POSIX 下 start_new_session=True),预算用尽时由 _retire_owned_probe 调用既有的 terminate_process_tree 回收这一次调用自己的进程组,然后关闭管道而不是排空,最后 wait(timeout=1.0)。关闭而不是读取,是因为继承了管道的后代在 leader 死后仍然写着,排空会一直等下去。
反例:一次性仓库里 core.fsmonitor 指向记录自身 PID 后每 100ms 追加一行的 hook,_tracked_scan_files 在预算内抛 GitProbeUnavailable,返回后心跳停止。新增用例 test_a_timed_out_probe_takes_the_helpers_it_started 断言的就是「返回」与「后代停止」两件事,断言前先要求心跳文件非空,避免 hook 未被调用时空断言通过。该用例按 os.name != "posix" skip,没有声称 Windows 已验证。
`doctor` inspects a checkout the operator may not control, and none of its six Git reads had a budget: `rev-parse` twice, the metadata runner behind `git_metadata_for_root`, `merge-base --is-ancestor`, `remote`, `remote get-url` and `rev-parse --verify`. A repository blocked on a stale index lock, a network mount or a very large history therefore held the diagnostic open with no output at all. Reproduced with a fake `git` on PATH that only sleeps: `_is_git_worktree_root` was still running after 5s. All six reads now go through `_run_git`, bounded by `GIT_PROBE_TIMEOUT_SECONDS`. The helper answers `None` when the budget runs out, which is the answer every caller already treats as "unknown": root detection fails closed, metadata fields stay `None`, revision relation stays `UNKNOWN`, and the trusted release ref is refused. No new failure mode, and the diagnostic still returns instead of raising. The same class in `loopx/contract.py` was bounded in loopx-project#8152. Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
A scan reads a checkout the operator may not control, and its four Git calls passed no budget at all: `rev-parse`, the `ls-files --error-unmatch` probe, `check-ignore`, and `ls-files -z`. A Git blocked on a stale index lock, a network mount or an oversized index therefore held the whole scan open, with no output and no way to tell the caller anything was wrong. Reproduced here with a fake `git` on PATH that only sleeps: `_git_probe` was still running after 5s and `scan_public_boundary` after 6s. Both calls are now bounded by `GIT_PROBE_TIMEOUT_SECONDS`. The budget needed the degradation paths, not just a `timeout` argument: `contract.py` handled no `TimeoutExpired` at all, so a bare timeout would have turned a hang into a traceback out of a read-only scan. A probe that runs out of budget now answers the same way a missing Git does -- ownership unknown -- so callers keep their conservative answer. The queries are local reads that never fan out into helpers, so the leader-only kill that `subprocess.run` performs covers them; the process tree retirement that `loopx/registry.py` needs does not apply here. Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
Two gaps in the five-second budget. A probe that ran out of budget answered False, and False is the scan's word for "not tracked": a tracked ACTIVE_GOAL_STATE.md was skipped as private state and `check --scan-root` exited 0 with a clean boundary. `_tracked_scan_files` had the same shape, where an empty list hid tracked files behind a pruned directory. Silence is not a negative answer, so `_git_worktree_root` raises GitProbeUnavailable when Git cannot answer at all, `_git_probe` reports `tracked: None`, and a scan that hit one records it in `unavailable_git_probes`, which fails `ok` and produces the `public_boundary_scan_git_unavailable` diagnostic. Known-untracked private state is still excluded before it is read, and the first unanswered question ends the probing instead of spending another budget on the same hang. `subprocess.run(timeout=...)` retired the Git leader only, while Git starts an fsmonitor hook for `ls-files` that outlived it. The probe now runs in a session of its own, and an exhausted budget retires that group through `terminate_process_tree` before dropping the pipes rather than draining them. Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
f52517a to
91c2cc6
Compare
`doctor` inspects a checkout the operator may not control, and none of its six Git reads had a budget: `rev-parse` twice, the metadata runner behind `git_metadata_for_root`, `merge-base --is-ancestor`, `remote`, `remote get-url` and `rev-parse --verify`. A repository blocked on a stale index lock, a network mount or a very large history therefore held the diagnostic open with no output at all. Reproduced with a fake `git` on PATH that only sleeps: `_is_git_worktree_root` was still running after 5s. All six reads now go through `_run_git`, bounded by `GIT_PROBE_TIMEOUT_SECONDS`. The helper answers `None` when the budget runs out, which is the answer every caller already treats as "unknown": root detection fails closed, metadata fields stay `None`, revision relation stays `UNKNOWN`, and the trusted release ref is refused. No new failure mode, and the diagnostic still returns instead of raising. The same class in `loopx/contract.py` was bounded in loopx-project#8152. Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent; model=gpt-6.1-sol; provider=OpenAI; declaration_source=runtime_reported; reasoning_effort=max.
评审提交 91c2cc6。REQUEST_CHANGES:超时修复成立,仍需修复取消后遗留 Git 进程的问题。
动机 / Motivation
维护者在公开提交前通过 check 检查选定目录是否包含不能公开的私有状态。
Git 查询卡住时,旧实现会无限等待;本次改为有界失败并保留未知状态,但用户取消后仍遗留辅助进程。
跟踪或枚举超时明确使扫描失败,正常私有排除与阻断保持;Ctrl-C 清理仍失败。
本次只评审公开边界扫描的 Git 探测,不改变配额、领取、发布权限或其他 doctor 路径。
改动思路 / Approach
实际入口是 loopx check --scan-root,沿现有配置/扫描 owner 读取文件与真实 Git 索引,再将结果交给现有 CLI JSON 和错误出口。没有理由继续保留无上限等待;只加 subprocess timeout 也不足以管理 Git 启动的 fsmonitor 辅助进程。当前采用独立进程组并复用 OS 清理能力,方向合理。
Git IO 保留在现有 Python owner,OS 取消复用 process_runtime;安全判断不交给传输层,也不建立第二份状态。
当前 PR 只需补齐同一探针的异常退出清理;无需创建通用框架或进行语言迁移。
与 registry 的有界 Git 探测及相关 #8152/#8153/#8154 比较后,没有要求跨模块调用私有 helper 或新增领域规则。用表格区分正常超时与取消,比另画流程图更直接:
| 实际入口观察 | 当前结果 |
|---|---|
| 正常/合法与不合规输入 | 安全结果保持 |
| Git 预算用尽 | 明确失败并清理组 |
| 用户 Ctrl-C | CLI 退出,隔离 Git 组继续工作 |
具体改动 / Changes
规范 spec_ref=docs/public-private-boundary.md; docs/reference/public-boundary-read-cost.md,不可变 spec_revision=927d0b389096a9ca8702c4da14a43836063ed29b。公共/私有边界 的 Active State 已实现;有界读取契约 的 Contract / 契约 保留已跟踪的剪枝目录文件与私有读前排除,User paths / 用户入口 保留显式检查及运行入口 deferred 分工。Qualification / 验证 尚未满足:下面的取消回归未修,必需验证失败也继续保留。
关键代码讲解
_git_query,L366:Runs isolated Git with timeout; cancellation does not retire its group.
_git_probe,L414:Separates a real negative answer from unavailable ownership and avoids the next ignore query on unknown.
_tracked_scan_files,L1017:Raises on unavailable enumeration instead of an empty complete set.
scan_public_boundary,L1197:Preserves private exclusion, gathers incomplete-input diagnostics and prevents clean success.
check_contract,L1407:Projects the unavailable list and a nonzero CLI error.
完整审阅两文件 +373/-48,生产代码 +181/-48,测试 192 行。跟踪查询延迟后不再把未知写成 False,私有文件不读且扫描退出 1;枚举延迟也退出 1,保留目录级 unavailable。正常已跟踪阻断、未跟踪排除和非 Git 三种完整 payload 在对齐 runtime 是否存在后比较,唯一有意差异是新增空的 unavailable_git_probes。初次 fixture 的 runtime 存在状态不同带来了 warning 差异,已保留并用同条件对照消除,未将它归因于产品。
同一真实索引中,跟踪与枚举延迟都得到 public_boundary_scan_git_unavailable,未报告 clean;真实 fsmonitor 超时后心跳 179→179,组内无存活成员。该完整扫描约 10.32 秒,仍可能先后用两个预算;本 PR 的局部探针预算没有声明整个扫描五秒完成。
对主干的风险 / Risks
[P2] 在 Ctrl-C 退出前回收隔离的 Git 进程组(loopx/contract.py:L387)。新增 start_new_session=True 后,终端向 CLI 进程组发送 SIGINT 不再覆盖 Git 组。这里仅在 TimeoutExpired 时调用清理;communicate 抛 KeyboardInterrupt 时直接退出。真实 loopx check --scan-root + Git core.fsmonitor 对照中,基线取消后无存活进程,当前 head 留下 Git、hook 和辅助子进程,心跳在 CLI 退出后 400ms 从 8 增至 15。取消后的重试会累积后台工作。请在异常退出(至少 KeyboardInterrupt)时有界回收原组、释放管道并等待,再传播取消;保留超时拒绝语义。补真实入口的 SIGINT、后代停止及正常路径对照。
这是实际源码 CLI、真实 Git 和 fsmonitor 的 POSIX 对照,不是 mock 给出的后置条件。最初 shell 后台子进程继承忽略 SIGINT,基线也会遗留该子进程;这组结果保留。随后显式恢复辅助子进程的正常 SIGINT 行为,在相同 fixture 对照:基线两条入口均无存活成员、心跳 1→1,新 head 则各有 3 个存活成员。所有测试结束都只清理自己记录的进程组,并验证没有活成员。不能把测试兜底清理写成产品已正确取消。
文档指定的扫描资格检查加新增测试得到 243 PASS / 12 FAIL;固定基线的共同九模块 238 PASS / 同 12 FAIL。逐项核对全部失败 node id 与归一化后的完整断言行,均相同:两条原始 receipt 缺失及十条 checkpoint 子进程断言;相关 admission、fixture、TypeScript 路径整文件未变。没有只比较汇总数字,也没有把 12 项改为 PASS。CI 范围及变更文件 Ruff、17 个 mypy 源、全树语义扫描、diff 与公开边界扫描 PASS。
语义与 CI 对齐
本次复用现有私有边界和局部诊断词汇,全树语义检查通过。TypeScript 必检仍 FAIL:基线与两个 head 的完整 158 字节诊断相同,conversation_binding.ts:1144 为 TS2345 unknown→string;对应 TypeScript、配置和锁文件整文件不变。raw maintainability ratchet 的完整 399 字节失败同样一致,定位到未变的 _admitted_interaction_next_cli_actions。不放宽预算、不要求本 PR 顺带修无关路径;严格 CQR 与 premerge 的原始 hold 单独保留。没有获取、轮询或等待远端 CI。
严格 CQR cqr_84c12f4216149452df7a 读回为 invalid_receipt。quick premerge 六项直接检查通过,语义检查通过,raw ratchet 与 first-connect smoke 失败;后者在固定基线同样于 L146 报 KeyError: should_run,完整脚本未变。整体 quality_invalid_receipt,未达到合并资格。
我的整体评价 / Verdict
完整当前提交请求修改。long_horizon=regression:取消后重试会积累后台工作;user_experience=regression:用户已经取消但实际 Git 工作仍在继续。正常和超时修复是可复核的有用改进,仍不能抵消这个新回归。代码范围与既有 Python IO owner 相称;同域简化应落实为一个异常安全的探针生命周期,保留现有输入与诊断。修复后需重新审查完整新 head,而不只复验这一行。
检查了实际 CLI 到现有 owner 的路径;没有渲染页面或布局变更,无需截图。
未验证原生 Windows 进程树及已安装 Host/采集服务行为。
这里的评审、CQR 与 fixture 记录均不授予安装、发布或合并权限,也不证明长期性能或模型采用收益。
English verdict: REQUEST_CHANGES — 91c2cc6: retire the isolated Git group on terminal cancellation; real timeout and healthy controls pass, while Ctrl-C leaves three live processes. Separate local validation holds remain.
| except OSError: | ||
| return None | ||
| try: | ||
| stdout, _stderr = process.communicate(timeout=GIT_PROBE_TIMEOUT_SECONDS) |
There was a problem hiding this comment.
[P2] 在 Ctrl-C 退出前回收隔离的 Git 进程组
新增 start_new_session=True 后,终端向 CLI 进程组发送 SIGINT 不再覆盖 Git 组。这里仅在 TimeoutExpired 时调用清理;communicate 抛 KeyboardInterrupt 时直接退出。真实 loopx check --scan-root + Git core.fsmonitor 对照中,基线取消后无存活进程,当前 head 留下 Git、hook 和辅助子进程,心跳在 CLI 退出后 400ms 从 8 增至 15。取消后的重试会累积后台工作。请在异常退出(至少 KeyboardInterrupt)时有界回收原组、释放管道并等待,再传播取消;保留超时拒绝语义。补真实入口的 SIGINT、后代停止及正常路径对照。
There was a problem hiding this comment.
这条成立:独立会话让终端 SIGINT 只到 CLI 自己的进程组,communicate 抛 KeyboardInterrupt 时会直接从唯一的持有者那里逃出去,那个会话里的 Git、hook 和辅助子进程都留着。
已在本分支补上:_git_query 在 except subprocess.TimeoutExpired 之后加了一个 except BaseException 分支,先 _retire_owned_probe(process) 回收本次拥有的会话,再原样 raise。超时仍然返回 unavailable 并回收,取消仍然是取消,正常路径不变。
回归用真实 Git 加被阻塞的 ls-files,等心跳出现后向扫描的进程组发 SIGINT:断言非零退出、返回后 0.6 秒心跳不再增长。这条用例在改前的 head(91c2cc681)上失败,本提交上通过;既有的超时用例与正常对照不变。
新 head:804cabfb2。
There was a problem hiding this comment.
补一句:上一条报的 head 804cabfb2 有一处 lint 漏网——tests/test_contract_git_probe_cancel.py 里未使用的 import json(F401)、Popen 参数列表里的隐式字符串拼接(ISC004)、以及 import 排序(I001)。提交前的检查没有按退出码拦住,是我的疏漏,已经修掉并 amend。
当前 head 是 7462420d3,逻辑没有变化:tests/test_contract_git_probe_cancel.py + tests/test_contract_git_probe_budget.py 仍是 6 passed;新测试文件在仓库默认规则下 ruff check 全绿,loopx/contract.py 在 CI 的 E4/E7/E9/F 选择集下也全绿。
* Recognize direct exec results in the trajectory tool inventory Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> (cherry picked from commit 8e9946d) Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * fix(chat): reject non-string queued input actions Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * feat(research): compare real captured LoopX request usage Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Honor cross-turn delta independently of progressive context Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * test: isolate machine CLI smokes from model context transport Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Honor scoped Todo resume conditions across external and managed schedulers Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Audit active solver coverage across every selected benchmark arm Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * fix(runtime): preserve raw quota JSON for native goal admission Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * feat(collaboration): share reader guidance and version return formatting Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * fix(chat): open long returned answers at their beginning Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * test(lark): verify full versioned returns through body workers Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * feat(research): compare real captured LoopX request usage Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * fix(runtime): preserve raw quota JSON for native goal admission Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * fix(research): show native command cache and output deltas Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * fix(chat): preserve queue edits when dismissing actions Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * docs(readme): remove redundant budget disclosure sentence Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * fix(collaboration): count budgets using persisted return format Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * fix(research): compare uncached command request shares Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * fix(context): preserve async reader handles in code-mode guidance Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * refactor(control-plane): isolate prompt upgrade CLI rendering Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * test(control-plane): qualify complete continuation decisions Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * fix(chat): answer exhausted requests through one restricted review Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * fix(runtime): wait for launchd job removal during service handoff Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * test(project): make the no-git registry fixture actually degrade the probe Signed-off-by: Weimr2627 <134630546+Weimr2627@users.noreply.github.com> * fix(research): distinguish declared retention from verified adoption Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * fix(doctor): hold every Git read to one budget `doctor` inspects a checkout the operator may not control, and none of its six Git reads had a budget: `rev-parse` twice, the metadata runner behind `git_metadata_for_root`, `merge-base --is-ancestor`, `remote`, `remote get-url` and `rev-parse --verify`. A repository blocked on a stale index lock, a network mount or a very large history therefore held the diagnostic open with no output at all. Reproduced with a fake `git` on PATH that only sleeps: `_is_git_worktree_root` was still running after 5s. All six reads now go through `_run_git`, bounded by `GIT_PROBE_TIMEOUT_SECONDS`. The helper answers `None` when the budget runs out, which is the answer every caller already treats as "unknown": root detection fails closed, metadata fields stay `None`, revision relation stays `UNKNOWN`, and the trusted release ref is refused. No new failure mode, and the diagnostic still returns instead of raising. The same class in `loopx/contract.py` was bounded in #8152. Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com> * fix(doctor): retire the probe's own group and keep a stale manifest out Two gaps the review found in the ten-second budget. `subprocess.run(timeout=...)` retires the Git leader only. Git starts its helpers -- fsmonitor hooks, pagers, credential helpers -- inside the group it runs in, so a blocked `status` left the hook and its child behind and repeated diagnostics accumulated them. The query now launches in a session of its own, and an exhausted budget retires that group through the existing `terminate_process_tree` before dropping the pipes instead of draining them: an inherited pipe outlives the leader, so a drain would wait on work this call no longer owns. Reproduced with a `core.fsmonitor` hook that never answers and keeps a child writing heartbeats -- leader-only kill left it growing, the group retirement stops it. `_is_git_worktree_root` answered False both for "there is no Git here" and for "there is Git here and it did not answer". `release_manifest._source_metadata` stood an older `release.json` in for the second case, so a checkout at commit B that carried a manifest claiming A wrote A back as its own source. The two cases are separate states now, and only a checkout with no `.git` of its own falls back to the recorded manifest; an unanswered read stays unknown. Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com> * fix(doctor): retire the probe's session when the diagnostic is cancelled The budget covered an exhausted wait, not an interrupted one. Git runs in a session of its own so the budget can retire its helpers, which also means a terminal Ctrl-C -- a SIGINT to the process group of the running `doctor` -- never reaches Git. `communicate` raised `KeyboardInterrupt` out of the only place that owns that session, the diagnostic exited, and the fsmonitor hook and its child kept running. Repeated cancellations accumulate the same background work the timeout fix removed. The wait now retires the owned session on any interrupted exit and re-raises the original interruption, so an exhausted budget keeps answering "unknown" while Ctrl-C stays Ctrl-C. Reproduced with the manifest writer CLI, real Git and a real `core.fsmonitor` hook that keeps a child writing heartbeats: interrupting the writer's group left the heartbeat growing on the previous head and stops it now. Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com> * fix(chat): retain context-limit failures in pending work Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * test(collaboration): recover complete per-turn delegation context Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * fix(lark): hold the collector's Git reads to one budget Admitting a collector config path asks Git three questions -- whether the project is a worktree, whether the file is tracked, and whether it is ignored -- and none of them carried a budget. A Git blocked on a stale index lock, a network mount or an oversized index therefore held the command open with no answer at all; reproduced here with a fake `git` on PATH that only sleeps, where `_project_config_path` was still running after 5s. The budget alone was not enough: a `TimeoutExpired` would have escaped the admission check as a traceback, so the refusal paths had to say what "Git did not answer" means. Acceptance needs proof that the config is ignored and untracked, so an unanswered probe refuses the path. The three reads share `GIT_PROBE_TIMEOUT_SECONDS` through `_run_git_probe`, and a Git that exits non-zero still takes the existing boundary and non-Git fallbacks unchanged. The queries are local reads that never fan out into helpers, so the leader-only kill that `subprocess.run` performs covers them. Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com> * fix(lark): stop the collector's Git questions together with its budget Two gaps in the config admission. The budget ended only the Git leader. Git starts helpers of its own -- an fsmonitor hook is the common one on `ls-files` -- inside the group it runs in, so a blocked admission returned its refusal while the hook and its child kept running, and repeated runs accumulated them. The probe now runs in a session of its own, and an exhausted budget retires that group through `terminate_process_tree` before dropping the pipes instead of draining them. The three questions each carried a fresh budget. Once the tracked query had timed out the admission could no longer be satisfied, yet `check-ignore` was still started and spent another full budget on the same hang. The questions now share one deadline, an unanswered tracked query refuses at once, and a question asked after the deadline has passed is not started at all. Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com> * fix(lark): retire the probe's session when collector-plan is cancelled The budget covered an exhausted wait, not an interrupted one. The probe runs in a session of its own so an exhausted budget can retire the helpers Git started, which also means a terminal Ctrl-C -- a SIGINT to the process group of the running `collector-plan` -- never reaches Git. `communicate` raised `KeyboardInterrupt` out of the only place that owns that session, the command exited, and the blocked Git and its helper kept running. Repeated cancellations accumulate the same background work the budget fix removed. The wait now retires the owned session on any interrupted exit and re-raises the original interruption: an exhausted budget keeps refusing the config, while Ctrl-C stays Ctrl-C. Reproduced with the `collector-plan` planning entry in a real child process, real Git and a `git` that blocks the ownership read with a helper writing heartbeats: interrupting the process group left the heartbeat growing on the previous head and stops it now. Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com> * docs: define router-coordinated controller worker units Signed-off-by: huangruiteng <huangrt01@163.com> * fix(chat): compact bounded history before new native turns Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * docs: prioritize controller attention with safe preemption Signed-off-by: huangruiteng <huangrt01@163.com> * fix(runtime): pin qualified service executables behind CLI aliases Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * docs: make controller worker execution the primary loop Signed-off-by: huangruiteng <huangrt01@163.com> * Pin the native first read and deliver the same-Turn selection route (#8100) * Reuse capture-bound navigation for the first progressive read Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Distinguish the pinned reader from an explicit navigation replan Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Pin the benchmark preflight view and deliver its same-Turn selection route Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> (cherry picked from commit 47db78b33792e584036b2f099bed4aa0904f7e2b) Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Reuse prepared Codex read guidance in benchmark checkpoint prompts Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Prove progressive benchmark choices from receiver-visible pages Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Deliver the captured review command on the first replan read Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Validate progressive replan against its published reader contract Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Isolate the user skill directory for native context comparisons Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Verify explicit guard opt-out on the real CLI Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * test(quota): exercise the original review after a successor loses its claim Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * refactor(context): reuse the quota action vocabulary for staged review Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * fix(types): widen context delivery source values Signed-off-by: huangruiteng <huangrt01@163.com> * refactor(context): make prepared native delivery the sole projection path Signed-off-by: huangruiteng <huangrt01@163.com> --------- Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Signed-off-by: huangruiteng <huangrt01@163.com> Co-authored-by: huangruiteng <huangrt01@163.com> * Read saved quota fields in the first native tool call (#8111) Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Avoid reparsing unchanged private delivery journals during discovery Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Verify captured vision audits and share them in one display request (#8113) Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Keep delivery discovery hints in their bounded provider module Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Reuse unchanged observed acceptance and preference bodies in native first reads (#8124) * Reuse observed preference and acceptance fields in native first reads Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Plan field reuse inside the existing delivery transaction and qualify invalidation Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Type the mixed native delivery request for the kernel contract check Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * Keep private field observation descriptors typed on both native routes Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * fix: observe reusable fields inside complete parent reads Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> * test: distinguish progressive defaults from delta opt-out Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --------- Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Signed-off-by: huangruiteng <huangrt01@163.com> Co-authored-by: huangruiteng <huangrt01@163.com> * fix(canary): retain current main maintainability baseline Signed-off-by: huangruiteng <huangrt01@163.com> --------- Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Signed-off-by: Weimr2627 <134630546+Weimr2627@users.noreply.github.com> Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com> Signed-off-by: huangruiteng <huangrt01@163.com> Co-authored-by: Weimr2627 <134630546+Weimr2627@users.noreply.github.com> Co-authored-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com> Co-authored-by: huangruiteng <huangrt01@163.com>
The probe runs in a session of its own so an exhausted budget can retire the helpers Git started. That also means a terminal Ctrl-C -- a SIGINT to the process group of the running `check` -- never reaches Git, and `communicate` raised `KeyboardInterrupt` out of the only place that owned that session. The scan exited while the blocked Git, its fsmonitor hook and the hook's child kept running, so repeated cancellations accumulated work the budget fix removed. An interrupted wait now retires the owned session before the cancellation keeps travelling: an exhausted budget still answers "unavailable", while Ctrl-C stays Ctrl-C. Reproduced with real Git and a blocked `ls-files` behind a helper that writes heartbeats; interrupting the scan's process group stops it now. Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>
804cabf to
7462420
Compare
loopx-agent
left a comment
There was a problem hiding this comment.
Reviewer: model_agent; model=gpt-6.1-sol; provider=OpenAI; declaration_source=runtime_reported; reasoning_effort=max.
完整复审 7462420:APPROVE,没有剩余阻断 finding。原超时误报 clean、fsmonitor 后代泄漏及取消后遗留进程均有独立修复证据。整体本地质量与合并门仍有红项,下面分别保留,不把评审通过写成已可合并。
动机 / Motivation
维护者要在公开提交前检查选定目录,既不能因Git卡住而一直等待,也不能把未完成的检查误报为安全。 受影响的是在发布前运行loopx check的维护者,以及对选定文件进行公共边界验证的工具。 公开检查依赖Git索引识别已跟踪私有状态;基线查询没有等待上限,上一修复版虽能处理超时,Ctrl-C后仍留下Git及辅助进程。 无界查询会占住检查;取消后遗留的辅助进程在重试时累积;把未知当未跟踪又会给出错误的clean结论。
上一正式评审头 O 在同类真实 Git/fsmonitor 取消场景留下3个活进程、心跳30→85;当前 H 退出 -2、无活成员、心跳25→25。 不回答的 Git 查询得到明确 unavailable,不能报告 clean。修好同一仓库后原入口可重新得出真实阻断或成功。只加timeout不足以区分未知和否定答案,也不能回收Git启动的hook;当前复用原扫描owner和现有进程树清理即可,不需新增策略存储或通用执行器。 当前精确头真实取消和超时均回收拥有组,未知不再clean,正常完整payload保持;新测试与原CI lint通过,没有剩余阻断finding。 原12项admission/checkpoint失败及分支继承的TS/ratchet/first-connect红项仍保留为独立合并资格hold;Windows、安装态及长期完整成本未测。 本轮不认证整个扫描五秒内完成、Windows原生进程树、安装态采用、长期模型效果或完整费用,也不执行合并、安装或发布。
改动思路 / Approach
原 check → check_contract → scan_public_boundary 仍以显式选择、实际文件系统和 Git 索引为输入,现有 scanner 持有完整性与私有边界规则。每次 Git 查询进入本次拥有的独立会话;预算用尽留下 unknown,取消先回收会话再原样传播;OS helper 不替调用方决定扫描安全,也不增加发布权限。
这是现有 Python filesystem/provider IO owner 内的一项有用修复:复用 terminate_process_tree 即可,不需要跨 owner 调私有 helper、增加配置或做 TS 迁移。同一异常安全生命周期覆盖超时和 Ctrl-C,范围与问题相称。本轮只评审公开边界扫描的 Git 探测,不改变配额、领取、发布权限或其他 doctor 路径。 原 status 仍传 include_public_boundary_scan=False;这不把公开内容扫描偷偷放进普通运行热路径。
每个 query 的五秒不等于整个扫描五秒。在中性临时目录的真实 Git/fsmonitor 反例中,当前头先后用两个预算,约 10.301秒 后明确拒绝,公开 README 仍扫描1项;心跳605→605、无活成员。观察发生在测试最后的独立兜底清理之前,因此证明的是产品回收。
| 实际场景 | 历史或当前对照 | 当前精确头 |
|---|---|---|
| 用户取消 | 历史 O=91c2cc681085 留3活成员、心跳30→85 |
无活成员、25→25;取消仍非零退出 |
| Git 不回答 | 原无界等待;初版会错误 clean | 明确 unavailable、无 clean、后代停止 |
| 正常 tracked/untracked/non-Git 冷/温 | 目标main快照 M的6个完整JSON | 仅新增空 unavailable_git_probes,其余相同 |
| 同仓库恢复及范围逃逸 | 不刷新权限或缓存 | 修hook后tracked拒绝;合法untracked成功;显式ignored私有路径仍拒绝 |
具体改动 / Changes
当前三点 diff 是3个文件 +483/-48:contract.py +189/-48,预算测试192行,取消测试102行。上一正式评审 O→H 补8行异常回收与取消用例;准备过程中 P=804cabfb22de 又被 amend,P→H仅整理新测试17行,生产代码和预算测试 blob 完全一致。本轮逐行读新测试,重跑当前头检查及真实 CLI,没有继承旧通过计数或批准结论。三个作者提交都有 DCO sign-off,diff/编译/双语检查通过。
关键代码讲解
_git_query,L366隔离会话并有界等待;TimeoutExpired清理后返回 unavailable,BaseException先清理再原样抛出,Ctrl-C 不再从唯一持有者处漏掉 Git 组。_retire_owned_probe,L345复用已有 OS 进程树能力,关闭继承管道并有界等待 leader;仅拥有并回收本次组。_git_probe,L422显式is None区分没答与未跟踪,unknown不再继续问 ignore,也不享有False对应的快速放行。scan_public_boundary,L1205保留完整枚举与私有读前排除,把 unavailable 纳入不完整输入;已跟踪的剪枝目录文件仍由原枚举路径补回。check_contract,L1415沿既有 JSON/error/退出码消费不完整结果,不再将失去Git答案投影为clean。
独立规范 spec_ref=docs/public-private-boundary.md; docs/reference/public-boundary-read-cost.md; .github/workflows/python-tests.yml,固定 spec_revision=927d0b389096a9ca8702c4da14a43836063ed29b。这是实际 PR merge base B;本轮目标 main 快照 M=a9485073f219a416689f62824c5117269289e596 也已检查。公共/私有边界、读取契约与 CI 文件在 B/M/H 的完整 Git blob 均相同,不拿作者新声明改写验收。
- Active State:跟踪私有状态及未知时不能错误放行,实际超时/恢复证据通过。
- Contract / 契约:完整选择、跟踪文件补回、已知未跟踪私有文件读前排除保持;六对完整payload及相关回归通过。
- User paths / 用户入口:显式 check 报真实结果,普通 status 继续 deferred;原入口完成取消与恢复,无新增问询或配置。
- Qualification / 验证:当前新增6项与 CI 原 lint 通过,规定九模块按相同 argv 逐项对照;下面12项继承失败和独立合并门仍是 FAIL,没有豁免或追认全绿。
对主干的风险 / Risks
没有剩余阻断 finding。 本次最强反例是“超时测试通过,Ctrl-C 仍让隔离组继续工作”;O 的真实反例会失败,当前头独立重跑已无残留。正常三场景各做冷/温对照,比较整个 JSON,仅替换生成 fixture 根字符串,并核对唯一新增的空 unavailable 列表;错误、warning、检查文字和计数未删。invalid UTF-8 的隐式私有文件没有被读;显式 ignored 私有文件仍阻断。
当前九规范模块加新模块为 244 PASS / 12 FAIL;相同 argv 的九模块在 B、M、H 都是 238 PASS / 同12 FAIL。不仅是汇总相同:失败 node id 与全部 E 断言行仅统一 pytest 临时目录编号后逐字相同,三点 diff 未修改相关 admission/fixture 路径。两项原 heartbeat receipt 缺失、十项 checkpoint 子进程断言继续保留,不能由这个修复核销。
语义与 CI 对齐
当前 H 的仓库 CI Lint test suite 原命令通过:uv run --extra test python -m ruff check tests loopx/canary loopx/control_plane loopx/domain_packs loopx/presentation。P曾有新增F401,旧失败回执保留;本轮先发现远端 amend,再复验 H,未发表旧头 finding。mypy 默认17根在 M/H 通过;全树 semantic smoke 在 M/H 通过,计数随 main 其它源变化而不同,不冒称完整输出一致。新 unknown/nullable/诊断是 scanner 的局部词汇 local_only,不是新共享 Enum、授权状态或领域控制规则;没有 Agent 指令改动。
分支与目标 main 必须分开:H 类型检查仍报 conversation_binding.ts(1144,59) TS2345,实际 merge base B 完整诊断相同;M 已通过。M 的该文件另有79增/3删,不在本 PR 三点 diff,不能把分支落后的问题说成 PR 新增,也不能说目标 main 快照 同样失败。M 的 raw maintainability ratchet 也已通过,H与实际merge base B保留同一旧失败;这些主线收敛未进入候选分支。相反,M 原 CI lint 另有未改 test_chat_codex_history.py 三条F811;H通过不代表这个 PR 修了它们。
当前 H quick premerge 六直接项与semantic通过,ratchet和first-connect失败。这两条 raw 命令在 B/H 各自有同一失败身份和完整归一化诊断:未改 interaction 函数规模、未改 first-connect 脚本L146的 KeyError: should_run。M上的 no-diff premerge选0项,不能当全门对照通过。P初次 Node 尚未准备的历史失败也保留,不捏造当时缺失的 raw 原因。没有抓取、轮询或等待远端 CI;整体 premerge、旧 CQR 及目标整合后的合并资格继续独立保留。
我的整体评价 / Verdict
这是 justified_increment / proportionate 的完整局部交付:查询预算、明确 unknown 与拥有组回收共同解决原问题;现有 owner 足够,最高价值的同域简化已经落实为一个异常安全生命周期。long_horizon=improved 与 user_experience=improved 的证据限真实本地检查:取消不累积后台工作,缺答案可见且原入口能恢复,不添加用户确认或策略存储。
本次评审为 APPROVE。原 P1/P2 在当前精确头有独立代码和实际反例修复证据;批准不核销仍红的本地门、旧评审或合并限制。没有页面/布局改动,实际CLI输出已审,无视觉交付需求。未测原生 Windows 与安装态,不证明全Goal效率、模型收益或完整成本;多个 query 可累计预算。本轮没有安装、发布或合并授权。
English verdict: APPROVE — 7462420 independently fixes unknown-as-clean and owned-process cleanup on timeout and cancellation; healthy full-payload controls pass. Inherited branch checks, target-main differences and merge-quality holds remain separate.
Goal And Delivered Outcome
Outcome basis / optional anchor: 无独立 issue;本 PR 给公开边界扫描里的 Git 探测加预算,并按评审意见补上「没有答案不等于否定答案」与「回收本次探测启动的进程树」两处。
Goal/source and gap: 扫描要用 Git 索引判断哪些私有状态文件已经被跟踪。加预算之后暴露出两个缺口:超时被折成
False,而False正是扫描里「不在索引」的写法,于是已跟踪的ACTIVE_GOAL_STATE.md被当成私有文件跳过,check --scan-root退出 0 并报告 clean;_tracked_scan_files的空列表则让剪枝目录内的已跟踪文件直接消失。另一个缺口是subprocess.run(timeout=...)只结束 Git 主进程,而ls-files会启动core.fsmonitorhook,预算之后它仍在运行。Observable before → after, with the validation row that proves it:
--error-unmatch阻塞,已跟踪私有状态ok=true、clean、文件被跳过ok=false,unavailable_git_probes记录该文件,报public_boundary_scan_git_unavailablels-files -z阻塞ok=false,根被记为不可用core.fsmonitorhook 不回答main.Author Declaration
Implemented against
docs/public-private-boundary.md与docs/reference/public-boundary-read-cost.md,固定faab5f5c;loopx/registry.py在 fix(registry): retire the git probe process tree on timeout #8115 中确立的进程组回收方式是实现依据。_git_probe的tracked: None分支、scan_public_boundary的unavailable_git_probespytest -q tests/test_contract_git_probe_budget.py_tracked_scan_files抛GitProbeUnavailablecheck_contract的public_boundary_scan_git_unavailablepublic_files()的tracked is False分支tests/test_contract_public_boundary_prefilter.py_git_query、_retire_owned_probecore.fsmonitor反例_git_probe的tracked is None早退、public_files的ownership_unavailablescan_public_boundary、iter_scan_files、check_contract三处消费者,确认ok与 error 的走向;用延迟查询的包装 git 复现了改前的ok=true,改后为ok=false并带 error;用真实 fsmonitor hook 验证后代停止。tests/test_contract*.py六个文件142 passed;与loopx.contract相邻的四个文件在基线上同为34 failed / 43 passed,失败集合未变;ruff --select E4,E7,E9,F全绿。没有在原生 Windows 上执行进程树回收。Scope And Continuation
改动集中在
loopx/contract.py的 Git 探测与扫描结论。ls-files -z非零退出的既有处理(返回空列表)保持不变,本 PR 只处理超时与启动失败。loopx/doctor_git.py与loopx/extensions/lark/event_collector.py的同族问题分别在 #8153、#8154 处理,三者各自是独立 owner。取消路径的收尾(
804cabfb22)评审指出的 P2 成立:
start_new_session=True让终端 SIGINT 只到达 CLI 自己的进程组,communicate抛KeyboardInterrupt时直接退出,隔离会话里的 Git 与 hook 辅助进程继续运行,反复取消会累积后台工作。本提交在
_git_query的except subprocess.TimeoutExpired之后加了一个except BaseException分支:先_retire_owned_probe(process)回收本次拥有的会话(终止该进程树、关闭继承的管道、有界等待 leader),再原样raise。预算用尽仍然返回 unavailable 并回收;取消仍然是取消,不吞掉。回归在
tests/test_contract_git_probe_cancel.py:真实 Git、被阻塞的ls-files、写心跳的辅助进程,向扫描的进程组发SIGINT,断言非零退出且返回后 0.6 秒内心跳不再增长。改前 head(91c2cc681)上这条用例失败,本提交上通过;tests/test_contract_git_probe_budget.py与正常路径对照不变。