Honcho client rebuild on timeout config changes
Cluster 57347 · agent. Data as of
Nothing open on GitHub. Fix merged in #66052 on 17 Jul 2026. Check it covers your case.
The short answer
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #66052. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
unresolved. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These items address a bug where the Honcho client behaves as a per-process singleton, causing timeout configuration changes to be ignored until restart. A fix rebuilds the client when timeout config changes, alongside several related community recall and config fixes.
Open issues0
Open PRs0
Fixes on record
-
#66052 fix(honcho): salvage five community recall/config fixes onto current main
files:
plugins/memory/honcho/__init__.py,plugins/memory/honcho/cli.py,plugins/memory/honcho/client.py,plugins/memory/honcho/session.py,scripts/release.py,tests/honcho_plugin/test_client.py,tests/honcho_plugin/test_session.pyShow the diff
-
plugins/memory/honcho/__init__.py@@ -125,13 +125,26 @@ "type": "string", "description": ( "Override the default reasoning depth. " - "Omit to use the configured default (typically low). " - "Guide:\n" - "- minimal: quick factual lookups (name, role, simple preference)\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" + "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"], }, @@ -1437,7 +1450,7 @@ def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str: return json.dumps({"result": card}) elif tool_name == "honcho_search": - query = args.get("query", "") + query = (args.get("query") or "").strip() if not query: return tool_error("Missing required parameter: query") max_tokens = [redacted:secret-assignment]"max_tokens", 800)), 2000) @@ -1450,7 +1463,7 @@ def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str: return json.dumps({"result": result}) elif tool_name == "honcho_reasoning": - query = args.get("query", "") + query = (args.get("query") or "").strip() if not query: return tool_error("Missing required parameter: query") peer = args.get("peer", "user") -
plugins/memory/honcho/cli.py@@ -1099,7 +1099,7 @@ def cmd_status(args) -> None: print(f" Recall mode: {hcfg.recall_mode}") print(f" Context budget: {hcfg.context_tokens or '(uncapped)'} tokens") raw = getattr(hcfg, "raw", None) or {} - dialectic_cadence = raw.get("dialecticCadence") or 1 + dialectic_cadence = getattr(hcfg, "dialectic_cadence", None) or raw.get("dialecticCadence") or 1 print(f" Dialectic cad: every {dialectic_cadence} turn{'s' if dialectic_cadence != 1 else ''}") reasoning_cap = raw.get("reasoningLevelCap") or hcfg.reasoning_level_cap heuristic_on = "on" if hcfg.reasoning_heuristic else "off" -
plugins/memory/honcho/client.py@@ -17,8 +17,10 @@ import os import logging import hashlib +import ipaddress from dataclasses import dataclass, field from pathlib import Path +from urllib.parse import urlparse from hermes_constants import get_hermes_home from hermes_cli.profiles import _get_default_hermes_home @@ -56,8 +58,9 @@ def resolve_active_host() -> str: Resolution order: 1. HERMES_HONCHO_HOST env var (explicit override) - 2. Active profile name via profiles system -> ``hermes.<profile>`` - 3. Fallback: ``"hermes"`` (default profile) + 2. Active profile name via profiles system -> ``hermes_<profile>`` + 3. defaultHost from the active config, but only for the default profile + 4. Fallback: ``"hermes"`` (default profile) """ explicit = os.environ.get("HERMES_HONCHO_HOST", "").strip() if explicit: @@ -66,10 +69,26 @@ def resolve_active_host() -> str: try: from hermes_cli.profiles import get_active_profile_name profile = get_active_profile_name() - return profile_host_key(profile) + profile_host = profile_host_key(profile) except Exception: - pass - return HOST + profile_host = HOST + + # Honcho's generic config can carry a defaultHost (for example "local"), + # but applying it before profile resolution makes every named Hermes + # profile share that same host. Keep named profiles isolated; only the + # default Hermes profile may opt into the config's default host. + if profile_host == HOST: + try: + path = resolve_config_path() + if path.exists(): + raw = json.loads(path.read_text(encoding="utf-8")) + default_host = str(raw.get("defaultHost", "")).strip() + if default_host: + return default_host + except Exception: + pass + + return profile_host def resolve_global_config_path() -> Path: @@ -226,6 +245,44 @@ def _parse_dialectic_depth_levels(host_val, root_val, depth: int) -> list[str] | _DEFAULT_HTTP_TIMEOUT = 30.0 +def _is_local_base_url(base_url: str | None) -> bool: + """Return True for loopback/LAN/VPN self-hosted Honcho URLs. + + Local Honcho deployments can run without auth, but the SDK requires a + non-empty api_key argument. Treat loopback plus RFC1918/link-local/ULA + and carrier-grade-NAT IPs as local so LAN/VPN URLs such as + ``http://192.168.2.112:8000`` get the same placeholder-key behavior as + localhost. + """ + if not base_url: + return False + + try: + parsed = urlparse(base_url) + host = (parsed.hostname or "").strip().lower() + except Exception: + host = "" + + if host in {"localhost", "127.0.0.1", "::1"}: + return True + if not host: + return False + + try: + ip = ipaddress.ip_address(host) + except ValueError: + return False + + if ip.is_loopback or ip.is_private or ip.is_link_local: + return True + + # Tailscale/other VPN setups often sit in carrier-grade NAT space. + if ip.version == 4 and ipaddress.ip_address("100.64.0.0") <= ip <= ipaddress.ip_address("100.127.255.255"): + return True + + return False + + def _resolve_optional_float(*values: Any) -> float | None: """Return the first non-empty value coerced to a positive float.""" for value in values: @@ -798,6 +855,50 @@ def resolve_session_name( _honcho_client_slot: SingletonSlot = SingletonSlot() +_cached_timeout: float | None = None +# Memo for the config.yaml-derived timeout, keyed on the file's mtime_ns so +# the staleness check on every get_honcho_client() call costs one stat() +# instead of a full YAML load. (None, None) = not yet populated. +_config_timeout_memo: tuple[int | None, float | None] = (None, None) + + +def _config_yaml_timeout() -> float | None: + """Read honcho.timeout / honcho.request_timeout from config.yaml, memoized on mtime.""" + global _config_timeout_memo + try: + from hermes_constants import get_hermes_home + + cfg_path = get_hermes_home() / "config.yaml" + try: + mtime_ns: int | None = cfg_path.stat().st_mtime_ns + except OSError: + mtime_ns = None + if _config_timeout_memo[0] is not None and _config_timeout_memo[0] == mtime_ns: + return _config_timeout_memo[1] + + from hermes_cli.config import load_config + + honcho_cfg = load_config().get("honcho", {}) + timeout = None + if isinstance(honcho_cfg, dict): + timeout = _resolve_optional_float( + honcho_cfg.get("timeout"), + honcho_cfg.get("request_timeout"), + ) + _config_timeout_memo = (mtime_ns, timeout) + return timeout + except Exception: + return None + + +def _resolve_timeout_from_sources(config: HonchoClientConfig | None) -> float: + """Resolve the effective timeout from env, config.yaml, and the explicit config.""" + timeout = config.timeout if config is not None else None + if timeout is None: + timeout = _resolve_optional_float(os.environ.get("HONCHO_TIMEOUT")) + if timeout is None: + timeout = _config_yaml_timeout() + return timeout if timeout is not None else _DEFAULT_HTTP_TIMEOUT def _apply_fresh_oauth_token(config: HonchoClientConfig) -> None: @@ -843,10 +944,20 @@ def get_honcho_client(config: HonchoClientConfig | None = None) -> Honcho: first calls (double-checked locking via ``SingletonSlot``), so racing threads can't each construct a client and leak the loser's connection. """ + global _cached_timeout cached = _honcho_client_slot.peek() if cached is not None: - _refresh_cached_oauth(cached, config) - return cached + # Detect timeout config changes in long-lived processes (gateway, + # dashboard). If the user changed the timeout after the client was + # built, rebuild with the new value. + new_timeout = _resolve_timeout_from_sources(config) + if new_timeout != _cached_timeout: + _honcho_client_slot.reset() + _cached_timeout = None + cached = None + else: + _refresh_cached_oauth(cached, config) + return cached if config is None: config = HonchoClientConfig.from_global_config() @@ -922,16 +1033,12 @@ def _build() -> "Honcho": # For local: only use config.api_key if the host block explicitly # sets apiKey (meaning the user wants local auth). Otherwise skip # the stored key -- it's likely a cloud key that would break local. - _is_local = resolved_base_url and ( - "localhost" in resolved_base_url - or "127.0.0.1" in resolved_base_url - or "::1" in resolved_base_url - ) + _is_local = _is_local_base_url(resolved_base_url) if _is_local: # Check if the host block has its own apiKey (explicit local auth). - # Auth-skipping is loopback-only: a stored key is likely a cloud key - # that would break a no-auth local server, so we substitute the SDK's - # required-non-empty placeholder unless the host block opts in. + # For local/LAN/VPN self-hosts, a stored root key is likely a cloud + # key that would break a no-auth local server, so we substitute the + # SDK's required-non-empty placeholder unless the host block opts in. _raw = config.raw or {} _host_block = (_raw.get("hosts") or {}).get(config.host, {}) _host_has_key = bool(_host_block.get("apiKey")) @@ -961,6 +1068,8 @@ def _build() -> "Honcho": if resolved_timeout is not None: kwargs["timeout"] = resolved_timeout + global _cached_timeout + _cached_timeout = resolved_timeout return Honcho(**kwargs) return _honcho_client_slot.get(_build) @@ -968,4 +1077,7 @@ def _build() -> "Honcho": def reset_honcho_client() -> None: """Reset the Honcho client singleton (useful for testing).""" + global _cached_timeout, _config_timeout_memo _honcho_client_slot.reset() + _cached_timeout = None + _config_timeout_memo = (None, None) -
plugins/memory/honcho/session.py@@ -742,7 +742,8 @@ def get_prefetch_context(self, session_key: str, user_message: str | None = None logger.debug("Failed to fetch session summary from Honcho: %s", e) try: - user_ctx = self._fetch_peer_context(session.user_peer_id, search_query=user_message or None, target=session.user_peer_id) + observer_peer_id, target_peer_id = self._resolve_observer_target(session, "user") + user_ctx = self._fetch_peer_context(observer_peer_id, search_query=user_message or None, target=target_peer_id or session.user_peer_id) result["representation"] = user_ctx["representation"] result["card"] = "\n".join(user_ctx["card"]) except Exception as e: -
scripts/release.py@@ -53,6 +53,9 @@ "[redacted:email]": "rlaehddus302", # PR #61985 salvage (gateway: secondary-adapter auth callback profile) "[redacted:email]": "aguung", # PR #64461 salvage (gateway: multiplex secret_scope for authz/Slack/webhooks) "[redacted:email]": "jtstothard", # PR #63256 salvage (gateway: multiplex secondary adapter config validation) + "[redacted:email]": "fjlaowan1983", # PR #11256 salvage (honcho: reject whitespace-only reasoning queries) + "[email protected]": "RainbowAndSun", # PR #62982 salvage (honcho: observer target in prefetch context) + "[redacted:email]": "Elektrofussel", # PR #61675 salvage (honcho: defaultHost + private-range local URL detection) "[redacted:email]": "SAMBAS123", # PR #64986 salvage (gateway: multiplex primary bot token scope) "[redacted:email]": "kocaemre", # PR #36051 salvage (docs: audit round 3 code/doc reconciliation) "[redacted:email]": "m1qaweb", # PR #29290 salvage (gateway: strip /queue prefix when idle) -
tests/honcho_plugin/test_client.py@@ -3,6 +3,8 @@ import importlib.util import json import os +import sys +import types from pathlib import Path from unittest.mock import patch, MagicMock @@ -439,7 +441,11 @@ def test_default_returns_hermes(self): with patch.dict(os.environ, {}, clear=True): os.environ.pop("HERMES_HONCHO_HOST", None) os.environ.pop("HERMES_HOME", None) - assert resolve_active_host() == "hermes" + with patch( + "plugins.memory.honcho.client.resolve_config_path", + return_value=Path("/nonexistent/honcho.json"), + ): + assert resolve_active_host() == "hermes" def test_explicit_env_var_wins(self): with patch.dict(os.environ, {"HERMES_HONCHO_HOST": "hermes.coder"}): @@ -451,16 +457,52 @@ def test_profile_name_derives_host(self): with patch("hermes_cli.profiles.get_active_profile_name", return_value="coder"): assert resolve_active_host() == "hermes_coder" + def test_default_host_does_not_override_named_profile(self, tmp_path): + """defaultHost is not applied before active-profile resolution.""" + config_file = tmp_path / "honcho.json" + config_file.write_text(json.dumps({ + "defaultHost": "local", + "hosts": {"local": {"workspace": "local-ws"}}, + })) + + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("HERMES_HONCHO_HOST", None) + with patch("hermes_cli.profiles.get_active_profile_name", return_value="coder"), \ + patch("plugins.memory.honcho.client.resolve_config_path", return_value=config_file): + assert resolve_active_host() == "hermes_coder" + + def test_default_host_applies_to_default_profile_only(self, tmp_path): + """default profile can use setup-generated defaultHost without leaking to other profiles.""" + config_file = tmp_path / "honcho.json" + config_file.write_text(json.dumps({ + "defaultHost": "local", + "hosts": {"local": {"workspace": "local-ws"}}, + })) + + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("HERMES_HONCHO_HOST", None) + with patch("hermes_cli.profiles.get_active_profile_name", return_value="default"), \ + patch("plugins.memory.honcho.client.resolve_config_path", return_value=config_file): + assert resolve_active_host() == "local" + def test_default_profile_returns_hermes(self): with patch.dict(os.environ, {}, clear=False): os.environ.pop("HERMES_HONCHO_HOST", None) - with patch("hermes_cli.profiles.get_active_profile_name", return_value="default"): + with patch("hermes_cli.profiles.get_active_profile_name", return_value="default"), \ + patch( + "plugins.memory.honcho.client.resolve_config_path", + return_value=Path("/nonexistent/honcho.json"), + ): assert resolve_active_host() == "hermes" def test_custom_profile_returns_hermes(self): with patch.dict(os.environ, {}, clear=False): os.environ.pop("HERMES_HONCHO_HOST", None) - with patch("hermes_cli.profiles.get_active_profile_name", return_value="custom"): + with patch("hermes_cli.profiles.get_active_profile_name", return_value="custom"), \ + patch( + "plugins.memory.honcho.client.resolve_config_path", + return_value=Path("/nonexistent/honcho.json"), + ): assert resolve_active_host() == "hermes" def test_profiles_import_failure_falls_back(self): @@ -693,6 +735,44 @@ def test_hermes_request_timeout_alias_used(self): mock_honcho.assert_called_once() assert mock_honcho.call_args.kwargs["timeout"] == 77.5 + @pytest.mark.skipif( + not importlib.util.find_spec("honcho"), + reason="honcho SDK not installed" + ) + def test_timeout_change_triggers_client_rebuild(self): + """Changing timeout config must rebuild the cached client.""" + fake_honcho_1 = MagicMock(name="Honcho_v1") + fake_honcho_2 = MagicMock(name="Honcho_v2") + cfg = HonchoClientConfig( + api_key="[redacted:secret-assignment]", + workspace_id="hermes", + environment="production", + ) + + with patch("honcho.Honcho", return_value=fake_honcho_1) as mock_h1, \ + patch("hermes_cli.config.load_config", return_value={"honcho": {"timeout": 30}}): + client1 = get_honcho_client(cfg) + + assert client1 is fake_honcho_1 + assert mock_h1.call_args.kwargs["timeout"] == 30.0 + + # Same config — should return cached client (no rebuild) + with patch("honcho.Honcho", return_value=fake_honcho_2) as mock_h2, \ + patch("hermes_cli.config.load_config", return_value={"honcho": {"timeout": 30}}): + client2 = get_honcho_client(cfg) + + assert client2 is fake_honcho_1 # still cached + mock_h2.assert_not_called() + + # Changed timeout — must rebuild + with patch("honcho.Honcho", return_value=fake_honcho_2) as mock_h3, \ + patch("hermes_cli.config.load_config", return_value={"honcho": {"timeout": 300}}): + client3 = get_honcho_client(cfg) + + assert client3 is fake_honcho_2 # rebuilt + mock_h3.assert_called_once() + assert mock_h3.call_args.kwargs["timeout"] == 300.0 + class TestResolveSessionNameGatewayKey: """Regression tests for gateway_session_key priority in resolve_session_name. @@ -976,6 +1056,74 @@ def test_local_base_url_without_version_unchanged(self): f"Expected 'http://localhost:38000', got {passed_base_url!r}" ) + def test_lan_default_host_empty_key_uses_local_placeholder(self, tmp_path): + """Regression for #61661: setup-style root baseUrl + defaultHost + LAN IP + must not pass an empty/None api_key to the SDK for a no-auth local server.""" + config_file = tmp_path / "honcho.json" + config_file.write_text(json.dumps({ + "defaultHost": "local", + "baseUrl": "http://192.168.2.112:8000", + "hosts": { + "local": { + "workspace": "local-ws", + "aiPeer": "local-ai", + "apiKey": "", + }, + }, + })) + + with patch.dict(os.environ, {}, clear=True), \ + patch("hermes_cli.profiles.get_active_profile_name", return_value="default"), \ + patch("plugins.memory.honcho.client.resolve_config_path", return_value=config_file): + cfg = HonchoClientConfig.from_global_config(config_path=config_file) + + assert cfg.host == "local" + assert cfg.workspace_id == "local-ws" + assert cfg.ai_peer == "local-ai" + assert cfg.api_key is None + assert cfg.base_url == "http://192.168.2.112:8000" + + fake_honcho = MagicMock(name="Honcho") + mock_honcho = MagicMock(return_value=fake_honcho) + fake_honcho_module = types.SimpleNamespace(Honcho=mock_honcho) + with patch.dict(sys.modules, {"honcho": fake_honcho_module}), \ + patch("hermes_cli.config.load_config", return_value={}): + get_honcho_client(cfg) + + mock_honcho.assert_called_once() + assert mock_honcho.call_args.kwargs["api_key"] == "local" + assert mock_honcho.call_args.kwargs["base_url"] == "http://192.168.2.112:8000" + + def test_lan_default_host_explicit_host_key_preserved(self, tmp_path): + """A host-block local JWT still wins for LAN/VPN local URLs.""" + config_file = tmp_path / "honcho.json" + config_file.write_text(json.dumps({ + "defaultHost": "local", + "baseUrl": "http://192.168.2.112:8000", + "hosts": { + "local": { + "workspace": "local-ws", + "aiPeer": "local-ai", + "apiKey": "[redacted:secret-assignment]", + }, + }, + })) + + with patch.dict(os.environ, {}, clear=True), \ + patch("hermes_cli.profiles.get_active_profile_name", return_value="default"), \ + patch("plugins.memory.honcho.client.resolve_config_path", return_value=config_file): + cfg = HonchoClientConfig.from_global_config(config_path=config_file) + + fake_honcho = MagicMock(name="Honcho") + mock_honcho = MagicMock(return_value=fake_honcho) + fake_honcho_module = types.SimpleNamespace(Honcho=mock_honcho) + with patch.dict(sys.modules, {"honcho": fake_honcho_module}), \ + patch("hermes_cli.config.load_config", return_value={}): + get_honcho_client(cfg) + + mock_honcho.assert_called_once() + assert mock_honcho.call_args.kwargs["api_key"] == "local-jwt" + @pytest.mark.skipif( not importlib.util.find_spec("honcho"), reason="honcho SDK not installed" -
tests/honcho_plugin/test_session.py@@ -361,6 +361,44 @@ def test_get_prefetch_context_fetches_user_and_ai_from_peer_api(self): user_peer.context.assert_called_once_with(target=session.user_peer_id) ai_peer.context.assert_called_once_with(target=session.assistant_peer_id) + def test_get_prefetch_context_uses_assistant_observer_for_user_when_ai_observe_others(self): + """With ai_observe_others enabled, get_prefetch_context must query + the user context through the assistant observer, not the user peer.""" + mgr, session = self._make_cached_manager() + mgr._ai_observe_others = True + + assistant_peer = MagicMock() + + def _assistant_context(**kwargs): + if kwargs.get("target") == session.user_peer_id: + return SimpleNamespace( + representation="User via assistant", + peer_card=["Name: Robert"], + ) + if kwargs.get("target") == session.assistant_peer_id: + return SimpleNamespace( + representation="AI self", + peer_card=["Role: Assistant"], + ) + return SimpleNamespace(representation="Unknown", peer_card=[]) + + assistant_peer.context.side_effect = _assistant_context + mgr._get_or_create_peer = MagicMock( + side_effect=[assistant_peer, assistant_peer], + ) + + result = mgr.get_prefetch_context(session.key) + + assert result == { + "representation": "User via assistant", + "card": "Name: Robert", + "ai_representation": "AI self", + "ai_card": "Role: Assistant", + } + assert assistant_peer.context.call_count == 2 + assistant_peer.context.assert_any_call(target=session.user_peer_id) + assistant_peer.context.assert_any_call(target=session.assistant_peer_id) + def test_get_ai_representation_uses_peer_api(self): mgr, session = self._make_cached_manager() ai_peer = MagicMock() @@ -566,6 +604,19 @@ def test_honcho_search_can_target_explicit_peer_id(self): peer="hermes", ) + def test_honcho_search_rejects_whitespace_only_query(self): + """Whitespace-only query must not hit Honcho search API.""" + import json + provider = HonchoMemoryProvider() + provider._session_initialized = True + provider._session_key = "telegram:123" + provider._manager = MagicMock() + + result = provider.handle_tool_call("honcho_search", {"query": " \t\n "}) + parsed = json.loads(result) + assert parsed == {"error": "Missing required parameter: query"} + provider._manager.search_context.assert_not_called() + def test_honcho_reasoning_can_target_explicit_peer_id(self): provider = HonchoMemoryProvider() provider._session_initialized = True @@ -587,6 +638,19 @@ def test_honcho_reasoning_can_target_explicit_peer_id(self): apply_injection_cap=False, ) + def test_honcho_reasoning_rejects_whitespace_only_query(self): + """Whitespace-only query must not hit Honcho dialectic API.""" + import json + provider = HonchoMemoryProvider() + provider._session_initialized = True + provider._session_key = "telegram:123" + provider._manager = MagicMock() + + result = provider.handle_tool_call("honcho_reasoning", {"query": " "}) + parsed = json.loads(result) + assert parsed == {"error": "Missing required parameter: query"} + provider._manager.dialectic_query.assert_not_called() + def test_honcho_conclude_missing_both_params_returns_error(self): """Calling honcho_conclude with neither conclusion nor delete_id returns a tool error.""" import json
-
1 PR closed without merging
From the close comments: merged via another change 1
-
#57437 fix(plugins): rebuild Honcho client when timeout config changes · merged via another change →
#66052
Closed issues · 1
- Closed#57347Honcho client is a per-process singleton — timeout config changes ignored until process restart
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- agent
- Merged PRs
- #66052
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 17 Jul 2026