Commit Graph

886 Commits

Author SHA1 Message Date
Maciej Lisiewski
bda3aea496 ci: remove unused variables (linter) 2026-10-10 15:20:14 +02:00
Maciej Lisiewski
0006a5e229 ci: fix PLW1509, PLW1510, PLW2101 linter errors 2026-10-10 15:20:13 +02:00
Mariano Nicolini
4710e757ab fix(image_gen): fall back only when Krea provably created no job, and apply the Krea default on first-run setup too 2026-10-09 15:36:14 -04:00
Mariano Nicolini
a9ebde190a feat(image_gen): fall back to FAL when managed Krea refuses a request before creating a job 2026-10-09 15:36:14 -04:00
Maciej Lisiewski
2ff58849f4 ci: ruff UP037 fixes 2026-10-09 17:34:38 +02:00
Maciej Lisiewski
9eab688b42 ci: ruff UP017 fixes 2026-10-09 17:34:36 +02:00
Maciej Lisiewski
aff64c3eeb ci: UP007, UP022, UP028, UP031, UP041 linter fixes 2026-10-09 17:31:52 +02:00
Teknium
a114e7a344 fix(memory): detect providers past the first 8 KB of __init__.py
The discovery heuristic only searched the first 8 KB of a plugin's
__init__.py for the provider contract, so a valid provider with a long
module docstring was invisible. Fall back to _defines_memory_provider
(AST parse, no import) when the cheap text search misses.
2026-10-09 02:33:35 -07:00
Maciej Lisiewski
4278b6aa24 ci: RUF013, RUF015 2026-10-08 21:29:48 +02:00
Maciej Lisiewski
aa2d120cba ci: prefix unused variables with underscore (RUF059) 2026-10-08 19:22:03 +02:00
Maciej Lisiewski
d76cce4f11 ci: apply all automatic linter fixes available 2026-10-08 17:22:54 +02:00
Teknium
8a33891bdd revert: dashboard-auth token exchange breaks on gzip IdP responses (partial revert of #133938)
Reverts the plugins/dashboard_auth half of ca38caeea0.

_request_limited_response streamed the body via iter_bytes() (already
gzip-decoded by httpx) and rebuilt an httpx.Response with the ORIGINAL
headers, Content-Encoding: gzip included, so .json() tried to gunzip
plaintext. Every Nous Portal dashboard login failed with
503 "Provider unreachable: Portal token endpoint unreachable: Error -3
while decompressing data: incorrect header check". Self-hosted OIDC
discovery/token/revocation used the same helper.

The urllib half (gateway health probe, ElevenLabs voices) is kept:
urllib does not decode Content-Encoding, and c061068b26 builds on its
_read_dashboard_json_response helper.
2026-10-07 17:52:08 -07:00
Austin Pickett
1a85226e22 fix(kanban): dashboard rejects stale archived/deleted board slugs
_resolve_board 404s archived tombstones, and the /events WebSocket closes
stale-tab streams (1008) at the handshake instead of handing the dead slug
to connect(), which resurrected an empty board (#43243).
2026-10-07 17:21:31 -04:00
Austin Pickett
63e5656409 fix(kanban): board identity requires board.json; archive leaves a tombstone
A directory holding only kanban.db (no board.json) is a stub, never a
board: _dir_holds_board/list_boards/board_exists now require the metadata
file, so historical DB-only ghosts stop surfacing as empty active boards
(#43243).

remove_board(archive=True) leaves an archived tombstone board.json at the
original slug so stale readers can distinguish an archived slug from a
never-created one; create_board clears the tombstone (explicit creation
wins), get_current_board falls through archived slugs, and boards switch/rm
explain the tombstone in their output.
2026-10-07 17:21:31 -04:00
kshitijk4poor
cb71296ebd fix(photon): a threaded reply to our bubble stays "own" after a restart
Cloud spectrum targets carry no direction and _sent_message_ids is
in-memory, so after a gateway restart a swipe-reply to one of our
bubbles hydrated the quoted text from the sent-text index but was
marked as not ours: the agent saw [Replying to: "..."] instead of
[Replying to your previous message: "..."]. Only our sends are indexed,
so an index hit is itself proof the target was ours.
2026-10-07 13:18:15 +05:30
David Uche
aba1f72ef2 fix(photon): record sent text at the _post_send choke point
Polls (clarify), the plain-text fallback resend and standalone attachment
sends reached the sidecar without recording what they sent, so a threaded
reply to those bubbles lost its quoted context. Record once in _post_send
(plus the standalone attachment leg, which has its own HTTP client) and
reuse _normalize_chat_key instead of a duplicate regex.
2026-10-07 13:18:15 +05:30
David Uche
348a68bb4e fix(photon): keep the user's words when they reply in an iMessage thread (#100663)
spectrum-ts 12.x delivers a threaded (swipe) reply as
{type: 'reply', content, target}. The sidecar had no branch for it, so
the agent received '[Photon content type not handled: reply]' and the
message text was lost.

- sidecar: normalize 'reply' to its inner content plus the target's id,
  direction and text
- adapter: unwrap it and set reply_to_message_id / reply_to_text /
  reply_to_is_own_message on the MessageEvent
- adapter: record outbound text (send, attachments, standalone send) in
  gateway.rich_sent_store and use it when spectrum can't hydrate the
  quoted text, which is the usual case for our own bubbles. DM GUIDs
  and bare E.164 share one key.
2026-10-07 13:18:15 +05:30
Teknium
05eecbcd97 chore(memory): remove the bundled openviking provider
OpenViking now ships from Volcengine's own repo through the plugin catalog
(plugin-catalog/openviking.yaml, pinned in #131267). Delete the in-tree copy
and its tests; memory_provider_migration already lists openviking in
_LEFT_CORE, so homes configured for it get the catalog plugin installed on
hermes update / first agent start.

The catalog pin starts openviking-server with the full Hermes environment
(the bundled copy filtered it). The docs now say so and point at
volcengine/OpenViking#5553; start openviking-server yourself to avoid it.
2026-10-06 23:13:01 -07:00
kshitijk4poor
650b294151 fix(kanban): make can_move total over task statuses; stats derives from the workflow
- can_move: `archived` is a valid source (restore). The dashboard PATCH accepts
  archived -> triage/todo/ready (measured live), so the manual table gains that
  row and the matrix test now measures it too. A same-column drop returns False
  instead of raising or meaning "allowed".
- `hermes kanban stats` text output iterated its own hardcoded status list,
  which omitted `review`; it now loops over DEFAULT_WORKFLOW.keys().
- Tests: drop three asserts that could not fail (VALID_STATUSES is
  DEFAULT_STATUSES; BOARD_COLUMNS is built from keys()); add one invariant:
  can_move answers for every value tasks.status can hold.
2026-10-07 11:19:10 +05:30
Ben Barclay
6fe5dfb1cf fix(kanban): drop the partial kernel-event model from the workflow groundwork
Review of #130067: the event edges claimed every block lands in 'blocked',
but _route_block routes to todo / triage / blocked, and ~20 kernel status
writes are conditional (crash/reclaim return, failure breaker, parent
reopen, import). Rather than publish a partial contract, Phase 0 keeps only
what is checked now (columns, traits, manual matrix, derived copies); the
event model is designed with the kernel switch in Phase 1.

Also from review:
- can_move rejects unknown column keys instead of defaulting (archive of a
  bogus key returned True).
- the matrix test only accepts 400/409 as a refusal; 5xx or 422 fails.
2026-10-07 11:19:10 +05:30
Ben Barclay
96db175da7 feat(kanban): one workflow definition for columns, traits and transitions
Groundwork for user-defined columns and workflow per board (#54818).

- hermes_cli/kanban_workflow.py: Column/Workflow + DEFAULT_WORKFLOW, today's
  board written down as data (column order, traits, default kernel-event
  edges, manual move allow-list). Not consulted by the kernel yet.
- VALID_STATUSES, dashboard BOARD_COLUMNS, the kanban_list status enum and
  CLI status icons now derive from it. Fixes drift: the tool enum lacked
  scheduled/review; CLI icons lacked triage/review.
- GET /workflow on the dashboard plugin API.
- Tests pin DEFAULT_WORKFLOW to real behavior: every default event edge vs.
  its kernel verb, and all 64 PATCH src->dst moves vs. can_move.
2026-10-07 11:19:10 +05:30
beardthelion
8e85a0fd8c fix(plugins/telegram): clamp negative inline-query offsets to page zero
Telegram echoes back the client-controlled inline_query.offset string on
every inline query. build_inline_results passed it through int() and
used it directly as a slice start, so a negative offset indexed the
catalog from the tail: small catalogs served their last few entries with
negative-prefixed result ids, and larger catalogs could return an empty
page with a misleading next_offset that skipped most of the catalog.

Clamp the parsed offset to >= 0. Malformed offsets still fall back to
page zero via the existing except path, and valid pagination is
unchanged.
2026-10-06 22:52:16 -05:00
KoNit-K
51dfebd82e fix(deepseek): honor effort=none as thinking disabled
Desktop Thinking Off can arrive as {effort: "none"} without enabled=False,
so V4 kept sending thinking:enabled and billed reasoning tokens.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 15:17:55 -05:00
Sergei Kabuldzhanov
fd4dc86146 feat(kanban-dashboard): show profiles on disk as lanes even when idle
`GET /api/plugins/kanban/board` returns an `assignees` list used by the
dashboard for the lane-by-profile sub-grouping in the running column.
Previously this came from a `SELECT DISTINCT assignee FROM tasks WHERE
... status != 'archived'` query, so a freshly-added profile only got
its own lane after being assigned its first non-archived task.

Switch the lookup to `kanban_db.known_assignees()`, the helper that
already backs the `/assignees` picker endpoint at plugin_api.py:1660.
It unions profiles configured on disk with profiles currently holding
non-archived tasks, so a new profile appears in the lane set
immediately. This also keeps the two endpoints from drifting —
previously they could disagree about who's "known."

Updates `test_board_empty` to reflect the new semantics: the implicit
`default` profile that the fixture's `HERMES_HOME` creates now shows
up in `assignees` even on an empty board.

Test plan:
  - `scripts/run_tests.sh tests/plugins/test_kanban_dashboard_plugin.py`
    — 94/94 pass (was 43/44 pre-test-update).
  - Manual: add a new profile via `hermes profile create foo`, then open
    the dashboard kanban; `foo` appears as an empty lane in the running
    column without needing to assign it work first.

Platforms tested: Linux (WSL2). Pure Python, no platform-specific code.
2026-10-06 14:23:10 -04:00
Austin Pickett
ca38caeea0 fix(dashboard+auth): cap HTTP JSON response body reads
Dashboard-side urllib readers (gateway health probe, ElevenLabs voices)
and self-hosted OIDC httpx readers (discovery, token exchange, revocation)
buffered remote response bodies with no size bound — a hostile or
compromised upstream could exhaust memory. Both halves now read with a
1 MiB hard cap: a shared _read_dashboard_json_response for urllib JSON
reads and a streaming _request_limited_response for IdP endpoints that
prechecks Content-Length and caps the chunked body regardless.

Adapted from #54809 and #55122 (credit @ooiuuii) to current main, where
the token exchange lives in plugins/dashboard_auth/_shared.py and the
health probe in hermes_cli/web_server_gateway.py.

Fixes #54808, Fixes #55121
2026-10-06 12:28:55 -04:00
Austin Pickett
c5cb0f597b fix(dashboard-auth): clock-skew leeway for JWT verification; classify not-yet-valid tokens as validation failures
Both dashboard-auth JWT verification paths (self-hosted OIDC ID token and the
Nous Portal access token, via the shared verify_jwt) called PyJWT jwt.decode
with the default leeway=0 while requiring iat. Any host whose clock lags the
issuer's — unsynced containers/VMs, NTP jitter — failed login with
ImmatureSignatureError, and the failure was folded into ProviderError, i.e.
HTTP 503 'provider unreachable', discarding already-rotated refresh tokens
and forcing unrecoverable re-logins.

- verify_jwt gains leeway (default 60s, RFC 7519 §4.1.4-4.1.6) and classifies
  ImmatureSignatureError as InvalidCodeError (validation failure -> 401 /
  refresh) instead of ProviderError/503.
- New parse_leeway helper: fail-closed parsing (unparseable / negative / NaN /
  inf -> 0, never widens the window); 0 honours strict mode.
- Self-hosted OIDC: id_token_leeway kwarg + dashboard.oauth.self_hosted.id_token_leeway.
- Nous: token_leeway kwarg + dashboard.oauth.token_leeway.
- No new env vars (config.yaml-only knobs).

Fixes #47815
2026-10-06 11:14:28 -04:00
teknium1
781334eea4 chore(memory): remove the bundled mem0 provider (moved to the plugin catalog)
plugin-catalog/mem0.yaml (#131267) points at mem0ai/mem0's own
integrations/hermes-plugin-mem0 at a reviewed pin. The entry stays dormant
while a bundled copy exists (bundled providers win in find_provider_dir), so
deleting the in-tree copy is what lets hermes_cli/memory_provider_migration.py
install it for every home whose memory.provider is "mem0". The vendor copy
keeps the provider name, mem0.json, the MEM0_* keys and the setup wizard.

Also dropped: the hermes-agent[mem0] extra, its win-arm64 platform row and
exclude-newer row, the pm/extras.py anchor (uv lock removes only mem0ai,
posthog, qdrant-client and sqlalchemy), the mem0-specific doctor check (the
generic check now names `hermes plugins install mem0`), and the tests that
imported plugins.memory.mem0. Prose that lists the bundled set now names mem0
as a catalog provider. MEM0_* env hints and redaction stay: the catalog copy
still reads them.
2026-10-06 05:50:08 -07:00
Teknium
05e8ee35a9 test(web): firecrawl client stub takes the capability argument 2026-10-06 02:53:56 -07:00
Teknium
22bc7eac71 refactor(solstice): rebuild the provider as a standard model-provider plugin
The whole provider now lives in plugins/model-providers/solstice/ on the
generic seams:
- auth.py: NAS config discovery plus the brokered exchange/refresh, given
  to the generic PKCE flow as token_request. A refresh that NAS rejects
  (it folds every Google failure into 502) is treated as a dead grant
  needing re-login.
- transport.py: a GeminiNativeClient subclass on the
  generateContentPerUserQuota methods with Bearer auth (~30 lines, where
  the PR duplicated a 257-line adapter).
- __init__.py: a hidden profile (create_client, fallback models,
  default_aux_model).

Removed from core: hermes_cli/solstice.py, agent/solstice_adapter.py, and
the provider's branches in main.py, auth_commands.py, models.py,
runtime_provider.py, model_setup_flows.py and agent_runtime_helpers.py.
The unused SOLSTICE_CLIENT_ID env read is gone too.
2026-10-05 23:33:41 -07:00
Teknium
a928a959e3 feat(plugins): let plugins register Automation Blueprints via ctx
Plugins can now call ctx.register_automation_blueprint(key, *, title,
description, schedule_template, prompt_template, category, slots,
deliver_default, skills, tags). The fields mirror the built-in
AutomationBlueprint, and slots are BlueprintSlot field dicts, so the call
also crosses the plugin-host boundary.

- One lookup, cron.blueprint_catalog.list_blueprints() / get_blueprint(),
  merges the built-ins with the active profile's plugin blueprints at call
  time. /blueprint (CLI/TUI/messengers), /api/cron/blueprints and the
  instantiate route all use it. Each dashboard route resolves inside the
  ?profile= scope, off the event loop.
- Keys are namespaced <plugin>:<key>, so a plugin cannot shadow a built-in.
  A ':' in the key, a duplicate key, an unknown placeholder, a bad slot type
  or a schedule that does not parse is warned and ignored at registration.
- Catalog entries carry source/plugin. Desktop fetches the catalog for the
  profile it creates jobs in, keyed per profile, and shows the plugin name
  on the blueprint row and in the "Start from" select. The dashboard shows
  a plugin badge.
- A created job is an ordinary cron job with the prompt and schedule already
  filled in, so it keeps running if the plugin is later disabled.
- The ctx content registrars (register_skill, now joined by
  register_automation_blueprint) move to hermes_cli/plugins_content.py.
  The dashboard's blueprint types move to web/src/lib/automation-blueprints.ts.

Closes #132373. Requested by @enckequity.
2026-10-05 17:34:36 -07:00
Teknium
3a6d709921 fix(plugins): agent builds wait out a slow provider import; only per-turn callers are bounded
The 10 s concurrent-load bound also hit slow but healthy first imports. A
second agent built while a provider's import took longer than 10 s got no
memory provider for its whole session, and with checkpoint_required every
compaction then failed closed.

The bound now applies only inside plugin_loader.bounded_load_wait(), which
the per-turn gateway memory identity signature uses. Other callers (agent
builds) wait for the in-flight load. Cycle detection still breaks a
cross-thread deadlock for every caller.
2026-10-05 15:55:58 -07:00
Teknium
1f11e7fa23 fix(plugins): bound and cycle-check the concurrent plugin load wait
Review fixes for the per-module plugin load lock:

- A second caller waits at most 10 s for another thread's load of the same
  module, then refuses with a named warning and returns None. While that
  import is still stuck, later callers refuse at once. A hung provider import
  no longer blocks every gateway turn, and there is no inline fallback load.
- The wait polls a wait-for graph spanning the loader locks and importlib's
  own module locks. When thread A holds a helper's import lock and waits on
  the plugin while thread B loads the plugin and imports that helper, the
  cycle is detected and the waiter takes the partial module, the same way
  importlib handles a concurrent circular import. Before, both threads hung.
- The lock is still a same-thread RLock with no worker thread, so recursive
  loads and ContextVars are unchanged.
- conversation_compression.py: compact the fork-checkpoint exemption so the
  file stays under its line cap after the rebase.
2026-10-05 15:55:58 -07:00
Teknium
a1a8a0320d test: cover concurrent plugin loads and persistence-isolated fork compaction
- a second load_plugin_module caller while the first is mid-exec must get the
  fully executed module, never the published shell
- a persistence-isolated fork (no session DB, no memory manager) skips the
  checkpoint gate; a durable agent still fails closed

Co-authored-by: Albert.Zhou <57474+albert748@users.noreply.github.com>
Co-authored-by: stephane <stephane@fastmail.com>
Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
Co-authored-by: Turgut Kural <58116817+TurgutKural@users.noreply.github.com>
2026-10-05 15:55:58 -07:00
Adolanium
8eeb6ec4c6 fix(zai): clamp GLM-5.3 thinking-off to enabled plus low
GLM-5.3 rejects thinking.type=disabled with HTTP 400 code 1210
(issue #85890, #96373). An explicit off preference now sends
enabled plus reasoning_effort=low. 4.5/4.6/5.2 still send disabled.
2026-10-05 16:43:54 -05:00
teknium1
4c4ad0d387 feat(discord): setup checks the bot token with Discord and prints the invite link
Discord was the hardest gateway to set up: the .env template had no Discord
section at all, the wizard saved whatever was pasted, and users had to find
the Message Content Intent toggle, build an OAuth2 invite URL by hand and turn
on Developer Mode just to learn their own user ID.

The wizard now reads GET /applications/@me with the pasted token: a rejected
token is re-prompted instead of saved, an intent that is off gets a direct
link to the toggle and a re-check, the invite link is printed with the
permissions the adapter uses, and the allowlist defaults to the application's
owner (or accepted team members). Offline, the token is still saved with the
manual intent reminder. The wizard moves from the 7k-line adapter into
plugins/platforms/discord/onboarding.py.
2026-10-03 23:40:33 -07:00
kshitijk4poor
483a8b3af2 test(gateway): pin the clarify text-fallback push and trim to two invariants
Add the clarify text-fallback invariant. It drives the real
TelegramAdapter send through text_fallback_coro and fails on base, where
the send carries disable_notification=True. Drop the runner metadata
test: it only pins that the metadata did not change from base. The
_notification_kwargs parametrize already fails on base for
is_approval_prompt, which keeps the stack at two invariant tests.
2026-10-04 11:58:00 +05:30
kshitijk4poor
943d4caa47 fix(gateway): keep approval prompts interim instead of tagging them notify
The salvaged change also put notify=True on both approval sends.
On A2A, `notify` marks the turn-final reply, and A2A has no native
approval renderer. A prompt that needs a human decision would therefore
complete the caller's task with the prompt text while the turn is still
waiting (@ehz0ah's review on #132527). Telegram already pushes the
prompt through the approval-aware _notification_kwargs, so only the
is_approval_prompt marker is needed. The card send goes back to the
verbatim thread metadata: Telegram's control-message path never silences
cards.

Tests: the runner test pins that the fallback metadata stays interim and
does not carry notify. The adapter tests become a single parametrize.

Fold the _notification_kwargs change back into the original one-line
shape, so adapter.py grows by one line instead of six.
2026-10-04 11:58:00 +05:30
liuhao1024
e26d4cb3e3 fix(telegram): push approval prompts instead of silently delivering them
In the default Telegram notification mode ("important") the plain-text
approval fallback marked only is_approval_prompt, which no notification
decision consults, so _notification_kwargs sent the prompt with
disable_notification=True: the phone got no push, the user never learned
a dangerous command was waiting, and the tool sat the full approvals.timeout
(default 300s) before failing closed (#132516).

Mirror the cron-delivery convention on both approval sends in
_approval_notify_sync: notify=True on the button card and the text
fallback, and make _notification_kwargs itself approval-aware
(is_approval_prompt defeats disable_notification) so every surface that
marks the prompt is covered in one place. Loosens no guard and
auto-approves nothing.

(cherry picked from commit 576fec2ab6a6cdca08f96968574ba741c47568c0)
2026-10-04 11:58:00 +05:30
kshitijk4poor
37dd129a76 test(telegram): pin per-chat FIFO when a waiting update is cancelled 2026-10-03 20:50:15 +05:30
kshitijk4poor
c03cca2763 refactor(telegram): parse max_concurrent_updates with coerce_port
Gate BR3 r1 reuse (Low): build_update_processor hand-rolled the int-or-
default parse that gateway.platforms._shared.coerce_port already does.
Neither caught OverflowError, so `max_concurrent_updates: .inf` in YAML
aborted connect(). coerce_port now treats non-finite values as unparseable
(as gateway.config._coerce_num already does), which also hardens its other
port/int callers; the telegram setting warns and falls back to 32.
2026-10-03 20:50:15 +05:30
kshitijk4poor
03395f9de7 refactor(telegram): build PerChatUpdateProcessor on SimpleUpdateProcessor
Gate BR3 r1 quality (Low): PTB's SimpleUpdateProcessor already provides the
no-op initialize/shutdown the class copied, and config.extra is always a
dict, so the `(extra or {})` guard was dead. The wiring test only compared
PTB's concurrent_updates property with itself (true for any processor);
assert the PerChatUpdateProcessor type and DEFAULT_MAX_CONCURRENT_UPDATES
instead - now red when a plain SimpleUpdateProcessor(32) is wired in.
2026-10-03 20:50:15 +05:30
kshitijk4poor
ff90457267 fix(telegram): keep per-chat chain intact when a waiting update is cancelled
PerChatUpdateProcessor awaited the predecessor's tail future directly, so
cancelling a waiting same-chat update also cancelled that future (gate
BR3 r1 2c + quality, Medium). The running predecessor then raised
InvalidStateError from done.set_result() after its handler succeeded, and
the next waiter started while the predecessor still ran (probe order:
start A, start C). Shield the await and, for a cancelled waiter, release
its turn only once its predecessor finishes.
2026-10-03 20:50:15 +05:30
kshitijk4poor
a6d7e08fd1 fix(telegram): keep same-chat updates in order under concurrent dispatch
Plain PTB concurrency (concurrent_updates=32) unblocks other chats while one
chat's handler is slow, but also lets that chat's NEXT update dispatch and
deliver before the first one finishes (download/vision/callback work happens
inline, before text batching can merge them).

PerChatUpdateProcessor (update_admission.py, not the 7k-line adapter) chains
each update behind the previous update of the same chat while keeping the
bounded cross-chat pool. The builder keeps the instance, so the connect-retry
rebuild in _initialize_app_with_retries reuses it. An invalid
max_concurrent_updates now logs a warning instead of silently falling back
(as #74652 did), and the dispatch-stall watchdog comment no longer claims PTB
dispatches sequentially.

Co-authored-by: fortona-space <299084105+fortona-space@users.noreply.github.com>
2026-10-03 20:50:15 +05:30
Yuan Li
7766d4627f test(telegram): regression tests for concurrent update processing
(cherry picked from commit 64cc98fb15e1a4f6643ff4ae4b8aae0b566ee74b)
2026-10-03 20:50:15 +05:30
kshitijk4poor
bb5b4f4ca6 test(telegram): run the ingress admission case for the full stall threshold
Fold of gate finding (2c Medium regression): raising _INGRESS_DISPATCH_STALL_HEARTBEATS to 4
left the `ingress` row of test_only_pre_handoff_failure_reopens_admission looping 3 heartbeats,
which can never reach the threshold, so its "no healthy-but-deaf report" assertion was disarmed.
Loop the threshold itself so the leg tracks future bumps. Verified: with update_admission's
dispatch counting removed, the heartbeat leg now fails (it passed with range(3)).
2026-10-03 18:40:38 +05:30
Froraut
b015e59fb9 fix(matrix): address review — budget edit mentions and move plugin tests
An edit carries m.mentions twice: once inside m.new_content and once at the
top level. When a budget-sized edit names many distinct Matrix IDs, that
fixed part alone could pass the content budget. Shortening the fallback body
then had no room to work with, and the edit still went out at about 54 KB,
which an encrypted room would reject with M_TOO_LARGE.

When the edit is still over budget with an empty fallback, it now drops the
top-level mentions copy. That copy only drives the edit's own notifications,
and m.new_content keeps the full list.

The event-size tests move to tests/plugins/platforms/matrix/test_event_size.py,
mirroring the plugin source tree, and gain a mention-heavy text. The
tests/gateway/test_matrix_message_length.py file is back to main apart from the
env-override value that the new ceiling requires.

(cherry picked from commit 4b06bcb7dbffb732bef6e877cb66dea2fbff8c73)
2026-10-03 16:31:35 +05:30
Benjamin Brumbaugh
d5cda9135c fix(disk-cleanup): fail closed on unresolved track paths 2026-10-02 21:21:36 -05:00
Benjamin Brumbaugh
cc2dd7016e test: harden disk cleanup path safety 2026-10-02 21:21:36 -05:00
Benjamin Brumbaugh
0ba4992c52 fix(disk-cleanup): accept macOS temp aliases 2026-10-02 21:21:36 -05:00
liuhao1024
e0daebeb6b fix(web/firecrawl): pass timeout to scrape API to match asyncio deadline
The Firecrawl scrape() call was missing the `timeout` parameter, so the
server-side used its 30s default.  The client-side asyncio.wait_for
expects 60s, causing SCRAPE_TIMEOUT on slow pages before the Python
deadline expires.

Fix: pass timeout=60000 (ms) to scrape() so both deadlines align.

Fixes #43272
2026-10-02 20:24:48 -05:00