570 Commits
Author SHA1 Message Date
nesquena-hermes 0adc1f6e4f Merge PR #7754 from starship-s/fix/image-turn-display-context 2026-09-24 10:54:56 +00:00
tancouandClaude Opus 5 90b814f673 fix(mcp): scope MCP status, inventory and /reload-mcp to the request profile
Two profiles configuring a same-named MCP server shared one connection and
the MCP settings panel showed another profile's runtime ("Active, N tools"
next to an empty inventory); /reload-mcp stopped every profile's servers and
rediscovered from the process-default config.

- api.profiles._set_hermes_home() is the single writer of the process-profile
  home: it records get_process_profile_home() and hands it to Hermes Agent's
  hermes_constants.pin_process_hermes_home when available, at startup and on
  switch_profile(process_wide=True), so the per-turn HERMES_HOME mirror no
  longer makes every turn look like the process profile (bare-name,
  cross-profile MCP keys) and the anchor cannot drift from HERMES_HOME.
- Run get_mcp_status/registry reads and /reload-mcp inside
  api.mcp_runtime.mcp_runtime_scope(): request profile's Hermes-home override
  and secret scope (root included), no os.environ mutation, restored on exit.
- One ledger predicate, ledger_key_serves_view() (owned via
  _server_scope_keys/key scope, or adopted via _server_tool_scopes), is shared
  by status, inventory and the reload summary. The Agent's launch-profile view
  is process-wide and name-indexed, so filter_runtime_status_to_view()
  rebuilds connected/tools/status/error from the entries serving the profile:
  the root profile never shows a routed profile's connection (nor lets its own
  parked failed-spawn task borrow a routed profile's live same-named one).
- Inventory lists only tools in the profile's own registry slot for servers it
  configures; responses carry runtime_scope.
- Fail closed when the agent's routing does not match the resolved profile:
  hide runtime data (runtime_scope "unavailable", shown as a notice in the MCP
  panel, 14 locales) and refuse /reload-mcp.
- /reload-mcp shuts down only the profile's own ledger names (parked
  included), drops its own connect cooldown/error entries so a failed spawn is
  retried (the wildcard cleared every cooldown; a scoped shutdown only clears
  the live keys it tears down), rediscovers under the profile's home and
  counts live connections only. The bare wildcard remains for agents without
  profile-scoped MCP.
- Signature introspection is centralized (accepts_keywords) and falls back to
  the bare call instead of blanking every status; ledger key helpers resolve
  to the Agent's tools.mcp_tool_scope when present; tools.mcp_tool is
  resolved by full name so stubbed agents still work.
- ARCHITECTURE.md §4.10 and docs/troubleshooting.md document the boundary.
- Tests: agent-free contract (models adoption, parked tasks, cooldowns and the
  process-wide root view) and real-Agent integration (fake stdio MCP server,
  skips without hermes-agent + mcp SDK).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 09:53:02 +02:00
starship-s 5d42c2acd2 chore(chat): merge upstream for image-turn PR
Keep image-turn context tagging and upstream's typed steer-wrapper cleanup
in the same settlement path. Verify the combined recovery and streaming
behavior against current master.

Assisted-by: Hermes Agent:gpt-6-sol
2026-09-23 18:43:39 -06:00
starship-s 0be6a1f454 fix(chat): prevent duplicate bubbles for conflicting provider payloads
Record pending-turn proof under caller-owned session locks during normal
runs and credential recovery. Preserve both conflicting provider payloads
for replay, but display one sidecar-owned bubble when row identity and
visible content are unambiguous.

Assisted-by: Hermes Agent:gpt-6-sol
Assisted-by: Codex:gpt-6-luna
2026-09-23 12:56:46 -06:00
nesquena-hermes ab130f2cd0 Merge PR #7610 into stage/7610-oob-writeback 2026-09-23 18:23:35 +00:00
starship-s 63c8e3df31 fix(chat): keep pending image turns private through reload
Persist trusted pending Agent timestamp evidence before the model call so
session reads, including metadata polls and cold reloads, keep only the
submitted prompt and attachment visible. Preserve conflicting provider
payloads without guessing which copy is newer.

Assisted-by: Hermes Agent:gpt-6-sol
Assisted-by: Codex:gpt-6-luna
2026-09-23 11:34:30 -06:00
webtecnica 618325501e Merge upstream/master into fix/oob-marker-writeback 2026-09-23 11:19:34 -03:00
webtecnica 6d1753bb6c fix(streaming): extract inner steer text from typed steer rows in place (#7600) 2026-09-23 11:19:25 -03:00
nesquena-hermes 9b6f70c959 Merge PR #7767 into stage/7767-runtime-model 2026-09-23 14:11:17 +00:00
Anthony Ruiz c45cab3779 feat(streaming): publish observed local runtime model to SSE and journal 2026-09-23 11:11:52 +00:00
nesquena-hermes 86b95eadb7 Merge PR #7760 into stage/7760-lease-warnings 2026-09-23 10:29:33 +00:00
starship-s 1f13717ca9 fix(chat): preserve plain-turn matching during settlement
Keep whitespace-normalized text matches for plain Agent rows, retain float timestamps for Gateway turn checkpoints, and test optional attachment suffix behavior rather than source spelling.

Assisted-by: Hermes Agent:gpt-6-sol
Assisted-by: Codex:gpt-6-luna
2026-09-23 02:50:40 -06:00
starship-s 2262444806 fix(chat): keep image-turn context out of user bubbles
Preserve the submitted image turn as the display owner while retaining rich Agent context for model replay and suppressing its durable scalar mirror from recovered WebUI transcripts.

Assisted-by: Hermes Agent:gpt-6-sol
Assisted-by: Codex:gpt-6-luna
Assisted-by: Claude Code:claude-opus-5-5
2026-09-23 02:50:40 -06:00
Anthony Ruiz 936710dc1d fix(streaming): surface Agent session-lease waits as warnings
When another Hermes process holds a session's turn lease, the Agent
(agent/turn_facade_lease.py) emits lifecycle statuses while waiting and on
admission, and a warn status when the wait times out and the message was
not processed. _agent_status_callback dropped all of them, so a WebUI turn
looked stuck with no explanation.

Classify these notices by Agent status kind (lifecycle/warn) plus the
Agent's exact phrases, and relay them as {'type': 'session_lease_wait'}
warning events rendered by the existing messages.js warning listener.
2026-09-23 08:31:39 +00:00
Frank Song 861a10ec34 fix: preserve successor ownership of reused Agents
(cherry picked from commit e4037f1d785aa86074e7cc84025942da5c0cc13a)
2026-09-23 11:56:10 +08:00
Frank Song acdcddf5c9 fix: make Agent cache publication atomic with Stop
(cherry picked from commit c40c43e0eee76a829b113d0119a476a651b60125)
2026-09-23 11:56:10 +08:00
Frank Song 281e91aca3 fix: publish newly constructed agents only after Stop admission
(cherry picked from commit 84a12254fc36aaff628b7e8e43daf48dfd648e67)
2026-09-23 11:56:10 +08:00
Frank Song 9b3951583d fix: fence self-heal worker registration against cancellation
(cherry picked from commit 0fb38b7b5c9453781353464ca5c48c7365359cf1)
2026-09-23 11:56:10 +08:00
nesquena-hermes 7109a9aed5 Merge PR #7569 from happy5318: perf(run-journal): rebuild live snapshots without quadratic work 2026-09-22 22:27:01 +00:00
nesquena-hermes e5839b1a20 Merge PR #7727 (contributor head bbdfbae2f7) 2026-09-22 21:14:09 +00:00
Ubuntu bbdfbae2f7 docs: update _title_language_mismatch docstring for #7693 fix 2026-09-22 22:20:28 +05:00
Ubuntu c42c5b4d30 fix: resolve #7693 — Cross-script title guard rejects valid CJK+English titles
When the user writes in CJK, the title language mismatch guard now allows
Latin characters in the title as long as the title also contains CJK
characters (cjk >= 2). This permits CJK titles with borrowed English
product/technical terms (e.g. 'WeChat Pay 回调失败排查', 'Python 代码修复')
while still rejecting pure-Latin titles for CJK conversations and
unrelated scripts (Cyrillic, Arabic, etc.).

Added regression tests: test_cjk_start_mixed_cjk_latin_title_is_allowed
and test_cjk_start_pure_latin_title_still_rejected.
2026-09-22 22:19:26 +05:00
KayZz69 1692296c10 fix(streaming): tolerate null tool call history 2026-09-22 07:13:40 +00:00
laitekin 5cbc5f7106 fix(run-journal): preserve cursor ids for unjournaled metering 2026-09-21 22:27:23 +08:00
happy5318 221f25dcda fix(run-journal): reset folded reason index on timed-flush buffer clear
The ~10 Hz reasoning throttle clears the coalescing buffer before emitting
but left _reasoning_buffer_index holding offsets for text that was just
removed. Subsequent _strip_reasoning_output_echo cut_to() calls then used
raw offsets that no longer correspond to the buffer, so the live-echo strip
truncated the buffer at a phantom position and falsely marked the interim
as reasoning_echo: true — leaking a truncated copy into both the SSE stream
and the durable journal.

Fix: call _reasoning_buffer_index.reset() immediately after zeroing the
buffer in the timed-flush branch, matching the flush/_flush_reasoning_buffer
paths that already keep the folded index in step with the raw text.

Adds tests/test_7569_timed_flush_index_reset.py pinning the call site:
- revert this fix and both tests fail (verified via revert + re-run).
2026-09-21 22:05:12 +08:00
happy5318 dcac71c74e fix(run-journal): match echo suffixes through an incremental folded index
Review follow-up for #7569: change 4 of the live-snapshot perf PR replaced
one quadratic with another. The window-free backward walk is correct, but it
re-walks the raw reasoning transcript's whitespace once per interim event —
on a whitespace-heavy transcript that span is megabytes, and the replay went
~55x slower than master on the production shape the gate measured
(1MB / 20-interim journal: 0.035s master → 1.922s branch).

## Thinking Path

The gate's finding was that the cost is structural, not a tweak: the matcher
consults the *whole* accumulated transcript on every interim event, so any
raw-text rescan of it is O(interim_count x whitespace_span). The fix is the
data-structure change the review asked for — maintain the folded view and the
raw cut offsets incrementally, and match/strip from that indexed state
instead of rescanning the raw buffer.

## What Changed

- `api/streaming.py`: new `_CompactEchoIndex` — an append-only folded index
  that records, per non-whitespace character, its raw index. `append` folds a
  chunk once (cost proportional to the chunk, never the buffer),
  `matches_tail`/`cut_to` compare only the candidate against the folded tail
  (O(len(candidate)), no raw walk), `reset` drops the state after the raw
  text is truncated. The old raw helpers (`_find_compact_echo_suffix_start`,
  `_strip_compact_echo_suffix`) stay as the equivalence oracle.
- `api/routes.py::_run_journal_live_snapshot`: reasoning chunks feed the
  index as they are appended; `reasoning_echo_tail_matches` and
  `strip_reasoning_echo_tail` consult the index. The strip re-indexes the
  truncated transcript so later probes match against it.
- `api/streaming.py` live path: the same swap for the live-stream echo strip.
  The three live reasoning buffers (STREAM_REASONING_TEXT, the coalescing
  buffer, the per-message segments) each carry a mirrored index, fed on every
  append and re-indexed after a strip. The buffer flush resets the buffer's
  index with the text it drops, so the index never describes text that is no
  longer there.
- `docs/rfcs/webui-run-state-consistency-contract.md`: the echo-match clause
  now names the indexed representation and the scaling regression that pins
  it.

## Why It Matters

Removing one quadratic by introducing another in the same hot path is the
exact risk the gate flagged — the headline benchmark used a journal shape
where the window-free walk's cost does not dominate, so the aggregate number
still looked like a win. The index keeps the original 8.9x and adds the
scaling guarantee on the shape that was regressing.

## Verification

- Measured on the reproduced shape (1MB whitespace span, 20 interim events):
  0.972s → 0.049s. Doubling both span and interim count scales ~2x per step
  (x1 0.041s / x2 0.090s / x4 0.175s) — linear, not quadratic.
- Behaviour equivalence: 300 randomized journals (whitespace-heavy,
  Unicode, chunked, flagged/unflagged interims) rebuilt with the indexed
  implementation and with the raw walk swapped in — sha256 of the canonical
  snapshot JSON matched on all 300.
- `tests/test_compact_echo_index.py` (new, 6 tests): oracle equivalence over
  2000+ randomized cases, chunked-append equivalence, matches_tail/cut_to
  agreement, reset-after-truncation, and two cost probes that fail the raw
  walk.
- `tests/test_live_snapshot_echo_scan_scaling.py` (new, 4 tests): the
  proportional-scaling regression the review asked for — verified to FAIL on
  the pre-fix matcher (14.6x time for 4x rows, the quadratic signature) and
  pass on the indexed one.
- Existing echo/journal suites unchanged and green: 19 items across
  `test_compact_echo_suffix_linear_scan` / `test_live_snapshot_echo_dedup` /
  `test_live_snapshot_single_parse`, plus 66 neighbouring items
  (`test_issue1361_cancel_data_loss`, `test_issue2565_reasoning_accumulation`,
  `test_issue2713_streaming_segment_flush`, `test_cancel_interrupt`,
  `test_run_journal`).

## Contract Change

None. The matcher's observable behaviour is unchanged (same cut points, same
whitespace-insensitive semantics); only its cost profile changes. No config
knobs, no API surface.

## Risks

- Index drift: if a future writer mutates one of the reasoning buffers
  without feeding (or resetting) its index, the matcher would silently miss
  echoes. Mitigated by keeping the index feed adjacent to every existing
  write site and by the reset-on-flush/reset-on-strip symmetry; the
  equivalence test would catch a dropped feed as a missed echo.
- Memory: the index stores one char plus one int per non-whitespace
  character. On a whitespace-heavy transcript this is bounded by the folded
  length, which is what the transcript would cost folded anyway; the raw
  text is still the larger object.

## Model Used

Hermes Agent (step-5-preview) with direct file edits and pytest verification.
Cost properties were measured with a purpose-built scaling probe (proportional
span x interim count), not a fixed-size fixture; the probe is kept as a
regression test rather than a script.

## Related

#7569 (this PR), #7310 (slow /api/session under load), #7534 (incremental
journal read cache — complementary), #7273/#7291 (metering write-side skip).
2026-09-21 12:34:30 +08:00
happy5318 3369bc6a6d fix(run-journal): match echo suffixes with a window-free backward walk
Review follow-up: the bounded echo probe folded at most len(text)*3 raw characters, so a compact-equivalent suffix whose raw span was stretched by interior whitespace was missed - the echo strip was dropped and the interim text was duplicated in the restored transcript. The same retired window existed in the reverse helper (_strip_compact_echo_suffix), pinned by its own test.

Both now share _find_compact_echo_suffix_start: a backward walk over value/suffix that skips whitespace in value, inspects only the echo span, and allocates nothing. It restores the previous output semantics (origin/master folded the whole reasoning text) and is cheaper than the windowed version (no per-call tail fold).

Tests: new journal-reconstruction regression (a 5000-space-stretched echo must appear exactly once; genuine interim text must not be treated as an echo), the suffix-helper oracle updated to window-free semantics, and the window-contract test now pins that a stretched echo IS stripped.
2026-09-21 12:34:30 +08:00
Anthony Ruiz e3db8f7e01 fix: settle steer on every terminal path and preserve recovery drafts 2026-09-20 17:07:58 +00:00
Anthony Ruiz 034e3edef7 fix(streaming): publish initial run and cancellation signal atomically 2026-09-20 17:07:57 +00:00
Anthony Ruiz a98d0c86d0 fix(steer): fence worker registration and final pending drain 2026-09-20 17:07:57 +00:00
Anthony Ruiz 26277f933b fix(steer): revalidate the active-run backend on the cache-only path
The cache-only Steer path only special-cased backend == "gateway" and sent
every other value — including a missing, empty, or unknown backend — to the
cached local agent. Another runtime could own the run while guidance was
enqueued on a stale local agent.

Fail closed instead: the in-process worker now registers its active run with
the explicit local backend tag (WEBUI_LOCAL_CHAT_BACKEND, the same "legacy"
value webui_chat_backend_mode() returns), and cache-only Steer only enqueues
agent.steer() on that tag. Gateway keeps its gateway_steer_queued outcome; a
missing, empty, or foreign backend returns stream_dead without steering.

Extends the positive-ownership regression with backend cases and adds a
worker-side check that the local backend tag is actually registered.
2026-09-20 17:07:57 +00:00
Anthony Ruiz 20c16e9167 fix(steer): require positive ownership on the cache-only path
The cache-only Steer branch (no AGENT_INSTANCES worker) only rejected
*conflicting* ownership, so a missing stream owner or a missing
active-run session_id was treated as non-conflicting and let
agent.steer() run and report accepted without proving the requesting
session owns the run.

Fail closed instead: both the stream owner and the active-run session
must be present and strictly equal to the requesting session (backend
and phase are still revalidated) under the same STREAMS_LOCK ->
ACTIVE_RUNS_LOCK edge; any absent identity returns stream_dead without
calling agent.steer(). No cache eviction, no agent closing, HTTP write
still outside the locks.

Regressions: missing owner, missing/empty run session, both missing,
missing run entry -> stream_dead + steer never called; both present and
equal -> accepted, cache untouched. Legacy happy-path fixtures now
register the ownership they rely on.
2026-09-20 17:07:57 +00:00
Anthony Ruiz 84d66fe45f fix(steer): make cache-only delivery atomic with cancellation
The cache-only compatibility path selected the matching cached agent,
checked stream liveness under STREAMS_LOCK, released the lock, and only
then called agent.steer(). A Stop running in that gap published
phase='cancelling' and detached STREAMS/AGENT_INSTANCES while the
earlier Steer still enqueued into the cached worker, so the HTTP
response reported accepted guidance that cancellation already owned.

Revalidate stream membership, owner, and active-run
session/backend/phase and call agent.steer() under the same
STREAMS_LOCK -> ACTIVE_RUNS_LOCK edge Stop uses; interrupt,
persistence, and HTTP writes stay outside the lock. Cache eviction and
agent closing remain absent.

Extend tests/test_steer_cancel_ordering.py with a cache-only reverse-
order test (AGENT_INSTANCES absent, matching cached worker, decoy cache
entry) gating both unlocked edges around the stream-lock reacquire.
Stop-first must return stream_dead without calling agent.steer(); the
selected cache object is asserted to be the intended worker. Proven
red on the pre-fix code (order ['cancel','steer']) and green after.
2026-09-20 17:07:57 +00:00
Anthony Ruiz 1655113a81 fix: serialize cancellation ownership with active-run steer 2026-09-20 17:07:57 +00:00
Anthony Ruiz 91bd3e03b9 fix(steer): honor Gateway ownership before local cache fallback 2026-09-20 17:07:57 +00:00
Anthony Ruiz c6152e4182 fix(steer): target the owned active worker after compression 2026-09-20 17:07:57 +00:00
Mike McHargueandClaude Opus 5 1f7e5e78da fix: resolve Hermes Agent names moved by the Sep 2026 decomposition
Hermes Agent moved several names the WebUI uses into sibling modules
(tools.approval_context, tools.mcp_tool_discovery/_lifecycle,
hermes_cli.kanban_db_connect/_dispatch, hermes_cli.models_local,
agent.anthropic_credentials). The old paths only resolved through
PLUGIN-COMPAT __getattr__ pointers that warn on every use and are removed
on schedule by reverting one commit.

With the pointers removed, every affected WebUI call site is wrapped in a
broad except, so nothing crashes; features silently degrade instead:
per-turn approval session-key binding is skipped, MCP discovery/status
return nothing, /reload-mcp reports "MCP runtime unavailable", Claude Code
credential linking returns None, every kanban connection raises
AttributeError, and the dispatcher reports "unavailable".

Add api.agent_compat.agent_attr, which resolves a moved name from the
original module's own namespace first (pre-split Agents and existing test
stubs), then from its new home (split Agents, with or without pointers),
then plain attribute access (non-module doubles). Importing only the new
modules would have broken pre-split Agents and bypassed the sys.modules
stubs the test suite installs on the original modules.

Names still native to their original module (_servers, _lock,
has_blocking_approval, is_session_yolo_enabled, register/unregister
gateway notify, the rest of kanban_db) are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 21:26:50 +00:00
webtecnica 9a6a9acffa fix(streaming): gate the OOB scrub to the carrier tool row (#7600)
`_strip_oob_markers_from_messages()` matched on marker presence alone and passed
the whole row to `_strip_oob_blocks()`, so a user row that legitimately quotes a
complete `[OUT-OF-BAND USER MESSAGE]…[/OUT-OF-BAND USER MESSAGE]` example lost
that text on the next settlement — and the shortened pre-match row broke
active-turn identity matching, which duplicated the turn.

The transport only ever appends a consumed `/steer` wrapper to the turn's last
tool result, so the scrub now considers `role == "tool"` rows only and rebuilds
just their `content` (shallow copy): user/assistant rows, stable ids, reasoning
metadata, and turn bookkeeping are never touched.

Measured on the focused file, helper restored to the pre-fix body as sabotage:
- pre-fix body: 1 failed, 7 passed — the new regression prints the deleted
  example plus 3 user rows instead of 2 (the duplicated turn)
- with the fix: 8 passed
Neighbors: test_context_history_sanitization.py 20 passed; writeback/settle
family (12 files) 120 passed, 5 skipped; media slice (7 files) 197 passed,
18 subtests.

Docs: docs/rfcs/webui-pending-intent-controls.md now records that the transport
wrapper is dropped from the settled display and the persisted context state, is
scoped to the carrier row, and that a transcript stored before this behavior
heals on its next settlement.
2026-09-16 18:32:45 -03:00
webtecnica 5c610866c8 fix(streaming): scrub OOB steer wrappers from the merged display transcript (#7600)
The display merge carries earlier turns' rows across verbatim, so a wrapper
persisted by a pre-fix settle would stay rendered for the rest of the session.
Scrub session.messages after the merge — the merge's identity matching still saw
the rows unchanged — so the persisted display copy is marker-free even when the
row was not first written by this turn. Covered by
test_settle_heals_a_transcript_that_already_stored_the_wrapper.
2026-09-16 16:14:28 -03:00
webtecnica 4a3c55fdb0 fix(streaming): strip consumed OOB steer wrapper from the settled transcript (#7600)
A mid-turn /steer is delivered to the agent as an [OUT-OF-BAND USER MESSAGE ...]
block appended to the turn's last tool result. The model-facing gateway history
always dropped that wrapper, but the writeback that produces session.messages
(rendered verbatim) and session.context_messages kept it, so the frontend showed
the raw control wrapper as a chat bubble.

Strip it in _prepare_marker_clean_writeback, the chokepoint both writebacks are
built from, reusing _strip_oob_blocks. Rows without a marker are returned
untouched so stable ids / reasoning metadata keep riding the same objects and the
scrubbed rows still compare equal to the rows persisted by earlier turns.
2026-09-16 16:11:25 -03:00
b3nw fb2e8b0088 fix(custom-providers): route @custom:<slug> models to their own base_url (#7319)
Route @custom:<slug> models to their owning custom provider's base_url so a model picked from a second custom provider group resolves correctly instead of 400ing.

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-09-15 20:05:40 -07:00
alfred-rootson b71c88f292 fix(workspace): preserve remote POSIX paths across Session & streaming callsites (#7131) (#7168)
Closes the remote POSIX workspace path preservation gap across Session and streaming. Profile-aware runtime path resolution.

Co-authored-by: alfred-rootson <alfred.wojtasordzik@gmail.com>
2026-09-15 15:37:11 -07:00
e4d9b799e4 Release: manual title regeneration for queued-user transcripts (#7545) (#7547)
* fix(api): scan to first complete exchange for title regeneration (#7543)

_first_exchange_snippets() aborted at a second consecutive user message
before collecting assistant text, so transcripts opening with queued user
rows (channel-backed imports) hit the missing_exchange rejection and the
deterministic fallback got persisted with HTTP 200 — invisible in the UI
and unrecoverable by retrying.

Keep scanning until the first complete user+assistant pair instead, and
fall back to the last complete exchange when the opening user message
yields no user text (privacy scrubbing) instead of failing with
empty_user_message.

Tests: tests/test_issue7543_title_first_exchange.py (8 cases, incl. the
issue-pinned transcript shape and walker-mocked discriminator tests for
both fixes).

* test(api): drop local-machine notes and sys.path hack from 7543 test module

- Docstring described the change as a local-only uncommitted hotfix with
  machine-specific paths — wrong for an upstream PR and unsafe to publish.
- Drop the defensive REPO_ROOT sys.path insert; the repo conftest already
  puts the package root on sys.path (matches the neighboring test modules).

* fix(api): drop unreachable latest-exchange fallback from title regeneration

Greptile flagged the mocked-walker fallback test (P2) as fabricating
internal state, and an exhaustive enumeration of realistic transcript
shapes (5460 combinations, lengths 1-6) confirms it: both walkers share
the same text extraction and the forward walker scans every message, so
'first walker empty while latest walker yields a pair' is unreachable
from real transcripts. The fallback branch was dead code.

Remove it together with its two walker-mocked tests; the fixed first-
exchange walker alone resolves the reported issue shape. Replace the
mocked tests with an observable E2E error-path test through the real
walkers, and make the aux mock guard-faithful (empty assistant_text ->
missing_exchange per the generate_title_raw_via_aux contract) so the
fail-before proof actually exercises the endpoint contract.

* fix(#7543): scope first-exchange scan-past to the manual-regen path only

The full suite caught that broadening the SHARED _first_exchange_snippets also
activated the automatic in-stream background-title path for consecutive-user-row
transcripts, moving stream teardown from the synchronous stream_end branch to the
background-title-thread branch (test_issue3929 emits_done regressed: stream_end
was no longer in the synchronous queue). #7543 is specifically about the MANUAL
'Regenerate title' endpoint, so gate the scan-past behavior behind an opt-in
scan_past_consecutive_users flag (default False = master behavior) and pass True
only from generate_session_title_for_session. Auto in-stream path is now
byte-identical to master; added a default-behavior regression guard test.

* docs(changelog): stamp #7545 (manual title regen for queued-user transcripts)

---------

Co-authored-by: Raylands <Raylands@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: n <a@n>
2026-09-12 21:12:52 +00:00
656575a400 Fix geometric duplication of recovered reasoning-only rows at turn settle (refs #7388, independent of #7416) (#7443)
* Fix geometric duplication of recovered reasoning-only rows at turn settle

_restore_display_reasoning_metadata() re-inserted every display-only
reasoning row at its API-safe position in the new result. After context
compaction that position is past the end of result_messages, so the row
was appended after the new reply; _message_identity() is None for an
empty assistant row, so the display merge could not dedup it. Each
settled turn therefore re-added every existing empty
_recovered_from_run_journal anchor once more (1,1,2,4,8,... blocks).

Only restore a reasoning-only row in front of its own API-safe
successor; a compacted result with no aligned slot is skipped.

Refs #7388, #7416 (independent: that PR fixes linear journal replay).

* tests: keep docstrings within the 5-line block budget

* Prefer stable ids when aligning a reasoning row's successor at restore

The successor-alignment guard compared the historical successor with the
aligned current row via _message_identity(), which ignores stable ids and
returns equal keys for repeated prompts ("continue"). A distinct current
turn with the same text false-matched the historical successor, every
recovered anchor before it was re-inserted, and 64 anchors became 128
after one restore->merge settle (nesquena-hermes gate on ed2076eda).

Alignment is now stable-id-first: when either row carries an id, both
must carry the same id; _message_identity() is the fallback only when
neither row has one. Adds a production-order regression through
_settle_result_messages() with 64 anchors, stable ids and repeated
"continue" prompts (RED on ed2076eda and origin/master: 128 != 64), plus
a unit test pinning all four id-presence branches.

* Fail closed on stable-id identity: positive-int only, unique, no content fallback

_restore_display_reasoning_metadata() compared ids with raw equality, so a
persisted historical successor with id=True (or 1.0) matched a distinct
current row minted as id=1 and restored the reasoning card before the
wrong turn. Ids are now accepted only when type(value) is int and > 0,
both rows must carry a valid equal id whenever either has an id field,
and the id must be the sole owner in both API-safe projections.
_assign_stable_message_ids() re-mints invalid ids so they cannot survive
into a collision with minted integers.

* tests: explicit zip strict= for the ruff B905 gate

* Resolve the active-turn boundary before restoration; never carry historical ids/rows into it

A historical assistant successor with the same content as the distinct
current assistant ("Done." / "Done.") let _restore_reasoning_metadata copy
the historical stable id onto the current row; the uniqueness guard then
accepted that forged identity and 64 recovered cards settled to 128.

_active_turn_boundary() is decided once in _prepare_marker_clean_writeback
before any restore (token > strict context prefix > last matching user row
> 0) and threaded through _settle_result_messages into both restores.
Rows at/after it receive no historical id/metadata and no historical
reasoning-only row. Assistant-only results are boundary zero.

* Keep the pinned _restore_reasoning_metadata signature; boundary goes through _restore_reasoning_metadata_before_boundary

tests/test_sprint49.py pins the two-arg signature as source text, so the
boundary-aware core lives in _restore_reasoning_metadata_before_boundary
and the original name stays a thin no-boundary wrapper.

* Content-only prefix is not turn ownership: fail closed to boundary 0; sync route passes result/Agent turn authority

- _active_turn_boundary: a previous-context prefix matched by _message_identity
  (ids/timestamps ignored) only narrows where the current user may be; the
  boundary is the prompt-matching user AT OR AFTER that prefix, else 0.
- _handle_chat_sync: resolve authority via _resolve_active_turn_authority
  (result, agent) instead of hard-coding None.
- Regressions: three-row current-only collision (boundary 0, one 64 block),
  exact-authority + append-only controls, ID-less-successor settle/save/load
  pinned at the persisted-display boundary, sync-route authority pass-through.

---------

Co-authored-by: carlotestor <carlotestor@users.noreply.github.com>
Co-authored-by: nesquena-hermes <nesquena+hermes@gmail.com>
2026-09-10 12:29:33 -07:00
Arush Khasru b39bf1a113 Fix key_cmd credentials in agent cache signatures 2026-09-10 04:40:26 +00:00
394ab31eea release: #7351 bind turn workspace as agent cwd (#7504)
* rebase #7351 onto master (bind turn workspace as agent session cwd)

Co-authored-by: samfoy <samfoy@users.noreply.github.com>

* fix test: use deployed agent.runtime_cwd API (_SESSION_CWD.get) not nonexistent _session_cwd_override

The two failing assertions referenced agent.runtime_cwd._session_cwd_override(), a
helper not present in the installed Agent (which exposes _SESSION_CWD, resolve_agent_cwd,
set/clear_session_cwd). The production binder is correct and verified: binding workspace=/tmp
sets _SESSION_CWD.get()=='/tmp' and resolve_agent_cwd()=='/tmp', reset restores _UNSET.
Line 343 was redundant with the surrounding assertions; line 386 now reads _SESSION_CWD.get().

Co-authored-by: samfoy <samfoy@users.noreply.github.com>

* docs(changelog): stamp #7351 turn workspace cwd binding

---------

Co-authored-by: n <a@n>
Co-authored-by: samfoy <samfoy@users.noreply.github.com>
2026-09-10 03:40:45 +00:00
2477d992f4 release: lock-disciplined STREAMS reads (#7339) (#7488)
* fix(streaming): lock-disciplined STREAMS reads via peek_stream()

Writers pop STREAMS under STREAMS_LOCK, but four hot reads used bare
STREAMS.get(): the streaming worker start, the gateway run start, and the two
SSE attach paths in routes.py. Each is individually saved today by GIL-atomic
dict.get plus a None-guard, but the check-then-act windows (get → register,
get → subscribe) race teardown pops, and the bare pattern invites unprotected
copies. peek_stream() centralizes the locked read; all four sites migrate with
no behavioral change.

* test: dual-patch config.STREAMS alongside routes.STREAMS in SSE seam tests

peek_stream() reads the canonical api.config.STREAMS registry; tests that
inject a fake registry by patching only the routes-level re-export left the
helper reading the real (empty) registry and hung the SSE loop to the
pytest-timeout ceiling. Mirror the test_issue6623 pattern: patch both
bindings.

* docs(changelog): stamp #7339 lock-disciplined STREAMS reads

---------

Co-authored-by: Sebastian B <sb@wmc.sh>
Co-authored-by: n <a@n>
2026-09-09 20:22:36 +00:00
b1a5ade137 release: multimodal title-generation fix (#7306) (#7484)
* fix(title): preserve multimodal title generation

Assisted-by: Hermes Agent:gpt-5.6-sol
Assisted-by: Codex:gpt-5.6-luna
Assisted-by: Claude Code:claude-opus-5

* docs(title): explain multimodal title generation

Assisted-by: Hermes Agent:gpt-6-astra

* docs(changelog): stamp #7306 multimodal title-generation fix

---------

Co-authored-by: starship-s <45587122+starship-s@users.noreply.github.com>
Co-authored-by: n <a@n>
2026-09-09 18:50:13 +00:00
webtecnica 61f3cf7ed9 fix(title): publish conversation context for aux title calls
Title generation via the aux client ran without the Agent's ambient
conversation context, so relay targets that key on it (OpenCode Go)
rejected the completion call with HTTP 400.

Publish the webui session id as the Agent conversation context around
the aux completion (restoring it in a finally block) and pass it from
all three title entry paths: background update, background refresh and
the on-demand regenerate helper.

Refs #7470
2026-09-08 22:06:32 -03:00
n 2a804ae38d Merge PR #7402 for gate 2026-09-08 05:23:11 +00:00