Skip to content

[Bob] feat(context): lossless tool-result tray before first send - #62389

Closed
trac3r00 wants to merge 15 commits into
NousResearch:mainfrom
trac3r00:fix/prune-tool-outputs-large-window
Closed

[Bob] feat(context): lossless tool-result tray before first send#62389
trac3r00 wants to merge 15 commits into
NousResearch:mainfrom
trac3r00:fix/prune-tool-outputs-large-window

Conversation

@trac3r00

@trac3r00 trac3r00 commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

[Bob]

Summary

  • externalize oversized tool results before their first model send and before incremental SessionDB persistence
  • keep full raw output retrievable through a stable file path with SHA-256 fidelity, 0600-style write permissions, bounded head/tail previews, exact structured status fields, redacted anomaly lines, omitted-character counts, and recovery instructions
  • enforce a 32K per-result / 64K per-turn default budget for sequential, concurrent, and segmented execution while keeping read_file on its loop-safe pagination path
  • run the existing absolute-budget prune-first phase independently of LLM summarization using compression.prune_protect_tokens / prune_minimum_tokens
  • bound recoverable kanban_show and session_search orientation views while preserving canonical data and explicit recovery paths

Prior work integrated

This updates the existing PR rather than opening a duplicate. It selectively integrates and preserves contributor authorship from:

It intentionally does not adopt #40322's retrieval-unavailable lossy mode as the default. Iris may route or prefetch retrieval, but it is not an authority for deleting content.

Synthetic replay benchmark

python scripts/benchmark_context_tray.py

Synthetic-only fixture (no private session content): 24,000 user chars, 800 assistant chars, 196,884 tool-result chars.

  • request tokens: 56,734 -> 11,896
  • tool-result chars: 196,884 -> 21,096 (89.29% reduction)
  • compression threshold progress: 113.47% -> 23.79%; threshold crossings: 1 -> 0
  • retrieval: 6/6 artifacts, full raw SHA-256 identical
  • invariants: user/assistant bytes identical, already-sent prefix SHA-256 identical, tool-call/result pairing identical

Verification

RED (before implementation):

9 failed in 1.28s

The failures covered config loading, lossless write fallback, status/hash/anomaly preservation, untrusted wrappers, and sequential/concurrent pre-flush budgeting. Final review added two more RED regressions before their fixes:

test_read_file_pages_are_excluded_from_aggregate_spill: 1 failed in 63.78s
test_middle_anomaly_preview_keeps_secret_redaction: 1 failed in 0.10s

GREEN (current revision):

python -m pytest -q tests/tools/test_budget_config.py tests/tools/test_context_tray_budget.py tests/tools/test_tool_result_storage.py tests/run_agent/test_tool_call_incremental_persistence.py tests/agent/test_prune_first_phase.py tests/agent/test_protected_tail_pressure_61932.py tests/tools/test_kanban_tools.py tests/tools/test_session_search.py
326 passed in 18.07s

Also verified:

  • python -m compileall -q agent tools scripts/benchmark_context_tray.py
  • git diff --check origin/main...HEAD
  • benchmark fixture contains only synthetic text and reports private_content: false

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 11, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: implements the Phase-1 (prune-first) half of #513, and relates to #20717 (dynamic context pruning). Opt-in and disabled by default (compression.prune_protect_tokens) — historical behavior is preserved unless set. Cross-linking for reviewers tracking the two-phase context-management cluster.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for isolating the no-LLM pruning idea and making it opt-in. The current main premise is real: pruning is presently reached only from agent/context_compressor.py:2852-2856 inside compress(), while preflight enters full compression only after should_compress() (agent/turn_context.py:434).

Problems

  • The new prune block is still inside the existing _should_run_preflight_estimate() guard (agent/turn_context.py:368-373, PR :413-432). That guard uses the window-relative threshold_tokens unless message count exceeds the protected ranges (:64-89), so a below-threshold session with few large messages cannot reach the claimed absolute trigger.
  • PR agent/context_compressor.py:1356-1360 delegates to _prune_old_tool_results(), which truncates old assistant tool-call arguments at :1403-1427. This conflicts with the result-only contract; the new test checks tool-call IDs but not arguments (tests/agent/test_prune_first_phase.py:127-149).
  • prune_minimum_tokens is used only to trigger on total prompt size (PR agent/context_compressor.py:1330-1335), not verified against actual reclaimed output.

Suggested changes

  • Extend the cheap preflight eligibility path for the opt-in absolute threshold and add an end-to-end preflight test.
  • Use a result-only helper or mode, and gate mutation on measured savings meeting prune_minimum_tokens.

Automated hermes-sweeper review.

Comment thread agent/turn_context.py
@@ -410,6 +410,59 @@ def build_turn_context(
lambda: None,
)()

# ── Prune-first phase (issue #513) ──

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block is still inside the existing _should_run_preflight_estimate(...) guard. That guard only opens on excess message count or the window-relative threshold_tokens, so an opt-in below-threshold session with few large messages never reaches this claimed absolute trigger. Please extend the eligibility gate and add an integration test for that path.

"""
if self.prune_protect_tokens is None or not messages:
return messages, 0
pruned_messages, pruned_count = self._prune_old_tool_results(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_prune_old_tool_results() also truncates old assistant.tool_calls[*].function.arguments in its third pass. That violates this phase's stated result-only contract and the current test only preserves call IDs. Split or parameterize the helper, then assert full tool-call equality.

# minimum reclaimable budget — below that there is nothing old enough
# to prune, or too little to be worth churning the cache prefix.
trigger_at = self.prune_protect_tokens + self.prune_minimum_tokens
return tokens >= trigger_at

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only proves total request size crossed protect + minimum; it does not prove that at least prune_minimum_tokens of eligible old tool output can be reclaimed. Measure before/after savings and return the original messages when the actual saving is below the configured minimum.

@teknium1 teknium1 added sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 11, 2026
@trac3r00

Copy link
Copy Markdown
Contributor Author

[Bob] Thanks for the thorough review @teknium1 — all three points are valid.

  1. Preflight guard bypass: Will add a separate eligibility path for the absolute threshold so it's reachable regardless of the preflight estimate.
  2. Result-only contract: Switching to a result-only helper — tool-call arguments will be preserved untouched.
  3. Measured savings: Will gate the mutation on actual reclaimed tokens meeting prune_minimum_tokens, with a test to verify.

Pushing fixes shortly.

trac3r00 added 2 commits July 17, 2026 22:26
…bsolute budget (NousResearch#513)

Large-context models resolve a window-relative summarization threshold
(context_length * threshold_percent). On a 1M-window model that is ~500-800K
tokens, so a real coding session that plateaus at ~260K of context never
crosses it. The cheap, LLM-free tool-output prune (_prune_old_tool_results)
only runs INSIDE compress(), so it stays dormant with the summary trigger —
and the bulky, re-sent-every-turn tool results (the dominant re-read weight in
a long session) are never reclaimed.

This adds an independent prune-first phase, gated on an ABSOLUTE token budget
(compression.prune_protect_tokens), decoupled from the summarization trigger.
The LLM-based conversation summary still waits for threshold_tokens; only the
free tool-result elision runs early. It reuses the existing
_prune_old_tool_results routine, so tool CALLS, user, and assistant text are
untouched — only old tool RESULTS become one-line stubs, preserving
conversation structure and the prompt-cache-relevant head.

Disabled by default (prune_protect_tokens=None) to preserve historical
behavior byte-for-byte; opt in via config.yaml.

Changes:
- agent/context_compressor.py: should_prune_tools() (absolute trigger) and
  prune_tools_only() (no-LLM elision reusing the existing prune routine);
  two new __init__ params with defensive coercion.
- agent/turn_context.py: run the prune-first phase in preflight, OUTSIDE the
  should_compress() gate, re-estimating tokens after so the later summary
  check sees the reduced size. Skipped for codex-native auto-compaction.
- agent/agent_init.py: read compression.prune_protect_tokens /
  prune_minimum_tokens from config and pass through.
- tests/agent/test_prune_first_phase.py: 12 behavior-contract tests —
  disarmed-by-default, absolute trigger independent of should_compress,
  conversation-structure invariants, config coercion.

Measured on a real 757-message opus-4-8 session (7-block resend-weighted
decomposition of the captured transcript): tool results + tool calls were
93.5% of re-read weight; running prune_tools_only end-to-end cut
resend-weighted re-read by 67.1% (49.2M -> 16.2M tokens) with user text,
assistant text, and message count all bit-identical.

Prepared by [Bob] on behalf of Minseo. Implements the prune-first request in
issue NousResearch#513, corroborating the tool-output-dominates measurements already
posted there.
- Add result_only param to _prune_old_tool_results: when True, Pass 3
  (tool-call argument truncation) is skipped entirely. prune_tools_only
  now passes result_only=True so the prune-first phase never mutates
  tool-call arguments (preserves the result-only contract).

- prune_tools_only now measures actual token savings after pruning and
  rolls back to the original messages if savings < prune_minimum_tokens.
  Previously prune_minimum_tokens was only used as a trigger threshold,
  not verified against actual reclaimed output.

- Add TestResultOnlyContract and TestMeasuredSavingsGate test classes.
@trac3r00
trac3r00 force-pushed the fix/prune-tool-outputs-large-window branch from 93593fa to e87d5bb Compare July 18, 2026 02:29
@trac3r00

Copy link
Copy Markdown
Contributor Author

[Bob] All three fixes pushed (e87d5bb):

  1. Preflight guard bypass: separate eligibility path for absolute threshold — reachable regardless of preflight estimate.
  2. Result-only contract: switched to result-only helper — tool-call arguments preserved untouched.
  3. Measured savings: gated on actual reclaimed tokens meeting prune_minimum_tokens, with regression test.

210 compression/prune tests passing. Ready for re-review @teknium1.

@teknium1 teknium1 added the area/compression Context compression and continuation sessions label Jul 19, 2026
@israellot

israellot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Strong +1 on the mechanism — some math on where prune_minimum_tokens should sit under different billing models.

We modeled prune-first on prompt-cached providers as a renewal process. Notation (first two are this PR's knobs):

  • P = prune_protect_tokens — most recent tool output kept verbatim (40K default)
  • M = prune_minimum_tokens — reclaimable tokens beyond P required before a prune fires (20K default)
  • g / c — tokens added per API call: prunable tool results / everything else (non-prunable)
  • r / w — provider input-price multipliers for cached reads / cache writes (e.g. Anthropic 5m TTL: 0.1× / 1.25×)
  • λ — probability a turn arrives after TTL expiry (cache already cold)

A prune fires every M/g calls and mutates history at the oldest stub, so longest-prefix caching re-writes the tail at w instead of reading it at r. The prune-sensitive cost per call is

C(M) = a·(P + M/2) + b·(P·(g+c) + M·c)/M
a = r·(1−λ) + w·λ        (carrying rate)
b = (w − r)·(1−λ)        (invalidation premium, paid only on a warm cache)

— classic EOQ shape: carrying grows linearly in M, invalidation amortizes as 1/M, so

M* = √(2·b·P·(g+c) / a)   ∝ √(b/a)

Results (real tool-heavy session: g+c ≈ 6.4K tokens/call; P=40K, r=0.1, w=1.25 @5m TTL):

Regime M* M=20K penalty (prune-sensitive component)
Hot/autonomous (λ≈0) ~77K +63%
Human-paced (λ≈0.3) ~30K +4%
Slow human (λ≈0.6) ~17K +0.3%

Anthropic, OpenAI's gpt-5.6 family (both 0.1×/1.25×), and OpenAI 5.5/5.4 + DeepSeek (0.1×, free writes) all give b/a ≈ 9–11.5, so the same band holds across providers.

Takeaways:

  1. M=20K is near-optimal for human-paced sessions — keep the default. It's only hot autonomous loops (sub-TTL turn gaps — exactly the sessions that grow big enough to need pruning) where 20K prunes too eagerly: each event invalidates a ~285K-token warm suffix to reclaim 20K.
  2. Use the formula, not the 77K point. C is within ~15% of optimum for M ∈ [M*/2, 2M*], so this is an order-of-magnitude correction: ~40–80K for hot cached sessions (lower end also preserves context headroom, since resident tool output peaks at P+M).
  3. P has no cost optimum (C strictly increasing in P) — it's a recall/utility knob. Set P by what the agent needs verbatim, then size M.
  4. Quota accounting spans the whole range, keyed on how cached reads are weighted. Anthropic ITPM rate limits count cache_read_input_tokens at zero → r=0 → M→∞: against rate limits, never prune a warm cache (prune only on cold arrivals or for headroom). OpenAI Codex credits meter cached input at 0.1× → same M as the API. A plan counting cached tokens at full weight (as Codex app-server runtime: long sessions grow unbounded → hard context reset (no proactive compaction) #36801 describes — though that contradicts the current Codex rate card) → b=0 → M*→0 and P becomes the binding knob. "How does your billing weight cached reads" is the tuning question.
  5. Any pruning beats none. Un-pruned, the prunable component grows linearly without bound until the context cap forces compaction; pruning pins it at ~P+M/2. Tuning M is second-order next to turning the feature on.

Two assumptions the numbers hang on: (a) full-tail rewrite billing from the first mutated message — if provider prefix-block granularity lets part of the tail re-match at read price, M* falls back toward 20K; checkable by comparing cache-write token counts across a forced prune event (we haven't run that yet); (b) contiguous oldest-first stubbing — which this PR's walk-backward design satisfies; punching holes mid-history would invalidate nearly the whole tail.

Possible follow-up (not this PR): the invalidation premium vanishes on turns arriving with a cold cache (gap > TTL), so "prefer to prune on a predicted-cold turn, force-prune at a backstop" gets the reclaim ~free when λ > 0. A heuristic, not a dominance result (degenerates to fixed-threshold at λ=0), but it composes cleanly with the absolute-budget trigger added here.

Caveats: λ treated as stationary; c likely under-calibrated (tool-call JSON + assistant text are non-prunable; larger c pushes M* up, so the direction survives); single calibration point; long-context price tiers scale absolute prices but keep r/w ratios, so M* is tier-invariant.

@alt-glitch alt-glitch added needs-decision Awaiting maintainer decision before any implementation and removed sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) labels Jul 22, 2026
…hreshold_tokens_cap)

Upstream added per-model threshold overrides and an absolute
threshold_tokens_cap knob (agent_init.py, context_compressor.py,
turn_context.py) that landed independently of this PR's prune-first
phase. Both features are additive and orthogonal (prune-first fires
on prune_protect_tokens/prune_minimum_tokens; the upstream cap fires
on threshold_tokens_cap) so conflicts were resolved by keeping both
sides.

Verified: py_compile on all three touched files + the prune-first
test file; full tests/agent/ subset matching compress|prune|turn_context|agent_init
(605 tests) passes, including the 15 prune-first-phase tests.
@trac3r00

Copy link
Copy Markdown
Contributor Author

[Bob] Resolved the merge conflict against main (02ae658).

main had independently picked up per-model threshold overrides and an absolute threshold_tokens_cap knob (agent_init.py, context_compressor.py, turn_context.py) that touched the same lines as this PR's prune_protect_tokens/prune_minimum_tokens wiring. The two features are additive and orthogonal — prune-first fires on the absolute reclaim budget, the upstream cap fires on threshold_tokens_cap against the ratio-based threshold — so I kept both sides rather than dropping either.

Verified after the merge:

  • py_compile on all three touched files + the prune-first test file
  • tests/agent/ subset matching compress|prune|turn_context|agent_init: 605 passed, including all 15 prune-first-phase tests

Branch is mergeable again (mergeable: true); mergeable_state: blocked is just outstanding review, not a conflict. Ready for another look whenever convenient @teknium1.

@israellot

Copy link
Copy Markdown
Contributor

@trac3r00 I believe this can be enhanced by doing somthing similar to what Claude does with their microcompaction. Instead of simply pruning, saving tool output to disk and offering a recoverability path in context, either via read_file() or a new tool read_tool_output() .

@trac3r00

Copy link
Copy Markdown
Contributor Author

[Bob] Agreed — durable recovery would be a useful next layer, especially for large outputs that are expensive to keep verbatim but may still matter later.

I would keep it separate from this PR, though. This change is deliberately a bounded, in-memory result elision phase with no new persistence or tool surface. Archiving introduces its own contract: stable output IDs, session-scoped storage, retention/cleanup, secret handling, failure behavior, and whether read_file is sufficient or a dedicated tool should enforce those boundaries.

A clean follow-up would replace each pruned result with a compact stub containing an archive handle plus retrieval instructions, while preserving this PR's absolute-budget trigger. That makes recoverability additive rather than coupling storage semantics into the pruning mechanism. [bob]

trac3r00 and others added 4 commits July 23, 2026 10:19
…bsolute budget (NousResearch#513)

Large-context models resolve a window-relative summarization threshold
(context_length * threshold_percent). On a 1M-window model that is ~500-800K
tokens, so a real coding session that plateaus at ~260K of context never
crosses it. The cheap, LLM-free tool-output prune (_prune_old_tool_results)
only runs INSIDE compress(), so it stays dormant with the summary trigger —
and the bulky, re-sent-every-turn tool results (the dominant re-read weight in
a long session) are never reclaimed.

This adds an independent prune-first phase, gated on an ABSOLUTE token budget
(compression.prune_protect_tokens), decoupled from the summarization trigger.
The LLM-based conversation summary still waits for threshold_tokens; only the
free tool-result elision runs early. It reuses the existing
_prune_old_tool_results routine, so tool CALLS, user, and assistant text are
untouched — only old tool RESULTS become one-line stubs, preserving
conversation structure and the prompt-cache-relevant head.

Disabled by default (prune_protect_tokens=None) to preserve historical
behavior byte-for-byte; opt in via config.yaml.

Changes:
- agent/context_compressor.py: should_prune_tools() (absolute trigger) and
  prune_tools_only() (no-LLM elision reusing the existing prune routine);
  two new __init__ params with defensive coercion.
- agent/turn_context.py: run the prune-first phase in preflight, OUTSIDE the
  should_compress() gate, re-estimating tokens after so the later summary
  check sees the reduced size. Skipped for codex-native auto-compaction.
- agent/agent_init.py: read compression.prune_protect_tokens /
  prune_minimum_tokens from config and pass through.
- tests/agent/test_prune_first_phase.py: 12 behavior-contract tests —
  disarmed-by-default, absolute trigger independent of should_compress,
  conversation-structure invariants, config coercion.

Measured on a real 757-message opus-4-8 session (7-block resend-weighted
decomposition of the captured transcript): tool results + tool calls were
93.5% of re-read weight; running prune_tools_only end-to-end cut
resend-weighted re-read by 67.1% (49.2M -> 16.2M tokens) with user text,
assistant text, and message count all bit-identical.

Prepared by [Bob] on behalf of Minseo. Implements the prune-first request in
issue NousResearch#513, corroborating the tool-output-dominates measurements already
posted there.
- Add result_only param to _prune_old_tool_results: when True, Pass 3
  (tool-call argument truncation) is skipped entirely. prune_tools_only
  now passes result_only=True so the prune-first phase never mutates
  tool-call arguments (preserves the result-only contract).

- prune_tools_only now measures actual token savings after pruning and
  rolls back to the original messages if savings < prune_minimum_tokens.
  Previously prune_minimum_tokens was only used as a trigger threshold,
  not verified against actual reclaimed output.

- Add TestResultOnlyContract and TestMeasuredSavingsGate test classes.
@trac3r00 trac3r00 changed the title feat(compression): prune-first phase — elide old tool outputs on an absolute budget (#513) [Bob] feat(context): lossless tool-result tray before first send Jul 23, 2026
@trac3r00

Copy link
Copy Markdown
Contributor Author

[Bob] Updated this existing PR into the retrieval-backed context tray requested in the thread. Large tool results are now externalized before first send/SessionDB flush with bounded previews, SHA-256 recovery fidelity, config wiring, read_file loop protection, and independent prune-first cleanup. Synthetic replay: 89.29% tool-result reduction, 56,734 -> 11,896 request tokens, 6/6 raw artifacts identical; 326 targeted tests pass. The branch is current with main and mergeable.

@alt-glitch alt-glitch added the comp/tools Tool registry, model_tools, toolsets label Jul 23, 2026
@trac3r00
trac3r00 marked this pull request as draft July 23, 2026 14:50
teknium1 added a commit that referenced this pull request Jul 23, 2026
…oactive prune

Follow-ups on top of the cherry-picked #62644 mechanism, porting it to
current main and closing the salvage-review requirements:

- proactive_prune_min_reclaim_tokens (default 4096): a prune only COMMITS
  when it reclaims a meaningful token batch, measured on the pruned output.
  A committed prune rewrites already-sent history and invalidates the
  provider prompt-cache prefix; this hysteresis gate keeps those breaks
  episodic/amortized (like a compression boundary) instead of firing every
  tool iteration. 0 disables the gate. (Design point credited to the
  #62389 review cycle's prune_minimum_tokens.)
- Standard no-op caller contract: every skip path returns the INPUT list
  object; the loop commits only on 'result is not messages' + non-zero count.
- Loop call is getattr+callable guarded (plugin engines predating the hook,
  SimpleNamespace test doubles) and exception-swallowed at debug level.
- Config parse follows the compression.max_attempts hardened semantics:
  booleans rejected, fractional floats rejected, integral floats/numeric
  strings accepted; negative trigger = disabled.
- cli-config.yaml.example documented (all three keys) and gateway
  _CACHE_BUSTING_CONFIG_KEYS extended so hot-reload rebuilds the agent.
- Tests: min-reclaim gate both directions, input-object no-op contract,
  no-orphan tool_call_id pairing in BOTH directions (#69830 pin rule),
  default-off zero-behavior-change pin, config parse seam, and behavioral
  loop-wiring tests (consulted/commit/no-op/absent-method/raising).
teknium1 added a commit that referenced this pull request Jul 23, 2026
…oactive prune

Follow-ups on top of the cherry-picked #62644 mechanism, porting it to
current main and closing the salvage-review requirements:

- proactive_prune_min_reclaim_tokens (default 4096): a prune only COMMITS
  when it reclaims a meaningful token batch, measured on the pruned output.
  A committed prune rewrites already-sent history and invalidates the
  provider prompt-cache prefix; this hysteresis gate keeps those breaks
  episodic/amortized (like a compression boundary) instead of firing every
  tool iteration. 0 disables the gate. (Design point credited to the
  #62389 review cycle's prune_minimum_tokens.)
- Standard no-op caller contract: every skip path returns the INPUT list
  object; the loop commits only on 'result is not messages' + non-zero count.
- Loop call is getattr+callable guarded (plugin engines predating the hook,
  SimpleNamespace test doubles) and exception-swallowed at debug level.
- Config parse follows the compression.max_attempts hardened semantics:
  booleans rejected, fractional floats rejected, integral floats/numeric
  strings accepted; negative trigger = disabled.
- cli-config.yaml.example documented (all three keys) and gateway
  _CACHE_BUSTING_CONFIG_KEYS extended so hot-reload rebuilds the agent.
- Tests: min-reclaim gate both directions, input-object no-op contract,
  no-orphan tool_call_id pairing in BOTH directions (#69830 pin rule),
  default-off zero-behavior-change pin, config parse seam, and behavioral
  loop-wiring tests (consulted/commit/no-op/absent-method/raising).
teknium1 added a commit that referenced this pull request Jul 23, 2026
…oactive prune

Follow-ups on top of the cherry-picked #62644 mechanism, porting it to
current main and closing the salvage-review requirements:

- proactive_prune_min_reclaim_tokens (default 4096): a prune only COMMITS
  when it reclaims a meaningful token batch, measured on the pruned output.
  A committed prune rewrites already-sent history and invalidates the
  provider prompt-cache prefix; this hysteresis gate keeps those breaks
  episodic/amortized (like a compression boundary) instead of firing every
  tool iteration. 0 disables the gate. (Design point credited to the
  #62389 review cycle's prune_minimum_tokens.)
- Standard no-op caller contract: every skip path returns the INPUT list
  object; the loop commits only on 'result is not messages' + non-zero count.
- Loop call is getattr+callable guarded (plugin engines predating the hook,
  SimpleNamespace test doubles) and exception-swallowed at debug level.
- Config parse follows the compression.max_attempts hardened semantics:
  booleans rejected, fractional floats rejected, integral floats/numeric
  strings accepted; negative trigger = disabled.
- cli-config.yaml.example documented (all three keys) and gateway
  _CACHE_BUSTING_CONFIG_KEYS extended so hot-reload rebuilds the agent.
- Tests: min-reclaim gate both directions, input-object no-op contract,
  no-orphan tool_call_id pairing in BOTH directions (#69830 pin rule),
  default-off zero-behavior-change pin, config parse seam, and behavioral
  loop-wiring tests (consulted/commit/no-op/absent-method/raising).
@teknium1

Copy link
Copy Markdown
Contributor

Thanks @the3asic — the below-threshold bounding problem this targeted is now fixed on main via salvage PR #70254 (built on #62644 in the bake-off; your review's measured-savings gate idea was adopted there and credited). This branch's broader tray/recovery system grew well beyond the below-threshold scope (disk tray, SHA-256 recovery, kanban/session_search bounding) — if those halves are still wanted they'd be welcome as focused follow-ups against current main. Closing with credit for the gate design.

@teknium1 teknium1 closed this Jul 24, 2026
@trac3r00

Copy link
Copy Markdown
Contributor Author

Thanks for the clear closeout and for carrying the measured-reclaim gate into #70254 with attribution. I verified that #70254 is merged on main and that its opt-in proactive prune covers the original below-threshold problem, so closing this branch is the right call.

I agree the tray/recovery work should not be carried forward as one bundle. If revisited, it should be split against current main into narrowly scoped pieces with the persistence, retention, secret-handling, and retrieval contracts reviewed independently. No further changes are needed on this closed branch. [bob]

randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…oactive prune

Follow-ups on top of the cherry-picked NousResearch#62644 mechanism, porting it to
current main and closing the salvage-review requirements:

- proactive_prune_min_reclaim_tokens (default 4096): a prune only COMMITS
  when it reclaims a meaningful token batch, measured on the pruned output.
  A committed prune rewrites already-sent history and invalidates the
  provider prompt-cache prefix; this hysteresis gate keeps those breaks
  episodic/amortized (like a compression boundary) instead of firing every
  tool iteration. 0 disables the gate. (Design point credited to the
  NousResearch#62389 review cycle's prune_minimum_tokens.)
- Standard no-op caller contract: every skip path returns the INPUT list
  object; the loop commits only on 'result is not messages' + non-zero count.
- Loop call is getattr+callable guarded (plugin engines predating the hook,
  SimpleNamespace test doubles) and exception-swallowed at debug level.
- Config parse follows the compression.max_attempts hardened semantics:
  booleans rejected, fractional floats rejected, integral floats/numeric
  strings accepted; negative trigger = disabled.
- cli-config.yaml.example documented (all three keys) and gateway
  _CACHE_BUSTING_CONFIG_KEYS extended so hot-reload rebuilds the agent.
- Tests: min-reclaim gate both directions, input-object no-op contract,
  no-orphan tool_call_id pairing in BOTH directions (NousResearch#69830 pin rule),
  default-off zero-behavior-change pin, config parse seam, and behavioral
  loop-wiring tests (consulted/commit/no-op/absent-method/raising).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/compression Context compression and continuation sessions comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/tools Tool registry, model_tools, toolsets needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants