Commit graph

8093 commits

Author SHA1 Message Date
Teknium
0cda648dfb
test(approval): loosen wall-clock perf bounds 0.15s -> 2.0s
These guard against catastrophic regex backtracking (seconds-to-minutes
class), but 0.15s is within scheduler-stall noise on loaded shared CI
runners — test_max_accepted_separator_free_input_is_fast failed a CI
shard this week on runner load alone. 2.0s still catches the regression
class with zero flake surface.
2026-07-17 13:54:04 -07:00
Teknium
06adcfabf9
feat(attribution): conflict-free contributor mappings via contributors/emails/ directory
The AUTHOR_MAP dict in scripts/release.py was a merge-conflict magnet:
every concurrent salvage PR appended entries to the same lines of the
same file, so parallel PRs re-conflicted on every merge to main.

New system: one file per email under contributors/emails/ — filename is
the commit-author email, first non-comment line is the GitHub login.
File additions never conflict, so any number of PRs can add mappings
concurrently.

- scripts/release.py: AUTHOR_MAP is now LEGACY_AUTHOR_MAP (frozen)
  merged with the directory at import time (directory wins). All
  existing consumers (resolve_author, contributor_audit.py) unchanged.
- scripts/add_contributor.py: idempotent CLI to add a mapping; refuses
  conflicting reassignments (incl. against the legacy map), validates
  email/login shapes.
- contributor-check.yml: attribution gate now accepts a mapping file OR
  a legacy entry; failure message prints the exact add_contributor
  command. Also auto-resolves bare <login>@users.noreply.github.com
  emails is intentionally NOT added (kept id+login form only, matching
  previous behavior).
- contributor_audit.py: guidance now points at add_contributor.py.
- tests/scripts/test_contributor_map.py: 12 tests covering loader,
  merge precedence, CLI idempotency/conflict/validation, subprocess E2E.
2026-07-17 13:54:04 -07:00
Teknium
07f07c7b51 fix(mem0): migrate legacy OSS base URL aliases
Normalize stale api_base keys to each mem0 provider's accepted URL field before Memory.from_config, without mutating the saved config.
2026-07-17 13:49:29 -07:00
Teknium
4c0546c9cc fix(moa): surface stale presets without retries
Keep invalid persisted preset names fail-closed, list the valid configured choices, and classify the local lookup failure as deterministic so it reaches Desktop immediately.
2026-07-17 13:49:12 -07:00
Teknium
61bbc39330 fix(codex): harden final cache-key boundaries
Fold #62349's broader provider-boundary handling into the header fix: bound top-level and xAI override keys again at preflight after middleware, preserve unrelated headers, and cover boundaries and collisions.

Co-authored-by: Nick Taylor <nicktaylor@TheWorldofNick-Lappy.local>
2026-07-17 13:48:41 -07:00
Teknium
81496a8925 test(codex): cover overlength cache-scope headers
Exercise the real transport path for long session ids, including stable hashing and bounded body/header cache keys.
2026-07-17 13:48:41 -07:00
Haider Sultan
18331b9bbd feat(codex): stream live app-server events to TUI/desktop tool cards
Extends the app-server event bridge (make_codex_app_server_event_bridge)
to fire the authoritative stable-ID tool_start_callback /
tool_complete_callback alongside the existing tool_progress_callback,
and route item/reasoning/summaryDelta through the reasoning channel.

Surfaces that render structured tool cards (TUI, desktop) — not just
progress bubbles — now correlate live cards with the projected history
entry after a resume, because the call ids mirror CodexEventProjector's
_deterministic_call_id. Guarded per-callback so a broken display
consumer can't tear down the codex turn loop.

Grafted from PR #65412 by @HaiderSultanArc onto the merged bridge (the
PR's parallel _codex_live_event implementation was reconciled into the
bridge's existing _fire_tool_started/_fire_tool_completed helpers).
2026-07-17 13:44:12 -07:00
Erosika
e4cdd8d9ad fix(honcho): delegate the config.yaml timeout read to load_config_readonly
The staleness check's bespoke mtime memo keyed only on the user
config.yaml, but load_config() merges the managed-scope config
(HERMES_MANAGED_DIR/config.yaml, /etc/hermes) whose leaf keys win. A
managed honcho.timeout with no user config.yaml made the memo cache
'no timeout' while _build resolved the managed value — the same
perpetual-rebuild mismatch this PR fixes for honcho.json. A managed
timeout edit was likewise invisible while the user file's mtime stayed
put.

load_config_readonly() is already cached on both files' signatures plus
the env-ref snapshot, so use it instead of duplicating that
invalidation logic; the defensive deepcopy the old memo existed to
avoid is skipped by the readonly variant. Drive the rebuild test
through a real config.yaml and add a HERMES_MANAGED_DIR regression
test covering stable reuse and managed-timeout edits.
2026-07-17 13:20:16 -07:00
Erosika
9769facae1 fix(honcho): resolve the timeout staleness check from honcho.json like the build path
The staleness check added in #66052 resolved the timeout from env,
config.yaml, and the default only, while the build path also reads the
honcho.json host block (timeout/requestTimeout). With a timeout
configured in honcho.json, the two permanently disagreed: every
no-config get_honcho_client() call — i.e. every HonchoSessionManager
.honcho property access — interpreted the mismatch as a config change
and tore down and rebuilt the client, defeating the singleton on the
hot path it was meant to protect.

Teach the check to read honcho.json through the same host-aware chain
as from_global_config, memoized on the file's mtime_ns so the per-call
cost stays one stat(). A genuine honcho.json timeout change is now also
detected, extending #57437 to that config surface.
2026-07-17 13:20:16 -07:00
brooklyn!
3e7c563ddd
Merge pull request #66449 from NousResearch/audit/desktop-model-picker
fix(desktop): session-scope fast mode, surface profile ownership + pinned model override
2026-07-17 14:36:21 -04:00
Brooklyn Nicholson
ba542338ea fix(desktop): session-scope fast mode, surface profile ownership + pinned model override
Model-picker audit follow-through — closes the remaining pieces of the
"switch one session, switches everywhere / can't tell whose session this
is" report class:

- tui_gateway: `config.set key=fast` with a session no longer writes the
  global agent.service_tier to config.yaml (sibling of the earlier
  `reasoning` scoping fix). It pins create_service_tier_override
  ("priority" / "" for explicit normal) so lazy builds and rebuilds keep
  the choice; the desktop's per-model presets were rewriting the global
  tier on every model pick. Fast-support validation now checks a draft's
  picked model, and `config.get key=fast` reads the pre-build pin.
- desktop: owning-profile tag (initial chip + tooltip/aria label) on
  pinned rows and search results in the All-profiles sidebar, and on the
  chat header once a second profile exists (#66003).
- desktop: composer model pill shows a pin dot + tooltip when a manual
  sticky pick is overriding the Settings default for new chats (#62055).

Closes #66003. Addresses #62055.
2026-07-17 14:31:10 -04:00
brooklyn!
41fdcae688
fix(streaming): make the single-writer fence best-effort so a missing guard can't crash a turn (#66448)
A cron job ("Daily Buzz Report") died with 'AIAgent' object has no
attribute '_claim_stream_writer'. The #65991 single-writer fence lives on
AIAgent (run_agent.py), but the streaming paths that use it live in other
modules — chat_completion_helpers (chat / anthropic / bedrock) and
codex_runtime (codex responses) — and called it directly as
agent._claim_stream_writer() / agent._stream_writer_is_current(). That makes
those modules hard-depend on the method being present on whatever object is
passed as agent.

The fence is an *additive* safety net that may only ever drop a provably
superseded stream, never the sole legitimate writer. But the direct calls
turned any agent that doesn't expose it — a version-skewed checkout (the
streaming helper module newer than run_agent), a hot-reloaded gateway mid
git-pull, a duck-typed agent, or a test double — into a fatal AttributeError
that aborts the whole turn (and, on cron, fails the job).

Route every cross-module claim/check through agent/stream_single_writer.py.
claim_stream_writer(agent) returns 0 when the fence is unavailable (or
raises), and stream_writer_is_current(agent, token) treats a 0 token or an
absent guard as "current" — so a guard-less agent degrades to "no fence"
instead of crashing, while a real AIAgent keeps the full single-writer
protection. Internal self.* uses inside run_agent are unchanged (self is
always a full AIAgent there).
2026-07-17 18:31:09 +00:00
HexLab
594308d4bb
fix(credential-pool): throttle "no available entries" log to stop Windows log-lock storm (contributes to #62698) (#66338)
* fix(credential-pool): throttle "no available entries" log to stop Windows log-lock storm

Credential selection runs on a hot path (every model call plus auxiliary
tasks), so an empty/exhausted pool logged "no available entries" at INFO on
*every* selection. On Windows, where multiple Hermes processes share one
rotating log guarded by concurrent-log-handler's cross-process lock, that
per-selection volume storms the lock (RuntimeError: Cannot acquire lock after
20 attempts), pegs a core, and stalls the asyncio event loop long enough that
the Desktop backend readiness probe times out ("Timed out connecting to Hermes
backend after 15000ms") even though the backend already announced
HERMES_BACKEND_READY.

Log the condition at most once per 60s window, re-arming on a successful
selection so recovery->re-exhaustion still surfaces promptly. Same fix class as
the warn-once dedup in #58265.

* test(credential-pool): cover no-available-entries log throttle

Assert the empty-pool INFO line logs at most once per throttle window, logs
again after the window elapses, and re-arms on a successful selection so a
recover->re-exhaust transition surfaces promptly. Uses a deterministic fake
monotonic clock (no sleeps, no network).
2026-07-17 13:08:46 -04:00
Teknium
ef9e0c98f5 test(compression): expect complete runtime tuple 2026-07-17 09:08:30 -07:00
Teknium
73057ed161 fix(auxiliary): scope runtime state to each turn 2026-07-17 09:08:30 -07:00
liuhao1024
ef9a983154 fix(tui): route images with the live switched model 2026-07-17 09:08:30 -07:00
srojk34
bcce700783 fix(compression): reset failure cooldown on runtime switch 2026-07-17 09:08:30 -07:00
Krowd
c201b72f34 fix(auxiliary): sync runtime after fallback restoration 2026-07-17 09:08:30 -07:00
dfein38347g
fdc6c32d7d fix(auxiliary): isolate runtime cache by live context 2026-07-17 09:08:30 -07:00
Teknium
9e1b1d7536
fix(state): self-heal FTS corruption on the SessionDB write path (#66296)
Complements the #65637 salvage (53d358838 + a9cc17fd8): the gateway
session store now retries transcript appends through its own queue, but
cron and CLI writers call SessionDB directly — a corrupt FTS index still
hard-failed their appends until the next process restart triggered the
offline repair.

_execute_write now detects the FTS-corruption error class (both the
generic 'database disk image is malformed' and newer SQLite's
'fts5: corrupt structure record' variant), performs a one-shot in-place
rebuild by delegating to the existing rebuild_fts(), and retries the
failed write. One-shot per instance so an unrecoverable database cannot
loop; lock/busy jitter-retry path untouched.

E2E-verified: corrupted messages_fts_data rejects appends; with this fix
the same append self-heals, persists, and FTS search works again.
2026-07-17 08:51:51 -07:00
Teknium
e4f87557b9
feat(kanban): modal create-task dialog, editable board project directory, comment workflow hint (#66333)
Community feedback (@LSanapalli on X): the inline task-creation form is
cramped inside a ~280px column with no way to resize; board-level
workspace defaults can't be changed after board creation; and users
believe they must block a task, comment, then unblock just to talk to
a worker.

- Create-task dialog: replace the inline column form with a centered
  modal (reuses hermes-kanban-dialog chrome, 36rem wide) with labeled
  fields for title, assignee, priority, skills, workspace kind/path,
  goal mode, and parent task. Same request shape; Enter/Escape behavior
  preserved; submit disabled until a title is present.
- Board settings dialog: new Settings button in the board switcher opens
  a modal to edit display name, description, and the board-level default
  project directory (default_workdir). PATCH /boards/:slug now accepts
  default_workdir (validated absolute existing dir; empty string clears;
  omitted leaves unchanged) and returns the recomputed
  default_workspace_kind so task-creation defaults follow immediately.
- Comment workflow hint: the task drawer's comment box now explains that
  comments land on the thread immediately and reach the worker on its
  next run/kanban_show() — no block/unblock dance needed — with a fuller
  tooltip for when blocking IS the right tool.
- i18n: new keys optional in the kanban namespace with English fallbacks
  in the bundle (established pattern; avoids churning 17 locale files).
- Docs: dashboard section updated for the dialog + Settings button.
2026-07-17 07:23:54 -07:00
Teknium
71252f0dcb
fix(terminal): fall back when the configured cwd is unenterable, not just missing (#66306)
A root-launched CLI session can leak /root into the terminal cwd state a
non-root gateway/cron process later resolves (#65583). os.path.isdir('/root')
is True for a non-root user — stat only needs search permission on / — so
_resolve_safe_cwd returned it and subprocess.Popen(cwd='/root') died with
PermissionError: [Errno 13], failing EVERY cron job's terminal/file/search
tool on every command until restart.

_resolve_safe_cwd now requires X_OK (new _cwd_usable helper) and climbs to
the nearest enterable ancestor, logging a WARNING that names the leak class
when an existing-but-denied cwd is skipped. Missing-cwd recovery (#17558)
behavior unchanged.

E2E-verified: LocalEnvironment constructed with an unenterable cwd now runs
commands from the fallback directory instead of raising.
2026-07-17 06:54:51 -07:00
Teknium
c49ed09336 test(tui): accept repair_alternation in the top-level server test doubles too
The widened resume sites pass repair_alternation=True; the DB doubles in
tests/test_tui_gateway_server.py (separate from tests/tui_gateway/) needed
the same signature update as Frowtek's originals.
2026-07-17 06:53:38 -07:00
Frowtek
bebcf95847 test(delegate): assert copilot probe with assert_any_call to de-flake under slicing
test_build_child_agent_ignores_acp_command_when_binary_missing patches
shutil.which globally and asserted the LAST call was which("copilot").
That is order-dependent: an unrelated which("uv") reached later in the same
process (which happens under some CI test-slice orderings) becomes the last
call, so assert_called_with("copilot") fails even though the copilot binary
was probed exactly as intended. Switch to assert_any_call("copilot"), which
verifies the actual intent and is robust to unrelated which() calls. The
behavioural assertions (provider, acp_command, acp_args) are unchanged.
2026-07-17 06:53:38 -07:00
Frowtek
7ada946436 test(tui_gateway): accept repair_alternation in resume-path DB doubles
The lazy session.resume path now calls
db.get_messages_as_conversation(target, repair_alternation=True), but the
fake _DB stubs in test_protocol.py still declared the pre-change signature,
so the resume raised "unexpected keyword argument 'repair_alternation'"
and the three session_resume_lazy tests failed.

Mirror the real get_messages_as_conversation signature in the stubs by
accepting (and ignoring) repair_alternation.
2026-07-17 06:53:38 -07:00
Frowtek
4579f26308 fix(state): heal alternation at the ACP / CLI-resume / TUI-resume restore sites too
Follow-up to the restore-boundary alternation heal (#65492): get_messages_
as_conversation grew a repair_alternation flag, wired into gateway
load_transcript and the CLI startup resume. Three other LIVE-REPLAY
restore sites still loaded the transcript verbatim, so a durable
'user;user' violation there re-fires the pre-request defensive repair on
every request for the rest of the session (it only ever mutates the
per-request list, never the restored working conversation):

- acp_adapter/session.py::SessionManager._restore — the loaded history
  becomes the resumed ACP (Zed) agent's SessionState.history.
- hermes_cli/cli_commands_mixin.py — the /resume slash command sets
  self.conversation_history from the load (the startup resume was fixed,
  this mid-session one was missed).
- tui_gateway/server.py — the resume handler feeds the load into the
  deferred session record's working conversation.

Pass repair_alternation=True at all three so the wedge is healed once at
restore. Inspection/export consumers (trace upload, context guard,
api_server history, display_history) keep the verbatim default.

Adds an end-to-end regression test driving the ACP _restore path: a
seeded user;user session restores to an alternation-clean live history
with no user input lost.
2026-07-17 06:53:38 -07:00
Teknium
ec3d958425 feat(codex): webSearch bubbles + bare hermes-tools names in app-server bridge
Two more display gaps from #26541 grafted onto the merged bridge:

- webSearch: codex's built-in web search now produces a tool.started/
  tool.completed bubble pair (query as preview + args). Previously the
  item type wasn't in _CODEX_TOOL_ITEM_TYPES, so built-in searches
  showed nothing.
- mcp.hermes-tools.* stripping: tools codex invokes through Hermes' own
  hermes-tools MCP server display as their bare names (web_search,
  browser_navigate) instead of mcp.hermes-tools.web_search. The inner
  dispatch subprocess can't fire native progress events, so the
  codex-level event is the display event — name it the way users know
  the tool.

Credit: both behaviors designed and first implemented by @simpolism in
PR #26541 (May 15, earliest of the app-server display-bridge family).
2026-07-17 06:49:47 -07:00
snav
11a91a6d17 fix(codex): forward drained notifications to on_event during approval roundtrips
The approval-drain loop in CodexAppServerSession.run_turn drains up to 8
pending notifications to keep per-turn state current before answering a
server-initiated approval request — but never forwarded them to the
on_event display hook. Tool bubbles for items drained alongside an
approval (e.g. the item/started for the very command awaiting approval)
silently disappeared.

Mirror the main notification path's on_event invocation in the drain
loop. Regression test demonstrates RED→GREEN.

Grafted from PR #26541 by @simpolism — the earliest submission of the
codex app-server display-bridge family (May 15). Confirmed independently
by #64698 and #65412.
2026-07-17 06:49:47 -07:00
Teknium
c7205040c3
fix(compression): affirm tool use stays active in the compaction handoff prefix (#66291)
The REFERENCE ONLY framing ('treat as background reference, NOT as active
instructions... Do NOT answer questions or fulfill requests') was observed
bleeding into general tool-use suppression: a production session went
narration-only for 7 consecutive turns immediately after a compression
event, describing edits instead of calling tools (#65848 report).

Fix is additive: one clause stating the note does not restrict HOW the
agent works — tools remain fully active for the active task. Every
anti-resumption protection stays intact; the previous prefix generation
is frozen into _HISTORICAL_SUMMARY_PREFIXES per the module contract so
persisted summaries still get the directive-strip on re-compaction.

The #65848 rewrite was not taken: dropping the 'Do NOT answer questions'
line and the four-heading discard directive risks re-opening the
stale-task-resumption class those clauses exist to prevent (the carveout
era regressions #41607/#38364/#42812 documented in this file).

Report and root-cause analysis: @yasserbousrih (#65848).
2026-07-17 06:49:42 -07:00
Teknium
d32a6d4cca fix(codex): claim the stream-writer token on the codex_responses path too
Widen the #65991 single-writer fence to run_codex_stream: each codex
attempt claims the delta sink before consuming events, and the consume
loop's interrupt_check now also stops the instant a newer attempt
supersedes this one. Parity with the chat_completions / anthropic /
bedrock paths from the salvaged fix.

Two regression tests: superseded codex stream is fenced mid-stream;
sole-writer codex stream delivers unchanged.
2026-07-17 06:49:23 -07:00
HexLab98
35cbffd5c8 test(streaming): cover the single-writer invariant for superseded streams
Assert that a superseded stream (older writer token, other thread) is fenced
from the delta sink, the active writer is never fenced, a non-claiming thread
is never treated as a writer, and the real consume loop stops the instant it is
superseded — so two streams can never interleave into one turn (#65991).
2026-07-17 06:49:23 -07:00
Teknium
c66891db08
fix(cli): arm exit watchdog on shutdown signal, not at chat startup (#66278)
A hermes --tui session whose main thread wedges before app.run() returns
never executes the finally that calls _run_cleanup — the only place the
exit watchdog was armed — so a dead CLI lingered indefinitely (observed
~47 min at 4% CPU, the #65998 class).

Arm the backstop from the SIGTERM/SIGHUP handlers instead (both the
interactive and single-query paths), the earliest moment shutdown intent
is unambiguous. The signal-armed leash is 2x HERMES_EXIT_WATCHDOG_S so a
slow-but-progressing _run_cleanup (which still arms its own tighter timer)
is never cut short; the outer timer only wins when cleanup was never
reached. Idempotent across repeated signals; never raises from a handler.

Deliberately NOT armed at startup: the watchdog thread calls os._exit(0)
unconditionally after its sleep, so a startup-armed timer (the #65998
approach) would hard-kill every session that outlives the timeout.

Supersedes #65998; thanks @JeffStone69 for the report and root-cause gap
analysis.
2026-07-17 06:49:04 -07:00
Teknium
348e9912ff
fix(agent): execute valid tool calls in mixed batches with invalid names (#66317)
Degrading models (observed with gpt-5.6 past ~350K input) emit tool-call
batches like 6 valid named calls + 1 blank-name call. Previously the
whole turn was voided — every valid call got 'Skipped: another tool call
in this turn used an invalid name' — and three such batches tripped the
3-strike stop, killing sessions that were still making progress.

Now a mixed batch error-results ONLY the invalid call(s) (terse
anti-priming error for blank names per #47967, catalog dump for typos)
and dispatches the valid subset for execution. The assistant message
keeps every emitted call so provider-side tool_call/result pairing stays
intact. The 3-strike counter only advances when a turn contains NO valid
call, so a fully-degenerate model still stops while a mostly-coherent
one keeps working. Broken JSON args on a never-executing invalid call no
longer trigger the whole-turn JSON retry loop.

Field evidence: July 2026 debug bundle showed gpt-5.6-sol emitting
6-call batches with one blank-name rider at 559K/384K-token context in
two separate sessions; 13 valid tool calls were discarded before the
session stopped as partial.
2026-07-17 06:48:42 -07:00
kshitijk4poor
a9cc17fd80 fix: harden transcript append retry — lock, matcher, encapsulation, cap
Follow-up fixes for salvaged PR #65637:

1. Clear _dirty_transcripts in rewrite_transcript + rewind_session —
   stale pending messages were re-inserted after /retry, /undo, /compress.

2. Narrow _is_fts_corruption_error to specific SQLite error strings —
   bare 'fts' substring matched 'shifts', 'gifts', etc.

3. Move DB write outside _transcript_retry_lock — holding the lock
   during writes serialized all sessions' transcript appends and blocked
   during FTS rebuild. Now the lock guards only the pending queue.

4. Push rebuild_fts() into SessionDB — SessionStore was reaching into
   _conn/_lock private attrs. SessionDB.rebuild_fts() follows the same
   pattern as optimize_fts().

5. Cap pending per session at 200 — prevents unbounded memory growth
   when DB is persistently broken. Oldest messages dropped with warning.

Added 4 new tests: dirty-clear on rewrite/rewind, FTS matcher false
positives, pending cap enforcement.
2026-07-17 18:58:29 +05:30
MaartenDMT
53d3588389 fix(gateway): retry transcript appends
Queue failed session DB appends so disk order cannot silently lag memory.\nRebuild corrupt FTS indexes once and surface repeated failures as warnings.
2026-07-17 18:58:29 +05:30
Teknium
b41b4b3ec0
test(file-safety): unbreak session-snapshot suite; de-flake fixture to env-var resolution (#66293)
Two changes to tests/agent/test_file_safety_session_state.py:

1. Drop the stale monkeypatch on tools.file_tools._get_live_tracking_cwd
   — the helper was deleted in the cwd-tracking refactor (c80b244b5),
   and monkeypatch.setattr on a missing attribute raises AttributeError,
   breaking CI slice 4/8 on main for every PR. The patch was redundant:
   the test writes an absolute path, so cwd resolution never engages.

2. Make the fixture stale-proof: instead of monkeypatching the private
   _hermes_home_path/_hermes_root_path helpers (same failure class if
   they're ever renamed), set HERMES_HOME to <root>/profiles/work and
   let the real resolution chain (get_hermes_home /
   get_default_hermes_root's profiles-parent rule) derive both paths.
   The fixture now references zero private symbols and exercises the
   production resolution path.
2026-07-17 05:45:12 -07:00
teknium1
abc22cdf1a fix(cron): harden execution attempt ledger 2026-07-17 04:58:35 -07:00
teknium1
d9dd05b69d feat(cron): add truthful execution ledger 2026-07-17 04:58:35 -07:00
Hermes Agent
174fc958ab fix(errors): classify Z.AI GLM token-limit message as context overflow
Port from anomalyco/opencode#35671: Z.AI / Zhipu GLM returns
'tokens in request more than max tokens allowed' (error code 1210) on
context overflow. This matched no pattern in _CONTEXT_OVERFLOW_PATTERNS,
so the error classified as unknown/retryable — the agent would retry the
oversized request instead of triggering context compression.

Proven live on main before the fix: classify_api_error() returned
FailoverReason.unknown for the exact Z.AI error shape; now returns
context_overflow.
2026-07-17 04:57:27 -07:00
Hermes Agent
6030ca8cea fix(mcp): follow nextCursor pagination in tools/resources/prompts discovery
Port from anomalyco/opencode#35439/#35500: preserve full MCP catalogs
across paginated tools/list responses.

The MCP spec allows servers to paginate tools/list, resources/list, and
prompts/list via an opaque nextCursor token. The Python SDK's
ClientSession.list_* methods fetch exactly one page per call, and hermes
never passed the cursor back — on a paginated server every tool,
resource, and prompt past page 1 was silently invisible to the agent.

Adds _paginate_full_list() (cursor-draining helper with a 50-page
runaway cap and spec-correct opaque-string cursor validation) and
applies it at all three discovery sites: _discover_tools(), the
tools/list_changed refresh handler, and the list_resources/list_prompts
utility handlers. The keepalive probe intentionally keeps its
single-page call (liveness only).

E2E: real stdio MCP server serving 3 pages (2/1/1 tools) — old code
discovered 2 tools, new code discovers all 4.
2026-07-17 04:57:12 -07:00
teknium1
c356752b6b fix(memory): drain queued writes on shutdown 2026-07-17 04:55:58 -07:00
teknium1
332fbadd7b fix(backup): fail closed on sqlite snapshot errors 2026-07-17 04:55:19 -07:00
teknium1
b4221c6db2 Inspired by Claude Code: protect session transcripts 2026-07-17 04:54:34 -07:00
Teknium
b78ff50d8d fix(gemini): prune required entries missing from properties in tool schemas
Port from Kilo-Org/kilocode#11955: Gemini validates every object schema's
required list strictly against the same node's properties and fails the
ENTIRE GenerateContentRequest with HTTP 400 'required[0]: property is not
defined' when a name has no matching property. MCP servers (e.g. the GitHub
remote MCP) routinely emit array item schemas carrying required without
properties, which made every request on the native Gemini path fail before
any model output.

sanitize_gemini_schema() now filters required to names present in the node's
properties and drops the keyword when nothing valid remains. Applies
recursively (properties / items / anyOf). Tool handlers still validate
required fields at execution time, so nothing the model could actually use
is lost.

Scoped to the Gemini-facing sanitizer only — the universal
tools/schema_sanitizer.py already prunes typed object nodes, and its
remaining gap (untyped nodes) is contested by open PR #20151.
2026-07-17 04:54:06 -07:00
Teknium
17485cbcd2 fix(cli): sanitize terminal escapes when replaying stored history (/resume recap, /status recap)
Port from openai/codex#31494: user-visible history replay must strip CSI
sequences and control characters. Stored conversation history can carry
raw terminal escapes (pasted content, gateway-origin text, model output
echoing injected tool results). Replaying it via /resume's recap panel or
build_recap (/status on CLI + gateway) wrote those bytes straight to the
terminal — an injected message could clear the screen, retitle the window,
move the cursor, or restyle the recap UI. Rich's Text() does not neutralize
raw escape bytes.

- tools/ansi_strip.py: add sanitize_display_text() — strip_ansi() plus
  bare C0/C1 control removal, preserving \n and \t, normalizing \r to \n
  (adapted to Python from Codex's sanitize_user_text; reuses the existing
  ECMA-48 stripper instead of transcribing their char-walk)
- hermes_cli/cli_agent_setup_mixin.py: sanitize user + assistant text in
  _display_resumed_history() before building the Rich recap panel
- hermes_cli/session_recap.py: sanitize preview lines in build_recap()
  (_truncate choke point) so /status recaps are clean on every platform
- tests: 10 new sanitize_display_text cases (incl. the exact codex#31494
  fixture), recap + resume-display leak assertions
2026-07-17 04:53:38 -07:00
Teknium
b90dbac1d6 fix(approval): unify execution-bearing option detection
Co-authored-by: MorAlekss <mor.aleksandr@yahoo.com>
2026-07-17 04:53:04 -07:00
Teknium
780e098077 fix: widen UTF-8 BOM tolerance to all sibling frontmatter parsers
The previous commit fixes the canonical agent/skill_utils.parse_frontmatter.
Six more modules reimplement the '---' fence check locally and had the
same bug:

- tools/skill_manager_tool.py _validate_frontmatter — rejected BOM'd
  skill_manage create/edit content outright
- tools/skills_hub.py GitHubSource._parse_frontmatter_quick and
  OptionalSkillSource._parse_frontmatter — hub browse/install metadata
- hermes_cli/skills_hub.py — local skill install validation
- gateway/run.py — skill slug discovery for disabled-skill hints
- agent/prompt_builder.py _strip_yaml_frontmatter — BOM'd context files
  (AGENTS.md) leaked raw frontmatter into the system prompt
- tools/blueprints.py _split_frontmatter — str.lstrip() does not strip
  U+FEFF (not whitespace), so the existing lstrip never covered it

Sibling-surface regression tests added.
Bug class also fixed upstream in cline/cline#12218 (found by the weekly
Cline PR scout).
2026-07-17 04:52:02 -07:00
Que0x
a4ecb3da9a fix(skills): strip UTF-8 BOM before parsing SKILL.md frontmatter
A UTF-8 BOM saved into a SKILL.md (e.g. Notepad or PowerShell `>`) is kept by
read_text(encoding="utf-8"), so the string handed to parse_frontmatter starts
with the BOM and the startswith("---") fence check fails. The whole frontmatter
is then silently dropped: the skill loads with no name/description, `platforms`
gating falls open (a macOS-only skill becomes visible everywhere), and
required_environment_variables / metadata.hermes.config setup never fires.

Strip a single leading BOM at the top of parse_frontmatter, the shared
chokepoint for every local skill-loading path (_parse_skill_file,
discover_all_skill_config_vars, DESCRIPTION.md parsing, _inject_skill_config,
and the tools/skills_tool._parse_frontmatter re-export), so the whole class is
covered, not just the reported site. Only the leading marker is removed; a BOM
mid-content is left as data. Mirrors the existing file-tools BOM handling
(#35278) and CONTRIBUTING.md "File encoding".

Adds tests: BOM'd frontmatter parses identically to plain, the body is
BOM-free, platforms gating and config-var extraction survive a BOM, and an
end-to-end BOM-write / plain-read round trip (mirroring _parse_skill_file).
2026-07-17 04:52:02 -07:00
Teknium
f28248c662 test: update lock-io auto-reset assertion to the promote write-path
test_auto_reset_does_not_recover_session_being_ended asserted the old
end_session('session_reset') call; auto-reset now writes through
promote_to_session_reset with the specific auditable reason
('suspended' for a suspended entry). Missed in the local targeted run
because the file wasn't in the touched-suite list; caught by CI shard 6.
2026-07-17 04:51:39 -07:00
Teknium
9fc0074bac fix(gateway): unify reset boundaries vs recovery — promote accidental ends, honor mode=none, adapter-aware resume guidance
Unifies the two gateway subsystems that were fighting each other: the
'never lose a session' recovery machinery (#54878 stale-route self-heal,
find_latest_gateway_session_for_peer reopening agent_close/ws_orphan_reap
rows) and the session reset/expiry machinery (expiry watcher, /new,
/resume, resume_pending freshness gate).

The unified contract:
- INTENTIONAL boundaries (expiry finalization, auto-reset, /new,
  /resume switch) are recorded durably via promote_to_session_reset(),
  which upgrades accidental recoverable end_reasons (agent_close,
  ws_orphan_reap) to the explicit boundary while preserving other
  explicit reasons (compression, etc.). Recovery then correctly refuses
  to resurrect them.
- ACCIDENTAL ends (crash, cleanup bug, mistaken reaper) stay
  recoverable — genuine crash recovery is untouched.

On top of the cherry-picked contributor commits:
- promote_to_session_reset widened to ws_orphan_reap + parameterized
  reason so auto-reset paths stay auditable (idle/daily/suspended/
  resume_pending_expired) (#61220, #61993, #63539)
- get_or_create_session auto-reset, reset_session (/new), and
  switch_session (/resume) all write through the promote path — the
  first-reason-wins end_session no-op could previously leave a reset
  session resurrectable behind a stale agent_close row (#61993)
- resume_pending freshness gate now honors session_reset.mode=none:
  explicit opt-out of automatic resets also opts out of the zombie
  gate (#61052)
- resume recovery note extracted to build_resume_recovery_note() and
  made adapter-aware via a new interactive_resume capability flag:
  webhook/api_server auto-resume turns now CONTINUE the interrupted
  task instead of emitting an unanswerable 'session restored'
  acknowledgement that abandoned the work (#57056)
- tests updated to call the real note builder instead of mirroring it

E2E-validated against a real SessionDB + SessionStore in a temp
HERMES_HOME: expiry->agent_close->no-resurrection, /new promote,
crash recovery preserved, mode=none opt-out, routing-table flag sync.
2026-07-17 04:51:39 -07:00