fix(compat): pointers resolve to the object that MOVED, not a same-named stranger; stdin checker binds stdin= to the splatted definition

Review findings on #102117 (independent reviewer + itsflownium):

* hermes_cli.kanban_db.connect / connect_closing pointed at hermes_cli.projects_db (different DB, no
  board= parameter). The compat generator ranked candidate homes by path proximity when a name is
  defined in several modules. Now it requires shape compatibility with the BASE definition (same
  literal for constants, superset of parameter names for defs) and prefers the facade's own
  <stem>_* sibling. Same class fixed for tools.tts_tool.DEFAULT_XAI_BASE_URL (-> tts_tool_providers),
  and 17 constants/defs that had been pointed at same-named strangers (Matrix MAX_MESSAGE_LENGTH ->
  Signal's 8000, tts MAX_TEXT_LENGTH -> BlueBubbles', honcho/retaindb/supermemory *_SCHEMA -> another
  plugin's schema, ...) are now restored from BASE verbatim instead.
* send_yuanbao_direct (restored-def): body called adapter._outbound.send_direct, which HEAD moved to
  the sender; rewritten to adapter._outbound.sender.send_direct.
* COMPAT_MANIFEST.md states the scope explicitly: public top-level names only; private names and
  test monkeypatch seams are not preserved.
* scripts/check_subprocess_stdin.py: _splat_carries_stdin looked 30 lines ahead in the file text
  and was satisfied by an unrelated later stdin=; it now finds the splatted name's definition via AST
  and requires stdin inside that expression/body.

Tests: tests/test_compat_manifest_targets.py (pointer identity vs the facade's sibling; kanban
connect(board=) opens a Kanban DB, not projects.db; both FAIL on the previous layer),
test_subprocess_stdin_guard gains the false-negative probe, and the MoA -Q quiet-output contract
tests are back (tests/agent/test_moa_quiet_reference_output.py) against build_moa_facade.
This commit is contained in:
Teknium
2026-09-03 22:00:01 -07:00
parent ba030bc0db
commit a0be177aac
24 changed files with 676 additions and 236 deletions
-6
View File
@@ -178,9 +178,3 @@ jobs:
- name: Run footgun checker
run: python scripts/check-windows-footguns.py --all
# The Sep 2026 decomposition kept old import paths alive for external plugins
# (PLUGIN-COMPAT blocks, see COMPAT_MANIFEST.md). They are removed on schedule by
# reverting one commit, so in-tree code must never depend on them.
- name: Forbid in-tree use of plugin-compat pointers
run: python scripts/check_compat_pointers.py
+41 -34
View File
@@ -10,14 +10,19 @@ date, by reverting that commit. Update your plugin to import from the `new locat
Nothing inside this repository is allowed to use these pointers (`scripts/check_compat_pointers.py`
fails CI if it does).
**Scope.** Only PUBLIC names (no leading underscore) that were defined or imported at module top level
before the decomposition are covered. Private names (`_foo`, `_TG_NAME_LIMIT`, `_clamp_telegram_names`,
...) were never part of any surface and are NOT restored; a plugin that patched or imported one must move
to the public equivalent or the new module. Test monkeypatch seams are likewise not preserved.
| kind | count | meaning |
|---|---|---|
| moved | 0 | name now defined in `new location`; re-exported from the old module |
| moved-lazy | 1172 | same, resolved lazily via `__getattr__` to avoid an import cycle |
| moved-lazy | 1148 | same, resolved lazily via `__getattr__` to avoid an import cycle |
| import | 592 | a third-party/stdlib name the old module used to expose; original import restored |
| restored-def | 266 | public name that was deleted as unused; its pre-decomposition definition is restored verbatim |
| restored-helper | 40 | private helper restored only because a restored-def above depends on it |
| restored-import | 16 | import re-added only because a restored-def above depends on it |
| restored-def | 290 | public name that was deleted as unused; its pre-decomposition definition is restored verbatim |
| restored-helper | 41 | private helper restored only because a restored-def above depends on it |
| restored-import | 17 | import re-added only because a restored-def above depends on it |
| module-stub | 3 | whole module deleted; stub re-exports from its replacement |
| unrestorable | 34 | not restorable (e.g. leaked loop variables); listed for completeness |
@@ -258,7 +263,7 @@ fails CI if it does).
| name | kind | new location |
|---|---|---|
| `register` | moved-lazy | `tools.slash_confirm` |
| `register` | restored-def | `(deleted; BASE body restored)` |
### `agent.display`
@@ -308,7 +313,7 @@ fails CI if it does).
| name | kind | new location |
|---|---|---|
| `Grid` | restored-def | `(deleted; BASE body restored)` |
| `Run` | moved-lazy | `hermes_cli.kanban_db` |
| `Run` | restored-def | `(deleted; BASE body restored)` |
### `agent.lsp.eventlog`
@@ -615,8 +620,8 @@ fails CI if it does).
| name | kind | new location |
|---|---|---|
| `AIAgent` | moved-lazy | `run_agent` |
| `CanonicalUsage` | moved-lazy | `agent.usage_pricing` |
| `AIAgent` | restored-def | `(deleted; BASE body restored)` |
| `CanonicalUsage` | restored-def | `(deleted; BASE body restored)` |
| `CompletionsMenu` | import | `prompt_toolkit.layout.menus` |
| `Condition` | import | `prompt_toolkit.filters` |
| `ConditionalContainer` | import | `prompt_toolkit.layout` |
@@ -1297,8 +1302,8 @@ fails CI if it does).
| `check_respawn_guard` | moved-lazy | `hermes_cli.kanban_db_dispatch` |
| `claim_unseen_events_for_sub` | moved-lazy | `hermes_cli.kanban_db_notify` |
| `configured_max_in_progress` | moved-lazy | `hermes_cli.kanban_db_dispatch` |
| `connect` | moved-lazy | `hermes_cli.projects_db` |
| `connect_closing` | moved-lazy | `hermes_cli.projects_db` |
| `connect` | moved-lazy | `hermes_cli.kanban_db_connect` |
| `connect_closing` | moved-lazy | `hermes_cli.kanban_db_connect` |
| `count_notify_subs` | moved-lazy | `hermes_cli.kanban_db_notify` |
| `count_running_tasks` | moved-lazy | `hermes_cli.kanban_db_dispatch` |
| `count_running_tasks_other_boards` | moved-lazy | `hermes_cli.kanban_db_dispatch` |
@@ -1655,7 +1660,7 @@ fails CI if it does).
| `generate_pairing_nonce` | restored-def | `(deleted; BASE body restored)` |
| `generate_username_slug` | restored-helper | `(deleted; restored as a dependency of generate_bot_username)` |
| `generate_username_slug` | restored-def | `(deleted; BASE body restored)` |
| `poll_for_token` | moved-lazy | `plugins.platforms.photon.auth` |
| `poll_for_token` | restored-def | `(deleted; BASE body restored)` |
| `poll_pairing_once` | restored-def | `(deleted; BASE body restored)` |
| `secrets` | restored-import | `secrets` |
| `secrets` | import | `secrets` |
@@ -2070,13 +2075,13 @@ fails CI if it does).
| `resume_cron_job` | moved-lazy | `hermes_cli.web_routers.cron` |
| `reveal_env_var` | moved-lazy | `hermes_cli.web_routers.config_env` |
| `revoke_pairing` | moved-lazy | `hermes_cli.web_routers.ops` |
| `run_backup` | moved-lazy | `hermes_cli.backup` |
| `run_backup` | moved-lazy | `hermes_cli.web_routers.ops` |
| `run_config_migrate` | moved-lazy | `hermes_cli.web_routers.status` |
| `run_curator` | moved-lazy | `hermes_cli.web_routers.status` |
| `run_debug_share_endpoint` | moved-lazy | `hermes_cli.web_routers.status` |
| `run_doctor` | moved-lazy | `hermes_cli.doctor` |
| `run_dump` | moved-lazy | `hermes_cli.dump` |
| `run_import` | moved-lazy | `hermes_cli.backup` |
| `run_import` | moved-lazy | `hermes_cli.web_routers.ops` |
| `run_import_upload` | moved-lazy | `hermes_cli.web_routers.ops` |
| `run_in_threadpool` | unrestorable | `no top-level definition on BASE` |
| `run_prompt_size` | moved-lazy | `hermes_cli.web_routers.status` |
@@ -2106,7 +2111,7 @@ fails CI if it does).
| `shlex` | import | `shlex` |
| `shutil` | import | `shutil` |
| `speak_stream_ws` | moved-lazy | `hermes_cli.web_routers.audio` |
| `speak_text` | moved-lazy | `hermes_cli.voice` |
| `speak_text` | moved-lazy | `hermes_cli.web_routers.audio` |
| `start_gateway` | moved-lazy | `hermes_cli.web_routers.ops` |
| `start_oauth_login` | moved-lazy | `hermes_cli.web_routers.oauth` |
| `start_telegram_onboarding` | moved-lazy | `hermes_cli.web_routers.messaging` |
@@ -2416,11 +2421,11 @@ fails CI if it does).
| name | kind | new location |
|---|---|---|
| `CONCLUDE_SCHEMA` | moved-lazy | `plugins.memory.honcho.tool_schemas` |
| `CONTEXT_SCHEMA` | moved-lazy | `plugins.memory.honcho.tool_schemas` |
| `PROFILE_SCHEMA` | moved-lazy | `plugins.memory.honcho.tool_schemas` |
| `REASONING_SCHEMA` | moved-lazy | `plugins.memory.honcho.tool_schemas` |
| `SEARCH_SCHEMA` | moved-lazy | `plugins.memory.honcho.tool_schemas` |
| `CONCLUDE_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `CONTEXT_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `PROFILE_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `REASONING_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `SEARCH_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `TRIVIAL_PROMPT_RE` | moved-lazy | `agent.memory_provider` |
### `plugins.memory.honcho.client`
@@ -2450,7 +2455,7 @@ fails CI if it does).
|---|---|---|
| `ADD_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `DELETE_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `SEARCH_SCHEMA` | moved-lazy | `plugins.memory.honcho.tool_schemas` |
| `SEARCH_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `UPDATE_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
### `plugins.memory.mem0._setup`
@@ -2463,26 +2468,26 @@ fails CI if it does).
| name | kind | new location |
|---|---|---|
| `CONTEXT_SCHEMA` | moved-lazy | `plugins.memory.honcho.tool_schemas` |
| `CONTEXT_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `Dict` | import | `typing` |
| `FILE_DELETE_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `FILE_INGEST_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `FILE_LIST_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `FILE_READ_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `FILE_UPLOAD_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `FORGET_SCHEMA` | moved-lazy | `plugins.memory.openviking` |
| `FORGET_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `List` | import | `typing` |
| `PROFILE_SCHEMA` | moved-lazy | `plugins.memory.honcho.tool_schemas` |
| `REMEMBER_SCHEMA` | moved-lazy | `plugins.memory.openviking` |
| `SEARCH_SCHEMA` | moved-lazy | `plugins.memory.honcho.tool_schemas` |
| `PROFILE_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `REMEMBER_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `SEARCH_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
### `plugins.memory.supermemory`
| name | kind | new location |
|---|---|---|
| `FORGET_SCHEMA` | moved-lazy | `plugins.memory.openviking` |
| `PROFILE_SCHEMA` | moved-lazy | `plugins.memory.honcho.tool_schemas` |
| `SEARCH_SCHEMA` | moved-lazy | `plugins.memory.honcho.tool_schemas` |
| `FORGET_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `PROFILE_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `SEARCH_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
| `STORE_SCHEMA` | restored-def | `(deleted; BASE body restored)` |
### `plugins.platforms.a2a.protocol`
@@ -2522,7 +2527,7 @@ fails CI if it does).
| name | kind | new location |
|---|---|---|
| `DINGTALK_TYPE_MAPPING` | moved-lazy | `plugins.platforms.dingtalk.inbound` |
| `EXT_MAP` | moved-lazy | `plugins.platforms.dingtalk.inbound` |
| `EXT_MAP` | restored-def | `(deleted; BASE body restored)` |
| `MessageType` | moved-lazy | `gateway.platforms.base` |
### `plugins.platforms.discord.adapter`
@@ -2562,7 +2567,7 @@ fails CI if it does).
| name | kind | new location |
|---|---|---|
| `MAX_MESSAGE_LENGTH` | moved-lazy | `gateway.platforms.signal` |
| `MAX_MESSAGE_LENGTH` | restored-def | `(deleted; BASE body restored)` |
| `PaginationDirection` | unrestorable | `no top-level definition on BASE` |
| `SyncToken` | unrestorable | `no top-level definition on BASE` |
| `_MATRIX_CAPABILITIES` | restored-helper | `(deleted; restored as a dependency of get_matrix_capabilities)` |
@@ -2672,7 +2677,8 @@ fails CI if it does).
| name | kind | new location |
|---|---|---|
| `compact_json` | moved-lazy | `gateway.hosted_rooms_common` |
| `compact_json` | restored-def | `(deleted; BASE body restored)` |
| `json` | restored-import | `json` |
| `json` | import | `json` |
### `plugins.spotify.tools`
@@ -3658,7 +3664,7 @@ fails CI if it does).
| `DEFAULT_OPENAI_VOICE` | moved-lazy | `tools.tts_tool_openai` |
| `DEFAULT_PIPER_VOICE` | moved-lazy | `tools.tts_tool_local` |
| `DEFAULT_XAI_AUTO_SPEECH_TAGS` | moved-lazy | `tools.tts_tool_providers` |
| `DEFAULT_XAI_BASE_URL` | moved-lazy | `tools.xai_http` |
| `DEFAULT_XAI_BASE_URL` | moved-lazy | `tools.tts_tool_providers` |
| `DEFAULT_XAI_BIT_RATE` | moved-lazy | `tools.tts_tool_providers` |
| `DEFAULT_XAI_LANGUAGE` | moved-lazy | `tools.tts_tool_providers` |
| `DEFAULT_XAI_OPTIMIZE_STREAMING_LATENCY_DEFAULT` | moved-lazy | `tools.tts_tool_providers` |
@@ -3670,6 +3676,7 @@ fails CI if it does).
| `DEFAULT_XAI_VOICE_ID` | moved-lazy | `tools.tts_tool_providers` |
| `ELEVENLABS_MODEL_MAX_TEXT_LENGTH` | moved-lazy | `tools.tts_tool_delivery` |
| `FALLBACK_MAX_TEXT_LENGTH` | moved-lazy | `tools.tts_tool_delivery` |
| `FALLBACK_MAX_TEXT_LENGTH` | restored-helper | `(deleted; restored as a dependency of MAX_TEXT_LENGTH)` |
| `Future` | import | `concurrent.futures` |
| `GEMINI_AUDIO_TAG_REWRITE_TASK` | moved-lazy | `tools.tts_tool_providers` |
| `GEMINI_TTS_CHANNELS` | restored-def | `(deleted; BASE body restored)` |
@@ -3677,7 +3684,7 @@ fails CI if it does).
| `GEMINI_TTS_SAMPLE_WIDTH` | restored-def | `(deleted; BASE body restored)` |
| `Iterator` | import | `typing` |
| `MANAGED_OPENAI_TTS_MODELS` | moved-lazy | `tools.tts_tool_openai` |
| `MAX_TEXT_LENGTH` | moved-lazy | `gateway.platforms.bluebubbles` |
| `MAX_TEXT_LENGTH` | restored-def | `(deleted; BASE body restored)` |
| `PROVIDER_MAX_TEXT_LENGTH` | moved-lazy | `tools.tts_tool_delivery` |
| `TTS_RESPONSE_BODY_CHUNK_BYTES` | moved-lazy | `tools.tts_tool_providers` |
| `TTS_RESPONSE_BODY_LIMIT_BYTES` | moved-lazy | `tools.tts_tool_providers` |
@@ -3735,7 +3742,7 @@ fails CI if it does).
| `DEFAULT_TTS_ECHO_SIMILARITY_THRESHOLD` | moved-lazy | `tools.voice_mode_transcript` |
| `DEFAULT_VOICE_STOP_PHRASES` | moved-lazy | `tools.voice_mode_transcript` |
| `MIN_FRAGMENT_LENGTH_FOR_ECHO` | moved-lazy | `tools.voice_mode_transcript` |
| `WHISPER_HALLUCINATIONS` | moved-lazy | `tools.voice_mode_transcript` |
| `WHISPER_HALLUCINATIONS` | restored-def | `(deleted; BASE body restored)` |
| `difflib` | import | `difflib` |
| `is_tts_echo` | moved-lazy | `tools.voice_mode_transcript` |
| `re` | import | `re` |
+3 -12
View File
@@ -266,16 +266,7 @@ _REGISTRY: List[RemovalStep] = [
# Internal code MUST NOT use these (scripts/check_compat_pointers.py fails CI if it does).
# The whole block is removed by reverting the commit that added it.
_PLUGIN_COMPAT_LAZY = {
'register': ('tools.slash_confirm', 'register'),
}
def __getattr__(name): # PEP 562 — lazy so no import cycles
target = _PLUGIN_COMPAT_LAZY.get(name)
if target is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
import importlib
return getattr(importlib.import_module(target[0]), target[1])
def register(step: RemovalStep) -> RemovalStep:
_REGISTRY.append(step)
return step
# ---- END PLUGIN-COMPAT ----
+1 -12
View File
@@ -426,16 +426,5 @@ def render_frames(payload: dict[str, Any], *, cols: int = 80, rows: int = 16, fr
Grid = list # list[Row]
_PLUGIN_COMPAT_LAZY = {
'Run': ('hermes_cli.kanban_db', 'Run'),
}
def __getattr__(name): # PEP 562 — lazy so no import cycles
target = _PLUGIN_COMPAT_LAZY.get(name)
if target is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
import importlib
return getattr(importlib.import_module(target[0]), target[1])
Run = list # [text, style, alpha, hex?]
# ---- END PLUGIN-COMPAT ----
+10 -2
View File
@@ -4604,10 +4604,18 @@ import copy # noqa: F401,E402
from rich import box as rich_box # noqa: F401,E402
import tempfile # noqa: F401,E402
def AIAgent(*args, **kwargs):
from run_agent import AIAgent as _AIAgent
return _AIAgent(*args, **kwargs)
def CanonicalUsage(*args, **kwargs):
from agent.usage_pricing import CanonicalUsage as _CanonicalUsage
return _CanonicalUsage(*args, **kwargs)
_PLUGIN_COMPAT_LAZY = {
'AIAgent': ('run_agent', 'AIAgent'),
'CanonicalUsage': ('agent.usage_pricing', 'CanonicalUsage'),
'DEFAULT_BROWSER_CDP_URL': ('hermes_cli.browser_connect', 'DEFAULT_BROWSER_CDP_URL'),
'HERMES_AGENT_LOGO': ('hermes_cli.banner', 'HERMES_AGENT_LOGO'),
'HERMES_CADUCEUS': ('hermes_cli.banner', 'HERMES_CADUCEUS'),
+76 -64
View File
@@ -719,8 +719,8 @@
{
"facade": "agent.credential_sources",
"name": "register",
"kind": "moved-lazy",
"target": "tools.slash_confirm"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "agent.display",
@@ -809,8 +809,8 @@
{
"facade": "agent.learning_graph_render",
"name": "Run",
"kind": "moved-lazy",
"target": "hermes_cli.kanban_db"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "agent.lsp.eventlog",
@@ -1391,14 +1391,14 @@
{
"facade": "cli",
"name": "AIAgent",
"kind": "moved-lazy",
"target": "run_agent"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "cli",
"name": "CanonicalUsage",
"kind": "moved-lazy",
"target": "agent.usage_pricing"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "cli",
@@ -2008,13 +2008,13 @@
},
{
"facade": "gateway.platforms.qqbot.keyboards",
"name": "Awaitable",
"name": "Callable",
"kind": "restored-import",
"target": "typing"
},
{
"facade": "gateway.platforms.qqbot.keyboards",
"name": "Callable",
"name": "Awaitable",
"kind": "restored-import",
"target": "typing"
},
@@ -3510,13 +3510,13 @@
"facade": "hermes_cli.kanban_db",
"name": "connect",
"kind": "moved-lazy",
"target": "hermes_cli.projects_db"
"target": "hermes_cli.kanban_db_connect"
},
{
"facade": "hermes_cli.kanban_db",
"name": "connect_closing",
"kind": "moved-lazy",
"target": "hermes_cli.projects_db"
"target": "hermes_cli.kanban_db_connect"
},
{
"facade": "hermes_cli.kanban_db",
@@ -4667,8 +4667,8 @@
{
"facade": "hermes_cli.telegram_managed_bot",
"name": "poll_for_token",
"kind": "moved-lazy",
"target": "plugins.platforms.photon.auth"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "hermes_cli.telegram_managed_bot",
@@ -6738,7 +6738,7 @@
"facade": "hermes_cli.web_server",
"name": "run_backup",
"kind": "moved-lazy",
"target": "hermes_cli.backup"
"target": "hermes_cli.web_routers.ops"
},
{
"facade": "hermes_cli.web_server",
@@ -6774,7 +6774,7 @@
"facade": "hermes_cli.web_server",
"name": "run_import",
"kind": "moved-lazy",
"target": "hermes_cli.backup"
"target": "hermes_cli.web_routers.ops"
},
{
"facade": "hermes_cli.web_server",
@@ -6948,7 +6948,7 @@
"facade": "hermes_cli.web_server",
"name": "speak_text",
"kind": "moved-lazy",
"target": "hermes_cli.voice"
"target": "hermes_cli.web_routers.audio"
},
{
"facade": "hermes_cli.web_server",
@@ -7949,32 +7949,32 @@
{
"facade": "plugins.memory.honcho",
"name": "CONCLUDE_SCHEMA",
"kind": "moved-lazy",
"target": "plugins.memory.honcho.tool_schemas"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "plugins.memory.honcho",
"name": "CONTEXT_SCHEMA",
"kind": "moved-lazy",
"target": "plugins.memory.honcho.tool_schemas"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "plugins.memory.honcho",
"name": "PROFILE_SCHEMA",
"kind": "moved-lazy",
"target": "plugins.memory.honcho.tool_schemas"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "plugins.memory.honcho",
"name": "REASONING_SCHEMA",
"kind": "moved-lazy",
"target": "plugins.memory.honcho.tool_schemas"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "plugins.memory.honcho",
"name": "SEARCH_SCHEMA",
"kind": "moved-lazy",
"target": "plugins.memory.honcho.tool_schemas"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "plugins.memory.honcho",
@@ -8033,8 +8033,8 @@
{
"facade": "plugins.memory.mem0",
"name": "SEARCH_SCHEMA",
"kind": "moved-lazy",
"target": "plugins.memory.honcho.tool_schemas"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "plugins.memory.mem0",
@@ -8051,8 +8051,8 @@
{
"facade": "plugins.memory.retaindb",
"name": "CONTEXT_SCHEMA",
"kind": "moved-lazy",
"target": "plugins.memory.honcho.tool_schemas"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "plugins.memory.retaindb",
@@ -8093,8 +8093,8 @@
{
"facade": "plugins.memory.retaindb",
"name": "FORGET_SCHEMA",
"kind": "moved-lazy",
"target": "plugins.memory.openviking"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "plugins.memory.retaindb",
@@ -8105,38 +8105,38 @@
{
"facade": "plugins.memory.retaindb",
"name": "PROFILE_SCHEMA",
"kind": "moved-lazy",
"target": "plugins.memory.honcho.tool_schemas"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "plugins.memory.retaindb",
"name": "REMEMBER_SCHEMA",
"kind": "moved-lazy",
"target": "plugins.memory.openviking"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "plugins.memory.retaindb",
"name": "SEARCH_SCHEMA",
"kind": "moved-lazy",
"target": "plugins.memory.honcho.tool_schemas"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "plugins.memory.supermemory",
"name": "FORGET_SCHEMA",
"kind": "moved-lazy",
"target": "plugins.memory.openviking"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "plugins.memory.supermemory",
"name": "PROFILE_SCHEMA",
"kind": "moved-lazy",
"target": "plugins.memory.honcho.tool_schemas"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "plugins.memory.supermemory",
"name": "SEARCH_SCHEMA",
"kind": "moved-lazy",
"target": "plugins.memory.honcho.tool_schemas"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "plugins.memory.supermemory",
@@ -8255,8 +8255,8 @@
{
"facade": "plugins.platforms.dingtalk.adapter",
"name": "EXT_MAP",
"kind": "moved-lazy",
"target": "plugins.platforms.dingtalk.inbound"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "plugins.platforms.dingtalk.adapter",
@@ -8315,8 +8315,8 @@
{
"facade": "plugins.platforms.matrix.adapter",
"name": "MAX_MESSAGE_LENGTH",
"kind": "moved-lazy",
"target": "gateway.platforms.signal"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "plugins.platforms.matrix.adapter",
@@ -8630,11 +8630,17 @@
"kind": "moved-lazy",
"target": "plugins.spotify.tools"
},
{
"facade": "plugins.spotify.client",
"name": "json",
"kind": "restored-import",
"target": "json"
},
{
"facade": "plugins.spotify.client",
"name": "compact_json",
"kind": "moved-lazy",
"target": "gateway.hosted_rooms_common"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "plugins.spotify.client",
@@ -10724,18 +10730,18 @@
"kind": "moved-lazy",
"target": "tools.skill_manager_guards"
},
{
"facade": "tools.skill_usage",
"name": "tempfile",
"kind": "restored-import",
"target": "tempfile"
},
{
"facade": "tools.skill_usage",
"name": "os",
"kind": "restored-import",
"target": "os"
},
{
"facade": "tools.skill_usage",
"name": "tempfile",
"kind": "restored-import",
"target": "tempfile"
},
{
"facade": "tools.skill_usage",
"name": "_suppressed_file",
@@ -11874,7 +11880,7 @@
"facade": "tools.tts_tool",
"name": "DEFAULT_XAI_BASE_URL",
"kind": "moved-lazy",
"target": "tools.xai_http"
"target": "tools.tts_tool_providers"
},
{
"facade": "tools.tts_tool",
@@ -11984,11 +11990,17 @@
"kind": "moved-lazy",
"target": "tools.tts_tool_openai"
},
{
"facade": "tools.tts_tool",
"name": "FALLBACK_MAX_TEXT_LENGTH",
"kind": "restored-helper",
"target": "(deleted; restored as a dependency of MAX_TEXT_LENGTH)"
},
{
"facade": "tools.tts_tool",
"name": "MAX_TEXT_LENGTH",
"kind": "moved-lazy",
"target": "gateway.platforms.bluebubbles"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "tools.tts_tool",
@@ -12245,8 +12257,8 @@
{
"facade": "tools.voice_mode",
"name": "WHISPER_HALLUCINATIONS",
"kind": "moved-lazy",
"target": "tools.voice_mode_transcript"
"kind": "restored-def",
"target": "(deleted; BASE body restored)"
},
{
"facade": "tools.voice_mode",
@@ -12412,13 +12424,13 @@
},
{
"facade": "tui_gateway.compute_host",
"name": "field",
"name": "dataclass",
"kind": "restored-import",
"target": "dataclasses"
},
{
"facade": "tui_gateway.compute_host",
"name": "dataclass",
"name": "field",
"kind": "restored-import",
"target": "dataclasses"
},
+1 -2
View File
@@ -2803,7 +2803,6 @@ class YuanbaoAdapter(BasePlatformAdapter):
return await SignManager.get_token(self._app_key, self._app_secret, self._api_domain, route_env=self._route_env)
# ---- BEGIN PLUGIN-COMPAT (revert-scheduled; see COMPAT_MANIFEST.md) ----
# Names external plugins imported from this module before the Sep 2026 decomposition.
# Internal code MUST NOT use these (scripts/check_compat_pointers.py fails CI if it does).
@@ -3002,5 +3001,5 @@ async def send_yuanbao_direct(
media_files: Optional[List[Tuple[str, bool]]] = None,
) -> Dict[str, Any]:
"""Delegate to ``OutboundManager.send_direct``."""
return await adapter._outbound.send_direct(chat_id, message, media_files)
return await adapter._outbound.sender.send_direct(chat_id, message, media_files)
# ---- END PLUGIN-COMPAT ----
+2 -2
View File
@@ -4240,8 +4240,8 @@ _PLUGIN_COMPAT_LAZY = {
'check_respawn_guard': ('hermes_cli.kanban_db_dispatch', 'check_respawn_guard'),
'claim_unseen_events_for_sub': ('hermes_cli.kanban_db_notify', 'claim_unseen_events_for_sub'),
'configured_max_in_progress': ('hermes_cli.kanban_db_dispatch', 'configured_max_in_progress'),
'connect': ('hermes_cli.projects_db', 'connect'),
'connect_closing': ('hermes_cli.projects_db', 'connect_closing'),
'connect': ('hermes_cli.kanban_db_connect', 'connect'),
'connect_closing': ('hermes_cli.kanban_db_connect', 'connect_closing'),
'count_notify_subs': ('hermes_cli.kanban_db_notify', 'count_notify_subs'),
'count_running_tasks': ('hermes_cli.kanban_db_dispatch', 'count_running_tasks'),
'count_running_tasks_other_boards': ('hermes_cli.kanban_db_dispatch', 'count_running_tasks_other_boards'),
+10 -13
View File
@@ -258,6 +258,16 @@ def generate_pairing_nonce() -> str:
"""
return secrets.token_hex(16)
def poll_for_token(
api_url: str | None,
pairing: TelegramPairing,
timeout: float = DEFAULT_POLL_TIMEOUT,
interval: float = POLL_INTERVAL,
) -> Optional[str]:
"""Poll the pairing API until the bot token is available or timeout."""
result = poll_for_setup_result(api_url, pairing, timeout=timeout, interval=interval)
return result.token if result else None
def poll_pairing_once(
api_url: str | None,
pairing: TelegramPairing,
@@ -266,17 +276,4 @@ def poll_pairing_once(
"""Poll the onboarding service once. Returns the token when ready."""
result = poll_pairing_result_once(api_url, pairing, timeout=timeout)
return result.token if result else None
_PLUGIN_COMPAT_LAZY = {
'poll_for_token': ('plugins.platforms.photon.auth', 'poll_for_token'),
}
def __getattr__(name): # PEP 562 — lazy so no import cycles
target = _PLUGIN_COMPAT_LAZY.get(name)
if target is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
import importlib
return getattr(importlib.import_module(target[0]), target[1])
# ---- END PLUGIN-COMPAT ----
+3 -3
View File
@@ -1745,13 +1745,13 @@ _PLUGIN_COMPAT_LAZY = {
'resume_cron_job': ('hermes_cli.web_routers.cron', 'resume_cron_job'),
'reveal_env_var': ('hermes_cli.web_routers.config_env', 'reveal_env_var'),
'revoke_pairing': ('hermes_cli.web_routers.ops', 'revoke_pairing'),
'run_backup': ('hermes_cli.backup', 'run_backup'),
'run_backup': ('hermes_cli.web_routers.ops', 'run_backup'),
'run_config_migrate': ('hermes_cli.web_routers.status', 'run_config_migrate'),
'run_curator': ('hermes_cli.web_routers.status', 'run_curator'),
'run_debug_share_endpoint': ('hermes_cli.web_routers.status', 'run_debug_share_endpoint'),
'run_doctor': ('hermes_cli.doctor', 'run_doctor'),
'run_dump': ('hermes_cli.dump', 'run_dump'),
'run_import': ('hermes_cli.backup', 'run_import'),
'run_import': ('hermes_cli.web_routers.ops', 'run_import'),
'run_import_upload': ('hermes_cli.web_routers.ops', 'run_import_upload'),
'run_prompt_size': ('hermes_cli.web_routers.status', 'run_prompt_size'),
'run_security_audit': ('hermes_cli.web_routers.ops', 'run_security_audit'),
@@ -1778,7 +1778,7 @@ _PLUGIN_COMPAT_LAZY = {
'set_webhook_enabled': ('hermes_cli.web_routers.ops', 'set_webhook_enabled'),
'setup_memory_provider': ('hermes_cli.web_routers.memory_providers', 'setup_memory_provider'),
'speak_stream_ws': ('hermes_cli.web_routers.audio', 'speak_stream_ws'),
'speak_text': ('hermes_cli.voice', 'speak_text'),
'speak_text': ('hermes_cli.web_routers.audio', 'speak_text'),
'start_gateway': ('hermes_cli.web_routers.ops', 'start_gateway'),
'start_oauth_login': ('hermes_cli.web_routers.oauth', 'start_oauth_login'),
'start_telegram_onboarding': ('hermes_cli.web_routers.messaging', 'start_telegram_onboarding'),
+193 -5
View File
@@ -810,13 +810,201 @@ def register(ctx) -> None:
# Internal code MUST NOT use these (scripts/check_compat_pointers.py fails CI if it does).
# The whole block is removed by reverting the commit that added it.
CONCLUDE_SCHEMA = {
"name": "honcho_conclude",
"description": (
"Write, delete, or list CONCLUSIONS — persistent, derived facts about a peer that "
"feeds their long-term profile (card + representation). Use this to record "
"something durable you've learned about the peer (a stable preference, a "
"correction, a standing constraint) so future sessions carry it forward. "
"You MUST pass exactly one of `conclusion` (to create), `delete_id` (to "
"delete), or `list` (to list/search); any other combination is an error. "
"A deletion ID is an opaque server-generated string: first call with `list=true` "
"and optionally `query`, then pass the returned ID as `delete_id`. "
"Deletion exists only for "
"PII removal — for merely wrong facts, write a corrected conclusion instead; "
"Honcho self-heals contradictions over time. This is a WRITE tool: to read "
"the profile use honcho_profile / honcho_context, and to search what was "
"said use honcho_search."
),
"parameters": {
"type": "object",
"properties": {
"conclusion": {
"type": "string",
"description": "A factual statement to persist. Provide this when creating a conclusion. Do not send it together with delete_id or list.",
},
"delete_id": {
"type": "string",
"description": "Conclusion ID to delete for PII removal. Provide this when deleting a conclusion. Do not send it together with conclusion or list. Get this id from a prior `list` call — never guess it.",
},
"list": {
"type": "boolean",
"description": "Set to true to list or search stored conclusions (with their ids) instead of creating or deleting one. Do not send together with conclusion or delete_id.",
},
"query": {
"type": "string",
"description": "Optional semantic search query, used only when `list` is true. Omit to list the most recent conclusions instead of searching.",
},
"peer": {
"type": "string",
"description": "The peer the conclusion is ABOUT. Built-in aliases: 'user' (default), 'ai'. Or pass any peer ID from this workspace.",
},
},
"required": [],
},
}
CONTEXT_SCHEMA = {
"name": "honcho_context",
"description": (
"Retrieve the standing SNAPSHOT Honcho holds for the current session — "
"session summary, the peer's representation, the peer card, and the most "
"recent messages — in one call. No query, no LLM synthesis (cheaper than "
"honcho_reasoning). Use it to orient yourself on what Honcho currently "
"knows about this conversation and peer. This is a fixed snapshot, not a "
"search: to look up a specific past fact use honcho_search; to ask a "
"question and get a synthesized answer use honcho_reasoning; for just the "
"compact card use honcho_profile."
),
"parameters": {
"type": "object",
"properties": {
"peer": {
"type": "string",
"description": "Peer to query. Built-in aliases: 'user' (default), 'ai'. Or pass any peer ID from this workspace.",
},
},
"required": [],
},
}
PROFILE_SCHEMA = {
"name": "honcho_profile",
"description": (
"Read or write a peer's CARD — a short, curated list of standing facts "
"about that peer (name, role, preferences, communication style, recurring "
"patterns). This is the cheapest, fastest Honcho call: no query, no LLM, "
"just the current card. Pass `card` to overwrite it; omit `card` to read. "
"An empty read returns a `hint` explaining why (observation disabled, fresh "
"peer, representation still warming up) — that is NOT an error; the card "
"accumulates over time from observed conversation. "
"Related tools: honcho_context for the fuller standing snapshot (card + "
"representation + summary + recent messages); honcho_search to find "
"specific things that were actually said; honcho_reasoning for a "
"synthesized answer to a question."
),
"parameters": {
"type": "object",
"properties": {
"peer": {
"type": "string",
"description": "Peer to query. Built-in aliases: 'user' (default), 'ai'. Or pass any peer ID from this workspace.",
},
"card": {
"type": "array",
"items": {"type": "string"},
"description": "New peer card as a list of fact strings. Omit to read the current card.",
},
},
"required": [],
},
}
REASONING_SCHEMA = {
"name": "honcho_reasoning",
"description": (
"Ask Honcho's dialectic agent a natural-language question about a peer and "
"get back a SYNTHESIZED answer. This is the only Honcho tool that runs an "
"LLM: it agentically searches both raw messages and derived conclusions, "
"reasons over them, and writes a prose answer — so it is the slowest and "
"most expensive call (seconds + tokens). Reach for it for nuanced or "
"open-ended questions ('how does this person prefer to receive feedback?', "
"'what's their relationship to project X?') where you want Honcho to do the "
"synthesis. For a specific fact that was stated, prefer honcho_search "
"(cheap, raw excerpts, you synthesize). For standing profile facts, prefer "
"honcho_profile / honcho_context (no LLM). "
"Pass reasoning_level to control depth: minimal (fast/cheap), low (default), "
"medium, high, max (deep/expensive). Omit for the configured default."
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "A natural language question.",
},
"reasoning_level": {
"type": "string",
"description": (
"Override the default reasoning depth. "
"Omit to use the configured default (typically low).\n"
"reasoning_level parameter guide:\n"
"- minimal: use ONLY for a single quick factual lookup (e.g. "
"'what is the user's name'). Honcho hard-caps this tier's output "
"at 250 tokens combined with the model's own hidden reasoning "
"tokens — a multi-part answer can get cut off mid-thought before "
"it even reaches the final-answer phase, especially on models "
"with reasoning/thinking enabled.\n"
"- low/medium/high/max: use for anything requiring a synthesized, "
"multi-fact, or summary-style answer (e.g. 'summarize known facts "
"about this peer', 'what are their communication preferences'). "
"These tiers have no output-token cap of their own (fall back to "
"Honcho's 8192-token global default), so they don't have "
"minimal's cutoff failure mode.\n"
" - low: straightforward questions with clear answers\n"
" - medium: multi-aspect questions requiring synthesis across observations\n"
" - high: complex behavioral patterns, contradictions, deep analysis\n"
" - max: thorough audit-level analysis, leave no stone unturned\n"
"Default to at least 'low' unless the query is genuinely a single "
"fact lookup."
),
"enum": ["minimal", "low", "medium", "high", "max"],
},
"peer": {
"type": "string",
"description": "Peer to query. Built-in aliases: 'user' (default), 'ai'. Or pass any peer ID from this workspace.",
},
},
"required": ["query"],
},
}
SEARCH_SCHEMA = {
"name": "honcho_search",
"description": (
"Hybrid (semantic + keyword) search over a peer's actual message "
"history across ALL past sessions they took part in — not just the "
"current one. Returns RRF-ranked raw message excerpts (what was "
"literally said, including the assistant's own messages about the "
"peer), no LLM synthesis. Cheaper and faster than honcho_reasoning. "
"Use this to recall specific past facts — 'what did I say about X', "
"'what was the regimen/decision/config we settled on' — and reason "
"over the excerpts yourself. For nuanced questions needing synthesis, "
"use honcho_reasoning instead."
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "What to look for — a topic, keyword, name, or natural-language description of the fact you're trying to recall.",
},
"max_tokens": {
"type": "integer",
"description": "Approximate budget for returned excerpts (default 800, max 2000). Larger budgets return more/longer ranked snippets.",
},
"peer": {
"type": "string",
"description": "Whose history to search. Built-in aliases: 'user' (default), 'ai'. Or pass any peer ID from this workspace. Spans every session that peer took part in.",
},
},
"required": ["query"],
},
}
_PLUGIN_COMPAT_LAZY = {
'CONCLUDE_SCHEMA': ('plugins.memory.honcho.tool_schemas', 'CONCLUDE_SCHEMA'),
'CONTEXT_SCHEMA': ('plugins.memory.honcho.tool_schemas', 'CONTEXT_SCHEMA'),
'PROFILE_SCHEMA': ('plugins.memory.honcho.tool_schemas', 'PROFILE_SCHEMA'),
'REASONING_SCHEMA': ('plugins.memory.honcho.tool_schemas', 'REASONING_SCHEMA'),
'SEARCH_SCHEMA': ('plugins.memory.honcho.tool_schemas', 'SEARCH_SCHEMA'),
'TRIVIAL_PROMPT_RE': ('agent.memory_provider', 'TRIVIAL_PROMPT_RE'),
}
+21 -13
View File
@@ -389,6 +389,27 @@ DELETE_SCHEMA = {
},
}
SEARCH_SCHEMA = {
"name": "mem0_search",
"description": (
"Search the user's memories by meaning; returns facts ranked by "
"relevance. Use this before answering any question that may depend on "
"what you know about the user (preferences, facts, history, people, "
"projects, past decisions). For multi-part or multi-hop questions, "
"call it several times — vary the wording and run follow-up searches "
"on what earlier results reveal; one search is rarely enough."
),
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "What to search for."},
"top_k": {"type": "integer", "description": "Max results (default: 10, max: 50)."},
"rerank": {"type": "boolean", "description": "Rerank results for relevance (default: false, platform mode only)."},
},
"required": ["query"],
},
}
UPDATE_SCHEMA = {
"name": "mem0_update",
"description": (
@@ -405,17 +426,4 @@ UPDATE_SCHEMA = {
"required": ["memory_id", "text"],
},
}
_PLUGIN_COMPAT_LAZY = {
'SEARCH_SCHEMA': ('plugins.memory.honcho.tool_schemas', 'SEARCH_SCHEMA'),
}
def __getattr__(name): # PEP 562 — lazy so no import cycles
target = _PLUGIN_COMPAT_LAZY.get(name)
if target is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
import importlib
return getattr(importlib.import_module(target[0]), target[1])
# ---- END PLUGIN-COMPAT ----
+57 -13
View File
@@ -497,6 +497,18 @@ def register(ctx) -> None:
from typing import Dict # noqa: F401,E402
from typing import List # noqa: F401,E402
CONTEXT_SCHEMA = {
"name": "retaindb_context",
"description": "Synthesized context block — what matters most for the current task, pulled from long-term memory.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Current task or question."},
},
"required": ["query"],
},
}
FILE_DELETE_SCHEMA = {
"name": "retaindb_delete_file",
"description": "Delete a stored file.",
@@ -561,20 +573,52 @@ FILE_UPLOAD_SCHEMA = {
},
}
_PLUGIN_COMPAT_LAZY = {
'CONTEXT_SCHEMA': ('plugins.memory.honcho.tool_schemas', 'CONTEXT_SCHEMA'),
'FORGET_SCHEMA': ('plugins.memory.openviking', 'FORGET_SCHEMA'),
'PROFILE_SCHEMA': ('plugins.memory.honcho.tool_schemas', 'PROFILE_SCHEMA'),
'REMEMBER_SCHEMA': ('plugins.memory.openviking', 'REMEMBER_SCHEMA'),
'SEARCH_SCHEMA': ('plugins.memory.honcho.tool_schemas', 'SEARCH_SCHEMA'),
FORGET_SCHEMA = {
"name": "retaindb_forget",
"description": "Delete a specific memory by ID.",
"parameters": {
"type": "object",
"properties": {
"memory_id": {"type": "string", "description": "Memory ID to delete."},
},
"required": ["memory_id"],
},
}
PROFILE_SCHEMA = {
"name": "retaindb_profile",
"description": "Get the user's stable profile — preferences, facts, and patterns recalled from long-term memory.",
"parameters": {"type": "object", "properties": {}, "required": []},
}
def __getattr__(name): # PEP 562 — lazy so no import cycles
target = _PLUGIN_COMPAT_LAZY.get(name)
if target is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
import importlib
return getattr(importlib.import_module(target[0]), target[1])
REMEMBER_SCHEMA = {
"name": "retaindb_remember",
"description": "Persist an explicit fact, preference, or decision to long-term memory.",
"parameters": {
"type": "object",
"properties": {
"content": {"type": "string", "description": "The fact to remember."},
"memory_type": {
"type": "string",
"enum": ["factual", "preference", "goal", "instruction", "event", "opinion"],
"description": "Category (default: factual).",
},
"importance": {"type": "number", "description": "Importance 0-1 (default: 0.7)."},
},
"required": ["content"],
},
}
SEARCH_SCHEMA = {
"name": "retaindb_search",
"description": "Semantic search across stored memories. Returns ranked results with relevance scores.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "What to search for."},
"top_k": {"type": "integer", "description": "Max results (default: 8, max: 20)."},
},
"required": ["query"],
},
}
# ---- END PLUGIN-COMPAT ----
+36 -15
View File
@@ -543,6 +543,42 @@ def register(ctx):
# Internal code MUST NOT use these (scripts/check_compat_pointers.py fails CI if it does).
# The whole block is removed by reverting the commit that added it.
FORGET_SCHEMA = {
"name": "supermemory_forget",
"description": "Forget a memory by exact id or by best-match query.",
"parameters": {
"type": "object",
"properties": {
"id": {"type": "string", "description": "Exact memory id to delete."},
"query": {"type": "string", "description": "Query used to find the memory to forget."},
},
},
}
PROFILE_SCHEMA = {
"name": "supermemory_profile",
"description": "Retrieve persistent profile facts and recent memory context.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Optional query to focus the profile response."},
},
},
}
SEARCH_SCHEMA = {
"name": "supermemory_search",
"description": "Search long-term memory by semantic similarity.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "What to search for."},
"limit": {"type": "integer", "description": "Maximum results to return, 1 to 20."},
},
"required": ["query"],
},
}
STORE_SCHEMA = {
"name": "supermemory_store",
"description": "Store an explicit memory for future recall.",
@@ -555,19 +591,4 @@ STORE_SCHEMA = {
"required": ["content"],
},
}
_PLUGIN_COMPAT_LAZY = {
'FORGET_SCHEMA': ('plugins.memory.openviking', 'FORGET_SCHEMA'),
'PROFILE_SCHEMA': ('plugins.memory.honcho.tool_schemas', 'PROFILE_SCHEMA'),
'SEARCH_SCHEMA': ('plugins.memory.honcho.tool_schemas', 'SEARCH_SCHEMA'),
}
def __getattr__(name): # PEP 562 — lazy so no import cycles
target = _PLUGIN_COMPAT_LAZY.get(name)
if target is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
import importlib
return getattr(importlib.import_module(target[0]), target[1])
# ---- END PLUGIN-COMPAT ----
+18 -1
View File
@@ -743,10 +743,27 @@ def register(ctx) -> None:
# Internal code MUST NOT use these (scripts/check_compat_pointers.py fails CI if it does).
# The whole block is removed by reverting the commit that added it.
EXT_MAP = {
"pdf": "application/pdf",
"png": "image/png",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"gif": "image/gif",
"webp": "image/webp",
"doc": "application/msword",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"xls": "application/vnd.ms-excel",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"md": "text/markdown",
"txt": "text/plain",
"csv": "text/csv",
"zip": "application/zip",
"mp4": "video/mp4",
}
_PLUGIN_COMPAT_LAZY = {
'DINGTALK_TYPE_MAPPING': ('plugins.platforms.dingtalk.inbound', 'DINGTALK_TYPE_MAPPING'),
'EXT_MAP': ('plugins.platforms.dingtalk.inbound', 'EXT_MAP'),
'MessageType': ('gateway.platforms.base', 'MessageType'),
}
+2 -13
View File
@@ -3017,6 +3017,8 @@ def register(ctx) -> None:
# Internal code MUST NOT use these (scripts/check_compat_pointers.py fails CI if it does).
# The whole block is removed by reverting the commit that added it.
MAX_MESSAGE_LENGTH = DEFAULT_MAX_MESSAGE_LENGTH
_MATRIX_CAPABILITIES: Dict[str, str] = {
"text": "yes",
"threads": "yes",
@@ -3036,17 +3038,4 @@ _MATRIX_CAPABILITIES: Dict[str, str] = {
def get_matrix_capabilities() -> Dict[str, str]:
"""Return Matrix gateway capabilities for docs and release checks."""
return dict(_MATRIX_CAPABILITIES)
_PLUGIN_COMPAT_LAZY = {
'MAX_MESSAGE_LENGTH': ('gateway.platforms.signal', 'MAX_MESSAGE_LENGTH'),
}
def __getattr__(name): # PEP 562 — lazy so no import cycles
target = _PLUGIN_COMPAT_LAZY.get(name)
if target is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
import importlib
return getattr(importlib.import_module(target[0]), target[1])
# ---- END PLUGIN-COMPAT ----
+3 -12
View File
@@ -166,17 +166,8 @@ def normalize_spotify_uris(values: Iterable[str], expected_type: Optional[str] =
# Internal code MUST NOT use these (scripts/check_compat_pointers.py fails CI if it does).
# The whole block is removed by reverting the commit that added it.
import json # noqa: F401,E402
import json # noqa: F401,E402
_PLUGIN_COMPAT_LAZY = {
'compact_json': ('gateway.hosted_rooms_common', 'compact_json'),
}
def __getattr__(name): # PEP 562 — lazy so no import cycles
target = _PLUGIN_COMPAT_LAZY.get(name)
if target is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
import importlib
return getattr(importlib.import_module(target[0]), target[1])
def compact_json(data: Any) -> str:
return json.dumps(data, ensure_ascii=False)
# ---- END PLUGIN-COMPAT ----
+3
View File
@@ -31,6 +31,9 @@ def _py_files():
parts = p.relative_to(ROOT).parts
if parts[0] in SKIP_DIRS or p.name == "check_compat_pointers.py":
continue
# The compat layer's own contract test uses the pointers on purpose; it is deleted with them.
if p.name == "test_compat_manifest_targets.py":
continue
yield p
+27 -11
View File
@@ -24,6 +24,7 @@ violation (does not modify files).
from __future__ import annotations
import ast
import os
import re
import sys
@@ -89,26 +90,41 @@ _SPLAT_RE = re.compile(r"\*\*\s*([A-Za-z_][A-Za-z0-9_]*)")
def _splat_carries_stdin(call_text: str, content: str) -> bool:
"""True when the call splats ``**name`` / ``**name(...)`` and ``name`` is defined in
the same file (assignment or ``def``) with an explicit ``stdin=`` in its body.
the same file (assignment or ``def``) whose OWN expression/body sets ``stdin=``.
Shared kwargs helpers (``_RUN_KW = dict(..., stdin=DEVNULL)``, ``def _run_kwargs(): return
dict(..., stdin=DEVNULL)``) legitimately carry the guard; we only accept them when the
definition provably sets stdin= — never on the helper's name alone.
definition provably sets stdin= — never on the helper's name alone, and never because an
unrelated later call in the file happens to pass ``stdin=``.
"""
names = set(_SPLAT_RE.findall(call_text))
if not names:
return False
try:
tree = ast.parse(content)
except SyntaxError:
return False
for name in names:
defn = re.compile(
rf"^[ \t]*(?:def[ \t]+{re.escape(name)}[ \t]*\(|{re.escape(name)}[ \t]*(?::[^=\n]*)?=(?!=))",
re.MULTILINE,
)
m = defn.search(content)
if m is None:
node = None
for n in ast.walk(tree):
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) and n.name == name:
node = n
break
if isinstance(n, (ast.Assign, ast.AnnAssign)):
targets = n.targets if isinstance(n, ast.Assign) else [n.target]
if any(isinstance(t, ast.Name) and t.id == name for t in targets):
node = n.value if n.value is not None else n
break
if node is None:
return False
body = content[m.start():]
body = "\n".join(body.split("\n")[:30])
if "stdin=" not in body:
# stdin appears as a keyword (dict(stdin=...)) or as a dict-literal key ({"stdin": ...})
# somewhere INSIDE this definition — not merely nearby in the file.
has = any(
(isinstance(sub, ast.keyword) and sub.arg == "stdin")
or (isinstance(sub, ast.Constant) and sub.value == "stdin")
for sub in ast.walk(node)
)
if not has:
return False
return True
@@ -0,0 +1,37 @@
"""MoA display events honour the ``-Q`` quiet contract.
``-Q`` (machine-readable CLI output) nulls ``agent.tool_progress_callback`` and sets
``tool_progress_mode = "off"``; the MoA reference relay reads the callback at emit time, so
quiet sessions must emit nothing and interactive sessions must relay every event.
"""
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
import pytest
from agent.moa_loop import build_moa_facade
def _facade(agent):
with patch("agent.moa_loop.MoAClient") as client_cls:
build_moa_facade(agent, "default")
return client_cls.call_args.kwargs["reference_callback"]
def test_quiet_cli_emits_no_moa_display_events():
agent = SimpleNamespace(platform="cli", tool_progress_mode="off", tool_progress_callback=None, provider="moa", model="default")
relay = _facade(agent)
relay("moa.reference", label="m1", text="answer", index=0, count=2) # must not raise
relay("moa.aggregating", aggregator="agg")
@pytest.mark.parametrize("platform", ["cli", "telegram"])
def test_interactive_surfaces_receive_moa_events(platform):
cb = MagicMock()
agent = SimpleNamespace(platform=platform, tool_progress_mode="all", tool_progress_callback=cb, provider="moa", model="default")
relay = _facade(agent)
relay("moa.reference", label="m1", text="answer", index=0, count=2)
relay("moa.aggregating", aggregator="agg")
events = [c.args[0] for c in cb.call_args_list]
assert events == ["moa.reference", "moa.aggregating"]
assert cb.call_args_list[0].kwargs == {"moa_index": 0, "moa_count": 2}
+88
View File
@@ -0,0 +1,88 @@
"""Plugin-compat pointers must resolve to the SAME object the name moved to, never a same-named stranger.
Regression: ``hermes_cli.kanban_db.connect`` was pointed at ``hermes_cli.projects_db.connect`` (a different
database, no ``board=`` parameter) because the generator ranked candidate homes by path proximity. The
manifest codified the mistake, so the compat lint treated it as valid.
Invariant checked here: for every ``moved-lazy`` entry whose target module also exists in the manifest of
some other facade under the same name, or whose facade stem has a sibling ``<stem>_*`` module defining the
name, the facade attribute IS the sibling's object.
"""
import importlib
import json
import pkgutil
import sqlite3
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
MANIFEST = ROOT / "compat_manifest.json"
pytestmark = pytest.mark.skipif(not MANIFEST.exists(), reason="compat layer removed (scheduled revert)")
def _entries():
return [e for e in json.loads(MANIFEST.read_text())["entries"] if e["kind"] == "moved-lazy"]
def _sibling_modules(facade: str) -> list[str]:
pkg, _, stem = facade.rpartition(".")
try:
parent = importlib.import_module(pkg) if pkg else None
except Exception:
return []
paths = getattr(parent, "__path__", None) if parent else [str(ROOT)]
if not paths:
return []
prefix = f"{pkg}." if pkg else ""
return [prefix + m.name for m in pkgutil.iter_modules(paths) if m.name.startswith(stem + "_")]
def test_moved_lazy_pointers_resolve_to_the_split_off_siblings_object():
"""When a facade's own ``<stem>_*`` sibling binds the name, the facade attribute must be THAT object.
A sibling may legitimately re-import the value from elsewhere (then the pointer target is the origin and
the objects are identical); what must never happen is the pointer resolving to a same-named stranger.
"""
bad = []
for e in _entries():
facade, name = e["facade"], e["name"]
sibs = _sibling_modules(facade)
if not sibs:
continue
try:
got = getattr(importlib.import_module(facade), name)
except Exception as exc: # unresolvable pointer is its own failure
bad.append((facade, name, f"unresolvable: {exc!r}"))
continue
for s in sibs:
try:
mod = importlib.import_module(s)
except Exception:
continue
if name in vars(mod):
sib_obj = vars(mod)[name]
same = (sib_obj == got) if isinstance(got, (int, float, str, bytes, bool, type(None))) else (sib_obj is got)
if not same:
bad.append((facade, name, e["target"], s))
assert not bad, f"compat pointers resolve to a different object than the facade's own sibling binds: {bad}"
def test_kanban_db_connect_opens_a_kanban_board(tmp_path, monkeypatch):
"""The historical ``kanban_db.connect(board=...)`` opens a Kanban DB, not projects.db."""
import hermes_cli.kanban_db as kanban_db
import hermes_cli.kanban_db_connect as kanban_db_connect
assert kanban_db.connect is kanban_db_connect.connect
assert kanban_db.connect_closing is kanban_db_connect.connect_closing
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
db = tmp_path / "board.db"
conn = kanban_db.connect(db, board="qa")
try:
tables = {r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")}
finally:
conn.close()
assert "tasks" in tables, tables
assert not (tmp_path / "projects.db").exists()
assert isinstance(sqlite3.connect(db), sqlite3.Connection)
@@ -104,7 +104,16 @@ def test_splatted_kwargs_helper_counts_only_when_it_sets_stdin():
"subprocess.run(['ls'], **_KW)\n"
)
undefined = "import subprocess\nsubprocess.run(['ls'], **_KW)\n"
# A later unrelated call passing stdin= must NOT vouch for the splat (reviewer-found false negative
# in the 30-line text-window version).
unrelated_later = (
"import subprocess\n"
"kwargs = {'capture_output': True}\n"
"subprocess.run(['child'], **kwargs)\n"
"subprocess.run(['other'], stdin=subprocess.DEVNULL)\n"
)
assert guard.find_subprocess_calls(safe_const, "x.py") == []
assert guard.find_subprocess_calls(safe_fn, "x.py") == []
assert len(guard.find_subprocess_calls(unsafe_const, "x.py")) == 1
assert len(guard.find_subprocess_calls(undefined, "x.py")) == 1
assert [v["line"] for v in guard.find_subprocess_calls(unrelated_later, "x.py")] == [3]
+5 -2
View File
@@ -604,6 +604,10 @@ GEMINI_TTS_SAMPLE_RATE = 24000
GEMINI_TTS_SAMPLE_WIDTH = 2 # 16-bit PCM (L16)
FALLBACK_MAX_TEXT_LENGTH = 4000
MAX_TEXT_LENGTH = FALLBACK_MAX_TEXT_LENGTH
_PLUGIN_COMPAT_LAZY = {
'AudioDeliveryProfile': ('tools.tts_tool_delivery', 'AudioDeliveryProfile'),
@@ -633,7 +637,7 @@ _PLUGIN_COMPAT_LAZY = {
'DEFAULT_OPENAI_VOICE': ('tools.tts_tool_openai', 'DEFAULT_OPENAI_VOICE'),
'DEFAULT_PIPER_VOICE': ('tools.tts_tool_local', 'DEFAULT_PIPER_VOICE'),
'DEFAULT_XAI_AUTO_SPEECH_TAGS': ('tools.tts_tool_providers', 'DEFAULT_XAI_AUTO_SPEECH_TAGS'),
'DEFAULT_XAI_BASE_URL': ('tools.xai_http', 'DEFAULT_XAI_BASE_URL'),
'DEFAULT_XAI_BASE_URL': ('tools.tts_tool_providers', 'DEFAULT_XAI_BASE_URL'),
'DEFAULT_XAI_BIT_RATE': ('tools.tts_tool_providers', 'DEFAULT_XAI_BIT_RATE'),
'DEFAULT_XAI_LANGUAGE': ('tools.tts_tool_providers', 'DEFAULT_XAI_LANGUAGE'),
'DEFAULT_XAI_OPTIMIZE_STREAMING_LATENCY_DEFAULT': ('tools.tts_tool_providers', 'DEFAULT_XAI_OPTIMIZE_STREAMING_LATENCY_DEFAULT'),
@@ -647,7 +651,6 @@ _PLUGIN_COMPAT_LAZY = {
'FALLBACK_MAX_TEXT_LENGTH': ('tools.tts_tool_delivery', 'FALLBACK_MAX_TEXT_LENGTH'),
'GEMINI_AUDIO_TAG_REWRITE_TASK': ('tools.tts_tool_providers', 'GEMINI_AUDIO_TAG_REWRITE_TASK'),
'MANAGED_OPENAI_TTS_MODELS': ('tools.tts_tool_openai', 'MANAGED_OPENAI_TTS_MODELS'),
'MAX_TEXT_LENGTH': ('gateway.platforms.bluebubbles', 'MAX_TEXT_LENGTH'),
'PROVIDER_MAX_TEXT_LENGTH': ('tools.tts_tool_delivery', 'PROVIDER_MAX_TEXT_LENGTH'),
'TTS_RESPONSE_BODY_CHUNK_BYTES': ('tools.tts_tool_providers', 'TTS_RESPONSE_BODY_CHUNK_BYTES'),
'TTS_RESPONSE_BODY_LIMIT_BYTES': ('tools.tts_tool_providers', 'TTS_RESPONSE_BODY_LIMIT_BYTES'),
+30 -1
View File
@@ -1521,12 +1521,41 @@ def cleanup_temp_recordings(max_age_seconds: int = 3600) -> int:
import difflib # noqa: F401,E402
import re # noqa: F401,E402
WHISPER_HALLUCINATIONS = {
"thank you.",
"thank you",
"thanks for watching.",
"thanks for watching",
"subscribe to my channel.",
"subscribe to my channel",
"like and subscribe.",
"like and subscribe",
"please subscribe.",
"please subscribe",
"thank you for watching.",
"thank you for watching",
"bye.",
"bye",
"you",
"the end.",
"the end",
# Non-English hallucinations (common on silence)
"продолжение следует",
"продолжение следует...",
"sous-titres",
"sous-titres réalisés par la communauté d'amara.org",
"sottotitoli creati dalla comunità amara.org",
"untertitel von stephanie geiges",
"amara.org",
"www.mooji.org",
"ご視聴ありがとうございました",
}
_PLUGIN_COMPAT_LAZY = {
'DEFAULT_TTS_ECHO_SIMILARITY_THRESHOLD': ('tools.voice_mode_transcript', 'DEFAULT_TTS_ECHO_SIMILARITY_THRESHOLD'),
'DEFAULT_VOICE_STOP_PHRASES': ('tools.voice_mode_transcript', 'DEFAULT_VOICE_STOP_PHRASES'),
'MIN_FRAGMENT_LENGTH_FOR_ECHO': ('tools.voice_mode_transcript', 'MIN_FRAGMENT_LENGTH_FOR_ECHO'),
'WHISPER_HALLUCINATIONS': ('tools.voice_mode_transcript', 'WHISPER_HALLUCINATIONS'),
'is_tts_echo': ('tools.voice_mode_transcript', 'is_tts_echo'),
'voice_stop_hint': ('tools.voice_mode_transcript', 'voice_stop_hint'),
}