Commit graph

2492 commits

Author SHA1 Message Date
kshitijk4poor
53559aaf86 fix(agent): protect batch-compaction markers from micro supersede/defrag
Phase 2 review findings on the salvage branch:

C1 (critical): batch and micro summary markers share
COMPRESSED_SUMMARY_METADATA_KEY, and compress() never reset micro state.
After micro absorbed exchanges 1..k, a batch compaction summarizing
1..m (m>k) could fire; the next micro pass's supersede then dropped the
batch marker (whose content the stale rolling summary does NOT contain)
and archive_and_compact immediately made the loss durable. Defrag had
the same hazard: it rewrote "the newest marker" even if that was a
batch marker. Empirically confirmed with a probe (batch marker content
destroyed in one pass).

Fix, three parts:
- Micro-created markers now carry MICRO_COMPACT_MARKER_KEY; supersede
  and defrag only ever touch micro-tagged markers. Rehydration in
  _resolve_compact_cursor tags the marker it absorbs (containment
  proof), which safely covers adopting a batch marker as the new
  rolling base after a reset.
- compress() success path resets micro rolling summary/cursor state so
  a stale summary can never claim cumulativeness over a batch marker.
- Regression tests for both directions plus the reset.

W4: _splice_micro_compact_result no longer strips _db_persisted stamps
from surviving messages. Micro archives in place under the SAME session
id (unlike batch's child-session rotation, #57491), so surviving stamps
are accurate; stripping them meant an archive_and_compact failure left
every previously-persisted message unstamped and the next append-only
flush re-inserted them all as duplicate active rows.

W5: finalize_turn micro gate now checks agent._persist_disabled —
persistence-isolated fork agents (background review) must not burn an
aux call per review turn, and must never archive_and_compact the
canonical session rows if their compressor ever gains a DB binding.

W1: _serialize_one_exchange now delegates to _serialize_for_summary
(was a ~70-line near-verbatim copy; one serializer, one place to fix).

S4: _find_one_exchange boundary guard rejects only assistant/tool
boundaries (the actual alternation hazard) instead of requiring user —
a stray mid-list system/injected message can no longer wedge the
cursor forever.

5 new regression tests; 38 micro/prune tests, 400 compression-suite
tests, 61 finalize/persist tests pass; ruff clean.
2026-07-31 17:44:19 +05:30
kshitijk4poor
c696a5fd9c fix(agent): harden the finalize-turn micro-compaction gate against duck-typed compressors
tests/run_agent/test_proactive_prune_loop_wiring.py builds agents with a
MagicMock compressor; getattr(mock, '_micro_compact_enabled', False)
returns a truthy auto-attribute, so the hook called _micro_compact on the
mock and spliced its (empty-iterating) return over the transcript —
wiping all messages before persist (CI slice 7/8 failure).

Gate now requires _micro_compact_enabled is True, a callable
_micro_compact, and a non-empty list result before touching messages.
Same hardening protects production plugin context engines that don't
subclass ContextCompressor.
2026-07-31 17:44:19 +05:30
kshitijk4poor
b8bfd68af1 fix(agent): make micro-compaction alternation-safe and defrag user-preserving
Two integration bugs found during review of #74522, both confirmed with
empirical probes against the production message-repair path:

1. Alternation: the summary marker was role="user" and an exchange was a
   single assistant+tools group, so splicing between two user turns produced
   user -> marker(user) -> user. The pre-request repair_message_sequence pass
   (conversation_loop.py, runs before EVERY API call) then merged the marker
   into the neighbouring real user message: metadata gone, cursor
   unrecoverable on resume, and the summary text duplicated into the
   transcript on every later pass (the transcript GREW every turn).
   Fix: an exchange is now a full agent turn (assistant + tools + follow-up
   assistant iterations, bounded by user messages), the marker is
   assistant-role, and superseding an old marker deliberately merges the two
   adjacent real user turns (plain-text \n\n-join, identical to repair
   pass 2) so the returned transcript is alternation-valid by construction.
   Probe result: repairs 0 (was 2), marker survives, no summary leakage.

2. Defrag destroyed user messages: _defrag_rolling_summary serialized the
   whole remaining middle (user turns included) and spliced it away —
   8 of 10 user prompts destroyed in one pass, contradicting the feature's
   "your messages are never compacted" invariant. Fix: defrag now
   re-summarizes only the rolling summary TEXT and rewrites the marker
   content in place; transcript shape, cursor, and user turns untouched.
   Probe result: 10 of 10 user prompts survive.

Also: marker provenance is now COMPRESSED_SUMMARY_HAS_USER_TURN_KEY=False —
micro markers absorb only assistant/tool content (#64650 invariant), and
real user turns remain in the transcript for provenance detection.

Adds 5 regression tests (repair-pass integration, alternation on
multi-iteration tool turns, defrag user survival, defrag input scope,
marker provenance); updates the two existing tests and the design doc to
the corrected semantics. 28 tests pass.
2026-07-31 17:44:19 +05:30
Michael Jordan
9ca4ee72ca feat(agent): make the micro-compaction cadence configurable
The on/off switch was the only knob. A pass fired after every completed turn,
absorbed exactly one exchange, and there was no way to ask for less. Since a
pass is also what breaks the prompt-cache prefix, "how often does it run" and
"how often do I pay a cache break" are the same question, and it had no answer.

Add `compression.micro_compact_every_n_turns` (default 1, clamped to >= 1). At 1
the behaviour is what it was; at 5 you get a fifth of the breaks and a fifth of
the reclaim rate. The counter advances per invocation rather than per committed
pass, so a turn that finds nothing to absorb still moves the cadence along and
cannot wedge it, and a bogus 0 or negative degrades to "every turn" instead of
silently disabling compaction.

Also expose `micro_compact_defrag_threshold_tokens`, which has been a hardcoded
attribute on the compressor with no path from config since it was added.

This does not give micro-compaction the prune's reclaim-size gate -- a pass
still commits whatever the single absorbed exchange saved. It makes the break
frequency tunable, which reaches the same end by absorbing less rather than by
waiting for a bigger win. The docs now say that plainly, including that a
reclaim threshold is the obvious follow-up and does not exist yet.

Tests cover the skip-until-due window, the cursor and prefix staying untouched
on skipped turns, the clamp, and that the feature is off unless enabled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 17:44:19 +05:30
Michael Jordan
d19c182879 fix(agent): ship micro-compaction opt-in, not default-on
Review raised whether default-on can be reconciled with the prompt-cache
contract in AGENTS.md, which permits mutating past context only for context
compression and treats per-conversation caching as sacred. It cannot, and the
codebase already says so in its own words.

A micro-compaction pass rewrites already-sent history, so it invalidates the
cached prefix every turn rather than at an episodic boundary. That is the exact
cost the proactive prune gates against: `proactive_prune_min_reclaim_tokens`
exists, per its own config comment, to keep rewrites to "one big episodic break
instead of a tiny break every tool iteration." Micro-compaction has no
equivalent gate -- one exchange per turn means one break per turn, by design.

Default to off. An operator who wants the amortized stall can opt in with
`compression.micro_compact: true` and accept the tradeoff knowingly; nobody
inherits a per-turn cache break from installing an update.

Also register the key in config_defaults so it is discoverable and picked up by
the update path's new-options check -- it was previously read by agent_init but
declared nowhere -- and document the cache cost in docs/micro-compaction.md
instead of only the benefit. The measurements behind the feature (occupancy
plateau, zero batch compactions) never priced cache invalidation, and the doc
now says which numbers a reader would need to measure to justify enabling it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 17:44:19 +05:30
Michael Jordan
60781a0cc8 fix(agent): do not destroy compacted history when a session resumes
The rolling summary lives only in memory. A resumed session starts with an
empty one while the marker carrying every previously absorbed exchange is
still in the transcript. The first pass after a resume therefore built a
marker from a single exchange and superseded the marker holding the entire
history -- silently discarding everything micro-compaction had accumulated.

This was introduced by the supersede fix. Before it, markers piled up
wastefully, but nothing was ever lost.

Two changes, so a single failure cannot lose data:

Rehydrate. When the cursor is recovered by scanning the transcript -- the
resume path -- also recover the rolling summary from that marker, so the
next pass merges into the existing history instead of replacing it.
Extraction uses rfind for the heading because SUMMARY_PREFIX references the
heading text itself, so the first occurrence is inside the preamble.

Gate superseding. Earlier markers are dropped only when this pass's summary
is demonstrably cumulative, i.e. the rolling summary was non-empty going in.
If rehydration ever fails, the pass keeps both markers: wasteful, but the
history survives.

Tests cover the resume path, the failed-rehydration fallback, and the
round trip of a summary through a marker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:44:19 +05:30
Michael Jordan
d626528560 fix(agent): derive the micro-compaction cursor from the spliced list
The cursor was set to the pre-splice `exchange_end`. A splice collapses the
absorbed span -- an assistant plus its tool results, often four or more
messages -- into a single marker, and may also drop a superseded marker
further back, so every index after it shifts.

The stale cursor therefore overshot, landing inside a *later* exchange's
tool group. The next pass's `_find_one_exchange` walked forward from there
to the following assistant, so the exchange it had landed inside was never
absorbed at all. On tool-bearing conversations micro-compaction was
silently doing roughly half the work it should.

Traced on a 3-tool-per-exchange transcript: the cursor sat at index 6 when
the marker was at 2, and the message count stalled at 32 instead of
continuing to 28.

Derive the cursor from the marker's actual position in the spliced result
instead, which is self-correcting regardless of how much the splice moved.
Apply it on the defrag path too, which had the same staleness.

Found by a randomized long-horizon harness (480 conversation shapes x 25
passes, varying tool-group sizes and summarizer failure modes) asserting
structural and progress invariants after every pass. Existing tests missed
it because their fixtures have no tool results, so the absorbed span is one
message and nothing shifts. The regression test uses tool groups.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:44:19 +05:30
Michael Jordan
ac48add3a7 feat(agent): report context occupancy, not just tokens saved
Tokens saved is the wrong headline for this feature. Micro-compaction is
not an efficiency optimisation — the same summarization work happens either
way. What it buys is (a) that work amortized across turns instead of one
stall, and (b) a window kept low enough that a session runs much further
before needing a hard compaction at all.

Neither shows up in "net tokens saved". A session can save nothing on paper
and still be a clear win on both counts.

So the telemetry now carries occupancy: tokens_after as a share of the
compaction threshold, plus the threshold and resolved window it was
computed from. That is the number that says whether a session has headroom
left. The report leads with it, and cross-references the batch
`compression_attempt` lines already in the log so it can show how often the
long pause actually fired — ideally never.

Occupancy is read from the cached threshold only. The public
`threshold_tokens` property resolves lazily and can issue a synchronous
/models probe (#32221); telemetry must never be the thing that blocks a
turn, so an unresolved window reports null. In practice a pass has already
resolved it via the tail calculation, so the field is populated. A test
pins the no-forcing behaviour directly against the emitter.

The report is pure ASCII: `scripts/check_subprocess_stdin.py` currently
dies on a cp1252 console before printing its results, and a diagnostic tool
that crashes on the platform it is diagnosing is worse than no tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:44:19 +05:30
Michael Jordan
cac9526d2a feat(agent): token telemetry for micro-compaction
The existing log line reports message counts, which is the least
informative number available here: absorbing one tool-heavy exchange can
drop hundreds of tokens while moving the count by one. There was no way to
answer "is this actually helping?" from a real session.

Emit one content-free JSON line per pass, in the same shape as the batch
compaction telemetry: before/after tokens, the delta, the size of the
absorbed exchange, the rolling summary size, duration, and running
per-session totals so a whole run can be read off the last line. No
transcript content rides along.

Add scripts/micro_compaction_report.py to aggregate those lines into
passes, outcome mix, net tokens saved, mean exchange size and durations,
with an optional per-session breakdown.

Measuring it immediately surfaced something worth documenting: the first
pass in a session normally *costs* tokens. The summary marker carries a
fixed ~400 tokens of scaffolding, paid on pass one against a single
absorbed exchange. From pass two the marker is replaced rather than added,
so the overhead is already paid and each exchange is close to pure saving.
Break-even is typically the second or third pass. Tests cover the
telemetry contract, the cumulative totals, and that first-pass/later-pass
shape so nobody reads a single turn and concludes it made things worse.

The estimator costs ~5 ms at 600 messages and ~20 ms at 1200, taken twice
per pass, post-turn — and only once an exchange is actually in hand, so
turns that no-op early pay nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:44:19 +05:30
Michael Jordan
a72f898d0f docs(agent): drop a fabricated issue reference
The `_micro_compact` docstring cited "#82483" for the resume double-load
problem. No such issue exists — the repository's highest number is 74323,
so the reference was invented rather than looked up.

The reasoning it was attached to is correct and stays: the session flush is
append-only, so an in-memory splice alone leaves the original rows active
and a resume loads both the summary and the messages it replaced. Only the
citation was wrong.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:44:19 +05:30
Michael Jordan
214b5d8612 docs(agent): state that user turns are never micro-compacted
`_find_one_exchange`'s docstring described an exchange as "(optional) user
message + assistant message + its tool results", but the walk skips past
user messages and starts at the assistant, so user turns are never absorbed
into the rolling summary.

The code is right and the docstring was wrong. Assistant output is largely
an account of what was done and survives summarising with little loss. The
user's messages are the intent everything else is derived from and cannot be
reconstructed from the work that followed — paraphrasing "use the existing
helper, don't add a new one" into a summary is how an agent ends up doing
the opposite six turns later. They are also cheap: a prompt is normally a
tiny fraction of what one tool result costs.

Correct the docstring, document the property (and its cost — a floor on how
small the middle can get, since user turns accumulate), and add a test so it
stays deliberate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:44:19 +05:30
Michael Jordan
186cad02f9 feat(agent): per-turn micro-compaction to amortize context compression
Batch compaction pauses a session for one large summarization once the
window fills. Micro-compaction spreads that cost out: after each completed
turn, `finalize_turn` folds the single oldest un-absorbed exchange
(assistant message plus its tool results) into a rolling summary, so the
work happens in small increments during post-turn idle time instead of one
long stall.

Mechanics:
  - a cursor tracks the first message not yet absorbed, recovered from the
    transcript's last summary marker when in-memory state is unavailable;
  - protected head and tail windows are never touched, so the system prompt
    and recent turns stay verbatim;
  - the absorbed span is replaced by a marker carrying the usual
    `_compressed_summary` metadata, so resume, handoff and `/compress`
    treat it exactly like a batch summary;
  - `archive_and_compact` keeps the session DB in step, otherwise the
    append-only flush would leave the original rows active and a resume
    would double-load both summary and originals;
  - when the rolling summary itself passes a token threshold it is
    defragged: re-summarized in one shot and the cursor jumps to the tail;
  - an exchange the summarizer can't handle is retried a bounded number of
    times, then skipped, so one poison exchange can't stall every turn.

Keep only the newest summary marker. The rolling summary is cumulative, so
each marker already contains everything the previous ones held; leaving them
stacked near-duplicate copies of the same text, each with its own heading and
end-marker scaffolding, and the transcript grew on every turn instead of
shrinking. Measured over six turns on a 12-exchange conversation with tool
output: 4104 -> 4797 tokens before, 4104 -> 2572 after.

Off switch: `compression.micro_compact: false` (default on).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:44:19 +05:30
rob-maron
4c7cc62f9f flux3 messaging system fixes 2026-07-30 15:20:09 -07:00
Matt Ezell
7965462d6c fix(compression): choose summary role by template-visible alternation
The compaction summary's role was selected against the LITERAL
neighbouring messages (compressed[-1] / tail_messages[0]). Mistral-family
chat templates (Devstral, Mistral Small 3.x, Magistral) enforce
user/assistant alternation but exempt the tool flow (tool results and
assistant messages carrying tool_calls) from the check, so a protected
head ending [user, assistant(tool_calls), tool] pinned the summary to
role="user" while the last role the template counts is "user": the
backend rejects the whole request with a Jinja alternation error
(HTTP 500). The summary persists in the stored conversation, every
retry replays the identical poisoned history, and the session is
permanently unrecoverable. Fires on EVERY compaction against a
Mistral-strict backend, captured byte-exact via a tee-proxy in front of
a llama.cpp/llama-swap Devstral deployment.

Fix: compute both neighbour roles through _template_visible_role(),
which skips template-exempt messages. The #52160 (Anthropic user-first)
and #58753 (zero-user-turn) forced-user guards are preserved; their
forced shapes (summary-user followed only by exempt messages) are
alternation-safe. When the visible head ends "assistant" and the
visible tail opens "user", no standalone role can alternate and the
existing merge-into-tail fallback now correctly fires (the literal
logic emitted a standalone user summary there: a second poisoning
shape).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGN45sMMbwM8cW9T9ga4ou
2026-07-30 21:07:36 +05:30
Brooklyn Nicholson
7d92056c49 feat(gateway): iMessage-style message reactions — storage, RPC, agent tool, model context
Reactions live in the existing messages.display_metadata JSON column (no new
table), with iOS Tapback semantics enforced DB-side: one reaction per author
per message, re-tap retracts, different emoji replaces. The desktop catches up
to the reaction contract five platform adapters already ship.

- SessionDB: set/get_message_reaction, latest_message_row_id (role + offset +
  require_text so invisible tool-call-only rows are never targeted),
  take_unseen_reactions (announce-exactly-once), get_message_role
- message.react RPC: accepts row_id or newest_role for live messages that
  haven't learned their durable id yet
- react_to_message tool: desktop-gated (check_fn), defaults to the user's
  latest visible message, messages_back for retroactive reactions
- Model context rides run_message only (beside the speech-interrupted note):
  the persisted prompt stays clean, so no [The user reacted …] scaffolding in
  transcripts, and no cached prefix ever changes
- Resume projection forwards row_id + reactions; _row_id is stripped from
  outgoing API copies next to display_metadata
2026-07-30 00:08:28 -05:00
Teknium
c770515e2b modernize re-added Vercel integrations: SDK 0.7.2, telemetry off, sibling-site wiring
- Bump vercel SDK pin 0.5.7 -> 0.7.2 (pyproject, lazy_deps) and regenerate uv.lock
- Disable the SDK's new default-on telemetry (VERCEL_TELEMETRY_DISABLED=1
  set before import, user-overridable) per the no-opt-out-telemetry policy
- Move _model_flow_ai_gateway into hermes_cli/model_setup_flows.py (god-file
  decomposition landed after the removal)
- Widen post-removal backend sets that vercel_sandbox missed: terminal_tool
  container_backend + _CONTAINER_BACKENDS, file_tools fallback set,
  env_probe._REMOTE_BACKENDS, approval._should_skip_container_guards,
  prompt_builder probe container_config
- Add terminal.vercel_runtime to config_defaults + TERMINAL_CONFIG_ENV_MAP
- Re-add vercel dependency group to nix #full variant (reverts #33773 workaround)
- Update restored tests to current contracts: upload-only credential sync-back
  (bcfc7458fa), registry-derived provider env list, parametrized backend fixture,
  drop tests superseded on main (slack wizard move #41112, nous status format)
2026-07-29 19:48:37 -07:00
Teknium
ad12df6ba4 Revert "remove Vercel AI Gateway and Vercel Sandbox (#33067)"
This reverts commit febc4cfec0.
2026-07-29 19:48:37 -07:00
Jasmine Naderi
91351b7b77 fix(state): make journal mode canonical and behaviorally verified
Use database.journal_mode as the sole non-secret operator setting, preserve the vulnerable-SQLite safety gate and existing WAL databases, validate explicit DELETE results, document the active config path, and cover real SQLite openers with behavioral tests.
2026-07-29 18:13:09 -07:00
Jasmine Naderi
04ec841462 fix(state): configurable journal_mode + centralize all DB openers
Add HERMES_JOURNAL_MODE env / database.journal_mode config for
virtiofs/NFS/SMB where WAL is not crash-safe. Route 5 bypass openers
through apply_wal_with_fallback so a single setting covers every .db
(#68545).
2026-07-29 18:13:09 -07:00
Gabriel Stoltemberg
59ee85ed50 perf: use load_config_readonly() at read-only call sites in agent/
Salvaged from #56085 (@Stoltemberg), rebased onto current main: sites
main had already converted (credential_pool, auxiliary_client MoA
paths, model_metadata, moa_loop, agent_runtime_helpers) resolve to
main's versions; the remaining ~29 read-only sites across 16 agent/
files swap to the no-deepcopy readonly loader (~135us saved per call).

Full per-site mutation audit performed (every enclosing function read,
escapes traced): 23 SAFE, 5 ESCAPES with read-only consumers, 1 UNSAFE
path (init_agent -> get_compatible_custom_providers -> normalizer
in-place alias writes) fixed by the preceding no-mutate commits, which
make the normalizer copy-safe for ALL callers.
2026-07-29 15:28:15 -07:00
Soju06
ce9f6712ff refactor(agent): remove the inter-tool delay
The 1.0s sleep between sequential tool calls has been present since the
initial commit with no documented rationale. It sleeps between local
tool executions — the next LLM request goes out only after the whole
batch — so it rate-limits nothing, and the parallel read-only path
already runs with no delay. Every multi-tool turn pays (N-1) seconds
of dead time. Remove the sleep, the internal tool_delay plumbing, and
dead test assignments. AIAgent.__init__ keeps tool_delay as a
deprecated no-op keyword for one release so existing programmatic
callers construct cleanly; passing it emits a DeprecationWarning.
2026-07-29 15:21:26 -07:00
Teknium
1a088989bc
Merge pull request #66730 from NousResearch/feat/hsp-sync-client
feat(sync): HSP/1 personal skill sync client (M1 client)
2026-07-29 15:20:35 -07:00
Teknium
28524adb0e fix(tests): eliminate flaky/broken tests — shadow sys.path inserts, unmocked network in compressor tests, stale-SDK feishu pin guard, quadratic redact regexes
- Remove tests/-shadowing sys.path.insert(dirname/'..') from 11 test files:
  it prepended the tests/ dir itself to sys.path, so 'import agent' /
  'import hermes_cli' resolved to the test packages and collection died
  with ModuleNotFoundError depending on import order (2 files failed in
  every full-suite run; 9 more were latent).
- Patch call_llm in 5 context-compressor tests that called compress()
  unmocked: each burned ~50s attempting live LLM traffic through the
  relay before falling back (572s file — the slowest in the suite, and
  flaky under the 300s per-file timeout). File now runs in ~5s.
- agent/redact.py: fix two catastrophically-backtracking regexes hit by
  the compressor's redaction pass on large payloads —
  _STRICT_URL_USERINFO_RE anchors on the mandatory '//' (optional-scheme
  prefix backtracked O(n^2): ~55s on a 320KB payload, now sub-ms;
  output-equivalence fuzz-verified on 20k random strings), and the
  _CFG_DOTTED_RE/_CFG_ANCHORED_RE subs gain an exact linear keyword
  pre-gate so secret-free text skips the quadratic pattern entirely.
- tests/gateway/test_feishu.py: version-guard the extra_ua_tags SDK
  signature check; the repo pins lark-oapi==1.6.8 but stale local
  installs (1.5.3) fail the assertion — skip below the pin.
- tests/tools/test_managed_browserbase_and_modal.py: stub
  agent.redact + agent.credential_persistence in the fake agent package
  (empty __path__ blocks all real agent.* imports added since the fake
  was written).
- tests/gateway/test_startup_restart_race.py: raise wait_for timeouts
  2s -> 30s; 2s wall-clock on a loaded 40-worker box flaked in the
  baseline run (passes instantly when the box is quiet).
2026-07-29 15:12:28 -07:00
Ben Barclay
f5b68ad58b Merge origin/main into feat/hsp-sync-client
Resolves the PR's conflict with main (2252 commits). Two conflicts, both
"each side added an independent block in the same place" — kept both:

- gateway/run.py — the housekeeping loop. This branch adds the Skill Sync
  pulls inside the CURATOR_EVERY branch (12-space indent); main adds a
  stale-session auto-archive as a sibling `if` at loop level (8-space).
  Different scopes, so the naive union would have mis-nested the archive
  block into the curator branch; kept each at its own indent level.
- tools/skill_manager_tool.py — the _edit_skill result dict. This branch
  appends the org auto-propose note; main appends
  _add_description_prompt_preview(). Independent, order-insensitive.

No behaviour dropped from either side.

Verified: 3552 passed / 0 failed across 63 suites (scope regenerated to
include main's new maybe_auto_archive / _add_description_prompt_preview
consumers) via scripts/run_tests.sh. `hermes sync` and `hermes sync status`
still work against a live token, resolving the production plane default.

The Pyright Optional-parameter warnings in skill_manager_tool.py are
pre-existing on main (`content: str = None` etc.), not introduced here.
2026-07-29 13:01:36 -07:00
teknium1
f8758dcaf8 refactor(agent): single-owner call_id + reasoning_content sanitization policies (wire-parity verified) 2026-07-29 12:29:39 -07:00
Francesco Bonacci
c268397752 feat(computer_use): align cua-driver 0.10 permission modes 2026-07-29 12:19:37 -07:00
Francesco Bonacci
847e401b74 feat(computer_use): align cua-driver 0.9 contracts
Salvaged from PR #67807 by @f-trycua onto current main.

- Foreground gate: discover delivery_mode support from the live tools/list
  inputSchema.properties (fail closed), not the never-shipped
  input.delivery_mode capability token
- bring_to_front: standalone strict-schema MCP tool (inject_session=False),
  separate approval scope, requires foreground
- Verdict precedence: confirmed > unverifiable (verify before retry) >
  suspected_noop/refusal (escalate); surfaced as explicit verdict field
- Typed cua_browser_* route inside computer_use (browser_route.py) with
  exact-binding, adapter-injected session, snapshot-scoped refs
- Per-Hermes-session backend isolation + release_computer_use_session seam
  wired into AIAgent.close()
- Recorded 0.9 tools/list fixture replaces fabricated capability tokens
2026-07-29 12:19:37 -07:00
teknium1
30c783589c perf(agent): cursor/memo optimizations for per-iteration full-history walks (byte-parity proven)
Three provably-safe optimizations for O(n)-per-iteration history walks:

1. sanitize_tool_call_arguments: optional identity-keyed cursor (strong
   refs to the exact validated message objects) skips re-json.loads-ing
   already-validated history each loop iteration. Any list rewrite
   (compression, repair, undo, steer) breaks the identity prefix match
   and forces re-scan from the divergence point. Wired via a per-agent
   cursor dict in conversation_loop.

2. estimate_messages_tokens_rough: per-message memo keyed on a deep
   identity fingerprint (strings pinned by strong reference so id()
   aliasing is impossible; scalars by value; dicts/lists structurally
   with key order). Equal fingerprints imply identical str(shadow)
   bytes, hence identical estimates. Unfingerprintable shapes fall
   through to direct compute. Bounded FIFO cache (4096 entries).

3. _flush_messages_to_session_db_unlocked: bounded scan that skips the
   identity-matched prefix of the previous successful flush's snapshot.
   Snapshot only taken on full success; cleared on exception. Compression
   rewrites use fresh copies, breaking identity and forcing full re-scan.

Parity proven in tests/agent/test_cursor_optimizations_parity.py:
500-message synthetic histories with tool calls, malformed args, unicode,
element-wise old==new across 3 iterations incl. simulated compression.

Measured (median of 5): sanitize 0.097ms->0.011ms, tokens 1.145ms->0.853ms,
persist-scan 179.5us->10.0us at 500 messages.
2026-07-29 11:54:18 -07:00
teknium1
5b751dc0ad chore: remove unused imports and dead locals (ruff F401/F841 sweep)
Cleans F401 unused imports and F841 dead local assignments across
root *.py, agent/, hermes_cli/, tools/, gateway/, cron/, tui_gateway/
(tests/, plugins/, skills/ excluded).

Intentionally KEPT (false positives / test-patch surfaces):
- agent/transports/__init__.py package re-exports
- cli.py browser_connect re-exports (DEFAULT_BROWSER_CDP_URL area,
  used by tests/cli/test_cli_browser_connect.py)
- hermes_cli/main.py _prompt_auth_credentials_choice /
  _model_flow_bedrock_api_key (accessed via main_mod attr in tests)
- gateway/run.py aliased replay_cleanup + whatsapp_identity re-exports
  and _PORT_BINDING_PLATFORM_VALUES (test-referenced)
- hermes_cli/web_server.py get_running_pid (tests monkeypatch it) and
  _OAUTH_TOKEN_URL availability probe
- hermes_cli/config.py get_process_hermes_home re-export (noqa'd F811
  chain) and yaml availability-probe import
- hermes_cli/nous_subscription.py managed_nous_tools_enabled
  (tests patch hermes_cli.nous_subscription.managed_nous_tools_enabled)
- try/except ImportError availability probes (env_loader, tts_tool,
  mcp_tool, web_server anthropic OAuth block)
- tools/web_tools.py noqa F401 re-exports
- hermes_cli/setup_whatsapp_cloud.py:263 'proceed' skipped: possible
  missing-guard bug, flagged for separate review
- unused function parameters (signature changes out of scope)

Side-effect RHS calls preserved where only the binding was dead
(e.g. web_server proc = _spawn_hermes_action -> bare call).
2026-07-29 11:53:39 -07:00
teknium1
c2eda92fd0 perf(config): stop deepcopying config on per-turn read-only paths
Four hot-path consumers paid a full config deepcopy per read:

- telemetry gate relay_shared_metrics.enabled() — runs 2-3x per agent
  turn (2x per API call from lifecycle hooks + 1x per tool call) and
  called read_raw_config(), which deepcopies the whole raw config every
  call. New read_raw_config_readonly() serves the cached dict directly:
  248 us -> 4.6 us per call (54x) on Teknium's real 77-key config.
- interruptible_streaming_api_call local-endpoint stale-timeout branch
  called load_config() once per API call for every local-model user.
- gateway get_inbound_media_max_bytes() + _get_ephemeral_system_ttl_default()
  called load_config() on per-message paths. All three switched to
  load_config_readonly() (345 us -> 12 us; PR #28866 lineage).

Together these account for ~90% of the ~1,900 deepcopy primitives per
turn measured in the 26-call stubbed-LLM profile.

read_raw_config_readonly() keeps the (mtime_ns, size) freshness key so
config edits are picked up next call, and preserves the identity
invariant (cache-miss returns the same object later hits serve) —
regression-tested with 'is', per the PR #28866 identity-bug lesson.
The mutable read_raw_config() is unchanged for save-path callers.

581 targeted tests green (config, relay metrics x2, ephemeral reply,
platform base, new readonly suite).
2026-07-29 11:33:41 -07:00
teknium1
eff3b11eb2 refactor: complete approval mode/timeout resolution migration to tools/approval.py core (TUI + codex surfaces)
TUI (tui_gateway/server.py _load_approval_mode): now delegates to
tools.approval._get_approval_mode instead of re-reading config raw via
_load_cfg + _deep_merge(DEFAULT_CONFIG, ...) and normalizing locally.
Behavior fix, not pure refactor: the canonical load_config path applies
managed-scope config overlays and ${VAR} env expansion, plus a legacy
max_turns lift, which the TUI's raw YAML read bypassed — under a managed
config that sets approvals.mode, the TUI previously reported/toggled a
different mode than the approval gate actually enforced. Both surfaces
now agree by construction. Name/signature and the mode-vocabulary clamp
are preserved.

Codex (agent/transports/codex_app_server_session.py): read confirmed the
_decide_exec_approval/_decide_apply_patch_approval paths carry NO
Hermes-side mode/timeout reads — the Hermes resolution already flows in
from agent/codex_runtime.py via tools.approval.is_approval_bypass_active()
(auto_approve_* routing) and via the shared approval-gate callback. So no
code extraction was needed; added docstrings pinning that invariant and a
cross-reference on the protocol-semantic choice mapping
(_approval_choice_to_codex_decision), which intentionally stays local.

Adds tests/tools/test_approval_mode_parity.py: cross-surface invariant
test asserting the core resolver, the TUI path, and the codex bypass
derivation agree for synthetic configs (unset defaults, global mode set,
YAML-bool off, malformed values, whitespace/case), plus a delegation-seam
test proving the TUI has no independent config read left.

Note: gateway/run.py has sibling raw reads but is intentionally untouched
(multiple in-flight PRs); flagged as follow-up.
2026-07-29 11:19:24 -07:00
teknium1
bc747001ee perf(imports): lazy-load heavy SDKs off the cold-start waterfall
Four deferrals following the established truthy-skip / PEP 562
lazy-load patterns (PRs #22681/#22859 lineage). Rebased over #74194,
which independently landed the browser_tool half of this work — that
file is dropped here; the remaining four modules are untouched by it:

- tools/vision_tools.py: defer agent.auxiliary_client
  (credential_pool -> hermes_cli.auth -> httpx -> rich, ~50 ms) to
  first vision handler call. async_call_llm /
  extract_content_or_reasoning stay patchable module attributes;
  injected test mocks win over the loader.
- agent/model_metadata.py: defer 'requests' (+urllib3, ~27 ms of the
  'import cli' waterfall) to the fetch functions. PEP 562 __getattr__
  keeps patch('agent.model_metadata.requests.get') working.
- tools/browser_supervisor.py: websockets (~22 ms) imports on first
  CDP connect; ClientConnection type under TYPE_CHECKING.
- cron/jobs.py: croniter (~15 ms) resolves on first cron-expression
  use; HAS_CRONITER stays monkeypatchable (None = unprobed sentinel).

A/B vs current main incl. #74194 (median of 7, cold subprocess):
  import cli          147 -> 132 ms  (-10%)
  import model_tools  244 -> 224 ms  (-8%)
  import run_agent    264 -> 244 ms  (-8%)

Lazy-verify: importing the four modules no longer pulls requests /
croniter / websockets into sys.modules. 369 targeted tests green
post-rebase.
2026-07-29 10:54:04 -07:00
teknium1
d7a4065568 perf(local-endpoints): disk L2 for server-type + ollama ctx probes, faster timeouts
Local-model users paid a fresh probe waterfall on EVERY CLI cold start
inside AIAgent.__init__: detect_local_server_type (up to 4 HTTP GETs,
2s timeout each on a hung server) + /api/show (3s timeout). The
existing caches were in-process only, so back-to-back invocations
(chat -q, cron ticks, subagents) re-paid the network every time.

- New 300s-TTL disk L2 at HERMES_HOME/cache/local_endpoint_probes.json
  for detect_local_server_type verdicts and query_ollama_num_ctx
  results. Only SUCCESSFUL probes persist (a down server never pins a
  negative verdict); stale entries pruned on write; corrupted cache
  degrades to a miss; atomic writes. 300s is strictly fresher than the
  1h in-process TTL that already accepts server-swap staleness.
- models.dev fetch timeout 15 -> (5, 10) connect/read tuple: a
  blackholed connect stalled the first-turn critical path 15s; now
  fails in 5s (matches the OpenRouter fetch convention, #46620).
- _auto_detect_local_model timeout 5 -> (2, 3): runs inside
  _get_model_config() at startup against a LOCAL endpoint; a hung local
  server cost 5s before the banner.

E2E (real HTTP server, two fresh subprocesses, isolated HERMES_HOME):
proc1 = 2 HTTP hits, proc2 = 0 HTTP hits, identical results
(ollama/131072), probe wall 74.5 -> 35.5 ms. 222 targeted tests green
incl. 9 new disk-L2 contract tests.
2026-07-29 10:52:13 -07:00
teknium1
0034da2975 perf(stream): replace per-chunk repr() with delta-length byte estimate
The streaming hot loop computed len(repr(chunk)) on EVERY chunk to feed
the retry-diagnostic byte counter — a full recursive pydantic repr at
5.5-8.8 us per chunk (measured), ~20-30 ms of pure CPU per 3,000-chunk
response, paid on every streaming response on every platform.

New _estimate_chunk_bytes() sizes the chunk from its delta payload
strings (content / reasoning / tool-call arguments) plus a 40-byte
framing floor: 2.1-2.4 us per chunk (~3x cheaper), independent of
pydantic field count, never raises on unknown shapes (Anthropic events,
stub providers fall back to the floor). Both call sites switched
(chat-completions loop + anthropic event loop).

The counter feeds only the stream-retry diagnostic log line
(agent/stream_diag.py) — an estimate proportional to traffic preserves
its purpose (distinguishing 'died at 0 bytes' from 'died mid-stream').

6 new contract tests; 64 targeted stream tests green.
2026-07-29 10:21:59 -07:00
teknium1
3d48f893da refactor: single build_subprocess_env() factory for all child-process spawns (profile + secret-scrub single owner) 2026-07-29 10:14:11 -07:00
teknium1
6f7f7cd064 refactor: use shared strip_ansi for inline ANSI regexes 2026-07-29 10:13:50 -07:00
teknium1
7c198c5e44 refactor: single shared Retry-After parser 2026-07-29 10:13:50 -07:00
teknium1
19055492aa fix: route stray HERMES_HOME hardcodes through get_hermes_home() (profile + native-Windows safety) 2026-07-29 09:33:48 -07:00
victor-kyriazakos
1773752c8c Merge remote-tracking branch 'origin/main' into feat/gateway-health-diagnostics-monitoring
# Conflicts:
#	uv.lock
2026-07-29 15:37:14 +00:00
kshitijk4poor
222ea2b6c9 refactor: fold simplify-code review findings
- extract _commit_registry/_note_refresh_failure shared by the background
  worker and foreground stage-4 (identical 4-step success + failure paths
  were duplicated); worker now commits under _models_dev_fetch_lock so a
  failing background refresh can never re-arm the backoff immediately
  after a successful force_refresh committed (unsynchronized-write race)
- add should_clear_context_pin_async to hermes_cli/route_identity.py
  (matching the get_model_context_length_async precedent) and use it at
  the 4 async gateway sites instead of inline asyncio.to_thread wraps;
  the sync _format_session_info site keeps the sync call (already
  off-loop via its callers' to_thread)
- test the background-refresh success path (the PR's primary new
  behavior): disk saved, mem cache swapped, backoff cleared, in_flight
  reset — mutation-checked
- replace the race-prone spin-wait on _models_dev_refresh_in_flight with
  a named-thread join in the backoff test
2026-07-29 17:13:48 +05:30
kshitijk4poor
ccf7129ed0 fix: restore zero-arg fetch_models_dev call on default paths
The branched call shape in get_provider_info/get_provider is deliberate:
~69 test sites across tests/hermes_cli and tests/gateway monkeypatch
fetch_models_dev (and get_provider_info) with zero/single-arg lambdas.
Passing allow_network= unconditionally broke 5 tests in CI slices 2/3/7.
Documented the constraint inline.
2026-07-29 17:13:48 +05:30
kshitijk4poor
11ca7eedf0 fix: follow-up hardening for salvaged #73621 + #35853
- _mark_stale_cache_grace only moves cache_time forward so a completed
  background refresh is never rewound to a 5-minute grace window
- clear _models_dev_refresh_in_flight if Thread.start() raises so a
  one-off thread-exhaustion failure doesn't disable refresh forever
- move empty-registry validation into _fetch_models_dev_from_network
  (was duplicated in the background worker and the foreground fetch)
- pass allow_network through as a plain kwarg in get_provider_info and
  hermes_cli.providers.get_provider instead of the branched call shape
- refresh the stale module docstring (no bundled snapshot exists; the
  resolution order now describes stale-serve + background refresh)
2026-07-29 17:13:48 +05:30
zapabob
a479a1599f fix(models): use stale cache before models.dev refresh 2026-07-29 17:13:48 +05:30
StellarisW
8c50aaceb6 fix(gateway): keep models.dev refreshes off event loop 2026-07-29 17:13:48 +05:30
kshitijk4poor
24a56f027c fix(lsp): expose lsp.idle_timeout in config, harden reaper, log reaps
Follow-ups on top of @DonutsDelivery's salvaged reaper commit (#64141):

- create_from_config() now parses lsp.idle_timeout (invalid values fall
  back to DEFAULT_IDLE_TIMEOUT) — previously the constructor knob was
  unreachable from config.yaml (config exposure adapted from #36892 by
  @0xbWy and #68091 by @9miya20)
- canonical default declared in hermes_cli DEFAULT_CONFIG so config
  discovery surfaces the knob (per sweeper review note on #64980)
- reaper loop survives transient sweep errors instead of dying and
  silently re-opening the leak (gap flagged in #68091 review)
- eventlog.log_reaped(): one INFO line per sweep + clears the
  log_active announce cache so respawns re-announce at INFO
- docs: replace the stale 'no idle-timeout reaper' paragraph with the
  new lifecycle description + config reference
- tests: reuse-refresh protection (the regression teknium's sweeper
  requested on #64141), reaper-survives-error, config propagation,
  invalid-value fallback, DEFAULT_CONFIG/manager-constant sync
2026-07-29 17:11:30 +05:30
DonutsDelivery
d7578018c5 fix(lsp): reap idle language servers 2026-07-29 17:11:30 +05:30
Teknium
63fc810b95 fix(gemini): sweep hardcoded Gemini default models to gemini-3.6-flash (#32360)
gemini-2.5-flash shuts down Oct 16 2026 (Google deprecation schedule)
and gemini-3-flash-preview is superseded. Update every hardcoded
default to the current GA flash model:

- gemini_native_adapter: probe_gemini_tier + _create_chat_completion
  default params; free-tier guidance de-pinned from a specific model's
  RPD number so it doesn't stale again
- auxiliary_client: gemini/kilocode fallback aux models,
  _OPENROUTER_MODEL, _NOUS_MODEL -> google/gemini-3.6-flash
  (verified live on both OpenRouter and Nous portal /models)
- provider plugins: kilocode + vertex default_aux_model
- hindsight memory plugin: gemini provider default
- setup wizard gemini list: 3-flash-preview -> 3.6-flash (matches the
  curated picker catalog)
- tests: aux-client assertions that pinned the old default literal now
  reference the _NOUS_MODEL constant, so the next default bump can't
  break them (change-detector cleanup)

Fixes #32360.
2026-07-28 18:17:56 -07:00
Atakan
01cb38e8ec fix(gemini): preserve bridged tool response name 2026-07-28 14:37:56 -07:00
Siddharth Balyan
3cedac00b7
fix(credits): reintroduce grant_spent, gated on an in-session crossing (#73634)
* Revert "fix(credits): remove the 'Grant spent · $X top-up left' notice"

This reverts commit 5dc6a14c14.

* fix(credits): reintroduce grant_spent behind an in-session crossing gate

The removed notice nagged because its condition is a steady state for
accounts living on top-up, the latch is per-session, and the cold-start seed
runs the policy at every session open — every session re-announced
'Grant spent · $X top-up left'. Reintroduce it gated so only a session that
WATCHES the grant run out announces:

- seen_grant_unspent crossing gate, mirroring seen_below_90: opens when the
  session observes the grant meaningfully unspent (>= GRANT_UNSPENT_MIN_MICROS,
  1 cent — portal-seeded states derive micros from float dollars and can carry
  sub-cent residue where headers report exactly spent). Seeds never prime it.
- The gate guards only the show branch and is consumed by the announcement —
  one announcement per crossing. Header flicker (used_fraction None and back)
  clears the sticky line but cannot re-announce; a renewal re-opens the gate.
- new_credits_latch() centralizes the latch shape (agent build, lazy re-init,
  and the policy test helper all build through it).
- Tests: steady-state-open stays silent (seed + policy seams), live crossing
  announces once, flicker/renewal/residual cases locked; the rendering test
  primes the gate and asserts its leg count so a gate regression cannot
  silently shrink coverage.
2026-07-28 21:36:23 +00:00
atakan g
0c2d9aee0b fix(model-metadata): prefer local Ollama num_ctx 2026-07-28 14:18:18 -07:00