@inline-chat/hermes-agent-adapter 0.0.10 → 0.0.12
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/README.md +9 -2
- package/package.json +1 -1
- package/plugin/inline/adapter.py +433 -22
- package/plugin/inline/cli.py +1 -1
- package/plugin/inline/plugin.yaml +5 -1
- package/plugin/inline/sidecar/index.mjs +24 -1
package/README.md
CHANGED
|
@@ -147,7 +147,7 @@ uv run ./hermes plugins list --plain --no-bundled
|
|
|
147
147
|
Expected local output includes:
|
|
148
148
|
|
|
149
149
|
```text
|
|
150
|
-
enabled user 0.0.
|
|
150
|
+
enabled user 0.0.12 inline-platform
|
|
151
151
|
```
|
|
152
152
|
|
|
153
153
|
## Update Or Reinstall
|
|
@@ -261,6 +261,12 @@ progress message after a successful final reply. Token streaming requires both
|
|
|
261
261
|
top-level `streaming.enabled: true` and
|
|
262
262
|
`display.platforms.inline.streaming: true`.
|
|
263
263
|
|
|
264
|
+
Hermes-native sends and scheduled deliveries accept Inline's explicit target
|
|
265
|
+
forms: a bare positive ID or `chat:ID` for a chat, `thread:ID` for a routable
|
|
266
|
+
reply thread, and `user:ID` for a direct user target. IDs are validated as
|
|
267
|
+
positive signed 64-bit integers before delivery. The parser hooks activate on
|
|
268
|
+
Hermes versions that expose the native plugin target-resolution contract.
|
|
269
|
+
|
|
264
270
|
Hermes also accepts a top-level `inline:` block for plugin-owned settings, but
|
|
265
271
|
`platforms.inline` matches the shape used by most Hermes platform docs and is
|
|
266
272
|
the safest form to copy into `~/.hermes/config.yaml`.
|
|
@@ -293,6 +299,7 @@ Access control follows Hermes' native platform model:
|
|
|
293
299
|
| `INLINE_CONTEXT_HISTORY_LIMIT` | Legacy compatibility shortcut. `0` maps to `INLINE_CONTEXT_BACKFILL=off`; `1` through `20` maps to `always` with that thread-context limit. Prefer the explicit settings above. |
|
|
294
300
|
| `INLINE_SETTINGS_PATH` | JSON settings file for per-chat `/threads` overrides. `/threads` shows native Auto/On/Off buttons; `reset` clears the chat override back to the global default. Defaults next to `INLINE_STATE_PATH`; `.env`-like paths are refused. |
|
|
295
301
|
| `INLINE_SYSTEM_EVENTS` | Delivers Inline lifecycle events such as edits, deletes, and participant changes as synthetic messages. Defaults to `false`. Reactions on bot messages are always delivered. |
|
|
302
|
+
| `INLINE_REACTIONS` | Shows 👀 while Hermes handles an inbound message, then ✅ on success or ❌ on failure. Cancellation clears the working marker. Defaults to `false`. |
|
|
296
303
|
| `INLINE_MENTION_PATTERNS` | JSON list, comma-separated, or newline-separated regex patterns for group wake words. |
|
|
297
304
|
| `INLINE_PARSE_MARKDOWN` | Controls whether supported outbound Inline Markdown is parsed. Defaults to `true`; `false` preserves the supplied syntax literally. |
|
|
298
305
|
| `INLINE_SYNC_COMMANDS` | Syncs Hermes slash commands into Inline's native `/` bot command menu on gateway connect. Defaults to `true`. |
|
|
@@ -316,7 +323,7 @@ Equivalent Hermes YAML can use `allow_from`, `allowed_users`,
|
|
|
316
323
|
`strict_mention`, `allowed_chats`, `free_response_chats`, `reply_threads`,
|
|
317
324
|
`context_backfill`, `thread_context_limit`, `reply_context_limit`,
|
|
318
325
|
`observed_context_limit`, `observe_unmentioned_messages`, `settings_path`, and
|
|
319
|
-
`mention_patterns` under the Inline platform config. Operational settings such
|
|
326
|
+
`mention_patterns`, and `reactions` under the Inline platform config. Operational settings such
|
|
320
327
|
as `base_url`, `parse_markdown`, `media_max_mb`, `upload_max_mb`,
|
|
321
328
|
`state_path`, `sidecar_port`, `connect_timeout_ms`, `sync_commands`, and
|
|
322
329
|
`command_limit` can also be set there.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inline-chat/hermes-agent-adapter",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "Hermes Agent platform adapter for Inline, with a native Python plugin and bundled Inline realtime sidecar.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/plugin/inline/adapter.py
CHANGED
|
@@ -62,12 +62,18 @@ _DEFAULT_SIDECAR_PORT = 8794
|
|
|
62
62
|
_DEFAULT_SIDECAR_BIND = "127.0.0.1"
|
|
63
63
|
_MAX_MESSAGE_LENGTH = 4000
|
|
64
64
|
_MODEL_PAGE_SIZE = 8
|
|
65
|
+
_CHOICE_PICKER_MAX_CHOICES = 16
|
|
66
|
+
_CHOICE_PICKER_TTL_SECONDS = 2 * 60
|
|
67
|
+
_UPDATE_PROMPT_TTL_SECONDS = 5 * 60
|
|
65
68
|
_DEDUP_MAX_SIZE = 5000
|
|
66
69
|
_DEDUP_WINDOW_SECONDS = 48 * 3600
|
|
67
70
|
_CHAT_INFO_CACHE_SECONDS = 10 * 60
|
|
68
71
|
_CHAT_INFO_CACHE_MAX_SIZE = 512
|
|
69
72
|
_BOT_SETTINGS_CHAT_INFO_TIMEOUT_SECONDS = 2.0
|
|
70
|
-
|
|
73
|
+
# Provider discovery may invoke host CLIs. Keep repeated panel opens and
|
|
74
|
+
# unrelated mutations off that slow path while always injecting the live
|
|
75
|
+
# current/default model into the document below.
|
|
76
|
+
_BOT_SETTINGS_MODEL_CATALOG_CACHE_SECONDS = 5 * 60
|
|
71
77
|
_DEFAULT_CONTEXT_BACKFILL = "selective"
|
|
72
78
|
_CONTEXT_BACKFILL_MODES = {"off", "selective", "always"}
|
|
73
79
|
_DEFAULT_THREAD_CONTEXT_LIMIT = 30
|
|
@@ -88,6 +94,7 @@ _SIDECAR_DIR = Path(__file__).parent / "sidecar"
|
|
|
88
94
|
_SIDECAR_ENTRY = _SIDECAR_DIR / "index.mjs"
|
|
89
95
|
_DEFAULT_MEDIA_MAX_MB = 25
|
|
90
96
|
_DEFAULT_UPLOAD_MAX_MB = 300
|
|
97
|
+
_MAX_INLINE_ID = 9_223_372_036_854_775_807
|
|
91
98
|
_MIN_NODE_MAJOR = 20
|
|
92
99
|
_DEFAULT_CONNECT_TIMEOUT_MS = 20_000
|
|
93
100
|
_INLINE_COMMAND_LIMIT = 100
|
|
@@ -348,13 +355,81 @@ def _target_from_chat_id(chat_id: str) -> Dict[str, str]:
|
|
|
348
355
|
raw = str(chat_id or "").strip()
|
|
349
356
|
if raw.startswith("inline:"):
|
|
350
357
|
raw = raw[len("inline:"):].strip()
|
|
351
|
-
if raw.startswith("chat:"):
|
|
352
|
-
return {"chatId": raw
|
|
358
|
+
if raw.startswith(("chat:", "thread:")):
|
|
359
|
+
return {"chatId": raw.split(":", 1)[1].strip()}
|
|
353
360
|
if raw.startswith("user:"):
|
|
354
361
|
return {"userId": raw[len("user:"):].strip()}
|
|
355
362
|
return {"chatId": raw}
|
|
356
363
|
|
|
357
364
|
|
|
365
|
+
def _normalize_inline_target_id(value: Any) -> Optional[str]:
|
|
366
|
+
text = str(value or "").strip()
|
|
367
|
+
if not re.fullmatch(r"[0-9]+", text):
|
|
368
|
+
return None
|
|
369
|
+
parsed = int(text)
|
|
370
|
+
if parsed <= 0 or parsed > _MAX_INLINE_ID:
|
|
371
|
+
return None
|
|
372
|
+
return str(parsed)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _parse_inline_target_ref(target_ref: str) -> Optional[tuple[str, Optional[str]]]:
|
|
376
|
+
"""Normalize Hermes delivery refs without losing direct-user routing."""
|
|
377
|
+
raw = str(target_ref or "").strip()
|
|
378
|
+
if raw.lower().startswith("inline:"):
|
|
379
|
+
raw = raw.split(":", 1)[1].strip()
|
|
380
|
+
if not raw:
|
|
381
|
+
return None
|
|
382
|
+
|
|
383
|
+
prefix = ""
|
|
384
|
+
if ":" in raw:
|
|
385
|
+
prefix, raw = raw.split(":", 1)
|
|
386
|
+
prefix = prefix.strip().lower()
|
|
387
|
+
raw = raw.strip()
|
|
388
|
+
if prefix not in {"chat", "thread", "user"} or not raw or ":" in raw:
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
normalized = _normalize_inline_target_id(raw)
|
|
392
|
+
if normalized is None:
|
|
393
|
+
# Preserve recognized explicit refs for the validator so callers get
|
|
394
|
+
# the same signed-Int64 diagnostic as other Inline ID entry points.
|
|
395
|
+
if prefix:
|
|
396
|
+
return (f"user:{raw}" if prefix == "user" else raw), None
|
|
397
|
+
return None
|
|
398
|
+
if prefix == "user":
|
|
399
|
+
return f"user:{normalized}", None
|
|
400
|
+
return normalized, None
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _validate_inline_target_ref(chat_id: str) -> bool | str:
|
|
404
|
+
raw = str(chat_id or "").strip()
|
|
405
|
+
if raw.lower().startswith("inline:"):
|
|
406
|
+
raw = raw.split(":", 1)[1].strip()
|
|
407
|
+
if ":" in raw:
|
|
408
|
+
prefix, raw = raw.split(":", 1)
|
|
409
|
+
if prefix.strip().lower() not in {"chat", "thread", "user"}:
|
|
410
|
+
return "Use a bare Inline ID or a chat:, thread:, or user: target"
|
|
411
|
+
raw = raw.strip()
|
|
412
|
+
if _normalize_inline_target_id(raw) is None:
|
|
413
|
+
return "Inline targets must use a positive signed 64-bit integer ID"
|
|
414
|
+
return True
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _target_registration_hooks() -> Dict[str, Callable[..., Any]]:
|
|
418
|
+
"""Expose newer Hermes target hooks without breaking older runtimes."""
|
|
419
|
+
try:
|
|
420
|
+
from gateway.platform_registry import PlatformEntry
|
|
421
|
+
|
|
422
|
+
fields = getattr(PlatformEntry, "__dataclass_fields__", {})
|
|
423
|
+
if {"parse_target_ref_fn", "validate_target_ref_fn"}.issubset(fields):
|
|
424
|
+
return {
|
|
425
|
+
"parse_target_ref_fn": _parse_inline_target_ref,
|
|
426
|
+
"validate_target_ref_fn": _validate_inline_target_ref,
|
|
427
|
+
}
|
|
428
|
+
except Exception:
|
|
429
|
+
logger.debug("[inline] Hermes target parser hooks are unavailable", exc_info=True)
|
|
430
|
+
return {}
|
|
431
|
+
|
|
432
|
+
|
|
358
433
|
def _inline_sender_profile(event: Dict[str, Any], message: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
359
434
|
raw = event.get("sender")
|
|
360
435
|
if not isinstance(raw, dict) and isinstance(message, dict):
|
|
@@ -670,6 +745,7 @@ def _apply_yaml_config(yaml_cfg: dict, platform_cfg: dict) -> Optional[dict]:
|
|
|
670
745
|
"text_chunk_limit",
|
|
671
746
|
"reply_threads",
|
|
672
747
|
"system_events",
|
|
748
|
+
"reactions",
|
|
673
749
|
"channel_prompts",
|
|
674
750
|
"channel_skill_bindings",
|
|
675
751
|
"typing_indicator",
|
|
@@ -735,6 +811,10 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
735
811
|
extra.get("system_events") if "system_events" in extra else os.getenv("INLINE_SYSTEM_EVENTS"),
|
|
736
812
|
False,
|
|
737
813
|
)
|
|
814
|
+
self._processing_reactions = _truthy(
|
|
815
|
+
extra.get("reactions") if "reactions" in extra else os.getenv("INLINE_REACTIONS"),
|
|
816
|
+
False,
|
|
817
|
+
)
|
|
738
818
|
self._sync_commands = _truthy(
|
|
739
819
|
extra.get("sync_commands") if "sync_commands" in extra else os.getenv("INLINE_SYNC_COMMANDS"),
|
|
740
820
|
True,
|
|
@@ -874,7 +954,11 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
874
954
|
self._clarify_sessions: "OrderedDict[str, str]" = OrderedDict()
|
|
875
955
|
self._approval_sessions: "OrderedDict[str, str]" = OrderedDict()
|
|
876
956
|
self._slash_sessions: "OrderedDict[str, str]" = OrderedDict()
|
|
957
|
+
self._choice_picker_sessions: "OrderedDict[str, Dict[str, Any]]" = OrderedDict()
|
|
958
|
+
self._update_prompt_sessions: "OrderedDict[str, Dict[str, Any]]" = OrderedDict()
|
|
877
959
|
self._model_picker_sessions: "OrderedDict[str, Dict[str, Any]]" = OrderedDict()
|
|
960
|
+
self._status_message_ids: "OrderedDict[tuple[str, str, str], str]" = OrderedDict()
|
|
961
|
+
self._processing_reaction_messages: "OrderedDict[tuple[str, str, str], Dict[str, str]]" = OrderedDict()
|
|
878
962
|
self._thread_action_sessions: "OrderedDict[str, Dict[str, Any]]" = OrderedDict()
|
|
879
963
|
self._chat_info_cache: "OrderedDict[str, tuple[float, Dict[str, Any]]]" = OrderedDict()
|
|
880
964
|
self._bot_agent_cache: "OrderedDict[str, tuple[float, Dict[str, Any]]]" = OrderedDict()
|
|
@@ -1058,7 +1142,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1058
1142
|
"actor_id": actor_id,
|
|
1059
1143
|
"chat_type": chat_type,
|
|
1060
1144
|
"is_reply_thread": is_reply_thread,
|
|
1061
|
-
"following": self._chat_follow_mode_following(info),
|
|
1145
|
+
"following": None if chat_type == "dm" else self._chat_follow_mode_following(info),
|
|
1062
1146
|
"reply_threads": self._reply_thread_mode_for_chat(scope_chat_id),
|
|
1063
1147
|
"can_set_default_model": can_modify,
|
|
1064
1148
|
"source": source,
|
|
@@ -1250,13 +1334,13 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1250
1334
|
}},
|
|
1251
1335
|
})
|
|
1252
1336
|
|
|
1253
|
-
sections: List[Dict[str, Any]] = [
|
|
1254
|
-
|
|
1255
|
-
{"id": "attention", "items": [{
|
|
1337
|
+
sections: List[Dict[str, Any]] = [{"id": "runtime", "items": runtime_items}]
|
|
1338
|
+
if context.get("following") is not None:
|
|
1339
|
+
sections.append({"id": "attention", "items": [{
|
|
1256
1340
|
"id": "following", "label": "Following", "description": "Wake on eligible activity.", **common,
|
|
1257
1341
|
"control": {"oneofKind": "toggle", "toggle": {"value": bool(context.get("following"))}},
|
|
1258
|
-
}]}
|
|
1259
|
-
|
|
1342
|
+
}]})
|
|
1343
|
+
sections.append({"id": "replies", "items": [{
|
|
1260
1344
|
"id": "reply-threads", "label": "Reply in threads", **common,
|
|
1261
1345
|
"control": {"oneofKind": "select", "select": {
|
|
1262
1346
|
"value": context.get("reply_threads") or "auto",
|
|
@@ -1266,8 +1350,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1266
1350
|
{"value": "off", "label": "Off", "description": "Stay in chat.", "disabled": False},
|
|
1267
1351
|
],
|
|
1268
1352
|
}},
|
|
1269
|
-
}]}
|
|
1270
|
-
]
|
|
1353
|
+
}]})
|
|
1271
1354
|
if disabled:
|
|
1272
1355
|
sections.append({"id": "access", "items": [{
|
|
1273
1356
|
"id": "read-only", "label": "Read-only", "disabled": False,
|
|
@@ -1351,14 +1434,11 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1351
1434
|
runner = context.get("runner")
|
|
1352
1435
|
source = context.get("source")
|
|
1353
1436
|
if item_id == "following" and value_body.get("oneofKind") == "boolValue":
|
|
1437
|
+
if context.get("following") is None:
|
|
1438
|
+
raise ValueError("following unavailable")
|
|
1354
1439
|
enabled = bool(value_body.get("boolValue"))
|
|
1355
|
-
target = (
|
|
1356
|
-
{"userId": context["actor_id"]}
|
|
1357
|
-
if context.get("chat_type") == "dm"
|
|
1358
|
-
else {"chatId": context["chat_id"]}
|
|
1359
|
-
)
|
|
1360
1440
|
await self._sidecar_call("/follow-mode", {
|
|
1361
|
-
"target":
|
|
1441
|
+
"target": {"chatId": context["chat_id"]},
|
|
1362
1442
|
"mode": "following" if enabled else "unfollowed",
|
|
1363
1443
|
})
|
|
1364
1444
|
self._invalidate_chat_info(context["chat_id"])
|
|
@@ -3583,6 +3663,14 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3583
3663
|
if not await self._action_allowed(event):
|
|
3584
3664
|
return True
|
|
3585
3665
|
return await self._handle_model_picker_action(event)
|
|
3666
|
+
if action_id.startswith("cp:"):
|
|
3667
|
+
if not await self._action_allowed(event):
|
|
3668
|
+
return True
|
|
3669
|
+
return await self._handle_choice_picker_action(event)
|
|
3670
|
+
if action_id.startswith("up:"):
|
|
3671
|
+
if not await self._action_allowed(event):
|
|
3672
|
+
return True
|
|
3673
|
+
return await self._handle_update_prompt_action(event)
|
|
3586
3674
|
if action_id.startswith("cl:"):
|
|
3587
3675
|
if not await self._action_allowed(event):
|
|
3588
3676
|
return True
|
|
@@ -3806,6 +3894,121 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3806
3894
|
def _is_model_picker_action(action_id: str) -> bool:
|
|
3807
3895
|
return action_id.startswith(("mp:", "mpg:", "mm:", "mc:", "mg:", "mb:", "mx:"))
|
|
3808
3896
|
|
|
3897
|
+
def _action_matches_state_target(self, event: Dict[str, Any], state: Dict[str, Any]) -> bool:
|
|
3898
|
+
target = state.get("target")
|
|
3899
|
+
if not isinstance(target, dict):
|
|
3900
|
+
return False
|
|
3901
|
+
expected_chat_id = str(target.get("chatId") or "").strip()
|
|
3902
|
+
expected_user_id = str(target.get("userId") or "").strip()
|
|
3903
|
+
target_matches = (
|
|
3904
|
+
bool(expected_chat_id) and expected_chat_id == self._chat_key(event.get("chatId"))
|
|
3905
|
+
) or (
|
|
3906
|
+
bool(expected_user_id) and expected_user_id == str(event.get("actorUserId") or "").strip()
|
|
3907
|
+
)
|
|
3908
|
+
expected_message_id = str(state.get("message_id") or "").strip()
|
|
3909
|
+
actual_message_id = str(event.get("messageId") or "").strip()
|
|
3910
|
+
return bool(
|
|
3911
|
+
target_matches
|
|
3912
|
+
and expected_message_id
|
|
3913
|
+
and expected_message_id == actual_message_id
|
|
3914
|
+
)
|
|
3915
|
+
|
|
3916
|
+
async def _handle_choice_picker_action(self, event: Dict[str, Any]) -> bool:
|
|
3917
|
+
action_id = str(event.get("actionId") or "")
|
|
3918
|
+
parts = action_id.split(":", 2)
|
|
3919
|
+
if len(parts) != 3 or parts[0] != "cp":
|
|
3920
|
+
return False
|
|
3921
|
+
_, picker_id, index_raw = parts
|
|
3922
|
+
interaction_id = str(event.get("interactionId") or "")
|
|
3923
|
+
state = self._choice_picker_sessions.get(picker_id)
|
|
3924
|
+
if not state:
|
|
3925
|
+
await self._answer_action(interaction_id, "Picker expired")
|
|
3926
|
+
return True
|
|
3927
|
+
if not self._action_matches_state_target(event, state):
|
|
3928
|
+
await self._answer_action(interaction_id, "Picker expired")
|
|
3929
|
+
return True
|
|
3930
|
+
if time.monotonic() - float(state.get("created_at") or 0) > _CHOICE_PICKER_TTL_SECONDS:
|
|
3931
|
+
self._choice_picker_sessions.pop(picker_id, None)
|
|
3932
|
+
await self._finish_action(event, "Picker expired", "Selection expired; no change was made.")
|
|
3933
|
+
return True
|
|
3934
|
+
try:
|
|
3935
|
+
index = int(index_raw)
|
|
3936
|
+
state_choices = state["choices"]
|
|
3937
|
+
if index < 0 or index >= len(state_choices):
|
|
3938
|
+
raise IndexError
|
|
3939
|
+
choice = state_choices[index]
|
|
3940
|
+
except (KeyError, TypeError, ValueError, IndexError):
|
|
3941
|
+
await self._answer_action(interaction_id, "Invalid selection")
|
|
3942
|
+
return True
|
|
3943
|
+
|
|
3944
|
+
callback = state.get("on_choice_selected")
|
|
3945
|
+
if not callable(callback):
|
|
3946
|
+
self._choice_picker_sessions.pop(picker_id, None)
|
|
3947
|
+
await self._finish_action(event, "Picker expired", "Selection expired; no change was made.")
|
|
3948
|
+
return True
|
|
3949
|
+
|
|
3950
|
+
# Claim the picker before awaiting the callback so concurrent taps cannot
|
|
3951
|
+
# apply the same setting more than once.
|
|
3952
|
+
self._choice_picker_sessions.pop(picker_id, None)
|
|
3953
|
+
failed = False
|
|
3954
|
+
try:
|
|
3955
|
+
result_text = callback(str(state.get("chat_id") or event.get("chatId") or ""), str(choice.get("value") or ""))
|
|
3956
|
+
if asyncio.iscoroutine(result_text):
|
|
3957
|
+
result_text = await result_text
|
|
3958
|
+
result_text = str(result_text or "Selection applied.")
|
|
3959
|
+
except Exception as exc:
|
|
3960
|
+
logger.error("[inline] choice picker selection failed (%s)", type(exc).__name__)
|
|
3961
|
+
result_text = "Selection failed; try again."
|
|
3962
|
+
failed = True
|
|
3963
|
+
await self._finish_action(
|
|
3964
|
+
event,
|
|
3965
|
+
"Selection failed" if failed else "Selection applied",
|
|
3966
|
+
result_text,
|
|
3967
|
+
)
|
|
3968
|
+
return True
|
|
3969
|
+
|
|
3970
|
+
async def _handle_update_prompt_action(self, event: Dict[str, Any]) -> bool:
|
|
3971
|
+
action_id = str(event.get("actionId") or "")
|
|
3972
|
+
parts = action_id.split(":", 2)
|
|
3973
|
+
if len(parts) != 3 or parts[0] != "up":
|
|
3974
|
+
return False
|
|
3975
|
+
_, prompt_id, answer = parts
|
|
3976
|
+
interaction_id = str(event.get("interactionId") or "")
|
|
3977
|
+
state = self._update_prompt_sessions.get(prompt_id)
|
|
3978
|
+
if not state:
|
|
3979
|
+
await self._answer_action(interaction_id, "Prompt expired")
|
|
3980
|
+
return True
|
|
3981
|
+
if not self._action_matches_state_target(event, state):
|
|
3982
|
+
await self._answer_action(interaction_id, "Prompt expired")
|
|
3983
|
+
return True
|
|
3984
|
+
if time.monotonic() - float(state.get("created_at") or 0) > _UPDATE_PROMPT_TTL_SECONDS:
|
|
3985
|
+
self._update_prompt_sessions.pop(prompt_id, None)
|
|
3986
|
+
await self._finish_action(event, "Prompt expired", "Update prompt expired; no response was sent.")
|
|
3987
|
+
return True
|
|
3988
|
+
if answer not in {"y", "n"}:
|
|
3989
|
+
await self._answer_action(interaction_id, "Invalid response")
|
|
3990
|
+
return True
|
|
3991
|
+
|
|
3992
|
+
try:
|
|
3993
|
+
from hermes_constants import get_hermes_home
|
|
3994
|
+
response_path = Path(get_hermes_home()) / ".update_response"
|
|
3995
|
+
tmp_path = response_path.with_suffix(".tmp")
|
|
3996
|
+
tmp_path.write_text(answer, encoding="utf-8")
|
|
3997
|
+
tmp_path.replace(response_path)
|
|
3998
|
+
except Exception:
|
|
3999
|
+
logger.exception("[inline] failed to write Hermes update response")
|
|
4000
|
+
await self._answer_action(interaction_id, "Response failed; try again")
|
|
4001
|
+
return True
|
|
4002
|
+
|
|
4003
|
+
self._update_prompt_sessions.pop(prompt_id, None)
|
|
4004
|
+
label = "Yes" if answer == "y" else "No"
|
|
4005
|
+
await self._finish_action(
|
|
4006
|
+
event,
|
|
4007
|
+
f"Update response: {label}",
|
|
4008
|
+
f"Hermes update prompt answered: {label}.",
|
|
4009
|
+
)
|
|
4010
|
+
return True
|
|
4011
|
+
|
|
3809
4012
|
@staticmethod
|
|
3810
4013
|
def _split_model_picker_action(action_id: str) -> Optional[tuple[str, str, str]]:
|
|
3811
4014
|
parts = action_id.split(":", 2)
|
|
@@ -3832,6 +4035,9 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3832
4035
|
if not state:
|
|
3833
4036
|
await self._answer_action(interaction_id, "Picker expired")
|
|
3834
4037
|
return True
|
|
4038
|
+
if not self._action_matches_state_target(event, state):
|
|
4039
|
+
await self._answer_action(interaction_id, "Picker expired")
|
|
4040
|
+
return True
|
|
3835
4041
|
|
|
3836
4042
|
if kind == "mp":
|
|
3837
4043
|
return await self._select_model_provider(event, picker_id, state, value)
|
|
@@ -3954,6 +4160,10 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3954
4160
|
self._model_picker_sessions.pop(picker_id, None)
|
|
3955
4161
|
await self._answer_action(interaction_id, "Picker expired")
|
|
3956
4162
|
return True
|
|
4163
|
+
# Claim the picker before awaiting provider work. The sidecar currently
|
|
4164
|
+
# dispatches serially, but this keeps the callback single-use if that
|
|
4165
|
+
# delivery contract changes.
|
|
4166
|
+
self._model_picker_sessions.pop(picker_id, None)
|
|
3957
4167
|
failed = False
|
|
3958
4168
|
try:
|
|
3959
4169
|
result_text = callback(str(state.get("chat_id") or event.get("chatId") or ""), model_id, provider_slug)
|
|
@@ -3961,10 +4171,9 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3961
4171
|
result_text = await result_text
|
|
3962
4172
|
result_text = str(result_text or "Model switched.")
|
|
3963
4173
|
except Exception as exc:
|
|
3964
|
-
logger.
|
|
3965
|
-
result_text =
|
|
4174
|
+
logger.error("[inline] model picker switch failed (%s)", type(exc).__name__)
|
|
4175
|
+
result_text = "Model switch failed; try again."
|
|
3966
4176
|
failed = True
|
|
3967
|
-
self._model_picker_sessions.pop(picker_id, None)
|
|
3968
4177
|
await self._edit_action_message(event, result_text, {"rows": []})
|
|
3969
4178
|
await self._answer_action(interaction_id, "Switch failed" if failed else "Model switched")
|
|
3970
4179
|
return True
|
|
@@ -4095,6 +4304,35 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
4095
4304
|
) -> bool:
|
|
4096
4305
|
return False
|
|
4097
4306
|
|
|
4307
|
+
async def send_or_update_status(
|
|
4308
|
+
self,
|
|
4309
|
+
chat_id: str,
|
|
4310
|
+
status_key: str,
|
|
4311
|
+
content: str,
|
|
4312
|
+
*,
|
|
4313
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
4314
|
+
) -> SendResult:
|
|
4315
|
+
target = self._target_for(chat_id, metadata)
|
|
4316
|
+
target_kind = "userId" if "userId" in target else "chatId"
|
|
4317
|
+
key = (target_kind, str(target.get(target_kind) or ""), str(status_key))
|
|
4318
|
+
cached_id = self._status_message_ids.get(key)
|
|
4319
|
+
if cached_id:
|
|
4320
|
+
result = await self.edit_message(
|
|
4321
|
+
chat_id,
|
|
4322
|
+
cached_id,
|
|
4323
|
+
content,
|
|
4324
|
+
metadata=metadata,
|
|
4325
|
+
)
|
|
4326
|
+
if result.success:
|
|
4327
|
+
self._remember(self._status_message_ids, key, str(result.message_id or cached_id))
|
|
4328
|
+
return result
|
|
4329
|
+
self._status_message_ids.pop(key, None)
|
|
4330
|
+
|
|
4331
|
+
result = await self.send(chat_id, content, metadata=metadata)
|
|
4332
|
+
if result.success and result.message_id:
|
|
4333
|
+
self._remember(self._status_message_ids, key, str(result.message_id))
|
|
4334
|
+
return result
|
|
4335
|
+
|
|
4098
4336
|
async def edit_message(
|
|
4099
4337
|
self,
|
|
4100
4338
|
chat_id: str,
|
|
@@ -4210,6 +4448,88 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
4210
4448
|
except Exception:
|
|
4211
4449
|
pass
|
|
4212
4450
|
|
|
4451
|
+
def _processing_reaction_target(
|
|
4452
|
+
self,
|
|
4453
|
+
event: MessageEvent,
|
|
4454
|
+
) -> Optional[tuple[tuple[str, str, str], Dict[str, str], str]]:
|
|
4455
|
+
if not self._processing_reactions:
|
|
4456
|
+
return None
|
|
4457
|
+
raw_message = getattr(event, "raw_message", None)
|
|
4458
|
+
if isinstance(raw_message, dict):
|
|
4459
|
+
# Lifecycle and reaction events can themselves invoke Hermes when
|
|
4460
|
+
# system events are enabled. Reacting to those would add noise and
|
|
4461
|
+
# could create a reaction-event loop. Agent-action turns resolve
|
|
4462
|
+
# their own source card in place and must not react to that card.
|
|
4463
|
+
kind = str(raw_message.get("kind") or "")
|
|
4464
|
+
if (kind and kind != "message.new") or raw_message.get("_inlineAgentAction"):
|
|
4465
|
+
return None
|
|
4466
|
+
source = getattr(event, "source", None)
|
|
4467
|
+
chat_id = str(getattr(source, "chat_id", None) or "").strip()
|
|
4468
|
+
message_id = str(getattr(event, "message_id", None) or "").strip()
|
|
4469
|
+
if not chat_id or not message_id:
|
|
4470
|
+
return None
|
|
4471
|
+
target = _target_from_chat_id(chat_id)
|
|
4472
|
+
target_kind = "userId" if "userId" in target else "chatId"
|
|
4473
|
+
target_id = str(target.get(target_kind) or "").strip()
|
|
4474
|
+
if not target_id:
|
|
4475
|
+
return None
|
|
4476
|
+
return (target_kind, target_id, message_id), target, message_id
|
|
4477
|
+
|
|
4478
|
+
async def _set_processing_reaction(
|
|
4479
|
+
self,
|
|
4480
|
+
target: Dict[str, str],
|
|
4481
|
+
message_id: str,
|
|
4482
|
+
emoji: str,
|
|
4483
|
+
*,
|
|
4484
|
+
remove: bool = False,
|
|
4485
|
+
) -> bool:
|
|
4486
|
+
body: Dict[str, Any] = {
|
|
4487
|
+
"target": target,
|
|
4488
|
+
"messageId": message_id,
|
|
4489
|
+
"emoji": emoji,
|
|
4490
|
+
}
|
|
4491
|
+
if remove:
|
|
4492
|
+
body["remove"] = True
|
|
4493
|
+
try:
|
|
4494
|
+
await self._sidecar_call("/reaction", body)
|
|
4495
|
+
return True
|
|
4496
|
+
except Exception as exc:
|
|
4497
|
+
logger.debug("[inline] processing reaction failed: %s", exc)
|
|
4498
|
+
return False
|
|
4499
|
+
|
|
4500
|
+
async def on_processing_start(self, event: MessageEvent) -> None:
|
|
4501
|
+
"""Mark an inbound message while Hermes is actively processing it."""
|
|
4502
|
+
reaction_target = self._processing_reaction_target(event)
|
|
4503
|
+
if not reaction_target:
|
|
4504
|
+
return
|
|
4505
|
+
key, target, message_id = reaction_target
|
|
4506
|
+
if key not in self._processing_reaction_messages and len(self._processing_reaction_messages) >= 512:
|
|
4507
|
+
evicted_key = next(iter(self._processing_reaction_messages))
|
|
4508
|
+
evicted_target = self._processing_reaction_messages[evicted_key]
|
|
4509
|
+
await self._set_processing_reaction(evicted_target, evicted_key[2], "👀", remove=True)
|
|
4510
|
+
self._processing_reaction_messages.pop(evicted_key, None)
|
|
4511
|
+
# Record cleanup ownership before awaiting the add. Cancellation or a
|
|
4512
|
+
# lost acknowledgement can otherwise strand a bot-owned marker.
|
|
4513
|
+
self._remember(self._processing_reaction_messages, key, target)
|
|
4514
|
+
if not await self._set_processing_reaction(target, message_id, "👀"):
|
|
4515
|
+
self._processing_reaction_messages.pop(key, None)
|
|
4516
|
+
|
|
4517
|
+
async def on_processing_complete(self, event: MessageEvent, outcome: Any) -> None:
|
|
4518
|
+
"""Replace the processing marker without affecting response delivery."""
|
|
4519
|
+
reaction_target = self._processing_reaction_target(event)
|
|
4520
|
+
if not reaction_target:
|
|
4521
|
+
return
|
|
4522
|
+
key, _, message_id = reaction_target
|
|
4523
|
+
target = self._processing_reaction_messages.pop(key, None)
|
|
4524
|
+
if not target:
|
|
4525
|
+
return
|
|
4526
|
+
await self._set_processing_reaction(target, message_id, "👀", remove=True)
|
|
4527
|
+
outcome_name = str(getattr(outcome, "value", outcome) or "").strip().lower()
|
|
4528
|
+
if outcome_name == "success":
|
|
4529
|
+
await self._set_processing_reaction(target, message_id, "✅")
|
|
4530
|
+
elif outcome_name == "failure":
|
|
4531
|
+
await self._set_processing_reaction(target, message_id, "❌")
|
|
4532
|
+
|
|
4213
4533
|
async def send_image_file(self, chat_id: str, image_path: str, caption: Optional[str] = None, reply_to: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> SendResult:
|
|
4214
4534
|
return await self._send_attachment(chat_id, image_path, "photo", caption, reply_to, metadata=metadata)
|
|
4215
4535
|
|
|
@@ -4362,8 +4682,9 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
4362
4682
|
if not clean_providers:
|
|
4363
4683
|
return await self.send(chat_id, "No authenticated models are available for this session.", metadata=metadata)
|
|
4364
4684
|
picker_id = secrets.token_hex(6)
|
|
4685
|
+
target = self._target_for(chat_id, metadata)
|
|
4365
4686
|
result = await self._send_sidecar("/send", {
|
|
4366
|
-
"target":
|
|
4687
|
+
"target": target,
|
|
4367
4688
|
"text": self._model_picker_text(current_model, current_provider),
|
|
4368
4689
|
"parseMarkdown": self._parse_markdown,
|
|
4369
4690
|
"actions": self._build_provider_actions(picker_id, clean_providers),
|
|
@@ -4378,6 +4699,95 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
4378
4699
|
"current_model": str(current_model or ""),
|
|
4379
4700
|
"current_provider": str(current_provider or ""),
|
|
4380
4701
|
"message_id": result.message_id,
|
|
4702
|
+
"target": target,
|
|
4703
|
+
})
|
|
4704
|
+
return result
|
|
4705
|
+
|
|
4706
|
+
async def send_choice_picker(
|
|
4707
|
+
self,
|
|
4708
|
+
chat_id: str,
|
|
4709
|
+
title: str,
|
|
4710
|
+
choices: list,
|
|
4711
|
+
session_key: str,
|
|
4712
|
+
on_choice_selected,
|
|
4713
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
4714
|
+
) -> SendResult:
|
|
4715
|
+
clean_choices: list[Dict[str, Any]] = []
|
|
4716
|
+
for choice in choices or []:
|
|
4717
|
+
if not isinstance(choice, dict):
|
|
4718
|
+
continue
|
|
4719
|
+
value = str(choice.get("value") or "").strip()
|
|
4720
|
+
label = str(choice.get("label") or value).strip()
|
|
4721
|
+
if not value or not label:
|
|
4722
|
+
continue
|
|
4723
|
+
clean_choices.append({
|
|
4724
|
+
"value": value,
|
|
4725
|
+
"label": label,
|
|
4726
|
+
"is_current": bool(choice.get("is_current")),
|
|
4727
|
+
})
|
|
4728
|
+
# Inline accepts at most eight action rows. Picker actions use two
|
|
4729
|
+
# buttons per row, so keep the native card within that hard limit.
|
|
4730
|
+
if len(clean_choices) >= _CHOICE_PICKER_MAX_CHOICES:
|
|
4731
|
+
break
|
|
4732
|
+
if not clean_choices:
|
|
4733
|
+
return _send_result(success=False, error="No choices")
|
|
4734
|
+
|
|
4735
|
+
picker_id = secrets.token_hex(6)
|
|
4736
|
+
actions = []
|
|
4737
|
+
for index, choice in enumerate(clean_choices):
|
|
4738
|
+
label = str(choice["label"])
|
|
4739
|
+
if choice["is_current"]:
|
|
4740
|
+
label = f"✓ {label}"
|
|
4741
|
+
actions.append(self._action(f"cp:{picker_id}:{index}", self._short_label(label)))
|
|
4742
|
+
target = self._target_for(chat_id, metadata)
|
|
4743
|
+
result = await self._send_sidecar("/send", {
|
|
4744
|
+
"target": target,
|
|
4745
|
+
"text": str(title or "Choose an option"),
|
|
4746
|
+
"parseMarkdown": self._parse_markdown,
|
|
4747
|
+
"actions": {"rows": self._action_rows(actions)},
|
|
4748
|
+
})
|
|
4749
|
+
if result.success:
|
|
4750
|
+
self._remember(self._choice_picker_sessions, picker_id, {
|
|
4751
|
+
"chat_id": str(chat_id),
|
|
4752
|
+
"choices": clean_choices,
|
|
4753
|
+
"session_key": str(session_key),
|
|
4754
|
+
"on_choice_selected": on_choice_selected,
|
|
4755
|
+
"target": target,
|
|
4756
|
+
"message_id": result.message_id,
|
|
4757
|
+
"created_at": time.monotonic(),
|
|
4758
|
+
})
|
|
4759
|
+
return result
|
|
4760
|
+
|
|
4761
|
+
async def send_update_prompt(
|
|
4762
|
+
self,
|
|
4763
|
+
chat_id: str,
|
|
4764
|
+
prompt: str,
|
|
4765
|
+
default: str = "",
|
|
4766
|
+
session_key: str = "",
|
|
4767
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
4768
|
+
) -> SendResult:
|
|
4769
|
+
prompt_id = secrets.token_hex(6)
|
|
4770
|
+
default_hint = f" (default: {default})" if default else ""
|
|
4771
|
+
target = self._target_for(chat_id, metadata)
|
|
4772
|
+
result = await self._send_sidecar("/send", {
|
|
4773
|
+
"target": target,
|
|
4774
|
+
"text": f"Hermes update needs your input\n\n{prompt}{default_hint}",
|
|
4775
|
+
"parseMarkdown": self._parse_markdown,
|
|
4776
|
+
"actions": {"rows": [{"actions": [
|
|
4777
|
+
self._action(f"up:{prompt_id}:y", "✓ Yes"),
|
|
4778
|
+
self._action(f"up:{prompt_id}:n", "✗ No"),
|
|
4779
|
+
]}]},
|
|
4780
|
+
})
|
|
4781
|
+
if not result.success:
|
|
4782
|
+
# Hermes 0.20.6 treats any non-raising hook call as delivered and
|
|
4783
|
+
# otherwise suppresses its plaintext /approve and /deny fallback.
|
|
4784
|
+
raise RuntimeError(result.error or "failed to send Inline update prompt")
|
|
4785
|
+
self._remember(self._update_prompt_sessions, prompt_id, {
|
|
4786
|
+
"chat_id": str(chat_id),
|
|
4787
|
+
"session_key": str(session_key),
|
|
4788
|
+
"target": target,
|
|
4789
|
+
"message_id": result.message_id,
|
|
4790
|
+
"created_at": time.monotonic(),
|
|
4381
4791
|
})
|
|
4382
4792
|
return result
|
|
4383
4793
|
|
|
@@ -4581,7 +4991,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
4581
4991
|
return self._target_for(chat_id, metadata)
|
|
4582
4992
|
|
|
4583
4993
|
@staticmethod
|
|
4584
|
-
def _remember(mapping: OrderedDict, key:
|
|
4994
|
+
def _remember(mapping: OrderedDict, key: Any, value: Any, limit: int = 512) -> None:
|
|
4585
4995
|
if key in mapping:
|
|
4586
4996
|
del mapping[key]
|
|
4587
4997
|
mapping[key] = value
|
|
@@ -5047,6 +5457,7 @@ def register(ctx) -> None:
|
|
|
5047
5457
|
pii_safe=False,
|
|
5048
5458
|
allow_update_command=True,
|
|
5049
5459
|
platform_hint=_tools.INLINE_PLATFORM_GUIDANCE,
|
|
5460
|
+
**_target_registration_hooks(),
|
|
5050
5461
|
)
|
|
5051
5462
|
register_command = getattr(ctx, "register_command", None)
|
|
5052
5463
|
if callable(register_command):
|
package/plugin/inline/cli.py
CHANGED
|
@@ -26,7 +26,7 @@ _CLI_INSTALL_URL = "https://inline.chat/cli/install.sh"
|
|
|
26
26
|
_MAX_TOKEN_BYTES = 16 * 1024
|
|
27
27
|
_MAX_PROBE_RESPONSE_BYTES = 64 * 1024
|
|
28
28
|
_MACHINE_SETUP_PROTOCOL_VERSION = 1
|
|
29
|
-
_PROBE_USER_AGENT = "inline-hermes-agent-adapter/0.0.
|
|
29
|
+
_PROBE_USER_AGENT = "inline-hermes-agent-adapter/0.0.12"
|
|
30
30
|
_ENV_REFERENCE_RE = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$")
|
|
31
31
|
|
|
32
32
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
name: inline-platform
|
|
2
2
|
label: Inline
|
|
3
3
|
kind: platform
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.12
|
|
5
5
|
description: >
|
|
6
6
|
Inline platform adapter for Hermes Agent. The adapter runs as a native
|
|
7
7
|
Hermes Python platform plugin and supervises a local Node sidecar that uses
|
|
@@ -110,6 +110,10 @@ optional_env:
|
|
|
110
110
|
description: "Deliver Inline lifecycle events such as edits, deletes, and participant changes as synthetic messages (true/false, default false)"
|
|
111
111
|
prompt: "Deliver lifecycle events? (true/false)"
|
|
112
112
|
password: false
|
|
113
|
+
- name: INLINE_REACTIONS
|
|
114
|
+
description: "Show processing reactions on handled inbound messages: eyes while working, then success or failure (true/false, default false)"
|
|
115
|
+
prompt: "Show processing reactions? (true/false)"
|
|
116
|
+
password: false
|
|
113
117
|
- name: INLINE_MENTION_PATTERNS
|
|
114
118
|
description: "Mention wake-word regexes, JSON list or comma/newline-separated"
|
|
115
119
|
prompt: "Mention patterns"
|
|
@@ -45251,6 +45251,7 @@ var portableCoreV1Vector = {
|
|
|
45251
45251
|
};
|
|
45252
45252
|
// src/sidecar/contract.ts
|
|
45253
45253
|
var MAX_INLINE_ID = 9223372036854775807n;
|
|
45254
|
+
var MAX_UINT64 = 18446744073709551615n;
|
|
45254
45255
|
var sensitiveUrlParams = new Set([
|
|
45255
45256
|
"access_token",
|
|
45256
45257
|
"auth",
|
|
@@ -45553,6 +45554,25 @@ function parseInlineId(value, field) {
|
|
|
45553
45554
|
throw new SidecarError(`${field} must be a positive signed 64-bit integer`, "bad_format");
|
|
45554
45555
|
}
|
|
45555
45556
|
}
|
|
45557
|
+
function parseUnsigned64Id(value, field) {
|
|
45558
|
+
try {
|
|
45559
|
+
if (typeof value === "number" && (!Number.isSafeInteger(value) || value <= 0)) {
|
|
45560
|
+
throw new Error("unsafe number");
|
|
45561
|
+
}
|
|
45562
|
+
const raw = typeof value === "string" ? value.trim() : value;
|
|
45563
|
+
if (typeof raw === "string" && !/^[1-9][0-9]*$/.test(raw))
|
|
45564
|
+
throw new Error("invalid digits");
|
|
45565
|
+
if (typeof raw !== "string" && typeof raw !== "bigint" && typeof raw !== "number") {
|
|
45566
|
+
throw new Error("invalid type");
|
|
45567
|
+
}
|
|
45568
|
+
const parsed = BigInt(raw);
|
|
45569
|
+
if (parsed <= 0n || parsed > MAX_UINT64)
|
|
45570
|
+
throw new Error("out of range");
|
|
45571
|
+
return parsed;
|
|
45572
|
+
} catch {
|
|
45573
|
+
throw new SidecarError(`${field} must be a positive unsigned 64-bit integer`, "bad_format");
|
|
45574
|
+
}
|
|
45575
|
+
}
|
|
45556
45576
|
function defaultErrorText(error) {
|
|
45557
45577
|
return error instanceof Error ? error.message : String(error);
|
|
45558
45578
|
}
|
|
@@ -46579,7 +46599,10 @@ async function endpointAnswerAction(res, body) {
|
|
|
46579
46599
|
}
|
|
46580
46600
|
async function endpointAnswerBotSettings(res, body) {
|
|
46581
46601
|
const record2 = asRecord(body);
|
|
46582
|
-
|
|
46602
|
+
if (record2.requestId == null || record2.requestId === "") {
|
|
46603
|
+
throw new SidecarError("missing requestId", "bad_format");
|
|
46604
|
+
}
|
|
46605
|
+
const requestId = parseUnsigned64Id(record2.requestId, "requestId");
|
|
46583
46606
|
const response = asRecord(record2.response);
|
|
46584
46607
|
await client.answerBotChatSettings({ requestId, response });
|
|
46585
46608
|
writeJson(res, 200, { ok: true, result: {} });
|