auto: 일일 백업 2026-09-11 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"bootstrapSeededAt": "2026-03-26T14:36:09.405Z",
|
||||
"setupCompletedAt": "2026-04-13T15:02:39.856Z"
|
||||
}
|
||||
@@ -102,3 +102,80 @@ LLM-LLM 자연어 통신은 프롬프트 인젝션·할루시네이션 증폭
|
||||
## 직접 고쳐 써라
|
||||
|
||||
이 문서는 출발점이다. 패턴이 보이면 직접 업데이트해라. 정비는 코디가 하지만, 이 매뉴얼의 작가는 너다.
|
||||
|
||||
## Tools
|
||||
|
||||
### Local notes (migrated from TOOLS.md)
|
||||
|
||||
# TOOLS.md - Local Notes
|
||||
|
||||
Skills define _how_ tools work. This file is for _your_ specifics — the stuff that's unique to your setup.
|
||||
|
||||
## What Goes Here
|
||||
|
||||
Things like:
|
||||
|
||||
- Camera names and locations
|
||||
- SSH hosts and aliases
|
||||
- Preferred voices for TTS
|
||||
- Speaker/room names
|
||||
- Device nicknames
|
||||
- Anything environment-specific
|
||||
|
||||
## Examples
|
||||
|
||||
```markdown
|
||||
### Cameras
|
||||
|
||||
- living-room → Main area, 180° wide angle
|
||||
- front-door → Entrance, motion-triggered
|
||||
|
||||
### SSH
|
||||
|
||||
- home-server → 192.168.1.100, user: admin
|
||||
|
||||
### TTS
|
||||
|
||||
- Preferred voice: "Nova" (warm, slightly British)
|
||||
- Default speaker: Kitchen HomePod
|
||||
```
|
||||
|
||||
## Why Separate?
|
||||
|
||||
Skills are shared. Your setup is yours. Keeping them apart means you can update skills without losing your notes, and share skills without leaking your infrastructure.
|
||||
|
||||
---
|
||||
|
||||
Add whatever helps you do your job. This is your cheat sheet.
|
||||
|
||||
## 후잉(가계부) 직접 입력
|
||||
|
||||
관리자님이 "가계부에 한 건 추가해줘" / "후잉에 등록해줘" 라고 하면, 골디의 manual 스크립트로 처리.
|
||||
|
||||
- 스크립트: `agents/budget/workspace/skills/whooing-sync/scripts/whooing_manual.py`
|
||||
- 계정 차트: `agents/budget/workspace/state/whooing_accounts.json` (여기 있는 이름만 사용)
|
||||
- 프로토콜: `agents/budget/workspace/skills/whooing-sync/SKILL.md` 의 "에이전트 대화 프로토콜 (스텝바이스텝)" 섹션을 따른다. 한 번에 하나씩 묻기.
|
||||
|
||||
## Playwright MCP (브라우저 자동화)
|
||||
|
||||
OpenClaw가 `openclaw.json` 의 MCP 서버 `playwright`로 노출. 도구는 `playwright__browser_*` prefix로 약 23개.
|
||||
|
||||
- **탐색:** `navigate / navigate_back / tabs / wait_for`
|
||||
- **관찰:** `snapshot` (접근성 트리 — 클릭 대상 ref 식별에 **우선 사용**) / `take_screenshot` / `console_messages` / `network_requests` / `network_request`
|
||||
- **조작:** `click / type / fill_form / press_key / hover / select_option / drag / drop / file_upload / handle_dialog / resize / close`
|
||||
- **평가:** `evaluate` (페이지 JS 컨텍스트) / `run_code_unsafe` (Playwright 코드 직접 — **RCE 등급, 마지막 수단**)
|
||||
|
||||
**표준 흐름:** `navigate` → `snapshot`으로 ref 확보 → `click`/`type`/`fill_form` → `wait_for`로 결과 대기 → `snapshot` 또는 `network_requests`로 검증. 스크린샷은 사람 보고용이지 다음 동작 분기 근거로 쓰지 말 것.
|
||||
|
||||
**운영 특성:**
|
||||
- 기본값 `--headless --isolated`. 매 세션 쿠키·로그인 폐기.
|
||||
- 영속 로그인 필요한 사이트는 시도 전 **코디에게 `user-data-dir` 분리·등록 요청**.
|
||||
- 첫 호출 시 npx spawn 1~2초 지연 — 시간 민감 작업은 미리 한 번 워밍업.
|
||||
- 키움 영웅문·HTS, 은행 공동인증서 등 보안프로그램 탑재 사이트는 차단 위험 → 시도 자제.
|
||||
- 공동인증서·OTP·CAPTCHA 화면이 나오면 **즉시 중단하고 관리자님께 보고**, 우회 시도 X.
|
||||
|
||||
**활용 시나리오 (클로 맥락):**
|
||||
- Google 워크스페이스 UI 작업 (API로 안 되는 라벨·필터·서명 정리 등)
|
||||
- 가격·재고 모니터링 (항공권·호텔·쇼핑몰) → 임계가 도달 시 텔레그램 알림
|
||||
- 택배 추적, 정부24/공공 사이트 등 OAuth 없는 조회·신청
|
||||
- 외부 페이지 변화 감지(공지·뉴스·블로그) → 결과만 텔레그램 요약
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<!-- Heartbeat template; comments-only content prevents scheduled heartbeat API calls. -->
|
||||
|
||||
# Keep this file empty (or with only comments) to skip heartbeat API calls.
|
||||
|
||||
# Add tasks below when you want the agent to check something periodically.
|
||||
@@ -1,72 +0,0 @@
|
||||
# TOOLS.md - Local Notes
|
||||
|
||||
Skills define _how_ tools work. This file is for _your_ specifics — the stuff that's unique to your setup.
|
||||
|
||||
## What Goes Here
|
||||
|
||||
Things like:
|
||||
|
||||
- Camera names and locations
|
||||
- SSH hosts and aliases
|
||||
- Preferred voices for TTS
|
||||
- Speaker/room names
|
||||
- Device nicknames
|
||||
- Anything environment-specific
|
||||
|
||||
## Examples
|
||||
|
||||
```markdown
|
||||
### Cameras
|
||||
|
||||
- living-room → Main area, 180° wide angle
|
||||
- front-door → Entrance, motion-triggered
|
||||
|
||||
### SSH
|
||||
|
||||
- home-server → 192.168.1.100, user: admin
|
||||
|
||||
### TTS
|
||||
|
||||
- Preferred voice: "Nova" (warm, slightly British)
|
||||
- Default speaker: Kitchen HomePod
|
||||
```
|
||||
|
||||
## Why Separate?
|
||||
|
||||
Skills are shared. Your setup is yours. Keeping them apart means you can update skills without losing your notes, and share skills without leaking your infrastructure.
|
||||
|
||||
---
|
||||
|
||||
Add whatever helps you do your job. This is your cheat sheet.
|
||||
|
||||
## 후잉(가계부) 직접 입력
|
||||
|
||||
관리자님이 "가계부에 한 건 추가해줘" / "후잉에 등록해줘" 라고 하면, 골디의 manual 스크립트로 처리.
|
||||
|
||||
- 스크립트: `agents/budget/workspace/skills/whooing-sync/scripts/whooing_manual.py`
|
||||
- 계정 차트: `agents/budget/workspace/state/whooing_accounts.json` (여기 있는 이름만 사용)
|
||||
- 프로토콜: `agents/budget/workspace/skills/whooing-sync/SKILL.md` 의 "에이전트 대화 프로토콜 (스텝바이스텝)" 섹션을 따른다. 한 번에 하나씩 묻기.
|
||||
|
||||
## Playwright MCP (브라우저 자동화)
|
||||
|
||||
OpenClaw가 `openclaw.json` 의 MCP 서버 `playwright`로 노출. 도구는 `playwright__browser_*` prefix로 약 23개.
|
||||
|
||||
- **탐색:** `navigate / navigate_back / tabs / wait_for`
|
||||
- **관찰:** `snapshot` (접근성 트리 — 클릭 대상 ref 식별에 **우선 사용**) / `take_screenshot` / `console_messages` / `network_requests` / `network_request`
|
||||
- **조작:** `click / type / fill_form / press_key / hover / select_option / drag / drop / file_upload / handle_dialog / resize / close`
|
||||
- **평가:** `evaluate` (페이지 JS 컨텍스트) / `run_code_unsafe` (Playwright 코드 직접 — **RCE 등급, 마지막 수단**)
|
||||
|
||||
**표준 흐름:** `navigate` → `snapshot`으로 ref 확보 → `click`/`type`/`fill_form` → `wait_for`로 결과 대기 → `snapshot` 또는 `network_requests`로 검증. 스크린샷은 사람 보고용이지 다음 동작 분기 근거로 쓰지 말 것.
|
||||
|
||||
**운영 특성:**
|
||||
- 기본값 `--headless --isolated`. 매 세션 쿠키·로그인 폐기.
|
||||
- 영속 로그인 필요한 사이트는 시도 전 **코디에게 `user-data-dir` 분리·등록 요청**.
|
||||
- 첫 호출 시 npx spawn 1~2초 지연 — 시간 민감 작업은 미리 한 번 워밍업.
|
||||
- 키움 영웅문·HTS, 은행 공동인증서 등 보안프로그램 탑재 사이트는 차단 위험 → 시도 자제.
|
||||
- 공동인증서·OTP·CAPTCHA 화면이 나오면 **즉시 중단하고 관리자님께 보고**, 우회 시도 X.
|
||||
|
||||
**활용 시나리오 (클로 맥락):**
|
||||
- Google 워크스페이스 UI 작업 (API로 안 되는 라벨·필터·서명 정리 등)
|
||||
- 가격·재고 모니터링 (항공권·호텔·쇼핑몰) → 임계가 도달 시 텔레그램 알림
|
||||
- 택배 추적, 정부24/공공 사이트 등 OAuth 없는 조회·신청
|
||||
- 외부 페이지 변화 감지(공지·뉴스·블로그) → 결과만 텔레그램 요약
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"bootstrapSeededAt": "2026-03-26T14:36:09.405Z",
|
||||
"setupCompletedAt": "2026-04-13T15:02:39.856Z"
|
||||
}
|
||||
@@ -1,15 +1,26 @@
|
||||
#!/usr/bin/env python3
|
||||
"""codex 폴백 감지 → iMessage 알림 (LLM 미경유).
|
||||
"""폴백 감지 → 클로(main) 텔레그램 알림 (LLM 미경유).
|
||||
|
||||
codex 주 모델(openai/gpt-5.5)이 죽으면 게이트웨이가 조용히 유료 OpenRouter로 폴백해
|
||||
토큰비가 새는 걸 관리자님이 모르는 문제 대응. 각 에이전트 세션 trajectory.jsonl의
|
||||
`model.completed` 이벤트(실제 턴을 완료한 승자 모델)를 스캔해, 승자 provider가
|
||||
`openrouter`면 = codex 다운 → 유료 대체 중으로 판단하고 클로(main) 텔레그램으로 즉시 알림.
|
||||
텔레그램은 Bot API에 HTTP POST만 하는 직접 발송이라 LLM(모델 세션) 미경유.
|
||||
주 모델이 죽으면 게이트웨이가 조용히 폴백해서, 관리자님이 그 사실을 모르는 채로
|
||||
품질·비용이 달라지는 문제 대응. 각 에이전트 DB(openclaw-agent.sqlite)의
|
||||
trajectory_runtime_events 테이블에 쌓이는 `model.completed` 이벤트(실제 턴을 완료한
|
||||
승자 모델)를 스캔한다. 텔레그램은 Bot API 에 HTTP POST 만 하는 직접 발송이라 LLM 미경유.
|
||||
|
||||
- 신호원: agents/*/sessions/*.trajectory.jsonl (on-disk, 무비용)
|
||||
- 정상: model.completed provider=openai model=gpt-5.5 (codex 하네스)
|
||||
- 폴백: provider=openrouter/* (config fallback = claude-haiku/sonnet, 유료)
|
||||
⚠️ **판정 기준이 2026-09-10 에 바뀌었다: provider → primary 와 다른 모델인가.**
|
||||
그전엔 `provider==openrouter` 를 폴백으로 봤는데, 폴백 체인이 전부 무과금
|
||||
(구독 gpt-5.6-terra/luna → openrouter/free)이 되면서 sol→terra 같은 **구독 내 폴백을
|
||||
통째로 놓치게** 됐다. 과금이 0이어도 '주 모델이 죽었다'는 사실은 알아야 한다.
|
||||
`_primary_models()` 로 에이전트별 primary 를 읽어 비교하고, primary 를 못 읽은
|
||||
경우에만 옛 규칙으로 후퇴한다.
|
||||
|
||||
알림은 과금 여부로 문구가 갈린다(`_is_billed`) — 유료 경로면 🚨 즉시 점검,
|
||||
무과금 폴백이면 ⚠️ 과금 없음. ⚠️ 무료 판정은 `_model_key` 정규화를 거쳐야 한다
|
||||
(openrouter 는 modelId 에 provider 가 이미 붙어 와서, 안 거치면 무료가 유료로 오판된다).
|
||||
|
||||
- 신호원: agents/*/agent/openclaw-agent.sqlite → trajectory_runtime_events (on-disk, 무비용)
|
||||
⚠️ 2026-09-10 OpenClaw 2026.9.3 업그레이드로 trajectory 가 파일(.trajectory.jsonl)에서 이 테이블로
|
||||
이관됐다. 파일 glob 을 계속 보면 매치 0건이라 '폴백 없음'만 반환하며 조용히 무력화된다.
|
||||
- 정상: model.completed provider=openai model=gpt-5.6-sol (codex 하네스)
|
||||
- dedupe: last_ts 워터마크 이후 이벤트만, 알림은 COOLDOWN당 1회(장기 다운 스팸 방지)
|
||||
- 결정론적, 실패해도 절대 raise 안 함(모니터가 워크플로 안 깨게)
|
||||
|
||||
@@ -21,6 +32,7 @@ from __future__ import annotations
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
@@ -31,9 +43,9 @@ ROOT = os.path.expanduser("~/.openclaw")
|
||||
STATE_PATH = os.path.join(ROOT, "workspace/state/codex_fallback_monitor.json")
|
||||
CONFIG_PATH = os.path.join(ROOT, "openclaw.json")
|
||||
TELEGRAM_ACCOUNT = "default" # 클로(main) 봇
|
||||
TRAJ_GLOB = os.path.join(ROOT, "agents/*/sessions/*.trajectory.jsonl")
|
||||
AGENT_DB_GLOB = os.path.join(ROOT, "agents/*/agent/openclaw-agent.sqlite")
|
||||
|
||||
LOOKBACK_SEC = 3 * 3600 # mtime 이 구간 내 파일만 스캔
|
||||
LOOKBACK_SEC = 3 * 3600 # created_at 이 구간 내 이벤트만 스캔
|
||||
COOLDOWN_SEC = 30 * 60 # 알림 최소 간격 (장기 다운 스팸 방지)
|
||||
FALLBACK_PROVIDERS = {"openrouter"} # 정상 primary=openai(codex). openrouter 승자 = 폴백
|
||||
|
||||
@@ -80,6 +92,53 @@ def _telegram_targets() -> tuple[str | None, list[str]]:
|
||||
return None, []
|
||||
|
||||
|
||||
def _primary_models() -> dict[str, str]:
|
||||
"""에이전트별 primary 모델 id(소문자). 못 읽으면 빈 dict.
|
||||
|
||||
폴백 판정 기준이 2026-09-10 에 provider 에서 **primary 와 다른 모델인가**로 바뀌었다.
|
||||
폴백 체인이 전부 무과금(구독 gpt-5.6-terra/luna + openrouter/free)이 되면서
|
||||
provider 만 보면 sol→terra 같은 구독 내 폴백을 통째로 놓치기 때문이다.
|
||||
과금은 0이어도 '주 모델이 죽었다'는 사실은 알아야 한다.
|
||||
"""
|
||||
try:
|
||||
with open(CONFIG_PATH, encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"[cfg] primary 조회 실패: {e}", file=sys.stderr)
|
||||
return {}
|
||||
ag = cfg.get("agents") or {}
|
||||
fallback_primary = ((ag.get("defaults") or {}).get("model") or {}).get("primary")
|
||||
out: dict[str, str] = {}
|
||||
for aid, entry in (ag.get("entries") or {}).items():
|
||||
p = ((entry or {}).get("model") or {}).get("primary") or fallback_primary
|
||||
if p:
|
||||
out[aid] = p.lower()
|
||||
return out
|
||||
|
||||
|
||||
def _model_key(provider: str | None, model_id: str | None) -> str:
|
||||
"""`provider/model` 정규화(소문자).
|
||||
|
||||
⚠️ openrouter 는 modelId 에 이미 provider 가 붙어 온다(`openrouter/free`).
|
||||
그대로 이어붙이면 `openrouter/openrouter/free` 가 되고, 무료 모델이
|
||||
`_is_billed` 의 무료 목록과 안 맞아 **유료로 오판**된다(2026-09-10 실측).
|
||||
"""
|
||||
p = (provider or "").lower()
|
||||
m = (model_id or "").lower()
|
||||
if not p:
|
||||
return m
|
||||
return m if m.startswith(f"{p}/") else f"{p}/{m}"
|
||||
|
||||
|
||||
def _is_billed(model_id: str) -> bool:
|
||||
"""유료 과금 경로인가. openai/*=ChatGPT 구독, openrouter/free·*:free=무료."""
|
||||
if model_id.startswith("openai/"):
|
||||
return False
|
||||
if model_id == "openrouter/free" or model_id.endswith(":free"):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def scan_fallbacks(since_ts: float) -> tuple[list[dict], float]:
|
||||
"""since_ts 이후 model.completed 이벤트에서 폴백 건 추출.
|
||||
|
||||
@@ -87,41 +146,51 @@ def scan_fallbacks(since_ts: float) -> tuple[list[dict], float]:
|
||||
"""
|
||||
events: list[dict] = []
|
||||
max_ts = since_ts
|
||||
cutoff_mtime = _now() - LOOKBACK_SEC
|
||||
for path in glob.glob(TRAJ_GLOB):
|
||||
try:
|
||||
if os.path.getmtime(path) < cutoff_mtime:
|
||||
continue
|
||||
except OSError:
|
||||
continue
|
||||
primaries = _primary_models()
|
||||
cutoff_ms = int((_now() - LOOKBACK_SEC) * 1000)
|
||||
for path in glob.glob(AGENT_DB_GLOB):
|
||||
agent = path.split("/agents/", 1)[-1].split("/", 1)[0]
|
||||
try:
|
||||
with open(path, encoding="utf-8", errors="ignore") as f:
|
||||
for line in f:
|
||||
if '"model.completed"' not in line:
|
||||
continue
|
||||
try:
|
||||
d = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if d.get("type") != "model.completed":
|
||||
continue
|
||||
ets = _parse_ts(d.get("ts", ""))
|
||||
if ets <= since_ts:
|
||||
continue
|
||||
if ets > max_ts:
|
||||
max_ts = ets
|
||||
provider = (d.get("provider") or "").lower()
|
||||
if provider in FALLBACK_PROVIDERS:
|
||||
events.append({
|
||||
"agent": agent,
|
||||
"ts": ets,
|
||||
"provider": d.get("provider"),
|
||||
"model": d.get("modelId"),
|
||||
"runId": d.get("runId", ""),
|
||||
})
|
||||
# 게이트웨이가 쓰는 중이므로 read-only + busy timeout
|
||||
con = sqlite3.connect(f"file:{path}?mode=ro", uri=True, timeout=5)
|
||||
try:
|
||||
rows = con.execute(
|
||||
"SELECT event_json FROM trajectory_runtime_events "
|
||||
"WHERE created_at >= ? ORDER BY created_at",
|
||||
(cutoff_ms,),
|
||||
).fetchall()
|
||||
finally:
|
||||
con.close()
|
||||
for (line,) in rows:
|
||||
if '"model.completed"' not in line:
|
||||
continue
|
||||
try:
|
||||
d = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
if d.get("type") != "model.completed":
|
||||
continue
|
||||
ets = _parse_ts(d.get("ts", ""))
|
||||
if ets <= since_ts:
|
||||
continue
|
||||
if ets > max_ts:
|
||||
max_ts = ets
|
||||
provider = (d.get("provider") or "").lower()
|
||||
model_id = _model_key(provider, d.get('modelId'))
|
||||
primary = primaries.get(agent)
|
||||
# primary 를 못 읽었을 때만 옛 규칙(openrouter=폴백)으로 후퇴한다.
|
||||
fell_back = (model_id != primary if primary
|
||||
else provider in FALLBACK_PROVIDERS)
|
||||
if fell_back:
|
||||
events.append({
|
||||
"agent": agent,
|
||||
"ts": ets,
|
||||
"provider": d.get("provider"),
|
||||
"model": d.get("modelId"),
|
||||
"runId": d.get("runId", ""),
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"[scan] {path} 읽기 실패: {e}", file=sys.stderr)
|
||||
print(f"[scan] {path} 조회 실패: {e}", file=sys.stderr)
|
||||
return events, max_ts
|
||||
|
||||
|
||||
@@ -129,14 +198,22 @@ def _compose(events: list[dict]) -> str:
|
||||
latest = max(events, key=lambda e: e["ts"])
|
||||
when = datetime.fromtimestamp(latest["ts"], tz=KST).strftime("%m-%d %H:%M")
|
||||
agents = sorted({e["agent"] for e in events})
|
||||
models = sorted({f"{e['provider']}/{e['model']}" for e in events})
|
||||
models = sorted({_model_key(e['provider'], e['model']) for e in events})
|
||||
billed = [m for m in models if _is_billed(m)]
|
||||
if billed:
|
||||
head = "🚨 유료 폴백 감지"
|
||||
note = (f"주 모델 실패 → 유료 경로로 대체 중입니다 ({', '.join(billed)}).\n"
|
||||
"→ 즉시 codex 점검 (토큰비 누수)")
|
||||
else:
|
||||
head = "⚠️ 폴백 감지 (과금 없음)"
|
||||
note = ("주 모델 실패 → 무과금 폴백으로 대체 중입니다.\n"
|
||||
"→ 과금은 없지만 codex 점검 필요")
|
||||
return (
|
||||
"⚠️ codex 폴백 감지\n"
|
||||
"codex 주 모델(gpt-5.5) 응답 실패 → 유료 OpenRouter로 대체 중입니다.\n"
|
||||
f"{head}\n"
|
||||
f"최근: {when} KST\n"
|
||||
f"대체모델: {', '.join(models)}\n"
|
||||
f"에이전트: {', '.join(agents)} (총 {len(events)}건)\n"
|
||||
"→ codex 점검 필요 (토큰비 누수)"
|
||||
f"{note}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user