Files
1jehuang--jcode/scripts/reload_recovery_audit.py
wehub-resource-sync a789495a98
CI / Quality Guardrails (push) Waiting to run
CI / Build & Test (macos-latest) (push) Waiting to run
CI / Build & Test (ubuntu-latest) (push) Waiting to run
CI / Build & Test (windows-latest) (push) Waiting to run
CI / Format (push) Waiting to run
CI / PowerShell Syntax (push) Waiting to run
CI / Windows Cross-Target Check (Linux) (push) Waiting to run
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:34 +08:00

642 lines
24 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Audit self-dev reload recovery handoffs.
This is a read-only diagnostic helper for bugs where `selfdev reload` restarts the
server but not every interrupted session continues. It correlates three sources:
1. ~/.jcode/reload-recovery/*.json durable recovery intent records
2. ~/.jcode/logs/jcode-*.log recovery lifecycle messages
3. ~/.jcode/sessions/<session>.json coarse transcript markers
The important invariant this helps check is:
A recovery intent should not become effectively terminal unless the client
queued/dispatched the continuation or the server accepted the continuation.
Older builds could mark the server-side intent "Delivered" while merely building
a History payload, before the TUI processed that payload. Current builds keep the
intent pending until the replacement server accepts the exact hidden
continuation. This script flags old-style claimed/delivered records that have no
client queue or server acceptance evidence.
Examples:
scripts/reload_recovery_audit.py
scripts/reload_recovery_audit.py --reload-id reload_1779786108758_15766248331403251908
scripts/reload_recovery_audit.py --max-log-files 5 --show-lines
scripts/reload_recovery_audit.py --json
"""
from __future__ import annotations
import argparse
import dataclasses
import json
import os
import pathlib
import re
import sys
from collections import defaultdict
from typing import Any
PERSISTED_RE = re.compile(
r"reload recovery store: persisted intent reload_id=(reload_[^ ]+) session=([^ ]+) role=([^ ]+)"
)
CLAIMED_RE = re.compile(
r"reload recovery store: claimed intent reload_id=(reload_[^ ]+) session=([^ ]+) role=([^ ]+)"
)
ATTACHED_RE = re.compile(
r"reload recovery store: attached pending intent reload_id=(reload_[^ ]+) session=([^ ]+) role=([^ ]+)"
)
DELIVERED_RE = re.compile(
r"reload recovery store: delivered intent reload_id=(reload_[^ ]+) session=([^ ]+) role=([^ ]+) accepted_by=([^ ]+)"
)
NON_PENDING_RE = re.compile(
r"reload recovery store: skipping non-pending intent session=([^ ]+) reload_id=(reload_[^ ]+) status=([^ ]+)"
)
HISTORY_QUEUE_RE = re.compile(
r"History payload requested reload recovery continuation: session=([^ ]+) was_interrupted=([^ ]+)"
)
TUI_FLOW_RE = re.compile(
r"reload recovery flow=([^ ]+) session_id=([^ ]+) outcome=([^ ]+) detail=(.*)"
)
HISTORY_SNAPSHOT_RE = re.compile(
r"history_reload_recovery_snapshot: (?:using|attaching) server-owned recovery intent for session=([^ ]+)"
)
SEND_HISTORY_RE = re.compile(r"\[TIMING\] send_history write: session=([^, ]+)")
HIDDEN_SEND_RE = re.compile(r"Sending hidden continuation reminder \((\d+) chars\)")
MESSAGE_RUNNING_RE = re.compile(
r"new_status=running old_status=[^ ]+ phase=member_status_updated .*session_id=([^ ]+)"
)
RELOAD_ID_RE = re.compile(r"reload_\d+_\d+")
@dataclasses.dataclass
class Event:
kind: str
line: int
text: str
file: str
reload_id: str | None = None
session_id: str | None = None
role: str | None = None
detail: str | None = None
def compact(self) -> dict[str, Any]:
return {
"kind": self.kind,
"line": self.line,
"file": self.file,
"reload_id": self.reload_id,
"session_id": self.session_id,
"role": self.role,
"detail": self.detail,
"text": self.text.strip(),
}
@dataclasses.dataclass
class IntentRecord:
reload_id: str
session_id: str
role: str | None = None
persisted: Event | None = None
claimed: Event | None = None
attached: list[Event] = dataclasses.field(default_factory=list)
delivered: list[Event] = dataclasses.field(default_factory=list)
delivery_mismatch: list[Event] = dataclasses.field(default_factory=list)
non_pending: list[Event] = dataclasses.field(default_factory=list)
history_snapshot: list[Event] = dataclasses.field(default_factory=list)
history_queued: list[Event] = dataclasses.field(default_factory=list)
tui_flows: list[Event] = dataclasses.field(default_factory=list)
send_history: list[Event] = dataclasses.field(default_factory=list)
hidden_sends_nearby: list[Event] = dataclasses.field(default_factory=list)
member_running: list[Event] = dataclasses.field(default_factory=list)
durable_status: str | None = None
durable_delivered_at: str | None = None
transcript_markers: dict[str, int] = dataclasses.field(default_factory=dict)
def has_client_queue_evidence(self) -> bool:
return bool(self.history_queued) or any(
event.detail
and (
"queued" in event.detail.lower()
or "outcome=resumed" in event.detail.lower()
)
for event in self.tui_flows
)
def has_server_acceptance_evidence(self) -> bool:
return bool(self.delivered) or any(
event.detail
and (
"accepted_by=client_message_accepted" in event.detail
or "accepted_by=server_startup_headless" in event.detail
)
for event in self.tui_flows
)
def verdict(self) -> str:
if self.has_client_queue_evidence():
return "ok-client-queued"
if self.has_server_acceptance_evidence():
return "ok-server-accepted"
if self.durable_status == "pending" or self.durable_status == "Pending":
return "pending"
if self.claimed or (self.durable_status or "").lower() == "delivered":
return "suspect-delivered-without-acceptance-log"
if self.attached:
return "attached-not-accepted"
if self.persisted:
return "persisted-not-attached"
return "observed-without-persist-log"
def compact(self, show_lines: bool = False) -> dict[str, Any]:
data: dict[str, Any] = {
"reload_id": self.reload_id,
"session_id": self.session_id,
"role": self.role,
"durable_status": self.durable_status,
"durable_delivered_at": self.durable_delivered_at,
"persisted": bool(self.persisted),
"claimed": bool(self.claimed),
"attached_count": len(self.attached),
"delivered_count": len(self.delivered),
"delivery_mismatch_count": len(self.delivery_mismatch),
"history_snapshot_count": len(self.history_snapshot),
"history_queued_count": len(self.history_queued),
"tui_flow_count": len(self.tui_flows),
"send_history_count": len(self.send_history),
"member_running_after_reload_count": len(self.member_running),
"transcript_markers": self.transcript_markers,
"verdict": self.verdict(),
}
if show_lines:
data["events"] = [
*(event.compact() for event in [self.persisted, self.claimed] if event),
*(event.compact() for event in self.attached),
*(event.compact() for event in self.delivered),
*(event.compact() for event in self.delivery_mismatch),
*(event.compact() for event in self.history_snapshot),
*(event.compact() for event in self.history_queued),
*(event.compact() for event in self.tui_flows),
*(event.compact() for event in self.send_history),
*(event.compact() for event in self.member_running),
]
return data
def jcode_home() -> pathlib.Path:
return pathlib.Path(os.environ.get("JCODE_HOME", pathlib.Path.home() / ".jcode"))
def log_files(log_dir: pathlib.Path, max_log_files: int) -> list[pathlib.Path]:
files = sorted(log_dir.glob("jcode-*.log"), key=lambda p: p.stat().st_mtime, reverse=True)
return list(reversed(files[:max_log_files]))
def normalize_reload_id(value: str | None) -> str | None:
if not value:
return None
return value if value.startswith("reload_") else f"reload_{value}"
def add_event(records: dict[tuple[str, str], IntentRecord], event: Event) -> None:
if not event.reload_id or not event.session_id:
return
key = (event.reload_id, event.session_id)
record = records.setdefault(
key,
IntentRecord(
reload_id=event.reload_id,
session_id=event.session_id,
role=event.role,
),
)
if event.role and not record.role:
record.role = event.role
if event.kind == "persisted":
record.persisted = event
elif event.kind == "claimed":
record.claimed = event
elif event.kind == "attached":
record.attached.append(event)
elif event.kind == "delivered":
record.delivered.append(event)
elif event.kind == "delivery_mismatch":
record.delivery_mismatch.append(event)
elif event.kind == "non_pending":
record.non_pending.append(event)
elif event.kind == "history_snapshot":
record.history_snapshot.append(event)
elif event.kind == "history_queued":
record.history_queued.append(event)
elif event.kind == "tui_flow":
record.tui_flows.append(event)
elif event.kind == "send_history":
record.send_history.append(event)
elif event.kind == "member_running":
record.member_running.append(event)
def parse_logs(files: list[pathlib.Path]) -> tuple[dict[tuple[str, str], IntentRecord], list[Event]]:
records: dict[tuple[str, str], IntentRecord] = {}
session_latest_reload: dict[str, str] = {}
global_hidden_sends: list[Event] = []
for path in files:
try:
handle = path.open(errors="ignore")
except OSError:
continue
with handle:
for line_no, line in enumerate(handle, 1):
text = line.rstrip("\n")
if match := PERSISTED_RE.search(text):
reload_id, session_id, role = match.groups()
session_latest_reload[session_id] = reload_id
add_event(
records,
Event(
"persisted",
line_no,
text,
path.name,
reload_id,
session_id,
role,
),
)
continue
if match := CLAIMED_RE.search(text):
reload_id, session_id, role = match.groups()
session_latest_reload[session_id] = reload_id
add_event(
records,
Event("claimed", line_no, text, path.name, reload_id, session_id, role),
)
continue
if match := ATTACHED_RE.search(text):
reload_id, session_id, role = match.groups()
session_latest_reload[session_id] = reload_id
add_event(
records,
Event("attached", line_no, text, path.name, reload_id, session_id, role),
)
continue
if match := DELIVERED_RE.search(text):
reload_id, session_id, role, accepted_by = match.groups()
session_latest_reload[session_id] = reload_id
add_event(
records,
Event(
"delivered",
line_no,
text,
path.name,
reload_id,
session_id,
role,
detail=f"accepted_by={accepted_by}",
),
)
continue
if match := NON_PENDING_RE.search(text):
session_id, reload_id, status = match.groups()
session_latest_reload[session_id] = reload_id
add_event(
records,
Event(
"non_pending",
line_no,
text,
path.name,
reload_id,
session_id,
detail=status,
),
)
continue
if match := HISTORY_QUEUE_RE.search(text):
session_id, was_interrupted = match.groups()
reload_id = session_latest_reload.get(session_id)
if reload_id:
add_event(
records,
Event(
"history_queued",
line_no,
text,
path.name,
reload_id,
session_id,
detail=f"was_interrupted={was_interrupted}",
),
)
continue
if match := TUI_FLOW_RE.search(text):
flow, session_id, outcome, detail = match.groups()
reload_id = session_latest_reload.get(session_id)
if reload_id:
add_event(
records,
Event(
"tui_flow",
line_no,
text,
path.name,
reload_id,
session_id,
detail=f"flow={flow} outcome={outcome} detail={detail}",
),
)
continue
if match := HISTORY_SNAPSHOT_RE.search(text):
session_id = match.group(1)
reload_id = session_latest_reload.get(session_id)
if reload_id:
add_event(
records,
Event(
"history_snapshot",
line_no,
text,
path.name,
reload_id,
session_id,
),
)
continue
if match := SEND_HISTORY_RE.search(text):
session_id = match.group(1)
reload_id = session_latest_reload.get(session_id)
if reload_id:
add_event(
records,
Event("send_history", line_no, text, path.name, reload_id, session_id),
)
continue
if match := MESSAGE_RUNNING_RE.search(text):
session_id = match.group(1)
reload_id = session_latest_reload.get(session_id)
if reload_id:
add_event(
records,
Event("member_running", line_no, text, path.name, reload_id, session_id),
)
continue
if match := HIDDEN_SEND_RE.search(text):
global_hidden_sends.append(
Event(
"hidden_send",
line_no,
text,
path.name,
detail=f"chars={match.group(1)}",
)
)
return records, global_hidden_sends
def parse_trace_files(records: dict[tuple[str, str], IntentRecord], trace_dir: pathlib.Path) -> None:
"""Merge structured ~/.jcode/reload-traces/*.jsonl lifecycle events."""
phase_to_kind = {
"intent_attached_to_history": "attached",
"intent_delivered": "delivered",
"intent_delivery_mismatch": "delivery_mismatch",
}
for path in sorted(trace_dir.glob("*.jsonl")):
try:
handle = path.open(errors="ignore")
except OSError:
continue
with handle:
for line_no, line in enumerate(handle, 1):
text = line.rstrip("\n")
try:
data = json.loads(text)
except json.JSONDecodeError:
continue
phase = data.get("phase")
kind = phase_to_kind.get(phase)
if not kind:
continue
reload_id = data.get("reload_id") or path.stem
session_id = data.get("session_id")
if not reload_id or not session_id:
continue
detail_parts = []
if accepted_by := data.get("accepted_by"):
detail_parts.append(f"accepted_by={accepted_by}")
if status := data.get("status"):
detail_parts.append(f"status={status}")
if phase:
detail_parts.append(f"phase={phase}")
add_event(
records,
Event(
kind,
line_no,
text,
path.name,
reload_id,
session_id,
data.get("role"),
detail=" ".join(detail_parts) or None,
),
)
def merge_durable_records(records: dict[tuple[str, str], IntentRecord], recovery_dir: pathlib.Path) -> None:
for path in sorted(recovery_dir.glob("*.json")):
try:
data = json.loads(path.read_text())
except Exception:
continue
reload_id = data.get("reload_id")
session_id = data.get("session_id")
if not reload_id or not session_id:
continue
key = (reload_id, session_id)
record = records.setdefault(
key,
IntentRecord(
reload_id=reload_id,
session_id=session_id,
role=data.get("role"),
),
)
record.role = record.role or data.get("role")
record.durable_status = data.get("status")
record.durable_delivered_at = data.get("delivered_at")
def add_transcript_markers(records: list[IntentRecord], sessions_dir: pathlib.Path) -> None:
markers = [
"Reload succeeded",
"interrupted by a server reload",
"Continue immediately from where you left off",
"Reload complete",
]
session_to_records: dict[str, list[IntentRecord]] = defaultdict(list)
for record in records:
session_to_records[record.session_id].append(record)
for session_id, session_records in session_to_records.items():
path = sessions_dir / f"{session_id}.json"
if not path.exists():
continue
try:
text = path.read_text(errors="ignore")
except OSError:
continue
counts = {marker: text.count(marker) for marker in markers}
for record in session_records:
record.transcript_markers = counts
def short_session(session_id: str) -> str:
if len(session_id) <= 34:
return session_id
return session_id[:31] + "..."
def render_table(records: list[IntentRecord], hidden_sends: list[Event], show_lines: bool) -> str:
if not records:
return "No reload recovery records found for the selected filters."
rows = []
for record in records:
client = "yes" if record.has_client_queue_evidence() else "no"
accepted = "yes" if record.has_server_acceptance_evidence() else "no"
claimed = "yes" if record.claimed else "no"
persisted = "yes" if record.persisted else "no"
transcript = sum(record.transcript_markers.values()) if record.transcript_markers else 0
rows.append(
[
record.reload_id,
short_session(record.session_id),
record.role or "?",
record.durable_status or "?",
persisted,
claimed,
str(len(record.attached)),
accepted,
client,
str(record.send_history and len(record.send_history) or 0),
str(record.member_running and len(record.member_running) or 0),
str(transcript),
record.verdict(),
]
)
headers = [
"reload_id",
"session",
"role",
"store",
"persist",
"old_claim",
"attach",
"accepted",
"client_queue",
"hist_write",
"ran_after",
"markers",
"verdict",
]
widths = [len(h) for h in headers]
for row in rows:
for idx, value in enumerate(row):
widths[idx] = max(widths[idx], len(value))
def fmt(row: list[str]) -> str:
return " ".join(value.ljust(widths[idx]) for idx, value in enumerate(row))
out = [fmt(headers), fmt(["-" * width for width in widths])]
out.extend(fmt(row) for row in rows)
suspects = [record for record in records if record.verdict().startswith("suspect")]
out.append("")
out.append(f"Global hidden continuation send log lines scanned: {len(hidden_sends)}")
out.append(f"Suspect delivered-without-acceptance records: {len(suspects)}")
if suspects:
out.append("")
out.append("Suspects:")
for record in suspects:
claim = f"{record.claimed.file}:{record.claimed.line}" if record.claimed else "durable-only"
out.append(
f" - {record.session_id} ({record.role}) {record.reload_id} terminal={claim}"
)
if show_lines:
out.append("")
out.append("Event lines:")
for record in records:
out.append(f"\n[{record.reload_id} {record.session_id}]")
for event in record.compact(show_lines=True).get("events", []):
out.append(f" {event['file']}:{event['line']} {event['kind']}: {event['text']}")
return "\n".join(out)
def main() -> int:
parser = argparse.ArgumentParser(description="Audit self-dev reload recovery handoffs")
parser.add_argument("--home", type=pathlib.Path, default=jcode_home(), help="JCODE_HOME path")
parser.add_argument("--reload-id", type=str, help="Only show one reload id")
parser.add_argument("--session", type=str, help="Only show one session id")
parser.add_argument("--max-log-files", type=int, default=3, help="Newest jcode logs to scan")
parser.add_argument("--show-lines", action="store_true", help="Show matching source log lines")
parser.add_argument("--json", action="store_true", help="Emit JSON")
args = parser.parse_args()
home = args.home
records, hidden_sends = parse_logs(log_files(home / "logs", args.max_log_files))
parse_trace_files(records, home / "reload-traces")
merge_durable_records(records, home / "reload-recovery")
reload_filter = normalize_reload_id(args.reload_id)
selected = list(records.values())
if reload_filter:
selected = [record for record in selected if record.reload_id == reload_filter]
if args.session:
selected = [record for record in selected if record.session_id == args.session]
selected.sort(key=lambda record: (record.reload_id, record.session_id))
add_transcript_markers(selected, home / "sessions")
if args.json:
print(
json.dumps(
{
"home": str(home),
"records": [record.compact(show_lines=args.show_lines) for record in selected],
"global_hidden_sends": [event.compact() for event in hidden_sends],
},
indent=2,
sort_keys=True,
)
)
else:
print(render_table(selected, hidden_sends, args.show_lines))
return 0
if __name__ == "__main__":
sys.exit(main())