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
@@ -4,6 +4,7 @@ import { fileURLToPath } from "url";
4
4
  import { mkdirSync } from "fs";
5
5
  import { openDb } from "../core/storage/db.js";
6
6
  import { createMemoryCoreService } from "../core/api/memoryCoreService.js";
7
+ import { deriveProjectId } from "./projectId.js";
7
8
  /**
8
9
  * Resolve the local OS username once per invocation and sanitize it to a
9
10
  * filename-safe token so it can be embedded in exports/filenames without path
@@ -28,29 +29,27 @@ function safeUserInfoName() {
28
29
  return "";
29
30
  }
30
31
  }
31
- function deriveProjectId() {
32
- // Env override is a test-injection seam (mirrors Plan 01's override pattern):
33
- // it lets a spawned binary target a deterministic projectId without touching
34
- // the real ~/.sessionmem. No privilege boundary is crossed the CLI runs as
35
- // the invoking user and the env var is operator-controlled.
36
- const envProjectId = process.env.SESSIONMEM_PROJECT_ID;
37
- if (envProjectId && envProjectId.trim() !== "")
38
- return envProjectId;
39
- const cwd = process.cwd();
40
- const parts = cwd.replace(/\\/g, "/").split("/");
41
- const raw = parts[parts.length - 1] || "default";
42
- // Sanitize to a filename-safe token (mirrors localUsername) so it can be
43
- // embedded in shared-path join()s without path traversal.
44
- const sanitized = raw.replace(/[^A-Za-z0-9._-]/g, "_");
45
- return sanitized === "" || sanitized === "." || sanitized === ".."
46
- ? "default"
47
- : sanitized;
32
+ /**
33
+ * Expand a leading `~/` (or bare `~`) to the user's home directory. Shells do
34
+ * this before a value reaches the process, but env vars set programmatically or
35
+ * in config files are passed through verbatim, so we expand here too.
36
+ */
37
+ export function expandTilde(p) {
38
+ if (p === "~")
39
+ return homedir();
40
+ if (p.startsWith("~/") || p.startsWith("~\\")) {
41
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
42
+ // `homedir()` is the fixed base; `p` is SESSIONMEM_DB_PATH, a local CLI
43
+ // config the operator sets for their own filesystem not external user input.
44
+ return join(homedir(), p.slice(2));
45
+ }
46
+ return p;
48
47
  }
49
48
  function defaultDbPath(dir) {
50
49
  // Env override seam (see deriveProjectId). Defaults to ~/.sessionmem/memories.db.
51
50
  const envDbPath = process.env.SESSIONMEM_DB_PATH;
52
51
  if (envDbPath && envDbPath.trim() !== "")
53
- return envDbPath;
52
+ return expandTilde(envDbPath);
54
53
  // `dir` is the fixed ~/.sessionmem dir computed above, not user input.
55
54
  // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
56
55
  return join(dir, "memories.db");
package/dist/cli/index.js CHANGED
@@ -2,6 +2,8 @@
2
2
  import { Command } from "commander";
3
3
  import { createRequire } from "node:module";
4
4
  import { runMcpServer } from "./commands/run.js";
5
+ import { sessionStartCommand } from "./commands/sessionStart.js";
6
+ import { sessionEndCommand } from "./commands/sessionEnd.js";
5
7
  import { installCommand } from "./commands/install.js";
6
8
  import { uninstallCommand } from "./commands/uninstall.js";
7
9
  import { pingCommand } from "./commands/ping.js";
@@ -30,19 +32,29 @@ program
30
32
  .command("run")
31
33
  .description("Start the sessionmem MCP server")
32
34
  .action(runMcpServer);
35
+ program
36
+ .command("session-start")
37
+ .description("Print prior memory context for the current project as Claude Code SessionStart hook output (invoked automatically by the hook installed during `sessionmem install`)")
38
+ .action(() => sessionStartCommand());
39
+ program
40
+ .command("session-end")
41
+ .description("Run the session-end pipeline (auto-summarize ingested session events + light retention prune) for the current project (invoked automatically by the SessionEnd hook installed during `sessionmem install`)")
42
+ .action(() => sessionEndCommand());
33
43
  program
34
44
  .command("install")
35
45
  .description("Install sessionmem into the current MCP host")
36
- .action(installCommand);
46
+ .option("--adapter <name>", "Force a host adapter instead of auto-detecting (claude-code, cursor, windsurf, cline, codex, antigravity, qcoder, generic)")
47
+ .action((options) => installCommand(options));
37
48
  program
38
49
  .command("uninstall")
39
50
  .description("Remove sessionmem from the current MCP host")
40
51
  .option("--purge", "Also delete the local memories database")
41
- .action(uninstallCommand);
52
+ .option("--adapter <name>", "Force a host adapter instead of auto-detecting (claude-code, cursor, windsurf, cline, codex, antigravity, qcoder, generic)")
53
+ .action((options) => uninstallCommand(options));
42
54
  program
43
55
  .command("ping")
44
- .description("Check sessionmem server connectivity")
45
- .action(pingCommand);
56
+ .description("Check sessionmem version and local database reachability")
57
+ .action(() => pingCommand());
46
58
  // NOTE: commander always appends its own Command instance as the final
47
59
  // argument to .action() callbacks. The search/list/show/forget/export/import/
48
60
  // stats commands all declare a trailing `ctx?: CliContext` parameter (the
@@ -0,0 +1,69 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * Sanitize a raw token to the filename-safe character set used across
4
+ * sessionmem (mirrors localUsername). Any character outside [A-Za-z0-9._-]
5
+ * becomes "_".
6
+ */
7
+ function sanitizeToken(raw) {
8
+ return raw.replace(/[^A-Za-z0-9._-]/g, "_");
9
+ }
10
+ /**
11
+ * Derive a stable, collision-resistant project id from an absolute working
12
+ * directory.
13
+ *
14
+ * Format: `<basename>-<hash8>` where `hash8` is the first 8 hex chars of the
15
+ * SHA-256 of the FULL absolute path. The human-readable basename keeps the id
16
+ * debuggable; the path hash guarantees two different projects that happen to
17
+ * share a basename (e.g. `~/a/api` and `~/b/api`) get distinct memory buckets
18
+ * instead of silently colliding.
19
+ *
20
+ * The basename-only scheme this replaces partitioned memory purely by the last
21
+ * path segment, so same-named projects in different directories shared one
22
+ * bucket with zero warning.
23
+ */
24
+ export function projectIdFromCwd(cwd) {
25
+ // On Windows the same directory can surface with either a lowercase or
26
+ // uppercase drive letter (e.g. `c:\proj` vs `C:\proj`). Lowercase the drive
27
+ // letter before normalizing slashes so both spellings hash to the same id.
28
+ let normalized = process.platform === "win32"
29
+ ? cwd.replace(/^[A-Za-z]:[\\/]/, (m) => m.toLowerCase()).replace(/\\/g, "/")
30
+ : cwd.replace(/\\/g, "/");
31
+ // Normalize UNC path host and share (//server/share/...) case-insensitively
32
+ // so `\\Server\Share\proj` and `\\server\share\proj` hash to the same id.
33
+ normalized = normalized.replace(/^(\/\/[^/]+\/[^/]+)/, (m) => m.toLowerCase());
34
+ const parts = normalized.split("/");
35
+ const rawBase = parts[parts.length - 1] || "default";
36
+ const sanitizedBase = sanitizeToken(rawBase);
37
+ const base = sanitizedBase === "" || sanitizedBase === "." || sanitizedBase === ".."
38
+ ? "default"
39
+ : sanitizedBase;
40
+ // Hash the normalized absolute path so the id is stable across runs from the
41
+ // same directory but unique per directory.
42
+ const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 8);
43
+ return `${base}-${hash}`;
44
+ }
45
+ /**
46
+ * Resolve the effective project id: the `SESSIONMEM_PROJECT_ID` env override
47
+ * (operator-controlled test/migration seam; also lets a user pin a legacy
48
+ * basename-only id) wins, otherwise derive from `process.cwd()`.
49
+ */
50
+ export function deriveProjectId() {
51
+ const envProjectId = process.env.SESSIONMEM_PROJECT_ID;
52
+ if (envProjectId && envProjectId.trim() !== "") {
53
+ // Sanitize the operator-supplied id to prevent path traversal: the id is
54
+ // later joined into filesystem paths (e.g. sync.ts `join(sharedPath,
55
+ // projectId)`), so strip path separators and `..` traversal sequences and
56
+ // bound the length. Fall through to the derived id only if nothing usable
57
+ // remains.
58
+ const sanitized = envProjectId
59
+ .replace(/[/\\]/g, "")
60
+ .replace(/\.\./g, "")
61
+ .slice(0, 128);
62
+ // Guard against empty or "." (current-dir) values, which would be unsafe or
63
+ // meaningless when joined into filesystem paths; fall through to derived id.
64
+ if (sanitized && sanitized !== ".") {
65
+ return sanitized;
66
+ }
67
+ }
68
+ return projectIdFromCwd(process.cwd());
69
+ }
@@ -1,8 +1,52 @@
1
1
  import { z } from "zod";
2
+ /**
3
+ * Canonical memory-kind taxonomy. This is the single source of truth — the
4
+ * storeMemory tool description, CLAUDE.md guidance block, and
5
+ * formatStartupInjection's KIND_ORDER all use exactly this set. The legacy
6
+ * `architecture` kind is mapped onto `decision` (it was never recognized by the
7
+ * formatter and scored at the bottom).
8
+ */
9
+ export const MEMORY_KINDS = [
10
+ "decision",
11
+ "fact",
12
+ "warning",
13
+ "preference",
14
+ "summary",
15
+ ];
16
+ /**
17
+ * Validate `kind` against {@link MEMORY_KINDS}, transparently mapping the legacy
18
+ * `architecture` value to `decision` so older callers/exports keep working.
19
+ */
20
+ const memoryKindSchema = z.preprocess((value) => (value === "architecture" ? "decision" : value), z.enum(MEMORY_KINDS));
21
+ /** Upper bound on stored memory content length (characters). */
22
+ export const MAX_CONTENT_LENGTH = 10000;
23
+ /** Maximum number of items accepted in a single batchStoreMemory call. */
24
+ export const MAX_BATCH_SIZE = 100;
25
+ /** Maximum number of records accepted in a single import/pull call. */
26
+ export const MAX_IMPORT_SIZE = 1000;
27
+ /**
28
+ * Maximum number of session events accepted in a single ingestSessionEvents
29
+ * call. Each event's payloadJson is capped at 50000 chars, so this bounds a
30
+ * single ingest request at ~25MB worst-case (500 × 50KB) rather than leaving
31
+ * the array unbounded — an agent looping ingestion could otherwise build a
32
+ * multi-MB JSON-RPC message that OOMs or times out the MCP stdio transport.
33
+ * Callers that need to ingest more than this should chunk across calls (the
34
+ * (project_id, session_id, event_index) UNIQUE index makes re-ingestion a
35
+ * no-op, so chunks never double-count).
36
+ */
37
+ export const MAX_INGEST_EVENTS = 500;
38
+ /**
39
+ * Default cap on the number of memories returned by listMemories when the caller
40
+ * omits an explicit `limit`. Bounds the MCP stdio response size for large
41
+ * projects; `total` still reports the full row count. Exported so the service
42
+ * layer and tests share a single source of truth — a change here is caught by
43
+ * the list-memories limit test rather than silently drifting.
44
+ */
45
+ export const LIST_MEMORIES_DEFAULT_LIMIT = 200;
2
46
  export const memorySchema = z.object({
3
47
  id: z.string().min(1),
4
48
  projectId: z.string().min(1),
5
- sessionId: z.string().min(1),
49
+ sessionId: z.string().min(1).max(200),
6
50
  sourceAdapter: z.string().min(1),
7
51
  kind: z.string().min(1),
8
52
  content: z.string().min(1),
@@ -18,23 +62,40 @@ export const memorySchema = z.object({
18
62
  updatedAt: z.string().min(1),
19
63
  });
20
64
  export const ingestSessionEventSchema = z.object({
21
- id: z.string().min(1),
65
+ id: z.string().min(1).max(200),
22
66
  eventIndex: z.number().int().nonnegative(),
23
- eventType: z.string().min(1),
24
- payloadJson: z.string().min(1),
67
+ eventType: z.string().min(1).max(100),
68
+ payloadJson: z
69
+ .string()
70
+ .min(1)
71
+ .max(50000)
72
+ .refine((v) => {
73
+ try {
74
+ JSON.parse(v);
75
+ return true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ }, { message: "must be valid JSON" }),
25
81
  createdAt: z.string().min(1).optional(),
26
82
  });
27
83
  export const ingestSessionEventsRequestSchema = z.object({
28
84
  projectId: z.string().min(1),
29
- sessionId: z.string().min(1),
30
- events: z.array(ingestSessionEventSchema).min(1),
85
+ sessionId: z.string().min(1).max(200),
86
+ events: z.array(ingestSessionEventSchema).min(1).max(MAX_INGEST_EVENTS),
31
87
  });
32
88
  export const summarizeSessionToMemoryRequestSchema = z.object({
33
- memoryId: z.string().min(1),
89
+ memoryId: z.string().min(1).max(200),
34
90
  projectId: z.string().min(1),
35
- sessionId: z.string().min(1),
36
- sourceAdapter: z.string().min(1),
37
- summary: z.string().min(1),
91
+ sessionId: z.string().min(1).max(200),
92
+ sourceAdapter: z
93
+ .string()
94
+ .min(1)
95
+ .max(100)
96
+ // eslint-disable-next-line no-control-regex -- intentionally rejects control chars
97
+ .regex(/^[^\n\r\x00-\x08\x0e-\x1f\x7f]*$/, "sourceAdapter must not contain control characters"),
98
+ summary: z.string().min(1).max(50000),
38
99
  importance: z.number().int().min(1).max(10),
39
100
  });
40
101
  export const factModeSchema = z.enum([
@@ -45,7 +106,7 @@ export const factModeSchema = z.enum([
45
106
  export const handleSessionEndConfigSchema = z.object({
46
107
  autoSummarize: z.boolean().default(true),
47
108
  minimumEventThreshold: z.number().int().min(1).max(100).default(3),
48
- summaryTokenCap: z.number().int().min(1).default(300),
109
+ summaryTokenCap: z.number().int().min(1).max(200000).default(300),
49
110
  // No `.default()`: omission must be distinguishable from an explicit value so
50
111
  // the service layer can fall back to the policy-config redactionEnabled
51
112
  // setting (override > config.json > default precedence).
@@ -56,18 +117,28 @@ export const handleSessionEndConfigSchema = z.object({
56
117
  });
57
118
  export const handleSessionEndRequestSchema = z.object({
58
119
  projectId: z.string().min(1),
59
- sessionId: z.string().min(1),
60
- sourceAdapter: z.string().min(1),
61
- memoryId: z.string().min(1).optional(),
120
+ sessionId: z.string().min(1).max(200),
121
+ sourceAdapter: z
122
+ .string()
123
+ .min(1)
124
+ .max(100)
125
+ // eslint-disable-next-line no-control-regex -- intentionally rejects control chars
126
+ .regex(/^[^\n\r\x00-\x08\x0e-\x1f\x7f]*$/, "sourceAdapter must not contain control characters"),
127
+ memoryId: z.string().min(1).max(200).optional(),
62
128
  config: handleSessionEndConfigSchema.default(() => handleSessionEndConfigSchema.parse({})),
63
129
  });
64
130
  export const storeMemoryRequestSchema = z.object({
65
- memoryId: z.string().min(1),
131
+ memoryId: z.string().min(1).max(200),
66
132
  projectId: z.string().min(1),
67
- sessionId: z.string().min(1),
68
- sourceAdapter: z.string().min(1),
69
- kind: z.string().min(1),
70
- content: z.string().min(1),
133
+ sessionId: z.string().min(1).max(200),
134
+ sourceAdapter: z
135
+ .string()
136
+ .min(1)
137
+ .max(100)
138
+ // eslint-disable-next-line no-control-regex -- intentionally rejects control chars
139
+ .regex(/^[^\n\r\x00-\x08\x0e-\x1f\x7f]*$/, "sourceAdapter must not contain control characters"),
140
+ kind: memoryKindSchema,
141
+ content: z.string().min(1).max(MAX_CONTENT_LENGTH),
71
142
  importance: z.number().int().min(1).max(10),
72
143
  // No `.default()`: omission must be distinguishable from an explicit value so
73
144
  // the service layer can fall back to the policy-config redactionEnabled
@@ -76,32 +147,43 @@ export const storeMemoryRequestSchema = z.object({
76
147
  });
77
148
  export const retrieveMemoriesRequestSchema = z.object({
78
149
  projectId: z.string().min(1),
79
- query: z.string().min(1),
150
+ query: z.string().min(1).max(1000),
80
151
  limit: z.number().int().min(1).max(100).default(20),
81
152
  mode: z.enum(["auto", "on-demand"]).default("auto"),
82
153
  depth: z.enum(["default", "deep"]).default("default"),
83
154
  });
84
155
  export const listMemoriesRequestSchema = z.object({
85
156
  projectId: z.string().min(1),
157
+ // Maximum number of memories to return. Bounds the response size so a large
158
+ // project (10k+ memories) cannot produce a multi-MB payload that OOMs or
159
+ // times out the MCP stdio client. `total` always reports the full count, so a
160
+ // returned array shorter than `total` signals truncation. Defaults to 200 in
161
+ // the service layer when omitted.
162
+ limit: z.number().int().positive().max(1000).optional(),
86
163
  });
87
164
  export const getMemoryRequestSchema = z.object({
88
165
  projectId: z.string().min(1),
89
- memoryId: z.string().min(1),
166
+ memoryId: z.string().min(1).max(200),
90
167
  });
91
168
  export const forgetMemoryRequestSchema = z.object({
92
169
  projectId: z.string().min(1),
93
- memoryId: z.string().min(1),
170
+ memoryId: z.string().min(1).max(200),
94
171
  });
95
172
  export const exportMemoriesRequestSchema = z.object({
96
173
  projectId: z.string().min(1),
97
174
  });
98
175
  export const importMemoryRecordSchema = z.object({
99
- id: z.string().min(1),
176
+ id: z.string().min(1).max(200),
100
177
  projectId: z.string().min(1),
101
- sessionId: z.string().min(1),
102
- sourceAdapter: z.string().min(1),
103
- kind: z.string().min(1),
104
- content: z.string().min(1),
178
+ sessionId: z.string().min(1).max(200),
179
+ sourceAdapter: z
180
+ .string()
181
+ .min(1)
182
+ .max(100)
183
+ // eslint-disable-next-line no-control-regex -- intentionally rejects control chars
184
+ .regex(/^[^\n\r\x00-\x08\x0e-\x1f\x7f]*$/, "sourceAdapter must not contain control characters"),
185
+ kind: memoryKindSchema,
186
+ content: z.string().min(1).max(MAX_CONTENT_LENGTH),
105
187
  importance: z.number().int().min(1).max(10),
106
188
  // OPTIONAL for backward-compat with exports predating team provenance (A3):
107
189
  // older exports lack author/originProjectId, so the service stamps the local
@@ -109,10 +191,32 @@ export const importMemoryRecordSchema = z.object({
109
191
  // `originProjectId: null` (and `author: ""`) for locally-authored rows — a
110
192
  // team sync round-trips those exported snapshots verbatim, so the schema must
111
193
  // accept the null the DTO carries rather than skip-and-warn every local row.
112
- author: z.string().nullable().optional(),
113
- originProjectId: z.string().nullable().optional(),
114
- createdAt: z.string().min(1).optional(),
115
- updatedAt: z.string().min(1).optional(),
194
+ // `author` is rendered directly into the per-session startup injection
195
+ // context, so it is bounded and stripped of control characters (newlines/CR/
196
+ // C0/DEL) that could break out of the injection block or smuggle in
197
+ // prompt-injection payloads.
198
+ author: z
199
+ .string()
200
+ .max(200)
201
+ .regex(
202
+ // eslint-disable-next-line no-control-regex -- intentionally rejects control chars
203
+ /^[^\n\r\x00-\x08\x0e-\x1f\x7f]*$/, "author must not contain control characters")
204
+ .nullable()
205
+ .optional(),
206
+ // Bounded like the other id-bearing fields (memoryId/author max 200): an
207
+ // import file or team-pull payload is externally-supplied, so an unbounded
208
+ // originProjectId would let a single record carry a multi-MB string. It is
209
+ // stored only (never rendered into the startup injection), so unlike `author`
210
+ // it needs no control-character stripping — just a length cap.
211
+ originProjectId: z.string().max(200).nullable().optional(),
212
+ createdAt: z
213
+ .string()
214
+ .regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/, "must be UTC ISO timestamp")
215
+ .optional(),
216
+ updatedAt: z
217
+ .string()
218
+ .regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/, "must be UTC ISO timestamp")
219
+ .optional(),
116
220
  });
117
221
  export const importMemoriesRequestSchema = z.object({
118
222
  projectId: z.string().min(1),
@@ -120,7 +224,7 @@ export const importMemoriesRequestSchema = z.object({
120
224
  // the service layer can fall back to the policy-config redactionEnabled
121
225
  // setting (override > config.json > default precedence).
122
226
  redactionEnabled: z.boolean().optional(),
123
- memories: z.array(importMemoryRecordSchema),
227
+ memories: z.array(importMemoryRecordSchema).max(MAX_IMPORT_SIZE),
124
228
  });
125
229
  // Team pull. Mirrors importMemoriesRequestSchema — the pull
126
230
  // path reuses importMemoryRecordSchema verbatim (carrying author/originProjectId
@@ -130,7 +234,7 @@ export const importMemoriesRequestSchema = z.object({
130
234
  export const pullMemoriesRequestSchema = z.object({
131
235
  projectId: z.string().min(1),
132
236
  redactionEnabled: z.boolean().optional(),
133
- memories: z.array(importMemoryRecordSchema),
237
+ memories: z.array(importMemoryRecordSchema).max(MAX_IMPORT_SIZE),
134
238
  });
135
239
  export const statsRequestSchema = z.object({
136
240
  projectId: z.string().min(1),
@@ -229,7 +333,7 @@ export const ingestSessionEventsResponseSchema = z.object({
229
333
  });
230
334
  export const summarizeSessionToMemoryResponseSchema = z.object({
231
335
  ok: z.literal(true),
232
- memoryId: z.string().min(1),
336
+ memoryId: z.string().min(1).max(200),
233
337
  });
234
338
  export const handleSessionEndResponseSchema = z.object({
235
339
  ok: z.literal(true),
@@ -238,7 +342,7 @@ export const handleSessionEndResponseSchema = z.object({
238
342
  warningCodes: z.array(z.string()),
239
343
  warningMessages: z.array(z.string()),
240
344
  failureRecordId: z.string().min(1).optional(),
241
- memoryId: z.string().min(1).optional(),
345
+ memoryId: z.string().min(1).max(200).optional(),
242
346
  });
243
347
  export const importMemoriesResponseSchema = z.object({
244
348
  ok: z.literal(true),
@@ -247,6 +351,10 @@ export const importMemoriesResponseSchema = z.object({
247
351
  // different project's memory. These are never upserted, preventing
248
352
  // cross-project overwrite/reassignment via ON CONFLICT(id).
249
353
  skippedCrossProject: z.number().int().nonnegative().default(0),
354
+ // Count of records skipped because their `id` already exists in THIS project.
355
+ // Imports never overwrite an existing same-project memory; only brand-new ids
356
+ // are upserted.
357
+ skippedExisting: z.number().int().nonnegative().default(0),
250
358
  warningCodes: z.array(z.string()),
251
359
  });
252
360
  // Team pull response: distinguishes brand-new inserts from updates so the
@@ -260,20 +368,25 @@ export const pullMemoriesResponseSchema = z.object({
260
368
  warningCodes: z.array(z.string()),
261
369
  });
262
370
  export const batchStoreMemoryItemSchema = z.object({
263
- memoryId: z.string().min(1),
264
- sessionId: z.string().min(1),
265
- sourceAdapter: z.string().min(1),
266
- kind: z.string().min(1),
267
- content: z.string().min(1),
371
+ memoryId: z.string().min(1).max(200),
372
+ sessionId: z.string().min(1).max(200),
373
+ sourceAdapter: z
374
+ .string()
375
+ .min(1)
376
+ .max(100)
377
+ // eslint-disable-next-line no-control-regex -- intentionally rejects control chars
378
+ .regex(/^[^\n\r\x00-\x08\x0e-\x1f\x7f]*$/, "sourceAdapter must not contain control characters"),
379
+ kind: memoryKindSchema,
380
+ content: z.string().min(1).max(MAX_CONTENT_LENGTH),
268
381
  importance: z.number().int().min(1).max(10),
269
382
  redactionEnabled: z.boolean().optional(),
270
383
  });
271
384
  export const batchStoreMemoryRequestSchema = z.object({
272
385
  projectId: z.string().min(1),
273
- memories: z.array(batchStoreMemoryItemSchema).min(1),
386
+ memories: z.array(batchStoreMemoryItemSchema).min(1).max(MAX_BATCH_SIZE),
274
387
  });
275
388
  export const batchStoreMemoryResultSchema = z.object({
276
- memoryId: z.string().min(1),
389
+ memoryId: z.string().min(1).max(200),
277
390
  ok: z.boolean(),
278
391
  memory: memorySchema.optional(),
279
392
  warningCodes: z.array(z.string()).optional(),
@@ -16,14 +16,11 @@ export function toErrorEnvelope(error) {
16
16
  details: error.details,
17
17
  };
18
18
  }
19
- if (error instanceof Error) {
20
- return {
21
- code: "INTERNAL",
22
- message: error.message,
23
- };
24
- }
19
+ // Don't surface internal .message (may contain fs paths) to MCP client.
20
+ // Log the real message to stderr; return a static string to the caller.
21
+ process.stderr.write(`[sessionmem] internal error: ${error instanceof Error ? error.message : String(error)}\n`);
25
22
  return {
26
23
  code: "INTERNAL",
27
- message: "Unexpected error",
24
+ message: "Internal error",
28
25
  };
29
26
  }