sessionmem 1.3.0 → 1.4.0

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.
@@ -1,19 +1,15 @@
1
1
  import { AdapterFactory } from "../../adapters/factory.js";
2
2
  import { mkdirSync, writeFileSync } from "fs";
3
- import { join } from "path";
4
- import { homedir } from "os";
3
+ import { join, dirname } from "path";
5
4
  import { deriveProjectId } from "../projectId.js";
6
- import { expandTilde } from "../context.js";
5
+ import { resolveDbPath } from "../context.js";
7
6
  export async function runMcpServer() {
8
7
  const adapter = AdapterFactory.detectAdapter();
9
- // Startup diagnostics: written to ~/.sessionmem/logs/mcp.log
10
- const logDir = join(homedir(), ".sessionmem", "logs");
8
+ // Keep diagnostics beside the selected database so an override never
9
+ // creates the default home directory just to write a log.
10
+ const dbPath = resolveDbPath();
11
+ const logDir = join(dirname(dbPath), "logs");
11
12
  const logPath = join(logDir, "mcp.log");
12
- // Derive db path for diagnostics (mirrors context.ts defaultDbPath)
13
- const envDbPath = process.env.SESSIONMEM_DB_PATH;
14
- const dbPath = envDbPath && envDbPath.trim() !== ""
15
- ? expandTilde(envDbPath)
16
- : join(homedir(), ".sessionmem", "memories.db");
17
13
  // Derive project ID for diagnostics. Shares the exact derivation the server
18
14
  // uses (createCliContext → deriveProjectId) so the logged id never diverges
19
15
  // from the bucket the server actually serves.
@@ -21,8 +17,10 @@ export async function runMcpServer() {
21
17
  const adapterName = adapter.name;
22
18
  const logMessage = `[${new Date().toISOString()}] Started sessionmem | adapter=${adapterName} db=${dbPath} project=${projectId}\n`;
23
19
  try {
24
- mkdirSync(logDir, { recursive: true });
25
- writeFileSync(logPath, logMessage, { flag: "a" });
20
+ if (dbPath !== ":memory:") {
21
+ mkdirSync(logDir, { recursive: true });
22
+ writeFileSync(logPath, logMessage, { flag: "a" });
23
+ }
26
24
  }
27
25
  catch {
28
26
  // best-effort logging; ignore failures
@@ -1,18 +1,22 @@
1
1
  import { createCliContext } from "../context.js";
2
2
  import { formatTable } from "../output.js";
3
- const DEFAULT_LIMIT = 20;
3
+ const DEFAULT_LIMIT = 10;
4
4
  function coerceLimit(value) {
5
5
  if (value === undefined)
6
6
  return DEFAULT_LIMIT;
7
- const parsed = Number.parseInt(String(value), 10);
8
- return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_LIMIT;
7
+ const parsed = Number(value);
8
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
9
+ throw new Error(`Invalid --limit "${value}": expected an integer from 1 to 100.`);
10
+ }
11
+ return parsed;
9
12
  }
10
13
  export async function searchCommand(query, options = {}, ctx) {
14
+ const limit = coerceLimit(options.limit);
11
15
  const context = ctx ?? createCliContext();
12
16
  const result = await context.service.call("retrieveMemories", {
13
17
  projectId: context.projectId,
14
18
  query,
15
- limit: coerceLimit(options.limit),
19
+ limit,
16
20
  mode: "auto",
17
21
  depth: "default",
18
22
  });
@@ -1,6 +1,7 @@
1
1
  import { existsSync, rmSync } from "fs";
2
2
  import { join } from "path";
3
3
  import { homedir } from "os";
4
+ import { resolveDbPath } from "../context.js";
4
5
  import { AdapterFactory } from "../../adapters/factory.js";
5
6
  import { removeClaudeMdBlock } from "../../adapters/claudeMdInjector.js";
6
7
  /**
@@ -51,7 +52,7 @@ export async function uninstallCommand(options = {}) {
51
52
  }
52
53
  }
53
54
  // Resolve dbPath: use injected override (for tests) or the default location
54
- const dbPath = options.dbPath ?? join(homedir(), ".sessionmem", "memories.db");
55
+ const dbPath = resolveDbPath(options.dbPath);
55
56
  if (options.purge) {
56
57
  // --purge: delete only memories.db, not logs or the ~/.sessionmem directory
57
58
  if (existsSync(dbPath)) {
@@ -42,19 +42,21 @@ export function expandTilde(p) {
42
42
  }
43
43
  return p;
44
44
  }
45
- function defaultDbPath(dir) {
45
+ export function resolveDbPath(override) {
46
+ if (override !== undefined)
47
+ return expandTilde(override);
46
48
  // Env override seam (see deriveProjectId). Defaults to ~/.sessionmem/memories.db.
47
49
  const envDbPath = process.env.SESSIONMEM_DB_PATH;
48
50
  if (envDbPath && envDbPath.trim() !== "")
49
51
  return expandTilde(envDbPath);
50
- // `dir` is the fixed ~/.sessionmem dir computed above, not user input.
51
52
  // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
52
- return join(dir, "memories.db");
53
+ return join(homedir(), ".sessionmem", "memories.db");
53
54
  }
54
55
  export function createCliContext(overrides = {}) {
55
- const dir = join(homedir(), ".sessionmem");
56
- mkdirSync(dir, { recursive: true });
57
- const dbPath = overrides.dbPath ?? defaultDbPath(dir);
56
+ const dbPath = resolveDbPath(overrides.dbPath);
57
+ if (!overrides.db && dbPath !== ":memory:") {
58
+ mkdirSync(dirname(dbPath), { recursive: true });
59
+ }
58
60
  const here = dirname(fileURLToPath(import.meta.url));
59
61
  const migrationsDir = overrides.migrationsDir ?? join(here, "..", "core", "schema", "migrations");
60
62
  const username = overrides.username ?? localUsername();
package/dist/cli/index.js CHANGED
@@ -30,7 +30,7 @@ import { browseCommand } from "./commands/browse.js";
30
30
  // "../../package.json" specifier resolves to the package root.
31
31
  const pkg = createRequire(import.meta.url)("../../package.json");
32
32
  const program = new Command();
33
- program.name("sessionmem").version(pkg.version);
33
+ program.name("sessionmem").description("Local-first memory for MCP coding agents").version(pkg.version);
34
34
  program
35
35
  .command("run")
36
36
  .description("Start the sessionmem MCP server")
@@ -74,15 +74,15 @@ program
74
74
  program
75
75
  .command("search <query>")
76
76
  .description("Search memories by semantic query")
77
- .option("--limit <n>", "Maximum number of results", "10")
77
+ .option("--limit <n>", "Maximum number of results (1-100)", "10")
78
78
  .action((query, options) => searchCommand(query, options));
79
79
  program
80
80
  .command("list")
81
- .description("List all memories for the current project")
81
+ .description("List up to 200 recent memories for the current project")
82
82
  .action(() => listCommand());
83
83
  program
84
84
  .command("show <id>")
85
- .description("Show full details of a memory by ID")
85
+ .description("Show memory details by ID (content capped at 2000 characters)")
86
86
  .action((id) => showCommand(id));
87
87
  program
88
88
  .command("forget <id>")
@@ -96,7 +96,7 @@ program
96
96
  program
97
97
  .command("import <path>")
98
98
  .description("Import memories from a JSON file")
99
- .option("--merge", "Overwrite existing memories with imported data")
99
+ .option("--merge", "Merge new records; existing IDs are preserved")
100
100
  .action((path, options) => importCommand(path, options));
101
101
  program
102
102
  .command("stats")
@@ -171,7 +171,7 @@ program
171
171
  .command("dedupe")
172
172
  .description("Find and optionally remove near-duplicate memories")
173
173
  .option("--apply", "Delete the lower-importance duplicate in each pair (default: dry-run)")
174
- .option("--threshold <n>", "Cosine similarity threshold 0-1 (default: 0.85)")
174
+ .option("--threshold <n>", "Cosine similarity threshold >0 to 1 (default: 0.85)")
175
175
  .action((options) => dedupeCommand({ dry: !options.apply, threshold: options.threshold }));
176
176
  // browse — cross-project memory viewer.
177
177
  program
@@ -88,7 +88,11 @@ export function readPolicyConfig(filePath) {
88
88
  let obj;
89
89
  try {
90
90
  raw = readFileSync(filePath, "utf8");
91
- obj = JSON.parse(raw);
91
+ const parsed = JSON.parse(raw);
92
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
93
+ return { ...DEFAULT_POLICY_CONFIG };
94
+ }
95
+ obj = parsed;
92
96
  }
93
97
  catch {
94
98
  return { ...DEFAULT_POLICY_CONFIG };
@@ -10,8 +10,16 @@ export function capTokens(text, cap) {
10
10
  if (tokens.length <= cap) {
11
11
  return text;
12
12
  }
13
- const trimmed = encoding.decode(tokens.slice(0, cap)).trimEnd();
14
- return `${trimmed} ...`;
13
+ // Reserve room for the truncation marker, then verify the re-encoded text:
14
+ // token boundaries can change when a decoded prefix and suffix are joined.
15
+ const suffix = " ...";
16
+ for (let keep = Math.max(0, Math.floor(cap) - countTokens(suffix)); keep >= 0; keep--) {
17
+ const trimmed = encoding.decode(tokens.slice(0, keep)).trimEnd();
18
+ const result = trimmed ? `${trimmed}${suffix}` : suffix.trim();
19
+ if (countTokens(result) <= cap)
20
+ return result;
21
+ }
22
+ return "";
15
23
  }
16
24
  export function trimLowestPriorityContent(included, options = {}) {
17
25
  const minContentTokens = options.minContentTokens ?? DEFAULT_MIN_CONTENT_TOKENS;
@@ -57,18 +57,19 @@ export function runMigrations(db, migrationsDir = DEFAULT_MIGRATIONS_DIR) {
57
57
  const hasMigrationStmt = db.prepare("SELECT name FROM _migrations WHERE name = ? LIMIT 1");
58
58
  const insertMigrationStmt = db.prepare("INSERT INTO _migrations(name) VALUES (?)");
59
59
  const runMigration = db.transaction((fileName) => {
60
+ if (hasMigrationStmt.get(fileName))
61
+ return;
60
62
  const filePath = path.join(migrationsDir, fileName);
61
63
  const sql = fs.readFileSync(filePath, "utf8");
62
64
  db.exec(sql);
63
65
  insertMigrationStmt.run(fileName);
64
66
  });
65
67
  for (const fileName of files) {
66
- const existing = hasMigrationStmt.get(fileName);
67
- if (existing) {
68
+ if (hasMigrationStmt.get(fileName))
68
69
  continue;
69
- }
70
70
  try {
71
- runMigration(fileName);
71
+ // Serialize the check and apply across processes opening the same DB.
72
+ runMigration.immediate(fileName);
72
73
  }
73
74
  catch (err) {
74
75
  // Idempotency guard for ALTER TABLE ADD COLUMN migrations (005/006).
@@ -90,7 +91,10 @@ export function runMigrations(db, migrationsDir = DEFAULT_MIGRATIONS_DIR) {
90
91
  const filePath = path.join(migrationsDir, fileName);
91
92
  const sql = fs.readFileSync(filePath, "utf8");
92
93
  if (allAddedColumnsExist(db, sql)) {
93
- insertMigrationStmt.run(fileName);
94
+ db.transaction(() => {
95
+ if (!hasMigrationStmt.get(fileName))
96
+ insertMigrationStmt.run(fileName);
97
+ }).immediate();
94
98
  continue;
95
99
  }
96
100
  }
@@ -2,13 +2,35 @@ import BetterSqlite3 from "better-sqlite3";
2
2
  import { runMigrations } from "../schema/runMigrations.js";
3
3
  export function openDb(options = {}) {
4
4
  const db = new BetterSqlite3(options.dbPath ?? ":memory:");
5
- // Performance pragmas — WAL enables concurrent reads during writes;
6
- // busy_timeout prevents SQLITE_BUSY under concurrent MCP tool calls.
7
- db.pragma("journal_mode = WAL");
8
- db.pragma("synchronous = NORMAL");
9
- db.pragma("foreign_keys = ON");
10
- db.pragma("busy_timeout = 5000");
11
- db.pragma("cache_size = -32000");
12
- runMigrations(db, options.migrationsDir);
13
- return db;
5
+ try {
6
+ // Performance pragmas — WAL enables concurrent reads during writes;
7
+ // busy_timeout prevents SQLITE_BUSY under concurrent MCP tool calls.
8
+ // Concurrent first opens can race while switching to WAL. SQLite may
9
+ // bypass the busy handler for this lock upgrade, so retry within the same
10
+ // five-second budget used for normal writes.
11
+ const deadline = Date.now() + 5000;
12
+ const pause = new Int32Array(new SharedArrayBuffer(4));
13
+ for (;;) {
14
+ try {
15
+ db.pragma("journal_mode = WAL");
16
+ break;
17
+ }
18
+ catch (err) {
19
+ if (!(err instanceof Error) || !("code" in err) || err.code !== "SQLITE_BUSY" || Date.now() >= deadline) {
20
+ throw err;
21
+ }
22
+ Atomics.wait(pause, 0, 0, 10);
23
+ }
24
+ }
25
+ db.pragma("synchronous = NORMAL");
26
+ db.pragma("foreign_keys = ON");
27
+ db.pragma("busy_timeout = 5000");
28
+ db.pragma("cache_size = -32000");
29
+ runMigrations(db, options.migrationsDir);
30
+ return db;
31
+ }
32
+ catch (err) {
33
+ db.close();
34
+ throw err;
35
+ }
14
36
  }
@@ -1,4 +1,5 @@
1
1
  import Anthropic from "@anthropic-ai/sdk";
2
+ import { capTokens } from "../injection/tokenBudget.js";
2
3
  import { summarizeLocalSessionEvents } from "./localSummarizer.js";
3
4
  import { DEFAULT_SUMMARIZER_MODEL } from "../config/policyConfig.js";
4
5
  const CLOUD_SYSTEM_PROMPT = "You are a memory compressor for AI coding sessions. Given this session transcript summary, produce a compact, high-signal list of facts, decisions, and context that should be remembered. Be extremely concise.";
@@ -26,7 +27,7 @@ export async function summarizeWithCloud(input) {
26
27
  messages: [{ role: "user", content: localResult.summary }],
27
28
  });
28
29
  // Extract text from response content blocks
29
- let text = response.content
30
+ const text = response.content
30
31
  .filter((block) => block.type === "text")
31
32
  .map((block) => block.text)
32
33
  .join("\n");
@@ -35,14 +36,8 @@ export async function summarizeWithCloud(input) {
35
36
  if (!text || text.trim().length === 0) {
36
37
  throw new Error("Cloud summarizer returned empty response");
37
38
  }
38
- // Cap cloud output to summaryTokenCap (rough token→char ratio) for parity
39
- // with the local summarizer, so a verbose cloud response cannot blow past the
40
- // configured budget.
41
- if (input.summaryTokenCap && text.length > input.summaryTokenCap * 4) {
42
- text = text.slice(0, input.summaryTokenCap * 4);
43
- }
44
39
  return {
45
- summary: text,
40
+ summary: capTokens(text, input.summaryTokenCap),
46
41
  warningCodes: localResult.warningCodes,
47
42
  };
48
43
  }
@@ -0,0 +1,53 @@
1
+ # Architecture
2
+
3
+ `sessionmem` connects a coding agent's MCP host to a local SQLite memory database. This overview covers its four subsystems and the retrieval and injection flows. It runs locally by default, with settings in `~/.sessionmem/config.json`.
4
+
5
+ ## Subsystems
6
+
7
+ ```
8
+ ┌──────────────────────────────────────────────┐
9
+ │ MCP host │
10
+ │ (Claude Code, Cursor, Codex, Cline, ...) │
11
+ └───────────────────────┬──────────────────────┘
12
+ │ MCP / hooks
13
+ ┌───────────▼───────────┐ ┌──────────────┐
14
+ │ adapters │ │ CLI │
15
+ │ (host-specific glue) │ │ (sessionmem) │
16
+ └───────────┬───────────┘ └──────┬───────┘
17
+ │ │
18
+ ▼ ▼
19
+ ┌──────────────────────────────────────────────┐
20
+ │ core engine │
21
+ │ capture · summarize · retrieval · injection │
22
+ └───────────────────────┬──────────────────────┘
23
+ │
24
+ ▼
25
+ ┌──────────────────────────────────────────────┐
26
+ │ SQLite storage (local) │
27
+ │ memories · session events · feedback │
28
+ └──────────────────────────────────────────────┘
29
+ ```
30
+
31
+ ### Core engine
32
+
33
+ The core engine captures session events, summarizes them into stored memories at session end, ranks those memories, and selects which to inject. It uses the adapter layer for all communication with specific tools. Retrieval combines semantic similarity, recency, and importance into a relevance score.
34
+
35
+ ### Adapters
36
+
37
+ Adapters translate between each MCP host's hooks or runtime and the core engine's API. There are adapters for Claude Code, Cursor, Codex, Cline, Windsurf, Antigravity, QCoder, and generic MCP hosts. They connect host lifecycle events to the engine's capture, summarize, and inject calls. A new platform needs an adapter; the core engine stays the same.
38
+
39
+ ### CLI
40
+
41
+ The CLI (`sessionmem`) registers the server with a host and runs the MCP server. It also has commands to search, list, show, forget, export, import, prune, and configure memories. These commands call the same core engine as the adapters.
42
+
43
+ ### SQLite storage
44
+
45
+ SQLite stores memories, embeddings for vector search, raw session events, and feedback history in one local file. Database access uses `better-sqlite3`, with forward-only [migrations](migration.md). No hosted database is required.
46
+
47
+ ## Retrieval flow
48
+
49
+ When an agent or `sessionmem search` submits a query, the core engine embeds it and runs a vector search over the `memories` table. It ranks candidates by semantic similarity, recency, and importance, then returns the memories with their scores.
50
+
51
+ ## Injection flow
52
+
53
+ At session start, the engine retrieves relevant memories and trims them to a token budget, a few hundred tokens by default. It keeps critical warnings and high-importance decisions first, dropping lower-priority content as needed. The adapter adds the result to the host's context. Claude Code's installed SessionStart hook does this automatically. Other hosts need an agent or lifecycle integration to call the corresponding MCP tools; MCP registration alone does not install hooks in those hosts.
@@ -0,0 +1,50 @@
1
+ # Benchmark
2
+
3
+ `npm run benchmark` (`scripts/benchmark.mjs`) generates this report using the production functions `retrieveMemories`, `formatStartupInjection`, and `countTokens`. It measures token reduction against full session history and retrieval relevance using curated, synthetic fixtures. It runs offline with a hash-based embedding function, `deterministicEmbed`, and a fixed clock, so repeated runs produce identical output.
4
+
5
+ Startup injection used **~85.6% fewer tokens** than full session history. Retrieval had a **100.0% hit-rate** across 10 curated queries.
6
+
7
+ ## Token reduction
8
+
9
+ Token counts use the production `countTokens` function (js-tiktoken `o200k_base`). The table compares a single startup injection and average per-query context with the same full-history baseline. The result above uses startup injection.
10
+
11
+ | Framing | Baseline tokens (full history) | Injected tokens | Reduction |
12
+ |---------|-------------------------------|-----------------|-----------|
13
+ | (a) Top-3 startup injection vs full history | 1587 | 228 | 85.6% |
14
+ | (b) Per-query injected context (avg) vs full history | 1587 | 232 | 85.4% |
15
+
16
+ Row (a) measures one startup injection of the top-ranked memories. Row (b) averages the injected context across the curated queries to model per-turn retrieval.
17
+
18
+ ## Retrieval relevance
19
+
20
+ Each query runs through `retrieveMemories` with a limit of 3 results. The returned ids are compared with the expected memory ids. A query counts as a hit if at least one expected memory is returned.
21
+
22
+ - **Hit-rate:** 100.0% (10 queries)
23
+ - **Overall recall:** 100.0%
24
+ - **Overall precision:** 33.3%
25
+
26
+ | Query (memory content) | Expected memory | Recall | Precision |
27
+ |------------------------|-----------------|--------|-----------|
28
+ | We chose JWT access tokens with a 15-minute expiry and ro... | mem-auth-decision | 100.0% | 33.3% |
29
+ | The persistence layer uses better-sqlite3 with WAL mode; ... | mem-db-decision | 100.0% | 33.3% |
30
+ | Embeddings are computed offline with a deterministic sha2... | mem-embed-fact | 100.0% | 33.3% |
31
+ | Retrieval ranks candidates by a weighted blend of semanti... | mem-retrieval-fact | 100.0% | 33.3% |
32
+ | Startup injection formats the top-ranked memories under a... | mem-injection-fact | 100.0% | 33.3% |
33
+ | Never edit an already-shipped migration file in place; ad... | mem-migration-warning | 100.0% | 33.3% |
34
+ | Redaction must run on every memory-write path before stor... | mem-secret-warning | 100.0% | 33.3% |
35
+ | The CLI exposes remember, retention prune, redact-scan, a... | mem-cli-fact | 100.0% | 33.3% |
36
+ | Tests use vitest; integration specs seed an in-memory dat... | mem-testing-fact | 100.0% | 33.3% |
37
+ | The MCP server runs over stdio and exposes memory retriev... | mem-mcp-decision | 100.0% | 33.3% |
38
+
39
+ ### Relevance interpretation
40
+
41
+ `deterministicEmbed` uses a hash-based fingerprint. Identical text has a cosine similarity of 1.0; wording changes produce noisy scores. It supports exact or near-exact recall rather than semantic matching of paraphrases. These queries contain the target memory's content, so the hit-rate measures whether ranking retrieves that memory in that case. Matching paraphrases requires a semantic embedding model. Replacing the embedding model leaves the retrieval and scoring code unchanged, so the benchmark can still exercise those paths.
42
+
43
+ ## Reproducing
44
+
45
+ ```bash
46
+ npm run build # benchmark imports built functions from dist/
47
+ npm run benchmark # regenerates this file deterministically
48
+ ```
49
+
50
+ _Fixtures are synthetic; the report contains only aggregate metrics and synthetic example content. Generated by `scripts/benchmark.mjs`._
@@ -0,0 +1,57 @@
1
+ # Cloud Summarization
2
+
3
+ ## Default Mode
4
+
5
+ `sessionmem` defaults to local summarization. Local mode runs without external API calls.
6
+
7
+ ## How to Enable Cloud Mode
8
+
9
+ Cloud mode activates only when both conditions are true:
10
+
11
+ 1. `allowCloudSummarization=true`
12
+ 2. `ANTHROPIC_API_KEY` is present (mapped to `anthropicApiKey` in runtime config)
13
+
14
+ If either condition is missing, summarization stays local.
15
+
16
+ ## How It Works
17
+
18
+ Cloud summarization runs in two steps:
19
+
20
+ 1. **Local preprocessing** - the local summarizer extracts and structures session events
21
+ into a short transcript, with secret redaction applied before it leaves the machine.
22
+
23
+ 2. **Cloud compression** - the transcript is sent to the Anthropic Messages API with
24
+ instructions to compress it into a short list of facts, decisions, and context.
25
+
26
+ ### Model Configuration
27
+
28
+ The default model is `claude-sonnet-4-6`, defined as `DEFAULT_SUMMARIZER_MODEL` in
29
+ `src/core/config/policyConfig.ts`. Callers can override the model via the `model` field
30
+ in `CloudSummarizeInput`.
31
+
32
+ ### Token Limits
33
+
34
+ The API's `max_tokens` parameter is `Math.min(Math.floor((summaryTokenCap ?? 4000) * 1.5), 8192)`.
35
+ The response is then trimmed to `summaryTokenCap`.
36
+
37
+ ## Runtime Warning Signals
38
+
39
+ When cloud mode is active, lifecycle responses include:
40
+
41
+ - warning code: `cloud_summarization_enabled`
42
+ - warning message: `Cloud summarization active: allowCloudSummarization=true and ANTHROPIC_API_KEY present`
43
+
44
+ Use these warnings to check whether cloud summarization is active.
45
+
46
+ ## Failure Fallback Behavior
47
+
48
+ Cloud summarization retries 2 times. If the retries fail, it falls back to local summarization automatically.
49
+
50
+ Cloud failures are saved in `summarization_failures`. If local fallback succeeds,
51
+ the response includes a warning about the cloud failure. If fallback also fails,
52
+ an additional failure record is saved.
53
+
54
+ ## Manual Summarization When Auto Is Disabled
55
+
56
+ Setting `autoSummarize=false` disables automatic `session_end` summarization only.
57
+ You can still call `summarizeSessionToMemory` manually.
@@ -0,0 +1,42 @@
1
+ # Migration and Upgrade Policy
2
+
3
+ This guide explains SQLite migrations and what to expect when upgrading sessionmem. Configuration and the default database live under `~/.sessionmem/`.
4
+
5
+ ## Migration system
6
+
7
+ `sessionmem` updates its SQLite schema with forward-only migrations. Each migration is a `.sql` file under `src/core/schema/migrations/`. Zero-padded numeric prefixes determine the order:
8
+
9
+ ```
10
+ src/core/schema/migrations/
11
+ 001_initial.sql
12
+ 002_indexes.sql
13
+ 003_summarization_failures.sql
14
+ 004_memory_feedback.sql
15
+ 005_team_provenance.sql
16
+ ```
17
+
18
+ ### How migrations run
19
+
20
+ At startup, the runner creates the `_migrations` tracking table if needed and sorts the `.sql` files by name. It applies each migration in its own transaction, skipping any already recorded in `_migrations`. Starting sessionmem again does not reapply completed migrations. There are no down-migrations.
21
+
22
+ ### Build-time copy: `copy-migrations`
23
+
24
+ The TypeScript compiler does not copy `.sql` files. The build step `copy-migrations` runs `scripts/copy-migrations.mjs` to copy them from `src/core/schema/migrations/` to `dist/core/schema/migrations/`. This includes the migrations in the published package alongside the compiled code.
25
+
26
+ At runtime, sessionmem loads migrations relative to the installed module, using the packaged `dist` copy. It does not search the working directory, where an attacker could place a malicious `migrations/` folder (Phase 5 Decision #15). After adding or editing a migration, re-run the build so `copy-migrations` updates `dist`.
27
+
28
+ ### Adding a migration (contributors)
29
+
30
+ 1. Add a new `NNN_description.sql` file with the next ordinal in `src/core/schema/migrations/`.
31
+ 2. Keep it forward-only and idempotent where practical.
32
+ 3. Re-run the build so `copy-migrations` copies it into `dist/core/schema/migrations/`.
33
+
34
+ ## Version-upgrade policy
35
+
36
+ `sessionmem` follows **semantic versioning** (semver): `MAJOR.MINOR.PATCH`.
37
+
38
+ - **Patch / minor upgrades** work with your existing database. Additive, forward-only migrations run automatically when the upgraded version first opens it. Stored memories are preserved and no manual migration is needed.
39
+ - **Major-version upgrades** may introduce breaking changes. The release notes will describe behavior changes and any manual migration steps.
40
+ - **Downgrades after migration are unsupported.** Before a major upgrade, run `sessionmem export` to keep a portable JSON copy of your memories that you can re-import if needed.
41
+
42
+ This policy applies to upgrades between published v1 releases. Each release opens existing databases through the same forward-only migration runner.
@@ -0,0 +1,139 @@
1
+ # Privacy and Retention
2
+
3
+ `sessionmem` stores memories locally, redacts secrets before saving, and prunes old memories according to a retention policy. Configure both in `~/.sessionmem/config.json` or with the CLI commands below.
4
+
5
+ ## Secret Redaction
6
+
7
+ Redaction replaces common secret patterns with `REDACTED` before saving memory content. It runs during:
8
+
9
+ - automatic summarization at session end,
10
+ - manual memory storage (`storeMemory` or `batchStoreMemory`),
11
+ - import.
12
+
13
+ The `redactionEnabled` flag controls all of these operations and defaults to `true`.
14
+
15
+ ### Redacted categories
16
+
17
+ | Category | Examples |
18
+ |----------|----------|
19
+ | Email addresses | `user@example.com` |
20
+ | `sk-` API keys | OpenAI-style `sk-...` keys |
21
+ | AWS access keys | `AKIA...` |
22
+ | GitHub tokens | `ghp_...`, `gho_...`, and related prefixes |
23
+ | Bearer tokens | `Authorization: Bearer ...` |
24
+ | Private key blocks | `-----BEGIN ... PRIVATE KEY-----` private key blocks |
25
+ | Connection-string assignments | `password=...`, `secret=...` |
26
+ | JWTs | three-part `xxxxx.yyyyy.zzzzz` tokens |
27
+
28
+ The project's CI secret scanning checks for similar categories.
29
+
30
+ ### Partial-failure warnings
31
+
32
+ If redaction partly fails, the response includes a `redaction_partial_failure` warning through the usual lifecycle warning mechanism. The write still completes. Review the affected memory when you see this warning.
33
+
34
+ ### Disabling redaction
35
+
36
+ Setting `redactionEnabled` to `false` disables redaction for all memory writes. Leave it enabled to keep these checks.
37
+
38
+ ## Retention Policy
39
+
40
+ Retention pruning limits growth of the local store by deleting memories older than a configured age.
41
+
42
+ - **Scope:** the `memories` table only. Session events and feedback history are never touched.
43
+ - **Age basis:** a memory's `created_at` timestamp.
44
+ - **Default window:** `90` days (`retentionDays`).
45
+ - **Disabling:** set `retentionDays` to `0` to disable pruning. Negative values are rejected by the config schema.
46
+ - **Hard delete:** pruned memories are permanently deleted from SQLite. There is no archive or soft-delete.
47
+
48
+ > Pruning is permanent. Run `sessionmem export` first if you want to keep a copy of the memories.
49
+
50
+ ## Automatic vs Manual Pruning
51
+
52
+ **Automatic (session-end):** a lightweight prune check runs alongside summarization without blocking or delaying it.
53
+
54
+ **Manual:** the prune command previews deletions by default. It only deletes memories when you pass `--force`:
55
+
56
+ ```bash
57
+ sessionmem retention prune
58
+ ```
59
+
60
+ Dry-run output:
61
+
62
+ ```
63
+ Would delete N memories older than {retentionDays} days. Pass --force to confirm.
64
+ ```
65
+
66
+ To delete eligible memories:
67
+
68
+ ```bash
69
+ sessionmem retention prune --force
70
+ ```
71
+
72
+ Use `--days <n>` to set the retention window for one run, overriding `config.json` and the built-in default:
73
+
74
+ ```bash
75
+ sessionmem retention prune --days 30
76
+ ```
77
+
78
+ ## One-time Redaction Scrub
79
+
80
+ Normal memory writes do not recheck older memories. To check memories saved before redaction coverage was expanded, run `redact-scan`. It previews changes by default:
81
+
82
+ ```bash
83
+ sessionmem redact-scan
84
+ ```
85
+
86
+ The preview shows matching memories and a count. To redact and update them in place:
87
+
88
+ ```bash
89
+ sessionmem redact-scan --apply
90
+ ```
91
+
92
+ `--apply` rewrites matching memories with `REDACTED` placeholders and prints a summary count.
93
+
94
+ ## Policy Config
95
+
96
+ Policy settings live in `~/.sessionmem/config.json`:
97
+
98
+ ```json
99
+ {
100
+ "retentionDays": 90,
101
+ "redactionEnabled": true
102
+ }
103
+ ```
104
+
105
+ ### Precedence
106
+
107
+ Settings take precedence in this order, highest first:
108
+
109
+ 1. **CLI flag** (e.g. `retention prune --days 30`)
110
+ 2. **`config.json`** value
111
+ 3. **built-in default**
112
+
113
+ If `config.json` is missing or invalid, sessionmem uses the built-in defaults unless a CLI flag overrides them.
114
+
115
+ ### Reading and writing config
116
+
117
+ Use `config get` and `config set` to read and change settings:
118
+
119
+ ```bash
120
+ sessionmem config get retentionDays
121
+ sessionmem config set retention.days 30
122
+ sessionmem config set redactionEnabled false
123
+ ```
124
+
125
+ You can use either the dotted key `retention.days` or the field name `retentionDays`. Invalid values, such as a non-integer for `retentionDays` or a value other than `true`/`false` for `redactionEnabled`, produce an error without changing the file.
126
+
127
+ ### Install and uninstall behavior
128
+
129
+ - `sessionmem install` creates `config.json` with defaults (`retentionDays: 90`, `redactionEnabled: true`) if the file is missing. It leaves an existing file unchanged.
130
+ - `sessionmem uninstall` keeps `config.json` and the database unless you pass `--purge`.
131
+
132
+ ## Stats Visibility
133
+
134
+ `sessionmem stats` shows the current retention and redaction settings, for example:
135
+
136
+ ```
137
+ Retention: 90 days (12 memories eligible for pruning)
138
+ Redaction: enabled
139
+ ```