Conversation
Related: #64780, #64832, and #63799. Current main still uses |
|
Thanks @alt-glitch — I dug into the three you linked. Correcting the record on my own PR, and flagging a gap none of the three close. #63799 and #64832 are genuine prior art on the same bug, opened 9-11 days before mine, same two production files. I searched issues before filing ( #64832 caught something mine does not. It also routes the store call: store = getattr(runner, "async_session_store", None)
entry = await store.get_or_create_session(source)I checked, and that is correct: The gap none of the three close — why this recurred after #63712 was closed That close cited the AST guard at _GATEWAY_FILES = ("gateway/run.py", "gateway/slash_commands.py")The un-awaited call here lives in None of #63799, #64832, or #70973 touch Two options, happy to do either (or neither, if this is better as a maintainer call):
I'd suggest (2) as a separate PR against whichever fix lands, so the detector stops being narrower than the invariant it advertises. Happy to write it. For reference, my repro is in #70966 with the exact production failure ( |
|
Pushed 7a728dd, acting on both points from the review thread. 1. Store read now goes through the async facade (credit @Manison502 / #64832). I verified the concern is real: I also dropped the sync-DB fallback I had added. Checking the actual callers, the only production ones are the two gateway sites and the runner always wires 2. Widened the guard that should have caught this in the first place.
_GATEWAY_FILES = ("gateway/run.py", "gateway/slash_commands.py")The gateway passes its runner into helpers that live outside The encouraging part: the visitor's alias tracking was already correct. Running it directly against the unpatched file on a clean So this is a scope fix, not new detection logic. Changes:
I deliberately did not widen the scan to every module referencing Verification. With the extended scan set applied to the unfixed That is the call that reached users while the old scope reported clean. 42 passed across One design note worth flagging: while writing this I first used a On duplication — I still think earlier authorship should win. #63799 and #64832 predate mine and I said so above; happy for this to be closed in favour of either. If a maintainer prefers one of those, the guard-scope fix here is independent of which call-site fix lands and I'd be glad to split it into its own PR against whichever does. It is the part that stops this class of bug recurring. |
|
Thanks for identifying a real gateway regression. Current main confirms the premise: Problems
Suggested changes
This is an automated hermes-sweeper review. |
7a728dd to
9a177eb
Compare
|
Thanks — both points were correct and are now addressed in 9a177eb (rebased onto current 1. Source-scanning tests removed. You're right, and I should have caught this myself: 2. I also reverted the While rebasing I checked whether the widened scan set still bought anything, and it doesn't: The only remaining consumer of Worth flagging for the record: that prune removed the guard which #63712 was closed What remains is production fix + behavioural coverage only, 3 files:
10 passed across Standing offer unchanged: #63799 and #64832 predate this PR on the same bug, and I'm happy for this to be closed in favour of either. |
9a177eb to
3c64f47
Compare
|
Rebased onto current What changed upstream. It does not fix this bug. The premise still reproduces on current Why the original fix is now the wrong shape. Making the helper a coroutine would force reverting The resolution. Since the helper now runs on a worker thread, unwrap to the synchronous handle instead of awaiting: sync_db = getattr(db, "_db", db)
messages = sync_db.get_messages_as_conversation(entry.session_id)That is the established pattern in this repo — On the store read. The earlier follow-up commit here also awaited Net effect on the diff. This PR is now 26 lines of fix plus the behavioural test file, down from the previous version. The guard-scope edit to Verification.
|
SummaryFive PRs address or reference this issue complex. #63799 and #70973 synchronously unwrap AsyncSessionDB inside the now-offloaded helper, #64832 and #64956 use the older async-helper approach, and #63806 neither implements its advertised await guard nor touches the warning-read cause. Related pull requests
Duplicates#63799 and current-head #70973 implement the same worker-thread synchronous unwrap, with #70973 providing stronger behavioral coverage. Closed #64956 is the incomplete, stacked counterpart of #64832; #63806 is not a fix for the warning-read issue. Suggested consolidationKeep #70973 open with a salvage path: retain its current-main-compatible worker-thread unwrap and real-DB behavioral tests. Author action on #64832: rebase onto main or split out an implementation that preserves the whole-helper offload; meanwhile close #63799 as a duplicate of #70973 despite its older keep_open review because the reviewed event-loop concern was superseded by the caller offload and its current diff now duplicates #70973, retain #64956 as closed in favor of #64832, and leave #63806 closed as already moot for #63712. Complex graphflowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I64780(["issue #64780 (open)"])
I70966(["issue #70966 (open)"])
subgraph Dup63799 ["PRs duplicating each other"]
P63799["PR #63799 (open)"]
P64832["PR #64832 (open)"]
P64956["PR #64956 (closed)"]
P70973["PR #70973 (open)"]
end
P70973 -->|best fix| I64780
P70973 -->|best fix| I70966
class I64780 open
class I70966 open
class P63799 open
class P64832 open
class P64956 closed
class P70973 open
class P63799 best
class P64832 best
class P64832 best
class P70973 best
class P70973 best
class P70973 target
click I64780 "https://github.com/NousResearch/hermes-agent/issues/64780"
click I70966 "https://github.com/NousResearch/hermes-agent/issues/70966"
click P63799 "https://github.com/NousResearch/hermes-agent/pull/63799"
click P64832 "https://github.com/NousResearch/hermes-agent/pull/64832"
click P64956 "https://github.com/NousResearch/hermes-agent/pull/64956"
click P70973 "https://github.com/NousResearch/hermes-agent/pull/70973"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 5 pull requests and 3 issues in this complex. Each diff was read against this issue; Assessment working set: 65 kB of PR diffs, 18 kB of issue/PR text, 18 kB of discussion (16 comments), 17 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
NousResearch#70966) The gateway holds its session DB as AsyncSessionDB, whose generic __getattr__ forwarder returns an awaitable for every method call so blocking SQLite work is offloaded via asyncio.to_thread. enrich_model_switch_warnings_for_gateway read through that facade, so `messages` was a coroutine rather than a list. _estimate_tokens then raised "TypeError: object of type 'coroutine' has no len()", which both gateway call sites swallow in a debug-level except — leaving the preflight-compression warning silently dead on every gateway /model switch, with only an unrelated-looking 'coroutine was never awaited' RuntimeWarning as evidence. a0b2934 since made the helper run off the event loop: both call sites now dispatch it through asyncio.to_thread because merge_preflight_compression_warning runs the blocking resolve_display_context_length provider probe. That fixed the loop-block but not this bug — the facade read is still unawaited on main, so the warning is still dead. Given the helper now runs on a worker thread, the fix is to unwrap to the underlying synchronous handle rather than to make it a coroutine: getattr(db, "_db", db), the established pattern at gateway/run.py:4658, :5538, :16260, :16328 and :18814. This keeps the helper synchronous, so a0b2934's to_thread dispatch and its offload tests are preserved unchanged. Failures reset messages to None so a DB error degrades to 'no warning' rather than forwarding a stale coroutine. runner.session_store is the plain sync SessionStore (the async facade is a separate attribute, async_session_store), so the store read needs no change once the helper is off-loop. Tests drive the real helper against a real on-disk SessionDB: the warning fires through the async facade, works with a plain sync DB, stays silent below threshold, and survives a DB error. Verified failing with the exact TypeError before this change and passing after; a0b2934's offload tests still pass.
3c64f47 to
4f646d4
Compare
Fixes #70966.
Problem
Every gateway
/modelswitch silently loses the preflight-compression warning. The user is never told the next message will compress, and nothing surfaces at default log level.The gateway holds its session DB as
AsyncSessionDB(gateway/run.py), whose generic__getattr__forwarder returns an awaitable for every method call so blocking SQLite work is offloaded viaasyncio.to_thread.enrich_model_switch_warnings_for_gatewaycalled it withoutawait:messageswas therefore a coroutine, not a list, and_estimate_tokensraised:Both gateway call sites wrap the helper in a bare
except Exceptionlogged at debug level, so the feature failed closed and silently. The only visible artifact was a misleadingRuntimeWarning: coroutine 'AsyncSessionDB.__getattr__.<locals>._offloaded' was never awaited. The warning added for #23767 has never fired on the gateway.Fix
enrich_model_switch_warnings_for_gatewaya coroutine andawaitthe facade call.awaitit at both call sites ingateway/slash_commands.py(both alreadyasync def).inspect.isawaitableso the helper still works when handed a plain syncSessionDB.messages = Noneinstead ofpass, so a DB error degrades to "no warning" rather than forwarding a stale coroutine into the estimator.Scope
AsyncSessionDBis constructed only atgateway/run.py, andgateway/run.pyitself awaits every facade call —context_switch_guard.pywas the only leak. The manyself._session_db.<method>()calls incli.py/run_agent.py/agent/*operate on a plain syncSessionDBand are correct as written; I checked them rather than assuming. CLI and TUI callmerge_preflight_compression_warningdirectly and are untouched, as is its signature (existingtests/hermes_cli/test_context_switch_guard.pypasses unchanged).Tests
tests/gateway/test_model_switch_preflight_warning.pydrives the real helper against a real on-diskSessionDB— no mock of the code under test — and asserts the behaviour contract rather than a frozen string:AsyncSessionDBfacade (the regression guard)SessionDBVerified failing before the change with the exact production
TypeError, and passing after:Neighbouring suites green:
tests/gateway/test_async_session_db.py,test_model_command_async_offload.py,test_model_switch_persistence.py,test_model_command_expensive_confirm.py,test_48031_model_switch_after_auto_reset.py(38 passed) andtests/hermes_cli/test_context_switch_guard.py(5 passed).