threadkeeper 0.21.0__tar.gz → 0.22.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 (314) hide show
  1. {threadkeeper-0.21.0/threadkeeper.egg-info → threadkeeper-0.22.0}/PKG-INFO +22 -2
  2. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/README.md +20 -0
  3. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/pyproject.toml +4 -2
  4. threadkeeper-0.22.0/tests/test_action_outcomes.py +266 -0
  5. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_memory_eval.py +2 -0
  6. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_retention.py +29 -0
  7. threadkeeper-0.22.0/threadkeeper/action_outcomes.py +414 -0
  8. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/authority.py +1 -0
  9. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/candidate_reviewer.py +5 -0
  10. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/db.py +33 -0
  11. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/forget.py +51 -10
  12. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/retention.py +8 -0
  13. {threadkeeper-0.21.0 → threadkeeper-0.22.0/threadkeeper.egg-info}/PKG-INFO +22 -2
  14. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper.egg-info/SOURCES.txt +2 -0
  15. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper.egg-info/requires.txt +1 -1
  16. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/LICENSE +0 -0
  17. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/setup.cfg +0 -0
  18. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_adapters.py +0 -0
  19. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_agent_status.py +0 -0
  20. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_antigravity_ingest.py +0 -0
  21. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_auto_update.py +0 -0
  22. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_backup.py +0 -0
  23. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_brief_footprint.py +0 -0
  24. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_brief_hook_size.py +0 -0
  25. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_brief_sections.py +0 -0
  26. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_candidate_reviewer.py +0 -0
  27. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_ci_parallel.py +0 -0
  28. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_concepts.py +0 -0
  29. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_config_settings.py +0 -0
  30. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_config_watcher.py +0 -0
  31. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_core_memory.py +0 -0
  32. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_curator.py +0 -0
  33. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_daemon_host_config.py +0 -0
  34. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_daemon_host_integration.py +0 -0
  35. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_daemon_liveness.py +0 -0
  36. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_daemon_state.py +0 -0
  37. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dashboard.py +0 -0
  38. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dashboard_app.py +0 -0
  39. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_db_compact.py +0 -0
  40. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_db_contention.py +0 -0
  41. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_db_leak_guard.py +0 -0
  42. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_db_migration_fts.py +0 -0
  43. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_db_runtime.py +0 -0
  44. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_db_schema.py +0 -0
  45. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_delegated_search.py +0 -0
  46. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialectic.py +0 -0
  47. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialectic_drift_eval.py +0 -0
  48. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialectic_feed_tools.py +0 -0
  49. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialectic_miner.py +0 -0
  50. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialectic_observation_resolve.py +0 -0
  51. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialectic_recompute.py +0 -0
  52. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialectic_tier.py +0 -0
  53. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialectic_validator.py +0 -0
  54. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialog_fts_external.py +0 -0
  55. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_documentation_inventory.py +0 -0
  56. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_egress_policy.py +0 -0
  57. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_embed_routing.py +0 -0
  58. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_embedding_generation.py +0 -0
  59. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_embedding_model_revision.py +0 -0
  60. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_embedding_storage_dedup.py +0 -0
  61. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_error_paths.py +0 -0
  62. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_eval_harness.py +0 -0
  63. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_evolve_applier.py +0 -0
  64. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_2.py +0 -0
  65. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_3.py +0 -0
  66. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_43.py +0 -0
  67. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_45.py +0 -0
  68. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_48.py +0 -0
  69. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_evolve_daemon.py +0 -0
  70. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_extract_daemon.py +0 -0
  71. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_extract_dedup.py +0 -0
  72. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_forget.py +0 -0
  73. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_github_budget.py +0 -0
  74. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_github_safety.py +0 -0
  75. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_helpers.py +0 -0
  76. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_hook_child_guard.py +0 -0
  77. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_host_embed.py +0 -0
  78. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_host_env.py +0 -0
  79. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_host_main.py +0 -0
  80. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_host_wedged.py +0 -0
  81. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_hybrid_retrieval.py +0 -0
  82. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_i18n_multilang.py +0 -0
  83. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_identity.py +0 -0
  84. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_ingest_cursor.py +0 -0
  85. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_ingest_denylist.py +0 -0
  86. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_ingest_secret_redaction.py +0 -0
  87. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_ingest_status.py +0 -0
  88. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_ingest_transactions.py +0 -0
  89. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_lesson_index.py +0 -0
  90. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_lesson_violations.py +0 -0
  91. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_lessons.py +0 -0
  92. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_mcp_protocol_conformance.py +0 -0
  93. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_mcp_resources_prompts.py +0 -0
  94. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_mcp_sdk_compat.py +0 -0
  95. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_mcp_server_instructions.py +0 -0
  96. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_mcp_skills_extension.py +0 -0
  97. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_mcp_tasks.py +0 -0
  98. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_memory_authority_lifecycle.py +0 -0
  99. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_memory_compatibility.py +0 -0
  100. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_memory_guard.py +0 -0
  101. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_memory_guard_host.py +0 -0
  102. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_menubar_app.py +0 -0
  103. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_missed_spawns.py +0 -0
  104. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_model_catalog.py +0 -0
  105. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_model_router.py +0 -0
  106. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_model_router_digest.py +0 -0
  107. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_model_router_tools.py +0 -0
  108. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_model_routes.py +0 -0
  109. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_notify_daemon.py +0 -0
  110. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_nudges.py +0 -0
  111. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_onnx_embeddings.py +0 -0
  112. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_panel.py +0 -0
  113. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_permissions.py +0 -0
  114. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_pickup.py +0 -0
  115. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_probe_daemon.py +0 -0
  116. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_process_health.py +0 -0
  117. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_project_scope.py +0 -0
  118. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_release_workflows.py +0 -0
  119. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_review_prompts.py +0 -0
  120. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_search_fts_punctuation.py +0 -0
  121. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_session_read_purity.py +0 -0
  122. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_setup.py +0 -0
  123. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_shadow_review.py +0 -0
  124. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_single_flight_prompt_prefixes.py +0 -0
  125. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_skill_hint.py +0 -0
  126. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_skill_passive_tier.py +0 -0
  127. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_skill_tier.py +0 -0
  128. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_skill_updater.py +0 -0
  129. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_skill_use_parser.py +0 -0
  130. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_skill_watcher.py +0 -0
  131. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_skills.py +0 -0
  132. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_background_cwd.py +0 -0
  133. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_budget.py +0 -0
  134. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_child_env.py +0 -0
  135. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_codex_stdin.py +0 -0
  136. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_config.py +0 -0
  137. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_hint.py +0 -0
  138. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_permission_gate.py +0 -0
  139. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_reap.py +0 -0
  140. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_result.py +0 -0
  141. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_slim.py +0 -0
  142. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_watchdog.py +0 -0
  143. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_worktree_isolation.py +0 -0
  144. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_wrap.py +0 -0
  145. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_storage.py +0 -0
  146. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_sync_capture.py +0 -0
  147. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_sync_identity.py +0 -0
  148. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_sync_migrate.py +0 -0
  149. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_sync_protocol.py +0 -0
  150. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_sync_reset_node.py +0 -0
  151. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_sync_transport.py +0 -0
  152. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_task_retention.py +0 -0
  153. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_task_spool.py +0 -0
  154. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_thin_session.py +0 -0
  155. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_thread_janitor.py +0 -0
  156. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_threads.py +0 -0
  157. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_tool_annotations.py +0 -0
  158. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_tool_profiles.py +0 -0
  159. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_tools_smoke.py +0 -0
  160. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_tracing.py +0 -0
  161. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_validate_threads.py +0 -0
  162. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_vec_search.py +0 -0
  163. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_verify_ingest.py +0 -0
  164. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_workflow_retention.py +0 -0
  165. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/__init__.py +0 -0
  166. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/_mcp.py +0 -0
  167. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/_setup.py +0 -0
  168. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/_spawn_wrap.py +0 -0
  169. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/__init__.py +0 -0
  170. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/_hook_helpers.py +0 -0
  171. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/antigravity.py +0 -0
  172. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/base.py +0 -0
  173. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/claude_code.py +0 -0
  174. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/claude_desktop.py +0 -0
  175. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/codex.py +0 -0
  176. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/copilot.py +0 -0
  177. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/vscode.py +0 -0
  178. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/agent_metadata.py +0 -0
  179. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/agent_status.py +0 -0
  180. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
  181. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/README.md +0 -0
  182. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
  183. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
  184. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
  185. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/auto_update.py +0 -0
  186. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/backup.py +0 -0
  187. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/brief.py +0 -0
  188. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/config.py +0 -0
  189. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/config_io.py +0 -0
  190. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/config_watcher.py +0 -0
  191. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/curator.py +0 -0
  192. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/curator_snapshots.py +0 -0
  193. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/daemon_liveness.py +0 -0
  194. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/daemon_state.py +0 -0
  195. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/daemon_supervisor.py +0 -0
  196. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/dashboard_app.py +0 -0
  197. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/dialectic_drift_eval.py +0 -0
  198. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/dialectic_miner.py +0 -0
  199. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/dialectic_validator.py +0 -0
  200. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/egress.py +0 -0
  201. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/elicitation.py +0 -0
  202. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/embeddings.py +0 -0
  203. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/eval/__init__.py +0 -0
  204. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/eval/__main__.py +0 -0
  205. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/eval/harness.py +0 -0
  206. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/evolve_applier.py +0 -0
  207. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/evolve_daemon.py +0 -0
  208. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/extract_daemon.py +0 -0
  209. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/github_budget.py +0 -0
  210. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/github_safety.py +0 -0
  211. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/harvest.py +0 -0
  212. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/helpers.py +0 -0
  213. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/host.py +0 -0
  214. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/host_embed.py +0 -0
  215. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/i18n.py +0 -0
  216. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/identity.py +0 -0
  217. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/ingest.py +0 -0
  218. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/instructions.py +0 -0
  219. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/lesson_violations.py +0 -0
  220. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/lessons.py +0 -0
  221. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/link_health.py +0 -0
  222. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/mcp_skills.py +0 -0
  223. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/mcp_tasks.py +0 -0
  224. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/memory_compat.py +0 -0
  225. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/memory_guard.py +0 -0
  226. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/menubar_app.py +0 -0
  227. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/migrate_embeddings.py +0 -0
  228. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/model_catalog.py +0 -0
  229. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/model_router.py +0 -0
  230. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/model_router_digest.py +0 -0
  231. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/model_routes.py +0 -0
  232. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/notify.py +0 -0
  233. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/nudges.py +0 -0
  234. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/permissions.py +0 -0
  235. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/probe_daemon.py +0 -0
  236. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/process_health.py +0 -0
  237. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/project_scope.py +0 -0
  238. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/protocol.py +0 -0
  239. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/read_context.py +0 -0
  240. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/retrieval.py +0 -0
  241. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/review_prompts.py +0 -0
  242. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/search_proxy.py +0 -0
  243. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/server.py +0 -0
  244. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/shadow_review.py +0 -0
  245. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/skill_audit.py +0 -0
  246. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/skill_updater.py +0 -0
  247. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/skill_watcher.py +0 -0
  248. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/spawn_budget.py +0 -0
  249. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/spawn_config.py +0 -0
  250. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/spawn_result.py +0 -0
  251. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/storage.py +0 -0
  252. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/sync/__init__.py +0 -0
  253. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/sync/capture.py +0 -0
  254. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/sync/daemon.py +0 -0
  255. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/sync/identity.py +0 -0
  256. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/sync/migrate.py +0 -0
  257. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/sync/protocol.py +0 -0
  258. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/sync/reset_node.py +0 -0
  259. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/sync/server.py +0 -0
  260. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/task_spool.py +0 -0
  261. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/thread_janitor.py +0 -0
  262. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tool_profiles.py +0 -0
  263. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tool_schemas.py +0 -0
  264. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/__init__.py +0 -0
  265. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/agent_status.py +0 -0
  266. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/authority.py +0 -0
  267. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/candidate_reviewer.py +0 -0
  268. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/capabilities.py +0 -0
  269. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/concepts.py +0 -0
  270. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/config_watch.py +0 -0
  271. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/consolidate.py +0 -0
  272. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/core_memory.py +0 -0
  273. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/correlation.py +0 -0
  274. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/curator.py +0 -0
  275. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/dashboard.py +0 -0
  276. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/db_maintenance.py +0 -0
  277. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/dialectic.py +0 -0
  278. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/dialectic_feed.py +0 -0
  279. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/dialog.py +0 -0
  280. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/distill.py +0 -0
  281. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/evolve_applier.py +0 -0
  282. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/evolve_research.py +0 -0
  283. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/extract.py +0 -0
  284. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/forget.py +0 -0
  285. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/graph.py +0 -0
  286. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/invariants.py +0 -0
  287. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/lessons.py +0 -0
  288. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/memory_guard.py +0 -0
  289. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/missed_spawns.py +0 -0
  290. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/model_router.py +0 -0
  291. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/panel.py +0 -0
  292. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/peers.py +0 -0
  293. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/pickup.py +0 -0
  294. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/probes.py +0 -0
  295. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/process_health.py +0 -0
  296. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/project_scope.py +0 -0
  297. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/prompts.py +0 -0
  298. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/resources.py +0 -0
  299. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/session.py +0 -0
  300. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/shadow_review.py +0 -0
  301. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/skills.py +0 -0
  302. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/spawn.py +0 -0
  303. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/style.py +0 -0
  304. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/sync.py +0 -0
  305. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/threads.py +0 -0
  306. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/validate.py +0 -0
  307. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tracing.py +0 -0
  308. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/trash.py +0 -0
  309. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/verify_ingest.py +0 -0
  310. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/wikilinks.py +0 -0
  311. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/workflow_artifacts.py +0 -0
  312. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper.egg-info/dependency_links.txt +0 -0
  313. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper.egg-info/entry_points.txt +0 -0
  314. {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: threadkeeper
3
- Version: 0.21.0
3
+ Version: 0.22.0
4
4
  Summary: Multi-agent shared brain across Claude Code/Desktop, Codex, Antigravity CLI, Copilot, and VS Code. Cross-session memory, self-improving skill loops, inter-agent signaling — one local MCP server.
5
5
  Author: thread-keeper contributors
6
6
  License: MIT
@@ -39,7 +39,7 @@ Provides-Extra: encrypted
39
39
  Requires-Dist: keyring>=25.0; extra == "encrypted"
40
40
  Requires-Dist: sqlcipher3-wheels>=0.5.7; extra == "encrypted"
41
41
  Provides-Extra: dev
42
- Requires-Dist: pytest>=9.1.1; extra == "dev"
42
+ Requires-Dist: pytest<9.2,>=9.1.1; extra == "dev"
43
43
  Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
44
44
  Requires-Dist: pytest-forked>=1.6; extra == "dev"
45
45
  Requires-Dist: pytest-xdist>=3.5; extra == "dev"
@@ -2280,6 +2280,15 @@ It reports these headline groups over a fixed ground-truth set:
2280
2280
  process. It reports a separate score for thread notes, verbatim memory,
2281
2281
  dialectic claims, and lessons or skills.
2282
2282
 
2283
+ - **operational recovery comparison** — a second fully synthetic fixture
2284
+ compares dialogue-only evidence with a bounded action/outcome record. Its
2285
+ fixed thresholds require useful recovery links, zero forbidden-string leaks,
2286
+ bounded storage relative to the dialogue baseline, and zero false causal
2287
+ links. The record shape contains only normalized action names, opaque
2288
+ objective/source references, coarse outcomes, error fingerprints, ordering,
2289
+ recovery links, authority, provenance, and expiry. It has no raw arguments,
2290
+ raw output, transcript text, paths, or secret field.
2291
+
2283
2292
  - **outcome classification** — retrieval failure, reasoning failure,
2284
2293
  stale-memory use, removed-memory use, and privacy-policy violation.
2285
2294
 
@@ -2290,6 +2299,17 @@ unavailable when the optional semantic extra is not installed. `--strict`
2290
2299
  requires the versioned fixture thresholds and zero safety violations, making
2291
2300
  the same command suitable for retrieval/model migration gates.
2292
2301
 
2302
+ Action/outcome capture is an **offline-only prototype**. The runtime has no
2303
+ live producer or export route. Evaluation records expire after 30 days, and
2304
+ the retention pass removes them; `tk-forget --selector-type objective <ref>
2305
+ --apply` removes an objective's records and invalidates any linked lesson.
2306
+ Records sourced from a forgotten dialog/session/thread are removed by the
2307
+ same targeted-erasure path. The existing candidate reviewer may see only the
2308
+ bounded recovery candidate; a lesson must keep its
2309
+ `action_outcome_candidate:<id>` provenance so invalidation hides it. Live use
2310
+ requires a later explicit opt-in plus a passing offline comparison; this
2311
+ release provides neither.
2312
+
2293
2313
  For DB concurrency, run the reproducible local gate:
2294
2314
 
2295
2315
  ```bash
@@ -2233,6 +2233,15 @@ It reports these headline groups over a fixed ground-truth set:
2233
2233
  process. It reports a separate score for thread notes, verbatim memory,
2234
2234
  dialectic claims, and lessons or skills.
2235
2235
 
2236
+ - **operational recovery comparison** — a second fully synthetic fixture
2237
+ compares dialogue-only evidence with a bounded action/outcome record. Its
2238
+ fixed thresholds require useful recovery links, zero forbidden-string leaks,
2239
+ bounded storage relative to the dialogue baseline, and zero false causal
2240
+ links. The record shape contains only normalized action names, opaque
2241
+ objective/source references, coarse outcomes, error fingerprints, ordering,
2242
+ recovery links, authority, provenance, and expiry. It has no raw arguments,
2243
+ raw output, transcript text, paths, or secret field.
2244
+
2236
2245
  - **outcome classification** — retrieval failure, reasoning failure,
2237
2246
  stale-memory use, removed-memory use, and privacy-policy violation.
2238
2247
 
@@ -2243,6 +2252,17 @@ unavailable when the optional semantic extra is not installed. `--strict`
2243
2252
  requires the versioned fixture thresholds and zero safety violations, making
2244
2253
  the same command suitable for retrieval/model migration gates.
2245
2254
 
2255
+ Action/outcome capture is an **offline-only prototype**. The runtime has no
2256
+ live producer or export route. Evaluation records expire after 30 days, and
2257
+ the retention pass removes them; `tk-forget --selector-type objective <ref>
2258
+ --apply` removes an objective's records and invalidates any linked lesson.
2259
+ Records sourced from a forgotten dialog/session/thread are removed by the
2260
+ same targeted-erasure path. The existing candidate reviewer may see only the
2261
+ bounded recovery candidate; a lesson must keep its
2262
+ `action_outcome_candidate:<id>` provenance so invalidation hides it. Live use
2263
+ requires a later explicit opt-in plus a passing offline comparison; this
2264
+ release provides neither.
2265
+
2246
2266
  For DB concurrency, run the reproducible local gate:
2247
2267
 
2248
2268
  ```bash
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "threadkeeper"
7
- version = "0.21.0"
7
+ version = "0.22.0"
8
8
  description = "Multi-agent shared brain across Claude Code/Desktop, Codex, Antigravity CLI, Copilot, and VS Code. Cross-session memory, self-improving skill loops, inter-agent signaling — one local MCP server."
9
9
  requires-python = ">=3.11"
10
10
  authors = [{ name = "thread-keeper contributors" }]
@@ -70,7 +70,9 @@ encrypted = [
70
70
  # not compose with --forked (collides on the xdist worker tmp dir) and a few
71
71
  # tests still race under it. See CONTRIBUTING → Running tests.
72
72
  dev = [
73
- "pytest>=9.1.1",
73
+ # pytest 9.2.0 breaks pytest-forked 1.7.5: every forked test exits with
74
+ # status 3 before reporting. Lift the cap once pytest-forked supports it.
75
+ "pytest>=9.1.1,<9.2",
74
76
  "pytest-cov>=7.1.0",
75
77
  "pytest-forked>=1.6",
76
78
  "pytest-xdist>=3.5",
@@ -0,0 +1,266 @@
1
+ """Offline action/outcome memory stays bounded, deletable, and non-live."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import time
6
+
7
+ import pytest
8
+
9
+
10
+ def _record_pair(conn, action_outcomes, *, objective="objective-42", source_id="dialog-42"):
11
+ failed = action_outcomes.record_evaluation_event(
12
+ conn,
13
+ event_id="ao-failed",
14
+ objective_ref=objective,
15
+ action_name="github.pr.create",
16
+ outcome="failure",
17
+ error_fingerprint="http.422.validation",
18
+ sequence=1,
19
+ source_kind="dialog",
20
+ source_id=source_id,
21
+ authority="observed",
22
+ principal="fixture-runner",
23
+ channel="offline-evaluation",
24
+ created_at=100,
25
+ )
26
+ recovered = action_outcomes.record_evaluation_event(
27
+ conn,
28
+ event_id="ao-recovered",
29
+ objective_ref=objective,
30
+ action_name="github.pr.create",
31
+ outcome="success",
32
+ sequence=2,
33
+ recovered_from=failed,
34
+ source_kind="dialog",
35
+ source_id=source_id,
36
+ authority="observed",
37
+ principal="fixture-runner",
38
+ channel="offline-evaluation",
39
+ created_at=101,
40
+ )
41
+ return failed, recovered
42
+
43
+
44
+ def test_schema_and_validation_exclude_raw_payloads(fresh_mp):
45
+ from threadkeeper import action_outcomes
46
+
47
+ conn = fresh_mp["db"].get_db()
48
+ columns = {
49
+ row["name"]
50
+ for row in conn.execute("PRAGMA table_info(action_outcome_events)")
51
+ }
52
+ assert columns == {
53
+ "id", "objective_ref", "action_name", "outcome", "error_fingerprint",
54
+ "recovered_from", "sequence", "source_kind", "source_id",
55
+ "authority_class", "provenance_version", "created_at", "expires_at",
56
+ }
57
+
58
+ with pytest.raises(ValueError, match="bounded identifier"):
59
+ action_outcomes.record_evaluation_event(
60
+ conn,
61
+ event_id="ao-secret",
62
+ objective_ref="objective-1",
63
+ action_name="github.pr.create --body raw-output",
64
+ outcome="success",
65
+ sequence=1,
66
+ source_kind="dialog",
67
+ source_id="dialog-1",
68
+ authority="observed",
69
+ principal="fixture-runner",
70
+ channel="offline-evaluation",
71
+ )
72
+ with pytest.raises(RuntimeError, match="offline evaluation"):
73
+ action_outcomes.record_live_event()
74
+
75
+
76
+ def test_recovery_order_and_authorized_candidate_are_bounded(fresh_mp):
77
+ from threadkeeper import action_outcomes
78
+
79
+ conn = fresh_mp["db"].get_db()
80
+ failed, recovered = _record_pair(conn, action_outcomes)
81
+ candidate_id = action_outcomes.derive_recovery_candidate(conn, recovered)
82
+
83
+ row = conn.execute(
84
+ "SELECT content, rationale, source_uuid FROM extract_candidates WHERE id=?",
85
+ (candidate_id,),
86
+ ).fetchone()
87
+ assert row["rationale"] == "operational_recovery"
88
+ assert row["source_uuid"] == f"action-outcome:{recovered}"
89
+ assert row["content"] == (
90
+ "Operational recovery: retry github.pr.create after "
91
+ "http.422.validation."
92
+ )
93
+ assert action_outcomes.candidate_source(conn, candidate_id) == (
94
+ "action_outcome_candidate", str(candidate_id)
95
+ )
96
+ exported = action_outcomes.export_events(conn)
97
+ assert [row["id"] for row in exported] == [failed, recovered]
98
+ assert set(exported[0]) == {
99
+ "id", "objective_ref", "action_name", "outcome", "error_fingerprint",
100
+ "recovered_from", "sequence", "source_kind", "source_id",
101
+ "authority_class", "provenance_version", "created_at", "expires_at",
102
+ }
103
+
104
+ with pytest.raises(ValueError, match="earlier failure"):
105
+ action_outcomes.record_evaluation_event(
106
+ conn,
107
+ event_id="ao-out-of-order",
108
+ objective_ref="objective-42",
109
+ action_name="github.pr.create",
110
+ outcome="success",
111
+ sequence=1,
112
+ recovered_from=failed,
113
+ source_kind="dialog",
114
+ source_id="dialog-42",
115
+ authority="observed",
116
+ principal="fixture-runner",
117
+ channel="offline-evaluation",
118
+ )
119
+ with pytest.raises(ValueError, match="same objective"):
120
+ action_outcomes.record_evaluation_event(
121
+ conn,
122
+ event_id="ao-cross-objective",
123
+ objective_ref="objective-43",
124
+ action_name="github.pr.create",
125
+ outcome="success",
126
+ sequence=2,
127
+ recovered_from=failed,
128
+ source_kind="dialog",
129
+ source_id="dialog-43",
130
+ authority="observed",
131
+ principal="fixture-runner",
132
+ channel="offline-evaluation",
133
+ )
134
+
135
+
136
+ def test_expiry_and_source_or_objective_forgetting_remove_candidates_and_invalidate_lessons(
137
+ fresh_mp,
138
+ ):
139
+ from threadkeeper import action_outcomes
140
+ from threadkeeper.authority import source_is_live
141
+ from threadkeeper.tools.lessons import lesson_append
142
+
143
+ conn = fresh_mp["db"].get_db()
144
+ _failed, recovered = _record_pair(conn, action_outcomes)
145
+ candidate_id = action_outcomes.derive_recovery_candidate(conn, recovered)
146
+ conn.commit()
147
+
148
+ result = lesson_append(
149
+ title="Recover GitHub validation errors",
150
+ summary="Use the linked recovery evidence.",
151
+ body="Retry the normalized action only after the stable validation error.",
152
+ source=f"action_outcome_candidate:{candidate_id}",
153
+ )
154
+ assert result.startswith("ok slug=recover-github-validation-errors")
155
+ assert source_is_live(conn, ("lesson", "recover-github-validation-errors"))
156
+
157
+ deleted = action_outcomes.forget_objective(conn, "objective-42")
158
+ assert deleted == {"events": 2, "candidates": 1}
159
+ assert not source_is_live(conn, ("lesson", "recover-github-validation-errors"))
160
+ assert conn.execute("SELECT COUNT(*) FROM action_outcome_events").fetchone()[0] == 0
161
+ assert conn.execute("SELECT COUNT(*) FROM extract_candidates").fetchone()[0] == 0
162
+
163
+ old = action_outcomes.record_evaluation_event(
164
+ conn,
165
+ event_id="ao-expired",
166
+ objective_ref="objective-44",
167
+ action_name="github.pr.create",
168
+ outcome="failure",
169
+ error_fingerprint="http.429.rate-limit",
170
+ sequence=1,
171
+ source_kind="dialog",
172
+ source_id="dialog-44",
173
+ authority="observed",
174
+ principal="fixture-runner",
175
+ channel="offline-evaluation",
176
+ created_at=int(time.time()) - 3 * 86400,
177
+ retention_days=1,
178
+ )
179
+ assert old == "ao-expired"
180
+ assert action_outcomes.prune_expired(conn, int(time.time())) == {"events": 1, "candidates": 0}
181
+
182
+ action_outcomes.record_evaluation_event(
183
+ conn,
184
+ event_id="ao-source-forget",
185
+ objective_ref="objective-45",
186
+ action_name="github.pr.create",
187
+ outcome="failure",
188
+ error_fingerprint="http.429.rate-limit",
189
+ sequence=1,
190
+ source_kind="dialog",
191
+ source_id="dialog-45",
192
+ authority="observed",
193
+ principal="fixture-runner",
194
+ channel="offline-evaluation",
195
+ )
196
+ assert action_outcomes.forget_sources(conn, [("dialog", "dialog-45")]) == {
197
+ "events": 1,
198
+ "candidates": 0,
199
+ }
200
+
201
+ conn.execute(
202
+ "INSERT INTO dialog_messages "
203
+ "(uuid, source, role, content, created_at) VALUES "
204
+ "('dialog-46', 'pytest', 'user', 'erasable source', 100)",
205
+ )
206
+ action_outcomes.record_evaluation_event(
207
+ conn,
208
+ event_id="ao-forget-selector",
209
+ objective_ref="objective-46",
210
+ action_name="github.pr.create",
211
+ outcome="failure",
212
+ error_fingerprint="http.429.rate-limit",
213
+ sequence=1,
214
+ source_kind="dialog",
215
+ source_id="dialog-46",
216
+ authority="observed",
217
+ principal="fixture-runner",
218
+ channel="offline-evaluation",
219
+ )
220
+ conn.commit()
221
+ from threadkeeper.forget import forget_selector
222
+
223
+ assert "action_outcomes=1" in forget_selector(
224
+ "dialog-46", selector_type="uuid", conn=conn,
225
+ )
226
+ forget_selector("dialog-46", selector_type="uuid", dry_run=False, conn=conn)
227
+ assert conn.execute("SELECT COUNT(*) FROM action_outcome_events").fetchone()[0] == 0
228
+
229
+
230
+ def test_offline_comparison_sets_thresholds_without_enabling_live_ingest():
231
+ from threadkeeper import action_outcomes
232
+
233
+ fixture = {
234
+ "dialogue_only": [
235
+ "tool arguments very-sensitive-arguments tool output unbounded-output " * 20,
236
+ ],
237
+ "events": [
238
+ {
239
+ "id": "failure", "objective_ref": "objective-1",
240
+ "action_name": "github.pr.create", "outcome": "failure",
241
+ "error_fingerprint": "http.422.validation", "sequence": 1,
242
+ },
243
+ {
244
+ "id": "recovery", "objective_ref": "objective-1",
245
+ "action_name": "github.pr.create", "outcome": "success",
246
+ "sequence": 2, "recovered_from": "failure",
247
+ },
248
+ ],
249
+ "expected_recoveries": [["failure", "recovery"]],
250
+ "forbidden_substrings": ["very-sensitive-arguments", "unbounded-output"],
251
+ "thresholds": {
252
+ "min_lesson_usefulness": 1.0,
253
+ "max_privacy_leakage": 0,
254
+ "max_storage_ratio": 0.5,
255
+ "max_false_causal_links": 0,
256
+ },
257
+ }
258
+ report = action_outcomes.evaluate_offline_fixture(fixture)
259
+
260
+ assert report["thresholds_pass"] is True
261
+ assert report["bounded_action_outcomes"]["privacy_leakage"] == 0
262
+ assert report["dialogue_only"]["privacy_leakage"] == 2
263
+ assert report["bounded_action_outcomes"]["lesson_usefulness"] == 1.0
264
+ assert report["live_ingestion_allowed"] is False
265
+ assert action_outcomes.live_ingestion_allowed(True, report) is True
266
+ assert json.dumps(report)
@@ -166,6 +166,8 @@ def test_smoke_emits_headline_metrics(smoke_report):
166
166
  assert r["evidence_recall"]["rate"] == 1.0
167
167
  assert r["final_answer"]["rate"] == 1.0
168
168
  assert r["thresholds_pass"] is True
169
+ assert r["operational_recovery"]["thresholds_pass"] is True
170
+ assert r["operational_recovery"]["live_ingestion_allowed"] is False
169
171
 
170
172
 
171
173
  def test_smoke_covers_every_versioned_axis(smoke_report):
@@ -229,6 +229,35 @@ def test_prune_then_vacuum_checkpoint_round_trip(fresh_mp, monkeypatch):
229
229
  assert _count(conn, "notes", "content='after prune'") == 1
230
230
 
231
231
 
232
+ def test_retention_expires_bounded_action_outcomes(fresh_mp, monkeypatch):
233
+ from threadkeeper import action_outcomes, retention
234
+
235
+ conn = fresh_mp["db"].get_db()
236
+ action_outcomes.record_evaluation_event(
237
+ conn,
238
+ event_id="ao-retention",
239
+ objective_ref="objective-retention",
240
+ action_name="github.pr.create",
241
+ outcome="failure",
242
+ error_fingerprint="http.429.rate-limit",
243
+ sequence=1,
244
+ source_kind="dialog",
245
+ source_id="dialog-retention",
246
+ authority="observed",
247
+ principal="fixture-runner",
248
+ channel="offline-evaluation",
249
+ created_at=int(time.time()) - 3 * 86400,
250
+ retention_days=1,
251
+ )
252
+ conn.commit()
253
+
254
+ _retention_config(monkeypatch, retention)
255
+ out = retention.run_retention_pass(force=True)
256
+
257
+ assert "action_outcomes=1" in out
258
+ assert _count(conn, "action_outcome_events") == 0
259
+
260
+
232
261
  def test_vacuum_rebases_dialog_cursors_and_rebuilds_fts(fresh_mp, monkeypatch):
233
262
  """VACUUM may renumber dialog_messages' implicit rowids once dialog rows
234
263
  were pruned. The retention vacuum must rebase the rowid ingest cursors to