fix(core): lock CommandRegistry.agentDirs in SetAgentDirs and Resolve - #1662
Open
hi-neason wants to merge 1 commit into
Open
fix(core): lock CommandRegistry.agentDirs in SetAgentDirs and Resolve#1662hi-neason wants to merge 1 commit into
hi-neason wants to merge 1 commit into
Conversation
SetAgentDirs wrote r.agentDirs with no lock held, while Resolve read the same field after releasing r.mu.RUnlock(), even though ListAll accessed it under RLock. Today the write happens at engine construction and does not overlap with Resolve, but the inconsistent locking meant any future runtime re-bind raced on the slice header under -race. - Take r.mu.Lock in SetAgentDirs and copy the incoming slice so later caller mutations don't leak into the registry. - Snapshot agentDirs under RLock in Resolve before doing file IO, so the lock is not held across disk reads but the slice header is read safely. - Add a -race regression test that concurrently swaps agent dirs while other goroutines resolve config and agent commands. Co-Authored-By: Claude <noreply@anthropic.com>
chenhg5
approved these changes
Aug 13, 2026
chenhg5
left a comment
Owner
There was a problem hiding this comment.
结论: Approve
总体判断: 一个与 PR #1664 同模式的 concurrency fix——把 CommandRegistry.agentDirs 的访问统一到 r.mu 锁下,与既有 ListAll 的 RLock 约定对齐。修复面精准,race test 同样在 fix 前会红、fix 后绿。建议合入。
Review 范围:
- 看了
core/command.go中SetAgentDirs+Resolve的锁添加 + slice copy 防御。 - 看了新增
core/command_race_test.go(TestCommandRegistry_ConcurrentDirSwap)。 - CI: run 31349096370 全绿。
✅ 做得好的地方:
- 同 #1664 模式:与 Discord race fix 一致的写法(Lock + defensive slice copy + snapshot-then-release),形成 codebase 内部统一的 concurrency pattern。
SetAgentDirs主动 slice copy:即使 caller 后续修改原 slice,registry 内部 state 不受影响——避免「race-by-side-channel」类的隐蔽 bug。Resolvesnapshot 后释放锁再 IO:锁内只读 slice header,IO 在锁外,避免锁跨越 syscall / disk read。- Race test 覆盖多入口:writers 反复 swap + readers 从 config/agent commands + ListAll 三处并发读——验证「所有 reader 路径」都受保护,不是只测一个入口。
🟠 建议改进(不阻塞):
Resolvesnapshot 后用 local slice range,但agentDirs[i]仍是 slice header 的间接引用:作者已经 copy 了 SetAgentDirs 入口的 slice,但 Resolve snapshot 的是 slice header 本身(slice 是 value type)。如果 writer 在 snapshot 后但 range 前修改了 underlying array 元素,会读到错的值。建议 Resolve 内对 snapshot slice 再 copy 一次,或确保 agentDirs 元素本身是 immutable(看现有实现是 string,应该 immutable,所以 OK——但加注释明确更稳)。engine.go:749是 SetAgentDirs 当前唯一调用点:作者指出 set happens only at engine construction,但 PR 没把这条调用注释加进去——建议在SetAgentDirs注释里写明「called once at engine construction; runtime re-bind is currently not supported」,避免 future 误用。
🔵 可选优化:
- 无。
Testing / Risk:
- 已看到的验证: -race 模式下 race test 通过;CI 全绿;fix revert 后 race test 失败(作者 self-verify)。
- Blast radius: 仅
core/command.go,对调用者 no behavior change。
Next step:
- 建议 owner 直接 merge。Concurrency fix 范围精准,与 #1664 同模式,race test 设计好。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
core/command.goguards thecommandsmap withr.mu, but access tor.agentDirswas inconsistent:ListAllreads it underRLock,SetAgentDirswrote it with no lock at all, andResolvereleased the read lock before ranging over it. TodaySetAgentDirsis only called at engine construction (engine.go:749), so there is no live crash, but the locking contract is misleading — any future runtime re-bind (or ago test -racerun that overlaps setup with message handling) races on the slice header.ListAllin the same file already treatsagentDirsas lock-guarded, so this just makes the other two methods follow that contract.Change
SetAgentDirs: taker.mu.Lock()and copy the incoming slice before storing it, so later mutations by the caller do not leak into the registry under a reader.Resolve: snapshotr.agentDirsunderRLockbefore doing file IO, so the slice header is read safely without holding the lock across disk reads.Type of change
Testing
Automated tests added in this PR
core/command_race_test.goTestCommandRegistry_ConcurrentDirSwap— spawns writers that repeatedly swapagentDirsbetween two temp directories while readers callResolveon a config command, agent commands, andListAll. Fails under-raceif either the write or the post-unlock read is unlocked.For bug fixes only — regression test
TestCommandRegistry_ConcurrentDirSwap.go test -race -run TestCommandRegistry_ConcurrentDirSwap ./core/failed withrace detected during execution of testpointing at the unlockedSetAgentDirswrite and the unlockedResolveread.Critical User Journeys (CUJ) impact
Manual / user-visible behavior change
None.
Checklist (reviewer will verify)
go build ./...passesgo test ./...passes, includinggo test -race -run TestCommandRegistry_ConcurrentDirSwap ./core/core/Related
ListAllin the same file already readsagentDirsunderRLock; this PR makesSetAgentDirsandResolvefollow that same contract.