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.
- {memplex-3.2.3 → memplex-3.2.4}/PKG-INFO +1 -1
- {memplex-3.2.3 → memplex-3.2.4}/README.md +2 -2
- {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/.claude-plugin/plugin.json +1 -1
- {memplex-3.2.3 → memplex-3.2.4}/memplex/adapters/agent_installer.py +12 -18
- {memplex-3.2.3 → memplex-3.2.4}/memplex/adapters/mcp_server.py +3 -7
- {memplex-3.2.3 → memplex-3.2.4}/memplex/compaction.py +18 -13
- {memplex-3.2.3 → memplex-3.2.4}/memplex/retrieval/reranker.py +11 -3
- {memplex-3.2.3 → memplex-3.2.4}/memplex.egg-info/PKG-INFO +1 -1
- {memplex-3.2.3 → memplex-3.2.4}/pyproject.toml +1 -1
- {memplex-3.2.3 → memplex-3.2.4}/tests/test_agent_runtime.py +24 -0
- {memplex-3.2.3 → memplex-3.2.4}/tests/test_hooks.py +19 -45
- {memplex-3.2.3 → memplex-3.2.4}/tests/test_install_scripts.py +3 -3
- {memplex-3.2.3 → memplex-3.2.4}/LICENSE +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/__main__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/.mcp.json +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/hooks/hooks.json +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/scripts/hook-runner.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/skills/mem-explore/SKILL.md +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/skills/mem-manage/SKILL.md +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/skills/mem-search/SKILL.md +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/_plugin/skills/mem-write/SKILL.md +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/adapters/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/adapters/agent_runtime.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/adapters/claude_skill.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/adapters/cli.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/adapters/http_api.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/base.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/benchmark_cli.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/evaluator.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/loader.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/locomo.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/memory_eval.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/memory_metrics.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/metrics.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/nq_trivia.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/benchmarks/popqa_hotpot.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/config.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/associator/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/associator/domain_classifier.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/associator/entity_aligner.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/associator/ref_linker.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/associator/term_mapper.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/dictionaries/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/engine.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/extractors/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/extractors/docx.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/extractors/image.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/extractors/markdown.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/extractors/pdf.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/extractors/vision_mapper.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/handlers/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/handlers/clipboard.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/handlers/file_handler.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/handlers/url_handler.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/hooks/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/hooks/collector.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/hooks/hook_event.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/core/hooks/registry.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/enhancer.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/fallback_chain.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/injection_guard.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/provider.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/providers/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/providers/anthropic.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/providers/local.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/providers/rule_based.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/llm/sanitizer.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/logging_utils.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/metrics.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/models/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/models/feedback.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/models/graph.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/models/memory.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/models/misc.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/models/paragraph.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/models/search.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/models/source.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/models/task.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/processing/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/processing/graph_builder.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/processing/merger/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/processing/merger/confidence_calculator.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/processing/merger/conflict_resolver.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/retrieval/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/retrieval/dedup.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/retrieval/embedding.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/service.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/storage/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/storage/base.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/storage/changelog.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/storage/feedback.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/storage/lite/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/storage/lite/store.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/storage/vector.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/wiki/__init__.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/wiki/community.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/wiki/compiler.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/wiki/generator.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/wiki/search.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex/worker.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex.egg-info/SOURCES.txt +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex.egg-info/dependency_links.txt +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex.egg-info/entry_points.txt +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex.egg-info/requires.txt +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/memplex.egg-info/top_level.txt +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/setup.cfg +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/tests/test_agent_hot_paths.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/tests/test_associators.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/tests/test_config.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/tests/test_core_engine.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/tests/test_graph_builder.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/tests/test_llm.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/tests/test_models.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/tests/test_service.py +0 -0
- {memplex-3.2.3 → memplex-3.2.4}/tests/test_storage.py +0 -0
|
@@ -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.
|
|
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.
|
|
55
|
+
uv tool install memplex==3.2.4
|
|
56
56
|
memplex setup
|
|
57
57
|
```
|
|
58
58
|
|
|
@@ -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
|
-
|
|
168
|
-
|
|
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=[
|
|
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.
|
|
946
|
+
return "3.2.4"
|
|
@@ -331,7 +331,7 @@ class MCPServer:
|
|
|
331
331
|
},
|
|
332
332
|
"serverInfo": {
|
|
333
333
|
"name": "memplex",
|
|
334
|
-
"version": "3.2.
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "memplex"
|
|
7
|
-
version = "3.2.
|
|
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.
|
|
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
|
-
|
|
914
|
-
|
|
891
|
+
"{\n"
|
|
892
|
+
" // user-managed config\n"
|
|
915
893
|
' "plugins": {\n'
|
|
916
894
|
' "slots": {"memory": "custom-memory",},\n'
|
|
917
|
-
|
|
918
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|