Skip to content

fix(change-window): bound every Git read to one budget - #8362

Open
gcl-coder wants to merge 1 commit into
loopx-project:mainfrom
gcl-coder:wip/change-window-v3
Open

gcl-coder wants to merge 1 commit into
loopx-project:mainfrom
gcl-coder:wip/change-window-v3

Conversation

@gcl-coder

Copy link
Copy Markdown
Contributor

动机 / Motivation

安装并启用 repository-change-window 后,每次 commit/push 都会由 Git hook 读取仓库状态并生成 pending-change 指纹。这里所有 Git 读取都走 repository.git() 的 subprocess.run,没有上限;stale index lock、网络挂载或卡住的 fsmonitor helper 会让 hook 一直等待,进而把 Git 操作本身挂住。8169 修的是 change-quality 的 scope 读取,8152/8153/8154 各有自己的 owner,这个默认关闭、一旦安装就位于提交路径的能力还没有同样的边界。

改动思路 / Approach

repository.git() 是 repository.py 中所有 Git 读取的唯一入口,因此只改这一处即可覆盖 scope、fingerprint、worktree inventory、provider config 与 hook admission 的读取。每次读取仍在独立 POSIX session 中启动;10 秒预算用尽或收到取消时,交给共享 runtime 的 retire_owned_process 在同一个 deadline 内回收本次读取自己的进程组、关闭未排空的管道。随后抛出 RepositoryChangeWindowError,让调用方 fail closed,而不是把未观察到的仓库当成 clean/empty。正常读取的 bytes、exit code 与 stdin 语义不变。这里的 10 秒是每次读取重新分配的预算,不是整个命令的共享 deadline。

Windows 分支不再是本次读取的弱点:retire_owned_process 复用 force_terminate_process 的有界路径,taskkill 卡住或返回非零都只会得到「清理未确认」,不会越过 deadline;管道 close() 放在 daemon helper 上按剩余预算 join,descendant 持有 reader 也不能延长原调用的等待。清理失败不替换本次读取的 timeout 或取消。

具体改动 / Changes

  • repository.git(): 从无上限的 subprocess.run 改为独立 session 的 Popen + communicate;超时或取消时先回收再抛出类型化错误。
  • _retire_owned_read(): 只回收本次读取拥有的进程组,Windows 与 POSIX 都由共享 runtime 约束终止与管道收尾。
  • test_change_window_git_budget.py: 覆盖正常 bytes/exit code/stdin、阻塞读取在预算内拒绝且 helper 不再存活、Git 无法启动时仍按原样抛 OSError,以及在 Windows 分支上 taskkill 卡住或非零时超时与取消都能返回。

对主干的风险 / Risks

  • POSIX 上 change-window 相关 7 项测试通过;新增 fake git 阻塞用例验证了预算内拒绝、heartbeat 停止增长;Windows 分支的两条故障 × 超时/取消都在 3 秒内返回。
  • 原生 Windows 未执行;两条 Windows 故障用例在 POSIX 上把 runtime 的 os.name 指向 Windows 分支来注入真实子进程,非 POSIX 平台按 os.name != "posix" 跳过。
  • 10 秒预算可能拒绝一个合法但极慢的 git diff --binary;这是有意的 fail-closed 边界,调用方会得到明确错误,而不是无限等待。没有改 policy、ledger schema、CLI 参数或 merge authority。
  • 依赖 fix(change-quality): hold every Git read of the scope to one budget #8169 提供的 retire_owned_process;本分支基于包含该提交的 main。

Signed-off-by: ChengLong GU <88359731+gcl-coder@users.noreply.github.com>

@loopx-agent loopx-agent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer: model_agent | gpt-6.1-sol | OpenAI | runtime_reported | reasoning_effort=max

APPROVE:未发现阻断问题;真实 Git 的 fsmonitor 阻塞会得到明确错误,未写入 pending-change ledger,解除阻塞后可继续记录。

动机 / Motivation

启用了仓库变更时间窗的开发者在提交或记录未合入工作时,需要及时得到允许、拒绝或读取失败的明确结果。旧实现会在 Git helper 卡住时无限等待;本次为每次 Git 操作设置等待上限,出错即拒绝继续,而不是把未读到的状态当成 clean。可验证的改进是调用能返回、失败不写错误指纹、解除阻塞后可重试;时间 policy、安装启用和远端写权限不在修改范围。

改动思路 / Approach

入口仍是 repository.git:正常调用保留 bytes、stdin 和非零返回处理,只有无法按时取得答案时才走已有 RepositoryChangeWindowError。新子进程在 POSIX 上拥有独立 session;取消或超时调用既有 process_runtime.retire_owned_process,统一承担进程组、Windows taskkill 和管道回收。后者有独立的有界清理预算,因此 10 秒是每次 Git communicate 的等待预算,不能解释为整个 CLI 的总 deadline。

现有 change_quality.scope 已采用同一清理 owner;复用进程生命周期而保留两个能力自己的 bytes/text、异常与输入接口,比增加跨能力 Git 框架更小。Python 代码仍属于既有 Git provider IO,没有新增 Core 决策 owner。当前切片只改变本能力的 Git 等待与取消;不改变时间 policy、安装激活、ledger 状态和提交权限。这个边界可独立回滚,也不要求用户补一次确认。

具体改动 / Changes

全 PR 为 2 文件,+236/-6;生产文件 +59/-6,新增测试 177 行。规范依据 loopx/capabilities/repository_change_window/README.md,不可变版本 4e949f405528fd8087f0623526c62f890c1ce982;使用变更前已存在且本 PR 未改的正文:install-and-inspect 的默认关闭、preview-first、安装不得授予远程写权限保持;pending-change-ledger 的完整指纹、重试幂等和失败不得伪造 clean 状态保持;authority-and-enforcement-boundary 的本地权限边界保持。

关键代码讲解

  1. git L53 接收 repo、Git 参数、check 和可选二进制 stdin。communicate 正常完成后重建 CompletedProcess;check=False 的真实错误退出码仍传给调用者,check=True 的原有 stderr/stdout 细节仍保留。没有把非零结果改成空成功。
  2. _retire_owned_read L34 只清理本次拥有的进程树;清理未确认不能替换原本的 timeout 或 KeyboardInterrupt。共享 runtime 的同版本实现已核对,未为本能力复制 Windows/POSIX 清理规则。
  3. repository_change_fingerprint L339 仍由 staged、unstaged、untracked 的真实 Git 读取生成指纹;异常发生在 ledger 写入前。独立公开 CLI 夹具验证 preview、execute、重复记录、verify、resolve、终态 list、未安装 reconcile 拒绝和恢复,base/head 的完整规范化 JSON 与退出码一致。

对主干的风险 / Risks

PASS:101 tests,1 skipped;包括真实 provider/hook、ledger、负例、stdin、退出码和共享 process runtime。独立 fsmonitor 夹具使用真实 Git,并仅将测试内等待常量设为 0.5 秒:旧版 3 秒仍不返回,由外层测试停止;本 head 约 1.85 秒返回 git diff --cached --name-only -z --no-ext-diff did not answer within 0.5 seconds,helper 心跳停止、ledger 文件摘要未变。关闭故障后两版均成功,完整恢复结果相同。首次夹具用了过短 change_id,在 Git 读取之前被拒绝,已保留该无效观察并改正,未将它算作超时证据。

本地 standard premerge 的 6 项直接检查和 2 项 catalog 检查通过。另行执行的 maintainability ratchet 仍 FAIL:Lark private_conversations.py 的 module_metric_budget;同一命令在不可变 base 与 head 的完整 450 字节输出相同,相关规则、配置及模块未改,原阈值保留。它是独立告警,不能掩盖本次反例。未查询或等待远端 CI。

默认关闭的 provider 不会因 Git 入口可用而安装或执行 hook;未安装/外部 guard 的路径和手动 ledger 路径已对照,现有真实安装/卸载与 linked-worktree 测试通过。测试仅在隔离仓库写状态。原生 Windows 未运行;POSIX 上注入的 Windows taskkill 超时/非零测试通过,但不据此认证原生 Windows 清理。极慢的合法 Git 读取也会被拒绝,这是公开 PR 已明确的默认行为变化;清理时间不计入那 10 秒等待值。

我的整体评价 / Verdict

long_horizon:improved,原本无限等待变成可观察失败与可恢复重试;未测真实模型采用或长期净成本。user_experience:improved,正常 CLI 合同保持且故障可返回,无新增操作轮次。此处没有 rendered presentation 改动,已检查 CLI/provider/ledger 路径,visual_review 为 not_applicable,未验证范围为原生 Windows 和生产安装采用。新增常量与两个本地函数不扩展共享语义词汇或 actor 权限。局部回收 wrapper 对接既有 owner,当前没有必须阻断的抽象重构;更广 Git runner 合并可按真实复用需求再判断。

English verdict: APPROVE — bounded Git failure and recovery are independently verified; native Windows remains unverified.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants