memplex 3.2.3__tar.gz → 3.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. {memplex-3.2.3 → memplex-3.2.4}/PKG-INFO +1 -1
  2. {memplex-3.2.3 → memplex-3.2.4}/README.md +2 -2
  3. {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/.claude-plugin/plugin.json +1 -1
  4. {memplex-3.2.3 → memplex-3.2.4}/memplex/adapters/agent_installer.py +12 -18
  5. {memplex-3.2.3 → memplex-3.2.4}/memplex/adapters/mcp_server.py +3 -7
  6. {memplex-3.2.3 → memplex-3.2.4}/memplex/compaction.py +18 -13
  7. {memplex-3.2.3 → memplex-3.2.4}/memplex/retrieval/reranker.py +11 -3
  8. {memplex-3.2.3 → memplex-3.2.4}/memplex.egg-info/PKG-INFO +1 -1
  9. {memplex-3.2.3 → memplex-3.2.4}/pyproject.toml +1 -1
  10. {memplex-3.2.3 → memplex-3.2.4}/tests/test_agent_runtime.py +24 -0
  11. {memplex-3.2.3 → memplex-3.2.4}/tests/test_hooks.py +19 -45
  12. {memplex-3.2.3 → memplex-3.2.4}/tests/test_install_scripts.py +3 -3
  13. {memplex-3.2.3 → memplex-3.2.4}/LICENSE +0 -0
  14. {memplex-3.2.3 → memplex-3.2.4}/memplex/__init__.py +0 -0
  15. {memplex-3.2.3 → memplex-3.2.4}/memplex/__main__.py +0 -0
  16. {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/.mcp.json +0 -0
  17. {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/__init__.py +0 -0
  18. {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/hooks/hooks.json +0 -0
  19. {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/scripts/hook-runner.py +0 -0
  20. {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/skills/mem-explore/SKILL.md +0 -0
  21. {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/skills/mem-manage/SKILL.md +0 -0
  22. {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/skills/mem-search/SKILL.md +0 -0
  23. {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/skills/mem-write/SKILL.md +0 -0
  24. {memplex-3.2.3 → memplex-3.2.4}/memplex/adapters/__init__.py +0 -0
  25. {memplex-3.2.3 → memplex-3.2.4}/memplex/adapters/agent_runtime.py +0 -0
  26. {memplex-3.2.3 → memplex-3.2.4}/memplex/adapters/claude_skill.py +0 -0
  27. {memplex-3.2.3 → memplex-3.2.4}/memplex/adapters/cli.py +0 -0
  28. {memplex-3.2.3 → memplex-3.2.4}/memplex/adapters/http_api.py +0 -0
  29. {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/__init__.py +0 -0
  30. {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/base.py +0 -0
  31. {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/benchmark_cli.py +0 -0
  32. {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/evaluator.py +0 -0
  33. {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/loader.py +0 -0
  34. {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/locomo.py +0 -0
  35. {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/memory_eval.py +0 -0
  36. {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/memory_metrics.py +0 -0
  37. {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/metrics.py +0 -0
  38. {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/nq_trivia.py +0 -0
  39. {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/popqa_hotpot.py +0 -0
  40. {memplex-3.2.3 → memplex-3.2.4}/memplex/config.py +0 -0
  41. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/__init__.py +0 -0
  42. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/associator/__init__.py +0 -0
  43. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/associator/domain_classifier.py +0 -0
  44. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/associator/entity_aligner.py +0 -0
  45. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/associator/ref_linker.py +0 -0
  46. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/associator/term_mapper.py +0 -0
  47. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/dictionaries/__init__.py +0 -0
  48. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/engine.py +0 -0
  49. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/extractors/__init__.py +0 -0
  50. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/extractors/docx.py +0 -0
  51. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/extractors/image.py +0 -0
  52. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/extractors/markdown.py +0 -0
  53. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/extractors/pdf.py +0 -0
  54. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/extractors/vision_mapper.py +0 -0
  55. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/handlers/__init__.py +0 -0
  56. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/handlers/clipboard.py +0 -0
  57. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/handlers/file_handler.py +0 -0
  58. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/handlers/url_handler.py +0 -0
  59. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/hooks/__init__.py +0 -0
  60. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/hooks/collector.py +0 -0
  61. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/hooks/hook_event.py +0 -0
  62. {memplex-3.2.3 → memplex-3.2.4}/memplex/core/hooks/registry.py +0 -0
  63. {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/__init__.py +0 -0
  64. {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/enhancer.py +0 -0
  65. {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/fallback_chain.py +0 -0
  66. {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/injection_guard.py +0 -0
  67. {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/provider.py +0 -0
  68. {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/providers/__init__.py +0 -0
  69. {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/providers/anthropic.py +0 -0
  70. {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/providers/local.py +0 -0
  71. {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/providers/rule_based.py +0 -0
  72. {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/sanitizer.py +0 -0
  73. {memplex-3.2.3 → memplex-3.2.4}/memplex/logging_utils.py +0 -0
  74. {memplex-3.2.3 → memplex-3.2.4}/memplex/metrics.py +0 -0
  75. {memplex-3.2.3 → memplex-3.2.4}/memplex/models/__init__.py +0 -0
  76. {memplex-3.2.3 → memplex-3.2.4}/memplex/models/feedback.py +0 -0
  77. {memplex-3.2.3 → memplex-3.2.4}/memplex/models/graph.py +0 -0
  78. {memplex-3.2.3 → memplex-3.2.4}/memplex/models/memory.py +0 -0
  79. {memplex-3.2.3 → memplex-3.2.4}/memplex/models/misc.py +0 -0
  80. {memplex-3.2.3 → memplex-3.2.4}/memplex/models/paragraph.py +0 -0
  81. {memplex-3.2.3 → memplex-3.2.4}/memplex/models/search.py +0 -0
  82. {memplex-3.2.3 → memplex-3.2.4}/memplex/models/source.py +0 -0
  83. {memplex-3.2.3 → memplex-3.2.4}/memplex/models/task.py +0 -0
  84. {memplex-3.2.3 → memplex-3.2.4}/memplex/processing/__init__.py +0 -0
  85. {memplex-3.2.3 → memplex-3.2.4}/memplex/processing/graph_builder.py +0 -0
  86. {memplex-3.2.3 → memplex-3.2.4}/memplex/processing/merger/__init__.py +0 -0
  87. {memplex-3.2.3 → memplex-3.2.4}/memplex/processing/merger/confidence_calculator.py +0 -0
  88. {memplex-3.2.3 → memplex-3.2.4}/memplex/processing/merger/conflict_resolver.py +0 -0
  89. {memplex-3.2.3 → memplex-3.2.4}/memplex/retrieval/__init__.py +0 -0
  90. {memplex-3.2.3 → memplex-3.2.4}/memplex/retrieval/dedup.py +0 -0
  91. {memplex-3.2.3 → memplex-3.2.4}/memplex/retrieval/embedding.py +0 -0
  92. {memplex-3.2.3 → memplex-3.2.4}/memplex/service.py +0 -0
  93. {memplex-3.2.3 → memplex-3.2.4}/memplex/storage/__init__.py +0 -0
  94. {memplex-3.2.3 → memplex-3.2.4}/memplex/storage/base.py +0 -0
  95. {memplex-3.2.3 → memplex-3.2.4}/memplex/storage/changelog.py +0 -0
  96. {memplex-3.2.3 → memplex-3.2.4}/memplex/storage/feedback.py +0 -0
  97. {memplex-3.2.3 → memplex-3.2.4}/memplex/storage/lite/__init__.py +0 -0
  98. {memplex-3.2.3 → memplex-3.2.4}/memplex/storage/lite/store.py +0 -0
  99. {memplex-3.2.3 → memplex-3.2.4}/memplex/storage/vector.py +0 -0
  100. {memplex-3.2.3 → memplex-3.2.4}/memplex/wiki/__init__.py +0 -0
  101. {memplex-3.2.3 → memplex-3.2.4}/memplex/wiki/community.py +0 -0
  102. {memplex-3.2.3 → memplex-3.2.4}/memplex/wiki/compiler.py +0 -0
  103. {memplex-3.2.3 → memplex-3.2.4}/memplex/wiki/generator.py +0 -0
  104. {memplex-3.2.3 → memplex-3.2.4}/memplex/wiki/search.py +0 -0
  105. {memplex-3.2.3 → memplex-3.2.4}/memplex/worker.py +0 -0
  106. {memplex-3.2.3 → memplex-3.2.4}/memplex.egg-info/SOURCES.txt +0 -0
  107. {memplex-3.2.3 → memplex-3.2.4}/memplex.egg-info/dependency_links.txt +0 -0
  108. {memplex-3.2.3 → memplex-3.2.4}/memplex.egg-info/entry_points.txt +0 -0
  109. {memplex-3.2.3 → memplex-3.2.4}/memplex.egg-info/requires.txt +0 -0
  110. {memplex-3.2.3 → memplex-3.2.4}/memplex.egg-info/top_level.txt +0 -0
  111. {memplex-3.2.3 → memplex-3.2.4}/setup.cfg +0 -0
  112. {memplex-3.2.3 → memplex-3.2.4}/tests/test_agent_hot_paths.py +0 -0
  113. {memplex-3.2.3 → memplex-3.2.4}/tests/test_associators.py +0 -0
  114. {memplex-3.2.3 → memplex-3.2.4}/tests/test_config.py +0 -0
  115. {memplex-3.2.3 → memplex-3.2.4}/tests/test_core_engine.py +0 -0
  116. {memplex-3.2.3 → memplex-3.2.4}/tests/test_graph_builder.py +0 -0
  117. {memplex-3.2.3 → memplex-3.2.4}/tests/test_llm.py +0 -0
  118. {memplex-3.2.3 → memplex-3.2.4}/tests/test_models.py +0 -0
  119. {memplex-3.2.3 → memplex-3.2.4}/tests/test_service.py +0 -0
  120. {memplex-3.2.3 → memplex-3.2.4}/tests/test_storage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memplex
3
- Version: 3.2.3
3
+ Version: 3.2.4
4
4
  Summary: Memplex - Memory Complex: multi-agent knowledge graph memory system with 3-layer retrieval
5
5
  Requires-Python: >=3.11
6
6
  License-File: LICENSE
@@ -44,7 +44,7 @@ npx memplex uninstall --agent all
44
44
  ```
45
45
 
46
46
  The npm wrapper creates a persistent Python environment at
47
- `~/.local/share/memplex/agent-venv`, installs `memplex==3.2.3`, detects local
47
+ `~/.local/share/memplex/agent-venv`, installs `memplex==3.2.4`, detects local
48
48
  Codex, Claude Code, OpenClaw, and Hermes config directories/commands, then
49
49
  registers Memplex with each detected agent. It uses `uv` when available and
50
50
  falls back to `python -m venv` plus `pip`.
@@ -52,7 +52,7 @@ falls back to `python -m venv` plus `pip`.
52
52
  Python-first users can use a persistent tool install:
53
53
 
54
54
  ```bash
55
- uv tool install memplex==3.2.3
55
+ uv tool install memplex==3.2.4
56
56
  memplex setup
57
57
  ```
58
58
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memplex",
3
- "version": "3.2.3",
3
+ "version": "3.2.4",
4
4
  "description": "Multi-agent memory system -- persistent knowledge graph with 3-layer retrieval, compaction, and wiki",
5
5
  "author": {
6
6
  "name": "articultur"
@@ -164,8 +164,8 @@ def _install_codex(target_dir: str | Path | None, *, dry_run: bool) -> AgentInst
164
164
  "[mcp_servers.memplex]",
165
165
  f'command = "{_python_command()}"',
166
166
  'args = ["-m", "memplex.adapters.mcp_server"]',
167
- 'startup_timeout_sec = 10',
168
- 'tool_timeout_sec = 60',
167
+ "startup_timeout_sec = 10",
168
+ "tool_timeout_sec = 60",
169
169
  MANAGED_END,
170
170
  "",
171
171
  ]
@@ -358,14 +358,10 @@ def _uninstall_openclaw(
358
358
  is_managed_entry = _is_managed_openclaw_entry(memplex_entry)
359
359
  config_changed = False
360
360
  previous_memory_slot = (
361
- memplex_entry.get("config", {})
362
- .get("managed", {})
363
- .get("previousMemorySlot")
361
+ memplex_entry.get("config", {}).get("managed", {}).get("previousMemorySlot")
364
362
  )
365
363
  added_allow_entry = (
366
- memplex_entry.get("config", {})
367
- .get("managed", {})
368
- .get("addedAllowEntry", False)
364
+ memplex_entry.get("config", {}).get("managed", {}).get("addedAllowEntry", False)
369
365
  )
370
366
  if is_managed_entry and slots.get("memory") == "memplex":
371
367
  if previous_memory_slot:
@@ -381,11 +377,7 @@ def _uninstall_openclaw(
381
377
  config_changed = True
382
378
  if config_changed and not dry_run:
383
379
  _write_json(config_path, config)
384
- if (
385
- extension_dir.exists()
386
- and _is_managed_openclaw_extension(extension_dir)
387
- and not dry_run
388
- ):
380
+ if extension_dir.exists() and _is_managed_openclaw_extension(extension_dir) and not dry_run:
389
381
  shutil.rmtree(extension_dir)
390
382
  return AgentInstallResult(
391
383
  agent="openclaw",
@@ -438,7 +430,11 @@ def _install_hermes(
438
430
  agent="hermes",
439
431
  action="install",
440
432
  status="planned" if dry_run else "installed",
441
- files=[str(provider_path), str(plugin_dir / "plugin.yaml"), str(plugin_dir / "__init__.py")],
433
+ files=[
434
+ str(provider_path),
435
+ str(plugin_dir / "plugin.yaml"),
436
+ str(plugin_dir / "__init__.py"),
437
+ ],
442
438
  message="Installed Memplex Hermes memory provider plugin and descriptor.",
443
439
  next_steps=["Restart Hermes and select the memplex memory provider."],
444
440
  )
@@ -629,9 +625,7 @@ def compact_memories(context):
629
625
  )
630
626
 
631
627
 
632
- def _write_hermes_provider_plugin(
633
- plugin_dir: Path, provider_config: dict[str, Any]
634
- ) -> None:
628
+ def _write_hermes_provider_plugin(plugin_dir: Path, provider_config: dict[str, Any]) -> None:
635
629
  plugin_dir.mkdir(parents=True, exist_ok=True)
636
630
  (plugin_dir / "plugin.yaml").write_text(
637
631
  "\n".join(
@@ -949,4 +943,4 @@ def _package_version() -> str:
949
943
  try:
950
944
  return pkg_version("memplex")
951
945
  except Exception:
952
- return "3.2.3"
946
+ return "3.2.4"
@@ -331,7 +331,7 @@ class MCPServer:
331
331
  },
332
332
  "serverInfo": {
333
333
  "name": "memplex",
334
- "version": "3.2.3",
334
+ "version": "3.2.4",
335
335
  },
336
336
  }
337
337
 
@@ -353,9 +353,7 @@ class MCPServer:
353
353
  "content": [
354
354
  {
355
355
  "type": "text",
356
- "text": json.dumps(
357
- result, default=str, ensure_ascii=False, indent=2
358
- ),
356
+ "text": json.dumps(result, default=str, ensure_ascii=False, indent=2),
359
357
  }
360
358
  ],
361
359
  }
@@ -400,9 +398,7 @@ class MCPServer:
400
398
  )
401
399
  return {
402
400
  "total": len(result.results),
403
- "scope": result.scope.value
404
- if hasattr(result.scope, "value")
405
- else str(result.scope),
401
+ "scope": result.scope.value if hasattr(result.scope, "value") else str(result.scope),
406
402
  "latency_ms": result.latency_ms,
407
403
  "results": [
408
404
  {
@@ -29,7 +29,16 @@ import logging
29
29
  import os
30
30
  import time
31
31
  from dataclasses import dataclass
32
- from datetime import datetime
32
+ from datetime import datetime, timezone
33
+
34
+
35
+ def _ensure_aware(dt: datetime) -> datetime:
36
+ """Normalize a datetime to offset-aware UTC for safe arithmetic."""
37
+ if dt.tzinfo is None:
38
+ return dt.replace(tzinfo=timezone.utc)
39
+ return dt
40
+
41
+
33
42
  from pathlib import Path
34
43
  from typing import TYPE_CHECKING, List, Optional
35
44
 
@@ -251,9 +260,7 @@ class CompactionPipeline:
251
260
  skipped=False,
252
261
  )
253
262
 
254
- async def _execute_stage(
255
- self, stage: str, scope: CompactionScope
256
- ) -> CompactionStageResult:
263
+ async def _execute_stage(self, stage: str, scope: CompactionScope) -> CompactionStageResult:
257
264
  """Dispatch to the correct stage handler."""
258
265
  handlers = {
259
266
  "extract": self._execute_extract,
@@ -334,9 +341,7 @@ class CompactionPipeline:
334
341
 
335
342
  # Sort by weight * observation composite score
336
343
  def _score(fv: FieldValue) -> float:
337
- return fv.weight * (
338
- fv.observation if fv.observation is not None else 1.0
339
- )
344
+ return fv.weight * (fv.observation if fv.observation is not None else 1.0)
340
345
 
341
346
  values.sort(key=_score, reverse=True)
342
347
  for fv in values[max_values:]:
@@ -376,7 +381,7 @@ class CompactionPipeline:
376
381
 
377
382
  removed = 0
378
383
  processed = len(functions)
379
- now = datetime.now()
384
+ now = datetime.now(timezone.utc)
380
385
 
381
386
  for func in functions:
382
387
  should_delete = False
@@ -394,7 +399,7 @@ class CompactionPipeline:
394
399
  except (ValueError, TypeError):
395
400
  updated = None
396
401
  if updated is not None:
397
- age_days = (now - updated).days
402
+ age_days = (now - _ensure_aware(updated)).days
398
403
  if age_days > max_age_days and func.access_count < min_access:
399
404
  should_delete = True
400
405
 
@@ -406,7 +411,7 @@ class CompactionPipeline:
406
411
  review_until = datetime.fromisoformat(review_until)
407
412
  except (ValueError, TypeError):
408
413
  review_until = None
409
- if review_until is not None and now > review_until:
414
+ if review_until is not None and now > _ensure_aware(review_until):
410
415
  should_delete = True
411
416
  elif review_until is None:
412
417
  # No expiry set -- use TTL from creation
@@ -416,7 +421,7 @@ class CompactionPipeline:
416
421
  created = datetime.fromisoformat(created)
417
422
  except (ValueError, TypeError):
418
423
  created = None
419
- if created is not None and (now - created).days > review_ttl:
424
+ if created is not None and (now - _ensure_aware(created)).days > review_ttl:
420
425
  should_delete = True
421
426
 
422
427
  # Prune deprecated FieldValue entries (not the whole Function)
@@ -457,7 +462,7 @@ class CompactionPipeline:
457
462
 
458
463
  functions = self._store.list_functions(limit=100000)
459
464
  max_age_days = self._config.compaction.prune_max_age_days
460
- now = datetime.now()
465
+ now = datetime.now(timezone.utc)
461
466
  archived = 0
462
467
 
463
468
  for func in functions:
@@ -471,7 +476,7 @@ class CompactionPipeline:
471
476
  if updated is None:
472
477
  continue
473
478
 
474
- age_days = (now - updated).days
479
+ age_days = (now - _ensure_aware(updated)).days
475
480
  if age_days > max_age_days and func.access_count == 0:
476
481
  # Write to archive
477
482
  archive_file = archive_dir / f"{func.id}.json"
@@ -23,7 +23,15 @@ from __future__ import annotations
23
23
 
24
24
  import logging
25
25
  import math
26
- from datetime import datetime
26
+ from datetime import datetime, timezone
27
+
28
+
29
+ def _ensure_aware(dt: datetime) -> datetime:
30
+ if dt.tzinfo is None:
31
+ return dt.replace(tzinfo=timezone.utc)
32
+ return dt
33
+
34
+
27
35
  from typing import TYPE_CHECKING, Dict, List, Optional
28
36
 
29
37
  from memplex.models import SearchResult, SourceType
@@ -181,7 +189,7 @@ class Reranker:
181
189
  updated_at = datetime.fromisoformat(updated_at)
182
190
  except (ValueError, TypeError):
183
191
  return 0.5
184
- days_since = max(0, (datetime.now() - updated_at).days)
192
+ days_since = max(0, (datetime.now(timezone.utc) - _ensure_aware(updated_at)).days)
185
193
  return min(1.0, math.exp(-days_since / 60))
186
194
 
187
195
  def _source_weight(self, source_type: SourceType) -> float:
@@ -212,7 +220,7 @@ class Reranker:
212
220
  except (ValueError, TypeError):
213
221
  last_accessed = None
214
222
  if last_accessed is not None:
215
- days = max(0, (datetime.now() - last_accessed).days)
223
+ days = max(0, (datetime.now(timezone.utc) - _ensure_aware(last_accessed)).days)
216
224
  recency = min(1.0, math.exp(-days / 60))
217
225
  else:
218
226
  recency = 0.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memplex
3
- Version: 3.2.3
3
+ Version: 3.2.4
4
4
  Summary: Memplex - Memory Complex: multi-agent knowledge graph memory system with 3-layer retrieval
5
5
  Requires-Python: >=3.11
6
6
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memplex"
7
- version = "3.2.3"
7
+ version = "3.2.4"
8
8
  description = "Memplex - Memory Complex: multi-agent knowledge graph memory system with 3-layer retrieval"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
@@ -46,6 +46,30 @@ def test_turn_loop_captures_and_recalls_without_manual_write(tmp_path):
46
46
  assert recalled.source == "live"
47
47
 
48
48
 
49
+ def test_recall_tolerates_mixed_timestamp_awareness_after_capture(tmp_path):
50
+ from memplex.adapters.agent_runtime import AgentMemoryRuntime
51
+
52
+ service = _make_service(tmp_path / "memory.json")
53
+ runtime = AgentMemoryRuntime(
54
+ service=service,
55
+ agent="codex",
56
+ user_id="user-1",
57
+ session_id="session-1",
58
+ top_k=5,
59
+ )
60
+
61
+ runtime.after_response(
62
+ user_message="I prefer timezone aware release status updates.",
63
+ assistant_message="Captured.",
64
+ )
65
+
66
+ first = runtime.before_prompt("How should release status updates be written?")
67
+ second = runtime.before_prompt("How should release status updates be written?")
68
+
69
+ assert "timezone aware release status updates" in first.context
70
+ assert "timezone aware release status updates" in second.context
71
+
72
+
49
73
  def test_hermes_prefetch_returns_cached_context_for_next_turn(tmp_path):
50
74
  from memplex.adapters.agent_runtime import AgentMemoryRuntime
51
75
 
@@ -40,9 +40,7 @@ def mcp_server(tmp_path):
40
40
  return MCPServer(config=cfg)
41
41
 
42
42
 
43
- HOOK_RUNNER = str(
44
- Path(__file__).resolve().parent.parent / "plugin" / "scripts" / "hook-runner.py"
45
- )
43
+ HOOK_RUNNER = str(Path(__file__).resolve().parent.parent / "plugin" / "scripts" / "hook-runner.py")
46
44
  PROJECT_ROOT = Path(__file__).resolve().parent.parent
47
45
 
48
46
 
@@ -65,9 +63,7 @@ class TestHookRunner:
65
63
  test_env = {
66
64
  **os.environ,
67
65
  "MEMPLEX_STORAGE_BACKEND": "lite",
68
- "MEMPLEX_PLUGIN_ROOT": str(
69
- Path(__file__).resolve().parent.parent / "plugin"
70
- ),
66
+ "MEMPLEX_PLUGIN_ROOT": str(Path(__file__).resolve().parent.parent / "plugin"),
71
67
  "MEMPLEX_PROJECT_ROOT": str(PROJECT_ROOT),
72
68
  }
73
69
  if env:
@@ -107,9 +103,7 @@ class TestHookRunner:
107
103
 
108
104
  def test_observation_exits_zero(self):
109
105
  stdin_data = '{"tool_name":"Write","tool_input":{"file_path":"/tmp/test.py"}}'
110
- r = self._run_hook(
111
- "observation", ["Write", "test-session-obs"], stdin_data=stdin_data
112
- )
106
+ r = self._run_hook("observation", ["Write", "test-session-obs"], stdin_data=stdin_data)
113
107
  assert r.returncode == 0, f"stderr: {r.stderr}"
114
108
 
115
109
  def test_observation_empty_stdin_exits_zero(self):
@@ -201,9 +195,7 @@ class TestHookRunner:
201
195
  )
202
196
  assert capture.returncode == 0, capture.stderr
203
197
 
204
- stdin_data = json.dumps(
205
- {"tool_input": {"file_path": str(tmp_path / "nested.py")}}
206
- )
198
+ stdin_data = json.dumps({"tool_input": {"file_path": str(tmp_path / "nested.py")}})
207
199
  r = self._run_hook(
208
200
  "file-context",
209
201
  stdin_data=stdin_data,
@@ -255,7 +247,7 @@ class TestMCPServerProtocol:
255
247
  result = mcp_server._handle_initialize({})
256
248
  assert result["protocolVersion"] == "2024-11-05"
257
249
  assert result["serverInfo"]["name"] == "memplex"
258
- assert result["serverInfo"]["version"] == "3.2.3"
250
+ assert result["serverInfo"]["version"] == "3.2.4"
259
251
  assert "tools" in result["capabilities"]
260
252
 
261
253
  def test_tools_list_returns_definitions(self, mcp_server):
@@ -288,17 +280,13 @@ class TestMCPServerProtocol:
288
280
  tools = mcp_server._handle_tools_list({})["tools"]
289
281
  search_tool = next(t for t in tools if t["name"] == "memory_search")
290
282
  assert (
291
- "ALWAYS" in search_tool["description"]
292
- or "filter" in search_tool["description"].lower()
283
+ "ALWAYS" in search_tool["description"] or "filter" in search_tool["description"].lower()
293
284
  )
294
285
 
295
286
  def test_get_tool_emphasizes_after_search(self, mcp_server):
296
287
  tools = mcp_server._handle_tools_list({})["tools"]
297
288
  get_tool = next(t for t in tools if t["name"] == "memory_get")
298
- assert (
299
- "AFTER" in get_tool["description"]
300
- or "search" in get_tool["description"].lower()
301
- )
289
+ assert "AFTER" in get_tool["description"] or "search" in get_tool["description"].lower()
302
290
 
303
291
 
304
292
  class TestMCPServerTools:
@@ -473,9 +461,7 @@ class TestMCPServerTools:
473
461
  cfg_path = tmp_path / "memplex.yaml"
474
462
  cfg_path.write_text("storage:\n backend: lite\n path: '%s'" % str(tmp_path))
475
463
 
476
- init_msg = json.dumps(
477
- {"jsonrpc": "2.0", "method": "initialize", "params": {}, "id": 1}
478
- )
464
+ init_msg = json.dumps({"jsonrpc": "2.0", "method": "initialize", "params": {}, "id": 1})
479
465
  search_msg = json.dumps(
480
466
  {
481
467
  "jsonrpc": "2.0",
@@ -552,14 +538,10 @@ class TestCLI:
552
538
  assert r.returncode == 0
553
539
 
554
540
  def test_write_text_command(self):
555
- r = self._run_cli(
556
- ["write", "--text", "CLI写入测试:当系统启动时,加载配置文件"]
557
- )
541
+ r = self._run_cli(["write", "--text", "CLI写入测试:当系统启动时,加载配置文件"])
558
542
  assert r.returncode == 0
559
543
  assert (
560
- "function" in r.stdout.lower()
561
- or "extracted" in r.stdout.lower()
562
- or "func_" in r.stdout
544
+ "function" in r.stdout.lower() or "extracted" in r.stdout.lower() or "func_" in r.stdout
563
545
  )
564
546
 
565
547
  def test_json_output(self):
@@ -729,9 +711,7 @@ class TestCLI:
729
711
  assert config["plugins"]["slots"]["memory"] == "memplex"
730
712
  assert config["plugins"]["entries"]["memplex"]["config"]["userId"] == "alice"
731
713
  assert config["plugins"]["entries"]["memplex"]["config"]["projectPath"] == "/repo/a"
732
- plugin = json.loads(
733
- (target / "extensions" / "memplex" / "plugin.json").read_text()
734
- )
714
+ plugin = json.loads((target / "extensions" / "memplex" / "plugin.json").read_text())
735
715
  openclaw_plugin = json.loads(
736
716
  (target / "extensions" / "memplex" / "openclaw.plugin.json").read_text()
737
717
  )
@@ -790,9 +770,7 @@ class TestCLI:
790
770
  config = json.loads((target / "openclaw.json").read_text())
791
771
  assert config["plugins"]["slots"]["memory"] == "memplex"
792
772
  assert (
793
- config["plugins"]["entries"]["memplex"]["config"]["managed"][
794
- "previousMemorySlot"
795
- ]
773
+ config["plugins"]["entries"]["memplex"]["config"]["managed"]["previousMemorySlot"]
796
774
  == "existing-memory"
797
775
  )
798
776
 
@@ -910,12 +888,12 @@ class TestCLI:
910
888
  target.mkdir()
911
889
  config_path = target / "openclaw.json"
912
890
  original = (
913
- '{\n'
914
- ' // user-managed config\n'
891
+ "{\n"
892
+ " // user-managed config\n"
915
893
  ' "plugins": {\n'
916
894
  ' "slots": {"memory": "custom-memory",},\n'
917
- ' },\n'
918
- '}\n'
895
+ " },\n"
896
+ "}\n"
919
897
  )
920
898
  config_path.write_text(original)
921
899
 
@@ -1293,22 +1271,18 @@ class TestPluginConfig:
1293
1271
  Path(PROJECT_ROOT / "plugin" / ".claude-plugin" / "plugin.json").read_text()
1294
1272
  )
1295
1273
  assert data["name"] == "memplex"
1296
- assert data["version"] == "3.2.3"
1274
+ assert data["version"] == "3.2.4"
1297
1275
  assert "repository" in data
1298
1276
 
1299
1277
  def test_hooks_json_valid(self):
1300
- data = json.loads(
1301
- Path(PROJECT_ROOT / "plugin" / "hooks" / "hooks.json").read_text()
1302
- )
1278
+ data = json.loads(Path(PROJECT_ROOT / "plugin" / "hooks" / "hooks.json").read_text())
1303
1279
  assert "hooks" in data
1304
1280
  assert "SessionStart" in data["hooks"]
1305
1281
  assert "PostToolUse" in data["hooks"]
1306
1282
  assert "Stop" in data["hooks"]
1307
1283
 
1308
1284
  def test_hooks_json_has_timeout(self):
1309
- data = json.loads(
1310
- Path(PROJECT_ROOT / "plugin" / "hooks" / "hooks.json").read_text()
1311
- )
1285
+ data = json.loads(Path(PROJECT_ROOT / "plugin" / "hooks" / "hooks.json").read_text())
1312
1286
  for hook_group in data["hooks"]["SessionStart"]:
1313
1287
  for hook in hook_group["hooks"]:
1314
1288
  assert "timeout" in hook
@@ -161,11 +161,11 @@ def test_agent_installer_all_uses_transactional_cli_path(tmp_path):
161
161
  def test_npm_hermes_installer_package_shape():
162
162
  memplex_package = json.loads(NPM_MEMPLEX_PACKAGE.read_text())
163
163
  assert memplex_package["name"] == "memplex"
164
- assert memplex_package["version"] == "3.2.3"
164
+ assert memplex_package["version"] == "3.2.4"
165
165
  assert memplex_package["bin"]["memplex"] == "bin/memplex.js"
166
166
  memplex_script = NPM_MEMPLEX_BIN.read_text()
167
167
  assert "npx memplex setup" in memplex_script
168
- assert "memplex==3.2.3" in memplex_script
168
+ assert "memplex==3.2.4" in memplex_script
169
169
 
170
170
  agent_package = json.loads(NPM_AGENT_PACKAGE.read_text())
171
171
  assert agent_package["name"] == "@articultur/memplex-agent-installer"
@@ -209,7 +209,7 @@ def test_npm_memplex_setup_runs_hosted_installer_dry_run(tmp_path):
209
209
  },
210
210
  )
211
211
  assert result.returncode == 0, result.stderr
212
- assert "memplex==3.2.3" in result.stdout
212
+ assert "memplex==3.2.4" in result.stdout
213
213
  assert "-m memplex agent install --agent codex" in result.stdout
214
214
  assert "--project-path /repo/a" in result.stdout
215
215
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes