How correlation works
When a report arrives, the package attaches the calls that led up to it. There is no explicit “conversation id” in MCP to do this with, so it matches on two signals: the client session and the user.
Two rules
Section titled “Two rules”capture_recent_calls builds the list, up to n calls (20 by default):
session: calls with the same session key as the current request, newest first.user_window: if that found fewer thann, calls by the sameuser_subwithin the lastwindow(15 minutes), skipping calls already taken.
The result is sorted oldest first and numbered from 0, and each link records the
rule that matched it. Candidates come from the in-memory buffer of recent
calls (recent_calls, 2000), which includes calls still waiting in the queue,
and, when there is a DatabaseSink, from the database, which adds calls from
other workers and from before a restart. That lookup is capped by
correlation_timeout (1 second).
Session keys
Section titled “Session keys”The session key of a call is the first of these that exists:
- The HTTP
mcp-session-idheader, sent by clients that open a session over Streamable HTTP. - On stdio, one key per server process, since a stdio server serves exactly one client.
- FastMCP’s
ctx.session_id.
Why sessionless clients link by user
Section titled “Why sessionless clients link by user”MCP protocol revision 2026-07-28 made the protocol stateless over HTTP: clients
send no mcp-session-id, and each request stands alone. Claude Code and FastMCP
4’s own Client speak it. For those requests FastMCP generates a fresh
ctx.session_id per request, so the third fallback above is different for every
call, and the session rule finds nothing.
So for HTTP servers the user_window rule does the work, and it needs a
user_sub, which only an identity resolver
can supply. Without one, an HTTP server with modern clients links no calls:
submit_feedback answers linked_calls: 0.
| Transport | Client | Links by |
|---|---|---|
| stdio | any | session (per process) |
| HTTP | older protocol, sends mcp-session-id | session, then user_window |
| HTTP | 2026-07-28, no session | user_window only; needs an identity resolver |
in-process Client(app) (tests) | FastMCP 4 | user_window only |
What the window trades off
Section titled “What the window trades off”Matching by user within a time window is a heuristic. A user running two
unrelated tasks at once gets both tasks’ calls on a report about one of them,
and a report filed more than 15 minutes after the problem misses the calls that
caused it. n and window are parameters of capture_recent_calls for hosts
that call it directly; submit_feedback uses the defaults.
Calls that are excluded or sampled out are never candidates, since they were never kept. Failures of sampled tools always are.
After linking
Section titled “After linking”Links are stored in ffb_feedback_call_links as (feedback_ref, call_id, position, rule, linked_at). They are never pruned, and the calls they point to
are exempt from retention, so a report keeps its evidence. feedback_context
joins them back to the call rows, and fills in from memory any call that has not
reached the database yet.