Clarify interrupted turn recovery marker

This commit is contained in:
Frank Song committed 2026-05-16 14:29:58 +08:00
1 parent e3035b3e40
commit 49bea3ad01
3 files changed
+28 -15

No files matched your search

+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- **PR TBD** by @franksong2702 (fixes #2370) — Recovered interrupted turns now explain that the WebUI process restarted before the agent finished, that the user message above was preserved, and that no agent output was recovered. Pre-fix, stale pending-turn repair appended the vague `Previous turn did not complete` marker, which looked like an unexplained assistant response after a WebUI restart killed the in-process worker.
## [v0.51.74] — 2026-05-16 — Release AX (stage-367 — 4-PR safe-lane batch — #2362 table-cell spacing + #2363 run-state-consistency RFC + #2365 custom_providers list-format + #2367 settings sidebar i18n)
### Added
+16 -12
View File
@@ -679,6 +679,20 @@ def _get_profile_home(profile) -> Path:
return Path(os.environ.get('HERMES_HOME') or '~/.hermes').expanduser()
def _interrupted_recovery_marker() -> dict:
return {
'role': 'assistant',
'content': (
'**Response interrupted.**\n\n'
'The WebUI process restarted before this turn finished. '
'The user message above was preserved, but no agent output was recovered.'
),
'timestamp': int(time.time()),
'_error': True,
'type': 'interrupted',
}
def _apply_core_sync_or_error_marker(
session,
core_path,
@@ -745,12 +759,7 @@ def _apply_core_sync_or_error_marker(
session.pending_user_message = None
session.pending_attachments = []
session.pending_started_at = None
session.messages.append({
'role': 'assistant',
'content': '**Previous turn did not complete.**',
'timestamp': int(time.time()),
'_error': True,
})
session.messages.append(_interrupted_recovery_marker())
session.save()
logger.info(
"Session %s: recovered pending user turn (messages non-empty), added error marker",
@@ -794,12 +803,7 @@ def _apply_core_sync_or_error_marker(
session.pending_user_message = None
session.pending_attachments = []
session.pending_started_at = None
session.messages.append({
'role': 'assistant',
'content': '**Previous turn did not complete.**',
'timestamp': int(time.time()),
'_error': True,
})
session.messages.append(_interrupted_recovery_marker())
session.save()
logger.info("Session %s: no core transcript found, added error marker", sid)
return True
+8 -3
View File
@@ -231,7 +231,7 @@ class TestRepairStalePendingNoDeadlock:
class TestDraftRecovery:
"""When no core transcript exists, the pending user message is restored as
a recovered user turn (_recovered=True) and the error marker says
'Previous turn did not complete.' — NOT 'preserved as a draft'."""
a clear restart interruption marker — NOT 'preserved as a draft'."""
def test_pending_message_recovered_as_user_turn(self, hermes_home, monkeypatch):
"""When core transcript is missing, the pending_user_message is appended
@@ -310,7 +310,10 @@ class TestDraftRecovery:
assert "preserved as a draft" not in content, (
f"Error marker should not say 'preserved as a draft', got: {content}"
)
assert "Previous turn did not complete" in content
assert "Response interrupted" in content
assert "WebUI process restarted" in content
assert "user message above was preserved" in content
assert error_msgs[0].get("type") == "interrupted"
def test_pending_attachments_recovered(self, hermes_home, monkeypatch):
"""Attachments on the pending message are carried over to the recovered turn."""
@@ -604,7 +607,9 @@ class TestNonEmptyMessagesPendingCleared:
# Exactly one error marker
error_msgs = [m for m in s.messages if m.get("_error")]
assert len(error_msgs) == 1
assert "Previous turn did not complete" in error_msgs[0]["content"]
assert "Response interrupted" in error_msgs[0]["content"]
assert "WebUI process restarted" in error_msgs[0]["content"]
assert error_msgs[0].get("type") == "interrupted"
# Pending fields fully cleared
assert s.pending_user_message is None