Commit graph

18053 commits

Author SHA1 Message Date
Brooklyn Nicholson
edadfcae96 perf(desktop): stop every zone subscribing to the whole layout tree
TreeGroup called useStore($layoutTree) to build its right-click menu's
move/split directions. That subscribes every zone — and therefore every
mounted pane and its entire transcript — to the whole layout tree. A sash
drag rewrites the tree once per frame, so dragging the sidebar re-rendered
all five tiles' message lists on every pointermove, for a context menu
nobody had open.

The directions are only read when the menu renders, so read the tree there
with .get() instead. Same lazy shape the neighbouring `closable` prop
already uses.

Measured over one 60px sash drag with five busy tiles:

  commits          83 -> 12
  ChatView        150 -> 10   (4465ms -> 353ms)
  AuiProvider    9450 -> 630  (9868ms -> 774ms)
  TreeGroup       180 -> 12
  TreeSplit        90 ->  6

Also fixes an observer effect in the harness: idle-cost recorded render
attribution *during* the timed gesture, and the counter walks the fiber
tree on every commit. That was large enough to hide this 15x reduction
behind an unchanged fps, so timing and attribution are separate passes now
and `record` defaults off.

Adds scripts/diag-drag-churn.mjs — the probe that found this. It reports
the transcript chain (who above the messages re-rendered) plus every atom
that notified, which is what named TreeGroup instead of leaving it to be
guessed at. Notably the atom list came back EMPTY: this was never store
churn, so the render-attribution path was the only thing that could have
found it.
2026-07-26 16:53:20 -05:00
Brooklyn Nicholson
f18d9b2843 fix(desktop): make idle-cost's drag actually drag
The synthetic gesture oscillated +/-3px, which nets to zero displacement
and can clamp to a no-op — so it reported a confident fps number for a
drag that never moved the sash. Sweeps monotonically now, dispatches
pointer events React's synthetic system accepts (isPrimary/button/buttons),
and records dragTarget + dragMoved so a drag that silently did nothing is
visible in the output rather than passing as a measurement.

Verified: dragMoved now reports 60px where it previously reported 0.
2026-07-26 16:36:42 -05:00
Brooklyn Nicholson
4798994dce perf(desktop): mount tooltips lazily, and measure the idle cost
Adds an `idle-cost` scenario for the symptom Brooklyn reported: with a
thread spinning, resizing the sidebar feels slow. It holds N tiles busy,
pushes NO tokens, and measures the renderer's self-inflicted commit rate
plus fps while dragging the splitter and while typing.

It reproduces immediately. Five busy tiles, nothing streaming:

  idle commits   17.7/sec   (should be 0 — nothing is happening)
  drag           1.4 fps    p95 812ms, worst frame 1.9s
  typing         61 fps     (fine — this is specific to resize)

Attributing the drag window showed 105,385 TooltipProvider renders and
~15s of component time across a 60-frame gesture. Cause: `Tip` mounts a
full Radix provider + Tooltip per call site, and there are ~107 of them.
Radix's Tooltip holds real state and Popper subscribes to layout, so an
unrelated interaction re-rendered all of them.

Mounts the machinery lazily instead, on first hover/focus. Tooltip churn
drops ~4x (105k -> 26k) and drag doubles to 3fps.

Note `defaultOpen` on the armed Tooltip is load-bearing: the pointerenter
that armed it has already fired, so Radix never sees it and the tip mounts
silently closed. A test caught exactly that, and now guards it.

3fps is still bad — the remaining cost is the whole transcript
re-rendering per resize frame (MessagePrimitive.Parts 12,600 renders /
10.5s, Block/Ct 24,300 each, all 100% wasted). Separate fix.
2026-07-26 16:36:42 -05:00
teknium1
0054c7edcb chore: add contributor email mapping for xd-Neji (PR #57365) 2026-07-26 14:31:47 -07:00
xd-Neji
7b662c8d72 fix(kanban): inherit notify subs for child tasks
Copy gateway notification subscriptions from parent tasks to child tasks created by create_task(..., parents=...), link_tasks(), and decompose_triage_task().

Inherited subscriptions start at the child's current event cursor, so linking an existing child does not replay pre-link task events.
2026-07-26 14:31:47 -07:00
brooklyn!
79b4a40568
Merge pull request #72220 from NousResearch/bb/tool-row-dedupe
fix(tools): stop the inline tool row stuttering its verb and repeating the command
2026-07-26 16:25:50 -05:00
teknium1
af8d698b41 fix(mcp): propagate profile HERMES_HOME override into shared discovery owner; restore stdio startup + WSTransport tests
Follow-up to @LionGateOS's #72135 salvage:
- Route ensure_mcp_discovery_started through hermes_cli.mcp_startup's
  shared owner instead of a hand-rolled bare thread, keeping the start
  lock, retry-after-zero-connected allowance, and interactive-OAuth
  suppression. The shared owner now captures the caller's context-local
  HERMES_HOME override and re-installs it inside the discovery thread,
  so discovery reads the selected profile's mcp_servers (#67605).
- Restore stdio TUI startup discovery in main() and the
  _mcp_discovery_enabled retry gate in wait_for_mcp_discovery, both
  dropped by the original branch.
- Restore the 3 WSTransport regression tests (send serialization,
  cross-batch ordering, drained-token ordering) deleted by the PR.
- Harden the profile-scoped discovery test against sibling-state leaks.
2026-07-26 14:14:03 -07:00
LionGateOS
a7bb123b4a fix(tui-gateway): scope verification and MCP discovery to active profile 2026-07-26 14:14:03 -07:00
LionGateOS
c6e811e48f fix(tui-gateway): scope MCP discovery to active profile 2026-07-26 14:14:03 -07:00
LionGateOS
830ff5967a fix(tui-gateway): start MCP discovery for websocket sessions 2026-07-26 14:14:03 -07:00
konsisumer
486c5ffc8d fix(cli): live-probe dashboard status and share update cleanup across paths
- hermes dashboard --status now verifies each matched PID is alive AND
  bound to a listening socket before reporting it, so stale PIDs and the
  desktop app's IPC-only 'serve --port 0' backends no longer masquerade
  as running dashboards (#58578).
- The git and Windows ZIP update paths share one
  _finish_dashboard_update_cleanup(), so the ZIP fallback gets the same
  stop/restart reporting.
- _kill_stale_dashboard_processes returns a structured
  {matched, killed, failed, unrecovered} result; the explicit was-stopped
  notice fires only for processes that could NOT be auto-restarted,
  meshing with the auto-respawn from #72192.
2026-07-26 14:11:05 -07:00
Brooklyn Nicholson
ec6719b04e fix(desktop): stop the tool row repeating its command three times
The expanded terminal row printed the same string as the title, as the
`$` transcript, and again as detail. shellCommand preferred the
backend's display preview over the real `command` arg, so the transcript
showed a summary of what ran rather than what ran; and a terminal call
with no output fell through to the generic fallback, which echoes
args.context under a transcript already showing it.
2026-07-26 16:03:49 -05:00
Brooklyn Nicholson
f0031abc39 fix(gateway): send a raw arg preview on tool.start, not a phrased label
_tool_ctx switched to build_tool_label in #55166, so every tool.start
carried an already-phrased string ("Running sleep 70 + 2 commands").
Both clients then apply their own verb on top: the TUI renders
Terminal("Running sleep 70 + 2 commands") and the desktop row reads
"Ran Running sleep 70 + 2 commands". The friendly labels stay where they
belong — the CLI spinner and the gateway progress line, which compose
verb + preview at their own call sites.
2026-07-26 16:03:49 -05:00
brooklyn!
f695ce3461
Merge pull request #72212 from NousResearch/bb/desktop-transcript-renders
fix(desktop): make render-churn measure streaming, not boot churn
2026-07-26 16:00:17 -05:00
Brooklyn Nicholson
f7aee9dc8c fix(desktop): make render-churn measure streaming, not boot churn
Two problems, both found by distrusting the harness's own numbers.

1. The scenario slept a fixed 1s after mounting tabs, then recorded. Boot
   and session hydration are not reliably done by then, so a variable
   amount of unrelated work landed inside the measurement window. Three
   back-to-back runs on identical code spread 2.2x on total_renders and
   3.8x on wasted_renders — wide enough that a single-run before/after
   delta could be mostly noise. Replaced with a quiesce gate that waits
   for commits to hold still before recording, and reports 'quiet:N' or
   'timeout:...' so a contaminated run is visible instead of silent.

2. The counter attributed a context-driven re-render as 'wasted', which
   pointed at memo() as the fix when memo cannot block context at all.
   Adds contextChanged via the fiber's context dependency list, and
   excludes it from wasted.

The gate also turned up a finding worth more than the fix: with five busy
tiles and NO driver running, the renderer still commits ~18x/sec. The
report now names the cascade roots (own state changed, props did not)
rather than leaving them to be guessed at — Streamdown re-renders itself
105 times while idle, which is what drives Block/Ct.
2026-07-26 15:54:18 -05:00
teknium1
920facdc4c fix(update): never blind-reinstall cua-driver during hermes update
'Refreshing cua-driver (Computer Use)...' could hang for minutes on
Windows: when the driver's native check-update verb returned an
indeterminate result (old driver without the verb, offline, GitHub
rate-limited, or the probe timing out), install_cua_driver(upgrade=True)
fell through to the full upstream installer — a silent, output-captured
run with a 660s ceiling, plus install.ps1's 600s concurrency-lock wait
on Windows on top. Every 'hermes update' paid that cost.

Two changes:

- install_cua_driver() grows require_confirmed_update: with it set, an
  indeterminate check keeps the installed version and returns fast,
  printing the force path (hermes computer-use install --upgrade).
  'hermes update' passes it; the explicit --upgrade CLI keeps the old
  fall-through so a force refresh still works when the check can't
  answer.
- cua_driver_update_check() default timeout is now 25s on Windows
  (8s unchanged on POSIX): first-spawn of the exe under Defender /
  SmartScreen routinely exceeds 8s, and a false timeout is exactly the
  indeterminate result that used to trigger the multi-minute reinstall.
2026-07-26 13:47:35 -07:00
teknium1
3dc2decd43 fix(update): explicit utf-8 decoding on systemctl restart calls
Windows-footguns lint: subprocess text=True without encoding= decodes
via locale.getpreferredencoding(). Match the file's house style.
2026-07-26 13:42:47 -07:00
teknium1
bd03960071 test(update): cover supervised-unit restart and manual argv respawn
11 new tests: owning-unit restart + dedupe + failure hint (#68934),
argv capture/respawn + --no-open + failure fallback (#40449),
/proc and ps cmdline capture, --stop never restarts.
All fail without the fix; 26 pre-existing tests unchanged.
2026-07-26 13:42:47 -07:00
Variable85
8e1fb9ea34 fix(update): respawn manually-started dashboard/serve backends after update
Capture each manually-started dashboard/serve process's argv before the
stale-process kill (/proc/<pid>/cmdline on Linux, ps -o command= on macOS),
then respawn it detached after the update — headless (--no-open) with output
to logs/dashboard-restart.log under the active profile's HERMES_HOME.

Supervised PIDs keep their systemd-unit restart; --stop stays a plain stop.

Salvaged from PR #41508 with scope fixes: serve matching preserved, profile-
aware log path, restart only on the update path (restart_managed=True).
2026-07-26 13:42:47 -07:00
webtecnica
d1f376006a fix(update): restart systemd-supervised backends after stale kill 2026-07-26 13:42:47 -07:00
teknium1
2a2ae3bca1 test(kanban): assert DM wake resumes the creator's real DM session key
Strengthen the salvaged regression test to prove the end-to-end claim in
#56580/#68874: a DM-created task's terminal wake must build the creator's
':dm:<chat_id>' session key via build_session_key(), not a group-scoped
key that forks a fresh session. Sabotage-verified: reverting the watcher
to the hardcoded chat_type='group' fails this test.
2026-07-26 13:42:33 -07:00
teknium1
a766f85042 chore: contributor email mappings for kanban wake chat-scope salvage 2026-07-26 13:42:33 -07:00
张满良
c03a06b8d9 fix(kanban): cover remaining add_notify_sub call sites for chat_type (#56580)
Follow-up to the main fix in this PR. rodriguez46p-ui's review on the
equivalent #56632 (closed stale) flagged that only the auto-subscribe
path in tools/kanban_tools.py was covered; the same gap existed in two
more call sites:

- gateway/slash_commands.py: the `/kanban create` slash command auto-
  subscribes the calling session but didn't pass chat_type. Read it
  from source.chat_type (already available on SessionSource).
- hermes_cli/kanban.py: the `kanban notify-subscribe` CLI command now
  accepts --chat-type and threads it through.

The dashboard plugin API (plugins/kanban/dashboard/plugin_api.py) still
has the gap because the home_channel config schema doesn't carry
chat_type — that's a follow-up that needs a config schema change.

Verified: 258 tests pass on the kanban + session_context suites.
2026-07-26 13:42:33 -07:00
lijun
a417c6e08d fix(gateway): preserve kanban notifier chat type 2026-07-26 13:42:33 -07:00
MaxFreedomPollard
aa636c6fca fix(config): merge duplicate kanban block so auto_subscribe_on_create default survives
DEFAULT_CONFIG declared "kanban" twice. Python keeps only the last
literal for a duplicate key, so the first kanban block was silently
dropped and its "auto_subscribe_on_create": True default never made it
into DEFAULT_CONFIG. The consumer in tools/kanban_tools.py masks the
miss with cfg_get(..., default=True), but the documented default was
absent from DEFAULT_CONFIG (so config templates / 'hermes config show'
omit it), and the duplicate key is a standing hazard: any future key
added to the first block would also vanish.

Merge auto_subscribe_on_create into the single canonical kanban block.

Adds a regression test asserting both default sets survive and a guard
against any duplicate top-level DEFAULT_CONFIG key.
2026-07-26 13:42:22 -07:00
teknium1
2365fed985 docs(config): update max_turns example to the new 500 default
Follow-up on top of @waroffchange's alignment fix (#55673): the real
default changed from 90 to 500 in #72176, so bring the example value
and comment up to the current default.
2026-07-26 13:42:16 -07:00
waroffchange
f2e1a71120 docs(config): align max_turns example with actual default (90) 2026-07-26 13:42:16 -07:00
teknium1
13590ce685 fix(gateway): GIS extensions, spaced MEDIA paths, and code-block-safe display strip
Follow-up wave to #72170 resolving the remaining open MEDIA-delivery gaps:

- #24032: add .kmz/.kml/.geojson/.gpx to MEDIA_DELIVERY_EXTS, and recover
  unknown-extension paths containing spaces via _match_extensionless_path —
  validation-gated forward extension across single spaces (bounded at 8
  tokens, stops at newline / next MEDIA: keyword). The regex itself stays
  non-greedy and whitespace-bounded so the #68773 absorption bug class
  cannot return; the on-disk file check is the oracle.

- #16434 (streaming half): _strip_media_tag_directives now uses the same
  mask-as-locator pattern as extract_media, so MEDIA tags inside fenced
  code blocks, inline-code examples, and JSON string values survive in
  streamed display text instead of being mangled. Display and delivery
  now agree on every protected-span rule.

- Updated three stream_consumer display expectations that pinned the old
  inconsistent behavior (backtick/double-quote tags stripped from display
  while delivery never attempted them).
2026-07-26 13:41:53 -07:00
dhruvkej9
83bba799e2 fix(gateway): deduplicate repeated media tags against prior assistant/tool output
The model sometimes echoes a previous MEDIA:path tag or bare file path in a later response. Previously both streaming delivery and non-streaming delivery would re-send those files, causing duplicate documents/images on later turns.

Changes:

- _collect_history_media_paths now also scans assistant messages for MEDIA: tags, not just tool results.

- _deliver_media_from_response accepts pre-computed history_media_paths and filters extracted media/local files.

- BasePlatformAdapter gains _history_media_paths_for_session for non-streaming dedup.
2026-07-26 13:41:53 -07:00
brooklyn!
6fc571afe9
Merge pull request #72201 from NousResearch/bb/composer-url-chips
feat(desktop): chip a link pasted or typed into the composer
2026-07-26 15:33:52 -05:00
Brooklyn Nicholson
adfaa95e3b feat(desktop): chip a link pasted or typed into the composer
A pasted link went in as raw URL text, wrapping across the composer and
staying inert. It now becomes the same `@url:` reference the "+ → Add URL"
dialog inserts — parsed in place, so a link mid-sentence keeps its position
and the punctuation that ended the sentence stays outside the chip. Typing
one and pressing space commits it the same way.

Both rich-editor surfaces get it: the composer and the message-edit box,
whose paste went through `execCommand` and could not produce a chip at all.
2026-07-26 15:27:06 -05:00
Brooklyn Nicholson
456b2f9c7d refactor(desktop): give the rich editor a chip-aware caret insert
`insertPlainTextAtCaret` dropped its text in verbatim, so a caller with
directives in hand had no way to land them as chips. It becomes
`insertComposerContentsAtCaret`, sharing the parse `renderComposerContents`
already uses, and gains a sibling `replaceBeforeCaret` for swapping a
just-typed token for a chip.
2026-07-26 15:23:51 -05:00
Brooklyn Nicholson
ee09162e45 fix(desktop): label a url chip with its host and path
The composer labeled a `@url:` chip with `refLabel`, which takes the last
path segment — three PR links all read as their number. The transcript had
its own better labeler that stopped at the hostname, so the same reference
read differently before and after send.

One `refChipLabel` now serves both: host without `www.`, path riding along
for the chip's existing truncate to cut, and the full value on the chip's
title so a cut-off link is still readable on hover.
2026-07-26 15:23:51 -05:00
Siddharth Balyan
59529afee0
feat(billing): carry the payment-method union through to clients (#71542)
* feat(billing): add payment_method union to the billing-state wire type

* feat(billing): carry the payment-method union through the gateway

NAS now sends a typed `paymentMethod` union on /api/billing/state alongside
the legacy `card` field. The gateway parses payloads field-by-field, so the
new field was dropped on the floor before reaching TUI/Desktop.

Parse it into PaymentMethodInfo and re-emit it as snake_case `payment_method`,
matching the translation the rest of this payload already does. The payment
method id is deliberately not carried through — clients have no use for it.

No client rendering changes: `card` stays populated for cards, so every
existing consumer behaves exactly as before and the new field is inert until
a surface opts into reading it.

* fix(billing): send only the fields each payment-method kind declares

The serializer emitted every key for every kind, so a Link method went out
carrying brand, last4 and wallet set to null. That contradicts the shared
type, where each kind declares its own fields: a client testing `'brand' in
pm` would read every Link method as a card, and one trusting the declared
non-null `brand` could crash on it.

Send each kind's own fields, and forward an unrecognized kind by name alone
so a client that predates it can still say something honest. The shared type
gains the matching fallback arm its own comment already promised.

Tests now follow a payload from the server response through to the client
wire for each kind, rather than checking parsing and serializing separately —
which is why the old expectation locked in the wrong shape without noticing.

* fix(billing): keep the payment-method kind narrowable

Typing the fallback arm's kind as `string & {}` borrowed a trick that only
works on unions of plain strings. On a union of objects it makes the
discriminant non-literal, so TypeScript stops narrowing on every arm — even
`if (pm.kind === 'card')` no longer gives you `brand`. The first client to
use this would have hit a compile error and reached for a cast.

An unrecognised kind now arrives as `unknown`, carrying the real name
alongside it, so every arm has a literal discriminant. A type-level test
pins this: it fails to compile if the discriminant stops narrowing.

The parser settles which kind it is, the way the card parser already does,
so the record cannot hold fields that do not belong to its kind and the
serializer no longer re-checks. The type comment also stops claiming `card`
is a safe signal — it is null for Link, so `!card` does not mean "nothing on
file".
2026-07-27 01:49:43 +05:30
brooklyn!
8943c9958b
Merge pull request #72163 from NousResearch/bb/desktop-render-waste
perf(desktop): stop the statusbar re-rendering per streaming token
2026-07-26 15:18:57 -05:00
teknium1
588b7059a8 test(tui): prove kanban poller reads the shared board under a profile override
The sweeper review on #66435 flagged that the collector doesn't bind
session["profile_home"]. That binding is intentionally unnecessary: the
kanban board is shared across profiles by design — kanban_home() anchors
on get_default_hermes_root(), which resolves the process env and ignores
context-local profile overrides (see the kanban_db.py module docstring).
Add a regression test that claims a subscription while a foreign-profile
set_hermes_home_override() is active, proving delivery still works for
non-launch-profile Desktop sessions.
2026-07-26 12:54:51 -07:00
falkoro
6247712c3f test(tui): drive a real subscription through _notification_poller_loop
Covers the poller wiring above _collect_kanban_notifications, per the
hermes-sweeper review: status.update emission, agent-turn dispatch via
_run_prompt_submit when the session is idle, and the busy-session
pending buffer that flushes once the session goes idle.
2026-07-26 12:54:51 -07:00
falkoro
badb240ffa fix(tui): deliver kanban notify subscriptions to TUI/desktop sessions
kanban_create auto-subscribes TUI/desktop sessions with platform="tui" and
chat_id=HERMES_SESSION_KEY, and tools/kanban_tools.py documents that the
TUI notification poller (tui_gateway/server.py) reads kanban_notify_subs
and posts completion messages into the running session — but that reader
was never implemented. The poller only watched process_registry completion
events, and the gateway notifier skips "tui" rows because no such
messaging adapter exists. Result: subscriptions accumulate with
last_event_id=0 forever and no task event is ever delivered (18 subs,
29 terminal events, 0 deliveries in the report).

Implement the missing delivery path in the TUI notification poller:

- every 5s, claim unseen terminal events for this session's
  platform="tui" subscriptions via claim_unseen_events_for_sub — the
  same atomic cursor-claim the gateway notifier uses, so an event is
  delivered exactly once even with a gateway polling the same board DB
- format events with the same wording as the gateway notifier
  (done/blocked/gave up/crashed/timed out/status; archived and
  unblocked are claimed but silent, so they can't wedge the cursor)
- emit a status.update for user visibility, then chain an agent turn
  when the session is idle — mirroring process-completion handling;
  claimed events buffer in the session until it goes idle since the
  cursor (unlike the process queue) cannot re-queue
- unsubscribe only at a truly final task status (done/archived),
  matching the gateway rule so respawned tasks keep notifying
- multi-board: iterate boards, polling each resolved DB path once

Fixes #59890
2026-07-26 12:54:51 -07:00
teknium1
1b081e4891 feat: raise default tool-calling iteration limit from 90 to 500
The default max_iterations/agent.max_turns budget was set when long
agentic runs were rare; complex tasks now routinely exceed 90 tool
calls. Raise the default to 500 across every surface that hardcodes
the fallback: AIAgent constructor, DEFAULT_CONFIG, CLI resolution
chain, gateway env bridge, cron scheduler, and TUI gateway. Explicit
user config values are unaffected (deep-merge preserves them; no
_config_version bump needed).

Docs (en + zh-Hans), CLI help text, tips, and pinned tests updated
to match.
2026-07-26 12:54:45 -07:00
teknium1
4a9f67acfb chore: map contributor emails for salvage attribution 2026-07-26 12:52:31 -07:00
teknium1
45af1118b5 test(discord): update send_document/send_video expectations to plural files= kwarg
Sibling tests pinned the old singular file= handle form that #66797
replaced with path-based File via files=[...].
2026-07-26 12:52:31 -07:00
teknium1
c82f4636f2 fix(gateway): deliver MEDIA tags with sentence-final punctuation and inline-code wrapping
Two remaining formatting variants that silently killed file delivery:

- A trailing sentence period (MEDIA:/x/data.csv.) failed the boundary
  lookahead, so the tag neither extracted nor stripped. The period is now
  accepted as a boundary only when followed by whitespace/EOL, keeping
  multi-part extensions (.tar.gz) intact.

- A whole tag wrapped in inline code (`MEDIA:/x/data.csv`) was masked as
  a prose example (#35695). Models routinely format paths as inline code,
  eating real deliveries. Inline-code tags now deliver when the path
  validates on disk; non-existent example paths stay masked and fenced
  code blocks remain fully masked.

Adds a regression matrix covering both plus the salvaged contributor
fixes (emphasis wrap, glued tags, glued [[as_document]], dedupe,
unknown-extension and extensionless delivery).
2026-07-26 12:52:31 -07:00
andy
0ec1b9f7fa fix(gateway,tools): add missing .3gp and .webm to video extension sets
MEDIA_DELIVERY_EXTS in gateway/platforms/base.py omitted .3gp, causing
MEDIA: tags with .3gp files to leak as plain text instead of being
extracted for native video delivery. _VIDEO_EXTS in
tools/send_message_tool.py and _MIGRATION_VIDEO_EXTS in the Feishu
adapter omitted .webm, causing .webm files to be classified as
documents instead of video on Telegram and other platforms.

Both extensions are already present in every gateway-side _VIDEO_EXTS
definition (run.py, kanban_watchers.py, weixin.py, base.py local).

Closes #71621, Closes #71603
2026-07-26 12:52:31 -07:00
Piyush Jagadish Bag
384b0a0b5b fix(discord): notify user on attachmentless MEDIA drop
Surface a user-visible delivery notice when non-streaming media dispatch
gets success=False after MEDIA tags were stripped, and validate forum
starter-message attachments the same way as direct channel sends (#66797).
2026-07-26 12:52:31 -07:00
Piyush Jagadish Bag
8eb29a1bb9 fix(discord): deliver MEDIA video attachments instead of silent drop
Outbound MEDIA video/document tags on the Discord non-streaming path
were extracted (stripped from the visible text) but never delivered:
no attachment, no error, and no dispatch log line (#66797). The
open-handle discord.File plus singular file= form could race Discord's
multipart encoder after an earlier image batch on the same channel and
return a successful message carrying zero attachments.

Fix _send_file_attachment (used by send_video/send_document/
send_image_file) to:
- use a path-based discord.File via the plural files=[...] kwarg, the
  same pattern as the working send_multiple_images batch path;
- pre-flight os.path.isfile and return a File not found result instead
  of raising deep inside discord.File;
- fail loud when Discord accepts the message but attaches nothing, so
  the dispatch loop surfaces a warning rather than a silent drop;
- add INFO dispatch logs (base non-image MEDIA fan-out, video send, and
  file attachment) so a MEDIA:.mp4 cannot vanish without a trace.

Tests (tests/gateway/test_discord_send.py):
- path-based files=[...] kwarg used, singular file= unset;
- fail-loud when the returned message has no attachments;
- missing file fails fast without resolving the channel;
- forum-parent delivery routes through create_thread with files=[...];
- end-to-end image+video response routes the mp4 to send_video while
  images still batch.

Fixes #66797
2026-07-26 12:52:31 -07:00
StartupBros
b129a72e0d fix(extract_media): dedupe identical MEDIA tags
When the same file is referenced multiple times in one message (common
when the agent emits MEDIA tags both inline and in a summary footer),
the platform adapter uploads/sends the same file twice — visible as
duplicate attachments in Telegram, duplicate posts in Slack, etc.

Set-based dedup on the expanded path, preserving first-occurrence order.
2026-07-26 12:52:31 -07:00
webtecnica
06ab6816cc fix(gateway): stop MEDIA tag regex from absorbing following tag or text (#68773)
Two MEDIA: path tags emitted back-to-back without a separator merged
into a single invalid path and were silently dropped. The same happened
for extension-less tags.

Root cause: both regexes in gateway/platforms/base.py used greedy
quantifiers in their path class, causing adjacent tags to be absorbed.

Fix: make both regexes non-greedy (add ? to quantifiers) and add
MEDIA: to the trailing lookahead boundary set so the next MEDIA:
keyword stops the current match cleanly.
2026-07-26 12:52:31 -07:00
Julien Talbot
aaddc6c93a fix(gateway): deliver MEDIA: tags wrapped in Markdown emphasis
Models routinely present a file to the user with the delivery tag wrapped in
Markdown emphasis — `**MEDIA:/path.pptx**`, `*MEDIA:/path*`, `_MEDIA:/path_`.
MEDIA_TAG_CLEANUP_RE only tolerated a single leading/trailing quote or backtick
(`[`"']?`), and its closing lookahead set excluded `*` and `_`, so an
emphasis-wrapped tag never matched. The file was then silently never delivered
and the literal `MEDIA:/path` text leaked into the chat instead — the user sees
a path, not the attachment.

Allow a short run of emphasis/quote markers (`[`"'*_]{0,3}`) on both sides of
the tag and add `*`/`_` to the closing lookahead. Code-block, inline-code and
blockquote contexts are still neutralised earlier by `_mask_protected_spans`
(#35695), so documentation/example tags remain non-deliverable; the
absolute-path anchor still rejects relative paths; `_` inside a filename is
unaffected.

Adds regression coverage in TestExtractMedia for bold/italic/underscore
wrapping, mid-prose bold, emphasis-wrapped .html, underscore-in-filename, and
emphasis-wrapped relative-path rejection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 12:52:31 -07:00
liuhao1024
b58b1fa962 fix(gateway): allow [[as_document]] glued directly to MEDIA path (#63632)
MEDIA_TAG_CLEANUP_RE failed to match when a directive like [[as_document]]
was concatenated directly to the file extension without whitespace
(e.g., MEDIA:/home/user/report.xlsx[[as_document]]). This caused the
file to be silently not delivered while the gateway reported success.

Root cause: the lookahead character class [\s`",;:)\}\]|$) did not
include [, so [[as_document]] immediately after the extension broke
the lookahead assertion.

Fix: add \[ to the lookahead class so directives can follow the path
without whitespace. This is safe because [ is already stripped elsewhere
in the same file via .replace("[[as_document]]", "").

Added regression tests covering:
- Directive glued to extension (issue case)
- Directive with whitespace (baseline)
- Tag at end of string ($ anchor)
2026-07-26 12:52:31 -07:00
Brooklyn Nicholson
41dd895e12 perf(desktop): stop the statusbar re-rendering per streaming token
The statusbar subscribed to `$focusedSessionState` — a projection of
`$sessionStates`, which is republished on every message delta — but reads
only three fields off it. Every token therefore re-ran useStatusbarItems
and rebuilt all ~9 item objects, and since StatusbarItemView was not
memoized, one changed item (the running timer) re-rendered the whole bar.

Adds `useStoreSelector` beside the existing `useSessionSlice`, the same
narrowing idea for a scalar instead of a keyed array: subscribe to the
store, but bail out unless the selected value changes. Applies it to the
three fields the statusbar actually reads, and memoizes StatusbarItemView.

Measured over five concurrent streaming tabs (`render-churn`):

  total renders   78,385 -> 21,701   (-72%)
  wasted renders  10,432 ->  6,112   (-41%)
  TooltipContent   2,304 ->    143   (-94%)
  StatusbarItemView 2,174 ->      0
2026-07-26 14:21:34 -05:00