virtual-context 0.4.2__tar.gz → 0.4.3__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 (321) hide show
  1. {virtual_context-0.4.2 → virtual_context-0.4.3}/.gitignore +1 -0
  2. {virtual_context-0.4.2 → virtual_context-0.4.3}/PKG-INFO +3 -1
  3. {virtual_context-0.4.2 → virtual_context-0.4.3}/pyproject.toml +3 -2
  4. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compaction_resume_e2e_postgres.py +5 -5
  5. virtual_context-0.4.3/tests/test_conversations_tenant_id_backfill_postgres.py +180 -0
  6. virtual_context-0.4.3/tests/test_conversations_tenant_id_propagation.py +186 -0
  7. virtual_context-0.4.3/tests/test_find_idle_deletable_conversations.py +223 -0
  8. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_postgres_store.py +56 -26
  9. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_proxy_session.py +82 -7
  10. virtual_context-0.4.3/tests/test_vcattach.py +903 -0
  11. virtual_context-0.4.3/tests/test_vcmerge_body_method.py +1795 -0
  12. virtual_context-0.4.3/tests/test_vcmerge_body_method_postgres.py +524 -0
  13. virtual_context-0.4.3/tests/test_vcmerge_composite_forwarders.py +433 -0
  14. virtual_context-0.4.3/tests/test_vcmerge_dual_handler_refuse.py +363 -0
  15. virtual_context-0.4.3/tests/test_vcmerge_schema.py +397 -0
  16. virtual_context-0.4.3/tests/test_vcmerge_schema_postgres.py +163 -0
  17. virtual_context-0.4.3/tests/test_vcmerge_storage.py +379 -0
  18. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/__init__.py +1 -1
  19. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/cli/main.py +3 -3
  20. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/config.py +5 -0
  21. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/composite_store.py +142 -4
  22. virtual_context-0.4.3/virtual_context/core/exceptions.py +101 -0
  23. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/engine.py +155 -12
  24. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/handlers.py +266 -15
  25. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/server.py +21 -11
  26. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/state.py +47 -26
  27. virtual_context-0.4.3/virtual_context/proxy/vcattach.py +165 -0
  28. virtual_context-0.4.3/virtual_context/storage/postgres.py +6580 -0
  29. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/storage/sqlite.py +1041 -54
  30. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/types.py +109 -5
  31. virtual_context-0.4.2/tests/test_vcattach.py +0 -413
  32. virtual_context-0.4.2/virtual_context/proxy/vcattach.py +0 -108
  33. virtual_context-0.4.2/virtual_context/storage/postgres.py +0 -5302
  34. {virtual_context-0.4.2 → virtual_context-0.4.3}/LICENSE +0 -0
  35. {virtual_context-0.4.2 → virtual_context-0.4.3}/README.md +0 -0
  36. {virtual_context-0.4.2 → virtual_context-0.4.3}/assets/dashboard.png +0 -0
  37. {virtual_context-0.4.2 → virtual_context-0.4.3}/assets/hero.png +0 -0
  38. {virtual_context-0.4.2 → virtual_context-0.4.3}/models.yaml +0 -0
  39. {virtual_context-0.4.2 → virtual_context-0.4.3}/scripts/install.ps1 +0 -0
  40. {virtual_context-0.4.2 → virtual_context-0.4.3}/scripts/install.sh +0 -0
  41. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/REGRESSION_MAP.md +0 -0
  42. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/conftest.py +0 -0
  43. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/docker-compose.test.yml +0 -0
  44. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/haiku/__init__.py +0 -0
  45. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/haiku/conftest.py +0 -0
  46. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/haiku/test_compaction.py +0 -0
  47. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/haiku/test_retrieval.py +0 -0
  48. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/haiku/test_tagging.py +0 -0
  49. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/ollama/__init__.py +0 -0
  50. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/ollama/conftest.py +0 -0
  51. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/ollama/test_compactor.py +0 -0
  52. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/ollama/test_pipeline.py +0 -0
  53. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/ollama/test_provider.py +0 -0
  54. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/ollama/test_tag_generator.py +0 -0
  55. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/proxy/__init__.py +0 -0
  56. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/proxy/test_dashboard_cors.py +0 -0
  57. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/proxy/test_metrics.py +0 -0
  58. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_active_compaction_op_invariants.py +0 -0
  59. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_assembler.py +0 -0
  60. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_backend_integration.py +0 -0
  61. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_beam_judge.py +0 -0
  62. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_budget_enforcement.py +0 -0
  63. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_canonical_row_epoch_guards.py +0 -0
  64. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_canonical_row_epoch_guards_postgres.py +0 -0
  65. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_canonical_turn_grouping.py +0 -0
  66. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_canonical_turns_schema.py +0 -0
  67. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_canonical_turns_schema_postgres.py +0 -0
  68. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_claim_compaction_lease_signature.py +0 -0
  69. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_claim_compaction_lease_signature_postgres.py +0 -0
  70. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_cleanup_abandoned_compaction.py +0 -0
  71. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_cleanup_abandoned_compaction_postgres.py +0 -0
  72. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_cli_init.py +0 -0
  73. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compacting_phase_takeover.py +0 -0
  74. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compaction_commit_prune.py +0 -0
  75. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compaction_heartbeat_sidecar.py +0 -0
  76. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compaction_lease_lost_abort.py +0 -0
  77. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compaction_lifecycle.py +0 -0
  78. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compaction_lifecycle_contract.py +0 -0
  79. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compaction_lifecycle_wiring.py +0 -0
  80. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compaction_operation.py +0 -0
  81. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compaction_operation_crud.py +0 -0
  82. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compaction_operation_crud_postgres.py +0 -0
  83. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compaction_operation_id_uuid.py +0 -0
  84. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compaction_operation_postgres.py +0 -0
  85. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compaction_ownership_guards.py +0 -0
  86. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compaction_progress_event.py +0 -0
  87. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compaction_resume_schema_migration.py +0 -0
  88. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compaction_resume_schema_migration_postgres.py +0 -0
  89. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compactor.py +0 -0
  90. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_compactor_concurrent.py +0 -0
  91. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_composite_store.py +0 -0
  92. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_config.py +0 -0
  93. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_context_bleed.py +0 -0
  94. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_conversation_coverage_report.py +0 -0
  95. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_conversation_identity.py +0 -0
  96. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_conversation_lifecycle.py +0 -0
  97. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_conversation_schema.py +0 -0
  98. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_conversation_schema_postgres.py +0 -0
  99. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_conversation_scoping.py +0 -0
  100. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_deferred_payload_compaction.py +0 -0
  101. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_delete_resurrect_epoch_races.py +0 -0
  102. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_embedding_tag_generator.py +0 -0
  103. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_empty_turn_skip.py +0 -0
  104. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_engine_event_bus.py +0 -0
  105. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_engine_integration.py +0 -0
  106. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_engine_lifecycle_epoch.py +0 -0
  107. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_engine_lookback.py +0 -0
  108. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_engine_state.py +0 -0
  109. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_engine_sync_turns.py +0 -0
  110. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_fact_enrichment.py +0 -0
  111. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_fact_graph_integration.py +0 -0
  112. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_fact_link_checker.py +0 -0
  113. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_fact_link_query.py +0 -0
  114. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_fact_link_types.py +0 -0
  115. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_fact_links_sqlite.py +0 -0
  116. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_fact_redesign.py +0 -0
  117. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_filesystem_store_compaction_compat.py +0 -0
  118. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_fill_pass.py +0 -0
  119. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_find_quote.py +0 -0
  120. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_find_stale_lease_helpers.py +0 -0
  121. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_format_agnostic.py +0 -0
  122. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_handle_prepare_payload.py +0 -0
  123. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_headless.py +0 -0
  124. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_history_filter.py +0 -0
  125. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_idf_retrieval.py +0 -0
  126. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_ingest_index_integrity.py +0 -0
  127. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_ingest_reconciler_append_fastpath.py +0 -0
  128. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_ingest_reconciler_epoch.py +0 -0
  129. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_ingestion_compaction_full_cycle.py +0 -0
  130. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_ingestion_db_sweep_catchup.py +0 -0
  131. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_ingestion_episode.py +0 -0
  132. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_ingestion_episode_crud.py +0 -0
  133. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_ingestion_episode_crud_postgres.py +0 -0
  134. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_ingestion_episode_postgres.py +0 -0
  135. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_ingestion_finalize_self_heal.py +0 -0
  136. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_ingestion_progress_event.py +0 -0
  137. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_ingestion_regression_resume.py +0 -0
  138. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_legacy_ingestion_lease_ownership.py +0 -0
  139. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_lifecycle_epoch.py +0 -0
  140. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_lifecycle_epoch_store.py +0 -0
  141. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_lifecycle_epoch_store_postgres.py +0 -0
  142. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_longmemeval_auth.py +0 -0
  143. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_mark_canonical_turns_compacted_all_halves.py +0 -0
  144. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_mcp_server.py +0 -0
  145. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_media.py +0 -0
  146. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_message_filter.py +0 -0
  147. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_metrics_persistence.py +0 -0
  148. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_model_catalog.py +0 -0
  149. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_model_limits.py +0 -0
  150. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_monitor.py +0 -0
  151. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_multi_instance.py +0 -0
  152. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_multi_worker_sliding_window.py +0 -0
  153. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_neo4j_falkordb_compaction_compat.py +0 -0
  154. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_noop_fact_link_store.py +0 -0
  155. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_openrouter_provider.py +0 -0
  156. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_orphan_compacting_repair.py +0 -0
  157. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_paging.py +0 -0
  158. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_passthrough_filter.py +0 -0
  159. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_phase_transition_events.py +0 -0
  160. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_presets.py +0 -0
  161. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_prev_context_leak.py +0 -0
  162. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_progress_event_bus.py +0 -0
  163. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_progress_events.py +0 -0
  164. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_progress_snapshot.py +0 -0
  165. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_progress_snapshot_postgres.py +0 -0
  166. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_provider_adapters.py +0 -0
  167. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_proxy.py +0 -0
  168. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_proxy_dashboard.py +0 -0
  169. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_proxy_formats.py +0 -0
  170. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_proxy_message_filter.py +0 -0
  171. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_proxy_streaming.py +0 -0
  172. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_raw_content.py +0 -0
  173. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_recall_all.py +0 -0
  174. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_refresh_compaction_heartbeat.py +0 -0
  175. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_registry_lifecycle.py +0 -0
  176. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_request_captures_persistence.py +0 -0
  177. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_request_metadata.py +0 -0
  178. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_request_metadata_postgres.py +0 -0
  179. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_retriever.py +0 -0
  180. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_rrf_scoring.py +0 -0
  181. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_segmenter.py +0 -0
  182. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_semantic_search.py +0 -0
  183. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_sender_identity.py +0 -0
  184. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_session_cache.py +0 -0
  185. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_session_date.py +0 -0
  186. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_session_state.py +0 -0
  187. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_sqlite_schema_add_column_race.py +0 -0
  188. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_storage_protocols.py +0 -0
  189. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_store_recovery.py +0 -0
  190. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_store_sqlite.py +0 -0
  191. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_stub_turn_handling.py +0 -0
  192. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_supersession.py +0 -0
  193. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_supersession_migration.py +0 -0
  194. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_tag_canonicalizer.py +0 -0
  195. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_tag_consolidator.py +0 -0
  196. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_tag_generator.py +0 -0
  197. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_tag_splitter.py +0 -0
  198. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_tagger_loop.py +0 -0
  199. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_telemetry.py +0 -0
  200. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_telemetry_integration.py +0 -0
  201. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_temporal_resolver.py +0 -0
  202. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_tool_loop.py +0 -0
  203. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_tool_output_interceptor.py +0 -0
  204. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_tool_result_filter.py +0 -0
  205. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_tool_tags.py +0 -0
  206. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_tui.py +0 -0
  207. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_turn_grouping.py +0 -0
  208. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_turn_tag_index.py +0 -0
  209. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_unified_budget.py +0 -0
  210. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_upstream_trim.py +0 -0
  211. {virtual_context-0.4.2 → virtual_context-0.4.3}/tests/test_verb_expansion.py +0 -0
  212. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual-context.yaml.example +0 -0
  213. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/cli/__init__.py +0 -0
  214. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/conversation_identity.py +0 -0
  215. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/__init__.py +0 -0
  216. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/assembler.py +0 -0
  217. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/canonical_turns.py +0 -0
  218. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/compaction_pipeline.py +0 -0
  219. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/compactor.py +0 -0
  220. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/conversation_store.py +0 -0
  221. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/coverage_report.py +0 -0
  222. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/embedding_provider.py +0 -0
  223. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/embedding_tag_generator.py +0 -0
  224. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/engine_utils.py +0 -0
  225. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/event_bus.py +0 -0
  226. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/fact_query.py +0 -0
  227. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/fts_preprocessor.py +0 -0
  228. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/hint_builder.py +0 -0
  229. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/ingest_reconciler.py +0 -0
  230. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/lifecycle_epoch.py +0 -0
  231. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/llm_utils.py +0 -0
  232. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/math_utils.py +0 -0
  233. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/model_catalog.py +0 -0
  234. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/monitor.py +0 -0
  235. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/paging_manager.py +0 -0
  236. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/progress_events.py +0 -0
  237. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/progress_snapshot.py +0 -0
  238. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/protocols.py +0 -0
  239. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/provider_adapters.py +0 -0
  240. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/quote_search.py +0 -0
  241. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/retrieval_assembler.py +0 -0
  242. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/retrieval_scoring.py +0 -0
  243. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/retriever.py +0 -0
  244. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/search_engine.py +0 -0
  245. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/segmenter.py +0 -0
  246. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/semantic_search.py +0 -0
  247. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/store.py +0 -0
  248. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/tag_canonicalizer.py +0 -0
  249. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/tag_consolidator.py +0 -0
  250. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/tag_generator.py +0 -0
  251. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/tag_scoring.py +0 -0
  252. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/tag_splitter.py +0 -0
  253. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/tagging_pipeline.py +0 -0
  254. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/telemetry.py +0 -0
  255. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/temporal_resolver.py +0 -0
  256. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/tool_loop.py +0 -0
  257. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/tool_query.py +0 -0
  258. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/core/turn_tag_index.py +0 -0
  259. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/data/anthropic-tokenizer/tokenizer.json +0 -0
  260. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/data/models.yaml +0 -0
  261. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/ingest/__init__.py +0 -0
  262. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/ingest/curator.py +0 -0
  263. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/ingest/date_resolver.py +0 -0
  264. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/ingest/parsers.py +0 -0
  265. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/ingest/supersession.py +0 -0
  266. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/mcp/__init__.py +0 -0
  267. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/mcp/server.py +0 -0
  268. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/model_limits.py +0 -0
  269. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/openclaw/virtual-context.mjs +0 -0
  270. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/patterns.py +0 -0
  271. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/presets/__init__.py +0 -0
  272. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/presets/agentic.py +0 -0
  273. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/presets/base.py +0 -0
  274. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/presets/coding.py +0 -0
  275. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/providers/__init__.py +0 -0
  276. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/providers/anthropic.py +0 -0
  277. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/providers/base.py +0 -0
  278. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/providers/generic_openai.py +0 -0
  279. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/providers/ollama_native.py +0 -0
  280. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/__init__.py +0 -0
  281. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/_envelope.py +0 -0
  282. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/dashboard.html +0 -0
  283. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/dashboard.py +0 -0
  284. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/formats.py +0 -0
  285. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/helpers.py +0 -0
  286. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/media.py +0 -0
  287. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/message_filter.py +0 -0
  288. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/metrics.py +0 -0
  289. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/multi.py +0 -0
  290. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/registry.py +0 -0
  291. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/session_cache.py +0 -0
  292. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/session_state.py +0 -0
  293. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/static/android-chrome-192x192.png +0 -0
  294. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/static/android-chrome-512x512.png +0 -0
  295. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/static/apple-touch-icon.png +0 -0
  296. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/static/favicon-16x16.png +0 -0
  297. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/static/favicon-32x32.png +0 -0
  298. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/static/favicon.ico +0 -0
  299. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/static/site.webmanifest +0 -0
  300. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/proxy/tool_output_interceptor.py +0 -0
  301. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/storage/__init__.py +0 -0
  302. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/storage/falkordb.py +0 -0
  303. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/storage/filesystem.py +0 -0
  304. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/storage/helpers.py +0 -0
  305. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/storage/neo4j.py +0 -0
  306. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/storage/noop_fact_link_store.py +0 -0
  307. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/token_counter.py +0 -0
  308. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/tui/__init__.py +0 -0
  309. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/tui/app.py +0 -0
  310. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/tui/chat.tcss +0 -0
  311. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/tui/chat_provider.py +0 -0
  312. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/tui/headless.py +0 -0
  313. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/tui/modals/__init__.py +0 -0
  314. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/tui/modals/turn_inspector.py +0 -0
  315. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/tui/state.py +0 -0
  316. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/tui/widgets/__init__.py +0 -0
  317. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/tui/widgets/budget_bar.py +0 -0
  318. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/tui/widgets/chat_view.py +0 -0
  319. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/tui/widgets/input_box.py +0 -0
  320. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/tui/widgets/tag_panel.py +0 -0
  321. {virtual_context-0.4.2 → virtual_context-0.4.3}/virtual_context/tui/widgets/turn_list.py +0 -0
@@ -64,3 +64,4 @@ extraneous/paper/*.png
64
64
 
65
65
  # OS
66
66
  .DS_Store
67
+ .agent-prompt.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: virtual-context
3
- Version: 0.4.2
3
+ Version: 0.4.3
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
@@ -28,6 +28,7 @@ Requires-Dist: tiktoken>=0.7
28
28
  Requires-Dist: torch>=2.0
29
29
  Requires-Dist: uvicorn>=0.30
30
30
  Provides-Extra: all
31
+ Requires-Dist: psycopg-pool>=3.1; extra == 'all'
31
32
  Requires-Dist: psycopg[binary]>=3.1; extra == 'all'
32
33
  Requires-Dist: redis>=5.0; extra == 'all'
33
34
  Provides-Extra: dev
@@ -40,6 +41,7 @@ Requires-Dist: falkordb>=1.0; extra == 'falkordb'
40
41
  Provides-Extra: neo4j
41
42
  Requires-Dist: neo4j>=5.0; extra == 'neo4j'
42
43
  Provides-Extra: postgres
44
+ Requires-Dist: psycopg-pool>=3.1; extra == 'postgres'
43
45
  Requires-Dist: psycopg[binary]>=3.1; extra == 'postgres'
44
46
  Provides-Extra: redis
45
47
  Requires-Dist: redis>=5.0; extra == 'redis'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "virtual-context"
7
- version = "0.4.2"
7
+ version = "0.4.3"
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"
@@ -34,12 +34,13 @@ dependencies = [
34
34
  ]
35
35
 
36
36
  [project.optional-dependencies]
37
- postgres = ["psycopg[binary]>=3.1"]
37
+ postgres = ["psycopg[binary]>=3.1", "psycopg-pool>=3.1"]
38
38
  neo4j = ["neo4j>=5.0"]
39
39
  falkordb = ["falkordb>=1.0"]
40
40
  redis = ["redis>=5.0"]
41
41
  all = [
42
42
  "psycopg[binary]>=3.1",
43
+ "psycopg-pool>=3.1",
43
44
  "redis>=5.0",
44
45
  ]
45
46
  dev = [
@@ -167,7 +167,7 @@ def _age_heartbeat(pg_store, op_id: str, age_s: float = 600) -> None:
167
167
  def test_compaction_resume_e2e_postgres():
168
168
  """Full takeover flow against real Postgres.
169
169
 
170
- Worker-death simulation: simple heartbeat back-dating (plan §5 "go with
170
+ Worker-death simulation: simple heartbeat back-dating (plan "go with
171
171
  the simple approach"). No subprocess spawning required.
172
172
 
173
173
  LLM stubbing: compact_if_needed is replaced with a MagicMock that
@@ -314,10 +314,10 @@ def test_compaction_resume_e2e_postgres():
314
314
  )
315
315
 
316
316
  # 7d. ALL canonical_turns have compacted_at NOT NULL
317
- # and compaction_operation_id = new_op
318
- # (compact_if_needed is mocked to return None so no mark_canonical
319
- # call fires inside the pipeline — skip these assertions when
320
- # the mock swallows them, but assert the zero-dead-op invariant)
317
+ # and compaction_operation_id = new_op
318
+ # (compact_if_needed is mocked to return None so no mark_canonical
319
+ # call fires inside the pipeline — skip these assertions when
320
+ # the mock swallows them, but assert the zero-dead-op invariant)
321
321
  dead_ct_count = conn.execute(
322
322
  """
323
323
  SELECT COUNT(*) AS cnt FROM canonical_turns
@@ -0,0 +1,180 @@
1
+ """PG smoke for conversations.tenant_id backfill from cloud_conversations.
2
+
3
+ Skipped when DATABASE_URL is absent (the standard project pattern at
4
+ tests/test_vcmerge_schema_postgres.py:28). Verifies the one-time
5
+ backfill UPDATE that ``PostgresStore._ensure_schema`` runs on bootstrap
6
+ correctly populates ``conversations.tenant_id`` from
7
+ ``cloud_conversations.tenant_id`` for rows that existed before the
8
+ insertion-path fix landed.
9
+
10
+ Per prod blocker: 14 existing user convs in prod tenant
11
+ ``43bd6d7f0f8d6798`` had ``conversations.tenant_id = ''`` (engine
12
+ predecessor passed empty placeholder); cloud_conversations was correctly
13
+ populated by TenantMiddleware. The migration needs to be idempotent so
14
+ re-running schema bootstrap is safe.
15
+
16
+ Marked @pytest.mark.regression("VCATTACH-DATALOSS-2026-04-26").
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import uuid
23
+ from datetime import datetime, timezone
24
+
25
+ import pytest
26
+
27
+
28
+ pytestmark = [
29
+ pytest.mark.skipif(
30
+ not os.environ.get("DATABASE_URL"),
31
+ reason="DATABASE_URL not set: Postgres smoke skipped",
32
+ ),
33
+ pytest.mark.regression("VCATTACH-DATALOSS-2026-04-26"),
34
+ ]
35
+
36
+
37
+ @pytest.fixture
38
+ def pg_store():
39
+ """Build a PostgresStore against DATABASE_URL. Cleanup teardown drops
40
+ test rows under the test tenant prefix."""
41
+ from virtual_context.storage.postgres import PostgresStore
42
+ dsn = os.environ["DATABASE_URL"]
43
+ store = PostgresStore(dsn)
44
+ yield store
45
+ try:
46
+ conn = store._get_conn()
47
+ conn.execute("DELETE FROM conversations WHERE tenant_id LIKE 'pgbf-%' "
48
+ "OR conversation_id LIKE 'pgbf-%'")
49
+ try:
50
+ conn.execute("DELETE FROM cloud_conversations WHERE tenant_id LIKE 'pgbf-%' "
51
+ "OR conversation_id LIKE 'pgbf-%'")
52
+ except Exception:
53
+ pass
54
+ except Exception:
55
+ pass
56
+
57
+
58
+ def _now():
59
+ return datetime.now(timezone.utc)
60
+
61
+
62
+ def _ensure_cloud_conversations_table(conn):
63
+ """If the test environment's PG doesn't have cloud_conversations
64
+ (pure-engine deploys), create a minimal version sufficient for the
65
+ backfill smoke. The real cloud schema is richer; this stub just has
66
+ the columns the backfill UPDATE references.
67
+ """
68
+ conn.execute(
69
+ """
70
+ CREATE TABLE IF NOT EXISTS cloud_conversations (
71
+ conversation_id TEXT NOT NULL,
72
+ tenant_id TEXT NOT NULL DEFAULT '',
73
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
74
+ )
75
+ """
76
+ )
77
+
78
+
79
+ def test_pg_backfill_populates_empty_tenant_id_from_cloud_conversations(pg_store):
80
+ """: simulate the prod state (conversations row with empty
81
+ tenant_id; matching cloud_conversations row with the right tenant_id).
82
+ Run schema bootstrap; assert backfill populated the empty cell.
83
+ """
84
+ conn = pg_store._get_conn()
85
+ _ensure_cloud_conversations_table(conn)
86
+
87
+ tid = f"pgbf-{uuid.uuid4().hex[:8]}"
88
+ cid = f"pgbf-conv-{uuid.uuid4().hex[:8]}"
89
+ # Pre-fold prod state: empty tenant_id on conversations.
90
+ conn.execute(
91
+ "INSERT INTO conversations (conversation_id, tenant_id, phase, "
92
+ "created_at, updated_at) VALUES (%s, '', 'active', %s, %s)",
93
+ (cid, _now(), _now()),
94
+ )
95
+ # Cloud's wrapper has the right tenant_id.
96
+ conn.execute(
97
+ "INSERT INTO cloud_conversations (conversation_id, tenant_id) "
98
+ "VALUES (%s, %s)",
99
+ (cid, tid),
100
+ )
101
+ # Re-trigger schema bootstrap (which runs the backfill UPDATE).
102
+ pg_store._ensure_schema()
103
+ # Backfill should have populated the empty cell from cloud_conversations.
104
+ row = conn.execute(
105
+ "SELECT tenant_id FROM conversations WHERE conversation_id = %s",
106
+ (cid,),
107
+ ).fetchone()
108
+ assert row["tenant_id"] == tid, (
109
+ f"Backfill should have set conversations.tenant_id to {tid!r}, "
110
+ f"got {row['tenant_id']!r}"
111
+ )
112
+
113
+
114
+ def test_pg_backfill_is_idempotent(pg_store):
115
+ """: second bootstrap run is a no-op (the WHERE filter on
116
+ empty target makes the migration idempotent)."""
117
+ conn = pg_store._get_conn()
118
+ _ensure_cloud_conversations_table(conn)
119
+
120
+ tid = f"pgbf-{uuid.uuid4().hex[:8]}"
121
+ cid = f"pgbf-conv-{uuid.uuid4().hex[:8]}"
122
+ conn.execute(
123
+ "INSERT INTO conversations (conversation_id, tenant_id, phase, "
124
+ "created_at, updated_at) VALUES (%s, '', 'active', %s, %s)",
125
+ (cid, _now(), _now()),
126
+ )
127
+ conn.execute(
128
+ "INSERT INTO cloud_conversations (conversation_id, tenant_id) "
129
+ "VALUES (%s, %s)",
130
+ (cid, tid),
131
+ )
132
+ pg_store._ensure_schema() # First backfill
133
+ # Manually mutate cloud_conversations to a different tenant; bootstrap
134
+ # again. The WHERE clause filters on empty target so the row's
135
+ # already-populated tenant_id should NOT be overwritten.
136
+ conn.execute(
137
+ "UPDATE cloud_conversations SET tenant_id = %s WHERE conversation_id = %s",
138
+ (f"{tid}-mutated", cid),
139
+ )
140
+ pg_store._ensure_schema() # Second run; should be no-op for this row
141
+ final = conn.execute(
142
+ "SELECT tenant_id FROM conversations WHERE conversation_id = %s",
143
+ (cid,),
144
+ ).fetchone()["tenant_id"]
145
+ assert final == tid, (
146
+ "Backfill should be idempotent; populated row's tenant_id must "
147
+ "NOT be overwritten on subsequent bootstraps"
148
+ )
149
+
150
+
151
+ def test_pg_backfill_skips_when_cloud_conversations_absent(pg_store, tmp_path):
152
+ """: when ``cloud_conversations`` is absent (engine-only deploy),
153
+ the backfill is silently skipped via the UndefinedTable narrow-catch.
154
+ Bootstrap completes without raising.
155
+
156
+ Validates the SAVEPOINT-via-nested-transaction pattern around the
157
+ backfill UPDATE: the missing-table raise rolls back ONLY the savepoint
158
+ and leaves the outer schema bootstrap alive.
159
+ """
160
+ conn = pg_store._get_conn()
161
+ # Drop cloud_conversations if it exists (simulating engine-only deploy).
162
+ try:
163
+ conn.execute("DROP TABLE IF EXISTS cloud_conversations")
164
+ except Exception:
165
+ pass
166
+ # Schema bootstrap must NOT raise.
167
+ pg_store._ensure_schema()
168
+ # And bootstrap should still create (or leave intact) other engine
169
+ # tables, evidenced by being able to insert a conversations row.
170
+ cid = f"pgbf-skipped-{uuid.uuid4().hex[:8]}"
171
+ conn.execute(
172
+ "INSERT INTO conversations (conversation_id, tenant_id, phase, "
173
+ "created_at, updated_at) VALUES (%s, '', 'active', %s, %s)",
174
+ (cid, _now(), _now()),
175
+ )
176
+ row = conn.execute(
177
+ "SELECT 1 FROM conversations WHERE conversation_id = %s",
178
+ (cid,),
179
+ ).fetchone()
180
+ assert row is not None
@@ -0,0 +1,186 @@
1
+ """Tenant_id propagation tests for the conversations table ( prod fold).
2
+
3
+ Per prod blocker (cloud E2E surfaced: every existing user conv
4
+ plus every freshly-created conv had ``conversations.tenant_id = ''``,
5
+ which broke VCMERGE Layer C tenant validation with HTTP 403):
6
+
7
+ 1. ``VirtualContextConfig.tenant_id`` field carries the request-scope
8
+ tenant context to every engine-internal ``upsert_conversation`` call.
9
+ 2. The engine's ``_load_lifecycle_epoch_into_engine_state`` and
10
+ ``sync_turns_from_payload`` paths now pass ``self.config.tenant_id``
11
+ instead of the empty-string placeholder.
12
+ 3. PostgreSQL ``_ensure_schema`` runs a one-time backfill UPDATE from
13
+ ``cloud_conversations.tenant_id`` on bootstrap (DATABASE_URL gated;
14
+ skipped silently when ``cloud_conversations`` is absent).
15
+
16
+ Tests pin the structural invariants behaviorally where possible and via
17
+ inspection where the cloud/PG cross-table join can't be exercised on
18
+ SQLite alone.
19
+
20
+ Marked @pytest.mark.regression("VCATTACH-DATALOSS-2026-04-26") so the
21
+ entire backstory is searchable.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import os
27
+ import uuid
28
+ from datetime import datetime, timezone
29
+
30
+ import pytest
31
+
32
+ from virtual_context.storage.sqlite import SQLiteStore
33
+ from virtual_context.types import VirtualContextConfig
34
+
35
+
36
+ pytestmark = pytest.mark.regression("VCATTACH-DATALOSS-2026-04-26")
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Config field: VirtualContextConfig.tenant_id is a string; defaults to ''.
41
+ # ---------------------------------------------------------------------------
42
+
43
+ def test_config_tenant_id_field_defaults_to_empty_string():
44
+ cfg = VirtualContextConfig()
45
+ assert hasattr(cfg, "tenant_id")
46
+ assert cfg.tenant_id == ""
47
+
48
+
49
+ def test_config_tenant_id_field_round_trips_via_yaml(tmp_path):
50
+ """Config builder accepts ``tenant_id`` in raw yaml dict."""
51
+ from virtual_context.config import _build_config
52
+ cfg = _build_config({"tenant_id": "tenant-xyz"})
53
+ assert cfg.tenant_id == "tenant-xyz"
54
+
55
+
56
+ def test_config_tenant_id_optional_in_yaml(tmp_path):
57
+ """No ``tenant_id`` key in yaml means empty default (single-user dev)."""
58
+ from virtual_context.config import _build_config
59
+ cfg = _build_config({})
60
+ assert cfg.tenant_id == ""
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Engine populates conversations.tenant_id from config when creating rows.
65
+ # ---------------------------------------------------------------------------
66
+
67
+ def test_upsert_conversation_writes_tenant_id_from_caller(tmp_path):
68
+ """``Store.upsert_conversation`` writes the caller-supplied tenant_id
69
+ into the conversations row (the storage primitive has always supported
70
+ this; the bug was at engine call sites that passed '')."""
71
+ store = SQLiteStore(tmp_path / "store.db")
72
+ conv_id = "test-conv-" + uuid.uuid4().hex[:8]
73
+ store.upsert_conversation(tenant_id="tenant-A", conversation_id=conv_id)
74
+ conn = store._get_conn()
75
+ row = conn.execute(
76
+ "SELECT tenant_id FROM conversations WHERE conversation_id = ?",
77
+ (conv_id,),
78
+ ).fetchone()
79
+ assert row["tenant_id"] == "tenant-A"
80
+
81
+
82
+ def test_engine_lifecycle_load_uses_config_tenant_id(tmp_path):
83
+ """Structural pin: ``Engine._load_lifecycle_epoch_into_engine_state``
84
+ no longer hard-codes ``tenant_id=""`` in its upsert_conversation call;
85
+ the call sources tenant_id from ``self.config.tenant_id``.
86
+ """
87
+ import inspect
88
+ from virtual_context.engine import VirtualContextEngine
89
+ src = inspect.getsource(
90
+ VirtualContextEngine._load_lifecycle_epoch_into_engine_state
91
+ )
92
+ # The literal hard-coded empty placeholder is gone.
93
+ assert 'tenant_id=""' not in src, (
94
+ "tenant_id-propagation: Engine._load_lifecycle_epoch_into_engine_state still "
95
+ "hard-codes tenant_id=\"\"; tenant scoping regression"
96
+ )
97
+ # The config-derived form is present.
98
+ assert "self.config.tenant_id" in src or "tenant_id=tenant_id" in src, (
99
+ "tenant_id-propagation: Engine._load_lifecycle_epoch_into_engine_state should "
100
+ "source tenant_id from self.config.tenant_id"
101
+ )
102
+
103
+
104
+ def test_engine_sync_turns_uses_config_tenant_id(tmp_path):
105
+ """Structural pin: ``Engine.sync_turns_from_payload`` no
106
+ longer hard-codes ``tenant_id=""`` in its upsert_conversation call.
107
+ """
108
+ import inspect
109
+ from virtual_context.engine import VirtualContextEngine
110
+ src = inspect.getsource(VirtualContextEngine.sync_turns_from_payload)
111
+ assert 'tenant_id=""' not in src, (
112
+ "tenant_id-propagation: Engine.sync_turns_from_payload still hard-codes "
113
+ "tenant_id=\"\"; tenant scoping regression"
114
+ )
115
+ assert "self.config.tenant_id" in src, (
116
+ "tenant_id-propagation: Engine.sync_turns_from_payload should source tenant_id "
117
+ "from self.config.tenant_id"
118
+ )
119
+
120
+
121
+ def test_engine_creates_conversations_row_with_config_tenant_id(tmp_path):
122
+ """End-to-end: instantiate engine with ``cfg.tenant_id`` set; the
123
+ bootstrap-created conversations row carries that tenant_id (was '' in
124
+ pre-fold prod, breaking VCMERGE tenant scoping)."""
125
+ from virtual_context.engine import VirtualContextEngine
126
+ cfg = VirtualContextConfig(
127
+ tenant_id="tenant-prod-fix",
128
+ conversation_id="conv-test-" + uuid.uuid4().hex[:8],
129
+ )
130
+ cfg.storage.backend = "sqlite"
131
+ cfg.storage.sqlite_path = str(tmp_path / "store.db")
132
+ engine = VirtualContextEngine(config=cfg)
133
+ try:
134
+ store = engine._store
135
+ # Drill into composite/inner stores to reach the SQLite path.
136
+ inner = getattr(store, "_segments", None) or getattr(store, "_store", None) or store
137
+ conn = inner._get_conn()
138
+ row = conn.execute(
139
+ "SELECT tenant_id FROM conversations WHERE conversation_id = ?",
140
+ (cfg.conversation_id,),
141
+ ).fetchone()
142
+ assert row is not None, "engine bootstrap did not create conversations row"
143
+ assert row["tenant_id"] == "tenant-prod-fix", (
144
+ f"conversations.tenant_id should be 'tenant-prod-fix' but got "
145
+ f"{row['tenant_id']!r}; pre-fold regression"
146
+ )
147
+ finally:
148
+ engine.close()
149
+
150
+
151
+ # ---------------------------------------------------------------------------
152
+ # backfill behavior: source tenant_id from cloud_conversations.
153
+ # Structural-only on SQLite (no cloud_conversations table); behavioral
154
+ # in tests/test_conversations_tenant_id_backfill_postgres.py against PG.
155
+ # ---------------------------------------------------------------------------
156
+
157
+ def test_pg_backfill_sql_present_in_ensure_schema():
158
+ """ structural pin: ``PostgresStore._ensure_schema`` must contain
159
+ the cloud_conversations backfill UPDATE. A future regression that drops
160
+ the migration would fail this test before reaching staging."""
161
+ import inspect
162
+ from virtual_context.storage import postgres as postgres_mod
163
+ src = inspect.getsource(postgres_mod.PostgresStore._ensure_schema)
164
+ # The backfill SQL fingerprint: "FROM cloud_conversations" + tenant_id update target.
165
+ assert "FROM cloud_conversations" in src, (
166
+ "tenant_id-propagation: PostgresStore._ensure_schema missing cloud_conversations "
167
+ "backfill UPDATE; existing prod convs would stay with empty tenant_id"
168
+ )
169
+ assert "UPDATE conversations" in src, (
170
+ "tenant_id-propagation: PostgresStore._ensure_schema backfill missing UPDATE "
171
+ "conversations target"
172
+ )
173
+ # Idempotency safeguard: WHERE filters on empty target.
174
+ assert (
175
+ "conversations.tenant_id = ''" in src
176
+ or 'conversations.tenant_id IS NULL' in src
177
+ ), (
178
+ "tenant_id-propagation: backfill UPDATE missing the empty-source idempotency "
179
+ "filter; would re-write tenant_id on every schema bootstrap"
180
+ )
181
+ # Best-effort gate: UndefinedTable swallowed.
182
+ assert "UndefinedTable" in src, (
183
+ "tenant_id-propagation: backfill should swallow only psycopg.errors.UndefinedTable "
184
+ "(missing cloud_conversations on engine-only deploys); other "
185
+ "exceptions must propagate"
186
+ )
@@ -0,0 +1,223 @@
1
+ """Tests for find_idle_deletable_conversations — the store helper
2
+ that feeds the sweeper's idle-conv auto-delete pass.
3
+
4
+ Contract under test:
5
+ - returns convs with canonical_turns count < max_msgs
6
+ - returns convs whose last-activity timestamp is older than min_age_s
7
+ - never returns deleted/compacting convs
8
+ - never returns convs with a running ingestion_episode
9
+ - never returns convs with a running or queued compaction_operation
10
+ - returns oldest-activity-first, bounded by ``limit``
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE")
16
+
17
+ import uuid as _uuid
18
+ from datetime import datetime, timezone, timedelta
19
+ from pathlib import Path
20
+
21
+ import pytest
22
+
23
+
24
+ def _make_store(tmp_path: Path):
25
+ from virtual_context.storage.sqlite import SQLiteStore
26
+ return SQLiteStore(db_path=str(tmp_path / "vc.db"))
27
+
28
+
29
+ def _seed_conv(store, conv_id: str, *, tenant="tnt", phase="active",
30
+ created_s_ago: float = 0.0, updated_s_ago: float = 0.0,
31
+ deleted: bool = False):
32
+ now = datetime.now(timezone.utc)
33
+ created = (now - timedelta(seconds=created_s_ago)).isoformat()
34
+ updated = (now - timedelta(seconds=updated_s_ago)).isoformat()
35
+ del_at = now.isoformat() if deleted else None
36
+ with store._get_conn() as c:
37
+ c.execute(
38
+ """INSERT OR REPLACE INTO conversations (
39
+ conversation_id, tenant_id, lifecycle_epoch, phase,
40
+ pending_raw_payload_entries, last_raw_payload_entries,
41
+ last_ingestible_payload_entries, created_at, updated_at, deleted_at
42
+ ) VALUES (?, ?, 1, ?, 0, 0, 0, ?, ?, ?)""",
43
+ (conv_id, tenant, phase, created, updated, del_at),
44
+ )
45
+
46
+
47
+ def _add_canonical_turns(store, conv_id: str, count: int, *, last_seen_s_ago: float = 0):
48
+ now = datetime.now(timezone.utc)
49
+ latest = now - timedelta(seconds=last_seen_s_ago)
50
+ with store._get_conn() as c:
51
+ for i in range(count):
52
+ seen = (latest - timedelta(seconds=i)).isoformat()
53
+ c.execute(
54
+ """INSERT INTO canonical_turns (
55
+ canonical_turn_id, conversation_id, sort_key, turn_hash,
56
+ hash_version, user_content, assistant_content,
57
+ primary_tag, tags_json, session_date, sender,
58
+ first_seen_at, last_seen_at, created_at, updated_at,
59
+ turn_group_number, covered_ingestible_entries, tagged_at
60
+ ) VALUES (?, ?, ?, ?, 1, 'u', 'a', 'p', '["p"]',
61
+ '2026-04-21', 'user', ?, ?, ?, ?, 0, 1, ?)""",
62
+ (f"{conv_id}-{i}", conv_id, float(i * 1000),
63
+ f"h{conv_id}-{i}", seen, seen, seen, seen, seen),
64
+ )
65
+
66
+
67
+ def _seed_running_episode(store, conv_id: str):
68
+ now = datetime.now(timezone.utc).isoformat()
69
+ with store._get_conn() as c:
70
+ c.execute(
71
+ """INSERT INTO ingestion_episode (
72
+ episode_id, conversation_id, lifecycle_epoch, raw_payload_entries,
73
+ started_at, status, owner_worker_id, heartbeat_ts
74
+ ) VALUES (?, ?, 1, 1, ?, 'running', 'w', ?)""",
75
+ (str(_uuid.uuid4()), conv_id, now, now),
76
+ )
77
+
78
+
79
+ def _seed_running_compaction(store, conv_id: str):
80
+ now = datetime.now(timezone.utc).isoformat()
81
+ with store._get_conn() as c:
82
+ c.execute(
83
+ """INSERT INTO compaction_operation (
84
+ operation_id, conversation_id, lifecycle_epoch,
85
+ phase_index, phase_count, phase_name, status,
86
+ started_at, heartbeat_ts, owner_worker_id, created_at
87
+ ) VALUES (?, ?, 1, 0, 7, 'segment_grouping', 'running',
88
+ ?, ?, 'w', ?)""",
89
+ (str(_uuid.uuid4()), conv_id, now, now, now),
90
+ )
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Basic selection
95
+ # ---------------------------------------------------------------------------
96
+
97
+ def test_idle_conv_with_few_turns_returned(tmp_path):
98
+ store = _make_store(tmp_path)
99
+ _seed_conv(store, "conv-idle", created_s_ago=7200, updated_s_ago=7200)
100
+ _add_canonical_turns(store, "conv-idle", count=2, last_seen_s_ago=7200)
101
+
102
+ rows = store.find_idle_deletable_conversations(max_msgs=5, min_age_s=3600)
103
+ ids = [r["conversation_id"] for r in rows]
104
+ assert "conv-idle" in ids
105
+ row = next(r for r in rows if r["conversation_id"] == "conv-idle")
106
+ assert row["msg_count"] == 2
107
+ assert row["age_s"] >= 7199
108
+ assert row["tenant_id"] == "tnt"
109
+
110
+
111
+ def test_fresh_conv_below_threshold_excluded(tmp_path):
112
+ """Age < min_age_s means "still fresh, may grow" → don't delete."""
113
+ store = _make_store(tmp_path)
114
+ _seed_conv(store, "conv-fresh", created_s_ago=30, updated_s_ago=30)
115
+ _add_canonical_turns(store, "conv-fresh", count=2, last_seen_s_ago=30)
116
+
117
+ rows = store.find_idle_deletable_conversations(max_msgs=5, min_age_s=3600)
118
+ assert "conv-fresh" not in [r["conversation_id"] for r in rows]
119
+
120
+
121
+ def test_conv_at_or_above_threshold_excluded(tmp_path):
122
+ """msg_count >= max_msgs → not trivial, keep."""
123
+ store = _make_store(tmp_path)
124
+ _seed_conv(store, "conv-big", created_s_ago=7200, updated_s_ago=7200)
125
+ _add_canonical_turns(store, "conv-big", count=5, last_seen_s_ago=7200)
126
+
127
+ rows = store.find_idle_deletable_conversations(max_msgs=5, min_age_s=3600)
128
+ assert "conv-big" not in [r["conversation_id"] for r in rows]
129
+
130
+
131
+ # ---------------------------------------------------------------------------
132
+ # Safety guards
133
+ # ---------------------------------------------------------------------------
134
+
135
+ def test_deleted_conv_excluded(tmp_path):
136
+ store = _make_store(tmp_path)
137
+ _seed_conv(store, "conv-dead", phase="deleted", deleted=True,
138
+ created_s_ago=7200, updated_s_ago=7200)
139
+ _add_canonical_turns(store, "conv-dead", count=1, last_seen_s_ago=7200)
140
+
141
+ rows = store.find_idle_deletable_conversations(max_msgs=5, min_age_s=3600)
142
+ assert "conv-dead" not in [r["conversation_id"] for r in rows]
143
+
144
+
145
+ def test_compacting_conv_excluded(tmp_path):
146
+ store = _make_store(tmp_path)
147
+ _seed_conv(store, "conv-comp", phase="compacting",
148
+ created_s_ago=7200, updated_s_ago=7200)
149
+ _add_canonical_turns(store, "conv-comp", count=2, last_seen_s_ago=7200)
150
+
151
+ rows = store.find_idle_deletable_conversations(max_msgs=5, min_age_s=3600)
152
+ assert "conv-comp" not in [r["conversation_id"] for r in rows]
153
+
154
+
155
+ def test_conv_with_running_episode_excluded(tmp_path):
156
+ """Even if idle + tiny, if there's a running ingestion_episode row,
157
+ the takeover pass owns it. Don't race."""
158
+ store = _make_store(tmp_path)
159
+ _seed_conv(store, "conv-lease", created_s_ago=7200, updated_s_ago=7200)
160
+ _add_canonical_turns(store, "conv-lease", count=2, last_seen_s_ago=7200)
161
+ _seed_running_episode(store, "conv-lease")
162
+
163
+ rows = store.find_idle_deletable_conversations(max_msgs=5, min_age_s=3600)
164
+ assert "conv-lease" not in [r["conversation_id"] for r in rows]
165
+
166
+
167
+ def test_conv_with_running_compaction_excluded(tmp_path):
168
+ store = _make_store(tmp_path)
169
+ _seed_conv(store, "conv-compop", created_s_ago=7200, updated_s_ago=7200)
170
+ _add_canonical_turns(store, "conv-compop", count=2, last_seen_s_ago=7200)
171
+ _seed_running_compaction(store, "conv-compop")
172
+
173
+ rows = store.find_idle_deletable_conversations(max_msgs=5, min_age_s=3600)
174
+ assert "conv-compop" not in [r["conversation_id"] for r in rows]
175
+
176
+
177
+ # ---------------------------------------------------------------------------
178
+ # Ordering + limit
179
+ # ---------------------------------------------------------------------------
180
+
181
+ def test_returns_oldest_first(tmp_path):
182
+ store = _make_store(tmp_path)
183
+ # Three idle convs with different ages
184
+ for i, age in enumerate([3700, 10000, 5000]):
185
+ cid = f"c{i}"
186
+ _seed_conv(store, cid, created_s_ago=age, updated_s_ago=age)
187
+ _add_canonical_turns(store, cid, count=2, last_seen_s_ago=age)
188
+
189
+ rows = store.find_idle_deletable_conversations(max_msgs=5, min_age_s=3600)
190
+ ages = [r["age_s"] for r in rows]
191
+ assert ages == sorted(ages, reverse=True), (
192
+ f"expected oldest-first; got {ages}"
193
+ )
194
+
195
+
196
+ def test_limit_bounds_results(tmp_path):
197
+ store = _make_store(tmp_path)
198
+ for i in range(5):
199
+ cid = f"c{i}"
200
+ _seed_conv(store, cid, created_s_ago=7200, updated_s_ago=7200)
201
+ _add_canonical_turns(store, cid, count=2, last_seen_s_ago=7200)
202
+
203
+ rows = store.find_idle_deletable_conversations(max_msgs=5, min_age_s=3600, limit=2)
204
+ assert len(rows) == 2
205
+
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Empty conv case
209
+ # ---------------------------------------------------------------------------
210
+
211
+ def test_empty_conv_with_old_created_at_returned(tmp_path):
212
+ """A conv with zero canonical_turns but created > min_age_s ago —
213
+ should fall out as deletable (msg_count=0, falls back to created_at
214
+ for last_activity_at)."""
215
+ store = _make_store(tmp_path)
216
+ _seed_conv(store, "conv-empty", created_s_ago=7200, updated_s_ago=7200)
217
+ # No canonical_turns added at all
218
+
219
+ rows = store.find_idle_deletable_conversations(max_msgs=5, min_age_s=3600)
220
+ ids = [r["conversation_id"] for r in rows]
221
+ assert "conv-empty" in ids
222
+ row = next(r for r in rows if r["conversation_id"] == "conv-empty")
223
+ assert row["msg_count"] == 0