🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

superlocalmemory

Package Overview
Dependencies
Maintainers
1
Versions
183
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

superlocalmemory - npm Package Compare versions

Comparing version
3.6.14
to
3.6.15
+120
src/superlocalmemo...ge/migrations/M016_add_scope_support.py
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
# Licensed under AGPL-3.0-or-later - see LICENSE file
# Part of SuperLocalMemory v3.6.15
"""M016 — scope and shared_with columns on core tables (memory.db, deferred).
Adds two columns to each of the 5 core tables for multi-scope memory support:
scope TEXT NOT NULL DEFAULT 'personal' — personal | global
shared_with TEXT — JSON array of profile_ids
Existing data retains scope='personal' (backward compatible). Indexes on
``scope`` and ``(profile_id, scope)`` speed up scope-filtered queries and are
created HERE (not in schema.create_all_tables) so an upgrading DB whose tables
predate the scope column doesn't hit "no such column: scope" when the boot-time
index DDL runs before this migration.
Applied via a conditional apply(conn) rather than static DDL because SQLite has
no ``ADD COLUMN IF NOT EXISTS``: a static ALTER fails on a fresh install (the
column already exists from create_all_tables) and on a deferred boot where a
table isn't created yet. apply() guards every step so it is idempotent and
tolerant of partial/missing tables.
Deferred like M006, M011, and M013 because the core tables are bootstrapped
at engine init, not at migration time. Daemon lifespan calls ``apply_deferred``
right after engine init so these columns materialise on first boot after upgrade.
Author: Varun Pratap Bhardwaj / Qualixar
"""
from __future__ import annotations
import sqlite3
NAME = "M016_add_scope_support"
DB_TARGET = "memory"
TABLES = [
"memories",
"atomic_facts",
"canonical_entities",
"graph_edges",
"temporal_events",
]
# Retained for the migration log's drift hash and as documentation of intent.
# apply() below is the authoritative, idempotent executor.
DDL = ";".join(
[f"ALTER TABLE {t} ADD COLUMN scope TEXT NOT NULL DEFAULT 'personal'" for t in TABLES]
+ [f"ALTER TABLE {t} ADD COLUMN shared_with TEXT" for t in TABLES]
+ [f"CREATE INDEX IF NOT EXISTS idx_{t}_scope ON {t}(scope)" for t in TABLES]
+ [
f"CREATE INDEX IF NOT EXISTS idx_{t}_profile_scope ON {t}(profile_id, scope)"
for t in TABLES
]
)
def _table_exists(conn: sqlite3.Connection, table: str) -> bool:
return conn.execute(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table,)
).fetchone() is not None
def _column_names(conn: sqlite3.Connection, table: str) -> set[str]:
return {r[1] for r in conn.execute(f"PRAGMA table_info({table})").fetchall()}
def apply(conn: sqlite3.Connection) -> None:
"""Idempotently add scope/shared_with columns + indexes to every core table.
Per table: skip if the table doesn't exist yet; add each column only if
missing (SQLite has no ADD COLUMN IF NOT EXISTS); create indexes with
IF NOT EXISTS. Safe to run on fresh installs (columns already present),
upgrades (columns missing), and partial/repeat applies.
"""
for t in TABLES:
if not _table_exists(conn, t):
continue
cols = _column_names(conn, t)
if "scope" not in cols:
conn.execute(
f"ALTER TABLE {t} ADD COLUMN scope TEXT NOT NULL DEFAULT 'personal'"
)
if "shared_with" not in cols:
conn.execute(f"ALTER TABLE {t} ADD COLUMN shared_with TEXT")
conn.execute(f"CREATE INDEX IF NOT EXISTS idx_{t}_scope ON {t}(scope)")
conn.execute(
f"CREATE INDEX IF NOT EXISTS idx_{t}_profile_scope ON {t}(profile_id, scope)"
)
def verify(conn: sqlite3.Connection) -> bool:
"""Applied only when atomic_facts has the scope column AND its scope index.
Checking the index (not just the column) ensures apply() still runs on a
fresh install where create_all_tables created the column but not the index.
"""
# v3.6.15: verify EVERY core table apply() touches — not just atomic_facts.
# A partial apply (scope added to atomic_facts but not the other tables) must
# NOT false-pass, or M016 is marked done and the remaining tables are left
# permanently without the scope column. Absent tables are skipped, matching
# apply()'s own skip-missing-table contract.
for t in TABLES:
try:
info = conn.execute(f"PRAGMA table_info({t})").fetchall()
except sqlite3.Error:
return False
if not info:
continue # table absent on this DB — apply() skips it too
cols = {r[1] for r in info}
if "scope" not in cols:
return False
idx = conn.execute(
"SELECT 1 FROM sqlite_master WHERE type='index' AND name=?",
(f"idx_{t}_scope",),
).fetchone()
if idx is None:
return False
return True
+1
-1
{
"name": "superlocalmemory",
"version": "3.6.14",
"version": "3.6.15",
"description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",

@@ -5,0 +5,0 @@ "keywords": [

@@ -20,5 +20,5 @@ ---

- `session_init(project_path, query, max_results, max_age_days)` — ONCE at session start; returns recent decisions + relevant memories.
- `recall(query, limit, session_id, fast)` — multi-channel semantic retrieval (default limit 10).
- `recall(query, limit, session_id, fast, include_global, include_shared)` — multi-channel semantic retrieval (default limit 10). Leave `include_global`/`include_shared` unset — recall is private-by-default (v3.6.15).
- `search(query, limit, profile_id)` — exact keyword / FTS5 BM25.
- `remember(content, tags, project, importance, session_id)` — store atomic fact; importance 1-10.
- `remember(content, tags, project, importance, session_id, scope, shared_with)` — store atomic fact; importance 1-10. Leave `scope` unset (defaults to `personal`/private).
- `update_memory(fact_id, content)` — correct by exact id.

@@ -37,5 +37,6 @@ - `forget(profile_id, dry_run)` — decay cycle; ALWAYS dry_run=True first, report, never apply blind.

7. SESSION END — close_session(session_id) when work meaningfully complete.
8. SCOPE IS OPT-IN (v3.6.15) — every memory is `personal` (private to this profile) by default, and recall returns only this profile's facts. Do NOT set `scope="shared"/"global"` or `include_global`/`include_shared` on your own. Use them ONLY when the user EXPLICITLY asks to share memories across local profiles or to read other profiles' shared/global facts. Default behaviour is identical to single-profile SLM.
# CLI fallback (MCP unavailable)
recall→`slm recall "<q>" --limit N` · search→`slm search "<q>"` · remember→`slm remember "<c>" --tags a,b --project p --importance N` · list→`slm list --limit N` · forget→`slm forget` (preview first) · status→`slm status`. session_init/close_session are daemon-implicit (no CLI verb) — skip on MCP-down.
recall→`slm recall "<q>" --limit N` (add `--include-global`/`--include-shared` only on explicit user request) · search→`slm search "<q>"` · remember→`slm remember "<c>" --tags a,b` (project/importance are MCP-only, NOT CLI flags; `--scope shared --shared-with a,b` only when the user asks to share) · list→`slm list --limit N` · forget→`slm forget` (preview first) · status→`slm status`. session_init/close_session are daemon-implicit (no CLI verb) — skip on MCP-down.

@@ -45,2 +46,2 @@ # What NOT to do

SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later

@@ -38,2 +38,2 @@ ---

SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later

@@ -22,2 +22,2 @@ ---

SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later

@@ -16,2 +16,2 @@ ---

SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later

@@ -14,4 +14,4 @@ ---

5. Confirm only on success:true. If success is not true, report the error — never claim "saved."
6. MCP unavailable → CLI fallback: `slm remember "$ARGUMENTS" --tags <tags> --importance <n>`.
6. MCP unavailable → CLI fallback: `slm remember "$ARGUMENTS" --tags <tags>` (note: `--importance` is MCP-only, not a CLI flag).
SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later

@@ -15,2 +15,2 @@ ---

SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later
{
"version": "3.6.14",
"version": "3.6.15",
"pluginName": "superlocalmemory",

@@ -4,0 +4,0 @@ "displayName": "SuperLocalMemory",

@@ -1,1 +0,1 @@

superlocalmemory==3.6.14
superlocalmemory==3.6.15

@@ -21,2 +21,3 @@ # SuperLocalMemory — Agent Rules

- **Never dump a whole file** — extract only the durable decision or constraint; store the path reference if needed.
- **Scope is opt-in (v3.6.15)** — memories are `personal` (private to this profile) by default. Only pass `scope="shared"/"global"` (or recall's `include_global`/`include_shared`) when the user EXPLICITLY asks to share across local profiles. Never opt in on your own; the default is identical to single-profile SLM.

@@ -60,3 +61,3 @@ ---

| `search` | `slm search "<query>"` |
| `remember` | `slm remember "<content>" --tags a,b --project p --importance N` |
| `remember` | `slm remember "<content>" --tags a,b` (project/importance are MCP-only) |
| `list_recent` | `slm list --limit N` |

@@ -77,4 +78,4 @@ | `forget` | `slm forget` (always preview first) |

|--------------------|--------------------------------------------------------------------------------|------------------------------------|
| `remember` | `content, tags="", project="", importance=5, session_id="", agent_id` | Store atomic fact |
| `recall` | `query, limit=10, agent_id, session_id="", fast=False` | Multi-channel semantic retrieval |
| `remember` | `content, tags="", project="", importance=5, session_id="", scope="personal", shared_with=""` | Store atomic fact. `scope` opt-in (personal default) |
| `recall` | `query, limit=10, agent_id, session_id="", fast=False, include_global, include_shared` | Multi-channel retrieval. Scope flags off by default |
| `search` | `query, limit=10, profile_id=""` | FTS5 BM25 keyword search |

@@ -93,2 +94,2 @@ | `fetch` | `url, ...` | Fetch remote content |

SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later

@@ -140,2 +140,2 @@ ---

SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later

@@ -143,2 +143,2 @@ ---

SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later

@@ -300,2 +300,2 @@ ---

SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later

@@ -167,2 +167,5 @@ ---

# Opt into shared/global facts for one query (v3.6.15 — off by default)
slm recall "<query>" --include-global --include-shared
# Keyword/FTS5 search (alias: slm search)

@@ -179,3 +182,3 @@ slm search "<query>" [--limit N] [--json]

Flags verified in source (main.py):
- `slm recall`: `--limit`, `--fast`, `--json`
- `slm recall`: `--limit`, `--fast`, `--json`, `--include-global` / `--no-global`, `--include-shared` / `--no-shared`
- `slm search`: `--limit`, `--json`

@@ -185,2 +188,7 @@ - `slm trace`: `--limit`, `--json`

> **Multi-scope (v3.6.15, opt-in):** recall is shared-OFF by default — it returns only
> this profile's facts. Pass `--include-global` / `--include-shared` (or the MCP
> `include_global` / `include_shared` args) to opt in for a query, or set the defaults in
> your `mode_a/b/c.json` config. See [docs/shared-memory.md](../../../docs/shared-memory.md).
**Flags that do NOT exist** (fabricated in old skills — never write these):

@@ -199,2 +207,2 @@ `--min-score`, `--format`, `--project`, `--tags` on recall or search.

*SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later*
*SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later*

@@ -106,5 +106,12 @@ ---

session_id: str = "",# from session_init; attributes the write to this session
scope: str = None, # v3.6.15 multi-scope: "personal" (default) | "shared" | "global"
shared_with: str = "",# comma-separated profile_ids for scope="shared"
)
```
> **Multi-scope (v3.6.15, opt-in):** leave `scope` unset for `personal` (private to
> this profile — the default, identical to 3.6.14). `"global"` is visible to every
> profile on the machine; `"shared"` is visible to the profiles in `shared_with`.
> See [docs/shared-memory.md](../../../docs/shared-memory.md).
**importance scale:**

@@ -163,4 +170,9 @@ - 1–3: Low — passing notes, ideas, soft preferences

# Flags verified in source (main.py): --tags, --json, --sync
# Store a shared/global fact (v3.6.15, opt-in)
slm remember "<content>" --scope global
slm remember "<content>" --scope shared --shared-with alice,bob
# Flags verified in source (main.py): --tags, --json, --sync, --scope, --shared-with
# --sync: wait for full enrichment before returning (default is async)
# --scope: personal (default) | shared | global ; --shared-with: profile ids for shared
```

@@ -184,2 +196,2 @@

*SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later*
*SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later*

@@ -207,2 +207,2 @@ ---

*SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later*
*SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later*

@@ -149,2 +149,2 @@ ---

SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later

@@ -19,3 +19,3 @@ {

"repository": "https://github.com/qualixar/superlocalmemory",
"version": "3.6.14"
"version": "3.6.15"
}

@@ -20,5 +20,5 @@ ---

- `session_init(project_path, query, max_results, max_age_days)` — ONCE at session start; returns recent decisions + relevant memories.
- `recall(query, limit, session_id, fast)` — multi-channel semantic retrieval (default limit 10).
- `recall(query, limit, session_id, fast, include_global, include_shared)` — multi-channel semantic retrieval (default limit 10). Leave `include_global`/`include_shared` unset — recall is private-by-default (v3.6.15).
- `search(query, limit, profile_id)` — exact keyword / FTS5 BM25.
- `remember(content, tags, project, importance, session_id)` — store atomic fact; importance 1-10.
- `remember(content, tags, project, importance, session_id, scope, shared_with)` — store atomic fact; importance 1-10. Leave `scope` unset (defaults to `personal`/private).
- `update_memory(fact_id, content)` — correct by exact id.

@@ -37,5 +37,6 @@ - `forget(profile_id, dry_run)` — decay cycle; ALWAYS dry_run=True first, report, never apply blind.

7. SESSION END — close_session(session_id) when work meaningfully complete.
8. SCOPE IS OPT-IN (v3.6.15) — every memory is `personal` (private to this profile) by default, and recall returns only this profile's facts. Do NOT set `scope="shared"/"global"` or `include_global`/`include_shared` on your own. Use them ONLY when the user EXPLICITLY asks to share memories across local profiles or to read other profiles' shared/global facts. Default behaviour is identical to single-profile SLM.
# CLI fallback (MCP unavailable)
recall→`slm recall "<q>" --limit N` · search→`slm search "<q>"` · remember→`slm remember "<c>" --tags a,b --project p --importance N` · list→`slm list --limit N` · forget→`slm forget` (preview first) · status→`slm status`. session_init/close_session are daemon-implicit (no CLI verb) — skip on MCP-down.
recall→`slm recall "<q>" --limit N` (add `--include-global`/`--include-shared` only on explicit user request) · search→`slm search "<q>"` · remember→`slm remember "<c>" --tags a,b` (project/importance are MCP-only, NOT CLI flags; `--scope shared --shared-with a,b` only when the user asks to share) · list→`slm list --limit N` · forget→`slm forget` (preview first) · status→`slm status`. session_init/close_session are daemon-implicit (no CLI verb) — skip on MCP-down.

@@ -45,2 +46,2 @@ # What NOT to do

SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later

@@ -38,2 +38,2 @@ ---

SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later

@@ -1,2 +0,2 @@

<!-- BEGIN SuperLocalMemory v3.6.14 -->
<!-- BEGIN SuperLocalMemory v3.6.15 -->

@@ -15,2 +15,3 @@ ## SuperLocalMemory (SLM) — Agent Rules

- Never dump a whole file; never claim "saved" without success:true.
- Scope is opt-in (v3.6.15): writes are `personal`/private by default. Only use `scope="shared"/"global"` (or recall's `include_global`/`include_shared`) when the user explicitly asks to share across local profiles.

@@ -31,3 +32,3 @@ ### Recall

### CLI fallback (MCP down)
`slm recall "<q>" --limit N` · `slm search "<q>"` · `slm remember "<c>" --tags t --importance N` · `slm list --limit N` · `slm forget` (preview first) · `slm status` · `slm optimize status`
`slm recall "<q>" --limit N` · `slm search "<q>"` · `slm remember "<c>" --tags t` · `slm list --limit N` · `slm forget` (preview first) · `slm status` · `slm optimize status`

@@ -43,4 +44,4 @@ ### Skills

<!-- END SuperLocalMemory v3.6.14 -->
<!-- END SuperLocalMemory v3.6.15 -->
SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later

@@ -1,1 +0,1 @@

superlocalmemory==3.6.14
superlocalmemory==3.6.15

@@ -140,2 +140,2 @@ ---

SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later

@@ -143,2 +143,2 @@ ---

SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later

@@ -300,2 +300,2 @@ ---

SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later

@@ -167,2 +167,5 @@ ---

# Opt into shared/global facts for one query (v3.6.15 — off by default)
slm recall "<query>" --include-global --include-shared
# Keyword/FTS5 search (alias: slm search)

@@ -179,3 +182,3 @@ slm search "<query>" [--limit N] [--json]

Flags verified in source (main.py):
- `slm recall`: `--limit`, `--fast`, `--json`
- `slm recall`: `--limit`, `--fast`, `--json`, `--include-global` / `--no-global`, `--include-shared` / `--no-shared`
- `slm search`: `--limit`, `--json`

@@ -185,2 +188,7 @@ - `slm trace`: `--limit`, `--json`

> **Multi-scope (v3.6.15, opt-in):** recall is shared-OFF by default — it returns only
> this profile's facts. Pass `--include-global` / `--include-shared` (or the MCP
> `include_global` / `include_shared` args) to opt in for a query, or set the defaults in
> your `mode_a/b/c.json` config. See [docs/shared-memory.md](../../../docs/shared-memory.md).
**Flags that do NOT exist** (fabricated in old skills — never write these):

@@ -199,2 +207,2 @@ `--min-score`, `--format`, `--project`, `--tags` on recall or search.

*SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later*
*SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later*

@@ -106,5 +106,12 @@ ---

session_id: str = "",# from session_init; attributes the write to this session
scope: str = None, # v3.6.15 multi-scope: "personal" (default) | "shared" | "global"
shared_with: str = "",# comma-separated profile_ids for scope="shared"
)
```
> **Multi-scope (v3.6.15, opt-in):** leave `scope` unset for `personal` (private to
> this profile — the default, identical to 3.6.14). `"global"` is visible to every
> profile on the machine; `"shared"` is visible to the profiles in `shared_with`.
> See [docs/shared-memory.md](../../../docs/shared-memory.md).
**importance scale:**

@@ -163,4 +170,9 @@ - 1–3: Low — passing notes, ideas, soft preferences

# Flags verified in source (main.py): --tags, --json, --sync
# Store a shared/global fact (v3.6.15, opt-in)
slm remember "<content>" --scope global
slm remember "<content>" --scope shared --shared-with alice,bob
# Flags verified in source (main.py): --tags, --json, --sync, --scope, --shared-with
# --sync: wait for full enrichment before returning (default is async)
# --scope: personal (default) | shared | global ; --shared-with: profile ids for shared
```

@@ -184,2 +196,2 @@

*SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later*
*SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later*

@@ -207,2 +207,2 @@ ---

*SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later*
*SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later*

@@ -149,2 +149,2 @@ ---

SuperLocalMemory v3.6.14 · Qualixar · AGPL-3.0-or-later
SuperLocalMemory v3.6.15 · Qualixar · AGPL-3.0-or-later
[project]
name = "superlocalmemory"
version = "3.6.14"
version = "3.6.15"
description = "Information-geometric agent memory with mathematical guarantees"

@@ -5,0 +5,0 @@ readme = "README.md"

@@ -5,6 +5,6 @@ <p align="center">

<h1 align="center">SuperLocalMemory V3.6.14</h1>
<h1 align="center">SuperLocalMemory V3.6.15</h1>
<p align="center"><strong>Cache. Compress. Remember. Three surfaces — proxy, MCP tools, or skill. Every setup covered.</strong><br/>
<em>To the best of our knowledge, the only zero-cloud agent memory that beats Mem0's zero-LLM score on LoCoMo. Mode A: 74.8% vs Mem0 64.2% — no GPU, no API key, on CPU.</em></p>
<p align="center"><code>v3.6.14</code> — <strong>Plugin-native. Profile-aware. Distributed-ready.</strong><br/>
<p align="center"><code>v3.6.15</code> — <strong>Plugin-native. Profile-aware. Distributed-ready.</strong><br/>
Proxy: <code>slm wrap claude</code> &nbsp;·&nbsp; MCP: add <code>slm_compress</code> to your config &nbsp;·&nbsp; Skill: zero-config</p>

@@ -104,2 +104,4 @@ <p align="center"><strong>3 published research papers</strong> (arXiv preprints + Zenodo-archived) · <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>

**Multi-scope memory (v3.6.15, opt-in):** keep memories `personal` (default), `shared` with named profiles, or `global` across the machine. Off by default — recall only ever returns your own facts until you turn sharing on, per call or in config. See **[docs/shared-memory.md](docs/shared-memory.md)**.
<a id="multilingual-embedding-support"></a>

@@ -297,2 +299,3 @@

|---|---|---|
| **v3.6.15** | Multi-scope | **Opt-in [shared memory](docs/shared-memory.md)** (personal/shared/global, off by default), default-deny scope at every read path, recall scope-race fix, contributor PRs #42/#43/#44, fixes #46–#49 |
| **v3.6.14** | Plugin-native | Claude Code Plugin (WP-06), MCP profiles (WP-01), IDE connect (WP-08), asset consolidation, UI polish (WP-12) |

@@ -299,0 +302,0 @@ | **v3.6.x** | Optimize Everywhere / Distributed-ready | Three surfaces (proxy/MCP/skill), `SLM_REMOTE=1` LAN mode, remote dashboard, custom LLM endpoints |

@@ -33,3 +33,3 @@ #!/usr/bin/env node

// ---------------------------------------------------------------------------
const VERSION = '3.6.14';
const VERSION = '3.6.15';
const MANIFEST_REL = 'plugin-src/manifest.json';

@@ -36,0 +36,0 @@ const GENERATED_BANNER = `# _GENERATED — DO NOT HAND-EDIT

Metadata-Version: 2.4
Name: superlocalmemory
Version: 3.6.14
Version: 3.6.15
Summary: Information-geometric agent memory with mathematical guarantees

@@ -99,6 +99,6 @@ Author-email: Varun Pratap Bhardwaj <admin@superlocalmemory.com>

<h1 align="center">SuperLocalMemory V3.6.14</h1>
<h1 align="center">SuperLocalMemory V3.6.15</h1>
<p align="center"><strong>Cache. Compress. Remember. Three surfaces — proxy, MCP tools, or skill. Every setup covered.</strong><br/>
<em>To the best of our knowledge, the only zero-cloud agent memory that beats Mem0's zero-LLM score on LoCoMo. Mode A: 74.8% vs Mem0 64.2% — no GPU, no API key, on CPU.</em></p>
<p align="center"><code>v3.6.14</code> — <strong>Plugin-native. Profile-aware. Distributed-ready.</strong><br/>
<p align="center"><code>v3.6.15</code> — <strong>Plugin-native. Profile-aware. Distributed-ready.</strong><br/>
Proxy: <code>slm wrap claude</code> &nbsp;·&nbsp; MCP: add <code>slm_compress</code> to your config &nbsp;·&nbsp; Skill: zero-config</p>

@@ -198,2 +198,4 @@ <p align="center"><strong>3 published research papers</strong> (arXiv preprints + Zenodo-archived) · <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>

**Multi-scope memory (v3.6.15, opt-in):** keep memories `personal` (default), `shared` with named profiles, or `global` across the machine. Off by default — recall only ever returns your own facts until you turn sharing on, per call or in config. See **[docs/shared-memory.md](docs/shared-memory.md)**.
<a id="multilingual-embedding-support"></a>

@@ -391,2 +393,3 @@

|---|---|---|
| **v3.6.15** | Multi-scope | **Opt-in [shared memory](docs/shared-memory.md)** (personal/shared/global, off by default), default-deny scope at every read path, recall scope-race fix, contributor PRs #42/#43/#44, fixes #46–#49 |
| **v3.6.14** | Plugin-native | Claude Code Plugin (WP-06), MCP profiles (WP-01), IDE connect (WP-08), asset consolidation, UI polish (WP-12) |

@@ -393,0 +396,0 @@ | **v3.6.x** | Optimize Everywhere / Distributed-ready | Three surfaces (proxy/MCP/skill), `SLM_REMOTE=1` LAN mode, remote dashboard, custom LLM endpoints |

@@ -436,2 +436,3 @@ AUTHORS.md

src/superlocalmemory/storage/migrations/M015_add_pinned_column.py
src/superlocalmemory/storage/migrations/M016_add_scope_support.py
src/superlocalmemory/storage/migrations/__init__.py

@@ -438,0 +439,0 @@ src/superlocalmemory/trust/__init__.py

@@ -35,3 +35,3 @@ """SuperLocalMemory — information-geometric agent memory.

__version__ = "3.6.14"
__version__ = "3.6.15"

@@ -38,0 +38,0 @@ _REQUIRED_VERSIONS = {

@@ -238,2 +238,11 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

)
remember_p.add_argument(
"--scope", default=None, choices=("personal", "shared", "global"),
help="Memory scope: personal, shared, or global. Unset uses the "
"configured default_scope (personal). Shared memory is opt-in.",
)
remember_p.add_argument(
"--shared-with", default=None,
help="Comma-separated profile IDs for shared scope",
)

@@ -255,2 +264,22 @@ # v3.6.12 (parity-3): `search` is an alias of `recall` so the CLI has the

)
# v3.6.15: shared memory is opt-in. Unset (None) → resolve the configured
# default (recall_include_global/shared, both False by default). Explicit
# flags override per-call. default=None on BOTH members of each pair so the
# store_false's implicit default=True can't sneak back in.
recall_p.add_argument(
"--include-global", dest="include_global", action="store_true", default=None,
help="Include global-scope facts in retrieval (opt-in; default off)",
)
recall_p.add_argument(
"--no-global", dest="include_global", action="store_false", default=None,
help="Exclude global-scope facts from retrieval",
)
recall_p.add_argument(
"--include-shared", dest="include_shared", action="store_true", default=None,
help="Include facts shared with this profile (opt-in; default off)",
)
recall_p.add_argument(
"--no-shared", dest="include_shared", action="store_false", default=None,
help="Exclude shared-scope facts from retrieval",
)

@@ -385,2 +414,18 @@ forget_p = sub.add_parser("forget", help="Delete memories matching a query (fuzzy)")

# #49: local session open/close (for hooks; no model roundtrip)
session_p = sub.add_parser(
"session", help="Open/close a session locally (for hooks; no model roundtrip)"
)
session_sub = session_p.add_subparsers(dest="session_command", title="session actions")
sopen_p = session_sub.add_parser("open", help="Warm session context")
sopen_p.add_argument("--project-path", default="", help="Project path to derive the warm query")
sopen_p.add_argument("--query", default="", help="Explicit warm query")
sopen_p.add_argument("--max-results", type=int, default=10, help="Max memories to warm (default 10)")
sclose_p = session_sub.add_parser(
"close", help="Close session, create temporal summaries"
)
sclose_p.add_argument(
"--session-id", default="", help="Session to close (default: most recent)"
)
obs_p = sub.add_parser("observe", help="Auto-capture content (pipe or argument)")

@@ -387,0 +432,0 @@ obs_p.add_argument("content", nargs="?", default="", help="Content to evaluate")

@@ -382,2 +382,29 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

# -- Multi-scope (shared memory) opt-in — v3.6.15 --
# OFF by default: your memories stay private to this profile (3.6.14 behaviour).
# Enabling only turns ON recall VISIBILITY of other profiles' shared/global facts;
# your own writes still default to 'personal' (mark a memory shared/global per call
# with --scope). Existing users can flip this later in the `scope` section of
# mode_a/b/c.json. See docs/shared-memory.md.
print()
print(" Shared memory lets other local profiles' 'shared'/'global' memories")
print(" appear in your recall. It is OFF by default — your memories stay private.")
if interactive:
sm_choice = _prompt(
" See shared/global memories from other profiles by default? [y/N] (default: N): ",
"n",
).lower()
else:
sm_choice = "n"
if sm_choice in ("y", "yes"):
from superlocalmemory.core.config import ScopeConfig
config.scope = ScopeConfig(
default_scope="personal",
recall_include_global=True,
recall_include_shared=True,
)
print(" ✓ Shared-memory recall ENABLED (your own writes still default to personal).")
else:
print(" ✓ Shared memory OFF (default) — enable later in mode_*.json if needed.")
if choice == "b":

@@ -384,0 +411,0 @@ print()

@@ -345,3 +345,4 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

try:
self._db.conn.execute(
# #47 fix: DatabaseManager has no `.conn`; execute() commits itself.
self._db.execute(
"INSERT OR REPLACE INTO backend_status "

@@ -352,5 +353,4 @@ "(backend_name, status, record_count, error_message, last_sync_at) "

)
self._db.conn.commit()
except Exception:
pass
except Exception as exc:
logger.debug("backend_status update failed for %s: %s", name, exc)

@@ -366,6 +366,10 @@ # ------------------------------------------------------------------

)
if not schema_version_applied(self._db.conn):
result = apply_migration(self._db.conn)
if result.get("errors"):
logger.warning("Schema v3.4.5 had errors: %s", result["errors"])
# #47 fix: use raw_connection() — DatabaseManager has no `.conn`,
# so the old code raised AttributeError that was silently swallowed,
# leaving the v3.4.5 migration (access_count_30d) permanently unapplied.
with self._db.raw_connection() as conn:
if not schema_version_applied(conn):
result = apply_migration(conn)
if result.get("errors"):
logger.warning("Schema v3.4.5 had errors: %s", result["errors"])
except ImportError:

@@ -372,0 +376,0 @@ logger.debug("schema_v345 not found — skipping")

@@ -133,2 +133,75 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

# ---------------------------------------------------------------------------
# Scope Weights
# ---------------------------------------------------------------------------
@dataclass
class ScopeWeights:
"""RRF fusion weights for multi-scope retrieval.
Personal scope has highest weight (most relevant to current profile).
Shared scope has medium weight (team/group memories).
Global scope has lowest weight (public/common knowledge).
"""
personal: float = 1.0
shared: float = 0.7
global_: float = 0.5 # trailing underscore avoids Python keyword
def __post_init__(self) -> None:
for name in ("personal", "shared", "global_"):
val = getattr(self, name)
if val < 0:
raise ValueError(f"ScopeWeights values must be non-negative, got {name}={val}")
def as_dict(self) -> dict[str, float]:
return {"personal": self.personal, "shared": self.shared, "global": self.global_}
# ---------------------------------------------------------------------------
# Scope Config (multi-scope memory behaviour defaults)
# ---------------------------------------------------------------------------
_VALID_SCOPES = ("personal", "shared", "global")
@dataclass
class ScopeConfig:
"""User-facing defaults for multi-scope (shared) memory.
SHARED MEMORY IS OPT-IN — NOT a default feature (v3.6.15 product decision).
The defaults below make a fresh / unconfigured install behave EXACTLY like
3.6.14: every write is ``personal`` and recall returns only this profile's
own facts. Another profile's ``global``/``shared`` facts never leak into
recall until the user explicitly turns sharing on.
- ``default_scope='personal'`` → writes stay private by default;
- ``recall_include_global=False`` → don't surface other profiles' global;
- ``recall_include_shared=False`` → don't surface facts shared *to* me.
Turning it on is a deliberate act, done either per-call (``--scope`` /
``--include-global`` / MCP args) or persistently by editing config.json /
mode_a|b|c.json (the installer can also write the choice). The CLI/MCP
boundary passes ``None`` ("not specified") so the engine resolves these
config values as the effective default.
"""
default_scope: str = "personal" # scope assigned to new memories
recall_include_global: bool = False # surface scope='global' facts in recall
recall_include_shared: bool = False # surface scope='shared' facts in recall
def __post_init__(self) -> None:
if self.default_scope not in _VALID_SCOPES:
raise ValueError(
f"default_scope must be one of {_VALID_SCOPES}, got {self.default_scope!r}"
)
def as_dict(self) -> dict:
return {
"default_scope": self.default_scope,
"recall_include_global": self.recall_include_global,
"recall_include_shared": self.recall_include_shared,
}
# ---------------------------------------------------------------------------
# Encoding Config

@@ -706,2 +779,4 @@ # ---------------------------------------------------------------------------

channel_weights: ChannelWeights = field(default_factory=ChannelWeights)
scope_weights: ScopeWeights = field(default_factory=ScopeWeights)
scope: ScopeConfig = field(default_factory=ScopeConfig)
encoding: EncodingConfig = field(default_factory=EncodingConfig)

@@ -865,2 +940,32 @@ retrieval: RetrievalConfig = field(default_factory=RetrievalConfig)

# Multi-scope memory: scope weights
sw = data.get("scope_weights", {})
if sw:
# v3.6.15: a malformed value (e.g. negative weight) must NOT brick
# every `slm` command via an uncaught ValueError out of load().
# Fall back to defaults and warn — the config is recoverable.
try:
config.scope_weights = ScopeWeights(**{
k: v for k, v in sw.items()
if k in ScopeWeights.__dataclass_fields__
})
except (ValueError, TypeError) as exc:
logger.warning(
"Ignoring invalid scope_weights in config (%s) — using defaults", exc
)
# Multi-scope memory: behaviour defaults (default scope + recall visibility)
sc = data.get("scope", {})
if sc:
# Same guard: a typo'd default_scope must not crash the whole CLI.
try:
config.scope = ScopeConfig(**{
k: v for k, v in sc.items()
if k in ScopeConfig.__dataclass_fields__
})
except (ValueError, TypeError) as exc:
logger.warning(
"Ignoring invalid scope config (%s) — using shared-off defaults", exc
)
return config

@@ -958,2 +1063,12 @@

# Multi-scope memory: scope weights
data["scope_weights"] = {
"personal": self.scope_weights.personal,
"shared": self.scope_weights.shared,
"global_": self.scope_weights.global_,
}
# Multi-scope memory: behaviour defaults
data["scope"] = self.scope.as_dict()
# Preserve existing V3.3 config sections that aren't in for_mode()

@@ -960,0 +1075,0 @@ for key in ("forgetting", "quantization", "sagq", "embedding_signature", "auto_invoke"):

@@ -167,2 +167,21 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

# v3.6.15: apply ALL pending migrations — including DEFERRED ones like
# M016 (scope/shared_with columns) — for DIRECT-engine usage: `slm
# remember --sync`, the Python API, and LangChain/CrewAI integrations.
# Previously only the daemon lifespan ran apply_deferred, so an existing
# pre-3.6.15 database used WITHOUT the daemon hit
# "table memories has no column named scope" on the first scoped write.
# Idempotent (skips applied migrations) + non-fatal; mirrors the daemon.
try:
from superlocalmemory.storage.migration_runner import (
apply_all, apply_deferred,
)
_base = self._config.base_dir
_learning_db = _base / "learning.db"
_memory_db = self._db.db_path
apply_all(_learning_db, _memory_db)
apply_deferred(_learning_db, _memory_db)
except Exception as exc:
logger.warning("v3.6.15 deferred migration apply failed: %s", exc)
# V3.4.7: Apply "Learning Brain" schema (tool_events, behavioral_assertions)

@@ -336,3 +355,21 @@ try:

try:
self.store(item["content"])
# v3.6.15 multi-scope: the pending row carries a metadata JSON
# blob that may hold scope/shared_with (written by the async
# /remember path). Replay them so a queued ``--scope global``
# write lands as global, not silently downgraded to personal.
import json as _json
meta = item.get("metadata")
if isinstance(meta, str):
try:
meta = _json.loads(meta) if meta else {}
except (ValueError, TypeError):
meta = {}
if not isinstance(meta, dict):
meta = {}
_scope = meta.get("scope") or "personal"
_shared = meta.get("shared_with")
self.store(
item["content"], metadata=meta or None,
scope=_scope, shared_with=_shared,
)
mark_done(item["id"], base_dir)

@@ -353,4 +390,11 @@ except Exception as exc:

metadata: dict[str, Any] | None = None,
*,
scope: str = "personal",
shared_with: list[str] | None = None,
) -> list[str]:
"""Store content and extract structured facts. Returns fact_ids."""
"""Store content and extract structured facts. Returns fact_ids.
Multi-scope: ``scope`` sets the visibility (personal/shared/global).
``shared_with`` is a list of profile_ids for shared scope.
"""
self._require_full("store")

@@ -364,2 +408,3 @@ self._ensure_init()

speaker=speaker, role=role, metadata=metadata,
scope=scope, shared_with=shared_with,
config=self._config, db=self._db,

@@ -404,3 +449,6 @@ embedder=self._embedder,

def store_fast(self, content: str, metadata: dict[str, Any] | None = None) -> list[str]:
def store_fast(
self, content: str, metadata: dict[str, Any] | None = None,
*, scope: str = "personal", shared_with: list[str] | None = None,
) -> list[str]:
"""v3.5.5 WRITE-THROUGH: synchronous verbatim insert for IMMEDIATE recall.

@@ -463,2 +511,3 @@

metadata=metadata or {},
scope=scope, shared_with=shared_with,
)

@@ -494,2 +543,3 @@ self._db.store_memory(record)

created_at=now,
scope=scope, shared_with=shared_with,
)

@@ -521,2 +571,5 @@ self._db.store_fact(fact) # FTS5 trigger → immediately BM25-recallable

fast: bool = False,
*,
include_global: bool | None = None,
include_shared: bool | None = None,
) -> RecallResponse:

@@ -537,2 +590,11 @@ """Recall relevant facts for a query.

but is silently treated as False.
Multi-scope: ``include_global`` / ``include_shared`` control which
scopes participate in retrieval. ``None`` (the default) means "use the
configured ScopeConfig default", which ships OFF — shared memory is
opt-in (v3.6.15). Personal facts are ALWAYS returned regardless, so a
config of False reproduces 3.6.14 pure-isolation behaviour exactly.
This is the single policy chokepoint: every recall path (CLI, MCP,
daemon HTTP, in-process adapter) flows through here, so a caller that
forgets to thread the flag still gets the safe configured default.
"""

@@ -542,2 +604,9 @@ self._require_full("recall")

# Resolve None → configured ScopeConfig default (shared-off by default).
_scope_cfg = getattr(self._config, "scope", None)
if include_global is None:
include_global = bool(getattr(_scope_cfg, "recall_include_global", False))
if include_shared is None:
include_shared = bool(getattr(_scope_cfg, "recall_include_shared", False))
if fast:

@@ -565,2 +634,4 @@ logger.warning(

fast=fast,
include_global=include_global,
include_shared=include_shared,
)

@@ -567,0 +638,0 @@

@@ -193,3 +193,4 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

facts = c.execute(
f"SELECT fact_id, content, confidence, created_at, canonical_entities_json "
f"SELECT fact_id, content, confidence, created_at, canonical_entities_json, "
f"scope, shared_with "
f"FROM atomic_facts "

@@ -219,2 +220,17 @@ f"WHERE fact_id IN ({placeholders}) ORDER BY created_at",

# v3.6.15 multi-scope: a summary must never be MORE visible than its
# sources, or it would leak a private fact into a shared/global summary.
# Preserve scope only when the whole cluster agrees; any mix (or shared
# facts with differing targets) falls back to 'personal' — the most
# restrictive scope. All-personal clusters (the common case) are
# unchanged. shared_with is preserved only for a uniform shared cluster.
_src_scopes = {(f["scope"] or "personal") for f in facts}
_src_shared = {f["shared_with"] for f in facts}
if _src_scopes == {"global"}:
_sum_scope, _sum_shared = "global", None
elif _src_scopes == {"shared"} and len(_src_shared) == 1:
_sum_scope, _sum_shared = "shared", facts[0]["shared_with"]
else:
_sum_scope, _sum_shared = "personal", None
# Collect entities from ALL source facts (already in the SELECT)

@@ -258,4 +274,4 @@ all_entities = set()

confidence, importance, evidence_count, access_count,
created_at, lifecycle)
VALUES (?, '', ?, ?, 'semantic', ?, ?, ?, 0.8, ?, 0, ?, 'active')
created_at, lifecycle, scope, shared_with)
VALUES (?, '', ?, ?, 'semantic', ?, ?, ?, 0.8, ?, 0, ?, 'active', ?, ?)
""", (

@@ -266,2 +282,3 @@ new_fact_id, profile_id, summary,

round(avg_confidence, 3), len(facts), now,
_sum_scope, _sum_shared,
))

@@ -268,0 +285,0 @@

@@ -67,2 +67,6 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

return True
except ProcessLookupError:
return False # ESRCH — no such process
except PermissionError:
return True # EPERM — process EXISTS, we just can't signal it
except OSError:

@@ -77,2 +81,6 @@ return False

return True
except ProcessLookupError:
return False # ESRCH — no such process
except PermissionError:
return True # EPERM — process EXISTS, we just can't signal it
except OSError:

@@ -79,0 +87,0 @@ return False

@@ -590,5 +590,10 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

fast: bool = False,
include_global: bool = False,
include_shared: bool = False,
) -> RecallResponse:
"""Recall relevant facts for a query.
Multi-scope: ``include_global`` / ``include_shared`` control which
scopes participate in retrieval (passed through to retrieval engine).
Pipeline: retrieval -> agentic sufficiency (if configured) -> post-recall updates.

@@ -627,2 +632,4 @@

extra_disabled_channels=extra_disabled,
include_global=include_global,
include_shared=include_shared,
)

@@ -629,0 +636,0 @@ _mark("retrieval(chan+rerank)")

@@ -64,6 +64,11 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

query: str, limit: int, session_id: str = "", fast: bool = False,
include_global: bool | None = None, include_shared: bool | None = None,
) -> dict:
engine = _get_engine()
# v3.6.15 multi-scope: None flags let engine.recall resolve the configured
# default (shared-off). The subprocess loads its own SLMConfig, so the
# resolution is identical to the in-process / daemon paths.
response = engine.recall(
query, limit=limit, session_id=session_id or None, fast=bool(fast),
include_global=include_global, include_shared=include_shared,
)

@@ -289,2 +294,4 @@

req.get("session_id", ""), bool(req.get("fast", False)),
include_global=req.get("include_global"),
include_shared=req.get("include_shared"),
)

@@ -291,0 +298,0 @@ _respond(result)

@@ -102,2 +102,13 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

signal_type=signal, created_at=fact.created_at,
pinned=getattr(fact, 'pinned', False),
# v3.6.15 multi-scope: scope is a per-MEMORY property — every fact
# derived from a memory inherits the memory's scope. The record is
# authoritative; fact-extractor output never carries scope, so reading
# it off the fact (as before) silently downgraded extracted facts to
# 'personal' and broke `--scope global` on the common extraction path.
scope=(getattr(record, 'scope', None)
or getattr(fact, 'scope', None) or 'personal'),
shared_with=(getattr(record, 'shared_with', None)
if getattr(record, 'scope', None) in ('shared', 'global')
else getattr(fact, 'shared_with', None)),
)

@@ -150,2 +161,4 @@

*,
scope: str = "personal",
shared_with: list[str] | None = None,
config: SLMConfig,

@@ -174,3 +187,7 @@ db: DatabaseManager,

) -> list[str]:
"""Store content and extract structured facts. Returns fact_ids."""
"""Store content and extract structured facts. Returns fact_ids.
Multi-scope: ``scope`` sets visibility (personal/shared/global).
``shared_with`` is a list of profile_ids for shared scope.
"""
# Pre-operation hooks (trust gate, ABAC, rate limiter)

@@ -209,2 +226,3 @@ hook_ctx = {

session_date=parsed_date, metadata=metadata or {},
scope=scope, shared_with=shared_with,
)

@@ -266,2 +284,4 @@ db.store_memory(record)

importance=0.5,
scope=scope,
shared_with=shared_with,
)

@@ -288,2 +308,4 @@ # Avoid duplicate if extraction already produced the exact same text

importance=0.3,
scope=scope,
shared_with=shared_with,
)]

@@ -290,0 +312,0 @@

@@ -71,2 +71,4 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

fast: bool = False,
include_global: bool | None = None,
include_shared: bool | None = None,
) -> dict:

@@ -79,7 +81,17 @@ """Run recall in worker subprocess. Returns result dict.

attach to.
v3.6.15 multi-scope: ``include_global``/``include_shared`` are forwarded
to the worker (and on to ``engine.recall``). ``None`` is sent verbatim
so the worker-side engine resolves the configured default — shared
memory is opt-in.
"""
return self._send({
msg = {
"cmd": "recall", "query": query, "limit": limit,
"session_id": session_id or "", "fast": bool(fast),
})
}
if include_global is not None:
msg["include_global"] = bool(include_global)
if include_shared is not None:
msg["include_shared"] = bool(include_shared)
return self._send(msg)

@@ -86,0 +98,0 @@ def store(self, content: str, metadata: dict | None = None) -> dict:

@@ -31,6 +31,10 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

Concurrency: one reader/writer lock (``fcntl.flock``) serialises
updates. Rollover: entries older than 1 hour are pruned on every
write. Fail-soft: every error path returns empty or the passed
default — the learning loop must never crash the hot path.
Concurrency: each write is atomic via write-temp + ``os.replace`` (atomic on
POSIX/Windows), so a concurrent reader never sees a half-written file —
last-writer-wins. This is best-effort, not lock-serialised: a concurrent
read-modify-write may lose an interleaved update, which is acceptable because
the registry only drives session attribution for closed-loop learning, not
memory correctness. Rollover: entries older than 1 hour are pruned on every
write. Fail-soft: every error path returns empty or the passed default — the
learning loop must never crash the hot path.

@@ -37,0 +41,0 @@ This is not a perfect correlation channel; two Claude sessions

@@ -49,4 +49,6 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

fast: bool = False,
include_global: bool | None = None,
include_shared: bool | None = None,
) -> dict[str, Any]:
params = urllib.parse.urlencode({
_params: dict[str, Any] = {
"q": query,

@@ -56,3 +58,11 @@ "limit": limit,

"fast": "true" if fast else "false",
})
}
# v3.6.15 multi-scope: only send the scope flags when explicitly set, so
# an unset value lets the daemon resolve the configured default (shared
# is opt-in). "None" must NOT become the string "none" on the wire.
if include_global is not None:
_params["include_global"] = "true" if include_global else "false"
if include_shared is not None:
_params["include_shared"] = "true" if include_shared else "false"
params = urllib.parse.urlencode(_params)
try:

@@ -59,0 +69,0 @@ with urllib.request.urlopen(

@@ -83,8 +83,17 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

"""
raw = _pool().recall(
query=query,
limit=limit,
session_id=str(kwargs.get("session_id") or ""),
fast=bool(kwargs.get("fast", False)),
)
# v3.6.15 multi-scope: forward the scope-visibility flags when the caller
# set them. ``None`` (the default) is passed through so the daemon/engine
# resolves the configured default — shared memory is opt-in, so omitting
# them keeps recall scoped to this profile only.
_recall_kwargs: dict[str, Any] = {
"query": query,
"limit": limit,
"session_id": str(kwargs.get("session_id") or ""),
"fast": bool(kwargs.get("fast", False)),
}
if "include_global" in kwargs:
_recall_kwargs["include_global"] = kwargs["include_global"]
if "include_shared" in kwargs:
_recall_kwargs["include_shared"] = kwargs["include_shared"]
raw = _pool().recall(**_recall_kwargs)
_unwrap_error(raw, "recall")

@@ -91,0 +100,0 @@ items = raw.get("results", []) if isinstance(raw, dict) else []

@@ -109,2 +109,4 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

agent_id: str = "mcp_client",
scope: str | None = None,
shared_with: str = "",
) -> dict:

@@ -115,2 +117,6 @@ """Store content to memory with intelligent indexing.

and indexes for 4-channel retrieval.
Multi-scope: ``scope`` sets visibility (personal/shared/global).
``shared_with`` is a comma-separated list of profile_ids for
shared scope.
"""

@@ -127,2 +133,4 @@ # v3.6.10: resolve "mcp_client" sentinel → URL path (HTTP) or env var (stdio)

}
# Parse shared_with from comma-separated string
_shared_list = [s.strip() for s in shared_with.split(",") if s.strip()] if shared_with else None
# v3.5.5 WRITE-THROUGH: route through the daemon's /remember, which does

@@ -142,2 +150,3 @@ # a synchronous verbatim insert (memory is keyword/BM25-recallable the

"content": content, "tags": tags, "metadata": meta,
"scope": scope, "shared_with": _shared_list,
})

@@ -158,2 +167,9 @@ if resp and (resp.get("fact_ids") is not None or resp.get("ok")):

from superlocalmemory.cli.pending_store import store_pending
# v3.6.15: preserve a non-personal scope through the offline path so
# the materializer replays the right visibility (else --scope global
# would silently downgrade to personal when the daemon is offline).
if scope and scope != "personal":
meta = {**meta, "scope": scope}
if _shared_list:
meta["shared_with"] = _shared_list
pending_id = store_pending(content, tags=tags, metadata=meta)

@@ -176,2 +192,4 @@ return {

session_id: str = "", fast: bool = False,
include_global: bool | None = None,
include_shared: bool | None = None,
) -> dict:

@@ -185,2 +203,7 @@ """Search memories by semantic query with 4-channel retrieval, RRF fusion, and reranking.

learning for this recall" — the recall itself always works.
Multi-scope: ``include_global`` / ``include_shared`` control which
scopes participate in retrieval. Leave them unset (``None``) to use the
configured default — shared memory is OPT-IN, so by default recall
returns only this profile's own facts. Pass ``True`` to opt in per call.
"""

@@ -240,2 +263,4 @@ # v3.6.10: resolve "mcp_client" sentinel → URL path (HTTP) or env var (stdio)

fast=bool(fast),
include_global=include_global,
include_shared=include_shared,
)

@@ -242,0 +267,0 @@ if result.get("ok"):

@@ -292,4 +292,9 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

try:
import asyncio
from superlocalmemory.mcp._daemon_proxy import choose_pool
raw = choose_pool().recall(query=query, limit=limit)
# choose_pool().recall uses blocking urllib; run off the event loop
# so recall_trace doesn't stall the MCP server for other tools.
raw = await asyncio.to_thread(
lambda: choose_pool().recall(query=query, limit=limit)
)
items = raw.get("results", []) if isinstance(raw, dict) else []

@@ -296,0 +301,0 @@ results = []

@@ -225,4 +225,6 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

# MCP process. Fall back to local import only if no daemon.
daemon_result = _try_daemon_post(
"/consolidate/cognitive", {"profile_id": pid},
import asyncio
# blocking urllib (60s timeout) — keep it off the MCP event loop.
daemon_result = await asyncio.to_thread(
_try_daemon_post, "/consolidate/cognitive", {"profile_id": pid},
)

@@ -438,4 +440,6 @@ if daemon_result is not None:

# the MCP process.
daemon_result = _try_daemon_post(
"/maintenance/run", {"profile_id": pid},
import asyncio
# blocking urllib — keep it off the MCP event loop.
daemon_result = await asyncio.to_thread(
_try_daemon_post, "/maintenance/run", {"profile_id": pid},
)

@@ -442,0 +446,0 @@ if daemon_result is not None:

@@ -89,5 +89,11 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

token_map = self._db.get_all_bm25_tokens(profile_id)
_inc_global = getattr(self, 'include_global', False)
_inc_shared = getattr(self, 'include_shared', False)
if not token_map:
# Fallback: tokenize facts directly if no pre-stored tokens
facts = self._db.get_all_facts(profile_id)
facts = self._db.get_all_facts(
profile_id,
include_global=_inc_global,
include_shared=_inc_shared,
)
for fact in facts:

@@ -108,3 +114,7 @@ if fact.fact_id in self._fact_id_set:

try:
facts = self._db.get_all_facts(profile_id)
facts = self._db.get_all_facts(
profile_id,
include_global=_inc_global,
include_shared=_inc_shared,
)
fact_content_map = {f.fact_id: f.content for f in facts}

@@ -111,0 +121,0 @@ except Exception:

@@ -19,2 +19,3 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

import re
import threading
import time

@@ -88,2 +89,8 @@ from typing import TYPE_CHECKING, Any, Protocol

self._trust_scorer = trust_scorer
# v3.6.15: serialise the per-recall scope-flag set + channel execution.
# Channel instances are SHARED across concurrent recalls (the daemon runs
# several in parallel); without this, recall B's flags could overwrite
# recall A's mid-flight on the shared channels. Uncontended for a single
# recall (~0 cost); only the channel phase of concurrent recalls serialises.
self._scope_lock = threading.Lock()

@@ -122,5 +129,11 @@ # V3.3.4: LRU cache for query embeddings (avoids redundant Ollama API calls)

extra_disabled_channels: set[str] | None = None,
include_global: bool = True,
include_shared: bool = True,
) -> RecallResponse:
"""Full retrieval pipeline: strategy -> channels -> RRF -> rerank.
Multi-scope: ``include_global`` / ``include_shared`` control which
scopes participate in retrieval. Both default to True for backward
compatibility (existing data has scope='personal' — no effect).
V3.4.40 (2026-05-09): ``extra_disabled_channels`` allows callers to

@@ -133,2 +146,8 @@ skip specific channels for a single recall (e.g. SpreadingActivation

# Multi-scope: scope flags are set on the (shared) channel instances +
# the channels executed atomically under self._scope_lock — see the
# `# 3. Run channels` block below. (profile_channel does not read scope.)
self._include_global = include_global
self._include_shared = include_shared
# v3.5.0 diagnostic: stage timing inside retrieval (SLM_RECALL_TIMING=1).

@@ -166,4 +185,14 @@ import os as _os_e

# 3. Run 4 channels
ch_results = self._run_channels(query, profile_id, strat)
# 3. Run channels. Set the scope flags on the shared channel instances
# and execute them under self._scope_lock so a concurrent recall can't
# interleave its scope visibility onto these channels mid-flight. The
# worker threads spawned inside _run_channels are joined before the lock
# releases, so every channel read sees THIS recall's flags.
with self._scope_lock:
for ch in (self._semantic, self._bm25, self._entity, self._temporal,
self._hopfield, self._spreading_activation):
if ch is not None:
ch.include_global = include_global
ch.include_shared = include_shared
ch_results = self._run_channels(query, profile_id, strat)
_em("run_channels")

@@ -665,3 +694,7 @@ if profile_hits:

return {}
facts = self._db.get_facts_by_ids(needed, profile_id)
facts = self._db.get_facts_by_ids(
needed, profile_id,
include_global=getattr(self, '_include_global', True),
include_shared=getattr(self, '_include_shared', True),
)
return {f.fact_id: f for f in facts}

@@ -668,0 +701,0 @@

@@ -286,3 +286,3 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

else:
for fact in self._db.get_facts_by_entity(eid, profile_id):
for fact in self._db.get_facts_by_entity(eid, profile_id, include_global=getattr(self, 'include_global', False), include_shared=getattr(self, 'include_shared', False)):
activation[fact.fact_id] = max(activation[fact.fact_id], 1.0)

@@ -321,3 +321,3 @@

# This fallback exists for mock/test DBs. See Phase 7 LLD H-01.
for edge in self._db.get_edges_for_node(fid, profile_id):
for edge in self._db.get_edges_for_node(fid, profile_id, include_global=getattr(self, 'include_global', False), include_shared=getattr(self, 'include_shared', False)):
neighbor = edge.target_id if edge.source_id == fid else edge.source_id

@@ -347,3 +347,3 @@ propagated = activation[fid] * self._decay

visited_entities.add(eid)
for fact in self._db.get_facts_by_entity(eid, profile_id):
for fact in self._db.get_facts_by_entity(eid, profile_id, include_global=getattr(self, 'include_global', False), include_shared=getattr(self, 'include_shared', False)):
if hop_decay > activation.get(fact.fact_id, 0.0):

@@ -444,3 +444,3 @@ activation[fact.fact_id] = hop_decay

else:
for fact in self._db.get_facts_by_entity(eid, profile_id):
for fact in self._db.get_facts_by_entity(eid, profile_id, include_global=getattr(self, 'include_global', False), include_shared=getattr(self, 'include_shared', False)):
activation[fact.fact_id] = max(activation[fact.fact_id], 1.0)

@@ -635,3 +635,3 @@

for entity_id, score in scored:
facts = self._db.get_facts_by_entity(entity_id, profile_id)
facts = self._db.get_facts_by_entity(entity_id, profile_id, include_global=getattr(self, 'include_global', False), include_shared=getattr(self, 'include_shared', False))
for fact in facts:

@@ -638,0 +638,0 @@ fact_scores.append((fact.fact_id, score))

@@ -251,3 +251,7 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

candidate_ids = [fid for fid, _ in knn_results]
candidates = self._db.get_facts_by_ids(candidate_ids, profile_id)
candidates = self._db.get_facts_by_ids(
candidate_ids, profile_id,
include_global=getattr(self, 'include_global', False),
include_shared=getattr(self, 'include_shared', False),
)
if not candidates:

@@ -308,3 +312,7 @@ return []

# deserialize the whole table just to slice it.
facts = self._db.get_all_facts(profile_id, limit=5000)
facts = self._db.get_all_facts(
profile_id, limit=5000,
include_global=getattr(self, 'include_global', False),
include_shared=getattr(self, 'include_shared', False),
)
if not facts:

@@ -311,0 +319,0 @@ return (None, [])

@@ -171,3 +171,7 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

knn_scores = {fid: score for fid, score in knn_results}
facts = self._db.get_facts_by_ids(candidate_ids, profile_id)
facts = self._db.get_facts_by_ids(
candidate_ids, profile_id,
include_global=getattr(self, 'include_global', False),
include_shared=getattr(self, 'include_shared', False),
)

@@ -234,3 +238,7 @@ if not facts:

facts = self._db.get_all_facts(profile_id)
facts = self._db.get_all_facts(
profile_id,
include_global=getattr(self, 'include_global', False),
include_shared=getattr(self, 'include_shared', False),
)

@@ -237,0 +245,0 @@ scored: list[tuple[str, float]] = []

@@ -46,2 +46,45 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

def _scope_where(
profile_id: str,
*,
include_global: bool = False,
include_shared: bool = False,
prefix: str = "",
) -> tuple[str, list]:
"""Build scope-filtering WHERE clause for multi-scope retrieval.
Returns ``(where_clause, params)`` for splicing into SQL queries.
When ``include_global=True``, facts with ``scope='global'`` are included
regardless of profile. When ``include_shared=True``, facts explicitly
shared with this profile (via ``shared_with`` JSON array) are also
included.
v3.6.15: defaults are SHARED-OFF (include_global/include_shared=False) so
any DIRECT caller (search, list_recent, fetch, resources) is private by
default — shared memory is opt-in. The recall channels pass explicit
resolved flags, so opt-in recall is unaffected. With both False the clause
collapses to ``profile_id = ?`` — identical to 3.6.14 isolation.
"""
table = f"{prefix}." if prefix else ""
clauses = [f"({table}profile_id = ?)"]
params: list = [profile_id]
if include_global:
clauses.append(f"({table}scope = 'global')")
if include_shared:
# Match the profile_id as a quoted JSON-array element. ESCAPE the LIKE
# metacharacters in profile_id so a profile id containing % or _ cannot
# false-positive-match another profile's shared_with list.
_esc = profile_id.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
clauses.append(
f"({table}scope = 'shared' AND {table}shared_with LIKE ? ESCAPE '\\')"
)
params.append(f'%"{_esc}"%')
where = "(" + " OR ".join(clauses) + ")"
return where, params
class DatabaseManager:

@@ -119,2 +162,24 @@ """Concurrent-safe SQLite manager with WAL, profile isolation, and FTS5.

@contextmanager
def raw_connection(self) -> Generator[sqlite3.Connection, None, None]:
"""Yield a live sqlite3.Connection for code that needs one directly.
For callers (e.g. schema migrations) that must hold a real connection
rather than going through execute(). Commits on success, rolls back on
error, and always closes — mirroring transaction(). This is the public
way to obtain a connection; there is no `.conn` attribute.
"""
with self._lock:
conn = self._connect()
self._txn_conn = conn
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
self._txn_conn = None
conn.close()
def execute(self, sql: str, params: tuple[Any, ...] = ()) -> list[sqlite3.Row]:

@@ -154,11 +219,14 @@ """Execute SQL with automatic retry on SQLITE_BUSY.

"""Persist a raw memory record. Returns memory_id."""
_scope = getattr(record, 'scope', None) or 'personal'
_shared = _jd(getattr(record, 'shared_with', None))
self.execute(
"""INSERT OR REPLACE INTO memories
(memory_id, profile_id, content, session_id, speaker,
role, session_date, created_at, metadata_json)
VALUES (?,?,?,?,?,?,?,?,?)""",
role, session_date, created_at, metadata_json,
scope, shared_with)
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
(record.memory_id, record.profile_id, record.content,
record.session_id, record.speaker, record.role,
record.session_date, record.created_at,
json.dumps(record.metadata)),
json.dumps(record.metadata), _scope, _shared),
)

@@ -235,2 +303,4 @@ return record.memory_id

return canonical_id
_scope = getattr(fact, 'scope', None) or 'personal'
_shared = _jd(getattr(fact, 'shared_with', None))
self.execute(

@@ -245,4 +315,5 @@ """INSERT OR REPLACE INTO atomic_facts

lifecycle, langevin_position,
emotional_valence, emotional_arousal, signal_type, created_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
emotional_valence, emotional_arousal, signal_type, created_at,
scope, shared_with)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(fact.fact_id, fact.memory_id, fact.profile_id, fact.content,

@@ -258,3 +329,3 @@ fact.fact_type.value,

fact.emotional_valence, fact.emotional_arousal,
fact.signal_type.value, fact.created_at),
fact.signal_type.value, fact.created_at, _scope, _shared),
)

@@ -289,2 +360,4 @@ return fact.fact_id

pinned=bool(d.get("pinned", 0)),
scope=d.get("scope", "personal"),
shared_with=_jl(d.get("shared_with"), None),
created_at=d["created_at"],

@@ -300,8 +373,17 @@ )

def get_pinned(self, profile_id: str) -> list[AtomicFact]:
def get_pinned(
self, profile_id: str,
include_global: bool = False,
include_shared: bool = False,
) -> list[AtomicFact]:
"""Return all pinned facts for a profile, highest-importance first."""
where, params = _scope_where(
profile_id,
include_global=include_global,
include_shared=include_shared,
)
rows = self.execute(
"SELECT * FROM atomic_facts WHERE profile_id = ? AND pinned = 1 "
f"SELECT * FROM atomic_facts WHERE {where} AND pinned = 1 "
"ORDER BY importance DESC",
(profile_id,),
(*params,),
)

@@ -312,2 +394,5 @@ return [self._row_to_fact(r) for r in rows]

self, profile_id: str, limit: int | None = None,
*,
include_global: bool = False,
include_shared: bool = False,
) -> list[AtomicFact]:

@@ -320,12 +405,17 @@ """All facts for a profile, newest first.

"""
where, params = _scope_where(
profile_id,
include_global=include_global,
include_shared=include_shared,
)
if limit is not None:
rows = self.execute(
"SELECT * FROM atomic_facts WHERE profile_id = ? "
f"SELECT * FROM atomic_facts WHERE {where} "
"ORDER BY created_at DESC LIMIT ?",
(profile_id, int(limit)),
(*params, int(limit)),
)
else:
rows = self.execute(
"SELECT * FROM atomic_facts WHERE profile_id = ? ORDER BY created_at DESC",
(profile_id,),
f"SELECT * FROM atomic_facts WHERE {where} ORDER BY created_at DESC",
(*params,),
)

@@ -336,3 +426,7 @@ return [self._row_to_fact(r) for r in rows]

def get_facts_by_entity(self, entity_id: str, profile_id: str) -> list[AtomicFact]:
def get_facts_by_entity(
self, entity_id: str, profile_id: str,
include_global: bool = False,
include_shared: bool = False,
) -> list[AtomicFact]:
"""Facts whose canonical_entities JSON array contains *entity_id*.

@@ -345,15 +439,29 @@

"""
where, params = _scope_where(
profile_id,
include_global=include_global,
include_shared=include_shared,
)
rows = self.execute(
"SELECT * FROM atomic_facts WHERE profile_id = ? AND canonical_entities_json LIKE ? "
f"SELECT * FROM atomic_facts WHERE {where} AND canonical_entities_json LIKE ? "
"ORDER BY created_at DESC LIMIT ?",
(profile_id, f'%"{entity_id}"%', self._MAX_FACTS_PER_ENTITY_LOOKUP),
(*params, f'%"{entity_id}"%', self._MAX_FACTS_PER_ENTITY_LOOKUP),
)
return [self._row_to_fact(r) for r in rows]
def get_facts_by_type(self, fact_type: FactType, profile_id: str) -> list[AtomicFact]:
def get_facts_by_type(
self, fact_type: FactType, profile_id: str,
include_global: bool = False,
include_shared: bool = False,
) -> list[AtomicFact]:
"""All facts of a given type for a profile."""
where, params = _scope_where(
profile_id,
include_global=include_global,
include_shared=include_shared,
)
rows = self.execute(
"SELECT * FROM atomic_facts WHERE profile_id = ? AND fact_type = ? "
f"SELECT * FROM atomic_facts WHERE {where} AND fact_type = ? "
"ORDER BY created_at DESC",
(profile_id, fact_type.value),
(*params, fact_type.value),
)

@@ -426,6 +534,15 @@ return [self._row_to_fact(r) for r in rows]

def get_fact_count(self, profile_id: str) -> int:
def get_fact_count(
self, profile_id: str,
include_global: bool = False,
include_shared: bool = False,
) -> int:
"""Total fact count for a profile."""
where, params = _scope_where(
profile_id,
include_global=include_global,
include_shared=include_shared,
)
rows = self.execute(
"SELECT COUNT(*) AS c FROM atomic_facts WHERE profile_id = ?", (profile_id,),
f"SELECT COUNT(*) AS c FROM atomic_facts WHERE {where}", (*params,),
)

@@ -497,8 +614,15 @@ return int(rows[0]["c"]) if rows else 0

self, memory_id: str, profile_id: str,
include_global: bool = False,
include_shared: bool = False,
) -> list[AtomicFact]:
"""Get all atomic facts for a given memory_id."""
where, params = _scope_where(
profile_id,
include_global=include_global,
include_shared=include_shared,
)
rows = self.execute(
"SELECT * FROM atomic_facts WHERE memory_id = ? AND profile_id = ? "
f"SELECT * FROM atomic_facts WHERE memory_id = ? AND {where} "
"ORDER BY confidence DESC",
(memory_id, profile_id),
(memory_id, *params),
)

@@ -530,17 +654,29 @@ return [self._row_to_fact(r) for r in rows]

return canonical_id
_scope = getattr(edge, 'scope', None) or 'personal'
_shared = _jd(getattr(edge, 'shared_with', None))
self.execute(
"""INSERT OR REPLACE INTO graph_edges
(edge_id, profile_id, source_id, target_id, edge_type, weight, created_at)
VALUES (?,?,?,?,?,?,?)""",
(edge_id, profile_id, source_id, target_id, edge_type, weight, created_at,
scope, shared_with)
VALUES (?,?,?,?,?,?,?,?,?)""",
(edge.edge_id, edge.profile_id, edge.source_id, edge.target_id,
edge.edge_type.value, edge.weight, edge.created_at),
edge.edge_type.value, edge.weight, edge.created_at, _scope, _shared),
)
return edge.edge_id
def get_edges_for_node(self, node_id: str, profile_id: str) -> list[GraphEdge]:
def get_edges_for_node(
self, node_id: str, profile_id: str,
include_global: bool = False,
include_shared: bool = False,
) -> list[GraphEdge]:
"""All edges where node_id is source or target."""
where, params = _scope_where(
profile_id,
include_global=include_global,
include_shared=include_shared,
)
rows = self.execute(
"SELECT * FROM graph_edges WHERE profile_id = ? "
f"SELECT * FROM graph_edges WHERE {where} "
"AND (source_id = ? OR target_id = ?)",
(profile_id, node_id, node_id),
(*params, node_id, node_id),
)

@@ -559,2 +695,4 @@ return [

"""Persist a temporal event. Returns event_id."""
_scope = getattr(event, 'scope', None) or 'personal'
_shared = _jd(getattr(event, 'shared_with', None))
self.execute(

@@ -564,16 +702,26 @@ """INSERT OR REPLACE INTO temporal_events

observation_date, referenced_date, interval_start, interval_end,
description)
VALUES (?,?,?,?,?,?,?,?,?)""",
description, scope, shared_with)
VALUES (?,?,?,?,?,?,?,?,?,?,?)""",
(event.event_id, event.profile_id, event.entity_id, event.fact_id,
event.observation_date, event.referenced_date,
event.interval_start, event.interval_end, event.description),
event.interval_start, event.interval_end, event.description,
_scope, _shared),
)
return event.event_id
def get_temporal_events(self, entity_id: str, profile_id: str) -> list[TemporalEvent]:
def get_temporal_events(
self, entity_id: str, profile_id: str,
include_global: bool = False,
include_shared: bool = False,
) -> list[TemporalEvent]:
"""All temporal events for an entity, newest first."""
where, params = _scope_where(
profile_id,
include_global=include_global,
include_shared=include_shared,
)
rows = self.execute(
"SELECT * FROM temporal_events WHERE profile_id = ? AND entity_id = ? "
f"SELECT * FROM temporal_events WHERE {where} AND entity_id = ? "
"ORDER BY observation_date DESC",
(profile_id, entity_id),
(*params, entity_id),
)

@@ -608,3 +756,7 @@ return [

def search_facts_fts(self, query: str, profile_id: str, limit: int = 20) -> list[AtomicFact]:
def search_facts_fts(
self, query: str, profile_id: str, limit: int = 20,
include_global: bool = False,
include_shared: bool = False,
) -> list[AtomicFact]:
"""Full-text search via FTS5, joined to facts table for reconstruction."""

@@ -620,8 +772,14 @@ # v3.6.12 (search-1): the raw query was passed straight into FTS5 MATCH,

match_expr = " OR ".join(f'"{t}"' for t in tokens)
where, params = _scope_where(
profile_id,
include_global=include_global,
include_shared=include_shared,
prefix="f",
)
rows = self.execute(
"""SELECT f.* FROM atomic_facts_fts AS fts
f"""SELECT f.* FROM atomic_facts_fts AS fts
JOIN atomic_facts AS f ON f.fact_id = fts.fact_id
WHERE fts.atomic_facts_fts MATCH ? AND f.profile_id = ?
WHERE fts.atomic_facts_fts MATCH ? AND {where}
ORDER BY fts.rank LIMIT ?""",
(match_expr, profile_id, limit),
(match_expr, *params, limit),
)

@@ -663,2 +821,4 @@ return [self._row_to_fact(r) for r in rows]

self, fact_ids: list[str], profile_id: str,
include_global: bool = False,
include_shared: bool = False,
) -> list[AtomicFact]:

@@ -668,7 +828,12 @@ """Get multiple facts by their IDs, scoped to a profile."""

return []
where, params = _scope_where(
profile_id,
include_global=include_global,
include_shared=include_shared,
)
placeholders = ",".join("?" for _ in fact_ids)
rows = self.execute(
f"SELECT * FROM atomic_facts WHERE fact_id IN ({placeholders}) "
f"AND profile_id = ? ORDER BY created_at DESC",
(*fact_ids, profile_id),
f"AND {where} ORDER BY created_at DESC",
(*fact_ids, *params),
)

@@ -844,10 +1009,17 @@ return [self._row_to_fact(r) for r in rows]

self, profile_id: str, start_date: str, end_date: str,
include_global: bool = False,
include_shared: bool = False,
) -> list[TemporalEvent]:
"""Temporal events within a date range (inclusive)."""
where, params = _scope_where(
profile_id,
include_global=include_global,
include_shared=include_shared,
)
rows = self.execute(
"SELECT * FROM temporal_events WHERE profile_id = ? "
f"SELECT * FROM temporal_events WHERE {where} "
"AND (referenced_date BETWEEN ? AND ? "
" OR observation_date BETWEEN ? AND ?) "
"ORDER BY observation_date DESC",
(profile_id, start_date, end_date, start_date, end_date),
(*params, start_date, end_date, start_date, end_date),
)

@@ -854,0 +1026,0 @@ return [

@@ -54,2 +54,3 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

M015_add_pinned_column as _M015,
M016_add_scope_support as _M016,
)

@@ -75,2 +76,3 @@

_M015.NAME: _M015,
_M016.NAME: _M016,
}

@@ -139,2 +141,5 @@

Migration(name=_M015.NAME, db_target="memory", ddl=_M015.DDL),
# M016 adds scope and shared_with columns to 5 core tables for
# multi-scope memory support (personal/global/shared).
Migration(name=_M016.NAME, db_target="memory", ddl=_M016.DDL),
]

@@ -272,3 +277,14 @@

try:
conn.executescript(migration.ddl)
# A migration module may ship a custom apply(conn) for conditional logic
# that static DDL can't express (e.g. SQLite has no ADD COLUMN IF NOT
# EXISTS, and ALTER on a missing/already-altered table can't be guarded
# in one executescript). If present, it runs instead of the DDL string;
# otherwise the DDL is applied as before. Pure-DDL migrations are
# unaffected.
_mod = _MODULES.get(migration.name)
_apply_fn = getattr(_mod, "apply", None) if _mod is not None else None
if callable(_apply_fn):
_apply_fn(conn)
else:
conn.executescript(migration.ddl)
except sqlite3.Error as exc:

@@ -275,0 +291,0 @@ # Best-effort rollback.

@@ -122,2 +122,4 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

profile_id: str = "default"
scope: str = "personal"
shared_with: list[str] | None = None
content: str = ""

@@ -143,2 +145,4 @@ session_id: str = ""

profile_id: str = "default"
scope: str = "personal"
shared_with: list[str] | None = None
content: str = "" # Atomic fact statement

@@ -198,2 +202,4 @@ fact_type: FactType = FactType.SEMANTIC

profile_id: str = "default"
scope: str = "personal"
shared_with: list[str] | None = None
canonical_name: str = ""

@@ -257,2 +263,4 @@ entity_type: str = "" # person / place / org / concept / event

profile_id: str = "default"
scope: str = "personal"
shared_with: list[str] | None = None
entity_id: str = "" # FK to CanonicalEntity

@@ -273,2 +281,4 @@ fact_id: str = "" # FK to AtomicFact

profile_id: str = "default"
scope: str = "personal"
shared_with: list[str] | None = None
source_id: str = "" # Fact ID or Entity ID

@@ -275,0 +285,0 @@ target_id: str = "" # Fact ID or Entity ID

@@ -113,2 +113,4 @@ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar

profile_id TEXT NOT NULL DEFAULT 'default',
scope TEXT NOT NULL DEFAULT 'personal',
shared_with TEXT,
content TEXT NOT NULL,

@@ -131,4 +133,3 @@ session_id TEXT NOT NULL DEFAULT '',

CREATE INDEX IF NOT EXISTS idx_memories_created
ON memories (created_at);
"""
ON memories (created_at);"""

@@ -145,2 +146,4 @@

profile_id TEXT NOT NULL DEFAULT 'default',
scope TEXT NOT NULL DEFAULT 'personal',
shared_with TEXT,
content TEXT NOT NULL,

@@ -214,4 +217,3 @@ fact_type TEXT NOT NULL DEFAULT 'semantic'

ON atomic_facts (profile_id, interval_start, interval_end)
WHERE interval_start IS NOT NULL;
"""
WHERE interval_start IS NOT NULL;"""

@@ -294,2 +296,4 @@

profile_id TEXT NOT NULL DEFAULT 'default',
scope TEXT NOT NULL DEFAULT 'personal',
shared_with TEXT,
canonical_name TEXT NOT NULL,

@@ -310,4 +314,3 @@ entity_type TEXT NOT NULL DEFAULT '',

CREATE INDEX IF NOT EXISTS idx_entities_type
ON canonical_entities (profile_id, entity_type);
"""
ON canonical_entities (profile_id, entity_type);"""

@@ -395,2 +398,4 @@

profile_id TEXT NOT NULL DEFAULT 'default',
scope TEXT NOT NULL DEFAULT 'personal',
shared_with TEXT,
entity_id TEXT NOT NULL,

@@ -418,4 +423,3 @@ fact_id TEXT NOT NULL,

ON temporal_events (profile_id, referenced_date)
WHERE referenced_date IS NOT NULL;
"""
WHERE referenced_date IS NOT NULL;"""

@@ -431,2 +435,4 @@

profile_id TEXT NOT NULL DEFAULT 'default',
scope TEXT NOT NULL DEFAULT 'personal',
shared_with TEXT,
source_id TEXT NOT NULL,

@@ -455,4 +461,3 @@ target_id TEXT NOT NULL,

CREATE INDEX IF NOT EXISTS idx_edges_exists_check
ON graph_edges (profile_id, source_id, target_id, edge_type);
"""
ON graph_edges (profile_id, source_id, target_id, edge_type);"""

@@ -459,0 +464,0 @@

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display