Commit Graph

14624 Commits

Author SHA1 Message Date
Teknium d3fdcef132 fix(goal): the re-paste pointer applies only to a near-whole re-paste, and on every surface
Independent review: substring matching lost the selected objective (after a
message offering API or UI work, '/goal ship the API' and '/goal ship the UI'
kicked identically), and the gateway and TUI /goal paths still re-sent the
full goal. The rule now lives once in hermes_cli.goals.goal_kick_prompt:
the pointer replaces the goal only when the goal is >= 400 chars, contained
in the last user message, and >= 80% of it. The CLI reads the last user
message from its history; the gateway and TUI read it from the SessionDB
(fail-safe: goal sent verbatim).

Tests: a fragment that selects one option is kicked verbatim; a long goal
that is a minority of a longer message is kicked verbatim; all three
surfaces call the shared function.
2026-09-05 06:36:52 -07:00
Teknium 7e2dbcfb33 fix(goal): /goal <text> kicks the loop with a short pointer when the user's last message already carries the goal
`/goal <text>` queues the goal text as the next user turn to start the loop.
When that text is what the user just pasted (a handoff note, a plan the agent
already has), the kickoff re-sends it verbatim: in the 1,393-agent run
`/goal <2,000-char handoff>` came 16 minutes after the same note was pasted
as a message, and the agent spent 11 API calls / 6 min deciding it was a
replay, with the note duplicated in context from then on.

_goal_kick_prompt() compares the normalized goal against the LAST user
message (string or block content); when contained, the kick is a one-line
'[Goal set] Continue with the goal you were just given; there is no need to
re-read it.' Otherwise the goal text is sent as before. Both /goal <text>
and /goal draft use it.

Tests (2): a just-pasted goal (plain and block content) kicks with the
pointer; a new goal, an unrelated last message, and a goal pasted in an
OLDER turn kick verbatim.
2026-09-05 02:03:50 -07:00
Teknium 41e6b60249 test(update): fold the #97332 plan-kind cases into one invariant test 2026-09-05 00:24:40 -07:00
Teknium 1cce7c6dd8 fix(update): unmapped gateway stops no longer fail the fleet check with "no rows"
A gateway with no profile mapping (or one whose relaunch could not be armed) is
SIGTERMed and listed under "Restart manually" — by design it has no successor and
publishes no fleet-matrix row. It still counted in ``killed_pids`` and the
pre-restart snapshot, so ``_fleet_probe_expected_runtimes`` demanded rows that
could not exist and a fully successful update exited 1 with "Fleet version check
returned no rows even though gateway runtimes were expected", leaving the
fleet_restart_pending marker behind and every later CLI start warning about it.

Track the unmapped stops on the restart outcome and subtract them from the
row-predicting signals (``fleet_probe_signals``); relaunched/systemd gateways
still predict rows exactly as before.
2026-09-05 00:24:40 -07:00
liuhao1024 b8e3c5c700 fix(update): scope fleet-probe runtime expectation to gateway-kind plan records
A dashboard-only runtime plan (gateway never started) made
_fleet_probe_expected_runtimes() return True from the unfiltered
'plan.runtimes is non-empty' check. collect_fleet_versions() reports
gateway identities only, so the probe waited for rows that cannot exist,
printed the incomplete-verification warning, and exited 1 after a
successful update (#97332).

Key the plan-derived expectation on kind == 'gateway' records — the same
row-capability rule already applied to the Windows resume token (#93406)
— and update the two tests that pinned the old object() placeholder so
they pin the runtime-kind distinction. Restart-phase, killed-PID, and
pre-restart-PID signals still fail closed unchanged.
2026-09-05 00:24:40 -07:00
Teknium e17dd8b428 fix(update): protect the whole hermes_cli.update_* family from the stale-module purge
Widen the salvaged receipt protection to every updater-owned module: the
pre-update plan's ``RuntimeRecord`` dataclass (``update_inventory``) has the same
failure — the plan is built before the purge and the post-restart reconciliation
filters with ``isinstance``, so an evicted module means a fresh class, zero matches,
and an empty runtime-outcome report. One prefix rule instead of a name list that
has to be re-fixed per symptom.
2026-09-05 00:24:40 -07:00
WolveBrown fada877519 fix(update): preserve active receipt across module purge 2026-09-05 00:24:40 -07:00
kshitijk4poor 5ac75e91e2 test(tools_config): assert display/runtime parity for disabled composite toolsets
The runtime-only assertion passed on unfixed main (it exercised
model_tools, not _get_platform_tools). Replace it with the contract the
fix exists for: a toolset is listed iff at least one of its tools survives
_select_tool_names.
2026-09-05 12:21:28 +05:30
aydnOktay 2494eadb39 fix(tools): align inspection surfaces with disabled composite toolsets
hermes tools --summary and CLI /tools used name-level disabled_toolsets subtraction, so disabling debugging still showed terminal/web/file as enabled while runtime stripped those tools. Prune platform toolsets after tool-level subtraction and pass disabled_toolsets into CLI get_tool_definitions calls.

Fixes #97015

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 12:21:28 +05:30
kshitijk4poor ec1e6d50bc test(distributions): keep two invariant tests for grouped entries
Drop the literal-key assertion (change-detector) and the seeded 500-roll
loop (leaked global RNG state; vacuous if neither member rolls). Exercise
the fallback against a throwaway distribution so the contract no longer
depends on which browser_tasks entry has the highest probability.
2026-09-05 12:21:21 +05:30
falkoro 6ba09c8cc3 fix(distributions): keep browser+web_search co-occurrence at 97% via grouped roll
Independent 97% rolls for browser and search co-occur only ~94% of the
time, versus 97% when web_search was bundled inside the browser toolset
(hermes-sweeper review). Grouped "+"-compound entries roll once and
select every member together; browser_tasks now uses "browser+search".
Deterministic tests cover the hit/miss/fallback/invalid-member contracts
and a 500-sample co-occurrence invariant.
2026-09-05 12:21:21 +05:30
falkoro decacbac35 fix(toolsets): stop disabled browser from stripping web_search
web_search belongs to the web/search toolsets, but the browser toolset
also listed it as a member. Because disabled_toolsets is a strict
end-of-pipeline subtraction (#17309), disabling browser — a natural
choice for headless/Docker deployments with no Chromium — stripped
web_search from every session even when web was enabled.

Remove web_search from the browser composite so its membership no longer
couples web search to browser availability. The browser_tasks RL
distribution kept web_search via browser, so add the search toolset there
to preserve its effective tool availability.

Fixes #64503
2026-09-05 12:21:21 +05:30
Sohom Sahaun 1e69c12b64 feat(slack): interactive Block Kit model picker for /model
Cherry-picked from PR #101542 by @sohomsahaun.

Two-step drill-down: provider static_select → model static_select,
with Back/Cancel buttons. Resolves through the same gateway callback
as Discord/Telegram, so persistence and failed-switch rollback
semantics come for free.

Changes:
- send_model_picker() posts Block Kit provider/model picker via chat_postMessage
- _handle_model_picker_action() dispatches provider/model/back/cancel actions
- _model_picker_state bounded dict (cap=100, workspace-scoped keys)
- Expired-state UX: missing state rewrites message to expiry notice
- Failure header: both exception and gateway error-prefix get 'Failed' header
- Index-based option values avoid Slack's 75-char value cap
- 21 tests covering send, action dispatch, gateway integration

Co-authored-by: Sohom Sahaun <ssahaun19@gmail.com>
2026-09-05 11:05:17 +05:30
kshitijk4poor f5832f81ed test(streaming): drive the Relay finalizer through its collector
The chat_completions finalizer now reads collector-observed chunks, not the
consumer loop, so the test feeds the captured on_chunk before calling it —
the same ordering Relay guarantees.
2026-09-05 10:54:19 +05:30
kshitijk4poor 74de0fd4fe fix(relay): build the chat-completions Relay response from collector-observed chunks
Relay invokes its finalizer as soon as the provider stream ends, concurrently with
Hermes' consumer thread, so any finalizer that reads the consumer loop's closures
races the last chunk. #103104 moved `usage` onto the collector; the same race still
truncated the final tool-call `arguments` delta, dropped the last content delta, and
downgraded `finish_reason` to "stop" in Relay's annotated response (reproduced ~50%
of runs). Every other Relay integration (Anthropic, Bedrock, Codex) already
rebuilds from collector-observed chunks; this makes chat_completions match via
`_RelayChatAccumulator` and removes the single-field nonlocal stopgap.

Test: the ordering harness is shared, and a second invariant test pins the
tool-call/finish_reason case (fails on the stopgap, passes here). Hermes' own
returned response was never affected.
2026-09-05 10:54:19 +05:30
mnajafian-nv 2cf5e68c5d fix(relay): retain usage from trailing chat completion chunks
Signed-off-by: mnajafian-nv <mnajafian@nvidia.com>
2026-09-05 10:54:19 +05:30
Austin Pickett 79445a496c fix(desktop): macOS parent-death watchdog treats ps: marker drift as inconclusive (#103172)
* fix(desktop): treat a macOS ps: parent-marker mismatch as inconclusive, not death

`ps -o lstart=` is a TZ/locale-rendered wall-clock string. Electron caches it
once per app lifetime; the backend re-renders it per spawn. A timezone change
(travel, or the DST boundary) while the app stays open makes the SAME instant
differ byte-for-byte, and _is_serve_orphaned() treated that as proof the parent
died -- os._exit(0) ~5ms after HERMES_BACKEND_READY, silently, on every respawn
until a full quit.

Route mismatches through _parent_start_marker_mismatch_is_conclusive(): linux:/
win:/winms: machine markers stay conclusive (PID-reuse defence intact); any ps:
side degrades to the PID-liveness check the legacy Desktop path already uses.
Log one warning before os._exit(0) so the exit is no longer traceless.

Fixes #95693
Fixes #93958

Co-authored-by: Ahmett101 <Ahmett101@users.noreply.github.com>

* fix(desktop): reject truncated ps: parent markers; blank marker env means absent

A ps: marker that got whitespace-split in env plumbing (`ps:Sat`) passed
_valid_parent_start_marker and armed a watchdog that could never match, so
the backend exited 0 right after HERMES_BACKEND_READY. Require a full
lstart value (>=4 tokens, a 4-digit year, a time). Treat empty
HERMES_PARENT_START_MARKER / HERMES_PARENT_NONCE as absent so a blank
inherited value degrades to PID-only tracking instead of disarming or
misfiring.

* chore: map contributor email for drewTuzson

* fix(desktop): log when the parent-death watchdog disarms on an unusable marker

Disarming is the fail-safe branch (the backend keeps serving) but it also
means this backend will never reap itself when the Desktop dies. Leave one
warning naming the rejected marker so that downgrade is not traceless.

* test(desktop): live macOS proof that a TZ-drifted ps: marker no longer kills the backend

Runs the real start_server in a subprocess against the host ps, with the
marker rendered under Europe/Paris and the backend under America/New_York.
Asserts READY + still alive after several watchdog polls, then that the
backend still exits once the stand-in parent is killed. Fails on main with
the reported symptom (exit 0, live parent); macos_only lane.

---------

Co-authored-by: ygd58 <buraysandro9@gmail.com>
Co-authored-by: Ahmett101 <Ahmett101@users.noreply.github.com>
Co-authored-by: Drew Tuzson <drew.tuzson@uqual.com>
2026-09-04 20:24:51 -04:00
Gille a99340c247 fix(dashboard): prevent PTY input from blocking event loop (#93565)
* fix(dashboard): prevent PTY input from blocking event loop

* fix(win-pty): don't terminate a healthy ConPTY on write cancellation; log leaked write workers

Review follow-up to the backpressure fix.

CancelledError on WinPtyBridge.write() ran the same path as a timeout and
force-terminated the ConPTY. Cancellation means the owning socket went away
mid-write, which is the keep-alive session's normal reattach case, not a
wedged child; killing the process there defeats the PTY-outlives-socket
design. Give the in-flight write the shutdown grace window and only
terminate if it never lands.

When terminate() fails to unblock pywinpty, the worker stays parked in the
default executor. That was swallowed by a bare except; log it so a slow
thread-pool starvation is diagnosable.

---------

Co-authored-by: Austin Pickett <pickett.austin@gmail.com>
2026-09-04 20:24:24 -04:00
Austin Pickett 04fd0172cd fix(dashboard): stop the embedded TUI repainting on every OS app-switch (#103165)
* feat(pty_bridge): mark the dashboard-spawned TUI with HERMES_PTY_HOST

Ink needs to know when its emulator is the dashboard's xterm.js rather
than a native terminal, so it can drop hidden-tab recovery work that only
makes sense for emulators that coalesce output.

Co-authored-by: Raymond <supere989@users.noreply.github.com>

* fix(tui): skip the focus-in erase+repaint under the dashboard PTY

Ink answers a DECSET 1004 focus-in with a full clear+repaint to heal rows
a native emulator may have dropped while the tab was hidden. xterm.js fed
by the dashboard WebSocket never drops frames, and it reports focus on
every OS window blur/focus, so under the dashboard that repaint was a
visible "session reloaded" flash on every alt-tab. Keep the mode
re-assert and keep delivering the focus report to TerminalFocusProvider
(the composer hides its cursor on blur); only the repaint is skipped.

Co-authored-by: Raymond <supere989@users.noreply.github.com>

* fix(web): restore terminal focus after an OS app-switch

Alt-tabbing away and back lands browser focus on <body>, so Ctrl+V never
reached the composer. Pull focus back into xterm on window focus under
the same ownership rule tab activation already uses, extracted into
shouldRestoreTerminalFocus so both paths share it.

Co-authored-by: Raymond <supere989@users.noreply.github.com>

---------

Co-authored-by: Raymond <supere989@users.noreply.github.com>
2026-09-04 20:24:02 -04:00
Teknium f159e581c7 feat(models): add GPT-6 Astra + Astra Pro with fast/flex speed tiers to Nous Portal and OpenRouter
Six slugs land in the nous and openrouter curated lists, above the gpt-5.6 line:
openai/gpt-6-astra{,-fast,-flex} and openai/gpt-6-astra-pro{,-fast,-flex}.

Nous Portal serves the tiers as distinct slugs (verified live: each echoes its id, service_tier
default/priority/flex, cost 1x/2x/0.5x). OpenRouter serves them as ENDPOINTS of the base model
(tags openai/fast, openai/flex) and silently routes an unknown suffix to the standard tier at
standard price, so the OpenRouter profile rewrites a tier slug to its base wire model and pins
provider.only to that tier's endpoints (OPENROUTER_ENDPOINT_PINS). The base slug is pinned to
openai/azure/azure-us so default routing never lands on a flex or fast endpoint.

Provider-agnostic metadata: one DEFAULT_CONTEXT_LENGTHS entry (gpt-6-astra: 1,050,000, live on
OpenRouter for both models; substring-matches -pro and the tier suffixes). Pricing is skipped:
both routes bill via official_models_api. Reasoning floor not added (no evidence of long thinks).
2026-09-04 15:58:59 -07:00
kshitijk4poor 5a0b1ba766 test(local-models): assert _assign_default reaches the real assignment body
Follow-up to the salvaged fix from #102994. The quickstart leg tests
stubbed web_deps.late itself, so the resolution path that regressed
(late() -> web_server facade -> getattr) was never exercised; the PR's
proposed tests pinned the module string instead, which fails on any
future move of the symbol even when the call site is updated.

Stub only the leaf (web_server_config._apply_model_assignment_sync) so
the real late() runs, and add one invariant test: _assign_default must
call it with ("main", "llamacpp", model_id, "", "", ""). Red on base
(AttributeError), green with the fix; not tied to where the symbol lives.
Drop the docstring parenthetical that described test mechanics.
2026-09-05 00:29:42 +05:30
Leanolf aef409343d fix(local-models): point _assign_default's late-bound model assignment at web_server_config
The whole-codebase refactor moved _apply_model_assignment_sync from the
web_server facade into web_server_config, but _assign_default in
web_routers/local_models.py still resolved it through the default late()
module (web_server), which no longer carries the symbol. A local-model
quickstart therefore failed its final 'making it your default' step with
AttributeError: module 'hermes_cli.web_server' has no attribute
'_apply_model_assignment_sync'.

Pass the defining module explicitly to web_deps.late() so the call
resolves against hermes_cli.web_server_config, matching how the sibling
models.py router imports the same helper. Update the two quickstart test
stubs to accept the module argument the real late(name, module=...) takes.
2026-09-05 00:29:42 +05:30
Nacho Avecilla a6e10e693f fix(gateway): stop dropping the first message after an Azure agent sleeps (#99736)
* feat(gateway): let NAS broker the scale-to-zero suspend where the guest has no lever

* fix(gateway): require the going_idle ack and outlast the broker before suspending

* fix(gateway): release the redial hold on every path that abandons the suspend

* fix(gateway): hold the re-dial only for the brokered lever, and require it

* fix(gateway): make the re-dial hold contract real and fence the in-guest suspend too

* refactor(gateway): share the watcher-iteration and descriptor helpers across the sleep tests

* refactor(gateway): move the sleep tests' repeated setup into their fixtures

* refactor(gateway): one docstring line per sleep test, and parametrise the abort and lever paths

* fix(gateway): fence the freeze gap, cool down aborts, and stop clobbering a shutdown drain

* fix(gateway): slice the Fly freeze fence on the wall clock and widen it for larger machines
2026-09-04 15:41:27 -03:00
Teknium 13e72fb205 Merge pull request #102986 from NousResearch/fix/browser-exec-packaged-chromium
browser_exec local mode drives the packaged Chromium instead of the user's Chrome
2026-09-04 08:42:27 -07:00
Teknium 9e13ac2f01 fix(browser): browser_exec local mode drives the packaged Chromium, not the user's Chrome
Symptom: with Browser Use mode (the default) and no cloud provider / CDP
override, browser_exec left BU_CDP_* unset, so the browser-use harness ran
its own local discovery — hunting for the user's INSTALLED Chrome on its
default profile. That path needs the chrome://inspect remote-debugging
toggle plus an "Allow remote debugging?" popup per run, is blocked outright
on Chrome >=136, and on a headless host (or one without Chrome) fails with
"chrome-not-running: no supported Chromium-family browser is running". The
built-in browser_* tools never had this problem: they drive the Chromium
Hermes installs, launched through agent-browser.

Change: `_resolve_backend_cdp` step 4 becomes the local ENGINE —
lightpanda when configured, otherwise `_resolve_managed_chromium_cdp`,
which runs `agent-browser --session <key> get cdp-url` through the legacy
`_run_browser_command` (Chromium preflight/auto-install, per-key session
cache, inactivity reaper, atexit) and exports the returned ws:// endpoint
as BU_CDP_WS. Each cache key (task, or bu-named-<session>) gets its own
Chromium, so named sessions are private and skip the own-tab preamble.
Running `get cdp-url` on every call also refreshes agent-browser's idle
timer (it never sees the harness's direct CDP traffic) and follows a
relaunch after the reaper closed the browser.

Live: before, browser_exec on origin/main → exit 1 "chrome-not-running";
after → new_tab/page_info succeed on a cold start, warm reuse, post-reap
relaunch, and a named session; a missing Chromium surfaces the install
hint instead of the harness's Chrome hunt.
2026-09-04 08:30:32 -07:00
Teknium 8924b3aed9 fix(skills): lead the lesson-layer contract with the primary purpose — how to do the task, to the user's specifications 2026-09-04 08:15:22 -07:00
Teknium c240e65399 fix(skills): self-improvement writes lessons, not incident logs
The skill review fork, the combined memory+skill review, and the curator's
consolidation pass all described references/ as the place for
"session-specific detail", and the curator's demote step said to move a
sibling's file under the umbrella. Followed literally over months that
produced one dev skill with a 100k SKILL.md dense in PR numbers and 443
one-per-session reference files, plus five sibling skills restating the
same rules and the repo's AGENTS.md.

The three prompts now share one shape contract: an entry is an imperative
rule plus one clause of why, stated once; no PR/issue numbers, dates, or
quoted chat as content; references/ is a small topical set extended in
place, never a per-session file; skills do not restate always-loaded
context. Consolidation is defined as distilling, and copying a sibling
verbatim under references/ is named as the failure. skill_manage's schema
carries the one-sentence version.

Two advisory linter rules make the shape visible in the tool result the
moment it starts to drift: incident-log-shape (PR/issue-number density in
prose) and references-sprawl (>60 reference files), the latter also run
on references/ writes. On the real before/after: the old skill trips both,
the consolidated one trips neither.
2026-09-04 08:15:22 -07:00
Teknium f1ccf436a2 feat(web): Perplexity Search API as a web_search + web_extract backend
Adds plugins/web/perplexity — a keyed-only WebSearchProvider over httpx:

- search: POST https://api.perplexity.ai/search (documented Search API),
  search_context_size=low so `snippet` stays description-sized;
  results[].snippet -> description, max_results capped at the API's 20.
- extract: POST /sdk/content/snippets — the query-relevant page-excerpt
  route behind `pplx content snippets` (the CLI's `content fetch` is
  deprecated upstream). web_extract has no query, so the URLs' path words
  serve as the relevance query; per-URL `error` entries survive a 200.
- Wired into the same touchpoints as the other keyed vendors: legacy
  backend set + credential ladder + availability probe (web_tools),
  registry preference walk, OPTIONAL_ENV_VARS, `hermes config`/status/
  dump key lists, nous_subscription direct-credential detection, setup
  summary, test conftests, docs.

Not a keyless-ring member (Perplexity has no anonymous tier). Related
closed PRs #9192 / #23981 / #45225 predate the plugin ABC.
2026-09-04 07:17:00 -07:00
Teknium 957710a119 fix(plugin-compat): scan the plugin's real package, never the CWD; override needs literal true
The compat scanner derived a directory from manifest.path by splitting at the
first ':' and falling back to .parent when the result was not a dir. An
entry-point plugin (`vendor_plugin:register`) and a Windows path
(`C:\Users\...`) both collapsed to '.', so a stray .py in the launch directory
was attributed to the plugin and the installed package was never scanned.
After the removal date that disables the wrong plugin. `_scan_root()` now
takes directory manifests verbatim and resolves entry points via
importlib.util.find_spec to the installed package dir; anything unresolvable
scans nothing.

`plugins.allow_deprecated_imports` used bool(), so the YAML string "false"
opened the post-removal bypass. It now requires the literal boolean True, and
summary_lines() says "force-loaded" instead of "DISABLED" when the override is
what kept the plugins running.

Reported-by: ayushnangia (PR #102117 review)
2026-09-04 04:56:11 -07:00
Teknium 38742342b2 fix(auth): qwen OAuth login reaches _mark_qwen_oauth_active in its moved owner
The compat sweep dropped the auth-facade re-export of _mark_qwen_oauth_active
while hermes_cli/auth_commands.py still reached it through the auth_mod module
alias, so every 'hermes auth add qwen' died with AttributeError on this branch.
Import it from hermes_cli/auth_qwen where it now lives. Module-alias attribute
reads dodge import-time checks; the sweep in the PR thread found this to be the
only production hit of that class.

Reported-by: yoniebans (PR #102117 review)
2026-09-04 04:37:08 -07:00
Teknium d61cff60e3 fix(context): subdirectory AGENTS.md hints keep head+tail and warn when over the ceiling; ceiling 8k -> 32k
The on-demand subdirectory hint loader (agent/subdirectory_hints.py, #5291) tail-chopped anything past
8,000 chars with a bare marker and no log line. apps/desktop/AGENTS.md (11k) has been arriving cut off in
every Desktop-area session since it was written, and nobody could tell. Compared with the field: Codex
caps its whole instruction chain at 32 KiB (project_doc_max_bytes) and documents it; Claude Code and
Cursor apply no cap to nested files; OpenCode has no nested discovery at all. Our on-demand + cache-safe
+ ancestor-walk design is the strongest of the four; only the constant and the silent cut were wrong.

Now: ceiling 32,000 chars (Codex's number, a guard against a stray huge CLAUDE.md in a vendored tree, not
a target), and truncation goes through prompt_builder._truncate_content — head 70% + tail 20%, a marker
naming the file to read_file for the rest, and a WARNING in the log. Area AGENTS.md files should stay
around 8k anyway: the text lands in a tool result on the first touch of the directory.

Tests: oversized hint keeps head+tail, names the path, and logs; a 12k area file (over the old cap, under
the new) arrives intact.
2026-09-04 02:03:08 -07:00
Teknium 78288b488b compat(plugins): discovery itself refreshes the Desktop's report file
Live Desktop E2E (real Electron, worktree backend, demo plugin on old paths) found the modal never fired:
compat_report() was only called from the CLI banner / doctor / update / plugins-compat surfaces, none of
which run inside the Desktop's `serve` backend, so .plugin-compat-report.json was never written.
PluginManager.discover_and_load now refreshes the report from the manifests it just discovered (fail-open).

E2E after the fix, all five acceptance steps green on the real seat: report written and names the plugin;
native dialog 'Plugins need an update' with plugin, date, and `hermes plugins compat`; OK persists the
dismissal; relaunch with the same userData shows nothing and logs no 'compat notice shown'; fixing the
plugin's imports deletes the report and shows nothing.
2026-09-04 01:48:47 -07:00
Teknium 0a5164cebe compat(plugins): tell users which installed plugins break on 2026-09-14, and stop loading them after
hermes_cli/plugin_compat.py is now the single source of truth for the compat window:
  COMPAT_REMOVAL_DATE = 2026-09-14; scan_plugin() statically finds `from F import n`, `import F` + `F.n`,
  alias forms and string targets against compat_manifest.json; compat_report() aggregates over the user's
  ENABLED external (non-bundled) plugins; disable_reason() decides the loader's skip.

Surfaces (all read from that one report):
  * CLI: yellow block under the banner naming plugins + date + `hermes plugins compat` (red + DISABLED after)
  * `hermes plugins compat [--json] [path]`: file:line, old -> new per hit; exit 1 while anything remains;
    `path` lets a plugin author scan their own checkout
  * `hermes doctor`: "Plugin import paths (removed Sep 14, 2026)" section next to the xAI retirement check
  * `hermes update`: post-update notice alongside the FTS/curator notices
  * Desktop: compat_report() writes HERMES_HOME/.plugin-compat-report.json (deleted when clean); Electron
    shows ONE warning dialog per distinct report after the backend is up and persists the dismissal in
    userData/plugin-compat-dismissed.json. A new affected plugin, or the date passing, is a new report.

From the date, PluginManager skips a hitting external plugin before importing it, with the reason in
LoadedPlugin.error ("uses N import path(s) removed on 2026-09-14; run `hermes plugins compat` ...") — the
same path a plugin with a broken register() takes, so nothing else is affected. Escape hatch:
plugins.allow_deprecated_imports: true (config_defaults), which only helps until the compat commit is
actually reverted.

Docs: COMPAT_MANIFEST.md (removal date, what-happens table, author instructions), plugin dev guide section.
Tests: tests/test_plugin_compat_notice.py (scanner forms, report scope, date gate + escape hatch, summary
text, report file lifecycle, loader skip via a real PluginManager), electron/plugin-compat-notice.test.ts
(show once, re-show on a different set or on the date passing, malformed file ignored).

Live A/B on this box with a demo plugin on old paths: before the date it loads and the banner/doctor/report
name it; with today=2026-09-14 it is skipped with the reason and the banner turns red; with the escape
hatch it loads again.
2026-09-04 01:28:31 -07:00
Teknium 1392e4b857 test: modal integration test imports terminal helpers from their defining modules
It side-loaded tools/terminal_tool.py via importlib.spec_from_file_location under the bare name
'terminal_tool', which bypassed both the package and scripts/check_compat_pointers.py, then read
cleanup_vm through the compat pointer. Found by running the suite with HermesPluginCompatWarning
promoted to an error — the only in-tree resolution through a pointer across 44,7k tests.
2026-09-04 00:53:42 -07:00
Teknium d63e380324 compat(plugins): warn once per name when a plugin resolves an old import path; lint step restored in CI
Every PLUGIN-COMPAT __getattr__ now calls hermes_cli.plugin_compat.warn_once(facade, name, target) before
resolving, emitting a HermesPluginCompatWarning (FutureWarning) once per process per name: old path, new
path, removal target. Importing a facade for its live API stays silent; only resolving a moved name warns.
COMPAT_MANIFEST.md documents the warning and how to silence it during migration.

Verified the runtime never routes through a pointer: every entry point (run_agent, cli, hermes_cli.main,
gateway.run, tui_gateway.server, web_server, model_tools + tool discovery, hermes_state, cron.scheduler,
browser_tool, mcp_tool, kanban, auth) imports clean and `hermes doctor` runs end to end with the warning
promoted to an error.

Also restores the check_compat_pointers CI step to .github/workflows/lint.yml, which a0be177aac dropped
when the compat layer was regenerated (the lint script itself was present; the workflow step was not).

hermes_cli/plugin_compat.py, tests/test_plugin_compat_warning.py and the two-line insert per facade are
part of the compat layer and go away with it.
2026-09-04 00:15:16 -07:00
Teknium 27a4023791 docs+evals: AGENTS.md for the facade/siblings layout; codebase-navigability benchmark harness
AGENTS.md: Project Structure tree reflects the decomposition (run_agent 1.5k not 12k, cli 4.6k not 11k,
hermes_state facade + 21 siblings, web_routers/, evals/, test counts); new "Facade + siblings layout"
section with the sibling families table and the rules that follow (find by topic, patch where production
reads, compat pointers off limits, don't recreate god files); AIAgent/Agent Loop point at agent/turn_*.py
and conversation_loop; CLI dispatch documents _SLASH_DISPATCH + the _handle_<name>_command convention and
"Adding a Slash Command" no longer tells you to add an elif (there is no ladder to add to on either surface).

evals/codebase_navigability/: what the codebase costs an agent, not the CPU.
  bench.py         ~19k real "locate X" tasks from tests/ imports; tokens (tiktoken o200k) of the defining
                   file vs the symbol, context-window fit, read windows, siblings, symbol CC
  lookup_sim.py    paired grep+read simulation over 4k common symbols; tool calls + tokens returned
  static_metrics.py LOC split, size distributions, elif/nesting, radon CC/MI, import graph + SCC cycles
  runtime_bench.py  fresh-interpreter import/CLI/hot-path/collection timings with tree-purity assertion
tests/evals/test_codebase_navigability.py pins the resolver's facade/sibling behaviour.
2026-09-03 23:55:56 -07:00
Teknium a0be177aac fix(compat): pointers resolve to the object that MOVED, not a same-named stranger; stdin checker binds stdin= to the splatted definition
Review findings on #102117 (independent reviewer + itsflownium):

* hermes_cli.kanban_db.connect / connect_closing pointed at hermes_cli.projects_db (different DB, no
  board= parameter). The compat generator ranked candidate homes by path proximity when a name is
  defined in several modules. Now it requires shape compatibility with the BASE definition (same
  literal for constants, superset of parameter names for defs) and prefers the facade's own
  <stem>_* sibling. Same class fixed for tools.tts_tool.DEFAULT_XAI_BASE_URL (-> tts_tool_providers),
  and 17 constants/defs that had been pointed at same-named strangers (Matrix MAX_MESSAGE_LENGTH ->
  Signal's 8000, tts MAX_TEXT_LENGTH -> BlueBubbles', honcho/retaindb/supermemory *_SCHEMA -> another
  plugin's schema, ...) are now restored from BASE verbatim instead.
* send_yuanbao_direct (restored-def): body called adapter._outbound.send_direct, which HEAD moved to
  the sender; rewritten to adapter._outbound.sender.send_direct.
* COMPAT_MANIFEST.md states the scope explicitly: public top-level names only; private names and
  test monkeypatch seams are not preserved.
* scripts/check_subprocess_stdin.py: _splat_carries_stdin looked 30 lines ahead in the file text
  and was satisfied by an unrelated later stdin=; it now finds the splatted name's definition via AST
  and requires stdin inside that expression/body.

Tests: tests/test_compat_manifest_targets.py (pointer identity vs the facade's sibling; kanban
connect(board=) opens a Kanban DB, not projects.db; both FAIL on the previous layer),
test_subprocess_stdin_guard gains the false-negative probe, and the MoA -Q quiet-output contract
tests are back (tests/agent/test_moa_quiet_reference_output.py) against build_moa_facade.
2026-09-03 22:00:01 -07:00
Teknium ba030bc0db fix(test-seams): monkeypatch.setattr facade aliases — also patch the defining module (57 files)
Tests did monkeypatch.setattr(<facade module>, name) where name is now defined in a
sibling module and the production path reads the sibling's binding. Where production
reads through BOTH bindings the setattr is duplicated onto the defining module (import
added next to the existing alias import); where only the sibling reads it the target is
repointed. Seams whose production readers go through the facade are left alone.
2026-09-03 19:33:28 -07:00
Teknium c50b91039d fix(test-seams): cron/update/auth/transcription/computer-use — patch the module production actually reads (10 files) 2026-09-03 19:27:52 -07:00
Teknium 0b74043c19 fix(ci-fallout): head-moved-gate test stub accepts the already_restarted_units kwarg the fleet verifier now passes 2026-09-03 18:02:02 -07:00
Teknium b35836c928 fix(ci-fallout): repoint 3 more tests off old facade paths; packaging test tolerates sibling scratch root modules; compat lint also catches monkeypatch.setattr/patch.object on facade aliases 2026-09-03 17:45:53 -07:00
Teknium f43b976209 fix(ci-fallout): repoint the in-container docker test, two windows-only tests and the desktop exe-integrity patches to the defining modules; name bot_relay turn timeout/attempts as constants so the Desktop mirror test reads them instead of counting subprocess.run sites 2026-09-03 17:28:02 -07:00
Teknium deb9f3c1bd simplify(compat): hermes_cli/commands — drop the last PEP 562 lazy re-export hook (25 names), repoint 10 callers + 11 test files to commands_platforms/commands_completion 2026-09-03 17:00:26 -07:00
Teknium 342879f267 fix(compat-fallout): repoint 7 test files that still imported hermes_state/run_agent names from the old facade 2026-09-03 16:35:01 -07:00
Teknium eb2e96d657 fix(compat-fallout): module-restore also rebinds pkg.<child> attributes so 'from pkg import child' matches sys.modules after the wipe 2026-09-03 16:20:10 -07:00
Teknium 1bc3af0c05 fix(compat-fallout): sys.modules-wiping agent tests restore the original module objects afterwards (their wipe of hermes_*/tools.* detached sibling files' monkeypatches now that facades no longer re-import the defining modules) 2026-09-03 16:11:41 -07:00
Teknium 830b82bd03 fix(compat-fallout): dashboard tests patch defining modules (main_web_build._build_web_ui, web_routers.skills._installed_hub_identifiers) 2026-09-03 15:56:38 -07:00
Teknium e6bfbf82fa fix(compat-fallout): repoint 4 test files to defining modules (update_cmd._detect_venv_python_processes, main_install_repair subprocess/_resolve_install_target_python, process_bootstrap.OpenAI, tools.mcp_tool gate import) 2026-09-03 15:54:46 -07:00
Teknium da96762ef8 fix(compat-fallout): repoint 8 test files off dropped facade names (run_agent._DB_PERSISTED_MARKER/_EPHEMERAL_SCAFFOLDING_FLAGS/_is_ephemeral_scaffolding, cli.AIAgent, main._make_tui_argv/_print_curator_recent_run_notice/_format_time_ago, model_switch._collect_authed_provider_slugs, acp has_provider shim); arg_coercion test imports model_tools to populate registry 2026-09-03 15:50:55 -07:00
Teknium 310bed4302 fix(compat-fallout): tui_gateway tests stub the defining modules (tools.browser_tool_lifecycle, tools.tts_tool_speaker) instead of the old tools.browser_tool/tts_tool facades 2026-09-03 15:45:55 -07:00