Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions tests/test_hook_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,31 @@ def test_idle_locks_go_and_a_queue_with_events_stays(tmp_path):
assert sorted(n for n in os.listdir(base_dir) if n != ".pruned") == [".kept.lock", "kept.queue"]


def test_the_prune_leaves_the_lock_of_a_session_older_than_idle(tmp_path):
"""A session's lock file is made at its first hook, and taking the lock writes nothing to it,
so the file of a session running for longer than IDLE_S looked idle. The prune removed it
while a hook held it, and on POSIX a parallel hook of that session then locked a new file
and ran beside the holder."""
base_dir = os.path.join(str(tmp_path), "locks", "claude")
os.makedirs(base_dir)
lock = os.path.join(base_dir, ".s.lock")
open(lock, "w", encoding="utf-8").close()
old = time.time() - hookqueue.IDLE_S - 60
os.utime(lock, (old, old))
ctrl = SimpleNamespace(ctx=SimpleNamespace(root=str(tmp_path)),
host=SimpleNamespace(name="claude"), sid="s", p={},
cfg=SimpleNamespace(repomap="off"))
beside = []

def handler():
with Store(base_dir).locked("s", timeout=0.1) as got:
beside.append(got)
return {}
assert hookqueue.run(ctrl, "PostToolUse", handler) == {}
assert beside == [False]
assert os.path.getmtime(lock) > old + hookqueue.IDLE_S


# ---- what a queued hook would have said -----------------------------------------------------
def stuck_turn(env, repo):
"""A repository the guard has learned, the calls of a stuck turn, and at which of them the
Expand Down
8 changes: 8 additions & 0 deletions tokeneconomizer/plugin/hookqueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ def run(ctrl, event, handler):
name = _safe(ctrl.sid)
queue = os.path.join(base, name + ".queue")
with Store(base).locked(name, timeout=WAITS.get(event, 3.0)) as got:
if got:
# Taking a lock writes nothing to its file, so its age would be the session's, and
# `_prune` would remove the lock of a session older than IDLE_S while a hook holds
# it: on POSIX the session's next hook would lock a new file and run beside it.
try:
os.utime(os.path.join(base, f".{name}.lock"))
except OSError:
pass
_prune(base)
if got:
drain(ctrl, queue)
Expand Down
Loading