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.
- {threadkeeper-0.21.0/threadkeeper.egg-info → threadkeeper-0.22.0}/PKG-INFO +22 -2
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/README.md +20 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/pyproject.toml +4 -2
- threadkeeper-0.22.0/tests/test_action_outcomes.py +266 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_memory_eval.py +2 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_retention.py +29 -0
- threadkeeper-0.22.0/threadkeeper/action_outcomes.py +414 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/authority.py +1 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/candidate_reviewer.py +5 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/db.py +33 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/forget.py +51 -10
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/retention.py +8 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0/threadkeeper.egg-info}/PKG-INFO +22 -2
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper.egg-info/SOURCES.txt +2 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper.egg-info/requires.txt +1 -1
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/LICENSE +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/setup.cfg +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_adapters.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_agent_status.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_antigravity_ingest.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_auto_update.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_backup.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_brief_footprint.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_brief_hook_size.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_brief_sections.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_candidate_reviewer.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_ci_parallel.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_concepts.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_config_settings.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_config_watcher.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_core_memory.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_curator.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_daemon_host_config.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_daemon_host_integration.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_daemon_liveness.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_daemon_state.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dashboard.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dashboard_app.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_db_compact.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_db_contention.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_db_leak_guard.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_db_migration_fts.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_db_runtime.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_db_schema.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_delegated_search.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialectic.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialectic_drift_eval.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialectic_feed_tools.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialectic_miner.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialectic_observation_resolve.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialectic_recompute.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialectic_tier.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialectic_validator.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_dialog_fts_external.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_documentation_inventory.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_egress_policy.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_embed_routing.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_embedding_generation.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_embedding_model_revision.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_embedding_storage_dedup.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_error_paths.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_eval_harness.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_evolve_applier.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_2.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_3.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_43.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_45.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_48.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_evolve_daemon.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_extract_daemon.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_extract_dedup.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_forget.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_github_budget.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_github_safety.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_helpers.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_hook_child_guard.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_host_embed.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_host_env.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_host_main.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_host_wedged.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_hybrid_retrieval.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_i18n_multilang.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_identity.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_ingest_cursor.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_ingest_denylist.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_ingest_secret_redaction.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_ingest_status.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_ingest_transactions.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_lesson_index.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_lesson_violations.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_lessons.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_mcp_protocol_conformance.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_mcp_resources_prompts.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_mcp_sdk_compat.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_mcp_server_instructions.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_mcp_skills_extension.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_mcp_tasks.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_memory_authority_lifecycle.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_memory_compatibility.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_memory_guard.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_memory_guard_host.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_menubar_app.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_missed_spawns.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_model_catalog.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_model_router.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_model_router_digest.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_model_router_tools.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_model_routes.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_notify_daemon.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_nudges.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_onnx_embeddings.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_panel.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_permissions.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_pickup.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_probe_daemon.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_process_health.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_project_scope.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_release_workflows.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_review_prompts.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_search_fts_punctuation.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_session_read_purity.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_setup.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_shadow_review.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_single_flight_prompt_prefixes.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_skill_hint.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_skill_passive_tier.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_skill_tier.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_skill_updater.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_skill_use_parser.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_skill_watcher.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_skills.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_background_cwd.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_budget.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_child_env.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_codex_stdin.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_config.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_hint.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_permission_gate.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_reap.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_result.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_slim.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_watchdog.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_worktree_isolation.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_spawn_wrap.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_storage.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_sync_capture.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_sync_identity.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_sync_migrate.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_sync_protocol.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_sync_reset_node.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_sync_transport.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_task_retention.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_task_spool.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_thin_session.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_thread_janitor.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_threads.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_tool_annotations.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_tool_profiles.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_tools_smoke.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_tracing.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_validate_threads.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_vec_search.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_verify_ingest.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/tests/test_workflow_retention.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/__init__.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/_mcp.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/_setup.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/_spawn_wrap.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/__init__.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/_hook_helpers.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/antigravity.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/base.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/claude_code.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/claude_desktop.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/codex.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/copilot.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/adapters/vscode.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/agent_metadata.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/agent_status.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/README.md +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/auto_update.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/backup.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/brief.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/config.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/config_io.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/config_watcher.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/curator.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/curator_snapshots.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/daemon_liveness.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/daemon_state.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/daemon_supervisor.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/dashboard_app.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/dialectic_drift_eval.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/dialectic_miner.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/dialectic_validator.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/egress.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/elicitation.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/embeddings.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/eval/__init__.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/eval/__main__.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/eval/harness.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/evolve_applier.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/evolve_daemon.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/extract_daemon.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/github_budget.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/github_safety.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/harvest.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/helpers.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/host.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/host_embed.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/i18n.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/identity.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/ingest.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/instructions.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/lesson_violations.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/lessons.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/link_health.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/mcp_skills.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/mcp_tasks.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/memory_compat.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/memory_guard.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/menubar_app.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/migrate_embeddings.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/model_catalog.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/model_router.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/model_router_digest.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/model_routes.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/notify.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/nudges.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/permissions.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/probe_daemon.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/process_health.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/project_scope.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/protocol.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/read_context.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/retrieval.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/review_prompts.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/search_proxy.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/server.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/shadow_review.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/skill_audit.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/skill_updater.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/skill_watcher.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/spawn_budget.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/spawn_config.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/spawn_result.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/storage.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/sync/__init__.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/sync/capture.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/sync/daemon.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/sync/identity.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/sync/migrate.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/sync/protocol.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/sync/reset_node.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/sync/server.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/task_spool.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/thread_janitor.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tool_profiles.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tool_schemas.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/__init__.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/agent_status.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/authority.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/candidate_reviewer.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/capabilities.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/concepts.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/config_watch.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/consolidate.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/core_memory.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/correlation.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/curator.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/dashboard.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/db_maintenance.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/dialectic.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/dialectic_feed.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/dialog.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/distill.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/evolve_applier.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/evolve_research.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/extract.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/forget.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/graph.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/invariants.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/lessons.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/memory_guard.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/missed_spawns.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/model_router.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/panel.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/peers.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/pickup.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/probes.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/process_health.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/project_scope.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/prompts.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/resources.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/session.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/shadow_review.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/skills.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/spawn.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/style.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/sync.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/threads.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tools/validate.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/tracing.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/trash.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/verify_ingest.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/wikilinks.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper/workflow_artifacts.py +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper.egg-info/dependency_links.txt +0 -0
- {threadkeeper-0.21.0 → threadkeeper-0.22.0}/threadkeeper.egg-info/entry_points.txt +0 -0
- {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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|