Background
The 4-PR audit chain ending at #5439 found that several sidecars stash per-message reply correlation in process-local dicts that vanish on sidecar restart. wechat (#5448) and wecom (#5449) were hotfixable by routing the key through ChannelUser.librefang_user. bluesky is structurally trickier: AT Protocol's reply needs a 2-tuple of {root: {uri, cid}, parent: {uri, cid}} — four fields, not one — so the single-string librefang_user slot doesn't fit.
Symptom
After a sidecar restart between an inbound mention and the bot's reply:
_thread_cache cache miss → _post_status posts the skeet WITHOUT a reply field
- AT Protocol treats this as a top-level post → visible to followers' feeds, not just the thread
- No user-visible warning
Investigation evidence
Suggested fixes (escalating)
- Cheapest: on
_thread_cache miss, do an XRPC app.bsky.feed.getPosts?uris[]=<uri> to re-fetch cid for the parent, and app.bsky.feed.getPostThread to walk up to the root. Adds one or two HTTP round-trips per reply but works across any restart scenario.
- Medium: also stash
parent_cid + root_uri + root_cid as separate metadata fields; widen the sidecar protocol's SidecarSendParams to round-trip arbitrary metadata bytewise.
- Heaviest (covers the wider class): add a
metadata: HashMap<String, Value> field to SidecarSendParams (verified absent at crates/librefang-channels/src/sidecar.rs:200-215) so the daemon round-trips arbitrary inbound state. This would let inbound stash arbitrary correlation state and have it survive serde + restart cleanly. Fixes bluesky, wechat, wecom, and any future sidecar that needs more than a single string for reply correlation.
Option 3 is the more principled fix — closes the class, not just the instance. Option 1 is the minimum-blast-radius fix.
Reproduction
# 1. Bot receives a mention on Bluesky, _parse_notification caches:
# a._thread_cache.put("at://did:plc:alice/post/1", {"root": ..., "parent": ...})
# 2. Sidecar restart (any reason).
# 3. Agent invokes on_send for the mention.
# 4. cmd.user.librefang_user = "at://did:plc:alice/post/1" (round-tripped from before-restart inbound).
# 5. _post_status calls _thread_cache.get("at://did:plc:alice/post/1") → cache miss after restart.
# 6. Skeet posted without `reply` field → top-level post, not a reply.
Related
Background
The 4-PR audit chain ending at #5439 found that several sidecars stash per-message reply correlation in process-local dicts that vanish on sidecar restart. wechat (#5448) and wecom (#5449) were hotfixable by routing the key through
ChannelUser.librefang_user. bluesky is structurally trickier: AT Protocol's reply needs a 2-tuple of{root: {uri, cid}, parent: {uri, cid}}— four fields, not one — so the single-stringlibrefang_userslot doesn't fit.Symptom
After a sidecar restart between an inbound mention and the bot's reply:
_thread_cachecache miss →_post_statusposts the skeet WITHOUT areplyfieldInvestigation evidence
sdk/python/librefang/sidecar/adapters/bluesky.py:583-604—_post_statuslooks up_thread_cache[uri]for the reply refbluesky.py:466(nowlibrefang_user=urifrom fix(channels/sidecars): recover per-message reply correlation via ChannelUser.librefang_user across 6 sidecars #5439) — only the URI is round-tripped, not the full reply structSuggested fixes (escalating)
_thread_cachemiss, do an XRPCapp.bsky.feed.getPosts?uris[]=<uri>to re-fetchcidfor the parent, andapp.bsky.feed.getPostThreadto walk up to the root. Adds one or two HTTP round-trips per reply but works across any restart scenario.parent_cid+root_uri+root_cidas separate metadata fields; widen the sidecar protocol'sSidecarSendParamsto round-trip arbitrary metadata bytewise.metadata: HashMap<String, Value>field toSidecarSendParams(verified absent atcrates/librefang-channels/src/sidecar.rs:200-215) so the daemon round-trips arbitrary inbound state. This would let inbound stash arbitrary correlation state and have it survive serde + restart cleanly. Fixes bluesky, wechat, wecom, and any future sidecar that needs more than a single string for reply correlation.Option 3 is the more principled fix — closes the class, not just the instance. Option 1 is the minimum-blast-radius fix.
Reproduction
Related