atomicmemory 1.0.1__tar.gz → 1.1.0__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 (160) hide show
  1. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/.gitignore +6 -0
  2. atomicmemory-1.1.0/CHANGELOG.md +46 -0
  3. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/PKG-INFO +49 -4
  4. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/README.md +47 -3
  5. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/__init__.py +26 -0
  6. atomicmemory-1.1.0/atomicmemory/_version.py +7 -0
  7. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/client/async_memory_client.py +91 -11
  8. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/client/memory_client.py +42 -11
  9. atomicmemory-1.1.0/atomicmemory/contract/__init__.py +18 -0
  10. atomicmemory-1.1.0/atomicmemory/contract/v1.py +400 -0
  11. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/memory/__init__.py +26 -0
  12. atomicmemory-1.1.0/atomicmemory/memory/capability_profiles.py +79 -0
  13. atomicmemory-1.1.0/atomicmemory/memory/meta_fact_filter.py +122 -0
  14. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/memory/registry.py +2 -2
  15. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/memory/service.py +121 -20
  16. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/memory/types.py +42 -0
  17. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/async_handle_impl.py +10 -7
  18. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/async_provider.py +18 -4
  19. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/config.py +7 -2
  20. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/handle.py +2 -0
  21. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/handle_impl.py +28 -8
  22. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/mappers.py +33 -1
  23. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/provider.py +41 -5
  24. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/scope_mapper.py +28 -5
  25. atomicmemory-1.1.0/contract/CONTRACT.md +114 -0
  26. atomicmemory-1.1.0/contract/VENDORED.json +8 -0
  27. atomicmemory-1.1.0/contract/v1/capabilities-descriptor.schema.json +8 -0
  28. atomicmemory-1.1.0/contract/v1/conformance/capabilities-descriptor.json +20 -0
  29. atomicmemory-1.1.0/contract/v1/conformance/ingest-text.json +18 -0
  30. atomicmemory-1.1.0/contract/v1/conformance/ingest-verbatim.json +19 -0
  31. atomicmemory-1.1.0/contract/v1/conformance/manifest.json +12 -0
  32. atomicmemory-1.1.0/contract/v1/conformance/search-with-retrieval-receipt.json +39 -0
  33. atomicmemory-1.1.0/contract/v1/ingest-input.schema.json +8 -0
  34. atomicmemory-1.1.0/contract/v1/provider-contract.schema.json +336 -0
  35. atomicmemory-1.1.0/contract/v1/search-result-page.schema.json +8 -0
  36. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/examples/async_pipeline.py +1 -1
  37. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/examples/basic_ingest_search.py +2 -2
  38. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/pyproject.toml +3 -1
  39. atomicmemory-1.1.0/tests/_lifecycle_fakes.py +153 -0
  40. atomicmemory-1.1.0/tests/client/test_client_lifecycle.py +312 -0
  41. atomicmemory-1.1.0/tests/contract/_schema_registry.py +66 -0
  42. atomicmemory-1.1.0/tests/contract/test_codec.py +207 -0
  43. atomicmemory-1.1.0/tests/contract/test_conformance.py +142 -0
  44. atomicmemory-1.1.0/tests/contract/test_vendored_tree.py +101 -0
  45. atomicmemory-1.1.0/tests/memory/test_capability_profiles.py +60 -0
  46. atomicmemory-1.1.0/tests/memory/test_meta_fact_filter.py +80 -0
  47. atomicmemory-1.1.0/tests/memory/test_service_lifecycle.py +188 -0
  48. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_async_provider.py +101 -0
  49. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_handle_base.py +98 -0
  50. atomicmemory-1.1.0/tests/providers/atomicmemory/test_ingest_content_class.py +50 -0
  51. atomicmemory-1.1.0/tests/providers/atomicmemory/test_integration.py +89 -0
  52. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_mappers.py +57 -0
  53. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_provider.py +90 -0
  54. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_scope_mapper.py +33 -0
  55. atomicmemory-1.1.0/tests/search/__init__.py +0 -0
  56. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/test_package_imports.py +1 -1
  57. atomicmemory-1.1.0/tests/test_version_consistency.py +26 -0
  58. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/uv.lock +445 -1
  59. atomicmemory-1.0.1/CHANGELOG.md +0 -25
  60. atomicmemory-1.0.1/atomicmemory/_version.py +0 -3
  61. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/LICENSE +0 -0
  62. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/client/__init__.py +0 -0
  63. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/client/atomic_memory_client.py +0 -0
  64. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/core/__init__.py +0 -0
  65. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/core/errors.py +0 -0
  66. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/core/events.py +0 -0
  67. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/core/logging.py +0 -0
  68. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/core/retry.py +0 -0
  69. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/core/validation.py +0 -0
  70. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/embeddings/__init__.py +0 -0
  71. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/embeddings/base.py +0 -0
  72. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/embeddings/sentence_transformers.py +0 -0
  73. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/kv_cache/__init__.py +0 -0
  74. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/kv_cache/adapter.py +0 -0
  75. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/kv_cache/memory_storage.py +0 -0
  76. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/kv_cache/sqlite_storage.py +0 -0
  77. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/memory/filters.py +0 -0
  78. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/memory/pipeline.py +0 -0
  79. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/memory/provider.py +0 -0
  80. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/__init__.py +0 -0
  81. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/__init__.py +0 -0
  82. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/agents.py +0 -0
  83. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/audit.py +0 -0
  84. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/config_handle.py +0 -0
  85. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/http.py +0 -0
  86. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/lessons.py +0 -0
  87. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/lifecycle.py +0 -0
  88. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/atomicmemory/path.py +0 -0
  89. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/hindsight/__init__.py +0 -0
  90. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/hindsight/async_provider.py +0 -0
  91. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/hindsight/config.py +0 -0
  92. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/hindsight/http.py +0 -0
  93. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/hindsight/mappers.py +0 -0
  94. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/hindsight/provider.py +0 -0
  95. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/mem0/__init__.py +0 -0
  96. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/mem0/async_provider.py +0 -0
  97. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/mem0/config.py +0 -0
  98. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/mem0/http.py +0 -0
  99. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/mem0/mappers.py +0 -0
  100. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/providers/mem0/provider.py +0 -0
  101. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/py.typed +0 -0
  102. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/search/__init__.py +0 -0
  103. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/search/chunking.py +0 -0
  104. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/search/ranking.py +0 -0
  105. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/search/semantic_search.py +0 -0
  106. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/search/similarity.py +0 -0
  107. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/storage/__init__.py +0 -0
  108. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/storage/_mapping.py +0 -0
  109. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/storage/async_client.py +0 -0
  110. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/storage/client.py +0 -0
  111. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/storage/errors.py +0 -0
  112. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/storage/types.py +0 -0
  113. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/utils/__init__.py +0 -0
  114. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/atomicmemory/utils/environment.py +0 -0
  115. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/examples/local_search.py +0 -0
  116. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/__init__.py +0 -0
  117. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/client/__init__.py +0 -0
  118. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/client/test_async_memory_client.py +0 -0
  119. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/client/test_atomic_memory_client.py +0 -0
  120. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/client/test_memory_client.py +0 -0
  121. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/conftest.py +0 -0
  122. {atomicmemory-1.0.1/tests/core → atomicmemory-1.1.0/tests/contract}/__init__.py +0 -0
  123. {atomicmemory-1.0.1/tests/embeddings → atomicmemory-1.1.0/tests/core}/__init__.py +0 -0
  124. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/core/test_errors.py +0 -0
  125. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/core/test_events.py +0 -0
  126. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/core/test_retry.py +0 -0
  127. {atomicmemory-1.0.1/tests/kv_cache → atomicmemory-1.1.0/tests/embeddings}/__init__.py +0 -0
  128. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/embeddings/test_sentence_transformers.py +0 -0
  129. {atomicmemory-1.0.1/tests/memory → atomicmemory-1.1.0/tests/kv_cache}/__init__.py +0 -0
  130. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/kv_cache/test_memory_storage.py +0 -0
  131. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/kv_cache/test_sqlite_storage.py +0 -0
  132. {atomicmemory-1.0.1/tests/providers → atomicmemory-1.1.0/tests/memory}/__init__.py +0 -0
  133. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/memory/test_filters.py +0 -0
  134. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/memory/test_provider_base.py +0 -0
  135. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/memory/test_registry.py +0 -0
  136. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/memory/test_service.py +0 -0
  137. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/memory/test_types.py +0 -0
  138. {atomicmemory-1.0.1/tests/providers/atomicmemory → atomicmemory-1.1.0/tests/providers}/__init__.py +0 -0
  139. {atomicmemory-1.0.1/tests/providers/mem0 → atomicmemory-1.1.0/tests/providers/atomicmemory}/__init__.py +0 -0
  140. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_handle_agents.py +0 -0
  141. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_handle_audit.py +0 -0
  142. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_handle_config.py +0 -0
  143. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_handle_lessons.py +0 -0
  144. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_handle_lifecycle.py +0 -0
  145. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_http.py +0 -0
  146. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/atomicmemory/test_path.py +0 -0
  147. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/hindsight/__init__.py +0 -0
  148. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/hindsight/test_async_provider.py +0 -0
  149. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/hindsight/test_integration.py +0 -0
  150. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/hindsight/test_mappers.py +0 -0
  151. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/hindsight/test_provider.py +0 -0
  152. {atomicmemory-1.0.1/tests/search → atomicmemory-1.1.0/tests/providers/mem0}/__init__.py +0 -0
  153. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/mem0/test_mappers.py +0 -0
  154. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/providers/mem0/test_provider.py +0 -0
  155. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/search/test_chunking.py +0 -0
  156. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/search/test_semantic_search.py +0 -0
  157. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/search/test_similarity.py +0 -0
  158. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/storage/__init__.py +0 -0
  159. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/storage/test_async_storage_client.py +0 -0
  160. {atomicmemory-1.0.1 → atomicmemory-1.1.0}/tests/storage/test_storage_client.py +0 -0
@@ -53,3 +53,9 @@ Thumbs.db
53
53
  *.log
54
54
  .env
55
55
  .env.local
56
+
57
+ # Internal planning hub (specs/plans) — not published
58
+ localdocs/
59
+
60
+ # Local review/feature worktrees
61
+ .worktrees/
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ All notable changes to `atomicmemory` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [1.1.0] - 2026-06-09
10
+
11
+ ### Added
12
+ - `atomicmemory.contract.v1`: a wire codec for the v1 provider contract's deliberately mixed-case encoding (`Memory.createdAt`/`updatedAt` and `SearchResult.rankingScore` are camelCase on the wire; `version_id`, `observed_at`, and retrieval-receipt fields are snake_case). Encode/decode helpers cover `Memory`, `Provenance`, `SearchResult`, `SearchResultPage`, `SearchRequest`, and ingest payloads (`IngestInput`, `IngestResult`). Dates follow the contract's ISO-8601 UTC millisecond `Z` form (`_to_iso_z`, equivalent to TS `toISOString()`). Naive datetimes in encode paths are assumed UTC. `encode_ingest_input` fails closed on the Python-ahead `content_class` field (no place in the v1 `additionalProperties: false` schemas; TS contract alignment is a recorded follow-up). Explicit-null `version_id` in `SearchResult` normalizes to absent on re-encode, matching the TS optional declaration. `encode_search_request` uses `by_alias=True` so Python-keyword-safe combinator field names (`and_`/`or_`/`not_`) emit their wire aliases; a recursive `_jsonify` walk converts any `datetime` operands in filter trees to the toISOString form. In-process models and provider mappers are unchanged.
13
+ - Vendored the TS SDK's versioned v1 wire contract (JSON Schemas, cross-provider conformance corpus, and CONTRACT.md) under `contract/`, with explicit provenance in `contract/VENDORED.json` and a documented refresh script (`scripts/refresh_contract.py`, never run in CI). A pytest conformance harness proves corpus fixtures decode into the Python models (directly for snake-on-wire types, through the codec for the mixed-case search response) and that SDK emissions validate against the vendored draft-2020-12 schemas, with the TS suite's negative cases mirrored against both schemas and Pydantic. The `capabilities-descriptor` case is schema-only (no Python model in this release — recorded follow-up).
14
+ - `atomicmemory.contract` re-exports `v1` as a specialty import surface; deliberately not re-exported from the package root to keep the root namespace focused on the core provider API.
15
+ - `AsyncProviderFactory` now accepts factories that return an `Awaitable[AsyncProviderRegistration]`, enabling lazy or async provider construction during `AsyncMemoryService.initialize()`.
16
+ - `MemoryService.initialize()` and `AsyncMemoryService.initialize()` raise `ConfigError` when the configured default provider has no registered factory, making a misconfigured default an immediate, explicit error rather than a silent no-op.
17
+
18
+ ### Changed
19
+ - `content_class` is now accepted on **every** ingest mode (`text`, `messages`, and `verbatim`), not just `verbatim`, and is forwarded to core for all modes. Extraction-based ingests (`text`/`messages`) can now satisfy a core running the default `RAW_CONTENT_POLICY=reject`. Still never defaulted — omitting it leaves the field off the wire and a reject-policy core fails closed.
20
+ - Both clients' `initialize()` is now concurrency-safe and idempotent: concurrent callers share a single initialization run (the first caller's registry wins), and the completed outcome — success or failure — is captured in loop-independent state for `AsyncMemoryClient`.
21
+ - A failed `initialize()` is sticky: retrying re-raises the original error from any caller; resolve the cause and construct a new client rather than retrying on the same instance.
22
+ - `AsyncMemoryClient.initialize()` shields each waiter from cancellation so that one waiter's timeout or cancellation never cancels the shared run for other concurrent callers.
23
+ - `AsyncMemoryClient.close()` during a pending initialization cancels the shared run; staged providers are torn down by the service's atomic-initialize cleanup, any concurrent `initialize()` waiter receives `CancelledError`, and the client ends in the not-initialized state without recording a sticky error.
24
+ - Both `MemoryService` and `AsyncMemoryService` stage provider registrations atomically: factories and provider `initialize()` calls run against a local staging area, and the maps are replaced only after every provider succeeds; on any failure, already-staged providers are torn down best-effort before the original error re-raises.
25
+ - `MemoryService.close()` and `AsyncMemoryService.close()` are best-effort: every provider gets a chance to close regardless of earlier failures, maps are cleared in a `finally` block, and the first failure is re-raised after all providers have been given the chance to close.
26
+
27
+ ### Fixed
28
+ - `atomicmemory.__version__` reported `1.0.0` while package metadata said `1.0.1`; all version sources now agree at `1.1.0`, guarded by a regression test that will fail if they drift again.
29
+
30
+ ## [1.0.1] - 2026-05-14
31
+
32
+ ### Changed
33
+ - Version bump for public package publication after internal-to-public repository sync.
34
+
35
+ ## [1.0.0]
36
+
37
+ Initial public stable release.
38
+
39
+ ### Added
40
+ - `AtomicMemoryClient` and `AsyncAtomicMemoryClient` as the primary public client surfaces.
41
+ - Memory ingestion, search, package, get, list, and delete support.
42
+ - AtomicMemory, Mem0, and Hindsight provider adapters.
43
+ - Typed AtomicMemory namespace handles for lifecycle, audit, lessons, agents, and runtime config.
44
+ - Direct artifact storage client with pointer and managed artifact workflows.
45
+ - Local embedding, semantic search, and KV cache helpers.
46
+ - Pydantic models, typed exceptions, and `py.typed` marker for downstream type checkers.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atomicmemory
3
- Version: 1.0.1
3
+ Version: 1.1.0
4
4
  Summary: Python client SDK for AtomicMemory memory and artifact storage.
5
5
  Project-URL: Homepage, https://github.com/atomicstrata/atomicmemory-python
6
6
  Project-URL: Repository, https://github.com/atomicstrata/atomicmemory-python
@@ -25,6 +25,7 @@ Requires-Dist: httpx>=0.27
25
25
  Requires-Dist: numpy>=1.26
26
26
  Requires-Dist: pydantic>=2.7
27
27
  Provides-Extra: dev
28
+ Requires-Dist: jsonschema[format]>=4.21; extra == 'dev'
28
29
  Requires-Dist: mypy>=1.10; extra == 'dev'
29
30
  Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
30
31
  Requires-Dist: pytest-mock>=3.12; extra == 'dev'
@@ -70,13 +71,13 @@ pip install 'atomicmemory[embeddings]' # + sentence-transformers for local
70
71
 
71
72
  ## Quick start
72
73
 
73
- Prerequisite: start `atomicmemory-core` first. Follow the [Core Quickstart](https://docs.atomicstrata.ai/quickstart) if you do not already have a backend at `http://localhost:3050`.
74
+ Prerequisite: start `atomicmemory-core` first. Follow the [Core Quickstart](https://docs.atomicstrata.ai/quickstart) if you do not already have a backend at `http://localhost:17350`.
74
75
 
75
76
  ```python
76
77
  from atomicmemory import AtomicMemoryClient
77
78
 
78
79
  with AtomicMemoryClient({
79
- "apiUrl": "http://localhost:3050",
80
+ "apiUrl": "http://localhost:17350",
80
81
  "apiKey": "server-api-key",
81
82
  "userId": "demo",
82
83
  }) as client:
@@ -110,7 +111,7 @@ from atomicmemory import AsyncAtomicMemoryClient
110
111
 
111
112
  async def main() -> None:
112
113
  async with AsyncAtomicMemoryClient({
113
- "apiUrl": "http://localhost:3050",
114
+ "apiUrl": "http://localhost:17350",
114
115
  "apiKey": "server-api-key",
115
116
  "userId": "demo",
116
117
  }) as client:
@@ -169,6 +170,50 @@ The `client.storage` namespace mirrors the TypeScript SDK's direct storage API:
169
170
 
170
171
  Every storage request sends `Authorization: Bearer <apiKey>` and `X-AtomicMemory-User-Id`. The SDK never sends the legacy `?user_id=` URL parameter.
171
172
 
173
+ ## v1 wire contract
174
+
175
+ `atomicmemory.contract.v1` is the wire codec for the v1 provider-contract encoding. The wire form is deliberately mixed-case — `Memory.createdAt`/`updatedAt` and `SearchResult.rankingScore` are camelCase; `version_id`, `observed_at`, and retrieval-receipt fields are snake_case — as pinned by the vendored `contract/CONTRACT.md`. This module is the only place that mapping lives; in-process models and provider mappers are unchanged.
176
+
177
+ ```python
178
+ from atomicmemory.contract import v1
179
+
180
+ # decode a wire search response (e.g. from a cross-SDK provider call)
181
+ wire_page = {
182
+ "results": [
183
+ {
184
+ "memory": {
185
+ "id": "mem_1",
186
+ "content": "I prefer aisle seats on flights.",
187
+ "scope": {"user": "demo"},
188
+ "kind": "fact",
189
+ "createdAt": "2026-05-30T12:00:00.000Z",
190
+ },
191
+ "score": 0.91,
192
+ "rankingScore": 0.87,
193
+ }
194
+ ],
195
+ "retrieval": {
196
+ "embedding_model": "text-embedding-x",
197
+ "embedding_model_version": "1",
198
+ "embedding_dimensions": 1536,
199
+ "query_text": "deploy gate",
200
+ "candidate_ids": ["mem_1"],
201
+ "trace_id": "trace-1",
202
+ },
203
+ }
204
+
205
+ page = v1.decode_search_result_page(wire_page)
206
+ for hit in page.results:
207
+ print(hit.memory.content, hit.score) # snake_case in-process models
208
+
209
+ # re-encode to the exact v1 wire form (millisecond-precision UTC datetimes)
210
+ wire_out = v1.encode_search_result_page(page)
211
+ ```
212
+
213
+ Two behaviors to know: naive datetimes passed to encode functions are assumed UTC (bare `astimezone()` would shift by the host's UTC offset); `encode_ingest_input` rejects models carrying `content_class` with a clear error because the v1 schemas have `additionalProperties: false` and no such field — this is a Python-ahead field pending TS contract alignment.
214
+
215
+ This is NOT the AtomicMemory core HTTP API. That boundary stays in the provider mappers. The import path is `atomicmemory.contract` — deliberately not re-exported from the package root to keep the root namespace focused on the core provider API.
216
+
172
217
  ## Development
173
218
 
174
219
  ```bash
@@ -32,13 +32,13 @@ pip install 'atomicmemory[embeddings]' # + sentence-transformers for local
32
32
 
33
33
  ## Quick start
34
34
 
35
- Prerequisite: start `atomicmemory-core` first. Follow the [Core Quickstart](https://docs.atomicstrata.ai/quickstart) if you do not already have a backend at `http://localhost:3050`.
35
+ Prerequisite: start `atomicmemory-core` first. Follow the [Core Quickstart](https://docs.atomicstrata.ai/quickstart) if you do not already have a backend at `http://localhost:17350`.
36
36
 
37
37
  ```python
38
38
  from atomicmemory import AtomicMemoryClient
39
39
 
40
40
  with AtomicMemoryClient({
41
- "apiUrl": "http://localhost:3050",
41
+ "apiUrl": "http://localhost:17350",
42
42
  "apiKey": "server-api-key",
43
43
  "userId": "demo",
44
44
  }) as client:
@@ -72,7 +72,7 @@ from atomicmemory import AsyncAtomicMemoryClient
72
72
 
73
73
  async def main() -> None:
74
74
  async with AsyncAtomicMemoryClient({
75
- "apiUrl": "http://localhost:3050",
75
+ "apiUrl": "http://localhost:17350",
76
76
  "apiKey": "server-api-key",
77
77
  "userId": "demo",
78
78
  }) as client:
@@ -131,6 +131,50 @@ The `client.storage` namespace mirrors the TypeScript SDK's direct storage API:
131
131
 
132
132
  Every storage request sends `Authorization: Bearer <apiKey>` and `X-AtomicMemory-User-Id`. The SDK never sends the legacy `?user_id=` URL parameter.
133
133
 
134
+ ## v1 wire contract
135
+
136
+ `atomicmemory.contract.v1` is the wire codec for the v1 provider-contract encoding. The wire form is deliberately mixed-case — `Memory.createdAt`/`updatedAt` and `SearchResult.rankingScore` are camelCase; `version_id`, `observed_at`, and retrieval-receipt fields are snake_case — as pinned by the vendored `contract/CONTRACT.md`. This module is the only place that mapping lives; in-process models and provider mappers are unchanged.
137
+
138
+ ```python
139
+ from atomicmemory.contract import v1
140
+
141
+ # decode a wire search response (e.g. from a cross-SDK provider call)
142
+ wire_page = {
143
+ "results": [
144
+ {
145
+ "memory": {
146
+ "id": "mem_1",
147
+ "content": "I prefer aisle seats on flights.",
148
+ "scope": {"user": "demo"},
149
+ "kind": "fact",
150
+ "createdAt": "2026-05-30T12:00:00.000Z",
151
+ },
152
+ "score": 0.91,
153
+ "rankingScore": 0.87,
154
+ }
155
+ ],
156
+ "retrieval": {
157
+ "embedding_model": "text-embedding-x",
158
+ "embedding_model_version": "1",
159
+ "embedding_dimensions": 1536,
160
+ "query_text": "deploy gate",
161
+ "candidate_ids": ["mem_1"],
162
+ "trace_id": "trace-1",
163
+ },
164
+ }
165
+
166
+ page = v1.decode_search_result_page(wire_page)
167
+ for hit in page.results:
168
+ print(hit.memory.content, hit.score) # snake_case in-process models
169
+
170
+ # re-encode to the exact v1 wire form (millisecond-precision UTC datetimes)
171
+ wire_out = v1.encode_search_result_page(page)
172
+ ```
173
+
174
+ Two behaviors to know: naive datetimes passed to encode functions are assumed UTC (bare `astimezone()` would shift by the host's UTC offset); `encode_ingest_input` rejects models carrying `content_class` with a clear error because the v1 schemas have `additionalProperties: false` and no such field — this is a Python-ahead field pending TS contract alignment.
175
+
176
+ This is NOT the AtomicMemory core HTTP API. That boundary stays in the provider mappers. The import path is `atomicmemory.contract` — deliberately not re-exported from the package root to keep the root namespace focused on the core provider API.
177
+
134
178
  ## Development
135
179
 
136
180
  ```bash
@@ -23,11 +23,25 @@ from atomicmemory.core.errors import (
23
23
  RateLimitError,
24
24
  ValidationError,
25
25
  )
26
+ from atomicmemory.memory.capability_profiles import (
27
+ CapabilityGap,
28
+ CapabilityProfile,
29
+ capability_gaps,
30
+ satisfies_profile,
31
+ )
26
32
  from atomicmemory.memory.filters import FieldFilter, FieldFilterOp, FilterExpr
33
+ from atomicmemory.memory.meta_fact_filter import (
34
+ DEFAULT_META_FACT_PATTERNS,
35
+ MetaFactFilterConfig,
36
+ filter_meta_facts,
37
+ is_meta_fact,
38
+ resolve_meta_fact_patterns,
39
+ )
27
40
  from atomicmemory.memory.types import (
28
41
  Capabilities,
29
42
  CapabilitiesExtensions,
30
43
  CapabilitiesRequiredScope,
44
+ ContentClass,
31
45
  ContextPackage,
32
46
  GraphEdge,
33
47
  GraphNode,
@@ -52,6 +66,7 @@ from atomicmemory.memory.types import (
52
66
  PackageRequest,
53
67
  Profile,
54
68
  Provenance,
69
+ RetrievalReceipt,
55
70
  Scope,
56
71
  SearchRequest,
57
72
  SearchResult,
@@ -87,6 +102,7 @@ from atomicmemory.storage import (
87
102
  )
88
103
 
89
104
  __all__ = [
105
+ "DEFAULT_META_FACT_PATTERNS",
90
106
  "ArtifactHead",
91
107
  "ArtifactInUseError",
92
108
  "ArtifactMetadata",
@@ -103,7 +119,10 @@ __all__ = [
103
119
  "Capabilities",
104
120
  "CapabilitiesExtensions",
105
121
  "CapabilitiesRequiredScope",
122
+ "CapabilityGap",
123
+ "CapabilityProfile",
106
124
  "ConfigError",
125
+ "ContentClass",
107
126
  "ContextPackage",
108
127
  "DeleteArtifactOptions",
109
128
  "DeleteArtifactPolicy",
@@ -133,6 +152,7 @@ __all__ = [
133
152
  "Message",
134
153
  "MessageIngest",
135
154
  "MessageRole",
155
+ "MetaFactFilterConfig",
136
156
  "NetworkError",
137
157
  "NotInitializedError",
138
158
  "PackageFormat",
@@ -146,6 +166,7 @@ __all__ = [
146
166
  "PutManagedInput",
147
167
  "PutPointerInput",
148
168
  "RateLimitError",
169
+ "RetrievalReceipt",
149
170
  "Scope",
150
171
  "SearchRequest",
151
172
  "SearchResult",
@@ -163,4 +184,9 @@ __all__ = [
163
184
  "VerificationResult",
164
185
  "VerifyArtifactOptions",
165
186
  "__version__",
187
+ "capability_gaps",
188
+ "filter_meta_facts",
189
+ "is_meta_fact",
190
+ "resolve_meta_fact_patterns",
191
+ "satisfies_profile",
166
192
  ]
@@ -0,0 +1,7 @@
1
+ """Version metadata for the atomicmemory Python SDK.
2
+
3
+ Exports:
4
+ __version__: The current package version string (PEP 440).
5
+ """
6
+
7
+ __version__ = "1.1.0"
@@ -1,5 +1,6 @@
1
1
  """AsyncMemoryClient — async facade for the V3 memory layer.
2
2
 
3
+ Port of `atomicmemory-sdk/src/client/memory-client.ts` (async variant).
3
4
  Mirrors :class:`atomicmemory.client.memory_client.MemoryClient` with
4
5
  ``async def`` for every I/O method and a ``__aenter__`` / ``__aexit__``
5
6
  context manager. Dict coercion + Pydantic-error wrapping is identical
@@ -8,6 +9,8 @@ to the sync client.
8
9
 
9
10
  from __future__ import annotations
10
11
 
12
+ import asyncio
13
+ import contextlib
11
14
  from dataclasses import dataclass
12
15
  from types import TracebackType
13
16
  from typing import Any
@@ -17,11 +20,13 @@ import atomicmemory.providers.atomicmemory
17
20
  import atomicmemory.providers.hindsight
18
21
  import atomicmemory.providers.mem0 # noqa: F401
19
22
  from atomicmemory.client.memory_client import (
23
+ MemoryProviderConfigs,
20
24
  _coerce_ingest,
21
25
  _coerce_list_request,
22
26
  _coerce_package,
23
27
  _coerce_ref,
24
28
  _coerce_search,
29
+ _pick_first_provider_key,
25
30
  )
26
31
  from atomicmemory.core.errors import ConfigError, NotInitializedError
27
32
  from atomicmemory.memory.provider import BaseAsyncMemoryProvider
@@ -42,8 +47,6 @@ from atomicmemory.memory.types import (
42
47
  )
43
48
  from atomicmemory.providers.atomicmemory.async_handle_impl import AsyncAtomicMemoryHandle
44
49
 
45
- MemoryProviderConfigs = dict[str, Any]
46
-
47
50
 
48
51
  @dataclass
49
52
  class AsyncProviderStatus:
@@ -62,7 +65,7 @@ class AsyncMemoryClient:
62
65
 
63
66
  Example:
64
67
  >>> async with AsyncMemoryClient(
65
- ... providers={"atomicmemory": {"api_url": "http://localhost:3050"}}
68
+ ... providers={"atomicmemory": {"api_url": "http://localhost:17350"}}
66
69
  ... ) as memory:
67
70
  ... await memory.initialize()
68
71
  ... await memory.ingest({"mode": "text", "content": "hi", "scope": {"user": "u1"}})
@@ -88,18 +91,82 @@ class AsyncMemoryClient:
88
91
  )
89
92
  )
90
93
  self._initialized = False
94
+ self._init_error: Exception | None = None
95
+ self._init_task: asyncio.Task[None] | None = None
91
96
 
92
97
  async def initialize(self, registry: AsyncProviderRegistry | None = None) -> None:
98
+ """Initialize all configured providers. Idempotent and concurrency-safe.
99
+
100
+ Concurrent calls on one event loop share a single initialization run
101
+ (the first call's ``registry`` wins). The COMPLETED outcome — success
102
+ or the original failure — is captured into loop-independent state, so
103
+ a failed initialization is sticky from any loop: retrying re-raises
104
+ the original error; construct a new client after resolving the cause.
105
+ An instance is bound to the event loop of its first ``initialize()``
106
+ while initialization is still PENDING — awaiting a pending run from a
107
+ different loop is unsupported. ``close()`` after a SUCCESSFUL
108
+ lifecycle returns the client to the uninitialized state.
109
+ """
93
110
  if self._initialized:
94
111
  return
95
- await self._service.initialize(registry if registry is not None else default_async_registry)
112
+ if self._init_error is not None:
113
+ raise self._init_error
114
+ if self._init_task is None:
115
+ self._init_task = asyncio.ensure_future(self._run_initialize(registry))
116
+ self._init_task.add_done_callback(_mark_retrieved)
117
+ task = self._init_task
118
+ try:
119
+ # shield: cancelling ONE waiter (e.g. wait_for timeout) must not
120
+ # cancel the shared run for everyone — promises aren't cancellable
121
+ # in TS, so unshielded awaiting would NOT be lifecycle parity.
122
+ await asyncio.shield(task)
123
+ finally:
124
+ if task.done():
125
+ self._init_task = None
126
+
127
+ async def _run_initialize(self, registry: AsyncProviderRegistry | None) -> None:
128
+ """Execute the shared initialization run; capture errors into sticky state.
129
+
130
+ CancelledError is BaseException and never caught here, so cancellation
131
+ never becomes sticky. A cancelled task's ``_init_task`` slot is cleared
132
+ by a surviving waiter's ``finally`` once the task is done, or by
133
+ ``close()``; either path lets a later call start fresh.
134
+ """
135
+ try:
136
+ await self._service.initialize(registry if registry is not None else default_async_registry)
137
+ except Exception as exc:
138
+ self._init_error = exc
139
+ raise
96
140
  self._initialized = True
97
141
 
98
142
  async def close(self) -> None:
143
+ """Close providers; safe to call multiple times.
144
+
145
+ Closing while an initialization is PENDING cancels that run: staged
146
+ providers are torn down by the service's atomic-initialize cleanup,
147
+ any concurrent initialize() waiter receives CancelledError, and the
148
+ client ends not-initialized (no sticky error is recorded for
149
+ cancellation). After a SUCCESSFUL lifecycle, close() returns the
150
+ client to the uninitialized state. A FAILED initialization remains
151
+ sticky — close() does not reset it.
152
+ """
153
+ task = self._init_task
154
+ if task is not None:
155
+ if not task.done():
156
+ task.cancel()
157
+ with contextlib.suppress(Exception, asyncio.CancelledError):
158
+ await task
159
+ # Always clear, even when already done: a run whose waiters were
160
+ # all cancelled leaves a stale DONE task behind, and a later
161
+ # initialize() awaiting it would resolve instantly WITHOUT
162
+ # re-running — silently leaving the client uninitialized.
163
+ self._init_task = None
99
164
  if not self._initialized:
100
165
  return
101
- await self._service.close()
102
- self._initialized = False
166
+ try:
167
+ await self._service.close()
168
+ finally:
169
+ self._initialized = False
103
170
 
104
171
  async def __aenter__(self) -> AsyncMemoryClient:
105
172
  return self
@@ -117,6 +184,7 @@ class AsyncMemoryClient:
117
184
  return await self._service.ingest(_coerce_ingest(input))
118
185
 
119
186
  async def ingest_direct(self, input: IngestInput | dict[str, Any]) -> IngestResult:
187
+ """Identical to :meth:`ingest`; preserved for wrapper-subclass parity with TS."""
120
188
  self._assert_initialized()
121
189
  return await self._service.ingest(_coerce_ingest(input))
122
190
 
@@ -125,6 +193,7 @@ class AsyncMemoryClient:
125
193
  return await self._service.search(_coerce_search(request))
126
194
 
127
195
  async def search_direct(self, request: SearchRequest | dict[str, Any]) -> SearchResultPage:
196
+ """Identical to :meth:`search`; preserved for wrapper-subclass parity with TS."""
128
197
  self._assert_initialized()
129
198
  return await self._service.search(_coerce_search(request))
130
199
 
@@ -133,6 +202,7 @@ class AsyncMemoryClient:
133
202
  return await self._service.package(_coerce_package(request))
134
203
 
135
204
  async def package_direct(self, request: PackageRequest | dict[str, Any]) -> ContextPackage:
205
+ """Identical to :meth:`package`; preserved for wrapper-subclass parity with TS."""
136
206
  self._assert_initialized()
137
207
  return await self._service.package(_coerce_package(request))
138
208
 
@@ -181,6 +251,11 @@ class AsyncMemoryClient:
181
251
 
182
252
  @property
183
253
  def atomicmemory(self) -> AsyncAtomicMemoryHandle | None:
254
+ """Typed access to AtomicMemory-specific routes.
255
+
256
+ Returns ``None`` when the client is not yet initialized or the
257
+ ``atomicmemory`` provider was not configured.
258
+ """
184
259
  if not self._initialized:
185
260
  return None
186
261
  if "atomicmemory" not in self._service.get_configured_providers():
@@ -196,8 +271,13 @@ class AsyncMemoryClient:
196
271
  raise NotInitializedError("AsyncMemoryClient is not initialized. Call await client.initialize() first.")
197
272
 
198
273
 
199
- def _pick_first_provider_key(providers: MemoryProviderConfigs) -> str | None:
200
- for key, value in providers.items():
201
- if value is not None and key != "default":
202
- return key
203
- return None
274
+ def _mark_retrieved(task: asyncio.Task[None]) -> None:
275
+ """Retrieve the task's exception so asyncio never logs 'never retrieved'.
276
+
277
+ A run whose waiters were all cancelled fails unobserved; without this
278
+ callback asyncio would log "Task exception was never retrieved" at GC.
279
+ Correctness is unchanged: waiters still see errors through the shield,
280
+ and stickiness is recorded by ``_run_initialize`` itself.
281
+ """
282
+ if not task.cancelled():
283
+ task.exception()
@@ -9,6 +9,7 @@ code. Async users get the same surface via
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import threading
12
13
  from dataclasses import dataclass
13
14
  from types import TracebackType
14
15
  from typing import Any
@@ -127,7 +128,7 @@ class MemoryClient:
127
128
  """Sync entry point for the V3 memory API.
128
129
 
129
130
  Example:
130
- >>> with MemoryClient(providers={"atomicmemory": {"api_url": "http://localhost:3050"}}) as memory:
131
+ >>> with MemoryClient(providers={"atomicmemory": {"api_url": "http://localhost:17350"}}) as memory:
131
132
  ... memory.initialize()
132
133
  ... memory.ingest({"mode": "text", "content": "hi", "scope": {"user": "u1"}})
133
134
  """
@@ -151,24 +152,54 @@ class MemoryClient:
151
152
  )
152
153
  )
153
154
  self._initialized = False
155
+ self._init_lock = threading.Lock()
156
+ self._init_error: Exception | None = None
154
157
 
155
158
  # ------------------------------------------------------------------
156
159
  # Lifecycle
157
160
  # ------------------------------------------------------------------
158
161
 
159
162
  def initialize(self, registry: ProviderRegistry | None = None) -> None:
160
- """Initialize all configured providers. Idempotent."""
161
- if self._initialized:
162
- return
163
- self._service.initialize(registry if registry is not None else default_registry)
164
- self._initialized = True
163
+ """Initialize all configured providers. Idempotent and thread-safe.
164
+
165
+ Concurrent and subsequent calls share a single initialization run
166
+ (the first call's ``registry`` wins; later arguments are ignored).
167
+ A FAILED initialization is sticky: retrying re-raises the original
168
+ error — resolve the cause and construct a new client. A successful
169
+ lifecycle stays re-openable: ``close()`` returns the client to the
170
+ uninitialized state. Factories must not call back into this client
171
+ instance; the non-reentrant lock would deadlock.
172
+ """
173
+ with self._init_lock:
174
+ if self._initialized:
175
+ return
176
+ if self._init_error is not None:
177
+ raise self._init_error
178
+ try:
179
+ self._service.initialize(registry if registry is not None else default_registry)
180
+ except Exception as exc:
181
+ # Sticky failures are real initialization errors ONLY —
182
+ # KeyboardInterrupt/SystemExit propagate without poisoning the client.
183
+ self._init_error = exc
184
+ raise
185
+ self._initialized = True
165
186
 
166
187
  def close(self) -> None:
167
- """Close every initialized provider; safe to call multiple times."""
168
- if not self._initialized:
169
- return
170
- self._service.close()
171
- self._initialized = False
188
+ """Close every initialized provider; safe to call multiple times.
189
+
190
+ A pending sync initialize holds the lock, so close() blocks until
191
+ initialization finishes, including any network I/O it performs —
192
+ deterministic by construction. A client that never initialized
193
+ successfully is unaffected: close() is a no-op and the sticky
194
+ initialization error is preserved — construct a new client.
195
+ """
196
+ with self._init_lock:
197
+ if not self._initialized:
198
+ return
199
+ try:
200
+ self._service.close()
201
+ finally:
202
+ self._initialized = False
172
203
 
173
204
  def __enter__(self) -> MemoryClient:
174
205
  return self
@@ -0,0 +1,18 @@
1
+ """atomicmemory.contract — v1 provider-contract wire codec.
2
+
3
+ Re-exports the public codec functions from :mod:`atomicmemory.contract.v1`.
4
+ Import the submodule directly for IDE discoverability:
5
+
6
+ from atomicmemory.contract import v1
7
+
8
+ page = v1.decode_search_result_page(wire_dict)
9
+ wire = v1.encode_search_result_page(page)
10
+
11
+ This package is a specialty surface; it is deliberately NOT re-exported from
12
+ the ``atomicmemory`` package root to keep the root namespace focused on the
13
+ core provider API.
14
+ """
15
+
16
+ from atomicmemory.contract import v1
17
+
18
+ __all__ = ["v1"]