Commit Graph

24886 Commits

Author SHA1 Message Date
Teknium 9ab056d4e8 chore: map wingkwong contributor email 2026-08-23 18:27:32 -07:00
Teknium f2639f8872 fix(cron): preserve map keys as ids and skip junk values when flattening id-keyed jobs.json
Harden the id-keyed-map flatten with an id-preserving merge:
{**value, "id": value.get("id") or key} — an inline "id" wins,
otherwise the map key is adopted (external tools often key by id and
omit the inline copy; plain list(values) would emit id-less records
that collide or get dropped downstream). Non-dict junk values are
skipped with a warning instead of crashing the load. The self-heal
rewrite persists the id-merged, junk-free records.

Tests: key adopted when no inline id (and inline id wins over a
differing key), non-dict junk skipped with warning + list_jobs
survives + self-heal persists only valid records, all-junk map
flattens to [].
2026-08-23 18:27:32 -07:00
Teknium ec4b3bc06a fix(cron): self-heal id-keyed jobs.json to canonical list form on load
Layer on the load-boundary flatten: when load_jobs() encounters an
ID-keyed jobs map ({"jobs": {"<job_id>": {...}, ...}} — written by
external tools or hand edits, never by save_jobs()), it now not only
flattens to the list contract but persists the canonical
{"jobs": [...]} form back to disk via the existing auto-repair path
(save_jobs), so the store self-heals and subsequent reads are
idempotent.

Note: _peek_jobs_unlocked() intentionally does NOT tolerate the dict
shape — it returns None so the save path never shrink-merges against
an unrepaired baseline. The flatten + repair live only at the
load_jobs() boundary.

Regression tests cover the flatten, the reported list_jobs() traceback
path, idempotent on-disk repair, and the empty-map edge case.

Salvaged from PR #92994.

Co-authored-by: a-yeyang <88581400+a-yeyang@users.noreply.github.com>
2026-08-23 18:27:32 -07:00
WK Wong 5a24dcf4f2 fix(cron): normalize id-keyed jobs stores on load 2026-08-23 18:27:32 -07:00
Teknium 2e75862790 fix(install.ps1): record 'skipped-long-path' when ConvertTo-LongPath short-circuits
The ordinary-long-path early return now records why no resolver ran, so
the ResolvedPathReport stays truthful instead of silently inheriting a
stale value from an earlier call in the same session. Diagnostics hunk
taken from PR #93100.

Co-authored-by: aniruddhaadak80 <aniruddhaadak80@users.noreply.github.com>
2026-08-23 18:27:21 -07:00
liuhao1024 203f111c98 fix(install.ps1): initialize LastResolver before the resolved-path report
ConvertTo-LongPath short-circuits for ordinary long paths (no ~\d alias),
so $script:LastResolver is only assigned when a short path actually needs
expansion. The ResolvedPathReport block read it unconditionally, which is
fatal under Set-StrictMode before any install stage runs (#93017: fresh
installs died at line 367 through three different invocation styles).

Initialize it to 'none' — the resolver's own value for "nothing ran" — at
script scope before Set-LongProfileEnvVars can invoke a resolver.
2026-08-23 18:27:21 -07:00
Teknium 3e3e6f94a0 test(cua): pin PATH-preservation contract, not byte equality
The CUA spawn-env tests froze PATH == '/usr/bin:/bin' verbatim.
_sanitize_subprocess_env now (intentionally) prepends the hermes
console-script dir for all sanitized children (#92998), so these
assertions flip to the contract: original entries preserved as
suffix, hermes bin dir first when prepended.
2026-08-23 18:27:15 -07:00
Teknium 83bb0f1b40 chore: map UniversePeak contributor email 2026-08-23 18:27:15 -07:00
Teknium ec06e706f1 test(cron): e2e regression — scrubbed child env resolves bare hermes under minimal parent PATH
Exercises the real build_subprocess_env()/_resolve_hermes_bin_dir chain (no
helper mocks) under a simulated systemd/cron minimal PATH, the exact call
path cron/scheduler._run_job_script uses. Companion to #93082.
2026-08-23 18:27:15 -07:00
UniversePeak b0001f45a2 fix(cron): keep hermes console script on child PATH 2026-08-23 18:27:15 -07:00
Teknium 04dd2bb233 test(agent): drain truncation warnings before and after each prompt-builder test
Follow-up to the ContextVar-leak fix: the autouse fixture now drains on
both sides (drain(); yield; drain()) so earlier files can't pollute this
file's assertions either.
2026-08-23 18:27:12 -07:00
Aniruddha Adak f168d857c3 test(agent): stop truncation-warning ContextVar leaking between test files
Running `pytest tests/agent/test_prompt_builder.py
tests/agent/test_system_prompt.py` failed
test_build_system_prompt_records_stable_prefix with AttributeError:
'...SimpleNamespace' object has no attribute '_emit_status'
(#93018). A truncation warning recorded by test_prompt_builder.py stays
in the shared thread context under plain pytest, so the later file's
build_system_prompt call drains a warning and forwards it to
agent._emit_status - which the test stub lacked.

Harden both sides:

- tests/agent/test_system_prompt.py: _make_agent() stub gains a no-op
  _emit_status, so draining a stray warning is harmless.
- tests/agent/test_prompt_builder.py: autouse fixture drains pending
  truncation warnings after every test, leaving the ContextVar clean.

The order-dependent failure no longer reproduces in either ordering.
2026-08-23 18:27:12 -07:00
aniruddhaadak80 ac83bb0667 chore: map contributor email aniruddhaadak80@gmail.com 2026-08-23 18:27:07 -07:00
aniruddhaadak80 cd6c088928 test(compression): align no-op strike tests with structural backoff (#93022)
Two suites still encoded the pre-#93093 contract that the three
structural no-op branches (insufficient_messages, no_compressible_window,
empty_post_handoff_window) increment _ineffective_compression_count:

- tests/agent/test_compaction_anti_thrash.py::
  TestMinimumMessagesBranch::test_too_few_messages_records_an_ineffective_pass
- tests/run_agent/test_infinite_compaction_loop.py::
  TestCompressNoOpRegistersIneffective::{test_no_op_increments_counter,
  test_two_no_ops_block_should_compress}

Structural no-ops are transcript-shape facts, not evidence of an
incompressible floor, so they now arm _structural_no_op_backoff_until
and leave the strike counter untouched. Update the tests to pin the new
contract (count unchanged, backoff armed via time.monotonic(),
should_compress blocked while it holds) and rename accordingly. The
outcome contract of test_two_no_ops_block_should_compress is preserved:
repeated no-ops still block further automatic compression.
2026-08-23 18:27:07 -07:00
Aniruddha Adak f778c0d941 fix(compression): structural no-ops defer retries instead of striking the breaker
Fixes #93022. A short session (protection window >= transcript) hits the
"insufficient messages" / "no compressible window" branches twice and
permanently trips the anti-thrash breaker, even though nothing was
eligible to compress - compression was never attempted, so there is
nothing "ineffective" to score. The session then rides past the
threshold with no compaction possible (recovery probes only soften,
not fix, the misclassification).

Distinguish "nothing eligible right now" from "attempted and
underperformed":

- New transient _structural_no_op_backoff_until (in-memory, 300s)
  armed by _record_structural_no_op() at the three structural no-op
  sites: insufficient_messages, no_compressible_window,
  empty_post_handoff_window. No strikes accumulate; auto-compaction
  resumes on its own once the backoff lapses or the transcript outgrows
  the protection window.
- The backoff gates should_compress via
  _automatic_compression_blocked_locally and surfaces in
  _compression_block_reason as "structural_backoff:<seconds>".
- #40803's frozen-CLI guarantee is preserved: a transcript that can
  never shrink retries at most once per backoff window instead of
  every turn.
- force=True (/compress) clears an active backoff before attempting;
  record_completed_compaction() lifts it - both prove the transcript
  is compressible/being worked.
- Genuine attempted-but-underperformed verdicts still strike the
  durable ineffective counter unchanged.

Tests: new tests/agent/test_context_compressor_structural_backoff.py;
updated the two tests that asserted the old strike-on-noop behavior.
2026-08-23 18:27:07 -07:00
Teknium 4bc31a435b chore: map Aintworth contributor email 2026-08-23 18:27:04 -07:00
Aintworth ce51f535d3 test(gemini): cover nested/list/non-pointer ref cases; document false-positive tolerance
Address review feedback:
- Add tests for deeply-nested $ref (recursion), top-level JSON array
  (already wrapped, no 400 path), and $ref without '#/' prefix (stays
  structured).
- Document the deliberate structural (false-positive-tolerant) detection and
  its O(n) cost in the helper docstring.
2026-08-23 18:27:04 -07:00
Aintworth 03477166f9 fix(gemini): wrap schema-bearing tool results as opaque text
Gemini 3 resolves JSON-Schema $ref/$defs pointers inside a
functionResponse.response payload and rejects unknown references with
HTTP 400 INVALID_ARGUMENT ('referenced name #/$defs/...' does not match
a display_name; see vercel/ai#14369).

tool_describe (and any tool whose result is itself a JSON Schema) returns
schema text that previously went back as a structured response, tripping
Gemini's pointer resolution. Detect such results with a $ref-pointer scan
and wrap them as opaque text instead.

Adds regression tests for the wrap path and the unchanged structured path.
2026-08-23 18:27:04 -07:00
Brooklyn Nicholson c25f206ecf fix(desktop): show the launch-source preference for a single connection
The toggle was gated on having 2+ registered sources, which hid it in exactly
the local-only state the drift produces — the state where a user most needs to
change what launch restores.
2026-08-23 20:26:46 -05:00
Brooklyn Nicholson d5463b3f46 fix(desktop): boot restore never overrides a live unnameable source
Reconciliation repairs the drift at its source, but it can still fail to
persist (read-only or full userData), which leaves a window live on a source
the registry cannot name. $activeConnectionId is null there, the preferred-id
guard misses, and the restore re-homes a working connection.

Return early when a connection is live but unnameable. The registry has no
claim on a source it does not know about.
2026-08-23 20:26:46 -05:00
Brooklyn Nicholson aaf63220ca fix(desktop): heal v1/v2 connection drift instead of re-homing onto local
migrateV1ToRegistry runs exactly once, only when connections.json is absent.
A user who was local at that moment and pointed Settings -> Gateway at a
remote afterwards gets a live remote the registry cannot name: the descriptor
resolves to no connectionId, primary still says 'local', and the boot-time
launch pick force-switches the window onto a fresh local backend seconds after
the sessions list paints. That backend has no provider, so onboarding pops.

Reconcile on read: when the v1 global route names a remote with no matching
registry entry, register it and adopt it as primary/last-used, then persist so
the repair happens once. Narrow on purpose — an already-registered route is
left alone even when primary names something else, because that is the user's
pick in the Connections panel, not drift.

Replaces the hand-edit-connections.json workaround users have been trading.
2026-08-23 20:26:46 -05:00
ClintonEmok 4fea0f04be fix(desktop): boot-time source restore keeps the All-profiles preference (#93197)
The showAllProfiles browse-mode flag is persisted to localStorage, but
every restart it was force-collapsed anyway: initializeConnectionsRegistry
restores the last-used source via selectConnection, and selectConnection's
post-activation path unconditionally ran $showAllProfiles.set(false).

That collapse is correct for a user click on the connection picker (a
concrete-source action), but the silent boot restore is not a user action.
Gate both reset sites on pendingTarget === null && activeConnectionId ===
null (the fresh-boot state) so the persisted preference survives restart,
while any user-initiated switch still collapses browse mode.

Regression tests cover both directions: boot restore preserves true, a
user switch collapses it.

Fixes #93197
2026-08-23 20:26:46 -05:00
Brooklyn Nicholson c5bf3b7ce3 test(desktop): replace apply/liveness source-regex assertions with behavior tests
The salvaged hardening tests matched main.ts source text to assert that
fetchConnectionStatus reaches for a bearer and that Apply preflights before
persisting. A rename breaks them while a real auth regression that keeps the
substrings passes.

Make the preflight a first-class option on applyConnectionConfigAtomically so
its ordering is observable, and assert it through the seam: preflight runs
before either write, and a rejected preflight leaves both stores and the
activation untouched.
2026-08-23 20:26:46 -05:00
Alessandro Levantini 96e5898542 fix(desktop): synchronize applied gateway registry 2026-08-23 20:26:46 -05:00
Alessandro Levantini 6a25ec0758 fix(desktop): authenticate remote liveness probes 2026-08-23 20:26:46 -05:00
Teknium cce2d9418b chore(tests): remove the never-executed kanban stress/chaos suite
tests/stress/ was dead weight: its own conftest set
collect_ignore_glob = ["*.py"], so pytest has never collected a single
file from it, the advertised --run-stress flag was a permanent no-op,
and no CI workflow ever invoked the scripts (#93135). Rather than wire
a nightly lane for scripts that were never verified end-to-end, remove
the suite. Kanban concurrency behavior remains covered by the regular
tests under tests/hermes_cli/ and tests/gateway/.

Closes #93135.
2026-08-23 18:25:49 -07:00
Teknium 31ade5da7b chore: add contributor email mappings for salvage 2026-08-23 18:25:42 -07:00
Teknium a6bec08f35 fix(install): actually invoke check_cxx_compiler in both install stages
Salvage follow-up for #88993: the preflight was defined but never
called from the prerequisites stage or the full-install path, so the
Fedora node-gyp failure (#93063) would still occur. Wire it in after
check_node in both sequences.
2026-08-23 18:25:42 -07:00
luckygreen 1fc9c1b41b fix(install): refresh Playwright upgrade for current main
Reapply the Playwright dependency update from
NousResearch/hermes-agent#77773 and regenerate package-lock.json
against current main.

This fixes the Chromium installation hang under Node 26.

Fixes NousResearch/hermes-agent#76312
Supersedes NousResearch/hermes-agent#77773
2026-08-23 18:25:42 -07:00
osgeek90 60cbc70430 installer: check for a C++ compiler before building native Node modules
npm install inside install_node_deps() builds native addons (e.g. node-pty) via node-gyp, which needs a C/C++ compiler. That was never checked, so a missing g++ only surfaced as a generic "npm install failed or timed out" deep inside npm's own output — and because install_node_deps failing short-circuits the rest of main() via `|| return`, users end up with no `hermes` command and no clue why.
2026-08-23 18:25:42 -07:00
Teknium 63b42d3070 chore: add contributor email mappings for salvage 2026-08-23 18:25:35 -07:00
fangliquanflq 8fca54f9f1 test(bots): pin draft sweep age boundary 2026-08-23 18:25:35 -07:00
fangliquanflq 1b18442f36 fix(bots): protect new drafts from title sweep 2026-08-23 18:25:35 -07:00
justcarlosm 478a09c06b fix(browser): floor browser-use CLI subprocess PATH with sane system dirs
Profile-spawned workers (kanban bots, cron jobs) can inherit a PATH of
only version-manager dirs — observed in the wild as one nvm node dir
repeated 7x. The uv-installed browser-use binary is a POSIX sh
trampoline that resolves dirname/realpath through PATH, so it died
with 'realpath: not found … exec: /python: not found' (exit 127)
before its own Python ever started.

_base_subprocess_env now floors the child PATH via browser_tool's
_merge_browser_path (the agent-browser backend already guards the same
hazard), degrading to appending FHS bin dirs if that import is ever
unavailable. Windows is a no-op (.cmd shims don't trampoline).

Verified: unit tests + real uvx browser-use --version under a
nvm-only-PATH worker env, rc 127 -> rc 0.
2026-08-23 18:25:35 -07:00
liuhao1024 8f9abc9873 fix(cron): re-anchor stale next_run_at after direct jobs.json schedule edits
get_due_jobs() fires purely off the stored next_run_at <= now, with no
check that the stored instant is still an occurrence of the schedule's
current expression. A direct jobs.json edit that narrows schedule.expr
(e.g. daily "0 7 * * *" -> weekdays "0 7 * * 1-5") keeps the stored
next_run_at computed under the old expression, so the job fires on days
the new expression excludes. The within-grace fire and the catch-up
"run once now" path both inherit the wrong instant.

Add a best-effort stale-schedule guard on the fire path: when the stored
next_run_at is not an occurrence of the current cron expression,
re-anchor it via compute_next_run() from the current expression and skip
the fire. Non-cron kinds, missing expr, croniter unavailability, and
malformed input all report a match so the fire path keeps its existing
semantics. Recomputation uses the current expression, so the re-anchor
converges and cannot defer a valid job forever.

Fixes #93049
2026-08-23 18:25:35 -07:00
liuhao1024 5d8b031514 fix(stt): surface the selection-specific error for explicit openai STT
When the managed openai-audio gateway is unavailable,
_resolve_openai_audio_client_config() raises a ValueError that names the
blocker (and, for managed-Nous users, the `hermes tools` remediation).
The boolean probe in _get_provider's explicit-openai branch flattened
that into False, so the log claimed "no API key available" and the
transcription result returned the all-provider install hint -- pointing
operators at unrelated setup instead of their managed route (#93045).

Resolve the config directly in the branch so the warning names the real
blocker, and let the dispatch's "none" fallback surface the
selection-specific error for an explicit openai choice. No fallback is
added: an unavailable selection still resolves to "none", it just
reports why.
2026-08-23 18:25:35 -07:00
王雪帆 c4871226f1 fix(cli): honor target_model when resolving custom providers
resolve_runtime_provider() documents target_model as the explicit model
override for mid-session switches and auxiliary slots, but the custom
provider path (_resolve_named_custom_runtime) never received it and
silently substituted the provider's configured default_model instead.

This made auxiliary slots such as auxiliary.background_review silently
run the provider's default model rather than the configured one — e.g. an
ocx-proxy slot configured for gemini-flash actually executed
cursor/claude-sonnet-5, hitting upstream rate limits.

Pass target_model through to the custom runtime resolver and prefer it
over the provider's default model in both the pooled and non-pooled
credential paths.
2026-08-23 18:25:35 -07:00
Andrex Ibiza, MBA 6d48fbed1b fix(tui): settle tmux clipboard load on child exit
Mark the write-only tmux load-buffer call as resolve-on-exit so a daemonized tmux server cannot retain inherited stdio and force a false timeout after the direct child has succeeded.

Completes the call-site acceptance item from #93134 as a companion to #93148.

Co-authored-by: JoaoMarcos44 <87440198+JoaoMarcos44@users.noreply.github.com>
2026-08-23 18:25:27 -07:00
liuhao1024 27fb1179f8 fix(tui): settle execFileNoThrow on timeout even when a daemon holds stdio
The timeout handler only called settle(124) when resolveOnExit was true.
In the default path the promise waits for 'close', which requires every
inherited stdio handle to close — a daemonized grandchild that kept the
pipes open meant 'close' never fired, and after the timeout SIGTERM
(which only reaches the direct child) nothing settled the promise. The
await hung forever: the clipboard path (setClipboard -> tmuxLoadBuffer
-> osc.ts spawn without resolveOnExit) leaked a pending promise whenever
a spawned tool forked a stdio-inheriting daemon (#93134).

Settle(124) unconditionally in the timeout handler. The settled-guard
makes it a no-op when the child's own 'exit'/'close' won the race, so
normal timeout behavior is unchanged; in the daemon case it becomes the
only exit and returns the same 124 the close path would have.

Also un-skips the documented-hang regression test, with a 30s daemon
sleeper so it genuinely outlives the timeout (and vitest's own 5s test
timeout — before the fix the test fails by timing out, not asserting),
plus an elapsed bound.
2026-08-23 18:25:27 -07:00
Finn763 74e6885f0d fix(review): fail-closed compressor detachment + warm-cache first request (#93057 review)
Adversarial-review fixes for the #93057 snapshot-compaction PR:

- Fail-closed detachment: only re-enable compression after
  bind_session_state successfully severs the engine's parent binding.
  A failed rebind keeps the historical compression_enabled=False
  behavior and warns, instead of running compaction against a
  compressor still bound to the parent's SessionDB (#38727 re-open).
- Warm-cache parity: defer both compression gates (turn-prologue
  preflight + pre-API pressure check) until the fork's first provider
  response, so the first request replays the full snapshot as the
  intended cached read and compaction applies from the second request
  on — matching the documented budget mental model.
- Tests: regression for the rebind-failure fail-closed path (red on
  pre-fix code) and the existing threshold-crossing test reworked to a
  two-request review asserting the warm first request + compacted
  second request. 116 tests green across all touched suites; ruff
  clean.
2026-08-23 18:25:19 -07:00
Finn763 4202a508fd fix(review): bound same-model background review replay
Detach the review fork's compressor from the parent SessionDB/session_id
and re-enable in-memory-only compaction for oversized snapshots, instead
of the historical compression_enabled=False guard that left the fork's
replayed transcript unbounded (350k-384k input tokens per request, 1.49M
total across one 8-request review). Add an aggregate input-token budget
(auxiliary.background_review.max_input_tokens, default 600k) so repeated
tool calls cannot recreate an unbounded transcript; the tool loop stops
before the provider call that would cross it.

Closes #93057
2026-08-23 18:25:19 -07:00
fangliquanflq 2033f4cc34 fix(agent): separate cancellation diagnostics from tool output 2026-08-23 18:25:19 -07:00
fangliquanflq c1c0efa375 fix(code-exec): preserve interrupt cancellation source 2026-08-23 18:25:19 -07:00
fangliquanflq ee8a66233f test(gateway): preserve replacement handles across close races 2026-08-23 18:25:12 -07:00
fangliquanflq 80cec2785d fix(gateway): preserve routing state across recovery 2026-08-23 18:25:12 -07:00
fangliquanflq 4b659f0e33 fix(gateway): retry failed session database opens 2026-08-23 18:25:12 -07:00
Andrex Ibiza, MBA 31a01f373b fix(state): make automatic repair non-destructive
Reproduce the schema-btree failure where the in-place writable_schema/VACUUM ladder can reduce a 3,048-page canonical state.db to 113 pages and still return repaired=False.

Move all mutating strategies behind a complete SQLite online-backup snapshot, retain one exclusive SQLite guard from staging through transactional promotion, preserve committed WAL frames and the live inode, fail closed on environmental hazards, and add adversarial regression coverage for failed-repair preservation, post-stage writer races, interrupted copies, stale scratch, disk admission, attempt-ledger semantics, and durability routing.

Fixes #93064
Supersedes the delivery mechanics of #87409 while preserving its implementation provenance.

Co-authored-by: cervantesh <11169707+cervantesh@users.noreply.github.com>
2026-08-23 18:25:12 -07:00
Teknium a2a43f7e82 fix(agent): widen composite-id alias matching to the compressor; unify variant policy owners (#63000)
Follow-up on top of the salvaged #93335:

- context_compressor._sanitize_tool_pairs now expands alias spellings on
  the RESULT side too (tool_result_id_variants), so a composite
  call|item-keyed result pairs with its split-field tool_call instead of
  being dropped and its call stripped.
- The compressor's _tool_call_id_variants staticmethod and
  agent_runtime_helpers' module-level _tool_call_id_variants are now thin
  forwarders to agent.message_sanitization.tool_call_id_variants — one
  policy owner for alias expansion, so the pre-call sanitizer, repair
  pass, dedup pass, and compression sanitizer can never drift apart.
- Preserved the #91768 SDK-object tolerance in repair pass 1 (the
  shared helper handles non-dict tool_calls via getattr; the salvaged
  commit's isinstance-dict guard was dropped in the merge resolution).

New regression tests: composite-keyed results through
sanitize_api_messages (both directions) and _sanitize_tool_pairs, with
negative controls. Sabotage-verified: compressor test fails with raw
tool_call_id tracking.
2026-08-23 18:24:43 -07:00
joaomarcos 5496d5995a fix(agent): preserve tool results across ID variants
Match Responses/Codex tool-call aliases across execution, repair, sanitization, replay, and duplicate handling so valid parallel results are not replaced by unavailable stubs.\n\nFixes #93251
2026-08-23 18:24:43 -07:00
Teknium a9e46229b2 fix: sniff fast-path keys on binary magic only, not NUL presence
A newer main-side sniff fast-path skipped any file with a NUL in its
head, short-circuiting before the magic-number check the salvaged fix
added — reintroducing the #77927 bypass. Key the fast-path on
executable magic only; NUL-bearing text falls through to the tail
logic (magic check, size-before-strip, NUL-strip, scan).
2026-08-23 18:24:36 -07:00