Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
f9daa4a
fix(deps): declare setuptools in dev extra for packaging tests (#34851)
teknium1 May 30, 2026
2062a84
fix(auxiliary): stop capping output with max_tokens by default (#3453…
teknium1 May 30, 2026
54aa4db
fix(cli): remove Hermes-managed node/npm/npx symlinks on uninstall
Bartok9 May 29, 2026
bcc8301
Inspired by Claude Code: /compress here [N] — boundary-aware 'summari…
teknium1 May 30, 2026
04de307
fix(cli): repaint input area after inline /steer and /model submit (#…
teknium1 May 30, 2026
e38b0b5
fix(compression): avoid repeat preflight compaction from rough estimates
helix4u May 29, 2026
9dbc372
test(compression): fix StopIteration in large-rough-growth preflight …
teknium1 May 30, 2026
45bc65a
fix(gateway): drop outbound silence-narration messages pre-send
Bartok9 May 29, 2026
2259c15
fix(gateway): clarify status session usage label
helix4u May 29, 2026
9d4c811
fix(gateway): name what the /status token number actually is
teknium1 May 30, 2026
897f953
fix: keep CLI context display in sync with preflight token estimate (…
teknium1 May 30, 2026
59b0ea9
fix(agent): explain abnormal turn endings instead of blank/partial reply
Bartok9 May 29, 2026
de6d602
test(run_agent): align test_dict_tool_call_args with explainer suffix
Bartok9 May 29, 2026
fb0ab27
fix(agent): register explainer config key + shorten footer prefix
teknium1 May 29, 2026
860cf28
docs: clarify compression threshold is derived from the main model's …
teknium1 May 30, 2026
5ad2b4c
fix(session): degrade gracefully when SQLite lacks FTS5
LeonSGP43 Apr 16, 2026
97ecfa0
fix(session): extend no-FTS5 degradation to the trigram CJK index
teknium1 May 30, 2026
4fa20f9
fix(install): ensure the uv-managed Python ships SQLite FTS5
teknium1 May 30, 2026
a7421dc
fix(session): point no-FTS5 warning at the supported install
teknium1 May 30, 2026
aa32edc
fix(setup): write config for image_gen and video_gen in apply_nous_ma…
alt-glitch May 30, 2026
827ce60
fix(honcho): harden self-hosted setup paths
erosika May 30, 2026
0437137
security: pin patched Starlette (>=1.0.1) for CVE-2026-48710 BadHost …
teknium1 May 30, 2026
7b09150
test: remove low-value model-catalog mirror tests
kshitijk4poor May 30, 2026
5a72e82
feat(tui): nudge toward /agents dashboard when delegation starts
kshitijk4poor May 29, 2026
8738cb9
Merge pull request #34704 from kshitijk4poor/feat/tui-agents-nudge
kshitijk4poor May 30, 2026
c1b2d09
fix(cli): don't treat any container as the Docker image for updates (…
teknium1 May 30, 2026
636ff63
fix(agent): strip schema-foreign keys from max-iterations summary req…
maxmilian May 29, 2026
e8076c1
fix(dashboard): allow chat websockets on insecure public bind
SeaXen May 27, 2026
17103a1
chore: add SeaXen to AUTHOR_MAP for salvaged PR #33278
teknium1 May 30, 2026
84ee80e
feat: set process title to 'hermes' in ps/top/htop
wenchengxucool May 30, 2026
e5765e6
chore(release): map wei.chen.coder@gmail.com -> wenchengxucool
teknium1 May 30, 2026
bb79bcd
fix: detect pyproject.toml / __init__.py version drift in hermes doct…
teknium1 May 30, 2026
9d2571c
fix: surface /agents nudge while delegate_task is in-flight (TUI + CLI)
kshitijk4poor May 30, 2026
e481b15
Merge pull request #35216 from kshitijk4poor/fix/agents-nudge-single-…
kshitijk4poor May 30, 2026
b4cf114
fix(vision): fail fast on non-retryable image download errors (#35221)
teknium1 May 30, 2026
64998fa
fix(tui): use base64 encoding for PowerShell clipboard writes to pres…
annguyenNous May 30, 2026
16882cf
refactor(tui): simplify base64 clipboard write to a stdin flag
teknium1 May 30, 2026
c70dca3
fix(kanban): rebuild legacy TEXT-PK tables to INTEGER AUTOINCREMENT o…
teknium1 May 30, 2026
6ab71d3
fix(kanban): prevent infinite retry loop when worker exhausts iterati…
liuhao1024 May 30, 2026
8e5a685
fix(kanban): align recompute_ready guard with breaker's configured fa…
teknium1 May 30, 2026
14517ac
fix(update): export launcher virtualenv to uv
LeonSGP43 May 30, 2026
93e6a05
feat(model-picker): group multi-endpoint providers under one row (#35…
teknium1 May 30, 2026
a57cc00
fix(packaging): include mcp_serve in py-modules so hermes mcp serve w…
liuhao1024 May 29, 2026
83a7d0b
fix(skills): fix transaction ordering in reset_bundled_skill and hand…
annguyenNous May 30, 2026
8ae0802
fix(skills): make _rmtree_writable handle read-only directories, not …
teknium1 May 30, 2026
6a08fd3
test(skills): assert restore via synced[copied], not manifest re-read
teknium1 May 30, 2026
39f6b6e
fix(file-tools): make write_file/patch atomic (temp-file + rename) (#…
teknium1 May 30, 2026
1bdb29d
fix(cli): use `uv tool upgrade` when Hermes is a uv tool install (#29…
briandevans May 21, 2026
bebd4f8
fix(cli): restrict uv-tool-install detection to running interpreter
briandevans May 27, 2026
2334228
fix(update): handle pipx installs + --system fallback in _cmd_update_pip
teknium1 May 30, 2026
4d7ea3f
chore(release): map inchargeautomation-lab author email
teknium1 May 30, 2026
a29d64e
fix(mcp): reap stdio MCP grandchildren via process-group signal
konsisumer May 11, 2026
41decf2
test(mcp): import os and pytest in test_mcp_stability
teknium1 May 30, 2026
460771b
fix(lsp): detect Windows wrapper binaries in installer probes
Sylw3ster May 20, 2026
296fcdf
fix(lsp): handle Windows .cmd shims in LSP process spawn
tuancookiez-hub May 29, 2026
c9e31a8
chore(release): map tuancookiez-hub for #34865 salvage
teknium1 May 30, 2026
794519c
fix(state): persist mid-session model switch to database
LengR May 30, 2026
e1945ff
test(state): cover update_session_model overwrite + getattr-guard tex…
teknium1 May 30, 2026
6baf001
fix(run_agent): gate concurrent checkpoint preflight on block_result …
beardthelion May 29, 2026
8bd0060
fix(google-workspace): handle Gmail header casing case-insensitively
donovan-yohan May 30, 2026
2475244
fix(update/windows): robustly exclude launcher-shim ancestors from co…
teknium1 May 30, 2026
40fcb96
fix(auxiliary): pass base_url/api_key/api_mode through set_runtime_ma…
liuhao1024 May 29, 2026
622e534
test(auxiliary): e2e routing assertions for custom-provider aux resol…
teknium1 May 30, 2026
d3724c0
fix(tools): recognize email addresses as explicit targets in send_mes…
liuhao1024 May 29, 2026
bfc4a26
fix(tools): point email home-channel error at EMAIL_HOME_ADDRESS
teknium1 May 30, 2026
cbf851a
perf(tui): stop slow/dead MCP servers from freezing TUI startup
kshitijk4poor May 30, 2026
61268ff
feat(cli): add hermes prompt-size diagnostic (#35276)
teknium1 May 30, 2026
0bfe19b
fix(gateway): merge nested gateway.platforms configuration block
Que0x May 29, 2026
6d2727e
fix(discord): bridge explicit allow_from configuration to env var map…
Que0x May 29, 2026
44f3e51
fix(gateway): run adapter config hooks for nested-only platform blocks
teknium1 May 30, 2026
5a1aa9e
fix(nous_account): add threading lock to prevent TOCTOU race on cache
sprmn24 May 28, 2026
5f84c91
fix(file-tools): handle UTF-8 BOM in read_file / write_file / patch (…
teknium1 May 30, 2026
ea6eaab
perf(read_file): compact line-number gutter — ~14% fewer tokens per r…
teknium1 May 30, 2026
10dec7c
fix(kanban): respect mobile safe areas in task detail drawer (#35378)
teknium1 May 30, 2026
2b16b75
fix(gateway): recover model on post-interrupt turn; gate fallback sta…
teknium1 May 30, 2026
bede3cf
fix(tools): wrap _run_tool cleanup in finally to prevent interrupt st…
liuhao1024 May 30, 2026
182739f
test(interrupt): assert no leaked tid instead of no-op block
teknium1 May 30, 2026
020601d
fix(compression): drop conflicting 'resume Active Task' directive in …
May 15, 2026
56b8dcc
fix(compressor): treat unanswered user questions as Active Task, not …
mathijsvandenhurk May 26, 2026
42bbd22
fix(compressor): strip stale handoff prefix on resume; reconcile #262…
teknium1 May 30, 2026
92ad7cc
fix(browser): recover from CDP DOM-node serialization crash in browse…
teknium1 May 30, 2026
9fbde54
fix(cli): fail closed on empty oneshot responses
briancl2 May 30, 2026
433bfff
fix(cli): surface oneshot agent exceptions to stderr with rc=1
teknium1 May 30, 2026
234ac00
fix(dashboard): allow insecure WS peers on explicit non-loopback bind…
teknium1 May 30, 2026
b0ce47d
feat: add text debounce batching for WhatsApp and WeChat platforms
redpiggy-cyber May 30, 2026
cddb728
fix(gateway): config.yaml path for WhatsApp/Weixin text-batch delays
teknium1 May 30, 2026
45465b0
fix(gateway): never auto-pause platforms on transient network/DNS fai…
teknium1 May 30, 2026
51d165a
fix(gateway): support Windows absolute paths in MEDIA tag regex and e…
Tranquil-Flow May 30, 2026
1b95545
test: use raw docstring in test_run_tool_media_re to silence escape w…
teknium1 May 30, 2026
20d073f
test: update extract_local_files Windows-path test for new matching b…
teknium1 May 30, 2026
b47cb1b
feat(kanban): file attachments on tasks (#35395)
teknium1 May 30, 2026
0c6e133
perf(cli): stop eager MCP discovery from blocking agent-capable startup
Sylw3ster May 30, 2026
96643b4
fix(file-tools): anchor relative-path resolution to absolute base; re…
teknium1 May 30, 2026
6a72af0
fix(managed-gateway): keep tool availability scans off the Nous token…
Sylw3ster May 30, 2026
5921d66
fix(cli): stop OSC 11 bg probe from trapping users in a stray editor …
OutThisLife May 30, 2026
b1a2540
perf(read_file): make compact gutter the only format; drop HERMES_REA…
teknium1 May 30, 2026
2282ff0
Merge remote-tracking branch 'upstream/main' into prod-sync-20260530
May 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 52 additions & 19 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1680,26 +1680,48 @@ def _read_main_provider() -> str:
# per turn — no lock needed. Cleared by ``clear_runtime_main()``.
_RUNTIME_MAIN_PROVIDER: str = ""
_RUNTIME_MAIN_MODEL: str = ""
_RUNTIME_MAIN_BASE_URL: str = ""
_RUNTIME_MAIN_API_KEY: str = ""
_RUNTIME_MAIN_API_MODE: str = ""


def set_runtime_main(provider: str, model: str) -> None:
"""Record the live runtime provider/model for the current AIAgent.
def set_runtime_main(
provider: str,
model: str,
*,
base_url: str = "",
api_key: str = "",
api_mode: str = "",
) -> None:
"""Record the live runtime provider/model/credentials for the current AIAgent.

Called by ``run_agent.AIAgent._sync_runtime_main_for_aux_routing`` (or
equivalent setter) at the top of each turn so that
``_read_main_provider`` / ``_read_main_model`` reflect CLI/gateway
overrides instead of the stale config.yaml default.

For ``custom:`` providers, ``base_url`` and ``api_key`` must also be
recorded so that ``_resolve_auto`` can construct a valid client in
Step 1 instead of falling through to the aggregator chain.
"""
global _RUNTIME_MAIN_PROVIDER, _RUNTIME_MAIN_MODEL
global _RUNTIME_MAIN_BASE_URL, _RUNTIME_MAIN_API_KEY, _RUNTIME_MAIN_API_MODE
_RUNTIME_MAIN_PROVIDER = (provider or "").strip().lower()
_RUNTIME_MAIN_MODEL = (model or "").strip()
_RUNTIME_MAIN_BASE_URL = (base_url or "").strip()
_RUNTIME_MAIN_API_KEY = api_key.strip() if isinstance(api_key, str) else ""
_RUNTIME_MAIN_API_MODE = (api_mode or "").strip()


def clear_runtime_main() -> None:
"""Clear the runtime override (e.g. on session end)."""
global _RUNTIME_MAIN_PROVIDER, _RUNTIME_MAIN_MODEL
global _RUNTIME_MAIN_BASE_URL, _RUNTIME_MAIN_API_KEY, _RUNTIME_MAIN_API_MODE
_RUNTIME_MAIN_PROVIDER = ""
_RUNTIME_MAIN_MODEL = ""
_RUNTIME_MAIN_BASE_URL = ""
_RUNTIME_MAIN_API_KEY = ""
_RUNTIME_MAIN_API_MODE = ""


def _resolve_custom_runtime() -> Tuple[Optional[str], Optional[str], Optional[str]]:
Expand Down Expand Up @@ -2980,6 +3002,18 @@ def _resolve_auto(main_runtime: Optional[Dict[str, Any]] = None) -> Tuple[Option
runtime_api_key = runtime.get("api_key", "")
runtime_api_mode = str(runtime.get("api_mode") or "")

# Fall back to process-local globals when main_runtime dict was not
# provided or was incomplete. ``set_runtime_main()`` now records
# base_url/api_key/api_mode alongside provider/model, so custom:
# providers get the full credential surface in Step 1 of the
# auto-detect chain.
if not runtime_base_url and _RUNTIME_MAIN_BASE_URL:
runtime_base_url = _RUNTIME_MAIN_BASE_URL
if not runtime_api_key and _RUNTIME_MAIN_API_KEY:
runtime_api_key = _RUNTIME_MAIN_API_KEY
if not runtime_api_mode and _RUNTIME_MAIN_API_MODE:
runtime_api_mode = _RUNTIME_MAIN_API_MODE

# ── Warn once if OPENAI_BASE_URL is set but config.yaml uses a named
# provider (not 'custom'). This catches the common "env poisoning"
# scenario where a user switches providers via `hermes model` but the
Expand Down Expand Up @@ -4725,24 +4759,23 @@ def _build_call_kwargs(
kwargs["temperature"] = temperature

if max_tokens is not None:
# Codex adapter handles max_tokens internally; OpenRouter/Nous use max_tokens.
# Direct OpenAI api.openai.com with newer models needs max_completion_tokens.
# ZAI vision models (glm-4v-flash, glm-4v-plus, etc.) reject max_tokens with
# error code 1210 ("API 调用参数有误") on multimodal requests — skip it.
_model_lower = (model or "").lower()
_skip_max_tokens = (
provider == "zai"
and ("4v" in _model_lower or "5v" in _model_lower or "-v" in _model_lower)
# We do NOT cap output by default. Most chat-completions providers treat
# an omitted max_tokens as "use the model's max output", which is what we
# want for auxiliary tasks (compression summaries, titles, vision, etc.) —
# an explicit cap only risks truncating a summary or 400-ing on providers
# that reject the parameter outright (e.g. GitHub Copilot / newer OpenAI
# GPT-5 models require max_completion_tokens, not max_tokens; ZAI vision
# models reject it entirely with error 1210). Omitting it sidesteps all of
# those wire-format quirks at once.
#
# The one exception is the Anthropic Messages wire (MiniMax and any
# ``/anthropic`` endpoint reached through the OpenAI SDK wrapper), where
# max_tokens is a MANDATORY field — omitting it is a hard 400. Keep it only
# there.
_effective_base = base_url or (
_current_custom_base_url() if provider == "custom" else ""
)
if _skip_max_tokens:
pass # ZAI vision models do not accept max_tokens
elif provider == "custom":
custom_base = base_url or _current_custom_base_url()
if base_url_hostname(custom_base) == "api.openai.com":
kwargs["max_completion_tokens"] = max_tokens
else:
kwargs["max_tokens"] = max_tokens
else:
if _is_anthropic_compat_endpoint(provider, _effective_base):
kwargs["max_tokens"] = max_tokens

if tools:
Expand Down
12 changes: 12 additions & 0 deletions agent/chat_completion_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,18 @@ def handle_max_iterations(agent, messages: list, api_call_count: int) -> str:
agent._copy_reasoning_content_for_api(msg, api_msg)
for internal_field in ("reasoning", "finish_reason", "_thinking_prefill"):
api_msg.pop(internal_field, None)
# Strict OpenAI-compatible gateways (Fireworks-backed OpenCode Go,
# Mistral, Moonshot/Kimi) reject any message key outside the Chat
# Completions schema. The main loop drops these via
# ChatCompletionsTransport.convert_messages(), but the summary path
# hand-builds messages and calls chat.completions.create() directly,
# bypassing the transport — so mirror that sanitization here:
# tool_name (SQLite FTS bookkeeping), the codex_* reasoning carriers,
# and every Hermes-internal underscore-prefixed scaffolding key.
for schema_foreign in ("tool_name", "codex_reasoning_items", "codex_message_items"):
api_msg.pop(schema_foreign, None)
for internal_key in [k for k in api_msg if isinstance(k, str) and k.startswith("_")]:
api_msg.pop(internal_key, None)
if _needs_sanitize:
agent._sanitize_tool_calls_for_strict_api(api_msg)
api_messages.append(api_msg)
Expand Down
125 changes: 113 additions & 12 deletions agent/context_compressor.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,46 @@
"window — treat it as background reference, NOT as active instructions. "
"Do NOT answer questions or fulfill requests mentioned in this summary; "
"they were already addressed. "
"Your current task is identified in the '## Active Task' section of the "
"summary — resume exactly from there. "
"Respond ONLY to the latest user message that appears AFTER this "
"summary — that message is the single source of truth for what to do "
"right now. "
"If the latest user message is consistent with the '## Active Task' "
"section, you may use the summary as background. If the latest user "
"message contradicts, supersedes, changes topic from, or in any way "
"diverges from '## Active Task' / '## In Progress' / '## Pending User "
"Asks' / '## Remaining Work', the latest message WINS — discard those "
"stale items entirely and do not 'wrap up the old task first'. "
"Reverse signals in the latest message (e.g. 'stop', 'undo', 'roll "
"back', 'just verify', 'don't do that anymore', 'never mind', a new "
"topic) must immediately end any in-flight work described in the "
"summary; do not re-surface it in later turns. "
"IMPORTANT: Your persistent memory (MEMORY.md, USER.md) in the system "
"prompt is ALWAYS authoritative and active — never ignore or deprioritize "
"memory content due to this compaction note. "
"The current session state (files, config, etc.) may reflect work "
"described here — avoid repeating it:"
)
LEGACY_SUMMARY_PREFIX = "[CONTEXT SUMMARY]:"

# Handoff prefixes that shipped in earlier releases. A summary persisted under
# one of these can be inherited into a resumed lineage (#35344); when it is
# re-normalized on re-compaction we must strip the OLD prefix too, otherwise the
# stale directive it carried (e.g. "resume exactly from Active Task") survives
# embedded in the body and keeps hijacking replies. Keep newest-first; entries
# are matched literally. Add a frozen copy here whenever SUMMARY_PREFIX changes.
_HISTORICAL_SUMMARY_PREFIXES = (
# Pre-#35344: contained the self-contradicting "resume exactly" directive.
"[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted "
"into the summary below. This is a handoff from a previous context "
"window — treat it as background reference, NOT as active instructions. "
"Do NOT answer questions or fulfill requests mentioned in this summary; "
"they were already addressed. "
"Your current task is identified in the '## Active Task' section of the "
"summary — resume exactly from there. "
"Respond ONLY to the latest user message "
"that appears AFTER this summary. The current session state (files, "
"config, etc.) may reflect work described here — avoid repeating it:"
"config, etc.) may reflect work described here — avoid repeating it:",
)
LEGACY_SUMMARY_PREFIX = "[CONTEXT SUMMARY]:"

# Minimum tokens for the summary output
_MIN_SUMMARY_TOKENS = 2000
Expand Down Expand Up @@ -518,6 +548,10 @@ def on_session_reset(self) -> None:
self._last_compression_savings_pct = 100.0
self._ineffective_compression_count = 0
self._summary_failure_cooldown_until = 0.0 # transient errors must not block a fresh session
self.last_real_prompt_tokens = 0
self.last_compression_rough_tokens = 0
self.last_rough_tokens_when_real_prompt_fit = 0
self.awaiting_real_usage_after_compression = False

def update_model(
self,
Expand Down Expand Up @@ -615,6 +649,10 @@ def __init__(

self.last_prompt_tokens = 0
self.last_completion_tokens = 0
self.last_real_prompt_tokens = 0
self.last_compression_rough_tokens = 0
self.last_rough_tokens_when_real_prompt_fit = 0
self.awaiting_real_usage_after_compression = False

self.summary_model = summary_model_override or ""

Expand Down Expand Up @@ -648,6 +686,44 @@ def update_from_response(self, usage: Dict[str, Any]):
self.last_prompt_tokens = usage.get("prompt_tokens", 0)
self.last_completion_tokens = usage.get("completion_tokens", 0)
self.last_total_tokens = usage.get("total_tokens", self.last_prompt_tokens + self.last_completion_tokens)
if self.last_prompt_tokens > 0:
self.last_real_prompt_tokens = self.last_prompt_tokens
if self.last_prompt_tokens < self.threshold_tokens:
if self.awaiting_real_usage_after_compression and self.last_compression_rough_tokens > 0:
self.last_rough_tokens_when_real_prompt_fit = self.last_compression_rough_tokens
else:
self.last_rough_tokens_when_real_prompt_fit = 0
self.awaiting_real_usage_after_compression = False

def should_defer_preflight_to_real_usage(self, rough_tokens: int) -> bool:
"""Return True when a high rough preflight estimate is known-noisy.

``estimate_request_tokens_rough(..., tools=...)`` intentionally
overestimates schema-heavy requests so Hermes compresses before a
provider rejects the payload. After a successful compressed API call,
though, provider ``prompt_tokens`` are a better signal than repeating
compaction from the same rough schema overhead. Defer only while the
rough estimate has grown modestly since a request the provider proved
fit under the threshold.
"""
if rough_tokens < self.threshold_tokens:
return False
if self.last_real_prompt_tokens <= 0:
return False
if self.last_real_prompt_tokens >= self.threshold_tokens:
return False

baseline = self.last_rough_tokens_when_real_prompt_fit or self.last_compression_rough_tokens
if baseline <= 0:
return False

growth = max(0, rough_tokens - baseline)
tolerated_growth = max(4096, int(self.threshold_tokens * 0.05))
if growth > tolerated_growth:
return False

self.last_rough_tokens_when_real_prompt_fit = max(baseline, rough_tokens)
return True

def should_compress(self, prompt_tokens: int = None) -> bool:
"""Check if context exceeds the compression threshold.
Expand Down Expand Up @@ -1190,11 +1266,27 @@ def _generate_summary(

# Shared structured template (used by both paths).
_template_sections = f"""## Active Task
[THE SINGLE MOST IMPORTANT FIELD. Copy the user's most recent request or
task assignment verbatim — the exact words they used. If multiple tasks
were requested and only some are done, list only the ones NOT yet completed.
Continuation should pick up exactly here. Example:
[THE SINGLE MOST IMPORTANT FIELD. Capture the user's most recent unfulfilled
input verbatim — the exact words they used. This includes:
- Explicit task assignments ("refactor the auth module")
- Questions awaiting an answer ("waarom staat X op Y?", "wat zijn de volgende stappen?")
- Decisions awaiting input ("optie A of B?")
- Ongoing discussions where the assistant owes the next substantive reply
A conversation where the user just asked a question IS an active task — the
task is "answer that question with full context". Do NOT write "None" merely
because the user did not issue an imperative command; reserve "None" for the
rare case where the last exchange was fully resolved and the user said
something like "thanks, that's all".
If multiple items are outstanding, list only the ones NOT yet completed.
Continuation should pick up exactly here. Examples:
"User asked: 'Now refactor the auth module to use JWT instead of sessions'"
"User asked: 'Waarom stond provider ineens op openrouter?' — needs investigation + answer"
"User chose option A; awaiting implementation of step 2"
If the user's most recent message was a reverse signal (stop, undo, roll
back, never mind, just verify, change of topic) that supersedes earlier
work, write the reverse signal verbatim and DO NOT carry forward the
cancelled task. Example: "User asked: 'Stop the i18n refactor and just
verify the current diff' — earlier i18n in-flight work is cancelled."
If no outstanding task exists, write "None."]

## Goal
Expand Down Expand Up @@ -1260,7 +1352,7 @@ def _generate_summary(
NEW TURNS TO INCORPORATE:
{content_to_summarize}

Update the summary using this exact structure. PRESERVE all existing information that is still relevant. ADD new completed actions to the numbered list (continue numbering). Move items from "In Progress" to "Completed Actions" when done. Move answered questions to "Resolved Questions". Update "Active State" to reflect current state. Remove information only if it is clearly obsolete. CRITICAL: Update "## Active Task" to reflect the user's most recent unfulfilled request — this is the most important field for task continuity.
Update the summary using this exact structure. PRESERVE all existing information that is still relevant. ADD new completed actions to the numbered list (continue numbering). Move items from "In Progress" to "Completed Actions" when done. Move answered questions to "Resolved Questions". Update "Active State" to reflect current state. Remove information only if it is clearly obsolete. CRITICAL: Update "## Active Task" to reflect the user's most recent unfulfilled input — this includes any question, decision request, or discussion turn that the assistant has not yet answered. Only write "None" if the last exchange was fully resolved.

{_template_sections}"""
else:
Expand Down Expand Up @@ -1424,9 +1516,16 @@ def _generate_summary(

@staticmethod
def _strip_summary_prefix(summary: str) -> str:
"""Return summary body without the current or legacy handoff prefix."""
"""Return summary body without the current, legacy, or any historical
handoff prefix.

Historical prefixes must be stripped too: a handoff persisted under an
older prefix can be inherited into a resumed lineage (#35344), and if we
only re-prepend the current prefix without removing the old one, the
stale directive it carried stays embedded in the body.
"""
text = (summary or "").strip()
for prefix in (SUMMARY_PREFIX, LEGACY_SUMMARY_PREFIX):
for prefix in (SUMMARY_PREFIX, LEGACY_SUMMARY_PREFIX, *_HISTORICAL_SUMMARY_PREFIXES):
if text.startswith(prefix):
return text[len(prefix):].lstrip()
return text
Expand All @@ -1440,7 +1539,9 @@ def _with_summary_prefix(cls, summary: str) -> str:
@staticmethod
def _is_context_summary_content(content: Any) -> bool:
text = _content_text_for_contains(content).lstrip()
return text.startswith(SUMMARY_PREFIX) or text.startswith(LEGACY_SUMMARY_PREFIX)
if text.startswith(SUMMARY_PREFIX) or text.startswith(LEGACY_SUMMARY_PREFIX):
return True
return any(text.startswith(p) for p in _HISTORICAL_SUMMARY_PREFIXES)

@classmethod
def _find_latest_context_summary(
Expand Down
9 changes: 9 additions & 0 deletions agent/context_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ def should_compress_preflight(self, messages: List[Dict[str, Any]]) -> bool:
"""
return False

def should_defer_preflight_to_real_usage(self, rough_tokens: int) -> bool:
"""Return True when preflight should trust recent real usage instead.

Built-in compression uses this to avoid re-compacting from known-noisy
rough estimates after a compressed request has already fit. Third-party
engines can ignore it safely.
"""
return False

# -- Optional: manual /compress preflight ------------------------------

def has_content_to_compress(self, messages: List[Dict[str, Any]]) -> bool:
Expand Down
15 changes: 7 additions & 8 deletions agent/conversation_compression.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,19 +575,18 @@ def _release_lock() -> None:
force=True,
)

# Update token estimate after compaction so pressure calculations
# use the post-compression count, not the stale pre-compression one.
# Use estimate_request_tokens_rough() so tool schemas are included —
# with 50+ tools enabled, schemas alone can add 20-30K tokens, and
# omitting them delays the next compression cycle far past the
# configured threshold (issue #14695).
# Keep the post-compression rough estimate for diagnostics, but do not
# treat it as provider-reported prompt usage. Schema-heavy rough estimates
# can remain above threshold even after the next real API request fits.
_compressed_est = estimate_request_tokens_rough(
compressed,
system_prompt=new_system_prompt or "",
tools=agent.tools or None,
)
agent.context_compressor.last_prompt_tokens = _compressed_est
agent.context_compressor.last_compression_rough_tokens = _compressed_est
agent.context_compressor.last_prompt_tokens = -1
agent.context_compressor.last_completion_tokens = 0
agent.context_compressor.awaiting_real_usage_after_compression = True

# Clear the file-read dedup cache. After compression the original
# read content is summarised away — if the model re-reads the same
Expand All @@ -599,7 +598,7 @@ def _release_lock() -> None:
pass

logger.info(
"context compression done: session=%s messages=%d->%d tokens=~%s",
"context compression done: session=%s messages=%d->%d rough_tokens=~%s awaiting_real_usage=true",
agent.session_id or "none", _pre_msg_count, len(compressed),
f"{_compressed_est:,}",
)
Expand Down
Loading
Loading