Commit Graph
974 Commits
Author SHA1 Message Date
liuhao1024 f9da99aefc fix(plugins): warn loudly when a plugin is in both enabled and disabled lists
A plugin listed in both plugins.enabled and plugins.disabled loaded nowhere
with only a DEBUG-level skip (#131980): no startup error, nothing in config
check, and plugins.enabled still listed it, so the surface looked enabled
while the gate silently dropped it.

The disable still wins (documented precedence), but gate_manifest now
detects the contradiction (matching either the path key or the manifest
name, as the disable check itself does) and logs a WARNING naming both
config keys; the placeholder error surfaces the conflict to introspection.

中文说明:修复 #131980 —— plugins.enabled 与 plugins.disabled 同时列出同一
插件时静默不加载(仅 DEBUG 日志)。保持 disabled 优先,但矛盾时升级为
WARNING 并指明两个配置键;占位 error 同步暴露冲突。
2026-10-25 03:59:27 -07:00
Teknium a9dfc2500d docs(browser): drop references to deleted browser tools
Model-facing text, startup tips (all 17 locales; the tips probe needs contiguous keys, so the
five stale tips are rewritten to live browser.* features), .env.example, developer-guide file
maps and the provider parity script still named browser_navigate/vision/console, record_sessions,
agent-browser/AGENT_BROWSER_* and the deleted browser_tool* modules.
2026-10-23 01:42:14 -07:00
Teknium 2b9846ffe8 fix(krea): the schema describes the stored model on direct Krea too
Direct dispatch passes image_gen.model to generate() explicitly, like the
managed path, so a KREA_IMAGE_MODEL override naming Krea 2 made the schema
hide Krea 3's thinking/resolution controls and describe edit inputs as style
references while Krea 3 ran.
2026-10-20 12:30:50 -07:00
Mariano Nicolini d1400be1f3 fix(krea): review follow-ups: edits keep their framing, schema follows the stored model, Krea 3 wording 2026-10-20 12:30:50 -07:00
Mariano Nicolini 693777d4fa feat(krea): Krea 3 edits and generates through the managed gateway, with thinking and resolution controls 2026-10-20 12:30:50 -07:00
Teknium 8ddf6d1234 test(a2a): the ::1 bind test skips on hosts without IPv6 2026-10-20 04:05:08 -07:00
Teknium d2b3446b1e fix(a2a): one IPv6 server class; dual-stack peers keep their IPv4 identity
Folds the two IPv6 server classes into one: every IPv6 host gets
AF_INET6, and V6ONLY=0 makes the :: wildcard answer IPv4 too.

A dual-stack socket reports an IPv4 peer as ::ffff:a.b.c.d, so the
shared-token / no-token identity became ip:::ffff:127.0.0.1 and an
A2A_TRUSTED_PEERS entry written as ip:127.0.0.1 no longer matched
(403 "peer not trusted"). _client_ip unmaps it. The test drives a
token-gated :: bind over both stacks plus an IPv4 tasks/list call
behind an IPv4 allow-list.
2026-10-20 04:05:08 -07:00
John Paul Soliva 1f0981bd8e fix(a2a): accept the bracketed [::1] spelling of an IPv6 A2A_HOST
A2A_HOST=[::1] was not recognised as loopback (no-token runs fell back
to 127.0.0.1 with a misleading warning), failed to bind with a token, and
advertised http://[[::1]]:port/. Strip URL brackets when capturing the
host, and pin the Host-less advertised URL in the IPv6 test.
2026-10-20 04:05:08 -07:00
John Paul Soliva 9f908ca3dd fix(a2a): bind and advertise IPv6 hosts such as ::1
A2A_HOST=::1 is an accepted loopback bind, but ThreadingHTTPServer is
AF_INET only, so the listener failed with bind_failed. An IPv6 host now
uses the AF_INET6 server, and the Agent Card URL brackets the literal.
2026-10-20 04:05:08 -07:00
chatchap-2nd-of-chartup 36a6c85c46 fix(a2a): dual-stack (AF_INET6, V6ONLY=0) bind when A2A_HOST=::
What this fixes: A2A_HOST=:: is the conventional IPv6 wildcard (one socket,
both stacks). stdlib ThreadingHTTPServer is AF_INET-pinned and cannot bind
:: at all (gaierror 11001 on Windows) - A2A stays down. The AF_INET pin is
the root cause.

Change: when host == ::, swap in an AF_INET6 + IPV6_V6ONLY=0 subclass; one
dual-stack socket, matching the aiohttp DM bind on 8642. Other hosts keep
the standard family. Failed binds stay loud via the existing bind_failed
fatal/retry path.

Test: regression test - with A2A_HOST=::, connect a live adapter, assert
dual-stack class + AF_INET6 + V6ONLY=0, GET the agent card over [::1] on
the bound port. Placed in TestBindSafety (default lane) because the repo
addopts -m "not integration and not live" deselect the integration class;
self-skips on hosts without IPv6. Verified against the upstream adapter
before the change: the same live bind fails (getaddrinfo EAFNOSUPPORT on
Linux; the same failure class as gaierror 11001 on Windows).

Related work: complements #126285 (IPv6 literal bind + bracketed agent
card URL) by making the A2A socket dual-stack (V6ONLY=0) so :: answers on
both stacks.

Base: 865ba906 (main).
2026-10-20 04:05:08 -07:00
Teknium 34ad3d3063 test(telegram): two invariant tests for the new-topic opener (#136567)
Replaces the 684-line opener/rename test files with two invariants on the
real adapter: thread-less follow-ups after a creation land in the new topic
(non-adjacent ids, split second chunk, stamped message ends the follow, the
name is not cached as an operator topic), and a creation is not adopted by
another sender/chat, after the follow window, or from an unauthorized user.
Both are red on main and on the adjacency-gated pairing.
2026-10-19 21:47:53 -07:00
sharpnotebot 7cfafba18e fix(telegram): 建立 topic 時不再快取名稱,恢復自動改名 (Refs #136567)
The forum_topic_created handler cached the user-chosen topic name (e.g.
"New Chat") in _dm_topics. _get_dm_topic_info treats any _dm_topics hit
as an operator-declared topic, so _rename_telegram_topic_for_session_title
silently skipped every dynamically created topic and session-title
auto-rename stopped working.

Keep only the pending-creation record (opener adoption is unchanged) and
leave _dm_topics to operator config. Topics declared in extra.dm_topics
stay protected from auto-rename.

Tests: real PTB creation -> thread-less opener -> runner rename path
renames once; a config-fixed topic is not renamed; a creation stays
pending without populating _dm_topics. The opener test that pinned the
cache side effect now asserts its absence.
2026-10-19 21:47:53 -07:00
sharpnotebot 37ec498ea7 fix(telegram): record topic creations only from authorized humans and consume them once (Refs #136567)
Builds on the salvaged #137348 commits above. The pairing store accepted
any private-chat forum_topic_created service message and kept records
whose opener arrived already threaded until the TTL ran out.

- Record a creation (and cache its topic name) only when the service
  Message carries message_thread_id, has a non-bot sender, and passes
  the existing intake prefilter _is_user_authorized_from_message, so a
  rejected user cannot populate adapter state.
- Cap the store at _NEW_TOPIC_OPENER_MAX_PENDING (256), evicting the
  oldest stamp after the TTL prune.
- The first later message from the same chat and sender consumes the
  record whether it is adopted, already threaded, non-adjacent or
  expired; adoption still requires a thread-less, adjacent, fresh
  opener. Other chats and senders neither adopt nor consume it.

Tests: adds real PTB Update.de_json -> connect() -> process_update
cases for unauthorized, bot, missing thread/sender, healthy consume,
non-adjacent consume, other chat/sender, expiry and the cap. Phase 1
of test_pending_created_topics_stays_bounded now expects the five
healthy records to be consumed (it previously pinned the leak), and an
unused unpacked `delivered` is renamed to satisfy RUF059.

Adapted from a local field-tested patch; the PTB service+opener
pairing design is from #137348 (PRELVAS94), and reading the topic id
from Message.message_thread_id rather than ForumTopicCreated follows
e756b3b in #136581 (liuhao1024).
2026-10-19 21:47:53 -07:00
Pedro Relvas 37347319dc test(telegram): drop the dead helper body and point the fixture doc at the real test (Refs #136567)
Review follow-up on #137348, addressing the two findings verbatim:

- The ``opener`` fixture carried a second, unreachable copy of its body after the
  return (a stray paste during round 3), plus a duplicated docstring. The live
  fixture is now the single body with one return; nothing downstream changed, so
  the 19 cases pass identically.
- The fixture docstring cited ``test_bot_created_topic_opener_is_adopted``, which
  does not exist. The same-sender-bot case it was describing is asserted by
  ``test_bot_created_topic_other_bots_opener_is_not_adopted`` (a bot-created topic
  is recorded under the bot's sender key), which is what the wording now names.

Also closes the reviewer's non-blocking note: ``reply_to_user`` was accepted by the
fixture but passed by no test, so the is_bot branch of the quoted-creation message
was dead weight. The reply-axis case now supplies it, so the shape is built for
real rather than merely parameterised.

Verified: 19/19 pass; the suite was run against this exact file, not a copy.
2026-10-19 21:47:53 -07:00
Pedro Relvas 9f97147f76 test(telegram): cover the reporter's verbatim captures and the reply axis (Refs #136567)
Adds the four captures itsjustmichele supplied on 2026-10-14 as a parametrized
regression matrix: ids, thread ids and update ids are verbatim from the receiving
side, texts redacted. Two rows are the regressed shape (thread-less opener, not a
reply) and must be adopted into the announced topic; two are the healthy shape
(opener already labeled, and a reply to the creation message) and must keep it.

Also pins that reply_to_message is NOT a routing input: adoption stays governed by
adjacency alone. The field data shows a clean 4/4 correlation between the two
shapes and the reply flag, which is exactly why it must not be allowed to leak
into the gate - if it ever did, a non-replying client would silently lose topics.

Verified: 19/19 pass with the fix in place; with adoption stubbed out the two
regressed capture rows and both reply-axis cases fail, so the matrix is load-bearing.
2026-10-19 21:47:53 -07:00
Pedro Relvas 1da18665f7 test(telegram): cover the reviewer's captured shape and the two missing negative cases (Refs #136567)
Drive the captured updates (forum_topic_created service message, then adjacent
thread-less opener) straight through _build_message_event, as @itsjustmichele's
review asked, plus the negative cases the suite lacked: a different sender's
opener must not claim the creation, and a bot-created topic (service message
attributed to the bot) is recorded under the bot's own sender key, so a human's
or another bot's thread-less opener is a different key - routing unchanged.
2026-10-19 21:47:53 -07:00
Pedro Relvas 328b64a9a7 fix(telegram): bound the pending-created-topics cache (Refs #136567)
The _pending_created_topics map grows unboundedly: records whose opener
arrives already threaded (the healthy path) return early before any pop,
and records that simply never get claimed are never revisited. Prune
expired records at record time inside _pending_created_topics_store.

Adds a regression test that demonstrates the leak (10 distinct keys all
survive), the prune (negative TTL evicts all), and the routing contract
(fresh record still claims its opener).

Also cleans stale update receipts in the test's connected() fixture so
the admission controller doesn't drop updates across test runs.
2026-10-19 21:47:53 -07:00
PRELVAS94 3687ef0bf4 fix(telegram): route a new DM topic's thread-less opening message into that topic
Telegram normally stamps a new private-chat topic's opening message with the new
message_thread_id, but it sometimes delivers that opener with no thread id at all.
A forum_topic_created service message carries no text, so no core handler matched it and
the new topic id was never learned: the opener arrived lobby-shaped and gateway topic
recovery (gateway/run_topics.py) pinned it to the most recently bound topic, leaving the
new topic with no session, binding or auto-rename.

- telegram_topic_routing.py: narrow handler for filters.StatusUpdate.FORUM_TOPIC_CREATED
  records (chat_id, sender_id) -> (new thread id, creation message id, stamp) for private
  chats and caches the topic name; the opener adopts that thread id in _build_message_event
  (one-shot, only for the message immediately after the creation, inside a 120s window).
- adapter.py: wire the handler + mixin in, and drop the now-unreachable
  forum_topic_created branch in _resolve_topic_binding.
- tests/plugins/test_telegram_forum_topic_created_opener.py: real PTB dispatch coverage.

Fixes #136567
2026-10-19 21:47:53 -07:00
Teknium bc3cbd50db fix(image_gen,video_gen): poll the FAL queue every 2 s instead of every 0.1 s (#141368)
fal_client's SyncRequestHandle.get() drains iter_events() at its default
interval=0.1, i.e. a status GET every ~0.3-0.5 s including RTT. Through the
Nous tool gateway (200 requests per fixed one-minute window) a single 12 s
image cost 42 requests (~208 req/min), so a second concurrent job — or the
same user's video job — trips RATE_LIMIT_EXCEEDED on submit, status and
result calls.

Add tools.fal_common.wait_fal_queue_result(): drain iter_events() at
FAL_QUEUE_POLL_SECONDS (2 s), then call get(), whose first status check now
sees Completed and goes straight to the result GET. Public SDK API only, no
monkeypatching. The image tool's interrupt-aware _wait_fal_result and both
FAL video waits (generation + SeedVR upscale) use it.

Live, one gpt-image-2.5 flare image via the gateway: 42 -> 9 requests,
12.1 s -> 12.5 s wall clock.

Test fakes that modelled the handle with only get() gain an empty
iter_events(); two touched test files also get encoding="utf-8" on their
config write_text (windows-footguns).
2026-10-19 01:31:31 -07:00
unsupportedpastels 419e6627aa fix(memory): serve a memory provider only to the profile it was initialized for
On a multiplexed gateway every served profile's agents share one process.
Each agent's MemoryManager owns its own provider instance, but nothing stopped
a tool call or lifecycle hook from another profile's context, or from a
context naming no profile, from reaching it. Nothing stopped two agents being
handed one shared instance either. The in-repo providers also bound their
stores from the ambient home rather than the one the manager passed.

MemoryManager.initialize_all records the profile home (and agent_identity) it
bound its providers to. Then:

- Tool calls and every lifecycle hook (prefetch, queue_prefetch, sync_turn,
  on_turn_start, on_memory_write, on_session_end, on_session_switch, the
  session boundary, on_pre_compress, on_delegation) run only when the caller
  explicitly names that profile: the context's HERMES_HOME override, or the
  home its secret scope was built for. On a multiplexed process a caller that
  names no profile is refused, never resolved to the process's own profile.
  A refused checkpoint compression raises, so the transcript stays
  uncompressed.
- Background hooks run under the owning profile's HERMES_HOME, set
  explicitly in the worker.
- An instance another profile's manager already bound is dropped, not
  re-initialized (logged as memory_provider_instance_shared). Claims are
  tracked by instance identity, never by the provider's own hash or
  equality; on a multiplexed process an instance that can't be tracked
  (no weak reference) is refused too.
- handle_tool_call passes hermes_home / agent_identity next to the previous
  commit's caller_gsk. A provider with the two-argument handle_tool_call gets
  only what it declares; caller_gsk alone made every call to it a TypeError.
- The model sees a generic "refused, check gateway.log". gateway.log keeps
  memory_caller_profile_unresolved and both homes.

ByteRover and RetainDB now bind their store from initialize()'s hermes_home.
Both already kept their bindings on the instance and hold no profile state at
module level.

The provider side of the rule is documented on MemoryProvider and in the
memory-provider plugin guide: a new instance per load, every binding from
initialize()'s hermes_home, no profile state at module or class level, no
per-call rebinding. Core cannot see inside an external provider; one that
keeps a process-wide binding still mixes profiles' memories.

The previous commit's two MagicMock tests in test_run_agent.py move into
tests/agent/test_memory_provider_caller_profile.py (test_run_agent.py is over
its line cap and may not grow). That file drives both dispatch paths and
every hook with real managers and two profiles interleaving on threads;
tests/plugins/memory/test_provider_multiplex_contract.py does the same for
ByteRover and RetainDB with a fake backend.

Refs #134889 (PLG-11)
Refs #140208
Refs #140058
2026-10-17 13:28:18 -04:00
Teknium 3d21ec1294 refactor(platforms): plugin PlatformEntry carries its prompt hint and display defaults
PLATFORM_HINTS (agent/prompt_builder.py) and _PLATFORM_DEFAULTS (gateway/display_config.py)
duplicated per-platform data for every bundled messaging plugin. Both consumers already fell
back to the registry (PlatformEntry.platform_hint / display_tier), so the plugin rows move onto
each plugin's register() call and the core tables keep only core surfaces (cron, cli, tui,
desktop, api_server hints; webhook, api_server display rows).

New PlatformEntry field display_overrides: per-key defaults layered over display_tier (telegram,
discord, slack, wecom had tier + tweaks). Hint text is copied byte-for-byte (the shared
_MEDIA_NATIVE fragment is inlined); a base-vs-head oracle of platform_hint() and
resolve_display_setting()/resolve_tool_progress() for every platform, with and without user
display overrides, is byte-identical. Oversize adapters keep the hint in adapter_hint.py.
2026-10-16 19:42:34 -07:00
Teknium f41ac07695 fix(slack): early auth reject checks the session thread's routed profile
_early_reject_unauthorized dropped the decoded thread_id, so a thread-scoped
profile_routes entry checked the default profile's pairing store: a caller
approved only in the routed profile had slash commands (even /stop) and
messages in that thread dropped. Pass thread_id from both the slash and the
message path to _is_sender_authorized and the build_source fallback.
2026-10-16 14:22:13 -07:00
TekniumandJohn Paul Soliva 0815b4ed4a fix(slack): concurrent slash commands of one session reach the gateway in arrival order
Names resolved for a slash turn are awaited before the handoff and Bolt runs
callbacks concurrently, so a /queue whose cold users.info answered late
reached the gateway FIFO after a later one. A slash now reserves its
session's slot before any lookup and hands over only after the slash ahead
returned from handle_message; a slash cancelled while waiting still chains
the one behind it. /stop, /approve, /deny, /pause take no slot.

Ported from #128722 (9b719083, eb546b6a) into adapter_slash_lane.py.

Co-authored-by: John Paul Soliva <soliva.johnpaul@icloud.com>
2026-10-16 14:22:13 -07:00
TekniumandJohn Paul Soliva 7659e49886 test(slack): message, slash and /goal kickoff turns render one pinned prompt
Drive message -> slash -> message in one thread session through the real gateway
pins (_pinned_channel_inputs / _pinned_session_context_prompt) and the real
/goal kickoff enqueue, so the class is judged by the cache outcome, not only by
event fields.

Co-authored-by: John Paul Soliva <soliva.johnpaul@icloud.com>
2026-10-16 14:22:13 -07:00
TekniumandJohn Paul Soliva e817ac8c8e fix(slack): /pause is handed over before any Slack lookup
/pause engages the emergency stop and starts no turn, like /stop and /approve,
so a cold users.info must not hold it back. Ported from 9b719083 in #128722
(its arrival-order handoff lane is left out).

Co-authored-by: John Paul Soliva <soliva.johnpaul@icloud.com>
2026-10-16 14:22:13 -07:00
John Paul Soliva d2ac28c77d fix(slack): a slash command that stops or unblocks the running turn waits for no Slack lookup
The names the slash path resolves (users.info, conversations.info) were awaited
before the event reached the active-session dispatcher. With a shared channel
session, a second operator's /stop sat behind their uncached users.info while
the first operator's worker kept running.

/stop, /new, /reset (the interrupt-then-dispatch commands) and /approve, /deny
start no turn of their own, so they are handed over before any lookup. Commands
that do start a turn (/queue, /retry, /goal, ...) and free-form questions keep
the names and the channel prompt.
2026-10-16 14:22:13 -07:00
John Paul Soliva bda86e96f6 fix(slack): a rejected slash-command sender costs no Slack lookup
The message path rejects an unauthorized sender before any lookup (5bb933eedf); the slash path now resolves the channel and user name, so a sender the gateway was going to reject cost a conversations.info and a users.info first. Ask the same early gate before resolving. In a channel the runner only drops such a sender, so stop there. In a DM it answers per unauthorized_dm_behavior (pairing code or decline), so the event still reaches it, without names, exactly as before this branch.

Docs: N/A, slack.md already says an unauthorized DM gets the pairing prompt.
2026-10-16 14:22:13 -07:00
John Paul Soliva c1c2bab742 fix(slack): load channel skill bindings on slash-command turns
`/hermes <question>` can open a new session, and bound skills load only
when a session starts. The slash event carried no auto_skill, so a session
opened that way never loaded the channel's slack.channel_skill_bindings,
unlike one opened by an ordinary message. Resolve it the same way the
message path does.
2026-10-16 14:22:13 -07:00
John Paul Soliva 1424ffa429 fix(slack): carry channel prompt and source names on slash-command turns
_handle_slash_command built its MessageEvent without channel_prompt and its
source without chat_name/user_name, unlike the message path. A /hermes turn
ran without the configured channel_prompts and the bot identity line, and as
a human turn it re-pinned channel_pin to None and re-keyed the pinned
session-context prompt; the next ordinary message flipped both back, so every
slash turn cost two agent rebuilds and two prompt-cache misses. Synthetic
events copied from it (/retry, /queue, /steer fallback, /goal kickoff)
inherited the gap.

Resolve the names and the channel prompt with the same helpers the message
path uses.
2026-10-16 14:22:13 -07:00
Teknium 12552a2197 test(yuanbao): import the plugin module; registration shape test
(cherry picked from commit a0dd3491e43d7ba5c573d6d0bf264cfa2c01af5f)
2026-10-16 07:09:45 -07:00
Teknium 7ada20f7ad fix(qqbot): setup status reads .env like the old core wizard row
is_connected now falls back to env_is_connected(QQ_APP_ID, QQ_CLIENT_SECRET)
(.env-backed, scope-aware), so 'hermes gateway setup' shows QQ Bot as
configured for a .env-only profile, matching base.

(cherry picked from commit 19968fb98a96a99e59212fea7cf3129c81c5b7df)
2026-10-16 07:09:45 -07:00
Teknium 9189f6c69b fix(qqbot): keep the plugin a pure move
- plugin.yaml name qqbot-platform (bundled-plugin convention)
- drop env_enablement/apply_yaml_config hooks and platform_hint: core
  config_env _ENV_STEPS and PLATFORM_HINTS still own them, the hooks were
  new behaviour (markdown/env seeding) the move must not add
- drop validate_config/max_message_length: base never config-gated or
  chunked QQ in send_message (REST sender truncates at 4000)
- install_hint carries the old _BUILTIN_ADAPTERS warning text
- drop the _send_qqbot compat alias
- replace env-seeding tests with a registration-shape test

(cherry picked from commit b34a6d21e42d934555a2e6243c9a126dc0d5de42)
2026-10-16 07:09:45 -07:00
cxyhhhhh 0a2992af65 refactor(qqbot): move the built-in adapter into the bundled platform plugin
QQ Bot is a messaging platform, so its adapter, setup, and standalone sender belong in plugins/platforms/qqbot behind one register() entry. The core keeps the shipped-platform name lists, so webhook, cron, and prompt hints still treat qqbot as built-in.

(cherry picked from commit a7eb428aad0cc69ea098747b435e1e0e1d7e3b67)
2026-10-16 07:09:45 -07:00
helix4u 39de69b19c fix(irc): give each inbound line a unique message identity 2026-10-16 02:27:48 -07:00
unsupportedpastels 4e59ac2b88 test(discord): pin when a role removed from YAML stops re-authorizing
Re-authorization re-reads config through load_gateway_config(), whose
Discord hook copies YAML allowed_roles into DISCORD_ALLOWED_ROLES (first
writer wins) outside multiplex, and env keeps precedence in _gate_raw.
So a role removed from YAML is revoked at once only for a multiplexed
profile (the copy is skipped); on a single-profile install it stays
granted until restart, as for ingress and as on main.

The revocation test passed only because its fixture deleted that env
var. Replace it with a parametrized test through the real loader: two
re-authorizations around a YAML edit, multiplexed (revoked) and single
profile (still granted, env holds the copy). Correct the comments that
implied immediate revocation. No behaviour change.

Refs #134889 (MSG-7)
Refs #139162
Refs #139148
2026-10-15 23:59:47 -04:00
unsupportedpastelsand686f6c61 68b3b6af2e fix(discord): re-authorize durable role grants through the ingress gate resolver
The durable re-authorization read DISCORD_ALLOWED_ROLES from the scoped
env or the raw top-level `discord:` block only, so a profile that sets
its role allowlist under `platforms.discord` (flat or `extra`) admitted a
role-authorized member at ingress and then dropped the message with
permission_denied.

Recovery now resolves the gate with the same `_gate_raw` ingress uses,
applied to the Discord PlatformConfig that `load_gateway_config()` builds
for the transport profile at check time: scoped env first, then `extra`
with the loader's precedence (`platforms.discord.extra` over the flat
key and the top-level block). Reading current config instead of the
connect-time `self.config` keeps a config revocation effective.

Refs #134889 (MSG-7)
Refs #139162
Fixes #139148

Co-authored-by: 686f6c61 <6115107+686f6c61@users.noreply.github.com>
2026-10-15 23:59:47 -04:00
Maciej Lisiewski d5442d0a47 fix(google_meet): say-queue appends survive the speaker's rewrite
The bot's speaker re-reads say_queue.jsonl after speaking and rewrites it
without the spoken head; a meet_say append from the agent process landing
between the re-read and the rewrite was truncated away, so a request
reported as enqueued was never spoken. Appenders and the speaker's
re-read+rewrite now share a cross-process lock beside the queue file.
2026-10-15 22:12:28 +02:00
Teknium a89f374d7c Merge origin/main (#45: 255 commits to 3b63f7b0a4) into feat/unified-gateway-runtime
Conflicts (2 files, 1 hunk each); every main-side change is kept, ported to
where the branch moved the code:

hermes_cli/kanban_db.py (build_worker_context helpers _ctx_cap .. _ctx_comments)
  main:   f5a6651231c/d56e357556f coerce foreign/text timestamps on read; in
          _ctx_role_history the "Recent work" stamp became
          _ctx_stamp(_to_epoch(row['ended_at']) or 0, now) instead of int().
  branch: moved the whole _ctx_* helper block into hermes_cli/kanban_db_context.py
          (kanban_db.py imports it back), deleting it from kanban_db.py.
  merged: kept the branch's deletion in kanban_db.py and applied main's change in
          kanban_db_context.py::_ctx_role_history as
          _ctx_stamp(_kb._to_epoch(row['ended_at']) or 0, now). Main's other
          timestamp coercions in kanban_db.py (Task/Run/Comment/Attachment/Event
          from_row, _TASK_EPOCH_COLUMNS, release_stale_claims,
          _extend_live_stale_claim, task_age) auto-merged unchanged.

hermes_cli/web_server.py (_lifespan)
  main:   d7830c6f42 reworded one comment in the free-tier bootstrap block
          ("the ONE place its identity is created" -> "the one boot-time creator
          of its identity"; guests are now also created on first connector use).
  branch: moved _lifespan's body into hermes_cli/web_server_app.py::app_lifespan.
  merged: kept the branch's relocation and applied main's comment rewording in
          web_server_app.py.

Merge-induced health fix (no conflict marker; the combination tripped the ratchet):
apps/desktop/src/app/session/hooks/use-cwd-actions.ts::changeSessionCwd
  main:   63ed3caa4ba/dbc538547fc grew the function (statusbar cwd change for the
          focused chat / tile runtime ids) to CC 20.
  branch: added the session_busy refusal mapping (sidebar.projects.moveBusy) in
          its catch, +1 branch -> CC 21 > target 20 (scripts/check health blocking).
  merged: both behaviours kept; the busy-error mapping moved verbatim into a
          module-level cwdChangeError(err) helper. Health back to 0 blocking.

Check: every line main added between 1fd1827726 and 3b63f7b0a4 (317 files) is
present in the merged tree.
2026-10-12 16:55:20 -07:00
Teknium cdf5732af6 refactor: move Langfuse out of core into the langfuse catalog plugin
The bundled observability/langfuse plugin now ships as the official catalog plugin `langfuse`
(NousResearch/hermes-official-plugins, subdir langfuse) with the same hooks, HERMES_LANGFUSE_* /
LANGFUSE_* env vars and capture modes. Same pattern as the Home Assistant and Spotify moves: a
LEFT_CORE row installs and enables the plugin on `hermes update` / first start for every profile
home that had observability/langfuse (or its bare name) in plugins.enabled and not in
plugins.disabled. Keys alone are not use: the plugin was opt-in.

- left_core_migration: langfuse row + langfuse_in_use; HERMES_LANGFUSE_SECRET_KEY stays a stripped
  child-process secret and the public key / base URL stay private env, exactly as core stripped them
  while it shipped the plugin. New plugin_key field: `hermes plugins enable observability/langfuse`
  (what every pre-move doc said) now names `hermes plugins install langfuse` for this home.
- Core drops the plugin, the `hermes tools` Langfuse category and its post-setup hook (the plugin
  prompts for its keys at install), the OPTIONAL_ENV_VARS / _EXTRA_ENV_KEYS rows, the `langfuse`
  extra + its PM anchor (uv.lock regenerated with CI's uv 0.12.13), and the
  plugin's tests (they moved to the plugin repo).
- Docs (en + zh-Hans): built-in plugins and env-var reference point at the catalog plugin.
2026-10-12 14:00:32 -07:00
kshitijk4poor 112b52832b test(kanban): text timestamps reach no reader uncoerced
Extends the board test to assert all three served task timestamps are
epoch ints (and garbage is None), and adds a `hermes kanban show` test
for an ISO completed_at - the CLI path that also crashed before the
Task.from_row coercion.
2026-10-12 18:35:29 +05:30
liuhao1024 e8fe3703a9 test(kanban): pin done-column order for mixed int/ISO/garbage completed_at
Board must render and order a parseable ISO completed_at by its parsed
time, parking an unparseable stamp last. Taken from #134696 (test only:
the serializer-level coercion from #120973 already makes the done-column
sort key safe, so the PR's separate _done_sort_key is not needed).
2026-10-12 18:35:29 +05:30
thewhitemage. 3278fbd281 feat(plugins): submit Tameru 1.4.1 through the plugin catalogue
Replace the in-tree vendor copy with a community catalogue entry pinned
to the tested standalone source at 9a3642f. Keep contributor attribution.
The entry discloses host compatibility and recovery requirements.
Verified: all 12 repository checks pass; catalogue schema valid; pinned
plugin admission checks safe; standalone real-Hermes suite 1386 passed.
2026-10-12 03:45:30 -07:00
thewhitemage. 475e0d0120 fix(context): sync Tameru 1.4.1 and preserve Hermes call contracts
Forward compressor kwargs including bypass_cooldown; derive multimodal
user queries from text parts. Stamp both manifests and add discovery-path
regressions, proven red on the old adapter. Verified: 322 Hermes tests
passed; Ruff, compilation and vendor drift checks pass.
2026-10-12 03:45:30 -07:00
Teknium 9b681c38c3 fix(a2a): a forwarded message refused before admission is refused again on resend
Mechanism: _prepare_task recorded a fresh forwarded messageId in the process retry
LRU before the anti-loop and empty-text guards ran. A fresh over-budget message was
rejected once, but its exact resend then looked like a retry, was assigned turn 0,
and was forwarded to the owner (COMPLETED -> REJECTED -> COMPLETED, two owner calls).

Fix: when a fresh (non-retry) input is refused by a pre-admission guard, drop its id
from the retry LRU so the resend is classified as fresh and refused again. Accepted
inputs keep their retry entry.

Test: tests/plugins/test_a2a_rejected_resend.py (red on r14-base: third state COMPLETED).

Review: andrexibiza 4243963753
2026-10-11 23:38:03 -07:00
Teknium 972b398642 fix(teams): claim a durable job once before scheduling its pipeline run
Mechanism: 3d2fc30b3d made the msgraph scheduler accept-then-detach:
create_job_from_notification persists the job and run_job runs as a
background task. Graph's notification `id` is optional; for a batch
carrying the same no-id notification twice, the second call returns the
same job still `received` (run_job persists progress only once it runs),
so two run_job tasks started and the Notion sink created two pages. Main
ran run_notification inline, so the duplicate saw the first job terminal.

Fix: the runtime's scheduler claims the job id in a per-runtime set
before spawning, refuses a job already claimed, and releases the claim
when the task ends (a later retry_scheduled redelivery still runs).

Red on r13-base2: tests/plugins/test_teams_duplicate_job_claim.py (two
run_job calls for one batch).

Review: andrexibiza C23
2026-10-11 19:31:35 -07:00
Teknium 994759f4c4 Merge origin/main (#39: 29 commits to d4d2ab7b5d) into feat/unified-gateway-runtime
No conflicts. Brings in main's reasoning-replay series, holographic memory leaving core
(run_profile_reconcile now imports it through import_provider_module), and the update
main-channel fix.
2026-10-10 23:38:42 -07:00
Teknium 8693fa6914 Revert the tasks/cancel half of "fix(a2a): a forwarded task's cancel is refused; retries survive the id LRU"
This partially reverts 702f87906a4 under the regression-only scope ruling.

On origin/main, _rpc_tasks_cancel is byte-identical: it marks the task CANCELED, resets the
turn counter and resolves the local future, and never reaches the profile the task was
forwarded to. On main that forward is a `hermes chat` subprocess the adapter also cannot
stop. The cancel behaviour is therefore PRE-EXISTING and goes to the follow-up issue.

Kept: the retry identity. _note_forwarded_input, its 1000-entry LRU and the owner admission
input_id are new in this PR (none of them exist on main). The durable context-log fallback
for an evicted id therefore stays, along with its test
test_a_resend_evicted_from_the_process_memory_is_still_a_retry. The task record's input_id
field stays because the forward path stamps it.

Review: andrexibiza 5480715949-P2c (cancel half: pre-existing)
(cherry picked from commit b3a6b3ff1b2698869ca8702ad72628146cd15f20)
2026-10-10 22:49:19 -07:00
Teknium ec881ef75b fix(a2a): a forwarded task's cancel is refused; retries survive the id LRU
tasks/cancel on a task forwarded to its profile owner marked the A2A
task CANCELED, reset the turn counter and resolved the local future,
but never reached the owner: the admitted turn ran to completion while
the peer was told it was canceled. Separately, the retry check for a
forwarded resend (the message's input_id) lived only in a 1000-entry
process LRU, so an evicted (or pre-restart) id was treated as a new
message: it spent the anti-loop turn budget and logged the user text
again.

The task record now carries the input_id it was forwarded as, and
tasks/cancel answers TaskNotCancelable for a forwarded task (the
adapter cannot stop owner work; a2a.forward has no cancel verb and
forward_to_owner's contract is that accepted work is never cancelled).
The context log stores the forwarded input_id with the user message,
and the retry check falls back to it when the LRU misses, so the
owner's exact-retry dedup and the local bookkeeping agree.

Tests (red on r11-base: cancel answers CANCELED; the evicted resend
spends turns and logs twice):
tests/plugins/test_a2a_forwarded_cancel_retry.py

Review: andrexibiza 5480715949-P2c
(cherry picked from commit 702f87906a4110c50c891a9e852b086062f8c701)
2026-10-10 22:49:19 -07:00
teknium1 cca83848ec chore(memory): holographic leaves core for its community catalog plugin (re-land #136424)
Holographic has a standalone third-party maintainer (itpartypattaya) and a
reviewed catalog entry, so it leaves core now rather than on Oct 15. Restores
9e827685f2 exactly: reverts the revert (435b7bd3a7) and its health waivers
(b9ecc727e9). byterover and retaindb have no maintainer and stay bundled until
the announced date.
2026-10-10 21:18:25 -07:00
teknium1 435b7bd3a7 Revert "chore(memory): holographic leaves core for its community catalog plugin"
This reverts commit 9e827685f2 (#136424). It landed before the announced
October 15, 2026 date for holographic leaving core; it re-lands on or after
that date. Restores plugins/memory/holographic and its tests/docs.
2026-10-10 20:58:02 -07:00