v0.1.4 — batch() correctness, thread-safe handler, defensive storage
The correctness release. If you have been running chain.batch([...]) or sharing a handler across concurrent chain.invoke calls, upgrade immediately.
See CHANGELOG.md for the full entry.
The bug this fixes
Prior to v0.1.4, both handlers kept per-invocation state on the instance. With chain.batch([q1, q2, q3]) or concurrent invokes on a shared handler, only the outermost chain's record would save, and fields bled across runs — the first query could be paired with the last query's answer. For a compliance library, silent audit loss with fabricated pairings is the most dangerous possible failure mode.
What shipped
- Per-run state in the LangChain handler. State now lives in
self._runs[run_id] = _RunState, guarded by athreading.Lock, withparent_run_idresolving to the tracked root viaself._run_parents. Each_RunStatetracks its descendants for O(1) cleanup on chain end. - Per-trace state in the LlamaIndex handler. State keyed by
trace_idinself._traces, with active trace routed viathreading.localso the trace_id-lesson_event_*callbacks reach the correct state. - Defensive
storage.save(). Both handlers wrapself.storage.save(record)intry/except. A misbehaving custom storage can no longer kill a chain or leak pending state. - New env var
RAGCOMPLIANCE_MAX_PENDING_RUNS(default 10000): soft cap on in-flight root run states with oldest-first eviction. Guards against memory leaks ifon_chain_endis never delivered.
Test suite: 145 passed (up from 126)
19 new regression tests covering:
- 3-query batch with correctly-paired records
- 10 and 20 concurrent threads on a shared handler
- Nested LCEL chains saving exactly once
- Inner events routing to root state
- Interleaved event ordering
- Soft-cap eviction with descendant cleanup
- Raising storage not killing the chain
on_chain_endp50 hot-path microbench- Bad env var values falling back to default
- LlamaIndex: 10 concurrent traces producing distinct records
Install
pip install --upgrade ragcompliance==0.1.4Upgrading from 0.1.3
No breaking API changes for normal users. If you were reaching into handler internals (e.g. handler._query), those attributes no longer exist — state now lives in handler._runs[run_id]. The public callback API is unchanged.