virtual-context 0.3.4__tar.gz → 0.3.6__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 (240) hide show
  1. virtual_context-0.3.6/.github/workflows/python-publish.yml +29 -0
  2. {virtual_context-0.3.4 → virtual_context-0.3.6}/.gitignore +3 -0
  3. {virtual_context-0.3.4 → virtual_context-0.3.6}/PKG-INFO +1 -1
  4. {virtual_context-0.3.4 → virtual_context-0.3.6}/pyproject.toml +1 -1
  5. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_assembler.py +31 -1
  6. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_engine_state.py +3 -0
  7. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_fact_redesign.py +62 -4
  8. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_proxy.py +41 -0
  9. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_proxy_formats.py +153 -0
  10. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_request_captures_persistence.py +74 -0
  11. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_retriever.py +7 -0
  12. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tag_generator.py +59 -0
  13. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_vcattach.py +90 -0
  14. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/__init__.py +1 -1
  15. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/assembler.py +7 -0
  16. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/compaction_pipeline.py +12 -0
  17. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/compactor.py +189 -15
  18. virtual_context-0.3.6/virtual_context/core/llm_utils.py +145 -0
  19. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/retriever.py +2 -0
  20. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/tag_generator.py +119 -8
  21. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/tagging_pipeline.py +1 -0
  22. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/engine.py +4 -0
  23. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/formats.py +234 -149
  24. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/handlers.py +8 -37
  25. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/metrics.py +60 -18
  26. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/server.py +34 -2
  27. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/session_state.py +2 -0
  28. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/state.py +1 -0
  29. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/storage/filesystem.py +19 -7
  30. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/storage/postgres.py +44 -11
  31. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/storage/sqlite.py +51 -15
  32. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/types.py +5 -1
  33. virtual_context-0.3.4/virtual_context/core/llm_utils.py +0 -65
  34. {virtual_context-0.3.4 → virtual_context-0.3.6}/CLAUDE.md +0 -0
  35. {virtual_context-0.3.4 → virtual_context-0.3.6}/LICENSE +0 -0
  36. {virtual_context-0.3.4 → virtual_context-0.3.6}/README-draft.md +0 -0
  37. {virtual_context-0.3.4 → virtual_context-0.3.6}/README.md +0 -0
  38. {virtual_context-0.3.4 → virtual_context-0.3.6}/README.md.backup +0 -0
  39. {virtual_context-0.3.4 → virtual_context-0.3.6}/assets/dashboard.png +0 -0
  40. {virtual_context-0.3.4 → virtual_context-0.3.6}/assets/hero.png +0 -0
  41. {virtual_context-0.3.4 → virtual_context-0.3.6}/models.yaml +0 -0
  42. {virtual_context-0.3.4 → virtual_context-0.3.6}/scripts/install.ps1 +0 -0
  43. {virtual_context-0.3.4 → virtual_context-0.3.6}/scripts/install.sh +0 -0
  44. {virtual_context-0.3.4 → virtual_context-0.3.6}/specs/cache-aware-compaction.md +0 -0
  45. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/REGRESSION_MAP.md +0 -0
  46. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/conftest.py +0 -0
  47. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/docker-compose.test.yml +0 -0
  48. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/haiku/__init__.py +0 -0
  49. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/haiku/conftest.py +0 -0
  50. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/haiku/test_compaction.py +0 -0
  51. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/haiku/test_retrieval.py +0 -0
  52. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/haiku/test_tagging.py +0 -0
  53. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/ollama/__init__.py +0 -0
  54. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/ollama/conftest.py +0 -0
  55. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/ollama/test_compactor.py +0 -0
  56. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/ollama/test_pipeline.py +0 -0
  57. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/ollama/test_provider.py +0 -0
  58. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/ollama/test_tag_generator.py +0 -0
  59. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/proxy/__init__.py +0 -0
  60. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/proxy/test_dashboard_cors.py +0 -0
  61. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/proxy/test_metrics.py +0 -0
  62. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_backend_integration.py +0 -0
  63. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_budget_enforcement.py +0 -0
  64. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_cli_init.py +0 -0
  65. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_compaction_commit_prune.py +0 -0
  66. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_compactor.py +0 -0
  67. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_compactor_concurrent.py +0 -0
  68. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_composite_store.py +0 -0
  69. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_config.py +0 -0
  70. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_context_bleed.py +0 -0
  71. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_conversation_identity.py +0 -0
  72. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_conversation_lifecycle.py +0 -0
  73. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_conversation_scoping.py +0 -0
  74. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_deferred_payload_compaction.py +0 -0
  75. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_embedding_tag_generator.py +0 -0
  76. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_empty_turn_skip.py +0 -0
  77. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_engine_integration.py +0 -0
  78. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_engine_lookback.py +0 -0
  79. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_engine_sync_turns.py +0 -0
  80. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_fact_enrichment.py +0 -0
  81. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_fact_graph_integration.py +0 -0
  82. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_fact_link_checker.py +0 -0
  83. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_fact_link_query.py +0 -0
  84. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_fact_link_types.py +0 -0
  85. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_fact_links_sqlite.py +0 -0
  86. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_fill_pass.py +0 -0
  87. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_find_quote.py +0 -0
  88. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_format_agnostic.py +0 -0
  89. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_headless.py +0 -0
  90. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_history_filter.py +0 -0
  91. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_idf_retrieval.py +0 -0
  92. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_ingest_index_integrity.py +0 -0
  93. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_longmemeval_auth.py +0 -0
  94. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_mcp_server.py +0 -0
  95. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_media.py +0 -0
  96. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_message_filter.py +0 -0
  97. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_metrics_persistence.py +0 -0
  98. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_model_catalog.py +0 -0
  99. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_model_limits.py +0 -0
  100. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_monitor.py +0 -0
  101. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_multi_instance.py +0 -0
  102. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_noop_fact_link_store.py +0 -0
  103. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_openrouter_provider.py +0 -0
  104. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_paging.py +0 -0
  105. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_passthrough_filter.py +0 -0
  106. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_presets.py +0 -0
  107. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_prev_context_leak.py +0 -0
  108. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_provider_adapters.py +0 -0
  109. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_proxy_dashboard.py +0 -0
  110. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_proxy_message_filter.py +0 -0
  111. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_proxy_session.py +0 -0
  112. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_proxy_streaming.py +0 -0
  113. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_raw_content.py +0 -0
  114. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_recall_all.py +0 -0
  115. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_registry_lifecycle.py +0 -0
  116. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_rrf_scoring.py +0 -0
  117. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_segmenter.py +0 -0
  118. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_semantic_search.py +0 -0
  119. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_sender_identity.py +0 -0
  120. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_session_cache.py +0 -0
  121. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_session_date.py +0 -0
  122. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_session_state.py +0 -0
  123. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_storage_protocols.py +0 -0
  124. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_store_recovery.py +0 -0
  125. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_store_sqlite.py +0 -0
  126. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_stub_turn_handling.py +0 -0
  127. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_supersession.py +0 -0
  128. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_supersession_migration.py +0 -0
  129. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tag_canonicalizer.py +0 -0
  130. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tag_consolidator.py +0 -0
  131. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tag_splitter.py +0 -0
  132. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_telemetry.py +0 -0
  133. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_telemetry_integration.py +0 -0
  134. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tool_loop.py +0 -0
  135. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tool_output_interceptor.py +0 -0
  136. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tool_result_filter.py +0 -0
  137. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tool_tags.py +0 -0
  138. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_tui.py +0 -0
  139. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_turn_grouping.py +0 -0
  140. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_turn_tag_index.py +0 -0
  141. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_unified_budget.py +0 -0
  142. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_upstream_trim.py +0 -0
  143. {virtual_context-0.3.4 → virtual_context-0.3.6}/tests/test_verb_expansion.py +0 -0
  144. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual-context.yaml +0 -0
  145. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual-context.yaml.example +0 -0
  146. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/cli/__init__.py +0 -0
  147. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/cli/main.py +0 -0
  148. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/config.py +0 -0
  149. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/conversation_identity.py +0 -0
  150. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/__init__.py +0 -0
  151. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/composite_store.py +0 -0
  152. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/conversation_store.py +0 -0
  153. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/embedding_provider.py +0 -0
  154. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/embedding_tag_generator.py +0 -0
  155. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/engine_utils.py +0 -0
  156. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/fact_query.py +0 -0
  157. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/fts_preprocessor.py +0 -0
  158. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/hint_builder.py +0 -0
  159. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/math_utils.py +0 -0
  160. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/model_catalog.py +0 -0
  161. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/monitor.py +0 -0
  162. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/paging_manager.py +0 -0
  163. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/protocols.py +0 -0
  164. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/provider_adapters.py +0 -0
  165. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/quote_search.py +0 -0
  166. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/retrieval_assembler.py +0 -0
  167. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/retrieval_scoring.py +0 -0
  168. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/search_engine.py +0 -0
  169. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/segmenter.py +0 -0
  170. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/semantic_search.py +0 -0
  171. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/store.py +0 -0
  172. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/tag_canonicalizer.py +0 -0
  173. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/tag_consolidator.py +0 -0
  174. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/tag_scoring.py +0 -0
  175. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/tag_splitter.py +0 -0
  176. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/telemetry.py +0 -0
  177. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/temporal_resolver.py +0 -0
  178. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/tool_loop.py +0 -0
  179. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/tool_query.py +0 -0
  180. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/core/turn_tag_index.py +0 -0
  181. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/data/anthropic-tokenizer/tokenizer.json +0 -0
  182. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/data/models.yaml +0 -0
  183. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/ingest/__init__.py +0 -0
  184. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/ingest/curator.py +0 -0
  185. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/ingest/date_resolver.py +0 -0
  186. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/ingest/parsers.py +0 -0
  187. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/ingest/supersession.py +0 -0
  188. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/mcp/__init__.py +0 -0
  189. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/mcp/server.py +0 -0
  190. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/model_limits.py +0 -0
  191. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/openclaw/virtual-context.mjs +0 -0
  192. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/patterns.py +0 -0
  193. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/presets/__init__.py +0 -0
  194. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/presets/agentic.py +0 -0
  195. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/presets/base.py +0 -0
  196. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/presets/coding.py +0 -0
  197. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/providers/__init__.py +0 -0
  198. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/providers/anthropic.py +0 -0
  199. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/providers/base.py +0 -0
  200. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/providers/generic_openai.py +0 -0
  201. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/providers/ollama_native.py +0 -0
  202. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/__init__.py +0 -0
  203. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/_envelope.py +0 -0
  204. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/dashboard.html +0 -0
  205. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/dashboard.py +0 -0
  206. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/helpers.py +0 -0
  207. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/media.py +0 -0
  208. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/message_filter.py +0 -0
  209. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/multi.py +0 -0
  210. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/registry.py +0 -0
  211. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/session_cache.py +0 -0
  212. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/static/android-chrome-192x192.png +0 -0
  213. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/static/android-chrome-512x512.png +0 -0
  214. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/static/apple-touch-icon.png +0 -0
  215. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/static/favicon-16x16.png +0 -0
  216. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/static/favicon-32x32.png +0 -0
  217. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/static/favicon.ico +0 -0
  218. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/static/site.webmanifest +0 -0
  219. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/tool_output_interceptor.py +0 -0
  220. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/proxy/vcattach.py +0 -0
  221. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/storage/__init__.py +0 -0
  222. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/storage/falkordb.py +0 -0
  223. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/storage/helpers.py +0 -0
  224. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/storage/neo4j.py +0 -0
  225. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/storage/noop_fact_link_store.py +0 -0
  226. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/token_counter.py +0 -0
  227. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/__init__.py +0 -0
  228. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/app.py +0 -0
  229. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/chat.tcss +0 -0
  230. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/chat_provider.py +0 -0
  231. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/headless.py +0 -0
  232. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/modals/__init__.py +0 -0
  233. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/modals/turn_inspector.py +0 -0
  234. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/state.py +0 -0
  235. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/widgets/__init__.py +0 -0
  236. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/widgets/budget_bar.py +0 -0
  237. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/widgets/chat_view.py +0 -0
  238. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/widgets/input_box.py +0 -0
  239. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/widgets/tag_panel.py +0 -0
  240. {virtual_context-0.3.4 → virtual_context-0.3.6}/virtual_context/tui/widgets/turn_list.py +0 -0
@@ -0,0 +1,29 @@
1
+ name: Upload Python Package
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - name: Build
22
+ run: |
23
+ python -m pip install build
24
+ python -m build
25
+
26
+ - name: Publish to PyPI
27
+ uses: pypa/gh-action-pypi-publish@release/v1
28
+ with:
29
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -8,6 +8,9 @@ build/
8
8
  # Virtual environment
9
9
  .venv/
10
10
 
11
+ # Infrastructure configs (contains secrets — never commit)
12
+ infrastructure/
13
+
11
14
  # Testing
12
15
  .pytest_cache/
13
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: virtual-context
3
- Version: 0.3.4
3
+ Version: 0.3.6
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.3.4"
7
+ version = "0.3.6"
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"
@@ -4,7 +4,7 @@ from datetime import datetime, timezone
4
4
 
5
5
  import pytest
6
6
 
7
- from virtual_context.core.assembler import ContextAssembler
7
+ from virtual_context.core.assembler import ContextAssembler, format_tag_section
8
8
  from virtual_context.types import (
9
9
  AssemblerConfig,
10
10
  Message,
@@ -77,6 +77,36 @@ def test_assemble_xml_tags(assembler, retrieval_result):
77
77
  assert "</virtual-context>" in section
78
78
 
79
79
 
80
+ def test_format_tag_section_renders_code_refs():
81
+ now = datetime.now(timezone.utc)
82
+ section = format_tag_section(
83
+ "backend",
84
+ [
85
+ StoredSummary(
86
+ ref="ref-1",
87
+ primary_tag="backend",
88
+ tags=["backend"],
89
+ summary="Request cache boundary moved ahead of the mutable reminder.",
90
+ summary_tokens=18,
91
+ full_tokens=50,
92
+ metadata=SegmentMetadata(
93
+ code_refs=[
94
+ {"file": "virtual_context/proxy/formats.py", "line": 1312, "symbol": "inject_context"},
95
+ {"file": "virtual_context/core/provider_adapters.py", "symbol": "AnthropicAdapter"},
96
+ ],
97
+ ),
98
+ created_at=now,
99
+ start_timestamp=now,
100
+ end_timestamp=now,
101
+ ),
102
+ ],
103
+ )
104
+
105
+ assert "[refs:" in section
106
+ assert "virtual_context/proxy/formats.py:1312 inject_context" in section
107
+ assert "virtual_context/core/provider_adapters.py AnthropicAdapter" in section
108
+
109
+
80
110
  def test_trim_conversation(assembler):
81
111
  messages = [
82
112
  Message(role="user", content="x" * 400),
@@ -28,6 +28,7 @@ def _make_snapshot(
28
28
  tags=[f"tag-{i}", "common"],
29
29
  primary_tag=f"tag-{i}",
30
30
  timestamp=datetime(2026, 1, 15, 10, i, 0, tzinfo=timezone.utc),
31
+ code_refs=[{"file": f"src/module_{i}.py", "line": i + 1, "symbol": f"func_{i}"}],
31
32
  )
32
33
  for i in range(turn_count)
33
34
  ]
@@ -75,6 +76,7 @@ class TestEngineStateSQLite:
75
76
  assert restored.tags == orig.tags
76
77
  assert restored.primary_tag == orig.primary_tag
77
78
  assert restored.timestamp == orig.timestamp
79
+ assert restored.code_refs == orig.code_refs
78
80
 
79
81
  def test_load_empty_store(self, tmp_path):
80
82
  """Loading from an empty store returns None."""
@@ -131,6 +133,7 @@ class TestEngineStateFilesystem:
131
133
  assert restored.message_hash == orig.message_hash
132
134
  assert restored.tags == orig.tags
133
135
  assert restored.primary_tag == orig.primary_tag
136
+ assert restored.code_refs == orig.code_refs
134
137
 
135
138
  def test_load_empty_store(self, tmp_path):
136
139
  """Loading from an empty store returns None."""
@@ -61,17 +61,75 @@ def test_compaction_uses_replace_facts_for_segment():
61
61
 
62
62
  def test_code_mode_prompt_appended():
63
63
  """When code_mode is True, the compactor prompt should include the code mode block."""
64
- from virtual_context.core.compactor import CODE_MODE_FACT_PROMPT
65
- assert "Do NOT extract intermediary" in CODE_MODE_FACT_PROMPT
66
- assert "conclusions, findings, discoveries" in CODE_MODE_FACT_PROMPT
64
+ from virtual_context.core.compactor import (
65
+ CODE_FACT_EXTRACTION_PROMPT,
66
+ CODE_SUMMARY_PROMPT,
67
+ CODE_TAG_SUMMARY_ROLLUP_PROMPT,
68
+ )
69
+ assert "Do NOT extract intermediary" in CODE_FACT_EXTRACTION_PROMPT
70
+ assert "conclusions, findings, discoveries" in CODE_FACT_EXTRACTION_PROMPT
71
+ assert "engineering conversation segment" in CODE_SUMMARY_PROMPT
72
+ assert '"code_refs"' in CODE_SUMMARY_PROMPT
73
+ assert "rolling up engineering context" in CODE_TAG_SUMMARY_ROLLUP_PROMPT
74
+
75
+
76
+ def test_compactor_template_path_appends_code_fact_prompt():
77
+ from datetime import datetime, timezone
78
+ from unittest.mock import MagicMock
79
+
80
+ from virtual_context.core.compactor import CODE_FACT_EXTRACTION_PROMPT, DomainCompactor
81
+ from virtual_context.types import Message, TaggedSegment
82
+
83
+ captured: dict[str, str] = {}
84
+
85
+ llm = MagicMock()
86
+
87
+ def _complete(*, system, user, max_tokens):
88
+ captured["user"] = user
89
+ return ('{"summary":"ok","entities":[],"key_decisions":[],"action_items":[],"date_references":[],"refined_tags":[],"code_refs":[]}', {})
90
+
91
+ llm.complete.side_effect = _complete
92
+
93
+ compactor = DomainCompactor(
94
+ llm_provider=llm,
95
+ config=CompactorConfig(code_mode=True),
96
+ )
97
+
98
+ seg = TaggedSegment(
99
+ primary_tag="backend",
100
+ tags=["backend"],
101
+ messages=[
102
+ Message(role="user", content="We changed the request cache boundary in formats.py."),
103
+ Message(role="assistant", content="It now sits above the mutable VC injection block."),
104
+ ],
105
+ token_count=40,
106
+ start_timestamp=datetime.now(timezone.utc),
107
+ end_timestamp=datetime.now(timezone.utc),
108
+ turn_count=1,
109
+ )
110
+
111
+ compactor._compact_one(seg)
112
+
113
+ assert CODE_FACT_EXTRACTION_PROMPT.strip() in captured["user"]
67
114
 
68
115
 
69
116
  def test_tag_generator_accepts_code_mode():
70
- from virtual_context.core.tag_generator import LLMTagGenerator
117
+ from virtual_context.core.tag_generator import (
118
+ CODE_TAG_GENERATOR_PROMPT_COMPACT,
119
+ CODE_TAG_GENERATOR_PROMPT_DETAILED,
120
+ CODE_TAG_GENERATOR_PROMPT_SMALL_MODEL,
121
+ LLMTagGenerator,
122
+ )
71
123
  from virtual_context.types import TagGeneratorConfig
72
124
  from unittest.mock import MagicMock
73
125
  gen = LLMTagGenerator(llm_provider=MagicMock(), config=TagGeneratorConfig(), code_mode=True)
74
126
  assert gen._code_mode is True
127
+ assert "engineering and code conversations" in CODE_TAG_GENERATOR_PROMPT_DETAILED
128
+ assert "fact_signals" in CODE_TAG_GENERATOR_PROMPT_DETAILED
129
+ assert "engineering and code conversations" in CODE_TAG_GENERATOR_PROMPT_COMPACT
130
+ assert "fact_signals" in CODE_TAG_GENERATOR_PROMPT_COMPACT
131
+ assert "engineering and code conversations" in CODE_TAG_GENERATOR_PROMPT_SMALL_MODEL
132
+ assert "fact_signals" in CODE_TAG_GENERATOR_PROMPT_SMALL_MODEL
75
133
 
76
134
 
77
135
  def test_build_tag_generator_passes_code_mode():
@@ -965,6 +965,47 @@ class TestRequestCapture:
965
965
  m = ProxyMetrics()
966
966
  m.update_request_tags(99, response_tags=["x"]) # should not raise
967
967
 
968
+ def test_same_turn_different_turn_ids_do_not_collide(self):
969
+ m = ProxyMetrics()
970
+ m.capture_request(
971
+ 7,
972
+ {"messages": []},
973
+ "anthropic",
974
+ conversation_id="conv-1",
975
+ turn_id="req-a",
976
+ inbound_tags=["first"],
977
+ )
978
+ m.capture_request(
979
+ 7,
980
+ {"messages": []},
981
+ "anthropic",
982
+ conversation_id="conv-1",
983
+ turn_id="req-b",
984
+ inbound_tags=["second"],
985
+ )
986
+ m.capture_response(
987
+ 7,
988
+ {"content": "a"},
989
+ conversation_id="conv-1",
990
+ turn_id="req-a",
991
+ upstream_input_tokens=100,
992
+ )
993
+ m.capture_response(
994
+ 7,
995
+ {"content": "b"},
996
+ conversation_id="conv-1",
997
+ turn_id="req-b",
998
+ upstream_input_tokens=200,
999
+ )
1000
+
1001
+ req_a = m.get_captured_request(7, conversation_id="conv-1", turn_id="req-a")
1002
+ req_b = m.get_captured_request(7, conversation_id="conv-1", turn_id="req-b")
1003
+
1004
+ assert req_a["inbound_tags"] == ["first"]
1005
+ assert req_a["upstream_input_tokens"] == 100
1006
+ assert req_b["inbound_tags"] == ["second"]
1007
+ assert req_b["upstream_input_tokens"] == 200
1008
+
968
1009
 
969
1010
  # ---------------------------------------------------------------------------
970
1011
  # Request event model field
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import copy
6
6
  import json
7
+ import math
7
8
  from unittest.mock import MagicMock
8
9
 
9
10
  import pytest
@@ -14,6 +15,7 @@ from virtual_context.proxy.formats import (
14
15
  OpenAIFormat,
15
16
  OpenAIResponsesFormat,
16
17
  GeminiFormat,
18
+ _estimate_media_tokens,
17
19
  detect_format,
18
20
  get_format,
19
21
  )
@@ -30,6 +32,10 @@ from virtual_context.proxy.server import (
30
32
  _strip_vc_prompt,
31
33
  )
32
34
 
35
+ _ONE_BY_ONE_PNG_B64 = (
36
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/a4sAAAAASUVORK5CYII="
37
+ )
38
+
33
39
 
34
40
  # ---------------------------------------------------------------------------
35
41
  # Format detection
@@ -236,6 +242,125 @@ class TestAnthropicFormat:
236
242
  assert content[1]["cache_control"] == {"type": "ephemeral"}
237
243
  assert content[2]["text"].startswith("<system-reminder>")
238
244
 
245
+ def test_estimate_message_tokens_counts_calibrated_tool_result_image_and_signature(self):
246
+ msg = {
247
+ "role": "user",
248
+ "content": [
249
+ {
250
+ "type": "tool_result",
251
+ "tool_use_id": "tool-1",
252
+ "content": [
253
+ {"type": "text", "text": "Screenshot captured"},
254
+ {
255
+ "type": "image",
256
+ "source": {
257
+ "type": "base64",
258
+ "media_type": "image/png",
259
+ "data": _ONE_BY_ONE_PNG_B64,
260
+ },
261
+ },
262
+ ],
263
+ },
264
+ {"type": "thinking", "text": "Working", "signature": "sig" * 1000},
265
+ ],
266
+ }
267
+ original = copy.deepcopy(msg)
268
+ estimate = self.fmt.estimate_message_tokens(msg)
269
+
270
+ media_tokens = sum(self.fmt._estimate_media_tokens(m) for m in self.fmt._collect_media_from_value(msg))
271
+ expected = (
272
+ self.fmt._TOOL_CALL_ID_TOKENS
273
+ + math.ceil(self.fmt._count("Screenshot captured") * self.fmt._TOOL_RESULT_TEXT_SCALE)
274
+ + math.ceil(self.fmt._count("Working") * self.fmt._ASSISTANT_TEXT_SCALE)
275
+ + math.ceil(self.fmt._count("sig" * 1000) * self.fmt._THINKING_SIGNATURE_SCALE)
276
+ + media_tokens
277
+ )
278
+
279
+ assert estimate == expected
280
+ assert msg == original
281
+
282
+ def test_estimate_payload_tokens_from_serialized_ignores_nested_tool_result_image(self):
283
+ body = {
284
+ "messages": [
285
+ {
286
+ "role": "user",
287
+ "content": [
288
+ {
289
+ "type": "tool_result",
290
+ "tool_use_id": "tool-1",
291
+ "content": [
292
+ {"type": "text", "text": "Screenshot captured"},
293
+ {
294
+ "type": "image",
295
+ "source": {
296
+ "type": "base64",
297
+ "media_type": "image/png",
298
+ "data": _ONE_BY_ONE_PNG_B64,
299
+ },
300
+ },
301
+ ],
302
+ }
303
+ ],
304
+ }
305
+ ],
306
+ "system": "You are helpful.",
307
+ }
308
+ serialized = json.dumps(body, default=str)
309
+ estimate = self.fmt.estimate_payload_tokens_from_serialized(body, serialized)
310
+
311
+ media_tokens = sum(self.fmt._estimate_media_tokens(m) for m in self.fmt._collect_media(body))
312
+ expected = (
313
+ math.ceil(self.fmt._count("You are helpful.") * self.fmt._SYSTEM_SCALE)
314
+ + self.fmt._TOOL_CALL_ID_TOKENS
315
+ + math.ceil(self.fmt._count("Screenshot captured") * self.fmt._TOOL_RESULT_TEXT_SCALE)
316
+ + media_tokens
317
+ )
318
+ assert estimate == expected
319
+
320
+ def test_estimate_message_tokens_counts_tool_use_semantics_not_wrapper_json(self):
321
+ fmt = AnthropicFormat()
322
+ fmt.set_token_counter(lambda text: len(text) if text else 0)
323
+ msg = {
324
+ "role": "assistant",
325
+ "content": [
326
+ {
327
+ "type": "tool_use",
328
+ "id": "toolu_123",
329
+ "name": "probe_tool",
330
+ "input": {"path": "/tmp/demo.txt", "limit": 5},
331
+ }
332
+ ],
333
+ }
334
+ expected = (
335
+ fmt._TOOL_CALL_ID_TOKENS
336
+ + math.ceil(len("probe_tool") * fmt._TOOL_NAME_SCALE)
337
+ + math.ceil(
338
+ len(json.dumps({"path": "/tmp/demo.txt", "limit": 5}, sort_keys=True))
339
+ * fmt._TOOL_USE_INPUT_SCALE
340
+ )
341
+ )
342
+ assert fmt.estimate_message_tokens(msg) == expected
343
+
344
+ def test_estimate_message_tokens_tool_id_length_does_not_change_estimate(self):
345
+ fmt = AnthropicFormat()
346
+ fmt.set_token_counter(lambda text: len(text) if text else 0)
347
+ short = {
348
+ "role": "assistant",
349
+ "content": [{"type": "tool_use", "id": "t1", "name": "probe_tool", "input": {"path": "/tmp/demo.txt"}}],
350
+ }
351
+ long = {
352
+ "role": "assistant",
353
+ "content": [{"type": "tool_use", "id": "toolu_0123456789abcdef", "name": "probe_tool", "input": {"path": "/tmp/demo.txt"}}],
354
+ }
355
+ assert fmt.estimate_message_tokens(short) == fmt.estimate_message_tokens(long)
356
+
357
+ def test_estimate_tools_tokens_scales_anthropic_tools(self):
358
+ fmt = AnthropicFormat()
359
+ fmt.set_token_counter(lambda text: len(text) if text else 0)
360
+ body = {"tools": [{"name": "probe_tool", "description": "Expand topic", "input_schema": {"type": "object"}}]}
361
+ raw = len(json.dumps(body["tools"]))
362
+ assert fmt.estimate_tools_tokens(body) == math.ceil(raw * fmt._TOOLS_SCALE)
363
+
239
364
  def test_extract_conversation_id(self):
240
365
  body = {"messages": [
241
366
  {"role": "user", "content": "hi"},
@@ -461,6 +586,34 @@ class TestGeminiFormat:
461
586
  body = {"contents": [{"role": "user", "parts": [{"text": "hi"}]}]}
462
587
  assert len(self.fmt.get_messages(body)) == 1
463
588
 
589
+ def test_estimate_message_tokens_recurse_nested_inline_data(self):
590
+ msg = {
591
+ "role": "user",
592
+ "parts": [
593
+ {
594
+ "functionResponse": {
595
+ "name": "chrome",
596
+ "response": {
597
+ "content": [
598
+ {"text": "screenshot"},
599
+ {
600
+ "inline_data": {
601
+ "mime_type": "image/png",
602
+ "data": _ONE_BY_ONE_PNG_B64,
603
+ }
604
+ },
605
+ ]
606
+ },
607
+ }
608
+ }
609
+ ],
610
+ }
611
+ estimate = self.fmt.estimate_message_tokens(msg)
612
+ blanked = copy.deepcopy(msg)
613
+ blanked["parts"][0]["functionResponse"]["response"]["content"][1]["inline_data"]["data"] = ""
614
+ expected = self.fmt._count(json.dumps(blanked, default=str)) + 1
615
+ assert estimate == expected
616
+
464
617
  def test_inject_context(self):
465
618
  body = {"contents": [
466
619
  {"role": "user", "parts": [{"text": "hi"}]},
@@ -175,6 +175,40 @@ class TestRequestCapturesPersistence:
175
175
  assert [c["model"] for c in store.load_request_captures(conversation_id="c1")] == ["model-a"]
176
176
  assert [c["model"] for c in store.load_request_captures(conversation_id="c2")] == ["model-b"]
177
177
 
178
+ def test_same_turns_are_isolated_by_turn_id(self, tmp_path):
179
+ store = SQLiteStore(tmp_path / "test.db")
180
+ for turn_id, model in (("req-a", "model-a"), ("req-b", "model-b")):
181
+ store.save_request_capture({
182
+ "turn": 7,
183
+ "turn_id": turn_id,
184
+ "ts": "2026-03-20T10:00:00+00:00",
185
+ "api_format": "anthropic",
186
+ "model": model,
187
+ "stream": False,
188
+ "message_count": 1,
189
+ "conversation_id": "c1",
190
+ "inbound_tags": [],
191
+ "response_tags": [],
192
+ "passthrough": False,
193
+ "inbound_tokens": 0,
194
+ "outbound_tokens": 0,
195
+ "inbound_bytes": 0,
196
+ "outbound_bytes": 0,
197
+ "context_tokens": 0,
198
+ "overhead_ms": 0,
199
+ "turns_dropped": 0,
200
+ "turns_stubbed": 0,
201
+ "message_preview": "",
202
+ "upstream_input_tokens": 0,
203
+ "upstream_output_tokens": 0,
204
+ "cache_creation_input_tokens": 0,
205
+ "cache_read_input_tokens": 0,
206
+ })
207
+
208
+ loaded = store.load_request_captures(conversation_id="c1")
209
+ assert len(loaded) == 2
210
+ assert sorted(c["turn_id"] for c in loaded) == ["req-a", "req-b"]
211
+
178
212
  def test_prune_turn_messages_keeps_suffix(self, tmp_path):
179
213
  store = SQLiteStore(tmp_path / "test.db")
180
214
  for turn in range(6):
@@ -288,6 +322,46 @@ class TestProxyMetricsStoreIntegration:
288
322
  assert loaded[0]["upstream_input_tokens"] == 100
289
323
  assert loaded[0]["upstream_output_tokens"] == 50
290
324
 
325
+ def test_capture_response_uses_turn_id_when_turn_collides(self, tmp_path):
326
+ from virtual_context.proxy.metrics import ProxyMetrics
327
+
328
+ store = self._make_store(tmp_path)
329
+ m = ProxyMetrics(store=store)
330
+ m.capture_request(
331
+ turn=1,
332
+ body={"model": "test-a", "messages": []},
333
+ api_format="anthropic",
334
+ conversation_id="c1",
335
+ turn_id="req-a",
336
+ )
337
+ m.capture_request(
338
+ turn=1,
339
+ body={"model": "test-b", "messages": []},
340
+ api_format="anthropic",
341
+ conversation_id="c1",
342
+ turn_id="req-b",
343
+ )
344
+ m.capture_response(
345
+ turn=1,
346
+ body={"content": "hello-a"},
347
+ upstream_input_tokens=100,
348
+ conversation_id="c1",
349
+ turn_id="req-a",
350
+ )
351
+ m.capture_response(
352
+ turn=1,
353
+ body={"content": "hello-b"},
354
+ upstream_input_tokens=200,
355
+ conversation_id="c1",
356
+ turn_id="req-b",
357
+ )
358
+
359
+ loaded = sorted(
360
+ store.load_request_captures(conversation_id="c1"),
361
+ key=lambda capture: capture["turn_id"],
362
+ )
363
+ assert [capture["upstream_input_tokens"] for capture in loaded] == [100, 200]
364
+
291
365
  def test_update_tags_persists(self, tmp_path):
292
366
  from virtual_context.proxy.metrics import ProxyMetrics
293
367
  store = self._make_store(tmp_path)
@@ -498,6 +498,7 @@ class TestSummaryFloor:
498
498
  tag="authentication",
499
499
  summary="JWT setup with RS256 signing and 1-hour expiry. Refresh token rotation.",
500
500
  summary_tokens=25,
501
+ code_refs=[{"file": "auth/jwt.py", "line": 88, "symbol": "issue_token"}],
501
502
  source_turn_numbers=[0, 1],
502
503
  created_at=now,
503
504
  updated_at=now,
@@ -531,6 +532,12 @@ class TestSummaryFloor:
531
532
  assert len(result.summaries) == 2
532
533
  assert result.total_tokens > 0
533
534
  assert result.retrieval_metadata.get("summary_floor") is True
535
+ assert any(
536
+ summary.metadata.code_refs == [
537
+ {"file": "auth/jwt.py", "line": 88, "symbol": "issue_token"}
538
+ ]
539
+ for summary in result.summaries
540
+ )
534
541
  store.close()
535
542
 
536
543
  def test_floor_inactive_pre_compaction(self, tmp_sqlite_db):
@@ -3,6 +3,7 @@
3
3
  import pytest
4
4
 
5
5
  from virtual_context.core.tag_generator import (
6
+ CODE_TAG_GENERATOR_PROMPTS,
6
7
  KeywordTagGenerator,
7
8
  LLMTagGenerator,
8
9
  build_tag_generator,
@@ -122,6 +123,64 @@ class TestLLMTagGenerator:
122
123
  assert generator._tag_vocabulary["auth"] == 10
123
124
  assert generator._tag_vocabulary["database"] == 5
124
125
 
126
+ def test_code_refs_parsed_and_normalized(self):
127
+ provider = MockLLMProvider(
128
+ response=(
129
+ '{"tags": ["backend"], "primary": "backend", '
130
+ '"code_refs": ['
131
+ '{"file": " server.py ", "line": "42", "symbol": "handle_request"}, '
132
+ '{"path": "server.py", "line": 42, "name": "handle_request"}, '
133
+ '{"file": "retriever.py", "class": "ContextRetriever"}'
134
+ ']}'
135
+ )
136
+ )
137
+ config = TagGeneratorConfig(type="llm", max_tags=5, min_tags=1)
138
+ generator = LLMTagGenerator(llm_provider=provider, config=config, code_mode=True)
139
+
140
+ result = generator.generate_tags("Patch the request path")
141
+
142
+ assert result.code_refs == [
143
+ {"file": "server.py", "line": 42, "symbol": "handle_request"},
144
+ {"file": "retriever.py", "symbol": "ContextRetriever"},
145
+ ]
146
+
147
+ @pytest.mark.parametrize("prompt_mode", ["detailed", "compact", "small_model"])
148
+ def test_code_mode_uses_dedicated_tagger_prompt_family(self, prompt_mode):
149
+ from virtual_context.core.compactor import CODE_FACT_EXTRACTION_PROMPT
150
+
151
+ provider = MockLLMProvider(
152
+ response='{"tags": ["backend"], "primary": "backend", "fact_signals": [], "code_refs": []}'
153
+ )
154
+ config = TagGeneratorConfig(type="llm", max_tags=5, min_tags=1, prompt_mode=prompt_mode)
155
+ generator = LLMTagGenerator(llm_provider=provider, config=config, code_mode=True)
156
+
157
+ generator.generate_tags("We fixed the cache boundary in server.py.")
158
+
159
+ system = provider.calls[0]["system"]
160
+ assert system == CODE_TAG_GENERATOR_PROMPTS[prompt_mode].format(min_tags=1, max_tags=5)
161
+ assert "fact_signals" in system
162
+ assert "code_refs" in system
163
+ assert CODE_FACT_EXTRACTION_PROMPT.strip() not in system
164
+
165
+ def test_fact_signals_parsed_from_code_mode_response(self):
166
+ provider = MockLLMProvider(
167
+ response=(
168
+ '{"tags": ["backend"], "primary": "backend", '
169
+ '"fact_signals": ['
170
+ '{"subject": "cache boundary", "verb": "changed", "object": "moved above vc reminder injection", '
171
+ '"status": "completed", "fact_type": "world", "what": "Cache boundary changed to sit above the VC reminder injection."}'
172
+ '], "code_refs": []}'
173
+ )
174
+ )
175
+ config = TagGeneratorConfig(type="llm", max_tags=5, min_tags=1)
176
+ generator = LLMTagGenerator(llm_provider=provider, config=config, code_mode=True)
177
+
178
+ result = generator.generate_tags("Patch the cache boundary.")
179
+
180
+ assert len(result.fact_signals) == 1
181
+ assert result.fact_signals[0].subject == "cache boundary"
182
+ assert result.fact_signals[0].verb == "changed"
183
+
125
184
  def test_select_relevant_store_tags_reuses_cached_embeddings(self):
126
185
  provider = MockLLMProvider(response='{"tags": ["database"], "primary": "database"}')
127
186
  cache_loads: list[list[str]] = []