memwal 0.1.1.dev0__tar.gz → 0.1.2.dev0__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 (22) hide show
  1. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/PKG-INFO +14 -10
  2. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/README.md +13 -9
  3. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/examples/.env.example +4 -2
  4. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/examples/async_remember_demo.py +3 -3
  5. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/examples/interactive_demo.py +3 -3
  6. memwal-0.1.2.dev0/examples/verify_credentials.py +67 -0
  7. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/memwal/__init__.py +1 -1
  8. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/memwal/client.py +21 -3
  9. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/pyproject.toml +1 -1
  10. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/run_tests.py +3 -3
  11. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/tests/test_client.py +41 -0
  12. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/tests/test_integration.py +5 -5
  13. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/.gitignore +0 -0
  14. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/examples/.gitignore +0 -0
  15. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/memwal/compatibility.py +0 -0
  16. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/memwal/middleware.py +0 -0
  17. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/memwal/types.py +0 -0
  18. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/memwal/utils.py +0 -0
  19. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/tests/__init__.py +0 -0
  20. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/tests/test_env_presets.py +0 -0
  21. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/tests/test_middleware.py +0 -0
  22. {memwal-0.1.1.dev0 → memwal-0.1.2.dev0}/tests/test_signing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memwal
3
- Version: 0.1.1.dev0
3
+ Version: 0.1.2.dev0
4
4
  Summary: Python SDK for MemWal — Privacy-first AI memory with Ed25519 signing
5
5
  Project-URL: Homepage, https://memwal.ai
6
6
  Project-URL: Documentation, https://docs.memwal.ai
@@ -62,11 +62,15 @@ pip install memwal[all] # Everything
62
62
  Set your environment variables first:
63
63
 
64
64
  ```bash
65
- export MEMWAL_KEY="your-ed25519-delegate-key-hex"
65
+ export MEMWAL_PRIVATE_KEY="your-ed25519-delegate-private-key-hex"
66
66
  export MEMWAL_ACCOUNT_ID="0x-your-memwal-account-id"
67
67
  export MEMWAL_SERVER_URL="https://relayer.memwal.ai"
68
68
  ```
69
69
 
70
+ `MEMWAL_KEY` is still accepted as a backwards-compatibility alias, but new apps
71
+ should use `MEMWAL_PRIVATE_KEY` so it is clear that the delegate private key is
72
+ the server-side secret.
73
+
70
74
  ### Async (recommended)
71
75
 
72
76
  ```python
@@ -76,7 +80,7 @@ from memwal import MemWal
76
80
 
77
81
  async def main():
78
82
  memwal = MemWal.create(
79
- key=os.environ["MEMWAL_KEY"],
83
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
80
84
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
81
85
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
82
86
  )
@@ -86,7 +90,7 @@ async def main():
86
90
  print(result.blob_id)
87
91
 
88
92
  # Recall memories
89
- matches = await memwal.recall("food allergies")
93
+ matches = await memwal.recall("food allergies", limit=10, max_distance=0.7)
90
94
  for memory in matches.results:
91
95
  print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
92
96
 
@@ -107,7 +111,7 @@ import os
107
111
  from memwal import MemWalSync
108
112
 
109
113
  client = MemWalSync.create(
110
- key=os.environ["MEMWAL_KEY"],
114
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
111
115
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
112
116
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
113
117
  )
@@ -124,7 +128,7 @@ import os
124
128
  from memwal import MemWal
125
129
 
126
130
  async with MemWal.create(
127
- key=os.environ["MEMWAL_KEY"],
131
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
128
132
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
129
133
  ) as memwal:
130
134
  await memwal.remember("I prefer dark mode")
@@ -139,7 +143,7 @@ Same shorthand as the TypeScript SDK and MCP package.
139
143
  from memwal import MemWal
140
144
 
141
145
  memwal = MemWal.create(
142
- key=os.environ["MEMWAL_KEY"],
146
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
143
147
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
144
148
  env="prod", # prod | dev | staging | local
145
149
  )
@@ -169,7 +173,7 @@ from memwal import with_memwal_langchain
169
173
  llm = ChatOpenAI(model="gpt-4o")
170
174
  smart_llm = with_memwal_langchain(
171
175
  llm,
172
- key=os.environ["MEMWAL_KEY"],
176
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
173
177
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
174
178
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
175
179
  max_memories=5,
@@ -190,7 +194,7 @@ from memwal import with_memwal_openai
190
194
  client = AsyncOpenAI()
191
195
  smart_client = with_memwal_openai(
192
196
  client,
193
- key=os.environ["MEMWAL_KEY"],
197
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
194
198
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
195
199
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
196
200
  )
@@ -213,7 +217,7 @@ Create a new async client.
213
217
  | Method | Description |
214
218
  |--------|-------------|
215
219
  | `await remember(text, namespace?)` | Store a memory |
216
- | `await recall(query, limit?, namespace?)` | Search memories |
220
+ | `await recall(query, limit?, namespace?, max_distance?)` | Search memories, optionally filtering by distance |
217
221
  | `await analyze(text, namespace?)` | Extract and store facts |
218
222
  | `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
219
223
  | `await restore(namespace, limit?)` | Restore a namespace |
@@ -23,11 +23,15 @@ pip install memwal[all] # Everything
23
23
  Set your environment variables first:
24
24
 
25
25
  ```bash
26
- export MEMWAL_KEY="your-ed25519-delegate-key-hex"
26
+ export MEMWAL_PRIVATE_KEY="your-ed25519-delegate-private-key-hex"
27
27
  export MEMWAL_ACCOUNT_ID="0x-your-memwal-account-id"
28
28
  export MEMWAL_SERVER_URL="https://relayer.memwal.ai"
29
29
  ```
30
30
 
31
+ `MEMWAL_KEY` is still accepted as a backwards-compatibility alias, but new apps
32
+ should use `MEMWAL_PRIVATE_KEY` so it is clear that the delegate private key is
33
+ the server-side secret.
34
+
31
35
  ### Async (recommended)
32
36
 
33
37
  ```python
@@ -37,7 +41,7 @@ from memwal import MemWal
37
41
 
38
42
  async def main():
39
43
  memwal = MemWal.create(
40
- key=os.environ["MEMWAL_KEY"],
44
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
41
45
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
42
46
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
43
47
  )
@@ -47,7 +51,7 @@ async def main():
47
51
  print(result.blob_id)
48
52
 
49
53
  # Recall memories
50
- matches = await memwal.recall("food allergies")
54
+ matches = await memwal.recall("food allergies", limit=10, max_distance=0.7)
51
55
  for memory in matches.results:
52
56
  print(f"{memory.text} (relevance: {1 - memory.distance:.2f})")
53
57
 
@@ -68,7 +72,7 @@ import os
68
72
  from memwal import MemWalSync
69
73
 
70
74
  client = MemWalSync.create(
71
- key=os.environ["MEMWAL_KEY"],
75
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
72
76
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
73
77
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
74
78
  )
@@ -85,7 +89,7 @@ import os
85
89
  from memwal import MemWal
86
90
 
87
91
  async with MemWal.create(
88
- key=os.environ["MEMWAL_KEY"],
92
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
89
93
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
90
94
  ) as memwal:
91
95
  await memwal.remember("I prefer dark mode")
@@ -100,7 +104,7 @@ Same shorthand as the TypeScript SDK and MCP package.
100
104
  from memwal import MemWal
101
105
 
102
106
  memwal = MemWal.create(
103
- key=os.environ["MEMWAL_KEY"],
107
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
104
108
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
105
109
  env="prod", # prod | dev | staging | local
106
110
  )
@@ -130,7 +134,7 @@ from memwal import with_memwal_langchain
130
134
  llm = ChatOpenAI(model="gpt-4o")
131
135
  smart_llm = with_memwal_langchain(
132
136
  llm,
133
- key=os.environ["MEMWAL_KEY"],
137
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
134
138
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
135
139
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
136
140
  max_memories=5,
@@ -151,7 +155,7 @@ from memwal import with_memwal_openai
151
155
  client = AsyncOpenAI()
152
156
  smart_client = with_memwal_openai(
153
157
  client,
154
- key=os.environ["MEMWAL_KEY"],
158
+ key=os.environ["MEMWAL_PRIVATE_KEY"],
155
159
  account_id=os.environ["MEMWAL_ACCOUNT_ID"],
156
160
  server_url=os.environ.get("MEMWAL_SERVER_URL", "https://relayer.memwal.ai"),
157
161
  )
@@ -174,7 +178,7 @@ Create a new async client.
174
178
  | Method | Description |
175
179
  |--------|-------------|
176
180
  | `await remember(text, namespace?)` | Store a memory |
177
- | `await recall(query, limit?, namespace?)` | Search memories |
181
+ | `await recall(query, limit?, namespace?, max_distance?)` | Search memories, optionally filtering by distance |
178
182
  | `await analyze(text, namespace?)` | Extract and store facts |
179
183
  | `await ask(question, limit?, namespace?)` | Ask a question answered using memories |
180
184
  | `await restore(namespace, limit?)` | Restore a namespace |
@@ -1,7 +1,9 @@
1
1
  # Local server (default) or remote relayer
2
- MEMWAL_SERVER_URL=http://localhost:3001
2
+ MEMWAL_SERVER_URL=http://localhost:8000
3
3
  # Ed25519 delegate private key (64-hex). Get from MemWal dashboard.
4
- MEMWAL_KEY=21b423e72282dcc47805de48ef9130331b642667b7b2a5cd621767928205e360
4
+ MEMWAL_PRIVATE_KEY=21b423e72282dcc47805de48ef9130331b642667b7b2a5cd621767928205e360
5
+ # Optional: paste the dashboard delegate public key so verification can catch mismatches.
6
+ MEMWAL_DELEGATE_PUBLIC_KEY=
5
7
  # MemWalAccount object ID on Sui (the wallet's account)
6
8
  MEMWAL_ACCOUNT_ID=0x8a1121b8f95d79e68bd07efaf71689ce6fd832b369cdb1b2a943ec7beb822392
7
9
  # Namespace for these test memories
@@ -65,13 +65,13 @@ def _ms(start: float) -> int:
65
65
 
66
66
 
67
67
  async def main() -> None:
68
- server_url = os.environ.get("MEMWAL_SERVER_URL", "http://localhost:3001")
69
- key = os.environ.get("MEMWAL_KEY")
68
+ server_url = os.environ.get("MEMWAL_SERVER_URL", "http://localhost:8000")
69
+ key = os.environ.get("MEMWAL_PRIVATE_KEY") or os.environ.get("MEMWAL_KEY")
70
70
  account_id = os.environ.get("MEMWAL_ACCOUNT_ID")
71
71
  namespace = os.environ.get("MEMWAL_NAMESPACE", "python-sdk-example")
72
72
 
73
73
  if not key or not account_id:
74
- print("ERROR: set MEMWAL_KEY + MEMWAL_ACCOUNT_ID in examples/.env")
74
+ print("ERROR: set MEMWAL_PRIVATE_KEY + MEMWAL_ACCOUNT_ID in examples/.env")
75
75
  sys.exit(2)
76
76
 
77
77
  print(f"server : {server_url}")
@@ -94,13 +94,13 @@ def _log_offset() -> int:
94
94
 
95
95
 
96
96
  async def main() -> None:
97
- server_url = os.environ.get("MEMWAL_SERVER_URL", "http://localhost:3001")
98
- key = os.environ.get("MEMWAL_KEY")
97
+ server_url = os.environ.get("MEMWAL_SERVER_URL", "http://localhost:8000")
98
+ key = os.environ.get("MEMWAL_PRIVATE_KEY") or os.environ.get("MEMWAL_KEY")
99
99
  account_id = os.environ.get("MEMWAL_ACCOUNT_ID")
100
100
  namespace = os.environ.get("MEMWAL_NAMESPACE", "python-sdk-example")
101
101
 
102
102
  if not key or not account_id:
103
- print("ERROR: set MEMWAL_KEY + MEMWAL_ACCOUNT_ID in examples/.env")
103
+ print("ERROR: set MEMWAL_PRIVATE_KEY + MEMWAL_ACCOUNT_ID in examples/.env")
104
104
  sys.exit(2)
105
105
 
106
106
  print(f"server : {server_url}")
@@ -0,0 +1,67 @@
1
+ """Verify MemWal example credentials before calling the relayer.
2
+
3
+ Usage:
4
+ MEMWAL_PRIVATE_KEY=<hex> MEMWAL_ACCOUNT_ID=0x... python examples/verify_credentials.py
5
+
6
+ Optional:
7
+ Set MEMWAL_DELEGATE_PUBLIC_KEY to the dashboard public key to fail on
8
+ public/private key mismatch.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import re
15
+ import sys
16
+
17
+ import nacl.signing
18
+
19
+ HEX_32_BYTES = re.compile(r"^(0x)?[0-9a-fA-F]{64}$")
20
+ ACCOUNT_ID = re.compile(r"^0x[0-9a-fA-F]{64}$")
21
+
22
+
23
+ def normalize_hex(value: str) -> str:
24
+ value = value.strip()
25
+ return value[2:] if value.lower().startswith("0x") else value
26
+
27
+
28
+ def main() -> None:
29
+ private_key = os.environ.get("MEMWAL_PRIVATE_KEY") or os.environ.get("MEMWAL_KEY") or ""
30
+ account_id = os.environ.get("MEMWAL_ACCOUNT_ID") or ""
31
+ expected_public_key = os.environ.get("MEMWAL_DELEGATE_PUBLIC_KEY") or ""
32
+ server_url = os.environ.get("MEMWAL_SERVER_URL") or ""
33
+
34
+ if not private_key:
35
+ raise SystemExit("MEMWAL_PRIVATE_KEY is required")
36
+ if not HEX_32_BYTES.match(private_key):
37
+ raise SystemExit("MEMWAL_PRIVATE_KEY must be a 64-character Ed25519 private key hex string")
38
+ if account_id and not ACCOUNT_ID.match(account_id):
39
+ raise SystemExit("MEMWAL_ACCOUNT_ID must be a 0x-prefixed 32-byte Sui object ID")
40
+
41
+ signing_key = nacl.signing.SigningKey(bytes.fromhex(normalize_hex(private_key)))
42
+ derived_public_key = signing_key.verify_key.encode().hex()
43
+
44
+ if expected_public_key and derived_public_key != normalize_hex(expected_public_key).lower():
45
+ raise SystemExit(
46
+ "MEMWAL_PRIVATE_KEY does not derive MEMWAL_DELEGATE_PUBLIC_KEY. "
47
+ "You may have pasted a public key or a key from another account."
48
+ )
49
+
50
+ print("MemWal credentials look parseable.")
51
+ print(f"Derived delegate public key: {derived_public_key}")
52
+ if account_id:
53
+ print(f"Account ID: {account_id}")
54
+ if server_url:
55
+ print(f"Relayer URL: {server_url}")
56
+ if not expected_public_key:
57
+ print("Set MEMWAL_DELEGATE_PUBLIC_KEY to fail on public/private key mismatch.")
58
+
59
+
60
+ if __name__ == "__main__":
61
+ try:
62
+ main()
63
+ except SystemExit:
64
+ raise
65
+ except Exception as exc:
66
+ print(exc, file=sys.stderr)
67
+ raise SystemExit(1) from exc
@@ -112,4 +112,4 @@ __all__ = [
112
112
  "RecallManualResult",
113
113
  ]
114
114
 
115
- __version__ = "0.1.1.dev0"
115
+ __version__ = "0.1.2.dev0"
@@ -79,6 +79,11 @@ from .utils import (
79
79
  T = TypeVar("T")
80
80
  SEAL_SESSION_TTL_MIN = 5
81
81
  SEAL_SESSION_SAFETY_MARGIN_MS = 30_000
82
+ AUTH_REJECTED_MESSAGE = (
83
+ "401 from relayer: typically wrong private key, key not registered on this "
84
+ "account, account ID mismatch, or staging/mainnet mismatch. Check .env.local "
85
+ "and dashboard credentials."
86
+ )
82
87
 
83
88
 
84
89
  # ============================================================
@@ -472,6 +477,7 @@ class MemWal:
472
477
  query: str,
473
478
  limit: int = 10,
474
479
  namespace: Optional[str] = None,
480
+ max_distance: Optional[float] = None,
475
481
  ) -> RecallResult:
476
482
  """Recall memories similar to a query.
477
483
 
@@ -481,6 +487,8 @@ class MemWal:
481
487
  query: Search query.
482
488
  limit: Max number of results (default: 10).
483
489
  namespace: Override the default namespace.
490
+ max_distance: Optional client-side relevance threshold. Memories with
491
+ ``distance >= max_distance`` are dropped.
484
492
 
485
493
  Returns:
486
494
  :class:`RecallResult` with decrypted text results.
@@ -498,6 +506,9 @@ class MemWal:
498
506
  )
499
507
  for m in data.get("results", [])
500
508
  ]
509
+ if max_distance is not None:
510
+ memories = [m for m in memories if m.distance < max_distance]
511
+ return RecallResult(results=memories, total=len(memories))
501
512
  return RecallResult(results=memories, total=data.get("total", len(memories)))
502
513
 
503
514
  async def analyze(self, text: str, namespace: Optional[str] = None) -> AnalyzeResult:
@@ -991,7 +1002,10 @@ class _HttpStatusError(MemWalError):
991
1002
  """
992
1003
 
993
1004
  def __init__(self, status: int, body: str) -> None:
994
- super().__init__(f"MemWal API error ({status}): {body}")
1005
+ if status == 401:
1006
+ super().__init__(AUTH_REJECTED_MESSAGE)
1007
+ else:
1008
+ super().__init__(f"MemWal API error ({status}): {body}")
995
1009
  self.status = status
996
1010
  self.body = body
997
1011
 
@@ -1170,10 +1184,14 @@ class MemWalSync:
1170
1184
  return self._run(self._inner.remember_bulk_and_wait(items, opts))
1171
1185
 
1172
1186
  def recall(
1173
- self, query: str, limit: int = 10, namespace: Optional[str] = None
1187
+ self,
1188
+ query: str,
1189
+ limit: int = 10,
1190
+ namespace: Optional[str] = None,
1191
+ max_distance: Optional[float] = None,
1174
1192
  ) -> RecallResult:
1175
1193
  """Synchronous version of :meth:`MemWal.recall`."""
1176
- return self._run(self._inner.recall(query, limit, namespace))
1194
+ return self._run(self._inner.recall(query, limit, namespace, max_distance))
1177
1195
 
1178
1196
  def analyze(self, text: str, namespace: Optional[str] = None) -> AnalyzeResult:
1179
1197
  """Synchronous version of :meth:`MemWal.analyze`."""
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "memwal"
7
- version = "0.1.1.dev0"
7
+ version = "0.1.2.dev0"
8
8
  description = "Python SDK for MemWal — Privacy-first AI memory with Ed25519 signing"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -8,7 +8,7 @@ Usage:
8
8
  python3 run_tests.py
9
9
 
10
10
  With dev server (integration tests):
11
- MEMWAL_KEY=<hex> MEMWAL_ACCOUNT_ID=0x... MEMWAL_SERVER_URL=https://... python3 run_tests.py
11
+ MEMWAL_PRIVATE_KEY=<hex> MEMWAL_ACCOUNT_ID=0x... MEMWAL_SERVER_URL=https://... python3 run_tests.py
12
12
  """
13
13
 
14
14
  from __future__ import annotations
@@ -27,7 +27,7 @@ DIM = "\033[2m"
27
27
  RESET = "\033[0m"
28
28
 
29
29
  SERVER_URL = os.environ.get("MEMWAL_SERVER_URL", "https://relayer.dev.memwal.ai")
30
- PRIVATE_KEY = os.environ.get("MEMWAL_KEY", "")
30
+ PRIVATE_KEY = os.environ.get("MEMWAL_PRIVATE_KEY") or os.environ.get("MEMWAL_KEY", "")
31
31
  ACCOUNT_ID = os.environ.get("MEMWAL_ACCOUNT_ID", "")
32
32
  HAS_KEY = bool(PRIVATE_KEY and ACCOUNT_ID)
33
33
 
@@ -192,7 +192,7 @@ def main() -> None:
192
192
  print(f" {'─' * 78}")
193
193
 
194
194
  if not HAS_KEY:
195
- print(f"\n {YELLOW}Integration tests skipped — set MEMWAL_KEY + MEMWAL_ACCOUNT_ID to run them{RESET}")
195
+ print(f"\n {YELLOW}Integration tests skipped — set MEMWAL_PRIVATE_KEY + MEMWAL_ACCOUNT_ID to run them{RESET}")
196
196
  print_totals(total_passed, total_failed, all_failures)
197
197
  return
198
198
 
@@ -290,6 +290,31 @@ class TestRecall:
290
290
  assert "x-seal-session" in headers
291
291
  assert "x-delegate-key" not in headers
292
292
 
293
+ @respx.mock
294
+ async def test_max_distance_filters_results(self, memwal_client: MemWal) -> None:
295
+ """recall() should filter weak matches when max_distance is provided."""
296
+ mock_seal_session_prereqs()
297
+ route = respx.post(f"{_TEST_SERVER}/api/recall").mock(
298
+ return_value=httpx.Response(
299
+ 200,
300
+ json={
301
+ "results": [
302
+ {"blob_id": "b1", "text": "I love coffee", "distance": 0.2},
303
+ {"blob_id": "b2", "text": "I live in Tokyo", "distance": 0.7},
304
+ ],
305
+ "total": 2,
306
+ },
307
+ )
308
+ )
309
+
310
+ result = await memwal_client.recall("coffee", limit=10, max_distance=0.7)
311
+
312
+ body = json.loads(route.calls[0].request.content)
313
+ assert body["limit"] == 10
314
+ assert len(result.results) == 1
315
+ assert result.total == 1
316
+ assert result.results[0].blob_id == "b1"
317
+
293
318
  @respx.mock
294
319
  async def test_get_signed_request_uses_empty_body_hash_and_no_wire_body(
295
320
  self, memwal_client: MemWal
@@ -341,6 +366,22 @@ class TestErrorHandling:
341
366
  with pytest.raises(MemWalError, match="401"):
342
367
  await memwal_client.remember("test")
343
368
 
369
+ @respx.mock
370
+ async def test_empty_401_uses_workshop_friendly_message(
371
+ self, memwal_client: MemWal
372
+ ) -> None:
373
+ """Empty-body auth failures should still give actionable guidance."""
374
+ mock_seal_session_prereqs()
375
+ respx.post(f"{_TEST_SERVER}/api/recall").mock(
376
+ return_value=httpx.Response(401, text="")
377
+ )
378
+
379
+ with pytest.raises(
380
+ MemWalError,
381
+ match="wrong private key.*account ID mismatch.*staging/mainnet mismatch",
382
+ ):
383
+ await memwal_client.recall("test")
384
+
344
385
  @respx.mock
345
386
  async def test_500_raises_memwal_error(self, memwal_client: MemWal) -> None:
346
387
  """Server errors should raise MemWalError."""
@@ -12,7 +12,7 @@ No-auth tests (always run, no env vars needed):
12
12
  - Future timestamp → 401
13
13
  - Unregistered key → SDK raises MemWalError
14
14
 
15
- Authenticated tests (require MEMWAL_KEY + MEMWAL_ACCOUNT_ID):
15
+ Authenticated tests (require MEMWAL_PRIVATE_KEY + MEMWAL_ACCOUNT_ID):
16
16
  - remember() acceptance
17
17
  - remember_and_wait()
18
18
  - recall()
@@ -26,10 +26,10 @@ Usage:
26
26
  python -m pytest tests/test_integration.py -v -m "not requires_key"
27
27
 
28
28
  # Run full suite with real credentials
29
- MEMWAL_KEY=<hex> MEMWAL_ACCOUNT_ID=0x... python -m pytest tests/test_integration.py -v
29
+ MEMWAL_PRIVATE_KEY=<hex> MEMWAL_ACCOUNT_ID=0x... python -m pytest tests/test_integration.py -v
30
30
 
31
31
  # Run against dev server using env vars
32
- export MEMWAL_KEY="944aa24c09d8b6d6cc6a8fbedc6dc0942a46e49db7d36596e1b6af6061ec9261"
32
+ export MEMWAL_PRIVATE_KEY="944aa24c09d8b6d6cc6a8fbedc6dc0942a46e49db7d36596e1b6af6061ec9261"
33
33
  export MEMWAL_ACCOUNT_ID="0x70f9a6ff2df0ef6a9ecbfdc3f44c27c289ec3eb0cab5e10a5c07ca6165528565"
34
34
  export MEMWAL_SERVER_URL="https://relayer.dev.memwal.ai"
35
35
  python -m pytest tests/test_integration.py -v
@@ -53,14 +53,14 @@ from memwal.utils import build_signature_message, bytes_to_hex
53
53
  # ── Config ───────────────────────────────────────────────────────────────────
54
54
 
55
55
  SERVER_URL = os.environ.get("MEMWAL_SERVER_URL", "https://relayer.dev.memwal.ai")
56
- PRIVATE_KEY_HEX = os.environ.get("MEMWAL_KEY", "")
56
+ PRIVATE_KEY_HEX = os.environ.get("MEMWAL_PRIVATE_KEY") or os.environ.get("MEMWAL_KEY", "")
57
57
  ACCOUNT_ID = os.environ.get("MEMWAL_ACCOUNT_ID", "")
58
58
 
59
59
  HAS_KEY = bool(PRIVATE_KEY_HEX and ACCOUNT_ID)
60
60
 
61
61
  requires_key = pytest.mark.skipif(
62
62
  not HAS_KEY,
63
- reason="MEMWAL_KEY and MEMWAL_ACCOUNT_ID not set",
63
+ reason="MEMWAL_PRIVATE_KEY and MEMWAL_ACCOUNT_ID not set",
64
64
  )
65
65
 
66
66
  # ── Helpers ───────────────────────────────────────────────────────────────────
File without changes
File without changes
File without changes