Skip to content

fix(hook): stop silent export skips from state-lock contention - #49

Open
kamikaze011001 wants to merge 1 commit into
langfuse:mainfrom
kamikaze011001:fix/lock-timeout-and-skip-visibility
Open

fix(hook): stop silent export skips from state-lock contention#49
kamikaze011001 wants to merge 1 commit into
langfuse:mainfrom
kamikaze011001:fix/lock-timeout-and-skip-visibility

Conversation

@kamikaze011001

Copy link
Copy Markdown

Fixes the two immediate items from #48 (items 3 and 4 in that issue's list).

Problem

emit_new_turns_from_transcript() holds one global flock for its entire duration — reading the transcript, parsing turns, emitting to Langfuse over the network, and saving state. Hold time therefore scales with how much a session has to flush, and a real 82-turn flush from my own log took 2.16s:

2026-08-08 00:09:38 [INFO] Processed 82 turns in 2.16s (session=4d012f34-...)

That already exceeds the 2.0s acquisition timeout, so any concurrent session reaching Stop during a large flush loses the race. On timeout, main() caught the TimeoutError, logged it at debug, and returned 0 — reporting success. Since CC_LANGFUSE_DEBUG defaults to false, this left no trace at all: the turn isn't exported until that session's next Stop, and never if the session goes idle first. The symptom is Langfuse collecting only one of several concurrent sessions, with the apparent fault server-side and the actual cause entirely client-side.

Changes

  • Lock timeout 2.0s → 30.0s. Waiting is strictly better than skipping here: the hook is async and the work is idempotent, so a slow acquire costs latency while a failed acquire costs data.
  • Skip logged at info instead of debug, so the cause is visible without opting into debug logging.

What this does not fix

This mitigates the contention; it doesn't remove it. The root problem is that the lock spans network I/O, so hold time stays unbounded and data-dependent — a large enough backfill could still exhaust any fixed timeout. The structural fixes suggested in #48 still stand:

  • Narrow the critical section so the lock isn't held across the emit, or
  • Move to per-session state files, which removes the contention outright since state is already keyed by get_session_state_key(session_id, transcript_path) and concurrent sessions share nothing that needs mutual exclusion.

Happy to follow up with either if you have a preference on direction.

Testing

uv run -m pytest123 passed. No existing test references FileLock or timeout_s, so nothing needed updating; I didn't add a test for the new default since asserting on a timing constant would mostly pin the number rather than the behaviour, but glad to add one if you'd like it.

🤖 Generated with Claude Code

The state-file lock is held for the whole of
emit_new_turns_from_transcript, including the network emit to Langfuse,
so hold time scales with how much a session has to flush. A real 82-turn
flush took 2.16s, which already exceeds the 2.0s acquisition timeout, so
any session reaching Stop during a large flush loses the race.

On timeout main() swallowed the TimeoutError at debug level and returned
0, reporting success. With CC_LANGFUSE_DEBUG defaulting to false this
left no trace anywhere: the turn is not exported until the session's next
Stop, and never at all if the session goes idle first. It surfaces as
Langfuse collecting only one of several concurrent sessions.

Two changes:

- Raise the default lock timeout 2.0s -> 30.0s. Waiting is strictly
  better than skipping: the hook is async and the work is idempotent, so
  a slow acquire costs latency while a failed acquire costs data.
- Log the skip at info instead of debug, so the cause is visible without
  opting into debug logging.

This mitigates the contention rather than removing it. The underlying
issue is that the lock spans network I/O; narrowing the critical section
or moving to per-session state files would remove the contention
entirely, since state is already keyed per session.

Refs langfuse#48

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants