Files
awoooi/scripts/aider_watch_client/aider_watch_client/aiderw.py
Your Name 9e9bd8679f
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
fix(aider-watch): code-review fixes (4 issues)
1. aiderw: session_end 補 model+cwd (AI Router feedback loop 修通)
2. repository: model_stats_since SQL 改 COALESCE(session_end, session_start) model
3. aider_event_service: classify_severity 移除 error_count 觸發告警(防假陽性)
4. worker: run_aider_event_processor_loop 包 proc.start() try/except(防靜默崩潰)

2026-04-20 @ Asia/Taipei
2026-04-21 00:59:21 +08:00

183 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# aider-watch-client aiderw | 2026-04-20 @ Asia/Taipei
"""aider subprocess wrapper — capture events, dispatch to awoooi (fallback buffer)."""
from __future__ import annotations
import atexit, json, os, subprocess, sys, threading, time
from datetime import datetime
from pathlib import Path
from ulid import ULID
from aider_watch_client import config, buffer
from aider_watch_client.api_client import post_events
from aider_watch_client.parsers import (
parse_stdout_banner, parse_chat_history, git_numstat, git_diff_head,
)
from aider_watch_client.redactor import redact
IDLE_THRESHOLD_SEC = 120
def _now_iso() -> str:
return datetime.now(config.TAIPEI).isoformat()
def _emit_live(ev: dict) -> None:
try:
with config.LIVE_LOG.open("a") as f:
red = redact(ev.get("payload", {}))
f.write(f"[{ev['ts']}] {ev['type']} "
f"{json.dumps(red, ensure_ascii=False, default=str)[:200]}\n")
except Exception:
pass
def _dispatch(events: list[dict]) -> None:
"""batchredact → live.log → HTTP POST失敗進 buffer。"""
redacted = [{**e, "payload": redact(e.get("payload", {}))} for e in events]
for e in redacted:
_emit_live(e)
if not post_events(redacted):
buffer.write(redacted)
def _pre_git_sha(cwd: Path) -> str | None:
try:
r = subprocess.run(["git","-C",str(cwd),"rev-parse","HEAD"],
capture_output=True, text=True, timeout=3)
return r.stdout.strip() if r.returncode == 0 else None
except Exception:
return None
def main(argv: list[str] | None = None) -> int:
argv = list(argv if argv is not None else sys.argv[1:])
config.ensure_dirs()
cwd = Path.cwd()
sid = str(ULID())
start_ts = _now_iso()
start_epoch = time.time()
stdout_buf: list[str] = []
last_output = [time.time()]
error_count = [0]
pre_sha = _pre_git_sha(cwd)
host = os.environ.get("AIDER_WATCH_HOSTNAME") or os.uname().nodename
aider_bin = os.environ.get("AIDER_BIN") or str(Path.home() / ".local/bin/aider")
# subprocess start
try:
proc = subprocess.Popen([aider_bin] + argv, cwd=str(cwd),
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, bufsize=1)
except FileNotFoundError:
print(f"aider binary not found at {aider_bin}", file=sys.stderr)
return 127
# session_start ASAP
_dispatch([{
"ts": start_ts, "session_id": sid, "host": host,
"type": "session_start",
"payload": {"cwd": str(cwd), "model": "unknown",
"aider_args": argv, "aider_pid": proc.pid,
"cli_version": "unknown"},
}])
# stdout reader
def _reader():
for line in proc.stdout: # type: ignore[arg-type]
sys.stdout.write(line); sys.stdout.flush()
stdout_buf.append(line)
last_output[0] = time.time()
lo = line.lower()
if "error" in lo or "exception" in lo:
error_count[0] += 1
t = threading.Thread(target=_reader, daemon=True); t.start()
# silent watcher
silent_fired = [False]
def _silent_watch():
while proc.poll() is None:
time.sleep(10)
idle = time.time() - last_output[0]
if idle >= IDLE_THRESHOLD_SEC and not silent_fired[0]:
silent_fired[0] = True
tail = "".join(stdout_buf[-1:])[-80:] if stdout_buf else ""
_dispatch([{"ts": _now_iso(), "session_id": sid, "host": host,
"type": "silent_timeout",
"payload": {"idle_sec": int(idle),
"last_output_tail": tail}}])
w = threading.Thread(target=_silent_watch, daemon=True); w.start()
# atexit safety
emergency_fired = [False]
def _emergency():
if emergency_fired[0]:
return
emergency_fired[0] = True
_dispatch([{"ts": _now_iso(), "session_id": sid, "host": host,
"type": "session_end",
"payload": {"duration_sec": int(time.time() - start_epoch),
"tokens_sent": 0, "tokens_received": 0,
"cost_usd": 0, "files_changed": 0,
"error_count": error_count[0], "exit_code": -999,
"wrapper_crash": True}}])
atexit.register(_emergency)
exit_code = proc.wait()
emergency_fired[0] = True # 正常結束,關閉保險
duration = int(time.time() - start_epoch)
stdout_text = "".join(stdout_buf)
events_tail: list[dict] = []
# canonical file edits from git diff
if pre_sha:
for e in git_numstat(cwd, pre_sha, "HEAD"):
head = git_diff_head(cwd, e["path"], pre_sha, 3)
events_tail.append({
"ts": _now_iso(), "session_id": sid, "host": host,
"type": "file_edit",
"payload": {"path": e["path"], "lines_added": e["lines_added"],
"lines_deleted": e["lines_deleted"],
"diff_head": head,
"is_new_file": e.get("is_new_file", False)},
})
# tokens from chat_history
tokens_sent = tokens_recv = 0
chat_md = cwd / ".aider.chat.history.md"
if chat_md.exists():
stats = parse_chat_history(chat_md.read_text(errors="ignore"))
tokens_sent = stats["tokens_sent"]; tokens_recv = stats["tokens_received"]
if exit_code != 0:
events_tail.append({
"ts": _now_iso(), "session_id": sid, "host": host,
"type": "error",
"payload": {"kind": "non_zero_exit",
"message": f"aider exited with {exit_code}",
"context_50chars": stdout_text[-50:]},
})
banner = parse_stdout_banner(stdout_text)
events_tail.append({
"ts": _now_iso(), "session_id": sid, "host": host,
"type": "session_end",
"payload": {"duration_sec": duration,
"tokens_sent": tokens_sent,
"tokens_received": tokens_recv, "cost_usd": 0,
"files_changed": len([e for e in events_tail
if e["type"] == "file_edit"]),
"error_count": error_count[0],
"exit_code": exit_code,
"model": banner.get("model", "unknown"),
"cwd": str(cwd)},
})
if events_tail:
_dispatch(events_tail)
return exit_code
if __name__ == "__main__":
sys.exit(main())