agent-memory-os 0.2.4__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/PKG-INFO +24 -12
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/README.md +23 -11
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/pyproject.toml +1 -1
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/cli.py +19 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/client.py +38 -1
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/db.py +245 -28
- agent_memory_os-0.3.0/src/agent_memory_os/embedding.py +114 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/web_app.py +29 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/web_ui.py +53 -1
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_fts_triggers.py +3 -0
- agent_memory_os-0.3.0/tests/test_persistence_v03.py +170 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/.github/workflows/ci.yml +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/.github/workflows/release.yml +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/.gitignore +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/CHANGELOG.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/LICENSE +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/NOTICE +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/PROGRESS.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/PROJECT_STATUS.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/SPEC.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/assets/README.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/assets/agent-memory-os-logo-ai-v1.png +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/assets/agent-memory-os-logo-integrated-v2.png +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/assets/agent-memory-os-logo.png +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/assets/agent-memory-os-logo.svg +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/assets/agent-memory-os-wordmark.png +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/assets/agent-memory-os-wordmark.svg +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/assets/integrations/claude-code.svg +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/assets/integrations/codex.svg +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/assets/integrations/hermes-agent.svg +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/assets/integrations/openclaw.svg +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/assets/logo-header.png +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/assets/logo-icon-128.png +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/assets/logo-icon-512.png +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/AgentMemoryOS_README.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/CANARY_MONITORING_SCHEDULE.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/HISTORY.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/INSTALLATION.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/Memory_Resonance_Prototype.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/Memory_Resonance_Spec.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/PROJECT_STATUS.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/ROADMAP.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/Roadmap.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/Shadow_Mode_Timeline.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/Shadow_Mode_Verification_Report.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/adr/ADR-0001-turbovec-semantic-sidecar-positioning.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/blockers/current.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/evidence/20260609_120837-repo-audit.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/evidence/20260609_121749-activation-gate-verification.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/evidence/20260617_221742-webui-availability-triage.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/evidence/20260617_225724-webui-port-8000-correction.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/handoffs/20260609_120837-mizuki-to-neo-governance-fill.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/hermes-activation-gates.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/integrations/claude-code.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/integrations/codex.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/integrations/hermes-agent.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/integrations/openclaw.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/plans/20260605_100751-memory-decay-recency-v0.2.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/plans/20260605_114049-retrieval-foundation-v0.2.1.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/plans/20260605_143442-version-downgrade-verification.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/plans/20260608_174539-turbovec-semantic-provider-validation.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/plans/20260613_dco-technical-design.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/portfolio-status.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/project-status/current.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/reviews/.gitkeep +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/reviews/20260609_120837-neo-consistency-review.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/reviews/20260609_122032-mizuki-portfolio-maintenance.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/docs/stress-cases/case-01-noisy-truth.md +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/logo.png +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/scripts/verify_acl_identities.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/scripts/verify_downgrade_compatibility.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/__init__.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/cache.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/candidates.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/context_pack.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/golden_recall.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/hermes_importer.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/mcp_server.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/memory_resonance.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/providers/__init__.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/providers/turbovec.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/schema.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/scoring.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/shadow_mode.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/src/agent_memory_os/tokens.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_acl_visibility.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_cli.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_cli_tools.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_context_orchestration.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_context_pack.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_decay_scoring.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_golden_recall.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_hermes_importer.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_memory_client.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_memory_decay_recency.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_memory_enhancements.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_memory_links.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_memory_resonance.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_retrieval_foundation.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_semantic_candidates.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_shadow_mode.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_truth_arbitration.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_turbovec_provider.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_verification_script.py +0 -0
- {agent_memory_os-0.2.4 → agent_memory_os-0.3.0}/tests/test_web_app.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-memory-os
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Local-first, MCP-native memory engine for AI agents with RAM cache and context-budgeted recall.
|
|
5
5
|
Author: Bastet Agent Team
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -140,21 +140,33 @@ AgentMemoryOS uses **two storage layers with strictly different authority**:
|
|
|
140
140
|
ACL/expiry hard gates before its content can be used, and the index can be
|
|
141
141
|
dropped and rebuilt at any time without touching the truth store.
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
provider in:
|
|
143
|
+
Semantic recall works out of the box:
|
|
145
144
|
|
|
146
145
|
```python
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
provider = TurbovecSemanticCandidateProvider.from_vectors(
|
|
150
|
-
vectors=embeddings, # one row per memory, any embedder
|
|
151
|
-
external_id_by_memory_id=id_mapping, # stable memory_id -> uint64
|
|
152
|
-
embed_query=embed, # str -> vector, same embedder
|
|
153
|
-
)
|
|
154
|
-
client = MemoryClient(home="~/.agent-memory", candidate_providers=[provider])
|
|
146
|
+
client = MemoryClient(home="~/.agent-memory", semantic="auto")
|
|
155
147
|
```
|
|
156
148
|
|
|
157
|
-
`
|
|
149
|
+
`semantic="auto"` wires in a self-syncing turbovec index over a built-in
|
|
150
|
+
deterministic hashing embedder (no model downloads; typo- and
|
|
151
|
+
morphology-tolerant lexical vectors). The index rebuilds itself whenever the
|
|
152
|
+
memories table changes and degrades silently to lexical + resonance recall
|
|
153
|
+
when the backend isn't installed. For deeper semantics, plug any embedding
|
|
154
|
+
model into `TurbovecSemanticCandidateProvider.from_vectors(...)` with your
|
|
155
|
+
own `embed_query`. `agent-memory doctor` confirms the backend is importable.
|
|
156
|
+
|
|
157
|
+
## Memory lifecycle & retention
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
agent-memory retention # archive expired + memories idle 4+ half-lives
|
|
161
|
+
agent-memory retention --half-lives 0 # expired only
|
|
162
|
+
agent-memory check # SQLite + FTS + link-graph integrity
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Archived memories leave recall entirely but stay restorable (Web UI → Tools →
|
|
166
|
+
Retention & archive, or `POST /api/archive/{id}/restore`). Pinned and
|
|
167
|
+
authority-track memories are never archived by decay. Databases self-migrate
|
|
168
|
+
through a versioned, forward-only migration table (`agent-memory check`
|
|
169
|
+
reports the schema version).
|
|
158
170
|
|
|
159
171
|
## Backup & restore
|
|
160
172
|
|
|
@@ -109,21 +109,33 @@ AgentMemoryOS uses **two storage layers with strictly different authority**:
|
|
|
109
109
|
ACL/expiry hard gates before its content can be used, and the index can be
|
|
110
110
|
dropped and rebuilt at any time without touching the truth store.
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
provider in:
|
|
112
|
+
Semantic recall works out of the box:
|
|
114
113
|
|
|
115
114
|
```python
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
provider = TurbovecSemanticCandidateProvider.from_vectors(
|
|
119
|
-
vectors=embeddings, # one row per memory, any embedder
|
|
120
|
-
external_id_by_memory_id=id_mapping, # stable memory_id -> uint64
|
|
121
|
-
embed_query=embed, # str -> vector, same embedder
|
|
122
|
-
)
|
|
123
|
-
client = MemoryClient(home="~/.agent-memory", candidate_providers=[provider])
|
|
115
|
+
client = MemoryClient(home="~/.agent-memory", semantic="auto")
|
|
124
116
|
```
|
|
125
117
|
|
|
126
|
-
`
|
|
118
|
+
`semantic="auto"` wires in a self-syncing turbovec index over a built-in
|
|
119
|
+
deterministic hashing embedder (no model downloads; typo- and
|
|
120
|
+
morphology-tolerant lexical vectors). The index rebuilds itself whenever the
|
|
121
|
+
memories table changes and degrades silently to lexical + resonance recall
|
|
122
|
+
when the backend isn't installed. For deeper semantics, plug any embedding
|
|
123
|
+
model into `TurbovecSemanticCandidateProvider.from_vectors(...)` with your
|
|
124
|
+
own `embed_query`. `agent-memory doctor` confirms the backend is importable.
|
|
125
|
+
|
|
126
|
+
## Memory lifecycle & retention
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
agent-memory retention # archive expired + memories idle 4+ half-lives
|
|
130
|
+
agent-memory retention --half-lives 0 # expired only
|
|
131
|
+
agent-memory check # SQLite + FTS + link-graph integrity
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Archived memories leave recall entirely but stay restorable (Web UI → Tools →
|
|
135
|
+
Retention & archive, or `POST /api/archive/{id}/restore`). Pinned and
|
|
136
|
+
authority-track memories are never archived by decay. Databases self-migrate
|
|
137
|
+
through a versioned, forward-only migration table (`agent-memory check`
|
|
138
|
+
reports the schema version).
|
|
127
139
|
|
|
128
140
|
## Backup & restore
|
|
129
141
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agent-memory-os"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Local-first, MCP-native memory engine for AI agents with RAM cache and context-budgeted recall."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -67,6 +67,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
67
67
|
restore = sub.add_parser("restore", help="Restore the memory database from a backup file")
|
|
68
68
|
restore.add_argument("src", help="Backup .db file to restore from")
|
|
69
69
|
restore.add_argument("--force", action="store_true", help="Overwrite an existing database")
|
|
70
|
+
|
|
71
|
+
sub.add_parser("check", help="Run database integrity and invariant checks")
|
|
72
|
+
|
|
73
|
+
retention = sub.add_parser("retention", help="Archive expired and deeply-decayed memories")
|
|
74
|
+
retention.add_argument(
|
|
75
|
+
"--half-lives", type=float, default=None,
|
|
76
|
+
help="Also archive unpinned memories idle for N decay half-lives (default 4; 0 = expired only)",
|
|
77
|
+
)
|
|
70
78
|
return p
|
|
71
79
|
|
|
72
80
|
|
|
@@ -229,6 +237,17 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
229
237
|
|
|
230
238
|
client = MemoryClient(home=args.home)
|
|
231
239
|
try:
|
|
240
|
+
if args.command == "check":
|
|
241
|
+
report = client.integrity_check()
|
|
242
|
+
print(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True))
|
|
243
|
+
return 0 if report["ok"] else 1
|
|
244
|
+
if args.command == "retention":
|
|
245
|
+
if args.half_lives is None:
|
|
246
|
+
result = client.run_retention()
|
|
247
|
+
else:
|
|
248
|
+
result = client.run_retention(decayed_half_lives=args.half_lives or None)
|
|
249
|
+
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
|
|
250
|
+
return 0
|
|
232
251
|
if args.command == "golden-recall":
|
|
233
252
|
cases = load_golden_query_cases(args.cases)
|
|
234
253
|
report = evaluate_golden_queries(
|
|
@@ -9,7 +9,7 @@ import json
|
|
|
9
9
|
from .candidates import CandidateProvider
|
|
10
10
|
from .cache import LRUCache
|
|
11
11
|
from .context_pack import ContextPackReport, build_context_pack, build_context_pack_report
|
|
12
|
-
from .db import MemoryStore
|
|
12
|
+
from .db import MemoryStore, RETENTION_MIN_HALF_LIVES
|
|
13
13
|
from .schema import MemoryLink, MemoryRecord, RecallProfile, SearchResult
|
|
14
14
|
|
|
15
15
|
class MemoryClient:
|
|
@@ -22,6 +22,7 @@ class MemoryClient:
|
|
|
22
22
|
resonance_hops: int = 1,
|
|
23
23
|
profile: RecallProfile | None = None,
|
|
24
24
|
check_same_thread: bool = True,
|
|
25
|
+
semantic: str | None = None,
|
|
25
26
|
):
|
|
26
27
|
home_path = Path(home or os.getenv("AGENT_MEMORY_HOME", "~/.agent-memory")).expanduser()
|
|
27
28
|
self.home = home_path
|
|
@@ -31,6 +32,20 @@ class MemoryClient:
|
|
|
31
32
|
resonance_hops=resonance_hops,
|
|
32
33
|
check_same_thread=check_same_thread,
|
|
33
34
|
)
|
|
35
|
+
self.semantic_enabled = False
|
|
36
|
+
if semantic == "auto":
|
|
37
|
+
# Out-of-the-box semantic recall: hashing embedder + self-syncing
|
|
38
|
+
# turbovec index. Degrades silently when the backend isn't
|
|
39
|
+
# installed — lexical/resonance recall still works.
|
|
40
|
+
from .providers.turbovec import semantic_backend_available
|
|
41
|
+
|
|
42
|
+
if semantic_backend_available():
|
|
43
|
+
from .embedding import AutoSemanticIndex
|
|
44
|
+
|
|
45
|
+
self.store.candidate_providers.append(AutoSemanticIndex(self.store))
|
|
46
|
+
self.semantic_enabled = True
|
|
47
|
+
elif semantic is not None:
|
|
48
|
+
raise ValueError('semantic must be "auto" or None')
|
|
34
49
|
self.profile = profile
|
|
35
50
|
self.cache: LRUCache[tuple, object] = LRUCache(max_items=cache_items)
|
|
36
51
|
self._profile_cache: dict[str, RecallProfile | None] = {}
|
|
@@ -64,6 +79,28 @@ class MemoryClient:
|
|
|
64
79
|
self.cache.clear()
|
|
65
80
|
return result
|
|
66
81
|
|
|
82
|
+
def run_retention(
|
|
83
|
+
self, *, decayed_half_lives: float | None = RETENTION_MIN_HALF_LIVES
|
|
84
|
+
) -> dict[str, int]:
|
|
85
|
+
"""Archive expired (and optionally deeply-decayed) memories.
|
|
86
|
+
|
|
87
|
+
`decayed_half_lives=None` (or 0) archives expired memories only.
|
|
88
|
+
"""
|
|
89
|
+
result = self.store.run_retention(decayed_half_lives=decayed_half_lives)
|
|
90
|
+
self.cache.clear()
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
def list_archived(self, *, limit: int = 20, offset: int = 0) -> list[dict[str, object]]:
|
|
94
|
+
return self.store.list_archived(limit=limit, offset=offset)
|
|
95
|
+
|
|
96
|
+
def restore_archived(self, memory_id: str) -> MemoryRecord:
|
|
97
|
+
restored = self.store.restore_archived(memory_id)
|
|
98
|
+
self.cache.clear()
|
|
99
|
+
return restored
|
|
100
|
+
|
|
101
|
+
def integrity_check(self) -> dict[str, object]:
|
|
102
|
+
return self.store.integrity_check()
|
|
103
|
+
|
|
67
104
|
def dashboard_stats(self) -> dict[str, object]:
|
|
68
105
|
return self.store.dashboard_stats() | {"cache_items": len(self.cache)}
|
|
69
106
|
|
|
@@ -99,6 +99,73 @@ AUTO_LINK_LIMIT = 5
|
|
|
99
99
|
CONSOLIDATION_MIN_CLUSTER_WEIGHT = 0.6
|
|
100
100
|
CONSOLIDATION_MIN_ACTIVATIONS = 3
|
|
101
101
|
CONSOLIDATION_MIN_CLUSTER_SIZE = 3
|
|
102
|
+
RETENTION_MIN_HALF_LIVES = 4.0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _migration_decay_columns(conn: sqlite3.Connection) -> None:
|
|
106
|
+
existing = {row["name"] for row in conn.execute("PRAGMA table_info(memories)")}
|
|
107
|
+
columns = {
|
|
108
|
+
"decay_policy": "TEXT NOT NULL DEFAULT 'exponential'",
|
|
109
|
+
"decay_half_life_days": "REAL NOT NULL DEFAULT 30.0",
|
|
110
|
+
"last_accessed_at": "TEXT",
|
|
111
|
+
"access_count": "INTEGER NOT NULL DEFAULT 0",
|
|
112
|
+
"pinned": "INTEGER NOT NULL DEFAULT 0",
|
|
113
|
+
}
|
|
114
|
+
for name, definition in columns.items():
|
|
115
|
+
if name not in existing:
|
|
116
|
+
conn.execute(f"ALTER TABLE memories ADD COLUMN {name} {definition}")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _migration_fix_fts_triggers(conn: sqlite3.Connection) -> None:
|
|
120
|
+
# Legacy AFTER UPDATE/DELETE triggers used the FTS5 'delete' command,
|
|
121
|
+
# which is invalid on regular FTS5 tables and raised 'SQL logic error'
|
|
122
|
+
# on every update_content()/delete().
|
|
123
|
+
rows = conn.execute(
|
|
124
|
+
"SELECT name, sql FROM sqlite_master WHERE type = 'trigger' AND name IN ('memories_ad', 'memories_au')"
|
|
125
|
+
).fetchall()
|
|
126
|
+
broken = [row["name"] for row in rows if "'delete'" in (row["sql"] or "")]
|
|
127
|
+
if not broken:
|
|
128
|
+
return
|
|
129
|
+
for name in broken:
|
|
130
|
+
conn.execute(f"DROP TRIGGER IF EXISTS {name}")
|
|
131
|
+
conn.executescript(SCHEMA)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _migration_archive_table(conn: sqlite3.Connection) -> None:
|
|
135
|
+
conn.execute(
|
|
136
|
+
"""
|
|
137
|
+
CREATE TABLE IF NOT EXISTS memories_archive (
|
|
138
|
+
id TEXT PRIMARY KEY,
|
|
139
|
+
owner TEXT NOT NULL,
|
|
140
|
+
scope TEXT NOT NULL,
|
|
141
|
+
type TEXT NOT NULL,
|
|
142
|
+
content TEXT NOT NULL,
|
|
143
|
+
summary TEXT NOT NULL,
|
|
144
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
145
|
+
visibility TEXT NOT NULL DEFAULT '[]',
|
|
146
|
+
source TEXT NOT NULL DEFAULT '{}',
|
|
147
|
+
confidence REAL NOT NULL DEFAULT 0.8,
|
|
148
|
+
importance REAL NOT NULL DEFAULT 0.5,
|
|
149
|
+
created_at TEXT NOT NULL,
|
|
150
|
+
updated_at TEXT NOT NULL,
|
|
151
|
+
expires_at TEXT,
|
|
152
|
+
decay_policy TEXT NOT NULL DEFAULT 'exponential',
|
|
153
|
+
decay_half_life_days REAL NOT NULL DEFAULT 30.0,
|
|
154
|
+
last_accessed_at TEXT,
|
|
155
|
+
access_count INTEGER NOT NULL DEFAULT 0,
|
|
156
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
157
|
+
archived_at TEXT NOT NULL,
|
|
158
|
+
archive_reason TEXT NOT NULL
|
|
159
|
+
)
|
|
160
|
+
"""
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
MIGRATIONS: list[tuple[int, str, object]] = [
|
|
165
|
+
(1, "decay and reinforcement columns", _migration_decay_columns),
|
|
166
|
+
(2, "repair FTS update/delete triggers", _migration_fix_fts_triggers),
|
|
167
|
+
(3, "cold archive table", _migration_archive_table),
|
|
168
|
+
]
|
|
102
169
|
|
|
103
170
|
|
|
104
171
|
class MemoryStore:
|
|
@@ -128,43 +195,70 @@ class MemoryStore:
|
|
|
128
195
|
self.conn.execute("PRAGMA busy_timeout=5000")
|
|
129
196
|
self.conn.execute("PRAGMA journal_mode=WAL")
|
|
130
197
|
self.conn.executescript(SCHEMA)
|
|
131
|
-
self.
|
|
132
|
-
self._ensure_valid_fts_triggers()
|
|
198
|
+
self._run_migrations()
|
|
133
199
|
self.conn.commit()
|
|
134
200
|
|
|
135
201
|
def close(self) -> None:
|
|
136
202
|
self.conn.close()
|
|
137
203
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
204
|
+
# ------------------------------------------------------------------ #
|
|
205
|
+
# Schema migrations: forward-only, versioned, recorded per database.
|
|
206
|
+
# Each migration must be idempotent (it may run once against databases
|
|
207
|
+
# created before the migrations table existed).
|
|
208
|
+
# ------------------------------------------------------------------ #
|
|
209
|
+
|
|
210
|
+
def _run_migrations(self) -> None:
|
|
211
|
+
self.conn.execute(
|
|
212
|
+
"""
|
|
213
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
214
|
+
version INTEGER PRIMARY KEY,
|
|
215
|
+
description TEXT NOT NULL,
|
|
216
|
+
applied_at TEXT NOT NULL
|
|
217
|
+
)
|
|
218
|
+
"""
|
|
219
|
+
)
|
|
220
|
+
applied = {
|
|
221
|
+
row["version"] for row in self.conn.execute("SELECT version FROM schema_migrations")
|
|
146
222
|
}
|
|
147
|
-
for
|
|
148
|
-
if
|
|
149
|
-
|
|
223
|
+
for version, description, migrate in MIGRATIONS:
|
|
224
|
+
if version in applied:
|
|
225
|
+
continue
|
|
226
|
+
migrate(self.conn)
|
|
227
|
+
self.conn.execute(
|
|
228
|
+
"INSERT INTO schema_migrations(version, description, applied_at) VALUES (?, ?, ?)",
|
|
229
|
+
(version, description, utc_now()),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def schema_version(self) -> int:
|
|
233
|
+
row = self.conn.execute("SELECT MAX(version) FROM schema_migrations").fetchone()
|
|
234
|
+
return int(row[0] or 0)
|
|
150
235
|
|
|
151
|
-
def
|
|
152
|
-
"""
|
|
236
|
+
def integrity_check(self) -> dict[str, object]:
|
|
237
|
+
"""Verify the database and the store's own invariants.
|
|
153
238
|
|
|
154
|
-
|
|
155
|
-
FTS5 tables; on this regular FTS5 table it raises 'SQL logic error' on
|
|
156
|
-
every UPDATE/DELETE. Databases created before the fix keep the broken
|
|
157
|
-
triggers because SCHEMA uses CREATE TRIGGER IF NOT EXISTS.
|
|
239
|
+
Returns per-check results; `ok` is the conjunction. Read-only.
|
|
158
240
|
"""
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
241
|
+
checks: dict[str, object] = {}
|
|
242
|
+
pragma = self.conn.execute("PRAGMA integrity_check").fetchone()[0]
|
|
243
|
+
checks["sqlite_integrity"] = pragma == "ok"
|
|
244
|
+
total = self.conn.execute("SELECT COUNT(*) FROM memories").fetchone()[0]
|
|
245
|
+
indexed = self.conn.execute("SELECT COUNT(*) FROM memories_fts").fetchone()[0]
|
|
246
|
+
checks["fts_in_sync"] = indexed == total
|
|
247
|
+
checks["fts_rows"] = {"memories": int(total), "indexed": int(indexed)}
|
|
248
|
+
orphan_edges = self.conn.execute(
|
|
249
|
+
"""
|
|
250
|
+
SELECT COUNT(*) FROM memory_links l
|
|
251
|
+
WHERE NOT EXISTS (SELECT 1 FROM memories m WHERE m.id = l.src_id)
|
|
252
|
+
OR NOT EXISTS (SELECT 1 FROM memories m WHERE m.id = l.dst_id)
|
|
253
|
+
"""
|
|
254
|
+
).fetchone()[0]
|
|
255
|
+
checks["orphan_links"] = int(orphan_edges)
|
|
256
|
+
checks["links_valid"] = orphan_edges == 0
|
|
257
|
+
checks["schema_version"] = self.schema_version()
|
|
258
|
+
checks["ok"] = bool(
|
|
259
|
+
checks["sqlite_integrity"] and checks["fts_in_sync"] and checks["links_valid"]
|
|
260
|
+
)
|
|
261
|
+
return checks
|
|
168
262
|
|
|
169
263
|
def add(self, record: MemoryRecord) -> MemoryRecord:
|
|
170
264
|
record.summary = record.normalized_summary()
|
|
@@ -422,6 +516,25 @@ class MemoryStore:
|
|
|
422
516
|
).fetchall()
|
|
423
517
|
return [self._row_to_record(row) for row in rows]
|
|
424
518
|
|
|
519
|
+
def semantic_corpus(self) -> list[tuple[int, str, str]]:
|
|
520
|
+
"""(rowid, memory_id, embeddable text) for every memory — the auto
|
|
521
|
+
semantic indexer's rebuild source. rowid doubles as the uint64
|
|
522
|
+
external id; full rebuilds make rowid reuse after deletes harmless."""
|
|
523
|
+
rows = self.conn.execute(
|
|
524
|
+
"SELECT rowid AS rid, id, content, summary, tags FROM memories"
|
|
525
|
+
).fetchall()
|
|
526
|
+
return [
|
|
527
|
+
(int(row["rid"]), row["id"], f"{row['content']}\n{row['summary']}\n{row['tags']}")
|
|
528
|
+
for row in rows
|
|
529
|
+
]
|
|
530
|
+
|
|
531
|
+
def semantic_signature(self) -> tuple:
|
|
532
|
+
"""Cheap change signature used to decide when the auto index rebuilds."""
|
|
533
|
+
row = self.conn.execute(
|
|
534
|
+
"SELECT COUNT(*), COALESCE(MAX(rowid), 0), COALESCE(MAX(updated_at), '') FROM memories"
|
|
535
|
+
).fetchone()
|
|
536
|
+
return (int(row[0]), int(row[1]), row[2])
|
|
537
|
+
|
|
425
538
|
def graph_snapshot(
|
|
426
539
|
self,
|
|
427
540
|
*,
|
|
@@ -640,6 +753,108 @@ class MemoryStore:
|
|
|
640
753
|
}
|
|
641
754
|
return [memory_id for memory_id in ordered if memory_id in visible]
|
|
642
755
|
|
|
756
|
+
_MEMORY_COLUMNS = (
|
|
757
|
+
"id, owner, scope, type, content, summary, tags, visibility, source, "
|
|
758
|
+
"confidence, importance, created_at, updated_at, expires_at, "
|
|
759
|
+
"decay_policy, decay_half_life_days, last_accessed_at, access_count, pinned"
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
def run_retention(
|
|
763
|
+
self, *, decayed_half_lives: float | None = RETENTION_MIN_HALF_LIVES
|
|
764
|
+
) -> dict[str, int]:
|
|
765
|
+
"""Move expired (and optionally deeply-decayed) memories to cold archive.
|
|
766
|
+
|
|
767
|
+
Archived memories leave active recall entirely but stay restorable.
|
|
768
|
+
Pinned and authority-track records are never archived by decay; only a
|
|
769
|
+
hard expiry can retire them.
|
|
770
|
+
"""
|
|
771
|
+
now = utc_now()
|
|
772
|
+
expired = self._archive_where(
|
|
773
|
+
"expires_at IS NOT NULL AND expires_at <= ?", [now], reason="expired"
|
|
774
|
+
)
|
|
775
|
+
decayed = 0
|
|
776
|
+
if decayed_half_lives and decayed_half_lives > 0:
|
|
777
|
+
decayed = self._archive_where(
|
|
778
|
+
"""
|
|
779
|
+
pinned = 0 AND decay_policy != 'none'
|
|
780
|
+
AND NOT (COALESCE(json_extract(source, '$.permanence'), 0) = 1
|
|
781
|
+
AND COALESCE(json_extract(source, '$.weight'), 0) >= 10)
|
|
782
|
+
AND (julianday(?) - julianday(COALESCE(last_accessed_at, updated_at)))
|
|
783
|
+
> (? * decay_half_life_days)
|
|
784
|
+
""",
|
|
785
|
+
[now, float(decayed_half_lives)],
|
|
786
|
+
reason="decayed",
|
|
787
|
+
)
|
|
788
|
+
return {"archived_expired": expired, "archived_decayed": decayed}
|
|
789
|
+
|
|
790
|
+
def _archive_where(self, where_sql: str, params: list[object], *, reason: str) -> int:
|
|
791
|
+
rows = self.conn.execute(
|
|
792
|
+
f"SELECT id FROM memories WHERE {where_sql}", params
|
|
793
|
+
).fetchall()
|
|
794
|
+
ids = [row["id"] for row in rows]
|
|
795
|
+
if not ids:
|
|
796
|
+
return 0
|
|
797
|
+
now = utc_now()
|
|
798
|
+
placeholders = ",".join("?" for _ in ids)
|
|
799
|
+
self.conn.execute(
|
|
800
|
+
f"""
|
|
801
|
+
INSERT OR REPLACE INTO memories_archive
|
|
802
|
+
SELECT {self._MEMORY_COLUMNS}, ?, ? FROM memories WHERE id IN ({placeholders})
|
|
803
|
+
""",
|
|
804
|
+
[now, reason, *ids],
|
|
805
|
+
)
|
|
806
|
+
self.conn.execute(
|
|
807
|
+
f"DELETE FROM memory_links WHERE src_id IN ({placeholders}) OR dst_id IN ({placeholders})",
|
|
808
|
+
[*ids, *ids],
|
|
809
|
+
)
|
|
810
|
+
self.conn.execute(f"DELETE FROM memories WHERE id IN ({placeholders})", ids)
|
|
811
|
+
self.conn.commit()
|
|
812
|
+
return len(ids)
|
|
813
|
+
|
|
814
|
+
def list_archived(self, *, limit: int = 20, offset: int = 0) -> list[dict[str, object]]:
|
|
815
|
+
rows = self.conn.execute(
|
|
816
|
+
"SELECT * FROM memories_archive ORDER BY archived_at DESC, rowid DESC LIMIT ? OFFSET ?",
|
|
817
|
+
(max(1, limit), max(0, offset)),
|
|
818
|
+
).fetchall()
|
|
819
|
+
return [
|
|
820
|
+
{
|
|
821
|
+
"id": row["id"], "owner": row["owner"], "scope": row["scope"],
|
|
822
|
+
"type": row["type"], "content": row["content"], "summary": row["summary"],
|
|
823
|
+
"archived_at": row["archived_at"], "archive_reason": row["archive_reason"],
|
|
824
|
+
}
|
|
825
|
+
for row in rows
|
|
826
|
+
]
|
|
827
|
+
|
|
828
|
+
def restore_archived(self, memory_id: str) -> MemoryRecord:
|
|
829
|
+
"""Bring an archived memory back into active recall.
|
|
830
|
+
|
|
831
|
+
The original expiry is cleared — restoring an expired memory that
|
|
832
|
+
stays expired would be a no-op — and updated_at restarts the decay
|
|
833
|
+
clock, because a human decision to restore IS a relevance signal.
|
|
834
|
+
"""
|
|
835
|
+
row = self.conn.execute(
|
|
836
|
+
"SELECT * FROM memories_archive WHERE id = ?", (memory_id,)
|
|
837
|
+
).fetchone()
|
|
838
|
+
if row is None:
|
|
839
|
+
raise KeyError(memory_id)
|
|
840
|
+
now = utc_now()
|
|
841
|
+
self.conn.execute(
|
|
842
|
+
f"""
|
|
843
|
+
INSERT INTO memories({self._MEMORY_COLUMNS})
|
|
844
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
845
|
+
""",
|
|
846
|
+
(
|
|
847
|
+
row["id"], row["owner"], row["scope"], row["type"], row["content"],
|
|
848
|
+
row["summary"], row["tags"], row["visibility"], row["source"],
|
|
849
|
+
row["confidence"], row["importance"], row["created_at"], now,
|
|
850
|
+
None, row["decay_policy"], row["decay_half_life_days"],
|
|
851
|
+
row["last_accessed_at"], row["access_count"], row["pinned"],
|
|
852
|
+
),
|
|
853
|
+
)
|
|
854
|
+
self.conn.execute("DELETE FROM memories_archive WHERE id = ?", (memory_id,))
|
|
855
|
+
self.conn.commit()
|
|
856
|
+
return self.get(memory_id)
|
|
857
|
+
|
|
643
858
|
def purge_owner(self, owner: str) -> dict[str, int]:
|
|
644
859
|
"""Delete every memory owned by `owner`, plus all links touching them.
|
|
645
860
|
|
|
@@ -1519,7 +1734,9 @@ class MemoryStore:
|
|
|
1519
1734
|
"stale_links": int(stale_links),
|
|
1520
1735
|
"top_hubs": top_hubs,
|
|
1521
1736
|
}
|
|
1737
|
+
archived = self.conn.execute("SELECT COUNT(*) FROM memories_archive").fetchone()[0]
|
|
1522
1738
|
return base | {
|
|
1739
|
+
"archived": int(archived),
|
|
1523
1740
|
"graph_health": graph_health,
|
|
1524
1741
|
"pinned": int(pinned),
|
|
1525
1742
|
"expired": int(expired),
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Embedding pipeline: semantic recall that works out of the box.
|
|
2
|
+
|
|
3
|
+
`HashingEmbedder` is the dependency-free default — deterministic feature
|
|
4
|
+
hashing over words and character trigrams. It captures lexical similarity in
|
|
5
|
+
vector space (typo- and morphology-tolerant), not deep semantics; swap in any
|
|
6
|
+
real embedding model by passing a `str -> sequence[float]` callable.
|
|
7
|
+
|
|
8
|
+
`AutoSemanticIndex` keeps a turbovec index in sync with the memories table by
|
|
9
|
+
watching a cheap change signature and rebuilding lazily — the disposable-index
|
|
10
|
+
philosophy: the vector index is never authoritative and can always be rebuilt
|
|
11
|
+
from SQLite.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import math
|
|
17
|
+
import re
|
|
18
|
+
import zlib
|
|
19
|
+
from typing import Iterable, Sequence
|
|
20
|
+
|
|
21
|
+
from .candidates import Candidate
|
|
22
|
+
|
|
23
|
+
_WORD_RE = re.compile(r"\w+", re.UNICODE)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HashingEmbedder:
|
|
27
|
+
"""Deterministic feature-hashing embedder (no model, no downloads)."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, dim: int = 256):
|
|
30
|
+
if dim < 16:
|
|
31
|
+
raise ValueError("dim must be >= 16")
|
|
32
|
+
self.dim = dim
|
|
33
|
+
|
|
34
|
+
def __call__(self, text: str) -> list[float]:
|
|
35
|
+
vector = [0.0] * self.dim
|
|
36
|
+
for feature in self._features(text):
|
|
37
|
+
digest = zlib.crc32(feature.encode("utf-8"))
|
|
38
|
+
index = digest % self.dim
|
|
39
|
+
sign = 1.0 if (digest >> 16) & 1 else -1.0
|
|
40
|
+
vector[index] += sign
|
|
41
|
+
norm = math.sqrt(sum(value * value for value in vector))
|
|
42
|
+
if norm == 0.0:
|
|
43
|
+
return vector
|
|
44
|
+
return [value / norm for value in vector]
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _features(text: str) -> Iterable[str]:
|
|
48
|
+
words = _WORD_RE.findall(text.lower())
|
|
49
|
+
for word in words:
|
|
50
|
+
yield "w:" + word
|
|
51
|
+
padded = f"^{word}$"
|
|
52
|
+
for i in range(len(padded) - 2):
|
|
53
|
+
yield "t:" + padded[i : i + 3]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AutoSemanticIndex:
|
|
57
|
+
"""CandidateProvider that self-maintains a turbovec index over the store.
|
|
58
|
+
|
|
59
|
+
Rebuilds whenever the memories table changes (count / max rowid / max
|
|
60
|
+
updated_at signature), so no write-path coupling is needed and multiple
|
|
61
|
+
processes sharing the database each converge on their own fresh index.
|
|
62
|
+
Returns candidate ids only; the store's ACL/expiry hard gates still apply.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
name = "turbovec-auto"
|
|
66
|
+
|
|
67
|
+
def __init__(self, store, embedder=None, *, dim: int = 256):
|
|
68
|
+
self._store = store
|
|
69
|
+
self._embedder = embedder or HashingEmbedder(dim)
|
|
70
|
+
self._provider = None
|
|
71
|
+
self._signature: tuple | None = None
|
|
72
|
+
|
|
73
|
+
def candidates(
|
|
74
|
+
self,
|
|
75
|
+
query: str,
|
|
76
|
+
*,
|
|
77
|
+
owner: str | None = None,
|
|
78
|
+
scope: str | None = None,
|
|
79
|
+
requester_agent_id: str | None = None,
|
|
80
|
+
requester_team_id: str | None = None,
|
|
81
|
+
limit: int = 10,
|
|
82
|
+
) -> Sequence[Candidate]:
|
|
83
|
+
signature = self._store.semantic_signature()
|
|
84
|
+
if signature != self._signature:
|
|
85
|
+
self._rebuild(signature)
|
|
86
|
+
if self._provider is None:
|
|
87
|
+
return []
|
|
88
|
+
return list(
|
|
89
|
+
self._provider.candidates(
|
|
90
|
+
query,
|
|
91
|
+
owner=owner,
|
|
92
|
+
scope=scope,
|
|
93
|
+
requester_agent_id=requester_agent_id,
|
|
94
|
+
requester_team_id=requester_team_id,
|
|
95
|
+
limit=limit,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _rebuild(self, signature: tuple) -> None:
|
|
100
|
+
from .providers.turbovec import TurbovecSemanticCandidateProvider
|
|
101
|
+
|
|
102
|
+
corpus = self._store.semantic_corpus()
|
|
103
|
+
if not corpus:
|
|
104
|
+
self._provider = None
|
|
105
|
+
self._signature = signature
|
|
106
|
+
return
|
|
107
|
+
vectors = [self._embedder(text) for _, _, text in corpus]
|
|
108
|
+
mapping = {memory_id: rowid for rowid, memory_id, _ in corpus}
|
|
109
|
+
self._provider = TurbovecSemanticCandidateProvider.from_vectors(
|
|
110
|
+
vectors=vectors,
|
|
111
|
+
external_id_by_memory_id=mapping,
|
|
112
|
+
embed_query=self._embedder,
|
|
113
|
+
)
|
|
114
|
+
self._signature = signature
|