diff options
| -rwxr-xr-x | scripts/newos4-feedback-autoreview.py | 48 |
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': |