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.
- package/dist/adapters/capabilities/fallbackTools.js +2 -2
- package/dist/adapters/claudeMdInjector.js +49 -5
- package/dist/adapters/factory.js +68 -9
- package/dist/adapters/generic.js +147 -12
- package/dist/adapters/global/antigravity.js +14 -7
- package/dist/adapters/global/claudeCode.js +46 -10
- package/dist/adapters/global/codex.js +73 -13
- package/dist/adapters/global/qcoder.js +18 -5
- package/dist/adapters/ide/cline.js +56 -9
- package/dist/adapters/ide/cursor.js +15 -13
- package/dist/adapters/ide/installer.js +201 -8
- package/dist/adapters/ide/windsurf.js +14 -13
- package/dist/cli/commands/config.js +10 -1
- package/dist/cli/commands/import.js +6 -1
- package/dist/cli/commands/install.js +57 -16
- package/dist/cli/commands/ping.js +42 -8
- package/dist/cli/commands/reEmbed.js +4 -3
- package/dist/cli/commands/run.js +7 -17
- package/dist/cli/commands/savings.js +33 -17
- package/dist/cli/commands/sessionEnd.js +124 -0
- package/dist/cli/commands/sessionStart.js +52 -0
- package/dist/cli/commands/sync.js +39 -9
- package/dist/cli/commands/uninstall.js +35 -9
- package/dist/cli/context.js +17 -18
- package/dist/cli/index.js +16 -4
- package/dist/cli/projectId.js +69 -0
- package/dist/core/api/contracts.js +155 -42
- package/dist/core/api/errors.js +4 -7
- package/dist/core/api/memoryCoreService.js +319 -252
- package/dist/core/api/sessionLifecycleService.js +8 -0
- package/dist/core/config/policyConfig.js +33 -6
- package/dist/core/injection/formatStartupInjection.js +53 -9
- package/dist/core/retrieve/recencyBands.js +4 -1
- package/dist/core/retrieve/retrieveMemories.js +10 -8
- package/dist/core/schema/migrations/005_team_provenance.sql +5 -0
- package/dist/core/schema/migrations/006_access_pattern_boosting.sql +5 -0
- package/dist/core/schema/migrations/008_fts5_search.sql +6 -2
- package/dist/core/schema/migrations/009_session_events_unique.sql +24 -0
- package/dist/core/schema/runMigrations.js +64 -2
- package/dist/core/storage/memoryRepo.js +164 -7
- package/dist/core/storage/memorySearchRepo.js +45 -7
- package/dist/core/storage/sessionEventsRepo.js +15 -2
- package/dist/core/summarize/cloudSummarizer.js +15 -2
- package/dist/core/summarize/redaction.js +45 -8
- package/package.json +2 -2
package/dist/cli/context.js
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
|
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
|
|
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
|
|
37
|
-
|
|
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
|
|
61
|
-
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
266
|
-
|
|
267
|
-
|
|
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(),
|
package/dist/core/api/errors.js
CHANGED
|
@@ -16,14 +16,11 @@ export function toErrorEnvelope(error) {
|
|
|
16
16
|
details: error.details,
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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: "
|
|
24
|
+
message: "Internal error",
|
|
28
25
|
};
|
|
29
26
|
}
|