@senpi/runtime 3.0.102 → 3.0.103
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.
|
@@ -47,6 +47,16 @@ write to that stream — and the next call opens a fresh one. It is logged as
|
|
|
47
47
|
`senpi_mcp_error` with `error_type: TimeoutError`, `timeout_seconds` and
|
|
48
48
|
`elapsed_ms` so a stall is distinguishable from a severed stream in ClickStack.
|
|
49
49
|
|
|
50
|
+
AMENDMENT, third part (dev 2026-09-07): the SDK's transport is an anyio task
|
|
51
|
+
group, so a failure raised through it arrives as an `ExceptionGroup` around the
|
|
52
|
+
real cause. The client classifies and logs the LEAF, never the group: a wrapped
|
|
53
|
+
closed stream still discards the session (the group's own type names no
|
|
54
|
+
transport class, so without unwrapping the dead session would have been kept —
|
|
55
|
+
the original wedge in a new coat), `error_type` is the leaf's class (never
|
|
56
|
+
`ExceptionGroup`), and a sole-leaf group is re-raised to `scan()` as that leaf.
|
|
57
|
+
Several leaves stay a group, logged as the first leaf plus `leaf_count`, and
|
|
58
|
+
rebuild when ANY leaf is a transport failure.
|
|
59
|
+
|
|
50
60
|
Response normalization MATCHES producer.mjs `createSenpiToolClient`:
|
|
51
61
|
1. isError truthy -> RAISE with the error text (never a silent None).
|
|
52
62
|
2. structuredContent present -> return it.
|
|
@@ -458,8 +468,54 @@ _TRANSPORT_ERROR_NAMES: frozenset[str] = frozenset(
|
|
|
458
468
|
)
|
|
459
469
|
|
|
460
470
|
|
|
471
|
+
# The SDK's `streamablehttp_client` runs an anyio task group, so a failure
|
|
472
|
+
# raised through it (a closed stream, an httpx error on the POST) surfaces as an
|
|
473
|
+
# `ExceptionGroup` wrapping the real cause — observed on dev 2026-09-07 as
|
|
474
|
+
# `error_type: ExceptionGroup, error: "unhandled errors in a TaskGroup (1
|
|
475
|
+
# sub-exception)"` against a 503. Classification and the fleet fingerprint must
|
|
476
|
+
# look at the LEAVES: the group's own MRO names no transport class, so a wrapped
|
|
477
|
+
# `ClosedResourceError` on a live session would otherwise keep the dead session
|
|
478
|
+
# (the pre-rebuild wedge). Matched by name so the `exceptiongroup` backport on
|
|
479
|
+
# Python < 3.11 unwraps like the builtin.
|
|
480
|
+
_EXCEPTION_GROUP_NAME = "BaseExceptionGroup"
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _is_exception_group(exc: BaseException) -> bool:
|
|
484
|
+
return any(klass.__name__ == _EXCEPTION_GROUP_NAME for klass in type(exc).__mro__)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _leaf_exceptions(exc: BaseException) -> list[BaseException]:
|
|
488
|
+
"""Flatten an exception-group tree to its leaves; a plain exception is its own leaf."""
|
|
489
|
+
if not _is_exception_group(exc):
|
|
490
|
+
return [exc]
|
|
491
|
+
leaves: list[BaseException] = []
|
|
492
|
+
for sub in exc.exceptions: # type: ignore[attr-defined]
|
|
493
|
+
leaves.extend(_leaf_exceptions(sub))
|
|
494
|
+
return leaves or [exc]
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _unwrap_sole_leaf(exc: BaseException) -> BaseException:
|
|
498
|
+
"""The one real cause behind a group, else `exc` itself.
|
|
499
|
+
|
|
500
|
+
A group with exactly one leaf is the anyio wrapping and nothing more, so
|
|
501
|
+
the caller gets the leaf. Several leaves are genuinely several failures
|
|
502
|
+
and stay a group — the log line then names the first leaf and carries the
|
|
503
|
+
count.
|
|
504
|
+
"""
|
|
505
|
+
leaves = _leaf_exceptions(exc)
|
|
506
|
+
return leaves[0] if len(leaves) == 1 else exc
|
|
507
|
+
|
|
508
|
+
|
|
461
509
|
def _is_transport_error(exc: BaseException) -> bool:
|
|
462
|
-
"""True when `exc` means the MCP session's transport is unusable.
|
|
510
|
+
"""True when `exc` means the MCP session's transport is unusable.
|
|
511
|
+
|
|
512
|
+
Looks through exception groups: a group is a transport failure when ANY of
|
|
513
|
+
its leaves is.
|
|
514
|
+
"""
|
|
515
|
+
return any(_leaf_is_transport_error(leaf) for leaf in _leaf_exceptions(exc))
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def _leaf_is_transport_error(exc: BaseException) -> bool:
|
|
463
519
|
if isinstance(exc, (OSError, EOFError)):
|
|
464
520
|
return True
|
|
465
521
|
return any(klass.__name__ in _TRANSPORT_ERROR_NAMES for klass in type(exc).__mro__)
|
|
@@ -623,22 +679,33 @@ class SenpiMcpClient:
|
|
|
623
679
|
# unwinds the tick untouched. This is the ONE exit for every failed
|
|
624
680
|
# call, so the tally sees each failure exactly once.
|
|
625
681
|
rebuilt = used is not None and _is_transport_error(exc) and self._discard(used)
|
|
626
|
-
|
|
682
|
+
# Classify and log the LEAF, never the anyio ExceptionGroup around
|
|
683
|
+
# it (see _leaf_exceptions): the fingerprint has to tell a 503 from
|
|
684
|
+
# a closed stream. One leaf is the failure; several are logged as
|
|
685
|
+
# the first leaf plus `leaf_count`.
|
|
686
|
+
leaves = _leaf_exceptions(exc)
|
|
687
|
+
primary = leaves[0]
|
|
688
|
+
timed_out = isinstance(primary, SenpiMcpTimeoutError)
|
|
627
689
|
# The fleet fingerprint for the bound is the builtin category, not
|
|
628
690
|
# the scaffold subclass: ClickStack queries `TimeoutError`. The tally
|
|
629
691
|
# counts under the SAME name, so the dominant type a blind tick
|
|
630
692
|
# reports to /errors matches the senpi_mcp_error lines beside it.
|
|
631
|
-
error_type = "TimeoutError" if timed_out else type(
|
|
693
|
+
error_type = "TimeoutError" if timed_out else type(primary).__name__
|
|
632
694
|
self.tick_errors.record(error_type)
|
|
633
695
|
log_event(
|
|
634
696
|
"senpi_mcp_error",
|
|
635
697
|
tool=name,
|
|
636
698
|
error_type=error_type,
|
|
637
|
-
error=str(
|
|
699
|
+
error=str(primary)[:500],
|
|
638
700
|
elapsed_ms=round((time.monotonic() - started) * 1000, 1),
|
|
701
|
+
**({"leaf_count": len(leaves)} if len(leaves) > 1 else {}),
|
|
639
702
|
**({"timeout_seconds": self._call_timeout} if timed_out else {}),
|
|
640
703
|
**({"session_rebuilt": True} if rebuilt else {}),
|
|
641
704
|
)
|
|
705
|
+
if len(leaves) == 1 and primary is not exc:
|
|
706
|
+
# The author's scan() sees the real cause (so `except OSError`
|
|
707
|
+
# or `except TimeoutError` keeps working), not the wrapper.
|
|
708
|
+
raise primary
|
|
642
709
|
raise
|
|
643
710
|
|
|
644
711
|
# -- persistent session lifecycle -------------------------------------
|
|
@@ -742,13 +809,17 @@ class SenpiMcpClient:
|
|
|
742
809
|
await close_requested.wait()
|
|
743
810
|
except BaseException as exc:
|
|
744
811
|
if not ready.done():
|
|
745
|
-
# Opening failed: surface it to the waiting call_tool.
|
|
746
|
-
#
|
|
747
|
-
# gets
|
|
812
|
+
# Opening failed: surface it to the waiting call_tool. The
|
|
813
|
+
# transport's anyio task group wraps the cause in an
|
|
814
|
+
# ExceptionGroup; the waiter gets the sole leaf (an httpx 503,
|
|
815
|
+
# a refused connection) so it can classify and re-raise the
|
|
816
|
+
# real error. A non-Exception (task cancellation) is wrapped
|
|
817
|
+
# so the waiter gets a plain error instead of blocking forever.
|
|
818
|
+
cause = _unwrap_sole_leaf(exc)
|
|
748
819
|
ready.set_exception(
|
|
749
|
-
|
|
750
|
-
if isinstance(
|
|
751
|
-
else RuntimeError(f"senpi_mcp: session open aborted by {type(
|
|
820
|
+
cause
|
|
821
|
+
if isinstance(cause, Exception)
|
|
822
|
+
else RuntimeError(f"senpi_mcp: session open aborted by {type(cause).__name__}")
|
|
752
823
|
)
|
|
753
824
|
# Otherwise the transport died under a published session (the
|
|
754
825
|
# next call_tool sees `alive` False and rebuilds) or its teardown
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "runtime",
|
|
3
3
|
"name": "Senpi Runtime",
|
|
4
4
|
"description": "Senpi runtime plugin for OpenClaw. Orchestrates scanners, LLM-based trade decisions, and a DSL trailing stop-loss exit engine.",
|
|
5
|
-
"version": "3.0.
|
|
5
|
+
"version": "3.0.103",
|
|
6
6
|
"commandAliases": [
|
|
7
7
|
{
|
|
8
8
|
"name": "senpi"
|