Skip to content

fix(desktop): serialize session-lease binds against stale-subagent cleanup / 修复子代理清理持锁竞态 - #7770

Open
zdjmrq wants to merge 2 commits into
esengine:main-v2from
zdjmrq:fix/issue-7627-session-lease-coordination
Open

fix(desktop): serialize session-lease binds against stale-subagent cleanup / 修复子代理清理持锁竞态#7770
zdjmrq wants to merge 2 commits into
esengine:main-v2from
zdjmrq:fix/issue-7627-session-lease-coordination

Conversation

@zdjmrq

@zdjmrq zdjmrq commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

跟进情况(Timeline)

上次修复方式(#7405)与复发原因

#7405 做了什么

  1. CleanupStaleRunning 每次 controller build 都执行(不再缓存"已清扫"标记),扫描 stale running subagent;
  2. desktop 进程内 parent-liveness probe(SubagentParentLive):cleanup 探活 parent lease 前先检查本进程是否有 live tab 持有该 session;
  3. 启动绑定加 50ms × 2 次重试(withSessionLeaseContentionRetry),吸收亚毫秒竞争。

为何 v1.19.7 仍复现

CleanupStaleRunning每个 stale parent 执行 acquire → 重写 metadata → release,约 15+6N 次文件操作(N = 该 parent 的 running subagent 数)。Windows + Defender/AV 扫描下,多个崩溃遗留 parent 时持锁总时长可达数百 ms 甚至超过 1 秒,而:

  • retry 窗口固定 100ms(2×50ms),小于持锁时长 → 重试耗尽仍报错;
  • probe 的每个漏判点(building 自身未绑定 tab、SessionPath 尚未发布、registry 晚于 boot.Build 发布)都会回落到同一个过短的重试;
  • 多个并行 build 的 cleanup 之间没有互斥,持锁窗口可能重叠叠加。

即:#7405 缩小了偶发窗口,但没有让"绑定"与"清扫持锁"在进程内真正互斥,重试窗口又不足以覆盖 IO 驱动的持锁时长。

为何选择本修复方式

候选方案 结论
继续加大重试窗口 治标:持锁时长是 IO 驱动的、不可预测,窗口再大只是降低概率
缩短 cleanup 持锁时长(跳过 re-read、精简写入) 收益有限(大头是 acquire/release 与原子替换的固定 syscall),且削弱跨进程安全语义(re-read 防止把已完成 subagent 误标 interrupted)
进程内互斥:绑定与清扫持锁串行化 治本:两个 lease 活跃区间在进程内不可能重叠,竞争从"时间窗口"变成"排队等待",等待必然成功
跨进程协调协议(进程间锁文件) 复杂度高;desktop 有 SingleInstanceLock 保证单实例,跨进程窗口只剩 CLI/serve 等,由重试兜底即可

最终选择:进程内互斥 + 跨进程重试兜底。desktop 进程内用一把进程级 mutex 把"cleanup 的 parent-lease 探活区间"与"启动绑定的 acquire 区间"串行化;CLI/serve/ACP 等非桌面前端保持原有 lease-only 行为(nil 钩子),跨进程瞬态窗口由加大的重试(10×100ms)覆盖。

本次修复内容

  1. internal/boot/boot.go
    • Options 新增 SubagentCleanupSerialized func(fn func() error) error:在钩子内执行 stale-subagent cleanup;nil 保持原有行为。
    • newSubagentStore 接受并应用该钩子。
  2. desktop/app.go
    • App 新增 sessionLeaseCoordinationMu sync.Mutex
    • ensureTabSessionLeaseForRebuildacquireCandidateSessionLease 的 acquire 闭包持有同一把锁(withSessionLeaseCoordination)。
    • 新增 serializedSubagentCleanup,并接入全部 7 个 boot.Build 调用点(SubagentCleanupSerialized)。
    • 重试窗口 2×50ms → 10×100ms(跨进程兜底)。
  3. 锁序sessionLeaseCoordinationMu → a.mu(R)Lock / tab 锁 → agent lease 内部,双向核对无死锁环(canReclaimCurrentProcessSessionLease 与 probe 均只用 a.mu.RLock)。
  4. 测试
    • boot:TestNewSubagentStoreRunsCleanupInsideSerializedCoordinatorTestNewSubagentStoreSerializedCoordinatorErrorPropagates
    • desktop:TestEnsureTabSessionLeaseForRebuildWaitsForSerializedCleanup——模拟 cleanup 持锁超过完整 retry 预算(1.3s),并发绑定必须等待而非失败;无修复时精确复现用户报错文本(已验证红→绿)。

验证

  • go test ./internal/boot/ -run TestNewSubagentStore -count=1
  • go test ./internal/agent/ -run TestSubagentStore -count=1
  • cd desktop && go test . -run "Lease|SessionRuntime|Startup|Contention|Rebuild|Recover" -count=1 ✅(93s)
  • go vet(boot + desktop)✅、gofmt 干净 ✅
  • 唯一失败 TestGoldenBaselineNoExtensions 为改动前已存在的 golden 漂移(stash 验证与本次无关)

潜在问题 / 若再发生

  • 理论残余窗口:另一个进程(CLI/serve)的 cleanup 在极端慢盘 + AV 下对单个 parent 持锁超过 1s(>60 次 syscall 全部 >16ms)。概率极低;若再发生,调大 sessionLeaseContentionRetryAttempts 或引入跨进程协调。
  • 真实冲突不变:另一个窗口/进程真正持有 lease 时仍会报同样错误(最多多等 ≤1s),这是保护机制的正确行为。
  • 语义保持:persisted subagent metadata 格式、prompt/tool schema、恢复行为均不变。

Documentation-impact: none - internal lifecycle coordination only; existing user documentation remains correct.
Cache-impact: low - process-local lease coordination and retry-window changes only; no provider-visible content or cache key changes.
Cache-guard: focused, race, and vet coverage passed on the PR head (see Verification above).
System-prompt-review: no system prompt, tool schema, configuration, or model input content changed.

Fixes #7627
Follow-up to #7405

…eanup / 修复子代理清理持锁竞态

Problem:
The false desktop startup error "this session is already open in another
Reasonix window" (esengine#7399, esengine#7627) still reproduced on v1.19.7 even though
esengine#7405 already added a bounded startup-lease retry (2 x 50ms) and a
process-local parent-liveness probe. A multi-parent stale-subagent sweep
on Windows can hold each parent session lease for longer than the whole
retry budget, so a concurrent tab build racing that sweep could exhaust
its retries and surface the spurious error for a lease that was genuinely
free milliseconds later.

Root cause:
CleanupStaleRunning runs inside every controller build and, per stale
subagent parent, acquires the parent session lease, rewrites metadata
(15+6N file ops on Windows), then releases. With several leftover running
subagents plus AV/Defender scanning, the sweep can hold the lease for
hundreds of milliseconds to over a second. The esengine#7405 retry window
(2 x 50ms) and its probe could not cover that span, and every probe
miss (own unbound build, not-yet-published SessionPath) fell back to the
same too-short retry.

Fix:
Serialize the two lease-active regions inside one process:
- boot.Options gains SubagentCleanupSerialized; desktop passes a
  process-local mutex (App.sessionLeaseCoordinationMu) so the stale
  sweep's parent-lease probes can never overlap a startup bind.
- The two startup bind paths (ensureTabSessionLeaseForRebuild,
  acquireCandidateSessionLease) hold the same mutex while acquiring.
- The startup retry window grows to 10 x 100ms as the cross-process
  fallback (CLI/serve sweeps are transient single-parent holds).

Verification:
- go test ./internal/boot ./internal/agent -run "TestNewSubagentStore|TestSubagentStore" -count=1
- cd desktop && go test . -run "TestEnsureTabSessionLeaseForRebuild|TestSessionParentLive|TestCleanupStaleRunningWithDesktopParentProbe|TestMetaNeverReportsReadyWithoutController" -count=1
- cd desktop && go test . -run "Lease|SessionRuntime|Startup|Contention|Rebuild|Recover" -count=1
- go vet ./internal/boot && cd desktop && go vet .
New regression test TestEnsureTabSessionLeaseForRebuildWaitsForSerializedCleanup
holds the coordination lock and the parent lease for longer than the full
retry budget: it reproduces the exact reported error text without the fix
and passes with it.

Fixes esengine#7627
Follow-up to esengine#7405
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development desktop Wails desktop app (desktop/**) config Configuration & setup (internal/config) labels Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

config Configuration & setup (internal/config) desktop Wails desktop app (desktop/**) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]:

1 participant