Commit Graph

19914 Commits

Author SHA1 Message Date
brooklyn!
18627ff009 Merge pull request #75931 from NousResearch/bb/tui-slash-priority
The TUI slash menu leads with the skills you actually use
2026-08-01 02:01:44 -05:00
kshitij
3572d4bca1 fix(mcp): ensure MCP discovery completes before agent build in non-interactive sessions
Non-interactive sessions (hermes chat -q, hermes -z) snapshot the tool
registry at AIAgent construction time. If background MCP discovery hasn't
finished, MCP tools are invisible for the entire session — and unlike
interactive mode, there is no between-turns late-binding refresh to recover.

Root cause: wait_for_mcp_discovery() only joins an already-created discovery
thread, so it no-ops if a direct/single-query path reaches agent construction
before MCP startup created that thread. Oneshot._run_agent() didn't call it
at all.

Fix:
- Add ensure_mcp_discovery_before_agent_build() helper to mcp_startup.py:
  idempotently starts discovery if needed + bounded wait. Fail-open on errors.
- Add single_query parameter to _resolve_discovery_timeout/wait_for_mcp_discovery:
  uses mcp_single_query_discovery_timeout (default 15s) instead of the
  interactive mcp_discovery_timeout (1.5s) because one-shot sessions have no
  second turn to recover.
- Wire into CLI _init_agent (single_query from _single_query_mode flag set
  in cli.py's single-query path) and oneshot._run_agent (single_query=True).
- Interactive sessions unchanged: keep 1.5s bound (between-turns refresh covers).

Closes #38448, #51316, #37013, #68137
Composite salvage of #60017 (chrishart0), #51322 (Bartok9), #38620 (buptwz),
#43544 (halonke), #36882 (vanhoof).
2026-08-01 12:27:31 +05:30
brooklyn!
d1c40a731d Merge pull request #75949 from NousResearch/bb/fix-copy-with-reactions
fix(desktop): copy selected chat text again
2026-08-01 01:56:43 -05:00
Brooklyn Nicholson
90e6fe4f55 fix(desktop): terminal selection mirror yields to chat copy
mirrorSelection called textarea.select() whenever xterm had a scrap,
which replaced any chat highlight so ⌘C copied the wrong thing. Only
claim the document selection while the terminal is focused and nothing
outside it is highlighted.
2026-08-01 01:49:38 -05:00
Brooklyn Nicholson
1d97c035ef fix(desktop): drag-select and ⌘C work on user bubbles again
User bubbles are buttons, so the global user-select:none rule killed
text selection. Right-click-to-react and click-to-edit also ate a live
highlight. Prefer selection when one exists.
2026-08-01 01:49:38 -05:00
brooklyn!
85148f79f7 Merge pull request #75937 from NousResearch/bb/win-icon-size
fix(desktop): make the Windows app icon match native icon size
2026-08-01 01:41:24 -05:00
webtecnica
4be138eb03 fix(config): skip URL alias without extra_headers instead of returning early (#74465)
get_custom_provider_extra_headers() was returning the result of
normalize_extra_headers() on the first matching base_url, even when
that entry had no extra_headers configured. A later providers.<name>
entry sharing the same URL but with headers set was therefore ignored.

Fix: store the normalized headers and only return when non-empty,
otherwise continue searching the remaining entries.

Fixes #74465
2026-07-31 23:33:12 -07:00
Baophan00
595be544c0 test(config): add regression tests for broken-YAML config preservation
Verify that set_config_value and unset_config_value refuse to write
when config.yaml contains YAML syntax errors, and the original file
is left intact.
2026-07-31 23:33:12 -07:00
Baophan00
df09a90cd6 fix(config): refuse to write when config.yaml has YAML syntax errors
set_config_value() and unset_config_value() silently replaced the
entire config with an empty dict when config.yaml could not be
parsed. A single YAML syntax error would cause 'hermes config set'
to wipe all settings and write only the new key. Now exits with
error and preserves the existing file.
2026-07-31 23:33:12 -07:00
Brooklyn Nicholson
e524310118 fix(desktop): render the ico truly full-bleed
The first regeneration kept a ~5% transparent margin around the icon
plate (94.9% coverage). Windows expects the plate itself to be the icon
edge — scale the artwork's rounded plate to span the canvas exactly.
2026-08-01 01:33:05 -05:00
brooklyn!
41e55679ee Merge pull request #75848 from NousResearch/bb/toggle-terminal-persist
Toggle any pane wherever you put it, and keep the header hidden
2026-08-01 01:32:36 -05:00
brooklyn!
d41d9e4faa Merge pull request #75935 from NousResearch/bb/card-retire
The changed-files card stops at five rows
2026-08-01 01:31:30 -05:00
Brooklyn Nicholson
80c86c4949 fix(desktop): make the Windows app icon match native icon size
The shipped artwork bakes in the macOS-style ~10% transparent margin
(content covered only ~80% of the canvas), so the taskbar/titlebar icon
rendered visibly smaller than neighboring Windows apps, which draw
full-bleed.

- Regenerate assets/icon.ico full-bleed (~95% coverage) from the same
  art, with the standard 16-256px frames. This feeds both the exe stamp
  (set-exe-identity via rcedit) and the installer.
- On Windows, resolve the BrowserWindow icon from the full-bleed ico
  (resources/icon.ico, shipped via extraResources) before falling back
  to the padded apple-touch PNG.

macOS is untouched: the dock icon and icon.icns keep the padded art,
which is correct there.
2026-08-01 01:26:46 -05:00
Brooklyn Nicholson
feaa325033 Merge origin/main into bb/toggle-terminal-persist
main reworked the same surface while this was open, so three hunks needed
deciding rather than accepting.

Logs became summon-only (#75862): the contribution only exists while $logsOpen
is on, docked as its OWN zone beside the terminal instead of a tab in its
strip. That supersedes the static logs pane and the bindToolPaneCollapse call
here — main already registers logs' closer/opener directly, so both were
dropped in favour of its version.

main also added a ⌘K "Toggle terminal" row reading $terminalTakeover, and
gave logs back a 7.5rem minHeight under a comment claiming the terminal's
sizing rule. Both are the bugs this branch fixes, so they move onto the
shared behaviour: the palette row reads isPaneVisible/togglePaneVisible like
every other pane toggle, and logs loses the floor so the comment is true —
the sash folds its zone to the rail instead of stranding a sliver.
2026-08-01 01:25:33 -05:00
Brooklyn Nicholson
0b8a3582c6 fix(desktop): cap the changed-files card and fade its overflow
A turn that rewrote twenty files grew a twenty-row card, so the summary
that is supposed to close the turn became the thing you scroll past to
reach the composer. Cap the rows at ~5 and let the clipped edge fade,
the way every other overflow in the app reads.

The horizontal padding moves onto the scroller so a row's hover fill
still bleeds to the card's edge instead of stopping at a scroll gutter.
2026-08-01 01:24:05 -05:00
Brooklyn Nicholson
15b0b95413 refactor(desktop): one edge-faded scroller for the whole app
The kanban drawer had grown the only edge-aware masked scroller in the
tree, and the next surface that wants one would have copied it. Lift it
to components/ui as FadeScroll, export it on the plugin SDK, and leave
kanban's ScrollFade as a name its call sites already pass `max` to.

The mask math comes out as two pure functions. jsdom's CSS parser drops
any gradient containing calc(), so a rendered mask-image can't be read
back off the style attribute -- edgeMask/scrollEdges are testable for
real where the component's inline style is not.
2026-08-01 01:23:59 -05:00
kshitijk4poor
4773e965c0 chore: add hans@groupg.org → hansai-art to AUTHOR_MAP
Contributor email mapping for PR #66011 (model-switch marker dedup).
2026-08-01 11:53:03 +05:30
Eugeniusz Gilewski
64dd865912 fix(deps): repair Google transitive security floors (#72108)
Google API and authentication packages permit vulnerable httplib2 and pyasn1
transitives, while the Workspace and Google Chat runtime installers previously
treated any importable version as sufficient. Existing environments could
therefore remain vulnerable after the project dependency pins were repaired.

Carry the fixed versions through the Google and Vertex extras, lazy feature
requirements, lockfile, and both runtime installers. Route the documented
Google Chat installation path through its maintained secure requirements
instead of an unconstrained direct pip command.

Detect stale distributions, install only unsatisfied requirements, and verify
the result before continuing. Behavioral tests cover those repair invariants
without freezing manifests, lockfiles, or complete package sets.

Related #72108
Extracted from #72840
Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
2026-07-31 23:18:38 -07:00
Teknium
e008b62a72 fix(state): route no-more-rows retries through the shared patience helper; add contributor mappings
The rebase onto main's extracted _sleep_before_write_retry() method left
three call sites pointing at the dropped local helper; rewire them.
Also adds contributors/emails mappings (Dannou, trippyogi, spfcraze).
2026-07-31 23:18:12 -07:00
RelaxJonh
b6ca4fc856 fix(state): heal session_model_usage PK unconditionally to restore token/cost accounting
Installs whose state.db reached schema_version >= 22 before the task
dimension was added carry a 5-column PRIMARY KEY on
session_model_usage. The column reconciler ADDs task as a bare
nullable, but SQLite cannot ALTER a primary key, and the version-gated
v22 rebuild is unreachable (current_version < 22 already false), so
the composite 6-column key never lands. Every upsert in
_record_model_usage then fails with 'ON CONFLICT clause does not match
any PRIMARY KEY or UNIQUE constraint', aborting the enclosing write
transaction — token/cost accounting permanently dead (#73823).

Add an idempotent _heal_session_model_usage_pk() modeled on
_heal_gateway_routing_pk(), run unconditionally from _init_schema on
every open. Salvaged from #73838 with fix-ups:

- ported to SessionSchemaMixin in hermes_state_schema.py (the schema
  code moved out of hermes_state.py in 21c7ae8563; the PR targeted the
  old location)
- rebuild wrapped in a PRAGMA foreign_keys=OFF/ON window: the
  connection enables FKs before _init_schema and OR IGNORE does NOT
  suppress FK violations, so a single orphaned usage row (session
  pruned while accounting was broken) would have aborted the heal
- COALESCE('') on the nullable reconciler-added task column (and the
  billing columns) during the copy
- stale-v22+ regression tests: rebuilt PK + restored upsert, orphan
  rows survive the FK window, healthy-DB no-op, no legacy leftover

Fixes #73823
2026-07-31 23:18:12 -07:00
Dannoob
14eca89779 fix(state): retry transient 'no more rows available' across all sqlite3.Error classes
Under dual gateway/agent WAL contention (FTS5 trigram sync holding the
write lock on large appends) the SQLite engine can raise a transient
'no more rows available' error. The exception CLASS varies with the
build — some surface it as InterfaceError, a SIBLING of DatabaseError —
so it escaped both existing retry branches in _execute_write on attempt
0 and killed the turn as session_persistence_failed even though the
identical write succeeds standalone.

Port of #74934 onto the deadline-patience rewrite (8da8a7887d): the
PR's attempt-counted constants (60 retries / 300ms jitter / 2.0s engine
timeout) predate that rewrite and are superseded by the patience
budget, so they are intentionally NOT carried over. Instead the check
is message-scoped and rides the existing deadline/patience loop:

- extract the jittered-sleep-within-deadline logic into a shared
  _sleep_before_retry helper (behavior-preserving for locked/busy)
- retry 'no more rows available' from OperationalError, DatabaseError
  (checked BEFORE the FTS-corruption rebuild path so it is not
  misrouted), and a message-scoped sqlite3.Error catch-all
- any other error in any class propagates untouched on attempt 0

Tests: transient InterfaceError retried to success; unrelated
InterfaceError propagates immediately; DatabaseError variant retried;
exhausted patience surfaces the original error.
2026-07-31 23:18:12 -07:00
Teknium
d5463e5f6d fix(agent): invalidate flush-scan cursor at the defrag marker-pop sibling site
The micro-compaction defrag pass (_defrag_rolling_summary) rewrites the
newest MICRO marker's content and pops _DB_PERSISTED_MARKER from the
LIVE dict in place — the same in-place pop class finalize_turn's fill
site was fixed for in #75170. Without invalidation the bounded
flush-scan cursor identity-skips the rewritten marker row and the
defragged rolling summary never reaches state.db (resume rehydrates a
stale summary).

The compressor holds no agent reference, so the pop site raises
_flush_scan_cursor_invalidated and the finalize_turn micro-compaction
block consumes it, setting agent._db_flush_scan_prefix = None.

The module-scope pop sites (context_compressor.py:175/224) operate on
fresh copies — identity-breaking by construction — and need no flag.

Follow-up to #75170 (fix-the-class sweep of _DB_PERSISTED_MARKER
in-place pops).
2026-07-31 23:18:12 -07:00
spfcraze
2aaeee2ee5 fix(agent): invalidate flush-scan cursor when finalizer pops db marker
The bounded flush-scan in _flush_messages_to_session_db_unlocked skips
the identity-matched prefix of its previous snapshot, on the documented
assumption that no code path pops _DB_PERSISTED_MARKER from a live dict
in place. finalize_turn's pure-tool-call-tail fill is exactly that path:
it pops the marker so the filled content gets re-persisted — but the
cursor then skips the row anyway, so the delivered final response never
reaches state.db and /resume replays content="" (the #43849/#44100
class resurfacing via the perf cursor). Invalidate the cursor at the
pop site so the filled row is re-examined.
2026-07-31 23:18:12 -07:00
Jeremy
a266155cc4 fix(cli): untrack sqlite connections only after close succeeds
A failed close left the FD open while the byte-probe guard thought
nothing was live. Keep the registry entry until close actually works.
2026-07-31 23:18:12 -07:00
Baophan00
05103c6bde test(config): add regression test for scalar model sub-key preservation
Verify that setting model.provider/model.api_key after a scalar model
assignment preserves the original model id as model.default.
2026-07-31 23:18:09 -07:00
Baophan00
3ba67fd7bd fix(config): preserve scalar model id when setting model sub-keys
When model is a bare scalar (e.g. 'model: gpt-4o'), running
'hermes config set model.provider openai' silently destroyed the
model id because _set_nested replaced the scalar with an empty dict
before writing the sub-key. Now the scalar is normalized to
{default: <id>} first, preserving the model id.
2026-07-31 23:18:09 -07:00
Brooklyn Nicholson
73b8847d7b fix(desktop): toggle every pane off the tree, not off its own boolean
The terminal fix was only one instance. An audit of the other pane toggles
found ⌘G and ⌘J diverging the same way, proven with a probe: with review
stacked behind files in the right column, or either pane inside a minimized
zone, the store reads open while nothing is on screen, so the press
re-asserts a value it already held and the key does nothing.

isPaneVisible / togglePaneVisible replace the tool-panel-only pair and now
back every toggle. Close still routes through closeTreePane, so each pane
keeps its own semantics: a tool panel collapses to its rail, files and
review close through their store, anything else is dismissed.

files and review were bound with a closer and no opener, so the boolean went
stale as soon as anything but the toggle revealed them. bindPaneVisibility
moves into the tree store beside bindToolPaneCollapse, documents the two as a
pair, and both panes now pass both halves. Keeping the binding in the store
also means the tests drive the real function — the earlier copy in the test
file passed with the fix reverted, which is how the missing opener survived
the first pass.

setTreePaneHidden keeps its quiet path: a reactive unhide (a cwd arriving)
must not front or un-minimize over what the user is looking at. Only user
intent goes through the reveal path.
2026-08-01 01:17:45 -05:00
Brooklyn Nicholson
40ec9834b2 fix(tui): keep slash completion alive after a leading command
Typing a second slash command went dead whenever the message started with
one: `/work /cle` offered nothing while `do /work then /cle` completed
fine, which reads as an intermittent glitch rather than a rule.

Only the first slash can be an invocation, so detect the inline shape
first. The leading-command branch claimed the whole line and handed it to
the backend's completer, which has nothing to say about a slash sitting in
a command's argument tail. The inline trigger requires a whitespace-preceded
slash at the caret, so ordinary argument completion (`/cron ad`,
`/personality alic`) is untouched — it fires only where completion was
already dead.
2026-08-01 01:17:41 -05:00
Brooklyn Nicholson
609cd28b17 feat(tui): rank the slash menu by the skills you actually use
The `/` menu was a flat first-30 slice of the completer's output, and the
completer emits every registry command before the first skill. On a
230-skill install that meant a bare `/` filled all 30 rows with commands
and offered no skill at all, while `/p` cut off inside the alphabetical
skill block — dropping /proving-a-fix-works (471 invocations) and
/pr-update (160) but keeping /pretext (2).

Spend the limit per kind and rank the skill block by recorded usage
(the same .usage.json count Capabilities shows), most-used first and A-Z
within a tie. A bare `/` is browsing, so bundled skills that shipped with
Hermes and were never opened are dropped as noise; a typed query is a
search, and a search that hides a match is broken, so there nothing is
pruned and the ranking only reorders. An argument stage keeps the order
its own command chose.
2026-08-01 01:17:41 -05:00
Teknium
6989a79745 chore: contributor email mappings (rkfshakti, x7peeps) 2026-07-31 23:16:58 -07:00
Teknium
3b9cf56aff fix(agent): exclude reasoning_details envelope from tail-budget walk (#73298)
Companion to the preflight fix: _estimate_msg_budget_tokens charged
reasoning_details at chars/4 via _REPLAY_BUDGET_KEYS, so the signed/base64
envelope (measured 72% of the reasoning mass on Anthropic-wire sessions)
consumed the tail budget and _find_tail_cut_by_tokens summarized away real
transcript to make room for tokens that are never sent (69 messages on the
measured session; up to ~4.8x budget inflation on thinking-heavy histories).

Per the #51800 counter-argument, actual thinking TEXT stays visible to the
budget: _reasoning_details_text_chars counts thinking/text/summary fields
and skips signature/data/encrypted blobs, and the text is skipped entirely
when reasoning/reasoning_content already carries the identical prose (so it
is charged once, not twice). codex_reasoning_items remains fully charged —
Codex Responses genuinely replays it every request (#55572).

Sabotage-verified: restoring reasoning_details to _REPLAY_BUDGET_KEYS fails
the new envelope and double-charge tests.
2026-07-31 23:16:58 -07:00
JonthanaHanh
530503a6a5 fix: exclude reasoning_details from preflight token estimate
The reasoning_details field (OpenRouter/Anthropic thinking blocks +
opaque cryptographic signature blobs) inflates the rough token estimate
by ~4x. Providers do not bill these envelope bytes as prompt tokens.

In a measured Kimi K3 session, reasoning_details held 2,124K chars
vs 281K chars of actual thinking text. The estimator reported ~533K
tokens when real prompt_tokens was ~140K — triggering compression at
~27% of the configured threshold.

Fix: skip reasoning_details in both _estimate_message_chars and
_estimate_message_tokens_without_images, alongside the existing
_anthropic_content_blocks exclusion.

Fixes #73298
2026-07-31 23:16:58 -07:00
Doud-FR
3127ddcb64 fix(agent): preserve a non-empty user query after compression 2026-07-31 23:16:58 -07:00
Teknium
021d191478 test(gateway): real-DB restart regression for persisted hygiene cooldown (#74136)
Fix-up for the cherry-picked cooldown persistence: the PR's tests mocked
the DB (SimpleNamespace(_db=MagicMock())), which cannot prove the cooldown
survives a restart. Replace with the production shape — a real SessionDB
on disk behind the real AsyncSessionDB facade — and add a restart
regression: fail a hygiene compression on runner #1, tear it down, build a
fresh GatewayRunner on the SAME database, and assert the cooldown is still
honored (no compression agent instantiated). Also updates the timeout test
to assert the DB-backed record_compression_failure_cooldown write instead
of the removed in-memory dict.

Sabotage-verified: reverting gateway/run.py to the in-memory dict makes
the restart test fail.
2026-07-31 23:16:58 -07:00
Shakti Prasad Mohapatra
8e9702d227 fix: persist session hygiene compression cooldown to state DB
The session hygiene compression path tracked its per-session failure
cooldown in an in-memory dict (_hygiene_compression_failure_cooldowns).
When the gateway restarted, the dict was gone, so the next message
re-triggered the same failing compression, wedging session storage.

The state DB already has a persistent column
(sessions.compression_failure_cooldown_until) and full read/write/clear
methods (record_/get_/clear_compression_failure_cooldown in hermes_state.py)
used by the in-conversation compression path (context_compressor.py) but
not by the session hygiene path in gateway/run.py.

Fix: replace the in-memory dict with calls to the persistent DB methods:
- Cooldown check: use get_compression_failure_cooldown instead of dict lookup
- Timeout failure: use record_compression_failure_cooldown instead of dict write
- Abort failure: use record_compression_failure_cooldown instead of dict write

After a restart, a session whose compression is in cooldown is now skipped
for the cooldown's remaining duration rather than re-attempted immediately.

Fixes #74136
2026-07-31 23:16:58 -07:00
Teknium
d2098331e7 test(agent): add compress()-level E2E regression for short tool-suffix OOB (#75588)
Follow-up to the boundary clamps: drive the REAL compress() pipeline over
the live 8-message transcript shape from #75588 (system + tool-only
suffix, aligned head == len(messages)). Asserts no exception, the summary
LLM is never invoked when the window is out of range, and the transcript
is returned unchanged. Sanity-checks the clamped tail-cut boundary so a
future regression of the n+1 floor is caught even where downstream
callsite clamps mask the IndexError.
2026-07-31 23:16:58 -07:00
x7peeps
a1f70343fd fix(agent): clamp tail-cut boundary and summary-scan indices to prevent IndexError
Fix #75588

## Root cause

When a short conversation ends in a tool-call/result group and the
protected head alignment reaches the end of the message list,
_find_tail_cut_by_tokens() could return len(messages) + 1. This
happened because the final return used max(cut_idx, head_end + 1)
which could push past the array length when head_end >= len(messages).

The out-of-range value then propagated into _find_context_summaries()
which iterated range(start, end) and indexed messages[idx] without
clamping, raising IndexError and failing the active gateway turn.

## Fix

Two-layer defense:
1. Source fix: _find_tail_cut_by_tokens() now clamps its return to
   min(n, ...) so it never exceeds len(messages).
2. Defensive clamp: _find_context_summaries() now bounds start/end
   to [0, len(messages)] so even if a future caller passes bad values,
   it cannot crash.

## Verification
- 7 new regression tests for the exact boundary conditions
- All 214 existing test_context_compressor.py tests pass
2026-07-31 23:16:58 -07:00
brooklyn!
1f5040bdd0 Merge pull request #75918 from NousResearch/bb/pin-assistant-ui-0.14
fix(desktop): revert the assistant-ui 0.15 bump
2026-08-01 01:13:03 -05:00
Teknium
a11d0bdb01 fix(tests): remove stale shadowed test definitions in test_doctor.py
2de1e86c16 appended updated versions of five doctor tests without
removing the originals; the earlier definitions were silently shadowed
(dead) and tests/test_no_shadowed_test_definitions.py now fails on every
PR slice that runs it. Keep the later (runtime-winning) definitions,
delete the stale earlier ones.
2026-07-31 23:06:51 -07:00
Yuanang Yang
b5ca19118e fix(mcp): guard against duplicate spawns and stale connecting entries (#58862)
Three fixes for concurrent MCP server spawn races in register_mcp_servers()
and discover_mcp_tools():

1. register_mcp_servers: add k not in _server_connecting guard to the
   new_servers filter. Without this, a concurrent second call sees the
   same servers as 'new' and spawns duplicate stdio subprocesses.

2. discover_mcp_tools: same _server_connecting guard in the
   new_server_names filter. This entry point is called from CLI, TUI,
   gateway, and cron — any two racing would double-spawn.

3. Stale _server_connecting cleanup on TimeoutError/InterruptedError.
   When _run_on_mcp_loop times out or is interrupted, _discover_all's
   gather may not have finished, leaving entries stranded in
   _server_connecting that block future reconnection attempts. The
   cleanup clears only entries added by this call (not external ones),
   logs a warning, and records connect errors.

Salvage of #58879 by @nanami7777777 (superset of #58867 by @liuhao1024).
Adapted to current main which has evolved significantly since July 5.

Closes #58862
Closes #58867
Closes #58879
2026-08-01 11:35:59 +05:30
Brooklyn Nicholson
5826450d17 fix(desktop): declare @assistant-ui/core as a direct dependency
Twenty-odd desktop files import `@assistant-ui/core` directly, but it was
never in `dependencies` — it resolved only because react 0.15 pulled core
0.3.2 and npm hoisted it to the workspace root. react 0.14.24 wants core
^0.2.19, which nests under `react/node_modules`, so the undeclared imports
stop resolving and the build fails on `@assistant-ui/core/internal`.

Declare it so resolution doesn't depend on hoisting luck either way.
2026-08-01 01:03:53 -05:00
Brooklyn Nicholson
41e4b96233 fix(desktop): restore reactive message and composer runtime hooks
The 0.15 migration swapped the `useMessageRuntime()` hook for a plain
`useAui().message` accessor read. The hook subscribes and re-renders on
change; the accessor does not, so message components paint once and then
go stale until the window is reloaded. `useComposerRuntime().subscribe`
was widened to `aui.subscribe` the same way.

Restores the 0.14 call form, including the test mock whose added
`getState` stub let the API break through CI unnoticed.
2026-08-01 00:58:32 -05:00
Brooklyn Nicholson
031b0a6786 fix(desktop): pin @assistant-ui/react back to 0.14.24
The 0.15 bump rode along with the npm audit pass in #75037, but it was
not required by any advisory — `npm audit` reports 0 vulnerabilities with
0.14.24 pinned. It is a breaking major that costs us a working transcript,
so take the API stability instead.
2026-08-01 00:58:32 -05:00
Xipong
9ceb0858ab fix(codex): defang reserved Harmony tokens in requests 2026-07-31 22:53:20 -07:00
Teknium
0d9892379c docs(kanban): document profile-owned notification delivery in multi-gateway setups
Follows PR #75592: the notifier is no longer gated on
kanban.dispatch_in_gateway. Every gateway delivers events for
subscriptions owned by the profiles whose adapters it hosts; legacy
unstamped subscriptions go only through the confirmed dispatcher
lock owner. Adds a 'Multi-profile setups' subsection to the kanban
Gateway notifications docs (en + zh-Hans).
2026-07-31 22:47:49 -07:00
wz-heng
6b6435a874 feat(cache): enable DeepSeek caching on OpenCode 2026-07-31 22:45:07 -07:00
Israel Lot
5c45d9c208 fix(agent): mirror substitute_api_content's guard in the estimator shadow
Review follow-up on #75102. The shadow substituted the sidecar whenever
the ``api_content`` key was merely PRESENT, but the wire only substitutes
a non-empty string sidecar on a user/assistant row (see
``turn_context.substitute_api_content``). For any other shape the sidecar
is popped and discarded while the clean ``content`` is sent -- so the
shadow dropped real content from the estimate and UNDERcounted, the
dangerous direction: compaction fires too late and the turn dies on a
hard context-length error instead of merely compressing early.

Gate the substitution on the same predicate, and cover the divergent
shapes (None, empty string, int, list, non-user/assistant role) with a
test that fails against the unconditional version.

Also rename the image test: it never carried a sidecar, so it was not
testing what its name claimed. It is a non-regression pin on the flat
per-image accounting that moved into ``_wire_message_shadow()``, and is
now named for that.
2026-08-01 11:10:15 +05:30
Israel Lot
e3bc517034 fix(agent): stop double-counting api_content in the token estimator
`api_content` is a SUBSTITUTE for `content`, not an addition to it.
`turn_context.substitute_api_content()` pops the sidecar and overwrites
`content` at every API-bound message-build site (the `api_messages` build
in `conversation_loop`, the max-iterations summary in
`chat_completion_helpers`, the chat-completions transport), so exactly one
of the two is ever sent to the provider.

The preflight estimator counted both, because both `_estimate_message_chars`
and `_estimate_message_tokens_without_images` walked every key of the
persisted dict with a single-entry denylist (`_anthropic_content_blocks`).
Any message whose sidecar differs from its clean stored content was counted
twice — exactly 2.00x on a 40KB sidecar.

The sidecar exists to keep the provider prompt-cache prefix byte-stable, so
it is written on precisely the long, cache-pinned messages where the
doubling hurts most. Because `estimate_messages_tokens_rough()` also feeds
the compaction threshold via `context_compressor` and `conversation_loop`,
the inflated estimate makes compression fire on phantom bytes.

Fix: substitute rather than sum, mirroring the wire. The two estimator
helpers had drifted into near-identical copies of the same shadow-building
loop, so this factors the shared logic into `_wire_message_shadow()` and
fixes the class once instead of patching one site and leaving the other.

Image accounting is unchanged: base64 payloads are still replaced with a
placeholder and charged at the flat `_count_image_tokens` rate, and the
`_multimodal` text_summary path is preserved.

Tests: three cases in `TestEstimateMessagesTokensRough` — sidecar equal to
content is counted once, a sidecar that DIFFERS is still counted (a lower
bound, so it fails if the field were dropped rather than substituted, which
would undercount the real request), and a sidecar cannot smuggle raw base64
past the flat image rate.

Verified on Linux (Python 3.11): 53 passed in
tests/agent/test_model_metadata.py, 57 passed with
tests/agent/test_context_breakdown.py, 656 passed / 3 skipped across the
compression/context/token/estimate/prune surface of tests/agent.
Mutation-tested: reverting the substitution fails the new equality test.
`scripts/check-windows-footguns.py` is not applicable — no file I/O,
process management, terminal handling, subprocesses, or signals.
2026-08-01 11:10:15 +05:30
Teknium
fae0c4f5f4 fix(hindsight): create embedded profile env file owner-only (0600)
The embedded Hindsight daemon's profile env file carries the plaintext
HINDSIGHT_API_LLM_API_KEY but was written via bare write_text(), leaving
it with umask-derived (typically world-readable) permissions.

- Create/truncate the file via os.open(..., 0o600); chmod a pre-existing
  file to 0600 BEFORE writing new secret bytes.
- Post-write validation on POSIX: verify 0600, retry chmod, and raise if
  the file still isn't owner-only.
- If validation fails, unlink the secret file so a plaintext key is never
  left behind with unverified permissions.
- Regression tests under tests/plugins/ for fresh-write mode, tightening a
  pre-existing 0644 file, and cleanup on validation failure.

Narrowed reimplementation of #74236 confined to plugins/memory/hindsight/;
the core utils.py atomic-replace opt-out from the PR was dropped.

Co-authored-by: carrion256 <carrion256@proton.me>
2026-07-31 22:39:34 -07:00
Teknium
ee5a66ae3e fix(distribution): path-aware allowlist; preserve legacy copy-everything when omitted
Follow-ups to the previous commit (#74414 by @webtecnica, re #74373):

- When distribution_owned is OMITTED, restore the legacy contract: every
  staged entry outside USER_OWNED_EXCLUDE is copied. The cherry-picked
  filter consulted owned_paths(), which silently narrowed omitted-list
  distributions to DEFAULT_DIST_OWNED and dropped undeclared payload
  (extra top-level files/dirs existing distributions legitimately ship).
- Make explicit allowlists path-aware so documented nested entries like
  skills/research/ and cron/digest.json select exactly that subtree/file
  instead of being dropped by the top-level name comparison. Traversal
  segments (.., absolute) and USER_OWNED_EXCLUDE roots are still rejected.
- Regression tests: omitted-list legacy behavior + nested-path allowlist.
2026-07-31 22:39:34 -07:00