sessionmem 1.0.6 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/adapters/capabilities/fallbackTools.js +2 -2
  2. package/dist/adapters/claudeMdInjector.js +49 -5
  3. package/dist/adapters/factory.js +68 -9
  4. package/dist/adapters/generic.js +147 -12
  5. package/dist/adapters/global/antigravity.js +14 -7
  6. package/dist/adapters/global/claudeCode.js +46 -10
  7. package/dist/adapters/global/codex.js +73 -13
  8. package/dist/adapters/global/qcoder.js +18 -5
  9. package/dist/adapters/ide/cline.js +56 -9
  10. package/dist/adapters/ide/cursor.js +15 -13
  11. package/dist/adapters/ide/installer.js +201 -8
  12. package/dist/adapters/ide/windsurf.js +14 -13
  13. package/dist/cli/commands/config.js +10 -1
  14. package/dist/cli/commands/import.js +6 -1
  15. package/dist/cli/commands/install.js +57 -16
  16. package/dist/cli/commands/ping.js +42 -8
  17. package/dist/cli/commands/reEmbed.js +4 -3
  18. package/dist/cli/commands/run.js +7 -17
  19. package/dist/cli/commands/savings.js +33 -17
  20. package/dist/cli/commands/sessionEnd.js +124 -0
  21. package/dist/cli/commands/sessionStart.js +52 -0
  22. package/dist/cli/commands/sync.js +39 -9
  23. package/dist/cli/commands/uninstall.js +35 -9
  24. package/dist/cli/context.js +17 -18
  25. package/dist/cli/index.js +16 -4
  26. package/dist/cli/projectId.js +69 -0
  27. package/dist/core/api/contracts.js +155 -42
  28. package/dist/core/api/errors.js +4 -7
  29. package/dist/core/api/memoryCoreService.js +319 -252
  30. package/dist/core/api/sessionLifecycleService.js +8 -0
  31. package/dist/core/config/policyConfig.js +33 -6
  32. package/dist/core/injection/formatStartupInjection.js +53 -9
  33. package/dist/core/retrieve/recencyBands.js +4 -1
  34. package/dist/core/retrieve/retrieveMemories.js +10 -8
  35. package/dist/core/schema/migrations/005_team_provenance.sql +5 -0
  36. package/dist/core/schema/migrations/006_access_pattern_boosting.sql +5 -0
  37. package/dist/core/schema/migrations/008_fts5_search.sql +6 -2
  38. package/dist/core/schema/migrations/009_session_events_unique.sql +24 -0
  39. package/dist/core/schema/runMigrations.js +64 -2
  40. package/dist/core/storage/memoryRepo.js +164 -7
  41. package/dist/core/storage/memorySearchRepo.js +45 -7
  42. package/dist/core/storage/sessionEventsRepo.js +15 -2
  43. package/dist/core/summarize/cloudSummarizer.js +15 -2
  44. package/dist/core/summarize/redaction.js +45 -8
  45. package/package.json +2 -2
@@ -9,7 +9,7 @@ export class FallbackToolRegistrar {
9
9
  "WHEN TO CALL: At session start and mid-session when you need to retrieve context and the host does not support MCP resources. Do not call if the host supports MCP resources — use the sessionmem:// resource URI or retrieveMemories tool instead.\n\n" +
10
10
  "Parameter `query`: natural-language description of what context you need to recall (e.g. 'API design decisions', 'database schema choices').",
11
11
  inputShape: {
12
- query: z.string().describe("Natural-language description of what context you need to recall."),
12
+ query: z.string().min(1).max(1000).describe("Natural-language description of what context you need to recall."),
13
13
  },
14
14
  execute: async (args) => {
15
15
  const result = await context.service.call("retrieveMemories", {
@@ -30,7 +30,7 @@ export class FallbackToolRegistrar {
30
30
  name: "startup_inject_memories",
31
31
  description: "Fallback startup-injection for hosts that do not support MCP prompts. Call this once at the very start of a session instead of relying on the automatic sessionmem startup prompt when the host lacks prompt support. Injects the top relevant memories for the current project into the working context. No parameters required.\n\n" +
32
32
  "WHEN TO CALL: Once per session start, before any user task work begins, when the host does not surface MCP prompts automatically. Do not call if the host already surfaces the sessionmem startup prompt — calling both duplicates injected context.\n\n" +
33
- "Read-only; no side effects.",
33
+ "Note: access counts are incremented on retrieval.",
34
34
  inputShape: {},
35
35
  execute: async () => {
36
36
  const result = await context.service.call("retrieveMemories", {
@@ -5,7 +5,19 @@ export const SESSIONMEM_BLOCK_END = "<!-- sessionmem:end -->";
5
5
  const BLOCK_CONTENT = `
6
6
  ## sessionmem — Persistent Memory
7
7
 
8
- sessionmem is an MCP memory layer that persists context across sessions. Use its tools to store important decisions, facts, and context so they're available in future sessions without the user re-explaining.
8
+ sessionmem is an MCP memory layer that persists context across sessions. It is installed and active via the \`sessionmem\` MCP server. Use its tools to recall prior context and to store important decisions, facts, and context so they're available in future sessions without the user re-explaining. The user should never have to ask you to do this — it is part of how you work in this project.
9
+
10
+ ### Startup
11
+ On Claude Code, prior context is injected automatically at session start by the
12
+ sessionmem \`SessionStart\` hook — you do not need to fetch it yourself. Do NOT
13
+ call \`startup_inject_memories\` on Claude Code: the hook already provides the
14
+ injection, so calling the tool would duplicate the context (it is not even
15
+ registered on Claude Code for this reason). If you do NOT see a "Relevant prior
16
+ context" block at the start of the session AND the \`startup_inject_memories\`
17
+ tool is available (e.g. the hook is not installed, or you are on a host without
18
+ hook support), call it once before any task work, or call \`retrieveMemories\`
19
+ with the current task as the query. Never inject twice if context was already
20
+ provided.
9
21
 
10
22
  ### When to store memories (storeMemory)
11
23
  - User makes an architectural or design decision
@@ -14,29 +26,35 @@ sessionmem is an MCP memory layer that persists context across sessions. Use its
14
26
  - User states a preference about how they want things done
15
27
  - A warning or pitfall is discovered that future sessions should know about
16
28
 
17
- ### When to retrieve memories (retrieveMemories)
18
- - At the start of a session or task to check for relevant prior context
29
+ ### When to retrieve memories mid-session (retrieveMemories)
19
30
  - Before making architectural decisions (check if prior decisions exist)
20
31
  - When the user references something from a previous session
21
32
  - When working in an area of the codebase that may have stored warnings or decisions
22
33
 
34
+ ### At session end (RECOMMENDED — do this without being asked)
35
+ Before the session ends, persist what was accomplished so the next session starts
36
+ informed. Store a concise \`summary\` memory (importance 7) of the key outcomes, plus
37
+ any new decisions, facts, or warnings. Use \`batchStoreMemory\` to write several at
38
+ once. This is what makes context survive across sessions and saves tokens later.
39
+
23
40
  ### Memory kinds
24
41
  - \`decision\` — architectural or design choices (importance: 7-9)
25
42
  - \`fact\` — project constraints, conventions, patterns (importance: 5-7)
26
43
  - \`warning\` — pitfalls, gotchas, things that broke before (importance: 8-10)
27
44
  - \`preference\` — how the user likes things done (importance: 5-7)
28
- - \`summary\` — session summaries (auto-generated, importance: 3-5)
45
+ - \`summary\` — session summaries (importance: 7)
29
46
 
30
47
  ### Other tools
31
48
  - \`listMemories\` — browse all stored memories for this project
32
49
  - \`getMemory\` — fetch a specific memory by ID
33
50
  - \`forgetMemory\` — delete an outdated or incorrect memory
51
+ - \`batchStoreMemory\` — store multiple memories in one call (use at session end)
34
52
  - \`stats\` — check memory count and health
35
53
 
36
54
  ### Guidelines
37
55
  - Don't store trivial or easily re-derivable information
38
56
  - Don't retrieve memories every single turn — retrieve at task boundaries
39
- - Keep memory content concise (1-3 sentences)
57
+ - Keep memory content concise (1-3 sentences) and self-contained
40
58
  - Use appropriate importance scores (see kinds above)
41
59
  `;
42
60
  export function generateClaudeMdBlock() {
@@ -73,6 +91,32 @@ export function injectClaudeMdBlock(filePath) {
73
91
  return false;
74
92
  }
75
93
  }
94
+ /**
95
+ * Inject the sessionmem guidance block into an arbitrary host-guidance file
96
+ * (CLAUDE.md, AGENTS.md, Windsurf global_rules.md, a Cursor `.mdc` rule, …).
97
+ *
98
+ * The block is the same markdown for every host. The only host-specific concern
99
+ * is Cursor's `.mdc` rule format: a newly-created rule file needs an
100
+ * `alwaysApply: true` frontmatter header for Cursor to apply it on every
101
+ * request, so we seed that header before appending the block. Existing files
102
+ * (and all non-`.mdc` targets) are handled exactly like CLAUDE.md.
103
+ */
104
+ export function injectGuidanceBlock(filePath) {
105
+ try {
106
+ if (filePath.endsWith(".mdc") && !existsSync(filePath)) {
107
+ const dir = dirname(filePath);
108
+ if (!existsSync(dir)) {
109
+ mkdirSync(dir, { recursive: true });
110
+ }
111
+ writeFileSync(filePath, "---\ndescription: sessionmem persistent memory guidance\nalwaysApply: true\n---\n", "utf8");
112
+ }
113
+ }
114
+ catch {
115
+ // Best-effort frontmatter seeding; fall through to block injection which
116
+ // creates the file itself if the seeding failed.
117
+ }
118
+ return injectClaudeMdBlock(filePath);
119
+ }
76
120
  export function removeClaudeMdBlock(filePath) {
77
121
  try {
78
122
  if (!existsSync(filePath)) {
@@ -7,28 +7,58 @@ import { ClineAdapter } from "./ide/cline.js";
7
7
  import { GenericMCPAdapter } from "./generic.js";
8
8
  import { CodexAdapter } from "./global/codex.js";
9
9
  import { QCoderAdapter } from "./global/qcoder.js";
10
+ /**
11
+ * Canonical adapter names accepted by `--adapter <name>` / SESSIONMEM_ADAPTER.
12
+ * Kept in sync with {@link AdapterFactory.forName}; surfaced to the CLI for the
13
+ * install command's choices list.
14
+ */
15
+ export const ADAPTER_NAMES = [
16
+ "claude-code",
17
+ "cursor",
18
+ "windsurf",
19
+ "cline",
20
+ "codex",
21
+ "antigravity",
22
+ "qcoder",
23
+ "generic",
24
+ ];
10
25
  export class AdapterFactory {
11
26
  /**
12
27
  * Detect the current host environment and return the appropriate adapter.
28
+ *
29
+ * Detection keys are the REAL environment variables each host sets, verified
30
+ * against live shells:
31
+ * - Claude Code sets `CLAUDECODE=1`, `CLAUDE_CODE_ENTRYPOINT=cli`, and
32
+ * `CLAUDE_CODE_SESSION_ID=...` (note the `_ID` suffix). `TERM_PROGRAM` is
33
+ * the HOST terminal (e.g. `vscode`), never `"claude-code"`. The previous
34
+ * `CLAUDE_CODE_SESSION` / `TERM_PROGRAM === "claude-code"` check never
35
+ * matched, so the SessionStart-hook install path was never selected.
36
+ * - Antigravity sets `ANTIGRAVITY_APP_DATA_DIR` / `ANTIGRAVITY_SESSION_ID`
37
+ * (and leaks `ANTIGRAVITY_CLI_ALIAS`); checked first so its own CLI wins in
38
+ * its own shell.
13
39
  */
14
40
  static detectAdapter() {
15
41
  const env = process.env;
16
42
  if (env.ANTIGRAVITY_APP_DATA_DIR || env.ANTIGRAVITY_SESSION_ID) {
17
43
  return new AntigravityAdapter();
18
44
  }
19
- if (env.CLAUDE_CODE_SESSION || env.TERM_PROGRAM === "claude-code") {
45
+ if (env.CLAUDECODE === "1" ||
46
+ env.CLAUDE_CODE_ENTRYPOINT !== undefined ||
47
+ env.CLAUDE_CODE_SESSION_ID !== undefined) {
20
48
  return new ClaudeCodeAdapter();
21
49
  }
22
- if (env.TERM_PROGRAM === "Cursor" || env.CURSOR_APP_VERSION) {
50
+ if (env.CURSOR_AGENT !== undefined ||
51
+ env.CURSOR_CLI !== undefined ||
52
+ env.CURSOR_TRACE_ID !== undefined) {
23
53
  return new CursorAdapter();
24
54
  }
25
- if (env.TERM_PROGRAM === "Windsurf") {
26
- return new WindsurfAdapter();
27
- }
28
- if (env.CLINE_SESSION_ID) {
29
- return new ClineAdapter();
30
- }
31
- if (env.CODEX_SESSION_ID) {
55
+ // Windsurf is a VS Code fork with no unique env var; --adapter windsurf
56
+ // required. No reliable auto-detection branch.
57
+ // Cline is a VS Code extension; auto-detection is impossible. Use
58
+ // --adapter cline.
59
+ // Codex sets CODEX_HOME (and may expose OPENAI_CODEX). CODEX_SESSION_ID was
60
+ // unverified and never matched.
61
+ if (env.CODEX_HOME !== undefined || env.OPENAI_CODEX !== undefined) {
32
62
  return new CodexAdapter();
33
63
  }
34
64
  if (env.QCODER_SESSION) {
@@ -37,4 +67,33 @@ export class AdapterFactory {
37
67
  // Fallback to generic MCP if no specific host is detected
38
68
  return new GenericMCPAdapter();
39
69
  }
70
+ /**
71
+ * Resolve an adapter by its canonical name. Powers the `--adapter <name>`
72
+ * install flag and the `SESSIONMEM_ADAPTER` override so a user can force a
73
+ * host explicitly when auto-detection cannot (e.g. installing from a plain
74
+ * terminal that is not inside any host). Throws on an unknown name so the CLI
75
+ * can surface a clear error rather than silently falling back to generic.
76
+ */
77
+ static forName(name) {
78
+ switch (name) {
79
+ case "claude-code":
80
+ return new ClaudeCodeAdapter();
81
+ case "cursor":
82
+ return new CursorAdapter();
83
+ case "windsurf":
84
+ return new WindsurfAdapter();
85
+ case "cline":
86
+ return new ClineAdapter();
87
+ case "codex":
88
+ return new CodexAdapter();
89
+ case "antigravity":
90
+ return new AntigravityAdapter();
91
+ case "qcoder":
92
+ return new QCoderAdapter();
93
+ case "generic":
94
+ return new GenericMCPAdapter();
95
+ default:
96
+ throw new Error(`Unknown adapter "${name}". Valid adapters: ${ADAPTER_NAMES.join(", ")}.`);
97
+ }
98
+ }
40
99
  }
@@ -1,10 +1,13 @@
1
+ import { createRequire } from "module";
1
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { batchStoreMemoryRequestSchema, forgetMemoryRequestSchema, getMemoryRequestSchema, listMemoriesRequestSchema, resetAccessCountsRequestSchema, retrieveMemoriesRequestSchema, statsRequestSchema, storeMemoryRequestSchema, } from "../core/api/contracts.js";
4
+ import { batchStoreMemoryRequestSchema, forgetMemoryRequestSchema, getMemoryRequestSchema, handleSessionEndRequestSchema, ingestSessionEventsRequestSchema, listMemoriesRequestSchema, resetAccessCountsRequestSchema, retrieveMemoriesRequestSchema, statsRequestSchema, storeMemoryRequestSchema, summarizeSessionToMemoryRequestSchema, } from "../core/api/contracts.js";
4
5
  import { join } from "path";
5
6
  import { createCliContext } from "../cli/context.js";
6
7
  import { IDEInstaller } from "./ide/installer.js";
7
8
  import { FallbackToolRegistrar } from "./capabilities/fallbackTools.js";
9
+ import { countStaleEmbeddings } from "../core/storage/memoryRepo.js";
10
+ import { EMBEDDING_VERSION } from "../core/embed/embeddingVersion.js";
8
11
  /**
9
12
  * Diagnostic logging sink for the stdio server. CRITICAL: the MCP protocol
10
13
  * frames are written to STDOUT by StdioServerTransport, so anything this server
@@ -14,6 +17,11 @@ import { FallbackToolRegistrar } from "./capabilities/fallbackTools.js";
14
17
  function logDiagnostic(message) {
15
18
  process.stderr.write(`[sessionmem] ${message}\n`);
16
19
  }
20
+ // Read the package version dynamically so the MCP server's advertised version
21
+ // tracks package.json on every release. A hardcoded literal silently drifts on
22
+ // `npm version` bumps (postversion does not rewrite source), so mirror ping.ts.
23
+ const require = createRequire(import.meta.url);
24
+ const SERVER_VERSION = require("../../package.json").version;
17
25
  /**
18
26
  * Strip `projectId` from a request schema's shape so the tool input only asks
19
27
  * the client for the fields it should provide; the server injects projectId.
@@ -22,14 +30,39 @@ function shapeWithoutProjectId(shape) {
22
30
  const { projectId: _projectId, ...rest } = shape;
23
31
  return rest;
24
32
  }
33
+ /**
34
+ * Resolve a default sessionId for tools that require one but were invoked
35
+ * without it. Agents pass arbitrary/inconsistent sessionIds (or none), which
36
+ * breaks the per-session soft-limit counter and handleSessionEnd correlation.
37
+ * Preferring CLAUDE_CODE_SESSION_ID ties an agent's storeMemory/ingest calls to
38
+ * the same Claude Code session the SessionStart hook ran under. Callers can
39
+ * still override by supplying an explicit sessionId.
40
+ */
41
+ // Evaluated once per process lifecycle so every storeMemory/ingest call in one
42
+ // MCP server process shares the same fallback session when no env session id is
43
+ // available. Computing `session-${Date.now()}` per call would hand each call a
44
+ // different fake session, breaking the per-session soft-limit counter and
45
+ // handleSessionEnd correlation.
46
+ const PROCESS_SESSION_FALLBACK = `session-${Date.now()}`;
47
+ function resolveDefaultSessionId() {
48
+ return (process.env.CLAUDE_CODE_SESSION_ID ??
49
+ process.env.SESSION_ID ??
50
+ PROCESS_SESSION_FALLBACK);
51
+ }
52
+ function isMissing(value) {
53
+ return value === undefined || value === null || value === "";
54
+ }
25
55
  const TOOL_DEFINITIONS = [
26
56
  {
27
57
  method: "retrieveMemories",
28
58
  description: "Semantically search stored memories and return the top matches ranked by a weighted combination of relevance, recency, and importance. Read-only; no side effects.\n\n" +
29
59
  "WHEN TO CALL: (1) At the start of every session — pass the current task or file as the query to pre-load relevant context. (2) Mid-session whenever a new topic, file, or decision area arises that may have prior context. Do NOT call on every user turn.\n\n" +
30
60
  "WHEN NOT TO CALL: If you already retrieved memories for this topic this session. Use getMemory if you have a specific memoryId. Use listMemories only to audit the full store, not for context loading.\n\n" +
31
- "Returns up to `limit` results (default 20). `mode='auto'` is the standard startup path; `mode='on-demand'` signals an explicit mid-session lookup. `depth='deep'` runs a broader semantic sweep at higher latency — use when the topic is unfamiliar. Phrase `query` as what you need to recall, not what you are about to do.",
32
- annotations: { readOnlyHint: true, idempotentHint: true },
61
+ "Returns up to `limit` results (default 20). `mode='auto'` is the standard startup path; `mode='on-demand'` signals an explicit mid-session lookup. `depth='deep'` runs a broader semantic sweep at higher latency — use when the topic is unfamiliar. Phrase `query` as what you need to recall, not what you are about to do.\n\n" +
62
+ "NOTE: this tool updates access-pattern counters on the memories it returns (used to boost frequently-recalled memories in future ranking), so it is NOT side-effect-free despite being a lookup.",
63
+ // retrieveMemories mutates access_count on the rows it returns, so it is
64
+ // not read-only and the previous idempotentHint was inaccurate.
65
+ annotations: { readOnlyHint: false },
33
66
  inputShape: {
34
67
  query: retrieveMemoriesRequestSchema.shape.query.describe("Natural-language description of what you need to recall. Phrase as a topic or question (e.g. 'database connection settings', 'auth flow decisions') — not an action ('store info about...')."),
35
68
  limit: retrieveMemoriesRequestSchema.shape.limit.describe("Maximum number of memories to return. Integer 1-100, default 20. Increase for broad topic sweeps; keep at default for focused lookups."),
@@ -42,13 +75,14 @@ const TOOL_DEFINITIONS = [
42
75
  description: "Persist a single memory unit to the local SQLite store. Accepts decisions, facts, architectural choices, warnings, and session summaries. NOT idempotent — each call creates a new record even with identical content. Writes to disk immediately.\n\n" +
43
76
  "WHEN TO CALL: After any significant decision, discovery, or conclusion that should be available in a future session. Good candidates: technology choices, non-obvious constraints, bug root-causes, architectural decisions, key facts about the codebase.\n\n" +
44
77
  "WHEN NOT TO CALL: For trivial observations, transient state, or content that duplicates what was just retrieved. Do not store entire files or full conversation transcripts.\n\n" +
45
- "`kind` categories: 'decision', 'fact', 'summary', 'warning', 'architecture'. Write `content` to be self-contained — it must be useful without any surrounding conversation context. `importance` 1-10 (10 = most critical); directly affects retrieval ranking in future sessions.",
78
+ "`kind` categories: 'decision', 'fact', 'summary', 'warning', 'preference'. Write `content` to be self-contained — it must be useful without any surrounding conversation context. `importance` 1-10 (10 = most critical); directly affects retrieval ranking in future sessions.\n\n" +
79
+ "RESPONSE may include `warningCodes`: 'session_write_limit_warning' (this session has stored many memories — stop storing trivia and prefer batchStoreMemory) and 'redaction_partial_failure' (a redaction rule errored; the write still succeeded). Treat them as advisory signals, not errors.",
46
80
  annotations: { destructiveHint: false, idempotentHint: false },
47
81
  inputShape: {
48
82
  memoryId: storeMemoryRequestSchema.shape.memoryId.describe("Caller-supplied unique UUID for this memory (e.g. crypto.randomUUID()). Used for deduplication and for later retrieval by ID via getMemory."),
49
83
  sessionId: storeMemoryRequestSchema.shape.sessionId.describe("Identifier for the current session. Used to group memories by session for diagnostics. Use a consistent ID within a single session."),
50
84
  sourceAdapter: storeMemoryRequestSchema.shape.sourceAdapter.describe("Name of the adapter or host creating this memory (e.g. 'claude-code', 'cursor', 'generic'). Used for provenance tracking."),
51
- kind: storeMemoryRequestSchema.shape.kind.describe("Category of this memory. Recommended values: 'decision', 'fact', 'summary', 'warning', 'architecture'. Any non-empty string is valid."),
85
+ kind: storeMemoryRequestSchema.shape.kind.describe("Category of this memory. One of: 'decision', 'fact', 'warning', 'preference', 'summary'. These are the only recognized kinds — others are rejected."),
52
86
  content: storeMemoryRequestSchema.shape.content.describe("The memory text. Must be self-contained and specific — written so it is useful without surrounding conversation context. Avoid vague phrases like 'the user decided to...'."),
53
87
  importance: storeMemoryRequestSchema.shape.importance.describe("Integer 1-10 indicating criticality (10 = most important). Directly affects ranking in future retrieveMemories calls. Use 8-10 for decisions that must not be forgotten; 3-5 for useful but non-critical facts."),
54
88
  redactionEnabled: storeMemoryRequestSchema.shape.redactionEnabled.describe("If true, PII is stripped from content before storage. Omit to use the project-level redaction setting from config.json."),
@@ -103,20 +137,75 @@ const TOOL_DEFINITIONS = [
103
137
  description: "Persist multiple memory units in a single atomic SQLite transaction. Significantly faster than calling storeMemory repeatedly for session-end writes of 10-20 memories.\n\n" +
104
138
  "WHEN TO CALL: At session end or whenever you have multiple memories to store at once. Reduces overhead from per-insert fsync by wrapping all writes in one transaction.\n\n" +
105
139
  "WHEN NOT TO CALL: For a single memory — use storeMemory instead. For imports from external files — use importMemories.\n\n" +
106
- "Each item in the `memories` array follows the same schema as storeMemory (memoryId, sessionId, sourceAdapter, kind, content, importance). Invalid items are reported individually; valid items are still stored atomically.",
140
+ "Each item in the `memories` array follows the same schema as storeMemory (memoryId, sessionId, sourceAdapter, kind, content, importance). Invalid items are reported individually; valid items are still stored atomically.\n\n" +
141
+ "NOTE: the per-item `memory` echoed back in the response has its `content` truncated to 2000 characters (a batch can return many rows). The full body is still persisted — fetch it with getMemory if you need the complete text. (Single-record storeMemory echoes the full content.)",
107
142
  annotations: { destructiveHint: false, idempotentHint: false },
108
143
  inputShape: {
109
- memories: batchStoreMemoryRequestSchema.shape.memories.describe("Array of memory objects to store. Each must include: memoryId (unique UUID), sessionId, sourceAdapter, kind, content (self-contained text), importance (1-10). Minimum 1 item."),
144
+ memories: batchStoreMemoryRequestSchema.shape.memories.describe("Array of memory objects to store. Each must include: memoryId (unique UUID), sessionId, sourceAdapter, kind, content (self-contained text), importance (1-10). Minimum 1 item, maximum 100.\n\n" +
145
+ "Per-item results may include `warningCodes` (e.g. 'session_write_limit_warning', 'redaction_partial_failure') — advisory signals, not failures."),
110
146
  },
111
147
  },
148
+ {
149
+ method: "ingestSessionEvents",
150
+ description: "Push raw session events (tool calls, decisions, file edits, user turns) to sessionmem so they can be summarized at session end and counted toward token-savings analytics. Writes immediately, in a single transaction. Re-ingesting the same (sessionId, eventIndex) is a no-op, so retries are safe.\n\n" +
151
+ "WHEN TO CALL: Periodically during a session (e.g. at task boundaries) to record what happened, OR in one batch shortly before the session ends. This is what powers automatic session-end summarization and `sessionmem savings`.\n\n" +
152
+ "WHEN NOT TO CALL: For durable, individually-important facts/decisions — use storeMemory for those. Session events are transient raw material for summarization, not first-class memories.\n\n" +
153
+ "Each event needs: id (unique), eventIndex (monotonic 0-based order within the session), eventType (e.g. 'tool_use', 'user_message'), payloadJson (a JSON string of the event body).\n\n" +
154
+ "LIMITS: at most 500 events per call. For more than 500 events, call this tool multiple times in chunks — re-ingestion of already-stored events is safe (idempotent via the (project, session, eventIndex) UNIQUE index), so overlapping chunks never double-count.",
155
+ annotations: { destructiveHint: false, idempotentHint: true },
156
+ inputShape: shapeWithoutProjectId(ingestSessionEventsRequestSchema.shape),
157
+ },
158
+ {
159
+ method: "summarizeSessionToMemory",
160
+ description: "Store an agent-authored session summary as a durable 'summary' memory in one call. Upserts on memoryId, so calling it again with the same memoryId replaces the prior summary rather than duplicating it.\n\n" +
161
+ "WHEN TO CALL: At session end when you have already written a concise summary of what was accomplished and want to persist it directly (the simpler alternative to handleSessionEnd's automatic summarization).\n\n" +
162
+ "WHEN NOT TO CALL: When you want sessionmem to generate the summary from ingested session events — use handleSessionEnd for that. For non-summary facts/decisions use storeMemory.\n\n" +
163
+ "Provide: memoryId (stable id for this session's summary), sessionId, sourceAdapter, summary (the text), importance (1-10; 7 is typical for summaries).",
164
+ annotations: { destructiveHint: false, idempotentHint: true },
165
+ inputShape: shapeWithoutProjectId(summarizeSessionToMemoryRequestSchema.shape),
166
+ },
167
+ {
168
+ method: "handleSessionEnd",
169
+ description: "Run the full session-end pipeline: auto-summarize the session's ingested events into a durable memory (when enough events exist) and apply a light retention prune of stale memories. Idempotent on the summary memory (upsert by sessionId).\n\n" +
170
+ "WHEN TO CALL: Once, at the very end of a session, after ingesting session events via ingestSessionEvents. Lets sessionmem generate and store the session summary for you.\n\n" +
171
+ "WHEN NOT TO CALL: Mid-session, or when you have already written your own summary (use summarizeSessionToMemory instead). On Claude Code this also runs automatically via the installed SessionEnd hook, so calling it explicitly is usually unnecessary there.\n\n" +
172
+ "Provide sessionId and sourceAdapter. `memoryId` (optional) pins the summary's id; omit to derive `${sessionId}-summary`. `config` (optional) tunes autoSummarize / minimumEventThreshold / cloud summarization; omit for sensible local-only defaults.\n\n" +
173
+ "RESPONSE `status` is one of: 'stored', 'skipped_threshold' (too few events), 'skipped_disabled', 'failed'. `warningCodes` may carry cloud/local fallback signals.",
174
+ annotations: { destructiveHint: false, idempotentHint: true },
175
+ inputShape: shapeWithoutProjectId(handleSessionEndRequestSchema.shape),
176
+ },
112
177
  ];
113
178
  export class GenericMCPAdapter {
114
179
  name = "Generic MCP";
180
+ /**
181
+ * When true, the `startup_inject_memories` fallback tool is NOT registered.
182
+ * Hosts that already inject prior context deterministically at session start
183
+ * (e.g. Claude Code via its SessionStart hook) set this so the agent cannot
184
+ * double-inject memories — calling the tool on top of the hook would duplicate
185
+ * the injected content and double-count access_count increments.
186
+ */
187
+ suppressStartupInjectionTool = false;
188
+ // The stdio server (startMcpServer) registers TOOLS only — it never calls
189
+ // server.registerPrompt() or server.registerResource(). Advertising prompt or
190
+ // resource support here would make FallbackToolRegistrar SKIP the
191
+ // startup_inject_memories / fetch_memories tools (it only registers them when
192
+ // the matching capability is absent), leaving the agent with no automatic
193
+ // startup-injection path. Capabilities therefore reflect reality: tools only.
194
+ // Host subclasses inherit this and MUST NOT re-enable prompts/resources unless
195
+ // they actually register them on the server.
115
196
  capabilities = {
116
- supportsPrompts: true,
117
- supportsResources: true,
197
+ supportsPrompts: false,
198
+ supportsResources: false,
118
199
  supportsTools: true,
119
200
  };
201
+ /**
202
+ * Default agent-guidance target for an undetected/generic MCP host: a
203
+ * project-local AGENTS.md (the emerging cross-tool standard). Host subclasses
204
+ * override this with the file their agent actually reads at startup.
205
+ */
206
+ guidanceTargets() {
207
+ return [join(process.cwd(), "AGENTS.md")];
208
+ }
120
209
  /**
121
210
  * Fallback for hosts that aren't specifically detected: register sessionmem
122
211
  * in a project-local `.mcp.json` (the de-facto generic MCP config format).
@@ -146,9 +235,23 @@ export class GenericMCPAdapter {
146
235
  // SESSIONMEM_PROJECT_ID) used for isolated integration tests.
147
236
  const ctx = createCliContext();
148
237
  const { service, projectId } = ctx;
238
+ // Surface stale embeddings (e.g. after an EMBEDDING_VERSION bump) so the
239
+ // operator knows semantic ranking has degraded to importance+recency for
240
+ // those rows until `sessionmem re-embed` is run. Best-effort; to stderr only
241
+ // so it can never corrupt the stdio protocol stream.
242
+ try {
243
+ const stale = countStaleEmbeddings(ctx.db, projectId, EMBEDDING_VERSION);
244
+ if (stale > 0) {
245
+ logDiagnostic(`${stale} memory(ies) have stale embeddings (version != ${EMBEDDING_VERSION}). ` +
246
+ `Run \`sessionmem re-embed\` to restore full semantic ranking.`);
247
+ }
248
+ }
249
+ catch {
250
+ // Never block server startup on a diagnostic query.
251
+ }
149
252
  const server = new McpServer({
150
253
  name: "sessionmem",
151
- version: "1.0.5",
254
+ version: SERVER_VERSION,
152
255
  });
153
256
  for (const def of TOOL_DEFINITIONS) {
154
257
  server.registerTool(def.method, {
@@ -157,7 +260,26 @@ export class GenericMCPAdapter {
157
260
  ...(def.annotations ? { annotations: def.annotations } : {}),
158
261
  }, async (args) => {
159
262
  // Inject the server-resolved projectId; clients never set it.
160
- const request = { ...args, projectId };
263
+ const enriched = { ...args, projectId };
264
+ // Default a missing sessionId for tools that require one so the
265
+ // per-session counters and session-end correlation stay consistent
266
+ // even when the agent omits (or cannot supply) a stable sessionId.
267
+ if ("sessionId" in def.inputShape && isMissing(enriched.sessionId)) {
268
+ enriched.sessionId = resolveDefaultSessionId();
269
+ }
270
+ // batchStoreMemory carries sessionId per-item, not at the top level —
271
+ // backfill each item that omitted it with a single shared default.
272
+ if (def.method === "batchStoreMemory" && Array.isArray(enriched.memories)) {
273
+ let sharedDefault;
274
+ enriched.memories = enriched.memories.map((entry) => {
275
+ if (entry && typeof entry === "object" && isMissing(entry.sessionId)) {
276
+ sharedDefault ??= resolveDefaultSessionId();
277
+ return { ...entry, sessionId: sharedDefault };
278
+ }
279
+ return entry;
280
+ });
281
+ }
282
+ const request = enriched;
161
283
  const result = await service.call(def.method, request);
162
284
  if (result.ok === false) {
163
285
  return {
@@ -186,13 +308,26 @@ export class GenericMCPAdapter {
186
308
  const fallbackTools = FallbackToolRegistrar.getFallbackTools(this.capabilities, {
187
309
  service,
188
310
  projectId,
189
- });
311
+ }).filter((fallback) => !(this.suppressStartupInjectionTool && fallback.name === "startup_inject_memories"));
190
312
  for (const fallback of fallbackTools) {
191
313
  server.registerTool(fallback.name, { description: fallback.description, inputSchema: fallback.inputShape }, async (args) => {
192
314
  const result = await fallback.execute(args);
193
315
  return { content: [{ type: "text", text: result }] };
194
316
  });
195
317
  }
318
+ // Graceful shutdown: close the DB on SIGINT/SIGTERM so SQLite checkpoints
319
+ // the WAL and releases its file handles cleanly before the process exits.
320
+ const shutdown = () => {
321
+ try {
322
+ ctx.db.close();
323
+ }
324
+ catch {
325
+ // best-effort; never block exit on a close failure
326
+ }
327
+ process.exit(0);
328
+ };
329
+ process.on("SIGINT", shutdown);
330
+ process.on("SIGTERM", shutdown);
196
331
  logDiagnostic(`Starting Generic MCP server over stdio (project: ${projectId})`);
197
332
  await server.connect(new StdioServerTransport());
198
333
  }
@@ -2,21 +2,28 @@ import { join } from "path";
2
2
  import { homedir } from "os";
3
3
  import { GenericMCPAdapter } from "../generic.js";
4
4
  import { IDEInstaller } from "../ide/installer.js";
5
+ // ⚠️ UNVERIFIED: The MCP config path for "Antigravity" has not been confirmed
6
+ // against official documentation. `~/.gemini/config/mcp_config.json` is the
7
+ // real path for Gemini CLI, and may apply if Antigravity is a Gemini-family
8
+ // tool. Verify before relying on this adapter for production installs.
9
+ // The `{ "mcpServers": { ... } }` structure IDEInstaller writes is consistent
10
+ // with Gemini CLI's documented MCP config format.
11
+ const ANTIGRAVITY_MCP_CONFIG = [".gemini", "config", "mcp_config.json"];
5
12
  export class AntigravityAdapter extends GenericMCPAdapter {
6
13
  name = "Antigravity";
7
- capabilities = {
8
- supportsPrompts: true,
9
- supportsResources: true,
10
- supportsTools: true,
11
- };
14
+ // Capabilities inherited from GenericMCPAdapter (tools only).
15
+ /** Antigravity reads AGENTS.md-style guidance from its global config dir. */
16
+ guidanceTargets() {
17
+ return [join(homedir(), ".antigravity", "AGENTS.md")];
18
+ }
12
19
  async install() {
13
- const configPath = join(homedir(), ".antigravity", "config.json");
20
+ const configPath = join(homedir(), ...ANTIGRAVITY_MCP_CONFIG);
14
21
  return IDEInstaller.injectMcpConfig(configPath, "sessionmem", "sessionmem", [
15
22
  "run",
16
23
  ]);
17
24
  }
18
25
  async uninstall() {
19
- const configPath = join(homedir(), ".antigravity", "config.json");
26
+ const configPath = join(homedir(), ...ANTIGRAVITY_MCP_CONFIG);
20
27
  return IDEInstaller.removeMcpConfig(configPath, "sessionmem");
21
28
  }
22
29
  }
@@ -1,22 +1,58 @@
1
1
  import { join } from "path";
2
2
  import { homedir } from "os";
3
3
  import { GenericMCPAdapter } from "../generic.js";
4
- import { IDEInstaller } from "../ide/installer.js";
4
+ import { IDEInstaller, SESSIONMEM_HOOK_COMMAND, SESSIONMEM_SESSION_END_HOOK_COMMAND, } from "../ide/installer.js";
5
5
  export class ClaudeCodeAdapter extends GenericMCPAdapter {
6
6
  name = "Claude Code";
7
- capabilities = {
8
- supportsPrompts: true,
9
- supportsResources: true,
10
- supportsTools: true,
11
- };
7
+ // Capabilities inherited from GenericMCPAdapter (tools only) so the
8
+ // fetch_memories fallback tool is registered.
9
+ // The installed SessionStart hook already injects prior context at the start
10
+ // of every session, so suppress the startup_inject_memories tool: an agent
11
+ // calling it on top of the hook would double-inject content and double-count
12
+ // access_count increments.
13
+ suppressStartupInjectionTool = true;
14
+ /**
15
+ * Claude Code reads ~/.claude/CLAUDE.md as global memory on every session
16
+ * across all projects — the right place for the sessionmem guidance block.
17
+ */
18
+ guidanceTargets() {
19
+ return [join(homedir(), ".claude", "CLAUDE.md")];
20
+ }
21
+ settingsPath() {
22
+ return join(homedir(), ".claude", "settings.json");
23
+ }
12
24
  async install() {
13
25
  const configPath = join(homedir(), ".claude.json");
14
- return IDEInstaller.injectMcpConfig(configPath, "sessionmem", "sessionmem", [
15
- "run",
16
- ]);
26
+ // On Windows, the globally-installed `sessionmem` bin is a `.cmd` shim.
27
+ // Claude Code spawns MCP servers WITHOUT a shell, and `.cmd` shims require
28
+ // cmd.exe to execute, so a bare `command: "sessionmem"` fails to start.
29
+ // Route through `cmd /c sessionmem run` so the shim is resolved correctly.
30
+ const isWindows = process.platform === "win32";
31
+ const command = isWindows ? "cmd" : "sessionmem";
32
+ const args = isWindows ? ["/c", "sessionmem", "run"] : ["run"];
33
+ const mcpOk = await IDEInstaller.injectMcpConfig(configPath, "sessionmem", command, args);
34
+ // Register a SessionStart hook so prior memories are injected automatically
35
+ // at the start of EVERY Claude Code session. This is the deterministic
36
+ // auto-injection path the advisory `startup_inject_memories` tool could
37
+ // never guarantee: a hook runs unconditionally and Claude Code adds its
38
+ // output to the session context without the agent choosing to call a tool.
39
+ // A hook-wiring failure now propagates so the install command reports a
40
+ // failure if the SessionStart/SessionEnd hooks could not be written.
41
+ const startHookOk = await IDEInstaller.injectClaudeHook(this.settingsPath(), SESSIONMEM_HOOK_COMMAND);
42
+ // Register a SessionEnd hook so the session-end pipeline (light retention
43
+ // prune + auto-summarization of ingested session events) runs once when a
44
+ // session ends — the deterministic write-side counterpart to the
45
+ // SessionStart read-side hook.
46
+ const endHookOk = await IDEInstaller.injectClaudeHook(this.settingsPath(), SESSIONMEM_SESSION_END_HOOK_COMMAND, "SessionEnd");
47
+ return mcpOk && startHookOk && endHookOk;
17
48
  }
18
49
  async uninstall() {
19
50
  const configPath = join(homedir(), ".claude.json");
20
- return IDEInstaller.removeMcpConfig(configPath, "sessionmem");
51
+ const mcpOk = await IDEInstaller.removeMcpConfig(configPath, "sessionmem");
52
+ // Propagate hook-removal results so a failure to clean up either hook is
53
+ // reported as an uninstall failure rather than silently ignored.
54
+ const startOk = await IDEInstaller.removeClaudeHook(this.settingsPath(), SESSIONMEM_HOOK_COMMAND);
55
+ const endOk = await IDEInstaller.removeClaudeHook(this.settingsPath(), SESSIONMEM_SESSION_END_HOOK_COMMAND, "SessionEnd");
56
+ return mcpOk && startOk && endOk;
21
57
  }
22
58
  }