thth 4.3.0__tar.gz → 4.4.0__tar.gz

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.
Files changed (770) hide show
  1. {thth-4.3.0 → thth-4.4.0}/PKG-INFO +3 -1
  2. {thth-4.3.0 → thth-4.4.0}/README.md +2 -0
  3. {thth-4.3.0 → thth-4.4.0}/tests/test_replies_refresh.py +5 -3
  4. {thth-4.3.0 → thth-4.4.0}/tests/test_v31_collection.py +2 -1
  5. thth-4.4.0/tests/test_v431_stopped_timer.py +81 -0
  6. thth-4.4.0/tests/test_v440_reply_follow.py +299 -0
  7. thth-4.4.0/thth/VERSION +1 -0
  8. {thth-4.3.0 → thth-4.4.0}/thth/accounts.py +6 -0
  9. {thth-4.3.0 → thth-4.4.0}/thth/collect.py +93 -2
  10. {thth-4.3.0 → thth-4.4.0}/thth/core.py +25 -2
  11. thth-4.4.0/thth/reply_follow.py +188 -0
  12. thth-4.3.0/thth/VERSION +0 -1
  13. {thth-4.3.0 → thth-4.4.0}/.gitignore +0 -0
  14. {thth-4.3.0 → thth-4.4.0}/LICENSE +0 -0
  15. {thth-4.3.0 → thth-4.4.0}/accounts.example/bluesky.json +0 -0
  16. {thth-4.3.0 → thth-4.4.0}/accounts.example/hatenablog.json +0 -0
  17. {thth-4.3.0 → thth-4.4.0}/accounts.example/mastodon.json +0 -0
  18. {thth-4.3.0 → thth-4.4.0}/accounts.example/principal-hatena.json +0 -0
  19. {thth-4.3.0 → thth-4.4.0}/accounts.example/threads.json +0 -0
  20. {thth-4.3.0 → thth-4.4.0}/accounts.example/x.json +0 -0
  21. {thth-4.3.0 → thth-4.4.0}/bin/thth +0 -0
  22. {thth-4.3.0 → thth-4.4.0}/bin/thth-remote +0 -0
  23. {thth-4.3.0 → thth-4.4.0}/bin/thth-run +0 -0
  24. {thth-4.3.0 → thth-4.4.0}/mcp/server.py +0 -0
  25. {thth-4.3.0 → thth-4.4.0}/pyproject.toml +0 -0
  26. {thth-4.3.0 → thth-4.4.0}/skills/thth/SKILL.md +0 -0
  27. {thth-4.3.0 → thth-4.4.0}/skills/thth-admin/SKILL.md +0 -0
  28. {thth-4.3.0 → thth-4.4.0}/systemd/thth@.service +0 -0
  29. {thth-4.3.0 → thth-4.4.0}/tests/__init__.py +0 -0
  30. {thth-4.3.0 → thth-4.4.0}/tests/conftest.py +0 -0
  31. {thth-4.3.0 → thth-4.4.0}/tests/coordination_snapshot.py +0 -0
  32. {thth-4.3.0 → thth-4.4.0}/tests/fixtures/GraphemeBreakTest-17.0.0.txt +0 -0
  33. {thth-4.3.0 → thth-4.4.0}/tests/fixtures/build_injection_eval.py +0 -0
  34. {thth-4.3.0 → thth-4.4.0}/tests/fixtures/callback/worker_responses.json +0 -0
  35. {thth-4.3.0 → thth-4.4.0}/tests/fixtures/injection_eval.json +0 -0
  36. {thth-4.3.0 → thth-4.4.0}/tests/fixtures/limits_stage3/README.txt +0 -0
  37. {thth-4.3.0 → thth-4.4.0}/tests/fixtures/limits_stage3/ledgers/vm-kopicha-hatenablog.json +0 -0
  38. {thth-4.3.0 → thth-4.4.0}/tests/fixtures/limits_stage3/ledgers/vm-kopicha-x.json +0 -0
  39. {thth-4.3.0 → thth-4.4.0}/tests/fixtures/limits_stage3/ledgers/vm-music-threads.json +0 -0
  40. {thth-4.3.0 → thth-4.4.0}/tests/fixtures/limits_stage3/principals/vm-kopicha.json +0 -0
  41. {thth-4.3.0 → thth-4.4.0}/tests/fixtures/twitter_text_v3_weighted.json +0 -0
  42. {thth-4.3.0 → thth-4.4.0}/tests/fixtures/umami-bile.md +0 -0
  43. {thth-4.3.0 → thth-4.4.0}/tests/hatena_s1_flow_helpers.py +0 -0
  44. {thth-4.3.0 → thth-4.4.0}/tests/hatena_s1_helpers.py +0 -0
  45. {thth-4.3.0 → thth-4.4.0}/tests/hatena_s2_helpers.py +0 -0
  46. {thth-4.3.0 → thth-4.4.0}/tests/hatena_s2_http.py +0 -0
  47. {thth-4.3.0 → thth-4.4.0}/tests/hatena_s3_helpers.py +0 -0
  48. {thth-4.3.0 → thth-4.4.0}/tests/hatena_s4_helpers.py +0 -0
  49. {thth-4.3.0 → thth-4.4.0}/tests/hatena_s5_helpers.py +0 -0
  50. {thth-4.3.0 → thth-4.4.0}/tests/hatena_s6_helpers.py +0 -0
  51. {thth-4.3.0 → thth-4.4.0}/tests/helpers/__init__.py +0 -0
  52. {thth-4.3.0 → thth-4.4.0}/tests/helpers/fake_oauth_server.py +0 -0
  53. {thth-4.3.0 → thth-4.4.0}/tests/helpers/fake_push_adapter.py +0 -0
  54. {thth-4.3.0 → thth-4.4.0}/tests/helpers/fake_redirect_server.py +0 -0
  55. {thth-4.3.0 → thth-4.4.0}/tests/helpers/fake_search_adapter.py +0 -0
  56. {thth-4.3.0 → thth-4.4.0}/tests/helpers/fake_threads_reader.py +0 -0
  57. {thth-4.3.0 → thth-4.4.0}/tests/helpers/fake_threads_status.py +0 -0
  58. {thth-4.3.0 → thth-4.4.0}/tests/helpers/fake_threads_writes.py +0 -0
  59. {thth-4.3.0 → thth-4.4.0}/tests/helpers/fake_x.py +0 -0
  60. {thth-4.3.0 → thth-4.4.0}/tests/helpers/hold_lock.py +0 -0
  61. {thth-4.3.0 → thth-4.4.0}/tests/helpers/hold_repo_lock.py +0 -0
  62. {thth-4.3.0 → thth-4.4.0}/tests/helpers/worker_probe.mjs +0 -0
  63. {thth-4.3.0 → thth-4.4.0}/tests/test_27_acceptance_fixes.py +0 -0
  64. {thth-4.3.0 → thth-4.4.0}/tests/test_account_name_check.py +0 -0
  65. {thth-4.3.0 → thth-4.4.0}/tests/test_account_report.py +0 -0
  66. {thth-4.3.0 → thth-4.4.0}/tests/test_accounts_dir.py +0 -0
  67. {thth-4.3.0 → thth-4.4.0}/tests/test_adapter_boundary.py +0 -0
  68. {thth-4.3.0 → thth-4.4.0}/tests/test_admin_http_mcp.py +0 -0
  69. {thth-4.3.0 → thth-4.4.0}/tests/test_admin_log.py +0 -0
  70. {thth-4.3.0 → thth-4.4.0}/tests/test_admin_notifications.py +0 -0
  71. {thth-4.3.0 → thth-4.4.0}/tests/test_admin_repairs.py +0 -0
  72. {thth-4.3.0 → thth-4.4.0}/tests/test_admin_report.py +0 -0
  73. {thth-4.3.0 → thth-4.4.0}/tests/test_after_cli.py +0 -0
  74. {thth-4.3.0 → thth-4.4.0}/tests/test_analytics_comparison.py +0 -0
  75. {thth-4.3.0 → thth-4.4.0}/tests/test_analytics_report.py +0 -0
  76. {thth-4.3.0 → thth-4.4.0}/tests/test_api_diagnostic.py +0 -0
  77. {thth-4.3.0 → thth-4.4.0}/tests/test_api_error_body.py +0 -0
  78. {thth-4.3.0 → thth-4.4.0}/tests/test_app_env_cli.py +0 -0
  79. {thth-4.3.0 → thth-4.4.0}/tests/test_approval.py +0 -0
  80. {thth-4.3.0 → thth-4.4.0}/tests/test_approve_cmd.py +0 -0
  81. {thth-4.3.0 → thth-4.4.0}/tests/test_ask.py +0 -0
  82. {thth-4.3.0 → thth-4.4.0}/tests/test_assess.py +0 -0
  83. {thth-4.3.0 → thth-4.4.0}/tests/test_atlas_fifth_review.py +0 -0
  84. {thth-4.3.0 → thth-4.4.0}/tests/test_atlas_fourth_review.py +0 -0
  85. {thth-4.3.0 → thth-4.4.0}/tests/test_atlas_review.py +0 -0
  86. {thth-4.3.0 → thth-4.4.0}/tests/test_atlas_second_review.py +0 -0
  87. {thth-4.3.0 → thth-4.4.0}/tests/test_atlas_third_review.py +0 -0
  88. {thth-4.3.0 → thth-4.4.0}/tests/test_audit1_fixes.py +0 -0
  89. {thth-4.3.0 → thth-4.4.0}/tests/test_audit2_fixes.py +0 -0
  90. {thth-4.3.0 → thth-4.4.0}/tests/test_audit_b_counterexamples.py +0 -0
  91. {thth-4.3.0 → thth-4.4.0}/tests/test_audit_b_m1_semantics.py +0 -0
  92. {thth-4.3.0 → thth-4.4.0}/tests/test_audit_b_n1_depth.py +0 -0
  93. {thth-4.3.0 → thth-4.4.0}/tests/test_audit_b_r1_closure.py +0 -0
  94. {thth-4.3.0 → thth-4.4.0}/tests/test_audit_v201.py +0 -0
  95. {thth-4.3.0 → thth-4.4.0}/tests/test_audit_v23_details.py +0 -0
  96. {thth-4.3.0 → thth-4.4.0}/tests/test_auth.py +0 -0
  97. {thth-4.3.0 → thth-4.4.0}/tests/test_auth_identity_required.py +0 -0
  98. {thth-4.3.0 → thth-4.4.0}/tests/test_b17_remote_reads.py +0 -0
  99. {thth-4.3.0 → thth-4.4.0}/tests/test_behind_remote.py +0 -0
  100. {thth-4.3.0 → thth-4.4.0}/tests/test_bluesky_adapter.py +0 -0
  101. {thth-4.3.0 → thth-4.4.0}/tests/test_bluesky_secret_scrub.py +0 -0
  102. {thth-4.3.0 → thth-4.4.0}/tests/test_board_needs_review.py +0 -0
  103. {thth-4.3.0 → thth-4.4.0}/tests/test_board_running.py +0 -0
  104. {thth-4.3.0 → thth-4.4.0}/tests/test_branch_replies.py +0 -0
  105. {thth-4.3.0 → thth-4.4.0}/tests/test_bundle.py +0 -0
  106. {thth-4.3.0 → thth-4.4.0}/tests/test_bundle_not_type_mismatch.py +0 -0
  107. {thth-4.3.0 → thth-4.4.0}/tests/test_charcount.py +0 -0
  108. {thth-4.3.0 → thth-4.4.0}/tests/test_cli_entrypoint.py +0 -0
  109. {thth-4.3.0 → thth-4.4.0}/tests/test_cli_help.py +0 -0
  110. {thth-4.3.0 → thth-4.4.0}/tests/test_cli_vm_path.py +0 -0
  111. {thth-4.3.0 → thth-4.4.0}/tests/test_collect.py +0 -0
  112. {thth-4.3.0 → thth-4.4.0}/tests/test_collect_engagements.py +0 -0
  113. {thth-4.3.0 → thth-4.4.0}/tests/test_collect_runs.py +0 -0
  114. {thth-4.3.0 → thth-4.4.0}/tests/test_collect_sent.py +0 -0
  115. {thth-4.3.0 → thth-4.4.0}/tests/test_comparability.py +0 -0
  116. {thth-4.3.0 → thth-4.4.0}/tests/test_control_chars_refused.py +0 -0
  117. {thth-4.3.0 → thth-4.4.0}/tests/test_conversation_paging.py +0 -0
  118. {thth-4.3.0 → thth-4.4.0}/tests/test_count_for.py +0 -0
  119. {thth-4.3.0 → thth-4.4.0}/tests/test_docs_links.py +0 -0
  120. {thth-4.3.0 → thth-4.4.0}/tests/test_doctor.py +0 -0
  121. {thth-4.3.0 → thth-4.4.0}/tests/test_doctor_all_permissions.py +0 -0
  122. {thth-4.3.0 → thth-4.4.0}/tests/test_doctor_next_step.py +0 -0
  123. {thth-4.3.0 → thth-4.4.0}/tests/test_duplicate_front_matter_keys.py +0 -0
  124. {thth-4.3.0 → thth-4.4.0}/tests/test_editorial_audit.py +0 -0
  125. {thth-4.3.0 → thth-4.4.0}/tests/test_editorial_n3_n4.py +0 -0
  126. {thth-4.3.0 → thth-4.4.0}/tests/test_engagements.py +0 -0
  127. {thth-4.3.0 → thth-4.4.0}/tests/test_external_acceptance.py +0 -0
  128. {thth-4.3.0 → thth-4.4.0}/tests/test_fake_api.py +0 -0
  129. {thth-4.3.0 → thth-4.4.0}/tests/test_form_spec.py +0 -0
  130. {thth-4.3.0 → thth-4.4.0}/tests/test_form_spec_cli.py +0 -0
  131. {thth-4.3.0 → thth-4.4.0}/tests/test_forms.py +0 -0
  132. {thth-4.3.0 → thth-4.4.0}/tests/test_fresh_install.py +0 -0
  133. {thth-4.3.0 → thth-4.4.0}/tests/test_front_matter_injection.py +0 -0
  134. {thth-4.3.0 → thth-4.4.0}/tests/test_healthcheck_notifications.py +0 -0
  135. {thth-4.3.0 → thth-4.4.0}/tests/test_hypotheses.py +0 -0
  136. {thth-4.3.0 → thth-4.4.0}/tests/test_hypothesis_boundaries.py +0 -0
  137. {thth-4.3.0 → thth-4.4.0}/tests/test_hypothesis_units.py +0 -0
  138. {thth-4.3.0 → thth-4.4.0}/tests/test_impact_cli.py +0 -0
  139. {thth-4.3.0 → thth-4.4.0}/tests/test_inbox_granted_scopes.py +0 -0
  140. {thth-4.3.0 → thth-4.4.0}/tests/test_incident_notifications.py +0 -0
  141. {thth-4.3.0 → thth-4.4.0}/tests/test_independent_thread_review.py +0 -0
  142. {thth-4.3.0 → thth-4.4.0}/tests/test_independent_topic_review.py +0 -0
  143. {thth-4.3.0 → thth-4.4.0}/tests/test_independent_topic_review_r2.py +0 -0
  144. {thth-4.3.0 → thth-4.4.0}/tests/test_inflight.py +0 -0
  145. {thth-4.3.0 → thth-4.4.0}/tests/test_inflight_tag_body_hash.py +0 -0
  146. {thth-4.3.0 → thth-4.4.0}/tests/test_jst.py +0 -0
  147. {thth-4.3.0 → thth-4.4.0}/tests/test_limits_policy.py +0 -0
  148. {thth-4.3.0 → thth-4.4.0}/tests/test_limits_stage2.py +0 -0
  149. {thth-4.3.0 → thth-4.4.0}/tests/test_limits_stage3.py +0 -0
  150. {thth-4.3.0 → thth-4.4.0}/tests/test_limits_stage4.py +0 -0
  151. {thth-4.3.0 → thth-4.4.0}/tests/test_lint.py +0 -0
  152. {thth-4.3.0 → thth-4.4.0}/tests/test_list_projections.py +0 -0
  153. {thth-4.3.0 → thth-4.4.0}/tests/test_llm_trial.py +0 -0
  154. {thth-4.3.0 → thth-4.4.0}/tests/test_llm_trial_engage.py +0 -0
  155. {thth-4.3.0 → thth-4.4.0}/tests/test_lock.py +0 -0
  156. {thth-4.3.0 → thth-4.4.0}/tests/test_m2_independent.py +0 -0
  157. {thth-4.3.0 → thth-4.4.0}/tests/test_maintain.py +0 -0
  158. {thth-4.3.0 → thth-4.4.0}/tests/test_mastodon_adapter.py +0 -0
  159. {thth-4.3.0 → thth-4.4.0}/tests/test_mastodon_visibility.py +0 -0
  160. {thth-4.3.0 → thth-4.4.0}/tests/test_max_per_run.py +0 -0
  161. {thth-4.3.0 → thth-4.4.0}/tests/test_mcp.py +0 -0
  162. {thth-4.3.0 → thth-4.4.0}/tests/test_mcp_robustness.py +0 -0
  163. {thth-4.3.0 → thth-4.4.0}/tests/test_measured.py +0 -0
  164. {thth-4.3.0 → thth-4.4.0}/tests/test_media_accounts.py +0 -0
  165. {thth-4.3.0 → thth-4.4.0}/tests/test_media_next_steps.py +0 -0
  166. {thth-4.3.0 → thth-4.4.0}/tests/test_media_roundtrip.py +0 -0
  167. {thth-4.3.0 → thth-4.4.0}/tests/test_media_traits.py +0 -0
  168. {thth-4.3.0 → thth-4.4.0}/tests/test_mismatch_diagnostics.py +0 -0
  169. {thth-4.3.0 → thth-4.4.0}/tests/test_n5_reply_null_element.py +0 -0
  170. {thth-4.3.0 → thth-4.4.0}/tests/test_n6_replies_incomplete_ledger.py +0 -0
  171. {thth-4.3.0 → thth-4.4.0}/tests/test_n7_doctor_error_body.py +0 -0
  172. {thth-4.3.0 → thth-4.4.0}/tests/test_no_limits.py +0 -0
  173. {thth-4.3.0 → thth-4.4.0}/tests/test_observation_provenance.py +0 -0
  174. {thth-4.3.0 → thth-4.4.0}/tests/test_operations_handoff.py +0 -0
  175. {thth-4.3.0 → thth-4.4.0}/tests/test_packaging.py +0 -0
  176. {thth-4.3.0 → thth-4.4.0}/tests/test_preview.py +0 -0
  177. {thth-4.3.0 → thth-4.4.0}/tests/test_production_mode.py +0 -0
  178. {thth-4.3.0 → thth-4.4.0}/tests/test_pull_cli.py +0 -0
  179. {thth-4.3.0 → thth-4.4.0}/tests/test_pulse.py +0 -0
  180. {thth-4.3.0 → thth-4.4.0}/tests/test_queue.py +0 -0
  181. {thth-4.3.0 → thth-4.4.0}/tests/test_quiet_hours.py +0 -0
  182. {thth-4.3.0 → thth-4.4.0}/tests/test_r2_r4_r5.py +0 -0
  183. {thth-4.3.0 → thth-4.4.0}/tests/test_read_cli_bare_runtimeerror.py +0 -0
  184. {thth-4.3.0 → thth-4.4.0}/tests/test_reason_adoption.py +0 -0
  185. {thth-4.3.0 → thth-4.4.0}/tests/test_redact.py +0 -0
  186. {thth-4.3.0 → thth-4.4.0}/tests/test_redirect_token_leak.py +0 -0
  187. {thth-4.3.0 → thth-4.4.0}/tests/test_refresh.py +0 -0
  188. {thth-4.3.0 → thth-4.4.0}/tests/test_refresh_destination.py +0 -0
  189. {thth-4.3.0 → thth-4.4.0}/tests/test_rejudge_new.py +0 -0
  190. {thth-4.3.0 → thth-4.4.0}/tests/test_rejudge_r2_r3.py +0 -0
  191. {thth-4.3.0 → thth-4.4.0}/tests/test_replies.py +0 -0
  192. {thth-4.3.0 → thth-4.4.0}/tests/test_repo_lock.py +0 -0
  193. {thth-4.3.0 → thth-4.4.0}/tests/test_repo_sync.py +0 -0
  194. {thth-4.3.0 → thth-4.4.0}/tests/test_report_checkout_import.py +0 -0
  195. {thth-4.3.0 → thth-4.4.0}/tests/test_report_http.py +0 -0
  196. {thth-4.3.0 → thth-4.4.0}/tests/test_report_isolation.py +0 -0
  197. {thth-4.3.0 → thth-4.4.0}/tests/test_report_service.py +0 -0
  198. {thth-4.3.0 → thth-4.4.0}/tests/test_retract_already_gone.py +0 -0
  199. {thth-4.3.0 → thth-4.4.0}/tests/test_reverse_audit.py +0 -0
  200. {thth-4.3.0 → thth-4.4.0}/tests/test_review_case_id_cluster_m2.py +0 -0
  201. {thth-4.3.0 → thth-4.4.0}/tests/test_review_case_id_cluster_r2.py +0 -0
  202. {thth-4.3.0 → thth-4.4.0}/tests/test_review_cli.py +0 -0
  203. {thth-4.3.0 → thth-4.4.0}/tests/test_review_evidence_account.py +0 -0
  204. {thth-4.3.0 → thth-4.4.0}/tests/test_review_form_spec_and_case_id.py +0 -0
  205. {thth-4.3.0 → thth-4.4.0}/tests/test_review_provenance.py +0 -0
  206. {thth-4.3.0 → thth-4.4.0}/tests/test_review_records.py +0 -0
  207. {thth-4.3.0 → thth-4.4.0}/tests/test_revoke.py +0 -0
  208. {thth-4.3.0 → thth-4.4.0}/tests/test_roundtrip.py +0 -0
  209. {thth-4.3.0 → thth-4.4.0}/tests/test_run_missing_env.py +0 -0
  210. {thth-4.3.0 → thth-4.4.0}/tests/test_run_record_failclosed.py +0 -0
  211. {thth-4.3.0 → thth-4.4.0}/tests/test_runs_collected.py +0 -0
  212. {thth-4.3.0 → thth-4.4.0}/tests/test_runs_read_rows.py +0 -0
  213. {thth-4.3.0 → thth-4.4.0}/tests/test_schedule_multiple.py +0 -0
  214. {thth-4.3.0 → thth-4.4.0}/tests/test_schedule_view.py +0 -0
  215. {thth-4.3.0 → thth-4.4.0}/tests/test_scopes_source_requested.py +0 -0
  216. {thth-4.3.0 → thth-4.4.0}/tests/test_search_engage_targets.py +0 -0
  217. {thth-4.3.0 → thth-4.4.0}/tests/test_search_media_routes.py +0 -0
  218. {thth-4.3.0 → thth-4.4.0}/tests/test_secret_value_redact.py +0 -0
  219. {thth-4.3.0 → thth-4.4.0}/tests/test_secrets_grep.py +0 -0
  220. {thth-4.3.0 → thth-4.4.0}/tests/test_security_small_fixes.py +0 -0
  221. {thth-4.3.0 → thth-4.4.0}/tests/test_select.py +0 -0
  222. {thth-4.3.0 → thth-4.4.0}/tests/test_selfupdate.py +0 -0
  223. {thth-4.3.0 → thth-4.4.0}/tests/test_send.py +0 -0
  224. {thth-4.3.0 → thth-4.4.0}/tests/test_send_cli.py +0 -0
  225. {thth-4.3.0 → thth-4.4.0}/tests/test_send_control_chars.py +0 -0
  226. {thth-4.3.0 → thth-4.4.0}/tests/test_send_guard_cli.py +0 -0
  227. {thth-4.3.0 → thth-4.4.0}/tests/test_send_media.py +0 -0
  228. {thth-4.3.0 → thth-4.4.0}/tests/test_send_visibility.py +0 -0
  229. {thth-4.3.0 → thth-4.4.0}/tests/test_sent_integrity.py +0 -0
  230. {thth-4.3.0 → thth-4.4.0}/tests/test_server_json.py +0 -0
  231. {thth-4.3.0 → thth-4.4.0}/tests/test_site.py +0 -0
  232. {thth-4.3.0 → thth-4.4.0}/tests/test_skill_doc.py +0 -0
  233. {thth-4.3.0 → thth-4.4.0}/tests/test_study_report.py +0 -0
  234. {thth-4.3.0 → thth-4.4.0}/tests/test_systemd.py +0 -0
  235. {thth-4.3.0 → thth-4.4.0}/tests/test_thread_publish.py +0 -0
  236. {thth-4.3.0 → thth-4.4.0}/tests/test_thread_read.py +0 -0
  237. {thth-4.3.0 → thth-4.4.0}/tests/test_threadrun.py +0 -0
  238. {thth-4.3.0 → thth-4.4.0}/tests/test_threads_read_permissions.py +0 -0
  239. {thth-4.3.0 → thth-4.4.0}/tests/test_threadshape.py +0 -0
  240. {thth-4.3.0 → thth-4.4.0}/tests/test_thth_root.py +0 -0
  241. {thth-4.3.0 → thth-4.4.0}/tests/test_token_exists.py +0 -0
  242. {thth-4.3.0 → thth-4.4.0}/tests/test_token_scopes_record.py +0 -0
  243. {thth-4.3.0 → thth-4.4.0}/tests/test_token_set.py +0 -0
  244. {thth-4.3.0 → thth-4.4.0}/tests/test_topic.py +0 -0
  245. {thth-4.3.0 → thth-4.4.0}/tests/test_topic_advice.py +0 -0
  246. {thth-4.3.0 → thth-4.4.0}/tests/test_topic_cli.py +0 -0
  247. {thth-4.3.0 → thth-4.4.0}/tests/test_topic_models.py +0 -0
  248. {thth-4.3.0 → thth-4.4.0}/tests/test_topic_review_fixes.py +0 -0
  249. {thth-4.3.0 → thth-4.4.0}/tests/test_topic_shelf_observers.py +0 -0
  250. {thth-4.3.0 → thth-4.4.0}/tests/test_topic_store.py +0 -0
  251. {thth-4.3.0 → thth-4.4.0}/tests/test_topics_record_concurrent.py +0 -0
  252. {thth-4.3.0 → thth-4.4.0}/tests/test_topics_tool.py +0 -0
  253. {thth-4.3.0 → thth-4.4.0}/tests/test_trial_frictions.py +0 -0
  254. {thth-4.3.0 → thth-4.4.0}/tests/test_v1015_path_semantics.py +0 -0
  255. {thth-4.3.0 → thth-4.4.0}/tests/test_v1015_take_what_we_can.py +0 -0
  256. {thth-4.3.0 → thth-4.4.0}/tests/test_v210_admin_transaction.py +0 -0
  257. {thth-4.3.0 → thth-4.4.0}/tests/test_v210_approve_account.py +0 -0
  258. {thth-4.3.0 → thth-4.4.0}/tests/test_v210_json_contract.py +0 -0
  259. {thth-4.3.0 → thth-4.4.0}/tests/test_v210_length.py +0 -0
  260. {thth-4.3.0 → thth-4.4.0}/tests/test_v210_lock_wait.py +0 -0
  261. {thth-4.3.0 → thth-4.4.0}/tests/test_v210_mastodon_scopes.py +0 -0
  262. {thth-4.3.0 → thth-4.4.0}/tests/test_v210_mentions.py +0 -0
  263. {thth-4.3.0 → thth-4.4.0}/tests/test_v210_post_urls.py +0 -0
  264. {thth-4.3.0 → thth-4.4.0}/tests/test_v210_repo_paths.py +0 -0
  265. {thth-4.3.0 → thth-4.4.0}/tests/test_v210_reposts.py +0 -0
  266. {thth-4.3.0 → thth-4.4.0}/tests/test_v210_study_add.py +0 -0
  267. {thth-4.3.0 → thth-4.4.0}/tests/test_v210_tool_version.py +0 -0
  268. {thth-4.3.0 → thth-4.4.0}/tests/test_v210_unanswered.py +0 -0
  269. {thth-4.3.0 → thth-4.4.0}/tests/test_v210_unanswered_scope.py +0 -0
  270. {thth-4.3.0 → thth-4.4.0}/tests/test_v210_where_arguments.py +0 -0
  271. {thth-4.3.0 → thth-4.4.0}/tests/test_v210_where_filters.py +0 -0
  272. {thth-4.3.0 → thth-4.4.0}/tests/test_v210_who_arguments.py +0 -0
  273. {thth-4.3.0 → thth-4.4.0}/tests/test_v211_auth_integration.py +0 -0
  274. {thth-4.3.0 → thth-4.4.0}/tests/test_v211_authflow.py +0 -0
  275. {thth-4.3.0 → thth-4.4.0}/tests/test_v211_bluesky_stdin.py +0 -0
  276. {thth-4.3.0 → thth-4.4.0}/tests/test_v211_doctor_binding.py +0 -0
  277. {thth-4.3.0 → thth-4.4.0}/tests/test_v211_mastodon_auth.py +0 -0
  278. {thth-4.3.0 → thth-4.4.0}/tests/test_v211_x_auth.py +0 -0
  279. {thth-4.3.0 → thth-4.4.0}/tests/test_v212_ask_coordination.py +0 -0
  280. {thth-4.3.0 → thth-4.4.0}/tests/test_v212_budget.py +0 -0
  281. {thth-4.3.0 → thth-4.4.0}/tests/test_v212_budget_auth.py +0 -0
  282. {thth-4.3.0 → thth-4.4.0}/tests/test_v212_budget_mcp.py +0 -0
  283. {thth-4.3.0 → thth-4.4.0}/tests/test_v212_deletion.py +0 -0
  284. {thth-4.3.0 → thth-4.4.0}/tests/test_v212_leave.py +0 -0
  285. {thth-4.3.0 → thth-4.4.0}/tests/test_v212_leave_gate.py +0 -0
  286. {thth-4.3.0 → thth-4.4.0}/tests/test_v212_leave_providers.py +0 -0
  287. {thth-4.3.0 → thth-4.4.0}/tests/test_v212_leave_token_safety.py +0 -0
  288. {thth-4.3.0 → thth-4.4.0}/tests/test_v212_mastodon_guide.py +0 -0
  289. {thth-4.3.0 → thth-4.4.0}/tests/test_v212_mcp_compat.py +0 -0
  290. {thth-4.3.0 → thth-4.4.0}/tests/test_v212_missing_token.py +0 -0
  291. {thth-4.3.0 → thth-4.4.0}/tests/test_v212_read_coordination.py +0 -0
  292. {thth-4.3.0 → thth-4.4.0}/tests/test_v212_relay.py +0 -0
  293. {thth-4.3.0 → thth-4.4.0}/tests/test_v212_server_writes.py +0 -0
  294. {thth-4.3.0 → thth-4.4.0}/tests/test_v212_token_parent.py +0 -0
  295. {thth-4.3.0 → thth-4.4.0}/tests/test_v212_write_storage.py +0 -0
  296. {thth-4.3.0 → thth-4.4.0}/tests/test_v2131_send_dry_run.py +0 -0
  297. {thth-4.3.0 → thth-4.4.0}/tests/test_v2131_threads_publish_wait.py +0 -0
  298. {thth-4.3.0 → thth-4.4.0}/tests/test_v2132_send_inflight_hint.py +0 -0
  299. {thth-4.3.0 → thth-4.4.0}/tests/test_v2132_threads_create_timeout.py +0 -0
  300. {thth-4.3.0 → thth-4.4.0}/tests/test_v2132_threads_create_unknown.py +0 -0
  301. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_acceptance_fixes.py +0 -0
  302. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_adts_asf.py +0 -0
  303. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_attachment_kinds.py +0 -0
  304. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_bluesky_external.py +0 -0
  305. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_bluesky_media.py +0 -0
  306. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_bluesky_metadata.py +0 -0
  307. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_bluesky_metadata_uri.py +0 -0
  308. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_bluesky_metadata_video.py +0 -0
  309. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_bluesky_record.py +0 -0
  310. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_bluesky_record_video.py +0 -0
  311. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_bluesky_video.py +0 -0
  312. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_bluesky_webm_named.py +0 -0
  313. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_bmff_typed_metadata.py +0 -0
  314. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_budget_lock.py +0 -0
  315. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_capabilities.py +0 -0
  316. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_carryover_timeout.py +0 -0
  317. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_graphemes.py +0 -0
  318. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_mastodon_auth_media.py +0 -0
  319. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_mastodon_bmff_audio.py +0 -0
  320. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_mastodon_flac.py +0 -0
  321. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_mastodon_gif.py +0 -0
  322. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_mastodon_link.py +0 -0
  323. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_mastodon_media.py +0 -0
  324. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_mastodon_mp3.py +0 -0
  325. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_mastodon_ogg_opus.py +0 -0
  326. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_mastodon_ogg_vorbis.py +0 -0
  327. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_mastodon_poll.py +0 -0
  328. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_mastodon_primary_size_boundary.py +0 -0
  329. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_mastodon_quote.py +0 -0
  330. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_mastodon_thumbnail.py +0 -0
  331. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_mastodon_wav.py +0 -0
  332. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_mastodon_webm_audio.py +0 -0
  333. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_mastodon_webm_video.py +0 -0
  334. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_media_cleanup.py +0 -0
  335. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_media_formats.py +0 -0
  336. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_media_foundation.py +0 -0
  337. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_media_prepared.py +0 -0
  338. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_media_relay.py +0 -0
  339. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_media_relay_refusals.py +0 -0
  340. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_media_upload.py +0 -0
  341. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_review_followups.py +0 -0
  342. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_stage810_audit.py +0 -0
  343. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_static_diagnostics.py +0 -0
  344. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_stop_observation.py +0 -0
  345. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_systemd_worker.py +0 -0
  346. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_threads_ghost.py +0 -0
  347. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_threads_gif.py +0 -0
  348. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_threads_link.py +0 -0
  349. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_threads_media.py +0 -0
  350. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_threads_media_spoiler.py +0 -0
  351. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_threads_poll.py +0 -0
  352. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_threads_quote.py +0 -0
  353. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_threads_quote_media.py +0 -0
  354. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_threads_replies.py +0 -0
  355. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_threads_text.py +0 -0
  356. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_threads_text_spoiler.py +0 -0
  357. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_threads_video.py +0 -0
  358. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_transport_review.py +0 -0
  359. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_video_typed_metadata.py +0 -0
  360. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_vorbis_comment_boundary.py +0 -0
  361. {thth-4.3.0 → thth-4.4.0}/tests/test_v213_webm_schema_boundary.py +0 -0
  362. {thth-4.3.0 → thth-4.4.0}/tests/test_v2141_draft_lint_reason.py +0 -0
  363. {thth-4.3.0 → thth-4.4.0}/tests/test_v2141_managed_repo_umask.py +0 -0
  364. {thth-4.3.0 → thth-4.4.0}/tests/test_v214_release_signing.py +0 -0
  365. {thth-4.3.0 → thth-4.4.0}/tests/test_v214_x_adapter.py +0 -0
  366. {thth-4.3.0 → thth-4.4.0}/tests/test_v214_x_auth.py +0 -0
  367. {thth-4.3.0 → thth-4.4.0}/tests/test_v214_x_media.py +0 -0
  368. {thth-4.3.0 → thth-4.4.0}/tests/test_v214_x_posts_budget.py +0 -0
  369. {thth-4.3.0 → thth-4.4.0}/tests/test_v21b_approved_writes.py +0 -0
  370. {thth-4.3.0 → thth-4.4.0}/tests/test_v28_tags.py +0 -0
  371. {thth-4.3.0 → thth-4.4.0}/tests/test_v3100_docs.py +0 -0
  372. {thth-4.3.0 → thth-4.4.0}/tests/test_v3100_invite_admin_approval.py +0 -0
  373. {thth-4.3.0 → thth-4.4.0}/tests/test_v3100_invite_create.py +0 -0
  374. {thth-4.3.0 → thth-4.4.0}/tests/test_v3100_invite_credential.py +0 -0
  375. {thth-4.3.0 → thth-4.4.0}/tests/test_v3100_invite_limits.py +0 -0
  376. {thth-4.3.0 → thth-4.4.0}/tests/test_v3100_invite_worker.py +0 -0
  377. {thth-4.3.0 → thth-4.4.0}/tests/test_v310_morning.py +0 -0
  378. {thth-4.3.0 → thth-4.4.0}/tests/test_v310_search_timeout.py +0 -0
  379. {thth-4.3.0 → thth-4.4.0}/tests/test_v310_watch_words.py +0 -0
  380. {thth-4.3.0 → thth-4.4.0}/tests/test_v3111_observe_counts_only.py +0 -0
  381. {thth-4.3.0 → thth-4.4.0}/tests/test_v3111_observe_no_world.py +0 -0
  382. {thth-4.3.0 → thth-4.4.0}/tests/test_v311_leave_same_user.py +0 -0
  383. {thth-4.3.0 → thth-4.4.0}/tests/test_v311_mastodon_retract.py +0 -0
  384. {thth-4.3.0 → thth-4.4.0}/tests/test_v311_morning_overdue.py +0 -0
  385. {thth-4.3.0 → thth-4.4.0}/tests/test_v311_morning_own_exclusion.py +0 -0
  386. {thth-4.3.0 → thth-4.4.0}/tests/test_v311_morning_unanswered_window.py +0 -0
  387. {thth-4.3.0 → thth-4.4.0}/tests/test_v311_notifications_guidance.py +0 -0
  388. {thth-4.3.0 → thth-4.4.0}/tests/test_v311_replies_owned.py +0 -0
  389. {thth-4.3.0 → thth-4.4.0}/tests/test_v3120_activity.py +0 -0
  390. {thth-4.3.0 → thth-4.4.0}/tests/test_v3120_admin_write_reasons.py +0 -0
  391. {thth-4.3.0 → thth-4.4.0}/tests/test_v3120_guard.py +0 -0
  392. {thth-4.3.0 → thth-4.4.0}/tests/test_v3120_worker_version.py +0 -0
  393. {thth-4.3.0 → thth-4.4.0}/tests/test_v312_leave_shared_token.py +0 -0
  394. {thth-4.3.0 → thth-4.4.0}/tests/test_v312_own_handles_stopped.py +0 -0
  395. {thth-4.3.0 → thth-4.4.0}/tests/test_v312_report_admin.py +0 -0
  396. {thth-4.3.0 → thth-4.4.0}/tests/test_v312_report_channel.py +0 -0
  397. {thth-4.3.0 → thth-4.4.0}/tests/test_v312_report_inbox.py +0 -0
  398. {thth-4.3.0 → thth-4.4.0}/tests/test_v312_report_mcp_user.py +0 -0
  399. {thth-4.3.0 → thth-4.4.0}/tests/test_v3140_api_relay.py +0 -0
  400. {thth-4.3.0 → thth-4.4.0}/tests/test_v3140_invite_key.py +0 -0
  401. {thth-4.3.0 → thth-4.4.0}/tests/test_v3140_remote_cli.py +0 -0
  402. {thth-4.3.0 → thth-4.4.0}/tests/test_v3140_server_reads.py +0 -0
  403. {thth-4.3.0 → thth-4.4.0}/tests/test_v3142_browser_login.py +0 -0
  404. {thth-4.3.0 → thth-4.4.0}/tests/test_v3143_activity_push.py +0 -0
  405. {thth-4.3.0 → thth-4.4.0}/tests/test_v3143_cli_umask_leave.py +0 -0
  406. {thth-4.3.0 → thth-4.4.0}/tests/test_v3143_inflight_remote.py +0 -0
  407. {thth-4.3.0 → thth-4.4.0}/tests/test_v3143_managed_repo_recover.py +0 -0
  408. {thth-4.3.0 → thth-4.4.0}/tests/test_v315_reactions.py +0 -0
  409. {thth-4.3.0 → thth-4.4.0}/tests/test_v317_run_lock_wait.py +0 -0
  410. {thth-4.3.0 → thth-4.4.0}/tests/test_v3180_json_names.py +0 -0
  411. {thth-4.3.0 → thth-4.4.0}/tests/test_v3180_map_timer.py +0 -0
  412. {thth-4.3.0 → thth-4.4.0}/tests/test_v3180_where_filters.py +0 -0
  413. {thth-4.3.0 → thth-4.4.0}/tests/test_v3180_where_keep.py +0 -0
  414. {thth-4.3.0 → thth-4.4.0}/tests/test_v3192_publish_hold.py +0 -0
  415. {thth-4.3.0 → thth-4.4.0}/tests/test_v31_analysis.py +0 -0
  416. {thth-4.3.0 → thth-4.4.0}/tests/test_v3200_x_weighted_length.py +0 -0
  417. {thth-4.3.0 → thth-4.4.0}/tests/test_v320_reply_to_file_display.py +0 -0
  418. {thth-4.3.0 → thth-4.4.0}/tests/test_v320_reply_to_file_lint.py +0 -0
  419. {thth-4.3.0 → thth-4.4.0}/tests/test_v320_reply_to_file_resolve.py +0 -0
  420. {thth-4.3.0 → thth-4.4.0}/tests/test_v320_report_fixes.py +0 -0
  421. {thth-4.3.0 → thth-4.4.0}/tests/test_v3220_account_scheduled.py +0 -0
  422. {thth-4.3.0 → thth-4.4.0}/tests/test_v3220_bluesky_reply_control.py +0 -0
  423. {thth-4.3.0 → thth-4.4.0}/tests/test_v3220_bluesky_retract.py +0 -0
  424. {thth-4.3.0 → thth-4.4.0}/tests/test_v3220_invite_mastodon.py +0 -0
  425. {thth-4.3.0 → thth-4.4.0}/tests/test_v3220_mentions_inbox.py +0 -0
  426. {thth-4.3.0 → thth-4.4.0}/tests/test_v3250_burst_reply_queue.py +0 -0
  427. {thth-4.3.0 → thth-4.4.0}/tests/test_v330_fingerprint_version.py +0 -0
  428. {thth-4.3.0 → thth-4.4.0}/tests/test_v330_held.py +0 -0
  429. {thth-4.3.0 → thth-4.4.0}/tests/test_v330_held_names.py +0 -0
  430. {thth-4.3.0 → thth-4.4.0}/tests/test_v330_last_refusals.py +0 -0
  431. {thth-4.3.0 → thth-4.4.0}/tests/test_v330_notification_route.py +0 -0
  432. {thth-4.3.0 → thth-4.4.0}/tests/test_v330_observe.py +0 -0
  433. {thth-4.3.0 → thth-4.4.0}/tests/test_v330_report_friction.py +0 -0
  434. {thth-4.3.0 → thth-4.4.0}/tests/test_v330_report_words.py +0 -0
  435. {thth-4.3.0 → thth-4.4.0}/tests/test_v330_reports_effect.py +0 -0
  436. {thth-4.3.0 → thth-4.4.0}/tests/test_v331_inflight_cli.py +0 -0
  437. {thth-4.3.0 → thth-4.4.0}/tests/test_v331_listing_resolve.py +0 -0
  438. {thth-4.3.0 → thth-4.4.0}/tests/test_v331_outbox_retention.py +0 -0
  439. {thth-4.3.0 → thth-4.4.0}/tests/test_v331_publish_status.py +0 -0
  440. {thth-4.3.0 → thth-4.4.0}/tests/test_v331_self_resolve.py +0 -0
  441. {thth-4.3.0 → thth-4.4.0}/tests/test_v340_plaza_cli.py +0 -0
  442. {thth-4.3.0 → thth-4.4.0}/tests/test_v340_plaza_docs.py +0 -0
  443. {thth-4.3.0 → thth-4.4.0}/tests/test_v340_plaza_fields.py +0 -0
  444. {thth-4.3.0 → thth-4.4.0}/tests/test_v340_plaza_mcp.py +0 -0
  445. {thth-4.3.0 → thth-4.4.0}/tests/test_v340_plaza_morning.py +0 -0
  446. {thth-4.3.0 → thth-4.4.0}/tests/test_v340_plaza_observe.py +0 -0
  447. {thth-4.3.0 → thth-4.4.0}/tests/test_v340_plaza_open.py +0 -0
  448. {thth-4.3.0 → thth-4.4.0}/tests/test_v340_plaza_open_confirm.py +0 -0
  449. {thth-4.3.0 → thth-4.4.0}/tests/test_v340_plaza_reply.py +0 -0
  450. {thth-4.3.0 → thth-4.4.0}/tests/test_v340_plaza_store.py +0 -0
  451. {thth-4.3.0 → thth-4.4.0}/tests/test_v350_map_docs.py +0 -0
  452. {thth-4.3.0 → thth-4.4.0}/tests/test_v350_map_nodes.py +0 -0
  453. {thth-4.3.0 → thth-4.4.0}/tests/test_v350_map_observe.py +0 -0
  454. {thth-4.3.0 → thth-4.4.0}/tests/test_v350_map_plaza.py +0 -0
  455. {thth-4.3.0 → thth-4.4.0}/tests/test_v350_map_retention.py +0 -0
  456. {thth-4.3.0 → thth-4.4.0}/tests/test_v350_map_self.py +0 -0
  457. {thth-4.3.0 → thth-4.4.0}/tests/test_v350_map_show.py +0 -0
  458. {thth-4.3.0 → thth-4.4.0}/tests/test_v350_map_world.py +0 -0
  459. {thth-4.3.0 → thth-4.4.0}/tests/test_v351_leave_timers.py +0 -0
  460. {thth-4.3.0 → thth-4.4.0}/tests/test_v351_plaza_open_holes.py +0 -0
  461. {thth-4.3.0 → thth-4.4.0}/tests/test_v351_who_owned.py +0 -0
  462. {thth-4.3.0 → thth-4.4.0}/tests/test_v360_goal_analytics.py +0 -0
  463. {thth-4.3.0 → thth-4.4.0}/tests/test_v360_goal_docs.py +0 -0
  464. {thth-4.3.0 → thth-4.4.0}/tests/test_v360_goal_observe_map.py +0 -0
  465. {thth-4.3.0 → thth-4.4.0}/tests/test_v360_goal_plaza.py +0 -0
  466. {thth-4.3.0 → thth-4.4.0}/tests/test_v360_goal_record.py +0 -0
  467. {thth-4.3.0 → thth-4.4.0}/tests/test_v360_goal_send.py +0 -0
  468. {thth-4.3.0 → thth-4.4.0}/tests/test_v360_report_timeline.py +0 -0
  469. {thth-4.3.0 → thth-4.4.0}/tests/test_v370_account_self_healing.py +0 -0
  470. {thth-4.3.0 → thth-4.4.0}/tests/test_v370_approve_pending.py +0 -0
  471. {thth-4.3.0 → thth-4.4.0}/tests/test_v370_click_attribution.py +0 -0
  472. {thth-4.3.0 → thth-4.4.0}/tests/test_v370_collection_stale_basis.py +0 -0
  473. {thth-4.3.0 → thth-4.4.0}/tests/test_v370_docs.py +0 -0
  474. {thth-4.3.0 → thth-4.4.0}/tests/test_v370_follow_comparison.py +0 -0
  475. {thth-4.3.0 → thth-4.4.0}/tests/test_v370_local_path.py +0 -0
  476. {thth-4.3.0 → thth-4.4.0}/tests/test_v370_weekly_goals.py +0 -0
  477. {thth-4.3.0 → thth-4.4.0}/tests/test_v370_where_aggregate.py +0 -0
  478. {thth-4.3.0 → thth-4.4.0}/tests/test_v370_where_also.py +0 -0
  479. {thth-4.3.0 → thth-4.4.0}/tests/test_v370_where_zero.py +0 -0
  480. {thth-4.3.0 → thth-4.4.0}/tests/test_v380_docs.py +0 -0
  481. {thth-4.3.0 → thth-4.4.0}/tests/test_v380_plaza_digest.py +0 -0
  482. {thth-4.3.0 → thth-4.4.0}/tests/test_v380_plaza_from.py +0 -0
  483. {thth-4.3.0 → thth-4.4.0}/tests/test_v380_plaza_mcp.py +0 -0
  484. {thth-4.3.0 → thth-4.4.0}/tests/test_v380_plaza_moments.py +0 -0
  485. {thth-4.3.0 → thth-4.4.0}/tests/test_v380_plaza_owner.py +0 -0
  486. {thth-4.3.0 → thth-4.4.0}/tests/test_v380_plaza_received.py +0 -0
  487. {thth-4.3.0 → thth-4.4.0}/tests/test_v380_plaza_triggers.py +0 -0
  488. {thth-4.3.0 → thth-4.4.0}/tests/test_v381_plaza_visibility_default.py +0 -0
  489. {thth-4.3.0 → thth-4.4.0}/tests/test_v382_confirm_file.py +0 -0
  490. {thth-4.3.0 → thth-4.4.0}/tests/test_v382_first_stage_not_refusal.py +0 -0
  491. {thth-4.3.0 → thth-4.4.0}/tests/test_v382_reader_lock_and_push_pending.py +0 -0
  492. {thth-4.3.0 → thth-4.4.0}/tests/test_v382_sync_without_fetch_head.py +0 -0
  493. {thth-4.3.0 → thth-4.4.0}/tests/test_v390_docs.py +0 -0
  494. {thth-4.3.0 → thth-4.4.0}/tests/test_v390_per_post_clicks.py +0 -0
  495. {thth-4.3.0 → thth-4.4.0}/tests/test_v390_where_rate.py +0 -0
  496. {thth-4.3.0 → thth-4.4.0}/tests/test_v390_where_suggest.py +0 -0
  497. {thth-4.3.0 → thth-4.4.0}/tests/test_v391_plaza_text_numbers.py +0 -0
  498. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_jikki_1017.py +0 -0
  499. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_jikki_1017_bookmark.py +0 -0
  500. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s1_approve.py +0 -0
  501. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s1_deliveries.py +0 -0
  502. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s1_events.py +0 -0
  503. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s1_fingerprints.py +0 -0
  504. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s1_guard.py +0 -0
  505. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s1_locks.py +0 -0
  506. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s1_principals.py +0 -0
  507. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s1_publish.py +0 -0
  508. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s1_remote.py +0 -0
  509. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s2_adapter.py +0 -0
  510. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s2_audit.py +0 -0
  511. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s2_audit_fixes.py +0 -0
  512. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s2_commit.py +0 -0
  513. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s2_convert.py +0 -0
  514. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s2_held.py +0 -0
  515. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s2_oauth1.py +0 -0
  516. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s2_principal_auth.py +0 -0
  517. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s2_syntax_readback.py +0 -0
  518. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s3_adapter.py +0 -0
  519. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s3_convert.py +0 -0
  520. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s3_flow.py +0 -0
  521. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s4_announce.py +0 -0
  522. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s4_audit_fixes.py +0 -0
  523. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s4_bookmark.py +0 -0
  524. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s4_ledger.py +0 -0
  525. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s5_audit_fixes.py +0 -0
  526. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s5_conflict.py +0 -0
  527. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s5_detect.py +0 -0
  528. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s5_guard_remote.py +0 -0
  529. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s5_jikki.py +0 -0
  530. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s6_audit_fixes.py +0 -0
  531. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s6_bundle.py +0 -0
  532. {thth-4.3.0 → thth-4.4.0}/tests/test_v400_s6_collect.py +0 -0
  533. {thth-4.3.0 → thth-4.4.0}/tests/test_v401_board_hatena_token.py +0 -0
  534. {thth-4.3.0 → thth-4.4.0}/tests/test_v401_bookmark_burst.py +0 -0
  535. {thth-4.3.0 → thth-4.4.0}/tests/test_v40_contract_json.py +0 -0
  536. {thth-4.3.0 → thth-4.4.0}/tests/test_v410_audit_night.py +0 -0
  537. {thth-4.3.0 → thth-4.4.0}/tests/test_v410_bookmark_batch.py +0 -0
  538. {thth-4.3.0 → thth-4.4.0}/tests/test_v411_batch_cleanup.py +0 -0
  539. {thth-4.3.0 → thth-4.4.0}/tests/test_v420_bookmark_edit.py +0 -0
  540. {thth-4.3.0 → thth-4.4.0}/tests/test_v420_bookmark_tag_bytes.py +0 -0
  541. {thth-4.3.0 → thth-4.4.0}/tests/test_v421_audit_fixes.py +0 -0
  542. {thth-4.3.0 → thth-4.4.0}/tests/test_v422_bookmark_rejections.py +0 -0
  543. {thth-4.3.0 → thth-4.4.0}/tests/test_v422_sparse_checkout.py +0 -0
  544. {thth-4.3.0 → thth-4.4.0}/tests/test_v430_reply_batch.py +0 -0
  545. {thth-4.3.0 → thth-4.4.0}/tests/test_value_domains_shown.py +0 -0
  546. {thth-4.3.0 → thth-4.4.0}/tests/test_version.py +0 -0
  547. {thth-4.3.0 → thth-4.4.0}/tests/test_where_cli.py +0 -0
  548. {thth-4.3.0 → thth-4.4.0}/tests/test_where_threads_tag_author.py +0 -0
  549. {thth-4.3.0 → thth-4.4.0}/tests/test_who_cli.py +0 -0
  550. {thth-4.3.0 → thth-4.4.0}/tests/test_writeback_rebase.py +0 -0
  551. {thth-4.3.0 → thth-4.4.0}/tests/test_writeback_validate.py +0 -0
  552. {thth-4.3.0 → thth-4.4.0}/tests/test_x_community.py +0 -0
  553. {thth-4.3.0 → thth-4.4.0}/tests/test_x_list.py +0 -0
  554. {thth-4.3.0 → thth-4.4.0}/tests/test_x_metrics.py +0 -0
  555. {thth-4.3.0 → thth-4.4.0}/tests/test_x_owned_metrics.py +0 -0
  556. {thth-4.3.0 → thth-4.4.0}/tests/test_x_search_thread.py +0 -0
  557. {thth-4.3.0 → thth-4.4.0}/tests/test_x_token_autorefresh.py +0 -0
  558. {thth-4.3.0 → thth-4.4.0}/tests/test_x_trends_counts.py +0 -0
  559. {thth-4.3.0 → thth-4.4.0}/thth/__init__.py +0 -0
  560. {thth-4.3.0 → thth-4.4.0}/thth/__main__.py +0 -0
  561. {thth-4.3.0 → thth-4.4.0}/thth/_grapheme_data.py +0 -0
  562. {thth-4.3.0 → thth-4.4.0}/thth/account_cli.py +0 -0
  563. {thth-4.3.0 → thth-4.4.0}/thth/account_report.py +0 -0
  564. {thth-4.3.0 → thth-4.4.0}/thth/account_settings.py +0 -0
  565. {thth-4.3.0 → thth-4.4.0}/thth/activity.py +0 -0
  566. {thth-4.3.0 → thth-4.4.0}/thth/adapters/__init__.py +0 -0
  567. {thth-4.3.0 → thth-4.4.0}/thth/adapters/_x_text_data.py +0 -0
  568. {thth-4.3.0 → thth-4.4.0}/thth/adapters/auth_mastodon.py +0 -0
  569. {thth-4.3.0 → thth-4.4.0}/thth/adapters/auth_threads.py +0 -0
  570. {thth-4.3.0 → thth-4.4.0}/thth/adapters/auth_x.py +0 -0
  571. {thth-4.3.0 → thth-4.4.0}/thth/adapters/base.py +0 -0
  572. {thth-4.3.0 → thth-4.4.0}/thth/adapters/bluesky.py +0 -0
  573. {thth-4.3.0 → thth-4.4.0}/thth/adapters/bluesky_media.py +0 -0
  574. {thth-4.3.0 → thth-4.4.0}/thth/adapters/bluesky_video.py +0 -0
  575. {thth-4.3.0 → thth-4.4.0}/thth/adapters/fotolife.py +0 -0
  576. {thth-4.3.0 → thth-4.4.0}/thth/adapters/hatena_common.py +0 -0
  577. {thth-4.3.0 → thth-4.4.0}/thth/adapters/hatenablog.py +0 -0
  578. {thth-4.3.0 → thth-4.4.0}/thth/adapters/hatenabookmark.py +0 -0
  579. {thth-4.3.0 → thth-4.4.0}/thth/adapters/hatenastar.py +0 -0
  580. {thth-4.3.0 → thth-4.4.0}/thth/adapters/mastodon.py +0 -0
  581. {thth-4.3.0 → thth-4.4.0}/thth/adapters/mastodon_media.py +0 -0
  582. {thth-4.3.0 → thth-4.4.0}/thth/adapters/threads.py +0 -0
  583. {thth-4.3.0 → thth-4.4.0}/thth/adapters/threads_media.py +0 -0
  584. {thth-4.3.0 → thth-4.4.0}/thth/adapters/x.py +0 -0
  585. {thth-4.3.0 → thth-4.4.0}/thth/adapters/x_media.py +0 -0
  586. {thth-4.3.0 → thth-4.4.0}/thth/adapters/x_text.py +0 -0
  587. {thth-4.3.0 → thth-4.4.0}/thth/admin_diagnose.py +0 -0
  588. {thth-4.3.0 → thth-4.4.0}/thth/admin_log.py +0 -0
  589. {thth-4.3.0 → thth-4.4.0}/thth/admin_notifications.py +0 -0
  590. {thth-4.3.0 → thth-4.4.0}/thth/admin_report.py +0 -0
  591. {thth-4.3.0 → thth-4.4.0}/thth/advise.py +0 -0
  592. {thth-4.3.0 → thth-4.4.0}/thth/after_cli.py +0 -0
  593. {thth-4.3.0 → thth-4.4.0}/thth/analytics_clicks.py +0 -0
  594. {thth-4.3.0 → thth-4.4.0}/thth/analytics_comparison.py +0 -0
  595. {thth-4.3.0 → thth-4.4.0}/thth/analytics_goals.py +0 -0
  596. {thth-4.3.0 → thth-4.4.0}/thth/analytics_report.py +0 -0
  597. {thth-4.3.0 → thth-4.4.0}/thth/analytics_shapes.py +0 -0
  598. {thth-4.3.0 → thth-4.4.0}/thth/analytics_threads.py +0 -0
  599. {thth-4.3.0 → thth-4.4.0}/thth/analytics_weekly.py +0 -0
  600. {thth-4.3.0 → thth-4.4.0}/thth/announce.py +0 -0
  601. {thth-4.3.0 → thth-4.4.0}/thth/api_diagnostic.py +0 -0
  602. {thth-4.3.0 → thth-4.4.0}/thth/api_requests.py +0 -0
  603. {thth-4.3.0 → thth-4.4.0}/thth/appconfig.py +0 -0
  604. {thth-4.3.0 → thth-4.4.0}/thth/appenv.py +0 -0
  605. {thth-4.3.0 → thth-4.4.0}/thth/approval.py +0 -0
  606. {thth-4.3.0 → thth-4.4.0}/thth/approve_pending.py +0 -0
  607. {thth-4.3.0 → thth-4.4.0}/thth/article_cli.py +0 -0
  608. {thth-4.3.0 → thth-4.4.0}/thth/article_reactions.py +0 -0
  609. {thth-4.3.0 → thth-4.4.0}/thth/ask.py +0 -0
  610. {thth-4.3.0 → thth-4.4.0}/thth/ask_cli.py +0 -0
  611. {thth-4.3.0 → thth-4.4.0}/thth/assess.py +0 -0
  612. {thth-4.3.0 → thth-4.4.0}/thth/audioformats.py +0 -0
  613. {thth-4.3.0 → thth-4.4.0}/thth/authclients.py +0 -0
  614. {thth-4.3.0 → thth-4.4.0}/thth/authflow.py +0 -0
  615. {thth-4.3.0 → thth-4.4.0}/thth/authpassword.py +0 -0
  616. {thth-4.3.0 → thth-4.4.0}/thth/bluesky_metadata.py +0 -0
  617. {thth-4.3.0 → thth-4.4.0}/thth/bookmark_batch.py +0 -0
  618. {thth-4.3.0 → thth-4.4.0}/thth/bookmark_cli.py +0 -0
  619. {thth-4.3.0 → thth-4.4.0}/thth/budget_x.py +0 -0
  620. {thth-4.3.0 → thth-4.4.0}/thth/budget_x_posts.py +0 -0
  621. {thth-4.3.0 → thth-4.4.0}/thth/bundle.py +0 -0
  622. {thth-4.3.0 → thth-4.4.0}/thth/burst_queue.py +0 -0
  623. {thth-4.3.0 → thth-4.4.0}/thth/cli.py +0 -0
  624. {thth-4.3.0 → thth-4.4.0}/thth/click_attribution.py +0 -0
  625. {thth-4.3.0 → thth-4.4.0}/thth/collection_context.py +0 -0
  626. {thth-4.3.0 → thth-4.4.0}/thth/collection_status.py +0 -0
  627. {thth-4.3.0 → thth-4.4.0}/thth/community_cli.py +0 -0
  628. {thth-4.3.0 → thth-4.4.0}/thth/conflict_cli.py +0 -0
  629. {thth-4.3.0 → thth-4.4.0}/thth/deletion.py +0 -0
  630. {thth-4.3.0 → thth-4.4.0}/thth/deliveries.py +0 -0
  631. {thth-4.3.0 → thth-4.4.0}/thth/deliveries_cli.py +0 -0
  632. {thth-4.3.0 → thth-4.4.0}/thth/doctor.py +0 -0
  633. {thth-4.3.0 → thth-4.4.0}/thth/ebmlaudio.py +0 -0
  634. {thth-4.3.0 → thth-4.4.0}/thth/ebmlvideo.py +0 -0
  635. {thth-4.3.0 → thth-4.4.0}/thth/engagements.py +0 -0
  636. {thth-4.3.0 → thth-4.4.0}/thth/events_cli.py +0 -0
  637. {thth-4.3.0 → thth-4.4.0}/thth/fingerprint_golden.py +0 -0
  638. {thth-4.3.0 → thth-4.4.0}/thth/flacformats.py +0 -0
  639. {thth-4.3.0 → thth-4.4.0}/thth/forms.py +0 -0
  640. {thth-4.3.0 → thth-4.4.0}/thth/fotolife_cli.py +0 -0
  641. {thth-4.3.0 → thth-4.4.0}/thth/goals.py +0 -0
  642. {thth-4.3.0 → thth-4.4.0}/thth/graphemes.py +0 -0
  643. {thth-4.3.0 → thth-4.4.0}/thth/guard.py +0 -0
  644. {thth-4.3.0 → thth-4.4.0}/thth/handoff_cursor.py +0 -0
  645. {thth-4.3.0 → thth-4.4.0}/thth/hatena_bookmark.py +0 -0
  646. {thth-4.3.0 → thth-4.4.0}/thth/hatena_cli.py +0 -0
  647. {thth-4.3.0 → thth-4.4.0}/thth/hatena_conflict.py +0 -0
  648. {thth-4.3.0 → thth-4.4.0}/thth/hatena_convert.py +0 -0
  649. {thth-4.3.0 → thth-4.4.0}/thth/hatena_entry.py +0 -0
  650. {thth-4.3.0 → thth-4.4.0}/thth/hatena_fotolife.py +0 -0
  651. {thth-4.3.0 → thth-4.4.0}/thth/hatena_publish.py +0 -0
  652. {thth-4.3.0 → thth-4.4.0}/thth/hatena_reactions.py +0 -0
  653. {thth-4.3.0 → thth-4.4.0}/thth/healthcheck.py +0 -0
  654. {thth-4.3.0 → thth-4.4.0}/thth/httpsafe.py +0 -0
  655. {thth-4.3.0 → thth-4.4.0}/thth/incident.py +0 -0
  656. {thth-4.3.0 → thth-4.4.0}/thth/inflight.py +0 -0
  657. {thth-4.3.0 → thth-4.4.0}/thth/inflight_cli.py +0 -0
  658. {thth-4.3.0 → thth-4.4.0}/thth/inflight_resolve.py +0 -0
  659. {thth-4.3.0 → thth-4.4.0}/thth/invites.py +0 -0
  660. {thth-4.3.0 → thth-4.4.0}/thth/jst.py +0 -0
  661. {thth-4.3.0 → thth-4.4.0}/thth/leave.py +0 -0
  662. {thth-4.3.0 → thth-4.4.0}/thth/leave_gate.py +0 -0
  663. {thth-4.3.0 → thth-4.4.0}/thth/limits_config.py +0 -0
  664. {thth-4.3.0 → thth-4.4.0}/thth/limits_policy.py +0 -0
  665. {thth-4.3.0 → thth-4.4.0}/thth/limits_view.py +0 -0
  666. {thth-4.3.0 → thth-4.4.0}/thth/lint.py +0 -0
  667. {thth-4.3.0 → thth-4.4.0}/thth/list_cli.py +0 -0
  668. {thth-4.3.0 → thth-4.4.0}/thth/lock.py +0 -0
  669. {thth-4.3.0 → thth-4.4.0}/thth/maintain.py +0 -0
  670. {thth-4.3.0 → thth-4.4.0}/thth/managed_repo.py +0 -0
  671. {thth-4.3.0 → thth-4.4.0}/thth/map_cli.py +0 -0
  672. {thth-4.3.0 → thth-4.4.0}/thth/map_store.py +0 -0
  673. {thth-4.3.0 → thth-4.4.0}/thth/map_view.py +0 -0
  674. {thth-4.3.0 → thth-4.4.0}/thth/map_world.py +0 -0
  675. {thth-4.3.0 → thth-4.4.0}/thth/measured.py +0 -0
  676. {thth-4.3.0 → thth-4.4.0}/thth/media.py +0 -0
  677. {thth-4.3.0 → thth-4.4.0}/thth/media_capabilities.py +0 -0
  678. {thth-4.3.0 → thth-4.4.0}/thth/media_cleanup.py +0 -0
  679. {thth-4.3.0 → thth-4.4.0}/thth/media_cli.py +0 -0
  680. {thth-4.3.0 → thth-4.4.0}/thth/media_delivery.py +0 -0
  681. {thth-4.3.0 → thth-4.4.0}/thth/media_observations.py +0 -0
  682. {thth-4.3.0 → thth-4.4.0}/thth/media_relay.py +0 -0
  683. {thth-4.3.0 → thth-4.4.0}/thth/media_traits/bluesky.json +0 -0
  684. {thth-4.3.0 → thth-4.4.0}/thth/media_traits/fotolife.json +0 -0
  685. {thth-4.3.0 → thth-4.4.0}/thth/media_traits/hatenablog.json +0 -0
  686. {thth-4.3.0 → thth-4.4.0}/thth/media_traits/hatenabookmark.json +0 -0
  687. {thth-4.3.0 → thth-4.4.0}/thth/media_traits/mastodon.json +0 -0
  688. {thth-4.3.0 → thth-4.4.0}/thth/media_traits/misskey.json +0 -0
  689. {thth-4.3.0 → thth-4.4.0}/thth/media_traits/threads.json +0 -0
  690. {thth-4.3.0 → thth-4.4.0}/thth/media_traits/x.json +0 -0
  691. {thth-4.3.0 → thth-4.4.0}/thth/media_traits.py +0 -0
  692. {thth-4.3.0 → thth-4.4.0}/thth/media_uploads.py +0 -0
  693. {thth-4.3.0 → thth-4.4.0}/thth/mediaformats.py +0 -0
  694. {thth-4.3.0 → thth-4.4.0}/thth/morning.py +0 -0
  695. {thth-4.3.0 → thth-4.4.0}/thth/mpeg_audio.py +0 -0
  696. {thth-4.3.0 → thth-4.4.0}/thth/notification_route.py +0 -0
  697. {thth-4.3.0 → thth-4.4.0}/thth/oauth.py +0 -0
  698. {thth-4.3.0 → thth-4.4.0}/thth/oauth1.py +0 -0
  699. {thth-4.3.0 → thth-4.4.0}/thth/oggformats.py +0 -0
  700. {thth-4.3.0 → thth-4.4.0}/thth/operations_handoff.py +0 -0
  701. {thth-4.3.0 → thth-4.4.0}/thth/plaza.py +0 -0
  702. {thth-4.3.0 → thth-4.4.0}/thth/plaza_cli.py +0 -0
  703. {thth-4.3.0 → thth-4.4.0}/thth/plaza_from.py +0 -0
  704. {thth-4.3.0 → thth-4.4.0}/thth/plaza_moments.py +0 -0
  705. {thth-4.3.0 → thth-4.4.0}/thth/plaza_observe.py +0 -0
  706. {thth-4.3.0 → thth-4.4.0}/thth/plaza_reads.py +0 -0
  707. {thth-4.3.0 → thth-4.4.0}/thth/plaza_redact.py +0 -0
  708. {thth-4.3.0 → thth-4.4.0}/thth/postid.py +0 -0
  709. {thth-4.3.0 → thth-4.4.0}/thth/principal_cli.py +0 -0
  710. {thth-4.3.0 → thth-4.4.0}/thth/principals.py +0 -0
  711. {thth-4.3.0 → thth-4.4.0}/thth/private_store.py +0 -0
  712. {thth-4.3.0 → thth-4.4.0}/thth/publish_hold.py +0 -0
  713. {thth-4.3.0 → thth-4.4.0}/thth/pulse.py +0 -0
  714. {thth-4.3.0 → thth-4.4.0}/thth/queuefile.py +0 -0
  715. {thth-4.3.0 → thth-4.4.0}/thth/reaction_cli.py +0 -0
  716. {thth-4.3.0 → thth-4.4.0}/thth/reactions.py +0 -0
  717. {thth-4.3.0 → thth-4.4.0}/thth/read_coordination.py +0 -0
  718. {thth-4.3.0 → thth-4.4.0}/thth/read_window.py +0 -0
  719. {thth-4.3.0 → thth-4.4.0}/thth/redact.py +0 -0
  720. {thth-4.3.0 → thth-4.4.0}/thth/refusals.py +0 -0
  721. {thth-4.3.0 → thth-4.4.0}/thth/relay.py +0 -0
  722. {thth-4.3.0 → thth-4.4.0}/thth/remote.py +0 -0
  723. {thth-4.3.0 → thth-4.4.0}/thth/replies.py +0 -0
  724. {thth-4.3.0 → thth-4.4.0}/thth/report.py +0 -0
  725. {thth-4.3.0 → thth-4.4.0}/thth/report_details.py +0 -0
  726. {thth-4.3.0 → thth-4.4.0}/thth/report_http.py +0 -0
  727. {thth-4.3.0 → thth-4.4.0}/thth/report_inbox.py +0 -0
  728. {thth-4.3.0 → thth-4.4.0}/thth/report_isolation.py +0 -0
  729. {thth-4.3.0 → thth-4.4.0}/thth/report_service.py +0 -0
  730. {thth-4.3.0 → thth-4.4.0}/thth/report_timeline.py +0 -0
  731. {thth-4.3.0 → thth-4.4.0}/thth/retract_cli.py +0 -0
  732. {thth-4.3.0 → thth-4.4.0}/thth/runs.py +0 -0
  733. {thth-4.3.0 → thth-4.4.0}/thth/scopes.py +0 -0
  734. {thth-4.3.0 → thth-4.4.0}/thth/secrets_fs.py +0 -0
  735. {thth-4.3.0 → thth-4.4.0}/thth/select.py +0 -0
  736. {thth-4.3.0 → thth-4.4.0}/thth/selfupdate.py +0 -0
  737. {thth-4.3.0 → thth-4.4.0}/thth/sent.py +0 -0
  738. {thth-4.3.0 → thth-4.4.0}/thth/server_files.py +0 -0
  739. {thth-4.3.0 → thth-4.4.0}/thth/server_reads.py +0 -0
  740. {thth-4.3.0 → thth-4.4.0}/thth/server_writes.py +0 -0
  741. {thth-4.3.0 → thth-4.4.0}/thth/stop_observation.py +0 -0
  742. {thth-4.3.0 → thth-4.4.0}/thth/study_cli.py +0 -0
  743. {thth-4.3.0 → thth-4.4.0}/thth/study_report.py +0 -0
  744. {thth-4.3.0 → thth-4.4.0}/thth/summary_cli.py +0 -0
  745. {thth-4.3.0 → thth-4.4.0}/thth/systemd_gen.py +0 -0
  746. {thth-4.3.0 → thth-4.4.0}/thth/tags.py +0 -0
  747. {thth-4.3.0 → thth-4.4.0}/thth/thread_read.py +0 -0
  748. {thth-4.3.0 → thth-4.4.0}/thth/threadrun.py +0 -0
  749. {thth-4.3.0 → thth-4.4.0}/thth/threads_read_cli.py +0 -0
  750. {thth-4.3.0 → thth-4.4.0}/thth/threadshape.py +0 -0
  751. {thth-4.3.0 → thth-4.4.0}/thth/threadthrow.py +0 -0
  752. {thth-4.3.0 → thth-4.4.0}/thth/timer_cleanup.py +0 -0
  753. {thth-4.3.0 → thth-4.4.0}/thth/tool_version.py +0 -0
  754. {thth-4.3.0 → thth-4.4.0}/thth/topic_advice.py +0 -0
  755. {thth-4.3.0 → thth-4.4.0}/thth/topic_cli.py +0 -0
  756. {thth-4.3.0 → thth-4.4.0}/thth/topic_models.py +0 -0
  757. {thth-4.3.0 → thth-4.4.0}/thth/topic_store.py +0 -0
  758. {thth-4.3.0 → thth-4.4.0}/thth/topics.py +0 -0
  759. {thth-4.3.0 → thth-4.4.0}/thth/trends_cli.py +0 -0
  760. {thth-4.3.0 → thth-4.4.0}/thth/unanswered.py +0 -0
  761. {thth-4.3.0 → thth-4.4.0}/thth/vorbisformats.py +0 -0
  762. {thth-4.3.0 → thth-4.4.0}/thth/watch_cli.py +0 -0
  763. {thth-4.3.0 → thth-4.4.0}/thth/where_cli.py +0 -0
  764. {thth-4.3.0 → thth-4.4.0}/thth/where_keep.py +0 -0
  765. {thth-4.3.0 → thth-4.4.0}/thth/where_suggest.py +0 -0
  766. {thth-4.3.0 → thth-4.4.0}/thth/who_cli.py +0 -0
  767. {thth-4.3.0 → thth-4.4.0}/thth/worker.py +0 -0
  768. {thth-4.3.0 → thth-4.4.0}/thth/worker_version.py +0 -0
  769. {thth-4.3.0 → thth-4.4.0}/thth/writeback.py +0 -0
  770. {thth-4.3.0 → thth-4.4.0}/thth/x_metrics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: thth
3
- Version: 4.3.0
3
+ Version: 4.4.0
4
4
  Summary: 承認済みの投稿だけを投げる門(Threads・Bluesky・Mastodon)。人が承認し、記録は利用者の git に残る。
5
5
  Project-URL: Homepage, https://thth.me
6
6
  Project-URL: Source, https://github.com/aokings/thth
@@ -44,6 +44,8 @@ English: [README.en.md](README.en.md) · サイト: https://thth.me · 文書の
44
44
  - **調べる**: `thth topics <アカウント> --search <語>` で、その語の件数・投稿者の数・最新の投稿を見ます。`thth where` は「次にどこで話すか」を、自分がすでに絡んだ相手を重ねて探します。X の行には投稿者のフォロワー数(「フォロワー 1.2 万」)と投稿数も付きます(`--json` は `author_followers`・`author_posts`。保存しません)。
45
45
  - **出す**: `thth send` で今すぐ、`thth schedule` で予約、`--reply-to` で返信です。
46
46
  - **ブックマークの連投は記事と別に数える**(4.0.1): はてなブックマークは主体の別の窓(既定 10 分に 10 件)で数え、超えても主体を止めずにその 1 件だけ断ります。1 日 30 件・同じホストへ 1 日 3 件(自分の記事は 1 日 1 件のまま)。記事の連投で止める作りは変わりません([リリースノート 4.0.1](docs/リリースノート_4.0.1_2026-10-17.md))。
47
+ - **会話の続く投稿は 20 分おきに返信を取り直す**(4.4.0): 投稿から 48 時間以内か、直近 48 時間に返信が付いた投稿は、刻みとは別に timer が会話を取り直します(1 回 10 本まで・刻みと数の記録は変えない・X は既定で外す・台帳の `reply_follow` で変えられる)([リリースノート 4.4.0](docs/リリースノート_4.4.0_2026-10-21.md))。
48
+ - **止めた口座では timer は何も出さない**(4.3.1): 安全装置か持ち主が止めた口座では、予約の投稿・返信を queue に残し、`thth account resume` で戻せば次の timer で出ます([リリースノート 4.3.1](docs/リリースノート_4.3.1_2026-10-20.md))。
47
49
  - **返信はまとめて出す**(4.3.0): 時刻の過ぎた返信は承認したその場で、timer では 1 回でまとめて出します(返信の窓は返信だけで 10 分に 10 件・投稿の窓に数えない・投稿の間隔を掛けない)。使い方は「`publish_at` を今か過去にしてまとめて承認する」だけです([リリースノート 4.3.0](docs/リリースノート_4.3.0_2026-10-19.md))。
48
50
  - **1 件の断りでまとめを止めない**(4.2.2): はてなに 1 件だけ断られた札は token を確かめてその札だけ外し、まとめを進めます。断られそうな札(半角の「%」・タグとコメントの長いもの)は 1 段目と lint で予告します(断りません)。止まったまとめは 60 分ごとにしか打たず、`thth bookmark batch-resume` で今すぐ見直せます。sparse-checkout の定義の外には記録を書かず、直し方を言います([リリースノート 4.2.2](docs/リリースノート_4.2.2_2026-10-18.md))。
49
51
  - **ブックマークの直しとタグの長さの柵**(4.2.0): `thth bookmark edit` で THTH が付けたブックマークのコメントとタグを 2 段の承認で直す。31 バイトを超えるタグは予告して断る。上限の設定は台帳の `limits` と別名で書け、断りに根拠と通し方が付きます([リリースノート 4.2.0](docs/リリースノート_4.2.0_2026-10-18.md))。
@@ -20,6 +20,8 @@ English: [README.en.md](README.en.md) · サイト: https://thth.me · 文書の
20
20
  - **調べる**: `thth topics <アカウント> --search <語>` で、その語の件数・投稿者の数・最新の投稿を見ます。`thth where` は「次にどこで話すか」を、自分がすでに絡んだ相手を重ねて探します。X の行には投稿者のフォロワー数(「フォロワー 1.2 万」)と投稿数も付きます(`--json` は `author_followers`・`author_posts`。保存しません)。
21
21
  - **出す**: `thth send` で今すぐ、`thth schedule` で予約、`--reply-to` で返信です。
22
22
  - **ブックマークの連投は記事と別に数える**(4.0.1): はてなブックマークは主体の別の窓(既定 10 分に 10 件)で数え、超えても主体を止めずにその 1 件だけ断ります。1 日 30 件・同じホストへ 1 日 3 件(自分の記事は 1 日 1 件のまま)。記事の連投で止める作りは変わりません([リリースノート 4.0.1](docs/リリースノート_4.0.1_2026-10-17.md))。
23
+ - **会話の続く投稿は 20 分おきに返信を取り直す**(4.4.0): 投稿から 48 時間以内か、直近 48 時間に返信が付いた投稿は、刻みとは別に timer が会話を取り直します(1 回 10 本まで・刻みと数の記録は変えない・X は既定で外す・台帳の `reply_follow` で変えられる)([リリースノート 4.4.0](docs/リリースノート_4.4.0_2026-10-21.md))。
24
+ - **止めた口座では timer は何も出さない**(4.3.1): 安全装置か持ち主が止めた口座では、予約の投稿・返信を queue に残し、`thth account resume` で戻せば次の timer で出ます([リリースノート 4.3.1](docs/リリースノート_4.3.1_2026-10-20.md))。
23
25
  - **返信はまとめて出す**(4.3.0): 時刻の過ぎた返信は承認したその場で、timer では 1 回でまとめて出します(返信の窓は返信だけで 10 分に 10 件・投稿の窓に数えない・投稿の間隔を掛けない)。使い方は「`publish_at` を今か過去にしてまとめて承認する」だけです([リリースノート 4.3.0](docs/リリースノート_4.3.0_2026-10-19.md))。
24
26
  - **1 件の断りでまとめを止めない**(4.2.2): はてなに 1 件だけ断られた札は token を確かめてその札だけ外し、まとめを進めます。断られそうな札(半角の「%」・タグとコメントの長いもの)は 1 段目と lint で予告します(断りません)。止まったまとめは 60 分ごとにしか打たず、`thth bookmark batch-resume` で今すぐ見直せます。sparse-checkout の定義の外には記録を書かず、直し方を言います([リリースノート 4.2.2](docs/リリースノート_4.2.2_2026-10-18.md))。
25
27
  - **ブックマークの直しとタグの長さの柵**(4.2.0): `thth bookmark edit` で THTH が付けたブックマークのコメントとタグを 2 段の承認で直す。31 バイトを超えるタグは予告して断る。上限の設定は台帳の `limits` と別名で書け、断りに根拠と通し方が付きます([リリースノート 4.2.0](docs/リリースノート_4.2.0_2026-10-18.md))。
@@ -39,9 +39,9 @@ class _口:
39
39
  raise AssertionError("**refresh が account_insights を呼んでいる**")
40
40
 
41
41
 
42
- def _仕立て(tmp_path, isolated_account_factory, *, posted_at=POSTED):
42
+ def _仕立て(tmp_path, isolated_account_factory, *, posted_at=POSTED, **overrides):
43
43
  pair = init_git_pair(tmp_path, seed_content="x\n", seed_name="seed.md")
44
- account = isolated_account_factory(repo_dir=pair["work"])
44
+ account = isolated_account_factory(repo_dir=pair["work"], **overrides)
45
45
  write_queue_file(account["queue_dir"], "a.md", fm_overrides={
46
46
  "status": "posted", "post_id": "POST1", "posted_at": posted_at})
47
47
  return pair, account
@@ -57,7 +57,9 @@ def _台帳(pair, post_id="POST1"):
57
57
 
58
58
  def test_刻みに当たらなくても取り直せる(tmp_path, isolated_account_factory):
59
59
  """**これが `--refresh` の目的。** 定期取得は 0.5h では何も取らない。"""
60
- pair, account = _仕立て(tmp_path, isolated_account_factory)
60
+ # 刻みの取得だけを見る(4.4.0 の会話の続く投稿の取り直しは切る・`tests/test_v440_reply_follow.py`)。
61
+ pair, account = _仕立て(tmp_path, isolated_account_factory,
62
+ reply_follow={"enabled": False})
61
63
  口 = _口([{"id": "R1", "text": "返信"}])
62
64
 
63
65
  定期 = collect_mod.collect_once(account["name"], adapter=口, now=NOW,
@@ -20,7 +20,8 @@ class Adapter:
20
20
  return []
21
21
 
22
22
  def setup_route(tmp_path,factory,monkeypatch,route,collect_days=14,medium='threads'):
23
- a=factory(name='test-'+medium,media=medium,collect_days=collect_days)
23
+ # 刻みの時計だけを見る(4.4.0 の会話の続く投稿の取り直しは切る・tests/test_v440_reply_follow.py)。
24
+ a=factory(name='test-'+medium,media=medium,collect_days=collect_days,reply_follow={'enabled':False})
24
25
  monkeypatch.setattr(c.writeback,'upstream_sha',lambda repo:None)
25
26
  files=[]
26
27
  path=tmp_path/'queue.md'
@@ -0,0 +1,81 @@
1
+ """4.3.1: 止めた口座では timer は何も出さない(masaru 10/20「止めた口座では timer も返信を出さないように直して」・
2
+ 予約の投稿も止めることに「それでよいです」)。
3
+
4
+ 4.3.0 までは承認の場の送り(`send_due_replies`)と thth.me・CLI の道だけが止めの印(`guard.stopped`)を見て、
5
+ timer の道(`throw_once`)は止めた口座でも予約の投稿と返信を出していた。
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import pytest
10
+
11
+ from tests.test_v430_reply_batch import NOW, _Media, _place, _status, _tick
12
+ from thth import guard
13
+
14
+
15
+ @pytest.fixture
16
+ def account(isolated_account_factory):
17
+ return isolated_account_factory(production=True, min_interval_hours=0)
18
+
19
+
20
+ def test_止めた口座ではtimerは返信も予約の投稿も出さない(account):
21
+ replies = [_place(account, f"r{i}.md", reply_to=f"1799{i}", minutes_ago=30 - i) for i in range(3)]
22
+ post = _place(account, "p.md", minutes_ago=40)
23
+ guard.stop(account["name"], "owner", now=NOW)
24
+ media = _Media()
25
+ result, lines = _tick(account, media)
26
+ assert result.exit_code == 0 and result.action == "stopped"
27
+ assert media.calls == []
28
+ assert _status(post) == "approved" and all(_status(p) == "approved" for p in replies) # queue に残る
29
+ assert "止まっているので出しません" in result.message
30
+ assert f"thth account resume {account['name']}" in result.message
31
+
32
+
33
+ def test_戻したら次のtimerで出す(account):
34
+ reply = _place(account, "r.md", reply_to="17990", minutes_ago=5)
35
+ post = _place(account, "p.md", minutes_ago=40)
36
+ guard.stop(account["name"], "owner", now=NOW)
37
+ media = _Media()
38
+ _tick(account, media)
39
+ assert media.calls == []
40
+ guard.resume(account["name"], by="masaru")
41
+ _tick(account, media, minutes=1)
42
+ assert sorted(c[1] or "" for c in media.calls) == ["", "17990"]
43
+ assert _status(reply) == "posted" and _status(post) == "posted"
44
+
45
+
46
+ def test_止めた口座ではdry_runも出さないと言う(account):
47
+ _place(account, "p.md", minutes_ago=40)
48
+ guard.stop(account["name"], "burst", now=NOW)
49
+ from thth import core
50
+ result = core.throw_once(account["name"], production_flag=False, adapter_factory=lambda *_: _Media(), now=NOW)
51
+ assert result.action == "stopped" and result.mode == "rehearsal"
52
+
53
+
54
+ def test_止めの印を置けない置き場では印を読まず出す(account, monkeypatch):
55
+ """運営者の CLI と同じ(`guard.check(cli=True)`): 印を置けない置き場には印が無い。"""
56
+ reply = _place(account, "r.md", reply_to="17990", minutes_ago=5)
57
+ monkeypatch.setattr(guard, "stop_place_usable", lambda account: False)
58
+ monkeypatch.setattr(guard, "stopped", lambda account: pytest.fail("印を読んだ"))
59
+ media = _Media()
60
+ _tick(account, media)
61
+ assert [c[1] for c in media.calls] == ["17990"] and _status(reply) == "posted"
62
+
63
+
64
+ def test_印が読めないときは止まっている側(account, monkeypatch):
65
+ from thth import server_files
66
+ _place(account, "r.md", reply_to="17990", minutes_ago=5)
67
+
68
+ def unreadable(account):
69
+ raise server_files.UnsafeFile("unsafe_server_directory")
70
+ monkeypatch.setattr(guard, "stopped", unreadable)
71
+ media = _Media()
72
+ result, _ = _tick(account, media)
73
+ assert result.action == "stopped" and media.calls == []
74
+
75
+
76
+ def test_止めた口座ではスレッド連投にも入らない(account, monkeypatch):
77
+ from thth import threadthrow
78
+ monkeypatch.setattr(threadthrow, "run_for_account", lambda *a, **k: pytest.fail("スレッド連投に入った"))
79
+ guard.stop(account["name"], "owner", now=NOW)
80
+ result, _ = _tick(account, _Media())
81
+ assert result.action == "stopped"
@@ -0,0 +1,299 @@
1
+ """4.4.0 会話の続く投稿の取り直し(`thth/reply_follow.py`・`collect._collect_follow`)。
2
+
3
+ kopicha 10/21: 返信が刻み(1・6・24・72・168 時間)の間に付くと、次の刻みまで台帳に入らず、
4
+ h2d の起こしが約 2 時間半遅れた。会話の続く投稿だけ、刻みとは別におおむね 20 分おきに
5
+ 取り直す。性質は `--refresh` と同じ(刻みを進めない・数を取らない)。
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import datetime as dt
10
+ import json
11
+ import os
12
+
13
+ import pytest
14
+
15
+ from thth import accounts, collect as c, jst, sent
16
+
17
+ POSTED = jst.parse("2026-10-20T00:00:00+09:00")
18
+ H = dt.timedelta(hours=1)
19
+ M = dt.timedelta(minutes=1)
20
+
21
+
22
+ class Adapter:
23
+ def __init__(self, rows=None, fail=False):
24
+ self.rows = rows or {}
25
+ self.fail = fail
26
+ self.insight_calls, self.reply_calls = [], []
27
+
28
+ def capabilities(self):
29
+ return set()
30
+
31
+ def insights(self, pid):
32
+ self.insight_calls.append(pid)
33
+ return {"metrics": {"views": 1}, "available": ["views"]}
34
+
35
+ def conversation(self, pid, **kw):
36
+ self.reply_calls.append((pid, kw))
37
+ if self.fail:
38
+ raise RuntimeError("fixture fail")
39
+ return list(self.rows.get(pid, []))
40
+
41
+
42
+ class XAdapter(Adapter):
43
+ PAID_READS = True
44
+ CONVERSATION_MIN = 10
45
+
46
+
47
+ def _account(factory, monkeypatch, *, media="threads", **overrides):
48
+ a = factory(name="test-" + media, media=media, **overrides)
49
+ monkeypatch.setattr(c.writeback, "upstream_sha", lambda repo: None)
50
+ monkeypatch.setattr(c.core, "list_queue_files", lambda *x, **k: [])
51
+ return a
52
+
53
+
54
+ def _post(a, pid, posted_at, reply_to=None):
55
+ sent.write(accounts.state_dir_for(a["name"]), post_id=pid, text="fixture",
56
+ body_hash="fixture", sent_at=jst.iso(posted_at), reply_to=reply_to)
57
+
58
+
59
+ def _ledger(a, pid):
60
+ cfg = accounts.load_account(a["name"])
61
+ path = os.path.join(accounts.data_dirs(cfg, a["name"])["replies"], f"{pid}.ndjson")
62
+ if not os.path.exists(path):
63
+ return []
64
+ return [json.loads(line) for line in open(path, encoding="utf-8") if line.strip()]
65
+
66
+
67
+ def _state_dir(a):
68
+ return accounts.state_dir_for(a["name"])
69
+
70
+
71
+ def _state(a):
72
+ path = os.path.join(_state_dir(a), "reply_follow.json")
73
+ if not os.path.exists(path):
74
+ return {}
75
+ return json.load(open(path, encoding="utf-8"))["last"]
76
+
77
+
78
+ def _ledger_bytes(a, pid):
79
+ cfg = accounts.load_account(a["name"])
80
+ path = os.path.join(accounts.data_dirs(cfg, a["name"])["replies"], f"{pid}.ndjson")
81
+ return path, (open(path, "rb").read() if os.path.exists(path) else None)
82
+
83
+
84
+ def _run(a, adapter, now):
85
+ return c.collect_once(a["name"], adapter=adapter, now=now, log=lambda _l: None)
86
+
87
+
88
+ def _called(adapter):
89
+ return [pid for pid, _kw in adapter.reply_calls]
90
+
91
+
92
+ def test_会話の続く投稿だけ取り直す(tmp_path, isolated_account_factory, monkeypatch):
93
+ """48 時間以内の投稿は取り直す。古くて返信も無い投稿は取り直さない。"""
94
+ a = _account(isolated_account_factory, monkeypatch)
95
+ now = POSTED + 100 * H
96
+ _post(a, "NEW", now - 3 * H)
97
+ _post(a, "OLD", POSTED) # 100 時間前・返信なし
98
+ adapter = Adapter()
99
+ _run(a, adapter, now) # 初回: NEW は 1h の刻み・OLD は 72h の刻み
100
+ adapter.reply_calls.clear()
101
+ _run(a, adapter, now + 30 * M) # 刻みに当たらない回
102
+ assert _called(adapter) == ["NEW"]
103
+ assert set(_state(a)) == {"NEW"}
104
+
105
+
106
+ def test_古い投稿でも直近48時間に返信が付けば取り直す(tmp_path, isolated_account_factory, monkeypatch):
107
+ a = _account(isolated_account_factory, monkeypatch)
108
+ now = POSTED + 100 * H
109
+ _post(a, "OLD", POSTED)
110
+ adapter = Adapter(rows={"OLD": [{"id": "R1", "timestamp": jst.iso(now - 5 * H)}]})
111
+ _run(a, adapter, now) # 72h の刻みで R1 が台帳に入る
112
+ adapter.reply_calls.clear()
113
+ _run(a, adapter, now + 30 * M)
114
+ assert _called(adapter) == ["OLD"]
115
+
116
+
117
+ def test_古い投稿でも自分が返した枝は取り直す(tmp_path, isolated_account_factory, monkeypatch):
118
+ a = _account(isolated_account_factory, monkeypatch)
119
+ now = POSTED + 100 * H
120
+ _post(a, "OLD", POSTED)
121
+ _post(a, "MINE", now - 2 * H, reply_to="OLD") # 自分の返信(2 時間前)
122
+ adapter = Adapter()
123
+ _run(a, adapter, now)
124
+ adapter.reply_calls.clear()
125
+ _run(a, adapter, now + 30 * M)
126
+ assert sorted(_called(adapter)) == ["MINE", "OLD"]
127
+
128
+
129
+ def test_20分たたない投稿は飛ばす(tmp_path, isolated_account_factory, monkeypatch):
130
+ a = _account(isolated_account_factory, monkeypatch)
131
+ _post(a, "P", POSTED)
132
+ adapter = Adapter()
133
+ _run(a, adapter, POSTED + 2 * H) # 1h の刻み
134
+ adapter.reply_calls.clear()
135
+ _run(a, adapter, POSTED + 2 * H + 10 * M) # 10 分後: 飛ばす
136
+ assert _called(adapter) == []
137
+ _run(a, adapter, POSTED + 2 * H + 19 * M) # 19 分後: まだ飛ばす
138
+ assert _called(adapter) == []
139
+ _run(a, adapter, POSTED + 2 * H + 20 * M) # 20 分後: 取り直す
140
+ assert _called(adapter) == ["P"]
141
+ _run(a, adapter, POSTED + 2 * H + 30 * M) # 取り直しから 10 分: 飛ばす
142
+ assert _called(adapter) == ["P"]
143
+
144
+
145
+ def test_間隔と窓と本数は台帳で変えられる(tmp_path, isolated_account_factory, monkeypatch):
146
+ a = _account(isolated_account_factory, monkeypatch,
147
+ reply_follow={"every_minutes": 60, "live_hours": 2, "max_posts": 1})
148
+ _post(a, "P", POSTED)
149
+ adapter = Adapter()
150
+ _run(a, adapter, POSTED + 1 * H) # 1h の刻み
151
+ adapter.reply_calls.clear()
152
+ _run(a, adapter, POSTED + 1 * H + 30 * M) # 60 分たっていない
153
+ assert _called(adapter) == []
154
+ _run(a, adapter, POSTED + 2 * H) # 60 分・窓(2 時間)の内
155
+ assert _called(adapter) == ["P"]
156
+ _run(a, adapter, POSTED + 4 * H) # 窓の外・返信なし
157
+ assert _called(adapter) == ["P"]
158
+
159
+
160
+ def test_上限の本数を新しい動きの順に取る(tmp_path, isolated_account_factory, monkeypatch):
161
+ a = _account(isolated_account_factory, monkeypatch)
162
+ now = POSTED + 30 * H
163
+ # 12 本・投稿が新しいほど動きが新しい。P00 がいちばん古い。
164
+ for i in range(12):
165
+ _post(a, f"P{i:02d}", POSTED + i * H)
166
+ adapter = Adapter()
167
+ _run(a, adapter, now) # 刻み(24h・6h など)で全部 1 回取る
168
+ adapter.reply_calls.clear()
169
+ _run(a, adapter, now + 30 * M)
170
+ assert len(adapter.reply_calls) == 10
171
+ assert _called(adapter) == [f"P{i:02d}" for i in range(11, 1, -1)]
172
+
173
+
174
+ def test_刻みと数の記録を進めない(tmp_path, isolated_account_factory, monkeypatch):
175
+ """取り直しは insights を読まず、`marks: []` で記録する。6h の刻みはそのまま 6h に取る。"""
176
+ a = _account(isolated_account_factory, monkeypatch)
177
+ _post(a, "P", POSTED)
178
+ adapter = Adapter(rows={"P": [{"id": "R1", "timestamp": jst.iso(POSTED + 30 * M)}]})
179
+ _run(a, adapter, POSTED + 1 * H) # 1h の刻み(数と返信)
180
+ assert adapter.insight_calls == ["P"]
181
+ adapter.rows["P"].append({"id": "R2", "timestamp": jst.iso(POSTED + 2 * H)})
182
+ _run(a, adapter, POSTED + 3 * H) # 取り直し(R2 が新しい)
183
+ assert adapter.insight_calls == ["P"], "取り直しが数を読んでいる"
184
+ fetches = [r for r in _ledger(a, "P") if r["kind"] == "fetch"]
185
+ assert [r["marks"] for r in fetches] == [[1], []]
186
+ assert [r["trigger"] for r in fetches] == ["marks", "follow"]
187
+ assert c.due_marks(3.0, fetches, marks=c.REPLY_MARKS_HOURS) == []
188
+ assert [r["id"] for r in _ledger(a, "P") if r["kind"] == "reply"] == ["R1", "R2"]
189
+ _run(a, adapter, POSTED + 6 * H) # 6h の刻みは刻みとして取る
190
+ assert adapter.insight_calls == ["P", "P"]
191
+ fetches = [r for r in _ledger(a, "P") if r["kind"] == "fetch"]
192
+ assert fetches[-1]["marks"] == [6] and fetches[-1]["trigger"] == "marks"
193
+ assert len(fetches) == 3, "刻みで取った回に取り直しでも読んでいる"
194
+
195
+
196
+ def test_Xは既定で外す(tmp_path, isolated_account_factory, monkeypatch):
197
+ a = _account(isolated_account_factory, monkeypatch, media="x")
198
+ _post(a, "123", POSTED)
199
+ adapter = XAdapter()
200
+ for minutes in (60, 90, 120):
201
+ _run(a, adapter, POSTED + minutes * M)
202
+ assert adapter.reply_calls == []
203
+
204
+
205
+ def test_Xは台帳で明示したときだけ最小の本数で読む(tmp_path, isolated_account_factory, monkeypatch):
206
+ a = _account(isolated_account_factory, monkeypatch, media="x",
207
+ reply_follow={"enabled": True})
208
+ _post(a, "123", POSTED)
209
+ adapter = XAdapter()
210
+ _run(a, adapter, POSTED + 60 * M)
211
+ assert adapter.reply_calls == [("123", {"limit": 10})]
212
+
213
+
214
+ def test_取り直しの失敗で採取を失敗にしない(tmp_path, isolated_account_factory, monkeypatch):
215
+ a = _account(isolated_account_factory, monkeypatch)
216
+ _post(a, "P", POSTED)
217
+ _run(a, Adapter(), POSTED + 1 * H) # 1h の刻み
218
+ adapter = Adapter(fail=True)
219
+ lines = []
220
+ result = c.collect_once(a["name"], adapter=adapter, now=POSTED + 2 * H, log=lines.append)
221
+ assert _called(adapter) == ["P"]
222
+ assert result["errors"] == [], "取り直しの失敗が採取の errors(終了コード)に入っている"
223
+ assert result["follow"]["failed"] == [{"post_id": "P", "reason": "fixture fail"}]
224
+ assert any("取り直し" in line for line in lines)
225
+ # 失敗した回は記録が残らないので、次の回で取り直す。
226
+ adapter.fail = False
227
+ _run(a, adapter, POSTED + 2 * H + 10 * M)
228
+ assert _called(adapter) == ["P", "P"]
229
+
230
+
231
+ @pytest.mark.parametrize("value", [
232
+ {"every_minutes": 1}, {"live_hours": 0}, {"max_posts": 51}, {"enabled": "yes"},
233
+ {"unknown": 1}, [], {"every_minutes": 20.5}])
234
+ def test_形の違う設定は台帳の読み込みで断る(tmp_path, isolated_account_factory, value):
235
+ a = isolated_account_factory(name="test-threads", reply_follow=value)
236
+ with pytest.raises(accounts.AccountError, match="invalid_reply_follow"):
237
+ accounts.load_account(a["name"])
238
+
239
+
240
+ def test_max_posts0は切る():
241
+ assert c.reply_follow_mod.settings({"media": "threads", "reply_follow": {"max_posts": 0}})["enabled"] is False
242
+ assert c.reply_follow_mod.settings({"media": "bluesky"})["enabled"] is True
243
+ assert c.reply_follow_mod.settings({"media": "mastodon"})["enabled"] is True
244
+ assert c.reply_follow_mod.settings({"media": "x"})["enabled"] is False
245
+
246
+
247
+ def test_新しい返信が無い取り直しでは利用者のrepoに書かない(tmp_path, isolated_account_factory, monkeypatch):
248
+ """masaru 10/21: THTH の記録は VM の state に持ち、利用者の repo には積まない。"""
249
+ a = _account(isolated_account_factory, monkeypatch)
250
+ _post(a, "P", POSTED)
251
+ adapter = Adapter(rows={"P": [{"id": "R1", "timestamp": jst.iso(POSTED + 30 * M)}]})
252
+ _run(a, adapter, POSTED + 1 * H) # 1h の刻みで R1 が入る
253
+ path, before = _ledger_bytes(a, "P")
254
+ result = _run(a, adapter, POSTED + 2 * H) # 取り直し・新しい返信なし
255
+ assert _called(adapter) == ["P", "P"]
256
+ assert result["follow"]["fetched"] == 1 and result["follow"]["new_replies"] == 0
257
+ assert _ledger_bytes(a, "P")[1] == before, "新しい返信が無いのに repo の台帳に書いた"
258
+ assert path not in result["touched"], "新しい返信が無いのに commit の対象にした"
259
+ assert set(_state(a)) == {"P"}
260
+ # 新しい返信が来た回だけ repo に書く。
261
+ adapter.rows["P"].append({"id": "R2", "timestamp": jst.iso(POSTED + 2 * H + 10 * M)})
262
+ result = _run(a, adapter, POSTED + 2 * H + 20 * M)
263
+ assert path in result["touched"]
264
+ assert [r["id"] for r in _ledger(a, "P") if r["kind"] == "reply"] == ["R1", "R2"]
265
+
266
+
267
+ def test_20分の判定はstateの時刻で効く(tmp_path, isolated_account_factory, monkeypatch):
268
+ a = _account(isolated_account_factory, monkeypatch)
269
+ _post(a, "P", POSTED)
270
+ adapter = Adapter()
271
+ _run(a, adapter, POSTED + 1 * H) # 1h の刻み(台帳の取得の行は 1h)
272
+ adapter.reply_calls.clear()
273
+ os.makedirs(_state_dir(a), exist_ok=True)
274
+ with open(os.path.join(_state_dir(a), "reply_follow.json"), "w", encoding="utf-8") as f:
275
+ json.dump({"last": {"P": jst.iso(POSTED + 3 * H - 10 * M)}}, f)
276
+ _run(a, adapter, POSTED + 3 * H) # 台帳では 2 時間・state では 10 分
277
+ assert _called(adapter) == [], "state の時刻を見ずに取り直した"
278
+ _run(a, adapter, POSTED + 3 * H + 10 * M) # state から 20 分
279
+ assert _called(adapter) == ["P"]
280
+ assert _state(a)["P"] == jst.iso(POSTED + 3 * H + 10 * M)
281
+ _run(a, adapter, POSTED + 3 * H + 20 * M) # 取り直しから 10 分(記録は state だけ)
282
+ assert _called(adapter) == ["P"]
283
+
284
+
285
+ @pytest.mark.parametrize("content", ["{壊れ", "[]", '{"last": 3}'])
286
+ def test_stateが壊れても採取は止まらない(tmp_path, isolated_account_factory, monkeypatch, content):
287
+ a = _account(isolated_account_factory, monkeypatch)
288
+ _post(a, "P", POSTED)
289
+ _run(a, Adapter(), POSTED + 1 * H)
290
+ os.makedirs(_state_dir(a), exist_ok=True)
291
+ with open(os.path.join(_state_dir(a), "reply_follow.json"), "w", encoding="utf-8") as f:
292
+ f.write(content)
293
+ adapter = Adapter()
294
+ lines = []
295
+ result = c.collect_once(a["name"], adapter=adapter, now=POSTED + 2 * H, log=lines.append)
296
+ assert result["errors"] == []
297
+ assert _called(adapter) == ["P"]
298
+ assert any("空として扱います" in line for line in lines)
299
+ assert set(_state(a)) == {"P"}, "壊れた記録を書き直していない"
@@ -0,0 +1 @@
1
+ 4.4.0
@@ -562,6 +562,12 @@ def load_account(name: str) -> dict:
562
562
  raise AccountError(
563
563
  f"{name}: 台帳の profile_links が受け取れません(invalid_profile_links・"
564
564
  f"http(s) の URL を最大 {PROFILE_LINKS_MAX} つ)")
565
+ # 会話の続く投稿の取り直し(4.4.0・`thth/reply_follow.py`)。形が違えば既定に倒さず断る。
566
+ from . import reply_follow
567
+ if "reply_follow" in data and not reply_follow.valid(data["reply_follow"]):
568
+ raise AccountError(
569
+ f"{name}: 台帳の reply_follow が受け取れません(invalid_reply_follow・鍵は "
570
+ f"enabled(true/false)・every_minutes(5〜1440)・live_hours(1〜720)・max_posts(0〜50))")
565
571
  # 安全装置(設計 3.12.0)。知らない値は黙って既定に倒さず断る。
566
572
  # `approval`(3.12.0)は読まない(IGNORED_FIELDS)。
567
573
  for key in GUARD_DEFAULTS:
@@ -52,6 +52,7 @@ from . import measured as measured_mod
52
52
  from . import postid as postid_mod
53
53
  from . import queuefile
54
54
  from . import redact as redact_mod
55
+ from . import reply_follow as reply_follow_mod
55
56
  from . import runs as runs_mod
56
57
  from . import writeback
57
58
  from .adapters import base as adapter_base
@@ -597,7 +598,8 @@ def collect_once(account_name: str, *, adapter, now=None, log=print) -> dict:
597
598
  # **X の自分の投稿の数字(Owned Reads)を読んでよい id**: THTH が出して `sent/` に
598
599
  # 記録があり、取り下げていないものだけ(masaru 10/12 決定 1・他人の投稿は読まない)。
599
600
  owned_sent_ids = set()
600
- for row in sent_mod.records(accounts_mod.state_dir_for(account_name)):
601
+ sent_rows = sent_mod.records(accounts_mod.state_dir_for(account_name))
602
+ for row in sent_rows:
601
603
  if row.get('post_id') and not row.get('retracted_at'):
602
604
  owned_sent_ids.add(str(row['post_id']))
603
605
  if isinstance(row.get('attachment_kinds'), list):
@@ -610,6 +612,29 @@ def collect_once(account_name: str, *, adapter, now=None, log=print) -> dict:
610
612
  paid_reads = bool(getattr(adapter, "PAID_READS", False))
611
613
  # X の自分の投稿で刻みを跨いだもの(ループの後で 1 回にまとめて読む)。
612
614
  owned_due: list = []
615
+ # **会話の続く投稿の取り直し**(4.4.0・`thth/reply_follow.py`)。刻みとは別に、候補を
616
+ # ループで集めて、ループの後で新しい動きの順に上限の本数だけ取り直す。
617
+ follow_cfg = reply_follow_mod.settings(account_cfg)
618
+ follow_due: list = []
619
+ own_reply_at = reply_follow_mod.own_reply_times(sent_rows) if follow_cfg["enabled"] else {}
620
+ follow_state_dir = accounts_mod.state_dir_for(account_name)
621
+ follow_state = (reply_follow_mod.read_state(follow_state_dir, log=log)
622
+ if follow_cfg["enabled"] else {})
623
+
624
+ def consider_follow(*, post_id, posted_at, age_hours, reply_path, source, reply_to):
625
+ if not follow_cfg["enabled"] or age_hours > collect_days * 24:
626
+ return
627
+ rows = _read_ndjson(reply_path)
628
+ key = reply_follow_mod.candidate(
629
+ now=now, posted_at=posted_at,
630
+ reply_rows=[r for r in rows if r.get("kind") != "fetch"],
631
+ fetch_rows=[r for r in rows if r.get("kind") == "fetch"],
632
+ own_reply_at=own_reply_at.get(str(post_id)), cfg=follow_cfg,
633
+ followed_at=follow_state.get(str(post_id)))
634
+ if key is not None:
635
+ follow_due.append((key, dict(post_id=post_id, age_hours=age_hours,
636
+ reply_path=reply_path, source=source,
637
+ reply_to=reply_to)))
613
638
 
614
639
  def write_insight(*, qf, fm, source, post_id, posted_at_raw, age_hours, marks,
615
640
  insight_path, section, metrics):
@@ -727,6 +752,10 @@ def collect_once(account_name: str, *, adapter, now=None, log=print) -> dict:
727
752
  # 読むのは `sent/` に記録がある(THTH が出した・取り下げていない)id だけ。
728
753
  if str(post_id) not in owned_sent_ids:
729
754
  continue
755
+ # X の取り直しは台帳で `reply_follow.enabled: true` を明示したときだけ(既定は外す)。
756
+ consider_follow(post_id=post_id, posted_at=posted_at, age_hours=age_hours,
757
+ reply_path=reply_path, source=source,
758
+ reply_to=fm.get("reply_to") or parents.get(str(post_id)))
730
759
  marks = due_marks(age_hours, _read_ndjson(insight_path)) if age_hours <= metric_window_days() * 24 else []
731
760
  if marks:
732
761
  owned_due.append(dict(qf=qf, fm=fm, source=source, post_id=str(post_id),
@@ -741,6 +770,11 @@ def collect_once(account_name: str, *, adapter, now=None, log=print) -> dict:
741
770
  # 「部分的な成功は次の実行で埋まる」という約束に反していた。
742
771
  marks = due_marks(age_hours, _read_ndjson(insight_path)) if age_hours <= metric_window_days() * 24 else []
743
772
  reply_marks = due_marks(age_hours, _reply_fetches(reply_path), marks=REPLY_MARKS_HOURS) if age_hours <= collect_days * 24 else []
773
+ if not reply_marks:
774
+ # 刻みで取る回は取り直さない(同じ回に 2 度読まない)。
775
+ consider_follow(post_id=post_id, posted_at=posted_at, age_hours=age_hours,
776
+ reply_path=reply_path, source=source,
777
+ reply_to=fm.get("reply_to") or parents.get(str(post_id)))
744
778
  if not marks and not reply_marks:
745
779
  continue
746
780
 
@@ -789,6 +823,16 @@ def collect_once(account_name: str, *, adapter, now=None, log=print) -> dict:
789
823
  if 新しい:
790
824
  log(f"返信 {len(新しい)} 件: {post_id}")
791
825
 
826
+ # --- 会話の続く投稿の取り直し(4.4.0)。**失敗は `errors` に積まない**——採取の
827
+ # 終了コードも投稿の道も悪くしない。理由は log に残す(次の回が取り直す)。
828
+ follow = _collect_follow(adapter, account_cfg, reply_follow_mod.pick(follow_due, follow_cfg),
829
+ now=now, owned=owned, paid_reads=paid_reads, log=log)
830
+ touched.extend(follow["touched"])
831
+ if follow["followed"]:
832
+ follow_state.update({pid: now for pid in follow["followed"]})
833
+ reply_follow_mod.write_state(follow_state_dir, follow_state, now=now,
834
+ live_hours=follow_cfg["live_hours"], log=log)
835
+
792
836
  # --- X の自分の投稿の数字(Owned Reads): 刻みを跨いだものを 100 本ずつ 1 回で
793
837
  if owned_due:
794
838
  _collect_owned(adapter, owned_due, now=now, errors=errors, write=write_insight,
@@ -810,7 +854,54 @@ def collect_once(account_name: str, *, adapter, now=None, log=print) -> dict:
810
854
  touched.append(account_path)
811
855
 
812
856
  return {"touched": sorted(set(touched)), "posts": posts_seen, "errors": errors,
813
- "inbox": inbox_state}
857
+ "inbox": inbox_state,
858
+ "follow": {k: follow[k] for k in ("requested", "fetched", "new_replies", "failed")}}
859
+
860
+
861
+ def _collect_follow(adapter, account_cfg: dict, due: list, *, now, owned, paid_reads: bool,
862
+ log) -> dict:
863
+ """会話の続く投稿を取り直す(4.4.0)。`--refresh` と同じ性質: `marks: []`・数は取らない。
864
+
865
+ 戻り値の `failed` は `[{"post_id", "reason"}]`。**ここで起きた失敗は採取の `errors` に
866
+ 入れない**(取り直しは刻みの補いで、取れなくても次の刻み・次の回で埋まる)。
867
+ """
868
+ out = {"requested": len(due), "fetched": 0, "new_replies": 0, "failed": [], "touched": [],
869
+ "followed": []}
870
+ for entry in due:
871
+ pid, reply_path = str(entry["post_id"]), entry["reply_path"]
872
+ try:
873
+ _rows, broken = _read_ndjson_strict(reply_path)
874
+ if broken:
875
+ raise ValueError("台帳が壊れています")
876
+ if paid_reads:
877
+ # 台帳で明示したときだけここに来る(X)。1 回の本数を最小にして代金を抑える。
878
+ limit = getattr(adapter, "CONVERSATION_MIN", None)
879
+ replies = (adapter.conversation(pid, limit=limit) if limit
880
+ else adapter.conversation(pid))
881
+ else:
882
+ replies = _fetch_replies(adapter, account_cfg, pid, entry["reply_to"], owned)
883
+ # **新しい返信が無ければ利用者の repo に何も書かない**(masaru 10/21)。書けば
884
+ # 20 分おきに利用者の main へ「収集」の commit が積まれ、承認の target がずれる。
885
+ # 取り直した時刻は VM の state(`reply_follow.json`)にだけ持つ。
886
+ known = {reply_row_id(row) for row in _reply_rows(reply_path)}
887
+ if any(reply_row_id(row) and reply_row_id(row) not in known for row in replies):
888
+ 新しい = _save_replies(reply_path, pid, replies, now=now,
889
+ age_hours=entry["age_hours"], marks=[],
890
+ trigger=reply_follow_mod.TRIGGER, source=entry["source"])
891
+ else:
892
+ 新しい = []
893
+ except Exception as e: # 取り直しの失敗で採取も投稿も止めない
894
+ reason = redact_mod.redact(str(e))
895
+ out["failed"].append({"post_id": pid, "reason": reason})
896
+ log(f"返信の取り直しができませんでした(次の回で取り直します): {pid}: {reason}")
897
+ continue
898
+ out["fetched"] += 1
899
+ out["followed"].append(pid)
900
+ out["new_replies"] += len(新しい)
901
+ if 新しい:
902
+ out["touched"].append(reply_path)
903
+ log(f"返信 {len(新しい)} 件(取り直し): {pid}")
904
+ return out
814
905
 
815
906
 
816
907
  def _collect_owned(adapter, due: list, *, now, errors: list, write, account_name: str) -> None: