"""ServiceBridge Python SDK main module.

Quick start::

    import asyncio
    from service_bridge import ServiceBridge

    svc = ServiceBridge("localhost:14445", "my-key")

    @svc.handle_rpc("my.fn")
    async def my_handler(payload: dict) -> dict:
        return {"ok": True}

    @svc.handle_event("my.topic", group_name="my.group")
    async def my_event(payload: dict, ec) -> None:
        pass

    asyncio.run(svc.serve())
"""

from __future__ import annotations

import asyncio
import base64
import binascii
import hashlib
import json
import logging
import random
import re
import threading
import time
from collections import deque
from concurrent import futures
from dataclasses import dataclass, field
from typing import Any, AsyncIterator, Callable, Optional

import grpc

from service_bridge.pb import servicebridge_pb2 as pb
from service_bridge.pb import servicebridge_pb2_grpc as pb_grpc
from service_bridge.pb.session.v1 import session_pb2 as spb
from service_bridge.pb.session.v1 import session_pb2_grpc as spb_grpc
from service_bridge.pb.controlplane.v1 import control_plane_pb2 as cpb
from service_bridge.pb.controlplane.v1 import control_plane_pb2_grpc as cpb_grpc
from service_bridge.pb.common.v1 import types_pb2 as tpb
from service_bridge.session_v2 import (
    V2SessionClient,
    V2Config,
    HelloAckV2,
    TransportMode as V2TransportMode,
    TransportConfigV2,
)
from service_bridge.http_catalog import (
    HttpSpan,
    extract_trace_from_headers,
    _new_hex_id,
)

logger = logging.getLogger(__name__)
_sdk_logger = logging.getLogger(__name__ + ".internal")

_DEFAULT_RPC_TIMEOUT_MS = 30_000
_DEFAULT_RPC_RETRIES = 3
_DEFAULT_RPC_RETRY_DELAY_MS = 300


def _outbound_ip() -> str:
    """Detect the IP the OS would use for outbound traffic (UDP dial trick).

    Works in Docker containers (returns the container's network IP) and on the
    host (returns the LAN/Wi-Fi interface IP).  No traffic is actually sent.
    """
    import socket
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        try:
            s.connect(("8.8.8.8", 53))
            return s.getsockname()[0]
        finally:
            s.close()
    except OSError:
        return ""


@dataclass
class _TLSMaterials:
    ca_cert: bytes
    client_cert: bytes
    client_key: bytes


_PEM_CERT_RE = re.compile(
    rb"-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----"
)


def _decode_b64url(segment: str, *, field_name: str) -> bytes:
    if not segment:
        raise RuntimeError(f"invalid service key: missing {field_name}")
    padded = segment + "=" * (-len(segment) % 4)
    try:
        return base64.urlsafe_b64decode(padded.encode())
    except (binascii.Error, ValueError) as exc:
        raise RuntimeError(f"invalid service key: malformed {field_name}") from exc


def _parse_service_key_v2(service_key: str) -> tuple[str, str, bytes]:
    parts = service_key.strip().split(".")
    if len(parts) != 4 or parts[0] != "sbv2":
        raise RuntimeError("legacy service keys are disabled: expected sbv2 key")
    key_id = parts[1].strip()
    if not key_id:
        raise RuntimeError("invalid service key: missing key id")
    secret_bytes = _decode_b64url(parts[2], field_name="secret segment")
    if len(secret_bytes) < 16:
        raise RuntimeError("invalid service key: malformed secret segment")
    ca_pem = _decode_b64url(parts[3], field_name="ca segment")
    _validate_ca_pem(ca_pem, error_message="invalid service key: malformed embedded ca pem")
    secret_hash = hashlib.sha256(secret_bytes).hexdigest()
    return key_id, secret_hash, ca_pem


def _resolve_ca_pem(service_key: str, explicit_ca_pem: str) -> bytes:
    _, _, key_ca_pem = _parse_service_key_v2(service_key)
    if explicit_ca_pem.strip():
        return _validate_ca_pem(
            explicit_ca_pem.encode(),
            error_message="invalid control-plane ca cert: malformed pem",
        )
    return key_ca_pem


def _validate_ca_pem(ca_pem: bytes, *, error_message: str) -> bytes:
    cert_blocks = _PEM_CERT_RE.findall(ca_pem)
    if not cert_blocks:
        raise RuntimeError(error_message)
    try:
        from cryptography import x509
    except ImportError as exc:
        raise RuntimeError("install 'cryptography' package for x509 validation") from exc
    try:
        for block in cert_blocks:
            x509.load_pem_x509_certificate(block)
    except Exception as exc:
        raise RuntimeError(error_message) from exc
    return ca_pem


def _validate_worker_cert_and_key(cert_pem: bytes, key_pem: bytes) -> None:
    try:
        from cryptography import x509
        from cryptography.hazmat.primitives import serialization
    except ImportError as exc:
        raise RuntimeError("install 'cryptography' package for x509 validation") from exc
    try:
        cert = x509.load_pem_x509_certificate(cert_pem)
    except Exception as exc:
        raise RuntimeError("invalid worker tls cert: malformed pem") from exc
    try:
        key = serialization.load_pem_private_key(key_pem, password=None)
    except Exception as exc:
        raise RuntimeError("invalid worker tls key: malformed pem") from exc
    cert_pub = cert.public_key().public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )
    key_pub = key.public_key().public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )
    if cert_pub != key_pub:
        raise RuntimeError("worker tls cert/key pair mismatch")


# ─── Error helpers ────────────────────────────────────────────────────────────

def _friendly_grpc_message(exc: Exception) -> str:
    """Return a concise human-readable description of a gRPC (or any) exception."""
    if isinstance(exc, grpc.RpcError):
        code = exc.code()  # type: ignore[attr-defined]
        if code == grpc.StatusCode.UNAVAILABLE:
            return "control plane unavailable"
        if code == grpc.StatusCode.DEADLINE_EXCEEDED:
            return "request timed out"
        if code == grpc.StatusCode.UNAUTHENTICATED:
            return "authentication failed — check service key"
        if code == grpc.StatusCode.PERMISSION_DENIED:
            return "permission denied — check service key"
        if code == grpc.StatusCode.RESOURCE_EXHAUSTED:
            return "rate limit exceeded"
        if code == grpc.StatusCode.NOT_FOUND:
            return "resource not found"
        if code == grpc.StatusCode.INTERNAL:
            details = exc.details()  # type: ignore[attr-defined]
            if details:
                return f"internal server error: {details}"
            return "internal server error"
        if code == grpc.StatusCode.FAILED_PRECONDITION:
            return "precondition failed"
        if code == grpc.StatusCode.CANCELLED:
            return "request cancelled"
        if code == grpc.StatusCode.UNIMPLEMENTED:
            return "operation not supported by control plane"
        if code == grpc.StatusCode.UNKNOWN:
            details = exc.details()  # type: ignore[attr-defined]
            if details:
                return f"unknown error from control plane: {details}"
            return "unknown error from control plane"
        msg = exc.details()  # type: ignore[attr-defined]
        return msg or code.name
    return str(exc)


# ─── Error types ──────────────────────────────────────────────────────────────

ServiceBridgeErrorSeverity = str  # "fatal" | "retriable" | "ignorable"


class ServiceBridgeError(Exception):
    """Typed error raised by all ServiceBridge SDK operations.

    Symmetric with Node ServiceBridgeError.

    Attributes:
        code: gRPC status code (int) if applicable.
        component: SDK subsystem (e.g. "event", "job").
        operation: Specific operation that failed.
        severity: "fatal", "retriable", or "ignorable".
        retryable: True when severity == "retriable".
    """

    def __init__(
        self,
        message: str,
        *,
        code: Optional[int] = None,
        component: str = "",
        operation: str = "",
        severity: ServiceBridgeErrorSeverity = "retriable",
        cause: Optional[BaseException] = None,
    ) -> None:
        super().__init__(message)
        self.code = code
        self.component = component
        self.operation = operation
        self.severity = severity
        self.retryable = severity == "retriable"
        if cause is not None:
            self.__cause__ = cause

    def __repr__(self) -> str:
        return (
            f"ServiceBridgeError({self!s}, component={self.component!r}, "
            f"operation={self.operation!r}, severity={self.severity!r})"
        )


# ─── Public types ──────────────────────────────────────────────────────────────

class StreamWriter:
    """Allows a handler to push incremental data chunks to ServiceBridge
    during execution (AppendStream). Writes are silently dropped when offline.
    """

    def __init__(self, trace_id: str, client: "ServiceBridge") -> None:
        self._trace_id = trace_id
        self._client = client

    async def write(self, data: Any, key: str = "default") -> None:
        """Push a data chunk to the named stream lane.

        Args:
            data: JSON-serialisable payload.
            key: Stream lane name (default: "default").
        """
        if not self._trace_id or not self._client._stub:
            return
        with self._client._queue_lock:
            online = self._client._is_online
        if not online:
            return  # stream writes are not buffered offline
        payload = json.dumps(data).encode()
        if self._client._session_send(spb.Envelope(
            timestamp_ms=int(time.time() * 1000),
            stream_append=spb.StreamAppendFrame(
                run_id=self._trace_id, key=key, data=payload
            ),
        )):
            return
        try:
            await asyncio.get_event_loop().run_in_executor(
                None,
                lambda: self._client._stub.AppendStream(
                    pb.AppendStreamRequest(run_id=self._trace_id, key=key, data=payload),
                    metadata=self._client._metadata(),
                    timeout=5,
                ),
            )
        except Exception:
            pass  # best-effort

    async def end(self, key: str = "") -> None:
        """No-op for cross-SDK API symmetry. Stream lifecycle is managed by the runtime."""
        pass


@dataclass
class EventContext:
    """Delivery metadata and control methods passed to event handlers."""

    trace_id: str
    span_id: str
    topic: str
    group_name: str
    message_id: str
    attempt: int
    headers: dict[str, str]
    stream: StreamWriter = field(repr=False, default=None)  # type: ignore[assignment]
    _retry_fn: Callable[[int], None] = field(repr=False, default=None)  # type: ignore[assignment]
    _reject_fn: Callable[[str], None] = field(repr=False, default=None)  # type: ignore[assignment]

    def retry(self, delay_ms: int = 1000) -> None:
        """Request a retry after delay_ms milliseconds."""
        if self._retry_fn:
            self._retry_fn(delay_ms)

    def reject(self, reason: str = "rejected_by_consumer") -> None:
        """Permanently reject this message (moves to DLQ)."""
        if self._reject_fn:
            self._reject_fn(reason)


@dataclass
class ScheduleOpts:
    """Configuration for a scheduled job."""

    cron: str = ""
    """Cron expression (e.g. "0 * * * *"). Mutually exclusive with delay_ms."""
    delay_ms: int = 0
    """One-shot delay in milliseconds."""
    timezone: str = "UTC"
    """IANA timezone name."""
    misfire: str = "fire_now"
    """Misfire policy: "fire_now" (default) or "skip"."""
    via: str = "rpc"
    """Trigger mechanism: "rpc" (default), "event", or "workflow"."""
    retry_policy_json: str = ""
    """JSON-encoded retry policy."""


@dataclass
class WorkflowStep:
    """One step in a workflow DAG."""

    id: str
    type: str
    """One of "rpc", "event", "event_wait", "sleep", "workflow"."""
    ref: str = ""
    """Function, event topic, or sub-workflow name."""
    service: str = ""
    """Target service for rpc/workflow steps (disambiguates same ref across services)."""
    deps: list[str] = field(default_factory=list)
    """Step IDs that must complete before this step runs."""
    if_expr: str = ""
    """Optional server-side filter expression."""
    timeout_ms: int = 0
    duration_ms: int = 0
    """Used only for type="sleep"."""


@dataclass
class WorkflowOpts:
    """Optional limits for a workflow definition."""

    state_limit_bytes: int = 0
    """Maximum serialized state size in bytes. Defaults to 262144 (256 KB) if zero."""
    step_timeout_ms: int = 0
    """Default per-step timeout in milliseconds. Defaults to 30000 (30 s) if zero."""


@dataclass
class WatchTraceOpts:
    """Options for watch_trace()."""

    key: str = ""
    """Filter to a specific stream lane. Empty = all keys."""
    from_sequence: int = 0
    """Replay chunks with sequence > from_sequence (0 = full replay)."""


@dataclass
class TraceStreamEvent:
    """One chunk or terminal event from watch_trace()."""

    sequence: int
    key: str
    data: Any
    done: bool
    trace_status: str = ""


@dataclass
class WorkerTLSOpts:
    """Explicit worker mTLS materials.

    When omitted, SDK auto-provisions worker certificates via
    ProvisionWorkerCertificate.
    """

    ca_cert: str = ""
    cert: str = ""
    key: str = ""
    server_name: str = ""


# ─── Schema types ─────────────────────────────────────────────────────────────

@dataclass
class RpcFieldDef:
    """Describes one field in an inline Protobuf schema."""

    type: str
    """Protobuf scalar type: "string", "int32", "int64", "float", "double",
    "bool", "bytes", "uint32", "uint64", "fixed32", "fixed64", "sint32",
    "sint64"."""
    id: int
    """Protobuf field number (must be unique within the message)."""
    repeated: bool = False
    """True for repeated (array) fields."""


# RpcSchema maps field names to their RpcFieldDef.
RpcSchema = dict[str, RpcFieldDef]


@dataclass
class RpcSchemaOpts:
    """Inline Protobuf schemas for an RPC function's input and output."""

    input: Optional[RpcSchema] = None
    output: Optional[RpcSchema] = None


@dataclass
class Options:
    """SDK configuration options."""

    ca_cert: str = ""
    """Optional control-plane CA PEM override. By default CA is read from sbv2 service key."""
    heartbeat_interval_ms: int = 10_000
    capture_logs: bool = True
    """When True (default), attaches a ServiceBridgeLogHandler to the root logger."""
    queue_max_size: int = 1_000
    """Maximum operations buffered in the offline queue (default: 1000)."""
    queue_overflow: str = "drop-oldest"
    """Overflow policy: "drop-oldest" (default), "drop-newest", or "error"."""
    discovery_refresh_ms: int = 10_000
    """How often per-function endpoint lists are refreshed (ms, default: 10000)."""
    timeout_ms: int = 30_000
    """Global default RPC timeout in milliseconds (default: 30000). Per-call overrides take priority."""
    retries: int = 3
    """Global default retry count for RPC calls (default: 3). Per-call overrides take priority."""
    retry_delay_ms: int = 300
    """Base delay between retries in ms (default: 300). Uses exponential backoff: delay * 2^(attempt-1)."""
    worker_tls: Optional[WorkerTLSOpts] = None
    """Global explicit worker mTLS materials (optional)."""


class _InProcessWorkerContext:
    """Minimal grpc.ServicerContext shim for in-process worker session dispatch."""

    def __init__(self, trace_id: str = "") -> None:
        self._trace_id = trace_id

    def invocation_metadata(self):
        if not self._trace_id:
            return []
        return [("servicebridge-trace-id", self._trace_id)]

    def peer_identities(self):
        return []

    def abort(self, _code, details):
        raise RuntimeError(details)


@dataclass
class _FnChannelEntry:
    channels: list[grpc.Channel]
    stubs: list[Any]
    endpoints: list[str]
    next_index: int = 0


# Context variable that propagates trace context through async code.
import contextvars as _cv
_trace_ctx: _cv.ContextVar[dict[str, str]] = _cv.ContextVar("_sb_trace_ctx", default={})


def get_trace_context() -> Optional[dict[str, str]]:
    """Return the current trace context (trace_id, span_id) or None."""
    ctx = _trace_ctx.get({})
    tid = (ctx or {}).get("trace_id", "")
    if not tid:
        return None
    return {"trace_id": tid, "span_id": (ctx or {}).get("span_id", "")}


def with_trace_context(ctx: dict[str, str], fn: Callable[[], Any]) -> Any:
    """Run fn with the given trace context set."""
    token = _trace_ctx.set(ctx)
    try:
        return fn()
    finally:
        _trace_ctx.reset(token)


# ─── Helper: is gRPC error a connection error? ────────────────────────────────

def _is_connection_error(exc: Exception) -> bool:
    """Return True for gRPC UNAVAILABLE / UNKNOWN status codes."""
    if isinstance(exc, grpc.RpcError):
        code = exc.code()  # type: ignore[attr-defined]
        return code in (grpc.StatusCode.UNAVAILABLE, grpc.StatusCode.UNKNOWN)
    return False


def _format_queue_summary(ops: list[dict]) -> str:
    """Return a human-readable breakdown of operation types, e.g. '3 event, 2 reportCall'."""
    counts: dict[str, int] = {}
    for op in ops:
        kind = op.get("kind", "unknown")
        counts[kind] = counts.get(kind, 0) + 1
    order = ["event", "job", "workflow", "reportCallStart", "reportCall"]
    parts = [f"{counts[k]} {k}" for k in order if k in counts]
    extras = [f"{n} {k}" for k, n in counts.items() if k not in order]
    return ", ".join(parts + extras)


def _normalize_non_negative_int(value: Any, fallback: int) -> int:
    try:
        n = int(value)
    except (TypeError, ValueError):
        return max(0, int(fallback))
    if n < 0:
        return max(0, int(fallback))
    return n


def _normalize_positive_int(value: Any, fallback: int) -> int:
    try:
        n = int(value)
    except (TypeError, ValueError):
        n = 0
    if n > 0:
        return n
    try:
        f = int(fallback)
    except (TypeError, ValueError):
        f = 0
    if f > 0:
        return f
    return 1


def servicebridge(
    url: str,
    service_key: str,
    service_or_opts: Optional[Any] = None,
    opts: Optional[Options] = None,
) -> "ServiceBridge":
    """Create a ServiceBridge client (symmetry with Node servicebridge()).

    Args:
        url: gRPC server address (e.g. "localhost:14445").
        service_key: Service authentication key.
        service_or_opts: Optional service name (str) or Options.
        opts: Options when service_or_opts is a service name.

    Returns:
        ServiceBridge instance.
    """
    resolved_opts = opts
    resolved_service = ""
    if isinstance(service_or_opts, Options):
        resolved_opts = service_or_opts
    elif isinstance(service_or_opts, str):
        resolved_service = service_or_opts.strip()
    elif service_or_opts is not None:
        raise TypeError("service_or_opts must be str, Options, or None")
    return ServiceBridge(url, service_key, resolved_service, resolved_opts)


# ─── ServiceBridge client ──────────────────────────────────────────────────────

class ServiceBridge:
    """ServiceBridge SDK client.

    Args:
        grpc_url: Address of the ServiceBridge gRPC server (e.g. ``"localhost:14445"``).
        service_key: Service authentication key (service name extracted automatically from key).
        opts: Optional :class:`Options`.
    """

    def __init__(
        self,
        grpc_url: str,
        service_key: str,
        opts: Optional[Options] = None,
    ) -> None:
        self._grpc_url = grpc_url
        self._service_key = service_key
        self._opts = opts or Options()
        self._instance_id = _new_hex_id(3)
        try:
            key_id, _, _ = _parse_service_key_v2(service_key)
            self._service_name = key_id
            self._service_hint = key_id
        except RuntimeError:
            self._service_name = ""
            self._service_hint = ""

        self._rpc_handlers: dict[str, Any] = {}
        self._rpc_opts: dict[str, dict[str, Any]] = {}  # fn -> {allowed_callers, schema}
        self._event_handlers: dict[str, dict[str, Any]] = {}

        self._channel: Optional[grpc.Channel] = None
        self._stub: Optional[pb_grpc.ServiceBridgeStub] = None
        self._session_stub: Optional[spb_grpc.SessionServiceStub] = None
        self._cp_stub: Optional[cpb_grpc.ControlPlaneStub] = None
        self._worker_server: Optional[grpc.Server] = None
        self._worker_servicer: Any = None
        self._worker_address: str = ""
        self._worker_tls_creds: Any = None
        self._worker_tls_server_name: str = ""
        self._session_thread: Optional[threading.Thread] = None
        self._session_max_in_flight: int = 128
        self._registration_weight: int = 1

        # Per-function gRPC channel cache with manual endpoint round-robin.
        self._fn_channels: dict[str, _FnChannelEntry] = {}
        self._fn_channels_lock = threading.Lock()

        # Schema registry (fn -> RpcSchemaOpts).
        self._schemas: dict[str, RpcSchemaOpts] = {}
        self._compiled_schemas: dict[str, Any] = {}  # fn -> compiled cache

        # Offline queue.
        self._is_online: bool = False
        self._is_flushing: bool = False          # guarded by _queue_lock
        self._is_watching_channel: bool = False  # guarded by _queue_lock
        self._queue: deque[dict[str, Any]] = deque()
        self._queue_lock = threading.Lock()

        # Heartbeat / lifecycle.
        self._heartbeat_thread: Optional[threading.Thread] = None
        self._stop_event = threading.Event()
        self._stopped = False

        # Log batching.
        self._log_batch: list[pb.LogEntry] = []
        self._log_lock = threading.Lock()
        self._log_timer: Optional[threading.Timer] = None

        # Active worker session enqueue function (set while session is alive).
        # Use _session_send() for fire-and-forget delivery.
        self._session_enqueue_fn: Optional[Callable[[Any], None]] = None
        self._session_enqueue_lock = threading.Lock()

        if self._opts.capture_logs:
            self.attach_logging_handler()

    # ─── Session send helpers ─────────────────────────────────────────────────

    def _set_session_enqueue(self, fn: Optional[Callable[[Any], None]]) -> None:
        """Called by the session loop to register/clear the enqueue function."""
        with self._session_enqueue_lock:
            self._session_enqueue_fn = fn

    def _session_send(self, frame: Any) -> bool:
        """Fire-and-forget send via the open worker session stream.

        Returns True if the frame was queued, False if no session is active.
        """
        with self._session_enqueue_lock:
            fn = self._session_enqueue_fn
        if fn is None:
            return False
        try:
            fn(frame)
            return True
        except Exception:
            return False

    # ─── Schema registration ───────────────────────────────────────────────────

    def register_schema(self, fn: str, opts: RpcSchemaOpts) -> None:
        """Compile and cache Protobuf schemas for the named function.

        Call before ``serve()`` to enable binary Protobuf encoding.
        The schema JSON is also published to the control plane via RegisterFunction.
        """
        self._schemas[fn] = opts
        # Lazy compilation happens on first encode/decode.

    # ─── Registration API ──────────────────────────────────────────────────────

    def handle_rpc(
        self,
        fn: str,
        *,
        allowed_callers: Optional[list[str]] = None,
        schema: Optional[RpcSchemaOpts] = None,
        timeout_ms: int = 0,
        retryable: Optional[bool] = None,
        concurrency: int = 0,
    ) -> Callable:
        """Decorator that registers an RPC handler.

        Usage::

            @svc.handle_rpc("order.create")
            async def create_order(payload: dict) -> dict:
                return {"id": "123"}

            # With full context (stream + trace):
            @svc.handle_rpc("order.create", allowed_callers=["payments"])
            async def create_order(payload: dict, ctx) -> dict:
                await ctx.stream.write({"progress": 50})
                return {"id": "123"}
        """
        def decorator(func: Callable) -> Callable:
            self._rpc_handlers[fn] = func
            self._rpc_opts[fn] = {
                "allowed_callers": allowed_callers or [],
                "schema": schema,
                # Advisory hints for runtime scheduling/semantics.
                "timeout_ms": _normalize_non_negative_int(timeout_ms, 0),
                "retryable": retryable if isinstance(retryable, bool) else None,
                "concurrency": _normalize_non_negative_int(concurrency, 0),
            }
            if schema:
                self._schemas[fn] = schema
            return func
        return decorator

    def handle_event(
        self,
        topic: str,
        retry_policy_json: str = "",
        filter_expr: str = "",
        concurrency: int = 0,
        prefetch: int = 0,
    ) -> Callable:
        """Decorator that registers an event handler.

        If the same group name is registered twice, a ValueError is raised.

        Usage::

            @svc.handle_event("order.created")
            async def on_order_created(payload: dict, ec: EventContext) -> None:
                pass
        """
        prefix = self._service_hint or "consumer"
        gname = f"{prefix}.{topic}"

        def decorator(func: Callable) -> Callable:
            if gname in self._event_handlers:
                raise ValueError(
                    f"handle_event: duplicate group name {gname!r} — each group name must be unique"
                )
            self._event_handlers[gname] = {
                "handler": func,
                "group_name": gname,
                "topic": topic,
                "retry_policy_json": retry_policy_json,
                "filter_expr": filter_expr,
                # Advisory hints for runtime scheduling/semantics.
                "concurrency": _normalize_non_negative_int(concurrency, 0),
                "prefetch": _normalize_non_negative_int(prefetch, 0),
            }
            return func

        return decorator

    # ─── Serve ────────────────────────────────────────────────────────────────

    async def serve(
        self,
        *,
        host: str = "localhost",
        max_in_flight: int = 128,
        instance_id: str = "",
        weight: int = 1,
        tls: Optional[WorkerTLSOpts] = None,
    ) -> None:
        """Start the worker server and block until cancelled.

        This is the main entrypoint — call it with ``asyncio.run(svc.serve())``.
        Raises ``ValueError`` if no handlers have been registered via
        ``handle_rpc()`` or ``handle_event()``.
        """
        if not self._rpc_handlers and not self._event_handlers:
            raise ValueError(
                "no handlers registered — call handle_rpc() or handle_event() before serve()"
            )
        if max_in_flight < 1:
            raise ValueError("max_in_flight must be >= 1")
        if instance_id.strip():
            self._instance_id = instance_id.strip()
        self._registration_weight = _normalize_positive_int(weight, 1)
        self._session_max_in_flight = int(max_in_flight)
        # 1. Connect control plane
        await self._connect_control_plane()
        self._worker_tls_creds = None
        self._worker_tls_server_name = ""
        explicit_tls = self._resolve_explicit_worker_tls(tls)
        if explicit_tls is None:
            explicit_tls = self._resolve_explicit_worker_tls(self._opts.worker_tls)
        if explicit_tls is not None:
            self._worker_tls_creds, self._worker_tls_server_name = explicit_tls

        # Mark online and flush any queued operations.
        with self._queue_lock:
            self._is_online = True
        threading.Thread(target=self._flush_queue, daemon=True).start()

        # 2. Start worker gRPC server
        self._worker_address = await self._start_worker_server(host)
        logger.info("[servicebridge] worker listening at %s", self._worker_address)
        self._start_worker_session()

        # 3. Register + heartbeat
        await self._sync_registrations()
        self._start_heartbeat()

        # 4. Block until cancelled
        try:
            await asyncio.Event().wait()
        except (asyncio.CancelledError, KeyboardInterrupt):
            pass
        finally:
            await self.stop()

    async def stop(self) -> None:
        """Gracefully shut down the SDK: flush logs, close channels, stop servers."""
        with self._queue_lock:
            if self._stopped:
                return
            self._stopped = True
            self._is_online = False
            self._is_watching_channel = False
            self._queue.clear()

        self._stop_event.set()
        self._log_flush()

        if self._worker_server:
            self._worker_server.stop(grace=2)
        if self._session_thread and self._session_thread.is_alive():
            self._session_thread.join(timeout=2)
            self._session_thread = None
        if self._channel:
            self._channel.close()

        # Close per-function channels.
        with self._fn_channels_lock:
            for entry in self._fn_channels.values():
                for ch in entry.channels:
                    ch.close()
            self._fn_channels.clear()

    # ─── Rpc ──────────────────────────────────────────────────────────────────

    async def rpc(
        self,
        target_service: str,
        fn: str,
        payload: Any = None,
        *,
        retries: Optional[int] = None,
        timeout_ms: Optional[int] = None,
        retry_delay_ms: Optional[int] = None,
        trace_id: str = "",
        parent_span_id: str = "",
        mode: Optional[str] = None,
    ) -> Any:
        """Call an RPC function on a remote worker.

        Uses a persistent gRPC channel per function with round-trip endpoint
        caching and background refresh (``discovery_refresh_ms``).

        Args:
            target_service: Logical service name of the callee (e.g. ``"payments"``).
            fn: Function name registered with ``handle_rpc`` (e.g. ``"payment.charge"``).
            payload: JSON-serialisable payload.
            retries: Number of retry attempts (default: ``Options.retries``).
            timeout_ms: Per-call timeout in milliseconds (default: ``Options.timeout_ms``).
            retry_delay_ms: Base retry delay in ms (default: ``Options.retry_delay_ms``).
                Uses exponential backoff: ``delay * 2^(attempt-1)``.
            trace_id: Override trace ID.
            parent_span_id: Explicit parent span ID for distributed tracing.
            mode: Transport mode — ``"direct"`` (default) connects to the
                worker directly, ``"proxy"`` routes through the control plane.

        Returns:
            Parsed JSON response from the worker.
        """
        default_retries = _normalize_non_negative_int(
            self._opts.retries, _DEFAULT_RPC_RETRIES
        )
        default_timeout_ms = _normalize_positive_int(
            self._opts.timeout_ms, _DEFAULT_RPC_TIMEOUT_MS
        )
        default_retry_delay_ms = _normalize_positive_int(
            self._opts.retry_delay_ms, _DEFAULT_RPC_RETRY_DELAY_MS
        )
        retries = (
            default_retries
            if retries is None
            else _normalize_non_negative_int(retries, default_retries)
        )
        timeout_ms = (
            default_timeout_ms
            if timeout_ms is None
            else _normalize_positive_int(timeout_ms, default_timeout_ms)
        )
        retry_delay_ms = (
            default_retry_delay_ms
            if retry_delay_ms is None
            else _normalize_positive_int(retry_delay_ms, default_retry_delay_ms)
        )
        svc_name = (target_service or "").strip()
        fname = (fn or "").strip()
        if not svc_name or not fname:
            raise ServiceBridgeError(
                "rpc: target_service and fn are required",
                component="rpc",
                operation="",
                severity="fatal",
            )
        lookup_key = f"{svc_name}/{fname}"

        if self._stub is None:
            raise ServiceBridgeError(
                "ServiceBridge not started — call serve() first",
                component="rpc", operation=lookup_key, severity="fatal",
            )

        tc = _trace_ctx.get({})
        effective_trace_id = trace_id or tc.get("trace_id", "") or _new_hex_id()
        effective_parent_span_id = parent_span_id or tc.get("span_id", "")
        span_id = _new_hex_id()

        worker_fn = fname
        # Encode payload for worker call (binary Protobuf if schema is registered).
        payload_bytes = self._encode_payload(fname, payload)

        # Telemetry payloads must always be valid JSON for runtime ingestion.
        def _telemetry_json_bytes(value: Any) -> bytes:
            try:
                return json.dumps(value if value is not None else {}).encode()
            except TypeError:
                return json.dumps(str(value)).encode()

        telemetry_input_buf = _telemetry_json_bytes(payload)
        started_at = time.time_ns()

        _telemetry_target_service = svc_name
        _telemetry_fn_name = fname

        go_report = asyncio.get_event_loop().run_in_executor(
            None,
            lambda: self._report_call_start(
                effective_trace_id,
                span_id,
                effective_parent_span_id,
                _telemetry_fn_name,
                started_at,
                telemetry_input_buf,
                1,
                target_service=_telemetry_target_service,
            ),
        )
        asyncio.ensure_future(go_report)

        # Proxy mode: route the call through the control plane.
        if mode == "proxy":
            return await self._rpc_proxy(
                lookup_key=lookup_key,
                target_service=svc_name,
                fn_name=fname,
                payload_bytes=payload_bytes,
                trace_id=effective_trace_id,
                span_id=span_id,
                timeout_ms=timeout_ms,
                started_at=started_at,
                telemetry_input_buf=telemetry_input_buf,
            )

        # Get (or lazily create) a persistent channel for this function.
        _, worker_stub = await self._get_fn_channel(lookup_key)

        last_err: Optional[Exception] = None
        last_attempt = 1

        for attempt in range(retries + 1):
            attempt_no = attempt + 1
            last_attempt = attempt_no
            try:
                attempt_timeout_ms = timeout_ms
                attempt_timeout_sec = max(0.001, attempt_timeout_ms / 1000.0)
                watchdog_slack_sec = max(0.25, attempt_timeout_sec * 0.1)
                request = pb.HandleRequest(
                    fn=worker_fn,
                    payload=payload_bytes,
                    trace_id=effective_trace_id,
                    span_id=span_id,
                )

                loop = asyncio.get_event_loop()
                handle_callable = worker_stub.Handle
                result: Any

                # Prefer grpc.Future API so we can cancel explicitly when the
                # local watchdog fires and avoid hanging spans.
                if hasattr(handle_callable, "future"):
                    try:
                        unary_call = handle_callable.future(
                            request,
                            timeout=attempt_timeout_sec,
                            wait_for_ready=True,
                        )
                    except TypeError:
                        unary_call = handle_callable.future(
                            request,
                            timeout=attempt_timeout_sec,
                        )

                    def _await_call_result() -> Any:
                        return unary_call.result(timeout=attempt_timeout_sec)

                    try:
                        result = await asyncio.wait_for(
                            loop.run_in_executor(None, _await_call_result),
                            timeout=attempt_timeout_sec + watchdog_slack_sec,
                        )
                    except asyncio.TimeoutError as exc:
                        unary_call.cancel()
                        raise TimeoutError(
                            f"rpc attempt timeout after {attempt_timeout_ms}ms"
                        ) from exc
                    except Exception as exc:
                        future_timeout_error = getattr(grpc, "FutureTimeoutError", None)
                        if future_timeout_error and isinstance(exc, future_timeout_error):
                            unary_call.cancel()
                            raise TimeoutError(
                                f"rpc attempt timeout after {attempt_timeout_ms}ms"
                            ) from exc
                        raise
                else:
                    result = await asyncio.wait_for(
                        loop.run_in_executor(
                            None,
                            lambda: handle_callable(
                                request,
                                timeout=attempt_timeout_sec,
                            ),
                        ),
                        timeout=attempt_timeout_sec + watchdog_slack_sec,
                    )
                if not result.success:
                    raise ServiceBridgeError(
                        result.error or "handler returned failure",
                        component="rpc", operation=lookup_key,
                    )
                # Decode output (binary Protobuf → JSON if schema present).
                out = self._decode_output(fname, result.output)
                telemetry_output_buf = _telemetry_json_bytes(out)
                go_finish = asyncio.get_event_loop().run_in_executor(
                    None,
                    lambda a=attempt_no, out_buf=telemetry_output_buf: self._report_call(
                        effective_trace_id,
                        span_id,
                        _telemetry_fn_name,
                        started_at,
                        telemetry_input_buf,
                        True,
                        a,
                        out_buf,
                        "",
                        target_service=_telemetry_target_service,
                    ),
                )
                asyncio.ensure_future(go_finish)
                return out
            except Exception as exc:
                last_err = exc
                if _is_connection_error(exc):
                    self._mark_offline()
                if attempt < retries:
                    await asyncio.sleep(retry_delay_ms / 1000 * 2 ** attempt)
        err_msg = str(last_err) if last_err is not None else "rpc failed"
        go_finish = asyncio.get_event_loop().run_in_executor(
            None,
            lambda a=last_attempt, msg=err_msg: self._report_call(
                effective_trace_id,
                span_id,
                _telemetry_fn_name,
                started_at,
                telemetry_input_buf,
                False,
                a,
                b"",
                msg,
                target_service=_telemetry_target_service,
            ),
        )
        asyncio.ensure_future(go_finish)

        if last_err is not None:
            raise last_err
        raise ServiceBridgeError(
            f"rpc {lookup_key!r} failed",
            component="rpc",
            operation=lookup_key,
            severity="retriable",
        )

    # ─── RPC proxy path ────────────────────────────────────────────────────────

    async def _rpc_proxy(
        self,
        *,
        lookup_key: str,
        target_service: str,
        fn_name: str,
        payload_bytes: bytes,
        trace_id: str,
        span_id: str,
        timeout_ms: int,
        started_at: int,
        telemetry_input_buf: bytes,
    ) -> Any:
        """Execute an RPC call through the control plane (proxy mode)."""
        if self._cp_stub is None:
            raise ServiceBridgeError(
                "control plane stub not available — call serve() first",
                component="rpc", operation=lookup_key, severity="fatal",
            )

        _proxy_target_svc = target_service
        _proxy_fn_name = fn_name

        timeout_sec = max(0.001, timeout_ms / 1000.0)
        try:
            req = cpb.ProxyCallRequest(
                function_name=lookup_key,
                payload=payload_bytes,
                trace_id=trace_id,
                span_id=span_id,
                timeout_ms=timeout_ms,
            )
            resp = await asyncio.get_event_loop().run_in_executor(
                None,
                lambda: self._cp_stub.ProxyCall(
                    req,
                    metadata=self._metadata(),
                    timeout=timeout_sec,
                ),
            )
            if not resp.success:
                err_msg = resp.error or "proxy call failed"
                go_finish = asyncio.get_event_loop().run_in_executor(
                    None,
                    lambda: self._report_call(
                        trace_id, span_id, _proxy_fn_name, started_at,
                        telemetry_input_buf, False, 1, b"", err_msg,
                        target_service=_proxy_target_svc,
                    ),
                )
                asyncio.ensure_future(go_finish)
                raise ServiceBridgeError(
                    err_msg, component="rpc", operation=lookup_key,
                    severity="retriable",
                )
            out = json.loads(resp.output) if resp.output else None

            def _telemetry_json_bytes(value: Any) -> bytes:
                try:
                    return json.dumps(value if value is not None else {}).encode()
                except TypeError:
                    return json.dumps(str(value)).encode()

            telemetry_output_buf = _telemetry_json_bytes(out)
            go_finish = asyncio.get_event_loop().run_in_executor(
                None,
                lambda out_buf=telemetry_output_buf: self._report_call(
                    trace_id, span_id, _proxy_fn_name, started_at,
                    telemetry_input_buf, True, 1, out_buf, "",
                    target_service=_proxy_target_svc,
                ),
            )
            asyncio.ensure_future(go_finish)
            return out
        except ServiceBridgeError:
            raise
        except Exception as exc:
            err_msg = str(exc)[:2048]
            go_finish = asyncio.get_event_loop().run_in_executor(
                None,
                lambda msg=err_msg: self._report_call(
                    trace_id, span_id, _proxy_fn_name, started_at,
                    telemetry_input_buf, False, 1, b"", msg,
                    target_service=_proxy_target_svc,
                ),
            )
            asyncio.ensure_future(go_finish)
            raise ServiceBridgeError(
                f"proxy rpc {lookup_key!r}: {exc}",
                component="rpc", operation=lookup_key,
            ) from exc

    # ─── Event ────────────────────────────────────────────────────────────────

    async def event(
        self,
        topic: str,
        payload: Any = None,
        *,
        idempotency_key: str = "",
        trace_id: str = "",
        parent_span_id: str = "",
        headers: Optional[dict[str, str]] = None,
    ) -> str:
        """Publish an event to a topic.

        When the control plane is offline, the operation is buffered in the
        offline queue and retried transparently on reconnect.

        Returns:
            Assigned message ID (empty string when buffered offline).
        """
        effective_trace_id = trace_id or _new_hex_id()
        parent_span_id = parent_span_id or _trace_ctx.get({}).get("span_id", "")
        payload_bytes = json.dumps(payload).encode() if payload is not None else b"{}"

        with self._queue_lock:
            online = self._is_online

        if not online:
            self._enqueue_offline({
                "kind": "event",
                "topic": topic,
                "payload": payload_bytes,
                "idempotency_key": idempotency_key,
                "trace_id": effective_trace_id,
                "parent_span_id": parent_span_id,
                "headers": headers or {},
            })
            return ""

        try:
            resp = await asyncio.get_event_loop().run_in_executor(
                None,
                lambda: self._stub.Publish(
                    pb.PublishRequest(
                        topic=topic,
                        payload=payload_bytes,
                        trace_id=effective_trace_id,
                        parent_span_id=parent_span_id,
                        producer_service=self._service_name,
                        idempotency_key=idempotency_key,
                        headers=headers or {},
                    ),
                    metadata=self._metadata(),
                    timeout=10,
                ),
            )
            return resp.message_id
        except Exception as exc:
            if _is_connection_error(exc):
                self._mark_offline()
                self._enqueue_offline({
                    "kind": "event",
                    "topic": topic,
                    "payload": payload_bytes,
                    "idempotency_key": idempotency_key,
                    "trace_id": effective_trace_id,
                    "parent_span_id": parent_span_id,
                    "headers": headers or {},
                })
                return ""
            raise ServiceBridgeError(
                f"publish event {topic!r}: {exc}",
                component="event", operation="publish",
            ) from exc

    async def publish_event(
        self,
        topic: str,
        payload: Any = None,
        *,
        trace_id: str = "",
        parent_span_id: str = "",
        headers: Optional[dict[str, str]] = None,
    ) -> str:
        """Publish an event via the worker session stream (symmetry with Node publishEvent).

        Uses event() when session stream is not available (Python session proto
        does not yet include publish_message). Same outcome, same offline queue.
        """
        return await self.event(
            topic,
            payload,
            trace_id=trace_id or (_trace_ctx.get({}).get("trace_id", "") or _new_hex_id()),
            parent_span_id=parent_span_id or _trace_ctx.get({}).get("span_id", ""),
            headers=headers,
        )

    # ─── Job ──────────────────────────────────────────────────────────────────

    async def job_rpc(self, service: str, fn: str, opts: Optional[ScheduleOpts] = None) -> str:
        """Register a scheduled RPC job with explicit service and function name.

        When offline, the operation is buffered and retried on reconnect.

        Returns:
            Assigned job ID (empty string when buffered offline).
        """
        if not service or not fn:
            raise ValueError("job_rpc: service and fn are required")
        o = opts or ScheduleOpts()
        o.via = "rpc"
        target = f"{service}/{fn}"
        return await self._register_job(target, o)

    async def job_event(self, topic: str, opts: Optional[ScheduleOpts] = None) -> str:
        """Register a scheduled event job.

        When offline, the operation is buffered and retried on reconnect.

        Returns:
            Assigned job ID (empty string when buffered offline).
        """
        if not topic:
            raise ValueError("job_event: topic is required")
        o = opts or ScheduleOpts()
        o.via = "event"
        return await self._register_job(topic, o)

    async def job_workflow(self, workflow_name: str, opts: Optional[ScheduleOpts] = None) -> str:
        """Register a scheduled workflow job.

        When offline, the operation is buffered and retried on reconnect.

        Returns:
            Assigned job ID (empty string when buffered offline).
        """
        if not workflow_name:
            raise ValueError("job_workflow: workflow_name is required")
        o = opts or ScheduleOpts()
        o.via = "workflow"
        return await self._register_job(workflow_name, o)

    async def _register_job(self, target: str, opts: ScheduleOpts) -> str:
        """Internal helper for job registration."""
        misfire = opts.misfire or "fire_now"
        via = opts.via or "rpc"
        tz = opts.timezone or "UTC"

        with self._queue_lock:
            online = self._is_online

        if not online:
            self._enqueue_offline({"kind": "job", "target": target, "opts": opts})
            return ""

        try:
            resp = await asyncio.get_event_loop().run_in_executor(
                None,
                lambda: self._stub.RegisterJob(
                    pb.RegisterJobRequest(
                        cron_expr=opts.cron,
                        timezone=tz,
                        misfire_policy=misfire,
                        target_type=via,
                        target_ref=target,
                        delay_ms=opts.delay_ms,
                        service_name=self._service_name,
                        retry_policy_json=opts.retry_policy_json,
                    ),
                    metadata=self._metadata(),
                    timeout=10,
                ),
            )
            return resp.id
        except Exception as exc:
            if _is_connection_error(exc):
                self._mark_offline()
                self._enqueue_offline({"kind": "job", "target": target, "opts": opts})
                return ""
            raise ServiceBridgeError(
                f"register job {target!r}: {exc}",
                component="job", operation="register",
            ) from exc

    # ─── Workflow ─────────────────────────────────────────────────────────────

    async def workflow(
        self,
        name: str,
        steps: list[WorkflowStep],
        opts: Optional["WorkflowOpts"] = None,
    ) -> str:
        """Register a workflow DAG with the control plane.

        When offline, the operation is buffered and retried on reconnect.

        Args:
            name: Workflow name.
            steps: Ordered list of :class:`WorkflowStep` definitions.
            opts: Optional :class:`WorkflowOpts` for per-workflow limits.

        Returns:
            Assigned workflow run ID (empty string when buffered offline).
        """
        definition = json.dumps([_step_to_dict(s) for s in steps])
        opts_json = _workflow_opts_json(opts)

        with self._queue_lock:
            online = self._is_online

        if not online:
            self._enqueue_offline({"kind": "workflow", "name": name, "steps": steps, "opts": opts})
            return ""

        try:
            req = pb.RegisterWorkflowRequest(
                name=name,
                definition=definition,
                service_name=self._service_name,
            )
            if opts_json:
                req.opts = opts_json
            resp = await asyncio.get_event_loop().run_in_executor(
                None,
                lambda: self._stub.RegisterWorkflow(req, metadata=self._metadata(), timeout=10),
            )
            return resp.id
        except Exception as exc:
            if _is_connection_error(exc):
                self._mark_offline()
                self._enqueue_offline({"kind": "workflow", "name": name, "steps": steps, "opts": opts})
                return ""
            raise ServiceBridgeError(
                f"register workflow {name!r}: {exc}",
                component="workflow", operation="register",
            ) from exc

    async def execute_workflow(
        self, service: str, name: str, input: Any = None
    ) -> dict[str, str]:
        """Start a workflow execution by target service and workflow name.

        Returns:
            Dict with ``traceId`` and ``groupTraceId`` keys.
        """
        if self._cp_stub is None:
            raise ServiceBridgeError(
                "control plane stub not available — call serve() first",
                component="workflow",
                operation="execute",
                severity="fatal",
            )
        svc = (service or "").strip()
        wf_name = (name or "").strip()
        if not svc or not wf_name:
            raise ServiceBridgeError(
                "execute_workflow: service and name are required",
                component="workflow",
                operation="execute",
            )
        payload = json.dumps(input if input is not None else {}).encode()
        req = cpb.ExecuteWorkflowRequest(
            workflow_name=wf_name,
            service_name=svc,
            input=payload,
        )
        try:
            resp = await asyncio.get_event_loop().run_in_executor(
                None,
                lambda: self._cp_stub.ExecuteWorkflow(
                    req,
                    metadata=self._metadata(),
                    timeout=10,
                ),
            )
            return {"traceId": resp.trace_id, "groupTraceId": resp.trace_id}
        except Exception as exc:
            raise ServiceBridgeError(
                f"execute workflow {wf_name!r}: {exc}",
                component="workflow",
                operation="execute",
            ) from exc

    async def cancel_workflow(self, trace_id: str) -> None:
        """Cancel an in-progress workflow by trace ID."""
        try:
            resp = await asyncio.get_event_loop().run_in_executor(
                None,
                lambda: self._stub.CancelWorkflow(
                    pb.CancelWorkflowRunRequest(run_id=trace_id),
                    metadata=self._metadata(),
                    timeout=10,
                ),
            )
            if not resp.cancelled:
                raise ServiceBridgeError(
                    f"cancel workflow {trace_id!r}: not cancelled",
                    component="workflow", operation="cancel",
                )
        except Exception as exc:
            if isinstance(exc, ServiceBridgeError):
                raise
            raise ServiceBridgeError(
                f"cancel workflow {trace_id!r}: {exc}",
                component="workflow",
                operation="cancel",
            ) from exc

    # ─── WatchTrace ───────────────────────────────────────────────────────────

    async def watch_trace(
        self,
        trace_id: str,
        opts: Optional[WatchTraceOpts] = None,
    ) -> AsyncIterator[TraceStreamEvent]:
        """Subscribe to a realtime stream of chunks for a workflow/job trace.

        Auto-reconnects with exponential backoff (0.5s → 5s) on transient errors.
        Deduplicates chunks by sequence number.

        Usage::

            async for event in svc.watch_trace(trace_id):
                print(event.data)
                if event.done:
                    break
        """
        key = opts.key if opts else ""
        from_seq = opts.from_sequence if opts else 0

        backoff = 0.5
        last_seq = from_seq
        loop = asyncio.get_event_loop()

        def _metadata_value(exc: Exception, key_name: str) -> str:
            key_lower = key_name.lower()
            providers: list[Any] = []

            trailing = getattr(exc, "trailing_metadata", None)
            if callable(trailing):
                try:
                    md = trailing()
                    if md:
                        providers.append(md)
                except Exception:
                    pass

            meta_attr = getattr(exc, "metadata", None)
            if meta_attr:
                providers.append(meta_attr)

            for md in providers:
                if hasattr(md, "get") and callable(md.get):
                    try:
                        values = md.get(key_name)
                    except Exception:
                        values = None
                    if values:
                        first = values[0]
                        return (
                            first.decode(errors="ignore")
                            if isinstance(first, bytes)
                            else str(first)
                        )
                for item in md:
                    if not isinstance(item, tuple) or len(item) != 2:
                        continue
                    k, v = item
                    if str(k).lower() != key_lower:
                        continue
                    return v.decode(errors="ignore") if isinstance(v, bytes) else str(v)
            return ""

        def _maybe_resume_from(exc: Exception) -> None:
            nonlocal last_seq
            raw = _metadata_value(exc, "servicebridge-trace-stream-resume-from")
            if not raw:
                return
            try:
                resume = int(raw)
            except ValueError:
                return
            if resume > last_seq:
                last_seq = resume

        def _should_retry(exc: Exception) -> bool:
            retryable_flag = _metadata_value(exc, "servicebridge-trace-stream-retryable")
            if retryable_flag.lower() == "true":
                return True
            if not isinstance(exc, grpc.RpcError):
                return False
            code = exc.code()  # type: ignore[attr-defined]
            return code in (
                grpc.StatusCode.UNAVAILABLE,
                grpc.StatusCode.UNKNOWN,
                grpc.StatusCode.DEADLINE_EXCEEDED,
                grpc.StatusCode.RESOURCE_EXHAUSTED,
            )

        while True:
            if self._cp_stub is None:
                await asyncio.sleep(backoff)
                backoff = min(backoff * 2, 5.0)
                continue

            try:
                request = cpb.WatchTraceRequest(
                    trace_id=trace_id,
                    key=key,
                    from_seq=last_seq,
                )
                stream_call = self._cp_stub.WatchTrace(request, metadata=self._metadata())
                backoff = 0.5

                async for chunk in _aiter_grpc_stream(
                    stream_call,
                    loop,
                    queue_limit=256,
                    overflow_error_factory=lambda: ServiceBridgeError(
                        f"watch trace {trace_id!r}: consumer is not draining fast enough",
                        component="stream",
                        operation="watch_trace",
                        severity="fatal",
                    ),
                ):
                    # ControlPlane.TraceStreamChunk: seq, key, event_type, data, trace_status
                    seq = chunk.seq
                    chunk_type = chunk.event_type
                    if seq > 0 and seq <= last_seq:
                        continue
                    if seq > last_seq:
                        last_seq = seq

                    data: Any = None
                    if chunk_type == "chunk" and chunk.data:
                        try:
                            data = json.loads(chunk.data)
                        except json.JSONDecodeError as exc:
                            raise ServiceBridgeError(
                                f"watch trace {trace_id!r}: non-json chunk payload at sequence {seq}",
                                component="stream",
                                operation="watch_trace",
                                severity="fatal",
                            ) from exc
                    elif chunk.data:
                        try:
                            data = json.loads(chunk.data)
                        except json.JSONDecodeError:
                            data = None

                    event = TraceStreamEvent(
                        sequence=seq,
                        key=chunk.key or key,
                        data=data,
                        done=chunk_type == "trace_complete",
                        trace_status=chunk.trace_status,
                    )
                    yield event
                    if event.done:
                        return
                return

            except asyncio.CancelledError:
                return
            except Exception as exc:
                if isinstance(exc, ServiceBridgeError) and exc.severity == "fatal":
                    raise
                _maybe_resume_from(exc)
                if not _should_retry(exc):
                    raise ServiceBridgeError(
                        f"watch trace {trace_id!r}: {exc}",
                        component="stream",
                        operation="watch_trace",
                        severity="fatal",
                    ) from exc
                await asyncio.sleep(backoff)
                backoff = min(backoff * 2, 5.0)

    # ─── HTTP helpers ──────────────────────────────────────────────────────────

    def start_http_span(
        self,
        method: str,
        path: str,
        trace_id: str = "",
        parent_span_id: str = "",
    ) -> HttpSpan:
        """Create a tracing span for an incoming HTTP request."""
        effective_trace_id = trace_id or _new_hex_id()
        span_id = _new_hex_id()
        fn = f"http:{method}:{path}"
        started_at = time.time_ns()
        input_buf = json.dumps({"method": method, "path": path}).encode()

        self._report_call_start(effective_trace_id, span_id, parent_span_id, fn, started_at, input_buf, 1, span_type="http")

        return HttpSpan(
            trace_id=effective_trace_id,
            span_id=span_id,
            _fn=fn,
            _started_at=started_at,
            _client=self,
            _input_buf=input_buf,
        )

    async def register_http_endpoint(
        self,
        method_or_opts: Optional[Any] = None,
        route: Optional[str] = None,
        *,
        opts: Optional[dict[str, Any]] = None,
        instance_id: str = "",
        endpoint: str = "",
        allowed_callers: Optional[list[str]] = None,
        request_schema_json: str = "",
        response_schema_json: str = "",
        transport: str = "",
    ) -> None:
        """Register an HTTP route in the ServiceBridge catalog (symmetry with Node).

        Args:
            method_or_opts: HTTP method (str) or opts dict. When dict, route from opts.
            route: Route pattern when method_or_opts is method string.
            opts: Full opts dict (Node style). Overrides method/route when provided.
            **kwargs: instance_id?, endpoint?, allowed_callers?, etc.
        """
        if isinstance(method_or_opts, dict):
            o = method_or_opts
            method = o.get("method", "")
            route = o.get("route", "")
        else:
            o = opts or {}
            method = (method_or_opts or o.get("method") or "").strip()
            route = (route or o.get("route") or "").strip()
        instance_id = o.get("instance_id", instance_id) or ""
        endpoint = o.get("endpoint", endpoint) or ""
        allowed_callers = o.get("allowed_callers", allowed_callers) or []
        request_schema_json = o.get("request_schema_json", request_schema_json) or ""
        response_schema_json = o.get("response_schema_json", response_schema_json) or ""
        transport = o.get("transport", transport) or ""

        if self._stub is None:
            raise ServiceBridgeError(
                "ServiceBridge not started — call serve() first",
                component="http",
                operation="register_http_endpoint",
                severity="fatal",
            )
        req = pb.RegisterHttpEndpointRequest(
            service_name=self._service_name,
            method=method,
            route_pattern=route,
            request_schema_json=request_schema_json,
            response_schema_json=response_schema_json,
            instance_id=instance_id or self._instance_id,
            endpoint=endpoint or self._worker_address,
            transport=transport or "http",
            allowed_callers=allowed_callers or [],
        )

        def _call() -> Any:
            return self._stub.RegisterHttpEndpoint(req, metadata=self._metadata(), timeout=10)

        resp = await asyncio.get_event_loop().run_in_executor(None, _call)
        if not resp.ok:
            raise ServiceBridgeError(
                "register_http_endpoint rejected by runtime",
                component="http",
                operation="register_http_endpoint",
            )

    # ─── Offline queue ────────────────────────────────────────────────────────

    def _enqueue_offline(self, op: dict[str, Any]) -> None:
        with self._queue_lock:
            max_size = self._opts.queue_max_size
            policy = self._opts.queue_overflow

            if len(self._queue) >= max_size:
                if policy == "error":
                    raise ServiceBridgeError(
                        "ServiceBridge offline queue is full",
                        component="queue", operation="enqueue",
                        severity="retriable",
                    )
                elif policy == "drop-newest":
                    return
                else:  # "drop-oldest"
                    self._queue.popleft()

            self._queue.append(op)

    def _mark_offline(self) -> None:
        with self._queue_lock:
            if not self._is_online or self._stopped:
                return
            self._is_online = False
        logger.warning("[servicebridge] lost connection to runtime — entering offline mode")
        # The watchChannelConnectivity thread will detect the READY transition
        # and restore the connection; ensure it is running.
        self._ensure_channel_watch()

    # ── Event-driven reconnect ─────────────────────────────────────────────────
    #
    # Design: instead of a blind threading.Timer(2.0) loop, we use
    # channel.subscribe(callback, try_to_connect=True) which invokes the
    # callback on every real gRPC connectivity-state transition
    # (IDLE → CONNECTING → READY / TRANSIENT_FAILURE).  gRPC-python handles
    # TRANSIENT_FAILURE → CONNECTING retries with its own exponential backoff
    # internally; we do not replicate that logic here.
    #
    #   IDLE ──try_to_connect──► CONNECTING ──success──► READY ──idle──► IDLE
    #                                  │                    │
    #                               failure              failure
    #                                  ▼                    ▼
    #                           TRANSIENT_FAILURE ◄──────────

    def _watch_channel_connectivity(self) -> None:
        """Subscribe to channel state changes and drive _is_online accordingly."""
        channel = self._channel
        if channel is None:
            return

        def _on_state(state: grpc.ChannelConnectivity) -> None:
            if state == grpc.ChannelConnectivity.READY:
                was_offline = False
                queue_len = 0
                with self._queue_lock:
                    if not self._is_online and not self._stopped:
                        self._is_online = True
                        was_offline = True
                        queue_len = len(self._queue)
                if was_offline:
                    if queue_len > 0:
                        logger.info(
                            "[servicebridge] reconnected to runtime — flushing %d queued operation(s)",
                            queue_len,
                        )
                    else:
                        logger.info("[servicebridge] reconnected to runtime")
                    # Flush synchronously in this callback thread; trigger
                    # a heartbeat only after all queued ops are delivered.
                    self._flush_queue()
                    self._trigger_immediate_heartbeat()

            elif state == grpc.ChannelConnectivity.TRANSIENT_FAILURE:
                with self._queue_lock:
                    if self._is_online and not self._stopped:
                        self._is_online = False
                    else:
                        return
                logger.warning(
                    "[servicebridge] lost connection to runtime — entering offline mode"
                )

            elif state in (grpc.ChannelConnectivity.SHUTDOWN,):
                with self._queue_lock:
                    self._is_watching_channel = False
            # CONNECTING and IDLE: do not change _is_online.

        channel.subscribe(_on_state, try_to_connect=True)

    def _ensure_channel_watch(self) -> None:
        """Start the channel watch subscription if not already active."""
        with self._queue_lock:
            if self._is_watching_channel or self._stopped or self._channel is None:
                return
            self._is_watching_channel = True
        self._watch_channel_connectivity()

    def _flush_queue(self) -> None:
        """Drain the offline queue FIFO when online."""
        with self._queue_lock:
            if self._is_flushing or not self._is_online or len(self._queue) == 0 or self._stopped:
                return
            self._is_flushing = True
            snapshot = list(self._queue)
        try:
            self.__flush_queue_inner(snapshot)
        finally:
            with self._queue_lock:
                self._is_flushing = False

    def __flush_queue_inner(self, snapshot: list[dict[str, Any]]) -> None:

        flushed = 0
        while True:
            with self._queue_lock:
                if not self._is_online or len(self._queue) == 0 or self._stopped:
                    break
                op = self._queue[0]

            try:
                self._flush_op(op)
                flushed += 1
            except Exception as exc:
                if _is_connection_error(exc):
                    self._mark_offline()
                    break
                _sdk_logger.debug("flush offline queue op %r: %s (skipping)", op.get("kind"), exc)

            with self._queue_lock:
                if self._queue and self._queue[0] is op:
                    self._queue.popleft()

        if flushed > 0:
            with self._queue_lock:
                remaining = len(self._queue)
            sent_ops = snapshot[:flushed]
            summary = _format_queue_summary(sent_ops)
            if remaining > 0:
                logger.info(
                    "[servicebridge] flushed %d/%d queued operation(s) to runtime (%s) — %d still queued",
                    flushed, len(snapshot), summary, remaining,
                )
            else:
                logger.info(
                    "[servicebridge] flushed %d/%d queued operation(s) to runtime (%s)",
                    flushed, len(snapshot), summary,
                )

    def _trigger_immediate_heartbeat(self) -> None:
        """Send a heartbeat immediately after reconnect to update the service map."""
        try:
            asyncio.run(self._sync_registrations())
        except Exception as exc:
            if _is_connection_error(exc):
                self._mark_offline()

    def _flush_op(self, op: dict[str, Any]) -> None:
        kind = op["kind"]
        md = self._metadata()

        if kind == "event":
            self._stub.Publish(
                pb.PublishRequest(
                    topic=op["topic"],
                    payload=op["payload"],
                    trace_id=op.get("trace_id", ""),
                    parent_span_id=op.get("parent_span_id", ""),
                    producer_service=self._service_name,
                    idempotency_key=op.get("idempotency_key", ""),
                    headers=op.get("headers", {}),
                ),
                metadata=md,
                timeout=10,
            )
        elif kind == "job":
            o: ScheduleOpts = op["opts"]
            self._stub.RegisterJob(
                pb.RegisterJobRequest(
                    cron_expr=o.cron,
                    timezone=o.timezone or "UTC",
                    misfire_policy=o.misfire or "fire_now",
                    target_type=o.via or "rpc",
                    target_ref=op["target"],
                    delay_ms=o.delay_ms,
                    service_name=self._service_name,
                    retry_policy_json=o.retry_policy_json,
                ),
                metadata=md,
                timeout=10,
            )
        elif kind == "workflow":
            steps: list[WorkflowStep] = op["steps"]
            definition = json.dumps([_step_to_dict(s) for s in steps])
            wf_req = pb.RegisterWorkflowRequest(
                name=op["name"],
                definition=definition,
                service_name=self._service_name,
            )
            wf_opts_json = _workflow_opts_json(op.get("opts"))
            if wf_opts_json:
                wf_req.opts = wf_opts_json
            self._stub.RegisterWorkflow(wf_req, metadata=md, timeout=10)
        elif kind == "reportCallStart":
            sent = self._session_send(spb.Envelope(
                timestamp_ms=int(time.time() * 1000),
                telemetry=spb.TelemetryFrame(spans=[spb.SpanEvent(
                    kind=spb.SpanEvent.KIND_START,
                    trace_id=op["trace_id"],
                    span_id=op["span_id"],
                    parent_span_id=op.get("parent_span_id", ""),
                    fn=op["fn"],
                    started_at=op["started_at"],
                    input=op["input"],
                    attempt=op["attempt"],
                    span_type=op.get("span_type", "rpc"),
                    target_service=op.get("target_service", ""),
                )]),
            ))
            if not sent:
                self._stub.ReportCallStart(
                    pb.ReportCallStartRequest(
                        trace_id=op["trace_id"],
                        span_id=op["span_id"],
                        parent_span_id=op.get("parent_span_id", ""),
                        fn=op["fn"],
                        service_name=self._service_name,
                        instance_id=self._instance_id,
                        started_at=op["started_at"],
                        input=op["input"],
                        attempt=op["attempt"],
                    ),
                    metadata=md,
                    timeout=5,
                )
        elif kind == "reportCall":
            duration_ms = op.get("duration_ms")
            if duration_ms is None:
                duration_ms = (time.time_ns() - op["started_at"]) // 1_000_000
            sent = self._session_send(spb.Envelope(
                timestamp_ms=int(time.time() * 1000),
                telemetry=spb.TelemetryFrame(spans=[spb.SpanEvent(
                    kind=spb.SpanEvent.KIND_FINISH,
                    trace_id=op["trace_id"],
                    span_id=op["span_id"],
                    fn=op["fn"],
                    started_at=op["started_at"],
                    duration_ms=duration_ms,
                    success=op["success"],
                    error=op.get("error", ""),
                    input=op["input"],
                    output=op.get("output", b""),
                    attempt=op["attempt"],
                    span_type=op.get("span_type", "rpc"),
                    target_service=op.get("target_service", ""),
                )]),
            ))
            if not sent:
                self._stub.ReportCall(
                    pb.ReportCallRequest(
                        trace_id=op["trace_id"],
                        span_id=op["span_id"],
                        fn=op["fn"],
                        service_name=self._service_name,
                        instance_id=self._instance_id,
                        started_at=op["started_at"],
                        duration_ms=duration_ms,
                        success=op["success"],
                        error=op.get("error", ""),
                        input=op["input"],
                        output=op.get("output", b""),
                        attempt=op["attempt"],
                    ),
                    metadata=md,
                    timeout=5,
                )

    # ─── Per-function channel cache ────────────────────────────────────────────

    async def _get_fn_channel(
        self, fn: str
    ) -> tuple[grpc.Channel, "pb_grpc.ServiceBridgeWorkerStub"]:
        """Return (or lazily create) a worker channel for ``fn`` with endpoint RR."""
        with self._fn_channels_lock:
            entry = self._fn_channels.get(fn)
            if entry is not None:
                return self._pick_fn_channel(entry)

        # Lookup endpoints for this function.
        loop = asyncio.get_event_loop()
        resp = await loop.run_in_executor(
            None,
            lambda: self._stub.LookupFunction(
                pb.LookupFunctionRequest(fn_name=fn),
                metadata=self._metadata(),
                timeout=10,
            ),
        )
        if not resp.found or not resp.endpoints.endpoints:
            raise ServiceBridgeError(
                f"no endpoints for '{fn}'",
                component="rpc", operation=fn, severity="retriable",
            )

        endpoints = self._resolve_lookup_endpoints(resp.endpoints.endpoints)
        if not endpoints:
            raise ServiceBridgeError(
                f"no endpoints for '{fn}'",
                component="rpc", operation=fn, severity="retriable",
            )
        new_entry = self._build_fn_channel_entry(endpoints)

        with self._fn_channels_lock:
            existing = self._fn_channels.get(fn)
            if existing is not None:
                self._close_fn_channel_entry(new_entry)
                return self._pick_fn_channel(existing)
            self._fn_channels[fn] = new_entry

        # Start background endpoint refresh for this function.
        threading.Thread(
            target=self._refresh_fn_channel_loop,
            args=(fn,),
            daemon=True,
        ).start()

        return self._pick_fn_channel(new_entry)

    def _build_fn_channel_entry(self, endpoints: list[str]) -> _FnChannelEntry:
        channels: list[grpc.Channel] = []
        stubs: list[Any] = []
        for endpoint in endpoints:
            ch = self._make_worker_channel(endpoint)
            channels.append(ch)
            stubs.append(pb_grpc.ServiceBridgeWorkerStub(ch))
        return _FnChannelEntry(channels=channels, stubs=stubs, endpoints=endpoints)

    def _pick_fn_channel(
        self, entry: _FnChannelEntry
    ) -> tuple[grpc.Channel, "pb_grpc.ServiceBridgeWorkerStub"]:
        if not entry.channels or not entry.stubs:
            raise RuntimeError("worker channel entry is empty")
        idx = entry.next_index % len(entry.channels)
        entry.next_index = (idx + 1) % len(entry.channels)
        return entry.channels[idx], entry.stubs[idx]

    def _close_fn_channel_entry(self, entry: _FnChannelEntry) -> None:
        for ch in entry.channels:
            try:
                ch.close()
            except Exception:
                pass

    def _refresh_fn_channel_loop(self, fn: str) -> None:
        """Periodically refresh endpoints for a function and recreate channel if changed."""
        refresh_ms = self._opts.discovery_refresh_ms
        while not self._stop_event.wait(refresh_ms / 1000):
            if self._stub is None:
                continue
            try:
                resp = self._stub.LookupFunction(
                    pb.LookupFunctionRequest(fn_name=fn),
                    metadata=self._metadata(),
                    timeout=10,
                )
                if not resp.found or not resp.endpoints.endpoints:
                    continue
                new_eps = self._resolve_lookup_endpoints(resp.endpoints.endpoints)
                if not new_eps:
                    continue
                old_entry: Optional[_FnChannelEntry] = None
                with self._fn_channels_lock:
                    current = self._fn_channels.get(fn)
                    if current is None:
                        break
                    if set(new_eps) == set(current.endpoints):
                        continue
                candidate = self._build_fn_channel_entry(new_eps)
                with self._fn_channels_lock:
                    current = self._fn_channels.get(fn)
                    if current is None:
                        self._close_fn_channel_entry(candidate)
                        break
                    if set(new_eps) == set(current.endpoints):
                        self._close_fn_channel_entry(candidate)
                        continue
                    old_entry = current
                    self._fn_channels[fn] = candidate
                if old_entry is not None:
                    self._close_fn_channel_entry(old_entry)
            except Exception:
                pass

    # ─── Telemetry ────────────────────────────────────────────────────────────

    def _report_call_start(
        self, trace_id, span_id, parent_span_id, fn, started_at, input_buf, attempt,
        span_type: str = "rpc",
        target_service: str = "",
    ) -> None:
        if self._stub is None:
            return

        if self._session_send(spb.Envelope(
            timestamp_ms=int(time.time() * 1000),
            telemetry=spb.TelemetryFrame(spans=[spb.SpanEvent(
                kind=spb.SpanEvent.KIND_START,
                trace_id=trace_id,
                span_id=span_id,
                parent_span_id=parent_span_id,
                fn=fn,
                started_at=started_at,
                input=input_buf,
                attempt=attempt,
                span_type=span_type,
                target_service=target_service,
            )]),
        )):
            return

        with self._queue_lock:
            online = self._is_online

        if not online:
            self._enqueue_offline({
                "kind": "reportCallStart",
                "trace_id": trace_id, "span_id": span_id,
                "parent_span_id": parent_span_id,
                "fn": fn, "started_at": started_at,
                "input": input_buf, "attempt": attempt,
                "target_service": target_service,
            })
            return

        try:
            self._stub.ReportCallStart(
                pb.ReportCallStartRequest(
                    trace_id=trace_id, span_id=span_id,
                    parent_span_id=parent_span_id, fn=fn,
                    service_name=self._service_name, instance_id=self._instance_id,
                    started_at=started_at, input=input_buf, attempt=attempt,
                ),
                metadata=self._metadata(), timeout=5,
            )
        except Exception as exc:
            if _is_connection_error(exc):
                self._mark_offline()
                self._enqueue_offline({
                    "kind": "reportCallStart",
                    "trace_id": trace_id,
                    "span_id": span_id,
                    "parent_span_id": parent_span_id,
                    "fn": fn,
                    "started_at": started_at,
                    "input": input_buf,
                    "attempt": attempt,
                    "target_service": target_service,
                })

    def _report_call(
        self, trace_id, span_id, fn, started_at, input_buf, success, attempt, output_buf, error_msg,
        span_type: str = "rpc",
        target_service: str = "",
    ) -> None:
        if self._stub is None:
            return
        duration_ms = (time.time_ns() - started_at) // 1_000_000

        if self._session_send(spb.Envelope(
            timestamp_ms=int(time.time() * 1000),
            telemetry=spb.TelemetryFrame(spans=[spb.SpanEvent(
                kind=spb.SpanEvent.KIND_FINISH,
                trace_id=trace_id,
                span_id=span_id,
                fn=fn,
                started_at=started_at,
                duration_ms=duration_ms,
                success=success,
                error=error_msg or "",
                input=input_buf,
                output=output_buf or b"",
                attempt=attempt,
                span_type=span_type,
                target_service=target_service,
            )]),
        )):
            return

        with self._queue_lock:
            online = self._is_online

        if not online:
            self._enqueue_offline({
                "kind": "reportCall",
                "trace_id": trace_id, "span_id": span_id, "fn": fn,
                "duration_ms": duration_ms,
                "started_at": started_at, "input": input_buf, "success": success,
                "attempt": attempt, "output": output_buf or b"", "error": error_msg,
                "target_service": target_service,
            })
            return

        try:
            self._stub.ReportCall(
                pb.ReportCallRequest(
                    trace_id=trace_id, span_id=span_id, fn=fn,
                    service_name=self._service_name, instance_id=self._instance_id,
                    started_at=started_at, duration_ms=duration_ms,
                    success=success, error=error_msg,
                    input=input_buf, output=output_buf or b"", attempt=attempt,
                ),
                metadata=self._metadata(), timeout=5,
            )
        except Exception as exc:
            if _is_connection_error(exc):
                self._mark_offline()
                self._enqueue_offline({
                    "kind": "reportCall",
                    "trace_id": trace_id,
                    "span_id": span_id,
                    "fn": fn,
                    "duration_ms": duration_ms,
                    "started_at": started_at,
                    "input": input_buf,
                    "success": success,
                    "attempt": attempt,
                    "output": output_buf or b"",
                    "error": error_msg,
                    "target_service": target_service,
                })

    # ─── Schema encode/decode helpers ─────────────────────────────────────────

    def _encode_payload(self, fn: str, payload: Any) -> bytes:
        """Encode payload to bytes. Uses binary Protobuf if a schema is registered."""
        opts = self._schema_opts_for_fn(fn)
        if opts and opts.input:
            try:
                from service_bridge.schema import compile_schema, encode_with_schema
                md = self._get_compiled_schema(fn, "input", opts.input)
                if md:
                    return encode_with_schema(md, payload)
            except Exception as exc:
                _sdk_logger.debug("schema encode failed for %r: %s — falling back to JSON", fn, exc)
        return json.dumps(payload).encode() if payload is not None else b"{}"

    def _decode_output(self, fn: str, raw: bytes) -> Any:
        """Decode output bytes. Uses binary Protobuf if a schema is registered."""
        if not raw:
            return {}
        opts = self._schema_opts_for_fn(fn)
        if opts and opts.output:
            try:
                from service_bridge.schema import decode_with_schema
                md = self._get_compiled_schema(fn, "output", opts.output)
                if md:
                    return decode_with_schema(md, raw)
            except Exception as exc:
                _sdk_logger.debug("schema decode failed for %r: %s — falling back to JSON", fn, exc)
        try:
            return json.loads(raw)
        except json.JSONDecodeError:
            return {}

    def _get_compiled_schema(self, fn: str, direction: str, schema: RpcSchema) -> Any:
        key = f"{fn}:{direction}"
        if key not in self._compiled_schemas:
            try:
                from service_bridge.schema import compile_schema
                self._compiled_schemas[key] = compile_schema(f"{fn}_{direction}", schema)
            except Exception:
                self._compiled_schemas[key] = None
        return self._compiled_schemas[key]

    # ─── Internal helpers ──────────────────────────────────────────────────────

    def _metadata(self) -> list[tuple[str, str]]:
        return [("x-service-key", self._service_key)]

    def _schema_opts_for_fn(self, fn: str) -> Optional[RpcSchemaOpts]:
        return self._schemas.get(fn)

    def _worker_channel_credentials(self) -> Optional[grpc.ChannelCredentials]:
        creds = self._worker_tls_creds
        if creds is None:
            return None
        return grpc.ssl_channel_credentials(
            root_certificates=creds.ca_cert,
            private_key=creds.client_key,
            certificate_chain=creds.client_cert,
        )

    def _make_worker_channel(self, target: str) -> grpc.Channel:
        options = [("grpc.service_config", '{"loadBalancingConfig":[{"round_robin":{}}]}')]
        if self._worker_tls_server_name:
            options.append(("grpc.ssl_target_name_override", self._worker_tls_server_name))
            options.append(("grpc.default_authority", self._worker_tls_server_name))
        creds = self._worker_channel_credentials()
        if creds is None:
            raise RuntimeError("worker TLS is not configured")
        return grpc.secure_channel(target, creds, options=options)

    def _resolve_lookup_endpoints(self, raw_endpoints: Any) -> list[str]:
        endpoints: list[str] = []
        seen: set[str] = set()
        for ep in raw_endpoints:
            endpoint = (getattr(ep, "endpoint", "") or "").strip()
            if endpoint:
                endpoint = endpoint.replace("grpc://", "").replace("tls://", "")
                if endpoint in seen:
                    continue
                seen.add(endpoint)
                endpoints.append(endpoint)
        return endpoints

    async def _connect_control_plane(self) -> None:
        ca_pem = _resolve_ca_pem(self._service_key, self._opts.ca_cert)
        channel_creds = grpc.ssl_channel_credentials(root_certificates=ca_pem)
        self._channel = grpc.secure_channel(self._grpc_url, channel_creds)
        self._stub = pb_grpc.ServiceBridgeStub(self._channel)
        self._session_stub = spb_grpc.SessionServiceStub(self._channel)
        self._cp_stub = cpb_grpc.ControlPlaneStub(self._channel)
        # Start the event-driven channel watch now that the channel exists.
        self._ensure_channel_watch()

    def _provision_worker_tls(self, extra_ips: Optional[list[str]] = None) -> _TLSMaterials:
        if self._stub is None:
            raise RuntimeError("control plane is not connected")
        try:
            from cryptography.hazmat.primitives import serialization
            from cryptography.hazmat.primitives.asymmetric import ec
        except ImportError as exc:
            raise RuntimeError(
                "Install 'cryptography' package for mTLS worker provisioning"
            ) from exc

        key = ec.generate_private_key(ec.SECP256R1())
        private_key_pem = key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.PKCS8,
            encryption_algorithm=serialization.NoEncryption(),
        )
        public_key_pem = key.public_key().public_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PublicFormat.SubjectPublicKeyInfo,
        )
        resp = self._stub.ProvisionWorkerCertificate(
            pb.ProvisionWorkerCertificateRequest(
                service_name=self._service_name,
                public_key_pem=public_key_pem.decode(),
                extra_ips=extra_ips or [],
            ),
            metadata=self._metadata(),
            timeout=15,
        )
        if not resp.cert_pem or not resp.ca_pem:
            raise RuntimeError("ProvisionWorkerCertificate returned empty cert/ca payload")
        return _TLSMaterials(
            ca_cert=_validate_ca_pem(
                resp.ca_pem.encode(),
                error_message="invalid worker provisioned ca pem",
            ),
            client_cert=resp.cert_pem.encode(),
            client_key=private_key_pem,
        )

    def _resolve_explicit_worker_tls(
        self, worker_tls: Optional[WorkerTLSOpts]
    ) -> Optional[tuple[_TLSMaterials, str]]:
        if worker_tls is None:
            return None
        cert_pem = worker_tls.cert.strip()
        key_pem = worker_tls.key.strip()
        if bool(cert_pem) != bool(key_pem):
            raise RuntimeError("worker_tls.cert and worker_tls.key must be provided together")
        if not cert_pem:
            return None
        ca_pem = (
            _validate_ca_pem(
                worker_tls.ca_cert.encode(),
                error_message="invalid worker tls ca cert: malformed pem",
            )
            if worker_tls.ca_cert.strip()
            else _resolve_ca_pem(self._service_key, self._opts.ca_cert)
        )
        cert_bytes = cert_pem.encode()
        key_bytes = key_pem.encode()
        _validate_worker_cert_and_key(cert_bytes, key_bytes)
        return (
            _TLSMaterials(
                ca_cert=ca_pem,
                client_cert=cert_bytes,
                client_key=key_bytes,
            ),
            worker_tls.server_name.strip(),
        )

    async def _start_worker_server(self, host: str) -> str:
        from service_bridge.worker import WorkerServicer

        bind_host = host or "localhost"

        advertise_host = bind_host
        try:
            import ipaddress
            if ipaddress.ip_address(bind_host).is_unspecified:
                detected = _outbound_ip()
                if detected:
                    advertise_host = detected
        except ValueError:
            pass

        if self._worker_tls_creds is None:
            extra_ips = [advertise_host] if advertise_host != bind_host else None
            self._worker_tls_creds = self._provision_worker_tls(extra_ips=extra_ips)

        server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
        self._worker_servicer = WorkerServicer(self)
        pb_grpc.add_ServiceBridgeWorkerServicer_to_server(self._worker_servicer, server)
        server_creds = grpc.ssl_server_credentials(
            private_key_certificate_chain_pairs=[(
                self._worker_tls_creds.client_key,
                self._worker_tls_creds.client_cert,
            )],
            root_certificates=self._worker_tls_creds.ca_cert,
            require_client_auth=True,
        )
        port = server.add_secure_port(f"{bind_host}:0", server_creds)
        if port <= 0:
            raise RuntimeError("failed to bind worker gRPC server")
        server.start()
        self._worker_server = server
        return f"{advertise_host}:{port}"

    async def _sync_registrations(self) -> None:
        if self._stub is None:
            return
        md = self._metadata()
        fn_names = []
        group_names = []

        # Build declarations for Reconcile
        fn_decls = []
        for fn, _handler in self._rpc_handlers.items():
            opts = self._rpc_opts.get(fn, {})
            schema_opts = opts.get("schema")
            input_schema_json = ""
            output_schema_json = ""
            if schema_opts and schema_opts.input:
                input_schema_json = json.dumps({
                    k: {"type": v.type, "id": v.id, "repeated": v.repeated}
                    for k, v in schema_opts.input.items()
                })
            if schema_opts and schema_opts.output:
                output_schema_json = json.dumps({
                    k: {"type": v.type, "id": v.id, "repeated": v.repeated}
                    for k, v in schema_opts.output.items()
                })
            fn_decls.append(tpb.FunctionDeclaration(
                fn_name=fn,
                weight=max(1, int(self._registration_weight)),
                input_schema_json=input_schema_json,
                output_schema_json=output_schema_json,
            ))
            fn_names.append(fn)

        cg_decls = []
        for gname, entry in self._event_handlers.items():
            cg_decls.append(tpb.ConsumerGroupDeclaration(
                name=gname,
                pattern=entry["topic"],
                mode="shared",
                retry_policy_json=entry["retry_policy_json"],
                active=True,
                filter_expr=entry["filter_expr"],
            ))
            group_names.append(gname)

        cpu_pct, ram_mb = _get_process_metrics()

        if self._cp_stub is not None:
            # v2: ControlPlane.Reconcile — одна декларативная регистрация
            metrics = tpb.ResourceMetrics()
            if cpu_pct > 0:
                metrics.cpu_percent = cpu_pct
            if ram_mb > 0:
                metrics.ram_mb = ram_mb
            try:
                self._cp_stub.Reconcile(
                    cpb.ReconcileRequest(
                        identity=tpb.WorkerIdentity(
                            service_name=self._service_name,
                            instance_id=self._instance_id,
                            endpoint=self._worker_address,
                            transport="tls",
                        ),
                        functions=fn_decls,
                        consumer_groups=cg_decls,
                        metrics=metrics,
                        transport_mode=tpb.TRANSPORT_MODE_DIRECT,
                        zone="",
                    ),
                    metadata=md,
                    timeout=15,
                )
                logger.debug("Reconcile OK: %d functions, %d groups", len(fn_names), len(group_names))
            except Exception as exc:
                logger.warning(
                    "Reconcile failed: %s — falling back to individual registration",
                    _friendly_grpc_message(exc),
                )
                self._sync_registrations_legacy(md, fn_decls, cg_decls, fn_names, group_names)
        else:
            self._sync_registrations_legacy(md, fn_decls, cg_decls, fn_names, group_names)

        # Send WorkerStatusFrame via session stream (or fall back to legacy Heartbeat RPC).
        status_frame = spb.WorkerStatusFrame(
            endpoint=self._worker_address,
            group_names=group_names,
            function_names=fn_names,
        )
        if cpu_pct > 0:
            status_frame.cpu_percent = cpu_pct
        if ram_mb > 0:
            status_frame.ram_mb = ram_mb

        if self._session_send(spb.Envelope(
            timestamp_ms=int(time.time() * 1000),
            worker_status=status_frame,
        )):
            return

        hb_req = pb.HeartbeatRequest(
            service_name=self._service_name,
            instance_id=self._instance_id,
            endpoint=self._worker_address,
            group_names=group_names,
            function_names=fn_names,
            transport="tls",
        )
        if cpu_pct > 0:
            hb_req.cpu_percent = cpu_pct
        if ram_mb > 0:
            hb_req.ram_mb = ram_mb

        try:
            self._stub.Heartbeat(hb_req, metadata=md, timeout=15)
        except Exception as exc:
            if isinstance(exc, grpc.RpcError):
                code = exc.code()  # type: ignore[attr-defined]
                if code in (grpc.StatusCode.NOT_FOUND, grpc.StatusCode.FAILED_PRECONDITION):
                    logger.warning("heartbeat: re-registration required — will re-sync on next cycle")
                    return
            if _is_connection_error(exc):
                self._mark_offline()
            logger.warning("heartbeat failed: %s — retrying", _friendly_grpc_message(exc))
            return

        try:
            self._stub.HeartbeatHttpEndpoint(
                pb.HeartbeatHttpEndpointRequest(
                    service_name=self._service_name,
                    instance_id=self._instance_id,
                ),
                metadata=md,
                timeout=15,
            )
        except Exception as exc:
            logger.warning("HTTP heartbeat failed: %s", _friendly_grpc_message(exc))

    def _sync_registrations_legacy(
        self,
        md: list,
        fn_decls: list,
        cg_decls: list,
        fn_names: list,
        group_names: list,
    ) -> None:
        """Legacy per-RPC registration fallback when ControlPlane.Reconcile is unavailable."""
        for fn_decl in fn_decls:
            try:
                self._stub.RegisterFunction(
                    pb.RegisterFunctionRequest(
                        fn_name=fn_decl.fn_name,
                        service_name=self._service_name,
                        instance_id=self._instance_id,
                        endpoint=self._worker_address,
                        transport="tls",
                        weight=fn_decl.weight,
                        input_schema_json=fn_decl.input_schema_json,
                        output_schema_json=fn_decl.output_schema_json,
                    ),
                    metadata=md,
                    timeout=15,
                )
            except Exception as exc:
                logger.warning(
                    "RegisterFunction '%s': %s — will retry on next cycle",
                    fn_decl.fn_name, _friendly_grpc_message(exc),
                )

        for cg_decl, gname in zip(cg_decls, group_names):
            entry = self._event_handlers.get(gname, {})
            try:
                self._stub.RegisterConsumerGroup(
                    pb.RegisterConsumerGroupRequest(
                        name=cg_decl.name,
                        pattern=cg_decl.pattern,
                        mode=cg_decl.mode or "shared",
                        retry_policy_json=cg_decl.retry_policy_json,
                        active=cg_decl.active,
                        filter_expr=cg_decl.filter_expr,
                    ),
                    metadata=md,
                    timeout=15,
                )
                self._stub.RegisterGroupMember(
                    pb.RegisterGroupMemberRequest(
                        group_name=gname,
                        service_name=self._service_name,
                        instance_id=self._instance_id,
                        endpoint=self._worker_address,
                        transport="tls",
                        weight=max(1, int(self._registration_weight)),
                    ),
                    metadata=md,
                    timeout=15,
                )
            except Exception as exc:
                logger.warning(
                    "RegisterConsumerGroup '%s': %s — will retry on next cycle",
                    gname, _friendly_grpc_message(exc),
                )

    def _start_worker_session(self) -> None:
        if self._session_thread and self._session_thread.is_alive():
            return
        ready = threading.Event()
        ready_state: dict[str, Any] = {"error": None}
        self._session_thread = threading.Thread(
            target=self._worker_session_loop,
            args=(ready, ready_state),
            daemon=True,
        )
        self._session_thread.start()
        if not ready.wait(timeout=15.0):
            raise RuntimeError("OpenWorkerSession initial connect timed out")
        if ready_state["error"] is not None:
            raise RuntimeError(
                f"OpenWorkerSession initial connect failed: {ready_state['error']}"
            )

    def _worker_session_loop(
        self,
        ready: Optional[threading.Event] = None,
        ready_state: Optional[dict[str, Any]] = None,
    ) -> None:
        backoff = 1.0
        ready_reported = False

        v2cfg = V2Config(
            server_address=self._grpc_url,
            service_name=self._service_name,
            instance_id=self._instance_id,
            max_inflight=self._session_max_in_flight,
            min_inflight=1,
        )
        v2client = V2SessionClient(v2cfg)

        while not self._stop_event.is_set():
            if self._session_stub is None:
                if self._stop_event.wait(backoff):
                    return
                backoff = min(backoff * 2, 15.0)
                continue

            q: deque[Any] = deque()
            q_cond = threading.Condition()

            def _enqueue(frame: Any) -> None:
                with q_cond:
                    q.append(frame)
                    q_cond.notify()

            def _request_iter():
                while not self._stop_event.is_set():
                    with q_cond:
                        while not q and not self._stop_event.is_set():
                            q_cond.wait(timeout=0.5)
                        if not q:
                            continue
                        item = q.popleft()
                    if item is None:
                        return
                    yield item

            try:
                stream = self._session_stub.Channel(_request_iter(), metadata=self._metadata())
                if not ready_reported and ready is not None:
                    if ready_state is not None:
                        ready_state["error"] = None
                    ready.set()
                    ready_reported = True

                # Send Hello v2 with all fields from V2SessionClient
                hello_fields = v2client.get_hello_fields()
                identity = hello_fields["identity"]
                _enqueue(spb.Envelope(
                    seq=0,
                    timestamp_ms=int(time.time() * 1000),
                    hello=spb.Hello(
                        identity=tpb.WorkerIdentity(
                            service_name=identity["service_name"],
                            instance_id=identity["instance_id"],
                            endpoint=self._worker_address,
                            transport="tls",
                        ),
                        max_inflight=hello_fields["max_inflight"],
                        min_inflight=hello_fields["min_inflight"],
                        resume_token=hello_fields["resume_token"],
                        epoch=hello_fields["epoch"],
                        last_received_seq=hello_fields["last_received_seq"],
                        last_sent_seq=hello_fields["last_sent_seq"],
                        completed_command_ids=hello_fields["completed_command_ids"],
                        sdk_version=hello_fields["sdk_version"],
                        capabilities=hello_fields["capabilities"],
                        zone=hello_fields["zone"],
                        transport_mode=tpb.TRANSPORT_MODE_DIRECT,
                    ),
                ))

                aux_stop = threading.Event()
                _seq_counter = [0]

                def _next_seq() -> int:
                    _seq_counter[0] += 1
                    return _seq_counter[0]

                def _ping_loop() -> None:
                    while not aux_stop.wait(10.0):
                        _enqueue(spb.Envelope(
                            seq=_next_seq(),
                            timestamp_ms=int(time.time() * 1000),
                            ping=spb.PingFrame(timestamp_ms=int(time.time() * 1000)),
                        ))

                def _position_update_loop() -> None:
                    while not aux_stop.wait(5.0):
                        _enqueue(spb.Envelope(
                            seq=_next_seq(),
                            timestamp_ms=int(time.time() * 1000),
                            position_update=spb.PositionUpdate(
                                last_received_seq=v2client.position.last_received_seq,
                            ),
                        ))

                ping_thread = threading.Thread(target=_ping_loop, daemon=True)
                position_thread = threading.Thread(target=_position_update_loop, daemon=True)
                ping_thread.start()
                position_thread.start()
                backoff = 1.0

                # Expose the enqueue function so telemetry/heartbeat/stream
                # can route messages through the session stream.
                self._set_session_enqueue(_enqueue)

                for resp in stream:
                    if self._stop_event.is_set():
                        break
                    payload_kind = resp.WhichOneof("payload")

                    if payload_kind == "dispatch":
                        cmd = resp.dispatch
                        v2client.position.record_received(resp.seq)
                        result = self._process_dispatch_command(cmd)
                        v2client.position.mark_completed(cmd.command_id)
                        v2client.position.record_sent(_next_seq())
                        result_seq = _next_seq()
                        result.result_seq = result_seq
                        _enqueue(spb.Envelope(
                            seq=result_seq,
                            ack_seq=resp.seq,
                            timestamp_ms=int(time.time() * 1000),
                            command_result=result,
                        ))
                        _enqueue(spb.Envelope(
                            seq=_next_seq(),
                            timestamp_ms=int(time.time() * 1000),
                            permit_release=spb.PermitRelease(released=1),
                        ))

                    elif payload_kind == "hello_ack":
                        ack_pb = resp.hello_ack
                        ack = HelloAckV2(
                            session_id=ack_pb.session_id,
                            resume_token=ack_pb.resume_token,
                            epoch=ack_pb.epoch,
                            resumed=ack_pb.resumed,
                            resume_from_seq=ack_pb.resume_from_seq,
                            replayed_commands=ack_pb.replayed_commands,
                            reconciled_results=ack_pb.reconciled_results,
                            heartbeat_interval_ms=ack_pb.heartbeat_interval_ms or 10000,
                            heartbeat_timeout_ms=ack_pb.heartbeat_timeout_ms or 30000,
                            initial_permits=ack_pb.initial_permits or self._session_max_in_flight,
                            max_permits=ack_pb.max_permits or self._session_max_in_flight,
                            effective_transport_mode=V2TransportMode.DIRECT,
                        )
                        import asyncio as _aio
                        _aio.run(v2client.on_hello_ack(ack))

                    elif payload_kind == "ping":
                        _enqueue(spb.Envelope(
                            seq=_next_seq(),
                            timestamp_ms=int(time.time() * 1000),
                            pong=spb.PongFrame(
                                echo_timestamp_ms=resp.ping.timestamp_ms,
                                server_timestamp_ms=int(time.time() * 1000),
                            ),
                        ))

                    elif payload_kind == "pong":
                        rtt_ms = int(time.time() * 1000) - resp.pong.echo_timestamp_ms
                        v2client.on_pong(max(0.0, float(rtt_ms)))

                    elif payload_kind == "goaway":
                        gw = resp.goaway
                        v2client.on_goaway(str(gw.code), gw.reason)
                        logger.info("session: goaway code=%s reason=%s", gw.code, gw.reason)
                        break

                    elif payload_kind == "config_push":
                        logger.debug("session: config_push received")

                    elif payload_kind == "permit_grant":
                        pg = resp.permit_grant
                        logger.debug("session: permit_grant additional=%d", pg.additional_permits)

                    elif payload_kind == "drain":
                        dr = resp.drain
                        v2client.on_drain(dr.reason, dr.deadline_ms)

                    elif payload_kind == "flow_update":
                        fu = resp.flow_update
                        logger.debug("session: flow_update window=%d reason=%s", fu.new_window_size, fu.reason)

                aux_stop.set()
                ping_thread.join(timeout=1)
                position_thread.join(timeout=1)
                self._set_session_enqueue(None)
                _enqueue(None)
            except Exception as exc:
                self._set_session_enqueue(None)
                if not ready_reported and ready is not None:
                    if ready_state is not None:
                        ready_state["error"] = exc
                    ready.set()
                    return
                logger.warning("worker session closed: %s — reconnecting", _friendly_grpc_message(exc))

            if self._stop_event.wait(backoff):
                return
            backoff = min(backoff * 2, 15.0)

    def _process_dispatch_command(self, cmd: "spb.DispatchCommand") -> "spb.CommandResult":
        """Process a v2 DispatchCommand (RpcTask or EventTask) and return CommandResult."""
        result = spb.CommandResult(command_id=cmd.command_id)
        if cmd.deadline_unix_ms and int(time.time() * 1000) > cmd.deadline_unix_ms:
            result.error = "command deadline exceeded"
            return result
        if self._worker_servicer is None:
            result.error = "worker servicer is not initialized"
            return result

        task_kind = cmd.WhichOneof("task")
        if task_kind == "rpc":
            rpc_task = cmd.rpc
            try:
                rpc_resp = self._worker_servicer.Handle(
                    pb.HandleRequest(
                        fn=rpc_task.fn,
                        payload=rpc_task.payload,
                        trace_id=rpc_task.trace_id,
                        span_id=rpc_task.span_id,
                    ),
                    _InProcessWorkerContext(rpc_task.trace_id),
                )
                result.success = rpc_resp.success
                result.output = rpc_resp.output
                result.error = rpc_resp.error
                return result
            except Exception as exc:
                result.error = str(exc)[:2048]
                return result

        if task_kind == "event":
            event_task = cmd.event
            try:
                ack = self._dispatch_event(
                    group_name=event_task.group_name,
                    topic=event_task.topic,
                    message_id=event_task.message_id,
                    attempt=event_task.attempt,
                    payload=event_task.payload,
                    headers=dict(event_task.headers),
                    trace_id=event_task.trace_id,
                    parent_span_id=event_task.parent_span_id,
                    stream_trace_id=event_task.trace_id,
                )
                result.success = ack.ack
                result.ack = ack.ack
                result.retry_after_ms = ack.retry_after_ms
                result.reject_reason = ack.reject_reason
                result.error = ack.error
                return result
            except Exception as exc:
                result.error = str(exc)[:2048]
                return result

        result.error = f"unsupported task kind: {task_kind}"
        return result

    def _process_worker_session_command(
        self, command: pb.WorkerSessionCommand
    ) -> pb.WorkerSessionCommandResult:
        result = pb.WorkerSessionCommandResult(command_id=command.command_id)
        if command.deadline_unix_ms and int(time.time() * 1000) > command.deadline_unix_ms:
            result.error = "command deadline exceeded"
            return result
        if self._worker_servicer is None:
            result.error = "worker servicer is not initialized"
            return result

        if command.kind == pb.WorkerCommandKind.WORKER_COMMAND_KIND_RPC:
            rpc_cmd = command.rpc
            try:
                rpc_resp = self._worker_servicer.Handle(
                    pb.HandleRequest(
                        fn=rpc_cmd.fn,
                        payload=rpc_cmd.payload,
                        trace_id=rpc_cmd.trace_id,
                        span_id=rpc_cmd.span_id,
                    ),
                    _InProcessWorkerContext(rpc_cmd.trace_id),
                )
                result.success = rpc_resp.success
                result.output = rpc_resp.output
                result.error = rpc_resp.error
                return result
            except Exception as exc:
                result.error = str(exc)[:2048]
                return result

        if command.kind == pb.WorkerCommandKind.WORKER_COMMAND_KIND_EVENT:
            event_cmd = command.event
            try:
                ack = self._dispatch_event_command(event_cmd)
                result.success = ack.ack
                result.ack = ack.ack
                result.retry_after_ms = ack.retry_after_ms
                result.reject_reason = ack.reject_reason
                result.error = ack.error
                return result
            except Exception as exc:
                result.error = str(exc)[:2048]
                return result

        result.error = f"unsupported command kind: {command.kind}"
        return result

    def _dispatch_event_command(self, event_cmd: pb.WorkerEventCommand) -> pb.DeliveryAck:
        return self._dispatch_event(
            group_name=event_cmd.group_name,
            topic=event_cmd.topic,
            message_id=event_cmd.message_id,
            attempt=event_cmd.attempt,
            payload=event_cmd.payload,
            headers=dict(event_cmd.headers),
            trace_id=event_cmd.trace_id,
            parent_span_id=event_cmd.parent_span_id,
        stream_trace_id=event_cmd.trace_id,
    )

    def _dispatch_event(
        self,
        *,
        group_name: str,
        topic: str,
        message_id: str,
        attempt: int,
        payload: bytes,
        headers: dict[str, str],
        trace_id: str,
        parent_span_id: str,
        stream_trace_id: str,
    ) -> pb.DeliveryAck:
        entry = self._event_handlers.get(group_name)
        if entry is None:
            return pb.DeliveryAck(ack=False, error="consumer_group_handler_missing")

        raw_payload = payload if payload else b"{}"
        try:
            parsed_payload = json.loads(raw_payload)
        except json.JSONDecodeError:
            return pb.DeliveryAck(ack=False, reject_reason="invalid_json_payload")

        should_retry = False
        retry_after_ms = 1000
        reject_reason = ""

        def retry_fn(delay_ms: int = 1000) -> None:
            nonlocal should_retry, retry_after_ms
            should_retry = True
            retry_after_ms = delay_ms

        def reject_fn(reason: str) -> None:
            nonlocal reject_reason
            reject_reason = reason

        stream_writer = StreamWriter(trace_id=stream_trace_id, client=self)
        ec = EventContext(
            trace_id=trace_id,
            span_id=parent_span_id,
            topic=topic,
            group_name=group_name,
            message_id=message_id,
            attempt=attempt,
            headers=headers,
            stream=stream_writer,
            _retry_fn=retry_fn,
            _reject_fn=reject_fn,
        )

        handler = entry["handler"]
        handler_err = ""
        try:
            loop = asyncio.new_event_loop()
            try:
                if asyncio.iscoroutinefunction(handler):
                    loop.run_until_complete(handler(parsed_payload, ec))
                else:
                    handler(parsed_payload, ec)
            finally:
                loop.close()
        except Exception as exc:
            handler_err = str(exc)[:2048]
            should_retry = True

        if reject_reason:
            return pb.DeliveryAck(ack=False, reject_reason=reject_reason, error=handler_err)
        if should_retry:
            return pb.DeliveryAck(ack=False, retry_after_ms=retry_after_ms, error=handler_err)
        return pb.DeliveryAck(ack=True)

    def _start_heartbeat(self) -> None:
        interval_ms = max(1000, int(self._opts.heartbeat_interval_ms or 10_000))

        def _loop() -> None:
            while not self._stop_event.is_set():
                # ±20% jitter on the heartbeat interval.
                jitter = random.uniform(-0.2, 0.2)
                delay = (interval_ms / 1000) * (1.0 + jitter)
                if self._stop_event.wait(delay):
                    return
                try:
                    asyncio.run(self._sync_registrations())
                except Exception as exc:
                    logger.warning("heartbeat failed: %s — retrying", _friendly_grpc_message(exc))
                    if _is_connection_error(exc):
                        self._mark_offline()

        self._heartbeat_thread = threading.Thread(target=_loop, daemon=True)
        self._heartbeat_thread.start()

    def _log_push(self, level: str, msg: str, attrs: dict[str, str]) -> None:
        """Internal: enqueue a log entry for batched delivery."""
        if not hasattr(self, "_log_batch"):
            self._log_batch = []
            self._log_lock = threading.Lock()
            self._log_timer = None

        tc = _trace_ctx.get({})
        entry = pb.LogEntry(
            service_name=self._service_name,
            instance_id=self._instance_id,
            level=level,
            message=msg,
            timestamp_ns=time.time_ns(),
            attributes=attrs,
            trace_id=tc.get("trace_id", ""),
            span_id=tc.get("span_id", ""),
        )
        flush_now = False
        with self._log_lock:
            self._log_batch.append(entry)
            if len(self._log_batch) >= 100:
                flush_now = True
            elif self._log_timer is None:
                t = threading.Timer(0.5, self._log_flush)
                t.daemon = True
                self._log_timer = t
                t.start()
        if flush_now:
            self._log_flush()

    def _log_flush(self) -> None:
        with self._log_lock:
            if not self._log_batch:
                return
            entries = list(self._log_batch)
            self._log_batch.clear()
            if self._log_timer is not None:
                self._log_timer.cancel()
                self._log_timer = None
        stub = self._stub
        if stub is None:
            return
        session_log_entries = [
            spb.LogEntry(
                trace_id=e.trace_id,
                span_id=e.span_id,
                service_name=e.service_name,
                level=e.level,
                message=e.message,
                timestamp_ns=e.timestamp_ns,
                attributes=dict(e.attributes),
                instance_id=e.instance_id,
            )
            for e in entries
        ]
        if self._session_send(spb.Envelope(
            timestamp_ms=int(time.time() * 1000),
            telemetry=spb.TelemetryFrame(logs=session_log_entries),
        )):
            return
        try:
            stub.ReportLog(
                pb.ReportLogRequest(entries=entries),
                metadata=self._metadata(),
                timeout=5,
            )
        except Exception as exc:
            _sdk_logger.debug("Log flush failed: %s", exc)

    def attach_logging_handler(
        self,
        logger_name: str = "",
        level: int = logging.NOTSET,
    ) -> "ServiceBridgeLogHandler":
        """Attach a :class:`ServiceBridgeLogHandler` to a Python standard logger.

        All records emitted by that logger (and its children) are forwarded to
        ServiceBridge automatically.  Original handlers are **not** removed.

        Args:
            logger_name: Name of the logger (empty = root logger = capture all).
            level: Minimum log level to forward (default: NOTSET = everything).

        Returns:
            The installed :class:`ServiceBridgeLogHandler` instance.
        """
        handler = ServiceBridgeLogHandler(self, level=level)
        logging.getLogger(logger_name).addHandler(handler)
        return handler


# ─── Process metrics ──────────────────────────────────────────────────────────

def _get_process_metrics() -> tuple[float, int]:
    """Return (cpu_percent, ram_mb). Best-effort; zero if unavailable."""
    ram_mb = 0
    cpu_pct = 0.0
    try:
        import resource
        usage = resource.getrusage(resource.RUSAGE_SELF)
        # ru_maxrss is in bytes on Linux, KB on macOS
        import sys
        if sys.platform == "darwin":
            ram_mb = int(usage.ru_maxrss / (1024 * 1024))
        else:
            ram_mb = int(usage.ru_maxrss / 1024)
    except Exception:
        pass
    return cpu_pct, ram_mb


# ─── Async gRPC stream helper ─────────────────────────────────────────────────

async def _aiter_grpc_stream(
    stream_call: Any,
    loop: asyncio.AbstractEventLoop,
    *,
    queue_limit: int = 256,
    overflow_error_factory: Optional[Callable[[], Exception]] = None,
) -> AsyncIterator:
    """Wrap a synchronous gRPC server-streaming call as an async generator.

    Runs the blocking iteration in a background thread and communicates via
    an asyncio.Queue with a 256-item backpressure limit.
    """
    q: asyncio.Queue = asyncio.Queue(maxsize=max(1, queue_limit) + 2)
    _sentinel = object()
    overflowed = threading.Event()

    def _overflow_error() -> Exception:
        if overflow_error_factory is not None:
            return overflow_error_factory()
        return RuntimeError("grpc stream queue overflow")

    def _emit(item: Any) -> None:
        if item is _sentinel:
            if q.full():
                try:
                    q.get_nowait()
                except Exception:
                    pass
            q.put_nowait(_sentinel)
            return
        if overflowed.is_set():
            return
        if not isinstance(item, Exception) and q.qsize() >= max(1, queue_limit):
            overflowed.set()
            try:
                q.put_nowait(_overflow_error())
            except Exception:
                pass
            try:
                q.put_nowait(_sentinel)
            except Exception:
                pass
            try:
                stream_call.cancel()
            except Exception:
                pass
            return
        try:
            q.put_nowait(item)
        except asyncio.QueueFull:
            overflowed.set()
            try:
                q.put_nowait(_overflow_error())
            except Exception:
                pass
            try:
                q.put_nowait(_sentinel)
            except Exception:
                pass

    def _reader() -> None:
        try:
            for chunk in stream_call:
                loop.call_soon_threadsafe(_emit, chunk)
        except Exception as exc:
            loop.call_soon_threadsafe(_emit, exc)
        finally:
            loop.call_soon_threadsafe(_emit, _sentinel)

    threading.Thread(target=_reader, daemon=True).start()

    while True:
        item = await q.get()
        if item is _sentinel:
            return
        if isinstance(item, Exception):
            raise item
        yield item


# ─── Workflow helpers ─────────────────────────────────────────────────────────

def _workflow_opts_json(opts: "Optional[WorkflowOpts]") -> str:
    """Serialize WorkflowOpts to a JSON string, omitting zero-value fields."""
    if opts is None:
        return ""
    d: dict[str, Any] = {}
    if opts.state_limit_bytes:
        d["stateLimitBytes"] = opts.state_limit_bytes
    if opts.step_timeout_ms:
        d["stepTimeoutMs"] = opts.step_timeout_ms
    return json.dumps(d) if d else ""


def _step_to_dict(s: WorkflowStep) -> dict[str, Any]:
    d: dict[str, Any] = {"id": s.id, "type": s.type}
    if s.ref:
        d["ref"] = s.ref
    if s.service:
        d["service"] = s.service
    if s.deps:
        d["deps"] = s.deps
    if s.if_expr:
        d["if"] = s.if_expr
    if s.timeout_ms:
        d["timeoutMs"] = s.timeout_ms
    if s.duration_ms:
        d["durationMs"] = s.duration_ms
    return d


# ─── Standard logging handler ─────────────────────────────────────────────────

_LEVEL_MAP: dict[int, str] = {
    logging.DEBUG: "DEBUG",
    logging.INFO: "INFO",
    logging.WARNING: "WARN",
    logging.ERROR: "ERROR",
    logging.CRITICAL: "ERROR",
}


class ServiceBridgeLogHandler(logging.Handler):
    """A :class:`logging.Handler` that ships Python log records to ServiceBridge.

    Records are batched and flushed every 500ms or when 100 records accumulate.
    Original handlers are preserved — logs still appear on the console.
    """

    def __init__(self, svc: "ServiceBridge", level: int = logging.NOTSET) -> None:
        super().__init__(level)
        self._svc = svc

    def emit(self, record: logging.LogRecord) -> None:
        try:
            msg = self.format(record)
            sb_level = _LEVEL_MAP.get(record.levelno, "INFO")
            attrs: dict[str, str] = {"logger": record.name}
            if record.exc_info:
                import traceback as _tb
                attrs["exception"] = "".join(_tb.format_exception(*record.exc_info)).strip()
            self._svc._log_push(sb_level, msg, attrs)
        except Exception:
            self.handleError(record)
