fix: token estimation accuracy, context length logging, batch integrity - #6629
fix: token estimation accuracy, context length logging, batch integrity#6629aaronlab wants to merge 2 commits into
Conversation
…agent loop reliability ## Summary Found 5 critical bugs in async error handling, context compression, and cron scheduling: **CRITICAL (2):** 1. Role violation after context compression (context_compressor.py:694-728) - Tool message validation missing when merging summary - Causes API crash and data loss after compression 2. Double-execution race condition in cron scheduler (scheduler.py:843-892) - File lock released before job execution completes - Allows duplicate jobs to be executed (DoS, duplicate messages) **HIGH (1):** 3. Unhandled context compression exceptions in main loop (run_agent.py:8204,8262,8338) - Silent crash when summarizer fails during API loop - No graceful degradation **MEDIUM (2):** 4. Error swallowing in auxiliary_client (auxiliary_client.py:2074-2106) - Original error overwritten on retry failure - Lost error context, unreachable fallback logic 5. Session ID change without exception recovery (run_agent.py:6041-6071) - Session state corruption on DB failures - Broken session lineage ## Details Full analysis with code snippets, scenarios, and fixes in: - AUDIT_ITERATION_2.md (400 lines, detailed technical analysis) - AUDIT_ITERATION_2_SUMMARY.txt (visual summary, testing recommendations) ## Recommended Priority 1. Bug NousResearch#1 (Role Violation) - FIX IMMEDIATELY 2. Bug NousResearch#2 (Double Execution) - FIX IMMEDIATELY 3. Bug NousResearch#3 (Unhandled Exceptions) - FIX SOON 4. Bug NousResearch#4 & NousResearch#5 - FIX AFTER critical bugs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tegrity
- Fix token estimation undercount (agent/model_metadata.py):
estimate_tokens_rough() used floor division (len // 4), causing short
texts to estimate as 0 tokens ("yes" → 0). This makes the compressor
and pre-flight checks systematically undercount token usage, especially
when many short tool results are present. Switched to ceiling division
((len + 3) // 4) so every non-empty text estimates at least 1 token.
- Add warning log for silent 128K context length fallback (agent/model_metadata.py):
get_model_context_length() silently returned the 128K default when all
10 detection methods failed, with zero logging. Users with small-context
models (8K, 32K) would silently get 128K, causing API failures that were
impossible to debug. Added a warning log with the model name and a hint
to set context_length in config.yaml.
- Add fsync to batch trajectory writes (batch_runner.py):
Trajectory entries were written without flush/fsync, while the checkpoint
immediately marked them as completed. A crash between write and disk sync
would leave the checkpoint claiming completion with no trajectory data.
Added flush() + fsync() to ensure durability before checkpoint update.
- Add KeyboardInterrupt handling in batch pool execution (batch_runner.py):
Ctrl+C during pool.imap_unordered() relied on context manager cleanup
which can hang for 180+ seconds waiting for workers to join. Added
explicit pool.terminate() + join(timeout=10) for both KeyboardInterrupt
and Exception cases for responsive shutdown.
Co-Authored-By: Claude Code <noreply@anthropic.com>
…rmula Switch estimate_tokens_rough(), estimate_messages_tokens_rough(), and estimate_request_tokens_rough() from floor division (len // 4) to ceiling division ((len + 3) // 4). Short texts (1-3 chars) previously estimated as 0 tokens, causing the compressor and pre-flight checks to systematically undercount when many short tool results are present. Also replaced the inline duplicate formula in run_conversation() (total_chars // 4) with a call to the shared estimate_messages_tokens_rough() function. Updated 4 tests that hardcoded floor-division expected values. Related: issue #6217, PR #6629
…rmula Switch estimate_tokens_rough(), estimate_messages_tokens_rough(), and estimate_request_tokens_rough() from floor division (len // 4) to ceiling division ((len + 3) // 4). Short texts (1-3 chars) previously estimated as 0 tokens, causing the compressor and pre-flight checks to systematically undercount when many short tool results are present. Also replaced the inline duplicate formula in run_conversation() (total_chars // 4) with a call to the shared estimate_messages_tokens_rough() function. Updated 4 tests that hardcoded floor-division expected values. Related: issue NousResearch#6217, PR NousResearch#6629
…rmula Switch estimate_tokens_rough(), estimate_messages_tokens_rough(), and estimate_request_tokens_rough() from floor division (len // 4) to ceiling division ((len + 3) // 4). Short texts (1-3 chars) previously estimated as 0 tokens, causing the compressor and pre-flight checks to systematically undercount when many short tool results are present. Also replaced the inline duplicate formula in run_conversation() (total_chars // 4) with a call to the shared estimate_messages_tokens_rough() function. Updated 4 tests that hardcoded floor-division expected values. Related: issue NousResearch#6217, PR NousResearch#6629
…rmula Switch estimate_tokens_rough(), estimate_messages_tokens_rough(), and estimate_request_tokens_rough() from floor division (len // 4) to ceiling division ((len + 3) // 4). Short texts (1-3 chars) previously estimated as 0 tokens, causing the compressor and pre-flight checks to systematically undercount when many short tool results are present. Also replaced the inline duplicate formula in run_conversation() (total_chars // 4) with a call to the shared estimate_messages_tokens_rough() function. Updated 4 tests that hardcoded floor-division expected values. Related: issue NousResearch#6217, PR NousResearch#6629
…rmula Switch estimate_tokens_rough(), estimate_messages_tokens_rough(), and estimate_request_tokens_rough() from floor division (len // 4) to ceiling division ((len + 3) // 4). Short texts (1-3 chars) previously estimated as 0 tokens, causing the compressor and pre-flight checks to systematically undercount when many short tool results are present. Also replaced the inline duplicate formula in run_conversation() (total_chars // 4) with a call to the shared estimate_messages_tokens_rough() function. Updated 4 tests that hardcoded floor-division expected values. Related: issue NousResearch#6217, PR NousResearch#6629
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the durability and observability investigation. The token-estimation fix has already landed in current main as 5c2ecde (agent/model_metadata.py:2467, with coverage at tests/agent/test_model_metadata.py:51-53), while the final fallback and batch durability concerns still merit focused work.
Problems
batch_runner.py:948and:953callpool.join(timeout=10), but CPython'smultiprocessing.pool.Pool.joinaccepts onlyself; both proposed exception paths would raiseTypeError.agent/model_metadata.py:971says to setcontext_length, whereas current resolver guidance usesmodel.context_length(agent/model_metadata.py:2232-2236).AUDIT_ITERATION_2.mdandAUDIT_ITERATION_2_SUMMARY.txtadd unrelated audit claims without implementing their listed gateway/compression fixes.- No tests cover the new fsync, fallback-log, or interrupt-cleanup paths.
Suggested changes
- Isolate the still-relevant fallback diagnostic and trajectory durability work; remove the landed token patch and audit documents.
- Use a valid Pool cleanup strategy and add regression coverage for interruption and checkpoint/output ordering.
Automated hermes-sweeper review.
| except KeyboardInterrupt: | ||
| print("\n⚠️ Interrupted — terminating batch workers...") | ||
| pool.terminate() | ||
| pool.join(timeout=10) |
There was a problem hiding this comment.
multiprocessing.pool.Pool.join() has no timeout parameter (CPython defines join(self)), so this KeyboardInterrupt cleanup path raises TypeError after termination instead of preserving the interrupt.
| # 10. Default fallback — 128K | ||
| logger.warning( | ||
| "Could not determine context length for model %s (base_url=%s) " | ||
| "— falling back to %d tokens. Set context_length in config.yaml " |
There was a problem hiding this comment.
The configuration hint should name the actual key: model.context_length in config.yaml, matching the current resolver guidance.
| @@ -0,0 +1,224 @@ | |||
| ================================================================================ | |||
There was a problem hiding this comment.
This audit adds unrelated gateway security findings, but this PR does not implement those gateway changes. Please split or remove it so the patch remains scoped to the token/context/batch work.
|
Thanks for the investigation @aaronlab — the three concerns you identified are legitimate. Addressing the review feedback:
I've salvaged the two still-relevant concerns (fallback diagnostic + batch durability) with the review issues fixed in #76027:
Your co-authorship is preserved in the commit. Closing this in favor of #76027. Regarding #6622 — it does not supersede this PR. They touch completely different files and concerns:
Both PRs share the same audit documents but their code changes are independent. |
…ol cleanup Salvage of NousResearch#6629 by aaronlab (kshitijk4poor reworked against current main). Three concerns from the original PR, reworked to address review feedback: 1. Context-length fallback diagnostic (agent/model_metadata.py): get_model_context_length() silently returned 256K when all 9 detection methods failed. Users with small-context models (8K, 32K) would get 256K silently, causing hard-to-debug API context-length errors. Added a warning log at the step 9 fallback with model name, base_url, and the correct config override hint (model.context_length, not context_length). The token-estimation ceiling-division fix from the original PR already landed on main (5c2ecde) with CJK handling — not duplicated here. 2. Fsync for batch trajectory writes (batch_runner.py): Trajectory entries were written without flush/fsync, but the checkpoint immediately marked them as completed. A crash between write and disk sync would leave the checkpoint claiming completion with no trajectory data on disk. Added flush() + os.fsync() before checkpoint update. 3. Pool cleanup on interruption (batch_runner.py): Ctrl+C during pool.imap_unordered() relied on context manager cleanup which can hang. Added explicit pool.terminate() + pool.join() for both KeyboardInterrupt and Exception paths. The original PR used pool.join(timeout=10) which is invalid — CPython's Pool.join() takes no timeout parameter. Fixed to use pool.join() without arguments. Tests: - test_warning_emitted_on_fallback: verifies warning fires at step 9 - test_no_warning_when_cached: verifies no false warning when cache hits - test_trajectory_entry_is_synced_to_disk: verifies os.fsync is called - test_pool_terminate_called_on_exception: verifies cleanup on RuntimeError - test_pool_terminate_called_on_keyboard_interrupt: verifies cleanup on Ctrl+C - test_pool_join_called_without_timeout: verifies no timeout arg to join() - test_real_pool_join_accepts_no_timeout: integration check on CPython API Co-authored-by: Aaron Lab <aaronlab@users.noreply.github.com>
…ol cleanup Salvage of #6629 by aaronlab (kshitijk4poor reworked against current main). Three concerns from the original PR, reworked to address review feedback: 1. Context-length fallback diagnostic (agent/model_metadata.py): get_model_context_length() silently returned 256K when all 9 detection methods failed. Users with small-context models (8K, 32K) would get 256K silently, causing hard-to-debug API context-length errors. Added a warning log at the step 9 fallback with model name, base_url, and the correct config override hint (model.context_length, not context_length). The token-estimation ceiling-division fix from the original PR already landed on main (5c2ecde) with CJK handling — not duplicated here. 2. Fsync for batch trajectory writes (batch_runner.py): Trajectory entries were written without flush/fsync, but the checkpoint immediately marked them as completed. A crash between write and disk sync would leave the checkpoint claiming completion with no trajectory data on disk. Added flush() + os.fsync() before checkpoint update. 3. Pool cleanup on interruption (batch_runner.py): Ctrl+C during pool.imap_unordered() relied on context manager cleanup which can hang. Added explicit pool.terminate() + pool.join() for both KeyboardInterrupt and Exception paths. The original PR used pool.join(timeout=10) which is invalid — CPython's Pool.join() takes no timeout parameter. Fixed to use pool.join() without arguments. Tests: - test_warning_emitted_on_fallback: verifies warning fires at step 9 - test_no_warning_when_cached: verifies no false warning when cache hits - test_trajectory_entry_is_synced_to_disk: verifies os.fsync is called - test_pool_terminate_called_on_exception: verifies cleanup on RuntimeError - test_pool_terminate_called_on_keyboard_interrupt: verifies cleanup on Ctrl+C - test_pool_join_called_without_timeout: verifies no timeout arg to join() - test_real_pool_join_accepts_no_timeout: integration check on CPython API Co-authored-by: Aaron Lab <aaronlab@users.noreply.github.com>
|
Merged via #76027 (rebase) — the still-relevant parts of this PR are now on main:
Your investigation and authorship are preserved — commit a1ff62a on main carries your Co-authored-by credit. Thanks @aaronlab! |
…ol cleanup Salvage of NousResearch#6629 by aaronlab (kshitijk4poor reworked against current main). Three concerns from the original PR, reworked to address review feedback: 1. Context-length fallback diagnostic (agent/model_metadata.py): get_model_context_length() silently returned 256K when all 9 detection methods failed. Users with small-context models (8K, 32K) would get 256K silently, causing hard-to-debug API context-length errors. Added a warning log at the step 9 fallback with model name, base_url, and the correct config override hint (model.context_length, not context_length). The token-estimation ceiling-division fix from the original PR already landed on main (938c90a) with CJK handling — not duplicated here. 2. Fsync for batch trajectory writes (batch_runner.py): Trajectory entries were written without flush/fsync, but the checkpoint immediately marked them as completed. A crash between write and disk sync would leave the checkpoint claiming completion with no trajectory data on disk. Added flush() + os.fsync() before checkpoint update. 3. Pool cleanup on interruption (batch_runner.py): Ctrl+C during pool.imap_unordered() relied on context manager cleanup which can hang. Added explicit pool.terminate() + pool.join() for both KeyboardInterrupt and Exception paths. The original PR used pool.join(timeout=10) which is invalid — CPython's Pool.join() takes no timeout parameter. Fixed to use pool.join() without arguments. Tests: - test_warning_emitted_on_fallback: verifies warning fires at step 9 - test_no_warning_when_cached: verifies no false warning when cache hits - test_trajectory_entry_is_synced_to_disk: verifies os.fsync is called - test_pool_terminate_called_on_exception: verifies cleanup on RuntimeError - test_pool_terminate_called_on_keyboard_interrupt: verifies cleanup on Ctrl+C - test_pool_join_called_without_timeout: verifies no timeout arg to join() - test_real_pool_join_accepts_no_timeout: integration check on CPython API Co-authored-by: Aaron Lab <aaronlab@users.noreply.github.com>
Summary
This PR addresses token estimation accuracy, debugging observability, and batch data integrity issues found during iteration #6 of a deep code audit:
Fix token estimation undercount (
agent/model_metadata.py):estimate_tokens_rough()used floor division (len // 4), causing short texts to estimate as 0 tokens (e.g.,"yes"→ 0). This makes the compressor and pre-flight checks systematically undercount, especially with many short tool results. Switched to ceiling division ((len + 3) // 4).Add warning log for silent 128K fallback (
agent/model_metadata.py):get_model_context_length()silently returned 128K when all 10 detection methods failed, with zero logging. Users with small-context models (8K, 32K) would get 128K silently, causing API failures impossible to debug. Added a warning with model name and config hint.Add fsync to batch trajectory writes (
batch_runner.py): Trajectory entries were written withoutflush()/fsync(), but the checkpoint immediately marked them as completed. A crash between write and disk sync would leave the checkpoint claiming completion with no trajectory data on disk.Add KeyboardInterrupt handling in batch pool (
batch_runner.py): Ctrl+C duringpool.imap_unordered()relied on context manager cleanup which can hang 180+ seconds. Added explicitpool.terminate()+join(timeout=10)for responsive shutdown.Files Changed
agent/model_metadata.pybatch_runner.pyTest plan
estimate_tokens_rough("yes")should return 1, not 0estimate_tokens_rough("")should return 0estimate_tokens_rough("hello world")should return 3 (11 chars → ceil(11/4) = 3)--batch_size 2and Ctrl+C → verify responsive shutdownpytest tests/🤖 Generated with Claude Code