fix(run_agent): 7 bug fixes — db flush, interrupt, tool error handling, retry loop - #76
fix(run_agent): 7 bug fixes — db flush, interrupt, tool error handling, retry loop#760xbyt4 wants to merge 1 commit into
Conversation
- fix(db): remove off-by-one in _flush_messages_to_session_db that skipped first new message - fix(interrupt): add clear_interrupt() to 3 early-return paths preventing stale flag on next run - fix(tools): catch handle_function_call exceptions in both quiet and normal mode paths - fix(todo): remove unrelated _set_interrupt(False) from _hydrate_todo_store - fix(flush): replace fragile identity check with sentinel marker in flush_memories cleanup - fix(retry): change retry loop from <= to < so max_retries=6 means 6 retries not 7 - chore: remove redundant local import re in _clean_session_content
Code fixes (run_agent.py): - Fix off-by-one in _flush_messages_to_session_db skipping one message per flush - Add clear_interrupt() to 3 early-return paths preventing stale interrupt state - Wrap handle_function_call in try/except so tool crashes don't kill the conversation - Replace fragile `is` identity check with _flush_sentinel marker for memory flush cleanup - Fix retry loop off-by-one (6 attempts not 7) - Remove redundant inline `import re`
|
Some of it was out of sync with other changes made earlier in the day, had to cherry pick from it. Thank you for identifying! |
Code fixes (run_agent.py): - Fix off-by-one in _flush_messages_to_session_db skipping one message per flush - Add clear_interrupt() to 3 early-return paths preventing stale interrupt state - Wrap handle_function_call in try/except so tool crashes don't kill the conversation - Replace fragile `is` identity check with _flush_sentinel marker for memory flush cleanup - Fix retry loop off-by-one (6 attempts not 7) - Remove redundant inline `import re`
|
Thanks for the audit here. We reviewed this against current main: the major fixes from this PR appear to have landed later through other changes/cherry-picks, including the DB flush off-by-one, tool-call exception handling, retry-loop bound, and interrupt cleanup paths. Given the branch is now stale and the PR is closed, I’d leave this closed rather than revive it. The only item that may still be worth a separate focused follow-up is whether Appreciate the original bug audit — it helped identify real issues. |
Code fixes (run_agent.py): - Fix off-by-one in _flush_messages_to_session_db skipping one message per flush - Add clear_interrupt() to 3 early-return paths preventing stale interrupt state - Wrap handle_function_call in try/except so tool crashes don't kill the conversation - Replace fragile `is` identity check with _flush_sentinel marker for memory flush cleanup - Fix retry loop off-by-one (6 attempts not 7) - Remove redundant inline `import re`
…, NousResearch#69) and mark arc complete (NousResearch#76) Session close-out audit found the Outcome section only mentioned PR NousResearch#67. PR NousResearch#69 explicitly self-describes as "follow-up to NousResearch#67" (unsigned-commit git fallback) and PR NousResearch#68 (AGENTS.md docs) is also a direct follow-up; neither was recorded. Also clarifies that NousResearch#71/NousResearch#72 (cron ticker heartbeat/stall fix) are an unrelated arc shipped the same day, not part of this project. Claude-Session: https://claude.ai/code/session_01PQKCc5mDedYAiCNyXnTezh Co-authored-by: Claude <noreply@anthropic.com>
…e policy) Whole-function owner tests for anthropic_prompt_cache_policy (cost-critical should_cache/native-layout tree: _cache_disabled short-circuit, native-Anthropic, OpenRouter/Portal Claude|Kimi envelope, Portal Qwen, third-party anthropic_messages Claude, MiniMax provider|host, Alibaba-Qwen, anthropic-wire non-claude-non-minimax fall-through, MoA aggregator resolution/no-agg/rt-fail, unmatched) and plan_cache_sections_for_destination (non-caching strip vs caching plan). Cluster (1999-2237) 100%. Found tech-debt NousResearch#76 (MoA branch recurses with no self/depth guard -> potential infinite recursion on a pathological aggregator.provider=='moa').
…he_policy (NousResearch#76) A MoA preset whose aggregator slot is itself declared provider='moa' (self-referential, or an A->B->A cycle across two presets) made the MoA branch re-enter itself until the stack blew. Measured: 993 recursive frames, each one re-running load_config() + resolve_moa_preset(). The resulting RecursionError was then swallowed by the branch's defensive `except Exception`, so the function silently returned (False, False) — i.e. prompt caching fully OFF, exactly the total cache loss (85% -> 2% share) this MoA branch was written to prevent, with no signal to the operator. Nested MoA aggregators are not a supported configuration, so the branch now refuses once, loudly (logger.warning naming the preset and telling the operator to point the aggregator at a concrete provider), instead of recursing. Verified: 993 resolve_moa_preset calls -> 1. Regression tests cover both the self-referential preset and the two-preset cycle, asserting the call count (the discriminator vs the old behaviour) and the warning. Region 2060-2220: 0 missing lines, 0 missing branches. 61 passed.
…e_approval) Adds a config-gated hold between auto-decompose and worker spawn. - kanban.auto_decompose_require_approval (default false): when true, the auto-decomposer still builds child graphs but calls decompose_task with auto_promote=False, leaving every child in 'todo' — no worker spawns until a human runs 'hermes kanban approve'. - decompose_task() gains an auto_promote override (None=read config), so the auto path can force a hold while manual/CLI decompose keeps its configured behaviour. - 'hermes kanban approve <id>' registered as a semantic alias of promote (same parser + handler) to make the human-in-the-loop intent explicit. - Gateway _resolve_auto_decompose_settings returns (enabled, per_tick, require_approval) so the flag is re-read live each tick like NousResearch#49638. - config_defaults + EN/ZH kanban docs updated; 6 new/updated tests. Closes the 'auto-decomposer spawned live cluster work without sign-off' failure (mike-infra NousResearch#76): fan-out can now be approval-gated so nothing launches against a deployment without human go-ahead.
…e_approval) Adds a config-gated hold between auto-decompose and worker spawn. - kanban.auto_decompose_require_approval (default false): when true, the auto-decomposer still builds child graphs but calls decompose_task with auto_promote=False, leaving every child in 'todo' — no worker spawns until a human runs 'hermes kanban approve'. - decompose_task() gains an auto_promote override (None=read config), so the auto path can force a hold while manual/CLI decompose keeps its configured behaviour. - 'hermes kanban approve <id>' registered as a semantic alias of promote (same parser + handler) to make the human-in-the-loop intent explicit. - Gateway _resolve_auto_decompose_settings returns (enabled, per_tick, require_approval) so the flag is re-read live each tick like NousResearch#49638. - config_defaults + EN/ZH kanban docs updated; 6 new/updated tests. Closes the 'auto-decomposer spawned live cluster work without sign-off' failure (mike-infra NousResearch#76): fan-out can now be approval-gated so nothing launches against a deployment without human go-ahead.
Summary
Systematic bug audit of
run_agent.pyidentified and fixed 7 verified bugs. Each fix is minimal and targeted — no refactoring, no new features.Critical
_flush_messages_to_session_db— first new message after conversation history was skipped, never written to DB (start_idx = len(history) + 1→len(history))clear_interrupt(), causing nextrun_conversation()call to exit immediatelyHigh
handle_function_callexceptions — quiet mode hadtry/finallywithoutexcept, normal mode had notryat all. An exception would crash the agent and break the API contract (missing tool response)Medium
flush_memoriesidentity check could wipe all messages —is not flush_msgcleanup used Python identity comparison; if reference was lost, the while loop would pop every message. Replaced with sentinel markerLow
_hydrate_todo_storecleared global interrupt flag — unrelated_set_interrupt(False)call removedwhile retry_count <= max_retriesallowed 7 retries instead of intended 6. Changed to<import rein_clean_session_content(already imported at module level)Changes
Single file:
run_agent.py(+22, -10)