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:
@@ -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
@@ -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` |
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
@@ -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
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'),
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
@@ -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'),
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
@@ -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 ----
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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}
|
||||
@@ -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
@@ -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
@@ -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'),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user