Initial release: aircontext
This commit is contained in:
106
scripts/on_tool_use.py
Normal file
106
scripts/on_tool_use.py
Normal file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PostToolUse hook — lightweight ticker.
|
||||
|
||||
Responsibilities (all must complete in well under 100 ms):
|
||||
1. Read AirContext/state.json. If paused, in-progress, or in cooldown, return.
|
||||
2. Estimate active-chain token usage from transcript_path.
|
||||
3. If usage / model_context_window >= trigger.threshold, fork compactor.py
|
||||
in the background and return immediately.
|
||||
|
||||
The compactor runs detached so it never blocks Claude's main loop.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
_HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(_HERE))
|
||||
|
||||
from core.config_loader import load_config # noqa: E402
|
||||
from core.state import StateFile # noqa: E402
|
||||
from core.token_estimator import estimate_active_chain_tokens # noqa: E402
|
||||
|
||||
|
||||
def _project_root(payload: dict) -> Path:
|
||||
cwd = payload.get("cwd") or os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd()
|
||||
return Path(cwd)
|
||||
|
||||
|
||||
def _spawn_compactor(project: Path, transcript: Path, session_id: str) -> None:
|
||||
"""Detach compactor.py so the hook returns immediately."""
|
||||
compactor = _HERE / "core" / "compactor.py"
|
||||
args = [sys.executable, str(compactor),
|
||||
"--project", str(project),
|
||||
"--transcript", str(transcript),
|
||||
"--session-id", session_id]
|
||||
log_dir = project / "AirContext" / "snapshots"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_file = log_dir / "compactor.log"
|
||||
|
||||
if os.name == "nt":
|
||||
DETACHED = 0x00000008 # DETACHED_PROCESS
|
||||
NEW_GROUP = 0x00000200
|
||||
creationflags = DETACHED | NEW_GROUP
|
||||
with log_file.open("ab") as lf:
|
||||
subprocess.Popen(args, stdout=lf, stderr=lf, stdin=subprocess.DEVNULL,
|
||||
creationflags=creationflags, close_fds=True)
|
||||
else:
|
||||
with log_file.open("ab") as lf:
|
||||
subprocess.Popen(args, stdout=lf, stderr=lf, stdin=subprocess.DEVNULL,
|
||||
start_new_session=True, close_fds=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
raw = sys.stdin.read() or "{}"
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return 0
|
||||
|
||||
project = _project_root(payload)
|
||||
air = project / "AirContext"
|
||||
config_path = air / "config.yaml"
|
||||
if not config_path.exists():
|
||||
return 0 # uninitialised — nothing to do
|
||||
|
||||
state = StateFile(air / "state.json")
|
||||
snap = state.read()
|
||||
if snap.get("paused") or snap.get("compaction_in_progress") or snap.get("compaction_ready"):
|
||||
return 0
|
||||
|
||||
try:
|
||||
cfg = load_config(config_path)
|
||||
except Exception:
|
||||
return 0 # config broken — UserPromptSubmit will surface it
|
||||
|
||||
cooldown = int(cfg.get("trigger", {}).get("cooldown_seconds", 300))
|
||||
if time.time() - snap.get("last_compaction_unix", 0) < cooldown:
|
||||
return 0
|
||||
|
||||
transcript = payload.get("transcript_path")
|
||||
if not transcript or not Path(transcript).exists():
|
||||
return 0
|
||||
session_id = payload.get("session_id") or snap.get("last_session_id")
|
||||
if not session_id:
|
||||
return 0
|
||||
|
||||
threshold = float(cfg.get("trigger", {}).get("threshold", 0.6))
|
||||
window = int(cfg.get("trigger", {}).get("model_context_window", 200000))
|
||||
method = cfg.get("trigger", {}).get("estimate_method", "char_div_3_5")
|
||||
|
||||
used = estimate_active_chain_tokens(Path(transcript), method=method)
|
||||
if used / max(window, 1) < threshold:
|
||||
return 0
|
||||
|
||||
# Mark in-progress immediately so successive PostToolUse calls don't double-fire.
|
||||
state.update(compaction_in_progress=True, last_session_id=session_id)
|
||||
_spawn_compactor(project, Path(transcript), session_id)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user