Commit graph

18060 commits

Author SHA1 Message Date
Brooklyn Nicholson
19d84d1071 test(desktop): pin the sidebar spinner's liveness contract
Investigating the missing-spinner report turned up no defect in the seeding
path: the active_list poll already lights a row for a turn the renderer never
saw start, holds it across polls, and follows a recycled runtime id onto its
new stored session. Pin all three so the reap change can't silently regress
turn-start while fixing turn-end.

Two boundaries worth naming rather than rediscovering:

- `starting` is deliberately NOT working. It means agent_build_started without
  agent_ready, and _start_agent_build runs on any incidental RPC that needs the
  agent — not just a prompt — so treating it as a turn would spin the row on
  merely opening a session.
- $workingSessionIds is keyed by STORED id and drops entries whose
  storedSessionId is null, while message.start flips busy without carrying one.
  A runtime that was never seeded with a stored id therefore goes busy
  invisibly. That is the remaining path by which a spinner can go missing.
2026-07-26 19:02:46 -05:00
Brooklyn Nicholson
6b273f419a fix(desktop): keep thinking traces and tool calls across a mid-turn switch
preserveReasoningParts was gated on exact text equality with the cached row.
Mid-turn the authoritative text has advanced by a delta or two, so the guard
fails and the row is rebuilt from the gateway's inflight projection — which
is text-only. The renderer's cache is the sole carrier of a running turn's
structure, so switching away and back stripped the reasoning and every tool
call, leaving the turn looking inert.

Carry tool calls alongside reasoning, dedupe them on toolCallId, and match on
same-turn (identical text, or authoritative text extending the cached text)
rather than strict equality. Attachment refs and image re-appending stay on
the strict path: those reconcile a settled row, and a growing row is by
definition not settled.
2026-07-26 18:39:11 -05:00
Brooklyn Nicholson
42a30d13db fix(desktop): settle sessions that vanish from the live snapshot
session.active_list is authoritative about absence, but the renderer only
read the rows it returned. A turn that ends while the websocket is degraded
— a remote gateway on a flaky link, a reconnect, a profile swap — drops out
of the gateway's _sessions without Desktop ever seeing the running=false
edge, so the row spins forever and the busy->idle transition that paints the
green unread dot never fires.

Track live runtime ids per gateway profile and settle anything that
disappears between polls through publishSessionState so the real transition
fires. Profile scoping is load-bearing: background profiles are served by
other gateways and never appear in this profile's snapshot, so an unscoped
reap would dark out every other profile's running rows.
2026-07-26 18:39:03 -05:00
brooklyn!
48bdde1deb
Merge pull request #72230 from NousResearch/bb/bootstrap-marker-triage
fix: make the bootstrap-complete marker consistent across every install path
2026-07-26 17:48:57 -05:00
teknium1
6d1e08b2bc fix(dashboard): QA pass — log colors, nameless channels, config bool, UX gaps
Companion fixes from a full dashboard QA pass (every page dogfooded
live), on top of the cherry-picked #31863 header-slot fix:

- ChatPage: harden the header-slot effect further — useLayoutEffect and
  never write the slot while inactive, so the handoff commentary and
  ownership rule live next to the code.
- LogsPage: level classification used raw substring matching, so INFO
  lines carrying 'parse_errors=0' (or paths like errors.log) rendered
  red. New unit-tested classifier (web/src/lib/log-classify.ts) anchors
  on the hermes_logging level token with a word-boundary fallback.
- Channels API: plugin platforms (irc, ntfy, photon, teams, …) rendered
  as nameless title-cased cards ('Irc', 'Ntfy') with empty descriptions.
  Two root causes: (1) plugin discovery never ran in the dashboard
  server process, so plugin_entries() was empty; (2) Platform enum
  pseudo-members claimed plugin ids before the registry could attach
  labels. The catalog now discovers plugins explicitly and resolves
  plugin metadata first; added descriptions + docs links for bundled
  plugin platforms and the msgraph_webhook / whatsapp_cloud / relay
  enum members. Regression test sabotage-verified against the old
  enum-first ordering.
- Config schema: updates.refresh_cua_driver declared type 'bool'
  (schema vocabulary is 'boolean'), so the switch rendered as a text
  input holding 'true'.
- Page titles: '/mcp' rendered as 'Mcp' via the naive capitalize
  fallback; literal-label table now covers MCP/Files/Channels/Webhooks/
  Pairing/System (unit-tested).
- AuthWidget: skip the guaranteed-401 /api/auth/me probe in loopback
  mode — every dashboard load logged a console error for nothing.
- Model picker: with no filter, providers that actually have models
  float above the wall of '0 models' rows.
- Cron: empty state now carries an actionable Create button.
2026-07-26 15:17:30 -07:00
02356abc
8c3c52b008 fix(dashboard): stop ChatPage from clearing all pages' header action buttons
When embedded chat is enabled, ChatPage renders persistently outside
<Routes> but is initially hidden during the plugin-loading window
(~2-4s). Once plugins finish loading, ChatPage mounts for the first time.

Its header-slot effect had early-return branches for !isActive and !narrow
that actively called setEnd(null). Because the user was on /cron, /models,
/sessions, or any non-chat page, this wiped the action buttons that the
current page had already placed in the header.

Affected pages include:
- Cron page — CREATE button disappears
- Models page — 7D/30D/90D filter buttons disappear
- Sessions page — search box disappears

The fix collapses the two early-return branches into one and removes the
setEnd(null) calls. Now ChatPage only sets end when it actually owns the
slot (isActive && narrow), and lets the normal cleanup handle unmounting.

PageHeaderProvider already clears all slots on pathname change via
useLayoutEffect, so ChatPage's active clearing was redundant and harmful.

Fixes #31862

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-26 15:17:30 -07: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
Brooklyn Nicholson
53707e405c fix(desktop): stop repair from deleting a healthy install's marker
Repair signalled "reinstall me" by deleting the bootstrap marker. That was
already destructive -- repair is reachable from a transient backend error on a
fully working install -- and it stranded users in first-run setup with no way
back short of hand-writing the marker file.

Carry the intent in an explicit flag instead. Repair forces the next resolve
through the installer and clears itself once the reinstall starts, so a forced
reinstall still works without destroying provenance about how the install was
created.

Closes #72166
2026-07-26 16:18:01 -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
Brooklyn Nicholson
048e9b2150 fix(desktop): launch a usable active runtime without a bootstrap marker
Marker presence was the launch gate, but the marker is provenance about who
ran the install -- not proof the runtime works. A CLI-installed repo+venv, or
a healthy install whose marker a repair deleted, both read as "never
installed" and dropped the user into first-run bootstrap on every launch.

Split the two questions: classifyActiveRuntime() reports marker validity and
runtime usability separately, and the resolver launches whenever the runtime
is usable, logging when it proceeds without a marker. An unusable runtime
still falls through to bootstrap even with a valid marker, so an interrupted
install can't spawn a dead backend.

Drops isBootstrapComplete(), which had no callers left once the gate moved.

Co-authored-by: iveywest <iveywest@users.noreply.github.com>
Co-authored-by: lihengming <lihengming@users.noreply.github.com>
2026-07-26 16:12:14 -05: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
2b0b5e4c53 fix(install): stamp the bootstrap-complete marker from install.sh too
install.ps1 wrote the marker on Windows and the Rust installer now writes it,
but install.sh -- the path every Mac and Linux CLI install takes -- never did.
A machine set up with install.sh therefore looked uninstalled to the desktop
app, which re-ran first-run bootstrap on every launch.

Stamp the same schema-v1 payload install.ps1 writes, from both the staged
`complete` stage and monolithic main(). An unresolvable HEAD skips the marker
rather than writing one the desktop validator rejects: absent reads as a clean
"bootstrap needed", malformed reads as a confusing half-state.
2026-07-26 16:06:50 -05: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 Nicholson
ea3be4191e fix(installer): stamp the bootstrap-complete marker from the Rust installer
The macOS launcher fast path gates on hermes_is_installed(), which needs
.hermes-bootstrap-complete next to a built desktop app. Nothing in the Rust
bootstrap pipeline ever wrote that marker -- only install.ps1 did -- so every
reopen of /Applications/Hermes.app re-ran setup instead of launching.

Publish the marker atomically (temp sibling + fsync + rename) because
hermes_is_installed() only checks existence: a torn direct write would arm
the fast path against a half-installed tree. A marker write failure emits
BootstrapEvent::Failed so the installer UI leaves the progress state.

Co-authored-by: giggling-ginger <giggling-ginger@users.noreply.github.com>
2026-07-26 16:02:13 -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