sync: auto-stage dirty hub state (recovery)
This commit is contained in:
77
.claude/hooks/heartbeat.py
Normal file
77
.claude/hooks/heartbeat.py
Normal file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PostToolUse hook that pushes agent heartbeats on a throttled interval.
|
||||
|
||||
Fires on every tool call but only invokes `crosslink heartbeat` if at least
|
||||
2 minutes have elapsed since the last push. This gives accurate liveness
|
||||
detection: heartbeats flow when Claude is actively working, and stop when
|
||||
it hangs — which is exactly the staleness signal lock detection needs.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
HEARTBEAT_INTERVAL_SECONDS = 120 # 2 minutes
|
||||
|
||||
|
||||
def main():
|
||||
# Find .crosslink directory
|
||||
cwd = os.getcwd()
|
||||
crosslink_dir = None
|
||||
current = cwd
|
||||
for _ in range(10):
|
||||
candidate = os.path.join(current, ".crosslink")
|
||||
if os.path.isdir(candidate):
|
||||
crosslink_dir = candidate
|
||||
break
|
||||
parent = os.path.dirname(current)
|
||||
if parent == current:
|
||||
break
|
||||
current = parent
|
||||
|
||||
if not crosslink_dir:
|
||||
sys.exit(0)
|
||||
|
||||
# Only push heartbeats if we're in an agent context (agent.json exists)
|
||||
if not os.path.exists(os.path.join(crosslink_dir, "agent.json")):
|
||||
sys.exit(0)
|
||||
|
||||
# Throttle: check timestamp file
|
||||
cache_dir = os.path.join(crosslink_dir, ".cache")
|
||||
stamp_file = os.path.join(cache_dir, "last-heartbeat")
|
||||
|
||||
now = time.time()
|
||||
try:
|
||||
if os.path.exists(stamp_file):
|
||||
last = os.path.getmtime(stamp_file)
|
||||
if now - last < HEARTBEAT_INTERVAL_SECONDS:
|
||||
sys.exit(0)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Update timestamp before pushing (avoid thundering herd on slow push)
|
||||
try:
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
with open(stamp_file, "w") as f:
|
||||
f.write(str(now))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Push heartbeat in background (don't block the tool call)
|
||||
try:
|
||||
subprocess.Popen(
|
||||
["crosslink", "heartbeat"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user