@inline-chat/hermes-agent-adapter 1.0.0-alpha.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/install.js +95 -95
- package/package.json +10 -4
- package/plugin/inline/adapter.py +104 -74
- package/plugin/inline/cli.py +9 -2
- package/plugin/inline/plugin.yaml +1 -1
- package/plugin/inline/sidecar/index.mjs +846 -801
- package/plugin/inline/tools.py +32 -8
package/plugin/inline/adapter.py
CHANGED
|
@@ -76,6 +76,16 @@ _CHAT_ACCESS_LOOKUP_TIMEOUT_SECONDS = 2.0
|
|
|
76
76
|
_INBOUND_ACCESS_LOOKUP_TIMEOUT_SECONDS = 30.0
|
|
77
77
|
_INBOUND_RETRY_INITIAL_SECONDS = 1.0
|
|
78
78
|
_MAX_INBOUND_DELIVERIES = 16
|
|
79
|
+
# Telegram's drop_pending_on_cold_boot, for Inline: updates the server recorded
|
|
80
|
+
# before this gateway process started are acknowledged without waking the agent.
|
|
81
|
+
# A lost or fresh SDK cursor otherwise replays chat history as new messages.
|
|
82
|
+
_PROCESS_STARTED_AT = time.time()
|
|
83
|
+
_COLD_BOOT_CLOCK_SKEW_SECONDS = 60
|
|
84
|
+
_COLD_BOOT_DROP_LOG_INTERVAL_SECONDS = 60.0
|
|
85
|
+
_COLD_BOOT_GATED_KINDS = frozenset({
|
|
86
|
+
"message.new", "message.edit", "message.delete", "message.history.clear",
|
|
87
|
+
"reaction.add", "reaction.delete", "chat.participant.add", "chat.participant.delete",
|
|
88
|
+
})
|
|
79
89
|
# Provider discovery may invoke host CLIs. Keep repeated panel opens and
|
|
80
90
|
# unrelated mutations off that slow path while always injecting the live
|
|
81
91
|
# current/default model into the document below.
|
|
@@ -610,11 +620,14 @@ def _inline_menu_commands(max_commands: int = _INLINE_COMMAND_LIMIT) -> tuple[Li
|
|
|
610
620
|
|
|
611
621
|
def _inline_skill_catalog() -> List[Dict[str, Any]]:
|
|
612
622
|
"""Return installed, enabled Hermes skills in the Bot API catalog shape."""
|
|
613
|
-
|
|
623
|
+
import tools.skills_tool as skills_tool
|
|
614
624
|
|
|
625
|
+
# Hermes 0.22.5 made these public; older supported hosts keep the private names.
|
|
626
|
+
find_all = getattr(skills_tool, "find_all_skills", None) or getattr(skills_tool, "_find_all_skills")
|
|
627
|
+
sort = getattr(skills_tool, "sort_skills", None) or getattr(skills_tool, "_sort_skills")
|
|
615
628
|
published: List[Dict[str, Any]] = []
|
|
616
629
|
seen: set[str] = set()
|
|
617
|
-
for skill in
|
|
630
|
+
for skill in sort(find_all(skip_disabled=False)):
|
|
618
631
|
if len(published) >= 250:
|
|
619
632
|
break
|
|
620
633
|
key = str(skill.get("name") or "").strip()
|
|
@@ -915,6 +928,14 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
915
928
|
else inline_env("INLINE_OBSERVE_UNMENTIONED_MESSAGES"),
|
|
916
929
|
True,
|
|
917
930
|
)
|
|
931
|
+
self._drop_pending_on_cold_boot = _truthy(
|
|
932
|
+
extra.get("drop_pending_on_cold_boot")
|
|
933
|
+
if "drop_pending_on_cold_boot" in extra
|
|
934
|
+
else inline_env("INLINE_DROP_PENDING_ON_COLD_BOOT"),
|
|
935
|
+
True,
|
|
936
|
+
)
|
|
937
|
+
self._cold_boot_dropped = 0
|
|
938
|
+
self._cold_boot_drop_logged_at = 0.0
|
|
918
939
|
self._reply_thread_mode = _reply_thread_mode(
|
|
919
940
|
extra.get("reply_threads") if "reply_threads" in extra else inline_env("INLINE_REPLY_THREADS"),
|
|
920
941
|
"auto",
|
|
@@ -1015,7 +1036,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1015
1036
|
self._reply_thread_cache: "OrderedDict[str, str]" = OrderedDict()
|
|
1016
1037
|
self._reply_thread_parent_reply_ids: "OrderedDict[str, set[str]]" = OrderedDict()
|
|
1017
1038
|
# Whether each request came from a bot, so answers to bots reply to them and reach them.
|
|
1018
|
-
self.
|
|
1039
|
+
self._request_quotes_sender: "OrderedDict[str, bool]" = OrderedDict()
|
|
1019
1040
|
# Commands this bot advertises in Inline's command menu, as last synced.
|
|
1020
1041
|
self._advertised_commands: set[str] = set()
|
|
1021
1042
|
self._reply_thread_parent_typing_targets: "OrderedDict[str, str]" = OrderedDict()
|
|
@@ -1176,18 +1197,23 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1176
1197
|
return {**(metadata or {}), "thread_id": thread_id} if thread_id else metadata
|
|
1177
1198
|
|
|
1178
1199
|
def set_reply_threads_from_tool(
|
|
1179
|
-
self, *, chat_id: str, chat_type: str,
|
|
1200
|
+
self, *, chat_id: str, chat_type: str, parent_chat_id: str, actor_id: str, actor_is_bot: bool, mode: str,
|
|
1180
1201
|
) -> Dict[str, Any]:
|
|
1181
|
-
"""Change
|
|
1182
|
-
|
|
1202
|
+
"""Change reply threads because the person talking to the agent asked, with the access
|
|
1203
|
+
/threads needs. Inside a reply thread it changes the parent chat, as /threads does,
|
|
1204
|
+
and chat_type is the parent's."""
|
|
1183
1205
|
if actor_is_bot or not actor_id:
|
|
1184
1206
|
raise PermissionError("only a person can change reply threads")
|
|
1185
|
-
|
|
1186
|
-
|
|
1207
|
+
target_chat_id = parent_chat_id or chat_id
|
|
1208
|
+
if parent_chat_id and self._actor_authorization(chat_type, actor_id, parent_chat_id) is not True:
|
|
1209
|
+
raise PermissionError("parent chat access could not be confirmed")
|
|
1187
1210
|
if not self._may_change_reply_threads(actor_id, chat_type):
|
|
1188
1211
|
raise PermissionError("changing reply threads needs access to the threads command")
|
|
1189
|
-
self._set_reply_threads_for_chat(
|
|
1190
|
-
return {"chat_id": self._chat_key(
|
|
1212
|
+
self._set_reply_threads_for_chat(target_chat_id, None if mode in {"default", "reset"} else _reply_thread_mode(mode))
|
|
1213
|
+
return {"chat_id": self._chat_key(target_chat_id), "mode": self._reply_thread_mode_for_chat(target_chat_id)}
|
|
1214
|
+
|
|
1215
|
+
def chat_type_from_info(self, info: Dict[str, Any]) -> str:
|
|
1216
|
+
return self._bot_settings_chat_type(info)
|
|
1191
1217
|
|
|
1192
1218
|
@staticmethod
|
|
1193
1219
|
def _bot_settings_chat_type(info: Dict[str, Any]) -> str:
|
|
@@ -1952,6 +1978,12 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1952
1978
|
self._inbound_task = asyncio.get_event_loop().create_task(self._inbound_loop())
|
|
1953
1979
|
self._mark_connected()
|
|
1954
1980
|
logger.info("[inline] connected via sidecar on %s:%d", self._sidecar_bind, self._sidecar_port)
|
|
1981
|
+
if not is_reconnect:
|
|
1982
|
+
logger.info(
|
|
1983
|
+
"[inline] Cold boot: %s Inline updates recorded before this gateway started "
|
|
1984
|
+
"(drop_pending_on_cold_boot: %s)",
|
|
1985
|
+
"dropping" if self._drop_pending_on_cold_boot else "preserving",
|
|
1986
|
+
"true" if self._drop_pending_on_cold_boot else "false")
|
|
1955
1987
|
self._schedule_bot_command_sync()
|
|
1956
1988
|
return True
|
|
1957
1989
|
|
|
@@ -2392,6 +2424,8 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2392
2424
|
# owns a coherent client-state cache.
|
|
2393
2425
|
self._invalidate_chat_info(event.get("chatId"))
|
|
2394
2426
|
kind = event.get("kind")
|
|
2427
|
+
if kind in _COLD_BOOT_GATED_KINDS and self._recorded_before_cold_boot(event):
|
|
2428
|
+
return
|
|
2395
2429
|
if kind == "bot.chatSettings.request":
|
|
2396
2430
|
if event.get("_inlineDeliveryId"):
|
|
2397
2431
|
await self._handle_bot_settings_request(event)
|
|
@@ -2436,6 +2470,29 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2436
2470
|
return
|
|
2437
2471
|
await self._dispatch_message(event)
|
|
2438
2472
|
|
|
2473
|
+
def _recorded_before_cold_boot(self, event: Dict[str, Any]) -> bool:
|
|
2474
|
+
"""Whether this update predates the gateway process and must not wake the agent.
|
|
2475
|
+
|
|
2476
|
+
The update date is when the server recorded the change, so a live edit or reaction
|
|
2477
|
+
on an old message still passes. Updates without a date pass rather than risk
|
|
2478
|
+
dropping live work."""
|
|
2479
|
+
if not self._drop_pending_on_cold_boot:
|
|
2480
|
+
return False
|
|
2481
|
+
date = _to_int(event.get("date"))
|
|
2482
|
+
if date is None and event.get("kind") == "message.new":
|
|
2483
|
+
date = _to_int((event.get("message") or {}).get("date"))
|
|
2484
|
+
if date is None or date <= 0 or date >= _PROCESS_STARTED_AT - _COLD_BOOT_CLOCK_SKEW_SECONDS:
|
|
2485
|
+
return False
|
|
2486
|
+
self._cold_boot_dropped += 1
|
|
2487
|
+
now = time.time()
|
|
2488
|
+
if now - self._cold_boot_drop_logged_at >= _COLD_BOOT_DROP_LOG_INTERVAL_SECONDS:
|
|
2489
|
+
self._cold_boot_drop_logged_at = now
|
|
2490
|
+
logger.info(
|
|
2491
|
+
"[inline] dropped %d update(s) recorded before this gateway started; latest %s in chat %s, %ds before startup",
|
|
2492
|
+
self._cold_boot_dropped, event.get("kind"), event.get("chatId") or "?",
|
|
2493
|
+
int(_PROCESS_STARTED_AT - date))
|
|
2494
|
+
return True
|
|
2495
|
+
|
|
2439
2496
|
def _is_duplicate(self, key: str) -> bool:
|
|
2440
2497
|
now = time.time()
|
|
2441
2498
|
old = self._seen_messages.get(key)
|
|
@@ -2683,13 +2740,17 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2683
2740
|
if not mentioned and not reply_wakes_thread and (addressed_to_others or not ambient):
|
|
2684
2741
|
self._remember_observed_context(chat_id, msg, text)
|
|
2685
2742
|
return
|
|
2686
|
-
|
|
2743
|
+
sender_is_bot = sender_profile.get("bot") is True
|
|
2744
|
+
self._remember_request_sender(chat_id, msg_id, sender_is_bot and (explicitly_mentions_me or command_addressed_to_me))
|
|
2687
2745
|
if edit:
|
|
2688
2746
|
text = f"message:edited:{text}" if text else "message:edited"
|
|
2689
2747
|
if (
|
|
2690
2748
|
not edit
|
|
2691
2749
|
and not agent_action
|
|
2692
2750
|
and not thread_id
|
|
2751
|
+
# A bot's message is answered in place even in a chat set to On: it wakes on
|
|
2752
|
+
# replies in the chat it asked from, and a thread would never reach it.
|
|
2753
|
+
and sender_profile.get("bot") is not True
|
|
2693
2754
|
and self._should_create_reply_thread_for_message(chat_id=chat_id, parent_chat_id=parent_chat_id)
|
|
2694
2755
|
):
|
|
2695
2756
|
created_thread_id = await self._create_reply_thread(chat_id, msg_id, text, reply_to_id)
|
|
@@ -2769,33 +2830,14 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2769
2830
|
reply_context_messages=context_backfill["reply_context_messages"],
|
|
2770
2831
|
recent_messages=context_backfill["recent_messages"],
|
|
2771
2832
|
)
|
|
2772
|
-
metadata = self._inline_event_metadata(
|
|
2773
|
-
chat_id=chat_id,
|
|
2774
|
-
msg_id=msg_id,
|
|
2775
|
-
from_id=from_id,
|
|
2776
|
-
thread_id=thread_id,
|
|
2777
|
-
parent_chat_id=parent_chat_id,
|
|
2778
|
-
parent_message_id=parent_message_id,
|
|
2779
|
-
entity_text=entity_text,
|
|
2780
|
-
)
|
|
2781
|
-
if agent_action:
|
|
2782
|
-
metadata["inline"]["action"] = {
|
|
2783
|
-
"event_kind": "message.action.invoke",
|
|
2784
|
-
"actor_user_id": str(agent_action.get("actorUserId") or ""),
|
|
2785
|
-
"chat_id": str(agent_action.get("chatId") or ""),
|
|
2786
|
-
"target_message_id": str(agent_action.get("messageId") or ""),
|
|
2787
|
-
"interaction_id": str(agent_action.get("interactionId") or ""),
|
|
2788
|
-
"action_id": str(agent_action.get("actionId") or ""),
|
|
2789
|
-
"callback_data_base64": str(agent_action.get("dataBase64") or ""),
|
|
2790
|
-
}
|
|
2791
|
-
|
|
2792
2833
|
session_chat_id = _session_chat_id(chat_id, chat_type, thread_id, parent_chat_id)
|
|
2793
2834
|
try:
|
|
2794
2835
|
from . import tools as _inline_tools
|
|
2795
2836
|
_inline_tools.remember_turn_message(
|
|
2796
2837
|
session_chat_id=session_chat_id, thread_id=str(thread_id or ""), message_id=msg_id,
|
|
2797
2838
|
chat_id=chat_id, sender_id=from_id, sender_is_bot=sender_profile.get("bot") is True,
|
|
2798
|
-
chat_type=chat_type,
|
|
2839
|
+
chat_type=chat_type,
|
|
2840
|
+
parent_chat_id=parent_chat_id if parent_chat_id and parent_chat_id != chat_id else "",
|
|
2799
2841
|
)
|
|
2800
2842
|
except Exception:
|
|
2801
2843
|
logger.debug("[inline] failed to record the turn's message for tools", exc_info=True)
|
|
@@ -2810,7 +2852,11 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2810
2852
|
parent_chat_id=parent_chat_id,
|
|
2811
2853
|
message_id=msg_id,
|
|
2812
2854
|
)
|
|
2855
|
+
# A bot's answer coming back needs no reply: the agent may stay silent instead of Hermes
|
|
2856
|
+
# posting its "needed a reply" warning. Hosts before reply_expected ignore this.
|
|
2857
|
+
answer_from_bot = sender_is_bot and not explicitly_mentions_me and not command_addressed_to_me
|
|
2813
2858
|
await self.handle_message(MessageEvent(
|
|
2859
|
+
**({"reply_expected": False} if answer_from_bot and "reply_expected" in getattr(MessageEvent, "__dataclass_fields__", {}) else {}),
|
|
2814
2860
|
text=text,
|
|
2815
2861
|
message_type=message_type,
|
|
2816
2862
|
source=source,
|
|
@@ -2833,7 +2879,6 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2833
2879
|
auto_skill=auto_skill,
|
|
2834
2880
|
channel_prompt=channel_prompt,
|
|
2835
2881
|
channel_context=channel_context,
|
|
2836
|
-
metadata=metadata,
|
|
2837
2882
|
timestamp=self._timestamp(event.get("date") or msg.get("date")),
|
|
2838
2883
|
allow_gateway_control=not bool(agent_action),
|
|
2839
2884
|
))
|
|
@@ -3817,33 +3862,6 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3817
3862
|
prefix += " from an unknown sender"
|
|
3818
3863
|
return f"{prefix}: {text}"
|
|
3819
3864
|
|
|
3820
|
-
def _inline_event_metadata(
|
|
3821
|
-
self,
|
|
3822
|
-
*,
|
|
3823
|
-
chat_id: str,
|
|
3824
|
-
msg_id: str,
|
|
3825
|
-
from_id: str,
|
|
3826
|
-
thread_id: Optional[str],
|
|
3827
|
-
parent_chat_id: Optional[str],
|
|
3828
|
-
parent_message_id: Optional[str],
|
|
3829
|
-
entity_text: Optional[str],
|
|
3830
|
-
) -> Dict[str, Any]:
|
|
3831
|
-
inline: Dict[str, Any] = {
|
|
3832
|
-
"chat_id": self._chat_key(chat_id),
|
|
3833
|
-
"message_id": str(msg_id),
|
|
3834
|
-
}
|
|
3835
|
-
if from_id:
|
|
3836
|
-
inline["sender_user_id"] = self._chat_key(from_id)
|
|
3837
|
-
if thread_id:
|
|
3838
|
-
inline["thread_id"] = self._chat_key(thread_id)
|
|
3839
|
-
if parent_chat_id:
|
|
3840
|
-
inline["parent_chat_id"] = self._chat_key(parent_chat_id)
|
|
3841
|
-
if parent_message_id:
|
|
3842
|
-
inline["parent_message_id"] = str(parent_message_id)
|
|
3843
|
-
if entity_text:
|
|
3844
|
-
inline["message_entities"] = entity_text
|
|
3845
|
-
return {"inline": inline}
|
|
3846
|
-
|
|
3847
3865
|
@staticmethod
|
|
3848
3866
|
def _reply_thread_title(text: str) -> str:
|
|
3849
3867
|
title = strip_markdown(str(text or "")).strip()
|
|
@@ -3937,8 +3955,10 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3937
3955
|
async def _reply_to_for_target(
|
|
3938
3956
|
self, reply_to: Optional[str], target: Dict[str, str], *, request_chat_id: str = "", anchored: bool = False,
|
|
3939
3957
|
) -> Optional[str]:
|
|
3940
|
-
"""Answers quote the message only when
|
|
3941
|
-
own messages, so that is how
|
|
3958
|
+
"""Answers quote the message only when a bot asked by addressing this one: a bot wakes on
|
|
3959
|
+
replies to its own messages, so that is how the answer reaches it. A bot's reply to this
|
|
3960
|
+
bot is that answer coming back and gets a plain response, otherwise two bots would quote
|
|
3961
|
+
each other forever; another round takes a new mention. People get plain answers."""
|
|
3942
3962
|
if not reply_to:
|
|
3943
3963
|
return None
|
|
3944
3964
|
target_chat_id = target.get("chatId") or ""
|
|
@@ -3949,8 +3969,8 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3949
3969
|
if anchored:
|
|
3950
3970
|
return str(reply_to)
|
|
3951
3971
|
key = f"{self._chat_key(target_chat_id)}:{reply_to}"
|
|
3952
|
-
|
|
3953
|
-
if
|
|
3972
|
+
quotes_sender = self._request_quotes_sender.get(key)
|
|
3973
|
+
if quotes_sender is None and target_chat_id and self._chat_key(request_chat_id) == self._chat_key(target_chat_id):
|
|
3954
3974
|
info = await self._get_chat_info(target_chat_id)
|
|
3955
3975
|
if not info:
|
|
3956
3976
|
return None
|
|
@@ -3962,16 +3982,16 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3962
3982
|
# Message ids are per chat, so only look it up where the request was sent; an answer
|
|
3963
3983
|
# moved into a thread would otherwise read the thread's message with the same id.
|
|
3964
3984
|
fetched = await self._fetch_message(target_chat_id, str(reply_to))
|
|
3965
|
-
|
|
3966
|
-
self._remember_request_sender(target_chat_id, str(reply_to),
|
|
3967
|
-
return str(reply_to) if
|
|
3985
|
+
quotes_sender = bool(fetched) and _inline_sender_profile({}, fetched).get("bot") is True and self._message_entity_mentions_me(fetched)
|
|
3986
|
+
self._remember_request_sender(target_chat_id, str(reply_to), quotes_sender)
|
|
3987
|
+
return str(reply_to) if quotes_sender else None
|
|
3968
3988
|
|
|
3969
|
-
def _remember_request_sender(self, chat_id: str, msg_id: str,
|
|
3989
|
+
def _remember_request_sender(self, chat_id: str, msg_id: str, quotes_sender: bool) -> None:
|
|
3970
3990
|
key = f"{self._chat_key(chat_id)}:{msg_id}"
|
|
3971
|
-
self.
|
|
3972
|
-
self.
|
|
3973
|
-
while len(self.
|
|
3974
|
-
self.
|
|
3991
|
+
self._request_quotes_sender[key] = quotes_sender
|
|
3992
|
+
self._request_quotes_sender.move_to_end(key)
|
|
3993
|
+
while len(self._request_quotes_sender) > _CHAT_INFO_CACHE_MAX_SIZE:
|
|
3994
|
+
self._request_quotes_sender.popitem(last=False)
|
|
3975
3995
|
|
|
3976
3996
|
async def _get_chat_info(self, chat_id: str, *, required: bool = False) -> Dict[str, Any]:
|
|
3977
3997
|
target = _target_from_chat_id(chat_id)
|
|
@@ -5555,6 +5575,12 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
5555
5575
|
if len(mapping) > limit:
|
|
5556
5576
|
mapping.popitem(last=False)
|
|
5557
5577
|
|
|
5578
|
+
def _send_retry_is_final(self, result: SendResult) -> bool:
|
|
5579
|
+
# The request reached Inline but its result was lost. A retry or plain-text
|
|
5580
|
+
# fallback would post the message twice; Hermes treats timeouts the same way.
|
|
5581
|
+
raw = getattr(result, "raw_response", None)
|
|
5582
|
+
return isinstance(raw, dict) and raw.get("outcomeUnknown") is True
|
|
5583
|
+
|
|
5558
5584
|
async def _send_sidecar(self, path: str, body: Dict[str, Any]) -> SendResult:
|
|
5559
5585
|
try:
|
|
5560
5586
|
data = await self._sidecar_call(path, body)
|
|
@@ -5570,12 +5596,15 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
5570
5596
|
except InlineSidecarError as exc:
|
|
5571
5597
|
if exc.error_kind == "unknown":
|
|
5572
5598
|
self._report_error("sidecar.send", exc)
|
|
5599
|
+
raw = exc.raw if isinstance(exc.raw, dict) else {}
|
|
5600
|
+
retry_after = _to_int(raw.get("retryAfterSeconds"))
|
|
5573
5601
|
return _send_result(
|
|
5574
5602
|
success=False,
|
|
5575
5603
|
error=str(exc),
|
|
5576
5604
|
raw_response=exc.raw,
|
|
5577
5605
|
retryable=exc.retryable,
|
|
5578
5606
|
error_kind=exc.error_kind,
|
|
5607
|
+
retry_after=float(retry_after) if retry_after and retry_after > 0 else None,
|
|
5579
5608
|
)
|
|
5580
5609
|
except Exception as exc:
|
|
5581
5610
|
self._report_error("sidecar.send", exc)
|
|
@@ -5603,8 +5632,9 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
5603
5632
|
|
|
5604
5633
|
def _send_result(**kwargs: Any) -> SendResult:
|
|
5605
5634
|
fields = getattr(SendResult, "__dataclass_fields__", {})
|
|
5606
|
-
|
|
5607
|
-
kwargs
|
|
5635
|
+
for optional in ("error_kind", "retry_after"):
|
|
5636
|
+
if optional in kwargs and optional not in fields:
|
|
5637
|
+
kwargs.pop(optional)
|
|
5608
5638
|
return SendResult(**kwargs)
|
|
5609
5639
|
|
|
5610
5640
|
|
package/plugin/inline/cli.py
CHANGED
|
@@ -27,7 +27,10 @@ _CLI_INSTALL_URL = "https://inline.chat/cli/install.sh"
|
|
|
27
27
|
_MAX_TOKEN_BYTES = 16 * 1024
|
|
28
28
|
_MAX_PROBE_RESPONSE_BYTES = 64 * 1024
|
|
29
29
|
_MACHINE_SETUP_PROTOCOL_VERSION = 1
|
|
30
|
-
|
|
30
|
+
# Hermes' own tolerance: macOS readings of one process's start time drift about a second
|
|
31
|
+
# after boot-clock adjustments, and 200 is 2 s on every platform's scale.
|
|
32
|
+
_START_TIME_DRIFT_TOLERANCE = 200
|
|
33
|
+
_PROBE_USER_AGENT = "inline-hermes-agent-adapter/1.0.0"
|
|
31
34
|
_ENV_REFERENCE_RE = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$")
|
|
32
35
|
|
|
33
36
|
|
|
@@ -581,7 +584,11 @@ def _gateway_status() -> dict:
|
|
|
581
584
|
return {"supported": False, "ready": False, "reason": "runtime_writer_identity_unavailable"}
|
|
582
585
|
# Runtime status can retain platform entries from the prior process.
|
|
583
586
|
# Only the current process's own connected projection is readiness proof.
|
|
584
|
-
|
|
587
|
+
writer_start_time = inline.get("writer_start_time") if isinstance(inline, dict) else None
|
|
588
|
+
writer_matches = (
|
|
589
|
+
isinstance(inline, dict) and inline.get("writer_pid") == pid and type(writer_start_time) is int
|
|
590
|
+
and abs(writer_start_time - start_time) <= _START_TIME_DRIFT_TOLERANCE
|
|
591
|
+
)
|
|
585
592
|
ready = generation is not None and writer_matches and state == "running" and platform_state == "connected"
|
|
586
593
|
return {
|
|
587
594
|
"supported": True,
|