@acedatacloud/skills 2026.726.11 → 2026.727.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acedatacloud/skills",
3
- "version": "2026.726.11",
3
+ "version": "2026.727.0",
4
4
  "description": "Agent Skills for AceDataCloud AI services — music, image, video generation, LLM chat, web search. Compatible with Claude Code, GitHub Copilot, Gemini CLI, OpenAI Codex, and 30+ AI coding agents.",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -87,7 +87,7 @@ Headers:
87
87
 
88
88
  | Parameter | Values | Description |
89
89
  |-----------|--------|-------------|
90
- | `model` | `"s1"`, `"s2-pro"` | Fish TTS engine selection |
90
+ | `model` | `"s1"`, `"s2-pro"`, `"s2.1-pro"` | Fish TTS engine selection |
91
91
 
92
92
  ### JSON body
93
93
 
@@ -55,8 +55,9 @@ python3 "$R" submissions --limit 10
55
55
  python3 "$R" comments --limit 10
56
56
  ```
57
57
 
58
- If a `comment` write ever reports an unknown outcome, run `comments` to check
59
- whether it actually landed **before** considering any retry.
58
+ `comment` verifies itself: when Reddit answers in a shape it cannot parse, it
59
+ looks the comment up via `comments` before reporting anything. If it still says
60
+ the write likely failed, re-check with `comments` yourself before resending.
60
61
 
61
62
  ## Find threads and check the rules
62
63
 
@@ -14,6 +14,7 @@ from __future__ import annotations
14
14
  import argparse
15
15
  import contextlib
16
16
  import gzip
17
+ import io
17
18
  import json
18
19
  import os
19
20
  import re
@@ -31,6 +32,9 @@ SUBREDDIT_RE = re.compile(r"^[A-Za-z0-9_]{2,21}$")
31
32
  COOKIE_NAME_RE = re.compile(r"^[!#$%&'*+.^_`|~0-9A-Za-z-]+$")
32
33
  # Only a comment (t1_) or a post (t3_) can be replied to.
33
34
  THING_ID_RE = re.compile(r"^t[13]_[A-Za-z0-9]{2,16}$")
35
+ VERIFY_ATTEMPTS = 2
36
+ VERIFY_RETRY_SECONDS = 3
37
+ VERIFY_CLOCK_SKEW_SECONDS = 15
34
38
 
35
39
 
36
40
  def output(value) -> None:
@@ -474,6 +478,78 @@ class RedditClient:
474
478
  return {"ok": True, "posted": True, "id": post.get("id"), "name": post.get("name"), "url": post_url}
475
479
 
476
480
 
481
+ def verify_recent_comment(
482
+ self, parent: str, body: str, written_at: float, seen_ids: set | None = None
483
+ ) -> dict:
484
+ """Resolve an unparsable write by finding the comment we just posted.
485
+
486
+ Matching on body alone would let a stale duplicate vouch for a rejected
487
+ write, so the hit must also belong to this thread and be newly created.
488
+ """
489
+ wanted = body.strip()
490
+ recent = []
491
+ for attempt in range(VERIFY_ATTEMPTS):
492
+ if attempt:
493
+ # The user-comments listing is a read replica and lags a write.
494
+ time.sleep(VERIFY_RETRY_SECONDS)
495
+ try:
496
+ with contextlib.redirect_stdout(io.StringIO()):
497
+ recent = self.comments(25)
498
+ except SystemExit:
499
+ recent = []
500
+ match = self.match_written_comment(recent, parent, wanted, written_at, seen_ids)
501
+ if match:
502
+ formatted = format_comment(match)
503
+ return {
504
+ "ok": True,
505
+ "commented": True,
506
+ "id": formatted["id"],
507
+ "name": None,
508
+ "url": formatted["url"],
509
+ "note": "Reddit returned an unrecognized response; the comment was confirmed via `comments`.",
510
+ }
511
+ die(
512
+ "Reddit returned an unrecognized comment response and the comment could not be "
513
+ "confirmed in this account's recent comments. It may have been rejected, or the "
514
+ "listing may still be catching up. Re-check with `comments` before resending; "
515
+ "do not replay automatically."
516
+ )
517
+
518
+ @staticmethod
519
+ def match_written_comment(
520
+ recent: list, parent: str, wanted: str, written_at: float, seen_ids: set | None = None
521
+ ) -> dict | None:
522
+ """Find the comment this write created — never merely a lookalike.
523
+
524
+ Body alone is not proof: the same text can already exist on the same
525
+ thread, so an id present before the write can never vouch for it.
526
+ """
527
+ thread = parent[3:] if parent.startswith("t3_") else ""
528
+ for item in recent:
529
+ if str(item.get("body") or "").strip() != wanted:
530
+ continue
531
+ if seen_ids is not None and str(item.get("id") or "") in seen_ids:
532
+ continue
533
+ created = item.get("created_utc")
534
+ if not isinstance(created, (int, float)) or created < written_at - VERIFY_CLOCK_SKEW_SECONDS:
535
+ continue
536
+ # Reply target must match: same parent, not merely the same thread.
537
+ parent_id = str(item.get("parent_id") or "")
538
+ if parent_id and parent_id != parent:
539
+ continue
540
+ if thread:
541
+ link_id = item.get("link_id")
542
+ if isinstance(link_id, str) and link_id.startswith("t3_"):
543
+ if link_id[3:] != thread:
544
+ continue
545
+ elif f"/comments/{thread}/" not in str(item.get("permalink") or ""):
546
+ continue
547
+ elif not parent_id:
548
+ # A t1_ reply we cannot attribute to its parent is not proof.
549
+ continue
550
+ return item
551
+ return None
552
+
477
553
  def comments(self, limit: int) -> list[dict]:
478
554
  """List my own recent comments — used to verify an ambiguous write."""
479
555
  username = self.username or str(self.me().get("name") or "")
@@ -541,13 +617,15 @@ class RedditClient:
541
617
  die("Reddit did not return a modhash; the Cookie session cannot perform writes.")
542
618
  form["uh"] = self.modhash
543
619
 
620
+ written_at = time.time()
544
621
  payload = self.request("POST", "/api/comment", form=form, write_kind="comment")
622
+ # Cookie mode can answer in jQuery form with no `json` object at all.
545
623
  if not isinstance(payload, dict) or not isinstance(payload.get("json"), dict):
546
- die_unknown_comment_response()
624
+ return self.verify_recent_comment(parent, body, written_at)
547
625
  json_payload = payload["json"]
548
626
  errors = json_payload.get("errors")
549
627
  if not isinstance(errors, list):
550
- die_unknown_comment_response()
628
+ return self.verify_recent_comment(parent, body, written_at)
551
629
  if errors:
552
630
  die(
553
631
  "Reddit rejected the comment. Check subreddit rules, account age/karma "
@@ -555,11 +633,11 @@ class RedditClient:
555
633
  "authenticated error details were omitted."
556
634
  )
557
635
  things = (json_payload.get("data") or {}).get("things")
558
- if not isinstance(things, list) or not things or not isinstance(things[0], dict):
559
- die_unknown_comment_response()
560
- created = things[0].get("data")
636
+ created = things[0].get("data") if isinstance(things, list) and things and isinstance(things[0], dict) else None
561
637
  if not isinstance(created, dict):
562
- die_unknown_comment_response()
638
+ # Cookie mode returns shapes this parser does not model. Never guess:
639
+ # look for the comment we just wrote before reporting any outcome.
640
+ return self.verify_recent_comment(parent, body, written_at)
563
641
  comment_name = created.get("name")
564
642
  comment_id = created.get("id")
565
643
  if not isinstance(comment_id, str) or not comment_id:
@@ -572,7 +650,7 @@ class RedditClient:
572
650
  return {"ok": True, "commented": True, "id": comment_id or None, "name": comment_name, "url": WEB_BASE + permalink}
573
651
 
574
652
  if not comment_id:
575
- die_unknown_comment_response()
653
+ return self.verify_recent_comment(parent, body, written_at)
576
654
 
577
655
  # link_id is the thread even when replying to a comment (t1_ parent).
578
656
  link_id = created.get("link_id")
@@ -609,6 +609,145 @@ class RedditSkillTests(unittest.TestCase):
609
609
  self.assertEqual(2, len(items))
610
610
  self.assertIsNone(reddit.format_comment(items[1])["url"])
611
611
 
612
+ def test_unparsable_write_is_resolved_by_verifying_recent_comments(self):
613
+ """Two live writes hit shapes this parser did not model; verify, never guess."""
614
+ client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
615
+ client.modhash = "csrf-secret"
616
+ client.username = "tester"
617
+ unparsable = {"json": {"errors": [], "data": {}}}
618
+ listing = {"data": {"children": [
619
+ {"data": {"id": "ozw0cnk", "body": "Honest answer: it depends.",
620
+ "link_id": "t3_1uqn02z", "created_utc": time.time(),
621
+ "permalink": "/r/aivideomaking/comments/1uqn02z/help/ozw0cnk/"}},
622
+ ]}}
623
+ with patch.object(client, "request", side_effect=[unparsable, listing]):
624
+ result = client.comment(parent="t3_1uqn02z", body="Honest answer: it depends.")
625
+
626
+ self.assertTrue(result["commented"])
627
+ self.assertEqual("ozw0cnk", result["id"])
628
+ self.assertEqual(
629
+ "https://www.reddit.com/r/aivideomaking/comments/1uqn02z/help/ozw0cnk/", result["url"]
630
+ )
631
+ self.assertIn("confirmed via", result["note"])
632
+
633
+ def test_unparsable_write_with_no_matching_comment_reports_likely_failure(self):
634
+ client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
635
+ client.modhash = "csrf-secret"
636
+ client.username = "tester"
637
+ unparsable = {"json": {"errors": [], "data": {}}}
638
+ listing = {"data": {"children": [
639
+ {"data": {"id": "other", "body": "an unrelated older comment", "link_id": "t3_zzz",
640
+ "created_utc": time.time(), "permalink": "/r/x/comments/a/b/other/"}},
641
+ ]}}
642
+ stream = io.StringIO()
643
+ with patch.object(client, "request", side_effect=[unparsable, listing, listing]), patch.object(
644
+ reddit.time, "sleep"
645
+ ), self.assertRaises(SystemExit), redirect_stdout(stream):
646
+ client.comment(parent="t3_1uqn02z", body="Body that was never posted")
647
+ message = stream.getvalue()
648
+ self.assertIn("could not be confirmed", message)
649
+ self.assertIn("do not replay", message)
650
+ self.assertNotIn("csrf-secret", message)
651
+
652
+ def test_jquery_rejection_is_never_laundered_into_success_by_a_stale_duplicate(self):
653
+ """A jQuery-shaped rejection has no errors array; a same-text older comment must not vouch for it."""
654
+ client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
655
+ client.modhash = "csrf-secret"
656
+ client.username = "tester"
657
+ jquery_rejection = {"jquery": [[0, 1, "call", ["you are doing that too much. try again in 6 minutes."]]]}
658
+ stale = {"data": {"children": [
659
+ {"data": {"id": "OLD111", "body": "Honest answer: it depends.", "link_id": "t3_OTHER",
660
+ "created_utc": time.time() - 86400,
661
+ "permalink": "/r/other/comments/OTHER/old_thread/OLD111/"}},
662
+ ]}}
663
+ stream = io.StringIO()
664
+ with patch.object(client, "request", side_effect=[jquery_rejection, stale, stale]), patch.object(
665
+ reddit.time, "sleep"
666
+ ), self.assertRaises(SystemExit), redirect_stdout(stream):
667
+ client.comment(parent="t3_NEWTHRD", body="Honest answer: it depends.")
668
+ self.assertIn("could not be confirmed", stream.getvalue())
669
+ self.assertNotIn("OLD111", stream.getvalue())
670
+
671
+ def test_jquery_shape_recovers_when_the_comment_really_landed(self):
672
+ client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
673
+ client.modhash = "csrf-secret"
674
+ client.username = "tester"
675
+ jquery = {"jquery": [[0, 1, "call", ["ok"]]]}
676
+ listing = {"data": {"children": [
677
+ {"data": {"id": "ozw0cnk", "body": "Looks good", "link_id": "t3_1v711cj",
678
+ "created_utc": time.time(), "permalink": "/r/test/comments/1v711cj/test/ozw0cnk/"}},
679
+ ]}}
680
+ with patch.object(client, "request", side_effect=[jquery, listing]):
681
+ result = client.comment(parent="t3_1v711cj", body="Looks good")
682
+ self.assertTrue(result["commented"])
683
+ self.assertEqual("https://www.reddit.com/r/test/comments/1v711cj/test/ozw0cnk/", result["url"])
684
+
685
+ def test_verify_retries_once_for_listing_replication_lag(self):
686
+ """A just-written comment can lag the replica; one empty read must not mean failure."""
687
+ client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
688
+ client.modhash = "csrf-secret"
689
+ client.username = "tester"
690
+ unparsable = {"json": {"errors": [], "data": {}}}
691
+ empty = {"data": {"children": []}}
692
+ landed = {"data": {"children": [
693
+ {"data": {"id": "lag1", "body": "Looks good", "link_id": "t3_1v711cj",
694
+ "created_utc": time.time(), "permalink": "/r/test/comments/1v711cj/t/lag1/"}},
695
+ ]}}
696
+ with patch.object(client, "request", side_effect=[unparsable, empty, landed]), patch.object(
697
+ reddit.time, "sleep"
698
+ ) as slept:
699
+ result = client.comment(parent="t3_1v711cj", body="Looks good")
700
+ self.assertTrue(result["commented"])
701
+ self.assertEqual("lag1", result["id"])
702
+ slept.assert_called()
703
+
704
+ def test_verify_ignores_a_comment_created_before_the_write(self):
705
+ client = reddit.RedditClient("cookie", cookies=reddit.parse_cookie_jar(COOKIE_JAR))
706
+ recent = [{"id": "old", "body": "same text", "link_id": "t3_thread1",
707
+ "created_utc": time.time() - 7200, "permalink": "/r/x/comments/thread1/s/old/"}]
708
+ self.assertIsNone(
709
+ reddit.RedditClient.match_written_comment(recent, "t3_thread1", "same text", time.time())
710
+ )
711
+
712
+ def test_match_requires_the_same_reply_target_not_just_recency(self):
713
+ """These fixtures are all RECENT, so only the thread/parent check can reject them."""
714
+ now = time.time()
715
+ cases = [
716
+ # same body, recent, but a different thread
717
+ ({"id": "x1", "body": "same text", "link_id": "t3_OTHER", "created_utc": now,
718
+ "permalink": "/r/o/comments/OTHER/s/x1/"}, "t3_THREADX"),
719
+ # link_id absent -> permalink must still place it in our thread
720
+ ({"id": "x2", "body": "same text", "created_utc": now,
721
+ "permalink": "/r/o/comments/OTHER/s/x2/"}, "t3_THREADX"),
722
+ # right thread, but it is a reply to a different comment
723
+ ({"id": "x3", "body": "same text", "link_id": "t3_THREADX", "parent_id": "t1_someother",
724
+ "created_utc": now, "permalink": "/r/o/comments/THREADX/s/x3/"}, "t3_THREADX"),
725
+ # t1_ parent, mismatched parent_id
726
+ ({"id": "x4", "body": "same text", "parent_id": "t1_wrong", "created_utc": now,
727
+ "permalink": "/r/o/comments/THREADX/s/x4/"}, "t1_target"),
728
+ # t1_ parent with no parent_id at all -> unattributable, not proof
729
+ ({"id": "x5", "body": "same text", "created_utc": now,
730
+ "permalink": "/r/o/comments/THREADX/s/x5/"}, "t1_target"),
731
+ ]
732
+ for item, parent in cases:
733
+ with self.subTest(item=item["id"]):
734
+ self.assertIsNone(
735
+ reddit.RedditClient.match_written_comment([item], parent, "same text", now)
736
+ )
737
+
738
+ def test_match_accepts_the_comment_this_write_created(self):
739
+ now = time.time()
740
+ top_level = {"id": "ok1", "body": "same text", "link_id": "t3_THREADX", "parent_id": "t3_THREADX",
741
+ "created_utc": now, "permalink": "/r/o/comments/THREADX/s/ok1/"}
742
+ self.assertEqual(
743
+ "ok1", reddit.RedditClient.match_written_comment([top_level], "t3_THREADX", "same text", now)["id"]
744
+ )
745
+ nested = {"id": "ok2", "body": "same text", "link_id": "t3_THREADX", "parent_id": "t1_target",
746
+ "created_utc": now, "permalink": "/r/o/comments/THREADX/s/ok2/"}
747
+ self.assertEqual(
748
+ "ok2", reddit.RedditClient.match_written_comment([nested], "t1_target", "same text", now)["id"]
749
+ )
750
+
612
751
  def test_comment_body_limits_are_enforced(self):
613
752
  for body in ["", " ", "x" * 10_001]:
614
753
  stream = io.StringIO()
@@ -674,12 +813,12 @@ class RedditSkillTests(unittest.TestCase):
674
813
 
675
814
  # Malformed shape.
676
815
  stream = io.StringIO()
677
- with patch.object(client, "request", return_value={"json": {"errors": []}}), self.assertRaises(
678
- SystemExit
679
- ), redirect_stdout(stream):
816
+ with patch.object(client, "request", return_value={"json": {"errors": []}}), patch.object(
817
+ reddit.time, "sleep"
818
+ ), self.assertRaises(SystemExit), redirect_stdout(stream):
680
819
  client.comment(parent="t3_post1", body="Body")
681
- self.assertIn("comment outcome is unknown", stream.getvalue())
682
- self.assertIn("parent thread", stream.getvalue())
820
+ self.assertIn("could not be confirmed", stream.getvalue())
821
+ self.assertIn("do not replay", stream.getvalue())
683
822
  self.assertNotIn("recent submissions", stream.getvalue())
684
823
 
685
824
  # Transport failure inside request().