virtual-context 0.2.2__tar.gz → 0.2.4__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.
- {virtual_context-0.2.2 → virtual_context-0.2.4}/PKG-INFO +1 -1
- {virtual_context-0.2.2 → virtual_context-0.2.4}/pyproject.toml +1 -1
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_conversation_lifecycle.py +22 -0
- virtual_context-0.2.4/tests/test_registry_lifecycle.py +41 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_store_sqlite.py +14 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_upstream_trim.py +6 -4
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/__init__.py +1 -1
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/compaction_pipeline.py +41 -6
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/composite_store.py +19 -4
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/conversation_store.py +4 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/hint_builder.py +6 -2
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/protocols.py +8 -2
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/retrieval_assembler.py +8 -3
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/retriever.py +4 -1
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/store.py +31 -2
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/tag_canonicalizer.py +26 -4
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/tag_consolidator.py +28 -2
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/tagging_pipeline.py +4 -1
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/engine.py +4 -1
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/dashboard.py +148 -45
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/formats.py +6 -111
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/handlers.py +103 -15
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/helpers.py +3 -1
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/message_filter.py +168 -58
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/registry.py +32 -2
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/server.py +58 -23
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/state.py +99 -14
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/storage/falkordb.py +16 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/storage/filesystem.py +62 -6
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/storage/neo4j.py +18 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/storage/postgres.py +81 -8
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/storage/sqlite.py +73 -8
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/types.py +38 -7
- {virtual_context-0.2.2 → virtual_context-0.2.4}/.gitignore +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/LICENSE +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/README.md +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/assets/hero.png +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/models.yaml +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/REGRESSION_MAP.md +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/conftest.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/docker-compose.test.yml +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/haiku/__init__.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/haiku/conftest.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/haiku/test_compaction.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/haiku/test_retrieval.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/haiku/test_tagging.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/ollama/__init__.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/ollama/conftest.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/ollama/test_compactor.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/ollama/test_pipeline.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/ollama/test_provider.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/ollama/test_tag_generator.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/proxy/__init__.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/proxy/test_dashboard_cors.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/proxy/test_metrics.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_assembler.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_backend_integration.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_cli_init.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_compaction_commit_prune.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_compactor.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_compactor_concurrent.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_composite_store.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_config.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_context_bleed.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_conversation_identity.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_conversation_scoping.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_embedding_tag_generator.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_empty_turn_skip.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_engine_integration.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_engine_lookback.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_engine_state.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_fact_enrichment.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_fact_graph_integration.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_fact_link_checker.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_fact_link_query.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_fact_link_types.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_fact_links_sqlite.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_find_quote.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_headless.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_history_filter.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_idf_retrieval.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_ingest_index_integrity.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_longmemeval_auth.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_mcp_server.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_message_filter.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_metrics_persistence.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_model_catalog.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_model_limits.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_monitor.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_multi_instance.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_noop_fact_link_store.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_openrouter_provider.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_paging.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_passthrough_filter.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_presets.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_prev_context_leak.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_provider_adapters.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_proxy.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_proxy_dashboard.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_proxy_formats.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_proxy_message_filter.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_proxy_session.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_proxy_streaming.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_raw_content.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_recall_all.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_request_captures_persistence.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_retriever.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_rrf_scoring.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_segmenter.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_semantic_search.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_sender_identity.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_session_cache.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_session_date.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_storage_protocols.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_stub_turn_handling.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_supersession.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_supersession_migration.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tag_canonicalizer.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tag_consolidator.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tag_generator.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tag_splitter.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_telemetry.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_telemetry_integration.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tool_loop.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tool_output_interceptor.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tool_result_filter.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tool_tags.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tui.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_turn_tag_index.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_unified_budget.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_verb_expansion.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual-context.yaml.example +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/cli/__init__.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/cli/main.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/config.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/conversation_identity.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/__init__.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/assembler.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/compactor.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/embedding_provider.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/embedding_tag_generator.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/engine_utils.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/fact_query.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/fts_preprocessor.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/llm_utils.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/math_utils.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/model_catalog.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/monitor.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/paging_manager.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/provider_adapters.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/quote_search.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/retrieval_scoring.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/search_engine.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/segmenter.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/semantic_search.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/tag_generator.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/tag_scoring.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/tag_splitter.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/telemetry.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/temporal_resolver.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/tool_loop.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/tool_query.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/turn_tag_index.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/data/anthropic-tokenizer/tokenizer.json +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/ingest/__init__.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/ingest/curator.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/ingest/date_resolver.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/ingest/parsers.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/ingest/supersession.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/mcp/__init__.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/mcp/server.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/model_limits.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/openclaw/virtual-context.mjs +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/patterns.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/presets/__init__.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/presets/agentic.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/presets/base.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/presets/coding.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/providers/__init__.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/providers/anthropic.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/providers/base.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/providers/generic_openai.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/providers/ollama_native.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/__init__.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/_envelope.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/dashboard.html +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/metrics.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/multi.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/session_cache.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/static/android-chrome-192x192.png +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/static/android-chrome-512x512.png +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/static/apple-touch-icon.png +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/static/favicon-16x16.png +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/static/favicon-32x32.png +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/static/favicon.ico +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/static/site.webmanifest +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/tool_output_interceptor.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/storage/__init__.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/storage/helpers.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/storage/noop_fact_link_store.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/token_counter.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/__init__.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/app.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/chat.tcss +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/chat_provider.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/headless.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/modals/__init__.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/modals/turn_inspector.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/state.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/widgets/__init__.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/widgets/budget_bar.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/widgets/chat_view.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/widgets/input_box.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/widgets/tag_panel.py +0 -0
- {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/widgets/turn_list.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: virtual-context
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: OS-style virtual memory for LLM session context management
|
|
5
5
|
Project-URL: Homepage, https://virtual-context.com
|
|
6
6
|
Project-URL: Repository, https://github.com/virtual-context/virtual-context
|
|
@@ -29,3 +29,25 @@ def test_conversation_store_view_blocks_stale_writes_after_delete(tmp_path):
|
|
|
29
29
|
view1 = ConversationStoreView(store, conversation_id, generation1)
|
|
30
30
|
view1.save_turn_message(conversation_id, 0, "fresh-u", "fresh-a")
|
|
31
31
|
assert store.get_turn_messages(conversation_id, [0])[0][:2] == ("fresh-u", "fresh-a")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_conversation_store_view_blocks_chain_and_tool_link_writes_after_delete(tmp_path):
|
|
35
|
+
store = SQLiteStore(tmp_path / "vc.db")
|
|
36
|
+
conversation_id = "conv-delete"
|
|
37
|
+
|
|
38
|
+
generation0 = store.activate_conversation(conversation_id)
|
|
39
|
+
view0 = ConversationStoreView(store, conversation_id, generation0)
|
|
40
|
+
store.begin_conversation_deletion(conversation_id)
|
|
41
|
+
|
|
42
|
+
with pytest.raises(StaleConversationWriteError):
|
|
43
|
+
view0.store_chain_snapshot("chain-1", conversation_id, 0, "{}", 0)
|
|
44
|
+
|
|
45
|
+
with pytest.raises(StaleConversationWriteError):
|
|
46
|
+
view0.link_turn_tool_output(conversation_id, 0, "tool-turn-1")
|
|
47
|
+
|
|
48
|
+
with pytest.raises(StaleConversationWriteError):
|
|
49
|
+
view0.link_segment_tool_output(conversation_id, "seg-1", "tool-seg-1")
|
|
50
|
+
|
|
51
|
+
assert store.get_chain_snapshot(conversation_id, "chain-1") is None
|
|
52
|
+
assert store.get_tool_outputs_for_turn(conversation_id, 0) == []
|
|
53
|
+
assert store.get_tool_outputs_for_segment(conversation_id, "seg-1") == []
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
|
|
5
|
+
from virtual_context.proxy.metrics import ProxyMetrics
|
|
6
|
+
from virtual_context.proxy.registry import SessionRegistry
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _dummy_state(conversation_id: str):
|
|
10
|
+
return SimpleNamespace(
|
|
11
|
+
engine=SimpleNamespace(
|
|
12
|
+
config=SimpleNamespace(conversation_id=conversation_id),
|
|
13
|
+
),
|
|
14
|
+
shutdown=lambda *args, **kwargs: None,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_remove_conversation_clears_all_routing_maps():
|
|
19
|
+
registry = SessionRegistry(
|
|
20
|
+
config_path=None,
|
|
21
|
+
upstream="",
|
|
22
|
+
metrics=ProxyMetrics(),
|
|
23
|
+
)
|
|
24
|
+
keep_state = _dummy_state("conv-keep")
|
|
25
|
+
delete_state = _dummy_state("conv-delete")
|
|
26
|
+
|
|
27
|
+
registry._conversations["conv-keep"] = keep_state
|
|
28
|
+
registry._conversations["conv-delete"] = delete_state
|
|
29
|
+
registry._sys_hashes = {"sys-keep": "conv-keep", "sys-delete": "conv-delete"}
|
|
30
|
+
registry._chat_ids = {"chat-keep": "conv-keep", "chat-delete": "conv-delete"}
|
|
31
|
+
registry._last_msg_hashes = {"msg-keep": "conv-keep", "msg-delete": "conv-delete"}
|
|
32
|
+
|
|
33
|
+
removed = registry.remove_conversation("conv-delete")
|
|
34
|
+
|
|
35
|
+
assert removed is delete_state
|
|
36
|
+
assert registry.get_state("conv-delete") is None
|
|
37
|
+
assert registry.get_state("conv-keep") is keep_state
|
|
38
|
+
assert "conv-delete" not in registry._conversations
|
|
39
|
+
assert "conv-delete" not in registry._sys_hashes.values()
|
|
40
|
+
assert "conv-delete" not in registry._chat_ids.values()
|
|
41
|
+
assert "conv-delete" not in registry._last_msg_hashes.values()
|
|
@@ -177,6 +177,20 @@ class TestSQLiteStore:
|
|
|
177
177
|
aliases = store.get_tag_aliases()
|
|
178
178
|
assert aliases["db"] == "database"
|
|
179
179
|
|
|
180
|
+
def test_delete_conversation_removes_only_conversation_scoped_aliases(self, store):
|
|
181
|
+
store.set_tag_alias("global-db", "database")
|
|
182
|
+
store.set_tag_alias("db", "database", conversation_id="session-1")
|
|
183
|
+
store.set_tag_alias("sql", "database", conversation_id="session-2")
|
|
184
|
+
|
|
185
|
+
store.delete_conversation("session-1")
|
|
186
|
+
|
|
187
|
+
aliases_session_1 = store.get_tag_aliases("session-1")
|
|
188
|
+
aliases_session_2 = store.get_tag_aliases("session-2")
|
|
189
|
+
assert aliases_session_1["global-db"] == "database"
|
|
190
|
+
assert "db" not in aliases_session_1
|
|
191
|
+
assert aliases_session_2["global-db"] == "database"
|
|
192
|
+
assert aliases_session_2["sql"] == "database"
|
|
193
|
+
|
|
180
194
|
def test_metadata_preserved(self, store):
|
|
181
195
|
seg = _make_segment()
|
|
182
196
|
seg.metadata = SegmentMetadata(
|
|
@@ -61,14 +61,16 @@ class TestTrimToUpstreamLimit:
|
|
|
61
61
|
assert "tools" in trimmed
|
|
62
62
|
assert len(trimmed["tools"]) == 1
|
|
63
63
|
|
|
64
|
-
def
|
|
64
|
+
def test_max_tokens_not_subtracted_from_budget(self):
|
|
65
|
+
"""max_tokens is NOT subtracted — upstream_limit is the input budget."""
|
|
65
66
|
body = _make_body(50, system="Sys.")
|
|
66
67
|
body["max_tokens"] = 50_000
|
|
67
68
|
fmt = self._fmt()
|
|
68
69
|
total_before = fmt.estimate_payload_tokens(body)
|
|
69
70
|
trimmed, removed = trim_to_upstream_limit(body, 60_000, fmt)
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
# With 60k budget and no max_tokens subtraction, small body fits
|
|
72
|
+
if total_before <= 60_000:
|
|
73
|
+
assert removed == 0
|
|
72
74
|
|
|
73
75
|
def test_anthropic_format(self):
|
|
74
76
|
body = {
|
|
@@ -116,7 +118,7 @@ class TestTrimToUpstreamLimit:
|
|
|
116
118
|
fmt = copy.copy(OpenAIResponsesFormat())
|
|
117
119
|
fmt.set_token_counter(lambda text: len(text) // 4)
|
|
118
120
|
|
|
119
|
-
trimmed, removed = trim_to_upstream_limit(body,
|
|
121
|
+
trimmed, removed = trim_to_upstream_limit(body, 1500, fmt)
|
|
120
122
|
|
|
121
123
|
assert removed > 0
|
|
122
124
|
rendered = str(trimmed["input"])
|
|
@@ -124,7 +124,10 @@ class CompactionPipeline:
|
|
|
124
124
|
# Messages to compact: everything between watermark and protected zone.
|
|
125
125
|
# Compact all available messages (not just the minimum) so compaction
|
|
126
126
|
# fires infrequently — one big batch instead of many small ones.
|
|
127
|
-
|
|
127
|
+
_total_turns = len(self._turn_tag_index.entries) if self._turn_tag_index else None
|
|
128
|
+
offset = self._engine_state.history_offset(
|
|
129
|
+
len(conversation_history), total_turns_indexed=_total_turns,
|
|
130
|
+
)
|
|
128
131
|
compact_messages = conversation_history[offset:-protected_count]
|
|
129
132
|
|
|
130
133
|
if not compact_messages:
|
|
@@ -137,9 +140,9 @@ class CompactionPipeline:
|
|
|
137
140
|
return None
|
|
138
141
|
|
|
139
142
|
logger.info(
|
|
140
|
-
"Compacting %d messages (offset=%d, watermark=%d, history=%d, protected=%d turns)",
|
|
143
|
+
"Compacting %d messages (offset=%d, watermark=%d, history=%d, protected=%d turns, indexed=%s)",
|
|
141
144
|
len(compact_messages), offset, self._engine_state.compacted_through,
|
|
142
|
-
len(conversation_history), protected_turns,
|
|
145
|
+
len(conversation_history), protected_turns, _total_turns,
|
|
143
146
|
)
|
|
144
147
|
report = self._run_compaction(conversation_history, compact_messages, progress_callback=progress_callback)
|
|
145
148
|
|
|
@@ -172,7 +175,10 @@ class CompactionPipeline:
|
|
|
172
175
|
logger.info("Not enough messages outside protected zone to compact")
|
|
173
176
|
return None
|
|
174
177
|
|
|
175
|
-
|
|
178
|
+
_total_turns = len(self._turn_tag_index.entries) if self._turn_tag_index else None
|
|
179
|
+
offset = self._engine_state.history_offset(
|
|
180
|
+
len(conversation_history), total_turns_indexed=_total_turns,
|
|
181
|
+
)
|
|
176
182
|
compact_messages = conversation_history[offset:-protected_count]
|
|
177
183
|
|
|
178
184
|
if not compact_messages:
|
|
@@ -524,6 +530,20 @@ class CompactionPipeline:
|
|
|
524
530
|
max_seg_tokens = self._config.compactor.max_segment_tokens
|
|
525
531
|
merge_threshold = self._config.compactor.merge_overlap_threshold
|
|
526
532
|
|
|
533
|
+
# ------------------------------------------------------------------
|
|
534
|
+
# Store-based skip: collect turn numbers already covered by stored
|
|
535
|
+
# tag summaries so we can skip segments that would re-compact the
|
|
536
|
+
# same turns (happens when compacted_through > history_len and
|
|
537
|
+
# history_offset() returns 0).
|
|
538
|
+
# ------------------------------------------------------------------
|
|
539
|
+
try:
|
|
540
|
+
_already_compacted_turns = self._store.get_compacted_turn_numbers(
|
|
541
|
+
self._config.conversation_id,
|
|
542
|
+
)
|
|
543
|
+
except Exception:
|
|
544
|
+
_already_compacted_turns = set()
|
|
545
|
+
_skipped_segments = 0
|
|
546
|
+
|
|
527
547
|
# ==================================================================
|
|
528
548
|
# Pass 1: Sequential pre-pass — stubs + merge check (no LLM calls)
|
|
529
549
|
# ==================================================================
|
|
@@ -537,6 +557,19 @@ class CompactionPipeline:
|
|
|
537
557
|
embed_fn = self._semantic.get_embed_fn() if self._semantic else None
|
|
538
558
|
|
|
539
559
|
for seg in segments:
|
|
560
|
+
# --- Store-based skip: already-compacted turn range ---
|
|
561
|
+
if _already_compacted_turns:
|
|
562
|
+
seg_range = segment_turn_ranges.get(seg.id)
|
|
563
|
+
if seg_range:
|
|
564
|
+
seg_turns = set(range(seg_range[0], seg_range[1]))
|
|
565
|
+
if seg_turns and seg_turns <= _already_compacted_turns:
|
|
566
|
+
_skipped_segments += 1
|
|
567
|
+
logger.info(
|
|
568
|
+
"SEGMENT SKIP (already compacted) ref=%s turns=%d-%d primary=%s",
|
|
569
|
+
seg.id[:8], seg_range[0], seg_range[1] - 1, seg.primary_tag,
|
|
570
|
+
)
|
|
571
|
+
continue
|
|
572
|
+
|
|
540
573
|
# --- Stub passthrough (no LLM) ---
|
|
541
574
|
text = " ".join(m.content for m in seg.messages)
|
|
542
575
|
if _is_stub_content_fn(text):
|
|
@@ -665,9 +698,11 @@ class CompactionPipeline:
|
|
|
665
698
|
)
|
|
666
699
|
return all_results
|
|
667
700
|
|
|
668
|
-
|
|
701
|
+
if _skipped_segments:
|
|
702
|
+
logger.info("Store-based skip: %d segments skipped (turns already compacted)", _skipped_segments)
|
|
703
|
+
logger.info("Pass 1 complete: %d stubs stored, %d segments ready for compaction (%d merges, %d skipped)",
|
|
669
704
|
len(all_results), len(compactable),
|
|
670
|
-
sum(1 for s in compactable if s.merge_ref))
|
|
705
|
+
sum(1 for s in compactable if s.merge_ref), _skipped_segments)
|
|
671
706
|
|
|
672
707
|
# ==================================================================
|
|
673
708
|
# Pass 2: Batch LLM compaction + store
|
|
@@ -89,11 +89,20 @@ class CompositeStore:
|
|
|
89
89
|
def get_conversation_stats(self) -> list[ConversationStats]:
|
|
90
90
|
return self._segments.get_conversation_stats()
|
|
91
91
|
|
|
92
|
-
def get_tag_aliases(self) -> dict[str, str]:
|
|
93
|
-
return self._segments.get_tag_aliases()
|
|
92
|
+
def get_tag_aliases(self, conversation_id: str | None = None) -> dict[str, str]:
|
|
93
|
+
return self._segments.get_tag_aliases(conversation_id=conversation_id)
|
|
94
94
|
|
|
95
|
-
def set_tag_alias(
|
|
96
|
-
|
|
95
|
+
def set_tag_alias(
|
|
96
|
+
self,
|
|
97
|
+
alias: str,
|
|
98
|
+
canonical: str,
|
|
99
|
+
conversation_id: str = "",
|
|
100
|
+
) -> None:
|
|
101
|
+
return self._segments.set_tag_alias(
|
|
102
|
+
alias,
|
|
103
|
+
canonical,
|
|
104
|
+
conversation_id=conversation_id,
|
|
105
|
+
)
|
|
97
106
|
|
|
98
107
|
def delete_segment(self, ref: str) -> bool:
|
|
99
108
|
return self._segments.delete_segment(ref)
|
|
@@ -152,6 +161,12 @@ class CompositeStore:
|
|
|
152
161
|
)
|
|
153
162
|
return deleted
|
|
154
163
|
|
|
164
|
+
def delete_tag_aliases_for_conversation(self, conversation_id: str) -> int:
|
|
165
|
+
delete_aliases = getattr(self._segments, "delete_tag_aliases_for_conversation", None)
|
|
166
|
+
if callable(delete_aliases):
|
|
167
|
+
return int(delete_aliases(conversation_id) or 0)
|
|
168
|
+
return 0
|
|
169
|
+
|
|
155
170
|
def save_turn_message(
|
|
156
171
|
self, conversation_id: str, turn_number: int,
|
|
157
172
|
user_content: str, assistant_content: str,
|
|
@@ -14,6 +14,9 @@ class ConversationStoreView:
|
|
|
14
14
|
|
|
15
15
|
_GUARDED_METHODS = {
|
|
16
16
|
"delete_segment",
|
|
17
|
+
"delete_fact_links",
|
|
18
|
+
"link_segment_tool_output",
|
|
19
|
+
"link_turn_tool_output",
|
|
17
20
|
"save_engine_state",
|
|
18
21
|
"save_request_capture",
|
|
19
22
|
"save_tag_summary",
|
|
@@ -25,6 +28,7 @@ class ConversationStoreView:
|
|
|
25
28
|
"store_chunk_embeddings",
|
|
26
29
|
"store_fact_links",
|
|
27
30
|
"store_facts",
|
|
31
|
+
"store_chain_snapshot",
|
|
28
32
|
"store_segment",
|
|
29
33
|
"store_tag_summary_embedding",
|
|
30
34
|
"store_tool_output",
|
|
@@ -59,7 +59,9 @@ def build_autonomous_hint(
|
|
|
59
59
|
"To find detailed information you have the following tools:\n"
|
|
60
60
|
"- vc_restore_tool(ref): ALL compacted tool turns can be restored to "
|
|
61
61
|
"full fidelity — thinking, tool calls, and raw output. Use the ref "
|
|
62
|
-
"from the compacted stub. This is the fastest way to get exact data
|
|
62
|
+
"from the compacted stub. This is the fastest way to get exact data "
|
|
63
|
+
"like file paths, directory listings, code blocks, command output, "
|
|
64
|
+
"and search results. Summaries never contain these — restore first.\n"
|
|
63
65
|
"- vc_find_quote(query): search raw text across ALL topics.\n"
|
|
64
66
|
"- vc_query_facts(subject?, verb?, status?, object_contains?): "
|
|
65
67
|
"structured fact lookup.\n"
|
|
@@ -73,7 +75,9 @@ def build_autonomous_hint(
|
|
|
73
75
|
"For counting/listing questions: scan [all topics] for every topic "
|
|
74
76
|
"that could relate — items are often spread across unrelated topics.\n"
|
|
75
77
|
"If a search already returned the answer, stop and respond.\n"
|
|
76
|
-
"
|
|
78
|
+
"Virtual context tools allow you to search and restore full "
|
|
79
|
+
"conversational depth and previously compacted tool calls. "
|
|
80
|
+
"Use liberally to answer the user's question.\n"
|
|
77
81
|
"FACT vs SUMMARY: The <facts> block contains structured events with "
|
|
78
82
|
"statuses (completed, planned, active). Summaries describe topics "
|
|
79
83
|
"DISCUSSED — they include plans, itineraries, and ideas that may "
|
|
@@ -35,8 +35,13 @@ class SegmentStore(Protocol):
|
|
|
35
35
|
def search(self, query: str, tags: list[str] | None = None, limit: int = 5, conversation_id: str | None = None) -> list[StoredSummary]: ...
|
|
36
36
|
def get_all_tags(self, conversation_id: str | None = None) -> list[TagStats]: ...
|
|
37
37
|
def get_conversation_stats(self) -> list[ConversationStats]: ...
|
|
38
|
-
def get_tag_aliases(self) -> dict[str, str]: ...
|
|
39
|
-
def set_tag_alias(
|
|
38
|
+
def get_tag_aliases(self, conversation_id: str | None = None) -> dict[str, str]: ...
|
|
39
|
+
def set_tag_alias(
|
|
40
|
+
self,
|
|
41
|
+
alias: str,
|
|
42
|
+
canonical: str,
|
|
43
|
+
conversation_id: str = "",
|
|
44
|
+
) -> None: ...
|
|
40
45
|
def delete_segment(self, ref: str) -> bool: ...
|
|
41
46
|
def cleanup(self, max_age: timedelta | None = None, max_total_tokens: int | None = None) -> int: ...
|
|
42
47
|
def save_tag_summary(self, tag_summary: TagSummary, conversation_id: str = "") -> None: ...
|
|
@@ -53,6 +58,7 @@ class SegmentStore(Protocol):
|
|
|
53
58
|
def search_tag_summaries_fts(self, query: str, limit: int = 20, conversation_id: str | None = None) -> list[tuple[str, float]]: ...
|
|
54
59
|
def store_tag_summary_embedding(self, tag: str, conversation_id: str, embedding: list[float]) -> None: ...
|
|
55
60
|
def load_tag_summary_embeddings(self, conversation_id: str | None = None) -> dict[str, list[float]]: ...
|
|
61
|
+
def delete_tag_aliases_for_conversation(self, conversation_id: str) -> int: ...
|
|
56
62
|
|
|
57
63
|
|
|
58
64
|
@runtime_checkable
|
|
@@ -115,7 +115,10 @@ class RetrievalAssembler:
|
|
|
115
115
|
active_tags = self._get_active_tags(conversation_history)
|
|
116
116
|
|
|
117
117
|
# Compute current utilization (only count un-compacted history)
|
|
118
|
-
|
|
118
|
+
_total_turns = len(self._turn_tag_index.entries) if self._turn_tag_index else None
|
|
119
|
+
_offset = self._engine_state.history_offset(
|
|
120
|
+
len(conversation_history), total_turns_indexed=_total_turns,
|
|
121
|
+
)
|
|
119
122
|
snapshot = self._monitor.build_snapshot(
|
|
120
123
|
conversation_history[_offset:]
|
|
121
124
|
)
|
|
@@ -267,7 +270,8 @@ class RetrievalAssembler:
|
|
|
267
270
|
ws_param, full_segments_param = self._load_working_set_segments()
|
|
268
271
|
|
|
269
272
|
_hist = history or []
|
|
270
|
-
|
|
273
|
+
_tti = len(self._turn_tag_index.entries) if self._turn_tag_index else None
|
|
274
|
+
uncompacted = _hist[self._engine_state.history_offset(len(_hist), total_turns_indexed=_tti):]
|
|
271
275
|
assembled = self._assembler.assemble(
|
|
272
276
|
core_context=core_context,
|
|
273
277
|
retrieval_result=rr,
|
|
@@ -351,7 +355,8 @@ class RetrievalAssembler:
|
|
|
351
355
|
return list(conversation_history)
|
|
352
356
|
|
|
353
357
|
# Skip compacted messages -- their content is in stored summaries
|
|
354
|
-
|
|
358
|
+
_tti2 = len(self._turn_tag_index.entries) if self._turn_tag_index else None
|
|
359
|
+
offset = self._engine_state.history_offset(total, total_turns_indexed=_tti2)
|
|
355
360
|
older = conversation_history[offset:-protected_count]
|
|
356
361
|
recent = conversation_history[-protected_count:]
|
|
357
362
|
|
|
@@ -429,7 +429,10 @@ def _alias_ride_along(
|
|
|
429
429
|
in X's alias group and retrieve segments that match those aliases but
|
|
430
430
|
weren't already selected. These ride free — no max_results cap.
|
|
431
431
|
"""
|
|
432
|
-
|
|
432
|
+
try:
|
|
433
|
+
aliases = store.get_tag_aliases(conversation_id=conversation_id)
|
|
434
|
+
except TypeError:
|
|
435
|
+
aliases = store.get_tag_aliases()
|
|
433
436
|
if not aliases:
|
|
434
437
|
return []
|
|
435
438
|
|
|
@@ -59,10 +59,15 @@ class ContextStore(ABC):
|
|
|
59
59
|
"""Return aggregate statistics grouped by conversation_id, newest first."""
|
|
60
60
|
|
|
61
61
|
@abstractmethod
|
|
62
|
-
def get_tag_aliases(self) -> dict[str, str]: ...
|
|
62
|
+
def get_tag_aliases(self, conversation_id: str | None = None) -> dict[str, str]: ...
|
|
63
63
|
|
|
64
64
|
@abstractmethod
|
|
65
|
-
def set_tag_alias(
|
|
65
|
+
def set_tag_alias(
|
|
66
|
+
self,
|
|
67
|
+
alias: str,
|
|
68
|
+
canonical: str,
|
|
69
|
+
conversation_id: str = "",
|
|
70
|
+
) -> None: ...
|
|
66
71
|
|
|
67
72
|
@abstractmethod
|
|
68
73
|
def delete_segment(self, ref: str) -> bool: ...
|
|
@@ -132,6 +137,23 @@ class ContextStore(ABC):
|
|
|
132
137
|
existing conversations when conversation markers are unavailable.
|
|
133
138
|
"""
|
|
134
139
|
|
|
140
|
+
# ------------------------------------------------------------------
|
|
141
|
+
# Compaction dedup: turn numbers already covered by stored segments
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
def get_compacted_turn_numbers(self, conversation_id: str) -> set[int]:
|
|
145
|
+
"""Return the set of turn numbers already covered by stored tag summaries.
|
|
146
|
+
|
|
147
|
+
Used by the compaction pipeline to skip segments whose turns have
|
|
148
|
+
already been compacted, preventing redundant LLM calls when the
|
|
149
|
+
compaction watermark drifts ahead of the in-memory history window.
|
|
150
|
+
"""
|
|
151
|
+
tag_summaries = self.get_all_tag_summaries(conversation_id=conversation_id)
|
|
152
|
+
covered: set[int] = set()
|
|
153
|
+
for ts in tag_summaries:
|
|
154
|
+
covered.update(ts.source_turn_numbers)
|
|
155
|
+
return covered
|
|
156
|
+
|
|
135
157
|
# ------------------------------------------------------------------
|
|
136
158
|
# Turn messages (lightweight per-turn text for post-restart recall)
|
|
137
159
|
# ------------------------------------------------------------------
|
|
@@ -352,6 +374,13 @@ class ContextStore(ABC):
|
|
|
352
374
|
def delete_conversation(self, conversation_id: str) -> int:
|
|
353
375
|
return 0
|
|
354
376
|
|
|
377
|
+
def delete_tag_aliases_for_conversation(self, conversation_id: str) -> int:
|
|
378
|
+
"""Delete aliases owned by ``conversation_id``.
|
|
379
|
+
|
|
380
|
+
Backends may keep legacy/global aliases under the empty conversation id.
|
|
381
|
+
"""
|
|
382
|
+
return 0
|
|
383
|
+
|
|
355
384
|
def store_tool_output(
|
|
356
385
|
self,
|
|
357
386
|
ref: str,
|
|
@@ -15,14 +15,37 @@ class TagCanonicalizer:
|
|
|
15
15
|
Normalization (lowercase, hyphenate) is always applied even without aliases.
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
def __init__(self, store=None) -> None:
|
|
18
|
+
def __init__(self, store=None, conversation_id: str = "") -> None:
|
|
19
19
|
self._alias_cache: dict[str, str] = {}
|
|
20
20
|
self._store = store
|
|
21
21
|
self._known_tags: set[str] = set()
|
|
22
|
+
self._conversation_id = conversation_id
|
|
23
|
+
|
|
24
|
+
def _load_store_aliases(self) -> dict[str, str]:
|
|
25
|
+
if not self._store:
|
|
26
|
+
return {}
|
|
27
|
+
getter = getattr(self._store, "get_tag_aliases", None)
|
|
28
|
+
if not callable(getter):
|
|
29
|
+
return {}
|
|
30
|
+
try:
|
|
31
|
+
return getter(conversation_id=self._conversation_id)
|
|
32
|
+
except TypeError:
|
|
33
|
+
return getter()
|
|
34
|
+
|
|
35
|
+
def _store_alias(self, alias: str, canonical: str) -> None:
|
|
36
|
+
if not self._store:
|
|
37
|
+
return
|
|
38
|
+
setter = getattr(self._store, "set_tag_alias", None)
|
|
39
|
+
if not callable(setter):
|
|
40
|
+
return
|
|
41
|
+
try:
|
|
42
|
+
setter(alias, canonical, conversation_id=self._conversation_id)
|
|
43
|
+
except TypeError:
|
|
44
|
+
setter(alias, canonical)
|
|
22
45
|
|
|
23
46
|
def load(self) -> None:
|
|
24
47
|
if self._store:
|
|
25
|
-
self._alias_cache = self.
|
|
48
|
+
self._alias_cache = self._load_store_aliases()
|
|
26
49
|
# Seed known tags from canonical values
|
|
27
50
|
self._known_tags.update(self._alias_cache.values())
|
|
28
51
|
|
|
@@ -65,8 +88,7 @@ class TagCanonicalizer:
|
|
|
65
88
|
def register_alias(self, alias: str, canonical: str) -> None:
|
|
66
89
|
normalized = alias.lower().strip().replace(" ", "-").replace("_", "-")
|
|
67
90
|
self._alias_cache[normalized] = canonical
|
|
68
|
-
|
|
69
|
-
self._store.set_tag_alias(normalized, canonical)
|
|
91
|
+
self._store_alias(normalized, canonical)
|
|
70
92
|
|
|
71
93
|
def get_aliases(self) -> dict[str, str]:
|
|
72
94
|
return dict(self._alias_cache)
|
|
@@ -87,6 +87,32 @@ class ConsolidationResult:
|
|
|
87
87
|
|
|
88
88
|
# ── core logic ──────────────────────────────────────────────────────────
|
|
89
89
|
|
|
90
|
+
def _store_conversation_id(store: ContextStore) -> str:
|
|
91
|
+
conversation_id = getattr(store, "conversation_id", "")
|
|
92
|
+
return conversation_id if isinstance(conversation_id, str) else ""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _get_store_aliases(store: ContextStore) -> dict[str, str]:
|
|
96
|
+
getter = getattr(store, "get_tag_aliases", None)
|
|
97
|
+
if not callable(getter):
|
|
98
|
+
return {}
|
|
99
|
+
conversation_id = _store_conversation_id(store)
|
|
100
|
+
try:
|
|
101
|
+
return getter(conversation_id=conversation_id or None)
|
|
102
|
+
except TypeError:
|
|
103
|
+
return getter()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _set_store_alias(store: ContextStore, alias: str, canonical: str) -> None:
|
|
107
|
+
setter = getattr(store, "set_tag_alias", None)
|
|
108
|
+
if not callable(setter):
|
|
109
|
+
return
|
|
110
|
+
conversation_id = _store_conversation_id(store)
|
|
111
|
+
try:
|
|
112
|
+
setter(alias, canonical, conversation_id=conversation_id)
|
|
113
|
+
except TypeError:
|
|
114
|
+
setter(alias, canonical)
|
|
115
|
+
|
|
90
116
|
def consolidate_tags(
|
|
91
117
|
store: ContextStore,
|
|
92
118
|
llm: LLMProvider,
|
|
@@ -171,11 +197,11 @@ def consolidate_tags(
|
|
|
171
197
|
return result
|
|
172
198
|
|
|
173
199
|
# Write aliases
|
|
174
|
-
existing_aliases = store
|
|
200
|
+
existing_aliases = _get_store_aliases(store)
|
|
175
201
|
for group in all_groups:
|
|
176
202
|
for alias in group.aliases:
|
|
177
203
|
if alias not in existing_aliases:
|
|
178
|
-
store
|
|
204
|
+
_set_store_alias(store, alias, group.canonical)
|
|
179
205
|
result.aliases_written += 1
|
|
180
206
|
|
|
181
207
|
logger.info("Wrote %d new aliases.", result.aliases_written)
|
|
@@ -327,7 +327,10 @@ class TaggingPipeline:
|
|
|
327
327
|
self._check_and_split_broad_tags(conversation_history)
|
|
328
328
|
|
|
329
329
|
# Build snapshot (only count un-compacted messages)
|
|
330
|
-
|
|
330
|
+
_total_turns = len(self._turn_tag_index.entries) if self._turn_tag_index else None
|
|
331
|
+
_offset = self._engine_state.history_offset(
|
|
332
|
+
len(conversation_history), total_turns_indexed=_total_turns,
|
|
333
|
+
)
|
|
331
334
|
snapshot = self._monitor.build_snapshot(
|
|
332
335
|
conversation_history[_offset:],
|
|
333
336
|
payload_tokens=payload_tokens,
|
|
@@ -315,7 +315,10 @@ class VirtualContextEngine:
|
|
|
315
315
|
self._temporal.reference_date = value
|
|
316
316
|
|
|
317
317
|
def _init_canonicalizer(self) -> None:
|
|
318
|
-
self._canonicalizer = TagCanonicalizer(
|
|
318
|
+
self._canonicalizer = TagCanonicalizer(
|
|
319
|
+
store=self._store,
|
|
320
|
+
conversation_id=self.config.conversation_id,
|
|
321
|
+
)
|
|
319
322
|
self._canonicalizer.load()
|
|
320
323
|
|
|
321
324
|
def _init_tag_generator(self) -> None:
|