lp_client_create() made the CALLING thread the client's owner thread. Callers
reach it through a lazily-created wrapper (the generated bind_<iface>() ->
LpClient::ensure()), so the first thread to make an outbound call captured the
whole transport for the life of the process.
For the qt_remote transport that thread also ends up owning the
QRemoteObjectNode and its QLocalSocket, which are only serviced by a thread
running a Qt event loop. A module whose first call came from a worker — an HTTP
handler, a timer thread — bound its transport to a thread that only pumps
events while it is already blocked inside a call. Replica acquisition then
never completed: every requestObject() burned its full 20s timeout and returned
nullptr, and since a failed acquire yields an empty result the data loss was
silent. openmetrics-module hit exactly this: one GET /metrics took 40s (2 x 20s)
and came back missing a module, /health went unanswered behind the wedged
libmicrohttpd thread, and the follow-up stop RPC failed.
Construct the client on the Qt main thread when the transport needs a Qt event
loop, so the per-call marshal that already exists (logos::runOnOwnerThread)
lands somewhere that can actually service it. This is the anchor the Qt path
always had — LogosAPI::getClient marshals construction to the LogosAPI's thread
— given to the lp path.
Plain (tcp/tcp_ssl) and mock transports are Qt-free and thread-agnostic, so
they keep the calling thread: a worker-thread consumer stays off the main
thread's back. LogosTransportFactory::needsQtEventLoop() carries that rule next
to the createConnection resolution it mirrors. When there is nothing to anchor
to (a Qt-affine transport with no QCoreApplication) we now warn instead of
letting it surface as a mute timeout.
Tests: a worker thread creates an lp client over qt_remote and calls a provider
published on the main thread; passes in ~0.15s, and with the construction hop
reverted fails after 24.8s / 49.9s — the acquire timeouts themselves. Plus a
truth table for needsQtEventLoop. 183/183 protocol tests pass.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Extract the Logos protocol layer from logos-cpp-sdk
Transports (plain TCP/TLS, qt_local, qt_remote/QRO, mock), token manager,
consumer core (LogosAPIClient/LogosAPIConsumer incl. the capability
auto-requestModule flow), ModuleProxy, the abstract LogosProviderObject
interface, and the canonical QVariant<->JSON conversion — now behind the
language-neutral lp_* C ABI (logos_protocol.h) carrying the protocol
semver (LOGOS_PROTOCOL_VERSION_*, lp_protocol_version()).
Bytes crossing the ABI use the lossless {"_bytes": base64url} tagging
(NUL-safe), matching the plain wire encoding.
Provider lp_* surface is compiled groundwork; serving lands with module
authoring.
* consumer: typed requestModule for the capability flow
Port of logos-cpp-sdk master f5a127dd ('use updated capability module',
cpp-sdk#85, Iuri Matias) — the touched files (logos_api_client.cpp,
logos_api_consumer.{h,cpp}) moved into this repo in the P1 extraction.
The capability auto-requestModule path now calls a typed std::string
helper on the consumer (which acquires the capability object directly)
instead of a stringly invokeRemoteMethod round-trip. 111/111 tests.