superlocalmemory 3.8.2 → 3.8.5
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.
- package/CHANGELOG.md +57 -0
- package/README.md +3 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/rbac.py +68 -76
- package/src/superlocalmemory/cli/commands.py +19 -0
- package/src/superlocalmemory/cli/ingest_cmd.py +11 -1
- package/src/superlocalmemory/cli/main.py +30 -0
- package/src/superlocalmemory/cli/pending_store.py +39 -14
- package/src/superlocalmemory/core/backend_orchestrator.py +93 -0
- package/src/superlocalmemory/core/config.py +78 -0
- package/src/superlocalmemory/core/consolidation_engine.py +79 -73
- package/src/superlocalmemory/core/engine.py +92 -11
- package/src/superlocalmemory/core/fact_consolidator.py +148 -30
- package/src/superlocalmemory/core/graph_pruner.py +436 -39
- package/src/superlocalmemory/core/ingestion_command.py +160 -31
- package/src/superlocalmemory/core/maintenance_scheduler.py +61 -1
- package/src/superlocalmemory/core/recall_pipeline.py +3 -0
- package/src/superlocalmemory/core/registry.py +5 -1
- package/src/superlocalmemory/core/remote_mode.py +3 -1
- package/src/superlocalmemory/core/scale_engine.py +41 -18
- package/src/superlocalmemory/core/store_pipeline.py +18 -4
- package/src/superlocalmemory/encoding/entity_resolver.py +18 -11
- package/src/superlocalmemory/hooks/_outcome_common.py +9 -2
- package/src/superlocalmemory/hooks/adapter_base.py +58 -44
- package/src/superlocalmemory/hooks/ide_connector.py +26 -8
- package/src/superlocalmemory/hooks/portable_kit.py +105 -9
- package/src/superlocalmemory/hooks/prewarm_auth.py +21 -2
- package/src/superlocalmemory/infra/auth_middleware.py +3 -1
- package/src/superlocalmemory/infra/cloud_backup.py +26 -27
- package/src/superlocalmemory/infra/event_bus.py +250 -88
- package/src/superlocalmemory/learning/consolidation_cycle.py +33 -16
- package/src/superlocalmemory/learning/entity_compiler.py +148 -132
- package/src/superlocalmemory/learning/memory_merge.py +97 -82
- package/src/superlocalmemory/learning/reward_archive.py +98 -90
- package/src/superlocalmemory/learning/reward_boost.py +40 -30
- package/src/superlocalmemory/mcp/http_transport.py +335 -3
- package/src/superlocalmemory/retrieval/engine.py +7 -1
- package/src/superlocalmemory/retrieval/entity_channel.py +25 -1
- package/src/superlocalmemory/retrieval/reranker.py +98 -15
- package/src/superlocalmemory/retrieval/spreading_activation.py +20 -12
- package/src/superlocalmemory/retrieval/vector_store.py +84 -69
- package/src/superlocalmemory/server/loopback.py +91 -0
- package/src/superlocalmemory/server/origin.py +9 -4
- package/src/superlocalmemory/server/routes/backup.py +6 -2
- package/src/superlocalmemory/server/routes/behavioral.py +6 -12
- package/src/superlocalmemory/server/routes/compliance.py +20 -23
- package/src/superlocalmemory/server/routes/config_api.py +83 -0
- package/src/superlocalmemory/server/routes/helpers.py +24 -13
- package/src/superlocalmemory/server/routes/memories.py +139 -91
- package/src/superlocalmemory/server/routes/mesh.py +7 -2
- package/src/superlocalmemory/server/routes/profiles.py +20 -21
- package/src/superlocalmemory/server/routes/rbac.py +0 -1
- package/src/superlocalmemory/server/routes/tiers.py +42 -30
- package/src/superlocalmemory/server/routes/v3_api.py +67 -77
- package/src/superlocalmemory/server/unified_daemon.py +283 -39
- package/src/superlocalmemory/server/write_identity.py +22 -4
- package/src/superlocalmemory/storage/database.py +109 -19
- package/src/superlocalmemory/storage/deferred_writes.py +153 -0
- package/src/superlocalmemory/storage/embedding_migrator.py +19 -0
- package/src/superlocalmemory/storage/memory_write.py +119 -0
- package/src/superlocalmemory/storage/migration_runner.py +7 -0
- package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +113 -78
- package/src/superlocalmemory/storage/migrations/M031_dead_letter_operations.py +80 -0
- package/src/superlocalmemory/storage/write_lock.py +88 -0
- package/src/superlocalmemory/ui/js/core.js +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,63 @@ All notable changes to SuperLocalMemory V3 will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.8.5] - 2026-07-26 — Reliable, full-quality recall
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Recall now consistently uses full-quality relevance ranking. The ranking model
|
|
12
|
+
warms up in the background with automatic retries, so recall no longer quietly
|
|
13
|
+
falls back to basic scoring when the model is slow to load, is recycled, or is
|
|
14
|
+
restarted. Full ranking resumes on its own within seconds if it is ever interrupted.
|
|
15
|
+
- The first recall after startup is now fast. The memory graph is fully warmed in
|
|
16
|
+
the background before queries arrive, instead of the first query paying a
|
|
17
|
+
multi-second load — and the graph no longer reloads on the query path during
|
|
18
|
+
normal use, so recall latency stays consistent.
|
|
19
|
+
- Recall stays responsive under heavy concurrent load. A startup contention issue
|
|
20
|
+
that could make the first queries slow on busy multi-agent systems has been removed.
|
|
21
|
+
- The internal recall cache is now cleaned up automatically. It could previously
|
|
22
|
+
grow without bound over months of use; on upgrade, stale entries are compacted away.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- Automatic scale-up for very large memory graphs. A database that grows past
|
|
26
|
+
millions of connections switches to a high-performance graph backend on its own;
|
|
27
|
+
smaller databases stay on the fast built-in engine, which is quicker for them. The
|
|
28
|
+
switch runs in the background, is verified for correctness before it takes effect,
|
|
29
|
+
and falls back safely to the built-in engine if it cannot complete.
|
|
30
|
+
|
|
31
|
+
### Notes
|
|
32
|
+
- Upgrades apply automatically on daemon start — no manual migration command needed.
|
|
33
|
+
- Recommended upgrade for all 3.8.x users.
|
|
34
|
+
|
|
35
|
+
## [3.8.4] - 2026-07-26
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
- Frequent "database is locked" errors under heavy or concurrent multi-agent use.
|
|
39
|
+
- MCP connections dropping when idle; connections now stay stable and recover cleanly.
|
|
40
|
+
- A just-saved memory is now instantly findable by meaning, not only by keyword (on a warm instance).
|
|
41
|
+
- HTTP write endpoints (dashboard Save / API) were rejected on some networked/containerized setups (#90).
|
|
42
|
+
- Background ingestion operations that failed no longer retry forever; failures move to a dead-letter queue (#77).
|
|
43
|
+
- Stale agent locks are cleared on restart; daemon shutdown no longer logs spurious import errors; the dashboard UI serves reliably on restricted filesystems.
|
|
44
|
+
### Added
|
|
45
|
+
- A choice of connection transport (stdio or HTTP) when connecting IDEs.
|
|
46
|
+
- Configurable graph-memory pruning (max edges per node, minimum edge weight) (#84).
|
|
47
|
+
### Notes
|
|
48
|
+
- Upgrades apply automatically on daemon start — no manual migration command needed.
|
|
49
|
+
|
|
50
|
+
## [3.8.3] - 2026-07-24 — Recall stays responsive under heavy load
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- **Recall no longer hangs when the system is busy.** Search and recall now
|
|
55
|
+
complete within a generous time budget even during background maintenance or
|
|
56
|
+
when many agents are querying at once. If a query can't finish in that window,
|
|
57
|
+
SLM returns keyword-matched results and marks them clearly instead of leaving
|
|
58
|
+
the request to time out. This applies everywhere recall runs — the dashboard
|
|
59
|
+
search, the CLI, and connected assistants — so results are consistent across
|
|
60
|
+
every surface.
|
|
61
|
+
- **Dashboard search waits long enough for a real answer.** The dashboard now
|
|
62
|
+
allows recall the full server-side budget before giving up, so heavy-load
|
|
63
|
+
queries return results rather than an aborted-request error.
|
|
64
|
+
|
|
8
65
|
## [3.8.2] - 2026-07-24 — Self-healing upgrades & faster, consistent recall
|
|
9
66
|
|
|
10
67
|
### Added
|
package/README.md
CHANGED
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
</picture>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
<h1 align="center">SuperLocalMemory V3.8.
|
|
8
|
+
<h1 align="center">SuperLocalMemory V3.8.5</h1>
|
|
9
9
|
<p align="center"><strong>Enterprise-grade, local-first memory for AI agents and teams.</strong><br/>
|
|
10
10
|
<em>A persistent, auditable long-term brain for your agents that runs on your own infrastructure — with multi-workspace isolation, role-based access, and GDPR + EU AI Act governance controls built in.</em></p>
|
|
11
|
-
<p align="center"><code>v3.8.
|
|
11
|
+
<p align="center"><code>v3.8.5</code> — one control plane: auditable retrieval · multi-scope memory (personal / shared / global) · Cache · Compress · trusted-peer Mesh · bounded loops — across CLI, MCP, dashboard, the <strong>Claude plugin</strong>, the <strong>Codex add-on</strong>, and documented IDE integrations.<br/>
|
|
12
12
|
Proxy: <code>slm wrap claude</code> · MCP: add <code>slm_compress</code> to your config · Skill: zero-config</p>
|
|
13
13
|
<p align="center"><strong>3 public research preprints</strong> (arXiv + Zenodo archives) · <a href="https://arxiv.org/abs/2603.02240">arXiv:2603.02240</a> · <a href="https://arxiv.org/abs/2603.14588">arXiv:2603.14588</a> · <a href="https://arxiv.org/abs/2604.04514">arXiv:2604.04514</a></p>
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
16
|
+
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/v3.8.5-Most_Stable_Release-2ea44f?style=for-the-badge&logo=checkmarx&logoColor=white" alt="v3.8.5 — Most Stable Release"/></a>
|
|
16
17
|
<a href="https://arxiv.org/abs/2603.14588"><img src="https://img.shields.io/badge/arXiv-2603.14588-b31b1b?style=for-the-badge&logo=arxiv&logoColor=white" alt="arXiv Paper"/></a>
|
|
17
18
|
<a href="#three-surfaces-proxy--mcp-tools--skill"><img src="https://img.shields.io/badge/Proxy_|_MCP_|_Skill-22c55e?style=for-the-badge" alt="Three Surfaces: Proxy, MCP Tools, Skill"/></a>
|
|
18
19
|
<a href="https://pypi.org/project/superlocalmemory/"><img src="https://img.shields.io/pypi/v/superlocalmemory?style=for-the-badge&logo=pypi&logoColor=white" alt="PyPI"/></a>
|
package/package.json
CHANGED
package/plugin/CLAUDE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- BEGIN SuperLocalMemory v3.8.
|
|
1
|
+
<!-- BEGIN SuperLocalMemory v3.8.5 -->
|
|
2
2
|
|
|
3
3
|
## SuperLocalMemory (SLM) — Agent Rules
|
|
4
4
|
|
|
@@ -39,6 +39,6 @@ slm-recall · slm-remember · slm-session · slm-status · slm-cache · slm-comp
|
|
|
39
39
|
### Subagents
|
|
40
40
|
slm-memory-advisor (memory decisions, session hygiene, scope/profile guidance) · slm-optimize-advisor (context compression + KV cache) · slm-governance-advisor (scope/roles/compliance/GDPR)
|
|
41
41
|
|
|
42
|
-
<!-- END SuperLocalMemory v3.8.
|
|
42
|
+
<!-- END SuperLocalMemory v3.8.5 -->
|
|
43
43
|
|
|
44
|
-
SuperLocalMemory v3.8.
|
|
44
|
+
SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later
|
|
@@ -77,4 +77,4 @@ slm-scope · slm-governance · slm-profile · slm-remember · slm-recall
|
|
|
77
77
|
# What NOT to do
|
|
78
78
|
Never session_init twice; never forget without dry-run preview; never store secrets; never bypass role checks; never claim an erasure succeeded without verifying via recall.
|
|
79
79
|
|
|
80
|
-
SuperLocalMemory v3.8.
|
|
80
|
+
SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later
|
|
@@ -46,4 +46,4 @@ slm-recall · slm-remember · slm-session · slm-scope · slm-profile · slm-gov
|
|
|
46
46
|
# What NOT to do
|
|
47
47
|
Never session_init twice; never forget dry_run=False without reporting preview; never dump a whole file into remember; never invent a memory; never claim "saved" without success:true / clean CLI exit; never bypass scope or governance restrictions.
|
|
48
48
|
|
|
49
|
-
SuperLocalMemory v3.8.
|
|
49
|
+
SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later
|
|
@@ -41,4 +41,4 @@ slm-compress · slm-cache · slm-status · slm-profile
|
|
|
41
41
|
# What NOT to do
|
|
42
42
|
Never compress code-for-edit/JSON-to-parse/<500 chars; never store secrets/ccr_ids; never let optimize failure block/alter the task; never claim a specific savings %; never carry ccr_ids across profile switches.
|
|
43
43
|
|
|
44
|
-
SuperLocalMemory v3.8.
|
|
44
|
+
SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later
|
package/plugin/requirements.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
superlocalmemory==3.8.
|
|
1
|
+
superlocalmemory==3.8.5
|
|
@@ -128,4 +128,4 @@ When the SLM MCP server is unavailable, use these CLI equivalents:
|
|
|
128
128
|
- **slm-optimize-advisor** — context compression and KV cache
|
|
129
129
|
- **slm-governance-advisor** — scope/role compliance, retention policies, GDPR
|
|
130
130
|
|
|
131
|
-
SuperLocalMemory v3.8.
|
|
131
|
+
SuperLocalMemory v3.8.4 · Qualixar · AGPL-3.0-or-later
|
package/pyproject.toml
CHANGED
|
@@ -32,7 +32,7 @@ if "OMP_NUM_THREADS" not in os.environ:
|
|
|
32
32
|
os.environ["OMP_NUM_THREADS"] = "2"
|
|
33
33
|
# ---------------------------------------------------------------------------
|
|
34
34
|
|
|
35
|
-
__version__ = "3.8.
|
|
35
|
+
__version__ = "3.8.5"
|
|
36
36
|
|
|
37
37
|
_REQUIRED_VERSIONS = {
|
|
38
38
|
"sentence_transformers": "5.3.0",
|
|
@@ -32,6 +32,7 @@ from __future__ import annotations
|
|
|
32
32
|
import hashlib
|
|
33
33
|
import hmac
|
|
34
34
|
import logging
|
|
35
|
+
import os
|
|
35
36
|
import secrets
|
|
36
37
|
import sqlite3
|
|
37
38
|
import uuid
|
|
@@ -39,9 +40,14 @@ from datetime import datetime, timedelta, timezone
|
|
|
39
40
|
from enum import Enum
|
|
40
41
|
from pathlib import Path
|
|
41
42
|
|
|
43
|
+
from superlocalmemory.storage.memory_write import memory_write
|
|
44
|
+
|
|
42
45
|
logger = logging.getLogger("superlocalmemory.access.rbac")
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
# Match the daemon's default (SLM_DB_BUSY_TIMEOUT_MS, default 10 000 ms).
|
|
48
|
+
# Reads use this via _conn(); writes go through memory_write() which reads the
|
|
49
|
+
# same env var internally, so in-process writers never see SQLITE_BUSY.
|
|
50
|
+
_BUSY_TIMEOUT_MS: int = max(0, int(os.environ.get("SLM_DB_BUSY_TIMEOUT_MS", "10000")))
|
|
45
51
|
_SESSION_TTL_HOURS = 12
|
|
46
52
|
|
|
47
53
|
# scrypt cost parameters (OWASP-recommended interactive-login range).
|
|
@@ -155,16 +161,15 @@ class RbacEngine:
|
|
|
155
161
|
raise RbacError("Username is required.")
|
|
156
162
|
pw_hash = _hash_password(password)
|
|
157
163
|
user_id = _uid()
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
# memory_write: process write lock + busy_timeout.
|
|
165
|
+
with memory_write(self._db_path) as conn:
|
|
160
166
|
# The UNIQUE(username) constraint is the source of truth; the SELECT
|
|
161
167
|
# is a fast path. Two concurrent creates can both pass the SELECT, so
|
|
162
168
|
# the loser's INSERT hits the constraint — convert that to RbacError
|
|
163
169
|
# (409) instead of leaking a raw IntegrityError as a 500.
|
|
164
|
-
|
|
170
|
+
if conn.execute(
|
|
165
171
|
"SELECT 1 FROM rbac_users WHERE username=?", (username,)
|
|
166
|
-
).fetchone()
|
|
167
|
-
if exists:
|
|
172
|
+
).fetchone():
|
|
168
173
|
raise RbacError(f"User '{username}' already exists.")
|
|
169
174
|
try:
|
|
170
175
|
conn.execute(
|
|
@@ -174,19 +179,15 @@ class RbacEngine:
|
|
|
174
179
|
(user_id, username, display_name or username, pw_hash,
|
|
175
180
|
_now(), created_by),
|
|
176
181
|
)
|
|
177
|
-
conn.commit()
|
|
178
182
|
except sqlite3.IntegrityError:
|
|
179
183
|
raise RbacError(f"User '{username}' already exists.")
|
|
180
|
-
finally:
|
|
181
|
-
conn.close()
|
|
182
184
|
logger.info("RBAC: created user '%s' (%s)", username, user_id)
|
|
183
185
|
return {"user_id": user_id, "username": username,
|
|
184
186
|
"display_name": display_name or username, "status": "active"}
|
|
185
187
|
|
|
186
188
|
def set_password(self, user_id: str, password: str) -> None:
|
|
187
189
|
pw_hash = _hash_password(password)
|
|
188
|
-
|
|
189
|
-
try:
|
|
190
|
+
with memory_write(self._db_path) as conn:
|
|
190
191
|
cur = conn.execute(
|
|
191
192
|
"UPDATE rbac_users SET password_hash=? WHERE user_id=?",
|
|
192
193
|
(pw_hash, user_id),
|
|
@@ -195,15 +196,11 @@ class RbacEngine:
|
|
|
195
196
|
raise RbacError("User not found.")
|
|
196
197
|
# A password change invalidates every existing session.
|
|
197
198
|
conn.execute("DELETE FROM rbac_sessions WHERE user_id=?", (user_id,))
|
|
198
|
-
conn.commit()
|
|
199
|
-
finally:
|
|
200
|
-
conn.close()
|
|
201
199
|
|
|
202
200
|
def set_status(self, user_id: str, status: str) -> None:
|
|
203
201
|
if status not in ("active", "disabled"):
|
|
204
202
|
raise RbacError("status must be 'active' or 'disabled'.")
|
|
205
|
-
|
|
206
|
-
try:
|
|
203
|
+
with memory_write(self._db_path) as conn:
|
|
207
204
|
cur = conn.execute(
|
|
208
205
|
"UPDATE rbac_users SET status=? WHERE user_id=?", (status, user_id)
|
|
209
206
|
)
|
|
@@ -211,21 +208,14 @@ class RbacEngine:
|
|
|
211
208
|
raise RbacError("User not found.")
|
|
212
209
|
if status == "disabled":
|
|
213
210
|
conn.execute("DELETE FROM rbac_sessions WHERE user_id=?", (user_id,))
|
|
214
|
-
conn.commit()
|
|
215
|
-
finally:
|
|
216
|
-
conn.close()
|
|
217
211
|
|
|
218
212
|
def delete_user(self, user_id: str) -> None:
|
|
219
|
-
|
|
220
|
-
try:
|
|
213
|
+
with memory_write(self._db_path) as conn:
|
|
221
214
|
conn.execute("DELETE FROM rbac_sessions WHERE user_id=?", (user_id,))
|
|
222
215
|
conn.execute("DELETE FROM rbac_memberships WHERE user_id=?", (user_id,))
|
|
223
216
|
cur = conn.execute("DELETE FROM rbac_users WHERE user_id=?", (user_id,))
|
|
224
217
|
if cur.rowcount == 0:
|
|
225
218
|
raise RbacError("User not found.")
|
|
226
|
-
conn.commit()
|
|
227
|
-
finally:
|
|
228
|
-
conn.close()
|
|
229
219
|
|
|
230
220
|
def get_user(self, user_id: str) -> dict | None:
|
|
231
221
|
conn = self._conn()
|
|
@@ -277,25 +267,35 @@ class RbacEngine:
|
|
|
277
267
|
raw = secrets.token_urlsafe(32)
|
|
278
268
|
now = datetime.now(timezone.utc)
|
|
279
269
|
expires = (now + timedelta(hours=ttl_hours)).isoformat()
|
|
280
|
-
|
|
281
|
-
try:
|
|
270
|
+
with memory_write(self._db_path) as conn:
|
|
282
271
|
conn.execute(
|
|
283
272
|
"INSERT INTO rbac_sessions (token_hash, user_id, created_at, "
|
|
284
273
|
"expires_at, last_seen) VALUES (?, ?, ?, ?, ?)",
|
|
285
274
|
(_hash_token(raw), user_id, now.isoformat(), expires, now.isoformat()),
|
|
286
275
|
)
|
|
287
|
-
conn.commit()
|
|
288
|
-
finally:
|
|
289
|
-
conn.close()
|
|
290
276
|
return raw
|
|
291
277
|
|
|
292
278
|
def resolve_session(self, raw_token: str) -> dict | None:
|
|
293
279
|
"""Return the active user for a session token, or None if invalid /
|
|
294
|
-
expired / disabled. Updates last_seen. Constant-time by hash lookup.
|
|
280
|
+
expired / disabled. Updates last_seen. Constant-time by hash lookup.
|
|
281
|
+
|
|
282
|
+
Concurrency design
|
|
283
|
+
------------------
|
|
284
|
+
Phase 1 (read): WAL allows concurrent readers without blocking the
|
|
285
|
+
writer — ``_conn()`` with ``busy_timeout`` only. The write lock is
|
|
286
|
+
NOT taken here so a burst of concurrent requests doesn't serialise
|
|
287
|
+
through the single writer on every call.
|
|
288
|
+
|
|
289
|
+
Phase 2 (conditional write): only if the session is expired/stale.
|
|
290
|
+
``memory_write()`` is opened AFTER the read connection is closed
|
|
291
|
+
(lock-ordering invariant: get_write_lock is outermost).
|
|
292
|
+
"""
|
|
295
293
|
if not raw_token:
|
|
296
294
|
return None
|
|
297
295
|
token_hash = _hash_token(raw_token)
|
|
298
296
|
now = datetime.now(timezone.utc)
|
|
297
|
+
|
|
298
|
+
# -- Phase 1: read-only lookup ----------------------------------------
|
|
299
299
|
conn = self._conn()
|
|
300
300
|
try:
|
|
301
301
|
row = conn.execute(
|
|
@@ -310,48 +310,52 @@ class RbacEngine:
|
|
|
310
310
|
expired = datetime.fromisoformat(row["expires_at"]) <= now
|
|
311
311
|
except ValueError:
|
|
312
312
|
expired = True
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
313
|
+
row_status = row["status"]
|
|
314
|
+
row_last_seen = row["last_seen"]
|
|
315
|
+
user_dict = {
|
|
316
|
+
"user_id": row["user_id"],
|
|
317
|
+
"username": row["username"],
|
|
318
|
+
"display_name": row["display_name"],
|
|
319
|
+
}
|
|
320
|
+
finally:
|
|
321
|
+
conn.close()
|
|
322
|
+
|
|
323
|
+
# -- Phase 2: write if needed (read conn closed above) ----------------
|
|
324
|
+
if expired or row_status != "active":
|
|
325
|
+
with memory_write(self._db_path) as wconn:
|
|
326
|
+
wconn.execute(
|
|
327
|
+
"DELETE FROM rbac_sessions WHERE token_hash=?", (token_hash,)
|
|
328
|
+
)
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
# Debounce last_seen: only write when stale (>60 s), so a burst of
|
|
332
|
+
# authenticated requests does not serialise through the writer every call.
|
|
333
|
+
stale = True
|
|
334
|
+
try:
|
|
335
|
+
stale = (now - datetime.fromisoformat(row_last_seen)).total_seconds() > 60
|
|
336
|
+
except (ValueError, TypeError, KeyError):
|
|
320
337
|
stale = True
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
stale = True
|
|
325
|
-
if stale:
|
|
326
|
-
conn.execute(
|
|
338
|
+
if stale:
|
|
339
|
+
with memory_write(self._db_path) as wconn:
|
|
340
|
+
wconn.execute(
|
|
327
341
|
"UPDATE rbac_sessions SET last_seen=? WHERE token_hash=?",
|
|
328
342
|
(now.isoformat(), token_hash),
|
|
329
343
|
)
|
|
330
|
-
|
|
331
|
-
return {"user_id": row["user_id"], "username": row["username"],
|
|
332
|
-
"display_name": row["display_name"]}
|
|
333
|
-
finally:
|
|
334
|
-
conn.close()
|
|
344
|
+
return user_dict
|
|
335
345
|
|
|
336
346
|
def revoke_session(self, raw_token: str) -> None:
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
finally:
|
|
343
|
-
conn.close()
|
|
347
|
+
with memory_write(self._db_path) as conn:
|
|
348
|
+
conn.execute(
|
|
349
|
+
"DELETE FROM rbac_sessions WHERE token_hash=?",
|
|
350
|
+
(_hash_token(raw_token),),
|
|
351
|
+
)
|
|
344
352
|
|
|
345
353
|
def purge_expired_sessions(self) -> int:
|
|
346
|
-
|
|
347
|
-
try:
|
|
354
|
+
with memory_write(self._db_path) as conn:
|
|
348
355
|
cur = conn.execute(
|
|
349
356
|
"DELETE FROM rbac_sessions WHERE expires_at <= ?", (_now(),)
|
|
350
357
|
)
|
|
351
|
-
|
|
352
|
-
return cur.rowcount or 0
|
|
353
|
-
finally:
|
|
354
|
-
conn.close()
|
|
358
|
+
return cur.rowcount or 0
|
|
355
359
|
|
|
356
360
|
# -- memberships ------------------------------------------------------
|
|
357
361
|
|
|
@@ -361,8 +365,7 @@ class RbacEngine:
|
|
|
361
365
|
role_enum = Role(role)
|
|
362
366
|
except ValueError:
|
|
363
367
|
raise RbacError(f"Invalid role '{role}'. Use admin/member/viewer.")
|
|
364
|
-
|
|
365
|
-
try:
|
|
368
|
+
with memory_write(self._db_path) as conn:
|
|
366
369
|
if not conn.execute(
|
|
367
370
|
"SELECT 1 FROM rbac_users WHERE user_id=?", (user_id,)
|
|
368
371
|
).fetchone():
|
|
@@ -374,21 +377,14 @@ class RbacEngine:
|
|
|
374
377
|
"added_at=excluded.added_at, added_by=excluded.added_by",
|
|
375
378
|
(profile_id, user_id, role_enum.value, _now(), added_by),
|
|
376
379
|
)
|
|
377
|
-
conn.commit()
|
|
378
|
-
finally:
|
|
379
|
-
conn.close()
|
|
380
380
|
return {"profile_id": profile_id, "user_id": user_id, "role": role_enum.value}
|
|
381
381
|
|
|
382
382
|
def remove_membership(self, profile_id: str, user_id: str) -> None:
|
|
383
|
-
|
|
384
|
-
try:
|
|
383
|
+
with memory_write(self._db_path) as conn:
|
|
385
384
|
conn.execute(
|
|
386
385
|
"DELETE FROM rbac_memberships WHERE profile_id=? AND user_id=?",
|
|
387
386
|
(profile_id, user_id),
|
|
388
387
|
)
|
|
389
|
-
conn.commit()
|
|
390
|
-
finally:
|
|
391
|
-
conn.close()
|
|
392
388
|
|
|
393
389
|
def get_role(self, user_id: str, profile_id: str) -> Role | None:
|
|
394
390
|
conn = self._conn()
|
|
@@ -442,16 +438,12 @@ class RbacEngine:
|
|
|
442
438
|
conn.close()
|
|
443
439
|
|
|
444
440
|
def set_policy(self, key: str, value: str) -> None:
|
|
445
|
-
|
|
446
|
-
try:
|
|
441
|
+
with memory_write(self._db_path) as conn:
|
|
447
442
|
conn.execute(
|
|
448
443
|
"INSERT INTO rbac_settings (key, value) VALUES (?, ?) "
|
|
449
444
|
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
|
450
445
|
(key, value),
|
|
451
446
|
)
|
|
452
|
-
conn.commit()
|
|
453
|
-
finally:
|
|
454
|
-
conn.close()
|
|
455
447
|
|
|
456
448
|
def require_login(self) -> bool:
|
|
457
449
|
"""Company mode: mutations require a valid user session (no owner
|
|
@@ -1135,6 +1135,9 @@ def cmd_connect(args: Namespace) -> None:
|
|
|
1135
1135
|
if ide_arg in IDE_MATRIX:
|
|
1136
1136
|
here = getattr(args, "here", False)
|
|
1137
1137
|
profile = getattr(args, "profile", None)
|
|
1138
|
+
transport = getattr(args, "transport", "stdio")
|
|
1139
|
+
daemon_port = getattr(args, "daemon_port", 8765)
|
|
1140
|
+
verify = getattr(args, "verify", False)
|
|
1138
1141
|
project = None
|
|
1139
1142
|
if here:
|
|
1140
1143
|
import pathlib
|
|
@@ -1148,8 +1151,24 @@ def cmd_connect(args: Namespace) -> None:
|
|
|
1148
1151
|
profile=profile,
|
|
1149
1152
|
agents_md_source=_agents_md_source_factory(),
|
|
1150
1153
|
dry_run=getattr(args, "dry_run", False),
|
|
1154
|
+
transport=transport,
|
|
1155
|
+
daemon_port=daemon_port,
|
|
1151
1156
|
)
|
|
1152
1157
|
|
|
1158
|
+
# --verify: probe daemon health after writing
|
|
1159
|
+
if verify and transport in ("http", "http-mcp-remote") and not result.get("error"):
|
|
1160
|
+
from superlocalmemory.hooks.portable_kit import _check_daemon_health
|
|
1161
|
+
reachable = _check_daemon_health(daemon_port)
|
|
1162
|
+
if reachable:
|
|
1163
|
+
print(f"[verify] Daemon reachable at http://127.0.0.1:{daemon_port}/api/v3/health")
|
|
1164
|
+
else:
|
|
1165
|
+
print(
|
|
1166
|
+
f"[verify] Warning: daemon NOT reachable at "
|
|
1167
|
+
f"http://127.0.0.1:{daemon_port}/api/v3/health. "
|
|
1168
|
+
f"Run `slm serve start` to start it.",
|
|
1169
|
+
file=sys.stderr,
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1153
1172
|
if not result.get("error") and not getattr(args, "dry_run", False):
|
|
1154
1173
|
from superlocalmemory.infra.local_diagnostics import record_operation
|
|
1155
1174
|
|