[BUG] Race conditions in teamMemorySync watcher: shutdown may miss pending pushes or leak timers
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Two race conditions exist in src/services/teamMemorySync/watcher.ts. During shutdown, pending team memory changes may be silently lost, or a debounce timer may leak and fire a pushTeamMemory() call after the watcher has already stopped.
Note: This analysis is based on source code from v2.1.88. We do not have access to the current version's source to verify whether these issues persist. Line numbers below refer to that version.
Race A: hasPendingChanges lost update during shutdown
schedulePush() sets hasPendingChanges = true, and executePush() clears it to false. When stopTeamMemoryWatcher() runs concurrently with in-flight pushes, the following interleaving is possible:
- T1:
schedulePush()→ setshasPendingChanges = true, arms debounce timer - T2:
schedulePush()→ setshasPendingChanges = trueagain - T3:
stopTeamMemoryWatcher()readshasPendingChanges— but a push already in flight may clear it before the stop handler acts on it, or the stop handler may readtruewhile the push has already completed
Result: pending changes silently lost on shutdown, or redundant double push.
Relevant locations (v2.1.88):
- Line 146:
schedulePush()writeshasPendingChanges = true - Line 98:
executePush()writeshasPendingChanges = false - Line 374:
stopTeamMemoryWatcher()readshasPendingChanges
Race B: debounceTimer leak across shutdown
schedulePush() clears and re-arms debounceTimer. stopTeamMemoryWatcher() also clears debounceTimer. When a notifyTeamMemoryWrite() call arrives during shutdown:
stopTeamMemoryWatcher()clears the currentdebounceTimer- A concurrent
notifyTeamMemoryWrite()→schedulePush()sets a newdebounceTimerafter the stop function has already cleared the old one - The stop function completes, unaware of the new timer
- After the 2s debounce,
pushTeamMemory()fires on a stopped watcher
Result: an HTTP push request fires after the watcher has been stopped, racing with process exit.
Relevant locations (v2.1.88):
- Lines 147–150:
schedulePush()reads/clearsdebounceTimer - Lines 152–161:
schedulePush()sets newdebounceTimer - Lines 352–356:
stopTeamMemoryWatcher()reads/clearsdebounceTimer
What Should Happen?
After stopTeamMemoryWatcher() returns, no further pushTeamMemory() calls should fire. All pending changes at shutdown should either be flushed before stop completes or explicitly discarded with a warning — never silently lost.
Error Messages/Logs
No error messages — the symptoms are silent (lost writes or post-shutdown push requests).
Steps to Reproduce
Race B is reproduced by injecting a notifyTeamMemoryWrite() call during stopTeamMemoryWatcher() execution:
- Start the team memory watcher normally
- Trigger a
notifyTeamMemoryWrite()so a debounce timer is armed - Call
stopTeamMemoryWatcher()— it clears the current timer - During stop execution, trigger another
notifyTeamMemoryWrite()— this arms a new timer that stop doesn't know about - Wait 2.5 seconds after stop returns
Observed:
- Push count before wait: 1
- Push count after wait: 2 (leaked timer fired)
stopTeamMemoryWatcher()had already returned: true
Reproduction rate: 1/1 (100%)
A minimal repro test is available — happy to share or open a PR with it.
Claude Model
None
Is this a regression?
I don't know
Last Working Version
2.1.88
Claude Code Version
2.1.88
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
WSL (Windows Subsystem for Linux)
Additional Information
Suggested fix direction: Use an AbortController signal scoped to the watcher lifecycle, so all pending timers and async operations are cancelled on stop, rather than tracking individual timer references. The hasPendingChanges flag could be replaced with an atomic counter or guarded by an async mutex to prevent lost updates.
---
Found by Haokun Li, Xiaoxue Ma, and Claude Code via formal concurrency interleaving analysis.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗