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.
Files changed (215) hide show
  1. {virtual_context-0.2.2 → virtual_context-0.2.4}/PKG-INFO +1 -1
  2. {virtual_context-0.2.2 → virtual_context-0.2.4}/pyproject.toml +1 -1
  3. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_conversation_lifecycle.py +22 -0
  4. virtual_context-0.2.4/tests/test_registry_lifecycle.py +41 -0
  5. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_store_sqlite.py +14 -0
  6. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_upstream_trim.py +6 -4
  7. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/__init__.py +1 -1
  8. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/compaction_pipeline.py +41 -6
  9. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/composite_store.py +19 -4
  10. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/conversation_store.py +4 -0
  11. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/hint_builder.py +6 -2
  12. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/protocols.py +8 -2
  13. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/retrieval_assembler.py +8 -3
  14. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/retriever.py +4 -1
  15. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/store.py +31 -2
  16. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/tag_canonicalizer.py +26 -4
  17. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/tag_consolidator.py +28 -2
  18. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/tagging_pipeline.py +4 -1
  19. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/engine.py +4 -1
  20. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/dashboard.py +148 -45
  21. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/formats.py +6 -111
  22. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/handlers.py +103 -15
  23. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/helpers.py +3 -1
  24. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/message_filter.py +168 -58
  25. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/registry.py +32 -2
  26. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/server.py +58 -23
  27. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/state.py +99 -14
  28. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/storage/falkordb.py +16 -0
  29. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/storage/filesystem.py +62 -6
  30. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/storage/neo4j.py +18 -0
  31. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/storage/postgres.py +81 -8
  32. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/storage/sqlite.py +73 -8
  33. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/types.py +38 -7
  34. {virtual_context-0.2.2 → virtual_context-0.2.4}/.gitignore +0 -0
  35. {virtual_context-0.2.2 → virtual_context-0.2.4}/LICENSE +0 -0
  36. {virtual_context-0.2.2 → virtual_context-0.2.4}/README.md +0 -0
  37. {virtual_context-0.2.2 → virtual_context-0.2.4}/assets/hero.png +0 -0
  38. {virtual_context-0.2.2 → virtual_context-0.2.4}/models.yaml +0 -0
  39. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/REGRESSION_MAP.md +0 -0
  40. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/conftest.py +0 -0
  41. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/docker-compose.test.yml +0 -0
  42. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/haiku/__init__.py +0 -0
  43. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/haiku/conftest.py +0 -0
  44. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/haiku/test_compaction.py +0 -0
  45. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/haiku/test_retrieval.py +0 -0
  46. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/haiku/test_tagging.py +0 -0
  47. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/ollama/__init__.py +0 -0
  48. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/ollama/conftest.py +0 -0
  49. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/ollama/test_compactor.py +0 -0
  50. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/ollama/test_pipeline.py +0 -0
  51. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/ollama/test_provider.py +0 -0
  52. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/ollama/test_tag_generator.py +0 -0
  53. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/proxy/__init__.py +0 -0
  54. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/proxy/test_dashboard_cors.py +0 -0
  55. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/proxy/test_metrics.py +0 -0
  56. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_assembler.py +0 -0
  57. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_backend_integration.py +0 -0
  58. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_cli_init.py +0 -0
  59. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_compaction_commit_prune.py +0 -0
  60. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_compactor.py +0 -0
  61. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_compactor_concurrent.py +0 -0
  62. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_composite_store.py +0 -0
  63. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_config.py +0 -0
  64. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_context_bleed.py +0 -0
  65. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_conversation_identity.py +0 -0
  66. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_conversation_scoping.py +0 -0
  67. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_embedding_tag_generator.py +0 -0
  68. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_empty_turn_skip.py +0 -0
  69. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_engine_integration.py +0 -0
  70. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_engine_lookback.py +0 -0
  71. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_engine_state.py +0 -0
  72. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_fact_enrichment.py +0 -0
  73. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_fact_graph_integration.py +0 -0
  74. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_fact_link_checker.py +0 -0
  75. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_fact_link_query.py +0 -0
  76. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_fact_link_types.py +0 -0
  77. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_fact_links_sqlite.py +0 -0
  78. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_find_quote.py +0 -0
  79. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_headless.py +0 -0
  80. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_history_filter.py +0 -0
  81. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_idf_retrieval.py +0 -0
  82. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_ingest_index_integrity.py +0 -0
  83. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_longmemeval_auth.py +0 -0
  84. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_mcp_server.py +0 -0
  85. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_message_filter.py +0 -0
  86. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_metrics_persistence.py +0 -0
  87. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_model_catalog.py +0 -0
  88. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_model_limits.py +0 -0
  89. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_monitor.py +0 -0
  90. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_multi_instance.py +0 -0
  91. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_noop_fact_link_store.py +0 -0
  92. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_openrouter_provider.py +0 -0
  93. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_paging.py +0 -0
  94. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_passthrough_filter.py +0 -0
  95. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_presets.py +0 -0
  96. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_prev_context_leak.py +0 -0
  97. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_provider_adapters.py +0 -0
  98. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_proxy.py +0 -0
  99. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_proxy_dashboard.py +0 -0
  100. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_proxy_formats.py +0 -0
  101. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_proxy_message_filter.py +0 -0
  102. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_proxy_session.py +0 -0
  103. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_proxy_streaming.py +0 -0
  104. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_raw_content.py +0 -0
  105. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_recall_all.py +0 -0
  106. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_request_captures_persistence.py +0 -0
  107. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_retriever.py +0 -0
  108. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_rrf_scoring.py +0 -0
  109. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_segmenter.py +0 -0
  110. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_semantic_search.py +0 -0
  111. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_sender_identity.py +0 -0
  112. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_session_cache.py +0 -0
  113. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_session_date.py +0 -0
  114. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_storage_protocols.py +0 -0
  115. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_stub_turn_handling.py +0 -0
  116. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_supersession.py +0 -0
  117. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_supersession_migration.py +0 -0
  118. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tag_canonicalizer.py +0 -0
  119. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tag_consolidator.py +0 -0
  120. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tag_generator.py +0 -0
  121. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tag_splitter.py +0 -0
  122. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_telemetry.py +0 -0
  123. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_telemetry_integration.py +0 -0
  124. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tool_loop.py +0 -0
  125. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tool_output_interceptor.py +0 -0
  126. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tool_result_filter.py +0 -0
  127. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tool_tags.py +0 -0
  128. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_tui.py +0 -0
  129. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_turn_tag_index.py +0 -0
  130. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_unified_budget.py +0 -0
  131. {virtual_context-0.2.2 → virtual_context-0.2.4}/tests/test_verb_expansion.py +0 -0
  132. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual-context.yaml.example +0 -0
  133. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/cli/__init__.py +0 -0
  134. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/cli/main.py +0 -0
  135. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/config.py +0 -0
  136. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/conversation_identity.py +0 -0
  137. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/__init__.py +0 -0
  138. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/assembler.py +0 -0
  139. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/compactor.py +0 -0
  140. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/embedding_provider.py +0 -0
  141. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/embedding_tag_generator.py +0 -0
  142. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/engine_utils.py +0 -0
  143. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/fact_query.py +0 -0
  144. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/fts_preprocessor.py +0 -0
  145. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/llm_utils.py +0 -0
  146. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/math_utils.py +0 -0
  147. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/model_catalog.py +0 -0
  148. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/monitor.py +0 -0
  149. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/paging_manager.py +0 -0
  150. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/provider_adapters.py +0 -0
  151. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/quote_search.py +0 -0
  152. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/retrieval_scoring.py +0 -0
  153. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/search_engine.py +0 -0
  154. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/segmenter.py +0 -0
  155. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/semantic_search.py +0 -0
  156. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/tag_generator.py +0 -0
  157. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/tag_scoring.py +0 -0
  158. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/tag_splitter.py +0 -0
  159. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/telemetry.py +0 -0
  160. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/temporal_resolver.py +0 -0
  161. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/tool_loop.py +0 -0
  162. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/tool_query.py +0 -0
  163. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/core/turn_tag_index.py +0 -0
  164. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/data/anthropic-tokenizer/tokenizer.json +0 -0
  165. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/ingest/__init__.py +0 -0
  166. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/ingest/curator.py +0 -0
  167. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/ingest/date_resolver.py +0 -0
  168. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/ingest/parsers.py +0 -0
  169. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/ingest/supersession.py +0 -0
  170. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/mcp/__init__.py +0 -0
  171. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/mcp/server.py +0 -0
  172. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/model_limits.py +0 -0
  173. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/openclaw/virtual-context.mjs +0 -0
  174. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/patterns.py +0 -0
  175. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/presets/__init__.py +0 -0
  176. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/presets/agentic.py +0 -0
  177. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/presets/base.py +0 -0
  178. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/presets/coding.py +0 -0
  179. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/providers/__init__.py +0 -0
  180. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/providers/anthropic.py +0 -0
  181. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/providers/base.py +0 -0
  182. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/providers/generic_openai.py +0 -0
  183. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/providers/ollama_native.py +0 -0
  184. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/__init__.py +0 -0
  185. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/_envelope.py +0 -0
  186. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/dashboard.html +0 -0
  187. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/metrics.py +0 -0
  188. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/multi.py +0 -0
  189. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/session_cache.py +0 -0
  190. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/static/android-chrome-192x192.png +0 -0
  191. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/static/android-chrome-512x512.png +0 -0
  192. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/static/apple-touch-icon.png +0 -0
  193. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/static/favicon-16x16.png +0 -0
  194. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/static/favicon-32x32.png +0 -0
  195. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/static/favicon.ico +0 -0
  196. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/static/site.webmanifest +0 -0
  197. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/proxy/tool_output_interceptor.py +0 -0
  198. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/storage/__init__.py +0 -0
  199. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/storage/helpers.py +0 -0
  200. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/storage/noop_fact_link_store.py +0 -0
  201. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/token_counter.py +0 -0
  202. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/__init__.py +0 -0
  203. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/app.py +0 -0
  204. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/chat.tcss +0 -0
  205. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/chat_provider.py +0 -0
  206. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/headless.py +0 -0
  207. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/modals/__init__.py +0 -0
  208. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/modals/turn_inspector.py +0 -0
  209. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/state.py +0 -0
  210. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/widgets/__init__.py +0 -0
  211. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/widgets/budget_bar.py +0 -0
  212. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/widgets/chat_view.py +0 -0
  213. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/widgets/input_box.py +0 -0
  214. {virtual_context-0.2.2 → virtual_context-0.2.4}/virtual_context/tui/widgets/tag_panel.py +0 -0
  215. {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.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "virtual-context"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "OS-style virtual memory for LLM session context management"
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-or-later"
@@ -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 test_accounts_for_max_tokens(self):
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
- if total_before > 10_000:
71
- assert removed > 0
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, 2500, fmt)
121
+ trimmed, removed = trim_to_upstream_limit(body, 1500, fmt)
120
122
 
121
123
  assert removed > 0
122
124
  rendered = str(trimmed["input"])
@@ -14,7 +14,7 @@ from .types import (
14
14
  VirtualContextConfig,
15
15
  )
16
16
 
17
- __version__ = "0.2.1"
17
+ __version__ = "0.2.4"
18
18
 
19
19
  __all__ = [
20
20
  "VirtualContextEngine",
@@ -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
- offset = self._engine_state.history_offset(len(conversation_history))
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
- offset = self._engine_state.history_offset(len(conversation_history))
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
- logger.info("Pass 1 complete: %d stubs stored, %d segments ready for compaction (%d merges)",
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(self, alias: str, canonical: str) -> None:
96
- return self._segments.set_tag_alias(alias, canonical)
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.\n"
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
- "Never answer without searching first.\n"
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(self, alias: str, canonical: str) -> None: ...
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
- _offset = self._engine_state.history_offset(len(conversation_history))
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
- uncompacted = _hist[self._engine_state.history_offset(len(_hist)):]
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
- offset = self._engine_state.history_offset(total)
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
- aliases = store.get_tag_aliases()
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(self, alias: str, canonical: str) -> None: ...
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._store.get_tag_aliases()
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
- if self._store:
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.get_tag_aliases()
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.set_tag_alias(alias, group.canonical)
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
- _offset = self._engine_state.history_offset(len(conversation_history))
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(store=self._store)
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: