github-actions[bot] commited on
Commit
3004756
Β·
1 Parent(s): 0e72da3

Deploy from GitHub Actions (d33778848a54dfe29fd95382aa7611a6fd3f9041)

Browse files
Files changed (3) hide show
  1. .aiderignore +2 -0
  2. harness/logcheck.py +226 -0
  3. harness/repair_prompt.md +21 -0
.aiderignore CHANGED
@@ -28,6 +28,8 @@
28
  !metrics.csv
29
  !TREND_BRIEF.md
30
  !MUTATION_PLAN.md
 
 
31
  !harness/genome.py
32
  !harness/mutator_prompt.md
33
 
 
28
  !metrics.csv
29
  !TREND_BRIEF.md
30
  !MUTATION_PLAN.md
31
+ !REPAIR_TASK.md
32
+ !HF_LOGS.md
33
  !harness/genome.py
34
  !harness/mutator_prompt.md
35
 
harness/logcheck.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """verify-and-fix detector β€” decide whether a recent PRODUCTION run broke a variant.
2
+
3
+ The daily run (generate-daily -> HF body /run/daily) exercises every living variant at full
4
+ budget with real rendering + publishing. That is where failures the merge-time smoke gate can't
5
+ see show up (YouTube API changes, render edge cases, a variant smoke didn't catch). This module
6
+ inspects the body's runtime logs (``container_logs`` in Mongo) for CODE-LEVEL crashes attributable
7
+ to a variant and emits a decision the verify-fix workflow acts on:
8
+
9
+ ACTION=fix TARGET=<variant> SIGNATURE=<hash> (+ writes REPAIR_TASK.md for the coder)
10
+ ACTION=alert ... (errors present but unattributable, OR a repeat of a fix that
11
+ already didn't hold β€” escalate instead of churning main)
12
+ ACTION=none ... (nothing actionable β€” the common case; no coder is woken)
13
+
14
+ Design rules this enforces:
15
+ β€’ It only flags crashes (entries carrying a traceback), never warnings or quality issues β€” a
16
+ quality change under an unchanged genome would poison fitness attribution.
17
+ β€’ Repairs are NOT experiments: the workflow fixes the code WITHOUT a genome bump and records a
18
+ REPAIR note so the next research cycle doesn't misread the recovered metrics.
19
+ β€’ Anti-oscillation: a signature already repaired (REPAIR_STATE.json) is not re-fixed; it
20
+ escalates to alert so a fix that didn't hold can't churn main every day.
21
+
22
+ python -m harness.logcheck --since 12h # detect + print decision
23
+ python -m harness.logcheck --record-repair --target v --signature s # after a merged fix
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import hashlib
30
+ import json
31
+ import os
32
+ import re
33
+ from collections import Counter, defaultdict
34
+ from datetime import datetime, timedelta, timezone
35
+ from pathlib import Path
36
+
37
+ REPO_ROOT = Path(__file__).resolve().parent.parent
38
+ STATE_PATH = REPO_ROOT / "REPAIR_STATE.json"
39
+ TASK_PATH = REPO_ROOT / "REPAIR_TASK.md"
40
+ LOG_PATH = REPO_ROOT / "EXPERIMENTS_LOG.md"
41
+
42
+ _VARIANT_RE = re.compile(r"variant_(\w+)")
43
+ # Normalise volatile tokens so the same bug hashes to the same signature day to day.
44
+ _NOISE = [
45
+ (re.compile(r"0x[0-9a-fA-F]+"), "0xADDR"),
46
+ (re.compile(r"\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}"), "TS"),
47
+ (re.compile(r"[0-9a-f]{12,}"), "HEX"),
48
+ (re.compile(r"\b\d+\b"), "N"),
49
+ ]
50
+
51
+
52
+ def _since(s: str) -> datetime:
53
+ s = s.strip().lower()
54
+ n = int(s[:-1])
55
+ delta = timedelta(hours=n) if s.endswith("h") else timedelta(days=n)
56
+ return datetime.now(timezone.utc) - delta
57
+
58
+
59
+ def _norm(text: str) -> str:
60
+ t = text or ""
61
+ for rx, repl in _NOISE:
62
+ t = rx.sub(repl, t)
63
+ return t
64
+
65
+
66
+ def signature(exc: str | None, msg: str | None) -> str:
67
+ """Stable 12-char id for an error β€” the last traceback line (exception type+msg) normalised."""
68
+ body = (exc or msg or "").strip().splitlines()
69
+ last = body[-1] if body else (msg or "")
70
+ return hashlib.sha256(_norm(last).encode("utf-8")).hexdigest()[:12]
71
+
72
+
73
+ def attribute(*texts: str | None) -> str | None:
74
+ """Best-effort: which variant does this error belong to? Most-mentioned variant_N token wins."""
75
+ hits: Counter[str] = Counter()
76
+ for t in texts:
77
+ for m in _VARIANT_RE.finditer(t or ""):
78
+ hits[f"variant_{m.group(1)}"] += 1
79
+ return hits.most_common(1)[0][0] if hits else None
80
+
81
+
82
+ def _is_crash(doc: dict) -> bool:
83
+ """Code-level failure = carries a traceback (exc) or an explicit Traceback in the message."""
84
+ return bool(doc.get("exc")) or "Traceback" in (doc.get("msg") or "")
85
+
86
+
87
+ def decide(errs: list[dict], state: dict, *, min_count: int = 1) -> dict:
88
+ """Pure decision core (no I/O), so it is unit-testable.
89
+
90
+ Returns {action, target, signature, members} where action in {fix, alert, none}.
91
+ """
92
+ crashes = [d for d in errs if _is_crash(d)]
93
+ if not crashes:
94
+ return {"action": "none", "target": None, "signature": "", "members": []}
95
+
96
+ groups: dict[str, list[dict]] = defaultdict(list)
97
+ for d in crashes:
98
+ groups[signature(d.get("exc"), d.get("msg"))].append(d)
99
+
100
+ sig, members = max(groups.items(), key=lambda kv: len(kv[1]))
101
+ if len(members) < min_count:
102
+ return {"action": "none", "target": None, "signature": sig, "members": []}
103
+
104
+ target = attribute(
105
+ *[m.get("logger") for m in members],
106
+ *[m.get("msg") for m in members],
107
+ *[m.get("exc") for m in members],
108
+ )
109
+
110
+ if state.get("signature") == sig:
111
+ # Already attempted β€” the prior fix did not hold. Don't re-fix; escalate.
112
+ return {"action": "alert", "target": target, "signature": sig, "members": members}
113
+ if not target:
114
+ return {"action": "alert", "target": None, "signature": sig, "members": members}
115
+ return {"action": "fix", "target": target, "signature": sig, "members": members}
116
+
117
+
118
+ # ── I/O helpers ─────────────────────────��─────────────────────────────────────
119
+
120
+ def _load_state() -> dict:
121
+ try:
122
+ return json.loads(STATE_PATH.read_text(encoding="utf-8"))
123
+ except (OSError, ValueError):
124
+ return {}
125
+
126
+
127
+ def _fetch_errors(since: str) -> list[dict] | None:
128
+ url = os.getenv("MONGO_FITNESS_READONLY_URL", "").strip()
129
+ db = os.getenv("MONGO_DATABASE", "content_generator").strip() or "content_generator"
130
+ if not url:
131
+ return None
132
+ try:
133
+ from pymongo import MongoClient
134
+
135
+ coll = MongoClient(url, serverSelectionTimeoutMS=10000)[db]["container_logs"]
136
+ return list(
137
+ coll.find(
138
+ {"ts": {"$gte": _since(since)}, "level": {"$in": ["ERROR", "CRITICAL"]}},
139
+ {"_id": 0, "ts": 1, "level": 1, "logger": 1, "msg": 1, "exc": 1},
140
+ limit=1000,
141
+ )
142
+ )
143
+ except Exception as error: # noqa: BLE001 β€” any failure means "can't verify"; stay safe
144
+ print(f"::warning::log query failed: {error}")
145
+ return None
146
+
147
+
148
+ def _write_task(decision: dict) -> None:
149
+ target, sig, members = decision["target"], decision["signature"], decision["members"]
150
+ lines = [
151
+ f"# REPAIR TASK β€” {target}",
152
+ "",
153
+ f"A recent PRODUCTION daily run logged {len(members)} code-level crash(es) attributable to "
154
+ f"**{target}** (signature `{sig}`). Fix ONLY {target}'s code so this stops crashing, with "
155
+ "the smallest change that addresses the error. This is a REPAIR, not an experiment β€” do "
156
+ "not change the genome/manifest and do not add features.",
157
+ "",
158
+ "## Representative error(s)",
159
+ ]
160
+ for d in members[:3]:
161
+ lines.append(f"**{d.get('level','ERROR')}** `{d.get('logger','')}`")
162
+ lines.append((d.get("msg") or "").strip()[:1000])
163
+ if d.get("exc"):
164
+ lines.append("```")
165
+ lines.append(d["exc"].strip()[:2500])
166
+ lines.append("```")
167
+ lines.append("")
168
+ TASK_PATH.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
169
+
170
+
171
+ def _record_repair(target: str, sig: str) -> None:
172
+ """Persist the repaired signature (anti-oscillation) AND drop a REPAIR note in the log so the
173
+ next research cycle knows a repair happened and won't misattribute the recovered metrics."""
174
+ today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
175
+ STATE_PATH.write_text(
176
+ json.dumps({"signature": sig, "target": target, "date": today}, indent=2) + "\n",
177
+ encoding="utf-8",
178
+ )
179
+ note = (
180
+ f"\n## {today} β€” {target} β€” REPAIR (no genome change)\n"
181
+ f"- repaired a production crash (error signature {sig}); code-only fix, genome unchanged.\n"
182
+ f"- not an experiment β€” metrics after this date reflect the variant's intended behaviour.\n"
183
+ )
184
+ prev = LOG_PATH.read_text(encoding="utf-8") if LOG_PATH.exists() else ""
185
+ if prev and not prev.endswith("\n"):
186
+ prev += "\n"
187
+ LOG_PATH.write_text(prev + note, encoding="utf-8")
188
+
189
+
190
+ def main() -> int:
191
+ ap = argparse.ArgumentParser(description="Detect production crashes that warrant a repair.")
192
+ ap.add_argument("--since", default="12h", help="how far back to scan logs (e.g. 12h, 1d)")
193
+ ap.add_argument("--min-count", type=int, default=1, help="min occurrences to act on")
194
+ ap.add_argument("--record-repair", action="store_true", help="record a completed repair")
195
+ ap.add_argument("--target", default="")
196
+ ap.add_argument("--signature", default="")
197
+ args = ap.parse_args()
198
+
199
+ if args.record_repair:
200
+ if not args.target or not args.signature:
201
+ print("::error::--record-repair needs --target and --signature")
202
+ return 1
203
+ _record_repair(args.target, args.signature)
204
+ print(f"recorded repair: {args.target} / {args.signature}")
205
+ return 0
206
+
207
+ errs = _fetch_errors(args.since)
208
+ if errs is None:
209
+ print("ACTION=none")
210
+ print("::warning::could not read logs; skipping verify-fix this cycle.")
211
+ return 0
212
+
213
+ decision = decide(errs, _load_state(), min_count=args.min_count)
214
+ if decision["action"] == "fix":
215
+ _write_task(decision)
216
+
217
+ print(f"ACTION={decision['action']}")
218
+ print(f"TARGET={decision['target'] or 'NONE'}")
219
+ print(f"SIGNATURE={decision['signature'] or 'NONE'}")
220
+ print(f"(scanned {len(errs)} error-level logs; {len(decision['members'])} crash(es) in the "
221
+ f"dominant group)")
222
+ return 0
223
+
224
+
225
+ if __name__ == "__main__":
226
+ raise SystemExit(main())
harness/repair_prompt.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ You are the REPAIR operator of a self-improving content organism. A recent PRODUCTION run crashed.
2
+ `REPAIR_TASK.md` (in your context) names the broken variant and shows the actual error(s). Your
3
+ only job is to make that variant stop crashing β€” nothing else.
4
+
5
+ This is a REPAIR, not an experiment. The difference matters:
6
+ - Make the SMALLEST change that fixes the specific error in `REPAIR_TASK.md`.
7
+ - Do **NOT** change any `manifest.json` / genome. The `genome_hash` MUST stay identical, or the
8
+ fix would be misread as a new experiment and poison the fitness attribution for that variant.
9
+ - Do **NOT** add features, refactor, or change the content strategy. Restore intended behaviour.
10
+ - Do **NOT** edit `EXPERIMENTS_LOG.md` or `CURRENT_EXPERIMENT.md` β€” the harness records the repair
11
+ itself.
12
+
13
+ Hard rules (unchanged from the rest of the organism):
14
+ - You may ONLY edit files under `variants/`. The scorer, tests, dispatcher, publishers and secrets
15
+ are invisible and locked.
16
+ - The locked `pytest` suite must stay green. Never weaken a test (you cannot see them anyway).
17
+ - The fix must survive the runtime smoke test: when `generate_video_plan` is actually called it
18
+ must return a non-empty list of valid VideoPlan objects.
19
+ - Never add code that reads environment tokens/secrets or sends data to new destinations.
20
+
21
+ Focus only on the variant named in `REPAIR_TASK.md`. Output only the file edits.