Commit graph

19375 commits

Author SHA1 Message Date
Teknium
92856bc28a
Merge pull request #74383 from NousResearch/tests/prune-low-value
test: prune low-value tests suite-wide — 58% fewer tests, half the wall time, zero flakes
2026-07-29 15:46:36 -07:00
Teknium
3913ac2ba0
test: pair load_config_readonly stubs at pruned-file sites (post-merge with main's readonly sweep)
Main's 243c9182b1/a16fd675df/7142dc4580 added load_config_readonly
sibling stubs across 38 files; our pruned versions of 11 of those files
kept only the load_config stubs. Re-applied the pairing at every
surviving site (26 patch()/setattr sites) — same return_value/
side_effect as the adjacent load_config stub. 494 tests green across
the 11 files.
2026-07-29 15:37:24 -07:00
Teknium
1cf5d3841b perf(cli): stop hermes -w stalling 30-60s on a flaky fetch in _resolve_worktree_base
The #71637 prune fix cut one stage of -w startup, but the base-ref
resolution right after it still ran an uncapped-in-practice
'git fetch origin main' (timeout=30) on every launch — and on a flaky
smart-HTTP connection that fetch intermittently stalled to the full 30s,
then cascaded into step 2's SECOND 30s fetch. Measured: back-to-back
fetches of 0.9s, 1.0s, 63.5s on the same box with healthy TLS (~185ms).

_resolve_worktree_base now:
- skips the fetch entirely when FETCH_HEAD is < 5 min old and the
  tracking ref exists (repeat launches pay zero network cost)
- caps the fetch at 5s and falls back to the locally-known tracking
  ref (labelled 'cached') on timeout/failure instead of cascading into
  a second fetch — genuine staleness stays backstopped by the pre-push
  stale-base gate
- caps 'git remote show origin' the same way

Worst case drops ~60s -> ~5s; warm path is ~0.02s (was up to 30.8s).
sync_base=False and the offline HEAD fallback are unchanged.
2026-07-29 15:34:53 -07:00
teknium1
7142dc4580 test: complete the readonly-stub pairing sweep (6 more files)
Final pass: per_model_threshold_init_ordering, memory_provider_init,
plugin_context_engine_init, api_max_retries_config,
invalid_context_length_warning, tool_call_guardrail_runtime all stub
agent_init config reads that now resolve through load_config_readonly().
Verified by running the complete 59-file suspect list (every test file
that stubs load_config in string or attribute form and intersects the
swapped modules): 3,063 tests, 0 failed.
2026-07-29 15:28:15 -07:00
teknium1
a16fd675df test: pair attribute-form load_config stubs with readonly siblings
Second stub shape the first sweep missed: monkeypatch.setattr(config_mod,
"load_config", ...) — attribute-form instead of string-form. Four files
(compression_max_attempts, preflight_compression_cap_e2e,
codex_gpt55_autoraise_notice, proactive_prune_config) stub agent_init
config reads that now go through load_config_readonly(). Swept the whole
tree for the attribute form; remaining hits stub modules that still use
the mutable loader. 1,210 tests green across the 19-file re-sweep.
2026-07-29 15:28:15 -07:00
teknium1
243c9182b1 test: patch load_config_readonly alongside load_config across affected suites
The readonly swaps mean agent/ modules now read config through
load_config_readonly(); tests that stubbed only load_config stopped
intercepting those reads. Added sibling readonly stubs (same
return_value/side_effect/lambda) at every affected site across 11 test
files, found via a tree-wide sweep of load_config stubs cross-referenced
against the swapped modules. 3,915 tests green across the 38-file
sibling sweep.
2026-07-29 15:28:15 -07:00
teknium1
97aba01406 test: patch load_config_readonly alongside load_config in run_agent init tests
agent_init's config reads now go through load_config_readonly(); 8
tests that stubbed only load_config stopped intercepting them. Each of
the 10 patch sites gains a sibling readonly patch with the same
return_value. 461/461 file-local tests green.
2026-07-29 15:28:15 -07:00
teknium1
d3cd6f4158 test: cache-aliasing regression tests for provider normalization
Pins the audit findings: normalizer never mutates its input
(api_key_env + camelCase forms), providers-dict round-trips leave a
cached config byte-identical, and the normalized models mapping does
not alias the caller's dict.
2026-07-29 15:28:15 -07:00
Gabriel Stoltemberg
59ee85ed50 perf: use load_config_readonly() at read-only call sites in agent/
Salvaged from #56085 (@Stoltemberg), rebased onto current main: sites
main had already converted (credential_pool, auxiliary_client MoA
paths, model_metadata, moa_loop, agent_runtime_helpers) resolve to
main's versions; the remaining ~29 read-only sites across 16 agent/
files swap to the no-deepcopy readonly loader (~135us saved per call).

Full per-site mutation audit performed (every enclosing function read,
escapes traced): 23 SAFE, 5 ESCAPES with read-only consumers, 1 UNSAFE
path (init_agent -> get_compatible_custom_providers -> normalizer
in-place alias writes) fixed by the preceding no-mutate commits, which
make the normalizer copy-safe for ALL callers.
2026-07-29 15:28:15 -07:00
teknium1
3c6e7b1b11 fix(config): don't share the cached models mapping with normalized entries
Companion to the no-mutate fix: normalized['models'] retained a
reference to the caller's (possibly cached) models dict, and the
normalized entry escapes into long-lived runtime state
(agent._custom_providers). Shallow-copy so runtime writes can never
reach the shared config cache.
2026-07-29 15:28:15 -07:00
golldyck
bfe4cbdc2a fix(config): stop _normalize_custom_provider_entry mutating the caller's dict
The normalizer writes alias keys into the entry it is given
(entry['key_env'] = entry['api_key_env'] and entry[snake] =
entry[camel]) while building its normalized copy. Two of its three
callers — get_compatible_custom_providers and
providers_dict_to_custom_providers — pass live sub-dicts straight
from load_config_readonly()'s shared cache (only
_custom_provider_entry_to_provider_config defends with dict(entry)).

A config written with the documented camelCase / api_key_env aliases
therefore gets its cached copy polluted with injected duplicate keys,
violating the cache's explicit no-mutation contract; every later
load_config() deepcopy inherits the duplicates, and any
save_config(load_config()) flow (setup wizard, dashboard writes,
model persist) writes them back to config.yaml. The aux-client TLS
resolution runs this on every auxiliary client build, so the
mutation also happens unlocked on worker threads against a shared
object.

Shallow-copy the entry up front; the function's return value is a
separately-built dict, so behavior is otherwise unchanged.
2026-07-29 15:28:15 -07:00
Teknium
7c5a98d888
Merge remote-tracking branch 'origin/main' into tests/prune-low-value
# Conflicts:
#	tests/run_agent/test_conversation_fallback_state.py
2026-07-29 15:24:14 -07:00
Teknium
25927884e0 docs(acp): document the Buzz Desktop model picker
Buzz Desktop v0.5.1 now renders Hermes' ACP model menu in agent runtime
settings. Add a short note under the Buzz Desktop host section explaining
where the list comes from (the shared authenticated-provider inventory),
the provider:model / custom:<name>:<model> ID shapes, and that a pick is
session-scoped rather than a Hermes-wide default change.
2026-07-29 15:21:39 -07:00
Soju06
ce9f6712ff refactor(agent): remove the inter-tool delay
The 1.0s sleep between sequential tool calls has been present since the
initial commit with no documented rationale. It sleeps between local
tool executions — the next LLM request goes out only after the whole
batch — so it rate-limits nothing, and the parallel read-only path
already runs with no delay. Every multi-tool turn pays (N-1) seconds
of dead time. Remove the sleep, the internal tool_delay plumbing, and
dead test assignments. AIAgent.__init__ keeps tool_delay as a
deprecated no-op keyword for one release so existing programmatic
callers construct cleanly; passing it emits a DeprecationWarning.
2026-07-29 15:21:26 -07:00
Teknium
1a088989bc
Merge pull request #66730 from NousResearch/feat/hsp-sync-client
feat(sync): HSP/1 personal skill sync client (M1 client)
2026-07-29 15:20:35 -07:00
Teknium
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
Teknium
a17ac2ca67
Merge remote-tracking branch 'origin/main' into tests/prune-low-value
# Conflicts:
#	tests/agent/test_context_compressor.py
#	tests/gateway/test_startup_restart_race.py
#	tests/hermes_cli/test_voice_wrapper.py
2026-07-29 15:13:21 -07:00
Teknium
28524adb0e fix(tests): eliminate flaky/broken tests — shadow sys.path inserts, unmocked network in compressor tests, stale-SDK feishu pin guard, quadratic redact regexes
- Remove tests/-shadowing sys.path.insert(dirname/'..') from 11 test files:
  it prepended the tests/ dir itself to sys.path, so 'import agent' /
  'import hermes_cli' resolved to the test packages and collection died
  with ModuleNotFoundError depending on import order (2 files failed in
  every full-suite run; 9 more were latent).
- Patch call_llm in 5 context-compressor tests that called compress()
  unmocked: each burned ~50s attempting live LLM traffic through the
  relay before falling back (572s file — the slowest in the suite, and
  flaky under the 300s per-file timeout). File now runs in ~5s.
- agent/redact.py: fix two catastrophically-backtracking regexes hit by
  the compressor's redaction pass on large payloads —
  _STRICT_URL_USERINFO_RE anchors on the mandatory '//' (optional-scheme
  prefix backtracked O(n^2): ~55s on a 320KB payload, now sub-ms;
  output-equivalence fuzz-verified on 20k random strings), and the
  _CFG_DOTTED_RE/_CFG_ANCHORED_RE subs gain an exact linear keyword
  pre-gate so secret-free text skips the quadratic pattern entirely.
- tests/gateway/test_feishu.py: version-guard the extra_ua_tags SDK
  signature check; the repo pins lark-oapi==1.6.8 but stale local
  installs (1.5.3) fail the assertion — skip below the pin.
- tests/tools/test_managed_browserbase_and_modal.py: stub
  agent.redact + agent.credential_persistence in the fake agent package
  (empty __path__ blocks all real agent.* imports added since the fake
  was written).
- tests/gateway/test_startup_restart_race.py: raise wait_for timeouts
  2s -> 30s; 2s wall-clock on a loaded 40-worker box flaked in the
  baseline run (passes instantly when the box is quiet).
2026-07-29 15:12:28 -07:00
kshitijk4poor
1f70ba6bca fix(mcp): propagate cancellation untouched in _connect_server orphan reap
Follow-up to the salvaged #62026 ownership fix, folding in #72054's
CancelledError rule by @adurham: start() already cancels/reaps its own
run task when the caller's connect timeout cancels start() itself, so
_connect_server() must propagate cancellation without awaiting a
redundant shutdown() inside a cancelled context. Non-cancellation
failures on the unclaimed (standalone probe) path still reap the parked
task, now with the reap failure logged instead of raising over the real
error.

Also maps mrz@mrzlab630.pw for the attribution check.

Co-authored-by: Adam Durham <amdnative@gmail.com>
2026-07-30 03:35:35 +05:30
Stepan Zadolia
c00a1d58d5 fix(mcp): retain parked startup tasks for clean shutdown 2026-07-30 03:35:35 +05:30
Seppe Gadeyne
fd5397d69a chore(release): map shady2k contributor email 2026-07-30 03:35:35 +05:30
Seppe Gadeyne
ab0d3fac3d fix(mcp): keep drain and stop on loop thread 2026-07-30 03:35:35 +05:30
Seppe Gadeyne
cac74e06c8 fix(mcp): bound loop-owned shutdown drain 2026-07-30 03:35:35 +05:30
shady
cc21c4f78f fix(mcp): drain pending tasks before closing the MCP loop
_stop_mcp_loop() stopped and closed the background loop without reaping
the tasks still on it. A task left suspended is resumed later by the GC,
whose finalizer drives its cleanup against the now-closed loop:

    Exception ignored in: <coroutine object MCPServerTask.run ...>
      File "tools/mcp_tool.py", line 2947, in run
        parked = await self._wait_for_reconnect_or_shutdown(
      File "tools/mcp_tool.py", line 2161, in _wait_for_reconnect_or_shutdown
        t.cancel()
    RuntimeError: Event loop is closed

shutdown_mcp_servers() only reaps servers held in _servers, so a server
that parked after exhausting its initial-connect budget — never inserted
there, because start() raises _error before the caller registers it — has
no owner to signal it and stays suspended until the loop is gone.

Drain the loop the way asyncio.run() does: cancel the remaining tasks and
gather them while the loop is still open, so each runs its own finally.
Cancel alone is not enough — Task.cancel() only schedules the throw.

This resolves the reported traceback, but not the ownership bug that
strands the task in the first place; that needs a follow-up. Deliberately
not using "Fixes" so #60197 stays open for it.

Addresses #60197
Addresses #66113

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 03:35:35 +05:30
Seppe Gadeyne
eded89ace2 test(mcp): cover parked shutdown drain path 2026-07-30 03:35:35 +05:30
Teknium
40ec417881
Merge remote-tracking branch 'origin/main' into tests/prune-low-value
# Conflicts:
#	tests/hermes_cli/test_install_cua_driver.py
#	tests/run_agent/test_codex_app_server_integration.py
#	tests/test_tui_gateway_server.py
#	tests/tools/test_computer_use_delivery_ladder.py
#	tests/tools/test_zombie_process_cleanup.py
2026-07-29 14:12:25 -07:00
brooklyn!
a4973c3f11
Merge pull request #74363 from helix4u/fix/windows-wake-input-device
fix(wake): keep desktop ownership and select input devices
2026-07-29 15:44:13 -05:00
Teknium
39975613b1
test: prune wave 2 + speed fixes — 28,106 → 19,757 test functions, suite wall 315s → 294s
Second, deeper pass over tools/gateway/hermes_cli plus first pass over
the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker,
dashboard, conformance, monitoring, secret_sources, hermes_state,
providers). Same rubric as wave 1 (AGENTS.md test policy); security,
alternation/caching invariants, issue-number regressions, and E2E kept.

Real test-quality fixes found and rooted out along the way:
- tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls
  (DEFAULT_CONFIG smart-approval leaked in) — pinned approval
  mode=manual via autouse fixture: 17.4s → 0.4s.
- test_model_switch_custom_providers.py / test_user_providers_model_switch.py
  silently probed live provider catalogs (~2s/test) — stubbed
  cached_provider_model_ids/provider_model_ids/fetch_api_models.
- test_telegram_noise_filter.py: 15-platform copy-paste matrix over
  shared gateway.run logic → 3 representative platforms (55s → 3.9s).
- test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on
  MagicMock agents — interrupt.side_effect now clears _running_agents
  (22s → 1.0s).
- test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x
  (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps
  patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait
  5s → 0.5s.
- test_telegram_init_deadline.py: loop-block margin restored to 1.0s
  with rationale comment — the watchdog-dump assertion needs the loop
  blocked well past deadline+grace under parallel load (flaked once in
  the 40-worker verification run at a 0.2s margin).

Verification: full hermetic suite via scripts/run_tests.sh —
2,438 files, 21,718 tests passed, 0 failed, 293.9s wall.
Suite totals vs original baseline: 46,820 → 19,757 test functions
(−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
2026-07-29 13:39:40 -07:00
teknium1
a022229566 refactor(gateway): TurnContext/TurnRunner seam — extract _run_agent_inner nested closures (byte-identical bodies) 2026-07-29 13:34:03 -07:00
hermes-seaeye[bot]
e23d158f48
fmt(js): npm run fix on merge (#74360)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-29 20:30:47 +00:00
Teknium
6b81590c55
test: prune low-value tests suite-wide (wave 1) — 46,820 → 28,106 test functions
Systematic prune per AGENTS.md test policy, one pass over every major
test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli,
cron, tui_gateway, honcho/openviking, root-level):

- DELETE: source-reading tests (read_text/getsource on prod files),
  change-detector tests (exact catalog counts, model-name snapshots,
  config version literals), mock-echo tests (assert a mock returns what
  it was told), assertion-free/trivial tests, near-duplicate
  parametrizations (boundaries + one representative kept), async/sync
  twin duplicates, cosmetic within-file variations.
- KEEP (mandatory): security/redaction/approval guards, message-role
  alternation invariants, prompt-caching/deterministic-call-id
  invariants, issue-number regression tests (deduped), E2E tests.
- 6 test files deleted outright (script-style/no-assert or fully
  redundant); conftest.py, fakes/, fixtures/ untouched.
- tests/acp/conftest.py added: autouse fixture stubs the live
  models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server
  tests performed on every session create — test_server.py 147s → 3.4s,
  and the tests are now genuinely hermetic.
- Sleep-based slowness shrunk where safe (codex_ttfb_watchdog,
  compression_concurrent_fork, etc.); no wall-clock assertion tightened.

Verification: full hermetic suite via scripts/run_tests.sh —
2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall
(baseline: 583s wall, 13,564s subprocess CPU).
2026-07-29 13:10:23 -07:00
Gille
94d1dff50d fix(wake): route desktop control and select input devices 2026-07-29 14:04:21 -06:00
Ben Barclay
f5b68ad58b Merge origin/main into feat/hsp-sync-client
Resolves the PR's conflict with main (2252 commits). Two conflicts, both
"each side added an independent block in the same place" — kept both:

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

No behaviour dropped from either side.

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

The Pyright Optional-parameter warnings in skill_manager_tool.py are
pre-existing on main (`content: str = None` etc.), not introduced here.
2026-07-29 13:01:36 -07:00
Teknium
e0233f8fc5 fix(desktop): full-duplex voice barge-in — interrupt during generation AND playback
The voice-interruption fix (5081551f0) covered the Python surfaces (CLI +
TUI gateway) but the desktop app has its own mic path in voice-barge-in.ts,
which still had both bugs on Windows:

- HALF-DUPLEX GAP: the barge monitor only opened when TTS playback started;
  during LLM generation no mic was listening at all.
- PLAYBACK DEAFNESS: the monitor calibrated its noise floor while the
  speakers were already playing TTS, baking bleed into the floor. On
  Windows, Chromium echoCancellation does not reliably cancel same-app
  output (measured live: quiet floor ~35-50 RMS vs playback bleed
  600-1700 RMS), making the trigger unreachable.

Changes mirror tools/voice_mode.full_duplex_listen:

- voice-barge-in.ts: phase-aware full-duplex monitor — quiet-only
  calibration (floor held through playback, never recalibrated against
  bleed), playback min-trigger clamp + ceiling, 500ms grace on playback
  onset only, windowed-majority detection.
- use-voice-conversation.ts: monitor arms at turn submit and spans
  generation + playback (ensureBargeMonitor, idempotent). Mid-generation
  speech fires the new onInterrupt callback; spoken stop-word during a
  barge ends the conversation; submit waits for the interrupt to settle.
- use-composer-voice.ts + composer/index.tsx: plumb onInterrupt: haltRun
  (same seam as the Stop button).

Tests: use-voice-conversation.test.tsx (6 tests) covering monitor lifecycle
across generation + playback, mid-generation interrupt, stop-word handling.
npm run typecheck + eslint clean; remaining desktop vitest failures
reproduce on a clean tree (pre-existing Windows env failures).
2026-07-29 12:52:42 -07:00
Shizoqua
70411a6152 fix(cron): scrub ALL GitHub auth-header curl blocks, not just the first
Salvaged from #31671 (@Shizoqua). The config-cache half of that PR was
superseded on main (9b8b054c2d gave _load_config_safe a readonly path),
but this cron-scanner half is still live: _strip_cron_safe_constructs
used re.search + a single str.replace, which only scrubbed occurrences
IDENTICAL to the first match. A cron job loading several GitHub skills
carries heterogeneous auth-header curl forms (-H vs --header, quoting,
token var names) — every non-identical block tripped the
exfil_curl_auth_header detector on every tick, blocking legitimate
GitHub cron jobs.

Now re.sub scrubs every occurrence; the trailing [^\n]* consumes the
URL path so no dangling fragment remains. Sabotage-verified: the old
implementation false-blocks the heterogeneous two-skill prompt the new
regression test pins; exfil to a non-GitHub host is still blocked.
79/79 cron tool tests green.
2026-07-29 12:41:40 -07:00
teknium1
f8758dcaf8 refactor(agent): single-owner call_id + reasoning_content sanitization policies (wire-parity verified) 2026-07-29 12:29:39 -07:00
Teknium
1ea2ea18fd
Merge pull request #74298 from NousResearch/opt/tui-methods-split
refactor(tui): split @method handlers into methods_* modules (mechanical move, registry set-equality verified)
2026-07-29 12:24:19 -07:00
teknium1
27b1377b4c refactor(web): extract git/profiles/cron routes to APIRouter modules (web_deps seam; route-table equality verified) 2026-07-29 12:23:28 -07:00
Teknium
8714040954 fix(computer_use): resolve gateway session-key namespace in permission-mode lookup
Follow-up to the #68246 salvage. The backend permission-mode resolution
only checked the DB session_id the tool path passes, but gateway /yolo
keys approval bypass off the gateway session_key (contextvar). Consult
both namespaces so /yolo works on messaging platforms, not just CLI/TUI.
Adds a regression test driving the real approval contextvar + yolo
toggle path E2E.
2026-07-29 12:19:37 -07:00
Francesco Bonacci
d59974fea5 test: update session yolo approval query 2026-07-29 12:19:37 -07:00
Francesco Bonacci
c268397752 feat(computer_use): align cua-driver 0.10 permission modes 2026-07-29 12:19:37 -07:00
Francesco Bonacci
847e401b74 feat(computer_use): align cua-driver 0.9 contracts
Salvaged from PR #67807 by @f-trycua onto current main.

- Foreground gate: discover delivery_mode support from the live tools/list
  inputSchema.properties (fail closed), not the never-shipped
  input.delivery_mode capability token
- bring_to_front: standalone strict-schema MCP tool (inject_session=False),
  separate approval scope, requires foreground
- Verdict precedence: confirmed > unverifiable (verify before retry) >
  suspected_noop/refusal (escalate); surfaced as explicit verdict field
- Typed cua_browser_* route inside computer_use (browser_route.py) with
  exact-binding, adapter-injected session, snapshot-scoped refs
- Per-Hermes-session backend isolation + release_computer_use_session seam
  wired into AIAgent.close()
- Recorded 0.9 tools/list fixture replaces fabricated capability tokens
2026-07-29 12:19:37 -07:00
Teknium
3dd8059a05
fix(tests): eliminate flaky/broken tests — shadow sys.path inserts, unmocked network in compressor tests, stale-SDK feishu pin guard, quadratic redact regexes
- Remove tests/-shadowing sys.path.insert(dirname/'..') from 11 test files:
  it prepended the tests/ dir itself to sys.path, so 'import agent' /
  'import hermes_cli' resolved to the test packages and collection died
  with ModuleNotFoundError depending on import order (2 files failed in
  every full-suite run; 9 more were latent).
- Patch call_llm in 5 context-compressor tests that called compress()
  unmocked: each burned ~50s attempting live LLM traffic through the
  relay before falling back (572s file — the slowest in the suite, and
  flaky under the 300s per-file timeout). File now runs in ~5s.
- agent/redact.py: fix two catastrophically-backtracking regexes hit by
  the compressor's redaction pass on large payloads —
  _STRICT_URL_USERINFO_RE anchors on the mandatory '//' (optional-scheme
  prefix backtracked O(n^2): ~55s on a 320KB payload, now sub-ms;
  output-equivalence fuzz-verified on 20k random strings), and the
  _CFG_DOTTED_RE/_CFG_ANCHORED_RE subs gain an exact linear keyword
  pre-gate so secret-free text skips the quadratic pattern entirely.
- tests/gateway/test_feishu.py: version-guard the extra_ua_tags SDK
  signature check; the repo pins lark-oapi==1.6.8 but stale local
  installs (1.5.3) fail the assertion — skip below the pin.
- tests/tools/test_managed_browserbase_and_modal.py: stub
  agent.redact + agent.credential_persistence in the fake agent package
  (empty __path__ blocks all real agent.* imports added since the fake
  was written).
- tests/gateway/test_startup_restart_race.py: raise wait_for timeouts
  2s -> 30s; 2s wall-clock on a loaded 40-worker box flaked in the
  baseline run (passes instantly when the box is quiet).
2026-07-29 12:18:07 -07:00
teknium1
bcb352eeab refactor: registry-owned execute() on CommandDef — informational commands unified (thin slice) 2026-07-29 12:11:24 -07:00
teknium1
ab08e8fc76 refactor(gateway): consolidate 19 session-keyed dicts into SessionState (turn/conversation/persistent scopes; eliminates wholesale-reset races)
GatewayRunner carried ~19 separate Dict[str, ...] attributes keyed by
session_key, each with an ad-hoc lifecycle. They now live in one
`self._sessions: dict[str, SessionState]` (gateway/session_state.py) with
three lifecycle scopes and a `_session_state(key)` get-or-create accessor.
Mechanical refactor: same state, same semantics, new container.

Migration table (dict -> old decl line -> current clear path -> new home):

| legacy dict                              | decl  | cleared by (before)                              | SessionState field                     |
|------------------------------------------|-------|--------------------------------------------------|----------------------------------------|
| _running_agents                          | 3505  | _release_running_agent_state; stop() .clear()    | turn.agent                             |
| _running_agents_ts                       | 3506  | same                                             | turn.started_ts                        |
| _active_session_leases                   | 3507  | same (+ lease.release())                         | turn.lease                             |
| _busy_ack_ts                             | 3539  | same                                             | turn.busy_ack_ts                       |
| _turn_lease_tokens ((key, gen)-keyed)    | 3518  | _release_turn_lease (generation-guarded)         | turn.lease_token + turn.lease_generation |
| _session_model_overrides                 | 3585  | _CONVERSATION_SCOPED_STATE funnel                | conversation.model_override            |
| _pending_one_turn_model_restores         | 3586  | funnel; one-shot pop in turn finally             | conversation.one_turn_restore          |
| _session_reasoning_overrides             | 3589  | funnel; lazy-init dict swap ~5692 (RACE)         | conversation.reasoning_override        |
| _session_service_tier_overrides          | 3592  | funnel; lazy-init dict swap ~5738 (RACE)         | conversation.service_tier_override     |
| _last_resolved_model ("*" = process-wide)| 3527  | funnel                                           | conversation.last_resolved_model       |
| _queued_events                           | 3537  | funnel; lazy-init dict swap ~5204 (RACE)         | conversation.queued_events             |
| _pending_turn_sidecar_notes              | 3597  | funnel; lazy-init dict swap ~19832 (RACE)        | conversation.sidecar_notes             |
| _session_ephemeral_pin                   | 3601  | agent-cache evict pop; lazy swap ~19885 (RACE)   | conversation.ephemeral_pin             |
| _session_vc_last                         | 3604  | agent-cache evict pop; lazy swap ~19864 (RACE)   | conversation.vc_last                   |
| _pending_approvals                       | 3611  | boundary security funnel; stop() .clear()        | persistent.approvals                   |
| _update_prompt_pending                   | 3623  | security funnel; update watcher pops             | persistent.update_prompt_pending       |
| _pending_native_image_paths_by_session   | 3538  | one-shot consume; lazy swap ~12944 (RACE)        | persistent.native_image_paths          |
| _pending_messages (runner-level, str)    | 3519  | _interrupt_and_clear_session pop; stop() flush   | persistent.pending_command_text        |
| _session_run_generation                  | 3540  | NEVER (monotonic, #28686)                        | persistent.run_generation (never reset)|

Races eliminated: every `self._X = {}` lazy-init/reset replaced the WHOLE
dict, so a writer on session A racing a lazy init triggered by session B
could lose its entry. All six such sites (_session_reasoning_overrides
~5692, _session_service_tier_overrides ~5738, _queued_events ~5204,
_pending_turn_sidecar_notes ~19832, _session_ephemeral_pin ~19885,
_session_vc_last ~19864, _pending_native_image_paths_by_session ~12944,
_turn_lease_tokens ~13644) are now per-session field writes on an existing
SessionState; a reset can no longer cross sessions structurally.

Registry successors:
- _release_running_agent_state -> state.turn.clear() (one structured reset
  instead of the drifting pop-list; still pops the slot lease and calls
  lease.release() first; still generation-guarded).
- _CONVERSATION_SCOPED_STATE funnel -> state.conversation.clear(); the
  tuple is retained for legacy plain-dict stores not yet folded in
  (_pending_model_notes) and for the public test contract.
- _turn_lease_tokens' (key, generation) tuple key -> lease_token +
  lease_generation fields; release/rebind only match when the generation is
  current, preserving the #28686/#64934 ownership check.
- _session_run_generation stays monotonic on persistent.run_generation and
  is never cleared (conversation boundaries and turn releases don't touch it).

Compatibility adapters: tests (and a few mixin call sites) access the old
dict names directly (137 direct assignments to _running_agents alone), so
each legacy name is kept as a thin @property returning a live MutableMapping
view over the corresponding SessionState field (legacy_dict_property /
legacy_lease_token_property in session_state.py). Setter accepts a plain
dict (the `runner._X = {...}` test pattern); views support ==, in, len,
.get/.pop/.clear. The shutdown path in _stop_impl deliberately keeps
duck-typed legacy-attribute access because test fakes borrow it with plain
dicts.

Name collision noted (NOT touched, out of scope): gateway/platforms/base.py
has its own _pending_messages Dict[str, MessageEvent] (adapter-level slot);
the runner-level Dict[str, str] of the same name is what moved to
persistent.pending_command_text.

Entry leaks preserved (follow-up, no new eviction in this PR): SessionState
entries in self._sessions are never evicted, matching the old dicts — e.g.
_last_resolved_model, _session_run_generation, _session_vc_last entries for
dead sessions leaked before and their fields still occupy a SessionState now.

Verification: 123 tests/gateway files referencing the old names +
_release_running_agent_state + _CONVERSATION_SCOPED_STATE all pass (sole
failure test_feishu.py::test_websocket_sdk_accepts_channel_ua_tag is
pre-existing, stash-verified); 8 non-gateway test files touching the names
pass (266 tests); `import gateway.run` subprocess smoke OK; ruff clean;
post-migration grep shows zero non-comment `self._<oldname>` references in
run.py outside the property adapters and the duck-typed shutdown block.
2026-07-29 12:11:15 -07:00
Ben Barclay
b521fd9dc9 docs(relay): finish the QA-N scrub in the new relay test files
The earlier scrub covered adapter.py; nine internal QA-campaign tracker IDs
remained in the two new test files, including a module docstring and an
assertion message. They mean nothing to a future reader — describe the
behavior instead. Comments only, no assertion changes.
2026-07-29 12:03:01 -07:00
Ben Barclay
0dc293f4f7 fix(relay): coerce Slack behavior flags exactly as the native adapter does
Both relay Slack knobs read their value through bool(), while the native
adapter they mirror uses str(raw).strip().lower() in {"1","true","yes","on"}.
A YAML-quoted string diverges:

    dm_top_level_threads_as_sessions: "false"   → relay True, native False

Non-empty strings are truthy, so the escape hatch is silently ignored in
exactly the shape an operator writes to switch it OFF. reply_in_thread has the
same defect and gates reply placement, session keying and run.py's progress
resolver, so one quoted "false" misfires three ways.

Route both through a shared _coerce_flag mirroring native's predicate. Real
booleans pass through untouched; None falls back to the default. Contract §8
documents the accepted spellings.

Tests: both knobs parametrized over the true/false spellings native accepts,
plus the absent-key default.
2026-07-29 12:03:01 -07:00
Ben Barclay
33833e232e fix(relay): apply the Slack thread anchor on the media lane too
The DM thread-anchor contract was resolved only in send(). _send_media() —
backing send_image, send_image_file, send_voice, send_video and send_document
— passed reply_to straight to the frame and never touched metadata, so
attachments egressing through the same connector-side Slack sender got both
failure shapes this branch set out to remove:

  flat mode   → reply_to survives, the image threads UNDER the user's DM
                message (the original reported symptom)
  thread mode → no metadata.thread_id, and threadTs() never reads reply_to,
                so the image lands in the home channel instead of the
                per-message thread

Both are reachable: gateway/run.py delivers agent artifacts through
send_voice/send_document.

Extract the three steps that must always happen together (mode gate, mirrored
reply_to_message_id strip, metadata promotion) into
_apply_slack_thread_anchor and route BOTH lanes through it, so text and media
cannot drift again. The media lane copies caller metadata rather than mutating
it — these helpers are called in loops with a shared mapping.

Also fold send_typing/stop_typing's duplicated status-anchor blocks into
_with_status_thread_anchor. They had already drifted (stop_typing omitted the
platform check) and the clear must target the thread the heartbeat set or the
status line sticks until Slack's own timeout.

Tests: media lane pinned in both modes plus the channel and
no-caller-mutation cases; verified as real by reverting the fix and watching
them fail.
2026-07-29 12:03:00 -07:00
brooklyn!
3bb239750d
Merge pull request #74286 from NousResearch/bb/tool-ticker-clip
fix(desktop): let an opened tool row escape the live run's one-line window
2026-07-29 14:01:51 -05:00
teknium1
30c783589c perf(agent): cursor/memo optimizations for per-iteration full-history walks (byte-parity proven)
Three provably-safe optimizations for O(n)-per-iteration history walks:

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

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

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

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

Measured (median of 5): sanitize 0.097ms->0.011ms, tokens 1.145ms->0.853ms,
persist-scan 179.5us->10.0us at 500 messages.
2026-07-29 11:54:18 -07:00