Commit graph

17080 commits

Author SHA1 Message Date
luyifan
f8aef2e4e0 Bound Slack response_url error reads 2026-07-22 20:55:51 -07:00
Soynchux
f36c748c85 fix(slack): stop consuming slash reply context outside slash sends
_pop_slash_context fell back to a channel-only scan when the
_slash_user_id ContextVar was unset (i.e. send() invoked from a
non-slash code path such as a cron delivery or a normal channel reply).
That scan could steal another user's pending slash reply context: the
normal message got swallowed into an ephemeral response_url POST that
replaces the invoker's ack, and the slash invoker's actual reply then
posted publicly. Remove the fallback — when the ContextVar is unset,
match nothing.

Surgical reapply of PR #26788 (originally against gateway/platforms/slack.py).
2026-07-22 20:55:51 -07:00
drafish
02f5ced766 fix(slack): pass event-derived chat_type to _has_active_session_for_thread
_has_active_session_for_thread() hardcoded chat_type='group', causing
session key mismatch for DM and MPIM threads. DM sessions key as
agent:main:slack:dm:{chat_id}:{thread_ts} but the lookup built
agent:main:slack:group:{chat_id}:{user_id}:{thread_ts}.

Impact: _has_active_session_for_thread always returned False for DM
threads, causing thread context to be prepended on every message. The
prepended context broke slash command detection (get_command() checks
text.startswith('/')), so /cmd and !cmd never worked in DM threads.

Fix: accept event-derived chat_type parameter instead of hardcoding
'group'. Both call sites pass chat_type='dm' if is_dm else 'group',
where is_dm is already computed from channel_type in {'im', 'mpim'}.

This correctly handles:
- IM channels (D-prefix): chat_type='dm'
- MPIM channels (G-prefix): chat_type='dm' (was missed by D-prefix heuristic)
- Channel messages (C-prefix): chat_type='group' (unchanged)

Added regression tests covering DM thread lookup, MPIM thread lookup,
and negative cases verifying the old hardcoded 'group' behavior fails.
2026-07-22 20:55:51 -07:00
Greg Duraj
a83ec95c76 fix(slack): avoid rich-text duplication in commands + keep slash thread identity
Slack rich_text blocks mirror the original message text. When bang
commands are rewritten from !model to /model, appending block text makes
the command arguments include a duplicate payload, so the model switcher
sees spaces in the model name and rejects valid commands like:

  !model qwen3.7-plus --provider opencode-go

Skip block extraction for command messages while preserving it for
normal messages. Also preserve Slack thread_ts (top-level or nested in
message/container payload shapes) on native slash-command payloads so
session-scoped commands like /model apply to the intended thread instead
of a channel+user key the next threaded message never matches.

Surgical reapply of PR #43533 (originally against gateway/platforms/slack.py,
now plugins/platforms/slack/adapter.py). Thread-shape widening credit also
to #66310.
2026-07-22 20:55:51 -07:00
cypres0099
5f3f1948b7 fix(slack): dispatch mentioned bang commands in threads 2026-07-22 20:55:51 -07:00
nu
ef8936d599 fix(slack): handle leading-space text commands 2026-07-22 20:55:51 -07:00
Teknium
ad8c06047d test: cover api_key_hint in strict pool doubles + real-pool routing regression
Follow-up to the #43755 salvage:
- Update the strict _Pool doubles in tests/run_agent/test_run_agent.py to
  accept api_key_hint and assert it carries the agent's failed key.
- Add a real-CredentialPool regression (no mocks) proving the hint routes
  exhaustion to the entry whose key actually failed, not pool.current(),
  plus the no-hint baseline (#43747 wrong-entry marking).
2026-07-22 20:54:29 -07:00
liuhao1024
702f5f10ad fix(agent): pass api_key_hint to mark_exhausted_and_rotate in credential pool recovery
recover_with_credential_pool() called mark_exhausted_and_rotate() without
api_key_hint, causing it to fall back to current() or _select_unlocked().
When a prior rotation left current() as None, _select_unlocked() returned
the NEXT (healthy) entry instead of the one that actually failed — marking
the wrong credential as exhausted (#43747).

Extract the current API key from agent.api_key (or pool.current().runtime_api_key
as fallback) and pass it as api_key_hint to all 4 call sites.
2026-07-22 20:54:29 -07:00
ethernet
3f9944bad9
fix(ci): run trusted Docker publish directly (#69803) 2026-07-22 23:36:45 -04:00
hermes-seaeye[bot]
7d96e602a9
fmt(js): npm run fix on merge (#69805)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-23 03:32:32 +00:00
ethernet
9f5e568812
fix(desktop): prevent stale worktree status (#69781)
Clear the coding rail while a session's workspace changes and discard late
Git status results for the previous cwd. This prevents Ctrl+Shift+B from
opening a worktree dialog against an old branch.
2026-07-22 23:24:17 -04:00
ethernet
4baf2ed8ad
fix(ci): authenticate gh-image installation (#69793)
Use the trusted publisher's scoped GitHub token when resolving the pinned
gh-image release. This avoids GitHub-hosted runners exhausting their shared
anonymous API rate limit before the evidence publisher can run.
2026-07-23 03:23:28 +00:00
ethernet
dbf4f69b72
fix(desktop): queue prompts during context compaction (#69783) 2026-07-22 23:19:22 -04:00
brooklyn!
5c4d358a7e
Merge pull request #69750 from NousResearch/bb/desktop-fork-new-tab
fix(desktop): open branched chat in a new tab and switch to it
2026-07-22 22:17:17 -05:00
brooklyn!
6d76f5ca51
Merge pull request #69769 from NousResearch/bb/salvage-69720-clarify-late
feat(desktop): skipped clarify keeps its choices visible and answerable
2026-07-22 22:12:53 -05:00
Brooklyn Nicholson
f91d5b2d13 chore(desktop): dedupe session-color precedence, tighten tileStoredRow
- Extract resolveSessionColor(): the sessionColorFor fallback no longer
  re-implements the override -> project-color precedence that the
  $sessionColorById computed already owns.
- tileStoredRow: collapse the nested project-tree walk into a flatMap +
  find, matching the local style.
2026-07-22 22:11:49 -05:00
Brooklyn Nicholson
e80e036086 fix(desktop): use <Tip> instead of native title= on clarify skip buttons
The no-native-title guard (added on main after this PR's first CI run)
bans native title= on <button>. Wrap the shared ChoiceButton in the
themed <Tip>, which renders the child untouched when the label is falsy
so the live card is unaffected.
2026-07-22 21:56:01 -05:00
Ben Barclay
305a3c7424
fix(relay): restore streaming delivery, Slack command parity, and status clearing (salvage of #69716) (#69747)
* fix(gateway): restore relay streaming delivery

* fix(relay): route Slack parent commands before session gates

* fix(relay): clear Slack typing status after turns

---------

Co-authored-by: Victor Kyriazakos <victor@rocketfueldev.com>
2026-07-23 12:51:13 +10:00
Brooklyn Nicholson
30f6fc81e2 fix(desktop): keep ever-active tab panes mounted so revisiting a tab doesn't layout-shift
A tab group rendered only the active pane's content, so every tab switch
unmounted and remounted the whole surface — revisiting a session tab
re-measured and re-scrolled the thread from scratch, visibly shifting
layout each time.

Panes that have been active in a zone now stay mounted in absolutely
positioned layers; the inactive ones hide via visibility (keeping their
layout box, so scroll positions and measurements survive) with
pointer-events disabled and aria-hidden. Mounting stays lazy — a pane
first mounts when first activated — so a boot-restored tab stack still
doesn't resume every session up front, and panes that leave the zone
(closed / moved) unmount as before.
2026-07-22 21:49:12 -05:00
Brooklyn Nicholson
bf21ba08e3 fix(desktop): resolve tab title and project color for sessions outside the recents page
Opening a session in a new tab left the tab titled 'Session' with no
project-color dot until new activity landed the row in the paginated
recents list. Two gaps:

1. tileTitle/tileAccent/tileDragPayload only looked the stored row up in
   $sessions (the recents page). Sessions opened from a project group are
   often older than that page, so the lookup missed entirely. Resolve
   through the project tree as a fallback (tileStoredRow) and re-sync pane
   titles/accents when $projectTree loads.

2. Even with the row present, liveSessionProjectId returned null for a
   session whose cwd sits outside its recorded git_repo_root (mid-session
   relocation / sibling worktree), because the cwd-under-root guard ran
   before the explicit-project folder match. The backend tree groups such
   rows under the project; the client now agrees — an explicit folder
   match is authoritative, only the auto-project (repo root) fallback
   still needs cwd-under-root confidence.

sessionColorFor also computes directly (overrides -> project color) when
the row isn't in the $sessionColorById map, which is keyed over $sessions
only.
2026-07-22 21:46:15 -05:00
ethernet
d63a1c4ccb
fix(desktop): separate workspace defaults from live cwd (#69765) 2026-07-23 02:44:26 +00:00
ethernet
b162371f50
fix(ci): retain delayed and composite-action jobs in review (#69768)
Keep the live PR review comment polling for a short grace period after
visible jobs complete. Preserve composite-action jobs in timing and live
status collection, and split the timing HTML from its linked review-status
artifact.
2026-07-22 22:43:21 -04:00
Brooklyn Nicholson
efa002dd5d refactor(desktop): share the clarify choice row and fix skip copy
Builds on the skipped-clarify card so it holds up beyond the timeout case:

- Extract a shared `ChoiceButton` (letter badge + label + row chrome) used
  by both the live pending card and the settled skip card. The two blocks
  had drifted into duplicated markup; now they can't diverge.
- Fix the hint copy. An empty `user_response` is emitted for BOTH a
  server-side timeout AND a manual Skip (tools/clarify_tool.py) — there is
  no field on the result that tells them apart — so asserting "This question
  timed out" was wrong half the time. Neutral wording ("This prompt is no
  longer waiting…") is correct for either, and the recover-your-answer path
  now also helps someone who mis-clicked Skip. Updated en/ja/zh/zh-hant.

No behavior change to the live prompt or the follow-up-message flow.

Co-authored-by: SHL0MS <SHL0MS@users.noreply.github.com>
2026-07-22 21:39:27 -05:00
SHL0MS
8b96fc57ed feat(desktop): skipped clarify keeps its choices visible and answerable
When a clarify prompt times out, the settled card collapsed to just
'Skipped' — the options were unrecoverable (the args carry them, the
renderer dropped them) and there was no way to answer late.

The skipped card now:
- renders the original choices, letter-badged like the live card
- clicking one drafts a quoted follow-up ('Re: "<question>" — my
  answer: <choice>') into the composer via the insert bus. Enter sends
  it; if the agent is mid-turn it queues like any other prompt.
- a hint line explains the question timed out and what picking does

No retroactive resolution of the expired request: the tool already
returned empty and the turn moved on — injecting into past context
would break prompt-cache and role-alternation invariants, and
clarify.respond on an expired id hard-errors (#56558). The follow-up
message path needs no backend change and works against old backends.

Answered clarifies and free-text (no-choice) skips are unchanged.

Interim UX for #44845 (durable ID-addressable clarify decisions).
2026-07-22 21:38:32 -05:00
Brooklyn Nicholson
35cdc63ca0 fix(desktop): use repo-forked codicon (git-fork has no glyph)
The bundled @vscode/codicons font has no `git-fork` glyph (only
`git-fork-private`), so the Branch menu item rendered blank. Use
`repo-forked`, the actual fork icon, to match the inline GitFork action.
2026-07-22 21:32:23 -05:00
Brooklyn Nicholson
a0a24ba215 fix(desktop): use fork icon for branch in session context menu
The row/tab context-menu 'Branch from here' item used the git-branch
codicon while the inline message action uses a GitFork icon. Switch the
menu item to the git-fork codicon so branching looks consistent across
surfaces.
2026-07-22 21:27:56 -05:00
ethernet
957ea640de
fix(ci): publish inline E2E evidence (#69699)
* fix(ci): publish inline E2E evidence

Upload bounded screenshot evidence from E2E, then publish validated images
from a trusted workflow_run job to commit-pinned branches in the evidence repo.

Wait briefly for the live CI review comment marker before publishing, so
GitHub's read-after-write delay cannot leave an orphaned evidence branch.

* fix(ci): isolate privileged credentials from PR jobs

Keep App private keys and Docker Hub credentials out of PR-controlled
workflows. Use protected environments for trusted publishing and a public
repository variable for the App client ID.

* fix(ci): attach E2E evidence with restricted bot session

Replace the App-backed evidence repository publisher with gh-image uploads
from a dedicated bot session in the gh-image environment.

* fix(ci): publish validated E2E evidence from forks

Let the trusted default-branch publisher handle bounded, validated evidence
artifacts from fork PR CI without checking out or executing fork code.
2026-07-23 02:15:23 +00:00
Brooklyn Nicholson
6326b30c93 fix(desktop): open branched chat in a new tab and switch to it
Branching/forking a chat replaced the primary chat (setActiveSessionId +
setSelectedStoredSessionId + navigate). Instead, open the branch as its own
session tile tab in the center zone and reveal it, leaving the parent chat
exactly where it is — mirroring openNewSessionTile. All branch entry points
(message GitFork button, /branch and /fork, sidebar Branch, tile Branch) go
through forkBranch, so this covers every surface.
2026-07-22 21:13:06 -05:00
ethernet
8a21df18ac
fix(desktop): place steer messages before redirected replies (#69739)
* test(desktop): reproduce steer transcript placement

* fix(desktop): place steer messages before redirected replies
2026-07-22 22:04:57 -04:00
brooklyn!
2c1d585002
Merge pull request #69655 from NousResearch/bb/out-of-credits-ux
feat(billing): consistent out-of-credits UX across CLI, TUI, and desktop
2026-07-22 21:03:38 -05:00
brooklyn!
da3c506db5
Merge pull request #69691 from NousResearch/bb/desktop-billing-polish
Desktop billing polish + shared Progress primitive + settings skeletons
2026-07-22 21:03:34 -05:00
ethernet
6d17b2a593
fix(desktop): preserve active correction on warm resume (#69725)
Update the gateway's live turn projection after an accepted correction so a warm session resume does not add the stale original prompt beside the persisted correction.

Cover the inference-time correction path end to end and assert both redirect entry points refresh the live user text.
2026-07-22 21:53:56 -04:00
ethernet
2a2474512b
test(desktop): cover queued prompt turn boundary (#69729) 2026-07-23 01:36:31 +00:00
Ben Barclay
e5b83633ec
feat(relay): egress typing indicators through the connector (op="typing") (#69721)
The base class spawns _keep_typing for every adapter — a 2s refresh loop
that runs for the whole turn, including while the stream consumer sends
the response — but RelayAdapter inherited BasePlatformAdapter's no-op
send_typing. The loop ran every turn and emitted nothing, so relay-fronted
chats (hosted Discord/Telegram/Signal/Slack) never showed 'is typing…'.

The rest of the pipe already existed on both sides: OutboundOp "typing"
is in the wire contract (contract_version 1), and every connector-side
sender implements it (Discord POST /channels/{id}/typing, Telegram
sendChatAction, Signal sendTyping, Slack assistant status).

This bridges the tick onto the existing outbound frame, mirroring send():
- _with_scope re-attaches the tenant discriminator (metadata.scope_id, or
  user_id for DMs) — the connector's routedEgressGuard wraps ALL ops, so
  an undiscriminated typing frame is declined like a bare send would be.
- the Phase 1.5 per-frame platform tag routes typing through the platform
  the chat lives on for multi-platform gateways.

Best-effort by design: transport errors are swallowed (typing is cosmetic;
the next tick retries), no transport is a silent no-op, and stop_typing
stays the base no-op (platform indicators self-expire). Additive within
contract_version 1 — an older connector returns an unsupported-op result
we ignore. Contract doc §4 updated (typing now carries metadata?).
2026-07-23 11:21:03 +10:00
ethernet
a23e39fe6d
test(desktop): cover correction resume without duplicate prompts (#69708)
* test(desktop): cover correction resume without duplicate prompts

Exercise a live composer correction, switch away and back before the
response settles, and assert both user turns retain their order and occur
exactly once.

* test(desktop): cover correction warm resume during tool run

Exercise a correction accepted at a foreground-tool boundary, a switch through a persisted session, and the warm resume back. Assert the original prompt and correction remain singular and ordered.
2026-07-23 01:06:30 +00:00
ethernet
deadb43cc2
fix(desktop): show composer action shortcuts (#69707)
Expose the existing platform-aware keybind hints on Send, Steer, and Queue
composer control tooltips. Add focused tooltip coverage for each action.
2026-07-22 21:00:50 -04:00
Brooklyn Nicholson
43787dab14 feat(desktop/settings): DOM-shaped skeleton loaders on every settings page
Model settings was the only page that kept its shape while loading; the rest
flashed a centered spinner (LoadingState) or empty placeholders. Standardize on
skeletons that mirror the settings rhythm.

- Add shared SectionHeadingSkeleton / ListRowSkeleton / SettingsSkeleton to
  settings/primitives.tsx (mirror SectionHeading + ListRow).
- Convert keys, providers, sessions, gateway, custom-endpoints, and config
  (non-model) from LoadingState to SettingsSkeleton; remove now-dead LoadingState.
- billing: BillingSkeleton (summary cards + sections) on first load instead of
  "—" placeholder cards.
- pet: skeleton grid on first load instead of a premature "unreachable" message.
2026-07-22 19:22:37 -05:00
Teknium
fae3ba2c44 test(gateway): pin routine-suppression + visible-carve-out contracts
- Iterate ROUTINE_COMPRESSION_STATUS_SAMPLES (formatted from the source
  constants the emit sites use) through _prepare_gateway_status_message on
  every chat platform — emission-wording drift now fails the suite without
  re-copying literals.
- Extend the pinned NOISY_STATUS_MESSAGES with the buffered retry chatter
  and post-#69332 wordings.
- New VISIBLE_COMPRESSION_MESSAGES negative suite: manual /compress
  headlines and abort/failure notices must never be swallowed by the
  widened regex.
2026-07-22 17:14:36 -07:00
Teknium
9981242f88 fix(gateway): widen compression noise filter to all routine status lines
Post-sweep audit of every compression status emission (conversation_loop,
turn_context, conversation_compression): the buffered overflow/attempt-cap
retry chatter (🗜️ 'Context too large…', 'Compressed X → Y, retrying…',
'Context reduced to…'), the #69332-reworded auto-lower notice
("Auto-lowered this session's threshold…"), the aux-provider-unavailable
notice, and the concurrent-compression skip all leaked past
_TELEGRAM_NOISY_STATUS_RE on chat platforms. Add anchored alternatives for
each; the ', retrying'/'— compressing' anchors keep manual /compress
feedback ('Compressed: 30 → 12 messages') and failure/abort notices
visible per the deliberate carve-outs.

Also extract every routine compression status string into importable
template constants in agent/conversation_compression.py (single source of
truth shared by all emission sites), so tests can iterate the actual
emitted wording instead of hand-copied literals.
2026-07-22 17:14:36 -07:00
Teknium
a2068c668b chore: map matt-strawbridge contributor email for attribution CI 2026-07-22 17:14:36 -07:00
matt-strawbridge
ccb5cb34a5 fix(gateway): suppress pre-API compression chatter 2026-07-22 17:14:36 -07:00
ethernet
3651627d88
fix(desktop): skip bootstrap for explicit Nix backend (#69688)
Treat HERMES_DESKTOP_HERMES as an authoritative deployment override.
This keeps the Nix desktop package on its matching immutable Hermes CLI
instead of falling through to install.sh when a best-effort version probe
fails or times out.
2026-07-22 20:00:48 -04:00
Teknium
c35fe293a1 test(compress): pin manual-compress-allowed-when-auto-disabled on every surface
Surface audit for #64438 consistency: cli.py and acp_adapter had their
gates removed by the salvaged #63630 commit (each with a behavioral
test). The remaining manual-compress surfaces never gated on
compression.enabled — pin that contract so a gate can't regress in:

- gateway/slash_commands.py _handle_compress_command (new behavioral
  test: compression_enabled=False still compresses, force=True).
- tui_gateway/server.py — all three manual routes (session.compress
  RPC, command.dispatch compress branch, slash.exec mirror) plus the
  compute-host slash.compress/session.compress controls converge on
  _compress_session_history; new test pins the helper ignores
  agent.compression_enabled and passes force=True.
2026-07-22 16:59:29 -07:00
Teknium
7580bc66d5 fix(acp): align salvaged /compact wording and test with current /compress command name
Main renamed the ACP slash command from /compact to /compress after #63630
was written; update the salvaged status line, comment, and behavioral test
to dispatch the command that actually exists.
2026-07-22 16:59:29 -07:00
babak
a007ac55c6 fix(compress): allow manual /compress when auto-compaction is disabled [overflow error directs users there]
compression.enabled: false is documented (agent/conversation_loop.py
overflow path) as disabling *automatic* compaction only — the terminal
context-overflow error explicitly tells users to run /compress manually,
and the gateway handler has never gated on the flag. But the classic CLI
(_manual_compress) and the ACP adapter (/compact) refused with
'Compression is disabled', leaving users at a full context with no
manual escape hatch on those surfaces.

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 16:59:29 -07:00
Teknium
e907ecccef test(search): prove discovery scope and bookend bounding work together on compacted sessions
Integration test for the two compaction layers landing this sweep:
#63144's discovery-scope fix (archived rows surface from the current
session) and #69334's bookend bounding (summary exclusion + content
caps). A single compacted session exercises both: the archived FTS hit
must surface while the compaction handoff at the session tail stays out
of bookend_end and long messages stay capped.
2026-07-22 16:59:09 -07:00
MacroAnarchy
98c0d8b291 fix(search): require compacted=1 not just active=0, scope delegation-under-compression
Addresses @teknium1's second review round on #63144:

1. _is_compacted_message now checks both active=0 AND compacted=1.
   Previously checked active=0 alone, which also matched rewind/undo rows
   (active=0, compacted=0) that must stay hidden.

2. Added _is_compression_ended() — checks only the session's own
   end_reason, not the lineage-wide has_compression_hop flag. This prevents
   delegation children living under a compression continuation from leaking
   through the lineage filter.

3. _discover lineage skip now uses is_ended_session (session-level check)
   instead of has_compression (lineage-level flag).

New tests:
- TestRewindExclusion: rewind rows stay hidden alongside compacted rows
- TestCompressionEndedHelper: session-level end_reason checks
- TestLegacyContinuationPlusDelegation: delegate child excluded while
  compression ancestors surface

78 passed (71 + 7 new).
2026-07-22 16:59:09 -07:00
MacroAnarchy
711f1c2f1a fix(search): surface compaction-archived and compression-parent sessions in discovery
After context compaction, pre-compaction content was invisible to
session_search — a memory black hole. The _discover() skip logic
filtered both same-session and same-lineage hits unconditionally,
without distinguishing compression-summarised content (gone from live
context) from delegation children (still visible to the parent agent).

Reworked _resolve_to_parent to return (root_id, has_compression_hop),
checking end_reason='compression' on every hop during the same
db.get_session() traversal — zero extra queries.

_discover() now has three compression-aware paths:
- In-place compaction: FTS hits on active=0 (compacted=1) rows pass
  through even when raw_sid == current_session_id
- Legacy rotation: lineage hits pass through when has_compression_hop
  is true on either side of the chain
- Delegation children: still excluded (no compression edge)

18 new tests covering all three scenarios + unit tests for the helpers.

Addresses Teknium's review feedback on #6256.
Closes #13840, #13841.
2026-07-22 16:59:09 -07:00
Yingliang Zhang
8c745314b9 fix(desktop): synchronize context usage and compaction status 2026-07-22 16:58:58 -07:00
Teknium
1f4eaec88a test(cron): assert no-rotation tip resolution is a finalize no-op
With compression.in_place defaulting True the cron session id never
rotates; get_compression_tip returns the input id. Pin that the
salvaged #67188 fix titles/ends the ORIGINAL cron session in that path
(and for falsy tip returns), i.e. zero behavior change when no
compression rotation happened. Also maps colingreig's contributor
email for attribution CI.
2026-07-22 16:58:47 -07:00