Skip to content

fix: harden session recovery, remote lifecycles, and Git review - #131

Open
S-huNsuke wants to merge 4 commits into
chenbstack:mainfrom
S-huNsuke:codex/fix-remaining-audit-issues
Open

S-huNsuke wants to merge 4 commits into
chenbstack:mainfrom
S-huNsuke:codex/fix-remaining-audit-issues

Conversation

@S-huNsuke

@S-huNsuke S-huNsuke commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

变更内容 / Summary

修复存档暂时读失败、正常远程请求突发和额度读取卡住时的行为:原存档保留原地并可显式重试恢复;文件面板繁忙或单条响应过大返回可恢复错误,终端连接继续工作;额度请求到期后可再次刷新。

已合入 main@2991431 并解决控制桥、CI 和网络集成测试的冲突,保留上游 PR #132 的本地 CLI、外控开关门控、后台窗格读写、请求长度限制和完整响应写入。继续使用本 PR,没有改写已发布提交历史。

审查反馈处理

对应 维护者的十项意见:

  1. 存档读取保护:I/O 失败不再移动 state.json,也不允许 fresh autosave 覆盖;启动时提供“重试并恢复 / 继续临时会话(不保存)”。确认并检查临时会话未发生未确认的变化后,重新读取并同步采用原状态。只有确认内容损坏才隔离;修好后的有效原件不会被 save 再次移动。读保护期间也跳过旧滚屏清理。
  2. 文件请求限额:单客户端或全局 I/O 达到限额时返回关联 file-busy,不踢掉终端。实际磁盘读取的票据仍保留至 I/O 完成,避免反复重试突破物理并发预算。
  3. 发送压力恢复:普通响应在总量内为终端流预留 512 KiB 和下一段输出的一个槽位;超大响应和短暂积压返回有限错误通知。通知保持生成顺序,且为最新快照拒绝独立保留一个关联控制通知,防止文件错误饱和后 UI 错认控制权。仅实际发送停滞才按期限取消连接。
  4. 额度超时:每个 agent/home 有独立的 30 秒截止任务;超时清理刷新槽、取消旧任务并保留已知额度。读取不响应取消时也不会阻止新刷新,迟到结果按请求 ID 丢弃;没有用会等待不合作子任务退出的 task-group 竞赛。
  5. Git 部分结果:未跟踪文件枚举失败时仍返回 tracked 更改,并显示本地化警告。原始 stderr 继续记录到日志;遵循项目安全约束,不向界面暴露内部路径或原始诊断。
  6. Review 加载状态:刷新/切换范围期间清除过时选择和 diff、显示 loading,拒绝旧行点击及迟到结果;整体失败时清理选择。
  7. HTTP 期限:固定截止时间只用于收请求头;响应按块发送,每块独立检查发送进度,允许持续传输超过请求头期限。
  8. 外控前置鉴权:主线程外先检查锁保护的 token/session 快照;进入主线程后重新检查权限与开关,兼顾快速拒绝无效凭证和排队期间的 stop/restart 撤销。
  9. Git 基线复用:每轮 working-tree reload 固定一次真实 HEAD OID(unborn 使用仓库格式的空树),文件选择和忽略空格复用该基线;下次 reload 重新解析。branch 范围保持原有语义,不额外增加 SSH 探测。
  10. 归档单复数:一个忙碌面板和多个面板使用独立本地化键;多数使用 %lld/Int64,保留英文和简体中文。

保留本 PR 其他修复:最终滚屏保存、有界未跟踪文件读取、Sparkle 精确版本和锁文件校验。没有持久化 schema 迁移或模块重构。

验证方式 / Testing

  • 修复提交:9f1cf6402092daf24a3a5dd4e66ea8703dd70b68;当前 head:defbb76935555e75a2c1df7764fa5800a0a4b5d0(仅触发 CI 的空提交,两者代码树完全相同)。
  • 本地最终组合 62/62 个 Node 测试入口及子测试通过,0 失败、0 跳过;这不是完整 App XCTest 数量。
  • 真实编译上游 CLI,并运行 scripts/test-cli.py:20 项通过。
  • 内部覆盖包括 Git 43 项检查、持久化 26 场景、工作区生命周期 21 场景、额度刷新 15 场景、HTTP 7 场景、发送队列 35 断言、发送/快照生命周期 5 场景和文件请求入场 7 场景。
  • 新行为均有先红后绿验证;独立审查发现并补充了交错终端输出槽位、通知顺序和饱和控制通知的反例。测试使用临时 Git 仓库、权限文件、Unix socket、loopback 连接及可控 I/O,未访问真实用户存档、Keychain 或额度 API。
  • Swift frontend parse、工程 plist/YAML、依赖锁、新文案翻译及项目规则同步检查通过。
  • 当前 head 的完整 Xcode CI:重试 41875143620 排队中,本地未执行完整 App/XCTest。 首轮 41870789601 因托管 macOS runner 无法分配,排队 15 分钟后取消,没有执行构建步骤。通过空提交触发重试,没有改写历史或修改代码。
node --test scripts/test-web-remote-*.mjs scripts/test-git-review.mjs \
  scripts/test-git-untracked.mjs scripts/test-persistence-recovery.mjs \
  scripts/test-workspace-lifecycle.mjs scripts/test-control-lifecycle.mjs \
  scripts/test-usage-refresh.mjs scripts/test-http-lifecycle.mjs \
  scripts/test-web-file-admission.mjs
node scripts/verify-dependency-lock.mjs
git diff --check

截图或录屏 / Screenshots or recordings

本机只有 Command Line Tools,未进行真实 AppKit 弹窗/Review 界面的手工验收。恢复与加载状态由真实方法回归覆盖;完整 App 构建与原生集成测试由 CI 验证。

关联议题 / Related issues

延续已合并的 #130,并处理本 PR 的维护者审查反馈;没有自动合并或发版。

@chenbstack

Copy link
Copy Markdown
Owner

感谢这一轮加固,方向很对。review 时发现几处新引入的行为风险,建议合并前处理前 4 条,其余可以后续跟进。

建议合并前修改

  1. Glint/Workspace/Persistence.swift:1118:读失败就挪走 state.json
    load(from:) 在任何 Data(contentsOf:) 失败时都会调用 preserveOriginal 把文件改名挪走,包括 EPERM、EIO、文件锁、卷或 iCloud 临时不可用这类瞬时错误。App 随后以空状态启动,下次保存会写出新的 state.json,用户的 workspace 看起来全部丢失,只能手动从 state.json.corrupt-<uuid> 恢复。改动前读失败只返回 nil、文件原地不动。
    建议:只在确认内容损坏(解码失败)时才挪走;读 I/O 错误应保留原文件并走重试或降级路径。另外 save() 里 corruptUnmovablePath 的情况也要注意:用户修好文件后,现在有效的文件会被再次挪走。

  2. Glint/WebRemote/WebRemoteServer.swift:959:文件请求超限时断开整个 client
    accessFiles 触发上限(单 client 2 个、全局 8 个)时调用 dropClientLocked,会杀掉该 client 的终端会话和所有 pane 流。浏览器文件面板同时发几个 list/read(例如打开目录的同时预览图片)就会被踢掉;其他 client 占满 8 个全局名额时,正常 client 也会被断。票据在 I/O 结束前一直占用全局预算,几个慢盘或 NFS 读取就能锁死所有 client。
    建议:超限时返回错误响应(如 rate-limit / busy),不要断连。

  3. Glint/WebRemote/WebRemoteServer.swift:1491:enqueueMessage 失败直接 cancel()
    超过 16 MiB 总字节或 256 条上限时断开整个连接。单条超过 maxQueuedBytes 的消息每次都会断连,浏览器重连后会重试并撞上同样的问题;约 5 MiB 的图片 base64 后约 7 MB,再来一个大响应或一批小 JSON 就会触发。
    建议:对超大单条消息返回错误响应;对队列积压则丢弃该条并通知,或只对确实卡死的 slow consumer 断连。

  4. Glint/Agent/UsageStore.swift:326:in-flight 去重没有超时
    refreshAgent 和 refreshCodexHomes 在已有读取进行时直接跳过(guard agentRefreshes[agent] == nil),但读取本身没有超时。网络卡住、keychain 弹窗没关、API 不返回,都会让 agentRefreshes 永远不清,之后每次 60s 定时和手动刷新都被静默忽略,额度条冻结,直到重启或开关该 agent。旧实现每次起新 Task,没有这种卡死。
    建议:给读取加超时,并保证超时或取消后清掉 agentRefreshes 条目。

建议后续跟进

  1. Glint/Review/ReviewWindow.swift:647:任何 Git 失败都替换整个文件列表
    一个慢的 ls-files --others(大仓库或 SSH)超时,就会让整个列表变成 "Couldn't load changed files",tracked 的 diff 也看不到,而且 stderr 只进 NSLog,用户看不到原因。
    建议:untracked 失败时保留 tracked 结果并提示,错误信息带上 Git 的 stderr 摘要。

  2. Glint/Review/ReviewWindow.swift:651:reload() 提前使旧 diff 失效
    reload() 在文件列表加载完成前就把 diffLoadToken 加 1 并设 loadingDiff=false。刷新或切换 scope 时如果新列表很慢或失败,diff 区域会是空白,没有 loading 状态,selected 还指向旧文件。
    建议:列表加载期间保持 loading 状态,失败时恢复或清空 selected。

  3. Glint/WebRemote/WebRemoteServer.swift:909:HTTP 固定 10 秒截止包含响应发送
    定时器从 accept 开始计时,同时覆盖收 header 和发响应,慢网络下载 web-remote bundle 或大资源时会在传输中途被取消,页面加载不全。
    建议:截止时间只用于收 header,或改成 idle timeout,响应发送阶段不套固定总时长。

  4. Glint/Agent/ControlBridge.swift:245:校验全部放在 main.sync 内
    主线程被 modal 或长布局卡住时,serve 线程会在 main.sync 上堆积(5s 的 SO_RCVTIMEO 只管读),未认证请求也会让主线程做本该被拒绝的工作。
    建议:token 和 generation 的廉价检查先在主线程外做,进入 main.sync 后再复查一次。

  5. Glint/Git/GitDiff.swift:516:每次 diff 都多跑 workingTreeDiffBase
    每个 fileDiff / changedFiles 前多 1 到 2 次串行 git 调用(rev-parse,unborn 分支下还有 symbolic-ref / show-ref / hash-object)。SSH 或大仓库下,点选文件或切换 Ignore Whitespace 的延迟会明显增加,首次调用抖动还会让本来正常的 diff 变成报错。
    建议:每次 reload 解析一次 HEAD 基准并传给 fileDiff。

  6. Glint/Workspace/WorkspaceStore.swift:3466:手拼复数的本地化文案
    归档确认里的 %d of its panes ... 在 1 个 pane 时语法错误("1 of its panes still have"),且 %d 传入的是 Int,应为 %lld。按项目 CLAUDE.md 的要求,单复数应拆成独立的 key(如 "1 file" / "%lld files"),方便其他语言单独翻译。


整体风险较高的是 1 到 4 条(数据丢失感知和无故断连)。另外 PR 说明里提到没有做本地完整 Xcode 和真实 UI 验证,上面的 Review 状态相关问题(5、6)建议在真机上点一遍确认。

🤖 Generated with Claude Code

No code changes. The prior workflow was cancelled before any build steps because no hosted runner was acquired.

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