You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
plugin: the daily lock prune deletes the lock of a session open for more than two days, and its hooks run in parallel again #210
Each session's hooks share one lock file, <root>/locks/<host>/.<session>.lock (#156). The file is created at the session's first hook. After that, taking the lock only opens the file and flocks it. Nothing is written, so the file's mtime stays at the session's first hook.
Once a day, _prune removes lock files whose mtime is more than IDLE_S (2 days) old. It runs inside run, while the hook holds its own lock:
So once a session has been open for more than 2 days (a long Claude Code desktop session, or a resumed one), its lock file looks idle. The prune then deletes it while a hook holds it. On Linux and macOS the next hook of that session opens a new file at the same path. It locks the new file right away and runs beside the hook that still holds the old one. That is the parallel-hooks race from #142: both read the session document, and the last writer drops the other's calls from the loop guard's turn. On Windows the delete fails while the file is open, so this only affects POSIX.
Repro (on main at 81e32e5, Linux)
importos, sys, tempfile, timefromtypesimportSimpleNamespacefromtokeneconomizer.pluginimporthookqueuefromtokeneconomizer.plugin.storeimportStoreroot=tempfile.mkdtemp()
base=os.path.join(root, "locks", "claude")
os.makedirs(base)
lock=os.path.join(base, ".s.lock")
open(lock, "a").close() # the session's first hook, three days agoold=time.time() -3*86400os.utime(lock, (old, old))
ctrl=SimpleNamespace(ctx=SimpleNamespace(root=root), host=SimpleNamespace(name="claude"),
sid="s", p={}, cfg=SimpleNamespace(repomap="off"))
seen= {}
defhandler():
# a parallel hook of the same session arrives meanwhilewithStore(base).locked("s", timeout=0.3) asgot:
seen["second hook got the lock"] =gotreturn {}
hookqueue.run(ctrl, "PostToolUse", handler)
print(seen)
{'second hook got the lock': True}
Expected
False. The first hook still holds the session's lock. A lock that is in use should not look idle to the prune.
I have a fix with a regression test and will open a PR.
Activity
plugin: keep a held session lock fresh, so the daily prune leaves it