virtual-context 0.4__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 (327) hide show
  1. {virtual_context-0.4 → virtual_context-0.4.3}/.gitignore +11 -0
  2. {virtual_context-0.4 → virtual_context-0.4.3}/PKG-INFO +3 -1
  3. {virtual_context-0.4 → virtual_context-0.4.3}/pyproject.toml +12 -2
  4. {virtual_context-0.4 → 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 → virtual_context-0.4.3}/tests/test_postgres_store.py +56 -26
  9. {virtual_context-0.4 → 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 → virtual_context-0.4.3}/virtual_context/__init__.py +1 -1
  19. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/cli/main.py +3 -3
  20. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/config.py +5 -0
  21. {virtual_context-0.4 → 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 → virtual_context-0.4.3}/virtual_context/engine.py +155 -12
  24. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/handlers.py +266 -15
  25. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/server.py +21 -11
  26. {virtual_context-0.4 → 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 → virtual_context-0.4.3}/virtual_context/storage/sqlite.py +1041 -54
  30. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/types.py +109 -5
  31. virtual_context-0.4/.github/workflows/python-publish.yml +0 -37
  32. virtual_context-0.4/CLAUDE.md +0 -19
  33. virtual_context-0.4/specs/cache-aware-compaction.md +0 -573
  34. virtual_context-0.4/tests/test_vcattach.py +0 -413
  35. virtual_context-0.4/tests_by_section.sh +0 -248
  36. virtual_context-0.4/tests_timing_report.md +0 -72
  37. virtual_context-0.4/virtual-context.yaml +0 -105
  38. virtual_context-0.4/virtual_context/proxy/vcattach.py +0 -108
  39. virtual_context-0.4/virtual_context/storage/postgres.py +0 -5302
  40. {virtual_context-0.4 → virtual_context-0.4.3}/LICENSE +0 -0
  41. {virtual_context-0.4 → virtual_context-0.4.3}/README.md +0 -0
  42. {virtual_context-0.4 → virtual_context-0.4.3}/assets/dashboard.png +0 -0
  43. {virtual_context-0.4 → virtual_context-0.4.3}/assets/hero.png +0 -0
  44. {virtual_context-0.4 → virtual_context-0.4.3}/models.yaml +0 -0
  45. {virtual_context-0.4 → virtual_context-0.4.3}/scripts/install.ps1 +0 -0
  46. {virtual_context-0.4 → virtual_context-0.4.3}/scripts/install.sh +0 -0
  47. {virtual_context-0.4 → virtual_context-0.4.3}/tests/REGRESSION_MAP.md +0 -0
  48. {virtual_context-0.4 → virtual_context-0.4.3}/tests/conftest.py +0 -0
  49. {virtual_context-0.4 → virtual_context-0.4.3}/tests/docker-compose.test.yml +0 -0
  50. {virtual_context-0.4 → virtual_context-0.4.3}/tests/haiku/__init__.py +0 -0
  51. {virtual_context-0.4 → virtual_context-0.4.3}/tests/haiku/conftest.py +0 -0
  52. {virtual_context-0.4 → virtual_context-0.4.3}/tests/haiku/test_compaction.py +0 -0
  53. {virtual_context-0.4 → virtual_context-0.4.3}/tests/haiku/test_retrieval.py +0 -0
  54. {virtual_context-0.4 → virtual_context-0.4.3}/tests/haiku/test_tagging.py +0 -0
  55. {virtual_context-0.4 → virtual_context-0.4.3}/tests/ollama/__init__.py +0 -0
  56. {virtual_context-0.4 → virtual_context-0.4.3}/tests/ollama/conftest.py +0 -0
  57. {virtual_context-0.4 → virtual_context-0.4.3}/tests/ollama/test_compactor.py +0 -0
  58. {virtual_context-0.4 → virtual_context-0.4.3}/tests/ollama/test_pipeline.py +0 -0
  59. {virtual_context-0.4 → virtual_context-0.4.3}/tests/ollama/test_provider.py +0 -0
  60. {virtual_context-0.4 → virtual_context-0.4.3}/tests/ollama/test_tag_generator.py +0 -0
  61. {virtual_context-0.4 → virtual_context-0.4.3}/tests/proxy/__init__.py +0 -0
  62. {virtual_context-0.4 → virtual_context-0.4.3}/tests/proxy/test_dashboard_cors.py +0 -0
  63. {virtual_context-0.4 → virtual_context-0.4.3}/tests/proxy/test_metrics.py +0 -0
  64. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_active_compaction_op_invariants.py +0 -0
  65. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_assembler.py +0 -0
  66. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_backend_integration.py +0 -0
  67. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_beam_judge.py +0 -0
  68. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_budget_enforcement.py +0 -0
  69. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_canonical_row_epoch_guards.py +0 -0
  70. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_canonical_row_epoch_guards_postgres.py +0 -0
  71. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_canonical_turn_grouping.py +0 -0
  72. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_canonical_turns_schema.py +0 -0
  73. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_canonical_turns_schema_postgres.py +0 -0
  74. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_claim_compaction_lease_signature.py +0 -0
  75. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_claim_compaction_lease_signature_postgres.py +0 -0
  76. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_cleanup_abandoned_compaction.py +0 -0
  77. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_cleanup_abandoned_compaction_postgres.py +0 -0
  78. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_cli_init.py +0 -0
  79. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compacting_phase_takeover.py +0 -0
  80. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compaction_commit_prune.py +0 -0
  81. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compaction_heartbeat_sidecar.py +0 -0
  82. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compaction_lease_lost_abort.py +0 -0
  83. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compaction_lifecycle.py +0 -0
  84. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compaction_lifecycle_contract.py +0 -0
  85. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compaction_lifecycle_wiring.py +0 -0
  86. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compaction_operation.py +0 -0
  87. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compaction_operation_crud.py +0 -0
  88. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compaction_operation_crud_postgres.py +0 -0
  89. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compaction_operation_id_uuid.py +0 -0
  90. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compaction_operation_postgres.py +0 -0
  91. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compaction_ownership_guards.py +0 -0
  92. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compaction_progress_event.py +0 -0
  93. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compaction_resume_schema_migration.py +0 -0
  94. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compaction_resume_schema_migration_postgres.py +0 -0
  95. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compactor.py +0 -0
  96. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_compactor_concurrent.py +0 -0
  97. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_composite_store.py +0 -0
  98. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_config.py +0 -0
  99. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_context_bleed.py +0 -0
  100. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_conversation_coverage_report.py +0 -0
  101. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_conversation_identity.py +0 -0
  102. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_conversation_lifecycle.py +0 -0
  103. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_conversation_schema.py +0 -0
  104. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_conversation_schema_postgres.py +0 -0
  105. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_conversation_scoping.py +0 -0
  106. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_deferred_payload_compaction.py +0 -0
  107. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_delete_resurrect_epoch_races.py +0 -0
  108. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_embedding_tag_generator.py +0 -0
  109. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_empty_turn_skip.py +0 -0
  110. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_engine_event_bus.py +0 -0
  111. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_engine_integration.py +0 -0
  112. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_engine_lifecycle_epoch.py +0 -0
  113. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_engine_lookback.py +0 -0
  114. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_engine_state.py +0 -0
  115. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_engine_sync_turns.py +0 -0
  116. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_fact_enrichment.py +0 -0
  117. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_fact_graph_integration.py +0 -0
  118. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_fact_link_checker.py +0 -0
  119. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_fact_link_query.py +0 -0
  120. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_fact_link_types.py +0 -0
  121. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_fact_links_sqlite.py +0 -0
  122. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_fact_redesign.py +0 -0
  123. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_filesystem_store_compaction_compat.py +0 -0
  124. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_fill_pass.py +0 -0
  125. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_find_quote.py +0 -0
  126. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_find_stale_lease_helpers.py +0 -0
  127. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_format_agnostic.py +0 -0
  128. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_handle_prepare_payload.py +0 -0
  129. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_headless.py +0 -0
  130. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_history_filter.py +0 -0
  131. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_idf_retrieval.py +0 -0
  132. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_ingest_index_integrity.py +0 -0
  133. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_ingest_reconciler_append_fastpath.py +0 -0
  134. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_ingest_reconciler_epoch.py +0 -0
  135. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_ingestion_compaction_full_cycle.py +0 -0
  136. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_ingestion_db_sweep_catchup.py +0 -0
  137. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_ingestion_episode.py +0 -0
  138. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_ingestion_episode_crud.py +0 -0
  139. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_ingestion_episode_crud_postgres.py +0 -0
  140. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_ingestion_episode_postgres.py +0 -0
  141. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_ingestion_finalize_self_heal.py +0 -0
  142. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_ingestion_progress_event.py +0 -0
  143. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_ingestion_regression_resume.py +0 -0
  144. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_legacy_ingestion_lease_ownership.py +0 -0
  145. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_lifecycle_epoch.py +0 -0
  146. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_lifecycle_epoch_store.py +0 -0
  147. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_lifecycle_epoch_store_postgres.py +0 -0
  148. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_longmemeval_auth.py +0 -0
  149. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_mark_canonical_turns_compacted_all_halves.py +0 -0
  150. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_mcp_server.py +0 -0
  151. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_media.py +0 -0
  152. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_message_filter.py +0 -0
  153. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_metrics_persistence.py +0 -0
  154. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_model_catalog.py +0 -0
  155. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_model_limits.py +0 -0
  156. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_monitor.py +0 -0
  157. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_multi_instance.py +0 -0
  158. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_multi_worker_sliding_window.py +0 -0
  159. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_neo4j_falkordb_compaction_compat.py +0 -0
  160. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_noop_fact_link_store.py +0 -0
  161. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_openrouter_provider.py +0 -0
  162. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_orphan_compacting_repair.py +0 -0
  163. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_paging.py +0 -0
  164. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_passthrough_filter.py +0 -0
  165. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_phase_transition_events.py +0 -0
  166. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_presets.py +0 -0
  167. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_prev_context_leak.py +0 -0
  168. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_progress_event_bus.py +0 -0
  169. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_progress_events.py +0 -0
  170. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_progress_snapshot.py +0 -0
  171. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_progress_snapshot_postgres.py +0 -0
  172. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_provider_adapters.py +0 -0
  173. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_proxy.py +0 -0
  174. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_proxy_dashboard.py +0 -0
  175. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_proxy_formats.py +0 -0
  176. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_proxy_message_filter.py +0 -0
  177. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_proxy_streaming.py +0 -0
  178. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_raw_content.py +0 -0
  179. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_recall_all.py +0 -0
  180. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_refresh_compaction_heartbeat.py +0 -0
  181. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_registry_lifecycle.py +0 -0
  182. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_request_captures_persistence.py +0 -0
  183. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_request_metadata.py +0 -0
  184. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_request_metadata_postgres.py +0 -0
  185. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_retriever.py +0 -0
  186. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_rrf_scoring.py +0 -0
  187. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_segmenter.py +0 -0
  188. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_semantic_search.py +0 -0
  189. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_sender_identity.py +0 -0
  190. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_session_cache.py +0 -0
  191. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_session_date.py +0 -0
  192. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_session_state.py +0 -0
  193. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_sqlite_schema_add_column_race.py +0 -0
  194. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_storage_protocols.py +0 -0
  195. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_store_recovery.py +0 -0
  196. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_store_sqlite.py +0 -0
  197. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_stub_turn_handling.py +0 -0
  198. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_supersession.py +0 -0
  199. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_supersession_migration.py +0 -0
  200. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_tag_canonicalizer.py +0 -0
  201. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_tag_consolidator.py +0 -0
  202. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_tag_generator.py +0 -0
  203. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_tag_splitter.py +0 -0
  204. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_tagger_loop.py +0 -0
  205. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_telemetry.py +0 -0
  206. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_telemetry_integration.py +0 -0
  207. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_temporal_resolver.py +0 -0
  208. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_tool_loop.py +0 -0
  209. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_tool_output_interceptor.py +0 -0
  210. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_tool_result_filter.py +0 -0
  211. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_tool_tags.py +0 -0
  212. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_tui.py +0 -0
  213. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_turn_grouping.py +0 -0
  214. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_turn_tag_index.py +0 -0
  215. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_unified_budget.py +0 -0
  216. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_upstream_trim.py +0 -0
  217. {virtual_context-0.4 → virtual_context-0.4.3}/tests/test_verb_expansion.py +0 -0
  218. {virtual_context-0.4 → virtual_context-0.4.3}/virtual-context.yaml.example +0 -0
  219. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/cli/__init__.py +0 -0
  220. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/conversation_identity.py +0 -0
  221. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/__init__.py +0 -0
  222. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/assembler.py +0 -0
  223. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/canonical_turns.py +0 -0
  224. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/compaction_pipeline.py +0 -0
  225. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/compactor.py +0 -0
  226. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/conversation_store.py +0 -0
  227. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/coverage_report.py +0 -0
  228. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/embedding_provider.py +0 -0
  229. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/embedding_tag_generator.py +0 -0
  230. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/engine_utils.py +0 -0
  231. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/event_bus.py +0 -0
  232. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/fact_query.py +0 -0
  233. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/fts_preprocessor.py +0 -0
  234. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/hint_builder.py +0 -0
  235. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/ingest_reconciler.py +0 -0
  236. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/lifecycle_epoch.py +0 -0
  237. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/llm_utils.py +0 -0
  238. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/math_utils.py +0 -0
  239. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/model_catalog.py +0 -0
  240. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/monitor.py +0 -0
  241. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/paging_manager.py +0 -0
  242. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/progress_events.py +0 -0
  243. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/progress_snapshot.py +0 -0
  244. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/protocols.py +0 -0
  245. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/provider_adapters.py +0 -0
  246. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/quote_search.py +0 -0
  247. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/retrieval_assembler.py +0 -0
  248. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/retrieval_scoring.py +0 -0
  249. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/retriever.py +0 -0
  250. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/search_engine.py +0 -0
  251. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/segmenter.py +0 -0
  252. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/semantic_search.py +0 -0
  253. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/store.py +0 -0
  254. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/tag_canonicalizer.py +0 -0
  255. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/tag_consolidator.py +0 -0
  256. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/tag_generator.py +0 -0
  257. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/tag_scoring.py +0 -0
  258. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/tag_splitter.py +0 -0
  259. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/tagging_pipeline.py +0 -0
  260. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/telemetry.py +0 -0
  261. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/temporal_resolver.py +0 -0
  262. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/tool_loop.py +0 -0
  263. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/tool_query.py +0 -0
  264. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/core/turn_tag_index.py +0 -0
  265. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/data/anthropic-tokenizer/tokenizer.json +0 -0
  266. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/data/models.yaml +0 -0
  267. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/ingest/__init__.py +0 -0
  268. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/ingest/curator.py +0 -0
  269. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/ingest/date_resolver.py +0 -0
  270. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/ingest/parsers.py +0 -0
  271. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/ingest/supersession.py +0 -0
  272. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/mcp/__init__.py +0 -0
  273. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/mcp/server.py +0 -0
  274. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/model_limits.py +0 -0
  275. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/openclaw/virtual-context.mjs +0 -0
  276. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/patterns.py +0 -0
  277. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/presets/__init__.py +0 -0
  278. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/presets/agentic.py +0 -0
  279. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/presets/base.py +0 -0
  280. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/presets/coding.py +0 -0
  281. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/providers/__init__.py +0 -0
  282. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/providers/anthropic.py +0 -0
  283. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/providers/base.py +0 -0
  284. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/providers/generic_openai.py +0 -0
  285. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/providers/ollama_native.py +0 -0
  286. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/__init__.py +0 -0
  287. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/_envelope.py +0 -0
  288. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/dashboard.html +0 -0
  289. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/dashboard.py +0 -0
  290. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/formats.py +0 -0
  291. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/helpers.py +0 -0
  292. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/media.py +0 -0
  293. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/message_filter.py +0 -0
  294. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/metrics.py +0 -0
  295. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/multi.py +0 -0
  296. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/registry.py +0 -0
  297. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/session_cache.py +0 -0
  298. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/session_state.py +0 -0
  299. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/static/android-chrome-192x192.png +0 -0
  300. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/static/android-chrome-512x512.png +0 -0
  301. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/static/apple-touch-icon.png +0 -0
  302. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/static/favicon-16x16.png +0 -0
  303. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/static/favicon-32x32.png +0 -0
  304. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/static/favicon.ico +0 -0
  305. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/static/site.webmanifest +0 -0
  306. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/proxy/tool_output_interceptor.py +0 -0
  307. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/storage/__init__.py +0 -0
  308. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/storage/falkordb.py +0 -0
  309. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/storage/filesystem.py +0 -0
  310. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/storage/helpers.py +0 -0
  311. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/storage/neo4j.py +0 -0
  312. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/storage/noop_fact_link_store.py +0 -0
  313. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/token_counter.py +0 -0
  314. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/tui/__init__.py +0 -0
  315. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/tui/app.py +0 -0
  316. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/tui/chat.tcss +0 -0
  317. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/tui/chat_provider.py +0 -0
  318. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/tui/headless.py +0 -0
  319. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/tui/modals/__init__.py +0 -0
  320. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/tui/modals/turn_inspector.py +0 -0
  321. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/tui/state.py +0 -0
  322. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/tui/widgets/__init__.py +0 -0
  323. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/tui/widgets/budget_bar.py +0 -0
  324. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/tui/widgets/chat_view.py +0 -0
  325. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/tui/widgets/input_box.py +0 -0
  326. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/tui/widgets/tag_panel.py +0 -0
  327. {virtual_context-0.4 → virtual_context-0.4.3}/virtual_context/tui/widgets/turn_list.py +0 -0
@@ -8,6 +8,9 @@ build/
8
8
  # Virtual environment
9
9
  .venv/
10
10
 
11
+ # Infrastructure configs (contains secrets — never commit)
12
+ infrastructure/
13
+
11
14
  # Testing
12
15
  .pytest_cache/
13
16
 
@@ -27,15 +30,22 @@ scripts/*
27
30
  !scripts/install.ps1
28
31
  !scripts/install.sh
29
32
 
33
+ # Environment files (API keys)
34
+ .env
35
+
30
36
  # Config files (may contain API keys)
31
37
  virtual-context-*.yaml
32
38
  !virtual-context.yaml.example
33
39
 
34
40
  # Internal docs
35
41
  CHANGELOG.md
42
+ CLAUDE.md
36
43
  docs/superpowers/
37
44
  docs/competitive-analysis.md
38
45
  diagnostics/
46
+ specs/
47
+ tests_timing_report.md
48
+ tests_by_section.sh
39
49
  uv.lock
40
50
  extraneous/paper/virtual_context_paper_latest.md
41
51
  extraneous/paper/data/
@@ -54,3 +64,4 @@ extraneous/paper/*.png
54
64
 
55
65
  # OS
56
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
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"
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 = [
@@ -67,6 +68,15 @@ exclude = [
67
68
  ".virtualcontext/",
68
69
  "*.db",
69
70
  "*.log",
71
+ # Internal / private — never ship in sdist
72
+ "CLAUDE.md",
73
+ "tests_timing_report.md",
74
+ "tests_by_section.sh",
75
+ "specs/",
76
+ "virtual-context.yaml",
77
+ ".github/",
78
+ ".gitignore",
79
+ ".worktrees/",
70
80
  ]
71
81
  artifacts = [
72
82
  "virtual_context/proxy/static/*",
@@ -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
+ )