generated from coulomb/repo-seed
session-memory: recurring-error signal + clustering (WP-0006 T02)
detect/signals.py sig_recurring_error emits one signal per distinct error fingerprint per session (magnitude = in-session occurrences), so the same error recurring across sessions/repos/flavors clusters into a candidate root-cause problem pattern via the existing clusterer — cross-flavor flagged automatically. 3 new tests; suite 98/98 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -156,10 +156,28 @@ def sig_tool_thrash(digest, ctx) -> list[Signal]:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def sig_recurring_error(digest, ctx) -> list[Signal]:
|
||||||
|
"""Problem: a normalized error fingerprint (WP-0006) — one signal per distinct
|
||||||
|
error in the session, so the same error across sessions/repos/flavors clusters
|
||||||
|
into a candidate root-cause pattern (locus = fingerprint, magnitude = in-session
|
||||||
|
occurrences). This is the content-level 'why', not just a coarse error count.
|
||||||
|
"""
|
||||||
|
out: list[Signal] = []
|
||||||
|
for snip in digest.get("error_snippets", []) or []:
|
||||||
|
fp = snip.get("fingerprint")
|
||||||
|
if not fp:
|
||||||
|
continue
|
||||||
|
out.append(_base(digest, "recurring_error", PROBLEM, fp, float(snip.get("count", 1)),
|
||||||
|
sample=snip.get("sample", ""), tool=snip.get("tool"),
|
||||||
|
occurrences=snip.get("count", 1)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
EXTRACTORS: list[Callable] = [
|
EXTRACTORS: list[Callable] = [
|
||||||
sig_retry_storm, sig_repeated_errors, sig_budget_overrun, sig_abandoned,
|
sig_retry_storm, sig_repeated_errors, sig_budget_overrun, sig_abandoned,
|
||||||
sig_clean_pass, sig_error_then_recovery,
|
sig_clean_pass, sig_error_then_recovery,
|
||||||
sig_infra_overhead, sig_schema_thrash, sig_tool_thrash,
|
sig_infra_overhead, sig_schema_thrash, sig_tool_thrash,
|
||||||
|
sig_recurring_error,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
59
tests/test_detect_recurring_error.py
Normal file
59
tests/test_detect_recurring_error.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"""Recurring-error signal + clustering (WP-0006 T02)."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from session_memory.detect.cluster import cluster # noqa: E402
|
||||||
|
from session_memory.detect.signals import ( # noqa: E402
|
||||||
|
extract_signals,
|
||||||
|
sig_recurring_error,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _digest(uid, repo, flavor="claude", snippets=None):
|
||||||
|
return {
|
||||||
|
"session_uid": uid, "flavor": flavor, "repo": repo, "outcome": "success",
|
||||||
|
"cost": {"input_tokens": 1, "output_tokens": 1},
|
||||||
|
"markers": {"errors": 0, "retries": 0, "test_runs": 0},
|
||||||
|
"tool_histogram": {}, "error_snippets": snippets or [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_FP = "modulenotfounderror: no module named 'foo' at <path>:<n>"
|
||||||
|
|
||||||
|
|
||||||
|
def test_signal_per_distinct_fingerprint():
|
||||||
|
d = _digest("claude:a", "r1", snippets=[
|
||||||
|
{"fingerprint": _FP, "sample": "ModuleNotFoundError ...", "count": 3, "tool": "Bash"},
|
||||||
|
{"fingerprint": "keyerror: <str>", "sample": "KeyError", "count": 1, "tool": None},
|
||||||
|
])
|
||||||
|
sigs = sig_recurring_error(d, {})
|
||||||
|
assert len(sigs) == 2
|
||||||
|
top = [s for s in sigs if s.locus == _FP][0]
|
||||||
|
assert top.type == "recurring_error"
|
||||||
|
assert top.magnitude == 3.0
|
||||||
|
assert top.detail["sample"].startswith("ModuleNotFound")
|
||||||
|
|
||||||
|
|
||||||
|
def test_clusters_across_sessions_and_flavors():
|
||||||
|
# same fingerprint in a claude and a grok session -> cross-flavor candidate
|
||||||
|
digs = [
|
||||||
|
_digest("claude:a", "r1", "claude",
|
||||||
|
[{"fingerprint": _FP, "sample": "ModuleNotFoundError", "count": 2, "tool": "Bash"}]),
|
||||||
|
_digest("grok:b", "r2", "grok",
|
||||||
|
[{"fingerprint": _FP, "sample": "ModuleNotFoundError", "count": 1, "tool": None}]),
|
||||||
|
]
|
||||||
|
signals = extract_signals(digs)
|
||||||
|
pats = cluster([s for s in signals if s.type == "recurring_error"], min_frequency=2)
|
||||||
|
assert len(pats) == 1
|
||||||
|
p = pats[0]
|
||||||
|
assert p.signal_type == "recurring_error"
|
||||||
|
assert p.cross_flavor is True
|
||||||
|
assert sorted(p.flavors) == ["claude", "grok"]
|
||||||
|
assert p.frequency == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_snippets_no_signal():
|
||||||
|
assert sig_recurring_error(_digest("claude:a", "r1"), {}) == []
|
||||||
@@ -48,7 +48,7 @@ sessions with repeated and varied errors.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: AGENTIC-WP-0006-T02
|
id: AGENTIC-WP-0006-T02
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "1a41b6f5-48bc-4080-bd18-94f2186ef566"
|
state_hub_task_id: "1a41b6f5-48bc-4080-bd18-94f2186ef566"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user