Skip to content

channels/bluesky-sidecar: _thread_cache lost on sidecar restart — replies degrade to top-level skeets #5452

Description

@houko

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)

  1. 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.
  2. 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.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/apiREST/WS endpoints and dashboardarea/channelsMessaging channel adaptershas-prA pull request has been linked to this issue

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions