Merge latest master into compression recovery action

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Frank SongandPaperclip committed 2026-07-05 17:23:05 +08:00
commit 322e5a44fa
3 files changed
+90

No files matched your search

+1
View File
@@ -14,6 +14,7 @@
### Fixed
- **Compacted (archived) messages no longer creep back into the model's context.** In-place context compaction marks old messages inactive in `state.db`, but WebUI's reconciliation reader still pulled those archived rows back into the next turn's model-facing context — undoing the compaction and making long conversations re-trigger compression every turn. The reader now excludes inactive rows by default (with an explicit opt-in for recovery/audit views), so compaction actually sticks. Schemas without the active-row marker are unaffected. Thanks @ai-ag2026. (#5626)
- **Internal (test reliability): fixed a test-isolation flake in the auth/profile-cookie suite.** A memoized password-hash cache in `api.auth` wasn't reset between tests, so a test that set an auth password could make later profile-cookie tests fail spuriously depending on run order (a latent CI flake, no user impact). Added an autouse fixture that resets the cache around every test. (#5588)
- **Clearing a forked conversation no longer resurrects the original conversation's messages if the original is later compressed.** When viewing a fork's full transcript, the display could stitch in the original (pre-fork) conversation's history after that original got compressed — so a cleared fork child appeared to bring back messages it shouldn't. Fork transcripts now include only their own fork-sourced history and stop at the first non-fork ancestor. Ordinary (non-fork) conversation history display is unchanged. Thanks @rodboev. (#5582, #5571)
- **Steering a response now keeps your attachments.** If you had files staged in the composer and submitted them mid-response (steer), the attachments were silently dropped. They're now uploaded and included with the steer so the agent can read them, the file chips stay put if the steer is rejected (nothing lost), and retrying a rejected steer reuses the already-uploaded files instead of uploading duplicates. Files you stage while the steer is uploading are preserved. Thanks @ruizanthony. (#5459)
+12
View File
@@ -6258,6 +6258,7 @@ def get_state_db_session_messages(
stitch_continuations: bool = False,
profile=None,
since_timestamp=None,
include_inactive: bool = False,
) -> list:
"""Read messages for a Hermes session from state.db.
@@ -6273,6 +6274,13 @@ def get_state_db_session_messages(
raw state.db scan to rows at or after a sidecar-derived timestamp floor while
preserving the caller's normal merge/window logic. Full-history callers must
leave it unset.
When the messages table exposes an ``active`` column, inactive rows are
compacted/archived history and are intentionally excluded by default. WebUI
reconciliation feeds this reader straight into the next model context; pulling
``active=0`` archive rows back in resurrects pre-compaction history and can
make every later turn re-trigger compression. Pass ``include_inactive=True``
only for explicit recovery/audit views.
"""
try:
import sqlite3
@@ -6364,11 +6372,15 @@ def get_state_db_session_messages(
if since_ts is not None:
since_clause = " AND (timestamp IS NULL OR timestamp >= ?)"
params.append(since_ts)
active_clause = ""
if 'active' in available and not include_inactive:
active_clause = " AND (active IS NULL OR active != 0)"
cur.execute(f"""
SELECT {', '.join(selected)}, session_id
FROM messages
WHERE session_id IN ({placeholders})
{since_clause}
{active_clause}
ORDER BY timestamp ASC, id ASC
""", params)
msgs = []
+77
View File
@@ -0,0 +1,77 @@
import sqlite3
import api.models as models
from api.models import Session, get_state_db_session_messages, reconciled_state_db_messages_for_session
def _make_state_db(path):
conn = sqlite3.connect(str(path))
conn.execute(
"""
CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
role TEXT,
content TEXT,
timestamp REAL,
active INTEGER
)
"""
)
conn.executemany(
"INSERT INTO messages (session_id, role, content, timestamp, active) VALUES (?, ?, ?, ?, ?)",
[
("sid", "user", "archived prompt", 1.0, 0),
("sid", "assistant", "archived answer", 2.0, 0),
("sid", "assistant", "compacted summary", 3.0, 1),
("sid", "user", "live follow-up", 4.0, 1),
],
)
conn.commit()
conn.close()
def test_state_db_reader_excludes_inactive_compaction_archive_by_default(tmp_path, monkeypatch):
db = tmp_path / "state.db"
_make_state_db(db)
monkeypatch.setattr(models, "_active_state_db_path", lambda: db)
messages = get_state_db_session_messages("sid")
assert [m["content"] for m in messages] == ["compacted summary", "live follow-up"]
def test_state_db_reader_can_include_inactive_for_explicit_recovery(tmp_path, monkeypatch):
db = tmp_path / "state.db"
_make_state_db(db)
monkeypatch.setattr(models, "_active_state_db_path", lambda: db)
messages = get_state_db_session_messages("sid", include_inactive=True)
assert [m["content"] for m in messages] == [
"archived prompt",
"archived answer",
"compacted summary",
"live follow-up",
]
def test_reconciled_context_does_not_resurrect_inactive_archive_rows(tmp_path, monkeypatch):
db = tmp_path / "state.db"
_make_state_db(db)
monkeypatch.setattr(models, "_active_state_db_path", lambda: db)
session = Session(
session_id="sid",
messages=[{"role": "user", "content": "full display transcript"}],
context_messages=[{"role": "assistant", "content": "compacted summary", "timestamp": 3.0}],
)
state_messages = get_state_db_session_messages("sid")
reconciled = reconciled_state_db_messages_for_session(
session,
prefer_context=True,
state_messages=state_messages,
)
assert [m["content"] for m in reconciled] == ["compacted summary", "live follow-up"]