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.
Files changed (94) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +3 -2
  3. package/package.json +1 -1
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +1 -1
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +1 -1
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +1 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-recall/SKILL.md +1 -1
  28. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  31. package/pyproject.toml +1 -1
  32. package/src/superlocalmemory/__init__.py +1 -1
  33. package/src/superlocalmemory/access/rbac.py +68 -76
  34. package/src/superlocalmemory/cli/commands.py +19 -0
  35. package/src/superlocalmemory/cli/ingest_cmd.py +11 -1
  36. package/src/superlocalmemory/cli/main.py +30 -0
  37. package/src/superlocalmemory/cli/pending_store.py +39 -14
  38. package/src/superlocalmemory/core/backend_orchestrator.py +93 -0
  39. package/src/superlocalmemory/core/config.py +78 -0
  40. package/src/superlocalmemory/core/consolidation_engine.py +79 -73
  41. package/src/superlocalmemory/core/engine.py +92 -11
  42. package/src/superlocalmemory/core/fact_consolidator.py +148 -30
  43. package/src/superlocalmemory/core/graph_pruner.py +436 -39
  44. package/src/superlocalmemory/core/ingestion_command.py +160 -31
  45. package/src/superlocalmemory/core/maintenance_scheduler.py +61 -1
  46. package/src/superlocalmemory/core/recall_pipeline.py +3 -0
  47. package/src/superlocalmemory/core/registry.py +5 -1
  48. package/src/superlocalmemory/core/remote_mode.py +3 -1
  49. package/src/superlocalmemory/core/scale_engine.py +41 -18
  50. package/src/superlocalmemory/core/store_pipeline.py +18 -4
  51. package/src/superlocalmemory/encoding/entity_resolver.py +18 -11
  52. package/src/superlocalmemory/hooks/_outcome_common.py +9 -2
  53. package/src/superlocalmemory/hooks/adapter_base.py +58 -44
  54. package/src/superlocalmemory/hooks/ide_connector.py +26 -8
  55. package/src/superlocalmemory/hooks/portable_kit.py +105 -9
  56. package/src/superlocalmemory/hooks/prewarm_auth.py +21 -2
  57. package/src/superlocalmemory/infra/auth_middleware.py +3 -1
  58. package/src/superlocalmemory/infra/cloud_backup.py +26 -27
  59. package/src/superlocalmemory/infra/event_bus.py +250 -88
  60. package/src/superlocalmemory/learning/consolidation_cycle.py +33 -16
  61. package/src/superlocalmemory/learning/entity_compiler.py +148 -132
  62. package/src/superlocalmemory/learning/memory_merge.py +97 -82
  63. package/src/superlocalmemory/learning/reward_archive.py +98 -90
  64. package/src/superlocalmemory/learning/reward_boost.py +40 -30
  65. package/src/superlocalmemory/mcp/http_transport.py +335 -3
  66. package/src/superlocalmemory/retrieval/engine.py +7 -1
  67. package/src/superlocalmemory/retrieval/entity_channel.py +25 -1
  68. package/src/superlocalmemory/retrieval/reranker.py +98 -15
  69. package/src/superlocalmemory/retrieval/spreading_activation.py +20 -12
  70. package/src/superlocalmemory/retrieval/vector_store.py +84 -69
  71. package/src/superlocalmemory/server/loopback.py +91 -0
  72. package/src/superlocalmemory/server/origin.py +9 -4
  73. package/src/superlocalmemory/server/routes/backup.py +6 -2
  74. package/src/superlocalmemory/server/routes/behavioral.py +6 -12
  75. package/src/superlocalmemory/server/routes/compliance.py +20 -23
  76. package/src/superlocalmemory/server/routes/config_api.py +83 -0
  77. package/src/superlocalmemory/server/routes/helpers.py +24 -13
  78. package/src/superlocalmemory/server/routes/memories.py +139 -91
  79. package/src/superlocalmemory/server/routes/mesh.py +7 -2
  80. package/src/superlocalmemory/server/routes/profiles.py +20 -21
  81. package/src/superlocalmemory/server/routes/rbac.py +0 -1
  82. package/src/superlocalmemory/server/routes/tiers.py +42 -30
  83. package/src/superlocalmemory/server/routes/v3_api.py +67 -77
  84. package/src/superlocalmemory/server/unified_daemon.py +283 -39
  85. package/src/superlocalmemory/server/write_identity.py +22 -4
  86. package/src/superlocalmemory/storage/database.py +109 -19
  87. package/src/superlocalmemory/storage/deferred_writes.py +153 -0
  88. package/src/superlocalmemory/storage/embedding_migrator.py +19 -0
  89. package/src/superlocalmemory/storage/memory_write.py +119 -0
  90. package/src/superlocalmemory/storage/migration_runner.py +7 -0
  91. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +113 -78
  92. package/src/superlocalmemory/storage/migrations/M031_dead_letter_operations.py +80 -0
  93. package/src/superlocalmemory/storage/write_lock.py +88 -0
  94. 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.2</h1>
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.2</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/>
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> &nbsp;·&nbsp; MCP: add <code>slm_compress</code> to your config &nbsp;·&nbsp; 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.8.2",
3
+ "version": "3.8.5",
4
4
  "description": "Local-first agent memory with MCP and an agent-native CLI. Documented clients include Claude Code, Cursor, and Windsurf.",
5
5
  "keywords": [
6
6
  "ai-memory",
@@ -15,5 +15,5 @@
15
15
  "mcpServers": "./.mcp.json",
16
16
  "name": "superlocalmemory",
17
17
  "repository": "https://github.com/qualixar/superlocalmemory",
18
- "version": "3.8.2"
18
+ "version": "3.8.5"
19
19
  }
package/plugin/CLAUDE.md CHANGED
@@ -1,4 +1,4 @@
1
- <!-- BEGIN SuperLocalMemory v3.8.2 -->
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.2 -->
42
+ <!-- END SuperLocalMemory v3.8.5 -->
43
43
 
44
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
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.2 · Qualixar · AGPL-3.0-or-later
80
+ SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later
@@ -68,4 +68,4 @@ assessment. The gate is the authority.
68
68
 
69
69
  ---
70
70
 
71
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
71
+ 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.2 · Qualixar · AGPL-3.0-or-later
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.2 · Qualixar · AGPL-3.0-or-later
44
+ SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later
@@ -1 +1 @@
1
- superlocalmemory==3.8.2
1
+ superlocalmemory==3.8.5
@@ -145,4 +145,4 @@ These subcommands control daemon-level cache settings. They do not read or write
145
145
 
146
146
  ---
147
147
 
148
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
148
+ SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later
@@ -147,4 +147,4 @@ Content over 1 MB (1 000 000 bytes UTF-8) is processed but `reversible` is force
147
147
 
148
148
  ---
149
149
 
150
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
150
+ SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later
@@ -245,4 +245,4 @@ Before running any destructive operation (`forget`, `compact_memories`):
245
245
 
246
246
  ---
247
247
 
248
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
248
+ *SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later*
@@ -311,4 +311,4 @@ profile. See `slm-profile` for the full profile switching workflow.
311
311
 
312
312
  ---
313
313
 
314
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
314
+ SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later
@@ -96,4 +96,4 @@ paused, name the approval needed; when errored, quote the short detail.
96
96
 
97
97
  ---
98
98
 
99
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
99
+ SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later
@@ -279,4 +279,4 @@ mesh availability.
279
279
 
280
280
  ---
281
281
 
282
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
282
+ *SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later*
@@ -145,4 +145,4 @@ Name them differently in your MCP config (e.g. `superlocalmemory-personal` and
145
145
 
146
146
  ---
147
147
 
148
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
148
+ *SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later*
@@ -236,4 +236,4 @@ before recalling, then switch back. See `slm-profile` for workspace switching.
236
236
 
237
237
  ---
238
238
 
239
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
239
+ *SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later*
@@ -238,4 +238,4 @@ different workspace, use `switch_profile` first. See `slm-profile`.
238
238
 
239
239
  ---
240
240
 
241
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
241
+ *SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later*
@@ -173,4 +173,4 @@ to review the impact. See `slm-remember` for the full deletion discipline.
173
173
 
174
174
  ---
175
175
 
176
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
176
+ *SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later*
@@ -227,4 +227,4 @@ explicitly and call `recall` with `include_global`/`include_shared` after
227
227
 
228
228
  ---
229
229
 
230
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
230
+ *SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later*
@@ -163,4 +163,4 @@ multi-profile setup. To switch the active profile, see `slm-profile`.
163
163
 
164
164
  ---
165
165
 
166
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
166
+ SuperLocalMemory v3.8.5 · Qualixar · AGPL-3.0-or-later
@@ -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.2 · Qualixar · AGPL-3.0-or-later
131
+ SuperLocalMemory v3.8.4 · Qualixar · AGPL-3.0-or-later
@@ -145,4 +145,4 @@ These subcommands control daemon-level cache settings. They do not read or write
145
145
 
146
146
  ---
147
147
 
148
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
148
+ SuperLocalMemory v3.8.4 · Qualixar · AGPL-3.0-or-later
@@ -147,4 +147,4 @@ Content over 1 MB (1 000 000 bytes UTF-8) is processed but `reversible` is force
147
147
 
148
148
  ---
149
149
 
150
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
150
+ SuperLocalMemory v3.8.4 · Qualixar · AGPL-3.0-or-later
@@ -311,4 +311,4 @@ profile. See `slm-profile` for the full profile switching workflow.
311
311
 
312
312
  ---
313
313
 
314
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
314
+ SuperLocalMemory v3.8.4 · Qualixar · AGPL-3.0-or-later
@@ -236,4 +236,4 @@ before recalling, then switch back. See `slm-profile` for workspace switching.
236
236
 
237
237
  ---
238
238
 
239
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
239
+ *SuperLocalMemory v3.8.4 · Qualixar · AGPL-3.0-or-later*
@@ -238,4 +238,4 @@ different workspace, use `switch_profile` first. See `slm-profile`.
238
238
 
239
239
  ---
240
240
 
241
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
241
+ *SuperLocalMemory v3.8.4 · Qualixar · AGPL-3.0-or-later*
@@ -227,4 +227,4 @@ explicitly and call `recall` with `include_global`/`include_shared` after
227
227
 
228
228
  ---
229
229
 
230
- *SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later*
230
+ *SuperLocalMemory v3.8.4 · Qualixar · AGPL-3.0-or-later*
@@ -163,4 +163,4 @@ multi-profile setup. To switch the active profile, see `slm-profile`.
163
163
 
164
164
  ---
165
165
 
166
- SuperLocalMemory v3.8.2 · Qualixar · AGPL-3.0-or-later
166
+ SuperLocalMemory v3.8.4 · Qualixar · AGPL-3.0-or-later
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.8.2"
3
+ version = "3.8.5"
4
4
  description = "Local-first agent memory with auditable hybrid retrieval"
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-or-later"
@@ -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.2"
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
- _BUSY_TIMEOUT_MS = 4000
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
- conn = self._conn()
159
- try:
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
- exists = conn.execute(
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
- conn = self._conn()
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
- conn = self._conn()
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
- conn = self._conn()
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
- conn = self._conn()
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
- if expired or row["status"] != "active":
314
- conn.execute("DELETE FROM rbac_sessions WHERE token_hash=?", (token_hash,))
315
- conn.commit()
316
- return None
317
- # Debounce last_seen: only write when it is stale (>60s), so a burst
318
- # of authenticated requests does not serialize through the single
319
- # SQLite writer on every call.
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
- try:
322
- stale = (now - datetime.fromisoformat(row["last_seen"])).total_seconds() > 60
323
- except (ValueError, TypeError, KeyError):
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
- conn.commit()
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
- conn = self._conn()
338
- try:
339
- conn.execute("DELETE FROM rbac_sessions WHERE token_hash=?",
340
- (_hash_token(raw_token),))
341
- conn.commit()
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
- conn = self._conn()
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
- conn.commit()
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
- conn = self._conn()
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
- conn = self._conn()
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
- conn = self._conn()
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