# aider-watch 設計稿（v1 — ⛔ DEPRECATED 2026-04-20）

> ⛔ **已被 v2 取代** — 統帥於 2026-04-20 指出此設計未對齊 awoooi AI 自主化飛輪全景，
> 獨立 PG + 獨立 bot + 獨立 repo 路線與 MASTER blueprint 割裂。
> 新版 spec：[2026-04-20-aider-watch-v2-design.md](2026-04-20-aider-watch-v2-design.md)
> v1 保留僅供歷史對照；實作已停在 ~/aider-watch Task 5（8 commits）。

> 統帥本機 aider CLI 的全程監控系統  
> 2026-04-19 Brainstorm 定稿，經統帥 §1-§7 逐段批准

---

## 背景與目標

統帥 2026-04-19 於本機安裝 aider 0.86.2（via pipx，Python 3.11.7），搭配 OpenRouter 免費 stealth 模型 `openrouter/openrouter/elephant-alpha`。aider 無原生 webhook/event stream，若不加監控，aider 自主工作時統帥無法掌握：改了什麼檔、花了多少 token、有沒有卡住、有沒有失敗。

**目標**：aider 每次執行都被完整記錄 + 重要事件即時推播到 Telegram DM + 每日/每週自動彙總。故障時 aider 本身不受影響（graceful degradation）。

**非目標**：
- 不整合 SignOz / awoooi telemetry（aider 是個人工具，避免綁死 awoooi infra）
- 不做 Web dashboard（本次先不碰，未來再議）
- 不監控其他人/其他機器（單機 single-user）

---

## §1 架構總覽

```
  統帥下指令
     │
     ▼
  aiderw (Python wrapper, 取代 `aider`)
     │
     ├─ 啟 subprocess: aider [統帥的參數...]
     │
     ├─ stdout tee ──────► [A] 即時解析 → 即時 events
     │                        • session_start (開場)
     │                        • silent_timeout (卡住 >2min)
     │                        • error (stderr / 異常 pattern)
     │
     └─ 退出時 ──────────► [B] 讀 .aider.chat.history.md + git diff
                              → canonical events
                              • file_edit (每檔 +N -M + 3 行 diff)
                              • commit (若 auto-commit=true)
                              • session_end (duration, tokens, cost, exit_code)

  [A] + [B] 合流 → Event Dispatcher
     │
     ├─► Telegram Bot → DM chat_id 5619078117
     ├─► Postgres 192.168.0.188/aider_watch
     │      └ 離線時 fallback → ~/aider-watch/buffer/*.jsonl
     ├─► ~/aider-watch/sessions/YYYY/MM/DD/<sid>.{jsonl,md}   (source-of-truth)
     └─► ~/aider-watch/live.log  (append plain text, tail -F 用)

  Launchd Jobs (獨立於 aiderw):
     • com.awoooi.aider-watch.flush    每 5 min 補寫 PG (buffer → PG)
     • com.awoooi.aider-watch.daily    每日 23:50 → 日報
     • com.awoooi.aider-watch.weekly   週日 22:00 → 週報
```

**關鍵設計決定**：

1. **雙層 event 來源**：stdout (即時性) + chat_history.md (準確性) 互補，不怕 aider 改 TUI 格式
2. **PG 非強依賴**：Mac 離線時 JSONL 做緩衝，連線恢復自動補寫 → aider 不因 PG 掛掉而失敗
3. **JSONL 是 source-of-truth**：即使 PG 資料毀了，靠 JSONL 可 replay 重建
4. **報表工作解耦**：3 支 launchd job 獨立 Python script，彼此壞不影響

---

## §2 元件分工

5 個獨立單元，每個有明確單一職責、介面、依賴：

| # | 檔案 | 職責 | 介面 | 依賴 |
|---|------|------|------|------|
| 1 | `aider_watch/wrapper.py` | **主入口**：fork aider subprocess、tee stdout、寫 live.log、發 event 到 dispatcher、exit 時讀 chat_history.md 重建 canonical events | CLI: `aiderw [args]` → 透明轉發給 aider；退出碼同 aider | `subprocess`, `pexpect` |
| 2 | `aider_watch/events.py` | **Event 資料契約**：dataclass/pydantic 定義 7 種 event type、序列化/反序列化 | `SessionStart`, `FileEdit`, `Error`, `Commit`, `SilentTimeout`, `SessionEnd`, `Raw` | `pydantic` |
| 3 | `aider_watch/storage.py` | **雙寫儲存**：優先寫 PG；PG 不可達時 fallback `buffer/*.jsonl`；提供 `flush_buffer()` 清空 | `write_event(ev)`, `write_session(s)`, `flush_buffer()` | `psycopg2`, `json` |
| 4 | `aider_watch/telegram.py` | **Telegram 客戶端**：格式化、rate-limit、429 指數退避、失敗不 raise（aider 不能卡） | `send(event) -> bool` | `requests` |
| 5 | `aider_watch/reporter.py` | **報表**：從 PG 撈資料 → 產日/週報 Telegram markdown | `daily()`, `weekly()` | `psycopg2` |

**CLI subcommands**：

```
aiderw [aider args...]           # 包 aider 跑
aider-watch report daily         # 立即產日報 (launchd 呼叫)
aider-watch report weekly        # 立即產週報 (launchd 呼叫)
aider-watch flush                # 手動 flush buffer (launchd 呼叫)
aider-watch doctor               # 檢查 PG 連線、Telegram 通道、設定檔
aider-watch replay <session_id>  # 從 JSONL 重建事件到 PG
```

**單元資料流**：

```
wrapper ──events──► events ──validated──► storage ──► PG / JSONL buffer
                              └──────────► telegram ──► Bot API
                              └──────────► live.log (tee via wrapper)
reporter ──query PG──► telegram
```

**重要**：wrapper 絕不直接碰 PG 或 Telegram，只發 event 給 dispatcher。dispatcher 可 mock 測。

---

## §3 Event Schema

7 種 event type，統一外殼 + type-specific payload：

```json
{
  "ts": "2026-04-19T22:05:33+08:00",
  "session_id": "01J7XZ...",
  "type": "session_start",
  "payload": { ... }
}
```

| type | 來源 | payload |
|------|------|---------|
| `session_start` | stdout (aider banner) | `{cwd, model, aider_args, aider_pid, cli_version}` |
| `file_edit` | 退出後 git diff + chat_history.md | `{path, lines_added, lines_deleted, diff_head, is_new_file}` |
| `error` | stderr + exit code ≠ 0 + "error" pattern | `{kind: "api_rate_limit\|api_auth\|build_fail\|unknown", message, context_50chars}` |
| `commit` | 退出後 git log 比對 | `{sha, message, author, files[]}` |
| `silent_timeout` | wrapper 監測：stdout >120s 無輸出 | `{idle_sec, last_output_tail}` |
| `session_end` | subprocess 結束 | `{duration_sec, tokens_sent, tokens_received, cost_usd, files_changed, error_count, exit_code}` |
| `raw` | debug 用，壓縮 stdout 片段 | `{chunk: "..."}` — 不推 Telegram，只寫 JSONL |

**Telegram 訊息範例**（對應事件 1-5, 8；事件 6/7 user/assistant message 刻意不推）：

```
🚀 aider 啟動
repo: /Users/ogt/awoooi
model: elephant-alpha
pid: 12345

✏️ edit apps/api/src/jobs/foo.py (+12 -3)
    @@ def scan_targets():
    +    logger.info("starting")
    -    pass

❌ error: api_rate_limit — OpenRouter 429 on elephant-alpha

📌 commit abc123: "fix: handle null auth"
    → apps/api/src/auth.py, apps/api/tests/test_auth.py

⏸️ aider 靜默 2 分鐘 (last: "Tokens: 4.2k sent")

🏁 session 結束
⏱️ 5m32s | 🎯 exit=0
📊 12.4k↑ / 3.8k↓ tokens | $0 (Elephant Alpha)
📝 改 3 檔 | ❌ 0 error
```

**時區**：所有 timestamp 以台北時區 `+08:00` 儲存（`feedback_timezone_taipei.md`）。

---

## §4 Postgres Schema

**Database**: `aider_watch`（獨立於 awoooi 其他 DB，同 PG instance 192.168.0.188:5432）  
**User**: `aider_watch`（只在此 DB 有權限，不碰 K3s datastore 或其他 awoooi DB）

```sql
-- ===== Sessions：一場 aider 執行 =====
CREATE TABLE sessions (
  id            TEXT PRIMARY KEY,              -- ULID
  started_at    TIMESTAMPTZ NOT NULL,
  ended_at      TIMESTAMPTZ,
  cwd           TEXT NOT NULL,
  model         TEXT NOT NULL,
  aider_args    TEXT[],
  aider_pid     INTEGER,
  duration_sec  INTEGER,
  tokens_sent   INTEGER DEFAULT 0,
  tokens_recv   INTEGER DEFAULT 0,
  cost_usd      NUMERIC(10,6) DEFAULT 0,
  files_changed INTEGER DEFAULT 0,
  error_count   INTEGER DEFAULT 0,
  exit_code     INTEGER,
  host          TEXT DEFAULT 'ogt-mac'
);
CREATE INDEX sessions_started_idx ON sessions(started_at DESC);
CREATE INDEX sessions_cwd_idx ON sessions(cwd);

-- ===== Events：每個細粒度事件 =====
CREATE TABLE events (
  id            BIGSERIAL PRIMARY KEY,
  session_id    TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
  ts            TIMESTAMPTZ NOT NULL,
  type          TEXT NOT NULL,
  payload       JSONB NOT NULL
);
CREATE INDEX events_session_ts_idx ON events(session_id, ts);
CREATE INDEX events_type_idx ON events(type);
CREATE INDEX events_ts_idx ON events(ts DESC);

-- ===== File touches：每次檔案變更展平，便於 top-N 查詢 =====
CREATE TABLE file_touches (
  id            BIGSERIAL PRIMARY KEY,
  session_id    TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
  path          TEXT NOT NULL,
  touched_at    TIMESTAMPTZ NOT NULL,
  lines_added   INTEGER DEFAULT 0,
  lines_deleted INTEGER DEFAULT 0,
  is_new_file   BOOLEAN DEFAULT FALSE
);
CREATE INDEX ft_path_idx ON file_touches(path);
CREATE INDEX ft_session_idx ON file_touches(session_id);
CREATE INDEX ft_date_idx ON file_touches((touched_at::date));
```

**為什麼不用純 event sourcing**：查詢日報每次都要 aggregate events 太慢。sessions 表是派生摘要，退出時一次寫。events 表原樣保留，萬一 session 摘要錯可從 events 重建。

**日/週報查詢範例**（reporter.py 用）：

```sql
-- 今日統計
SELECT count(*) AS sessions, sum(duration_sec) AS total_sec,
       sum(tokens_sent) AS toks_sent, sum(files_changed) AS files,
       sum(error_count) AS errors
FROM sessions WHERE started_at::date = CURRENT_DATE;

-- 今日 Top 3 最常改的檔
SELECT path, count(*) AS touches, sum(lines_added) AS adds, sum(lines_deleted) AS dels
FROM file_touches WHERE touched_at::date = CURRENT_DATE
GROUP BY path ORDER BY touches DESC LIMIT 3;
```

---

## §5 檔案佈局

```
~/aider-watch/                             # 主工作目錄
├── bin/aiderw                             # 入口 shim
├── aider_watch/                           # Python 套件
│   ├── __init__.py
│   ├── wrapper.py
│   ├── events.py
│   ├── storage.py
│   ├── telegram.py
│   ├── reporter.py
│   └── cli.py
├── sessions/                              # source-of-truth（永遠寫）
│   └── 2026/04/19/
│       ├── 01J7XZABC_awoooi_12345.jsonl
│       └── 01J7XZABC_awoooi_12345.md
├── buffer/                                # PG 離線時 fallback
│   └── pending_01J7XZABC.jsonl
├── live.log                               # append-only 純文字流
├── state.json                             # 健康狀態
└── logs/                                  # launchd job stdout/stderr
    ├── flush.log
    ├── daily.log
    └── weekly.log

~/.aider-watch.env                         # chmod 600
    AIDER_WATCH_DATABASE_URL=postgresql://aider_watch:<PW-安裝時生成>@192.168.0.188:5432/aider_watch
    AIDER_WATCH_TELEGRAM_TOKEN=<bot token - 已確認 @NemoTronAwoooI_Bot>
    AIDER_WATCH_TELEGRAM_CHAT_ID=5619078117
    AIDER_WATCH_HOSTNAME=ogt-mac

~/Library/LaunchAgents/
├── com.awoooi.aider-watch.flush.plist     # 每 5 min
├── com.awoooi.aider-watch.daily.plist     # 每日 23:50
└── com.awoooi.aider-watch.weekly.plist    # 每週日 22:00

/opt/homebrew/bin/aiderw → ~/aider-watch/bin/aiderw   # PATH 入口 symlink
```

**PATH 策略**：
- `aiderw` → symlink → Python 包 → subprocess aider
- 原生 `aider` 仍可直接呼叫（繞過監控，debug 用）
- 可選：shell alias `alias aider=aiderw`，不強制

---

## §6 錯誤處理 / 降級策略

| 失敗情境 | 處理 | 結果 |
|---------|------|------|
| **PG 188 不可達** | 寫 `buffer/pending_<sid>.jsonl`；`state.json` 記 `last_flush_fail_ts`；launchd flush job 每 5min 重試 | 資料不丟 |
| **Telegram 429** | 指數退避 3 次（1s→4s→16s），失敗→ `live.log` + 旗標進下次日報 | aider 絕不卡住 |
| **Telegram 網路 timeout** | 同上，最多 5s | 同上 |
| **aider subprocess 崩潰 / OOM** | wrapper 收 SIGCHLD → 寫 `session_end` `exit_code≠0` `error_count≥1` + Telegram 推 error | 死得清楚 |
| **wrapper 自己崩潰** | `atexit` handler 強寫 `session_end` 含 `wrapper_crash=true` | 半份紀錄仍有 |
| **stdout 解析漏掉 event** | 退出時 chat_history.md + git diff 補上 canonical events | 準確性不犧牲 |
| **secrets 外洩** | `~/.aider-watch.env` chmod 600；payload 偵測 `sk-or-*`、`ghp_*`、`AIza*` pattern → 遮罩 `<redacted>` | 符合 `feedback_secrets_leak_incidents_2026-04-18.md` |
| **Launchd job 死** | `KeepAlive=true` + stderr 寫 `logs/*.log`；`doctor` 指令檢查狀態 | `aider-watch doctor` 一眼看出 |
| **PG DB 被手動刪** | `aiderw` 啟動時 ping PG + 必要表 → 缺表自動跑 migration（內建 `schema.sql`） | 自癒 |
| **磁碟爆滿** | `sessions/` JSONL+MD 保留 90 天，更早壓縮成 `YYYY-MM.tar.gz` 移到 `archive/`；**PG 不設 retention**（資料量小，查詢歷史統計用）；doctor 顯示本機磁碟用量 | 有上限 |

**核心原則**：aiderw 的任何故障絕不能讓 aider 跑不起來。wrapper 外層 try/except 全包，最壞情況只是「監控失效」，aider 本身 pass-through 照跑。

---

## §7 測試策略

遵循 `feedback_no_mock_testing.md`（禁 mock），盡量用真服務。

| 層級 | 範圍 | 怎麼跑 | 通過標準 |
|------|------|--------|----------|
| **Unit** | `events.py` schema、`telegram.py` 格式化、secret redactor | pytest，fixtures 不 mock | 欄位 100% 正確、redactor 6 種 token pattern 全中 |
| **Integration: PG** | `storage.py` 真寫獨立的 `aider_watch_test` DB（非 prod `aider_watch` 內的 schema） | 跑真 PG 188，測完 DROP | session/event/file_touches round-trip |
| **Integration: Telegram** | 真發訊息到 chat_id 5619078117 | 前綴 `[TEST]` | Telegram API 回 ok=true、收到訊息 |
| **E2E: aider session** | `aiderw --message "print hello" --exit` 跑真 aider | `/tmp/aider-smoke` | TG 收 🚀+🏁；PG sessions 有 1 筆；JSONL 產生 |
| **E2E: 斷線降級** | `ifconfig en0 down/up`；跑 aiderw；恢復網路；跑 flush | 手動 | buffer 產生 → flush 後 PG 補齊、buffer 清空 |
| **E2E: 日/週報** | `aider-watch report daily/weekly` | PG 有至少 1 筆 session | TG 收到日/週報，統計正確 |
| **Doctor** | `aider-watch doctor` | 正常 + 刻意破壞（停 PG / 刪 env / 刪 plist） | 全綠 / 破壞時精準指出問題 |

**關鍵**：第一次上線必跑 E2E 斷線降級測試。這是整個設計最容易壞的地方。

---

## 相關 Memory 依據

- `feedback_timezone_taipei.md` — timestamp +08:00
- `feedback_no_mock_testing.md` — 測試不 mock
- `feedback_secrets_leak_incidents_2026-04-18.md` — secret 遮罩 + chmod 600
- `feedback_change_annotation_standard.md` — 檔案變更註解標準（實作時加入）
- `reference_aider_setup.md` — aider 本機安裝現況
- `reference_four_hosts.md` — PG 位於 192.168.0.188:5432
- `reference_secrets_architecture_v2.md` — secret 4 層架構（本工具走 L4 本機 env file）

---

## 後續

本設計稿批准後，進入 writing-plans 階段產出逐步實作計畫（建立 DB/user、寫 Python 套件、裝 launchd、E2E 驗收 ...）。
