Commit Graph

57 Commits

Author SHA1 Message Date
teknium1
efc947d72a fix: send the title model call after the turn on a shared custom endpoint
On a `custom` main route (llama.cpp, Ollama, vLLM, ...) whose
auxiliary.title_generation is not pinned elsewhere, the turn prologue fired the
`response_format: json_schema` title request on a daemon thread at the same
instant as the turn's own streaming request, against the same self-hosted
server. A single-slot server can decode the title grammar/completion into the
main reply: the user then receives `{"title": ...}` as the assistant turn, the
main loop persists it as a genuine assistant row, replays it, and the model
adopts the format (#117296). No Hermes writer routes the aux response into the
transcript; the leaked JSON is the main completion itself.

`maybe_auto_title` now returns the upgrade thread and leaves it UNSTARTED when
`title_upgrade_must_wait_for_turn(main_runtime)`; the prologue parks it on
`agent._deferred_title_upgrade` and `finalize_turn` starts it once the model
has answered. Hosted providers keep the turn-start timing. Usage accounting
(`task='title_generation'`) and `sessions.title` are unchanged.
2026-09-20 14:09:57 -07:00
teknium1
85f5f560c3 fix(agent): a paste-only opener's expansion footer must not demote the title preview
The titler receives the opening message AFTER @-reference expansion, so
the Desktop's generated pasted_content @file: ref arrives with a
'--- Context Warnings ---' (or '--- Attached Context ---') footer. That
footer made the ref-only check in build_title_input fail, so the live
wire capture showed the path + warning leading the title-model input
with 'Pasted content: ...' appended after it. Strip the expander footer
when a preview is present, so a paste-only opener lets the preview lead.

Invariant: test_expanded_paste_ref_footer_does_not_demote_the_preview
(red on a5dac801, green here). Re-checked on the wire against the stub
model: title input now starts with the pasted topic, no @file:/warning.
2026-09-18 10:56:00 -07:00
teknium1
34ba61bf67 fix(agent): paste title hint reaches the instant title and the prompt.submit contract
Build on #114129 (@KoNit-K), which carries a Desktop-generated large-paste
preview from the composer through `prompt.submit` -> `display_metadata` ->
turn context -> the shared title input. Two gaps closed:

- `apply_instant_title` never received the preview, so the instant title of a
  paste-only opener was the generated `@file:` path — and stayed that way,
  because the upgrade thread's `derive_title` fallback writes `derived`
  provenance, which never replaces the `derived` title already stored.
  Thread the hint into the instant stage too.
- `build_title_input` let the `@file:` ref lead when the opener was nothing
  but the generated attachment ref; the preview now leads for a ref-only
  opener (an instruction still leads when the user typed one).
- `prompt.submit` gains `title_preview` in the contract (regenerated shared
  TS/OpenRPC); documented as title-only input in the configuration guide.
- Tests trimmed to two invariants (shared input reaches both stages; budget +
  manual attachments stay unread).
2026-09-18 10:56:00 -07:00
KoNit-K
8d25e69b6e fix(desktop): use generated paste previews for titles 2026-09-18 10:56:00 -07:00
teknium1
96b6c534c3 fix(title): demote only the exact greeting placeholder, not prefixed topical titles
_is_provisional_greeting_title used a prefix match, so a real model title
such as 'Friendly greeting card design' was ranked derived; derived never
replaces derived, and after turn 3 the session locked without an llm title
(the gateway topic-rename lane consumes llm titles only). Match the
placeholder exactly ('friendly greeting' / 'friendly greeting in chat').

Also re-shape the turn-3 cap test so it fails on an uncapped or never-retrying
variant: a derived title still gets a model call on turn 3, none on turn 4.
2026-09-18 09:58:45 -07:00
teknium1
f85d9910fa fix(agent): a placeholder title gets re-titled by the first substantive turn
maybe_auto_title used to skip any turn past the opener once the session had a
title of ANY provenance, so a greeting opener locked the name for good: the
instant "hi how are you" (derived) blocked the demoted greeting title, and
nothing ever asked again. Gate the skip on an llm/user title instead, so a
derived placeholder is replaced by the first real request; cap the retry at
turn 3 so a failing title model does not cost one call per turn. Untitled
sessions keep their unlimited retry.

Detect the greeting placeholder by prefix ("Friendly greeting in chat" too)
and keep it as a prompt example on purpose: a predictable placeholder is
detectable, an improvised one would land as llm and lock the title again.

Fixes #113864

Co-authored-by: eminogrande <eminogrande@users.noreply.github.com>
2026-09-18 09:58:45 -07:00
KoNit-K
cc19fcf949 fix(agent): upgrade provisional greeting titles
The model title for a bare greeting ("Friendly greeting") is persisted at
derived authority instead of llm, so it is a placeholder like the instant
title rather than the session's final name.

Part of #113864

Co-authored-by: eminogrande <eminogrande@users.noreply.github.com>
2026-09-18 09:58:45 -07:00
teknium1
eff18764ff fix(aux): bracket-prefixed and fence-containing prose titles are kept
`_is_truncated_structured_output` flagged any reply starting with `{`/`[` or
containing an odd number of fences, so a provider that ignores response_format
and answers `[WIP] Fix login flow` or `Fix ``` rendering in chat` lost its
title (the derived title stayed). Truncated JSON is recognised by its JSON-shaped
opener (`{"`, `["`, `[{`) and an unclosed fence only when the fence opens the
reply — the fragments from #83903 are still dropped.

Review follow-up on #113960; two legit-title cases added to the existing
parametrized invariant (red on the previous head).
2026-09-17 09:07:08 -07:00
teknium1
0b52e40347 fix: auto-titles no longer drop reasoning-model JSON or store truncated fragments
Two failures of the title upgrade shared one root cause: the parser only
looked at message.content and trusted whatever it found there.

- A reasoning model under the json_schema response_format (glm-5,
  minimax on opencode-go) returns content='' with the {"title": ...}
  payload in reasoning_content / reasoning. generate_title read only
  content, so the session kept its derived title forever (#82291). The
  structured extraction now also looks at those fields — structured only,
  so chain-of-thought prose can never become a title.
- max_tokens=64 cut fenced/prefixed JSON mid-value and the prose fallback
  persisted the fragment ('{"title":"Investigate and fix the login butt',
  '```json') as the session title (#83903). Truncated structured output
  (leading brace/bracket, unclosed fence) is now rejected so the derived
  title survives, and the ceiling is 512 — a compliant model stops after
  ~15 tokens regardless, so the budget only costs on replies that would
  have been garbage anyway.

Live: stand-in replaying the issues' logged bodies through the real
generate_title path, before: fragment titles / None, after: correct title
or None; OpenRouter glm-5 and OpenAI controls unchanged.

Co-authored-by: chenlichao <chenlichao@users.noreply.github.com>
Co-authored-by: olympusbuildz <olympusbuildz@users.noreply.github.com>
Co-authored-by: woshicby <woshicby@users.noreply.github.com>
2026-09-17 09:07:08 -07:00
JonthanaHanh
90ae99aa66 fix(agent): use provider-default temperature for title generation (#72351)
Title generation hardcoded temperature=0.3 in its call_llm() call.
Models like GPT-5.6 only accept their server-side default temperature
and reject explicit values with "Unsupported value: 'temperature'".
While call_llm() has a retry that strips temperature on error, the
daemon thread races with session cleanup in short-lived CLI sessions,
causing the retry to fail with a connection error.

Fix: pass temperature=None so the provider uses its own default.  This
avoids the unsupported-temperature error entirely and eliminates the
need for the retry path.

Fixes #72351
2026-09-17 08:45:46 -07:00
teknium1
c02616acb3 fix(aux): model_upgrade_enabled only silences the automatic title upgrade
The gate also sat inside `generate_title`, so the explicit operator repair
command `hermes sessions retitle-skills` returned None for every row when the
toggle was off, although the operator asked for a model call. The toggle's
contract is "never spend a model call upgrading the instant title": only
`maybe_auto_title` (the background path) is gated now; a direct
`generate_title` call still asks the model. Docs sentence adjusted.

Review follow-up on #113955; the existing toggle test now also asserts the
explicit path still titles (red on the previous head).
2026-09-17 08:45:08 -07:00
Gustavo Payano
06387ae8d3 fix(title): keep the derived title while skipping the model upgrade (#85194)
`auxiliary.title_generation.enabled: false` turned off both title stages, so an
operator who only wanted to stop the background model call (unavailable or
metered endpoint) also lost the instant derived title. `model_upgrade_enabled:
false` keeps the derived title and starts no `auto-title` thread; missing keeps
the two-stage default and `enabled: false` still disables both.

Salvaged from #85401 onto current main: the gate reuses `_title_config()` and
sits before `spawn_context_thread` (the thread seam moved off `threading.Thread`).
2026-09-17 08:45:08 -07:00
teknium1
8899aeff53 fix(oneshot): --usage-file ledger reports auxiliary LLM spend
`hermes -z --usage-file` copied only the main-loop result, so title generation,
vision, compression, web_extract and background-review calls — recorded per task
in session_model_usage — never reached the pipeline ledger the flag advertises
as "so pipelines can always account for spend". The Insights page already folds
those rows in (#23270); the ledger is now consistent with it.

- SessionDB.auxiliary_usage_by_task(session_id): per-task sums over the
  session's compression lineage (aux calls bill to the id the turn started with
  while compression mints child ids mid-turn).
- oneshot snapshots aux usage before the turn and attaches the delta after it,
  so a resumed session's earlier runs are not re-billed.
- The report gains `auxiliary` (totals + `by_task`) and
  `total_including_auxiliary`; every existing key keeps its main-loop meaning.
- The auto-title upgrade runs on a daemon thread and can still be writing when
  the turn returns: title_generator tracks in-flight upgrade threads and
  oneshot joins them (bounded) before reading — no sleep, no eager read.

Fixes #112848. Direction shared with #112852 (@KoNit-K), which folded aux into
the headline counters; this keeps them backward compatible instead.

Co-authored-by: KoNit-K <124019182+KoNit-K@users.noreply.github.com>
2026-09-16 17:03:25 -07:00
teknium
9977df914d fix(kanban): delegate_task children never act on the parent's HERMES_KANBAN_TASK
In-process delegate_task children (and cron runs fired from a worker) inherit
the dispatcher worker's HERMES_KANBAN_TASK via os.environ. Three readers still
gated on the bare env var instead of is_dispatcher_owned_worker_context():

- agent/turn_finalizer.py: a child exhausting ITS iteration budget recorded
  `timed_out` against the parent's task and released the parent's claim.
- tools/kanban_tools.py::inject_new_comments_from_env: a child polled operator
  notes addressed to the worker, steered itself with them, and advanced the
  shared per-task watermark so the worker never saw them.
- agent/title_generator.py::_kanban_task_title: a child's session was titled
  after the parent's card.

Each now uses the single identity predicate. Dispatcher-owned workers are
unchanged (existing #87096 tests still pass).

Fixes #112817
2026-09-16 10:43:48 -07:00
teknium1
1b7355d7fa fix: cap kanban card titles so an overlong card still names its worker
Kanban cards have no length limit, but the session title store rejects
titles past SessionDB.MAX_TITLE_LENGTH with ValueError. _persist_session_title
reads that as a unique-title collision, retries with a "#N" suffix (longer
still), and the caller suppresses the second failure - so a worker spawned on
a >100-char card ended up with no title at all, where main at least gave it a
derived one. Trim the card title (with room for the "#N" retry suffix) before
persisting; a retried card now gets "<trimmed> #2" within the cap.

Review finding: >100-char card title left the kanban worker session untitled.
2026-09-15 06:08:20 -07:00
teknium1
c12a3397b1 fix(kanban): read the worker's card title from the board instead of a new env var
Follow-up to the salvaged commit from #111169 (@KoNit-K):

- Drop HERMES_KANBAN_TASK_TITLE. The worker already has HERMES_KANBAN_TASK
  and HERMES_KANBAN_BOARD/HERMES_KANBAN_DB pinned in its env, so
  maybe_auto_title reads the card title from the board itself (no new
  HERMES_* env var for non-secret config; the dispatcher and the
  delegation scrub list stay untouched).
- Unreadable or missing card: the session is named `Kanban task <id>`
  with zero auxiliary calls (the fallback the issue asked for; the
  #109743 seed left such workers untitled).
- The card title persists at `llm` authority via set_auto_title, so a
  manual /title still wins and the upgrade thread never starts.
- Tests trimmed to two invariants against a real board + SessionDB
  (card title, unreadable-card fallback), both red on origin/main.
2026-09-15 06:08:20 -07:00
KoNit-K
56555f88df fix(kanban): name worker sessions from task title 2026-09-15 06:08:20 -07:00
teknium1
8b181940b4 fix(multiplex): background threads and teardown paths carry the turn's profile scope
The profile scope (HERMES_HOME override, secret scope, terminal policy) is a
contextvar bundle bound per turn. A bare threading.Thread / Timer / gRPC
callback starts with an empty context and resolves the LAUNCH profile:

- agent/title_generator.py: the auto-title thread read
  auxiliary.title_generation (model, language, provider key) from the default
  profile's config and billed the default's key for a secondary's session.
  Spawn via agent.memory_provider.spawn_context_thread (copy_context).
- tui_gateway/session_lifecycle.py: every teardown caller is a bare Timer
  (ws-orphan reap), the idle-reaper thread, atexit _shutdown_sessions, the
  session.close pool RPC, superseded_by_resume or compute_host flush - none
  carries a scope, yet on_session_end / commit_memory_session / agent.close ->
  shutdown_memory_provider read the provider's config + credentials at call
  time. Under multiplex they failed closed (tail never committed, #110622
  class); on the Desktop backend a secondary's transcript went to the launch
  profile's memory tenant. _finalize_session and _teardown_session now bind
  _session_profile_runtime_scope(session) around those blocks, which covers
  every spawn site through the single chokepoint.
- plugins/platforms/google_chat/adapter.py: Pub/Sub callbacks run on the gRPC
  SubscriberClient's threads and run_coroutine_threadsafe copies THAT empty
  context onto the loop task, so _dispatch_message and everything under it
  (attachment cache, per-user OAuth token store via _acquire_user_chat_api ->
  _load_per_user_chat_api, TTS keys, delivery ledger, bot-id cache) resolved
  the launch profile. connect() captures its scope; _on_pubsub_message and
  _submit_on_loop run under a per-callback copy of it.

spawn_context_thread gains a kwargs passthrough for the title thread's
callbacks.
2026-09-15 03:47:15 -07:00
Teknium
f0d194df81 Port from QwenLM/qwen-code#9709: reject session titles that echo the prompt's own examples
Small title models parroting a prompt example back verbatim produced
sessions named "Fix login button on mobile" with no relation to the
conversation. The example lines in _TITLE_PROMPT_TEMPLATE now render
from _PROMPT_GOOD_EXAMPLES so the guard set and prompt cannot drift,
and generate_title rejects exact (case-insensitive, wrapper-stripped)
echoes so the instant derived title survives instead. 'Friendly
greeting' stays allowed — it is prescribed output for bare greetings.
2026-09-13 21:08:32 -07:00
liuhao1024
5ea655771b fix(agent): disable reasoning on the title-generation pass 2026-09-12 21:10:02 -07:00
Teknium
d98c48b410 review-fix(comments): restore lost rationale in the 7 files the sweep had to skip (docstring/comment-only) 2026-09-03 10:40:25 -07:00
Teknium
f2fceda404 refactor(agent/E_session): final code compaction — inline single-use predicates, tail() keyword helper, dict literals, elif chains 2026-09-02 23:08:45 -07:00
Teknium
63f1950b4f refactor(agent/E_session): by-hand docstring/comment compaction across 9 files; transcript_repair row lookup helper; activity snapshot literal 2026-09-02 22:23:45 -07:00
Teknium
f6fe5452de refactor(agent/title_generator): compact constant tables, collapse redundant empty-guards 2026-09-02 20:57:06 -07:00
Teknium
34ac5f326a refactor(agent/title_generator): _strip_one_wrapper helper, flatten derive_title cut 2026-09-02 19:41:53 -07:00
Teknium
334b72256a refactor(agent/E_session): contextlib.suppress for swallow-only try/except 2026-09-02 19:27:20 -07:00
Teknium
59dfa83260 refactor(agent/E_session): compact docstrings/comments by hand, keep every WHY 2026-09-02 19:13:00 -07:00
Teknium
b269b9c2a3 refactor(agent/title_generator): _has_upgraded_title/_first_line helpers 2026-09-02 19:05:50 -07:00
Teknium
c01255c90b refactor(agent/title_generator): unify title-prefix strip, compact docstrings 2026-09-02 18:24:28 -07:00
Teknium
c408601937 refactor(agent/review): simplify curator, background_review, verify, insights, title and learning modules (-22% LOC)
Cluster: agent/{curator,curator_backup,background_review,review_engine,
review_idle_queue,insights,learning_graph,learning_graph_render,
learning_mutations,learn_prompt,verification_evidence,verification_stop,
verify_hooks,side_question,title_generator,turn_summary,
manual_compression_feedback,trajectory,moa_trace,trace_upload,verify/*}.
13662 -> 10693 LOC (-2969, -21.7%), behavior-neutral.

- Dead code: 27 private helpers with zero references removed
  (_auto_title_session, _resolve_review_model, _parse_make_targets,
  _filter_verifiable_paths, _find_subsequence, _is_under_root/_temp_dir,
  _merge_runs, learning_graph_render bucket/period/node helpers,
  _memories_dir/_memory_local_index/_node_detail, _cron_jobs_file,
  _retention_cutoff, _scope_for_args, _clean_token, _count_diff_lines,
  _ordered_verbs, _hermes_meta, _iter_skill_files).
- Unified helpers: _read_config_section (curator + curator_backup),
  _write_file/_write_json (4 curator report writers), _msg_text
  (background_review <- side_question), _report_failure/_notify_title
  (title_generator instant/auto paths), _is_under (verification_evidence),
  _scoped SQL pair builder + _query (insights), _optional_lock
  (background_review), verify.recipes table-driven detection.
- if/elif routing -> dict dispatch: side_question role labels,
  curator_backup summary bits, learning_graph_render buckets, insights
  section rendering, verify recipe pickers.
- Redundant defensive layers, single-use wrappers and verbose narrative
  comments collapsed; every non-obvious WHY/invariant kept in compact form.

Verification: parity.py (all REMOVED symbols zero-ref), import smoke for
every module + cli/run_agent/gateway.run/hermes_cli.main/
agent.conversation_loop/tui_gateway.server, old-vs-new fuzz parity on all
shared pure functions, SQL trace parity for insights and
verification_evidence, cluster tests 1354 passed / 0 failed (46 files).
2026-09-02 13:30:25 -07:00
Teknium
d5167831b8 Port from can1357/oh-my-pi#7306: reject answer-shaped auto-title output
A tiny title model that ignores the 3-7 word titling task and answers
the user's first message instead used to have its whole reply stored
(truncated at 80 chars) as the session title. Truncating an assistant
blob still leaves an assistant blob — generate_title now rejects output
over 12 words and returns None, letting maybe_auto_title retry on the
next exchange. The 80-char truncation remains for genuine-but-wordy
titles that pass the word bound.
2026-08-16 22:10:02 -07:00
Brooklyn Nicholson
c0d502db64 refactor(title): decide on the stored title and the real turns behind it
Folds the model-switch fix in with the untitled retry. They answer
different halves and each is wrong alone: counting alone left a session
that merely opened with machinery nameless forever, because nothing
reconsidered it, and the stored title alone would never title at all on a
store too old to report one. Skip only when both agree — past the opening
turn, and already named.

Counting a turn now judges a multimodal one on its text, so "here's a
screenshot, fix the login" counts as the question it is rather than
reading as machinery and undercounting the conversation.

Co-authored-by: yy28 <yy28@vip.sina.com>
2026-08-09 04:33:58 -05:00
yy28
b684cbb094 fix(title): stop model-switch marker from becoming the session title
Switching models before sending the first real message titled the session
"[System: The active model for this chat has…" instead of the user's actual
question.

`_append_model_switch_marker` persists its notice with `role="user"` because
strict OpenAI-compatible providers reject a system message that is not first
(#48338). Titling had no way to tell that apart from a genuine opening turn,
which caused two distinct failures:

1. `_MACHINE_PREFIXES` did not cover the marker. Its `[System: ` prefix
   matches none of `[CONTEXT COMPACTION`, `[Runtime note:`, or `[SYSTEM]`
   (different case, no closing bracket), so `is_titleable_user_message()`
   returned True and the marker was formatted into the title.

2. `maybe_auto_title()` counted the marker as a user message. With the marker
   present, the first real question arrived at `user_msg_count == 2` and the
   `> 1` guard returned early, so the session was never titled at all and its
   `title` column stayed NULL. Fixing only (1) would therefore have traded a
   wrong title for a permanently missing one.

Add the marker prefix to `_MACHINE_PREFIXES` (kept in sync with
`tui_gateway.server._MODEL_SWITCH_MARKER_PREFIX`) and count only titleable
user messages when detecting the opening turn.

The guard stays narrow: ordinary user text that happens to start with
"[System:" still titles normally.

Adds 6 regression tests, verified to fail without the fix.
2026-08-09 04:33:58 -05:00
Brooklyn Nicholson
53a4003208 fix(agent): stop titling a session after our own scaffolding, or a TTS model
Two lookalike gaps found auditing the titler.

_MACHINE_PREFIXES missed the compressor's legacy summary opener and the
"[System note:" injections, so a compacted or resumed session could be
named after the note that carried it. Take the summary prefix from the
compressor that emits it rather than keeping a fourth local copy.

The fast-model exclude list covered embedders but not the other non-chat
siblings a provider names after its chat model — "gpt-4o-mini-tts"
satisfies the "-mini" rung and cannot answer a prompt.
2026-08-09 04:33:58 -05:00
Brooklyn Nicholson
34577fcb03 fix(gateway): rename a Discord thread once, after the reply lands
Titling is two-stage — a slice of the user's own words lands inline, the
model's version replaces it a second later — and the platform rename lanes
fired on both. That is two rate-limited calls to reach one name, and
Discord allows two channel renames per ten minutes, so the throwaway could
be the one that survived. The callback now carries which stage it is, and
the lanes take the model's.

The relay lane also asked where the reply landed at title time, which is
before the model has answered: it polled the send-result cache for ten
seconds and read the timeout as "never auto-threaded", so any turn with
tool calls in it silently kept its raw thread name. Wait on the send
itself instead — the adapter already owns that cache, so it can say when a
reply arrives and, just as usefully, that one arrived carrying nothing.
2026-08-09 04:33:58 -05:00
Brooklyn Nicholson
f726090d48 feat(sessions): name a session the moment it starts
Titling fired on the first response, so a session sat unnamed for the whole
opening turn - p50 151s, p90 1212s across real sessions, because a turn is
tool calls, not one round-trip. A turn that failed or was interrupted never
got a title at all. Four surfaces each carried their own copy of the call.

Move it into the shared turn prologue and split it in two: a deterministic
title derived from the user's opening message, written inline before the
model runs, then one small-model call that upgrades it. The response is
constrained to a JSON object so there is no preamble to strip, and control
wrappers are stripped rather than refused, so a slash command titles as
what the user asked for instead of the command itself.
2026-08-08 17:07:21 -05:00
Gabriel Stoltemberg
59ee85ed50 perf: use load_config_readonly() at read-only call sites in agent/
Salvaged from #56085 (@Stoltemberg), rebased onto current main: sites
main had already converted (credential_pool, auxiliary_client MoA
paths, model_metadata, moa_loop, agent_runtime_helpers) resolve to
main's versions; the remaining ~29 read-only sites across 16 agent/
files swap to the no-deepcopy readonly loader (~135us saved per call).

Full per-site mutation audit performed (every enclosing function read,
escapes traced): 23 SAFE, 5 ESCAPES with read-only consumers, 1 UNSAFE
path (init_agent -> get_compatible_custom_providers -> normalizer
in-place alias writes) fixed by the preceding no-mutate commits, which
make the normalizer copy-safe for ALL callers.
2026-07-29 15:28:15 -07:00
Brooklyn Nicholson
dcf5fc0eea fix(sessions): stop titling a session after the skill it invoked
generate_title() sent the first 500 characters of the user turn to the
auxiliary model. On a /skill invocation those characters are the skill's own
opening prose, so the session got named after the skill instead of the request
— /work sessions came back as "Isolated Git Worktree Setup".

Route the turn through describe_skill_invocation() first, so the titler sees
what the user typed. Also keep only the first line of the response: a model
that ignores "return ONLY the title" and answers the prompt would otherwise
have a shell transcript stored as the title, truncated mid-command.
2026-07-26 02:58:16 -05:00
Thatgfsj
ef1c622105 fix(title): prevent stale background title generation from reloading unloaded Ollama models
Add a runtime_validator callback to generate_title() / auto_title_session()
/ maybe_auto_title(). Callers snapshot the session's model+provider when
spawning the background titler; the validator runs right before the LLM
request and skips it silently when the live runtime no longer matches —
so a stale title request can't reload a model that strict_single_load
already evicted after a user model switch. Fail-open: a raising validator
never disables titling.

Wired at all four call sites (cli, gateway, tui_gateway, acp_adapter).

Surgical reapply of PR #19137 (base was 8k+ commits stale; the original
patch predates the pinned-language prompts, the atomic-write helper, and
the moved TUI/ACP call sites). Original work by @Thatgfsj. Closes #19027.
2026-07-16 23:07:13 -07:00
Trevor Gordon
9bf5822a2f fix(cron): robust session title generation (#50535, #50536, #50537) 2026-07-16 22:39:47 -07:00
Raşit Akyol
d05cd7c1ef fix(agent): make auto-title write atomic 2026-07-16 22:39:47 -07:00
Raşit Akyol
f725cf830f fix(agent): avoid overwriting manual session titles 2026-07-16 22:39:47 -07:00
Teknium
8222b16785 fix(title): follow-ups for salvaged #37349 — lazy config import, guard ordering, config example
- Make the config imports lazy inside _auto_title_enabled(), matching the
  existing _title_language() pattern (title_generator is imported from agent
  code paths where a module-level hermes_cli import risks circularity).
- Check the enabled flag after the cheap first-exchange guard in
  maybe_auto_title so config isn't read on every turn of a long session.
- Repoint the two new tests at the real import site.
- Document the key in cli-config.yaml.example and merge the enabled flag
  into the existing title_generation block in configuration.md.
- AUTHOR_MAP entry for the contributor.
2026-07-16 22:24:45 -07:00
Danny Huang
e20c3c1c29 fix: honor disabled title generation config 2026-07-16 22:24:45 -07:00
Teknium
31a3822b80 fix(title): contain auto-title thread exceptions instead of dumping tracebacks to the terminal (#65792)
auto_title_session runs as a bare daemon-thread target. Any exception
escaping it hits the default threading excepthook and sprays a raw
traceback into the user's terminal mid-session. The canonical trigger
is the post-'hermes update' stale-module window: the function's lazy
imports read NEW source from disk while already-imported modules
(agent.portal_tags) are still the OLD cached version, producing an
ImportError that repeats on every auto-title attempt until the
long-running process restarts (seen live after 9ce0e67f2 added
set_conversation_context).

The public entrypoint now wraps the body in a catch-all that logs one
WARNING naming the likely cause ('restart the running Hermes process'),
routes the exception through the existing failure_callback channel
(user-visible warning in CLI, debug-suppressed in gateway per #23246),
and never re-raises. This also makes the function honor its own
docstring contract ('silently skips if title generation fails').
2026-07-16 12:41:56 -07:00
Teknium
eb6aa03609 feat(analytics): record auxiliary model usage per task in session accounting (#65537)
* feat(analytics): record auxiliary model usage per task in session accounting

Auxiliary LLM calls (vision, compression, title_generation, web_extract,
session_search, ...) discarded their token usage, leaving dashboard
analytics blind to aux model spend (issue #23270).

- hermes_state.py: session_model_usage gains a task PK dimension
  (''=main loop) via v22 table-rebuild migration (SQLite can't alter a
  PK); record_auxiliary_usage() writes per-(model,provider,task) deltas
  WITHOUT touching sessions counters (gateway overwrites those with
  absolute main-loop totals — folding aux in would double-count or be
  clobbered). Aux rows never inherit the session's main-loop route.
- agent/aux_accounting.py: ContextVar ambient accounting context
  (mirrors the portal_tags conversation context); record_aux_usage()
  normalizes usage via usage_pricing.normalize_usage, estimates cost,
  and is strictly best-effort. moa_reference/moa_aggregator excluded —
  conversation_loop already folds MoA usage+cost into the main delta.
- agent/auxiliary_client.py: _validate_llm_response is the recording
  chokepoint — every successful non-streaming aux response passes
  through it exactly once, sync and async, including fallback paths
  (model read from the response itself stays accurate across
  fallbacks).
- run_agent.py: run_conversation publishes/resets the accounting
  context; agent/title_generator.py republishes on its bare thread.
- hermes_cli/web_server.py: /api/analytics/usage folds aux rows into
  by_model (aux-only models finally appear) and adds a by_task
  summary; /api/analytics/models surfaces aux rows on the Models page.

Design per review of PR #62850 by @eeksock (thread-local + separate
auxiliary_usage table): rebuilt on ContextVar (async-safe — thread-local
cross-attributes concurrent coroutines on one event loop) and the
existing session_model_usage table instead of a parallel accounting
path, extended beyond vision to every aux task, and wired the analytics
endpoints so the dashboard actually shows it. Credit to @eeksock for
the approach and @tboatman for the detailed root-cause analysis.

* test(moa): match _validate_llm_response mock to new accounting-hint signature

* test(aux): accept accounting-hint kwargs in remaining _validate_llm_response mocks
2026-07-16 04:23:12 -07:00
Teknium
9ce0e67f27 feat(portal): ambient conversation context entangles aux/MoA/delegate calls
Extends the conversation=<id> Portal tag (salvaged from PR #65183 by
@J-SUPHA) from main-loop-only to every LLM call in a conversation:

- agent/portal_tags.py: ContextVar-based conversation context.
  nous_portal_tags() falls back to the ambient id when no explicit
  session_id is passed, so every aux tag site (auxiliary_client,
  chat_completion_helpers summary path, web_tools) inherits the tag
  with zero per-call-site plumbing. Ambient id wins over explicit
  per-segment ids since it carries the lineage root.
- hermes_state.py: SessionDB.get_conversation_root() — public wrapper
  over the lineage walk; returns the ROOT session id, so one
  user-facing conversation keeps a single conversation= value across
  context-compression rotation, and delegate subagent trees tag as
  their parent conversation.
- run_agent.py: run_conversation() publishes the root id for the turn
  and resets it in finally. _conversation_root_id() resolves via
  _parent_session_id for subagents.
- agent/moa_loop.py: MoA reference fan-out workers now run under
  propagate_context_to_thread so advisor slots attribute to the acting
  conversation (also fixes approval-callback propagation on that path).
- agent/title_generator.py: bare title thread republishes the context
  from its session id (spawned after turn reset).

Tests: ContextVar semantics, cross-context isolation, thread-hop
propagation, lineage-root resolution incl. cycle guard.
2026-07-16 01:13:43 -07:00
shawchanshek
3b739b990b fix(title_generator): strip think blocks from LLM output before extracting title
Think-enabled models (MiniMax M2.7, DeepSeek, etc.) emit inline
<think>...</think> reasoning even for simple prompts like title
generation, and the raw XML was leaking into session titles. Route the
title-model response through the canonical strip_think_blocks scrubber
before cleanup so every tag variant — closed pairs, unterminated blocks,
orphan closes, mixed case — is handled, not just a single literal
<think> pair.

- 2 regression tests: closed <think> pair stripped, unterminated block
  at start yields no title.

Salvaged from PR #44126 by @shawchanshek.
2026-07-01 04:18:48 -07:00
shandian64
5126902f1d fix(title): honor configured auxiliary timeout 2026-07-01 16:41:43 +05:30
Teknium
cf58f1a520 feat(titles): support language-aware title generation (#45296)
Make auxiliary title prompts match the user language by default, with an optional pinned `auxiliary.title_generation.language` config.
2026-06-19 17:15:52 -07:00