c7e50ba4cb
The user ran a game ~20s with no crash but the AI dredged up old log lines, guessed the wrong game, and gave Windows advice. Fixes: - Prompt now includes the real game name + capture duration + outcome (clean vs crash), so the model uses the known game instead of guessing from log paths. - gamelogs.collect(since=…): scope Steam-console lines by timestamp and skip a stale per-app Proton log (mtime before the session) — no unrelated past run. - ai_knowledge: flag benign Steam/Proton lines (libnvidia-ml.so.1 assertion, routine minidumps, "fork without exec") as non-causal. - System prompt: Linux-only steps (no "run as administrator"); don't manufacture a problem on a clean run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
117 lines
3.9 KiB
Python
117 lines
3.9 KiB
Python
"""Collect recent game / Proton / Steam logs to enrich an AI diagnostic (M14).
|
|
|
|
Reads logs that already exist on disk — no change to how the game is launched. Two reliable
|
|
sources: Proton's per-app log (``~/steam-<appid>.log``, written when ``PROTON_LOG=1``) and
|
|
Steam's own console log. Each is tail-read and size-bounded so the AI prompt stays small. The
|
|
text is fed to the AI alongside the findings so it can see *when* something went wrong (a
|
|
vkd3d/DXVK error, a crash line, the exit code) rather than only the sensor summary.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import time
|
|
from pathlib import Path
|
|
|
|
# Steam keeps logs under its install root; ~/.steam/steam usually symlinks to the real one.
|
|
_STEAM_LOG_DIRS = ("~/.steam/steam/logs", "~/.local/share/Steam/logs", "~/.steam/root/logs")
|
|
_STEAM_LOG_FILES = ("console-linux.txt", "console_log.txt", "stderr.txt")
|
|
_TS = re.compile(r"^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]")
|
|
|
|
|
|
def _line_epoch(line: str) -> float | None:
|
|
m = _TS.match(line)
|
|
if not m:
|
|
return None
|
|
try:
|
|
return time.mktime(time.strptime(m.group(1), "%Y-%m-%d %H:%M:%S"))
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def _since_filter(text: str, since: float) -> str:
|
|
"""Keep lines from the first timestamp >= `since` onward (logs are chronological).
|
|
|
|
Untimestamped lines before the window are dropped; once inside the window every line is
|
|
kept (so multi-line entries survive). This scopes a long-lived Steam log to one session.
|
|
"""
|
|
out: list[str] = []
|
|
including = False
|
|
for line in text.splitlines():
|
|
epoch = _line_epoch(line)
|
|
if epoch is not None and epoch >= since:
|
|
including = True
|
|
if including:
|
|
out.append(line)
|
|
return "\n".join(out)
|
|
|
|
|
|
def _tail(path: Path, max_bytes: int) -> str:
|
|
"""Last ``max_bytes`` of a file, decoded leniently (empty string on error)."""
|
|
try:
|
|
size = path.stat().st_size
|
|
with path.open("rb") as fh:
|
|
if size > max_bytes:
|
|
fh.seek(size - max_bytes)
|
|
return fh.read().decode("utf-8", "replace")
|
|
except OSError:
|
|
return ""
|
|
|
|
|
|
def _proton_logs() -> list[Path]:
|
|
try:
|
|
logs = list(Path.home().glob("steam-*.log"))
|
|
except OSError:
|
|
return []
|
|
return sorted(logs, key=lambda p: p.stat().st_mtime, reverse=True)
|
|
|
|
|
|
def _steam_console() -> Path | None:
|
|
for directory in _STEAM_LOG_DIRS:
|
|
base = Path(os.path.expanduser(directory))
|
|
for name in _STEAM_LOG_FILES:
|
|
candidate = base / name
|
|
if candidate.exists():
|
|
return candidate
|
|
return None
|
|
|
|
|
|
def available() -> bool:
|
|
return bool(_proton_logs() or _steam_console())
|
|
|
|
|
|
def collect(since: float | None = None, max_bytes: int = 8000) -> str:
|
|
"""Recent Proton + Steam log tails as one labelled text block ('' if none).
|
|
|
|
With ``since`` (epoch), scope to that session: skip a Proton log not written during/after
|
|
the session (a stale per-app log from an earlier game), and keep only Steam-console lines
|
|
timestamped at/after ``since`` — so we don't feed the model an unrelated past session.
|
|
"""
|
|
sections: list[str] = []
|
|
|
|
protons = _proton_logs()
|
|
if protons:
|
|
log = protons[0]
|
|
fresh = since is None or _mtime(log) >= since
|
|
tail = _tail(log, max_bytes).strip() if fresh else ""
|
|
if tail:
|
|
sections.append(f"--- Proton log ({log.name}) ---\n{tail}")
|
|
|
|
console = _steam_console()
|
|
if console:
|
|
raw = _tail(console, 40000 if since else max_bytes)
|
|
if since is not None:
|
|
raw = _since_filter(raw, since)
|
|
raw = raw.strip()[-max_bytes:].strip()
|
|
if raw:
|
|
sections.append(f"--- Steam log ({console.name}) ---\n{raw}")
|
|
return "\n\n".join(sections)
|
|
|
|
|
|
def _mtime(path: Path) -> float:
|
|
try:
|
|
return path.stat().st_mtime
|
|
except OSError:
|
|
return 0.0
|