diff options
| author | Agando <roduccomm@gmail.com> | 2026-11-02 20:05:14 +0100 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-11-02 20:05:14 +0100 |
| commit | cec2baad28213a296ae491330a3f7f8953a3fea1 (patch) | |
| tree | b8e457ac2160f65c268398055a1b31c7e35bf6e6 | |
| parent | 0f02d398c86640547f58e0717735d45489abb766 (diff) | |
feat(newos4): R5 stale-artefact rule + dashboard queues a decision once
Feedback queue triage 2026-11-02: 23 reports, 22 decided, 1 open -- and the open
one (RPT-20261029-115241, "[Terminal] Error in ShellModule.test: {\"silent\":true}")
was already fixed in v4.6.135 (4472c81, modules/core/terminal.js:1966) yet could
never be cleared by automation.
Root cause: identifiers() keeps only generic class names (Terminal, ShellModule),
both of which necessarily stay in the code, so R2 (every identifier absent) cannot
fire; R4 needs a stack and this report has none. A stack-less
"Class.method: {json}" console.error whose emitter gets *guarded* -- rather than
deleted -- therefore sits "awaiting decision" forever.
- newos4-feedback-autoreview.py: new R5 stale artefact -- auto report, no stack,
every error count <= 1, and nothing newer than STALE_DAYS=5 in
data/debug/errors.json -> resolve with the measured quiet_days as evidence.
New --errors <path> knob so the rule can be run against fixtures.
RED (pristine copy): "nothing provable (1 left for the operator)"
GREEN (aged fixture): "resolve ... quiet 5.3d -- reopen if it recurs"
NEG (live store 4.3d, and a report with count 21): both still refuse.
- admin/feedback.html: markDecided() patches the local snapshot, so a queued
decision leaves the default "Awaiting only" list immediately instead of after
the 10-minute sync rebuild. Measured cause of 3x approve on one plan in 3 min
(10-30 16:03/16:05/16:05) and 6 items with duplicate rows.
- AGENTS.md: documents R5 and the fixture knob (also carries the already-live
2026-11-02 cookie-notice paragraph that was pending in the tree).
| -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': |