mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
design: compression eval harness — add three scrubbed fixtures + scrubber
Adds scripts/compression_eval/ with a design doc, README, a placeholder run_eval.py, and three checked-in scrubbed session fixtures. No working eval yet — PR is for design review before implementation. Motivation: we edit agent/context_compressor.py prompts and _template_sections by hand and ship without any automated check that compression still preserves file paths, error codes, or the active task. Factory.ai's Dec 2025 write-up (https://factory.ai/news/evaluating-compression) documents a probe-based eval scored on six dimensions. We adopt the methodology; we do not publish scores. Contents: - DESIGN.md — fixture format, probe format (recall / artifact / continuation / decision), six grading dimensions, report format, cost expectations, scrubber pipeline, open questions, and staged follow-up PR plan. - README.md — short 'what this is / when to run it' page. - run_eval.py — placeholder that prints 'not implemented, see DESIGN.md' and exits 1. - scrub_fixtures.py — reproducible pipeline that converts real sessions from ~/.hermes/sessions/*.jsonl into public-safe JSON fixtures. Applies: redact_sensitive_text, username path normalization, personal handle scrubbing, email and git-author normalization, reasoning scratchpad / <think> stripping, platform user-mention scrubbing, first-user paraphrase, system-prompt placeholder, orphan-message pruning, and tool-output size truncation for fixture readability. - fixtures/feature-impl-context-priority.json — 75 msgs / ~17k tokens. Investigate → patch → test → PR → merge. - fixtures/debug-session-feishu-id-model.json — 59 msgs / ~13k tokens. PR triage + upstream docs + decision. - fixtures/config-build-competitive-scouts.json — 61 msgs / ~23k tokens. Iterative config accumulation (11 cron jobs across 7 weekdays). PII audit: zero matches across the three fixtures for the maintainer's handle (all case variants), personal email domains, and known contributor emails. Only 'contributor@example.com' placeholder remains. Why scripts/: requires API credentials, costs ~\$1 per run, LLM-graded (non-deterministic), must not run in CI. scripts/sample_and_compress.py is the existing precedent for offline credentialed tooling.
This commit is contained in:
parent
c6b734e24d
commit
9f5c13f874
10 changed files with 2642 additions and 0 deletions
|
|
@ -0,0 +1,626 @@
|
|||
{
|
||||
"name": "debug-session-feishu-id-model",
|
||||
"description": "~60-turn debug/triage PR-review session: a third-party bug report says the gateway's Feishu adapter misuses the open_id / union_id / user_id identity model (open_id is app-scoped, not the bot's canonical ID). An open community PR (#8388) tries to fix it. Agent reviews the PR against current main, fetches upstream Feishu/Lark identity docs, and produces a decision. Exercises long tool-heavy context with PR diffs, upstream docs, and a clear decision at the end — the classic 'can the summary still name the PR number, the root cause, and the decision?' scenario.",
|
||||
"model": "anthropic/claude-sonnet-4.6",
|
||||
"context_length": 200000,
|
||||
"source": "~/.hermes/sessions/20260412_233741_3f2119a8.jsonl",
|
||||
"truncated_to": 58,
|
||||
"scrubbed_at": "2026-04-24T07:40:21Z",
|
||||
"scrubbing_passes": [
|
||||
"redact_sensitive_text (agent.redact)",
|
||||
"username paths replaced with /home/user",
|
||||
"personal handles (all case variants of the maintainer name) replaced with 'user'",
|
||||
"email addresses replaced with contributor@example.com",
|
||||
"git 'Author: Name <addr>' header lines normalised",
|
||||
"reasoning scratchpad blocks stripped from assistant content",
|
||||
"think tag blocks stripped from assistant content",
|
||||
"messaging-platform user mentions replaced with <@user>",
|
||||
"first user message paraphrased to remove personal voice",
|
||||
"subsequent user messages kept verbatim (after above redactions)",
|
||||
"system prompt replaced with generic public-safe placeholder",
|
||||
"orphan empty-assistant messages and trailing tool messages dropped",
|
||||
"tool outputs longer than 2000 chars truncated with a note"
|
||||
],
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful AI coding assistant with access to tools (terminal, file editing, search, web, etc.). You operate in a conversational loop: the user gives you a task, you call tools to accomplish it, and you report back concisely."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "A community user reports the Feishu/Lark adapter gets the identity model wrong — open_id is app-scoped, not the bot's canonical ID. There's an open PR #8388 trying to fix it. Use the hermes-agent-dev skill and the pr-triage-salvage skill to review it."
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "\n\nInteresting. Let me pull up that PR and take a look at what they're proposing.",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_01DhZUXFbRmZecEXrKGbG3p1",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "skill_view",
|
||||
"arguments": "{\"name\": \"pr-triage-salvage\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toolu_bdrk_01NxU3C2z6gbGvXHiG3umKmQ",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd ~/repos/hermes-agent && git fetch origin pull/8388/head:pr-8388 2>&1 | tail -5\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"success\": true, \"name\": \"pr-triage-salvage\", \"description\": \"Triage external PRs — review, cherry-pick/salvage valid fixes onto main, close broken/redundant ones. Handles stale PRs, mixed-quality changes, and contributor attribution.\", \"tags\": [\"github\", \"pr\", \"triage\", \"salvage\", \"cherry-pick\", \"code-review\"], \"related_skills\": [], \"content\": \"---\\nname: pr-triage-salvage\\ndescription: Triage external PRs — review, cherry-pick/salvage valid fixes onto main, close broken/redundant ones. Handles stale PRs, mixed-quality changes, and contributor attribution.\\ntags: [github, pr, triage, salvage, cherry-pick, code-review]\\ntriggers:\\n - triaging PRs\\n - salvaging a PR\\n - reviewing community contributions\\n - cherry-picking external fixes\\n - mega-PR with many bundled features\\n - large community PR review\\n---\\n\\n# PR Triage & Salvage Workflow\\n\\nSystematic process for reviewing external PRs, salvaging valid fixes, closing broken/redundant ones.\\n\\n## Workflow\\n\\n### 1. Assess the PR\\n\\n```bash\\ngh pr view <NUMBER>\\ngh pr diff <NUMBER>\\n# Check staleness\\ngit fetch origin pull/<NUMBER>/head:pr-<NUMBER>\\ngit rev-list --count $(git merge-base origin/main pr-<NUMBER>)..origin/main\\n```\\n\\n**Batch triage (30+ PRs):** Bucket by similarity, flag dupes. Pick most complete.\\n\\n**Mega-PR decomp:** Cherry-pick if commits grouped, reimplement if interleaved.\\n\\n**Conflicting PRs:** Build combined fix from scratch on main; credit all. **Partial fix on main:** Before salvaging, check\n\n[Truncated: tool response was 102,838 chars. Full output could not be saved to sandbox.]",
|
||||
"tool_call_id": "toolu_bdrk_01DhZUXFbRmZecEXrKGbG3p1"
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"/usr/bin/bash: line 3: cd: /home/user/repos/hermes-agent: No such file or directory\", \"exit_code\": 1, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_01NxU3C2z6gbGvXHiG3umKmQ"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_01W6fYofRYbMWf294dDumDt5",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"find /home -maxdepth 4 -name \\\"hermes-agent\\\" -type d 2>/dev/null; find / -maxdepth 4 -name \\\"hermes-agent\\\" -type d 2>/dev/null | head -5\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"/home/user/.hermes/hermes-agent\\n/home/user/.hermes/hermes-agent\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_01W6fYofRYbMWf294dDumDt5"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_01FYdZ4867K7kL78kcQfgez3",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git fetch origin pull/8388/head:pr-8388 2>&1 | tail -5\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"From github.com:NousResearch/hermes-agent\\n * [new ref] refs/pull/8388/head -> pr-8388\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_01FYdZ4867K7kL78kcQfgez3"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_01K3yrYegnpxP8nDf8gvsSzP",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && gh pr view 8388 2>&1\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"title:\\tfix(feishu): correct Feishu identity model and enable multi-account routing\\nstate:\\tOPEN\\nauthor:\\thchen13\\nlabels:\\t\\nassignees:\\t\\nreviewers:\\t\\nprojects:\\t\\nmilestone:\\t\\nnumber:\\t8388\\nurl:\\thttps://github.com/NousResearch/hermes-agent/pull/8388\\nadditions:\\t4647\\ndeletions:\\t239\\nauto-merge:\\tdisabled\\n--\\n## What this PR fixes\\n\\nThis PR corrects Hermes' Feishu identity model.\\n\\nThe core bug is that Hermes previously treated Feishu IDs with the wrong mental model, especially around bot identity versus human identity. In practice this caused Hermes to behave as if Feishu were effectively a single-bot platform and led to incorrect routing assumptions in group mentions, home-channel delivery, and cross-account usage.\\n\\nThe most important correction is this:\\n\\n- `open_id` is **not** the canonical identity of a Feishu bot.\\n- A Feishu bot/app is identified by its configured `app_id`.\\n- For human users, `user_id` is the stable tenant-scoped identifier.\\n- `open_id` is an app-scoped alias for a human user, so the same person can have different `open_id` values under different bot namespaces.\\n\\nThat identity-model mismatch is the root bug. This PR fixes that model first, then builds the routing and tooling changes on top of it.\\n\\n## What follows from that fix\\n\\nOnce Hermes uses the correct Feishu identity model, a few capabilities naturally become possible or necessary:\\n\\n- multi-account Feishu support under one Hermes agent\\n- account-aware session routing\\n- account-aware outbound delivery\\n- correct group-mention handling against the receiving bot account\\n- agent-side Feishu identity/chat inspection via `feishu_id`\\n\\nSo multi-account and ID-aware delivery are not the primary story by themselves; they are the downstream capability expansio\n\n[... tool output truncated for fixture — original was 4163 chars]",
|
||||
"tool_call_id": "toolu_bdrk_01K3yrYegnpxP8nDf8gvsSzP"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "4647 additions for a \"bug fix\" - that's a feature PR wearing a bugfix hat. Let me dig into the actual diff.",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_01C9hZc9tuYcb8juESP7WkU2",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 --stat 2>&1\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"cron/scheduler.py | 140 +-\\n gateway/config.py | 276 +++-\\n gateway/delivery.py | 31 +-\\n gateway/platforms/base.py | 4 +-\\n gateway/platforms/feishu.py | 275 +++-\\n gateway/platforms/webhook.py | 37 +-\\n gateway/run.py | 619 +++++++--\\n gateway/session.py | 16 +-\\n gateway/session_context.py | 10 +\\n model_tools.py | 1 +\\n tests/cron/test_scheduler.py | 158 +++\\n .../gateway/test_command_bypass_active_session.py | 12 +\\n tests/gateway/test_config.py | 125 +-\\n tests/gateway/test_feishu.py | 241 +++-\\n tests/gateway/test_feishu_ws_runtime.py | 86 ++\\n tests/gateway/test_runner_fatal_adapter.py | 32 +-\\n tests/gateway/test_session.py | 12 +\\n tests/gateway/test_session_env.py | 43 +-\\n tests/gateway/test_sethome_multi_account.py | 268 ++++\\n tests/gateway/test_shared_thread_sender_prefix.py | 92 ++\\n tests/gateway/test_unauthorized_dm_behavior.py | 137 +-\\n tests/tools/test_feishu_id_tool.py | 606 +++++++++\\n tests/tools/test_send_message_tool.py | 113 ++\\n tools/cronjob_tools.py | 4 +\\n tools/feishu_id_tool.py | 1340 ++++++++++++++++++++\\n tools/send_message_tool.py | 73 +-\\n toolsets.py | 2 +\\n website/docs/user-guide/messaging/feishu.md | 133 +-\\n 28 files changed, 4647 insertions(+), 239 deletions(-)\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_01C9hZc9tuYcb8juESP7WkU2"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_013LzgRSQhAqppkoTuFj3LoC",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 -- gateway/platforms/feishu.py 2>&1 | head -200\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toolu_bdrk_01LgEXZxn6fvcRVnCLNmT1kP",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 -- gateway/run.py 2>&1 | head -300\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toolu_bdrk_012AJZhndXjKnzGL55KMHTe1",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_search",
|
||||
"arguments": "{\"query\": \"Feishu Lark open_id vs user_id app-scoped identity model API documentation\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"diff --git a/gateway/platforms/feishu.py b/gateway/platforms/feishu.py\\nindex 7fce74de..8789cd68 100644\\n--- a/gateway/platforms/feishu.py\\n+++ b/gateway/platforms/feishu.py\\n@@ -71,7 +71,9 @@ try:\\n UpdateMessageRequest,\\n UpdateMessageRequestBody,\\n )\\n+ from lark_oapi.core import AccessTokenType, HttpMethod\\n from lark_oapi.core.const import FEISHU_DOMAIN, LARK_DOMAIN\\n+ from lark_oapi.core.model.base_request import BaseRequest\\n from lark_oapi.event.callback.model.p2_card_action_trigger import P2CardActionTriggerResponse\\n from lark_oapi.event.dispatcher_handler import EventDispatcherHandler\\n from lark_oapi.ws import Client as FeishuWSClient\\n@@ -85,6 +87,9 @@ except ImportError:\\n FeishuWSClient = None # type: ignore[assignment]\\n FEISHU_DOMAIN = None # type: ignore[assignment]\\n LARK_DOMAIN = None # type: ignore[assignment]\\n+ AccessTokenType = None # type: ignore[assignment]\\n+ HttpMethod = None # type: ignore[assignment]\\n+ BaseRequest = None # type: ignore[assignment]\\n \\n FEISHU_WEBSOCKET_AVAILABLE = websockets is not None\\n FEISHU_WEBHOOK_AVAILABLE = aiohttp is not None\\n@@ -274,6 +279,7 @@ class FeishuAdapterSettings:\\n encrypt_key: str\\n verification_token: str\\n group_policy: str\\n+ require_mention: bool\\n allowed_group_users: frozenset[str]\\n bot_open_id: str\\n bot_user_id: str\\n@@ -301,6 +307,7 @@ class FeishuGroupRule:\\n \\\"\\\"\\\"Per-group policy rule for controlling which users may interact with the bot.\\\"\\\"\\\"\\n \\n policy: str # \\\"open\\\" | \\\"allowlist\\\" | \\\"blacklist\\\" | \\\"admin_only\\\" | \\\"disabled\\\"\\n+ require_mention: Optional[bool] = None\\n allowlist: set[str] = field(default_factory=set)\\n blacklist: set[str] = field(default\n\n[... tool output truncated for fixture — original was 9031 chars]",
|
||||
"tool_call_id": "toolu_bdrk_013LzgRSQhAqppkoTuFj3LoC"
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"diff --git a/gateway/run.py b/gateway/run.py\\nindex 31fe724f..74bc1a1e 100644\\n--- a/gateway/run.py\\n+++ b/gateway/run.py\\n@@ -241,6 +241,7 @@ if not _configured_cwd or _configured_cwd in (\\\".\\\", \\\"auto\\\", \\\"cwd\\\"):\\n from gateway.config import (\\n Platform,\\n GatewayConfig,\\n+ HomeChannel,\\n load_gateway_config,\\n )\\n from gateway.session import (\\n@@ -534,6 +535,7 @@ class GatewayRunner:\\n def __init__(self, config: Optional[GatewayConfig] = None):\\n self.config = config or load_gateway_config()\\n self.adapters: Dict[Platform, BasePlatformAdapter] = {}\\n+ self._adapters_by_binding: Dict[Any, BasePlatformAdapter] = {}\\n \\n # Load ephemeral config from config.yaml / env vars.\\n # Both are injected at API-call time only and never persisted.\\n@@ -591,8 +593,8 @@ class GatewayRunner:\\n self._pending_approvals: Dict[str, Dict[str, Any]] = {}\\n \\n # Track platforms that failed to connect for background reconnection.\\n- # Key: Platform enum, Value: {\\\"config\\\": platform_config, \\\"attempts\\\": int, \\\"next_retry\\\": float}\\n- self._failed_platforms: Dict[Platform, Dict[str, Any]] = {}\\n+ # Key: (platform, account_id), Value: {\\\"config\\\": platform_config, \\\"attempts\\\": int, \\\"next_retry\\\": float}\\n+ self._failed_platforms: Dict[Any, Dict[str, Any]] = {}\\n \\n # Track pending /update prompt responses per session.\\n # Key: session_key, Value: True when a prompt is waiting for user input.\\n@@ -633,6 +635,142 @@ class GatewayRunner:\\n # Track background tasks to prevent garbage collection mid-execution\\n self._background_tasks: set = set()\\n \\n+ def _adapter_binding_key(\\n+ self,\\n+ platform: Platform,\\n+ account_id: O\n\n[... tool output truncated for fixture — original was 14156 chars]",
|
||||
"tool_call_id": "toolu_bdrk_01LgEXZxn6fvcRVnCLNmT1kP"
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\n \"success\": true,\n \"data\": {\n \"web\": [\n {\n \"url\": \"https://open.feishu.cn/document/home/user-identity-introduction/introduction\",\n \"title\": \"User identification concepts - Developer Guides - Documentation\",\n \"description\": \"User identity within a tenant. The user_id of the same Feishu user in tenant A and tenant B is different. user_id is also known as employee_id, ...\",\n \"category\": null\n },\n {\n \"url\": \"https://open.feishu.cn/document/server-docs/contact-v3/user/batch_get_id\",\n \"title\": \"Obtain user ID via email or mobile number - Documentation\",\n \"description\": \"Optional values are: open_id :Identifies a user to an app. The same user has different Open IDs in different apps. How to get Open ID.\",\n \"category\": null\n },\n {\n \"url\": \"https://open.larkoffice.com/document/server-docs/contact-v3/department/search\",\n \"title\": \"Search for departments - Documentation - Feishu Open Platform\",\n \"description\": \"User ID is usually used to communicate user data between different apps. How to get User ID. Default value: open_id. When the value is user_id , ...\",\n \"category\": null\n },\n {\n \"url\": \"https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/contact-v3/user/create\",\n \"title\": \"Create a user - 服务端 API - Documentation - Feishu Open Platform\",\n \"description\": \"User ID is usually used to communicate user data between different apps. How to get User ID. Default value: open_id. When the value is user_id , ...\",\n \"category\": null\n },\n {\n \"url\": \"https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/contact-v3/department/children\",\n \"title\": \"Obtain the list of sub-departments - Server API \n\n[... tool output truncated for fixture — original was 2037 chars]",
|
||||
"tool_call_id": "toolu_bdrk_012AJZhndXjKnzGL55KMHTe1"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_01HcmboQ91UbC3TgT6nwk6ek",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_extract",
|
||||
"arguments": "{\"urls\": [\"https://open.feishu.cn/document/home/user-identity-introduction/introduction\"]}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toolu_bdrk_01QUVxXbPkFjDSN8tLe45JDE",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 -- tools/feishu_id_tool.py 2>&1 | head -100\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toolu_bdrk_01KVSbFkSywrEDjRjT8bMz7p",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 -- gateway/config.py 2>&1 | head -200\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\n \"results\": [\n {\n \"url\": \"https://open.feishu.cn/document/home/user-identity-introduction/introduction\",\n \"title\": \"Feishu Open Platform\",\n \"content\": \"Feedback\\n\\nOnCall\\n\\nCollapse\\n\\nExpand\",\n \"error\": null\n }\n ]\n}",
|
||||
"tool_call_id": "toolu_bdrk_01HcmboQ91UbC3TgT6nwk6ek"
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"diff --git a/tools/feishu_id_tool.py b/tools/feishu_id_tool.py\\nnew file mode 100644\\nindex 00000000..a73dcbe7\\n--- /dev/null\\n+++ b/tools/feishu_id_tool.py\\n@@ -0,0 +1,1340 @@\\n+\\\"\\\"\\\"Feishu identity/chat lookup tool with official API first, local fallback.\\n+\\n+Primary resolution uses the official Feishu APIs for:\\n+- user identity lookup by open_id/user_id/union_id\\n+- chat lookup by chat_id\\n+- chat search by name/query\\n+- chat member enumeration\\n+\\n+When official API credentials are unavailable, or an authoritative lookup does\\n+not succeed, the tool falls back to Hermes' locally observed session metadata\\n+from ``~/.hermes/sessions/sessions.json``.\\n+\\\"\\\"\\\"\\n+\\n+from __future__ import annotations\\n+\\n+import json\\n+import logging\\n+import os\\n+import re\\n+from pathlib import Path\\n+from typing import Any, Dict, List, Optional, Tuple\\n+\\n+from hermes_constants import get_hermes_home\\n+\\n+logger = logging.getLogger(__name__)\\n+\\n+_SESSION_KEY_ACCOUNT_RE = re.compile(r\\\":feishu\\\\[([^\\\\]]+)\\\\]:\\\")\\n+\\n+_ID_PREFIX_MAP = {\\n+ \\\"ou_\\\": \\\"open_id\\\",\\n+ \\\"on_\\\": \\\"union_id\\\",\\n+ \\\"u_\\\": \\\"user_id\\\",\\n+}\\n+\\n+FEISHU_ID_SCHEMA = {\\n+ \\\"name\\\": \\\"feishu_id\\\",\\n+ \\\"description\\\": (\\n+ \\\"Inspect Feishu identities, chats, and session routes for Hermes. \\\"\\n+ \\\"Uses official Feishu APIs when credentials are configured, and falls back \\\"\\n+ \\\"to Hermes' local observed session metadata when needed. Supports chat_id/name \\\"\\n+ \\\"lookup, open_id/user_id/union_id lookup, group member lookup, and session-key resolution.\\\"\\n+ ),\\n+ \\\"parameters\\\": {\\n+ \\\"type\\\": \\\"object\\\",\\n+ \\\"properties\\\": {\\n+ \\\"action\\\": {\\n+ \\\"type\\\": \\\"string\\\",\\n+ \\\"enum\\\": [\\n+ \n\n[... tool output truncated for fixture — original was 3795 chars]",
|
||||
"tool_call_id": "toolu_bdrk_01QUVxXbPkFjDSN8tLe45JDE"
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"diff --git a/gateway/config.py b/gateway/config.py\\nindex 7d616592..a263ce01 100644\\n--- a/gateway/config.py\\n+++ b/gateway/config.py\\n@@ -79,13 +79,17 @@ class HomeChannel:\\n platform: Platform\\n chat_id: str\\n name: str # Human-readable name for display\\n+ account_id: Optional[str] = None\\n \\n def to_dict(self) -> Dict[str, Any]:\\n- return {\\n+ payload = {\\n \\\"platform\\\": self.platform.value,\\n \\\"chat_id\\\": self.chat_id,\\n \\\"name\\\": self.name,\\n }\\n+ if self.account_id:\\n+ payload[\\\"account_id\\\"] = self.account_id\\n+ return payload\\n \\n @classmethod\\n def from_dict(cls, data: Dict[str, Any]) -> \\\"HomeChannel\\\":\\n@@ -93,6 +97,7 @@ class HomeChannel:\\n platform=Platform(data[\\\"platform\\\"]),\\n chat_id=str(data[\\\"chat_id\\\"]),\\n name=data.get(\\\"name\\\", \\\"Home\\\"),\\n+ account_id=str(data[\\\"account_id\\\"]).strip() if data.get(\\\"account_id\\\") else None,\\n )\\n \\n \\n@@ -175,6 +180,16 @@ class PlatformConfig:\\n home_channel = None\\n if \\\"home_channel\\\" in data:\\n home_channel = HomeChannel.from_dict(data[\\\"home_channel\\\"])\\n+ extra = data.get(\\\"extra\\\", {})\\n+ if not isinstance(extra, dict):\\n+ extra = {}\\n+ # Preserve unknown top-level keys inside ``extra`` so platform-specific\\n+ # schemas can evolve without forcing a core dataclass change first.\\n+ extra = dict(extra)\\n+ for key, value in data.items():\\n+ if key in {\\\"enabled\\\", \\\"token\\\", \\\"api_key\\\", \\\"home_channel\\\", \\\"reply_to_mode\\\", \\\"extra\\\"}:\\n+ continue\\n+ extra[key] = value\\n \\n return cls(\\n \n\n[... tool output truncated for fixture — original was 8772 chars]",
|
||||
"tool_call_id": "toolu_bdrk_01KVSbFkSywrEDjRjT8bMz7p"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_013pi1eiBL8aos8si1TesgJQ",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_extract",
|
||||
"arguments": "{\"urls\": [\"https://open.larkoffice.com/document/home/user-identity-introduction/introduction\"]}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toolu_bdrk_01YRkjL2FzF8kkd8mCFKhvLp",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && grep -n \\\"open_id\\\\|bot_open_id\\\\|bot_user_id\\\" gateway/platforms/feishu.py | head -40\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\n \"results\": [\n {\n \"url\": \"https://open.larkoffice.com/document/home/user-identity-introduction/introduction\",\n \"title\": \"Feishu Open Platform\",\n \"content\": \"Feedback\\n\\nOnCall\\n\\nCollapse\\n\\nExpand\",\n \"error\": null\n }\n ]\n}",
|
||||
"tool_call_id": "toolu_bdrk_013pi1eiBL8aos8si1TesgJQ"
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"230: \\\"open_id\\\",\\n278: bot_open_id: str\\n279: bot_user_id: str\\n539: str(element.get(\\\"open_id\\\", \\\"\\\")).strip()\\n1122: bot_open_id=os.getenv(\\\"FEISHU_BOT_OPEN_ID\\\", \\\"\\\").strip(),\\n1123: bot_user_id=os.getenv(\\\"FEISHU_BOT_USER_ID\\\", \\\"\\\").strip(),\\n1177: self._bot_open_id = settings.bot_open_id\\n1178: self._bot_user_id = settings.bot_user_id\\n1952: open_id = str(getattr(operator, \\\"open_id\\\", \\\"\\\") or \\\"\\\")\\n1953: if not chat_id or not open_id:\\n1954: logger.debug(\\\"[Feishu] Card action missing chat_id or operator open_id, dropping\\\")\\n1987: sender_id = SimpleNamespace(open_id=open_id, user_id=None, union_id=None)\\n1989: user_name = sender_profile.get(\\\"user_name\\\") or open_id\\n2013: sender_id = SimpleNamespace(open_id=open_id, user_id=None, union_id=None)\\n2033: logger.info(\\\"[Feishu] Routing card action %r from %s in %s as synthetic command\\\", action_tag, open_id, chat_id)\\n2903: open_id = getattr(sender_id, \\\"open_id\\\", None) or None\\n2906: primary_id = open_id or user_id\\n2917: ID-type detection mirrors openclaw: ou_ → open_id, on_ → union_id, else user_id.\\n2934: id_type = \\\"open_id\\\"\\n3011: sender_open_id = getattr(sender_id, \\\"open_id\\\", None)\\n3013: sender_ids = {sender_open_id, sender_user_id} - {None}\\n3064: mention_open_id = getattr(mention_id, \\\"open_id\\\", None)\\n3068: if self._bot_open_id and mention_open_id == self._bot_open_id:\\n3070: if self._bot_user_id and mention_user_id == self._bot_user_id:\\n3080: if self._bot_open_id and self._bot_open_id in mentioned_ids:\\n3082: if self._bot_user_id and self._bot_user_id in mentioned_ids:\\n\n\n[... tool output truncated for fixture — original was 2530 chars]",
|
||||
"tool_call_id": "toolu_bdrk_01YRkjL2FzF8kkd8mCFKhvLp"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_01XPRM4pSLsPSJ6TrC9FjfjA",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 --stat -- tools/feishu_id_tool.py gateway/run.py gateway/config.py cron/scheduler.py tools/send_message_tool.py tools/cronjob_tools.py toolsets.py model_tools.py 2>&1\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"cron/scheduler.py | 140 ++++-\\n gateway/config.py | 276 ++++++++-\\n gateway/run.py | 619 ++++++++++++++++----\\n model_tools.py | 1 +\\n tools/cronjob_tools.py | 4 +\\n tools/feishu_id_tool.py | 1340 ++++++++++++++++++++++++++++++++++++++++++++\\n tools/send_message_tool.py | 73 ++-\\n toolsets.py | 2 +\\n 8 files changed, 2314 insertions(+), 141 deletions(-)\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_01XPRM4pSLsPSJ6TrC9FjfjA"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_01CW7jkdTW628KrHJQG9j9Dz",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 -- cron/scheduler.py 2>&1 | head -150\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toolu_bdrk_01XkuWrYyTcNSvCpYAQ1aLxu",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 -- tools/send_message_tool.py 2>&1 | head -150\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"diff --git a/cron/scheduler.py b/cron/scheduler.py\\nindex e6db77c0..7f9b9e66 100644\\n--- a/cron/scheduler.py\\n+++ b/cron/scheduler.py\\n@@ -84,26 +84,25 @@ def _resolve_delivery_target(job: dict) -> Optional[dict]:\\n \\n if deliver == \\\"origin\\\":\\n if origin:\\n- return {\\n+ target = {\\n \\\"platform\\\": origin[\\\"platform\\\"],\\n \\\"chat_id\\\": str(origin[\\\"chat_id\\\"]),\\n \\\"thread_id\\\": origin.get(\\\"thread_id\\\"),\\n }\\n+ if origin.get(\\\"account_id\\\"):\\n+ target[\\\"account_id\\\"] = origin.get(\\\"account_id\\\")\\n+ return target\\n # Origin missing (e.g. job created via API/script) — try each\\n # platform's home channel as a fallback instead of silently dropping.\\n- for platform_name in (\\\"matrix\\\", \\\"telegram\\\", \\\"discord\\\", \\\"slack\\\", \\\"bluebubbles\\\"):\\n- chat_id = os.getenv(f\\\"{platform_name.upper()}_HOME_CHANNEL\\\", \\\"\\\")\\n- if chat_id:\\n+ for platform_name in (\\\"matrix\\\", \\\"telegram\\\", \\\"discord\\\", \\\"slack\\\", \\\"bluebubbles\\\", \\\"feishu\\\"):\\n+ target = _resolve_home_delivery_target(platform_name, origin=None)\\n+ if target:\\n logger.info(\\n \\\"Job '%s' has deliver=origin but no origin; falling back to %s home channel\\\",\\n job.get(\\\"name\\\", job.get(\\\"id\\\", \\\"?\\\")),\\n platform_name,\\n )\\n- return {\\n- \\\"platform\\\": platform_name,\\n- \\\"chat_id\\\": chat_id,\\n- \\\"thread_id\\\": None,\\n- }\\n+ return target\\n return None\\n \\n if \\\":\\\" in deliver:\\n@@ -131,31 +130,113 @@ def _resolve_\n\n[... tool output truncated for fixture — original was 5780 chars]",
|
||||
"tool_call_id": "toolu_bdrk_01CW7jkdTW628KrHJQG9j9Dz"
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"diff --git a/tools/send_message_tool.py b/tools/send_message_tool.py\\nindex a2b3e984..275cd764 100644\\n--- a/tools/send_message_tool.py\\n+++ b/tools/send_message_tool.py\\n@@ -99,6 +99,45 @@ def _handle_list():\\n return json.dumps(_error(f\\\"Failed to load channel directory: {e}\\\"))\\n \\n \\n+def _get_config_callable(config, name):\\n+ \\\"\\\"\\\"Return a real config helper method, ignoring MagicMock auto-attributes.\\\"\\\"\\\"\\n+ try:\\n+ instance_attr = vars(config).get(name)\\n+ except Exception:\\n+ instance_attr = None\\n+ if callable(instance_attr):\\n+ return instance_attr\\n+\\n+ class_attr = getattr(type(config), name, None)\\n+ if callable(class_attr):\\n+ return getattr(config, name)\\n+ return None\\n+\\n+\\n+def _resolve_platform_config(config, platform, account_id=None):\\n+ \\\"\\\"\\\"Support both GatewayConfig and lightweight test doubles.\\\"\\\"\\\"\\n+ getter = _get_config_callable(config, \\\"get_platform_config\\\")\\n+ if getter:\\n+ try:\\n+ return getter(platform, account_id=account_id)\\n+ except TypeError:\\n+ return getter(platform)\\n+\\n+ platforms = getattr(config, \\\"platforms\\\", {}) or {}\\n+ return platforms.get(platform)\\n+\\n+\\n+def _resolve_home_channel(config, platform, account_id=None):\\n+ \\\"\\\"\\\"Support both GatewayConfig and lightweight test doubles.\\\"\\\"\\\"\\n+ getter = _get_config_callable(config, \\\"get_home_channel\\\")\\n+ if getter:\\n+ try:\\n+ return getter(platform, account_id=account_id)\\n+ except TypeError:\\n+ return getter(platform)\\n+ return None\\n+\\n+\\n def _handle_send(args):\\n \\\"\\\"\\\"Send a message to a platform target.\\\"\\\"\\\"\\n target = args.get(\\\"target\\\", \\\"\\\")\\n@@ -168,9 +207,12 @@ def _handle_send(args)\n\n[... tool output truncated for fixture — original was 5807 chars]",
|
||||
"tool_call_id": "toolu_bdrk_01XkuWrYyTcNSvCpYAQ1aLxu"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_015UcVYQRoZkvXo2X3WVE383",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 -- gateway/run.py 2>&1 | wc -l\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toolu_bdrk_019zsHzqWjkBuyBKHSdRvG7R",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 -- gateway/run.py 2>&1 | grep \\\"^+\\\" | grep -c \\\"account_id\\\\|binding\\\\|_ensure_adapter\\\\|_register_adapter\\\\|_unregister\\\\|_get_adapter\\\\|_all_adapter\\\\|_iter_enabled\\\\|_binding_label\\\\|_adapters_by_binding\\\"\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toolu_bdrk_01HPmygaWCphhGAbJLgDgz3v",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 -- gateway/run.py 2>&1 | grep -c \\\"^[+-]\\\" \"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"1162\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_015UcVYQRoZkvXo2X3WVE383"
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"154\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_019zsHzqWjkBuyBKHSdRvG7R"
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"621\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_01HPmygaWCphhGAbJLgDgz3v"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_017ie77GPPRhyGna7raJCzS4",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 -- gateway/run.py 2>&1 | grep \\\"^+\\\" | grep -v \\\"^+++\\\" | grep -c \\\"feishu_id\\\\|feishu\\\\|sethome.*feishu\\\\|FEISHU\\\"\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toolu_bdrk_01XaiiozutypWDXfZDsuRbx5",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 -- gateway/run.py 2>&1 | tail -500 | head -300\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"9\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_017ie77GPPRhyGna7raJCzS4"
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"- adapter = self.adapters.get(source.platform)\\n+ adapter = self._get_adapter_for_source(source)\\n if adapter:\\n adapter._pending_messages[_quick_key] = event\\n return None\\n@@ -3167,7 +3397,7 @@ class GatewayRunner:\\n and platform_name not in policy.notify_exclude_platforms\\n )\\n if should_notify:\\n- adapter = self.adapters.get(source.platform)\\n+ adapter = self._get_adapter_for_source(source)\\n if adapter:\\n if reset_reason == \\\"suspended\\\":\\n reason_text = \\\"previous session was stopped or interrupted\\\"\\n@@ -3491,9 +3721,8 @@ class GatewayRunner:\\n # Skip for webhooks - they deliver directly to configured targets (github_comment, etc.)\\n if not history and source.platform and source.platform != Platform.LOCAL and source.platform != Platform.WEBHOOK:\\n platform_name = source.platform.value\\n- env_key = f\\\"{platform_name.upper()}_HOME_CHANNEL\\\"\\n- if not os.getenv(env_key):\\n- adapter = self.adapters.get(source.platform)\\n+ if not self._has_home_channel_for_source(source):\\n+ adapter = self._get_adapter_for_source(source)\\n if adapter:\\n await adapter.send(\\n source.chat_id,\\n@@ -3537,6 +3766,132 @@ class GatewayRunner:\\n if message_text is None:\\n return\\n \\n+ if event.media_urls:\\n+ image_paths = []\\n+ for i, path in enumerate(event.media_urls):\\n+ # Check media_types if available; otherwise infer from me\n\n[... tool output truncated for fixture — original was 16254 chars]",
|
||||
"tool_call_id": "toolu_bdrk_01XaiiozutypWDXfZDsuRbx5"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_01THagoH9sx4am3t1y9fkQcS",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && grep -n \\\"_enrich_message_with_vision\\\\|_enrich_message_with_transcription\\\\|reply_to_text\\\\|preprocess_context_references_async\\\\|_TEXT_EXTENSIONS\\\\|MessageType.DOCUMENT\\\" gateway/run.py | head -20\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toolu_bdrk_01LbDoTuApRArrf6MXfQ8eBQ",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 -- gateway/run.py 2>&1 | grep \\\"^+\\\" | grep -c \\\"vision\\\\|transcri\\\\|reply_to_text\\\\|context_references\\\\|DOCUMENT\\\\|_TEXT_EXTENSION\\\\|_stt_\\\\|voice message\\\"\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"2974: message_text = await self._enrich_message_with_vision(\\n2980: message_text = await self._enrich_message_with_transcription(\\n3013: if event.media_urls and event.message_type == MessageType.DOCUMENT:\\n3016: _TEXT_EXTENSIONS = {\\\".txt\\\", \\\".md\\\", \\\".csv\\\", \\\".log\\\", \\\".json\\\", \\\".xml\\\", \\\".yaml\\\", \\\".yml\\\", \\\".toml\\\", \\\".ini\\\", \\\".cfg\\\"}\\n3023: if _ext in _TEXT_EXTENSIONS:\\n3054: if getattr(event, \\\"reply_to_text\\\", None) and event.reply_to_message_id:\\n3055: reply_snippet = event.reply_to_text[:500]\\n3066: from agent.context_references import preprocess_context_references_async\\n3074: _ctx_result = await preprocess_context_references_async(\\n6978: async def _enrich_message_with_vision(\\n7046: async def _enrich_message_with_transcription(\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_01THagoH9sx4am3t1y9fkQcS"
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"21\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_01LbDoTuApRArrf6MXfQ8eBQ"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_01Wdw4Lho816SVHt3BJQkMjL",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git log --oneline origin/main -- gateway/run.py | head -5\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toolu_bdrk_01BCB9orVkTcaHTtYwjcYMJW",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git show origin/main:gateway/run.py | grep -c \\\"_enrich_message_with_vision\\\\|_enrich_message_with_transcription\\\\|reply_to_text.*reply_to_message_id\\\\|preprocess_context_references_async\\\\|MessageType.DOCUMENT\\\"\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"964ef681 fix(gateway): improve /restart response with fallback instructions\\n276d20e6 fix(gateway): /restart uses service restart under systemd instead of detached subprocess\\n8a64f3e3 feat(gateway): notify /restart requester when gateway comes back online\\na0cd2c53 fix(gateway): verbose tool progress no longer truncates args when tool_preview_length is 0 (#8735)\\n3cd6cbee feat: add /debug slash command for all platforms\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_01Wdw4Lho816SVHt3BJQkMjL"
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"8\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_01BCB9orVkTcaHTtYwjcYMJW"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_01AGkGk8zcyZ8zUGxHGBGYL5",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git show origin/main:gateway/run.py | grep -n \\\"_enrich_message_with_vision\\\\|_enrich_message_with_transcription\\\\|reply_to_text.*reply_to_message_id\\\\|preprocess_context_references_async\\\\|MessageType.DOCUMENT\\\" | head -20\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"2974: message_text = await self._enrich_message_with_vision(\\n2980: message_text = await self._enrich_message_with_transcription(\\n3013: if event.media_urls and event.message_type == MessageType.DOCUMENT:\\n3054: if getattr(event, \\\"reply_to_text\\\", None) and event.reply_to_message_id:\\n3066: from agent.context_references import preprocess_context_references_async\\n3074: _ctx_result = await preprocess_context_references_async(\\n6978: async def _enrich_message_with_vision(\\n7046: async def _enrich_message_with_transcription(\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_01AGkGk8zcyZ8zUGxHGBGYL5"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_012CzR8xizYCPkv2MLJpRixn",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git log --oneline origin/main..pr-8388 2>&1\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"05b87f47 fix(gateway): remove stale shared-thread sender check\\nbc62efd6 feat(feishu): support multi-account routing and id-aware delivery\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_012CzR8xizYCPkv2MLJpRixn"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_019sat1paLRz2Y4vkrrkF6vu",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 -- gateway/session_context.py 2>&1\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toolu_bdrk_01RnHYKvDVUjGw2scWn7J7uN",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 -- gateway/delivery.py 2>&1\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"diff --git a/gateway/session_context.py b/gateway/session_context.py\\nindex b9fdcdfa..56d977c1 100644\\n--- a/gateway/session_context.py\\n+++ b/gateway/session_context.py\\n@@ -43,19 +43,23 @@ from contextvars import ContextVar\\n # ---------------------------------------------------------------------------\\n \\n _SESSION_PLATFORM: ContextVar[str] = ContextVar(\\\"HERMES_SESSION_PLATFORM\\\", default=\\\"\\\")\\n+_SESSION_ACCOUNT_ID: ContextVar[str] = ContextVar(\\\"HERMES_SESSION_ACCOUNT_ID\\\", default=\\\"\\\")\\n _SESSION_CHAT_ID: ContextVar[str] = ContextVar(\\\"HERMES_SESSION_CHAT_ID\\\", default=\\\"\\\")\\n _SESSION_CHAT_NAME: ContextVar[str] = ContextVar(\\\"HERMES_SESSION_CHAT_NAME\\\", default=\\\"\\\")\\n _SESSION_THREAD_ID: ContextVar[str] = ContextVar(\\\"HERMES_SESSION_THREAD_ID\\\", default=\\\"\\\")\\n _SESSION_USER_ID: ContextVar[str] = ContextVar(\\\"HERMES_SESSION_USER_ID\\\", default=\\\"\\\")\\n+_SESSION_USER_ID_ALT: ContextVar[str] = ContextVar(\\\"HERMES_SESSION_USER_ID_ALT\\\", default=\\\"\\\")\\n _SESSION_USER_NAME: ContextVar[str] = ContextVar(\\\"HERMES_SESSION_USER_NAME\\\", default=\\\"\\\")\\n _SESSION_KEY: ContextVar[str] = ContextVar(\\\"HERMES_SESSION_KEY\\\", default=\\\"\\\")\\n \\n _VAR_MAP = {\\n \\\"HERMES_SESSION_PLATFORM\\\": _SESSION_PLATFORM,\\n+ \\\"HERMES_SESSION_ACCOUNT_ID\\\": _SESSION_ACCOUNT_ID,\\n \\\"HERMES_SESSION_CHAT_ID\\\": _SESSION_CHAT_ID,\\n \\\"HERMES_SESSION_CHAT_NAME\\\": _SESSION_CHAT_NAME,\\n \\\"HERMES_SESSION_THREAD_ID\\\": _SESSION_THREAD_ID,\\n \\\"HERMES_SESSION_USER_ID\\\": _SESSION_USER_ID,\\n+ \\\"HERMES_SESSION_USER_ID_ALT\\\": _SESSION_USER_ID_ALT,\\n \\\"HERMES_SESSION_USER_NAME\\\": _SESSION_USER_NAME,\\n \\\"HERMES_SESSION_KEY\\\": _SESSION_KEY,\\n }\\n@@ -63,10 +67,12 @@ _VAR_MAP = {\\n \\n def set_session_vars(\\n platform: str = \\\"\\\",\\n+ account_id: str = \\\"\\\",\\n chat_\n\n[... tool output truncated for fixture — original was 2894 chars]",
|
||||
"tool_call_id": "toolu_bdrk_019sat1paLRz2Y4vkrrkF6vu"
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"diff --git a/gateway/delivery.py b/gateway/delivery.py\\nindex d7fa6afd..e357b93e 100644\\n--- a/gateway/delivery.py\\n+++ b/gateway/delivery.py\\n@@ -37,6 +37,7 @@ class DeliveryTarget:\\n - \\\"telegram:123456\\\" → specific Telegram chat\\n \\\"\\\"\\\"\\n platform: Platform\\n+ account_id: Optional[str] = None\\n chat_id: Optional[str] = None # None means use home channel\\n thread_id: Optional[str] = None\\n is_origin: bool = False\\n@@ -59,6 +60,7 @@ class DeliveryTarget:\\n if origin:\\n return cls(\\n platform=origin.platform,\\n+ account_id=origin.account_id,\\n chat_id=origin.chat_id,\\n thread_id=origin.thread_id,\\n is_origin=True,\\n@@ -122,6 +124,7 @@ class DeliveryRouter:\\n \\\"\\\"\\\"\\n self.config = config\\n self.adapters = adapters or {}\\n+ self.account_adapters: Dict[tuple[Platform, Optional[str]], Any] = {}\\n self.output_dir = get_hermes_home() / \\\"cron\\\" / \\\"output\\\"\\n \\n async def deliver(\\n@@ -228,14 +231,29 @@ class DeliveryRouter:\\n metadata: Optional[Dict[str, Any]]\\n ) -> Dict[str, Any]:\\n \\\"\\\"\\\"Deliver content to a messaging platform.\\\"\\\"\\\"\\n- adapter = self.adapters.get(target.platform)\\n- \\n+ if not target.chat_id:\\n+ home = self.config.get_home_channel(target.platform, account_id=target.account_id)\\n+ if home:\\n+ target = DeliveryTarget(\\n+ platform=target.platform,\\n+ account_id=getattr(home, \\\"account_id\\\", None) or target.account_id,\\n+ chat_id=home.chat_id,\\n+ thread_id=target.thread_id,\\n+ \n\n[... tool output truncated for fixture — original was 2946 chars]",
|
||||
"tool_call_id": "toolu_bdrk_01RnHYKvDVUjGw2scWn7J7uN"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_01MuFViB9E4hgh1Z8qRdQ3Kt",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 -- tests/tools/test_feishu_id_tool.py 2>&1 | head -100\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toolu_bdrk_01UUFRbWLBdD2nKzLTgij43u",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && wc -l tools/feishu_id_tool.py 2>/dev/null; git show pr-8388:tools/feishu_id_tool.py 2>/dev/null | wc -l\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"diff --git a/tests/tools/test_feishu_id_tool.py b/tests/tools/test_feishu_id_tool.py\\nnew file mode 100644\\nindex 00000000..c1e90daf\\n--- /dev/null\\n+++ b/tests/tools/test_feishu_id_tool.py\\n@@ -0,0 +1,606 @@\\n+import json\\n+from datetime import datetime\\n+from pathlib import Path\\n+from types import SimpleNamespace\\n+from unittest.mock import patch\\n+\\n+from gateway.config import Platform\\n+from gateway.session import SessionEntry, SessionSource\\n+from tools.feishu_id_tool import feishu_id_tool\\n+\\n+\\n+class _FakeResponse:\\n+ def __init__(self, *, success: bool, data=None, code=0, msg=\\\"ok\\\"):\\n+ self._success = success\\n+ self.data = data\\n+ self.code = code\\n+ self.msg = msg\\n+\\n+ def success(self):\\n+ return self._success\\n+\\n+\\n+def _make_fake_client(\\n+ *,\\n+ user=None,\\n+ chat=None,\\n+ members=None,\\n+ batch_users=None,\\n+ search_chats=None,\\n+):\\n+ def _user_get(_request):\\n+ if user is None:\\n+ return _FakeResponse(success=False, code=404, msg=\\\"user not found\\\")\\n+ return _FakeResponse(success=True, data=SimpleNamespace(user=user))\\n+\\n+ def _user_batch(_request):\\n+ if batch_users is None:\\n+ return _FakeResponse(success=False, code=404, msg=\\\"batch not found\\\")\\n+ return _FakeResponse(success=True, data=SimpleNamespace(items=batch_users))\\n+\\n+ def _chat_get(_request):\\n+ if chat is None:\\n+ return _FakeResponse(success=False, code=404, msg=\\\"chat not found\\\")\\n+ return _FakeResponse(success=True, data=chat)\\n+\\n+ def _chat_search(_request):\\n+ if search_chats is None:\\n+ return _FakeResponse(success=False, code=404, msg=\\\"search not found\\\")\\n+ return _FakeResponse(success=True\n\n[... tool output truncated for fixture — original was 3324 chars]",
|
||||
"tool_call_id": "toolu_bdrk_01MuFViB9E4hgh1Z8qRdQ3Kt"
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"1340\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_01UUFRbWLBdD2nKzLTgij43u"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_01DMDwTRn33PPs873SPDLihR",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 2>&1 | grep -c \\\"_get_config_callable\\\\|_resolve_platform_config\\\\|_resolve_home_channel\\\"\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toolu_bdrk_01THAT7RkuypsXBnDyFxztjZ",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 2>&1 | grep \\\"^+.*def _get_config_callable\\\\|^+.*def _resolve_platform_config\\\\|^+.*def _resolve_home_channel\\\"\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"24\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_01DMDwTRn33PPs873SPDLihR"
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"+def _get_config_callable(config, name):\\n+def _resolve_platform_config(config, platform, account_id=None):\\n+def _resolve_home_channel(config, platform, account_id=None):\\n+def _get_config_callable(config, name):\\n+def _resolve_platform_config(config, account_id: Optional[str] = None):\\n+def _get_config_callable(config, name):\\n+def _resolve_platform_config(config, platform, account_id=None):\\n+def _resolve_home_channel(config, platform, account_id=None):\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_01THAT7RkuypsXBnDyFxztjZ"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "toolu_bdrk_012p43DPonvSLugt3FaYoJNv",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git show 05b87f47 --stat 2>&1\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "toolu_bdrk_01VkHMceN4tHnGDcWV4tDUt9",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "terminal",
|
||||
"arguments": "{\"command\": \"cd /home/user/.hermes/hermes-agent && git diff origin/main...pr-8388 -- tests/gateway/test_shared_thread_sender_prefix.py 2>&1 | head -50\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"commit 05b87f47911ae28bdad14d2281ad454b8587aa2a\\nAuthor: contributor <contributor@example.com>\\nDate: Mon Apr 13 13:18:48 2026 +0800\\n\\n fix(gateway): remove stale shared-thread sender check\\n\\n gateway/run.py | 2 -\\n tests/gateway/test_shared_thread_sender_prefix.py | 92 +++++++++++++++++++++++\\n 2 files changed, 92 insertions(+), 2 deletions(-)\", \"exit_code\": 0, \"error\": null}",
|
||||
"tool_call_id": "toolu_bdrk_012p43DPonvSLugt3FaYoJNv"
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "{\"output\": \"diff --git a/tests/gateway/test_shared_thread_sender_prefix.py b/tests/gateway/test_shared_thread_sender_prefix.py\\nnew file mode 100644\\nindex 00000000..777db5c5\\n--- /dev/null\\n+++ b/tests/gateway/test_shared_thread_sender_prefix.py\\n@@ -0,0 +1,92 @@\\n+import importlib\\n+import sys\\n+import types\\n+from datetime import datetime\\n+from types import SimpleNamespace\\n+from unittest.mock import AsyncMock, MagicMock\\n+\\n+import pytest\\n+\\n+from gateway.config import GatewayConfig, Platform, PlatformConfig\\n+from gateway.platforms.base import MessageEvent\\n+from gateway.session import SessionEntry, SessionSource\\n+\\n+\\contributor@example.com\\n+async def test_handle_message_with_agent_shared_thread_prefixes_sender_once(monkeypatch):\\n+ fake_dotenv = types.ModuleType(\\\"dotenv\\\")\\n+ fake_dotenv.load_dotenv = lambda *args, **kwargs: None\\n+ monkeypatch.setitem(sys.modules, \\\"dotenv\\\", fake_dotenv)\\n+\\n+ gateway_run = importlib.import_module(\\\"gateway.run\\\")\\n+ GatewayRunner = gateway_run.GatewayRunner\\n+\\n+ runner = object.__new__(GatewayRunner)\\n+ runner.config = GatewayConfig(\\n+ platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token=\\\"fake-token\\\")}\\n+ )\\n+ adapter = MagicMock()\\n+ adapter.send = AsyncMock()\\n+ adapter.stop_typing = AsyncMock()\\n+ runner.adapters = {Platform.TELEGRAM: adapter}\\n+ runner._voice_mode = {}\\n+ runner.hooks = SimpleNamespace(emit=AsyncMock(), loaded_hooks=False)\\n+ runner.session_store = MagicMock()\\n+ runner.session_store.get_or_create_session.return_value = SessionEntry(\\n+ session_key=\\\"agent:main:telegram:group:-1001:17585\\\",\\n+ session_id=\\\"sess-1\\\",\\n+ created_at=datetime.now(),\\n+ updated_at=datetime.now(),\\n+ platform=Platform.T\n\n[... tool output truncated for fixture — original was 2006 chars]",
|
||||
"tool_call_id": "toolu_bdrk_01VkHMceN4tHnGDcWV4tDUt9"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Here's my review:\n\nThe core claim about Feishu's identity model is technically correct. Feishu's own docs confirm: \"open_id identifies a user to an app. The same user has different Open IDs in different apps.\" So yes, `open_id` is app-scoped, not a canonical identity.\n\nBut the rest of this PR is a bait-and-switch.\n\n**What it claims to be:** A Feishu identity model bugfix.\n\n**What it actually is:** A 4647-line feature PR that adds:\n\n→ Full multi-account Feishu support (account-aware routing, binding keys, adapter registry overhaul in gateway/run.py)\n→ A brand new 1340-line `feishu_id` tool (user/chat/member lookup via Feishu API)\n→ Thread-local websocket proxy refactoring for concurrent bot connections\n→ Account-aware delivery, session context, cron scheduler changes\n→ New session context variables (`HERMES_SESSION_ACCOUNT_ID`, `USER_ID_ALT`)\n→ A separate unrelated fix (\"remove stale shared-thread sender check\") bundled in\n\n**The \"bug\" doesn't actually affect anyone.** Current single-bot Feishu deployments work fine. `open_id` is technically an app-scoped alias, but when you only have one app, it's de facto unique and stable. The identity model is imprecise in naming but functionally correct for the existing use case.\n\n**Code quality concerns:**\n→ `_get_config_callable`, `_resolve_platform_config`, and `_resolve_home_channel` are copy-pasted 3 separate times across `cron/scheduler.py`, `tools/send_message_tool.py`, and `tools/feishu_id_tool.py` instead of living in a shared module\n→ `_ensure_adapter_registry()` has defensive `not hasattr` checks that suggest the multi-account plumbing was bolted on rather than designed in\n→ 619 lines of changes to gateway/run.py, most of which is adapter-binding indirection for a feature nobody requested\n\n**Recommendation:** Close it. The identity model observation is valid as documentation/knowledge, but this PR uses it to justify smuggling in a massive multi-account feature. If multi-account Feishu is ever needed, it should be a planned feature with proper design review, not a 4600-line \"bugfix.\" The actual identity correction (if wanted) would be maybe 50 lines of renaming/comments."
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue