topos-node 0.1.8__tar.gz → 0.1.9__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 (256) hide show
  1. {topos_node-0.1.8 → topos_node-0.1.9}/PKG-INFO +1 -1
  2. {topos_node-0.1.8 → topos_node-0.1.9}/pyproject.toml +1 -1
  3. {topos_node-0.1.8 → topos_node-0.1.9}/topos/__version__.py +2 -2
  4. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/source_install.py +30 -0
  5. {topos_node-0.1.8 → topos_node-0.1.9}/topos/app.py +9 -0
  6. {topos_node-0.1.8 → topos_node-0.1.9}/topos/cli/commands.py +30 -64
  7. {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/handlers.py +14 -0
  8. {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/logging.py +7 -2
  9. topos_node-0.1.9/topos/runtime_update.py +246 -0
  10. {topos_node-0.1.8 → topos_node-0.1.9}/topos/sources/install_service.py +95 -0
  11. {topos_node-0.1.8 → topos_node-0.1.9}/topos/sync/client.py +3 -0
  12. {topos_node-0.1.8 → topos_node-0.1.9}/topos_node.egg-info/PKG-INFO +1 -1
  13. {topos_node-0.1.8 → topos_node-0.1.9}/topos_node.egg-info/SOURCES.txt +1 -0
  14. {topos_node-0.1.8 → topos_node-0.1.9}/LICENSE +0 -0
  15. {topos_node-0.1.8 → topos_node-0.1.9}/README.md +0 -0
  16. {topos_node-0.1.8 → topos_node-0.1.9}/setup.cfg +0 -0
  17. {topos_node-0.1.8 → topos_node-0.1.9}/shared/__init__.py +0 -0
  18. {topos_node-0.1.8 → topos_node-0.1.9}/shared/filtering.py +0 -0
  19. {topos_node-0.1.8 → topos_node-0.1.9}/shared/schema_registry.py +0 -0
  20. {topos_node-0.1.8 → topos_node-0.1.9}/topos/__init__.py +0 -0
  21. {topos_node-0.1.8 → topos_node-0.1.9}/topos/analytics/__init__.py +0 -0
  22. {topos_node-0.1.8 → topos_node-0.1.9}/topos/analytics/duckdb_adapter.py +0 -0
  23. {topos_node-0.1.8 → topos_node-0.1.9}/topos/analytics/messenger_communities.py +0 -0
  24. {topos_node-0.1.8 → topos_node-0.1.9}/topos/analytics/messenger_graph.py +0 -0
  25. {topos_node-0.1.8 → topos_node-0.1.9}/topos/analytics/messenger_labels.py +0 -0
  26. {topos_node-0.1.8 → topos_node-0.1.9}/topos/analytics/profiles.py +0 -0
  27. {topos_node-0.1.8 → topos_node-0.1.9}/topos/analytics/query_engine.py +0 -0
  28. {topos_node-0.1.8 → topos_node-0.1.9}/topos/analytics/raw_queries.py +0 -0
  29. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/__init__.py +0 -0
  30. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/analytics.py +0 -0
  31. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/app_registry.py +0 -0
  32. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/backup.py +0 -0
  33. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/compute_remote.py +0 -0
  34. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/data_commit.py +0 -0
  35. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/data_explorer_table_prefs.py +0 -0
  36. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/db.py +0 -0
  37. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/device.py +0 -0
  38. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/enrichment.py +0 -0
  39. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/filter_lab.py +0 -0
  40. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/health.py +0 -0
  41. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/ingestion_api.py +0 -0
  42. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/ingestion_compat.py +0 -0
  43. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/ingestion_sources.py +0 -0
  44. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/llm.py +0 -0
  45. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/local_mcp.py +0 -0
  46. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/messenger_analytics.py +0 -0
  47. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/query_api.py +0 -0
  48. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/sanitization_ollama_config.py +0 -0
  49. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/sources.py +0 -0
  50. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/sync.py +0 -0
  51. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/ui_config.py +0 -0
  52. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/uma_data.py +0 -0
  53. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/usage.py +0 -0
  54. {topos_node-0.1.8 → topos_node-0.1.9}/topos/api/user_identity.py +0 -0
  55. {topos_node-0.1.8 → topos_node-0.1.9}/topos/auth.py +0 -0
  56. {topos_node-0.1.8 → topos_node-0.1.9}/topos/canonicalization/__init__.py +0 -0
  57. {topos_node-0.1.8 → topos_node-0.1.9}/topos/canonicalization/mappers/__init__.py +0 -0
  58. {topos_node-0.1.8 → topos_node-0.1.9}/topos/canonicalization/mappers/base.py +0 -0
  59. {topos_node-0.1.8 → topos_node-0.1.9}/topos/canonicalization/mappers/chatgpt_mapper.py +0 -0
  60. {topos_node-0.1.8 → topos_node-0.1.9}/topos/canonicalization/mappers/grok_mapper.py +0 -0
  61. {topos_node-0.1.8 → topos_node-0.1.9}/topos/canonicalization/mappers/messenger_mapper.py +0 -0
  62. {topos_node-0.1.8 → topos_node-0.1.9}/topos/canonicalization/models.py +0 -0
  63. {topos_node-0.1.8 → topos_node-0.1.9}/topos/canonicalization/resolver.py +0 -0
  64. {topos_node-0.1.8 → topos_node-0.1.9}/topos/cli/__init__.py +0 -0
  65. {topos_node-0.1.8 → topos_node-0.1.9}/topos/cli/__main__.py +0 -0
  66. {topos_node-0.1.8 → topos_node-0.1.9}/topos/config/__init__.py +0 -0
  67. {topos_node-0.1.8 → topos_node-0.1.9}/topos/config/sanitization_ollama.py +0 -0
  68. {topos_node-0.1.8 → topos_node-0.1.9}/topos/config/settings.py +0 -0
  69. {topos_node-0.1.8 → topos_node-0.1.9}/topos/contacts/__init__.py +0 -0
  70. {topos_node-0.1.8 → topos_node-0.1.9}/topos/contacts/identity.py +0 -0
  71. {topos_node-0.1.8 → topos_node-0.1.9}/topos/control_plane_client.py +0 -0
  72. {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/__init__.py +0 -0
  73. {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/api_models.py +0 -0
  74. {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/connection_resilience.py +0 -0
  75. {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/device_helpers.py +0 -0
  76. {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/errors.py +0 -0
  77. {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/events.py +0 -0
  78. {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/metrics.py +0 -0
  79. {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/startup_banner.py +0 -0
  80. {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/state.py +0 -0
  81. {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/table_layers.py +0 -0
  82. {topos_node-0.1.8 → topos_node-0.1.9}/topos/core/types.py +0 -0
  83. {topos_node-0.1.8 → topos_node-0.1.9}/topos/data_explorer_table_prefs.py +0 -0
  84. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/__init__.py +0 -0
  85. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/backends/__init__.py +0 -0
  86. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/backends/base.py +0 -0
  87. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/backends/huggingface.py +0 -0
  88. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/backends/ollama.py +0 -0
  89. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/backends/stub.py +0 -0
  90. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/engine.py +0 -0
  91. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/intake.py +0 -0
  92. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/queue_manager.py +0 -0
  93. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/registration.py +0 -0
  94. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/result_formatter.py +0 -0
  95. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/router.py +0 -0
  96. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/scoped_token.py +0 -0
  97. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/tasks.py +0 -0
  98. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/transport.py +0 -0
  99. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/usage_guard.py +0 -0
  100. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/usage_observation.py +0 -0
  101. {topos_node-0.1.8 → topos_node-0.1.9}/topos/engine/validator.py +0 -0
  102. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/__init__.py +0 -0
  103. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/derived_tables.py +0 -0
  104. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/__init__.py +0 -0
  105. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/base.py +0 -0
  106. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/__init__.py +0 -0
  107. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/embeddings_job.py +0 -0
  108. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/emo_27_job.py +0 -0
  109. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/entities_job.py +0 -0
  110. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/sentiment_job.py +0 -0
  111. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/canonical/topics_job.py +0 -0
  112. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/raw/__init__.py +0 -0
  113. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/raw/attachments_job.py +0 -0
  114. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/raw/language_job.py +0 -0
  115. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/raw/time_normalization_job.py +0 -0
  116. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/jobs/raw/tool_calls_job.py +0 -0
  117. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/models/__init__.py +0 -0
  118. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/models/manager.py +0 -0
  119. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/models/registry.py +0 -0
  120. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/models/versioning.py +0 -0
  121. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/orchestrator.py +0 -0
  122. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/processor.py +0 -0
  123. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/progress_bar.py +0 -0
  124. {topos_node-0.1.8 → topos_node-0.1.9}/topos/enrichment/website_classifier.py +0 -0
  125. {topos_node-0.1.8 → topos_node-0.1.9}/topos/filter_lab/__init__.py +0 -0
  126. {topos_node-0.1.8 → topos_node-0.1.9}/topos/filter_lab/bundles.py +0 -0
  127. {topos_node-0.1.8 → topos_node-0.1.9}/topos/filter_lab/schema.py +0 -0
  128. {topos_node-0.1.8 → topos_node-0.1.9}/topos/filter_lab/service.py +0 -0
  129. {topos_node-0.1.8 → topos_node-0.1.9}/topos/filter_lab/store.py +0 -0
  130. {topos_node-0.1.8 → topos_node-0.1.9}/topos/filter_lab/worker.py +0 -0
  131. {topos_node-0.1.8 → topos_node-0.1.9}/topos/hosted_pool_lease.py +0 -0
  132. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/__init__.py +0 -0
  133. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/checkpoints/__init__.py +0 -0
  134. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/checkpoints/checkpoint_store.py +0 -0
  135. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/checkpoints/sqlite_checkpoint_store.py +0 -0
  136. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/ingest_helpers.py +0 -0
  137. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/jobs.py +0 -0
  138. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/local_sync.py +0 -0
  139. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/log_preview.py +0 -0
  140. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/manager.py +0 -0
  141. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parser.py +0 -0
  142. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parsers/__init__.py +0 -0
  143. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parsers/base.py +0 -0
  144. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parsers/browser_parser.py +0 -0
  145. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parsers/calendar_parser.py +0 -0
  146. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parsers/chatgpt_conversation_flattener.py +0 -0
  147. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parsers/chatgpt_parser.py +0 -0
  148. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parsers/grok_parser.py +0 -0
  149. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/parsers/messenger_parser.py +0 -0
  150. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/progress.py +0 -0
  151. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/__init__.py +0 -0
  152. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/base.py +0 -0
  153. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/calendar.py +0 -0
  154. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/chatgpt.py +0 -0
  155. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/contact_importers.py +0 -0
  156. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/grok.py +0 -0
  157. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/imessage_reader.py +0 -0
  158. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/signal_export_parser.py +0 -0
  159. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/sources/signal_reader.py +0 -0
  160. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/state_machine.py +0 -0
  161. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/triggers/__init__.py +0 -0
  162. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/triggers/file_trigger.py +0 -0
  163. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/triggers/sqlite_trigger.py +0 -0
  164. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/validation/__init__.py +0 -0
  165. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/validation/base.py +0 -0
  166. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/validation/schema_registry.py +0 -0
  167. {topos_node-0.1.8 → topos_node-0.1.9}/topos/ingestion/validation/schema_validator.py +0 -0
  168. {topos_node-0.1.8 → topos_node-0.1.9}/topos/lineage/__init__.py +0 -0
  169. {topos_node-0.1.8 → topos_node-0.1.9}/topos/lineage/provenance.py +0 -0
  170. {topos_node-0.1.8 → topos_node-0.1.9}/topos/lineage/tracker.py +0 -0
  171. {topos_node-0.1.8 → topos_node-0.1.9}/topos/mcp_stdio_proxy.py +0 -0
  172. {topos_node-0.1.8 → topos_node-0.1.9}/topos/observability/__init__.py +0 -0
  173. {topos_node-0.1.8 → topos_node-0.1.9}/topos/observability/alerts.py +0 -0
  174. {topos_node-0.1.8 → topos_node-0.1.9}/topos/observability/metrics.py +0 -0
  175. {topos_node-0.1.8 → topos_node-0.1.9}/topos/observability/tracing.py +0 -0
  176. {topos_node-0.1.8 → topos_node-0.1.9}/topos/openai_client.py +0 -0
  177. {topos_node-0.1.8 → topos_node-0.1.9}/topos/projections/__init__.py +0 -0
  178. {topos_node-0.1.8 → topos_node-0.1.9}/topos/projections/vector_index/__init__.py +0 -0
  179. {topos_node-0.1.8 → topos_node-0.1.9}/topos/projections/vector_index/base.py +0 -0
  180. {topos_node-0.1.8 → topos_node-0.1.9}/topos/projections/vector_index/builders.py +0 -0
  181. {topos_node-0.1.8 → topos_node-0.1.9}/topos/projections/vector_index/health_checks.py +0 -0
  182. {topos_node-0.1.8 → topos_node-0.1.9}/topos/rate_limit.py +0 -0
  183. {topos_node-0.1.8 → topos_node-0.1.9}/topos/sanitization/__init__.py +0 -0
  184. {topos_node-0.1.8 → topos_node-0.1.9}/topos/sanitization/ollama_transforms.py +0 -0
  185. {topos_node-0.1.8 → topos_node-0.1.9}/topos/scope_resolution.py +0 -0
  186. {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/__init__.py +0 -0
  187. {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/container.py +0 -0
  188. {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/embeddings/__init__.py +0 -0
  189. {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/embeddings/base.py +0 -0
  190. {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/embeddings/local.py +0 -0
  191. {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/embeddings/remote.py +0 -0
  192. {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/interfaces.py +0 -0
  193. {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/llm/__init__.py +0 -0
  194. {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/llm/base.py +0 -0
  195. {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/llm/openai.py +0 -0
  196. {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/local.py +0 -0
  197. {topos_node-0.1.8 → topos_node-0.1.9}/topos/services/postgres.py +0 -0
  198. {topos_node-0.1.8 → topos_node-0.1.9}/topos/sources/__init__.py +0 -0
  199. {topos_node-0.1.8 → topos_node-0.1.9}/topos/sources/definitions.py +0 -0
  200. {topos_node-0.1.8 → topos_node-0.1.9}/topos/sources/registry.py +0 -0
  201. {topos_node-0.1.8 → topos_node-0.1.9}/topos/sources/runtime_install.py +0 -0
  202. {topos_node-0.1.8 → topos_node-0.1.9}/topos/startup_banner.py +0 -0
  203. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/__init__.py +0 -0
  204. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/__init__.py +0 -0
  205. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/ai_chat/__init__.py +0 -0
  206. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/ai_chat/canonicalizer.py +0 -0
  207. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/ai_chat/mapper.py +0 -0
  208. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/ai_chat/model.py +0 -0
  209. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/ai_chat/tables.py +0 -0
  210. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/canonical_store.py +0 -0
  211. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/conversations_tables.py +0 -0
  212. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/mapping_store.py +0 -0
  213. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/canonical/postgres.py +0 -0
  214. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/db/__init__.py +0 -0
  215. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/db/client.py +0 -0
  216. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/db/migrations/__init__.py +0 -0
  217. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/db/migrations/stage9_column_renames.py +0 -0
  218. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/db/paths.py +0 -0
  219. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/db/postgres.py +0 -0
  220. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/db/schema.py +0 -0
  221. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/enrichment/__init__.py +0 -0
  222. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/enrichment/canonical_enrichment_store.py +0 -0
  223. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/enrichment/raw_enrichment_store.py +0 -0
  224. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/normalized/__init__.py +0 -0
  225. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/normalized/normalized_store.py +0 -0
  226. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/oplog/__init__.py +0 -0
  227. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/oplog/decision.py +0 -0
  228. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/oplog/oplog_store.py +0 -0
  229. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/oplog/postgres.py +0 -0
  230. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/projections/__init__.py +0 -0
  231. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/projections/index_ops_store.py +0 -0
  232. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/projections/vector_index_store.py +0 -0
  233. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/raw/__init__.py +0 -0
  234. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/raw/browser_flat_tables.py +0 -0
  235. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/raw/file_store.py +0 -0
  236. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/raw/raw_store.py +0 -0
  237. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/raw/raw_tables_manager.py +0 -0
  238. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/raw/sqlite_raw_store.py +0 -0
  239. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/security/encryption.py +0 -0
  240. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/signal_identity.py +0 -0
  241. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/source_settings.py +0 -0
  242. {topos_node-0.1.8 → topos_node-0.1.9}/topos/storage/user_identity.py +0 -0
  243. {topos_node-0.1.8 → topos_node-0.1.9}/topos/sync/__init__.py +0 -0
  244. {topos_node-0.1.8 → topos_node-0.1.9}/topos/sync_handlers.py +0 -0
  245. {topos_node-0.1.8 → topos_node-0.1.9}/topos/testing/__init__.py +0 -0
  246. {topos_node-0.1.8 → topos_node-0.1.9}/topos/testing/lifespan.py +0 -0
  247. {topos_node-0.1.8 → topos_node-0.1.9}/topos/uma_contact_enrichment.py +0 -0
  248. {topos_node-0.1.8 → topos_node-0.1.9}/topos/uma_filters.py +0 -0
  249. {topos_node-0.1.8 → topos_node-0.1.9}/topos/uma_resource_id.py +0 -0
  250. {topos_node-0.1.8 → topos_node-0.1.9}/topos/uma_rpt.py +0 -0
  251. {topos_node-0.1.8 → topos_node-0.1.9}/topos/utils/base_object.py +0 -0
  252. {topos_node-0.1.8 → topos_node-0.1.9}/topos/websocket_client.py +0 -0
  253. {topos_node-0.1.8 → topos_node-0.1.9}/topos_node.egg-info/dependency_links.txt +0 -0
  254. {topos_node-0.1.8 → topos_node-0.1.9}/topos_node.egg-info/entry_points.txt +0 -0
  255. {topos_node-0.1.8 → topos_node-0.1.9}/topos_node.egg-info/requires.txt +0 -0
  256. {topos_node-0.1.8 → topos_node-0.1.9}/topos_node.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: topos-node
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: Topos personal AI engine (FastAPI): local data, sync, control plane WebSocket client
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Homepage, https://topos.dialogues.ai
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "topos-node"
7
- version = "0.1.8"
7
+ version = "0.1.9"
8
8
  description = "Topos personal AI engine (FastAPI): local data, sync, control plane WebSocket client"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  """Topos Control Plane Version Information."""
2
2
 
3
- __version__ = "0.1.8"
4
- __version_info__ = (0, 1, 8)
3
+ __version__ = "0.1.9"
4
+ __version_info__ = (0, 1, 9)
5
5
 
6
6
  __all__ = ["__version__", "__version_info__"]
@@ -145,6 +145,21 @@ async def _list_sources_core(payload: Dict[str, Any]) -> Dict[str, Any]:
145
145
  return {"status": "ok", "sources": sources}
146
146
 
147
147
 
148
+ async def _patch_source_install_core(payload: Dict[str, Any]) -> Dict[str, Any]:
149
+ source_id = str(payload.get("source_id") or "").strip()
150
+ if not source_id:
151
+ raise ValueError("source_id is required")
152
+ partial = payload.get("source_definition_json")
153
+ if not isinstance(partial, dict):
154
+ raise ValueError("source_definition_json object is required")
155
+ record = install_service.patch_source_install(
156
+ source_id=source_id,
157
+ scope=_scope_from_payload(payload),
158
+ source_definition_json=partial,
159
+ )
160
+ return {"status": "ok", "install": record.to_dict()}
161
+
162
+
148
163
  async def _uninstall_source_core(payload: Dict[str, Any]) -> Dict[str, Any]:
149
164
  source_id = str(payload.get("source_id") or "").strip()
150
165
  if not source_id:
@@ -266,6 +281,21 @@ async def source_install_status(
266
281
  return _ok_envelope(request_id, result)
267
282
 
268
283
 
284
+ @router.patch("/source-install", dependencies=[Depends(require_api_key)])
285
+ async def patch_source_install(payload: Dict[str, Any] = Body(default_factory=dict)) -> Dict[str, Any]:
286
+ request_id = str(uuid.uuid4())
287
+ _log_request("patch_source_install", request_id, payload)
288
+ try:
289
+ result = await _patch_source_install_core(payload)
290
+ return _ok_envelope(request_id, result)
291
+ except ValueError as exc:
292
+ raise HTTPException(status_code=400, detail=str(exc))
293
+ except RuntimeError as exc:
294
+ raise HTTPException(status_code=503, detail=str(exc))
295
+ except Exception as exc: # noqa: BLE001
296
+ raise HTTPException(status_code=500, detail=str(exc))
297
+
298
+
269
299
  @router.delete("/source-install", dependencies=[Depends(require_api_key)])
270
300
  async def uninstall_source(payload: Dict[str, Any] = Body(default_factory=dict)) -> Dict[str, Any]:
271
301
  request_id = str(uuid.uuid4())
@@ -46,6 +46,11 @@ from .control_plane_client import ControlPlaneClient
46
46
  from .engine.registration import build_engine_heartbeat_message, build_engine_register_message
47
47
  from .hosted_pool_lease import HostedPoolLeaseClient
48
48
  from .services.container import get_services
49
+ from .runtime_update import (
50
+ start_runtime_update_monitor,
51
+ start_update_hotkey_listener,
52
+ stop_runtime_update_monitor,
53
+ )
49
54
  from .startup_banner import emit_startup_banner
50
55
  from .sync import SyncClient
51
56
  from .sync_handlers import handle_sync_op
@@ -206,6 +211,9 @@ async def startup_event() -> None:
206
211
  await state.control_plane_client.send_message(build_engine_heartbeat_message())
207
212
 
208
213
  state.engine_presence_task = asyncio.create_task(_presence_loop())
214
+ start_runtime_update_monitor()
215
+ start_update_hotkey_listener()
216
+
209
217
  if settings.enable_sync and settings.topos_user_id:
210
218
  state.sync_client = SyncClient(
211
219
  sync_url=settings.get_sync_url(),
@@ -229,6 +237,7 @@ async def startup_event() -> None:
229
237
 
230
238
  @app.on_event("shutdown")
231
239
  async def shutdown_event() -> None:
240
+ await stop_runtime_update_monitor()
232
241
  if state.engine_presence_task:
233
242
  state.engine_presence_task.cancel()
234
243
  try:
@@ -2,28 +2,42 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import json
6
5
  import os
7
- import subprocess
8
6
  import sys
9
- from importlib.metadata import PackageNotFoundError, version as package_version
10
7
  from pathlib import Path
11
- from urllib.error import URLError
12
- from urllib.request import urlopen
13
8
 
14
9
  import click
15
10
  import uvicorn
16
- from packaging.version import InvalidVersion, Version
17
11
 
18
12
  # Add parent directory to path for imports
19
13
  sys.path.insert(0, str(Path(__file__).parent.parent.parent))
20
14
 
15
+ from topos.runtime_update import (
16
+ DEFAULT_PACKAGE_NAME,
17
+ apply_package_update,
18
+ can_prompt_for_input,
19
+ check_for_update,
20
+ get_installed_package_version,
21
+ get_latest_pypi_version,
22
+ get_module_version,
23
+ should_skip_update_check,
24
+ )
21
25
  from topos.storage.db.paths import discover_databases
22
26
  from topos.startup_banner import emit_startup_banner
23
27
 
24
28
  USER_ENV_PATH = Path.home() / ".topos" / ".env"
25
29
  LEGACY_ENV_PATH = Path(__file__).resolve().parent.parent / ".env"
26
30
 
31
+ # Re-exported for tests that monkeypatch commands.*
32
+ _get_installed_package_version = get_installed_package_version
33
+ _get_latest_pypi_version = get_latest_pypi_version
34
+ _get_module_version = get_module_version
35
+ _can_prompt_for_input = can_prompt_for_input
36
+
37
+
38
+ def _get_runtime_version(package_name: str = DEFAULT_PACKAGE_NAME) -> str:
39
+ return _get_module_version() or _get_installed_package_version(package_name) or "unknown"
40
+
27
41
 
28
42
  def _load_env_file(env_path: Path) -> None:
29
43
  if not env_path.exists():
@@ -69,10 +83,6 @@ def _save_topos_key(topos_key: str, env_path: Path = USER_ENV_PATH) -> Path:
69
83
  return env_path
70
84
 
71
85
 
72
- def _can_prompt_for_input() -> bool:
73
- return sys.stdin.isatty() and sys.stdout.isatty()
74
-
75
-
76
86
  def _prompt_for_topos_key() -> str:
77
87
  click.echo("TOPOS_KEY is required to connect Topos Node.")
78
88
  click.echo("Enter your TOPOS_KEY to save it locally and continue.")
@@ -107,26 +117,7 @@ def _resolve_topos_key(cli_topos_key: str | None, env_path: Path = USER_ENV_PATH
107
117
  )
108
118
 
109
119
 
110
- def _get_installed_package_version(package_name: str) -> str | None:
111
- try:
112
- return package_version(package_name)
113
- except PackageNotFoundError:
114
- return None
115
-
116
-
117
- def _get_module_version() -> str | None:
118
- try:
119
- from topos.__version__ import __version__
120
- except Exception:
121
- return None
122
- return __version__ or None
123
-
124
-
125
- def _get_runtime_version(package_name: str = "topos-node") -> str:
126
- return _get_module_version() or _get_installed_package_version(package_name) or "unknown"
127
-
128
-
129
- def _emit_startup_banner(host: str, port: int, package_name: str = "topos-node") -> None:
120
+ def _emit_startup_banner(host: str, port: int, package_name: str = DEFAULT_PACKAGE_NAME) -> None:
130
121
  runtime_version = _get_runtime_version(package_name=package_name)
131
122
  emit_startup_banner(
132
123
  click.echo,
@@ -138,45 +129,18 @@ def _emit_startup_banner(host: str, port: int, package_name: str = "topos-node")
138
129
  os.environ["TOPOS_STARTUP_BANNER_EMITTED"] = "1"
139
130
 
140
131
 
141
- def _get_latest_pypi_version(package_name: str, timeout_seconds: float = 2.0) -> str | None:
142
- url = f"https://pypi.org/pypi/{package_name}/json"
143
- try:
144
- with urlopen(url, timeout=timeout_seconds) as response:
145
- payload = json.loads(response.read().decode("utf-8"))
146
- except (OSError, URLError, ValueError):
147
- return None
148
- return str(payload.get("info", {}).get("version") or "").strip() or None
149
-
150
-
151
- def _should_skip_update_check(skip_update_check: bool) -> bool:
152
- if skip_update_check:
153
- return True
154
- env_value = (os.getenv("TOPOS_SKIP_UPDATE_CHECK") or "").strip().lower()
155
- return env_value in {"1", "true", "yes", "on"}
156
-
157
-
158
132
  def _maybe_offer_self_update(
159
133
  skip_update_check: bool,
160
- package_name: str = "topos-node",
134
+ package_name: str = DEFAULT_PACKAGE_NAME,
161
135
  ) -> bool:
162
- if _should_skip_update_check(skip_update_check):
136
+ if should_skip_update_check(cli_skip=skip_update_check):
163
137
  return False
164
138
 
165
- installed = _get_installed_package_version(package_name)
166
- if not installed:
139
+ info = check_for_update(package_name)
140
+ if not info:
167
141
  return False
168
142
 
169
- latest = _get_latest_pypi_version(package_name)
170
- if not latest:
171
- return False
172
-
173
- try:
174
- if Version(latest) <= Version(installed):
175
- return False
176
- except InvalidVersion:
177
- return False
178
-
179
- click.echo(f"Update available for {package_name}: {installed} -> {latest}")
143
+ click.echo(f"Update available for {package_name}: {info.installed} -> {info.latest}")
180
144
  if not _can_prompt_for_input():
181
145
  click.echo(f"Run `uv tool upgrade {package_name}` to update.")
182
146
  return False
@@ -185,8 +149,7 @@ def _maybe_offer_self_update(
185
149
  return False
186
150
 
187
151
  click.echo(f"Updating {package_name}...")
188
- result = subprocess.run(["uv", "tool", "upgrade", package_name], check=False)
189
- if result.returncode != 0:
152
+ if not apply_package_update(package_name):
190
153
  click.echo("Update failed. Continuing with current version.")
191
154
  return False
192
155
 
@@ -246,6 +209,9 @@ def main(db_path, topos_key, set_topos_key, discover, port, host, skip_update_ch
246
209
  click.echo("No existing databases found")
247
210
  return
248
211
 
212
+ if skip_update_check:
213
+ os.environ["TOPOS_SKIP_UPDATE_CHECK"] = "1"
214
+
249
215
  if _maybe_offer_self_update(skip_update_check=skip_update_check):
250
216
  return
251
217
 
@@ -1314,6 +1314,20 @@ async def handle_control_plane_request(message: Dict[str, Any]) -> Optional[Dict
1314
1314
  except Exception as exc: # noqa: BLE001
1315
1315
  return {"id": req_id, "status": "error", "error": str(exc)}
1316
1316
 
1317
+ if msg_type == "patch_source_install":
1318
+ from ..api.source_install import _patch_source_install_core
1319
+
1320
+ payload = message.get("payload") if isinstance(message.get("payload"), dict) else {}
1321
+ try:
1322
+ result = await _patch_source_install_core(payload)
1323
+ return {"id": req_id, "status": "ok", "payload": result}
1324
+ except ValueError as exc:
1325
+ return {"id": req_id, "status": "error", "error": str(exc)}
1326
+ except RuntimeError as exc:
1327
+ return {"id": req_id, "status": "error", "error": str(exc)}
1328
+ except Exception as exc: # noqa: BLE001
1329
+ return {"id": req_id, "status": "error", "error": str(exc)}
1330
+
1317
1331
  if msg_type == "post_source_test_ingestion":
1318
1332
  from ..api.source_install import _test_ingestion_core
1319
1333
 
@@ -5,6 +5,7 @@ from datetime import datetime
5
5
  from time import time
6
6
 
7
7
  from ..config.settings import settings
8
+ from ..runtime_update import is_update_available
8
9
 
9
10
  _LOG_FORMAT: str | None = None
10
11
 
@@ -96,8 +97,9 @@ class ColorFormatter(logging.Formatter):
96
97
  logging.CRITICAL: "\x1b[38;5;196m", # Red
97
98
  }
98
99
 
99
- # Timestamp color: Forest green
100
+ # Timestamp color: Forest green (amber when a newer Topos release is on PyPI)
100
101
  _TIMESTAMP_COLOR = "\x1b[38;5;28m" # Forest green
102
+ _TIMESTAMP_UPDATE_COLOR = "\x1b[38;5;214m" # Amber
101
103
 
102
104
  # Separator color: Light gray
103
105
  _SEPARATOR_COLOR = "\x1b[38;5;244m" # Light gray
@@ -119,7 +121,10 @@ class ColorFormatter(logging.Formatter):
119
121
  def format(self, record: logging.LogRecord) -> str: # noqa: D401
120
122
  # Format timestamp: YYYY-MM-DD HH:MM:SS.ms (green)
121
123
  timestamp_str = datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
122
- timestamp = f"{self._TIMESTAMP_COLOR}{timestamp_str}{self._RESET}"
124
+ timestamp_color = (
125
+ self._TIMESTAMP_UPDATE_COLOR if is_update_available() else self._TIMESTAMP_COLOR
126
+ )
127
+ timestamp = f"{timestamp_color}{timestamp_str}{self._RESET}"
123
128
 
124
129
  # Get text color for log level (no background)
125
130
  level_color = self._LEVEL_COLORS.get(record.levelno, "")
@@ -0,0 +1,246 @@
1
+ """Runtime PyPI update checks for the Topos node (non-blocking while server runs)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ import os
9
+ import subprocess
10
+ import sys
11
+ import threading
12
+ from dataclasses import dataclass
13
+ from importlib.metadata import PackageNotFoundError, version as package_version
14
+ from urllib.error import URLError
15
+ from urllib.request import urlopen
16
+
17
+ from packaging.version import InvalidVersion, Version
18
+
19
+ DEFAULT_PACKAGE_NAME = "topos-node"
20
+ DEFAULT_CHECK_INTERVAL_SECONDS = 6 * 60 * 60
21
+ INITIAL_CHECK_DELAY_SECONDS = 10.0
22
+ PYPI_TIMEOUT_SECONDS = 2.0
23
+
24
+ _logger = logging.getLogger("topos.runtime_update")
25
+ _lock = threading.Lock()
26
+ _update_info: UpdateInfo | None = None
27
+ _announcement_logged = False
28
+ _monitor_task: asyncio.Task[None] | None = None
29
+ _hotkey_thread: threading.Thread | None = None
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class UpdateInfo:
34
+ package_name: str
35
+ installed: str
36
+ latest: str
37
+
38
+
39
+ def should_skip_update_check(*, cli_skip: bool = False) -> bool:
40
+ if cli_skip:
41
+ return True
42
+ env_value = (os.getenv("TOPOS_SKIP_UPDATE_CHECK") or "").strip().lower()
43
+ return env_value in {"1", "true", "yes", "on"}
44
+
45
+
46
+ def get_installed_package_version(package_name: str = DEFAULT_PACKAGE_NAME) -> str | None:
47
+ try:
48
+ return package_version(package_name)
49
+ except PackageNotFoundError:
50
+ return None
51
+
52
+
53
+ def get_module_version() -> str | None:
54
+ try:
55
+ from topos.__version__ import __version__
56
+ except Exception:
57
+ return None
58
+ return __version__ or None
59
+
60
+
61
+ def get_runtime_version(package_name: str = DEFAULT_PACKAGE_NAME) -> str:
62
+ return get_module_version() or get_installed_package_version(package_name) or "unknown"
63
+
64
+
65
+ def get_latest_pypi_version(
66
+ package_name: str = DEFAULT_PACKAGE_NAME,
67
+ timeout_seconds: float = PYPI_TIMEOUT_SECONDS,
68
+ ) -> str | None:
69
+ url = f"https://pypi.org/pypi/{package_name}/json"
70
+ try:
71
+ with urlopen(url, timeout=timeout_seconds) as response:
72
+ payload = json.loads(response.read().decode("utf-8"))
73
+ except (OSError, URLError, ValueError):
74
+ return None
75
+ return str(payload.get("info", {}).get("version") or "").strip() or None
76
+
77
+
78
+ def is_update_available() -> bool:
79
+ with _lock:
80
+ return _update_info is not None
81
+
82
+
83
+ def get_update_info() -> UpdateInfo | None:
84
+ with _lock:
85
+ return _update_info
86
+
87
+
88
+ def _set_update_info(info: UpdateInfo | None) -> None:
89
+ global _update_info
90
+ with _lock:
91
+ _update_info = info
92
+
93
+
94
+ def _is_newer_version(installed: str, latest: str) -> bool:
95
+ try:
96
+ return Version(latest) > Version(installed)
97
+ except InvalidVersion:
98
+ return False
99
+
100
+
101
+ def check_for_update(package_name: str = DEFAULT_PACKAGE_NAME) -> UpdateInfo | None:
102
+ """Return update details when PyPI has a newer release, else None."""
103
+ if should_skip_update_check():
104
+ return None
105
+
106
+ installed = get_installed_package_version(package_name) or get_module_version()
107
+ if not installed or installed == "unknown":
108
+ return None
109
+
110
+ latest = get_latest_pypi_version(package_name)
111
+ if not latest or not _is_newer_version(installed, latest):
112
+ return None
113
+
114
+ return UpdateInfo(package_name=package_name, installed=installed, latest=latest)
115
+
116
+
117
+ def apply_package_update(package_name: str = DEFAULT_PACKAGE_NAME) -> bool:
118
+ """Install the latest PyPI release via `uv tool upgrade`. Returns True on success."""
119
+ result = subprocess.run(["uv", "tool", "upgrade", package_name], check=False)
120
+ return result.returncode == 0
121
+
122
+
123
+ def _log_update_available_once(info: UpdateInfo) -> None:
124
+ global _announcement_logged
125
+ with _lock:
126
+ if _announcement_logged:
127
+ return
128
+ _announcement_logged = True
129
+
130
+ _logger.warning(
131
+ "New Topos version available: %s -> %s. "
132
+ "Timestamps will show in amber until you restart. "
133
+ "Update with `uv tool upgrade %s` then stop (Ctrl+C) and re-run `topos-node`. "
134
+ "In an interactive terminal, type `:update` and press Enter to install now.",
135
+ info.installed,
136
+ info.latest,
137
+ info.package_name,
138
+ )
139
+
140
+
141
+ async def _monitor_loop(
142
+ *,
143
+ package_name: str = DEFAULT_PACKAGE_NAME,
144
+ initial_delay_seconds: float = INITIAL_CHECK_DELAY_SECONDS,
145
+ interval_seconds: float = DEFAULT_CHECK_INTERVAL_SECONDS,
146
+ ) -> None:
147
+ await asyncio.sleep(initial_delay_seconds)
148
+ while True:
149
+ try:
150
+ info = await asyncio.to_thread(check_for_update, package_name)
151
+ if info:
152
+ _set_update_info(info)
153
+ _log_update_available_once(info)
154
+ except asyncio.CancelledError:
155
+ raise
156
+ except Exception as exc: # noqa: BLE001
157
+ _logger.debug("Runtime update check failed (non-fatal): %s", exc)
158
+ await asyncio.sleep(interval_seconds)
159
+
160
+
161
+ def start_runtime_update_monitor(
162
+ *,
163
+ cli_skip: bool = False,
164
+ package_name: str = DEFAULT_PACKAGE_NAME,
165
+ ) -> asyncio.Task[None] | None:
166
+ """Schedule periodic PyPI checks; safe to call once at app startup."""
167
+ global _monitor_task
168
+ if should_skip_update_check(cli_skip=cli_skip):
169
+ return None
170
+ if _monitor_task is not None and not _monitor_task.done():
171
+ return _monitor_task
172
+
173
+ loop = asyncio.get_running_loop()
174
+ _monitor_task = loop.create_task(
175
+ _monitor_loop(package_name=package_name),
176
+ name="topos-runtime-update-monitor",
177
+ )
178
+ return _monitor_task
179
+
180
+
181
+ async def stop_runtime_update_monitor() -> None:
182
+ global _monitor_task
183
+ task = _monitor_task
184
+ _monitor_task = None
185
+ if task is None:
186
+ return
187
+ task.cancel()
188
+ try:
189
+ await task
190
+ except asyncio.CancelledError:
191
+ pass
192
+
193
+
194
+ def _handle_hotkey_line(line: str, package_name: str = DEFAULT_PACKAGE_NAME) -> None:
195
+ command = line.strip().lower()
196
+ if command not in {":update", ":u"}:
197
+ return
198
+
199
+ info = get_update_info() or check_for_update(package_name)
200
+ if not info:
201
+ _logger.info("Topos is already on the latest published version.")
202
+ return
203
+
204
+ _logger.info("Updating %s (%s -> %s)...", package_name, info.installed, info.latest)
205
+ if apply_package_update(package_name):
206
+ _logger.warning(
207
+ "Update installed. Stop Topos (Ctrl+C) and re-run `topos-node` to use %s.",
208
+ info.latest,
209
+ )
210
+ else:
211
+ _logger.error("Update failed. Run `uv tool upgrade %s` manually.", package_name)
212
+
213
+
214
+ def _hotkey_listener_loop(package_name: str = DEFAULT_PACKAGE_NAME) -> None:
215
+ try:
216
+ for line in sys.stdin:
217
+ _handle_hotkey_line(line, package_name=package_name)
218
+ except Exception as exc: # noqa: BLE001
219
+ _logger.debug("Update hotkey listener stopped: %s", exc)
220
+
221
+
222
+ def start_update_hotkey_listener(
223
+ *,
224
+ cli_skip: bool = False,
225
+ package_name: str = DEFAULT_PACKAGE_NAME,
226
+ ) -> None:
227
+ """Listen for `:update` on stdin (interactive terminals only)."""
228
+ global _hotkey_thread
229
+ if should_skip_update_check(cli_skip=cli_skip):
230
+ return
231
+ if not sys.stdin.isatty():
232
+ return
233
+ if _hotkey_thread is not None and _hotkey_thread.is_alive():
234
+ return
235
+
236
+ _hotkey_thread = threading.Thread(
237
+ target=_hotkey_listener_loop,
238
+ kwargs={"package_name": package_name},
239
+ name="topos-update-hotkey",
240
+ daemon=True,
241
+ )
242
+ _hotkey_thread.start()
243
+
244
+
245
+ def can_prompt_for_input() -> bool:
246
+ return sys.stdin.isatty() and sys.stdout.isatty()
@@ -782,6 +782,101 @@ def uninstall_source(
782
782
  }
783
783
 
784
784
 
785
+ _PATCHABLE_DEFINITION_KEYS = frozenset(
786
+ {
787
+ "default_scope_id",
788
+ "ingestion_trigger",
789
+ "enrichment_trigger",
790
+ "raw_enrichment_jobs",
791
+ "canonical_enrichment_jobs",
792
+ }
793
+ )
794
+ _IMMUTABLE_DEFINITION_KEYS = frozenset(
795
+ {
796
+ "source_id",
797
+ "display_name",
798
+ "source_type",
799
+ "schema_id",
800
+ "parser_id",
801
+ "canonical_mapper_id",
802
+ "canonical_group_id",
803
+ }
804
+ )
805
+
806
+
807
+ def patch_source_install(
808
+ *,
809
+ source_id: str,
810
+ scope: Optional[Dict[str, Any]] = None,
811
+ source_definition_json: Optional[Dict[str, Any]] = None,
812
+ ) -> InstallRecord:
813
+ """Update an active install's definition in place (user configuration changes)."""
814
+ ensure_install_schema()
815
+ sid = str(source_id or "").strip()
816
+ if not sid:
817
+ raise ValueError("source_id is required")
818
+ partial = source_definition_json if isinstance(source_definition_json, dict) else {}
819
+ if not partial:
820
+ raise ValueError("source_definition_json is required")
821
+
822
+ _validate_concrete_install_scope(scope)
823
+ scope_key = _scope_key(scope)
824
+ now = _utc_now_iso()
825
+
826
+ with _db_conn() as conn:
827
+ with _LOCK:
828
+ active = _get_active_record(conn, scope_key, sid)
829
+ if not active or not active.is_active:
830
+ raise ValueError(f"No active install found for source_id={sid}")
831
+
832
+ merged = dict(active.source_definition_json if isinstance(active.source_definition_json, dict) else {})
833
+ for key, value in partial.items():
834
+ if key in _IMMUTABLE_DEFINITION_KEYS and value != merged.get(key):
835
+ raise ValueError(f"Cannot change immutable field {key!r} via PATCH")
836
+ if key in _PATCHABLE_DEFINITION_KEYS:
837
+ merged[key] = value
838
+
839
+ merged = _normalize_enrichment_bindings(merged)
840
+ _validate_source_contract(merged)
841
+ if str(merged.get("source_id") or "").strip() != sid:
842
+ raise ValueError("source_id in definition must match install source_id")
843
+
844
+ try:
845
+ handle = install_source_definition(merged)
846
+ except Exception as exc:
847
+ raise RuntimeError(str(exc)) from exc
848
+
849
+ execute_query(
850
+ conn,
851
+ f"""
852
+ UPDATE {INSTALL_TABLE}
853
+ SET source_definition_json = %s, updated_at = %s
854
+ WHERE install_id = %s
855
+ """,
856
+ (
857
+ json.dumps(merged, separators=(",", ":"), ensure_ascii=True),
858
+ now,
859
+ active.install_id,
860
+ ),
861
+ )
862
+ if settings.topos_database_mode != "postgres":
863
+ conn.commit()
864
+ _ACTIVE_HANDLES[(scope_key, sid)] = handle
865
+ return InstallRecord(
866
+ install_id=active.install_id,
867
+ scope=_scope_dict(scope_key),
868
+ source_id=sid,
869
+ version_id=active.version_id,
870
+ status="active",
871
+ is_active=True,
872
+ source_definition_json=merged,
873
+ source_version_row_json=active.source_version_row_json,
874
+ failure_reason=None,
875
+ created_at=active.created_at,
876
+ updated_at=now,
877
+ )
878
+
879
+
785
880
  def get_active_source_definition(*, source_id: str, scope: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
786
881
  try:
787
882
  with _db_conn() as conn:
@@ -148,6 +148,9 @@ class SyncClient:
148
148
  self._consecutive_failures = 0
149
149
  self._last_connected_at = utc_now_iso()
150
150
  logger.info("Sync client connected to relay")
151
+ # Let concurrent readiness waiters observe the connection before
152
+ # processing messages; short-lived test sockets can close immediately.
153
+ await asyncio.sleep(0)
151
154
 
152
155
  await self._send_connect()
153
156
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: topos-node
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: Topos personal AI engine (FastAPI): local data, sync, control plane WebSocket client
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Homepage, https://topos.dialogues.ai
@@ -14,6 +14,7 @@ topos/hosted_pool_lease.py
14
14
  topos/mcp_stdio_proxy.py
15
15
  topos/openai_client.py
16
16
  topos/rate_limit.py
17
+ topos/runtime_update.py
17
18
  topos/scope_resolution.py
18
19
  topos/startup_banner.py
19
20
  topos/sync_handlers.py