Repository navigation
修复 state.json 持久化竞态、负载膨胀与幂等重放 (PR-1) - #29
Conversation
- flush_state 改代际语义并降级不 raise,消除并发 save 触发的虚假 RuntimeError(S1) - 落盘失败最多重试 3 次后降级为 warning + _state_error(H-08) - state task 行瘦身 message<=2000/result_text<=6000,全文留 result/transcript(S2) - RESIDENT_SESSION_KEEP=50 限制常驻 worker 会话(S2,soak 实测 ~970 worker/20GB) - request_id 幂等键持久化到 state.json 的 requests 键,sha256 指纹,上限 1000(H-01) - wait_task 终态返回前 flush,新增 persisted 字段(B7)
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
- flush_state 不再 await 共享 flusher 全程:持续 save 流下按代际轮询,目标代际落盘即返回(wait_task 不再被拖死) - 抽出 _cap_resident_sessions,_run_task 完成即复查常驻上限——burst 完成不再全体滞留(此前只在 dispatch/start 时修剪) - unload 成功后弹出 _adapters 映射(over-cap 与 idle 路径一致),否则 unloaded 会话永不进入淘汰候选、状态索引持续增长
…load 任务后再关 adapter
…e worker 已由 idle_unload_sec 覆盖,上限只会误伤不可复活的 cursor/dsh);flush_state 返回落盘结果、缩短轮询至 50ms,start/stop 恢复失败即抛错,wait_task 按代际上报 persisted
There was a problem hiding this comment.
Devin Review found 1 new potential issue.
3 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| persisted = False | ||
| if task.status in TERMINAL_STATUSES: | ||
| # A terminal answer must also be on disk, not merely queued | ||
| # behind the background flusher. | ||
| persisted = await self.flush_state() |
There was a problem hiding this comment.
严重程度:高
问题: persisted 只保证「当前 _save_gen 已落盘」,并不保证「这份终态快照」已落盘。wait_task 在 task.status 已是终态时会跳过 _done.wait(),立刻 flush_state()。而 _run_task 是先改 status,再 await(finally 里的 await watch、merge_files_changed;stall 路径里更是先标 failed 再 await adapter.cancel),最后才 _done.set() + save()。窗口内上一档 running 快照往往已经 flushed,于是 persisted=True 配上内存里的终态。
触发条件: 协调端按文档短轮询 wait_task(例如 Cursor timeout_sec≈30)。上一轮 event.wait() 超时返回时,任务已在 stall/cancel/收尾中变成终态但尚未 save()。下一轮看到终态、跳过 wait、把旧代际当成已持久化。stall 取消可维持到 STALL_CANCEL_GRACE_SEC(15s),窗口足够跨过一次短轮询。
影响: 调用方以为终态已在 state.json。若此时进程退出,重启会把磁盘上的 running 收成 failed/bridge_restarted,stop_reason、error 与刚返回的 stalled/cancelled 不一致;request_id 重放也会绑到这条被改写的记录,而不是调用方以为已落盘的终态。
建议改法: 以 _done 为准,而不是内存 status:event 未 set 就继续 wait(超时仍返回 timed_out)。_run_task 里 save() 与 _done.set() 之间没有 await,等到 event 后再 flush_state() 才对应终态代际。更稳的是把终态 save() 挪到任何收尾 await 之前,并让 flush_state 等到那一档 _save_gen。
- 新增 diagnostics.redact_diagnostic,task.error/warnings、Codex/Antigravity 错误事件落盘前脱敏 - [env] deny 支持用户配置的 glob 名单,显式 set/agent env 优先;无内置禁止名单;dsh 回填同样遵守 - [agents.<name>] permission_policy:allow_once(默认)/ allow_always / deny;allow_once 下不选择 allow_always - 同步 ORCHESTRATION、SETUP、skill 与 agents.toml.example 中 #29–#32 的契约变化


Summary
深度测试(soak 三次崩溃复现)发现的持久化层缺陷修复:state.json 既当「索引」又当「数据」,且
flush_state的快照语义错误。flush_state原实现要求 await 后_pending_state全局为空,并发save()落在 await 之后会虚假抛RuntimeError拖垮整个 Registry。改为三代际计数(_save_gen/_pending_gen/_flushed_gen),只等调用时刻已发起的 save 落盘。flush_state永不 raise——最多 3 次尝试后置_state_error+ warning,快照累积性保证下次save()自动重试(H-08)。_state_task_row瘦身(message≤2000、result_text≤6000,全文仍在 result/transcript 工件);RESIDENT_SESSION_KEEP=50按 last_active 强卸最老空闲常驻会话——soak 实测idle_unload_sec=0下 ~970 会话 → ~20GB 子进程 RSS 是 OOM 主因。request_id幂等绑定持久化到 state.jsonrequests键(sha256 指纹、上限 1000、owner_pid戳使_merge_owned可删自身行),重启后重放返回reused: true。wait_task终态返回前flush_state();返回体新增persisted: bool告知落盘是否真实完成。测试:+8 用例,全套 630 passed / 1 skipped;
repro_flush_race(bursty save 下原代码 3s 必中)现 0/76000。Link to Devin session: https://app.devin.ai/sessions/b270264aa3634bc5934cad9bcec9f9ae
Open in Devin Desktop: https://app.devin.ai/desktop/session/b270264aa3634bc5934cad9bcec9f9ae?variant=devin
Requested by: @FeiZhuLulu