Commit Graph

4359 Commits

Author SHA1 Message Date
teknium1
cca83848ec chore(memory): holographic leaves core for its community catalog plugin (re-land #136424)
Holographic has a standalone third-party maintainer (itpartypattaya) and a
reviewed catalog entry, so it leaves core now rather than on Oct 15. Restores
9e827685f2 exactly: reverts the revert (435b7bd3a7) and its health waivers
(b9ecc727e9). byterover and retaindb have no maintainer and stay bundled until
the announced date.
2026-10-10 21:18:25 -07:00
teknium1
435b7bd3a7 Revert "chore(memory): holographic leaves core for its community catalog plugin"
This reverts commit 9e827685f2 (#136424). It landed before the announced
October 15, 2026 date for holographic leaving core; it re-lands on or after
that date. Restores plugins/memory/holographic and its tests/docs.
2026-10-10 20:58:02 -07:00
Teknium
42f210de11 fix: Alibaba's content guardrail ends the turn instead of a 4-minute retry loop
Qwen via OpenRouter / the Nous Portal relays Alibaba Model Studio's output
guardrail as an in-band 502 marked provider_unavailable ("Output data may
contain inappropriate content."), and its input guardrail as DashScope's 400
data_inspection_failed. Neither phrase was a content-policy pattern, so the
502 rode the server-error retry ladder and Hermes re-sent the same content
into the same refusal. Both now classify as content_policy_blocked.
2026-10-10 20:44:53 -07:00
Teknium
9e827685f2 chore(memory): holographic leaves core for its community catalog plugin
Holographic now ships from itpartypattaya/hermes-plugin-holographic through the
plugin catalog (plugin-catalog/holographic.yaml, v0.6.1 @ 46b6f908, a fork of
Nous's archived handoff copy). It keeps the provider name, memory_store.db and
plugins.hermes-memory-store, and memory_provider_migration already lists
holographic in _LEFT_CORE. Deleting the in-tree copy and its tests lets
`hermes update` and agent start install the catalog plugin for every home that
has memory.provider: holographic.

Host code that still releases memory-store handles on profile unserve/delete
resolves the store through import_provider_module, the same way profiles.py
already does, and no longer imports plugins.memory.holographic. Tests that used
holographic as "a bundled provider" now use byterover. The journal-mode
contract drops the holographic row because the plugin owns that store now.

People who installed the archived Nous repo by URL have no catalog record, so
`hermes plugins update` kept pulling the archive. read_catalog_sidecar now
adopts an install from that repo as the holographic catalog entry, and update
re-pins it to the maintained repo at the reviewed sha. That catalog re-pin
also needed the reviewed-pin scan trust that catalog installs already get. Without
it, update_plugin scanned the reviewed tree as an unreviewed community source
and blocked on caution findings, and that blocked every catalog re-pin of
holographic (the reviewed 46b6f908 tree has 14 caution findings).
2026-10-10 20:31:02 -07:00
Teknium
1f2b9e0864 test: LSP hard-kill test no longer races its own cleanup
The descendant can exit between is_running() and kill(); psutil then raises
NoSuchProcess in the finally block. Killing a gone process is the goal.
2026-10-10 19:22:07 -07:00
Teknium
df3c21400d fix: /steer, /branch and compaction accounting keep reasoning intact
- A steer merged into a tool_result turn no longer ends the in-flight tool loop,
  so last-turn-only Claude models keep the loop's thinking.
- CLI /branch copies reasoning_content like the gateway and TUI branches do.
- The preflight estimate prices Gemini/Copilot signatures and the Converse
  sidecar at 0 like the compaction tail walk; a mismatch loops compaction.
2026-10-10 19:22:07 -07:00
Teknium
37d6492474 fix: encrypted reasoning items are never deleted from retained history
Compaction pruned every earlier-turn codex_reasoning_items on routes assumed not
to read them (a cost choice, #71058), and the first invalid_encrypted_content
rung deleted foreign-issuer items the failing request never sent, then retried a
byte-identical request. Compaction now drops only shadowed checkpoints, and the
rung strips only unverified items that were actually replayed. The request
builder already filters items per issuer and model on the wire.
2026-10-10 19:22:07 -07:00
Teknium
d6f3199584 fix: Bedrock Converse reasoning survives a reload from state.db
Signed thinking lived only in the live bedrock_content_blocks sidecar, which is
never persisted, so a reloaded Claude or gpt-oss turn replayed no reasoning.
Converse reasoning is now stored in reasoning_details in Anthropic shape and the
converter reads it back: signed for Claude, readable text for other models.
2026-10-10 19:22:07 -07:00
Teknium
f691e7b7fb fix: Gemini/Copilot signatures survive reloads; an error body can't switch off DeepSeek replay
Route shaping dropped reasoning_details on routes that do not read the array
(Gemini, Vertex, Copilot, custom) before shape_wire_carriers could lift the
private native_assistant record out of it. Every reloaded turn (each gateway
turn, every --resume) lost the signature and flipped the cached prefix. The
private records now survive shaping; the transport still filters them per route.

The field-rejection recorder matched generic phrases ('not allowed', 'is
unsupported'), so a value complaint like 'reasoning_content is not allowed to be
empty' was recorded as a schema rejection and persisted: every later tool turn
then 400'd. Only unknown-field phrasing counts now, a must-echo route never
records reasoning_content, and the recorder runs on chat_completions only.
Also restores the stealth/missingno must-echo pad (f86276d2eb) as a table row.
2026-10-10 19:22:07 -07:00
Teknium
86aa201bba fix: compaction salvage no longer strips reasoning from retained turns
salvage_grown_transcript (the anti-growth pass at the compaction commit site)
popped reasoning / reasoning_content / reasoning_details from every assistant
turn but the newest, and pruned every prior-turn codex_reasoning_items, before
the result was committed as canonical history. With replay on by default that
is a permanent continuity loss on exactly the long sessions compaction serves.
Reasoning text is now never a salvage shrink target (tool output and summaries
still are), and codex items follow compress()'s route rule
(route_reads_prior_turn_reasoning).
2026-10-10 19:22:07 -07:00
Teknium
3a53716ce4 fix: OpenAI and xAI models keep reasoning across turns and through compaction
Teknium ruling: every model's reasoning is replayed wherever the provider can consume it.
Live consumption matrix (api.openai.com, api.x.ai) showed four places Responses routes lost it:

- transports/codex.py: send reasoning.context="all_turns" on api.openai.com for the
  gpt-5.4 / 5.5 / 5.6 / 6.x families (table in codex_responses_adapter). gpt-5.4/5.5 default
  to current_turn and discard replayed earlier-turn blobs without it. o-series, base
  gpt-5-mini/nano and gpt-5.1-5.3 400 on it and are absent from the table; the openai-codex
  backend and every other host are unchanged. A 400 naming reasoning.context omits it for
  that model for the session and retries once (ahead of the reasoning-mandatory rung, which
  would otherwise drop the whole reasoning config).
- context_compressor.py: compaction keeps earlier-turn codex_reasoning_items when the route
  reads them (xAI, Meta, all_turns OpenAI models: route_reads_prior_turn_reasoning). The
  newest-checkpoint rule (#102374) still applies on every route.
- codex_responses_adapter.py: on api.openai.com a blob minted by a sibling variant of the
  same family replays (gpt-5.6-luna -> terra, 5.4 <-> 5.4-mini, 6-astra <-> 6.1-sol all
  rendered live); exact-model matching everywhere else.
- turn_recovery.py: the first invalid_encrypted_content rejection now drops only blobs not
  stamped by exactly this endpoint+model; the full strip / replay disable runs only when
  there is nothing unverified to drop or the narrowed retry fails again.
2026-10-10 19:22:07 -07:00
Teknium
2b2e4d908d fix: Gemini and Copilot keep their reasoning carriers across turns
Gemini text-turn thought signatures (native thoughtSignature on a text or
trailing empty-text part; OpenAI-compat/Vertex message-level
extra_content.google.thought_signature) were discarded at capture, and the
Copilot /chat/completions reasoning_text / reasoning_opaque fields and the
Gemini-3 function.thought_signature were never read. Replaying the Gemini
text-turn signature makes the server rehydrate the turn's thoughts (live:
31 -> 237/250 prompt tokens).

agent/reasoning_carriers.py captures them (stream + non-stream), stores
them top-level on the live dict plus one private
hermes.native_assistant reasoning_details record (persisted in the
existing column, so resume/gateway replay keep them), and lifts each one
onto the wire only for the route that reads it: extra_content on the
same Gemini gate as tool-call extra_content, reasoning_opaque/text and
function.thought_signature only to githubcopilot.com. The private record
is never forwarded (no profile declares that type). Copilot reasoning_text
is shown as the turn's reasoning.
2026-10-10 19:22:07 -07:00
Teknium
8ed7d31b20 fix(anthropic): Haiku 5.5 thinks adaptively
A blanket "haiku" skip in _thinking_kwargs sent Haiku 5.5 no thinking at
all; Anthropic and Bedrock list it as an adaptive-thinking model (live:
the Portal Messages route returns a signed thinking block for it, and
rejects manual thinking for Haiku 4.5, which keeps the skip).
2026-10-10 19:22:07 -07:00
Teknium
da9c15942e fix(bedrock): Anthropic-shaped thinking stays Claude's on Converse
Only Claude reads the anthropic_content_blocks / signed reasoning_details
carriers on Converse: they hold Claude signatures, which other Converse
models reject. Non-Claude models keep base behaviour for those carriers.
2026-10-10 19:22:07 -07:00
Teknium
3e31dc2239 fix(bedrock): Claude reasons over Converse and replays per-model reasoning
Converse (the bearer-token Claude path and the sticky fallback after a
stream-denied AnthropicBedrock error) never sent a thinking switch, so
Claude and Nova 2 produced no reasoning there; the fallback carrier path
also dropped Anthropic-shaped signed thinking and every prior turn's
reasoning went to models whose docs say to remove it.

- build_converse_kwargs / BedrockTransport.build_kwargs take
  reasoning_config: Claude gets the same thinking/output_config the
  AnthropicBedrock path sends inside additionalModelRequestFields (manual
  budgets add temperature 1, room above the budget and the interleaved
  beta); Nova 2 Lite gets reasoningConfig (maxTokens unset at high, per
  AWS). Other models get nothing.
- _assistant_blocks converts an anthropic_content_blocks sidecar (signed
  thinking -> reasoningContent.reasoningText{text,signature}, redacted ->
  redactedContent, tool input re-sourced from the redacted tool_calls) and
  maps signed thinking in reasoning_details, both ahead of toolUse.
- Per-model replay table for turns before the in-flight tool loop: Claude
  verbatim; DeepSeek-R1 / Kimi K3 strip (AWS docs; K3 raises
  InternalServerException); everyone else readable text only, never a
  signature or redacted blob from another model. The in-flight loop always
  replays as captured.
- chat_completion_helpers._build_bedrock_kwargs forwards reasoning_config
  (two-line hunk).
- provider_wire_snapshot: bedrock high now carries the thinking fields.
2026-10-10 19:22:07 -07:00
Teknium
33d6139ccd fix(anthropic): replay signed thinking on Bedrock, Vertex, Foundry and MiniMax
The "not anthropic.com => third_party => strip all thinking" catch-all
dropped Claude's signed thinking on every Anthropic-signed cloud platform
and MiniMax's own signed blocks. Anthropic documents signatures as valid
across the Claude API, Bedrock and Google Cloud, and Bedrock requires the
last tool-use turn's thinking back; MiniMax's Anthropic API asks for its
thinking blocks "unchanged in later turns". Without them manual-budget
models silently ran tool-loop continuations with thinking disabled and
adaptive models lost their interleaved chain and cached prefix.

- anthropic_endpoints: _is_bedrock_runtime_endpoint / _is_vertex_ai_endpoint
  / _is_claude_platform_endpoint (hostname-matched, lookalikes rejected).
- anthropic_thinking_policy: route resolution is a first-match rule table;
  Claude platforms -> native, MiniMax /anthropic -> new "minimax" route
  (replay verbatim on every turn). SIGNED_REPLAY_ROUTES drives the existing
  signature-400 self-heal (tracks_rejected_thinking) for both. Unknown
  relays still strip; DeepSeek keeps unsigned-only on any non-Anthropic
  host (Claude platforms included); Kimi unchanged.
- Haiku 5.5 is keep-all per Anthropic's preservation table: the last-turn
  regex now only matches Claude 3.x; Haiku is version-gated (>= 5.5).
- Last-turn-only models keep thinking on every assistant turn of the
  in-flight tool loop (after the last non-tool-result user message), not
  just the latest assistant message. Older turns stay stripped so the
  cached prefix only changes where the API itself drops them.
- AnthropicTransport.convert_messages forwards model= so preserving models
  are not treated as last-turn-only on that entry point.
2026-10-10 19:22:07 -07:00
Teknium
f89688440e fix: a learned reasoning-field rejection survives a resumed process
A strict lane (the Portal's Fireworks :US models) rejects `reasoning` /
`reasoning_details` by name. The in-memory record stopped the 400 for the
rest of the process, but `hermes chat --resume` (or a gateway restart)
re-learned it with another 400 on its first request. The record now lives in
the session's model_config (same seam as the Anthropic rejected-thinking
fingerprints) and is loaded once per session_id. Still keyed by
(provider, host, model), so sibling models keep every carrier.
2026-10-10 19:22:07 -07:00
Teknium
336b11584c fix: replay prior reasoning to every chat-completions route that can read it
Every model reasons and intends that reasoning to be replayed, but the
chat-completions wire echoed reasoning_content only to DeepSeek/Kimi/MiMo/
Ollama (plus a hardcoded Portal stealth model) and reasoning_details only to
OpenRouter; turn_context popped `reasoning` everywhere, so Ollama's /v1
(reads only `reasoning`) never saw any.

- message_sanitization owns one route->carrier table
  (_REASONING_CARRIER_ROUTES): strict schemas (Mistral, Groq, Cerebras,
  SambaNova) get nothing, Fireworks reasoning_content only, OpenRouter-format
  gateways (OpenRouter, Kilo, Vercel AI Gateway, Nous Portal) all three,
  Novita/MiniMax /v1/TokenHub + reasoning_details, local/Ollama/Upstage +
  reasoning, everyone else reasoning_content.
- The " " pad stays limited to the must-echo tier (DeepSeek, Kimi, MiMo);
  Ollama leaves it (no echo requirement) and the stealth hardcode is gone.
- Reactive recovery: a 4xx unknown-field rejection that NAMES a reasoning
  key this request carried is recorded per (provider, host, model) and the
  retry re-shapes api_messages without it. Generic upstream errors never
  count; a recorded key falls through, so no loop. History is untouched.
- Nous Portal reads reasoning_details again: the #118182 "replay budget"
  does not exist in the Portal and does not reproduce live.
- Gemini thought_signature survives on Gemini hosts/providers even when the
  model id is an alias.
- Vendor keep-reasoning flags (docs-only): GLM thinking.clear_thinking=false,
  DashScope preserve_thinking / clear_thinking, Kimi K2.x thinking.keep=all.
- stale_thinking_reaches_wire follows the same table, so compaction trigger
  and tail walks agree with what is sent.
2026-10-10 19:22:07 -07:00
ethernet
9456670d2f Merge pull request #135743 from NousResearch/harder-linter-fixes-part3
Harder linter fixes part 3
2026-10-10 17:55:39 -04:00
kshitijk4poor
a62979dc36 fix(compression): a refused candidate's attempt record claims no effect
Review thread on #136050 (agent/context_compressor_telemetry.py, "Refusals
report unsaved effects"). The compressor records a candidate's effect
fields before the commit checks run. When the growth guard refused the
candidate, or a database failure rolled the transcript back, the aborted
record still reported its `messages_after`, `tokens_after`,
`tokens_reclaimed` and `items_dropped`. That claimed reclaimed tokens for a
conversation that stayed unchanged.

Drop those fields from any record that neither committed nor rewrote the
history. The input-side counts stay.
2026-10-10 20:18:10 +05:30
kshitijk4poor
b862bfaaf0 fix(compression): keep a running attempt's record when an overlapping call stops at a gate
Review threads on #136050 and #136051 (agent/conversation_compression.py,
"Overlapping calls erase attempt details" / "Skipped calls erase active
records"). `_begin_compression_attempt` cleared the compressor's three
attempt-telemetry fields and rewrote its seed at entry, before the
automatic gate and the session lease. A second automatic call that stopped
at the gate (e.g. a cooldown while overflow recovery ran with
`bypass_cooldown=True`) therefore erased the running attempt's record. That
attempt could still commit, but logged `method: none` with no counts and no
fallback class, so a fallback commit read as a clean shared-metric success.

The previous-attempt clear (#118580) now runs in `_run_summary_phase`, under
the lease, just before the summary dispatch. The same statement writes this
attempt's own seed, so the entry-time seed write goes. Calls that stop at a
gate, the lease, or a stale snapshot never touch the shared fields.

A rewrite that survives a later bookkeeping failure (`failed_not_indexed`
after the atomic archive) also gets its counts finalized from the saved
transcript, using the same `rewritten` result that names the Relay mark
(thread "Saved tails disappear from counts" on #136051).
2026-10-10 20:18:10 +05:30
kshitijk4poor
5265f9e070 fix(relay): mark an in-place engine's archived compaction as a compaction
Review thread on #136083 (agent/conversation_compression.py, "Committed
rewrites look failed"). The commit-path attempt record decided
`history_rewritten` with `compressed is not messages`. A context engine may
compact its input list in place, so the returned list IS the input even
after `archive_and_compact` stored it. If a later bookkeeping write
(`update_system_prompt`) then fails, the record read
`history_rewritten: false`, the Relay mark was `compaction.attempt`, and
Relay skipped the history-freshness reset although the model now sees the
compacted transcript.

Use `commit.compacted_in_place`, which `_commit_compaction` sets on the line
after the atomic archive returns, alongside the identity check.
2026-10-10 20:18:10 +05:30
kshitijk4poor
62fb70d564 fix(compression): name the guard that blocked an overflow attempt
Overflow recovery calls the automatic gate with `bypass_cooldown=True`, so
the summary-failure cooldown cannot be what blocked it. But
`_compression_block_reason()` always reported the cooldown first, so with a
cooldown also active:

- the attempt record said `blocked:cooldown` while the structural backoff
  or the ineffective breaker was the real blocker, and
- `_mark_compression_blocked_transient` marked an `ineffective` block as
  transient, so overflow recovery deferred instead of counting toward
  exhaustion, which #97488 says must never happen.

`_compression_block_reason` takes the gate's `ignore_cooldown` flag.
`_automatic_compression_gate_blocks` reads the reason once, with the same
flag the gate got (older engines keep the no-argument calls), and returns it;
the transient signal and the blocked attempt record both use that one
reason, so the telemetry module no longer re-derives the guard order.
2026-10-10 20:18:10 +05:30
kshitijk4poor
760c93fef7 fix(shared-metrics): count an empty-transcript refusal under its own class
#135807 makes the empty-transcript refusal emit its attempt record through
the shared-metric lifecycle (on main that exit recorded nothing), but
`empty_transcript` was not in the closed `COMPRESSION_FAILURE_CLASSES` set,
so the `hermes.compression.count` row read `failed / other`. Add the class
to the contract, the v4 package schema enum and the metric docs so the row
says what happened.
2026-10-10 20:18:10 +05:30
Maciej Lisiewski
443ea25b57 ci: fix linter errors after rebase 2026-10-10 16:01:32 +02:00
Maciej Lisiewski
bda3aea496 ci: remove unused variables (linter) 2026-10-10 15:20:14 +02:00
Maciej Lisiewski
0006a5e229 ci: fix PLW1509, PLW1510, PLW2101 linter errors 2026-10-10 15:20:13 +02:00
Teknium
f925b01791 fix(relay): stop waiting on Relay after one stalled compaction mark
A wedged Relay pipeline made every compaction block for SCOPE_OP_TIMEOUT
(10s) while it still held the compression lease, with no breaker. The
first stalled mark now trips a process-wide flag and later marks return
immediately; marks are observability and must never tax compaction.

Also: make relay_runtime.SCOPE_OP_TIMEOUT public (relay_compaction reads
it), fold the commit-time telemetry hold into _hold_attempt_telemetry
with one _ATTEMPT_TELEMETRY_FIELDS tuple, and drop the mapping-table
change-detector tests (the ATOF freshness and failure invariants stay).
2026-10-10 04:27:27 -07:00
mnajafian-nv
a32f77e53f test(relay): make stalled mark assertion scheduler independent
The 50 ms publisher timeout could expire before its worker started, so
asserting that the worker entered the fake event made a correct timeout
look like a failure under load. Use an event to observe caller completion
and a two-second timeout consistent with the test timing rule.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit b25b6c3b0ce4b933b35d6cb5cee114b65ff097bf)
2026-10-10 04:27:27 -07:00
mnajafian-nv
3b146be475 fix(relay): bound compaction marks and skip unused prune estimates
A stalled Relay event could hold the compression lease while a mark was
published. Use the same timeout as other Relay scope operations so the
agent can continue after a stalled publisher.

A committed proactive prune estimated both full transcripts even when
no Relay scope could receive the mark. Check for a target before doing
that work.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 808eab610aa161875ad07c79947282fe54da0999)
2026-10-10 04:27:27 -07:00
mnajafian-nv
814cde892a fix(relay): mark a micro-compaction in a rotated child on the turn it ran in
The micro-compaction publisher resolved its Relay parent from the
compressor's current session only. After a rotating compaction earlier in
the turn that session is a child with no Relay scope, so the pass the
finalizer runs before the turn ends lost its mark, while the attempt, prune
and provider-native publishers already fall back to the session the turn
started in.

The finalizer now passes that session to the pass, and its mark takes the
same fallback.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 3519c942fad6ba2dce3b4475ca7ff56a02c7410f)
2026-10-10 04:27:27 -07:00
mnajafian-nv
f0afd34a9d fix(relay): keep the empty_transcript class on its attempt mark
The attempt record now logs empty_transcript when a context engine returns no
messages, but the hermes.compaction failure_class set did not list it, so the
compaction.attempt mark read failure_class: other and lost the reason. Add it
to the attempt-only classes and the documented set.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 0bdf840844f35e95361ae89edcb1264d2767663c)
2026-10-10 04:27:27 -07:00
mnajafian-nv
4b0245198c test(relay): publish one attempt mark when the Codex route raises
The Codex route now logs an attempt record when compact_thread() raises
or a required checkpoint refuses the route, and the record reaches the
compaction publisher like every other Codex exit. Pin that each raising
exit produces exactly one compaction.attempt mark (provider_native,
failed, failure_class exception) without the error text, so a later
change to the Codex route cannot drop these marks again.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 3a3b6b0bfeda336c0f1f118f8ff26eced9d67228)
2026-10-10 04:27:27 -07:00
mnajafian-nv
8810cf24b2 fix(relay): record why an overflow compaction ran in overflow_reason
overflow_reason is a v1 field, but it was always null: both overflow
callers passed only trigger="overflow", although each already holds the
error classifier's verdict for the provider rejection it recovers from.
A consumer could not tell a 413 from a context-length rejection or the
Anthropic long-context tier gate.

Both callers now pass the classified reason through _compress_context
into the attempt seed, so the attempt log line and the compaction mark
carry it. The mark keeps it in a closed set (context_overflow,
payload_too_large, long_context_tier, else other) and leaves it null for
every other trigger. The Relay docs list the set instead of calling the
field reserved.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit c3ad6f7fb8bfcc8f9c350f3a4272029d63333c7f)
2026-10-10 04:27:27 -07:00
mnajafian-nv
0edc904913 fix(relay): mark a second rotating compaction on the turn it ran in
With compression.in_place: false every commit moves the agent to a
child session, and that child has no Relay scope until a Relay-managed
LLM call or the next turn opens one. A multi-pass preflight rotates
twice before any LLM call, so the second attempt's record named a
session with neither a live turn nor a session scope, and its mark was
dropped at DEBUG.

The publisher now passes the session the agent's turn started in
(_inflight_turn_session_id, which the turn start already records because
compression can rotate mid-turn). _resolve_target keeps the same session's
live turn and then its session scope first; only when both are missing
does it fall back to that turn, and only while it is the live,
instrumented turn of that session. A child that already has a scope
keeps the mark there, because that scope owns the child's next LLM
starts and so must receive the freshness reset. The record's session_id
still names the child.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit af5c1c9bfde91105e88535a9235ceca6bd662887)
2026-10-10 04:27:27 -07:00
mnajafian-nv
5220f064c6 test(relay): call a failed summary attempt failed, not aborted
The two tests assert outcome failed (summary_generation_aborted is the
class, not the outcome), but their names and the ATOF module docstring
said aborted, which is a different v1 outcome.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit be76bc7f4ffad4b0bb580ad932979f7773056236)
2026-10-10 04:27:27 -07:00
mnajafian-nv
2159dc3e78 fix(relay): align the hermes.compaction v1 contract with what Hermes emits
Review of the first cut of the marks found four places where the record
said something the attempt did not:

- A split that failed after it had stored the compacted history
  (failed_not_indexed without a rollback) returns the compacted
  transcript, so the model sees it next, but the record read
  aborted/failed and was named compaction.attempt. Relay then never
  reset freshness and the compacted history was never recorded. The
  commit path now logs history_rewritten (whether the returned
  transcript replaced the input), and such a record is a committed
  compaction with failure_class session_split_failed and split_status
  failed_not_indexed.
- in_place and session_rotated read false/false on a commit without a
  session database. They are now set only when split_status is
  in_place_committed or rotated_committed, and null otherwise.
- trigger_class read auto for a record with no trigger or an unlisted
  one. It now reads unknown there; an unlabelled automatic caller
  (trigger_source auto) keeps trigger unknown with trigger_class auto.
- The v1 sets listed values Hermes never emits (kind request_truncation
  and window_reduction, scope request, trigger recovery, outcome noop,
  token_count_method provider_usage, split_status pending and unknown).
  Consumers treated them as live. They are dropped from the v1 sets and
  documented as reserved for later versions.

session_ownership_unreadable joins the attempt-only failure classes.

The Compaction Marks docs now list the four sources exactly, what does
not emit a mark yet (OpenAI Responses native compaction and its
pre-checkpoint pruning, image shrinking after a provider rejection,
request-only truncation), the closed sets and null policy, why a Codex
app-server compaction keeps the compaction name (Relay records no LLM
start for those turns, so the freshness reset only affects a later
Hermes-side call) and is never marked twice, and two known gaps around
rotating compactions.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 6e6bc81fc0e074c88d1d632e8110d7490e203015)
2026-10-10 04:27:27 -07:00
mnajafian-nv
1c97c0ec35 fix(relay): emit no compaction mark from an uninstrumented turn or a detached fork
A second concurrent turn on a live session (a background-review fork or
a concurrent gateway turn) runs with Relay instrumentation off, but its
compaction mark fell back to the live session's scope. The mark claimed
the live conversation was compacted when only the fork's throwaway
transcript was, and the reserved compaction name reset the live agent
scope's freshness. That also bypassed the opt-out resolve_execution_context
honors.

_resolve_target now returns nothing when relay_instrumentation_enabled()
is false. Publishing also skips persistence-detached agents
(_persist_disabled, set on background-review forks), as micro-compaction
already does in the turn finalizer.

The proactive prune mark moves from ContextCompressor to its caller in
turn_preflight, the one place that knows the agent. That lets it skip a
fork, name the agent's session (a compressor without a bound session id
used to drop the mark), and measure tokens with the same rough message
estimate the attempt record uses.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 37e177f1494570303f4a010534273a64760b30d3)
2026-10-10 04:27:27 -07:00
mnajafian-nv
a64501e60c feat(relay): emit compaction marks from the compression attempt record
Hermes rewrites a session's history in several ways (batch summary,
deterministic fallback, micro-compaction, proactive tool-result prune,
Codex thread compaction) but never told Relay about any of them. A Relay
consumer could not see what was compacted, why, when, how long it took
or whether it went through compress_context, and Relay never reset
LLM-history freshness after a rewrite, so LLM starts after a compaction
stayed projected to the current user turn.

agent/compaction_events.py builds one flat, content-free
hermes.compaction v1 record per rewrite with closed enums (kind, scope,
official, method, trigger, trigger_class, outcome, failure_class) and an
other fallback, plus the effect counts and timings the attempt record
already carries. agent/relay_compaction.py emits it with the existing
scope.event API on the owning session's scope stack: under the live turn
of the same session, else under the session scope, else not at all.

A committed history or provider rewrite uses Relay's reserved
compaction mark name, which resets freshness for the session's agent
scope; every other outcome is a compaction.attempt mark. Records come
from the single attempt telemetry seam (so blocked, Codex-routed and
aborted attempts are covered), the micro-compaction telemetry record,
the proactive prune commit and Codex-native compactions Hermes observes.
No payload carries message, summary, focus-topic, error or path text,
and publishing never raises into compaction.

The marks are on whenever the Relay runtime is live; Relay exporters are
configured separately, so this adds no outbound path and no config key.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit dc5fd0875e7ec895b7826ee829bfc1b7f082de97)
2026-10-10 04:27:27 -07:00
mnajafian-nv
43fda4b388 fix(compression): report committed effects without leaking metric state
Finalize effect counts from the transcript that crosses the commit boundary, including retained partial-compression tails and boundary anchors. Explicitly discard bypassed shared-metric state so a later refusal cannot consume stale attempt metadata.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit ddf3b229a9e2fdee651f36afbec974717d334f76)
2026-10-10 04:27:27 -07:00
mnajafian-nv
7ae7fac09c fix(compression): log one attempt record when an engine returns an empty transcript
The candidate guard refused an empty transcript from a context engine and
returned without an attempt record, so that compress_context call logged
nothing and the shared metric never counted it. Every other refusal in the
guard logs its own class.

Log it as an aborted attempt with failure_class empty_transcript; the shared
metric counts it once as failed under its closed-set "other" class.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 3f6a58fb3f2695a58d67fedb9f7410abff95ad58)
2026-10-10 04:27:27 -07:00
mnajafian-nv
c3af207f28 fix(compression): keep a late-unwinding attempt's own id, session and trigger
The attempt record read its identity from the agent's current-attempt fields,
which the next attempt overwrites when it begins. A stalled attempt that
unwound after its stall fallback had started therefore logged the fallback's
attempt_id, session_id and trigger_source, so two records claimed one attempt
and the stalled one disappeared.

Each attempt now carries its own seed on _Attempt and passes it to every
emit, so the record always describes the attempt that wrote it. Emits with no
attempt in hand (pool saturation) keep reading the agent's copy.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit a36db44d63888406702cab44d3ca29335727a6e2)
2026-10-10 04:27:27 -07:00
mnajafian-nv
319d2fb082 fix(compression): log one attempt record when the Codex route raises
Every returning exit of the Codex route logs one attempt record, but
two exits raised after the attempt began and logged nothing: an
exception from compact_thread() only stopped the heartbeat and
re-raised, and compress_context raised CompressionCheckpointUnavailable
for a Codex-owned thread with compression.checkpoint_required set. A
consumer counting attempts saw neither.

Both exits now log one codex_app_server record, aborted with
exception:<Type>, the same class shape the Hermes route records when its
summary phase raises. The record carries the type name only, never the
error text, and the caller still receives the original exception object.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 17e55b05801624ea16f034b31e2408d386ee19f0)
2026-10-10 04:27:27 -07:00
mnajafian-nv
30d2b1030b test(compression): cover the attempt record of a rotating session-backed commit
#118580's regression test covers an in-place commit. A rotating commit
runs the same memory flush and then switches the agent to the child
session, so its record could lose the attempt's fields or name the child.
Drive both commit modes through a real AIAgent with a session database
and assert the record keeps this attempt's trigger, method, effect and
regions, and names the session the attempt started in. Both cases fail
without the telemetry hold around commit_memory_session.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit be2006f29a758e9f4f7c380396a997b6377bef02)
2026-10-10 04:27:27 -07:00
mnajafian-nv
e862be076e test(compression): prove each automatic call site passes its trigger label
The attempt record learns trigger_source only from the trigger keyword
each caller passes to _compress_context. The existing test checked only
the shared-metric bucket mapping, so dropping a label from any of the
seven call sites passed every test.

Drive each real caller with an agent double that records the keyword:
idle, turn-start threshold, engine preflight, pre-API and post-tool in
the turn loop, and both gateway hygiene paths (the live Codex agent and
the detached hygiene agent). Removing any one label turns exactly its
case red.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 93604a6eb31b741605acedfa38ce21770f4cb187)
2026-10-10 04:27:27 -07:00
mnajafian-nv
490f4e4397 fix(compression): tell an unreadable session-ownership lookup from a lost one
_adopt_if_parent_rotated returns the same sit-out result for two
different exits: the lease found the parent already rotated by another
path (ownership lost), and the ownership lookup itself raised
("skipping compression this cycle", ownership unknown). Both were logged
as session_ownership_lost.

Each exit now writes its own record from inside the helper: a rotated
parent is skipped / session_ownership_lost, and a failed lookup is
aborted / session_ownership_unreadable, the same shape as the existing
cooldown_state_unreadable exit. Both records name the session the
attempt started in, not the child an adoption switched the agent to.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 83dfe1eaf6e630198734a65d8c13a26945ff43d9)
2026-10-10 04:27:27 -07:00
mnajafian-nv
4280f8b00e fix(compression): describe an aborted attempt from its own seed
An attempt record that is written before compress() seeds this
attempt's telemetry, or after a pre-commit restore, did not describe
that attempt:

- snapshot_stale and the early commit_fence_cancelled exit run after the
  attempt-start clear, so the record had no trigger_source;
- a commit fence refused after the summary restores the compressor's
  pre-attempt state, which puts the previous attempt's dict back, so the
  record carried the previous attempt_id, trigger, method and reclaimed
  tokens with this attempt's failure class;
- pool_saturated fires before any attempt begins, but still took the
  previous attempt_id from the agent.

_begin_compression_attempt now keeps the attempt seed (attempt_id, the
session the attempt started in, trigger) on the agent as well, since a
restore replaces the compressor's copy. When the compressor's telemetry
is not this attempt's own, the emitter starts the record from that seed
with method none and no token counts, and ignores the compressor's
fallback flags, which belong to the same earlier attempt. A refusal with
no attempt begun gets a fresh id and trigger unknown. Bypassed exits read
the same seed, so they also name the session the attempt started in
rather than a child it adopted.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit c29dba73a4c07c833951666f517a237e1e0fc65d)
2026-10-10 04:27:27 -07:00
mnajafian-nv
4c7ab8b2a0 fix(compression): name the automatic trigger and effect in attempt telemetry
The content-free attempt record is the one place that says why a
compaction ran and what it did, but it could not answer either question:

- Every automatic caller passed no trigger, so idle, turn-start
  threshold, engine preflight, pre-API, post-tool and gateway hygiene
  attempts all logged trigger_source=auto. Each call site now passes its
  own label; the shared metric still buckets them as auto.
- tokens after, message counts and how the summary was made were
  computed and logged as prose but never reached the record. A finished
  rewrite now records messages_before/after, tokens_before/after,
  tokens_reclaimed (signed; a summary can grow a short transcript),
  token_count_method=estimate_rough, method (llm_summary,
  aux_fallback_main, deterministic_fallback), items_dropped, and the
  tool_results_pruned / reasoning_items_pruned pass counts.
- Attempts that never reached the local compressor returned silently:
  the automatic breaker gate (before and under the lease), every Codex
  app-server exit, a lease that found the session already rotated, and
  an unreadable durable cooldown. Each now logs one record with a
  commit_status (blocked, skipped, failed, aborted, committed), a route,
  and a class such as blocked:cooldown. These stay out of shared metrics,
  which never counted them.

Test doubles for _compress_context gain the trigger keyword the call
sites now pass.

Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
(cherry picked from commit 1795993d1a01d91dd76e6ec824cb606f8a03b897)
2026-10-10 04:27:27 -07:00
Finn763
5ce01755b5 fix(compression): drop stale attempt telemetry before the next attempt
The commit-time hold keeps the telemetry trio for its own attempt's
success emit. Clear it at the start of the next attempt (and keep
pool-saturation refusal emits from inheriting the last attempt's
payload) so a later emit cannot report the previous attempt's numbers.

Follow-up on the #118580 review.

[Ported onto main, where _emit_compression_attempt_telemetry now lives in
agent/conversation_compression_telemetry.py: the include_last_telemetry
parameter moved there with the same behavior.]
(cherry picked from commit 1c05c6a6e713cd77aee3111d5a809572ad3f7d0c)
(cherry picked from commit 5a551af25a643d0d6c1f1ded2ce9d9c4a52276ba)
2026-10-10 04:27:27 -07:00
Finn763
dda4275846 fix(compression): hold attempt telemetry across commit-time memory flush
_commit_compaction calls commit_memory_session before the success emit.
The engine session-end notification answers with a full per-session
reset, nulling _last/_active_compression_telemetry, so the emit fell
back to {} and every committed line carried emitter defaults only.

Closes #118580

(cherry picked from commit b2242e0aa79dbc9ce2995e1a0cf8a88867869801)
(cherry picked from commit 865ace8293ec8138c309a02d8fc18b34f7e6fffb)
2026-10-10 04:27:27 -07:00