memem 2.9.5__py3-none-any.whl
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.
- memem/__init__.py +3 -0
- memem/assembly.py +104 -0
- memem/capabilities.py +153 -0
- memem/cli.py +1166 -0
- memem/cross_encoder_rerank.py +159 -0
- memem/cross_vault.py +253 -0
- memem/decay.py +141 -0
- memem/dreamer.py +1439 -0
- memem/embedding_index.py +411 -0
- memem/eval/__init__.py +4 -0
- memem/eval/canaries.py +217 -0
- memem/eval/eval_set.py +211 -0
- memem/eval/legacy_scorecard.py +166 -0
- memem/eval_capture.py +203 -0
- memem/eval_replay.py +341 -0
- memem/feedback.py +174 -0
- memem/graph_index.py +725 -0
- memem/haiku_prompts.py +132 -0
- memem/io_utils.py +103 -0
- memem/lessons.py +179 -0
- memem/migrate_layers.py +597 -0
- memem/mine-cron.sh +14 -0
- memem/mine_delta.py +1636 -0
- memem/mining.py +316 -0
- memem/models.py +188 -0
- memem/obsidian_store.py +1636 -0
- memem/operations.py +198 -0
- memem/playbook.py +191 -0
- memem/profiles.py +440 -0
- memem/quarantine.py +64 -0
- memem/recall.py +668 -0
- memem/recall_log.py +230 -0
- memem/render.py +73 -0
- memem/retrieve.py +976 -0
- memem/search_index.py +249 -0
- memem/security.py +49 -0
- memem/server.py +315 -0
- memem/session_blocks.py +331 -0
- memem/session_state.py +421 -0
- memem/session_state_db.py +366 -0
- memem/settings.py +32 -0
- memem/status.py +221 -0
- memem/telemetry.py +208 -0
- memem/transcripts.py +756 -0
- memem-2.9.5.dist-info/METADATA +582 -0
- memem-2.9.5.dist-info/RECORD +49 -0
- memem-2.9.5.dist-info/WHEEL +4 -0
- memem-2.9.5.dist-info/entry_points.txt +2 -0
- memem-2.9.5.dist-info/licenses/LICENSE +21 -0
memem/__init__.py
ADDED
memem/assembly.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Explicit assembly projection.
|
|
2
|
+
|
|
3
|
+
context_assemble is the secondary path for building a structured context
|
|
4
|
+
briefing. After m4, it calls the active slice engine 1-2 times, merges the
|
|
5
|
+
resulting slices into a composite "assembled" MemorySlice, and renders it
|
|
6
|
+
via render_slice_markdown.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from memem.models import _normalize_scope_id, now_iso
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger("memem-assembly")
|
|
15
|
+
|
|
16
|
+
# Threshold: if the primary slice has fewer items than this, augment with general scope.
|
|
17
|
+
_SPARSE_THRESHOLD = 5
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _merge_slices(sub_slices: "list[dict]", query: str, project: str) -> "dict":
|
|
21
|
+
"""Fold N item lists into one composite slice dict."""
|
|
22
|
+
from memem.recall import _layer_summary_from_items, _stable_id
|
|
23
|
+
|
|
24
|
+
seen_ids: set[str] = set()
|
|
25
|
+
merged_items: list[dict] = []
|
|
26
|
+
|
|
27
|
+
for sub in sub_slices:
|
|
28
|
+
for item in sub.get("items", []):
|
|
29
|
+
item_id = item.get("id", "")
|
|
30
|
+
if item_id and item_id in seen_ids:
|
|
31
|
+
continue
|
|
32
|
+
if item_id:
|
|
33
|
+
seen_ids.add(item_id)
|
|
34
|
+
merged_items.append(item)
|
|
35
|
+
|
|
36
|
+
layer_summary = _layer_summary_from_items(merged_items)
|
|
37
|
+
|
|
38
|
+
n_subs = len(sub_slices)
|
|
39
|
+
strategy = "primary-only" if n_subs == 1 else "primary+general-augmentation"
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
"slice_id": _stable_id("assembled", {"query": query, "project": project, "generated_at": now_iso()}),
|
|
43
|
+
"scope_id": project,
|
|
44
|
+
"query": query,
|
|
45
|
+
"generated_at": now_iso(),
|
|
46
|
+
"slice_kind": "assembled",
|
|
47
|
+
"items": merged_items,
|
|
48
|
+
"layer_summary": layer_summary,
|
|
49
|
+
"sub_slices": sub_slices,
|
|
50
|
+
"composition_strategy": strategy,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def context_assemble(query: str, project: str = "default") -> str:
|
|
55
|
+
"""Assemble a composite briefing using the v2.0.0 recall pipeline.
|
|
56
|
+
|
|
57
|
+
Calls memory_search for the active project scope, and optionally augments
|
|
58
|
+
with general scope results when the primary result is sparse. Renders via
|
|
59
|
+
the inline recall markdown renderer.
|
|
60
|
+
"""
|
|
61
|
+
from memem.recall import (
|
|
62
|
+
_memory_to_item,
|
|
63
|
+
_render_recall_markdown,
|
|
64
|
+
_search_memories,
|
|
65
|
+
_stable_id,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
normalized = _normalize_scope_id(project)
|
|
69
|
+
|
|
70
|
+
# Primary slice: search active project scope
|
|
71
|
+
primary_mems = _search_memories(
|
|
72
|
+
query, scope_id=normalized, limit=10, record_access=False, expand_links=False
|
|
73
|
+
)
|
|
74
|
+
primary_items = [_memory_to_item(m, include_snippet=True) for m in primary_mems]
|
|
75
|
+
primary_as_sub: dict = {
|
|
76
|
+
"scope_id": normalized,
|
|
77
|
+
"slice_id": _stable_id("assembled-primary", {"query": query, "project": normalized}),
|
|
78
|
+
"items": primary_items,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
sub_slices = [primary_as_sub]
|
|
82
|
+
|
|
83
|
+
# Cross-project augmentation when primary is sparse
|
|
84
|
+
if len(primary_items) < _SPARSE_THRESHOLD and normalized != "general":
|
|
85
|
+
general_mems = _search_memories(
|
|
86
|
+
query, scope_id="general", limit=10, record_access=False, expand_links=False
|
|
87
|
+
)
|
|
88
|
+
general_items = [_memory_to_item(m, include_snippet=True) for m in general_mems]
|
|
89
|
+
if general_items:
|
|
90
|
+
sub_slices.append({
|
|
91
|
+
"scope_id": "general",
|
|
92
|
+
"slice_id": _stable_id("assembled-general", {"query": query}),
|
|
93
|
+
"items": general_items,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
composite = _merge_slices(sub_slices, query=query, project=normalized)
|
|
97
|
+
|
|
98
|
+
# Early return if nothing was assembled
|
|
99
|
+
if not composite.get("items"):
|
|
100
|
+
return ""
|
|
101
|
+
|
|
102
|
+
composite["slice_kind"] = "search"
|
|
103
|
+
composite["query"] = query
|
|
104
|
+
return _render_recall_markdown(composite)
|
memem/capabilities.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Runtime capability detection + serialization.
|
|
2
|
+
|
|
3
|
+
memem writes a small JSON file at ``~/.memem/.capabilities`` during the
|
|
4
|
+
bootstrap shim (bootstrap.sh) and on every ``--doctor`` invocation.
|
|
5
|
+
|
|
6
|
+
The capabilities file is written by ``write_capabilities()`` (called by
|
|
7
|
+
--doctor/bootstrap) and is used by ``pretty_report()`` for diagnostic output.
|
|
8
|
+
The package itself does NOT read this file at runtime — capability checks are
|
|
9
|
+
done live (e.g. shutil.which) at the point of use.
|
|
10
|
+
|
|
11
|
+
Schema (v1)::
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
"schema_version": 1,
|
|
15
|
+
"updated_at": "2026-04-14T12:34:56+00:00",
|
|
16
|
+
"python_version": "3.11.6",
|
|
17
|
+
"mcp": true,
|
|
18
|
+
"claude_cli": true,
|
|
19
|
+
"writable_state_dir": true,
|
|
20
|
+
"writable_vault": true,
|
|
21
|
+
"uv": true,
|
|
22
|
+
"notes": []
|
|
23
|
+
}
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import logging
|
|
30
|
+
import os
|
|
31
|
+
import shutil
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Any
|
|
36
|
+
|
|
37
|
+
from memem.models import MEMEM_DIR, OBSIDIAN_MEMORIES_DIR, now_iso
|
|
38
|
+
|
|
39
|
+
log = logging.getLogger("memem-capabilities")
|
|
40
|
+
|
|
41
|
+
CAPABILITIES_FILE = MEMEM_DIR / ".capabilities"
|
|
42
|
+
SCHEMA_VERSION = 1
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _can_write(path: Path) -> bool:
|
|
46
|
+
"""Probe whether we can actually create and remove a file under ``path``."""
|
|
47
|
+
try:
|
|
48
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
canary = path / ".memem-write-check"
|
|
50
|
+
canary.write_text("ok")
|
|
51
|
+
canary.unlink(missing_ok=True)
|
|
52
|
+
return True
|
|
53
|
+
except OSError:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _mcp_importable() -> bool:
|
|
58
|
+
try:
|
|
59
|
+
import importlib.util
|
|
60
|
+
return importlib.util.find_spec("mcp") is not None
|
|
61
|
+
except Exception:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _claude_cli_available() -> bool:
|
|
66
|
+
if shutil.which("claude") is None:
|
|
67
|
+
return False
|
|
68
|
+
# Extra sanity: make sure it actually runs (some installs are stale symlinks).
|
|
69
|
+
try:
|
|
70
|
+
result = subprocess.run(
|
|
71
|
+
["claude", "--version"],
|
|
72
|
+
capture_output=True, text=True, timeout=3,
|
|
73
|
+
)
|
|
74
|
+
return result.returncode == 0
|
|
75
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _uv_available() -> bool:
|
|
80
|
+
return shutil.which("uv") is not None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def detect_capabilities() -> dict[str, Any]:
|
|
84
|
+
"""Run every probe and return the capabilities dict. Does not write to disk."""
|
|
85
|
+
caps: dict[str, Any] = {
|
|
86
|
+
"schema_version": SCHEMA_VERSION,
|
|
87
|
+
"updated_at": now_iso(),
|
|
88
|
+
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
|
89
|
+
"mcp": _mcp_importable(),
|
|
90
|
+
"claude_cli": _claude_cli_available(),
|
|
91
|
+
"writable_state_dir": _can_write(MEMEM_DIR),
|
|
92
|
+
"writable_vault": _can_write(OBSIDIAN_MEMORIES_DIR),
|
|
93
|
+
"uv": _uv_available(),
|
|
94
|
+
"notes": [],
|
|
95
|
+
}
|
|
96
|
+
return caps
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def write_capabilities(caps: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
100
|
+
"""Detect (if not supplied) and atomically persist to ``~/.memem/.capabilities``."""
|
|
101
|
+
if caps is None:
|
|
102
|
+
caps = detect_capabilities()
|
|
103
|
+
MEMEM_DIR.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
tmp = CAPABILITIES_FILE.with_suffix(".tmp")
|
|
105
|
+
with open(tmp, "w") as fh:
|
|
106
|
+
fh.write(json.dumps(caps, indent=2, sort_keys=True))
|
|
107
|
+
fh.flush()
|
|
108
|
+
os.fsync(fh.fileno())
|
|
109
|
+
os.replace(tmp, CAPABILITIES_FILE)
|
|
110
|
+
return caps
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def pretty_report(caps: dict[str, Any] | None = None) -> str:
|
|
114
|
+
"""Human-readable multi-line report. Used by ``--doctor``."""
|
|
115
|
+
if caps is None:
|
|
116
|
+
caps = detect_capabilities()
|
|
117
|
+
lines = [
|
|
118
|
+
"memem Doctor",
|
|
119
|
+
"=" * 40,
|
|
120
|
+
f" Python version : {caps.get('python_version', '?')}",
|
|
121
|
+
f" mcp importable : {'yes' if caps.get('mcp') else 'NO — pip install mcp'}",
|
|
122
|
+
f" claude CLI on PATH : {'yes' if caps.get('claude_cli') else 'NO — Haiku assembly disabled (degraded)'}",
|
|
123
|
+
f" uv available : {'yes' if caps.get('uv') else 'no (bootstrap.sh will install)'}",
|
|
124
|
+
f" state dir writable : {'yes' if caps.get('writable_state_dir') else 'NO — set MEMEM_DIR env var'}",
|
|
125
|
+
f" vault writable : {'yes' if caps.get('writable_vault') else 'NO — set MEMEM_OBSIDIAN_VAULT env var'}",
|
|
126
|
+
f" updated_at : {caps.get('updated_at', '?')}",
|
|
127
|
+
"=" * 40,
|
|
128
|
+
]
|
|
129
|
+
if caps.get("notes"):
|
|
130
|
+
lines.append("Notes:")
|
|
131
|
+
for note in caps["notes"]:
|
|
132
|
+
lines.append(f" - {note}")
|
|
133
|
+
lines.append("=" * 40)
|
|
134
|
+
|
|
135
|
+
blockers = []
|
|
136
|
+
if not caps.get("mcp"):
|
|
137
|
+
blockers.append("mcp package missing — MCP server cannot start")
|
|
138
|
+
if not caps.get("writable_state_dir"):
|
|
139
|
+
blockers.append("~/.memem is not writable")
|
|
140
|
+
if not caps.get("writable_vault"):
|
|
141
|
+
blockers.append("obsidian vault directory is not writable")
|
|
142
|
+
|
|
143
|
+
if blockers:
|
|
144
|
+
lines.append("BLOCKERS:")
|
|
145
|
+
for b in blockers:
|
|
146
|
+
lines.append(f" ✗ {b}")
|
|
147
|
+
lines.append("=" * 40)
|
|
148
|
+
lines.append("RESULT: FAILING — fix blockers above before first use.")
|
|
149
|
+
else:
|
|
150
|
+
degraded = not caps.get("claude_cli", False)
|
|
151
|
+
status = "DEGRADED (FTS-only recall, no Haiku assembly)" if degraded else "HEALTHY"
|
|
152
|
+
lines.append(f"RESULT: {status}")
|
|
153
|
+
return "\n".join(lines)
|