virtual-context 0.4.2__tar.gz → 0.4.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. {virtual_context-0.4.2 → virtual_context-0.4.4}/.gitignore +4 -0
  2. {virtual_context-0.4.2 → virtual_context-0.4.4}/PKG-INFO +3 -1
  3. {virtual_context-0.4.2 → virtual_context-0.4.4}/pyproject.toml +3 -2
  4. virtual_context-0.4.4/tests/test_admin_backfill_cli.py +171 -0
  5. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_resume_e2e_postgres.py +5 -5
  6. virtual_context-0.4.4/tests/test_conversations_tenant_id_backfill_postgres.py +180 -0
  7. virtual_context-0.4.4/tests/test_conversations_tenant_id_propagation.py +186 -0
  8. virtual_context-0.4.4/tests/test_engine_alias_resolution.py +2232 -0
  9. virtual_context-0.4.4/tests/test_find_idle_deletable_conversations.py +223 -0
  10. virtual_context-0.4.4/tests/test_import_adapters.py +236 -0
  11. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_postgres_store.py +56 -26
  12. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_proxy.py +3 -0
  13. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_proxy_session.py +98 -9
  14. virtual_context-0.4.4/tests/test_tag_summary_backfill.py +637 -0
  15. virtual_context-0.4.4/tests/test_vcattach.py +1321 -0
  16. virtual_context-0.4.4/tests/test_vcmerge_body_method.py +1795 -0
  17. virtual_context-0.4.4/tests/test_vcmerge_body_method_postgres.py +524 -0
  18. virtual_context-0.4.4/tests/test_vcmerge_composite_forwarders.py +433 -0
  19. virtual_context-0.4.4/tests/test_vcmerge_dual_handler_refuse.py +363 -0
  20. virtual_context-0.4.4/tests/test_vcmerge_schema.py +397 -0
  21. virtual_context-0.4.4/tests/test_vcmerge_schema_postgres.py +163 -0
  22. virtual_context-0.4.4/tests/test_vcmerge_storage.py +379 -0
  23. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/__init__.py +1 -1
  24. virtual_context-0.4.4/virtual_context/cli/import_cmd.py +75 -0
  25. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/cli/main.py +201 -3
  26. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/config.py +5 -0
  27. virtual_context-0.4.4/virtual_context/core/alias_resolution.py +162 -0
  28. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/compaction_pipeline.py +186 -103
  29. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/composite_store.py +224 -8
  30. virtual_context-0.4.4/virtual_context/core/exceptions.py +180 -0
  31. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/protocols.py +15 -2
  32. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/store.py +37 -0
  33. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/tagging_pipeline.py +2 -0
  34. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/engine.py +673 -16
  35. virtual_context-0.4.4/virtual_context/import_adapters/__init__.py +32 -0
  36. virtual_context-0.4.4/virtual_context/import_adapters/base.py +48 -0
  37. virtual_context-0.4.4/virtual_context/import_adapters/chatgpt.py +63 -0
  38. virtual_context-0.4.4/virtual_context/import_adapters/claude.py +47 -0
  39. virtual_context-0.4.4/virtual_context/import_adapters/grok.py +61 -0
  40. virtual_context-0.4.4/virtual_context/import_adapters/loader.py +65 -0
  41. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/handlers.py +395 -25
  42. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/server.py +125 -27
  43. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/state.py +47 -26
  44. virtual_context-0.4.4/virtual_context/proxy/vcattach.py +203 -0
  45. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/storage/filesystem.py +13 -0
  46. virtual_context-0.4.4/virtual_context/storage/postgres.py +6864 -0
  47. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/storage/sqlite.py +1354 -60
  48. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/types.py +179 -6
  49. virtual_context-0.4.2/tests/test_vcattach.py +0 -413
  50. virtual_context-0.4.2/virtual_context/proxy/vcattach.py +0 -108
  51. virtual_context-0.4.2/virtual_context/storage/postgres.py +0 -5302
  52. {virtual_context-0.4.2 → virtual_context-0.4.4}/LICENSE +0 -0
  53. {virtual_context-0.4.2 → virtual_context-0.4.4}/README.md +0 -0
  54. {virtual_context-0.4.2 → virtual_context-0.4.4}/assets/dashboard.png +0 -0
  55. {virtual_context-0.4.2 → virtual_context-0.4.4}/assets/hero.png +0 -0
  56. {virtual_context-0.4.2 → virtual_context-0.4.4}/models.yaml +0 -0
  57. {virtual_context-0.4.2 → virtual_context-0.4.4}/scripts/install.ps1 +0 -0
  58. {virtual_context-0.4.2 → virtual_context-0.4.4}/scripts/install.sh +0 -0
  59. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/REGRESSION_MAP.md +0 -0
  60. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/conftest.py +0 -0
  61. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/docker-compose.test.yml +0 -0
  62. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/haiku/__init__.py +0 -0
  63. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/haiku/conftest.py +0 -0
  64. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/haiku/test_compaction.py +0 -0
  65. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/haiku/test_retrieval.py +0 -0
  66. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/haiku/test_tagging.py +0 -0
  67. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/ollama/__init__.py +0 -0
  68. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/ollama/conftest.py +0 -0
  69. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/ollama/test_compactor.py +0 -0
  70. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/ollama/test_pipeline.py +0 -0
  71. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/ollama/test_provider.py +0 -0
  72. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/ollama/test_tag_generator.py +0 -0
  73. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/proxy/__init__.py +0 -0
  74. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/proxy/test_dashboard_cors.py +0 -0
  75. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/proxy/test_metrics.py +0 -0
  76. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_active_compaction_op_invariants.py +0 -0
  77. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_assembler.py +0 -0
  78. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_backend_integration.py +0 -0
  79. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_beam_judge.py +0 -0
  80. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_budget_enforcement.py +0 -0
  81. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_canonical_row_epoch_guards.py +0 -0
  82. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_canonical_row_epoch_guards_postgres.py +0 -0
  83. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_canonical_turn_grouping.py +0 -0
  84. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_canonical_turns_schema.py +0 -0
  85. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_canonical_turns_schema_postgres.py +0 -0
  86. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_claim_compaction_lease_signature.py +0 -0
  87. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_claim_compaction_lease_signature_postgres.py +0 -0
  88. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_cleanup_abandoned_compaction.py +0 -0
  89. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_cleanup_abandoned_compaction_postgres.py +0 -0
  90. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_cli_init.py +0 -0
  91. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compacting_phase_takeover.py +0 -0
  92. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_commit_prune.py +0 -0
  93. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_heartbeat_sidecar.py +0 -0
  94. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_lease_lost_abort.py +0 -0
  95. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_lifecycle.py +0 -0
  96. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_lifecycle_contract.py +0 -0
  97. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_lifecycle_wiring.py +0 -0
  98. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_operation.py +0 -0
  99. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_operation_crud.py +0 -0
  100. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_operation_crud_postgres.py +0 -0
  101. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_operation_id_uuid.py +0 -0
  102. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_operation_postgres.py +0 -0
  103. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_ownership_guards.py +0 -0
  104. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_progress_event.py +0 -0
  105. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_resume_schema_migration.py +0 -0
  106. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compaction_resume_schema_migration_postgres.py +0 -0
  107. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compactor.py +0 -0
  108. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_compactor_concurrent.py +0 -0
  109. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_composite_store.py +0 -0
  110. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_config.py +0 -0
  111. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_context_bleed.py +0 -0
  112. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_conversation_coverage_report.py +0 -0
  113. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_conversation_identity.py +0 -0
  114. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_conversation_lifecycle.py +0 -0
  115. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_conversation_schema.py +0 -0
  116. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_conversation_schema_postgres.py +0 -0
  117. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_conversation_scoping.py +0 -0
  118. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_deferred_payload_compaction.py +0 -0
  119. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_delete_resurrect_epoch_races.py +0 -0
  120. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_embedding_tag_generator.py +0 -0
  121. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_empty_turn_skip.py +0 -0
  122. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_engine_event_bus.py +0 -0
  123. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_engine_integration.py +0 -0
  124. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_engine_lifecycle_epoch.py +0 -0
  125. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_engine_lookback.py +0 -0
  126. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_engine_state.py +0 -0
  127. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_engine_sync_turns.py +0 -0
  128. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_fact_enrichment.py +0 -0
  129. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_fact_graph_integration.py +0 -0
  130. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_fact_link_checker.py +0 -0
  131. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_fact_link_query.py +0 -0
  132. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_fact_link_types.py +0 -0
  133. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_fact_links_sqlite.py +0 -0
  134. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_fact_redesign.py +0 -0
  135. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_filesystem_store_compaction_compat.py +0 -0
  136. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_fill_pass.py +0 -0
  137. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_find_quote.py +0 -0
  138. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_find_stale_lease_helpers.py +0 -0
  139. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_format_agnostic.py +0 -0
  140. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_handle_prepare_payload.py +0 -0
  141. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_headless.py +0 -0
  142. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_history_filter.py +0 -0
  143. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_idf_retrieval.py +0 -0
  144. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingest_index_integrity.py +0 -0
  145. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingest_reconciler_append_fastpath.py +0 -0
  146. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingest_reconciler_epoch.py +0 -0
  147. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_compaction_full_cycle.py +0 -0
  148. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_db_sweep_catchup.py +0 -0
  149. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_episode.py +0 -0
  150. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_episode_crud.py +0 -0
  151. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_episode_crud_postgres.py +0 -0
  152. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_episode_postgres.py +0 -0
  153. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_finalize_self_heal.py +0 -0
  154. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_progress_event.py +0 -0
  155. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_ingestion_regression_resume.py +0 -0
  156. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_legacy_ingestion_lease_ownership.py +0 -0
  157. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_lifecycle_epoch.py +0 -0
  158. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_lifecycle_epoch_store.py +0 -0
  159. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_lifecycle_epoch_store_postgres.py +0 -0
  160. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_longmemeval_auth.py +0 -0
  161. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_mark_canonical_turns_compacted_all_halves.py +0 -0
  162. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_mcp_server.py +0 -0
  163. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_media.py +0 -0
  164. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_message_filter.py +0 -0
  165. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_metrics_persistence.py +0 -0
  166. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_model_catalog.py +0 -0
  167. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_model_limits.py +0 -0
  168. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_monitor.py +0 -0
  169. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_multi_instance.py +0 -0
  170. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_multi_worker_sliding_window.py +0 -0
  171. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_neo4j_falkordb_compaction_compat.py +0 -0
  172. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_noop_fact_link_store.py +0 -0
  173. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_openrouter_provider.py +0 -0
  174. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_orphan_compacting_repair.py +0 -0
  175. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_paging.py +0 -0
  176. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_passthrough_filter.py +0 -0
  177. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_phase_transition_events.py +0 -0
  178. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_presets.py +0 -0
  179. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_prev_context_leak.py +0 -0
  180. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_progress_event_bus.py +0 -0
  181. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_progress_events.py +0 -0
  182. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_progress_snapshot.py +0 -0
  183. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_progress_snapshot_postgres.py +0 -0
  184. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_provider_adapters.py +0 -0
  185. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_proxy_dashboard.py +0 -0
  186. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_proxy_formats.py +0 -0
  187. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_proxy_message_filter.py +0 -0
  188. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_proxy_streaming.py +0 -0
  189. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_raw_content.py +0 -0
  190. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_recall_all.py +0 -0
  191. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_refresh_compaction_heartbeat.py +0 -0
  192. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_registry_lifecycle.py +0 -0
  193. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_request_captures_persistence.py +0 -0
  194. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_request_metadata.py +0 -0
  195. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_request_metadata_postgres.py +0 -0
  196. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_retriever.py +0 -0
  197. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_rrf_scoring.py +0 -0
  198. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_segmenter.py +0 -0
  199. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_semantic_search.py +0 -0
  200. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_sender_identity.py +0 -0
  201. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_session_cache.py +0 -0
  202. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_session_date.py +0 -0
  203. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_session_state.py +0 -0
  204. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_sqlite_schema_add_column_race.py +0 -0
  205. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_storage_protocols.py +0 -0
  206. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_store_recovery.py +0 -0
  207. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_store_sqlite.py +0 -0
  208. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_stub_turn_handling.py +0 -0
  209. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_supersession.py +0 -0
  210. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_supersession_migration.py +0 -0
  211. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tag_canonicalizer.py +0 -0
  212. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tag_consolidator.py +0 -0
  213. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tag_generator.py +0 -0
  214. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tag_splitter.py +0 -0
  215. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tagger_loop.py +0 -0
  216. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_telemetry.py +0 -0
  217. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_telemetry_integration.py +0 -0
  218. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_temporal_resolver.py +0 -0
  219. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tool_loop.py +0 -0
  220. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tool_output_interceptor.py +0 -0
  221. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tool_result_filter.py +0 -0
  222. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tool_tags.py +0 -0
  223. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_tui.py +0 -0
  224. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_turn_grouping.py +0 -0
  225. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_turn_tag_index.py +0 -0
  226. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_unified_budget.py +0 -0
  227. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_upstream_trim.py +0 -0
  228. {virtual_context-0.4.2 → virtual_context-0.4.4}/tests/test_verb_expansion.py +0 -0
  229. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual-context.yaml.example +0 -0
  230. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/cli/__init__.py +0 -0
  231. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/conversation_identity.py +0 -0
  232. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/__init__.py +0 -0
  233. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/assembler.py +0 -0
  234. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/canonical_turns.py +0 -0
  235. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/compactor.py +0 -0
  236. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/conversation_store.py +0 -0
  237. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/coverage_report.py +0 -0
  238. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/embedding_provider.py +0 -0
  239. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/embedding_tag_generator.py +0 -0
  240. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/engine_utils.py +0 -0
  241. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/event_bus.py +0 -0
  242. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/fact_query.py +0 -0
  243. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/fts_preprocessor.py +0 -0
  244. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/hint_builder.py +0 -0
  245. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/ingest_reconciler.py +0 -0
  246. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/lifecycle_epoch.py +0 -0
  247. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/llm_utils.py +0 -0
  248. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/math_utils.py +0 -0
  249. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/model_catalog.py +0 -0
  250. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/monitor.py +0 -0
  251. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/paging_manager.py +0 -0
  252. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/progress_events.py +0 -0
  253. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/progress_snapshot.py +0 -0
  254. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/provider_adapters.py +0 -0
  255. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/quote_search.py +0 -0
  256. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/retrieval_assembler.py +0 -0
  257. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/retrieval_scoring.py +0 -0
  258. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/retriever.py +0 -0
  259. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/search_engine.py +0 -0
  260. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/segmenter.py +0 -0
  261. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/semantic_search.py +0 -0
  262. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/tag_canonicalizer.py +0 -0
  263. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/tag_consolidator.py +0 -0
  264. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/tag_generator.py +0 -0
  265. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/tag_scoring.py +0 -0
  266. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/tag_splitter.py +0 -0
  267. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/telemetry.py +0 -0
  268. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/temporal_resolver.py +0 -0
  269. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/tool_loop.py +0 -0
  270. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/tool_query.py +0 -0
  271. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/core/turn_tag_index.py +0 -0
  272. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/data/anthropic-tokenizer/tokenizer.json +0 -0
  273. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/data/models.yaml +0 -0
  274. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/ingest/__init__.py +0 -0
  275. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/ingest/curator.py +0 -0
  276. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/ingest/date_resolver.py +0 -0
  277. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/ingest/parsers.py +0 -0
  278. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/ingest/supersession.py +0 -0
  279. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/mcp/__init__.py +0 -0
  280. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/mcp/server.py +0 -0
  281. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/model_limits.py +0 -0
  282. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/openclaw/virtual-context.mjs +0 -0
  283. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/patterns.py +0 -0
  284. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/presets/__init__.py +0 -0
  285. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/presets/agentic.py +0 -0
  286. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/presets/base.py +0 -0
  287. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/presets/coding.py +0 -0
  288. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/providers/__init__.py +0 -0
  289. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/providers/anthropic.py +0 -0
  290. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/providers/base.py +0 -0
  291. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/providers/generic_openai.py +0 -0
  292. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/providers/ollama_native.py +0 -0
  293. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/__init__.py +0 -0
  294. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/_envelope.py +0 -0
  295. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/dashboard.html +0 -0
  296. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/dashboard.py +0 -0
  297. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/formats.py +0 -0
  298. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/helpers.py +0 -0
  299. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/media.py +0 -0
  300. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/message_filter.py +0 -0
  301. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/metrics.py +0 -0
  302. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/multi.py +0 -0
  303. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/registry.py +0 -0
  304. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/session_cache.py +0 -0
  305. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/session_state.py +0 -0
  306. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/static/android-chrome-192x192.png +0 -0
  307. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/static/android-chrome-512x512.png +0 -0
  308. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/static/apple-touch-icon.png +0 -0
  309. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/static/favicon-16x16.png +0 -0
  310. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/static/favicon-32x32.png +0 -0
  311. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/static/favicon.ico +0 -0
  312. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/static/site.webmanifest +0 -0
  313. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/proxy/tool_output_interceptor.py +0 -0
  314. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/storage/__init__.py +0 -0
  315. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/storage/falkordb.py +0 -0
  316. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/storage/helpers.py +0 -0
  317. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/storage/neo4j.py +0 -0
  318. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/storage/noop_fact_link_store.py +0 -0
  319. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/token_counter.py +0 -0
  320. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/__init__.py +0 -0
  321. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/app.py +0 -0
  322. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/chat.tcss +0 -0
  323. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/chat_provider.py +0 -0
  324. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/headless.py +0 -0
  325. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/modals/__init__.py +0 -0
  326. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/modals/turn_inspector.py +0 -0
  327. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/state.py +0 -0
  328. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/widgets/__init__.py +0 -0
  329. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/widgets/budget_bar.py +0 -0
  330. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/widgets/chat_view.py +0 -0
  331. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/widgets/input_box.py +0 -0
  332. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/widgets/tag_panel.py +0 -0
  333. {virtual_context-0.4.2 → virtual_context-0.4.4}/virtual_context/tui/widgets/turn_list.py +0 -0
@@ -64,3 +64,7 @@ extraneous/paper/*.png
64
64
 
65
65
  # OS
66
66
  .DS_Store
67
+ .agent-prompt.md
68
+
69
+ # Riptide artifacts (cloud-synced)
70
+ .humanlayer/tasks/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: virtual-context
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: OS-style virtual memory for LLM session context management
5
5
  Project-URL: Homepage, https://virtual-context.com
6
6
  Project-URL: Repository, https://github.com/virtual-context/virtual-context
@@ -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.4"
8
8
  description = "OS-style virtual memory for LLM session context management"
9
9
  readme = "README.md"
10
10
  license = "AGPL-3.0-or-later"
@@ -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 = [
@@ -0,0 +1,171 @@
1
+ """Tests for the ``admin backfill-tag-summaries`` CLI subcommand.
2
+
3
+ Covers the storage-override flags + ``DATABASE_URL`` env fallback
4
+ introduced so the subcommand runs inside containers that don't have a
5
+ config file mounted at a standard path.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+
12
+ os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE")
13
+
14
+ from types import SimpleNamespace
15
+
16
+ import pytest
17
+
18
+ from virtual_context.cli.main import _apply_storage_overrides
19
+ from virtual_context.types import StorageConfig, VirtualContextConfig
20
+
21
+
22
+ def _make_config(*, backend="sqlite", postgres_dsn="", sqlite_path=".db"):
23
+ return VirtualContextConfig(
24
+ storage=StorageConfig(
25
+ backend=backend,
26
+ postgres_dsn=postgres_dsn,
27
+ sqlite_path=sqlite_path,
28
+ ),
29
+ )
30
+
31
+
32
+ def test_storage_override_explicit_flag_wins(monkeypatch):
33
+ """Explicit ``--postgres-dsn`` overrides whatever config carried."""
34
+ monkeypatch.delenv("DATABASE_URL", raising=False)
35
+ config = _make_config(backend="sqlite", postgres_dsn="from-config")
36
+ args = SimpleNamespace(
37
+ config=None,
38
+ storage_backend="postgres",
39
+ postgres_dsn="postgresql://override/db",
40
+ sqlite_path=None,
41
+ )
42
+ _apply_storage_overrides(config, args)
43
+ assert config.storage.backend == "postgres"
44
+ assert config.storage.postgres_dsn == "postgresql://override/db"
45
+
46
+
47
+ def test_storage_override_sqlite_path_flag(monkeypatch):
48
+ """``--sqlite-path`` overrides ``storage.sqlite_path``."""
49
+ monkeypatch.delenv("DATABASE_URL", raising=False)
50
+ config = _make_config(backend="sqlite", sqlite_path=".db")
51
+ args = SimpleNamespace(
52
+ config=None,
53
+ storage_backend=None,
54
+ postgres_dsn=None,
55
+ sqlite_path="/tmp/override.db",
56
+ )
57
+ _apply_storage_overrides(config, args)
58
+ assert config.storage.sqlite_path == "/tmp/override.db"
59
+ # Backend unchanged because no --storage-backend flag.
60
+ assert config.storage.backend == "sqlite"
61
+
62
+
63
+ def test_storage_override_database_url_env_fallback(monkeypatch):
64
+ """When no flag and no ``-c`` are given, ``DATABASE_URL`` env
65
+ promotes the engine to Postgres with that DSN. Covers the
66
+ container-based invocation shape where ops just runs the
67
+ subcommand with the env already set by the container runtime."""
68
+ monkeypatch.setenv("DATABASE_URL", "postgresql://from-env/db")
69
+ config = _make_config(backend="sqlite", postgres_dsn="")
70
+ args = SimpleNamespace(
71
+ config=None,
72
+ storage_backend=None,
73
+ postgres_dsn=None,
74
+ sqlite_path=None,
75
+ )
76
+ _apply_storage_overrides(config, args)
77
+ assert config.storage.backend == "postgres"
78
+ assert config.storage.postgres_dsn == "postgresql://from-env/db"
79
+
80
+
81
+ def test_storage_override_database_url_skipped_when_config_provided(monkeypatch):
82
+ """When the caller passes ``-c``, the env fallback DOES NOT fire;
83
+ the config file is treated as authoritative."""
84
+ monkeypatch.setenv("DATABASE_URL", "postgresql://from-env/db")
85
+ config = _make_config(backend="sqlite", postgres_dsn="")
86
+ args = SimpleNamespace(
87
+ config="/some/config.yaml",
88
+ storage_backend=None,
89
+ postgres_dsn=None,
90
+ sqlite_path=None,
91
+ )
92
+ _apply_storage_overrides(config, args)
93
+ # Backend + DSN unchanged because ``-c`` was given.
94
+ assert config.storage.backend == "sqlite"
95
+ assert config.storage.postgres_dsn == ""
96
+
97
+
98
+ def test_storage_override_database_url_skipped_when_any_flag_set(monkeypatch):
99
+ """When ANY storage flag is set, the env fallback does NOT fire
100
+ (explicit flags are authoritative)."""
101
+ monkeypatch.setenv("DATABASE_URL", "postgresql://from-env/db")
102
+ config = _make_config(backend="sqlite")
103
+ args = SimpleNamespace(
104
+ config=None,
105
+ storage_backend=None,
106
+ postgres_dsn=None,
107
+ sqlite_path="/tmp/explicit.db",
108
+ )
109
+ _apply_storage_overrides(config, args)
110
+ # Only the sqlite_path flag applies; env fallback is skipped.
111
+ assert config.storage.sqlite_path == "/tmp/explicit.db"
112
+ assert config.storage.backend == "sqlite"
113
+ assert config.storage.postgres_dsn == ""
114
+
115
+
116
+ def test_storage_override_no_change_when_no_flags_and_no_env(monkeypatch):
117
+ """Defaults intact when neither flags, ``-c``, nor ``DATABASE_URL``
118
+ are present. (``-c`` absent means ``load_config()`` already returned
119
+ defaults; this assertion guards against accidental mutation.)"""
120
+ monkeypatch.delenv("DATABASE_URL", raising=False)
121
+ config = _make_config(backend="sqlite", sqlite_path=".db")
122
+ args = SimpleNamespace(
123
+ config=None,
124
+ storage_backend=None,
125
+ postgres_dsn=None,
126
+ sqlite_path=None,
127
+ )
128
+ _apply_storage_overrides(config, args)
129
+ assert config.storage.backend == "sqlite"
130
+ assert config.storage.postgres_dsn == ""
131
+ assert config.storage.sqlite_path == ".db"
132
+
133
+
134
+ def test_cli_parser_accepts_new_storage_flags():
135
+ """End-to-end parser smoke test: the ``admin backfill-tag-summaries``
136
+ subparser accepts the three new flags + ``DATABASE_URL`` precedence
137
+ metadata in the help text."""
138
+ from virtual_context.cli.main import main as _main
139
+ import argparse
140
+
141
+ # Re-build the parser the way ``main()`` does, then parse a sample
142
+ # invocation. (Doing a full ``main()`` call would actually run the
143
+ # backfill; we only need argument parsing here.)
144
+ parser = argparse.ArgumentParser(prog="virtual-context")
145
+ parser.add_argument("--config", "-c")
146
+ subparsers = parser.add_subparsers(dest="command")
147
+ admin_parser = subparsers.add_parser("admin")
148
+ admin_sub = admin_parser.add_subparsers(dest="admin_command")
149
+ backfill_parser = admin_sub.add_parser("backfill-tag-summaries")
150
+ backfill_parser.add_argument("conversation_id")
151
+ backfill_parser.add_argument("--tenant-id", default="")
152
+ backfill_parser.add_argument("--force-rebuild", action="store_true")
153
+ backfill_parser.add_argument(
154
+ "--storage-backend",
155
+ choices=("sqlite", "postgres", "filesystem"),
156
+ )
157
+ backfill_parser.add_argument("--postgres-dsn")
158
+ backfill_parser.add_argument("--sqlite-path")
159
+
160
+ args = parser.parse_args([
161
+ "admin", "backfill-tag-summaries", "conv-xyz",
162
+ "--tenant-id", "t1",
163
+ "--storage-backend", "postgres",
164
+ "--postgres-dsn", "postgresql://h/db",
165
+ ])
166
+ assert args.conversation_id == "conv-xyz"
167
+ assert args.tenant_id == "t1"
168
+ assert args.storage_backend == "postgres"
169
+ assert args.postgres_dsn == "postgresql://h/db"
170
+ assert args.sqlite_path is None
171
+ assert args.force_rebuild is False
@@ -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
+ )