Commit Graph

2670 Commits

Author SHA1 Message Date
ethernet 5a9788babe ci: add newci-* shadow workflows on GKE self-hosted runners
Run a duplicate of CI on the new ARC (Actions Runner Controller) runners in
GKE, beside the existing CI. The duplicate does not change production CI.
Every workflow in .github/workflows/ that does not start with newci- is
byte-identical to main. Watch the shadow runs for a few days, then migrate.

The shadow set is 16 files: newci-ci.yml plus the 15 reusable workflows that
ci.yml calls on a pull request. Only pull-request workflows are copied.
js-autofix, deploy-site, and skills-index run on push or on a schedule. A
copy of those would push branches and deploy the site a second time.

Safety properties of the shadow:

- Concurrency groups are newci-prefixed. This is the important one. The
  production groups use cancel-in-progress, so a shared group would let a
  shadow run cancel the production run.
- Cache keys are newci-prefixed. The shadow cannot poison or evict a
  production cache entry.
- Reusable-workflow calls point only at other newci-* files. No shadow job
  calls a production workflow.
- The PR review comment runs with --dry-run. It prints the comment body to
  the job log. Two pollers cannot fight over the hermes-ci-review-bot
  comment.
- The gate job is renamed to "[newci] All checks pass (informational)". The
  production check "All required checks pass" stays the only merge gate.
- The shadow runs on pull_request only. The push trigger is removed.
- docker publish and merge jobs are unreachable. Their conditions require a
  push to main or a release.

Runner infrastructure, in the shadow copies only:

- Jobs go to three scale sets: arc-runner-small for short gate jobs,
  arc-runner-set for general work, arc-runner-docker and arc-runner-arm64
  for image builds. dind is only on the docker sets, so the other jobs stop
  paying for a privileged sidecar.
- The runner image supplies node 26, npm 12, uv, Python, and ripgrep. The
  setup-node, setup-uv, and per-job install steps are gone.
- Checkout uses a node-local git mirror, seeded from the runner pod env.
- buildx layer cache moved to Artifact Registry in us-central1, the same
  region as the runners. Reads are keyless through GKE Workload Identity.
  Writes use GitHub OIDC and happen only on main pushes and releases, so
  pull-request code cannot write a layer that the publish job reads.

Merge-base work, in the shadow copies only:

- A new composite action, .github/actions/merge-base, deepens a shallow
  clone until the two histories connect. fetch-depth: 0 fetches all ~1400
  refs and measured 76-81s, against 3-6s for a shallow checkout.
- The action fails by default when no merge base exists. A three-dot diff
  over a missing merge base scans nothing and reports clean, so the
  supply-chain audit must stop. history-check sets fail-on-missing to false,
  because absence is the result it measures.
- lint diffs against the base commit directly. The job checks out the PR
  merge ref, so base.sha is already the correct comparison point.
- contributor-check uses origin/main..HEAD. The result equals the merge-base
  form, and the extra git call also expanded a SHA without quotes.

Other changes:

- .github/actionlint.yaml declares the four ARC labels. actionlint knows
  only GitHub-hosted labels, so every runs-on in the repo was reported as an
  unknown label: 40 warnings that hid real findings.
- scripts/ci/resource_profile.py records CPU and memory for a job step. The
  timing report shows the data per step.
- run_tests_parallel.py can list test files from the git index. The slice
  generator then needs no blobs.
- Docker test files are split so boot-heavy tests run in parallel.
- Container-environment parity fixes in doctor, gateway, and skill_utils,
  with tests.

To retire the shadow: delete .github/workflows/newci-*.yml.
2026-08-05 00:22:10 -04:00
brooklyn! 43717123ca
fix(models): a model id missing its vendor prefix says so instead of 404ing (#78856)
Selecting an NVIDIA NIM model whose id reached config without the nvidia/
prefix produced a bare "HTTP 404: 404 page not found" — retried three times,
never naming the model. It reads exactly like an outage or an auth failure,
which is where the Discord thread spent its time before the id was spotted.

normalize_model_for_provider() had no branch for nvidia, so a bare id passed
straight through to the API. Repair it from the provider's curated catalogue:
a bare name that matches exactly one entry modulo the prefix gets it back.
That's a lookup, not a guess — build.nvidia.com also fronts local NIM
containers and third-party models, and anything absent from the catalogue is
left alone. Because the repair runs on every runtime setup, an already-broken
config self-heals on the next turn and prints what it changed.

If a bare id still reaches the wire, the 404 now explains itself. The
classifier consults the same catalogue: a prefix-less id the provider only
serves as vendor/model is a deterministic failure, so it classifies as
model_not_found instead of burning three retries on a retryable "unknown",
and the error trace names the id to use.

Fixes #78796
2026-08-04 19:35:57 +00:00
Jeffrey Quesnelle f40fbcf409
Merge pull request #68882 from afourniernv/feat/hermes-relay-tool-metrics
feat(observability): aggregate bounded tool metrics
2026-08-04 15:04:29 -04:00
Brooklyn Nicholson 9cd0338688 fix(credential-pool): bench a billing 403 fully, even as the sole key
The sole-credential cooldown sized the bench from the raw HTTP status, but
403 is overloaded: error_classifier maps OpenRouter's "key limit exceeded"
and xAI's spending-limit block to FailoverReason.billing, while an edge
throttle with the same status is transient. Only 402 was excluded from the
short cooldown, so a spent account on a single key retried every 60 seconds
and re-failed forever.

Thread the classified reason from recover_with_credential_pool through
mark_exhausted_and_rotate to _exhausted_ttl. Billing keeps the full bench
regardless of status; everything else transient still recovers in 60s. The
verdict is stored on the entry (_EXTRA_KEYS, so it persists to auth.json) —
without that a restart would re-read a bare 403 and downgrade the bench.

Tests: sole billing-403 stays benched, survives reload, unclassified 403
still recovers; call-site coverage that the reason actually reaches the pool.
Three existing kwargs assertions updated for the new argument.
2026-08-04 23:33:39 +05:30
kshitij d1eb08fcf3 fix: thread sole_credential into next_available_at sibling site
next_available_at() was computing the full 1-hour TTL for a sole
credential on a 429, contradicting the 60s cooldown in _available_entries.
The fallback restore gate (agent_runtime_helpers) uses next_available_at
to decide when to switch back from fallback to primary — so the agent
stayed on fallback for an hour instead of ~60s.

Add sole_credential computation in next_available_at mirroring
_available_entries, and a test verifying the short cooldown propagates.
2026-08-04 23:33:39 +05:30
A.Alzaro dcd7504349 fix(credential-pool): short cooldown for sole credential on transient throttle
A pool with only one usable (non-DEAD) credential has nothing to rotate to.
On a transient throttle (429 rate-limit, 403 edge-throttle, 5xx) the offending
key was benched for a full hour (EXHAUSTED_TTL_429/DEFAULT), so single-key /
no-fallback setups got an hour of hard failures for a throttle that resets in
seconds. The pool already special-cases 401 to recover quickly for single-key
setups; extend that to transient throttles when the credential is the sole
non-DEAD entry. 402 (billing/quota) keeps the full bench — a quick retry can't
help. Provider-supplied reset_at still overrides.

Adds tests covering sole 429/403 recovery, 402 full-bench, and multi-key
(no early recovery).
2026-08-04 23:33:39 +05:30
Jeongseok Kang 2d70f56327
fix(agent): adopt .env credential/base-url edits at the turn boundary (#67843)
* fix(agent): adopt .env credential/base-url edits at the turn boundary

A Settings save (desktop PUT /api/env, hermes setup) updates .env and
the saving process's os.environ, but a live session worker keeps the
base_url/api_key captured at agent init until restart — an open chat
silently kept calling the old endpoint (e.g. a local-server key sent to
api.openai.com, failing with an opaque 401).

Add AIAgent._try_refresh_env_client_credentials(), called at the start
of each conversation turn: re-resolve the provider's env-sourced
credentials (load_env() is mtime-memoized, so an unchanged file costs
one stat()) and rebuild the client via the existing
_replace_primary_openai_client machinery when the user edited them.

The refresh reacts only to env edits — resolved values changed since
the last look — never to mere divergence from the agent's current
values: credential-pool rotation and failover legitimately move the
session off the env credential, and stomping those back would flap.
Config model.base_url / pool custom endpoints keep precedence: edits
are only adopted while the session still runs on the registry default
or the previously-seen env value.

Lift _get_env_prefer_dotenv out of _seed_from_env to module level
(get_env_prefer_dotenv) so both the pool seeder and the per-turn
refresh share the same .env-over-os.environ resolution, including the
op:// indirection handling.

Fixes #67821

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(agent): address sweeper review on env credential refresh

- Cover named custom providers (#67935): provider="custom" has no
  PROVIDER_REGISTRY entry, so resolve the config block's key_env through
  the same lookup the runtime resolver uses.
- Make the edit baseline transactional: a failed client rebuild rolls the
  agent back and leaves _env_creds_seen un-advanced so the unchanged edit
  is retried next turn.
- Recompute route-derived TLS material and default headers on a base-url
  change, via a _reapply_route_client_config helper shared with
  credential-pool rotation so the two paths cannot drift.
- Rebase onto main: get_env_prefer_dotenv keeps the scoped _get_secret
  semantics from the profile-isolation fix (no raw os.environ reads).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: map jskang@lablup.com to rapsealk

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
2026-08-04 17:53:17 +00:00
Aleksandr Pasevin fe859a1f55
fix(credential-pool): clear exhaustion state on key rotation (#22622)
* fix(credential-pool): clear exhaustion state on key rotation

When a user rotates an API key (e.g. via `hermes setup` after hitting a
rate limit), _upsert_entry updates the access_token on the existing pool
entry but preserves the stale last_status=exhausted from the old key.
On the next session the pool finds the entry, sees it exhausted, and
returns no usable credentials — even though the new key is valid.

Fix: when access_token changes on an existing entry, reset last_status,
last_error_code, last_error_reason, last_error_message, and
last_error_reset_at. The exhaustion state belongs to the old key, not
the new one.

* chore: add pasevin@gmail.com to AUTHOR_MAP

* fix: clear last_status_at on key rotation, remove unused pytest import

Address review feedback from teknium1 on PR #22622:
- Add last_status_at=None to the reset block (matches all other
  token-sync reset paths in credential_pool.py)
- Assert last_status_at is None in the regression test
- Remove unused pytest import flagged by ruff + ty
2026-08-04 11:35:06 -06:00
Jeffrey Quesnelle daf67f2e59
Merge branch 'main' into feat/hermes-relay-tool-metrics 2026-08-04 12:43:38 -04:00
Jeffrey Quesnelle 5943bab1ec
Merge branch 'main' into feat/hermes-relay-model-metrics 2026-08-04 12:07:51 -04:00
Jeffrey Quesnelle 42708f8bb3
Merge pull request #74864 from bbednarski9/fix/relay-concurrent-turn-scopes
fix(relay): avoid concurrent turn scope corruption
2026-08-04 12:04:42 -04:00
kshitij f5be9236e0 refactor(xai): simplify _xai_prefers_native_web_search to use registry
Drop the manual web.search_backend / web.backend config-reading block
that duplicated _read_config_key in web_search_registry.py. The function
now delegates directly to get_active_search_provider() (which reads the
same config keys via the registry's canonical resolver) and falls back
to _get_search_backend() only when the registry has no providers loaded.

Also updates the TestXaiWebSearchBackendPreference tests to monkeypatch
the registry instead of load_config_readonly, and adds two new tests for
the legacy fallback path (no provider registered -> _get_search_backend).
2026-08-04 15:44:15 +05:30
xxxigm d2772b4206 fix(xai): honor configured web search backend on Responses path
When Grok runs on xAI Responses, only swap to native server-side
web_search when the active/configured backend is xai. For Firecrawl
and other Hermes providers, keep client dispatch under a renamed wire
tool so Grok cannot hijack web_search and ignore user config.
2026-08-04 15:44:15 +05:30
kshitijk4poor 4075c8fd5a fix(credential-pool): lock the quarantine read-modify-write of _entries
#71775 moved deferred single-use-token refreshes outside the pool lock
(correct — they hold a cross-process flock plus network I/O). But
_refresh_entry_impl's three terminal-auth-failure quarantine paths do a
bare read-modify-write of self._entries. Those used to run with the
caller holding self._lock; on the deferred path they run unlocked, so a
concurrent mutation between the read and the write is silently lost.

Wrap all three in 'with self._lock' (an RLock, so locked callers
re-enter safely) and correct the _refresh_pending_entries docstring,
which claimed the mutations were already self-locking.

Post-merge gate-sweep finding on the #71775 salvage (#77714).
Sibling to the acquire_lease re-select fix.
2026-08-04 13:20:15 +05:30
kshitijk4poor db0bd42119 fix(credential-pool): re-select in acquire_lease after a deferred refresh
select() re-selects once deferred single-use-token refreshes complete;
acquire_lease() performed the refresh but returned its pre-refresh
answer. Since _acquire_lease_under_lock returns early exactly when a
refresh is pending (if not available: return None, pending_refresh),
a pool whose entries all needed a refresh always returned None — the
caller failed an answerable request right after the refresh succeeded.

Retry once, only when the first pass was empty and a refresh ran.

Post-merge gate-sweep finding on the #71775 salvage (#77714).
2026-08-04 13:12:24 +05:30
kshitij 9fc8926975 perf: reuse request_input_estimate instead of recomputing estimate_request_tokens_rough
The output-cap error handler already computes request_input_estimate at
line 4722 via estimate_request_tokens_rough(api_messages, tools=...).
The new compression block ~50 lines below was calling the same function
with the same inputs again. Reuse the existing local.
2026-08-04 11:26:39 +05:30
BobClawblaw 04098e2b5f fix(conversation_loop): prune dead vision-strip fallback; harden output-cap retry tests 2026-08-04 11:26:39 +05:30
Hermes Agent 9938d20503 fix(conversation_loop): compress messages on output-cap retry path (#55546)
The output-cap retry loop reduced max_tokens by 64 tokens per attempt but
never called _compress_context(), so the compressor never fired. Input
growth (~65 tokens/attempt) canceled the savings, leaving the session
stuck at 200,001 tokens — 1 over the 200,000 ceiling.

The fix adds compression to the output-cap retry path. The compressor
drops the middle window, freeing ~50% of tokens. If compression makes
>=5% savings, the session continues; otherwise vision payloads are
stripped or the session ends with compression_exhausted=True.

Also adds CHANGELOG.md entry and bug fix report.
2026-08-04 11:26:39 +05:30
kshitij 15d51bb88a refactor: dedup stale-marker regex — use compiled _STALE_MARKER_RE in conversation_loop
The bracketed-marker regex was inlined in conversation_loop.py as
re.fullmatch(r"\[...", ...) while hermes_state.py defines the same
pattern as _STALE_TOOL_CALL_MARKER_RE. Both must agree on what counts
as a stale marker — a drift here means the runtime guard silently
disagrees with the load-on-read repair and CLI purge in hermes_state.

Consolidate onto a single compiled constant (_STALE_MARKER_RE) at
module level in conversation_loop.py, with a comment noting it must
mirror _STALE_TOOL_CALL_MARKER_RE in hermes_state.py. A direct import
from hermes_state was tried first but caused a regression: hermes_state
initializes DEFAULT_DB_PATH = get_hermes_home() / 'state.db' at module
import time, which breaks tests that monkeypatch get_hermes_home() to
return a str (test_slash_worker_accepts_profile_home).

Follow-up to PR #78175 (@JoaoMarcos44).
2026-08-04 11:26:15 +05:30
joaomarcos ba9068c8b6 fix(agent): discard bare tool-call marker before fallback/persistence (#78148)
Local tool-call templates can emit a bare bracketed token (e.g. "[memory]")
as assistant content alongside a function call. The loop treated that
protocol scaffolding as visible content: it got cached as the post-tool
fallback, and when the next turn came back empty, the marker was replayed
as the final response and written into the persisted transcript. Later
context compaction preserved that history, letting the model repeat the
marker in subsequent turns.

Detect content that is only a bracketed marker (`[name]`) when the
response also carries tool_calls, and drop it before it can be cached
or persisted. Scoped narrowly: only fires alongside tool_calls, so a
genuine final response of "[memory]" without a tool call is unaffected.
2026-08-04 11:26:15 +05:30
Jordan 60c721ada6 fix(model_metadata): read llama.cpp context from meta.n_ctx + accept sole model 2026-08-04 11:08:32 +05:30
xxxigm 3a0a295109 fix(agent): keep context_length pin for named custom providers
Empty model.base_url plus a runtime custom-provider URL was treated as a
route mismatch, so gateway session-reset banners dropped model.context_length
and fell back to the Qwen family default (131K) while /status still showed
the configured 262K pin.
2026-08-04 11:00:04 +05:30
Teknium 3c3ae7428d feat(models): add qwen3.8-max to Nous portal + OpenRouter catalogs, replacing qwen3.7-max
Qwen3.8 Max is live on both OpenRouter and the Nous portal
(qwen/qwen3.8-max, 1M context, 131K max output). Per the
newest-max-replaces-last-max convention, it takes qwen3.7-max's slot
in both curated lists.

- hermes_cli/models.py: OPENROUTER_MODELS + _PROVIDER_MODELS[nous]
  swap qwen/qwen3.7-max -> qwen/qwen3.8-max
- agent/model_metadata.py: DEFAULT_CONTEXT_LENGTHS entry for
  qwen3.8-max at 1,000,000 (verified against OpenRouter live
  metadata and Nous /v1/models 2026-08-03)
- tests/test_empty_model_fallback.py: swap incidental catalog fixture
  to the surviving slug
- website/static/api/model-catalog.json: regenerated

Pricing snapshot skipped: both routes bill via official_models_api
(live pricing), verified with resolve_billing_route. Reasoning
timeout floor already covered by the qwen3 prefix (180s).
2026-08-03 17:19:49 -07:00
Bryan Bednarski e1caa611bf
fix(relay): preserve skipped turn context
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-03 13:43:17 -06:00
Bryan Bednarski a2a08fe147
fix(relay): gate skipped turn metrics
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-03 13:43:17 -06:00
Bryan Bednarski 9a9b670e29
fix(relay): avoid concurrent turn scope corruption
Signed-off-by: Bryan Bednarski <bbednarski@nvidia.com>
2026-08-03 13:43:16 -06:00
kshitij 0845232d76 fix: prefer explicit anthropic api key
Cherry-picked from PR #58560 by @itsflownium, adapted to current main
(_getenv instead of os.getenv). Moves ANTHROPIC_API_KEY check ahead of
Claude Code credential file and credential_pool auto-discovery so an
explicitly configured key is never shadowed by auto-discovered OAuth.

Fixes #58546
2026-08-04 00:12:45 +05:30
kshitij 9555525a7d docs(system_prompt): fix stale reconstruct_static_prefix docstring example
Simplify-pass finding: the safety note still cited 'skills edited' as a stable-tier input whose change mismatches the rebuilt prefix — after this PR a skill edit changes only the volatile tail (that's the point). Swap the example for genuinely stable-tier inputs.
2026-08-03 23:23:39 +05:30
marzukia 9b9cbdd7eb fix(system_prompt): move skills index to the volatile band
The skills index is runtime-mutable: the agent adds and patches skills mid-session, so it is not byte-stable. Keeping it in the stable band breaks that band prefix-cache contract, because every skill edit changes the stable band and invalidates the entire cached prefix in front of it. Move it to the front of the volatile band so the stable scaffold (identity, tool guidance, model guidance) stays cacheable across skill edits.
2026-08-03 23:23:39 +05:30
kshitijk4poor ddae511ab1 fix: thread extra_headers through the call_llm split
The PR's concurrency wrapper splits call_llm into a semaphore-guarded
entry + _call_llm_impl; main added extra_headers to call_llm's
signature after the PR's base, so the split has to forward it too
(dropped silently otherwise — Azure Foundry and custom-endpoint
callers set it).
2026-08-03 23:01:03 +05:30
konsisumer 23f8ae32c0 fix(agent): cap auxiliary LLM concurrency per task 2026-08-03 23:01:03 +05:30
kshitij f8f475569f perf(compressor): release allocator pages after successful compaction
A successful compaction frees the largest allocation a long session ever
drops (the compressed-away message dicts), but Python's arena allocator
keeps those pages in the heap — RSS retains the pre-compaction
high-water mark until exit. #76905's trim_memory lifecycle covers the
gateway/TUI housekeeping loops but not the CLI compression path.

Call trim_memory(reason='post-compression') at the compression-success
point in ContextCompressor.compress(), following the house pattern
(lazy import in try, debug-level log on failure). The helper is
glibc-gated, config-gated and rate-limited, so it is a safe no-op on
other platforms and cannot fail compression.

Re-expresses the intent of #70782 (JonthanaHanh), which reached for a
bare gc.collect(); trim_memory is the house mechanism and already
wraps a collect.
2026-08-03 22:55:36 +05:30
kshitij df9dbba2ba fix(backoff): keep 60s first-hit cooldown, escalate only on consecutive rate-limits
Review follow-up on the #30223 salvage: the original changed the base
cooldown from 60s to 1800s, benching the primary for 30 minutes on the
FIRST 429 (30x regression in primary-restore latency) and breaking the
existing test_rate_limit_exhaustion_keeps_60s_cooldown contract.

Keep upstream's 60s base and escalate per consecutive rate-limit:
60s -> 2m -> 4m -> 8m -> ... capped at 4h. Counter still resets on
successful primary restore (cicae's mechanism, unchanged).

New tests: escalation doubling, 14400s cap, reset-on-restore.
Existing 60s contract test passes UNCHANGED. Mutation-checked:
escalation disabled -> 2 fail; reset disabled -> 1 fails.
2026-08-03 22:54:54 +05:30
cicav 9267c7823c fix: exponential backoff for rate-limit fallback cooldown
Replace the fixed 60-second cooldown with exponential backoff:
30min → 1h → 2h → 4h cap.

The counter is reset by restore_primary_runtime on successful
primary-provider recovery, so the backoff is strictly for
consecutive failures within a single degradation window.

Closes #29702
2026-08-03 22:54:54 +05:30
kshitijk4poor a7ad713f43 fix(tool-executor): unpack 5-tuple runnable_calls in _max_workers_for_tool_batch 2026-08-03 22:53:32 +05:30
EndeavorYen c0b0cc3925 feat(image): parallelize image_generate batches 2026-08-03 22:53:32 +05:30
Ahmett101 d1c6c6b58e perf(moa): cache resolved preset + per-slot runtime to cut cold-start latency (#66793) 2026-08-03 22:43:15 +05:30
rlaope 1f8acb340f fix(agent): stop re-probing endpoints that blackhole TCP connects
Salvage of #71282 (Fixes #71281): a routable-but-dead endpoint (corp
LAN address while off-VPN) blackholes TCP SYNs, so every probe in the
model-metadata waterfall waits out its full connect timeout — 20+
seconds of stall per startup across detect_local_server_type,
fetch_endpoint_model_metadata, and the per-model probes.

A module-level blackhole cache keyed on host:port is populated when
any probe observes a ConnectTimeout (httpx or requests; read timeouts
deliberately excluded — an accepted connection is not a blackhole) and
consulted at the top of each guarded function. 30s TTL: long enough to
collapse one startup burst, short enough that VPN recovery is picked
up without a restart. Guard ordering: blackhole check -> disk L2 ->
HTTP waterfall, and a blackholed leg aborts the remaining legs instead
of letting each stall in turn.

Squash of the PR's two real commits (the branch's merge commits made
it un-rebase-merge-able; content verified identical via merge-tree).
2026-08-03 21:53:13 +05:30
kshitij 733e7d26c0 fix(model_metadata): guard _localhost_to_ipv4 against non-string urls
CI slice 3/7 failures: run_conversation tests pass MagicMock base_urls
through the metadata probe path; re.sub raised TypeError where the old
code let non-strings flow through. Preserve that contract.
2026-08-03 21:12:56 +05:30
pierrenode fc32a38c3a fix(model_metadata): rewrite localhost->IPv4 for the remaining local probe sites
fetch_endpoint_model_metadata's generic (non-LM-Studio) /models fetch and
its llama.cpp /v1/props context-length follow-up built request URLs
straight from the unrewritten candidate, unlike every other local-probe
site. Both retained the multi-second dual-stack IPv6 connect penalty
that _localhost_to_ipv4() exists to skip (measured on macOS: localhost
32.9ms vs 127.0.0.1 0.1ms on a dead port; ~2s on Windows). normalized
stays the cache key so caching behavior is unchanged; only the outbound
request target is rewritten.

Re-derived from PR #61528 onto current main (original no longer applied
cleanly).
2026-08-03 21:12:56 +05:30
BK Bot 5bbd0dbd86 fix(context): dedupe subdirectory hints by content digest and skip backup/vendor dirs
SubdirectoryHintTracker re-injected identical context files whenever the same
AGENTS.md was reachable through more than one path. Symlinked shared
workspaces, hardlinks, and timestamped backup copies all alias a single file,
so a normal session could ship the same 8KB of instructions two or three
times. Nothing deduped it and nothing excluded directories that only ever
hold copies.

Two changes:

* Track a sha256 of every injected hint body. Repeat content is skipped, and
  the working directory's own context file is seeded at construction so the
  copy prompt_builder already loaded at startup is never sent again.
* Skip directories that hold copies rather than authoritative context
  (backups, node_modules, venv, site-packages, .git, .Trash, vendor, caches).
  Screening is relative to working_dir, so a project that legitimately lives
  under vendor/ keeps discovering its own subdirectory hints.

Measured on a real session that touched a symlinked shared workspace:
3 injections / ~24,000 chars before, 1 injection / 8,112 chars after.

14 new tests cover symlink aliasing, byte-identical copies, working-dir
seeding, distinct content still being injected, each excluded directory name,
excluded ancestors, and the working-dir-inside-excluded-name case.
2026-08-03 20:43:34 +05:30
ehz0ah e43bc0b7aa fix(openviking): integrate reliability and configuration hardening 2026-08-03 20:35:47 +05:30
kshitijk4poor 82019e7c1b fix(credential_pool): unpack the tuple in next_available_at's gate
Cross-PR interaction fix: #77714 (salvage of #71775) changed
_available_entries to return (available, pending_refresh) while #77631
(salvage of #67642) added next_available_at() which still truthiness-
tests the bare return. A non-empty tuple is always truthy — even
([], []) — so the reset-aware gate silently returned None ('no wait
info') for every exhausted pool, disabling the feature #77631 shipped.
Unpack the tuple and test the available list.

Also adapts the lock-probe test for the RLock introduced by #77714
(same-thread non-blocking acquire always succeeds on an RLock; probe
from a helper thread instead).
2026-08-03 19:32:50 +05:30
kshitijk4poor 687dd632a1 fix(credential_pool): serialize deferred-refresh pool mutations
Review folds on the #71775 salvage (dossier findings 1+2):

- self._lock becomes an RLock and the mutation primitives
  (_replace_entry, _persist) are now self-locking, so the deferred
  single-use-token refresh path — which deliberately runs its
  cross-process flock + OAuth network I/O OUTSIDE the pool lock —
  still serializes its pool mutations against concurrent
  select()/rotation. In-lock callers re-acquire reentrantly.
- Dropped _refresh_pending_entries' redundant second _replace_entry:
  _refresh_entry already merges the refreshed entry internally.

Adds tests/agent/test_credential_pool_deferred_refresh.py pinning both
invariants: select() must NOT hold the lock during the refresh window
(the PR's whole point), and the post-refresh mutations MUST contend on
the lock (blocking-thread probe).
2026-08-03 19:08:09 +05:30
dsad 9cd605f303 fix(credential_pool): defer single-use-token refresh outside threading lock
select() and acquire_lease() held self._lock during the entire
_available_entries() loop, which for openai-codex and xai-oauth providers
includes a cross-process file lock (_auth_store_lock) plus OAuth token
refresh HTTP POST.  The lock timeout can exceed 20 seconds, blocking all
credential pool consumers across every gateway thread and subagent.

Collect single-use-token refresh entries under the lock, then execute the
refreshes outside it.  On success the refreshed entry is merged back into
the pool and re-selected.  Non-single-use providers (anthropic, nous)
continue refreshing inside the lock since their refresh is a simple HTTP
POST with no cross-process coordination.
2026-08-03 19:08:09 +05:30
kshitijk4poor 4c2d473a80 fix(credential_pool): run next_available_at under the pool lock
Review fold on the #67642 salvage: next_available_at() called
_available_entries() — which prunes DEAD entries, syncs tokens, and
persists — and iterated self._entries with no lock, racing concurrent
select()/rotation exactly as has_available()'s comment warns. Wrap the
method body in self._lock and pin it with a non-blocking-acquire probe
test.
2026-08-03 19:02:12 +05:30
WojtekMR3 6611d87003 feat: reset-aware primary restore — stay on fallback until the rate-limit window resets
restore_primary_runtime retries the primary every turn once the 60s
transient cooldown clears. For subscription-window limits (Claude
Pro/Max 5h windows, Codex weekly caps) the reset is hours or days away,
so every retry is a guaranteed failure costing two provider switches
and two prompt-cache invalidations per turn.

Add CredentialPool.next_available_at() (earliest reset across exhausted
entries; None when available now or no reset info) and gate the restore
on it: skip while the primary's pool says nobody can serve, restore on
the first turn after the reset elapses. Fail-open: any gate error or
missing reset info falls through to the existing per-turn retry, so
recovery can never be later than today. Cross-provider fallbacks
consult the PRIMARY's pool (not the attached fallback pool), reusing
the loaded pool for the existing rebind to keep auth reads at one per
restore.
2026-08-03 19:02:12 +05:30
kshitij 773d69057e refactor(insights): drop consumer-less get_skill_breakdown alias (simplify-pass)
The 2-line alias had zero production consumers (web_server calls
get_usage_breakdown directly). Tests rewired onto the real API; the
contracts they pin are unchanged. Stale test docstring fixed.
2026-08-03 18:45:43 +05:30
Rod Boev e3ce092f8d perf(dashboard): keep tools in focused analytics usage (#18511) 2026-08-03 18:45:43 +05:30
Rod Boev c1639322c2 perf(dashboard): skip full InsightsEngine on /api/analytics/usage (#18511) 2026-08-03 18:45:43 +05:30