Commit Graph

20602 Commits

Author SHA1 Message Date
Ben Barclay 244b6fdb55 fix(imports): keep `import gateway` free of httpx via lazy AuxiliaryExplicitCancellation import
`import gateway` reaches agent/conversation_compression.py through
gateway/session.py -> agent/turn_context.py, and a module-level
`from agent.auxiliary_client import AuxiliaryExplicitCancellation`
there dragged in agent.credential_pool -> hermes_cli.auth -> httpx at
import time. Minimal consumers that import the gateway package with only
the lightweight wire deps (websockets/aiohttp/pyyaml/requests) — e.g. the
gateway-gateway cross-repo live E2E suite — now crash with
ModuleNotFoundError: No module named 'httpx' before running anything.

Move the import to call time inside compress_context(), matching the
existing lazy-import pattern for aux_progress_hook /
aux_interrupt_protection in the same function. Both uses of the exception
class (the raise and the except) are inside compress_context's dynamic
extent, so behavior is unchanged.

Add tests/gateway/test_gateway_import_hygiene.py: a fresh-interpreter
probe that blocks httpx/openai/anthropic on the meta path and imports
gateway.relay.ws_transport, so an eager heavyweight import on the
gateway path fails CI instead of breaking downstream repos.

Verified: probe fails on current main, passes with this change; also
reproduced in a real minimal venv (pip install websockets aiohttp pyyaml
requests) where `from gateway.relay.ws_transport import
WebSocketRelayTransport` now succeeds with httpx absent.
2026-08-03 14:26:58 -06:00
Teknium a991dfc25d docs: document /personality none|default|neutral reset across personality docs
The reset keywords have existed in both CLI and gateway handlers since
June but were undocumented — users couldn't find how to cancel a
personality overlay. Adds a 'Resetting to the default' section to the
personality feature page and mentions the reset in the CLI guide,
slash-command reference (both tables), and messaging command table.
2026-08-03 12:29:49 -07: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
Hao Wang aad8f7412c fix(backup): serialize and atomically publish snapshots 2026-08-03 23:48:55 +05:30
kshitij 53cc628045
Merge pull request #77912 from kshitijk4poor/chore-haowang-email
chore: add contributor email mapping for HAOWANG116
2026-08-03 23:42:45 +05:30
kshitijk4poor f40d63d5db chore: add contributor email mapping for HAOWANG116 2026-08-03 23:42:33 +05:30
kshitij d92a28dd34
Merge pull request #77902 from kshitijk4poor/chore-zabih-email
chore: add contributor email mapping for zabih-sudo
2026-08-03 23:32:26 +05:30
kshitijk4poor 164c3d60b2 chore: add contributor email mapping for zabih-sudo 2026-08-03 23:32:12 +05:30
kshitij efbfe0842f fix(prompt_size): search volatile tier for skills block after the stable->volatile move
CI-caught: compute_prompt_breakdown still looked for <available_skills> in the stable tier, but #37117 moved it to volatile. Search volatile first, fall back to stable for older sessions.
2026-08-03 23:23:39 +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
ethernet 7eefb09314 fix(nix): tie devShell's HERMES_PYTHON to the venv actually on PATH
`nix/devShell.nix` collected `devShellHook` by scanning every package:

    nonNpmHooks = map (p: p.passthru.devShellHook or "") packages;

But `minimal` and `messaging` are `.override` variants of `default`, so
each carries its own `devShellHook` exporting its own HERMES_PYTHON. The
scan therefore concatenated three conflicting exports and forced Nix to
evaluate and realise three separate uv2nix editable venvs on every
`nix develop`.

`attrValues` is alphabetical, so the last hook won (`minimal`) while
`python`/VIRTUAL_ENV came from `default`'s devDeps:

    HERMES_PYTHON        = ...dimim2... (minimal — no optional deps)
    python / VIRTUAL_ENV = ...85r28... (full)

Inside the shell `$HERMES_PYTHON -c "import anthropic"` failed while
`python -c "import anthropic"` succeeded. Worse, `scripts/run_tests.sh`
prefers HERMES_PYTHON, so the suite ran against the minimal venv. Its
guard did not catch this: it only checks that HERMES_PYTHON has pytest,
and minimal's venv does (pytest is in the `dev` group), so the wrong
interpreter was silently accepted.

Tying the hook to `packages.default` — the same package whose `devDeps`
are installed — keeps HERMES_PYTHON, `python`, and VIRTUAL_ENV pointing
at one venv by construction.

    editable venvs referenced   3      -> 1
    their combined closure      421 MB -> 140 MB
    test failures               85     -> 32

The venv mismatch was masking 53 failures; e.g. test_web_tools_config.py
goes 2-failed -> 38-passed. Full suite is now 25369 passed / 32 failed,
and those 32 reproduce identically on a pristine HEAD worktree with no
nix/ changes under the same interpreter (mostly NixOS artifacts — tests
spawning bare `python3` in a scrubbed env exit 127).
2026-08-03 13:32:08 -04:00
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 00475e1b26 fix(catalog): validate http+api_key manifests declare the header's env key
Simplify-pass follow-up on the #70782 salvage: _bearer_auth_headers
hard-emits ${MCP_<NAME>_API_KEY} but install_entry only persists
auth.env-declared vars — a manifest naming its key differently (the
shipped n8n style) would install cleanly yet send a literal-placeholder
header at connect time (silent 401, the #37792 bug class). Enforce the
naming contract at parse time. Also pins the secret-stays-in-.env
property in the install test (raw config.yaml carries the template,
never the secret). Mutation-checked: validation disabled -> guard test
fails.
2026-08-03 22:55:36 +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
JonthanaHanh 861ca18c67 fix(catalog): wire api_key auth headers for http MCP servers
When an optional-mcps manifest declares transport.type=http with
auth.type=api_key, install_entry() prompts for the key and saves it to
.env, but _build_server_config() only handled the oauth case — the
api_key case produced a bare url entry with no headers, so every
request to the server was unauthenticated (-> 401).

Reuse _bearer_auth_headers(entry.name) from mcp_config.py so the
catalog path emits the same 'Authorization: Bearer ${MCP_..._API_KEY}'
template as the manual 'hermes mcp add --url' path.

Salvaged from #70782 (production hunk applied clean; tests re-anchored
onto current main). Credit: JonthanaHanh.
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 952d86b797 fix(file-sync): serialize concurrent sync cycles 2026-08-03 22:53:32 +05:30
EndeavorYen c0b0cc3925 feat(image): parallelize image_generate batches 2026-08-03 22:53:32 +05:30
Brin Shadewater e6f1d613b6 fix(discord): leave voice channels before cancelling the bot task
`DiscordAdapter.disconnect()` cancelled the bot task before tearing down voice
clients. `leave_voice_channel()` ends in `await vc.disconnect()`, and discord.py
sends a voice state update over the main gateway websocket and then waits for the
voice socket to close. The bot task is the loop running that gateway connection,
so cancelling it first left the handshake with no transport: it could never
complete and blocked until the caller's shutdown timeout fired.

The effect was a fixed ~5s penalty on every shutdown with a voice connection
open, ending in "discord disconnect timed out after 5.0s - forcing continue",
with the voice disconnect abandoned rather than completed.

Measured on a live gateway with a voice connection open in both cases:

  before: timed out after 5.0s, all adapters disconnected at +5.29s
  after:  discord disconnected (0.12s), all adapters disconnected at +0.46s

Moving the voice-cleanup loop above `_cancel_bot_task()` preserves the
zombie-client protection its comment describes: the bot task is still cancelled
before `client.close()`, just after voice teardown rather than before it. Voice
teardown is the one step that still requires a live gateway.

Adds a regression test asserting the ordering. It fails on the previous ordering
at index 1 with `cancel_bot_task != leave_voice_channel:111`.

Fixes #76044
2026-08-03 22:47:14 +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
kshitij f03eb252cb
Merge pull request #77867 from kshitijk4poor/chore-ahmett-email
chore: add contributor email mapping for Ahmett101
2026-08-03 22:35:45 +05:30
kshitijk4poor 376370691d chore: add contributor email mapping for Ahmett101 2026-08-03 22:35:34 +05:30
Teknium 3c27eb6234 chore: release v0.20.0 (2026.8.3)
The Herald Release — voice (streaming TTS, barge-in, wake words), A2A v1.0,
outbound webhooks, grounded citations, desktop platform wave. ~3,650 commits,
~1,400 PRs, ~1,200 issues closed, 650+ contributors since v0.19.0.

Also: contributor audit additions (18 email mappings, bot-filter widening).
2026-08-03 09:57:23 -07:00
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
Ben Barclay 2f09df5615
fix(relay): route Discord tool-progress into the auto-thread, not the parent channel (#77830)
When a Discord channel message initiates a relay auto-thread, the thread does
not exist at ingest (source.thread_id is None) — the connector creates it on
its FIRST send and auto-threads any outbound carrying the reply anchor. The
final reply carries that anchor, so it lands in the thread. But the
tool-progress / status bubbles (the "Searching the web for..." updates and the
streaming preamble) were sent with _progress_metadata=None and
_progress_reply_to=None: _resolve_progress_thread_id returns None for Discord
(only slack/mattermost get a synthetic thread), so the progress send had no
anchor and the connector posted it FLAT in the parent channel. Result: the
search-status updates leaked outside the thread while the answer threaded
(staging repro 2026-08-02).

The connector now stamps prospective_thread_id on the inbound (the anchor
message id == the id of the thread it will create). Reuse it: when a
relay-delivered Discord channel-initiate carries prospective_thread_id and has
no real thread yet, carry the reply anchor (event_message_id) on both the
progress metadata (reply_to_message_id) and the progress reply_to, so the
connector routes the progress bubble into the SAME auto-thread as the final
reply. Applied to both the tool-progress path (_progress_metadata /
_progress_reply_to) and the status/interim callback path
(_status_thread_metadata). Events already arriving in a real thread, DMs, and
non-relay sources are untouched (guarded on delivered_via_upstream_relay +
prospective_thread_id + not thread_id).

Tests: two new cases in test_run_progress_topics.py — a relay Discord
channel-initiate asserts every progress send carries the anchor (reply_to +
metadata.reply_to_message_id + non_conversational), and an event already in a
real thread asserts the synthetic-anchor path does NOT engage. Full gateway
progress + relay + session suites green (228 passed).
2026-08-03 15:58:15 +00:00
314574126 003b4c8893 perf(gateway): per-platform skip_context_files to cut agent build latency
Salvage of #26860 (hunk 2, ported \u2014 the PR's base predates the current
gateway layout by ~11.9K commits). Messaging platforms can set
gateway.platforms.<key>.skip_context_files: true to skip the
filesystem-heavy context-file discovery (SOUL.md, AGENTS.md,
.cursorrules walks) during AIAgent construction \u2014 10-100x slower
stat()/walk costs on Windows made this a real per-turn tax. Soul
identity is still loaded (single small file), so the persona survives.

The flag participates in _agent_config_signature so toggling it
rebuilds the cached agent instead of silently reusing a prompt built
under the other setting (prompt-cache correctness).

The PR's hunk 1 (mtime-caching the per-turn dotenv reload) was dropped:
df51ad797 mtime-cached load_config/read_raw_config and c2eda92fd
removed the per-turn deepcopies, capturing most of that win; the
function has since gained a multiplex early-return and managed-scope
overlay that the original whole-function skip would have bypassed.
2026-08-03 21:22:56 +05:30
kshitijk4poor 0bb14627b8 test: harden cold-start regression tests + debug-log the env-var skip
Review folds on the #60807 salvage:
- resolve_skin tests are behavioral (thread-ident probe + ready-frame
  wiring check) instead of pure source inspection, per the #72720
  pattern; a source assertion remains as belt-and-braces.
- The warm-list test does REAL imports and checks sys.modules —
  _warm_gateway_module swallows ImportError by design, so the PR's
  tracking-stub test would pass even with a typo'd module name.
- resolve_copilot_token logs a debug line when the env-var
  short-circuit skips the gh-CLI fallback (behavioral change made
  observable).
2026-08-03 21:16:09 +05:30
jinglun010 25a9c2c245 perf(cold-start): mitigate ~14s GIL stall during backend init (#60800)
Three fixes for the Desktop/TUI cold-start stall where the event loop
is blocked for ~14s between HERMES_BACKEND_READY and the first
prompt (#60800):

1. copilot_auth: skip  subprocess fallback when any
   Copilot env var is explicitly set (even if invalid). The user
   expressed token intent via env var; silently substituting a CLI
   token is surprising and the subprocess adds up to 5s on Windows.

2. tui_gateway/ws: run resolve_skin() via asyncio.to_thread so config
   loading + skin engine init do not block the WS read loop during
   the cold-start RPC burst.

3. web_server: extend _warm_gateway_module to pre-import the heavy
   module chains (auth, copilot_auth, runtime_provider, skin_engine,
   inventory, model_switch) that the first WS connection + RPC burst
   would otherwise import on the loop thread. These trigger .pyc
   compilation and Defender scans on Windows (15-30s per the existing
   comment) and were not covered by the original gateway-only warm.

Tests: 5 new tests in test_cold_start_gil_stall.py + 2 new tests in
test_copilot_auth.py. All 36 copilot_auth tests + 16 ws/web_server
tests pass.
2026-08-03 21:16:09 +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
kshitij d5584a32d8
Merge pull request #77821 from kshitijk4poor/chore-archer-email
chore: add contributor email mapping for ArcherQAQ
2026-08-03 21:07:58 +05:30
kshitijk4poor d48a78a293 chore: add contributor email mapping for ArcherQAQ 2026-08-03 21:07:47 +05:30
pierrenode 67d4bbb812 fix(state): route session-resume reads through the WAL read-only connection
get_messages_as_conversation, get_resume_conversations, and
get_ancestor_display_prefix still took self._lock — the same global
choke point the read-path split (WAL per-thread read-only connections)
was meant to remove from every recall/browse read. These three are the
hottest reads in the file: every session resume across the gateway,
CLI, and ACP adapter goes through one of them, so a resume racing a
burst of concurrent-session writer flushes still convoys behind them
exactly like the fixed paths used to.

_session_lineage_root_to_tip (the lineage walk shared by all three,
plus get_conversation_root) had its own independent self._lock use and
needed the same conversion — without it the outer functions still
blocked on the very first line.

Verified empirically: a reader thread calling all three functions
while another thread holds self._lock blocked for the writer's full
hold duration before the fix, and returned immediately after (SQLite
3.50.4 in this dev venv falls back to journal_mode=DELETE per the
WAL-reset-bug guard, so the requires_wal-marked regression test is
exercised via a local WAL-forced script instead; it still runs and
passes on any runtime where WAL is actually active).
2026-08-03 21:02:11 +05:30
ethernet 177002838b ci: retry uv python install 2026-08-03 11:26:56 -04:00
asill-livestream 128ca2efd5 perf(tui): memoize useSessionLifecycle return (idea from #38491)
Re-derivation of #38491 by @stremtec onto current main (the original is
10,119 commits behind; the hook moved into ui-tui/src/app/). The hook
returned a fresh object literal every render, defeating memoization in
useMainApp's consumers; useMemo over the (all-useCallback-stable)
handles makes the return referentially stable.

Dep array covers ALL nine returned handles incl. trimTail (the
re-derivation initially omitted it - stale-closure class).
2026-08-03 20:47:32 +05:30
kshitij 5bff3984be fix(tests): update two more append_message.call_args assertions to append_messages_batch
CI-caught: test_verification_stop_caching and test_tui_gateway_server::test_native_vision_turn_persists_a_renderable_image_ref both assert on append_message.call_args, but the flush loop now calls append_messages_batch. Same class of test-fake fallout fixed in 5 other files — these two were missed.
2026-08-03 20:43:38 +05:30
kshitij da6d9604dd refactor(state): fold simplify findings — reuse _insert_message_rows, share guards, chunk seeds
Simplify-pass folds on the #23254 salvage:

- REUSE (HIGH): append_messages_batch now delegates row serialization to
  the pre-existing _insert_message_rows helper (already shared by
  replace_messages / archive_and_compact / portability import) instead
  of adding a third serialization path (_prepare_message_row +
  _MESSAGE_INSERT_SQL are gone). One row-writer for every multi-row
  path; the row-ID return was consumed by no production caller, so the
  batch returns the inserted count.

- QUALITY (HIGH): the compression-lock + compression-closed admission
  guards are extracted into _check_transcript_write_guards, shared by
  append_message and append_messages_batch (previously duplicated 23
  lines that had already needed targeted fixes, #74478). The role-gated
  reasoning filtering is no longer duplicated in run_agent.py — it
  lives at its one site inside _insert_message_rows.

- EFFICIENCY (MEDIUM, measured): unbounded seed copies hold one BEGIN
  IMMEDIATE for seconds (10k rows ~= 2.4s; FTS triggers dominate) and
  monopolize the in-process write lock. append_messages_batch grows a
  chunk_rows param; all seed/copy call sites use chunk_rows=500. Same
  recovery semantics as the old per-row loops, bounded lock holds.

- REUSE (MEDIUM): the two remaining per-row branch-copy loops found by
  the pass (gateway/slash_commands.py /branch, hermes_cli
  cli_commands_mixin.py branch) are converted to chunked batches too
  (AsyncSessionDB's generic to_thread forwarder covers the async site).

Turn-flush benchmark unchanged after the refactor: 2.43 -> 0.87 ms
median per 5-message flush (64% faster).
2026-08-03 20:43:38 +05:30
kshitij 84146fb9c6 test(run-agent): update flush-path fakes and assertions for batched writes
The flush now goes through append_messages_batch; MagicMock-based
assertions and barrier fakes that hooked append_message observed
nothing (the flush's try/except swallowed the AttributeError). Assert
on the batch payload instead.
2026-08-03 20:43:38 +05:30
kshitij b58b3adb9b perf(tui-gateway): batch branch-seed history copies (whole-bug-class)
Sibling sites of the per-message flush pattern: both branch-seed
paths (session.branch in methods_session.py and the lazy seed persist
in server.py) copied the parent history row-by-row -- one transaction
per row, and a branch seed can be hundreds of rows. Route both through
SessionDB.append_messages_batch. The server.py path also gains real
atomicity: _branch_seed_persisted assumed every row landed, which the
per-row loop could not guarantee.
2026-08-03 20:43:38 +05:30
devsart95 06ae5b6faa perf(state): batch the turn flush into one SQLite transaction
Re-derivation of #23254 (@devsart95) on today's flush loop. The turn
flush in _flush_messages_to_session_db wrote one BEGIN IMMEDIATE
transaction per message row; a typical agent turn (user + assistant +
tool results) paid 3-8 transactions -- and, off WAL (the default on
macOS while the WAL-reset guard is active), 3-8 fsyncs -- per turn.

Adds SessionDB.append_messages_batch: same row shape as append_message
(shared _prepare_message_row serializer + _MESSAGE_INSERT_SQL column
list, so the two writers cannot drift), same compression-lock and
compression-closed guards, one aggregated session-counter UPDATE, one
transaction for the whole batch. Row serialization stays outside the
write lock.

The flush loop now collects the turn's new rows and writes them in one
call. All-or-nothing pairs exactly with the persisted-marker stamping:
on failure no rows landed and no markers were stamped, so the next
flush re-writes the whole tail (same recovery contract as before,
minus the partial-prefix case that could double-count).

Measured (same harness, 5-message turn, journal_mode=DELETE,
synchronous=FULL): 2.32ms -> 0.83ms median per turn flush (64% faster,
5 fsyncs -> 1). On WAL the win is smaller but the atomicity fix holds.
2026-08-03 20:43:38 +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
kshitij fe4cf36c2c
Merge pull request #77796 from kshitijk4poor/chore/attrib-cicav
chore: contributor email mapping for cicav (legacy noreply form)
2026-08-03 20:41:26 +05:30
joncaldwell90 58286878ef fix(tui): avoid writable Kanban opens on empty polls 2026-08-03 20:40:28 +05:30
kshitij 26133b5341 chore: map cicav legacy noreply email 2026-08-03 20:40:27 +05:30
embwl0x 7d066c3c56 fix(state): deduplicate session system prompts 2026-08-03 20:37:17 +05:30
kshitij 41cc4a13fe fix(openviking): catch endpoint errors in setup validation functions
Review follow-up for salvaged PR #76782. Three setup-wizard
validation functions called _normalize_openviking_url outside their
try/except blocks. Since _normalize_openviking_url now raises
_OpenVikingEndpointError for blocked or malformed endpoints, an
invalid endpoint would crash the wizard instead of returning a
friendly (False, message) tuple.

- _validate_openviking_auth: move _normalize_openviking_url inside try
- _validate_openviking_root_access: same
- _validate_openviking_setup_values: catch _OpenVikingEndpointError explicitly
- Remove dead ternary in _normalize_openviking_url safety check (candidate
  always has http/https scheme by that point)
- Replace redundant float('-inf') < x < float('inf') with math.isfinite()
  in _setting_float; drop the redundant infinity check from _setting_int
  (is_integer() already rejects inf/nan)
2026-08-03 20:35:47 +05:30