aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xscripts/newos4-feedback-autoreview.py48
1 files changed, 47 insertions, 1 deletions
diff --git a/scripts/newos4-feedback-autoreview.py b/scripts/newos4-feedback-autoreview.py
index e99a516..2021de3 100755
--- a/scripts/newos4-feedback-autoreview.py
+++ b/scripts/newos4-feedback-autoreview.py
@@ -17,6 +17,8 @@ MODULES = f'{ROOT}/spaces/newos4/modules'
DECISIONS = f'{ROOT}/spaces/newos4/data/feedback/decisions.jsonl'
AUTONOMY = f'{ROOT}/spaces/newos4/data/feedback/autonomy.json'
SUMMARY = f'{ROOT}/spaces/newos4/data/feedback/autoreview-latest.json'
+ERRORS = f'{ROOT}/spaces/newos4/data/debug/errors.json' # symptom store (count/last_seen)
+STALE_DAYS = 5 # quiet this long => stale artefact
CAPS = {'safe': 2, 'aggressive': 5}
NOISE = re.compile(r'fetch failed|failed to fetch|network ?error|load failed|user aborted'
@@ -115,6 +117,32 @@ def token_history(tokens):
return out
+def _age_days(ts):
+ """Days since an ISO-8601 stamp; tolerant of Z, fractional seconds and bare datetimes."""
+ s = str(ts or '').strip().replace('Z', '+00:00')
+ if not s:
+ return None
+ try:
+ dt = datetime.fromisoformat(s)
+ except ValueError:
+ return None
+ if dt.tzinfo is None:
+ dt = dt.replace(tzinfo=timezone.utc)
+ return (datetime.now(timezone.utc) - dt).total_seconds() / 86400.0
+
+
+def error_stats(msg):
+ """count/last_seen for one exact message in the debug error store; None when unknown."""
+ rows = load_json(ERRORS, [])
+ if not isinstance(rows, list):
+ return None
+ key = str(msg or '').strip()
+ for e in rows:
+ if isinstance(e, dict) and str(e.get('message') or '').strip() == key:
+ return {'count': int(e.get('count') or 0), 'last_seen': str(e.get('last_seen') or '')}
+ return None
+
+
def fingerprints():
"""fingerprint -> id, for reports that already exist (duplicate detection)."""
out = {}
@@ -175,12 +203,28 @@ def classify(rep, dec, fps, cap_left):
if stack and present and cap_left <= 0:
return None, 'daily auto-approve cap reached — left for the operator', {'present_tokens': present}
+ # R5 stale artefact: one occurrence that never came back, and no stack to act on.
+ # Distinct from R2 (identifier removed) — here the emitter is still there but the symptom is not.
+ if errs and not stack and all(int(e.get('count') or 0) <= 1 for e in errs):
+ stats = [error_stats(str(e.get('message') or '')) for e in errs]
+ stats = [s for s in stats if s and s.get('last_seen')]
+ ages = [_age_days(s['last_seen']) for s in stats]
+ if len(stats) == len(errs) and ages and all(a is not None for a in ages):
+ quiet = max(a for a in ages if a is not None)
+ if quiet >= STALE_DAYS and all(s['count'] <= 1 for s in stats):
+ return 'resolve', (f'stale single-occurrence auto report: no stack, count 1, '
+ f'quiet {quiet:.1f}d — reopen if it recurs'), {
+ 'counts': [s['count'] for s in stats],
+ 'last_seen': [s['last_seen'] for s in stats],
+ 'quiet_days': round(quiet, 1),
+ 'emitter_tokens': tokens}
+
return None, 'not provable by the rules — left for the operator', {
'tokens': tokens, 'present': present, 'stack': stack}
def main():
- global REPORTS, MODULES, DECISIONS, ROOTDIR
+ global REPORTS, MODULES, DECISIONS, ROOTDIR, ERRORS
ap = argparse.ArgumentParser()
ap.add_argument('--root', default=ROOT)
ap.add_argument('--dry-run', action='store_true')
@@ -188,10 +232,12 @@ def main():
ap.add_argument('--decisions', default=DECISIONS)
ap.add_argument('--modules', default=MODULES)
ap.add_argument('--cap', type=int, default=None)
+ ap.add_argument('--errors', default=ERRORS, help='debug error store (fixtures)')
ap.add_argument('--include-plans', action='store_true', help='never used in production')
a = ap.parse_args()
ROOTDIR = a.root
REPORTS, MODULES, DECISIONS = a.reports_dir, a.modules, a.decisions
+ ERRORS = a.errors
m, cap = mode(a.cap)
if m == 'off':