Commit Graph

122 Commits

Author SHA1 Message Date
teknium1 c0be0e0826 refactor(display): one think-tag list; ACP tool titles derive from agent/display previews
The CLI stream mixin and the gateway think filter each carried a hand-copied think-tag
tuple guarded by a "must stay in sync" comment; adding a tag meant three edits. The
scrubber (agent/think_scrubber.py) now exports THINK_OPEN_TAGS/THINK_CLOSE_TAGS and both
consumers (and strip_think_blocks' regexes) bind to them.

acp_adapter/tools.py::_TITLE_BUILDERS hand-rolled 25 per-tool titles that
agent/display.build_tool_preview already produces (with redaction). ACP titles are now
"<tool>: <preview>"; no ACP-specific overrides remained necessary.
2026-09-13 05:20:26 -07:00
teknium1 7114da3de6 refactor(agent): manual /compress runs through one core with --preview/--aggressive on every surface
CLI, gateway, TUI and ACP each re-sequenced the same chain (partial split -> estimate ->
_compress_context(force=True) -> lock-skip detection -> rejoin tail -> summary), and the
flag set differed per surface: TUI treated `--preview` as a focus topic, ACP ignored
arguments entirely. For the one command that legitimately breaks the prompt cache that
divergence is a correctness problem, not a style one.

`agent/conversation_compression_manual.py::compress_now` owns the sequence; surfaces parse
their own argv, install `after_messages`, re-anchor session ids and render. TUI and ACP gain
`--preview`, `--aggressive` refusal and `here [N]` parity.
2026-09-13 05:20:26 -07:00
teknium1 60797460b6 fix(acp): messages=[] from run_conversation clears the ACP transcript
`_finish_turn` gated the history update on truthiness, so a turn that
legitimately returned an empty transcript left the previous history
in place and the client kept rendering stale state. Same falsey-vs-absent
class as the step-callback fix (#10845); gate on key presence.

Refs #10844
2026-09-12 08:29:48 -07:00
Bartok 8b79d8731b fix(acp): preserve falsey tool results in step callback
make_step_cb() used 'or' to fall back from 'result' to 'output' key:
    result = tool_info.get('result') or tool_info.get('output')

This collapsed valid falsey results (empty string, 0, False) to None,
because Python's 'or' operator treats all falsey values as missing.

Use explicit key presence check instead:
    result = tool_info.get('result') if 'result' in tool_info else tool_info.get('output')

This preserves the actual value when the 'result' key exists (even if
falsey), and only falls back to 'output' when 'result' is truly absent.

Adds regression tests for empty string, zero, and output-key fallback.

Fixes #10845.
2026-09-12 08:29:48 -07:00
teknium1 b2b713984e test(acp): first persisted row carries the provider snapshot
Invariant test for #9812 (red on origin/main: model_config was {"cwd"} only).
Adapted from the test in PR #9883 to the current save_session() flow, where an
empty-history session stays ephemeral until the first message.
2026-09-12 08:26:37 -07:00
Teknium 8f7640b622 fix(acp): retain only ephemeral-new-session persistence guard
Keep two invariants covering empty create/cwd/save/fork, genuine content and existing-row metadata. Preserve original authorship and avoid source-only legacy pruning: an empty ACP row does not prove its owner is dead. Native ACP wire plus a local streaming model fixture verifies the first turn and nonempty fork remain durable.
2026-09-07 04:46:04 -07:00
Willhong 64fda8c73f fix(acp): stop persisting sessions that never received a prompt
Fixes #104724

The ACP wire contract has session/new as a separate round trip from
session/prompt precisely so a client can open a session before it knows a
prompt is coming, and at least one shipping client opens sessions it will
never prompt: bb discovers a model catalog by spawning a throwaway
`hermes acp`, sending initialize + session/new, reading
NewSessionResponse.models, and killing the process. Its discovery cache
TTL is 60s, so an open editor re-probes continuously.

_persist() created the state.db row the moment a session was created, so
every such probe left a permanent message_count=0 shell. Measured on one
workstation: 116 accumulated, appearing on a time cadence rather than a
per-conversation one (26 real ACP user turns vs 13 shells in a day; exactly
120.0-minute spacing overnight with zero user activity).

The shells are indistinguishable from real chats in the session list and
`hermes sessions prune` cannot remove them: the rows are never ended, so
ended_at stays NULL and prune skips them by design, leaving
`hermes sessions delete <id>` one id at a time as the only remedy.

Defer row creation until the session has history. Nothing is lost for a
genuine conversation: AIAgent._ensure_db_session() creates the row on the
first turn and the post-prompt save_session() lands the ACP metadata on
top, which makes the create-time write redundant for every session except
the one case that should not be recorded at all.

Gate on state.history rather than message_count so fork_session, which
deep-copies a non-empty history into a fresh id, still persists at once.

Verified by differential execution of an identical probe against both
trees: on 693641aa8b an unprompted session/new adds a row, with this
change it adds none, and a session that does receive a prompt persists
in both. tests/acp: 138 passed. Reverting only the source change leaves
the two new assertions failing, confirming they exercise the fix.
2026-09-07 04:46:04 -07:00
Teknium 0ee524a3d6 test(acp): reproduce empty session persistence on the native wire 2026-09-07 04:46:04 -07:00
Teknium 14791b4d4e simplify(compat): approval — drop 43 facade re-exports + _command_detection_variants late-bind seam, repoint 30 callers + 46 test files
tools/approval.py no longer re-exports sibling names (approval_context/prompt/floors/detection/
human_wait/smart/gateway_wait); it imports only what it uses. Siblings reference sibling-defined
names directly (module-attribute reads on tools.approval_context so patching the defining module
still works); only facade-owned state (_lock, _gateway_queues, _permanent_approved, _denied,
_denial_breaker_addendum, _gateway_notify_cb) is still read back through tools.approval.
approval_detection calls its own _command_detection_variants instead of late-binding through the facade.
2026-09-03 13:49:57 -07:00
Teknium 7b8c11bcf7 simplify(compat): models — drop 52 re-exports from hermes_cli.models, repoint 16 callers + 41 test files 2026-09-03 13:48:49 -07:00
Teknium 1e6cfaa0d0 simplify(compat): setup/model_switch/nous_subscription — drop 34 re-exports, repoint 14 callers + 26 test files (~100 sites) 2026-09-03 13:33:10 -07:00
Teknium f6938b37f3 simplify(compat): terminal/file/environments — drop 42 re-exports/aliases, repoint 20 callers + 41 test files
tools/terminal_tool.py: drop 30 pure re-export names (lifecycle/config/backends/
sudo/guards/result/interrupt/utils/_DockerEnvironment/is_managed_tool_gateway_ready)
and the noqa-F401 comments on the 25 names the facade itself uses. Sibling modules
(terminal_tool_backends/_result/_sudo/_lifecycle, environments/base, process_registry)
that read removed names through the facade now import from the defining module.
tools/environments/base.py: drop 11 re-exports (base_output/base_session_env/
path_utils) and the BaseEnvironment.stop() compat alias (no in-tree caller; the
lifecycle hasattr(env, 'stop') fallback stays for third-party envs).
tools/environments/docker.py: drop 1 re-export + the re-export comment.
Callers/tests repointed to tools.terminal_tool_{lifecycle,backends,sudo,config,
guards,result}, tools.interrupt, tools.environments.{base_output,base_session_env,
path_utils}.
2026-09-03 13:29:55 -07:00
Teknium 98c140bc4b simplify(compat): code_execution_tool/environments.local — drop 36 re-exports, repoint 5 callers + 14 test files 2026-09-03 13:24:04 -07:00
Teknium b39948106a simplify(compat): acp_adapter — drop 1 re-export, re-remove 5 shim-only names, repoint 4 tests
server: drop ACP_MAX_MODELS_PER_PROVIDER re-export (test -> model_catalog).
Re-removed from 96c104c903: SessionManager.remove_session/cleanup (+ now-orphaned _clear_task_cwd/_delete_persisted)
and their 4 tests; auth.has_provider (tests assert detect_provider() directly);
edit_approval.clear_/get_edit_approval_requester (test teardown -> set_edit_approval_requester(None)).
2026-09-03 13:04:17 -07:00
Teknium 34abf954bd review-fix(public-api): restore get_session_activity, latest_user_message_row_id, resolve_multiple_toolsets, has_provider, nous_token_has_billing_scope, curated_models_for_provider, clear_edit_approval_requester + tests
All public on BASE 63279301bc, dropped by the simplify refactor (their tests were deleted or
rewritten to the replacement API). Restore each with BASE signature/body as a thin wrapper over the
surviving implementation, and restore the tests at the original call sites: test_message_reactions
again asserts the role=user contract (a newer assistant message is never the default target);
test_hermes_state / test_watchdog_review_76354 go back to get_session_activity(); toolsets, acp auth,
edit_approval, billing-scope and curated-models tests restored/extended.
2026-09-03 09:40:49 -07:00
Teknium 96c104c903 review-fix(public-api): restore from_platform_entry, SessionManager.remove_session/cleanup, is_relay_media_url, SessionTurnLeaseRegistry.__len__ + tests
BASE exposed these public names; the simplify refactor dropped them (and deleted/removed their
tests) although plugins/connectors import them. Restore each with BASE's signature and body
(download() again routes its auth decision through is_relay_media_url), and restore the covering
tests ported to the new layout, plus DB-only/task-cwd coverage for remove_session/cleanup, the
zero->4096 chunking normalization for from_platform_entry, and len() on empty/populated registries.
A/B vs 63279301bc: /tmp/rf/rev/ab_publicapi.py identical output on both trees.
2026-09-03 09:32:40 -07:00
Teknium cbd2011900 refactor(acp): move picker assembly into model_catalog.build_model_state; server keeps only the fallback row 2026-09-02 18:25:53 -07:00
Teknium 4a9d196f0a fix(integration): let acp _execute_write stub accept the new patience_s kwarg 2026-09-02 15:41:13 -07:00
Teknium 88b74d6ef0 Merge branch 'simp/hclib' into simp/integration 2026-09-02 15:05:48 -07:00
Teknium 7df99a7787 test(hclib): update tests for moved/removed symbols 2026-09-02 15:03:42 -07:00
Teknium beacf4e942 refactor(tui): resume — verified partial work (acp_adapter + methods_* compaction, bind_module via globals()) 2026-09-02 14:09:01 -07:00
Brooklyn Nicholson 165d1849e2 fix(approval): stop the CLI and ACP offering a scope the protected gate discards
The protected agent-instruction gate grants one operation and persists
nothing, but only the TUI/desktop and Runs transports were taught that.
The prompt_toolkit panel, the input() fallback, and the ACP editor menu
still rendered "Allow for session", so a user editing SOUL.md tapped it,
got re-prompted on the next write, and read the gate as broken.

Thread allow_session through prompt_dangerous_approval so a caller that
re-asks every time collapses every surface to once/deny, and cover the
producer-to-transport contract end to end.
2026-08-23 17:45:47 -05:00
kshitijk4poor 016ba66176 fix: stamp _db_persisted at row load time so resumed transcripts never re-append (#92231)
Resumed sessions loaded message dicts from state.db WITHOUT the
_DB_PERSISTED_MARKER, so any flush that lost the identity boundary
(compression durable-snapshot adoption, incremental tool-call persists,
rotation preflight on cold resume) re-appended the ENTIRE loaded
transcript as new rows. Compression cycles then doubled the copies:
the incident session grew 998 -> 1995 -> 3990 -> 7981 rows across
three aborted rotations (15,962 active rows, only 472 distinct).

Fix at the architectural chokepoint: SessionDB._rows_to_conversation
(shared by get_messages_as_conversation and get_resume_conversations)
now stamps the marker at row materialization time - a dict built FROM
a durable row is persisted by construction, regardless of which caller
loads it or how the list is later handed to a flush.

Safety:
- Wire-safe: every transport strips underscore-prefixed keys before
  the API request (chat_completion_helpers, anthropic_adapter), same
  contract as the existing _row_id stamp in the same function.
- Rotation handoffs still write: compression's assembly copies strip
  the marker (_fresh_compaction_message_copy + the terminal
  _strip_persistence_markers sweep), so compacted transcripts still
  flush to the child session (#57491 invariant preserved).
- Branch/seed copies unaffected: /branch and _persist_branch_seed
  build fresh field-projected dicts and write via append_messages_batch
  directly, not through the marker-gated flush.

Tests: new regression suite (marker sync, load stamping, 3-cycle
amplification repro, new-tail write guard, compaction-copy handoff);
updated the #68454 control test that asserted the old double-write
behavior and the ACP restore shape test.
2026-08-23 13:34:32 +05:30
Vadelma 81e813507f fix(models): preserve empty catalog and custom model semantics
Co-authored-by: Taneli Mielikäinen <taneli.mielikainen@iki.fi>
2026-08-18 14:26:16 -07:00
Vadelma f70e9abce9 fix(models): close final provider discovery edge cases
Co-authored-by: Taneli Mielikäinen <taneli.mielikainen@iki.fi>
2026-08-18 14:26:16 -07:00
Vadelma 17405de9c1 feat(acp): preserve native provider catalogs and identities
Co-authored-by: Taneli Mielikäinen <taneli.mielikainen@iki.fi>
2026-08-18 14:26:16 -07:00
Teknium 1995207470 Port from PrimeIntellect-ai/prime-agent#628: resolve symlink aliases in ACP cwd comparison
macOS reports editor workspaces as /var/... while sessions are stored
under /private/var/... (same for /tmp vs /private/tmp), so the lexical
normpath comparison in _normalize_cwd_for_compare treated them as
different directories and ACP history filters silently dropped a
workspace's own sessions.

Canonicalize with os.path.realpath; nonexistent paths (e.g.
WSL-translated Windows drives on a Linux host) keep the previous
lexical behavior since realpath(strict=False) is lexical for them.
2026-08-16 22:07:00 -07:00
Teknium aac74be2f1 fix(approval): classify CLI/TUI approval timeouts separately from explicit denials
When an approval prompt expired without a response, every CLI-side path
collapsed the timeout into the same 'deny' choice as an explicit user
refusal, so the agent was told the user denied the action when the user
simply never answered. The gateway wait already distinguished the two
('timed out without user response... Silence is not consent.'); this
brings the CLI/TUI/ACP surfaces to parity.

- prompt_dangerous_approval(): input()-path expiry now returns a distinct
  'timeout' choice (still fail-closed).
- cli.py _approval_callback + hermes_cli/callbacks.py approval_callback:
  deadline expiry returns 'timeout' instead of 'deny'.
- check_all_command_guards / _run_approval_gate CLI tails: 'timeout' maps
  to outcome='timeout' with a 'timed out without user response... Silence
  is not consent.' BLOCKED message (matching the gateway wording);
  explicit deny keeps outcome='denied' and gains user_consent=False for
  shape parity.
- computer_use: 'timeout' verdict threads through the CLI adapter and
  yields a 'prompt timed out — the user did not respond' error instead of
  'denied by user'.
- ACP permissions bridge: FutureTimeout returns 'timeout' (other failures
  still 'deny'); elicitation maps 'timeout' to 'cancel' like the gateway's
  unresolved outcome; codex wire mapping documents deny/timeout→decline.
- write_approval already treats unknown choices as 'stage, not drop', so
  a timeout now stages the memory write instead of silently refusing it.

Every timeout path remains fail-closed — the action never runs; only the
classification reported to the agent changes.
2026-08-02 20:21:59 -07:00
dsad a5f94e93ad fix(acp): bind session_id in session context for subprocess isolation
The ACP prompt path called set_session_vars(session_key=session_id, ...)
without passing session_id, so the HERMES_SESSION_ID ContextVar was bound
to its explicit "" default. Once the session-context machinery is engaged,
_inject_session_context_env treats an explicitly-bound "" as authoritative
and writes it to the child env — so subprocesses spawned during an ACP turn
got an empty HERMES_SESSION_ID instead of the session's own id.

Pass session_id through so child subprocesses carry the correct id.

Salvage of the ACP half of #53454 by @necoweb3 (the V4A-path half is
salvaged separately in the file-tools PR).

Co-authored-by: necoweb3 <sswdarius@gmail.com>
2026-08-01 14:53:34 -07: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
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
teknium1 b6accee0d7 fix(acp): pin the session cwd for slash-command handlers too
Slash commands run on the event-loop thread, outside the per-turn
contextvars.copy_context() that pins the session cwd for the agent call.
/compress reaches agent._build_system_prompt(), whose "Current working
directory" line comes from resolve_agent_cwd() — so an unpinned handler
rebuilt the prompt against the Hermes install tree and PERSISTED it as
the session's cached prompt, re-poisoning every later turn even though
the turn itself is now pinned.

Pin inside a fresh context copy so the write cannot leak into other
concurrent ACP sessions on the shared loop and needs no teardown.
2026-07-25 18:06:32 -07:00
Steve Darlow cca2a2fc8d fix(acp): pin the session cwd for the turn
An ACP session registered the client's cwd for the *tools*
(`_register_task_cwd` -> `register_task_env_overrides`) but never pinned it
for the *prompt*. `agent/prompt_builder.py` reports
`Current working directory: {resolve_agent_cwd()}`, and `resolve_agent_cwd()`
reads the `_SESSION_CWD` contextvar, which ACP left unset — so it fell back
to `TERMINAL_CWD` / the launch dir.

The system prompt therefore advertised one root (commonly
`~/.hermes/workspace`, or the install tree) while the tools were rooted at the
editor's project. When the model emitted a *relative* path the tools resolved
it correctly; when it emitted an *absolute* path built from the advertised
root, the write landed outside the client's workspace and the turn still
reported success.

Observed in Zed/Buzz-shaped usage: the first prompt in a fresh workspace
creates the file under `~/.hermes/workspace/` and answers "Done." The client's
directory is untouched. Later sessions on the same cwd appear to work once a
session cwd record exists, which makes it look intermittent.

`_run_agent` already calls `set_session_vars(session_key=session_id)` inside
its `contextvars.copy_context()`, and that helper's `cwd` argument exists to
"pin the logical working directory for this context". It simply was not
passed. `gateway/session_context.py` and `tui_gateway/server.py` both already
pass it; ACP was the only surface that did not.

Adds a regression test asserting that the resolved cwd *during the turn* is
the cwd the client passed to `session/new`. It fails without this change
(resolving the install tree instead of the client's project).
2026-07-25 18:06:32 -07:00
Israel Lot 4be38125af feat(acp): list named custom providers in the ACP model selector
Named endpoints from the providers: mapping (and legacy custom_providers:
list) never appear in the ACP model selector: _build_model_state lists
only the canonical current provider's catalog, and canonical provider
enumeration does not include user-defined named endpoints. The TUI
/model picker already renders these entries (#47039, implemented for the
TUI surface only), so editor clients silently hide endpoints the user
configured — e.g. an OpenAI-compatible Bedrock Mantle Responses provider.

Add _named_custom_provider_catalogs(), sourcing entries from
get_compatible_custom_providers() (covers both config shapes), and append
its models to the selector payload. Choice ids use the custom:<name>
slug shape so custom:<name>:<model> selections round-trip through
parse_model_input / resolve_runtime_provider unchanged on set_session_model.

Declared models (default_model + models) survive failed live /models
discovery — some OpenAI-compatible endpoints expose no /models route yet
serve their declared models fine. Honors providers.<name>.enabled: false
and discover_models: false.

Verified: scripts/run_tests.sh tests/acp/ — 318 passed, 0 failed;
scripts/check-windows-footguns.py clean.
2026-07-24 15:57:30 -07:00
amanning3390 366242e479 fix(acp): allow hosts to skip configured MCP startup 2026-07-24 15:51:20 -07:00
amanning3390 615a0d9141 perf(acp): bound the cross-provider model inventory for ACP clients
ACP clients (Zed, Buzz) render the whole availableModels array in a single
dropdown, so requesting the shared inventory with max_models=None could
hand an editor an unbounded cross-provider catalog.

Request the same per-provider cap the MoA picker already uses
(hermes_cli/moa_cmd.py), exposed as ACP_MAX_MODELS_PER_PROVIDER so the
intent is documented at the call site.

This bounds each provider's row rather than the total, matching the shared
inventory's own semantics: aggregator providers stay intentionally
uncapped, and the existing current-model fallback still re-inserts a
selection that falls outside the cap. At present no authenticated provider
approaches 200 models, so the visible catalog is unchanged; the cap is a
guardrail for large catalogs (e.g. OpenRouter) rather than a change to
today's lists.

The new test asserts the contract - bounded row plus a reachable current
selection - instead of a fixed catalog size, so growing the inventory
cannot turn it into a change-detector.

Co-authored-by: amanning3390 <adam.manning@pro-serveinc.com>
Signed-off-by: amanning3390 <adam.manning@pro-serveinc.com>
2026-07-24 15:51:06 -07:00
amanning3390 33908ff9ff feat(acp): expose authenticated cross-provider model choices 2026-07-24 15:51:06 -07:00
Teknium 7580bc66d5 fix(acp): align salvaged /compact wording and test with current /compress command name
Main renamed the ACP slash command from /compact to /compress after #63630
was written; update the salvaged status line, comment, and behavioral test
to dispatch the command that actually exists.
2026-07-22 16:59:29 -07:00
babak a007ac55c6 fix(compress): allow manual /compress when auto-compaction is disabled [overflow error directs users there]
compression.enabled: false is documented (agent/conversation_loop.py
overflow path) as disabling *automatic* compaction only — the terminal
context-overflow error explicitly tells users to run /compress manually,
and the gateway handler has never gated on the flag. But the classic CLI
(_manual_compress) and the ACP adapter (/compact) refused with
'Compression is disabled', leaving users at a full context with no
manual escape hatch on those surfaces.

Remove the stale gates (they predate the overflow-path design; the CLI
gate came from the original /compress commit's boilerplate) and unify
force=True across all manual-compaction call sites: ACP /compact and the
TUI's _compress_session_history (manual-only helper) now bypass the
summary-failure cooldown exactly like the CLI and gateway already did.
Also reword the ACP /context status line so a disabled-compression agent
no longer implies /compact is unavailable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 16:59:29 -07:00
Israel Lot 24e4c6fbf6 fix(acp): flag replayed compaction summaries via _meta
A context-compaction handoff is persisted as an ordinary history message
but is not a real turn. The ACP history replay streamed it as a bare
user/agent message chunk, dropping the in-process _compressed_summary
marker, so ACP frontends (editors, vscode-hermes) rendered the entire
handoff as a regular message.

Tag replayed summary chunks under _meta.hermes (ACP's extensibility
channel), covering all three persistence shapes the compressor emits:

- standalone role="user" handoff -> compactionSummary: true
- standalone role="assistant" handoff (alternation-driven role pick)
  -> compactionSummary: true
- merge-into-tail message (preserved tail content + appended summary)
  -> containsCompactionSummary: true, a distinct key so clients that
  collapse standalone summaries cannot hide the preserved real content

Detection honors the in-process metadata flag and falls back to a new
ContextCompressor.classify_summary_content() content classifier
(standalone/merged/None), so it also works for a DB-reloaded session
that lost the in-memory flag. _is_context_summary_content is now a thin
wrapper over the classifier, keeping existing callers unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 16:58:35 -07:00
ethernet d84e11af4d rip out brew + pip/PyPI wheel support (#68217)
Removes Homebrew and PyPI wheel/sdist as Hermes distribution paths while
preserving the supported source, Docker, and Nix workflows.

Changes:
- Removes the Homebrew formula, PyPI publish workflow, sdist manifest
  (MANIFEST.in), and wheel/sdist release-attachment logic from scripts/release.py.
- Keeps setuptools metadata and entry points required by editable installs
  and Docker/Nix builds, but adds a setup.py guard that rejects wheel/sdist
  builds outside a sealed Nix derivation (HERMES_NIX_BUILD=1).
- Removes pip/Homebrew install detection, PyPI update checks, the pip
  self-update path, the deprecation-banner state, the postinstall subcommand,
  wheel data-directory fallbacks in agent/i18n.py and hermes_constants.py,
  and the ACP Registry manifest/version-lockstep release logic.
- Adds /nix/store/ path detection so `nix run` / `nix profile install`
  installs (which don't set HERMES_MANAGED) are correctly identified as
  "nix" rather than falling through to "git"/"unknown".
- Retired install-method values ("pip", "homebrew") in existing
  .install_method stamps (both code-scoped and home-scoped) are ignored by
  the allowlist reader and fall through to "unknown" instead of resurrecting
  a retired enum value.
- Updates Nix packaging to ship bare runtime data (locales, optional-mcps)
  through store symlinks and wrapper env vars instead of wheel data-files.
- Removes the ACP Registry manifest/icon and their version-lockstep tests.
- Deletes or rewrites packaging, pip-update, Homebrew, and ACP Registry
  tests; adds parametrized coverage for the packaging build guard covering
  BOTH sdist and wheel paths (the guards live in separate cmdclass entries
  — a passing sdist test proves nothing about the wheel path).
- Updates installation/platform documentation and related user-facing copy.
- Adjusts the supply-chain scan so deleted install-hook files do not trigger
  a finding, while additions or modifications still require the existing
  ci-reviewed label gate.

Supported installation paths (unchanged):
- git installer (install.sh)
- Docker
- Nix/NixOS
- editable development installs (uv sync, uv pip install -e ., pip install -e .)
2026-07-22 16:51:01 -04:00
liuhao1024 cb39be92e4 fix: update test assertions for /compact -> /compress rename 2026-07-22 06:58:05 -07:00
Steven Seagondollar ed2f48b2e6 fix(titles): use active runtime for gateway sessions 2026-07-16 22:54:09 -07:00
nima20002000 193871f1a6 fix(code-exec): expose truncated stdout metadata 2026-07-16 04:25:19 -07:00
墨綠BG 6803519aa5 🐛 fix(acp): reset session counters on slash reset 2026-07-16 04:24:34 -07:00
Frowtek 05473428cb fix(acp): don't let a malformed tool argument abort the tool-call render
build_tool_start renders every ACP (Zed) tool call — on the live tool-progress
callback (acp_adapter/events.py) and during session history replay
(acp_adapter/server.py). It called build_tool_title and extract_locations
directly, so a model that emits a malformed argument crashed the render:

- terminal `command` as null/number -> TypeError (len() in build_tool_title)
- delegate_task `goal` as a number  -> TypeError (len())
- read_file `path` as a non-string  -> pydantic ValidationError building a
  ToolCallLocation

A live crash breaks the tool-call event; a persisted one breaks history replay
on every resume of that session. The sibling CLI label builder
get_cute_tool_message was already wrapped for exactly this reason
(agent/display.py: "display must never abort a turn").

Wrap build_tool_start the same way: on any builder failure, fall back to a
minimal, valid start event (tool name as title, resolved kind). The happy path
is unchanged.

Adds tests for the non-string command, path, and goal cases.
2026-07-15 00:10:22 -07:00
Teknium d48bf743f2 fix(approval): scope smart deny owner overrides to one operation
Co-authored-by: Sergei Ivanov <kavi@local.hermes>
2026-07-13 04:31:55 -07:00
kshitijk4poor 54e1864577 fix(acp): unwrap web extract object titles 2026-07-10 19:14:06 +05:30
kshitijk4poor 723ccda275 fix(acp): also preserve archived rows on model-switch / restore saves
Follow-up widening the archived-history fix to the sibling save paths the
original PR did not cover. Model switches (_cmd_model, set_session_model) and
_restore mint a fresh AIAgent with _session_db_created=False, so the
agent-owns-persistence guard evaluates False and the blind full-history
replace_messages() fired — DELETEing the durable active=0/compacted=1 rows on
any compressed ACP session (same data-loss class the PR fixes, different
trigger).

- hermes_state.replace_messages: add active_only=True to delete/reinsert only
  the live (active=1) rows, leaving soft-archived rows untouched (idea adopted
  from the competing PR #50306 by @mrparker0980, credited).
- hermes_state.has_archived_messages: cheap existence probe for active=0 rows.
- acp_adapter._persist: when the agent doesn't own persistence but the session
  already has archived rows on disk, replace active-only; otherwise the
  destructive full replace stays (fresh create/fork has nothing to lose).
- Regression test: model-switch save on a compacted session keeps the archived
  turn discoverable via get_messages(include_inactive=True) + search_messages.
2026-07-01 17:16:51 +05:30
sasquatch9818 897240462a fix(acp): stop _persist from deleting compression-archived history
ACP's SessionManager._persist() called db.replace_messages() on every
save. That delete-then-reinsert is destructive by design. The agent
backing each ACP session already persists to the same SessionDB itself:
it flushes turns incrementally via append_message and, on context
compression, preserves pre-compaction turns non-destructively through
archive_and_compact() as searchable active=0/compacted=1 rows.

So the per-save replace_messages() was a redundant double-write that
deleted exactly those archived rows (and their FTS entries). Worse,
after a compression-driven id rotation the agent's live head no longer
equals the ACP session id, so the replace overwrote the ended parent
transcript while new turns flowed to the new id — split-brain corruption
of one conversation. Any ACP conversation (VS Code / Zed / JetBrains)
long enough to compress lost history.

Now _persist skips the destructive replace when the agent owns
persistence to this DB (its _session_db is this db and its row exists),
relying on the agent's own incremental + archival flush. It still falls
back to the atomic replace when the agent is not self-persisting — test
agent factories, and fresh create/fork sessions whose copied history the
agent has not flushed yet — so the #13675 rollback guarantee holds.

## What does this PR do?

Fixes silent history loss in ACP editor sessions. ACP _persist no longer
destroys the compression-archived transcript the agent already wrote.
Long enough conversations compress; that compression archives old turns
non-destructively; ACP then hard-deleted them on the next save. After an
id rotation it also clobbered the ended parent and split the
conversation across two ids. This change defers to the agent's own
persistence when it owns the DB and only uses the destructive replace
when nothing else is writing the transcript.

## Related Issue

N/A

## Type of Change

- [x] 🐛 Bug fix (non-breaking change that fixes an issue)
- [ ] ✨ New feature (non-breaking change that adds functionality)
- [ ] 🔒 Security fix
- [ ] 📝 Documentation update
- [ ] ✅ Tests (adding or improving test coverage)
- [ ] ♻️ Refactor (no behavior change)
- [ ] 🎯 New skill (bundled or hub)

## Changes Made

- `acp_adapter/session.py`: in `SessionManager._persist`, guard the
  `db.replace_messages()` call. Skip it when the agent owns persistence
  to this DB (`agent._session_db is db` and `agent._session_db_created`);
  otherwise keep the destructive atomic replace as the fallback.
- `tests/acp/test_session.py`: add a regression test proving archived
  (active=0/compacted=1) rows survive a save when the agent self-persists
  and stay FTS-searchable; add a test confirming the replace path still
  runs for agents that do not own DB persistence.

## How to Test

1. Run `pytest tests/acp/test_session.py -q` — 43 pass.
2. `test_save_session_preserves_agent_archived_history`: archive a turn
   via `archive_and_compact`, save, and confirm it survives and is found
   by `search_messages` (fails before this fix — replace_messages deleted
   it).
3. `test_save_session_still_replaces_when_agent_not_self_persisting`:
   confirm history still overwrites cleanly for non-self-persisting
   agents.

## Checklist

### Code

- [x] I've read the Contributing Guide
- [x] My commit messages follow Conventional Commits (`fix(scope):`, `feat(scope):`, etc.)
- [x] I searched for existing PRs to make sure this isn't a duplicate
- [x] My PR contains only changes related to this fix/feature (no unrelated commits)
- [x] I've run `pytest tests/ -q` and all tests pass
- [x] I've added tests for my changes (required for bug fixes, strongly encouraged for features)
- [x] I've tested on my platform: macOS 15 (Darwin 25.5)

### Documentation & Housekeeping

- [x] I've updated relevant documentation (README, `docs/`, docstrings) — or N/A
- [x] I've updated `cli-config.yaml.example` if I added/changed config keys — or N/A
- [x] I've updated `CONTRIBUTING.md` or `AGENTS.md` if I changed architecture or workflows — or N/A
- [x] I've considered cross-platform impact (Windows, macOS) — or N/A
- [x] I've updated tool descriptions/schemas if I changed tool behavior — or N/A
2026-07-01 17:16:51 +05:30