threadkeeper 0.20.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.20.0 → threadkeeper-0.22.0}/PKG-INFO +60 -4
- threadkeeper-0.20.0/threadkeeper.egg-info/PKG-INFO → threadkeeper-0.22.0/README.md +58 -49
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/pyproject.toml +4 -2
- threadkeeper-0.22.0/tests/test_action_outcomes.py +266 -0
- threadkeeper-0.22.0/tests/test_brief_hook_size.py +69 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_brief_sections.py +43 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_curator.py +164 -90
- threadkeeper-0.22.0/tests/test_dialectic_drift_eval.py +97 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_evolve_applier.py +13 -1
- threadkeeper-0.22.0/tests/test_evolve_apply_48.py +92 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_forget.py +66 -0
- threadkeeper-0.22.0/tests/test_lesson_index.py +76 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_memory_eval.py +35 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_retention.py +29 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_slim.py +104 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_watchdog.py +59 -10
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/_setup.py +10 -0
- threadkeeper-0.22.0/threadkeeper/action_outcomes.py +414 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/authority.py +5 -2
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/brief.py +53 -1
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/candidate_reviewer.py +5 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/curator.py +295 -149
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/db.py +33 -0
- threadkeeper-0.22.0/threadkeeper/dialectic_drift_eval.py +321 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/dialectic_validator.py +5 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/forget.py +142 -11
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/instructions.py +5 -2
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/lessons.py +49 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/retention.py +8 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/spawn_budget.py +38 -45
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/curator.py +51 -18
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/spawn.py +62 -2
- threadkeeper-0.20.0/README.md → threadkeeper-0.22.0/threadkeeper.egg-info/PKG-INFO +105 -2
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper.egg-info/SOURCES.txt +7 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper.egg-info/requires.txt +1 -1
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/LICENSE +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/setup.cfg +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_adapters.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_agent_status.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_antigravity_ingest.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_auto_update.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_backup.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_brief_footprint.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_candidate_reviewer.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_ci_parallel.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_concepts.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_config_settings.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_config_watcher.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_core_memory.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_daemon_host_config.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_daemon_host_integration.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_daemon_liveness.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_daemon_state.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dashboard.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dashboard_app.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_db_compact.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_db_contention.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_db_leak_guard.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_db_migration_fts.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_db_runtime.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_db_schema.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_delegated_search.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dialectic.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dialectic_feed_tools.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dialectic_miner.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dialectic_observation_resolve.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dialectic_recompute.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dialectic_tier.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dialectic_validator.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_dialog_fts_external.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_documentation_inventory.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_egress_policy.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_embed_routing.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_embedding_generation.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_embedding_model_revision.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_embedding_storage_dedup.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_error_paths.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_eval_harness.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_2.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_3.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_43.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_evolve_apply_45.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_evolve_daemon.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_extract_daemon.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_extract_dedup.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_github_budget.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_github_safety.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_helpers.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_hook_child_guard.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_host_embed.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_host_env.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_host_main.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_host_wedged.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_hybrid_retrieval.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_i18n_multilang.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_identity.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_ingest_cursor.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_ingest_denylist.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_ingest_secret_redaction.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_ingest_status.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_ingest_transactions.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_lesson_violations.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_lessons.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_mcp_protocol_conformance.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_mcp_resources_prompts.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_mcp_sdk_compat.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_mcp_server_instructions.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_mcp_skills_extension.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_mcp_tasks.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_memory_authority_lifecycle.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_memory_compatibility.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_memory_guard.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_memory_guard_host.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_menubar_app.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_missed_spawns.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_model_catalog.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_model_router.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_model_router_digest.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_model_router_tools.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_model_routes.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_notify_daemon.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_nudges.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_onnx_embeddings.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_panel.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_permissions.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_pickup.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_probe_daemon.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_process_health.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_project_scope.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_release_workflows.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_review_prompts.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_search_fts_punctuation.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_session_read_purity.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_setup.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_shadow_review.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_single_flight_prompt_prefixes.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_skill_hint.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_skill_passive_tier.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_skill_tier.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_skill_updater.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_skill_use_parser.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_skill_watcher.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_skills.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_background_cwd.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_budget.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_child_env.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_codex_stdin.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_config.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_hint.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_permission_gate.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_reap.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_result.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_worktree_isolation.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_spawn_wrap.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_storage.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_sync_capture.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_sync_identity.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_sync_migrate.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_sync_protocol.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_sync_reset_node.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_sync_transport.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_task_retention.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_task_spool.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_thin_session.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_thread_janitor.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_threads.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_tool_annotations.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_tool_profiles.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_tools_smoke.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_tracing.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_validate_threads.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_vec_search.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_verify_ingest.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/tests/test_workflow_retention.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/__init__.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/_mcp.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/_spawn_wrap.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/__init__.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/_hook_helpers.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/antigravity.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/base.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/claude_code.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/claude_desktop.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/codex.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/copilot.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/adapters/vscode.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/agent_metadata.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/agent_status.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/Info.plist +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/README.md +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/ThreadKeeperAgentStatus.swift +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/build.sh +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/assets/macos-agent-status/install.sh +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/auto_update.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/backup.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/config.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/config_io.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/config_watcher.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/curator_snapshots.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/daemon_liveness.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/daemon_state.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/daemon_supervisor.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/dashboard_app.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/dialectic_miner.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/egress.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/elicitation.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/embeddings.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/eval/__init__.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/eval/__main__.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/eval/harness.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/evolve_applier.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/evolve_daemon.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/extract_daemon.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/github_budget.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/github_safety.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/harvest.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/helpers.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/host.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/host_embed.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/i18n.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/identity.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/ingest.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/lesson_violations.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/link_health.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/mcp_skills.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/mcp_tasks.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/memory_compat.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/memory_guard.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/menubar_app.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/migrate_embeddings.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/model_catalog.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/model_router.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/model_router_digest.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/model_routes.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/notify.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/nudges.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/permissions.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/probe_daemon.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/process_health.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/project_scope.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/protocol.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/read_context.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/retrieval.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/review_prompts.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/search_proxy.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/server.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/shadow_review.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/skill_audit.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/skill_updater.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/skill_watcher.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/spawn_config.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/spawn_result.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/storage.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/sync/__init__.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/sync/capture.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/sync/daemon.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/sync/identity.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/sync/migrate.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/sync/protocol.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/sync/reset_node.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/sync/server.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/task_spool.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/thread_janitor.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tool_profiles.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tool_schemas.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/__init__.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/agent_status.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/authority.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/candidate_reviewer.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/capabilities.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/concepts.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/config_watch.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/consolidate.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/core_memory.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/correlation.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/dashboard.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/db_maintenance.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/dialectic.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/dialectic_feed.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/dialog.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/distill.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/evolve_applier.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/evolve_research.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/extract.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/forget.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/graph.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/invariants.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/lessons.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/memory_guard.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/missed_spawns.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/model_router.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/panel.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/peers.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/pickup.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/probes.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/process_health.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/project_scope.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/prompts.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/resources.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/session.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/shadow_review.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/skills.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/style.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/sync.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/threads.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tools/validate.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/tracing.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/trash.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/verify_ingest.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/wikilinks.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper/workflow_artifacts.py +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper.egg-info/dependency_links.txt +0 -0
- {threadkeeper-0.20.0 → threadkeeper-0.22.0}/threadkeeper.egg-info/entry_points.txt +0 -0
- {threadkeeper-0.20.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"
|
|
@@ -972,8 +972,12 @@ recovery snapshot right before its first mutating evaluator.
|
|
|
972
972
|
its token cost); the evaluator then judges currency from local evidence only.
|
|
973
973
|
A batch that holds no skills never gets a researcher: the fixed research topics
|
|
974
974
|
only concern skills, so its evaluator starts at once.
|
|
975
|
-
|
|
976
|
-
|
|
975
|
+
Curator pass and snapshot values are explicit, per-child launch data rather
|
|
976
|
+
than temporary server-process environment. Ordinary foreground and daemon
|
|
977
|
+
spawns strip stale inherited values. The private launcher binds each task,
|
|
978
|
+
child identity, pass, batch, phase, role, origin, and exact
|
|
979
|
+
report/research destination before launch; a watchdog retry restores only that
|
|
980
|
+
recorded data.
|
|
977
981
|
`curator_report_write` records its SHA-256 in `curator_report_provenance`.
|
|
978
982
|
This makes the report directory an untrusted transport: a stray or forged
|
|
979
983
|
`REPORT-*.md` file cannot acquire the provenance needed by the applier.
|
|
@@ -1055,6 +1059,18 @@ within an 8-second budget per call; a call that runs out falls back to the
|
|
|
1055
1059
|
lexical ranking and the next call resumes. Each line shows the lesson's summary. Without a query a session
|
|
1056
1060
|
would only ever see the newest few lessons, whatever the task.
|
|
1057
1061
|
|
|
1062
|
+
**Lesson index.** Agents rarely search lessons on their own, so every lesson
|
|
1063
|
+
write also refreshes `lesson-index.md` next to `lessons.md`
|
|
1064
|
+
(`~/.threadkeeper/lesson-index.md` by default), a sorted list of all lesson
|
|
1065
|
+
titles (about 35 000 characters for 550 lessons). The managed
|
|
1066
|
+
instructions import it (`@~/.threadkeeper/lesson-index.md`), so a CLI that
|
|
1067
|
+
supports imports, such as Claude Code, loads every title with its instructions;
|
|
1068
|
+
other CLIs are told to read the file at session start. Each title is the rule,
|
|
1069
|
+
and the agent opens the ones that apply with `lesson_get`. On 160 real user
|
|
1070
|
+
prompts an agent that saw only this index opened 65-70% of the lessons judged
|
|
1071
|
+
relevant, and 51-59% of its picks were relevant. Injecting the lessons whose
|
|
1072
|
+
embedding is closest to the prompt reached 25% recall at 12% precision.
|
|
1073
|
+
|
|
1058
1074
|
Lesson access is tracked the same way skill access is: `lesson_list` increments
|
|
1059
1075
|
`lesson_usage.view_count` for displayed rows and `lesson_get` increments
|
|
1060
1076
|
`lesson_usage.use_count` for the returned lesson. Only foreground reads count:
|
|
@@ -1448,6 +1464,13 @@ claim and its evidence but closes the old valid-time interval at the new claim's
|
|
|
1448
1464
|
slice; `dialectic_review(as_of=...)` and
|
|
1449
1465
|
`dialectic_synthesis(include_history=True)` expose past validity intervals.
|
|
1450
1466
|
|
|
1467
|
+
**Durable-preference drift pilot.** Run
|
|
1468
|
+
`python -m threadkeeper.dialectic_drift_eval --json` to replay the fixed,
|
|
1469
|
+
anonymized durable-change / task-exception / ambiguity / poisoning corpus. It
|
|
1470
|
+
reports supersession precision and recall, false durable-flip and clarification
|
|
1471
|
+
rates, poisoning containment, cited memory IDs, and counterfactuals. The pilot
|
|
1472
|
+
is read-only and does not change the live supersession policy.
|
|
1473
|
+
|
|
1451
1474
|
**Source-based evidence discount.** Each evidence row's effective weight
|
|
1452
1475
|
is `base_weight × discount(WRITE_ORIGIN)`. Foreground (direct user / human
|
|
1453
1476
|
signal) = 1.0. shadow_review / background_review / candidate_review /
|
|
@@ -2251,6 +2274,21 @@ It reports these headline groups over a fixed ground-truth set:
|
|
|
2251
2274
|
- **retrieval latency** — mean / p50 / p95 / max wall-clock milliseconds. With
|
|
2252
2275
|
`--semantic`, the backend is reported as `hybrid`, because dense candidates
|
|
2253
2276
|
augment rather than replace FTS.
|
|
2277
|
+
- **forgetting lifecycle** — the bundled mutation fixture writes a fact,
|
|
2278
|
+
corrects it twice, supersedes its dialectic claim, consolidates duplicate
|
|
2279
|
+
verbatim evidence, forgets the old source, and reopens the store in a new
|
|
2280
|
+
process. It reports a separate score for thread notes, verbatim memory,
|
|
2281
|
+
dialectic claims, and lessons or skills.
|
|
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
|
+
|
|
2254
2292
|
- **outcome classification** — retrieval failure, reasoning failure,
|
|
2255
2293
|
stale-memory use, removed-memory use, and privacy-policy violation.
|
|
2256
2294
|
|
|
@@ -2261,6 +2299,17 @@ unavailable when the optional semantic extra is not installed. `--strict`
|
|
|
2261
2299
|
requires the versioned fixture thresholds and zero safety violations, making
|
|
2262
2300
|
the same command suitable for retrieval/model migration gates.
|
|
2263
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
|
+
|
|
2264
2313
|
For DB concurrency, run the reproducible local gate:
|
|
2265
2314
|
|
|
2266
2315
|
```bash
|
|
@@ -2286,6 +2335,13 @@ retrieved context, or replayed answer is written to stdout, artifacts, or the
|
|
|
2286
2335
|
repository. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the corpus
|
|
2287
2336
|
contract and migration-gate interpretation.
|
|
2288
2337
|
|
|
2338
|
+
The lifecycle score gives current evidence `+1` and subtracts `0.5` for every
|
|
2339
|
+
obsolete or invalidated fact that the same retrieval path returns. A current
|
|
2340
|
+
fact alongside stale evidence therefore does not receive full credit. The
|
|
2341
|
+
post-forgetting and post-restart stages must score `1.0` in both deterministic
|
|
2342
|
+
FTS and hybrid variants; the JSON report names the lifecycle stage and memory
|
|
2343
|
+
class for any release-gate failure.
|
|
2344
|
+
|
|
2289
2345
|
## Evaluating learning-loop decision quality
|
|
2290
2346
|
|
|
2291
2347
|
`verify_ingest` answers *"did we capture the data?"*. The decision-quality
|
|
@@ -1,50 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: threadkeeper
|
|
3
|
-
Version: 0.20.0
|
|
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
|
-
Author: thread-keeper contributors
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/po4erk91/thread-keeper
|
|
8
|
-
Project-URL: Repository, https://github.com/po4erk91/thread-keeper
|
|
9
|
-
Project-URL: Issues, https://github.com/po4erk91/thread-keeper/issues
|
|
10
|
-
Project-URL: Documentation, https://github.com/po4erk91/thread-keeper#readme
|
|
11
|
-
Project-URL: Changelog, https://github.com/po4erk91/thread-keeper/releases
|
|
12
|
-
Keywords: mcp,model-context-protocol,claude,codex,antigravity,copilot,memory,agents,self-improving,skills
|
|
13
|
-
Classifier: Development Status :: 4 - Beta
|
|
14
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
-
Classifier: Operating System :: MacOS
|
|
16
|
-
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
-
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
-
Requires-Python: >=3.11
|
|
23
|
-
Description-Content-Type: text/markdown
|
|
24
|
-
License-File: LICENSE
|
|
25
|
-
Requires-Dist: mcp<3,>=2.3.0
|
|
26
|
-
Requires-Dist: pydantic>=2.13.4
|
|
27
|
-
Requires-Dist: pydantic-settings>=2.14.2
|
|
28
|
-
Requires-Dist: pyyaml>=6.0.3
|
|
29
|
-
Provides-Extra: semantic
|
|
30
|
-
Requires-Dist: fastembed<0.9,>=0.8; extra == "semantic"
|
|
31
|
-
Requires-Dist: huggingface-hub>=0.20; extra == "semantic"
|
|
32
|
-
Requires-Dist: numpy>=1.24.0; extra == "semantic"
|
|
33
|
-
Requires-Dist: sqlite-vec>=0.1.9; extra == "semantic"
|
|
34
|
-
Provides-Extra: semantic-st
|
|
35
|
-
Requires-Dist: sentence-transformers>=2.2.0; extra == "semantic-st"
|
|
36
|
-
Requires-Dist: numpy>=1.24.0; extra == "semantic-st"
|
|
37
|
-
Requires-Dist: sqlite-vec>=0.1.9; extra == "semantic-st"
|
|
38
|
-
Provides-Extra: encrypted
|
|
39
|
-
Requires-Dist: keyring>=25.0; extra == "encrypted"
|
|
40
|
-
Requires-Dist: sqlcipher3-wheels>=0.5.7; extra == "encrypted"
|
|
41
|
-
Provides-Extra: dev
|
|
42
|
-
Requires-Dist: pytest>=9.1.1; extra == "dev"
|
|
43
|
-
Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
|
|
44
|
-
Requires-Dist: pytest-forked>=1.6; extra == "dev"
|
|
45
|
-
Requires-Dist: pytest-xdist>=3.5; extra == "dev"
|
|
46
|
-
Dynamic: license-file
|
|
47
|
-
|
|
48
1
|
# thread-keeper
|
|
49
2
|
|
|
50
3
|
[](https://github.com/po4erk91/thread-keeper/actions/workflows/test.yml)
|
|
@@ -972,8 +925,12 @@ recovery snapshot right before its first mutating evaluator.
|
|
|
972
925
|
its token cost); the evaluator then judges currency from local evidence only.
|
|
973
926
|
A batch that holds no skills never gets a researcher: the fixed research topics
|
|
974
927
|
only concern skills, so its evaluator starts at once.
|
|
975
|
-
|
|
976
|
-
|
|
928
|
+
Curator pass and snapshot values are explicit, per-child launch data rather
|
|
929
|
+
than temporary server-process environment. Ordinary foreground and daemon
|
|
930
|
+
spawns strip stale inherited values. The private launcher binds each task,
|
|
931
|
+
child identity, pass, batch, phase, role, origin, and exact
|
|
932
|
+
report/research destination before launch; a watchdog retry restores only that
|
|
933
|
+
recorded data.
|
|
977
934
|
`curator_report_write` records its SHA-256 in `curator_report_provenance`.
|
|
978
935
|
This makes the report directory an untrusted transport: a stray or forged
|
|
979
936
|
`REPORT-*.md` file cannot acquire the provenance needed by the applier.
|
|
@@ -1055,6 +1012,18 @@ within an 8-second budget per call; a call that runs out falls back to the
|
|
|
1055
1012
|
lexical ranking and the next call resumes. Each line shows the lesson's summary. Without a query a session
|
|
1056
1013
|
would only ever see the newest few lessons, whatever the task.
|
|
1057
1014
|
|
|
1015
|
+
**Lesson index.** Agents rarely search lessons on their own, so every lesson
|
|
1016
|
+
write also refreshes `lesson-index.md` next to `lessons.md`
|
|
1017
|
+
(`~/.threadkeeper/lesson-index.md` by default), a sorted list of all lesson
|
|
1018
|
+
titles (about 35 000 characters for 550 lessons). The managed
|
|
1019
|
+
instructions import it (`@~/.threadkeeper/lesson-index.md`), so a CLI that
|
|
1020
|
+
supports imports, such as Claude Code, loads every title with its instructions;
|
|
1021
|
+
other CLIs are told to read the file at session start. Each title is the rule,
|
|
1022
|
+
and the agent opens the ones that apply with `lesson_get`. On 160 real user
|
|
1023
|
+
prompts an agent that saw only this index opened 65-70% of the lessons judged
|
|
1024
|
+
relevant, and 51-59% of its picks were relevant. Injecting the lessons whose
|
|
1025
|
+
embedding is closest to the prompt reached 25% recall at 12% precision.
|
|
1026
|
+
|
|
1058
1027
|
Lesson access is tracked the same way skill access is: `lesson_list` increments
|
|
1059
1028
|
`lesson_usage.view_count` for displayed rows and `lesson_get` increments
|
|
1060
1029
|
`lesson_usage.use_count` for the returned lesson. Only foreground reads count:
|
|
@@ -1448,6 +1417,13 @@ claim and its evidence but closes the old valid-time interval at the new claim's
|
|
|
1448
1417
|
slice; `dialectic_review(as_of=...)` and
|
|
1449
1418
|
`dialectic_synthesis(include_history=True)` expose past validity intervals.
|
|
1450
1419
|
|
|
1420
|
+
**Durable-preference drift pilot.** Run
|
|
1421
|
+
`python -m threadkeeper.dialectic_drift_eval --json` to replay the fixed,
|
|
1422
|
+
anonymized durable-change / task-exception / ambiguity / poisoning corpus. It
|
|
1423
|
+
reports supersession precision and recall, false durable-flip and clarification
|
|
1424
|
+
rates, poisoning containment, cited memory IDs, and counterfactuals. The pilot
|
|
1425
|
+
is read-only and does not change the live supersession policy.
|
|
1426
|
+
|
|
1451
1427
|
**Source-based evidence discount.** Each evidence row's effective weight
|
|
1452
1428
|
is `base_weight × discount(WRITE_ORIGIN)`. Foreground (direct user / human
|
|
1453
1429
|
signal) = 1.0. shadow_review / background_review / candidate_review /
|
|
@@ -2251,6 +2227,21 @@ It reports these headline groups over a fixed ground-truth set:
|
|
|
2251
2227
|
- **retrieval latency** — mean / p50 / p95 / max wall-clock milliseconds. With
|
|
2252
2228
|
`--semantic`, the backend is reported as `hybrid`, because dense candidates
|
|
2253
2229
|
augment rather than replace FTS.
|
|
2230
|
+
- **forgetting lifecycle** — the bundled mutation fixture writes a fact,
|
|
2231
|
+
corrects it twice, supersedes its dialectic claim, consolidates duplicate
|
|
2232
|
+
verbatim evidence, forgets the old source, and reopens the store in a new
|
|
2233
|
+
process. It reports a separate score for thread notes, verbatim memory,
|
|
2234
|
+
dialectic claims, and lessons or skills.
|
|
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
|
+
|
|
2254
2245
|
- **outcome classification** — retrieval failure, reasoning failure,
|
|
2255
2246
|
stale-memory use, removed-memory use, and privacy-policy violation.
|
|
2256
2247
|
|
|
@@ -2261,6 +2252,17 @@ unavailable when the optional semantic extra is not installed. `--strict`
|
|
|
2261
2252
|
requires the versioned fixture thresholds and zero safety violations, making
|
|
2262
2253
|
the same command suitable for retrieval/model migration gates.
|
|
2263
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
|
+
|
|
2264
2266
|
For DB concurrency, run the reproducible local gate:
|
|
2265
2267
|
|
|
2266
2268
|
```bash
|
|
@@ -2286,6 +2288,13 @@ retrieved context, or replayed answer is written to stdout, artifacts, or the
|
|
|
2286
2288
|
repository. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the corpus
|
|
2287
2289
|
contract and migration-gate interpretation.
|
|
2288
2290
|
|
|
2291
|
+
The lifecycle score gives current evidence `+1` and subtracts `0.5` for every
|
|
2292
|
+
obsolete or invalidated fact that the same retrieval path returns. A current
|
|
2293
|
+
fact alongside stale evidence therefore does not receive full credit. The
|
|
2294
|
+
post-forgetting and post-restart stages must score `1.0` in both deterministic
|
|
2295
|
+
FTS and hybrid variants; the JSON report names the lifecycle stage and memory
|
|
2296
|
+
class for any release-gate failure.
|
|
2297
|
+
|
|
2289
2298
|
## Evaluating learning-loop decision quality
|
|
2290
2299
|
|
|
2291
2300
|
`verify_ingest` answers *"did we capture the data?"*. The decision-quality
|
|
@@ -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)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""The SessionStart brief stays under the CLI's hook output limit."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
HOOK = Path(__file__).resolve().parents[1] / "scripts" / "hooks" / "tk-brief.sh"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _run_hook(tmp_path: Path, context_lines: int) -> str:
|
|
13
|
+
fake_python = tmp_path / "python"
|
|
14
|
+
body = "\\n".join(f"rule {i}: " + "x" * 60 for i in range(context_lines))
|
|
15
|
+
fake_python.write_text(
|
|
16
|
+
"#!/bin/sh\ncat >/dev/null\n"
|
|
17
|
+
f"printf 'STATUS\\tthread-keeper: ok\\nCONTEXT\\t{body}\\n'\n",
|
|
18
|
+
encoding="utf-8",
|
|
19
|
+
)
|
|
20
|
+
fake_python.chmod(0o700)
|
|
21
|
+
env = {
|
|
22
|
+
"PATH": os.environ.get("PATH", "/usr/bin:/bin"),
|
|
23
|
+
"HOME": str(tmp_path),
|
|
24
|
+
"THREADKEEPER_PYTHON": str(fake_python),
|
|
25
|
+
}
|
|
26
|
+
proc = subprocess.run(
|
|
27
|
+
["bash", str(HOOK)], input="{}", capture_output=True, text=True, env=env, timeout=30,
|
|
28
|
+
)
|
|
29
|
+
assert proc.returncode == 0, proc.stderr
|
|
30
|
+
return json.loads(proc.stdout)["hookSpecificOutput"]["additionalContext"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_oversized_brief_is_cut_at_a_line_break_under_the_hook_limit(tmp_path):
|
|
34
|
+
context = _run_hook(tmp_path, context_lines=400)
|
|
35
|
+
|
|
36
|
+
assert len(context) <= 9000
|
|
37
|
+
assert context.endswith("call brief() for the rest]")
|
|
38
|
+
assert "\nrule 0: " in context
|
|
39
|
+
kept = context.splitlines()[-2]
|
|
40
|
+
assert kept.startswith("rule ") and kept.endswith("x" * 60)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_brief_under_the_limit_is_passed_through_whole(tmp_path):
|
|
44
|
+
context = _run_hook(tmp_path, context_lines=20)
|
|
45
|
+
|
|
46
|
+
assert "rule 19: " in context
|
|
47
|
+
assert "trimmed" not in context
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_brief_without_line_breaks_is_cut_at_the_limit(tmp_path):
|
|
51
|
+
fake_python = tmp_path / "python"
|
|
52
|
+
fake_python.write_text(
|
|
53
|
+
"#!/bin/sh\ncat >/dev/null\n"
|
|
54
|
+
"printf 'STATUS\\tok\\nCONTEXT\\t%s\\n' \"$(head -c 20000 /dev/zero | tr '\\0' x)\"\n",
|
|
55
|
+
encoding="utf-8",
|
|
56
|
+
)
|
|
57
|
+
fake_python.chmod(0o700)
|
|
58
|
+
env = {
|
|
59
|
+
"PATH": os.environ.get("PATH", "/usr/bin:/bin"),
|
|
60
|
+
"HOME": str(tmp_path),
|
|
61
|
+
"THREADKEEPER_PYTHON": str(fake_python),
|
|
62
|
+
}
|
|
63
|
+
proc = subprocess.run(
|
|
64
|
+
["bash", str(HOOK)], input="{}", capture_output=True, text=True, env=env, timeout=30,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
context = json.loads(proc.stdout)["hookSpecificOutput"]["additionalContext"]
|
|
68
|
+
assert 8000 < len(context) <= 9000
|
|
69
|
+
assert context.endswith("call brief() for the rest]")
|
|
@@ -146,6 +146,49 @@ def test_consulted_skills_silent_without_events(mp_with_cid):
|
|
|
146
146
|
assert "consulted_skills" not in txt
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
def test_consulted_lessons_surface_recall_telemetry_and_keep_brief_sections(
|
|
150
|
+
mp_with_cid,
|
|
151
|
+
):
|
|
152
|
+
"""A recalled lesson keeps its read telemetry visible without dropping
|
|
153
|
+
the existing thread, evolve, or user-facing brief sections."""
|
|
154
|
+
pkg = mp_with_cid(_FAKE_CID)
|
|
155
|
+
lesson_append = _tool(pkg, "lesson_append")
|
|
156
|
+
lesson_list = _tool(pkg, "lesson_list")
|
|
157
|
+
lesson_get = _tool(pkg, "lesson_get")
|
|
158
|
+
open_thread = _tool(pkg, "open_thread")
|
|
159
|
+
|
|
160
|
+
assert lesson_append(
|
|
161
|
+
title="Recall telemetry", body="Use this lesson for brief coverage.",
|
|
162
|
+
source="shadow",
|
|
163
|
+
).startswith("ok")
|
|
164
|
+
lesson_list(k=1)
|
|
165
|
+
assert "Use this lesson for brief coverage." in lesson_get(
|
|
166
|
+
slug="recall-telemetry"
|
|
167
|
+
)
|
|
168
|
+
open_thread(question="keep the open section")
|
|
169
|
+
|
|
170
|
+
conn = pkg["db"].get_db()
|
|
171
|
+
conn.execute(
|
|
172
|
+
"INSERT INTO evolve (suggestion, rationale, applied, status, created_at) "
|
|
173
|
+
"VALUES (?, ?, 0, 'promoted', ?)",
|
|
174
|
+
("seeded promoted suggestion", "brief coverage", int(time.time())),
|
|
175
|
+
)
|
|
176
|
+
conn.commit()
|
|
177
|
+
|
|
178
|
+
txt = _brief_text(pkg)
|
|
179
|
+
|
|
180
|
+
assert "consulted_lessons" in txt
|
|
181
|
+
assert "recall-telemetry" in txt
|
|
182
|
+
assert "viewed×1" in txt
|
|
183
|
+
assert "used×1" in txt
|
|
184
|
+
assert "last_recalled=" in txt
|
|
185
|
+
assert "open" in txt
|
|
186
|
+
assert "keep the open section" in txt
|
|
187
|
+
assert "evolve_pending" in txt
|
|
188
|
+
assert "★ \"seeded promoted suggestion\"" in txt
|
|
189
|
+
assert "⚠️ user-facing: paraphrase plain." in txt
|
|
190
|
+
|
|
191
|
+
|
|
149
192
|
def test_skill_nudge_silent_after_materialization(mp_with_cid, monkeypatch):
|
|
150
193
|
monkeypatch.setenv("THREADKEEPER_SKILL_NUDGE_INTERVAL", "3")
|
|
151
194
|
pkg = mp_with_cid(_FAKE_CID)
|