Nothing open on GitHub. Fix merged in #217 on 2 Mar 2026. Check it covers your case.
The short answer
0open issues
0open PRs
3items grouped
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #217. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #217. That is separate from the fix event. - Still open
- Nothing open on GitHub.
The /retry, /undo, and /reset gateway commands were not functioning correctly, with transcript changes failing to persist and memories being silently lost. These members cover the bug report and associated fixes for this behavior.
Open issues0
Open PRs0
Fixes on record
-
#217 fix(gateway): persist transcript changes in /retry, /undo and fix /reset attribute
files:
gateway/run.py,gateway/session.py,tests/gateway/test_session.pyShow the diff
-
gateway/run.py@@ -901,13 +901,12 @@ async def _handle_reset_command(self, event: MessageEvent) -> str: source = event.source # Get existing session key - session_key = f"agent:main:{source.platform.value}:" + \ - (f"dm" if source.chat_type == "dm" else f"{source.chat_type}:{source.chat_id}") + session_key = self.session_store._generate_session_key(source) # Memory flush before reset: load the old transcript and let a # temporary agent save memories before the session is wiped. try: - old_entry = self.session_store._sessions.get(session_key) + old_entry = self.session_store._entries.get(session_key) if old_entry: old_history = self.session_store.load_transcript(old_entry.session_id) if old_history: @@ -1135,7 +1134,7 @@ async def _handle_retry_command(self, event: MessageEvent) -> str: # Truncate history to before the last user message truncated = history[:last_user_idx] - session_entry.conversation_history = truncated + self.session_store.rewrite_transcript(session_entry.session_id, truncated) # Re-send by creating a fake text event with the old message retry_event = MessageEvent( @@ -1167,7 +1166,7 @@ async def _handle_undo_command(self, event: MessageEvent) -> str: removed_msg = history[last_user_idx].get("content", "") removed_count = len(history) - last_user_idx - session_entry.conversation_history = history[:last_user_idx] + self.session_store.rewrite_transcript(session_entry.session_id, history[:last_user_idx]) preview = removed_msg[:40] + "..." if len(removed_msg) > 40 else removed_msg return f"↩️ Undid {removed_count} message(s).\nRemoved: \"{preview}\"" -
gateway/session.py@@ -567,6 +567,37 @@ def append_to_transcript(self, session_id: str, message: Dict[str, Any]) -> None with open(transcript_path, "a") as f: f.write(json.dumps(message, ensure_ascii=False) + "\n") + def rewrite_transcript(self, session_id: str, messages: List[Dict[str, Any]]) -> None: + """Replace a session's transcript with the given messages.""" + # Rewrite SQLite + if self._db: + try: + self._db._conn.execute( + "DELETE FROM messages WHERE session_id = ?", (session_id,) + ) + self._db._conn.execute( + "UPDATE sessions SET message_count = 0, tool_call_count = 0 WHERE id = ?", + (session_id,), + ) + self._db._conn.commit() + for msg in messages: + self._db.append_message( + session_id=session_id, + role=msg.get("role", "unknown"), + content=msg.get("content"), + tool_name=msg.get("tool_name"), + tool_calls=msg.get("tool_calls"), + tool_call_id=msg.get("tool_call_id"), + ) + except Exception as e: + logger.debug("Session DB rewrite failed: %s", e) + + # Rewrite legacy JSONL + transcript_path = self.get_transcript_path(session_id) + with open(transcript_path, "w") as f: + for msg in messages: + f.write(json.dumps(msg, ensure_ascii=False) + "\n") + def load_transcript(self, session_id: str) -> List[Dict[str, Any]]: """Load all messages from a session's transcript.""" # Try SQLite first -
tests/gateway/test_session.py@@ -1,9 +1,13 @@ """Tests for gateway session management.""" +import json import pytest +from pathlib import Path +from unittest.mock import patch, MagicMock from gateway.config import Platform, HomeChannel, GatewayConfig, PlatformConfig from gateway.session import ( SessionSource, + SessionStore, build_session_context, build_session_context_prompt, ) @@ -199,3 +203,59 @@ def test_whatsapp_prompt(self): prompt = build_session_context_prompt(ctx) assert "WhatsApp" in prompt or "whatsapp" in prompt.lower() + + +class TestSessionStoreRewriteTranscript: + """Regression: /retry and /undo must persist truncated history to disk.""" + + @pytest.fixture() + def store(self, tmp_path): + config = GatewayConfig() + with patch("gateway.session.SessionStore._ensure_loaded"): + s = SessionStore(sessions_dir=tmp_path, config=config) + s._db = None # no SQLite for these tests + s._loaded = True + return s + + def test_rewrite_replaces_jsonl(self, store, tmp_path): + session_id = "test_session_1" + # Write initial transcript + for msg in [ + {"role": "user", "content": "hello"}, + {"role": "assistant", "content": "hi"}, + {"role": "user", "content": "undo this"}, + {"role": "assistant", "content": "ok"}, + ]: + store.append_to_transcript(session_id, msg) + + # Rewrite with truncated history + store.rewrite_transcript(session_id, [ + {"role": "user", "content": "hello"}, + {"role": "assistant", "content": "hi"}, + ]) + + reloaded = store.load_transcript(session_id) + assert len(reloaded) == 2 + assert reloaded[0]["content"] == "hello" + assert reloaded[1]["content"] == "hi" + + def test_rewrite_with_empty_list(self, store): + session_id = "test_session_2" + store.append_to_transcript(session_id, {"role": "user", "content": "hi"}) + + store.rewrite_transcript(session_id, []) + + reloaded = store.load_transcript(session_id) + assert reloaded == [] + + +class TestSessionStoreEntriesAttribute: + """Regression: /reset must access _entries, not _sessions.""" + + def test_entries_attribute_exists(self): + config = GatewayConfig() + with patch("gateway.session.SessionStore._ensure_loaded"): + store = SessionStore(sessions_dir=Path("/tmp"), config=config) + store._loaded = True + assert hasattr(store, "_entries") + assert not hasattr(store, "_sessions")
-
1 PR closed without merging
From the close comments: closed without comment 1
- #213 fix(gateway): make /retry, /undo, and /reset actually work (fixes #210) · closed without comment
Closed issues · 1
- Closed#210/retry and /undo gateway commands have no effect, /reset silently loses memories
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Merged PRs
- #217
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 2 Mar 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.