virtual-context 0.3.7__tar.gz → 0.3.8__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 (244) hide show
  1. {virtual_context-0.3.7 → virtual_context-0.3.8}/.github/workflows/python-publish.yml +6 -0
  2. {virtual_context-0.3.7 → virtual_context-0.3.8}/PKG-INFO +1 -1
  3. {virtual_context-0.3.7 → virtual_context-0.3.8}/pyproject.toml +1 -1
  4. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_compaction_commit_prune.py +15 -0
  5. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_config.py +2 -1
  6. virtual_context-0.3.8/tests/test_postgres_store.py +102 -0
  7. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_presets.py +3 -3
  8. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_proxy_session.py +31 -14
  9. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_request_captures_persistence.py +31 -1
  10. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_session_state.py +15 -0
  11. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_temporal_resolver.py +139 -2
  12. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_tool_loop.py +76 -0
  13. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_tool_result_filter.py +39 -1
  14. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_vcattach.py +145 -7
  15. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual-context.yaml +0 -4
  16. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual-context.yaml.example +1 -8
  17. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/__init__.py +1 -1
  18. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/config.py +0 -1
  19. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/compaction_pipeline.py +0 -10
  20. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/composite_store.py +3 -0
  21. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/protocols.py +1 -0
  22. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/temporal_resolver.py +348 -82
  23. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/tool_loop.py +63 -13
  24. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/presets/agentic.py +4 -8
  25. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/presets/coding.py +3 -8
  26. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/formats.py +156 -0
  27. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/handlers.py +106 -47
  28. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/metrics.py +15 -0
  29. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/registry.py +5 -3
  30. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/server.py +13 -0
  31. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/session_state.py +21 -0
  32. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/vcattach.py +24 -13
  33. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/storage/filesystem.py +5 -0
  34. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/storage/postgres.py +31 -8
  35. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/storage/sqlite.py +8 -0
  36. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/types.py +1 -2
  37. {virtual_context-0.3.7 → virtual_context-0.3.8}/.gitignore +0 -0
  38. {virtual_context-0.3.7 → virtual_context-0.3.8}/CLAUDE.md +0 -0
  39. {virtual_context-0.3.7 → virtual_context-0.3.8}/LICENSE +0 -0
  40. {virtual_context-0.3.7 → virtual_context-0.3.8}/README.md +0 -0
  41. {virtual_context-0.3.7 → virtual_context-0.3.8}/assets/dashboard.png +0 -0
  42. {virtual_context-0.3.7 → virtual_context-0.3.8}/assets/hero.png +0 -0
  43. {virtual_context-0.3.7 → virtual_context-0.3.8}/models.yaml +0 -0
  44. {virtual_context-0.3.7 → virtual_context-0.3.8}/scripts/install.ps1 +0 -0
  45. {virtual_context-0.3.7 → virtual_context-0.3.8}/scripts/install.sh +0 -0
  46. {virtual_context-0.3.7 → virtual_context-0.3.8}/specs/cache-aware-compaction.md +0 -0
  47. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/REGRESSION_MAP.md +0 -0
  48. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/conftest.py +0 -0
  49. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/docker-compose.test.yml +0 -0
  50. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/haiku/__init__.py +0 -0
  51. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/haiku/conftest.py +0 -0
  52. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/haiku/test_compaction.py +0 -0
  53. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/haiku/test_retrieval.py +0 -0
  54. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/haiku/test_tagging.py +0 -0
  55. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/ollama/__init__.py +0 -0
  56. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/ollama/conftest.py +0 -0
  57. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/ollama/test_compactor.py +0 -0
  58. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/ollama/test_pipeline.py +0 -0
  59. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/ollama/test_provider.py +0 -0
  60. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/ollama/test_tag_generator.py +0 -0
  61. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/proxy/__init__.py +0 -0
  62. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/proxy/test_dashboard_cors.py +0 -0
  63. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/proxy/test_metrics.py +0 -0
  64. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_assembler.py +0 -0
  65. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_backend_integration.py +0 -0
  66. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_beam_judge.py +0 -0
  67. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_budget_enforcement.py +0 -0
  68. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_cli_init.py +0 -0
  69. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_compactor.py +0 -0
  70. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_compactor_concurrent.py +0 -0
  71. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_composite_store.py +0 -0
  72. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_context_bleed.py +0 -0
  73. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_conversation_coverage_report.py +0 -0
  74. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_conversation_identity.py +0 -0
  75. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_conversation_lifecycle.py +0 -0
  76. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_conversation_scoping.py +0 -0
  77. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_deferred_payload_compaction.py +0 -0
  78. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_embedding_tag_generator.py +0 -0
  79. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_empty_turn_skip.py +0 -0
  80. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_engine_integration.py +0 -0
  81. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_engine_lookback.py +0 -0
  82. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_engine_state.py +0 -0
  83. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_engine_sync_turns.py +0 -0
  84. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_fact_enrichment.py +0 -0
  85. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_fact_graph_integration.py +0 -0
  86. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_fact_link_checker.py +0 -0
  87. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_fact_link_query.py +0 -0
  88. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_fact_link_types.py +0 -0
  89. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_fact_links_sqlite.py +0 -0
  90. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_fact_redesign.py +0 -0
  91. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_fill_pass.py +0 -0
  92. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_find_quote.py +0 -0
  93. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_format_agnostic.py +0 -0
  94. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_full_text_backfill.py +0 -0
  95. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_headless.py +0 -0
  96. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_history_filter.py +0 -0
  97. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_idf_retrieval.py +0 -0
  98. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_ingest_index_integrity.py +0 -0
  99. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_longmemeval_auth.py +0 -0
  100. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_mcp_server.py +0 -0
  101. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_media.py +0 -0
  102. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_message_filter.py +0 -0
  103. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_metrics_persistence.py +0 -0
  104. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_model_catalog.py +0 -0
  105. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_model_limits.py +0 -0
  106. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_monitor.py +0 -0
  107. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_multi_instance.py +0 -0
  108. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_noop_fact_link_store.py +0 -0
  109. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_openrouter_provider.py +0 -0
  110. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_paging.py +0 -0
  111. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_passthrough_filter.py +0 -0
  112. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_prev_context_leak.py +0 -0
  113. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_provider_adapters.py +0 -0
  114. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_proxy.py +0 -0
  115. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_proxy_dashboard.py +0 -0
  116. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_proxy_formats.py +0 -0
  117. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_proxy_message_filter.py +0 -0
  118. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_proxy_streaming.py +0 -0
  119. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_raw_content.py +0 -0
  120. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_recall_all.py +0 -0
  121. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_registry_lifecycle.py +0 -0
  122. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_retriever.py +0 -0
  123. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_rrf_scoring.py +0 -0
  124. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_segmenter.py +0 -0
  125. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_semantic_search.py +0 -0
  126. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_sender_identity.py +0 -0
  127. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_session_cache.py +0 -0
  128. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_session_date.py +0 -0
  129. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_storage_protocols.py +0 -0
  130. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_store_recovery.py +0 -0
  131. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_store_sqlite.py +0 -0
  132. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_stub_turn_handling.py +0 -0
  133. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_supersession.py +0 -0
  134. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_supersession_migration.py +0 -0
  135. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_tag_canonicalizer.py +0 -0
  136. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_tag_consolidator.py +0 -0
  137. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_tag_generator.py +0 -0
  138. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_tag_splitter.py +0 -0
  139. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_telemetry.py +0 -0
  140. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_telemetry_integration.py +0 -0
  141. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_tool_output_interceptor.py +0 -0
  142. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_tool_tags.py +0 -0
  143. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_tui.py +0 -0
  144. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_turn_grouping.py +0 -0
  145. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_turn_tag_index.py +0 -0
  146. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_unified_budget.py +0 -0
  147. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_upstream_trim.py +0 -0
  148. {virtual_context-0.3.7 → virtual_context-0.3.8}/tests/test_verb_expansion.py +0 -0
  149. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/cli/__init__.py +0 -0
  150. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/cli/main.py +0 -0
  151. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/conversation_identity.py +0 -0
  152. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/__init__.py +0 -0
  153. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/assembler.py +0 -0
  154. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/compactor.py +0 -0
  155. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/conversation_store.py +0 -0
  156. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/coverage_report.py +0 -0
  157. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/embedding_provider.py +0 -0
  158. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/embedding_tag_generator.py +0 -0
  159. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/engine_utils.py +0 -0
  160. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/fact_query.py +0 -0
  161. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/fts_preprocessor.py +0 -0
  162. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/full_text_backfill.py +0 -0
  163. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/hint_builder.py +0 -0
  164. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/llm_utils.py +0 -0
  165. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/math_utils.py +0 -0
  166. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/model_catalog.py +0 -0
  167. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/monitor.py +0 -0
  168. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/paging_manager.py +0 -0
  169. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/provider_adapters.py +0 -0
  170. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/quote_search.py +0 -0
  171. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/retrieval_assembler.py +0 -0
  172. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/retrieval_scoring.py +0 -0
  173. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/retriever.py +0 -0
  174. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/search_engine.py +0 -0
  175. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/segmenter.py +0 -0
  176. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/semantic_search.py +0 -0
  177. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/store.py +0 -0
  178. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/tag_canonicalizer.py +0 -0
  179. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/tag_consolidator.py +0 -0
  180. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/tag_generator.py +0 -0
  181. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/tag_scoring.py +0 -0
  182. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/tag_splitter.py +0 -0
  183. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/tagging_pipeline.py +0 -0
  184. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/telemetry.py +0 -0
  185. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/tool_query.py +0 -0
  186. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/core/turn_tag_index.py +0 -0
  187. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/data/anthropic-tokenizer/tokenizer.json +0 -0
  188. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/data/models.yaml +0 -0
  189. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/engine.py +0 -0
  190. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/ingest/__init__.py +0 -0
  191. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/ingest/curator.py +0 -0
  192. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/ingest/date_resolver.py +0 -0
  193. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/ingest/parsers.py +0 -0
  194. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/ingest/supersession.py +0 -0
  195. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/mcp/__init__.py +0 -0
  196. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/mcp/server.py +0 -0
  197. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/model_limits.py +0 -0
  198. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/openclaw/virtual-context.mjs +0 -0
  199. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/patterns.py +0 -0
  200. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/presets/__init__.py +0 -0
  201. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/presets/base.py +0 -0
  202. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/providers/__init__.py +0 -0
  203. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/providers/anthropic.py +0 -0
  204. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/providers/base.py +0 -0
  205. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/providers/generic_openai.py +0 -0
  206. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/providers/ollama_native.py +0 -0
  207. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/__init__.py +0 -0
  208. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/_envelope.py +0 -0
  209. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/dashboard.html +0 -0
  210. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/dashboard.py +0 -0
  211. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/helpers.py +0 -0
  212. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/media.py +0 -0
  213. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/message_filter.py +0 -0
  214. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/multi.py +0 -0
  215. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/session_cache.py +0 -0
  216. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/state.py +0 -0
  217. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/static/android-chrome-192x192.png +0 -0
  218. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/static/android-chrome-512x512.png +0 -0
  219. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/static/apple-touch-icon.png +0 -0
  220. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/static/favicon-16x16.png +0 -0
  221. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/static/favicon-32x32.png +0 -0
  222. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/static/favicon.ico +0 -0
  223. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/static/site.webmanifest +0 -0
  224. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/proxy/tool_output_interceptor.py +0 -0
  225. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/storage/__init__.py +0 -0
  226. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/storage/falkordb.py +0 -0
  227. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/storage/helpers.py +0 -0
  228. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/storage/neo4j.py +0 -0
  229. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/storage/noop_fact_link_store.py +0 -0
  230. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/token_counter.py +0 -0
  231. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/tui/__init__.py +0 -0
  232. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/tui/app.py +0 -0
  233. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/tui/chat.tcss +0 -0
  234. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/tui/chat_provider.py +0 -0
  235. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/tui/headless.py +0 -0
  236. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/tui/modals/__init__.py +0 -0
  237. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/tui/modals/turn_inspector.py +0 -0
  238. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/tui/state.py +0 -0
  239. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/tui/widgets/__init__.py +0 -0
  240. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/tui/widgets/budget_bar.py +0 -0
  241. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/tui/widgets/chat_view.py +0 -0
  242. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/tui/widgets/input_box.py +0 -0
  243. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/tui/widgets/tag_panel.py +0 -0
  244. {virtual_context-0.3.7 → virtual_context-0.3.8}/virtual_context/tui/widgets/turn_list.py +0 -0
@@ -22,6 +22,12 @@ jobs:
22
22
  with:
23
23
  python-version: "3.12"
24
24
 
25
+ - name: Set version from tag
26
+ run: |
27
+ VERSION=${GITHUB_REF_NAME#v}
28
+ sed -i "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
29
+ sed -i "s/^__version__ = .*/__version__ = \"$VERSION\"/" virtual_context/__init__.py
30
+
25
31
  - name: Build
26
32
  run: |
27
33
  python -m pip install build
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: virtual-context
3
- Version: 0.3.7
3
+ Version: 0.3.8
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.7"
7
+ version = "0.3.8"
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"
@@ -70,3 +70,18 @@ def test_compaction_skips_prune_when_committed_checkpoint_lags_expected():
70
70
  pipeline._commit_compaction_state([])
71
71
 
72
72
  store.prune_turn_messages.assert_not_called()
73
+
74
+
75
+ def test_run_compaction_never_triggers_store_cleanup_from_tag_rules():
76
+ pipeline, store = _make_pipeline(save_ok=True, committed_turn=4)
77
+ pipeline._segmenter.segment = MagicMock(return_value=[])
78
+ pipeline._compact_and_store = MagicMock(return_value=[])
79
+ pipeline._refresh_shared_retrieval_snapshots = MagicMock()
80
+ pipeline._config = SimpleNamespace(
81
+ conversation_id="conv-1",
82
+ tag_rules=[SimpleNamespace(match="debug*", priority=7)],
83
+ )
84
+
85
+ pipeline._run_compaction([], [], generated_by_turn_id="turn-123")
86
+
87
+ store.cleanup.assert_not_called()
@@ -52,7 +52,8 @@ class TestLoadConfig:
52
52
  assert len(config.tag_rules) == 2
53
53
  assert config.tag_rules[0].match == "architecture*"
54
54
  assert config.tag_rules[0].priority == 10
55
- assert config.tag_rules[1].ttl_days == 7
55
+ assert config.tag_rules[1].match == "debug*"
56
+ assert config.tag_rules[1].priority == 7
56
57
 
57
58
  def test_load_strategy_config(self):
58
59
  config = load_config(config_dict={
@@ -0,0 +1,102 @@
1
+ """Focused unit tests for PostgresStore connection management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+
7
+
8
+ class _FakeConn:
9
+ def __init__(self, name: str) -> None:
10
+ self.name = name
11
+ self.closed = False
12
+ self.executed: list[tuple[str, tuple | None]] = []
13
+
14
+ def execute(self, sql: str, params=None):
15
+ self.executed.append((sql, params))
16
+ return self
17
+
18
+ def close(self) -> None:
19
+ self.closed = True
20
+
21
+
22
+ class _FakeRowsResult:
23
+ def __init__(self, rows):
24
+ self._rows = rows
25
+
26
+ def fetchall(self):
27
+ return self._rows
28
+
29
+
30
+ def test_postgres_store_uses_thread_local_connections(monkeypatch):
31
+ from virtual_context.storage import postgres as pg
32
+
33
+ created: list[_FakeConn] = []
34
+
35
+ def _fake_connect(*args, **kwargs):
36
+ conn = _FakeConn(f"conn-{len(created)}")
37
+ created.append(conn)
38
+ return conn
39
+
40
+ monkeypatch.setattr(pg.psycopg, "connect", _fake_connect)
41
+
42
+ store = pg.PostgresStore("postgresql://example")
43
+ main_conn = store._get_conn()
44
+
45
+ from_worker: list[_FakeConn] = []
46
+
47
+ def _worker() -> None:
48
+ from_worker.append(store._get_conn())
49
+
50
+ worker = threading.Thread(target=_worker)
51
+ worker.start()
52
+ worker.join()
53
+
54
+ assert store._get_conn() is main_conn
55
+ assert from_worker == [created[1]]
56
+ assert from_worker[0] is not main_conn
57
+ assert len(created) == 2
58
+
59
+ store.close()
60
+
61
+ assert all(conn.closed for conn in created)
62
+
63
+
64
+ def test_postgres_store_get_all_segments_uses_batch_tag_lookup(monkeypatch):
65
+ from virtual_context.storage import postgres as pg
66
+
67
+ class _RowsConn(_FakeConn):
68
+ def execute(self, sql: str, params=None):
69
+ self.executed.append((sql, params))
70
+ if "FROM segments" in sql:
71
+ return _FakeRowsResult([
72
+ {
73
+ "ref": "seg-1",
74
+ "conversation_id": "conv-1",
75
+ "primary_tag": "tag-a",
76
+ "summary": "summary",
77
+ "summary_tokens": 3,
78
+ "full_text": "full text",
79
+ "full_tokens": 6,
80
+ "messages_json": "[]",
81
+ "metadata_json": "{\"turn_count\": 2}",
82
+ "created_at": "2026-04-14T00:00:00+00:00",
83
+ "start_timestamp": "2026-04-14T00:00:00+00:00",
84
+ "end_timestamp": "2026-04-14T00:01:00+00:00",
85
+ "compaction_model": "test-model",
86
+ "compression_ratio": 0.5,
87
+ }
88
+ ])
89
+ return _FakeRowsResult([])
90
+
91
+ conn = _RowsConn("conn-0")
92
+
93
+ monkeypatch.setattr(pg.psycopg, "connect", lambda *args, **kwargs: conn)
94
+
95
+ store = pg.PostgresStore("postgresql://example")
96
+ monkeypatch.setattr(store, "_batch_get_tags", lambda refs: {"seg-1": ["tag-a", "tag-b"]})
97
+
98
+ segments = store.get_all_segments(conversation_id="conv-1")
99
+
100
+ assert len(segments) == 1
101
+ assert segments[0].ref == "seg-1"
102
+ assert segments[0].tags == ["tag-a", "tag-b"]
@@ -74,9 +74,9 @@ def test_architecture_highest_priority():
74
74
  assert arch_rule["priority"] == 10
75
75
 
76
76
 
77
- def test_debugging_short_ttl():
78
- debug_rule = next(r for r in CODING_TAG_RULES if r["match"] == "debugging*")
79
- assert debug_rule["ttl_days"] == 7
77
+ def test_coding_rules_do_not_define_ttl():
78
+ for rule in CODING_TAG_RULES:
79
+ assert "ttl_days" not in rule
80
80
 
81
81
 
82
82
  def test_catch_all_rule_exists():
@@ -640,11 +640,15 @@ class TestContentFingerprintRouting:
640
640
  assert is_new is False
641
641
  assert result is state
642
642
 
643
- def test_marker_prefers_live_conversation_over_stale_alias(self):
644
- """Explicit session ids should not chain through stale VCATTACH aliases."""
643
+ def test_alias_wins_even_when_live_conversation_exists(self):
644
+ """VCATTACH aliases are authoritative — they redirect even when the
645
+ source conversation still has a live session. VCATTACH no longer
646
+ deletes the old conversation, so the alias table is the single
647
+ source of truth for routing.
648
+ """
645
649
  metrics = ProxyMetrics()
646
650
  store = MagicMock()
647
- store.resolve_conversation_alias.return_value = "wrong-session"
651
+ store.resolve_conversation_alias.return_value = "redirected-target"
648
652
  registry = SessionRegistry(
649
653
  config_path=None,
650
654
  upstream="http://fake:9999",
@@ -652,17 +656,23 @@ class TestContentFingerprintRouting:
652
656
  store=store,
653
657
  )
654
658
 
655
- engine = MagicMock()
656
- engine.config.conversation_id = "target-session"
657
- state = ProxyState(engine, metrics=metrics)
658
- registry._conversations["target-session"] = state
659
+ live_engine = MagicMock()
660
+ live_engine.config.conversation_id = "original-session"
661
+ live_state = ProxyState(live_engine, metrics=metrics)
662
+ registry._conversations["original-session"] = live_state
663
+
664
+ target_engine = MagicMock()
665
+ target_engine.config.conversation_id = "redirected-target"
666
+ target_state = ProxyState(target_engine, metrics=metrics)
667
+ registry._conversations["redirected-target"] = target_state
659
668
 
660
669
  body = self._make_body(["hello"])
661
- result, is_new = registry.get_or_create("target-session", body=body)
670
+ result, is_new = registry.get_or_create("original-session", body=body)
662
671
 
672
+ # Alias redirects to the target even though original-session is live.
663
673
  assert is_new is False
664
- assert result is state
665
- store.resolve_conversation_alias.assert_not_called()
674
+ assert result is target_state
675
+ store.resolve_conversation_alias.assert_called_once_with("original-session")
666
676
 
667
677
  def test_cold_start_marker_restores_persisted_session(self, tmp_path):
668
678
  """A marker referencing a persisted-but-not-loaded session should restore it, not create a new random one."""
@@ -699,8 +709,13 @@ class TestContentFingerprintRouting:
699
709
  assert result.engine.config.conversation_id == saved_id
700
710
  assert len(result.engine._turn_tag_index.entries) > 0
701
711
 
702
- def test_cold_start_marker_prefers_persisted_session_over_stale_alias(self, tmp_path):
703
- """Persisted sessions should not redirect through older alias rows."""
712
+ def test_cold_start_alias_redirects_even_when_persisted_source_exists(self, tmp_path):
713
+ """VCATTACH aliases persistently redirect: when a client sends the old
714
+ id after a VCATTACH, the alias routes to the target even though the
715
+ original conversation still has persisted data. VCATTACH no longer
716
+ deletes the old conversation, so the alias is the only signal that
717
+ a redirect is intended.
718
+ """
704
719
  from virtual_context.engine import VirtualContextEngine
705
720
 
706
721
  config_path = tmp_path / "vc.yaml"
@@ -714,7 +729,7 @@ class TestContentFingerprintRouting:
714
729
  engine1.close()
715
730
 
716
731
  from virtual_context.storage.sqlite import SQLiteStore
717
- SQLiteStore(str(tmp_path / "store.db")).save_conversation_alias(saved_id, "wrong-session")
732
+ SQLiteStore(str(tmp_path / "store.db")).save_conversation_alias(saved_id, "target-session")
718
733
 
719
734
  metrics = ProxyMetrics()
720
735
  registry = SessionRegistry(
@@ -727,7 +742,9 @@ class TestContentFingerprintRouting:
727
742
  body = self._make_body(["new message"])
728
743
  result, is_new = registry.get_or_create(saved_id, body=body)
729
744
 
730
- assert result.engine.config.conversation_id == saved_id
745
+ # Alias redirects saved_id -> "target-session", producing a new
746
+ # session for target-session (there's no persisted target yet).
747
+ assert result.engine.config.conversation_id == "target-session"
731
748
  assert is_new is True
732
749
 
733
750
 
@@ -335,6 +335,34 @@ class TestProxyMetricsStoreIntegration:
335
335
  assert loaded[0]["client_payload_earliest_timestamp"] == "2026-03-15T16:10:00+00:00"
336
336
  assert loaded[0]["client_payload_latest_timestamp"] == "2026-03-16T18:12:00+00:00"
337
337
 
338
+ def test_capture_persists_payload_accounting(self, tmp_path):
339
+ from virtual_context.proxy.metrics import ProxyMetrics
340
+
341
+ store = self._make_store(tmp_path)
342
+ m = ProxyMetrics(store=store)
343
+ m.capture_request(
344
+ turn=1,
345
+ body={"model": "test", "messages": [{"role": "user", "content": "hi"}]},
346
+ api_format="anthropic",
347
+ conversation_id="c1",
348
+ payload_accounting={
349
+ "raw_payload_entry_count": 4624,
350
+ "normalized_payload_entry_count": 986,
351
+ "normalized_chat_entry_count": 986,
352
+ "normalized_non_chat_entry_count": 3638,
353
+ "extracted_history_message_count": 986,
354
+ "extracted_history_pair_count": 493,
355
+ "tool_result_user_entries_folded": 1770,
356
+ },
357
+ )
358
+
359
+ loaded = store.load_request_captures(conversation_id="c1")
360
+ assert len(loaded) == 1
361
+ assert loaded[0]["raw_payload_entry_count"] == 4624
362
+ assert loaded[0]["normalized_payload_entry_count"] == 986
363
+ assert loaded[0]["extracted_history_pair_count"] == 493
364
+ assert loaded[0]["payload_accounting"]["tool_result_user_entries_folded"] == 1770
365
+
338
366
  def test_capture_enriched_persists_to_store(self, tmp_path):
339
367
  from virtual_context.proxy.metrics import ProxyMetrics
340
368
  from unittest.mock import patch
@@ -347,7 +375,9 @@ class TestProxyMetricsStoreIntegration:
347
375
  # Verify _persist_capture is called by capture_enriched
348
376
  with patch.object(m, '_persist_capture', wraps=m._persist_capture) as mock_persist:
349
377
  m.capture_enriched(turn=1, body={"model": "test", "messages": [], "system": "ctx"})
350
- mock_persist.assert_called_once_with(1)
378
+ mock_persist.assert_called_once()
379
+ assert mock_persist.call_args.args == (1,)
380
+ assert mock_persist.call_args.kwargs == {"turn_id": None}
351
381
  # The store should still have the capture (enriched body excluded from summary)
352
382
  loaded = store.load_request_captures()
353
383
  assert len(loaded) == 1
@@ -136,6 +136,21 @@ def test_save_rejected_after_tombstone(provider, mock_redis):
136
136
  assert blob["deleted"] is True
137
137
 
138
138
 
139
+ def test_undelete_allows_reuse_of_same_conversation_id(provider, mock_redis):
140
+ provider.delete("conv-123")
141
+
142
+ provider.undelete("conv-123")
143
+
144
+ state = SessionState()
145
+ state.last_indexed_turn = 7
146
+ provider.save("conv-123", state)
147
+
148
+ loaded = provider.load("conv-123")
149
+ assert loaded is not None
150
+ assert loaded.deleted is False
151
+ assert loaded.last_indexed_turn == 7
152
+
153
+
139
154
  def test_turn_tag_entries_roundtrip(provider, mock_redis):
140
155
  state = SessionState()
141
156
  state.turn_tag_entries = [
@@ -369,7 +369,7 @@ def test_change_over_time_supplements_fact_backed_empty_dates() -> None:
369
369
  assert supplemented[0]["match_type"] == "summary_date_expand"
370
370
 
371
371
 
372
- def test_remember_when_uses_query_matched_facts_before_date_fallback():
372
+ def test_remember_when_uses_query_matched_facts_after_date_window_scan():
373
373
  store = FakeStore()
374
374
  search = FakeSearch()
375
375
  resolver = TemporalResolver(store=store, search_engine=search, config=_make_config())
@@ -394,11 +394,88 @@ def test_remember_when_uses_query_matched_facts_before_date_fallback():
394
394
  )
395
395
 
396
396
  fact_dates = [item["when"] for item in result["facts_in_window"]]
397
- assert store.fallback_called is False
397
+ assert store.fallback_called is True
398
398
  assert "2024-07-29" in fact_dates
399
399
  assert any("microservices" in item["what"].lower() for item in result["facts_in_window"])
400
400
 
401
401
 
402
+ def test_remember_when_lookup_scans_window_even_for_weak_query() -> None:
403
+ store = FakeStore()
404
+ search = FakeSearch()
405
+ resolver = TemporalResolver(store=store, search_engine=search, config=_make_config())
406
+
407
+ store.segments["seg-day-1"] = _make_segment(
408
+ "seg-day-1",
409
+ "nginx-proxy",
410
+ "April-13-2026",
411
+ "Configured nginx reverse proxy for the internal dashboard on port 5757.",
412
+ )
413
+ store.segments["seg-day-2"] = _make_segment(
414
+ "seg-day-2",
415
+ "dashboard-auth",
416
+ "April-13-2026",
417
+ "Worked on dashboard authentication flow and access restrictions.",
418
+ )
419
+ store.segments["seg-other"] = _make_segment(
420
+ "seg-other",
421
+ "other-day",
422
+ "April-12-2026",
423
+ "Worked on an unrelated caching task.",
424
+ )
425
+
426
+ result = resolver.remember_when(
427
+ query="what did we work on on april 13th",
428
+ time_range={"kind": "between_dates", "start": "2026-04-13", "end": "2026-04-13"},
429
+ max_results=4,
430
+ mode="lookup",
431
+ )
432
+
433
+ assert result["found"] is True
434
+ assert {item["segment_ref"] for item in result["results"]} == {"seg-day-1", "seg-day-2"}
435
+ assert all(item["session_date_normalized"] == "2026-04-13" for item in result["results"])
436
+
437
+
438
+ def test_remember_when_window_overview_allows_blank_query() -> None:
439
+ store = FakeStore()
440
+ search = FakeSearch()
441
+ resolver = TemporalResolver(store=store, search_engine=search, config=_make_config())
442
+
443
+ store.segments["seg-day-1"] = _make_segment(
444
+ "seg-day-1",
445
+ "nginx-proxy",
446
+ "April-13-2026",
447
+ "Configured nginx reverse proxy for the internal dashboard on port 5757.",
448
+ )
449
+ store.segments["seg-day-2"] = _make_segment(
450
+ "seg-day-2",
451
+ "dashboard-auth",
452
+ "April-13-2026",
453
+ "Worked on dashboard authentication flow and access restrictions.",
454
+ )
455
+ store.fallback_facts = [
456
+ _make_fact(
457
+ "fact-day-1",
458
+ "2026-04-13",
459
+ "Set up nginx proxy routing for the dashboard service",
460
+ tags=["nginx-proxy", "dashboard"],
461
+ segment_ref="seg-day-1",
462
+ ),
463
+ ]
464
+
465
+ result = resolver.remember_when(
466
+ query="",
467
+ time_range={"kind": "between_dates", "start": "2026-04-13", "end": "2026-04-13"},
468
+ max_results=4,
469
+ mode="window_overview",
470
+ )
471
+
472
+ assert result["mode"] == "window_overview"
473
+ assert result["found"] is True
474
+ assert {item["segment_ref"] for item in result["results"]} == {"seg-day-1", "seg-day-2"}
475
+ assert any(item["what"].startswith("Set up nginx proxy routing") for item in result["facts_in_window"])
476
+ assert result["date_buckets"][0]["date"] == "2026-04-13"
477
+
478
+
402
479
  def test_remember_when_change_over_time_mode_merges_broad_window_facts():
403
480
  store = FakeStore()
404
481
  search = FakeSearch()
@@ -683,6 +760,66 @@ def test_remember_when_summarize_over_time_emits_ordered_milestones():
683
760
  assert "deploy stages" in milestones[3]["point"].lower()
684
761
 
685
762
 
763
+ def test_remember_when_change_over_time_also_emits_ordered_milestones():
764
+ store = FakeStore()
765
+ search = FakeSearch()
766
+ resolver = TemporalResolver(store=store, search_engine=search, config=_make_config())
767
+
768
+ store.segment_hits["architecture"] = [
769
+ _make_quote("seg-1", "architecture", "July-03-2024", "architecture anchor"),
770
+ ]
771
+ store.segments["seg-1"] = _make_segment("seg-1", "architecture", "July-03-2024", "July 3 summary")
772
+
773
+ store.fact_hits["architecture"] = [
774
+ _make_fact(
775
+ "fact-anchor",
776
+ "2024-07-03",
777
+ "Architecture updated to support higher throughput and availability",
778
+ tags=["architecture", "performance-optimization"],
779
+ segment_ref="seg-1",
780
+ ),
781
+ ]
782
+ store.fallback_facts = [
783
+ _make_fact(
784
+ "fact-health",
785
+ "2024-07-09",
786
+ "Weighted round robin load balancing with health checks for unhealthy servers",
787
+ tags=["load-balancing", "high-availability"],
788
+ ),
789
+ _make_fact(
790
+ "fact-redis",
791
+ "2024-07-11",
792
+ "Redis caching tier added for repeated query traffic",
793
+ tags=["distributed-caching", "performance-optimization"],
794
+ ),
795
+ _make_fact(
796
+ "fact-cicd",
797
+ "2024-07-24",
798
+ "GitLab CI/CD pipeline added build, test, and deploy stages for the rollout",
799
+ tags=["deployment", "ci-cd"],
800
+ ),
801
+ ]
802
+
803
+ result = resolver.remember_when(
804
+ query="system architecture performance optimization plans",
805
+ time_range={"kind": "between_dates", "start": "2024-07-01", "end": "2024-07-29"},
806
+ max_results=4,
807
+ mode="change_over_time",
808
+ )
809
+
810
+ milestones = result["ordered_milestones"]
811
+
812
+ assert [item["date"] for item in milestones] == [
813
+ "2024-07-03",
814
+ "2024-07-09",
815
+ "2024-07-11",
816
+ "2024-07-24",
817
+ ]
818
+ assert "health checks" in milestones[1]["point"].lower()
819
+ assert "redis caching" in milestones[2]["point"].lower()
820
+ assert "deploy stages" in milestones[3]["point"].lower()
821
+
822
+
686
823
  def test_ordered_milestones_prefer_segment_summary_for_shared_date():
687
824
  resolver = TemporalResolver(store=FakeStore(), search_engine=FakeSearch(), config=_make_config())
688
825
 
@@ -485,6 +485,22 @@ class TestToolResultVerificationHint:
485
485
  "facts": [],
486
486
  }
487
487
  ],
488
+ "ordered_milestones": [
489
+ {
490
+ "date": "2024-11-05",
491
+ "theme": "context window mismatch",
492
+ "point": "Window size mismatch error handling was discussed.",
493
+ "source": "segment",
494
+ }
495
+ ],
496
+ "phase_milestones": [
497
+ {
498
+ "start_date": "2024-11-05",
499
+ "end_date": "2024-11-05",
500
+ "focus": "context window mismatch",
501
+ "points": ["Window size mismatch error handling was discussed."],
502
+ }
503
+ ],
488
504
  }
489
505
 
490
506
  result = execute_vc_tool(
@@ -499,11 +515,71 @@ class TestToolResultVerificationHint:
499
515
 
500
516
  parsed = json.loads(result)
501
517
  assert parsed["mode"] == "change_over_time"
518
+ assert "phase_milestones" in parsed
519
+ assert "ordered_milestones" in parsed
502
520
  assert parsed["date_buckets"][0]["date"] == "2024-11-05"
503
521
  assert "results" not in parsed
504
522
  assert "facts_in_window" not in parsed
505
523
  assert "reader_hint" in parsed
506
524
 
525
+ def test_remember_when_window_overview_prefers_date_buckets_payload(self):
526
+ engine = _mock_engine()
527
+ engine.remember_when.return_value = {
528
+ "query": "",
529
+ "mode": "window_overview",
530
+ "found": True,
531
+ "range": {
532
+ "kind": "between_dates",
533
+ "start": "2026-04-13",
534
+ "end": "2026-04-13",
535
+ },
536
+ "results": [
537
+ {"excerpt": "Configured nginx reverse proxy", "topic": "nginx-proxy"}
538
+ ],
539
+ "facts_in_window": [
540
+ {"what": "Set up nginx proxy routing", "when": "2026-04-13"}
541
+ ],
542
+ "date_buckets": [
543
+ {
544
+ "date": "2026-04-13",
545
+ "results": [
546
+ {
547
+ "topic": "nginx-proxy",
548
+ "excerpt": "Configured nginx reverse proxy",
549
+ "matched_terms": [],
550
+ "segment_ref": "seg-1",
551
+ "match_type": "summary_window",
552
+ }
553
+ ],
554
+ "facts": [
555
+ {
556
+ "what": "Set up nginx proxy routing",
557
+ "tags": ["nginx-proxy"],
558
+ "matched_terms": [],
559
+ "segment_ref": "seg-1",
560
+ }
561
+ ],
562
+ }
563
+ ],
564
+ }
565
+
566
+ result = execute_vc_tool(
567
+ engine,
568
+ "vc_remember_when",
569
+ {
570
+ "query": "",
571
+ "time_range": {"kind": "between_dates", "start": "2026-04-13", "end": "2026-04-13"},
572
+ "mode": "window_overview",
573
+ },
574
+ )
575
+
576
+ parsed = json.loads(result)
577
+ assert parsed["mode"] == "window_overview"
578
+ assert parsed["date_buckets"][0]["date"] == "2026-04-13"
579
+ assert "results" not in parsed
580
+ assert "facts_in_window" not in parsed
581
+ assert "reader_hint" in parsed
582
+
507
583
  def test_unknown_tool_returns_error(self):
508
584
  engine = MagicMock()
509
585
  result = execute_vc_tool(engine, "vc_unknown", {})
@@ -1,7 +1,11 @@
1
1
  """Tests for tool_result filtering in extract_history_pairs — assistant responses
2
2
  after tool_result must not be skipped."""
3
3
 
4
- from virtual_context.proxy.formats import AnthropicFormat
4
+ from virtual_context.proxy.formats import (
5
+ AnthropicFormat,
6
+ summarize_payload_accounting,
7
+ summarize_raw_payload_entries,
8
+ )
5
9
 
6
10
 
7
11
  class TestToolResultFilterPreservesAssistantResponse:
@@ -106,3 +110,37 @@ class TestToolResultFilterPreservesAssistantResponse:
106
110
  all_content = " ".join(p.content for p in pairs)
107
111
 
108
112
  assert "48MP" in all_content, "Final assistant synthesis was dropped"
113
+
114
+ def test_payload_accounting_explains_tool_fold_and_pair_counts(self):
115
+ fmt = AnthropicFormat()
116
+ body = {
117
+ "messages": [
118
+ {"role": "user", "content": "Tell me about rare beauty ingredients"},
119
+ {"role": "assistant", "content": [
120
+ {"type": "tool_use", "id": "t1", "name": "web_search", "input": {"q": "rare beauty"}},
121
+ {"type": "tool_use", "id": "t2", "name": "web_search", "input": {"q": "revolution"}},
122
+ ]},
123
+ {"role": "user", "content": [
124
+ {"type": "tool_result", "tool_use_id": "t1", "content": "Rare Beauty uses..."},
125
+ {"type": "tool_result", "tool_use_id": "t2", "content": "Revolution uses..."},
126
+ ]},
127
+ {"role": "assistant", "content": [
128
+ {"type": "thinking", "thinking": "Comparing the two..."},
129
+ {"type": "text", "text": "Same Caprylic/Capric Triglyceride for lightweight moisture"},
130
+ ]},
131
+ {"role": "user", "content": "Why do you think I have medium skin tone?"},
132
+ {"role": "assistant", "content": "Based on the shade recommendations..."},
133
+ ]
134
+ }
135
+
136
+ raw = summarize_raw_payload_entries(body["messages"])
137
+ accounting = summarize_payload_accounting(body, fmt, raw_summary=raw)
138
+
139
+ assert accounting["raw_payload_entry_count"] == 6
140
+ assert accounting["raw_tool_use_blocks"] == 2
141
+ assert accounting["raw_tool_result_only_user_entries"] == 1
142
+ assert accounting["assistant_tool_only_entries_folded"] == 1
143
+ assert accounting["tool_result_user_entries_folded"] == 1
144
+ assert accounting["tool_followup_pairs"] == 1
145
+ assert accounting["extracted_history_message_count"] == 4
146
+ assert accounting["extracted_history_pair_count"] == 2