Conversation
Traces were never reaching Langfuse due to a cross-context bug in the plugin itself: start_as_current_observation() was entered in one hook call and its span ended in another, and since Hermes fires pre/post hooks across different worker threads, OTEL's contextvars Token could not be popped in the thread it was created in — raising "ValueError: <Token ...> was created in a different Context" on every turn, silently, with the trace just never sent. Switch to the detached start_observation() API, which returns the same span object without mutating OTEL's "current span" contextvar, so start/end can safely happen on different threads. Also document (README) that the >=3.0 SDK pin is load-bearing (an unpinned install can silently resolve to a 2.x SDK, which fails an import this plugin needs and makes every hook a no-op with zero errors), that self-hosted Langfuse servers must also be v3+ (v3 SDKs only speak OTLP, which 2.x servers don't implement), and add a known- working v3 self-hosted docker-compose reference plus a troubleshooting table for the failure modes hit while debugging this end to end.
Related cluster in |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for isolating the root-observation lifecycle and documenting the observed Langfuse failure modes. The detached-observation direction is plausible, but this needs a small compatibility pass before it can be safely salvaged.
Problems
- Removing
root_ctxatplugins/observability/langfuse/__init__.py:45leaves six current-main test constructions passingroot_ctx=None(for exampletests/plugins/test_langfuse_plugin.py:721); the PR does not update that file. - README
plugins/observability/langfuse/README.md:15-24calls SDK v3 mandatory, but the interactive path still treats any importable SDK as valid and installs unversionedlangfuse(hermes_cli/tools_config.py:1389-1397). - The PR adds no automated coverage for the new
start_observation()path or its claimed cross-context finalization behavior.
Suggested changes
- Update the Langfuse fakes and all
TraceStatetest constructors, then add a regression test for detached root creation and finalization. - Make the setup/version behavior and both user-facing setup documents agree, or narrow the README claim.
Automated hermes-sweeper review.
| # trace silently never reaches Langfuse. start_observation() returns the | ||
| # same span object without touching the "current span" contextvar, so | ||
| # start/end can safely happen on different threads. | ||
| root_span = client.start_observation( |
There was a problem hiding this comment.
The existing Langfuse fake in tests/plugins/test_langfuse_plugin.py:265-304 implements only start_as_current_observation(), and the test file still constructs TraceState(..., root_ctx=None, ...) in six places. Please update that test contract and add coverage for this detached API before merging.
|
|
||
| # Manual | ||
| pip install langfuse | ||
| pip install 'langfuse>=3.0' |
There was a problem hiding this comment.
This version floor is not applied by the recommended interactive setup: hermes_cli/tools_config.py:1389-1397 accepts any importable SDK and otherwise installs unversioned langfuse. Please make the installer/version check and the primary built-in-plugin documentation enforce the same requirement, or narrow this claim.
|
Data point from Langfuse Cloud, which the test plan lists as untested.
So on Cloud + v4 the two symptoms are separable — the detach error fires |
Summary
Traces silently never reach Langfuse in the bundled
observability/langfuseplugin. Root cause:_start_root_traceentersclient.start_as_current_observation(...)via a manual.__enter__()call, then ends the span later via.end()from_finish_trace— but Hermes fires its pre/post hooks across different worker threads. OTEL'scontextvars.Token(pushed on enter) can only be popped in the same thread/context it was created in, so the mismatched enter/exit raisesValueError: <Token ...> was created in a different Contexton essentially every turn. The exception is swallowed with no user-facing error or log line pointing at the real cause — the trace just never shows up.client.start_observation(...)API, which returns the same span type without mutating OTEL's "current span" contextvar, so start and end can safely happen on different threads.langfuse>=3.0SDK pin is load-bearing — an unpinned install can silently resolve to a 2.x SDK, which lackspropagate_attributes/OTEL support this plugin needs, making every hook a no-op with zero errors.docker-composereference plus a troubleshooting table for the failure modes hit while debugging this end to end.Test plan
tui,cron,discordplatforms) now produce complete traces in a self-hosted Langfuse v3 instance — nested LLM-call spans, token usage, cost fields, tool-call counts all present, confirmed viaGET /api/public/traces/<id>.python -c "import ast; ast.parse(...)"syntax check on the modified plugin file.🤖 Generated with Claude Code