Commit Graph

212 Commits

Author SHA1 Message Date
DmytroVolodymyrson
a699234f81 fix(kanban): wake controllers on review changes
Deliver changes_requested review outcomes through kanban subscriptions and
wake the origin for notify+wake / wake modes. Review-specific only: no task
mutation. Reasons are redacted, path-scrubbed and truncated before delivery.

Salvaged from #88694; conflicts with the #87733 wake-kinds expansion resolved
keep-both.
2026-08-26 15:31:36 -07:00
Ben Barclay
a1a1bee5c2 Merge branch 'main' into feat/relay-slack-parity
One conflict, gateway/relay/adapter.py send_for_platform: main added the
turn-final draft-seal interception (_sfp_metadata with the _interim_send
marker stripped, seal-or-fall-through); this branch added format-hint
stamping on the same frame. COMPOSED: the plain-send frame now stamps
_with_format_hints_for_platform over _sfp_metadata (the stripped copy),
so both the seal fall-through contract and the cron-lane block hints
hold. Note: the seal frame itself (op:draft final) does not stamp hints
— cron sends are never open drafts, so the flagship path is unaffected;
noted as a connector-PR follow-up for streamed interactive finals.
2026-08-20 20:56:03 +10:00
Bryan Bednarski
6f7596e9a2 docs(relay): link supported observability exporters
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:04 -07:00
Alex Fournier
31402f630b fix(relay): complete native plugin cutover
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-08-19 08:52:03 -07:00
Bryan Bednarski
6ec2c0ba8b fix(relay): define process-wide profile policy
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:03 -07:00
Bryan Bednarski
3fad83df31 fix(relay): guard native plugin ownership cutover
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:03 -07:00
Bryan Bednarski
ad9fb060c5 fix(relay): clarify opt-in plugin layering
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:02 -07:00
Bryan Bednarski
8afd98ef2a refactor(relay): remove legacy observability plugin
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:02 -07:00
Bryan Bednarski
0a079b946f fix(relay): retain legacy observability plugin
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:02 -07:00
Bryan Bednarski
e8644e05a3 refactor(relay): remove legacy observability plugin
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-19 08:52:01 -07:00
Victor Kyriazakos
afcf4f5214 docs(relay): document the two new descriptor capability bits in the contract §2 table
test_contract_doc_conformance enforces that every CapabilityDescriptor
field appears in docs/relay-connector-contract.md §2 so connector authors
mirror the full surface — caught in CI (slice 2/12); the descriptor gained
supports_inchannel_continuable and supports_block_formatting without the
doc rows.
2026-08-19 14:25:16 +00:00
Nikola Hristov
d083b85591 feat(hooks): pre_tool_call content transformation via modify directive
Adds a `modify` response type to pre_tool_call hooks so a hook can
transform tool arguments before the tool executes, instead of repairing
results afterwards via post_tool_call.

- hermes_cli/plugins.py: _dispatch_pre_tool_call_hooks() fires hooks once
  and returns (block_message, modified_args); modify directives
  shallow-merge into an accumulated dict built from the original args.
- agent/shell_hooks.py: _parse_response() accepts both the canonical
  {"action": "modify", "args": {...}} and Claude Code-compatible
  {"decision": "modify", "tool_input": {...}} wire formats.
- model_tools.py, agent/tool_executor.py, agent/agent_runtime_helpers.py:
  dispatch sites migrated; modified args applied before execution.
- Docs + 10 new tests (merge semantics, precedence, block interplay).

Salvaged from PR #28953. Best fix for #18988.
2026-08-15 23:03:52 -07:00
David Metcalfe
da68ecf4b3 design(kanban): add side-by-side dialog prototype (4 variants)
Reference prototype for the upcoming 'replace window.confirm/prompt/alert in
the kanban plugin' work. Self-contained HTML — open in any browser, no build
step. Served at docs/design/kanban-dialogs/index.html.

Four variants side-by-side, each rendered in the same kanban context:
- A (Conservative): direct host ConfirmDialog mapping, minimal chrome
- B (Strong-fit, Pro's pick): textarea + SVG icon + inline validation
- B-refined (synthesis, recommended): auto-focus, dual-validation,
  cancellable spinner during PATCH, per-task summaries in bulk-many
- C (Divergent): undo toast for non-destructive moves

Decision matrix at the bottom of the page. Copy is verbatim from
web/src/i18n/en.ts (confirmDone, confirmArchive, confirmBlocked,
trash.confirm, completionSummary, etc.).

Design pass: Gemini 3.1 Pro (initial brief) + GPT-OSS 120B (cross-vendor
review). Not shipped to users — review reference only.
2026-08-15 00:33:32 -07:00
terry197913
4e1b2e436c fix: scope plugin manager by resolved hermes home (keyed cache)
fix: remove .codegraph artifacts from commit
2026-08-12 19:13:32 -07:00
Teknium
6bf93c0e38 feat(plugins): add namespaced config and durable state bridge 2026-08-12 16:26:43 -07:00
Topher Ross
223e74345e docs(rfc): align config bridge with #64227 — namespace jail, real APIs, acceptance criteria
- Replace cfg_set references with cfg_get (cfg_set deferred to #64227)
- Add Namespace jail section: key-prefix enforcement, cross-plugin rejection,
  path-traversal rejection, read allow-list
- Rewrite cron sketch: replace CronManager/command with create_job() using
  prompt/script params per cron/jobs.py:1039-1234
- Add #64227 to Related
2026-08-12 16:26:43 -07:00
Topher Ross
516ca4170a docs: plugin config & state bridge design proposal
RFC covering four PluginContext additions driven by kanban-advanced needs:
- ctx.get_config() / ctx.set_config() — typed config access
- ctx.register_config_schema() — schema validation surfaced to hermes doctor
- ctx.cron facade — cron CRUD without subprocess fragility
- provides_config_defaults in plugin.yaml — safe config defaults on install
2026-08-12 16:26:43 -07:00
hans
299996f7e0 docs(rfcs): plugin-architecture research spike — Pi and OpenCode lessons
Code-level analysis of Pi (earendil-works/pi @ eb79351) and OpenCode
(anomalyco/opencode @ c69abee) plugin architectures across the six
dimensions #64180 specifies, with a 13-row adopt/adapt/avoid table
mapped to #64164/#64161/#64162/#64165/#64229/#64230. Key findings:
neither system has hook timeouts (both shipped hang-class bugs),
OpenCode's permission.ask is typed-but-dead (hook wire-up drift),
Pi treats prompt-cache stability as API contract, and both systems
lack ADRs. Fixes #64180.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013b1XyXitAxV7phGmKWigJX
2026-08-12 16:25:01 -07:00
Ben Barclay
bb597e1c02 fix(cron): managed-cron fires execute in the gateway process (live adapters + dashboard forwarder) (#84339)
* fix(gateway): pass live adapters to cron fire webhook's fire_due

The Chronos fire webhook (/api/cron/fire) called
provider.fire_due(job_id, adapters=None, loop=loop), so every
externally-triggered fire delivered through the standalone path even
with a live gateway in-process. E2EE platforms and relay-fronted
logical platforms (whose ONLY send path is the live relay adapter — no
native credential exists on the box) failed every external fire with
"platform 'X' not configured/enabled", while the same job delivered
fine under the built-in ticker (gateway/run.py passes runner.adapters).

Resolve the runner (self.gateway_runner → app['gateway_runner'] →
_gateway_runner_ref(), the same chain the drain check uses) and forward
its adapters. No runner → adapters=None, preserving the historical
standalone path byte-identically.

Note: does not by itself fix Fly-hosted scale-to-zero deployments where
NAS's callback lands on the DASHBOARD process (internal_port 9119) —
_fire_cron_job_for_profile there has no gateway runner in-process. That
topology needs a separate fire handoff (design pending).

* fix(cron): dashboard forwards Chronos fires to the gateway (503 when unreachable)

The dashboard's /api/cron/fire executed cron jobs in the DASHBOARD
process via _fire_cron_job_for_profile with adapters=None. On hosted
deployments (Fly proxy exposes only the dashboard's port) that made
every managed-cron fire deliver through the standalone send path, which
cannot serve relay-fronted logical platforms (their only sender is the
live relay adapter in the gateway process — no native credential exists
on the box) or E2EE rooms. It also ran the whole agent turn inside the
dashboard: wrong process for memory/session ownership and fire-claim
attribution.

Restore the invariant that the GATEWAY owns cron execution:

- Dashboard route: after verifying the NAS JWT and resolving the job's
  profile, FORWARD the fire to the gateway api_server's own
  /api/cron/fire on loopback, NAS bearer preserved (the gateway
  re-verifies the JWT — defense in depth, no new trust link), and pass
  the gateway's response through. Gateway unreachable → 503 so NAS
  retries per the Chronos contract (non-2xx = retryable; the store CAS
  de-dupes the eventual double fire). Deliberately NO local-execution
  fallback.
- Endpoint resolution mirrors gateway/config.py's api_server load order
  per target profile (config.yaml extra.port → API_SERVER_PORT from
  process env or the profile's .env → 8642), with /p/<profile>/ prefix
  routing under multiplex.
- docker/stage2-hook.sh: generate a strong API_SERVER_KEY into .env on
  first boot when absent (never overwrites an operator value), so the
  loopback api_server passes its startup guard on hosted images. The
  fire route itself is NAS-JWT-authed; the key gates the rest of the
  api_server surface. The listener binds 127.0.0.1 by default and the
  Fly service exposes only the dashboard port.
- _fire_cron_job_for_profile kept but deprecated (late-binding seam
  compatibility); no route calls it.
- docs/chronos-managed-cron-contract.md: document the two-hop inbound
  topology and the 503-retry semantics.

Depends on the previous commit (fire webhook passes live adapters to
fire_due) — together they make NAS→dashboard→gateway fires deliver over
relay end to end.

* fix(cron): read the profile api_server port via the canonical config loader

CI guard test_config_read_guard flagged the new _gateway_fire_endpoint
for a raw yaml.safe_load of the profile's config.yaml — the exact drift
class the guard exists to kill (raw reads miss the managed-scope
overlay, ${ENV_VAR} expansion, and root-model normalization).

Read through load_config() under a HERMES_HOME override scoped to the
target profile instead (the same pattern the deprecated
_fire_cron_job_for_profile uses for its store scope), and pull the port
with cfg_get. Test updated to stub load_config rather than write a raw
config.yaml.

* fix(gateway): only messaging platforms count for the scale-to-zero arm gate

The stage2 hook now generates API_SERVER_KEY for every Docker container,
and key presence force-enables the api_server platform. The scale-to-zero
arm gate counted every enabled platform, so the loopback api_server
listener made messaging_is_relay_only_or_absent False on every hosted
instance — silently disarming the feature (the not-armed log would show
enabled platforms=['relay','api_server']).

The arm gate and the not-armed logger now share one helper that filters
to enabled MESSAGING platforms, excluding LOCAL/API_SERVER/WEBHOOK —
the same non-messaging exclusion set _connect_platforms already uses.
A genuinely enabled direct-socket platform (Discord/Telegram) still
disarms. Two of the three new tests fail without this fix.
2026-08-12 17:04:44 +10:00
Ben Barclay
a189286aea fix(relay): stop sibling gateways answering another instance's button press (#83677)
* fix(relay): stop sibling gateways answering another instance's button press

A Discord button press arrives on the passthrough plane, and the connector
fans a passthrough forward out to EVERY live gateway session of the tenant
(relayServer.routeBusMessage delivers `passthrough` via sessionsByTenant),
unlike a message, which it narrows to the admitted instance set. The prompt
went out from exactly one instance and _pending_prompts is process-local, so
every sibling gateway saw an answer for a prompt it never minted, could not
tell that from its own prompt expiring, and fell through to chat dispatch --
where the option-shaped text ("/c1") is not a real command and run.py replied
"Unknown command `/c1`". One copy per sibling, under the single real ack.

Prompt ids are now minted as `<per-process nonce>.<8 hex>`, so an answer can
be attributed to the process that minted it. A prompt answer is always
consumed, never re-dispatched as chat: a sibling's prompt and a repeat answer
are both dropped silently, and an expired prompt of our own gets a short
"no longer waiting" notice from the owning gateway only.

Ids stay inside the connector codec's contract ([A-Za-z0-9_.-], <=32 chars,
64-byte callback budget -- verified against promptCodec.ts: 52 bytes worst
case with a full-length option id). An id with no nonce segment (a prompt in
flight across an in-place upgrade) is still treated as ours.

Tests: 4 added, each verified to fail without the fix. Full relay suite green
(160 tests).

* style(tests): ruff-format the added relay prompt tests
2026-08-12 10:04:26 +10:00
ethernet
0e2b5f3835 chore: remove old plan files 2026-08-09 15:17:01 -04:00
GodsBoy
8cb066404e fix(plugins): address portable MCP review feedback 2026-08-07 09:44:21 -07:00
GodsBoy
e288d93fc1 fix(review): harden portable plugin boundaries 2026-08-07 09:44:21 -07:00
GodsBoy
c5117655b6 feat(plugins): validate portable agent packages 2026-08-07 09:44:21 -07:00
HexLab98
2d0c2682c7 docs: document the gateway agent cache memory budget
Record why the cache needs a third bound and what the pressure pass will and
will not shed, so an operator tuning agent.agent_cache knows which knob to
reach for. Adds the config keys to the session-lifecycle appendix and a user
guide section covering the "auto" cgroup-derived budget.
2026-08-07 21:02:33 +05:30
Alex Fournier
535a59c5c0 docs(observability): clarify active profile identity
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-08-04 15:13:34 -07:00
Alex Fournier
942d731553 Merge updated client resource metrics into active-install metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-08-02 20:13:05 -07:00
Alex Fournier
d0322fad69 Merge updated skill metrics into client resource metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-08-02 20:12:50 -07:00
Alex Fournier
884c2daa1c Merge updated tool metrics into skill metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>

# Conflicts:
#	tests/tools/test_skills_hub.py
2026-08-02 20:12:37 -07:00
Alex Fournier
14c8bd646c Merge updated model metrics into tool metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-08-02 20:11:20 -07:00
Alex Fournier
a97abcd55a Merge upstream main into model metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>

# Conflicts:
#	tests/agent/test_auxiliary_relay.py
2026-08-02 20:10:47 -07:00
Gille
991f5f1e9e fix(kanban): deliver notifications from non-dispatch gateways 2026-07-31 21:52:11 -07:00
Alex Fournier
07ac9ca687 Merge updated client resource metrics into active-install metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>

# Conflicts:
#	hermes_cli/observability/schemas/hermes.shared_metrics.v1.schema.json
#	hermes_cli/observability/shared_metrics_contract.py
#	hermes_cli/observability/shared_metrics_subscriber.py
#	scripts/smoke_nemo_relay_shared_metrics.py
#	tests/hermes_cli/test_relay_shared_metrics.py
#	tests/hermes_cli/test_relay_shared_metrics_runtime.py
2026-07-31 07:43:11 -07:00
Alex Fournier
9b8d4133db Merge updated skill metrics into client resource metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>

# Conflicts:
#	docs/observability/relay-shared-metrics.md
#	hermes_cli/observability/shared_metrics.py
#	tests/hermes_cli/test_relay_shared_metrics.py
2026-07-31 07:36:57 -07:00
Alex Fournier
3d5fcab70f Merge updated tool metrics into skill metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>

# Conflicts:
#	hermes_cli/observability/schemas/hermes.shared_metrics.v1.schema.json
#	scripts/smoke_nemo_relay_shared_metrics.py
#	tests/agent/test_skill_commands.py
#	tests/hermes_cli/test_relay_shared_metrics.py
#	tests/hermes_cli/test_relay_shared_metrics_runtime.py
#	tests/tools/test_skill_manager_tool.py
#	tests/tools/test_skill_usage.py
#	tests/tools/test_skills_tool.py
2026-07-31 07:30:30 -07:00
Alex Fournier
aef76ba398 Merge updated model metrics into tool metrics
# Conflicts:
#	hermes_cli/observability/shared_metrics_contract.py
#	hermes_cli/observability/shared_metrics_subscriber.py
#	scripts/smoke_nemo_relay_shared_metrics.py
#	tests/hermes_cli/test_plugins.py
#	tests/hermes_cli/test_relay_shared_metrics.py
#	tests/hermes_cli/test_relay_shared_metrics_runtime.py
#	tests/run_agent/test_run_agent.py
#	tests/test_model_tools.py
#	tests/tools/test_approval.py
2026-07-31 07:18:02 -07:00
Alex Fournier
0558ea0c48 Merge upstream main into feat/hermes-relay-model-metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-31 07:04:37 -07:00
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
e8237050fc docs(agent): state the real cost, and make model choice the main knob
Three corrections, all from measuring a real 3.5 hour session rather than
reasoning about the design.

"During the idle moment after a response" was wrong. A pass is a real call
to the compression model at the end of a turn: the answer has streamed, but
the turn does not close until it finishes. Measured 2 to 37 seconds, median
around 31, on a small local model. Say so.

Add the choice of `auxiliary.compression` model as its own section, because
it dominates everything else here. A pass sends only a few thousand tokens
but runs every turn, so latency is felt repeatedly, and reasoning models are
a poor fit -- merging one exchange into a summary is mechanical work, and a
thinking model spends reasoning tokens on it for no benefit. Two measured
data points are given as illustrations of the shape, explicitly not as
recommendations: the right answer depends on the operator's hardware.

Add what a working session actually looks like: occupancy climbing to ~22%
and flattening (equilibrium -- 4,841 tokens added between the last two
passes, 4,395 reclaimed), zero batch compactions, and reclamation only
ramping after the tail budget is crossed. Also state the cost in the same
breath rather than burying it.

Frame the feature as a tuning option rather than a win: it lets you choose
how the compression cost is distributed and which model pays it. It is not
a magic bullet and the docs should not imply otherwise.

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
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
cd9d9d03b3 docs(agent): explain micro-compaction
Covers what it does, the head/tail protection, the cursor and rolling
summary, defrag, how the session DB is kept in step, and the failure
paths. States the tradeoff up front: compression cost is amortized across
turns, at the price of older detail becoming summarized earlier in a
session than batch-only compaction would.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:44:19 +05:30
Alex Fournier
c0369f0891 Merge remote-tracking branch 'origin/main' into feat/hermes-relay-model-metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-30 11:12:51 -07:00
Teknium
fc551f9691 Merge pull request #72103 from victor-kyriazakos/feat/relay-slack-blockkit-native-parity
fix(relay): Slack DM-root prompts + flat-DM edit-streaming (native _resolve_thread_ts parity)
2026-07-29 15:13:42 -07:00
Alex Fournier
dfb8c1bd4d fix(observability): preserve shared metrics compatibility
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-29 14:09:47 -07:00
Alex Fournier
46bae75042 feat(observability): add Relay active install metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-29 12:20:34 -07:00
Alex Fournier
5607d09e03 feat(observability): add Relay client resource metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-29 12:20:34 -07:00