Commit graph

8917 commits

Author SHA1 Message Date
Brooklyn Nicholson
29dd621498 fix(tui_gateway): bind the branched agent to the parent profile's home + state.db
session.branch wrote the child ROW into the parent's profile db but
built the live agent with the launch defaults: _make_agent fell back to
_get_db() and no HERMES_HOME override was active. The branched agent's
own message flushes — and any later compression rotation it performed —
therefore landed back on the launch profile, splitting the lineage one
turn after the branch. Mirror session.create/resume: open the parent
profile's SessionDB for the agent and hold the home override across the
build, so config/skills/memory resolve to the profile too.

Spotted in #70605's sibling implementation of the same fix.

Co-authored-by: HexLab98 <liruixinch@outlook.com>
2026-07-24 10:05:05 -05:00
Brooklyn Nicholson
e9a243ef78 fix(state): inherit and stamp profile_name across rotation and branch children
profile_name was only written on the agent's initial lazy create
(e8b7ce8c1); every parented child row — compression rotation, TUI
/branch, desktop branch first-persist — was created without it. A
non-default profile's lineage therefore turned NULL on its first
compression or branch and aggregated as "default" in unified session
lists, completing the cross-profile session-jump.

Fix the class at the DB layer: _insert_session_row's parent backfill now
COALESCEs profile_name from the parent alongside cwd/git_* (#64709
pattern), so any parented child inherits its lineage's owning profile.
Stamp it explicitly at the three create sites as well — compression
rotation (mirroring _ensure_db_session), TUI session.branch, and the
TUI first-prompt row persist — so rows are self-describing even when the
parent row predates the profile_name column.
2026-07-24 01:49:22 -05:00
Bartok9
b9d2eb7f43 fix(tui_gateway): honor params.profile for session.* state.db access
Closes #62503

Root cause: session.resume already opened the requested profile's state.db,
but session.list / most_recent / delete / history / title / status, create's
immediate info payload, teardown, session.branch, and post-turn pending_title
still used the launch profile (_get_db / _current_profile_name).

Fix: add _db_for_profile / _profile_db / _session_db helpers and route
session.* methods through them; report requested profile on create/status;
lifecycle uses session-owned profile stores with branch profile_home inherit.

Rebase onto current main keeps _session_usage_snapshot in session info.
2026-07-24 01:37:43 -05:00
Teknium
23476207bc feat(moa): default advisor fanout to user_turn — the cheapest cadence
Flips the default fan-out cadence from per_iteration (advisors re-run on
every tool iteration, multiplying advisor spend by tool-loop depth) to
user_turn (advisors run once on the first message of each user turn; the
acting aggregator works the rest of the tool loop with that turn's
advice). Until per-mode benchmarks justify a costlier default, MoA
defaults to the cheapest, lowest-impact cadence (#67199).

One default for everyone — no split legacy/new-preset semantics; presets
that want per-step advising set fanout: per_iteration explicitly. All
three modes (user_turn / per_iteration / every_n:N) remain selectable;
every_n:1 still collapses to per_iteration (semantic identity), while
unparseable values now fall to user_turn (the default).

Docs updated with a default-change note; the per-iteration rerun test
pins its mode explicitly.

Co-authored-by: skyer-flyyy <188930297+skyer-flyyy@users.noreply.github.com>
2026-07-23 21:07:18 -07:00
Alan Harman-Box
accbf4d912 feat: show system_prompt_preview when skill description exceeds prompt limit
When a skill is created or edited with a description longer than
SKILL_PROMPT_DESC_LIMIT (60 chars), the tool response now includes a
system_prompt_preview field showing exactly what the system prompt
skill index will display. This gives the agent immediate feedback to
self-correct truncated trigger phrases.

Also adds tool schema guidance about the 57-char window and fixes a
stale docstring in skill_commands.py that incorrectly claimed the
system prompt renders the full description.
2026-07-23 21:06:56 -07:00
Teknium
6441b05888 fix(skills): keep xurl SKILL self-contained; move x_search routing to gated surfaces
Follow-up on the salvaged commit: the xurl skill loads even when x_search
isn't registered (check_fn-gated on xAI credentials), so per the
cross-toolset reference rule the skill must not name it. Replaced the
skill's x_search routing block and workflow step with skill-native
wording (raw engageable posts, authenticated context, write-evidence
rule). The cross-surface comparison stays on surfaces where both are
known to exist: x-search feature docs, toolset description, tools-config
setup note, and the x_search tool schema (kept generic, no tool names).
Rewrote the routing tests to pin the placement contract, including that
the skill never names credential-gated surfaces.
2026-07-23 21:06:47 -07:00
Julien Talbot
b9b100da11 docs(xai): clarify x_search vs xurl routing without schema cross-refs
Make the x_search / xurl boundary explicit in the skill, feature docs,
toolset metadata, setup note, and reference pages, while keeping the
model-facing x_search schema generic (no static xurl name).

Regression tests assert behavioral routing invariants rather than frozen
prose snapshots. Drop the stale CI-only plugin/hangup hunks already on
main so this rebases cleanly.
2026-07-23 21:06:47 -07:00
iammuhammadfurqan
2978a9e9c2 fix(skills): rename pinecone-research, shorten descriptions, add scripts + tests
- Rename research/pinecone to pinecone-research (distinct from mlops/pinecone)
- Shorten mlops/pinecone description to <=60 chars
- Add rag_pipeline.py and memory_manager.py scripts
- Add test_pinecone_research_skill.py (10 tests: frontmatter, scripts, naming)
- Remove unconditional description: prefix strip from extract_skill_description()
2026-07-23 21:06:33 -07:00
Ben Barclay
ef6ce56cad
fix(cron): reconcile external provider after a claimed direct run (#70479)
A direct run (cronjob action='run' / webhook-triggered manual fire) takes
the job's fire_claim and advances next_run_at. When it races the external
provider's scheduled fire for the same occurrence, Chronos loses the claim
and — by design — does not re-arm (the winner owns the re-arm). But the
direct-run winner never notified the provider, so the NAS one-shot for the
consumed occurrence was left stale forever and the recurring job silently
stopped firing.

Observed in production: a managed 1-minute review job stalled for 20 hours
because a GitHub-webhook direct run claimed the job 2s before the Chronos
fire arrived; every subsequent occurrence was orphaned while /api/status
stayed green.

Fix: after a *claimed* direct execution completes (success or failure —
next_run_at advances at claim time either way), call
_notify_provider_jobs_changed_safe() so the active provider re-arms the
post-run next_run_at. No-op for the built-in ticker; claim-lost direct
runs still never notify (the winning scheduler owns the re-arm).
2026-07-24 13:37:04 +10:00
Teknium
d4b6165018 fix(skills): move dogfood skill into the software-development category
skills/dogfood/SKILL.md sat at the root of the bundled skills tree,
making it one of the few uncategorized skills (Discord /skill
autocomplete listed it under 'uncategorized'; hermes_cli/commands.py
cited it as the example). Move it to
skills/software-development/dogfood/ alongside the other QA/testing
skills (test-driven-development, systematic-debugging,
requesting-code-review).

- git mv skills/dogfood -> skills/software-development/dogfood
  (history preserved)
- fix test fixture path in tests/tools/test_browser_console.py
- update root-level-skill docstring examples (commands.py,
  generate-skill-docs.py) to cite computer-use, which is still root-level
- drop 'dogfood' from the category list in hermes-agent-skill-authoring
  SKILL.md (en + zh-Hans docs mirrors)
- docs: regenerate/move the bundled page to
  software-development-dogfood (en + zh-Hans), update sidebars.ts,
  skills-catalog.md, and the adversarial-ux-test related-skills link

E2E validated: fresh sync_skills() copies to the new nested path;
existing installs with the old flat copy keep it untouched (manifest is
name-keyed, hash unchanged -> skipped, no duplicate);
_get_category_from_path resolves 'software-development'; docusaurus
build green.
2026-07-23 19:45:54 -07:00
Teknium
13fe08d7e9 fix(context-engine): short-circuit the inherited no-op select_context before any per-request work
Verification follow-up for the #51226 salvage: the host call site guarded
select_context with hasattr(), but the ABC defines a default on every
engine, so the built-in ContextCompressor (and any non-implementing
engine) still paid per-request shallow copies of the conversation
history plus a hook call on every provider request. Identity-check the
bound method against ContextEngine.select_context and return the
request untouched — mirroring the existing base-method short-circuit in
_notify_context_engine_turn_complete — so the default path does zero
work, not just produces an identical result.

Adds two pins: the base no-op is never invoked (patched-to-raise base
stays silent), and ContextCompressor.__dict__ contains neither new verb.

Also registers the contributor email mapping for @chaos-xxl.
2026-07-23 19:44:35 -07:00
xue xinglong
56e00f4ca1 docs+test(context-engine): sync public guide coverage note; pin finalization-seam observation contract
- website guide: on_turn_complete() now carries the same best-effort coverage
  caveat as the ABC docstring (fires from the finalization seam; abnormal
  early-return paths bypass it) — removes the doc/code inconsistency.
- test: finalization seam emits on_turn_complete with usage=None + the
  interrupted flag for an interrupted finalized turn. Docstring records that
  the negative early-return-bypass half is best-effort and deferred to a
  shared-seam follow-up rather than pinned via a full run_conversation harness.
2026-07-23 19:44:35 -07:00
xue xinglong
5f65f0b0f8 fix(context-engine): snapshot select_context read-only inputs; scope on_turn_complete coverage doc
Addresses the hermes-sweeper review on #51226:
- _apply_context_engine_selection now passes shallow copies of the read-only
  conversation_messages / incoming_message to the hook, so an engine mutating
  them in place cannot corrupt persisted transcript state (enforces the
  request-only contract, not just documents it). Adds a mutation-regression
  test asserting persisted history + incoming message are untouched.
- on_turn_complete docstring: scope the coverage claim to the standard
  finalization seam. Some abnormal early-return paths (content-policy block,
  provider terminal failure) currently persist+return without finalization and
  don't emit the hook; documented as best-effort with a shared-seam follow-up,
  rather than over-promising a guaranteed callback for every early exit.
2026-07-23 19:44:35 -07:00
xue xinglong
915942935d fix(context-engine): fail open on empty select_context() result + doc public hooks
- _apply_context_engine_selection: reject an empty list. all([]) is True, so
  a [] returned by a failing/buggy engine previously replaced a valid request
  with an empty message list the downstream sanitizers can't restore; now it
  falls open to the unmodified request (honors the fail-open contract).
  Thanks @johnnykor82 for catching this on #41918's review.
- test: empty list keeps the original request (fail-open regression).
- docs: document select_context()/on_turn_complete() in the public
  context-engine plugin guide (were still describing only the old contract).
2026-07-23 19:44:35 -07:00
xue xinglong
71220cdf5b docs+test(context-engine): document select_context ordering/cache contract; add cache-stability + downstream-sanitizer tests
- context_engine.py: document that select_context() runs before cache-control
  and all request sanitizers, so (a) replacements still pass host validation
  and (b) the no-op default keeps the request byte-stable (AGENTS.md prompt-
  cache invariant). Note the hook is evaluated per provider request.
- tests: no-op path is byte-stable for cache-control; a role-unusual
  replacement is passed through for the existing downstream sanitizers to
  normalize (select_context does structural validation only).
2026-07-23 19:44:35 -07:00
xue xinglong
589cbafb87 feat(context-engine): forward real usage to on_turn_complete()
The on_turn_complete() observation hook is the engine's post-turn signal,
so it should receive the completed turn's canonical token usage when the
host has it, not a hardcoded None. Per @johnnykor82's #41918 contract: the
engine uses prompt/completion + cache_read/write/reasoning buckets to judge
how large/expensive the selected context was before the next select_context().

- conversation_loop.py: stash the most recent provider response's usage_dict
  (the same canonical shape fed to update_from_response) on the agent as
  _last_turn_usage; reset to None at turn start so turns that never reach a
  provider response (early failure / interrupt) forward None, not a stale
  prior turn's usage.
- turn_finalizer.py: forward agent._last_turn_usage instead of usage=None.
- context_engine.py: document the usage param contract on the ABC hook.
- tests: cover both ends through the real finalize_turn path — completed turn
  forwards the full canonical bucket set intact; no-response turn forwards None.

Co-authored-by: johnnykor82 <johnnykor82@users.noreply.github.com>
2026-07-23 19:44:35 -07:00
xue xinglong
bb9ef9d72c feat(context-engine): add on_turn_complete() observation hook
Adds the post-turn observation verb as the companion to select_context():
an optional, no-op-default on_turn_complete() called once after the
assistant/tool loop finishes, with the finalized transcript snapshot. Lets
an engine ingest/index/summarize the completed turn to inform the next
select_context(). Wired via _notify_context_engine_turn_complete() from
turn_finalizer.finalize_turn(); fail-open, base no-op short-circuited so
non-implementing engines (incl. the built-in compressor) pay nothing.

This is the request-assembly + observation pair from #41918; with this
commit the PR fully subsumes #41918's two hooks (prepare_request_messages
-> select_context, on_turn_complete) rather than only the selection half.

Co-authored-by: johnnykor82 <johnnykor82@users.noreply.github.com>
2026-07-23 19:44:35 -07:00
xue xinglong
dec464c351 feat(context-engine): add select_context() per-turn selection hook
Adds an optional, no-op-default select_context() hook to the ContextEngine
ABC, called every turn after the request messages are assembled and before
provider dispatch — independent of should_compress(). Lets an engine select
or replace which context enters the prompt for a single request (retrieval,
topic routing, role/branch switching) without mutating persisted history,
removing the need to abuse should_compress()=True as a per-turn callback.

The host call site (_apply_context_engine_selection) is fail-open: a missing
hook, an exception, or an invalid return value leaves the assembled request
untouched. Additive and non-breaking: the built-in compressor and every
existing engine are unaffected.

Consolidates the per-turn request-assembly surface proposed across #41918,

Related: #36765 #41918 #24949 #47109 #50053 #23837 #25115 #29370
2026-07-23 19:44:35 -07:00
Teknium
4025329ac4
feat(gateway): opt-in compression progress notices via compression.progress_notices (#52995) (#70457)
Routine automatic compression stays silent-by-design on chat platforms
(default unchanged, byte-identical). New opt-in config key
compression.progress_notices (bool, default false) opens a gate on the
gateway noise filter (_prepare_gateway_status_message) that lets ROUTINE
compression progress statuses through to chat surfaces.

- Membership is derived from the #69550 status template constants in
  agent/conversation_compression.py (compiled to a literal-escaped regex,
  never re-inlined wording), so unrelated noisy statuses (aux failures,
  provider retry/rate-limit chatter) stay suppressed even when enabled.
- The compaction completion notice (COMPACTION_DONE_STATUS, #69546
  lifecycle 'compacted' edge) already flows through the status path and
  passes the filter — no new emit site needed.
- Config wired everywhere: hermes_cli/config.py DEFAULT_CONFIG,
  cli-config.yaml.example, gateway raw-YAML read (live, mtime-cached),
  gateway hot-reload cache-busting key list, website configuration docs.
- Failure notices and manual /compress feedback remain always-visible;
  VISIBLE_COMPRESSION_MESSAGES and emit sites untouched.

Design by @havok-training (issue #52995).
2026-07-23 19:44:19 -07:00
Teknium
7b65073dc9 fix(moa): tolerate SDK-shaped tool_call entries in _render_tool_calls
_render_tool_calls only handled dict-shaped entries; a SimpleNamespace-
shaped tool_call (SDK-style stream-stitched responses) rendered as
'[called tool: tool]', silently losing the function name and arguments
from the advisory view. Handle both shapes (including a namespace-shaped
nested function inside a dict entry).

One-hunk hardening salvaged from closed #59712.

Co-authored-by: SquabbyZ <601709253@qq.com>
2026-07-23 18:40:09 -07:00
Teknium
975eb3a365 fix(moa): trim reference messages to fit each model's context window
Reference models may have a smaller context window than the aggregator
(e.g. kimi-k2.7-code @ 262K advising a glm-5.2 @ 1M conversation).
Without context-length protection, a reference whose window is exceeded
gets a hard HTTP 400 from the provider, which _run_reference's
try/except silently converts to a [failed: …] note — the MoA turn
silently degrades to fewer references (#60345).

Redesigned implementation of #60387:
- Estimate AFTER the advisory system prompt is prepended, so the
  request that is actually sent is what gets budgeted.
- Reserve output headroom: the preset's reference_max_tokens when set,
  else an 8192-token constant, plus a 10% estimator-error fraction.
- Trim on advisory-view boundaries (text-only user/assistant turns; no
  tool-result frames to orphan), preserving the system prompt, the
  user-first invariant after every pop (never assistant-first), and the
  trailing synthetic user turn.
- Cache get_model_context_length per (provider, model) in a per-fan-out
  dict shared across the worker threads, so a turn resolves each
  window once instead of probing metadata sources
  per-reference-per-iteration (failures are cached too).

Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
2026-07-23 18:40:09 -07:00
Teknium
55f3826224 fix(moa): keep real accounting for interrupted-but-billed references; don't cache interrupted results
Follow-ups for salvaged #56344:

- A reference that completes between the interrupt check and the reap
  keeps its REAL output and accounting (the provider call billed) instead
  of being zeroed with a placeholder.
- A reference still in flight at interrupt time gets a placeholder in the
  results, but its future now carries a done-callback that folds the
  eventual real usage/cost into the facade's pending accounting
  (late_accounting_sink -> _record_late_reference_accounting), so billed
  spend is never silently dropped. Pending totals are folded (not
  overwritten) and guarded by a lock since done-callbacks fire on
  executor worker threads.
- Interrupted placeholder results are no longer written into the facade's
  turn-scoped reference cache: a cache HIT never re-runs references, so
  caching a partial snapshot would replay '[skipped: interrupted by
  user]' notes for the rest of the turn. The cache is left empty and the
  next create() re-runs the fan-out.
2026-07-23 18:40:09 -07:00
srojk34
68cd755731 fix(moa): allow a user interrupt to abort the reference fan-out wait
agent/tool_executor.py's concurrent tool batch checks agent._interrupt_requested
and aborts the wait early; agent/moa_loop.py's _run_references_parallel had
no equivalent, so a MoA-enabled turn blocked on ThreadPoolExecutor.result()
until every reference model finished or hit its own individual
auxiliary.moa_reference timeout -- there was no way for the user to abort a
live turn mid-fanout.

Thread an optional `agent` parameter through aggregate_moa_context ->
_run_references_parallel (used when MoA references run alongside the main
model) and MoAClient/MoAChatCompletions (used when the MoA preset itself is
the acting model), then poll concurrent.futures.wait() in
_REFERENCE_POLL_INTERVAL_S slices instead of blocking on future.result() per
reference, checking agent._interrupt_requested each cycle.

Deliberately scoped to interrupt/cancel only -- no new or changed timeout
value, so this doesn't overlap open PRs #53784/#53875 (which lower the
per-reference timeout default but don't add interrupt support). `agent` is
optional and defaults to None, so any caller that doesn't pass it keeps
today's uninterruptible blocking behavior unchanged.
2026-07-23 18:40:09 -07:00
Teknium
62c2b299a3 fix(moa): act aggregator-alone on the facade path when all references fail
Extends the all-references-failed short-circuit (#56975) to the
persistent `provider: moa` facade path: MoAChatCompletions.create()
previously attached 'use the reference responses below' guidance built
entirely from failure sentinels and called the aggregator with it. Now
an all-failed turn attaches either the sanitized unavailability notice
(loud policy) or nothing (silent policy), and the aggregator — which IS
the acting model — simply acts alone. Advisor accounting for the failed
fan-out is still recorded.

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
2026-07-23 18:40:09 -07:00
liuhao1024
f0ed77b627 fix(moa): skip aggregator synthesis when all references fail
When every MoA reference model returns a failure (HTTP error, timeout,
etc.) or is skipped by the recursion guard, the one-shot aggregator
synthesis call is now skipped entirely. Previously it would try to
synthesise a wall of failure sentinels, which could block for the full
provider timeout (observed ~6 min on SenseNova) before returning a
non-retryable error that left the session hanging.

The early return carries the sanitized unavailability notice (never raw
provider error text, per the failed-reference containment) so the main
agent loop can still act in single-model mode.

Salvaged from #56975, reworked atop the _is_failed_reference helpers.
2026-07-23 18:40:09 -07:00
Teknium
d3fc27bbf8 fix(moa): make reference_timeout default inherit auxiliary config; filter recursion-guard skips
Follow-ups for salvaged #53784:

- reference_timeout now defaults to None = no per-preset override, so the
  reference fan-out inherits auxiliary.moa_reference.timeout (900s default)
  via call_llm's own per-task timeout resolution. The PR's 30.0s default
  would have cut off long-thinking advisors mid-response, and its 300s max
  cap capped legitimate explicit values — both removed. Explicit per-preset
  values are still honored as-is.
- _is_failed_reference also treats '[skipped: …]' recursion-guard notes as
  internal sentinels, keeping them out of both aggregator prompts.
- Dashboard/desktop TS types updated to number | null; web_server validator
  accepts null/empty as 'inherit'.
2026-07-23 18:40:09 -07:00
robbyczgw-cla
223881e492 fix(dashboard): reject invalid MoA controls 2026-07-23 18:40:09 -07:00
robbyczgw-cla
ccdf171bcd fix(moa): contain failed reference details 2026-07-23 18:40:09 -07:00
Teknium
3d693ae034 chore(moa): add trailing newline to reference-prompt test file
Follow-up for salvaged #61454.
2026-07-23 18:40:09 -07:00
liuhao1024
6afbb33af1 fix(moa): add explicit warnings to reference prompt against claiming tool execution 2026-07-23 18:40:09 -07:00
sgtworkman
3dfe712384 fix(moa): scope quiet relay to machine-readable CLI
Keep MoA reference display events off the machine-readable -Q stdout
surface (platform=cli with tool_progress_mode=off) while preserving them
everywhere else. Extracts the relay into module-level helpers so the
policy is testable.

Salvaged from #67334.
2026-07-23 18:40:09 -07:00
Teknium
0b17d4d71e fix(windows): re-fit env_probe console suppression to the temp-file _run + add no-window tests (#67690 follow-up)
Follow-up to the #67690 salvage (@m4r13y). The PR's tools/env_probe.py
hunk was written against the old capture_output=True _run(); #67964/#67999
rewrote _run to temp-file capture on July 20, so that hunk no longer
applied — but the rewritten _run still lacked creationflags and kept
flashing one console per probe (~5 per kanban worker start) from
windowless parents. Re-implement the one-line fix against the current
shape: creationflags=windows_hide_flags() on the temp-file subprocess.run,
preserving the #67964 grandchild-can't-wedge-the-pipe contract.

Also add the tests the PR didn't ship, in
tests/test_windows_subprocess_no_window_flags.py:
- env_probe._run passes CREATE_NO_WINDOW and keeps temp-file (non-PIPE)
  stdout/stderr + DEVNULL stdin
- lazy_deps uv install / pip --version probe / pip install fallback /
  ensurepip bootstrap all pass CREATE_NO_WINDOW
- suppress_platform_ver_console: POSIX no-op (platform._syscmd_ver
  untouched, win32_ver() still returns), and simulated-Windows stubbing
  (echo stub installed, idempotent, never raises)
2026-07-23 18:20:24 -07:00
Teknium
385a0655e5 test(moa): tolerate enabled flag in slot-shape assertions after cross-cluster rebase
The per-advisor enabled toggle adds enabled=True to normalized slots;
the JSON-string-parse and per-slot max_tokens tests from sibling
clusters asserted exact dicts. Compare against the enabled-augmented
expectation instead.
2026-07-23 18:11:57 -07:00
Teknium
513d302ed9 test(web_server): update MoA slot-shape assertions for per-advisor enabled flag
The per-reference-model enabled toggle (#59753 salvage) intentionally adds
'enabled' to normalized slot dicts. The two endpoint tests asserted the
exact key set {provider, model} — convert them to subset + round-trip
contracts so optional slot keys (enabled, reasoning_effort, max_tokens)
don't break them again.
2026-07-23 18:11:57 -07:00
Teknium
280c4dce70 test(moa): round-trip regression for per-slot reasoning_effort + enabled
Follow-up for salvaged PR #59753 rebased over the per-slot
reasoning_effort feature: _clean_slot now round-trips reasoning_effort
AND enabled together; add a normalize→normalize regression test, update
the validate/normalize agreement contract for the canonical enabled
default, restore the desktop per-slot toggle test on the current
autosave editor, and map oppenheimor's contributor email.
2026-07-23 18:11:57 -07:00
oppenheimor
ca294d3e62 feat(moa): add reference model toggles 2026-07-23 18:11:57 -07:00
SquabbyZ
89e6f4c989 feat(agent): add MOA progress indicator (#59546)
Adds per-reference progress events and a phase-transition marker to the
MoA display pipeline so TUI / CLI / desktop surfaces can render a status
bar like `MOA: 2/3 refs done` and surface which phase (reference vs
aggregator) is currently active.

  - `moa.progress`  — fired once per reference completion with
                       `refs_done`, `refs_total`, and the source label
  - `moa.phase`     — fired on phase transitions (currently the single
                       `phase="aggregator"` transition once the fan-out
                       finishes)

Plumbed through the existing `reference_callback` →
`tool_progress_callback` → gateway path; no new UI surface. The legacy
`moa.reference` / `moa.aggregating` events are unchanged for backwards
compatibility.

AI-assisted fix by https://github.com/SquabbyZ/peaks-loop
2026-07-23 18:11:57 -07:00
Teknium
4276fe8ded test(windows): pin hide-flags contract for LSP client spawn and npm/go installers
Regression tests for the #47971 salvage: the LSP language-server spawn
must pass windows_hide_flags() creationflags while keeping PIPE stdio
and start_new_session, and the npm/go LSP auto-installer subprocess.run
calls must carry the same hide flags with DEVNULL stdin and
capture_output intact.
2026-07-23 17:59:52 -07:00
Gabriel Steenhoek
30bb55588f fix(gateway): retry detached restart watcher without breakaway
The Windows /restart watcher's outer Popen spawns the watcher with
windows_detach_popen_kwargs() (which carries CREATE_BREAKAWAY_FROM_JOB),
but a restrictive parent job object can reject that bit with OSError and
the current call has no retry. Preserve the current watcher
implementation and add a focused breakaway-denied fallback.

Preserved from current main: watcher_python / pythonw.exe selection, the
str(restart_after_s) deadline, the scrubbed watcher_env, the intentional
no-breakaway inline respawn, and the entire POSIX setsid/bash path.

- primary keeps **windows_detach_popen_kwargs()
- on OSError, retry the same argv/env with
  creationflags=windows_detach_flags_without_breakaway()
- on dual failure, log a definitive, path-safe warning (interpreter
  basename + numeric winerror/errno only) and return without crashing

Replace the superseded breakaway-first inline design and its AST tests
with focused behavioral coverage that drives the real coroutine with a
mocked subprocess.Popen (retry, argv/env/DEVNULL preservation, POSIX
single-session kwarg, no-breakaway inline respawn, secret-safe logging).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 17:59:47 -07:00
Teknium
f7b90e6f80 feat(moa): add privacy redaction filter with display/full modes
Adds moa.privacy_filter ('' | display | full, default off — issue #59959):

- display: redact user-visible surfaces only (reference blocks emitted to
  the UI + saved MoA trace records, including per-advisor full input/output
  and the aggregator-input copy); the aggregator sees raw advisor text so
  synthesis quality is unaffected.
- full: additionally redact the advisor text injected into the aggregator
  prompt, on both the persistent facade path and the one-shot /moa
  synthesis path (the issue's literal ask). Legacy boolean true maps here.

Secret/credential shapes (API-key prefixes, JWTs, private keys, DB
connection strings) are delegated to the central redactor
(agent.redact.redact_sensitive_text, force=True + code_file=True); the MoA
filter adds only email and clearly delimited phone-number patterns. No
bare 10-digit matching: line numbers, timestamps, epoch values, git SHAs,
IPs, versions, and source-code assignments in code-review-shaped advisory
text pass through byte-identical. The reference cache always holds raw
text — redaction happens at each consuming surface, so a mid-session mode
change never leaks or double-redacts.

Reworked from PR #60463: replaced its hand-rolled pattern list (which
matched bare digit runs and re-implemented key shapes) with central-
redactor reuse + safe patterns, and split the single boolean into
display/full modes. Credited for the feature framing.

Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
2026-07-23 17:50:40 -07:00
Teknium
850f576f3d feat(moa): add every_n fanout cadence with cached-guidance reuse
Extends the fanout enum with 'every_n:<N>' (N >= 2): advisors run on the
first iteration of each user turn and every Nth tool iteration after it;
off-cadence iterations REUSE the cached guidance from the last on-cadence
run via the same cache mechanism the user_turn fanout uses, so the
aggregator still gets advice on every step. The cadence counter is scoped
per user turn (resets on a new user message) and only advances when the
advisory state actually changes, so streaming retries never consume a
cadence slot. Mapping form {mode: every_n, n: N} normalizes to the
canonical string. Unknown/degenerate values fall back to per_iteration.

Addresses issue #63393 (advisor fan-out multiplies turn latency/cost by
the tool-iteration count). Redesigned from PR #63448: the submitted shape
skipped references entirely on off-cadence iterations (aggregator ran
advice-less); this version keeps the last advice in play, credited for
the idea and cadence framing.

Config-gated, default-off (default fanout remains per_iteration).

Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
2026-07-23 17:50:40 -07:00
Teknium
74a56b76b0 test(moa): regression for aggregator-model thought_signature resolution (#66212)
Adds test_moa_gemini_aggregator_sanitize_uses_real_model: drives a full MoA
tool-call turn (virtual-provider mode) with a Gemini aggregator and asserts
the strict-API sanitize pass is invoked with the resolved aggregator model
(gemini-3-pro-preview), never the virtual preset name once a slot is
resolved — the exact path that stripped extra_content/thought_signature and
made Gemini aggregators 400 (#65092).

Writing the test surfaced a gap in the salvaged #66212 fix: in virtual-
provider MoA mode (provider=moa, no moa_config threaded through
run_conversation) the conversation-loop branch never fired because it only
consulted moa_config. Extend it to fall back to the facade's
last_aggregator_slot — the same source the handle_max_iterations fix uses —
so both MoA entry modes resolve the real aggregator model.

Also adds the contributors/emails mapping for the #15676 credit base.
2026-07-23 17:26:24 -07:00
Teknium
16950a4568 fix(moa): normalize Copilot aliases + carry x-initiator through retry rebuilds (#60293)
Follow-ups to the salvaged core of #60293:

- Gate the x-initiator header on _normalize_aux_provider() instead of a
  literal 'copilot' string compare, so slot configs spelled github /
  github-copilot / github-models / copilot-acp / mixed case all get the
  user-turn attribution.
- Thread extra_headers through _retry_same_provider_sync/_async so the
  credential-refresh and pool-rotation retry rebuilds don't silently drop
  the header (the rebuilt kwargs previously started from scratch).
- Add a transport-boundary test asserting the header reaches the SDK
  client's create() kwargs (no call_llm mocking), an alias-spelling
  matrix test, and a retry-rebuild preservation test.
2026-07-23 17:26:24 -07:00
dyreckt
4c66307c36 fix(moa): pass Copilot initiator header to advisors 2026-07-23 17:26:24 -07:00
Teknium
0749cac7a1 fix(moa): share facade factory so restore/recover keep reference relay (#53802)
Follow-up to the salvaged core of #53802: a naive MoAClient(preset) rebuild
restores a working facade but silently drops the reference_callback relay
wired in agent_init, so moa.reference / moa.aggregating display events stop
reaching every frontend for the rest of the session.

Introduce agent.moa_loop.build_moa_facade(agent, preset) as the single
construction point for the MoA facade and use it at:
- initial client construction (agent_init.py)
- turn-start fallback restore (restore_primary_runtime)
- transient transport recovery (try_recover_primary_transport — previously
  fell through to _create_openai_client with MoA's empty client_kwargs and
  died with 'api_key client option must be set')
- mid-session model switches (switch_model)

The relay reads agent.tool_progress_callback at emit time, so callbacks
attached after construction are picked up automatically.

Adds test_moa_restored_facade_still_emits_reference_events covering event
delivery through a restored facade.
2026-07-23 17:26:24 -07:00
kosta
5501187847 fix(moa): restore virtual runtime after fallback 2026-07-23 17:26:24 -07:00
Dineth Hettiarachchi
8d14e19f9a fix(agent): close MoA stream on interrupt 2026-07-23 17:26:24 -07:00
Idris Almalki
8d119832b4 fix(gemini): emit thoughtSignature sentinel for cross-provider tool_calls in native adapter
When Hermes fails over from a non-Gemini provider (xAI, Anthropic, etc.) to
Gemini mid-conversation, the existing assistant tool_calls in history carry
no Gemini ``extra_content.google.thought_signature`` (the originating provider
never emits one).  The native adapter's ``_translate_tool_call_to_gemini``
omitted ``thoughtSignature`` entirely in that case, so Gemini 3 thinking
models rejected every replayed turn with::

    HTTP 400 INVALID_ARGUMENT
    Function call is missing a thought_signature in functionCall parts.
    Additional data, function call default_api:<tool_name>, position N.

The Cloud Code Assist sibling adapter already handles this exact case by
emitting a sentinel ``"skip_thought_signature_validator"`` (see
``agent/gemini_cloudcode_adapter.py:106``, originally added in #11270 and
documented as matching ``opencode-gemini-auth``'s approach).  This change
mirrors that fallback in the native adapter so the two paths behave
identically when replaying cross-provider history.

Verified live against ``generativelanguage.googleapis.com/v1beta`` with
``gemini-3-pro-preview``: synthetic 2-turn conversation with no real
``thoughtSignature`` returns 400 without the sentinel and 200 with it.

Test added: ``test_build_native_request_emits_sentinel_for_cross_provider_tool_call``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-23 17:26:24 -07:00
Teknium
d43cc2ca80 fix(compress): gate N-user tail guarantee to actionable turns, behavior-preserving default
Follow-up fixes on top of the salvaged #22566 mechanism:

- N-collector now counts only REAL actionable user turns via
  _is_actionable_user_turn + _is_synthetic_compression_user_turn —
  the same filter pair _find_last_user_message_idx uses post-#69291.
  The contributor's bare role=='user' + _is_context_summary_content
  check let blank platform echoes and continuation/todo rows consume
  N slots, silently degrading the guarantee.
- Default flipped 3 -> 1 (behavior-preserving): a default of 3 was
  measured to change the tail cut on transcripts whose budget covers
  only the last turn. min_tail_user_messages=1 delegates to the
  existing single-user anchor; N>1 is opt-in, and the call site is
  gated so the default path is byte-identical to main.
- Hardened config parse in agent_init (bool rejected, fractional
  floats rejected, floor 1) matching the max_attempts parser shape.
- Wired the recurring external-PR config gaps: hermes_cli/config.py
  DEFAULT_CONFIG + cli-config.yaml.example (PR only had cli.py).
- Regression tests: blank echoes / synthetic rows don't count toward
  N; tool-call/result pairs never split by the N-boundary (no-orphan
  both directions); N-guarantee wins over tail_token_budget and the
  _MAX_TAIL_MESSAGE_FLOOR (floor is a minimum, not a cap); default
  parity pin; DEFAULT_CONFIG pin.
2026-07-23 17:03:49 -07:00
Jerry
a9c868225e feat(compress): preserve recent N user messages during context compression
Add _ensure_last_n_user_messages_in_tail to guarantee the last N user
messages survive compression in the uncompressed tail, with surrounding
assistant/tool context preserved.

- Add min_tail_user_messages parameter (default 3) to ContextCompressor
- New _ensure_last_n_user_messages_in_tail method generalizes single-user protection
- Skip context-summary handoff banners when counting user messages
- User messages are clean boundaries — skip _align_boundary_backward
- Wire through cli.py, agent_init.py, and gateway cache busting keys

Config:
  compression:
    min_tail_user_messages: 3

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-23 17:03:49 -07:00