Skip to content

fix(langfuse): shutdown client on session finalize to avoid interpreter-teardown TypeError - #81054

Closed
bgodlin wants to merge 2 commits into
NousResearch:mainfrom
bgodlin:fix/langfuse-shutdown-on-session-finalize
Closed

fix(langfuse): shutdown client on session finalize to avoid interpreter-teardown TypeError#81054
bgodlin wants to merge 2 commits into
NousResearch:mainfrom
bgodlin:fix/langfuse-shutdown-on-session-finalize

Conversation

@bgodlin

@bgodlin bgodlin commented Aug 7, 2026

Copy link
Copy Markdown

Problem

Quitting a Hermes session with the observability/langfuse plugin enabled prints a noisy traceback:

Exception ignored in: <generator object Langfuse._create_span_with_parent_context at 0x...>
Traceback (most recent call last):
  File ".../langfuse/_client/client.py", line 1297, in _create_span_with_parent_context
  File ".../opentelemetry/trace/__init__.py", line 597, in use_span
TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union

Root cause

The langfuse plugin never called client.shutdown(). It relied entirely on the Langfuse SDK's own atexit.register(self.shutdown) handler (resource_manager.py:279).

That atexit handler fires during interpreter finalization — by then module globals (notably opentelemetry.trace.Span) may already be torn down to None. The SDK's span-finalization path runs use_spanisinstance(span, Span) (opentelemetry/trace/__init__.py:597), and Span being None raises the TypeError. Python suppresses it as "Exception ignored in: ".

This is cosmetic (no data loss — spans were already queued), but every langfuse-enabled session sees it on quit.

Fix

Register an on_session_finalize hook in the plugin that explicitly calls client.shutdown() while the interpreter is still alive. on_session_finalize fires from the normal CLI exit path (cli.py:1228lifecycle.finalize_session), not from atexit — so all modules are intact.

client.shutdown() flushes pending spans and joins the background export threads. The SDK's own atexit handler then becomes a no-op (it checks _shutdown and unregisters itself), so the race with interpreter teardown never starts.

Test plan

  • tests/plugins/test_langfuse_plugin.py — 24/24 pass (updated manifest assertion for the new hook)
  • tests/hermes_cli/test_lifecycle.py — 3/3 pass
  • Manual: quit a Hermes session with the langfuse plugin enabled — the "Exception ignored in: " traceback is gone

…er-teardown TypeError

The langfuse plugin never called client.shutdown(), relying on the SDK's
atexit handler. That fires during interpreter finalization, after
opentelemetry.trace.Span is torn down to None — use_span's
isinstance(span, Span) raises TypeError, surfaced as 'Exception ignored
in: <generator>' on quit. Register on_session_finalize to call
client.shutdown() while the interpreter is alive.
@alt-glitch alt-glitch added type/bug Something isn't working comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have duplicate This issue or pull request already exists labels Aug 7, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Duplicate of #80674 — it already implements the same session-finalize Langfuse client shutdown, and also closes suspended root contexts.

…down TypeError

The on_session_finalize hook (added in the previous commit) calls
client.shutdown() but that only flushes the SDK's internal queues. It
does not unwind the root observation context managers the plugin itself
created: _start_root_trace enters start_as_current_observation(...).__enter__()
but _finish_trace only called root_span.end(), never root_ctx.__exit__().

The generator stays suspended inside 'with otel_trace_api.use_span(parent_span):'
until the GC collects it during interpreter teardown. By then
opentelemetry.trace.Span has been torn down to None, and use_span's
isinstance(span, Span) raises:

  TypeError: isinstance() arg 2 must be a type

surfaced as 'Exception ignored in: <generator>' on every CLI exit.

Fix: call root_ctx.__exit__(None, None, None) right after root_span.end()
in both _finish_trace and _evict_stale_locked. This unwinds the generator
while all modules are intact.

Regression test: test_finish_trace_exits_root_context_manager verifies
__exit__ is called and fails on the pre-fix code.
@erosika

erosika commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

adopted into #83437 with authorship preserved (both commits cherry-picked). one interaction fix on top: the shutdown is gated on reason == "shutdown" there, since on_session_finalize also fires on /new, /reset, and gateway session expiry where the process keeps running and the client must stay alive.

kshitijk4poor added a commit that referenced this pull request Aug 13, 2026
… fan-out

Salvaged from PR #83437 by @erosika, with adopted fixes from @bgodlin (#81054),
@aldoeliacim (#82332), @nftpoetrist (#42326), @rodboev (#39653), @FnExpress
(#64292, supersedes #32175 by @db-aeon), @Per0-1 (#61166), @NaMinhyeok (#64797),
and @liuhao1024 (#43130).

Widens the bundled Langfuse plugin from 6 to 11 hooks and fixes two
attribution bugs. Also adopts shutdown/atexit lifecycle fixes and composes
8 prior community PRs with interaction-fix follow-ups.

Model attribution: on_pre_llm_request and on_post_llm_call now prefer the
wire value (request body model, response model) over the agent attribute,
which goes stale after /model switch or provider fallback.

Cost total: both cost paths now send a summed total alongside the per-type
breakdown, since Langfuse does not derive calculatedTotalCost from
cost_details keys. Subscription-included routes send no cost keys at all.

New coverage: api_request_error closes failed generations with ERROR level;
on_session_finalize/on_session_end close dangling traces for tool-only and
interrupted turns; subagent_start/subagent_stop trace delegated children as
spans; MoA advisor fan-out emits one generation per advisor priced at the
advisor's own model.

Capture modes: HERMES_LANGFUSE_CAPTURE=metadata|sanitized|full (default
sanitized). Sanitized mode redacts secret patterns before truncation.

Adopted lifecycle fixes: shutdown client at session finalize when
reason=shutdown (not on session rotation); atexit finalizer ends open root
spans for short-lived processes; root context manager exited to prevent
interpreter-teardown TypeError; TOCTOU on _get_langfuse() fixed with lock;
reasoning_content surfaced in traces; system prompt included in generation
input for Anthropic/Codex/Bedrock; SDK v3 update_trace replaces set_trace_io.

Closes #29482, #43129, #72661.
Supersedes #81054, #82332, #42326, #39653, #64292, #32175, #61166, #64797, #43130.
Partially addresses #67544 (capture modes + secret redaction; user_id remains open).
@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Merged via #85439 — your fix was adopted and composed into the wider Langfuse tracing PR by @erosika. Your contribution is credited in the commit body. Thanks @bgodlin!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins duplicate This issue or pull request already exists P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants