Commit graph

8718 commits

Author SHA1 Message Date
Teknium
2ca38e5df4 test(compression): pin scaffolding-tail standalone append + stale-snapshot refresh
Covers the follow-up hardening: continuation-marker and summary-as-user
tails keep the flagged standalone snapshot (zero-user provenance #69292
verified via _transcript_has_real_user_turn on the projected rows),
stale snapshot rows are refreshed in place, a previously merged snapshot
is stripped before re-injection, and an all-completed todo store injects
nothing (#26981).
2026-07-23 07:25:44 -07:00
Yingliang Zhang
33fd705421 fix(compression): preserve multimodal todo tails 2026-07-23 07:25:44 -07:00
Yingliang Zhang
d2bb6cc251 fix(compression): merge todo snapshot into trailing user msg to avoid consecutive user/user turns
After context compression, the preserved todo list was unconditionally
appended as a standalone user message. When the compressed transcript
already ends with a user message (common case), this creates consecutive
user/user turns — a role-alternation violation some providers reject.

Fix: fold the snapshot into the trailing user message (blank-line separated)
when one exists with plain-string content. Falls back to append when the
tail is non-user, empty, or has structured (list) content.

Rebased on current upstream/main.

Closes #53890
2026-07-23 07:25:44 -07:00
Teknium
18d83b4da9 test(agent): pin the #61932 all-oversized-tail dead-end shape as compressible
Regression test for the exact issue #61932 report: head + an 8-message
protected tail made exclusively of oversized tool pairs.  Pre-fix,
compress_start >= compress_end made compress() a pure no-op and the
retry loop ended in 'Cannot compress further'; post-fix the Phase-1
pressure demotion reclaims the tail in one pass while preserving
tool_call/tool_result pairing.
2026-07-23 07:25:33 -07:00
giggling-ginger
d12ea20009 test(agent): cover protected-tail last resort 2026-07-23 07:25:33 -07:00
giggling-ginger
fe2ae40983 fix(agent): demote oversized tool results in protected compression tail
After multiple in-place compactions, short tool-heavy sessions can leave
nearly every remaining message inside protect_last_n while those messages
are huge completed file/tool outputs. The middle compress window then
makes no material token progress and the turn dies with
"Cannot compress further" (#61932).

Cap the prune message floor at the same bound as tail-cut, and under
pressure demote bulky protected-tail tool bodies (keeping a short recent
floor) so preflight can reclaim headroom without wiping the active ask.
2026-07-23 07:25:33 -07:00
Teknium
4fbfb26704 test(compression): audit abort paths for per-attempt in-place state reset
Follow-up for salvaged #58629: thread the previous flush baseline through
the idle-compaction caller in turn_context.py (the one caller the original
PR predates), and add regression coverage that every early-return path in
compress_context (breaker skip, no-progress, plus the completed-rotation
boundary) resets the per-attempt in-place outcome so a stale
_last_compaction_in_place from an earlier successful in-place compaction
can never baseline unflushed turns as persisted.
2026-07-23 07:25:21 -07:00
Brett Bonner
79a83830ba test(compression): verify abort persistence after restart 2026-07-23 07:25:21 -07:00
Brett Bonner
03c96b7ab5 test(compression): retain durable persistence regression 2026-07-23 07:25:21 -07:00
Brett Bonner
17b3a4bd41 fix(compression): preserve flush baseline after abort 2026-07-23 07:25:21 -07:00
Teknium
929c952596 fix(agent): wire should_compress_preflight into the turn-start preflight flow
Relocates the #20424 wiring: the preflight region moved out of
run_agent.py into agent/turn_context.py (and through the
compression.max_attempts unification, #69315), so the contributor's
elif branch is reapplied at its current home as the else arm of the
threshold dispatch chain.

Integration contracts:
- Byte-identical default: the built-in ContextCompressor inherits
  ContextEngine.should_compress_preflight() -> False, so the default
  path performs no compression and touches no turn bookkeeping
  (pinned by test_builtin_compressor_default_sub_threshold_path_unchanged).
- Attempt-cap: the engine gets exactly ONE compress() pass per turn,
  mutually exclusive with the cap-bounded threshold multi-pass loop,
  so turn-start passes stay within the resolved
  compression.max_attempts budget in every case.
- No-op blocking (#64382 / 377244f7c): an engine pass that no-ops
  (_compress_context returns the input list object) neither sets nor
  clears preflight_compression_blocked and does not re-baseline the
  flush history — a sub-threshold maintenance no-op proves nothing
  about over-threshold compressibility.
- Engine exceptions are swallowed at debug level; cooldown/defer/
  codex-native gates run before the hook is ever consulted.

Salvaged from #20424 by @Beandon13. Fixes #20316.
2026-07-23 07:25:08 -07:00
Brandon Zarnitz
d1c0c33a8b fix(run_agent): call should_compress_preflight() for sub-threshold engines (#20316)
Context engines that override ``should_compress_preflight()`` (e.g. the
hermes-lcm plugin's incremental leaf-chunk compaction) never had their
hook fired by ``run_conversation`` because the preflight block exited
early once the hardcoded ``>= threshold_tokens`` check failed.  As a
result, ``LCM_DEFERRED_MAINTENANCE_ENABLED=1`` and friends were inert
and accumulated raw_backlog debt indefinitely.

Add an ``elif`` branch that delegates to the engine's preflight hook
when the legacy threshold check does not fire.  The default
``ContextEngine.should_compress_preflight()`` returns ``False`` so the
built-in ``ContextCompressor`` is unaffected; engines opting in get a
chance to ingest messages and request a single ``compress()`` pass for
deferred maintenance.  Exceptions are swallowed at debug level so a
buggy engine cannot break an otherwise-healthy turn.

Closes #20316
2026-07-23 07:25:08 -07:00
Teknium
d462116226 test(cli): prove /compress type-ahead queue-drain; map contributor email
- tests/cli/test_compress_type_ahead.py: end-to-end proof of the PR #68284
  docstring claim — a prompt queued into _pending_input while /compress runs
  survives compaction untouched and is the next item process_loop drains,
  i.e. it is processed against the compacted history. Plus a structural
  guard that handle_enter never gates on _command_running /
  _command_blocks_input (read-only enforcement belongs solely to the
  TextArea Condition; a busy-gate in handle_enter would silently drop
  type-ahead input).
- tests/test_cli_manual_compress.py: update the one remaining _busy_command
  stub (added on main after the PR branched) to accept the new
  blocks_input kwarg.
- contributors/emails: map lucas@policastromd.com -> enzo2.
2026-07-23 07:24:46 -07:00
Lucas Policastro
0bc7fb2b3b fix(cli): keep composer editable during compression 2026-07-23 07:24:46 -07:00
Teknium
2cabeeabca refactor(tui): relocate /compress arg parsing into _compress_session_history choke point
Follow-up to the salvaged #35533 fix: instead of parsing 'here [N]' only in
_mirror_slash_side_effects, parse it inside _compress_session_history — the
single helper all three manual-compress routes converge on (session.compress
RPC, command.dispatch /compress|/compact, slash-exec mirror). Every route now
honors the boundary-aware forms (here [N], up to here, --keep N) with the
same head/tail split + rejoin as cli.py and gateway/slash_commands.py, and
keeps the choke point's existing guards (lock-free LLM call, history_version
race check, deferred context-engine notification).

Tests: choke-point unit tests for 'here N', degenerate-split fallback, and
focus-topic passthrough, plus endpoint-level tests on each of the three
routes.
2026-07-23 07:24:34 -07:00
AhmetArif0
9284a3402f fix(tui-gateway): parse partial compress args in /compress here [N]
_mirror_slash_side_effects() passed the raw argument after /compress
directly as focus_topic to _compress_session_history. So /compress here
3 silently used "here 3" as a summary focus topic and did a full
compress instead of preserving the last 3 exchanges verbatim.

Fix: call parse_partial_compress_args() on the argument first. When it
detects a boundary-aware form (here, here N, up to here, --keep N),
split the history into head/tail using split_history_for_partial_compress,
compress only the head via agent._compress_context, and rejoin with
rejoin_compressed_head_and_tail — exactly mirroring cli.py and
gateway/run.py's /compress here implementation (PR #35252).

Non-boundary forms (plain /compress, /compress <focus>) fall through
to _compress_session_history unchanged.
2026-07-23 07:24:34 -07:00
mattshapsss
9d4cc12605 fix(photon): register Photon as a low-verbosity display tier
Photon (managed iMessage) shipped without a _PLATFORM_DEFAULTS entry, so it
inherited the noisy global ('all') display defaults and narrated tool
progress / heartbeats / busy-ack detail into a permanent-message iMessage
thread. Register it as TIER_LOW alongside BlueBubbles and Signal, plus a
regression test guarding the tier.

Salvaged from #50511 (Photon tier piece only).
2026-07-23 07:24:21 -07:00
wz-heng
91546b8337 fix: preserve named custom provider vision overrides 2026-07-23 17:57:33 +05:30
kshitij
97d51ca20d fix: use canonical _get_platform_tools resolver for memory tool status
The PR's inline toolset resolution (checking 'memory' in cli_toolsets
list) produced wrong results for composite toolsets like 'hermes-cli'
which expand to include the memory tool. Replace with the canonical
_get_platform_tools() from tools_config.py which correctly handles
composite toolsets and all edge cases.

Update tests to mock _get_platform_tools instead of raw config.
2026-07-23 17:50:20 +05:30
kshitij
c82c5786f4 test: cover config-aware memory status labels + AUTHOR_MAP entry
Add tests/hermes_cli/test_memory_status.py with 11 tests covering:
- No hardcoded 'always active' label
- memory_enabled, user_profile_enabled, memory toolset indicators
- Tool enabled/disabled via platform_toolsets.cli
- Provider still shown alongside indicators

Add huajiang@tubi.tv → thirstycrow to AUTHOR_MAP (PR #23630 salvage).
2026-07-23 17:50:20 +05:30
Fangliquan
9220c0c0bb fix(agent): close tool-result tails on invalid-tool and truncated-tool early returns
Invalid-tool exhaustion and truncated-tool early returns skipped finalize_turn, leaving role=tool transcripts that become tool→user on the next turn for strict providers. Call close_interrupted_tool_sequence before persist on those paths (same as interrupt aborts).
2026-07-23 17:38:05 +05:30
HexLab98
a94f8e69e1 test(state): cover SQLite WAL-reset version gate and doctor probe
Assert the version matrix, fresh-DB DELETE fallback, already-WAL left
alone (no checkpoint/DELETE), fixed-SQLite WAL path, and warn-only
doctor output for vulnerable builds (#69784).
2026-07-23 17:32:38 +05:30
Brooklyn Nicholson
9859e1f7df fix(voice): speak the whole turn, not just its first bubble; idle-flush held narration
Two fixes for desktop hands-free voice:

- The live speech session bound to the first assistant bubble with text, so
  a tool-calling turn spoke only the opening narration and silently dropped
  every later interim AND the final answer. The conversation selector now
  aggregates all unspoken assistant bubbles in order (turn-scoped speech);
  auto-speak keeps its latest-reply-only behavior.

- The speak-stream WS producer blocked forever on the text queue, so a
  narration line with no trailing whitespace ("Let me check.") sat in the
  sentence chunker until end-of-turn — spoken long after the tool finished,
  with the UI stuck on "Preparing audio…". Mirror the CLI speaker's idle
  flush: sentence-terminated buffers flush after 0.5s of producer silence,
  anything else after ~2s; open <think> blocks are never flushed.
2026-07-23 01:53:37 -05:00
Brooklyn Nicholson
cc1765dce2 Merge remote-tracking branch 'origin/main' into bb/computer-use-perf
# Conflicts:
#	hermes_cli/config.py
#	tools/computer_use/cua_backend.py
2026-07-23 01:39:21 -05:00
brooklyn!
93f8da55cb
Merge pull request #69903 from NousResearch/bb/salvage-53841-no-overlay
fix(computer_use): disable cua-driver overlay by default on macOS/WSL (supersedes #53841)
2026-07-23 01:36:49 -05:00
Brooklyn Nicholson
59a735b8f3 feat(credits): report $used of $cap instead of % in the usage notice
The usage gauge is Nous subscription-cap-only (used_fraction requires a cap;
non-Nous providers emit no headers, so no notice fires). A bare percentage
implied a universal unit that doesn't exist, so report the absolute dollars
used of the cap instead: used = cap - remaining, from micros (money-safe),
clamped to [0, cap]. Still a snapshot at band-crossing (re-emits on band
change, not every turn) to keep the single escalating line and stay quiet on
append-only surfaces (messaging pushes one message per crossing).
2026-07-23 01:28:58 -05:00
Brooklyn Nicholson
69b97a97f7 Merge remote-tracking branch 'origin/main' into bb/salvage-53841-no-overlay
# Conflicts:
#	tools/computer_use/cua_backend.py
2026-07-23 01:26:21 -05:00
brooklyn!
a2172547a8
Merge pull request #69902 from NousResearch/bb/salvage-cua-driver-path
fix(computer_use): resolve cua-driver under thin GUI PATH (supersedes #55631)
2026-07-23 01:24:04 -05:00
Brooklyn Nicholson
8b6d34ad85 test(computer_use): pin resolved driver for update-check tests
cua_driver_update_check now short-circuits to None when no driver
resolves; CI has none installed, so pin resolve_cua_driver_cmd in the
update-check and env-sanitization tests.
2026-07-23 01:10:19 -05:00
Brooklyn Nicholson
12ad13ddca perf(computer_use): cap capture size and cache vision routing
Cut steady-state Computer Use latency without changing default behavior
or waiting on cua-driver:

- Cap screenshots via set_config(max_image_dimension) on session start
  (config: computer_use.max_image_dimension, default 1456)
- Cache aux-vision routing per (provider, model) so captures skip
  repeated load_config()
- Add computer_use.capture_after_mode (default som) so users can opt
  follow-ups down to ax (elements only) for speed
2026-07-23 00:52:43 -05:00
Brooklyn Nicholson
f957fe3760 fix(computer_use): default --no-overlay on macOS for idle CPU
Auto-detect now disables the cursor overlay on darwin as well as
headless/WSL2 Linux. After start_session, also call
set_agent_cursor_enabled(false) when the policy is on so older drivers
without --no-overlay still tear the overlay down.

Co-authored-by: David Metcalfe <80915+DavidMetcalfe@users.noreply.github.com>
2026-07-23 00:45:26 -05:00
ethernet
de5ece9944
fix(ci): report E2E evidence upload failures (#69901)
Print gh-image stdout and stderr on attachment failures, then replace the
pending inline-evidence marker in the PR review comment with an escaped
failure notice before preserving the failing workflow result.
2026-07-23 05:45:17 +00:00
David Metcalfe
3d84689714 fix(computer_use): address sweeper feedback on --no-overlay subprocess + manifest probe
The hermes-sweeper review #4701565902 (2026-07-15) flagged two
consistency issues in `_cua_driver_supports_no_overlay` and one
additive-config concern:

1. `cua-backend.py:260` — the `cua-driver --help` support probe
   inherited the full parent environment. cua-driver is a third-party
   binary; every other spawn site in this file (manifest probe at
   `:214`, MCP spawn at `:697`, install probe at `:997`) uses
   `_sanitize_subprocess_env(cua_driver_child_env())`. The `--help`
   probe should match. This was a low-impact leak (only help output
   exits), but inconsistency is the wrong default for a third-party
   subprocess.

2. `cua_backend.py:238` — when the manifest returned a `command`
   different from the input `driver_cmd` parameter (e.g. a relocated
   executable at `/opt/relocated/cua-driver` while the system binary
   is at `/usr/bin/cua-driver`), the support probe ran against
   `_CUA_DRIVER_CMD` (the default) instead of the manifest-discovered
   `command`. Two failure modes:
   - The wrapper binary supports `--no-overlay` but the system binary
     doesn't → probe returns False → overlay kept despite capability.
   - The system binary supports `--no-overlay` but the wrapper doesn't
     → probe returns True → MCP spawn crashes on the unknown flag.

3. The original commit bumped `_config_version` 31→32 for an additive
   default (`computer_use.no_overlay: None`). AGENTS.md specifies that
   additive defaults in existing sections are handled by deep merge
   and should NOT trigger a version bump. After cherry-picking onto
   current `origin/main` (which is already at 33), the bump is
   effectively dropped — resolved to main's 33.

Changes:

- Add `env=_sanitize_subprocess_env(cua_driver_child_env())` to the
  `--help` subprocess (with the same import + rationale comment as
  the manifest probe).
- Pass `driver_cmd=command` (or `driver_cmd=driver_cmd` for the
  fallback path) into `_mcp_args_with_overlay_flag`, so the support
  probe runs against the binary that will actually be launched.

Tests (3 new):

- `test_help_probe_passes_sanitized_env` — verifies `subprocess.run`
  is called with an `env=` kwarg.
- `test_manifest_command_drives_support_probe` — verifies the probe
  runs against the manifest command when it differs from the input
  driver_cmd.
- `test_fallback_uses_input_driver_cmd_for_support_probe` — verifies
  the fallback path (no command in manifest) uses the input
  driver_cmd.
- `test_probe_distinguishes_support_between_binaries` — sanity check
  that the lru_cache key on `driver_cmd` prevents cross-binary
  cache leakage.

File-revert negative test confirmed all three of the new
"manifest/probe" tests are load-bearing: with the pre-fix code, they
fail (probe runs against the default binary instead of the resolved
one); with the fix, they pass. 20/20 tests in
`tests/computer_use/test_cua_no_overlay.py` green.
`TestMcpInvocationResolution` (8/8) still green.

Refs: sweeper review #4701565902
2026-07-23 00:44:43 -05:00
David Metcalfe
8d4f7a0002 fix(computer_use): refine auto-detect to headless/WSL2 only, add driver version probe
Address review feedback from cross-vendor review (Flash + GPT-OSS):

1. Auto-detect now checks for headless Linux (no DISPLAY), WSL2
   (/proc/version contains 'microsoft'), instead of all Linux.
   Desktop Linux with a compositor keeps the overlay.

2. Add _cua_driver_supports_no_overlay() that probes cua-driver --help
   to check if the flag is supported. Older drivers (< 0.6.x) reject
   unknown flags, so passing --no-overlay would crash the MCP spawn.

3. Update tests to cover headless vs desktop Linux, WSL2 detection,
   version probe, and the unsupported-driver fallback path.
2026-07-23 00:44:43 -05:00
David Metcalfe
f43ff5b4bb fix(computer_use): mock _cua_no_overlay in existing tests, fix platform-dependent assertions
- Add autouse fixture to TestMcpInvocationResolution to disable
  --no-overlay flag so existing tests assert baseline args
- Make test_config_load_failure_fails_safe and test_missing_section_enables
  platform-aware (Linux auto-detect returns True, macOS/Windows False)
2026-07-23 00:44:43 -05:00
David Metcalfe
8ceada6e30 fix(computer_use): pass --no-overlay to cua-driver on Linux/WSL2 to prevent idle CPU
cua-driver's cursor overlay rendering loop can consume CPU indefinitely
when idle (#28152, #47032). On Linux/WSL2, the overlay serves no visual
purpose and the rendering path is the primary source of idle CPU usage.

Add computer_use.no_overlay config option (default: auto-detect) that
passes --no-overlay to cua-driver when enabled. Auto-detection disables
the overlay on Linux (covers WSL2, headless, containers) where it has no
benefit, and keeps it enabled on macOS/Windows where it is visually
useful.

Refs: #28152, #47032
2026-07-23 00:44:43 -05:00
Brooklyn Nicholson
be96f22026 fix(computer_use): probe resolved cua-driver path in post_setup
Keep thin-PATH resolution while asserting the absolute binary on the
already-installed version check. Map contributor emails for the salvage.

Co-authored-by: Tianqing Yun <yuntianqing@yahoo.com>
Co-authored-by: Trevor Gordon <trevorbgordon@gmail.com>
Co-authored-by: Adrian Soto Mora <adrian.soto6@gmail.com>
2026-07-23 00:44:27 -05:00
Tianqing Yun
2f9d88caee fix: resolve cua-driver across computer-use surfaces 2026-07-23 00:42:46 -05:00
Tianqing Yun
030822b68d fix: resolve cua-driver from user-local install paths 2026-07-23 00:42:46 -05:00
brooklyn!
a8e6c0f853
fix(dashboard): isolate Desktop-inherited env from standalone launch (#69891)
Supersedes #52948 and #67402. Closes #52945.

Standalone hermes dashboard/serve was trusting HERMES_WEB_DIST and
HERMES_SERVE_HEADLESS inherited from a Desktop Electron parent, which
could serve the packaged desktop renderer ("Desktop IPC bridge is
unavailable") or disable the SPA. Drop only Electron-packaged WEB_DIST
paths (app.asar*) when HERMES_DESKTOP!=1, and clear inherited headless
for non-serve launches, while preserving caller-managed custom dist
overrides and the desktop-spawned backend path.

Co-authored-by: Bartok9 <danielrpike9@gmail.com>
Co-authored-by: Commander <commander@tianji.local>
2026-07-23 00:31:23 -05:00
brooklyn!
0721d2ea80
Merge pull request #69884 from NousResearch/bb/salvage-55707-onboarding
fix(desktop): stop spurious onboarding for keyless custom providers (supersedes #55707, #45224)
2026-07-23 00:28:29 -05:00
Teknium
c1b0f6f3c1
feat(kanban): per-task model dropdown — set/override worker model+provider from the board (#69876)
Adds the missing write path for the per-task model_override column (which
was previously only settable via manual SQL) and pairs it with a
provider_override so cross-provider switches resolve correctly:

- kanban_db: provider_override column (+migration), set_model_override()
  with model_override_set event, create_task(model_override=,
  provider_override=), dispatcher spawns worker with -m <model>
  [--provider <name>]
- dashboard: Model row in the task drawer — dropdown fed by a new
  /model-options endpoint (build_models_payload substrate, provider-grouped,
  free-text fallback), PATCH + bulk model override support
- CLI: kanban create --model/--provider, new kanban set-model subcommand,
  show prints the provider
- agent tools: kanban_create accepts model/provider; show/list expose
  provider_override

Rate-limit recovery flow: override is settable on running tasks and takes
effect on the next dispatch, without touching the worker profile's config.
2026-07-22 22:23:24 -07:00
Brooklyn Nicholson
a01499136d fix(tui_gateway): skip credential warning for keyless custom providers
`no-key-required` is a valid sentinel for local/self-hosted/custom
routers. Warning on it made Desktop treat a working setup as missing
API keys.

Co-authored-by: Yingliang Zhang <zhangyingliang@outlook.com>
Co-authored-by: Brandon R <kingdomwarrior23@gmail.com>
2026-07-23 00:18:21 -05:00
brooklyn!
e59dcf46f1
fix(desktop): end the #67603 model-switch dup, cross-profile session bleed, and [System:] bubble (#69861)
* fix(desktop): stop model-switch dup + route recovery resumes to the owning profile

Fixes two Desktop session-reconciliation symptoms from #67603.

Symptom 1 — duplicated user bubble after a model switch. The gateway
persists model-switch / personality notices as role=user `[System: …]`
rows (tui_gateway/server.py) so strict OpenAI-compatible providers don't
reject a non-leading system message (#48338). `preserveLocalPendingTurnMessages`
paired local optimistic rows with the stored transcript by user-role
ordinal, so a marker between two real user turns shifted every later
ordinal and the optimistic row was re-appended at the bottom. The single
trailing-marker case is already covered by the compression-era
`latestAuthoritativeUser` guard, but two switches around one turn (marker
before AND after the committed prompt) still duplicated it. Exclude
`[System:` bookkeeping markers from ordinal pairing on both sides.

Symptom 2 — a session appearing under two profiles. The main resume path
already resolves a session's owning profile via `resolveStoredSession`
(cache → active backend → cross-profile probe), but the recovery
`session.resume` calls (stale runtime id, session-not-found, wedged loop,
redirect) omitted `profile`, so the gateway fell back to the launch-profile
DB and forked the conversation into the wrong profile. Route every recovery
resume — and an uncached right-click branch — through the same resolver so
the profile is carried even for sessions outside the paginated sidebar
window (the cache-miss gap).

Tests: discriminating two-switch marker test (fails before, passes after);
cache-hit + cross-profile cache-miss coverage for the recovery resume and
for branching an uncached session.

Supersedes #68665 and #63590.
Closes #67603.

Co-authored-by: Dolverin <5910064+Dolverin@users.noreply.github.com>
Co-authored-by: oliviaaaa7788 <274182427+oliviaaaa7788@users.noreply.github.com>

* fix(desktop): scope the remembered session id per profile

A single global `hermes.desktop.lastSessionId` key remembered ONE session
across every profile, so a relaunch or cold start under profile B would try
to restore a session owned by profile A — reinforcing the impression that a
conversation had bled between profiles (#67603, second symptom).

Key the remembered id by the session's owning profile (resolved from the
session row's `profile`, falling back to the active gateway profile), read it
back for the active profile on restore, and clear an exhausted session under
its owner. The default profile keeps the original unsuffixed key so existing
installs' remembered session survives the upgrade.

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

* fix(gateway): hide [System:] bookkeeping markers from every transcript

Model-switch and personality notices are persisted as role=user `[System: …]`
rows so strict providers accept them mid-history, but they are model-facing
runtime metadata, not user turns. `_history_to_messages` — the single display
projection every client reads — passed them straight through, so on resume or
reload they rendered as a fake user bubble in the desktop, TUI, CLI, and web
transcripts.

Drop them in that projection. The raw marker stays in `session["history"]`
for the model, so nothing changes for inference; only the display loses a row
that never belonged to the user. This also removes the stored marker from the
payload the desktop reconciles against, killing the ordinal shift that
duplicated the optimistic prompt (#67603) at its source — the desktop-side
marker exclusion remains as a fallback for older backends.

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

---------

Co-authored-by: Dolverin <5910064+Dolverin@users.noreply.github.com>
Co-authored-by: oliviaaaa7788 <274182427+oliviaaaa7788@users.noreply.github.com>
Co-authored-by: oliviaaaa7788 <oliviaaaa7788@users.noreply.github.com>
Co-authored-by: Dolverin <Dolverin@users.noreply.github.com>
2026-07-23 00:05:25 -05:00
Gille
8e01309917
fix(tui_gateway): preserve websocket batch order (#69684)
* fix: serialize TUI gateway websocket sends

* fix(tui_gateway): preserve websocket batch order

* refactor(tui_gateway): drop unused _safe_send wrapper

The batch-serialization fix routes every send through _safe_send_many;
_safe_send became a dead single-line wrapper with no callers. Remove it.

---------

Co-authored-by: supplefrog <78985073+supplefrog@users.noreply.github.com>
Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
2026-07-23 05:02:45 +00:00
Teknium
b0358cf3c8 fix(slack): unify DM-resolution caches and bound wave-2 caches (C16 policy)
Post-rebase consolidation over the merged C7/C16/C10 work:
- _ensure_dm_conversation now records workspace ownership via
  _remember_channel_team and bounds _dm_conversation_cache (cap 5000)
- module-level _slack_dm_cache (C7 standalone path) bounded oldest-first
- _user_is_bot_cache (C10) bounded with _trim_oldest_dict_entries
- caption-mode contract tests updated for the C7+C8 merged media path
2026-07-22 21:58:13 -07:00
Teknium
994a405f2d fix(slack): resolve bare user targets to DMs across all adapter send paths
Widen the #19237 send_message fix to the live adapter: send, _upload_file,
send_multiple_images, send_image, send_video, send_document,
send_exec_approval, send_slash_confirm, and send_clarify now route bare
Slack user IDs (U.../W...) through a shared _ensure_dm_conversation helper
before calling chat.postMessage / files_upload_v2, which reject user IDs.

This closes the gap in #17261 where an attachment worked when replying in
a thread but failed when directed at a user DM, and extends the DM-open
fallback to clarify/approval Block Kit prompts so gated actions can reach
a user directly.

Resolution uses the workspace-scoped client (multi-workspace installs open
the DM with the right bot token), caches per (team, user), and records the
opened D... channel in the channel→team map. On failure the original
target passes through so the downstream API call surfaces the real Slack
error.

Fixes #17261
Refs #19236
2026-07-22 21:58:13 -07:00
Teknium
4ab4894f44 fix(gateway): make post-stream media delivery explicit-only
The post-stream helper (_deliver_media_from_response) rescanned the
already-streamed response and promoted bare local filesystem paths into
real uploads via extract_local_files. Since the visible reply was already
streamed verbatim, any bare path there is either text the user has seen
or stale inspected/tool content — not an attachment request. On Slack this
uploaded images from stale inspected content after otherwise clean replies.

Post-stream delivery now honors only explicit MEDIA: directives. The
non-streaming path in gateway/platforms/base.py keeps its bare-path
auto-detect, because that path controls the visible text and strips the
path from the reply when it attaches — auto-attach is intentional there.

Regression tests: bare image/document paths in a streamed reply produce
no upload; explicit MEDIA: tags still deliver.

Fixes #20834
2026-07-22 21:58:13 -07:00
dsad
c83a196402 fix(slack): gate message files before metadata fetch 2026-07-22 21:58:13 -07:00
dsad
f54e8706f7 fix(platforms): block image upload redirects to private URLs 2026-07-22 21:58:13 -07:00