Merge pull request #7477 from nesquena/stage/7476

Release exp-v0.52.280: warn once per state.db missing source column (#7476, @rodrigogs)
This commit is contained in:
nesquena-hermes authored and GitHub committed 2026-09-08 19:33:49 -07:00
commit f4fa4bf77d
3 files changed
+93 -6

No files matched your search

+2
View File
@@ -5,6 +5,8 @@
### Fixed
- **A pre-`source`-column `state.db` no longer floods `errors.log` with the same warning every ~15 seconds.** When an older profile's `state.db` lacks the `source` column, the session-listing path logs a "no 'source' column" warning and returns an empty list — but the sidebar poll (behind a 5s cache) re-ran it every ~15s, so a single obsolete-schema profile DB produced a duplicate warning line for the life of the process (an operator saw 276 identical lines in ~70 min). The warning is now emitted once per resolved `state.db` path per process lifetime; the degraded-path behavior is otherwise unchanged, and a restart warns again. Thanks @rodrigogs. (#7476, closes #634)
- **Automatic session-title generation now publishes the conversation context, so relay backends route the title call to the right target.** The aux-client title generator (`generate_title_raw_via_aux` → `call_llm`) ran with no Agent conversation context published, so relay targets that derive routing from it (e.g. OpenCode Go) mis-routed the title request (#7470). The title call now republishes the WebUI session id as the Agent's ambient conversation context for the duration of the aux call and resets it on every exit path (success, early return, and exception), using the real runtime's `ContextVar` so concurrent title calls stay isolated. An older or absent Agent runtime without the context setter keeps the previous behavior rather than failing title generation. Thanks @webtecnica. (#7474, closes #7470)
- **Live progress text in Transparent Stream no longer tears in the middle of a word.** During a streaming turn, a live progress row could split a word across a block boundary and render the tail on its own line (e.g. `Ces deu` / `x fichiers passent.`). The streamed text was always intact — it was purely a client-side render artifact in the live fade/reconcile path. The renderer now adopts the parser-owned DOM once after a parser-owner replacement (keeping the per-token fast path for normal ticks) and handles the source-space cursor at block boundaries, so live prose stays whole with no lost, duplicated, or reordered text. Verified with the crown-jewel stream gate (no flicker, no alternating, no prose reversals across transparent/worklog/hidden modes). Thanks @ruizanthony. (#7082)
+20 -6
View File
@@ -6,6 +6,17 @@ from pathlib import Path
logger = logging.getLogger(__name__)
# state.db paths that already produced the "no 'source' column" warning below.
# ``get_cli_sessions(all_profiles=True)`` re-reads every profile DB on every
# sidebar poll (behind a 5 s cache), so a single pre-``source`` profile DB would
# otherwise re-emit the identical WARNING line every ~15 s for the life of the
# process. The condition is a property of the DB file, not of the poll, so it
# is reported once per path. Process-lifetime only: a restart warns again,
# which is the desired behaviour (the log line is the operator's cue that the
# agent still needs upgrading). Plain ``set`` mutation under the GIL is
# sufficient here; a duplicate line from two racing first calls is harmless.
_SOURCE_COLUMN_WARNED_DB_PATHS: set[str] = set()
def open_state_db_readonly(db_path: Path, log: logging.Logger | None = None) -> sqlite3.Connection:
"""Open the live agent ``state.db`` read-only for a pure-read projection.
@@ -562,12 +573,15 @@ def read_importable_agent_session_rows(
cur.execute("PRAGMA table_info(messages)")
message_cols = {row[1] for row in cur.fetchall()}
if 'source' not in session_cols:
log.warning(
"agent session listing skipped: state.db at %s has no 'source' column "
"(older hermes-agent?). Agent sessions unavailable. "
"Upgrade hermes-agent to fix this.",
db_path,
)
warned_key = str(db_path.resolve())
if warned_key not in _SOURCE_COLUMN_WARNED_DB_PATHS:
_SOURCE_COLUMN_WARNED_DB_PATHS.add(warned_key)
log.warning(
"agent session listing skipped: state.db at %s has no 'source' column "
"(older hermes-agent?). Agent sessions unavailable. "
"Upgrade hermes-agent to fix this.",
db_path,
)
return []
parent_expr = _optional_col('parent_session_id', session_cols)
+71
View File
@@ -77,3 +77,74 @@ class TestCliSessionsErrorSurface:
assert select_pos != -1, "SELECT query not found"
assert pragma_pos < select_pos, \
"Schema introspection must run before the main SQL query"
# ---------------------------------------------------------------------------
# Follow-up: the warning must fire once per state.db, not once per poll.
#
# get_cli_sessions(all_profiles=True) calls read_importable_agent_session_rows()
# for every profile on every sidebar poll (behind a 5 s cache). On an install
# with one pre-``source`` profile DB that produced one identical WARNING line
# every ~15 s in errors.log. The message is correct; its repetition is not.
# ---------------------------------------------------------------------------
import logging
import sqlite3
import pytest
from api import agent_sessions
def _make_pre_source_state_db(path):
"""Build a state.db whose ``sessions`` table predates the ``source`` column."""
conn = sqlite3.connect(str(path))
conn.execute(
"CREATE TABLE sessions (id TEXT PRIMARY KEY, title TEXT, model TEXT, "
"message_count INTEGER, started_at REAL)"
)
conn.execute("CREATE TABLE messages (id INTEGER PRIMARY KEY, session_id TEXT, role TEXT, timestamp REAL)")
conn.commit()
conn.close()
def _source_column_warnings(caplog):
return [
rec for rec in caplog.records
if rec.levelno == logging.WARNING and "has no 'source' column" in rec.getMessage()
]
class TestMissingSourceColumnWarnsOncePerDb:
@pytest.fixture(autouse=True)
def _fresh_dedupe_state(self, monkeypatch):
# The dedupe set is process-lifetime state; give each test its own.
monkeypatch.setattr(agent_sessions, "_SOURCE_COLUMN_WARNED_DB_PATHS", set(), raising=False)
def test_same_db_path_twice_logs_once(self, tmp_path, caplog):
db = tmp_path / "state.db"
_make_pre_source_state_db(db)
with caplog.at_level(logging.WARNING, logger="api.agent_sessions"):
first = agent_sessions.read_importable_agent_session_rows(db, exclude_sources=None)
second = agent_sessions.read_importable_agent_session_rows(db, exclude_sources=None)
assert first == [] and second == []
warnings = _source_column_warnings(caplog)
assert len(warnings) == 1, [w.getMessage() for w in warnings]
# Message text is unchanged: still names the path and the upgrade hint.
assert str(db) in warnings[0].getMessage()
assert "Upgrade hermes-agent" in warnings[0].getMessage()
def test_different_db_path_logs_again(self, tmp_path, caplog):
db_a = tmp_path / "planner" / "state.db"
db_b = tmp_path / "coder" / "state.db"
db_a.parent.mkdir()
db_b.parent.mkdir()
_make_pre_source_state_db(db_a)
_make_pre_source_state_db(db_b)
with caplog.at_level(logging.WARNING, logger="api.agent_sessions"):
agent_sessions.read_importable_agent_session_rows(db_a, exclude_sources=None)
agent_sessions.read_importable_agent_session_rows(db_b, exclude_sources=None)
agent_sessions.read_importable_agent_session_rows(db_a, exclude_sources=None)
warnings = _source_column_warnings(caplog)
assert len(warnings) == 2, [w.getMessage() for w in warnings]
assert str(db_a) in warnings[0].getMessage()
assert str(db_b) in warnings[1].getMessage()