feat(title): heuristic session title generation for instant titles - #55246
feat(title): heuristic session title generation for instant titles#55246DavidMetcalfe wants to merge 2 commits into
Conversation
Adds a synchronous regex-based heuristic title extractor that runs
before the LLM title generation call. The heuristic sets an instant
placeholder title (~0ms), then the LLM overwrites it when it returns.
Flow:
first exchange -> heuristic extracts title -> title appears instantly
-> LLM call runs in background -> overwrites heuristic
This guarantees every session has a title immediately, even when the
LLM call fails or is slow. The LLM still runs to produce better
titles for edge cases.
The heuristic uses pattern matching inspired by RAKE:
- Strip conversational prefixes ('Can you', 'Hey, I need help')
- Match action-verb patterns (fix/add/review/refactor, preserving
the original verb — no normalization)
- Match question patterns ('how does X work' -> 'How X works')
- Progressive length guard (retry with less aggressive cleanup if
result is < 3 words)
- RAKE-inspired keyphrase fallback (split at constituent boundaries,
score by TF x position x length bonus)
Race-condition protection: the LLM thread only overwrites the title
if the current title still matches the heuristic placeholder,
protecting user-set titles that arrived in the interim.
New module: agent/title_heuristic.py (standalone, no external deps)
New tests: tests/agent/test_title_heuristic.py (35 tests)
Updated: tests/agent/test_title_generator.py (race-condition tests)
Closes NousResearch#55201
Hermes Agent review: blocking race regressionThanks for the PR — the heuristic itself is nicely isolated and the focused tests pass locally. I found one behavioral regression in the LLM worker guard that should be fixed before merge. Blocking
Why this matters: Reproducer I ran in the PR worktree: from unittest.mock import MagicMock, patch
from agent.title_generator import auto_title_session
db = MagicMock()
db.get_session_title.return_value = "User Custom Title"
with patch("agent.title_generator.generate_title", return_value="LLM Title"):
auto_title_session(db, "sess", "hi", "hello", heuristic_placeholder=None)
print(db.set_session_title.call_args_list)Actual on this PR: Expected, matching Suggested fix: preserve the old “skip if any title exists” behavior when there is no heuristic placeholder, and keep the new placeholder-specific overwrite only when the current title still equals the heuristic placeholder. A test should assert that Local checks run
Hosted CI was still in progress when checked; completed lints/e2e/supply-chain/contributor checks were green, Python slices and Docker jobs were still running. |
…ne path The reviewer identified a regression: when heuristic_placeholder=None, auto_title_session unconditionally overwrites any existing title. On main, it always checked get_session_title() first and returned if a title existed. Fix: restore the existing-title guard. When heuristic_placeholder is set, use the placeholder-matching logic (only overwrite if current title still matches the placeholder). When heuristic_placeholder is None, preserve the legacy 'skip if any title exists' behavior. Closes NousResearch#55201 (reviewer feedback)
|
@rodriguez46p-ui Good catch — confirmed as a BLOCKER via Gemini Pro review. Root cause: when Fix (pushed): restored the existing-title guard. When Reproducer verified: your exact reproducer now correctly skips — no from unittest.mock import MagicMock, patch
from agent.title_generator import auto_title_session
db = MagicMock()
db.get_session_title.return_value = "User Custom Title"
with patch("agent.title_generator.generate_title", return_value="LLM Title"):
auto_title_session(db, "sess", "hi", "hello", heuristic_placeholder=None)
# Now correctly: no set_session_title call57 tests pass. The |
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: LGTM
Well-designed heuristic session title generation. The approach is clean: a synchronous heuristic sets an instant placeholder title before the LLM call runs in a background thread. The LLM result overwrites the heuristic only if the title still matches the placeholder, protecting user-set titles that arrived in the interim.
Correctness: Race-condition guard is well-thought-out — only overwrites if the current title matches the heuristic placeholder.
Code Quality: Clean separation between heuristic and LLM title paths.
Reviewed by Hermes Agent
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: LGTM
Adds synchronous regex-based heuristic title extractor that runs before LLM title generation. Sets an instant placeholder title (~0ms), then LLM result overwrites it when available.
- Clean separation of heuristic vs LLM paths
- Good fallback behavior when LLM fails
- Well-scoped: 4 files, 608 additions
Reviewed 4 files, 608 additions. Approved.
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the focused fallback implementation. The instant-title premise still holds on current main: agent/title_generator.py:193-204 starts a background worker, whose LLM call precedes the title write at :148-155.
Problems
- The placeholder check in this PR happens before
generate_title()(agent/title_generator.py:149-170), but the write is later at:174-175. A manual/titleduring the LLM call can still be overwritten;hermes_state.py:2988-2993supplies only an unconditional update. - The new heuristic callback at
agent/title_generator.py:230-232runs before the existing final callback. Gateway sends that callback to Discord thread renaming (gateway/run.py:19142-19147), which accepts only the original thread name (gateway/run.py:13686-13692;plugins/platforms/discord/adapter.py:5432-5437). A placeholder rename can therefore prevent the final LLM title from reaching the thread.
Suggested changes
- Use a post-LLM conditional update keyed to the heuristic placeholder, with a test that injects a manual rename during generation.
- Distinguish placeholder UI updates from final external rename callbacks, and cover the two-title Discord sequence.
Automated hermes-sweeper review.
| existing = session_db.get_session_title(session_id) | ||
| if existing: | ||
| return | ||
| if heuristic_placeholder is not None: |
There was a problem hiding this comment.
This comparison is before generate_title(), while the title write is after it. A user rename during the LLM call can still be overwritten; re-check atomically immediately before the write (preferably with a conditional SessionDB update) and add a test that mutates the title from generate_title's side effect.
| logger.debug("Heuristic session title: %s", heuristic_title) | ||
| if title_callback is not None: | ||
| try: | ||
| title_callback(heuristic_title) |
There was a problem hiding this comment.
This sends the same callback twice per session. Gateway maps it to Discord semantic-thread renaming, whose only_if_current_name guard accepts only the original thread name; after this placeholder rename succeeds, the final LLM callback can be rejected. Keep placeholder UI notification separate from final external rename behavior, or make the callback stage-aware.
|
Thanks @rodriguez46p-ui — fixed in commit What I changedRestored the try:
existing = session_db.get_session_title(session_id)
if existing:
if heuristic_placeholder is not None:
# Race-condition guard: only overwrite if the current title
# still matches the heuristic placeholder.
if existing != heuristic_placeholder:
logger.debug(
"Title changed from heuristic placeholder (%r -> %r), skipping LLM overwrite",
heuristic_placeholder, existing,
)
return
# Title matches placeholder — proceed to overwrite with LLM
else:
# Legacy path: any existing title means skip
return
except Exception:
returnVerifying against your reproducerRunning your exact reproducer against the fix: No overwrite. The guard correctly returns when Updated test coverageRenamed db.get_session_title.return_value = "User Custom Title"
with patch("agent.title_generator.generate_title", return_value="LLM Title") as gen:
auto_title_session(db, "sess-1", "hi", "hello",
heuristic_placeholder=None)
gen.assert_not_called()
db.set_session_title.assert_not_called()All four quadrants are now covered:
Test results
The fix was cross-vendor reviewed (Flash + GPT-OSS); both reviewers confirmed the quadrant logic and matched legacy semantics for the |
|
This shipped on main in f726090 — Closing as already implemented. The LLM-failure gap you called out in #55201 is finished off in #82390: an opener that no heuristic can name (an image with no caption, a compaction handoff) now gets reconsidered on a later turn instead of staying #55201 stays open for phase 2 — the title generator still learns nothing from past titles or manual renames. |
Summary
Adds a synchronous regex-based heuristic title extractor that runs before the existing LLM title generation call. The heuristic sets an instant placeholder title (~0ms), then the LLM overwrites it when it returns.
Closes #55201.
What changed
Before
After
New files
agent/title_heuristic.py(~325 lines) — standalone regex-based title extractor with no external dependenciestests/agent/test_title_heuristic.py(35 tests) — comprehensive test suiteModified files
agent/title_generator.py— wired heuristic intomaybe_auto_titletests/agent/test_title_generator.py— updated tests for new behavior, added race-condition testsHow the heuristic works
Uses pattern matching inspired by RAKE (Rapid Automatic Keyword Extraction):
Example outputs:
Race-condition protection
The LLM thread only overwrites the title if the current title still matches the heuristic placeholder. This protects user-set titles that arrive in the interim window between the heuristic set and the LLM completion.
Phase 2 (follow-up PR, outlined in #55201)
Enhance the LLM title generation prompt with examples from the user's past manual renames via a
title_sourcecolumn on thesessionstable. Depends on this PR but ships separately.Testing
pytest-asyncio) are unrelated