Skip to content

Commit 4a1a1e6

Browse files
author
Echo
committed
fix: proper thread root/parent refs in replies
Previously, post_reply() used the same URI/CID for both root and parent, which broke threading for deep replies. BlueSky threading requires: - root: the original post that started the thread - parent: the immediate post being replied to Changes: - Added parent_cid and root_cid fields to Post dataclass - Extract root/parent refs from reply_ref when creating Posts - post_reply() now accepts optional root_uri/root_cid params - Call site looks up original Post to pass correct root info This fixes threading for conversation continuations and deep replies. Tested by Jennifer RM via quote+reply combo.
1 parent 35eeac9 commit 4a1a1e6

File tree

1 file changed

+50
-11
lines changed

1 file changed

+50
-11
lines changed

bsky_cli/engage.py

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ class Post:
5151
like_count: int = 0
5252
is_reply: bool = False
5353
parent_uri: str | None = None
54+
parent_cid: str | None = None
5455
root_uri: str | None = None
56+
root_cid: str | None = None
5557

5658
# Scoring
5759
base_score: float = 1.0
@@ -318,24 +320,33 @@ def get_replies_to_our_posts(pds: str, jwt: str, our_did: str, conversations: di
318320
continue
319321

320322
thread = r.json().get("thread", {})
323+
# Get root post CID for threading
324+
root_post = thread.get("post", {})
325+
root_cid = root_post.get("cid", "")
326+
321327
for reply in thread.get("replies", []):
322328
post_data = reply.get("post", {})
323329
if post_data.get("author", {}).get("did") == our_did:
324330
continue # Skip our own replies
325331

332+
record = post_data.get("record", {})
333+
reply_ref = record.get("reply", {})
334+
326335
replies.append(Post(
327336
uri=post_data.get("uri", ""),
328337
cid=post_data.get("cid", ""),
329338
author_did=post_data.get("author", {}).get("did", ""),
330339
author_handle=post_data.get("author", {}).get("handle", ""),
331-
text=post_data.get("record", {}).get("text", "")[:500],
332-
created_at=post_data.get("record", {}).get("createdAt", ""),
340+
text=record.get("text", "")[:500],
341+
created_at=record.get("createdAt", ""),
333342
reply_count=post_data.get("replyCount", 0),
334343
like_count=post_data.get("likeCount", 0),
335344
repost_count=post_data.get("repostCount", 0),
336345
is_reply=True,
337-
parent_uri=our_post_uri,
338-
root_uri=thread_key
346+
parent_uri=reply_ref.get("parent", {}).get("uri") or our_post_uri,
347+
parent_cid=reply_ref.get("parent", {}).get("cid"),
348+
root_uri=reply_ref.get("root", {}).get("uri") or thread_key,
349+
root_cid=reply_ref.get("root", {}).get("cid") or root_cid
339350
))
340351
except Exception:
341352
continue
@@ -443,7 +454,9 @@ def filter_recent_posts(posts: list[dict], hours: int = 12) -> list[Post]:
443454
repost_count=post_data.get("repostCount", 0),
444455
is_reply=is_reply,
445456
parent_uri=reply_ref.get("parent", {}).get("uri") if is_reply else None,
446-
root_uri=reply_ref.get("root", {}).get("uri") if is_reply else None
457+
parent_cid=reply_ref.get("parent", {}).get("cid") if is_reply else None,
458+
root_uri=reply_ref.get("root", {}).get("uri") if is_reply else None,
459+
root_cid=reply_ref.get("root", {}).get("cid") if is_reply else None
447460
))
448461

449462
return recent
@@ -549,16 +562,33 @@ def select_posts_with_llm(candidates: list[Post], state: dict, dry_run: bool = F
549562
return []
550563

551564

552-
def post_reply(pds: str, jwt: str, did: str, parent_uri: str, parent_cid: str, text: str) -> dict | None:
553-
"""Post a reply to a post. Returns the created post data or None."""
565+
def post_reply(
566+
pds: str, jwt: str, did: str,
567+
parent_uri: str, parent_cid: str,
568+
text: str,
569+
root_uri: str | None = None,
570+
root_cid: str | None = None
571+
) -> dict | None:
572+
"""Post a reply to a post. Returns the created post data or None.
573+
574+
Args:
575+
parent_uri/parent_cid: The post we're directly replying to
576+
root_uri/root_cid: The thread root (if different from parent)
577+
578+
If root is not provided, parent is used as root (for top-level replies).
579+
"""
554580
now = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
555581

582+
# Use parent as root if root not specified (replying to a non-reply post)
583+
actual_root_uri = root_uri or parent_uri
584+
actual_root_cid = root_cid or parent_cid
585+
556586
record = {
557587
"$type": "app.bsky.feed.post",
558588
"text": text,
559589
"createdAt": now,
560590
"reply": {
561-
"root": {"uri": parent_uri, "cid": parent_cid},
591+
"root": {"uri": actual_root_uri, "cid": actual_root_cid},
562592
"parent": {"uri": parent_uri, "cid": parent_cid}
563593
}
564594
}
@@ -667,15 +697,24 @@ def run(args) -> int:
667697
print()
668698

669699
if not dry_run:
670-
result = post_reply(pds, jwt, did, sel["uri"], sel["cid"], sel["reply"])
700+
# Look up original Post to get root info for proper threading
701+
original_post = next((p for p in candidates if p.uri == sel["uri"]), None)
702+
root_uri = original_post.root_uri if original_post else None
703+
root_cid = original_post.root_cid if original_post else None
704+
705+
result = post_reply(
706+
pds, jwt, did,
707+
sel["uri"], sel["cid"], sel["reply"],
708+
root_uri=root_uri, root_cid=root_cid
709+
)
671710
if result:
672711
print(f" ✓ Posted!")
673712
state["replied_posts"].append(sel["uri"])
674713
state.setdefault("replied_accounts_today", []).append(
675-
next((p.author_did for p in candidates if p.uri == sel["uri"]), "")
714+
original_post.author_did if original_post else ""
676715
)
677716
# Track for conversation continuation
678-
track_reply(conversations, result.get("uri", ""), sel["uri"], sel.get("root_uri"))
717+
track_reply(conversations, result.get("uri", ""), sel["uri"], root_uri)
679718
else:
680719
print(f" ✗ Failed to post")
681720

0 commit comments

Comments
 (0)