aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-11-02 20:05:14 +0100
committerAgando <roduccomm@gmail.com>2026-11-02 20:05:14 +0100
commitcec2baad28213a296ae491330a3f7f8953a3fea1 (patch)
treeb8e457ac2160f65c268398055a1b31c7e35bf6e6
parent0f02d398c86640547f58e0717735d45489abb766 (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-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':