You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fix three runtime defects that silently corrupted metering and parsing
An unreachable max_seconds poisoned the deadline guard process-wide.
setitimer raises OverflowError past the platform's time_t, and it raises
*after* the SIGALRM handler is installed and the process-wide slot taken —
both were left that way, so every later guard found the slot held and fell
back to the async-exception mechanism, which cannot unwind a blocking
syscall. Measured: a 0.3s ceiling honoured at 0.30s, then at 5.00s after a
single 1e10 run. Arming is undone on failure now, and the armed delay is
clamped to what both mechanisms accept.
Every async def node double-charged its token re-reports. The re-report
ledger was keyed by threading.get_ident(), but on_llm_end is sync, so under
ainvoke LangChain dispatches it to a worker thread while the body stays on
the event loop. The automatic charge found no ledger, never recorded the
call, and the node's named re-report — the documented free path — was
charged again. Any node using charge_usage, AgentNode._charge_tokens or
planner.proposal._charge reported double its real spend and hit max_tokens
at half its declared allowance. The ledger is a contextvars scope now,
verified to propagate across that hop; nesting also stops discarding the
enclosing node's ledger.
A bracket in the model's prose hijacked JSON extraction, because only the
first { or [ was ever tried. "Based on the context [lines 3-5]: {...}" was
rejected as unparseable, and — worse — 'Analysis (note [1]): {"supported":
false}' returned a valid [1], substituting a fabricated value for the
verifier's actual answer. Every opener is tried now and the longest parse
wins, which also prefers a complete structure over a nested fragment. Junk
still returns None, so the caller's fail-closed path is unchanged.
A node's end event also reported the movement of the run's *shared* meter
rather than its own spend, so overlapping fan-out workers each absorbed
their siblings' concurrent charges: three workers costing 8 tokens each
traced as 24/16/8, and metrics and cost both reported 48 for 24 tokens of
real work, doubling the estimated bill purely because the work ran in
parallel. Attribution now comes from a per-node scope on the meter.
Each fix ships a test that fails without it, confirmed by stashing the
source and watching them go red.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0 commit comments