From 59128f14a3b6021782e2e0b8cfda4b10b5e0ffe8 Mon Sep 17 00:00:00 2001 From: yanghang <2390375302@qq.com> Date: Tue, 2 Jun 2026 19:19:55 +0800 Subject: [PATCH] fix(continue): robust session preview for malformed logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /continue list previews showed JSON debris (e.g. `{`, `" } ]} === Response ===`) for some sessions. Two causes in _preview_from_file: 1. Models sometimes emit an unclosed , so the non-greedy DOTALL regex pairs it with a far-away and captures === block headers / JSON across rounds. Now only the latest is used, and it is rejected if it looks like such a cross-segment capture (contains `=== `/`"role"` or >200 chars) — falling through instead of digging for older summaries. 2. The fallback scanned for the first non-=== line, returning JSON structure lines. It now uses the last real user prompt via a new _last_user(), which scans Prompt blocks directly (so response-less/aborted sessions still preview) and reuses _user_text() to skip tool_result continuations and all _INJECT_MARKERS (WORKING MEMORY / SYSTEM TIPS / DANGER ...). Scanned 261 local logs: garbage previews 26 -> 0. --- frontends/continue_cmd.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/frontends/continue_cmd.py b/frontends/continue_cmd.py index 260a8c34..f4d53370 100644 --- a/frontends/continue_cmd.py +++ b/frontends/continue_cmd.py @@ -46,6 +46,20 @@ def _first_user(pairs): return '' +def _last_user(text): + """Last real user prompt. Scans `=== Prompt ===` blocks directly (no + Prompt/Response pairing, so response-less/aborted sessions still preview), + newest-first, returning the first one `_user_text` accepts (it drops + tool_result continuations + all _INJECT_MARKERS). Better preview anchor than + the first prompt — reflects what the session was most recently about.""" + for label, body in reversed(_BLOCK_RE.findall(text or '')): + if label == 'Prompt': + t = _user_text(body) + if t: + return t + return '' + + def _last_summary(pairs): for _, response_body in reversed(pairs): try: @@ -173,12 +187,20 @@ def _preview_from_file(path): fh.seek(-_PREVIEW_WIN, 2); tail = fh.read() except OSError: return '' tail_s = tail.decode('utf-8', errors='replace') - m = _SUMMARY_RE.findall(tail_s) - if m: return m[-1].strip() - for line in head.decode('utf-8', errors='replace').splitlines(): - s = line.strip() - if s and not s.startswith('===') and not s.startswith('###') and '' not in s: + # Use only the latest , and reject it if dirty. Models sometimes emit + # an unclosed , so the non-greedy DOTALL match pairs it with a far-away + # and swallows === block headers / JSON across rounds. Treat such a + # match as invalid and fall through to the last user prompt (don't dig older ones). + cands = _SUMMARY_RE.findall(tail_s) + if cands: + s = ' '.join(cands[-1].split()) + if s and '=== ' not in s and '"role"' not in s and len(s) <= 200: return s + # Summary invalid/absent -> last real user prompt (JSON-aware, skips anchors; + # scans Prompt blocks directly so response-less sessions still preview). + lu = _last_user(tail_s) or _last_user(head.decode('utf-8', errors='replace')) + if lu: + return ' '.join(lu.split())[:120] return ''