diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bae4299b1b..7a49a62203 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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 diff --git a/COMPAT_MANIFEST.md b/COMPAT_MANIFEST.md index 8d9abc25b0..96ff4c2e1c 100644 --- a/COMPAT_MANIFEST.md +++ b/COMPAT_MANIFEST.md @@ -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` | diff --git a/agent/credential_sources.py b/agent/credential_sources.py index 1dc2947700..ba90e312f1 100644 --- a/agent/credential_sources.py +++ b/agent/credential_sources.py @@ -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 ---- diff --git a/agent/learning_graph_render.py b/agent/learning_graph_render.py index 8200680c8d..7bbed7d237 100644 --- a/agent/learning_graph_render.py +++ b/agent/learning_graph_render.py @@ -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 ---- diff --git a/cli.py b/cli.py index 240f795b4b..2d79f53d0e 100644 --- a/cli.py +++ b/cli.py @@ -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'), diff --git a/compat_manifest.json b/compat_manifest.json index 724d740760..e663cd0ff6 100644 --- a/compat_manifest.json +++ b/compat_manifest.json @@ -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" }, diff --git a/gateway/platforms/yuanbao.py b/gateway/platforms/yuanbao.py index 02093b501d..dc49137580 100644 --- a/gateway/platforms/yuanbao.py +++ b/gateway/platforms/yuanbao.py @@ -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 ---- diff --git a/hermes_cli/kanban_db.py b/hermes_cli/kanban_db.py index ea33f297cf..f718545c91 100644 --- a/hermes_cli/kanban_db.py +++ b/hermes_cli/kanban_db.py @@ -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'), diff --git a/hermes_cli/telegram_managed_bot.py b/hermes_cli/telegram_managed_bot.py index c124f49e85..fa505c3de7 100644 --- a/hermes_cli/telegram_managed_bot.py +++ b/hermes_cli/telegram_managed_bot.py @@ -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 ---- diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index d5fd2a3e13..216746302e 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -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'), diff --git a/plugins/memory/honcho/__init__.py b/plugins/memory/honcho/__init__.py index bfac07824c..309fde2562 100644 --- a/plugins/memory/honcho/__init__.py +++ b/plugins/memory/honcho/__init__.py @@ -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'), } diff --git a/plugins/memory/mem0/__init__.py b/plugins/memory/mem0/__init__.py index ce208c1641..f45773ec5e 100644 --- a/plugins/memory/mem0/__init__.py +++ b/plugins/memory/mem0/__init__.py @@ -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 ---- diff --git a/plugins/memory/retaindb/__init__.py b/plugins/memory/retaindb/__init__.py index cf4b1f4be4..95ed0ded36 100644 --- a/plugins/memory/retaindb/__init__.py +++ b/plugins/memory/retaindb/__init__.py @@ -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 ---- diff --git a/plugins/memory/supermemory/__init__.py b/plugins/memory/supermemory/__init__.py index a5939722ae..ee6a1c8717 100644 --- a/plugins/memory/supermemory/__init__.py +++ b/plugins/memory/supermemory/__init__.py @@ -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 ---- diff --git a/plugins/platforms/dingtalk/adapter.py b/plugins/platforms/dingtalk/adapter.py index 5f5ef7035d..f0ea6abf62 100644 --- a/plugins/platforms/dingtalk/adapter.py +++ b/plugins/platforms/dingtalk/adapter.py @@ -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'), } diff --git a/plugins/platforms/matrix/adapter.py b/plugins/platforms/matrix/adapter.py index 6ba4a36cda..f9874e5ead 100644 --- a/plugins/platforms/matrix/adapter.py +++ b/plugins/platforms/matrix/adapter.py @@ -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 ---- diff --git a/plugins/spotify/client.py b/plugins/spotify/client.py index b747d95324..22bd836ea0 100644 --- a/plugins/spotify/client.py +++ b/plugins/spotify/client.py @@ -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 ---- diff --git a/scripts/check_compat_pointers.py b/scripts/check_compat_pointers.py index 2fa5966da3..c36bea08ec 100644 --- a/scripts/check_compat_pointers.py +++ b/scripts/check_compat_pointers.py @@ -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 diff --git a/scripts/check_subprocess_stdin.py b/scripts/check_subprocess_stdin.py index e35bcda295..94a768a182 100644 --- a/scripts/check_subprocess_stdin.py +++ b/scripts/check_subprocess_stdin.py @@ -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 diff --git a/tests/agent/test_moa_quiet_reference_output.py b/tests/agent/test_moa_quiet_reference_output.py new file mode 100644 index 0000000000..5e9dcf6288 --- /dev/null +++ b/tests/agent/test_moa_quiet_reference_output.py @@ -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} diff --git a/tests/test_compat_manifest_targets.py b/tests/test_compat_manifest_targets.py new file mode 100644 index 0000000000..e72396f457 --- /dev/null +++ b/tests/test_compat_manifest_targets.py @@ -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 ``_*`` 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 ``_*`` 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) diff --git a/tests/tools/test_subprocess_stdin_guard.py b/tests/tools/test_subprocess_stdin_guard.py index 1c2d284021..15f7482e5d 100644 --- a/tests/tools/test_subprocess_stdin_guard.py +++ b/tests/tools/test_subprocess_stdin_guard.py @@ -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] diff --git a/tools/tts_tool.py b/tools/tts_tool.py index 2c37e43596..6cf486826a 100644 --- a/tools/tts_tool.py +++ b/tools/tts_tool.py @@ -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'), diff --git a/tools/voice_mode.py b/tools/voice_mode.py index f0f4d0cc20..05014bd7a1 100644 --- a/tools/voice_mode.py +++ b/tools/voice_mode.py @@ -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'), }