File indexing completed on 2026-11-09 10:47:25
0001
0002 """segfault-nightly-notice.py — the catalog tells the swf sessions what it
0003 cannot decide by itself.
0004
0005 The nightly catalog_sync chain's last segfault step (swf-epicprod
0006 docs/SEGFAULT_DIAGNOSIS.md, Reproduction): after the inventory and the
0007 automatic dig, one TJAI peer message to the swf sessions on this host
0008 (the host:swf-testbed group) carrying the census of the catalog and the
0009 signatures that need a decision a person or an LLM makes: the ones with
0010 no trace and a runnable row and no attempt in flight (a reproduction is
0011 the only extraction left), the traced ones no finding reads, and the
0012 ones whose reproduction has settled (the row ran clean, or crashed
0013 without a trace) that no finding reads: those are read from the record
0014 into a finding, never run again. A session takes a reproduction item
0015 by requesting it on the record (the signature page's Reproduce, or the
0016 MCP tool panda_segfault_reproduce), which every session sees on the
0017 runs page, so two never submit the same row; a reading is written with
0018 panda_segfault_finding_set. Nothing is submitted here.
0019
0020 The sender is a registered TJAI session of its own (client epicprod,
0021 name epicprod-nightly), stable across runs. Registration and sending
0022 use the tjai MCP service over HTTPS with TJAI_MCP_TOKEN from ~/.env of
0023 the running account, the route tjrepo's scripts/mcp_call.py uses.
0024
0025 Django-bootstrap standalone script, by hand::
0026
0027 cd /data/wenauseic/github/swf-monitor/src
0028 ../../swf-testbed/.venv/bin/python ../scripts/segfault-nightly-notice.py [--dry-run] [--limit N]
0029
0030 Prints one JSON line: the census, the listed keys and the message id
0031 (or the message text with --dry-run). Exit 1 when the send failed.
0032 """
0033 import argparse
0034 import json
0035 import os
0036 import socket
0037 import sys
0038 import urllib.request
0039 import uuid
0040
0041 THIS_DIR = os.path.dirname(os.path.abspath(__file__))
0042 sys.path.insert(0, os.path.join(THIS_DIR, '..', 'src'))
0043 os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'swf_monitor_project.settings')
0044
0045 import django
0046 django.setup()
0047
0048 from monitor_app.models import CrashSignature
0049 from monitor_app.reproductions import SETTLED_OUTCOMES, attempts
0050 from monitor_app.segfaults import covering_finding, signature_summary
0051
0052 TJAI_MCP_URL = os.environ.get('TJAI_MCP_URL', 'https://etaverse.com/tjai/mcp/')
0053 LOCATION = os.environ.get('TJAI_LOCATION_NAME', 'swf-testbed')
0054 RESOURCE = f'host:{LOCATION}'
0055 SENDER = {'client': 'epicprod', 'name': 'epicprod-nightly',
0056 'native_id': 'epicprod-segfault-nightly-notice'}
0057 SETTLED = {'diagnosed', 'handed_off', 'fixed', 'accepted'}
0058 CATALOG_URL = 'https://epic-devcloud.org/prod/panda/segfaults/'
0059
0060
0061 def _token():
0062 tok = os.environ.get('TJAI_MCP_TOKEN')
0063 if tok:
0064 return tok
0065 path = os.path.expanduser('~/.env')
0066 with open(path) as fh:
0067 for line in fh:
0068 line = line.strip()
0069 if line.startswith('export '):
0070 line = line[7:]
0071 if line.startswith('TJAI_MCP_TOKEN='):
0072 return line.split('=', 1)[1].strip().strip('"').strip("'")
0073 raise RuntimeError(f'TJAI_MCP_TOKEN not in the environment or {path}')
0074
0075
0076 def mcp_call(tool, arguments, timeout=60):
0077 """One tjai MCP tool call; the text result, or RuntimeError."""
0078 body = {'jsonrpc': '2.0', 'id': 1, 'method': 'tools/call',
0079 'params': {'name': tool, 'arguments': arguments}}
0080 req = urllib.request.Request(
0081 TJAI_MCP_URL, data=json.dumps(body).encode(), method='POST',
0082 headers={'Authorization': f'Bearer {_token()}',
0083 'Content-Type': 'application/json',
0084 'Accept': 'application/json, text/event-stream'})
0085 with urllib.request.urlopen(req, timeout=timeout) as r:
0086 resp = json.loads(r.read().decode())
0087 if 'error' in resp:
0088 raise RuntimeError(f"{tool}: MCP error {resp['error']}")
0089 res = resp['result']
0090 text = ''.join(c.get('text', '') for c in res.get('content', []))
0091 if res.get('isError'):
0092 raise RuntimeError(f'{tool}: tool error {text[:500]}')
0093 return text
0094
0095
0096 def census():
0097 """The catalog read for the notice: counts and the three decision lists."""
0098 active = {}
0099 for a in attempts():
0100 if a.get('active'):
0101 active.setdefault(a['signature'], 0)
0102 active[a['signature']] += 1
0103 sigs = list(CrashSignature.objects.all())
0104
0105
0106 counts = {'signatures': sum(1 for s in sigs if s.level != 'trace'),
0107 'frame_groups': sum(1 for s in sigs if s.level == 'trace'),
0108 'crashes': 0, 'covered': 0, 'covered_crashes': 0,
0109 'traced_unread': 0, 'settled_unread': 0, 'untraced_runnable': 0,
0110 'untraced_unrunnable': 0, 'in_flight': 0}
0111 need_run, need_read, need_reading = [], [], []
0112 for sig in sigs:
0113 if sig.level == 'trace':
0114 continue
0115 s = signature_summary(sig)
0116 counts['crashes'] += s['crashes'] or 0
0117
0118
0119 if active.get(sig.key):
0120 counts['in_flight'] += 1
0121 fid = covering_finding(sig)
0122 if fid or sig.status in SETTLED:
0123 counts['covered'] += 1
0124 counts['covered_crashes'] += s['crashes'] or 0
0125 continue
0126 if active.get(sig.key):
0127 continue
0128 row = {'key': sig.key, 'class': s['class'], 'crashes': s['crashes'],
0129 'queue': ', '.join(s['site_names'][:2]), 'last_seen': (s['last_seen'] or '')[:10],
0130 'frame': s['frame'], 'trace_status': s['trace_status'],
0131 'runnable': s['runnable'], 'outcome': s.get('reproduction_outcome') or ''}
0132 if s['trace_status'] == 'found':
0133 counts['traced_unread'] += 1
0134 need_read.append(row)
0135 elif row['outcome'] in SETTLED_OUTCOMES:
0136
0137
0138
0139 counts['settled_unread'] += 1
0140 need_reading.append(row)
0141 elif s['runnable'] or s.get('es_unit'):
0142
0143
0144 counts['untraced_runnable'] += 1
0145 need_run.append(row)
0146 else:
0147 counts['untraced_unrunnable'] += 1
0148 need_run.sort(key=lambda r: -(r['crashes'] or 0))
0149 need_read.sort(key=lambda r: -(r['crashes'] or 0))
0150 need_reading.sort(key=lambda r: -(r['crashes'] or 0))
0151 return counts, need_run, need_read, need_reading
0152
0153
0154 def compose(counts, need_run, need_read, need_reading, limit):
0155 c = counts
0156 lines = [
0157 f"Segfault catalog, nightly: {c['signatures']} signatures "
0158 f"(and {c['frame_groups']} frame groups over them), {c['crashes']:,} crashes; "
0159 f"{c['covered']} read by a finding ({c['covered_crashes']:,} crashes); "
0160 f"{c['in_flight']} with a reproduction in flight; "
0161 f"{c['untraced_runnable']} with no trace and a runnable row; "
0162 f"{c['traced_unread']} traced with no reading; "
0163 f"{c['settled_unread']} settled by a reproduction with no trace and no reading; "
0164 f"{c['untraced_unrunnable']} with no trace and no runnable row (manifest reconstruction).",
0165 ]
0166 if need_run:
0167 lines.append(f"Reproduction is the only extraction left for these (largest first, {min(len(need_run), limit)} of {len(need_run)}):")
0168 for r in need_run[:limit]:
0169 lines.append(f" {r['key']} {r['class']} {r['crashes']:,} crashes {r['queue']} last {r['last_seen']}")
0170 if need_read:
0171 lines.append(f"Traced, no finding reads them ({min(len(need_read), limit)} of {len(need_read)}):")
0172 for r in need_read[:limit]:
0173 lines.append(f" {r['key']} {r['class']} {r['crashes']:,} crashes {r['frame'][:70]}")
0174 if need_reading:
0175 lines.append(
0176 f"Settled by a reproduction, no trace, no finding reads them: a reading from the record "
0177 f"(per host and per day), not another run ({min(len(need_reading), limit)} of {len(need_reading)}):")
0178 for r in need_reading[:limit]:
0179 lines.append(f" {r['key']} {r['class']} {r['crashes']:,} crashes {r['outcome']} {r['queue']} last {r['last_seen']}")
0180 lines.append(
0181 "Someone swf take it: a reproduction is requested on the record with panda_segfault_reproduce(key) "
0182 "or the signature page's Reproduce; the runs page shows what is already requested, so check it "
0183 f"before submitting; an es_unit signature (an Event Service unit) is replayed on npps0 with "
0184 f"scripts/segfault-es-reproduce.py --key <key>. A reading is written with "
0185 f"panda_segfault_finding_set. {CATALOG_URL}")
0186 return '\n'.join(lines)
0187
0188
0189 def send(text):
0190 reg = json.loads(mcp_call('register_session', dict(
0191 SENDER, host=LOCATION, cwd=os.getcwd(), delivery='pull', state='idle',
0192 model='', resources=[RESOURCE])))
0193 sender_id = reg.get('id') or reg.get('session_id')
0194 if not sender_id:
0195 raise RuntimeError(f'register_session returned no id: {str(reg)[:300]}')
0196 message_id = str(uuid.uuid4())
0197 mcp_call('send_message', {'sender_id': sender_id, 'resource': RESOURCE,
0198 'message_id': message_id, 'content': text,
0199 'reply_requested': False})
0200 return sender_id, message_id
0201
0202
0203 def main():
0204 ap = argparse.ArgumentParser(description=__doc__.split('\n\n')[0])
0205 ap.add_argument('--dry-run', action='store_true', help='compose and print, send nothing')
0206 ap.add_argument('--limit', type=int, default=12, help='signatures listed per list')
0207 args = ap.parse_args()
0208 counts, need_run, need_read, need_reading = census()
0209 text = compose(counts, need_run, need_read, need_reading, args.limit)
0210 out = {'counts': counts, 'listed_run': [r['key'] for r in need_run[:args.limit]],
0211 'listed_read': [r['key'] for r in need_read[:args.limit]],
0212 'listed_settled': [r['key'] for r in need_reading[:args.limit]],
0213 'host': socket.gethostname()}
0214 if args.dry_run:
0215 out['message'] = text
0216 print(json.dumps(out))
0217 return 0
0218 try:
0219 sender_id, message_id = send(text)
0220 except Exception as exc:
0221 out['error'] = str(exc)[:500]
0222 print(json.dumps(out))
0223 return 1
0224 out.update(sender_id=sender_id, message_id=message_id, resource=RESOURCE)
0225 print(json.dumps(out))
0226 return 0
0227
0228
0229 if __name__ == '__main__':
0230 sys.exit(main())